@locusai/cli 0.17.6 → 0.17.7

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.
Files changed (2) hide show
  1. package/bin/locus.js +452 -96
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -2805,11 +2805,17 @@ ${dim("Press Ctrl+C again to exit")}\r
2805
2805
  render();
2806
2806
  return;
2807
2807
  case SEQ_WORD_LEFT:
2808
+ case SEQ_SHIFT_LEFT:
2809
+ case SEQ_META_LEFT:
2810
+ case SEQ_META_SHIFT_LEFT:
2808
2811
  case "\x1Bb":
2809
2812
  moveWordLeft();
2810
2813
  render();
2811
2814
  return;
2812
2815
  case SEQ_WORD_RIGHT:
2816
+ case SEQ_SHIFT_RIGHT:
2817
+ case SEQ_META_RIGHT:
2818
+ case SEQ_META_SHIFT_RIGHT:
2813
2819
  case "\x1Bf":
2814
2820
  moveWordRight();
2815
2821
  render();
@@ -2833,7 +2839,7 @@ ${dim("Press Ctrl+C again to exit")}\r
2833
2839
  render();
2834
2840
  return;
2835
2841
  default:
2836
- if (seq.charCodeAt(0) >= 32 || seq.length > 1) {
2842
+ if (seq.charCodeAt(0) >= 32) {
2837
2843
  insertText(seq);
2838
2844
  render();
2839
2845
  }
@@ -3136,7 +3142,7 @@ ${dim("Press")} ${yellow("ESC")} ${dim("again to force exit")}\r
3136
3142
  }
3137
3143
  var CSI = "\x1B[", SAVE_CURSOR = "\x1B7", RESTORE_CURSOR = "\x1B8", ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, ENABLE_KITTY_KEYBOARD = "\x1B[>1u", DISABLE_KITTY_KEYBOARD = "\x1B[<u", PASTE_START, PASTE_END, CTRL_A = "\x01", CTRL_C = "\x03", CTRL_D = "\x04", CTRL_E = "\x05", CTRL_K = "\v", CTRL_J = `
3138
3144
  `, CTRL_U = "\x15", CTRL_W = "\x17", TAB = "\t", ENTER = "\r", ENTER_CRLF = `\r
3139
- `, ESC = "\x1B", BACKSPACE = "", SEQ_LEFT, SEQ_RIGHT, SEQ_UP, SEQ_DOWN, SEQ_HOME, SEQ_END, SEQ_HOME_1, SEQ_END_4, SEQ_HOME_O = "\x1BOH", SEQ_END_O = "\x1BOF", SEQ_DELETE, SEQ_WORD_LEFT, SEQ_WORD_RIGHT, SEQ_SHIFT_ENTER_CSI_U, SEQ_SHIFT_ENTER_MODIFY, SEQ_SHIFT_ENTER_TILDE, SEQ_ALT_ENTER, CONTROL_SEQUENCES;
3145
+ `, ESC = "\x1B", BACKSPACE = "", SEQ_LEFT, SEQ_RIGHT, SEQ_UP, SEQ_DOWN, SEQ_HOME, SEQ_END, SEQ_HOME_1, SEQ_END_4, SEQ_HOME_O = "\x1BOH", SEQ_END_O = "\x1BOF", SEQ_DELETE, SEQ_WORD_LEFT, SEQ_WORD_RIGHT, SEQ_SHIFT_LEFT, SEQ_SHIFT_RIGHT, SEQ_META_LEFT, SEQ_META_RIGHT, SEQ_META_SHIFT_LEFT, SEQ_META_SHIFT_RIGHT, SEQ_SHIFT_ENTER_CSI_U, SEQ_SHIFT_ENTER_MODIFY, SEQ_SHIFT_ENTER_TILDE, SEQ_ALT_ENTER, CONTROL_SEQUENCES;
3140
3146
  var init_input_handler = __esm(() => {
3141
3147
  init_terminal();
3142
3148
  init_image_detect();
@@ -3155,6 +3161,12 @@ var init_input_handler = __esm(() => {
3155
3161
  SEQ_DELETE = `${CSI}3~`;
3156
3162
  SEQ_WORD_LEFT = `${CSI}1;5D`;
3157
3163
  SEQ_WORD_RIGHT = `${CSI}1;5C`;
3164
+ SEQ_SHIFT_LEFT = `${CSI}1;2D`;
3165
+ SEQ_SHIFT_RIGHT = `${CSI}1;2C`;
3166
+ SEQ_META_LEFT = `${CSI}1;9D`;
3167
+ SEQ_META_RIGHT = `${CSI}1;9C`;
3168
+ SEQ_META_SHIFT_LEFT = `${CSI}1;10D`;
3169
+ SEQ_META_SHIFT_RIGHT = `${CSI}1;10C`;
3158
3170
  SEQ_SHIFT_ENTER_CSI_U = `${CSI}13;2u`;
3159
3171
  SEQ_SHIFT_ENTER_MODIFY = `${CSI}27;2;13~`;
3160
3172
  SEQ_SHIFT_ENTER_TILDE = `${CSI}13;2~`;
@@ -3168,6 +3180,12 @@ var init_input_handler = __esm(() => {
3168
3180
  SEQ_ALT_ENTER,
3169
3181
  SEQ_WORD_LEFT,
3170
3182
  SEQ_WORD_RIGHT,
3183
+ SEQ_META_SHIFT_LEFT,
3184
+ SEQ_META_SHIFT_RIGHT,
3185
+ SEQ_META_LEFT,
3186
+ SEQ_META_RIGHT,
3187
+ SEQ_SHIFT_LEFT,
3188
+ SEQ_SHIFT_RIGHT,
3171
3189
  SEQ_DELETE,
3172
3190
  SEQ_HOME_1,
3173
3191
  SEQ_END_4,
@@ -3454,6 +3472,10 @@ var init_runner = __esm(() => {
3454
3472
  });
3455
3473
 
3456
3474
  // src/ai/run-ai.ts
3475
+ var exports_run_ai = {};
3476
+ __export(exports_run_ai, {
3477
+ runAI: () => runAI
3478
+ });
3457
3479
  async function runAI(options) {
3458
3480
  const indicator = getStatusIndicator();
3459
3481
  const renderer = options.silent ? null : new StreamRenderer;
@@ -7673,14 +7695,23 @@ __export(exports_plan, {
7673
7695
  parsePlanOutput: () => parsePlanOutput,
7674
7696
  parsePlanArgs: () => parsePlanArgs
7675
7697
  });
7676
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
7698
+ import {
7699
+ existsSync as existsSync13,
7700
+ mkdirSync as mkdirSync10,
7701
+ readdirSync as readdirSync7,
7702
+ readFileSync as readFileSync10,
7703
+ writeFileSync as writeFileSync8
7704
+ } from "node:fs";
7677
7705
  import { join as join14 } from "node:path";
7678
7706
  function printHelp() {
7679
7707
  process.stderr.write(`
7680
7708
  ${bold("locus plan")} — AI-powered sprint planning
7681
7709
 
7682
7710
  ${bold("Usage:")}
7683
- locus plan "<directive>" ${dim("# AI breaks down into issues")}
7711
+ locus plan "<directive>" ${dim("# AI creates a plan file")}
7712
+ locus plan approve <id> ${dim("# Create GitHub issues from saved plan")}
7713
+ locus plan list ${dim("# List saved plans")}
7714
+ locus plan show <id> ${dim("# Show a saved plan")}
7684
7715
  locus plan --from-issues --sprint <name> ${dim("# Organize existing issues")}
7685
7716
 
7686
7717
  ${bold("Options:")}
@@ -7691,6 +7722,8 @@ ${bold("Options:")}
7691
7722
  ${bold("Examples:")}
7692
7723
  locus plan "Build user authentication with OAuth"
7693
7724
  locus plan "Improve API performance" --sprint "Sprint 3"
7725
+ locus plan approve abc123
7726
+ locus plan list
7694
7727
  locus plan --from-issues --sprint "Sprint 2"
7695
7728
 
7696
7729
  `);
@@ -7698,11 +7731,48 @@ ${bold("Examples:")}
7698
7731
  function normalizeSprintName(name) {
7699
7732
  return name.trim().toLowerCase();
7700
7733
  }
7734
+ function getPlansDir(projectRoot) {
7735
+ return join14(projectRoot, ".locus", "plans");
7736
+ }
7737
+ function ensurePlansDir(projectRoot) {
7738
+ const dir = getPlansDir(projectRoot);
7739
+ if (!existsSync13(dir)) {
7740
+ mkdirSync10(dir, { recursive: true });
7741
+ }
7742
+ return dir;
7743
+ }
7744
+ function generateId() {
7745
+ return `${Math.random().toString(36).slice(2, 8)}`;
7746
+ }
7747
+ function loadPlanFile(projectRoot, id) {
7748
+ const dir = getPlansDir(projectRoot);
7749
+ if (!existsSync13(dir))
7750
+ return null;
7751
+ const files = readdirSync7(dir).filter((f) => f.endsWith(".json"));
7752
+ const match = files.find((f) => f.startsWith(id));
7753
+ if (!match)
7754
+ return null;
7755
+ try {
7756
+ const content = readFileSync10(join14(dir, match), "utf-8");
7757
+ return JSON.parse(content);
7758
+ } catch {
7759
+ return null;
7760
+ }
7761
+ }
7701
7762
  async function planCommand(projectRoot, args, flags = {}) {
7702
7763
  if (args[0] === "help" || args.length === 0) {
7703
7764
  printHelp();
7704
7765
  return;
7705
7766
  }
7767
+ if (args[0] === "list") {
7768
+ return handleListPlans(projectRoot);
7769
+ }
7770
+ if (args[0] === "show") {
7771
+ return handleShowPlan(projectRoot, args[1]);
7772
+ }
7773
+ if (args[0] === "approve") {
7774
+ return handleApprovePlan(projectRoot, args[1], flags);
7775
+ }
7706
7776
  const parsedArgs = parsePlanArgs(args);
7707
7777
  if (parsedArgs.error) {
7708
7778
  process.stderr.write(`${red("✗")} ${parsedArgs.error}
@@ -7726,7 +7796,138 @@ async function planCommand(projectRoot, args, flags = {}) {
7726
7796
  }
7727
7797
  return handleAIPlan(projectRoot, config, directive, sprintName, flags);
7728
7798
  }
7799
+ function handleListPlans(projectRoot) {
7800
+ const dir = getPlansDir(projectRoot);
7801
+ if (!existsSync13(dir)) {
7802
+ process.stderr.write(`${dim("No saved plans yet.")}
7803
+ `);
7804
+ return;
7805
+ }
7806
+ const files = readdirSync7(dir).filter((f) => f.endsWith(".json")).sort().reverse();
7807
+ if (files.length === 0) {
7808
+ process.stderr.write(`${dim("No saved plans yet.")}
7809
+ `);
7810
+ return;
7811
+ }
7812
+ process.stderr.write(`
7813
+ ${bold("Saved Plans:")}
7814
+
7815
+ `);
7816
+ for (const file of files) {
7817
+ const id = file.replace(".json", "");
7818
+ try {
7819
+ const content = readFileSync10(join14(dir, file), "utf-8");
7820
+ const plan = JSON.parse(content);
7821
+ const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
7822
+ const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
7823
+ process.stderr.write(` ${cyan(id.slice(0, 12))} ${plan.directive.slice(0, 55)} ${dim(`${issueCount} issues`)} ${dim(date)}
7824
+ `);
7825
+ } catch {
7826
+ process.stderr.write(` ${cyan(id.slice(0, 12))} ${dim("(unreadable)")}
7827
+ `);
7828
+ }
7829
+ }
7830
+ process.stderr.write(`
7831
+ `);
7832
+ process.stderr.write(` Approve a plan: ${bold("locus plan approve <id>")}
7833
+
7834
+ `);
7835
+ }
7836
+ function handleShowPlan(projectRoot, id) {
7837
+ if (!id) {
7838
+ process.stderr.write(`${red("✗")} Please provide a plan ID.
7839
+ `);
7840
+ process.stderr.write(` Usage: ${bold("locus plan show <id>")}
7841
+ `);
7842
+ return;
7843
+ }
7844
+ const plan = loadPlanFile(projectRoot, id);
7845
+ if (!plan) {
7846
+ process.stderr.write(`${red("✗")} Plan "${id}" not found.
7847
+ `);
7848
+ process.stderr.write(` List plans with: ${bold("locus plan list")}
7849
+ `);
7850
+ return;
7851
+ }
7852
+ process.stderr.write(`
7853
+ ${bold("Plan:")} ${cyan(plan.directive)}
7854
+ `);
7855
+ process.stderr.write(` ${dim(`ID: ${plan.id}`)}
7856
+ `);
7857
+ if (plan.sprint) {
7858
+ process.stderr.write(` ${dim(`Sprint: ${plan.sprint}`)}
7859
+ `);
7860
+ }
7861
+ process.stderr.write(` ${dim(`Created: ${plan.createdAt.slice(0, 10)}`)}
7862
+ `);
7863
+ process.stderr.write(`
7864
+ `);
7865
+ process.stderr.write(` ${dim("Order")} ${dim("Title".padEnd(50))} ${dim("Priority")} ${dim("Type")}
7866
+ `);
7867
+ for (const issue of plan.issues) {
7868
+ process.stderr.write(` ${String(issue.order).padStart(5)} ${issue.title.padEnd(50).slice(0, 50)} ${issue.priority.padEnd(10)} ${issue.type}
7869
+ `);
7870
+ }
7871
+ process.stderr.write(`
7872
+ `);
7873
+ process.stderr.write(` Approve: ${bold(`locus plan approve ${plan.id.slice(0, 8)}`)}
7874
+
7875
+ `);
7876
+ }
7877
+ async function handleApprovePlan(projectRoot, id, flags) {
7878
+ if (!id) {
7879
+ process.stderr.write(`${red("✗")} Please provide a plan ID.
7880
+ `);
7881
+ process.stderr.write(` Usage: ${bold("locus plan approve <id>")}
7882
+ `);
7883
+ process.stderr.write(` List plans with: ${bold("locus plan list")}
7884
+ `);
7885
+ return;
7886
+ }
7887
+ const plan = loadPlanFile(projectRoot, id);
7888
+ if (!plan) {
7889
+ process.stderr.write(`${red("✗")} Plan "${id}" not found.
7890
+ `);
7891
+ process.stderr.write(` List plans with: ${bold("locus plan list")}
7892
+ `);
7893
+ return;
7894
+ }
7895
+ if (!Array.isArray(plan.issues) || plan.issues.length === 0) {
7896
+ process.stderr.write(`${red("✗")} Plan "${id}" has no issues.
7897
+ `);
7898
+ return;
7899
+ }
7900
+ const config = loadConfig(projectRoot);
7901
+ process.stderr.write(`
7902
+ ${bold("Approving plan:")} ${cyan(plan.directive)}
7903
+ `);
7904
+ if (plan.sprint) {
7905
+ process.stderr.write(` ${dim(`Sprint: ${plan.sprint}`)}
7906
+ `);
7907
+ }
7908
+ process.stderr.write(`
7909
+ `);
7910
+ process.stderr.write(` ${dim("Order")} ${dim("Title".padEnd(50))} ${dim("Priority")} ${dim("Type")}
7911
+ `);
7912
+ for (const issue of plan.issues) {
7913
+ process.stderr.write(` ${String(issue.order).padStart(5)} ${issue.title.padEnd(50).slice(0, 50)} ${issue.priority.padEnd(10)} ${issue.type}
7914
+ `);
7915
+ }
7916
+ process.stderr.write(`
7917
+ `);
7918
+ if (flags.dryRun) {
7919
+ process.stderr.write(`${yellow("⚠")} ${bold("Dry run")} — no issues created.
7920
+
7921
+ `);
7922
+ return;
7923
+ }
7924
+ await createPlannedIssues(projectRoot, config, plan.issues, plan.sprint ?? undefined);
7925
+ }
7729
7926
  async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
