intentdocs 0.8.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/README.md +261 -0
- package/bin/intent.js +2 -0
- package/dist/chunk-4OWPN2RE.js +249 -0
- package/dist/chunk-6FGYJ3W2.js +140 -0
- package/dist/chunk-6XKB6RTU.js +123 -0
- package/dist/chunk-ICOMMHKG.js +48 -0
- package/dist/chunk-IKF3ZLHK.js +621 -0
- package/dist/chunk-ITQUQYWB.js +21 -0
- package/dist/chunk-PCNDP5SX.js +34 -0
- package/dist/chunk-WPVDURTJ.js +79 -0
- package/dist/complexity-NSSYVT54.js +79 -0
- package/dist/import-prd-CKV3R67G.js +134 -0
- package/dist/index.js +2821 -0
- package/dist/keys-EDWET3WG.js +90 -0
- package/dist/log-TT2UCJP7.js +94 -0
- package/dist/map-extract-I3AACTGL.js +98 -0
- package/dist/map-import-IYUMRASO.js +358 -0
- package/dist/map-schema-JVL3O2TJ.js +114 -0
- package/dist/prompt-WAWCRGHN.js +16 -0
- package/dist/research-6SXJ37TI.js +59 -0
- package/dist/scope-OGKQ472D.js +10 -0
- package/dist/setup-SOI2HQAL.js +11 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2821 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
captureCli,
|
|
4
|
+
setup,
|
|
5
|
+
shutdownCliAnalytics,
|
|
6
|
+
sync
|
|
7
|
+
} from "./chunk-IKF3ZLHK.js";
|
|
8
|
+
import {
|
|
9
|
+
scope
|
|
10
|
+
} from "./chunk-4OWPN2RE.js";
|
|
11
|
+
import {
|
|
12
|
+
criterionPriority,
|
|
13
|
+
criterionStatusLabel,
|
|
14
|
+
findBlockingMusts,
|
|
15
|
+
storyContextPath,
|
|
16
|
+
writeStoryContext
|
|
17
|
+
} from "./chunk-6FGYJ3W2.js";
|
|
18
|
+
import {
|
|
19
|
+
ask,
|
|
20
|
+
closePrompt,
|
|
21
|
+
confirm,
|
|
22
|
+
pausePrompt
|
|
23
|
+
} from "./chunk-WPVDURTJ.js";
|
|
24
|
+
import {
|
|
25
|
+
UPGRADE_MESSAGE_PLAIN,
|
|
26
|
+
isPlanGated
|
|
27
|
+
} from "./chunk-ITQUQYWB.js";
|
|
28
|
+
import {
|
|
29
|
+
apiFetch,
|
|
30
|
+
apiUrl,
|
|
31
|
+
createStories,
|
|
32
|
+
getContext,
|
|
33
|
+
listStories,
|
|
34
|
+
raiseBlocker,
|
|
35
|
+
reportCriteria,
|
|
36
|
+
updateStories,
|
|
37
|
+
uploadScreenshot
|
|
38
|
+
} from "./chunk-6XKB6RTU.js";
|
|
39
|
+
import {
|
|
40
|
+
hasConfig,
|
|
41
|
+
readConfig,
|
|
42
|
+
requireConfig,
|
|
43
|
+
writeConfig
|
|
44
|
+
} from "./chunk-ICOMMHKG.js";
|
|
45
|
+
import {
|
|
46
|
+
API_BASE,
|
|
47
|
+
getAuth,
|
|
48
|
+
requireAuth,
|
|
49
|
+
saveAuth
|
|
50
|
+
} from "./chunk-PCNDP5SX.js";
|
|
51
|
+
|
|
52
|
+
// src/index.ts
|
|
53
|
+
import { Command } from "commander";
|
|
54
|
+
import { createRequire as createRequire2 } from "module";
|
|
55
|
+
|
|
56
|
+
// src/commands/init.ts
|
|
57
|
+
import { writeFileSync, existsSync, readFileSync, appendFileSync, statSync } from "fs";
|
|
58
|
+
import { join } from "path";
|
|
59
|
+
import { createInterface } from "readline";
|
|
60
|
+
|
|
61
|
+
// src/commands/login.ts
|
|
62
|
+
import { exec } from "child_process";
|
|
63
|
+
function openBrowser(url) {
|
|
64
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
|
|
65
|
+
exec(cmd);
|
|
66
|
+
}
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
69
|
+
}
|
|
70
|
+
async function login() {
|
|
71
|
+
const existing = getAuth();
|
|
72
|
+
if (existing) {
|
|
73
|
+
console.log("Already logged in. Run `intent logout` to switch accounts.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log("Logging in to intentdocs...\n");
|
|
77
|
+
const res = await fetch(`${API_BASE}/api/cli/auth`, { method: "POST" });
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
console.error("Failed to start login flow. Try again.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const { code } = await res.json();
|
|
83
|
+
const authUrl = `${API_BASE}/cli/auth?code=${code}`;
|
|
84
|
+
console.log("Opening browser to authorize...");
|
|
85
|
+
console.log(`If it doesn't open, visit: ${authUrl}
|
|
86
|
+
`);
|
|
87
|
+
openBrowser(authUrl);
|
|
88
|
+
const maxAttempts = 60;
|
|
89
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
90
|
+
await sleep(3e3);
|
|
91
|
+
try {
|
|
92
|
+
const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
|
|
93
|
+
if (!checkRes.ok) continue;
|
|
94
|
+
const data = await checkRes.json();
|
|
95
|
+
if (data.status === "approved" && data.token) {
|
|
96
|
+
saveAuth({
|
|
97
|
+
token: data.token,
|
|
98
|
+
apiBase: API_BASE,
|
|
99
|
+
...data.userId && { userId: data.userId },
|
|
100
|
+
...data.posthogKey && { posthogKey: data.posthogKey },
|
|
101
|
+
...data.posthogHost && { posthogHost: data.posthogHost }
|
|
102
|
+
});
|
|
103
|
+
console.log("\u2705 Logged in to intentdocs!\n");
|
|
104
|
+
console.log("You can now run: intent init");
|
|
105
|
+
console.log(
|
|
106
|
+
"Optional: add your Anthropic API key to unlock AI features on any plan \u2014 intent keys set"
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (data.status === "expired") {
|
|
111
|
+
console.error("Login request expired. Run `intent login` to try again.");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(".");
|
|
115
|
+
} catch {
|
|
116
|
+
process.stdout.write(".");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.error("\nLogin timed out. Run `intent login` to try again.");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
async function logout() {
|
|
123
|
+
const { existsSync: existsSync5, unlinkSync } = await import("fs");
|
|
124
|
+
const { join: join5 } = await import("path");
|
|
125
|
+
const { homedir } = await import("os");
|
|
126
|
+
const authFile = join5(homedir(), ".intent", "auth.json");
|
|
127
|
+
if (existsSync5(authFile)) {
|
|
128
|
+
unlinkSync(authFile);
|
|
129
|
+
console.log("Logged out.");
|
|
130
|
+
} else {
|
|
131
|
+
console.log("Not logged in.");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/commands/init.ts
|
|
136
|
+
function ask2(rl, question) {
|
|
137
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
138
|
+
}
|
|
139
|
+
async function pickFromList(rl, items, label, prompt) {
|
|
140
|
+
items.forEach((item, i) => {
|
|
141
|
+
console.log(` ${i + 1}. ${label(item)}`);
|
|
142
|
+
});
|
|
143
|
+
while (true) {
|
|
144
|
+
const raw = await ask2(rl, `
|
|
145
|
+
${prompt} [1-${items.length}]: `);
|
|
146
|
+
const n = parseInt(raw.trim(), 10);
|
|
147
|
+
if (n >= 1 && n <= items.length) return items[n - 1];
|
|
148
|
+
console.log(` Please enter a number between 1 and ${items.length}.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function authHeaders(token) {
|
|
152
|
+
return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
153
|
+
}
|
|
154
|
+
function detectStack(cwd) {
|
|
155
|
+
try {
|
|
156
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
157
|
+
const raw = readFileSync(join(cwd, "package.json"), "utf-8");
|
|
158
|
+
const pkg = JSON.parse(raw);
|
|
159
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
160
|
+
if (deps["next"]) return "Next.js (React, TypeScript)";
|
|
161
|
+
if (deps["remix"] || deps["@remix-run/react"]) return "Remix (React, TypeScript)";
|
|
162
|
+
if (deps["react"]) return "React (TypeScript, Vite)";
|
|
163
|
+
if (deps["vue"]) return "Vue 3 (TypeScript, Vite)";
|
|
164
|
+
if (deps["svelte"] || deps["@sveltejs/kit"]) return "SvelteKit (TypeScript)";
|
|
165
|
+
if (deps["fastify"]) return "Node.js (Fastify, TypeScript)";
|
|
166
|
+
if (deps["express"]) return "Node.js (Express, TypeScript)";
|
|
167
|
+
return "Node.js (TypeScript)";
|
|
168
|
+
}
|
|
169
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "requirements.txt"))) {
|
|
170
|
+
const hasFastapi = existsSync(join(cwd, "pyproject.toml")) && readFileSync(join(cwd, "pyproject.toml"), "utf-8").includes("fastapi");
|
|
171
|
+
return hasFastapi ? "Python (FastAPI)" : "Python";
|
|
172
|
+
}
|
|
173
|
+
if (existsSync(join(cwd, "Cargo.toml"))) return "Rust";
|
|
174
|
+
if (existsSync(join(cwd, "go.mod"))) return "Go";
|
|
175
|
+
if (existsSync(join(cwd, "pubspec.yaml"))) return "Flutter (Dart)";
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
async function init(tokenArg) {
|
|
181
|
+
if (hasConfig()) {
|
|
182
|
+
console.log("Already initialized. Run `intent sync` to refresh.");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (tokenArg) {
|
|
186
|
+
await initWithSyncToken(tokenArg);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let auth = getAuth();
|
|
190
|
+
if (!auth) {
|
|
191
|
+
console.log("You need to log in first.\n");
|
|
192
|
+
await login();
|
|
193
|
+
auth = getAuth();
|
|
194
|
+
if (!auth) {
|
|
195
|
+
console.error("Login failed.");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
200
|
+
const initStart = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
console.log("\nFetching your projects...");
|
|
203
|
+
const projectsRes = await fetch(`${auth.apiBase}/api/cli/projects`, {
|
|
204
|
+
headers: authHeaders(auth.token)
|
|
205
|
+
});
|
|
206
|
+
if (!projectsRes.ok) {
|
|
207
|
+
console.error("Failed to fetch projects. Try `intent login` first.");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const { projects } = await projectsRes.json();
|
|
211
|
+
if (projects.length === 0) {
|
|
212
|
+
console.log("\nNo projects found. Create one at intentdocs.com first.");
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
console.log("\nYour projects:\n");
|
|
216
|
+
const project = await pickFromList(rl, projects, (p) => p.name, "Select project");
|
|
217
|
+
console.log("\nFetching story maps...");
|
|
218
|
+
const mapsRes = await fetch(
|
|
219
|
+
`${auth.apiBase}/api/cli/projects/${project.id}/maps`,
|
|
220
|
+
{ headers: authHeaders(auth.token) }
|
|
221
|
+
);
|
|
222
|
+
if (!mapsRes.ok) {
|
|
223
|
+
console.error("Failed to fetch story maps.");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const { maps, appUrl } = await mapsRes.json();
|
|
227
|
+
if (maps.length === 0) {
|
|
228
|
+
console.log("\nNo story maps in this project. Create one at intentdocs.com first.");
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
console.log(`
|
|
232
|
+
Story maps in "${project.name}":
|
|
233
|
+
`);
|
|
234
|
+
const map2 = await pickFromList(rl, maps, (m) => m.title, "Select story map");
|
|
235
|
+
const config = {
|
|
236
|
+
syncToken: map2.syncToken,
|
|
237
|
+
apiBase: auth.apiBase,
|
|
238
|
+
mapTitle: map2.title,
|
|
239
|
+
projectId: project.id,
|
|
240
|
+
mapId: map2.id,
|
|
241
|
+
...appUrl && { appUrl }
|
|
242
|
+
};
|
|
243
|
+
console.log(`
|
|
244
|
+
Connecting to "${map2.title}"...`);
|
|
245
|
+
try {
|
|
246
|
+
const data = await listStories(config);
|
|
247
|
+
console.log(`\u2713 Connected to "${data.map.title}"`);
|
|
248
|
+
console.log(
|
|
249
|
+
` ${data.summary.total} stories \u2014 ${data.summary.done} done, ${data.summary.todo} todo
|
|
250
|
+
`
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
console.error("Failed to validate connection. Please try again.");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
writeConfig(config);
|
|
257
|
+
await setupFiles(config);
|
|
258
|
+
await setupStack(rl);
|
|
259
|
+
const specPath = join(process.cwd(), "INTENT.md");
|
|
260
|
+
const claudeMdBytes = existsSync(specPath) ? statSync(specPath).size : 0;
|
|
261
|
+
captureCli(
|
|
262
|
+
"harness_initialized",
|
|
263
|
+
{
|
|
264
|
+
sections_generated: 0,
|
|
265
|
+
claude_md_bytes: claudeMdBytes,
|
|
266
|
+
duration_ms: Date.now() - initStart
|
|
267
|
+
},
|
|
268
|
+
{ project: project.id }
|
|
269
|
+
);
|
|
270
|
+
console.log(`
|
|
271
|
+
\u2705 Ready! Next steps:
|
|
272
|
+
|
|
273
|
+
intent status \u2014 see your story map
|
|
274
|
+
intent next \u2014 pick up the first story
|
|
275
|
+
intent setup \u2014 configure Claude Code / Cursor slash commands
|
|
276
|
+
intent propose \u2014 plan a new feature from the terminal
|
|
277
|
+
`);
|
|
278
|
+
} finally {
|
|
279
|
+
rl.close();
|
|
280
|
+
await shutdownCliAnalytics();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function setupStack(rl) {
|
|
284
|
+
const cwd = process.cwd();
|
|
285
|
+
const archPath = join(cwd, "ARCHITECTURE.md");
|
|
286
|
+
if (existsSync(archPath)) return;
|
|
287
|
+
const detected = detectStack(cwd);
|
|
288
|
+
let stack;
|
|
289
|
+
if (detected) {
|
|
290
|
+
console.log(`
|
|
291
|
+
Detected stack: ${detected}`);
|
|
292
|
+
const confirm2 = await ask2(rl, ` Is this correct? (press Enter to confirm, or type your stack): `);
|
|
293
|
+
stack = confirm2.trim() || detected;
|
|
294
|
+
} else {
|
|
295
|
+
console.log("\n No stack detected automatically.");
|
|
296
|
+
stack = await ask2(rl, " Describe your tech stack (e.g. 'Next.js, Supabase, TypeScript'): ");
|
|
297
|
+
if (!stack.trim()) stack = "Not specified";
|
|
298
|
+
}
|
|
299
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
300
|
+
const archContent = `# Architecture
|
|
301
|
+
|
|
302
|
+
Your AI coding agent reads this before implementing features.
|
|
303
|
+
Keep it up to date \u2014 it's how the agent knows how your system is built.
|
|
304
|
+
|
|
305
|
+
## Stack
|
|
306
|
+
|
|
307
|
+
${stack}
|
|
308
|
+
|
|
309
|
+
## Decisions
|
|
310
|
+
|
|
311
|
+
| Date | Decision | Rationale |
|
|
312
|
+
|------|----------|-----------|
|
|
313
|
+
| ${today} | Project initialised with intentdocs | Story-map-driven development |
|
|
314
|
+
|
|
315
|
+
## Patterns
|
|
316
|
+
|
|
317
|
+
_Document recurring patterns as you establish them._
|
|
318
|
+
|
|
319
|
+
## Trade-offs
|
|
320
|
+
|
|
321
|
+
_Record trade-offs and why._
|
|
322
|
+
|
|
323
|
+
## Key files
|
|
324
|
+
|
|
325
|
+
_Add important files and what they do as you build._
|
|
326
|
+
`;
|
|
327
|
+
writeFileSync(archPath, archContent);
|
|
328
|
+
console.log(` \u2713 Created ARCHITECTURE.md with your stack (edit it as you build)`);
|
|
329
|
+
}
|
|
330
|
+
async function initWithSyncToken(syncToken) {
|
|
331
|
+
if (!syncToken.startsWith("intent_sync_") && !syncToken.startsWith("pb_sync_") && !syncToken.startsWith("sc_sync_")) {
|
|
332
|
+
console.error("Invalid token. It should start with intent_sync_");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
console.log("\nConnecting to intentdocs...");
|
|
336
|
+
const config = { syncToken, apiBase: API_BASE };
|
|
337
|
+
try {
|
|
338
|
+
const data = await listStories(config);
|
|
339
|
+
console.log(`\u2713 Connected to "${data.map.title}"`);
|
|
340
|
+
console.log(
|
|
341
|
+
` ${data.summary.total} stories \u2014 ${data.summary.done} done, ${data.summary.todo} todo
|
|
342
|
+
`
|
|
343
|
+
);
|
|
344
|
+
writeConfig({ ...config, mapTitle: data.map.title });
|
|
345
|
+
} catch {
|
|
346
|
+
console.error("Failed to connect. Check your sync token.");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
350
|
+
try {
|
|
351
|
+
await setupFiles(config);
|
|
352
|
+
await setupStack(rl);
|
|
353
|
+
} finally {
|
|
354
|
+
rl.close();
|
|
355
|
+
}
|
|
356
|
+
console.log("\n\u2705 Ready! Try: intent status");
|
|
357
|
+
}
|
|
358
|
+
async function setupFiles(config) {
|
|
359
|
+
try {
|
|
360
|
+
const md = await getContext(config);
|
|
361
|
+
writeFileSync(join(process.cwd(), "INTENT.md"), md);
|
|
362
|
+
console.log("\u2713 Downloaded INTENT.md");
|
|
363
|
+
} catch {
|
|
364
|
+
console.warn("\u26A0 Could not download INTENT.md");
|
|
365
|
+
}
|
|
366
|
+
const gitignore = join(process.cwd(), ".gitignore");
|
|
367
|
+
if (existsSync(gitignore)) {
|
|
368
|
+
const content = readFileSync(gitignore, "utf-8");
|
|
369
|
+
if (!content.includes(".intent/")) {
|
|
370
|
+
appendFileSync(gitignore, "\n# intentdocs\n.intent/\n");
|
|
371
|
+
console.log("\u2713 Added .intent/ to .gitignore");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const cwd = process.cwd();
|
|
375
|
+
if (existsSync(join(cwd, "CLAUDE.md"))) {
|
|
376
|
+
const claudeMd = readFileSync(join(cwd, "CLAUDE.md"), "utf-8");
|
|
377
|
+
if (!claudeMd.includes("INTENT.md")) {
|
|
378
|
+
appendFileSync(
|
|
379
|
+
join(cwd, "CLAUDE.md"),
|
|
380
|
+
"\n\n# intentdocs\nRead INTENT.md for the product story map, personas, and backlog.\n"
|
|
381
|
+
);
|
|
382
|
+
console.log("\u2713 Added INTENT.md reference to CLAUDE.md");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (existsSync(join(cwd, ".cursorrules"))) {
|
|
386
|
+
const rules = readFileSync(join(cwd, ".cursorrules"), "utf-8");
|
|
387
|
+
if (!rules.includes("INTENT.md")) {
|
|
388
|
+
appendFileSync(
|
|
389
|
+
join(cwd, ".cursorrules"),
|
|
390
|
+
"\n\nRead INTENT.md for the product story map, personas, and backlog.\n"
|
|
391
|
+
);
|
|
392
|
+
console.log("\u2713 Added INTENT.md reference to .cursorrules");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/commands/status.ts
|
|
398
|
+
function timeAgo(iso) {
|
|
399
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
400
|
+
const mins = Math.floor(diff / 6e4);
|
|
401
|
+
if (mins < 2) return "just now";
|
|
402
|
+
if (mins < 60) return `${mins}m ago`;
|
|
403
|
+
const hours = Math.floor(mins / 60);
|
|
404
|
+
if (hours < 24) return `${hours}h ago`;
|
|
405
|
+
const days = Math.floor(hours / 24);
|
|
406
|
+
return `${days}d ago`;
|
|
407
|
+
}
|
|
408
|
+
async function status() {
|
|
409
|
+
const config = requireConfig();
|
|
410
|
+
const [storiesData, activityRes] = await Promise.allSettled([
|
|
411
|
+
listStories(config),
|
|
412
|
+
apiFetch(config, "/activity")
|
|
413
|
+
]);
|
|
414
|
+
if (storiesData.status === "rejected") throw storiesData.reason;
|
|
415
|
+
const data = storiesData.value;
|
|
416
|
+
const { summary } = data;
|
|
417
|
+
const pct = summary.total > 0 ? Math.round(summary.done / summary.total * 100) : 0;
|
|
418
|
+
const barLen = 20;
|
|
419
|
+
const filled = Math.round(pct / 100 * barLen);
|
|
420
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
421
|
+
console.log(`
|
|
422
|
+
${data.map.title}`);
|
|
423
|
+
console.log(` ${bar} ${pct}%
|
|
424
|
+
`);
|
|
425
|
+
console.log(` \u2705 Done: ${summary.done}`);
|
|
426
|
+
console.log(` \u{1F528} In progress: ${summary.in_progress}`);
|
|
427
|
+
console.log(` \u2B1C Todo: ${summary.todo}`);
|
|
428
|
+
console.log(` \u2500\u2500 Total: ${summary.total}
|
|
429
|
+
`);
|
|
430
|
+
const byActivity = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const s of data.stories) {
|
|
432
|
+
const cur = byActivity.get(s.activity) ?? { done: 0, total: 0 };
|
|
433
|
+
cur.total++;
|
|
434
|
+
if (s.status === "done") cur.done++;
|
|
435
|
+
byActivity.set(s.activity, cur);
|
|
436
|
+
}
|
|
437
|
+
for (const [activity, counts] of byActivity) {
|
|
438
|
+
const actPct = counts.total > 0 ? Math.round(counts.done / counts.total * 100) : 0;
|
|
439
|
+
console.log(` ${activity}: ${counts.done}/${counts.total} (${actPct}%)`);
|
|
440
|
+
}
|
|
441
|
+
if (activityRes.status === "fulfilled" && activityRes.value.ok) {
|
|
442
|
+
const { activity } = await activityRes.value.json();
|
|
443
|
+
if (activity.length > 0) {
|
|
444
|
+
console.log("\n Recent activity:");
|
|
445
|
+
for (const event of activity.slice(0, 5)) {
|
|
446
|
+
console.log(` \xB7 ${event.summary} (${timeAgo(event.created_at)})`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
console.log();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/stories.ts
|
|
454
|
+
var STATUS_ICON = {
|
|
455
|
+
done: "\u2705",
|
|
456
|
+
in_progress: "\u{1F528}",
|
|
457
|
+
todo: "\u2B1C"
|
|
458
|
+
};
|
|
459
|
+
var STATUS_ALIASES = {
|
|
460
|
+
todo: "todo",
|
|
461
|
+
"in-progress": "in_progress",
|
|
462
|
+
in_progress: "in_progress",
|
|
463
|
+
wip: "in_progress",
|
|
464
|
+
done: "done"
|
|
465
|
+
};
|
|
466
|
+
async function stories(opts) {
|
|
467
|
+
const config = requireConfig();
|
|
468
|
+
const data = await listStories(config);
|
|
469
|
+
let filtered = data.stories;
|
|
470
|
+
if (opts.status) {
|
|
471
|
+
const s = STATUS_ALIASES[opts.status.toLowerCase()];
|
|
472
|
+
if (!s) {
|
|
473
|
+
console.error(`Unknown status "${opts.status}". Use: todo, in-progress, done`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
filtered = filtered.filter((story) => story.status === s);
|
|
477
|
+
} else if (opts.filter) {
|
|
478
|
+
const f = opts.filter.toLowerCase();
|
|
479
|
+
const mappedStatus = STATUS_ALIASES[f];
|
|
480
|
+
if (mappedStatus) {
|
|
481
|
+
filtered = filtered.filter((s) => s.status === mappedStatus);
|
|
482
|
+
} else {
|
|
483
|
+
filtered = filtered.filter(
|
|
484
|
+
(s) => s.activity.toLowerCase().includes(f) || s.title.toLowerCase().includes(f)
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (filtered.length === 0) {
|
|
489
|
+
console.log("No stories found.");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (opts.kanban) {
|
|
493
|
+
kanbanView(filtered);
|
|
494
|
+
} else {
|
|
495
|
+
listView(filtered);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function listView(stories2) {
|
|
499
|
+
const byActivity = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const s of stories2) {
|
|
501
|
+
const list = byActivity.get(s.activity) ?? [];
|
|
502
|
+
list.push(s);
|
|
503
|
+
byActivity.set(s.activity, list);
|
|
504
|
+
}
|
|
505
|
+
for (const [activity, actStories] of byActivity) {
|
|
506
|
+
const done2 = actStories.filter((s) => s.status === "done").length;
|
|
507
|
+
console.log(`
|
|
508
|
+
${activity} (${done2}/${actStories.length})`);
|
|
509
|
+
for (const s of actStories) {
|
|
510
|
+
const icon = STATUS_ICON[s.status] ?? "\u2B1C";
|
|
511
|
+
const id = s.id.slice(0, 8);
|
|
512
|
+
console.log(` ${icon} ${s.title.slice(0, 70)} (${id})`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
console.log();
|
|
516
|
+
}
|
|
517
|
+
function kanbanView(stories2) {
|
|
518
|
+
const termWidth = process.stdout.columns || 100;
|
|
519
|
+
const colW = Math.max(22, Math.floor((termWidth - 4) / 3));
|
|
520
|
+
const innerW = colW - 2;
|
|
521
|
+
const cols = [
|
|
522
|
+
{ label: "TODO", stories: stories2.filter((s) => s.status === "todo") },
|
|
523
|
+
{ label: "IN PROGRESS", stories: stories2.filter((s) => s.status === "in_progress") },
|
|
524
|
+
{ label: "DONE", stories: stories2.filter((s) => s.status === "done") }
|
|
525
|
+
];
|
|
526
|
+
const rule = (l, mid, r) => l + Array(3).fill("\u2500".repeat(colW)).join(mid) + r;
|
|
527
|
+
const cell = (text) => " " + text.slice(0, innerW).padEnd(innerW) + " ";
|
|
528
|
+
const emptyCell = () => " ".repeat(colW);
|
|
529
|
+
const row = (cells) => "\u2502" + cells.join("\u2502") + "\u2502";
|
|
530
|
+
console.log("\n" + rule("\u250C", "\u252C", "\u2510"));
|
|
531
|
+
console.log(
|
|
532
|
+
row(
|
|
533
|
+
cols.map((c) => cell(`${c.label} (${c.stories.length})`))
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
console.log(rule("\u251C", "\u253C", "\u2524"));
|
|
537
|
+
const maxRows = Math.max(...cols.map((c) => c.stories.length), 0);
|
|
538
|
+
for (let i = 0; i < maxRows; i++) {
|
|
539
|
+
console.log(
|
|
540
|
+
row(
|
|
541
|
+
cols.map((c) => {
|
|
542
|
+
const s = c.stories[i];
|
|
543
|
+
return s ? cell(s.title) : emptyCell();
|
|
544
|
+
})
|
|
545
|
+
)
|
|
546
|
+
);
|
|
547
|
+
console.log(
|
|
548
|
+
row(
|
|
549
|
+
cols.map((c) => {
|
|
550
|
+
const s = c.stories[i];
|
|
551
|
+
return s ? cell(`${s.activity} \xB7 ${s.priority}`) : emptyCell();
|
|
552
|
+
})
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
console.log(
|
|
556
|
+
row(
|
|
557
|
+
cols.map((c) => {
|
|
558
|
+
const s = c.stories[i];
|
|
559
|
+
return s ? cell(s.id.slice(0, 8)) : emptyCell();
|
|
560
|
+
})
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
if (i < maxRows - 1) {
|
|
564
|
+
console.log(row(cols.map(() => emptyCell())));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
console.log(rule("\u2514", "\u2534", "\u2518") + "\n");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/commands/next.ts
|
|
571
|
+
import { execSync } from "child_process";
|
|
572
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
573
|
+
import { join as join2 } from "path";
|
|
574
|
+
|
|
575
|
+
// src/lib/verification-suggestions.ts
|
|
576
|
+
function needsVerificationSuggestion(ac) {
|
|
577
|
+
return !(ac.verification?.method === "executable" && Boolean(ac.verification.command));
|
|
578
|
+
}
|
|
579
|
+
function buildVerificationSuggestionPrompt(title, userStory, criteria) {
|
|
580
|
+
const list = criteria.map((ac, i) => `${i + 1}. ${ac.text}${ac.verification ? ` (current method: ${ac.verification.method})` : ""}`).join("\n");
|
|
581
|
+
return `You are wiring up automatic verification for a user story's acceptance criteria.
|
|
582
|
+
|
|
583
|
+
Story: ${title}
|
|
584
|
+
${userStory ? `User story: ${userStory}
|
|
585
|
+
` : ""}
|
|
586
|
+
For each criterion below, decide whether it can be verified by running a real shell command (a test, a script, curl against a running server, a build check, etc). Bias toward "yes" whenever anything runnable applies \u2014 that's a stronger signal than a human/agent opinion.
|
|
587
|
+
|
|
588
|
+
Criteria:
|
|
589
|
+
${list}
|
|
590
|
+
|
|
591
|
+
Return ONLY a JSON array, one entry per criterion in order, no markdown fences, no commentary:
|
|
592
|
+
[
|
|
593
|
+
{ "index": 1, "executable": true, "command": "npm test -- onboarding-redirect", "expected": "exit 0, all tests pass" },
|
|
594
|
+
{ "index": 2, "executable": false, "reason": "requires eyeballing the rendered layout" }
|
|
595
|
+
]`;
|
|
596
|
+
}
|
|
597
|
+
function parseVerificationSuggestions(raw) {
|
|
598
|
+
const cleaned = raw.replace(/```json\s*/g, "").replace(/```/g, "").trim();
|
|
599
|
+
let parsed;
|
|
600
|
+
try {
|
|
601
|
+
parsed = JSON.parse(cleaned);
|
|
602
|
+
} catch {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
if (!Array.isArray(parsed)) return [];
|
|
606
|
+
return parsed.filter((item) => {
|
|
607
|
+
if (typeof item !== "object" || item === null) return false;
|
|
608
|
+
const o = item;
|
|
609
|
+
if (typeof o.index !== "number" || typeof o.executable !== "boolean") return false;
|
|
610
|
+
if (o.executable) return typeof o.command === "string" && o.command.length > 0;
|
|
611
|
+
return true;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/commands/next.ts
|
|
616
|
+
var PRIORITY_ORDER = { must: 0, should: 1, could: 2 };
|
|
617
|
+
var EFFORT_ORDER = { S: 0, M: 1, L: 2, XL: 3 };
|
|
618
|
+
async function next(opts = {}) {
|
|
619
|
+
const config = requireConfig();
|
|
620
|
+
const data = await listStories(config);
|
|
621
|
+
let s;
|
|
622
|
+
if (opts.storyId) {
|
|
623
|
+
const match = data.stories.find((st) => st.id === opts.storyId || st.id.startsWith(opts.storyId));
|
|
624
|
+
if (!match) {
|
|
625
|
+
console.log(`
|
|
626
|
+
\u274C No story found matching ID: ${opts.storyId}
|
|
627
|
+
`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
s = match;
|
|
631
|
+
} else {
|
|
632
|
+
if (!opts.skipWip) {
|
|
633
|
+
const inProgress = data.stories.filter((st) => st.status === "in_progress");
|
|
634
|
+
if (inProgress.length > 0) {
|
|
635
|
+
console.log(`
|
|
636
|
+
\u26A0\uFE0F You have ${inProgress.length} story${inProgress.length > 1 ? " stories" : ""} in progress:
|
|
637
|
+
`);
|
|
638
|
+
for (const ip of inProgress) {
|
|
639
|
+
console.log(` \u{1F528} ${ip.title}`);
|
|
640
|
+
console.log(` ID: ${ip.id.slice(0, 8)} | Mark done: intent done ${ip.id.slice(0, 8)}`);
|
|
641
|
+
}
|
|
642
|
+
console.log(`
|
|
643
|
+
Finish those first, or run \`intent next --skip-wip\` to pick the next todo anyway.
|
|
644
|
+
`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const doneIds = new Set(
|
|
649
|
+
data.stories.filter((st) => st.status === "done").map((st) => st.id)
|
|
650
|
+
);
|
|
651
|
+
const todos = data.stories.filter((st) => st.status === "todo").filter((st) => {
|
|
652
|
+
if (!st.depends_on?.length) return true;
|
|
653
|
+
return st.depends_on.every((depId) => doneIds.has(depId));
|
|
654
|
+
}).sort((a, b) => {
|
|
655
|
+
const aSeq = a.build_sequence ?? Infinity;
|
|
656
|
+
const bSeq = b.build_sequence ?? Infinity;
|
|
657
|
+
if (aSeq !== bSeq) return aSeq - bSeq;
|
|
658
|
+
if (a.release !== b.release) return a.release - b.release;
|
|
659
|
+
const pa = PRIORITY_ORDER[a.priority] ?? 9;
|
|
660
|
+
const pb = PRIORITY_ORDER[b.priority] ?? 9;
|
|
661
|
+
if (pa !== pb) return pa - pb;
|
|
662
|
+
return (EFFORT_ORDER[a.effort] ?? 9) - (EFFORT_ORDER[b.effort] ?? 9);
|
|
663
|
+
});
|
|
664
|
+
if (todos.length === 0) {
|
|
665
|
+
const allTodos = data.stories.filter((st) => st.status === "todo");
|
|
666
|
+
if (allTodos.length > 0) {
|
|
667
|
+
console.log(`
|
|
668
|
+
\u{1F512} ${allTodos.length} stories remain but all have unfinished dependencies.`);
|
|
669
|
+
console.log(` Finish in-progress stories first, then run \`intent next\` again.
|
|
670
|
+
`);
|
|
671
|
+
} else {
|
|
672
|
+
console.log("\n \u{1F389} All stories are done! Ship it.\n");
|
|
673
|
+
}
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
s = todos[0];
|
|
677
|
+
}
|
|
678
|
+
const specUrl = `${config.apiBase}/storymapper/${data.map.id}?story=${s.id}`;
|
|
679
|
+
const hasPlan = s.implementation_plan?.status === "ready" && s.implementation_plan.markdown;
|
|
680
|
+
console.log(`
|
|
681
|
+
\u{1F4CB} Next up:`);
|
|
682
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
683
|
+
console.log(` ${s.title}`);
|
|
684
|
+
console.log(` Activity: ${s.activity}`);
|
|
685
|
+
console.log(` Priority: ${s.priority} | Effort: ${s.effort} | Release: ${s.release}${s.build_sequence ? ` | Build step: ${s.build_sequence}` : ""}`);
|
|
686
|
+
if (s.acceptance_criteria.length > 0) {
|
|
687
|
+
console.log(` Done when:`);
|
|
688
|
+
for (const ac of s.acceptance_criteria) {
|
|
689
|
+
console.log(` \u2713 ${ac.text}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
console.log(`
|
|
693
|
+
ID: ${s.id}`);
|
|
694
|
+
console.log(` Spec: ${specUrl}`);
|
|
695
|
+
if (hasPlan) {
|
|
696
|
+
console.log(` Plan: \u2705 Implementation plan available`);
|
|
697
|
+
}
|
|
698
|
+
console.log(`
|
|
699
|
+
Start building: intent implement ${s.id.slice(0, 8)}
|
|
700
|
+
`);
|
|
701
|
+
if (opts.open) {
|
|
702
|
+
openBrowser2(specUrl);
|
|
703
|
+
}
|
|
704
|
+
await shutdownCliAnalytics();
|
|
705
|
+
}
|
|
706
|
+
async function generateVerificationSuggestions(story, contextPath) {
|
|
707
|
+
const acs = story.acceptance_criteria;
|
|
708
|
+
const candidates = acs.filter(needsVerificationSuggestion);
|
|
709
|
+
if (acs.length === 0) {
|
|
710
|
+
console.log(` \u26A0\uFE0F No acceptance criteria \u2014 nothing to suggest verification for.
|
|
711
|
+
`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (candidates.length === 0) {
|
|
715
|
+
console.log(` \u2713 Every criterion already has an executable command.
|
|
716
|
+
`);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
720
|
+
if (!apiKey) {
|
|
721
|
+
console.log(` \u26A0\uFE0F ANTHROPIC_API_KEY not set \u2014 skipping verification suggestions.
|
|
722
|
+
`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
console.log(` Suggesting verification commands for ${candidates.length} criteri${candidates.length !== 1 ? "a" : "on"}\u2026`);
|
|
726
|
+
const prompt = buildVerificationSuggestionPrompt(story.title, story.user_story, candidates);
|
|
727
|
+
let raw;
|
|
728
|
+
try {
|
|
729
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: {
|
|
732
|
+
"Content-Type": "application/json",
|
|
733
|
+
"x-api-key": apiKey,
|
|
734
|
+
"anthropic-version": "2023-06-01"
|
|
735
|
+
},
|
|
736
|
+
body: JSON.stringify({
|
|
737
|
+
model: "claude-haiku-4-5-20251001",
|
|
738
|
+
max_tokens: 1024,
|
|
739
|
+
messages: [{ role: "user", content: prompt }]
|
|
740
|
+
})
|
|
741
|
+
});
|
|
742
|
+
if (!res.ok) {
|
|
743
|
+
console.log(` \u26A0\uFE0F Anthropic API error (${res.status}) \u2014 suggestion generation skipped.
|
|
744
|
+
`);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
const json = await res.json();
|
|
748
|
+
raw = json.content.find((c) => c.type === "text")?.text ?? "";
|
|
749
|
+
} catch {
|
|
750
|
+
console.log(` \u26A0\uFE0F Network error \u2014 suggestion generation skipped.
|
|
751
|
+
`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const suggestions = parseVerificationSuggestions(raw);
|
|
755
|
+
if (suggestions.length === 0) {
|
|
756
|
+
console.log(` \u26A0\uFE0F Couldn't parse a usable response from Anthropic \u2014 suggestion generation skipped.
|
|
757
|
+
`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const lines = [`# Suggested verification commands: ${story.title}`, ``, `_Generated by \`intent next --stub\` \u2014 copy into the criterion's command field, or run \`intent check\` once wired up._`, ``];
|
|
761
|
+
console.log();
|
|
762
|
+
for (const suggestion of suggestions) {
|
|
763
|
+
const ac = candidates[suggestion.index - 1];
|
|
764
|
+
if (!ac) continue;
|
|
765
|
+
if (suggestion.executable && suggestion.command) {
|
|
766
|
+
console.log(` \u2192 ${ac.text}`);
|
|
767
|
+
console.log(` command: ${suggestion.command}${suggestion.expected ? ` (expect: ${suggestion.expected})` : ""}`);
|
|
768
|
+
lines.push(`- **${ac.text}**`, ` - command: \`${suggestion.command}\``, ...suggestion.expected ? [` - expected: ${suggestion.expected}`] : [], ``);
|
|
769
|
+
} else {
|
|
770
|
+
console.log(` \u2192 ${ac.text}`);
|
|
771
|
+
console.log(` not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`);
|
|
772
|
+
lines.push(`- **${ac.text}**`, ` - not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`, ``);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const suggestionsPath = contextPath.replace(/\.md$/, ".checks.md");
|
|
776
|
+
writeFileSync2(suggestionsPath, lines.join("\n"));
|
|
777
|
+
const relPath = suggestionsPath.replace(join2(process.cwd(), "/"), "");
|
|
778
|
+
console.log(`
|
|
779
|
+
\u2713 Suggestions written: ${relPath}
|
|
780
|
+
`);
|
|
781
|
+
}
|
|
782
|
+
function openBrowser2(url) {
|
|
783
|
+
try {
|
|
784
|
+
const platform = process.platform;
|
|
785
|
+
const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
786
|
+
execSync(cmd, { stdio: "ignore" });
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/commands/implement.ts
|
|
792
|
+
async function implement(storyId, opts = {}) {
|
|
793
|
+
const config = requireConfig();
|
|
794
|
+
const data = await listStories(config);
|
|
795
|
+
const match = data.stories.find(
|
|
796
|
+
(st) => st.id === storyId || st.id.startsWith(storyId)
|
|
797
|
+
);
|
|
798
|
+
if (!match) {
|
|
799
|
+
console.log(`
|
|
800
|
+
\u274C No story found matching ID: ${storyId}
|
|
801
|
+
`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const s = match;
|
|
805
|
+
const specUrl = `${config.apiBase}/storymapper/${data.map.id}?story=${s.id}`;
|
|
806
|
+
if (s.status === "done") {
|
|
807
|
+
console.log(`
|
|
808
|
+
\u2705 "${s.title}" is already done.
|
|
809
|
+
`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (s.status === "in_progress") {
|
|
813
|
+
console.log(`
|
|
814
|
+
\u{1F528} "${s.title}" is already in progress.`);
|
|
815
|
+
console.log(` Mark done: intent done ${s.id.slice(0, 8)}
|
|
816
|
+
`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const doneIds = new Set(
|
|
820
|
+
data.stories.filter((st) => st.status === "done").map((st) => st.id)
|
|
821
|
+
);
|
|
822
|
+
if (s.depends_on?.length) {
|
|
823
|
+
const blocked = s.depends_on.filter((depId) => !doneIds.has(depId));
|
|
824
|
+
if (blocked.length > 0) {
|
|
825
|
+
console.log(`
|
|
826
|
+
\u{1F512} "${s.title}" is blocked by ${blocked.length} unfinished dependenc${blocked.length > 1 ? "ies" : "y"}:`);
|
|
827
|
+
for (const depId of blocked) {
|
|
828
|
+
const dep = data.stories.find((st) => st.id === depId);
|
|
829
|
+
console.log(` - ${dep ? dep.title : depId.slice(0, 8)} (${dep?.status ?? "unknown"})`);
|
|
830
|
+
}
|
|
831
|
+
console.log(`
|
|
832
|
+
Finish those first.
|
|
833
|
+
`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (s.effort === "XL") {
|
|
838
|
+
const acCount = s.acceptance_criteria.length;
|
|
839
|
+
console.log(`
|
|
840
|
+
\u250C\u2500 High complexity story \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
|
|
841
|
+
console.log(` \u2502 \u2502`);
|
|
842
|
+
console.log(` \u2502 This story is sized XL \u2014 it may benefit from \u2502`);
|
|
843
|
+
console.log(` \u2502 breaking it into smaller pieces before starting. \u2502`);
|
|
844
|
+
console.log(` \u2502 \u2502`);
|
|
845
|
+
if (acCount === 0) {
|
|
846
|
+
console.log(` \u2502 \u26A0\uFE0F No acceptance criteria defined \u2014 the agent \u2502`);
|
|
847
|
+
console.log(` \u2502 won't know what "done" looks like. \u2502`);
|
|
848
|
+
} else {
|
|
849
|
+
console.log(` \u2502 \u2713 ${acCount} acceptance criteri${acCount !== 1 ? "a" : "on"} defined. \u2502`);
|
|
850
|
+
}
|
|
851
|
+
console.log(` \u2502 \u2502`);
|
|
852
|
+
console.log(` \u2502 Consider: \u2502`);
|
|
853
|
+
console.log(` \u2502 intent sync --context refresh CLAUDE.md first \u2502`);
|
|
854
|
+
console.log(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
855
|
+
`);
|
|
856
|
+
const proceed = await confirm(" Assign this story and mark in progress?");
|
|
857
|
+
if (!proceed) {
|
|
858
|
+
console.log(`
|
|
859
|
+
Skipped.
|
|
860
|
+
`);
|
|
861
|
+
closePrompt();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (s.acceptance_criteria.length === 0) {
|
|
866
|
+
console.log(`
|
|
867
|
+
\u26A0\uFE0F No acceptance criteria defined for this story.
|
|
868
|
+
|
|
869
|
+
Your AI agent won't know what "done" looks like \u2014 it will
|
|
870
|
+
have to guess, which leads to incomplete or misaligned work.
|
|
871
|
+
|
|
872
|
+
Add acceptance criteria before starting:
|
|
873
|
+
\u2192 ${specUrl}
|
|
874
|
+
`);
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
await updateStories(config, [{ id: s.id, status: "in_progress", cancelled_at: null }]);
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
const contextPath = writeStoryContext(s);
|
|
881
|
+
const relPath = contextPath.replace(process.cwd() + "/", "");
|
|
882
|
+
const effortToComplexity = (e) => e === "XL" || e === "L" ? "high" : e === "M" ? "med" : "low";
|
|
883
|
+
captureCli(
|
|
884
|
+
"story_started",
|
|
885
|
+
{
|
|
886
|
+
story_id: s.id,
|
|
887
|
+
complexity: effortToComplexity(s.effort),
|
|
888
|
+
has_eval_stub: false,
|
|
889
|
+
context_loaded: true
|
|
890
|
+
},
|
|
891
|
+
{ project: config.projectId }
|
|
892
|
+
);
|
|
893
|
+
console.log(`
|
|
894
|
+
\u{1F528} Picked up: ${s.title}`);
|
|
895
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
896
|
+
console.log(` Activity: ${s.activity}`);
|
|
897
|
+
console.log(` Priority: ${s.priority} | Effort: ${s.effort} | Release: ${s.release}${s.build_sequence ? ` | Build step: ${s.build_sequence}` : ""}`);
|
|
898
|
+
console.log(` Context: ${relPath}`);
|
|
899
|
+
console.log(` Spec: ${specUrl}`);
|
|
900
|
+
console.log(` Mark done: intent done ${s.id.slice(0, 8)}
|
|
901
|
+
`);
|
|
902
|
+
if (opts.stub) {
|
|
903
|
+
await generateVerificationSuggestions(s, contextPath);
|
|
904
|
+
}
|
|
905
|
+
await shutdownCliAnalytics();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/commands/done.ts
|
|
909
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
910
|
+
|
|
911
|
+
// src/lib/screenshot.ts
|
|
912
|
+
import { createRequire } from "module";
|
|
913
|
+
import { pathToFileURL } from "url";
|
|
914
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
915
|
+
import { extname, join as join3 } from "path";
|
|
916
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
917
|
+
var EXT_MIME = {
|
|
918
|
+
".png": "image/png",
|
|
919
|
+
".jpg": "image/jpeg",
|
|
920
|
+
".jpeg": "image/jpeg",
|
|
921
|
+
".webp": "image/webp"
|
|
922
|
+
};
|
|
923
|
+
async function loadPlaywright() {
|
|
924
|
+
try {
|
|
925
|
+
const req = createRequire(join3(process.cwd(), "package.json"));
|
|
926
|
+
const resolved = req.resolve("playwright");
|
|
927
|
+
return await import(pathToFileURL(resolved).href);
|
|
928
|
+
} catch {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function readScreenshotFile(path) {
|
|
933
|
+
if (!existsSync2(path)) {
|
|
934
|
+
console.error(` Screenshot file not found: ${path}`);
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
const ext = extname(path).toLowerCase();
|
|
938
|
+
const mimeType = EXT_MIME[ext];
|
|
939
|
+
if (!mimeType) {
|
|
940
|
+
console.error(` Unsupported screenshot type "${ext}" \u2014 use .png, .jpg, or .webp`);
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
const buffer = readFileSync2(path);
|
|
944
|
+
if (buffer.length > MAX_BYTES) {
|
|
945
|
+
console.error(` Screenshot too large (${(buffer.length / 1024 / 1024).toFixed(1)}MB, max 5MB)`);
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
return { buffer, mimeType, filename: `screenshot${ext}` };
|
|
949
|
+
}
|
|
950
|
+
async function captureScreenshot(appUrl) {
|
|
951
|
+
const playwright = await loadPlaywright();
|
|
952
|
+
if (!playwright) {
|
|
953
|
+
console.log(
|
|
954
|
+
" \u21B3 Screenshot skipped: playwright not found. Run `npm i -D playwright` in this project to enable auto-capture, or pass --screenshot <path>."
|
|
955
|
+
);
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
let browser = null;
|
|
959
|
+
try {
|
|
960
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
961
|
+
const page = await browser.newPage();
|
|
962
|
+
await page.goto(appUrl, { timeout: 1e4, waitUntil: "load" });
|
|
963
|
+
const buffer = await page.screenshot({ type: "png" });
|
|
964
|
+
if (buffer.length > MAX_BYTES) {
|
|
965
|
+
console.log(" \u21B3 Screenshot skipped: capture exceeded the 5MB limit.");
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
return { buffer, mimeType: "image/png", filename: "screenshot.png" };
|
|
969
|
+
} catch (err) {
|
|
970
|
+
const message = err instanceof Error ? err.message : "capture failed";
|
|
971
|
+
console.log(` \u21B3 Screenshot skipped: ${message}`);
|
|
972
|
+
return null;
|
|
973
|
+
} finally {
|
|
974
|
+
await browser?.close().catch(() => void 0);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/commands/done.ts
|
|
979
|
+
function extractImplementationContext(storyId) {
|
|
980
|
+
const path = storyContextPath(storyId);
|
|
981
|
+
if (!existsSync3(path)) return { summary: null, files: [] };
|
|
982
|
+
const content = readFileSync3(path, "utf-8");
|
|
983
|
+
const lines = content.split("\n");
|
|
984
|
+
const notesStart = lines.findIndex((l) => l.trim() === "## Implementation notes");
|
|
985
|
+
const decisionsStart = lines.findIndex((l) => l.trim() === "## Decisions");
|
|
986
|
+
const filesStart = lines.findIndex((l) => l.trim() === "## Relevant files");
|
|
987
|
+
let summaryLines = [];
|
|
988
|
+
if (notesStart !== -1) {
|
|
989
|
+
const end = decisionsStart !== -1 && decisionsStart > notesStart ? decisionsStart : filesStart !== -1 && filesStart > notesStart ? filesStart : lines.length;
|
|
990
|
+
summaryLines = lines.slice(notesStart + 1, end).filter((l) => l.trim() && !l.trim().startsWith("_Notes added via"));
|
|
991
|
+
}
|
|
992
|
+
let files = [];
|
|
993
|
+
if (filesStart !== -1) {
|
|
994
|
+
const end = lines.findIndex((l, i) => i > filesStart && l.startsWith("## "));
|
|
995
|
+
const fileLines = lines.slice(filesStart + 1, end === -1 ? void 0 : end);
|
|
996
|
+
files = fileLines.map((l) => l.replace(/^[-*]\s*/, "").trim()).filter((l) => l && !l.startsWith("_") && (l.includes("/") || l.includes(".")));
|
|
997
|
+
}
|
|
998
|
+
const summary = summaryLines.length > 0 ? summaryLines.join("\n").trim() : null;
|
|
999
|
+
return { summary, files };
|
|
1000
|
+
}
|
|
1001
|
+
async function passesCriteriaGuard(story) {
|
|
1002
|
+
if (story.acceptance_criteria.length === 0) return { proceed: true, forced: false };
|
|
1003
|
+
console.log();
|
|
1004
|
+
console.log(" Acceptance criteria:");
|
|
1005
|
+
for (const ac of story.acceptance_criteria) {
|
|
1006
|
+
console.log(` [${criterionPriority(ac)}] [${criterionStatusLabel(ac)}] ${ac.text}`);
|
|
1007
|
+
}
|
|
1008
|
+
const blocking = findBlockingMusts(story.acceptance_criteria);
|
|
1009
|
+
if (blocking.length === 0) return { proceed: true, forced: false };
|
|
1010
|
+
console.log();
|
|
1011
|
+
console.log(` ${blocking.length} 'must' criterion/criteria still failing or unreported:`);
|
|
1012
|
+
for (const ac of blocking) {
|
|
1013
|
+
console.log(` - [${criterionStatusLabel(ac)}] ${ac.text}`);
|
|
1014
|
+
}
|
|
1015
|
+
console.log();
|
|
1016
|
+
const proceed = await confirm(" Mark done anyway?");
|
|
1017
|
+
return { proceed, forced: proceed };
|
|
1018
|
+
}
|
|
1019
|
+
async function done(idPrefix, opts = {}) {
|
|
1020
|
+
const config = requireConfig();
|
|
1021
|
+
const data = await listStories(config);
|
|
1022
|
+
const match = data.stories.find((s) => s.id.startsWith(idPrefix));
|
|
1023
|
+
if (!match) {
|
|
1024
|
+
console.error(`No story found matching "${idPrefix}"`);
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
if (match.status === "done") {
|
|
1028
|
+
console.log(`Already done: ${match.title}`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const guard = await passesCriteriaGuard(match);
|
|
1032
|
+
if (!guard.proceed) {
|
|
1033
|
+
closePrompt();
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const { summary, files } = extractImplementationContext(match.id);
|
|
1037
|
+
const captured = opts.screenshot ? readScreenshotFile(opts.screenshot) : config.appUrl ? await captureScreenshot(config.appUrl) : null;
|
|
1038
|
+
let screenshotUrl;
|
|
1039
|
+
if (captured) {
|
|
1040
|
+
const uploaded = await uploadScreenshot(config, match.id, captured);
|
|
1041
|
+
if (uploaded) {
|
|
1042
|
+
screenshotUrl = uploaded.url;
|
|
1043
|
+
} else {
|
|
1044
|
+
console.error(" Failed to upload screenshot (continuing without it).");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const update = { id: match.id, status: "done" };
|
|
1048
|
+
if (guard.forced) update.forced = true;
|
|
1049
|
+
if (summary) update.implementation_summary = summary;
|
|
1050
|
+
if (files.length > 0) update.implemented_files = files;
|
|
1051
|
+
if (screenshotUrl) update.screenshot_url = screenshotUrl;
|
|
1052
|
+
const result = await updateStories(config, [update]);
|
|
1053
|
+
const effortToComplexity = (e) => e === "XL" || e === "L" ? "high" : e === "M" ? "med" : "low";
|
|
1054
|
+
captureCli(
|
|
1055
|
+
"story_shipped",
|
|
1056
|
+
{
|
|
1057
|
+
story_id: match.id,
|
|
1058
|
+
complexity: effortToComplexity(match.effort),
|
|
1059
|
+
context_loaded: summary !== null,
|
|
1060
|
+
acceptance_criteria_count: match.acceptance_criteria.length,
|
|
1061
|
+
eval_stub_generated: false,
|
|
1062
|
+
cycle_time_hours: 0
|
|
1063
|
+
},
|
|
1064
|
+
{ project: config.projectId }
|
|
1065
|
+
);
|
|
1066
|
+
console.log(`\u2705 Done: ${match.title}`);
|
|
1067
|
+
if (summary) console.log(` \u21B3 Implementation summary posted to story map`);
|
|
1068
|
+
if (files.length > 0) console.log(` \u21B3 ${files.length} file(s) recorded`);
|
|
1069
|
+
if (screenshotUrl) console.log(` \u21B3 Screenshot attached`);
|
|
1070
|
+
for (const release of result.completedReleases ?? []) {
|
|
1071
|
+
console.log();
|
|
1072
|
+
console.log(`\u{1F680} Release shipped: ${release.label} (${release.storyCount} stories)`);
|
|
1073
|
+
}
|
|
1074
|
+
console.log();
|
|
1075
|
+
const wantsDecision = await confirm(" Log a decision or lesson from this story?");
|
|
1076
|
+
if (wantsDecision) {
|
|
1077
|
+
const title = await ask(" Decision title: ");
|
|
1078
|
+
const description = await ask(" What was decided / learned: ");
|
|
1079
|
+
if (title.trim() && description.trim()) {
|
|
1080
|
+
const res = await apiFetch(config, "/decisions", {
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
headers: { "Content-Type": "application/json" },
|
|
1083
|
+
body: JSON.stringify({
|
|
1084
|
+
title: title.trim(),
|
|
1085
|
+
description: description.trim(),
|
|
1086
|
+
category: "technical",
|
|
1087
|
+
story_id: match.id
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
if (res.ok) {
|
|
1091
|
+
console.log(` \u2713 Decision logged: "${title.trim()}"`);
|
|
1092
|
+
captureCli(
|
|
1093
|
+
"decision_logged",
|
|
1094
|
+
{
|
|
1095
|
+
trigger: "stc_done",
|
|
1096
|
+
story_id: match.id,
|
|
1097
|
+
has_lesson: description.trim().length > 0
|
|
1098
|
+
},
|
|
1099
|
+
{ project: config.projectId }
|
|
1100
|
+
);
|
|
1101
|
+
} else {
|
|
1102
|
+
console.error(" Failed to log decision.");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
apiFetch(config, "/activity", {
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
headers: { "Content-Type": "application/json" },
|
|
1109
|
+
body: JSON.stringify({
|
|
1110
|
+
event_type: "story_done",
|
|
1111
|
+
summary: `Marked done: ${match.title}`,
|
|
1112
|
+
metadata: { story_id: match.id, ...screenshotUrl ? { screenshot_url: screenshotUrl } : {} },
|
|
1113
|
+
source: "cli"
|
|
1114
|
+
})
|
|
1115
|
+
}).catch(() => void 0);
|
|
1116
|
+
await shutdownCliAnalytics();
|
|
1117
|
+
closePrompt();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/commands/stop.ts
|
|
1121
|
+
async function stop(storyId) {
|
|
1122
|
+
const config = requireConfig();
|
|
1123
|
+
const data = await listStories(config);
|
|
1124
|
+
const match = data.stories.find(
|
|
1125
|
+
(st) => st.id === storyId || st.id.startsWith(storyId)
|
|
1126
|
+
);
|
|
1127
|
+
if (!match) {
|
|
1128
|
+
console.log(`
|
|
1129
|
+
\u274C No story found matching ID: ${storyId}
|
|
1130
|
+
`);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (match.status !== "in_progress") {
|
|
1134
|
+
console.log(`
|
|
1135
|
+
\u26A0\uFE0F "${match.title}" is not in progress (status: ${match.status}).
|
|
1136
|
+
`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
await updateStories(config, [
|
|
1140
|
+
{ id: match.id, status: "todo", cancelled_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1141
|
+
]);
|
|
1142
|
+
console.log(`
|
|
1143
|
+
\u{1F6D1} Stopped: ${match.title}`);
|
|
1144
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1145
|
+
console.log(` Status reset to todo. Any agent working on this story`);
|
|
1146
|
+
console.log(` will detect the cancellation and stop.
|
|
1147
|
+
`);
|
|
1148
|
+
await shutdownCliAnalytics();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/lib/run-criterion.ts
|
|
1152
|
+
import { execSync as execSync2 } from "child_process";
|
|
1153
|
+
var MAX_DETAIL_LENGTH = 1e3;
|
|
1154
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1155
|
+
function truncate(text) {
|
|
1156
|
+
const trimmed = text.trim();
|
|
1157
|
+
return trimmed.length > MAX_DETAIL_LENGTH ? `${trimmed.slice(0, MAX_DETAIL_LENGTH)}
|
|
1158
|
+
\u2026 (truncated)` : trimmed;
|
|
1159
|
+
}
|
|
1160
|
+
function runExecutableCriterion(command, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1161
|
+
try {
|
|
1162
|
+
const output2 = execSync2(command, {
|
|
1163
|
+
encoding: "utf-8",
|
|
1164
|
+
timeout: timeoutMs,
|
|
1165
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1166
|
+
});
|
|
1167
|
+
return { status: "pass", detail: truncate(output2) || "exit 0" };
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
const e = err;
|
|
1170
|
+
const combined = [e.stdout, e.stderr].filter(Boolean).map(String).join("\n").trim();
|
|
1171
|
+
const reason = e.signal === "SIGTERM" ? `timed out after ${timeoutMs}ms` : `exit ${e.status ?? "unknown"}`;
|
|
1172
|
+
return { status: "fail", detail: truncate(combined || reason || e.message || "unknown error") };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/commands/check.ts
|
|
1177
|
+
var METHOD_LABELS = {
|
|
1178
|
+
executable: "executable",
|
|
1179
|
+
agent_self_check: "self-check",
|
|
1180
|
+
visual_render: "visual",
|
|
1181
|
+
founder_confirm: "founder"
|
|
1182
|
+
};
|
|
1183
|
+
function methodLabel(ac) {
|
|
1184
|
+
return ac.verification ? METHOD_LABELS[ac.verification.method] ?? ac.verification.method : "founder";
|
|
1185
|
+
}
|
|
1186
|
+
function isRunnable(ac) {
|
|
1187
|
+
return ac.verification?.method === "executable" && Boolean(ac.verification.command);
|
|
1188
|
+
}
|
|
1189
|
+
function resolveStory(stories2, idPrefix) {
|
|
1190
|
+
if (idPrefix) {
|
|
1191
|
+
const story = stories2.find((s) => s.id.startsWith(idPrefix));
|
|
1192
|
+
return story ? { story } : { error: `No story found matching "${idPrefix}"` };
|
|
1193
|
+
}
|
|
1194
|
+
const inProgress = stories2.filter((s) => s.status === "in_progress");
|
|
1195
|
+
if (inProgress.length === 1) return { story: inProgress[0] };
|
|
1196
|
+
return {
|
|
1197
|
+
error: inProgress.length === 0 ? "No story in progress. Specify one: intent check <idPrefix>" : `${inProgress.length} stories in progress \u2014 specify one: intent check <idPrefix>`
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function check(idPrefix, opts = {}) {
|
|
1201
|
+
const config = requireConfig();
|
|
1202
|
+
const data = await listStories(config);
|
|
1203
|
+
const { story, error } = resolveStory(data.stories, idPrefix);
|
|
1204
|
+
if (!story) {
|
|
1205
|
+
console.error(error);
|
|
1206
|
+
process.exitCode = 1;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (story.acceptance_criteria.length === 0) {
|
|
1210
|
+
console.log(`
|
|
1211
|
+
${story.title} has no acceptance criteria.
|
|
1212
|
+
`);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
console.log(`
|
|
1216
|
+
Checking: ${story.title}`);
|
|
1217
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1218
|
+
const toReport = [];
|
|
1219
|
+
let anyFail = false;
|
|
1220
|
+
for (const ac of story.acceptance_criteria) {
|
|
1221
|
+
const priority = criterionPriority(ac);
|
|
1222
|
+
const method = methodLabel(ac);
|
|
1223
|
+
if (isRunnable(ac)) {
|
|
1224
|
+
const result = runExecutableCriterion(ac.verification.command);
|
|
1225
|
+
if (result.status === "fail") anyFail = true;
|
|
1226
|
+
const icon = result.status === "pass" ? "\u2705" : "\u274C";
|
|
1227
|
+
console.log(` ${icon} [${priority}][${method}] ${ac.text}`);
|
|
1228
|
+
if (result.status === "fail") {
|
|
1229
|
+
console.log(` ${result.detail.split("\n")[0]}`);
|
|
1230
|
+
}
|
|
1231
|
+
toReport.push({ criterion_id: ac.id, status: result.status, detail: result.detail, reported_by: "human" });
|
|
1232
|
+
} else {
|
|
1233
|
+
console.log(` \u2B1C [${priority}][${method}] ${ac.text} \u2014 manual`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
console.log();
|
|
1237
|
+
if (opts.report) {
|
|
1238
|
+
if (toReport.length === 0) {
|
|
1239
|
+
console.log(` No executable results to report.
|
|
1240
|
+
`);
|
|
1241
|
+
} else {
|
|
1242
|
+
await reportCriteria(config, story.id, toReport);
|
|
1243
|
+
console.log(` \u2713 Reported ${toReport.length} result(s) to the story map.
|
|
1244
|
+
`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (anyFail) process.exitCode = 1;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/commands/add.ts
|
|
1251
|
+
async function add(title, opts) {
|
|
1252
|
+
const config = requireConfig();
|
|
1253
|
+
const result = await createStories(config, [
|
|
1254
|
+
{
|
|
1255
|
+
title,
|
|
1256
|
+
activity: opts.activity,
|
|
1257
|
+
priority: opts.priority ?? "should",
|
|
1258
|
+
effort: opts.effort ?? "M"
|
|
1259
|
+
}
|
|
1260
|
+
]);
|
|
1261
|
+
if (result.created > 0) {
|
|
1262
|
+
const s = result.stories[0];
|
|
1263
|
+
console.log(`\u2713 Created: ${s.title}`);
|
|
1264
|
+
console.log(` Activity: ${s.activity} | ID: ${s.id.slice(0, 8)}`);
|
|
1265
|
+
} else {
|
|
1266
|
+
console.error("Failed to create story.");
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/lib/banner.ts
|
|
1271
|
+
import chalk from "chalk";
|
|
1272
|
+
var VERSION = "0.2.2";
|
|
1273
|
+
var brand = chalk.hex("#6366F1");
|
|
1274
|
+
var muted = chalk.dim;
|
|
1275
|
+
var white = chalk.white.bold;
|
|
1276
|
+
var PB_LOGO = `
|
|
1277
|
+
${brand("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
1278
|
+
${brand("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")} ${white("intentdocs")}
|
|
1279
|
+
${brand("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")} ${muted("Specs \u2192 Code. Fast.")}
|
|
1280
|
+
${brand("\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}
|
|
1281
|
+
${brand("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551")} ${muted(`v${VERSION}`)}
|
|
1282
|
+
${brand("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D")}
|
|
1283
|
+
`;
|
|
1284
|
+
function printBanner(subtitle) {
|
|
1285
|
+
console.log(PB_LOGO);
|
|
1286
|
+
if (subtitle) {
|
|
1287
|
+
console.log(` ${white(subtitle)}`);
|
|
1288
|
+
console.log(` ${muted("\u2500".repeat(subtitle.length))}
|
|
1289
|
+
`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/commands/start.ts
|
|
1294
|
+
import ora from "ora";
|
|
1295
|
+
async function apiFetch2(path, token, opts = {}) {
|
|
1296
|
+
return fetch(`${API_BASE}${path}`, {
|
|
1297
|
+
...opts,
|
|
1298
|
+
headers: {
|
|
1299
|
+
"Content-Type": "application/json",
|
|
1300
|
+
Authorization: `Bearer ${token}`,
|
|
1301
|
+
...opts.headers ?? {}
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
async function streamResponse(path, token, body, options) {
|
|
1306
|
+
const res = await apiFetch2(path, token, {
|
|
1307
|
+
method: "POST",
|
|
1308
|
+
body: JSON.stringify(body)
|
|
1309
|
+
});
|
|
1310
|
+
if (!res.ok) {
|
|
1311
|
+
const err = await res.text();
|
|
1312
|
+
throw new Error(`API error ${res.status}: ${err}`);
|
|
1313
|
+
}
|
|
1314
|
+
let full = "";
|
|
1315
|
+
const reader = res.body?.getReader();
|
|
1316
|
+
const decoder = new TextDecoder();
|
|
1317
|
+
if (reader) {
|
|
1318
|
+
while (true) {
|
|
1319
|
+
const { done: done2, value } = await reader.read();
|
|
1320
|
+
if (done2) break;
|
|
1321
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1322
|
+
if (!options?.silent) {
|
|
1323
|
+
process.stdout.write(chunk);
|
|
1324
|
+
}
|
|
1325
|
+
full += chunk;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (!options?.silent) {
|
|
1329
|
+
console.log();
|
|
1330
|
+
}
|
|
1331
|
+
return full;
|
|
1332
|
+
}
|
|
1333
|
+
async function start() {
|
|
1334
|
+
const auth = requireAuth();
|
|
1335
|
+
const { token } = auth;
|
|
1336
|
+
printBanner("Start a new project");
|
|
1337
|
+
console.log(" Let's start with the problem you want to solve.\n");
|
|
1338
|
+
const problem = await ask(" What problem are you trying to solve?\n > ");
|
|
1339
|
+
if (!problem || problem.length < 10) {
|
|
1340
|
+
console.log("\n Please describe the problem in more detail (at least 10 characters).");
|
|
1341
|
+
closePrompt();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
pausePrompt();
|
|
1345
|
+
const spinner = ora(" Thinking about your idea...").start();
|
|
1346
|
+
let discoveryText;
|
|
1347
|
+
try {
|
|
1348
|
+
discoveryText = await streamResponse("/api/blueprint/discovery", token, {
|
|
1349
|
+
idea: problem
|
|
1350
|
+
}, { silent: true });
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
spinner.fail(" Failed to connect");
|
|
1353
|
+
console.error(` ${err.message}`);
|
|
1354
|
+
console.error(" Make sure you're logged in: intent login");
|
|
1355
|
+
closePrompt();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
spinner.succeed(" Got it \u2014 a few questions to sharpen the idea:\n");
|
|
1359
|
+
const questions = discoveryText.split(/\d+\.\s+/).filter((q) => q.trim().length > 10).map((q) => q.trim().replace(/\n/g, " "));
|
|
1360
|
+
const qa = [];
|
|
1361
|
+
for (const q of questions.slice(0, 3)) {
|
|
1362
|
+
console.log();
|
|
1363
|
+
const answer = await ask(` ${q}
|
|
1364
|
+
> `);
|
|
1365
|
+
if (answer) {
|
|
1366
|
+
qa.push({ question: q, answer });
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
pausePrompt();
|
|
1370
|
+
const pitchSpinner = ora(" Synthesising your pitch...").start();
|
|
1371
|
+
const pitchRes = await apiFetch2("/api/blueprint/synthesize-pitch", token, {
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
body: JSON.stringify({ problem, qa })
|
|
1374
|
+
});
|
|
1375
|
+
if (!pitchRes.ok) {
|
|
1376
|
+
pitchSpinner.fail(" Failed to synthesise pitch. Try again.");
|
|
1377
|
+
closePrompt();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const { pitch, target_user, names } = await pitchRes.json();
|
|
1381
|
+
pitchSpinner.succeed(" Pitch ready!\n");
|
|
1382
|
+
console.log(` ${pitch}
|
|
1383
|
+
`);
|
|
1384
|
+
console.log(` Target user: ${target_user}
|
|
1385
|
+
`);
|
|
1386
|
+
console.log(" Suggested names:");
|
|
1387
|
+
for (let i = 0; i < names.length; i++) {
|
|
1388
|
+
console.log(` ${i + 1}. ${names[i]}`);
|
|
1389
|
+
}
|
|
1390
|
+
console.log(` ${names.length + 1}. Type my own name`);
|
|
1391
|
+
const nameChoice = await ask(`
|
|
1392
|
+
Pick a name (1-${names.length + 1}): `);
|
|
1393
|
+
const nameIdx = parseInt(nameChoice, 10) - 1;
|
|
1394
|
+
let productName;
|
|
1395
|
+
if (nameIdx >= 0 && nameIdx < names.length) {
|
|
1396
|
+
productName = names[nameIdx];
|
|
1397
|
+
} else {
|
|
1398
|
+
productName = await ask(" Enter your product name: ");
|
|
1399
|
+
}
|
|
1400
|
+
if (!productName.trim()) {
|
|
1401
|
+
productName = names[0] ?? "My Product";
|
|
1402
|
+
}
|
|
1403
|
+
console.log(`
|
|
1404
|
+
Great \u2014 "${productName}" it is.
|
|
1405
|
+
`);
|
|
1406
|
+
console.log(" Creating project...");
|
|
1407
|
+
const createRes = await apiFetch2("/api/blueprint/create", token, {
|
|
1408
|
+
method: "POST",
|
|
1409
|
+
body: JSON.stringify({
|
|
1410
|
+
messages: [{ role: "user", content: problem }],
|
|
1411
|
+
titleOverride: productName,
|
|
1412
|
+
pitchOverride: pitch
|
|
1413
|
+
})
|
|
1414
|
+
});
|
|
1415
|
+
if (!createRes.ok) {
|
|
1416
|
+
const contentType = createRes.headers.get("content-type") ?? "";
|
|
1417
|
+
if (contentType.includes("application/json")) {
|
|
1418
|
+
const data = await createRes.json();
|
|
1419
|
+
if (data.error === "PROJECT_LIMIT_REACHED") {
|
|
1420
|
+
console.error("\n Project limit reached. Upgrade to Pro at intentdocs.com/pricing");
|
|
1421
|
+
} else {
|
|
1422
|
+
console.error(`
|
|
1423
|
+
Failed to create project: ${data.error ?? "unknown error"}`);
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
console.error("\n Failed to create project. Try logging in again: intent login");
|
|
1427
|
+
}
|
|
1428
|
+
closePrompt();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const createContentType = createRes.headers.get("content-type") ?? "";
|
|
1432
|
+
if (!createContentType.includes("application/json")) {
|
|
1433
|
+
console.error("\n Unexpected response from server. Try logging in again: intent login");
|
|
1434
|
+
closePrompt();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const { projectId } = await createRes.json();
|
|
1438
|
+
console.log(` \u2713 Project created (${projectId.slice(0, 8)})
|
|
1439
|
+
`);
|
|
1440
|
+
const validated = await confirm(
|
|
1441
|
+
` Have you validated there is demand for ${productName}?`
|
|
1442
|
+
);
|
|
1443
|
+
if (validated) {
|
|
1444
|
+
const howValidated = await ask(" How did you validate demand?\n > ");
|
|
1445
|
+
console.log(`
|
|
1446
|
+
Noted${howValidated ? ": " + howValidated : "."}
|
|
1447
|
+
`);
|
|
1448
|
+
} else {
|
|
1449
|
+
const wantPublish = await confirm(
|
|
1450
|
+
" Want to publish on intentdocs to get feedback and intent signals?"
|
|
1451
|
+
);
|
|
1452
|
+
if (wantPublish) {
|
|
1453
|
+
console.log(`
|
|
1454
|
+
Publishing "${productName}"...
|
|
1455
|
+
`);
|
|
1456
|
+
console.log(" \u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1457
|
+
console.log(` Name: ${productName}`);
|
|
1458
|
+
console.log(` Pitch: ${pitch}`);
|
|
1459
|
+
console.log(` Target: ${target_user}`);
|
|
1460
|
+
console.log(` Problem: ${problem.slice(0, 100)}${problem.length > 100 ? "..." : ""}`);
|
|
1461
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
1462
|
+
const confirmPublish = await confirm(" Publish this to Explore?");
|
|
1463
|
+
if (confirmPublish) {
|
|
1464
|
+
const pubRes = await apiFetch2(
|
|
1465
|
+
`/api/projects/${projectId}/publish`,
|
|
1466
|
+
token,
|
|
1467
|
+
{
|
|
1468
|
+
method: "POST",
|
|
1469
|
+
body: JSON.stringify({
|
|
1470
|
+
is_published: true,
|
|
1471
|
+
tagline: pitch,
|
|
1472
|
+
problem_statement: problem
|
|
1473
|
+
})
|
|
1474
|
+
}
|
|
1475
|
+
);
|
|
1476
|
+
if (pubRes.ok) {
|
|
1477
|
+
const pubContentType = pubRes.headers.get("content-type") ?? "";
|
|
1478
|
+
if (pubContentType.includes("application/json")) {
|
|
1479
|
+
const pubData = await pubRes.json();
|
|
1480
|
+
console.log(` \u2713 Published! View at: ${API_BASE}/p/${pubData.public_slug ?? projectId}
|
|
1481
|
+
`);
|
|
1482
|
+
} else {
|
|
1483
|
+
console.log(` \u2713 Published!
|
|
1484
|
+
`);
|
|
1485
|
+
}
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log(" \u26A0 Publishing failed \u2014 you can publish later from the UI.\n");
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const readyToScope = await confirm(
|
|
1493
|
+
` Ready to start scoping out ${productName}?`
|
|
1494
|
+
);
|
|
1495
|
+
if (!readyToScope) {
|
|
1496
|
+
console.log(`
|
|
1497
|
+
No worries! When you're ready:
|
|
1498
|
+
\u2022 Run: intent scope ${projectId}
|
|
1499
|
+
\u2022 Or visit: ${API_BASE}
|
|
1500
|
+
|
|
1501
|
+
Your project is saved and waiting for you.
|
|
1502
|
+
`);
|
|
1503
|
+
closePrompt();
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const { scope: scope2 } = await import("./scope-OGKQ472D.js");
|
|
1507
|
+
await scope2(projectId);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/commands/propose.ts
|
|
1511
|
+
import { mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1512
|
+
import { join as join4 } from "path";
|
|
1513
|
+
async function propose(changeName, opts = {}) {
|
|
1514
|
+
requireAuth();
|
|
1515
|
+
if (!hasConfig()) {
|
|
1516
|
+
console.log(" Not connected to a project. Run `intent init` first.");
|
|
1517
|
+
closePrompt();
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const config = requireConfig();
|
|
1521
|
+
if (opts.from) {
|
|
1522
|
+
await proposeFromFile(opts.from, config, changeName);
|
|
1523
|
+
closePrompt();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
console.log("\n \u{1F4CB} New change proposal\n");
|
|
1527
|
+
console.log(" This will create stories in your story map and save a local spec.\n");
|
|
1528
|
+
if (!changeName?.trim()) {
|
|
1529
|
+
changeName = await ask(" Change name (kebab-case, e.g. add-dark-mode): ");
|
|
1530
|
+
}
|
|
1531
|
+
const slug = (changeName ?? "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
1532
|
+
if (!slug) {
|
|
1533
|
+
console.log(" Change name required.");
|
|
1534
|
+
closePrompt();
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const problem = await ask(
|
|
1538
|
+
"\n What problem does this solve?\n > "
|
|
1539
|
+
);
|
|
1540
|
+
const solution = await ask(
|
|
1541
|
+
"\n What are you proposing to build?\n > "
|
|
1542
|
+
);
|
|
1543
|
+
const activity = await ask(
|
|
1544
|
+
"\n Which activity does this belong to? (e.g. 'Onboarding', 'Core Flow')\n > "
|
|
1545
|
+
);
|
|
1546
|
+
const priorityInput = await ask(
|
|
1547
|
+
"\n Priority \u2014 must / should / could [should]: "
|
|
1548
|
+
);
|
|
1549
|
+
const priority = ["must", "should", "could"].includes(priorityInput.trim()) ? priorityInput.trim() : "should";
|
|
1550
|
+
console.log("\n Now add stories for this change. Enter an empty title when done.\n");
|
|
1551
|
+
const stories2 = [];
|
|
1552
|
+
let storyNum = 1;
|
|
1553
|
+
while (true) {
|
|
1554
|
+
const title = await ask(` Story ${storyNum} title (or press Enter to finish): `);
|
|
1555
|
+
if (!title.trim()) break;
|
|
1556
|
+
const effortInput = await ask(` Effort for "${title}" \u2014 S / M / L / XL [M]: `);
|
|
1557
|
+
const effort = ["S", "M", "L", "XL"].includes(effortInput.trim().toUpperCase()) ? effortInput.trim().toUpperCase() : "M";
|
|
1558
|
+
console.log(` Acceptance criteria for "${title}" (one per line, empty to finish):`);
|
|
1559
|
+
const acs = [];
|
|
1560
|
+
while (true) {
|
|
1561
|
+
const ac = await ask(` AC ${acs.length + 1}: `);
|
|
1562
|
+
if (!ac.trim()) break;
|
|
1563
|
+
acs.push(ac.trim());
|
|
1564
|
+
}
|
|
1565
|
+
stories2.push({ title: title.trim(), activity: activity.trim() || "General", priority, effort, acceptance_criteria: acs });
|
|
1566
|
+
storyNum++;
|
|
1567
|
+
}
|
|
1568
|
+
if (stories2.length === 0) {
|
|
1569
|
+
console.log("\n No stories added. Proposal cancelled.");
|
|
1570
|
+
closePrompt();
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
console.log(`
|
|
1574
|
+
Ready to create ${stories2.length} story/stories in your story map:
|
|
1575
|
+
`);
|
|
1576
|
+
for (const s of stories2) {
|
|
1577
|
+
console.log(` [${s.priority.toUpperCase()}] ${s.title} (${s.effort})`);
|
|
1578
|
+
for (const ac of s.acceptance_criteria) {
|
|
1579
|
+
console.log(` \u2022 ${ac}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const ok = await confirm("\n Create these stories and save local proposal?");
|
|
1583
|
+
if (!ok) {
|
|
1584
|
+
console.log(" Cancelled.");
|
|
1585
|
+
closePrompt();
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
let created = [];
|
|
1589
|
+
try {
|
|
1590
|
+
const result = await createStories(config, stories2.map((s) => ({
|
|
1591
|
+
title: s.title,
|
|
1592
|
+
activity: s.activity,
|
|
1593
|
+
priority: s.priority,
|
|
1594
|
+
effort: s.effort,
|
|
1595
|
+
acceptance_criteria: s.acceptance_criteria
|
|
1596
|
+
})));
|
|
1597
|
+
created = result.stories;
|
|
1598
|
+
console.log(`
|
|
1599
|
+
\u2713 Created ${result.created} stories in your story map`);
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
console.error(`
|
|
1602
|
+
\u2717 Failed to create stories: ${err instanceof Error ? err.message : String(err)}`);
|
|
1603
|
+
console.log(" Saving proposal locally anyway...");
|
|
1604
|
+
}
|
|
1605
|
+
const changesDir = join4(process.cwd(), "intent-changes", slug);
|
|
1606
|
+
if (!existsSync4(changesDir)) mkdirSync(changesDir, { recursive: true });
|
|
1607
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1608
|
+
const proposalMd = [
|
|
1609
|
+
`# Proposal: ${changeName}`,
|
|
1610
|
+
``,
|
|
1611
|
+
`**Date:** ${date}`,
|
|
1612
|
+
`**Status:** proposed`,
|
|
1613
|
+
`**Priority:** ${priority}`,
|
|
1614
|
+
``,
|
|
1615
|
+
`## Problem`,
|
|
1616
|
+
``,
|
|
1617
|
+
problem,
|
|
1618
|
+
``,
|
|
1619
|
+
`## Solution`,
|
|
1620
|
+
``,
|
|
1621
|
+
solution,
|
|
1622
|
+
``,
|
|
1623
|
+
`## Stories`,
|
|
1624
|
+
``,
|
|
1625
|
+
...stories2.map((s, i) => {
|
|
1626
|
+
const storyId = created[i]?.id ?? null;
|
|
1627
|
+
const idNote = storyId ? ` <!-- INTENT-${storyId} -->` : "";
|
|
1628
|
+
const lines = [`### ${s.title}${idNote}`, ``, `**Activity:** ${s.activity} | **Priority:** ${s.priority} | **Effort:** ${s.effort}`, ``];
|
|
1629
|
+
if (s.acceptance_criteria.length) {
|
|
1630
|
+
lines.push(`**Acceptance criteria:**`, ``);
|
|
1631
|
+
for (const ac of s.acceptance_criteria) lines.push(`- [ ] ${ac}`);
|
|
1632
|
+
lines.push(``);
|
|
1633
|
+
}
|
|
1634
|
+
return lines.join("\n");
|
|
1635
|
+
}),
|
|
1636
|
+
`## Implementation notes`,
|
|
1637
|
+
``,
|
|
1638
|
+
`_Add notes here as you build._`,
|
|
1639
|
+
``
|
|
1640
|
+
].join("\n");
|
|
1641
|
+
writeFileSync3(join4(changesDir, "proposal.md"), proposalMd);
|
|
1642
|
+
console.log(` \u2713 Saved intent-changes/${slug}/proposal.md`);
|
|
1643
|
+
const gitignorePath = join4(process.cwd(), ".gitignore");
|
|
1644
|
+
if (existsSync4(gitignorePath)) {
|
|
1645
|
+
const { readFileSync: readFileSync5, appendFileSync: appendFileSync2 } = await import("fs");
|
|
1646
|
+
const gi = readFileSync5(gitignorePath, "utf-8");
|
|
1647
|
+
if (!gi.includes("intent-changes")) {
|
|
1648
|
+
appendFileSync2(gitignorePath, "\n# intentdocs local change proposals\nstc-changes/\n");
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
console.log(`
|
|
1652
|
+
\u2705 Proposal "${changeName}" ready!
|
|
1653
|
+
|
|
1654
|
+
Stories created in your story map \u2014 view them at intentdocs.com
|
|
1655
|
+
Local spec saved at: intent-changes/${slug}/proposal.md
|
|
1656
|
+
|
|
1657
|
+
Next:
|
|
1658
|
+
intent next \u2014 pick up the first story
|
|
1659
|
+
intent status \u2014 see full progress
|
|
1660
|
+
intent sync \u2014 refresh INTENT.md
|
|
1661
|
+
`);
|
|
1662
|
+
closePrompt();
|
|
1663
|
+
}
|
|
1664
|
+
function parseProposalFile(filePath) {
|
|
1665
|
+
const raw = readFileSync4(filePath, "utf-8");
|
|
1666
|
+
const lines = raw.split("\n");
|
|
1667
|
+
let changeName = "";
|
|
1668
|
+
let problem = "";
|
|
1669
|
+
let solution = "";
|
|
1670
|
+
let activity = "General";
|
|
1671
|
+
let priority = "should";
|
|
1672
|
+
const stories2 = [];
|
|
1673
|
+
let currentStory = null;
|
|
1674
|
+
for (const raw2 of lines) {
|
|
1675
|
+
const line = raw2.trim();
|
|
1676
|
+
if (line.startsWith("# ")) {
|
|
1677
|
+
changeName = line.replace(/^#\s+/, "").replace(/^Proposal:\s*/i, "").trim();
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
if (line.startsWith("## ")) {
|
|
1681
|
+
if (currentStory) stories2.push(currentStory);
|
|
1682
|
+
currentStory = {
|
|
1683
|
+
title: line.replace(/^##\s+/, "").trim(),
|
|
1684
|
+
effort: "M",
|
|
1685
|
+
acceptance_criteria: [],
|
|
1686
|
+
activity,
|
|
1687
|
+
priority
|
|
1688
|
+
};
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
if (currentStory) {
|
|
1692
|
+
const effortMatch = line.match(/^\*\*Effort:\*\*\s*([SMLXLxsml]+)/i);
|
|
1693
|
+
if (effortMatch) {
|
|
1694
|
+
const e = effortMatch[1].toUpperCase();
|
|
1695
|
+
if (["S", "M", "L", "XL"].includes(e)) currentStory.effort = e;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const actMatch = line.match(/^\*\*Activity:\*\*\s*(.+)/i);
|
|
1699
|
+
if (actMatch) {
|
|
1700
|
+
currentStory.activity = actMatch[1].trim();
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
const priMatch = line.match(/^\*\*Priority:\*\*\s*(must|should|could)/i);
|
|
1704
|
+
if (priMatch) {
|
|
1705
|
+
currentStory.priority = priMatch[1].toLowerCase();
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
if (line.startsWith("- ")) {
|
|
1709
|
+
currentStory.acceptance_criteria.push(line.slice(2).replace(/^\[ \]\s*/, "").trim());
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
} else {
|
|
1713
|
+
const probMatch = line.match(/^\*\*Problem:\*\*\s*(.+)/i);
|
|
1714
|
+
if (probMatch) {
|
|
1715
|
+
problem = probMatch[1].trim();
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
const solMatch = line.match(/^\*\*Solution:\*\*\s*(.+)/i);
|
|
1719
|
+
if (solMatch) {
|
|
1720
|
+
solution = solMatch[1].trim();
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
const actMatch = line.match(/^\*\*Activity:\*\*\s*(.+)/i);
|
|
1724
|
+
if (actMatch) {
|
|
1725
|
+
activity = actMatch[1].trim();
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const priMatch = line.match(/^\*\*Priority:\*\*\s*(must|should|could)/i);
|
|
1729
|
+
if (priMatch) {
|
|
1730
|
+
priority = priMatch[1].toLowerCase();
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (currentStory) stories2.push(currentStory);
|
|
1736
|
+
return { changeName, problem, solution, activity, priority, stories: stories2 };
|
|
1737
|
+
}
|
|
1738
|
+
async function proposeFromFile(filePath, config, nameOverride) {
|
|
1739
|
+
if (!existsSync4(filePath)) {
|
|
1740
|
+
console.error(`
|
|
1741
|
+
File not found: ${filePath}
|
|
1742
|
+
`);
|
|
1743
|
+
process.exit(1);
|
|
1744
|
+
}
|
|
1745
|
+
const parsed = parseProposalFile(filePath);
|
|
1746
|
+
const changeName = nameOverride?.trim() || parsed.changeName || filePath.replace(/.*\//, "").replace(/\.md$/, "");
|
|
1747
|
+
const slug = changeName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
1748
|
+
if (parsed.stories.length === 0) {
|
|
1749
|
+
console.error("\n No stories found in the file. Add ## headings for each story.\n");
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
console.log(`
|
|
1753
|
+
\u{1F4CB} Proposing "${changeName}" from ${filePath}
|
|
1754
|
+
`);
|
|
1755
|
+
for (const s of parsed.stories) {
|
|
1756
|
+
console.log(` [${s.priority.toUpperCase()}] ${s.title} (${s.effort}) \u2014 ${s.acceptance_criteria.length} ACs`);
|
|
1757
|
+
}
|
|
1758
|
+
let created = [];
|
|
1759
|
+
try {
|
|
1760
|
+
const result = await createStories(config, parsed.stories.map((s) => ({
|
|
1761
|
+
title: s.title,
|
|
1762
|
+
activity: s.activity,
|
|
1763
|
+
priority: s.priority,
|
|
1764
|
+
effort: s.effort,
|
|
1765
|
+
acceptance_criteria: s.acceptance_criteria
|
|
1766
|
+
})));
|
|
1767
|
+
created = result.stories;
|
|
1768
|
+
console.log(`
|
|
1769
|
+
\u2713 Created ${result.created} stories in your story map`);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
console.error(`
|
|
1772
|
+
\u2717 Failed to create stories: ${err instanceof Error ? err.message : String(err)}`);
|
|
1773
|
+
console.log(" Saving proposal locally anyway...");
|
|
1774
|
+
}
|
|
1775
|
+
const changesDir = join4(process.cwd(), "intent-changes", slug);
|
|
1776
|
+
if (!existsSync4(changesDir)) mkdirSync(changesDir, { recursive: true });
|
|
1777
|
+
const proposalMd = [
|
|
1778
|
+
`# Proposal: ${changeName}`,
|
|
1779
|
+
``,
|
|
1780
|
+
`**Date:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
1781
|
+
`**Source:** ${filePath}`,
|
|
1782
|
+
`**Status:** proposed`,
|
|
1783
|
+
``,
|
|
1784
|
+
`## Problem`,
|
|
1785
|
+
``,
|
|
1786
|
+
parsed.problem || "_See source file_",
|
|
1787
|
+
``,
|
|
1788
|
+
`## Solution`,
|
|
1789
|
+
``,
|
|
1790
|
+
parsed.solution || "_See source file_",
|
|
1791
|
+
``,
|
|
1792
|
+
`## Stories`,
|
|
1793
|
+
``,
|
|
1794
|
+
...parsed.stories.map((s, i) => {
|
|
1795
|
+
const storyId = created[i]?.id ?? null;
|
|
1796
|
+
const idNote = storyId ? ` <!-- INTENT-${storyId} -->` : "";
|
|
1797
|
+
const acLines = s.acceptance_criteria.map((ac) => `- [ ] ${ac}`);
|
|
1798
|
+
return [
|
|
1799
|
+
`### ${s.title}${idNote}`,
|
|
1800
|
+
``,
|
|
1801
|
+
`**Activity:** ${s.activity} | **Priority:** ${s.priority} | **Effort:** ${s.effort}`,
|
|
1802
|
+
``,
|
|
1803
|
+
...acLines.length ? [`**Acceptance criteria:**`, ``, ...acLines, ``] : []
|
|
1804
|
+
].join("\n");
|
|
1805
|
+
})
|
|
1806
|
+
].join("\n");
|
|
1807
|
+
writeFileSync3(join4(changesDir, "proposal.md"), proposalMd);
|
|
1808
|
+
console.log(` \u2713 Saved intent-changes/${slug}/proposal.md`);
|
|
1809
|
+
console.log(`
|
|
1810
|
+
\u2705 Done! Run \`intent next\` to start building.
|
|
1811
|
+
`);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// src/commands/show.ts
|
|
1815
|
+
var STATUS_ICON2 = {
|
|
1816
|
+
done: "\u2705",
|
|
1817
|
+
in_progress: "\u{1F528}",
|
|
1818
|
+
todo: "\u2B1C"
|
|
1819
|
+
};
|
|
1820
|
+
async function show(idPrefix) {
|
|
1821
|
+
const config = requireConfig();
|
|
1822
|
+
const data = await listStories(config);
|
|
1823
|
+
const story = data.stories.find((s) => s.id.startsWith(idPrefix));
|
|
1824
|
+
if (!story) {
|
|
1825
|
+
console.error(`
|
|
1826
|
+
No story found matching "${idPrefix}"
|
|
1827
|
+
Run \`intent stories\` to list IDs.
|
|
1828
|
+
`);
|
|
1829
|
+
process.exit(1);
|
|
1830
|
+
}
|
|
1831
|
+
const statusIcon = STATUS_ICON2[story.status ?? "todo"] ?? "\u2B1C";
|
|
1832
|
+
const specUrl = `${config.apiBase}/storymapper/${data.map.id}?story=${story.id}`;
|
|
1833
|
+
console.log(`
|
|
1834
|
+
${statusIcon} ${story.title}`);
|
|
1835
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1836
|
+
console.log(` Activity : ${story.activity}`);
|
|
1837
|
+
console.log(` Type : ${story.story_type ?? "user_story"}`);
|
|
1838
|
+
console.log(` Priority : ${story.priority} | Effort : ${story.effort} | Release : ${story.release}`);
|
|
1839
|
+
console.log(` ID : ${story.id}`);
|
|
1840
|
+
console.log(` Spec : ${specUrl}`);
|
|
1841
|
+
if (story.as_a || story.i_want || story.so_that) {
|
|
1842
|
+
console.log(`
|
|
1843
|
+
User story`);
|
|
1844
|
+
if (story.as_a) console.log(` As a : ${story.as_a}`);
|
|
1845
|
+
if (story.i_want) console.log(` I want : ${story.i_want}`);
|
|
1846
|
+
if (story.so_that) console.log(` So that : ${story.so_that}`);
|
|
1847
|
+
}
|
|
1848
|
+
if (story.acceptance_criteria.length > 0) {
|
|
1849
|
+
console.log(`
|
|
1850
|
+
Done when:`);
|
|
1851
|
+
for (const ac of story.acceptance_criteria) {
|
|
1852
|
+
const check2 = ac.done ? "\u2611" : "\u2610";
|
|
1853
|
+
console.log(` ${check2} ${ac.text}`);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (story.notes) {
|
|
1857
|
+
console.log(`
|
|
1858
|
+
Notes:`);
|
|
1859
|
+
for (const line of story.notes.split("\n")) {
|
|
1860
|
+
console.log(` ${line}`);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (story.evidence && story.evidence.length > 0) {
|
|
1864
|
+
console.log(`
|
|
1865
|
+
Evidence (${story.evidence.length}):`);
|
|
1866
|
+
for (const ev of story.evidence) {
|
|
1867
|
+
console.log(` [${ev.type}] ${ev.content.slice(0, 80)}${ev.content.length > 80 ? "\u2026" : ""}${ev.source ? ` \u2014 ${ev.source}` : ""}`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const contextPath = writeStoryContext(story);
|
|
1871
|
+
const relPath = contextPath.replace(process.cwd() + "/", "");
|
|
1872
|
+
console.log(`
|
|
1873
|
+
Context : ${relPath}`);
|
|
1874
|
+
console.log(` Mark done: intent done ${story.id.slice(0, 8)}
|
|
1875
|
+
`);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/commands/example.ts
|
|
1879
|
+
import * as readline from "readline/promises";
|
|
1880
|
+
import { stdin as input, stdout as output } from "process";
|
|
1881
|
+
async function example() {
|
|
1882
|
+
const config = requireConfig();
|
|
1883
|
+
const rl = readline.createInterface({ input, output });
|
|
1884
|
+
console.log("\n Add a reference pattern (few-shot example)\n");
|
|
1885
|
+
try {
|
|
1886
|
+
const title = (await rl.question(" Title: ")).trim();
|
|
1887
|
+
if (!title) {
|
|
1888
|
+
console.error(" \u2717 Title is required.");
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
const description = (await rl.question(" Description (optional): ")).trim();
|
|
1892
|
+
const LANGUAGES = ["typescript", "javascript", "python", "bash", "sql", "other"];
|
|
1893
|
+
console.log(`
|
|
1894
|
+
Language options: ${LANGUAGES.join(", ")}`);
|
|
1895
|
+
const langInput = (await rl.question(" Language [typescript]: ")).trim().toLowerCase();
|
|
1896
|
+
const language = LANGUAGES.includes(langInput) ? langInput : "typescript";
|
|
1897
|
+
console.log("\n Before code (paste, then press Enter twice to finish):");
|
|
1898
|
+
const beforeCode = await readMultiline(rl);
|
|
1899
|
+
console.log("\n After code (paste, then press Enter twice to finish):");
|
|
1900
|
+
const afterCode = await readMultiline(rl);
|
|
1901
|
+
rl.close();
|
|
1902
|
+
const res = await apiFetch(config, "/examples", {
|
|
1903
|
+
method: "POST",
|
|
1904
|
+
headers: { "Content-Type": "application/json" },
|
|
1905
|
+
body: JSON.stringify({
|
|
1906
|
+
title,
|
|
1907
|
+
description: description || void 0,
|
|
1908
|
+
language,
|
|
1909
|
+
before_code: beforeCode || void 0,
|
|
1910
|
+
after_code: afterCode || void 0
|
|
1911
|
+
})
|
|
1912
|
+
});
|
|
1913
|
+
if (!res.ok) {
|
|
1914
|
+
const body = await res.text();
|
|
1915
|
+
console.error(`
|
|
1916
|
+
\u2717 Failed to save example (${res.status}): ${body}`);
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
const data = await res.json();
|
|
1920
|
+
console.log(`
|
|
1921
|
+
\u2713 Example saved: "${data.title}"
|
|
1922
|
+
`);
|
|
1923
|
+
console.log(" Run `intent sync --context` to regenerate your CLAUDE.md with this pattern.\n");
|
|
1924
|
+
} catch {
|
|
1925
|
+
rl.close();
|
|
1926
|
+
throw new Error("Aborted");
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function readMultiline(rl) {
|
|
1930
|
+
const lines = [];
|
|
1931
|
+
let emptyCount = 0;
|
|
1932
|
+
while (emptyCount < 1) {
|
|
1933
|
+
const line = await rl.question("");
|
|
1934
|
+
if (line === "") {
|
|
1935
|
+
emptyCount++;
|
|
1936
|
+
} else {
|
|
1937
|
+
emptyCount = 0;
|
|
1938
|
+
lines.push(line);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return lines.join("\n");
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/templates/index.ts
|
|
1945
|
+
var templates = {
|
|
1946
|
+
saas: {
|
|
1947
|
+
name: "SaaS Starter",
|
|
1948
|
+
description: "Multi-tenant SaaS with auth, billing, and dashboards",
|
|
1949
|
+
personas: [
|
|
1950
|
+
{ name: "Admin", role: "Account administrator who manages the team" },
|
|
1951
|
+
{ name: "User", role: "End user who uses the core product daily" }
|
|
1952
|
+
],
|
|
1953
|
+
activities: [
|
|
1954
|
+
{ title: "Sign up & Onboard", steps: ["Registration", "Team setup", "First-run experience"] },
|
|
1955
|
+
{ title: "Core Workflow", steps: ["Dashboard", "Main feature", "Data management"] },
|
|
1956
|
+
{ title: "Team Management", steps: ["Invite members", "Roles & permissions"] },
|
|
1957
|
+
{ title: "Billing & Plans", steps: ["Plan selection", "Checkout", "Usage tracking"] },
|
|
1958
|
+
{ title: "Settings & Account", steps: ["Profile", "Notifications", "Integrations"] }
|
|
1959
|
+
],
|
|
1960
|
+
stories: [
|
|
1961
|
+
{ title: "Sign up with email or OAuth", activity: "Sign up & Onboard", step: "Registration", priority: "must", effort: "M", release: 1 },
|
|
1962
|
+
{ title: "Email verification flow", activity: "Sign up & Onboard", step: "Registration", priority: "must", effort: "S", release: 1 },
|
|
1963
|
+
{ title: "Create workspace / tenant", activity: "Sign up & Onboard", step: "Team setup", priority: "must", effort: "M", release: 1 },
|
|
1964
|
+
{ title: "Onboarding wizard (3 steps)", activity: "Sign up & Onboard", step: "First-run experience", priority: "should", effort: "M", release: 1 },
|
|
1965
|
+
{ title: "Main dashboard with key metrics", activity: "Core Workflow", step: "Dashboard", priority: "must", effort: "L", release: 1 },
|
|
1966
|
+
{ title: "CRUD for primary resource", activity: "Core Workflow", step: "Main feature", priority: "must", effort: "L", release: 1 },
|
|
1967
|
+
{ title: "Search and filter", activity: "Core Workflow", step: "Data management", priority: "must", effort: "M", release: 1 },
|
|
1968
|
+
{ title: "Export data as CSV", activity: "Core Workflow", step: "Data management", priority: "should", effort: "S", release: 2 },
|
|
1969
|
+
{ title: "Invite team members by email", activity: "Team Management", step: "Invite members", priority: "must", effort: "M", release: 1 },
|
|
1970
|
+
{ title: "Role-based access control", activity: "Team Management", step: "Roles & permissions", priority: "must", effort: "L", release: 1 },
|
|
1971
|
+
{ title: "Stripe subscription checkout", activity: "Billing & Plans", step: "Checkout", priority: "must", effort: "L", release: 1 },
|
|
1972
|
+
{ title: "Free trial with upgrade prompt", activity: "Billing & Plans", step: "Plan selection", priority: "should", effort: "M", release: 1 },
|
|
1973
|
+
{ title: "Usage-based billing meter", activity: "Billing & Plans", step: "Usage tracking", priority: "could", effort: "L", release: 3 },
|
|
1974
|
+
{ title: "Edit profile and avatar", activity: "Settings & Account", step: "Profile", priority: "must", effort: "S", release: 1 },
|
|
1975
|
+
{ title: "Email notification preferences", activity: "Settings & Account", step: "Notifications", priority: "should", effort: "M", release: 2 },
|
|
1976
|
+
{ title: "Webhook integrations", activity: "Settings & Account", step: "Integrations", priority: "could", effort: "L", release: 3 }
|
|
1977
|
+
]
|
|
1978
|
+
},
|
|
1979
|
+
marketplace: {
|
|
1980
|
+
name: "Marketplace",
|
|
1981
|
+
description: "Two-sided marketplace connecting buyers and sellers",
|
|
1982
|
+
personas: [
|
|
1983
|
+
{ name: "Buyer", role: "Person searching for and purchasing services/products" },
|
|
1984
|
+
{ name: "Seller", role: "Person listing and selling services/products" }
|
|
1985
|
+
],
|
|
1986
|
+
activities: [
|
|
1987
|
+
{ title: "Discover", steps: ["Browse listings", "Search & filter", "View details"] },
|
|
1988
|
+
{ title: "List & Sell", steps: ["Create listing", "Manage inventory", "Pricing"] },
|
|
1989
|
+
{ title: "Transact", steps: ["Add to cart", "Checkout", "Payment processing"] },
|
|
1990
|
+
{ title: "Communicate", steps: ["Messaging", "Reviews & ratings"] },
|
|
1991
|
+
{ title: "Manage Account", steps: ["Profile", "Order history", "Payouts"] }
|
|
1992
|
+
],
|
|
1993
|
+
stories: [
|
|
1994
|
+
{ title: "Browse listings with categories", activity: "Discover", step: "Browse listings", priority: "must", effort: "L", release: 1 },
|
|
1995
|
+
{ title: "Full-text search with filters", activity: "Discover", step: "Search & filter", priority: "must", effort: "L", release: 1 },
|
|
1996
|
+
{ title: "Listing detail page with images", activity: "Discover", step: "View details", priority: "must", effort: "M", release: 1 },
|
|
1997
|
+
{ title: "Create listing with photos and description", activity: "List & Sell", step: "Create listing", priority: "must", effort: "L", release: 1 },
|
|
1998
|
+
{ title: "Set pricing and availability", activity: "List & Sell", step: "Pricing", priority: "must", effort: "M", release: 1 },
|
|
1999
|
+
{ title: "Manage active listings", activity: "List & Sell", step: "Manage inventory", priority: "must", effort: "M", release: 1 },
|
|
2000
|
+
{ title: "Shopping cart", activity: "Transact", step: "Add to cart", priority: "must", effort: "M", release: 1 },
|
|
2001
|
+
{ title: "Stripe checkout flow", activity: "Transact", step: "Checkout", priority: "must", effort: "L", release: 1 },
|
|
2002
|
+
{ title: "Split payments (platform fee + seller payout)", activity: "Transact", step: "Payment processing", priority: "must", effort: "XL", release: 1 },
|
|
2003
|
+
{ title: "Buyer-seller messaging", activity: "Communicate", step: "Messaging", priority: "should", effort: "L", release: 2 },
|
|
2004
|
+
{ title: "Leave reviews and ratings", activity: "Communicate", step: "Reviews & ratings", priority: "should", effort: "M", release: 2 },
|
|
2005
|
+
{ title: "Seller payout dashboard", activity: "Manage Account", step: "Payouts", priority: "must", effort: "L", release: 1 },
|
|
2006
|
+
{ title: "Order history for buyers", activity: "Manage Account", step: "Order history", priority: "must", effort: "M", release: 1 }
|
|
2007
|
+
]
|
|
2008
|
+
},
|
|
2009
|
+
"mobile-app": {
|
|
2010
|
+
name: "Mobile App",
|
|
2011
|
+
description: "Consumer mobile app with auth, feed, and notifications",
|
|
2012
|
+
personas: [
|
|
2013
|
+
{ name: "New User", role: "First-time user downloading the app" },
|
|
2014
|
+
{ name: "Power User", role: "Daily active user who has customized their experience" }
|
|
2015
|
+
],
|
|
2016
|
+
activities: [
|
|
2017
|
+
{ title: "Onboard", steps: ["App store \u2192 first open", "Sign up", "Permissions"] },
|
|
2018
|
+
{ title: "Core Experience", steps: ["Feed / home", "Create content", "Interact"] },
|
|
2019
|
+
{ title: "Profile & Social", steps: ["View profile", "Follow / connect", "Notifications"] },
|
|
2020
|
+
{ title: "Settings", steps: ["Preferences", "Privacy", "Account"] }
|
|
2021
|
+
],
|
|
2022
|
+
stories: [
|
|
2023
|
+
{ title: "App store listing and screenshots", activity: "Onboard", step: "App store \u2192 first open", priority: "must", effort: "M", release: 1 },
|
|
2024
|
+
{ title: "Social login (Apple, Google)", activity: "Onboard", step: "Sign up", priority: "must", effort: "M", release: 1 },
|
|
2025
|
+
{ title: "Push notification permission prompt", activity: "Onboard", step: "Permissions", priority: "must", effort: "S", release: 1 },
|
|
2026
|
+
{ title: "Main feed with pull-to-refresh", activity: "Core Experience", step: "Feed / home", priority: "must", effort: "L", release: 1 },
|
|
2027
|
+
{ title: "Create and publish content", activity: "Core Experience", step: "Create content", priority: "must", effort: "L", release: 1 },
|
|
2028
|
+
{ title: "Like, comment, share", activity: "Core Experience", step: "Interact", priority: "must", effort: "M", release: 1 },
|
|
2029
|
+
{ title: "User profile page", activity: "Profile & Social", step: "View profile", priority: "must", effort: "M", release: 1 },
|
|
2030
|
+
{ title: "Follow/unfollow users", activity: "Profile & Social", step: "Follow / connect", priority: "must", effort: "M", release: 1 },
|
|
2031
|
+
{ title: "Push notifications for interactions", activity: "Profile & Social", step: "Notifications", priority: "should", effort: "L", release: 2 },
|
|
2032
|
+
{ title: "Dark mode toggle", activity: "Settings", step: "Preferences", priority: "should", effort: "S", release: 2 },
|
|
2033
|
+
{ title: "Block and report users", activity: "Settings", step: "Privacy", priority: "must", effort: "M", release: 1 },
|
|
2034
|
+
{ title: "Delete account", activity: "Settings", step: "Account", priority: "must", effort: "S", release: 1 }
|
|
2035
|
+
]
|
|
2036
|
+
},
|
|
2037
|
+
"ai-tool": {
|
|
2038
|
+
name: "AI Tool",
|
|
2039
|
+
description: "AI-powered tool with prompt input, generation, and history",
|
|
2040
|
+
personas: [
|
|
2041
|
+
{ name: "Creator", role: "Professional using AI to augment their workflow" }
|
|
2042
|
+
],
|
|
2043
|
+
activities: [
|
|
2044
|
+
{ title: "Input & Configure", steps: ["Describe task", "Select model / options", "Upload context"] },
|
|
2045
|
+
{ title: "Generate", steps: ["Streaming output", "Edit & refine", "Regenerate"] },
|
|
2046
|
+
{ title: "Save & Share", steps: ["History", "Export", "Share link"] },
|
|
2047
|
+
{ title: "Account & Billing", steps: ["Usage dashboard", "Plan management", "API keys"] }
|
|
2048
|
+
],
|
|
2049
|
+
stories: [
|
|
2050
|
+
{ title: "Natural language input with prompt templates", activity: "Input & Configure", step: "Describe task", priority: "must", effort: "M", release: 1 },
|
|
2051
|
+
{ title: "Model / style selector", activity: "Input & Configure", step: "Select model / options", priority: "should", effort: "M", release: 1 },
|
|
2052
|
+
{ title: "File upload for context (PDF, images)", activity: "Input & Configure", step: "Upload context", priority: "should", effort: "L", release: 2 },
|
|
2053
|
+
{ title: "Streaming AI response with progress", activity: "Generate", step: "Streaming output", priority: "must", effort: "L", release: 1 },
|
|
2054
|
+
{ title: "Inline editing of generated output", activity: "Generate", step: "Edit & refine", priority: "must", effort: "M", release: 1 },
|
|
2055
|
+
{ title: "Regenerate with tweaked parameters", activity: "Generate", step: "Regenerate", priority: "should", effort: "S", release: 1 },
|
|
2056
|
+
{ title: "Generation history with search", activity: "Save & Share", step: "History", priority: "must", effort: "M", release: 1 },
|
|
2057
|
+
{ title: "Export as markdown / PDF", activity: "Save & Share", step: "Export", priority: "should", effort: "M", release: 2 },
|
|
2058
|
+
{ title: "Shareable public link", activity: "Save & Share", step: "Share link", priority: "could", effort: "M", release: 2 },
|
|
2059
|
+
{ title: "Token usage dashboard", activity: "Account & Billing", step: "Usage dashboard", priority: "must", effort: "M", release: 1 },
|
|
2060
|
+
{ title: "Stripe subscription with usage limits", activity: "Account & Billing", step: "Plan management", priority: "must", effort: "L", release: 1 },
|
|
2061
|
+
{ title: "API key management for developers", activity: "Account & Billing", step: "API keys", priority: "could", effort: "M", release: 3 }
|
|
2062
|
+
]
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/mcp/server.ts
|
|
2067
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2068
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2069
|
+
import {
|
|
2070
|
+
CallToolRequestSchema,
|
|
2071
|
+
ListToolsRequestSchema,
|
|
2072
|
+
ListResourcesRequestSchema,
|
|
2073
|
+
ReadResourceRequestSchema
|
|
2074
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2075
|
+
var TOOLS = [
|
|
2076
|
+
// ── Core tools (~5k tokens) ──────────────────────────────────────────────
|
|
2077
|
+
{
|
|
2078
|
+
name: "list_stories",
|
|
2079
|
+
description: "List all stories from the intentdocs story map with their status (todo/in_progress/done), priority, effort, and acceptance criteria.",
|
|
2080
|
+
inputSchema: {
|
|
2081
|
+
type: "object",
|
|
2082
|
+
properties: {
|
|
2083
|
+
filter: {
|
|
2084
|
+
type: "string",
|
|
2085
|
+
description: "Optional filter: 'todo', 'in_progress', 'done', or a search term"
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
mode: "core"
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
name: "mark_done",
|
|
2093
|
+
description: "Mark a story as done on the intentdocs story map. Use the story ID from list_stories. If any 'must' acceptance criterion is failing or has never been reported via report_criteria, this returns that list instead of completing \u2014 fix and re-check, or pass force:true to override.",
|
|
2094
|
+
inputSchema: {
|
|
2095
|
+
type: "object",
|
|
2096
|
+
properties: {
|
|
2097
|
+
story_id: { type: "string", description: "The story ID (full UUID or prefix)" },
|
|
2098
|
+
force: {
|
|
2099
|
+
type: "boolean",
|
|
2100
|
+
description: "Mark done even if must criteria are failing or unreported"
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
2103
|
+
required: ["story_id"]
|
|
2104
|
+
},
|
|
2105
|
+
mode: "core"
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
name: "report_criteria",
|
|
2109
|
+
description: "Report pass/fail/blocked results for one or more acceptance criteria on a story, after implementing and evaluating each one. This is the acceptance-criteria loop's feedback channel \u2014 results flow back to the story map so criteria flip green there.",
|
|
2110
|
+
inputSchema: {
|
|
2111
|
+
type: "object",
|
|
2112
|
+
properties: {
|
|
2113
|
+
story_id: { type: "string", description: "The story ID (full UUID or prefix)" },
|
|
2114
|
+
results: {
|
|
2115
|
+
type: "array",
|
|
2116
|
+
items: {
|
|
2117
|
+
type: "object",
|
|
2118
|
+
properties: {
|
|
2119
|
+
criterion_id: { type: "string", description: "The criterion ID from list_stories" },
|
|
2120
|
+
status: { type: "string", enum: ["pass", "fail", "blocked"] },
|
|
2121
|
+
detail: { type: "string", description: "What you observed \u2014 the evidence for this result" },
|
|
2122
|
+
attempt: {
|
|
2123
|
+
type: "number",
|
|
2124
|
+
description: "Which attempt this is, if retrying a failing must criterion"
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
required: ["criterion_id", "status"]
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
},
|
|
2131
|
+
required: ["story_id", "results"]
|
|
2132
|
+
},
|
|
2133
|
+
mode: "core"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
name: "raise_blocker",
|
|
2137
|
+
description: "Raise a blocker on a story when a 'must' criterion keeps failing after repeated fix attempts (3 strikes). Surfaces the single decision the founder needs to make. Stop implementing this story once you call this \u2014 do not call mark_done afterward.",
|
|
2138
|
+
inputSchema: {
|
|
2139
|
+
type: "object",
|
|
2140
|
+
properties: {
|
|
2141
|
+
story_id: { type: "string", description: "The story ID (full UUID or prefix)" },
|
|
2142
|
+
criterion_id: { type: "string", description: "Optional: the specific criterion that's blocked" },
|
|
2143
|
+
summary: { type: "string", description: "Short summary of the blocker" },
|
|
2144
|
+
attempting: { type: "string", description: "What you were trying to do" },
|
|
2145
|
+
failing: { type: "string", description: "What's failing and why" },
|
|
2146
|
+
decision_needed: { type: "string", description: "The single decision the founder needs to make" },
|
|
2147
|
+
attempts: { type: "number", description: "How many fix attempts were made" }
|
|
2148
|
+
},
|
|
2149
|
+
required: ["story_id", "summary", "attempting", "failing", "decision_needed"]
|
|
2150
|
+
},
|
|
2151
|
+
mode: "core"
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
name: "mark_in_progress",
|
|
2155
|
+
description: "Mark a story as in progress on the intentdocs story map.",
|
|
2156
|
+
inputSchema: {
|
|
2157
|
+
type: "object",
|
|
2158
|
+
properties: {
|
|
2159
|
+
story_id: { type: "string", description: "The story ID" }
|
|
2160
|
+
},
|
|
2161
|
+
required: ["story_id"]
|
|
2162
|
+
},
|
|
2163
|
+
mode: "core"
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
name: "get_status",
|
|
2167
|
+
description: "Get a summary of story map progress \u2014 total stories, done, in progress, todo counts.",
|
|
2168
|
+
inputSchema: { type: "object", properties: {} },
|
|
2169
|
+
mode: "core"
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
name: "add_note",
|
|
2173
|
+
description: "Add implementation notes to a story. Use this to record how something was built, gotchas, or context for future developers.",
|
|
2174
|
+
inputSchema: {
|
|
2175
|
+
type: "object",
|
|
2176
|
+
properties: {
|
|
2177
|
+
story_id: { type: "string", description: "The story ID" },
|
|
2178
|
+
note: { type: "string", description: "Implementation note" }
|
|
2179
|
+
},
|
|
2180
|
+
required: ["story_id", "note"]
|
|
2181
|
+
},
|
|
2182
|
+
mode: "core"
|
|
2183
|
+
},
|
|
2184
|
+
// ── Standard tools (~10k tokens) ────────────────────────────────────────
|
|
2185
|
+
{
|
|
2186
|
+
name: "create_story",
|
|
2187
|
+
description: "Create a new story on the intentdocs story map. Use when you discover a new requirement during development.",
|
|
2188
|
+
inputSchema: {
|
|
2189
|
+
type: "object",
|
|
2190
|
+
properties: {
|
|
2191
|
+
title: { type: "string", description: "Story title" },
|
|
2192
|
+
activity: { type: "string", description: "Activity name to add the story under" },
|
|
2193
|
+
priority: { type: "string", enum: ["must", "should", "could"], description: "Priority level" },
|
|
2194
|
+
effort: { type: "string", enum: ["S", "M", "L", "XL"], description: "Effort estimate" }
|
|
2195
|
+
},
|
|
2196
|
+
required: ["title"]
|
|
2197
|
+
},
|
|
2198
|
+
mode: "standard"
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
name: "get_context",
|
|
2202
|
+
description: "Get the full INTENT.md context \u2014 the product story map with all personas, stories, and acceptance criteria.",
|
|
2203
|
+
inputSchema: { type: "object", properties: {} },
|
|
2204
|
+
mode: "standard"
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: "log_decision",
|
|
2208
|
+
description: "Log an architectural or product decision to the intentdocs story map. Use this when you make a significant technical choice during development.",
|
|
2209
|
+
inputSchema: {
|
|
2210
|
+
type: "object",
|
|
2211
|
+
properties: {
|
|
2212
|
+
title: { type: "string", description: "Short decision title" },
|
|
2213
|
+
description: { type: "string", description: "What was decided and why" },
|
|
2214
|
+
category: {
|
|
2215
|
+
type: "string",
|
|
2216
|
+
enum: ["architecture", "product", "technical", "trade-off"],
|
|
2217
|
+
description: "Decision category"
|
|
2218
|
+
},
|
|
2219
|
+
story_id: { type: "string", description: "Optional: link to a story ID" }
|
|
2220
|
+
},
|
|
2221
|
+
required: ["title", "description"]
|
|
2222
|
+
},
|
|
2223
|
+
mode: "standard"
|
|
2224
|
+
},
|
|
2225
|
+
// ── All tools (~15k tokens) ─────────────────────────────────────────────
|
|
2226
|
+
{
|
|
2227
|
+
name: "analyze_complexity",
|
|
2228
|
+
description: "Analyze the complexity of stories using AI. Returns a 1-10 complexity score, reasoning, risks, and suggested effort for each story. Useful before starting work to identify hidden complexity.",
|
|
2229
|
+
inputSchema: {
|
|
2230
|
+
type: "object",
|
|
2231
|
+
properties: {
|
|
2232
|
+
story_ids: {
|
|
2233
|
+
type: "array",
|
|
2234
|
+
items: { type: "string" },
|
|
2235
|
+
description: "Optional list of story IDs to analyze. If omitted, analyzes all non-done stories."
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
mode: "all"
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
name: "research",
|
|
2243
|
+
description: "Research a topic using AI with your project context. Returns findings, recommendations, risks, and next steps. Use for architectural decisions, library choices, or technical questions.",
|
|
2244
|
+
inputSchema: {
|
|
2245
|
+
type: "object",
|
|
2246
|
+
properties: {
|
|
2247
|
+
query: { type: "string", description: "What to research" }
|
|
2248
|
+
},
|
|
2249
|
+
required: ["query"]
|
|
2250
|
+
},
|
|
2251
|
+
mode: "all"
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
name: "import_prd",
|
|
2255
|
+
description: "Parse a PRD or spec document into user stories. Returns structured stories with titles, activities, priorities, effort estimates, and acceptance criteria.",
|
|
2256
|
+
inputSchema: {
|
|
2257
|
+
type: "object",
|
|
2258
|
+
properties: {
|
|
2259
|
+
prd: { type: "string", description: "The PRD or spec text to parse" },
|
|
2260
|
+
format: { type: "string", description: "Document format: PRD, Markdown, text", enum: ["PRD", "Markdown", "text"] }
|
|
2261
|
+
},
|
|
2262
|
+
required: ["prd"]
|
|
2263
|
+
},
|
|
2264
|
+
mode: "all"
|
|
2265
|
+
}
|
|
2266
|
+
];
|
|
2267
|
+
var MODE_LEVELS = { core: 0, standard: 1, all: 2 };
|
|
2268
|
+
function getToolsForMode(mode) {
|
|
2269
|
+
const level = MODE_LEVELS[mode];
|
|
2270
|
+
return TOOLS.filter((t) => MODE_LEVELS[t.mode] <= level);
|
|
2271
|
+
}
|
|
2272
|
+
async function handleToolCall(name, args, config) {
|
|
2273
|
+
const guardExempt = /* @__PURE__ */ new Set(["list_stories", "get_status"]);
|
|
2274
|
+
if (!guardExempt.has(name)) {
|
|
2275
|
+
const storyId = args?.story_id;
|
|
2276
|
+
if (storyId) {
|
|
2277
|
+
const data = await listStories(config);
|
|
2278
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2279
|
+
if (match?.cancelled_at) {
|
|
2280
|
+
return {
|
|
2281
|
+
content: [{
|
|
2282
|
+
type: "text",
|
|
2283
|
+
text: `\u{1F6D1} Development of "${match.title}" was cancelled at ${match.cancelled_at}. Stop all work on this story immediately. Do not make any more changes.`
|
|
2284
|
+
}]
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
switch (name) {
|
|
2290
|
+
case "list_stories": {
|
|
2291
|
+
const data = await listStories(config);
|
|
2292
|
+
let storyList = data.stories;
|
|
2293
|
+
const filter = args?.filter ?? "";
|
|
2294
|
+
if (filter) {
|
|
2295
|
+
const f = filter.toLowerCase();
|
|
2296
|
+
if (["todo", "in_progress", "done"].includes(f)) {
|
|
2297
|
+
storyList = storyList.filter((s) => s.status === f);
|
|
2298
|
+
} else {
|
|
2299
|
+
storyList = storyList.filter(
|
|
2300
|
+
(s) => s.title.toLowerCase().includes(f) || s.activity.toLowerCase().includes(f)
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return {
|
|
2305
|
+
content: [{
|
|
2306
|
+
type: "text",
|
|
2307
|
+
text: JSON.stringify({ summary: data.summary, stories: storyList }, null, 2)
|
|
2308
|
+
}]
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
case "mark_in_progress": {
|
|
2312
|
+
const storyId = args?.story_id;
|
|
2313
|
+
if (!storyId) return { content: [{ type: "text", text: "story_id is required" }] };
|
|
2314
|
+
const data = await listStories(config);
|
|
2315
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2316
|
+
if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
|
|
2317
|
+
await updateStories(config, [{ id: match.id, status: "in_progress" }]);
|
|
2318
|
+
return {
|
|
2319
|
+
content: [{ type: "text", text: `\u2713 ${match.title} \u2192 in_progress` }]
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
case "mark_done": {
|
|
2323
|
+
const storyId = args?.story_id;
|
|
2324
|
+
if (!storyId) return { content: [{ type: "text", text: "story_id is required" }] };
|
|
2325
|
+
const force = args?.force === true;
|
|
2326
|
+
const data = await listStories(config);
|
|
2327
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2328
|
+
if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
|
|
2329
|
+
const blocking = findBlockingMusts(match.acceptance_criteria);
|
|
2330
|
+
if (!force && blocking.length > 0) {
|
|
2331
|
+
const list = blocking.map((ac) => ` - [${ac.last_result ? ac.last_result.status : "unreported"}] ${ac.text}`).join("\n");
|
|
2332
|
+
return {
|
|
2333
|
+
content: [{
|
|
2334
|
+
type: "text",
|
|
2335
|
+
text: `Can't mark done \u2014 ${blocking.length} 'must' criterion/criteria still failing or unreported:
|
|
2336
|
+
${list}
|
|
2337
|
+
|
|
2338
|
+
Fix and call report_criteria, or call mark_done again with force:true to override.`
|
|
2339
|
+
}]
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
const forced = force && blocking.length > 0;
|
|
2343
|
+
await updateStories(config, [{ id: match.id, status: "done", ...forced ? { forced: true } : {} }]);
|
|
2344
|
+
return {
|
|
2345
|
+
content: [{ type: "text", text: `\u2713 ${match.title} \u2192 done` }]
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
case "report_criteria": {
|
|
2349
|
+
const storyId = args?.story_id;
|
|
2350
|
+
const results = args?.results;
|
|
2351
|
+
if (!storyId || !results?.length) {
|
|
2352
|
+
return { content: [{ type: "text", text: "story_id and results are required" }] };
|
|
2353
|
+
}
|
|
2354
|
+
const data = await listStories(config);
|
|
2355
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2356
|
+
if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
|
|
2357
|
+
const result = await reportCriteria(
|
|
2358
|
+
config,
|
|
2359
|
+
match.id,
|
|
2360
|
+
results
|
|
2361
|
+
);
|
|
2362
|
+
const passCount = results.filter((r) => r.status === "pass").length;
|
|
2363
|
+
const failCount = results.filter((r) => r.status !== "pass").length;
|
|
2364
|
+
const unmatchedNote = result.unmatched.length ? ` (${result.unmatched.length} criterion_id(s) not found on this story: ${result.unmatched.join(", ")})` : "";
|
|
2365
|
+
return {
|
|
2366
|
+
content: [{
|
|
2367
|
+
type: "text",
|
|
2368
|
+
text: `\u2713 Reported ${passCount + failCount} result(s) for ${match.title}: ${passCount} passed, ${failCount} not passed${unmatchedNote}`
|
|
2369
|
+
}]
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
case "raise_blocker": {
|
|
2373
|
+
const storyId = args?.story_id;
|
|
2374
|
+
const summary = args?.summary;
|
|
2375
|
+
const attempting = args?.attempting;
|
|
2376
|
+
const failing = args?.failing;
|
|
2377
|
+
const decisionNeeded = args?.decision_needed;
|
|
2378
|
+
if (!storyId || !summary || !attempting || !failing || !decisionNeeded) {
|
|
2379
|
+
return {
|
|
2380
|
+
content: [{
|
|
2381
|
+
type: "text",
|
|
2382
|
+
text: "story_id, summary, attempting, failing, and decision_needed are required"
|
|
2383
|
+
}]
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
const data = await listStories(config);
|
|
2387
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2388
|
+
if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
|
|
2389
|
+
await raiseBlocker(config, {
|
|
2390
|
+
story_id: match.id,
|
|
2391
|
+
criterion_id: args?.criterion_id,
|
|
2392
|
+
summary,
|
|
2393
|
+
attempting,
|
|
2394
|
+
failing,
|
|
2395
|
+
decision_needed: decisionNeeded,
|
|
2396
|
+
attempts: args?.attempts
|
|
2397
|
+
});
|
|
2398
|
+
return {
|
|
2399
|
+
content: [{
|
|
2400
|
+
type: "text",
|
|
2401
|
+
text: `\u{1F6A7} Blocker raised on ${match.title}: ${summary}
|
|
2402
|
+
Decision needed: ${decisionNeeded}`
|
|
2403
|
+
}]
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
case "create_story": {
|
|
2407
|
+
const result = await createStories(config, [
|
|
2408
|
+
{
|
|
2409
|
+
title: args?.title,
|
|
2410
|
+
activity: args?.activity,
|
|
2411
|
+
priority: args?.priority,
|
|
2412
|
+
effort: args?.effort
|
|
2413
|
+
}
|
|
2414
|
+
]);
|
|
2415
|
+
return {
|
|
2416
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
case "get_context": {
|
|
2420
|
+
const md = await getContext(config);
|
|
2421
|
+
return { content: [{ type: "text", text: md }] };
|
|
2422
|
+
}
|
|
2423
|
+
case "get_status": {
|
|
2424
|
+
const data = await listStories(config);
|
|
2425
|
+
const { summary } = data;
|
|
2426
|
+
const pct = summary.total > 0 ? Math.round(summary.done / summary.total * 100) : 0;
|
|
2427
|
+
let text = `${data.map.title}: ${summary.done}/${summary.total} done (${pct}%) \u2014 ${summary.in_progress} in progress, ${summary.todo} todo`;
|
|
2428
|
+
const cancelled = data.stories.filter((s) => s.cancelled_at);
|
|
2429
|
+
if (cancelled.length > 0) {
|
|
2430
|
+
text += `
|
|
2431
|
+
|
|
2432
|
+
\u{1F6D1} CANCELLED STORIES (stop all work on these):
|
|
2433
|
+
` + cancelled.map((s) => ` - "${s.title}" (${s.id.slice(0, 8)}) cancelled at ${s.cancelled_at}`).join("\n");
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
content: [{ type: "text", text }]
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
case "log_decision": {
|
|
2440
|
+
const res = await apiFetch(config, "/decisions", {
|
|
2441
|
+
method: "POST",
|
|
2442
|
+
headers: { "Content-Type": "application/json" },
|
|
2443
|
+
body: JSON.stringify({
|
|
2444
|
+
title: args?.title,
|
|
2445
|
+
description: args?.description,
|
|
2446
|
+
category: args?.category ?? "technical",
|
|
2447
|
+
story_id: args?.story_id
|
|
2448
|
+
})
|
|
2449
|
+
});
|
|
2450
|
+
if (res.ok) {
|
|
2451
|
+
return { content: [{ type: "text", text: `\u2713 Decision logged: ${args?.title}` }] };
|
|
2452
|
+
}
|
|
2453
|
+
return { content: [{ type: "text", text: "Failed to log decision" }] };
|
|
2454
|
+
}
|
|
2455
|
+
case "add_note": {
|
|
2456
|
+
const storyId = args?.story_id;
|
|
2457
|
+
const note = args?.note;
|
|
2458
|
+
if (!storyId || !note) {
|
|
2459
|
+
return { content: [{ type: "text", text: "story_id and note required" }] };
|
|
2460
|
+
}
|
|
2461
|
+
const data = await listStories(config);
|
|
2462
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
2463
|
+
if (!match) {
|
|
2464
|
+
return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
|
|
2465
|
+
}
|
|
2466
|
+
await apiFetch(config, "/stories", {
|
|
2467
|
+
method: "PATCH",
|
|
2468
|
+
headers: { "Content-Type": "application/json" },
|
|
2469
|
+
body: JSON.stringify({ stories: [{ id: match.id, notes: note }] })
|
|
2470
|
+
});
|
|
2471
|
+
return { content: [{ type: "text", text: `\u2713 Note added to: ${match.title}` }] };
|
|
2472
|
+
}
|
|
2473
|
+
// ── New tools ─────────────────────────────────────────────────────────
|
|
2474
|
+
case "analyze_complexity": {
|
|
2475
|
+
const storyIds = args?.story_ids;
|
|
2476
|
+
const body = {};
|
|
2477
|
+
if (storyIds?.length) body.story_ids = storyIds;
|
|
2478
|
+
else {
|
|
2479
|
+
const data = await listStories(config);
|
|
2480
|
+
body.story_ids = data.stories.filter((s) => s.status !== "done").map((s) => s.id);
|
|
2481
|
+
}
|
|
2482
|
+
const res = await fetch(apiUrl(config, "/complexity"), {
|
|
2483
|
+
method: "POST",
|
|
2484
|
+
headers: { "Content-Type": "application/json" },
|
|
2485
|
+
body: JSON.stringify(body)
|
|
2486
|
+
});
|
|
2487
|
+
if (!res.ok) {
|
|
2488
|
+
if (isPlanGated(res.status)) {
|
|
2489
|
+
return { content: [{ type: "text", text: UPGRADE_MESSAGE_PLAIN }] };
|
|
2490
|
+
}
|
|
2491
|
+
return { content: [{ type: "text", text: "Complexity analysis failed" }] };
|
|
2492
|
+
}
|
|
2493
|
+
const result = await res.json();
|
|
2494
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2495
|
+
}
|
|
2496
|
+
case "research": {
|
|
2497
|
+
const query = args?.query;
|
|
2498
|
+
if (!query) return { content: [{ type: "text", text: "query is required" }] };
|
|
2499
|
+
const res = await fetch(apiUrl(config, "/research"), {
|
|
2500
|
+
method: "POST",
|
|
2501
|
+
headers: { "Content-Type": "application/json" },
|
|
2502
|
+
body: JSON.stringify({ query })
|
|
2503
|
+
});
|
|
2504
|
+
if (!res.ok) {
|
|
2505
|
+
if (isPlanGated(res.status)) {
|
|
2506
|
+
return { content: [{ type: "text", text: UPGRADE_MESSAGE_PLAIN }] };
|
|
2507
|
+
}
|
|
2508
|
+
return { content: [{ type: "text", text: "Research failed" }] };
|
|
2509
|
+
}
|
|
2510
|
+
const result = await res.json();
|
|
2511
|
+
return { content: [{ type: "text", text: result.research }] };
|
|
2512
|
+
}
|
|
2513
|
+
case "import_prd": {
|
|
2514
|
+
const prd = args?.prd;
|
|
2515
|
+
if (!prd) return { content: [{ type: "text", text: "prd text is required" }] };
|
|
2516
|
+
const res = await fetch(apiUrl(config, "/import"), {
|
|
2517
|
+
method: "POST",
|
|
2518
|
+
headers: { "Content-Type": "application/json" },
|
|
2519
|
+
body: JSON.stringify({ prd, format: args?.format ?? "PRD" })
|
|
2520
|
+
});
|
|
2521
|
+
if (!res.ok) {
|
|
2522
|
+
if (isPlanGated(res.status)) {
|
|
2523
|
+
return { content: [{ type: "text", text: UPGRADE_MESSAGE_PLAIN }] };
|
|
2524
|
+
}
|
|
2525
|
+
return { content: [{ type: "text", text: "PRD import failed" }] };
|
|
2526
|
+
}
|
|
2527
|
+
const result = await res.json();
|
|
2528
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2529
|
+
}
|
|
2530
|
+
default:
|
|
2531
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
var RESOURCES = [
|
|
2535
|
+
{
|
|
2536
|
+
uri: "intentdocs://project/storymap",
|
|
2537
|
+
name: "Story Map",
|
|
2538
|
+
description: "Full story map with personas, activities, stories, and acceptance criteria",
|
|
2539
|
+
mimeType: "text/markdown"
|
|
2540
|
+
},
|
|
2541
|
+
{
|
|
2542
|
+
uri: "intentdocs://project/stories",
|
|
2543
|
+
name: "Stories",
|
|
2544
|
+
description: "All stories with status, priority, effort, and acceptance criteria",
|
|
2545
|
+
mimeType: "application/json"
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
uri: "intentdocs://project/status",
|
|
2549
|
+
name: "Project Status",
|
|
2550
|
+
description: "Progress summary \u2014 done/in-progress/todo counts",
|
|
2551
|
+
mimeType: "application/json"
|
|
2552
|
+
},
|
|
2553
|
+
{
|
|
2554
|
+
uri: "intentdocs://project/decisions",
|
|
2555
|
+
name: "Decisions",
|
|
2556
|
+
description: "Architectural and product decisions logged during development",
|
|
2557
|
+
mimeType: "application/json"
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
uri: "intentdocs://project/data-model",
|
|
2561
|
+
name: "Data Model",
|
|
2562
|
+
description: "Data model and schema definitions from the spec",
|
|
2563
|
+
mimeType: "application/json"
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
uri: "intentdocs://project/build-order",
|
|
2567
|
+
name: "Build Order",
|
|
2568
|
+
description: "Recommended story implementation order based on dependencies",
|
|
2569
|
+
mimeType: "application/json"
|
|
2570
|
+
}
|
|
2571
|
+
];
|
|
2572
|
+
async function startMcpServer(mode = "all") {
|
|
2573
|
+
const config = requireConfig();
|
|
2574
|
+
const toolMode = ["core", "standard", "all"].includes(mode) ? mode : "all";
|
|
2575
|
+
const activeTools = getToolsForMode(toolMode);
|
|
2576
|
+
const server = new Server(
|
|
2577
|
+
{ name: "intentdocs", version: "0.2.0" },
|
|
2578
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
2579
|
+
);
|
|
2580
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2581
|
+
tools: activeTools.map(({ mode: _m, ...rest }) => rest)
|
|
2582
|
+
}));
|
|
2583
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
2584
|
+
resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({
|
|
2585
|
+
uri,
|
|
2586
|
+
name,
|
|
2587
|
+
description,
|
|
2588
|
+
mimeType
|
|
2589
|
+
}))
|
|
2590
|
+
}));
|
|
2591
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2592
|
+
const { uri } = request.params;
|
|
2593
|
+
try {
|
|
2594
|
+
if (uri === "intentdocs://project/storymap") {
|
|
2595
|
+
const text = await getContext(config);
|
|
2596
|
+
return {
|
|
2597
|
+
contents: [{ uri, mimeType: "text/markdown", text }]
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
if (uri === "intentdocs://project/stories") {
|
|
2601
|
+
const data = await listStories(config);
|
|
2602
|
+
const text = JSON.stringify({ summary: data.summary, stories: data.stories }, null, 2);
|
|
2603
|
+
return {
|
|
2604
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
if (uri === "intentdocs://project/status") {
|
|
2608
|
+
const data = await listStories(config);
|
|
2609
|
+
const { summary, map: map2 } = data;
|
|
2610
|
+
const pct = summary.total > 0 ? Math.round(summary.done / summary.total * 100) : 0;
|
|
2611
|
+
const text = JSON.stringify(
|
|
2612
|
+
{
|
|
2613
|
+
map_title: map2.title,
|
|
2614
|
+
progress: `${summary.done}/${summary.total} done (${pct}%)`,
|
|
2615
|
+
done: summary.done,
|
|
2616
|
+
in_progress: summary.in_progress,
|
|
2617
|
+
todo: summary.todo,
|
|
2618
|
+
total: summary.total
|
|
2619
|
+
},
|
|
2620
|
+
null,
|
|
2621
|
+
2
|
|
2622
|
+
);
|
|
2623
|
+
return {
|
|
2624
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
if (uri === "intentdocs://project/decisions") {
|
|
2628
|
+
const res = await fetch(apiUrl(config, "/decisions"));
|
|
2629
|
+
if (!res.ok) {
|
|
2630
|
+
const text2 = JSON.stringify({ note: "Failed to fetch decisions" });
|
|
2631
|
+
return { contents: [{ uri, mimeType: "application/json", text: text2 }] };
|
|
2632
|
+
}
|
|
2633
|
+
const data = await res.json();
|
|
2634
|
+
const text = JSON.stringify(data, null, 2);
|
|
2635
|
+
return {
|
|
2636
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
if (uri === "intentdocs://project/data-model") {
|
|
2640
|
+
const res = await fetch(apiUrl(config, "/data-model"));
|
|
2641
|
+
let data;
|
|
2642
|
+
if (res.status === 404) {
|
|
2643
|
+
data = { note: "No data model defined yet. Run the interview or add one via the web UI." };
|
|
2644
|
+
} else if (res.ok) {
|
|
2645
|
+
data = await res.json();
|
|
2646
|
+
} else {
|
|
2647
|
+
data = { note: "Failed to fetch data model" };
|
|
2648
|
+
}
|
|
2649
|
+
const text = JSON.stringify(data, null, 2);
|
|
2650
|
+
return {
|
|
2651
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
if (uri === "intentdocs://project/build-order") {
|
|
2655
|
+
const res = await fetch(apiUrl(config, "/build-order"));
|
|
2656
|
+
let data;
|
|
2657
|
+
if (res.status === 404) {
|
|
2658
|
+
data = { note: "No build order defined yet." };
|
|
2659
|
+
} else if (res.ok) {
|
|
2660
|
+
data = await res.json();
|
|
2661
|
+
} else {
|
|
2662
|
+
data = { note: "Failed to fetch build order" };
|
|
2663
|
+
}
|
|
2664
|
+
const text = JSON.stringify(data, null, 2);
|
|
2665
|
+
return {
|
|
2666
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
return {
|
|
2670
|
+
contents: [{ uri, mimeType: "text/plain", text: `Unknown resource: ${uri}` }]
|
|
2671
|
+
};
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
const text = JSON.stringify({
|
|
2674
|
+
error: error instanceof Error ? error.message : "Unknown error reading resource"
|
|
2675
|
+
});
|
|
2676
|
+
return {
|
|
2677
|
+
contents: [{ uri, mimeType: "application/json", text }]
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2682
|
+
const { name, arguments: args } = request.params;
|
|
2683
|
+
if (!activeTools.find((t) => t.name === name)) {
|
|
2684
|
+
return {
|
|
2685
|
+
content: [{
|
|
2686
|
+
type: "text",
|
|
2687
|
+
text: `Tool "${name}" is not available in "${toolMode}" mode. Use --mode standard or --mode all to enable it.`
|
|
2688
|
+
}]
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
return handleToolCall(name, args, config);
|
|
2692
|
+
});
|
|
2693
|
+
const transport = new StdioServerTransport();
|
|
2694
|
+
await server.connect(transport);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
// src/commands/openclaw-register.ts
|
|
2698
|
+
import { execSync as execSync3 } from "child_process";
|
|
2699
|
+
async function openclawRegister() {
|
|
2700
|
+
if (!hasConfig()) {
|
|
2701
|
+
console.error("Not initialised. Run: intent init");
|
|
2702
|
+
process.exit(1);
|
|
2703
|
+
}
|
|
2704
|
+
const config = readConfig();
|
|
2705
|
+
const cwd = process.cwd();
|
|
2706
|
+
const mapTitle = config?.mapTitle ?? "this project";
|
|
2707
|
+
const registration = JSON.stringify({
|
|
2708
|
+
command: "npx",
|
|
2709
|
+
args: ["intentdocs", "mcp"],
|
|
2710
|
+
cwd
|
|
2711
|
+
});
|
|
2712
|
+
console.log(`
|
|
2713
|
+
Registering intentdocs MCP with OpenClaw...`);
|
|
2714
|
+
console.log(` Project : ${mapTitle}`);
|
|
2715
|
+
console.log(` cwd : ${cwd}
|
|
2716
|
+
`);
|
|
2717
|
+
try {
|
|
2718
|
+
execSync3(`openclaw mcp set intentdocs '${registration}'`, {
|
|
2719
|
+
stdio: "inherit"
|
|
2720
|
+
});
|
|
2721
|
+
console.log(`
|
|
2722
|
+
\u2705 Done. Any coding agent OpenClaw spawns from this project will have`);
|
|
2723
|
+
console.log(` access to list_stories, mark_done, add_note, create_story, and more.
|
|
2724
|
+
`);
|
|
2725
|
+
console.log(` To register a different project, cd into it and run this command again.
|
|
2726
|
+
`);
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
console.error("\n\u2717 Registration failed. Is OpenClaw installed and on your PATH?");
|
|
2729
|
+
console.error(" You can register manually:\n");
|
|
2730
|
+
console.error(` openclaw mcp set intentdocs '${registration}'
|
|
2731
|
+
`);
|
|
2732
|
+
process.exit(1);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// src/index.ts
|
|
2737
|
+
var require2 = createRequire2(import.meta.url);
|
|
2738
|
+
var { version } = require2("../package.json");
|
|
2739
|
+
var program = new Command();
|
|
2740
|
+
program.name("intent").description("Write specs, map stories, ship with AI \u2014 from your terminal").version(version);
|
|
2741
|
+
program.command("login").description("Log in to intentdocs (opens browser)").action(() => void login());
|
|
2742
|
+
program.command("logout").description("Log out of intentdocs").action(() => void logout());
|
|
2743
|
+
var keys = program.command("keys").description("Manage your Anthropic API key \u2014 unlock AI features on any plan");
|
|
2744
|
+
keys.command("set [key]").description("Save your Anthropic API key (prompts if omitted)").action((key) => {
|
|
2745
|
+
import("./keys-EDWET3WG.js").then((m) => void m.keysSet(key));
|
|
2746
|
+
});
|
|
2747
|
+
keys.command("status").description("Show whether a key is set (masked)").action(() => {
|
|
2748
|
+
import("./keys-EDWET3WG.js").then((m) => void m.keysStatus());
|
|
2749
|
+
});
|
|
2750
|
+
keys.command("remove").description("Remove your stored key").action(() => {
|
|
2751
|
+
import("./keys-EDWET3WG.js").then((m) => void m.keysRemove());
|
|
2752
|
+
});
|
|
2753
|
+
program.command("start").description("Start a new project \u2014 guided flow from problem to pitch").action(() => void start());
|
|
2754
|
+
program.command("scope [projectId]").description("Scope a project \u2014 generate blueprint, story map, and mockup").action((id) => void scope(id));
|
|
2755
|
+
program.command("setup").description("Configure your AI tool (Claude Code / Cursor) with intentdocs agent workflows").action(() => void setup());
|
|
2756
|
+
program.command("propose [name]").description("Propose a new change \u2014 creates stories in your map and saves a local spec").option("--from <file>", "Parse stories from a markdown proposal file instead of prompting").action((name, opts) => void propose(name, opts));
|
|
2757
|
+
program.command("templates [name]").description("Browse story map templates").action((name) => {
|
|
2758
|
+
if (!name) {
|
|
2759
|
+
console.log("\n Available templates:\n");
|
|
2760
|
+
for (const [key, tmpl] of Object.entries(templates)) {
|
|
2761
|
+
console.log(` ${key.padEnd(20)} ${tmpl.description}`);
|
|
2762
|
+
}
|
|
2763
|
+
console.log(`
|
|
2764
|
+
Usage: intent templates <name>
|
|
2765
|
+
`);
|
|
2766
|
+
} else {
|
|
2767
|
+
const tmpl = templates[name];
|
|
2768
|
+
if (!tmpl) {
|
|
2769
|
+
console.error(`Unknown template: "${name}". Run \`intent templates\` to list.`);
|
|
2770
|
+
} else {
|
|
2771
|
+
console.log(`
|
|
2772
|
+
${tmpl.name}: ${tmpl.description}`);
|
|
2773
|
+
console.log(` ${tmpl.stories.length} stories, ${tmpl.activities.length} activities, ${tmpl.personas.length} personas
|
|
2774
|
+
`);
|
|
2775
|
+
for (const act of tmpl.activities) {
|
|
2776
|
+
console.log(` ${act.title}: ${act.steps.join(" \u2192 ")}`);
|
|
2777
|
+
}
|
|
2778
|
+
console.log();
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
program.command("init [token]").description("Connect this project to a intentdocs story map").action((token) => void init(token));
|
|
2783
|
+
program.command("sync").description("Refresh INTENT.md from the live story map").option("--context", "Refresh only the CLAUDE.md/AGENTS.md context file (not stories)").action((opts) => void sync(opts));
|
|
2784
|
+
program.command("status").description("Show story map progress").action(() => void status());
|
|
2785
|
+
program.command("stories").description("List all stories").option("-f, --filter <filter>", "Filter by status (todo/in-progress/done) or search term").option("-s, --status <status>", "Filter by exact status: todo, in-progress, wip, done").option("-k, --kanban", "View stories as a kanban board (todo | in-progress | done)").action((opts) => void stories(opts));
|
|
2786
|
+
program.command("next").description("Show the next highest-priority story to work on").option("-o, --open", "Open the spec page in your browser").option("--skip-wip", "Show next todo even if stories are in progress").option("--story-id <id>", "Show a specific story by ID").action((opts) => void next(opts));
|
|
2787
|
+
program.command("implement <id>").description("Pick up a story and mark it in-progress").option("--stub", "Suggest verification commands for acceptance criteria (requires ANTHROPIC_API_KEY)").action((id, opts) => void implement(id, opts));
|
|
2788
|
+
program.command("check [idPrefix]").description("Run executable acceptance criteria locally and print pass/fail (defaults to the in-progress story)").option("--report", "Report results back to the story map (reported_by: human)").action((idPrefix, opts) => void check(idPrefix, opts));
|
|
2789
|
+
program.command("show <id>").description("Show the full spec for a story (accepts ID prefix)").action((id) => void show(id));
|
|
2790
|
+
program.command("done <id>").description("Mark a story as done (accepts ID prefix)").option("--screenshot <path>", "Attach a screenshot of the product from an image file").action((id, opts) => void done(id, opts));
|
|
2791
|
+
program.command("stop <id>").description("Stop development of an in-progress story (resets to todo, signals agents to stop)").action((id) => void stop(id));
|
|
2792
|
+
program.command("add <title>").description("Create a new story").option("-a, --activity <activity>", "Activity to add the story under").option("-p, --priority <priority>", "Priority: must, should, could", "should").option("-e, --effort <effort>", "Effort: S, M, L, XL", "M").action((title, opts) => void add(title, opts));
|
|
2793
|
+
program.command("decide [title]").description("Log an architectural or product decision").option("-d, --description <desc>", "Decision description").option("-c, --category <cat>", "Category: architecture, product, technical, trade-off", "technical").option("-s, --story <id>", "Link to a story ID").action((title, opts) => {
|
|
2794
|
+
import("./log-TT2UCJP7.js").then((m) => void m.logDecision(title, opts));
|
|
2795
|
+
});
|
|
2796
|
+
program.command("note <storyId> [note]").description("Add implementation notes to a story").action((storyId, note) => {
|
|
2797
|
+
import("./log-TT2UCJP7.js").then((m) => void m.logNote(storyId, note));
|
|
2798
|
+
});
|
|
2799
|
+
program.command("example").description("Add a few-shot reference pattern (before/after code) to the project").action(() => void example());
|
|
2800
|
+
program.command("complexity [storyId]").description("Analyze story complexity with AI \u2014 scores, risks, effort estimates").action((id) => {
|
|
2801
|
+
import("./complexity-NSSYVT54.js").then((m) => void m.complexity(id));
|
|
2802
|
+
});
|
|
2803
|
+
program.command("research [query]").description("Research a topic with AI using your project context").action((query) => {
|
|
2804
|
+
import("./research-6SXJ37TI.js").then((m) => void m.research(query));
|
|
2805
|
+
});
|
|
2806
|
+
program.command("import [file]").description("Import a PRD or spec into your story map as stories").action((file) => {
|
|
2807
|
+
import("./import-prd-CKV3R67G.js").then((m) => void m.importPrd(file));
|
|
2808
|
+
});
|
|
2809
|
+
var map = program.command("map").description("Import or extract story maps from codebases");
|
|
2810
|
+
map.command("extract").description("Output a prompt for your coding agent to extract a story map from an existing codebase").action(() => {
|
|
2811
|
+
import("./map-extract-I3AACTGL.js").then((m) => void m.mapExtract());
|
|
2812
|
+
});
|
|
2813
|
+
map.command("import <file>").description("Import a storymap.json file generated by your coding agent").option("--yes", "Skip confirmation prompt").action((file, opts) => {
|
|
2814
|
+
import("./map-import-IYUMRASO.js").then((m) => void m.mapImport(file, opts));
|
|
2815
|
+
});
|
|
2816
|
+
map.command("schema").description("Print the JSON Schema for storymap.json").action(() => {
|
|
2817
|
+
import("./map-schema-JVL3O2TJ.js").then((m) => void m.mapSchema());
|
|
2818
|
+
});
|
|
2819
|
+
program.command("mcp").description("Start MCP server for Claude Code / Cursor integration").option("-m, --mode <mode>", "Tool mode: core, standard, all", "all").action((opts) => void startMcpServer(opts.mode));
|
|
2820
|
+
program.command("openclaw-register").description("Register this project with OpenClaw MCP \u2014 gives spawned coding agents access to your story map").action(() => void openclawRegister());
|
|
2821
|
+
program.parse();
|