@corbat-tech/coco 2.2.5 → 2.4.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.
package/dist/cli/index.js CHANGED
@@ -4001,8 +4001,10 @@ var init_pricing = __esm({
4001
4001
  deepseek: { inputPerMillion: 0.14, outputPerMillion: 0.28, contextWindow: 128e3 },
4002
4002
  // Very cheap
4003
4003
  together: { inputPerMillion: 0.2, outputPerMillion: 0.2, contextWindow: 32768 },
4004
- huggingface: { inputPerMillion: 0, outputPerMillion: 0, contextWindow: 32768 }
4004
+ huggingface: { inputPerMillion: 0, outputPerMillion: 0, contextWindow: 32768 },
4005
4005
  // Free tier
4006
+ qwen: { inputPerMillion: 0.3, outputPerMillion: 1.2, contextWindow: 131072 }
4007
+ // qwen-coder-plus pricing
4006
4008
  };
4007
4009
  }
4008
4010
  });
@@ -4496,6 +4498,8 @@ function getApiKey(provider) {
4496
4498
  return process.env["TOGETHER_API_KEY"];
4497
4499
  case "huggingface":
4498
4500
  return process.env["HF_TOKEN"] ?? process.env["HUGGINGFACE_API_KEY"];
4501
+ case "qwen":
4502
+ return process.env["DASHSCOPE_API_KEY"] ?? process.env["QWEN_API_KEY"];
4499
4503
  default:
4500
4504
  return void 0;
4501
4505
  }
@@ -4528,6 +4532,8 @@ function getBaseUrl(provider) {
4528
4532
  return process.env["TOGETHER_BASE_URL"] ?? "https://api.together.xyz/v1";
4529
4533
  case "huggingface":
4530
4534
  return process.env["HF_BASE_URL"] ?? "https://api-inference.huggingface.co/v1";
4535
+ case "qwen":
4536
+ return process.env["DASHSCOPE_BASE_URL"] ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
4531
4537
  default:
4532
4538
  return void 0;
4533
4539
  }
@@ -4562,6 +4568,8 @@ function getDefaultModel(provider) {
4562
4568
  return process.env["TOGETHER_MODEL"] ?? "Qwen/Qwen2.5-Coder-32B-Instruct";
4563
4569
  case "huggingface":
4564
4570
  return process.env["HF_MODEL"] ?? "Qwen/Qwen2.5-Coder-32B-Instruct";
4571
+ case "qwen":
4572
+ return process.env["QWEN_MODEL"] ?? "qwen-coder-plus";
4565
4573
  default:
4566
4574
  return "gpt-5.3-codex";
4567
4575
  }
@@ -4669,7 +4677,8 @@ var init_env = __esm({
4669
4677
  "mistral",
4670
4678
  "deepseek",
4671
4679
  "together",
4672
- "huggingface"
4680
+ "huggingface",
4681
+ "qwen"
4673
4682
  ];
4674
4683
  cachedPreferences = null;
4675
4684
  env = {
@@ -4780,6 +4789,10 @@ async function createProvider(type, config = {}) {
4780
4789
  provider = new OpenAIProvider("huggingface", "HuggingFace Inference");
4781
4790
  mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api-inference.huggingface.co/v1";
4782
4791
  break;
4792
+ case "qwen":
4793
+ provider = new OpenAIProvider("qwen", "Alibaba Qwen");
4794
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://dashscope.aliyuncs.com/compatible-mode/v1";
4795
+ break;
4783
4796
  default:
4784
4797
  throw new ProviderError(`Unknown provider type: ${type}`, {
4785
4798
  provider: type
@@ -4812,6 +4825,7 @@ function listProviders() {
4812
4825
  { id: "deepseek", name: "DeepSeek", configured: !!getApiKey("deepseek") },
4813
4826
  { id: "together", name: "Together AI", configured: !!getApiKey("together") },
4814
4827
  { id: "huggingface", name: "HuggingFace Inference", configured: !!getApiKey("huggingface") },
4828
+ { id: "qwen", name: "Alibaba Qwen (DashScope)", configured: !!getApiKey("qwen") },
4815
4829
  { id: "lmstudio", name: "LM Studio (Local)", configured: true },
4816
4830
  { id: "ollama", name: "Ollama (Local)", configured: true }
4817
4831
  ];
@@ -24175,6 +24189,70 @@ var PROVIDER_DEFINITIONS = {
24175
24189
  }
24176
24190
  ]
24177
24191
  },
24192
+ // Alibaba Qwen - DashScope API (OpenAI-compatible)
24193
+ qwen: {
24194
+ id: "qwen",
24195
+ name: "Alibaba Qwen",
24196
+ emoji: "\u{1F7E6}",
24197
+ description: "Qwen models via Alibaba DashScope \u2014 strong coding at low cost",
24198
+ envVar: "DASHSCOPE_API_KEY",
24199
+ apiKeyUrl: "https://dashscope.aliyuncs.com",
24200
+ docsUrl: "https://help.aliyun.com/zh/model-studio/developer-reference/",
24201
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
24202
+ supportsCustomModels: true,
24203
+ openaiCompatible: true,
24204
+ paymentType: "api",
24205
+ features: {
24206
+ streaming: true,
24207
+ functionCalling: true,
24208
+ vision: true
24209
+ },
24210
+ models: [
24211
+ {
24212
+ id: "qwen-coder-plus",
24213
+ name: "Qwen Coder Plus",
24214
+ description: "Best coding model \u2014 Qwen3 based, 131K context",
24215
+ contextWindow: 131072,
24216
+ maxOutputTokens: 8192,
24217
+ recommended: true
24218
+ },
24219
+ {
24220
+ id: "qwen-max",
24221
+ name: "Qwen Max",
24222
+ description: "Most capable general model \u2014 32K context",
24223
+ contextWindow: 32768,
24224
+ maxOutputTokens: 8192
24225
+ },
24226
+ {
24227
+ id: "qwen-plus",
24228
+ name: "Qwen Plus",
24229
+ description: "Good balance of speed and quality \u2014 131K context",
24230
+ contextWindow: 131072,
24231
+ maxOutputTokens: 8192
24232
+ },
24233
+ {
24234
+ id: "qwen-turbo",
24235
+ name: "Qwen Turbo",
24236
+ description: "Fastest and cheapest \u2014 1M context",
24237
+ contextWindow: 1e6,
24238
+ maxOutputTokens: 8192
24239
+ },
24240
+ {
24241
+ id: "qwen2.5-coder-32b-instruct",
24242
+ name: "Qwen 2.5 Coder 32B",
24243
+ description: "Open weights coding model \u2014 32K context",
24244
+ contextWindow: 32768,
24245
+ maxOutputTokens: 8192
24246
+ },
24247
+ {
24248
+ id: "qwq-plus",
24249
+ name: "QwQ Plus",
24250
+ description: "Reasoning model \u2014 chain-of-thought, 131K context",
24251
+ contextWindow: 131072,
24252
+ maxOutputTokens: 8192
24253
+ }
24254
+ ]
24255
+ },
24178
24256
  // HuggingFace Inference - Free tier for open models
24179
24257
  huggingface: {
24180
24258
  id: "huggingface",
@@ -25659,6 +25737,120 @@ async function loadAgentConfig(projectPath) {
25659
25737
  }
25660
25738
  }
25661
25739
 
25740
+ // src/swarm/complexity-classifier.ts
25741
+ var AGENT_ROSTERS = {
25742
+ trivial: ["tdd-developer"],
25743
+ simple: ["tdd-developer", "qa"],
25744
+ moderate: ["tdd-developer", "qa", "architect"],
25745
+ complex: ["tdd-developer", "qa", "architect", "security-auditor", "external-reviewer"]
25746
+ };
25747
+ var LEVEL_THRESHOLDS = [
25748
+ [3, "trivial"],
25749
+ [5, "simple"],
25750
+ [7, "moderate"],
25751
+ [10, "complex"]
25752
+ ];
25753
+ var COMPLEX_KEYWORDS = ["auth", "security", "migration", "refactor"];
25754
+ var SIMPLE_KEYWORDS = ["fix", "typo", "style"];
25755
+ function scoreToLevel(score) {
25756
+ for (const [threshold, level] of LEVEL_THRESHOLDS) {
25757
+ if (score <= threshold) return level;
25758
+ }
25759
+ return "complex";
25760
+ }
25761
+ function classifyFeatureHeuristic(feature) {
25762
+ const description = feature.description ?? "";
25763
+ const criteria = feature.acceptanceCriteria ?? [];
25764
+ const deps = feature.dependencies ?? [];
25765
+ const wordCount = description.split(/\s+/).filter(Boolean).length;
25766
+ const lowerDesc = description.toLowerCase();
25767
+ let score = 1;
25768
+ const reasons = [
25769
+ `words=${wordCount}`,
25770
+ `criteria=${criteria.length}`,
25771
+ `deps=${deps.length}`
25772
+ ];
25773
+ if (wordCount > 50) score += 3;
25774
+ else if (wordCount > 20) score += 2;
25775
+ else if (wordCount > 5) score += 1;
25776
+ if (criteria.length >= 4) score += 3;
25777
+ else if (criteria.length >= 2) score += 2;
25778
+ else if (criteria.length >= 1) score += 1;
25779
+ if (deps.length >= 3) score += 8;
25780
+ else if (deps.length >= 1) score += 4;
25781
+ const hasComplexKeyword = COMPLEX_KEYWORDS.some((kw) => lowerDesc.includes(kw));
25782
+ if (hasComplexKeyword) {
25783
+ score += 8;
25784
+ reasons.push("complex-keywords");
25785
+ }
25786
+ const hasSimpleKeyword = SIMPLE_KEYWORDS.some((kw) => lowerDesc.includes(kw));
25787
+ if (hasSimpleKeyword) {
25788
+ score -= 2;
25789
+ reasons.push("simple-keywords");
25790
+ }
25791
+ score = Math.max(1, Math.min(10, score));
25792
+ const level = scoreToLevel(score);
25793
+ return {
25794
+ score,
25795
+ level,
25796
+ agents: AGENT_ROSTERS[level],
25797
+ reasoning: `Heuristic score ${score}: ${reasons.join(", ")}`
25798
+ };
25799
+ }
25800
+ async function classifyFeatureComplexity(feature, provider) {
25801
+ const criteriaCount = (feature.acceptanceCriteria ?? []).length;
25802
+ const depsCount = (feature.dependencies ?? []).length;
25803
+ const userMessage = `Rate the complexity of this software feature on a scale of 1-10.
25804
+
25805
+ Feature: ${feature.name}
25806
+ Description: ${feature.description ?? ""}
25807
+ Acceptance Criteria count: ${criteriaCount}
25808
+ Dependencies count: ${depsCount}
25809
+
25810
+ Complexity guidelines:
25811
+ - 1-3 (trivial): Fix, typo, style change \u2014 short description, 0-1 criteria, 0 deps
25812
+ - 4-5 (simple): Small enhancement \u2014 medium description, 2-3 criteria
25813
+ - 6-7 (moderate): Feature with logic \u2014 longer description, 4+ criteria or 1+ deps
25814
+ - 8-10 (complex): Security, auth, migration, heavy refactor \u2014 many deps or critical path
25815
+
25816
+ Return only JSON: { "score": <number 1-10>, "reasoning": "<brief explanation>" }`;
25817
+ try {
25818
+ const response = await provider.chat([{ role: "user", content: userMessage }], {
25819
+ maxTokens: 256,
25820
+ temperature: 0.2
25821
+ });
25822
+ const json2 = extractJsonScore(response.content);
25823
+ if (json2 !== null && typeof json2.score === "number") {
25824
+ const score = Math.max(1, Math.min(10, Math.round(json2.score)));
25825
+ const level = scoreToLevel(score);
25826
+ return {
25827
+ score,
25828
+ level,
25829
+ agents: AGENT_ROSTERS[level],
25830
+ reasoning: json2.reasoning ?? `LLM score: ${score}`
25831
+ };
25832
+ }
25833
+ } catch {
25834
+ }
25835
+ return classifyFeatureHeuristic(feature);
25836
+ }
25837
+ function extractJsonScore(text13) {
25838
+ try {
25839
+ const stripped = text13.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
25840
+ return JSON.parse(stripped);
25841
+ } catch {
25842
+ const match = text13.match(/\{[\s\S]*?\}/);
25843
+ if (match) {
25844
+ try {
25845
+ return JSON.parse(match[0]);
25846
+ } catch {
25847
+ return null;
25848
+ }
25849
+ }
25850
+ return null;
25851
+ }
25852
+ }
25853
+
25662
25854
  // src/swarm/task-board.ts
25663
25855
  async function createBoard(projectPath, spec) {
25664
25856
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -26199,6 +26391,12 @@ var AGENT_DEFINITIONS = {
26199
26391
  };
26200
26392
 
26201
26393
  // src/swarm/lifecycle.ts
26394
+ var COMPLEXITY_RANK = {
26395
+ trivial: 0,
26396
+ simple: 1,
26397
+ moderate: 2,
26398
+ complex: 3
26399
+ };
26202
26400
  async function runSwarmLifecycle(options) {
26203
26401
  const ctx = {
26204
26402
  options,
@@ -26415,17 +26613,33 @@ async function processFeature(ctx, feature) {
26415
26613
  );
26416
26614
  continue;
26417
26615
  }
26616
+ const classified = ctx.options.skipComplexityCheck ? {
26617
+ score: 10,
26618
+ level: "complex",
26619
+ agents: AGENT_ROSTERS.complex} : await classifyFeatureComplexity(feature, provider);
26620
+ const threshold = ctx.options.complexityThreshold;
26621
+ const roster = threshold !== void 0 && COMPLEXITY_RANK[classified.level] >= COMPLEXITY_RANK[threshold] ? {
26622
+ score: 10,
26623
+ level: "complex",
26624
+ agents: AGENT_ROSTERS.complex,
26625
+ reasoning: `complexityThreshold=${threshold} (feature classified as ${classified.level})`
26626
+ } : classified;
26627
+ progress(
26628
+ ctx.options,
26629
+ "feature_loop",
26630
+ `Feature: ${feature.name} \u2014 complexity=${roster.level} (score=${roster.score}) roster=[${roster.agents.join(",")}]`
26631
+ );
26418
26632
  const [archReview, secReview, qaReview] = await Promise.all([
26419
- runArchReview(feature, provider, agentConfig.architect),
26420
- runSecurityAudit(feature, provider, agentConfig["security-auditor"]),
26421
- runQAReview(feature, provider, agentConfig.qa)
26633
+ roster.agents.includes("architect") ? runArchReview(feature, provider, agentConfig.architect) : Promise.resolve(void 0),
26634
+ roster.agents.includes("security-auditor") ? runSecurityAudit(feature, provider, agentConfig["security-auditor"]) : Promise.resolve(void 0),
26635
+ roster.agents.includes("qa") ? runQAReview(feature, provider, agentConfig.qa) : Promise.resolve(void 0)
26422
26636
  ]);
26423
- const extReview = await runExternalReviewer(
26637
+ const extReview = roster.agents.includes("external-reviewer") ? await runExternalReviewer(
26424
26638
  feature,
26425
26639
  { arch: archReview, security: secReview, qa: qaReview },
26426
26640
  provider,
26427
26641
  agentConfig["external-reviewer"]
26428
- );
26642
+ ) : synthesizeLocalReviews({ arch: archReview, security: secReview, qa: qaReview });
26429
26643
  lastReviewScore = extReview.score;
26430
26644
  await emitGate(
26431
26645
  projectPath,
@@ -26720,13 +26934,17 @@ Return JSON with: { "score": number, "issues": ["..."], "summary": "..." }`;
26720
26934
  }
26721
26935
  async function runExternalReviewer(feature, reviews, provider, _config) {
26722
26936
  const prompt = AGENT_DEFINITIONS["external-reviewer"].systemPrompt;
26937
+ const reviewLines = [
26938
+ reviews.arch ? `Architecture review: ${JSON.stringify(reviews.arch)}` : null,
26939
+ reviews.security ? `Security audit: ${JSON.stringify(reviews.security)}` : null,
26940
+ reviews.qa ? `QA review: ${JSON.stringify(reviews.qa)}` : null
26941
+ ].filter(Boolean).join("\n");
26723
26942
  const userMessage = `Synthesize these reviews for feature: ${feature.name}
26724
26943
 
26725
- Architecture review: ${JSON.stringify(reviews.arch)}
26726
- Security audit: ${JSON.stringify(reviews.security)}
26727
- QA review: ${JSON.stringify(reviews.qa)}
26944
+ ${reviewLines}
26728
26945
 
26729
26946
  Return JSON with: { "verdict": "APPROVE|REQUEST_CHANGES|REJECT", "score": number, "blockers": ["..."], "summary": "..." }`;
26947
+ const fallback = synthesizeLocalReviews(reviews);
26730
26948
  try {
26731
26949
  const response = await provider.chat([{ role: "user", content: userMessage }], {
26732
26950
  system: prompt,
@@ -26734,27 +26952,24 @@ Return JSON with: { "verdict": "APPROVE|REQUEST_CHANGES|REJECT", "score": number
26734
26952
  temperature: 0.4
26735
26953
  });
26736
26954
  const json2 = extractJson(response.content);
26737
- const avgScore = Math.round(
26738
- (reviews.arch.score + reviews.security.score + reviews.qa.score) / 3
26739
- );
26740
- return json2 ?? {
26741
- verdict: avgScore >= 85 ? "APPROVE" : "REQUEST_CHANGES",
26742
- score: avgScore,
26743
- blockers: [],
26744
- summary: `Synthesized review score: ${avgScore}`
26745
- };
26955
+ return json2 ?? fallback;
26746
26956
  } catch {
26747
- const avgScore = Math.round(
26748
- (reviews.arch.score + reviews.security.score + reviews.qa.score) / 3
26749
- );
26750
- return {
26751
- verdict: avgScore >= 85 ? "APPROVE" : "REQUEST_CHANGES",
26752
- score: avgScore,
26753
- blockers: [],
26754
- summary: `External review score: ${avgScore}`
26755
- };
26957
+ return fallback;
26756
26958
  }
26757
26959
  }
26960
+ function synthesizeLocalReviews(reviews) {
26961
+ const scores = [reviews.arch?.score, reviews.security?.score, reviews.qa?.score].filter(
26962
+ (s) => s !== void 0
26963
+ );
26964
+ const avgScore = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 85;
26965
+ const reviewCount = scores.length;
26966
+ return {
26967
+ verdict: avgScore >= 85 ? "APPROVE" : "REQUEST_CHANGES",
26968
+ score: avgScore,
26969
+ blockers: [],
26970
+ summary: `Local synthesis (${reviewCount} review${reviewCount !== 1 ? "s" : ""}): score ${avgScore}`
26971
+ };
26972
+ }
26758
26973
  async function runIntegratorAgent(spec, featureResults, provider, _config) {
26759
26974
  const prompt = AGENT_DEFINITIONS.integrator.systemPrompt;
26760
26975
  const results = Array.from(featureResults.values());
@@ -26810,14 +27025,20 @@ async function emitGate(projectPath, gate, passed, reason) {
26810
27025
  function topologicalSort(features) {
26811
27026
  const sorted = [];
26812
27027
  const visited = /* @__PURE__ */ new Set();
27028
+ const inProgress = /* @__PURE__ */ new Set();
26813
27029
  const featureMap = new Map(features.map((f) => [f.id, f]));
26814
27030
  function visit(featureId) {
26815
27031
  if (visited.has(featureId)) return;
27032
+ if (inProgress.has(featureId)) {
27033
+ throw new Error(`Circular dependency detected involving feature "${featureId}"`);
27034
+ }
26816
27035
  const feature = featureMap.get(featureId);
26817
27036
  if (!feature) return;
27037
+ inProgress.add(featureId);
26818
27038
  for (const depId of feature.dependencies) {
26819
27039
  visit(depId);
26820
27040
  }
27041
+ inProgress.delete(featureId);
26821
27042
  visited.add(featureId);
26822
27043
  sorted.push(feature);
26823
27044
  }
@@ -26984,7 +27205,7 @@ var helpCommand = {
26984
27205
  title: "Quality Mode",
26985
27206
  commands: [
26986
27207
  {
26987
- cmd: "/coco [on|off]",
27208
+ cmd: "/quality [on|off]",
26988
27209
  desc: "Auto-test, self-review, iterate until quality \u2265 85/100",
26989
27210
  highlight: true
26990
27211
  }
@@ -28582,7 +28803,6 @@ async function selectProviderInteractively(providers, currentProviderId) {
28582
28803
  let lastTotalLines = 0;
28583
28804
  const clearPrevious = () => {
28584
28805
  if (lastTotalLines === 0) return;
28585
- process.stdout.write("\x1B[2K\r");
28586
28806
  for (let i = 0; i < lastTotalLines; i++) {
28587
28807
  process.stdout.write("\x1B[1A\x1B[2K");
28588
28808
  }
@@ -28725,6 +28945,7 @@ async function switchProvider(initialProvider, session) {
28725
28945
  const userFacingProviderId = initialProvider.id;
28726
28946
  let internalProviderId = initialProvider.id;
28727
28947
  let selectedAuthMethod = "apikey";
28948
+ let newApiKeyForSaving = null;
28728
28949
  if (newProvider.id === "lmstudio" || newProvider.id === "ollama") {
28729
28950
  const result = newProvider.id === "ollama" ? await setupOllamaProvider() : await setupLMStudioProvider();
28730
28951
  if (!result) {
@@ -28870,6 +29091,7 @@ Using existing API key...`));
28870
29091
  }
28871
29092
  process.env[newProvider.envVar] = key;
28872
29093
  selectedAuthMethod = "apikey";
29094
+ newApiKeyForSaving = key;
28873
29095
  }
28874
29096
  } else if (authChoice === "remove") {
28875
29097
  const removeOptions = [];
@@ -28927,6 +29149,7 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
28927
29149
  }
28928
29150
  process.env[newProvider.envVar] = key;
28929
29151
  selectedAuthMethod = "apikey";
29152
+ newApiKeyForSaving = key;
28930
29153
  }
28931
29154
  }
28932
29155
  const recommendedModel = getRecommendedModel(newProvider.id);
@@ -28948,16 +29171,25 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
28948
29171
  spinner19.stop(chalk25.green("Connected!"));
28949
29172
  session.config.provider.type = userFacingProviderId;
28950
29173
  session.config.provider.model = newModel;
28951
- await saveProviderPreference(
28952
- userFacingProviderId,
28953
- newModel,
28954
- selectedAuthMethod
28955
- );
29174
+ if (newApiKeyForSaving) {
29175
+ await saveConfiguration({
29176
+ type: userFacingProviderId,
29177
+ model: newModel,
29178
+ apiKey: newApiKeyForSaving
29179
+ });
29180
+ } else {
29181
+ await saveProviderPreference(
29182
+ userFacingProviderId,
29183
+ newModel,
29184
+ selectedAuthMethod
29185
+ );
29186
+ }
28956
29187
  console.log(chalk25.green(`
28957
29188
  \u2713 Switched to ${newProvider.emoji} ${newProvider.name}`));
28958
29189
  console.log(chalk25.dim(` Model: ${newModel}`));
28959
29190
  if (selectedAuthMethod === "oauth") {
28960
- console.log(chalk25.dim(` Auth: ChatGPT subscription (OAuth)`));
29191
+ const authLabel = newProvider.id === "gemini" ? "Google account (OAuth)" : "ChatGPT subscription (OAuth)";
29192
+ console.log(chalk25.dim(` Auth: ${authLabel}`));
28961
29193
  }
28962
29194
  console.log(chalk25.dim(` Use /model to change models
28963
29195
  `));
@@ -32654,7 +32886,6 @@ var RECOMMENDED_PROJECT = [
32654
32886
  "copy_file",
32655
32887
  "move_file",
32656
32888
  "git_add",
32657
- "git_commit",
32658
32889
  "run_tests",
32659
32890
  "run_test_file",
32660
32891
  "run_script",
@@ -32673,12 +32904,28 @@ var RECOMMENDED_PROJECT = [
32673
32904
  "bash:npm:test",
32674
32905
  "bash:npm:ci",
32675
32906
  "bash:pnpm:install",
32907
+ "bash:pnpm:i",
32676
32908
  "bash:pnpm:run",
32677
32909
  "bash:pnpm:test",
32910
+ "bash:pnpm:typecheck",
32911
+ "bash:pnpm:lint",
32912
+ "bash:pnpm:build",
32913
+ "bash:pnpm:check",
32914
+ "bash:pnpm:format",
32915
+ "bash:pnpm:dev",
32916
+ "bash:pnpm:add",
32917
+ "bash:pnpm:remove",
32918
+ "bash:pnpm:update",
32919
+ "bash:pnpm:exec",
32920
+ "bash:pnpm:rebuild",
32678
32921
  "bash:yarn:install",
32679
32922
  "bash:yarn:run",
32680
32923
  "bash:yarn:test",
32681
32924
  "bash:node",
32925
+ "bash:vitest",
32926
+ "bash:tsc",
32927
+ "bash:tsx",
32928
+ "bash:oxlint",
32682
32929
  // ── Bash: JVM toolchain ──
32683
32930
  "bash:java",
32684
32931
  "bash:javac",
@@ -32706,11 +32953,13 @@ var RECOMMENDED_PROJECT = [
32706
32953
  "bash:go:test",
32707
32954
  "bash:go:vet",
32708
32955
  "bash:pip:install",
32709
- // ── Bash: git local (staging only — push is in ASK) ──
32710
- "bash:git:add",
32711
- "bash:git:commit"
32956
+ // ── Bash: git local (staging only — commit and push are in ASK) ──
32957
+ "bash:git:add"
32712
32958
  ];
32713
32959
  var ALWAYS_ASK = [
32960
+ // ── Git commit — always ask by default; use /permissions allow-commits to opt in ──
32961
+ "git_commit",
32962
+ "bash:git:commit",
32714
32963
  // ── Coco native (risky) ──
32715
32964
  "delete_file",
32716
32965
  "git_push",
@@ -32787,6 +33036,7 @@ var RECOMMENDED_DENY = [
32787
33036
  // ── Package publishing ──
32788
33037
  "bash:npm:publish",
32789
33038
  "bash:yarn:publish",
33039
+ "bash:pnpm:publish",
32790
33040
  "bash:cargo:publish",
32791
33041
  // ── Disk / low-level destructive ──
32792
33042
  "bash:dd",
@@ -32835,8 +33085,10 @@ async function showPermissionSuggestion() {
32835
33085
  console.log(chalk25.magenta.bold(" \u{1F4CB} Recommended Permissions"));
32836
33086
  console.log();
32837
33087
  console.log(chalk25.dim(" Coco has a curated set of tool permissions for developers:"));
32838
- console.log(chalk25.dim(" \u2022 Allow: file read/write, search, git (local), build, tests..."));
32839
- console.log(chalk25.dim(" \u2022 Ask each time: curl, rm, git pull, docker exec, cloud..."));
33088
+ console.log(chalk25.dim(" \u2022 Allow: file read/write, search, git staging, build, tests..."));
33089
+ console.log(
33090
+ chalk25.dim(" \u2022 Ask each time: git commit, curl, rm, git pull, docker exec, cloud...")
33091
+ );
32840
33092
  console.log(chalk25.dim(" \u2022 Deny: sudo, git push, git rebase, docker push, k8s apply..."));
32841
33093
  console.log();
32842
33094
  console.log(chalk25.dim(" Stored in ~/.coco/trusted-tools.json \u2014 edit manually or let"));
@@ -32927,7 +33179,7 @@ var permissionsCommand = {
32927
33179
  name: "permissions",
32928
33180
  aliases: ["perms"],
32929
33181
  description: "Manage tool permissions and recommended allowlist",
32930
- usage: "/permissions [apply|view|reset]",
33182
+ usage: "/permissions [apply|view|reset|allow-commits|revoke-commits]",
32931
33183
  async execute(args, session) {
32932
33184
  const subcommand = args[0]?.toLowerCase() ?? "status";
32933
33185
  switch (subcommand) {
@@ -32940,6 +33192,12 @@ var permissionsCommand = {
32940
33192
  case "reset":
32941
33193
  await resetPermissions(session);
32942
33194
  return false;
33195
+ case "allow-commits":
33196
+ await allowCommits(session);
33197
+ return false;
33198
+ case "revoke-commits":
33199
+ await revokeCommits(session);
33200
+ return false;
32943
33201
  case "status":
32944
33202
  default:
32945
33203
  await showStatus(session);
@@ -32984,9 +33242,15 @@ async function showStatus(session) {
32984
33242
  }
32985
33243
  }
32986
33244
  console.log();
32987
- console.log(chalk25.dim(" /permissions apply \u2014 Apply recommended permissions"));
32988
- console.log(chalk25.dim(" /permissions view \u2014 View recommended template"));
32989
- console.log(chalk25.dim(" /permissions reset \u2014 Reset to empty"));
33245
+ console.log(chalk25.dim(" /permissions apply \u2014 Apply recommended permissions"));
33246
+ console.log(chalk25.dim(" /permissions view \u2014 View recommended template"));
33247
+ console.log(chalk25.dim(" /permissions reset \u2014 Reset to empty"));
33248
+ console.log(
33249
+ chalk25.dim(" /permissions allow-commits \u2014 Auto-approve git commit for this project")
33250
+ );
33251
+ console.log(
33252
+ chalk25.dim(" /permissions revoke-commits \u2014 Require confirmation for git commit again")
33253
+ );
32990
33254
  console.log();
32991
33255
  }
32992
33256
  async function applyRecommended(session) {
@@ -33000,6 +33264,25 @@ async function applyRecommended(session) {
33000
33264
  console.log(chalk25.green(" \u2713 Recommended permissions applied!"));
33001
33265
  console.log(chalk25.dim(" Use /permissions to review."));
33002
33266
  }
33267
+ async function allowCommits(session) {
33268
+ const commitTools = ["git_commit", "bash:git:commit"];
33269
+ for (const tool of commitTools) {
33270
+ session.trustedTools.add(tool);
33271
+ await saveTrustedTool(tool, session.projectPath, false);
33272
+ }
33273
+ console.log(chalk25.green(" \u2713 git commit will be auto-approved for this project"));
33274
+ console.log(chalk25.dim(" Use /permissions revoke-commits to require confirmation again."));
33275
+ }
33276
+ async function revokeCommits(session) {
33277
+ const commitTools = ["git_commit", "bash:git:commit"];
33278
+ for (const tool of commitTools) {
33279
+ session.trustedTools.delete(tool);
33280
+ await removeTrustedTool(tool, session.projectPath, false);
33281
+ await removeTrustedTool(tool, session.projectPath, true);
33282
+ }
33283
+ console.log(chalk25.yellow(" \u25CB git commit will now require confirmation for this project"));
33284
+ console.log(chalk25.dim(" Use /permissions allow-commits to enable auto-approve again."));
33285
+ }
33003
33286
  async function resetPermissions(session) {
33004
33287
  const confirmed = await p25.confirm({
33005
33288
  message: "Reset all tool permissions? This removes all trusted tools.",
@@ -33024,15 +33307,15 @@ async function resetPermissions(session) {
33024
33307
  console.log(chalk25.green(" \u2713 All tool permissions reset."));
33025
33308
  }
33026
33309
 
33027
- // src/cli/repl/coco-mode.ts
33310
+ // src/cli/repl/quality-loop.ts
33028
33311
  init_paths();
33029
- var cocoModeEnabled = true;
33312
+ var qualityLoopEnabled = true;
33030
33313
  var hintShown = false;
33031
- function isCocoMode() {
33032
- return cocoModeEnabled;
33314
+ function isQualityLoop() {
33315
+ return qualityLoopEnabled;
33033
33316
  }
33034
- function setCocoMode(enabled) {
33035
- cocoModeEnabled = enabled;
33317
+ function setQualityLoop(enabled) {
33318
+ qualityLoopEnabled = enabled;
33036
33319
  }
33037
33320
  function wasHintShown() {
33038
33321
  return hintShown;
@@ -33063,8 +33346,8 @@ function looksLikeFeatureRequest(input) {
33063
33346
  ];
33064
33347
  return featureKeywords.some((re) => re.test(trimmed));
33065
33348
  }
33066
- function formatCocoHint() {
33067
- return chalk25.dim(" tip: ") + chalk25.magenta("/coco") + chalk25.dim(" enables auto-test & iterate until quality converges");
33349
+ function formatQualityLoopHint() {
33350
+ return chalk25.dim(" tip: ") + chalk25.magenta("/quality") + chalk25.dim(" enables auto-test & iterate until quality converges");
33068
33351
  }
33069
33352
  function formatQualityResult(result) {
33070
33353
  const lines = [];
@@ -33097,19 +33380,20 @@ function formatQualityResult(result) {
33097
33380
  lines.push("");
33098
33381
  return lines.join("\n");
33099
33382
  }
33100
- async function loadCocoModePreference() {
33383
+ async function loadQualityLoopPreference() {
33101
33384
  try {
33102
33385
  const content = await fs32__default.readFile(CONFIG_PATHS.config, "utf-8");
33103
33386
  const config = JSON.parse(content);
33104
- if (typeof config.cocoMode === "boolean") {
33105
- cocoModeEnabled = config.cocoMode;
33106
- return config.cocoMode;
33387
+ const value = config.qualityLoop ?? config.cocoMode;
33388
+ if (typeof value === "boolean") {
33389
+ qualityLoopEnabled = value;
33390
+ return value;
33107
33391
  }
33108
33392
  } catch {
33109
33393
  }
33110
33394
  return true;
33111
33395
  }
33112
- async function saveCocoModePreference(enabled) {
33396
+ async function saveQualityLoopPreference(enabled) {
33113
33397
  try {
33114
33398
  let config = {};
33115
33399
  try {
@@ -33117,13 +33401,13 @@ async function saveCocoModePreference(enabled) {
33117
33401
  config = JSON.parse(content);
33118
33402
  } catch {
33119
33403
  }
33120
- config.cocoMode = enabled;
33404
+ config.qualityLoop = enabled;
33121
33405
  await fs32__default.writeFile(CONFIG_PATHS.config, JSON.stringify(config, null, 2) + "\n");
33122
33406
  } catch {
33123
33407
  }
33124
33408
  }
33125
- function parseCocoQualityReport(content) {
33126
- const marker = "COCO_QUALITY_REPORT";
33409
+ function parseQualityLoopReport(content) {
33410
+ const marker = "QUALITY_LOOP_REPORT";
33127
33411
  const idx = content.indexOf(marker);
33128
33412
  if (idx === -1) return null;
33129
33413
  const block = content.slice(idx);
@@ -33152,11 +33436,11 @@ function parseCocoQualityReport(content) {
33152
33436
  securityScore: isNaN(security) ? void 0 : security
33153
33437
  };
33154
33438
  }
33155
- function getCocoModeSystemPrompt() {
33439
+ function getQualityLoopSystemPrompt() {
33156
33440
  return `
33157
- ## COCO Quality Mode (ACTIVE)
33441
+ ## Quality Loop Mode (ACTIVE)
33158
33442
 
33159
- You are operating in COCO quality mode. After implementing code changes, you MUST follow this iteration cycle:
33443
+ You are operating in quality loop mode. After implementing code changes, you MUST follow this iteration cycle:
33160
33444
 
33161
33445
  1. **Implement** the requested changes (code + tests)
33162
33446
  2. **Run tests** using the run_tests or bash_exec tool
@@ -33171,7 +33455,7 @@ You are operating in COCO quality mode. After implementing code changes, you MUS
33171
33455
  After completing the cycle, output a quality summary in this exact format:
33172
33456
 
33173
33457
  \`\`\`
33174
- COCO_QUALITY_REPORT
33458
+ QUALITY_LOOP_REPORT
33175
33459
  score_history: [first_score, ..., final_score]
33176
33460
  tests_passed: X
33177
33461
  tests_total: Y
@@ -33190,12 +33474,12 @@ Key rules:
33190
33474
  - Report honestly - don't inflate scores`;
33191
33475
  }
33192
33476
 
33193
- // src/cli/repl/commands/coco.ts
33194
- var cocoCommand = {
33195
- name: "coco",
33196
- aliases: [],
33477
+ // src/cli/repl/commands/quality.ts
33478
+ var qualityCommand = {
33479
+ name: "quality",
33480
+ aliases: ["coco"],
33197
33481
  description: "Toggle quality mode \u2014 auto-test, self-review, iterate until converged",
33198
- usage: "/coco [on|off]",
33482
+ usage: "/quality [on|off]",
33199
33483
  async execute(args, session) {
33200
33484
  const arg = args[0]?.toLowerCase();
33201
33485
  let newState;
@@ -33204,12 +33488,12 @@ var cocoCommand = {
33204
33488
  } else if (arg === "off") {
33205
33489
  newState = false;
33206
33490
  } else if (arg === "status") {
33207
- const state = isCocoMode();
33491
+ const state = isQualityLoop();
33208
33492
  const skillAvailable = session.skillRegistry?.has("coco-fix-iterate");
33209
33493
  const modeType = state && skillAvailable ? chalk25.cyan(" (skill-based)") : state ? chalk25.dim(" (prompt-based)") : "";
33210
33494
  console.log();
33211
33495
  console.log(
33212
- chalk25.magenta(" COCO quality mode: ") + (state ? chalk25.green.bold("ON") : chalk25.dim("OFF")) + modeType
33496
+ chalk25.magenta(" Quality loop: ") + (state ? chalk25.green.bold("ON") : chalk25.dim("OFF")) + modeType
33213
33497
  );
33214
33498
  console.log();
33215
33499
  if (state) {
@@ -33225,24 +33509,24 @@ var cocoCommand = {
33225
33509
  console.log(chalk25.dim(" 3. Self-review against 12 quality dimensions"));
33226
33510
  console.log(chalk25.dim(" 4. Iterate until quality converges (\u226585/100)"));
33227
33511
  } else {
33228
- console.log(chalk25.dim(" Enable with /coco on for quality-driven development"));
33512
+ console.log(chalk25.dim(" Enable with /quality on for quality-driven development"));
33229
33513
  }
33230
33514
  console.log();
33231
33515
  return false;
33232
33516
  } else {
33233
- newState = !isCocoMode();
33517
+ newState = !isQualityLoop();
33234
33518
  }
33235
- setCocoMode(newState);
33236
- saveCocoModePreference(newState).catch(() => {
33519
+ setQualityLoop(newState);
33520
+ saveQualityLoopPreference(newState).catch(() => {
33237
33521
  });
33238
33522
  console.log();
33239
33523
  if (newState) {
33240
- console.log(chalk25.magenta(" COCO quality mode: ") + chalk25.green.bold("ON"));
33524
+ console.log(chalk25.magenta(" Quality loop: ") + chalk25.green.bold("ON"));
33241
33525
  console.log(
33242
33526
  chalk25.dim(" Agent will auto-test, self-review, and iterate until quality \u2265 85/100")
33243
33527
  );
33244
33528
  } else {
33245
- console.log(chalk25.magenta(" COCO quality mode: ") + chalk25.dim("OFF"));
33529
+ console.log(chalk25.magenta(" Quality loop: ") + chalk25.dim("OFF"));
33246
33530
  console.log(chalk25.dim(" Fast mode \u2014 agent responds without quality iteration"));
33247
33531
  }
33248
33532
  console.log();
@@ -33447,7 +33731,7 @@ var tutorialCommand = {
33447
33731
  {
33448
33732
  step: "3",
33449
33733
  title: "Enable quality mode",
33450
- desc: "Type /coco to enable auto-iteration: test \u2192 analyze \u2192 fix \u2192 repeat until score \u2265 85"
33734
+ desc: "Type /quality to enable auto-iteration: test \u2192 analyze \u2192 fix \u2192 repeat until score \u2265 85"
33451
33735
  },
33452
33736
  {
33453
33737
  step: "4",
@@ -33467,7 +33751,7 @@ var tutorialCommand = {
33467
33751
  }
33468
33752
  console.log(chalk25.bold("Useful commands:"));
33469
33753
  console.log(
33470
- ` ${chalk25.yellow("/coco")} ${chalk25.dim("Toggle quality mode (auto-iteration)")}`
33754
+ ` ${chalk25.yellow("/quality")} ${chalk25.dim("Toggle quality mode (auto-iteration)")}`
33471
33755
  );
33472
33756
  console.log(` ${chalk25.yellow("/init")} ${chalk25.dim("Initialize a new project")}`);
33473
33757
  console.log(` ${chalk25.yellow("/help")} ${chalk25.dim("See all available commands")}`);
@@ -40741,7 +41025,7 @@ var commands = [
40741
41025
  copyCommand,
40742
41026
  allowPathCommand,
40743
41027
  permissionsCommand,
40744
- cocoCommand,
41028
+ qualityCommand,
40745
41029
  fullAccessCommand,
40746
41030
  updateCocoCommand,
40747
41031
  imageCommand,
@@ -40890,11 +41174,11 @@ function createInputHandler(_session) {
40890
41174
  const getPrompt = () => {
40891
41175
  const imageIndicator = hasPendingImage() ? chalk25.cyan(" \u{1F4CE} 1 image") : "";
40892
41176
  const imageIndicatorLen = hasPendingImage() ? 10 : 0;
40893
- if (isCocoMode()) {
41177
+ if (isQualityLoop()) {
40894
41178
  return {
40895
- str: "\u{1F965} " + chalk25.magenta("[coco]") + " \u203A " + imageIndicator,
40896
- // 🥥=2 + space=1 + [coco]=6 + space=1 + ›=1 + space=1 = 12 + image indicator
40897
- visualLen: 12 + imageIndicatorLen
41179
+ str: "\u{1F965} " + chalk25.magenta("[quality]") + " \u203A " + imageIndicator,
41180
+ // 🥥=2 + space=1 + [quality]=9 + space=1 + ›=1 + space=1 = 15 + image indicator
41181
+ visualLen: 15 + imageIndicatorLen
40898
41182
  };
40899
41183
  }
40900
41184
  return {
@@ -43194,8 +43478,8 @@ function formatStatusBar(projectPath, config, gitCtx) {
43194
43478
  const providerName = config.provider.type;
43195
43479
  const modelName = config.provider.model || "default";
43196
43480
  parts.push(chalk25.dim(`${providerName}/`) + chalk25.cyan(modelName));
43197
- if (isCocoMode()) {
43198
- parts.push(chalk25.green("\u{1F504} coco"));
43481
+ if (isQualityLoop()) {
43482
+ parts.push(chalk25.green("\u{1F504} quality loop"));
43199
43483
  }
43200
43484
  if (isFullAccessMode()) {
43201
43485
  parts.push(chalk25.yellow("\u26A1 full-access"));
@@ -43268,7 +43552,7 @@ async function startRepl(options = {}) {
43268
43552
  const { loadFullAccessPreference: loadFullAccessPreference2 } = await Promise.resolve().then(() => (init_full_access_mode(), full_access_mode_exports));
43269
43553
  const { loadFullPowerRiskPreference: loadFullPowerRiskPreference2 } = await Promise.resolve().then(() => (init_full_power_risk_mode(), full_power_risk_mode_exports));
43270
43554
  await Promise.all([
43271
- loadCocoModePreference(),
43555
+ loadQualityLoopPreference(),
43272
43556
  loadFullAccessPreference2(),
43273
43557
  loadFullPowerRiskPreference2()
43274
43558
  ]);
@@ -43530,13 +43814,13 @@ async function startRepl(options = {}) {
43530
43814
  };
43531
43815
  let originalSystemPrompt;
43532
43816
  try {
43533
- if (typeof agentMessage === "string" && !isCocoMode() && !wasHintShown() && looksLikeFeatureRequest(agentMessage)) {
43817
+ if (typeof agentMessage === "string" && !isQualityLoop() && !wasHintShown() && looksLikeFeatureRequest(agentMessage)) {
43534
43818
  markHintShown();
43535
- console.log(formatCocoHint());
43819
+ console.log(formatQualityLoopHint());
43536
43820
  }
43537
43821
  console.log();
43538
43822
  let cocoForkPrompt;
43539
- if (isCocoMode()) {
43823
+ if (isQualityLoop()) {
43540
43824
  const skillId = "coco-fix-iterate";
43541
43825
  const skillAvailable = session.skillRegistry?.has(skillId);
43542
43826
  if (skillAvailable && typeof agentMessage === "string") {
@@ -43550,7 +43834,7 @@ async function startRepl(options = {}) {
43550
43834
  }
43551
43835
  } else {
43552
43836
  originalSystemPrompt = session.config.agent.systemPrompt;
43553
- session.config.agent.systemPrompt = originalSystemPrompt + "\n" + getCocoModeSystemPrompt();
43837
+ session.config.agent.systemPrompt = originalSystemPrompt + "\n" + getQualityLoopSystemPrompt();
43554
43838
  }
43555
43839
  }
43556
43840
  const effectiveMessage = cocoForkPrompt ?? agentMessage;
@@ -43628,7 +43912,7 @@ async function startRepl(options = {}) {
43628
43912
  }
43629
43913
  renderToolStart(result2.name, result2.input);
43630
43914
  renderToolEnd(result2);
43631
- if (isCocoMode()) {
43915
+ if (isQualityLoop()) {
43632
43916
  setSpinner("Processing results & checking quality...");
43633
43917
  } else {
43634
43918
  setSpinner("Processing...");
@@ -43645,7 +43929,7 @@ async function startRepl(options = {}) {
43645
43929
  if (!thinkingStartTime) return;
43646
43930
  const elapsed = Math.floor((Date.now() - thinkingStartTime) / 1e3);
43647
43931
  if (elapsed < 4) return;
43648
- if (isCocoMode()) {
43932
+ if (isQualityLoop()) {
43649
43933
  if (elapsed < 8) setSpinner("Analyzing request...");
43650
43934
  else if (elapsed < 15) setSpinner("Running quality checks...");
43651
43935
  else if (elapsed < 25) setSpinner("Iterating for quality...");
@@ -43732,8 +44016,8 @@ async function startRepl(options = {}) {
43732
44016
  continue;
43733
44017
  }
43734
44018
  console.log();
43735
- if (isCocoMode() && result.content) {
43736
- const qualityResult = parseCocoQualityReport(result.content);
44019
+ if (isQualityLoop() && result.content) {
44020
+ const qualityResult = parseQualityLoopReport(result.content);
43737
44021
  if (qualityResult) {
43738
44022
  console.log(formatQualityResult(qualityResult));
43739
44023
  }
@@ -43854,7 +44138,7 @@ async function printWelcome(session, gitCtx, mcpManager) {
43854
44138
  if (gitCtx) {
43855
44139
  console.log(` ${formatGitLine(gitCtx)}`);
43856
44140
  }
43857
- const cocoStatus = isCocoMode() ? chalk25.magenta(" \u{1F504} quality mode: ") + chalk25.green.bold("on") + chalk25.dim(" \u2014 iterates until quality \u2265 85. /coco to disable") : chalk25.dim(" \u{1F4A1} /coco on \u2014 enable auto-test & quality iteration");
44141
+ const cocoStatus = isQualityLoop() ? chalk25.magenta(" \u{1F504} quality mode: ") + chalk25.green.bold("on") + chalk25.dim(" \u2014 iterates until quality \u2265 85. /quality to disable") : chalk25.dim(" \u{1F4A1} /quality on \u2014 enable auto-test & quality iteration");
43858
44142
  console.log(cocoStatus);
43859
44143
  const skillTotal = session.skillRegistry?.size ?? 0;
43860
44144
  const mcpServers = mcpManager?.getConnectedServers() ?? [];