@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +81 -96
  3. package/dist/packages/core/src/domain/BriefCards.js +320 -0
  4. package/dist/packages/core/src/domain/BriefCards.js.map +1 -0
  5. package/dist/packages/core/src/domain/DraftChecklist.js +5 -1
  6. package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
  7. package/dist/packages/core/src/domain/QuotedText.js +5 -0
  8. package/dist/packages/core/src/domain/QuotedText.js.map +1 -1
  9. package/dist/packages/core/src/domain/TicketText.js +337 -0
  10. package/dist/packages/core/src/domain/TicketText.js.map +1 -0
  11. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js +7 -3
  12. package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -1
  13. package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js +48 -0
  14. package/dist/packages/core/src/infrastructure/IdsAcrossBranches.js.map +1 -0
  15. package/dist/packages/mcp-spec-driven/src/native-tools.js +1 -1
  16. package/dist/packages/mcp-spec-driven/src/native-tools.js.map +1 -1
  17. package/dist/scripts/agents/commands.js +8 -6
  18. package/dist/scripts/agents/commands.js.map +1 -1
  19. package/dist/scripts/agents/contract-file.js +44 -0
  20. package/dist/scripts/agents/contract-file.js.map +1 -1
  21. package/dist/scripts/alm/cli.js +16 -4
  22. package/dist/scripts/alm/cli.js.map +1 -1
  23. package/dist/scripts/alm/port.js +1 -1
  24. package/dist/scripts/alm/port.js.map +1 -1
  25. package/dist/scripts/alm/providers/http-client.js +46 -3
  26. package/dist/scripts/alm/providers/http-client.js.map +1 -1
  27. package/dist/scripts/alm/providers/jira.js +108 -32
  28. package/dist/scripts/alm/providers/jira.js.map +1 -1
  29. package/dist/scripts/cli/commands/alm/PrCommand.js +198 -0
  30. package/dist/scripts/cli/commands/alm/PrCommand.js.map +1 -0
  31. package/dist/scripts/cli/commands/change/ChangeCommand.js +32 -6
  32. package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
  33. package/dist/scripts/cli/commands/change/TicketCommand.js +657 -0
  34. package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -0
  35. package/dist/scripts/cli/commands/harness/RunCommand.js +4 -2
  36. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  37. package/dist/scripts/cli/commands/project/AdoptCommand.js +58 -0
  38. package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
  39. package/dist/scripts/cli/commands/project/UpdateCommand.js +4 -0
  40. package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
  41. package/dist/scripts/cli/commands/quality/CheckCommand.js +12 -0
  42. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  43. package/dist/scripts/cli/commands/spec/DraftCommand.js +240 -27
  44. package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
  45. package/dist/scripts/cli/commands/spec/ExploreCommand.js +13 -13
  46. package/dist/scripts/cli/commands/spec/ExploreCommand.js.map +1 -1
  47. package/dist/scripts/cli/commands/spec/StatusCommand.js +18 -6
  48. package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
  49. package/dist/scripts/docs/journey-storyboard.js +67 -44
  50. package/dist/scripts/docs/journey-storyboard.js.map +1 -1
  51. package/dist/scripts/docs/nav.js +156 -139
  52. package/dist/scripts/docs/nav.js.map +1 -1
  53. package/dist/scripts/docs/record-journey.js +139 -49
  54. package/dist/scripts/docs/record-journey.js.map +1 -1
  55. package/dist/scripts/docs/terminal-storyboard.js +15 -11
  56. package/dist/scripts/docs/terminal-storyboard.js.map +1 -1
  57. package/dist/scripts/draft/brief-check.js +34 -0
  58. package/dist/scripts/draft/brief-check.js.map +1 -0
  59. package/dist/scripts/draft/brief-setup.js +182 -17
  60. package/dist/scripts/draft/brief-setup.js.map +1 -1
  61. package/dist/scripts/draft/drafting-guide.js +43 -1
  62. package/dist/scripts/draft/drafting-guide.js.map +1 -1
  63. package/dist/scripts/draft/ticket-guide.js +129 -0
  64. package/dist/scripts/draft/ticket-guide.js.map +1 -0
  65. package/dist/scripts/lib/surface.js +2 -0
  66. package/dist/scripts/lib/surface.js.map +1 -1
  67. package/dist/scripts/lib/ticket-keys.js +140 -0
  68. package/dist/scripts/lib/ticket-keys.js.map +1 -0
  69. package/dist/scripts/propose.js +8 -2
  70. package/dist/scripts/propose.js.map +1 -1
  71. package/dist/scripts/vcs/github-pr.js +88 -0
  72. package/dist/scripts/vcs/github-pr.js.map +1 -0
  73. package/package.json +1 -1
  74. 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
- o.check = value();
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
- else if (e.name.endsWith(".md") || e.name.endsWith(".feature")) {
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: nextScenario(projectDir),
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("draft") + `${(0, brief_setup_1.noBriefExplanation)(rel)}\n`);
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("draft", rel) +
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("draft", rel));
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("draft", rel));
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("draft --new-brief") +
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. The sections below — what the product is, who uses it and how, what\n" +
662
- " each one must be able to do, the rules and numbers you already know,\n" +
663
- " what you don't know yet, and what is out of scope.\n\n" +
664
- " Have a plan or an email already? Paste it under the four fields instead\n" +
665
- " of the sections — Specgate reads it as it is.\n" +
666
- ui.next("specgate propose --from-brief", "it checks the four fields and says what is next")));
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 first = (0, DraftChecklist_1.briefModules)(brief.text)[0];
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("draft --agent", brief.rel) +
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("draft --agent", brief.rel) +
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(`draft ${opts.module}`) +
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(`draft ${opts.module}`, brief.rel) +
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(`draft ${opts.module}`, brief) +
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(`draft ${opts.module}`, brief)) +
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(`draft ${opts.module}`, brief));
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(`draft --check ${opts.check}`));
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