gsd-opencode 1.22.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-advisor-researcher.md +112 -0
- package/agents/gsd-assumptions-analyzer.md +110 -0
- package/agents/gsd-codebase-mapper.md +1 -2
- package/agents/gsd-debugger.md +119 -2
- package/agents/gsd-executor.md +25 -4
- package/agents/gsd-integration-checker.md +1 -2
- package/agents/gsd-nyquist-auditor.md +1 -2
- package/agents/gsd-phase-researcher.md +151 -5
- package/agents/gsd-plan-checker.md +71 -5
- package/agents/gsd-planner.md +50 -4
- package/agents/gsd-project-researcher.md +29 -3
- package/agents/gsd-research-synthesizer.md +1 -2
- package/agents/gsd-roadmapper.md +30 -2
- package/agents/gsd-ui-auditor.md +445 -0
- package/agents/gsd-ui-checker.md +305 -0
- package/agents/gsd-ui-researcher.md +368 -0
- package/agents/gsd-user-profiler.md +173 -0
- package/agents/gsd-verifier.md +124 -4
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +41 -0
- package/commands/gsd/gsd-debug.md +5 -0
- package/commands/gsd/gsd-discuss-phase.md +10 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-execute-phase.md +20 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +39 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -0
- package/commands/gsd/gsd-new-workspace.md +44 -0
- package/commands/gsd/gsd-next.md +24 -0
- package/commands/gsd/gsd-note.md +34 -0
- package/commands/gsd/gsd-plan-phase.md +3 -1
- package/commands/gsd/gsd-plant-seed.md +28 -0
- package/commands/gsd/gsd-pr-branch.md +25 -0
- package/commands/gsd/gsd-profile-user.md +46 -0
- package/commands/gsd/gsd-quick.md +4 -2
- package/commands/gsd/gsd-reapply-patches.md +10 -6
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +5 -0
- package/commands/gsd/gsd-resume-work.md +1 -1
- package/commands/gsd/gsd-review-backlog.md +61 -0
- package/commands/gsd/gsd-review.md +37 -0
- package/commands/gsd/gsd-session-report.md +19 -0
- package/commands/gsd/gsd-set-profile.md +24 -23
- package/commands/gsd/gsd-ship.md +23 -0
- package/commands/gsd/gsd-stats.md +18 -0
- package/commands/gsd/gsd-thread.md +127 -0
- package/commands/gsd/gsd-ui-phase.md +34 -0
- package/commands/gsd/gsd-ui-review.md +32 -0
- package/commands/gsd/gsd-workstreams.md +66 -0
- package/get-shit-done/bin/gsd-tools.cjs +410 -84
- package/get-shit-done/bin/lib/commands.cjs +429 -18
- package/get-shit-done/bin/lib/config.cjs +318 -45
- package/get-shit-done/bin/lib/core.cjs +822 -84
- package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
- package/get-shit-done/bin/lib/init.cjs +836 -104
- package/get-shit-done/bin/lib/milestone.cjs +44 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
- package/get-shit-done/bin/lib/phase.cjs +293 -306
- package/get-shit-done/bin/lib/profile-output.cjs +952 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +55 -24
- package/get-shit-done/bin/lib/security.cjs +382 -0
- package/get-shit-done/bin/lib/state.cjs +363 -53
- package/get-shit-done/bin/lib/template.cjs +2 -2
- package/get-shit-done/bin/lib/uat.cjs +282 -0
- package/get-shit-done/bin/lib/verify.cjs +104 -36
- package/get-shit-done/bin/lib/workstream.cjs +491 -0
- package/get-shit-done/references/checkpoints.md +12 -10
- package/get-shit-done/references/decimal-phase-calculation.md +2 -3
- package/get-shit-done/references/git-integration.md +47 -0
- package/get-shit-done/references/model-profile-resolution.md +2 -0
- package/get-shit-done/references/model-profiles.md +62 -16
- package/get-shit-done/references/phase-argument-parsing.md +2 -2
- package/get-shit-done/references/planning-config.md +3 -1
- package/get-shit-done/references/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +58 -0
- package/get-shit-done/templates/UAT.md +21 -3
- package/get-shit-done/templates/UI-SPEC.md +100 -0
- package/get-shit-done/templates/claude-md.md +122 -0
- package/get-shit-done/templates/config.json +10 -3
- package/get-shit-done/templates/context.md +61 -6
- package/get-shit-done/templates/dev-preferences.md +21 -0
- package/get-shit-done/templates/discussion-log.md +63 -0
- package/get-shit-done/templates/phase-prompt.md +46 -5
- package/get-shit-done/templates/project.md +2 -0
- package/get-shit-done/templates/state.md +2 -2
- package/get-shit-done/templates/user-profile.md +146 -0
- package/get-shit-done/workflows/add-phase.md +2 -2
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +3 -3
- package/get-shit-done/workflows/audit-milestone.md +13 -5
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +891 -0
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +9 -6
- package/get-shit-done/workflows/diagnose-issues.md +15 -3
- package/get-shit-done/workflows/discovery-phase.md +2 -2
- package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
- package/get-shit-done/workflows/discuss-phase.md +411 -38
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/execute-phase.md +405 -18
- package/get-shit-done/workflows/execute-plan.md +77 -12
- package/get-shit-done/workflows/fast.md +105 -0
- package/get-shit-done/workflows/forensics.md +265 -0
- package/get-shit-done/workflows/health.md +28 -6
- package/get-shit-done/workflows/help.md +124 -7
- package/get-shit-done/workflows/insert-phase.md +2 -2
- package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/list-workspaces.md +56 -0
- package/get-shit-done/workflows/manager.md +362 -0
- package/get-shit-done/workflows/map-codebase.md +74 -13
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +120 -18
- package/get-shit-done/workflows/new-project.md +178 -39
- package/get-shit-done/workflows/new-workspace.md +237 -0
- package/get-shit-done/workflows/next.md +97 -0
- package/get-shit-done/workflows/node-repair.md +92 -0
- package/get-shit-done/workflows/note.md +156 -0
- package/get-shit-done/workflows/pause-work.md +62 -8
- package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
- package/get-shit-done/workflows/plan-phase.md +332 -33
- package/get-shit-done/workflows/plant-seed.md +169 -0
- package/get-shit-done/workflows/pr-branch.md +129 -0
- package/get-shit-done/workflows/profile-user.md +450 -0
- package/get-shit-done/workflows/progress.md +145 -20
- package/get-shit-done/workflows/quick.md +205 -49
- package/get-shit-done/workflows/remove-phase.md +2 -2
- package/get-shit-done/workflows/remove-workspace.md +90 -0
- package/get-shit-done/workflows/research-phase.md +11 -3
- package/get-shit-done/workflows/resume-project.md +35 -16
- package/get-shit-done/workflows/review.md +228 -0
- package/get-shit-done/workflows/session-report.md +146 -0
- package/get-shit-done/workflows/set-profile.md +2 -2
- package/get-shit-done/workflows/settings.md +80 -11
- package/get-shit-done/workflows/ship.md +228 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +147 -20
- package/get-shit-done/workflows/ui-phase.md +302 -0
- package/get-shit-done/workflows/ui-review.md +165 -0
- package/get-shit-done/workflows/update.md +108 -25
- package/get-shit-done/workflows/validate-phase.md +15 -8
- package/get-shit-done/workflows/verify-phase.md +16 -5
- package/get-shit-done/workflows/verify-work.md +72 -18
- package/package.json +1 -1
- package/skills/gsd-audit-milestone/SKILL.md +29 -0
- package/skills/gsd-cleanup/SKILL.md +19 -0
- package/skills/gsd-complete-milestone/SKILL.md +131 -0
- package/skills/gsd-discuss-phase/SKILL.md +54 -0
- package/skills/gsd-execute-phase/SKILL.md +49 -0
- package/skills/gsd-plan-phase/SKILL.md +37 -0
- package/skills/gsd-ui-phase/SKILL.md +24 -0
- package/skills/gsd-ui-review/SKILL.md +24 -0
- package/skills/gsd-verify-work/SKILL.md +30 -0
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
|
|
10
|
+
/** Shorthand — every state command needs this path */
|
|
11
|
+
function getStatePath(cwd) {
|
|
12
|
+
return planningPaths(cwd).state;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// Shared helper: extract a field value from STATE.md content.
|
|
11
16
|
// Supports both **Field:** bold and plain Field: format.
|
|
12
17
|
function stateExtractField(content, fieldName) {
|
|
@@ -21,15 +26,15 @@ function stateExtractField(content, fieldName) {
|
|
|
21
26
|
|
|
22
27
|
function cmdStateLoad(cwd, raw) {
|
|
23
28
|
const config = loadConfig(cwd);
|
|
24
|
-
const
|
|
29
|
+
const planDir = planningPaths(cwd).planning;
|
|
25
30
|
|
|
26
31
|
let stateRaw = '';
|
|
27
32
|
try {
|
|
28
|
-
stateRaw = fs.readFileSync(path.join(
|
|
29
|
-
} catch {}
|
|
33
|
+
stateRaw = fs.readFileSync(path.join(planDir, 'STATE.md'), 'utf-8');
|
|
34
|
+
} catch { /* intentionally empty */ }
|
|
30
35
|
|
|
31
|
-
const configExists = fs.existsSync(path.join(
|
|
32
|
-
const roadmapExists = fs.existsSync(path.join(
|
|
36
|
+
const configExists = fs.existsSync(path.join(planDir, 'config.json'));
|
|
37
|
+
const roadmapExists = fs.existsSync(path.join(planDir, 'ROADMAP.md'));
|
|
33
38
|
const stateExists = stateRaw.length > 0;
|
|
34
39
|
|
|
35
40
|
const result = {
|
|
@@ -65,7 +70,7 @@ function cmdStateLoad(cwd, raw) {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function cmdStateGet(cwd, section, raw) {
|
|
68
|
-
const statePath =
|
|
73
|
+
const statePath = planningPaths(cwd).state;
|
|
69
74
|
try {
|
|
70
75
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
71
76
|
|
|
@@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// Try to find markdown section or field
|
|
78
|
-
const fieldEscaped = section
|
|
83
|
+
const fieldEscaped = escapeRegex(section);
|
|
79
84
|
|
|
80
85
|
// Check for **field:** value (bold format)
|
|
81
86
|
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
|
@@ -110,22 +115,37 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
110
115
|
function readTextArgOrFile(cwd, value, filePath, label) {
|
|
111
116
|
if (!filePath) return value;
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
// Path traversal guard: ensure file resolves within project directory
|
|
119
|
+
const { validatePath } = require('./security.cjs');
|
|
120
|
+
const pathCheck = validatePath(filePath, cwd, { allowAbsolute: true });
|
|
121
|
+
if (!pathCheck.safe) {
|
|
122
|
+
throw new Error(`${label} path rejected: ${pathCheck.error}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
try {
|
|
115
|
-
return fs.readFileSync(
|
|
126
|
+
return fs.readFileSync(pathCheck.resolved, 'utf-8').trimEnd();
|
|
116
127
|
} catch {
|
|
117
128
|
throw new Error(`${label} file not found: ${filePath}`);
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
function cmdStatePatch(cwd, patches, raw) {
|
|
122
|
-
|
|
133
|
+
// Validate all field names before processing
|
|
134
|
+
const { validateFieldName } = require('./security.cjs');
|
|
135
|
+
for (const field of Object.keys(patches)) {
|
|
136
|
+
const fieldCheck = validateFieldName(field);
|
|
137
|
+
if (!fieldCheck.valid) {
|
|
138
|
+
error(`state patch: ${fieldCheck.error}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const statePath = planningPaths(cwd).state;
|
|
123
143
|
try {
|
|
124
144
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
125
145
|
const results = { updated: [], failed: [] };
|
|
126
146
|
|
|
127
147
|
for (const [field, value] of Object.entries(patches)) {
|
|
128
|
-
const fieldEscaped = field
|
|
148
|
+
const fieldEscaped = escapeRegex(field);
|
|
129
149
|
// Try **Field:** bold format first, then plain Field: format
|
|
130
150
|
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
131
151
|
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
@@ -156,10 +176,17 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
156
176
|
error('field and value required for state update');
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
|
|
179
|
+
// Validate field name to prevent regex injection via crafted field names
|
|
180
|
+
const { validateFieldName } = require('./security.cjs');
|
|
181
|
+
const fieldCheck = validateFieldName(field);
|
|
182
|
+
if (!fieldCheck.valid) {
|
|
183
|
+
error(`state update: ${fieldCheck.error}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const statePath = planningPaths(cwd).state;
|
|
160
187
|
try {
|
|
161
188
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
162
|
-
const fieldEscaped = field
|
|
189
|
+
const fieldEscaped = escapeRegex(field);
|
|
163
190
|
// Try **Field:** bold format first, then plain Field: format
|
|
164
191
|
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
165
192
|
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
|
@@ -180,21 +207,10 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
// ─── State Progression Engine ────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function stateExtractField(content, fieldName) {
|
|
185
|
-
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
186
|
-
// Try **Field:** bold format first
|
|
187
|
-
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
|
188
|
-
const boldMatch = content.match(boldPattern);
|
|
189
|
-
if (boldMatch) return boldMatch[1].trim();
|
|
190
|
-
// Fall back to plain Field: format
|
|
191
|
-
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
|
192
|
-
const plainMatch = content.match(plainPattern);
|
|
193
|
-
return plainMatch ? plainMatch[1].trim() : null;
|
|
194
|
-
}
|
|
210
|
+
// stateExtractField is defined above (shared helper) — do not duplicate.
|
|
195
211
|
|
|
196
212
|
function stateReplaceField(content, fieldName, newValue) {
|
|
197
|
-
const escaped = fieldName
|
|
213
|
+
const escaped = escapeRegex(fieldName);
|
|
198
214
|
// Try **Field:** bold format first, then plain Field: format
|
|
199
215
|
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
|
200
216
|
if (boldPattern.test(content)) {
|
|
@@ -207,37 +223,106 @@ function stateReplaceField(content, fieldName, newValue) {
|
|
|
207
223
|
return null;
|
|
208
224
|
}
|
|
209
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Replace a STATE.md field with fallback field name support.
|
|
228
|
+
* Tries `primary` first, then `fallback` (if provided), returns content unchanged
|
|
229
|
+
* if neither matches. This consolidates the replaceWithFallback pattern that was
|
|
230
|
+
* previously duplicated inline across phase.cjs, milestone.cjs, and state.cjs.
|
|
231
|
+
*/
|
|
232
|
+
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
233
|
+
let result = stateReplaceField(content, primary, value);
|
|
234
|
+
if (result) return result;
|
|
235
|
+
if (fallback) {
|
|
236
|
+
result = stateReplaceField(content, fallback, value);
|
|
237
|
+
if (result) return result;
|
|
238
|
+
}
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update fields within the ## Current Position section of STATE.md.
|
|
244
|
+
* This keeps the Current Position body in sync with the bold frontmatter fields.
|
|
245
|
+
* Only updates fields that already exist in the section; does not add new lines.
|
|
246
|
+
* Fixes #1365: advance-plan could not update Status/Last activity after begin-phase.
|
|
247
|
+
*/
|
|
248
|
+
function updateCurrentPositionFields(content, fields) {
|
|
249
|
+
const posPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
250
|
+
const posMatch = content.match(posPattern);
|
|
251
|
+
if (!posMatch) return content;
|
|
252
|
+
|
|
253
|
+
let posBody = posMatch[2];
|
|
254
|
+
|
|
255
|
+
if (fields.status && /^Status:/m.test(posBody)) {
|
|
256
|
+
posBody = posBody.replace(/^Status:.*$/m, `Status: ${fields.status}`);
|
|
257
|
+
}
|
|
258
|
+
if (fields.lastActivity && /^Last activity:/im.test(posBody)) {
|
|
259
|
+
posBody = posBody.replace(/^Last activity:.*$/im, `Last activity: ${fields.lastActivity}`);
|
|
260
|
+
}
|
|
261
|
+
if (fields.plan && /^Plan:/m.test(posBody)) {
|
|
262
|
+
posBody = posBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return content.replace(posPattern, `${posMatch[1]}${posBody}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
210
268
|
function cmdStateAdvancePlan(cwd, raw) {
|
|
211
|
-
const statePath =
|
|
269
|
+
const statePath = planningPaths(cwd).state;
|
|
212
270
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
213
271
|
|
|
214
272
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
215
|
-
const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10);
|
|
216
|
-
const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10);
|
|
217
273
|
const today = new Date().toISOString().split('T')[0];
|
|
218
274
|
|
|
275
|
+
// Try legacy separate fields first, then compound "Plan: X of Y" format
|
|
276
|
+
const legacyPlan = stateExtractField(content, 'Current Plan');
|
|
277
|
+
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
|
|
278
|
+
const planField = stateExtractField(content, 'Plan');
|
|
279
|
+
|
|
280
|
+
let currentPlan, totalPlans;
|
|
281
|
+
let useCompoundFormat = false;
|
|
282
|
+
|
|
283
|
+
if (legacyPlan && legacyTotal) {
|
|
284
|
+
currentPlan = parseInt(legacyPlan, 10);
|
|
285
|
+
totalPlans = parseInt(legacyTotal, 10);
|
|
286
|
+
} else if (planField) {
|
|
287
|
+
// Compound format: "2 of 6 in current phase" or "2 of 6"
|
|
288
|
+
currentPlan = parseInt(planField, 10);
|
|
289
|
+
const ofMatch = planField.match(/of\s+(\d+)/);
|
|
290
|
+
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
|
|
291
|
+
useCompoundFormat = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
219
294
|
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
|
220
295
|
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
|
221
296
|
return;
|
|
222
297
|
}
|
|
223
298
|
|
|
224
299
|
if (currentPlan >= totalPlans) {
|
|
225
|
-
content =
|
|
226
|
-
content =
|
|
300
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
|
|
301
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
302
|
+
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
|
|
227
303
|
writeStateMd(statePath, content, cwd);
|
|
228
304
|
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
|
229
305
|
} else {
|
|
230
306
|
const newPlan = currentPlan + 1;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
307
|
+
let planDisplayValue;
|
|
308
|
+
if (useCompoundFormat) {
|
|
309
|
+
// Preserve compound format: "X of Y in current phase" → replace X only
|
|
310
|
+
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
|
311
|
+
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
|
|
312
|
+
} else {
|
|
313
|
+
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
|
314
|
+
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
|
315
|
+
}
|
|
316
|
+
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
|
|
317
|
+
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
|
318
|
+
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
|
|
234
319
|
writeStateMd(statePath, content, cwd);
|
|
235
320
|
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
|
236
321
|
}
|
|
237
322
|
}
|
|
238
323
|
|
|
239
324
|
function cmdStateRecordMetric(cwd, options, raw) {
|
|
240
|
-
const statePath =
|
|
325
|
+
const statePath = planningPaths(cwd).state;
|
|
241
326
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
242
327
|
|
|
243
328
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
@@ -271,19 +356,21 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
271
356
|
}
|
|
272
357
|
|
|
273
358
|
function cmdStateUpdateProgress(cwd, raw) {
|
|
274
|
-
const statePath =
|
|
359
|
+
const statePath = planningPaths(cwd).state;
|
|
275
360
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
276
361
|
|
|
277
362
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
278
363
|
|
|
279
|
-
// Count summaries across
|
|
280
|
-
const phasesDir =
|
|
364
|
+
// Count summaries across current milestone phases only
|
|
365
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
281
366
|
let totalPlans = 0;
|
|
282
367
|
let totalSummaries = 0;
|
|
283
368
|
|
|
284
369
|
if (fs.existsSync(phasesDir)) {
|
|
370
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
285
371
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
286
|
-
.filter(e => e.isDirectory()).map(e => e.name)
|
|
372
|
+
.filter(e => e.isDirectory()).map(e => e.name)
|
|
373
|
+
.filter(isDirInMilestone);
|
|
287
374
|
for (const dir of phaseDirs) {
|
|
288
375
|
const files = fs.readdirSync(path.join(phasesDir, dir));
|
|
289
376
|
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
@@ -314,7 +401,7 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
314
401
|
}
|
|
315
402
|
|
|
316
403
|
function cmdStateAddDecision(cwd, options, raw) {
|
|
317
|
-
const statePath =
|
|
404
|
+
const statePath = planningPaths(cwd).state;
|
|
318
405
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
319
406
|
|
|
320
407
|
const { phase, summary, summary_file, rationale, rationale_file } = options;
|
|
@@ -352,7 +439,7 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
352
439
|
}
|
|
353
440
|
|
|
354
441
|
function cmdStateAddBlocker(cwd, text, raw) {
|
|
355
|
-
const statePath =
|
|
442
|
+
const statePath = planningPaths(cwd).state;
|
|
356
443
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
357
444
|
const blockerOptions = typeof text === 'object' && text !== null ? text : { text };
|
|
358
445
|
let blockerText = null;
|
|
@@ -385,7 +472,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
385
472
|
}
|
|
386
473
|
|
|
387
474
|
function cmdStateResolveBlocker(cwd, text, raw) {
|
|
388
|
-
const statePath =
|
|
475
|
+
const statePath = planningPaths(cwd).state;
|
|
389
476
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
390
477
|
if (!text) { output({ error: 'text required' }, raw); return; }
|
|
391
478
|
|
|
@@ -417,7 +504,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
417
504
|
}
|
|
418
505
|
|
|
419
506
|
function cmdStateRecordSession(cwd, options, raw) {
|
|
420
|
-
const statePath =
|
|
507
|
+
const statePath = planningPaths(cwd).state;
|
|
421
508
|
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
|
422
509
|
|
|
423
510
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
@@ -452,7 +539,7 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
452
539
|
}
|
|
453
540
|
|
|
454
541
|
function cmdStateSnapshot(cwd, raw) {
|
|
455
|
-
const statePath =
|
|
542
|
+
const statePath = planningPaths(cwd).state;
|
|
456
543
|
|
|
457
544
|
if (!fs.existsSync(statePath)) {
|
|
458
545
|
output({ error: 'STATE.md not found' }, raw);
|
|
@@ -574,7 +661,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
574
661
|
const info = getMilestoneInfo(cwd);
|
|
575
662
|
milestone = info.version;
|
|
576
663
|
milestoneName = info.name;
|
|
577
|
-
} catch {}
|
|
664
|
+
} catch { /* intentionally empty */ }
|
|
578
665
|
}
|
|
579
666
|
|
|
580
667
|
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
@@ -584,7 +671,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
584
671
|
|
|
585
672
|
if (cwd) {
|
|
586
673
|
try {
|
|
587
|
-
const phasesDir =
|
|
674
|
+
const phasesDir = planningPaths(cwd).phases;
|
|
588
675
|
if (fs.existsSync(phasesDir)) {
|
|
589
676
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
590
677
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
@@ -609,7 +696,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
609
696
|
totalPlans = diskTotalPlans;
|
|
610
697
|
completedPlans = diskTotalSummaries;
|
|
611
698
|
}
|
|
612
|
-
} catch {}
|
|
699
|
+
} catch { /* intentionally empty */ }
|
|
613
700
|
}
|
|
614
701
|
|
|
615
702
|
let progressPercent = null;
|
|
@@ -662,27 +749,92 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
662
749
|
}
|
|
663
750
|
|
|
664
751
|
function stripFrontmatter(content) {
|
|
665
|
-
|
|
752
|
+
// Strip ALL frontmatter blocks at the start of the file.
|
|
753
|
+
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
|
754
|
+
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
|
755
|
+
let result = content;
|
|
756
|
+
// eslint-disable-next-line no-constant-condition
|
|
757
|
+
while (true) {
|
|
758
|
+
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
759
|
+
if (stripped === result) break;
|
|
760
|
+
result = stripped;
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
666
763
|
}
|
|
667
764
|
|
|
668
765
|
function syncStateFrontmatter(content, cwd) {
|
|
766
|
+
// read existing frontmatter BEFORE stripping — it may contain values
|
|
767
|
+
// that the body no longer has (e.g., Status field removed by an agent).
|
|
768
|
+
const existingFm = extractFrontmatter(content);
|
|
669
769
|
const body = stripFrontmatter(content);
|
|
670
|
-
const
|
|
671
|
-
|
|
770
|
+
const derivedFm = buildStateFrontmatter(body, cwd);
|
|
771
|
+
|
|
772
|
+
// Preserve existing frontmatter status when body-derived status is 'unknown'.
|
|
773
|
+
// This prevents a missing Status: field in the body from overwriting a
|
|
774
|
+
// previously valid status (e.g., 'executing' → 'unknown').
|
|
775
|
+
if (derivedFm.status === 'unknown' && existingFm.status && existingFm.status !== 'unknown') {
|
|
776
|
+
derivedFm.status = existingFm.status;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
672
780
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
673
781
|
}
|
|
674
782
|
|
|
675
783
|
/**
|
|
676
784
|
* write STATE.md with synchronized YAML frontmatter.
|
|
677
785
|
* All STATE.md writes should use this instead of raw writeFileSync.
|
|
786
|
+
* Uses a simple lockfile to prevent parallel agents from overwriting
|
|
787
|
+
* each other's changes (race condition with read-modify-write cycle).
|
|
678
788
|
*/
|
|
679
789
|
function writeStateMd(statePath, content, cwd) {
|
|
680
790
|
const synced = syncStateFrontmatter(content, cwd);
|
|
681
|
-
|
|
791
|
+
const lockPath = statePath + '.lock';
|
|
792
|
+
const maxRetries = 10;
|
|
793
|
+
const retryDelay = 200; // ms
|
|
794
|
+
|
|
795
|
+
// Acquire lock (spin with backoff)
|
|
796
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
797
|
+
try {
|
|
798
|
+
// O_EXCL fails if file already exists — atomic lock
|
|
799
|
+
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
800
|
+
fs.writeSync(fd, String(process.pid));
|
|
801
|
+
fs.closeSync(fd);
|
|
802
|
+
break;
|
|
803
|
+
} catch (err) {
|
|
804
|
+
if (err.code === 'EEXIST') {
|
|
805
|
+
// Check for stale lock (> 10s old)
|
|
806
|
+
try {
|
|
807
|
+
const stat = fs.statSync(lockPath);
|
|
808
|
+
if (Date.now() - stat.mtimeMs > 10000) {
|
|
809
|
+
fs.unlinkSync(lockPath);
|
|
810
|
+
continue; // retry immediately after clearing stale lock
|
|
811
|
+
}
|
|
812
|
+
} catch { /* lock was released between check — retry */ }
|
|
813
|
+
|
|
814
|
+
if (i === maxRetries - 1) {
|
|
815
|
+
// Last resort: write anyway rather than losing data
|
|
816
|
+
try { fs.unlinkSync(lockPath); } catch {}
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
// Spin-wait with small jitter
|
|
820
|
+
const jitter = Math.floor(Math.random() * 50);
|
|
821
|
+
const start = Date.now();
|
|
822
|
+
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
break; // non-EEXIST error — proceed without lock
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
|
831
|
+
} finally {
|
|
832
|
+
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
|
833
|
+
}
|
|
682
834
|
}
|
|
683
835
|
|
|
684
836
|
function cmdStateJson(cwd, raw) {
|
|
685
|
-
const statePath =
|
|
837
|
+
const statePath = planningPaths(cwd).state;
|
|
686
838
|
if (!fs.existsSync(statePath)) {
|
|
687
839
|
output({ error: 'STATE.md not found' }, raw, 'STATE.md not found');
|
|
688
840
|
return;
|
|
@@ -701,9 +853,164 @@ function cmdStateJson(cwd, raw) {
|
|
|
701
853
|
output(fm, raw, JSON.stringify(fm, null, 2));
|
|
702
854
|
}
|
|
703
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Update STATE.md when a new phase begins execution.
|
|
858
|
+
* Updates body text fields (Current focus, Status, Last Activity, Current Position)
|
|
859
|
+
* and synchronizes frontmatter via writeStateMd.
|
|
860
|
+
* Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text).
|
|
861
|
+
*/
|
|
862
|
+
function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
863
|
+
const statePath = planningPaths(cwd).state;
|
|
864
|
+
if (!fs.existsSync(statePath)) {
|
|
865
|
+
output({ error: 'STATE.md not found' }, raw);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
let content = fs.readFileSync(statePath, 'utf-8');
|
|
870
|
+
const today = new Date().toISOString().split('T')[0];
|
|
871
|
+
const updated = [];
|
|
872
|
+
|
|
873
|
+
// Update Status field
|
|
874
|
+
const statusValue = `Executing Phase ${phaseNumber}`;
|
|
875
|
+
let result = stateReplaceField(content, 'Status', statusValue);
|
|
876
|
+
if (result) { content = result; updated.push('Status'); }
|
|
877
|
+
|
|
878
|
+
// Update Last Activity
|
|
879
|
+
result = stateReplaceField(content, 'Last Activity', today);
|
|
880
|
+
if (result) { content = result; updated.push('Last Activity'); }
|
|
881
|
+
|
|
882
|
+
// Update Last Activity Description if it exists
|
|
883
|
+
const activityDesc = `Phase ${phaseNumber} execution started`;
|
|
884
|
+
result = stateReplaceField(content, 'Last Activity Description', activityDesc);
|
|
885
|
+
if (result) { content = result; updated.push('Last Activity Description'); }
|
|
886
|
+
|
|
887
|
+
// Update Current Phase
|
|
888
|
+
result = stateReplaceField(content, 'Current Phase', String(phaseNumber));
|
|
889
|
+
if (result) { content = result; updated.push('Current Phase'); }
|
|
890
|
+
|
|
891
|
+
// Update Current Phase Name
|
|
892
|
+
if (phaseName) {
|
|
893
|
+
result = stateReplaceField(content, 'Current Phase Name', phaseName);
|
|
894
|
+
if (result) { content = result; updated.push('Current Phase Name'); }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Update Current Plan to 1 (starting from the first plan)
|
|
898
|
+
result = stateReplaceField(content, 'Current Plan', '1');
|
|
899
|
+
if (result) { content = result; updated.push('Current Plan'); }
|
|
900
|
+
|
|
901
|
+
// Update Total Plans in Phase
|
|
902
|
+
if (planCount) {
|
|
903
|
+
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
|
904
|
+
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Update **Current focus:** body text line (#1104)
|
|
908
|
+
const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
|
|
909
|
+
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
|
|
910
|
+
if (focusPattern.test(content)) {
|
|
911
|
+
content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
|
|
912
|
+
updated.push('Current focus');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Update ## Current Position section (#1104, #1365)
|
|
916
|
+
// Update individual fields within Current Position instead of replacing the
|
|
917
|
+
// entire section, so that Status, Last activity, and Progress are preserved.
|
|
918
|
+
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
|
919
|
+
const positionMatch = content.match(positionPattern);
|
|
920
|
+
if (positionMatch) {
|
|
921
|
+
const header = positionMatch[1];
|
|
922
|
+
let posBody = positionMatch[2];
|
|
923
|
+
|
|
924
|
+
// Update or insert Phase line
|
|
925
|
+
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
926
|
+
if (/^Phase:/m.test(posBody)) {
|
|
927
|
+
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
928
|
+
} else {
|
|
929
|
+
posBody = newPhase + '\n' + posBody;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Update or insert Plan line
|
|
933
|
+
const newPlan = `Plan: 1 of ${planCount || '?'}`;
|
|
934
|
+
if (/^Plan:/m.test(posBody)) {
|
|
935
|
+
posBody = posBody.replace(/^Plan:.*$/m, newPlan);
|
|
936
|
+
} else {
|
|
937
|
+
posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Update Status line if present
|
|
941
|
+
const newStatus = `Status: Executing Phase ${phaseNumber}`;
|
|
942
|
+
if (/^Status:/m.test(posBody)) {
|
|
943
|
+
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Update Last activity line if present
|
|
947
|
+
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
|
|
948
|
+
if (/^Last activity:/im.test(posBody)) {
|
|
949
|
+
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
content = content.replace(positionPattern, `${header}${posBody}`);
|
|
953
|
+
updated.push('Current Position');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (updated.length > 0) {
|
|
957
|
+
writeStateMd(statePath, content, cwd);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* write a WAITING.json signal file when GSD hits a decision point.
|
|
965
|
+
* External watchers (fswatch, polling, orchestrators) can detect this.
|
|
966
|
+
* File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists).
|
|
967
|
+
* Fixes #1034.
|
|
968
|
+
*/
|
|
969
|
+
function cmdSignalWaiting(cwd, type, question, options, phase, raw) {
|
|
970
|
+
const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : planningDir(cwd);
|
|
971
|
+
const waitingPath = path.join(gsdDir, 'WAITING.json');
|
|
972
|
+
|
|
973
|
+
const signal = {
|
|
974
|
+
status: 'waiting',
|
|
975
|
+
type: type || 'decision_point',
|
|
976
|
+
question: question || null,
|
|
977
|
+
options: options ? options.split('|').map(o => o.trim()) : [],
|
|
978
|
+
since: new Date().toISOString(),
|
|
979
|
+
phase: phase || null,
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
fs.mkdirSync(gsdDir, { recursive: true });
|
|
984
|
+
fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8');
|
|
985
|
+
output({ signaled: true, path: waitingPath }, raw, 'true');
|
|
986
|
+
} catch (e) {
|
|
987
|
+
output({ signaled: false, error: e.message }, raw, 'false');
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Remove the WAITING.json signal file when user answers and agent resumes.
|
|
993
|
+
*/
|
|
994
|
+
function cmdSignalResume(cwd, raw) {
|
|
995
|
+
const paths = [
|
|
996
|
+
path.join(cwd, '.gsd', 'WAITING.json'),
|
|
997
|
+
path.join(planningDir(cwd), 'WAITING.json'),
|
|
998
|
+
];
|
|
999
|
+
|
|
1000
|
+
let removed = false;
|
|
1001
|
+
for (const p of paths) {
|
|
1002
|
+
if (fs.existsSync(p)) {
|
|
1003
|
+
try { fs.unlinkSync(p); removed = true; } catch {}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
704
1010
|
module.exports = {
|
|
705
1011
|
stateExtractField,
|
|
706
1012
|
stateReplaceField,
|
|
1013
|
+
stateReplaceFieldWithFallback,
|
|
707
1014
|
writeStateMd,
|
|
708
1015
|
cmdStateLoad,
|
|
709
1016
|
cmdStateGet,
|
|
@@ -718,4 +1025,7 @@ module.exports = {
|
|
|
718
1025
|
cmdStateRecordSession,
|
|
719
1026
|
cmdStateSnapshot,
|
|
720
1027
|
cmdStateJson,
|
|
1028
|
+
cmdStateBeginPhase,
|
|
1029
|
+
cmdSignalWaiting,
|
|
1030
|
+
cmdSignalResume,
|
|
721
1031
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, toPosixPath, output, error } = require('./core.cjs');
|
|
7
|
+
const { normalizePhaseName, findPhaseInternal, generateSlugInternal, normalizeMd, toPosixPath, output, error } = require('./core.cjs');
|
|
8
8
|
const { reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
|
|
10
10
|
function cmdTemplateSelect(cwd, planPath, raw) {
|
|
@@ -214,7 +214,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
fs.writeFileSync(outPath, fullContent, 'utf-8');
|
|
217
|
+
fs.writeFileSync(outPath, normalizeMd(fullContent), 'utf-8');
|
|
218
218
|
const relPath = toPosixPath(path.relative(cwd, outPath));
|
|
219
219
|
output({ created: true, path: relPath, template: templateType }, raw, relPath);
|
|
220
220
|
}
|