intentdocs 0.8.0 → 0.10.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.
Files changed (34) hide show
  1. package/README.md +2 -1
  2. package/bin/intentdocs.js +2 -0
  3. package/dist/{chunk-4OWPN2RE.js → chunk-5USVG4DW.js} +24 -43
  4. package/dist/{chunk-IKF3ZLHK.js → chunk-EEKEF2FD.js} +65 -101
  5. package/dist/chunk-HEHHSSUM.js +43 -0
  6. package/dist/chunk-IDNUBI2P.js +317 -0
  7. package/dist/chunk-LQTPZL2Y.js +15 -0
  8. package/dist/chunk-LXQXJULN.js +105 -0
  9. package/dist/{chunk-6FGYJ3W2.js → chunk-NFIXRIHM.js} +43 -4
  10. package/dist/{chunk-ICOMMHKG.js → chunk-UF535HZL.js} +12 -1
  11. package/dist/{chunk-ITQUQYWB.js → chunk-UR73WX5I.js} +1 -1
  12. package/dist/chunk-UYTVTYNB.js +11 -0
  13. package/dist/{chunk-WPVDURTJ.js → chunk-WRRUCCVZ.js} +27 -3
  14. package/dist/chunk-YUW3W43S.js +170 -0
  15. package/dist/ci-LBOCMRKZ.js +11 -0
  16. package/dist/{complexity-NSSYVT54.js → complexity-ZOB3BPVD.js} +12 -7
  17. package/dist/{import-prd-CKV3R67G.js → import-prd-MJUCIMHX.js} +13 -11
  18. package/dist/index.js +420 -179
  19. package/dist/{keys-EDWET3WG.js → keys-5PA3D6CX.js} +8 -7
  20. package/dist/log-EVLWM64C.js +206 -0
  21. package/dist/{map-extract-I3AACTGL.js → map-extract-W6WJLVOF.js} +2 -1
  22. package/dist/{map-import-IYUMRASO.js → map-import-LL4AIAQE.js} +12 -10
  23. package/dist/{map-schema-JVL3O2TJ.js → map-schema-6GL2UKZQ.js} +1 -1
  24. package/dist/projects-F7GMH66U.js +37 -0
  25. package/dist/{prompt-WAWCRGHN.js → prompt-GOAFIKBZ.js} +3 -1
  26. package/dist/{research-6SXJ37TI.js → research-4ASCZSZN.js} +12 -10
  27. package/dist/scope-S3LZZ3KB.js +11 -0
  28. package/dist/setup-YLUPBLK6.js +14 -0
  29. package/package.json +4 -2
  30. package/dist/chunk-6XKB6RTU.js +0 -123
  31. package/dist/chunk-PCNDP5SX.js +0 -34
  32. package/dist/log-TT2UCJP7.js +0 -94
  33. package/dist/scope-OGKQ472D.js +0 -10
  34. package/dist/setup-SOI2HQAL.js +0 -11
package/dist/index.js CHANGED
@@ -1,53 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- captureCli,
3
+ UPGRADE_MESSAGE_PLAIN,
4
+ isPlanGated
5
+ } from "./chunk-UR73WX5I.js";
6
+ import {
7
+ ci
8
+ } from "./chunk-IDNUBI2P.js";
9
+ import {
4
10
  setup,
5
- shutdownCliAnalytics,
6
11
  sync
7
- } from "./chunk-IKF3ZLHK.js";
12
+ } from "./chunk-EEKEF2FD.js";
13
+ import {
14
+ captureCli,
15
+ shutdownCliAnalytics
16
+ } from "./chunk-HEHHSSUM.js";
8
17
  import {
9
18
  scope
10
- } from "./chunk-4OWPN2RE.js";
19
+ } from "./chunk-5USVG4DW.js";
11
20
  import {
12
21
  criterionPriority,
13
22
  criterionStatusLabel,
14
23
  findBlockingMusts,
24
+ formatDesignRefLine,
15
25
  storyContextPath,
16
26
  writeStoryContext
17
- } from "./chunk-6FGYJ3W2.js";
27
+ } from "./chunk-NFIXRIHM.js";
18
28
  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";
29
+ matchesStoryRef,
30
+ storyRef
31
+ } from "./chunk-LQTPZL2Y.js";
28
32
  import {
33
+ amendDecision,
29
34
  apiFetch,
30
35
  apiUrl,
31
36
  createStories,
32
37
  getContext,
38
+ listDecisions,
33
39
  listStories,
34
40
  raiseBlocker,
35
41
  reportCriteria,
42
+ retractDecision,
43
+ syncHeaders,
36
44
  updateStories,
37
45
  uploadScreenshot
38
- } from "./chunk-6XKB6RTU.js";
46
+ } from "./chunk-YUW3W43S.js";
39
47
  import {
40
48
  hasConfig,
41
49
  readConfig,
42
50
  requireConfig,
43
51
  writeConfig
44
- } from "./chunk-ICOMMHKG.js";
52
+ } from "./chunk-UF535HZL.js";
53
+ import {
54
+ ask,
55
+ closePrompt,
56
+ confirm,
57
+ isInteractive,
58
+ pausePrompt
59
+ } from "./chunk-WRRUCCVZ.js";
45
60
  import {
46
- API_BASE,
61
+ authenticatedFetch,
62
+ bindingLabel,
63
+ clearAuth,
47
64
  getAuth,
48
- requireAuth,
49
- saveAuth
50
- } from "./chunk-PCNDP5SX.js";
65
+ requireValidAuth,
66
+ saveAuth,
67
+ validateAuth
68
+ } from "./chunk-LXQXJULN.js";
69
+ import {
70
+ API_BASE
71
+ } from "./chunk-UYTVTYNB.js";
51
72
 
52
73
  // src/index.ts
53
74
  import { Command } from "commander";
@@ -60,6 +81,27 @@ import { createInterface } from "readline";
60
81
 
61
82
  // src/commands/login.ts
