gswd 1.0.1 → 1.1.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/bin/gswd-tools.cjs +228 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +3 -2
- package/templates/gswd/DECISIONS.template.md +3 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* State module tests — atomic writes, STATE.json CRUD, idempotent init, ID registry
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_test_1 = require("node:test");
|
|
40
|
+
const assert = __importStar(require("node:assert"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const os = __importStar(require("node:os"));
|
|
44
|
+
const state_js_1 = require("../lib/state.js");
|
|
45
|
+
let tmpDir;
|
|
46
|
+
(0, node_test_1.beforeEach)(() => {
|
|
47
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-test-'));
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.afterEach)(() => {
|
|
50
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
// ─── safeWriteFile ───────────────────────────────────────────────────────────
|
|
53
|
+
(0, node_test_1.describe)('safeWriteFile', () => {
|
|
54
|
+
(0, node_test_1.it)('writes file atomically with correct content', () => {
|
|
55
|
+
const filePath = path.join(tmpDir, 'test.txt');
|
|
56
|
+
(0, state_js_1.safeWriteFile)(filePath, 'hello world');
|
|
57
|
+
assert.strictEqual(fs.readFileSync(filePath, 'utf-8'), 'hello world');
|
|
58
|
+
});
|
|
59
|
+
(0, node_test_1.it)('leaves no .tmp file after successful write', () => {
|
|
60
|
+
const filePath = path.join(tmpDir, 'test.txt');
|
|
61
|
+
(0, state_js_1.safeWriteFile)(filePath, 'content');
|
|
62
|
+
assert.strictEqual(fs.existsSync(filePath + '.tmp'), false);
|
|
63
|
+
});
|
|
64
|
+
(0, node_test_1.it)('creates parent directories if missing', () => {
|
|
65
|
+
const filePath = path.join(tmpDir, 'deep', 'nested', 'dir', 'test.txt');
|
|
66
|
+
(0, state_js_1.safeWriteFile)(filePath, 'nested content');
|
|
67
|
+
assert.strictEqual(fs.readFileSync(filePath, 'utf-8'), 'nested content');
|
|
68
|
+
});
|
|
69
|
+
(0, node_test_1.it)('overwrites existing file atomically', () => {
|
|
70
|
+
const filePath = path.join(tmpDir, 'test.txt');
|
|
71
|
+
(0, state_js_1.safeWriteFile)(filePath, 'first');
|
|
72
|
+
(0, state_js_1.safeWriteFile)(filePath, 'second');
|
|
73
|
+
assert.strictEqual(fs.readFileSync(filePath, 'utf-8'), 'second');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// ─── safeWriteJson ───────────────────────────────────────────────────────────
|
|
77
|
+
(0, node_test_1.describe)('safeWriteJson', () => {
|
|
78
|
+
(0, node_test_1.it)('writes valid JSON with 2-space indent and trailing newline', () => {
|
|
79
|
+
const filePath = path.join(tmpDir, 'test.json');
|
|
80
|
+
(0, state_js_1.safeWriteJson)(filePath, { key: 'value' });
|
|
81
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
82
|
+
assert.strictEqual(content, '{\n "key": "value"\n}\n');
|
|
83
|
+
});
|
|
84
|
+
(0, node_test_1.it)('round-trips: write then read produces identical object', () => {
|
|
85
|
+
const filePath = path.join(tmpDir, 'test.json');
|
|
86
|
+
const data = { a: 1, b: [2, 3], c: { nested: true } };
|
|
87
|
+
(0, state_js_1.safeWriteJson)(filePath, data);
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
89
|
+
assert.deepStrictEqual(parsed, data);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ─── createDefaultState ──────────────────────────────────────────────────────
|
|
93
|
+
(0, node_test_1.describe)('createDefaultState', () => {
|
|
94
|
+
(0, node_test_1.it)('returns object with all required top-level keys', () => {
|
|
95
|
+
const state = (0, state_js_1.createDefaultState)('my-project');
|
|
96
|
+
const keys = Object.keys(state);
|
|
97
|
+
assert.ok(keys.includes('version'));
|
|
98
|
+
assert.ok(keys.includes('project_slug'));
|
|
99
|
+
assert.ok(keys.includes('created_at'));
|
|
100
|
+
assert.ok(keys.includes('updated_at'));
|
|
101
|
+
assert.ok(keys.includes('stage'));
|
|
102
|
+
assert.ok(keys.includes('stage_status'));
|
|
103
|
+
assert.ok(keys.includes('last_checkpoint'));
|
|
104
|
+
assert.ok(keys.includes('auto'));
|
|
105
|
+
assert.ok(keys.includes('approvals'));
|
|
106
|
+
});
|
|
107
|
+
(0, node_test_1.it)('sets version to 1', () => {
|
|
108
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
109
|
+
assert.strictEqual(state.version, 1);
|
|
110
|
+
});
|
|
111
|
+
(0, node_test_1.it)('sets project_slug from argument', () => {
|
|
112
|
+
const state = (0, state_js_1.createDefaultState)('my-project');
|
|
113
|
+
assert.strictEqual(state.project_slug, 'my-project');
|
|
114
|
+
});
|
|
115
|
+
(0, node_test_1.it)('sets all four stages to not_started', () => {
|
|
116
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
117
|
+
assert.strictEqual(state.stage_status.imagine, 'not_started');
|
|
118
|
+
assert.strictEqual(state.stage_status.specify, 'not_started');
|
|
119
|
+
assert.strictEqual(state.stage_status.audit, 'not_started');
|
|
120
|
+
assert.strictEqual(state.stage_status.compile, 'not_started');
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.it)('sets auto.policy to balanced', () => {
|
|
123
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
124
|
+
assert.strictEqual(state.auto.policy, 'balanced');
|
|
125
|
+
});
|
|
126
|
+
(0, node_test_1.it)('sets last_checkpoint to null', () => {
|
|
127
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
128
|
+
assert.strictEqual(state.last_checkpoint, null);
|
|
129
|
+
});
|
|
130
|
+
(0, node_test_1.it)('sets stage to init', () => {
|
|
131
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
132
|
+
assert.strictEqual(state.stage, 'init');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// ─── readState ───────────────────────────────────────────────────────────────
|
|
136
|
+
(0, node_test_1.describe)('readState', () => {
|
|
137
|
+
(0, node_test_1.it)('returns null for missing file', () => {
|
|
138
|
+
const result = (0, state_js_1.readState)(path.join(tmpDir, 'nonexistent.json'));
|
|
139
|
+
assert.strictEqual(result, null);
|
|
140
|
+
});
|
|
141
|
+
(0, node_test_1.it)('returns null for invalid JSON', () => {
|
|
142
|
+
const filePath = path.join(tmpDir, 'bad.json');
|
|
143
|
+
fs.writeFileSync(filePath, 'not json');
|
|
144
|
+
assert.strictEqual((0, state_js_1.readState)(filePath), null);
|
|
145
|
+
});
|
|
146
|
+
(0, node_test_1.it)('returns null for JSON missing required fields', () => {
|
|
147
|
+
const filePath = path.join(tmpDir, 'incomplete.json');
|
|
148
|
+
fs.writeFileSync(filePath, JSON.stringify({ foo: 'bar' }));
|
|
149
|
+
assert.strictEqual((0, state_js_1.readState)(filePath), null);
|
|
150
|
+
});
|
|
151
|
+
(0, node_test_1.it)('returns parsed state for valid STATE.json', () => {
|
|
152
|
+
const state = (0, state_js_1.createDefaultState)('test');
|
|
153
|
+
const filePath = path.join(tmpDir, 'STATE.json');
|
|
154
|
+
(0, state_js_1.safeWriteJson)(filePath, state);
|
|
155
|
+
const read = (0, state_js_1.readState)(filePath);
|
|
156
|
+
assert.deepStrictEqual(read, state);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
// ─── initState (idempotent) ──────────────────────────────────────────────────
|
|
160
|
+
(0, node_test_1.describe)('initState', () => {
|
|
161
|
+
(0, node_test_1.it)('creates STATE.json when missing', () => {
|
|
162
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
163
|
+
const state = (0, state_js_1.initState)(stateDir, 'new-project');
|
|
164
|
+
assert.strictEqual(state.project_slug, 'new-project');
|
|
165
|
+
assert.ok(fs.existsSync(path.join(stateDir, 'STATE.json')));
|
|
166
|
+
});
|
|
167
|
+
(0, node_test_1.it)('returns existing state unchanged when valid STATE.json exists', () => {
|
|
168
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
169
|
+
const first = (0, state_js_1.initState)(stateDir, 'project');
|
|
170
|
+
const second = (0, state_js_1.initState)(stateDir, 'project');
|
|
171
|
+
assert.deepStrictEqual(first, second);
|
|
172
|
+
});
|
|
173
|
+
(0, node_test_1.it)('does NOT update timestamps on existing valid state', () => {
|
|
174
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
175
|
+
const first = (0, state_js_1.initState)(stateDir, 'project');
|
|
176
|
+
// Wait a tick to ensure timestamp would differ
|
|
177
|
+
const second = (0, state_js_1.initState)(stateDir, 'project');
|
|
178
|
+
assert.strictEqual(first.created_at, second.created_at);
|
|
179
|
+
assert.strictEqual(first.updated_at, second.updated_at);
|
|
180
|
+
});
|
|
181
|
+
(0, node_test_1.it)('creates directory if missing', () => {
|
|
182
|
+
const stateDir = path.join(tmpDir, 'deep', 'nested', 'gswd');
|
|
183
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
184
|
+
assert.ok(fs.existsSync(path.join(stateDir, 'STATE.json')));
|
|
185
|
+
});
|
|
186
|
+
(0, node_test_1.it)('handles corrupt JSON by creating fresh state', () => {
|
|
187
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
188
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(stateDir, 'STATE.json'), 'corrupt!!!');
|
|
190
|
+
const state = (0, state_js_1.initState)(stateDir, 'recovered');
|
|
191
|
+
assert.strictEqual(state.project_slug, 'recovered');
|
|
192
|
+
assert.strictEqual(state.version, 1);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// ─── updateStageStatus ───────────────────────────────────────────────────────
|
|
196
|
+
(0, node_test_1.describe)('updateStageStatus', () => {
|
|
197
|
+
(0, node_test_1.it)('updates specific stage to new status', () => {
|
|
198
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
199
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
200
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
201
|
+
(0, state_js_1.updateStageStatus)(statePath, 'imagine', 'in_progress');
|
|
202
|
+
const state = (0, state_js_1.readState)(statePath);
|
|
203
|
+
assert.strictEqual(state.stage_status.imagine, 'in_progress');
|
|
204
|
+
});
|
|
205
|
+
(0, node_test_1.it)('preserves all other fields', () => {
|
|
206
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
207
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
208
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
209
|
+
(0, state_js_1.updateStageStatus)(statePath, 'imagine', 'done');
|
|
210
|
+
const state = (0, state_js_1.readState)(statePath);
|
|
211
|
+
assert.strictEqual(state.project_slug, 'project');
|
|
212
|
+
assert.strictEqual(state.stage_status.specify, 'not_started');
|
|
213
|
+
assert.strictEqual(state.stage_status.audit, 'not_started');
|
|
214
|
+
assert.strictEqual(state.stage_status.compile, 'not_started');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
// ─── writeCheckpoint ─────────────────────────────────────────────────────────
|
|
218
|
+
(0, node_test_1.describe)('writeCheckpoint', () => {
|
|
219
|
+
(0, node_test_1.it)('sets last_checkpoint with workflow, checkpoint_id, timestamp', () => {
|
|
220
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
221
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
222
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
223
|
+
(0, state_js_1.writeCheckpoint)(statePath, 'gswd/imagine', 'step-1');
|
|
224
|
+
const state = (0, state_js_1.readState)(statePath);
|
|
225
|
+
assert.ok(state.last_checkpoint);
|
|
226
|
+
assert.strictEqual(state.last_checkpoint.workflow, 'gswd/imagine');
|
|
227
|
+
assert.strictEqual(state.last_checkpoint.checkpoint_id, 'step-1');
|
|
228
|
+
assert.ok(state.last_checkpoint.timestamp);
|
|
229
|
+
});
|
|
230
|
+
(0, node_test_1.it)('preserves all other state fields', () => {
|
|
231
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
232
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
233
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
234
|
+
(0, state_js_1.writeCheckpoint)(statePath, 'gswd/specify', 'mid-point');
|
|
235
|
+
const state = (0, state_js_1.readState)(statePath);
|
|
236
|
+
assert.strictEqual(state.project_slug, 'project');
|
|
237
|
+
assert.strictEqual(state.version, 1);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ─── ID Registry ─────────────────────────────────────────────────────────────
|
|
241
|
+
(0, node_test_1.describe)('allocateIdRange', () => {
|
|
242
|
+
(0, node_test_1.it)('first allocation returns range starting at 1', () => {
|
|
243
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
244
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
245
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
246
|
+
const range = (0, state_js_1.allocateIdRange)(statePath, 'FR', 'agent-1');
|
|
247
|
+
assert.strictEqual(range.start, 1);
|
|
248
|
+
assert.strictEqual(range.end, 50);
|
|
249
|
+
});
|
|
250
|
+
(0, node_test_1.it)('second allocation continues from previous end', () => {
|
|
251
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
252
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
253
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
254
|
+
(0, state_js_1.allocateIdRange)(statePath, 'FR', 'agent-1');
|
|
255
|
+
const range2 = (0, state_js_1.allocateIdRange)(statePath, 'FR', 'agent-2');
|
|
256
|
+
assert.strictEqual(range2.start, 51);
|
|
257
|
+
assert.strictEqual(range2.end, 100);
|
|
258
|
+
});
|
|
259
|
+
(0, node_test_1.it)('different ID types have independent ranges', () => {
|
|
260
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
261
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
262
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
263
|
+
const fr = (0, state_js_1.allocateIdRange)(statePath, 'FR', 'agent-1');
|
|
264
|
+
const j = (0, state_js_1.allocateIdRange)(statePath, 'J', 'agent-1');
|
|
265
|
+
assert.strictEqual(fr.start, 1);
|
|
266
|
+
assert.strictEqual(j.start, 1);
|
|
267
|
+
});
|
|
268
|
+
(0, node_test_1.it)('respects custom range size', () => {
|
|
269
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
270
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
271
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
272
|
+
const range = (0, state_js_1.allocateIdRange)(statePath, 'FR', 'agent-1', 25);
|
|
273
|
+
assert.strictEqual(range.start, 1);
|
|
274
|
+
assert.strictEqual(range.end, 25);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
(0, node_test_1.describe)('getIdRanges', () => {
|
|
278
|
+
(0, node_test_1.it)('returns empty object when no registry exists', () => {
|
|
279
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
280
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
281
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
282
|
+
const ranges = (0, state_js_1.getIdRanges)(statePath);
|
|
283
|
+
assert.deepStrictEqual(ranges, {});
|
|
284
|
+
});
|
|
285
|
+
(0, node_test_1.it)('returns all ranges when no type filter', () => {
|
|
286
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
287
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
288
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
289
|
+
(0, state_js_1.allocateIdRange)(statePath, 'FR', 'a1');
|
|
290
|
+
(0, state_js_1.allocateIdRange)(statePath, 'J', 'a1');
|
|
291
|
+
const ranges = (0, state_js_1.getIdRanges)(statePath);
|
|
292
|
+
assert.ok(ranges.FR);
|
|
293
|
+
assert.ok(ranges.J);
|
|
294
|
+
});
|
|
295
|
+
(0, node_test_1.it)('filters by type when specified', () => {
|
|
296
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
297
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
298
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
299
|
+
(0, state_js_1.allocateIdRange)(statePath, 'FR', 'a1');
|
|
300
|
+
(0, state_js_1.allocateIdRange)(statePath, 'J', 'a1');
|
|
301
|
+
const ranges = (0, state_js_1.getIdRanges)(statePath, 'FR');
|
|
302
|
+
assert.ok(ranges.FR);
|
|
303
|
+
assert.strictEqual(ranges.J, undefined);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
(0, node_test_1.describe)('resetIdRegistry', () => {
|
|
307
|
+
(0, node_test_1.it)('clears all ranges', () => {
|
|
308
|
+
const stateDir = path.join(tmpDir, 'gswd');
|
|
309
|
+
(0, state_js_1.initState)(stateDir, 'project');
|
|
310
|
+
const statePath = path.join(stateDir, 'STATE.json');
|
|
311
|
+
(0, state_js_1.allocateIdRange)(statePath, 'FR', 'a1');
|
|
312
|
+
(0, state_js_1.resetIdRegistry)(statePath);
|
|
313
|
+
const ranges = (0, state_js_1.getIdRanges)(statePath);
|
|
314
|
+
assert.deepStrictEqual(ranges, {});
|
|
315
|
+
});
|
|
316
|
+
});
|
package/lib/bootstrap.ts
CHANGED
|
@@ -20,7 +20,8 @@ import { initState, readState, writeState, writeCheckpoint } from './state.js';
|
|
|
20
20
|
import type { StageStatus } from './state.js';
|
|
21
21
|
import { getGswdConfig } from './config.js';
|
|
22
22
|
import type { GswdConfig } from './config.js';
|
|
23
|
-
import { renderBanner, renderCheckpoint, renderNextUp } from './render.js';
|
|
23
|
+
import { renderBanner, renderCheckpoint, renderNextUp, renderFileInventory, renderContextBanner, extractProductContext, STAGE_CONTEXT_TEMPLATES } from './render.js';
|
|
24
|
+
import type { ProductContext } from './render.js';
|
|
24
25
|
import type { AutoDecision } from './imagine-synthesis.js';
|
|
25
26
|
import { runImagine } from './imagine.js';
|
|
26
27
|
import type { ImagineResult } from './imagine.js';
|
|
@@ -330,9 +331,9 @@ export function handleInterrupt(
|
|
|
330
331
|
// Step 2: Render checkpoint box
|
|
331
332
|
const box = renderCheckpoint(
|
|
332
333
|
'Auto Mode Interrupted',
|
|
333
|
-
'
|
|
334
|
+
'Pipeline cannot continue automatically. Resolve:',
|
|
334
335
|
reasons.map(r => r.length > 50 ? r.slice(0, 47) + '...' : r),
|
|
335
|
-
'Resolve issues above, then re-run
|
|
336
|
+
'Resolve issues above, then re-run /gswd:start'
|
|
336
337
|
);
|
|
337
338
|
console.log(box);
|
|
338
339
|
|
|
@@ -446,13 +447,17 @@ export async function runBootstrap(options: BootstrapOptions): Promise<Bootstrap
|
|
|
446
447
|
writeState(statePath, initStateObj);
|
|
447
448
|
}
|
|
448
449
|
|
|
449
|
-
console.log(renderBanner('
|
|
450
|
+
console.log(renderBanner('PIPELINE'));
|
|
451
|
+
|
|
452
|
+
// Extract product context for contextual banners
|
|
453
|
+
const productCtx = extractProductContext(planningDir);
|
|
450
454
|
|
|
451
455
|
// ── IMAGINE ──────────────────────────────────────────────────────────
|
|
452
456
|
const imagineState = readState(statePath)!;
|
|
453
457
|
if (!shouldSkipStage('imagine', imagineState, planningDir, options.resume ?? false)) {
|
|
454
458
|
writeCheckpoint(statePath, 'gswd/bootstrap', 'imagine-start');
|
|
455
|
-
|
|
459
|
+
const imagineCtxLine = STAGE_CONTEXT_TEMPLATES.imagine?.(productCtx) || '';
|
|
460
|
+
console.log(renderContextBanner('IMAGINE', imagineCtxLine));
|
|
456
461
|
// runImagine is ASYNC — MUST await
|
|
457
462
|
const imagineResult: ImagineResult = await runImagine({
|
|
458
463
|
ideaFilePath: options.ideaFilePath,
|
|
@@ -502,7 +507,10 @@ export async function runBootstrap(options: BootstrapOptions): Promise<Bootstrap
|
|
|
502
507
|
const specifyState = readState(statePath)!;
|
|
503
508
|
if (!shouldSkipStage('specify', specifyState, planningDir, options.resume ?? false)) {
|
|
504
509
|
writeCheckpoint(statePath, 'gswd/bootstrap', 'specify-start');
|
|
505
|
-
|
|
510
|
+
// Re-extract after imagine (DECISIONS.md now exists with precise data)
|
|
511
|
+
const specifyCtx = extractProductContext(planningDir);
|
|
512
|
+
const specifyCtxLine = STAGE_CONTEXT_TEMPLATES.specify?.(specifyCtx) || '';
|
|
513
|
+
console.log(renderContextBanner('SPECIFY', specifyCtxLine));
|
|
506
514
|
// runSpecify is ASYNC — MUST await
|
|
507
515
|
// When no spawnFn provided, set skipAgents: true (Pitfall 1)
|
|
508
516
|
const specifyResult: SpecifyResult = await runSpecify({
|
|
@@ -542,7 +550,9 @@ export async function runBootstrap(options: BootstrapOptions): Promise<Bootstrap
|
|
|
542
550
|
const auditState = readState(statePath)!;
|
|
543
551
|
if (!shouldSkipStage('audit', auditState, planningDir, options.resume ?? false)) {
|
|
544
552
|
writeCheckpoint(statePath, 'gswd/bootstrap', 'audit-start');
|
|
545
|
-
|
|
553
|
+
const auditCtx = extractProductContext(planningDir);
|
|
554
|
+
const auditCtxLine = STAGE_CONTEXT_TEMPLATES.audit?.(auditCtx) || '';
|
|
555
|
+
console.log(renderContextBanner('AUDIT', auditCtxLine));
|
|
546
556
|
// runAuditWorkflow is SYNC — no await (Pitfall 7)
|
|
547
557
|
const auditResult: AuditWorkflowResult = runAuditWorkflow({
|
|
548
558
|
planningDir,
|
|
@@ -602,10 +612,26 @@ export async function runBootstrap(options: BootstrapOptions): Promise<Bootstrap
|
|
|
602
612
|
console.log(renderAutoDecisionSummary(allAutoDecisions));
|
|
603
613
|
}
|
|
604
614
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'/
|
|
608
|
-
|
|
615
|
+
// Display dynamic file inventory (matches manual mode output)
|
|
616
|
+
const inventoryFiles = [
|
|
617
|
+
{ path: '.planning/IMAGINE.md', description: 'Product vision, target user, and direction' },
|
|
618
|
+
{ path: '.planning/DECISIONS.md', description: 'Frozen decisions, success metrics, risks' },
|
|
619
|
+
{ path: '.planning/SPEC.md', description: 'Functional requirements with acceptance criteria' },
|
|
620
|
+
{ path: '.planning/NFR.md', description: 'Non-functional requirements with thresholds' },
|
|
621
|
+
{ path: '.planning/JOURNEYS.md', description: 'User journeys with FR and integration linkages' },
|
|
622
|
+
{ path: '.planning/INTEGRATIONS.md', description: 'External integrations with status and fallbacks' },
|
|
623
|
+
{ path: '.planning/PROJECT.md', description: 'GSD project context (compiled)' },
|
|
624
|
+
{ path: '.planning/REQUIREMENTS.md', description: 'GSD requirements with traceability (compiled)' },
|
|
625
|
+
{ path: '.planning/ROADMAP.md', description: 'GSD roadmap with phased journeys (compiled)' },
|
|
626
|
+
{ path: '.planning/STATE.md', description: 'GSD project state and decisions (compiled)' },
|
|
627
|
+
{ path: '.planning/research/gswd/SUMMARY.md', description: 'GSD-format research bridge (compiled)' },
|
|
628
|
+
].filter(f => {
|
|
629
|
+
try { fs.accessSync(path.join(process.cwd(), f.path)); return true; }
|
|
630
|
+
catch { return false; }
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
console.log(renderFileInventory(inventoryFiles));
|
|
634
|
+
console.log('\nNext up:\n /gsd:plan-phase 01\n\nTip:\n If context is getting crowded, run /clear first.');
|
|
609
635
|
|
|
610
636
|
return {
|
|
611
637
|
status: 'done',
|