@locusai/cli 0.17.6 → 0.17.8

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 +443 -116
  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,14 +8753,13 @@ __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
- import { createInterface as createInterface2 } from "node:readline";
8541
8763
  function printHelp4() {
8542
8764
  process.stderr.write(`
8543
8765
  ${bold("locus discuss")} — AI-powered architectural discussions
@@ -8546,6 +8768,7 @@ ${bold("Usage:")}
8546
8768
  locus discuss "<topic>" ${dim("# Start a new discussion")}
8547
8769
  locus discuss list ${dim("# List all discussions")}
8548
8770
  locus discuss show <id> ${dim("# Show a discussion")}
8771
+ locus discuss plan <id> ${dim("# Convert discussion to a plan")}
8549
8772
  locus discuss delete <id> ${dim("# Delete a discussion")}
8550
8773
 
8551
8774
  ${bold("Examples:")}
@@ -8553,6 +8776,7 @@ ${bold("Examples:")}
8553
8776
  locus discuss "Monorepo vs polyrepo for our microservices"
8554
8777
  locus discuss list
8555
8778
  locus discuss show abc123
8779
+ locus discuss plan abc123
8556
8780
 
8557
8781
  `);
8558
8782
  }
@@ -8562,11 +8786,11 @@ function getDiscussionsDir(projectRoot) {
8562
8786
  function ensureDiscussionsDir(projectRoot) {
8563
8787
  const dir = getDiscussionsDir(projectRoot);
8564
8788
  if (!existsSync15(dir)) {
8565
- mkdirSync10(dir, { recursive: true });
8789
+ mkdirSync11(dir, { recursive: true });
8566
8790
  }
8567
8791
  return dir;
8568
8792
  }
8569
- function generateId() {
8793
+ function generateId2() {
8570
8794
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8571
8795
  }
8572
8796
  async function discussCommand(projectRoot, args, flags = {}) {
@@ -8581,16 +8805,15 @@ async function discussCommand(projectRoot, args, flags = {}) {
8581
8805
  if (subcommand === "show") {
8582
8806
  return showDiscussion(projectRoot, args[1]);
8583
8807
  }
8808
+ if (subcommand === "plan") {
8809
+ return convertDiscussionToPlan(projectRoot, args[1]);
8810
+ }
8584
8811
  if (subcommand === "delete") {
8585
8812
  return deleteDiscussion(projectRoot, args[1]);
8586
8813
  }
8587
8814
  if (args.length === 0) {
8588
- const topic2 = await promptForTopic();
8589
- if (!topic2) {
8590
- printHelp4();
8591
- return;
8592
- }
8593
- return startDiscussion(projectRoot, topic2, flags);
8815
+ printHelp4();
8816
+ return;
8594
8817
  }
8595
8818
  const topic = args.join(" ").trim();
8596
8819
  return startDiscussion(projectRoot, topic, flags);
@@ -8602,7 +8825,7 @@ function listDiscussions(projectRoot) {
8602
8825
  `);
8603
8826
  return;
8604
8827
  }
8605
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8828
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md")).sort().reverse();
8606
8829
  if (files.length === 0) {
8607
8830
  process.stderr.write(`${dim("No discussions yet.")}
8608
8831
  `);
@@ -8637,7 +8860,7 @@ function showDiscussion(projectRoot, id) {
8637
8860
  `);
8638
8861
  return;
8639
8862
  }
8640
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8863
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8641
8864
  const match = files.find((f) => f.startsWith(id));
8642
8865
  if (!match) {
8643
8866
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8660,7 +8883,7 @@ function deleteDiscussion(projectRoot, id) {
8660
8883
  `);
8661
8884
  return;
8662
8885
  }
8663
- const files = readdirSync7(dir).filter((f) => f.endsWith(".md"));
8886
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8664
8887
  const match = files.find((f) => f.startsWith(id));
8665
8888
  if (!match) {
8666
8889
  process.stderr.write(`${red("✗")} Discussion "${id}" not found.
@@ -8671,73 +8894,153 @@ function deleteDiscussion(projectRoot, id) {
8671
8894
  process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
8672
8895
  `);
8673
8896
  }
8674
- async function promptForTopic() {
8675
- return new Promise((resolve2) => {
8676
- const rl = createInterface2({
8677
- input: process.stdin,
8678
- output: process.stderr,
8679
- terminal: true
8680
- });
8681
- process.stderr.write(`${bold("Discussion topic:")} `);
8682
- rl.once("line", (line) => {
8683
- rl.close();
8684
- resolve2(line.trim());
8685
- });
8686
- rl.once("close", () => resolve2(""));
8897
+ async function convertDiscussionToPlan(projectRoot, id) {
8898
+ if (!id) {
8899
+ process.stderr.write(`${red("✗")} Please provide a discussion ID.
8900
+ `);
8901
+ process.stderr.write(` Usage: ${bold("locus discuss plan <id>")}
8902
+ `);
8903
+ return;
8904
+ }
8905
+ const dir = getDiscussionsDir(projectRoot);
8906
+ if (!existsSync15(dir)) {
8907
+ process.stderr.write(`${red("✗")} No discussions found.
8908
+ `);
8909
+ return;
8910
+ }
8911
+ const files = readdirSync8(dir).filter((f) => f.endsWith(".md"));
8912
+ const match = files.find((f) => f.startsWith(id));
8913
+ if (!match) {
8914
+ process.stderr.write(`${red("✗")} Discussion "${id}" not found.
8915
+ `);
8916
+ return;
8917
+ }
8918
+ const content = readFileSync12(join16(dir, match), "utf-8");
8919
+ process.stderr.write(`
8920
+ ${bold("Converting discussion to plan:")} ${cyan(id)}
8921
+
8922
+ `);
8923
+ await planCommand(projectRoot, [
8924
+ `Create a detailed, actionable implementation plan based on this discussion document:
8925
+
8926
+ ${content.slice(0, 8000)}`
8927
+ ], {});
8928
+ }
8929
+ async function promptForAnswers() {
8930
+ const input = new InputHandler({
8931
+ prompt: `${cyan("you")} ${dim(">")} `
8687
8932
  });
8933
+ const result = await input.readline();
8934
+ if (result.type === "submit") {
8935
+ return result.text.trim();
8936
+ }
8937
+ return "";
8938
+ }
8939
+ function isQuestionsResponse(output) {
8940
+ const trimmed = output.trimStart();
8941
+ if (trimmed.startsWith("#"))
8942
+ return false;
8943
+ const questionMarks = (trimmed.match(/\?/g) ?? []).length;
8944
+ return questionMarks >= 2;
8688
8945
  }
8689
8946
  async function startDiscussion(projectRoot, topic, flags) {
8690
8947
  const config = loadConfig(projectRoot);
8691
8948
  const timer = createTimer();
8692
- const id = generateId();
8949
+ const id = generateId2();
8693
8950
  process.stderr.write(`
8694
8951
  ${bold("Discussion:")} ${cyan(topic)}
8695
8952
 
8696
8953
  `);
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(`
8954
+ const conversation = [];
8955
+ let finalAnalysis = "";
8956
+ for (let round = 0;round < MAX_DISCUSSION_ROUNDS; round++) {
8957
+ const isFinalRound = round === MAX_DISCUSSION_ROUNDS - 1;
8958
+ const prompt = buildDiscussionPrompt(projectRoot, config, topic, conversation, isFinalRound);
8959
+ const aiResult = await runAI({
8960
+ prompt,
8961
+ provider: config.ai.provider,
8962
+ model: flags.model ?? config.ai.model,
8963
+ cwd: projectRoot,
8964
+ activity: "discussion"
8965
+ });
8966
+ if (aiResult.interrupted) {
8967
+ process.stderr.write(`
8707
8968
  ${yellow("⚡")} Discussion interrupted.
8708
8969
  `);
8709
- if (!aiResult.output.trim())
8970
+ if (!aiResult.output.trim())
8971
+ return;
8972
+ finalAnalysis = aiResult.output.trim();
8973
+ break;
8974
+ }
8975
+ if (!aiResult.success && !aiResult.interrupted) {
8976
+ process.stderr.write(`
8977
+ ${red("✗")} Discussion failed: ${aiResult.error}
8978
+ `);
8710
8979
  return;
8711
- }
8712
- if (!aiResult.success && !aiResult.interrupted) {
8980
+ }
8981
+ const response = aiResult.output.trim();
8982
+ conversation.push({ role: "assistant", content: response });
8983
+ if (!isQuestionsResponse(response) || isFinalRound) {
8984
+ finalAnalysis = response;
8985
+ break;
8986
+ }
8987
+ process.stderr.write(`
8988
+ ${dim("─".repeat(50))}
8989
+ ${bold("Your answers:")} ${dim("(Shift+Enter for newlines, Enter to submit)")}
8990
+
8991
+ `);
8992
+ const answers = await promptForAnswers();
8993
+ if (!answers.trim()) {
8994
+ conversation.push({
8995
+ role: "user",
8996
+ content: "Please proceed with your analysis based on the information available."
8997
+ });
8998
+ } else {
8999
+ conversation.push({ role: "user", content: answers });
9000
+ }
8713
9001
  process.stderr.write(`
8714
- ${red("✗")} Discussion failed: ${aiResult.error}
8715
9002
  `);
8716
- return;
8717
9003
  }
8718
- const output = aiResult.output;
9004
+ if (!finalAnalysis)
9005
+ return;
8719
9006
  const dir = ensureDiscussionsDir(projectRoot);
8720
9007
  const date = new Date().toISOString().slice(0, 10);
8721
- const markdown = `# ${topic}
9008
+ const transcript = conversation.map((turn) => {
9009
+ const label = turn.role === "user" ? "You" : "AI";
9010
+ return `**${label}:**
8722
9011
 
8723
- **Date:** ${date}
8724
- **Provider:** ${config.ai.provider} / ${flags.model ?? config.ai.model}
9012
+ ${turn.content}`;
9013
+ }).join(`
8725
9014
 
8726
9015
  ---
8727
9016
 
8728
- ${output}
8729
- `;
8730
- writeFileSync8(join16(dir, `${id}.md`), markdown, "utf-8");
9017
+ `);
9018
+ const markdown = [
9019
+ `# ${topic}`,
9020
+ ``,
9021
+ `**Date:** ${date}`,
9022
+ `**Provider:** ${config.ai.provider} / ${flags.model ?? config.ai.model}`,
9023
+ ``,
9024
+ `---`,
9025
+ ``,
9026
+ finalAnalysis,
9027
+ ``,
9028
+ ...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
9029
+ ].join(`
9030
+ `);
9031
+ writeFileSync9(join16(dir, `${id}.md`), markdown, "utf-8");
8731
9032
  process.stderr.write(`
8732
9033
  ${green("✓")} Discussion saved: ${cyan(id)} ${dim(`(${timer.formatted()})`)}
8733
9034
  `);
8734
9035
  process.stderr.write(` View with: ${bold(`locus discuss show ${id.slice(0, 8)}`)}
9036
+ `);
9037
+ process.stderr.write(` Plan with: ${bold(`locus discuss plan ${id.slice(0, 8)}`)}
8735
9038
 
8736
9039
  `);
8737
9040
  }
8738
- function buildDiscussionPrompt(projectRoot, config, topic) {
9041
+ function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
8739
9042
  const parts = [];
8740
- parts.push(`You are a senior software architect helping make decisions for the ${config.github.owner}/${config.github.repo} project.`);
9043
+ parts.push(`You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.`);
8741
9044
  parts.push("");
8742
9045
  const locusPath = join16(projectRoot, "LOCUS.md");
8743
9046
  if (existsSync15(locusPath)) {
@@ -8753,26 +9056,49 @@ function buildDiscussionPrompt(projectRoot, config, topic) {
8753
9056
  parts.push(content.slice(0, 2000));
8754
9057
  parts.push("");
8755
9058
  }
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");
9059
+ parts.push(`DISCUSSION TOPIC: ${topic}`);
8765
9060
  parts.push("");
8766
- parts.push("Be specific to this project's codebase and constraints.");
8767
- parts.push("Reference specific files or patterns where relevant.");
9061
+ if (conversation.length === 0) {
9062
+ parts.push("Before providing recommendations, you need to ask targeted clarifying questions.");
9063
+ parts.push("");
9064
+ parts.push("Ask 3-5 focused questions that will significantly improve the quality of your analysis.");
9065
+ parts.push("Format as a numbered list. Be specific and focused on the most important unknowns.");
9066
+ parts.push("Do NOT provide any analysis yet — questions only.");
9067
+ } else {
9068
+ parts.push("CONVERSATION SO FAR:");
9069
+ parts.push("");
9070
+ for (const turn of conversation) {
9071
+ if (turn.role === "user") {
9072
+ parts.push(`USER: ${turn.content}`);
9073
+ } else {
9074
+ parts.push(`ASSISTANT: ${turn.content}`);
9075
+ }
9076
+ parts.push("");
9077
+ }
9078
+ if (forceFinal) {
9079
+ parts.push("Based on everything discussed, provide your complete analysis and recommendations now.");
9080
+ parts.push("Format as a thorough markdown document with a clear title (# Heading), sections, trade-offs, and actionable recommendations.");
9081
+ } else {
9082
+ parts.push("Review the information gathered so far.");
9083
+ parts.push("");
9084
+ parts.push("If you have enough information to make a thorough recommendation:");
9085
+ parts.push(" → Provide a complete analysis as a markdown document with a title (# Heading), sections, trade-offs, and concrete recommendations.");
9086
+ parts.push("");
9087
+ parts.push("If you still need key information to give a good answer:");
9088
+ parts.push(" → Ask 2-3 more focused follow-up questions (numbered list only, no analysis yet).");
9089
+ }
9090
+ }
8768
9091
  return parts.join(`
8769
9092
  `);
8770
9093
  }
9094
+ var MAX_DISCUSSION_ROUNDS = 5;
8771
9095
  var init_discuss = __esm(() => {
8772
9096
  init_run_ai();
8773
9097
  init_config();
8774
9098
  init_progress();
8775
9099
  init_terminal();
9100
+ init_input_handler();
9101
+ init_plan();
8776
9102
  });
8777
9103
 
8778
9104
  // src/commands/artifacts.ts
@@ -8784,7 +9110,7 @@ __export(exports_artifacts, {
8784
9110
  formatDate: () => formatDate2,
8785
9111
  artifactsCommand: () => artifactsCommand
8786
9112
  });
8787
- import { existsSync as existsSync16, readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
9113
+ import { existsSync as existsSync16, readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
8788
9114
  import { join as join17 } from "node:path";
8789
9115
  function printHelp5() {
8790
9116
  process.stderr.write(`
@@ -8811,7 +9137,7 @@ function listArtifacts(projectRoot) {
8811
9137
  const dir = getArtifactsDir(projectRoot);
8812
9138
  if (!existsSync16(dir))
8813
9139
  return [];
8814
- return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
9140
+ return readdirSync9(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
8815
9141
  const filePath = join17(dir, fileName);
8816
9142
  const stat = statSync4(filePath);
8817
9143
  return {
@@ -9123,7 +9449,8 @@ ${bold("Examples:")}
9123
9449
  locus init ${dim("# Set up Locus in this repo")}
9124
9450
  locus exec ${dim("# Start interactive REPL")}
9125
9451
  locus issue create "Fix login bug" ${dim("# Create a new issue")}
9126
- locus plan "Build auth system" ${dim("# AI plans issues + sprint")}
9452
+ locus plan "Build auth system" ${dim("# AI creates a plan file")}
9453
+ locus plan approve <id> ${dim("# Create issues from saved plan")}
9127
9454
  locus run ${dim("# Execute active sprint")}
9128
9455
  locus run 42 43 ${dim("# Run issues in parallel")}
9129
9456
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.17.6",
3
+ "version": "0.17.8",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {