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 +385 -364
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
projectName
|
|
16637
|
-
|
|
16638
|
-
|
|
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
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
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
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
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
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
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
|
-
|
|
16713
|
-
message: "
|
|
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
|
-
|
|
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
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
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
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16771
|
-
|
|
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
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
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
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
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
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
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
|
-
|
|
16846
|
-
|
|
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
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
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
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
console.
|
|
17049
|
-
|
|
17050
|
-
}
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
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
|
-
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
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 (
|
|
17085
|
-
|
|
17074
|
+
if (action === "status") {
|
|
17075
|
+
printStatus(workflowState);
|
|
17086
17076
|
return;
|
|
17087
17077
|
}
|
|
17088
|
-
|
|
17078
|
+
startPhase = action;
|
|
17079
|
+
} else {
|
|
17080
|
+
startPhase = "intake";
|
|
17089
17081
|
}
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
const
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
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
|
-
|
|
17112
|
-
|
|
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
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
|
|
17158
|
-
|
|
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
|
-
|
|
17173
|
-
|
|
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
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
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
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17192
|
-
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
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
|
});
|