7927
+ const id = generateId();
7928
+ const plansDir = ensurePlansDir(projectRoot);
7929
+ const planPath = join14(plansDir, `${id}.json`);
7930
+ const planPathRelative = `.locus/plans/${id}.json`;
7730
7931
  process.stderr.write(`
7731
7932
  ${bold("Planning:")} ${cyan(directive)}
7732
7933
  `);
@@ -7736,55 +7937,65 @@ ${bold("Planning:")} ${cyan(directive)}
7736
7937
  }
7737
7938
  process.stderr.write(`
7738
7939
  `);
7739
- const context = buildPlanningContext(projectRoot, config, directive);
7740
- const aiResult = await runAI({
7741
- prompt: context,
7742
- provider: config.ai.provider,
7743
- model: flags.model ?? config.ai.model,
7744
- cwd: projectRoot,
7745
- activity: "sprint planning"
7746
- });
7747
- if (aiResult.interrupted) {
7940
+ const prompt = buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative);
7941
+ const { execCommand: execCommand2 } = await Promise.resolve().then(() => (init_exec(), exports_exec));
7942
+ await execCommand2(projectRoot, [prompt], {});
7943
+ if (!existsSync13(planPath)) {
7748
7944
  process.stderr.write(`
7749
- ${yellow("")} Planning interrupted.
7945
+ ${yellow("")} Plan file was not created at ${bold(planPathRelative)}.
7946
+ `);
7947
+ process.stderr.write(` Try again or create issues manually with ${bold("locus issue create")}.
7750
7948
  `);
7751
7949
  return;
7752
7950
  }
7753
- if (!aiResult.success) {
7951
+ let plan;
7952
+ try {
7953
+ const content = readFileSync10(planPath, "utf-8");
7954
+ plan = JSON.parse(content);
7955
+ } catch {
7754
7956
  process.stderr.write(`
7755
- ${red("✗")} Planning failed: ${aiResult.error ?? "Unknown error"}
7957
+ ${red("✗")} Plan file at ${bold(planPathRelative)} is not valid JSON.
7756
7958
  `);
7757
7959
  return;
7758
7960
  }
7759
- const output = sanitizePlanOutput(aiResult.output);
7760
- const planned = parsePlanOutput(output);
7761
- if (planned.length === 0) {
7961
+ if (!Array.isArray(plan.issues) || plan.issues.length === 0) {
7762
7962
  process.stderr.write(`
7763
- ${yellow("⚠")} Could not extract structured issues from plan.
7764
- `);
7765
- process.stderr.write(` The AI output is shown above. Create issues manually with ${bold("locus issue create")}.
7963
+ ${yellow("⚠")} Plan file has no issues.
7766
7964
  `);
7767
7965
  return;
7768
7966
  }
7967
+ if (!plan.id)
7968
+ plan.id = id;
7969
+ if (!plan.directive)
7970
+ plan.directive = directive;
7971
+ if (!plan.sprint && sprintName)
7972
+ plan.sprint = sprintName;
7973
+ if (!plan.createdAt)
7974
+ plan.createdAt = new Date().toISOString();
7975
+ writeFileSync8(planPath, JSON.stringify(plan, null, 2), "utf-8");
7769
7976
  process.stderr.write(`
7770
- ${bold("Planned Issues:")}
7977
+ ${bold("Plan saved:")} ${cyan(id)}
7771
7978
 
7772
7979
  `);
7773
- process.stderr.write(` ${dim("Order")} ${dim("Title".padEnd(45))} ${dim("Priority")} ${dim("Type")}
7980
+ process.stderr.write(` ${dim("Order")} ${dim("Title".padEnd(50))} ${dim("Priority")} ${dim("Type")}
7774
7981
  `);
7775
- for (const issue of planned) {
7776
- process.stderr.write(` ${String(issue.order).padStart(5)} ${issue.title.padEnd(45).slice(0, 45)} ${issue.priority.padEnd(10)} ${issue.type}
7982
+ for (const issue of plan.issues) {
7983
+ process.stderr.write(` ${String(issue.order).padStart(5)} ${issue.title.padEnd(50).slice(0, 50)} ${(issue.priority ?? "medium").padEnd(10)} ${issue.type ?? "feature"}
7777
7984
  `);
7778
7985
  }
7779
7986
  process.stderr.write(`
7780
7987
  `);
7781
7988
  if (flags.dryRun) {
7782
7989
  process.stderr.write(`${yellow("⚠")} ${bold("Dry run")} — no issues created.
7990
+ `);
7991
+ process.stderr.write(` Approve later with: ${bold(`locus plan approve ${id.slice(0, 8)}`)}
7783
7992
 
7784
7993
  `);
7785
7994
  return;
7786
7995
  }
7787
- await createPlannedIssues(projectRoot, config, planned, sprintName);
7996
+ process.stderr.write(` To create these issues: ${bold(`locus plan approve ${id.slice(0, 8)}`)}
7997
+
7998
+ `);
7788
7999
  }
7789
8000
  async function handleFromIssues(projectRoot, config, sprintName, flags) {
7790
8001
  if (!sprintName) {
@@ -7806,6 +8017,7 @@ ${bold("Organizing issues for:")} ${cyan(sprintName)}
7806
8017
  ${i.body?.slice(0, 300) ?? ""}`).join(`
7807
8018
 
7808
8019
  `);
8020
+ const { runAI: runAI2 } = await Promise.resolve().then(() => (init_run_ai(), exports_run_ai));
7809
8021
  const prompt = `You are organizing GitHub issues for a sprint. Analyze these issues and suggest the optimal execution order.
7810
8022
 
7811
8023
  Issues:
@@ -7816,7 +8028,7 @@ ORDER: #<number> <reason for this position>
7816
8028
 
7817
8029
  Order them so that dependencies are respected (issues that produce code needed by later issues should come first).
7818
8030
  Start with foundational/setup tasks, then core features, then integration/testing.`;
7819
- const aiResult = await runAI({
8031
+ const aiResult = await runAI2({
7820
8032
  prompt,
7821
8033
  provider: config.ai.provider,
7822
8034
  model: flags.model ?? config.ai.model,
@@ -7884,11 +8096,14 @@ ${bold("Suggested Order:")}
7884
8096
  `);
7885
8097
  }
7886
8098
  }
7887
- function buildPlanningContext(projectRoot, config, directive) {
8099
+ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
7888
8100
  const parts = [];
7889
8101
  parts.push(`You are a sprint planning assistant for the GitHub repository ${config.github.owner}/${config.github.repo}.`);
7890
8102
  parts.push("");
7891
8103
  parts.push(`DIRECTIVE: ${directive}`);
8104
+ if (sprintName) {
8105
+ parts.push(`SPRINT: ${sprintName}`);
8106
+ }
7892
8107
  parts.push("");
7893
8108
  const locusPath = join14(projectRoot, "LOCUS.md");
7894
8109
  if (existsSync13(locusPath)) {
@@ -7904,28 +8119,37 @@ function buildPlanningContext(projectRoot, config, directive) {
7904
8119
  parts.push(content.slice(0, 2000));
7905
8120
  parts.push("");
7906
8121
  }
7907
- parts.push("INSTRUCTIONS:");
7908
- parts.push("Break down the directive into specific, actionable GitHub issues.");
7909
- parts.push("Each issue should be independently executable by an AI agent.");
7910
- parts.push("Order them so dependencies are respected (foundational tasks first).");
8122
+ parts.push("TASK:");
8123
+ parts.push(`Break down the directive into specific, actionable GitHub issues and write them to the file: ${planPathRelative}`);
7911
8124
  parts.push("");
7912
- parts.push("For EACH issue, output EXACTLY this format (one per issue):");
8125
+ parts.push(`Write ONLY a valid JSON file to ${planPathRelative} with this exact structure:`);
7913
8126
  parts.push("");
7914
- parts.push("---ISSUE---");
7915
- parts.push("ORDER: <number>");
7916
- parts.push("TITLE: <concise issue title>");
7917
- parts.push("PRIORITY: <critical|high|medium|low>");
7918
- parts.push("TYPE: <feature|bug|chore|refactor|docs>");
7919
- parts.push("DEPENDS_ON: <comma-separated previous order numbers, or 'none'>");
7920
- parts.push("BODY:");
7921
- parts.push("<detailed issue description with acceptance criteria>");
7922
- parts.push("---END---");
8127
+ parts.push("```json");
8128
+ parts.push("{");
8129
+ parts.push(` "id": "${id}",`);
8130
+ parts.push(` "directive": ${JSON.stringify(directive)},`);
8131
+ parts.push(` "sprint": ${sprintName ? JSON.stringify(sprintName) : "null"},`);
8132
+ parts.push(` "createdAt": "${new Date().toISOString()}",`);
8133
+ parts.push(' "issues": [');
8134
+ parts.push(" {");
8135
+ parts.push(' "order": 1,');
8136
+ parts.push(' "title": "concise issue title",');
8137
+ parts.push(' "body": "detailed markdown body with acceptance criteria",');
8138
+ parts.push(' "priority": "critical|high|medium|low",');
8139
+ parts.push(' "type": "feature|bug|chore|refactor|docs",');
8140
+ parts.push(' "dependsOn": "none or comma-separated order numbers"');
8141
+ parts.push(" }");
8142
+ parts.push(" ]");
8143
+ parts.push("}");
8144
+ parts.push("```");
7923
8145
  parts.push("");
7924
- parts.push("Be specific in issue bodies. Include:");
7925
- parts.push("- What code/files should be created or modified");
7926
- parts.push("- Acceptance criteria (testable conditions)");
7927
- parts.push("- What previous tasks produce that this task needs");
7928
- parts.push("- Use valid GitHub Markdown only (no ANSI color/control codes)");
8146
+ parts.push("Requirements for the issues:");
8147
+ parts.push("- Break the directive into 3-10 specific, actionable issues");
8148
+ parts.push("- Each issue must be independently executable by an AI agent");
8149
+ parts.push("- Order them so dependencies are respected (foundational tasks first)");
8150
+ parts.push("- Write detailed issue bodies with clear acceptance criteria");
8151
+ parts.push("- Use valid GitHub Markdown only in issue bodies");
8152
+ parts.push("- Create the file using the Write tool — do not print the JSON to the terminal");
7929
8153
  return parts.join(`
7930
8154
  `);
7931
8155
  }
@@ -8055,7 +8279,6 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
8055
8279
  `);
8056
8280
  }
8057
8281
  var init_plan = __esm(() => {
8058
- init_run_ai();
8059
8282
  init_config();
8060
8283
  init_github();
8061
8284
  init_terminal();
@@ -8530,11 +8753,11 @@ __export(exports_discuss, {
8530
8753
  });
8531
8754
  import {
8532
8755
  existsSync as existsSync15,
8533
- mkdirSync as mkdirSync10,
8534
- readdirSync as readdirSync7,
8756
+ mkdirSync as mkdirSync11,
8757
+ readdirSync as readdirSync8,
8535
8758
  readFileSync as readFileSync12,
8536
8759
  unlinkSync as unlinkSync5,
8537
- writeFileSync as writeFileSync8
8760
+ writeFileSync as writeFileSync9
8538
8761
  } from "node:fs";
8539
8762
  import { join as join16 } from "node:path";
8540
8763
  import { createInterface as createInterface2 } from "node:readline";
@@ -8546,6 +8769,7 @@ ${bold("Usage:")}
8546
8769
  locus discuss "<topic>" ${dim("# Start a new discussion")}
8547
8770
  locus discuss list ${dim("# List all discussions")}
8548
8771
  locus discuss show <id> ${dim("# Show a discussion")}
8772
+ locus discuss plan <id> ${dim("# Convert discussion to a plan")}
8549
8773
  locus discuss delete <id> ${dim("# Delete a discussion")}
8550
8774
 
8551
8775
  ${bold("Examples:")}
@@ -8553,6 +8777,7 @@ ${bold("Examples:")}
8553
8777
  locus discuss "Monorepo vs polyrepo for our microservices"
8554
8778
  locus discuss list
8555
8779
  locus discuss show abc123
8780
+ locus discuss plan abc123
8556
8781
 
8557
8782
  `);
8558
8783
  }
@@ -8562,11 +8787,11 @@ function getDiscussionsDir(projectRoot) {
8562
8787
  function ensureDiscussionsDir(projectRoot) {
8563
8788
  const dir = getDiscussionsDir(projectRoot);
8564
8789
  if (!existsSync15(dir)) {
8565
- mkdirSync10(dir, { recursive: true });
8790
+ mkdirSync11(dir, { recursive: true });
8566
8791
  }
8567
8792
  return dir;
8568
8793
  }
8569
- function generateId() {
8794
+ function generateId2() {
8570
8795
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8571
8796
  }
8572
8797
  async function discussCommand(projectRoot, args, flags = {}) {
@@ -8581,6 +8806,9 @@ async function discussCommand(projectRoot, args, flags = {}) {
8581
8806
  if (subcommand === "show") {
8582
8807
  return showDiscussion(projectRoot, args[1]);
8583
8808
  }
8809
+ if (subcommand === "plan") {
8810
+ return convertDiscussionToPlan(projectRoot, args[1]);
8811
+ }
8584
8812
  if (subcommand === "delete") {
8585
8813
  return deleteDiscussion(projectRoot, args[1]);
8586
8814
  }
@@ -8602,7 +8830,7 @@ function listDiscussions(projectRoot) {
8602
8830
  `);
8603
8831
  return;
8604
8832
  }
8605
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8833
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8606
8834
  if (files.length === 0) {
8607
8835
  process.stderr.write(`${dim("No discussions yet.")}
8608
8836
  `);
@@ -8637,7 +8865,7 @@ function showDiscussion(projectRoot, id) {
8637
8865
  `);
8638
8866
  return;
8639
8867
  }
8640
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8868
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8641
8869
  const match = files.find((f) => f.startsWith(id));
8642
8870
  if (!match) {
8643
8871
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8660,7 +8888,7 @@ function deleteDiscussion(projectRoot, id) {
8660
8888
  `);
8661
8889
  return;
8662
8890
  }
8663
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8891
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8664
8892
  const match = files.find((f) => f.startsWith(id));
8665
8893
  if (!match) {
8666
8894
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8671,6 +8899,38 @@ function deleteDiscussion(projectRoot, id) {
8671
8899
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
8672
8900
  `);
8673
8901
  }
8902
+ async function convertDiscussionToPlan(projectRoot, id) {
8903
+ if (!id) {
8904
+ process.stderr.write(`${red("✗")} Please provide a discussion ID.
8905
+ `);
8906
+ process.stderr.write(` Usage: ${bold("locus discuss plan <id>")}
8907
+ `);
8908
+ return;
8909
+ }
8910
+ const dir = getDiscussionsDir(projectRoot);
8911
+ if (!existsSync15(dir)) {
8912
+ process.stderr.write(`${red("✗")} No discussions found.
8913
+ `);
8914
+ return;
8915
+ }
8916
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8917
+ const match = files.find((f) => f.startsWith(id));
8918
+ if (!match) {
8919
+ process.stderr.write(`${red("✗")} Discussion "${id}" not found.
8920
+ `);
8921
+ return;
8922
+ }
8923
+ const content = readFileSync12(join16(dir, match), "utf-8");
8924
+ process.stderr.write(`
8925
+ ${bold("Converting discussion to plan:")} ${cyan(id)}
8926
+
8927
+ `);
8928
+ await planCommand(projectRoot, [
8929
+ `Create a detailed, actionable implementation plan based on this discussion document:
8930
+
8931
+ ${content.slice(0, 8000)}`
8932
+ ], {});
8933
+ }
8674
8934
  async function promptForTopic() {
8675
8935
  return new Promise((resolve2) => {
8676
8936
  const rl = createInterface2({
@@ -8686,58 +8946,131 @@ async function promptForTopic() {
8686
8946
  rl.once("close", () => resolve2(""));
8687
8947
  });
8688
8948
  }
8949
+ async function promptForAnswers() {
8950
+ return new Promise((resolve2) => {
8951
+ const rl = createInterface2({
8952
+ input: process.stdin,
8953
+ output: process.stderr,
8954
+ terminal: true
8955
+ });
8956
+ const lines = [];
8957
+ rl.on("line", (line) => {
8958
+ if (line.trim() === "" && lines.length > 0) {
8959
+ rl.close();
8960
+ resolve2(lines.join(`
8961
+ `).trim());
8962
+ } else {
8963
+ lines.push(line);
8964
+ }
8965
+ });
8966
+ rl.once("close", () => resolve2(lines.join(`
8967
+ `).trim()));
8968
+ });
8969
+ }
8970
+ function isQuestionsResponse(output) {
8971
+ const trimmed = output.trimStart();
8972
+ if (trimmed.startsWith("#"))
8973
+ return false;
8974
+ const questionMarks = (trimmed.match(/\?/g) ?? []).length;
8975
+ return questionMarks >= 2;
8976
+ }
8689
8977
  async function startDiscussion(projectRoot, topic, flags) {
8690
8978
  const config = loadConfig(projectRoot);
8691
8979
  const timer = createTimer();
8692
- const id = generateId();
8980
+ const id = generateId2();
8693
8981
  process.stderr.write(`
8694
8982
  ${bold("Discussion:")} ${cyan(topic)}
8695
8983
 
8696
8984
  `);
8697
- const prompt = buildDiscussionPrompt(projectRoot, config, topic);
8698
- const aiResult = await runAI({
8699
- prompt,
8700
- provider: config.ai.provider,
8701
- model: flags.model ?? config.ai.model,
8702
- cwd: projectRoot,
8703
- activity: "discussion"
8704
- });
8705
- if (aiResult.interrupted) {
8706
- process.stderr.write(`
8985
+ const conversation = [];
8986
+ let finalAnalysis = "";
8987
+ for (let round = 0;round < MAX_DISCUSSION_ROUNDS; round++) {
8988
+ const isFinalRound = round === MAX_DISCUSSION_ROUNDS - 1;
8989
+ const prompt = buildDiscussionPrompt(projectRoot, config, topic, conversation, isFinalRound);
8990
+ const aiResult = await runAI({
8991
+ prompt,
8992
+ provider: config.ai.provider,
8993
+ model: flags.model ?? config.ai.model,
8994
+ cwd: projectRoot,
8995
+ activity: "discussion"
8996
+ });
8997
+ if (aiResult.interrupted) {
8998
+ process.stderr.write(`
8707
8999
  ${yellow("⚡")} Discussion interrupted.
8708
9000
  `);
8709
- if (!aiResult.output.trim())
9001
+ if (!aiResult.output.trim())
9002
+ return;
9003
+ finalAnalysis = aiResult.output.trim();
9004
+ break;
9005
+ }
9006
+ if (!aiResult.success && !aiResult.interrupted) {
9007
+ process.stderr.write(`
9008
+ ${red("✗")} Discussion failed: ${aiResult.error}
9009
+ `);
8710
9010
  return;
8711
- }
8712
- if (!aiResult.success && !aiResult.interrupted) {
9011
+ }
9012
+ const response = aiResult.output.trim();
9013
+ conversation.push({ role: "assistant", content: response });
9014
+ if (!isQuestionsResponse(response) || isFinalRound) {
9015
+ finalAnalysis = response;
9016
+ break;
9017
+ }
9018
+ process.stderr.write(`
9019
+ ${dim("─".repeat(50))}
9020
+ ${bold("Your answers:")} ${dim("(press Enter on an empty line when done)")}
9021
+ `);
9022
+ const answers = await promptForAnswers();
9023
+ if (!answers.trim()) {
9024
+ conversation.push({
9025
+ role: "user",
9026
+ content: "Please proceed with your analysis based on the information available."
9027
+ });
9028
+ } else {
9029
+ conversation.push({ role: "user", content: answers });
9030
+ }
8713
9031
  process.stderr.write(`
8714
- ${red("✗")} Discussion failed: ${aiResult.error}
8715
9032
  `);
8716
- return;
8717
9033
  }
8718
- const output = aiResult.output;
9034
+ if (!finalAnalysis)
9035
+ return;
8719
9036
  const dir = ensureDiscussionsDir(projectRoot);
8720
9037
  const date = new Date().toISOString().slice(0, 10);
8721
- const markdown = `# ${topic}
9038
+ const transcript = conversation.map((turn) => {
9039
+ const label = turn.role === "user" ? "You" : "AI";
9040
+ return `**${label}:**
8722
9041
 
8723
- **Date:** ${date}
8724
- **Provider:** ${config.ai.provider} / ${flags.model ?? config.ai.model}
9042
+ ${turn.content}`;
9043
+ }).join(`
8725
9044
 
8726
9045
  ---
8727
9046
 
8728
- ${output}
8729
- `;
8730
- writeFileSync8(join16(dir, `${id}.md`), markdown, "utf-8");
9047
+ `);
9048
+ const markdown = [
9049
+ `# ${topic}`,
9050
+ ``,
9051
+ `**Date:** ${date}`,
9052
+ `**Provider:** ${config.ai.provider} / ${flags.model ?? config.ai.model}`,
9053
+ ``,
9054
+ `---`,
9055
+ ``,
9056
+ finalAnalysis,
9057
+ ``,
9058
+ ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
9059
+ ].join(`
9060
+ `);
9061
+ writeFileSync9(join16(dir, `${id}.md`), markdown, "utf-8");
8731
9062
  process.stderr.write(`
8732
9063
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
8733
9064
  `);
8734
9065
  process.stderr.write(` View with: ${bold(`locus discuss show ${id.slice(0, 8)}`)}
9066
+ `);
9067
+ process.stderr.write(` Plan with: ${bold(`locus discuss plan ${id.slice(0, 8)}`)}
8735
9068
 
8736
9069
  `);
8737
9070
  }
8738
- function buildDiscussionPrompt(projectRoot, config, topic) {
9071
+ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
8739
9072
  const parts = [];
8740
- parts.push(`You are a senior software architect helping make decisions for the ${config.github.owner}/${config.github.repo} project.`);
9073
+ parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
8741
9074
  parts.push("");
8742
9075
  const locusPath = join16(projectRoot, "LOCUS.md");
8743
9076
  if (existsSync15(locusPath)) {
@@ -8753,26 +9086,48 @@ function buildDiscussionPrompt(projectRoot, config, topic) {
8753
9086
  parts.push(content.slice(0, 2000));
8754
9087
  parts.push("");
8755
9088
  }
8756
- parts.push(`TOPIC: ${topic}`);
8757
- parts.push("");
8758
- parts.push("Please provide a thorough analysis covering:");
8759
- parts.push("1. **Context**: Restate the problem/question and why it matters");
8760
- parts.push("2. **Options**: List all viable approaches with pros/cons");
8761
- parts.push("3. **Recommendation**: Your recommended approach with reasoning");
8762
- parts.push("4. **Trade-offs**: What we gain and what we sacrifice");
8763
- parts.push("5. **Implementation Notes**: Key technical considerations");
8764
- parts.push("6. **Decision**: A clear, actionable conclusion");
9089
+ parts.push(`DISCUSSION TOPIC: ${topic}`);
8765
9090
  parts.push("");
8766
- parts.push("Be specific to this project's codebase and constraints.");
8767
- parts.push("Reference specific files or patterns where relevant.");
9091
+ if (conversation.length === 0) {
9092
+ parts.push("Before providing recommendations, you need to ask targeted clarifying questions.");
9093
+ parts.push("");
9094
+ parts.push("Ask 3-5 focused questions that will significantly improve the quality of your analysis.");
9095
+ parts.push("Format as a numbered list. Be specific and focused on the most important unknowns.");
9096
+ parts.push("Do NOT provide any analysis yet — questions only.");
9097
+ } else {
9098
+ parts.push("CONVERSATION SO FAR:");
9099
+ parts.push("");
9100
+ for (const turn of conversation) {
9101
+ if (turn.role === "user") {
9102
+ parts.push(`USER: ${turn.content}`);
9103
+ } else {
9104
+ parts.push(`ASSISTANT: ${turn.content}`);
9105
+ }
9106
+ parts.push("");
9107
+ }
9108
+ if (forceFinal) {
9109
+ parts.push("Based on everything discussed, provide your complete analysis and recommendations now.");
9110
+ parts.push("Format as a thorough markdown document with a clear title (# Heading), sections, trade-offs, and actionable recommendations.");
9111
+ } else {
9112
+ parts.push("Review the information gathered so far.");
9113
+ parts.push("");
9114
+ parts.push("If you have enough information to make a thorough recommendation:");
9115
+ parts.push(" → Provide a complete analysis as a markdown document with a title (# Heading), sections, trade-offs, and concrete recommendations.");
9116
+ parts.push("");
9117
+ parts.push("If you still need key information to give a good answer:");
9118
+ parts.push(" → Ask 2-3 more focused follow-up questions (numbered list only, no analysis yet).");
9119
+ }
9120
+ }
8768
9121
  return parts.join(`
8769
9122
  `);
8770
9123
  }
9124
+ var MAX_DISCUSSION_ROUNDS = 5;
8771
9125
  var init_discuss = __esm(() => {
8772
9126
  init_run_ai();
8773
9127
  init_config();
8774
9128
  init_progress();
8775
9129
  init_terminal();
9130
+ init_plan();
8776
9131
  });
8777
9132
 
8778
9133
  // src/commands/artifacts.ts
@@ -8784,7 +9139,7 @@ __export(exports_artifacts, {
8784
9139
  formatDate: () => formatDate2,
8785
9140
  artifactsCommand: () => artifactsCommand
8786
9141
  });
8787
- import { existsSync as existsSync16, readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
9142
+ import { existsSync as existsSync16, readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
8788
9143
  import { join as join17 } from "node:path";
8789
9144
  function printHelp5() {
8790
9145
  process.stderr.write(`
@@ -8811,7 +9166,7 @@ function listArtifacts(projectRoot) {
8811
9166
  const dir = getArtifactsDir(projectRoot);
8812
9167
  if (!existsSync16(dir))
8813
9168
  return [];
8814
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
9169
+ return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
8815
9170
  const filePath = join17(dir, fileName);
8816
9171
  const stat = statSync4(filePath);
8817
9172
  return {
@@ -9123,7 +9478,8 @@ ${bold("Examples:")}
9123
9478
  locus init ${dim("# Set up Locus in this repo")}
9124
9479
  locus exec ${dim("# Start interactive REPL")}
9125
9480
  locus issue create "Fix login bug" ${dim("# Create a new issue")}
9126
- locus plan "Build auth system" ${dim("# AI plans issues + sprint")}
9481
+ locus plan "Build auth system" ${dim("# AI creates a plan file")}
9482
+ locus plan approve <id> ${dim("# Create issues from saved plan")}
9127
9483
  locus run ${dim("# Execute active sprint")}
9128
9484
  locus run 42 43 ${dim("# Run issues in parallel")}
9129
9485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {