@xcompiler/cli 0.2.3 → 0.2.4

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/acp/index.js CHANGED
@@ -55,19 +55,19 @@ var init_types = __esm({
55
55
  });
56
56
 
57
57
  // src/acp/server.ts
58
- import path30 from "path";
58
+ import path31 from "path";
59
59
  import { randomUUID as randomUUID2 } from "crypto";
60
60
 
61
61
  // src/version.ts
62
- var XCOMPILER_VERSION = "0.2.3";
62
+ var XCOMPILER_VERSION = "0.2.4";
63
63
  var XCOMPILER_PLUGIN_API_VERSION = 1;
64
64
 
65
65
  // src/runtime/commands.ts
66
- import path29 from "path";
67
- import { promises as fs26 } from "fs";
66
+ import path30 from "path";
67
+ import { promises as fs28 } from "fs";
68
68
 
69
69
  // src/core/project_file.ts
70
- import { promises as fs2 } from "fs";
70
+ import { promises as fs3 } from "fs";
71
71
  import path4 from "path";
72
72
  import { z as z3 } from "zod";
73
73
 
@@ -253,7 +253,7 @@ var PlanSchema = z.object({
253
253
  }));
254
254
 
255
255
  // src/core/storage.ts
256
- import { promises as fs } from "fs";
256
+ import { promises as fs2 } from "fs";
257
257
  import path3 from "path";
258
258
 
259
259
  // src/core/docs.ts
@@ -341,6 +341,9 @@ function testPlanDocForIteration(testPhase, iterationId = "P1") {
341
341
  return basename ? `docs/iterations/${iterationId}/tests/${basename}` : canonical;
342
342
  }
343
343
 
344
+ // src/core/language.ts
345
+ import { promises as fs } from "fs";
346
+
344
347
  // src/core/entry_gate.ts
345
348
  import path from "path";
346
349
 
@@ -442,6 +445,7 @@ var PYTHON_EXECUTOR_SYSTEM = `You are XCompiler's Step Executor. You may only in
442
445
  Every round you must return strict JSON:
443
446
  {
444
447
  "thoughts": "<one sentence describing this round's intent>",
448
+ "issueResolutionPlan": "<required only in DEBUG issue mode: concise root cause, repair target, and validation plan>",
445
449
  "actions": [ { "tool": "<tool name>", "args": { ... } }, ... ],
446
450
  "done": true | false
447
451
  }
@@ -526,6 +530,7 @@ var TYPESCRIPT_EXECUTOR_SYSTEM = `You are XCompiler's Step Executor. You may onl
526
530
  Every round you must return strict JSON:
527
531
  {
528
532
  "thoughts": "<one sentence describing this round's intent>",
533
+ "issueResolutionPlan": "<required only in DEBUG issue mode: concise root cause, repair target, and validation plan>",
529
534
  "actions": [ { "tool": "<tool name>", "args": { ... } }, ... ],
530
535
  "done": true | false
531
536
  }
@@ -549,7 +554,7 @@ Rules:
549
554
  7. package.json is the dependency manifest. Use add_dependency for npm packages; never write requirements.txt.
550
555
  8. run_program runs the project entry with \`npx tsx\`, run_tests runs Vitest via \`npm test\`, and the final delivery gate also verifies the direct Node entry command.`;
551
556
  var PLANNER_CLARIFY_SYSTEM = `You are the requirements analyst for XCompiler's V-model. Do not restate the topic; expose unresolved decisions that would change functional design, acceptance, or architecture boundaries.
552
- Return strict JSON only. Each question must be directly answerable by a product owner, cover one decision, and avoid vague catch-all or implementation-stack questions.`;
557
+ Return strict JSON only. Each question must be directly answerable by a product owner and cover one decision. Avoid vague catch-all or implementation-stack questions, except for the single development-language question when the user prompt explicitly requires it.`;
553
558
  function buildPlannerSystem(profile) {
554
559
  return (profile.id === "typescript" ? TYPESCRIPT_PLANNER_SYSTEM : PYTHON_PLANNER_SYSTEM) + profile.plannerPromptOverride;
555
560
  }
@@ -641,13 +646,13 @@ var messages = {
641
646
  preflightOllamaUnreachable: (baseUrl, message) => `preflight: ollama ${baseUrl} unreachable: ${message}`,
642
647
  preflightAutoAdded: (providers, roles) => `preflight: auto-added ${providers} provider(s) for roles [${roles}]`,
643
648
  scoreFileHeader: "# XCompiler LLM provider score snapshot (maintained automatically by ScoreStore; do not edit)",
644
- scoreFileSemantics: "# Scores: default 1.0; automatic range 0.1-1.0; providers tagged cluster default to 0.2-0.5 unless llm.cluster_score_min/max widens it; failure -0.5; success +0.1; only user-configured score=0 disables a provider."
649
+ scoreFileSemantics: "# Scores: dynamic snapshot; default 1.0; automatic range 0.1-1.0; providers tagged cluster default to 0.2-0.5 unless llm.cluster_score_min/max widens it; failure -0.5; success +0.1. Put user overrides in llm_scores_user.yaml; 0 disables a provider."
645
650
  },
646
651
  system: {
647
652
  configEnvMissing: (names) => `[xcompiler] unset config environment variables were replaced with empty strings: ${names}`,
648
653
  unhandledError: (message) => `Unhandled error: ${message}`,
649
654
  unsupportedPypiOnlyNetwork: "network=pypi-only is rejected because Docker cannot enforce a PyPI-only allowlist by itself. Use network=off for isolation or network=download-only for explicitly unrestricted outbound downloads.",
650
- dockerInsideContainerUnsupported: "XCompiler is running inside a container, so sandbox=docker is unsupported because Docker-outside-of-Docker can mis-map bind mounts and docker.sock permissions. Use agent.sandbox=subprocess, run XCompiler on the host, or set XC_IN_CONTAINER=0 only in a controlled environment.",
655
+ dockerInsideContainerUnsupported: "XCompiler is running inside a container, so sandbox mode docker is unsupported because Docker-outside-of-Docker can mis-map bind mounts and docker.sock permissions. Use agent.sandboxes.<language>.mode=subprocess, run XCompiler on the host, or set XC_IN_CONTAINER=0 only in a controlled environment.",
651
656
  firejailUnsupported: "sandbox=firejail is not implemented; use subprocess or docker.",
652
657
  smokeHeader: (baseUrl) => `Smoke test against ${baseUrl} (streaming)`,
653
658
  smokeOk: (model, totalMs, firstTokenMs, chunks, preview) => `[OK total=${totalMs}ms first-token=${firstTokenMs}ms chunks=${chunks}] ${model} -> ${preview}`,
@@ -664,8 +669,8 @@ var messages = {
664
669
  loaded: (plugin, version) => `Plugin ${plugin}@${version} loaded.`,
665
670
  extensionConflict: (plugin, kind, name) => `Plugin ${plugin} cannot replace existing ${kind} "${name}".`,
666
671
  hookFailed: (plugin, stage, message) => `Plugin ${plugin} failed during ${stage}: ${message}`,
667
- manifestReadFailed: (path31, message) => `Cannot read plugin manifest ${path31}: ${message}`,
668
- moduleLoadFailed: (plugin, path31, message) => `Cannot load plugin ${plugin} from ${path31}: ${message}`,
672
+ manifestReadFailed: (path32, message) => `Cannot read plugin manifest ${path32}: ${message}`,
673
+ moduleLoadFailed: (plugin, path32, message) => `Cannot load plugin ${plugin} from ${path32}: ${message}`,
669
674
  exportInvalid: (plugin, exportName) => `Plugin ${plugin} export ${exportName} is not a valid XCompiler plugin`,
670
675
  manifestMismatch: (plugin) => `Plugin ${plugin} runtime manifest does not match its preflight manifest`
671
676
  },
@@ -753,6 +758,7 @@ var messages = {
753
758
  optIntent: "plan intent: greenfield | feature | refactor | self",
754
759
  optBaselinePlan: "existing baseline phasePlan.json / plan.json path (default <workspace>/phasePlan.json)",
755
760
  optProjectFile: "XXX.xc project file path (default <workspace>/<name>.xc)",
761
+ optDebugWikiPath: "debug wiki root directory path (default <XCompiler path>/.xcompiler/debug-wiki)",
756
762
  argPlan: "phasePlan.json or legacy plan.json path (default = <workspace>/phasePlan.json)",
757
763
  argProjectFile: "XXX.xc project file",
758
764
  argStepId: "Step ID, e.g. S001",
@@ -899,7 +905,7 @@ var messages = {
899
905
  stepHeader: (id, phase, title, status, retries, maxRetries) => `${id} ${phase} ${title} ${status} retries=${retries}/${maxRetries}`,
900
906
  stepRoleTools: (role, tools) => `role=${role} tools=[${tools}]`,
901
907
  stepDependsOn: (ids) => `dependsOn: ${ids}`,
902
- outputStatus: (exists, p) => `${exists ? "\u2713" : "\u2717"} ${p}`,
908
+ outputStatus: (exists2, p) => `${exists2 ? "\u2713" : "\u2717"} ${p}`,
903
909
  auditEntry: (ts, kind, message) => `${ts} ${kind} ${message}`
904
910
  },
905
911
  execute: {
@@ -967,7 +973,7 @@ var messages = {
967
973
  deliveryGateReason: (command, exitCode, timedOut) => `FUNCTIONAL_TEST gate: \`${command}\` exit=${exitCode}${timedOut ? " (timeout)" : ""}`,
968
974
  missingPythonEntrypoint: "missing Python entrypoint: expected src/main.py, src/<package>/__main__.py, or an explicit CLI file such as src/cli.py",
969
975
  missingTypeScriptEntrypoint: "missing TypeScript entrypoint: expected package.json start/bin or one of src/main.ts, src/index.ts, src/main.tsx",
970
- invalidPythonEntrypointSource: (path31) => `invalid Python entrypoint source in ${path31}: expected a real CLI entry structure such as def main(...), argparse.ArgumentParser, or if __name__ == "__main__"; placeholder/import-only files are not runnable applications.`,
976
+ invalidPythonEntrypointSource: (path32) => `invalid Python entrypoint source in ${path32}: expected a real CLI entry structure such as def main(...), argparse.ArgumentParser, or if __name__ == "__main__"; placeholder/import-only files are not runnable applications.`,
971
977
  entrypointHelpOutputMissing: (command) => `entrypoint probe \`${command}\` exited 0 but produced no meaningful help/usage text; implement --help instead of relying on an empty script exit.`,
972
978
  reasonLine: (reason) => `reason: ${reason}`,
973
979
  roundsLine: (rounds) => `rounds: ${rounds}`,
@@ -1036,12 +1042,13 @@ Question mix (functionality first):
1036
1042
  - Label options sequentially from A through the last generated option, for example A-B, A-C, A-D, or A-E. A should be the recommended/default setting when one is apparent. Options must be concrete business/product settings, not vague placeholders.
1037
1043
  - Do not include \u201COther\u201D, \u201CCustom\u201D, or \u201CLet the user decide\u201D as an option. The CLI already allows the user to reply with one of the shown labels or enter a custom free-form answer.
1038
1044
  ${opts.projectShapeAmbiguous ? "- Required for this topic: ask the API library vs runnable application vs mixed-deliverable boundary explicitly.\n" : ""}
1045
+ ${opts.languageAmbiguous ? "- Required for this topic: include exactly one boundary question confirming the target development language. Options must be A. Python (default/recommended) and B. TypeScript / Node.js. The user may still answer with custom free-form text.\n" : ""}
1039
1046
 
1040
- [Hard constraint] The implementation stack is already fixed by XCompiler config / the existing project baseline. Do not reopen language/runtime/package-manager decisions.
1047
+ ${opts.languageAmbiguous ? `[Stack decision] XCompiler could not infer the target language from the topic or baseline. Ask only the development-language question described above; do not ask package manager, test framework, or OS questions. If the user does not choose, Python is the default.` : `[Hard constraint] The implementation stack is already fixed by the user's topic or existing project baseline. Do not reopen language/runtime/package-manager decisions.
1041
1048
  **Do NOT** ask questions of these forms:
1042
1049
  - "Which programming language / framework / runtime should this use?"
1043
1050
  - "Which test framework / build tool / package manager?"
1044
- - "Which OS is the target platform?"
1051
+ - "Which OS is the target platform?"`}
1045
1052
  ${opts.intent && opts.intent !== "greenfield" ? `This is an incremental ${opts.intent} request against an existing project${opts.hasBaseline ? " with a separate baseline summary that will be provided during decomposition" : ""}. Ask ONLY delta questions; do not ask to rebuild the project from scratch.` : ""}The majority of questions must concern functional behaviour; performance, boundaries, and extensibility should eliminate ambiguities that affect this delivery.`,
1046
1053
  plannerDecompose: (raw, qa, addenda, opts = {}) => `Original requirement:
1047
1054
  """
@@ -1143,7 +1150,7 @@ Return strict JSON StepPlan for the current phase only.`,
1143
1150
  executorDebugBlock: (reason, suggestions) => `
1144
1151
 
1145
1152
  You are now in DEBUG retry mode. Previous failure reason: ${reason}
1146
- DEBUG may edit upstream source files and tests within the current allowedWrites. If the failure reveals a real implementation, contract, or downstream integration mismatch, fix that real defect; do not pass by weakening assertions, skipping tests, deleting failing cases, or merely accommodating an incorrect test. If this rollback is in a design/requirements step and the concrete code change belongs to a later V-model step outside the current allowedWrites, update the current contract, test plan, or diagnostic artifact and finish this step so the later CODE step can implement it; do not attempt denied writes. If the failure is a missing third-party dependency or wrong library choice, use add_dependency with the real package name or change the source back to the real library selected by HIGH_LEVEL_DESIGN; never add try/except ImportError fake modules, fake classes/functions, empty implementations, or fallback mocks in production src/ code to bypass the error. Begin with read_file / code_search to localise the issue, then make the smallest possible fix via apply_patch / replace_in_file / add_dependency, and finally run_tests to verify. A DEBUG retry cannot be marked complete from read-only inspection alone: it must produce a successful repair action or a successful verification command in this retry. If the previous failure reason mentions repeated read-only/probe actions, use the existing failure log as sufficient context and make the next action a patch/write/dependency change or a verification command. When a test executes and fails an assertion about returned behaviour, do not repeatedly rewrite fixtures or samples. Only edit fixtures when the evidence is missing-file, malformed-fixture, or parse-error in the fixture itself; otherwise patch the implementation, interface contract, dependency choice, or test expectation that is actually wrong. If the failure log shows a network/API failure, do not stop at probing endpoints: use at most two consecutive http_fetch probes, reject 2xx responses with empty or unusable bodies, then patch the real integration and verify with run_program plus run_tests. Do not set done=true while the entrypoint still reports a network/API failure.` + (suggestions ? `
1153
+ When this retry is handling an issue, every JSON response must include issueResolutionPlan before or while fixing it. The plan must be concise and actionable: root cause hypothesis, files/contracts to change, validation command or gate, and what would disprove the plan. DEBUG may edit upstream source files and tests within the current allowedWrites. If the failure reveals a real implementation, contract, or downstream integration mismatch, fix that real defect; do not pass by weakening assertions, skipping tests, deleting failing cases, or merely accommodating an incorrect test. If this rollback is in a design/requirements step and the concrete code change belongs to a later V-model step outside the current allowedWrites, update the current contract, test plan, or diagnostic artifact and finish this step so the later CODE step can implement it; do not attempt denied writes. If the failure is a missing third-party dependency or wrong library choice, use add_dependency with the real package name or change the source back to the real library selected by HIGH_LEVEL_DESIGN; never add try/except ImportError fake modules, fake classes/functions, empty implementations, or fallback mocks in production src/ code to bypass the error. Begin with read_file / code_search to localise the issue, then make the smallest possible fix via apply_patch / replace_in_file / add_dependency, and finally run_tests to verify. A DEBUG retry cannot be marked complete from read-only inspection alone: it must produce a successful repair action or a successful verification command in this retry. If the previous failure reason mentions repeated read-only/probe actions, use the existing failure log as sufficient context and make the next action a patch/write/dependency change or a verification command. When a test executes and fails an assertion about returned behaviour, do not repeatedly rewrite fixtures or samples. Only edit fixtures when the evidence is missing-file, malformed-fixture, or parse-error in the fixture itself; otherwise patch the implementation, interface contract, dependency choice, or test expectation that is actually wrong. If the failure log shows a network/API failure, do not stop at probing endpoints: use at most two consecutive http_fetch probes, reject 2xx responses with empty or unusable bodies, then patch the real integration and verify with run_program plus run_tests. Do not set done=true while the entrypoint still reports a network/API failure.` + (suggestions ? `
1147
1154
 
1148
1155
  ${suggestions}` : ""),
1149
1156
  executorGlobalBlock: (globalPrompt) => `
@@ -1160,7 +1167,8 @@ ${sp}`,
1160
1167
  executorFeedbackVerifyMissing: (paths) => `outputs still missing: ${paths}. Please continue.`,
1161
1168
  executorFeedbackReadOnlyLoopWarning: (rounds, targets) => `Loop guard warning: the last ${rounds} round(s) used only read/probe tools` + (targets ? ` (${targets})` : "") + ". Next response must include a successful repair action (apply_patch / replace_in_file / write_file / add_dependency) or a concrete verification action (run_tests / run_program). Do not continue with only read_file, list_dir, code_search, or http_fetch.",
1162
1169
  executorFeedbackReadOnlyRecoveryRequired: "Read-only recovery mode is active because the previous attempt already failed from probing. The next response must use existing failure evidence to patch/write/change dependency or run verification; one more read-only-only response will fail this retry.",
1163
- executorFeedbackRepairEvidenceMissing: "Invalid DEBUG completion: this retry has not produced repair evidence yet. Before done=true, perform at least one successful repair action or successful verification run; otherwise stop only with a concrete blocker in thoughts."
1170
+ executorFeedbackRepairEvidenceMissing: "Invalid DEBUG completion: this retry has not produced repair evidence yet. Before done=true, perform at least one successful repair action or successful verification run; otherwise stop only with a concrete blocker in thoughts.",
1171
+ executorFeedbackIssueResolutionPlanMissing: "Invalid DEBUG issue completion: issueResolutionPlan is required before the issue can be resolved. Return JSON with a concise handling plan plus the needed repair or verification actions."
1164
1172
  },
1165
1173
  skills: {
1166
1174
  patcher: "Use apply_patch / replace_in_file for small in-place edits to existing files; never overwrite a whole file.",
@@ -1181,7 +1189,7 @@ ${sp}`,
1181
1189
  summaryOk: "all checks passed.",
1182
1190
  summaryWarn: (n) => `passed with ${n} warning(s).`,
1183
1191
  summaryFail: (n) => `${n} failure(s) detected.`,
1184
- configLoadOk: (path31) => `config loaded: ${path31}`,
1192
+ configLoadOk: (path32) => `config loaded: ${path32}`,
1185
1193
  configLoadFail: (msg) => `failed to load config: ${msg}`,
1186
1194
  configLocale: (locale) => `locale=${locale}`,
1187
1195
  llmNoProviders: "no LLM providers defined in config.llm.providers",
@@ -1199,7 +1207,7 @@ ${sp}`,
1199
1207
  roleOk: (role, provider) => `role "${role}" \u2192 ${provider}`,
1200
1208
  sandboxKind: (kind) => `sandbox=${kind}`,
1201
1209
  sandboxNetworkPolicy: (policy, ports) => `network=${policy}` + (ports.length ? ` (expose_ports=[${ports.join(", ")}])` : ""),
1202
- sandboxFullNoPorts: "network=full but no expose_ports configured \u2014 host-side cannot reach container services. Add `agent.sandbox_limits.expose_ports: [<port>]` in config.yaml.",
1210
+ sandboxFullNoPorts: "network=full but no expose_ports configured \u2014 host-side cannot reach container services. Add `agent.sandboxes.<language>.<local|docker>.limits.expose_ports: [<port>]` in config.yaml.",
1203
1211
  sandboxNodeMissing: "node not found on PATH (required by TypeScript subprocess sandbox)",
1204
1212
  sandboxNodeOk: (version) => `node OK (${version})`,
1205
1213
  sandboxNpmMissing: "npm not found on PATH (required by TypeScript subprocess sandbox)",
@@ -1318,6 +1326,7 @@ var PYTHON_EXECUTOR_SYSTEM2 = `\u4F60\u662F XCompiler \u7684 Step Executor\u3002
1318
1326
  \u6BCF\u4E00\u8F6E\u4F60\u5FC5\u987B\u8FD4\u56DE\u4E25\u683C JSON\uFF1A
1319
1327
  {
1320
1328
  "thoughts": "<\u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u672C\u8F6E\u610F\u56FE>",
1329
+ "issueResolutionPlan": "<\u4EC5 DEBUG issue \u6A21\u5F0F\u5FC5\u586B\uFF1A\u7B80\u660E\u8BF4\u660E\u6839\u56E0\u5047\u8BBE\u3001\u4FEE\u590D\u76EE\u6807\u548C\u9A8C\u8BC1\u65B9\u6848>",
1321
1330
  "actions": [ { "tool": "<\u5DE5\u5177\u540D>", "args": { ... } }, ... ],
1322
1331
  "done": true | false
1323
1332
  }
@@ -1401,6 +1410,7 @@ var TYPESCRIPT_EXECUTOR_SYSTEM2 = `\u4F60\u662F XCompiler \u7684 Step Executor\u
1401
1410
  \u6BCF\u4E00\u8F6E\u4F60\u5FC5\u987B\u8FD4\u56DE\u4E25\u683C JSON\uFF1A
1402
1411
  {
1403
1412
  "thoughts": "<\u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u672C\u8F6E\u610F\u56FE>",
1413
+ "issueResolutionPlan": "<\u4EC5 DEBUG issue \u6A21\u5F0F\u5FC5\u586B\uFF1A\u7B80\u660E\u8BF4\u660E\u6839\u56E0\u5047\u8BBE\u3001\u4FEE\u590D\u76EE\u6807\u548C\u9A8C\u8BC1\u65B9\u6848>",
1404
1414
  "actions": [ { "tool": "<\u5DE5\u5177\u540D>", "args": { ... } }, ... ],
1405
1415
  "done": true | false
1406
1416
  }
@@ -1514,13 +1524,13 @@ var messages2 = {
1514
1524
  preflightOllamaUnreachable: (baseUrl, message) => `\u9884\u68C0\uFF1AOllama ${baseUrl} \u4E0D\u53EF\u8FBE\uFF1A${message}`,
1515
1525
  preflightAutoAdded: (providers, roles) => `\u9884\u68C0\uFF1A\u81EA\u52A8\u589E\u52A0 ${providers} \u4E2A provider\uFF0C\u8986\u76D6\u89D2\u8272 [${roles}]`,
1516
1526
  scoreFileHeader: "# XCompiler LLM provider \u8BC4\u5206\u5FEB\u7167\uFF08\u7531 ScoreStore \u81EA\u52A8\u7EF4\u62A4\uFF0C\u8BF7\u52FF\u624B\u5DE5\u7F16\u8F91\uFF09",
1517
- scoreFileSemantics: "# \u8BC4\u5206\u8BED\u4E49\uFF1A\u9ED8\u8BA4 1.0\uFF1B\u81EA\u52A8\u8BC4\u5206\u8303\u56F4 0.1\uFF5E1.0\uFF1Btags: [cluster] \u7684 provider \u9ED8\u8BA4 0.2\uFF5E0.5\uFF0C\u9664\u975E llm.cluster_score_min/max \u6269\u5BBD\uFF1B\u5931\u8D25 -0.5\uFF1B\u6210\u529F +0.1\uFF1B\u53EA\u6709\u7528\u6237\u914D\u7F6E score=0 \u8868\u793A\u7981\u7528\u3002"
1527
+ scoreFileSemantics: "# \u8BC4\u5206\u8BED\u4E49\uFF1A\u8FD9\u662F\u52A8\u6001\u8BC4\u5206\u5FEB\u7167\uFF1B\u9ED8\u8BA4 1.0\uFF1B\u81EA\u52A8\u8BC4\u5206\u8303\u56F4 0.1\uFF5E1.0\uFF1Btags: [cluster] \u7684 provider \u9ED8\u8BA4 0.2\uFF5E0.5\uFF0C\u9664\u975E llm.cluster_score_min/max \u6269\u5BBD\uFF1B\u5931\u8D25 -0.5\uFF1B\u6210\u529F +0.1\u3002\u7528\u6237\u8986\u76D6\u8BF7\u5199 llm_scores_user.yaml\uFF0C0 \u8868\u793A\u7981\u7528\u3002"
1518
1528
  },
1519
1529
  system: {
1520
1530
  configEnvMissing: (names) => `[xcompiler] \u914D\u7F6E\u4E2D\u7684\u73AF\u5883\u53D8\u91CF\u672A\u8BBE\u7F6E\uFF0C\u5DF2\u66FF\u6362\u4E3A\u7A7A\u5B57\u7B26\u4E32\uFF1A${names}`,
1521
1531
  unhandledError: (message) => `\u672A\u5904\u7406\u9519\u8BEF\uFF1A${message}`,
1522
1532
  unsupportedPypiOnlyNetwork: "\u62D2\u7EDD network=pypi-only\uFF1ADocker \u672C\u8EAB\u65E0\u6CD5\u53EF\u9760\u6267\u884C\u201C\u4EC5 PyPI\u201D\u57DF\u540D\u767D\u540D\u5355\u3002\u9700\u8981\u9694\u79BB\u8BF7\u4F7F\u7528 network=off\uFF1B\u660E\u786E\u5141\u8BB8\u4EFB\u610F\u51FA\u7AD9\u4E0B\u8F7D\u65F6\u4F7F\u7528 network=download-only\u3002",
1523
- dockerInsideContainerUnsupported: "\u68C0\u6D4B\u5230 XCompiler \u8FD0\u884C\u5728\u5BB9\u5668\u5185\uFF0Csandbox=docker \u53EF\u80FD\u5BFC\u81F4 bind-mount \u8DEF\u5F84\u53CA docker.sock \u6743\u9650\u9519\u4F4D\uFF0C\u56E0\u6B64\u4E0D\u53D7\u652F\u6301\u3002\u8BF7\u4F7F\u7528 agent.sandbox=subprocess\u3001\u6539\u5728\u5BBF\u4E3B\u673A\u8FD0\u884C\uFF0C\u6216\u4EC5\u5728\u53D7\u63A7\u73AF\u5883\u8BBE\u7F6E XC_IN_CONTAINER=0\u3002",
1533
+ dockerInsideContainerUnsupported: "\u68C0\u6D4B\u5230 XCompiler \u8FD0\u884C\u5728\u5BB9\u5668\u5185\uFF0Csandbox mode=docker \u53EF\u80FD\u5BFC\u81F4 bind-mount \u8DEF\u5F84\u53CA docker.sock \u6743\u9650\u9519\u4F4D\uFF0C\u56E0\u6B64\u4E0D\u53D7\u652F\u6301\u3002\u8BF7\u4F7F\u7528 agent.sandboxes.<language>.mode=subprocess\u3001\u6539\u5728\u5BBF\u4E3B\u673A\u8FD0\u884C\uFF0C\u6216\u4EC5\u5728\u53D7\u63A7\u73AF\u5883\u8BBE\u7F6E XC_IN_CONTAINER=0\u3002",
1524
1534
  firejailUnsupported: "\u5C1A\u672A\u5B9E\u73B0 sandbox=firejail\uFF0C\u8BF7\u4F7F\u7528 subprocess \u6216 docker\u3002",
1525
1535
  smokeHeader: (baseUrl) => `\u6B63\u5728\u5BF9 ${baseUrl} \u6267\u884C\u6D41\u5F0F\u5192\u70DF\u6D4B\u8BD5`,
1526
1536
  smokeOk: (model, totalMs, firstTokenMs, chunks, preview) => `[\u6210\u529F \u603B\u8017\u65F6=${totalMs}ms \u9996Token=${firstTokenMs}ms \u5206\u5757=${chunks}] ${model} -> ${preview}`,
@@ -1537,8 +1547,8 @@ var messages2 = {
1537
1547
  loaded: (plugin, version) => `\u63D2\u4EF6 ${plugin}@${version} \u5DF2\u52A0\u8F7D\u3002`,
1538
1548
  extensionConflict: (plugin, kind, name) => `\u63D2\u4EF6 ${plugin} \u4E0D\u80FD\u8986\u76D6\u5DF2\u6709 ${kind} \u201C${name}\u201D\u3002`,
1539
1549
  hookFailed: (plugin, stage, message) => `\u63D2\u4EF6 ${plugin} \u5728 ${stage} \u9636\u6BB5\u6267\u884C\u5931\u8D25\uFF1A${message}`,
1540
- manifestReadFailed: (path31, message) => `\u65E0\u6CD5\u8BFB\u53D6\u63D2\u4EF6\u6E05\u5355 ${path31}\uFF1A${message}`,
1541
- moduleLoadFailed: (plugin, path31, message) => `\u65E0\u6CD5\u4ECE ${path31} \u52A0\u8F7D\u63D2\u4EF6 ${plugin}\uFF1A${message}`,
1550
+ manifestReadFailed: (path32, message) => `\u65E0\u6CD5\u8BFB\u53D6\u63D2\u4EF6\u6E05\u5355 ${path32}\uFF1A${message}`,
1551
+ moduleLoadFailed: (plugin, path32, message) => `\u65E0\u6CD5\u4ECE ${path32} \u52A0\u8F7D\u63D2\u4EF6 ${plugin}\uFF1A${message}`,
1542
1552
  exportInvalid: (plugin, exportName) => `\u63D2\u4EF6 ${plugin} \u7684\u5BFC\u51FA ${exportName} \u4E0D\u662F\u6709\u6548 XCompiler \u63D2\u4EF6`,
1543
1553
  manifestMismatch: (plugin) => `\u63D2\u4EF6 ${plugin} \u7684\u8FD0\u884C\u65F6\u6E05\u5355\u4E0E\u9884\u68C0\u6E05\u5355\u4E0D\u4E00\u81F4`
1544
1554
  },
@@ -1626,6 +1636,7 @@ var messages2 = {
1626
1636
  optIntent: "\u8BA1\u5212\u610F\u56FE\uFF1Agreenfield | feature | refactor | self",
1627
1637
  optBaselinePlan: "\u5DF2\u6709\u57FA\u7EBF phasePlan.json / plan.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <workspace>/phasePlan.json\uFF09",
1628
1638
  optProjectFile: "XXX.xc \u5DE5\u7A0B\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4 <workspace>/<name>.xc\uFF09",
1639
+ optDebugWikiPath: "debug wiki \u6839\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4 <XCompiler path>/.xcompiler/debug-wiki\uFF09",
1629
1640
  argPlan: "phasePlan.json \u6216\u5386\u53F2 plan.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 = <workspace>/phasePlan.json\uFF09",
1630
1641
  argProjectFile: "XXX.xc \u5DE5\u7A0B\u6587\u4EF6",
1631
1642
  argStepId: "Step ID\uFF0C\u5982 S001",
@@ -1772,7 +1783,7 @@ var messages2 = {
1772
1783
  stepHeader: (id, phase, title, status, retries, maxRetries) => `${id} ${phase} ${title} ${status} \u91CD\u8BD5=${retries}/${maxRetries}`,
1773
1784
  stepRoleTools: (role, tools) => `\u89D2\u8272=${role} \u5DE5\u5177=[${tools}]`,
1774
1785
  stepDependsOn: (ids) => `\u4F9D\u8D56\uFF1A${ids}`,
1775
- outputStatus: (exists, p) => `${exists ? "\u2713" : "\u2717"} ${p}`,
1786
+ outputStatus: (exists2, p) => `${exists2 ? "\u2713" : "\u2717"} ${p}`,
1776
1787
  auditEntry: (ts, kind, message) => `${ts} ${kind} ${message}`
1777
1788
  },
1778
1789
  execute: {
@@ -1840,7 +1851,7 @@ var messages2 = {
1840
1851
  deliveryGateReason: (command, exitCode, timedOut) => `FUNCTIONAL_TEST \u95E8\u7981\uFF1A\`${command}\` \u9000\u51FA\u7801=${exitCode}${timedOut ? "\uFF08\u8D85\u65F6\uFF09" : ""}`,
1841
1852
  missingPythonEntrypoint: "\u7F3A\u5C11 Python \u5165\u53E3\uFF1A\u9700\u8981 src/main.py\u3001src/<package>/__main__.py\uFF0C\u6216 src/cli.py \u7B49\u663E\u5F0F CLI \u6587\u4EF6",
1842
1853
  missingTypeScriptEntrypoint: "\u7F3A\u5C11 TypeScript \u5165\u53E3\uFF1A\u9700\u8981 package.json start/bin \u6216 src/main.ts\u3001src/index.ts\u3001src/main.tsx \u4E4B\u4E00",
1843
- invalidPythonEntrypointSource: (path31) => `Python \u5165\u53E3\u6E90\u7801\u65E0\u6548\uFF1A${path31} \u5FC5\u987B\u662F\u771F\u5B9E CLI \u5165\u53E3\uFF0C\u81F3\u5C11\u5305\u542B def main(...)\u3001argparse.ArgumentParser \u6216 if __name__ == "__main__" \u8FD9\u7C7B\u5165\u53E3\u7ED3\u6784\uFF1B\u4EC5 import/comment \u7684\u5360\u4F4D\u6587\u4EF6\u4E0D\u80FD\u7B97\u53EF\u8FD0\u884C\u5E94\u7528\u3002`,
1854
+ invalidPythonEntrypointSource: (path32) => `Python \u5165\u53E3\u6E90\u7801\u65E0\u6548\uFF1A${path32} \u5FC5\u987B\u662F\u771F\u5B9E CLI \u5165\u53E3\uFF0C\u81F3\u5C11\u5305\u542B def main(...)\u3001argparse.ArgumentParser \u6216 if __name__ == "__main__" \u8FD9\u7C7B\u5165\u53E3\u7ED3\u6784\uFF1B\u4EC5 import/comment \u7684\u5360\u4F4D\u6587\u4EF6\u4E0D\u80FD\u7B97\u53EF\u8FD0\u884C\u5E94\u7528\u3002`,
1844
1855
  entrypointHelpOutputMissing: (command) => `\u5165\u53E3\u63A2\u6D4B \`${command}\` \u867D\u7136\u9000\u51FA\u7801\u4E3A 0\uFF0C\u4F46\u6CA1\u6709\u8F93\u51FA\u6709\u610F\u4E49\u7684 help/usage \u6587\u672C\uFF1B\u5FC5\u987B\u5B9E\u73B0 --help\uFF0C\u4E0D\u80FD\u9760\u7A7A\u811A\u672C\u81EA\u7136\u9000\u51FA\u8FC7\u5173\u3002`,
1845
1856
  reasonLine: (reason) => `\u539F\u56E0\uFF1A${reason}`,
1846
1857
  roundsLine: (rounds) => `\u8F6E\u6B21\uFF1A${rounds}`,
@@ -1885,7 +1896,7 @@ var messages2 = {
1885
1896
  - \u6BCF\u4E2A CODE/\u6D4B\u8BD5\u4EA7\u7269\u5FC5\u987B\u4E25\u683C\u9650\u5B9A\u5728\u672C\u6B21\u589E\u91CF\u8303\u56F4\uFF0C\u7981\u6B62\u6574\u4F53\u91CD\u5EFA\u6216\u66FF\u6362\u4ED3\u5E93\u3002
1886
1897
  - \u5C06\u7A33\u5B9A\u5BBF\u4E3B\u89C6\u4E3A N \u4EE3\u3001\u9694\u79BB worktree \u4E2D\u7684\u5019\u9009\u7248\u672C\u89C6\u4E3A N+1 \u4EE3\uFF1B\u7981\u6B62\u8BBE\u8BA1\u8FDB\u7A0B\u5185\u70ED\u66FF\u6362\u3002`,
1887
1898
  plannerClarifySystem: `\u4F60\u662F XCompiler V \u6A21\u578B\u7684\u9700\u6C42\u5206\u6790\u5E08\u3002\u4F60\u7684\u804C\u8D23\u4E0D\u662F\u590D\u8FF0 topic\uFF0C\u800C\u662F\u53D1\u73B0\u4F1A\u6539\u53D8\u529F\u80FD\u8BBE\u8BA1\u3001\u9A8C\u6536\u7ED3\u679C\u6216\u67B6\u6784\u8FB9\u754C\u7684\u672A\u51B3\u4E8B\u9879\u3002
1888
- \u53EA\u8FD4\u56DE\u4E25\u683C JSON\u3002\u95EE\u9898\u5FC5\u987B\u53EF\u7531\u4E1A\u52A1\u65B9\u76F4\u63A5\u4F5C\u7B54\u3001\u4E00\u6B21\u53EA\u786E\u8BA4\u4E00\u4E2A\u51B3\u7B56\uFF0C\u907F\u514D\u7A7A\u6CDB\u7684\u201C\u8FD8\u6709\u4EC0\u4E48\u8981\u6C42\u201D\u6216\u6280\u672F\u6808\u9009\u578B\u95EE\u9898\u3002`,
1899
+ \u53EA\u8FD4\u56DE\u4E25\u683C JSON\u3002\u95EE\u9898\u5FC5\u987B\u53EF\u7531\u4E1A\u52A1\u65B9\u76F4\u63A5\u4F5C\u7B54\u3001\u4E00\u6B21\u53EA\u786E\u8BA4\u4E00\u4E2A\u51B3\u7B56\uFF0C\u907F\u514D\u7A7A\u6CDB\u7684\u201C\u8FD8\u6709\u4EC0\u4E48\u8981\u6C42\u201D\u6216\u6280\u672F\u6808\u9009\u578B\u95EE\u9898\uFF1B\u53EA\u6709\u7528\u6237\u63D0\u793A\u660E\u786E\u8981\u6C42\u65F6\uFF0C\u624D\u5141\u8BB8\u63D0\u51FA\u552F\u4E00\u4E00\u6761\u5F00\u53D1\u8BED\u8A00\u786E\u8BA4\u95EE\u9898\u3002`,
1889
1900
  plannerClarify: (raw, opts = {}) => `\u7528\u6237\u7684\u539F\u59CB\u9700\u6C42\u5982\u4E0B\uFF1A
1890
1901
 
1891
1902
  """
@@ -1910,12 +1921,13 @@ ${raw}
1910
1921
  - \u9009\u9879\u4ECE A \u5F00\u59CB\u8FDE\u7EED\u6807\u53F7\uFF0C\u5230\u5B9E\u9645\u6700\u540E\u4E00\u4E2A\u9009\u9879\u7ED3\u675F\uFF0C\u4F8B\u5982 A-B\u3001A-C\u3001A-D \u6216 A-E\uFF1B\u5982\u679C\u80FD\u5224\u65AD\u63A8\u8350/\u9ED8\u8BA4\u65B9\u6848\uFF0CA \u5E94\u662F\u6700\u9AD8\u4F18\u5148\u7EA7\u65B9\u6848\u3002\u9009\u9879\u5FC5\u987B\u662F\u5177\u4F53\u4E1A\u52A1/\u4EA7\u54C1\u8BBE\u5B9A\uFF0C\u4E0D\u8981\u5199\u6210\u7A7A\u6CDB\u5360\u4F4D\u3002
1911
1922
  - \u4E0D\u8981\u628A\u201C\u5176\u4ED6 / \u81EA\u5B9A\u4E49 / \u7528\u6237\u51B3\u5B9A\u201D\u4F5C\u4E3A\u9009\u9879\uFF1BCLI \u5DF2\u652F\u6301\u7528\u6237\u8F93\u5165\u5DF2\u5C55\u793A\u7684\u9009\u9879\u5B57\u6BCD\u6216\u76F4\u63A5\u8F93\u5165\u81EA\u5B9A\u4E49\u56DE\u7B54\u5185\u5BB9\u3002
1912
1923
  ${opts.projectShapeAmbiguous ? "- \u672C topic \u5FC5\u95EE\uFF1A\u660E\u786E\u786E\u8BA4 API library / \u53EF\u8FD0\u884C\u5E94\u7528 / mixed \u4EA4\u4ED8\u8FB9\u754C\u3002\n" : ""}
1924
+ ${opts.languageAmbiguous ? "- \u672C topic \u5FC5\u95EE\uFF1A\u5FC5\u987B\u5305\u542B\u4E14\u53EA\u5305\u542B 1 \u4E2A boundary \u95EE\u9898\u786E\u8BA4\u76EE\u6807\u5F00\u53D1\u8BED\u8A00\u3002\u9009\u9879\u5FC5\u987B\u662F A. Python\uFF08\u9ED8\u8BA4/\u63A8\u8350\uFF09 \u548C B. TypeScript / Node.js\uFF1B\u7528\u6237\u4ECD\u53EF\u8F93\u5165\u81EA\u5B9A\u4E49\u56DE\u7B54\u3002\n" : ""}
1913
1925
 
1914
- \u3010\u786C\u7EA6\u675F\u3011\u5B9E\u73B0\u6280\u672F\u6808\u5DF2\u7ECF\u7531 XCompiler \u914D\u7F6E / \u73B0\u6709\u5DE5\u7A0B\u57FA\u7EBF\u56FA\u5B9A\uFF0C\u4E0D\u8981\u91CD\u65B0\u8BE2\u95EE\u8BED\u8A00\u3001\u8FD0\u884C\u65F6\u3001\u5305\u7BA1\u7406\u5668\u8FD9\u7C7B\u95EE\u9898\u3002
1926
+ ${opts.languageAmbiguous ? `\u3010\u6280\u672F\u6808\u786E\u8BA4\u3011XCompiler \u65E0\u6CD5\u4ECE topic \u6216\u57FA\u7EBF\u4E2D\u5224\u65AD\u76EE\u6807\u5F00\u53D1\u8BED\u8A00\u3002\u53EA\u8BE2\u95EE\u4E0A\u9762\u8981\u6C42\u7684\u5F00\u53D1\u8BED\u8A00\u95EE\u9898\uFF1B\u4E0D\u8981\u8BE2\u95EE\u5305\u7BA1\u7406\u5668\u3001\u6D4B\u8BD5\u6846\u67B6\u6216\u64CD\u4F5C\u7CFB\u7EDF\u3002\u82E5\u7528\u6237\u4E0D\u9009\u62E9\uFF0C\u9ED8\u8BA4 Python\u3002` : `\u3010\u786C\u7EA6\u675F\u3011\u5B9E\u73B0\u6280\u672F\u6808\u5DF2\u7ECF\u7531\u7528\u6237 topic / \u73B0\u6709\u5DE5\u7A0B\u57FA\u7EBF\u56FA\u5B9A\uFF0C\u4E0D\u8981\u91CD\u65B0\u8BE2\u95EE\u8BED\u8A00\u3001\u8FD0\u884C\u65F6\u3001\u5305\u7BA1\u7406\u5668\u8FD9\u7C7B\u95EE\u9898\u3002
1915
1927
  **\u4E25\u7981**\u63D0\u51FA\u4EE5\u4E0B\u7C7B\u578B\u7684\u95EE\u9898\uFF1A
1916
1928
  - "\u5E0C\u671B\u7528\u4EC0\u4E48\u7F16\u7A0B\u8BED\u8A00 / \u6846\u67B6 / \u8FD0\u884C\u65F6\u5B9E\u73B0\uFF1F"
1917
1929
  - "\u9700\u8981\u54EA\u79CD\u6D4B\u8BD5\u6846\u67B6 / \u6784\u5EFA\u5DE5\u5177 / \u5305\u7BA1\u7406\u5668\uFF1F"
1918
- - "\u76EE\u6807\u5E73\u53F0\u662F\u54EA\u79CD\u64CD\u4F5C\u7CFB\u7EDF\uFF1F"
1930
+ - "\u76EE\u6807\u5E73\u53F0\u662F\u54EA\u79CD\u64CD\u4F5C\u7CFB\u7EDF\uFF1F"`}
1919
1931
  ${opts.intent && opts.intent !== "greenfield" ? `\u8FD9\u662F\u4E00\u6761\u9488\u5BF9\u73B0\u6709\u5DE5\u7A0B\u7684\u589E\u91CF ${opts.intent} \u8BF7\u6C42${opts.hasBaseline ? "\uFF1B\u5206\u89E3\u9636\u6BB5\u8FD8\u4F1A\u63D0\u4F9B\u4E00\u4EFD\u57FA\u7EBF\u6458\u8981" : ""}\u3002\u8BF7\u53EA\u95EE\u201C\u53D8\u66F4\u589E\u91CF\u201D\u76F8\u5173\u95EE\u9898\uFF0C\u4E0D\u8981\u628A\u9879\u76EE\u5F53\u6210\u4ECE\u96F6\u5F00\u59CB\u91CD\u505A\u3002` : ""}\u95EE\u9898\u4E3B\u4F53\u5FC5\u987B\u805A\u7126\u529F\u80FD\u884C\u4E3A\uFF1B\u6027\u80FD\u3001\u8FB9\u754C\u548C\u6269\u5C55\u6027\u7528\u4E8E\u6D88\u9664\u4F1A\u5F71\u54CD\u672C\u671F\u8BBE\u8BA1\u7684\u5173\u952E\u6B67\u4E49\u3002`,
1920
1932
  plannerDecompose: (raw, qa, addenda, opts = {}) => `\u539F\u59CB\u9700\u6C42\uFF1A
1921
1933
  """
@@ -2017,7 +2029,7 @@ ${opts.phasePlan}
2017
2029
  executorDebugBlock: (reason, suggestions) => `
2018
2030
 
2019
2031
  \u6B63\u5904\u4E8E DEBUG \u91CD\u8BD5\u6A21\u5F0F\u3002\u4E0A\u4E00\u8F6E\u5931\u8D25\u539F\u56E0: ${reason}
2020
- DEBUG \u53EF\u4EE5\u4FEE\u6539\u5F53\u524D allowedWrites \u5185\u7684\u4E0A\u6E38\u6E90\u7801\u4E0E\u6D4B\u8BD5\u6587\u4EF6\uFF1B\u82E5\u5931\u8D25\u66B4\u9732\u7684\u662F\u5B9E\u73B0\u3001\u5951\u7EA6\u6216\u4E0B\u6E38\u8C03\u7528\u4E0D\u4E00\u81F4\uFF0C\u5FC5\u987B\u4FEE\u771F\u5B9E\u7F3A\u9677\uFF0C\u7981\u6B62\u901A\u8FC7\u524A\u5F31\u65AD\u8A00\u3001\u8DF3\u8FC7\u6D4B\u8BD5\u3001\u5220\u9664\u5931\u8D25\u7528\u4F8B\u6216\u53EA\u8FCE\u5408\u9519\u8BEF\u6D4B\u8BD5\u6765\u8FC7\u5173\u3002\u5982\u679C\u672C\u6B21\u56DE\u9000\u5904\u4E8E\u9700\u6C42/\u8BBE\u8BA1\u9636\u6BB5\uFF0C\u800C\u5177\u4F53\u6E90\u7801\u4FEE\u6539\u5C5E\u4E8E\u540E\u7EED V \u6A21\u578B Step \u4E14\u76EE\u6807\u6587\u4EF6\u4E0D\u5728\u5F53\u524D allowedWrites \u5185\uFF0C\u5E94\u66F4\u65B0\u5F53\u524D\u5951\u7EA6\u3001\u6D4B\u8BD5\u8BA1\u5212\u6216\u8BCA\u65AD\u4EA7\u7269\u5E76\u5B8C\u6210\u5F53\u524D Step\uFF0C\u8BA9\u540E\u7EED CODE Step \u5B9E\u73B0\uFF1B\u4E0D\u8981\u5C1D\u8BD5\u88AB\u62D2\u7EDD\u7684\u8D8A\u6743\u5199\u5165\u3002\u82E5\u5931\u8D25\u662F\u7B2C\u4E09\u65B9\u4F9D\u8D56\u7F3A\u5931\u6216\u5E93\u9009\u578B\u9519\u8BEF\uFF0C\u5FC5\u987B\u7528 add_dependency \u5199\u5165\u771F\u5B9E\u5305\u540D\uFF0C\u6216\u628A\u6E90\u7801\u6539\u56DE HIGH_LEVEL_DESIGN \u9009\u5B9A\u7684\u771F\u5B9E\u5E93\uFF1B\u4E25\u7981\u5728 src/ \u751F\u4EA7\u4EE3\u7801\u91CC try/except ImportError \u540E\u4F2A\u9020 module\u3001fake class/function\u3001\u7A7A\u5B9E\u73B0\u6216 fallback mock \u6765\u7ED5\u8FC7\u9519\u8BEF\u3002\u8BF7\u5305\u542B read_file/code_search \u5148\u5B9A\u4F4D\u95EE\u9898\uFF0C\u518D\u4EE5 apply_patch / replace_in_file / add_dependency \u4F5C\u6700\u5C0F\u4FEE\u6539\uFF0C\u6700\u540E run_tests \u9A8C\u8BC1\u3002DEBUG \u91CD\u8BD5\u4E0D\u80FD\u53EA\u9760\u53EA\u8BFB\u68C0\u67E5\u5C31\u6807\u8BB0\u5B8C\u6210\uFF1A\u672C\u6B21\u91CD\u8BD5\u5FC5\u987B\u4EA7\u751F\u4E00\u6B21\u6210\u529F\u7684\u4FEE\u590D\u52A8\u4F5C\uFF0C\u6216\u4E00\u6B21\u6210\u529F\u7684\u9A8C\u8BC1\u547D\u4EE4\u3002\u5982\u679C\u4E0A\u4E00\u8F6E\u5931\u8D25\u539F\u56E0\u5305\u542B repeated read-only/probe actions\uFF0C\u8BF7\u628A\u5DF2\u6709 failure log \u89C6\u4E3A\u8DB3\u591F\u4E0A\u4E0B\u6587\uFF0C\u4E0B\u4E00\u6B65\u76F4\u63A5 patch/write/\u6539\u4F9D\u8D56\u6216\u6267\u884C\u9A8C\u8BC1\u547D\u4EE4\u3002\u5F53\u6D4B\u8BD5\u5DF2\u6B63\u5E38\u6267\u884C\u4F46\u5931\u8D25\u70B9\u662F\u8FD4\u56DE\u884C\u4E3A\u65AD\u8A00\u65F6\uFF0C\u7981\u6B62\u53CD\u590D\u91CD\u5199 fixture \u6216\u6837\u4F8B\u3002\u53EA\u6709\u8BC1\u636E\u660E\u786E\u662F\u6587\u4EF6\u7F3A\u5931\u3001fixture \u683C\u5F0F\u9519\u8BEF\u6216 fixture \u672C\u8EAB\u89E3\u6790\u5931\u8D25\u65F6\u624D\u4FEE\u6539 fixture\uFF1B\u5426\u5219\u5E94\u4FEE\u5B9E\u73B0\u3001\u63A5\u53E3\u5951\u7EA6\u3001\u4F9D\u8D56\u9009\u578B\u6216\u786E\u5B9E\u9519\u8BEF\u7684\u65AD\u8A00\u3002\u5982\u679C\u5931\u8D25\u65E5\u5FD7\u663E\u793A\u7F51\u7EDC/API \u8C03\u7528\u5931\u8D25\uFF0C\u4E0D\u5141\u8BB8\u53EA\u505C\u7559\u5728\u63A2\u6D4B\u63A5\u53E3\uFF1A\u6700\u591A\u8FDE\u7EED\u6267\u884C 2 \u6B21 http_fetch \u63A2\u6D4B\uFF1BHTTP 2xx \u4F46 body \u4E3A\u7A7A\u6216\u683C\u5F0F\u4E0D\u53EF\u7528\u4E0D\u7B97\u53EF\u7528\u63A5\u53E3\uFF1B\u968F\u540E\u5FC5\u987B patch \u771F\u5B9E\u96C6\u6210\u4EE3\u7801\uFF0C\u5E76\u7528 run_program \u548C run_tests \u9A8C\u8BC1\u3002\u5165\u53E3\u4ECD\u8F93\u51FA\u7F51\u7EDC/API \u5931\u8D25\u65F6\u4E0D\u5F97 done=true\u3002` + (suggestions ? `
2032
+ \u5F53\u672C\u6B21 DEBUG \u6B63\u5728\u5904\u7406 issue \u65F6\uFF0C\u6BCF\u4E00\u8F6E JSON \u90FD\u5FC5\u987B\u5728\u4FEE\u590D\u524D\u6216\u4FEE\u590D\u8FC7\u7A0B\u4E2D\u989D\u5916\u5305\u542B issueResolutionPlan\u3002\u8BE5\u65B9\u6848\u5FC5\u987B\u7B80\u6D01\u4E14\u53EF\u6267\u884C\uFF1A\u6839\u56E0\u5047\u8BBE\u3001\u8981\u4FEE\u6539\u7684\u6587\u4EF6/\u5951\u7EA6\u3001\u9A8C\u8BC1\u547D\u4EE4\u6216\u95E8\u7981\uFF0C\u4EE5\u53CA\u4EC0\u4E48\u8BC1\u636E\u4F1A\u63A8\u7FFB\u8BE5\u65B9\u6848\u3002DEBUG \u53EF\u4EE5\u4FEE\u6539\u5F53\u524D allowedWrites \u5185\u7684\u4E0A\u6E38\u6E90\u7801\u4E0E\u6D4B\u8BD5\u6587\u4EF6\uFF1B\u82E5\u5931\u8D25\u66B4\u9732\u7684\u662F\u5B9E\u73B0\u3001\u5951\u7EA6\u6216\u4E0B\u6E38\u8C03\u7528\u4E0D\u4E00\u81F4\uFF0C\u5FC5\u987B\u4FEE\u771F\u5B9E\u7F3A\u9677\uFF0C\u7981\u6B62\u901A\u8FC7\u524A\u5F31\u65AD\u8A00\u3001\u8DF3\u8FC7\u6D4B\u8BD5\u3001\u5220\u9664\u5931\u8D25\u7528\u4F8B\u6216\u53EA\u8FCE\u5408\u9519\u8BEF\u6D4B\u8BD5\u6765\u8FC7\u5173\u3002\u5982\u679C\u672C\u6B21\u56DE\u9000\u5904\u4E8E\u9700\u6C42/\u8BBE\u8BA1\u9636\u6BB5\uFF0C\u800C\u5177\u4F53\u6E90\u7801\u4FEE\u6539\u5C5E\u4E8E\u540E\u7EED V \u6A21\u578B Step \u4E14\u76EE\u6807\u6587\u4EF6\u4E0D\u5728\u5F53\u524D allowedWrites \u5185\uFF0C\u5E94\u66F4\u65B0\u5F53\u524D\u5951\u7EA6\u3001\u6D4B\u8BD5\u8BA1\u5212\u6216\u8BCA\u65AD\u4EA7\u7269\u5E76\u5B8C\u6210\u5F53\u524D Step\uFF0C\u8BA9\u540E\u7EED CODE Step \u5B9E\u73B0\uFF1B\u4E0D\u8981\u5C1D\u8BD5\u88AB\u62D2\u7EDD\u7684\u8D8A\u6743\u5199\u5165\u3002\u82E5\u5931\u8D25\u662F\u7B2C\u4E09\u65B9\u4F9D\u8D56\u7F3A\u5931\u6216\u5E93\u9009\u578B\u9519\u8BEF\uFF0C\u5FC5\u987B\u7528 add_dependency \u5199\u5165\u771F\u5B9E\u5305\u540D\uFF0C\u6216\u628A\u6E90\u7801\u6539\u56DE HIGH_LEVEL_DESIGN \u9009\u5B9A\u7684\u771F\u5B9E\u5E93\uFF1B\u4E25\u7981\u5728 src/ \u751F\u4EA7\u4EE3\u7801\u91CC try/except ImportError \u540E\u4F2A\u9020 module\u3001fake class/function\u3001\u7A7A\u5B9E\u73B0\u6216 fallback mock \u6765\u7ED5\u8FC7\u9519\u8BEF\u3002\u8BF7\u5305\u542B read_file/code_search \u5148\u5B9A\u4F4D\u95EE\u9898\uFF0C\u518D\u4EE5 apply_patch / replace_in_file / add_dependency \u4F5C\u6700\u5C0F\u4FEE\u6539\uFF0C\u6700\u540E run_tests \u9A8C\u8BC1\u3002DEBUG \u91CD\u8BD5\u4E0D\u80FD\u53EA\u9760\u53EA\u8BFB\u68C0\u67E5\u5C31\u6807\u8BB0\u5B8C\u6210\uFF1A\u672C\u6B21\u91CD\u8BD5\u5FC5\u987B\u4EA7\u751F\u4E00\u6B21\u6210\u529F\u7684\u4FEE\u590D\u52A8\u4F5C\uFF0C\u6216\u4E00\u6B21\u6210\u529F\u7684\u9A8C\u8BC1\u547D\u4EE4\u3002\u5982\u679C\u4E0A\u4E00\u8F6E\u5931\u8D25\u539F\u56E0\u5305\u542B repeated read-only/probe actions\uFF0C\u8BF7\u628A\u5DF2\u6709 failure log \u89C6\u4E3A\u8DB3\u591F\u4E0A\u4E0B\u6587\uFF0C\u4E0B\u4E00\u6B65\u76F4\u63A5 patch/write/\u6539\u4F9D\u8D56\u6216\u6267\u884C\u9A8C\u8BC1\u547D\u4EE4\u3002\u5F53\u6D4B\u8BD5\u5DF2\u6B63\u5E38\u6267\u884C\u4F46\u5931\u8D25\u70B9\u662F\u8FD4\u56DE\u884C\u4E3A\u65AD\u8A00\u65F6\uFF0C\u7981\u6B62\u53CD\u590D\u91CD\u5199 fixture \u6216\u6837\u4F8B\u3002\u53EA\u6709\u8BC1\u636E\u660E\u786E\u662F\u6587\u4EF6\u7F3A\u5931\u3001fixture \u683C\u5F0F\u9519\u8BEF\u6216 fixture \u672C\u8EAB\u89E3\u6790\u5931\u8D25\u65F6\u624D\u4FEE\u6539 fixture\uFF1B\u5426\u5219\u5E94\u4FEE\u5B9E\u73B0\u3001\u63A5\u53E3\u5951\u7EA6\u3001\u4F9D\u8D56\u9009\u578B\u6216\u786E\u5B9E\u9519\u8BEF\u7684\u65AD\u8A00\u3002\u5982\u679C\u5931\u8D25\u65E5\u5FD7\u663E\u793A\u7F51\u7EDC/API \u8C03\u7528\u5931\u8D25\uFF0C\u4E0D\u5141\u8BB8\u53EA\u505C\u7559\u5728\u63A2\u6D4B\u63A5\u53E3\uFF1A\u6700\u591A\u8FDE\u7EED\u6267\u884C 2 \u6B21 http_fetch \u63A2\u6D4B\uFF1BHTTP 2xx \u4F46 body \u4E3A\u7A7A\u6216\u683C\u5F0F\u4E0D\u53EF\u7528\u4E0D\u7B97\u53EF\u7528\u63A5\u53E3\uFF1B\u968F\u540E\u5FC5\u987B patch \u771F\u5B9E\u96C6\u6210\u4EE3\u7801\uFF0C\u5E76\u7528 run_program \u548C run_tests \u9A8C\u8BC1\u3002\u5165\u53E3\u4ECD\u8F93\u51FA\u7F51\u7EDC/API \u5931\u8D25\u65F6\u4E0D\u5F97 done=true\u3002` + (suggestions ? `
2021
2033
 
2022
2034
  ${suggestions}` : ""),
2023
2035
  executorGlobalBlock: (globalPrompt) => `
@@ -2034,7 +2046,8 @@ ${sp}`,
2034
2046
  executorFeedbackVerifyMissing: (paths) => `outputs \u4ECD\u7F3A\u5931\uFF1A${paths}\u3002\u8BF7\u7EE7\u7EED\u3002`,
2035
2047
  executorFeedbackReadOnlyLoopWarning: (rounds, targets) => `\u5FAA\u73AF\u95E8\u7981\u8B66\u544A\uFF1A\u6700\u8FD1 ${rounds} \u8F6E\u53EA\u8C03\u7528\u4E86\u8BFB\u53D6/\u63A2\u6D4B\u5DE5\u5177` + (targets ? `\uFF08${targets}\uFF09` : "") + "\u3002\u4E0B\u4E00\u8F6E\u5FC5\u987B\u5305\u542B\u4E00\u6B21\u6210\u529F\u7684\u4FEE\u590D\u52A8\u4F5C\uFF08apply_patch / replace_in_file / write_file / add_dependency\uFF09\u6216\u660E\u786E\u7684\u9A8C\u8BC1\u52A8\u4F5C\uFF08run_tests / run_program\uFF09\u3002\u4E0D\u8981\u7EE7\u7EED\u53EA\u8C03\u7528 read_file\u3001list_dir\u3001code_search \u6216 http_fetch\u3002",
2036
2048
  executorFeedbackReadOnlyRecoveryRequired: "\u53EA\u8BFB\u6062\u590D\u6A21\u5F0F\u5DF2\u542F\u7528\uFF1A\u4E0A\u4E00\u8F6E\u5DF2\u7ECF\u56E0\u4E3A\u6301\u7EED\u63A2\u6D4B\u5931\u8D25\u3002\u672C\u8F6E\u5FC5\u987B\u57FA\u4E8E\u5DF2\u6709 failure log \u76F4\u63A5 patch/write/\u6539\u4F9D\u8D56\u6216\u6267\u884C\u9A8C\u8BC1\uFF1B\u5982\u679C\u4E0B\u4E00\u8F6E\u4ECD\u7136\u53EA\u6709\u53EA\u8BFB/\u63A2\u6D4B\u52A8\u4F5C\uFF0C\u672C\u6B21\u91CD\u8BD5\u4F1A\u5931\u8D25\u5E76\u56DE\u9000\u3002",
2037
- executorFeedbackRepairEvidenceMissing: "DEBUG \u5B8C\u6210\u65E0\u6548\uFF1A\u672C\u6B21\u91CD\u8BD5\u8FD8\u6CA1\u6709\u4EA7\u751F\u4FEE\u590D\u8BC1\u636E\u3002\u8BBE\u7F6E done=true \u524D\uFF0C\u5FC5\u987B\u81F3\u5C11\u5B8C\u6210\u4E00\u6B21\u6210\u529F\u7684\u4FEE\u590D\u52A8\u4F5C\u6216\u6210\u529F\u7684\u9A8C\u8BC1\u8FD0\u884C\uFF1B\u5426\u5219\u53EA\u80FD\u5728 thoughts \u4E2D\u7ED9\u51FA\u5177\u4F53 blocker \u540E\u505C\u6B62\u3002"
2049
+ executorFeedbackRepairEvidenceMissing: "DEBUG \u5B8C\u6210\u65E0\u6548\uFF1A\u672C\u6B21\u91CD\u8BD5\u8FD8\u6CA1\u6709\u4EA7\u751F\u4FEE\u590D\u8BC1\u636E\u3002\u8BBE\u7F6E done=true \u524D\uFF0C\u5FC5\u987B\u81F3\u5C11\u5B8C\u6210\u4E00\u6B21\u6210\u529F\u7684\u4FEE\u590D\u52A8\u4F5C\u6216\u6210\u529F\u7684\u9A8C\u8BC1\u8FD0\u884C\uFF1B\u5426\u5219\u53EA\u80FD\u5728 thoughts \u4E2D\u7ED9\u51FA\u5177\u4F53 blocker \u540E\u505C\u6B62\u3002",
2050
+ executorFeedbackIssueResolutionPlanMissing: "DEBUG issue \u5B8C\u6210\u65E0\u6548\uFF1AissueResolutionPlan \u662F issue resolved \u7684\u5FC5\u8981\u5B57\u6BB5\u3002\u8BF7\u8FD4\u56DE\u5305\u542B\u7B80\u660E\u5904\u7406\u65B9\u6848\u7684 JSON\uFF0C\u5E76\u540C\u65F6\u7ED9\u51FA\u5FC5\u8981\u4FEE\u590D\u6216\u9A8C\u8BC1\u52A8\u4F5C\u3002"
2038
2051
  },
2039
2052
  skills: {
2040
2053
  patcher: "\u901A\u8FC7 apply_patch / replace_in_file \u5BF9\u5DF2\u6709\u6587\u4EF6\u505A\u5C0F\u6539\u52A8\uFF0C\u7981\u6B62\u6574\u6587\u4EF6\u8986\u76D6\u3002",
@@ -2055,7 +2068,7 @@ ${sp}`,
2055
2068
  summaryOk: "\u5168\u90E8\u68C0\u67E5\u901A\u8FC7\u3002",
2056
2069
  summaryWarn: (n) => `\u901A\u8FC7\uFF0C\u4F46\u6709 ${n} \u6761 warning\u3002`,
2057
2070
  summaryFail: (n) => `\u68C0\u6D4B\u5230 ${n} \u9879\u5931\u8D25\u3002`,
2058
- configLoadOk: (path31) => `\u914D\u7F6E\u5DF2\u52A0\u8F7D\uFF1A${path31}`,
2071
+ configLoadOk: (path32) => `\u914D\u7F6E\u5DF2\u52A0\u8F7D\uFF1A${path32}`,
2059
2072
  configLoadFail: (msg) => `\u914D\u7F6E\u52A0\u8F7D\u5931\u8D25\uFF1A${msg}`,
2060
2073
  configLocale: (locale) => `locale=${locale}`,
2061
2074
  llmNoProviders: "config.llm.providers \u4E3A\u7A7A\uFF0C\u672A\u58F0\u660E\u4EFB\u4F55 provider",
@@ -2073,7 +2086,7 @@ ${sp}`,
2073
2086
  roleOk: (role, provider) => `\u89D2\u8272 "${role}" \u2192 ${provider}`,
2074
2087
  sandboxKind: (kind) => `sandbox=${kind}`,
2075
2088
  sandboxNetworkPolicy: (policy, ports) => `network=${policy}` + (ports.length ? `\uFF08expose_ports=[${ports.join(", ")}]\uFF09` : ""),
2076
- sandboxFullNoPorts: "network=full \u4F46\u672A\u914D\u7F6E expose_ports\u2014\u5BBF\u4E3B\u4FA7\u65E0\u6CD5\u8BBF\u95EE\u5BB9\u5668\u5185\u670D\u52A1\u3002\u8BF7\u5728 config.yaml \u4E2D\u8BBE\u7F6E `agent.sandbox_limits.expose_ports: [<port>]`\u3002",
2089
+ sandboxFullNoPorts: "network=full \u4F46\u672A\u914D\u7F6E expose_ports\u2014\u5BBF\u4E3B\u4FA7\u65E0\u6CD5\u8BBF\u95EE\u5BB9\u5668\u5185\u670D\u52A1\u3002\u8BF7\u5728 config.yaml \u4E2D\u8BBE\u7F6E `agent.sandboxes.<language>.<local|docker>.limits.expose_ports: [<port>]`\u3002",
2077
2090
  sandboxNodeMissing: "PATH \u4E0A\u627E\u4E0D\u5230 node\uFF08TypeScript subprocess \u6C99\u76D2\u5FC5\u9700\uFF09",
2078
2091
  sandboxNodeOk: (version) => `node OK\uFF08${version}\uFF09`,
2079
2092
  sandboxNpmMissing: "PATH \u4E0A\u627E\u4E0D\u5230 npm\uFF08TypeScript subprocess \u6C99\u76D2\u5FC5\u9700\uFF09",
@@ -2135,8 +2148,10 @@ function detectNetworkApiFailure(text) {
2135
2148
  function isTestAssertionDiagnosticLine(line) {
2136
2149
  const text = line.trim();
2137
2150
  if (!text) return false;
2151
+ if (/^\d+\|\s/u.test(text)) return true;
2138
2152
  if (/^(?:[→>-]\s*)?expected\b/iu.test(text)) return true;
2139
2153
  if (/\bAssertionError\b/iu.test(text) && /\bexpected\b/iu.test(text)) return true;
2154
+ if (/\bexpect\s*\(/u.test(text) && /\.(?:to|not)\w*\s*\(/u.test(text)) return true;
2140
2155
  return /\bexpected\b[\s\S]{0,240}\b(?:got|received|to\s+(?:be|equal|throw|contain|have|match))\b/iu.test(text);
2141
2156
  }
2142
2157
  function isTestRunnerStatusLine(line) {
@@ -2183,8 +2198,8 @@ async function autoFixSrcImports(ws, audit) {
2183
2198
  const candidates = [];
2184
2199
  if (await ws.exists("src/main.py")) candidates.push("src/main.py");
2185
2200
  try {
2186
- const fs27 = await import("fs/promises");
2187
- const entries = await fs27.readdir(ws.abs("src"), { withFileTypes: true });
2201
+ const fs29 = await import("fs/promises");
2202
+ const entries = await fs29.readdir(ws.abs("src"), { withFileTypes: true });
2188
2203
  for (const e of entries) {
2189
2204
  if (!e.isDirectory()) continue;
2190
2205
  const rel = `src/${e.name}/__main__.py`;
@@ -2335,8 +2350,8 @@ async function detectPythonEntrypoint(ws) {
2335
2350
  if (await ws.exists("src/main.py")) return fileCandidate("src/main.py");
2336
2351
  if (await ws.exists("src/__main__.py")) return fileCandidate("src/__main__.py");
2337
2352
  try {
2338
- const fs27 = await import("fs/promises");
2339
- const entries = await fs27.readdir(ws.abs("src"), { withFileTypes: true });
2353
+ const fs29 = await import("fs/promises");
2354
+ const entries = await fs29.readdir(ws.abs("src"), { withFileTypes: true });
2340
2355
  for (const e of entries) {
2341
2356
  if (!e.isDirectory()) continue;
2342
2357
  const rel = `src/${e.name}/__main__.py`;
@@ -2395,7 +2410,7 @@ var typescriptProfile = {
2395
2410
  manifestFile: "package.json",
2396
2411
  codeExtensions: [".ts", ".tsx"],
2397
2412
  seedManifestFromDeps: false,
2398
- defaultDockerImage: "node:20-slim",
2413
+ defaultDockerImage: "node:24-slim",
2399
2414
  renderManifest(deps) {
2400
2415
  const pkg = {
2401
2416
  name: "app",
@@ -2424,6 +2439,9 @@ var typescriptProfile = {
2424
2439
  },
2425
2440
  plannerPromptOverride: "",
2426
2441
  executorPromptOverride: "",
2442
+ async autoFixImports(ws, audit) {
2443
+ return autoFixTypeScriptTypeOnlyImports(ws, audit);
2444
+ },
2427
2445
  async probeEntry(ws, sandbox) {
2428
2446
  return probeTsEntrypoint(ws, sandbox);
2429
2447
  }
@@ -2573,6 +2591,99 @@ async function readJsonFile(ws, rel) {
2573
2591
  return null;
2574
2592
  }
2575
2593
  }
2594
+ var TYPE_ONLY_IMPORTS_BY_PACKAGE = {
2595
+ axios: /* @__PURE__ */ new Set([
2596
+ "AxiosAdapter",
2597
+ "AxiosBasicCredentials",
2598
+ "AxiosHeaderValue",
2599
+ "AxiosInstance",
2600
+ "AxiosInterceptorManager",
2601
+ "AxiosPromise",
2602
+ "AxiosProxyConfig",
2603
+ "AxiosRequestConfig",
2604
+ "AxiosRequestHeaders",
2605
+ "AxiosResponse",
2606
+ "AxiosResponseHeaders",
2607
+ "CreateAxiosDefaults",
2608
+ "InternalAxiosRequestConfig",
2609
+ "RawAxiosRequestHeaders"
2610
+ ])
2611
+ };
2612
+ async function autoFixTypeScriptTypeOnlyImports(ws, audit) {
2613
+ const files = await listTypeScriptSourceFiles(ws, "src");
2614
+ const fixed = [];
2615
+ for (const rel of files) {
2616
+ const original = await ws.readFile(rel);
2617
+ const next = rewriteKnownTypeOnlyImports(original);
2618
+ if (next === original) continue;
2619
+ await ws.writeFile(rel, next);
2620
+ fixed.push(rel);
2621
+ await audit.event("note", `fixed type-only imports in ${rel}`, {
2622
+ messageId: "audit.typescript_imports_autofix",
2623
+ path: rel
2624
+ });
2625
+ }
2626
+ return fixed;
2627
+ }
2628
+ async function listTypeScriptSourceFiles(ws, dir) {
2629
+ const abs = ws.abs(dir);
2630
+ let entries;
2631
+ try {
2632
+ entries = await fs.readdir(abs, { withFileTypes: true });
2633
+ } catch {
2634
+ return [];
2635
+ }
2636
+ const files = [];
2637
+ for (const entry of entries) {
2638
+ const rel = `${dir}/${entry.name}`.replace(/\\/g, "/");
2639
+ if (entry.isDirectory()) {
2640
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
2641
+ files.push(...await listTypeScriptSourceFiles(ws, rel));
2642
+ } else if (entry.isFile() && /\.tsx?$/.test(entry.name)) {
2643
+ files.push(rel);
2644
+ }
2645
+ }
2646
+ return files.sort();
2647
+ }
2648
+ function rewriteKnownTypeOnlyImports(source) {
2649
+ const importRe = /^import\s+([^'"\n]+?)\s+from\s+(['"])([^'"]+)\2\s*;?$/gm;
2650
+ return source.replace(importRe, (full, clauseRaw, quote, specifier) => {
2651
+ const knownTypes = TYPE_ONLY_IMPORTS_BY_PACKAGE[specifier];
2652
+ if (!knownTypes) return full;
2653
+ if (clauseRaw.trim().startsWith("type ")) return full;
2654
+ const parsed = splitImportClause(clauseRaw);
2655
+ if (!parsed?.named.length) return full;
2656
+ const valueNamed = [];
2657
+ const typeNamed = [];
2658
+ for (const item of parsed.named) {
2659
+ const importedName = item.replace(/^type\s+/u, "").split(/\s+as\s+/iu)[0]?.trim() ?? "";
2660
+ if (knownTypes.has(importedName)) typeNamed.push(item.replace(/^type\s+/u, "").trim());
2661
+ else valueNamed.push(item);
2662
+ }
2663
+ if (typeNamed.length === 0) return full;
2664
+ const lines = [];
2665
+ if (parsed.defaultImport && valueNamed.length > 0) {
2666
+ lines.push(`import ${parsed.defaultImport}, { ${valueNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2667
+ } else if (parsed.defaultImport) {
2668
+ lines.push(`import ${parsed.defaultImport} from ${quote}${specifier}${quote};`);
2669
+ } else if (valueNamed.length > 0) {
2670
+ lines.push(`import { ${valueNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2671
+ }
2672
+ lines.push(`import type { ${typeNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2673
+ return lines.join("\n");
2674
+ });
2675
+ }
2676
+ function splitImportClause(clauseRaw) {
2677
+ const clause = clauseRaw.trim();
2678
+ const namedMatch = clause.match(/\{([\s\S]*)\}$/u);
2679
+ if (!namedMatch) return void 0;
2680
+ const beforeNamed = clause.slice(0, namedMatch.index).replace(/,\s*$/u, "").trim();
2681
+ const named = (namedMatch[1] ?? "").split(",").map((item) => item.trim()).filter(Boolean);
2682
+ return {
2683
+ defaultImport: beforeNamed || void 0,
2684
+ named
2685
+ };
2686
+ }
2576
2687
 
2577
2688
  // src/core/architecture.ts
2578
2689
  var COMPLEXITY_SURFACES = [
@@ -2764,7 +2875,7 @@ function validateArchitectureContract(modules, steps, language, demand) {
2764
2875
  }
2765
2876
  allTestPaths.add(testPath);
2766
2877
  }
2767
- const owners = codeSteps.filter((step) => module.sourcePaths.every((path31) => pathCoveredByOutputs(path31, step.outputs))).sort((a, b) => (stepIndex.get(a.id) ?? 0) - (stepIndex.get(b.id) ?? 0));
2878
+ const owners = codeSteps.filter((step) => module.sourcePaths.every((path32) => pathCoveredByOutputs(path32, step.outputs))).sort((a, b) => (stepIndex.get(a.id) ?? 0) - (stepIndex.get(b.id) ?? 0));
2768
2879
  if (owners.length === 0) {
2769
2880
  issues.push({
2770
2881
  message: `${module.id} must map all sourcePaths to a CODE macro step; found 0.`
@@ -2794,7 +2905,7 @@ function validateArchitectureContract(modules, steps, language, demand) {
2794
2905
  ownedModules.push(module);
2795
2906
  modulesByCodeOwner.set(codeOwner.id, ownedModules);
2796
2907
  const matchingTests = testSteps.filter(
2797
- (step) => module.testPaths.some((path31) => pathCoveredByOutputs(path31, step.outputs))
2908
+ (step) => module.testPaths.some((path32) => pathCoveredByOutputs(path32, step.outputs))
2798
2909
  );
2799
2910
  if (matchingTests.length === 0) {
2800
2911
  issues.push({ message: `${module.id} testPaths are not produced by any MODULE_TEST step.` });
@@ -2851,8 +2962,8 @@ function flattenSubTasks(step) {
2851
2962
  }
2852
2963
  return out;
2853
2964
  }
2854
- function pathCoveredByOutputs(path31, outputs) {
2855
- const normalizedPath = normalizePath(path31);
2965
+ function pathCoveredByOutputs(path32, outputs) {
2966
+ const normalizedPath = normalizePath(path32);
2856
2967
  return outputs.some((output) => {
2857
2968
  const normalizedOutput = normalizePath(output);
2858
2969
  return normalizedPath === normalizedOutput || normalizedPath.startsWith(`${normalizedOutput}/`);
@@ -2862,14 +2973,14 @@ function missingArchitectureDocumentTokens(content, modules) {
2862
2973
  const required = modules.flatMap((module) => [module.id, ...module.sourcePaths, ...module.testPaths]);
2863
2974
  return [...new Set(required)].filter((token) => !content.includes(token));
2864
2975
  }
2865
- function isSourcePath(path31, extensions) {
2866
- return path31.startsWith("src/") && extensions.some((extension) => path31.endsWith(extension));
2976
+ function isSourcePath(path32, extensions) {
2977
+ return path32.startsWith("src/") && extensions.some((extension) => path32.endsWith(extension));
2867
2978
  }
2868
- function isTestPath(path31, extensions) {
2869
- return path31.startsWith("tests/") && extensions.some((extension) => path31.endsWith(extension));
2979
+ function isTestPath(path32, extensions) {
2980
+ return path32.startsWith("tests/") && extensions.some((extension) => path32.endsWith(extension));
2870
2981
  }
2871
- function normalizePath(path31) {
2872
- return path31.replace(/\\/g, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2982
+ function normalizePath(path32) {
2983
+ return path32.replace(/\\/g, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2873
2984
  }
2874
2985
  function transitivelyDependsOn(step, targetId, byId) {
2875
2986
  const seen = /* @__PURE__ */ new Set();
@@ -3451,21 +3562,21 @@ function parseLoadedPlan(json) {
3451
3562
  }
3452
3563
  async function savePlan(planPath, plan) {
3453
3564
  PlanSchema.parse(plan);
3454
- await fs.mkdir(path3.dirname(planPath), { recursive: true });
3455
- await fs.writeFile(planPath, JSON.stringify(plan, null, 2) + "\n", "utf8");
3565
+ await fs2.mkdir(path3.dirname(planPath), { recursive: true });
3566
+ await fs2.writeFile(planPath, JSON.stringify(plan, null, 2) + "\n", "utf8");
3456
3567
  }
3457
3568
  async function loadPhasePlan(phasePlanPath) {
3458
- const raw = await fs.readFile(phasePlanPath, "utf8");
3569
+ const raw = await fs2.readFile(phasePlanPath, "utf8");
3459
3570
  return PhasePlanSchema.parse(JSON.parse(raw));
3460
3571
  }
3461
3572
  async function savePhasePlan(phasePlanPath, phasePlan) {
3462
3573
  PhasePlanSchema.parse(phasePlan);
3463
- await fs.mkdir(path3.dirname(phasePlanPath), { recursive: true });
3464
- await fs.writeFile(phasePlanPath, JSON.stringify(phasePlan, null, 2) + "\n", "utf8");
3574
+ await fs2.mkdir(path3.dirname(phasePlanPath), { recursive: true });
3575
+ await fs2.writeFile(phasePlanPath, JSON.stringify(phasePlan, null, 2) + "\n", "utf8");
3465
3576
  }
3466
3577
  async function loadPlanTarget(inputPath) {
3467
3578
  const requestedPath = path3.resolve(inputPath);
3468
- const raw = await fs.readFile(requestedPath, "utf8");
3579
+ const raw = await fs2.readFile(requestedPath, "utf8");
3469
3580
  const json = JSON.parse(raw);
3470
3581
  const phasePlanResult = PhasePlanSchema.safeParse(json);
3471
3582
  if (phasePlanResult.success) {
@@ -3475,7 +3586,7 @@ async function loadPlanTarget(inputPath) {
3475
3586
  throw new Error(`phasePlan ${requestedPath} has no planPath for current phase ${phasePlan.currentPhaseId}`);
3476
3587
  }
3477
3588
  const planPath = path3.resolve(path3.dirname(requestedPath), phase.planPath);
3478
- const loaded2 = parseLoadedPlan(JSON.parse(await fs.readFile(planPath, "utf8")));
3589
+ const loaded2 = parseLoadedPlan(JSON.parse(await fs2.readFile(planPath, "utf8")));
3479
3590
  return {
3480
3591
  plan: loaded2.plan,
3481
3592
  planPath,
@@ -3580,7 +3691,7 @@ async function findProjectFile(workspace) {
3580
3691
  const ws = path4.resolve(workspace);
3581
3692
  let entries;
3582
3693
  try {
3583
- entries = await fs2.readdir(ws, { withFileTypes: true });
3694
+ entries = await fs3.readdir(ws, { withFileTypes: true });
3584
3695
  } catch {
3585
3696
  return void 0;
3586
3697
  }
@@ -3630,8 +3741,8 @@ async function updateProjectFile(opts) {
3630
3741
  history: nextHistory
3631
3742
  };
3632
3743
  XCompilerProjectFileSchema.parse(data);
3633
- await fs2.mkdir(path4.dirname(filePath), { recursive: true });
3634
- await fs2.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
3744
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
3745
+ await fs3.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
3635
3746
  return filePath;
3636
3747
  }
3637
3748
  function buildProjectProgress(plan) {
@@ -3674,7 +3785,7 @@ function buildProjectProgress(plan) {
3674
3785
  }
3675
3786
  async function readExistingProjectFile(filePath) {
3676
3787
  try {
3677
- const raw = await fs2.readFile(filePath, "utf8");
3788
+ const raw = await fs3.readFile(filePath, "utf8");
3678
3789
  return XCompilerProjectFileSchema.parse(JSON.parse(raw));
3679
3790
  } catch {
3680
3791
  return void 0;
@@ -3704,10 +3815,10 @@ function assertProjectFileExtension(filePath) {
3704
3815
 
3705
3816
  // src/runtime/build.ts
3706
3817
  import path13 from "path";
3707
- import { promises as fs11 } from "fs";
3818
+ import { promises as fs12 } from "fs";
3708
3819
 
3709
3820
  // src/config/config.ts
3710
- import { promises as fs3 } from "fs";
3821
+ import { promises as fs4 } from "fs";
3711
3822
  import path5 from "path";
3712
3823
  import YAML from "yaml";
3713
3824
  import { z as z4 } from "zod";
@@ -3773,6 +3884,73 @@ var ProviderSchema = z4.object({
3773
3884
  think: z4.boolean().optional()
3774
3885
  });
3775
3886
  var LocaleSchema = z4.enum(["en", "zh"]);
3887
+ var TargetLanguageSchema = z4.enum(["python", "typescript"]);
3888
+ var SandboxModeSchema = z4.enum(["subprocess", "docker", "firejail"]);
3889
+ var SandboxLimitsSchema = z4.object({
3890
+ cpu: z4.number().positive().default(1),
3891
+ memory_mb: z4.number().int().positive().default(1024),
3892
+ wall_seconds: z4.number().int().positive().default(60),
3893
+ /**
3894
+ * Sandbox network policy.
3895
+ * - `off` — no network at all (`docker --network none`).
3896
+ * - `download-only` — outbound traffic allowed, no inbound port publishing.
3897
+ * - `pypi-only` — legacy value; rejected at sandbox creation rather than silently
3898
+ * allowing unrestricted outbound traffic.
3899
+ * - `full` — outbound + every port in `expose_ports` is published
3900
+ * to `127.0.0.1` so host-side tests can reach the app.
3901
+ */
3902
+ network: z4.enum(["off", "pypi-only", "download-only", "full"]).default("download-only"),
3903
+ /** Container ports to publish to 127.0.0.1 when `network=full`. */
3904
+ expose_ports: z4.array(z4.number().int().min(1).max(65535)).default([])
3905
+ }).default({
3906
+ cpu: 1,
3907
+ memory_mb: 1024,
3908
+ wall_seconds: 60,
3909
+ network: "download-only",
3910
+ expose_ports: []
3911
+ });
3912
+ var LocalSandboxSchema = z4.object({
3913
+ sandbox_dir: z4.string().min(1).optional(),
3914
+ python_bin: z4.string().min(1).optional(),
3915
+ inherit_env: z4.boolean().optional(),
3916
+ limits: SandboxLimitsSchema
3917
+ }).default(() => ({ limits: defaultSandboxLimits() }));
3918
+ var DockerSandboxSchema = z4.object({
3919
+ image: z4.string().default("python:3.11-slim"),
3920
+ workdir: z4.string().default("/workspace"),
3921
+ pull: z4.boolean().default(false),
3922
+ docker_bin: z4.string().default("docker"),
3923
+ extra_run_args: z4.array(z4.string()).default([]),
3924
+ sandbox_dir: z4.string().min(1).optional(),
3925
+ limits: SandboxLimitsSchema
3926
+ }).default({
3927
+ image: "python:3.11-slim",
3928
+ workdir: "/workspace",
3929
+ pull: false,
3930
+ docker_bin: "docker",
3931
+ extra_run_args: [],
3932
+ limits: defaultSandboxLimits()
3933
+ });
3934
+ var LanguageSandboxSchema = z4.object({
3935
+ mode: SandboxModeSchema.default("subprocess"),
3936
+ local: LocalSandboxSchema,
3937
+ docker: DockerSandboxSchema
3938
+ }).default(() => ({
3939
+ mode: "subprocess",
3940
+ local: { limits: defaultSandboxLimits() },
3941
+ docker: {
3942
+ image: "python:3.11-slim",
3943
+ workdir: "/workspace",
3944
+ pull: false,
3945
+ docker_bin: "docker",
3946
+ extra_run_args: [],
3947
+ limits: defaultSandboxLimits()
3948
+ }
3949
+ }));
3950
+ var SandboxesSchema = z4.object({
3951
+ python: LanguageSandboxSchema.optional(),
3952
+ typescript: LanguageSandboxSchema.optional()
3953
+ }).default({});
3776
3954
  var LlmSchema = z4.object({
3777
3955
  default: z4.string(),
3778
3956
  providers: z4.record(z4.string(), ProviderSchema),
@@ -3780,7 +3958,7 @@ var LlmSchema = z4.object({
3780
3958
  * 角色 → provider 数组的映射。
3781
3959
  * 兼容旧格式:单字符串 `Coder: ollama_code` 自动归一化为 `[ollama_code]`。
3782
3960
  * 数组形式 `Coder: [ollama_code, openai]` 表示该角色的候选 LLM 池;
3783
- * 实际选择顺序由 `llm.scores` 评分降序决定;用户显式设置评分=0 的 provider 直接跳过。
3961
+ * 实际选择顺序由 ScoreStore 有效评分降序决定;有效评分为用户覆盖优先,否则使用动态评分。
3784
3962
  */
3785
3963
  roles: z4.record(z4.string(), z4.union([z4.string(), z4.array(z4.string())])).default({}).transform((obj) => {
3786
3964
  const out = {};
@@ -3794,9 +3972,8 @@ var LlmSchema = z4.object({
3794
3972
  /** 可选:按角色指定 fallback 链(覆盖全局) */
3795
3973
  role_fallbacks: z4.record(z4.string(), z4.array(z4.string())).default({}),
3796
3974
  /**
3797
- * Provider 评分快照(启动时从 sidecar 文件加载,运行时由 ScoreStore 维护并自动落盘)。
3798
- * 配置文件里也可以手工设置初始值;不存在的 provider 默认评分 = 1。
3799
- * 用户手工设置评分 = 0 表示禁用;运行时自动评分只在 0.1~1.0 范围内调整。
3975
+ * Provider 兼容初值。运行时动态评分由 ScoreStore 写入 llm_scores.yaml;
3976
+ * 用户手动覆盖应写入 llm_scores_user.yaml。旧配置里显式 0 仍表示用户禁用。
3800
3977
  */
3801
3978
  scores: z4.record(z4.string(), z4.number().min(0)).default({}),
3802
3979
  /**
@@ -3816,65 +3993,107 @@ var LlmSchema = z4.object({
3816
3993
  });
3817
3994
  }
3818
3995
  });
3996
+ var AgentSchema = z4.object({
3997
+ /** @deprecated Target project language is inferred from topic/baseline. Kept for legacy configs only. */
3998
+ language: TargetLanguageSchema.optional(),
3999
+ max_steps: z4.number().int().positive().default(50),
4000
+ max_debug_retries: z4.number().int().positive().default(3),
4001
+ /** Debugger 滑动窗口的硬上限(默认 = max(max_debug_retries*4, 10))。 */
4002
+ max_debug_retries_cap: z4.number().int().positive().optional(),
4003
+ max_rounds_per_step: z4.number().int().positive().default(6),
4004
+ max_debug_rounds_per_step: z4.number().int().positive().optional(),
4005
+ max_edit_lines_per_step: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
4006
+ max_write_chunk_bytes: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
4007
+ /** @deprecated Use agent.sandboxes.<language>.mode. */
4008
+ sandbox: SandboxModeSchema.optional(),
4009
+ /** @deprecated Use agent.sandboxes.<language>.<local|docker>.limits. */
4010
+ sandbox_limits: SandboxLimitsSchema.optional(),
4011
+ /** @deprecated Use agent.sandboxes.<language>.docker. */
4012
+ sandbox_docker: DockerSandboxSchema.optional(),
4013
+ sandboxes: SandboxesSchema
4014
+ }).transform((agent) => {
4015
+ const legacyLanguage = agent.language ?? "python";
4016
+ const legacyMode = agent.sandbox ?? "subprocess";
4017
+ const legacyLimits = agent.sandbox_limits ?? defaultSandboxLimits();
4018
+ const defaults = {
4019
+ python: defaultLanguageSandbox("python", legacyMode, legacyLimits),
4020
+ typescript: defaultLanguageSandbox("typescript", legacyMode, legacyLimits)
4021
+ };
4022
+ const sandboxes = {
4023
+ python: mergeLanguageSandbox(
4024
+ defaults.python,
4025
+ agent.sandboxes.python,
4026
+ legacyLanguage === "python" ? agent.sandbox_docker : void 0
4027
+ ),
4028
+ typescript: mergeLanguageSandbox(
4029
+ defaults.typescript,
4030
+ agent.sandboxes.typescript,
4031
+ legacyLanguage === "typescript" ? agent.sandbox_docker : void 0
4032
+ )
4033
+ };
4034
+ return {
4035
+ ...agent,
4036
+ language: legacyLanguage,
4037
+ sandbox: sandboxes.python.mode,
4038
+ sandbox_limits: sandboxes.python.local.limits,
4039
+ sandbox_docker: sandboxes.python.docker,
4040
+ sandboxes
4041
+ };
4042
+ });
3819
4043
  var ConfigSchema = z4.object({
3820
4044
  /** CLI / prompt locale. Accepts 'en' (default) or 'zh'. */
3821
4045
  locale: LocaleSchema.optional(),
3822
4046
  /** @deprecated use `locale` instead. Kept as a backwards-compatible alias. */
3823
4047
  ui_language: LocaleSchema.optional(),
3824
4048
  llm: LlmSchema,
3825
- agent: z4.object({
3826
- language: z4.enum(["python", "typescript"]).default("python"),
3827
- max_steps: z4.number().int().positive().default(50),
3828
- max_debug_retries: z4.number().int().positive().default(3),
3829
- /** Debugger 滑动窗口的硬上限(默认 = max(max_debug_retries*4, 10))。 */
3830
- max_debug_retries_cap: z4.number().int().positive().optional(),
3831
- max_rounds_per_step: z4.number().int().positive().default(6),
3832
- max_debug_rounds_per_step: z4.number().int().positive().optional(),
3833
- max_edit_lines_per_step: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3834
- max_write_chunk_bytes: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3835
- sandbox: z4.enum(["subprocess", "docker", "firejail"]).default("subprocess"),
3836
- sandbox_limits: z4.object({
3837
- cpu: z4.number().positive().default(1),
3838
- memory_mb: z4.number().int().positive().default(1024),
3839
- wall_seconds: z4.number().int().positive().default(60),
3840
- /**
3841
- * Sandbox network policy.
3842
- * - `off` — no network at all (`docker --network none`).
3843
- * - `download-only` — outbound traffic allowed, no inbound port publishing
3844
- * (default; lets python pip-install / fetch web pages).
3845
- * - `pypi-only` — legacy value; rejected at sandbox creation rather than silently
3846
- * allowing unrestricted outbound traffic.
3847
- * - `full` — outbound + every port in `expose_ports` is published
3848
- * to `127.0.0.1` so host-side tests can reach the app.
3849
- */
3850
- network: z4.enum(["off", "pypi-only", "download-only", "full"]).default("download-only"),
3851
- /** Container ports to publish to 127.0.0.1 when `network=full`. */
3852
- expose_ports: z4.array(z4.number().int().min(1).max(65535)).default([])
3853
- }).default({
3854
- cpu: 1,
3855
- memory_mb: 1024,
3856
- wall_seconds: 60,
3857
- network: "download-only",
3858
- expose_ports: []
3859
- }),
3860
- sandbox_docker: z4.object({
3861
- image: z4.string().default("python:3.11-slim"),
3862
- workdir: z4.string().default("/workspace"),
3863
- pull: z4.boolean().default(false),
3864
- docker_bin: z4.string().default("docker"),
3865
- extra_run_args: z4.array(z4.string()).default([])
3866
- }).default({
3867
- image: "python:3.11-slim",
3868
- workdir: "/workspace",
3869
- pull: false,
3870
- docker_bin: "docker",
3871
- extra_run_args: []
3872
- })
3873
- })
4049
+ agent: AgentSchema
3874
4050
  }).transform(({ locale, ui_language, ...rest }) => ({
3875
4051
  locale: locale ?? ui_language ?? "en",
3876
4052
  ...rest
3877
4053
  }));
4054
+ function defaultSandboxLimits() {
4055
+ return {
4056
+ cpu: 1,
4057
+ memory_mb: 1024,
4058
+ wall_seconds: 60,
4059
+ network: "download-only",
4060
+ expose_ports: []
4061
+ };
4062
+ }
4063
+ function defaultLanguageSandbox(language, mode, limits) {
4064
+ return {
4065
+ mode,
4066
+ local: {
4067
+ sandbox_dir: `.sandbox/${language}`,
4068
+ limits: { ...limits, expose_ports: [...limits.expose_ports ?? []] }
4069
+ },
4070
+ docker: {
4071
+ image: language === "typescript" ? "node:24-slim" : "python:3.11-slim",
4072
+ workdir: "/workspace",
4073
+ pull: false,
4074
+ docker_bin: "docker",
4075
+ extra_run_args: [],
4076
+ sandbox_dir: `.sandbox/${language}`,
4077
+ limits: { ...limits, expose_ports: [...limits.expose_ports ?? []] }
4078
+ }
4079
+ };
4080
+ }
4081
+ function mergeLanguageSandbox(defaults, override, legacyDocker) {
4082
+ const dockerOverride = legacyDocker ?? override?.docker;
4083
+ return {
4084
+ mode: override?.mode ?? defaults.mode,
4085
+ local: {
4086
+ ...defaults.local,
4087
+ ...override?.local ?? {},
4088
+ limits: override?.local?.limits ?? defaults.local.limits
4089
+ },
4090
+ docker: {
4091
+ ...defaults.docker,
4092
+ ...dockerOverride ?? {},
4093
+ limits: dockerOverride?.limits ?? defaults.docker.limits
4094
+ }
4095
+ };
4096
+ }
3878
4097
  function getXCompilerPath() {
3879
4098
  const env = xcEnv("PATH");
3880
4099
  if (env && env.trim()) return path5.resolve(env.trim());
@@ -3896,7 +4115,7 @@ async function loadConfigWithPath(explicitPath) {
3896
4115
  for (const abs of candidates) {
3897
4116
  tried.push(abs);
3898
4117
  try {
3899
- const raw = await fs3.readFile(abs, "utf8");
4118
+ const raw = await fs4.readFile(abs, "utf8");
3900
4119
  const expanded = expandEnv(raw);
3901
4120
  const data = YAML.parse(expanded);
3902
4121
  return { config: ConfigSchema.parse(data), path: abs };
@@ -3938,6 +4157,11 @@ var LOOP_REPEATS = 12;
3938
4157
  var LOOP_MIN_LEN = 1500;
3939
4158
  var LOOP_MIN_WINDOW = 96;
3940
4159
  var LOOP_MAX_PERIOD = 256;
4160
+ var TEXT_LOOP_MIN_LEN = 6e3;
4161
+ var TEXT_LOOP_TAIL = 14e3;
4162
+ var TEXT_LOOP_SAMPLE_LEN = 48;
4163
+ var TEXT_LOOP_STRIDE = 8;
4164
+ var TEXT_LOOP_REPEATS = 5;
3941
4165
  function detectCyclicTokenLoop(aggregate) {
3942
4166
  if (aggregate.length < LOOP_MIN_LEN) return false;
3943
4167
  const maxPeriod = Math.min(LOOP_MAX_PERIOD, Math.floor(aggregate.length / LOOP_REPEATS));
@@ -3959,6 +4183,35 @@ function detectCyclicTokenLoop(aggregate) {
3959
4183
  }
3960
4184
  return false;
3961
4185
  }
4186
+ function detectRepeatedTextLoop(aggregate) {
4187
+ if (aggregate.length < TEXT_LOOP_MIN_LEN) return false;
4188
+ const tail = aggregate.slice(-TEXT_LOOP_TAIL).replace(/\s+/gu, " ").trim();
4189
+ if (tail.length < TEXT_LOOP_MIN_LEN) return false;
4190
+ const seen = /* @__PURE__ */ new Map();
4191
+ for (let i = 0; i + TEXT_LOOP_SAMPLE_LEN <= tail.length; i += TEXT_LOOP_STRIDE) {
4192
+ const sample = tail.slice(i, i + TEXT_LOOP_SAMPLE_LEN);
4193
+ if (!isHighSignalLoopSample(sample)) continue;
4194
+ const prior = seen.get(sample);
4195
+ if (!prior) {
4196
+ seen.set(sample, { count: 1, first: i, last: i });
4197
+ continue;
4198
+ }
4199
+ const next = { count: prior.count + 1, first: prior.first, last: i };
4200
+ seen.set(sample, next);
4201
+ if (next.count >= TEXT_LOOP_REPEATS && next.last - next.first >= TEXT_LOOP_SAMPLE_LEN * 3) {
4202
+ return true;
4203
+ }
4204
+ }
4205
+ return false;
4206
+ }
4207
+ function isHighSignalLoopSample(sample) {
4208
+ const chars = [...sample];
4209
+ const signalChars = chars.filter((ch) => /[\p{L}\p{N}]/u.test(ch));
4210
+ if (signalChars.length < 20) return false;
4211
+ if (signalChars.length / Math.max(1, chars.length) < 0.4) return false;
4212
+ const unique = new Set(signalChars.map((ch) => ch.toLowerCase())).size;
4213
+ return unique >= 10;
4214
+ }
3962
4215
  var RepeatTokenDetector = class {
3963
4216
  constructor(threshold = 40) {
3964
4217
  this.threshold = threshold;
@@ -4137,6 +4390,12 @@ function streamPostNdjson(url, body, watchdog, onLine, shouldStopWhen) {
4137
4390
  );
4138
4391
  return;
4139
4392
  }
4393
+ if (detectRepeatedTextLoop(aggregate)) {
4394
+ fail(
4395
+ new Error("detected repeated text loop in stream (semantic tail repetition); aborting")
4396
+ );
4397
+ return;
4398
+ }
4140
4399
  }
4141
4400
  if (obj.error) {
4142
4401
  fail(new Error(`Ollama error: ${obj.error}`));
@@ -4165,6 +4424,10 @@ function streamPostNdjson(url, body, watchdog, onLine, shouldStopWhen) {
4165
4424
  fail(new Error("detected token loop in stream; aborting"));
4166
4425
  return;
4167
4426
  }
4427
+ if (detectRepeatedTextLoop(aggregate)) {
4428
+ fail(new Error("detected repeated text loop in stream; aborting"));
4429
+ return;
4430
+ }
4168
4431
  }
4169
4432
  });
4170
4433
  res.on("end", () => {
@@ -4435,6 +4698,9 @@ var OpenAIClient = class {
4435
4698
  if (detectCyclicTokenLoop(aggregate)) {
4436
4699
  throw new Error("detected cyclic token loop in OpenAI stream (periodic tail); aborting");
4437
4700
  }
4701
+ if (detectRepeatedTextLoop(aggregate)) {
4702
+ throw new Error("detected repeated text loop in OpenAI stream (semantic tail repetition); aborting");
4703
+ }
4438
4704
  if (maxOutputChars > 0 && expectsJsonObject && aggregate.length > maxOutputChars && hasInvalidJsonPrefix(aggregate)) {
4439
4705
  throw new Error(`OpenAI stream exceeded ${maxOutputChars} chars without a valid JSON prefix; aborting`);
4440
4706
  }
@@ -4971,22 +5237,28 @@ async function reportRoleModelAdvice(router, audit, reporter = (message) => {
4971
5237
  }
4972
5238
 
4973
5239
  // src/llm/scores.ts
4974
- import { promises as fs4 } from "fs";
5240
+ import { promises as fs5 } from "fs";
4975
5241
  import path6 from "path";
4976
5242
  import YAML2 from "yaml";
4977
5243
  var CLUSTER_PROVIDER_TAG = "cluster";
5244
+ var LLM_DYNAMIC_SCORES_FILE = "llm_scores.yaml";
5245
+ var LLM_USER_SCORES_FILE = "llm_scores_user.yaml";
4978
5246
  var ScoreStore = class _ScoreStore {
4979
5247
  constructor(configPath, initial = {}, audit, opts = {}) {
4980
5248
  this.audit = audit;
4981
- this.sidecarPath = path6.join(path6.dirname(configPath), "llm_scores.yaml");
5249
+ const configDir = path6.dirname(configPath);
5250
+ this.sidecarPath = path6.join(configDir, LLM_DYNAMIC_SCORES_FILE);
5251
+ this.userScoresPath = path6.join(configDir, LLM_USER_SCORES_FILE);
4982
5252
  this.clusterProviders = new Set(opts.clusterProviderNames ?? []);
4983
5253
  const bounds = normalizeClusterBounds(opts.clusterScoreMin, opts.clusterScoreMax);
4984
5254
  this.clusterMin = bounds.min;
4985
5255
  this.clusterMax = bounds.max;
4986
5256
  for (const [k, v] of Object.entries(initial)) {
4987
- const score = this.clampConfigured(k, v);
4988
- this.scores.set(k, score);
4989
- if (score === _ScoreStore.DISABLED) this.userDisabled.add(k);
5257
+ if (v === _ScoreStore.DISABLED) {
5258
+ this.userScores.set(k, _ScoreStore.DISABLED);
5259
+ } else {
5260
+ this.dynamicScores.set(k, this.clampDynamic(k, v));
5261
+ }
4990
5262
  }
4991
5263
  }
4992
5264
  audit;
@@ -4998,24 +5270,28 @@ var ScoreStore = class _ScoreStore {
4998
5270
  static CLUSTER_MAX = 0.5;
4999
5271
  static DECAY = 0.5;
5000
5272
  static BOOST = 0.1;
5001
- scores = /* @__PURE__ */ new Map();
5002
- userDisabled = /* @__PURE__ */ new Set();
5273
+ dynamicScores = /* @__PURE__ */ new Map();
5274
+ userScores = /* @__PURE__ */ new Map();
5003
5275
  clusterProviders;
5004
5276
  clusterMin;
5005
5277
  clusterMax;
5006
5278
  dirty = false;
5007
5279
  writeQueue = Promise.resolve();
5008
5280
  sidecarPath;
5009
- /** 异步加载 sidecar 文件;失败/不存在不抛错,使用 ctor 提供的初值。 */
5281
+ userScoresPath;
5282
+ /** 异步加载动态 sidecar 与用户覆盖文件;失败/不存在不抛错,使用 ctor 提供的初值。 */
5010
5283
  async load() {
5284
+ await this.loadDynamicScores();
5285
+ await this.loadUserScores();
5286
+ }
5287
+ async loadDynamicScores() {
5011
5288
  try {
5012
- const raw = await fs4.readFile(this.sidecarPath, "utf8");
5289
+ const raw = await fs5.readFile(this.sidecarPath, "utf8");
5013
5290
  const parsed = YAML2.parse(raw);
5014
5291
  if (parsed && typeof parsed === "object") {
5015
5292
  for (const [k, v] of Object.entries(parsed)) {
5016
5293
  if (typeof v === "number" && Number.isFinite(v)) {
5017
- if (this.userDisabled.has(k)) continue;
5018
- this.scores.set(k, this.clampDynamic(k, v));
5294
+ this.dynamicScores.set(k, this.clampPersistedDynamic(k, v));
5019
5295
  }
5020
5296
  }
5021
5297
  }
@@ -5028,44 +5304,73 @@ var ScoreStore = class _ScoreStore {
5028
5304
  }
5029
5305
  }
5030
5306
  }
5307
+ async loadUserScores() {
5308
+ try {
5309
+ const raw = await fs5.readFile(this.userScoresPath, "utf8");
5310
+ const parsed = YAML2.parse(raw);
5311
+ if (parsed && typeof parsed === "object") {
5312
+ for (const [k, v] of Object.entries(parsed)) {
5313
+ if (typeof v === "number" && Number.isFinite(v)) {
5314
+ this.userScores.set(k, this.clampUserScore(v));
5315
+ }
5316
+ }
5317
+ }
5318
+ } catch (err) {
5319
+ if (err.code !== "ENOENT") {
5320
+ await this.audit?.event("llm.error", t().llm.scoreReadFailed(this.userScoresPath, err.message), {
5321
+ messageId: "llm.user_score_read_failed",
5322
+ sidecarPath: this.userScoresPath
5323
+ });
5324
+ }
5325
+ }
5326
+ }
5031
5327
  get(name) {
5032
- return this.scores.has(name) ? this.scores.get(name) : this.defaultFor(name);
5328
+ if (this.userScores.has(name)) return this.userScores.get(name);
5329
+ return this.dynamicScores.has(name) ? this.dynamicScores.get(name) : this.defaultFor(name);
5033
5330
  }
5034
- /** 主动设置评分;value=0 仅用于显式禁用,其它值会归一到动态范围。 */
5331
+ /** 主动设置动态评分;用户覆盖只从配置或 llm_scores_user.yaml 读取。 */
5035
5332
  set(name, value, reason) {
5036
- if (this.userDisabled.has(name) && value !== _ScoreStore.DISABLED) return;
5037
5333
  const v = value === _ScoreStore.DISABLED ? _ScoreStore.DISABLED : this.clampDynamic(name, value);
5038
- const prev = this.scores.get(name);
5334
+ const prev = this.dynamicScores.get(name);
5039
5335
  if (prev === v) return;
5040
- this.scores.set(name, v);
5336
+ this.dynamicScores.set(name, v);
5041
5337
  this.dirty = true;
5042
- void this.audit?.event("llm.score", t().llm.scoreChanged(name, v.toFixed(2), (prev ?? _ScoreStore.DEFAULT).toFixed(2)), {
5338
+ void this.audit?.event("llm.score", t().llm.scoreChanged(name, v.toFixed(2), (prev ?? this.defaultFor(name)).toFixed(2)), {
5043
5339
  messageId: "llm.score_changed",
5044
5340
  provider: name,
5045
5341
  score: v,
5046
- previous: prev ?? _ScoreStore.DEFAULT,
5342
+ previous: prev ?? this.defaultFor(name),
5047
5343
  reason: reason ?? "set"
5048
5344
  });
5049
5345
  this.scheduleSave();
5050
5346
  }
5051
5347
  decay(name, reason) {
5052
- if (this.get(name) === _ScoreStore.DISABLED) return;
5053
- this.set(name, Math.max(this.minFor(name), this.get(name) - _ScoreStore.DECAY), reason);
5348
+ if (this.isUserDisabled(name)) return;
5349
+ const cur = this.dynamicScore(name);
5350
+ if (cur === _ScoreStore.DISABLED) return;
5351
+ this.set(name, Math.max(this.minFor(name), cur - _ScoreStore.DECAY), reason);
5054
5352
  }
5055
5353
  boost(name, reason) {
5056
- const cur = this.get(name);
5354
+ if (this.isUserDisabled(name)) return;
5355
+ const cur = this.dynamicScore(name);
5057
5356
  if (cur === _ScoreStore.DISABLED) return;
5058
5357
  if (cur >= this.maxFor(name)) return;
5059
5358
  this.set(name, Math.min(this.maxFor(name), cur + _ScoreStore.BOOST), reason);
5060
5359
  }
5061
- /** True only for providers explicitly disabled by user config (`llm.scores.<provider>: 0`). */
5360
+ /** True only for providers explicitly disabled by user config or llm_scores_user.yaml. */
5062
5361
  isUserDisabled(name) {
5063
- return this.userDisabled.has(name);
5362
+ return this.userScores.get(name) === _ScoreStore.DISABLED;
5064
5363
  }
5065
- /** 全量快照(用于测试与 audit 输出)。 */
5364
+ /** 动态评分快照(用于持久化、测试与 audit 输出;不包含用户覆盖)。 */
5066
5365
  snapshot() {
5067
5366
  const out = {};
5068
- for (const [k, v] of this.scores) out[k] = v;
5367
+ for (const [k, v] of this.dynamicScores) out[k] = v;
5368
+ return out;
5369
+ }
5370
+ /** 用户覆盖快照(用于测试/诊断;运行时不会改写用户文件)。 */
5371
+ userSnapshot() {
5372
+ const out = {};
5373
+ for (const [k, v] of this.userScores) out[k] = v;
5069
5374
  return out;
5070
5375
  }
5071
5376
  /** 等待待写入完成(CLI 退出前调用,确保评分已落盘)。 */
@@ -5088,8 +5393,8 @@ var ScoreStore = class _ScoreStore {
5088
5393
  ${t().llm.scoreFileSemantics}
5089
5394
  ` + YAML2.stringify(data);
5090
5395
  const tmp = `${this.sidecarPath}.tmp`;
5091
- await fs4.writeFile(tmp, yaml, "utf8");
5092
- await fs4.rename(tmp, this.sidecarPath);
5396
+ await fs5.writeFile(tmp, yaml, "utf8");
5397
+ await fs5.rename(tmp, this.sidecarPath);
5093
5398
  }
5094
5399
  defaultFor(name) {
5095
5400
  return this.isClusterProvider(name) ? this.clusterMax : _ScoreStore.DEFAULT;
@@ -5103,10 +5408,8 @@ ${t().llm.scoreFileSemantics}
5103
5408
  isClusterProvider(name) {
5104
5409
  return this.clusterProviders.has(name);
5105
5410
  }
5106
- clampConfigured(name, v) {
5107
- if (!Number.isFinite(v)) return this.defaultFor(name);
5108
- if (v === _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5109
- return this.clampDynamic(name, v);
5411
+ dynamicScore(name) {
5412
+ return this.dynamicScores.has(name) ? this.dynamicScores.get(name) : this.defaultFor(name);
5110
5413
  }
5111
5414
  clampDynamic(name, v) {
5112
5415
  if (!Number.isFinite(v)) return this.defaultFor(name);
@@ -5115,6 +5418,18 @@ ${t().llm.scoreFileSemantics}
5115
5418
  if (v > this.maxFor(name)) return this.maxFor(name);
5116
5419
  return v;
5117
5420
  }
5421
+ clampPersistedDynamic(name, v) {
5422
+ if (!Number.isFinite(v)) return this.defaultFor(name);
5423
+ if (v === _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5424
+ return this.clampDynamic(name, v);
5425
+ }
5426
+ clampUserScore(v) {
5427
+ if (!Number.isFinite(v)) return _ScoreStore.DEFAULT;
5428
+ if (v <= _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5429
+ if (v < _ScoreStore.MIN) return _ScoreStore.MIN;
5430
+ if (v > _ScoreStore.MAX) return _ScoreStore.MAX;
5431
+ return v;
5432
+ }
5118
5433
  };
5119
5434
  function normalizeClusterBounds(rawMin, rawMax) {
5120
5435
  const min = clampBound(rawMin ?? ScoreStore.CLUSTER_MIN);
@@ -5294,7 +5609,7 @@ async function defaultFetchTags(baseUrl, timeoutMs) {
5294
5609
  }
5295
5610
 
5296
5611
  // src/workspace/workspace.ts
5297
- import { promises as fs5 } from "fs";
5612
+ import { promises as fs6 } from "fs";
5298
5613
  import path7 from "path";
5299
5614
  var Workspace = class {
5300
5615
  constructor(root) {
@@ -5305,31 +5620,31 @@ var Workspace = class {
5305
5620
  return path7.resolve(this.root, ...p);
5306
5621
  }
5307
5622
  async ensure(dir) {
5308
- await fs5.mkdir(this.abs(dir), { recursive: true });
5623
+ await fs6.mkdir(this.abs(dir), { recursive: true });
5309
5624
  }
5310
5625
  async writeFile(rel, content) {
5311
5626
  const full = this.abs(rel);
5312
- await fs5.mkdir(path7.dirname(full), { recursive: true });
5313
- await fs5.writeFile(full, content, "utf8");
5627
+ await fs6.mkdir(path7.dirname(full), { recursive: true });
5628
+ await fs6.writeFile(full, content, "utf8");
5314
5629
  }
5315
5630
  async readFile(rel) {
5316
- return fs5.readFile(this.abs(rel), "utf8");
5631
+ return fs6.readFile(this.abs(rel), "utf8");
5317
5632
  }
5318
5633
  async exists(rel) {
5319
5634
  try {
5320
- await fs5.stat(this.abs(rel));
5635
+ await fs6.stat(this.abs(rel));
5321
5636
  return true;
5322
5637
  } catch {
5323
5638
  return false;
5324
5639
  }
5325
5640
  }
5326
5641
  async remove(rel) {
5327
- await fs5.rm(this.abs(rel), { recursive: true, force: true });
5642
+ await fs6.rm(this.abs(rel), { recursive: true, force: true });
5328
5643
  }
5329
5644
  };
5330
5645
 
5331
5646
  // src/workspace/doc_archive.ts
5332
- import { promises as fs6 } from "fs";
5647
+ import { promises as fs7 } from "fs";
5333
5648
  import path8 from "path";
5334
5649
  async function archiveIfExists(ws, rel, audit) {
5335
5650
  const norm = rel.replaceAll("\\", "/");
@@ -5342,7 +5657,7 @@ async function archiveIfExists(ws, rel, audit) {
5342
5657
  const target = `docs/history/${base}-${ts}${ext}`;
5343
5658
  try {
5344
5659
  await ws.ensure("docs/history");
5345
- await fs6.rename(ws.abs(norm), ws.abs(target));
5660
+ await fs7.rename(ws.abs(norm), ws.abs(target));
5346
5661
  await audit?.event("plan.persist", t().audit.documentArchived(norm, target), {
5347
5662
  messageId: "audit.document_archived",
5348
5663
  from: norm,
@@ -5544,6 +5859,18 @@ var DOC_PATH_ALIASES = {
5544
5859
  "docs/integration-test.md": DOC_NAMES.integrationTest,
5545
5860
  "docs/module-test.md": DOC_NAMES.moduleTest,
5546
5861
  "docs/functional-test.md": DOC_NAMES.functionalTest,
5862
+ "docs/unit-test-plan.md": DOC_NAMES.unitTestPlan,
5863
+ "docs/unit_test_plan.md": DOC_NAMES.unitTestPlan,
5864
+ "docs/tests/unit_test_plan.md": DOC_NAMES.unitTestPlan,
5865
+ "docs/integration-test-plan.md": DOC_NAMES.integrationTestPlan,
5866
+ "docs/integration_test_plan.md": DOC_NAMES.integrationTestPlan,
5867
+ "docs/tests/integration_test_plan.md": DOC_NAMES.integrationTestPlan,
5868
+ "docs/module-test-plan.md": DOC_NAMES.moduleTestPlan,
5869
+ "docs/module_test_plan.md": DOC_NAMES.moduleTestPlan,
5870
+ "docs/tests/module_test_plan.md": DOC_NAMES.moduleTestPlan,
5871
+ "docs/functional-test-plan.md": DOC_NAMES.functionalTestPlan,
5872
+ "docs/functional_test_plan.md": DOC_NAMES.functionalTestPlan,
5873
+ "docs/tests/functional_test_plan.md": DOC_NAMES.functionalTestPlan,
5547
5874
  "docs/quick-start.md": DOC_NAMES.quickstart,
5548
5875
  "docs/quick_start.md": DOC_NAMES.quickstart,
5549
5876
  "docs/quickstart.md": DOC_NAMES.quickstart,
@@ -5551,13 +5878,25 @@ var DOC_PATH_ALIASES = {
5551
5878
  "docs/api_guide.md": DOC_NAMES.apiGuide,
5552
5879
  "docs/api-guide.md": DOC_NAMES.apiGuide
5553
5880
  };
5881
+ function canonicalTestPlanAlias(path32) {
5882
+ const normalized = path32.replace(/\\/g, "/");
5883
+ const match = normalized.match(
5884
+ /^docs\/(?:tests\/)?(?:\d{1,2}[-_])?(functional|integration|module|unit)[-_]?test[-_]?plan\.md$/iu
5885
+ );
5886
+ const kind = match?.[1]?.toLowerCase();
5887
+ if (kind === "functional") return DOC_NAMES.functionalTestPlan;
5888
+ if (kind === "integration") return DOC_NAMES.integrationTestPlan;
5889
+ if (kind === "module") return DOC_NAMES.moduleTestPlan;
5890
+ if (kind === "unit") return DOC_NAMES.unitTestPlan;
5891
+ return void 0;
5892
+ }
5554
5893
  function calibrateDocPaths(steps, projectType = "application") {
5555
- const remap = (p) => DOC_PATH_ALIASES[p] ?? p;
5894
+ const remap = (p) => DOC_PATH_ALIASES[p] ?? canonicalTestPlanAlias(p) ?? p;
5556
5895
  const dropTopic = (p) => p !== DOC_NAMES.topic;
5557
5896
  return steps.map((s) => {
5558
5897
  const iterationId = s.iterationId ?? "P1";
5559
- const inputs = (s.inputs ?? []).map((p) => iterationScopedInput(remap(p), s.phase, iterationId));
5560
- let outputs = (s.outputs ?? []).map((p) => iterationScopedDoc(remap(p), s.phase, iterationId)).filter(dropTopic);
5898
+ const inputs = dedup2((s.inputs ?? []).map((p) => iterationScopedInput(remap(p), s.phase, iterationId)));
5899
+ let outputs = dedup2((s.outputs ?? []).map((p) => iterationScopedDoc(remap(p), s.phase, iterationId)).filter(dropTopic));
5561
5900
  outputs = outputs.filter((out) => {
5562
5901
  const ownerPhase = testPlanOwnerPhase(out, iterationId);
5563
5902
  return !ownerPhase || ownerPhase === s.phase;
@@ -5601,6 +5940,31 @@ function calibrateVModelDependencies(steps) {
5601
5940
  }
5602
5941
  return out;
5603
5942
  }
5943
+ function calibrateArchitectureModuleDependencies(modules, dependencies) {
5944
+ const architectureModules = (modules ?? []).map((module) => ({
5945
+ ...module,
5946
+ dependencies: [...module.dependencies ?? []]
5947
+ }));
5948
+ const moduleIds = new Set(architectureModules.map((module) => module.id));
5949
+ const projectDependencies = [...dependencies ?? []];
5950
+ for (const module of architectureModules) {
5951
+ const internalDependencies = [];
5952
+ for (const rawDependency of module.dependencies) {
5953
+ const dependency = String(rawDependency ?? "").trim();
5954
+ if (!dependency) continue;
5955
+ if (moduleIds.has(dependency) || /^M\d{3,}$/u.test(dependency)) {
5956
+ internalDependencies.push(dependency);
5957
+ } else {
5958
+ projectDependencies.push(dependency);
5959
+ }
5960
+ }
5961
+ module.dependencies = dedup2(internalDependencies);
5962
+ }
5963
+ return {
5964
+ architectureModules,
5965
+ dependencies: dedup2(projectDependencies.map((dependency) => dependency.trim()).filter(Boolean))
5966
+ };
5967
+ }
5604
5968
  function calibrateLanguageStepOwnership(steps, args) {
5605
5969
  const profile = getLanguageProfile(args.language);
5606
5970
  const out = steps.map((step) => ({
@@ -5662,35 +6026,35 @@ function preferredTestOwnerPhase(output, modules) {
5662
6026
  function findIterationStep(steps, iterationId, phase) {
5663
6027
  return steps.find((step) => (step.iterationId ?? "P1") === iterationId && step.phase === phase);
5664
6028
  }
5665
- function testPlanOwnerPhase(path31, iterationId) {
6029
+ function testPlanOwnerPhase(path32, iterationId) {
5666
6030
  for (const [sourcePhase, testPhase] of Object.entries(V_MODEL_SOURCE_TO_TEST_PHASE)) {
5667
- if (path31 === testPlanDocForIteration(testPhase, iterationId)) {
6031
+ if (path32 === testPlanDocForIteration(testPhase, iterationId)) {
5668
6032
  return sourcePhase;
5669
6033
  }
5670
6034
  }
5671
6035
  return void 0;
5672
6036
  }
5673
- function iterationScopedDoc(path31, phase, iterationId) {
5674
- if (iterationId === "P1") return path31;
5675
- if (path31 === DOC_NAMES.readme && phase === "FUNCTIONAL_TEST") {
6037
+ function iterationScopedDoc(path32, phase, iterationId) {
6038
+ if (iterationId === "P1") return path32;
6039
+ if (path32 === DOC_NAMES.readme && phase === "FUNCTIONAL_TEST") {
5676
6040
  return `docs/iterations/${iterationId}/README.md`;
5677
6041
  }
5678
- if (path31 === DOC_NAMES.quickstart && phase === "FUNCTIONAL_TEST") {
6042
+ if (path32 === DOC_NAMES.quickstart && phase === "FUNCTIONAL_TEST") {
5679
6043
  return `docs/iterations/${iterationId}/quickstart.md`;
5680
6044
  }
5681
- if (path31 === DOC_NAMES.apiGuide && phase === "FUNCTIONAL_TEST") {
6045
+ if (path32 === DOC_NAMES.apiGuide && phase === "FUNCTIONAL_TEST") {
5682
6046
  return `docs/iterations/${iterationId}/api-guide.md`;
5683
6047
  }
5684
6048
  for (const [docPhase, canonical] of Object.entries(PHASE_DOC)) {
5685
- if (path31 === canonical) {
5686
- return phaseDocForIteration(docPhase, iterationId) ?? path31;
6049
+ if (path32 === canonical) {
6050
+ return phaseDocForIteration(docPhase, iterationId) ?? path32;
5687
6051
  }
5688
6052
  }
5689
- return path31;
6053
+ return path32;
5690
6054
  }
5691
- function iterationScopedInput(path31, phase, iterationId) {
5692
- if (iterationId === "P1" || phase === "REQUIREMENT_ANALYSIS") return path31;
5693
- return iterationScopedDoc(path31, phase, iterationId);
6055
+ function iterationScopedInput(path32, phase, iterationId) {
6056
+ if (iterationId === "P1" || phase === "REQUIREMENT_ANALYSIS") return path32;
6057
+ return iterationScopedDoc(path32, phase, iterationId);
5694
6058
  }
5695
6059
  function calibrateStepIds(steps) {
5696
6060
  const map = /* @__PURE__ */ new Map();
@@ -6021,8 +6385,8 @@ function dedupModules(modules) {
6021
6385
  }
6022
6386
  return out;
6023
6387
  }
6024
- function isTestImplementationPath(path31) {
6025
- return /^tests\/.+\.(?:py|ts|tsx)$/i.test(path31);
6388
+ function isTestImplementationPath(path32) {
6389
+ return /^tests\/.+\.(?:py|ts|tsx)$/i.test(path32);
6026
6390
  }
6027
6391
  function isNonModuleTestPhase(phase) {
6028
6392
  return phase === "UNIT_TEST" || phase === "INTEGRATION_TEST" || phase === "FUNCTIONAL_TEST";
@@ -6045,7 +6409,7 @@ function testPathPrefixForPhase(phase) {
6045
6409
  return "unit";
6046
6410
  }
6047
6411
  function inferUnitTestExtension(modules) {
6048
- return modules.some((module) => module.testPaths.some((path31) => /\.tsx?$/i.test(path31))) ? ".test.ts" : ".py";
6412
+ return modules.some((module) => module.testPaths.some((path32) => /\.tsx?$/i.test(path32))) ? ".test.ts" : ".py";
6049
6413
  }
6050
6414
  function collectTransitiveDependencyIds(step, stepById) {
6051
6415
  const seen = /* @__PURE__ */ new Set();
@@ -6545,7 +6909,8 @@ var Planner = class {
6545
6909
  intent: opts.intent ?? "greenfield",
6546
6910
  hasBaseline: !!opts.hasBaseline,
6547
6911
  complex: demand.nonTrivial,
6548
- projectShapeAmbiguous
6912
+ projectShapeAmbiguous,
6913
+ languageAmbiguous: !!opts.languageAmbiguous
6549
6914
  });
6550
6915
  const rep = makeStreamReporter("Planner.clarify", this.llm.name);
6551
6916
  let provider;
@@ -6569,7 +6934,8 @@ var Planner = class {
6569
6934
  // 在 provider fallback 层校验问题集质量,避免“只有两三个泛泛问题”直接进入 Gate 1。
6570
6935
  validate: (t2) => validateClarifyJson(t2, demand.nonTrivial, {
6571
6936
  projectShapeAmbiguous,
6572
- externalApiRequired
6937
+ externalApiRequired,
6938
+ languageAmbiguous: !!opts.languageAmbiguous
6573
6939
  })
6574
6940
  }
6575
6941
  );
@@ -6640,14 +7006,14 @@ ${t().prompts.plannerSelfMode}` : "")
6640
7006
  },
6641
7007
  { role: "user", content: prompt + feedback }
6642
7008
  ],
6643
- validate: (t2) => parsePhaseStepPlanJson(t2, parseContext, phasePlan, currentPhase.id)
7009
+ validate: (t2) => parsePhaseStepPlanJson(t2, parseContext, phasePlan, currentPhase)
6644
7010
  });
6645
7011
  await this.audit?.plannerThought("decompose", text, {
6646
7012
  qaCount: input.clarifications.length,
6647
7013
  provider,
6648
7014
  phaseId: currentPhase.id
6649
7015
  });
6650
- return parsePhaseStepPlanJson(text, parseContext, phasePlan, currentPhase.id);
7016
+ return parsePhaseStepPlanJson(text, parseContext, phasePlan, currentPhase);
6651
7017
  }
6652
7018
  async chatWithStructuredValidationRetry(input) {
6653
7019
  const maxAttempts = plannerStructuredRepairAttemptLimit(input.context);
@@ -6726,7 +7092,7 @@ function formatPlannerValidationFeedback(err) {
6726
7092
  function isPlannerStructuredValidationError(err) {
6727
7093
  const message = errorMessage2(err);
6728
7094
  if (isPlannerTransportFailure(message)) return false;
6729
- return /^Planner (?:architecture|phase|JSON|draft|complexityAssessment|implementationPhases|iteration)/u.test(message);
7095
+ return /^Planner (?:architecture|phase|PhasePlan|JSON|draft|complexityAssessment|implementationPhases|iteration)/u.test(message);
6730
7096
  }
6731
7097
  function isPlannerTransportFailure(message) {
6732
7098
  const text = message.toLowerCase();
@@ -6767,6 +7133,12 @@ function buildPlan(draft, opts = {}) {
6767
7133
  draft.requirementDigest
6768
7134
  );
6769
7135
  const phaseId = implementationPhases.find((phase) => phase.status === "current")?.id ?? draft.steps.find((step) => step.iterationId)?.iterationId ?? "P1";
7136
+ const architectureDependencyCalibration = calibrateArchitectureModuleDependencies(
7137
+ draft.architectureModules ?? [],
7138
+ draft.dependencies
7139
+ );
7140
+ const architectureModules = architectureDependencyCalibration.architectureModules;
7141
+ const draftDependencies = architectureDependencyCalibration.dependencies;
6770
7142
  const iterated = normalizeStepIterations(draft.steps, implementationPhases);
6771
7143
  const shaped = calibrateLanguageStepOwnership(
6772
7144
  calibrateVModelDependencies(
@@ -6775,14 +7147,14 @@ function buildPlan(draft, opts = {}) {
6775
7147
  {
6776
7148
  language,
6777
7149
  intent: opts.intent ?? "greenfield",
6778
- architectureModules: draft.architectureModules ?? []
7150
+ architectureModules
6779
7151
  }
6780
7152
  );
6781
- const mapped = calibrateArchitectureStepMappings(shaped, draft.architectureModules ?? []);
6782
- const contracted = injectArchitectureContractPrompts(mapped, draft.architectureModules ?? []);
7153
+ const mapped = calibrateArchitectureStepMappings(shaped, architectureModules);
7154
+ const contracted = injectArchitectureContractPrompts(mapped, architectureModules);
6783
7155
  const languageContracted = injectLanguageContractPrompts(contracted, language);
6784
7156
  const steps = calibratePlanCoverage(languageContracted, language);
6785
- const dependencies = language === "python" ? calibratePythonRequirements(draft.dependencies) : [...new Set((draft.dependencies ?? []).map((d) => d.trim()).filter(Boolean))];
7157
+ const dependencies = language === "python" ? calibratePythonRequirements(draftDependencies) : [...new Set((draftDependencies ?? []).map((d) => d.trim()).filter(Boolean))];
6786
7158
  return {
6787
7159
  version: "1",
6788
7160
  language,
@@ -6792,7 +7164,7 @@ function buildPlan(draft, opts = {}) {
6792
7164
  requirementDigest: draft.requirementDigest,
6793
7165
  complexityAssessment,
6794
7166
  implementationPhases,
6795
- architectureModules: draft.architectureModules,
7167
+ architectureModules,
6796
7168
  globalPrompt: draft.globalPrompt,
6797
7169
  baselineSummary: opts.baselineSummary ?? "",
6798
7170
  dependencies,
@@ -7002,6 +7374,11 @@ function validateClarifyJson(text, complex, opts = {}) {
7002
7374
  "clarify missing required external API credential question: ask whether the user has an API/key/token; if not, default to open no-key APIs"
7003
7375
  );
7004
7376
  }
7377
+ if (opts.languageAmbiguous && !parsed.some(isDevelopmentLanguageClarification)) {
7378
+ throw new Error(
7379
+ "clarify missing required development language question: ask whether the project should use Python or TypeScript/Node.js, with Python as the default option"
7380
+ );
7381
+ }
7005
7382
  return parsed;
7006
7383
  }
7007
7384
  function parsePhasePlanJson(text, context) {
@@ -7069,7 +7446,7 @@ function parsePhasePlanJson(text, context) {
7069
7446
  implementationPhases
7070
7447
  };
7071
7448
  }
7072
- function parsePhaseStepPlanJson(text, context, phasePlan, currentIterationId) {
7449
+ function parsePhaseStepPlanJson(text, context, phasePlan, currentPhase) {
7073
7450
  const data = safeJson(text);
7074
7451
  if (!data || typeof data !== "object") {
7075
7452
  throw new Error("Planner did not return a JSON object for phase Step planning.");
@@ -7077,7 +7454,7 @@ function parsePhaseStepPlanJson(text, context, phasePlan, currentIterationId) {
7077
7454
  const obj = data;
7078
7455
  const rawDeps = Array.isArray(obj.dependencies) ? obj.dependencies : Array.isArray(obj.pythonRequirements) ? obj.pythonRequirements : [];
7079
7456
  const draft = {
7080
- requirementDigest: typeof obj.requirementDigest === "string" && obj.requirementDigest.trim() ? obj.requirementDigest : phasePlan.requirementDigest,
7457
+ requirementDigest: typeof obj.requirementDigest === "string" && obj.requirementDigest.trim() ? obj.requirementDigest : currentPhase.objective || phasePlan.requirementDigest,
7081
7458
  globalPrompt: typeof obj.globalPrompt === "string" && obj.globalPrompt.trim() ? obj.globalPrompt : phasePlan.globalPrompt,
7082
7459
  projectType: phasePlan.projectType,
7083
7460
  complexityAssessment: phasePlan.complexityAssessment,
@@ -7086,7 +7463,11 @@ function parsePhaseStepPlanJson(text, context, phasePlan, currentIterationId) {
7086
7463
  architectureModules: obj.architectureModules,
7087
7464
  steps: obj.steps
7088
7465
  };
7089
- const parsed = parseDraftPlanJson(JSON.stringify(draft), context);
7466
+ const parsed = parseDraftPlanJson(JSON.stringify(draft), {
7467
+ ...context,
7468
+ phaseDemandText: phaseDemandText(currentPhase)
7469
+ });
7470
+ const currentIterationId = currentPhase.id;
7090
7471
  const wrongIteration = parsed.steps.find((step) => (step.iterationId ?? "P1") !== currentIterationId);
7091
7472
  if (wrongIteration) {
7092
7473
  throw new Error(
@@ -7121,7 +7502,7 @@ function parseDraftPlanJson(text, context) {
7121
7502
  context?.baselineSummary ?? ""
7122
7503
  ].join("\n"));
7123
7504
  const rawDeps = Array.isArray(obj.dependencies) ? obj.dependencies : Array.isArray(obj.pythonRequirements) ? obj.pythonRequirements : [];
7124
- const dependencies = rawDeps.filter((s) => typeof s === "string");
7505
+ let dependencies = rawDeps.filter((s) => typeof s === "string");
7125
7506
  if (context) {
7126
7507
  const validPhaseNames = new Set(PHASES);
7127
7508
  const nonCanonical = steps.map((rawStep, index) => {
@@ -7154,7 +7535,12 @@ function parseDraftPlanJson(text, context) {
7154
7535
  if (!architectureResult.success) {
7155
7536
  throw new Error(`Planner architectureModules invalid: ${architectureResult.error.issues.map((i) => i.message).join("; ")}`);
7156
7537
  }
7157
- const architectureModules = architectureResult.data;
7538
+ const architectureDependencyCalibration = calibrateArchitectureModuleDependencies(
7539
+ architectureResult.data,
7540
+ dependencies
7541
+ );
7542
+ const architectureModules = architectureDependencyCalibration.architectureModules;
7543
+ dependencies = architectureDependencyCalibration.dependencies;
7158
7544
  const parsedComplexityAssessment = parseComplexityAssessment(obj.complexityAssessment);
7159
7545
  if (context && !parsedComplexityAssessment) {
7160
7546
  throw new Error("Planner JSON missing valid complexityAssessment; complexity must be assessed during plan decomposition.");
@@ -7188,14 +7574,7 @@ function parseDraftPlanJson(text, context) {
7188
7574
  }
7189
7575
  if (context) {
7190
7576
  const demand = analyzeArchitectureDemand(
7191
- {
7192
- requirementDigest: digest,
7193
- rawRequirement: context.rawRequirement,
7194
- userAddenda: context.userAddenda,
7195
- globalPrompt,
7196
- baselineSummary: context.baselineSummary,
7197
- intent: context.intent
7198
- },
7577
+ architectureDemandInputForDraft(context, digest, globalPrompt),
7199
7578
  context.language
7200
7579
  );
7201
7580
  const forcedPhaseSplit = hasForcedPhaseSplit([
@@ -7245,6 +7624,34 @@ function parseDraftPlanJson(text, context) {
7245
7624
  steps: stepsWithIterations
7246
7625
  };
7247
7626
  }
7627
+ function architectureDemandInputForDraft(context, digest, globalPrompt) {
7628
+ if (!context.phaseDemandText) {
7629
+ return {
7630
+ requirementDigest: digest,
7631
+ rawRequirement: context.rawRequirement,
7632
+ userAddenda: context.userAddenda,
7633
+ globalPrompt,
7634
+ baselineSummary: context.baselineSummary,
7635
+ intent: context.intent
7636
+ };
7637
+ }
7638
+ return {
7639
+ requirementDigest: context.phaseDemandText,
7640
+ baselineSummary: context.baselineSummary,
7641
+ intent: context.intent
7642
+ };
7643
+ }
7644
+ function phaseDemandText(currentPhase) {
7645
+ const gate = currentPhase.verificationGate;
7646
+ return [
7647
+ currentPhase.title,
7648
+ currentPhase.objective,
7649
+ currentPhase.scope.join("\n"),
7650
+ currentPhase.deliverables.join("\n"),
7651
+ gate?.summary ?? "",
7652
+ gate?.checks.join("\n") ?? ""
7653
+ ].map((part) => part.trim()).filter(Boolean).join("\n");
7654
+ }
7248
7655
  function parseComplexityAssessment(value) {
7249
7656
  const result = ComplexityAssessmentSchema.safeParse(value);
7250
7657
  return result.success ? result.data : void 0;
@@ -7331,6 +7738,15 @@ ${question.options.map((option) => option.answer).join("\n")}`.toLowerCase();
7331
7738
  const externalApiContext = /\b(api|url|endpoint|provider|external|third[- ]party|fetch|request)\b/u.test(text) || /外部|第三方|接口|数据源|天气|节假日|联网/u.test(text);
7332
7739
  return externalApiContext && asksCredential && mentionsNoKeyFallback;
7333
7740
  }
7741
+ function isDevelopmentLanguageClarification(question) {
7742
+ const text = `${question.question}
7743
+ ${question.why}
7744
+ ${question.options.map((option) => option.answer).join("\n")}`.toLowerCase();
7745
+ const mentionsPython = /\bpython\b/u.test(text) || /python\s*脚本|python\s*项目/u.test(text);
7746
+ const mentionsTypeScript = /\btypescript\b|\btype\s*script\b|\bnode(?:\.js)?\b/u.test(text) || /type\s*script|ts\s*(程序|工程|项目|脚本|语言|实现)|typescript\s*项目/u.test(text);
7747
+ const asksLanguage = /\b(language|runtime|implementation stack|programming language)\b/u.test(text) || /开发语言|编程语言|实现语言|运行时|技术栈/u.test(text);
7748
+ return asksLanguage && mentionsPython && mentionsTypeScript;
7749
+ }
7334
7750
  function projectShapeSignals(text) {
7335
7751
  const lower = text.toLowerCase();
7336
7752
  const libraryLike = /\b(api[- ]?library|library|sdk|package|npm package|pypi package|client library|api client|reusable module|public api)\b/u.test(lower) || /api\s*(库|客户端)|公共\s*api|可复用接口|公共库|库项目|软件包|客户端库|开发包/u.test(text);
@@ -7458,11 +7874,11 @@ ${text.slice(0, 500)}`);
7458
7874
  }
7459
7875
 
7460
7876
  // src/core/incremental.ts
7461
- import { promises as fs8 } from "fs";
7877
+ import { promises as fs9 } from "fs";
7462
7878
  import path10 from "path";
7463
7879
 
7464
7880
  // src/core/project_memory.ts
7465
- import { promises as fs7 } from "fs";
7881
+ import { promises as fs8 } from "fs";
7466
7882
  import path9 from "path";
7467
7883
  var PROJECT_MEMORY_PATH = ".xcompiler/project_memory.json";
7468
7884
  async function refreshProjectMemory(ws, opts = {}) {
@@ -7620,7 +8036,7 @@ async function defaultPlanMetadataPath(root) {
7620
8036
  }
7621
8037
  async function fileExists(filePath) {
7622
8038
  try {
7623
- await fs7.stat(filePath);
8039
+ await fs8.stat(filePath);
7624
8040
  return true;
7625
8041
  } catch {
7626
8042
  return false;
@@ -7648,7 +8064,7 @@ async function readWorkspaceText(ws, rel, maxChars) {
7648
8064
  }
7649
8065
  async function readAbsoluteText(fullPath, maxChars) {
7650
8066
  try {
7651
- const raw = await fs7.readFile(fullPath, "utf8");
8067
+ const raw = await fs8.readFile(fullPath, "utf8");
7652
8068
  return truncate(raw, maxChars);
7653
8069
  } catch {
7654
8070
  return "";
@@ -7670,7 +8086,7 @@ async function walk(abs, rel, out, depth) {
7670
8086
  if (depth > 6 || out.length >= 160) return;
7671
8087
  let entries;
7672
8088
  try {
7673
- entries = await fs7.readdir(abs, { withFileTypes: true });
8089
+ entries = await fs8.readdir(abs, { withFileTypes: true });
7674
8090
  } catch {
7675
8091
  return;
7676
8092
  }
@@ -7897,20 +8313,20 @@ function extractImportedStems(text) {
7897
8313
  function normalizeImportStem(value) {
7898
8314
  return value.replace(/^\.\//u, "").replace(/^\.\.\//u, "").replace(/\.(js|ts|tsx|py)$/u, "").replace(/\/index$/u, "").trim();
7899
8315
  }
7900
- function extractContractsFromDoc(path31, text) {
8316
+ function extractContractsFromDoc(path32, text) {
7901
8317
  const contracts = [];
7902
8318
  for (const rawLine of text.split("\n")) {
7903
8319
  const line = rawLine.replace(/^[-*#\d.\s]+/u, "").trim();
7904
8320
  if (line.length < 18) continue;
7905
8321
  const lower = line.toLowerCase();
7906
8322
  if (INVARIANT_RE.test(lower)) {
7907
- contracts.push({ kind: "invariant", subject: path31, path: path31, detail: trimContractLine(line) });
8323
+ contracts.push({ kind: "invariant", subject: path32, path: path32, detail: trimContractLine(line) });
7908
8324
  } else if (LIMITATION_RE.test(lower)) {
7909
- contracts.push({ kind: "limitation", subject: path31, path: path31, detail: trimContractLine(line) });
8325
+ contracts.push({ kind: "limitation", subject: path32, path: path32, detail: trimContractLine(line) });
7910
8326
  } else if (EXTENSION_RE.test(lower)) {
7911
- contracts.push({ kind: "extension-point", subject: path31, path: path31, detail: trimContractLine(line) });
8327
+ contracts.push({ kind: "extension-point", subject: path32, path: path32, detail: trimContractLine(line) });
7912
8328
  } else if (INTEGRATION_RE.test(lower)) {
7913
- contracts.push({ kind: "integration", subject: path31, path: path31, detail: trimContractLine(line) });
8329
+ contracts.push({ kind: "integration", subject: path32, path: path32, detail: trimContractLine(line) });
7914
8330
  }
7915
8331
  }
7916
8332
  return contracts;
@@ -8009,7 +8425,7 @@ ${sec.slice(0, remain)}
8009
8425
  }
8010
8426
  async function loadPlanSummary(_ws, planPath, label) {
8011
8427
  try {
8012
- const raw = await fs8.readFile(planPath, "utf8");
8428
+ const raw = await fs9.readFile(planPath, "utf8");
8013
8429
  const json = JSON.parse(raw);
8014
8430
  const phasePlanParsed = PhasePlanSchema.safeParse(json);
8015
8431
  if (phasePlanParsed.success) {
@@ -8077,7 +8493,7 @@ async function defaultBaselinePlanPath(root) {
8077
8493
  }
8078
8494
  async function fileExists2(filePath) {
8079
8495
  try {
8080
- await fs8.stat(filePath);
8496
+ await fs9.stat(filePath);
8081
8497
  return true;
8082
8498
  } catch {
8083
8499
  return false;
@@ -8141,7 +8557,7 @@ async function walk2(abs, rel, out, depth) {
8141
8557
  if (depth > 6 || out.length >= 120) return;
8142
8558
  let entries;
8143
8559
  try {
8144
- entries = await fs8.readdir(abs, { withFileTypes: true });
8560
+ entries = await fs9.readdir(abs, { withFileTypes: true });
8145
8561
  } catch {
8146
8562
  return;
8147
8563
  }
@@ -8319,7 +8735,7 @@ function renderSubTasks(tasks, depth) {
8319
8735
  }
8320
8736
 
8321
8737
  // src/audit/audit.ts
8322
- import { promises as fs9, appendFileSync, mkdirSync, existsSync, writeFileSync } from "fs";
8738
+ import { promises as fs10, appendFileSync, mkdirSync, existsSync, writeFileSync } from "fs";
8323
8739
  import { createHash } from "crypto";
8324
8740
  import path11 from "path";
8325
8741
  var AuditLogger = class {
@@ -8476,8 +8892,8 @@ var AuditLogger = class {
8476
8892
  });
8477
8893
  }
8478
8894
  /**
8479
- * 记录一轮 Executor 思考:thoughts 文本、计划调用的 actions、是否完成。
8480
- * 写入 jsonl + markdown 折叠块,交付时可作为"AI 思考过程完整记录"。
8895
+ * 记录一轮 Executor 执行摘要:简短 intent、issue 处理方案、计划调用的 actions、是否完成。
8896
+ * 写入 jsonl + markdown 折叠块,交付时可追溯每轮决策和动作。
8481
8897
  */
8482
8898
  async executorTurn(stepId, role, round, payload) {
8483
8899
  const storedPayload = protectAuditContent(payload, this.contentMode);
@@ -8549,8 +8965,8 @@ ${t().audit.processLogPreamble}
8549
8965
  }
8550
8966
  async appendMd(text) {
8551
8967
  this.mdQueue = this.mdQueue.then(
8552
- () => fs9.appendFile(this.mdAbs, text, "utf8"),
8553
- () => fs9.appendFile(this.mdAbs, text, "utf8")
8968
+ () => fs10.appendFile(this.mdAbs, text, "utf8"),
8969
+ () => fs10.appendFile(this.mdAbs, text, "utf8")
8554
8970
  ).catch((err) => {
8555
8971
  console.warn(t().audit.markdownAppendFailed(err.message));
8556
8972
  });
@@ -8632,7 +9048,7 @@ function redactText(value) {
8632
9048
  }
8633
9049
 
8634
9050
  // src/core/lock.ts
8635
- import { promises as fs10, unlinkSync } from "fs";
9051
+ import { promises as fs11, unlinkSync } from "fs";
8636
9052
  import path12 from "path";
8637
9053
  import os from "os";
8638
9054
  var LockError = class extends Error {
@@ -8659,7 +9075,7 @@ function isAlive(pid) {
8659
9075
  }
8660
9076
  async function readLock(file) {
8661
9077
  try {
8662
- const raw = await fs10.readFile(file, "utf8");
9078
+ const raw = await fs11.readFile(file, "utf8");
8663
9079
  return JSON.parse(raw);
8664
9080
  } catch {
8665
9081
  return null;
@@ -8667,7 +9083,7 @@ async function readLock(file) {
8667
9083
  }
8668
9084
  async function acquireLock(workspace, command, options = {}) {
8669
9085
  const file = lockPath(workspace);
8670
- await fs10.mkdir(path12.dirname(file), { recursive: true });
9086
+ await fs11.mkdir(path12.dirname(file), { recursive: true });
8671
9087
  const info = {
8672
9088
  pid: process.pid,
8673
9089
  host: os.hostname(),
@@ -8677,7 +9093,7 @@ async function acquireLock(workspace, command, options = {}) {
8677
9093
  const payload = JSON.stringify(info, null, 2);
8678
9094
  const tryCreate = async () => {
8679
9095
  try {
8680
- const fh = await fs10.open(file, "wx");
9096
+ const fh = await fs11.open(file, "wx");
8681
9097
  await fh.writeFile(payload, "utf8");
8682
9098
  await fh.close();
8683
9099
  return true;
@@ -8690,14 +9106,14 @@ async function acquireLock(workspace, command, options = {}) {
8690
9106
  return makeReleaser(file);
8691
9107
  }
8692
9108
  if (options.force) {
8693
- await fs10.writeFile(file, payload, "utf8");
9109
+ await fs11.writeFile(file, payload, "utf8");
8694
9110
  return makeReleaser(file);
8695
9111
  }
8696
9112
  const existing = await readLock(file);
8697
9113
  if (existing) {
8698
9114
  const sameHost = existing.host === info.host;
8699
9115
  if (sameHost && !isAlive(existing.pid)) {
8700
- await fs10.writeFile(file, payload, "utf8");
9116
+ await fs11.writeFile(file, payload, "utf8");
8701
9117
  return makeReleaser(file);
8702
9118
  }
8703
9119
  throw new LockError(
@@ -8706,7 +9122,7 @@ async function acquireLock(workspace, command, options = {}) {
8706
9122
  existing
8707
9123
  );
8708
9124
  }
8709
- await fs10.writeFile(file, payload, "utf8");
9125
+ await fs11.writeFile(file, payload, "utf8");
8710
9126
  return makeReleaser(file);
8711
9127
  }
8712
9128
  function makeReleaser(file) {
@@ -8715,7 +9131,7 @@ function makeReleaser(file) {
8715
9131
  if (released) return;
8716
9132
  released = true;
8717
9133
  try {
8718
- await fs10.unlink(file);
9134
+ await fs11.unlink(file);
8719
9135
  } catch {
8720
9136
  }
8721
9137
  };
@@ -9079,8 +9495,51 @@ function resolveClarificationAnswer(q, rawAnswer) {
9079
9495
  }
9080
9496
  return answer;
9081
9497
  }
9082
- function resolveCompileLanguage(configuredLanguage, intent, baseline) {
9083
- return isIncrementalIntent(intent) ? baseline.language ?? configuredLanguage : configuredLanguage;
9498
+ function resolveCompileLanguage(inputOrConfigured, intent, baseline) {
9499
+ if (typeof inputOrConfigured === "string") {
9500
+ return isIncrementalIntent(intent ?? "greenfield") ? baseline?.language ?? inputOrConfigured : inputOrConfigured;
9501
+ }
9502
+ const input = inputOrConfigured;
9503
+ if (isIncrementalIntent(input.intent ?? "greenfield") && input.baseline?.language) {
9504
+ return { language: input.baseline.language, source: "baseline", ambiguous: false };
9505
+ }
9506
+ const topicInferred = inferCompileLanguageFromText(input.rawRequirement);
9507
+ if (topicInferred) {
9508
+ return {
9509
+ language: topicInferred,
9510
+ source: "topic",
9511
+ ambiguous: false
9512
+ };
9513
+ }
9514
+ const clarificationText = formatLanguageClarificationText(input.clarifications ?? []);
9515
+ const clarifiedInferred = inferCompileLanguageFromText([
9516
+ clarificationText,
9517
+ input.userAddenda ?? ""
9518
+ ].join("\n"));
9519
+ if (clarifiedInferred) {
9520
+ return { language: clarifiedInferred, source: "clarification", ambiguous: false };
9521
+ }
9522
+ return { language: "python", source: "default", ambiguous: true };
9523
+ }
9524
+ function inferCompileLanguageFromText(text) {
9525
+ const normalized = text.toLowerCase().replace(/[,。;:、()【】]/gu, " ").replace(/\s+/gu, " ");
9526
+ const pythonStrong = /\bpython\b/u.test(normalized) || /python\s*脚本/u.test(normalized) || /\.py\b/u.test(normalized) || /\bpytest\b/u.test(normalized) || /\bpip\b/u.test(normalized);
9527
+ const typescriptStrong = /\btypescript\b/u.test(normalized) || /\btype\s*script\b/u.test(normalized) || /(^|[^a-z0-9])ts\s*(程序|工程|项目|脚本|语言|实现)/u.test(normalized) || /\.tsx?\b/u.test(normalized) || /\btsx\b/u.test(normalized);
9528
+ if (pythonStrong && !typescriptStrong) return "python";
9529
+ if (typescriptStrong && !pythonStrong) return "typescript";
9530
+ const pythonWeak = /\bopenpyxl\b/u.test(normalized) || /\bpandas\b/u.test(normalized) || /\bfastapi\b/u.test(normalized) || /\bflask\b/u.test(normalized);
9531
+ const typescriptWeak = /\bnode(?:\.js)?\b/u.test(normalized) || /\bnpm\b/u.test(normalized) || /\bvitest\b/u.test(normalized) || /\bpackage\.json\b/u.test(normalized) || /\bjavascript\b/u.test(normalized) || /\bjs\s*(程序|工程|项目|脚本|语言|实现)\b/u.test(normalized);
9532
+ if ((pythonStrong || pythonWeak) && !(typescriptStrong || typescriptWeak)) return "python";
9533
+ if ((typescriptStrong || typescriptWeak) && !(pythonStrong || pythonWeak)) return "typescript";
9534
+ return void 0;
9535
+ }
9536
+ function formatLanguageClarificationText(input) {
9537
+ return input.map((item) => [
9538
+ item.question,
9539
+ item.answer,
9540
+ item.why ?? "",
9541
+ ...(item.options ?? []).map((option) => option.answer)
9542
+ ].join("\n")).join("\n\n");
9084
9543
  }
9085
9544
  async function runCompile(opts) {
9086
9545
  const io = opts.io ?? silentRuntimeIO;
@@ -9157,15 +9616,7 @@ async function runCompile(opts) {
9157
9616
  if (baseline.summary) {
9158
9617
  await runtimeLog(io, "success", M.compile.baselineLoaded(intent, baseline.sources.join(", ")));
9159
9618
  }
9160
- const language = resolveCompileLanguage(cfg.agent.language, intent, baseline);
9161
- if (isIncrementalIntent(intent) && baseline.language && baseline.language !== cfg.agent.language) {
9162
- await runtimeLog(
9163
- io,
9164
- "warning",
9165
- M.compile.baselineLanguageOverride(baseline.language, baseline.languageSource ?? "baseline", cfg.agent.language)
9166
- );
9167
- }
9168
- const planner = new Planner(router.for("Planner"), audit, language);
9619
+ const plannerClient = router.for("Planner");
9169
9620
  const trace = (msg) => {
9170
9621
  if (xcEnv("TRACE") === "1") {
9171
9622
  void runtimeLog(io, "dim", t().audit.traceLine("xcompiler-trace", msg));
@@ -9174,7 +9625,7 @@ async function runCompile(opts) {
9174
9625
  let rawRequirement;
9175
9626
  if (topicMode) {
9176
9627
  trace("topic.read");
9177
- rawRequirement = await fs11.readFile(path13.resolve(opts.topicFile), "utf8");
9628
+ rawRequirement = await fs12.readFile(path13.resolve(opts.topicFile), "utf8");
9178
9629
  if (!rawRequirement.trim()) {
9179
9630
  await runtimeLog(io, "error", M.compile.topicEmptyExit);
9180
9631
  await audit.end({ status: "aborted", reason: "empty topic file" });
@@ -9195,19 +9646,26 @@ async function runCompile(opts) {
9195
9646
  await audit.userInput(M.compile.auditOriginalRequirement, rawRequirement);
9196
9647
  trace("audit.userInput.intake.done");
9197
9648
  }
9649
+ const initialLanguage = resolveCompileLanguage({
9650
+ rawRequirement,
9651
+ intent,
9652
+ baseline
9653
+ });
9198
9654
  trace("clarify.section.enter");
9199
9655
  const clarifications = [];
9200
9656
  let clarificationQuestions = [];
9201
9657
  trace(`clarify.section.flag yes=${opts.yes} topicMode=${topicMode}`);
9202
9658
  if (!opts.yes && !topicMode) {
9659
+ const clarifyPlanner = new Planner(plannerClient, audit, initialLanguage.language);
9203
9660
  trace("ora.clarify.start");
9204
9661
  const spin = io.progress(M.compile.spinClarify, { animate: false });
9205
9662
  trace("ora.clarify.started");
9206
9663
  try {
9207
9664
  trace("planner.clarify.call");
9208
- clarificationQuestions = await planner.clarify(rawRequirement, {
9665
+ clarificationQuestions = await clarifyPlanner.clarify(rawRequirement, {
9209
9666
  intent,
9210
- hasBaseline: !!baseline.summary
9667
+ hasBaseline: !!baseline.summary,
9668
+ languageAmbiguous: initialLanguage.ambiguous
9211
9669
  });
9212
9670
  trace(`planner.clarify.return n=${clarificationQuestions.length}`);
9213
9671
  spin.succeed(M.compile.clarifySucceed(clarificationQuestions.length));
@@ -9296,6 +9754,20 @@ ${M.compile.topicPreviewHeader}`);
9296
9754
  }
9297
9755
  trace("ws.readFile.finalTopic");
9298
9756
  const finalTopicMd = await ws.readFile(draftTopic);
9757
+ const languageResolution = resolveCompileLanguage({
9758
+ rawRequirement: finalTopicMd,
9759
+ clarifications,
9760
+ userAddenda,
9761
+ intent,
9762
+ baseline
9763
+ });
9764
+ const language = languageResolution.language;
9765
+ await audit.event("note", `target language resolved: ${language}`, {
9766
+ messageId: "compile.language_resolved",
9767
+ language,
9768
+ source: languageResolution.source,
9769
+ ambiguous: languageResolution.ambiguous
9770
+ });
9299
9771
  await archiveIfExists(ws, DOC_NAMES.topic, audit);
9300
9772
  await ws.writeFile(DOC_NAMES.topic, finalTopicMd);
9301
9773
  await audit.event("topic.persist", M.compile.auditTopicPersisted(ws.abs(DOC_NAMES.topic)), {
@@ -9309,6 +9781,7 @@ ${M.compile.topicPreviewHeader}`);
9309
9781
  trace("ora.spin2.started");
9310
9782
  let draft;
9311
9783
  try {
9784
+ const planner = new Planner(plannerClient, audit, language);
9312
9785
  const plannerInput = {
9313
9786
  rawRequirement: finalTopicMd,
9314
9787
  clarifications,
@@ -9446,7 +9919,7 @@ async function tryLoadPhasePlan(phasePlanPath) {
9446
9919
  }
9447
9920
  async function intake(inputFile, io) {
9448
9921
  if (inputFile) {
9449
- return fs11.readFile(path13.resolve(inputFile), "utf8");
9922
+ return fs12.readFile(path13.resolve(inputFile), "utf8");
9450
9923
  }
9451
9924
  return requireRuntimeInteraction(io, "requirement intake").readMultiline({
9452
9925
  message: t().compile.requirementInputHint
@@ -9490,10 +9963,10 @@ function renderTopicDraft(raw, qa, addenda = "") {
9490
9963
  }
9491
9964
 
9492
9965
  // src/runtime/run.ts
9493
- import path27 from "path";
9966
+ import path28 from "path";
9494
9967
 
9495
9968
  // src/workspace/git.ts
9496
- import { promises as fs12 } from "fs";
9969
+ import { promises as fs13 } from "fs";
9497
9970
  import path14 from "path";
9498
9971
  import { simpleGit } from "simple-git";
9499
9972
  var RUNTIME_EXCLUDE_PATTERNS = [
@@ -9545,14 +10018,14 @@ var GitService = class {
9545
10018
  const excludePath = this.ws.abs(".git/info/exclude");
9546
10019
  let current2 = "";
9547
10020
  try {
9548
- current2 = await fs12.readFile(excludePath, "utf8");
10021
+ current2 = await fs13.readFile(excludePath, "utf8");
9549
10022
  } catch {
9550
10023
  return;
9551
10024
  }
9552
10025
  const missing = RUNTIME_EXCLUDE_PATTERNS.filter((pattern) => !current2.split(/\r?\n/u).includes(pattern));
9553
10026
  if (missing.length === 0) return;
9554
10027
  const prefix = current2.endsWith("\n") ? "\n" : "\n\n";
9555
- await fs12.appendFile(
10028
+ await fs13.appendFile(
9556
10029
  excludePath,
9557
10030
  `${prefix}# XCompiler runtime artifacts
9558
10031
  ${missing.join("\n")}
@@ -9597,7 +10070,7 @@ import { existsSync as existsSync2, readFileSync } from "fs";
9597
10070
 
9598
10071
  // src/sandbox/subprocess.ts
9599
10072
  import { spawn } from "child_process";
9600
- import { promises as fs13 } from "fs";
10073
+ import { promises as fs14 } from "fs";
9601
10074
  import path15 from "path";
9602
10075
  import crypto from "crypto";
9603
10076
 
@@ -9651,6 +10124,9 @@ function formatCommand(cmd, argv) {
9651
10124
  }
9652
10125
 
9653
10126
  // src/sandbox/subprocess.ts
10127
+ var INSTALL_IDLE_TIMEOUT_FLOOR_MS = 15 * 6e4;
10128
+ var INSTALL_IDLE_TIMEOUT_MULTIPLIER = 10;
10129
+ var INSTALL_PROGRESS_CHECK_INTERVAL_MS = 15e3;
9654
10130
  var SubprocessSandbox = class {
9655
10131
  constructor(opts) {
9656
10132
  this.opts = opts;
@@ -9704,12 +10180,13 @@ var SubprocessSandbox = class {
9704
10180
  * Python → requirements.txt (venv + pip);TypeScript → package.json (npm install)。
9705
10181
  */
9706
10182
  async build(manifestFile) {
9707
- await fs13.mkdir(this.sandboxAbs, { recursive: true });
10183
+ await fs14.mkdir(this.sandboxAbs, { recursive: true });
9708
10184
  if (this.opts.inheritEnv === false) {
9709
10185
  await Promise.all([
9710
- fs13.mkdir(path15.join(this.sandboxAbs, "home"), { recursive: true }),
9711
- fs13.mkdir(path15.join(this.sandboxAbs, "tmp"), { recursive: true }),
9712
- fs13.mkdir(path15.join(this.sandboxAbs, "npm-cache"), { recursive: true })
10186
+ fs14.mkdir(path15.join(this.sandboxAbs, "home"), { recursive: true }),
10187
+ fs14.mkdir(path15.join(this.sandboxAbs, "tmp"), { recursive: true }),
10188
+ fs14.mkdir(path15.join(this.sandboxAbs, "npm-cache"), { recursive: true }),
10189
+ fs14.mkdir(path15.join(this.sandboxAbs, "pip-cache"), { recursive: true })
9713
10190
  ]);
9714
10191
  }
9715
10192
  if (this.language === "typescript") {
@@ -9719,10 +10196,10 @@ var SubprocessSandbox = class {
9719
10196
  }
9720
10197
  async buildPython(requirementsTxt) {
9721
10198
  const reqAbs = this.opts.ws.abs(requirementsTxt);
9722
- const reqContent = await fs13.readFile(reqAbs, "utf8").catch(() => "");
10199
+ const reqContent = await fs14.readFile(reqAbs, "utf8").catch(() => "");
9723
10200
  const sig = crypto.createHash("sha256").update(reqContent).digest("hex");
9724
- const cached = await fs13.readFile(this.cacheFile, "utf8").catch(() => "");
9725
- const venvExists = await fs13.stat(this.pythonInVenv).then(() => true).catch(() => false);
10201
+ const cached = await fs14.readFile(this.cacheFile, "utf8").catch(() => "");
10202
+ const venvExists = await fs14.stat(this.pythonInVenv).then(() => true).catch(() => false);
9726
10203
  if (venvExists && cached === sig) {
9727
10204
  return { rebuilt: false, reason: "cache hit" };
9728
10205
  }
@@ -9739,7 +10216,7 @@ var SubprocessSandbox = class {
9739
10216
  }
9740
10217
  }
9741
10218
  const pyVenv = this.pythonInVenv;
9742
- const pyVenvExists = await fs13.stat(pyVenv).then(() => true).catch(() => false);
10219
+ const pyVenvExists = await fs14.stat(pyVenv).then(() => true).catch(() => false);
9743
10220
  if (!pyVenvExists) {
9744
10221
  throw new Error(`venv python missing after creation: ${pyVenv} (install system package python3-venv / python3-virtualenv)`);
9745
10222
  }
@@ -9756,17 +10233,26 @@ Install on the host: apt-get install python3-venv python3-pip or yum install
9756
10233
  }
9757
10234
  }
9758
10235
  if (reqContent.trim().length > 0) {
10236
+ const progressWatch = createInstallProgressWatch(
10237
+ [this.venvAbs, path15.join(this.sandboxAbs, "pip-cache")],
10238
+ this.opts.limits,
10239
+ "pip install"
10240
+ );
10241
+ await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `pip install -r ${reqAbs}`), {
10242
+ messageId: "sandbox.command",
10243
+ cwd: this.opts.ws.root,
10244
+ progressIdleTimeoutMs: progressWatch.idleTimeoutMs,
10245
+ progressPaths: progressWatch.paths
10246
+ });
9759
10247
  const r = await execRaw(pyVenv, ["-m", "pip", "install", "-r", reqAbs, "--quiet", "--disable-pip-version-check"], {
9760
- timeoutMs: this.opts.limits.wall_seconds * 1e3 * 5
10248
+ env: this.baseEnvironment(),
10249
+ progressWatch
9761
10250
  });
9762
10251
  if (r.exitCode !== 0) {
9763
- throw new Error(
9764
- `pip install failed (venv=${this.venvAbs}, requirements=${reqAbs}):
9765
- ${r.stderr || r.stdout}`
9766
- );
10252
+ throw new Error(formatExecFailure(`pip install failed (venv=${this.venvAbs}, requirements=${reqAbs})`, r));
9767
10253
  }
9768
10254
  }
9769
- await fs13.writeFile(this.cacheFile, sig, "utf8");
10255
+ await fs14.writeFile(this.cacheFile, sig, "utf8");
9770
10256
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.subprocessBuilt(!!reqContent), {
9771
10257
  messageId: "sandbox.subprocess_built"
9772
10258
  });
@@ -9774,29 +10260,39 @@ ${r.stderr || r.stdout}`
9774
10260
  }
9775
10261
  async buildNode(manifestFile) {
9776
10262
  const pkgAbs = this.opts.ws.abs(manifestFile);
9777
- const pkgContent = await fs13.readFile(pkgAbs, "utf8").catch(() => "");
10263
+ const pkgContent = await fs14.readFile(pkgAbs, "utf8").catch(() => "");
9778
10264
  if (!pkgContent) {
9779
10265
  return { rebuilt: false, reason: "no package.json yet" };
9780
10266
  }
9781
10267
  const lockAbs = this.opts.ws.abs("package-lock.json");
9782
- const lockContent = await fs13.readFile(lockAbs, "utf8").catch(() => "");
10268
+ const lockContent = await fs14.readFile(lockAbs, "utf8").catch(() => "");
9783
10269
  const sig = crypto.createHash("sha256").update(pkgContent + "\n" + lockContent).digest("hex");
9784
- const cached = await fs13.readFile(this.cacheFile, "utf8").catch(() => "");
9785
- const modulesExist = await fs13.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
10270
+ const cached = await fs14.readFile(this.cacheFile, "utf8").catch(() => "");
10271
+ const modulesExist = await fs14.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
9786
10272
  if (modulesExist && cached === sig) {
9787
10273
  return { rebuilt: false, reason: "cache hit" };
9788
10274
  }
9789
10275
  const installArgs = lockContent.trim() ? ["ci", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--ignore-scripts", "--no-audit", "--no-fund"];
10276
+ const progressWatch = createInstallProgressWatch(
10277
+ [this.opts.ws.abs("node_modules"), path15.join(this.sandboxAbs, "npm-cache")],
10278
+ this.opts.limits,
10279
+ "npm install"
10280
+ );
10281
+ await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `npm ${installArgs.join(" ")}`), {
10282
+ messageId: "sandbox.command",
10283
+ cwd: this.opts.ws.root,
10284
+ progressIdleTimeoutMs: progressWatch.idleTimeoutMs,
10285
+ progressPaths: progressWatch.paths
10286
+ });
9790
10287
  const r = await execRaw("npm", installArgs, {
9791
10288
  cwd: this.opts.ws.root,
9792
10289
  env: this.baseEnvironment(),
9793
- timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10
10290
+ progressWatch
9794
10291
  });
9795
10292
  if (r.exitCode !== 0) {
9796
- throw new Error(`npm dependency install failed (cwd=${this.opts.ws.root}):
9797
- ${r.stderr || r.stdout}`);
10293
+ throw new Error(formatExecFailure(`npm dependency install failed (cwd=${this.opts.ws.root})`, r));
9798
10294
  }
9799
- await fs13.writeFile(this.cacheFile, sig, "utf8");
10295
+ await fs14.writeFile(this.cacheFile, sig, "utf8");
9800
10296
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.subprocessNodeBuilt, {
9801
10297
  messageId: "sandbox.subprocess_node_built"
9802
10298
  });
@@ -9820,9 +10316,11 @@ ${r.stderr || r.stdout}`);
9820
10316
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `${cmd} ${argv.join(" ")}`), {
9821
10317
  messageId: "sandbox.command",
9822
10318
  cwd,
9823
- timeoutMs
10319
+ timeoutMs,
10320
+ progressIdleTimeoutMs: extra?.progressWatch?.idleTimeoutMs,
10321
+ progressPaths: extra?.progressWatch?.paths
9824
10322
  });
9825
- const r = await execRaw(cmd, argv, { cwd, env, timeoutMs });
10323
+ const r = await execRaw(cmd, argv, { cwd, env, timeoutMs, progressWatch: extra?.progressWatch });
9826
10324
  return r;
9827
10325
  }
9828
10326
  baseEnvironment() {
@@ -9856,16 +10354,55 @@ ${r.stderr || r.stdout}`);
9856
10354
  }
9857
10355
  /** 安装额外依赖(不会写入依赖清单,需要由调用方自行回写)。 */
9858
10356
  async installDeps(packages) {
10357
+ const progressWatch = createInstallProgressWatch(
10358
+ [
10359
+ this.language === "typescript" ? this.opts.ws.abs("node_modules") : this.venvAbs,
10360
+ path15.join(this.sandboxAbs, this.language === "typescript" ? "npm-cache" : "pip-cache")
10361
+ ],
10362
+ this.opts.limits,
10363
+ this.language === "typescript" ? "npm install" : "pip install"
10364
+ );
9859
10365
  if (this.language === "typescript") {
9860
- return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages]);
10366
+ return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages], {
10367
+ timeoutMs: 0,
10368
+ progressWatch
10369
+ });
9861
10370
  }
9862
- return this.exec(this.pythonInVenv, ["-m", "pip", "install", ...packages, "--quiet", "--disable-pip-version-check"]);
10371
+ return this.exec(this.pythonInVenv, ["-m", "pip", "install", ...packages, "--quiet", "--disable-pip-version-check"], {
10372
+ timeoutMs: 0,
10373
+ progressWatch
10374
+ });
9863
10375
  }
9864
10376
  };
9865
10377
  function sanitizeVenvName(name) {
9866
10378
  const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
9867
10379
  return cleaned.length > 0 ? cleaned : "venv";
9868
10380
  }
10381
+ function createInstallProgressWatch(paths, limits, label = "dependency install") {
10382
+ const idleTimeoutMs = Math.max(
10383
+ limits.wall_seconds * 1e3 * INSTALL_IDLE_TIMEOUT_MULTIPLIER,
10384
+ INSTALL_IDLE_TIMEOUT_FLOOR_MS
10385
+ );
10386
+ return {
10387
+ paths: dedupPaths(paths),
10388
+ idleTimeoutMs,
10389
+ checkIntervalMs: INSTALL_PROGRESS_CHECK_INTERVAL_MS,
10390
+ label
10391
+ };
10392
+ }
10393
+ function formatExecFailure(label, r) {
10394
+ const parts = [
10395
+ `${label} (exit=${r.exitCode}, timedOut=${r.timedOut ? "true" : "false"}, durationMs=${r.durationMs}${r.timeoutReason ? `, reason=${r.timeoutReason}` : ""}):`
10396
+ ];
10397
+ const stdout = clipExecOutput(r.stdout);
10398
+ const stderr = clipExecOutput(r.stderr);
10399
+ if (stdout) parts.push(`stdout:
10400
+ ${stdout}`);
10401
+ if (stderr) parts.push(`stderr:
10402
+ ${stderr}`);
10403
+ if (!stdout && !stderr) parts.push("(no stdout/stderr captured)");
10404
+ return parts.join("\n");
10405
+ }
9869
10406
  async function execRaw(cmd, argv, opts = {}) {
9870
10407
  const start = Date.now();
9871
10408
  return new Promise((resolve) => {
@@ -9877,10 +10414,46 @@ async function execRaw(cmd, argv, opts = {}) {
9877
10414
  let stdout = "";
9878
10415
  let stderr = "";
9879
10416
  let timedOut = false;
10417
+ let timeoutReason;
10418
+ let settled = false;
10419
+ let progressTimer = null;
10420
+ let progressStopped = false;
9880
10421
  const t2 = opts.timeoutMs ? setTimeout(() => {
9881
10422
  timedOut = true;
10423
+ timeoutReason = `wall-clock timeout after ${opts.timeoutMs}ms`;
9882
10424
  child.kill("SIGKILL");
9883
10425
  }, opts.timeoutMs) : null;
10426
+ const finish = (result) => {
10427
+ if (settled) return;
10428
+ settled = true;
10429
+ progressStopped = true;
10430
+ if (t2) clearTimeout(t2);
10431
+ if (progressTimer) clearTimeout(progressTimer);
10432
+ resolve(result);
10433
+ };
10434
+ if (opts.progressWatch?.paths.length) {
10435
+ const watch = opts.progressWatch;
10436
+ let lastProgressAt = start;
10437
+ let lastObservedSize = null;
10438
+ const checkProgress = async () => {
10439
+ if (progressStopped) return;
10440
+ const observedAt = Date.now();
10441
+ const currentSize = await directoryTreeSize(watch.paths);
10442
+ if (progressStopped) return;
10443
+ if (lastObservedSize !== null && currentSize > lastObservedSize) {
10444
+ lastProgressAt = observedAt;
10445
+ }
10446
+ lastObservedSize = currentSize;
10447
+ if (observedAt - lastProgressAt >= watch.idleTimeoutMs) {
10448
+ timedOut = true;
10449
+ timeoutReason = `${watch.label ?? "process"} progress idle for ${watch.idleTimeoutMs}ms; watched paths did not grow: ${watch.paths.join(", ")}`;
10450
+ child.kill("SIGKILL");
10451
+ return;
10452
+ }
10453
+ progressTimer = setTimeout(checkProgress, watch.checkIntervalMs ?? INSTALL_PROGRESS_CHECK_INTERVAL_MS);
10454
+ };
10455
+ progressTimer = setTimeout(checkProgress, watch.checkIntervalMs ?? INSTALL_PROGRESS_CHECK_INTERVAL_MS);
10456
+ }
9884
10457
  child.stdout.on("data", (b) => {
9885
10458
  stdout += b.toString();
9886
10459
  });
@@ -9888,31 +10461,69 @@ async function execRaw(cmd, argv, opts = {}) {
9888
10461
  stderr += b.toString();
9889
10462
  });
9890
10463
  child.on("error", (err) => {
9891
- if (t2) clearTimeout(t2);
9892
- resolve({
10464
+ finish({
9893
10465
  exitCode: -1,
9894
10466
  stdout,
9895
10467
  stderr: stderr + `
9896
10468
  [spawn error] ${err.message}`,
9897
10469
  timedOut,
10470
+ ...timeoutReason ? { timeoutReason } : {},
9898
10471
  durationMs: Date.now() - start
9899
10472
  });
9900
10473
  });
9901
10474
  child.on("close", (code) => {
9902
- if (t2) clearTimeout(t2);
9903
- resolve({
10475
+ finish({
9904
10476
  exitCode: code ?? -1,
9905
10477
  stdout,
9906
10478
  stderr,
9907
10479
  timedOut,
10480
+ ...timeoutReason ? { timeoutReason } : {},
9908
10481
  durationMs: Date.now() - start
9909
10482
  });
9910
10483
  });
9911
10484
  });
9912
10485
  }
10486
+ async function directoryTreeSize(paths) {
10487
+ let total = 0;
10488
+ for (const p of dedupPaths(paths)) {
10489
+ total += await pathTreeSize(p);
10490
+ }
10491
+ return total;
10492
+ }
10493
+ async function pathTreeSize(abs) {
10494
+ let stat;
10495
+ try {
10496
+ stat = await fs14.lstat(abs);
10497
+ } catch {
10498
+ return 0;
10499
+ }
10500
+ if (!stat.isDirectory()) {
10501
+ return stat.isFile() ? stat.size : 0;
10502
+ }
10503
+ let total = stat.size;
10504
+ let entries;
10505
+ try {
10506
+ entries = await fs14.readdir(abs, { withFileTypes: true });
10507
+ } catch {
10508
+ return total;
10509
+ }
10510
+ for (const entry of entries) {
10511
+ total += await pathTreeSize(path15.join(abs, entry.name));
10512
+ }
10513
+ return total;
10514
+ }
10515
+ function dedupPaths(paths) {
10516
+ return Array.from(new Set(paths.map((p) => path15.resolve(p)).filter(Boolean)));
10517
+ }
10518
+ function clipExecOutput(text, max = 6e3) {
10519
+ const normalized = String(text ?? "").trim();
10520
+ if (normalized.length <= max) return normalized;
10521
+ return `${normalized.slice(0, max)}
10522
+ ...[truncated ${normalized.length - max} chars]`;
10523
+ }
9913
10524
 
9914
10525
  // src/sandbox/docker.ts
9915
- import { promises as fs14 } from "fs";
10526
+ import { promises as fs15 } from "fs";
9916
10527
  import path16 from "path";
9917
10528
  import crypto2 from "crypto";
9918
10529
  var DockerSandbox = class {
@@ -9922,7 +10533,7 @@ var DockerSandbox = class {
9922
10533
  throw new Error(t().system.unsupportedPypiOnlyNetwork);
9923
10534
  }
9924
10535
  this.language = opts.language ?? "python";
9925
- this.image = opts.image ?? (this.language === "typescript" ? "node:20-slim" : "python:3.11-slim");
10536
+ this.image = opts.image ?? (this.language === "typescript" ? "node:24-slim" : "python:3.11-slim");
9926
10537
  this.workdir = opts.workdir ?? "/workspace";
9927
10538
  this.sandboxRel = (opts.sandboxDir ?? ".sandbox").replaceAll("\\", "/");
9928
10539
  this.cacheRel = `${this.sandboxRel}/${this.language === "typescript" ? "package.sha256" : "requirements.sha256"}`;
@@ -9971,7 +10582,7 @@ var DockerSandbox = class {
9971
10582
  async build(manifestFile) {
9972
10583
  await this.assertDocker();
9973
10584
  const sandboxAbs = this.opts.ws.abs(this.sandboxRel);
9974
- await fs14.mkdir(sandboxAbs, { recursive: true });
10585
+ await fs15.mkdir(sandboxAbs, { recursive: true });
9975
10586
  if (this.language === "typescript") {
9976
10587
  return this.buildNode(manifestFile ?? "package.json");
9977
10588
  }
@@ -9980,11 +10591,11 @@ var DockerSandbox = class {
9980
10591
  async buildPython(requirementsTxt) {
9981
10592
  const sandboxAbs = this.opts.ws.abs(this.sandboxRel);
9982
10593
  const reqAbs = this.opts.ws.abs(requirementsTxt);
9983
- const reqContent = await fs14.readFile(reqAbs, "utf8").catch(() => "");
10594
+ const reqContent = await fs15.readFile(reqAbs, "utf8").catch(() => "");
9984
10595
  const sig = crypto2.createHash("sha256").update(this.image + "\n" + reqContent).digest("hex");
9985
10596
  const cacheAbs = this.opts.ws.abs(this.cacheRel);
9986
- const cached = await fs14.readFile(cacheAbs, "utf8").catch(() => "");
9987
- const venvExists = await fs14.stat(path16.join(sandboxAbs, this.venvName, "bin", "python")).then(() => true).catch(() => false);
10597
+ const cached = await fs15.readFile(cacheAbs, "utf8").catch(() => "");
10598
+ const venvExists = await fs15.stat(path16.join(sandboxAbs, this.venvName, "bin", "python")).then(() => true).catch(() => false);
9988
10599
  if (venvExists && cached === sig) {
9989
10600
  return { rebuilt: false, reason: "cache hit" };
9990
10601
  }
@@ -9996,6 +10607,11 @@ var DockerSandbox = class {
9996
10607
  }
9997
10608
  const reqInContainer = `${this.workdir}/${requirementsTxt}`;
9998
10609
  const installCmd = reqContent.trim().length > 0 ? `python -m venv ${this.venvInContainer} && ${this.pipInContainer} install --quiet -r ${reqInContainer}` : `python -m venv ${this.venvInContainer}`;
10610
+ const progressWatch = createInstallProgressWatch(
10611
+ [path16.join(sandboxAbs, this.venvName)],
10612
+ this.opts.limits,
10613
+ "docker pip install"
10614
+ );
9999
10615
  const r = await execRaw(
10000
10616
  this.dockerBin,
10001
10617
  [
@@ -10011,15 +10627,12 @@ var DockerSandbox = class {
10011
10627
  "-lc",
10012
10628
  installCmd
10013
10629
  ],
10014
- { timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10 }
10630
+ { progressWatch }
10015
10631
  );
10016
10632
  if (r.exitCode !== 0) {
10017
- throw new Error(
10018
- `docker sandbox build failed (exit=${r.exitCode}):
10019
- ${r.stderr || r.stdout}`
10020
- );
10633
+ throw new Error(formatExecFailure(`docker sandbox build failed (image=${this.image}, cwd=${this.opts.ws.root})`, r));
10021
10634
  }
10022
- await fs14.writeFile(cacheAbs, sig, "utf8");
10635
+ await fs15.writeFile(cacheAbs, sig, "utf8");
10023
10636
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.dockerBuilt(!!reqContent), {
10024
10637
  messageId: "sandbox.docker_built",
10025
10638
  image: this.image
@@ -10028,15 +10641,15 @@ ${r.stderr || r.stdout}`
10028
10641
  }
10029
10642
  async buildNode(manifestFile) {
10030
10643
  const pkgAbs = this.opts.ws.abs(manifestFile);
10031
- const pkgContent = await fs14.readFile(pkgAbs, "utf8").catch(() => "");
10644
+ const pkgContent = await fs15.readFile(pkgAbs, "utf8").catch(() => "");
10032
10645
  if (!pkgContent) {
10033
10646
  return { rebuilt: false, reason: "no package.json yet" };
10034
10647
  }
10035
- const lockContent = await fs14.readFile(this.opts.ws.abs("package-lock.json"), "utf8").catch(() => "");
10648
+ const lockContent = await fs15.readFile(this.opts.ws.abs("package-lock.json"), "utf8").catch(() => "");
10036
10649
  const sig = crypto2.createHash("sha256").update(this.image + "\n" + pkgContent + "\n" + lockContent).digest("hex");
10037
10650
  const cacheAbs = this.opts.ws.abs(this.cacheRel);
10038
- const cached = await fs14.readFile(cacheAbs, "utf8").catch(() => "");
10039
- const modulesExist = await fs14.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
10651
+ const cached = await fs15.readFile(cacheAbs, "utf8").catch(() => "");
10652
+ const modulesExist = await fs15.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
10040
10653
  if (modulesExist && cached === sig) {
10041
10654
  return { rebuilt: false, reason: "cache hit" };
10042
10655
  }
@@ -10047,6 +10660,11 @@ ${r.stderr || r.stdout}`
10047
10660
  }
10048
10661
  }
10049
10662
  const installCommand = lockContent.trim() ? "npm ci --ignore-scripts --no-audit --no-fund" : "npm install --ignore-scripts --no-audit --no-fund";
10663
+ const progressWatch = createInstallProgressWatch(
10664
+ [this.opts.ws.abs("node_modules")],
10665
+ this.opts.limits,
10666
+ "docker npm install"
10667
+ );
10050
10668
  const r = await execRaw(
10051
10669
  this.dockerBin,
10052
10670
  [
@@ -10062,13 +10680,12 @@ ${r.stderr || r.stdout}`
10062
10680
  "-lc",
10063
10681
  installCommand
10064
10682
  ],
10065
- { timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10 }
10683
+ { progressWatch }
10066
10684
  );
10067
10685
  if (r.exitCode !== 0) {
10068
- throw new Error(`docker sandbox build failed (exit=${r.exitCode}):
10069
- ${r.stderr || r.stdout}`);
10686
+ throw new Error(formatExecFailure(`docker sandbox build failed (image=${this.image}, cwd=${this.opts.ws.root})`, r));
10070
10687
  }
10071
- await fs14.writeFile(cacheAbs, sig, "utf8");
10688
+ await fs15.writeFile(cacheAbs, sig, "utf8");
10072
10689
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.dockerNodeBuilt, {
10073
10690
  messageId: "sandbox.docker_node_built",
10074
10691
  image: this.image
@@ -10121,7 +10738,7 @@ ${r.stderr || r.stdout}`);
10121
10738
  image: this.image,
10122
10739
  network: this.opts.limits.network
10123
10740
  });
10124
- return execRaw(this.dockerBin, dockerArgs, { timeoutMs });
10741
+ return execRaw(this.dockerBin, dockerArgs, { timeoutMs, progressWatch: extra?.progressWatch });
10125
10742
  }
10126
10743
  async runProgram(args, extra) {
10127
10744
  if (this.language === "typescript") {
@@ -10139,10 +10756,23 @@ ${r.stderr || r.stdout}`);
10139
10756
  return this.exec(this.pythonInContainer, ["-m", "pytest", ...args], extra);
10140
10757
  }
10141
10758
  async installDeps(packages) {
10142
- if (this.language === "typescript") {
10143
- return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages]);
10144
- }
10145
- return this.exec(this.pipInContainer, ["install", "--quiet", ...packages]);
10759
+ const progressWatch = createInstallProgressWatch(
10760
+ [
10761
+ this.language === "typescript" ? this.opts.ws.abs("node_modules") : this.opts.ws.abs(`${this.sandboxRel}/${this.venvName}`)
10762
+ ],
10763
+ this.opts.limits,
10764
+ this.language === "typescript" ? "docker npm install" : "docker pip install"
10765
+ );
10766
+ if (this.language === "typescript") {
10767
+ return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages], {
10768
+ timeoutMs: 0,
10769
+ progressWatch
10770
+ });
10771
+ }
10772
+ return this.exec(this.pipInContainer, ["install", "--quiet", ...packages], {
10773
+ timeoutMs: 0,
10774
+ progressWatch
10775
+ });
10146
10776
  }
10147
10777
  /** 将宿主机绝对路径映射回容器路径;相对路径或 undefined 原样返回(相对则拼到 workdir)。 */
10148
10778
  toContainerPath(p) {
@@ -10173,12 +10803,11 @@ function isRunningInContainer() {
10173
10803
  }
10174
10804
  return false;
10175
10805
  }
10176
- function createSandbox(cfg, ws, audit, language = cfg.agent.language) {
10177
- const profile = getLanguageProfile(language);
10178
- const configuredImage = cfg.agent.sandbox_docker.image;
10179
- const image = language === cfg.agent.language ? configuredImage : profile.defaultDockerImage;
10180
- const kind = cfg.agent.sandbox;
10181
- if (cfg.agent.sandbox_limits.network === "pypi-only") {
10806
+ function createSandbox(cfg, ws, audit, language = "python") {
10807
+ const languageSandbox = cfg.agent.sandboxes?.[language] ?? legacyLanguageSandbox(cfg, language);
10808
+ const kind = languageSandbox.mode;
10809
+ const activeLimits = kind === "docker" ? languageSandbox.docker.limits : languageSandbox.local.limits;
10810
+ if (activeLimits.network === "pypi-only") {
10182
10811
  throw new Error(t().system.unsupportedPypiOnlyNetwork);
10183
10812
  }
10184
10813
  if (kind === "docker") {
@@ -10187,24 +10816,60 @@ function createSandbox(cfg, ws, audit, language = cfg.agent.language) {
10187
10816
  }
10188
10817
  return new DockerSandbox({
10189
10818
  ws,
10190
- limits: cfg.agent.sandbox_limits,
10819
+ limits: languageSandbox.docker.limits,
10191
10820
  audit,
10192
10821
  language,
10193
- image,
10194
- workdir: cfg.agent.sandbox_docker.workdir,
10195
- pull: cfg.agent.sandbox_docker.pull,
10196
- dockerBin: cfg.agent.sandbox_docker.docker_bin,
10197
- extraRunArgs: cfg.agent.sandbox_docker.extra_run_args
10822
+ image: languageSandbox.docker.image ?? getLanguageProfile(language).defaultDockerImage,
10823
+ workdir: languageSandbox.docker.workdir,
10824
+ pull: languageSandbox.docker.pull,
10825
+ dockerBin: languageSandbox.docker.docker_bin,
10826
+ extraRunArgs: languageSandbox.docker.extra_run_args,
10827
+ sandboxDir: languageSandbox.docker.sandbox_dir
10198
10828
  });
10199
10829
  }
10200
10830
  if (kind === "firejail") {
10201
10831
  throw new Error(t().system.firejailUnsupported);
10202
10832
  }
10203
- return new SubprocessSandbox({ ws, limits: cfg.agent.sandbox_limits, audit, language });
10833
+ return new SubprocessSandbox({
10834
+ ws,
10835
+ limits: languageSandbox.local.limits,
10836
+ audit,
10837
+ language,
10838
+ sandboxDir: languageSandbox.local.sandbox_dir,
10839
+ pythonBin: languageSandbox.local.python_bin,
10840
+ inheritEnv: languageSandbox.local.inherit_env
10841
+ });
10842
+ }
10843
+ function legacyLanguageSandbox(cfg, language) {
10844
+ const legacyAgent = cfg.agent;
10845
+ const limits = legacyAgent.sandbox_limits ?? {
10846
+ cpu: 1,
10847
+ memory_mb: 1024,
10848
+ wall_seconds: 60,
10849
+ network: "download-only",
10850
+ expose_ports: []
10851
+ };
10852
+ const useLegacyDocker = legacyAgent.language === language ? legacyAgent.sandbox_docker : void 0;
10853
+ return {
10854
+ mode: legacyAgent.sandbox ?? "subprocess",
10855
+ local: {
10856
+ sandbox_dir: `.sandbox/${language}`,
10857
+ limits
10858
+ },
10859
+ docker: {
10860
+ image: useLegacyDocker?.image ?? getLanguageProfile(language).defaultDockerImage,
10861
+ workdir: useLegacyDocker?.workdir ?? "/workspace",
10862
+ pull: useLegacyDocker?.pull ?? false,
10863
+ docker_bin: useLegacyDocker?.docker_bin ?? "docker",
10864
+ extra_run_args: useLegacyDocker?.extra_run_args ?? [],
10865
+ sandbox_dir: useLegacyDocker?.sandbox_dir ?? `.sandbox/${language}`,
10866
+ limits: useLegacyDocker?.limits ?? limits
10867
+ }
10868
+ };
10204
10869
  }
10205
10870
 
10206
10871
  // src/core/engine.ts
10207
- import path26 from "path";
10872
+ import path27 from "path";
10208
10873
  import chalk2 from "chalk";
10209
10874
 
10210
10875
  // src/util/spinner.ts
@@ -10290,10 +10955,10 @@ init_types();
10290
10955
  // src/tools/fs.ts
10291
10956
  init_types();
10292
10957
  import path18 from "path";
10293
- import { promises as fs16 } from "fs";
10958
+ import { promises as fs17 } from "fs";
10294
10959
 
10295
10960
  // src/tools/path_guard.ts
10296
- import { promises as fs15 } from "fs";
10961
+ import { promises as fs16 } from "fs";
10297
10962
  import path17 from "path";
10298
10963
  async function resolveWorkspacePath(ws, rawPath, operation, opts = {}) {
10299
10964
  const raw = rawPath && rawPath.trim() ? rawPath : ".";
@@ -10302,19 +10967,19 @@ async function resolveWorkspacePath(ws, rawPath, operation, opts = {}) {
10302
10967
  if (!isInside(root, abs)) return deny(operation, raw);
10303
10968
  if (opts.mustExist) {
10304
10969
  try {
10305
- const real = await fs15.realpath(abs);
10970
+ const real = await fs16.realpath(abs);
10306
10971
  if (!isInside(root, real)) return deny(operation, raw);
10307
10972
  } catch (err) {
10308
10973
  return { ok: false, error: `${operation} failed: ${err.message}` };
10309
10974
  }
10310
10975
  } else if (opts.forWrite) {
10311
- const existingTarget = await fs15.realpath(abs).catch(() => void 0);
10976
+ const existingTarget = await fs16.realpath(abs).catch(() => void 0);
10312
10977
  if (existingTarget && !isInside(root, existingTarget)) return deny(operation, raw);
10313
10978
  const parent = await nearestExistingParent(path17.dirname(abs));
10314
- const realParent = await fs15.realpath(parent).catch(() => parent);
10979
+ const realParent = await fs16.realpath(parent).catch(() => parent);
10315
10980
  if (!isInside(root, realParent)) return deny(operation, raw);
10316
10981
  } else {
10317
- const existing = await fs15.realpath(abs).catch(() => void 0);
10982
+ const existing = await fs16.realpath(abs).catch(() => void 0);
10318
10983
  if (existing && !isInside(root, existing)) return deny(operation, raw);
10319
10984
  }
10320
10985
  return { ok: true, abs };
@@ -10324,13 +10989,13 @@ function isInside(root, candidate) {
10324
10989
  return rel === "" || !!rel && !rel.startsWith("..") && !path17.isAbsolute(rel);
10325
10990
  }
10326
10991
  async function realpathOrResolve(p) {
10327
- return fs15.realpath(p).catch(() => path17.resolve(p));
10992
+ return fs16.realpath(p).catch(() => path17.resolve(p));
10328
10993
  }
10329
10994
  async function nearestExistingParent(start) {
10330
10995
  let current2 = path17.resolve(start);
10331
10996
  while (true) {
10332
10997
  try {
10333
- const stat = await fs15.stat(current2);
10998
+ const stat = await fs16.stat(current2);
10334
10999
  if (stat.isDirectory()) return current2;
10335
11000
  } catch {
10336
11001
  }
@@ -10356,9 +11021,9 @@ var readFileTool = {
10356
11021
  const resolved = await resolveWorkspacePath(ctx.ws, args.path, "read_file", { mustExist: true });
10357
11022
  if (!resolved.ok) return { ok: false, error: resolved.error };
10358
11023
  const abs = resolved.abs;
10359
- const stat = await fs16.stat(abs);
11024
+ const stat = await fs17.stat(abs);
10360
11025
  if (!stat.isFile()) return { ok: false, error: "not a file" };
10361
- const buf = await fs16.readFile(abs);
11026
+ const buf = await fs17.readFile(abs);
10362
11027
  const limit = args.maxBytes ?? 2e5;
10363
11028
  const content = buf.byteLength > limit ? buf.subarray(0, limit).toString("utf8") + `
10364
11029
  ... [truncated ${buf.byteLength - limit} bytes]` : buf.toString("utf8");
@@ -10419,8 +11084,8 @@ var writeFileTool = {
10419
11084
  }
10420
11085
  try {
10421
11086
  const abs = resolved.abs;
10422
- await fs16.mkdir(path18.dirname(abs), { recursive: true });
10423
- await fs16.writeFile(abs, args.content, "utf8");
11087
+ await fs17.mkdir(path18.dirname(abs), { recursive: true });
11088
+ await fs17.writeFile(abs, args.content, "utf8");
10424
11089
  return {
10425
11090
  ok: true,
10426
11091
  data: { bytes: size },
@@ -10456,11 +11121,11 @@ var appendFileTool = {
10456
11121
  }
10457
11122
  try {
10458
11123
  const abs = resolved.abs;
10459
- await fs16.mkdir(path18.dirname(abs), { recursive: true });
10460
- await fs16.appendFile(abs, args.content, "utf8");
11124
+ await fs17.mkdir(path18.dirname(abs), { recursive: true });
11125
+ await fs17.appendFile(abs, args.content, "utf8");
10461
11126
  let total = size;
10462
11127
  try {
10463
- total = (await fs16.stat(abs)).size;
11128
+ total = (await fs17.stat(abs)).size;
10464
11129
  } catch {
10465
11130
  }
10466
11131
  return {
@@ -10493,7 +11158,7 @@ var listDirTool = {
10493
11158
  const resolved = await resolveWorkspacePath(ctx.ws, args.path ?? ".", "list_dir", { mustExist: true });
10494
11159
  if (!resolved.ok) return { ok: false, error: resolved.error };
10495
11160
  const abs = resolved.abs;
10496
- const entries = await fs16.readdir(abs, { withFileTypes: true });
11161
+ const entries = await fs17.readdir(abs, { withFileTypes: true });
10497
11162
  return {
10498
11163
  ok: true,
10499
11164
  data: { entries: entries.map((e) => e.isDirectory() ? e.name + "/" : e.name) },
@@ -10508,7 +11173,7 @@ var listDirTool = {
10508
11173
  // src/tools/patch.ts
10509
11174
  init_types();
10510
11175
  import path19 from "path";
10511
- import { promises as fs17 } from "fs";
11176
+ import { promises as fs18 } from "fs";
10512
11177
  var applyPatchTool = {
10513
11178
  name: "apply_patch",
10514
11179
  description: "\u5E94\u7528 unified diff \u8865\u4E01\uFF1B\u76EE\u6807\u6587\u4EF6\u5FC5\u987B\u5728\u5F53\u524D Step writable allowlist \u5185\u3002",
@@ -10526,14 +11191,14 @@ var applyPatchTool = {
10526
11191
  const abs = resolved.abs;
10527
11192
  let original = "";
10528
11193
  try {
10529
- original = await fs17.readFile(abs, "utf8");
11194
+ original = await fs18.readFile(abs, "utf8");
10530
11195
  } catch {
10531
11196
  if (!fd.isNewFile) return { ok: false, error: `target file missing: ${fd.target}` };
10532
11197
  }
10533
11198
  const next = applyHunks(original, fd.hunks);
10534
11199
  if (next.error) return { ok: false, error: `${fd.target}: ${next.error}` };
10535
- await fs17.mkdir(path19.dirname(abs), { recursive: true });
10536
- await fs17.writeFile(abs, next.content, "utf8");
11200
+ await fs18.mkdir(path19.dirname(abs), { recursive: true });
11201
+ await fs18.writeFile(abs, next.content, "utf8");
10537
11202
  changed.push(fd.target);
10538
11203
  }
10539
11204
  return { ok: true, data: { changedFiles: changed }, summary: `patched ${changed.join(", ")}` };
@@ -10749,7 +11414,7 @@ var pipInstallTool = {
10749
11414
  // src/tools/edit.ts
10750
11415
  init_types();
10751
11416
  import path20 from "path";
10752
- import { promises as fs18 } from "fs";
11417
+ import { promises as fs19 } from "fs";
10753
11418
  var replaceInFileTool = {
10754
11419
  name: "replace_in_file",
10755
11420
  description: "\u628A\u5F53\u524D Step writable allowlist \u5185\u76EE\u6807\u6587\u4EF6\u7684 find \u5B57\u7B26\u4E32\u7CBE\u786E\u66FF\u6362\u4E3A replace\uFF08\u9ED8\u8BA4\u8981\u6C42\u51FA\u73B0 1 \u6B21\uFF09\u3002",
@@ -10770,7 +11435,7 @@ var replaceInFileTool = {
10770
11435
  const abs = resolved.abs;
10771
11436
  let original;
10772
11437
  try {
10773
- original = await fs18.readFile(abs, "utf8");
11438
+ original = await fs19.readFile(abs, "utf8");
10774
11439
  } catch {
10775
11440
  return { ok: false, error: `file not found: ${args.path}` };
10776
11441
  }
@@ -10802,8 +11467,8 @@ var replaceInFileTool = {
10802
11467
  return { ok: false, error: hint.join("\n") };
10803
11468
  }
10804
11469
  const next = parts.join(args.replace);
10805
- await fs18.mkdir(path20.dirname(abs), { recursive: true });
10806
- await fs18.writeFile(abs, next, "utf8");
11470
+ await fs19.mkdir(path20.dirname(abs), { recursive: true });
11471
+ await fs19.writeFile(abs, next, "utf8");
10807
11472
  return {
10808
11473
  ok: true,
10809
11474
  data: { occurrences },
@@ -10820,7 +11485,7 @@ var codeSearchTool = {
10820
11485
  const resolved = await resolveWorkspacePath(ctx.ws, args.root ?? ".", "code_search", { mustExist: true });
10821
11486
  if (!resolved.ok) return { ok: false, error: resolved.error };
10822
11487
  const root = resolved.abs;
10823
- const workspaceRoot = await fs18.realpath(ctx.ws.root).catch(() => ctx.ws.root);
11488
+ const workspaceRoot = await fs19.realpath(ctx.ws.root).catch(() => ctx.ws.root);
10824
11489
  const max = args.maxResults ?? 50;
10825
11490
  const exts = args.ext && args.ext.length > 0 ? new Set(args.ext.map((e) => e.startsWith(".") ? e : "." + e)) : null;
10826
11491
  const matches = [];
@@ -10829,9 +11494,9 @@ var codeSearchTool = {
10829
11494
  if (/(?:^|\/)(node_modules|\.git|\.sandbox|\.xcompiler|dist|__pycache__)\//.test(abs)) return;
10830
11495
  let content;
10831
11496
  try {
10832
- const stat = await fs18.stat(abs);
11497
+ const stat = await fs19.stat(abs);
10833
11498
  if (stat.size > 512e3) return;
10834
- content = await fs18.readFile(abs, "utf8");
11499
+ content = await fs19.readFile(abs, "utf8");
10835
11500
  } catch {
10836
11501
  return;
10837
11502
  }
@@ -10854,7 +11519,7 @@ var codeSearchTool = {
10854
11519
  async function walk3(dir, onFile) {
10855
11520
  let entries;
10856
11521
  try {
10857
- entries = await fs18.readdir(dir, { withFileTypes: true });
11522
+ entries = await fs19.readdir(dir, { withFileTypes: true });
10858
11523
  } catch {
10859
11524
  return;
10860
11525
  }
@@ -10908,7 +11573,7 @@ var analyzeErrorTool = {
10908
11573
  };
10909
11574
 
10910
11575
  // src/tools/deps.ts
10911
- import { promises as fs19 } from "fs";
11576
+ import { promises as fs20 } from "fs";
10912
11577
  var addDependencyTool = {
10913
11578
  name: "add_dependency",
10914
11579
  description: "\u5411\u4F9D\u8D56\u6E05\u5355\u8FFD\u52A0\u4F9D\u8D56\uFF08python: requirements.txt\uFF1Btypescript: package.json\uFF09\u5E76\u91CD\u5EFA\u6C99\u76D2\u3002",
@@ -10928,7 +11593,7 @@ var addDependencyTool = {
10928
11593
  const added = [];
10929
11594
  let final;
10930
11595
  if (ctx.language === "typescript") {
10931
- const pkg = await fs19.readFile(abs, "utf8").then((text) => JSON.parse(text)).catch(() => ({}));
11596
+ const pkg = await fs20.readFile(abs, "utf8").then((text) => JSON.parse(text)).catch(() => ({}));
10932
11597
  const existingDeps = pkg.dependencies && typeof pkg.dependencies === "object" && !Array.isArray(pkg.dependencies) ? { ...pkg.dependencies } : {};
10933
11598
  const before = new Set(Object.keys(existingDeps));
10934
11599
  for (const name of normalized) {
@@ -10937,11 +11602,11 @@ var addDependencyTool = {
10937
11602
  }
10938
11603
  final = Object.keys(existingDeps).sort();
10939
11604
  pkg.dependencies = Object.fromEntries(final.map((name) => [name, existingDeps[name] ?? "*"]));
10940
- await fs19.writeFile(abs, JSON.stringify(pkg, null, 2) + "\n", "utf8");
11605
+ await fs20.writeFile(abs, JSON.stringify(pkg, null, 2) + "\n", "utf8");
10941
11606
  } else {
10942
11607
  let existing = "";
10943
11608
  try {
10944
- existing = await fs19.readFile(abs, "utf8");
11609
+ existing = await fs20.readFile(abs, "utf8");
10945
11610
  } catch {
10946
11611
  }
10947
11612
  const set = /* @__PURE__ */ new Set();
@@ -10955,7 +11620,7 @@ var addDependencyTool = {
10955
11620
  set.add(p);
10956
11621
  }
10957
11622
  final = [...set].sort();
10958
- await fs19.writeFile(abs, final.join("\n") + "\n", "utf8");
11623
+ await fs20.writeFile(abs, final.join("\n") + "\n", "utf8");
10959
11624
  }
10960
11625
  try {
10961
11626
  await ctx.sandbox.build(manifestPath);
@@ -10974,7 +11639,7 @@ var addDependencyTool = {
10974
11639
  };
10975
11640
 
10976
11641
  // src/tools/net.ts
10977
- import { promises as fs20 } from "fs";
11642
+ import { promises as fs21 } from "fs";
10978
11643
  import path21 from "path";
10979
11644
  var DEFAULT_TIMEOUT_MS = 15e3;
10980
11645
  var DEFAULT_MAX_BYTES = 256 * 1024;
@@ -11036,8 +11701,8 @@ var httpFetchTool = {
11036
11701
  if (timer) clearTimeout(timer);
11037
11702
  const trimmed = buf.length > maxBytes ? buf.subarray(0, maxBytes) : buf;
11038
11703
  const abs = saveAsAbs;
11039
- await fs20.mkdir(path21.dirname(abs), { recursive: true });
11040
- await fs20.writeFile(abs, trimmed);
11704
+ await fs21.mkdir(path21.dirname(abs), { recursive: true });
11705
+ await fs21.writeFile(abs, trimmed);
11041
11706
  await ctx.audit?.event("tool.call", t().audit.httpFetchSaved(method, args.url, args.saveAs, trimmed.length), {
11042
11707
  messageId: "audit.http_fetch_saved",
11043
11708
  stepId: ctx.stepId,
@@ -11095,7 +11760,7 @@ var httpFetchTool = {
11095
11760
  init_types();
11096
11761
 
11097
11762
  // src/tools/guard.ts
11098
- import { promises as fs21 } from "fs";
11763
+ import { promises as fs22 } from "fs";
11099
11764
  import path22 from "path";
11100
11765
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "append_file", "apply_patch", "replace_in_file", "add_dependency"]);
11101
11766
  var DEFAULT_EDIT_LINES_PER_STEP = 400;
@@ -11178,8 +11843,8 @@ var EditGuard = class {
11178
11843
  async record(partial) {
11179
11844
  const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), stepId: this.opts.stepId, ...partial };
11180
11845
  try {
11181
- await fs21.mkdir(path22.dirname(this.logAbs), { recursive: true });
11182
- await fs21.appendFile(this.logAbs, JSON.stringify(rec) + "\n", "utf8");
11846
+ await fs22.mkdir(path22.dirname(this.logAbs), { recursive: true });
11847
+ await fs22.appendFile(this.logAbs, JSON.stringify(rec) + "\n", "utf8");
11183
11848
  } catch {
11184
11849
  }
11185
11850
  }
@@ -11235,8 +11900,9 @@ function buildDefaultRegistry() {
11235
11900
 
11236
11901
  // src/agents/executor.ts
11237
11902
  import path23 from "path";
11238
- import { promises as fs22 } from "fs";
11903
+ import { promises as fs23 } from "fs";
11239
11904
  import { jsonrepair } from "jsonrepair";
11905
+ var MISSING_OUTPUT_STALL_ROUND_LIMIT = 3;
11240
11906
  var StepExecutor = class {
11241
11907
  constructor(opts) {
11242
11908
  this.opts = opts;
@@ -11260,8 +11926,12 @@ var StepExecutor = class {
11260
11926
  ];
11261
11927
  const calls = [];
11262
11928
  let finalThought;
11263
- const initialMissing = (await verifyOutputs(inp)).missing.length;
11929
+ let issueResolutionPlan;
11930
+ const initialVerify = await verifyOutputs(inp);
11931
+ const initialMissing = initialVerify.missing.length;
11264
11932
  const hardRoundLimit = Math.max(maxRounds, maxRounds + Math.min(12, Math.max(4, initialMissing * 2)));
11933
+ let lastMissingCount = initialMissing;
11934
+ let missingOutputStallRounds = 0;
11265
11935
  let parseFailures = 0;
11266
11936
  let repeatedTurns = 0;
11267
11937
  let lastActionsKey = null;
@@ -11310,11 +11980,12 @@ var StepExecutor = class {
11310
11980
  } catch (err) {
11311
11981
  rep.done("failed");
11312
11982
  const errMsg = err.message;
11983
+ actualRounds = round;
11313
11984
  const dumpRel = `.xcompiler/llm-stream/${inp.step.id}-${role}-r${round}.txt`;
11314
11985
  try {
11315
11986
  const dumpAbs = inp.ctx.ws.abs(dumpRel);
11316
- await fs22.mkdir(path23.dirname(dumpAbs), { recursive: true });
11317
- await fs22.writeFile(
11987
+ await fs23.mkdir(path23.dirname(dumpAbs), { recursive: true });
11988
+ await fs23.writeFile(
11318
11989
  dumpAbs,
11319
11990
  `${t().audit.partialFailureHeader(errMsg)}
11320
11991
  ${t().audit.streamLength(rawAggregate.length)}
@@ -11343,11 +12014,33 @@ ${rawAggregate}`,
11343
12014
  partialBytes: rawAggregate.length
11344
12015
  }
11345
12016
  );
12017
+ if (isLowQualityLLMResponseError(errMsg)) {
12018
+ repeatedTurns++;
12019
+ const verify2 = await verifyOutputs(inp);
12020
+ const metrics2 = computeMetrics({
12021
+ rounds: actualRounds,
12022
+ parseFailures,
12023
+ repeatedTurns,
12024
+ calls,
12025
+ initialMissing,
12026
+ currentMissing: verify2.missing.length
12027
+ });
12028
+ return {
12029
+ success: false,
12030
+ rounds: round,
12031
+ toolCalls: calls,
12032
+ finalThought,
12033
+ issueResolutionPlan,
12034
+ error: errMsg,
12035
+ metrics: metrics2
12036
+ };
12037
+ }
11346
12038
  throw err;
11347
12039
  }
11348
12040
  rep.done();
11349
12041
  const turn = parseTurn(text);
11350
12042
  finalThought = turn.thoughts;
12043
+ issueResolutionPlan = extractIssueResolutionPlan(turn) ?? issueResolutionPlan;
11351
12044
  const normalizedActions = normalizeActions(turn.actions);
11352
12045
  const actions = normalizedActions.actions;
11353
12046
  if (normalizedActions.invalid.length > 0) {
@@ -11397,6 +12090,7 @@ ${rawAggregate}`,
11397
12090
  }
11398
12091
  await inp.ctx.audit?.executorTurn(inp.step.id, role, round, {
11399
12092
  thoughts: turn.thoughts,
12093
+ issueResolutionPlan,
11400
12094
  actions,
11401
12095
  done: turn.done === true,
11402
12096
  raw: text,
@@ -11505,12 +12199,25 @@ ${rawAggregate}`,
11505
12199
  turnResults.push({ ...r, tool: a.tool });
11506
12200
  }
11507
12201
  const verify = await verifyOutputs(inp);
12202
+ const mutationSucceededThisRound = turnResults.some((r) => r.ok && isRepairEvidenceTool(r.tool));
12203
+ if (verify.missing.length < lastMissingCount) {
12204
+ lastMissingCount = verify.missing.length;
12205
+ missingOutputStallRounds = 0;
12206
+ } else if (!verify.ok && initialMissing > 0 && mutationSucceededThisRound && !readOnlyRound && unresolvedToolFailures.size === 0) {
12207
+ missingOutputStallRounds++;
12208
+ }
11508
12209
  if (turnResults.some((r) => r.tool === "run_tests" && !r.ok) && !advisoryFailureTools.has("run_tests")) {
11509
12210
  failedTestRunRounds++;
11510
12211
  }
11511
12212
  const repairGateOk = !repairRequired || repairEvidence || canAcceptOutputCompletionRecovery(inp, initialMissing);
12213
+ const issueResolutionPlanRequired = role === "Debugger" && !!inp.debugContext?.issueId;
12214
+ const issueResolutionPlanOk = !issueResolutionPlanRequired || !!issueResolutionPlan?.trim();
11512
12215
  const verifiedCompletion = !turn.done && hasSuccessfulCompletionVerification(calls);
11513
- if ((turn.done || verifiedCompletion) && verify.ok && unresolvedToolFailures.size === 0 && repairGateOk) {
12216
+ const outputCompletionRecovery = verify.ok && canAcceptOutputCompletionRecovery(inp, initialMissing);
12217
+ const supersededContractFailures = repairEvidence && hasSuccessfulCompletionVerification(calls) && hasOnlySupersededToolContractFailures(unresolvedToolFailures);
12218
+ const unresolvedFailuresOk = unresolvedToolFailures.size === 0 || outputCompletionRecovery && hasOnlyUntargetedToolContractFailures(unresolvedToolFailures) || supersededContractFailures;
12219
+ const completionSignal = turn.done || verifiedCompletion || outputCompletionRecovery;
12220
+ if (completionSignal && verify.ok && unresolvedFailuresOk && repairGateOk && issueResolutionPlanOk) {
11514
12221
  const metrics2 = computeMetrics({
11515
12222
  rounds: actualRounds,
11516
12223
  parseFailures,
@@ -11519,7 +12226,7 @@ ${rawAggregate}`,
11519
12226
  initialMissing,
11520
12227
  currentMissing: verify.missing.length
11521
12228
  });
11522
- return { success: true, rounds: round, toolCalls: calls, finalThought, metrics: metrics2 };
12229
+ return { success: true, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, metrics: metrics2 };
11523
12230
  }
11524
12231
  if (this.opts.maxFailedTestRuns && failedTestRunRounds >= this.opts.maxFailedTestRuns) {
11525
12232
  const metrics2 = computeMetrics({
@@ -11538,7 +12245,27 @@ ${rawAggregate}`,
11538
12245
  failedTestRunRounds,
11539
12246
  maxFailedTestRuns: this.opts.maxFailedTestRuns
11540
12247
  });
11541
- return { success: false, rounds: round, toolCalls: calls, finalThought, error, metrics: metrics2 };
12248
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
12249
+ }
12250
+ if (missingOutputStallRounds >= MISSING_OUTPUT_STALL_ROUND_LIMIT) {
12251
+ repeatedTurns++;
12252
+ const metrics2 = computeMetrics({
12253
+ rounds: actualRounds,
12254
+ parseFailures,
12255
+ repeatedTurns,
12256
+ calls,
12257
+ initialMissing,
12258
+ currentMissing: verify.missing.length
12259
+ });
12260
+ const error = `write/progress actions did not reduce missing outputs for ${missingOutputStallRounds} rounds; missing outputs: ${verify.missing.join(", ")}. Next attempt must create those exact outputs before rewriting already-existing files.`;
12261
+ await inp.ctx.audit?.event("note", error, {
12262
+ messageId: "audit.executor_missing_output_stall",
12263
+ stepId: inp.step.id,
12264
+ round,
12265
+ missingOutputStallRounds,
12266
+ missingOutputs: verify.missing
12267
+ });
12268
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
11542
12269
  }
11543
12270
  const readOnlyRecoveryViolation = readOnlyRecoveryMode && readOnlyRecoveryRounds >= 2;
11544
12271
  const readOnlyRoundLimit = directRepairMode ? 2 : 3;
@@ -11561,7 +12288,7 @@ ${rawAggregate}`,
11561
12288
  consecutiveReadOnlyRounds,
11562
12289
  actions
11563
12290
  });
11564
- return { success: false, rounds: round, toolCalls: calls, finalThought, error, metrics: metrics2 };
12291
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
11565
12292
  }
11566
12293
  if (round >= roundLimit && roundLimit < hardRoundLimit && shouldExtendProductiveRun({
11567
12294
  parseFailures,
@@ -11595,8 +12322,13 @@ ${rawAggregate}`,
11595
12322
  rounds: consecutiveReadOnlyRounds,
11596
12323
  targets: actions.flatMap((action) => actionTargetPaths(action.tool, action.args)).join(", ")
11597
12324
  } : void 0,
12325
+ missingOutputStallWarning: missingOutputStallRounds >= MISSING_OUTPUT_STALL_ROUND_LIMIT - 1 && !verify.ok ? {
12326
+ rounds: missingOutputStallRounds,
12327
+ missing: verify.missing.join(", ")
12328
+ } : void 0,
11598
12329
  readOnlyRecoveryWarning: (readOnlyRecoveryMode || directRepairMode) && readOnlyRound,
11599
- repairEvidenceMissing: repairRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !repairEvidence
12330
+ repairEvidenceMissing: repairRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !repairEvidence,
12331
+ issueResolutionPlanMissing: issueResolutionPlanRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !issueResolutionPlanOk
11600
12332
  })
11601
12333
  });
11602
12334
  }
@@ -11614,7 +12346,8 @@ ${rawAggregate}`,
11614
12346
  rounds: actualRounds || roundLimit,
11615
12347
  toolCalls: calls,
11616
12348
  finalThought,
11617
- error: repairRequired && finalVerify.ok && unresolvedToolFailures.size === 0 && !repairEvidence && !canAcceptOutputCompletionRecovery(inp, initialMissing) ? "DEBUG retry ended without repair evidence; run a successful patch/write/dependency change or verification command before done=true." : finalVerify.ok && unresolvedToolFailures.size > 0 ? `unresolved tool failures remain: ${[...unresolvedToolFailures.values()].join("; ")}` : "max rounds exceeded without satisfying outputs",
12349
+ issueResolutionPlan,
12350
+ error: role === "Debugger" && !!inp.debugContext?.issueId && !issueResolutionPlan?.trim() ? "DEBUG issue completion missing issueResolutionPlan; provide a concrete handling plan before marking the issue resolved." : repairRequired && finalVerify.ok && unresolvedToolFailures.size === 0 && !repairEvidence && !canAcceptOutputCompletionRecovery(inp, initialMissing) ? "DEBUG retry ended without repair evidence; run a successful patch/write/dependency change or verification command before done=true." : finalVerify.ok && unresolvedToolFailures.size > 0 ? `unresolved tool failures remain: ${[...unresolvedToolFailures.values()].join("; ")}` : "max rounds exceeded without satisfying outputs",
11618
12351
  metrics
11619
12352
  };
11620
12353
  }
@@ -11627,6 +12360,9 @@ function isReadOnlyOrProbeAction(action) {
11627
12360
  function isReadOnlyLoopFailure(reason) {
11628
12361
  return /repeated read-only\/probe actions without progress/i.test(reason) || /read-only recovery mode repeated probe actions/i.test(reason);
11629
12362
  }
12363
+ function isLowQualityLLMResponseError(message) {
12364
+ return /low-quality (?:debugger )?response/i.test(message) || /read-only\/probe actions in read-only recovery mode/i.test(message);
12365
+ }
11630
12366
  function hasActionableDebuggerFailure(debugContext) {
11631
12367
  if (!debugContext) return false;
11632
12368
  const text = [
@@ -11642,6 +12378,18 @@ function canAcceptOutputCompletionRecovery(inp, initialMissing) {
11642
12378
  if (initialMissing !== 0) return false;
11643
12379
  return isOutputCompletionFailure(inp.debugContext.reason, inp.debugContext.failureLog);
11644
12380
  }
12381
+ function hasOnlyUntargetedToolContractFailures(unresolved) {
12382
+ if (unresolved.size === 0) return true;
12383
+ return [...unresolved.entries()].every(
12384
+ ([key, detail]) => key.startsWith("tool:") && /invalid (?:write_file|append_file|replace_in_file|read_file) args: path must be a non-empty string/i.test(detail)
12385
+ );
12386
+ }
12387
+ function hasOnlySupersededToolContractFailures(unresolved) {
12388
+ if (unresolved.size === 0) return true;
12389
+ return [...unresolved.values()].every(
12390
+ (detail) => /invalid (?:write_file|append_file|replace_in_file|apply_patch|read_file) args/i.test(detail) || /(?:read_file|write_file|append_file|replace_in_file|apply_patch) denied: path "\." is outside the project directory/i.test(detail) || /(?:read_file|write_file|append_file|replace_in_file) failed:.*path must be a non-empty string/i.test(detail) || /path must be a non-empty string/i.test(detail)
12391
+ );
12392
+ }
11645
12393
  function isOutputCompletionFailure(reason = "", failureLog = "") {
11646
12394
  const text = `${reason}
11647
12395
  ${failureLog}`;
@@ -11716,6 +12464,7 @@ function compactTurnForHistory(turn) {
11716
12464
  const normalized = normalizeActions(turn.actions);
11717
12465
  return JSON.stringify({
11718
12466
  thoughts: truncate2(turn.thoughts ?? "", 500),
12467
+ issueResolutionPlan: truncate2(extractIssueResolutionPlan(turn) ?? "", 900),
11719
12468
  actions: normalized.actions.map((action) => ({
11720
12469
  tool: action.tool,
11721
12470
  args: compactActionArgs(action.tool, action.args)
@@ -12054,8 +12803,8 @@ async function verifyOutputs(inp) {
12054
12803
  const missing = [];
12055
12804
  for (const out of inp.step.outputs) {
12056
12805
  if (out.endsWith("/")) continue;
12057
- const exists = await inp.ctx.ws.exists(out);
12058
- if (!exists) missing.push(out);
12806
+ const exists2 = await inp.ctx.ws.exists(out);
12807
+ if (!exists2) missing.push(out);
12059
12808
  }
12060
12809
  return { ok: missing.length === 0, missing };
12061
12810
  }
@@ -12071,13 +12820,21 @@ function renderUserPrompt(inp, toolDocs) {
12071
12820
  ${truncate2(s.content, s.path === ".xcompiler/architecture-contract.json" ? architectureLimit : snippetLimit)}
12072
12821
  \`\`\``
12073
12822
  ).join("\n\n");
12074
- const dbg = inp.debugContext ? `## debug failure log
12823
+ const dbg = inp.debugContext ? [
12824
+ inp.debugContext.issueId ? `## issue
12825
+ id: ${inp.debugContext.issueId}
12826
+ ` : "",
12827
+ inp.debugContext.debugBrief ? `${inp.debugContext.debugBrief}
12828
+ ` : "",
12829
+ `## compact failure evidence
12075
12830
  \`\`\`
12076
12831
  ${truncate2(inp.debugContext.failureLog, failureLogLimit)}
12077
12832
  \`\`\`
12078
- ` + (inp.debugContext.suggestions ? `
12833
+ `,
12834
+ inp.debugContext.suggestions ? `
12079
12835
  ${inp.debugContext.suggestions}
12080
- ` : "") : "";
12836
+ ` : ""
12837
+ ].join("\n") : "";
12081
12838
  return [
12082
12839
  `# Step ${inp.step.id} \u2014 ${inp.step.title}`,
12083
12840
  `phase: ${inp.step.phase}`,
@@ -12165,6 +12922,11 @@ function renderFeedback(results, verify, turn) {
12165
12922
  )
12166
12923
  );
12167
12924
  }
12925
+ if (turn.missingOutputStallWarning) {
12926
+ lines.push(
12927
+ `Output progress warning: required outputs have not decreased for ${turn.missingOutputStallWarning.rounds} write/progress rounds. Create these exact missing outputs next: ${turn.missingOutputStallWarning.missing}. Do not keep rewriting files that already satisfy declared outputs.`
12928
+ );
12929
+ }
12168
12930
  if (turn.readOnlyRecoveryWarning) {
12169
12931
  lines.push(M.executorFeedbackReadOnlyRecoveryRequired);
12170
12932
  if (!verify.ok && verify.missing.length > 0) {
@@ -12173,6 +12935,9 @@ function renderFeedback(results, verify, turn) {
12173
12935
  );
12174
12936
  }
12175
12937
  }
12938
+ if (turn.issueResolutionPlanMissing) {
12939
+ lines.push(M.executorFeedbackIssueResolutionPlanMissing);
12940
+ }
12176
12941
  if (turn.unresolvedFailures && turn.unresolvedFailures.length > 0) {
12177
12942
  lines.push(
12178
12943
  `Unresolved tool failures remain: ${turn.unresolvedFailures.map((failure2) => truncate2(failure2, 1200)).join("; ")}`
@@ -12383,6 +13148,21 @@ function isTurnObject(value) {
12383
13148
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
12384
13149
  return value;
12385
13150
  }
13151
+ function extractIssueResolutionPlan(turn) {
13152
+ const candidates = [
13153
+ turn.issueResolutionPlan,
13154
+ turn.issue_resolution_plan,
13155
+ turn.resolutionPlan,
13156
+ turn.handlingPlan,
13157
+ turn.fixPlan
13158
+ ];
13159
+ for (const value of candidates) {
13160
+ if (typeof value === "string" && value.trim().length > 0) {
13161
+ return truncate2(value.trim(), 2400);
13162
+ }
13163
+ }
13164
+ return void 0;
13165
+ }
12386
13166
  function normalizeJsonLikeStrings(text) {
12387
13167
  let out = "";
12388
13168
  let inStr = false;
@@ -12520,8 +13300,301 @@ function buildDefaultSkills() {
12520
13300
  }
12521
13301
 
12522
13302
  // src/core/debug_cache.ts
12523
- import { promises as fs23 } from "fs";
13303
+ import { promises as fs24 } from "fs";
12524
13304
  import path24 from "path";
13305
+
13306
+ // src/core/debug_brief.ts
13307
+ var MAX_EVIDENCE = 8;
13308
+ var MAX_EVIDENCE_LINE = 260;
13309
+ var MAX_TOOL_FAILURES = 6;
13310
+ var MAX_FAILED_TESTS = 8;
13311
+ var MAX_FILES = 10;
13312
+ function buildDebugBrief(input) {
13313
+ const reason = oneLine(input.reason ?? "");
13314
+ const raw = `${reason}
13315
+ ${input.failureLog ?? ""}`.trim();
13316
+ const sections = splitFailureSections(raw);
13317
+ const rootSignals = extractSignals(sections.root || raw);
13318
+ const latestSignals = sections.latest ? extractSignals(sections.latest) : void 0;
13319
+ const chosen = choosePrimarySignals(rootSignals, latestSignals);
13320
+ const category = chosen.category;
13321
+ const primaryError = chosen.primaryError || reason || "Unknown failure";
13322
+ const failedTests = dedup4([...rootSignals.failedTests ?? [], ...latestSignals?.failedTests ?? []]).slice(0, MAX_FAILED_TESTS);
13323
+ const files = dedup4([...rootSignals.files ?? [], ...latestSignals?.files ?? []]).slice(0, MAX_FILES);
13324
+ const toolFailures = dedup4([...rootSignals.toolFailures ?? [], ...latestSignals?.toolFailures ?? []]).slice(0, MAX_TOOL_FAILURES);
13325
+ const statusCodes = dedup4([...rootSignals.statusCodes ?? [], ...latestSignals?.statusCodes ?? []]).slice(0, 6);
13326
+ const evidence = selectEvidenceLines(raw, category, primaryError, failedTests, files, toolFailures);
13327
+ return {
13328
+ version: 1,
13329
+ category,
13330
+ summary: buildSummary({ category, reason, primaryError, failedTests, files, phase: input.phase, targetPhase: input.targetPhase }),
13331
+ primaryError,
13332
+ debugDemand: buildDebugDemand(category, input.targetPhase ?? input.phase, statusCodes),
13333
+ failedTests,
13334
+ files,
13335
+ toolFailures,
13336
+ statusCodes,
13337
+ evidence: evidence.lines,
13338
+ omittedEvidenceLines: evidence.omitted
13339
+ };
13340
+ }
13341
+ function renderDebugBriefForPrompt(brief) {
13342
+ const lines = [
13343
+ "## debug brief",
13344
+ `- category: ${brief.category}`,
13345
+ `- summary: ${brief.summary}`,
13346
+ `- primaryError: ${brief.primaryError}`,
13347
+ `- debugDemand: ${brief.debugDemand}`
13348
+ ];
13349
+ if (brief.failedTests.length > 0) lines.push(`- failedTests: ${brief.failedTests.join(", ")}`);
13350
+ if (brief.files.length > 0) lines.push(`- likelyFiles: ${brief.files.join(", ")}`);
13351
+ if (brief.toolFailures.length > 0) lines.push(`- toolFailures: ${brief.toolFailures.join(" | ")}`);
13352
+ if (brief.statusCodes.length > 0) lines.push(`- httpStatus: ${brief.statusCodes.join(", ")}`);
13353
+ if (brief.evidence.length > 0) {
13354
+ lines.push("- keyEvidence:");
13355
+ for (const line of brief.evidence) lines.push(` - ${line}`);
13356
+ }
13357
+ if (brief.omittedEvidenceLines > 0) {
13358
+ lines.push(`- omittedEvidenceLines: ${brief.omittedEvidenceLines}`);
13359
+ }
13360
+ return lines.join("\n");
13361
+ }
13362
+ function compactFailureEvidence(input) {
13363
+ const maxChars = input.maxChars ?? 2400;
13364
+ const maxLines = input.maxLines ?? 50;
13365
+ const reason = shouldSuppressReasonInEvidence(input.reason, input.failureLog) ? "" : input.reason ?? "";
13366
+ const raw = `${reason}
13367
+ ${input.failureLog ?? ""}`.trim();
13368
+ if (!raw) return "";
13369
+ const brief = buildDebugBrief({ ...input, reason });
13370
+ const important = selectEvidenceLines(
13371
+ raw,
13372
+ brief.category,
13373
+ brief.primaryError,
13374
+ brief.failedTests,
13375
+ brief.files,
13376
+ brief.toolFailures,
13377
+ Math.min(MAX_EVIDENCE + 4, 12)
13378
+ ).lines;
13379
+ const tail = raw.split(/\r?\n/u).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-Math.max(10, Math.floor(maxLines / 2))).map((line) => truncateLine(line, 320));
13380
+ const lines = dedup4([...important, ...tail]).slice(-maxLines);
13381
+ const joined = lines.join("\n");
13382
+ if (joined.length <= maxChars) return joined;
13383
+ const head = joined.slice(0, Math.floor(maxChars * 0.45));
13384
+ const tailText2 = joined.slice(-Math.floor(maxChars * 0.45));
13385
+ return `${head}
13386
+ ... [debug evidence truncated ${joined.length - head.length - tailText2.length} chars]
13387
+ ${tailText2}`;
13388
+ }
13389
+ function splitFailureSections(text) {
13390
+ const marker = /\n##\s+latest Debugger attempt failure\b/u.exec(text);
13391
+ if (!marker) return { root: text };
13392
+ return {
13393
+ root: text.slice(0, marker.index).trim(),
13394
+ latest: text.slice(marker.index + 1).trim()
13395
+ };
13396
+ }
13397
+ function choosePrimarySignals(root, latest) {
13398
+ if (!latest) return root;
13399
+ if (isProcessNoise(latest.category) && !isProcessNoise(root.category)) return root;
13400
+ if (latest.category !== "unknown") return latest;
13401
+ return root.category !== "unknown" ? root : latest;
13402
+ }
13403
+ function extractSignals(text) {
13404
+ const lines = normalizedLines(text);
13405
+ const failedTests = extractFailedTests(text);
13406
+ const files = extractFiles(text);
13407
+ const toolFailures = extractToolFailures(lines);
13408
+ const statusCodes = extractStatusCodes(text);
13409
+ const category = classify(text, lines, failedTests, toolFailures);
13410
+ return {
13411
+ category,
13412
+ primaryError: findPrimaryError(text, lines, category, failedTests, toolFailures),
13413
+ failedTests,
13414
+ files,
13415
+ toolFailures,
13416
+ statusCodes
13417
+ };
13418
+ }
13419
+ function classify(text, lines, failedTests, toolFailures) {
13420
+ const lower = text.toLowerCase();
13421
+ if (/openai|ollama|openrouter|llm provider|provider_call_failed|all llm providers failed|prefill_memory_exceeded|context window|token limit|prompt too long/u.test(lower)) {
13422
+ return "llm_provider";
13423
+ }
13424
+ if (/repeated read-only\/probe actions|read-only recovery mode|low-quality debugger response/u.test(lower)) {
13425
+ return "tool_loop";
13426
+ }
13427
+ if (/permission denied/u.test(lower)) return "permission_denied";
13428
+ if (/network api failure detected|http_fetch|https?:\/\/|http\s+(?:401|403|404|408|409|410|422|429|5\d\d)|timed out|timeout/u.test(lower)) {
13429
+ if (!/openai|ollama|openrouter|llm provider/u.test(lower)) return "network_api_failure";
13430
+ }
13431
+ if (/modulenotfounderror|importerror/u.test(lower)) return "import_error";
13432
+ if (/could not find a version|no matching distribution|pip install|add_dependency/u.test(lower)) return "dependency_error";
13433
+ if (/syntaxerror|indentationerror|taberror/u.test(lower)) return "syntax_error";
13434
+ if (/outputs? (?:still )?missing|missing required outputs?|outputs? \S*缺失|仍缺失/u.test(lower)) return "missing_output";
13435
+ if (failedTests.length > 0 || /pytest exit=\s*[1-9]|tests? exit=\s*[1-9]|test gate|测试门禁|vitest|assertionerror|failed tests?|test failures?|(?:unit|integration|module|functional|gate) regression failed/u.test(lower)) {
13436
+ return "test_failure";
13437
+ }
13438
+ if (toolFailures.length > 0) return "exception";
13439
+ if (lines.some((line) => /error|exception|traceback|failed/i.test(line))) return "exception";
13440
+ return "unknown";
13441
+ }
13442
+ function findPrimaryError(text, lines, category, failedTests, toolFailures) {
13443
+ if (failedTests.length > 0 && category === "test_failure") return `failed test: ${failedTests[0]}`;
13444
+ if (toolFailures.length > 0 && (category === "tool_loop" || category === "exception")) return toolFailures[0];
13445
+ const patterns = [
13446
+ /(?:SyntaxError|IndentationError|TabError|ModuleNotFoundError|ImportError|AssertionError|TypeError|ValueError|FileNotFoundError|AttributeError|RuntimeError):[^\n]+/u,
13447
+ /\bFAILED\s+[^\n]+/u,
13448
+ /\b(?:pytest|vitest)[^\n]*(?:exit|failed|FAIL)[^\n]*/iu,
13449
+ /\bHTTP\s+(?:401|403|404|408|409|410|422|429|5\d\d)[^\n]*/iu,
13450
+ /Network API failure detected[^\n]*/iu,
13451
+ /outputs?[^\n]*(?:missing|缺失|仍缺失)[^\n]*/iu,
13452
+ /repeated read-only\/probe actions[^\n]*/iu
13453
+ ];
13454
+ for (const pattern of patterns) {
13455
+ const match = text.match(pattern)?.[0];
13456
+ if (match) return oneLine(match);
13457
+ }
13458
+ const lastMeaningful = [...lines].reverse().find((line) => /error|exception|failed|exit=\s*[1-9]|缺失/i.test(line));
13459
+ return oneLine(lastMeaningful ?? lines.at(-1) ?? "Unknown failure");
13460
+ }
13461
+ function buildSummary(args) {
13462
+ const scope = args.targetPhase || args.phase ? ` in ${args.targetPhase ?? args.phase}` : "";
13463
+ if (args.failedTests.length > 0) return `${args.category}${scope}: ${args.failedTests[0]} failed`;
13464
+ if (args.files.length > 0) return `${args.category}${scope}: ${args.primaryError} (${args.files[0]})`;
13465
+ return `${args.category}${scope}: ${args.primaryError || args.reason || "failure"}`;
13466
+ }
13467
+ function buildDebugDemand(category, phase, statusCodes = []) {
13468
+ const phaseHint = phase ? ` for ${phase}` : "";
13469
+ switch (category) {
13470
+ case "test_failure":
13471
+ return `Fix the root implementation/contract defect${phaseHint}, then run the smallest relevant test command before done=true. Do not rewrite fixtures unless evidence says the fixture is missing or malformed.`;
13472
+ case "syntax_error":
13473
+ return `Read the referenced file, patch the syntax/indentation at the failing location, then run tests.`;
13474
+ case "import_error":
13475
+ return `Resolve the real import/module path or dependency. Do not add fake fallback modules or swallow ImportError in production code.`;
13476
+ case "dependency_error":
13477
+ return `Replace hallucinated dependency names with real package names and update the manifest via add_dependency.`;
13478
+ case "network_api_failure":
13479
+ return networkDemand(statusCodes);
13480
+ case "missing_output":
13481
+ return `Create or repair the declared output files. Do not mark done=true until verify outputs passes.`;
13482
+ case "tool_loop":
13483
+ return `Stop repeating read-only/probe actions. Use the current evidence to make a patch/write/dependency change or run a concrete verification command.`;
13484
+ case "permission_denied":
13485
+ return `Treat the denied operation as a real blocker unless an allowed alternative exists; do not bypass the permission gate.`;
13486
+ case "llm_provider":
13487
+ return `This is provider/context infrastructure, not a project code bug. Reduce prompt/debug context or fix provider config before retrying.`;
13488
+ case "exception":
13489
+ return `Localize the exception to a file or tool call, make the smallest allowed repair, then verify.`;
13490
+ case "unknown":
13491
+ return `Read the most relevant files and produce a concrete diagnosis before making a minimal allowed repair.`;
13492
+ }
13493
+ }
13494
+ function networkDemand(statusCodes) {
13495
+ if (statusCodes.some((code) => code === "401" || code === "403")) {
13496
+ return "The API is unauthorized/forbidden. If no user key/token is available, switch to a public no-key API and verify the real integration.";
13497
+ }
13498
+ if (statusCodes.some((code) => code === "404" || code === "410")) {
13499
+ return "The API URL/resource is unavailable. Stop retrying the same URL; switch to a maintained endpoint and verify response shape.";
13500
+ }
13501
+ if (statusCodes.includes("429")) {
13502
+ return "The API is rate-limited. Switch to a suitable fallback API or implement explicit retry/cache behaviour and tests.";
13503
+ }
13504
+ if (statusCodes.some((code) => /^5/u.test(code))) {
13505
+ return "The API server failed. Use a stable fallback endpoint or fail closed with a clear user-visible error path.";
13506
+ }
13507
+ return "Locate the failing URL/status/body, patch the real API integration, and verify with run_program plus tests. Do not hide the API failure.";
13508
+ }
13509
+ function isProcessNoise(category) {
13510
+ return ["tool_loop", "llm_provider", "permission_denied", "exception"].includes(category);
13511
+ }
13512
+ function shouldSuppressReasonInEvidence(reason, failureLog) {
13513
+ if (!reason || !failureLog?.trim()) return false;
13514
+ const lowerReason = reason.toLowerCase();
13515
+ const lowerLog = failureLog.toLowerCase();
13516
+ const hasRootSignal = /pytest exit=\s*[1-9]|tests? exit=\s*[1-9]|test gate|测试门禁|failed\s+(?:tests?|src)\/|assertionerror|syntaxerror|modulenotfounderror|importerror|network api failure|http\s+(?:401|403|404|408|409|410|422|429|5\d\d)|outputs?.*(?:missing|缺失|仍缺失)/u.test(lowerLog);
13517
+ if (!hasRootSignal) return false;
13518
+ return /script exhausted|completed phase debug finished without|repeated read-only\/probe actions|read-only recovery mode|low-quality debugger response|openai http (?:400|401|403|408|409|429|5\d\d)|rate limit exceeded|free-models-per-day|stream (?:wall-clock|idle)|request timed out|provider_call_failed|all llm providers failed/u.test(lowerReason);
13519
+ }
13520
+ function extractFailedTests(text) {
13521
+ const patterns = [
13522
+ /\bFAILED\s+([^\s]+(?:\.py|\.ts|\.tsx|\.js|\.jsx)(?:::[^\s]+)?)/gu,
13523
+ /([^\s]+(?:\.py|\.ts|\.tsx|\.js|\.jsx)::[A-Za-z0-9_:[\].-]+)/gu,
13524
+ /[×x]\s+([^\n]+?>\s+[^\n]+)/gu
13525
+ ];
13526
+ const out = [];
13527
+ for (const pattern of patterns) {
13528
+ for (const match of text.matchAll(pattern)) {
13529
+ const value = oneLine(match[1] ?? "");
13530
+ if (value) out.push(value);
13531
+ }
13532
+ }
13533
+ return dedup4(out);
13534
+ }
13535
+ function extractFiles(text) {
13536
+ const out = [];
13537
+ const patterns = [
13538
+ /\b((?:src|tests?|docs)\/[A-Za-z0-9_./-]+\.(?:py|ts|tsx|js|jsx|json|md|dbc|csv|xlsx?))/gu,
13539
+ /File\s+["']([^"']+)["']/gu
13540
+ ];
13541
+ for (const pattern of patterns) {
13542
+ for (const match of text.matchAll(pattern)) {
13543
+ const file = normalizePath2(match[1] ?? "");
13544
+ if (file && !file.includes("node_modules/")) out.push(file);
13545
+ }
13546
+ }
13547
+ return dedup4(out);
13548
+ }
13549
+ function extractToolFailures(lines) {
13550
+ return lines.filter((line) => /(?:\b(?:FAIL|failed|denied|exit=[1-9]|Error:)|失败)/iu.test(line)).filter((line) => /\b(?:run_tests|run_program|write_file|replace_in_file|apply_patch|append_file|http_fetch|add_dependency|read_file)\b/u.test(line)).map((line) => oneLine(line)).slice(0, MAX_TOOL_FAILURES);
13551
+ }
13552
+ function extractStatusCodes(text) {
13553
+ const out = [];
13554
+ for (const match of text.matchAll(/\b(?:HTTP\s*)?(401|403|404|408|409|410|422|429|5\d\d)\b/giu)) {
13555
+ out.push(match[1]);
13556
+ }
13557
+ return dedup4(out);
13558
+ }
13559
+ function selectEvidenceLines(text, category, primaryError, failedTests, files, toolFailures, maxEvidence = MAX_EVIDENCE) {
13560
+ const lines = normalizedLines(text);
13561
+ const needles = [
13562
+ primaryError,
13563
+ ...failedTests,
13564
+ ...files,
13565
+ ...toolFailures,
13566
+ category === "network_api_failure" ? "http" : "",
13567
+ category === "missing_output" ? "missing" : "",
13568
+ category === "tool_loop" ? "read-only" : ""
13569
+ ].map((item) => item.toLowerCase()).filter(Boolean);
13570
+ const selected = [];
13571
+ for (const line of lines) {
13572
+ const lower = line.toLowerCase();
13573
+ if (needles.some((needle) => lower.includes(needle.slice(0, 80))) || /\b(?:FAILED|Traceback|Error|Exception|AssertionError|SyntaxError|ModuleNotFoundError|pytest exit=|HTTP\s*[45]\d\d|outputs?.*missing)\b/iu.test(line) || /失败/u.test(line)) {
13574
+ selected.push(truncateLine(line, MAX_EVIDENCE_LINE));
13575
+ }
13576
+ }
13577
+ const compact = dedup4(selected).slice(0, maxEvidence);
13578
+ return { lines: compact, omitted: Math.max(0, selected.length - compact.length) };
13579
+ }
13580
+ function normalizedLines(text) {
13581
+ return text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !/^##\s+历史\s+DEBUG/u.test(line)).filter((line) => !/^##\s+修复建议/u.test(line)).filter((line) => !/^prior suggestions:/iu.test(line));
13582
+ }
13583
+ function oneLine(text) {
13584
+ return truncateLine(text.replace(/\s+/gu, " ").trim(), MAX_EVIDENCE_LINE);
13585
+ }
13586
+ function truncateLine(text, max) {
13587
+ if (text.length <= max) return text;
13588
+ return `${text.slice(0, Math.max(0, max - 24))}... [truncated ${text.length - max + 24} chars]`;
13589
+ }
13590
+ function normalizePath2(file) {
13591
+ return file.replace(/\\/g, "/").replace(/^.*?((?:src|tests?|docs)\/)/u, "$1");
13592
+ }
13593
+ function dedup4(items) {
13594
+ return [...new Set(items.filter((item) => String(item ?? "").length > 0))];
13595
+ }
13596
+
13597
+ // src/core/debug_cache.ts
12525
13598
  var EMPTY = { version: 1, steps: {} };
12526
13599
  function sanitizeDebugFailureLogForPrompt(log) {
12527
13600
  const lines = log.split(/\r?\n/u);
@@ -12533,19 +13606,23 @@ function sanitizeDebugFailureLogForPrompt(log) {
12533
13606
  mode = "skip-history";
12534
13607
  continue;
12535
13608
  }
13609
+ if (/^##\s+(?:debug brief|root issue brief|current retry brief)\b/u.test(line)) {
13610
+ mode = "skip-brief";
13611
+ continue;
13612
+ }
12536
13613
  if (/^##\s+修复建议/u.test(line)) {
12537
13614
  mode = "skip-suggestions";
12538
13615
  continue;
12539
13616
  }
12540
13617
  if (mode === "skip-history") {
12541
- if (/^(原因:|Reason:|##\s+debug failure log\b)/u.test(line)) {
13618
+ if (/^(原因:|Reason:|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
12542
13619
  mode = "keep";
12543
13620
  } else {
12544
13621
  continue;
12545
13622
  }
12546
13623
  }
12547
13624
  if (mode === "skip-suggestions") {
12548
- if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+历史\s+DEBUG\s+尝试|##\s+debug failure log\b)/u.test(line)) {
13625
+ if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+历史\s+DEBUG\s+尝试|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
12549
13626
  mode = "keep";
12550
13627
  if (/^##\s+历史\s+DEBUG\s+尝试/u.test(line)) {
12551
13628
  mode = "skip-history";
@@ -12555,6 +13632,13 @@ function sanitizeDebugFailureLogForPrompt(log) {
12555
13632
  continue;
12556
13633
  }
12557
13634
  }
13635
+ if (mode === "skip-brief") {
13636
+ if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+修复建议|##\s+历史\s+DEBUG\s+尝试|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
13637
+ mode = "keep";
13638
+ } else {
13639
+ continue;
13640
+ }
13641
+ }
12558
13642
  if (/^\s*suggestions:\s/u.test(line)) continue;
12559
13643
  if (/^(原因:|Reason:)/u.test(line)) {
12560
13644
  const normalized = line.trim();
@@ -12592,7 +13676,7 @@ var DebugCache = class {
12592
13676
  if (this.loaded) return;
12593
13677
  this.loaded = true;
12594
13678
  try {
12595
- const raw = await fs23.readFile(this.file, "utf8");
13679
+ const raw = await fs24.readFile(this.file, "utf8");
12596
13680
  const parsed = JSON.parse(raw);
12597
13681
  if (parsed && parsed.version === 1 && parsed.steps && typeof parsed.steps === "object") {
12598
13682
  this.data = { version: 1, steps: parsed.steps };
@@ -12620,11 +13704,24 @@ var DebugCache = class {
12620
13704
  const cleanedFailureLog = stripNestedLatestDebuggerFailures(
12621
13705
  sanitizeDebugFailureLogForPrompt(entry.failureLogTail ?? "")
12622
13706
  );
13707
+ const debugBrief = buildDebugBrief({
13708
+ reason: entry.reason,
13709
+ failureLog: cleanedFailureLog
13710
+ });
13711
+ const compactFailureLog = compactFailureEvidence({
13712
+ reason: entry.reason,
13713
+ failureLog: cleanedFailureLog,
13714
+ maxChars: 3600,
13715
+ maxLines: this.maxLogLines
13716
+ });
12623
13717
  const e = {
12624
13718
  attempt: entry.attempt,
12625
13719
  ts: entry.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
12626
13720
  reason: (entry.reason ?? "").slice(0, 500),
12627
- failureLogTail: tail(cleanedFailureLog),
13721
+ failureLogTail: tail(compactFailureLog),
13722
+ debugBrief,
13723
+ contextMode: entry.contextMode,
13724
+ testScopeArgs: entry.testScopeArgs,
12628
13725
  suggestions: entry.suggestions,
12629
13726
  snapshotSha: entry.snapshotSha,
12630
13727
  metrics: entry.metrics
@@ -12689,8 +13786,10 @@ var DebugCache = class {
12689
13786
  const m = e.metrics ? ` [health=${e.metrics.healthScore.toFixed(2)} parseFail=${e.metrics.parseFailures} repeat=${e.metrics.repeatedTurns}]` : "";
12690
13787
  const sugg = e.suggestions && e.suggestions.length > 0 ? `
12691
13788
  prior suggestions: omitted (${e.suggestions.length}) to avoid stale guidance; use the current failure log first.` : "";
13789
+ const brief = e.debugBrief ? `
13790
+ brief: ${e.debugBrief.category} \u2014 ${e.debugBrief.summary}` : "";
12692
13791
  return `- attempt #${e.attempt} @ ${e.ts}${m}
12693
- reason: ${e.reason}${sugg}`;
13792
+ reason: ${e.reason}${brief}${sugg}`;
12694
13793
  });
12695
13794
  const omitted = [
12696
13795
  noisyCount > 0 ? `- omitted ${noisyCount} noisy provider/read-only/recovery attempt(s); keep focus on the current actionable failure.` : "",
@@ -12706,18 +13805,609 @@ var DebugCache = class {
12706
13805
  ].join("\n");
12707
13806
  }
12708
13807
  async save() {
12709
- await fs23.mkdir(path24.dirname(this.file), { recursive: true });
12710
- await fs23.writeFile(this.file, JSON.stringify(this.data, null, 2), "utf8");
13808
+ await fs24.mkdir(path24.dirname(this.file), { recursive: true });
13809
+ await fs24.writeFile(this.file, JSON.stringify(this.data, null, 2), "utf8");
12711
13810
  }
12712
13811
  };
12713
13812
  function isNoisyDebugReason(reason) {
12714
13813
  const text = reason.toLowerCase();
12715
- return /repeated read-only\/probe actions without progress/u.test(text) || /read-only recovery mode repeated probe actions/u.test(text) || /low-quality debugger response/u.test(text) || /without repair evidence/u.test(text) || /had an unresolved failure from a previous run.*rolling back/u.test(text) || /tool verification failed.*rolling back/u.test(text) || /openai http (?:400|401|403|408|409|429|5\d\d)/u.test(text) || /rate limit exceeded|free-models-per-day|retry_after_seconds|retry-after/u.test(text) || /response_format|json_object|json_schema|invalid_request_body/u.test(text) || /stream (?:wall-clock|idle)|request timed out|fetch failed/u.test(text);
13814
+ return /repeated read-only\/probe actions without progress/u.test(text) || /read-only recovery mode repeated probe actions/u.test(text) || /low-quality debugger response/u.test(text) || /script exhausted/u.test(text) || /without repair evidence/u.test(text) || /without a successful repair mutation or verification tool call/u.test(text) || /had an unresolved failure from a previous run.*rolling back/u.test(text) || /tool verification failed.*rolling back/u.test(text) || /openai http (?:400|401|403|408|409|429|5\d\d)/u.test(text) || /rate limit exceeded|free-models-per-day|retry_after_seconds|retry-after/u.test(text) || /response_format|json_object|json_schema|invalid_request_body/u.test(text) || /stream (?:wall-clock|idle)|request timed out|fetch failed/u.test(text);
12716
13815
  }
12717
13816
 
12718
- // src/core/project_audit.ts
12719
- import { promises as fs24 } from "fs";
13817
+ // src/core/debug_wiki.ts
13818
+ import { promises as fs25 } from "fs";
12720
13819
  import path25 from "path";
13820
+ import { fileURLToPath } from "url";
13821
+ import YAML3 from "yaml";
13822
+ var DEFAULT_DEBUG_WIKI_REL_PATH = ".xcompiler/debug-wiki";
13823
+ var BUNDLED_DEBUG_WIKI_REL_PATH = "debug-wiki";
13824
+ var DEBUG_WIKI_VERSION = 1;
13825
+ var LAYERS = ["system", "agent", "external"];
13826
+ var EMPTY_STATS = { uses: 0, successes: 0, failures: 0 };
13827
+ function defaultDebugWikiPath() {
13828
+ const configured = xcEnv("PATH")?.trim();
13829
+ const base = configured ? path25.resolve(configured) : path25.resolve(path25.dirname(fileURLToPath(import.meta.url)), "..", "..");
13830
+ return path25.join(base, DEFAULT_DEBUG_WIKI_REL_PATH);
13831
+ }
13832
+ function bundledDebugWikiPath() {
13833
+ return path25.join(path25.resolve(path25.dirname(fileURLToPath(import.meta.url)), "..", ".."), BUNDLED_DEBUG_WIKI_REL_PATH);
13834
+ }
13835
+ var DebugWiki = class {
13836
+ loaded = false;
13837
+ entries = [];
13838
+ rootPath;
13839
+ filePath;
13840
+ bundledPath;
13841
+ constructor(rootPath, opts = {}) {
13842
+ this.rootPath = path25.resolve(rootPath);
13843
+ this.filePath = this.rootPath;
13844
+ this.bundledPath = opts.bundledPath ?? bundledDebugWikiPath();
13845
+ }
13846
+ async load() {
13847
+ if (this.loaded) return;
13848
+ this.loaded = true;
13849
+ await this.ensureLayout();
13850
+ await this.ensureRootReadme();
13851
+ await this.ensureOperationLog();
13852
+ await this.copyBundledLayers();
13853
+ this.entries = [];
13854
+ for (const layer of LAYERS) {
13855
+ this.entries.push(...await this.readLayer(layer));
13856
+ }
13857
+ await this.applyFeedbackLog();
13858
+ await this.writeIndex();
13859
+ }
13860
+ async search(brief, opts = {}) {
13861
+ await this.load();
13862
+ const limit = opts.limit ?? 3;
13863
+ return this.rank(brief, opts.language).filter((match) => match.score >= 4).slice(0, limit);
13864
+ }
13865
+ async recordUse(entryIds, input) {
13866
+ await this.load();
13867
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13868
+ const feedback = this.feedbackFrom(entryIds, input, now, "used");
13869
+ if (feedback.length === 0) return;
13870
+ for (const item of feedback) {
13871
+ const entry = this.byId(item.entryId);
13872
+ if (!entry) continue;
13873
+ entry.stats.uses += 1;
13874
+ entry.lastUsedAt = now;
13875
+ entry.updatedAt = now;
13876
+ pushFeedback(entry, item);
13877
+ }
13878
+ await this.appendLayerFeedback(feedback);
13879
+ await this.persistExternalEntries();
13880
+ await this.writeIndex(now);
13881
+ await this.appendOperationLog({
13882
+ at: now,
13883
+ action: "use",
13884
+ entryIds: feedback.map((item) => item.entryId).filter(Boolean),
13885
+ issueId: input.issueId,
13886
+ stepId: input.stepId,
13887
+ phase: input.phase,
13888
+ summary: input.brief.summary
13889
+ });
13890
+ }
13891
+ async recordFailure(entryIds, input) {
13892
+ await this.load();
13893
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13894
+ const feedback = this.feedbackFrom(entryIds, input, now, "failure", input.reason);
13895
+ if (feedback.length === 0) return;
13896
+ for (const item of feedback) {
13897
+ const entry = this.byId(item.entryId);
13898
+ if (!entry) continue;
13899
+ entry.stats.failures += 1;
13900
+ entry.status = "needs_review";
13901
+ entry.updatedAt = now;
13902
+ pushFeedback(entry, item);
13903
+ }
13904
+ await this.appendLayerFeedback(feedback);
13905
+ await this.persistExternalEntries();
13906
+ await this.writeIndex(now);
13907
+ await this.appendOperationLog({
13908
+ at: now,
13909
+ action: "failure",
13910
+ entryIds: feedback.map((item) => item.entryId).filter(Boolean),
13911
+ issueId: input.issueId,
13912
+ stepId: input.stepId,
13913
+ phase: input.phase,
13914
+ summary: input.brief.summary,
13915
+ reason: input.reason
13916
+ });
13917
+ }
13918
+ async recordResolution(input) {
13919
+ await this.load();
13920
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13921
+ const used = this.byIds(input.usedEntryIds ?? []);
13922
+ const externalTargets = used.filter((entry) => entry.layer === "external");
13923
+ const target = externalTargets.length > 0 ? externalTargets : this.rank(input.brief, input.language).filter((match) => match.entry.layer === "external" && match.score >= 8).slice(0, 1).map((m) => m.entry);
13924
+ const updated = [];
13925
+ let createdId;
13926
+ for (const entry of target) {
13927
+ this.applyResolution(entry, input, now, entry.stats.failures > 0 ? "corrected" : "success");
13928
+ updated.push(entry.id);
13929
+ }
13930
+ if (updated.length === 0) {
13931
+ const created = createEntry(input, now, this.nextExternalId(now), "external");
13932
+ created.supersedes = used.length > 0 ? used.map((entry) => entry.id) : void 0;
13933
+ this.entries.push(created);
13934
+ createdId = created.id;
13935
+ }
13936
+ const correctedFeedback = this.feedbackFrom(
13937
+ used.filter((entry) => entry.layer !== "external").map((entry) => entry.id),
13938
+ input,
13939
+ now,
13940
+ "corrected"
13941
+ );
13942
+ for (const item of correctedFeedback) {
13943
+ const entry = this.byId(item.entryId);
13944
+ if (!entry) continue;
13945
+ entry.stats.successes += 1;
13946
+ entry.status = "active";
13947
+ entry.updatedAt = now;
13948
+ pushFeedback(entry, item);
13949
+ }
13950
+ await this.appendLayerFeedback(correctedFeedback);
13951
+ await this.persistExternalEntries();
13952
+ await this.writeIndex(now);
13953
+ await this.appendOperationLog({
13954
+ at: now,
13955
+ action: createdId ? "resolution_created" : "resolution_updated",
13956
+ entryIds: createdId ? [createdId] : updated,
13957
+ issueId: input.issueId,
13958
+ stepId: input.stepId,
13959
+ phase: input.phase,
13960
+ summary: input.brief.summary
13961
+ });
13962
+ return createdId ? { created: createdId, updated: [] } : { updated };
13963
+ }
13964
+ async ensureLayout() {
13965
+ for (const layer of LAYERS) {
13966
+ await fs25.mkdir(this.layerDir(layer), { recursive: true });
13967
+ }
13968
+ }
13969
+ async ensureRootReadme() {
13970
+ const to = path25.join(this.rootPath, "README.md");
13971
+ if (await exists(to)) return;
13972
+ const from = path25.join(this.bundledPath, "README.md");
13973
+ const fallback = defaultDebugWikiReadme();
13974
+ const text = await fs25.readFile(from, "utf8").catch(() => fallback);
13975
+ await fs25.writeFile(to, text.endsWith("\n") ? text : `${text}
13976
+ `, "utf8");
13977
+ }
13978
+ async ensureOperationLog() {
13979
+ const file = this.operationLogPath();
13980
+ if (await exists(file)) return;
13981
+ await fs25.writeFile(file, "# XCompiler Debug Wiki Log\n\nAppend-only operational notes for retrieval, failed reuse, and confirmed repairs.\n", "utf8");
13982
+ }
13983
+ async copyBundledLayers() {
13984
+ if (path25.resolve(this.bundledPath) === this.rootPath) return;
13985
+ for (const layer of ["system", "agent"]) {
13986
+ const from = path25.join(this.bundledPath, "wiki", layer);
13987
+ const to = this.layerDir(layer);
13988
+ if (!await exists(from)) continue;
13989
+ await fs25.cp(from, to, { recursive: true, force: true });
13990
+ }
13991
+ }
13992
+ async readLayer(layer) {
13993
+ const dir = this.layerDir(layer);
13994
+ const files = (await fs25.readdir(dir).catch(() => [])).filter((file) => file.endsWith(".md")).sort();
13995
+ const entries = [];
13996
+ for (const file of files) {
13997
+ const abs = path25.join(dir, file);
13998
+ const page = parseWikiPage(await fs25.readFile(abs, "utf8"));
13999
+ const entry = normalizeEntry({ ...page.data, layer, solution: page.data.solution ?? page.body }, layer);
14000
+ entry.sourcePath = path25.relative(this.rootPath, abs).replace(/\\/g, "/");
14001
+ entries.push(entry);
14002
+ }
14003
+ return entries;
14004
+ }
14005
+ async applyFeedbackLog() {
14006
+ const log = await fs25.readFile(this.feedbackPath(), "utf8").catch(() => "");
14007
+ for (const line of log.split(/\r?\n/u)) {
14008
+ if (!line.trim()) continue;
14009
+ const item = JSON.parse(line);
14010
+ const entry = this.byId(item.entryId);
14011
+ if (!entry) continue;
14012
+ if (item.kind === "used") entry.stats.uses += 1;
14013
+ if (item.kind === "failure") {
14014
+ entry.stats.failures += 1;
14015
+ entry.status = "needs_review";
14016
+ }
14017
+ if (item.kind === "success" || item.kind === "corrected") {
14018
+ entry.stats.successes += 1;
14019
+ if (item.kind === "corrected") entry.status = "active";
14020
+ }
14021
+ entry.updatedAt = item.at;
14022
+ pushFeedback(entry, item);
14023
+ }
14024
+ }
14025
+ applyResolution(entry, input, now, kind) {
14026
+ entry.status = "active";
14027
+ entry.updatedAt = now;
14028
+ entry.summary = input.brief.summary;
14029
+ entry.primaryError = input.brief.primaryError;
14030
+ entry.debugDemand = input.brief.debugDemand;
14031
+ entry.fingerprints = dedup5([...entry.fingerprints, ...fingerprints(input.brief)]);
14032
+ entry.symptoms = dedup5([...input.brief.evidence, ...entry.symptoms]).slice(0, 12);
14033
+ entry.evidence = dedup5([...input.evidence ?? [], ...entry.evidence]).slice(0, 12);
14034
+ if (input.resolutionPlan?.trim()) entry.resolutionPlan = input.resolutionPlan.trim();
14035
+ entry.solution = mergeSolution(entry.solution, input.solution);
14036
+ entry.repairFiles = dedup5([...input.repairFiles ?? [], ...entry.repairFiles ?? []]).slice(0, 12);
14037
+ entry.stats.successes += 1;
14038
+ pushFeedback(entry, {
14039
+ at: now,
14040
+ kind,
14041
+ entryId: entry.id,
14042
+ issueId: input.issueId,
14043
+ stepId: input.stepId,
14044
+ phase: input.phase,
14045
+ summary: input.brief.summary
14046
+ });
14047
+ }
14048
+ async persistExternalEntries() {
14049
+ for (const entry of this.entries.filter((item) => item.layer === "external")) {
14050
+ const abs = path25.join(this.rootPath, entry.sourcePath ?? this.externalEntryPath(entry));
14051
+ entry.sourcePath = path25.relative(this.rootPath, abs).replace(/\\/g, "/");
14052
+ await fs25.mkdir(path25.dirname(abs), { recursive: true });
14053
+ await fs25.writeFile(abs, renderWikiPage(entry), "utf8");
14054
+ }
14055
+ }
14056
+ async appendFeedback(feedback) {
14057
+ if (feedback.length === 0) return;
14058
+ await fs25.mkdir(path25.dirname(this.feedbackPath()), { recursive: true });
14059
+ await fs25.appendFile(this.feedbackPath(), feedback.map((item) => JSON.stringify(item)).join("\n") + "\n", "utf8");
14060
+ }
14061
+ async appendLayerFeedback(feedback) {
14062
+ await this.appendFeedback(feedback.filter((item) => this.byId(item.entryId)?.layer !== "external"));
14063
+ }
14064
+ async appendOperationLog(entry) {
14065
+ await fs25.mkdir(path25.dirname(this.operationLogPath()), { recursive: true });
14066
+ await fs25.appendFile(this.operationLogPath(), renderOperationLogEntry(entry), "utf8");
14067
+ }
14068
+ async writeIndex(now = (/* @__PURE__ */ new Date()).toISOString()) {
14069
+ const layerCounts = Object.fromEntries(LAYERS.map((layer) => [
14070
+ layer,
14071
+ { entries: this.entries.filter((entry) => entry.layer === layer).length, writable: layer === "external" }
14072
+ ]));
14073
+ const index = {
14074
+ version: DEBUG_WIKI_VERSION,
14075
+ updatedAt: now,
14076
+ root: this.rootPath,
14077
+ layers: layerCounts,
14078
+ entries: this.entries.map((entry) => ({
14079
+ id: entry.id,
14080
+ layer: entry.layer,
14081
+ status: entry.status,
14082
+ category: entry.category,
14083
+ summary: entry.summary,
14084
+ updatedAt: entry.updatedAt,
14085
+ sourcePath: entry.sourcePath
14086
+ }))
14087
+ };
14088
+ await fs25.writeFile(path25.join(this.rootPath, "index.json"), `${JSON.stringify(index, null, 2)}
14089
+ `, "utf8");
14090
+ await fs25.writeFile(path25.join(this.rootPath, "index.md"), renderReadableIndex(index, this.entries), "utf8");
14091
+ }
14092
+ feedbackFrom(ids, input, now, kind, reason) {
14093
+ return dedup5(ids).filter((id) => this.byId(id)).map((id) => ({
14094
+ at: now,
14095
+ kind,
14096
+ entryId: id,
14097
+ issueId: input.issueId,
14098
+ stepId: input.stepId,
14099
+ phase: input.phase,
14100
+ summary: input.brief.summary,
14101
+ reason
14102
+ }));
14103
+ }
14104
+ rank(brief, language) {
14105
+ const queryTokens = new Set(tokensForBrief(brief));
14106
+ const queryFingerprints = fingerprints(brief);
14107
+ return this.entries.filter((entry) => entry.status !== "superseded").map((entry) => {
14108
+ const reasons = [];
14109
+ let score = entry.layer === "agent" ? 1 : entry.layer === "system" ? 0.5 : 0;
14110
+ if (entry.category === brief.category) {
14111
+ score += 4;
14112
+ reasons.push(`category:${entry.category}`);
14113
+ }
14114
+ if (language && entry.language === language) score += 1;
14115
+ const exact = entry.fingerprints.filter((fp) => queryFingerprints.includes(fp));
14116
+ if (exact.length > 0) {
14117
+ score += exact.length * 3;
14118
+ reasons.push(`fingerprint:${exact.length}`);
14119
+ }
14120
+ const entryTokens = new Set(tokensForEntry(entry));
14121
+ let overlap = 0;
14122
+ for (const token of queryTokens) if (entryTokens.has(token)) overlap++;
14123
+ score += Math.min(6, overlap);
14124
+ if (overlap > 0) reasons.push(`tokens:${overlap}`);
14125
+ const confidence = confidenceFor(entry);
14126
+ return { entry, score: score * confidence, confidence, reasons };
14127
+ }).sort((a, b) => b.score - a.score);
14128
+ }
14129
+ byId(id) {
14130
+ return this.entries.find((entry) => entry.id === id);
14131
+ }
14132
+ byIds(ids) {
14133
+ const wanted = new Set(ids);
14134
+ return this.entries.filter((entry) => wanted.has(entry.id));
14135
+ }
14136
+ layerDir(layer) {
14137
+ return path25.join(this.rootPath, "wiki", layer);
14138
+ }
14139
+ feedbackPath() {
14140
+ return path25.join(this.rootPath, "wiki", "external", "feedback.jsonl");
14141
+ }
14142
+ operationLogPath() {
14143
+ return path25.join(this.rootPath, "log.md");
14144
+ }
14145
+ nextExternalId(now) {
14146
+ const stamp = now.replace(/[-:.TZ]/g, "").slice(0, 14);
14147
+ const count = this.entries.filter((entry) => entry.layer === "external").length + 1;
14148
+ return `external.${stamp}.${String(count).padStart(4, "0")}`;
14149
+ }
14150
+ externalEntryPath(entry) {
14151
+ return path25.join("wiki", "external", `${slugify(entry.id)}.md`);
14152
+ }
14153
+ };
14154
+ function renderDebugWikiMatchesForPrompt(matches) {
14155
+ if (matches.length === 0) return "";
14156
+ const lines = [
14157
+ "## debug wiki matches",
14158
+ "LLM-wiki layered retrieval. Treat entries as hypotheses, verify against current files/tests, and stop using any entry that current evidence disproves."
14159
+ ];
14160
+ for (const match of matches) {
14161
+ const entry = match.entry;
14162
+ lines.push(
14163
+ `- ${entry.id} layer=${entry.layer} score=${match.score.toFixed(2)} confidence=${match.confidence.toFixed(2)} status=${entry.status}`,
14164
+ ` problem: [${entry.category}] ${entry.summary}`,
14165
+ ` symptoms: ${entry.symptoms.slice(0, 4).join(" | ") || entry.primaryError}`,
14166
+ entry.resolutionPlan ? ` priorPlan: ${entry.resolutionPlan}` : "",
14167
+ ` confirmedSolution: ${entry.solution}`,
14168
+ ` feedback: uses=${entry.stats.uses} successes=${entry.stats.successes} failures=${entry.stats.failures}`
14169
+ );
14170
+ if (entry.repairFiles?.length) lines.push(` repairFiles: ${entry.repairFiles.join(", ")}`);
14171
+ if (entry.supersedes?.length) lines.push(` supersedes: ${entry.supersedes.join(", ")}`);
14172
+ if (match.reasons.length) lines.push(` matchedBy: ${match.reasons.join(", ")}`);
14173
+ }
14174
+ return lines.filter(Boolean).join("\n");
14175
+ }
14176
+ function createEntry(input, now, id, layer) {
14177
+ return normalizeEntry({
14178
+ id,
14179
+ layer,
14180
+ createdAt: now,
14181
+ updatedAt: now,
14182
+ status: "active",
14183
+ category: input.brief.category,
14184
+ summary: input.brief.summary,
14185
+ primaryError: input.brief.primaryError,
14186
+ debugDemand: input.brief.debugDemand,
14187
+ fingerprints: fingerprints(input.brief),
14188
+ symptoms: input.brief.evidence.slice(0, 12),
14189
+ resolutionPlan: input.resolutionPlan?.trim(),
14190
+ solution: input.solution,
14191
+ evidence: (input.evidence ?? input.brief.evidence).slice(0, 12),
14192
+ sourceIssueId: input.issueId,
14193
+ sourceStepId: input.stepId,
14194
+ sourcePhase: input.phase,
14195
+ targetPhase: input.targetPhase,
14196
+ language: input.language,
14197
+ repairFiles: input.repairFiles?.slice(0, 12),
14198
+ stats: { uses: 0, successes: 1, failures: 0 },
14199
+ feedback: [{ at: now, kind: "success", entryId: id, issueId: input.issueId, stepId: input.stepId, phase: input.phase, summary: input.brief.summary }]
14200
+ }, layer);
14201
+ }
14202
+ function normalizeEntry(raw, layer) {
14203
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14204
+ return {
14205
+ id: String(raw.id ?? `${layer}.${slugify(raw.summary ?? raw.primaryError ?? "entry")}`),
14206
+ layer: raw.layer ?? layer,
14207
+ createdAt: raw.createdAt ?? now,
14208
+ updatedAt: raw.updatedAt ?? raw.createdAt ?? now,
14209
+ status: raw.status ?? "active",
14210
+ category: raw.category ?? "unknown",
14211
+ summary: raw.summary ?? raw.primaryError ?? "Debug wiki entry",
14212
+ primaryError: raw.primaryError ?? raw.summary ?? "",
14213
+ debugDemand: raw.debugDemand ?? "",
14214
+ fingerprints: raw.fingerprints ?? [],
14215
+ symptoms: raw.symptoms ?? [],
14216
+ resolutionPlan: raw.resolutionPlan,
14217
+ solution: raw.solution ?? "",
14218
+ evidence: raw.evidence ?? [],
14219
+ sourceIssueId: raw.sourceIssueId,
14220
+ sourceStepId: raw.sourceStepId,
14221
+ sourcePhase: raw.sourcePhase,
14222
+ targetPhase: raw.targetPhase,
14223
+ language: raw.language,
14224
+ repairFiles: raw.repairFiles ?? [],
14225
+ supersedes: raw.supersedes ?? [],
14226
+ stats: { ...EMPTY_STATS, ...raw.stats ?? {} },
14227
+ lastUsedAt: raw.lastUsedAt,
14228
+ feedback: (raw.feedback ?? []).slice(-20),
14229
+ sourcePath: raw.sourcePath
14230
+ };
14231
+ }
14232
+ function renderWikiPage(entry) {
14233
+ const frontmatter = { ...entry, sourcePath: void 0 };
14234
+ return [
14235
+ "---",
14236
+ YAML3.stringify(frontmatter).trim(),
14237
+ "---",
14238
+ "",
14239
+ `# ${entry.summary}`,
14240
+ "",
14241
+ "## Problem",
14242
+ "",
14243
+ `- category: ${entry.category}`,
14244
+ `- status: ${entry.status}`,
14245
+ `- primaryError: ${entry.primaryError || "n/a"}`,
14246
+ `- debugDemand: ${entry.debugDemand || "n/a"}`,
14247
+ "",
14248
+ "## Resolution Plan",
14249
+ "",
14250
+ entry.resolutionPlan?.trim() || "No explicit plan recorded.",
14251
+ "",
14252
+ "## Confirmed Solution",
14253
+ "",
14254
+ entry.solution.trim() || "No confirmed solution recorded.",
14255
+ "",
14256
+ "## Evidence",
14257
+ "",
14258
+ renderMarkdownList(entry.evidence),
14259
+ "",
14260
+ "## Retrieval",
14261
+ "",
14262
+ `- fingerprints: ${entry.fingerprints.join(", ") || "n/a"}`,
14263
+ `- repairFiles: ${(entry.repairFiles ?? []).join(", ") || "n/a"}`,
14264
+ `- stats: uses=${entry.stats.uses} successes=${entry.stats.successes} failures=${entry.stats.failures}`,
14265
+ "",
14266
+ "## Feedback",
14267
+ "",
14268
+ renderMarkdownList(entry.feedback.slice(-8).map((item) => `${item.at} ${item.kind}: ${item.summary}`)),
14269
+ ""
14270
+ ].join("\n");
14271
+ }
14272
+ function parseWikiPage(text) {
14273
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u);
14274
+ if (!match) return { data: {}, body: text.trim() };
14275
+ return { data: YAML3.parse(match[1] ?? "") ?? {}, body: (match[2] ?? "").trim() };
14276
+ }
14277
+ function fingerprints(brief) {
14278
+ return dedup5([
14279
+ `cat:${brief.category}`,
14280
+ brief.primaryError ? `err:${normalize(brief.primaryError)}` : "",
14281
+ ...brief.failedTests.map((test) => `test:${normalize(test)}`),
14282
+ ...brief.files.map((file) => `file:${normalize(file)}`),
14283
+ ...brief.statusCodes.map((code) => `http:${code}`)
14284
+ ]);
14285
+ }
14286
+ function tokensForBrief(brief) {
14287
+ return tokenize2([brief.summary, brief.primaryError, brief.debugDemand, ...brief.failedTests, ...brief.files, ...brief.evidence, ...brief.statusCodes].join(" "));
14288
+ }
14289
+ function tokensForEntry(entry) {
14290
+ return tokenize2([entry.id, entry.summary, entry.primaryError, entry.debugDemand, entry.resolutionPlan ?? "", entry.solution, ...entry.symptoms, ...entry.evidence, ...entry.repairFiles ?? []].join(" "));
14291
+ }
14292
+ function tokenize2(text) {
14293
+ return dedup5(text.toLowerCase().split(/[^a-z0-9_./:-]+/u).filter((token) => token.length >= 3));
14294
+ }
14295
+ function normalize(text) {
14296
+ return tokenize2(text).slice(0, 24).join(" ");
14297
+ }
14298
+ function renderReadableIndex(index, entries) {
14299
+ const lines = [
14300
+ "# XCompiler Debug Wiki Index",
14301
+ "",
14302
+ `Updated: ${index.updatedAt}`,
14303
+ "",
14304
+ "This file is regenerated from wiki pages and feedback overlays. Edit knowledge pages under `wiki/`, not this index.",
14305
+ "",
14306
+ "## Layers",
14307
+ "",
14308
+ "| Layer | Entries | Writable | Purpose |",
14309
+ "| --- | ---: | --- | --- |"
14310
+ ];
14311
+ for (const layer of LAYERS) {
14312
+ const info = index.layers[layer];
14313
+ lines.push(`| ${layer} | ${info.entries} | ${info.writable ? "yes" : "no"} | ${layerPurpose(layer)} |`);
14314
+ }
14315
+ for (const layer of LAYERS) {
14316
+ const layerEntries = entries.filter((entry) => entry.layer === layer);
14317
+ lines.push("", `## ${layer}`, "");
14318
+ if (layerEntries.length === 0) {
14319
+ lines.push("No entries.");
14320
+ continue;
14321
+ }
14322
+ lines.push("| ID | Status | Category | Summary | Source |", "| --- | --- | --- | --- | --- |");
14323
+ for (const entry of layerEntries) {
14324
+ const source = entry.sourcePath ? `[${entry.sourcePath}](${entry.sourcePath})` : "";
14325
+ lines.push(`| ${escapeTable(entry.id)} | ${entry.status} | ${entry.category} | ${escapeTable(entry.summary)} | ${source} |`);
14326
+ }
14327
+ }
14328
+ return `${lines.join("\n")}
14329
+ `;
14330
+ }
14331
+ function renderOperationLogEntry(entry) {
14332
+ const lines = [
14333
+ "",
14334
+ `- ${entry.at} ${entry.action}: ${entry.entryIds.join(", ") || "none"}`,
14335
+ ` - issue: ${entry.issueId ?? "n/a"}; step: ${entry.stepId ?? "n/a"}; phase: ${entry.phase ?? "n/a"}`,
14336
+ ` - summary: ${entry.summary}`
14337
+ ];
14338
+ if (entry.reason) lines.push(` - reason: ${entry.reason}`);
14339
+ return `${lines.join("\n")}
14340
+ `;
14341
+ }
14342
+ function defaultDebugWikiReadme() {
14343
+ return [
14344
+ "# XCompiler Debug Wiki",
14345
+ "",
14346
+ "This directory is an LLM-wiki style knowledge base for Debugger repair.",
14347
+ "",
14348
+ "- `wiki/system/` contains system-level debug policies and safety rules.",
14349
+ "- `wiki/agent/` contains agent-level calibration knowledge derived from recurring LLM failure patterns.",
14350
+ "- `wiki/external/` stores real project issue resolutions and feedback.",
14351
+ "- `index.md` is a human-readable regenerated catalog.",
14352
+ "- `index.json` is the machine-readable retrieval cache.",
14353
+ "- `log.md` is an append-only operational log.",
14354
+ ""
14355
+ ].join("\n");
14356
+ }
14357
+ function layerPurpose(layer) {
14358
+ switch (layer) {
14359
+ case "system":
14360
+ return "bundled system debug policies";
14361
+ case "agent":
14362
+ return "bundled agent calibration knowledge";
14363
+ case "external":
14364
+ return "local project issue resolutions";
14365
+ }
14366
+ }
14367
+ function renderMarkdownList(items) {
14368
+ const compact = items.map((item) => item.trim()).filter(Boolean);
14369
+ if (compact.length === 0) return "- n/a";
14370
+ return compact.map((item) => `- ${item}`).join("\n");
14371
+ }
14372
+ function escapeTable(text) {
14373
+ return text.replace(/\|/gu, "\\|").replace(/\r?\n/gu, " ");
14374
+ }
14375
+ function confidenceFor(entry) {
14376
+ const total = entry.stats.uses + entry.stats.successes + entry.stats.failures;
14377
+ const base = (entry.stats.successes + 1) / Math.max(2, total + 2);
14378
+ const statusFactor = entry.status === "needs_review" ? 0.45 : 1;
14379
+ const layerFactor = entry.layer === "system" ? 0.9 : 1;
14380
+ return Math.max(0.1, Math.min(1, base * statusFactor * layerFactor));
14381
+ }
14382
+ function mergeSolution(previous, next) {
14383
+ const trimmed = next.trim();
14384
+ if (!trimmed || previous.includes(trimmed)) return previous;
14385
+ if (!previous.trim()) return trimmed;
14386
+ return `${previous.trim()}
14387
+ Corrected/confirmed resolution: ${trimmed}`;
14388
+ }
14389
+ function pushFeedback(entry, feedback) {
14390
+ entry.feedback.push(feedback);
14391
+ if (entry.feedback.length > 20) entry.feedback.splice(0, entry.feedback.length - 20);
14392
+ }
14393
+ function slugify(text) {
14394
+ return text.toLowerCase().replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 96) || "entry";
14395
+ }
14396
+ async function exists(filePath) {
14397
+ try {
14398
+ await fs25.stat(filePath);
14399
+ return true;
14400
+ } catch {
14401
+ return false;
14402
+ }
14403
+ }
14404
+ function dedup5(items) {
14405
+ return [...new Set(items.filter((item) => String(item ?? "").length > 0))];
14406
+ }
14407
+
14408
+ // src/core/project_audit.ts
14409
+ import { promises as fs26 } from "fs";
14410
+ import path26 from "path";
12721
14411
  function renderProjectAuditFailureLog(result) {
12722
14412
  const failed = result.checks.filter((check) => !check.ok);
12723
14413
  const interesting = failed.length > 0 ? failed : result.checks;
@@ -12766,12 +14456,12 @@ async function checkDocumentationBundle(ws, projectType, iterationId) {
12766
14456
  const docs = iterationId ? deliveryDocsForIteration(projectType, iterationId) : deliveryDocsForProjectType(projectType);
12767
14457
  const checks = [];
12768
14458
  for (const doc of docs) {
12769
- const exists = await ws.exists(doc);
14459
+ const exists2 = await ws.exists(doc);
12770
14460
  checks.push({
12771
14461
  name: docCheckName(doc),
12772
- severity: exists ? "info" : "error",
12773
- ok: exists,
12774
- summary: exists ? t().execute.auditDocPresent(doc) : t().execute.auditDocMissing(doc)
14462
+ severity: exists2 ? "info" : "error",
14463
+ ok: exists2,
14464
+ summary: exists2 ? t().execute.auditDocPresent(doc) : t().execute.auditDocMissing(doc)
12775
14465
  });
12776
14466
  }
12777
14467
  return checks;
@@ -12892,14 +14582,14 @@ async function listFiles(ws, dir) {
12892
14582
  async function walk4(abs, rel, out) {
12893
14583
  let entries;
12894
14584
  try {
12895
- entries = await fs24.readdir(abs, { withFileTypes: true });
14585
+ entries = await fs26.readdir(abs, { withFileTypes: true });
12896
14586
  } catch {
12897
14587
  return;
12898
14588
  }
12899
14589
  for (const entry of entries) {
12900
14590
  if (entry.name.startsWith(".")) continue;
12901
14591
  const childRel = `${rel}/${entry.name}`;
12902
- const childAbs = path25.join(abs, entry.name);
14592
+ const childAbs = path26.join(abs, entry.name);
12903
14593
  if (entry.isDirectory()) {
12904
14594
  await walk4(childAbs, childRel, out);
12905
14595
  } else {
@@ -12919,6 +14609,7 @@ var PhaseEngine = class {
12919
14609
  this.skills = opts.skills ?? buildDefaultSkills();
12920
14610
  this.plugins = opts.plugins ?? new PluginHost();
12921
14611
  this.debugCache = new DebugCache(opts.ws.abs(".xcompiler/debug_cache.json"));
14612
+ this.debugWiki = new DebugWiki(opts.debugWikiPath ?? defaultDebugWikiPath());
12922
14613
  }
12923
14614
  opts;
12924
14615
  registry;
@@ -12927,6 +14618,8 @@ var PhaseEngine = class {
12927
14618
  pluginExtensionsApplied = false;
12928
14619
  /** 跨 xcompiler run 持久化的 debug 历史(`<workspace>/.xcompiler/debug_cache.json`)。 */
12929
14620
  debugCache;
14621
+ /** 跨项目 debug 知识库,记录错误摘要、解决方案和正/负反馈。 */
14622
+ debugWiki;
12930
14623
  /** 当前 Plan 的语言 profile(在 run() 起始处按 plan.language 解析)。 */
12931
14624
  profile = getLanguageProfile("python");
12932
14625
  /** 当前 workspace 的项目记忆,用于给执行阶段注入更稳定的跨轮上下文。 */
@@ -12946,6 +14639,21 @@ var PhaseEngine = class {
12946
14639
  spin(text, options) {
12947
14640
  return this.terminalOutput ? spinner(text, options).start() : null;
12948
14641
  }
14642
+ async withDebugWiki(operation, action, fallback) {
14643
+ try {
14644
+ return await action();
14645
+ } catch (err) {
14646
+ const message = err.message;
14647
+ await this.opts.audit.event("note", `debug wiki ${operation} failed: ${message}`, {
14648
+ messageId: "engine.debug_wiki_failed",
14649
+ operation,
14650
+ path: this.debugWiki.filePath,
14651
+ error: message
14652
+ });
14653
+ if (this.opts.debugWikiStrict) throw err;
14654
+ return fallback;
14655
+ }
14656
+ }
12949
14657
  async requestEnginePermission(request2) {
12950
14658
  if (!this.opts.requestPermission) return { approved: true };
12951
14659
  const decision = await this.opts.requestPermission(request2);
@@ -12970,6 +14678,7 @@ var PhaseEngine = class {
12970
14678
  }
12971
14679
  async run(plan) {
12972
14680
  await this.plugins.initialize();
14681
+ await this.withDebugWiki("load", () => this.debugWiki.load(), void 0);
12973
14682
  if (!this.pluginExtensionsApplied) {
12974
14683
  this.plugins.applyExtensions({ tools: this.registry, skills: this.skills });
12975
14684
  this.pluginExtensionsApplied = true;
@@ -13177,7 +14886,23 @@ var PhaseEngine = class {
13177
14886
  const now = (/* @__PURE__ */ new Date()).toISOString();
13178
14887
  this.issueSeq += 1;
13179
14888
  const id = `ISSUE-${now.replace(/[-:.TZ]/g, "").slice(0, 14)}-${String(this.issueSeq).padStart(3, "0")}`;
13180
- const failureLog = cleanFailureLogForDebugContext(input.failureLog);
14889
+ const rawFailureLog = input.failureLog ?? "";
14890
+ const cleanedFailureLog = cleanFailureLogForDebugContext(rawFailureLog);
14891
+ const debugBrief = buildDebugBrief({
14892
+ reason: input.reason,
14893
+ failureLog: cleanedFailureLog,
14894
+ phase: step?.phase
14895
+ });
14896
+ const failureLog = compactFailureEvidence({
14897
+ reason: input.reason,
14898
+ failureLog: cleanedFailureLog,
14899
+ phase: step?.phase,
14900
+ maxChars: 6e3,
14901
+ maxLines: 90
14902
+ });
14903
+ const rawFailureLogPath = `.xcompiler/issues/${id}/failure.raw.log`;
14904
+ await this.opts.ws.writeFile(rawFailureLogPath, rawFailureLog.endsWith("\n") ? rawFailureLog : `${rawFailureLog}
14905
+ `);
13181
14906
  const issue = {
13182
14907
  id,
13183
14908
  createdAt: now,
@@ -13195,6 +14920,9 @@ var PhaseEngine = class {
13195
14920
  title: step?.title,
13196
14921
  reason: input.reason,
13197
14922
  failureLog,
14923
+ failureLogBytes: Buffer.byteLength(rawFailureLog, "utf8"),
14924
+ rawFailureLogPath,
14925
+ debugBrief,
13198
14926
  metrics: input.metrics,
13199
14927
  evidence: input.evidence
13200
14928
  };
@@ -13212,6 +14940,12 @@ var PhaseEngine = class {
13212
14940
  issue.status = "routed";
13213
14941
  issue.targetStepId = target.id;
13214
14942
  issue.targetPhase = target.phase;
14943
+ issue.debugBrief = buildDebugBrief({
14944
+ reason: issue.reason,
14945
+ failureLog: issue.failureLog,
14946
+ phase: issue.phase,
14947
+ targetPhase: target.phase
14948
+ });
13215
14949
  issue.routedAt = (/* @__PURE__ */ new Date()).toISOString();
13216
14950
  issue.updatedAt = issue.routedAt;
13217
14951
  await this.persistIssue(issue, "routed", { routingReason: reason });
@@ -13230,13 +14964,27 @@ var PhaseEngine = class {
13230
14964
  issue.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
13231
14965
  await this.persistIssue(issue, "unresolved", { reason });
13232
14966
  }
13233
- async markIssueResolved(issueId, step, repair) {
14967
+ async markIssueResolved(issueId, step, repair, issueResolutionPlan) {
13234
14968
  const issue = issueId ? this.findIssue(issueId) : void 0;
13235
14969
  if (!issue) return;
13236
14970
  issue.status = "resolved";
13237
14971
  issue.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
13238
14972
  issue.updatedAt = issue.resolvedAt;
13239
14973
  if (repair) issue.repair = repair;
14974
+ if (issueResolutionPlan?.trim()) {
14975
+ issue.issueResolutionPlan = issueResolutionPlan.trim();
14976
+ issue.resolutionPlanHistory = [
14977
+ ...issue.resolutionPlanHistory ?? [],
14978
+ {
14979
+ at: issue.resolvedAt,
14980
+ stepId: step.id,
14981
+ phase: step.phase,
14982
+ plan: issue.issueResolutionPlan,
14983
+ outcome: "accepted"
14984
+ }
14985
+ ].slice(-8);
14986
+ }
14987
+ await this.recordDebugWikiResolution(issue, step, repair);
13240
14988
  await this.persistIssue(issue, "resolved");
13241
14989
  await this.opts.audit.event("issue.resolve", `${issue.id} resolved by ${step.id} ${step.phase}`, {
13242
14990
  messageId: "engine.issue_resolved",
@@ -13246,6 +14994,82 @@ var PhaseEngine = class {
13246
14994
  repair
13247
14995
  });
13248
14996
  }
14997
+ async recordDebugWikiFailure(step, debug, outcome) {
14998
+ const entryIds = dedup6(debug.debugWikiEntryIds ?? []);
14999
+ if (entryIds.length === 0) return;
15000
+ const issue = debug.issueId ? this.findIssue(debug.issueId) : void 0;
15001
+ const brief = buildDebugBrief({
15002
+ reason: outcome.reason ?? debug.reason,
15003
+ failureLog: cleanFailureLogForDebugContext(outcome.failureLog),
15004
+ phase: issue?.phase ?? step.phase,
15005
+ targetPhase: issue?.targetPhase ?? step.phase
15006
+ });
15007
+ await this.withDebugWiki(
15008
+ "record-failure",
15009
+ () => this.debugWiki.recordFailure(entryIds, {
15010
+ brief,
15011
+ issueId: issue?.id,
15012
+ stepId: step.id,
15013
+ phase: step.phase,
15014
+ targetPhase: issue?.targetPhase,
15015
+ language: this.profile.id,
15016
+ solution: "retrieved wiki solution did not resolve this attempt",
15017
+ reason: outcome.reason ?? debug.reason
15018
+ }),
15019
+ void 0
15020
+ );
15021
+ await this.opts.audit.event("note", `debug wiki marked ${entryIds.join(", ")} for review`, {
15022
+ messageId: "engine.debug_wiki_feedback",
15023
+ kind: "failure",
15024
+ entryIds,
15025
+ issueId: issue?.id,
15026
+ stepId: step.id,
15027
+ reason: outcome.reason ?? debug.reason
15028
+ });
15029
+ }
15030
+ async recordDebugWikiResolution(issue, step, repair) {
15031
+ const brief = issue.debugBrief ?? buildDebugBrief({
15032
+ reason: issue.reason,
15033
+ failureLog: issue.failureLog,
15034
+ phase: issue.phase,
15035
+ targetPhase: issue.targetPhase
15036
+ });
15037
+ const repairFiles = repair?.changedFiles?.length ? repair.changedFiles : step.outputs;
15038
+ const evidenceSummary = [
15039
+ `Resolved ${issue.kind} by Debugger in ${step.id}/${step.phase}.`,
15040
+ `Mode: ${repair?.mode ?? "verification"}.`,
15041
+ repairFiles.length > 0 ? `Changed/verified files: ${repairFiles.join(", ")}.` : "",
15042
+ repair?.patchPath ? `Patch: ${repair.patchPath}.` : "",
15043
+ `Demand: ${brief.debugDemand}`
15044
+ ].filter(Boolean).join(" ");
15045
+ const solution = issue.issueResolutionPlan ? `${issue.issueResolutionPlan}
15046
+ Resolution evidence: ${evidenceSummary}` : evidenceSummary;
15047
+ const result = await this.withDebugWiki(
15048
+ "record-resolution",
15049
+ () => this.debugWiki.recordResolution({
15050
+ brief,
15051
+ issueId: issue.id,
15052
+ stepId: step.id,
15053
+ phase: step.phase,
15054
+ targetPhase: issue.targetPhase,
15055
+ language: this.profile.id,
15056
+ resolutionPlan: issue.issueResolutionPlan,
15057
+ solution,
15058
+ evidence: brief.evidence,
15059
+ repairFiles,
15060
+ usedEntryIds: issue.debugWikiEntryIds
15061
+ }),
15062
+ { updated: [] }
15063
+ );
15064
+ if (result.created || result.updated.length > 0) {
15065
+ await this.opts.audit.event("note", `debug wiki updated after ${issue.id}`, {
15066
+ messageId: "engine.debug_wiki_updated",
15067
+ issueId: issue.id,
15068
+ created: result.created,
15069
+ updated: result.updated
15070
+ });
15071
+ }
15072
+ }
13249
15073
  findIssue(issueId) {
13250
15074
  return this.issues.find((issue) => issue.id === issueId);
13251
15075
  }
@@ -13265,6 +15089,10 @@ var PhaseEngine = class {
13265
15089
  targetStepId: issue.targetStepId,
13266
15090
  targetPhase: issue.targetPhase,
13267
15091
  reason: issue.reason,
15092
+ debugSummary: issue.debugBrief?.summary,
15093
+ debugDemand: issue.debugBrief?.debugDemand,
15094
+ debugWikiEntryIds: issue.debugWikiEntryIds,
15095
+ issueResolutionPlan: issue.issueResolutionPlan,
13268
15096
  ...extra
13269
15097
  });
13270
15098
  await this.opts.ws.writeFile(eventPath, `${existing}${line}
@@ -13284,6 +15112,7 @@ var PhaseEngine = class {
13284
15112
  const diff = await this.opts.git.raw().diff([beforeRef, "--"]).catch((err) => `# git diff failed: ${err.message}
13285
15113
  `);
13286
15114
  const mode = inferRepairMode(toolCalls);
15115
+ const changedFiles = parsePatchChangedFiles(diff);
13287
15116
  await this.opts.ws.writeFile(patchPath, diff || "# No textual diff captured.\n");
13288
15117
  await this.opts.ws.writeFile(
13289
15118
  summaryPath,
@@ -13307,7 +15136,8 @@ var PhaseEngine = class {
13307
15136
  completedBeforeDebug,
13308
15137
  mode,
13309
15138
  patchPath,
13310
- summaryPath
15139
+ summaryPath,
15140
+ changedFiles
13311
15141
  };
13312
15142
  }
13313
15143
  async completedPhaseRepairViolation(toolCalls) {
@@ -13324,13 +15154,13 @@ var PhaseEngine = class {
13324
15154
  return void 0;
13325
15155
  }
13326
15156
  async repairChangedFiles() {
13327
- const planPath = normalizeGitPath(path26.relative(this.opts.ws.root, path26.resolve(this.opts.planPath)));
15157
+ const planPath = normalizeGitPath(path27.relative(this.opts.ws.root, path27.resolve(this.opts.planPath)));
13328
15158
  const status = await this.opts.git.raw().status();
13329
15159
  return status.files.map((file) => normalizeGitPath(file.path)).filter((file) => file.length > 0).filter((file) => !isRuntimeOnlyChange(file, planPath));
13330
15160
  }
13331
15161
  testGateArgsForStep(step) {
13332
15162
  if (step.phase === "FUNCTIONAL_TEST") return [];
13333
- return dedup4(
15163
+ return dedup6(
13334
15164
  step.outputs.filter((out) => typeof out === "string" && !out.endsWith("/")).map((out) => normalizeGitPath(out)).filter(isTestFilePath)
13335
15165
  );
13336
15166
  }
@@ -13498,7 +15328,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13498
15328
  initialDebug: {
13499
15329
  reason,
13500
15330
  failureLog: debugFailureLog,
13501
- contextPaths: dedup4([
15331
+ contextPaths: dedup6([
13502
15332
  ...sourceStep.inputs,
13503
15333
  ...sourceStep.outputs,
13504
15334
  ...routedTest.inputs,
@@ -13506,7 +15336,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13506
15336
  ...failedTest.inputs,
13507
15337
  ...failedTest.outputs
13508
15338
  ]),
13509
- extraAllowedWrites: dedup4([...routedTest.outputs]),
15339
+ extraAllowedWrites: dedup6([...routedTest.outputs]),
13510
15340
  contextMode: "test-rollback",
13511
15341
  testScopeArgs: this.testGateArgsForStep(routedTest),
13512
15342
  issueId: issue?.id,
@@ -13647,6 +15477,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13647
15477
  await this.debugCache.load();
13648
15478
  const initialDebug = opts.initialDebug;
13649
15479
  const inheritedExtraAllowedWrites = initialDebug?.extraAllowedWrites;
15480
+ const inheritedContextMode = initialDebug?.contextMode;
15481
+ const inheritedTestScopeArgs = initialDebug?.testScopeArgs;
13650
15482
  const completedBeforeDebug = initialDebug?.completedBeforeDebug ?? step.status === "DONE";
13651
15483
  const rootDebugFailureLog = initialDebug ? cleanFailureLogForDebugContext(initialDebug.failureLog) : void 0;
13652
15484
  const includeRootDebugFailureLog = (failureLog, reason) => rootDebugFailureLog ? composeDebugRetryFailureLog(rootDebugFailureLog, failureLog, reason) : failureLog;
@@ -13680,7 +15512,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13680
15512
  priorAttemptsPrompt: priorPrompt,
13681
15513
  contextPaths: initialDebug.contextPaths,
13682
15514
  extraAllowedWrites: inheritedExtraAllowedWrites,
13683
- contextMode: initialDebug.contextMode,
15515
+ contextMode: inheritedContextMode,
15516
+ testScopeArgs: inheritedTestScopeArgs,
13684
15517
  issueId: activeIssueId,
13685
15518
  completedBeforeDebug
13686
15519
  });
@@ -13688,6 +15521,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13688
15521
  const attempts = this.debugCache.attempts(step.id);
13689
15522
  const last = attempts.slice(-1)[0];
13690
15523
  const resume = latestActionableDebugAttempt(attempts) ?? last;
15524
+ const cachedTestScopeArgs = inferCachedTestScopeArgs(resume);
15525
+ const cachedContextMode = resume.contextMode ?? (cachedTestScopeArgs.length > 0 ? "test-rollback" : void 0);
13691
15526
  this.log(
13692
15527
  chalk2.yellow(
13693
15528
  t().engine.debugResumeNotice(step.id, attempts.length)
@@ -13715,6 +15550,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13715
15550
  reason: resume.reason,
13716
15551
  priorAttemptsPrompt: priorPrompt,
13717
15552
  extraAllowedWrites: inheritedExtraAllowedWrites,
15553
+ contextMode: cachedContextMode,
15554
+ testScopeArgs: cachedTestScopeArgs,
13718
15555
  issueId: activeIssueId,
13719
15556
  completedBeforeDebug
13720
15557
  });
@@ -13743,6 +15580,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13743
15580
  reason: resume.reason,
13744
15581
  priorAttemptsPrompt: priorPrompt,
13745
15582
  extraAllowedWrites: inheritedExtraAllowedWrites,
15583
+ contextMode: cachedContextMode,
15584
+ testScopeArgs: cachedTestScopeArgs,
13746
15585
  issueId: activeIssueId,
13747
15586
  completedBeforeDebug
13748
15587
  });
@@ -13793,6 +15632,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13793
15632
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(initialFailureLogForRecord), initialReason).map(
13794
15633
  (s) => `[${s.code}] ${s.hint}`
13795
15634
  ),
15635
+ contextMode: inheritedContextMode,
15636
+ testScopeArgs: inheritedTestScopeArgs,
13796
15637
  metrics: initial.metrics ? {
13797
15638
  healthScore: initial.metrics.healthScore,
13798
15639
  parseFailures: initial.metrics.parseFailures,
@@ -13864,6 +15705,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13864
15705
  reason: lastReason,
13865
15706
  priorAttemptsPrompt: priorPrompt,
13866
15707
  extraAllowedWrites: inheritedExtraAllowedWrites,
15708
+ contextMode: inheritedContextMode,
15709
+ testScopeArgs: inheritedTestScopeArgs,
13867
15710
  issueId: activeIssueId,
13868
15711
  completedBeforeDebug
13869
15712
  });
@@ -13919,6 +15762,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13919
15762
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(recordedFailureLog2), rollbackReason).map(
13920
15763
  (s) => `[${s.code}] ${s.hint}`
13921
15764
  ),
15765
+ contextMode: inheritedContextMode,
15766
+ testScopeArgs: inheritedTestScopeArgs,
13922
15767
  metrics: r.metrics ? {
13923
15768
  healthScore: r.metrics.healthScore,
13924
15769
  parseFailures: r.metrics.parseFailures,
@@ -13938,9 +15783,11 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13938
15783
  }
13939
15784
  const m = r.metrics;
13940
15785
  const readOnlyProbeLoop = isReadOnlyProbeLoopFailure(r.reason);
15786
+ const missingOutputStall = isMissingOutputStallFailure(r.reason);
15787
+ const lowQualityDebuggerResponse = isLowQualityDebuggerResponseFailure(r.reason);
13941
15788
  const repairEvidenceMissing = isRepairEvidenceMissingFailure(r.reason);
13942
- const healthy = !readOnlyProbeLoop && !repairEvidenceMissing && !!m && m.parseFailures === 0 && m.repeatedTurns <= 1 && m.healthScore >= 0.6;
13943
- const bad = readOnlyProbeLoop || repairEvidenceMissing || !!m && (m.healthScore < 0.3 || m.parseFailures + m.repeatedTurns >= Math.max(2, Math.ceil(m.rounds / 2)));
15789
+ const healthy = !readOnlyProbeLoop && !missingOutputStall && !lowQualityDebuggerResponse && !repairEvidenceMissing && !!m && m.parseFailures === 0 && m.repeatedTurns <= 1 && m.healthScore >= 0.6;
15790
+ const bad = readOnlyProbeLoop || missingOutputStall || lowQualityDebuggerResponse || repairEvidenceMissing || !!m && (m.healthScore < 0.3 || m.parseFailures + m.repeatedTurns >= Math.max(2, Math.ceil(m.rounds / 2)));
13944
15791
  const tag = m ? `health=${m.healthScore.toFixed(2)} parseFail=${m.parseFailures} repeat=${m.repeatedTurns} progress=${m.progressRatio.toFixed(2)}` : "";
13945
15792
  if (healthy) {
13946
15793
  const before = budget;
@@ -13991,6 +15838,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
13991
15838
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(recordedFailureLog), r.reason ?? "").map(
13992
15839
  (s) => `[${s.code}] ${s.hint}`
13993
15840
  ),
15841
+ contextMode: inheritedContextMode,
15842
+ testScopeArgs: inheritedTestScopeArgs,
13994
15843
  metrics: m ? {
13995
15844
  healthScore: m.healthScore,
13996
15845
  parseFailures: m.parseFailures,
@@ -14110,9 +15959,75 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14110
15959
  `${iterationPrefix}/api-guide.md`
14111
15960
  ] : []
14112
15961
  ];
14113
- if (failedNames.has("entrypoint")) return dedup4([...codeAndTests, ...docs]);
14114
- if (failedNames.has("tests") || failedNames.has("test-files")) return dedup4([...codeAndTests, ...docs]);
14115
- return dedup4([...codeAndTests, ...step.inputs, ...docs]);
15962
+ if (failedNames.has("entrypoint")) return dedup6([...codeAndTests, ...docs]);
15963
+ if (failedNames.has("tests") || failedNames.has("test-files")) return dedup6([...codeAndTests, ...docs]);
15964
+ return dedup6([...codeAndTests, ...step.inputs, ...docs]);
15965
+ }
15966
+ async buildDebugPromptPayload(step, debug, failureLog) {
15967
+ const issue = debug.issueId ? this.findIssue(debug.issueId) : void 0;
15968
+ const currentBrief = buildDebugBrief({
15969
+ reason: debug.reason,
15970
+ failureLog,
15971
+ phase: issue?.phase ?? step.phase,
15972
+ targetPhase: issue?.targetPhase ?? step.phase
15973
+ });
15974
+ const rootBrief = issue?.debugBrief;
15975
+ const briefBlocks = rootBrief && rootBrief.summary !== currentBrief.summary ? [
15976
+ "## root issue brief",
15977
+ renderDebugBriefForPrompt(rootBrief),
15978
+ "",
15979
+ "## current retry brief",
15980
+ renderDebugBriefForPrompt(currentBrief)
15981
+ ] : [renderDebugBriefForPrompt(currentBrief)];
15982
+ const evidence = compactFailureEvidence({
15983
+ reason: debug.reason,
15984
+ failureLog,
15985
+ phase: issue?.phase ?? step.phase,
15986
+ targetPhase: issue?.targetPhase ?? step.phase,
15987
+ maxChars: 2600,
15988
+ maxLines: 50
15989
+ });
15990
+ const suggestions = renderDebugSuggestions(
15991
+ calibrateDebugSuggestions(failureLog, debug.reason)
15992
+ );
15993
+ const wikiMatches = await this.withDebugWiki(
15994
+ "search",
15995
+ () => this.debugWiki.search(currentBrief, { language: this.profile.id, limit: 3 }),
15996
+ []
15997
+ );
15998
+ const debugWikiEntryIds = wikiMatches.map((match) => match.entry.id);
15999
+ if (debugWikiEntryIds.length > 0) {
16000
+ debug.debugWikiEntryIds = dedup6([...debug.debugWikiEntryIds ?? [], ...debugWikiEntryIds]);
16001
+ if (issue) {
16002
+ issue.debugWikiEntryIds = dedup6([...issue.debugWikiEntryIds ?? [], ...debugWikiEntryIds]);
16003
+ }
16004
+ await this.withDebugWiki(
16005
+ "record-use",
16006
+ () => this.debugWiki.recordUse(debugWikiEntryIds, {
16007
+ brief: currentBrief,
16008
+ issueId: issue?.id,
16009
+ stepId: step.id,
16010
+ phase: step.phase,
16011
+ targetPhase: issue?.targetPhase,
16012
+ language: this.profile.id,
16013
+ solution: "retrieved for Debugger prompt"
16014
+ }),
16015
+ void 0
16016
+ );
16017
+ await this.opts.audit.event("note", `debug wiki matched ${debugWikiEntryIds.join(", ")}`, {
16018
+ messageId: "engine.debug_wiki_matched",
16019
+ entryIds: debugWikiEntryIds,
16020
+ stepId: step.id,
16021
+ phase: step.phase
16022
+ });
16023
+ }
16024
+ const wikiPrompt = renderDebugWikiMatchesForPrompt(wikiMatches);
16025
+ return {
16026
+ debugBrief: [briefBlocks.join("\n"), wikiPrompt].filter(Boolean).join("\n\n"),
16027
+ failureLog: evidence,
16028
+ suggestions,
16029
+ debugWikiEntryIds
16030
+ };
14116
16031
  }
14117
16032
  /** 一次执行尝试:可选 debug 模式(使用 Debugger 角色 + 注入失败日志)。 */
14118
16033
  async runOneAttempt(plan, step, debug) {
@@ -14126,6 +16041,9 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14126
16041
  });
14127
16042
  try {
14128
16043
  const outcome = await this.runOneAttemptCore(plan, step, debug);
16044
+ if (debug && !outcome.ok) {
16045
+ await this.recordDebugWikiFailure(step, debug, outcome);
16046
+ }
14129
16047
  await this.plugins.emit("step.attempt.after", {
14130
16048
  plan,
14131
16049
  step,
@@ -14141,6 +16059,9 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14141
16059
  failureLog: error instanceof Error ? error.stack ?? error.message : String(error),
14142
16060
  reason: error instanceof Error ? error.message : String(error)
14143
16061
  };
16062
+ if (debug) {
16063
+ await this.recordDebugWikiFailure(step, debug, outcome);
16064
+ }
14144
16065
  await this.plugins.emit("step.attempt.after", {
14145
16066
  plan,
14146
16067
  step,
@@ -14164,10 +16085,10 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14164
16085
  hints.push(`[debugger] ${dbg.prompt}`);
14165
16086
  }
14166
16087
  }
14167
- const allNames = dedup4([...resolvedToolNames, ...extraNames]);
16088
+ const allNames = dedup6([...resolvedToolNames, ...extraNames]);
14168
16089
  const baseTools = this.registry.pick(allNames);
14169
- const allowedWrites = debug ? dedup4([...this.computeDebugAllowedWrites(plan, step), ...debug.extraAllowedWrites ?? []]) : this.computeStepAllowedWrites(plan, step);
14170
- const augmentedWrites = this.isVModelTestPhase(step.phase) || step.phase === "DEBUG" || debug ? dedup4([...allowedWrites, "tests/fixtures"]) : allowedWrites;
16090
+ const allowedWrites = debug ? dedup6([...this.computeDebugAllowedWrites(plan, step), ...debug.extraAllowedWrites ?? []]) : this.computeStepAllowedWrites(plan, step);
16091
+ const augmentedWrites = this.isVModelTestPhase(step.phase) || step.phase === "DEBUG" || debug ? dedup6([...allowedWrites, "tests/fixtures"]) : allowedWrites;
14171
16092
  const budgetContext = {
14172
16093
  phase: step.phase,
14173
16094
  role,
@@ -14254,6 +16175,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14254
16175
  { animate: false }
14255
16176
  );
14256
16177
  const debugFailureLog = debug ? cleanFailureLogForDebugContext(debug.failureLog) : void 0;
16178
+ const debugPayload = debug ? await this.buildDebugPromptPayload(step, debug, debugFailureLog ?? debug.failureLog) : void 0;
14257
16179
  try {
14258
16180
  const r = await executor.run({
14259
16181
  step,
@@ -14263,14 +16185,15 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
14263
16185
  contextSnippets: ctxSnippets,
14264
16186
  skillHints: hints,
14265
16187
  debugContext: debug ? {
16188
+ issueId: debug.issueId,
14266
16189
  reason: debug.reason,
14267
- failureLog: debugFailureLog ?? debug.failureLog,
16190
+ failureLog: debugPayload?.failureLog ?? debugFailureLog ?? debug.failureLog,
16191
+ debugBrief: debugPayload?.debugBrief,
14268
16192
  repairRequired: !debug.completedBeforeDebug,
14269
- suggestions: renderDebugSuggestions(
14270
- calibrateDebugSuggestions(debugFailureLog ?? debug.failureLog, debug.reason)
14271
- ) + (debug.priorAttemptsPrompt ? `
14272
-
14273
- ${debug.priorAttemptsPrompt}` : "")
16193
+ suggestions: [
16194
+ debugPayload?.suggestions,
16195
+ debug.priorAttemptsPrompt
16196
+ ].filter(Boolean).join("\n\n")
14274
16197
  } : void 0,
14275
16198
  globalPrompt: plan.globalPrompt,
14276
16199
  languageProfile: this.profile
@@ -14523,7 +16446,7 @@ ${pt.stderr}`);
14523
16446
  r.toolCalls
14524
16447
  ) : void 0;
14525
16448
  if (debug?.issueId) {
14526
- await this.markIssueResolved(debug.issueId, step, repair);
16449
+ await this.markIssueResolved(debug.issueId, step, repair, r.issueResolutionPlan);
14527
16450
  }
14528
16451
  await this.opts.git.snapshot(step.id, step.retries, debug ? "debug done" : "done");
14529
16452
  spin?.succeed(t().engine.phaseDone(step.id, r.rounds));
@@ -14532,9 +16455,12 @@ ${pt.stderr}`);
14532
16455
  rounds: r.rounds,
14533
16456
  retry: step.retries
14534
16457
  });
14535
- return { ok: true, failureLog: "" };
16458
+ return { ok: true, failureLog: "", issueResolutionPlan: r.issueResolutionPlan };
16459
+ }
16460
+ let reason = r.error ?? t().engine.outputsMissing(verify.missing.join(", "));
16461
+ if (debug?.completedBeforeDebug && !hasSuccessfulRepairMutation(r.toolCalls) && hasFailedVerificationEvidence(r.toolCalls)) {
16462
+ reason = "completed phase debug finished with failed verification but without a successful repair mutation";
14536
16463
  }
14537
- const reason = r.error ?? t().engine.outputsMissing(verify.missing.join(", "));
14538
16464
  const m = r.metrics;
14539
16465
  const metricsLine = m ? t().engine.metricsLine(m.healthScore.toFixed(2), m.parseFailures, m.repeatedTurns, m.toolFailRatio.toFixed(2), m.progressRatio.toFixed(2)) : t().engine.metricsUnavailable;
14540
16466
  const failureLog = [
@@ -14568,7 +16494,7 @@ ${pt.stderr}`);
14568
16494
  } else {
14569
16495
  await this.opts.git.revertTo(sha);
14570
16496
  }
14571
- return { ok: false, failureLog, reason, metrics: m, issueKind: "phase" };
16497
+ return { ok: false, failureLog, reason, metrics: m, issueKind: "phase", issueResolutionPlan: r.issueResolutionPlan };
14572
16498
  } catch (err) {
14573
16499
  const msg = err.message;
14574
16500
  const stack = err.stack ?? msg;
@@ -14582,7 +16508,7 @@ ${pt.stderr}`);
14582
16508
  });
14583
16509
  return { ok: false, failureLog: stack, reason: msg, issueKind: "exception" };
14584
16510
  } finally {
14585
- void path26;
16511
+ void path27;
14586
16512
  }
14587
16513
  }
14588
16514
  async buildContextSnippets(plan, step, debug) {
@@ -14639,7 +16565,7 @@ ${pt.stderr}`);
14639
16565
  if (downstream) {
14640
16566
  out.set(`.xcompiler/downstream/${step.id}.md`, downstream);
14641
16567
  }
14642
- return [...out.entries()].map(([path31, content]) => ({ path: path31, content }));
16568
+ return [...out.entries()].map(([path32, content]) => ({ path: path32, content }));
14643
16569
  }
14644
16570
  findOwningTestStepForFailure(plan, currentStep, failureText) {
14645
16571
  const failedPaths = extractFailedTestPaths(failureText);
@@ -14762,7 +16688,7 @@ ${pt.stderr}`);
14762
16688
  ].join("\n").length;
14763
16689
  }
14764
16690
  };
14765
- function dedup4(arr) {
16691
+ function dedup6(arr) {
14766
16692
  return [...new Set(arr)];
14767
16693
  }
14768
16694
  function isDesignSourcePhase(phase) {
@@ -14794,6 +16720,9 @@ var REPAIR_VERIFICATION_TOOLS = /* @__PURE__ */ new Set([
14794
16720
  function hasSuccessfulVerificationEvidence(toolCalls) {
14795
16721
  return toolCalls.some((call) => call.ok && REPAIR_VERIFICATION_TOOLS.has(call.tool));
14796
16722
  }
16723
+ function hasFailedVerificationEvidence(toolCalls) {
16724
+ return toolCalls.some((call) => !call.ok && REPAIR_VERIFICATION_TOOLS.has(call.tool));
16725
+ }
14797
16726
  function shouldRollbackTestPhaseFromToolFailures(step, toolCalls) {
14798
16727
  let unresolvedTestFailure = false;
14799
16728
  for (const call of toolCalls) {
@@ -14913,32 +16842,65 @@ function extractFailedTestPaths(text) {
14913
16842
  if (file && isTestFilePath(file)) found.push(file);
14914
16843
  }
14915
16844
  }
14916
- return dedup4(found);
16845
+ return dedup6(found);
14917
16846
  }
14918
16847
  function isNonDebuggableInfrastructureFailure(reason, failureLog) {
14919
16848
  const text = `${reason ?? ""}
14920
16849
  ${failureLog ?? ""}`.toLowerCase();
14921
- return /typeerror:\s*fetch failed/u.test(text) || /(?:openai|ollama) http (?:401|403|408|409|429|5\d\d)\b/u.test(text) || /\bhttp (?:401|403|408|409|429|5\d\d)\b/u.test(text) || /rate limit exceeded|free-models-per-day|retry_after_seconds|retry-after/u.test(text) || /(?:openai|ollama|llm|provider)[^\n]{0,180}(?:rate[- ]?limit|rate limited|rate-limited|retry-after|retry_after_seconds)/u.test(text) || /(?:response_format|json_object|json_schema)[^\n]{0,220}(?:not support|unsupported|invalid_request_body|supported formats)/u.test(text) || /openai stream (?:wall-clock|idle)/u.test(text) || /ollama stream (?:wall-clock|idle)/u.test(text) || /request timed out after \d+ms/u.test(text) || /prefill_memory_exceeded|prefill memory guard|dynamic ceiling/u.test(text) || /context (?:length|window)|token limit|too many tokens|prompt too long|max(?:imum)? context/u.test(text) || /input[^\n]{0,80}tokens[^\n]{0,120}(?:exceed|limit|ceiling)/u.test(text) || /llm provider|provider_call_failed|all llm providers failed/u.test(text);
16850
+ if (/low-quality debugger response/u.test(text)) return false;
16851
+ return /typeerror:\s*fetch failed/u.test(text) || /(?:openai|ollama) http (?:401|403|408|409|429|5\d\d)\b/u.test(text) || /rate limit exceeded|free-models-per-day|retry_after_seconds|retry-after/u.test(text) || /(?:openai|ollama|llm|provider)[^\n]{0,180}(?:rate[- ]?limit|rate limited|rate-limited|retry-after|retry_after_seconds)/u.test(text) || /(?:response_format|json_object|json_schema)[^\n]{0,220}(?:not support|unsupported|invalid_request_body|supported formats)/u.test(text) || /openai stream (?:wall-clock|idle)/u.test(text) || /ollama stream (?:wall-clock|idle)/u.test(text) || /request timed out after \d+ms/u.test(text) || /prefill_memory_exceeded|prefill memory guard|dynamic ceiling/u.test(text) || /context (?:length|window)|token limit|too many tokens|prompt too long|max(?:imum)? context/u.test(text) || /input[^\n]{0,80}tokens[^\n]{0,120}(?:exceed|limit|ceiling)/u.test(text) || /llm provider|provider_call_failed|all llm providers failed/u.test(text);
14922
16852
  }
14923
16853
  function isReadOnlyProbeLoopFailure(reason) {
14924
16854
  return /repeated read-only\/probe actions without progress/u.test(reason ?? "") || /read-only recovery mode repeated probe actions/u.test(reason ?? "");
14925
16855
  }
16856
+ function isMissingOutputStallFailure(reason) {
16857
+ return /write\/progress actions did not reduce missing outputs/u.test(reason ?? "");
16858
+ }
16859
+ function isLowQualityDebuggerResponseFailure(reason) {
16860
+ return /low-quality debugger response/u.test(reason ?? "");
16861
+ }
14926
16862
  function isRepairEvidenceMissingFailure(reason) {
14927
- return /without repair evidence/u.test(reason ?? "") || /without a successful repair mutation or verification tool call/u.test(reason ?? "");
16863
+ return /without repair evidence/u.test(reason ?? "") || /without a successful repair mutation/u.test(reason ?? "") || /without a successful repair mutation or verification tool call/u.test(reason ?? "");
14928
16864
  }
14929
16865
  function shouldRollbackTestPhaseFailure(reason, failureLog) {
14930
16866
  const text = `${reason ?? ""}
14931
16867
  ${failureLog ?? ""}`.toLowerCase();
14932
16868
  if (isReadOnlyProbeLoopFailure(reason) || isRepairEvidenceMissingFailure(reason)) return false;
16869
+ if (/tool verification failed.*rolling back to paired/u.test(text)) return true;
14933
16870
  if (isCachedTestArtifactDiscoveryFailure(text)) return false;
14934
16871
  if (/missing outputs|outputs? 校验|verify outputs|declared outputs|产物/u.test(text)) return false;
14935
16872
  if (/invalid action|args must be an object|invalid json|parse failure/u.test(text)) return false;
14936
- if (/tool verification failed.*rolling back to paired/u.test(text)) return true;
14937
- if (/test gate|functional gate|functional entry probe|entry probe|delivery gate/u.test(text)) return true;
16873
+ if (/test gate|functional gate|functional entry probe|entry probe|delivery gate|测试门禁|功能门禁|功能入口探测|入口探测|交付门禁/u.test(text)) return true;
14938
16874
  if (/pytest exit=\s*[1-9]\d*|exit code\s*[1-9]\d*|failed tests?|test failures?/u.test(text)) return true;
14939
16875
  if (/assertionerror|failed:\s+did not|traceback \(most recent call last\)/u.test(text)) return true;
14940
16876
  return false;
14941
16877
  }
16878
+ function inferCachedTestScopeArgs(entry) {
16879
+ const explicit = (entry.testScopeArgs ?? []).map(normalizeGitPath).filter(isTestFilePath);
16880
+ if (explicit.length > 0) return dedup6(explicit);
16881
+ const text = [
16882
+ entry.failureLogTail,
16883
+ entry.debugBrief?.toolFailures?.join("\n") ?? "",
16884
+ entry.debugBrief?.evidence?.join("\n") ?? ""
16885
+ ].filter(Boolean).join("\n");
16886
+ const fromRunTestsArgs = extractRunTestsArgs(text);
16887
+ if (fromRunTestsArgs.length > 0) return fromRunTestsArgs;
16888
+ const failedPaths = extractFailedTestPaths(text);
16889
+ if (failedPaths.length > 0) return failedPaths;
16890
+ return dedup6((entry.debugBrief?.files ?? []).map(normalizeGitPath).filter(isTestFilePath));
16891
+ }
16892
+ function extractRunTestsArgs(text) {
16893
+ const out = [];
16894
+ for (const match of text.matchAll(/\brun_tests[^\n]*\bargs=([^\n]+)/giu)) {
16895
+ const raw = match[1] ?? "";
16896
+ for (const token of raw.split(/\s+/u)) {
16897
+ const cleaned = token.replace(/^["'`]+|["'`,;]+$/gu, "");
16898
+ const normalized = normalizeGitPath(cleaned);
16899
+ if (isTestFilePath(normalized)) out.push(normalized);
16900
+ }
16901
+ }
16902
+ return dedup6(out);
16903
+ }
14942
16904
  function isCachedTestArtifactDiscoveryFailure(text) {
14943
16905
  return /no test files? found|no tests? found/u.test(text) || /filter:\s+tests?\//u.test(text) || /(?:enoent|no such file or directory|not a file)[^\n]{0,240}tests?\//u.test(text);
14944
16906
  }
@@ -14994,6 +16956,13 @@ function inferRepairMode(toolCalls) {
14994
16956
  if (usedRewrite) return "rewrite";
14995
16957
  return "verification";
14996
16958
  }
16959
+ function parsePatchChangedFiles(diff) {
16960
+ const files = [];
16961
+ for (const match of diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gmu)) {
16962
+ files.push(normalizeGitPath(match[2] ?? match[1] ?? ""));
16963
+ }
16964
+ return dedup6(files.filter(Boolean));
16965
+ }
14997
16966
 
14998
16967
  // src/runtime/run.ts
14999
16968
  async function runExecute(opts) {
@@ -15002,7 +16971,7 @@ async function runExecute(opts) {
15002
16971
  io.interaction?.pauseStdin?.();
15003
16972
  } catch {
15004
16973
  }
15005
- const ws = new Workspace(path27.resolve(opts.workspace));
16974
+ const ws = new Workspace(path28.resolve(opts.workspace));
15006
16975
  const { config: cfg, path: cfgPath } = await loadConfigWithPath(opts.configPath);
15007
16976
  if (!hasXcEnv("LANG")) setLocale(cfg.locale);
15008
16977
  let lock;
@@ -15172,6 +17141,8 @@ async function runExecute(opts) {
15172
17141
  maxEditLinesPerStep: cfg.agent.max_edit_lines_per_step,
15173
17142
  maxWriteChunkBytes: cfg.agent.max_write_chunk_bytes,
15174
17143
  terminalOutput: opts.terminalOutput ?? true,
17144
+ debugWikiPath: opts.debugWikiPath ? path28.resolve(opts.debugWikiPath) : void 0,
17145
+ debugWikiStrict: !!opts.debugWikiPath,
15175
17146
  requestPermission: io.requestPermission ? async (request2) => {
15176
17147
  await io.emit({ type: "permission", status: "requested", request: request2 });
15177
17148
  const decision = await io.requestPermission(request2);
@@ -15416,8 +17387,8 @@ async function emitProjectAudit(io, result) {
15416
17387
  }
15417
17388
 
15418
17389
  // src/runtime/workspace.ts
15419
- import path28 from "path";
15420
- import { promises as fs25 } from "fs";
17390
+ import path29 from "path";
17391
+ import { promises as fs27 } from "fs";
15421
17392
  function defaultProjectName(now = /* @__PURE__ */ new Date()) {
15422
17393
  const pad = (n) => n.toString().padStart(2, "0");
15423
17394
  return `xcompiler-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
@@ -15425,14 +17396,14 @@ function defaultProjectName(now = /* @__PURE__ */ new Date()) {
15425
17396
  async function resolveCompileWorkspace(opts) {
15426
17397
  const explicit = opts.output ?? opts.workspace;
15427
17398
  if (explicit) {
15428
- const ws2 = path28.resolve(explicit);
15429
- await fs25.mkdir(ws2, { recursive: true });
17399
+ const ws2 = path29.resolve(explicit);
17400
+ await fs27.mkdir(ws2, { recursive: true });
15430
17401
  return ws2;
15431
17402
  }
15432
- const base = opts.baseDir ? path28.resolve(opts.baseDir) : "/tmp";
17403
+ const base = opts.baseDir ? path29.resolve(opts.baseDir) : "/tmp";
15433
17404
  const name = opts.name ?? defaultProjectName();
15434
- const ws = path28.join(base, name);
15435
- await fs25.mkdir(ws, { recursive: true });
17405
+ const ws = path29.join(base, name);
17406
+ await fs27.mkdir(ws, { recursive: true });
15436
17407
  return ws;
15437
17408
  }
15438
17409
 
@@ -15449,8 +17420,8 @@ async function runBuildCommand(opts) {
15449
17420
  async function runRunCommand(opts) {
15450
17421
  const cwd = opts.cwd ?? process.cwd();
15451
17422
  const explicit = opts.output ?? opts.workspace;
15452
- const workspace = explicit ? path29.resolve(explicit) : opts.planArg ? path29.dirname(path29.resolve(opts.planArg)) : cwd;
15453
- const planPath = opts.planArg ? path29.resolve(opts.planArg) : await defaultRunnablePlanPath(workspace);
17423
+ const workspace = explicit ? path30.resolve(explicit) : opts.planArg ? path30.dirname(path30.resolve(opts.planArg)) : cwd;
17424
+ const planPath = opts.planArg ? path30.resolve(opts.planArg) : await defaultRunnablePlanPath(workspace);
15454
17425
  return runExecute({
15455
17426
  ...opts,
15456
17427
  workspace,
@@ -15459,15 +17430,15 @@ async function runRunCommand(opts) {
15459
17430
  });
15460
17431
  }
15461
17432
  async function defaultRunnablePlanPath(workspace) {
15462
- const phasePlanPath = path29.join(workspace, DEFAULT_PHASE_PLAN_FILE);
17433
+ const phasePlanPath = path30.join(workspace, DEFAULT_PHASE_PLAN_FILE);
15463
17434
  if (await fileExists3(phasePlanPath)) return phasePlanPath;
15464
- const legacyPlanPath = path29.join(workspace, DEFAULT_PLAN_FILE);
17435
+ const legacyPlanPath = path30.join(workspace, DEFAULT_PLAN_FILE);
15465
17436
  if (await fileExists3(legacyPlanPath)) return legacyPlanPath;
15466
17437
  return phasePlanPath;
15467
17438
  }
15468
17439
  async function fileExists3(filePath) {
15469
17440
  try {
15470
- await fs26.stat(filePath);
17441
+ await fs28.stat(filePath);
15471
17442
  return true;
15472
17443
  } catch {
15473
17444
  return false;
@@ -16039,7 +18010,7 @@ var AcpServer = class {
16039
18010
  }
16040
18011
  createSession(params) {
16041
18012
  const p = asOptionalRecord(params);
16042
- const workspace = typeof p.cwd === "string" ? path30.resolve(p.cwd) : typeof p.workspace === "string" ? path30.resolve(p.workspace) : process.cwd();
18013
+ const workspace = typeof p.cwd === "string" ? path31.resolve(p.cwd) : typeof p.workspace === "string" ? path31.resolve(p.workspace) : process.cwd();
16043
18014
  const session = this.sessions.create(workspace);
16044
18015
  return { sessionId: session.id };
16045
18016
  }
@@ -16051,7 +18022,7 @@ var AcpServer = class {
16051
18022
  async prompt(params) {
16052
18023
  const p = parsePromptParams(params);
16053
18024
  const session = this.sessions.get(p.sessionId);
16054
- const workspace = path30.resolve(session.workspace || process.cwd());
18025
+ const workspace = path31.resolve(session.workspace || process.cwd());
16055
18026
  const task = this.sessions.createTask(session, { workspace, userTask: p.task, protocol: "acp" });
16056
18027
  try {
16057
18028
  await this.runCodeTask(session, task, { ...p, workspace });
@@ -16071,7 +18042,7 @@ var AcpServer = class {
16071
18042
  startTask(params, protocol) {
16072
18043
  const p = parseTaskParams(params);
16073
18044
  const session = this.sessions.get(p.sessionId);
16074
- const workspace = path30.resolve(p.workspace || session.workspace || process.cwd());
18045
+ const workspace = path31.resolve(p.workspace || session.workspace || process.cwd());
16075
18046
  const task = this.sessions.createTask(session, { workspace, userTask: p.task, protocol });
16076
18047
  this.runCodeTask(session, task, { ...p, workspace }).catch((err) => {
16077
18048
  task.status = "failed";