62
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");
99
+ }
100
+ console.log("You can now run: intentdocs init");
101
+ console.log(
102
+ "Optional: add your Anthropic API key to unlock AI features on any plan \u2014 intentdocs keys set"
103
+ );
104
+ }
63
105
  function openBrowser(url) {
64
106
  const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
65
107
  exec(cmd);
@@ -70,8 +112,24 @@ function sleep(ms) {
70
112
  async function login() {
71
113
  const existing = getAuth();
72
114
  if (existing) {
73
- console.log("Already logged in. Run `intent logout` to switch accounts.");
74
- return;
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();
75
133
  }
76
134
  console.log("Logging in to intentdocs...\n");
77
135
  const res = await fetch(`${API_BASE}/api/cli/auth`, { method: "POST" });
@@ -85,38 +143,83 @@ async function login() {
85
143
  console.log(`If it doesn't open, visit: ${authUrl}
86
144
  `);
87
145
  openBrowser(authUrl);
88
- const maxAttempts = 60;
89
- for (let i = 0; i < maxAttempts; i++) {
90
- await sleep(3e3);
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;
91
165
  try {
92
166
  const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
93
167
  if (!checkRes.ok) continue;
94
168
  const data = await checkRes.json();
95
169
  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
- );
170
+ finishLogin(data);
108
171
  return;
109
172
  }
110
173
  if (data.status === "expired") {
111
- console.error("Login request expired. Run `intent login` to try again.");
174
+ console.error("\n\nThe server-side login request expired. Run `intentdocs login` to start a new one.");
112
175
  process.exit(1);
113
176
  }
114
- process.stdout.write(".");
115
177
  } catch {
116
- process.stdout.write(".");
117
178
  }
118
179
  }
119
- console.error("\nLogin timed out. Run `intent login` to try again.");
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 `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 `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");
120
223
  process.exit(1);
121
224
  }
122
225
  async function logout() {
@@ -133,6 +236,7 @@ async function logout() {
133
236
  }
134
237
 
135
238
  // 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 `intent 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";
136
240
  function ask2(rl, question) {
137
241
  return new Promise((resolve) => rl.question(question, resolve));
138
242
  }
@@ -148,9 +252,6 @@ ${prompt} [1-${items.length}]: `);
148
252
  console.log(` Please enter a number between 1 and ${items.length}.`);
149
253
  }
150
254
  }
151
- function authHeaders(token) {
152
- return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
153
- }
154
255
  function detectStack(cwd) {
155
256
  try {
156
257
  if (existsSync(join(cwd, "package.json"))) {
@@ -179,12 +280,12 @@ function detectStack(cwd) {
179
280
  }
180
281
  async function init(tokenArg) {
181
282
  if (hasConfig()) {
182
- console.log("Already initialized. Run `intent sync` to refresh.");
283
+ console.log("Already initialized. Run `intentdocs sync` to refresh.");
183
284
  return;
184
285
  }
185
286
  if (tokenArg) {
186
- await initWithSyncToken(tokenArg);
187
- return;
287
+ console.error("Sync tokens have been retired \u2014 `intentdocs init` no longer takes one. Run `intentdocs login`, then `intentdocs init` and pick the project and map.");
288
+ process.exit(1);
188
289
  }
189
290
  let auth = getAuth();
190
291
  if (!auth) {
@@ -200,24 +301,23 @@ async function init(tokenArg) {
200
301
  const initStart = Date.now();
201
302
  try {
202
303
  console.log("\nFetching your projects...");
203
- const projectsRes = await fetch(`${auth.apiBase}/api/cli/projects`, {
204
- headers: authHeaders(auth.token)
205
- });
304
+ const projectsRes = await authenticatedFetch("/api/cli/projects");
206
305
  if (!projectsRes.ok) {
207
- console.error("Failed to fetch projects. Try `intent login` first.");
306
+ console.error("Failed to fetch projects. The server returned an error \u2014 try again or check intentdocs.com for status.");
208
307
  process.exit(1);
209
308
  }
210
- const { projects } = await projectsRes.json();
309
+ const { projects, workspace: whereIAm } = await projectsRes.json();
211
310
  if (projects.length === 0) {
212
311
  console.log("\nNo projects found. Create one at intentdocs.com first.");
213
312
  process.exit(0);
214
313
  }
314
+ if (whereIAm) console.log(`
315
+ ${whereIAm}`);
215
316
  console.log("\nYour projects:\n");
216
317
  const project = await pickFromList(rl, projects, (p) => p.name, "Select project");
217
318
  console.log("\nFetching story maps...");
218
- const mapsRes = await fetch(
219
- `${auth.apiBase}/api/cli/projects/${project.id}/maps`,
220
- { headers: authHeaders(auth.token) }
319
+ const mapsRes = await authenticatedFetch(
320
+ `/api/cli/projects/${project.id}/maps`
221
321
  );
222
322
  if (!mapsRes.ok) {
223
323
  console.error("Failed to fetch story maps.");
@@ -233,15 +333,15 @@ Story maps in "${project.name}":
233
333
  `);
234
334
  const map2 = await pickFromList(rl, maps, (m) => m.title, "Select story map");
235
335
  const config = {
236
- syncToken: map2.syncToken,
237
336
  apiBase: auth.apiBase,
238
337
  mapTitle: map2.title,
239
338
  projectId: project.id,
240
339
  mapId: map2.id,
340
+ workspace: project.workspace ? { orgId: project.workspace.orgId, name: project.workspace.name } : { orgId: null, name: null },
241
341
  ...appUrl && { appUrl }
242
342
  };
243
343
  console.log(`
244
- Connecting to "${map2.title}"...`);
344
+ Connecting to "${map2.title}"${project.workspace ? ` in ${project.workspace.name ?? "its workspace"}` : ""}...`);
245
345
  try {
246
346
  const data = await listStories(config);
247
347
  console.log(`\u2713 Connected to "${data.map.title}"`);
@@ -273,8 +373,16 @@ Connecting to "${map2.title}"...`);
273
373
  intent status \u2014 see your story map
274
374
  intent next \u2014 pick up the first story
275
375
  intent setup \u2014 configure Claude Code / Cursor slash commands
376
+ intent ci \u2014 report your tests' results from CI to each story
276
377
  intent propose \u2014 plan a new feature from the terminal
277
378
  `);
379
+ const setUpCi = await ask2(rl, " Set up CI test reporting now? It creates a key and adds the workflow. (y/N): ");
380
+ if (/^y(es)?$/i.test(setUpCi.trim())) {
381
+ const { ci: ci2 } = await import("./ci-LBOCMRKZ.js");
382
+ await ci2();
383
+ } else {
384
+ console.log(" You can run `intent ci` any time to turn it on.\n");
385
+ }
278
386
  } finally {
279
387
  rl.close();
280
388
  await shutdownCliAnalytics();
@@ -327,34 +435,6 @@ _Add important files and what they do as you build._
327
435
  writeFileSync(archPath, archContent);
328
436
  console.log(` \u2713 Created ARCHITECTURE.md with your stack (edit it as you build)`);
329
437
  }
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
438
  async function setupFiles(config) {
359
439
  try {
360
440
  const md = await getContext(config);
@@ -375,10 +455,7 @@ async function setupFiles(config) {
375
455
  if (existsSync(join(cwd, "CLAUDE.md"))) {
376
456
  const claudeMd = readFileSync(join(cwd, "CLAUDE.md"), "utf-8");
377
457
  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
- );
458
+ appendFileSync(join(cwd, "CLAUDE.md"), INTENTDOCS_CLAUDE_BLOCK);
382
459
  console.log("\u2713 Added INTENT.md reference to CLAUDE.md");
383
460
  }
384
461
  }
@@ -508,7 +585,7 @@ function listView(stories2) {
508
585
  ${activity} (${done2}/${actStories.length})`);
509
586
  for (const s of actStories) {
510
587
  const icon = STATUS_ICON[s.status] ?? "\u2B1C";
511
- const id = s.id.slice(0, 8);
588
+ const id = storyRef(s);
512
589
  console.log(` ${icon} ${s.title.slice(0, 70)} (${id})`);
513
590
  }
514
591
  }
@@ -556,7 +633,7 @@ function kanbanView(stories2) {
556
633
  row(
557
634
  cols.map((c) => {
558
635
  const s = c.stories[i];
559
- return s ? cell(s.id.slice(0, 8)) : emptyCell();
636
+ return s ? cell(storyRef(s)) : emptyCell();
560
637
  })
561
638
  )
562
639
  );
@@ -615,18 +692,32 @@ function parseVerificationSuggestions(raw) {
615
692
  // src/commands/next.ts
616
693
  var PRIORITY_ORDER = { must: 0, should: 1, could: 2 };
617
694
  var EFFORT_ORDER = { S: 0, M: 1, L: 2, XL: 3 };
695
+ function unsequencedRefusal(story, apiBase, mapId) {
696
+ const planUrl = `${apiBase}/storymapper/${mapId}?phase=plan&story=${story.id}`;
697
+ 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:
702
+ \u2192 ${planUrl}
703
+ `;
704
+ }
618
705
  async function next(opts = {}) {
619
706
  const config = requireConfig();
620
707
  const data = await listStories(config);
621
708
  let s;
622
709
  if (opts.storyId) {
623
- const match = data.stories.find((st) => st.id === opts.storyId || st.id.startsWith(opts.storyId));
710
+ const match = data.stories.find((st) => matchesStoryRef(st, opts.storyId));
624
711
  if (!match) {
625
712
  console.log(`
626
713
  \u274C No story found matching ID: ${opts.storyId}
627
714
  `);
628
715
  return;
629
716
  }
717
+ if (match.build_sequence == null) {
718
+ console.log(unsequencedRefusal(match, config.apiBase, data.map.id));
719
+ return;
720
+ }
630
721
  s = match;
631
722
  } else {
632
723
  if (!opts.skipWip) {
@@ -640,7 +731,7 @@ async function next(opts = {}) {
640
731
  console.log(` ID: ${ip.id.slice(0, 8)} | Mark done: intent done ${ip.id.slice(0, 8)}`);
641
732
  }
642
733
  console.log(`
643
- Finish those first, or run \`intent next --skip-wip\` to pick the next todo anyway.
734
+ Finish those first, or run \`intentdocs next --skip-wip\` to pick the next todo anyway.
644
735
  `);
645
736
  return;
646
737
  }
@@ -648,12 +739,13 @@ async function next(opts = {}) {
648
739
  const doneIds = new Set(
649
740
  data.stories.filter((st) => st.status === "done").map((st) => st.id)
650
741
  );
651
- const todos = data.stories.filter((st) => st.status === "todo").filter((st) => {
742
+ const allTodos = data.stories.filter((st) => st.status === "todo");
743
+ const todos = allTodos.filter((st) => st.build_sequence != null).filter((st) => {
652
744
  if (!st.depends_on?.length) return true;
653
745
  return st.depends_on.every((depId) => doneIds.has(depId));
654
746
  }).sort((a, b) => {
655
- const aSeq = a.build_sequence ?? Infinity;
656
- const bSeq = b.build_sequence ?? Infinity;
747
+ const aSeq = a.build_sequence;
748
+ const bSeq = b.build_sequence;
657
749
  if (aSeq !== bSeq) return aSeq - bSeq;
658
750
  if (a.release !== b.release) return a.release - b.release;
659
751
  const pa = PRIORITY_ORDER[a.priority] ?? 9;
@@ -662,11 +754,17 @@ async function next(opts = {}) {
662
754
  return (EFFORT_ORDER[a.effort] ?? 9) - (EFFORT_ORDER[b.effort] ?? 9);
663
755
  });
664
756
  if (todos.length === 0) {
665
- const allTodos = data.stories.filter((st) => st.status === "todo");
666
- if (allTodos.length > 0) {
757
+ const unsequencedCount = allTodos.filter((st) => st.build_sequence == null).length;
758
+ const blockedCount = allTodos.length - unsequencedCount;
759
+ if (unsequencedCount > 0) {
667
760
  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.
761
+ \u{1F512} ${unsequencedCount} stor${unsequencedCount === 1 ? "y is" : "ies are"} unsequenced \u2014 agents only build stories you've deliberately ordered.`);
762
+ console.log(` Sequence ${unsequencedCount === 1 ? "it" : "them"} on the Plan view, then run \`intentdocs next\` again.
763
+ `);
764
+ } else if (blockedCount > 0) {
765
+ console.log(`
766
+ \u{1F512} ${blockedCount} stories remain but all have unfinished dependencies.`);
767
+ console.log(` Finish in-progress stories first, then run \`intentdocs next\` again.
670
768
  `);
671
769
  } else {
672
770
  console.log("\n \u{1F389} All stories are done! Ship it.\n");
@@ -757,7 +855,7 @@ async function generateVerificationSuggestions(story, contextPath) {
757
855
  `);
758
856
  return;
759
857
  }
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._`, ``];
858
+ const lines = [`# Suggested verification commands: ${story.title}`, ``, `_Generated by \`intentdocs next --stub\` \u2014 copy into the criterion's command field, or run \`intentdocs check\` once wired up._`, ``];
761
859
  console.log();
762
860
  for (const suggestion of suggestions) {
763
861
  const ac = candidates[suggestion.index - 1];
@@ -816,6 +914,10 @@ async function implement(storyId, opts = {}) {
816
914
  `);
817
915
  return;
818
916
  }
917
+ if (s.build_sequence == null) {
918
+ console.log(unsequencedRefusal(s, config.apiBase, data.map.id));
919
+ return;
920
+ }
819
921
  const doneIds = new Set(
820
922
  data.stories.filter((st) => st.status === "done").map((st) => st.id)
821
923
  );
@@ -853,7 +955,7 @@ async function implement(storyId, opts = {}) {
853
955
  console.log(` \u2502 intent sync --context refresh CLAUDE.md first \u2502`);
854
956
  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
957
  `);
856
- const proceed = await confirm(" Assign this story and mark in progress?");
958
+ const proceed = isInteractive() ? await confirm(" Assign this story and mark in progress?") : false;
857
959
  if (!proceed) {
858
960
  console.log(`
859
961
  Skipped.
@@ -999,27 +1101,30 @@ function extractImplementationContext(storyId) {
999
1101
  return { summary, files };
1000
1102
  }
1001
1103
  async function passesCriteriaGuard(story) {
1002
- if (story.acceptance_criteria.length === 0) return { proceed: true, forced: false };
1104
+ if (story.acceptance_criteria.length === 0) return { proceed: true, forced: false, blockedNonInteractive: false };
1003
1105
  console.log();
1004
1106
  console.log(" Acceptance criteria:");
1005
1107
  for (const ac of story.acceptance_criteria) {
1006
1108
  console.log(` [${criterionPriority(ac)}] [${criterionStatusLabel(ac)}] ${ac.text}`);
1007
1109
  }
1008
1110
  const blocking = findBlockingMusts(story.acceptance_criteria);
1009
- if (blocking.length === 0) return { proceed: true, forced: false };
1111
+ if (blocking.length === 0) return { proceed: true, forced: false, blockedNonInteractive: false };
1010
1112
  console.log();
1011
1113
  console.log(` ${blocking.length} 'must' criterion/criteria still failing or unreported:`);
1012
1114
  for (const ac of blocking) {
1013
1115
  console.log(` - [${criterionStatusLabel(ac)}] ${ac.text}`);
1014
1116
  }
1015
1117
  console.log();
1118
+ if (!isInteractive()) {
1119
+ return { proceed: false, forced: false, blockedNonInteractive: true };
1120
+ }
1016
1121
  const proceed = await confirm(" Mark done anyway?");
1017
- return { proceed, forced: proceed };
1122
+ return { proceed, forced: proceed, blockedNonInteractive: false };
1018
1123
  }
1019
1124
  async function done(idPrefix, opts = {}) {
1020
1125
  const config = requireConfig();
1021
1126
  const data = await listStories(config);
1022
- const match = data.stories.find((s) => s.id.startsWith(idPrefix));
1127
+ const match = data.stories.find((s) => matchesStoryRef(s, idPrefix));
1023
1128
  if (!match) {
1024
1129
  console.error(`No story found matching "${idPrefix}"`);
1025
1130
  process.exit(1);
@@ -1031,6 +1136,13 @@ async function done(idPrefix, opts = {}) {
1031
1136
  const guard = await passesCriteriaGuard(match);
1032
1137
  if (!guard.proceed) {
1033
1138
  closePrompt();
1139
+ if (guard.blockedNonInteractive) {
1140
+ console.error(
1141
+ ` \u2717 Not marked done: "${match.title}" has unresolved 'must' criteria and no TTY is available to confirm an override.`
1142
+ );
1143
+ console.error(` Report the criteria (intent check / report_criteria) or re-run interactively to force it.`);
1144
+ process.exit(1);
1145
+ }
1034
1146
  return;
1035
1147
  }
1036
1148
  const { summary, files } = extractImplementationContext(match.id);
@@ -1050,6 +1162,17 @@ async function done(idPrefix, opts = {}) {
1050
1162
  if (files.length > 0) update.implemented_files = files;
1051
1163
  if (screenshotUrl) update.screenshot_url = screenshotUrl;
1052
1164
  const result = await updateStories(config, [update]);
1165
+ const verify = await listStories(config);
1166
+ const persisted = verify.stories.find((s) => s.id === match.id);
1167
+ if (persisted?.status !== "done") {
1168
+ console.error(
1169
+ ` \u2717 Done-mark did not persist: server reports status="${persisted?.status ?? "not found"}" for "${match.title}" after the update.`
1170
+ );
1171
+ console.error(` Not treating this run as successful \u2014 re-run \`intent done ${idPrefix}\` or check the sync API.`);
1172
+ await shutdownCliAnalytics();
1173
+ closePrompt();
1174
+ process.exit(1);
1175
+ }
1053
1176
  const effortToComplexity = (e) => e === "XL" || e === "L" ? "high" : e === "M" ? "med" : "low";
1054
1177
  captureCli(
1055
1178
  "story_shipped",
@@ -1067,12 +1190,12 @@ async function done(idPrefix, opts = {}) {
1067
1190
  if (summary) console.log(` \u21B3 Implementation summary posted to story map`);
1068
1191
  if (files.length > 0) console.log(` \u21B3 ${files.length} file(s) recorded`);
1069
1192
  if (screenshotUrl) console.log(` \u21B3 Screenshot attached`);
1070
- for (const release of result.completedReleases ?? []) {
1193
+ for (const release of result.readyReleases ?? []) {
1071
1194
  console.log();
1072
- console.log(`\u{1F680} Release shipped: ${release.label} (${release.storyCount} stories)`);
1195
+ console.log(`\u{1F3C1} ${release.label} is ready to close (${release.storyCount} stories done) \u2014 close it on Validate \u203A Release`);
1073
1196
  }
1074
1197
  console.log();
1075
- const wantsDecision = await confirm(" Log a decision or lesson from this story?");
1198
+ const wantsDecision = isInteractive() ? await confirm(" Log a decision or lesson from this story?") : false;
1076
1199
  if (wantsDecision) {
1077
1200
  const title = await ask(" Decision title: ");
1078
1201
  const description = await ask(" What was decided / learned: ");
@@ -1103,16 +1226,19 @@ async function done(idPrefix, opts = {}) {
1103
1226
  }
1104
1227
  }
1105
1228
  }
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);
1229
+ try {
1230
+ await apiFetch(config, "/activity", {
1231
+ method: "POST",
1232
+ headers: { "Content-Type": "application/json" },
1233
+ body: JSON.stringify({
1234
+ event_type: "story_done",
1235
+ summary: `Marked done: ${match.title}`,
1236
+ metadata: { story_id: match.id, ...screenshotUrl ? { screenshot_url: screenshotUrl } : {} },
1237
+ source: "cli"
1238
+ })
1239
+ });
1240
+ } catch {
1241
+ }
1116
1242
  await shutdownCliAnalytics();
1117
1243
  closePrompt();
1118
1244
  }
@@ -1188,7 +1314,7 @@ function isRunnable(ac) {
1188
1314
  }
1189
1315
  function resolveStory(stories2, idPrefix) {
1190
1316
  if (idPrefix) {
1191
- const story = stories2.find((s) => s.id.startsWith(idPrefix));
1317
+ const story = stories2.find((s) => matchesStoryRef(s, idPrefix));
1192
1318
  return story ? { story } : { error: `No story found matching "${idPrefix}"` };
1193
1319
  }
1194
1320
  const inProgress = stories2.filter((s) => s.status === "in_progress");
@@ -1292,18 +1418,8 @@ function printBanner(subtitle) {
1292
1418
 
1293
1419
  // src/commands/start.ts
1294
1420
  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, {
1421
+ async function streamResponse(path, body, options) {
1422
+ const res = await authenticatedFetch(path, {
1307
1423
  method: "POST",
1308
1424
  body: JSON.stringify(body)
1309
1425
  });
@@ -1331,8 +1447,7 @@ async function streamResponse(path, token, body, options) {
1331
1447
  return full;
1332
1448
  }
1333
1449
  async function start() {
1334
- const auth = requireAuth();
1335
- const { token } = auth;
1450
+ await requireValidAuth();
1336
1451
  printBanner("Start a new project");
1337
1452
  console.log(" Let's start with the problem you want to solve.\n");
1338
1453
  const problem = await ask(" What problem are you trying to solve?\n > ");
@@ -1345,13 +1460,13 @@ async function start() {
1345
1460
  const spinner = ora(" Thinking about your idea...").start();
1346
1461
  let discoveryText;
1347
1462
  try {
1348
- discoveryText = await streamResponse("/api/blueprint/discovery", token, {
1463
+ discoveryText = await streamResponse("/api/blueprint/discovery", {
1349
1464
  idea: problem
1350
1465
  }, { silent: true });
1351
1466
  } catch (err) {
1352
1467
  spinner.fail(" Failed to connect");
1353
1468
  console.error(` ${err.message}`);
1354
- console.error(" Make sure you're logged in: intent login");
1469
+ console.error(" Make sure you're logged in: intentdocs login");
1355
1470
  closePrompt();
1356
1471
  return;
1357
1472
  }
@@ -1368,7 +1483,7 @@ async function start() {
1368
1483
  }
1369
1484
  pausePrompt();
1370
1485
  const pitchSpinner = ora(" Synthesising your pitch...").start();
1371
- const pitchRes = await apiFetch2("/api/blueprint/synthesize-pitch", token, {
1486
+ const pitchRes = await authenticatedFetch("/api/blueprint/synthesize-pitch", {
1372
1487
  method: "POST",
1373
1488
  body: JSON.stringify({ problem, qa })
1374
1489
  });
@@ -1404,7 +1519,7 @@ async function start() {
1404
1519
  Great \u2014 "${productName}" it is.
1405
1520
  `);
1406
1521
  console.log(" Creating project...");
1407
- const createRes = await apiFetch2("/api/blueprint/create", token, {
1522
+ const createRes = await authenticatedFetch("/api/blueprint/create", {
1408
1523
  method: "POST",
1409
1524
  body: JSON.stringify({
1410
1525
  messages: [{ role: "user", content: problem }],
@@ -1423,14 +1538,14 @@ async function start() {
1423
1538
  Failed to create project: ${data.error ?? "unknown error"}`);
1424
1539
  }
1425
1540
  } else {
1426
- console.error("\n Failed to create project. Try logging in again: intent login");
1541
+ console.error("\n Failed to create project. Try logging in again: intentdocs login");
1427
1542
  }
1428
1543
  closePrompt();
1429
1544
  return;
1430
1545
  }
1431
1546
  const createContentType = createRes.headers.get("content-type") ?? "";
1432
1547
  if (!createContentType.includes("application/json")) {
1433
- console.error("\n Unexpected response from server. Try logging in again: intent login");
1548
+ console.error("\n Unexpected response from server. Try logging in again: intentdocs login");
1434
1549
  closePrompt();
1435
1550
  return;
1436
1551
  }
@@ -1461,9 +1576,8 @@ async function start() {
1461
1576
  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
1577
  const confirmPublish = await confirm(" Publish this to Explore?");
1463
1578
  if (confirmPublish) {
1464
- const pubRes = await apiFetch2(
1579
+ const pubRes = await authenticatedFetch(
1465
1580
  `/api/projects/${projectId}/publish`,
1466
- token,
1467
1581
  {
1468
1582
  method: "POST",
1469
1583
  body: JSON.stringify({
@@ -1503,7 +1617,7 @@ async function start() {
1503
1617
  closePrompt();
1504
1618
  return;
1505
1619
  }
1506
- const { scope: scope2 } = await import("./scope-OGKQ472D.js");
1620
+ const { scope: scope2 } = await import("./scope-S3LZZ3KB.js");
1507
1621
  await scope2(projectId);
1508
1622
  }
1509
1623
 
@@ -1511,9 +1625,9 @@ async function start() {
1511
1625
  import { mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1512
1626
  import { join as join4 } from "path";
1513
1627
  async function propose(changeName, opts = {}) {
1514
- requireAuth();
1628
+ await requireValidAuth();
1515
1629
  if (!hasConfig()) {
1516
- console.log(" Not connected to a project. Run `intent init` first.");
1630
+ console.log(" Not connected to a project. Run `intentdocs init` first.");
1517
1631
  closePrompt();
1518
1632
  return;
1519
1633
  }
@@ -1807,7 +1921,7 @@ async function proposeFromFile(filePath, config, nameOverride) {
1807
1921
  writeFileSync3(join4(changesDir, "proposal.md"), proposalMd);
1808
1922
  console.log(` \u2713 Saved intent-changes/${slug}/proposal.md`);
1809
1923
  console.log(`
1810
- \u2705 Done! Run \`intent next\` to start building.
1924
+ \u2705 Done! Run \`intentdocs next\` to start building.
1811
1925
  `);
1812
1926
  }
1813
1927
 
@@ -1820,11 +1934,11 @@ var STATUS_ICON2 = {
1820
1934
  async function show(idPrefix) {
1821
1935
  const config = requireConfig();
1822
1936
  const data = await listStories(config);
1823
- const story = data.stories.find((s) => s.id.startsWith(idPrefix));
1937
+ const story = data.stories.find((s) => matchesStoryRef(s, idPrefix));
1824
1938
  if (!story) {
1825
1939
  console.error(`
1826
1940
  No story found matching "${idPrefix}"
1827
- Run \`intent stories\` to list IDs.
1941
+ Run \`intentdocs stories\` to list IDs.
1828
1942
  `);
1829
1943
  process.exit(1);
1830
1944
  }
@@ -1853,6 +1967,13 @@ async function show(idPrefix) {
1853
1967
  console.log(` ${check2} ${ac.text}`);
1854
1968
  }
1855
1969
  }
1970
+ if (story.design_refs && story.design_refs.length > 0) {
1971
+ console.log(`
1972
+ Design references:`);
1973
+ for (const ref of story.design_refs) {
1974
+ console.log(` ${formatDesignRefLine(ref)}`);
1975
+ }
1976
+ }
1856
1977
  if (story.notes) {
1857
1978
  console.log(`
1858
1979
  Notes:`);
@@ -1871,7 +1992,7 @@ async function show(idPrefix) {
1871
1992
  const relPath = contextPath.replace(process.cwd() + "/", "");
1872
1993
  console.log(`
1873
1994
  Context : ${relPath}`);
1874
- console.log(` Mark done: intent done ${story.id.slice(0, 8)}
1995
+ console.log(` Mark done: intent done ${storyRef(story)}
1875
1996
  `);
1876
1997
  }
1877
1998
 
@@ -1920,7 +2041,7 @@ async function example() {
1920
2041
  console.log(`
1921
2042
  \u2713 Example saved: "${data.title}"
1922
2043
  `);
1923
- console.log(" Run `intent sync --context` to regenerate your CLAUDE.md with this pattern.\n");
2044
+ console.log(" Run `intentdocs sync --context` to regenerate your CLAUDE.md with this pattern.\n");
1924
2045
  } catch {
1925
2046
  rl.close();
1926
2047
  throw new Error("Aborted");
@@ -2222,6 +2343,38 @@ var TOOLS = [
2222
2343
  },
2223
2344
  mode: "standard"
2224
2345
  },
2346
+ {
2347
+ name: "amend_decision",
2348
+ description: "Amend a previously logged decision's title, description, or category. Use when you got a decision wrong or need to update it after new information. Cannot amend a decision that's been retracted.",
2349
+ inputSchema: {
2350
+ type: "object",
2351
+ properties: {
2352
+ decision_id: { type: "string", description: "Decision ID (full UUID or prefix)" },
2353
+ title: { type: "string", description: "New title" },
2354
+ description: { type: "string", description: "New description" },
2355
+ category: {
2356
+ type: "string",
2357
+ enum: ["architecture", "product", "technical", "trade-off"],
2358
+ description: "New category"
2359
+ }
2360
+ },
2361
+ required: ["decision_id"]
2362
+ },
2363
+ mode: "standard"
2364
+ },
2365
+ {
2366
+ name: "retract_decision",
2367
+ description: "Retract a previously logged decision \u2014 a soft, audited removal (marked retracted with a timestamp and optional reason), never a hard delete. Use when a decision was made in error or no longer applies.",
2368
+ inputSchema: {
2369
+ type: "object",
2370
+ properties: {
2371
+ decision_id: { type: "string", description: "Decision ID (full UUID or prefix)" },
2372
+ reason: { type: "string", description: "Why this decision is being retracted" }
2373
+ },
2374
+ required: ["decision_id"]
2375
+ },
2376
+ mode: "standard"
2377
+ },
2225
2378
  // ── All tools (~15k tokens) ─────────────────────────────────────────────
2226
2379
  {
2227
2380
  name: "analyze_complexity",
@@ -2275,7 +2428,7 @@ async function handleToolCall(name, args, config) {
2275
2428
  const storyId = args?.story_id;
2276
2429
  if (storyId) {
2277
2430
  const data = await listStories(config);
2278
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2431
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2279
2432
  if (match?.cancelled_at) {
2280
2433
  return {
2281
2434
  content: [{
@@ -2312,9 +2465,27 @@ async function handleToolCall(name, args, config) {
2312
2465
  const storyId = args?.story_id;
2313
2466
  if (!storyId) return { content: [{ type: "text", text: "story_id is required" }] };
2314
2467
  const data = await listStories(config);
2315
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2468
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2316
2469
  if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2317
- await updateStories(config, [{ id: match.id, status: "in_progress" }]);
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
+ const result = await updateStories(config, [{ id: match.id, status: "in_progress" }]);
2481
+ if (result.missing?.includes(match.id)) {
2482
+ return {
2483
+ content: [{
2484
+ type: "text",
2485
+ text: `\u2717 "${match.title}" was not updated \u2014 the server reports the id matched no row. Re-run list_stories and retry.`
2486
+ }]
2487
+ };
2488
+ }
2318
2489
  return {
2319
2490
  content: [{ type: "text", text: `\u2713 ${match.title} \u2192 in_progress` }]
2320
2491
  };
@@ -2324,7 +2495,7 @@ async function handleToolCall(name, args, config) {
2324
2495
  if (!storyId) return { content: [{ type: "text", text: "story_id is required" }] };
2325
2496
  const force = args?.force === true;
2326
2497
  const data = await listStories(config);
2327
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2498
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2328
2499
  if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2329
2500
  const blocking = findBlockingMusts(match.acceptance_criteria);
2330
2501
  if (!force && blocking.length > 0) {
@@ -2340,7 +2511,15 @@ Fix and call report_criteria, or call mark_done again with force:true to overrid
2340
2511
  };
2341
2512
  }
2342
2513
  const forced = force && blocking.length > 0;
2343
- await updateStories(config, [{ id: match.id, status: "done", ...forced ? { forced: true } : {} }]);
2514
+ const result = await updateStories(config, [{ id: match.id, status: "done", ...forced ? { forced: true } : {} }]);
2515
+ if (result.missing?.includes(match.id)) {
2516
+ return {
2517
+ content: [{
2518
+ type: "text",
2519
+ text: `\u2717 "${match.title}" was not marked done \u2014 the server reports the id matched no row. Re-run list_stories and retry.`
2520
+ }]
2521
+ };
2522
+ }
2344
2523
  return {
2345
2524
  content: [{ type: "text", text: `\u2713 ${match.title} \u2192 done` }]
2346
2525
  };
@@ -2352,7 +2531,7 @@ Fix and call report_criteria, or call mark_done again with force:true to overrid
2352
2531
  return { content: [{ type: "text", text: "story_id and results are required" }] };
2353
2532
  }
2354
2533
  const data = await listStories(config);
2355
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2534
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2356
2535
  if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2357
2536
  const result = await reportCriteria(
2358
2537
  config,
@@ -2384,7 +2563,7 @@ Fix and call report_criteria, or call mark_done again with force:true to overrid
2384
2563
  };
2385
2564
  }
2386
2565
  const data = await listStories(config);
2387
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2566
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2388
2567
  if (!match) return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2389
2568
  await raiseBlocker(config, {
2390
2569
  story_id: match.id,
@@ -2452,6 +2631,48 @@ Decision needed: ${decisionNeeded}`
2452
2631
  }
2453
2632
  return { content: [{ type: "text", text: "Failed to log decision" }] };
2454
2633
  }
2634
+ case "amend_decision": {
2635
+ const decisionId = args?.decision_id;
2636
+ if (!decisionId) return { content: [{ type: "text", text: "decision_id is required" }] };
2637
+ const title = args?.title;
2638
+ const description = args?.description;
2639
+ const category = args?.category;
2640
+ if (!title?.trim() && !description?.trim() && !category) {
2641
+ return { content: [{ type: "text", text: "Nothing to amend \u2014 pass title, description, and/or category" }] };
2642
+ }
2643
+ const { decisions } = await listDecisions(config);
2644
+ const match = decisions.find((d) => d.id.startsWith(decisionId));
2645
+ if (!match) return { content: [{ type: "text", text: `No decision found matching "${decisionId}"` }] };
2646
+ if (match.retracted_at) {
2647
+ return { content: [{ type: "text", text: `"${match.title}" has been retracted and can't be amended.` }] };
2648
+ }
2649
+ try {
2650
+ const updated = await amendDecision(config, match.id, {
2651
+ ...title?.trim() ? { title: title.trim() } : {},
2652
+ ...description?.trim() ? { description: description.trim() } : {},
2653
+ ...category ? { category } : {}
2654
+ });
2655
+ return { content: [{ type: "text", text: `\u2713 Decision amended: ${updated.title}` }] };
2656
+ } catch {
2657
+ return { content: [{ type: "text", text: "Failed to amend decision" }] };
2658
+ }
2659
+ }
2660
+ case "retract_decision": {
2661
+ const decisionId = args?.decision_id;
2662
+ if (!decisionId) return { content: [{ type: "text", text: "decision_id is required" }] };
2663
+ const { decisions } = await listDecisions(config);
2664
+ const match = decisions.find((d) => d.id.startsWith(decisionId));
2665
+ if (!match) return { content: [{ type: "text", text: `No decision found matching "${decisionId}"` }] };
2666
+ if (match.retracted_at) {
2667
+ return { content: [{ type: "text", text: `"${match.title}" is already retracted.` }] };
2668
+ }
2669
+ try {
2670
+ await retractDecision(config, match.id, args?.reason);
2671
+ return { content: [{ type: "text", text: `\u2713 Decision retracted: ${match.title}` }] };
2672
+ } catch {
2673
+ return { content: [{ type: "text", text: "Failed to retract decision" }] };
2674
+ }
2675
+ }
2455
2676
  case "add_note": {
2456
2677
  const storyId = args?.story_id;
2457
2678
  const note = args?.note;
@@ -2459,7 +2680,7 @@ Decision needed: ${decisionNeeded}`
2459
2680
  return { content: [{ type: "text", text: "story_id and note required" }] };
2460
2681
  }
2461
2682
  const data = await listStories(config);
2462
- const match = data.stories.find((s) => s.id.startsWith(storyId));
2683
+ const match = data.stories.find((s) => matchesStoryRef(s, storyId));
2463
2684
  if (!match) {
2464
2685
  return { content: [{ type: "text", text: `No story found matching "${storyId}"` }] };
2465
2686
  }
@@ -2481,7 +2702,7 @@ Decision needed: ${decisionNeeded}`
2481
2702
  }
2482
2703
  const res = await fetch(apiUrl(config, "/complexity"), {
2483
2704
  method: "POST",
2484
- headers: { "Content-Type": "application/json" },
2705
+ headers: syncHeaders({ "Content-Type": "application/json" }),
2485
2706
  body: JSON.stringify(body)
2486
2707
  });
2487
2708
  if (!res.ok) {
@@ -2498,7 +2719,7 @@ Decision needed: ${decisionNeeded}`
2498
2719
  if (!query) return { content: [{ type: "text", text: "query is required" }] };
2499
2720
  const res = await fetch(apiUrl(config, "/research"), {
2500
2721
  method: "POST",
2501
- headers: { "Content-Type": "application/json" },
2722
+ headers: syncHeaders({ "Content-Type": "application/json" }),
2502
2723
  body: JSON.stringify({ query })
2503
2724
  });
2504
2725
  if (!res.ok) {
@@ -2515,7 +2736,7 @@ Decision needed: ${decisionNeeded}`
2515
2736
  if (!prd) return { content: [{ type: "text", text: "prd text is required" }] };
2516
2737
  const res = await fetch(apiUrl(config, "/import"), {
2517
2738
  method: "POST",
2518
- headers: { "Content-Type": "application/json" },
2739
+ headers: syncHeaders({ "Content-Type": "application/json" }),
2519
2740
  body: JSON.stringify({ prd, format: args?.format ?? "PRD" })
2520
2741
  });
2521
2742
  if (!res.ok) {
@@ -2625,7 +2846,7 @@ async function startMcpServer(mode = "all") {
2625
2846
  };
2626
2847
  }
2627
2848
  if (uri === "intentdocs://project/decisions") {
2628
- const res = await fetch(apiUrl(config, "/decisions"));
2849
+ const res = await fetch(apiUrl(config, "/decisions"), { headers: syncHeaders() });
2629
2850
  if (!res.ok) {
2630
2851
  const text2 = JSON.stringify({ note: "Failed to fetch decisions" });
2631
2852
  return { contents: [{ uri, mimeType: "application/json", text: text2 }] };
@@ -2637,7 +2858,7 @@ async function startMcpServer(mode = "all") {
2637
2858
  };
2638
2859
  }
2639
2860
  if (uri === "intentdocs://project/data-model") {
2640
- const res = await fetch(apiUrl(config, "/data-model"));
2861
+ const res = await fetch(apiUrl(config, "/data-model"), { headers: syncHeaders() });
2641
2862
  let data;
2642
2863
  if (res.status === 404) {
2643
2864
  data = { note: "No data model defined yet. Run the interview or add one via the web UI." };
@@ -2652,7 +2873,7 @@ async function startMcpServer(mode = "all") {
2652
2873
  };
2653
2874
  }
2654
2875
  if (uri === "intentdocs://project/build-order") {
2655
- const res = await fetch(apiUrl(config, "/build-order"));
2876
+ const res = await fetch(apiUrl(config, "/build-order"), { headers: syncHeaders() });
2656
2877
  let data;
2657
2878
  if (res.status === 404) {
2658
2879
  data = { note: "No build order defined yet." };
@@ -2737,18 +2958,18 @@ Registering intentdocs MCP with OpenClaw...`);
2737
2958
  var require2 = createRequire2(import.meta.url);
2738
2959
  var { version } = require2("../package.json");
2739
2960
  var program = new Command();
2740
- program.name("intent").description("Write specs, map stories, ship with AI \u2014 from your terminal").version(version);
2961
+ program.name("intentdocs").description("Write specs, map stories, ship with AI \u2014 from your terminal").version(version);
2741
2962
  program.command("login").description("Log in to intentdocs (opens browser)").action(() => void login());
2742
2963
  program.command("logout").description("Log out of intentdocs").action(() => void logout());
2743
2964
  var keys = program.command("keys").description("Manage your Anthropic API key \u2014 unlock AI features on any plan");
2744
2965
  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));
2966
+ import("./keys-5PA3D6CX.js").then((m) => void m.keysSet(key));
2746
2967
  });
2747
2968
  keys.command("status").description("Show whether a key is set (masked)").action(() => {
2748
- import("./keys-EDWET3WG.js").then((m) => void m.keysStatus());
2969
+ import("./keys-5PA3D6CX.js").then((m) => void m.keysStatus());
2749
2970
  });
2750
2971
  keys.command("remove").description("Remove your stored key").action(() => {
2751
- import("./keys-EDWET3WG.js").then((m) => void m.keysRemove());
2972
+ import("./keys-5PA3D6CX.js").then((m) => void m.keysRemove());
2752
2973
  });
2753
2974
  program.command("start").description("Start a new project \u2014 guided flow from problem to pitch").action(() => void start());
2754
2975
  program.command("scope [projectId]").description("Scope a project \u2014 generate blueprint, story map, and mockup").action((id) => void scope(id));
@@ -2761,12 +2982,12 @@ program.command("templates [name]").description("Browse story map templates").ac
2761
2982
  console.log(` ${key.padEnd(20)} ${tmpl.description}`);
2762
2983
  }
2763
2984
  console.log(`
2764
- Usage: intent templates <name>
2985
+ Usage: intentdocs templates <name>
2765
2986
  `);
2766
2987
  } else {
2767
2988
  const tmpl = templates[name];
2768
2989
  if (!tmpl) {
2769
- console.error(`Unknown template: "${name}". Run \`intent templates\` to list.`);
2990
+ console.error(`Unknown template: "${name}". Run \`intentdocs templates\` to list.`);
2770
2991
  } else {
2771
2992
  console.log(`
2772
2993
  ${tmpl.name}: ${tmpl.description}`);
@@ -2779,8 +3000,12 @@ program.command("templates [name]").description("Browse story map templates").ac
2779
3000
  }
2780
3001
  }
2781
3002
  });
3003
+ program.command("projects").description("List the projects your token can reach \u2014 the whole workspace it is bound to").action(() => {
3004
+ import("./projects-F7GMH66U.js").then((m) => void m.projects());
3005
+ });
2782
3006
  program.command("init [token]").description("Connect this project to a intentdocs story map").action((token) => void init(token));
2783
3007
  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
+ 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));
2784
3009
  program.command("status").description("Show story map progress").action(() => void status());
2785
3010
  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
3011
  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));
@@ -2790,32 +3015,48 @@ program.command("show <id>").description("Show the full spec for a story (accept
2790
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));
2791
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));
2792
3017
  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));
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-EVLWM64C.js").then((m) => void m.logDecision(title, opts));
3020
+ });
3021
+ decide.command("list").description("List logged decisions (id, category, title)").action(() => {
3022
+ import("./log-EVLWM64C.js").then((m) => void m.listLoggedDecisions());
3023
+ });
3024
+ 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-EVLWM64C.js").then((m) => void m.amendDecision(id, opts));
3026
+ });
3027
+ 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-EVLWM64C.js").then((m) => void m.deleteDecision(id, opts));
2795
3029
  });
2796
3030
  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));
3031
+ import("./log-EVLWM64C.js").then((m) => void m.logNote(storyId, note));
2798
3032
  });
2799
3033
  program.command("example").description("Add a few-shot reference pattern (before/after code) to the project").action(() => void example());
2800
3034
  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));
3035
+ import("./complexity-ZOB3BPVD.js").then((m) => void m.complexity(id));
2802
3036
  });
2803
3037
  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));
3038
+ import("./research-4ASCZSZN.js").then((m) => void m.research(query));
2805
3039
  });
