fathom-cli 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16630,222 +16630,230 @@ function displayIntakeTable(projectName, slices, totalTokens, totalCost) {
16630
16630
  );
16631
16631
  }
16632
16632
  var intakeCommand = new Command2("intake").description("Extract work items from raw feedback, bug reports, or meeting notes").argument("[text]", "Raw text input to analyze").option("-f, --file <path>", "Read input from a file").option("-p, --project <name>", "Project name").option("-o, --output <path>", "Write markdown spec to file").option("--json", "Output as JSON").option("--markdown", "Output as markdown to stdout").action(async (text3, options) => {
16633
- const cwd = process.cwd();
16634
- let projectName = options.project ?? getProjectName();
16635
- if (projectName === "unnamed") {
16636
- projectName = await input({
16637
- message: "Project name:",
16638
- default: basename(cwd)
16639
- });
16640
- updateProjectConfig({ project: projectName });
16641
- }
16642
- let rawInput;
16643
- let source;
16644
- if (options.file) {
16645
- const filePath = resolve5(options.file);
16646
- if (!existsSync4(filePath)) {
16647
- console.error(chalk3.red(`File not found: ${filePath}`));
16648
- process.exit(1);
16649
- }
16650
- rawInput = readFileSync4(filePath, "utf-8");
16651
- source = "file";
16652
- } else if (text3) {
16653
- rawInput = text3;
16654
- source = "text";
16655
- } else {
16656
- const localFiles = findLocalMarkdownFiles();
16657
- if (localFiles.length > 0) {
16658
- const fileChoices = localFiles.slice(0, 8).map((f) => ({
16659
- name: f,
16660
- value: f
16661
- }));
16662
- fileChoices.push(
16663
- { name: "Other file...", value: "__other__" },
16664
- { name: "Type/paste text instead", value: "__text__" }
16665
- );
16666
- const chosen = await select({
16667
- message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename(cwd)}):`,
16668
- choices: fileChoices
16633
+ try {
16634
+ const cwd = process.cwd();
16635
+ let projectName = options.project ?? getProjectName();
16636
+ if (projectName === "unnamed") {
16637
+ projectName = await input({
16638
+ message: "Project name:",
16639
+ default: basename(cwd)
16669
16640
  });
16670
- if (chosen === "__text__") {
16671
- rawInput = await input({
16672
- message: "Paste your feedback / requirements:"
16673
- });
16674
- source = "text";
16675
- } else if (chosen === "__other__") {
16676
- const filePath = await input({
16677
- message: "Path to input file:",
16678
- default: cwd + "/"
16679
- });
16680
- const resolved = resolve5(filePath.trim());
16681
- if (!existsSync4(resolved)) {
16682
- console.error(chalk3.red(`File not found: ${resolved}`));
16683
- process.exit(1);
16684
- }
16685
- rawInput = readFileSync4(resolved, "utf-8");
16686
- source = "file";
16687
- } else {
16688
- rawInput = readFileSync4(resolve5(chosen), "utf-8");
16689
- source = "file";
16641
+ updateProjectConfig({ project: projectName });
16642
+ }
16643
+ let rawInput;
16644
+ let source;
16645
+ if (options.file) {
16646
+ const filePath = resolve5(options.file);
16647
+ if (!existsSync4(filePath)) {
16648
+ console.error(chalk3.red(`File not found: ${filePath}`));
16649
+ process.exit(1);
16690
16650
  }
16651
+ rawInput = readFileSync4(filePath, "utf-8");
16652
+ source = "file";
16653
+ } else if (text3) {
16654
+ rawInput = text3;
16655
+ source = "text";
16691
16656
  } else {
16692
- const inputType = await select({
16693
- message: "How would you like to provide input?",
16694
- choices: [
16695
- { name: "Enter a file path", value: "file" },
16696
- { name: "Type/paste text", value: "text" }
16697
- ]
16698
- });
16699
- if (inputType === "file") {
16700
- const filePath = await input({
16701
- message: "Path to input file:",
16702
- default: cwd + "/"
16657
+ const localFiles = findLocalMarkdownFiles();
16658
+ if (localFiles.length > 0) {
16659
+ const fileChoices = localFiles.slice(0, 8).map((f) => ({
16660
+ name: f,
16661
+ value: f
16662
+ }));
16663
+ fileChoices.push(
16664
+ { name: "Other file...", value: "__other__" },
16665
+ { name: "Type/paste text instead", value: "__text__" }
16666
+ );
16667
+ const chosen = await select({
16668
+ message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename(cwd)}):`,
16669
+ choices: fileChoices
16703
16670
  });
16704
- const resolved = resolve5(filePath.trim());
16705
- if (!existsSync4(resolved)) {
16706
- console.error(chalk3.red(`File not found: ${resolved}`));
16707
- process.exit(1);
16671
+ if (chosen === "__text__") {
16672
+ rawInput = await input({
16673
+ message: "Paste your feedback / requirements:"
16674
+ });
16675
+ source = "text";
16676
+ } else if (chosen === "__other__") {
16677
+ const filePath = await input({
16678
+ message: "Path to input file:",
16679
+ default: cwd + "/"
16680
+ });
16681
+ const resolved = resolve5(filePath.trim());
16682
+ if (!existsSync4(resolved)) {
16683
+ console.error(chalk3.red(`File not found: ${resolved}`));
16684
+ process.exit(1);
16685
+ }
16686
+ rawInput = readFileSync4(resolved, "utf-8");
16687
+ source = "file";
16688
+ } else {
16689
+ rawInput = readFileSync4(resolve5(chosen), "utf-8");
16690
+ source = "file";
16708
16691
  }
16709
- rawInput = readFileSync4(resolved, "utf-8");
16710
- source = "file";
16711
16692
  } else {
16712
- rawInput = await input({
16713
- message: "Paste your feedback / requirements:"
16693
+ const inputType = await select({
16694
+ message: "How would you like to provide input?",
16695
+ choices: [
16696
+ { name: "Enter a file path", value: "file" },
16697
+ { name: "Type/paste text", value: "text" }
16698
+ ]
16714
16699
  });
16715
- source = "text";
16700
+ if (inputType === "file") {
16701
+ const filePath = await input({
16702
+ message: "Path to input file:",
16703
+ default: cwd + "/"
16704
+ });
16705
+ const resolved = resolve5(filePath.trim());
16706
+ if (!existsSync4(resolved)) {
16707
+ console.error(chalk3.red(`File not found: ${resolved}`));
16708
+ process.exit(1);
16709
+ }
16710
+ rawInput = readFileSync4(resolved, "utf-8");
16711
+ source = "file";
16712
+ } else {
16713
+ rawInput = await input({
16714
+ message: "Paste your feedback / requirements:"
16715
+ });
16716
+ source = "text";
16717
+ }
16716
16718
  }
16717
16719
  }
16718
- }
16719
- if (!process.env.ANTHROPIC_API_KEY) {
16720
- console.error(chalk3.red("ANTHROPIC_API_KEY environment variable is required."));
16721
- console.error(chalk3.dim(" Add it to your .env file or export it:"));
16722
- console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
16723
- process.exit(1);
16724
- }
16725
- console.log(chalk3.dim("\nExtracting work items via Claude..."));
16726
- let items;
16727
- try {
16728
- items = await extractWorkItems(rawInput);
16729
- } catch (err) {
16730
- console.error(chalk3.red("Extraction failed:"), err.message);
16731
- process.exit(1);
16732
- }
16733
- if (items.length === 0) {
16734
- console.log(chalk3.yellow("No actionable work items found in input."));
16735
- process.exit(0);
16736
- }
16737
- console.log(chalk3.green(`Extracted ${items.length} work item${items.length === 1 ? "" : "s"}`));
16738
- console.log(chalk3.dim("Enriching with estimates..."));
16739
- const data = loadData();
16740
- const slices = enrichSlices(items, { data, project: projectName });
16741
- const totalTokens = slices.reduce((s, sl) => s + sl.estimatedTokens, 0);
16742
- const totalCost = slices.reduce((s, sl) => s + sl.estimatedCost, 0);
16743
- const result = {
16744
- project: projectName,
16745
- source,
16746
- rawInput,
16747
- slices,
16748
- totalEstimatedTokens: totalTokens,
16749
- totalEstimatedCost: totalCost,
16750
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
16751
- };
16752
- writeRegistry(projectName, slices);
16753
- if (options.json) {
16754
- console.log(JSON.stringify(result, null, 2));
16755
- return;
16756
- }
16757
- if (options.output) {
16758
- const outPath = resolve5(options.output);
16759
- mkdirSync4(dirname3(outPath), { recursive: true });
16760
- writeFileSync4(outPath, formatAsMarkdown(result));
16761
- console.log(chalk3.green(`
16720
+ if (!process.env.ANTHROPIC_API_KEY) {
16721
+ console.error(chalk3.red("ANTHROPIC_API_KEY environment variable is required."));
16722
+ console.error(chalk3.dim(" Add it to your .env file or export it:"));
16723
+ console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
16724
+ process.exit(1);
16725
+ }
16726
+ console.log(chalk3.dim("\nExtracting work items via Claude..."));
16727
+ let items;
16728
+ try {
16729
+ items = await extractWorkItems(rawInput);
16730
+ } catch (err) {
16731
+ console.error(chalk3.red("Extraction failed:"), err.message);
16732
+ process.exit(1);
16733
+ }
16734
+ if (items.length === 0) {
16735
+ console.log(chalk3.yellow("No actionable work items found in input."));
16736
+ process.exit(0);
16737
+ }
16738
+ console.log(chalk3.green(`Extracted ${items.length} work item${items.length === 1 ? "" : "s"}`));
16739
+ console.log(chalk3.dim("Enriching with estimates..."));
16740
+ const data = loadData();
16741
+ const slices = enrichSlices(items, { data, project: projectName });
16742
+ const totalTokens = slices.reduce((s, sl) => s + sl.estimatedTokens, 0);
16743
+ const totalCost = slices.reduce((s, sl) => s + sl.estimatedCost, 0);
16744
+ const result = {
16745
+ project: projectName,
16746
+ source,
16747
+ rawInput,
16748
+ slices,
16749
+ totalEstimatedTokens: totalTokens,
16750
+ totalEstimatedCost: totalCost,
16751
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
16752
+ };
16753
+ writeRegistry(projectName, slices);
16754
+ if (options.json) {
16755
+ console.log(JSON.stringify(result, null, 2));
16756
+ return;
16757
+ }
16758
+ if (options.output) {
16759
+ const outPath = resolve5(options.output);
16760
+ mkdirSync4(dirname3(outPath), { recursive: true });
16761
+ writeFileSync4(outPath, formatAsMarkdown(result));
16762
+ console.log(chalk3.green(`
16762
16763
  Spec written to ${outPath}`));
16763
- await doConvexSync(result, rawInput, source, projectName);
16764
- return;
16765
- }
16766
- if (options.markdown) {
16767
- console.log(formatAsMarkdown(result));
16768
- await doConvexSync(result, rawInput, source, projectName);
16769
- return;
16770
- }
16771
- console.log(chalk3.bold(`
16764
+ await doConvexSync(result, rawInput, source, projectName);
16765
+ return;
16766
+ }
16767
+ if (options.markdown) {
16768
+ console.log(formatAsMarkdown(result));
16769
+ await doConvexSync(result, rawInput, source, projectName);
16770
+ return;
16771
+ }
16772
+ console.log(chalk3.bold(`
16772
16773
  Fathom \u2014 Intake Analysis: ${projectName}`));
16773
- console.log(chalk3.dim("\u2500".repeat(60)));
16774
- const tableRows = formatSliceTable(slices);
16775
- const table = new Table({
16776
- head: [
16777
- chalk3.white("#"),
16778
- chalk3.white("Name"),
16779
- chalk3.white("Cat."),
16780
- chalk3.white("Size"),
16781
- chalk3.white("Pri."),
16782
- chalk3.white("Tokens"),
16783
- chalk3.white("Cost"),
16784
- chalk3.white("Benchmark")
16785
- ],
16786
- colAligns: ["right", "left", "left", "center", "center", "right", "right", "left"]
16787
- });
16788
- tableRows.forEach((row, i) => {
16789
- table.push([
16790
- String(i + 1),
16791
- row.name,
16792
- row.category,
16793
- row.complexity,
16794
- row.priority,
16795
- row.tokens,
16796
- row.cost,
16797
- row.benchmark
16798
- ]);
16799
- });
16800
- console.log(table.toString());
16801
- console.log(
16802
- chalk3.bold(`
16774
+ console.log(chalk3.dim("\u2500".repeat(60)));
16775
+ const tableRows = formatSliceTable(slices);
16776
+ const table = new Table({
16777
+ head: [
16778
+ chalk3.white("#"),
16779
+ chalk3.white("Name"),
16780
+ chalk3.white("Cat."),
16781
+ chalk3.white("Size"),
16782
+ chalk3.white("Pri."),
16783
+ chalk3.white("Tokens"),
16784
+ chalk3.white("Cost"),
16785
+ chalk3.white("Benchmark")
16786
+ ],
16787
+ colAligns: ["right", "left", "left", "center", "center", "right", "right", "left"]
16788
+ });
16789
+ tableRows.forEach((row, i) => {
16790
+ table.push([
16791
+ String(i + 1),
16792
+ row.name,
16793
+ row.category,
16794
+ row.complexity,
16795
+ row.priority,
16796
+ row.tokens,
16797
+ row.cost,
16798
+ row.benchmark
16799
+ ]);
16800
+ });
16801
+ console.log(table.toString());
16802
+ console.log(
16803
+ chalk3.bold(`
16803
16804
  Total: ${totalTokens.toLocaleString()} tokens \u2014 $${totalCost.toFixed(2)}`)
16804
- );
16805
- const intakeDir = getIntakeDir();
16806
- const datestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16807
- const defaultFile = `${intakeDir}/${projectName}-intake-${datestamp}.md`;
16808
- const saveAction = await select({
16809
- message: "What would you like to do with this spec?",
16810
- choices: [
16811
- { name: `Save to ${defaultFile}`, value: "default" },
16812
- { name: "Save to a custom location", value: "custom" },
16813
- { name: "Print markdown to console", value: "print" },
16814
- { name: "Skip (don't save)", value: "skip" }
16815
- ]
16816
- });
16817
- if (saveAction === "default" || saveAction === "custom") {
16818
- let outPath;
16819
- if (saveAction === "custom") {
16820
- const customPath = await input({
16821
- message: "Output file path:",
16822
- default: defaultFile
16823
- });
16824
- outPath = resolve5(customPath);
16825
- } else {
16826
- outPath = resolve5(defaultFile);
16827
- }
16828
- mkdirSync4(dirname3(outPath), { recursive: true });
16829
- writeFileSync4(outPath, formatAsMarkdown(result));
16830
- console.log(chalk3.green(`
16805
+ );
16806
+ const intakeDir = getIntakeDir();
16807
+ const datestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16808
+ const defaultFile = `${intakeDir}/${projectName}-intake-${datestamp}.md`;
16809
+ const saveAction = await select({
16810
+ message: "What would you like to do with this spec?",
16811
+ choices: [
16812
+ { name: `Save to ${defaultFile}`, value: "default" },
16813
+ { name: "Save to a custom location", value: "custom" },
16814
+ { name: "Print markdown to console", value: "print" },
16815
+ { name: "Skip (don't save)", value: "skip" }
16816
+ ]
16817
+ });
16818
+ if (saveAction === "default" || saveAction === "custom") {
16819
+ let outPath;
16820
+ if (saveAction === "custom") {
16821
+ const customPath = await input({
16822
+ message: "Output file path:",
16823
+ default: defaultFile
16824
+ });
16825
+ outPath = resolve5(customPath);
16826
+ } else {
16827
+ outPath = resolve5(defaultFile);
16828
+ }
16829
+ mkdirSync4(dirname3(outPath), { recursive: true });
16830
+ writeFileSync4(outPath, formatAsMarkdown(result));
16831
+ console.log(chalk3.green(`
16831
16832
  Spec written to ${outPath}`));
16832
- const chosenDir = dirname3(outPath);
16833
- const resolvedDefaultDir = resolve5(intakeDir);
16834
- if (chosenDir !== resolvedDefaultDir) {
16835
- const remember = await confirm({
16836
- message: `Use "${dirname3(outPath).replace(resolve5(".") + "/", "")}" as default output directory next time?`,
16837
- default: true
16838
- });
16839
- if (remember) {
16840
- const relativeDir = dirname3(outPath).replace(resolve5(".") + "/", "");
16841
- updateProjectConfig({ intakeDir: relativeDir });
16842
- console.log(chalk3.dim(` Saved to .claude/te/config.json`));
16833
+ const chosenDir = dirname3(outPath);
16834
+ const resolvedDefaultDir = resolve5(intakeDir);
16835
+ if (chosenDir !== resolvedDefaultDir) {
16836
+ const remember = await confirm({
16837
+ message: `Use "${dirname3(outPath).replace(resolve5(".") + "/", "")}" as default output directory next time?`,
16838
+ default: true
16839
+ });
16840
+ if (remember) {
16841
+ const relativeDir = dirname3(outPath).replace(resolve5(".") + "/", "");
16842
+ updateProjectConfig({ intakeDir: relativeDir });
16843
+ console.log(chalk3.dim(` Saved to .claude/te/config.json`));
16844
+ }
16843
16845
  }
16846
+ } else if (saveAction === "print") {
16847
+ console.log("\n" + formatAsMarkdown(result));
16844
16848
  }
16845
- } else if (saveAction === "print") {
16846
- console.log("\n" + formatAsMarkdown(result));
16849
+ await doConvexSync(result, rawInput, source, projectName);
16850
+ } catch (err) {
16851
+ if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
16852
+ console.log(chalk3.dim("\n Exiting.\n"));
16853
+ return;
16854
+ }
16855
+ throw err;
16847
16856
  }
16848
- await doConvexSync(result, rawInput, source, projectName);
16849
16857
  });
16850
16858
  function writeRegistry(projectName, slices) {
16851
16859
  const teDir = resolve5(process.cwd(), ".claude", "te");
@@ -17015,191 +17023,199 @@ function printProgress(state) {
17015
17023
  );
17016
17024
  }
17017
17025
  var goCommand = new Command3("go").description("Run the full workflow: intake \u2192 build \u2192 review").option("--from <phase>", "Start from a specific phase (intake, build, review)").option("--auto", "Skip confirmation prompts").action(async (options) => {
17018
- const projectState = detectProjectState();
17019
- const config = readProjectConfig();
17020
- const workflowState = readWorkflowState();
17021
- let projectName = config?.project ?? "unnamed";
17022
- console.log();
17023
- if (projectState === "new") {
17024
- console.log(chalk4.bold(" Welcome to Fathom"));
17025
- console.log();
17026
- console.log(chalk4.dim(" No project detected. Let's set one up."));
17027
- console.log();
17028
- const name = await input2({
17029
- message: "Project name:",
17030
- default: basename2(process.cwd())
17031
- });
17032
- scaffoldProject(name, { quiet: true });
17033
- updateProjectConfig({ project: name });
17034
- console.log(chalk4.green(` \u2713 Project "${name}" initialized`));
17026
+ try {
17027
+ const projectState = detectProjectState();
17028
+ const config = readProjectConfig();
17029
+ const workflowState = readWorkflowState();
17030
+ let projectName = config?.project ?? "unnamed";
17035
17031
  console.log();
17036
- projectName = name;
17037
- } else if (projectState === "returning") {
17038
- console.log(chalk4.bold(` Fathom \u2014 ${projectName}`));
17039
- printProgress(workflowState);
17040
- } else {
17041
- console.log(chalk4.bold(` Fathom \u2014 ${projectName}`));
17042
- }
17043
- console.log();
17044
- let startPhase;
17045
- if (options.from) {
17046
- const valid = ["intake", "build", "review"];
17047
- if (!valid.includes(options.from)) {
17048
- console.error(chalk4.red(`Invalid phase: ${options.from}. Use: intake, build, review`));
17049
- process.exit(1);
17050
- }
17051
- startPhase = options.from;
17052
- } else if (projectState === "returning") {
17053
- const action = await select2({
17054
- message: "What would you like to do?",
17055
- choices: [
17056
- {
17057
- name: `Continue building${workflowState?.buildQueue[0] ? ` (next: ${workflowState.buildQueue[0]})` : ""}`,
17058
- value: "build"
17059
- },
17060
- { name: "New intake \u2014 add more work", value: "intake" },
17061
- { name: "Review \u2014 reconcile recent sessions", value: "review" },
17062
- { name: "Status overview", value: "status" }
17063
- ]
17064
- });
17065
- if (action === "status") {
17066
- printStatus(workflowState);
17067
- return;
17032
+ if (projectState === "new") {
17033
+ console.log(chalk4.bold(" Welcome to Fathom"));
17034
+ console.log();
17035
+ console.log(chalk4.dim(" No project detected. Let's set one up."));
17036
+ console.log();
17037
+ const name = await input2({
17038
+ message: "Project name:",
17039
+ default: basename2(process.cwd())
17040
+ });
17041
+ scaffoldProject(name, { quiet: true });
17042
+ updateProjectConfig({ project: name });
17043
+ console.log(chalk4.green(` \u2713 Project "${name}" initialized`));
17044
+ console.log();
17045
+ projectName = name;
17046
+ } else if (projectState === "returning") {
17047
+ console.log(chalk4.bold(` Fathom \u2014 ${projectName}`));
17048
+ printProgress(workflowState);
17049
+ } else {
17050
+ console.log(chalk4.bold(` Fathom \u2014 ${projectName}`));
17068
17051
  }
17069
- startPhase = action;
17070
- } else {
17071
- startPhase = "intake";
17072
- }
17073
- const phaseIndex = PHASES.findIndex((p) => p.id === startPhase);
17074
- for (let i = phaseIndex; i < PHASES.length; i++) {
17075
- const phase = PHASES[i];
17076
- console.log(chalk4.bold(` \u2500\u2500 ${phase.label} ${"\u2500".repeat(40 - phase.label.length)}`));
17077
- console.log(chalk4.dim(` ${phase.description}`));
17078
17052
  console.log();
17079
- if (i > phaseIndex && !options.auto) {
17080
- const proceed = await confirm2({
17081
- message: `Continue to ${phase.label.toLowerCase()} phase?`,
17082
- default: true
17053
+ let startPhase;
17054
+ if (options.from) {
17055
+ const valid = ["intake", "build", "review"];
17056
+ if (!valid.includes(options.from)) {
17057
+ console.error(chalk4.red(`Invalid phase: ${options.from}. Use: intake, build, review`));
17058
+ process.exit(1);
17059
+ }
17060
+ startPhase = options.from;
17061
+ } else if (projectState === "returning") {
17062
+ const action = await select2({
17063
+ message: "What would you like to do?",
17064
+ choices: [
17065
+ {
17066
+ name: `Continue building${workflowState?.buildQueue[0] ? ` (next: ${workflowState.buildQueue[0]})` : ""}`,
17067
+ value: "build"
17068
+ },
17069
+ { name: "New intake \u2014 add more work", value: "intake" },
17070
+ { name: "Review \u2014 reconcile recent sessions", value: "review" },
17071
+ { name: "Status overview", value: "status" }
17072
+ ]
17083
17073
  });
17084
- if (!proceed) {
17085
- console.log(chalk4.dim("\n Stopped. Resume with: fathom --from " + phase.id));
17074
+ if (action === "status") {
17075
+ printStatus(workflowState);
17086
17076
  return;
17087
17077
  }
17088
- console.log();
17078
+ startPhase = action;
17079
+ } else {
17080
+ startPhase = "intake";
17089
17081
  }
17090
- switch (phase.id) {
17091
- case "intake": {
17092
- const { result, specPath } = await runIntakeFlow(projectName);
17093
- const newQueue = result.slices.sort((a, b) => {
17094
- const priOrder = { P0: 0, P1: 1, P2: 2, P3: 3 };
17095
- return (priOrder[a.priority] ?? 9) - (priOrder[b.priority] ?? 9);
17096
- }).map((s) => s.sliceId);
17097
- const existingState = readWorkflowState();
17098
- if (existingState && existingState.featuresTotal > 0) {
17099
- const mergedQueue = [.../* @__PURE__ */ new Set([...existingState.buildQueue, ...newQueue])];
17100
- const newP1s = result.slices.filter((s) => s.priority === "P0" || s.priority === "P1");
17101
- updateWorkflowState({
17102
- ...existingState,
17103
- phase: "build",
17104
- specPath,
17105
- featuresTotal: existingState.featuresTotal + result.slices.length,
17106
- totalBudget: existingState.totalBudget + result.totalEstimatedCost,
17107
- buildQueue: mergedQueue
17108
- });
17109
- console.log(chalk4.green(`
17082
+ const phaseIndex = PHASES.findIndex((p) => p.id === startPhase);
17083
+ for (let i = phaseIndex; i < PHASES.length; i++) {
17084
+ const phase = PHASES[i];
17085
+ console.log(chalk4.bold(` \u2500\u2500 ${phase.label} ${"\u2500".repeat(40 - phase.label.length)}`));
17086
+ console.log(chalk4.dim(` ${phase.description}`));
17087
+ console.log();
17088
+ if (i > phaseIndex && !options.auto) {
17089
+ const proceed = await confirm2({
17090
+ message: `Continue to ${phase.label.toLowerCase()} phase?`,
17091
+ default: true
17092
+ });
17093
+ if (!proceed) {
17094
+ console.log(chalk4.dim("\n Stopped. Resume with: fathom --from " + phase.id));
17095
+ return;
17096
+ }
17097
+ console.log();
17098
+ }
17099
+ switch (phase.id) {
17100
+ case "intake": {
17101
+ const { result, specPath } = await runIntakeFlow(projectName);
17102
+ const newQueue = result.slices.sort((a, b) => {
17103
+ const priOrder = { P0: 0, P1: 1, P2: 2, P3: 3 };
17104
+ return (priOrder[a.priority] ?? 9) - (priOrder[b.priority] ?? 9);
17105
+ }).map((s) => s.sliceId);
17106
+ const existingState = readWorkflowState();
17107
+ if (existingState && existingState.featuresTotal > 0) {
17108
+ const mergedQueue = [.../* @__PURE__ */ new Set([...existingState.buildQueue, ...newQueue])];
17109
+ const newP1s = result.slices.filter((s) => s.priority === "P0" || s.priority === "P1");
17110
+ updateWorkflowState({
17111
+ ...existingState,
17112
+ phase: "build",
17113
+ specPath,
17114
+ featuresTotal: existingState.featuresTotal + result.slices.length,
17115
+ totalBudget: existingState.totalBudget + result.totalEstimatedCost,
17116
+ buildQueue: mergedQueue
17117
+ });
17118
+ console.log(chalk4.green(`
17110
17119
  \u2713 ${result.slices.length} new features added (${existingState.featuresTotal + result.slices.length} total)`));
17111
- if (newP1s.length > 0 && !options.auto) {
17112
- console.log(chalk4.bold(`
17120
+ if (newP1s.length > 0 && !options.auto) {
17121
+ console.log(chalk4.bold(`
17113
17122
  ${newP1s.length} new high-priority item${newP1s.length === 1 ? "" : "s"}: ${newP1s.map((s) => s.name).join(", ")}`));
17114
- const reprioritize = await confirm2({
17115
- message: "Move new P0/P1 items to front of build queue?",
17116
- default: true
17117
- });
17118
- if (reprioritize) {
17119
- const p1Ids = new Set(newP1s.map((s) => s.sliceId));
17120
- const reordered = [
17121
- ...newQueue.filter((id) => p1Ids.has(id)),
17122
- ...mergedQueue.filter((id) => !p1Ids.has(id))
17123
- ];
17124
- updateWorkflowState({
17125
- ...readWorkflowState(),
17126
- buildQueue: reordered
17123
+ const reprioritize = await confirm2({
17124
+ message: "Move new P0/P1 items to front of build queue?",
17125
+ default: true
17127
17126
  });
17128
- console.log(chalk4.green(" \u2713 Build queue reprioritized"));
17127
+ if (reprioritize) {
17128
+ const p1Ids = new Set(newP1s.map((s) => s.sliceId));
17129
+ const reordered = [
17130
+ ...newQueue.filter((id) => p1Ids.has(id)),
17131
+ ...mergedQueue.filter((id) => !p1Ids.has(id))
17132
+ ];
17133
+ updateWorkflowState({
17134
+ ...readWorkflowState(),
17135
+ buildQueue: reordered
17136
+ });
17137
+ console.log(chalk4.green(" \u2713 Build queue reprioritized"));
17138
+ }
17129
17139
  }
17140
+ } else {
17141
+ updateWorkflowState({
17142
+ phase: "build",
17143
+ specPath,
17144
+ featuresTotal: result.slices.length,
17145
+ featuresComplete: 0,
17146
+ totalBudget: result.totalEstimatedCost,
17147
+ totalSpent: 0,
17148
+ buildQueue: newQueue
17149
+ });
17130
17150
  }
17131
- } else {
17132
- updateWorkflowState({
17133
- phase: "build",
17134
- specPath,
17135
- featuresTotal: result.slices.length,
17136
- featuresComplete: 0,
17137
- totalBudget: result.totalEstimatedCost,
17138
- totalSpent: 0,
17139
- buildQueue: newQueue
17140
- });
17141
- }
17142
- break;
17143
- }
17144
- case "build": {
17145
- const currentState = readWorkflowState();
17146
- if (!currentState || currentState.buildQueue.length === 0) {
17147
- console.log(chalk4.dim(" No features in build queue."));
17148
17151
  break;
17149
17152
  }
17150
- const promptPath = generateBuildPrompt({
17151
- projectName,
17152
- workflowState: currentState
17153
- });
17154
- console.log(chalk4.green(` \u2713 Build prompt generated`));
17155
- const nextFeatures = currentState.buildQueue.slice(0, 3);
17156
- console.log(chalk4.dim(` Next up: ${nextFeatures.join(", ")}`));
17157
- console.log();
17158
- const savedMode = readProjectConfig()?.buildMode;
17159
- let buildMode;
17160
- if (savedMode) {
17161
- buildMode = savedMode;
17162
- console.log(chalk4.dim(` Using saved build mode: ${buildMode}`));
17163
- } else {
17164
- const chosen = await select2({
17165
- message: "How would you like to build?",
17166
- choices: [
17167
- { name: "Launch Claude Code (recommended)", value: "launch" },
17168
- { name: "Launch in worktree (isolated branch per feature)", value: "worktree" },
17169
- { name: "I'll handle it \u2014 just give me the spec", value: "manual" }
17170
- ]
17153
+ case "build": {
17154
+ const currentState = readWorkflowState();
17155
+ if (!currentState || currentState.buildQueue.length === 0) {
17156
+ console.log(chalk4.dim(" No features in build queue."));
17157
+ break;
17158
+ }
17159
+ const promptPath = generateBuildPrompt({
17160
+ projectName,
17161
+ workflowState: currentState
17171
17162
  });
17172
- buildMode = chosen;
17173
- updateProjectConfig({ buildMode });
17163
+ console.log(chalk4.green(` \u2713 Build prompt generated`));
17164
+ const nextFeatures = currentState.buildQueue.slice(0, 3);
17165
+ console.log(chalk4.dim(` Next up: ${nextFeatures.join(", ")}`));
17166
+ console.log();
17167
+ const savedMode = readProjectConfig()?.buildMode;
17168
+ let buildMode;
17169
+ if (savedMode) {
17170
+ buildMode = savedMode;
17171
+ console.log(chalk4.dim(` Using saved build mode: ${buildMode}`));
17172
+ } else {
17173
+ const chosen = await select2({
17174
+ message: "How would you like to build?",
17175
+ choices: [
17176
+ { name: "Launch Claude Code (recommended)", value: "launch" },
17177
+ { name: "Launch in worktree (isolated branch per feature)", value: "worktree" },
17178
+ { name: "I'll handle it \u2014 just give me the spec", value: "manual" }
17179
+ ]
17180
+ });
17181
+ buildMode = chosen;
17182
+ updateProjectConfig({ buildMode });
17183
+ }
17184
+ const skipReview = await executeBuildMode(buildMode, promptPath, currentState);
17185
+ if (skipReview) {
17186
+ return;
17187
+ }
17188
+ break;
17174
17189
  }
17175
- const skipReview = await executeBuildMode(buildMode, promptPath, currentState);
17176
- if (skipReview) {
17177
- return;
17190
+ case "review": {
17191
+ await runReviewPhase(projectName);
17192
+ break;
17178
17193
  }
17179
- break;
17180
- }
17181
- case "review": {
17182
- await runReviewPhase(projectName);
17183
- break;
17184
17194
  }
17195
+ console.log();
17196
+ }
17197
+ console.log(chalk4.bold(" \u2500\u2500 DONE " + "\u2500".repeat(38)));
17198
+ const finalState = readWorkflowState();
17199
+ if (finalState) {
17200
+ printProgress(finalState);
17185
17201
  }
17186
17202
  console.log();
17187
- }
17188
- console.log(chalk4.bold(" \u2500\u2500 DONE " + "\u2500".repeat(38)));
17189
- const finalState = readWorkflowState();
17190
- if (finalState) {
17191
- printProgress(finalState);
17192
- }
17193
- console.log();
17194
- if (!options.auto && finalState && finalState.buildQueue.length > 0) {
17195
- const cont = await confirm2({
17196
- message: "Continue building?",
17197
- default: true
17198
- });
17199
- if (cont) {
17200
- console.log(chalk4.dim("\n Restarting from build phase...\n"));
17201
- await goCommand.parseAsync(["node", "fathom", "go", "--from", "build"], { from: "node" });
17203
+ if (!options.auto && finalState && finalState.buildQueue.length > 0) {
17204
+ const cont = await confirm2({
17205
+ message: "Continue building?",
17206
+ default: true
17207
+ });
17208
+ if (cont) {
17209
+ console.log(chalk4.dim("\n Restarting from build phase...\n"));
17210
+ await goCommand.parseAsync(["node", "fathom", "go", "--from", "build"], { from: "node" });
17211
+ }
17212
+ }
17213
+ } catch (err) {
17214
+ if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
17215
+ console.log(chalk4.dim("\n Exiting.\n"));
17216
+ return;
17202
17217
  }
17218
+ throw err;
17203
17219
  }
17204
17220
  });
17205
17221
  async function runReviewPhase(projectName) {
@@ -17290,7 +17306,11 @@ function launchClaude(promptPath, cwd, firstFeature) {
17290
17306
  stdio: "inherit",
17291
17307
  cwd: cwd ?? process.cwd()
17292
17308
  });
17309
+ const sigintHandler = () => {
17310
+ };
17311
+ process.on("SIGINT", sigintHandler);
17293
17312
  child.on("error", (err) => {
17313
+ process.removeListener("SIGINT", sigintHandler);
17294
17314
  if (err.code === "ENOENT") {
17295
17315
  console.error(chalk4.red(" Claude Code not found. Install it first: https://claude.ai/code"));
17296
17316
  } else {
@@ -17299,6 +17319,7 @@ function launchClaude(promptPath, cwd, firstFeature) {
17299
17319
  resolve18();
17300
17320
  });
17301
17321
  child.on("close", () => {
17322
+ process.removeListener("SIGINT", sigintHandler);
17302
17323
  resolve18();
17303
17324
  });
17304
17325
  });