@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,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI config: `.waterfall` in the working directory and `~/.waterfall` (key=value, # comments).
|
|
3
|
+
* File merge: cwd wins over home for each key. Full precedence:
|
|
4
|
+
* argv flags > process.env (WATERFALL_*) > ./.waterfall > ~/.waterfall > defaults.
|
|
5
|
+
* Log level: WATERFALL_LOG_LEVEL or `level=` in .waterfall; flags --log-level / --quiet override.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import type { SyncWorkItemBlock } from "./sync-work-item-config";
|
|
11
|
+
import { emptySyncWorkItemBlock, parseSyncLead } from "./sync-work-item-config";
|
|
12
|
+
|
|
13
|
+
export const DOT_WATERFALL_BASENAME = ".waterfall";
|
|
14
|
+
|
|
15
|
+
/** Real home, or WATERFALL_TEST_HOME when set (Vitest setup). */
|
|
16
|
+
export function waterfallConfigHome(): string {
|
|
17
|
+
const t = process.env.WATERFALL_TEST_HOME?.trim();
|
|
18
|
+
if (t) return t;
|
|
19
|
+
return os.homedir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function dotWaterfallPath(home: string = waterfallConfigHome()): string {
|
|
23
|
+
return path.join(home, DOT_WATERFALL_BASENAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Project-local config: `<cwd>/.waterfall` */
|
|
27
|
+
export function dotWaterfallPathCwd(cwd: string = process.cwd()): string {
|
|
28
|
+
return path.join(path.resolve(cwd), DOT_WATERFALL_BASENAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DotWaterfallParsed = {
|
|
32
|
+
backend?: string;
|
|
33
|
+
specRoot?: string;
|
|
34
|
+
/** CLI trace: silent | normal | verbose */
|
|
35
|
+
level?: string;
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
executeAgent?: boolean;
|
|
38
|
+
/** embed | reference — how workflow markdown is passed to the agent (see --prompt-delivery) */
|
|
39
|
+
promptDelivery?: string;
|
|
40
|
+
/** true: `export pdf` uses system pandoc; false/omit: bundled pandoc-wasm + puppeteer */
|
|
41
|
+
useNativePandoc?: boolean;
|
|
42
|
+
/** `waterfall sync stories` — see sync_story_* keys in .waterfall */
|
|
43
|
+
syncStory?: SyncWorkItemBlock;
|
|
44
|
+
/** `waterfall sync bugs` — see sync_bug_* keys in .waterfall */
|
|
45
|
+
syncBug?: SyncWorkItemBlock;
|
|
46
|
+
/** UPPER_SNAKE=value merged into env for backend child processes */
|
|
47
|
+
envExports: Record<string, string>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function parseBool(raw: string): boolean | undefined {
|
|
51
|
+
const v = raw.trim().toLowerCase();
|
|
52
|
+
if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
|
|
53
|
+
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Known keys are stripped; remaining UPPER_SNAKE keys go to envExports. */
|
|
58
|
+
export function parseDotWaterfallContent(content: string): DotWaterfallParsed {
|
|
59
|
+
const out: DotWaterfallParsed = { envExports: {} };
|
|
60
|
+
for (const line of content.split(/\r?\n/)) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
63
|
+
const eq = trimmed.indexOf("=");
|
|
64
|
+
if (eq <= 0) continue;
|
|
65
|
+
const key = trimmed.slice(0, eq).trim();
|
|
66
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
67
|
+
if (
|
|
68
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
69
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
70
|
+
) {
|
|
71
|
+
val = val.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
const lower = key.toLowerCase().replace(/-/g, "_");
|
|
74
|
+
if (lower === "backend") {
|
|
75
|
+
out.backend = val;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (lower === "spec_root" || lower === "specroot") {
|
|
79
|
+
out.specRoot = val;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
lower === "level" ||
|
|
84
|
+
lower === "log_level" ||
|
|
85
|
+
lower === "verbosity"
|
|
86
|
+
) {
|
|
87
|
+
out.level = val;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (lower === "dry_run" || lower === "dryrun") {
|
|
91
|
+
const b = parseBool(val);
|
|
92
|
+
if (b !== undefined) out.dryRun = b;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (lower === "execute_agent" || lower === "executeagent") {
|
|
96
|
+
const b = parseBool(val);
|
|
97
|
+
if (b !== undefined) out.executeAgent = b;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (lower === "prompt_delivery" || lower === "promptdelivery") {
|
|
101
|
+
out.promptDelivery = val;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
lower === "use_native_pandoc" ||
|
|
106
|
+
lower === "usenativepandoc" ||
|
|
107
|
+
lower === "native_pandoc" ||
|
|
108
|
+
lower === "nativepandoc"
|
|
109
|
+
) {
|
|
110
|
+
const b = parseBool(val);
|
|
111
|
+
if (b !== undefined) out.useNativePandoc = b;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (lower.startsWith("sync_story_")) {
|
|
115
|
+
const suffix = lower.slice("sync_story_".length);
|
|
116
|
+
if (!out.syncStory) out.syncStory = emptySyncWorkItemBlock();
|
|
117
|
+
if (suffix === "direction") {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (suffix === "provider") {
|
|
121
|
+
out.syncStory.provider = val;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (suffix === "leading") {
|
|
125
|
+
const l = parseSyncLead(val);
|
|
126
|
+
if (l) out.syncStory.leading = l;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
out.syncStory.options[suffix] = val;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (lower.startsWith("sync_bug_")) {
|
|
133
|
+
const suffix = lower.slice("sync_bug_".length);
|
|
134
|
+
if (!out.syncBug) out.syncBug = emptySyncWorkItemBlock();
|
|
135
|
+
if (suffix === "direction") {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (suffix === "provider") {
|
|
139
|
+
out.syncBug.provider = val;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (suffix === "leading") {
|
|
143
|
+
const l = parseSyncLead(val);
|
|
144
|
+
if (l) out.syncBug.leading = l;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
out.syncBug.options[suffix] = val;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(key)) {
|
|
151
|
+
out.envExports[key] = val;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readDotWaterfallFile(absPath: string): DotWaterfallParsed {
|
|
158
|
+
if (!fs.existsSync(absPath)) return { envExports: {} };
|
|
159
|
+
try {
|
|
160
|
+
return parseDotWaterfallContent(fs.readFileSync(absPath, "utf8"));
|
|
161
|
+
} catch {
|
|
162
|
+
return { envExports: {} };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Overlay wins for defined fields; envExports are merged (overlay keys replace). */
|
|
167
|
+
export function mergeDotWaterfallParsed(
|
|
168
|
+
base: DotWaterfallParsed,
|
|
169
|
+
overlay: DotWaterfallParsed,
|
|
170
|
+
): DotWaterfallParsed {
|
|
171
|
+
return {
|
|
172
|
+
backend: overlay.backend ?? base.backend,
|
|
173
|
+
specRoot: overlay.specRoot ?? base.specRoot,
|
|
174
|
+
level: overlay.level ?? base.level,
|
|
175
|
+
dryRun: overlay.dryRun !== undefined ? overlay.dryRun : base.dryRun,
|
|
176
|
+
executeAgent:
|
|
177
|
+
overlay.executeAgent !== undefined ? overlay.executeAgent : base.executeAgent,
|
|
178
|
+
promptDelivery: overlay.promptDelivery ?? base.promptDelivery,
|
|
179
|
+
useNativePandoc:
|
|
180
|
+
overlay.useNativePandoc !== undefined
|
|
181
|
+
? overlay.useNativePandoc
|
|
182
|
+
: base.useNativePandoc,
|
|
183
|
+
syncStory: mergeSyncWorkItemBlock(base.syncStory, overlay.syncStory),
|
|
184
|
+
syncBug: mergeSyncWorkItemBlock(base.syncBug, overlay.syncBug),
|
|
185
|
+
envExports: { ...base.envExports, ...overlay.envExports },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mergeSyncWorkItemBlock(
|
|
190
|
+
base: SyncWorkItemBlock | undefined,
|
|
191
|
+
overlay: SyncWorkItemBlock | undefined,
|
|
192
|
+
): SyncWorkItemBlock | undefined {
|
|
193
|
+
if (!overlay && !base) return undefined;
|
|
194
|
+
if (!overlay) return base;
|
|
195
|
+
if (!base) return overlay;
|
|
196
|
+
return {
|
|
197
|
+
leading: overlay.leading ?? base.leading,
|
|
198
|
+
provider: overlay.provider ?? base.provider,
|
|
199
|
+
options: { ...base.options, ...overlay.options },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type LoadDotWaterfallOptions = {
|
|
204
|
+
/** Defaults to waterfallConfigHome() */
|
|
205
|
+
home?: string;
|
|
206
|
+
/** Defaults to process.cwd() */
|
|
207
|
+
cwd?: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Load `~/.waterfall` then merge `<cwd>/.waterfall` on top (working directory wins).
|
|
212
|
+
*/
|
|
213
|
+
export function loadDotWaterfallConfig(opts?: LoadDotWaterfallOptions): DotWaterfallParsed {
|
|
214
|
+
const home = opts?.home ?? waterfallConfigHome();
|
|
215
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
216
|
+
const fromHome = readDotWaterfallFile(dotWaterfallPath(home));
|
|
217
|
+
const fromCwd = readDotWaterfallFile(dotWaterfallPathCwd(cwd));
|
|
218
|
+
return mergeDotWaterfallParsed(fromHome, fromCwd);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Machine-readable `.waterfall` / `~/.waterfall` documentation for `waterfall help -o json`.
|
|
223
|
+
* Stays aligned with {@link parseDotWaterfallContent}.
|
|
224
|
+
*/
|
|
225
|
+
export const CLI_JSON_DOT_WATERFALL = {
|
|
226
|
+
description:
|
|
227
|
+
"Optional key=value config files merged before flags and env (cwd file wins over home per key).",
|
|
228
|
+
syntax: {
|
|
229
|
+
format: "One KEY=value per line; leading/trailing whitespace trimmed; # starts a comment line.",
|
|
230
|
+
quotedValues: "Values may be wrapped in double or single quotes.",
|
|
231
|
+
},
|
|
232
|
+
files: [
|
|
233
|
+
{
|
|
234
|
+
path: "~/.waterfall",
|
|
235
|
+
role: "User-wide defaults; created with a commented template on first CLI run when missing.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
path: ".waterfall",
|
|
239
|
+
role: "Working-directory overrides; wins over ~/.waterfall for each key.",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
merge: "Read ~/.waterfall then merge ./.waterfall (overlay replaces per field).",
|
|
243
|
+
knownKeys: [
|
|
244
|
+
{
|
|
245
|
+
keys: ["backend"],
|
|
246
|
+
valueType: "enum" as const,
|
|
247
|
+
enumValues: ["cline-ollama", "cursor", "claude-code"],
|
|
248
|
+
description: "Agent backend for prompt commands.",
|
|
249
|
+
cliFlag: "--backend",
|
|
250
|
+
env: "WATERFALL_BACKEND",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
keys: ["spec_root", "specroot"],
|
|
254
|
+
valueType: "path" as const,
|
|
255
|
+
description: "Filesystem path to waterfall-spec root (optional).",
|
|
256
|
+
cliFlag: "--spec-root",
|
|
257
|
+
env: "WATERFALL_SPEC_ROOT",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
keys: ["level", "log_level", "verbosity"],
|
|
261
|
+
valueType: "enum" as const,
|
|
262
|
+
enumValues: ["silent", "normal", "verbose"],
|
|
263
|
+
description: "CLI trace verbosity.",
|
|
264
|
+
cliFlag: "--log-level",
|
|
265
|
+
env: "WATERFALL_LOG_LEVEL",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
keys: ["dry_run", "dryrun"],
|
|
269
|
+
valueType: "boolean" as const,
|
|
270
|
+
description: "true: plan agent runs only; false: allow execute when combined with execute_agent.",
|
|
271
|
+
cliFlag: "--dry-run",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
keys: ["execute_agent", "executeagent"],
|
|
275
|
+
valueType: "boolean" as const,
|
|
276
|
+
description: "true: spawn real backend CLIs; false: dry-run only.",
|
|
277
|
+
cliFlag: "--execute-agent",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
keys: ["prompt_delivery", "promptdelivery"],
|
|
281
|
+
valueType: "enum" as const,
|
|
282
|
+
enumValues: ["embed", "reference"],
|
|
283
|
+
description: "How workflow markdown is passed to the agent.",
|
|
284
|
+
cliFlag: "--prompt-delivery",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
keys: [
|
|
288
|
+
"use_native_pandoc",
|
|
289
|
+
"usenativepandoc",
|
|
290
|
+
"native_pandoc",
|
|
291
|
+
"nativepandoc",
|
|
292
|
+
],
|
|
293
|
+
valueType: "boolean" as const,
|
|
294
|
+
description:
|
|
295
|
+
"true: `waterfall export pdf` invokes system `pandoc` (see WATERFALL_PANDOC_ARGS); false: bundled pandoc-wasm + puppeteer.",
|
|
296
|
+
dotWaterfallKey: "use_native_pandoc",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
keys: ["sync_story_provider", "sync_bug_provider"],
|
|
300
|
+
valueType: "string" as const,
|
|
301
|
+
description:
|
|
302
|
+
"Tracker adapter id: jira (stub), or direct (remote snapshot from CLI hint JSON, same shape as sync --dump-spec; sync always applies remote comments to spec then diffs; direct also appends source=waterfall-direct-out JSON after the report). Each kind has its own provider. Additional options use keys sync_story_<suffix> / sync_bug_<suffix> (e.g. sync_story_jira_site, sync_story_jira_project).",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
keys: ["sync_story_leading", "sync_bug_leading"],
|
|
306
|
+
valueType: "enum" as const,
|
|
307
|
+
enumValues: ["waterfall", "remote"],
|
|
308
|
+
description:
|
|
309
|
+
"Authoritative side when spec and remote disagree for the same id (`waterfall` = spec tree wins, `remote` = tracker wins). Defaults: STORY → waterfall, BUG → remote.",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
booleanValues: ["true", "false", "1", "0", "yes", "no", "on", "off"],
|
|
313
|
+
keyMatching:
|
|
314
|
+
"Known keys are matched case-insensitively with - and _ normalized to _ for comparison.",
|
|
315
|
+
environmentPassthrough: {
|
|
316
|
+
rule: "Any KEY that looks like UPPER_SNAKE (^[A-Z][A-Z0-9_]*$) and is not consumed as a known key is stored and merged into process.env for agent subprocesses only where env is unset.",
|
|
317
|
+
examples: [
|
|
318
|
+
"ANTHROPIC_API_KEY",
|
|
319
|
+
"OPENAI_API_KEY",
|
|
320
|
+
"CURSOR_API_KEY",
|
|
321
|
+
"OLLAMA_HOST",
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
} as const;
|
|
325
|
+
|
|
326
|
+
export const DOT_WATERFALL_TEMPLATE = `# Waterfall CLI user configuration
|
|
327
|
+
# Path: ~/.waterfall (also supported: .waterfall in the current working directory)
|
|
328
|
+
# If both exist, the file in the working directory wins for each setting.
|
|
329
|
+
# Precedence: flags > environment variables > ./.waterfall > this file > built-in defaults
|
|
330
|
+
#
|
|
331
|
+
# Lines starting with # are comments. Use KEY=value (no spaces around =).
|
|
332
|
+
|
|
333
|
+
# --- Agent backend (cline-ollama | cursor | claude-code) ---
|
|
334
|
+
# backend=cline-ollama
|
|
335
|
+
|
|
336
|
+
# --- waterfall-spec checkout (optional; overrides walk-up / waterfall.json) ---
|
|
337
|
+
# spec_root=/absolute/path/to/waterfall-spec
|
|
338
|
+
|
|
339
|
+
# --- CLI tracing: silent | normal | verbose (see also WATERFALL_LOG_LEVEL, --log-level, --quiet) ---
|
|
340
|
+
# level=normal
|
|
341
|
+
|
|
342
|
+
# --- Prompt steps: dry-run only vs run real agent CLIs ---
|
|
343
|
+
# dry_run=true
|
|
344
|
+
# execute_agent=false
|
|
345
|
+
|
|
346
|
+
# When execute_agent=true, backends run (cline -y, agent|cursor -p --force, claude -p).
|
|
347
|
+
# execute_agent=true
|
|
348
|
+
# dry_run=false
|
|
349
|
+
|
|
350
|
+
# --- Workflow prompt in agent message: embed (full .md inlined, default) | reference (paths only) ---
|
|
351
|
+
# prompt_delivery=embed
|
|
352
|
+
# Overridden per run by: --prompt-delivery embed|reference
|
|
353
|
+
|
|
354
|
+
# --- export pdf: use system pandoc + PDF engine instead of bundled wasm + headless Chrome ---
|
|
355
|
+
# use_native_pandoc=false
|
|
356
|
+
|
|
357
|
+
# --- sync stories|bugs ↔ issue tracker (separate STORY vs BUG; all keys live in this file) ---
|
|
358
|
+
# sync_story_provider=jira
|
|
359
|
+
# sync_story_provider=direct # remote side from CLI hint: waterfall sync stories @remote.json (JSON = --dump-spec shape)
|
|
360
|
+
# sync_story_leading=waterfall
|
|
361
|
+
# sync_story_jira_site=https://your.atlassian.net
|
|
362
|
+
# sync_story_jira_project=BOOK
|
|
363
|
+
# sync_bug_provider=jira
|
|
364
|
+
# sync_bug_leading=remote
|
|
365
|
+
# sync_bug_jira_site=https://your.atlassian.net
|
|
366
|
+
# sync_bug_jira_project=BOOK
|
|
367
|
+
# (Auth: e.g. JIRA_API_TOKEN + JIRA_USER_EMAIL as UPPER_SNAKE above for agent passthrough; wire in provider when implemented.)
|
|
368
|
+
|
|
369
|
+
# --- Optional: pass through to agent processes (uncomment and set as needed) ---
|
|
370
|
+
# ANTHROPIC_API_KEY=
|
|
371
|
+
# OPENAI_API_KEY=
|
|
372
|
+
# CURSOR_API_KEY=
|
|
373
|
+
# OLLAMA_HOST=http://127.0.0.1:11434
|
|
374
|
+
|
|
375
|
+
# Cline: run \`cline auth\` to pick provider (Ollama, Anthropic, etc.) — see https://docs.cline.bot/cline-cli/getting-started
|
|
376
|
+
# Claude Code: \`claude\` uses its own auth / API key from env
|
|
377
|
+
# Cursor: \`agent\` / \`cursor\` CLI — see https://cursor.com/docs/cli/overview
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Create ~/.waterfall with commented template if missing. Returns true if created.
|
|
382
|
+
*/
|
|
383
|
+
export function ensureDotWaterfallFile(home?: string): boolean {
|
|
384
|
+
// Avoid creating ~/.waterfall on the developer machine during unit tests.
|
|
385
|
+
if (process.env.VITEST === "true" && home === undefined) return false;
|
|
386
|
+
const p = dotWaterfallPath(home ?? waterfallConfigHome());
|
|
387
|
+
if (fs.existsSync(p)) return false;
|
|
388
|
+
try {
|
|
389
|
+
fs.writeFileSync(p, DOT_WATERFALL_TEMPLATE, { mode: 0o600 });
|
|
390
|
+
return true;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Merge UPPER_SNAKE keys into process.env (only if not already set).
|
|
398
|
+
*/
|
|
399
|
+
export function applyDotWaterfallEnvExports(exports: Record<string, string>): void {
|
|
400
|
+
for (const [k, v] of Object.entries(exports)) {
|
|
401
|
+
if (v === "" || v === undefined) continue;
|
|
402
|
+
if (process.env[k] === undefined) process.env[k] = v;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BackendId } from "../agent/backends";
|
|
2
|
+
import type { PromptDelivery } from "../agent/agent-message";
|
|
3
|
+
import type { LogLevel } from "../core/cli-log";
|
|
4
|
+
import type { SyncWorkItemBlock } from "./sync-work-item-config";
|
|
5
|
+
|
|
6
|
+
export type GlobalCli = {
|
|
7
|
+
cwd: string;
|
|
8
|
+
specRoot?: string;
|
|
9
|
+
backend: BackendId;
|
|
10
|
+
dryRun: boolean;
|
|
11
|
+
executeAgent: boolean;
|
|
12
|
+
logLevel: LogLevel;
|
|
13
|
+
/** Inlined prompt body vs filesystem paths only (smaller argv). */
|
|
14
|
+
promptDelivery: PromptDelivery;
|
|
15
|
+
/** From `.waterfall` / `~/.waterfall` `use_native_pandoc=` — `export pdf` calls system pandoc instead of bundled wasm + puppeteer. */
|
|
16
|
+
useNativePandoc: boolean;
|
|
17
|
+
/** From `.waterfall` `sync_story_*` — `waterfall sync stories`. */
|
|
18
|
+
syncStory?: SyncWorkItemBlock;
|
|
19
|
+
/** From `.waterfall` `sync_bug_*` — `waterfall sync bugs`. */
|
|
20
|
+
syncBug?: SyncWorkItemBlock;
|
|
21
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WorkItemSyncKind } from "../sync/provider-types";
|
|
2
|
+
|
|
3
|
+
/** Authoritative side when spec and remote disagree for the same work item id. */
|
|
4
|
+
export type SyncLeadSystem = "waterfall" | "remote";
|
|
5
|
+
|
|
6
|
+
export function parseSyncLead(raw: string | undefined): SyncLeadSystem | undefined {
|
|
7
|
+
const v = raw?.trim().toLowerCase();
|
|
8
|
+
if (v === "waterfall") return "waterfall";
|
|
9
|
+
if (v === "remote") return "remote";
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** STORY → waterfall; BUG → remote (when `leading` is omitted in `.waterfall`). */
|
|
14
|
+
export function defaultSyncLeadForKind(kind: WorkItemSyncKind): SyncLeadSystem {
|
|
15
|
+
return kind === "bug" ? "remote" : "waterfall";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function effectiveSyncLead(
|
|
19
|
+
block: SyncWorkItemBlock | undefined,
|
|
20
|
+
kind: WorkItemSyncKind,
|
|
21
|
+
): SyncLeadSystem {
|
|
22
|
+
return block?.leading ?? defaultSyncLeadForKind(kind);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SyncWorkItemBlock = {
|
|
26
|
+
/** Which side wins on field mismatch (`waterfall` = spec tree, `remote` = tracker). */
|
|
27
|
+
leading?: SyncLeadSystem;
|
|
28
|
+
provider?: string;
|
|
29
|
+
options: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function emptySyncWorkItemBlock(): SyncWorkItemBlock {
|
|
33
|
+
return { options: {} };
|
|
34
|
+
}
|