@rsaglobaltech/specgate 0.17.10 → 0.19.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 +133 -1
- package/README.md +4 -4
- package/bin/specgate.ts +48 -47
- package/dist/bin/specgate.js +48 -46
- package/dist/bin/specgate.js.map +1 -1
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js +6 -2
- package/dist/packages/core/src/application/ArchiveChangeUseCase.js.map +1 -1
- package/dist/packages/core/src/application/ValidateProjectUseCase.js +1 -1
- package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/ChangeTemplates.js +1 -1
- package/dist/packages/core/src/domain/DeltaFeature.js +3 -3
- package/dist/packages/core/src/domain/DeltaFeature.js.map +1 -1
- package/dist/packages/core/src/domain/DerivedMatrix.js +9 -9
- package/dist/packages/core/src/domain/DerivedMatrix.js.map +1 -1
- package/dist/packages/core/src/domain/DoneVerification.js +1 -1
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
- package/dist/packages/core/src/domain/DraftChecklist.js +38 -6
- package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
- package/dist/packages/core/src/domain/GherkinTags.js +12 -12
- package/dist/packages/core/src/domain/GherkinTags.js.map +1 -1
- package/dist/packages/core/src/domain/InboundChange.js +1 -1
- package/dist/packages/core/src/domain/InboundChange.js.map +1 -1
- package/dist/packages/core/src/domain/Language.js +2 -2
- package/dist/packages/core/src/domain/Language.js.map +1 -1
- package/dist/packages/core/src/domain/Lockfile.js +7 -7
- package/dist/packages/core/src/domain/Lockfile.js.map +1 -1
- package/dist/packages/core/src/domain/PackDelta.js +1 -1
- package/dist/packages/core/src/domain/PackDelta.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/packages/core/src/domain/RequirementGraph.js +2 -2
- package/dist/packages/core/src/domain/SpecParser.js +6 -6
- package/dist/packages/core/src/domain/SpecParser.js.map +1 -1
- package/dist/packages/core/src/domain/TraceMerge.js +4 -4
- package/dist/packages/core/src/domain/TraceMerge.js.map +1 -1
- package/dist/packages/core/src/domain/TraceabilityFormat.js +4 -4
- package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -1
- package/dist/packages/core/src/domain/ValueAnnotations.js +8 -8
- package/dist/packages/core/src/domain/ValueAnnotations.js.map +1 -1
- package/dist/packages/core/src/domain/WriteScope.js +3 -3
- package/dist/packages/core/src/domain/WriteScope.js.map +1 -1
- package/dist/packages/core/src/infrastructure/ChangeWorkspace.js +2 -2
- package/dist/packages/core/src/infrastructure/ChangeWorkspace.js.map +1 -1
- package/dist/packages/core/src/infrastructure/ConsoleReporter.js +2 -2
- package/dist/packages/core/src/infrastructure/ConsoleReporter.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DerivedMatrixSources.js +2 -2
- package/dist/packages/core/src/infrastructure/DerivedMatrixSources.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js +4 -4
- package/dist/packages/core/src/infrastructure/DiskLanguageRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js +1 -1
- package/dist/packages/core/src/infrastructure/DiskPackDeltaRepository.js.map +1 -1
- package/dist/packages/core/src/infrastructure/GitContributionStager.js +3 -3
- package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -1
- package/dist/packages/core/src/infrastructure/RemotePackResolver.js +5 -5
- package/dist/packages/core/src/infrastructure/RemotePackResolver.js.map +1 -1
- package/dist/packages/core/test/unit/delta-feature.test.js +3 -3
- package/dist/packages/core/test/unit/delta-feature.test.js.map +1 -1
- package/dist/packages/core/test/unit/derived-matrix.test.js +8 -8
- package/dist/packages/core/test/unit/derived-matrix.test.js.map +1 -1
- package/dist/packages/core/test/unit/gherkin-tags.test.js +8 -8
- package/dist/packages/core/test/unit/gherkin-tags.test.js.map +1 -1
- package/dist/packages/core/test/unit/inbound-change.test.js +1 -1
- package/dist/packages/core/test/unit/inbound-change.test.js.map +1 -1
- package/dist/packages/core/test/unit/matrix-dependencies.test.js +5 -5
- package/dist/packages/core/test/unit/matrix-dependencies.test.js.map +1 -1
- package/dist/packages/core/test/unit/new-requirement.test.js +1 -1
- package/dist/packages/core/test/unit/new-requirement.test.js.map +1 -1
- package/dist/packages/core/test/unit/value-annotations.test.js +5 -5
- package/dist/packages/core/test/unit/value-annotations.test.js.map +1 -1
- package/dist/packages/core/test/unit/write-scope-status.test.js +5 -5
- package/dist/packages/core/test/unit/write-scope-status.test.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/native-tools.js +3 -3
- package/dist/packages/mcp-spec-driven/src/native-tools.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/server.js +1 -7
- package/dist/packages/mcp-spec-driven/src/server.js.map +1 -1
- package/dist/packages/mcp-spec-driven/src/tools.js +4 -4
- package/dist/packages/mcp-spec-driven/src/tools.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 +35 -48
- package/dist/scripts/agents/init.js.map +1 -1
- package/dist/scripts/alm/cli.js +37 -3
- package/dist/scripts/alm/cli.js.map +1 -1
- package/dist/scripts/alm/providers/index.js +1 -1
- package/dist/scripts/alm/providers/index.js.map +1 -1
- package/dist/scripts/change/instructions.js +2 -2
- package/dist/scripts/change/instructions.js.map +1 -1
- package/dist/scripts/cli/commands/alm/PullCommand.js +1 -1
- package/dist/scripts/cli/commands/alm/PullCommand.js.map +1 -1
- package/dist/scripts/cli/commands/change/ChangeCommand.js +106 -101
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/CiInitCommand.js +6 -4
- package/dist/scripts/cli/commands/config/CiInitCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/CompletionCommand.js +10 -10
- package/dist/scripts/cli/commands/config/CompletionCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/ConfigInitCommand.js +11 -19
- package/dist/scripts/cli/commands/config/ConfigInitCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js +5 -1
- package/dist/scripts/cli/commands/config/ConfigSetCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +73 -38
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/BundlePackCommand.js +36 -2
- package/dist/scripts/cli/commands/pack/BundlePackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js +2 -2
- package/dist/scripts/cli/commands/pack/ExpandDomainPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/InferPackCommand.js +2 -1
- package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/InitPackCommand.js +23 -32
- package/dist/scripts/cli/commands/pack/InitPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/LintPackCommand.js +4 -3
- package/dist/scripts/cli/commands/pack/LintPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +17 -4
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js +8 -5
- package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/OnboardCommand.js +17 -24
- package/dist/scripts/cli/commands/project/OnboardCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/UpdateCommand.js +23 -43
- package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +30 -17
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/DoctorCommand.js +27 -10
- package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js +24 -29
- package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js +8 -4
- package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +69 -16
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +37 -23
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DraftCommand.js +724 -26
- package/dist/scripts/cli/commands/spec/DraftCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/MatrixCommand.js +24 -27
- package/dist/scripts/cli/commands/spec/MatrixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/NewCommand.js +11 -14
- package/dist/scripts/cli/commands/spec/NewCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/PetCommand.js +102 -0
- package/dist/scripts/cli/commands/spec/PetCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/PlanCommand.js +34 -39
- package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ReqCommand.js +51 -58
- package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +99 -45
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StudioCommand.js +16 -24
- package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/ContributeCommand.js +24 -32
- package/dist/scripts/cli/commands/specops/ContributeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/DiffCommand.js +3 -2
- package/dist/scripts/cli/commands/specops/DiffCommand.js.map +1 -1
- package/dist/scripts/cli/commands/specops/SyncCommand.js +4 -3
- package/dist/scripts/cli/commands/specops/SyncCommand.js.map +1 -1
- package/dist/scripts/docs/blocks.js +4 -4
- package/dist/scripts/docs/blocks.js.map +1 -1
- package/dist/scripts/docs/build-site.js +24 -9
- package/dist/scripts/docs/build-site.js.map +1 -1
- package/dist/scripts/docs/components.js +24 -24
- package/dist/scripts/docs/components.js.map +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 +120 -23
- 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 +260 -43
- 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 +367 -0
- package/dist/scripts/draft/brief-setup.js.map +1 -0
- package/dist/scripts/draft/confined-agent.js +194 -0
- package/dist/scripts/draft/confined-agent.js.map +1 -0
- package/dist/scripts/draft/drafting-guide.js +147 -0
- package/dist/scripts/draft/drafting-guide.js.map +1 -0
- package/dist/scripts/gen_mcp_tools.js +4 -4
- package/dist/scripts/gen_mcp_tools.js.map +1 -1
- package/dist/scripts/harness/init.js +25 -16
- package/dist/scripts/harness/init.js.map +1 -1
- package/dist/scripts/harness/report.js +39 -40
- package/dist/scripts/harness/report.js.map +1 -1
- package/dist/scripts/harness/worktree-lock.js +2 -2
- package/dist/scripts/harness/worktree-lock.js.map +1 -1
- package/dist/scripts/lib/command.js +3 -2
- package/dist/scripts/lib/command.js.map +1 -1
- package/dist/scripts/lib/derived-writes.js +1 -1
- package/dist/scripts/lib/diagnostics.js +12 -16
- package/dist/scripts/lib/diagnostics.js.map +1 -1
- package/dist/scripts/lib/placeholders.js +4 -4
- package/dist/scripts/lib/placeholders.js.map +1 -1
- package/dist/scripts/lib/surface.js +33 -15
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/lib/ui.js +356 -0
- package/dist/scripts/lib/ui.js.map +1 -0
- package/dist/scripts/merge-traceability.js +3 -3
- package/dist/scripts/pet/pet.js +331 -0
- package/dist/scripts/pet/pet.js.map +1 -0
- package/dist/scripts/pet.js +10 -0
- package/dist/scripts/pet.js.map +1 -0
- package/dist/scripts/plugin/gate-hook.js +1 -1
- package/dist/scripts/plugin/gate-hook.js.map +1 -1
- package/dist/scripts/schema/registry.js +2 -2
- package/dist/scripts/schema/registry.js.map +1 -1
- package/dist/scripts/specops/add.js +23 -32
- package/dist/scripts/specops/add.js.map +1 -1
- package/dist/scripts/specops/lock.js +4 -4
- package/dist/scripts/specops/lock.js.map +1 -1
- package/dist/scripts/specops/remove.js +15 -24
- package/dist/scripts/specops/remove.js.map +1 -1
- package/package.json +3 -2
- package/templates/base/AGENTS.md.tpl +6 -0
|
@@ -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,25 +48,58 @@ 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;
|
|
46
54
|
exports.archivedAssumptions = archivedAssumptions;
|
|
47
55
|
exports.briefOf = briefOf;
|
|
48
56
|
exports.draftReport = draftReport;
|
|
57
|
+
exports.moduleStatus = moduleStatus;
|
|
58
|
+
exports.briefProgress = briefProgress;
|
|
59
|
+
exports.draftNextCommand = draftNextCommand;
|
|
60
|
+
exports.pasteLine = pasteLine;
|
|
49
61
|
const fs = __importStar(require("node:fs"));
|
|
50
62
|
const path = __importStar(require("node:path"));
|
|
51
63
|
const command_1 = require("../../../lib/command");
|
|
64
|
+
const ui = __importStar(require("../../../lib/ui"));
|
|
65
|
+
const ui_1 = require("../../../lib/ui");
|
|
52
66
|
const agent_1 = require("../../../lib/agent");
|
|
53
67
|
const diagnostics_1 = require("../../../lib/diagnostics");
|
|
54
68
|
const project_root_1 = require("../../../lib/project-root");
|
|
55
69
|
const DraftChecklist_1 = require("../../../../packages/core/src/domain/DraftChecklist");
|
|
70
|
+
const ChangeWorkspace_1 = require("../../../../packages/core/src/infrastructure/ChangeWorkspace");
|
|
71
|
+
const ChangeTemplates_1 = require("../../../../packages/core/src/domain/ChangeTemplates");
|
|
72
|
+
const DiskLanguageRepository_1 = require("../../../../packages/core/src/infrastructure/DiskLanguageRepository");
|
|
73
|
+
const drafting_guide_1 = require("../../../draft/drafting-guide");
|
|
74
|
+
const confined_agent_1 = require("../../../draft/confined-agent");
|
|
75
|
+
const brief_setup_1 = require("../../../draft/brief-setup");
|
|
56
76
|
const GherkinTags_1 = require("../../../../packages/core/src/domain/GherkinTags");
|
|
57
77
|
const DerivedMatrixSources_1 = require("../../../../packages/core/src/infrastructure/DerivedMatrixSources");
|
|
58
78
|
const CHANGES = path.join("docs", "specs", "changes");
|
|
79
|
+
/** Where `specgate draft` looks for the plan when `--brief` is not given. */
|
|
80
|
+
exports.DEFAULT_BRIEF = "docs/brief.md";
|
|
81
|
+
const DEFAULT_RESERVE = 10;
|
|
82
|
+
/** `draft-bookings` → `bookings`; any other change id → `--check <id>`. */
|
|
83
|
+
function draftModuleOf(changeId) {
|
|
84
|
+
return changeId.startsWith("draft-") ? changeId.slice("draft-".length) : `--check ${changeId}`;
|
|
85
|
+
}
|
|
59
86
|
function usage() {
|
|
60
87
|
return ("Usage:\n" +
|
|
61
|
-
" specgate draft
|
|
62
|
-
"
|
|
88
|
+
" specgate draft your brief's modules, and the one to do next\n" +
|
|
89
|
+
" specgate draft <module> open that module's draft — or check it, once it exists\n" +
|
|
90
|
+
' specgate draft <module> --agent "claude -p"\n' +
|
|
91
|
+
" have your agent write it, then check it\n" +
|
|
92
|
+
" specgate draft --check <change-id> the checklist on any change\n" +
|
|
93
|
+
" specgate draft --new-brief write docs/brief.md to fill in, when you have none\n" +
|
|
94
|
+
' specgate draft --agent "claude -p" have your agent propose the brief\'s front matter\n\n' +
|
|
95
|
+
" --brief <file> Your plan or brief (default: docs/brief.md). Any markdown; a front\n" +
|
|
96
|
+
" matter with modules, actors and surfaces lets the checklist do more.\n" +
|
|
97
|
+
" --reserve <n> Requirement ids a new draft reserves (default 10, at most the --max-requirements).\n" +
|
|
98
|
+
" --agent <cmd> Your agent's command. It gets the drafting guide on standard input,\n" +
|
|
99
|
+
" or in a file if the command contains {prompt_file}. Whatever it writes\n" +
|
|
100
|
+
" outside the draft is undone.\n" +
|
|
101
|
+
" [--max-requirements <n>] [--project-dir <dir>] [--json]\n\n" +
|
|
102
|
+
"The checklist (ADR-0029):\n" +
|
|
63
103
|
" D1 every requirement has a scenario D5 a cited law or standard names its source\n" +
|
|
64
104
|
" D2 every requirement has a kind D6 a stated value is in the brief or an assumption\n" +
|
|
65
105
|
" D3 a scenario per surface each actor uses D7 an open question names what it blocks\n" +
|
|
@@ -67,14 +107,17 @@ function usage() {
|
|
|
67
107
|
" D9 every scenario passes the harness gate's rules\n" +
|
|
68
108
|
" D10 no id the project already uses\n" +
|
|
69
109
|
" D12 an actor can get the ids it acts on\n\n" +
|
|
70
|
-
"
|
|
71
|
-
" run, and the report says so. Also read from `brief:` in change.yaml.\n");
|
|
110
|
+
"Without a brief, D3 and D6 cannot run, and the report says so.\n");
|
|
72
111
|
}
|
|
73
112
|
function parseArgs(argv) {
|
|
74
113
|
const o = {
|
|
75
114
|
check: "",
|
|
115
|
+
module: "",
|
|
76
116
|
brief: "",
|
|
77
117
|
maxRequirements: 25,
|
|
118
|
+
reserve: DEFAULT_RESERVE,
|
|
119
|
+
agent: "",
|
|
120
|
+
newBrief: false,
|
|
78
121
|
projectDir: ".",
|
|
79
122
|
json: (0, agent_1.wantsJson)(argv),
|
|
80
123
|
};
|
|
@@ -92,6 +135,12 @@ function parseArgs(argv) {
|
|
|
92
135
|
o.brief = value();
|
|
93
136
|
else if (a === "--max-requirements")
|
|
94
137
|
o.maxRequirements = Number(value());
|
|
138
|
+
else if (a === "--reserve")
|
|
139
|
+
o.reserve = Number(value());
|
|
140
|
+
else if (a === "--agent")
|
|
141
|
+
o.agent = value();
|
|
142
|
+
else if (a === "--new-brief")
|
|
143
|
+
o.newBrief = true;
|
|
95
144
|
else if (a === "--project-dir")
|
|
96
145
|
o.projectDir = value();
|
|
97
146
|
else if (a === "--json")
|
|
@@ -100,15 +149,29 @@ function parseArgs(argv) {
|
|
|
100
149
|
process.stdout.write(usage());
|
|
101
150
|
process.exit(0);
|
|
102
151
|
}
|
|
152
|
+
else if (!a.startsWith("-") && !o.module)
|
|
153
|
+
o.module = a;
|
|
103
154
|
else
|
|
104
155
|
throw new Error(`unknown argument ${a}`);
|
|
105
156
|
}
|
|
106
|
-
if (
|
|
107
|
-
throw new Error("--check <change-id
|
|
157
|
+
if (o.check && o.module) {
|
|
158
|
+
throw new Error("give a module or --check <change-id>, not both");
|
|
159
|
+
}
|
|
160
|
+
if (o.module && !ChangeWorkspace_1.CHANGE_ID_RE.test(o.module)) {
|
|
161
|
+
throw new Error(`"${o.module}" is not a module name: use lowercase words and dashes, e.g. bookings`);
|
|
108
162
|
}
|
|
109
163
|
if (!Number.isInteger(o.maxRequirements) || o.maxRequirements < 1) {
|
|
110
164
|
throw new Error("--max-requirements needs a positive integer");
|
|
111
165
|
}
|
|
166
|
+
if (!Number.isInteger(o.reserve) || o.reserve < 1 || o.reserve > o.maxRequirements) {
|
|
167
|
+
throw new Error(`--reserve needs a whole number from 1 to ${o.maxRequirements}`);
|
|
168
|
+
}
|
|
169
|
+
if (o.newBrief && (o.module || o.check || o.agent)) {
|
|
170
|
+
throw new Error("--new-brief only writes the brief: run it on its own");
|
|
171
|
+
}
|
|
172
|
+
if (o.agent && o.check) {
|
|
173
|
+
throw new Error("--agent drafts a module, or proposes the brief's front matter — not --check");
|
|
174
|
+
}
|
|
112
175
|
return o;
|
|
113
176
|
}
|
|
114
177
|
function specFiles(dir, rel = "") {
|
|
@@ -159,7 +222,7 @@ function projectIds(projectDir) {
|
|
|
159
222
|
owners.set(id, id);
|
|
160
223
|
}
|
|
161
224
|
for (const f of sources.features || []) {
|
|
162
|
-
for (const tags of Object.values((0, GherkinTags_1.
|
|
225
|
+
for (const tags of Object.values((0, GherkinTags_1.specgateTagsByScenario)(f.source))) {
|
|
163
226
|
const req = tags.find((t) => /^@REQ-/.test(t));
|
|
164
227
|
for (const t of tags.filter((x) => /^@SCN-/.test(x))) {
|
|
165
228
|
if (req)
|
|
@@ -241,6 +304,213 @@ function draftReport(projectDir, changeId, briefOption = "", maxRequirements = 2
|
|
|
241
304
|
}),
|
|
242
305
|
};
|
|
243
306
|
}
|
|
307
|
+
/** The placeholder `templateDelta` writes: a draft nobody has written yet. */
|
|
308
|
+
const PLACEHOLDER = "feature=features/<area>/<name>.feature";
|
|
309
|
+
function deltaSources(projectDir, changeId) {
|
|
310
|
+
const specs = path.join(projectDir, CHANGES, changeId, "specs");
|
|
311
|
+
return specFiles(specs).map((rel) => fs.readFileSync(path.join(specs, rel), "utf8"));
|
|
312
|
+
}
|
|
313
|
+
function moduleStatus(projectDir, module, brief = "") {
|
|
314
|
+
const change = `draft-${module}`;
|
|
315
|
+
const archived = (0, ChangeWorkspace_1.listArchivedIds)(projectDir).find((id) => id === change || id.endsWith(`-${change}`));
|
|
316
|
+
const base = { module, change, findings: 0 };
|
|
317
|
+
if (!fs.existsSync(path.join(projectDir, CHANGES, change))) {
|
|
318
|
+
return archived
|
|
319
|
+
? { ...base, state: "archived", archivedAs: archived }
|
|
320
|
+
: { ...base, state: "not started" };
|
|
321
|
+
}
|
|
322
|
+
const deltas = deltaSources(projectDir, change);
|
|
323
|
+
if (deltas.length === 0 || deltas.every((d) => d.includes(PLACEHOLDER))) {
|
|
324
|
+
return { ...base, state: "not written" };
|
|
325
|
+
}
|
|
326
|
+
const result = draftReport(projectDir, change, brief);
|
|
327
|
+
if ("failure" in result)
|
|
328
|
+
return { ...base, state: "drafting", findings: 1 };
|
|
329
|
+
const findings = result.report.status.length;
|
|
330
|
+
return { ...base, state: findings === 0 ? "ready" : "drafting", findings };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Where the brief stands, for `status`: its modules, and the first one not
|
|
334
|
+
* yet archived. Null when there is no brief, or it lists no modules.
|
|
335
|
+
*/
|
|
336
|
+
function briefProgress(projectDir) {
|
|
337
|
+
const text = readIf(path.join(projectDir, exports.DEFAULT_BRIEF));
|
|
338
|
+
if (text === undefined)
|
|
339
|
+
return null;
|
|
340
|
+
const modules = (0, DraftChecklist_1.briefModules)(text).map((m) => moduleStatus(projectDir, m));
|
|
341
|
+
if (modules.length === 0)
|
|
342
|
+
return null;
|
|
343
|
+
return { brief: exports.DEFAULT_BRIEF, modules, next: modules.find((m) => m.state !== "archived") };
|
|
344
|
+
}
|
|
345
|
+
/** What to run for this module, given where it stands. */
|
|
346
|
+
function draftNextCommand(s) {
|
|
347
|
+
return s.state === "ready" ? `specgate change archive ${s.change}` : `specgate draft ${s.module}`;
|
|
348
|
+
}
|
|
349
|
+
/** `➜ claude -p … is drafting catalogo…`, and why a flag was added. */
|
|
350
|
+
function agentLine(command, doing, added) {
|
|
351
|
+
return (` ${ui_1.c.brand}${ui_1.sym.next}${ui_1.c.reset} ${ui.cmd(command)} ${doing}${ui.muted("…")}\n` +
|
|
352
|
+
(added
|
|
353
|
+
? ` ${ui.muted("edit permission added: claude -p cannot write files otherwise")}\n`
|
|
354
|
+
: "") +
|
|
355
|
+
"\n");
|
|
356
|
+
}
|
|
357
|
+
/** Each module of the brief and where it stands, on one line: `auth ✔ ready · catalog ○ not started`. */
|
|
358
|
+
function briefLine(projectDir, brief) {
|
|
359
|
+
const text = readIf(path.resolve(projectDir, brief));
|
|
360
|
+
const modules = text === undefined ? [] : (0, DraftChecklist_1.briefModules)(text);
|
|
361
|
+
if (modules.length < 2)
|
|
362
|
+
return null;
|
|
363
|
+
const mark = {
|
|
364
|
+
archived: `${ui.state("done")} archived`,
|
|
365
|
+
ready: `${ui.state("ready")} ready`,
|
|
366
|
+
drafting: `${ui.state("doing")} drafting`,
|
|
367
|
+
"not written": `${ui.state("todo")} opened`,
|
|
368
|
+
"not started": `${ui.state("todo")} not started`,
|
|
369
|
+
};
|
|
370
|
+
const states = modules.map((m) => moduleStatus(projectDir, m, brief));
|
|
371
|
+
const done = states.filter((s) => s.state === "archived").length;
|
|
372
|
+
return (states.map((s) => `${s.module} ${mark[s.state]}`).join(ui.muted(` ${ui_1.sym.bullet} `)) +
|
|
373
|
+
ui.muted(` (${done} of ${modules.length} archived)`));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A second `--agent` pass on a draft with findings: the agent cannot run the
|
|
377
|
+
* checker (it may only edit), so it gets the findings in its prompt — without
|
|
378
|
+
* them it would redraft blind and leave the same ones behind.
|
|
379
|
+
*/
|
|
380
|
+
function openFindings(projectDir, changeId, brief) {
|
|
381
|
+
if (!fs.existsSync(path.join(projectDir, CHANGES, changeId)))
|
|
382
|
+
return "";
|
|
383
|
+
const result = draftReport(projectDir, changeId, brief);
|
|
384
|
+
if ("failure" in result || result.report.status.length === 0)
|
|
385
|
+
return "";
|
|
386
|
+
const lines = result.report.status.map((d) => `- ${d.code}: ${d.message}${d.fix ? `\n fix: ${d.fix}` : ""}`);
|
|
387
|
+
return ("\n\n---\n\n## The draft already exists. Fix these findings, and only these:\n\n" +
|
|
388
|
+
lines.join("\n") +
|
|
389
|
+
"\n");
|
|
390
|
+
}
|
|
391
|
+
/** The first module of the brief, other than this one, not yet archived. */
|
|
392
|
+
function nextModuleAfter(projectDir, brief, current) {
|
|
393
|
+
const text = readIf(path.resolve(projectDir, brief));
|
|
394
|
+
if (text === undefined)
|
|
395
|
+
return null;
|
|
396
|
+
return ((0, DraftChecklist_1.briefModules)(text).find((m) => m !== current && moduleStatus(projectDir, m, brief).state !== "archived") || null);
|
|
397
|
+
}
|
|
398
|
+
/** `docs/specs/BRIEF-SETUP.md`: the guide an agent follows to propose the front matter. */
|
|
399
|
+
function writeSetupGuide(projectDir, brief) {
|
|
400
|
+
const file = path.join(projectDir, brief_setup_1.BRIEF_SETUP_FILE);
|
|
401
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
402
|
+
fs.writeFileSync(file, (0, brief_setup_1.briefSetupGuide)(brief), "utf8");
|
|
403
|
+
}
|
|
404
|
+
/** The next free SCN number, past every scenario the project or a change uses. */
|
|
405
|
+
function nextScenario(projectDir) {
|
|
406
|
+
let max = 0;
|
|
407
|
+
for (const id of projectIds(projectDir).keys()) {
|
|
408
|
+
const m = /^SCN-(\d+)$/.exec(id);
|
|
409
|
+
if (m)
|
|
410
|
+
max = Math.max(max, Number(m[1]));
|
|
411
|
+
}
|
|
412
|
+
const walk = (dir) => {
|
|
413
|
+
let entries = [];
|
|
414
|
+
try {
|
|
415
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
for (const e of entries) {
|
|
421
|
+
const full = path.join(dir, e.name);
|
|
422
|
+
if (e.isDirectory())
|
|
423
|
+
walk(full);
|
|
424
|
+
else if (e.name.endsWith(".md") || e.name.endsWith(".feature")) {
|
|
425
|
+
for (const m of fs.readFileSync(full, "utf8").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
426
|
+
max = Math.max(max, Number(m[1]));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
walk(path.join(projectDir, "docs", "specs"));
|
|
432
|
+
walk(path.join(projectDir, "features"));
|
|
433
|
+
for (const m of (readIf(path.join(projectDir, "spec.md")) || "").matchAll(/\bSCN-(\d+)\b/g)) {
|
|
434
|
+
max = Math.max(max, Number(m[1]));
|
|
435
|
+
}
|
|
436
|
+
return `SCN-${String(max + 1).padStart(3, "0")}`;
|
|
437
|
+
}
|
|
438
|
+
/** The one line to paste into any agent. */
|
|
439
|
+
function pasteLine(module) {
|
|
440
|
+
return `Draft the ${module} module: follow ${CHANGES.split(path.sep).join("/")}/draft-${module}/DRAFTING.md`;
|
|
441
|
+
}
|
|
442
|
+
function proposalFor(module, changeId, brief) {
|
|
443
|
+
return [
|
|
444
|
+
`# Proposal: ${changeId}`,
|
|
445
|
+
"",
|
|
446
|
+
"## Intent",
|
|
447
|
+
"",
|
|
448
|
+
`The ${module} module of the brief (\`${brief}\`), written down as requirements and`,
|
|
449
|
+
"scenarios a person can review before anything is built.",
|
|
450
|
+
"",
|
|
451
|
+
"## Scope",
|
|
452
|
+
"",
|
|
453
|
+
`In scope: what the brief says about ${module}.`,
|
|
454
|
+
"",
|
|
455
|
+
"Out of scope: every other module of the brief.",
|
|
456
|
+
"",
|
|
457
|
+
].join("\n");
|
|
458
|
+
}
|
|
459
|
+
/** Open `draft-<module>`: the change, linked to the brief, with its guide. */
|
|
460
|
+
function openDraft(projectDir, module, brief, o) {
|
|
461
|
+
const changeId = `draft-${module}`;
|
|
462
|
+
const dir = path.join(projectDir, CHANGES, changeId);
|
|
463
|
+
const reqRange = (0, ChangeWorkspace_1.reserveReqRange)(projectDir, o.reserve);
|
|
464
|
+
const config = {
|
|
465
|
+
...ChangeWorkspace_1.DEFAULT_CONFIG,
|
|
466
|
+
created: new Date().toISOString().slice(0, 10),
|
|
467
|
+
rigor: "lite",
|
|
468
|
+
req_range: reqRange,
|
|
469
|
+
};
|
|
470
|
+
const briefText = fs.readFileSync(path.resolve(projectDir, brief), "utf8");
|
|
471
|
+
const created = [];
|
|
472
|
+
const write = (rel, contents) => {
|
|
473
|
+
const full = path.join(dir, rel);
|
|
474
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
475
|
+
fs.writeFileSync(full, contents, "utf8");
|
|
476
|
+
created.push(path.join(CHANGES, changeId, rel).split(path.sep).join("/"));
|
|
477
|
+
};
|
|
478
|
+
write("DRAFTING.md", (0, drafting_guide_1.draftingGuide)({
|
|
479
|
+
module,
|
|
480
|
+
changeId,
|
|
481
|
+
brief,
|
|
482
|
+
reqRange,
|
|
483
|
+
firstScenario: nextScenario(projectDir),
|
|
484
|
+
maxRequirements: o.maxRequirements,
|
|
485
|
+
actors: (0, DraftChecklist_1.actorSurfaces)(briefText),
|
|
486
|
+
}));
|
|
487
|
+
write(path.join("specs", module, "spec.md"), (0, ChangeTemplates_1.templateDelta)(module, reqRange[0], (0, DiskLanguageRepository_1.phrases)(projectDir)));
|
|
488
|
+
write("change.yaml", `${(0, ChangeWorkspace_1.renderConfig)(config).trimEnd()}\nbrief: ${brief}\n`);
|
|
489
|
+
write("proposal.md", proposalFor(module, changeId, brief));
|
|
490
|
+
return { changeId, reqRange, created };
|
|
491
|
+
}
|
|
492
|
+
const ICON = {
|
|
493
|
+
"not started": () => ui.state("todo"),
|
|
494
|
+
"not written": () => ui.state("todo"),
|
|
495
|
+
drafting: () => ui.state("doing"),
|
|
496
|
+
ready: () => ui.state("ready"),
|
|
497
|
+
archived: () => ui.state("done"),
|
|
498
|
+
};
|
|
499
|
+
function describe(s) {
|
|
500
|
+
if (s.state === "drafting")
|
|
501
|
+
return `drafting — ${s.findings} finding(s) to fix`;
|
|
502
|
+
if (s.state === "ready")
|
|
503
|
+
return "ready to review";
|
|
504
|
+
if (s.state === "archived")
|
|
505
|
+
return `archived (${s.archivedAs})`;
|
|
506
|
+
if (s.state === "not written")
|
|
507
|
+
return "opened, not written yet";
|
|
508
|
+
return "not started";
|
|
509
|
+
}
|
|
510
|
+
/** What to run after this module, given where it stands. */
|
|
511
|
+
function nextFor(s) {
|
|
512
|
+
return s ? draftNextCommand(s) : "specgate status";
|
|
513
|
+
}
|
|
244
514
|
class DraftCommand extends command_1.BaseCommand {
|
|
245
515
|
execute() {
|
|
246
516
|
const io0 = (0, agent_1.agentIo)((0, agent_1.wantsJson)(this.args));
|
|
@@ -254,6 +524,428 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
254
524
|
]);
|
|
255
525
|
}
|
|
256
526
|
const opts = o;
|
|
527
|
+
if (opts.check)
|
|
528
|
+
return this.check(opts);
|
|
529
|
+
if (opts.module)
|
|
530
|
+
return this.module(opts);
|
|
531
|
+
return this.overview(opts);
|
|
532
|
+
}
|
|
533
|
+
/** The brief's path and text, or the failure that says where to put it. */
|
|
534
|
+
brief(projectDir, opts, linked) {
|
|
535
|
+
const rel = opts.brief || linked || exports.DEFAULT_BRIEF;
|
|
536
|
+
const text = readIf(path.resolve(projectDir, rel));
|
|
537
|
+
if (text !== undefined)
|
|
538
|
+
return { rel, text };
|
|
539
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
540
|
+
io.fail({ draft: null }, [
|
|
541
|
+
(0, diagnostics_1.error)("draft_no_brief", `No plan or brief at ${rel}.`, {
|
|
542
|
+
fix: `Save your plan or brief as ${exports.DEFAULT_BRIEF} — any markdown — then run \`specgate draft\`. ` +
|
|
543
|
+
"Somewhere else: --brief <file>.",
|
|
544
|
+
}),
|
|
545
|
+
]);
|
|
546
|
+
}
|
|
547
|
+
// ── specgate draft ──────────────────────────────────────────────────────────
|
|
548
|
+
overview(opts) {
|
|
549
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
550
|
+
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
551
|
+
const rel = opts.brief || exports.DEFAULT_BRIEF;
|
|
552
|
+
const file = path.resolve(projectDir, rel);
|
|
553
|
+
if (opts.newBrief)
|
|
554
|
+
return this.newBrief(opts, file, rel);
|
|
555
|
+
const text = readIf(file);
|
|
556
|
+
// (a) No brief: what one is, and how to get one.
|
|
557
|
+
if (text === undefined) {
|
|
558
|
+
if (!opts.json) {
|
|
559
|
+
process.stdout.write(ui.header("draft") + `${(0, brief_setup_1.noBriefExplanation)(rel)}\n`);
|
|
560
|
+
process.exitCode = 1;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
io.fail({ draft: null }, [
|
|
564
|
+
(0, diagnostics_1.error)("draft_no_brief", `No plan or brief at ${rel}.`, {
|
|
565
|
+
fix: `Save your plan or brief as ${rel} — any markdown — then run \`specgate draft\`. ` +
|
|
566
|
+
"No plan yet: `specgate draft --new-brief` writes one to fill in. Somewhere else: --brief <file>.",
|
|
567
|
+
}),
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
const brief = { rel, text: text };
|
|
571
|
+
if (opts.agent)
|
|
572
|
+
return this.frontMatterWithAgent(opts, projectDir, brief);
|
|
573
|
+
// (b) No front matter: every field, explained, and the agent to propose it.
|
|
574
|
+
if ((0, brief_setup_1.frontMatter)(brief.text) === null) {
|
|
575
|
+
writeSetupGuide(projectDir, rel);
|
|
576
|
+
io.emit({
|
|
577
|
+
draft: {
|
|
578
|
+
brief: rel,
|
|
579
|
+
frontMatter: null,
|
|
580
|
+
guide: brief_setup_1.BRIEF_SETUP_FILE,
|
|
581
|
+
prompt: brief_setup_1.BRIEF_SETUP_PASTE,
|
|
582
|
+
next: 'specgate draft --agent "claude -p"',
|
|
583
|
+
},
|
|
584
|
+
}, () => process.stdout.write(ui.header("draft", rel) +
|
|
585
|
+
(0, brief_setup_1.frontMatterExplanation)(rel) +
|
|
586
|
+
"\n" +
|
|
587
|
+
ui.section("Or let your agent propose it") +
|
|
588
|
+
" Paste this into Claude Code, Cursor, Codex…\n\n" +
|
|
589
|
+
ui.quote(brief_setup_1.BRIEF_SETUP_PASTE) +
|
|
590
|
+
ui.next('specgate draft --agent "claude -p"', "Specgate calls your agent for you — then review the four fields and run specgate draft again")));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// (c) A front matter with problems: each in plain words, with its fix.
|
|
594
|
+
const problems = (0, brief_setup_1.checkFrontMatter)(brief.text);
|
|
595
|
+
if (problems.length > 0) {
|
|
596
|
+
io.emit({
|
|
597
|
+
draft: { brief: rel, frontMatter: "invalid", next: `edit ${rel}, then specgate draft` },
|
|
598
|
+
status: problems.map((p) => (0, diagnostics_1.error)(p.code, p.message, { fix: p.fix })),
|
|
599
|
+
}, () => {
|
|
600
|
+
const out = process.stdout;
|
|
601
|
+
out.write(ui.header("draft", rel));
|
|
602
|
+
out.write(` The front matter at the top of ${rel} needs ${problems.length} fix(es):\n\n`);
|
|
603
|
+
for (const p of problems)
|
|
604
|
+
out.write(ui.fail(p.message) + ui.fix(p.fix));
|
|
605
|
+
out.write(ui.next("specgate draft", `once they are fixed in ${rel}`));
|
|
606
|
+
});
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
// (d) Ready: the modules, where each stands, what is next.
|
|
611
|
+
const modules = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
612
|
+
const states = modules.map((m) => moduleStatus(projectDir, m, rel));
|
|
613
|
+
const pending = states.find((s) => s.state !== "archived");
|
|
614
|
+
const next = nextFor(pending);
|
|
615
|
+
const product = /^product:\s*(.+)$/m.exec((0, brief_setup_1.frontMatter)(brief.text) || "");
|
|
616
|
+
io.emit({ draft: { brief: rel, modules: states, next } }, () => {
|
|
617
|
+
const out = process.stdout;
|
|
618
|
+
out.write(ui.header("draft", rel));
|
|
619
|
+
if (product)
|
|
620
|
+
out.write(ui.kv("Product", ui.bold(product[1].trim()), 10));
|
|
621
|
+
const archived = states.filter((s) => s.state === "archived").length;
|
|
622
|
+
out.write(ui.kv("Progress", ui.progress(archived, states.length, "archived"), 10));
|
|
623
|
+
out.write("\n" + ui.section("Modules"));
|
|
624
|
+
out.write(ui.table(states.map((s) => [`${ICON[s.state]()} ${s.module}`, ui.muted(describe(s))])));
|
|
625
|
+
out.write(pending
|
|
626
|
+
? ui.next(next)
|
|
627
|
+
: "\n" +
|
|
628
|
+
ui.ok("Every module is drafted and archived.") +
|
|
629
|
+
ui.next(next, "what to build first"));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
/** `--new-brief`: the template, never over a brief that exists. */
|
|
633
|
+
newBrief(opts, file, rel) {
|
|
634
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
635
|
+
if (fs.existsSync(file)) {
|
|
636
|
+
io.fail({ draft: null }, [
|
|
637
|
+
(0, diagnostics_1.error)("draft_brief_exists", `${rel} already exists — nothing was written.`, {
|
|
638
|
+
fix: "Edit it, then run `specgate draft`; it says what the top of the brief still needs.",
|
|
639
|
+
}),
|
|
640
|
+
]);
|
|
641
|
+
}
|
|
642
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
643
|
+
fs.writeFileSync(file, brief_setup_1.BRIEF_TEMPLATE, "utf8");
|
|
644
|
+
io.emit({ draft: { brief: rel, written: true, next: "specgate draft" } }, () => process.stdout.write(ui.header("draft --new-brief") +
|
|
645
|
+
ui.ok(`Wrote ${rel} — a brief to fill in.`) +
|
|
646
|
+
"\n Fill it in, in your own words:\n" +
|
|
647
|
+
" 1. The four fields at the top — product, surfaces, actors, modules.\n" +
|
|
648
|
+
" Each has an example in the comment above it.\n" +
|
|
649
|
+
" 2. The sections below — what the product is, who uses it and how, what\n" +
|
|
650
|
+
" each one must be able to do, the rules and numbers you already know,\n" +
|
|
651
|
+
" what you don't know yet, and what is out of scope.\n\n" +
|
|
652
|
+
" Have a plan or an email already? Paste it under the four fields instead\n" +
|
|
653
|
+
" of the sections — Specgate reads it as it is.\n" +
|
|
654
|
+
ui.next("specgate draft", "it checks the four fields and says what is next")));
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* `draft --agent` without a module: the agent proposes the front matter.
|
|
658
|
+
* It may write only the brief, and the brief below the front matter must
|
|
659
|
+
* come back byte for byte — it is the client's words, and every draft is
|
|
660
|
+
* checked against them.
|
|
661
|
+
*/
|
|
662
|
+
frontMatterWithAgent(opts, projectDir, brief) {
|
|
663
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
664
|
+
if ((0, brief_setup_1.frontMatter)(brief.text) !== null && (0, brief_setup_1.checkFrontMatter)(brief.text).length === 0) {
|
|
665
|
+
const first = (0, DraftChecklist_1.briefModules)(brief.text)[0];
|
|
666
|
+
io.emit({ draft: { brief: brief.rel, frontMatter: "ok", next: `specgate draft ${first}` } }, () => process.stdout.write(ui.header("draft --agent", brief.rel) +
|
|
667
|
+
ui.ok(`The front matter of ${brief.rel} is already there and complete — nothing for the agent to do.`) +
|
|
668
|
+
ui.next(`specgate draft ${first}`)));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
writeSetupGuide(projectDir, brief.rel);
|
|
672
|
+
const own = path
|
|
673
|
+
.relative(projectDir, path.resolve(projectDir, brief.rel))
|
|
674
|
+
.split(path.sep)
|
|
675
|
+
.join("/");
|
|
676
|
+
const agent = (0, confined_agent_1.withWritePermission)(opts.agent);
|
|
677
|
+
if (!opts.json) {
|
|
678
|
+
process.stdout.write(ui.header("draft --agent", brief.rel) +
|
|
679
|
+
agentLine(agent.command, `is proposing the front matter of ${brief.rel}`, agent.added));
|
|
680
|
+
}
|
|
681
|
+
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
682
|
+
projectDir,
|
|
683
|
+
changeId: "brief",
|
|
684
|
+
allowed: (rel) => rel === own,
|
|
685
|
+
command: agent.command,
|
|
686
|
+
prompt: (0, brief_setup_1.briefSetupPrompt)((0, brief_setup_1.briefSetupGuide)(brief.rel)),
|
|
687
|
+
showOutput: !opts.json,
|
|
688
|
+
});
|
|
689
|
+
if (run.problem === "no_git") {
|
|
690
|
+
io.fail({ draft: null }, [
|
|
691
|
+
(0, diagnostics_1.error)("draft_agent_needs_git", "--agent needs a git repository.", {
|
|
692
|
+
fix: "Git is how Specgate finds what the agent wrote and undoes anything else. Run `git init`, " +
|
|
693
|
+
`then try again — or paste this into your agent yourself: ${brief_setup_1.BRIEF_SETUP_PASTE}`,
|
|
694
|
+
}),
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
if (run.reverted.length > 0 && !opts.json) {
|
|
698
|
+
process.stdout.write("\n" +
|
|
699
|
+
ui.warn(`${ui_1.sym.undo} Undone — the agent wrote outside the brief: ${run.reverted.join(", ")}`));
|
|
700
|
+
}
|
|
701
|
+
const after = readIf(path.resolve(projectDir, brief.rel)) ?? "";
|
|
702
|
+
if ((0, brief_setup_1.briefBody)(after) !== (0, brief_setup_1.briefBody)(brief.text)) {
|
|
703
|
+
fs.writeFileSync(path.resolve(projectDir, brief.rel), brief.text, "utf8");
|
|
704
|
+
io.fail({ draft: null }, [
|
|
705
|
+
(0, diagnostics_1.error)("draft_brief_body_changed", "The agent changed the brief itself, not only its front matter — undone.", {
|
|
706
|
+
fix: `${brief.rel} is back as it was. Try again, or write the four fields yourself (\`specgate draft\` shows them).`,
|
|
707
|
+
}),
|
|
708
|
+
]);
|
|
709
|
+
}
|
|
710
|
+
if (!run.ok) {
|
|
711
|
+
io.fail({ draft: null }, [
|
|
712
|
+
(0, diagnostics_1.error)("draft_agent_failed", `The agent ${run.detail}.`, {
|
|
713
|
+
fix: `Check the command runs on its own, or paste this into your agent: ${brief_setup_1.BRIEF_SETUP_PASTE}`,
|
|
714
|
+
}),
|
|
715
|
+
]);
|
|
716
|
+
}
|
|
717
|
+
const proposed = (0, brief_setup_1.frontMatter)(after);
|
|
718
|
+
if (proposed === null) {
|
|
719
|
+
io.fail({ draft: null }, [
|
|
720
|
+
(0, diagnostics_1.error)("draft_no_front_matter", "The agent wrote no front matter.", {
|
|
721
|
+
fix: `${confined_agent_1.NO_WRITE_HINT} Or run \`specgate draft\` to see the four fields, and write them yourself.`,
|
|
722
|
+
}),
|
|
723
|
+
]);
|
|
724
|
+
}
|
|
725
|
+
const problems = (0, brief_setup_1.checkFrontMatter)(after);
|
|
726
|
+
io.emit({
|
|
727
|
+
draft: { brief: brief.rel, frontMatter: proposed, next: "specgate draft" },
|
|
728
|
+
status: problems.map((p) => (0, diagnostics_1.error)(p.code, p.message, { fix: p.fix })),
|
|
729
|
+
}, () => {
|
|
730
|
+
const out = process.stdout;
|
|
731
|
+
out.write(ui.rule() + "\n");
|
|
732
|
+
out.write(ui.ok(`The agent proposed this, at the top of ${brief.rel}:`) + "\n");
|
|
733
|
+
out.write(ui.block(["---", ...proposed.split("\n"), "---"].join("\n")));
|
|
734
|
+
out.write("\n" + ui.info("Your brief below it is unchanged."));
|
|
735
|
+
if (problems.length > 0) {
|
|
736
|
+
out.write(`\n It still needs ${problems.length} fix(es):\n`);
|
|
737
|
+
for (const p of problems)
|
|
738
|
+
out.write(ui.fail(p.message) + ui.fix(p.fix));
|
|
739
|
+
}
|
|
740
|
+
out.write(ui.next("specgate draft", "review it — change anything — then run it"));
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
// ── specgate draft <module> ─────────────────────────────────────────────────
|
|
744
|
+
module(opts) {
|
|
745
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
746
|
+
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
747
|
+
const changeId = `draft-${opts.module}`;
|
|
748
|
+
const status = moduleStatus(projectDir, opts.module);
|
|
749
|
+
if (status.state === "archived") {
|
|
750
|
+
io.emit({
|
|
751
|
+
draft: {
|
|
752
|
+
module: opts.module,
|
|
753
|
+
change: changeId,
|
|
754
|
+
state: "archived",
|
|
755
|
+
next: "specgate draft",
|
|
756
|
+
},
|
|
757
|
+
}, () => process.stdout.write(ui.header(`draft ${opts.module}`) +
|
|
758
|
+
ui.ok(`${opts.module} is already drafted and archived ${ui.muted(`(${status.archivedAs})`)}`) +
|
|
759
|
+
ui.next("specgate draft", "the next module of the brief, or what to build first")));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (status.state === "not started") {
|
|
763
|
+
if (!fs.existsSync(path.join(projectDir, "spec.md"))) {
|
|
764
|
+
io.fail({ draft: null }, [
|
|
765
|
+
(0, diagnostics_1.error)("draft_no_project", "This folder is not a Specgate project yet.", {
|
|
766
|
+
fix: "Run `specgate init` first, then `specgate draft " + opts.module + "`.",
|
|
767
|
+
}),
|
|
768
|
+
]);
|
|
769
|
+
}
|
|
770
|
+
const brief = this.brief(projectDir, opts);
|
|
771
|
+
const listed = (0, DraftChecklist_1.briefModules)(brief.text);
|
|
772
|
+
const opened = openDraft(projectDir, opts.module, brief.rel, opts);
|
|
773
|
+
const notes = listed.length > 0 && !listed.includes(opts.module)
|
|
774
|
+
? [
|
|
775
|
+
(0, diagnostics_1.info)("draft_module_not_in_brief", `${opts.module} is not among the brief's modules (${listed.join(", ")}).`, {
|
|
776
|
+
fix: "Add it to `modules:` in the brief's front matter, so `specgate draft` lists it.",
|
|
777
|
+
}),
|
|
778
|
+
]
|
|
779
|
+
: [];
|
|
780
|
+
if (opts.agent)
|
|
781
|
+
return this.withAgent(opts, projectDir, brief.rel, opened.created);
|
|
782
|
+
io.emit({
|
|
783
|
+
draft: {
|
|
784
|
+
module: opts.module,
|
|
785
|
+
change: opened.changeId,
|
|
786
|
+
state: "not written",
|
|
787
|
+
brief: brief.rel,
|
|
788
|
+
reserved: opened.reqRange,
|
|
789
|
+
created: opened.created,
|
|
790
|
+
prompt: pasteLine(opts.module),
|
|
791
|
+
next: `specgate draft ${opts.module}`,
|
|
792
|
+
},
|
|
793
|
+
status: notes,
|
|
794
|
+
}, () => {
|
|
795
|
+
const out = process.stdout;
|
|
796
|
+
const dir = `${CHANGES.split(path.sep).join("/")}/${opened.changeId}`;
|
|
797
|
+
const files = [
|
|
798
|
+
[`${dir}/DRAFTING.md`, "how to write it"],
|
|
799
|
+
[`${dir}/specs/${opts.module}/spec.md`, "the requirements go here"],
|
|
800
|
+
[`${dir}/change.yaml, proposal.md`, ""],
|
|
801
|
+
];
|
|
802
|
+
const w = Math.max(...files.map((r) => r[0].length));
|
|
803
|
+
out.write(ui.header(`draft ${opts.module}`, brief.rel) +
|
|
804
|
+
ui.ok(`Draft ${ui.bold(opened.changeId)} opened ` +
|
|
805
|
+
ui.muted(`· REQ ids ${opened.reqRange[0]}…${opened.reqRange[1]} reserved`)) +
|
|
806
|
+
"\n" +
|
|
807
|
+
files.map(([f, why]) => ui.fileLine("added", why ? ui.pad(f, w) : f, why)).join("") +
|
|
808
|
+
"\n" +
|
|
809
|
+
ui.section("Have your agent write it") +
|
|
810
|
+
" Paste this into Claude Code, Cursor, Codex…\n\n" +
|
|
811
|
+
ui.quote(pasteLine(opts.module)) +
|
|
812
|
+
ui.next(`specgate draft ${opts.module} --agent "claude -p"`, `or let Specgate call it — then run specgate draft ${opts.module} until it says Ready to review`));
|
|
813
|
+
for (const n of notes)
|
|
814
|
+
out.write(ui.info(n.message) + ui.fix(n.fix) + "\n");
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const brief = this.brief(projectDir, opts, briefOf(projectDir, changeId));
|
|
819
|
+
if (opts.agent)
|
|
820
|
+
return this.withAgent(opts, projectDir, brief.rel, []);
|
|
821
|
+
this.report(opts, projectDir, brief.rel);
|
|
822
|
+
}
|
|
823
|
+
/** Hand the guide to the agent, undo what it writes elsewhere, then check. */
|
|
824
|
+
withAgent(opts, projectDir, brief, created) {
|
|
825
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
826
|
+
const changeId = `draft-${opts.module}`;
|
|
827
|
+
const guide = readIf(path.join(projectDir, CHANGES, changeId, "DRAFTING.md")) || "";
|
|
828
|
+
const agent = (0, confined_agent_1.withWritePermission)(opts.agent);
|
|
829
|
+
if (!opts.json) {
|
|
830
|
+
process.stdout.write(ui.header(`draft ${opts.module}`, brief) +
|
|
831
|
+
(created.length ? ui.ok(`Draft ${changeId} opened, linked to ${brief}.`) : "") +
|
|
832
|
+
agentLine(agent.command, `is drafting ${opts.module}`, agent.added));
|
|
833
|
+
}
|
|
834
|
+
const run = (0, confined_agent_1.runConfinedAgent)({
|
|
835
|
+
projectDir,
|
|
836
|
+
changeId,
|
|
837
|
+
command: agent.command,
|
|
838
|
+
prompt: (0, drafting_guide_1.draftingPrompt)(opts.module, changeId, guide) + openFindings(projectDir, changeId, brief),
|
|
839
|
+
showOutput: !opts.json,
|
|
840
|
+
});
|
|
841
|
+
if (run.problem === "no_git") {
|
|
842
|
+
io.fail({ draft: null }, [
|
|
843
|
+
(0, diagnostics_1.error)("draft_agent_needs_git", "--agent needs a git repository.", {
|
|
844
|
+
fix: "Git is how Specgate finds what the agent wrote and undoes anything outside the draft. " +
|
|
845
|
+
"Run `git init`, then `specgate draft " +
|
|
846
|
+
opts.module +
|
|
847
|
+
' --agent "…"` again — or paste the prompt into your agent yourself.',
|
|
848
|
+
}),
|
|
849
|
+
]);
|
|
850
|
+
}
|
|
851
|
+
if (run.reverted.length > 0 && !opts.json) {
|
|
852
|
+
process.stdout.write("\n" +
|
|
853
|
+
ui.warn(`${ui_1.sym.undo} Undone — the agent wrote outside the draft: ${run.reverted.join(", ")}`));
|
|
854
|
+
}
|
|
855
|
+
if (!run.ok) {
|
|
856
|
+
io.fail({ draft: null }, [
|
|
857
|
+
(0, diagnostics_1.error)("draft_agent_failed", `The agent ${run.detail}.`, {
|
|
858
|
+
fix: `Check the command runs on its own, or paste this into your agent: ${pasteLine(opts.module)}`,
|
|
859
|
+
}),
|
|
860
|
+
]);
|
|
861
|
+
}
|
|
862
|
+
this.report(opts, projectDir, brief, run.reverted, true);
|
|
863
|
+
}
|
|
864
|
+
/** The checklist on `draft-<module>`, and what comes next. */
|
|
865
|
+
report(opts, projectDir, brief, reverted = [], afterAgent = false) {
|
|
866
|
+
const io = (0, agent_1.agentIo)(opts.json);
|
|
867
|
+
const changeId = `draft-${opts.module}`;
|
|
868
|
+
const status = moduleStatus(projectDir, opts.module, brief);
|
|
869
|
+
// The agent's own output is long and ends just above this: a rule marks
|
|
870
|
+
// where Specgate takes over again, so the next step is not lost in it (#96).
|
|
871
|
+
if (afterAgent && !opts.json)
|
|
872
|
+
process.stdout.write(ui.rule());
|
|
873
|
+
if (status.state === "not written") {
|
|
874
|
+
io.emit({
|
|
875
|
+
draft: {
|
|
876
|
+
module: opts.module,
|
|
877
|
+
change: changeId,
|
|
878
|
+
state: status.state,
|
|
879
|
+
brief,
|
|
880
|
+
prompt: pasteLine(opts.module),
|
|
881
|
+
next: `specgate draft ${opts.module}`,
|
|
882
|
+
},
|
|
883
|
+
}, () => process.stdout.write((afterAgent ? "\n" : ui.header(`draft ${opts.module}`, brief)) +
|
|
884
|
+
(afterAgent
|
|
885
|
+
? ui.fail("The agent ran, but the draft is still the placeholder.") +
|
|
886
|
+
ui.fix(confined_agent_1.NO_WRITE_HINT) +
|
|
887
|
+
"\n"
|
|
888
|
+
: " The draft is still the placeholder. Have your agent write it:\n\n" +
|
|
889
|
+
ui.quote(pasteLine(opts.module)) +
|
|
890
|
+
ui.next(`specgate draft ${opts.module} --agent "claude -p"`, "or let Specgate call it"))));
|
|
891
|
+
process.exitCode = 1;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const result = draftReport(projectDir, changeId, brief, opts.maxRequirements);
|
|
895
|
+
if ("failure" in result) {
|
|
896
|
+
io.fail({ draft: null }, [result.failure]);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const report = result.report;
|
|
900
|
+
const diags = [...report.status, ...skippedNotes(report)];
|
|
901
|
+
const errors = report.status.length;
|
|
902
|
+
const next = errors === 0 ? `specgate change archive ${changeId}` : `specgate draft ${opts.module}`;
|
|
903
|
+
io.emit({
|
|
904
|
+
draft: {
|
|
905
|
+
module: opts.module,
|
|
906
|
+
change: changeId,
|
|
907
|
+
state: errors === 0 ? "ready" : "drafting",
|
|
908
|
+
brief,
|
|
909
|
+
requirements: report.requirements,
|
|
910
|
+
scenarios: report.scenarios,
|
|
911
|
+
skipped: report.skipped,
|
|
912
|
+
reverted,
|
|
913
|
+
next,
|
|
914
|
+
},
|
|
915
|
+
status: diags,
|
|
916
|
+
}, () => {
|
|
917
|
+
const out = process.stdout;
|
|
918
|
+
out.write(afterAgent ? "\n" : ui.header(`draft ${opts.module}`, brief));
|
|
919
|
+
writeFindings(report, diags);
|
|
920
|
+
const dir = `${CHANGES.split(path.sep).join("/")}/${changeId}`;
|
|
921
|
+
const steps = [];
|
|
922
|
+
if (errors === 0) {
|
|
923
|
+
out.write(`${diags.length ? "\n" : ""}` + ui.ok(ui.bold("Ready to review")));
|
|
924
|
+
steps.push(`Read ${dir}/specs/ with whoever knows the business`, fs.existsSync(path.join(projectDir, CHANGES, changeId, "questions.md"))
|
|
925
|
+
? `Answer what you can in ${dir}/questions.md`
|
|
926
|
+
: `Check ${dir}/assumptions.md, if the agent wrote one`, next);
|
|
927
|
+
const after = nextModuleAfter(projectDir, brief, opts.module);
|
|
928
|
+
steps.push(after
|
|
929
|
+
? [`specgate draft ${after}`, "the next module of the brief"]
|
|
930
|
+
: ["specgate status", "every module drafted: what to build first"]);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
out.write("\n" +
|
|
934
|
+
ui.fail(`${ui.bold(`${errors} finding(s) to fix`)} ${ui.muted("— each says how; DRAFTING.md has the formats")}`));
|
|
935
|
+
steps.push([
|
|
936
|
+
`specgate draft ${opts.module} --agent "claude -p"`,
|
|
937
|
+
"your agent fixes them — or fix them yourself",
|
|
938
|
+
], [next, "again, until it says Ready to review"]);
|
|
939
|
+
}
|
|
940
|
+
const progress = briefLine(projectDir, brief);
|
|
941
|
+
if (progress)
|
|
942
|
+
out.write(`\n ${ui_1.c.bold}Brief${ui_1.c.reset} ${progress}\n`);
|
|
943
|
+
out.write(ui.steps("What now", steps));
|
|
944
|
+
});
|
|
945
|
+
process.exitCode = errors === 0 ? 0 : 1;
|
|
946
|
+
}
|
|
947
|
+
// ── specgate draft --check <id> ─────────────────────────────────────────────
|
|
948
|
+
check(opts) {
|
|
257
949
|
const io = (0, agent_1.agentIo)(opts.json);
|
|
258
950
|
const projectDir = (0, project_root_1.resolveProjectDir)(opts.projectDir);
|
|
259
951
|
const result = draftReport(projectDir, opts.check, opts.brief, opts.maxRequirements);
|
|
@@ -262,12 +954,7 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
262
954
|
return;
|
|
263
955
|
}
|
|
264
956
|
const report = result.report;
|
|
265
|
-
const status = [
|
|
266
|
-
...report.status,
|
|
267
|
-
...report.skipped.map((s) => (0, diagnostics_1.info)("draft_rule_skipped", `${s.rule} did not run: ${s.why}.`, {
|
|
268
|
-
fix: "Pass --brief <file>, with actors and their surfaces in its front matter for D3.",
|
|
269
|
-
})),
|
|
270
|
-
];
|
|
957
|
+
const status = [...report.status, ...skippedNotes(report)];
|
|
271
958
|
const errors = report.status.length;
|
|
272
959
|
io.emit({
|
|
273
960
|
draft: {
|
|
@@ -279,13 +966,8 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
279
966
|
status,
|
|
280
967
|
}, () => {
|
|
281
968
|
const out = process.stdout;
|
|
282
|
-
out.write(
|
|
283
|
-
|
|
284
|
-
for (const d of status) {
|
|
285
|
-
out.write(` ${d.severity === "error" ? "✖" : "ℹ"} ${d.code} ${d.message}\n`);
|
|
286
|
-
if (d.fix && d.severity === "error")
|
|
287
|
-
out.write(` fix: ${d.fix}\n`);
|
|
288
|
-
}
|
|
969
|
+
out.write(ui.header(`draft --check ${opts.check}`));
|
|
970
|
+
writeFindings(report, status);
|
|
289
971
|
const skipped = report.skipped.length;
|
|
290
972
|
out.write(errors === 0
|
|
291
973
|
? `\n ✔ Ready to review${skipped ? ` (${skipped} rule(s) did not run — see above)` : ""}.\n`
|
|
@@ -295,4 +977,20 @@ class DraftCommand extends command_1.BaseCommand {
|
|
|
295
977
|
}
|
|
296
978
|
}
|
|
297
979
|
exports.DraftCommand = DraftCommand;
|
|
980
|
+
function skippedNotes(report) {
|
|
981
|
+
return report.skipped.map((s) => (0, diagnostics_1.info)("draft_rule_skipped", `${s.rule} did not run: ${s.why}.`, {
|
|
982
|
+
fix: "Pass --brief <file>, with actors and their surfaces in its front matter for D3.",
|
|
983
|
+
}));
|
|
984
|
+
}
|
|
985
|
+
function writeFindings(report, status) {
|
|
986
|
+
const out = process.stdout;
|
|
987
|
+
out.write(` ${ui_1.c.bold}${report.requirements}${ui_1.c.reset} requirement(s) ${ui.muted(ui_1.sym.bullet)} ` +
|
|
988
|
+
`${ui_1.c.bold}${report.scenarios}${ui_1.c.reset} scenario(s)\n\n`);
|
|
989
|
+
for (const d of status) {
|
|
990
|
+
const line = `${ui.muted(d.code)} ${d.message}`;
|
|
991
|
+
out.write(d.severity === "error" ? ui.fail(line) : ui.info(line));
|
|
992
|
+
if (d.fix && d.severity === "error")
|
|
993
|
+
out.write(ui.fix(d.fix));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
298
996
|
//# sourceMappingURL=DraftCommand.js.map
|