2806
3040
  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));
3041
+ import("./import-prd-MJUCIMHX.js").then((m) => void m.importPrd(file));
2808
3042
  });
2809
3043
  var map = program.command("map").description("Import or extract story maps from codebases");
2810
3044
  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());
3045
+ import("./map-extract-W6WJLVOF.js").then((m) => void m.mapExtract());
2812
3046
  });
2813
3047
  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));
3048
+ import("./map-import-LL4AIAQE.js").then((m) => void m.mapImport(file, opts));
2815
3049
  });
2816
3050
  map.command("schema").description("Print the JSON Schema for storymap.json").action(() => {
2817
- import("./map-schema-JVL3O2TJ.js").then((m) => void m.mapSchema());
3051
+ import("./map-schema-6GL2UKZQ.js").then((m) => void m.mapSchema());
2818
3052
  });
2819
3053
  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
3054
  program.command("openclaw-register").description("Register this project with OpenClaw MCP \u2014 gives spawned coding agents access to your story map").action(() => void openclawRegister());
3055
+ var invokedAs = process.argv[1]?.split("/").pop()?.replace(/\.js$/, "") ?? "";
3056
+ if (invokedAs === "intent" && !process.argv.includes("--quiet")) {
3057
+ const args = process.argv.slice(2);
3058
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
3059
+ console.log("\n Tip: the canonical command is `intentdocs`. `intent` also works but may collide with other tools.\n");
3060
+ }
3061
+ }
2821
3062
  program.parse();