intentdocs 0.10.1 → 0.12.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/dist/index.js CHANGED
@@ -1,22 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ appendToNotes
4
+ } from "./chunk-GFKZ5CRL.js";
2
5
  import {
3
6
  UPGRADE_MESSAGE_PLAIN,
4
7
  isPlanGated
5
8
  } from "./chunk-UQDMUKLP.js";
6
9
  import {
7
10
  ci
8
- } from "./chunk-6X4BORMW.js";
11
+ } from "./chunk-5GATVQEI.js";
9
12
  import {
10
13
  setup,
11
14
  sync
12
- } from "./chunk-PUGYRSMK.js";
13
- import {
14
- captureCli,
15
- shutdownCliAnalytics
16
- } from "./chunk-KPQ42YE6.js";
17
- import {
18
- scope
19
- } from "./chunk-NHNBCNND.js";
15
+ } from "./chunk-SDXDVNNP.js";
20
16
  import {
21
17
  criterionPriority,
22
18
  criterionStatusLabel,
@@ -24,11 +20,22 @@ import {
24
20
  formatDesignRefLine,
25
21
  storyContextPath,
26
22
  writeStoryContext
27
- } from "./chunk-F52HPVNQ.js";
23
+ } from "./chunk-BNEU6TLJ.js";
28
24
  import {
29
25
  matchesStoryRef,
30
26
  storyRef
31
27
  } from "./chunk-LQTPZL2Y.js";
28
+ import {
29
+ captureCli,
30
+ shutdownCliAnalytics
31
+ } from "./chunk-KPQ42YE6.js";
32
+ import {
33
+ scope
34
+ } from "./chunk-Q6BSSJK5.js";
35
+ import {
36
+ login,
37
+ logout
38
+ } from "./chunk-64I7RLP3.js";
32
39
  import {
33
40
  amendDecision,
34
41
  apiFetch,
@@ -43,13 +50,14 @@ import {
43
50
  syncHeaders,
44
51
  updateStories,
45
52
  uploadScreenshot
46
- } from "./chunk-34JCN66H.js";
53
+ } from "./chunk-KTGQO3AE.js";
47
54
  import {
55
+ checkoutConfig,
48
56
  hasConfig,
49
57
  readConfig,
50
58
  requireConfig,
51
59
  writeConfig
52
- } from "./chunk-JX2RLGOM.js";
60
+ } from "./chunk-RGFGTIIH.js";
53
61
  import {
54
62
  ask,
55
63
  closePrompt,
@@ -59,12 +67,8 @@ import {
59
67
  } from "./chunk-WRRUCCVZ.js";
60
68
  import {
61
69
  authenticatedFetch,
62
- bindingLabel,
63
- clearAuth,
64
70
  getAuth,
65
- requireValidAuth,
66
- saveAuth,
67
- validateAuth
71
+ requireValidAuth
68
72
  } from "./chunk-XZE2XP6O.js";
69
73
  import {
70
74
  API_BASE
@@ -75,168 +79,96 @@ import { Command } from "commander";
75
79
  import { createRequire as createRequire2 } from "module";
76
80
 
77
81
  // src/commands/init.ts
78
- import { writeFileSync, existsSync, readFileSync, appendFileSync, statSync } from "fs";
79
- import { join } from "path";
82
+ import { writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync, statSync } from "fs";
83
+ import { join as join2 } from "path";
80
84
  import { createInterface } from "readline";
81
85
 
82
- // src/commands/login.ts
83
- import { exec } from "child_process";
84
- function finishLogin(data) {
85
- saveAuth({
86
- token: data.token,
87
- apiBase: API_BASE,
88
- ...data.userId && { userId: data.userId },
89
- binding: data.binding ?? null,
90
- ...data.posthogKey && { posthogKey: data.posthogKey },
91
- ...data.posthogHost && { posthogHost: data.posthogHost }
92
- });
93
- console.log(`
94
-
95
- \u2705 Logged in as ${data.userId ?? "unknown user"} \u2014 token ${bindingLabel(data.binding)}
96
- `);
97
- if (data.binding?.approvalStatus === "pending") {
98
- console.log("An admin has been asked to approve it. Until they do, this token reads your personal projects only.\n");
86
+ // src/lib/agent-permissions.ts
87
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
88
+ import { homedir } from "os";
89
+ import { dirname, join } from "path";
90
+ var READ_TOOLS = [
91
+ "list_projects",
92
+ "get_context",
93
+ "get_session_context",
94
+ "list_stories",
95
+ "get_story",
96
+ "get_status",
97
+ "get_build_order",
98
+ "get_vision",
99
+ "get_design",
100
+ "get_run"
101
+ ];
102
+ var BUILD_LOOP_TOOLS = [
103
+ "next_in_run",
104
+ "update_run",
105
+ "start_story",
106
+ "report_criteria",
107
+ "save_tests",
108
+ "complete_story",
109
+ "link_pr",
110
+ "add_note"
111
+ ];
112
+ var STILL_ASKS = ["replace_criteria", "update_stories", "create_stories", "cancel_story"];
113
+ var ALLOWED_TOOLS = [...READ_TOOLS, ...BUILD_LOOP_TOOLS];
114
+ var DEFAULT_SERVER_NAME = "intentdocs";
115
+ function readJson(path) {
116
+ if (!existsSync(path)) return null;
117
+ try {
118
+ return JSON.parse(readFileSync(path, "utf-8"));
119
+ } catch {
120
+ return null;
99
121
  }
100
- console.log("You can now run: npx intentdocs init");
101
- console.log(
102
- "Optional: add your Anthropic API key to unlock AI features on any plan \u2014 npx intentdocs keys set"
103
- );
104
122
  }
105
- function openBrowser(url) {
106
- const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
107
- exec(cmd);
123
+ function intentdocsServers(servers) {
124
+ if (!servers || typeof servers !== "object") return [];
125
+ return Object.entries(servers).filter(([, s]) => (s?.url ?? "").includes("intentdocs.com/api/mcp") || (s?.args ?? []).includes("intentdocs")).map(([name]) => name);
108
126
  }
109
- function sleep(ms) {
110
- return new Promise((r) => setTimeout(r, ms));
127
+ function detectServerNames(cwd = process.cwd(), home = homedir()) {
128
+ const names = /* @__PURE__ */ new Set();
129
+ for (const n of intentdocsServers(readJson(join(cwd, ".mcp.json"))?.mcpServers)) names.add(n);
130
+ const user = readJson(join(home, ".claude.json"));
131
+ for (const n of intentdocsServers(user?.mcpServers)) names.add(n);
132
+ const projects = user?.projects ?? {};
133
+ for (const n of intentdocsServers(projects[cwd]?.mcpServers)) names.add(n);
134
+ return names.size > 0 ? [...names] : [DEFAULT_SERVER_NAME];
111
135
  }
112
- async function login() {
113
- const existing = getAuth();
114
- if (existing) {
115
- const result = await validateAuth();
116
- if (result.valid) {
117
- console.log(`Already logged in (user: ${result.userId}). Run \`intentdocs logout\` to switch accounts.`);
118
- return;
119
- }
120
- switch (result.reason) {
121
- case "revoked":
122
- console.log("Your stored token has been revoked. Starting a fresh login...\n");
123
- break;
124
- case "not_found":
125
- console.log("Your stored token is no longer valid. Starting a fresh login...\n");
126
- break;
127
- case "network_error":
128
- console.error("Could not reach intentdocs.com to verify your token. Check your internet connection and try again.");
129
- process.exit(1);
130
- break;
131
- }
132
- clearAuth();
133
- }
134
- console.log("Logging in to intentdocs...\n");
135
- const res = await fetch(`${API_BASE}/api/cli/auth`, { method: "POST" });
136
- if (!res.ok) {
137
- console.error("Failed to start login flow. Try again.");
138
- process.exit(1);
139
- }
140
- const { code } = await res.json();
141
- const authUrl = `${API_BASE}/cli/auth?code=${code}`;
142
- console.log("Opening browser to authorize...");
143
- console.log(`If it doesn't open, visit: ${authUrl}
144
- `);
145
- openBrowser(authUrl);
146
- const POLL_INTERVAL_MS = 3e3;
147
- const INITIAL_TIMEOUT_S = 300;
148
- const EXTEND_TIMEOUT_S = 120;
149
- let totalWaitedS = 0;
150
- let phase = "initial";
151
- let timeoutS = INITIAL_TIMEOUT_S;
152
- while (totalWaitedS < timeoutS) {
153
- const elapsed = totalWaitedS;
154
- const minutesLeft = Math.ceil((timeoutS - elapsed) / 60);
155
- if (elapsed === 0) {
156
- process.stdout.write("Waiting for browser approval...");
157
- } else if (elapsed % 30 === 0) {
158
- process.stdout.write(`
159
- Still waiting for approval (${minutesLeft}m remaining)...`);
160
- } else {
161
- process.stdout.write(".");
162
- }
163
- await sleep(POLL_INTERVAL_MS);
164
- totalWaitedS += POLL_INTERVAL_MS / 1e3;
165
- try {
166
- const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
167
- if (!checkRes.ok) continue;
168
- const data = await checkRes.json();
169
- if (data.status === "approved" && data.token) {
170
- finishLogin(data);
171
- return;
172
- }
173
- if (data.status === "expired") {
174
- console.error("\n\nThe server-side login request expired. Run `npx intentdocs login` to start a new one.");
175
- process.exit(1);
176
- }
177
- } catch {
178
- }
179
- }
180
- if (phase === "initial") {
181
- console.log("\n\nThe initial wait has ended. The browser approval hasn't come through yet.");
182
- console.log("This can happen if you needed to create an account or got distracted.\n");
183
- const rl = await import("readline");
184
- const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
185
- const answer = await new Promise(
186
- (resolve) => iface.question("Keep waiting? (y/n): ", resolve)
187
- );
188
- iface.close();
189
- if (answer.trim().toLowerCase().startsWith("y")) {
190
- phase = "extended";
191
- timeoutS = totalWaitedS + EXTEND_TIMEOUT_S;
192
- console.log(`
193
- Waiting ${EXTEND_TIMEOUT_S / 60} more minutes...`);
194
- while (totalWaitedS < timeoutS) {
195
- const minutesLeft = Math.ceil((timeoutS - totalWaitedS) / 60);
196
- if (Math.floor(totalWaitedS) % 30 === 0 && totalWaitedS > INITIAL_TIMEOUT_S) {
197
- process.stdout.write(`
198
- Still waiting (${minutesLeft}m remaining)...`);
199
- } else {
200
- process.stdout.write(".");
201
- }
202
- await sleep(POLL_INTERVAL_MS);
203
- totalWaitedS += POLL_INTERVAL_MS / 1e3;
204
- try {
205
- const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
206
- if (!checkRes.ok) continue;
207
- const data = await checkRes.json();
208
- if (data.status === "approved" && data.token) {
209
- finishLogin(data);
210
- return;
211
- }
212
- if (data.status === "expired") {
213
- console.error("\n\nThe server-side login request expired. Run `npx intentdocs login` to start a new one.");
214
- process.exit(1);
215
- }
216
- } catch {
217
- }
218
- }
219
- }
220
- }
221
- console.error("\n\nLogin timed out. Run `npx intentdocs login` to try again.");
222
- console.error("If the browser page is still open, complete it and re-run the command \u2014 your approval will still work within 10 minutes.\n");
223
- process.exit(1);
136
+ function allowRules(serverNames, tools = ALLOWED_TOOLS) {
137
+ return serverNames.flatMap((server) => tools.map((tool) => `mcp__${server}__${tool}`));
224
138
  }
225
- async function logout() {
226
- const { existsSync: existsSync5, unlinkSync } = await import("fs");
227
- const { join: join5 } = await import("path");
228
- const { homedir } = await import("os");
229
- const authFile = join5(homedir(), ".intent", "auth.json");
230
- if (existsSync5(authFile)) {
231
- unlinkSync(authFile);
232
- console.log("Logged out.");
233
- } else {
234
- console.log("Not logged in.");
235
- }
139
+ function withAllowRules(settings, rules) {
140
+ const permissions = { ...settings.permissions ?? {} };
141
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
142
+ permissions.allow = [...allow, ...rules.filter((r) => !allow.includes(r))];
143
+ return { ...settings, permissions };
144
+ }
145
+ function writeProjectAllowList(cwd, rules) {
146
+ const path = join(cwd, ".claude", "settings.json");
147
+ const before = readJson(path) ?? {};
148
+ const existing = new Set(before.permissions?.allow ?? []);
149
+ const next2 = withAllowRules(before, rules);
150
+ mkdirSync(dirname(path), { recursive: true });
151
+ writeFileSync(path, JSON.stringify(next2, null, 2) + "\n");
152
+ return { path, added: rules.filter((r) => !existing.has(r)).length };
236
153
  }
154
+ function describeAllowList(serverNames) {
155
+ return [
156
+ ` Allow these IntentDocs tools without asking, in .claude/settings.json (server: ${serverNames.join(", ")})?`,
157
+ "",
158
+ ` Reads: ${READ_TOOLS.join(", ")}`,
159
+ ` Build loop: ${BUILD_LOOP_TOOLS.join(", ")}`,
160
+ "",
161
+ ` Still asks: ${STILL_ASKS.join(", ")} \u2014 anything that changes the spec or cancels work.`
162
+ ].join("\n");
163
+ }
164
+ var CURSOR_ALLOW_NOTE = [
165
+ " Cursor: in its settings, find the agent's auto-run options and allow these",
166
+ ` intentdocs MCP tools: ${ALLOWED_TOOLS.join(", ")}.`,
167
+ ` Leave ${STILL_ASKS.join(", ")} asking.`
168
+ ].join("\n");
237
169
 
238
170
  // src/commands/init.ts
239
- var INTENTDOCS_CLAUDE_BLOCK = "\n\n# intentdocs\nRead INTENT.md for the product story map, personas, and backlog.\nWhen you call `save_tests`, pass the `file` and the `full_name` (the full test name, describe blocks included) for every test, so CI can report each test's result back to its story.\nWhen you open a pull request for a story, pass its URL to `npx intentdocs done` or `complete_story`; intentdocs stamps the PR with a 'Spec \u2192 Code via intentdocs.com' footer linking back to the story when GitHub is connected. If it is not, end the PR body with:\n---\nSpec \u2192 Code via [intentdocs.com](https://intentdocs.com?utm_source=github&utm_medium=pr-footer) \xB7 [story <story-id>](<story-url>)\n";
171
+ var INTENTDOCS_CLAUDE_BLOCK = "\n\n# intentdocs\nRead INTENT.md for the product story map, personas, and backlog.\nSave every test you write for a story with `save_tests`, linked to the acceptance criteria it proves, and pass the `file` and the `full_name` (the full test name, describe blocks included) for each, so CI can report each test's result back to its story.\nWhen you open a pull request for a story, pass its URL to `npx intentdocs done <story> --pr <url>` or to `complete_story` (or `link_pr`); intentdocs stamps the PR with a 'Spec \u2192 Code via intentdocs.com' footer linking back to the story when GitHub is connected. Add the footer yourself only when that reply asks you to (it says nothing when the project turned the line off); it will look like:\n---\nSpec \u2192 Code via [intentdocs.com](https://intentdocs.com?utm_source=github&utm_medium=pr-footer) \xB7 [story <story-id>](<story-url>)\n";
240
172
  function ask2(rl, question) {
241
173
  return new Promise((resolve) => rl.question(question, resolve));
242
174
  }
@@ -254,8 +186,8 @@ ${prompt} [1-${items.length}]: `);
254
186
  }
255
187
  function detectStack(cwd) {
256
188
  try {
257
- if (existsSync(join(cwd, "package.json"))) {
258
- const raw = readFileSync(join(cwd, "package.json"), "utf-8");
189
+ if (existsSync2(join2(cwd, "package.json"))) {
190
+ const raw = readFileSync2(join2(cwd, "package.json"), "utf-8");
259
191
  const pkg = JSON.parse(raw);
260
192
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
261
193
  if (deps["next"]) return "Next.js (React, TypeScript)";
@@ -267,13 +199,13 @@ function detectStack(cwd) {
267
199
  if (deps["express"]) return "Node.js (Express, TypeScript)";
268
200
  return "Node.js (TypeScript)";
269
201
  }
270
- if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "requirements.txt"))) {
271
- const hasFastapi = existsSync(join(cwd, "pyproject.toml")) && readFileSync(join(cwd, "pyproject.toml"), "utf-8").includes("fastapi");
202
+ if (existsSync2(join2(cwd, "pyproject.toml")) || existsSync2(join2(cwd, "requirements.txt"))) {
203
+ const hasFastapi = existsSync2(join2(cwd, "pyproject.toml")) && readFileSync2(join2(cwd, "pyproject.toml"), "utf-8").includes("fastapi");
272
204
  return hasFastapi ? "Python (FastAPI)" : "Python";
273
205
  }
274
- if (existsSync(join(cwd, "Cargo.toml"))) return "Rust";
275
- if (existsSync(join(cwd, "go.mod"))) return "Go";
276
- if (existsSync(join(cwd, "pubspec.yaml"))) return "Flutter (Dart)";
206
+ if (existsSync2(join2(cwd, "Cargo.toml"))) return "Rust";
207
+ if (existsSync2(join2(cwd, "go.mod"))) return "Go";
208
+ if (existsSync2(join2(cwd, "pubspec.yaml"))) return "Flutter (Dart)";
277
209
  } catch {
278
210
  }
279
211
  return "";
@@ -332,14 +264,7 @@ ${whereIAm}`);
332
264
  Story maps in "${project.name}":
333
265
  `);
334
266
  const map2 = await pickFromList(rl, maps, (m) => m.title, "Select story map");
335
- const config = {
336
- apiBase: auth.apiBase,
337
- mapTitle: map2.title,
338
- projectId: project.id,
339
- mapId: map2.id,
340
- workspace: project.workspace ? { orgId: project.workspace.orgId, name: project.workspace.name } : { orgId: null, name: null },
341
- ...appUrl && { appUrl }
342
- };
267
+ const config = checkoutConfig({ apiBase: auth.apiBase, project, map: map2, appUrl });
343
268
  console.log(`
344
269
  Connecting to "${map2.title}"${project.workspace ? ` in ${project.workspace.name ?? "its workspace"}` : ""}...`);
345
270
  try {
@@ -356,8 +281,8 @@ Connecting to "${map2.title}"${project.workspace ? ` in ${project.workspace.name
356
281
  writeConfig(config);
357
282
  await setupFiles(config);
358
283
  await setupStack(rl);
359
- const specPath = join(process.cwd(), "INTENT.md");
360
- const claudeMdBytes = existsSync(specPath) ? statSync(specPath).size : 0;
284
+ const specPath = join2(process.cwd(), "INTENT.md");
285
+ const claudeMdBytes = existsSync2(specPath) ? statSync(specPath).size : 0;
361
286
  captureCli(
362
287
  "harness_initialized",
363
288
  {
@@ -376,9 +301,10 @@ Connecting to "${map2.title}"${project.workspace ? ` in ${project.workspace.name
376
301
  npx intentdocs ci \u2014 report your tests' results from CI to each story
377
302
  npx intentdocs propose \u2014 plan a new feature from the terminal
378
303
  `);
304
+ await offerAllowList((q) => ask2(rl, q));
379
305
  const setUpCi = await ask2(rl, " Set up CI test reporting now? It creates a key and adds the workflow. (y/N): ");
380
306
  if (/^y(es)?$/i.test(setUpCi.trim())) {
381
- const { ci: ci2 } = await import("./ci-XCOXFTLB.js");
307
+ const { ci: ci2 } = await import("./ci-CWPLSRCQ.js");
382
308
  await ci2();
383
309
  } else {
384
310
  console.log(" You can run `npx intentdocs ci` any time to turn it on.\n");
@@ -388,10 +314,29 @@ Connecting to "${map2.title}"${project.workspace ? ` in ${project.workspace.name
388
314
  await shutdownCliAnalytics();
389
315
  }
390
316
  }
317
+ async function offerAllowList(askFn, cwd = process.cwd(), home) {
318
+ const servers = detectServerNames(cwd, home);
319
+ console.log(`
320
+ ${describeAllowList(servers)}
321
+ `);
322
+ const answer = await askFn(" Add them? (y/N): ");
323
+ if (!/^y(es)?$/i.test(answer.trim())) {
324
+ console.log(" Left as is \u2014 Claude Code will keep asking before each call.\n");
325
+ console.log(`${CURSOR_ALLOW_NOTE}
326
+ `);
327
+ return false;
328
+ }
329
+ const { path, added } = writeProjectAllowList(cwd, allowRules(servers));
330
+ console.log(` \u2713 Allowed ${added} ${added === 1 ? "tool" : "tools"} in ${path.replace(cwd + "/", "")}
331
+ `);
332
+ console.log(`${CURSOR_ALLOW_NOTE}
333
+ `);
334
+ return true;
335
+ }
391
336
  async function setupStack(rl) {
392
337
  const cwd = process.cwd();
393
- const archPath = join(cwd, "ARCHITECTURE.md");
394
- if (existsSync(archPath)) return;
338
+ const archPath = join2(cwd, "ARCHITECTURE.md");
339
+ if (existsSync2(archPath)) return;
395
340
  const detected = detectStack(cwd);
396
341
  let stack;
397
342
  if (detected) {
@@ -432,38 +377,38 @@ _Record trade-offs and why._
432
377
 
433
378
  _Add important files and what they do as you build._
434
379
  `;
435
- writeFileSync(archPath, archContent);
380
+ writeFileSync2(archPath, archContent);
436
381
  console.log(` \u2713 Created ARCHITECTURE.md with your stack (edit it as you build)`);
437
382
  }
438
383
  async function setupFiles(config) {
439
384
  try {
440
385
  const md = await getContext(config);
441
- writeFileSync(join(process.cwd(), "INTENT.md"), md);
386
+ writeFileSync2(join2(process.cwd(), "INTENT.md"), md);
442
387
  console.log("\u2713 Downloaded INTENT.md");
443
388
  } catch {
444
389
  console.warn("\u26A0 Could not download INTENT.md");
445
390
  }
446
- const gitignore = join(process.cwd(), ".gitignore");
447
- if (existsSync(gitignore)) {
448
- const content = readFileSync(gitignore, "utf-8");
391
+ const gitignore = join2(process.cwd(), ".gitignore");
392
+ if (existsSync2(gitignore)) {
393
+ const content = readFileSync2(gitignore, "utf-8");
449
394
  if (!content.includes(".intent/")) {
450
395
  appendFileSync(gitignore, "\n# intentdocs\n.intent/\n");
451
396
  console.log("\u2713 Added .intent/ to .gitignore");
452
397
  }
453
398
  }
454
399
  const cwd = process.cwd();
455
- if (existsSync(join(cwd, "CLAUDE.md"))) {
456
- const claudeMd = readFileSync(join(cwd, "CLAUDE.md"), "utf-8");
400
+ if (existsSync2(join2(cwd, "CLAUDE.md"))) {
401
+ const claudeMd = readFileSync2(join2(cwd, "CLAUDE.md"), "utf-8");
457
402
  if (!claudeMd.includes("INTENT.md")) {
458
- appendFileSync(join(cwd, "CLAUDE.md"), INTENTDOCS_CLAUDE_BLOCK);
403
+ appendFileSync(join2(cwd, "CLAUDE.md"), INTENTDOCS_CLAUDE_BLOCK);
459
404
  console.log("\u2713 Added INTENT.md reference to CLAUDE.md");
460
405
  }
461
406
  }
462
- if (existsSync(join(cwd, ".cursorrules"))) {
463
- const rules = readFileSync(join(cwd, ".cursorrules"), "utf-8");
407
+ if (existsSync2(join2(cwd, ".cursorrules"))) {
408
+ const rules = readFileSync2(join2(cwd, ".cursorrules"), "utf-8");
464
409
  if (!rules.includes("INTENT.md")) {
465
410
  appendFileSync(
466
- join(cwd, ".cursorrules"),
411
+ join2(cwd, ".cursorrules"),
467
412
  "\n\nRead INTENT.md for the product story map, personas, and backlog.\n"
468
413
  );
469
414
  console.log("\u2713 Added INTENT.md reference to .cursorrules");
@@ -646,8 +591,8 @@ function kanbanView(stories2) {
646
591
 
647
592
  // src/commands/next.ts
648
593
  import { execSync } from "child_process";
649
- import { writeFileSync as writeFileSync2 } from "fs";
650
- import { join as join2 } from "path";
594
+ import { writeFileSync as writeFileSync3 } from "fs";
595
+ import { join as join3 } from "path";
651
596
 
652
597
  // src/lib/verification-suggestions.ts
653
598
  function needsVerificationSuggestion(ac) {
@@ -692,13 +637,11 @@ function parseVerificationSuggestions(raw) {
692
637
  // src/commands/next.ts
693
638
  var PRIORITY_ORDER = { must: 0, should: 1, could: 2 };
694
639
  var EFFORT_ORDER = { S: 0, M: 1, L: 2, XL: 3 };
695
- function unsequencedRefusal(story, apiBase, mapId) {
640
+ function unsequencedNote(story, apiBase, mapId) {
696
641
  const planUrl = `${apiBase}/storymapper/${mapId}?phase=plan&story=${story.id}`;
697
642
  return `
698
- \u{1F512} "${story.title}" is unsequenced \u2014 it hasn't been ordered on the Plan view yet.
699
-
700
- Agents only build stories you've deliberately sequenced. Drag it into the
701
- plan (or set its build order) here, then retry:
643
+ \u2139\uFE0F "${story.title}" isn't in the build order yet. Starting it adds it to the end;
644
+ reorder it on the Plan view if it belongs elsewhere:
702
645
  \u2192 ${planUrl}
703
646
  `;
704
647
  }
@@ -714,10 +657,7 @@ async function next(opts = {}) {
714
657
  `);
715
658
  return;
716
659
  }
717
- if (match.build_sequence == null) {
718
- console.log(unsequencedRefusal(match, config.apiBase, data.map.id));
719
- return;
720
- }
660
+ if (match.build_sequence == null) console.log(unsequencedNote(match, config.apiBase, data.map.id));
721
661
  s = match;
722
662
  } else {
723
663
  if (!opts.skipWip) {
@@ -794,14 +734,14 @@ async function next(opts = {}) {
794
734
  console.log(` Plan: \u2705 Implementation plan available`);
795
735
  }
796
736
  console.log(`
797
- Start building: npx intentdocs implement ${s.id.slice(0, 8)}
737
+ Start building: npx intentdocs implement ${storyRef(s)}
798
738
  `);
799
739
  if (opts.open) {
800
- openBrowser2(specUrl);
740
+ openBrowser(specUrl);
801
741
  }
802
742
  await shutdownCliAnalytics();
803
743
  }
804
- async function generateVerificationSuggestions(story, contextPath) {
744
+ async function generateVerificationSuggestions(story, contextPath, config) {
805
745
  const acs = story.acceptance_criteria;
806
746
  const candidates = acs.filter(needsVerificationSuggestion);
807
747
  if (acs.length === 0) {
@@ -814,13 +754,84 @@ async function generateVerificationSuggestions(story, contextPath) {
814
754
  `);
815
755
  return;
816
756
  }
817
- const apiKey = process.env.ANTHROPIC_API_KEY;
818
- if (!apiKey) {
819
- console.log(` \u26A0\uFE0F ANTHROPIC_API_KEY not set \u2014 skipping verification suggestions.
757
+ console.log(` Suggesting verification commands for ${candidates.length} criteri${candidates.length !== 1 ? "a" : "on"}\u2026`);
758
+ let found = null;
759
+ let serverRefusal = null;
760
+ if (config?.mapId) {
761
+ try {
762
+ const res = await fetch(apiUrl(config, "/verification-suggestions"), {
763
+ method: "POST",
764
+ headers: syncHeaders({ "Content-Type": "application/json" }),
765
+ body: JSON.stringify({ story_id: story.id })
766
+ });
767
+ if (res.ok) {
768
+ const json = await res.json();
769
+ const byId = new Map(acs.map((ac) => [ac.id, ac]));
770
+ found = {
771
+ criteria: json.criteria.map((c) => byId.get(c.id) ?? { id: c.id, text: c.text, done: false }),
772
+ suggestions: json.suggestions
773
+ };
774
+ } else if (isPlanGated(res.status)) {
775
+ serverRefusal = "plan";
776
+ } else if (res.status === 404 || res.status === 405) {
777
+ serverRefusal = "unsupported";
778
+ } else {
779
+ console.log(` \u26A0\uFE0F intentdocs couldn't suggest commands (${res.status}) \u2014 suggestion generation skipped.
780
+ `);
781
+ return;
782
+ }
783
+ } catch {
784
+ console.log(` \u26A0\uFE0F Network error \u2014 suggestion generation skipped.
785
+ `);
786
+ return;
787
+ }
788
+ } else {
789
+ serverRefusal = "unsupported";
790
+ }
791
+ if (!found) {
792
+ const envKey = process.env.ANTHROPIC_API_KEY;
793
+ if (!envKey) {
794
+ if (serverRefusal === "plan") {
795
+ console.log(` \u26A0\uFE0F Verification suggestions need a paid plan or your own Anthropic key \u2014 run \`npx intentdocs keys set\`. Skipped.
796
+ `);
797
+ } else {
798
+ console.log(` \u26A0\uFE0F No saved Anthropic key reached the server and ANTHROPIC_API_KEY is not set \u2014 skipping verification suggestions.
799
+ `);
800
+ }
801
+ return;
802
+ }
803
+ const suggestions = await suggestWithKey(envKey, story, candidates);
804
+ if (!suggestions) return;
805
+ found = { criteria: candidates, suggestions };
806
+ }
807
+ if (found.suggestions.length === 0) {
808
+ console.log(` \u26A0\uFE0F Couldn't parse a usable response from Anthropic \u2014 suggestion generation skipped.
820
809
  `);
821
810
  return;
822
811
  }
823
- console.log(` Suggesting verification commands for ${candidates.length} criteri${candidates.length !== 1 ? "a" : "on"}\u2026`);
812
+ const lines = [`# Suggested verification commands: ${story.title}`, ``, `_Generated by \`npx intentdocs implement --stub\` \u2014 copy into the criterion's command field, or run \`npx intentdocs check\` once wired up._`, ``];
813
+ console.log();
814
+ for (const suggestion of found.suggestions) {
815
+ const ac = found.criteria[suggestion.index - 1];
816
+ if (!ac) continue;
817
+ if (suggestion.executable && suggestion.command) {
818
+ console.log(` \u2192 ${ac.text}`);
819
+ console.log(` command: ${suggestion.command}${suggestion.expected ? ` (expect: ${suggestion.expected})` : ""}`);
820
+ lines.push(`- **${ac.text}**`, ` - command: \`${suggestion.command}\``, ...suggestion.expected ? [` - expected: ${suggestion.expected}`] : [], ``);
821
+ } else {
822
+ console.log(` \u2192 ${ac.text}`);
823
+ console.log(` not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`);
824
+ lines.push(`- **${ac.text}**`, ` - not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`, ``);
825
+ }
826
+ }
827
+ const suggestionsPath = contextPath.replace(/\.md$/, ".checks.md");
828
+ writeFileSync3(suggestionsPath, lines.join("\n"));
829
+ const relPath = suggestionsPath.replace(join3(process.cwd(), "/"), "");
830
+ console.log(`
831
+ \u2713 Suggestions written: ${relPath}
832
+ `);
833
+ }
834
+ async function suggestWithKey(apiKey, story, candidates) {
824
835
  const prompt = buildVerificationSuggestionPrompt(story.title, story.user_story, candidates);
825
836
  let raw;
826
837
  try {
@@ -840,44 +851,18 @@ async function generateVerificationSuggestions(story, contextPath) {
840
851
  if (!res.ok) {
841
852
  console.log(` \u26A0\uFE0F Anthropic API error (${res.status}) \u2014 suggestion generation skipped.
842
853
  `);
843
- return;
854
+ return null;
844
855
  }
845
856
  const json = await res.json();
846
857
  raw = json.content.find((c) => c.type === "text")?.text ?? "";
847
858
  } catch {
848
859
  console.log(` \u26A0\uFE0F Network error \u2014 suggestion generation skipped.
849
860
  `);
850
- return;
851
- }
852
- const suggestions = parseVerificationSuggestions(raw);
853
- if (suggestions.length === 0) {
854
- console.log(` \u26A0\uFE0F Couldn't parse a usable response from Anthropic \u2014 suggestion generation skipped.
855
- `);
856
- return;
857
- }
858
- const lines = [`# Suggested verification commands: ${story.title}`, ``, `_Generated by \`npx intentdocs next --stub\` \u2014 copy into the criterion's command field, or run \`npx intentdocs check\` once wired up._`, ``];
859
- console.log();
860
- for (const suggestion of suggestions) {
861
- const ac = candidates[suggestion.index - 1];
862
- if (!ac) continue;
863
- if (suggestion.executable && suggestion.command) {
864
- console.log(` \u2192 ${ac.text}`);
865
- console.log(` command: ${suggestion.command}${suggestion.expected ? ` (expect: ${suggestion.expected})` : ""}`);
866
- lines.push(`- **${ac.text}**`, ` - command: \`${suggestion.command}\``, ...suggestion.expected ? [` - expected: ${suggestion.expected}`] : [], ``);
867
- } else {
868
- console.log(` \u2192 ${ac.text}`);
869
- console.log(` not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`);
870
- lines.push(`- **${ac.text}**`, ` - not executable${suggestion.reason ? `: ${suggestion.reason}` : ""}`, ``);
871
- }
861
+ return null;
872
862
  }
873
- const suggestionsPath = contextPath.replace(/\.md$/, ".checks.md");
874
- writeFileSync2(suggestionsPath, lines.join("\n"));
875
- const relPath = suggestionsPath.replace(join2(process.cwd(), "/"), "");
876
- console.log(`
877
- \u2713 Suggestions written: ${relPath}
878
- `);
863
+ return parseVerificationSuggestions(raw);
879
864
  }
880
- function openBrowser2(url) {
865
+ function openBrowser(url) {
881
866
  try {
882
867
  const platform = process.platform;
883
868
  const cmd = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
@@ -890,13 +875,12 @@ function openBrowser2(url) {
890
875
  async function implement(storyId, opts = {}) {
891
876
  const config = requireConfig();
892
877
  const data = await listStories(config);
893
- const match = data.stories.find(
894
- (st) => st.id === storyId || st.id.startsWith(storyId)
895
- );
878
+ const match = data.stories.find((st) => matchesStoryRef(st, storyId));
896
879
  if (!match) {
897
880
  console.log(`
898
- \u274C No story found matching ID: ${storyId}
881
+ \u274C No story found matching "${storyId}"
899
882
  `);
883
+ process.exitCode = 1;
900
884
  return;
901
885
  }
902
886
  const s = match;
@@ -910,14 +894,11 @@ async function implement(storyId, opts = {}) {
910
894
  if (s.status === "in_progress") {
911
895
  console.log(`
912
896
  \u{1F528} "${s.title}" is already in progress.`);
913
- console.log(` Mark done: npx intentdocs done ${s.id.slice(0, 8)}
897
+ console.log(` Mark done: npx intentdocs done ${storyRef(s)}
914
898
  `);
915
899
  return;
916
900
  }
917
- if (s.build_sequence == null) {
918
- console.log(unsequencedRefusal(s, config.apiBase, data.map.id));
919
- return;
920
- }
901
+ if (s.build_sequence == null) console.log(unsequencedNote(s, config.apiBase, data.map.id));
921
902
  const doneIds = new Set(
922
903
  data.stories.filter((st) => st.status === "done").map((st) => st.id)
923
904
  );
@@ -976,8 +957,18 @@ async function implement(storyId, opts = {}) {
976
957
  `);
977
958
  }
978
959
  try {
979
- await updateStories(config, [{ id: s.id, status: "in_progress", cancelled_at: null }]);
980
- } catch {
960
+ const result = await updateStories(config, [{ id: s.id, status: "in_progress", cancelled_at: null }]);
961
+ if (result.missing?.includes(s.id)) {
962
+ throw new Error("the server reports the story matched no row \u2014 run `npx intentdocs stories` and try again");
963
+ }
964
+ } catch (err) {
965
+ console.error(`
966
+ \u2717 Not picked up: "${s.title}"`);
967
+ console.error(` ${err instanceof Error ? err.message : String(err)}
968
+ `);
969
+ process.exitCode = 1;
970
+ await shutdownCliAnalytics();
971
+ return;
981
972
  }
982
973
  const contextPath = writeStoryContext(s);
983
974
  const relPath = contextPath.replace(process.cwd() + "/", "");
@@ -999,22 +990,22 @@ async function implement(storyId, opts = {}) {
999
990
  console.log(` Priority: ${s.priority} | Effort: ${s.effort} | Release: ${s.release}${s.build_sequence ? ` | Build step: ${s.build_sequence}` : ""}`);
1000
991
  console.log(` Context: ${relPath}`);
1001
992
  console.log(` Spec: ${specUrl}`);
1002
- console.log(` Mark done: npx intentdocs done ${s.id.slice(0, 8)}
993
+ console.log(` Mark done: npx intentdocs done ${storyRef(s)}
1003
994
  `);
1004
995
  if (opts.stub) {
1005
- await generateVerificationSuggestions(s, contextPath);
996
+ await generateVerificationSuggestions(s, contextPath, config);
1006
997
  }
1007
998
  await shutdownCliAnalytics();
1008
999
  }
1009
1000
 
1010
1001
  // src/commands/done.ts
1011
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1002
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1012
1003
 
1013
1004
  // src/lib/screenshot.ts
1014
1005
  import { createRequire } from "module";
1015
1006
  import { pathToFileURL } from "url";
1016
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1017
- import { extname, join as join3 } from "path";
1007
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1008
+ import { extname, join as join4 } from "path";
1018
1009
  var MAX_BYTES = 5 * 1024 * 1024;
1019
1010
  var EXT_MIME = {
1020
1011
  ".png": "image/png",
@@ -1024,7 +1015,7 @@ var EXT_MIME = {
1024
1015
  };
1025
1016
  async function loadPlaywright() {
1026
1017
  try {
1027
- const req = createRequire(join3(process.cwd(), "package.json"));
1018
+ const req = createRequire(join4(process.cwd(), "package.json"));
1028
1019
  const resolved = req.resolve("playwright");
1029
1020
  return await import(pathToFileURL(resolved).href);
1030
1021
  } catch {
@@ -1032,7 +1023,7 @@ async function loadPlaywright() {
1032
1023
  }
1033
1024
  }
1034
1025
  function readScreenshotFile(path) {
1035
- if (!existsSync2(path)) {
1026
+ if (!existsSync3(path)) {
1036
1027
  console.error(` Screenshot file not found: ${path}`);
1037
1028
  return null;
1038
1029
  }
@@ -1042,7 +1033,7 @@ function readScreenshotFile(path) {
1042
1033
  console.error(` Unsupported screenshot type "${ext}" \u2014 use .png, .jpg, or .webp`);
1043
1034
  return null;
1044
1035
  }
1045
- const buffer = readFileSync2(path);
1036
+ const buffer = readFileSync3(path);
1046
1037
  if (buffer.length > MAX_BYTES) {
1047
1038
  console.error(` Screenshot too large (${(buffer.length / 1024 / 1024).toFixed(1)}MB, max 5MB)`);
1048
1039
  return null;
@@ -1080,8 +1071,8 @@ async function captureScreenshot(appUrl) {
1080
1071
  // src/commands/done.ts
1081
1072
  function extractImplementationContext(storyId) {
1082
1073
  const path = storyContextPath(storyId);
1083
- if (!existsSync3(path)) return { summary: null, files: [] };
1084
- const content = readFileSync3(path, "utf-8");
1074
+ if (!existsSync4(path)) return { summary: null, files: [] };
1075
+ const content = readFileSync4(path, "utf-8");
1085
1076
  const lines = content.split("\n");
1086
1077
  const notesStart = lines.findIndex((l) => l.trim() === "## Implementation notes");
1087
1078
  const decisionsStart = lines.findIndex((l) => l.trim() === "## Decisions");
@@ -1121,8 +1112,25 @@ async function passesCriteriaGuard(story) {
1121
1112
  const proceed = await confirm(" Mark done anyway?");
1122
1113
  return { proceed, forced: proceed, blockedNonInteractive: false };
1123
1114
  }
1115
+ function prNumberFromUrl(url) {
1116
+ const m = url.match(/\/pull\/(\d+)\/?(?:[?#].*)?$/);
1117
+ return m ? Number(m[1]) : null;
1118
+ }
1124
1119
  async function done(idPrefix, opts = {}) {
1125
1120
  const config = requireConfig();
1121
+ const prUrl = opts.pr?.trim();
1122
+ if (prUrl !== void 0) {
1123
+ let valid = false;
1124
+ try {
1125
+ valid = /^https?:$/.test(new URL(prUrl).protocol);
1126
+ } catch {
1127
+ valid = false;
1128
+ }
1129
+ if (!valid) {
1130
+ console.error(` \u2717 --pr needs the pull request's URL, e.g. https://github.com/acme/app/pull/12 (got "${opts.pr}").`);
1131
+ process.exit(1);
1132
+ }
1133
+ }
1126
1134
  const data = await listStories(config);
1127
1135
  const match = data.stories.find((s) => matchesStoryRef(s, idPrefix));
1128
1136
  if (!match) {
@@ -1161,6 +1169,11 @@ async function done(idPrefix, opts = {}) {
1161
1169
  if (summary) update.implementation_summary = summary;
1162
1170
  if (files.length > 0) update.implemented_files = files;
1163
1171
  if (screenshotUrl) update.screenshot_url = screenshotUrl;
1172
+ if (prUrl) {
1173
+ update.pr_url = prUrl;
1174
+ const prNumber = prNumberFromUrl(prUrl);
1175
+ if (prNumber) update.pr_number = prNumber;
1176
+ }
1164
1177
  const result = await updateStories(config, [update]);
1165
1178
  const verify = await listStories(config);
1166
1179
  const persisted = verify.stories.find((s) => s.id === match.id);
@@ -1190,6 +1203,11 @@ async function done(idPrefix, opts = {}) {
1190
1203
  if (summary) console.log(` \u21B3 Implementation summary posted to story map`);
1191
1204
  if (files.length > 0) console.log(` \u21B3 ${files.length} file(s) recorded`);
1192
1205
  if (screenshotUrl) console.log(` \u21B3 Screenshot attached`);
1206
+ if (prUrl) {
1207
+ console.log(` \u21B3 PR linked: ${prUrl}`);
1208
+ const stamp = result.pr_stamps?.find((p) => p.id === match.id);
1209
+ if (stamp?.message) console.log(` ${stamp.message.replace(/\n/g, "\n ")}`);
1210
+ }
1193
1211
  for (const release of result.readyReleases ?? []) {
1194
1212
  console.log();
1195
1213
  console.log(`\u{1F3C1} ${release.label} is ready to close (${release.storyCount} stories done) \u2014 close it on Validate \u203A Release`);
@@ -1247,13 +1265,12 @@ async function done(idPrefix, opts = {}) {
1247
1265
  async function stop(storyId) {
1248
1266
  const config = requireConfig();
1249
1267
  const data = await listStories(config);
1250
- const match = data.stories.find(
1251
- (st) => st.id === storyId || st.id.startsWith(storyId)
1252
- );
1268
+ const match = data.stories.find((st) => matchesStoryRef(st, storyId));
1253
1269
  if (!match) {
1254
1270
  console.log(`
1255
- \u274C No story found matching ID: ${storyId}
1271
+ \u274C No story found matching "${storyId}"
1256
1272
  `);
1273
+ process.exitCode = 1;
1257
1274
  return;
1258
1275
  }
1259
1276
  if (match.status !== "in_progress") {
@@ -1617,13 +1634,24 @@ async function start() {
1617
1634
  closePrompt();
1618
1635
  return;
1619
1636
  }
1620
- const { scope: scope2 } = await import("./scope-ZZLKQEF5.js");
1637
+ const { scope: scope2 } = await import("./scope-ORIFXV4P.js");
1621
1638
  await scope2(projectId);
1622
1639
  }
1623
1640
 
1624
1641
  // src/commands/propose.ts
1625
- import { mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1626
- import { join as join4 } from "path";
1642
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, existsSync as existsSync5, appendFileSync as appendFileSync2 } from "fs";
1643
+ import { join as join5 } from "path";
1644
+ function ensureChangesIgnored(cwd = process.cwd()) {
1645
+ const gitignorePath = join5(cwd, ".gitignore");
1646
+ if (!existsSync5(gitignorePath)) return;
1647
+ const covered = readFileSync5(gitignorePath, "utf-8").split(/\r?\n/).some((line) => /^\/?intent-changes\/?(\*\*?)?$/.test(line.trim()));
1648
+ if (covered) return;
1649
+ const gi = readFileSync5(gitignorePath, "utf-8");
1650
+ appendFileSync2(gitignorePath, `${gi.endsWith("\n") || gi === "" ? "" : "\n"}
1651
+ # intentdocs local change proposals
1652
+ intent-changes/
1653
+ `);
1654
+ }
1627
1655
  async function propose(changeName, opts = {}) {
1628
1656
  await requireValidAuth();
1629
1657
  if (!hasConfig()) {
@@ -1716,8 +1744,8 @@ async function propose(changeName, opts = {}) {
1716
1744
  \u2717 Failed to create stories: ${err instanceof Error ? err.message : String(err)}`);
1717
1745
  console.log(" Saving proposal locally anyway...");
1718
1746
  }
1719
- const changesDir = join4(process.cwd(), "intent-changes", slug);
1720
- if (!existsSync4(changesDir)) mkdirSync(changesDir, { recursive: true });
1747
+ const changesDir = join5(process.cwd(), "intent-changes", slug);
1748
+ if (!existsSync5(changesDir)) mkdirSync2(changesDir, { recursive: true });
1721
1749
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1722
1750
  const proposalMd = [
1723
1751
  `# Proposal: ${changeName}`,
@@ -1752,16 +1780,9 @@ async function propose(changeName, opts = {}) {
1752
1780
  `_Add notes here as you build._`,
1753
1781
  ``
1754
1782
  ].join("\n");
1755
- writeFileSync3(join4(changesDir, "proposal.md"), proposalMd);
1783
+ writeFileSync4(join5(changesDir, "proposal.md"), proposalMd);
1756
1784
  console.log(` \u2713 Saved intent-changes/${slug}/proposal.md`);
1757
- const gitignorePath = join4(process.cwd(), ".gitignore");
1758
- if (existsSync4(gitignorePath)) {
1759
- const { readFileSync: readFileSync5, appendFileSync: appendFileSync2 } = await import("fs");
1760
- const gi = readFileSync5(gitignorePath, "utf-8");
1761
- if (!gi.includes("intent-changes")) {
1762
- appendFileSync2(gitignorePath, "\n# intentdocs local change proposals\nstc-changes/\n");
1763
- }
1764
- }
1785
+ ensureChangesIgnored();
1765
1786
  console.log(`
1766
1787
  \u2705 Proposal "${changeName}" ready!
1767
1788
 
@@ -1776,7 +1797,7 @@ async function propose(changeName, opts = {}) {
1776
1797
  closePrompt();
1777
1798
  }
1778
1799
  function parseProposalFile(filePath) {
1779
- const raw = readFileSync4(filePath, "utf-8");
1800
+ const raw = readFileSync5(filePath, "utf-8");
1780
1801
  const lines = raw.split("\n");
1781
1802
  let changeName = "";
1782
1803
  let problem = "";
@@ -1850,7 +1871,7 @@ function parseProposalFile(filePath) {
1850
1871
  return { changeName, problem, solution, activity, priority, stories: stories2 };
1851
1872
  }
1852
1873
  async function proposeFromFile(filePath, config, nameOverride) {
1853
- if (!existsSync4(filePath)) {
1874
+ if (!existsSync5(filePath)) {
1854
1875
  console.error(`
1855
1876
  File not found: ${filePath}
1856
1877
  `);
@@ -1886,8 +1907,8 @@ async function proposeFromFile(filePath, config, nameOverride) {
1886
1907
  \u2717 Failed to create stories: ${err instanceof Error ? err.message : String(err)}`);
1887
1908
  console.log(" Saving proposal locally anyway...");
1888
1909
  }
1889
- const changesDir = join4(process.cwd(), "intent-changes", slug);
1890
- if (!existsSync4(changesDir)) mkdirSync(changesDir, { recursive: true });
1910
+ const changesDir = join5(process.cwd(), "intent-changes", slug);
1911
+ if (!existsSync5(changesDir)) mkdirSync2(changesDir, { recursive: true });
1891
1912
  const proposalMd = [
1892
1913
  `# Proposal: ${changeName}`,
1893
1914
  ``,
@@ -1918,8 +1939,9 @@ async function proposeFromFile(filePath, config, nameOverride) {
1918
1939
  ].join("\n");
1919
1940
  })
1920
1941
  ].join("\n");
1921
- writeFileSync3(join4(changesDir, "proposal.md"), proposalMd);
1942
+ writeFileSync4(join5(changesDir, "proposal.md"), proposalMd);
1922
1943
  console.log(` \u2713 Saved intent-changes/${slug}/proposal.md`);
1944
+ ensureChangesIgnored();
1923
1945
  console.log(`
1924
1946
  \u2705 Done! Run \`npx intentdocs next\` to start building.
1925
1947
  `);
@@ -2291,7 +2313,7 @@ var TOOLS = [
2291
2313
  },
2292
2314
  {
2293
2315
  name: "add_note",
2294
- description: "Add implementation notes to a story. Use this to record how something was built, gotchas, or context for future developers.",
2316
+ description: "Add an implementation note to a story \u2014 appended to its existing notes, never replacing them. Use this to record how something was built, gotchas, or context for future developers.",
2295
2317
  inputSchema: {
2296
2318
  type: "object",
2297
2319
  properties: {
@@ -2467,16 +2489,6 @@ async function handleToolCall(name, args, config) {
2467
2489
  const data = await listStories(config);
2468
2490
  const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2469
2491
  if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2470
- if (match.build_sequence == null) {
2471
- return {
2472
- content: [{
2473
- type: "text",
2474
- text: `\u{1F512} "${match.title}" is unsequenced \u2014 it hasn't been ordered on the Plan view yet.
2475
-
2476
- Agents only build stories that have been deliberately sequenced. Sequence it on the Plan view (drag it into the plan or set its build order), then retry.`
2477
- }]
2478
- };
2479
- }
2480
2492
  const result = await updateStories(config, [{ id: match.id, status: "in_progress" }]);
2481
2493
  if (result.missing?.includes(match.id)) {
2482
2494
  return {
@@ -2486,8 +2498,11 @@ Agents only build stories that have been deliberately sequenced. Sequence it on
2486
2498
  }]
2487
2499
  };
2488
2500
  }
2501
+ const placed = result.sequenced?.find((x) => x.id === match.id);
2502
+ const note = placed ? `
2503
+ It wasn't in the build order, so it's been placed at the end of the build order.` : "";
2489
2504
  return {
2490
- content: [{ type: "text", text: `\u2713 ${match.title} \u2192 in_progress` }]
2505
+ content: [{ type: "text", text: `\u2713 ${match.title} \u2192 in_progress${note}` }]
2491
2506
  };
2492
2507
  }
2493
2508
  case "mark_done": {
@@ -2687,7 +2702,7 @@ Decision needed: ${decisionNeeded}`
2687
2702
  await apiFetch(config, "/stories", {
2688
2703
  method: "PATCH",
2689
2704
  headers: { "Content-Type": "application/json" },
2690
- body: JSON.stringify({ stories: [{ id: match.id, notes: note }] })
2705
+ body: JSON.stringify({ stories: [{ id: match.id, notes: appendToNotes(match.notes, note) }] })
2691
2706
  });
2692
2707
  return { content: [{ type: "text", text: `\u2713 Note added to: ${match.title}` }] };
2693
2708
  }
@@ -2961,15 +2976,15 @@ var program = new Command();
2961
2976
  program.name("intentdocs").description("Write specs, map stories, ship with AI \u2014 from your terminal").version(version);
2962
2977
  program.command("login").description("Log in to intentdocs (opens browser)").action(() => void login());
2963
2978
  program.command("logout").description("Log out of intentdocs").action(() => void logout());
2964
- var keys = program.command("keys").description("Manage your Anthropic API key \u2014 unlock AI features on any plan");
2965
- keys.command("set [key]").description("Save your Anthropic API key (prompts if omitted)").action((key) => {
2966
- import("./keys-UVXCCWCS.js").then((m) => void m.keysSet(key));
2979
+ var keys = program.command("keys").description("Manage your Anthropic (sk-ant-), xAI (xai-), or OpenAI (sk- / sk-proj-) API key \u2014 unlock AI features on any plan");
2980
+ keys.command("set [key]").description("Save an Anthropic (sk-ant-), xAI (xai-), or OpenAI (sk- / sk-proj-) API key (prompts if omitted)").action((key) => {
2981
+ import("./keys-2W5V56FS.js").then((m) => void m.keysSet(key));
2967
2982
  });
2968
2983
  keys.command("status").description("Show whether a key is set (masked)").action(() => {
2969
- import("./keys-UVXCCWCS.js").then((m) => void m.keysStatus());
2984
+ import("./keys-2W5V56FS.js").then((m) => void m.keysStatus());
2970
2985
  });
2971
2986
  keys.command("remove").description("Remove your stored key").action(() => {
2972
- import("./keys-UVXCCWCS.js").then((m) => void m.keysRemove());
2987
+ import("./keys-2W5V56FS.js").then((m) => void m.keysRemove());
2973
2988
  });
2974
2989
  program.command("start").description("Start a new project \u2014 guided flow from problem to pitch").action(() => void start());
2975
2990
  program.command("scope [projectId]").description("Scope a project \u2014 generate blueprint, story map, and mockup").action((id) => void scope(id));
@@ -3003,49 +3018,55 @@ program.command("templates [name]").description("Browse story map templates").ac
3003
3018
  program.command("projects").description("List the projects your token can reach \u2014 the whole workspace it is bound to").action(() => {
3004
3019
  import("./projects-AYAYVFKL.js").then((m) => void m.projects());
3005
3020
  });
3021
+ program.command("push").description("Upload this repo's nikOS intent/ folder: log in, create the project, and link the repo").action(() => {
3022
+ import("./push-7LJ654MJ.js").then((m) => m.push());
3023
+ });
3024
+ program.command("pull").description("Show web edits to this repo's feature files as a diff, then write the ones you agree to").option("-y, --yes", "write web changes without asking (never settles a story changed on both sides)").action((opts) => {
3025
+ import("./pull-MDI63WI7.js").then((m) => m.pull(opts));
3026
+ });
3006
3027
  program.command("init [token]").description("Connect this project to a intentdocs story map").action((token) => void init(token));
3007
3028
  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));
3008
3029
  program.command("ci").description("Set up CI test reporting \u2014 create a key, add the workflow, store the secret").option("--status", "Show how the last CI run's results matched your saved tests").option("--new", "Create a new CI key even if one already exists (rotate)").action((opts) => void ci(opts));
3009
3030
  program.command("status").description("Show story map progress").action(() => void status());
3010
3031
  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));
3011
3032
  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));
3012
- 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));
3033
+ program.command("implement <id>").description("Pick up a story and mark it in-progress (accepts its key or an ID prefix)").option("--stub", "Suggest verification commands for acceptance criteria (uses your saved key or plan; ANTHROPIC_API_KEY as a fallback)").action((id, opts) => void implement(id, opts));
3013
3034
  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));
3014
3035
  program.command("show <id>").description("Show the full spec for a story (accepts ID prefix)").action((id) => void show(id));
3015
- 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));
3016
- program.command("stop <id>").description("Stop development of an in-progress story (resets to todo, signals agents to stop)").action((id) => void stop(id));
3036
+ program.command("done <id>").description("Mark a story as done (accepts its key or an ID prefix)").option("--screenshot <path>", "Attach a screenshot of the product from an image file").option("--pr <url>", "Link the story's pull request (stamped with the intentdocs footer when GitHub is connected)").action((id, opts) => void done(id, opts));
3037
+ program.command("stop <id>").description("Stop development of an in-progress story (resets to todo, signals agents to stop; accepts its key or an ID prefix)").action((id) => void stop(id));
3017
3038
  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));
3018
- var decide = 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) => {
3019
- import("./log-XIH4BGFL.js").then((m) => void m.logDecision(title, opts));
3039
+ var decide = 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 (its key like PB-42, or an ID prefix)").action((title, opts) => {
3040
+ import("./log-VTIAG4PC.js").then((m) => void m.logDecision(title, opts));
3020
3041
  });
3021
3042
  decide.command("list").description("List logged decisions (id, category, title)").action(() => {
3022
- import("./log-XIH4BGFL.js").then((m) => void m.listLoggedDecisions());
3043
+ import("./log-VTIAG4PC.js").then((m) => void m.listLoggedDecisions());
3023
3044
  });
3024
3045
  decide.command("amend <id>").description("Amend a logged decision's title, description, or category (accepts an ID prefix)").option("-t, --title <title>", "New title").option("-d, --description <desc>", "New description").option("-c, --category <cat>", "New category: architecture, product, technical, trade-off").action((id, opts) => {
3025
- import("./log-XIH4BGFL.js").then((m) => void m.amendDecision(id, opts));
3046
+ import("./log-VTIAG4PC.js").then((m) => void m.amendDecision(id, opts));
3026
3047
  });
3027
3048
  decide.command("delete <id>").alias("retract").description("Retract a logged decision \u2014 soft-deleted and audited, not removed (accepts an ID prefix)").option("-r, --reason <reason>", "Why this decision is being retracted").option("-y, --yes", "Skip the confirmation prompt (required when there's no TTY)").action((id, opts) => {
3028
- import("./log-XIH4BGFL.js").then((m) => void m.deleteDecision(id, opts));
3049
+ import("./log-VTIAG4PC.js").then((m) => void m.deleteDecision(id, opts));
3029
3050
  });
3030
- program.command("note <storyId> [note]").description("Add implementation notes to a story").action((storyId, note) => {
3031
- import("./log-XIH4BGFL.js").then((m) => void m.logNote(storyId, note));
3051
+ program.command("note <storyId> [note]").description("Add an implementation note to a story (appended to its notes)").action((storyId, note) => {
3052
+ import("./log-VTIAG4PC.js").then((m) => void m.logNote(storyId, note));
3032
3053
  });
3033
3054
  program.command("example").description("Add a few-shot reference pattern (before/after code) to the project").action(() => void example());
3034
3055
  program.command("complexity [storyId]").description("Analyze story complexity with AI \u2014 scores, risks, effort estimates").action((id) => {
3035
- import("./complexity-X3IJKNNV.js").then((m) => void m.complexity(id));
3056
+ import("./complexity-HLJTXY55.js").then((m) => void m.complexity(id));
3036
3057
  });
3037
3058
  program.command("research [query]").description("Research a topic with AI using your project context").action((query) => {
3038
- import("./research-CO3W7PLC.js").then((m) => void m.research(query));
3059
+ import("./research-L27DLYXC.js").then((m) => void m.research(query));
3039
3060
  });
3040
- program.command("import [file]").description("Import a PRD or spec into your story map as stories").action((file) => {
3041
- import("./import-prd-IJTM6FZE.js").then((m) => void m.importPrd(file));
3061
+ program.command("import [file]").description("Import a PRD or spec into your story map as stories (reads standard input when no file is given; needs --yes then)").option("-y, --yes", "Import without asking for confirmation (required when reading from standard input)").action((file, opts) => {
3062
+ import("./import-prd-FQI4J45M.js").then((m) => void m.importPrd(file, opts));
3042
3063
  });
3043
3064
  var map = program.command("map").description("Import or extract story maps from codebases");
3044
3065
  map.command("extract").description("Output a prompt for your coding agent to extract a story map from an existing codebase").action(() => {
3045
- import("./map-extract-V6D27I3H.js").then((m) => void m.mapExtract());
3066
+ import("./map-extract-4C44YGGD.js").then((m) => void m.mapExtract());
3046
3067
  });
3047
3068
  map.command("import <file>").description("Import a storymap.json file generated by your coding agent").option("--yes", "Skip confirmation prompt").action((file, opts) => {
3048
- import("./map-import-LORTI4K4.js").then((m) => void m.mapImport(file, opts));
3069
+ import("./map-import-TSSDU766.js").then((m) => void m.mapImport(file, opts));
3049
3070
  });
3050
3071
  map.command("schema").description("Print the JSON Schema for storymap.json").action(() => {
3051
3072
  import("./map-schema-5BEIJM2J.js").then((m) => void m.mapSchema());