@pknx/waterfall-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/waterfall.mjs +14 -0
- package/lib/cli/agent/agent-message.ts +71 -0
- package/lib/cli/agent/agent-translators.ts +145 -0
- package/lib/cli/agent/backend-invoke.ts +133 -0
- package/lib/cli/agent/backends.ts +100 -0
- package/lib/cli/agent/global-prompts.ts +55 -0
- package/lib/cli/commands/bug-start.ts +115 -0
- package/lib/cli/commands/comment-add.ts +47 -0
- package/lib/cli/commands/cr-all.ts +18 -0
- package/lib/cli/commands/cr-finish.ts +176 -0
- package/lib/cli/commands/cr-start.ts +105 -0
- package/lib/cli/commands/cr-to-rq.ts +18 -0
- package/lib/cli/commands/export-pdf.ts +193 -0
- package/lib/cli/commands/horizontal/horizontal.ts +232 -0
- package/lib/cli/commands/horizontal-create.ts +34 -0
- package/lib/cli/commands/horizontal-update.ts +32 -0
- package/lib/cli/commands/join-hint.ts +4 -0
- package/lib/cli/commands/registry.ts +59 -0
- package/lib/cli/commands/resolve-operator-hint.ts +120 -0
- package/lib/cli/commands/rq-all.ts +18 -0
- package/lib/cli/commands/rq-to-uc.ts +18 -0
- package/lib/cli/commands/story-close.ts +124 -0
- package/lib/cli/commands/sync-work-items.ts +59 -0
- package/lib/cli/commands/sys-start.ts +96 -0
- package/lib/cli/commands/test-all.ts +18 -0
- package/lib/cli/commands/test-to-story.ts +18 -0
- package/lib/cli/commands/types.ts +33 -0
- package/lib/cli/commands/uc-all.ts +18 -0
- package/lib/cli/commands/uc-to-story.ts +18 -0
- package/lib/cli/commands/uc-to-test.ts +18 -0
- package/lib/cli/comments/item-comments.ts +285 -0
- package/lib/cli/config/dot-waterfall.ts +404 -0
- package/lib/cli/config/global-cli.ts +21 -0
- package/lib/cli/config/sync-work-item-config.ts +34 -0
- package/lib/cli/core/cli-help-spec.ts +833 -0
- package/lib/cli/core/cli-log.ts +124 -0
- package/lib/cli/core/exec-file.ts +8 -0
- package/lib/cli/core/prompt-map.ts +64 -0
- package/lib/cli/core/slug.ts +44 -0
- package/lib/cli/entry.ts +4 -0
- package/lib/cli/export/collect-md.ts +41 -0
- package/lib/cli/export/export-items.ts +104 -0
- package/lib/cli/export/export-pdf-path.ts +88 -0
- package/lib/cli/export/merge-md.ts +37 -0
- package/lib/cli/export/mermaid-run.ts +104 -0
- package/lib/cli/export/pandoc-pdf.ts +90 -0
- package/lib/cli/export/pdf-bundled-worker.mjs +73 -0
- package/lib/cli/export/pdf-bundled.ts +36 -0
- package/lib/cli/git/cr-agent-context.ts +62 -0
- package/lib/cli/git/git-branch-guards.ts +60 -0
- package/lib/cli/git/git-cli-mock.ts +191 -0
- package/lib/cli/git/git-cli.ts +24 -0
- package/lib/cli/main.ts +434 -0
- package/lib/cli/paths.ts +9 -0
- package/lib/cli/project/pom-json.ts +55 -0
- package/lib/cli/spec/spec-init.ts +216 -0
- package/lib/cli/spec/spec-root.ts +93 -0
- package/lib/cli/sync/apply-remote-comments.ts +87 -0
- package/lib/cli/sync/attachment-category.ts +43 -0
- package/lib/cli/sync/diff-work-items.ts +113 -0
- package/lib/cli/sync/materialize-remote-bugs.ts +66 -0
- package/lib/cli/sync/provider-types.ts +43 -0
- package/lib/cli/sync/providers/direct-provider.ts +27 -0
- package/lib/cli/sync/providers/jira-provider.ts +34 -0
- package/lib/cli/sync/providers/registry.ts +26 -0
- package/lib/cli/sync/run-sync-work-items.ts +202 -0
- package/lib/cli/sync/spec-work-items.ts +226 -0
- package/lib/cli/sync/sync-hint-json.ts +163 -0
- package/lib/cli/sync/work-item-meta.ts +117 -0
- package/lib/cli/work-items/infer-bug-sys.ts +147 -0
- package/lib/cli/work-items/remote-bug-import-scaffold.ts +32 -0
- package/lib/cli/work-items/write-bug-to-spec.ts +158 -0
- package/package.json +54 -0
- package/prompts/commands/bug-start.md +46 -0
- package/prompts/commands/cr-finish.md +44 -0
- package/prompts/commands/cr-start.md +65 -0
- package/prompts/commands/cr-to-rq.md +62 -0
- package/prompts/commands/horizontal-create.md +27 -0
- package/prompts/commands/horizontal-update.md +39 -0
- package/prompts/commands/rq-to-uc.md +62 -0
- package/prompts/commands/story-close-all.md +34 -0
- package/prompts/commands/story-close.md +44 -0
- package/prompts/commands/sync-bugs-refine-imports.md +33 -0
- package/prompts/commands/sys-start.md +63 -0
- package/prompts/commands/test-to-story.md +64 -0
- package/prompts/commands/uc-to-story.md +85 -0
- package/prompts/commands/uc-to-test.md +58 -0
- package/prompts/global/before-changing-spec.md +62 -0
- package/prompts/global/content-requirements-vs-use-cases.md +116 -0
- package/prompts/global/cursor-overview.md +31 -0
- package/prompts/global/git-usage.md +46 -0
- package/prompts/global/horizontal-structure.md +75 -0
- package/prompts/global/workflows-index.md +59 -0
- package/prompts/items/bug-document-structure.md +23 -0
- package/prompts/items/cr-document-structure.md +45 -0
- package/prompts/items/rq-theme-document-structure.md +36 -0
- package/prompts/items/story-document-structure.md +49 -0
- package/prompts/items/sys-document-structure.md +36 -0
- package/prompts/items/tst-document-structure.md +55 -0
- package/prompts/items/uc-document-structure.md +38 -0
- package/spec-template/README.md +11 -0
- package/spec-template/full/doc/spec-structure.md +16 -0
- package/spec-template/full/prompts/before-changing-spec.md +7 -0
- package/spec-template/full/prompts/workflows.md +25 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON shape for STORY/BUG sync: comparable snapshots from spec or remote trackers.
|
|
3
|
+
* Comments: `user` + `email` + body; spec `{STORY|BUG}-NNN.comments.json`; remote tracker. Compared on sync by `email` + `createdAt` + `user` + body.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Reference to an external system (Jira issue, Linear, etc.). */
|
|
7
|
+
export type WorkItemExternalRef = {
|
|
8
|
+
system: string;
|
|
9
|
+
id: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Comment thread entry — tracker and/or waterfall `{id}.comments.json`. Author identity is `email`; `user` is display name. */
|
|
14
|
+
export type WorkItemComment = {
|
|
15
|
+
/** Display name (waterfall `from`). */
|
|
16
|
+
user: string;
|
|
17
|
+
/** Stable author id (required for sync identity; same person as `user`). */
|
|
18
|
+
email: string;
|
|
19
|
+
/** ISO 8601 date-time */
|
|
20
|
+
createdAt: string;
|
|
21
|
+
body: string;
|
|
22
|
+
/** Remote comment id when syncing from a tracker */
|
|
23
|
+
externalId?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attachment bucket for sync (maps to PDF, images, video, Excel/Sheets, Word, …).
|
|
28
|
+
* Use {@link attachmentCategoryFromMimeOrName} when inferring from MIME or filename.
|
|
29
|
+
*/
|
|
30
|
+
export type WorkItemAttachmentCategory =
|
|
31
|
+
| "pdf"
|
|
32
|
+
| "image"
|
|
33
|
+
| "video"
|
|
34
|
+
| "spreadsheet"
|
|
35
|
+
| "document"
|
|
36
|
+
| "other";
|
|
37
|
+
|
|
38
|
+
export type WorkItemAttachment = {
|
|
39
|
+
/** Stable id (spec filename, Jira attachment id, UUID, …) */
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
category: WorkItemAttachmentCategory;
|
|
43
|
+
mimeType?: string;
|
|
44
|
+
sizeBytes?: number;
|
|
45
|
+
/** Spec-relative path, https URL, or opaque provider locator */
|
|
46
|
+
href?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** One work item in a provider-agnostic form suitable for diffing. */
|
|
50
|
+
export type WorkItemRecord = {
|
|
51
|
+
/** Stable id in waterfall-spec space, e.g. STORY-001, BUG-002 */
|
|
52
|
+
canonicalId: string;
|
|
53
|
+
kind: "story" | "bug";
|
|
54
|
+
/** Normalized lifecycle: open | closed | draft (extend as needed). */
|
|
55
|
+
state: string;
|
|
56
|
+
title: string;
|
|
57
|
+
/** SYS-NNN when inferable from path */
|
|
58
|
+
sysId?: string;
|
|
59
|
+
/** Path under spec root using / */
|
|
60
|
+
relPath?: string;
|
|
61
|
+
externalRefs?: WorkItemExternalRef[];
|
|
62
|
+
comments?: WorkItemComment[];
|
|
63
|
+
attachments?: WorkItemAttachment[];
|
|
64
|
+
/** Optional provider echo for debugging (not used in equality by default). */
|
|
65
|
+
providerPayload?: Record<string, unknown>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Sort nested arrays for deterministic JSON and diff. */
|
|
69
|
+
export function normalizeWorkItemRecord(r: WorkItemRecord): WorkItemRecord {
|
|
70
|
+
const { comments: c0, attachments: a0, ...rest } = r;
|
|
71
|
+
const comments = c0?.length
|
|
72
|
+
? [...c0].sort((a, b) =>
|
|
73
|
+
`${a.email}\t${a.createdAt}\t${a.user}`.localeCompare(
|
|
74
|
+
`${b.email}\t${b.createdAt}\t${b.user}`,
|
|
75
|
+
"en",
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
: undefined;
|
|
79
|
+
const attachments = a0?.length
|
|
80
|
+
? [...a0].sort((a, b) => a.id.localeCompare(b.id, "en"))
|
|
81
|
+
: undefined;
|
|
82
|
+
return {
|
|
83
|
+
...rest,
|
|
84
|
+
...(comments ? { comments } : {}),
|
|
85
|
+
...(attachments ? { attachments } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type WorkItemMetaDocument = {
|
|
90
|
+
source: string;
|
|
91
|
+
kind: "story" | "bug";
|
|
92
|
+
generatedAt: string;
|
|
93
|
+
specRoot?: string;
|
|
94
|
+
items: WorkItemRecord[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function createMetaDocument(
|
|
98
|
+
source: string,
|
|
99
|
+
kind: "story" | "bug",
|
|
100
|
+
items: WorkItemRecord[],
|
|
101
|
+
specRoot?: string,
|
|
102
|
+
): WorkItemMetaDocument {
|
|
103
|
+
return {
|
|
104
|
+
source,
|
|
105
|
+
kind,
|
|
106
|
+
generatedAt: new Date().toISOString(),
|
|
107
|
+
specRoot,
|
|
108
|
+
items: [...items]
|
|
109
|
+
.map(normalizeWorkItemRecord)
|
|
110
|
+
.sort((a, b) => a.canonicalId.localeCompare(b.canonicalId, "en")),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Deterministic JSON for file diff / tests (sorted keys, sorted items). */
|
|
115
|
+
export function stringifyWorkItemMeta(doc: WorkItemMetaDocument): string {
|
|
116
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseWorkItemCanonicalId } from "../comments/item-comments";
|
|
4
|
+
import type { WorkItemRecord } from "../sync/work-item-meta";
|
|
5
|
+
import { resolveBugParentSysDir } from "./write-bug-to-spec";
|
|
6
|
+
|
|
7
|
+
const SYS_DIR_RE = /^SYS-(\d{3})-/i;
|
|
8
|
+
|
|
9
|
+
function listSysFolderNames(technicalDir: string): string[] {
|
|
10
|
+
if (!fs.existsSync(technicalDir)) return [];
|
|
11
|
+
return fs
|
|
12
|
+
.readdirSync(technicalDir, { withFileTypes: true })
|
|
13
|
+
.filter((e) => e.isDirectory() && SYS_DIR_RE.test(e.name))
|
|
14
|
+
.map((e) => e.name)
|
|
15
|
+
.sort((a, b) => a.localeCompare(b, "en"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sysIdFromFolderName(folderName: string): string {
|
|
19
|
+
const m = folderName.match(SYS_DIR_RE);
|
|
20
|
+
if (!m?.[1]) throw new Error(`invalid SYS folder name: ${folderName}`);
|
|
21
|
+
return `SYS-${m[1]}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readFirstHeading(mdPath: string): string {
|
|
25
|
+
try {
|
|
26
|
+
const text = fs.readFileSync(mdPath, "utf8");
|
|
27
|
+
const line = text.match(/^#\s+([^\n]+)/m)?.[1]?.trim();
|
|
28
|
+
return line ?? "";
|
|
29
|
+
} catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tokenizeForOverlap(s: string): Set<string> {
|
|
35
|
+
const out = new Set<string>();
|
|
36
|
+
for (const w of s.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
37
|
+
if (w.length >= 3) out.add(w);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function overlapScore(a: string, b: string): number {
|
|
43
|
+
const ta = tokenizeForOverlap(a);
|
|
44
|
+
const tb = tokenizeForOverlap(b);
|
|
45
|
+
let n = 0;
|
|
46
|
+
for (const w of ta) {
|
|
47
|
+
if (tb.has(w)) n++;
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildSysCorpus(sysDir: string, folderName: string): string {
|
|
53
|
+
const parts: string[] = [folderName.replace(/-/g, " ")];
|
|
54
|
+
let sysMd: string | undefined;
|
|
55
|
+
try {
|
|
56
|
+
for (const f of fs.readdirSync(sysDir)) {
|
|
57
|
+
if (/^SYS-\d{3}.*\.md$/i.test(f)) {
|
|
58
|
+
sysMd = path.join(sysDir, f);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
if (sysMd) parts.push(readFirstHeading(sysMd));
|
|
66
|
+
try {
|
|
67
|
+
for (const e of fs.readdirSync(sysDir, { withFileTypes: true })) {
|
|
68
|
+
if (e.isDirectory() && /^STORY-\d{3}-/i.test(e.name)) {
|
|
69
|
+
parts.push(e.name.replace(/-/g, " "));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
return parts.join(" ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bugTextForInference(record: WorkItemRecord): string {
|
|
79
|
+
const bits = [record.title ?? ""];
|
|
80
|
+
for (const c of record.comments ?? []) {
|
|
81
|
+
bits.push(c.body);
|
|
82
|
+
bits.push(c.user);
|
|
83
|
+
}
|
|
84
|
+
return bits.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve **SYS-NNN** for nesting a remote bug: explicit `record.sysId`, else single subsystem,
|
|
89
|
+
* else best token overlap against SYS folder slug, SYS markdown title, and STORY folder names.
|
|
90
|
+
*/
|
|
91
|
+
export function inferSysIdForRemoteBug(
|
|
92
|
+
specRoot: string,
|
|
93
|
+
record: WorkItemRecord,
|
|
94
|
+
): string {
|
|
95
|
+
if (record.kind !== "bug") {
|
|
96
|
+
throw new Error("inferSysIdForRemoteBug: expected kind bug");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const root = path.resolve(specRoot);
|
|
100
|
+
const technical = path.join(root, "technical");
|
|
101
|
+
const dirs = listSysFolderNames(technical);
|
|
102
|
+
if (dirs.length === 0) {
|
|
103
|
+
throw new Error("no `technical/SYS-NNN-*` subsystem folders");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rawSys = record.sysId?.trim();
|
|
107
|
+
if (rawSys) {
|
|
108
|
+
const { prefix, canonicalId } = parseWorkItemCanonicalId(rawSys);
|
|
109
|
+
if (prefix !== "SYS") {
|
|
110
|
+
throw new Error(`sysId must be SYS-NNN (got "${rawSys}")`);
|
|
111
|
+
}
|
|
112
|
+
const parent = resolveBugParentSysDir(root, canonicalId);
|
|
113
|
+
if (!parent) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`no folder matches sysId ${canonicalId} under technical/ (create that subsystem or fix sysId)`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return canonicalId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (dirs.length === 1) {
|
|
122
|
+
return sysIdFromFolderName(dirs[0]!);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const bugText = bugTextForInference(record);
|
|
126
|
+
const scored = dirs.map((name) => {
|
|
127
|
+
const sysDir = path.join(technical, name);
|
|
128
|
+
const corpus = buildSysCorpus(sysDir, name);
|
|
129
|
+
return {
|
|
130
|
+
sysId: sysIdFromFolderName(name),
|
|
131
|
+
score: overlapScore(bugText, corpus),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
scored.sort((a, b) => b.score - a.score);
|
|
135
|
+
|
|
136
|
+
if (scored[0]!.score === 0) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`cannot infer SYS for ${record.canonicalId} (no keyword overlap with any subsystem); set "sysId" on the remote item. Subsystems: ${scored.map((s) => s.sysId).join(", ")}.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (scored.length > 1 && scored[0]!.score === scored[1]!.score) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`ambiguous SYS for ${record.canonicalId} (${scored[0]!.sysId} and ${scored[1]!.sysId} both score ${scored[0]!.score}); set "sysId" on the remote item.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return scored[0]!.sysId;
|
|
147
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WorkItemRecord } from "../sync/work-item-meta";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Summary body for a bug materialized from remote sync: same refinement contract as
|
|
5
|
+
* `bug start` (Draft steering → agent removes block after synthesizing Summary).
|
|
6
|
+
*/
|
|
7
|
+
export function buildRemoteBugImportSummaryBody(record: WorkItemRecord): string {
|
|
8
|
+
const commentBlocks =
|
|
9
|
+
record.comments?.length ?
|
|
10
|
+
record.comments
|
|
11
|
+
.map((c) => {
|
|
12
|
+
const quoted = c.body
|
|
13
|
+
.trim()
|
|
14
|
+
.split(/\r?\n/)
|
|
15
|
+
.map((line) => (line.length ? `> ${line}` : ">"))
|
|
16
|
+
.join("\n");
|
|
17
|
+
return `**${c.user}** (\`${c.email}\`, ${c.createdAt}):\n\n${quoted}\n`;
|
|
18
|
+
})
|
|
19
|
+
.join("\n")
|
|
20
|
+
: "_*(no comments in remote snapshot)*_\n";
|
|
21
|
+
|
|
22
|
+
return `*(Replace with a clear defect summary per \`prompts/items/bug-document-structure.md\`. **Synthesize** from **Draft steering (remote import)** below — remove that section when done.)*
|
|
23
|
+
|
|
24
|
+
## Draft steering (remote import)
|
|
25
|
+
|
|
26
|
+
**Remote title:** ${(record.title ?? "").trim() || "(empty)"}
|
|
27
|
+
|
|
28
|
+
**Tracker comments:**
|
|
29
|
+
|
|
30
|
+
${commentBlocks}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { slugifyDescription } from "../core/slug";
|
|
4
|
+
import { parseWorkItemCanonicalId } from "../comments/item-comments";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pick `technical/SYS-NNN-*` to nest a new BUG under.
|
|
8
|
+
* Uses `sysId` when set (`SYS-001`); otherwise the first existing `SYS-*` folder (sorted).
|
|
9
|
+
*/
|
|
10
|
+
export function resolveBugParentSysDir(
|
|
11
|
+
specRoot: string,
|
|
12
|
+
sysId?: string,
|
|
13
|
+
): string | undefined {
|
|
14
|
+
const technical = path.join(path.resolve(specRoot), "technical");
|
|
15
|
+
if (!fs.existsSync(technical)) return undefined;
|
|
16
|
+
const dirs = fs
|
|
17
|
+
.readdirSync(technical, { withFileTypes: true })
|
|
18
|
+
.filter((e) => e.isDirectory() && /^SYS-\d{3}-/i.test(e.name))
|
|
19
|
+
.map((e) => e.name)
|
|
20
|
+
.sort((a, b) => a.localeCompare(b, "en"));
|
|
21
|
+
if (dirs.length === 0) return undefined;
|
|
22
|
+
|
|
23
|
+
const want = sysId?.trim().match(/^SYS-(\d{3})$/i)?.[1];
|
|
24
|
+
if (want) {
|
|
25
|
+
const prefix = `SYS-${want}-`;
|
|
26
|
+
const hit = dirs.find((n) => n.toUpperCase().startsWith(prefix.toUpperCase()));
|
|
27
|
+
if (hit) return path.join(technical, hit);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return path.join(technical, dirs[0]!);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function bugFolderBaseName(canonicalId: string, title: string): string {
|
|
34
|
+
const { canonicalId: id } = parseWorkItemCanonicalId(canonicalId);
|
|
35
|
+
const m = id.match(/^BUG-(\d{3})$/i);
|
|
36
|
+
const pad = m?.[1] ?? "000";
|
|
37
|
+
const rawTitle = title?.trim() || "imported";
|
|
38
|
+
const stripped = rawTitle.replace(
|
|
39
|
+
new RegExp(`^BUG-${pad}\\s*[—-]?\\s*`, "i"),
|
|
40
|
+
"",
|
|
41
|
+
).trim();
|
|
42
|
+
const slug = slugifyDescription(stripped || rawTitle);
|
|
43
|
+
return `BUG-${pad}-${slug || "imported"}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function statusLineForBugMd(state: string): string {
|
|
47
|
+
const s = state.trim().toLowerCase();
|
|
48
|
+
if (s === "closed" || s === "draft" || s === "open" || s === "in progress") {
|
|
49
|
+
return s === "in progress" ? "in progress" : s;
|
|
50
|
+
}
|
|
51
|
+
return s || "open";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function bugHeadingLine(canonicalId: string, title: string): string {
|
|
55
|
+
const { canonicalId: id } = parseWorkItemCanonicalId(canonicalId);
|
|
56
|
+
const t = title?.trim() || id;
|
|
57
|
+
if (/^BUG-\d{3}\b/i.test(t)) return `# ${t}`;
|
|
58
|
+
return `# ${id} — ${t}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function sysLineForBugFromParent(
|
|
62
|
+
parentDir: string,
|
|
63
|
+
sysIdOverride?: string,
|
|
64
|
+
): string {
|
|
65
|
+
if (sysIdOverride?.trim()) return sysIdOverride.trim();
|
|
66
|
+
const base = path.basename(parentDir);
|
|
67
|
+
const m = base.match(/^(SYS-\d{3})/i);
|
|
68
|
+
return m?.[1]?.toUpperCase() ?? "TBD";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildBugMarkdownContent(input: {
|
|
72
|
+
canonicalId: string;
|
|
73
|
+
title: string;
|
|
74
|
+
state: string;
|
|
75
|
+
sysLine: string;
|
|
76
|
+
summaryBody: string;
|
|
77
|
+
}): string {
|
|
78
|
+
const { canonicalId } = parseWorkItemCanonicalId(input.canonicalId);
|
|
79
|
+
const summary = input.summaryBody.replace(/\n+$/, "");
|
|
80
|
+
return [
|
|
81
|
+
bugHeadingLine(canonicalId, input.title),
|
|
82
|
+
"",
|
|
83
|
+
`**Id:** ${canonicalId}`,
|
|
84
|
+
`**SYS:** ${input.sysLine}`,
|
|
85
|
+
`**Status:** ${statusLineForBugMd(input.state)}`,
|
|
86
|
+
"",
|
|
87
|
+
"## Summary",
|
|
88
|
+
"",
|
|
89
|
+
summary,
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type WriteBugToSpecInput = {
|
|
95
|
+
specRoot: string;
|
|
96
|
+
canonicalId: string;
|
|
97
|
+
title: string;
|
|
98
|
+
state: string;
|
|
99
|
+
/** Overrides **SYS:** line; otherwise derived from parent folder name. */
|
|
100
|
+
sysId?: string;
|
|
101
|
+
/** Markdown under `## Summary` (may include multiple paragraphs / sections). */
|
|
102
|
+
summaryBody: string;
|
|
103
|
+
/** If set, nest the bug here; otherwise {@link resolveBugParentSysDir}. */
|
|
104
|
+
parentDir?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type WriteBugToSpecResult = {
|
|
108
|
+
root: string;
|
|
109
|
+
itemDir: string;
|
|
110
|
+
mdPath: string;
|
|
111
|
+
mdRel: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create `technical/SYS-…/BUG-NNN-slug/BUG-NNN.md` (shared by `bug start` and remote bug materialization).
|
|
116
|
+
*/
|
|
117
|
+
export function writeBugToSpec(input: WriteBugToSpecInput): WriteBugToSpecResult {
|
|
118
|
+
const root = path.resolve(input.specRoot);
|
|
119
|
+
const { canonicalId, prefix } = parseWorkItemCanonicalId(input.canonicalId);
|
|
120
|
+
if (prefix !== "BUG" || !/^BUG-\d{3}$/i.test(canonicalId)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[waterfall] write bug: expected BUG-NNN id, got "${input.canonicalId}"`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parent =
|
|
127
|
+
input.parentDir ?? resolveBugParentSysDir(root, input.sysId);
|
|
128
|
+
if (!parent) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[waterfall] write bug: no \`technical/SYS-NNN-*\` folder under spec; add a subsystem or pass a matching \`sysId\` (${canonicalId}).`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const folderBase = bugFolderBaseName(canonicalId, input.title);
|
|
135
|
+
const itemDir = path.join(parent, folderBase);
|
|
136
|
+
if (fs.existsSync(itemDir)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`[waterfall] write bug: folder already exists: ${path.relative(root, itemDir)}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mdName = `${canonicalId}.md`;
|
|
143
|
+
const mdPath = path.join(itemDir, mdName);
|
|
144
|
+
fs.mkdirSync(itemDir, { recursive: true });
|
|
145
|
+
|
|
146
|
+
const sysLine = sysLineForBugFromParent(parent, input.sysId);
|
|
147
|
+
const body = buildBugMarkdownContent({
|
|
148
|
+
canonicalId,
|
|
149
|
+
title: input.title,
|
|
150
|
+
state: input.state,
|
|
151
|
+
sysLine,
|
|
152
|
+
summaryBody: input.summaryBody,
|
|
153
|
+
});
|
|
154
|
+
fs.writeFileSync(mdPath, body, "utf8");
|
|
155
|
+
|
|
156
|
+
const mdRel = path.relative(root, mdPath).split(path.sep).join("/");
|
|
157
|
+
return { root, itemDir, mdPath, mdRel };
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pknx/waterfall-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for waterfall-spec workflows: CR/story lifecycle, agent prompts, work-item sync, PDF export.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"waterfall",
|
|
10
|
+
"waterfall-spec",
|
|
11
|
+
"cli",
|
|
12
|
+
"cursor",
|
|
13
|
+
"spec",
|
|
14
|
+
"workflow",
|
|
15
|
+
"agent"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"waterfall": "bin/waterfall.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"lib",
|
|
27
|
+
"prompts",
|
|
28
|
+
"spec-template",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"!lib/**/*.test.ts"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "echo \"@pknx/waterfall-cli: TypeScript runs via tsx at runtime; no compile step.\"",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"prepublishOnly": "npm test",
|
|
37
|
+
"pack:check": "npm pack --dry-run",
|
|
38
|
+
"waterfall": "node bin/waterfall.mjs",
|
|
39
|
+
"link:global": "npm link .",
|
|
40
|
+
"link:global:sudo": "sudo npm link ."
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@mermaid-js/mermaid-cli": "^11.12.0",
|
|
44
|
+
"pandoc-wasm": "^1.0.1",
|
|
45
|
+
"puppeteer": "^23.11.1",
|
|
46
|
+
"tsx": "^4.21.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"markdownlint-cli": "^0.48.0",
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"vitest": "^2.1.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Process: start a new BUG (`waterfall bug start`)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Create a new **BUG** folder under an existing **`technical/SYS-<NNN>-<slug>/`**, with a minimal **`BUG-<NNN>.md`** scaffold, and advance `number.json` `next.BUG`.
|
|
6
|
+
|
|
7
|
+
The **Waterfall CLI** runs **mechanical** scaffold + commit first, then invokes this prompt with **`--execute-agent`** (or dry-run) so **Summary** is **synthesized** prose per **`bug-document-structure`**, not an italic placeholder.
|
|
8
|
+
|
|
9
|
+
## When this prompt runs (agent refinement — phase 2)
|
|
10
|
+
|
|
11
|
+
The CLI has **already** created **`technical/SYS-…/BUG-<NNN>-<slug>/BUG-<NNN>.md`** (placeholder **Summary** + **`## Draft steering (CLI)`** nested under the matching SYS), bumped **`number.json`**, and **committed**.
|
|
12
|
+
|
|
13
|
+
**Your task:** edit **only** that **`BUG-<NNN>.md`**:
|
|
14
|
+
|
|
15
|
+
- Replace the **Summary** placeholder with a clear defect description per **[`bug-document-structure.md`](../items/bug-document-structure.md)** — **synthesize** from **Draft steering**; **no verbatim paste** as the only Summary.
|
|
16
|
+
- **Remove** **`## Draft steering (CLI)`** (and nested content under it) when done.
|
|
17
|
+
- Keep **Id**, **SYS**, **Status** unchanged unless correcting a clear mistake.
|
|
18
|
+
|
|
19
|
+
**Out of scope for this agent pass:** **`number.json`**, other artifacts, SYS or STORY rewrites.
|
|
20
|
+
|
|
21
|
+
## Scope of edits — mechanical phase only (CLI / human)
|
|
22
|
+
|
|
23
|
+
Only touch:
|
|
24
|
+
|
|
25
|
+
- New folder: `technical/SYS-…/BUG-<NNN>-<slug>/`
|
|
26
|
+
- New file: `BUG-<NNN>.md`
|
|
27
|
+
- `number.json` (`next.BUG` increment in same change)
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
1. Use `next.BUG` as the numeric source of truth (three-digit, zero-padded).
|
|
32
|
+
2. Keep folder slug kebab-case and short.
|
|
33
|
+
3. `BUG-<NNN>.md` must include identity fields and **`## Summary`** per **[`bug-document-structure.md`](../items/bug-document-structure.md)**.
|
|
34
|
+
|
|
35
|
+
### Operator steering vs canonical BUG prose
|
|
36
|
+
|
|
37
|
+
The CLI **`waterfall bug start`** scaffold may include **`## Draft steering (CLI)`** and an italic placeholder under **Summary**.
|
|
38
|
+
|
|
39
|
+
- **Synthesize** defect narrative from that steering — **not** a 1:1 paste of the steering block.
|
|
40
|
+
- **Remove** **`## Draft steering (CLI)`** when **Summary** is complete.
|
|
41
|
+
|
|
42
|
+
## Done when
|
|
43
|
+
|
|
44
|
+
- **Summary** reads like a real defect report (repro + expected/actual where applicable).
|
|
45
|
+
- **Draft steering** is removed.
|
|
46
|
+
- Typed references to other items use **ids only**, not paths.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Process: finish CR feature branch (`waterfall cr finish`)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
**Before** running the CLI, use this checklist so the CR is **substantively** ready (content, consistency, completeness). The **`waterfall cr finish`** command only enforces **mechanical** git/spec rules and then **merges** your **`feature/CR-…`** branch **into local `develop`** (no PR flow).
|
|
6
|
+
|
|
7
|
+
| Layer | Responsibility |
|
|
8
|
+
|--------|----------------|
|
|
9
|
+
| **This prompt (agent / human)** | Meaningful spec content: sections not empty, internal consistency, required links and downstream refs, triage state matches claims. |
|
|
10
|
+
| **CLI `cr finish`** | Clean tree, commits ahead of `develop`, non-empty diff vs `develop`, `number.json` + **`changerequests/CR-<NNN>-*/CR-<NNN>.md`** present, no accidental `CURSOR.md` / `schemas/` in that diff, then **merge** into **`develop`**. |
|
|
11
|
+
|
|
12
|
+
## Scope of edits (strict)
|
|
13
|
+
|
|
14
|
+
| In scope | When |
|
|
15
|
+
|----------|------|
|
|
16
|
+
| **Mechanical fixes only** | Broken markdown links, front-matter typos (**Id**), `number.json` syntax — fixes that do not add new product scope. |
|
|
17
|
+
| **Substance** | Filled **CR** sections, valid **Links and dependencies**, readiness to integrate — validated **via this prompt only**, not by the CLI. |
|
|
18
|
+
|
|
19
|
+
**Out of scope here:** **New** **RQ**/**UC**/**STORY**/**TST** creation as part of “finish”; if triage is incomplete, stop and use the right edge prompt (**`cr-to-rq.md`**, etc.) before **`cr finish`**.
|
|
20
|
+
|
|
21
|
+
## Preconditions
|
|
22
|
+
|
|
23
|
+
- On **`feature/CR-<NNN>`** or **`feature/CR-<NNN>-<slug>`** (see **`CURSOR.md`**).
|
|
24
|
+
- **[`before-changing-spec.md`](../global/before-changing-spec.md)** — understand what will integrate: **`git diff develop`**, **`git diff develop...HEAD`**.
|
|
25
|
+
|
|
26
|
+
## Validation checklist (prompt only — not enforced by CLI)
|
|
27
|
+
|
|
28
|
+
1. **Working tree** — intentional changes are **committed** or will be picked up by **`git add -A`** on finish; no stray untracked noise.
|
|
29
|
+
2. **CR body** — **Summary** and scoped sections are **non-placeholder**; **Status** and **Links** match reality.
|
|
30
|
+
3. **Downstream references** — links in **`## Links and dependencies`** resolve to existing artifacts where claimed.
|
|
31
|
+
4. **`number.json`** — counters and ids are **consistent** with files on the branch (CLI only checks JSON shape + presence of **`next`**).
|
|
32
|
+
5. **Closed artifacts** — no inappropriate edits under **`_STORY-*`** / **`_BUG-*`** for new scope.
|
|
33
|
+
6. **Branch policy** — **`chore/*`** not used; work stayed on **`feature/CR-*`**.
|
|
34
|
+
|
|
35
|
+
When this checklist passes, run **`waterfall cr finish`**.
|
|
36
|
+
|
|
37
|
+
## CLI behavior (reference)
|
|
38
|
+
|
|
39
|
+
The command commits remaining changes if needed, runs mechanical checks, **`git checkout develop`**, and **`git merge --no-ff`** the feature branch. If a check fails, it prints an error and **does not** merge.
|
|
40
|
+
|
|
41
|
+
## Done when
|
|
42
|
+
|
|
43
|
+
- Prompt checklist: **pass** or explicit fail with reasons.
|
|
44
|
+
- CLI: **`develop`** checked out and feature branch **merged** locally, or CLI error with no merge.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Process: start a new CR (`waterfall cr start`)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Bootstrap a new change request **CR-NNN** with minimal files and a dedicated branch **`feature/CR-NNN-short-slug`** from **`develop`**, per **[`CURSOR.md`](CURSOR.md)** (Git workflow). **Convention:** **NNN** is the **three-digit zero-padded** id (e.g. `005`); **short-slug** is kebab-case — substitute both in every path and branch name.
|
|
6
|
+
|
|
7
|
+
The **Waterfall CLI** does this in **two steps:** (1) **mechanical** — git + scaffold + `number.json` + commit; (2) **agent** — runs this prompt with **`--execute-agent`** (or dry-run preview) so **Summary** and **Proposed change** are **meaningful synthesized prose**, not placeholders.
|
|
8
|
+
|
|
9
|
+
## When this prompt runs (agent refinement — phase 2)
|
|
10
|
+
|
|
11
|
+
The CLI has **already** created the branch, written **`changerequests/CR-NNN-short-slug/CR-NNN.md`** (placeholders + **`## Draft steering (CLI)`**), bumped **`number.json`**, and **committed**. **Git diff vs `develop`** in the wire payload lists what changed.
|
|
12
|
+
|
|
13
|
+
**Your task:** edit **only** that **`CR-NNN.md`** file:
|
|
14
|
+
|
|
15
|
+
- Replace italic **Summary** and **Proposed change** placeholders with real sections per **[`cr-document-structure.md`](../items/cr-document-structure.md)** — **synthesize** from **Draft steering** and the operator hint; **no verbatim paste** of steering as the only content.
|
|
16
|
+
- **Remove** **`## Draft steering (CLI)`** when done.
|
|
17
|
+
- Keep **Id**, **Status**, **`## Links and dependencies`** / **`### Downstream references`** shape intact (downstream may stay empty).
|
|
18
|
+
|
|
19
|
+
**Out of scope for this agent pass:** **`number.json`**, **`git`**, any other path, **`RQ-*`** / **`UC-*`**. Do not re-run scaffold or change the assigned **CR-NNN** id.
|
|
20
|
+
|
|
21
|
+
## Scope of edits — mechanical phase only (CLI / human without agent)
|
|
22
|
+
|
|
23
|
+
When performing the **full** bootstrap **without** the CLI (or documenting what the CLI does first), only touch:
|
|
24
|
+
|
|
25
|
+
- **New folder + file:** `changerequests/CR-NNN-short-slug/CR-NNN.md`
|
|
26
|
+
- **`number.json`:** read **`next.CR`** for **NNN**, then increment **`next.CR`** in the **same** commit
|
|
27
|
+
|
|
28
|
+
**Out of scope:** `RQ-*`, `UC-*`, `SYS-*`, `STORY-*`, `BUG-*`, `TST-*`, any existing `CR-*` except the new pair, **`CURSOR.md`**. Do not create RQ or UC here — use **[`cr-to-rq.md`](cr-to-rq.md)** after the CR prose exists.
|
|
29
|
+
|
|
30
|
+
## Preconditions
|
|
31
|
+
|
|
32
|
+
- **`develop`** is current enough for your team (`git pull` if you use a remote).
|
|
33
|
+
- Read **[`before-changing-spec.md`](../global/before-changing-spec.md)** before touching **`number.json`** or **`changerequests/`**.
|
|
34
|
+
- **`next.CR`** must be the next free id (see **`CURSOR.md`**, **`number.json`**).
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
1. **Branch:** from **`develop`**, create **`feature/CR-NNN-short-slug`**. The **`feature/`** prefix is mandatory (bare `cr-NNN-…` without `feature/` is out of process).
|
|
39
|
+
2. **Slug:** kebab-case from the user description; short, filesystem-safe.
|
|
40
|
+
3. **CR markdown:** include **Id**, **Status:** `draft`, plus **Summary** — **no** **`**Upstream:**`** field and **no** **`### Upstream`** (see **[`cr-document-structure.md`](../items/cr-document-structure.md)**).
|
|
41
|
+
4. Include `## Links and dependencies` with a `### Downstream references` subsection only (empty on creation, filled during CR -> RQ).
|
|
42
|
+
5. Linking this CR from **CR-001** is optional and can be a separate change unless team policy bundles it.
|
|
43
|
+
|
|
44
|
+
### Operator steering vs canonical CR prose
|
|
45
|
+
|
|
46
|
+
The CLI **`waterfall cr start`** scaffold may include **`## Draft steering (CLI)`** (blockquoted raw operator text) and italic placeholders under **Summary** / **Proposed change**. That steering is **input only**, not shippable spec.
|
|
47
|
+
|
|
48
|
+
- **Synthesize:** derive goals, scope, constraints, and rationale from the steering text; write **new** markdown that satisfies **[`cr-document-structure.md`](../items/cr-document-structure.md)**.
|
|
49
|
+
- **Forbidden:** leaving **Summary** or **Proposed change** as a **verbatim copy** of **Draft steering** or of the operator hint in the agent wire payload.
|
|
50
|
+
- **Remove** **`## Draft steering (CLI)`** once canonical sections read well on their own.
|
|
51
|
+
|
|
52
|
+
## Steps
|
|
53
|
+
|
|
54
|
+
1. `git checkout develop` and `git pull` if applicable.
|
|
55
|
+
2. `git checkout -b feature/CR-NNN-short-slug`.
|
|
56
|
+
3. Read **`number.json`** → **`next.CR`** → form **NNN** with three-digit padding in filenames.
|
|
57
|
+
4. Create **`changerequests/CR-NNN-short-slug/`** and add **`CR-NNN.md`** inside it.
|
|
58
|
+
5. Set **`next.CR`** to NNN+1 in **`number.json`** in the same commit.
|
|
59
|
+
6. Commit on the feature branch (e.g. `feat(CR-NNN): scaffold change request`).
|
|
60
|
+
|
|
61
|
+
## Done when
|
|
62
|
+
|
|
63
|
+
- Branch **`feature/CR-NNN-short-slug`** contains only the new CR markdown and **`number.json`** bump.
|
|
64
|
+
- No RQ, UC, STORY, or TST files were created in this pass.
|
|
65
|
+
|