@rsaglobaltech/specgate 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/README.md +81 -96
- package/dist/packages/core/src/domain/BriefCards.js +320 -0
- package/dist/packages/core/src/domain/BriefCards.js.map +1 -0
- package/dist/packages/core/src/domain/DraftChecklist.js +5 -1
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/QuotedText.js +5 -0
- package/dist/packages/core/src/domain/QuotedText.js.map +1 -1
- package/dist/packages/core/src/domain/TicketText.js +337 -0
- package/dist/packages/core/src/domain/TicketText.js.map +1 -0
- package/dist/packages/core/src/infrastructure/ChangeWorkspace.js +7 -3
- package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -1
- package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js +48 -0
- package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js.map +1 -0
- package/dist/packages/mcp-spec-driven/src/native-tools.js +1 -1
- package/dist/packages/mcp-spec-driven/src/native-tools.js.map +1 -1
- package/dist/scripts/agents/commands.js +8 -6
- package/dist/scripts/agents/commands.js.map +1 -1
- package/dist/scripts/agents/contract-file.js +44 -0
- package/dist/scripts/agents/contract-file.js.map +1 -1
- package/dist/scripts/alm/cli.js +16 -4
- package/dist/scripts/alm/cli.js.map +1 -1
- package/dist/scripts/alm/port.js +1 -1
- package/dist/scripts/alm/port.js.map +1 -1
- package/dist/scripts/alm/providers/http-client.js +46 -3
- package/dist/scripts/alm/providers/http-client.js.map +1 -1
- package/dist/scripts/alm/providers/jira.js +108 -32
- package/dist/scripts/alm/providers/jira.js.map +1 -1
- package/dist/scripts/cli/commands/alm/PrCommand.js +198 -0
- package/dist/scripts/cli/commands/alm/PrCommand.js.map +1 -0
- package/dist/scripts/cli/commands/change/ChangeCommand.js +32 -6
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/change/TicketCommand.js +657 -0
- package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -0
- package/dist/scripts/cli/commands/harness/RunCommand.js +4 -2
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +58 -0
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/UpdateCommand.js +4 -0
- package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +12 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +240 -27
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ExploreCommand.js +13 -13
- package/dist/scripts/cli/commands/spec/ExploreCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +18 -6
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/docs/journey-storyboard.js +67 -44
- package/dist/scripts/docs/journey-storyboard.js.map +1 -1
- package/dist/scripts/docs/nav.js +156 -139
- package/dist/scripts/docs/nav.js.map +1 -1
- package/dist/scripts/docs/record-journey.js +139 -49
- package/dist/scripts/docs/record-journey.js.map +1 -1
- package/dist/scripts/docs/terminal-storyboard.js +15 -11
- package/dist/scripts/docs/terminal-storyboard.js.map +1 -1
- package/dist/scripts/draft/brief-check.js +34 -0
- package/dist/scripts/draft/brief-check.js.map +1 -0
- package/dist/scripts/draft/brief-setup.js +182 -17
- package/dist/scripts/draft/brief-setup.js.map +1 -1
- package/dist/scripts/draft/drafting-guide.js +43 -1
- package/dist/scripts/draft/drafting-guide.js.map +1 -1
- package/dist/scripts/draft/ticket-guide.js +129 -0
- package/dist/scripts/draft/ticket-guide.js.map +1 -0
- package/dist/scripts/lib/surface.js +2 -0
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/lib/ticket-keys.js +140 -0
- package/dist/scripts/lib/ticket-keys.js.map +1 -0
- package/dist/scripts/propose.js +8 -2
- package/dist/scripts/propose.js.map +1 -1
- package/dist/scripts/vcs/github-pr.js +88 -0
- package/dist/scripts/vcs/github-pr.js.map +1 -0
- package/package.json +1 -1
- package/templates/base/AGENTS.md.tpl +6 -5
|
@@ -57,6 +57,9 @@ exports.draftReport = draftReport;
|
|
|
57
57
|
exports.moduleStatus = moduleStatus;
|
|
58
58
|
exports.briefProgress = briefProgress;
|
|
59
59
|
exports.draftNextCommand = draftNextCommand;
|
|
60
|
+
exports.openFindings = openFindings;
|
|
61
|
+
exports.nextScenario = nextScenario;
|
|
62
|
+
exports.reserveScenarioRange = reserveScenarioRange;
|
|
60
63
|
exports.pasteLine = pasteLine;
|
|
61
64
|
const fs = __importStar(require("node:fs"));
|
|
62
65
|
const path = __importStar(require("node:path"));
|
|
@@ -69,10 +72,13 @@ const project_root_1 = require("../../../lib/project-root");
|
|
|
69
72
|
const DraftChecklist_1 = require("../../../../packages/core/src/domain/DraftChecklist");
|
|
70
73
|
const ChangeWorkspace_1 = require("../../../../packages/core/src/infrastructure/ChangeWorkspace");
|
|
71
74
|
const ChangeTemplates_1 = require("../../../../packages/core/src/domain/ChangeTemplates");
|
|
75
|
+
const IdsAcrossBranches_1 = require("../../../../packages/core/src/infrastructure/IdsAcrossBranches");
|
|
72
76
|
const DiskLanguageRepository_1 = require("../../../../packages/core/src/infrastructure/DiskLanguageRepository");
|
|
73
77
|
const drafting_guide_1 = require("../../../draft/drafting-guide");
|
|
74
78
|
const confined_agent_1 = require("../../../draft/confined-agent");
|
|
75
79
|
const brief_setup_1 = require("../../../draft/brief-setup");
|
|
80
|
+
const brief_check_1 = require("../../../draft/brief-check");
|
|
81
|
+
const BriefCards_1 = require("../../../../packages/core/src/domain/BriefCards");
|
|
76
82
|
const GherkinTags_1 = require("../../../../packages/core/src/domain/GherkinTags");
|
|
77
83
|
const DerivedMatrixSources_1 = require("../../../../packages/core/src/infrastructure/DerivedMatrixSources");
|
|
78
84
|
const CHANGES = path.join("docs", "specs", "changes");
|
|
@@ -90,6 +96,7 @@ function usage() {
|
|
|
90
96
|
" specgate draft <module> open that module's draft — or check it, once it exists\n" +
|
|
91
97
|
' specgate draft <module> --agent "claude -p"\n' +
|
|
92
98
|
" have your agent write it, then check it\n" +
|
|
99
|
+
" specgate draft --check the brief rules (B1–B11) on the brief, before drafting\n" +
|
|
93
100
|
" specgate draft --check <change-id> the checklist on any change\n" +
|
|
94
101
|
" specgate draft --new-brief write docs/brief.md to fill in, when you have none\n" +
|
|
95
102
|
' specgate draft --agent "claude -p" have your agent propose the brief\'s front matter\n\n' +
|
|
@@ -108,11 +115,20 @@ function usage() {
|
|
|
108
115
|
" D9 every scenario passes the harness gate's rules\n" +
|
|
109
116
|
" D10 no id the project already uses\n" +
|
|
110
117
|
" D12 an actor can get the ids it acts on\n\n" +
|
|
111
|
-
"Without a brief, D3 and D6 cannot run, and the report says so.\n"
|
|
118
|
+
"Without a brief, D3 and D6 cannot run, and the report says so.\n\n" +
|
|
119
|
+
"The brief rules (ADR-0038) — errors stop a draft, warnings are for you:\n" +
|
|
120
|
+
" B1 front matter fields in the shape Specgate reads B5 a vague word in a rule\n" +
|
|
121
|
+
" B2 the brief, and each module section, says something B6 a promised quantity with no number\n" +
|
|
122
|
+
" B3 a section per module, a module per section B7 a framework or database in a rule\n" +
|
|
123
|
+
" B4 every module has rules, and examples B8 more than 7 rules or 3 questions\n" +
|
|
124
|
+
" B9 a declared actor never mentioned\n" +
|
|
125
|
+
" B10 a rule that names a non-goal\n" +
|
|
126
|
+
" B11 a condition after the rule (EARS)\n");
|
|
112
127
|
}
|
|
113
128
|
function parseArgs(argv) {
|
|
114
129
|
const o = {
|
|
115
130
|
check: "",
|
|
131
|
+
checkBrief: false,
|
|
116
132
|
module: "",
|
|
117
133
|
brief: "",
|
|
118
134
|
maxRequirements: 25,
|
|
@@ -130,8 +146,14 @@ function parseArgs(argv) {
|
|
|
130
146
|
throw new Error(`${a} needs a value`);
|
|
131
147
|
return v;
|
|
132
148
|
};
|
|
133
|
-
if (a === "--check")
|
|
134
|
-
|
|
149
|
+
if (a === "--check") {
|
|
150
|
+
// `--check <change-id>` checks a draft; `--check` alone, the brief.
|
|
151
|
+
const next = argv[i + 1];
|
|
152
|
+
if (next === undefined || next.startsWith("-"))
|
|
153
|
+
o.checkBrief = true;
|
|
154
|
+
else
|
|
155
|
+
o.check = value();
|
|
156
|
+
}
|
|
135
157
|
else if (a === "--brief")
|
|
136
158
|
o.brief = value();
|
|
137
159
|
else if (a === "--max-requirements")
|
|
@@ -158,6 +180,9 @@ function parseArgs(argv) {
|
|
|
158
180
|
if (o.check && o.module) {
|
|
159
181
|
throw new Error("give a module or --check <change-id>, not both");
|
|
160
182
|
}
|
|
183
|
+
if (o.checkBrief && (o.module || o.agent || o.newBrief)) {
|
|
184
|
+
throw new Error("--check only checks the brief: run it on its own");
|
|
185
|
+
}
|
|
161
186
|
if (o.module && !ChangeWorkspace_1.CHANGE_ID_RE.test(o.module)) {
|
|
162
187
|
throw new Error(`"${o.module}" is not a module name: use lowercase words and dashes, e.g. bookings`);
|
|
163
188
|
}
|
|
@@ -428,7 +453,8 @@ function nextScenario(projectDir) {
|
|
|
428
453
|
const full = path.join(dir, e.name);
|
|
429
454
|
if (e.isDirectory())
|
|
430
455
|
walk(full);
|
|
431
|
-
|
|
456
|
+
// change.yaml holds a change's reserved scenario range (REQ-157).
|
|
457
|
+
else if (e.name.endsWith(".md") || e.name.endsWith(".feature") || e.name === "change.yaml") {
|
|
432
458
|
for (const m of fs.readFileSync(full, "utf8").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
433
459
|
max = Math.max(max, Number(m[1]));
|
|
434
460
|
}
|
|
@@ -437,11 +463,19 @@ function nextScenario(projectDir) {
|
|
|
437
463
|
};
|
|
438
464
|
walk(path.join(projectDir, "docs", "specs"));
|
|
439
465
|
walk(path.join(projectDir, "features"));
|
|
466
|
+
max = Math.max(max, (0, IdsAcrossBranches_1.highestOnOtherBranches)(projectDir, "SCN"));
|
|
440
467
|
for (const m of (readIf(path.join(projectDir, "spec.md")) || "").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
441
468
|
max = Math.max(max, Number(m[1]));
|
|
442
469
|
}
|
|
443
470
|
return `SCN-${String(max + 1).padStart(3, "0")}`;
|
|
444
471
|
}
|
|
472
|
+
/** Three scenario ids per requirement id, from the next free one (REQ-157). */
|
|
473
|
+
function reserveScenarioRange(projectDir, reqRange) {
|
|
474
|
+
const first = nextScenario(projectDir);
|
|
475
|
+
const reqs = Number(reqRange[1].slice(4)) - Number(reqRange[0].slice(4)) + 1;
|
|
476
|
+
const last = Number(first.slice(4)) + Math.max(1, reqs) * 3 - 1;
|
|
477
|
+
return [first, `SCN-${String(last).padStart(3, "0")}`];
|
|
478
|
+
}
|
|
445
479
|
/** The one line to paste into any agent. */
|
|
446
480
|
function pasteLine(module) {
|
|
447
481
|
return `Draft the ${module} module: follow ${CHANGES.split(path.sep).join("/")}/draft-${module}/DRAFTING.md`;
|
|
@@ -463,11 +497,23 @@ function proposalFor(module, changeId, brief) {
|
|
|
463
497
|
"",
|
|
464
498
|
].join("\n");
|
|
465
499
|
}
|
|
500
|
+
/** The module's section of the brief, for `DRAFTING.md`; undefined when it has none. */
|
|
501
|
+
function moduleCards(briefText, module) {
|
|
502
|
+
const section = (0, BriefCards_1.moduleSection)(briefText, module);
|
|
503
|
+
if (!section)
|
|
504
|
+
return undefined;
|
|
505
|
+
return {
|
|
506
|
+
source: section.source,
|
|
507
|
+
values: (0, BriefCards_1.cardValues)(section),
|
|
508
|
+
questions: section.questions.map((q) => q.text),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
466
511
|
/** Open `draft-<module>`: the change, linked to the brief, with its guide. */
|
|
467
512
|
function openDraft(projectDir, module, brief, o) {
|
|
468
513
|
const changeId = `draft-${module}`;
|
|
469
514
|
const dir = path.join(projectDir, CHANGES, changeId);
|
|
470
515
|
const reqRange = (0, ChangeWorkspace_1.reserveReqRange)(projectDir, o.reserve);
|
|
516
|
+
const scnRange = reserveScenarioRange(projectDir, reqRange);
|
|
471
517
|
const config = {
|
|
472
518
|
...ChangeWorkspace_1.DEFAULT_CONFIG,
|
|
473
519
|
created: new Date().toISOString().slice(0, 10),
|
|
@@ -487,12 +533,14 @@ function openDraft(projectDir, module, brief, o) {
|
|
|
487
533
|
changeId,
|
|
488
534
|
brief,
|
|
489
535
|
reqRange,
|
|
490
|
-
firstScenario:
|
|
536
|
+
firstScenario: scnRange[0],
|
|
537
|
+
lastScenario: scnRange[1],
|
|
491
538
|
maxRequirements: o.maxRequirements,
|
|
492
539
|
actors: (0, DraftChecklist_1.actorSurfaces)(briefText),
|
|
540
|
+
cards: moduleCards(briefText, module),
|
|
493
541
|
}));
|
|
494
542
|
write(path.join("specs", module, "spec.md"), (0, ChangeTemplates_1.templateDelta)(module, reqRange[0], (0, DiskLanguageRepository_1.phrases)(projectDir)));
|
|
495
|
-
write("change.yaml", `${(0, ChangeWorkspace_1.renderConfig)(config).trimEnd()}\nbrief: ${brief}\n`);
|
|
543
|
+
write("change.yaml", `${(0, ChangeWorkspace_1.renderConfig)(config).trimEnd()}\nscn_range: [${scnRange[0]}, ${scnRange[1]}]\nbrief: ${brief}\n`);
|
|
496
544
|
write("proposal.md", proposalFor(module, changeId, brief));
|
|
497
545
|
return { changeId, reqRange, created };
|
|
498
546
|
}
|
|
@@ -519,6 +567,12 @@ function nextFor(s) {
|
|
|
519
567
|
return s ? draftNextCommand(s) : "specgate status";
|
|
520
568
|
}
|
|
521
569
|
class DraftCommand extends command_1.BaseCommand {
|
|
570
|
+
/** How the person called it, for the header: `draft`, or `propose --from-brief`. */
|
|
571
|
+
verb;
|
|
572
|
+
constructor(args = [], verb = "draft") {
|
|
573
|
+
super(args);
|
|
574
|
+
this.verb = verb;
|
|
575
|
+
}
|
|
522
576
|
execute() {
|
|
523
577
|
const io0 = (0, agent_1.agentIo)((0, agent_1.wantsJson)(this.args));
|
|
524
578
|
let o;
|
|
@@ -532,6 +586,8 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
532
586
|
]);
|
|
533
587
|
}
|
|
534
588
|
const opts = o;
|
|
589
|
+
if (opts.checkBrief)
|
|
590
|
+
return this.checkBrief(opts);
|
|
535
591
|
if (opts.check)
|
|
536
592
|
return this.check(opts);
|
|
537
593
|
if (opts.module)
|
|
@@ -564,7 +620,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
564
620
|
// (a) No brief: what one is, and how to get one.
|
|
565
621
|
if (text === undefined) {
|
|
566
622
|
if (!opts.json) {
|
|
567
|
-
process.stdout.write(ui.header(
|
|
623
|
+
process.stdout.write(ui.header(this.verb) + `${(0, brief_setup_1.noBriefExplanation)(rel)}\n`);
|
|
568
624
|
process.exitCode = 1;
|
|
569
625
|
return;
|
|
570
626
|
}
|
|
@@ -589,7 +645,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
589
645
|
prompt: brief_setup_1.BRIEF_SETUP_PASTE,
|
|
590
646
|
next: 'specgate propose --from-brief --agent "claude -p"',
|
|
591
647
|
},
|
|
592
|
-
}, () => process.stdout.write(ui.header(
|
|
648
|
+
}, () => process.stdout.write(ui.header(this.verb, rel) +
|
|
593
649
|
(0, brief_setup_1.frontMatterExplanation)(rel) +
|
|
594
650
|
"\n" +
|
|
595
651
|
ui.section("Or let your agent propose it") +
|
|
@@ -610,7 +666,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
610
666
|
status: problems.map((p) => (0, diagnostics_1.error)(p.code, p.message, { fix: p.fix })),
|
|
611
667
|
}, () => {
|
|
612
668
|
const out = process.stdout;
|
|
613
|
-
out.write(ui.header(
|
|
669
|
+
out.write(ui.header(this.verb, rel));
|
|
614
670
|
out.write(` The front matter at the top of ${rel} needs ${problems.length} fix(es):\n\n`);
|
|
615
671
|
for (const p of problems)
|
|
616
672
|
out.write(ui.fail(p.message) + ui.fix(p.fix));
|
|
@@ -619,21 +675,43 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
619
675
|
process.exitCode = 1;
|
|
620
676
|
return;
|
|
621
677
|
}
|
|
678
|
+
// (c') The body: an empty brief, a module without its section or cards.
|
|
679
|
+
const findings = (0, brief_check_1.checkBrief)(brief.text, rel);
|
|
680
|
+
const errors = findings.filter((d) => d.severity === "error");
|
|
681
|
+
if (errors.length > 0) {
|
|
682
|
+
io.emit({
|
|
683
|
+
draft: { brief: rel, ready: false, next: "specgate propose --from-brief --check" },
|
|
684
|
+
status: findings,
|
|
685
|
+
}, () => {
|
|
686
|
+
const out = process.stdout;
|
|
687
|
+
out.write(ui.header(this.verb, rel));
|
|
688
|
+
out.write(` ${rel} needs ${errors.length} fix(es) before a module is drafted:\n\n`);
|
|
689
|
+
writeBriefFindings(errors);
|
|
690
|
+
out.write(ui.next("specgate propose --from-brief --check", `once they are fixed in ${rel}`));
|
|
691
|
+
});
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const warnings = findings.filter((d) => d.severity === "warning").length;
|
|
622
696
|
// (d) Ready: the modules, where each stands, what is next.
|
|
623
697
|
const modules = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
624
698
|
const states = modules.map((m) => moduleStatus(projectDir, m, rel));
|
|
625
699
|
const pending = states.find((s) => s.state !== "archived");
|
|
626
700
|
const next = nextFor(pending);
|
|
627
701
|
const product = /^product:\s*(.+)$/m.exec((0, brief_setup_1.frontMatter)(brief.text) || "");
|
|
628
|
-
io.emit({ draft: { brief: rel, modules: states, next } }, () => {
|
|
702
|
+
io.emit({ draft: { brief: rel, modules: states, next }, status: findings }, () => {
|
|
629
703
|
const out = process.stdout;
|
|
630
|
-
out.write(ui.header(
|
|
704
|
+
out.write(ui.header(this.verb, rel));
|
|
631
705
|
if (product)
|
|
632
706
|
out.write(ui.kv("Product", ui.bold(product[1].trim()), 10));
|
|
633
707
|
const archived = states.filter((s) => s.state === "archived").length;
|
|
634
708
|
out.write(ui.kv("Progress", ui.progress(archived, states.length, "archived"), 10));
|
|
635
709
|
out.write("\n" + ui.section("Modules"));
|
|
636
710
|
out.write(ui.table(states.map((s) => [`${ICON[s.state]()} ${s.module}`, ui.muted(describe(s))])));
|
|
711
|
+
if (warnings > 0) {
|
|
712
|
+
out.write("\n" +
|
|
713
|
+
ui.info(`${warnings} warning(s) on the brief's wording ${ui.muted("— specgate propose --from-brief --check")}`));
|
|
714
|
+
}
|
|
637
715
|
out.write(pending
|
|
638
716
|
? ui.next(next)
|
|
639
717
|
: "\n" +
|
|
@@ -641,6 +719,37 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
641
719
|
ui.next(next, "what to build first"));
|
|
642
720
|
});
|
|
643
721
|
}
|
|
722
|
+
/** `--check`: the brief rules on the brief, writing nothing (ADR-0038). */
|
|
723
|
+
checkBrief(opts) {
|
|
724
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
725
|
+
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
726
|
+
const brief = this.brief(projectDir, opts);
|
|
727
|
+
const findings = (0, brief_check_1.checkBrief)(brief.text, brief.rel);
|
|
728
|
+
const errors = findings.filter((d) => d.severity === "error").length;
|
|
729
|
+
const warnings = findings.filter((d) => d.severity === "warning").length;
|
|
730
|
+
const first = (0, DraftChecklist_1.briefModules)(brief.text).find((m) => moduleStatus(projectDir, m, brief.rel).state !== "archived");
|
|
731
|
+
const next = errors > 0
|
|
732
|
+
? "specgate propose --from-brief --check"
|
|
733
|
+
: first
|
|
734
|
+
? `specgate propose --from-brief ${first}`
|
|
735
|
+
: "specgate propose --from-brief";
|
|
736
|
+
io.emit({
|
|
737
|
+
draft: { brief: brief.rel, ready: errors === 0, errors, warnings, next },
|
|
738
|
+
status: findings,
|
|
739
|
+
}, () => {
|
|
740
|
+
const out = process.stdout;
|
|
741
|
+
out.write(ui.header(`${this.verb} --check`, brief.rel));
|
|
742
|
+
writeBriefFindings(findings);
|
|
743
|
+
out.write(errors > 0
|
|
744
|
+
? (findings.length ? "\n" : "") +
|
|
745
|
+
ui.fail(`${ui.bold(`${errors} error(s) to fix before drafting`)} ${ui.muted("— each says how")}`)
|
|
746
|
+
: (findings.length ? "\n" : "") +
|
|
747
|
+
ui.ok(ui.bold("Ready to draft") +
|
|
748
|
+
(warnings ? ui.muted(` — ${warnings} warning(s) for you to judge`) : "")));
|
|
749
|
+
out.write(ui.next(next, errors > 0 ? `once they are fixed in ${brief.rel}` : "the first module"));
|
|
750
|
+
});
|
|
751
|
+
process.exitCode = errors > 0 ? 1 : 0;
|
|
752
|
+
}
|
|
644
753
|
/** `--new-brief`: the template, never over a brief that exists. */
|
|
645
754
|
newBrief(opts, file, rel) {
|
|
646
755
|
const io = (0, agent_1.agentIo)(opts.json);
|
|
@@ -653,17 +762,17 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
653
762
|
}
|
|
654
763
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
655
764
|
fs.writeFileSync(file, brief_setup_1.BRIEF_TEMPLATE, "utf8");
|
|
656
|
-
io.emit({ draft: { brief: rel, written: true, next: "specgate propose --from-brief" } }, () => process.stdout.write(ui.header(
|
|
765
|
+
io.emit({ draft: { brief: rel, written: true, next: "specgate propose --from-brief --check" } }, () => process.stdout.write(ui.header(`${this.verb} --new-brief`) +
|
|
657
766
|
ui.ok(`Wrote ${rel} — a brief to fill in.`) +
|
|
658
767
|
"\n Fill it in, in your own words:\n" +
|
|
659
768
|
" 1. The four fields at the top — product, surfaces, actors, modules.\n" +
|
|
660
769
|
" Each has an example in the comment above it.\n" +
|
|
661
|
-
" 2.
|
|
662
|
-
"
|
|
663
|
-
"
|
|
664
|
-
" Have a plan or an email already? Paste it under
|
|
665
|
-
|
|
666
|
-
ui.next("specgate propose --from-brief", "it checks the
|
|
770
|
+
" 2. Why — the problem, and a success you can measure.\n" +
|
|
771
|
+
" 3. Per module, a section: its Rules, an Example per rule, and the\n" +
|
|
772
|
+
" Questions you cannot answer yet.\n\n" +
|
|
773
|
+
" Have a plan or an email already? Paste it under Notes — your agent can\n" +
|
|
774
|
+
' turn it into the module sections: specgate propose --from-brief --agent "claude -p"\n' +
|
|
775
|
+
ui.next("specgate propose --from-brief --check", "it checks the brief and says what is missing")));
|
|
667
776
|
}
|
|
668
777
|
/**
|
|
669
778
|
* `draft --agent` without a module: the agent proposes the front matter.
|
|
@@ -674,14 +783,18 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
674
783
|
frontMatterWithAgent(opts, projectDir, brief) {
|
|
675
784
|
const io = (0, agent_1.agentIo)(opts.json);
|
|
676
785
|
if ((0, brief_setup_1.frontMatter)(brief.text) !== null && (0, brief_setup_1.checkFrontMatter)(brief.text).length === 0) {
|
|
677
|
-
const
|
|
786
|
+
const modules = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
787
|
+
if (modules.length > 0 && (0, BriefCards_1.readBriefBody)(brief.text).modules.length === 0) {
|
|
788
|
+
return this.cardsWithAgent(opts, projectDir, brief, modules);
|
|
789
|
+
}
|
|
790
|
+
const first = modules[0];
|
|
678
791
|
io.emit({
|
|
679
792
|
draft: {
|
|
680
793
|
brief: brief.rel,
|
|
681
794
|
frontMatter: "ok",
|
|
682
795
|
next: `specgate propose --from-brief ${first}`,
|
|
683
796
|
},
|
|
684
|
-
}, () => process.stdout.write(ui.header(
|
|
797
|
+
}, () => process.stdout.write(ui.header(`${this.verb} --agent`, brief.rel) +
|
|
685
798
|
ui.ok(`The front matter of ${brief.rel} is already there and complete — nothing for the agent to do.`) +
|
|
686
799
|
ui.next(`specgate propose --from-brief ${first}`)));
|
|
687
800
|
return;
|
|
@@ -693,7 +806,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
693
806
|
.join("/");
|
|
694
807
|
const agent = (0, confined_agent_1.withWritePermission)(opts.agent);
|
|
695
808
|
if (!opts.json) {
|
|
696
|
-
process.stdout.write(ui.header(
|
|
809
|
+
process.stdout.write(ui.header(`${this.verb} --agent`, brief.rel) +
|
|
697
810
|
agentLine(agent.command, `is proposing the front matter of ${brief.rel}`, agent.added));
|
|
698
811
|
}
|
|
699
812
|
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
@@ -758,6 +871,80 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
758
871
|
out.write(ui.next("specgate propose --from-brief", "review it — change anything — then run it"));
|
|
759
872
|
});
|
|
760
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* `draft --agent` on a brief with its front matter and no module sections:
|
|
876
|
+
* the agent proposes one section per module from the brief's own text. It
|
|
877
|
+
* may write only the brief, and must keep every line already there.
|
|
878
|
+
*/
|
|
879
|
+
cardsWithAgent(opts, projectDir, brief, modules) {
|
|
880
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
881
|
+
const guide = (0, brief_setup_1.briefCardsGuide)(brief.rel, modules);
|
|
882
|
+
const guideFile = path.join(projectDir, brief_setup_1.BRIEF_CARDS_FILE);
|
|
883
|
+
fs.mkdirSync(path.dirname(guideFile), { recursive: true });
|
|
884
|
+
fs.writeFileSync(guideFile, guide, "utf8");
|
|
885
|
+
const own = path
|
|
886
|
+
.relative(projectDir, path.resolve(projectDir, brief.rel))
|
|
887
|
+
.split(path.sep)
|
|
888
|
+
.join("/");
|
|
889
|
+
const agent = (0, confined_agent_1.withWritePermission)(opts.agent);
|
|
890
|
+
if (!opts.json) {
|
|
891
|
+
process.stdout.write(ui.header(`${this.verb} --agent`, brief.rel) +
|
|
892
|
+
agentLine(agent.command, `is proposing the module sections of ${brief.rel}`, agent.added));
|
|
893
|
+
}
|
|
894
|
+
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
895
|
+
projectDir,
|
|
896
|
+
changeId: "brief",
|
|
897
|
+
allowed: (rel) => rel === own,
|
|
898
|
+
command: agent.command,
|
|
899
|
+
prompt: (0, brief_setup_1.briefCardsPrompt)(guide),
|
|
900
|
+
showOutput: !opts.json,
|
|
901
|
+
});
|
|
902
|
+
if (run.problem === "no_git") {
|
|
903
|
+
io.fail({ draft: null }, [
|
|
904
|
+
(0, diagnostics_1.error)("draft_agent_needs_git", "--agent needs a git repository.", {
|
|
905
|
+
fix: `Run \`git init\`, then try again — or paste this into your agent yourself: ${brief_setup_1.BRIEF_CARDS_PASTE}`,
|
|
906
|
+
}),
|
|
907
|
+
]);
|
|
908
|
+
}
|
|
909
|
+
if (run.reverted.length > 0 && !opts.json) {
|
|
910
|
+
process.stdout.write("\n" +
|
|
911
|
+
ui.warn(`${ui_1.sym.undo} Undone — the agent wrote outside the brief: ${run.reverted.join(", ")}`));
|
|
912
|
+
}
|
|
913
|
+
const file = path.resolve(projectDir, brief.rel);
|
|
914
|
+
const after = readIf(file) ?? "";
|
|
915
|
+
if (!(0, brief_setup_1.keepsEveryLine)(brief.text, after)) {
|
|
916
|
+
fs.writeFileSync(file, brief.text, "utf8");
|
|
917
|
+
io.fail({ draft: null }, [
|
|
918
|
+
(0, diagnostics_1.error)("draft_brief_text_lost", "The agent changed or removed a line of the brief, not only added sections — undone.", {
|
|
919
|
+
fix: `${brief.rel} is back as it was. Try again, or write the sections yourself: \`## Module: ${modules[0]}\` with Rules, Examples and Questions.`,
|
|
920
|
+
}),
|
|
921
|
+
]);
|
|
922
|
+
}
|
|
923
|
+
if (!run.ok) {
|
|
924
|
+
io.fail({ draft: null }, [
|
|
925
|
+
(0, diagnostics_1.error)("draft_agent_failed", `The agent ${run.detail}.`, {
|
|
926
|
+
fix: `Check the command runs on its own, or paste this into your agent: ${brief_setup_1.BRIEF_CARDS_PASTE}`,
|
|
927
|
+
}),
|
|
928
|
+
]);
|
|
929
|
+
}
|
|
930
|
+
const added = (0, BriefCards_1.readBriefBody)(after).modules.map((m) => m.name);
|
|
931
|
+
if (added.length === 0) {
|
|
932
|
+
io.fail({ draft: null }, [
|
|
933
|
+
(0, diagnostics_1.error)("draft_no_cards", "The agent added no module section.", {
|
|
934
|
+
fix: `${confined_agent_1.NO_WRITE_HINT} Or write them yourself: \`## Module: ${modules[0]}\` with Rules, Examples and Questions.`,
|
|
935
|
+
}),
|
|
936
|
+
]);
|
|
937
|
+
}
|
|
938
|
+
const findings = (0, brief_check_1.checkBrief)(after, brief.rel);
|
|
939
|
+
const next = "specgate propose --from-brief --check";
|
|
940
|
+
io.emit({ draft: { brief: brief.rel, sections: added, next }, status: findings }, () => {
|
|
941
|
+
const out = process.stdout;
|
|
942
|
+
out.write(ui.rule() + "\n");
|
|
943
|
+
out.write(ui.ok(`The agent proposed ${added.length} module section(s) in ${brief.rel}: ${added.join(", ")}`) + "\n");
|
|
944
|
+
out.write(ui.info("Every line you wrote is still there."));
|
|
945
|
+
out.write(ui.next(next, "review the sections — change anything — then check them"));
|
|
946
|
+
});
|
|
947
|
+
}
|
|
761
948
|
// ── specgate draft <module> ─────────────────────────────────────────────────
|
|
762
949
|
module(opts) {
|
|
763
950
|
const io = (0, agent_1.agentIo)(opts.json);
|
|
@@ -772,7 +959,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
772
959
|
state: "archived",
|
|
773
960
|
next: "specgate propose --from-brief",
|
|
774
961
|
},
|
|
775
|
-
}, () => process.stdout.write(ui.header(
|
|
962
|
+
}, () => process.stdout.write(ui.header(`${this.verb} ${opts.module}`) +
|
|
776
963
|
ui.ok(`${opts.module} is already drafted and archived ${ui.muted(`(${status.archivedAs})`)}`) +
|
|
777
964
|
ui.next("specgate propose --from-brief", "the next module of the brief, or what to build first")));
|
|
778
965
|
return;
|
|
@@ -788,6 +975,17 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
788
975
|
]);
|
|
789
976
|
}
|
|
790
977
|
const brief = this.brief(projectDir, opts);
|
|
978
|
+
// A brief that is plain markdown, with no front matter, still drafts:
|
|
979
|
+
// only the rules that apply to what it does say stop it.
|
|
980
|
+
const blocking = (0, brief_check_1.checkBrief)(brief.text, brief.rel).filter((d) => d.code !== "brief_no_front_matter");
|
|
981
|
+
if (!(0, brief_check_1.briefReady)(blocking)) {
|
|
982
|
+
io.fail({ draft: null }, [
|
|
983
|
+
(0, diagnostics_1.error)("brief_not_ready", `${brief.rel} is not ready to draft ${opts.module} from.`, {
|
|
984
|
+
fix: "Fix what `specgate propose --from-brief --check` reports, then run this again.",
|
|
985
|
+
}),
|
|
986
|
+
...blocking.filter((d) => d.severity === "error"),
|
|
987
|
+
]);
|
|
988
|
+
}
|
|
791
989
|
const listed = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
792
990
|
const opened = openDraft(projectDir, opts.module, brief.rel, opts);
|
|
793
991
|
const notes = listed.length > 0 && !listed.includes(opts.module)
|
|
@@ -820,7 +1018,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
820
1018
|
[`${dir}/change.yaml, proposal.md`, ""],
|
|
821
1019
|
];
|
|
822
1020
|
const w = Math.max(...files.map((r) => r[0].length));
|
|
823
|
-
out.write(ui.header(
|
|
1021
|
+
out.write(ui.header(`${this.verb} ${opts.module}`, brief.rel) +
|
|
824
1022
|
ui.ok(`Draft ${ui.bold(opened.changeId)} opened ` +
|
|
825
1023
|
ui.muted(`· REQ ids ${opened.reqRange[0]}…${opened.reqRange[1]} reserved`)) +
|
|
826
1024
|
"\n" +
|
|
@@ -848,7 +1046,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
848
1046
|
const guide = readIf(path.join(projectDir, CHANGES, changeId, "DRAFTING.md")) || "";
|
|
849
1047
|
const agent = (0, confined_agent_1.withWritePermission)(opts.agent);
|
|
850
1048
|
if (!opts.json) {
|
|
851
|
-
process.stdout.write(ui.header(
|
|
1049
|
+
process.stdout.write(ui.header(`${this.verb} ${opts.module}`, brief) +
|
|
852
1050
|
(created.length ? ui.ok(`Draft ${changeId} opened, linked to ${brief}.`) : "") +
|
|
853
1051
|
agentLine(agent.command, `is drafting ${opts.module}`, agent.added));
|
|
854
1052
|
}
|
|
@@ -901,7 +1099,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
901
1099
|
prompt: pasteLine(opts.module),
|
|
902
1100
|
next: `specgate propose --from-brief ${opts.module}`,
|
|
903
1101
|
},
|
|
904
|
-
}, () => process.stdout.write((afterAgent ? "\n" : ui.header(
|
|
1102
|
+
}, () => process.stdout.write((afterAgent ? "\n" : ui.header(`${this.verb} ${opts.module}`, brief)) +
|
|
905
1103
|
(afterAgent
|
|
906
1104
|
? ui.fail("The agent ran, but the draft is still the placeholder.") +
|
|
907
1105
|
ui.fix(confined_agent_1.NO_WRITE_HINT) +
|
|
@@ -938,7 +1136,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
938
1136
|
status: diags,
|
|
939
1137
|
}, () => {
|
|
940
1138
|
const out = process.stdout;
|
|
941
|
-
out.write(afterAgent ? "\n" : ui.header(
|
|
1139
|
+
out.write(afterAgent ? "\n" : ui.header(`${this.verb} ${opts.module}`, brief));
|
|
942
1140
|
writeFindings(report, diags);
|
|
943
1141
|
const dir = `${CHANGES.split(path.sep).join("/")}/${changeId}`;
|
|
944
1142
|
const steps = [];
|
|
@@ -989,7 +1187,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
989
1187
|
status,
|
|
990
1188
|
}, () => {
|
|
991
1189
|
const out = process.stdout;
|
|
992
|
-
out.write(ui.header(
|
|
1190
|
+
out.write(ui.header(`${this.verb} --check ${opts.check}`));
|
|
993
1191
|
writeFindings(report, status);
|
|
994
1192
|
const skipped = report.skipped.length;
|
|
995
1193
|
out.write(errors === 0
|
|
@@ -1016,4 +1214,19 @@ function writeFindings(report, status) {
|
|
|
1016
1214
|
out.write(ui.fix(d.fix));
|
|
1017
1215
|
}
|
|
1018
1216
|
}
|
|
1217
|
+
/** The brief rules' findings: code, the module they are about, message, fix. */
|
|
1218
|
+
function writeBriefFindings(findings) {
|
|
1219
|
+
const out = process.stdout;
|
|
1220
|
+
for (const d of findings) {
|
|
1221
|
+
const where = d.target ? `${ui.bold(d.target)} ` : "";
|
|
1222
|
+
const line = `${ui.muted(d.code)} ${where}${d.message}`;
|
|
1223
|
+
out.write(d.severity === "error"
|
|
1224
|
+
? ui.fail(line)
|
|
1225
|
+
: d.severity === "warning"
|
|
1226
|
+
? ui.warn(line)
|
|
1227
|
+
: ui.info(line));
|
|
1228
|
+
if (d.fix)
|
|
1229
|
+
out.write(ui.fix(d.fix));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1019
1232
|
//# sourceMappingURL=DraftCommand.js.map
|