@rsaglobaltech/specgate 0.17.9 → 0.18.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/CHANGELOG.md +97 -0
- package/README.md +5 -5
- package/bin/specgate.ts +1 -0
- package/dist/bin/specgate.js +1 -0
- package/dist/bin/specgate.js.map +1 -1
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js +5 -1
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/DerivedMatrix.js +32 -0
- package/dist/packages/core/src/domain/DerivedMatrix.js.map +1 -1
- package/dist/packages/core/src/domain/DraftChecklist.js +101 -24
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/PackSpec.js +21 -2
- package/dist/packages/core/src/domain/PackSpec.js.map +1 -1
- package/dist/scripts/agents/commands.js +18 -0
- package/dist/scripts/agents/commands.js.map +1 -1
- package/dist/scripts/agents/init.js +9 -1
- package/dist/scripts/agents/init.js.map +1 -1
- package/dist/scripts/cli/commands/change/ChangeCommand.js +23 -3
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +14 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +20 -0
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +152 -4
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +702 -57
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +41 -2
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/docs/build-site.js +1 -1
- package/dist/scripts/docs/journey-storyboard.js +358 -0
- package/dist/scripts/docs/journey-storyboard.js.map +1 -0
- package/dist/scripts/docs/nav.js +117 -5
- package/dist/scripts/docs/nav.js.map +1 -1
- package/dist/scripts/docs/record-journey.js +376 -0
- package/dist/scripts/docs/record-journey.js.map +1 -0
- package/dist/scripts/docs/record-terminal.js +252 -35
- package/dist/scripts/docs/record-terminal.js.map +1 -1
- package/dist/scripts/docs/terminal-storyboard.js +151 -2
- package/dist/scripts/docs/terminal-storyboard.js.map +1 -1
- package/dist/scripts/draft/brief-setup.js +363 -0
- package/dist/scripts/draft/brief-setup.js.map +1 -0
- package/dist/scripts/draft/confined-agent.js +175 -0
- package/dist/scripts/draft/confined-agent.js.map +1 -0
- package/dist/scripts/draft/drafting-guide.js +145 -0
- package/dist/scripts/draft/drafting-guide.js.map +1 -0
- package/dist/scripts/harness/init.js +33 -0
- package/dist/scripts/harness/init.js.map +1 -1
- package/dist/scripts/lib/surface.js +8 -1
- package/dist/scripts/lib/surface.js.map +1 -1
- package/package.json +3 -1
- package/templates/base/AGENTS.md.tpl +6 -0
- package/templates/harness/prompt-prefix.md.tpl +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `specgate draft
|
|
4
|
-
*
|
|
3
|
+
* `specgate draft` — from a plan or brief to a reviewed specification, one
|
|
4
|
+
* module at a time. (ADR-0029, docs/specs/draft-from-brief.md)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* specgate draft the brief's modules, where each stands, what next
|
|
7
|
+
* specgate draft <module> open the module's draft, or check it
|
|
8
|
+
* specgate draft <module> --agent "claude -p" have an agent write it, then check it
|
|
9
|
+
* specgate draft --check <id> the checklist on any change, as before
|
|
10
|
+
*
|
|
11
|
+
* Most projects start from a plan, and the path from that plan to a draft
|
|
12
|
+
* used to take four commands, a hand edit of `change.yaml` and a prompt the
|
|
13
|
+
* user had to write (#93, #94). `draft <module>` is the one command a person
|
|
14
|
+
* repeats: the first run opens the draft and leaves `DRAFTING.md` for
|
|
15
|
+
* whoever writes it; every run after that checks it and says what is next.
|
|
9
16
|
*/
|
|
10
17
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
18
|
if (k2 === undefined) k2 = k;
|
|
@@ -41,8 +48,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
48
|
};
|
|
42
49
|
})();
|
|
43
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.DraftCommand = void 0;
|
|
51
|
+
exports.DraftCommand = exports.DEFAULT_BRIEF = void 0;
|
|
52
|
+
exports.draftModuleOf = draftModuleOf;
|
|
45
53
|
exports.parseArgs = parseArgs;
|
|
54
|
+
exports.archivedAssumptions = archivedAssumptions;
|
|
55
|
+
exports.briefOf = briefOf;
|
|
56
|
+
exports.draftReport = draftReport;
|
|
57
|
+
exports.moduleStatus = moduleStatus;
|
|
58
|
+
exports.briefProgress = briefProgress;
|
|
59
|
+
exports.draftNextCommand = draftNextCommand;
|
|
60
|
+
exports.pasteLine = pasteLine;
|
|
46
61
|
const fs = __importStar(require("node:fs"));
|
|
47
62
|
const path = __importStar(require("node:path"));
|
|
48
63
|
const command_1 = require("../../../lib/command");
|
|
@@ -50,27 +65,57 @@ const agent_1 = require("../../../lib/agent");
|
|
|
50
65
|
const diagnostics_1 = require("../../../lib/diagnostics");
|
|
51
66
|
const project_root_1 = require("../../../lib/project-root");
|
|
52
67
|
const DraftChecklist_1 = require("../../../../packages/core/src/domain/DraftChecklist");
|
|
68
|
+
const ChangeWorkspace_1 = require("../../../../packages/core/src/infrastructure/ChangeWorkspace");
|
|
69
|
+
const ChangeTemplates_1 = require("../../../../packages/core/src/domain/ChangeTemplates");
|
|
70
|
+
const DiskLanguageRepository_1 = require("../../../../packages/core/src/infrastructure/DiskLanguageRepository");
|
|
71
|
+
const drafting_guide_1 = require("../../../draft/drafting-guide");
|
|
72
|
+
const confined_agent_1 = require("../../../draft/confined-agent");
|
|
73
|
+
const brief_setup_1 = require("../../../draft/brief-setup");
|
|
53
74
|
const GherkinTags_1 = require("../../../../packages/core/src/domain/GherkinTags");
|
|
54
75
|
const DerivedMatrixSources_1 = require("../../../../packages/core/src/infrastructure/DerivedMatrixSources");
|
|
55
76
|
const CHANGES = path.join("docs", "specs", "changes");
|
|
77
|
+
/** Where `specgate draft` looks for the plan when `--brief` is not given. */
|
|
78
|
+
exports.DEFAULT_BRIEF = "docs/brief.md";
|
|
79
|
+
const DEFAULT_RESERVE = 10;
|
|
80
|
+
/** `draft-bookings` → `bookings`; any other change id → `--check <id>`. */
|
|
81
|
+
function draftModuleOf(changeId) {
|
|
82
|
+
return changeId.startsWith("draft-") ? changeId.slice("draft-".length) : `--check ${changeId}`;
|
|
83
|
+
}
|
|
56
84
|
function usage() {
|
|
57
85
|
return ("Usage:\n" +
|
|
58
|
-
" specgate draft
|
|
59
|
-
"
|
|
86
|
+
" specgate draft your brief's modules, and the one to do next\n" +
|
|
87
|
+
" specgate draft <module> open that module's draft — or check it, once it exists\n" +
|
|
88
|
+
' specgate draft <module> --agent "claude -p"\n' +
|
|
89
|
+
" have your agent write it, then check it\n" +
|
|
90
|
+
" specgate draft --check <change-id> the checklist on any change\n" +
|
|
91
|
+
" specgate draft --new-brief write docs/brief.md to fill in, when you have none\n" +
|
|
92
|
+
' specgate draft --agent "claude -p" have your agent propose the brief\'s front matter\n\n' +
|
|
93
|
+
" --brief <file> Your plan or brief (default: docs/brief.md). Any markdown; a front\n" +
|
|
94
|
+
" matter with modules, actors and surfaces lets the checklist do more.\n" +
|
|
95
|
+
" --reserve <n> Requirement ids a new draft reserves (default 10, at most the --max-requirements).\n" +
|
|
96
|
+
" --agent <cmd> Your agent's command. It gets the drafting guide on standard input,\n" +
|
|
97
|
+
" or in a file if the command contains {prompt_file}. Whatever it writes\n" +
|
|
98
|
+
" outside the draft is undone.\n" +
|
|
99
|
+
" [--max-requirements <n>] [--project-dir <dir>] [--json]\n\n" +
|
|
100
|
+
"The checklist (ADR-0029):\n" +
|
|
60
101
|
" D1 every requirement has a scenario D5 a cited law or standard names its source\n" +
|
|
61
102
|
" D2 every requirement has a kind D6 a stated value is in the brief or an assumption\n" +
|
|
62
103
|
" D3 a scenario per surface each actor uses D7 an open question names what it blocks\n" +
|
|
63
104
|
" D4 a non-functional requirement is measured D8 the draft is small enough to review\n" +
|
|
64
105
|
" D9 every scenario passes the harness gate's rules\n" +
|
|
65
|
-
" D10 no id the project already uses\n
|
|
66
|
-
"
|
|
67
|
-
"
|
|
106
|
+
" D10 no id the project already uses\n" +
|
|
107
|
+
" D12 an actor can get the ids it acts on\n\n" +
|
|
108
|
+
"Without a brief, D3 and D6 cannot run, and the report says so.\n");
|
|
68
109
|
}
|
|
69
110
|
function parseArgs(argv) {
|
|
70
111
|
const o = {
|
|
71
112
|
check: "",
|
|
113
|
+
module: "",
|
|
72
114
|
brief: "",
|
|
73
115
|
maxRequirements: 25,
|
|
116
|
+
reserve: DEFAULT_RESERVE,
|
|
117
|
+
agent: "",
|
|
118
|
+
newBrief: false,
|
|
74
119
|
projectDir: ".",
|
|
75
120
|
json: (0, agent_1.wantsJson)(argv),
|
|
76
121
|
};
|
|
@@ -88,6 +133,12 @@ function parseArgs(argv) {
|
|
|
88
133
|
o.brief = value();
|
|
89
134
|
else if (a === "--max-requirements")
|
|
90
135
|
o.maxRequirements = Number(value());
|
|
136
|
+
else if (a === "--reserve")
|
|
137
|
+
o.reserve = Number(value());
|
|
138
|
+
else if (a === "--agent")
|
|
139
|
+
o.agent = value();
|
|
140
|
+
else if (a === "--new-brief")
|
|
141
|
+
o.newBrief = true;
|
|
91
142
|
else if (a === "--project-dir")
|
|
92
143
|
o.projectDir = value();
|
|
93
144
|
else if (a === "--json")
|
|
@@ -96,15 +147,29 @@ function parseArgs(argv) {
|
|
|
96
147
|
process.stdout.write(usage());
|
|
97
148
|
process.exit(0);
|
|
98
149
|
}
|
|
150
|
+
else if (!a.startsWith("-") && !o.module)
|
|
151
|
+
o.module = a;
|
|
99
152
|
else
|
|
100
153
|
throw new Error(`unknown argument ${a}`);
|
|
101
154
|
}
|
|
102
|
-
if (
|
|
103
|
-
throw new Error("--check <change-id
|
|
155
|
+
if (o.check && o.module) {
|
|
156
|
+
throw new Error("give a module or --check <change-id>, not both");
|
|
157
|
+
}
|
|
158
|
+
if (o.module && !ChangeWorkspace_1.CHANGE_ID_RE.test(o.module)) {
|
|
159
|
+
throw new Error(`"${o.module}" is not a module name: use lowercase words and dashes, e.g. bookings`);
|
|
104
160
|
}
|
|
105
161
|
if (!Number.isInteger(o.maxRequirements) || o.maxRequirements < 1) {
|
|
106
162
|
throw new Error("--max-requirements needs a positive integer");
|
|
107
163
|
}
|
|
164
|
+
if (!Number.isInteger(o.reserve) || o.reserve < 1 || o.reserve > o.maxRequirements) {
|
|
165
|
+
throw new Error(`--reserve needs a whole number from 1 to ${o.maxRequirements}`);
|
|
166
|
+
}
|
|
167
|
+
if (o.newBrief && (o.module || o.check || o.agent)) {
|
|
168
|
+
throw new Error("--new-brief only writes the brief: run it on its own");
|
|
169
|
+
}
|
|
170
|
+
if (o.agent && o.check) {
|
|
171
|
+
throw new Error("--agent drafts a module, or proposes the brief's front matter — not --check");
|
|
172
|
+
}
|
|
108
173
|
return o;
|
|
109
174
|
}
|
|
110
175
|
function specFiles(dir, rel = "") {
|
|
@@ -170,6 +235,231 @@ function projectIds(projectDir) {
|
|
|
170
235
|
return owners;
|
|
171
236
|
}
|
|
172
237
|
const readIf = (file) => (fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined);
|
|
238
|
+
/** `assumptions.md` of every archived change, for MODIFIED requirements (#81). */
|
|
239
|
+
function archivedAssumptions(projectDir) {
|
|
240
|
+
const archive = path.join(projectDir, CHANGES, "archive");
|
|
241
|
+
let dirs = [];
|
|
242
|
+
try {
|
|
243
|
+
dirs = fs.readdirSync(archive).sort();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
return dirs
|
|
249
|
+
.map((d) => readIf(path.join(archive, d, "assumptions.md")) || "")
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
/** Whether a change was opened from a brief (`brief:` in change.yaml): a draft. */
|
|
254
|
+
function briefOf(projectDir, changeId) {
|
|
255
|
+
const yaml = readIf(path.join(projectDir, CHANGES, changeId, "change.yaml")) || "";
|
|
256
|
+
const m = /^brief:\s*["']?([^"'\n]+)["']?\s*$/m.exec(yaml);
|
|
257
|
+
return m ? m[1].trim() : undefined;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The checklist on one change, as `draft --check` runs it. Shared with
|
|
261
|
+
* `change archive`, which refuses a draft that does not pass it (#57).
|
|
262
|
+
*/
|
|
263
|
+
function draftReport(projectDir, changeId, briefOption = "", maxRequirements = 25) {
|
|
264
|
+
const changeDir = path.join(projectDir, CHANGES, changeId);
|
|
265
|
+
if (!fs.existsSync(changeDir)) {
|
|
266
|
+
return {
|
|
267
|
+
failure: (0, diagnostics_1.error)("draft_no_change", `No change '${changeId}' under ${CHANGES}/.`, {
|
|
268
|
+
fix: "specgate change list — or open one with `specgate change new <id>`.",
|
|
269
|
+
}),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const specs = path.join(changeDir, "specs");
|
|
273
|
+
const deltas = specFiles(specs).map((rel) => ({
|
|
274
|
+
path: path.join(CHANGES, changeId, "specs", rel).split(path.sep).join("/"),
|
|
275
|
+
source: fs.readFileSync(path.join(specs, rel), "utf8"),
|
|
276
|
+
}));
|
|
277
|
+
if (deltas.length === 0) {
|
|
278
|
+
return {
|
|
279
|
+
failure: (0, diagnostics_1.error)("draft_no_specs", `Change '${changeId}' has no specs/<capability>/spec.md to check.`),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// The brief: --brief, else `brief: <path>` in change.yaml.
|
|
283
|
+
const briefPath = briefOption || briefOf(projectDir, changeId);
|
|
284
|
+
let brief;
|
|
285
|
+
if (briefPath) {
|
|
286
|
+
brief = readIf(path.resolve(projectDir, briefPath));
|
|
287
|
+
if (brief === undefined) {
|
|
288
|
+
return { failure: (0, diagnostics_1.error)("draft_no_brief", `The brief ${briefPath} does not exist.`) };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
report: (0, DraftChecklist_1.checkDraft)({
|
|
293
|
+
deltas,
|
|
294
|
+
brief,
|
|
295
|
+
assumptions: readIf(path.join(changeDir, "assumptions.md")),
|
|
296
|
+
archivedAssumptions: archivedAssumptions(projectDir),
|
|
297
|
+
specText: projectSpecText(projectDir),
|
|
298
|
+
idsInUse: projectIds(projectDir),
|
|
299
|
+
specs: projectSpecs(projectDir),
|
|
300
|
+
questions: readIf(path.join(changeDir, "questions.md")),
|
|
301
|
+
maxRequirements,
|
|
302
|
+
}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** The placeholder `templateDelta` writes: a draft nobody has written yet. */
|
|
306
|
+
const PLACEHOLDER = "feature=features/<area>/<name>.feature";
|
|
307
|
+
function deltaSources(projectDir, changeId) {
|
|
308
|
+
const specs = path.join(projectDir, CHANGES, changeId, "specs");
|
|
309
|
+
return specFiles(specs).map((rel) => fs.readFileSync(path.join(specs, rel), "utf8"));
|
|
310
|
+
}
|
|
311
|
+
function moduleStatus(projectDir, module, brief = "") {
|
|
312
|
+
const change = `draft-${module}`;
|
|
313
|
+
const archived = (0, ChangeWorkspace_1.listArchivedIds)(projectDir).find((id) => id === change || id.endsWith(`-${change}`));
|
|
314
|
+
const base = { module, change, findings: 0 };
|
|
315
|
+
if (!fs.existsSync(path.join(projectDir, CHANGES, change))) {
|
|
316
|
+
return archived
|
|
317
|
+
? { ...base, state: "archived", archivedAs: archived }
|
|
318
|
+
: { ...base, state: "not started" };
|
|
319
|
+
}
|
|
320
|
+
const deltas = deltaSources(projectDir, change);
|
|
321
|
+
if (deltas.length === 0 || deltas.every((d) => d.includes(PLACEHOLDER))) {
|
|
322
|
+
return { ...base, state: "not written" };
|
|
323
|
+
}
|
|
324
|
+
const result = draftReport(projectDir, change, brief);
|
|
325
|
+
if ("failure" in result)
|
|
326
|
+
return { ...base, state: "drafting", findings: 1 };
|
|
327
|
+
const findings = result.report.status.length;
|
|
328
|
+
return { ...base, state: findings === 0 ? "ready" : "drafting", findings };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Where the brief stands, for `status`: its modules, and the first one not
|
|
332
|
+
* yet archived. Null when there is no brief, or it lists no modules.
|
|
333
|
+
*/
|
|
334
|
+
function briefProgress(projectDir) {
|
|
335
|
+
const text = readIf(path.join(projectDir, exports.DEFAULT_BRIEF));
|
|
336
|
+
if (text === undefined)
|
|
337
|
+
return null;
|
|
338
|
+
const modules = (0, DraftChecklist_1.briefModules)(text).map((m) => moduleStatus(projectDir, m));
|
|
339
|
+
if (modules.length === 0)
|
|
340
|
+
return null;
|
|
341
|
+
return { brief: exports.DEFAULT_BRIEF, modules, next: modules.find((m) => m.state !== "archived") };
|
|
342
|
+
}
|
|
343
|
+
/** What to run for this module, given where it stands. */
|
|
344
|
+
function draftNextCommand(s) {
|
|
345
|
+
return s.state === "ready" ? `specgate change archive ${s.change}` : `specgate draft ${s.module}`;
|
|
346
|
+
}
|
|
347
|
+
/** `docs/specs/BRIEF-SETUP.md`: the guide an agent follows to propose the front matter. */
|
|
348
|
+
function writeSetupGuide(projectDir, brief) {
|
|
349
|
+
const file = path.join(projectDir, brief_setup_1.BRIEF_SETUP_FILE);
|
|
350
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
351
|
+
fs.writeFileSync(file, (0, brief_setup_1.briefSetupGuide)(brief), "utf8");
|
|
352
|
+
}
|
|
353
|
+
/** The next free SCN number, past every scenario the project or a change uses. */
|
|
354
|
+
function nextScenario(projectDir) {
|
|
355
|
+
let max = 0;
|
|
356
|
+
for (const id of projectIds(projectDir).keys()) {
|
|
357
|
+
const m = /^SCN-(\d+)$/.exec(id);
|
|
358
|
+
if (m)
|
|
359
|
+
max = Math.max(max, Number(m[1]));
|
|
360
|
+
}
|
|
361
|
+
const walk = (dir) => {
|
|
362
|
+
let entries = [];
|
|
363
|
+
try {
|
|
364
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const e of entries) {
|
|
370
|
+
const full = path.join(dir, e.name);
|
|
371
|
+
if (e.isDirectory())
|
|
372
|
+
walk(full);
|
|
373
|
+
else if (e.name.endsWith(".md") || e.name.endsWith(".feature")) {
|
|
374
|
+
for (const m of fs.readFileSync(full, "utf8").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
375
|
+
max = Math.max(max, Number(m[1]));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
walk(path.join(projectDir, "docs", "specs"));
|
|
381
|
+
walk(path.join(projectDir, "features"));
|
|
382
|
+
for (const m of (readIf(path.join(projectDir, "spec.md")) || "").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
383
|
+
max = Math.max(max, Number(m[1]));
|
|
384
|
+
}
|
|
385
|
+
return `SCN-${String(max + 1).padStart(3, "0")}`;
|
|
386
|
+
}
|
|
387
|
+
/** The one line to paste into any agent. */
|
|
388
|
+
function pasteLine(module) {
|
|
389
|
+
return `Draft the ${module} module: follow ${CHANGES.split(path.sep).join("/")}/draft-${module}/DRAFTING.md`;
|
|
390
|
+
}
|
|
391
|
+
function proposalFor(module, changeId, brief) {
|
|
392
|
+
return [
|
|
393
|
+
`# Proposal: ${changeId}`,
|
|
394
|
+
"",
|
|
395
|
+
"## Intent",
|
|
396
|
+
"",
|
|
397
|
+
`The ${module} module of the brief (\`${brief}\`), written down as requirements and`,
|
|
398
|
+
"scenarios a person can review before anything is built.",
|
|
399
|
+
"",
|
|
400
|
+
"## Scope",
|
|
401
|
+
"",
|
|
402
|
+
`In scope: what the brief says about ${module}.`,
|
|
403
|
+
"",
|
|
404
|
+
"Out of scope: every other module of the brief.",
|
|
405
|
+
"",
|
|
406
|
+
].join("\n");
|
|
407
|
+
}
|
|
408
|
+
/** Open `draft-<module>`: the change, linked to the brief, with its guide. */
|
|
409
|
+
function openDraft(projectDir, module, brief, o) {
|
|
410
|
+
const changeId = `draft-${module}`;
|
|
411
|
+
const dir = path.join(projectDir, CHANGES, changeId);
|
|
412
|
+
const reqRange = (0, ChangeWorkspace_1.reserveReqRange)(projectDir, o.reserve);
|
|
413
|
+
const config = {
|
|
414
|
+
...ChangeWorkspace_1.DEFAULT_CONFIG,
|
|
415
|
+
created: new Date().toISOString().slice(0, 10),
|
|
416
|
+
rigor: "lite",
|
|
417
|
+
req_range: reqRange,
|
|
418
|
+
};
|
|
419
|
+
const briefText = fs.readFileSync(path.resolve(projectDir, brief), "utf8");
|
|
420
|
+
const created = [];
|
|
421
|
+
const write = (rel, contents) => {
|
|
422
|
+
const full = path.join(dir, rel);
|
|
423
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
424
|
+
fs.writeFileSync(full, contents, "utf8");
|
|
425
|
+
created.push(path.join(CHANGES, changeId, rel).split(path.sep).join("/"));
|
|
426
|
+
};
|
|
427
|
+
write("DRAFTING.md", (0, drafting_guide_1.draftingGuide)({
|
|
428
|
+
module,
|
|
429
|
+
changeId,
|
|
430
|
+
brief,
|
|
431
|
+
reqRange,
|
|
432
|
+
firstScenario: nextScenario(projectDir),
|
|
433
|
+
maxRequirements: o.maxRequirements,
|
|
434
|
+
actors: (0, DraftChecklist_1.actorSurfaces)(briefText),
|
|
435
|
+
}));
|
|
436
|
+
write(path.join("specs", module, "spec.md"), (0, ChangeTemplates_1.templateDelta)(module, reqRange[0], (0, DiskLanguageRepository_1.phrases)(projectDir)));
|
|
437
|
+
write("change.yaml", `${(0, ChangeWorkspace_1.renderConfig)(config).trimEnd()}\nbrief: ${brief}\n`);
|
|
438
|
+
write("proposal.md", proposalFor(module, changeId, brief));
|
|
439
|
+
return { changeId, reqRange, created };
|
|
440
|
+
}
|
|
441
|
+
const ICON = {
|
|
442
|
+
"not started": "○",
|
|
443
|
+
"not written": "○",
|
|
444
|
+
drafting: "✎",
|
|
445
|
+
ready: "✔",
|
|
446
|
+
archived: "✔",
|
|
447
|
+
};
|
|
448
|
+
function describe(s) {
|
|
449
|
+
if (s.state === "drafting")
|
|
450
|
+
return `drafting — ${s.findings} finding(s) to fix`;
|
|
451
|
+
if (s.state === "ready")
|
|
452
|
+
return "ready to review";
|
|
453
|
+
if (s.state === "archived")
|
|
454
|
+
return `archived (${s.archivedAs})`;
|
|
455
|
+
if (s.state === "not written")
|
|
456
|
+
return "opened, not written yet";
|
|
457
|
+
return "not started";
|
|
458
|
+
}
|
|
459
|
+
/** What to run after this module, given where it stands. */
|
|
460
|
+
function nextFor(s) {
|
|
461
|
+
return s ? draftNextCommand(s) : "specgate status";
|
|
462
|
+
}
|
|
173
463
|
class DraftCommand extends command_1.BaseCommand {
|
|
174
464
|
execute() {
|
|
175
465
|
const io0 = (0, agent_1.agentIo)((0, agent_1.wantsJson)(this.args));
|
|
@@ -183,59 +473,405 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
183
473
|
]);
|
|
184
474
|
}
|
|
185
475
|
const opts = o;
|
|
476
|
+
if (opts.check)
|
|
477
|
+
return this.check(opts);
|
|
478
|
+
if (opts.module)
|
|
479
|
+
return this.module(opts);
|
|
480
|
+
return this.overview(opts);
|
|
481
|
+
}
|
|
482
|
+
/** The brief's path and text, or the failure that says where to put it. */
|
|
483
|
+
brief(projectDir, opts, linked) {
|
|
484
|
+
const rel = opts.brief || linked || exports.DEFAULT_BRIEF;
|
|
485
|
+
const text = readIf(path.resolve(projectDir, rel));
|
|
486
|
+
if (text !== undefined)
|
|
487
|
+
return { rel, text };
|
|
488
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
489
|
+
io.fail({ draft: null }, [
|
|
490
|
+
(0, diagnostics_1.error)("draft_no_brief", `No plan or brief at ${rel}.`, {
|
|
491
|
+
fix: `Save your plan or brief as ${exports.DEFAULT_BRIEF} — any markdown — then run \`specgate draft\`. ` +
|
|
492
|
+
"Somewhere else: --brief <file>.",
|
|
493
|
+
}),
|
|
494
|
+
]);
|
|
495
|
+
}
|
|
496
|
+
// ── specgate draft ──────────────────────────────────────────────────────────
|
|
497
|
+
overview(opts) {
|
|
186
498
|
const io = (0, agent_1.agentIo)(opts.json);
|
|
187
499
|
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
188
|
-
const
|
|
189
|
-
|
|
500
|
+
const rel = opts.brief || exports.DEFAULT_BRIEF;
|
|
501
|
+
const file = path.resolve(projectDir, rel);
|
|
502
|
+
if (opts.newBrief)
|
|
503
|
+
return this.newBrief(opts, file, rel);
|
|
504
|
+
const text = readIf(file);
|
|
505
|
+
// (a) No brief: what one is, and how to get one.
|
|
506
|
+
if (text === undefined) {
|
|
507
|
+
if (!opts.json) {
|
|
508
|
+
process.stdout.write(`\n${(0, brief_setup_1.noBriefExplanation)(rel)}\n`);
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
io.fail({ draft: null }, [
|
|
513
|
+
(0, diagnostics_1.error)("draft_no_brief", `No plan or brief at ${rel}.`, {
|
|
514
|
+
fix: `Save your plan or brief as ${rel} — any markdown — then run \`specgate draft\`. ` +
|
|
515
|
+
"No plan yet: `specgate draft --new-brief` writes one to fill in. Somewhere else: --brief <file>.",
|
|
516
|
+
}),
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
519
|
+
const brief = { rel, text: text };
|
|
520
|
+
if (opts.agent)
|
|
521
|
+
return this.frontMatterWithAgent(opts, projectDir, brief);
|
|
522
|
+
// (b) No front matter: every field, explained, and the agent to propose it.
|
|
523
|
+
if ((0, brief_setup_1.frontMatter)(brief.text) === null) {
|
|
524
|
+
writeSetupGuide(projectDir, rel);
|
|
525
|
+
io.emit({
|
|
526
|
+
draft: {
|
|
527
|
+
brief: rel,
|
|
528
|
+
frontMatter: null,
|
|
529
|
+
guide: brief_setup_1.BRIEF_SETUP_FILE,
|
|
530
|
+
prompt: brief_setup_1.BRIEF_SETUP_PASTE,
|
|
531
|
+
next: 'specgate draft --agent "claude -p"',
|
|
532
|
+
},
|
|
533
|
+
}, () => process.stdout.write(`\n specgate draft · ${rel}\n\n` +
|
|
534
|
+
(0, brief_setup_1.frontMatterExplanation)(rel) +
|
|
535
|
+
"\n Or let your agent propose it from your brief. Paste this into Claude Code,\n" +
|
|
536
|
+
" Cursor, Codex…:\n\n" +
|
|
537
|
+
` ${brief_setup_1.BRIEF_SETUP_PASTE}\n\n` +
|
|
538
|
+
' or let Specgate call it: specgate draft --agent "claude -p"\n\n' +
|
|
539
|
+
" Either way, review the four fields, then run specgate draft again.\n\n"));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// (c) A front matter with problems: each in plain words, with its fix.
|
|
543
|
+
const problems = (0, brief_setup_1.checkFrontMatter)(brief.text);
|
|
544
|
+
if (problems.length > 0) {
|
|
545
|
+
io.emit({
|
|
546
|
+
draft: { brief: rel, frontMatter: "invalid", next: `edit ${rel}, then specgate draft` },
|
|
547
|
+
status: problems.map((p) => (0, diagnostics_1.error)(p.code, p.message, { fix: p.fix })),
|
|
548
|
+
}, () => {
|
|
549
|
+
const out = process.stdout;
|
|
550
|
+
out.write(`\n specgate draft · ${rel}\n\n`);
|
|
551
|
+
out.write(` The front matter at the top of ${rel} needs ${problems.length} fix(es):\n\n`);
|
|
552
|
+
for (const p of problems)
|
|
553
|
+
out.write(` ✖ ${p.message}\n ${p.fix}\n`);
|
|
554
|
+
out.write(`\n Next fix them in ${rel}, then specgate draft\n\n`);
|
|
555
|
+
});
|
|
556
|
+
process.exitCode = 1;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// (d) Ready: the modules, where each stands, what is next.
|
|
560
|
+
const modules = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
561
|
+
const states = modules.map((m) => moduleStatus(projectDir, m, rel));
|
|
562
|
+
const pending = states.find((s) => s.state !== "archived");
|
|
563
|
+
const next = nextFor(pending);
|
|
564
|
+
const product = /^product:\s*(.+)$/m.exec((0, brief_setup_1.frontMatter)(brief.text) || "");
|
|
565
|
+
io.emit({ draft: { brief: rel, modules: states, next } }, () => {
|
|
566
|
+
const out = process.stdout;
|
|
567
|
+
out.write(`\n specgate draft · ${rel}\n\n`);
|
|
568
|
+
if (product)
|
|
569
|
+
out.write(` Product ${product[1].trim()}\n\n`);
|
|
570
|
+
const width = Math.max(...modules.map((m) => m.length));
|
|
571
|
+
out.write(" Modules\n");
|
|
572
|
+
for (const s of states) {
|
|
573
|
+
out.write(` ${ICON[s.state]} ${s.module.padEnd(width)} ${describe(s)}\n`);
|
|
574
|
+
}
|
|
575
|
+
out.write(pending
|
|
576
|
+
? `\n Next ${next}\n\n`
|
|
577
|
+
: `\n Every module is drafted and archived.\n Next ${next} — what to build first\n\n`);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/** `--new-brief`: the template, never over a brief that exists. */
|
|
581
|
+
newBrief(opts, file, rel) {
|
|
582
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
583
|
+
if (fs.existsSync(file)) {
|
|
190
584
|
io.fail({ draft: null }, [
|
|
191
|
-
(0, diagnostics_1.error)("
|
|
192
|
-
fix: "specgate
|
|
585
|
+
(0, diagnostics_1.error)("draft_brief_exists", `${rel} already exists — nothing was written.`, {
|
|
586
|
+
fix: "Edit it, then run `specgate draft`; it says what the top of the brief still needs.",
|
|
193
587
|
}),
|
|
194
588
|
]);
|
|
195
589
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
590
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
591
|
+
fs.writeFileSync(file, brief_setup_1.BRIEF_TEMPLATE, "utf8");
|
|
592
|
+
io.emit({ draft: { brief: rel, written: true, next: "specgate draft" } }, () => process.stdout.write(`\n ✔ Wrote ${rel} — a brief to fill in.\n\n` +
|
|
593
|
+
" Fill it in, in your own words:\n" +
|
|
594
|
+
" 1. The four fields at the top — product, surfaces, actors, modules.\n" +
|
|
595
|
+
" Each has an example in the comment above it.\n" +
|
|
596
|
+
" 2. The sections below — what the product is, who uses it and how, what\n" +
|
|
597
|
+
" each one must be able to do, the rules and numbers you already know,\n" +
|
|
598
|
+
" what you don't know yet, and what is out of scope.\n\n" +
|
|
599
|
+
" Have a plan or an email already? Paste it under the four fields instead\n" +
|
|
600
|
+
" of the sections — Specgate reads it as it is.\n\n" +
|
|
601
|
+
" Next specgate draft — it checks the four fields and says what is next\n\n"));
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* `draft --agent` without a module: the agent proposes the front matter.
|
|
605
|
+
* It may write only the brief, and the brief below the front matter must
|
|
606
|
+
* come back byte for byte — it is the client's words, and every draft is
|
|
607
|
+
* checked against them.
|
|
608
|
+
*/
|
|
609
|
+
frontMatterWithAgent(opts, projectDir, brief) {
|
|
610
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
611
|
+
if ((0, brief_setup_1.frontMatter)(brief.text) !== null && (0, brief_setup_1.checkFrontMatter)(brief.text).length === 0) {
|
|
612
|
+
const first = (0, DraftChecklist_1.briefModules)(brief.text)[0];
|
|
613
|
+
io.emit({ draft: { brief: brief.rel, frontMatter: "ok", next: `specgate draft ${first}` } }, () => process.stdout.write(`\n The front matter of ${brief.rel} is already there and complete — nothing for the agent to do.\n` +
|
|
614
|
+
` Next specgate draft ${first}\n\n`));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
writeSetupGuide(projectDir, brief.rel);
|
|
618
|
+
const own = path
|
|
619
|
+
.relative(projectDir, path.resolve(projectDir, brief.rel))
|
|
620
|
+
.split(path.sep)
|
|
621
|
+
.join("/");
|
|
622
|
+
if (!opts.json) {
|
|
623
|
+
process.stdout.write(`\n → ${opts.agent} is proposing the front matter of ${brief.rel}…\n\n`);
|
|
624
|
+
}
|
|
625
|
+
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
626
|
+
projectDir,
|
|
627
|
+
changeId: "brief",
|
|
628
|
+
allowed: (rel) => rel === own,
|
|
629
|
+
command: opts.agent,
|
|
630
|
+
prompt: (0, brief_setup_1.briefSetupPrompt)((0, brief_setup_1.briefSetupGuide)(brief.rel)),
|
|
631
|
+
showOutput: !opts.json,
|
|
632
|
+
});
|
|
633
|
+
if (run.problem === "no_git") {
|
|
634
|
+
io.fail({ draft: null }, [
|
|
635
|
+
(0, diagnostics_1.error)("draft_agent_needs_git", "--agent needs a git repository.", {
|
|
636
|
+
fix: "Git is how Specgate finds what the agent wrote and undoes anything else. Run `git init`, " +
|
|
637
|
+
`then try again — or paste this into your agent yourself: ${brief_setup_1.BRIEF_SETUP_PASTE}`,
|
|
638
|
+
}),
|
|
639
|
+
]);
|
|
640
|
+
}
|
|
641
|
+
if (run.reverted.length > 0 && !opts.json) {
|
|
642
|
+
process.stdout.write(`\n ↺ Undone — the agent wrote outside the brief: ${run.reverted.join(", ")}\n`);
|
|
643
|
+
}
|
|
644
|
+
const after = readIf(path.resolve(projectDir, brief.rel)) ?? "";
|
|
645
|
+
if ((0, brief_setup_1.briefBody)(after) !== (0, brief_setup_1.briefBody)(brief.text)) {
|
|
646
|
+
fs.writeFileSync(path.resolve(projectDir, brief.rel), brief.text, "utf8");
|
|
647
|
+
io.fail({ draft: null }, [
|
|
648
|
+
(0, diagnostics_1.error)("draft_brief_body_changed", "The agent changed the brief itself, not only its front matter — undone.", {
|
|
649
|
+
fix: `${brief.rel} is back as it was. Try again, or write the four fields yourself (\`specgate draft\` shows them).`,
|
|
650
|
+
}),
|
|
651
|
+
]);
|
|
652
|
+
}
|
|
653
|
+
if (!run.ok) {
|
|
654
|
+
io.fail({ draft: null }, [
|
|
655
|
+
(0, diagnostics_1.error)("draft_agent_failed", `The agent ${run.detail}.`, {
|
|
656
|
+
fix: `Check the command runs on its own, or paste this into your agent: ${brief_setup_1.BRIEF_SETUP_PASTE}`,
|
|
657
|
+
}),
|
|
658
|
+
]);
|
|
659
|
+
}
|
|
660
|
+
const proposed = (0, brief_setup_1.frontMatter)(after);
|
|
661
|
+
if (proposed === null) {
|
|
202
662
|
io.fail({ draft: null }, [
|
|
203
|
-
(0, diagnostics_1.error)("
|
|
663
|
+
(0, diagnostics_1.error)("draft_no_front_matter", "The agent wrote no front matter.", {
|
|
664
|
+
fix: "Run `specgate draft` to see the four fields, and write them yourself.",
|
|
665
|
+
}),
|
|
204
666
|
]);
|
|
205
667
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
668
|
+
const problems = (0, brief_setup_1.checkFrontMatter)(after);
|
|
669
|
+
io.emit({
|
|
670
|
+
draft: { brief: brief.rel, frontMatter: proposed, next: "specgate draft" },
|
|
671
|
+
status: problems.map((p) => (0, diagnostics_1.error)(p.code, p.message, { fix: p.fix })),
|
|
672
|
+
}, () => {
|
|
673
|
+
const out = process.stdout;
|
|
674
|
+
out.write(`\n The agent proposed this, at the top of ${brief.rel}:\n\n`);
|
|
675
|
+
for (const l of ["---", ...proposed.split("\n"), "---"])
|
|
676
|
+
out.write(` ${l}\n`);
|
|
677
|
+
out.write("\n Your brief below it is unchanged.\n");
|
|
678
|
+
if (problems.length > 0) {
|
|
679
|
+
out.write(`\n It still needs ${problems.length} fix(es):\n`);
|
|
680
|
+
for (const p of problems)
|
|
681
|
+
out.write(` ✖ ${p.message}\n ${p.fix}\n`);
|
|
682
|
+
}
|
|
683
|
+
out.write("\n Review it — change anything — then specgate draft\n\n");
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// ── specgate draft <module> ─────────────────────────────────────────────────
|
|
687
|
+
module(opts) {
|
|
688
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
689
|
+
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
690
|
+
const changeId = `draft-${opts.module}`;
|
|
691
|
+
const status = moduleStatus(projectDir, opts.module);
|
|
692
|
+
if (status.state === "archived") {
|
|
693
|
+
io.emit({
|
|
694
|
+
draft: {
|
|
695
|
+
module: opts.module,
|
|
696
|
+
change: changeId,
|
|
697
|
+
state: "archived",
|
|
698
|
+
next: "specgate draft",
|
|
699
|
+
},
|
|
700
|
+
}, () => process.stdout.write(`\n ✔ ${opts.module} is already drafted and archived (${status.archivedAs}).\n\n` +
|
|
701
|
+
" Next specgate draft — the next module of the brief, or what to build first\n\n"));
|
|
702
|
+
return;
|
|
213
703
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
brief = readIf(path.resolve(projectDir, briefPath));
|
|
217
|
-
if (brief === undefined) {
|
|
704
|
+
if (status.state === "not started") {
|
|
705
|
+
if (!fs.existsSync(path.join(projectDir, "spec.md"))) {
|
|
218
706
|
io.fail({ draft: null }, [
|
|
219
|
-
(0, diagnostics_1.error)("
|
|
707
|
+
(0, diagnostics_1.error)("draft_no_project", "This folder is not a Specgate project yet.", {
|
|
708
|
+
fix: "Run `specgate init` first, then `specgate draft " + opts.module + "`.",
|
|
709
|
+
}),
|
|
220
710
|
]);
|
|
221
711
|
}
|
|
712
|
+
const brief = this.brief(projectDir, opts);
|
|
713
|
+
const listed = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
714
|
+
const opened = openDraft(projectDir, opts.module, brief.rel, opts);
|
|
715
|
+
const notes = listed.length > 0 && !listed.includes(opts.module)
|
|
716
|
+
? [
|
|
717
|
+
(0, diagnostics_1.info)("draft_module_not_in_brief", `${opts.module} is not among the brief's modules (${listed.join(", ")}).`, {
|
|
718
|
+
fix: "Add it to `modules:` in the brief's front matter, so `specgate draft` lists it.",
|
|
719
|
+
}),
|
|
720
|
+
]
|
|
721
|
+
: [];
|
|
722
|
+
if (opts.agent)
|
|
723
|
+
return this.withAgent(opts, projectDir, brief.rel, opened.created);
|
|
724
|
+
io.emit({
|
|
725
|
+
draft: {
|
|
726
|
+
module: opts.module,
|
|
727
|
+
change: opened.changeId,
|
|
728
|
+
state: "not written",
|
|
729
|
+
brief: brief.rel,
|
|
730
|
+
reserved: opened.reqRange,
|
|
731
|
+
created: opened.created,
|
|
732
|
+
prompt: pasteLine(opts.module),
|
|
733
|
+
next: `specgate draft ${opts.module}`,
|
|
734
|
+
},
|
|
735
|
+
status: notes,
|
|
736
|
+
}, () => {
|
|
737
|
+
const out = process.stdout;
|
|
738
|
+
const dir = `${CHANGES.split(path.sep).join("/")}/${opened.changeId}`;
|
|
739
|
+
out.write(`\n ✔ Draft ${opened.changeId} opened, linked to ${brief.rel} ` +
|
|
740
|
+
`(REQ ids ${opened.reqRange[0]}…${opened.reqRange[1]} reserved)\n\n` +
|
|
741
|
+
[
|
|
742
|
+
[`${dir}/DRAFTING.md`, "how to write it"],
|
|
743
|
+
[`${dir}/specs/${opts.module}/spec.md`, "the requirements go here"],
|
|
744
|
+
[`${dir}/change.yaml, proposal.md`, ""],
|
|
745
|
+
]
|
|
746
|
+
.map(([f, why], _i, all) => {
|
|
747
|
+
const w = Math.max(...all.map((r) => r[0].length));
|
|
748
|
+
return ` + ${why ? f.padEnd(w) + " " + why : f}\n`;
|
|
749
|
+
})
|
|
750
|
+
.join("") +
|
|
751
|
+
"\n" +
|
|
752
|
+
" Have your agent write it. Paste this into Claude Code, Cursor, Codex…:\n\n" +
|
|
753
|
+
` ${pasteLine(opts.module)}\n\n` +
|
|
754
|
+
` or let Specgate call it: specgate draft ${opts.module} --agent "claude -p"\n\n` +
|
|
755
|
+
` Then run specgate draft ${opts.module} until it says Ready to review.\n\n`);
|
|
756
|
+
for (const n of notes)
|
|
757
|
+
out.write(` ℹ ${n.message}\n ${n.fix}\n\n`);
|
|
758
|
+
});
|
|
759
|
+
return;
|
|
222
760
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
brief,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
761
|
+
const brief = this.brief(projectDir, opts, briefOf(projectDir, changeId));
|
|
762
|
+
if (opts.agent)
|
|
763
|
+
return this.withAgent(opts, projectDir, brief.rel, []);
|
|
764
|
+
this.report(opts, projectDir, brief.rel);
|
|
765
|
+
}
|
|
766
|
+
/** Hand the guide to the agent, undo what it writes elsewhere, then check. */
|
|
767
|
+
withAgent(opts, projectDir, brief, created) {
|
|
768
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
769
|
+
const changeId = `draft-${opts.module}`;
|
|
770
|
+
const guide = readIf(path.join(projectDir, CHANGES, changeId, "DRAFTING.md")) || "";
|
|
771
|
+
if (!opts.json) {
|
|
772
|
+
process.stdout.write(`\n ${created.length ? `✔ Draft ${changeId} opened, linked to ${brief}.\n ` : ""}` +
|
|
773
|
+
`→ ${opts.agent} is drafting ${opts.module}…\n\n`);
|
|
774
|
+
}
|
|
775
|
+
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
776
|
+
projectDir,
|
|
777
|
+
changeId,
|
|
778
|
+
command: opts.agent,
|
|
779
|
+
prompt: (0, drafting_guide_1.draftingPrompt)(opts.module, changeId, guide),
|
|
780
|
+
showOutput: !opts.json,
|
|
232
781
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
782
|
+
if (run.problem === "no_git") {
|
|
783
|
+
io.fail({ draft: null }, [
|
|
784
|
+
(0, diagnostics_1.error)("draft_agent_needs_git", "--agent needs a git repository.", {
|
|
785
|
+
fix: "Git is how Specgate finds what the agent wrote and undoes anything outside the draft. " +
|
|
786
|
+
"Run `git init`, then `specgate draft " +
|
|
787
|
+
opts.module +
|
|
788
|
+
' --agent "…"` again — or paste the prompt into your agent yourself.',
|
|
789
|
+
}),
|
|
790
|
+
]);
|
|
791
|
+
}
|
|
792
|
+
if (run.reverted.length > 0 && !opts.json) {
|
|
793
|
+
process.stdout.write(`\n ↺ Undone — the agent wrote outside the draft: ${run.reverted.join(", ")}\n`);
|
|
794
|
+
}
|
|
795
|
+
if (!run.ok) {
|
|
796
|
+
io.fail({ draft: null }, [
|
|
797
|
+
(0, diagnostics_1.error)("draft_agent_failed", `The agent ${run.detail}.`, {
|
|
798
|
+
fix: `Check the command runs on its own, or paste this into your agent: ${pasteLine(opts.module)}`,
|
|
799
|
+
}),
|
|
800
|
+
]);
|
|
801
|
+
}
|
|
802
|
+
this.report(opts, projectDir, brief, run.reverted);
|
|
803
|
+
}
|
|
804
|
+
/** The checklist on `draft-<module>`, and what comes next. */
|
|
805
|
+
report(opts, projectDir, brief, reverted = []) {
|
|
806
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
807
|
+
const changeId = `draft-${opts.module}`;
|
|
808
|
+
const status = moduleStatus(projectDir, opts.module, brief);
|
|
809
|
+
if (status.state === "not written") {
|
|
810
|
+
io.emit({
|
|
811
|
+
draft: {
|
|
812
|
+
module: opts.module,
|
|
813
|
+
change: changeId,
|
|
814
|
+
state: status.state,
|
|
815
|
+
brief,
|
|
816
|
+
prompt: pasteLine(opts.module),
|
|
817
|
+
next: `specgate draft ${opts.module}`,
|
|
818
|
+
},
|
|
819
|
+
}, () => process.stdout.write(`\n specgate draft ${opts.module} · ${brief}\n\n` +
|
|
820
|
+
" The draft is still the placeholder. Have your agent write it:\n\n" +
|
|
821
|
+
` ${pasteLine(opts.module)}\n\n` +
|
|
822
|
+
` or: specgate draft ${opts.module} --agent "claude -p"\n\n`));
|
|
823
|
+
process.exitCode = 1;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const result = draftReport(projectDir, changeId, brief, opts.maxRequirements);
|
|
827
|
+
if ("failure" in result) {
|
|
828
|
+
io.fail({ draft: null }, [result.failure]);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const report = result.report;
|
|
832
|
+
const diags = [...report.status, ...skippedNotes(report)];
|
|
833
|
+
const errors = report.status.length;
|
|
834
|
+
const next = errors === 0 ? `specgate change archive ${changeId}` : `specgate draft ${opts.module}`;
|
|
835
|
+
io.emit({
|
|
836
|
+
draft: {
|
|
837
|
+
module: opts.module,
|
|
838
|
+
change: changeId,
|
|
839
|
+
state: errors === 0 ? "ready" : "drafting",
|
|
840
|
+
brief,
|
|
841
|
+
requirements: report.requirements,
|
|
842
|
+
scenarios: report.scenarios,
|
|
843
|
+
skipped: report.skipped,
|
|
844
|
+
reverted,
|
|
845
|
+
next,
|
|
846
|
+
},
|
|
847
|
+
status: diags,
|
|
848
|
+
}, () => {
|
|
849
|
+
const out = process.stdout;
|
|
850
|
+
out.write(`\n specgate draft ${opts.module} · ${brief}\n\n`);
|
|
851
|
+
writeFindings(report, diags);
|
|
852
|
+
if (errors === 0) {
|
|
853
|
+
out.write(`${diags.length ? "\n" : ""} ✔ Ready to review.\n\n` +
|
|
854
|
+
` Next read it with whoever knows the business, answer what you can in\n` +
|
|
855
|
+
` questions.md, then ${next}\n\n`);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
out.write(`\n ✖ ${errors} finding(s) to fix. Each says how; DRAFTING.md has the formats.\n` +
|
|
859
|
+
` Next fix them (or ask your agent to), then ${next}\n\n`);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
process.exitCode = errors === 0 ? 0 : 1;
|
|
863
|
+
}
|
|
864
|
+
// ── specgate draft --check <id> ─────────────────────────────────────────────
|
|
865
|
+
check(opts) {
|
|
866
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
867
|
+
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
868
|
+
const result = draftReport(projectDir, opts.check, opts.brief, opts.maxRequirements);
|
|
869
|
+
if ("failure" in result) {
|
|
870
|
+
io.fail({ draft: null }, [result.failure]);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const report = result.report;
|
|
874
|
+
const status = [...report.status, ...skippedNotes(report)];
|
|
239
875
|
const errors = report.status.length;
|
|
240
876
|
io.emit({
|
|
241
877
|
draft: {
|
|
@@ -248,12 +884,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
248
884
|
}, () => {
|
|
249
885
|
const out = process.stdout;
|
|
250
886
|
out.write(`\n specgate draft --check ${opts.check}\n\n`);
|
|
251
|
-
|
|
252
|
-
for (const d of status) {
|
|
253
|
-
out.write(` ${d.severity === "error" ? "✖" : "ℹ"} ${d.code} ${d.message}\n`);
|
|
254
|
-
if (d.fix && d.severity === "error")
|
|
255
|
-
out.write(` fix: ${d.fix}\n`);
|
|
256
|
-
}
|
|
887
|
+
writeFindings(report, status);
|
|
257
888
|
const skipped = report.skipped.length;
|
|
258
889
|
out.write(errors === 0
|
|
259
890
|
? `\n ✔ Ready to review${skipped ? ` (${skipped} rule(s) did not run — see above)` : ""}.\n`
|
|
@@ -263,4 +894,18 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
263
894
|
}
|
|
264
895
|
}
|
|
265
896
|
exports.DraftCommand = DraftCommand;
|
|
897
|
+
function skippedNotes(report) {
|
|
898
|
+
return report.skipped.map((s) => (0, diagnostics_1.info)("draft_rule_skipped", `${s.rule} did not run: ${s.why}.`, {
|
|
899
|
+
fix: "Pass --brief <file>, with actors and their surfaces in its front matter for D3.",
|
|
900
|
+
}));
|
|
901
|
+
}
|
|
902
|
+
function writeFindings(report, status) {
|
|
903
|
+
const out = process.stdout;
|
|
904
|
+
out.write(` ${report.requirements} requirement(s), ${report.scenarios} scenario(s)\n\n`);
|
|
905
|
+
for (const d of status) {
|
|
906
|
+
out.write(` ${d.severity === "error" ? "✖" : "ℹ"} ${d.code} ${d.message}\n`);
|
|
907
|
+
if (d.fix && d.severity === "error")
|
|
908
|
+
out.write(` fix: ${d.fix}\n`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
266
911
|
//# sourceMappingURL=DraftCommand.js.map
|