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 +390 -368
- 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) {
|
|
@@ -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
|
|
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
|
});
|