@locusai/cli 0.17.5 → 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 +458 -97
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -1271,7 +1271,12 @@ function updateIssueLabels(number, addLabels, removeLabels, options = {}) {
1271
1271
  gh(args, options);
1272
1272
  }
1273
1273
  function addIssueComment(number, body, options = {}) {
1274
- gh(`issue comment ${number} --body ${JSON.stringify(body)}`, options);
1274
+ const cwd = options.cwd ?? process.cwd();
1275
+ execFileSync("gh", ["issue", "comment", String(number), "--body", body], {
1276
+ cwd,
1277
+ encoding: "utf-8",
1278
+ stdio: ["pipe", "pipe", "pipe"]
1279
+ });
1275
1280
  }
1276
1281
  function createMilestone(owner, repo, title, dueOn, description, options = {}) {
1277
1282
  let args = `api repos/${owner}/${repo}/milestones -f title=${JSON.stringify(title)}`;
@@ -2800,11 +2805,17 @@ ${dim("Press Ctrl+C again to exit")}\r
2800
2805
  render();
2801
2806
  return;
2802
2807
  case SEQ_WORD_LEFT:
2808
+ case SEQ_SHIFT_LEFT:
2809
+ case SEQ_META_LEFT:
2810
+ case SEQ_META_SHIFT_LEFT:
2803
2811
  case "\x1Bb":
2804
2812
  moveWordLeft();
2805
2813
  render();
2806
2814
  return;
2807
2815
  case SEQ_WORD_RIGHT:
2816
+ case SEQ_SHIFT_RIGHT:
2817
+ case SEQ_META_RIGHT:
2818
+ case SEQ_META_SHIFT_RIGHT:
2808
2819
  case "\x1Bf":
2809
2820
  moveWordRight();
2810
2821
  render();
@@ -2828,7 +2839,7 @@ ${dim("Press Ctrl+C again to exit")}\r
2828
2839
  render();
2829
2840
  return;
2830
2841
  default:
2831
- if (seq.charCodeAt(0) >= 32 || seq.length > 1) {
2842
+ if (seq.charCodeAt(0) >= 32) {
2832
2843
  insertText(seq);
2833
2844
  render();
2834
2845
  }
@@ -3131,7 +3142,7 @@ ${dim("Press")} ${yellow("ESC")} ${dim("again to force exit")}\r
3131
3142
  }
3132
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 = `
3133
3144
  `, CTRL_U = "\x15", CTRL_W = "\x17", TAB = "\t", ENTER = "\r", ENTER_CRLF = `\r
3134
- `, 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;
3135
3146
  var init_input_handler = __esm(() => {
3136
3147
  init_terminal();
3137
3148
  init_image_detect();
@@ -3150,6 +3161,12 @@ var init_input_handler = __esm(() => {
3150
3161
  SEQ_DELETE = `${CSI}3~`;
3151
3162
  SEQ_WORD_LEFT = `${CSI}1;5D`;
3152
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`;
3153
3170
  SEQ_SHIFT_ENTER_CSI_U = `${CSI}13;2u`;
3154
3171
  SEQ_SHIFT_ENTER_MODIFY = `${CSI}27;2;13~`;
3155
3172
  SEQ_SHIFT_ENTER_TILDE = `${CSI}13;2~`;
@@ -3163,6 +3180,12 @@ var init_input_handler = __esm(() => {
3163
3180
  SEQ_ALT_ENTER,
3164
3181
  SEQ_WORD_LEFT,
3165
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,
3166
3189
  SEQ_DELETE,
3167
3190
  SEQ_HOME_1,
3168
3191
  SEQ_END_4,
@@ -3449,6 +3472,10 @@ var init_runner = __esm(() => {
3449
3472
  });
3450
3473
 
3451
3474
  // src/ai/run-ai.ts
3475
+ var exports_run_ai = {};
3476
+ __export(exports_run_ai, {
3477
+ runAI: () => runAI
3478
+ });
3452
3479
  async function runAI(options) {
3453
3480
  const indicator = getStatusIndicator();
3454
3481
  const renderer = options.silent ? null : new StreamRenderer;
@@ -7668,14 +7695,23 @@ __export(exports_plan, {
7668
7695
  parsePlanOutput: () => parsePlanOutput,
7669
7696
  parsePlanArgs: () => parsePlanArgs
7670
7697
  });
7671
- 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";
7672
7705
  import { join as join14 } from "node:path";
7673
7706
  function printHelp() {
7674
7707
  process.stderr.write(`
7675
7708
  ${bold("locus plan")} — AI-powered sprint planning
7676
7709
 
7677
7710
  ${bold("Usage:")}
7678
- 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")}
7679
7715
  locus plan --from-issues --sprint <name> ${dim("# Organize existing issues")}
7680
7716
 
7681
7717
  ${bold("Options:")}
@@ -7686,6 +7722,8 @@ ${bold("Options:")}
7686
7722
  ${bold("Examples:")}
7687
7723
  locus plan "Build user authentication with OAuth"
7688
7724
  locus plan "Improve API performance" --sprint "Sprint 3"
7725
+ locus plan approve abc123
7726
+ locus plan list
7689
7727
  locus plan --from-issues --sprint "Sprint 2"
7690
7728
 
7691
7729
  `);
@@ -7693,11 +7731,48 @@ ${bold("Examples:")}
7693
7731
  function normalizeSprintName(name) {
7694
7732
  return name.trim().toLowerCase();
7695
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
+ }
7696
7762
  async function planCommand(projectRoot, args, flags = {}) {
7697
7763
  if (args[0] === "help" || args.length === 0) {
7698
7764
  printHelp();
7699
7765
  return;
7700
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
+ }
7701
7776
  const parsedArgs = parsePlanArgs(args);
7702
7777
  if (parsedArgs.error) {
7703
7778
  process.stderr.write(`${red("✗")} ${parsedArgs.error}
@@ -7721,7 +7796,138 @@ async function planCommand(projectRoot, args, flags = {}) {
7721
7796
  }
7722
7797
  return handleAIPlan(projectRoot, config, directive, sprintName, flags);
7723
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
+ }
7724
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`;
7725
7931
  process.stderr.write(`
7726
7932
  ${bold("Planning:")} ${cyan(directive)}
7727
7933
  `);
@@ -7731,55 +7937,65 @@ ${bold("Planning:")} ${cyan(directive)}
7731
7937
  }
7732
7938
  process.stderr.write(`
7733
7939
  `);
7734
- const context = buildPlanningContext(projectRoot, config, directive);
7735
- const aiResult = await runAI({
7736
- prompt: context,
7737
- provider: config.ai.provider,
7738
- model: flags.model ?? config.ai.model,
7739
- cwd: projectRoot,
7740
- activity: "sprint planning"
7741
- });
7742
- 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)) {
7743
7944
  process.stderr.write(`
7744
- ${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")}.
7745
7948
  `);
7746
7949
  return;
7747
7950
  }
7748
- if (!aiResult.success) {
7951
+ let plan;
7952
+ try {
7953
+ const content = readFileSync10(planPath, "utf-8");
7954
+ plan = JSON.parse(content);
7955
+ } catch {
7749
7956
  process.stderr.write(`
7750
- ${red("✗")} Planning failed: ${aiResult.error ?? "Unknown error"}
7957
+ ${red("✗")} Plan file at ${bold(planPathRelative)} is not valid JSON.
7751
7958
  `);
7752
7959
  return;
7753
7960
  }
7754
- const output = sanitizePlanOutput(aiResult.output);
7755
- const planned = parsePlanOutput(output);
7756
- if (planned.length === 0) {
7961
+ if (!Array.isArray(plan.issues) || plan.issues.length === 0) {
7757
7962
  process.stderr.write(`
7758
- ${yellow("⚠")} Could not extract structured issues from plan.
7759
- `);
7760
- process.stderr.write(` The AI output is shown above. Create issues manually with ${bold("locus issue create")}.
7963
+ ${yellow("⚠")} Plan file has no issues.
7761
7964
  `);
7762
7965
  return;
7763
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");
7764
7976
  process.stderr.write(`
7765
- ${bold("Planned Issues:")}
7977
+ ${bold("Plan saved:")} ${cyan(id)}
7766
7978
 
7767
7979
  `);
7768
- 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")}
7769
7981
  `);
7770
- for (const issue of planned) {
7771
- 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"}
7772
7984
  `);
7773
7985
  }
7774
7986
  process.stderr.write(`
7775
7987
  `);
7776
7988
  if (flags.dryRun) {
7777
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)}`)}
7778
7992
 
7779
7993
  `);
7780
7994
  return;
7781
7995
  }
7782
- await createPlannedIssues(projectRoot, config, planned, sprintName);
7996
+ process.stderr.write(` To create these issues: ${bold(`locus plan approve ${id.slice(0, 8)}`)}
7997
+
7998
+ `);
7783
7999
  }
7784
8000
  async function handleFromIssues(projectRoot, config, sprintName, flags) {
7785
8001
  if (!sprintName) {
@@ -7801,6 +8017,7 @@ ${bold("Organizing issues for:")} ${cyan(sprintName)}
7801
8017
  ${i.body?.slice(0, 300) ?? ""}`).join(`
7802
8018
 
7803
8019
  `);
8020
+ const { runAI: runAI2 } = await Promise.resolve().then(() => (init_run_ai(), exports_run_ai));
7804
8021
  const prompt = `You are organizing GitHub issues for a sprint. Analyze these issues and suggest the optimal execution order.
7805
8022
 
7806
8023
  Issues:
@@ -7811,7 +8028,7 @@ ORDER: #<number> <reason for this position>
7811
8028
 
7812
8029
  Order them so that dependencies are respected (issues that produce code needed by later issues should come first).
7813
8030
  Start with foundational/setup tasks, then core features, then integration/testing.`;
7814
- const aiResult = await runAI({
8031
+ const aiResult = await runAI2({
7815
8032
  prompt,
7816
8033
  provider: config.ai.provider,
7817
8034
  model: flags.model ?? config.ai.model,
@@ -7879,11 +8096,14 @@ ${bold("Suggested Order:")}
7879
8096
  `);
7880
8097
  }
7881
8098
  }
7882
- function buildPlanningContext(projectRoot, config, directive) {
8099
+ function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
7883
8100
  const parts = [];
7884
8101
  parts.push(`You are a sprint planning assistant for the GitHub repository ${config.github.owner}/${config.github.repo}.`);
7885
8102
  parts.push("");
7886
8103
  parts.push(`DIRECTIVE: ${directive}`);
8104
+ if (sprintName) {
8105
+ parts.push(`SPRINT: ${sprintName}`);
8106
+ }
7887
8107
  parts.push("");
7888
8108
  const locusPath = join14(projectRoot, "LOCUS.md");
7889
8109
  if (existsSync13(locusPath)) {
@@ -7899,28 +8119,37 @@ function buildPlanningContext(projectRoot, config, directive) {
7899
8119
  parts.push(content.slice(0, 2000));
7900
8120
  parts.push("");
7901
8121
  }
7902
- parts.push("INSTRUCTIONS:");
7903
- parts.push("Break down the directive into specific, actionable GitHub issues.");
7904
- parts.push("Each issue should be independently executable by an AI agent.");
7905
- 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}`);
7906
8124
  parts.push("");
7907
- 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:`);
7908
8126
  parts.push("");
7909
- parts.push("---ISSUE---");
7910
- parts.push("ORDER: <number>");
7911
- parts.push("TITLE: <concise issue title>");
7912
- parts.push("PRIORITY: <critical|high|medium|low>");
7913
- parts.push("TYPE: <feature|bug|chore|refactor|docs>");
7914
- parts.push("DEPENDS_ON: <comma-separated previous order numbers, or 'none'>");
7915
- parts.push("BODY:");
7916
- parts.push("<detailed issue description with acceptance criteria>");
7917
- 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("```");
7918
8145
  parts.push("");
7919
- parts.push("Be specific in issue bodies. Include:");
7920
- parts.push("- What code/files should be created or modified");
7921
- parts.push("- Acceptance criteria (testable conditions)");
7922
- parts.push("- What previous tasks produce that this task needs");
7923
- 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");
7924
8153
  return parts.join(`
7925
8154
  `);
7926
8155
  }
@@ -8050,7 +8279,6 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
8050
8279
  `);
8051
8280
  }
8052
8281
  var init_plan = __esm(() => {
8053
- init_run_ai();
8054
8282
  init_config();
8055
8283
  init_github();
8056
8284
  init_terminal();
@@ -8525,11 +8753,11 @@ __export(exports_discuss, {
8525
8753
  });
8526
8754
  import {
8527
8755
  existsSync as existsSync15,
8528
- mkdirSync as mkdirSync10,
8529
- readdirSync as readdirSync7,
8756
+ mkdirSync as mkdirSync11,
8757
+ readdirSync as readdirSync8,
8530
8758
  readFileSync as readFileSync12,
8531
8759
  unlinkSync as unlinkSync5,
8532
- writeFileSync as writeFileSync8
8760
+ writeFileSync as writeFileSync9
8533
8761
  } from "node:fs";
8534
8762
  import { join as join16 } from "node:path";
8535
8763
  import { createInterface as createInterface2 } from "node:readline";
@@ -8541,6 +8769,7 @@ ${bold("Usage:")}
8541
8769
  locus discuss "<topic>" ${dim("# Start a new discussion")}
8542
8770
  locus discuss list ${dim("# List all discussions")}
8543
8771
  locus discuss show <id> ${dim("# Show a discussion")}
8772
+ locus discuss plan <id> ${dim("# Convert discussion to a plan")}
8544
8773
  locus discuss delete <id> ${dim("# Delete a discussion")}
8545
8774
 
8546
8775
  ${bold("Examples:")}
@@ -8548,6 +8777,7 @@ ${bold("Examples:")}
8548
8777
  locus discuss "Monorepo vs polyrepo for our microservices"
8549
8778
  locus discuss list
8550
8779
  locus discuss show abc123
8780
+ locus discuss plan abc123
8551
8781
 
8552
8782
  `);
8553
8783
  }
@@ -8557,11 +8787,11 @@ function getDiscussionsDir(projectRoot) {
8557
8787
  function ensureDiscussionsDir(projectRoot) {
8558
8788
  const dir = getDiscussionsDir(projectRoot);
8559
8789
  if (!existsSync15(dir)) {
8560
- mkdirSync10(dir, { recursive: true });
8790
+ mkdirSync11(dir, { recursive: true });
8561
8791
  }
8562
8792
  return dir;
8563
8793
  }
8564
- function generateId() {
8794
+ function generateId2() {
8565
8795
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8566
8796
  }
8567
8797
  async function discussCommand(projectRoot, args, flags = {}) {
@@ -8576,6 +8806,9 @@ async function discussCommand(projectRoot, args, flags = {}) {
8576
8806
  if (subcommand === "show") {
8577
8807
  return showDiscussion(projectRoot, args[1]);
8578
8808
  }
8809
+ if (subcommand === "plan") {
8810
+ return convertDiscussionToPlan(projectRoot, args[1]);
8811
+ }
8579
8812
  if (subcommand === "delete") {
8580
8813
  return deleteDiscussion(projectRoot, args[1]);
8581
8814
  }
@@ -8597,7 +8830,7 @@ function listDiscussions(projectRoot) {
8597
8830
  `);
8598
8831
  return;
8599
8832
  }
8600
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8833
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8601
8834
  if (files.length === 0) {
8602
8835
  process.stderr.write(`${dim("No discussions yet.")}
8603
8836
  `);
@@ -8632,7 +8865,7 @@ function showDiscussion(projectRoot, id) {
8632
8865
  `);
8633
8866
  return;
8634
8867
  }
8635
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8868
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8636
8869
  const match = files.find((f) => f.startsWith(id));
8637
8870
  if (!match) {
8638
8871
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8655,7 +8888,7 @@ function deleteDiscussion(projectRoot, id) {
8655
8888
  `);
8656
8889
  return;
8657
8890
  }
8658
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8891
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8659
8892
  const match = files.find((f) => f.startsWith(id));
8660
8893
  if (!match) {
8661
8894
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8666,6 +8899,38 @@ function deleteDiscussion(projectRoot, id) {
8666
8899
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
8667
8900
  `);
8668
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
+ }
8669
8934
  async function promptForTopic() {
8670
8935
  return new Promise((resolve2) => {
8671
8936
  const rl = createInterface2({
@@ -8681,58 +8946,131 @@ async function promptForTopic() {
8681
8946
  rl.once("close", () => resolve2(""));
8682
8947
  });
8683
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
+ }
8684
8977
  async function startDiscussion(projectRoot, topic, flags) {
8685
8978
  const config = loadConfig(projectRoot);
8686
8979
  const timer = createTimer();
8687
- const id = generateId();
8980
+ const id = generateId2();
8688
8981
  process.stderr.write(`
8689
8982
  ${bold("Discussion:")} ${cyan(topic)}
8690
8983
 
8691
8984
  `);
8692
- const prompt = buildDiscussionPrompt(projectRoot, config, topic);
8693
- const aiResult = await runAI({
8694
- prompt,
8695
- provider: config.ai.provider,
8696
- model: flags.model ?? config.ai.model,
8697
- cwd: projectRoot,
8698
- activity: "discussion"
8699
- });
8700
- if (aiResult.interrupted) {
8701
- 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(`
8702
8999
  ${yellow("⚡")} Discussion interrupted.
8703
9000
  `);
8704
- 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
+ `);
8705
9010
  return;
8706
- }
8707
- 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
+ }
8708
9031
  process.stderr.write(`
8709
- ${red("✗")} Discussion failed: ${aiResult.error}
8710
9032
  `);
8711
- return;
8712
9033
  }
8713
- const output = aiResult.output;
9034
+ if (!finalAnalysis)
9035
+ return;
8714
9036
  const dir = ensureDiscussionsDir(projectRoot);
8715
9037
  const date = new Date().toISOString().slice(0, 10);
8716
- const markdown = `# ${topic}
9038
+ const transcript = conversation.map((turn) => {
9039
+ const label = turn.role === "user" ? "You" : "AI";
9040
+ return `**${label}:**
8717
9041
 
8718
- **Date:** ${date}
8719
- **Provider:** ${config.ai.provider} / ${flags.model ?? config.ai.model}
9042
+ ${turn.content}`;
9043
+ }).join(`
8720
9044
 
8721
9045
  ---
8722
9046
 
8723
- ${output}
8724
- `;
8725
- 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");
8726
9062
  process.stderr.write(`
8727
9063
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
8728
9064
  `);
8729
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)}`)}
8730
9068
 
8731
9069
  `);
8732
9070
  }
8733
- function buildDiscussionPrompt(projectRoot, config, topic) {
9071
+ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
8734
9072
  const parts = [];
8735
- 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.`);
8736
9074
  parts.push("");
8737
9075
  const locusPath = join16(projectRoot, "LOCUS.md");
8738
9076
  if (existsSync15(locusPath)) {
@@ -8748,26 +9086,48 @@ function buildDiscussionPrompt(projectRoot, config, topic) {
8748
9086
  parts.push(content.slice(0, 2000));
8749
9087
  parts.push("");
8750
9088
  }
8751
- parts.push(`TOPIC: ${topic}`);
9089
+ parts.push(`DISCUSSION TOPIC: ${topic}`);
8752
9090
  parts.push("");
8753
- parts.push("Please provide a thorough analysis covering:");
8754
- parts.push("1. **Context**: Restate the problem/question and why it matters");
8755
- parts.push("2. **Options**: List all viable approaches with pros/cons");
8756
- parts.push("3. **Recommendation**: Your recommended approach with reasoning");
8757
- parts.push("4. **Trade-offs**: What we gain and what we sacrifice");
8758
- parts.push("5. **Implementation Notes**: Key technical considerations");
8759
- parts.push("6. **Decision**: A clear, actionable conclusion");
8760
- parts.push("");
8761
- parts.push("Be specific to this project's codebase and constraints.");
8762
- 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
+ }
8763
9121
  return parts.join(`
8764
9122
  `);
8765
9123
  }
9124
+ var MAX_DISCUSSION_ROUNDS = 5;
8766
9125
  var init_discuss = __esm(() => {
8767
9126
  init_run_ai();
8768
9127
  init_config();
8769
9128
  init_progress();
8770
9129
  init_terminal();
9130
+ init_plan();
8771
9131
  });
8772
9132
 
8773
9133
  // src/commands/artifacts.ts
@@ -8779,7 +9139,7 @@ __export(exports_artifacts, {
8779
9139
  formatDate: () => formatDate2,
8780
9140
  artifactsCommand: () => artifactsCommand
8781
9141
  });
8782
- 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";
8783
9143
  import { join as join17 } from "node:path";
8784
9144
  function printHelp5() {
8785
9145
  process.stderr.write(`
@@ -8806,7 +9166,7 @@ function listArtifacts(projectRoot) {
8806
9166
  const dir = getArtifactsDir(projectRoot);
8807
9167
  if (!existsSync16(dir))
8808
9168
  return [];
8809
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
9169
+ return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
8810
9170
  const filePath = join17(dir, fileName);
8811
9171
  const stat = statSync4(filePath);
8812
9172
  return {
@@ -9118,7 +9478,8 @@ ${bold("Examples:")}
9118
9478
  locus init ${dim("# Set up Locus in this repo")}
9119
9479
  locus exec ${dim("# Start interactive REPL")}
9120
9480
  locus issue create "Fix login bug" ${dim("# Create a new issue")}
9121
- 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")}
9122
9483
  locus run ${dim("# Execute active sprint")}
9123
9484
  locus run 42 43 ${dim("# Run issues in parallel")}
9124
9485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.17.5",
3
+ "version": "0.17.7",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {