@anthropologies/claudestory 0.1.27 → 0.1.29

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/cli.js CHANGED
@@ -491,6 +491,15 @@ var init_project_state = __esm({
491
491
  get blockedCount() {
492
492
  return this.leafTickets.filter((t) => t.status !== "complete" && this.isBlocked(t)).length;
493
493
  }
494
+ /** True when the project has been initialized but not yet populated with tickets/issues/handovers. */
495
+ get isEmptyScaffold() {
496
+ return this.tickets.length === 0 && this.issues.length === 0 && this.handoverFilenames.length === 0 && this.isDefaultScaffoldPhases;
497
+ }
498
+ get isDefaultScaffoldPhases() {
499
+ const { phases } = this.roadmap;
500
+ if (phases.length === 0) return true;
501
+ return phases.length === 1 && phases[0].id === "p0";
502
+ }
494
503
  ticketByID(id) {
495
504
  return this.ticketsByID.get(id);
496
505
  }
@@ -1673,6 +1682,7 @@ var init_queries = __esm({
1673
1682
  // src/core/output-formatter.ts
1674
1683
  var output_formatter_exports = {};
1675
1684
  __export(output_formatter_exports, {
1685
+ EMPTY_SCAFFOLD_HEADING: () => EMPTY_SCAFFOLD_HEADING,
1676
1686
  ExitCode: () => ExitCode,
1677
1687
  errorEnvelope: () => errorEnvelope,
1678
1688
  escapeMarkdownInline: () => escapeMarkdownInline,
@@ -1763,6 +1773,7 @@ function formatStatus(state, format) {
1763
1773
  activeLessons: state.activeLessonCount,
1764
1774
  deprecatedLessons: state.deprecatedLessonCount,
1765
1775
  handovers: state.handoverFilenames.length,
1776
+ isEmptyScaffold: state.isEmptyScaffold,
1766
1777
  phases: phases.map((p) => ({
1767
1778
  id: p.phase.id,
1768
1779
  name: p.phase.name,
@@ -1790,6 +1801,13 @@ function formatStatus(state, format) {
1790
1801
  const summary = p.phase.summary ?? truncate(p.phase.description, 80);
1791
1802
  lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
1792
1803
  }
1804
+ if (state.isEmptyScaffold) {
1805
+ lines.push("");
1806
+ lines.push(EMPTY_SCAFFOLD_HEADING);
1807
+ lines.push("");
1808
+ lines.push("This project has been initialized but has no tickets, issues, or handovers yet.");
1809
+ lines.push("Run the /story setup flow to analyze your project and create an initial roadmap.");
1810
+ }
1793
1811
  return lines.join("\n");
1794
1812
  }
1795
1813
  function formatPhaseList(state, format) {
@@ -2629,11 +2647,14 @@ function formatReference(commands, mcpTools, format) {
2629
2647
  lines.push("- **/story not available:** Run `claudestory setup-skill` to install the skill");
2630
2648
  return lines.join("\n");
2631
2649
  }
2632
- function formatRecommendations(result, format) {
2650
+ function formatRecommendations(result, state, format) {
2633
2651
  if (format === "json") {
2634
- return JSON.stringify(successEnvelope(result), null, 2);
2652
+ return JSON.stringify(successEnvelope({ ...result, isEmptyScaffold: state.isEmptyScaffold }), null, 2);
2635
2653
  }
2636
2654
  if (result.recommendations.length === 0) {
2655
+ if (state.isEmptyScaffold) {
2656
+ return "No recommendations yet \u2014 this project needs tickets and phases. Run the /story setup flow to get started.";
2657
+ }
2637
2658
  return "No recommendations \u2014 all work is complete or blocked.";
2638
2659
  }
2639
2660
  const lines = ["# Recommendations", ""];
@@ -2652,12 +2673,13 @@ function formatRecommendations(result, format) {
2652
2673
  }
2653
2674
  return lines.join("\n");
2654
2675
  }
2655
- var ExitCode;
2676
+ var EMPTY_SCAFFOLD_HEADING, ExitCode;
2656
2677
  var init_output_formatter = __esm({
2657
2678
  "src/core/output-formatter.ts"() {
2658
2679
  "use strict";
2659
2680
  init_esm_shims();
2660
2681
  init_queries();
2682
+ EMPTY_SCAFFOLD_HEADING = "## Getting Started";
2661
2683
  ExitCode = {
2662
2684
  OK: 0,
2663
2685
  USER_ERROR: 1,
@@ -4667,7 +4689,7 @@ var init_recommend = __esm({
4667
4689
  // src/cli/commands/recommend.ts
4668
4690
  function handleRecommend(ctx, count) {
4669
4691
  const result = recommend(ctx.state, count);
4670
- return { output: formatRecommendations(result, ctx.format) };
4692
+ return { output: formatRecommendations(result, ctx.state, ctx.format) };
4671
4693
  }
4672
4694
  var init_recommend2 = __esm({
4673
4695
  "src/cli/commands/recommend.ts"() {
@@ -5002,6 +5024,7 @@ var init_session_types = __esm({
5002
5024
  "VERIFY",
5003
5025
  "FINALIZE",
5004
5026
  "COMPACT",
5027
+ "LESSON_CAPTURE",
5005
5028
  "ISSUE_SWEEP"
5006
5029
  ]);
5007
5030
  IDLE_STATES = /* @__PURE__ */ new Set([
@@ -5028,6 +5051,7 @@ var init_session_types = __esm({
5028
5051
  "COMPACT",
5029
5052
  "HANDOVER",
5030
5053
  "COMPLETE",
5054
+ "LESSON_CAPTURE",
5031
5055
  "ISSUE_SWEEP",
5032
5056
  "SESSION_END"
5033
5057
  ];
@@ -5594,6 +5618,8 @@ var init_state_machine = __esm({
5594
5618
  // pass → FINALIZE, fail → IMPLEMENT, retry
5595
5619
  FINALIZE: ["COMPLETE"],
5596
5620
  COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
5621
+ LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
5622
+ // advance → ISSUE_SWEEP, retry self, done → HANDOVER
5597
5623
  ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
5598
5624
  // retry (next issue), done → HANDOVER, loop → PICK_TICKET
5599
5625
  HANDOVER: ["COMPACT", "SESSION_END", "PICK_TICKET"],
@@ -5967,6 +5993,18 @@ function findFirstPostComplete(postComplete, ctx) {
5967
5993
  }
5968
5994
  return { kind: "exhausted" };
5969
5995
  }
5996
+ function findNextPostComplete(postComplete, currentId, ctx) {
5997
+ const currentIndex = postComplete.indexOf(currentId);
5998
+ const startIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
5999
+ for (let i = startIndex; i < postComplete.length; i++) {
6000
+ const id = postComplete[i];
6001
+ const stage = stages.get(id);
6002
+ if (!stage) return { kind: "unregistered", id };
6003
+ if (stage.skip?.(ctx)) continue;
6004
+ return { kind: "found", stage };
6005
+ }
6006
+ return { kind: "exhausted" };
6007
+ }
5970
6008
  var stages;
5971
6009
  var init_registry = __esm({
5972
6010
  "src/autonomous/stages/registry.ts"() {
@@ -7403,6 +7441,74 @@ var init_complete = __esm({
7403
7441
  }
7404
7442
  });
7405
7443
 
7444
+ // src/autonomous/stages/lesson-capture.ts
7445
+ var LessonCaptureStage;
7446
+ var init_lesson_capture = __esm({
7447
+ "src/autonomous/stages/lesson-capture.ts"() {
7448
+ "use strict";
7449
+ init_esm_shims();
7450
+ LessonCaptureStage = class {
7451
+ id = "LESSON_CAPTURE";
7452
+ skip(ctx) {
7453
+ const config = ctx.recipe.stages?.LESSON_CAPTURE;
7454
+ return !config?.enabled;
7455
+ }
7456
+ async enter(ctx) {
7457
+ const planReviews = ctx.state.reviews.plan ?? [];
7458
+ const codeReviews = ctx.state.reviews.code ?? [];
7459
+ const ticketsDone = ctx.state.completedTickets.length;
7460
+ const planFindings = planReviews.reduce((sum, r) => sum + (r.findingCount ?? 0), 0);
7461
+ const planCritical = planReviews.reduce((sum, r) => sum + (r.criticalCount ?? 0), 0);
7462
+ const planMajor = planReviews.reduce((sum, r) => sum + (r.majorCount ?? 0), 0);
7463
+ const codeFindings = codeReviews.reduce((sum, r) => sum + (r.findingCount ?? 0), 0);
7464
+ const codeCritical = codeReviews.reduce((sum, r) => sum + (r.criticalCount ?? 0), 0);
7465
+ const codeMajor = codeReviews.reduce((sum, r) => sum + (r.majorCount ?? 0), 0);
7466
+ const totalFindings = planFindings + codeFindings;
7467
+ if (totalFindings === 0) {
7468
+ ctx.appendEvent("lesson_capture", { result: "no_findings", ticketsDone });
7469
+ return { action: "advance" };
7470
+ }
7471
+ ctx.appendEvent("lesson_capture", {
7472
+ result: "started",
7473
+ ticketsDone,
7474
+ planFindings,
7475
+ codeFindings
7476
+ });
7477
+ return {
7478
+ instruction: [
7479
+ "# Capture Lessons from Review Findings",
7480
+ "",
7481
+ `This session completed ${ticketsDone} ticket(s). Review summary:`,
7482
+ `- **Plan reviews:** ${planReviews.length} round(s), ${planCritical} critical, ${planMajor} major, ${planFindings} total findings`,
7483
+ `- **Code reviews:** ${codeReviews.length} round(s), ${codeCritical} critical, ${codeMajor} major, ${codeFindings} total findings`,
7484
+ "",
7485
+ "Review these findings for recurring patterns worth capturing as lessons:",
7486
+ "",
7487
+ "1. Call `claudestory_lesson_list` to see existing lessons",
7488
+ "2. For each pattern that recurred or was critical/major:",
7489
+ " - If it matches an existing lesson \u2192 call `claudestory_lesson_reinforce`",
7490
+ ' - If it\'s a new pattern \u2192 call `claudestory_lesson_create` with `source: "review"`',
7491
+ "3. Skip patterns that are one-off or already well-covered",
7492
+ `4. Call \`claudestory_autonomous_guide\` with completedAction: "lessons_captured"`,
7493
+ "",
7494
+ "```json",
7495
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "lessons_captured" } }`,
7496
+ "```"
7497
+ ].join("\n"),
7498
+ reminders: [
7499
+ "Check existing lessons first \u2014 reinforce before creating duplicates.",
7500
+ "Only capture patterns worth remembering across sessions."
7501
+ ]
7502
+ };
7503
+ }
7504
+ async report(ctx, _report) {
7505
+ ctx.appendEvent("lesson_capture", { result: "completed" });
7506
+ return { action: "advance" };
7507
+ }
7508
+ };
7509
+ }
7510
+ });
7511
+
7406
7512
  // src/autonomous/stages/issue-sweep.ts
7407
7513
  var IssueSweepStage;
7408
7514
  var init_issue_sweep = __esm({
@@ -7611,6 +7717,7 @@ var init_stages = __esm({
7611
7717
  init_verify();
7612
7718
  init_finalize();
7613
7719
  init_complete();
7720
+ init_lesson_capture();
7614
7721
  init_issue_sweep();
7615
7722
  init_handover2();
7616
7723
  registerStage(new PickTicketStage());
@@ -7623,6 +7730,7 @@ var init_stages = __esm({
7623
7730
  registerStage(new VerifyStage());
7624
7731
  registerStage(new FinalizeStage());
7625
7732
  registerStage(new CompleteStage());
7733
+ registerStage(new LessonCaptureStage());
7626
7734
  registerStage(new IssueSweepStage());
7627
7735
  registerStage(new HandoverStage());
7628
7736
  }
@@ -8238,7 +8346,8 @@ async function processAdvance(ctx, currentStage, advance, depth = 0) {
8238
8346
  }
8239
8347
  if (next.kind === "exhausted") {
8240
8348
  const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
8241
- const post = findFirstPostComplete(postComplete, ctx);
8349
+ const isInPostComplete = postComplete.includes(currentStage.id);
8350
+ const post = isInPostComplete ? findNextPostComplete(postComplete, currentStage.id, ctx) : findFirstPostComplete(postComplete, ctx);
8242
8351
  if (post.kind === "found") {
8243
8352
  assertTransition(currentStage.id, post.stage.id);
8244
8353
  ctx.writeState({ state: post.stage.id, previousState: currentStage.id });
@@ -8384,6 +8493,7 @@ async function handleResume(root, args) {
8384
8493
  // T-128: tests invalidated by HEAD change
8385
8494
  CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
8386
8495
  FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
8496
+ LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
8387
8497
  ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
8388
8498
  // T-128: post-complete, restart sweep
8389
8499
  };
@@ -10129,7 +10239,7 @@ var init_mcp = __esm({
10129
10239
  init_init();
10130
10240
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
10131
10241
  CONFIG_PATH2 = ".story/config.json";
10132
- version = "0.1.27";
10242
+ version = "0.1.29";
10133
10243
  main().catch((err) => {
10134
10244
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
10135
10245
  `);
@@ -13407,7 +13517,7 @@ async function runCli() {
13407
13517
  registerConfigCommand: registerConfigCommand2,
13408
13518
  registerSessionCommand: registerSessionCommand2
13409
13519
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13410
- const version2 = "0.1.27";
13520
+ const version2 = "0.1.29";
13411
13521
  class HandledError extends Error {
13412
13522
  constructor() {
13413
13523
  super("HANDLED_ERROR");
package/dist/index.d.ts CHANGED
@@ -561,6 +561,9 @@ declare class ProjectState {
561
561
  */
562
562
  isBlocked(ticket: Ticket): boolean;
563
563
  get blockedCount(): number;
564
+ /** True when the project has been initialized but not yet populated with tickets/issues/handovers. */
565
+ get isEmptyScaffold(): boolean;
566
+ private get isDefaultScaffoldPhases();
564
567
  ticketByID(id: string): Ticket | undefined;
565
568
  issueByID(id: string): Issue | undefined;
566
569
  noteByID(id: string): Note | undefined;
@@ -1722,6 +1725,8 @@ declare function buildRecap(currentState: ProjectState, snapshotInfo: {
1722
1725
  filename: string;
1723
1726
  } | null): RecapResult;
1724
1727
 
1728
+ /** SKILL PROTOCOL: SKILL.md Step 2b matches this literal string. Do not change without updating SKILL.md. */
1729
+ declare const EMPTY_SCAFFOLD_HEADING = "## Getting Started";
1725
1730
  declare const ExitCode: {
1726
1731
  readonly OK: 0;
1727
1732
  readonly USER_ERROR: 1;
@@ -1792,6 +1797,6 @@ declare function formatSnapshotResult(result: {
1792
1797
  }, format: OutputFormat): string;
1793
1798
  declare function formatRecap(recap: RecapResult, state: ProjectState, format: OutputFormat): string;
1794
1799
  declare function formatExport(state: ProjectState, mode: "all" | "phase", phaseId: string | null, format: OutputFormat): string;
1795
- declare function formatRecommendations(result: RecommendResult, format: OutputFormat): string;
1800
+ declare function formatRecommendations(result: RecommendResult, state: ProjectState, format: OutputFormat): string;
1796
1801
 
1797
- export { type Blocker, BlockerSchema, CURRENT_SCHEMA_VERSION, type Config, ConfigSchema, DATE_REGEX, DateSchema, ERROR_CODES, type ErrorCode, type ErrorEnvelope, ExitCode, type ExitCodeValue, type Features, FeaturesSchema, INTEGRITY_WARNING_TYPES, ISSUE_ID_REGEX, ISSUE_SEVERITIES, ISSUE_STATUSES, type InitOptions, type InitResult, type Issue, IssueIdSchema, IssueSchema, type IssueSeverity, type IssueStatus, type LoadOptions, type LoadResult, type LoadWarning, type LoadWarningType, NOTE_ID_REGEX, NOTE_STATUSES, type NextTicketAllBlocked, type NextTicketAllComplete, type NextTicketEmpty, type NextTicketOutcome, type NextTicketResult, type Note, NoteIdSchema, NoteSchema, type NoteStatus, OUTPUT_FORMATS, type OutputFormat, type PartialEnvelope, type Phase, PhaseSchema, type PhaseStatus, type PhaseWithStatus, ProjectLoaderError, ProjectState, type RecapResult, type RecommendCategory, type RecommendItemKind, type RecommendResult, type Recommendation, type Roadmap, RoadmapSchema, type SnapshotDiff, type SnapshotV1, SnapshotV1Schema, type SuccessEnvelope, TICKET_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, type Ticket, TicketIdSchema, TicketSchema, type TicketStatus, type TicketType, type UmbrellaProgress, type UnblockImpact, type ValidationFinding, type ValidationLevel, type ValidationResult, type WithProjectLockOptions, atomicWrite, blockedTickets, buildRecap, currentPhase, deleteIssue, deleteNote, deleteTicket, descendantLeaves, diffStates, discoverProjectRoot, errorEnvelope, escapeMarkdownInline, extractHandoverDate, fencedBlock, formatBlockedTickets, formatBlockerList, formatError, formatExport, formatHandoverContent, formatHandoverCreateResult, formatHandoverList, formatInitResult, formatIssue, formatIssueList, formatNextTicketOutcome, formatPhaseList, formatPhaseTickets, formatRecap, formatRecommendations, formatSnapshotResult, formatStatus, formatTicket, formatTicketList, formatValidation, guardPath, initProject, isBlockerCleared, listHandovers, loadLatestSnapshot, loadProject, mergeValidation, nextIssueID, nextNoteID, nextOrder, nextTicket, nextTicketID, nextTickets, partialEnvelope, phasesWithStatus, readHandover, recommend, runTransaction, runTransactionUnlocked, saveSnapshot, serializeJSON, sortKeysDeep, successEnvelope, ticketsUnblockedBy, umbrellaProgress, validateProject, withProjectLock, writeConfig, writeIssue, writeIssueUnlocked, writeNote, writeNoteUnlocked, writeRoadmap, writeRoadmapUnlocked, writeTicket, writeTicketUnlocked };
1802
+ export { type Blocker, BlockerSchema, CURRENT_SCHEMA_VERSION, type Config, ConfigSchema, DATE_REGEX, DateSchema, EMPTY_SCAFFOLD_HEADING, ERROR_CODES, type ErrorCode, type ErrorEnvelope, ExitCode, type ExitCodeValue, type Features, FeaturesSchema, INTEGRITY_WARNING_TYPES, ISSUE_ID_REGEX, ISSUE_SEVERITIES, ISSUE_STATUSES, type InitOptions, type InitResult, type Issue, IssueIdSchema, IssueSchema, type IssueSeverity, type IssueStatus, type LoadOptions, type LoadResult, type LoadWarning, type LoadWarningType, NOTE_ID_REGEX, NOTE_STATUSES, type NextTicketAllBlocked, type NextTicketAllComplete, type NextTicketEmpty, type NextTicketOutcome, type NextTicketResult, type Note, NoteIdSchema, NoteSchema, type NoteStatus, OUTPUT_FORMATS, type OutputFormat, type PartialEnvelope, type Phase, PhaseSchema, type PhaseStatus, type PhaseWithStatus, ProjectLoaderError, ProjectState, type RecapResult, type RecommendCategory, type RecommendItemKind, type RecommendResult, type Recommendation, type Roadmap, RoadmapSchema, type SnapshotDiff, type SnapshotV1, SnapshotV1Schema, type SuccessEnvelope, TICKET_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, type Ticket, TicketIdSchema, TicketSchema, type TicketStatus, type TicketType, type UmbrellaProgress, type UnblockImpact, type ValidationFinding, type ValidationLevel, type ValidationResult, type WithProjectLockOptions, atomicWrite, blockedTickets, buildRecap, currentPhase, deleteIssue, deleteNote, deleteTicket, descendantLeaves, diffStates, discoverProjectRoot, errorEnvelope, escapeMarkdownInline, extractHandoverDate, fencedBlock, formatBlockedTickets, formatBlockerList, formatError, formatExport, formatHandoverContent, formatHandoverCreateResult, formatHandoverList, formatInitResult, formatIssue, formatIssueList, formatNextTicketOutcome, formatPhaseList, formatPhaseTickets, formatRecap, formatRecommendations, formatSnapshotResult, formatStatus, formatTicket, formatTicketList, formatValidation, guardPath, initProject, isBlockerCleared, listHandovers, loadLatestSnapshot, loadProject, mergeValidation, nextIssueID, nextNoteID, nextOrder, nextTicket, nextTicketID, nextTickets, partialEnvelope, phasesWithStatus, readHandover, recommend, runTransaction, runTransactionUnlocked, saveSnapshot, serializeJSON, sortKeysDeep, successEnvelope, ticketsUnblockedBy, umbrellaProgress, validateProject, withProjectLock, writeConfig, writeIssue, writeIssueUnlocked, writeNote, writeNoteUnlocked, writeRoadmap, writeRoadmapUnlocked, writeTicket, writeTicketUnlocked };
package/dist/index.js CHANGED
@@ -326,6 +326,15 @@ var ProjectState = class _ProjectState {
326
326
  get blockedCount() {
327
327
  return this.leafTickets.filter((t) => t.status !== "complete" && this.isBlocked(t)).length;
328
328
  }
329
+ /** True when the project has been initialized but not yet populated with tickets/issues/handovers. */
330
+ get isEmptyScaffold() {
331
+ return this.tickets.length === 0 && this.issues.length === 0 && this.handoverFilenames.length === 0 && this.isDefaultScaffoldPhases;
332
+ }
333
+ get isDefaultScaffoldPhases() {
334
+ const { phases } = this.roadmap;
335
+ if (phases.length === 0) return true;
336
+ return phases.length === 1 && phases[0].id === "p0";
337
+ }
329
338
  ticketByID(id) {
330
339
  return this.ticketsByID.get(id);
331
340
  }
@@ -2289,6 +2298,7 @@ async function pruneSnapshots(dir) {
2289
2298
  }
2290
2299
 
2291
2300
  // src/core/output-formatter.ts
2301
+ var EMPTY_SCAFFOLD_HEADING = "## Getting Started";
2292
2302
  var ExitCode = {
2293
2303
  OK: 0,
2294
2304
  USER_ERROR: 1,
@@ -2343,6 +2353,7 @@ function formatStatus(state, format) {
2343
2353
  activeLessons: state.activeLessonCount,
2344
2354
  deprecatedLessons: state.deprecatedLessonCount,
2345
2355
  handovers: state.handoverFilenames.length,
2356
+ isEmptyScaffold: state.isEmptyScaffold,
2346
2357
  phases: phases.map((p) => ({
2347
2358
  id: p.phase.id,
2348
2359
  name: p.phase.name,
@@ -2370,6 +2381,13 @@ function formatStatus(state, format) {
2370
2381
  const summary = p.phase.summary ?? truncate(p.phase.description, 80);
2371
2382
  lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
2372
2383
  }
2384
+ if (state.isEmptyScaffold) {
2385
+ lines.push("");
2386
+ lines.push(EMPTY_SCAFFOLD_HEADING);
2387
+ lines.push("");
2388
+ lines.push("This project has been initialized but has no tickets, issues, or handovers yet.");
2389
+ lines.push("Run the /story setup flow to analyze your project and create an initial roadmap.");
2390
+ }
2373
2391
  return lines.join("\n");
2374
2392
  }
2375
2393
  function formatPhaseList(state, format) {
@@ -2967,11 +2985,14 @@ function formatTicketOneLiner(t, state) {
2967
2985
  const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
2968
2986
  return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
2969
2987
  }
2970
- function formatRecommendations(result, format) {
2988
+ function formatRecommendations(result, state, format) {
2971
2989
  if (format === "json") {
2972
- return JSON.stringify(successEnvelope(result), null, 2);
2990
+ return JSON.stringify(successEnvelope({ ...result, isEmptyScaffold: state.isEmptyScaffold }), null, 2);
2973
2991
  }
2974
2992
  if (result.recommendations.length === 0) {
2993
+ if (state.isEmptyScaffold) {
2994
+ return "No recommendations yet \u2014 this project needs tickets and phases. Run the /story setup flow to get started.";
2995
+ }
2975
2996
  return "No recommendations \u2014 all work is complete or blocked.";
2976
2997
  }
2977
2998
  const lines = ["# Recommendations", ""];
@@ -2996,6 +3017,7 @@ export {
2996
3017
  ConfigSchema,
2997
3018
  DATE_REGEX,
2998
3019
  DateSchema,
3020
+ EMPTY_SCAFFOLD_HEADING,
2999
3021
  ERROR_CODES,
3000
3022
  ExitCode,
3001
3023
  FeaturesSchema,
package/dist/mcp.js CHANGED
@@ -433,6 +433,15 @@ var init_project_state = __esm({
433
433
  get blockedCount() {
434
434
  return this.leafTickets.filter((t) => t.status !== "complete" && this.isBlocked(t)).length;
435
435
  }
436
+ /** True when the project has been initialized but not yet populated with tickets/issues/handovers. */
437
+ get isEmptyScaffold() {
438
+ return this.tickets.length === 0 && this.issues.length === 0 && this.handoverFilenames.length === 0 && this.isDefaultScaffoldPhases;
439
+ }
440
+ get isDefaultScaffoldPhases() {
441
+ const { phases } = this.roadmap;
442
+ if (phases.length === 0) return true;
443
+ return phases.length === 1 && phases[0].id === "p0";
444
+ }
436
445
  ticketByID(id) {
437
446
  return this.ticketsByID.get(id);
438
447
  }
@@ -1563,6 +1572,7 @@ function formatStatus(state, format) {
1563
1572
  activeLessons: state.activeLessonCount,
1564
1573
  deprecatedLessons: state.deprecatedLessonCount,
1565
1574
  handovers: state.handoverFilenames.length,
1575
+ isEmptyScaffold: state.isEmptyScaffold,
1566
1576
  phases: phases.map((p) => ({
1567
1577
  id: p.phase.id,
1568
1578
  name: p.phase.name,
@@ -1590,6 +1600,13 @@ function formatStatus(state, format) {
1590
1600
  const summary = p.phase.summary ?? truncate(p.phase.description, 80);
1591
1601
  lines.push(`${indicator} **${escapeMarkdownInline(p.phase.name)}** (${p.leafCount} tickets) \u2014 ${escapeMarkdownInline(summary)}`);
1592
1602
  }
1603
+ if (state.isEmptyScaffold) {
1604
+ lines.push("");
1605
+ lines.push(EMPTY_SCAFFOLD_HEADING);
1606
+ lines.push("");
1607
+ lines.push("This project has been initialized but has no tickets, issues, or handovers yet.");
1608
+ lines.push("Run the /story setup flow to analyze your project and create an initial roadmap.");
1609
+ }
1593
1610
  return lines.join("\n");
1594
1611
  }
1595
1612
  function formatPhaseList(state, format) {
@@ -2356,11 +2373,14 @@ function formatTicketOneLiner(t, state) {
2356
2373
  const blocked = state.isBlocked(t) ? " [BLOCKED]" : "";
2357
2374
  return `${status} ${t.id}: ${escapeMarkdownInline(t.title)}${blocked}`;
2358
2375
  }
2359
- function formatRecommendations(result, format) {
2376
+ function formatRecommendations(result, state, format) {
2360
2377
  if (format === "json") {
2361
- return JSON.stringify(successEnvelope(result), null, 2);
2378
+ return JSON.stringify(successEnvelope({ ...result, isEmptyScaffold: state.isEmptyScaffold }), null, 2);
2362
2379
  }
2363
2380
  if (result.recommendations.length === 0) {
2381
+ if (state.isEmptyScaffold) {
2382
+ return "No recommendations yet \u2014 this project needs tickets and phases. Run the /story setup flow to get started.";
2383
+ }
2364
2384
  return "No recommendations \u2014 all work is complete or blocked.";
2365
2385
  }
2366
2386
  const lines = ["# Recommendations", ""];
@@ -2379,12 +2399,13 @@ function formatRecommendations(result, format) {
2379
2399
  }
2380
2400
  return lines.join("\n");
2381
2401
  }
2382
- var ExitCode;
2402
+ var EMPTY_SCAFFOLD_HEADING, ExitCode;
2383
2403
  var init_output_formatter = __esm({
2384
2404
  "src/core/output-formatter.ts"() {
2385
2405
  "use strict";
2386
2406
  init_esm_shims();
2387
2407
  init_queries();
2408
+ EMPTY_SCAFFOLD_HEADING = "## Getting Started";
2388
2409
  ExitCode = {
2389
2410
  OK: 0,
2390
2411
  USER_ERROR: 1,
@@ -3472,6 +3493,7 @@ var init_session_types = __esm({
3472
3493
  "COMPACT",
3473
3494
  "HANDOVER",
3474
3495
  "COMPLETE",
3496
+ "LESSON_CAPTURE",
3475
3497
  "ISSUE_SWEEP",
3476
3498
  "SESSION_END"
3477
3499
  ];
@@ -4883,7 +4905,7 @@ function sortByPhaseAndOrder(tickets, phaseIndex) {
4883
4905
  init_output_formatter();
4884
4906
  function handleRecommend(ctx, count) {
4885
4907
  const result = recommend(ctx.state, count);
4886
- return { output: formatRecommendations(result, ctx.format) };
4908
+ return { output: formatRecommendations(result, ctx.state, ctx.format) };
4887
4909
  }
4888
4910
 
4889
4911
  // src/cli/commands/snapshot.ts
@@ -5200,6 +5222,8 @@ var TRANSITIONS = {
5200
5222
  // pass → FINALIZE, fail → IMPLEMENT, retry
5201
5223
  FINALIZE: ["COMPLETE"],
5202
5224
  COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
5225
+ LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
5226
+ // advance → ISSUE_SWEEP, retry self, done → HANDOVER
5203
5227
  ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
5204
5228
  // retry (next issue), done → HANDOVER, loop → PICK_TICKET
5205
5229
  HANDOVER: ["COMPACT", "SESSION_END", "PICK_TICKET"],
@@ -5559,6 +5583,18 @@ function findFirstPostComplete(postComplete, ctx) {
5559
5583
  }
5560
5584
  return { kind: "exhausted" };
5561
5585
  }
5586
+ function findNextPostComplete(postComplete, currentId, ctx) {
5587
+ const currentIndex = postComplete.indexOf(currentId);
5588
+ const startIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
5589
+ for (let i = startIndex; i < postComplete.length; i++) {
5590
+ const id = postComplete[i];
5591
+ const stage = stages.get(id);
5592
+ if (!stage) return { kind: "unregistered", id };
5593
+ if (stage.skip?.(ctx)) continue;
5594
+ return { kind: "found", stage };
5595
+ }
5596
+ return { kind: "exhausted" };
5597
+ }
5562
5598
 
5563
5599
  // src/autonomous/stages/types.ts
5564
5600
  init_esm_shims();
@@ -6915,6 +6951,68 @@ var CompleteStage = class {
6915
6951
  }
6916
6952
  };
6917
6953
 
6954
+ // src/autonomous/stages/lesson-capture.ts
6955
+ init_esm_shims();
6956
+ var LessonCaptureStage = class {
6957
+ id = "LESSON_CAPTURE";
6958
+ skip(ctx) {
6959
+ const config = ctx.recipe.stages?.LESSON_CAPTURE;
6960
+ return !config?.enabled;
6961
+ }
6962
+ async enter(ctx) {
6963
+ const planReviews = ctx.state.reviews.plan ?? [];
6964
+ const codeReviews = ctx.state.reviews.code ?? [];
6965
+ const ticketsDone = ctx.state.completedTickets.length;
6966
+ const planFindings = planReviews.reduce((sum, r) => sum + (r.findingCount ?? 0), 0);
6967
+ const planCritical = planReviews.reduce((sum, r) => sum + (r.criticalCount ?? 0), 0);
6968
+ const planMajor = planReviews.reduce((sum, r) => sum + (r.majorCount ?? 0), 0);
6969
+ const codeFindings = codeReviews.reduce((sum, r) => sum + (r.findingCount ?? 0), 0);
6970
+ const codeCritical = codeReviews.reduce((sum, r) => sum + (r.criticalCount ?? 0), 0);
6971
+ const codeMajor = codeReviews.reduce((sum, r) => sum + (r.majorCount ?? 0), 0);
6972
+ const totalFindings = planFindings + codeFindings;
6973
+ if (totalFindings === 0) {
6974
+ ctx.appendEvent("lesson_capture", { result: "no_findings", ticketsDone });
6975
+ return { action: "advance" };
6976
+ }
6977
+ ctx.appendEvent("lesson_capture", {
6978
+ result: "started",
6979
+ ticketsDone,
6980
+ planFindings,
6981
+ codeFindings
6982
+ });
6983
+ return {
6984
+ instruction: [
6985
+ "# Capture Lessons from Review Findings",
6986
+ "",
6987
+ `This session completed ${ticketsDone} ticket(s). Review summary:`,
6988
+ `- **Plan reviews:** ${planReviews.length} round(s), ${planCritical} critical, ${planMajor} major, ${planFindings} total findings`,
6989
+ `- **Code reviews:** ${codeReviews.length} round(s), ${codeCritical} critical, ${codeMajor} major, ${codeFindings} total findings`,
6990
+ "",
6991
+ "Review these findings for recurring patterns worth capturing as lessons:",
6992
+ "",
6993
+ "1. Call `claudestory_lesson_list` to see existing lessons",
6994
+ "2. For each pattern that recurred or was critical/major:",
6995
+ " - If it matches an existing lesson \u2192 call `claudestory_lesson_reinforce`",
6996
+ ' - If it\'s a new pattern \u2192 call `claudestory_lesson_create` with `source: "review"`',
6997
+ "3. Skip patterns that are one-off or already well-covered",
6998
+ `4. Call \`claudestory_autonomous_guide\` with completedAction: "lessons_captured"`,
6999
+ "",
7000
+ "```json",
7001
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "lessons_captured" } }`,
7002
+ "```"
7003
+ ].join("\n"),
7004
+ reminders: [
7005
+ "Check existing lessons first \u2014 reinforce before creating duplicates.",
7006
+ "Only capture patterns worth remembering across sessions."
7007
+ ]
7008
+ };
7009
+ }
7010
+ async report(ctx, _report) {
7011
+ ctx.appendEvent("lesson_capture", { result: "completed" });
7012
+ return { action: "advance" };
7013
+ }
7014
+ };
7015
+
6918
7016
  // src/autonomous/stages/issue-sweep.ts
6919
7017
  init_esm_shims();
6920
7018
  var IssueSweepStage = class {
@@ -7105,6 +7203,7 @@ registerStage(new CodeReviewStage());
7105
7203
  registerStage(new VerifyStage());
7106
7204
  registerStage(new FinalizeStage());
7107
7205
  registerStage(new CompleteStage());
7206
+ registerStage(new LessonCaptureStage());
7108
7207
  registerStage(new IssueSweepStage());
7109
7208
  registerStage(new HandoverStage());
7110
7209
 
@@ -7728,7 +7827,8 @@ async function processAdvance(ctx, currentStage, advance, depth = 0) {
7728
7827
  }
7729
7828
  if (next.kind === "exhausted") {
7730
7829
  const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
7731
- const post = findFirstPostComplete(postComplete, ctx);
7830
+ const isInPostComplete = postComplete.includes(currentStage.id);
7831
+ const post = isInPostComplete ? findNextPostComplete(postComplete, currentStage.id, ctx) : findFirstPostComplete(postComplete, ctx);
7732
7832
  if (post.kind === "found") {
7733
7833
  assertTransition(currentStage.id, post.stage.id);
7734
7834
  ctx.writeState({ state: post.stage.id, previousState: currentStage.id });
@@ -7874,6 +7974,7 @@ async function handleResume(root, args) {
7874
7974
  // T-128: tests invalidated by HEAD change
7875
7975
  CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
7876
7976
  FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
7977
+ LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
7877
7978
  ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
7878
7979
  // T-128: post-complete, restart sweep
7879
7980
  };
@@ -9294,7 +9395,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9294
9395
  // src/mcp/index.ts
9295
9396
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9296
9397
  var CONFIG_PATH2 = ".story/config.json";
9297
- var version = "0.1.27";
9398
+ var version = "0.1.29";
9298
9399
  function tryDiscoverRoot() {
9299
9400
  const envRoot = process.env[ENV_VAR2];
9300
9401
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "license": "UNLICENSED",
5
5
  "description": "Cross-session context persistence for AI coding projects. Tracks tickets, issues, roadmap, and handovers so every session builds on the last.",
6
6
  "keywords": [
@@ -199,6 +199,14 @@ Call these in order:
199
199
  5. **Lessons learned** — call `claudestory_lesson_digest` MCP tool
200
200
  6. **Recent commits** — run `git log --oneline -10`
201
201
 
202
+ ## Step 2b: Empty Scaffold Check
203
+
204
+ After `claudestory_status` returns, check in order:
205
+
206
+ 1. **Integrity guard** — if the response starts with "Warning:" and contains "item(s) skipped due to data integrity issues", this is NOT an empty scaffold. Tell the user to run `claudestory validate`. Continue Step 2/3 normally.
207
+ 2. **Scaffold detection** — check BOTH: output contains "## Getting Started" AND shows `Tickets: 0/0 complete` + `Handovers: 0`. If met AND the project has code indicators (git history, package manifest, source files), route to the AI-Assisted Setup Flow (section 1b above) instead of Step 3. Skip remaining Step 2 calls and Step 3.
208
+ 3. **Empty without code** — if scaffold detected but no code indicators (truly empty directory), continue to Step 3 which will show: "Your project is set up but has no tickets yet. Would you like me to help you create your first phase and tickets?"
209
+
202
210
  ## Step 3: Present Summary
203
211
 
204
212
  After loading context, present a concise summary: