@kaddo/cli 3.13.2 → 3.15.0

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/dist/index.js +328 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -101,6 +101,7 @@ project:
101
101
  name: "${meta.name}"
102
102
  state: ${meta.state}
103
103
  structure: ${meta.structure}
104
+ language: ${meta.language}
104
105
  domains: []
105
106
  team:
106
107
  size: ${meta.teamSize}
@@ -210,11 +211,20 @@ async function runInit() {
210
211
  ],
211
212
  initialValue: "monorepo"
212
213
  });
214
+ const language = await select2({
215
+ message: "Project knowledge language (the CLI stays in English)",
216
+ options: [
217
+ { value: "en", label: "English" },
218
+ { value: "es", label: "Spanish" }
219
+ ],
220
+ initialValue: "en"
221
+ });
213
222
  const meta = {
214
223
  name: projectName.trim(),
215
224
  state,
216
225
  teamSize,
217
- structure
226
+ structure,
227
+ language
218
228
  };
219
229
  const kaddoConfigPath = join(dir, KADDO_DIR, "config.yml");
220
230
  writeFile(kaddoConfigPath, buildConfig(meta));
@@ -510,11 +520,20 @@ import { parse as parseYaml2 } from "yaml";
510
520
  var PROJECT_STATES = ["new", "pre-ai", "legacy"];
511
521
  var TEAM_SIZES = ["indie", "small", "medium", "enterprise"];
512
522
  var REPOSITORY_STRUCTURES = ["monorepo", "multirepo"];
523
+ var PROJECT_LANGUAGES = ["en", "es"];
513
524
  var DEFAULTS = {
514
525
  state: "pre-ai",
515
526
  structure: "monorepo",
516
- teamSize: "indie"
527
+ teamSize: "indie",
528
+ language: "en"
517
529
  };
530
+ function languageLabel(lang) {
531
+ return lang === "es" ? "Spanish" : "English";
532
+ }
533
+ function projectLanguage(config) {
534
+ const lang = config.project.language;
535
+ return lang === "es" ? "es" : "en";
536
+ }
518
537
  var ConfigError = class extends Error {
519
538
  constructor(message) {
520
539
  super(message);
@@ -536,12 +555,18 @@ var structureSchema = z.enum(["monorepo", "multirepo"], {
536
555
  message: `project.structure must be one of: ${REPOSITORY_STRUCTURES.join(", ")}`
537
556
  })
538
557
  });
558
+ var languageSchema = z.enum(["en", "es"], {
559
+ errorMap: () => ({
560
+ message: `project.language must be one of: ${PROJECT_LANGUAGES.join(", ")}`
561
+ })
562
+ });
539
563
  var configSchema = z.object({
540
564
  version: z.union([z.string(), z.number()]).optional(),
541
565
  project: z.object({
542
566
  name: z.string().default("project"),
543
567
  state: projectStateSchema.default(DEFAULTS.state),
544
568
  structure: structureSchema.default(DEFAULTS.structure),
569
+ language: languageSchema.default(DEFAULTS.language),
545
570
  domains: z.array(z.string()).optional()
546
571
  }).passthrough().default({}),
547
572
  team: z.object({
@@ -599,8 +624,8 @@ function createGuidanceForState(state) {
599
624
  }
600
625
  function describeProject(config) {
601
626
  const state = config.project.state;
602
- const stateLabel4 = state === "pre-ai" ? "pre-AI" : state;
603
- return `${stateLabel4} ${config.project.structure}`;
627
+ const stateLabel3 = state === "pre-ai" ? "pre-AI" : state;
628
+ return `${stateLabel3} ${config.project.structure}`;
604
629
  }
605
630
 
606
631
  // src/core/command-help.ts
@@ -1586,6 +1611,19 @@ var RESPONSIBILITY_MATRIX = {
1586
1611
  ],
1587
1612
  next: ["kaddo scan", "kaddo owners suggest", "kaddo guard", "kaddo explain"]
1588
1613
  },
1614
+ "ownership-agent": {
1615
+ agent: "ownership-agent",
1616
+ responsibleFor: ["Precise code: ownership for Work Items and artifacts"],
1617
+ produces: ["proposed code: globs"],
1618
+ canSuggest: ["kaddo owners suggest", "kaddo guard"],
1619
+ cannotSuggest: [
1620
+ "code",
1621
+ "branches",
1622
+ "commits",
1623
+ "modifying files without confirmation"
1624
+ ],
1625
+ next: ["kaddo owners suggest", "kaddo guard"]
1626
+ },
1589
1627
  "guard-agent": {
1590
1628
  agent: "guard-agent",
1591
1629
  responsibleFor: ["Knowledge drift"],
@@ -1654,11 +1692,24 @@ function renderAgentTrace(agent) {
1654
1692
  ""
1655
1693
  ].join("\n");
1656
1694
  }
1695
+ function renderLanguageRule() {
1696
+ return [
1697
+ "## Project Language",
1698
+ "",
1699
+ "The project knowledge language is defined in `.kaddo/config.yml` (`project.language`) and shown",
1700
+ "in the context pack's Project Metadata (`Language:`). Write **all** generated knowledge",
1701
+ "artifacts in that language (default: English).",
1702
+ "",
1703
+ "Do not translate: code, file names, CLI commands or configuration keys.",
1704
+ ""
1705
+ ].join("\n");
1706
+ }
1657
1707
  function withResponsibilityTrace(fileName, content) {
1658
1708
  const agent = fileName.replace(/\.md$/, "");
1659
1709
  if (!RESPONSIBILITY_MATRIX[agent]) return content;
1660
1710
  return `${content.trimEnd()}
1661
1711
 
1712
+ ${renderLanguageRule()}
1662
1713
  ${renderAgentBoundaries(agent)}
1663
1714
  ${renderAgentTrace(agent)}`;
1664
1715
  }
@@ -1846,13 +1897,15 @@ Generated from Kaddo Context Pack.
1846
1897
 
1847
1898
  Save the architecture overview as \`knowledge/tech/current-state.md\`, supporting notes as
1848
1899
  \`knowledge/tech/architecture-notes.md\`, and decision candidates as
1849
- \`knowledge/tech/decision-candidates.md\`.
1900
+ \`knowledge/tech/decision-candidates.md\`. Final ADRs always live under
1901
+ \`knowledge/tech/decisions/\` \u2014 never directly in \`knowledge/tech/\`.
1850
1902
 
1851
1903
  ## Quality Checklist
1852
1904
 
1853
1905
  - Every component is backed by evidence from the context pack.
1854
1906
  - Assumptions and confidence are explicit.
1855
1907
  - No final decisions are asserted \u2014 only candidates.
1908
+ - Final ADRs go to \`knowledge/tech/decisions/\`, not \`knowledge/tech/\`.
1856
1909
  - Open questions are listed.
1857
1910
  `;
1858
1911
  var ROADMAP_AGENT = `# Roadmap Agent
@@ -2171,12 +2224,16 @@ Generated from Kaddo Context Pack.
2171
2224
 
2172
2225
  ## Where to Save the Result
2173
2226
 
2174
- Save the output as \`knowledge/tech/decision-candidates.md\`.
2227
+ Save decision **candidates** as \`knowledge/tech/decision-candidates.md\`. When a candidate becomes
2228
+ a **final ADR**, it must live under \`knowledge/tech/decisions/\` (one file per decision, e.g.
2229
+ \`knowledge/tech/decisions/ADR-0001-<slug>.md\`) \u2014 **never** directly in \`knowledge/tech/\`.
2230
+ (Decision = the concept \xB7 ADR = the format \xB7 Path = \`knowledge/tech/decisions/\`.)
2175
2231
 
2176
2232
  ## Quality Checklist
2177
2233
 
2178
2234
  - Each candidate has context and alternatives.
2179
2235
  - No decision is asserted as final.
2236
+ - Final ADRs go to \`knowledge/tech/decisions/\`, never to \`knowledge/tech/\` directly.
2180
2237
  - Assumptions are marked.
2181
2238
  - Validation needs are explicit.
2182
2239
  `;
@@ -2875,6 +2932,69 @@ Code, tests and migrations live in the repository. Knowledge updates go under \`
2875
2932
  - Affected knowledge is updated.
2876
2933
  - Commit is suggested and awaits human confirmation \u2014 never run automatically.
2877
2934
  `;
2935
+ var OWNERSHIP_AGENT = `# Ownership Agent
2936
+
2937
+ ## Role
2938
+
2939
+ You are the Kaddo Ownership Agent. Your job is to propose **precise** \`code:\` ownership globs for
2940
+ Work Items and knowledge artifacts, so Guard can relate code changes to the right knowledge.
2941
+
2942
+ You do not write code, you do not modify files, and you never run Git. You propose; the human
2943
+ confirms and applies (with \`kaddo owners suggest\`).
2944
+
2945
+ ## When to Use
2946
+
2947
+ Use this agent after \`kaddo scan\` and \`kaddo context\`, when Work Items or artifacts are missing
2948
+ \`code:\` ownership, or when existing ownership is too broad or inaccurate.
2949
+
2950
+ ## Input Required
2951
+
2952
+ Provide \`.kaddo/context-pack.md\` as the primary input, plus the Work Items under
2953
+ \`knowledge/delivery/work-items/\`, \`knowledge/tech/codebase.md\` and \`knowledge/inventory.md\` when
2954
+ they exist (for the real source structure).
2955
+
2956
+ ## Expected Output
2957
+
2958
+ For each artifact, a precise set of \`code:\` globs.
2959
+
2960
+ ## Instructions
2961
+
2962
+ 1. Map each Work Item / artifact to the smallest set of paths that actually implement it.
2963
+ 2. Prefer **narrow** globs (e.g. \`src/payments/**\`) over broad ones (e.g. \`src/**\`).
2964
+ 3. Use real paths from the inventory/codebase \u2014 do not invent directories.
2965
+ 4. Include relevant root files (e.g. \`package.json\`, \`tsconfig.json\`) when they belong.
2966
+ 5. Flag artifacts where ownership is genuinely unclear instead of guessing broadly.
2967
+
2968
+ ## Constraints
2969
+
2970
+ - Do not implement code.
2971
+ - Do not modify files without confirmation \u2014 propose globs for the human to apply.
2972
+ - Do not create branches or commits; never run Git.
2973
+ - Prefer precision: broad globs reduce Guard usefulness.
2974
+
2975
+ ## Output Format
2976
+
2977
+ \`\`\`yaml
2978
+ # <Work Item id> \u2014 proposed ownership
2979
+ code:
2980
+ - package.json
2981
+ - tsconfig.json
2982
+ - src/cli/**
2983
+ - src/shared/**
2984
+ \`\`\`
2985
+
2986
+ ## Where to Save the Result
2987
+
2988
+ The human applies the proposed globs to the artifact's front matter with \`kaddo owners suggest\`
2989
+ (or by editing the \`code:\` field). This agent does not write files.
2990
+
2991
+ ## Quality Checklist
2992
+
2993
+ - Globs are narrow and based on real paths.
2994
+ - No \`src/**\`-style catch-alls unless truly justified.
2995
+ - Unclear ownership is flagged, not guessed.
2996
+ - Output is a proposal for human confirmation \u2014 nothing is applied automatically.
2997
+ `;
2878
2998
  var BACKLOG_AGENT = `# Backlog Agent
2879
2999
 
2880
3000
  ## Role
@@ -2986,7 +3106,9 @@ var AGENT_PROMPTS = [
2986
3106
  // Implementation (the only agent that may suggest a branch — VS-044)
2987
3107
  { fileName: "implementation-agent.md", content: IMPLEMENTATION_AGENT },
2988
3108
  // Backlog capture (idea → draft / roadmap candidate — VS-050)
2989
- { fileName: "backlog-agent.md", content: BACKLOG_AGENT }
3109
+ { fileName: "backlog-agent.md", content: BACKLOG_AGENT },
3110
+ // Ownership proposals (precise code: globs — VS-052)
3111
+ { fileName: "ownership-agent.md", content: OWNERSHIP_AGENT }
2990
3112
  // Every official prompt ends with its responsibility boundaries + Agent Trace footer.
2991
3113
  ].map((p2) => ({ fileName: p2.fileName, content: withResponsibilityTrace(p2.fileName, p2.content) }));
2992
3114
 
@@ -3008,6 +3130,7 @@ var AGENT_GROUPS = {
3008
3130
  "roadmap-agent.md",
3009
3131
  "work-item-agent.md",
3010
3132
  "implementation-agent.md",
3133
+ "ownership-agent.md",
3011
3134
  "git-strategy-agent.md"
3012
3135
  ],
3013
3136
  utilities: ["legacy-agent.md"]
@@ -3041,7 +3164,8 @@ var RECOMMENDED_BY_STATE = {
3041
3164
  "roadmap-agent.md",
3042
3165
  "backlog-agent.md",
3043
3166
  "work-item-agent.md",
3044
- "implementation-agent.md"
3167
+ "implementation-agent.md",
3168
+ "ownership-agent.md"
3045
3169
  ],
3046
3170
  legacy: [
3047
3171
  "legacy-agent.md",
@@ -3050,7 +3174,8 @@ var RECOMMENDED_BY_STATE = {
3050
3174
  "roadmap-agent.md",
3051
3175
  "backlog-agent.md",
3052
3176
  "work-item-agent.md",
3053
- "implementation-agent.md"
3177
+ "implementation-agent.md",
3178
+ "ownership-agent.md"
3054
3179
  ]
3055
3180
  };
3056
3181
  function recommendedAgents(state) {
@@ -4196,6 +4321,14 @@ async function isGitRepo(dir) {
4196
4321
  return false;
4197
4322
  }
4198
4323
  }
4324
+ async function getUntrackedFiles() {
4325
+ try {
4326
+ const result = await execa("git", ["ls-files", "--others", "--exclude-standard"]);
4327
+ return result.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
4328
+ } catch {
4329
+ return [];
4330
+ }
4331
+ }
4199
4332
 
4200
4333
  // src/services/artifact-reader.ts
4201
4334
  import matter from "gray-matter";
@@ -4216,7 +4349,8 @@ function parseArtifact(filePath, raw) {
4216
4349
  status: String(data.status ?? ""),
4217
4350
  phase: String(data.phase ?? ""),
4218
4351
  initiative: String(data.initiative ?? data.source_initiative ?? ""),
4219
- source: data.source ? String(data.source) : ""
4352
+ source: data.source ? String(data.source) : "",
4353
+ sourceId: String(data.source_id ?? "")
4220
4354
  };
4221
4355
  } catch {
4222
4356
  return null;
@@ -4854,6 +4988,15 @@ async function runGuard(opts = {}) {
4854
4988
  const plugins = resolvePlugins(config.plugins ?? []);
4855
4989
  const mode = opts.staged ? "staged" : "head";
4856
4990
  const currentFiles = await getModifiedFiles(mode);
4991
+ const untrackedFiles = opts.staged ? [] : await getUntrackedFiles();
4992
+ const printUntracked = () => {
4993
+ if (untrackedFiles.length === 0 || jsonMode) return;
4994
+ console.log("");
4995
+ console.log(`Untracked files detected (${untrackedFiles.length}):`);
4996
+ for (const f of untrackedFiles.slice(0, 10)) console.log(` - ${f}`);
4997
+ if (untrackedFiles.length > 10) console.log(` \u2026and ${untrackedFiles.length - 10} more`);
4998
+ console.log("Guard may not fully evaluate these files until they are tracked. (FYI \u2014 non-blocking)");
4999
+ };
4857
5000
  let workspaceScan = null;
4858
5001
  if (opts.workspace) {
4859
5002
  workspaceScan = await collectWorkspaceChanges(dir, mode);
@@ -4863,6 +5006,7 @@ async function runGuard(opts = {}) {
4863
5006
  if (touchedFiles.length === 0) {
4864
5007
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
4865
5008
  console.log("kaddo guard: no modified files detected.");
5009
+ printUntracked();
4866
5010
  return;
4867
5011
  }
4868
5012
  const archDir = join(dir, ARCH_DIR3);
@@ -4902,10 +5046,12 @@ async function runGuard(opts = {}) {
4902
5046
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
4903
5047
  printHeader(touchedFiles);
4904
5048
  console.log(" No artifact ownership matches found.");
5049
+ printUntracked();
4905
5050
  return;
4906
5051
  }
4907
5052
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
4908
5053
  printHeader(touchedFiles);
5054
+ printUntracked();
4909
5055
  for (const match of activeMatches) {
4910
5056
  printFYI(match, dir);
4911
5057
  }
@@ -5031,22 +5177,30 @@ function assessPhase(input) {
5031
5177
  const firstOf = (s) => items.find((i) => i.lifecycle === s);
5032
5178
  let recommendedAgents2 = [];
5033
5179
  let nextStep = "";
5180
+ let llmInstructions = [];
5034
5181
  switch (phase) {
5035
5182
  case "Discovery": {
5036
5183
  const m = firstMissingLayerAgent(input.layers);
5037
5184
  recommendedAgents2 = [m.agent];
5038
5185
  nextStep = m.step;
5186
+ llmInstructions = [`Use the ${m.agent} to fill the missing base knowledge.`, "Do not write code."];
5039
5187
  break;
5040
5188
  }
5041
5189
  case "Planning": {
5042
5190
  recommendedAgents2 = ["roadmap-agent"];
5043
5191
  nextStep = "Use roadmap-agent to create knowledge/delivery/roadmap.md";
5192
+ llmInstructions = ["Use the roadmap-agent.", "Do not write code.", "Generate roadmap candidates."];
5044
5193
  break;
5045
5194
  }
5046
5195
  case "Delivery Preparation": {
5047
5196
  recommendedAgents2 = ["kaddo create --from roadmap", "work-item-agent"];
5048
5197
  const n = input.roadmap.remaining || input.roadmap.candidates;
5049
5198
  nextStep = `Run \`kaddo create --from roadmap\`${n ? ` (${n} candidate(s))` : ""}, then refine with work-item-agent`;
5199
+ llmInstructions = [
5200
+ "Materialize roadmap candidates with `kaddo create --from roadmap`.",
5201
+ "Use the work-item-agent to refine them.",
5202
+ "Do not implement yet."
5203
+ ];
5050
5204
  break;
5051
5205
  }
5052
5206
  case "Active Delivery": {
@@ -5054,21 +5208,41 @@ function assessPhase(input) {
5054
5208
  const wi = firstOf("ready");
5055
5209
  recommendedAgents2 = ["implementation-agent"];
5056
5210
  nextStep = wi ? `Start ${wi.id} \u2014 ${wi.title} (ready \u2192 in-progress)` : "Start a ready Work Item";
5211
+ llmInstructions = [
5212
+ "Use the implementation-agent.",
5213
+ "Suggest a branch name only.",
5214
+ "Do not run git commands."
5215
+ ];
5057
5216
  } else if (bs["in-progress"] > 0) {
5058
5217
  const wi = firstOf("in-progress");
5059
5218
  recommendedAgents2 = ["implementation-agent", "kaddo scan", "kaddo owners suggest", "kaddo guard"];
5060
5219
  nextStep = wi ? `Continue ${wi.id} \u2014 ${wi.title}; then run kaddo scan, owners suggest, guard` : "Continue the in-progress Work Item; then scan, owners suggest, guard";
5220
+ llmInstructions = [
5221
+ "Continue the in-progress Work Item with the implementation-agent.",
5222
+ "After changes, run `kaddo scan`, `kaddo owners suggest` and `kaddo guard`.",
5223
+ "Do not commit, push or merge without explicit human confirmation."
5224
+ ];
5061
5225
  } else if (bs.draft > 0) {
5062
5226
  const wi = firstOf("draft");
5063
5227
  recommendedAgents2 = ["work-item-agent"];
5064
5228
  nextStep = wi ? `Refine ${wi.id} from draft to ready` : "Refine a draft Work Item to ready";
5229
+ llmInstructions = [
5230
+ "Refine draft Work Items to ready.",
5231
+ "Use the work-item-agent.",
5232
+ "Do not implement unless the user explicitly asks."
5233
+ ];
5065
5234
  } else if (bs.blocked > 0) {
5066
5235
  const wi = firstOf("blocked");
5067
5236
  recommendedAgents2 = ["work-item-agent"];
5068
5237
  nextStep = wi ? `Resolve the blocker on ${wi.id} \u2014 ${wi.title}` : "Resolve the blockers on active work";
5238
+ llmInstructions = [
5239
+ "Resolve the blockers with the work-item-agent.",
5240
+ "Do not implement blocked work."
5241
+ ];
5069
5242
  }
5070
5243
  if (input.ownership.workItemsMissingOwnership > 0) {
5071
5244
  recommendedAgents2.push("kaddo owners suggest");
5245
+ llmInstructions.push("Ownership is incomplete \u2014 propose `code:` globs (run `kaddo owners suggest`).");
5072
5246
  }
5073
5247
  break;
5074
5248
  }
@@ -5076,14 +5250,16 @@ function assessPhase(input) {
5076
5250
  if (input.roadmap.remaining > 0) {
5077
5251
  recommendedAgents2 = ["kaddo create --from roadmap", "work-item-agent"];
5078
5252
  nextStep = `Materialize ${input.roadmap.remaining} remaining roadmap candidate(s)`;
5253
+ llmInstructions = ["Materialize the remaining roadmap candidates.", "Do not implement yet."];
5079
5254
  } else {
5080
5255
  recommendedAgents2 = ["roadmap-agent"];
5081
5256
  nextStep = "No active work \u2014 use roadmap-agent to plan the next initiative";
5257
+ llmInstructions = ["No active work.", "Use the roadmap-agent to plan the next initiative."];
5082
5258
  }
5083
5259
  break;
5084
5260
  }
5085
5261
  }
5086
- return { phase, reasons, recommendedAgents: recommendedAgents2, nextStep };
5262
+ return { phase, reasons, recommendedAgents: recommendedAgents2, nextStep, llmInstructions };
5087
5263
  }
5088
5264
 
5089
5265
  // src/core/knowledge-discovery.ts
@@ -5220,6 +5396,30 @@ function renderLayersMarkdown(layers) {
5220
5396
 
5221
5397
  // src/core/project-explain.ts
5222
5398
  var ARCH_DIR4 = "knowledge";
5399
+ function normalizeTitle(t) {
5400
+ return t.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
5401
+ }
5402
+ function findDuplicateWorkItems(items) {
5403
+ const groups = [];
5404
+ const seen = /* @__PURE__ */ new Set();
5405
+ const bucket = (key, reason, pred) => {
5406
+ const matched = items.filter(pred);
5407
+ if (matched.length > 1) {
5408
+ const id = `${reason}:${key}:${matched.map((m) => m.id).sort().join(",")}`;
5409
+ if (!seen.has(id)) {
5410
+ seen.add(id);
5411
+ groups.push({ reason, items: matched.map((m) => ({ id: m.id, title: m.title })) });
5412
+ }
5413
+ }
5414
+ };
5415
+ for (const sid of new Set(items.map((i) => i.sourceId).filter(Boolean))) {
5416
+ bucket(sid, `same source candidate (${sid})`, (i) => i.sourceId === sid);
5417
+ }
5418
+ for (const nt of new Set(items.map((i) => normalizeTitle(i.title)).filter(Boolean))) {
5419
+ bucket(nt, "same normalized title", (i) => normalizeTitle(i.title) === nt);
5420
+ }
5421
+ return groups;
5422
+ }
5223
5423
  function first(values) {
5224
5424
  if (Array.isArray(values)) {
5225
5425
  const v = values.find((x) => typeof x === "string" && x);
@@ -5271,7 +5471,8 @@ function buildProjectExplanation(dir) {
5271
5471
  name: config?.project.name ?? "unknown",
5272
5472
  state: config?.project.state ?? "unknown",
5273
5473
  teamSize: config?.team.size ?? "unknown",
5274
- structure: config?.project.structure ?? "unknown"
5474
+ structure: config?.project.structure ?? "unknown",
5475
+ language: config ? languageLabel(projectLanguage(config)) : "English"
5275
5476
  };
5276
5477
  const scan2 = loadScan(dir);
5277
5478
  const stack = scan2 ? {
@@ -5337,6 +5538,9 @@ function buildProjectExplanation(dir) {
5337
5538
  workItemsMissingOwnership: items.length - withOwnership
5338
5539
  };
5339
5540
  const domains = [...new Set(workItemArtifacts.flatMap((a) => a.domains))].filter(Boolean);
5541
+ const duplicateWorkItems = findDuplicateWorkItems(
5542
+ workItemArtifacts.map((a) => ({ id: a.id || a.title, title: a.title, sourceId: a.sourceId }))
5543
+ );
5340
5544
  const roadmapPath = join(dir, ARCH_DIR4, "delivery", "roadmap.md");
5341
5545
  const roadmapMd = exists(roadmapPath) ? readFile(roadmapPath) : null;
5342
5546
  const roadmap = roadmapStats(roadmapMd, items.length);
@@ -5386,6 +5590,7 @@ function buildProjectExplanation(dir) {
5386
5590
  workItems,
5387
5591
  ownership,
5388
5592
  domains,
5593
+ duplicateWorkItems,
5389
5594
  layers,
5390
5595
  roadmap,
5391
5596
  mappedModules,
@@ -5417,6 +5622,7 @@ function renderExplanationHuman(exp) {
5417
5622
  lines.push(`- State: ${stateLabel(exp.project.state)}`);
5418
5623
  lines.push(`- Team: ${exp.project.teamSize}`);
5419
5624
  lines.push(`- Structure: ${exp.project.structure}`);
5625
+ lines.push(`- Project language: ${exp.project.language}`);
5420
5626
  lines.push("");
5421
5627
  lines.push("## Knowledge Layers");
5422
5628
  lines.push(renderLayersMarkdown(exp.layers));
@@ -5518,6 +5724,15 @@ function renderExplanationHuman(exp) {
5518
5724
  for (const m of exp.missingKnowledge) lines.push(`- ${m}`);
5519
5725
  lines.push("");
5520
5726
  }
5727
+ if (exp.duplicateWorkItems.length > 0) {
5728
+ lines.push("## Possible Duplicate Work Items");
5729
+ for (const g of exp.duplicateWorkItems) {
5730
+ lines.push(`- ${g.reason}:`);
5731
+ for (const i of g.items) lines.push(` - ${i.id} \u2014 ${i.title}`);
5732
+ }
5733
+ lines.push("Review before continuing (non-blocking).");
5734
+ lines.push("");
5735
+ }
5521
5736
  const assessment = assessPhase(exp);
5522
5737
  lines.push("## Phase");
5523
5738
  lines.push(`- Phase: ${assessment.phase}`);
@@ -5886,7 +6101,8 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
5886
6101
  name: config.project.name,
5887
6102
  state,
5888
6103
  teamSize: config.team.size,
5889
- structure: config.project.structure
6104
+ structure: config.project.structure,
6105
+ language: languageLabel(projectLanguage(config))
5890
6106
  },
5891
6107
  scan: {
5892
6108
  available: scanJson !== null,
@@ -5911,10 +6127,12 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
5911
6127
  deliveryMix,
5912
6128
  mappedModules,
5913
6129
  missing,
6130
+ // VS-052: the handoff is driven by the REAL phase, not project.state, so the pack never
6131
+ // contradicts the Current Phase block above it.
5914
6132
  handoff: {
5915
- recommendedAgents: recommendedAgentsForState(state),
5916
- nextSteps: nextStepsForState2(state),
5917
- instructions: LLM_INSTRUCTIONS,
6133
+ recommendedAgents: phase.recommendedAgents.length > 0 ? phase.recommendedAgents : recommendedAgentsForState(state),
6134
+ nextSteps: phase.nextStep ? [phase.nextStep] : nextStepsForState2(state),
6135
+ instructions: phase.llmInstructions.length > 0 ? phase.llmInstructions : LLM_INSTRUCTIONS,
5918
6136
  operatingRules: OPERATING_RULES
5919
6137
  }
5920
6138
  };
@@ -5924,9 +6142,6 @@ function serializeContextPackJson(pack) {
5924
6142
  }
5925
6143
 
5926
6144
  // src/templates/context-pack-template.ts
5927
- function stateLabel2(state) {
5928
- return state === "pre-ai" ? "pre-AI" : state;
5929
- }
5930
6145
  function renderContextPack(pack) {
5931
6146
  const { project, scan: scan2, knowledge, missing, handoff } = pack;
5932
6147
  const parts = [];
@@ -5942,7 +6157,8 @@ function renderContextPack(pack) {
5942
6157
  `- Name: ${project.name}`,
5943
6158
  `- State: ${project.state}`,
5944
6159
  `- Team size: ${project.teamSize}`,
5945
- `- Structure: ${project.structure}`
6160
+ `- Structure: ${project.structure}`,
6161
+ `- Language: ${project.language}`
5946
6162
  ].join("\n") + "\n"
5947
6163
  );
5948
6164
  parts.push("## Current Phase\n");
@@ -6066,11 +6282,11 @@ function renderContextPack(pack) {
6066
6282
  parts.push("_None \u2014 all expected context is present._\n");
6067
6283
  }
6068
6284
  parts.push("## Recommended Agent Handoff\n");
6069
- parts.push(`Recommended agents for a ${stateLabel2(project.state)} project:
6285
+ parts.push(`Recommended next for the **${pack.phase.phase}** phase:
6070
6286
  `);
6071
6287
  parts.push(handoff.recommendedAgents.map((a, i) => `${i + 1}. ${a}`).join("\n") + "\n");
6072
6288
  if (handoff.nextSteps.length > 0) {
6073
- parts.push("Next steps:\n");
6289
+ parts.push("Next step:\n");
6074
6290
  parts.push(handoff.nextSteps.map((s) => `- ${s}`).join("\n") + "\n");
6075
6291
  }
6076
6292
  parts.push("## Instructions for the LLM\n");
@@ -6169,7 +6385,7 @@ function buildUnderstandPlan(dir, config) {
6169
6385
  }
6170
6386
 
6171
6387
  // src/templates/understand-template.ts
6172
- function stateLabel3(state) {
6388
+ function stateLabel2(state) {
6173
6389
  return state === "pre-ai" ? "pre-AI" : state;
6174
6390
  }
6175
6391
  function agentName(file) {
@@ -6192,7 +6408,7 @@ function renderUnderstand(plan) {
6192
6408
  ].join("\n") + "\n"
6193
6409
  );
6194
6410
  parts.push("## Recommended Agent Flow\n");
6195
- parts.push(`Recommended order for a ${stateLabel3(project.state)} project:
6411
+ parts.push(`Recommended order for a ${stateLabel2(project.state)} project:
6196
6412
  `);
6197
6413
  parts.push(
6198
6414
  steps.map((s, i) => {
@@ -6365,6 +6581,7 @@ function runUnderstand() {
6365
6581
  );
6366
6582
  }
6367
6583
  console.log(renderUnderstandTerminal(plan));
6584
+ console.log(`Project language: ${pack.project.language} (knowledge artifacts are written in this language)`);
6368
6585
  const exp = buildProjectExplanation(dir);
6369
6586
  const assessment = assessPhase(exp);
6370
6587
  console.log("");
@@ -6943,6 +7160,64 @@ function runAdd(moduleName, opts = {}, dir = cwd()) {
6943
7160
  // src/core/ownership-suggest.ts
6944
7161
  import matter5 from "gray-matter";
6945
7162
  var SCAN_PATH = ".kaddo/scan.json";
7163
+ function normalizeGlob(input) {
7164
+ let g = input.trim().replace(/\\/g, "/");
7165
+ if (!g) return g;
7166
+ if (g.endsWith("/")) return `${g.slice(0, -1)}/**`;
7167
+ if (!g.includes("*")) {
7168
+ const base = g.split("/").pop() ?? "";
7169
+ const looksLikeFile = /\.[A-Za-z0-9]+$/.test(base);
7170
+ if (!looksLikeFile) return `${g}/**`;
7171
+ }
7172
+ return g;
7173
+ }
7174
+ function levenshtein(a, b) {
7175
+ const m = a.length;
7176
+ const n = b.length;
7177
+ const d = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
7178
+ for (let j = 0; j <= n; j++) d[0][j] = j;
7179
+ for (let i = 1; i <= m; i++) {
7180
+ for (let j = 1; j <= n; j++) {
7181
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
7182
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
7183
+ }
7184
+ }
7185
+ return d[m][n];
7186
+ }
7187
+ function analyzeGlob(dir, input) {
7188
+ const normalized = normalizeGlob(input);
7189
+ const warnings = [];
7190
+ let suggestion;
7191
+ if (/^\*+$/.test(normalized) || /^[^/*]+\/\*\*$/.test(normalized)) {
7192
+ warnings.push(
7193
+ "This glob is broad and may reduce Guard usefulness. Prefer narrower ownership when possible."
7194
+ );
7195
+ }
7196
+ const starIdx = normalized.indexOf("*");
7197
+ const literal = (starIdx >= 0 ? normalized.slice(0, starIdx) : normalized).replace(/\/+$/, "");
7198
+ if (literal && !exists(join(dir, literal))) {
7199
+ const slash = literal.lastIndexOf("/");
7200
+ const parent = slash >= 0 ? literal.slice(0, slash) : "";
7201
+ const target = slash >= 0 ? literal.slice(slash + 1) : literal;
7202
+ const parentFull = parent ? join(dir, parent) : dir;
7203
+ let near;
7204
+ try {
7205
+ if (exists(parentFull)) {
7206
+ near = readDir(parentFull).find(
7207
+ (e) => e !== target && levenshtein(e.toLowerCase(), target.toLowerCase()) <= 2
7208
+ );
7209
+ }
7210
+ } catch {
7211
+ }
7212
+ if (near) {
7213
+ suggestion = `${parent ? parent + "/" : ""}${near}${normalized.slice(literal.length)}`;
7214
+ warnings.push(`Path does not exist. Did you mean ${suggestion}?`);
7215
+ } else {
7216
+ warnings.push(`Path "${literal}" does not exist in the project.`);
7217
+ }
7218
+ }
7219
+ return { normalized, warnings, suggestion };
7220
+ }
6946
7221
  function toStringArray3(value) {
6947
7222
  return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
6948
7223
  }
@@ -7088,7 +7363,19 @@ async function chooseGlobs(suggested) {
7088
7363
  placeholder: "src/payments/**",
7089
7364
  validate: (v) => v.trim().length === 0 ? "A glob is required." : void 0
7090
7365
  });
7091
- chosen.push(glob.trim());
7366
+ const analysis = analyzeGlob(cwd(), glob);
7367
+ for (const w of analysis.warnings) log2.warn(w);
7368
+ let value = analysis.normalized;
7369
+ if (analysis.suggestion) {
7370
+ const useSuggested = await confirm2({
7371
+ message: `Use ${analysis.suggestion} instead?`,
7372
+ initialValue: true
7373
+ });
7374
+ if (useSuggested) value = analysis.suggestion;
7375
+ } else if (analysis.normalized !== glob.trim()) {
7376
+ log2.info(`Normalized to ${analysis.normalized}`);
7377
+ }
7378
+ chosen.push(value);
7092
7379
  addMore = await confirm2({ message: "Add another glob?", initialValue: false });
7093
7380
  }
7094
7381
  return [...new Set(chosen)];
@@ -9003,6 +9290,12 @@ var BOOTSTRAP_LAYERS = ["Business", "Product", "Tech"];
9003
9290
  function bootstrap(dir) {
9004
9291
  const written = [];
9005
9292
  const skipped = [];
9293
+ let language = "en";
9294
+ try {
9295
+ const config = loadConfig(dir);
9296
+ if (config) language = projectLanguage(config);
9297
+ } catch {
9298
+ }
9006
9299
  for (const target of TARGETS) {
9007
9300
  const full = join(dir, target.path);
9008
9301
  if (exists(full)) {
@@ -9010,13 +9303,22 @@ function bootstrap(dir) {
9010
9303
  continue;
9011
9304
  }
9012
9305
  const tpl = getTemplate(target.templateId);
9013
- const content = tpl ? tpl.content : "";
9306
+ const base = tpl ? tpl.content : "";
9307
+ const content = withLanguageDirective(base, language);
9014
9308
  writeFile(full, content.endsWith("\n") ? content : `${content}
9015
9309
  `);
9016
9310
  written.push(target.path);
9017
9311
  }
9018
9312
  return { written, skipped, layers: BOOTSTRAP_LAYERS };
9019
9313
  }
9314
+ function withLanguageDirective(content, language) {
9315
+ if (language !== "es") return content;
9316
+ const note = "> Idioma del proyecto: **espa\xF1ol**. Escribe este conocimiento en espa\xF1ol. Mant\xE9n en ingl\xE9s el c\xF3digo, los nombres de archivo, los comandos y las claves de configuraci\xF3n.\n";
9317
+ const fm = content.match(/^---\n[\s\S]*?\n---\n/);
9318
+ if (fm) return content.slice(0, fm[0].length) + "\n" + note + content.slice(fm[0].length);
9319
+ return `${note}
9320
+ ${content}`;
9321
+ }
9020
9322
  async function runBootstrap(dir = cwd()) {
9021
9323
  intro2("kaddo bootstrap");
9022
9324
  if (!exists(join(dir, CONFIG_PATH8))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.13.2",
3
+ "version": "3.15.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {