get-shit-done-cc 1.42.1 → 1.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,6 +144,8 @@ GSD is built for frictionless automation. Skip-permissions is how it's intended
144
144
 
145
145
  Install only the skills you need with `--profile=core` (six core-loop skills), `--profile=standard` (core + phase management), or the default full install. Profiles compose: `--profile=core,audit`. `--minimal` is an alias for `--profile=core`. See **[docs/USER-GUIDE.md](docs/USER-GUIDE.md)** for the full walkthrough, non-interactive install flags for all 15 runtimes, and permissions configuration. See [ADR-0011](docs/adr/0011-skill-surface-budget-module.md) for the profile model and runtime surface control.
146
146
 
147
+ Current release highlights are in [docs/RELEASE-v1.42.1.md](docs/RELEASE-v1.42.1.md): package legitimacy checks, safer installer migrations, runtime surface control, custom ship PR sections, reviewer defaults, fallow structural review, and quota-aware execution recovery.
148
+
147
149
  ---
148
150
 
149
151
  ## Commands
@@ -196,6 +198,8 @@ Key dials:
196
198
 
197
199
  Optional structural review: set `code_quality.fallow.enabled` to `true` to add a fallow pre-pass to `/gsd-code-review`. GSD writes `.planning/phases/<phase>/FALLOW.json` and surfaces a `Structural Findings (fallow)` section in `REVIEW.md`. Install with `npm install -D fallow@^2.70.0` (or system-wide via `cargo install fallow`; note that the Rust binary's JSON schema must match the documented v2.70+ contract — older versions may produce silent zero-finding output).
198
200
 
201
+ Package legitimacy checks are built into the research, planning, and execution path: recommended dependencies get audited, unverified packages require a human checkpoint, and failed installs stop instead of trying similarly named alternatives.
202
+
199
203
  For the full configuration reference — all settings, git branching strategies, per-runtime model overrides, workstream config inheritance, agent skills injection — see **[docs/CONFIGURATION.md](docs/CONFIGURATION.md)**.
200
204
 
201
205
  ---
@@ -726,6 +726,31 @@ function normalizePhaseName(phase) {
726
726
  return str;
727
727
  }
728
728
 
729
+ /**
730
+ * Render a regex source fragment matching a phase number against ROADMAP/STATE
731
+ * prose regardless of zero-padding on either side. Skills pass the resolved
732
+ * padded form (`02.7`), but human-authored ROADMAP prose is conventionally
733
+ * un-padded (`### Phase 2.7:`); a naive `escapeRegex(phaseNum)` fragment never
734
+ * matches when the two diverge. Strips leading zeros from the integer part
735
+ * before re-emitting with a `0*` prefix, so the fragment matches both `2.7`
736
+ * and `02.7` (and `002.7`).
737
+ *
738
+ * Falls back to `escapeRegex(phaseNum)` for non-numeric IDs (custom project
739
+ * codes like `PROJ-42`) so callers can substitute it unconditionally.
740
+ *
741
+ * See #3537 — wired into every ROADMAP-prose regex builder.
742
+ */
743
+ function phaseMarkdownRegexSource(phaseNum) {
744
+ const stripped = String(phaseNum).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
745
+ const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
746
+ if (!match) return escapeRegex(phaseNum);
747
+
748
+ const integer = match[1].replace(/^0+/, '') || '0';
749
+ const letter = match[2] ? escapeRegex(match[2]) : '';
750
+ const decimal = match[3] ? escapeRegex(match[3]) : '';
751
+ return `0*${escapeRegex(integer)}${letter}${decimal}`;
752
+ }
753
+
729
754
  function comparePhaseNum(a, b) {
730
755
  // Strip optional project_code prefix before comparing (e.g., 'CK-01-name' → '01-name')
731
756
  const sa = String(a).replace(/^[A-Z]{1,6}-/, '');
@@ -1071,19 +1096,13 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
1071
1096
  const roadmapRaw = platformReadSync(roadmapPath);
1072
1097
  if (roadmapRaw === null) throw new Error('missing');
1073
1098
  const content = extractCurrentMilestone(roadmapRaw, cwd);
1074
- // Strip leading zeros from purely numeric phase numbers so "03" matches "Phase 3:"
1075
- // in canonical ROADMAP headings. Non-numeric IDs (e.g. "PROJ-42") are kept as-is.
1076
- const normalized = /^\d+$/.test(String(phaseNum))
1077
- ? String(phaseNum).replace(/^0+(?=\d)/, '')
1078
- : String(phaseNum);
1079
- const escapedPhase = escapeRegex(normalized);
1080
- // Match both numeric and custom (Phase PROJ-42:) headers.
1081
- // For purely numeric phases allow optional leading zeros so both "Phase 1:" and
1082
- // "Phase 01:" are matched regardless of whether the ROADMAP uses padded numbers.
1083
- const isNumeric = /^\d+$/.test(String(phaseNum));
1084
- const phasePattern = isNumeric
1085
- ? new RegExp(`#{2,4}\\s*Phase\\s+0*${escapedPhase}:\\s*([^\\n]+)`, 'i')
1086
- : new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
1099
+ // #3537: route through canonical padding-tolerant fragment. The prior
1100
+ // hand-rolled `isNumeric` branch only stripped padding on integer-only
1101
+ // ids and missed decimal padding (`02.7` against `Phase 2.7:` headings).
1102
+ const phasePattern = new RegExp(
1103
+ `#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`,
1104
+ 'i'
1105
+ );
1087
1106
  const headerMatch = content.match(phasePattern);
1088
1107
  if (!headerMatch) return null;
1089
1108
 
@@ -1880,6 +1899,7 @@ module.exports = {
1880
1899
  isGitIgnored,
1881
1900
  escapeRegex,
1882
1901
  normalizePhaseName,
1902
+ phaseMarkdownRegexSource,
1883
1903
  comparePhaseNum,
1884
1904
  searchPhaseInDir,
1885
1905
  extractPhaseToken,
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
8
8
  const { platformWriteSync, platformReadSync, platformEnsureDir } = require('./shell-command-projection.cjs');
9
9
  const { planningDir, withPlanningLock } = require('./planning-workspace.cjs');
10
10
  const { extractFrontmatter } = require('./frontmatter.cjs');
@@ -170,8 +170,10 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
170
170
  if (fs.existsSync(roadmapPath)) {
171
171
  try {
172
172
  const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
173
+ // #3537: padding-tolerant on both sides — `0*${escapeRegex(...)}`
174
+ // tolerated extra padding but not missing.
173
175
  const phasePattern = new RegExp(
174
- `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalized)}\\.(\\d+)\\s*:`, 'gi'
176
+ `#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalized)}\\.(\\d+)\\s*:`, 'gi'
175
177
  );
176
178
  let pm;
177
179
  while ((pm = phasePattern.exec(roadmapContent)) !== null) {
@@ -691,13 +693,14 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
691
693
  const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
692
694
  const content = extractCurrentMilestone(rawContent, cwd);
693
695
 
694
- // Normalize input then strip leading zeros for flexible matching
696
+ // Normalize input then route through canonical padding-tolerant fragment
697
+ // (#3537). The prior hand-rolled `0*${unpadded}` worked for the integer
698
+ // base but duplicated logic — funnel it through the shared helper.
695
699
  const normalizedAfter = normalizePhaseName(afterPhase);
696
- const unpadded = normalizedAfter.replace(/^0+/, '');
697
- const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
698
- const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
700
+ const afterPhaseEscaped = phaseMarkdownRegexSource(normalizedAfter);
701
+ const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
699
702
  if (!targetPattern.test(content)) {
700
- const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
703
+ const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${afterPhaseEscaped}:`, 'i');
701
704
  if (checklistPattern.test(content)) {
702
705
  error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
703
706
  }
@@ -719,9 +722,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
719
722
  }
720
723
  } catch { /* intentionally empty */ }
721
724
 
722
- // Also scan ROADMAP.md content (already loaded) for decimal entries
725
+ // Also scan ROADMAP.md content (already loaded) for decimal entries.
726
+ // #3537: padding-tolerant fragment so un-padded `Phase 2.7:` is found
727
+ // when caller passes the padded base `02`.
723
728
  const rmPhasePattern = new RegExp(
724
- `#{2,4}\\s*Phase\\s+0*${escapeRegex(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
729
+ `#{2,4}\\s*Phase\\s+${phaseMarkdownRegexSource(normalizedBase)}\\.(\\d+)\\s*:`, 'gi'
725
730
  );
726
731
  let rmMatch;
727
732
  while ((rmMatch = rmPhasePattern.exec(rawContent)) !== null) {
@@ -745,7 +750,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
745
750
  const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${_decimalPhase} to break down)\n`;
746
751
 
747
752
  // Insert after the target phase section
748
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
753
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
749
754
  const headerMatch = rawContent.match(headerPattern);
750
755
  if (!headerMatch) {
751
756
  error(`Could not find Phase ${afterPhase} header`);
@@ -1030,14 +1035,16 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1030
1035
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
1031
1036
 
1032
1037
  // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
1038
+ // #3537: padding-tolerant fragment so the caller-resolved padded id
1039
+ // matches un-padded ROADMAP prose.
1040
+ const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
1033
1041
  const checkboxPattern = new RegExp(
1034
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
1042
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
1035
1043
  'i'
1036
1044
  );
1037
1045
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
1038
1046
 
1039
1047
  // Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
1040
- const phaseEscaped = escapeRegex(phaseNum);
1041
1048
  const tableRowPattern = new RegExp(
1042
1049
  `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
1043
1050
  'im'
@@ -1093,8 +1100,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1093
1100
  // Update REQUIREMENTS.md traceability for this phase's requirements
1094
1101
  const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
1095
1102
  if (fs.existsSync(reqPath)) {
1096
- // Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
1097
- const phaseEsc = escapeRegex(phaseNum);
1103
+ // Extract the current phase section from roadmap (scoped to avoid cross-phase matching).
1104
+ // #3537: padding-tolerant fragment so an un-padded `Phase 2.7:` heading
1105
+ // is found when caller resolved to padded `02.7`.
1106
+ const phaseEsc = phaseMarkdownRegexSource(phaseNum);
1098
1107
  const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
1099
1108
  const phaseSectionMatch = currentMilestoneRoadmap.match(
1100
1109
  new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = require('./core.cjs');
8
8
  const { platformWriteSync } = require('./shell-command-projection.cjs');
9
9
  const { planningPaths, withPlanningLock } = require('./planning-workspace.cjs');
10
10
  const scanPhasePlans = require('./plan-scan.cjs');
@@ -52,16 +52,8 @@ function countPhasePlansAndSummaries(phaseDir) {
52
52
  };
53
53
  }
54
54
 
55
- function phaseMarkdownRegexSource(phaseNum) {
56
- const stripped = String(phaseNum).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
57
- const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
58
- if (!match) return escapeRegex(phaseNum);
59
-
60
- const integer = match[1].replace(/^0+/, '') || '0';
61
- const letter = match[2] ? escapeRegex(match[2]) : '';
62
- const decimal = match[3] ? escapeRegex(match[3]) : '';
63
- return `0*${escapeRegex(integer)}${letter}${decimal}`;
64
- }
55
+ // `phaseMarkdownRegexSource` moved to core.cjs (#3537) so phase.cjs and
56
+ // core.cjs itself can consume it without circular deps. Imported above.
65
57
 
66
58
  /**
67
59
  * Search for a phase header (and its section) within the given content string.
@@ -147,8 +139,9 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
147
139
  const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
148
140
  const milestoneContent = extractCurrentMilestone(rawContent, cwd);
149
141
 
150
- // Escape special regex chars in phase number, handle decimal
151
- const escapedPhase = escapeRegex(phaseNum);
142
+ // #3537: padding-tolerant fragment so callers passing `02.7` still match
143
+ // un-padded ROADMAP prose (`### Phase 2.7:`).
144
+ const escapedPhase = phaseMarkdownRegexSource(phaseNum);
152
145
 
153
146
  // Search the current milestone slice first, then fall back to full roadmap.
154
147
  // A malformed_roadmap result (checklist-only) from the milestone should not
@@ -248,8 +241,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
248
241
  }
249
242
  } catch { /* intentionally empty */ }
250
243
 
251
- // Check ROADMAP checkbox status
252
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
244
+ // Check ROADMAP checkbox status.
245
+ // #3537: padding-tolerant fragment the heading discovered above may use
246
+ // a different padding than the summary-bullet checkbox below it (mixed
247
+ // padding inside one ROADMAP is legal and seen in real projects).
248
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseMarkdownRegexSource(phaseNum)}[:\\s]`, 'i');
253
249
  const checkboxMatch = content.match(checkboxPattern);
254
250
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
255
251
 
@@ -511,8 +507,10 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
511
507
  withPlanningLock(cwd, () => {
512
508
  let content = fs.readFileSync(roadmapPath, 'utf-8');
513
509
 
514
- // Find the phase section
515
- const phaseEscaped = escapeRegex(phaseNum);
510
+ // Find the phase section.
511
+ // #3537: padding-tolerant fragment so the caller's resolved padded id
512
+ // matches un-padded ROADMAP headings.
513
+ const phaseEscaped = phaseMarkdownRegexSource(phaseNum);
516
514
  const phaseHeaderPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}:[^\\n]*)`, 'i');
517
515
  const phaseMatch = content.match(phaseHeaderPattern);
518
516
  if (!phaseMatch) return;
@@ -1,122 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * STATE.md Document Module
4
+ * STATE.md Document Module — CJS adapter.
5
5
  *
6
- * Pure transforms for STATE.md text. This module does not read the filesystem
7
- * and does not own persistence or locking.
6
+ * The implementation is generated from sdk/src/query/state-document.ts and
7
+ * lives in state-document.generated.cjs. This file is a thin re-export so
8
+ * that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
9
+ * and tests) can continue to require('./state-document') unchanged.
8
10
  */
9
11
 
10
- function escapeRegex(str) {
11
- return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
- }
13
-
14
- function stateExtractField(content, fieldName) {
15
- const escaped = escapeRegex(fieldName);
16
- const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
17
- const boldMatch = content.match(boldPattern);
18
- if (boldMatch) return boldMatch[1].trim();
19
- const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
20
- const plainMatch = content.match(plainPattern);
21
- return plainMatch ? plainMatch[1].trim() : null;
22
- }
23
-
24
- function stateReplaceField(content, fieldName, newValue) {
25
- const escaped = escapeRegex(fieldName);
26
- const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
27
- if (boldPattern.test(content)) {
28
- return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
29
- }
30
- const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
31
- if (plainPattern.test(content)) {
32
- return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
33
- }
34
- return null;
35
- }
36
-
37
- function stateReplaceFieldWithFallback(content, primary, fallback, value) {
38
- let result = stateReplaceField(content, primary, value);
39
- if (result) return result;
40
- if (fallback) {
41
- result = stateReplaceField(content, fallback, value);
42
- if (result) return result;
43
- }
44
- return content;
45
- }
46
-
47
- function normalizeStateStatus(status, pausedAt) {
48
- let normalizedStatus = status || 'unknown';
49
- const statusLower = (status || '').toLowerCase();
50
- if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
51
- normalizedStatus = 'paused';
52
- } else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
53
- normalizedStatus = 'executing';
54
- } else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
55
- normalizedStatus = 'planning';
56
- } else if (statusLower.includes('discussing')) {
57
- normalizedStatus = 'discussing';
58
- } else if (statusLower.includes('verif')) {
59
- normalizedStatus = 'verifying';
60
- } else if (statusLower.includes('complete') || statusLower.includes('done')) {
61
- normalizedStatus = 'completed';
62
- } else if (statusLower.includes('ready to execute')) {
63
- normalizedStatus = 'executing';
64
- }
65
- return normalizedStatus;
66
- }
67
-
68
- function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
69
- const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
70
- const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
71
-
72
- if (!hasPlanData && !hasPhaseData) return null;
73
-
74
- const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
75
- const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
76
-
77
- return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
78
- }
79
-
80
- function toFiniteNumber(value) {
81
- const number = Number(value);
82
- return Number.isFinite(number) ? number : null;
83
- }
84
-
85
- function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
86
- const existing = toFiniteNumber(existingProgress[key]);
87
- const derived = toFiniteNumber(derivedProgress[key]);
88
- return existing !== null && derived !== null && existing > derived;
89
- }
90
-
91
- function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
92
- if (!existingProgress || typeof existingProgress !== 'object') return false;
93
- if (!derivedProgress || typeof derivedProgress !== 'object') return false;
94
-
95
- return (
96
- existingProgressExceedsDerived(existingProgress, derivedProgress, 'total_phases') ||
97
- existingProgressExceedsDerived(existingProgress, derivedProgress, 'completed_phases') ||
98
- existingProgressExceedsDerived(existingProgress, derivedProgress, 'total_plans') ||
99
- existingProgressExceedsDerived(existingProgress, derivedProgress, 'completed_plans')
100
- );
101
- }
102
-
103
- function normalizeProgressNumbers(progress) {
104
- if (!progress || typeof progress !== 'object') return progress;
105
-
106
- const normalized = { ...progress };
107
- for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
108
- const number = toFiniteNumber(normalized[key]);
109
- if (number !== null) normalized[key] = number;
110
- }
111
- return normalized;
112
- }
113
-
114
- module.exports = {
115
- computeProgressPercent,
116
- normalizeProgressNumbers,
117
- normalizeStateStatus,
118
- shouldPreserveExistingProgress,
119
- stateExtractField,
120
- stateReplaceField,
121
- stateReplaceFieldWithFallback,
122
- };
12
+ module.exports = require('./state-document.generated.cjs');
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GENERATED FILE — DO NOT EDIT.
5
+ *
6
+ * Source: sdk/src/query/state-document.ts
7
+ * Regenerate: cd sdk && npm run gen:state-document
8
+ *
9
+ * STATE.md Document Module — pure transforms for STATE.md text.
10
+ * This module does not read the filesystem and does not own persistence or locking.
11
+ */
12
+
13
+ // Internal helpers
14
+ function escapeRegex(str) {
15
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }
17
+
18
+ function toFiniteNumber(value) {
19
+ const number = Number(value);
20
+ return Number.isFinite(number) ? number : null;
21
+ }
22
+
23
+ function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
24
+ const existing = toFiniteNumber(existingProgress[key]);
25
+ const derived = toFiniteNumber(derivedProgress[key]);
26
+ return existing !== null && derived !== null && existing > derived;
27
+ }
28
+
29
+ function stateExtractField(content, fieldName) {
30
+ const escaped = escapeRegex(fieldName);
31
+ const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
32
+ const boldMatch = content.match(boldPattern);
33
+ if (boldMatch)
34
+ return boldMatch[1].trim();
35
+ const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
36
+ const plainMatch = content.match(plainPattern);
37
+ return plainMatch ? plainMatch[1].trim() : null;
38
+ }
39
+
40
+ function stateReplaceField(content, fieldName, newValue) {
41
+ const escaped = escapeRegex(fieldName);
42
+ const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
43
+ if (boldPattern.test(content)) {
44
+ return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
45
+ }
46
+ const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
47
+ if (plainPattern.test(content)) {
48
+ return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
54
+ let result = stateReplaceField(content, primary, value);
55
+ if (result)
56
+ return result;
57
+ if (fallback) {
58
+ result = stateReplaceField(content, fallback, value);
59
+ if (result)
60
+ return result;
61
+ }
62
+ return content;
63
+ }
64
+
65
+ function normalizeStateStatus(status, pausedAt) {
66
+ let normalizedStatus = status || 'unknown';
67
+ const statusLower = (status || '').toLowerCase();
68
+ if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
69
+ normalizedStatus = 'paused';
70
+ }
71
+ else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
72
+ normalizedStatus = 'executing';
73
+ }
74
+ else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
75
+ normalizedStatus = 'planning';
76
+ }
77
+ else if (statusLower.includes('discussing')) {
78
+ normalizedStatus = 'discussing';
79
+ }
80
+ else if (statusLower.includes('verif')) {
81
+ normalizedStatus = 'verifying';
82
+ }
83
+ else if (statusLower.includes('complete') || statusLower.includes('done')) {
84
+ normalizedStatus = 'completed';
85
+ }
86
+ else if (statusLower.includes('ready to execute')) {
87
+ normalizedStatus = 'executing';
88
+ }
89
+ return normalizedStatus;
90
+ }
91
+
92
+ function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
93
+ const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
94
+ const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
95
+ if (!hasPlanData && !hasPhaseData)
96
+ return null;
97
+ const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
98
+ const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
99
+ return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
100
+ }
101
+
102
+ function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
103
+ if (!existingProgress || typeof existingProgress !== 'object')
104
+ return false;
105
+ if (!derivedProgress || typeof derivedProgress !== 'object')
106
+ return false;
107
+ const existing = existingProgress;
108
+ const derived = derivedProgress;
109
+ return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
110
+ existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
111
+ existingProgressExceedsDerived(existing, derived, 'total_plans') ||
112
+ existingProgressExceedsDerived(existing, derived, 'completed_plans'));
113
+ }
114
+
115
+ function normalizeProgressNumbers(progress) {
116
+ if (!progress || typeof progress !== 'object')
117
+ return progress;
118
+ const normalized = { ...progress };
119
+ for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
120
+ const number = toFiniteNumber(normalized[key]);
121
+ if (number !== null)
122
+ normalized[key] = number;
123
+ }
124
+ return normalized;
125
+ }
126
+
127
+ module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-shit-done-cc",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
5
5
  "bin": {
6
6
  "get-shit-done-cc": "bin/install.js",
@@ -62,6 +62,7 @@
62
62
  "build:hooks": "node scripts/build-hooks.js",
63
63
  "build:sdk": "cd sdk && npm ci && npm run build",
64
64
  "check:alias-drift": "cd sdk && npm run check:alias-drift",
65
+ "check:state-document-fresh": "cd sdk && npm run check:state-document-fresh",
65
66
  "prepublishOnly": "npm run build:hooks && npm run build:sdk",
66
67
  "pretest": "npm run build:sdk && npm run lint:skill-deps",
67
68
  "pretest:coverage": "npm run build:sdk",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@gsd-build/sdk",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@gsd-build/sdk",
9
- "version": "1.42.1",
9
+ "version": "1.42.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/claude-agent-sdk": "^0.2.84",
@@ -18,6 +18,7 @@
18
18
  "devDependencies": {
19
19
  "@types/node": "^22.0.0",
20
20
  "@types/ws": "^8.18.1",
21
+ "tsx": "^4.22.0",
21
22
  "typescript": "^5.7.0",
22
23
  "vitest": "^3.1.1"
23
24
  },
@@ -1754,6 +1755,509 @@
1754
1755
  "node": ">=14.0.0"
1755
1756
  }
1756
1757
  },
1758
+ "node_modules/tsx": {
1759
+ "version": "4.22.0",
1760
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz",
1761
+ "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==",
1762
+ "dev": true,
1763
+ "license": "MIT",
1764
+ "dependencies": {
1765
+ "esbuild": "~0.28.0"
1766
+ },
1767
+ "bin": {
1768
+ "tsx": "dist/cli.mjs"
1769
+ },
1770
+ "engines": {
1771
+ "node": ">=18.0.0"
1772
+ },
1773
+ "optionalDependencies": {
1774
+ "fsevents": "~2.3.3"
1775
+ }
1776
+ },
1777
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
1778
+ "version": "0.28.0",
1779
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
1780
+ "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
1781
+ "cpu": [
1782
+ "ppc64"
1783
+ ],
1784
+ "dev": true,
1785
+ "license": "MIT",
1786
+ "optional": true,
1787
+ "os": [
1788
+ "aix"
1789
+ ],
1790
+ "engines": {
1791
+ "node": ">=18"
1792
+ }
1793
+ },
1794
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
1795
+ "version": "0.28.0",
1796
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
1797
+ "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
1798
+ "cpu": [
1799
+ "arm"
1800
+ ],
1801
+ "dev": true,
1802
+ "license": "MIT",
1803
+ "optional": true,
1804
+ "os": [
1805
+ "android"
1806
+ ],
1807
+ "engines": {
1808
+ "node": ">=18"
1809
+ }
1810
+ },
1811
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
1812
+ "version": "0.28.0",
1813
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
1814
+ "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
1815
+ "cpu": [
1816
+ "arm64"
1817
+ ],
1818
+ "dev": true,
1819
+ "license": "MIT",
1820
+ "optional": true,
1821
+ "os": [
1822
+ "android"
1823
+ ],
1824
+ "engines": {
1825
+ "node": ">=18"
1826
+ }
1827
+ },
1828
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
1829
+ "version": "0.28.0",
1830
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
1831
+ "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
1832
+ "cpu": [
1833
+ "x64"
1834
+ ],
1835
+ "dev": true,
1836
+ "license": "MIT",
1837
+ "optional": true,
1838
+ "os": [
1839
+ "android"
1840
+ ],
1841
+ "engines": {
1842
+ "node": ">=18"
1843
+ }
1844
+ },
1845
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
1846
+ "version": "0.28.0",
1847
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
1848
+ "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
1849
+ "cpu": [
1850
+ "arm64"
1851
+ ],
1852
+ "dev": true,
1853
+ "license": "MIT",
1854
+ "optional": true,
1855
+ "os": [
1856
+ "darwin"
1857
+ ],
1858
+ "engines": {
1859
+ "node": ">=18"
1860
+ }
1861
+ },
1862
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
1863
+ "version": "0.28.0",
1864
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
1865
+ "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
1866
+ "cpu": [
1867
+ "x64"
1868
+ ],
1869
+ "dev": true,
1870
+ "license": "MIT",
1871
+ "optional": true,
1872
+ "os": [
1873
+ "darwin"
1874
+ ],
1875
+ "engines": {
1876
+ "node": ">=18"
1877
+ }
1878
+ },
1879
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
1880
+ "version": "0.28.0",
1881
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
1882
+ "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
1883
+ "cpu": [
1884
+ "arm64"
1885
+ ],
1886
+ "dev": true,
1887
+ "license": "MIT",
1888
+ "optional": true,
1889
+ "os": [
1890
+ "freebsd"
1891
+ ],
1892
+ "engines": {
1893
+ "node": ">=18"
1894
+ }
1895
+ },
1896
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
1897
+ "version": "0.28.0",
1898
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
1899
+ "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
1900
+ "cpu": [
1901
+ "x64"
1902
+ ],
1903
+ "dev": true,
1904
+ "license": "MIT",
1905
+ "optional": true,
1906
+ "os": [
1907
+ "freebsd"
1908
+ ],
1909
+ "engines": {
1910
+ "node": ">=18"
1911
+ }
1912
+ },
1913
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
1914
+ "version": "0.28.0",
1915
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
1916
+ "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
1917
+ "cpu": [
1918
+ "arm"
1919
+ ],
1920
+ "dev": true,
1921
+ "license": "MIT",
1922
+ "optional": true,
1923
+ "os": [
1924
+ "linux"
1925
+ ],
1926
+ "engines": {
1927
+ "node": ">=18"
1928
+ }
1929
+ },
1930
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
1931
+ "version": "0.28.0",
1932
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
1933
+ "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
1934
+ "cpu": [
1935
+ "arm64"
1936
+ ],
1937
+ "dev": true,
1938
+ "license": "MIT",
1939
+ "optional": true,
1940
+ "os": [
1941
+ "linux"
1942
+ ],
1943
+ "engines": {
1944
+ "node": ">=18"
1945
+ }
1946
+ },
1947
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
1948
+ "version": "0.28.0",
1949
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
1950
+ "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
1951
+ "cpu": [
1952
+ "ia32"
1953
+ ],
1954
+ "dev": true,
1955
+ "license": "MIT",
1956
+ "optional": true,
1957
+ "os": [
1958
+ "linux"
1959
+ ],
1960
+ "engines": {
1961
+ "node": ">=18"
1962
+ }
1963
+ },
1964
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
1965
+ "version": "0.28.0",
1966
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
1967
+ "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
1968
+ "cpu": [
1969
+ "loong64"
1970
+ ],
1971
+ "dev": true,
1972
+ "license": "MIT",
1973
+ "optional": true,
1974
+ "os": [
1975
+ "linux"
1976
+ ],
1977
+ "engines": {
1978
+ "node": ">=18"
1979
+ }
1980
+ },
1981
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
1982
+ "version": "0.28.0",
1983
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
1984
+ "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
1985
+ "cpu": [
1986
+ "mips64el"
1987
+ ],
1988
+ "dev": true,
1989
+ "license": "MIT",
1990
+ "optional": true,
1991
+ "os": [
1992
+ "linux"
1993
+ ],
1994
+ "engines": {
1995
+ "node": ">=18"
1996
+ }
1997
+ },
1998
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
1999
+ "version": "0.28.0",
2000
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
2001
+ "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
2002
+ "cpu": [
2003
+ "ppc64"
2004
+ ],
2005
+ "dev": true,
2006
+ "license": "MIT",
2007
+ "optional": true,
2008
+ "os": [
2009
+ "linux"
2010
+ ],
2011
+ "engines": {
2012
+ "node": ">=18"
2013
+ }
2014
+ },
2015
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
2016
+ "version": "0.28.0",
2017
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
2018
+ "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
2019
+ "cpu": [
2020
+ "riscv64"
2021
+ ],
2022
+ "dev": true,
2023
+ "license": "MIT",
2024
+ "optional": true,
2025
+ "os": [
2026
+ "linux"
2027
+ ],
2028
+ "engines": {
2029
+ "node": ">=18"
2030
+ }
2031
+ },
2032
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
2033
+ "version": "0.28.0",
2034
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
2035
+ "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
2036
+ "cpu": [
2037
+ "s390x"
2038
+ ],
2039
+ "dev": true,
2040
+ "license": "MIT",
2041
+ "optional": true,
2042
+ "os": [
2043
+ "linux"
2044
+ ],
2045
+ "engines": {
2046
+ "node": ">=18"
2047
+ }
2048
+ },
2049
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
2050
+ "version": "0.28.0",
2051
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
2052
+ "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
2053
+ "cpu": [
2054
+ "x64"
2055
+ ],
2056
+ "dev": true,
2057
+ "license": "MIT",
2058
+ "optional": true,
2059
+ "os": [
2060
+ "linux"
2061
+ ],
2062
+ "engines": {
2063
+ "node": ">=18"
2064
+ }
2065
+ },
2066
+ "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
2067
+ "version": "0.28.0",
2068
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
2069
+ "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
2070
+ "cpu": [
2071
+ "arm64"
2072
+ ],
2073
+ "dev": true,
2074
+ "license": "MIT",
2075
+ "optional": true,
2076
+ "os": [
2077
+ "netbsd"
2078
+ ],
2079
+ "engines": {
2080
+ "node": ">=18"
2081
+ }
2082
+ },
2083
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
2084
+ "version": "0.28.0",
2085
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
2086
+ "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
2087
+ "cpu": [
2088
+ "x64"
2089
+ ],
2090
+ "dev": true,
2091
+ "license": "MIT",
2092
+ "optional": true,
2093
+ "os": [
2094
+ "netbsd"
2095
+ ],
2096
+ "engines": {
2097
+ "node": ">=18"
2098
+ }
2099
+ },
2100
+ "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
2101
+ "version": "0.28.0",
2102
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
2103
+ "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
2104
+ "cpu": [
2105
+ "arm64"
2106
+ ],
2107
+ "dev": true,
2108
+ "license": "MIT",
2109
+ "optional": true,
2110
+ "os": [
2111
+ "openbsd"
2112
+ ],
2113
+ "engines": {
2114
+ "node": ">=18"
2115
+ }
2116
+ },
2117
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
2118
+ "version": "0.28.0",
2119
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
2120
+ "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
2121
+ "cpu": [
2122
+ "x64"
2123
+ ],
2124
+ "dev": true,
2125
+ "license": "MIT",
2126
+ "optional": true,
2127
+ "os": [
2128
+ "openbsd"
2129
+ ],
2130
+ "engines": {
2131
+ "node": ">=18"
2132
+ }
2133
+ },
2134
+ "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
2135
+ "version": "0.28.0",
2136
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
2137
+ "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
2138
+ "cpu": [
2139
+ "arm64"
2140
+ ],
2141
+ "dev": true,
2142
+ "license": "MIT",
2143
+ "optional": true,
2144
+ "os": [
2145
+ "openharmony"
2146
+ ],
2147
+ "engines": {
2148
+ "node": ">=18"
2149
+ }
2150
+ },
2151
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
2152
+ "version": "0.28.0",
2153
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
2154
+ "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
2155
+ "cpu": [
2156
+ "x64"
2157
+ ],
2158
+ "dev": true,
2159
+ "license": "MIT",
2160
+ "optional": true,
2161
+ "os": [
2162
+ "sunos"
2163
+ ],
2164
+ "engines": {
2165
+ "node": ">=18"
2166
+ }
2167
+ },
2168
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
2169
+ "version": "0.28.0",
2170
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
2171
+ "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
2172
+ "cpu": [
2173
+ "arm64"
2174
+ ],
2175
+ "dev": true,
2176
+ "license": "MIT",
2177
+ "optional": true,
2178
+ "os": [
2179
+ "win32"
2180
+ ],
2181
+ "engines": {
2182
+ "node": ">=18"
2183
+ }
2184
+ },
2185
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
2186
+ "version": "0.28.0",
2187
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
2188
+ "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
2189
+ "cpu": [
2190
+ "ia32"
2191
+ ],
2192
+ "dev": true,
2193
+ "license": "MIT",
2194
+ "optional": true,
2195
+ "os": [
2196
+ "win32"
2197
+ ],
2198
+ "engines": {
2199
+ "node": ">=18"
2200
+ }
2201
+ },
2202
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
2203
+ "version": "0.28.0",
2204
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
2205
+ "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
2206
+ "cpu": [
2207
+ "x64"
2208
+ ],
2209
+ "dev": true,
2210
+ "license": "MIT",
2211
+ "optional": true,
2212
+ "os": [
2213
+ "win32"
2214
+ ],
2215
+ "engines": {
2216
+ "node": ">=18"
2217
+ }
2218
+ },
2219
+ "node_modules/tsx/node_modules/esbuild": {
2220
+ "version": "0.28.0",
2221
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
2222
+ "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
2223
+ "dev": true,
2224
+ "hasInstallScript": true,
2225
+ "license": "MIT",
2226
+ "bin": {
2227
+ "esbuild": "bin/esbuild"
2228
+ },
2229
+ "engines": {
2230
+ "node": ">=18"
2231
+ },
2232
+ "optionalDependencies": {
2233
+ "@esbuild/aix-ppc64": "0.28.0",
2234
+ "@esbuild/android-arm": "0.28.0",
2235
+ "@esbuild/android-arm64": "0.28.0",
2236
+ "@esbuild/android-x64": "0.28.0",
2237
+ "@esbuild/darwin-arm64": "0.28.0",
2238
+ "@esbuild/darwin-x64": "0.28.0",
2239
+ "@esbuild/freebsd-arm64": "0.28.0",
2240
+ "@esbuild/freebsd-x64": "0.28.0",
2241
+ "@esbuild/linux-arm": "0.28.0",
2242
+ "@esbuild/linux-arm64": "0.28.0",
2243
+ "@esbuild/linux-ia32": "0.28.0",
2244
+ "@esbuild/linux-loong64": "0.28.0",
2245
+ "@esbuild/linux-mips64el": "0.28.0",
2246
+ "@esbuild/linux-ppc64": "0.28.0",
2247
+ "@esbuild/linux-riscv64": "0.28.0",
2248
+ "@esbuild/linux-s390x": "0.28.0",
2249
+ "@esbuild/linux-x64": "0.28.0",
2250
+ "@esbuild/netbsd-arm64": "0.28.0",
2251
+ "@esbuild/netbsd-x64": "0.28.0",
2252
+ "@esbuild/openbsd-arm64": "0.28.0",
2253
+ "@esbuild/openbsd-x64": "0.28.0",
2254
+ "@esbuild/openharmony-arm64": "0.28.0",
2255
+ "@esbuild/sunos-x64": "0.28.0",
2256
+ "@esbuild/win32-arm64": "0.28.0",
2257
+ "@esbuild/win32-ia32": "0.28.0",
2258
+ "@esbuild/win32-x64": "0.28.0"
2259
+ }
2260
+ },
1757
2261
  "node_modules/typescript": {
1758
2262
  "version": "5.9.3",
1759
2263
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
package/sdk/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd-build/sdk",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "description": "GSD SDK — programmatic interface for running GSD plans via the Agent SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,8 @@
36
36
  "scripts": {
37
37
  "build": "tsc",
38
38
  "check:alias-drift": "npm run build && node scripts/check-command-aliases-fresh.mjs",
39
+ "gen:state-document": "npm run build && npx tsx scripts/gen-state-document.ts",
40
+ "check:state-document-fresh": "npm run build && node scripts/check-state-document-fresh.mjs",
39
41
  "prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js",
40
42
  "test": "vitest run",
41
43
  "test:unit": "vitest run --project unit",
@@ -48,6 +50,7 @@
48
50
  "devDependencies": {
49
51
  "@types/node": "^22.0.0",
50
52
  "@types/ws": "^8.18.1",
53
+ "tsx": "^4.22.0",
51
54
  "typescript": "^5.7.0",
52
55
  "vitest": "^3.1.1"
53
56
  }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Unit tests for STATE.md Document Module — stateExtractField.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ stateExtractField,
8
+ stateReplaceField,
9
+ stateReplaceFieldWithFallback,
10
+ normalizeStateStatus,
11
+ computeProgressPercent,
12
+ shouldPreserveExistingProgress,
13
+ normalizeProgressNumbers,
14
+ } from './state-document.js';
15
+
16
+ describe('stateExtractField', () => {
17
+ it('extracts value from bold pattern', () => {
18
+ const content = 'Some content\n**FieldName:** the value\nMore content';
19
+ expect(stateExtractField(content, 'FieldName')).toBe('the value');
20
+ });
21
+
22
+ it('extracts value from plain pattern', () => {
23
+ const content = 'Some content\nFieldName: the value\nMore content';
24
+ expect(stateExtractField(content, 'FieldName')).toBe('the value');
25
+ });
26
+
27
+ it('returns null when field is missing', () => {
28
+ const content = 'Some content\nOtherField: something\nMore content';
29
+ expect(stateExtractField(content, 'FieldName')).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe('stateReplaceField', () => {
34
+ it('replaces value in bold pattern', () => {
35
+ const content = 'Some content\n**Status:** old value\nMore content';
36
+ const result = stateReplaceField(content, 'Status', 'new value');
37
+ expect(result).toBe('Some content\n**Status:** new value\nMore content');
38
+ });
39
+
40
+ it('replaces value in plain pattern', () => {
41
+ const content = 'Some content\nStatus: old value\nMore content';
42
+ const result = stateReplaceField(content, 'Status', 'new value');
43
+ expect(result).toBe('Some content\nStatus: new value\nMore content');
44
+ });
45
+
46
+ it('returns null when field is missing', () => {
47
+ const content = 'Some content\nOtherField: something\nMore content';
48
+ const result = stateReplaceField(content, 'Status', 'new value');
49
+ expect(result).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('stateReplaceFieldWithFallback', () => {
54
+ it('replaces primary field when it exists', () => {
55
+ const content = 'Status: old\nState: backup';
56
+ const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
57
+ expect(result).toBe('Status: new\nState: backup');
58
+ });
59
+
60
+ it('replaces fallback field when primary is missing', () => {
61
+ const content = 'Other: something\nState: backup';
62
+ const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
63
+ expect(result).toBe('Other: something\nState: new');
64
+ });
65
+
66
+ it('returns content unchanged when neither field exists', () => {
67
+ const content = 'Other: something\nAnother: value';
68
+ const result = stateReplaceFieldWithFallback(content, 'Status', 'State', 'new');
69
+ expect(result).toBe(content);
70
+ });
71
+ });
72
+
73
+ describe('normalizeStateStatus', () => {
74
+ it('returns paused for status containing "paused"', () => {
75
+ expect(normalizeStateStatus('paused')).toBe('paused');
76
+ });
77
+
78
+ it('returns paused for status containing "stopped"', () => {
79
+ expect(normalizeStateStatus('stopped')).toBe('paused');
80
+ });
81
+
82
+ it('returns paused when non-null pausedAt is provided', () => {
83
+ expect(normalizeStateStatus('active', '2024-01-01')).toBe('paused');
84
+ });
85
+
86
+ it('returns executing for status containing "executing"', () => {
87
+ expect(normalizeStateStatus('executing')).toBe('executing');
88
+ });
89
+
90
+ it('returns executing for status "in progress"', () => {
91
+ expect(normalizeStateStatus('in progress')).toBe('executing');
92
+ });
93
+
94
+ it('returns executing for status "ready to execute"', () => {
95
+ expect(normalizeStateStatus('ready to execute')).toBe('executing');
96
+ });
97
+
98
+ it('returns planning for status containing "planning"', () => {
99
+ expect(normalizeStateStatus('planning')).toBe('planning');
100
+ });
101
+
102
+ it('returns discussing for status containing "discussing"', () => {
103
+ expect(normalizeStateStatus('discussing')).toBe('discussing');
104
+ });
105
+
106
+ it('returns verifying for status containing "verif"', () => {
107
+ expect(normalizeStateStatus('verifying')).toBe('verifying');
108
+ });
109
+
110
+ it('returns completed for status containing "complete"', () => {
111
+ expect(normalizeStateStatus('completed')).toBe('completed');
112
+ });
113
+
114
+ it('returns completed for status containing "done"', () => {
115
+ expect(normalizeStateStatus('done')).toBe('completed');
116
+ });
117
+
118
+ it('returns unknown for unrecognized status', () => {
119
+ expect(normalizeStateStatus('something-else')).toBe('something-else');
120
+ });
121
+
122
+ it('returns unknown for null status', () => {
123
+ expect(normalizeStateStatus(null)).toBe('unknown');
124
+ });
125
+ });
126
+
127
+ describe('computeProgressPercent', () => {
128
+ it('uses only plans data when phases data is absent', () => {
129
+ expect(computeProgressPercent(3, 10, null, null)).toBe(30);
130
+ });
131
+
132
+ it('uses only phases data when plans data is absent', () => {
133
+ expect(computeProgressPercent(null, null, 2, 4)).toBe(50);
134
+ });
135
+
136
+ it('uses minimum fraction when both plans and phases data are present', () => {
137
+ // plans: 8/10 = 80%, phases: 3/10 = 30% → min = 30%
138
+ expect(computeProgressPercent(8, 10, 3, 10)).toBe(30);
139
+ });
140
+
141
+ it('returns null when neither plans nor phases data is present', () => {
142
+ expect(computeProgressPercent(null, null, null, null)).toBeNull();
143
+ });
144
+
145
+ it('returns null when total is 0 (treated as no data)', () => {
146
+ expect(computeProgressPercent(0, 0, null, null)).toBeNull();
147
+ });
148
+ });
149
+
150
+ describe('shouldPreserveExistingProgress', () => {
151
+ it('returns true when existing total_phases exceeds derived', () => {
152
+ expect(shouldPreserveExistingProgress({ total_phases: 10 }, { total_phases: 5 })).toBe(true);
153
+ });
154
+
155
+ it('returns false when derived exceeds existing', () => {
156
+ expect(shouldPreserveExistingProgress({ total_phases: 5 }, { total_phases: 10 })).toBe(false);
157
+ });
158
+
159
+ it('returns false when existingProgress is not an object', () => {
160
+ expect(shouldPreserveExistingProgress(null, { total_phases: 5 })).toBe(false);
161
+ });
162
+
163
+ it('returns false when both are null', () => {
164
+ expect(shouldPreserveExistingProgress(null, null)).toBe(false);
165
+ });
166
+ });
167
+
168
+ describe('normalizeProgressNumbers', () => {
169
+ it('coerces all five tracked keys to numbers', () => {
170
+ const input = {
171
+ total_phases: '10',
172
+ completed_phases: '3',
173
+ total_plans: '5',
174
+ completed_plans: '2',
175
+ percent: '60',
176
+ };
177
+ expect(normalizeProgressNumbers(input)).toEqual({
178
+ total_phases: 10,
179
+ completed_phases: 3,
180
+ total_plans: 5,
181
+ completed_plans: 2,
182
+ percent: 60,
183
+ });
184
+ });
185
+
186
+ it('returns non-object input unchanged', () => {
187
+ expect(normalizeProgressNumbers(null)).toBeNull();
188
+ expect(normalizeProgressNumbers('string')).toBe('string');
189
+ });
190
+
191
+ it('preserves extra keys untouched', () => {
192
+ const input = { total_phases: '4', extra_key: 'hello' };
193
+ const result = normalizeProgressNumbers(input) as Record<string, unknown>;
194
+ expect(result.total_phases).toBe(4);
195
+ expect(result.extra_key).toBe('hello');
196
+ });
197
+ });
Binary file