fathom-cli 0.2.2 → 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) {
@@ -17233,7 +17249,7 @@ async function executeBuildMode(mode, promptPath, state) {
17233
17249
  console.log(chalk4.dim("\n Launching Claude Code..."));
17234
17250
  console.log(chalk4.dim(` Context: ${state.featuresTotal} features, starting with ${state.buildQueue[0] ?? "queue"}`));
17235
17251
  console.log(chalk4.dim(" " + "\u2500".repeat(50)));
17236
- await launchClaude(promptPath);
17252
+ await launchClaude(promptPath, void 0, state.buildQueue[0]);
17237
17253
  console.log(chalk4.dim(" " + "\u2500".repeat(50)));
17238
17254
  console.log(chalk4.dim(" Session complete."));
17239
17255
  updateWorkflowState({ ...state, phase: "review", lastSessionAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -17262,7 +17278,7 @@ async function executeBuildMode(mode, promptPath, state) {
17262
17278
  const launchDir = existsSync6(worktreePath) ? worktreePath : process.cwd();
17263
17279
  console.log(chalk4.dim(` Launching Claude Code in ${launchDir}...`));
17264
17280
  console.log(chalk4.dim(" " + "\u2500".repeat(50)));
17265
- await launchClaude(promptPath, launchDir);
17281
+ await launchClaude(promptPath, launchDir, state.buildQueue[0]);
17266
17282
  console.log(chalk4.dim(" " + "\u2500".repeat(50)));
17267
17283
  console.log(chalk4.dim(" Session complete."));
17268
17284
  updateWorkflowState({ ...state, phase: "review", lastSessionAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -17282,14 +17298,19 @@ async function executeBuildMode(mode, promptPath, state) {
17282
17298
  }
17283
17299
  return false;
17284
17300
  }
17285
- function launchClaude(promptPath, cwd) {
17301
+ function launchClaude(promptPath, cwd, firstFeature) {
17286
17302
  return new Promise((resolve18) => {
17287
17303
  const promptContent = readFileSync6(promptPath, "utf-8");
17288
- const child = spawn("claude", ["--append-system-prompt", promptContent], {
17304
+ const initialPrompt = firstFeature ? `Read the build context in your system prompt. Start working on feature \`${firstFeature}\` \u2014 read the spec reference, plan the implementation, and begin.` : "Read the build context in your system prompt and start working on the first feature in the priority queue.";
17305
+ const child = spawn("claude", ["--append-system-prompt", promptContent, initialPrompt], {
17289
17306
  stdio: "inherit",
17290
17307
  cwd: cwd ?? process.cwd()
17291
17308
  });
17309
+ const sigintHandler = () => {
17310
+ };
17311
+ process.on("SIGINT", sigintHandler);
17292
17312
  child.on("error", (err) => {
17313
+ process.removeListener("SIGINT", sigintHandler);
17293
17314
  if (err.code === "ENOENT") {
17294
17315
  console.error(chalk4.red(" Claude Code not found. Install it first: https://claude.ai/code"));
17295
17316
  } else {
@@ -17298,6 +17319,7 @@ function launchClaude(promptPath, cwd) {
17298
17319
  resolve18();
17299
17320
  });
17300
17321
  child.on("close", () => {
17322
+ process.removeListener("SIGINT", sigintHandler);
17301
17323
  resolve18();
17302
17324
  });
17303
17325
  });