@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.
@@ -59,11 +59,11 @@ var init_types = __esm({
59
59
  import { Command } from "commander";
60
60
 
61
61
  // src/runtime/commands.ts
62
- import path29 from "path";
63
- import { promises as fs26 } from "fs";
62
+ import path30 from "path";
63
+ import { promises as fs28 } from "fs";
64
64
 
65
65
  // src/core/project_file.ts
66
- import { promises as fs2 } from "fs";
66
+ import { promises as fs3 } from "fs";
67
67
  import path4 from "path";
68
68
  import { z as z3 } from "zod";
69
69
 
@@ -249,7 +249,7 @@ var PlanSchema = z.object({
249
249
  }));
250
250
 
251
251
  // src/core/storage.ts
252
- import { promises as fs } from "fs";
252
+ import { promises as fs2 } from "fs";
253
253
  import path3 from "path";
254
254
 
255
255
  // src/core/docs.ts
@@ -337,6 +337,9 @@ function testPlanDocForIteration(testPhase, iterationId = "P1") {
337
337
  return basename ? `docs/iterations/${iterationId}/tests/${basename}` : canonical;
338
338
  }
339
339
 
340
+ // src/core/language.ts
341
+ import { promises as fs } from "fs";
342
+
340
343
  // src/core/entry_gate.ts
341
344
  import path from "path";
342
345
 
@@ -438,6 +441,7 @@ var PYTHON_EXECUTOR_SYSTEM = `You are XCompiler's Step Executor. You may only in
438
441
  Every round you must return strict JSON:
439
442
  {
440
443
  "thoughts": "<one sentence describing this round's intent>",
444
+ "issueResolutionPlan": "<required only in DEBUG issue mode: concise root cause, repair target, and validation plan>",
441
445
  "actions": [ { "tool": "<tool name>", "args": { ... } }, ... ],
442
446
  "done": true | false
443
447
  }
@@ -522,6 +526,7 @@ var TYPESCRIPT_EXECUTOR_SYSTEM = `You are XCompiler's Step Executor. You may onl
522
526
  Every round you must return strict JSON:
523
527
  {
524
528
  "thoughts": "<one sentence describing this round's intent>",
529
+ "issueResolutionPlan": "<required only in DEBUG issue mode: concise root cause, repair target, and validation plan>",
525
530
  "actions": [ { "tool": "<tool name>", "args": { ... } }, ... ],
526
531
  "done": true | false
527
532
  }
@@ -545,7 +550,7 @@ Rules:
545
550
  7. package.json is the dependency manifest. Use add_dependency for npm packages; never write requirements.txt.
546
551
  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.`;
547
552
  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.
548
- 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.`;
553
+ 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.`;
549
554
  function buildPlannerSystem(profile) {
550
555
  return (profile.id === "typescript" ? TYPESCRIPT_PLANNER_SYSTEM : PYTHON_PLANNER_SYSTEM) + profile.plannerPromptOverride;
551
556
  }
@@ -637,13 +642,13 @@ var messages = {
637
642
  preflightOllamaUnreachable: (baseUrl, message) => `preflight: ollama ${baseUrl} unreachable: ${message}`,
638
643
  preflightAutoAdded: (providers, roles) => `preflight: auto-added ${providers} provider(s) for roles [${roles}]`,
639
644
  scoreFileHeader: "# XCompiler LLM provider score snapshot (maintained automatically by ScoreStore; do not edit)",
640
- 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."
645
+ 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."
641
646
  },
642
647
  system: {
643
648
  configEnvMissing: (names) => `[xcompiler] unset config environment variables were replaced with empty strings: ${names}`,
644
649
  unhandledError: (message) => `Unhandled error: ${message}`,
645
650
  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.",
646
- 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.",
651
+ 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.",
647
652
  firejailUnsupported: "sandbox=firejail is not implemented; use subprocess or docker.",
648
653
  smokeHeader: (baseUrl) => `Smoke test against ${baseUrl} (streaming)`,
649
654
  smokeOk: (model, totalMs, firstTokenMs, chunks, preview) => `[OK total=${totalMs}ms first-token=${firstTokenMs}ms chunks=${chunks}] ${model} -> ${preview}`,
@@ -660,8 +665,8 @@ var messages = {
660
665
  loaded: (plugin, version) => `Plugin ${plugin}@${version} loaded.`,
661
666
  extensionConflict: (plugin, kind, name) => `Plugin ${plugin} cannot replace existing ${kind} "${name}".`,
662
667
  hookFailed: (plugin, stage, message) => `Plugin ${plugin} failed during ${stage}: ${message}`,
663
- manifestReadFailed: (path30, message) => `Cannot read plugin manifest ${path30}: ${message}`,
664
- moduleLoadFailed: (plugin, path30, message) => `Cannot load plugin ${plugin} from ${path30}: ${message}`,
668
+ manifestReadFailed: (path31, message) => `Cannot read plugin manifest ${path31}: ${message}`,
669
+ moduleLoadFailed: (plugin, path31, message) => `Cannot load plugin ${plugin} from ${path31}: ${message}`,
665
670
  exportInvalid: (plugin, exportName) => `Plugin ${plugin} export ${exportName} is not a valid XCompiler plugin`,
666
671
  manifestMismatch: (plugin) => `Plugin ${plugin} runtime manifest does not match its preflight manifest`
667
672
  },
@@ -749,6 +754,7 @@ var messages = {
749
754
  optIntent: "plan intent: greenfield | feature | refactor | self",
750
755
  optBaselinePlan: "existing baseline phasePlan.json / plan.json path (default <workspace>/phasePlan.json)",
751
756
  optProjectFile: "XXX.xc project file path (default <workspace>/<name>.xc)",
757
+ optDebugWikiPath: "debug wiki root directory path (default <XCompiler path>/.xcompiler/debug-wiki)",
752
758
  argPlan: "phasePlan.json or legacy plan.json path (default = <workspace>/phasePlan.json)",
753
759
  argProjectFile: "XXX.xc project file",
754
760
  argStepId: "Step ID, e.g. S001",
@@ -895,7 +901,7 @@ var messages = {
895
901
  stepHeader: (id, phase, title, status, retries, maxRetries) => `${id} ${phase} ${title} ${status} retries=${retries}/${maxRetries}`,
896
902
  stepRoleTools: (role, tools) => `role=${role} tools=[${tools}]`,
897
903
  stepDependsOn: (ids) => `dependsOn: ${ids}`,
898
- outputStatus: (exists, p) => `${exists ? "\u2713" : "\u2717"} ${p}`,
904
+ outputStatus: (exists2, p) => `${exists2 ? "\u2713" : "\u2717"} ${p}`,
899
905
  auditEntry: (ts, kind, message) => `${ts} ${kind} ${message}`
900
906
  },
901
907
  execute: {
@@ -963,7 +969,7 @@ var messages = {
963
969
  deliveryGateReason: (command, exitCode, timedOut) => `FUNCTIONAL_TEST gate: \`${command}\` exit=${exitCode}${timedOut ? " (timeout)" : ""}`,
964
970
  missingPythonEntrypoint: "missing Python entrypoint: expected src/main.py, src/<package>/__main__.py, or an explicit CLI file such as src/cli.py",
965
971
  missingTypeScriptEntrypoint: "missing TypeScript entrypoint: expected package.json start/bin or one of src/main.ts, src/index.ts, src/main.tsx",
966
- invalidPythonEntrypointSource: (path30) => `invalid Python entrypoint source in ${path30}: expected a real CLI entry structure such as def main(...), argparse.ArgumentParser, or if __name__ == "__main__"; placeholder/import-only files are not runnable applications.`,
972
+ 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.`,
967
973
  entrypointHelpOutputMissing: (command) => `entrypoint probe \`${command}\` exited 0 but produced no meaningful help/usage text; implement --help instead of relying on an empty script exit.`,
968
974
  reasonLine: (reason) => `reason: ${reason}`,
969
975
  roundsLine: (rounds) => `rounds: ${rounds}`,
@@ -1032,12 +1038,13 @@ Question mix (functionality first):
1032
1038
  - 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.
1033
1039
  - 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.
1034
1040
  ${opts.projectShapeAmbiguous ? "- Required for this topic: ask the API library vs runnable application vs mixed-deliverable boundary explicitly.\n" : ""}
1041
+ ${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" : ""}
1035
1042
 
1036
- [Hard constraint] The implementation stack is already fixed by XCompiler config / the existing project baseline. Do not reopen language/runtime/package-manager decisions.
1043
+ ${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.
1037
1044
  **Do NOT** ask questions of these forms:
1038
1045
  - "Which programming language / framework / runtime should this use?"
1039
1046
  - "Which test framework / build tool / package manager?"
1040
- - "Which OS is the target platform?"
1047
+ - "Which OS is the target platform?"`}
1041
1048
  ${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.`,
1042
1049
  plannerDecompose: (raw, qa, addenda, opts = {}) => `Original requirement:
1043
1050
  """
@@ -1139,7 +1146,7 @@ Return strict JSON StepPlan for the current phase only.`,
1139
1146
  executorDebugBlock: (reason, suggestions) => `
1140
1147
 
1141
1148
  You are now in DEBUG retry mode. Previous failure reason: ${reason}
1142
- 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 ? `
1149
+ 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 ? `
1143
1150
 
1144
1151
  ${suggestions}` : ""),
1145
1152
  executorGlobalBlock: (globalPrompt) => `
@@ -1156,7 +1163,8 @@ ${sp}`,
1156
1163
  executorFeedbackVerifyMissing: (paths) => `outputs still missing: ${paths}. Please continue.`,
1157
1164
  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.",
1158
1165
  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.",
1159
- 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."
1166
+ 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.",
1167
+ 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."
1160
1168
  },
1161
1169
  skills: {
1162
1170
  patcher: "Use apply_patch / replace_in_file for small in-place edits to existing files; never overwrite a whole file.",
@@ -1177,7 +1185,7 @@ ${sp}`,
1177
1185
  summaryOk: "all checks passed.",
1178
1186
  summaryWarn: (n) => `passed with ${n} warning(s).`,
1179
1187
  summaryFail: (n) => `${n} failure(s) detected.`,
1180
- configLoadOk: (path30) => `config loaded: ${path30}`,
1188
+ configLoadOk: (path31) => `config loaded: ${path31}`,
1181
1189
  configLoadFail: (msg) => `failed to load config: ${msg}`,
1182
1190
  configLocale: (locale) => `locale=${locale}`,
1183
1191
  llmNoProviders: "no LLM providers defined in config.llm.providers",
@@ -1195,7 +1203,7 @@ ${sp}`,
1195
1203
  roleOk: (role, provider) => `role "${role}" \u2192 ${provider}`,
1196
1204
  sandboxKind: (kind) => `sandbox=${kind}`,
1197
1205
  sandboxNetworkPolicy: (policy, ports) => `network=${policy}` + (ports.length ? ` (expose_ports=[${ports.join(", ")}])` : ""),
1198
- 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.",
1206
+ 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.",
1199
1207
  sandboxNodeMissing: "node not found on PATH (required by TypeScript subprocess sandbox)",
1200
1208
  sandboxNodeOk: (version) => `node OK (${version})`,
1201
1209
  sandboxNpmMissing: "npm not found on PATH (required by TypeScript subprocess sandbox)",
@@ -1314,6 +1322,7 @@ var PYTHON_EXECUTOR_SYSTEM2 = `\u4F60\u662F XCompiler \u7684 Step Executor\u3002
1314
1322
  \u6BCF\u4E00\u8F6E\u4F60\u5FC5\u987B\u8FD4\u56DE\u4E25\u683C JSON\uFF1A
1315
1323
  {
1316
1324
  "thoughts": "<\u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u672C\u8F6E\u610F\u56FE>",
1325
+ "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>",
1317
1326
  "actions": [ { "tool": "<\u5DE5\u5177\u540D>", "args": { ... } }, ... ],
1318
1327
  "done": true | false
1319
1328
  }
@@ -1397,6 +1406,7 @@ var TYPESCRIPT_EXECUTOR_SYSTEM2 = `\u4F60\u662F XCompiler \u7684 Step Executor\u
1397
1406
  \u6BCF\u4E00\u8F6E\u4F60\u5FC5\u987B\u8FD4\u56DE\u4E25\u683C JSON\uFF1A
1398
1407
  {
1399
1408
  "thoughts": "<\u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u672C\u8F6E\u610F\u56FE>",
1409
+ "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>",
1400
1410
  "actions": [ { "tool": "<\u5DE5\u5177\u540D>", "args": { ... } }, ... ],
1401
1411
  "done": true | false
1402
1412
  }
@@ -1510,13 +1520,13 @@ var messages2 = {
1510
1520
  preflightOllamaUnreachable: (baseUrl, message) => `\u9884\u68C0\uFF1AOllama ${baseUrl} \u4E0D\u53EF\u8FBE\uFF1A${message}`,
1511
1521
  preflightAutoAdded: (providers, roles) => `\u9884\u68C0\uFF1A\u81EA\u52A8\u589E\u52A0 ${providers} \u4E2A provider\uFF0C\u8986\u76D6\u89D2\u8272 [${roles}]`,
1512
1522
  scoreFileHeader: "# XCompiler LLM provider \u8BC4\u5206\u5FEB\u7167\uFF08\u7531 ScoreStore \u81EA\u52A8\u7EF4\u62A4\uFF0C\u8BF7\u52FF\u624B\u5DE5\u7F16\u8F91\uFF09",
1513
- 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"
1523
+ 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"
1514
1524
  },
1515
1525
  system: {
1516
1526
  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}`,
1517
1527
  unhandledError: (message) => `\u672A\u5904\u7406\u9519\u8BEF\uFF1A${message}`,
1518
1528
  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",
1519
- 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",
1529
+ 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",
1520
1530
  firejailUnsupported: "\u5C1A\u672A\u5B9E\u73B0 sandbox=firejail\uFF0C\u8BF7\u4F7F\u7528 subprocess \u6216 docker\u3002",
1521
1531
  smokeHeader: (baseUrl) => `\u6B63\u5728\u5BF9 ${baseUrl} \u6267\u884C\u6D41\u5F0F\u5192\u70DF\u6D4B\u8BD5`,
1522
1532
  smokeOk: (model, totalMs, firstTokenMs, chunks, preview) => `[\u6210\u529F \u603B\u8017\u65F6=${totalMs}ms \u9996Token=${firstTokenMs}ms \u5206\u5757=${chunks}] ${model} -> ${preview}`,
@@ -1533,8 +1543,8 @@ var messages2 = {
1533
1543
  loaded: (plugin, version) => `\u63D2\u4EF6 ${plugin}@${version} \u5DF2\u52A0\u8F7D\u3002`,
1534
1544
  extensionConflict: (plugin, kind, name) => `\u63D2\u4EF6 ${plugin} \u4E0D\u80FD\u8986\u76D6\u5DF2\u6709 ${kind} \u201C${name}\u201D\u3002`,
1535
1545
  hookFailed: (plugin, stage, message) => `\u63D2\u4EF6 ${plugin} \u5728 ${stage} \u9636\u6BB5\u6267\u884C\u5931\u8D25\uFF1A${message}`,
1536
- manifestReadFailed: (path30, message) => `\u65E0\u6CD5\u8BFB\u53D6\u63D2\u4EF6\u6E05\u5355 ${path30}\uFF1A${message}`,
1537
- moduleLoadFailed: (plugin, path30, message) => `\u65E0\u6CD5\u4ECE ${path30} \u52A0\u8F7D\u63D2\u4EF6 ${plugin}\uFF1A${message}`,
1546
+ manifestReadFailed: (path31, message) => `\u65E0\u6CD5\u8BFB\u53D6\u63D2\u4EF6\u6E05\u5355 ${path31}\uFF1A${message}`,
1547
+ moduleLoadFailed: (plugin, path31, message) => `\u65E0\u6CD5\u4ECE ${path31} \u52A0\u8F7D\u63D2\u4EF6 ${plugin}\uFF1A${message}`,
1538
1548
  exportInvalid: (plugin, exportName) => `\u63D2\u4EF6 ${plugin} \u7684\u5BFC\u51FA ${exportName} \u4E0D\u662F\u6709\u6548 XCompiler \u63D2\u4EF6`,
1539
1549
  manifestMismatch: (plugin) => `\u63D2\u4EF6 ${plugin} \u7684\u8FD0\u884C\u65F6\u6E05\u5355\u4E0E\u9884\u68C0\u6E05\u5355\u4E0D\u4E00\u81F4`
1540
1550
  },
@@ -1622,6 +1632,7 @@ var messages2 = {
1622
1632
  optIntent: "\u8BA1\u5212\u610F\u56FE\uFF1Agreenfield | feature | refactor | self",
1623
1633
  optBaselinePlan: "\u5DF2\u6709\u57FA\u7EBF phasePlan.json / plan.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 <workspace>/phasePlan.json\uFF09",
1624
1634
  optProjectFile: "XXX.xc \u5DE5\u7A0B\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4 <workspace>/<name>.xc\uFF09",
1635
+ optDebugWikiPath: "debug wiki \u6839\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4 <XCompiler path>/.xcompiler/debug-wiki\uFF09",
1625
1636
  argPlan: "phasePlan.json \u6216\u5386\u53F2 plan.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 = <workspace>/phasePlan.json\uFF09",
1626
1637
  argProjectFile: "XXX.xc \u5DE5\u7A0B\u6587\u4EF6",
1627
1638
  argStepId: "Step ID\uFF0C\u5982 S001",
@@ -1768,7 +1779,7 @@ var messages2 = {
1768
1779
  stepHeader: (id, phase, title, status, retries, maxRetries) => `${id} ${phase} ${title} ${status} \u91CD\u8BD5=${retries}/${maxRetries}`,
1769
1780
  stepRoleTools: (role, tools) => `\u89D2\u8272=${role} \u5DE5\u5177=[${tools}]`,
1770
1781
  stepDependsOn: (ids) => `\u4F9D\u8D56\uFF1A${ids}`,
1771
- outputStatus: (exists, p) => `${exists ? "\u2713" : "\u2717"} ${p}`,
1782
+ outputStatus: (exists2, p) => `${exists2 ? "\u2713" : "\u2717"} ${p}`,
1772
1783
  auditEntry: (ts, kind, message) => `${ts} ${kind} ${message}`
1773
1784
  },
1774
1785
  execute: {
@@ -1836,7 +1847,7 @@ var messages2 = {
1836
1847
  deliveryGateReason: (command, exitCode, timedOut) => `FUNCTIONAL_TEST \u95E8\u7981\uFF1A\`${command}\` \u9000\u51FA\u7801=${exitCode}${timedOut ? "\uFF08\u8D85\u65F6\uFF09" : ""}`,
1837
1848
  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",
1838
1849
  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",
1839
- invalidPythonEntrypointSource: (path30) => `Python \u5165\u53E3\u6E90\u7801\u65E0\u6548\uFF1A${path30} \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`,
1850
+ 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`,
1840
1851
  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`,
1841
1852
  reasonLine: (reason) => `\u539F\u56E0\uFF1A${reason}`,
1842
1853
  roundsLine: (rounds) => `\u8F6E\u6B21\uFF1A${rounds}`,
@@ -1881,7 +1892,7 @@ var messages2 = {
1881
1892
  - \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
1882
1893
  - \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`,
1883
1894
  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
1884
- \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`,
1895
+ \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`,
1885
1896
  plannerClarify: (raw, opts = {}) => `\u7528\u6237\u7684\u539F\u59CB\u9700\u6C42\u5982\u4E0B\uFF1A
1886
1897
 
1887
1898
  """
@@ -1906,12 +1917,13 @@ ${raw}
1906
1917
  - \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
1907
1918
  - \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
1908
1919
  ${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" : ""}
1920
+ ${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" : ""}
1909
1921
 
1910
- \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
1922
+ ${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
1911
1923
  **\u4E25\u7981**\u63D0\u51FA\u4EE5\u4E0B\u7C7B\u578B\u7684\u95EE\u9898\uFF1A
1912
1924
  - "\u5E0C\u671B\u7528\u4EC0\u4E48\u7F16\u7A0B\u8BED\u8A00 / \u6846\u67B6 / \u8FD0\u884C\u65F6\u5B9E\u73B0\uFF1F"
1913
1925
  - "\u9700\u8981\u54EA\u79CD\u6D4B\u8BD5\u6846\u67B6 / \u6784\u5EFA\u5DE5\u5177 / \u5305\u7BA1\u7406\u5668\uFF1F"
1914
- - "\u76EE\u6807\u5E73\u53F0\u662F\u54EA\u79CD\u64CD\u4F5C\u7CFB\u7EDF\uFF1F"
1926
+ - "\u76EE\u6807\u5E73\u53F0\u662F\u54EA\u79CD\u64CD\u4F5C\u7CFB\u7EDF\uFF1F"`}
1915
1927
  ${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`,
1916
1928
  plannerDecompose: (raw, qa, addenda, opts = {}) => `\u539F\u59CB\u9700\u6C42\uFF1A
1917
1929
  """
@@ -2013,7 +2025,7 @@ ${opts.phasePlan}
2013
2025
  executorDebugBlock: (reason, suggestions) => `
2014
2026
 
2015
2027
  \u6B63\u5904\u4E8E DEBUG \u91CD\u8BD5\u6A21\u5F0F\u3002\u4E0A\u4E00\u8F6E\u5931\u8D25\u539F\u56E0: ${reason}
2016
- 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 ? `
2028
+ \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 ? `
2017
2029
 
2018
2030
  ${suggestions}` : ""),
2019
2031
  executorGlobalBlock: (globalPrompt) => `
@@ -2030,7 +2042,8 @@ ${sp}`,
2030
2042
  executorFeedbackVerifyMissing: (paths) => `outputs \u4ECD\u7F3A\u5931\uFF1A${paths}\u3002\u8BF7\u7EE7\u7EED\u3002`,
2031
2043
  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",
2032
2044
  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",
2033
- 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"
2045
+ 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",
2046
+ 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"
2034
2047
  },
2035
2048
  skills: {
2036
2049
  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",
@@ -2051,7 +2064,7 @@ ${sp}`,
2051
2064
  summaryOk: "\u5168\u90E8\u68C0\u67E5\u901A\u8FC7\u3002",
2052
2065
  summaryWarn: (n) => `\u901A\u8FC7\uFF0C\u4F46\u6709 ${n} \u6761 warning\u3002`,
2053
2066
  summaryFail: (n) => `\u68C0\u6D4B\u5230 ${n} \u9879\u5931\u8D25\u3002`,
2054
- configLoadOk: (path30) => `\u914D\u7F6E\u5DF2\u52A0\u8F7D\uFF1A${path30}`,
2067
+ configLoadOk: (path31) => `\u914D\u7F6E\u5DF2\u52A0\u8F7D\uFF1A${path31}`,
2055
2068
  configLoadFail: (msg) => `\u914D\u7F6E\u52A0\u8F7D\u5931\u8D25\uFF1A${msg}`,
2056
2069
  configLocale: (locale) => `locale=${locale}`,
2057
2070
  llmNoProviders: "config.llm.providers \u4E3A\u7A7A\uFF0C\u672A\u58F0\u660E\u4EFB\u4F55 provider",
@@ -2069,7 +2082,7 @@ ${sp}`,
2069
2082
  roleOk: (role, provider) => `\u89D2\u8272 "${role}" \u2192 ${provider}`,
2070
2083
  sandboxKind: (kind) => `sandbox=${kind}`,
2071
2084
  sandboxNetworkPolicy: (policy, ports) => `network=${policy}` + (ports.length ? `\uFF08expose_ports=[${ports.join(", ")}]\uFF09` : ""),
2072
- 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",
2085
+ 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",
2073
2086
  sandboxNodeMissing: "PATH \u4E0A\u627E\u4E0D\u5230 node\uFF08TypeScript subprocess \u6C99\u76D2\u5FC5\u9700\uFF09",
2074
2087
  sandboxNodeOk: (version) => `node OK\uFF08${version}\uFF09`,
2075
2088
  sandboxNpmMissing: "PATH \u4E0A\u627E\u4E0D\u5230 npm\uFF08TypeScript subprocess \u6C99\u76D2\u5FC5\u9700\uFF09",
@@ -2131,8 +2144,10 @@ function detectNetworkApiFailure(text) {
2131
2144
  function isTestAssertionDiagnosticLine(line) {
2132
2145
  const text = line.trim();
2133
2146
  if (!text) return false;
2147
+ if (/^\d+\|\s/u.test(text)) return true;
2134
2148
  if (/^(?:[→>-]\s*)?expected\b/iu.test(text)) return true;
2135
2149
  if (/\bAssertionError\b/iu.test(text) && /\bexpected\b/iu.test(text)) return true;
2150
+ if (/\bexpect\s*\(/u.test(text) && /\.(?:to|not)\w*\s*\(/u.test(text)) return true;
2136
2151
  return /\bexpected\b[\s\S]{0,240}\b(?:got|received|to\s+(?:be|equal|throw|contain|have|match))\b/iu.test(text);
2137
2152
  }
2138
2153
  function isTestRunnerStatusLine(line) {
@@ -2179,8 +2194,8 @@ async function autoFixSrcImports(ws, audit) {
2179
2194
  const candidates = [];
2180
2195
  if (await ws.exists("src/main.py")) candidates.push("src/main.py");
2181
2196
  try {
2182
- const fs27 = await import("fs/promises");
2183
- const entries = await fs27.readdir(ws.abs("src"), { withFileTypes: true });
2197
+ const fs29 = await import("fs/promises");
2198
+ const entries = await fs29.readdir(ws.abs("src"), { withFileTypes: true });
2184
2199
  for (const e of entries) {
2185
2200
  if (!e.isDirectory()) continue;
2186
2201
  const rel = `src/${e.name}/__main__.py`;
@@ -2331,8 +2346,8 @@ async function detectPythonEntrypoint(ws) {
2331
2346
  if (await ws.exists("src/main.py")) return fileCandidate("src/main.py");
2332
2347
  if (await ws.exists("src/__main__.py")) return fileCandidate("src/__main__.py");
2333
2348
  try {
2334
- const fs27 = await import("fs/promises");
2335
- const entries = await fs27.readdir(ws.abs("src"), { withFileTypes: true });
2349
+ const fs29 = await import("fs/promises");
2350
+ const entries = await fs29.readdir(ws.abs("src"), { withFileTypes: true });
2336
2351
  for (const e of entries) {
2337
2352
  if (!e.isDirectory()) continue;
2338
2353
  const rel = `src/${e.name}/__main__.py`;
@@ -2391,7 +2406,7 @@ var typescriptProfile = {
2391
2406
  manifestFile: "package.json",
2392
2407
  codeExtensions: [".ts", ".tsx"],
2393
2408
  seedManifestFromDeps: false,
2394
- defaultDockerImage: "node:20-slim",
2409
+ defaultDockerImage: "node:24-slim",
2395
2410
  renderManifest(deps) {
2396
2411
  const pkg = {
2397
2412
  name: "app",
@@ -2420,6 +2435,9 @@ var typescriptProfile = {
2420
2435
  },
2421
2436
  plannerPromptOverride: "",
2422
2437
  executorPromptOverride: "",
2438
+ async autoFixImports(ws, audit) {
2439
+ return autoFixTypeScriptTypeOnlyImports(ws, audit);
2440
+ },
2423
2441
  async probeEntry(ws, sandbox) {
2424
2442
  return probeTsEntrypoint(ws, sandbox);
2425
2443
  }
@@ -2569,6 +2587,99 @@ async function readJsonFile(ws, rel) {
2569
2587
  return null;
2570
2588
  }
2571
2589
  }
2590
+ var TYPE_ONLY_IMPORTS_BY_PACKAGE = {
2591
+ axios: /* @__PURE__ */ new Set([
2592
+ "AxiosAdapter",
2593
+ "AxiosBasicCredentials",
2594
+ "AxiosHeaderValue",
2595
+ "AxiosInstance",
2596
+ "AxiosInterceptorManager",
2597
+ "AxiosPromise",
2598
+ "AxiosProxyConfig",
2599
+ "AxiosRequestConfig",
2600
+ "AxiosRequestHeaders",
2601
+ "AxiosResponse",
2602
+ "AxiosResponseHeaders",
2603
+ "CreateAxiosDefaults",
2604
+ "InternalAxiosRequestConfig",
2605
+ "RawAxiosRequestHeaders"
2606
+ ])
2607
+ };
2608
+ async function autoFixTypeScriptTypeOnlyImports(ws, audit) {
2609
+ const files = await listTypeScriptSourceFiles(ws, "src");
2610
+ const fixed = [];
2611
+ for (const rel of files) {
2612
+ const original = await ws.readFile(rel);
2613
+ const next = rewriteKnownTypeOnlyImports(original);
2614
+ if (next === original) continue;
2615
+ await ws.writeFile(rel, next);
2616
+ fixed.push(rel);
2617
+ await audit.event("note", `fixed type-only imports in ${rel}`, {
2618
+ messageId: "audit.typescript_imports_autofix",
2619
+ path: rel
2620
+ });
2621
+ }
2622
+ return fixed;
2623
+ }
2624
+ async function listTypeScriptSourceFiles(ws, dir) {
2625
+ const abs = ws.abs(dir);
2626
+ let entries;
2627
+ try {
2628
+ entries = await fs.readdir(abs, { withFileTypes: true });
2629
+ } catch {
2630
+ return [];
2631
+ }
2632
+ const files = [];
2633
+ for (const entry of entries) {
2634
+ const rel = `${dir}/${entry.name}`.replace(/\\/g, "/");
2635
+ if (entry.isDirectory()) {
2636
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
2637
+ files.push(...await listTypeScriptSourceFiles(ws, rel));
2638
+ } else if (entry.isFile() && /\.tsx?$/.test(entry.name)) {
2639
+ files.push(rel);
2640
+ }
2641
+ }
2642
+ return files.sort();
2643
+ }
2644
+ function rewriteKnownTypeOnlyImports(source) {
2645
+ const importRe = /^import\s+([^'"\n]+?)\s+from\s+(['"])([^'"]+)\2\s*;?$/gm;
2646
+ return source.replace(importRe, (full, clauseRaw, quote, specifier) => {
2647
+ const knownTypes = TYPE_ONLY_IMPORTS_BY_PACKAGE[specifier];
2648
+ if (!knownTypes) return full;
2649
+ if (clauseRaw.trim().startsWith("type ")) return full;
2650
+ const parsed = splitImportClause(clauseRaw);
2651
+ if (!parsed?.named.length) return full;
2652
+ const valueNamed = [];
2653
+ const typeNamed = [];
2654
+ for (const item of parsed.named) {
2655
+ const importedName = item.replace(/^type\s+/u, "").split(/\s+as\s+/iu)[0]?.trim() ?? "";
2656
+ if (knownTypes.has(importedName)) typeNamed.push(item.replace(/^type\s+/u, "").trim());
2657
+ else valueNamed.push(item);
2658
+ }
2659
+ if (typeNamed.length === 0) return full;
2660
+ const lines = [];
2661
+ if (parsed.defaultImport && valueNamed.length > 0) {
2662
+ lines.push(`import ${parsed.defaultImport}, { ${valueNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2663
+ } else if (parsed.defaultImport) {
2664
+ lines.push(`import ${parsed.defaultImport} from ${quote}${specifier}${quote};`);
2665
+ } else if (valueNamed.length > 0) {
2666
+ lines.push(`import { ${valueNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2667
+ }
2668
+ lines.push(`import type { ${typeNamed.join(", ")} } from ${quote}${specifier}${quote};`);
2669
+ return lines.join("\n");
2670
+ });
2671
+ }
2672
+ function splitImportClause(clauseRaw) {
2673
+ const clause = clauseRaw.trim();
2674
+ const namedMatch = clause.match(/\{([\s\S]*)\}$/u);
2675
+ if (!namedMatch) return void 0;
2676
+ const beforeNamed = clause.slice(0, namedMatch.index).replace(/,\s*$/u, "").trim();
2677
+ const named = (namedMatch[1] ?? "").split(",").map((item) => item.trim()).filter(Boolean);
2678
+ return {
2679
+ defaultImport: beforeNamed || void 0,
2680
+ named
2681
+ };
2682
+ }
2572
2683
 
2573
2684
  // src/core/architecture.ts
2574
2685
  var COMPLEXITY_SURFACES = [
@@ -2760,7 +2871,7 @@ function validateArchitectureContract(modules, steps, language, demand) {
2760
2871
  }
2761
2872
  allTestPaths.add(testPath);
2762
2873
  }
2763
- const owners = codeSteps.filter((step) => module.sourcePaths.every((path30) => pathCoveredByOutputs(path30, step.outputs))).sort((a, b) => (stepIndex.get(a.id) ?? 0) - (stepIndex.get(b.id) ?? 0));
2874
+ 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));
2764
2875
  if (owners.length === 0) {
2765
2876
  issues.push({
2766
2877
  message: `${module.id} must map all sourcePaths to a CODE macro step; found 0.`
@@ -2790,7 +2901,7 @@ function validateArchitectureContract(modules, steps, language, demand) {
2790
2901
  ownedModules.push(module);
2791
2902
  modulesByCodeOwner.set(codeOwner.id, ownedModules);
2792
2903
  const matchingTests = testSteps.filter(
2793
- (step) => module.testPaths.some((path30) => pathCoveredByOutputs(path30, step.outputs))
2904
+ (step) => module.testPaths.some((path31) => pathCoveredByOutputs(path31, step.outputs))
2794
2905
  );
2795
2906
  if (matchingTests.length === 0) {
2796
2907
  issues.push({ message: `${module.id} testPaths are not produced by any MODULE_TEST step.` });
@@ -2847,8 +2958,8 @@ function flattenSubTasks(step) {
2847
2958
  }
2848
2959
  return out;
2849
2960
  }
2850
- function pathCoveredByOutputs(path30, outputs) {
2851
- const normalizedPath = normalizePath(path30);
2961
+ function pathCoveredByOutputs(path31, outputs) {
2962
+ const normalizedPath = normalizePath(path31);
2852
2963
  return outputs.some((output) => {
2853
2964
  const normalizedOutput = normalizePath(output);
2854
2965
  return normalizedPath === normalizedOutput || normalizedPath.startsWith(`${normalizedOutput}/`);
@@ -2858,14 +2969,14 @@ function missingArchitectureDocumentTokens(content, modules) {
2858
2969
  const required = modules.flatMap((module) => [module.id, ...module.sourcePaths, ...module.testPaths]);
2859
2970
  return [...new Set(required)].filter((token) => !content.includes(token));
2860
2971
  }
2861
- function isSourcePath(path30, extensions) {
2862
- return path30.startsWith("src/") && extensions.some((extension) => path30.endsWith(extension));
2972
+ function isSourcePath(path31, extensions) {
2973
+ return path31.startsWith("src/") && extensions.some((extension) => path31.endsWith(extension));
2863
2974
  }
2864
- function isTestPath(path30, extensions) {
2865
- return path30.startsWith("tests/") && extensions.some((extension) => path30.endsWith(extension));
2975
+ function isTestPath(path31, extensions) {
2976
+ return path31.startsWith("tests/") && extensions.some((extension) => path31.endsWith(extension));
2866
2977
  }
2867
- function normalizePath(path30) {
2868
- return path30.replace(/\\/g, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2978
+ function normalizePath(path31) {
2979
+ return path31.replace(/\\/g, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2869
2980
  }
2870
2981
  function transitivelyDependsOn(step, targetId, byId) {
2871
2982
  const seen = /* @__PURE__ */ new Set();
@@ -3398,12 +3509,12 @@ function parseLoadedPlan(json) {
3398
3509
  }
3399
3510
  async function savePlan(planPath, plan) {
3400
3511
  PlanSchema.parse(plan);
3401
- await fs.mkdir(path3.dirname(planPath), { recursive: true });
3402
- await fs.writeFile(planPath, JSON.stringify(plan, null, 2) + "\n", "utf8");
3512
+ await fs2.mkdir(path3.dirname(planPath), { recursive: true });
3513
+ await fs2.writeFile(planPath, JSON.stringify(plan, null, 2) + "\n", "utf8");
3403
3514
  }
3404
3515
  async function loadPlanTarget(inputPath) {
3405
3516
  const requestedPath = path3.resolve(inputPath);
3406
- const raw = await fs.readFile(requestedPath, "utf8");
3517
+ const raw = await fs2.readFile(requestedPath, "utf8");
3407
3518
  const json = JSON.parse(raw);
3408
3519
  const phasePlanResult = PhasePlanSchema.safeParse(json);
3409
3520
  if (phasePlanResult.success) {
@@ -3413,7 +3524,7 @@ async function loadPlanTarget(inputPath) {
3413
3524
  throw new Error(`phasePlan ${requestedPath} has no planPath for current phase ${phasePlan.currentPhaseId}`);
3414
3525
  }
3415
3526
  const planPath = path3.resolve(path3.dirname(requestedPath), phase.planPath);
3416
- const loaded2 = parseLoadedPlan(JSON.parse(await fs.readFile(planPath, "utf8")));
3527
+ const loaded2 = parseLoadedPlan(JSON.parse(await fs2.readFile(planPath, "utf8")));
3417
3528
  return {
3418
3529
  plan: loaded2.plan,
3419
3530
  planPath,
@@ -3518,7 +3629,7 @@ async function findProjectFile(workspace) {
3518
3629
  const ws = path4.resolve(workspace);
3519
3630
  let entries;
3520
3631
  try {
3521
- entries = await fs2.readdir(ws, { withFileTypes: true });
3632
+ entries = await fs3.readdir(ws, { withFileTypes: true });
3522
3633
  } catch {
3523
3634
  return void 0;
3524
3635
  }
@@ -3568,8 +3679,8 @@ async function updateProjectFile(opts) {
3568
3679
  history: nextHistory
3569
3680
  };
3570
3681
  XCompilerProjectFileSchema.parse(data);
3571
- await fs2.mkdir(path4.dirname(filePath), { recursive: true });
3572
- await fs2.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
3682
+ await fs3.mkdir(path4.dirname(filePath), { recursive: true });
3683
+ await fs3.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
3573
3684
  return filePath;
3574
3685
  }
3575
3686
  function buildProjectProgress(plan) {
@@ -3612,7 +3723,7 @@ function buildProjectProgress(plan) {
3612
3723
  }
3613
3724
  async function readExistingProjectFile(filePath) {
3614
3725
  try {
3615
- const raw = await fs2.readFile(filePath, "utf8");
3726
+ const raw = await fs3.readFile(filePath, "utf8");
3616
3727
  return XCompilerProjectFileSchema.parse(JSON.parse(raw));
3617
3728
  } catch {
3618
3729
  return void 0;
@@ -3642,10 +3753,10 @@ function assertProjectFileExtension(filePath) {
3642
3753
 
3643
3754
  // src/runtime/build.ts
3644
3755
  import path13 from "path";
3645
- import { promises as fs11 } from "fs";
3756
+ import { promises as fs12 } from "fs";
3646
3757
 
3647
3758
  // src/config/config.ts
3648
- import { promises as fs3 } from "fs";
3759
+ import { promises as fs4 } from "fs";
3649
3760
  import path5 from "path";
3650
3761
  import YAML from "yaml";
3651
3762
  import { z as z4 } from "zod";
@@ -3711,6 +3822,73 @@ var ProviderSchema = z4.object({
3711
3822
  think: z4.boolean().optional()
3712
3823
  });
3713
3824
  var LocaleSchema = z4.enum(["en", "zh"]);
3825
+ var TargetLanguageSchema = z4.enum(["python", "typescript"]);
3826
+ var SandboxModeSchema = z4.enum(["subprocess", "docker", "firejail"]);
3827
+ var SandboxLimitsSchema = z4.object({
3828
+ cpu: z4.number().positive().default(1),
3829
+ memory_mb: z4.number().int().positive().default(1024),
3830
+ wall_seconds: z4.number().int().positive().default(60),
3831
+ /**
3832
+ * Sandbox network policy.
3833
+ * - `off` — no network at all (`docker --network none`).
3834
+ * - `download-only` — outbound traffic allowed, no inbound port publishing.
3835
+ * - `pypi-only` — legacy value; rejected at sandbox creation rather than silently
3836
+ * allowing unrestricted outbound traffic.
3837
+ * - `full` — outbound + every port in `expose_ports` is published
3838
+ * to `127.0.0.1` so host-side tests can reach the app.
3839
+ */
3840
+ network: z4.enum(["off", "pypi-only", "download-only", "full"]).default("download-only"),
3841
+ /** Container ports to publish to 127.0.0.1 when `network=full`. */
3842
+ expose_ports: z4.array(z4.number().int().min(1).max(65535)).default([])
3843
+ }).default({
3844
+ cpu: 1,
3845
+ memory_mb: 1024,
3846
+ wall_seconds: 60,
3847
+ network: "download-only",
3848
+ expose_ports: []
3849
+ });
3850
+ var LocalSandboxSchema = z4.object({
3851
+ sandbox_dir: z4.string().min(1).optional(),
3852
+ python_bin: z4.string().min(1).optional(),
3853
+ inherit_env: z4.boolean().optional(),
3854
+ limits: SandboxLimitsSchema
3855
+ }).default(() => ({ limits: defaultSandboxLimits() }));
3856
+ var DockerSandboxSchema = z4.object({
3857
+ image: z4.string().default("python:3.11-slim"),
3858
+ workdir: z4.string().default("/workspace"),
3859
+ pull: z4.boolean().default(false),
3860
+ docker_bin: z4.string().default("docker"),
3861
+ extra_run_args: z4.array(z4.string()).default([]),
3862
+ sandbox_dir: z4.string().min(1).optional(),
3863
+ limits: SandboxLimitsSchema
3864
+ }).default({
3865
+ image: "python:3.11-slim",
3866
+ workdir: "/workspace",
3867
+ pull: false,
3868
+ docker_bin: "docker",
3869
+ extra_run_args: [],
3870
+ limits: defaultSandboxLimits()
3871
+ });
3872
+ var LanguageSandboxSchema = z4.object({
3873
+ mode: SandboxModeSchema.default("subprocess"),
3874
+ local: LocalSandboxSchema,
3875
+ docker: DockerSandboxSchema
3876
+ }).default(() => ({
3877
+ mode: "subprocess",
3878
+ local: { limits: defaultSandboxLimits() },
3879
+ docker: {
3880
+ image: "python:3.11-slim",
3881
+ workdir: "/workspace",
3882
+ pull: false,
3883
+ docker_bin: "docker",
3884
+ extra_run_args: [],
3885
+ limits: defaultSandboxLimits()
3886
+ }
3887
+ }));
3888
+ var SandboxesSchema = z4.object({
3889
+ python: LanguageSandboxSchema.optional(),
3890
+ typescript: LanguageSandboxSchema.optional()
3891
+ }).default({});
3714
3892
  var LlmSchema = z4.object({
3715
3893
  default: z4.string(),
3716
3894
  providers: z4.record(z4.string(), ProviderSchema),
@@ -3718,7 +3896,7 @@ var LlmSchema = z4.object({
3718
3896
  * 角色 → provider 数组的映射。
3719
3897
  * 兼容旧格式:单字符串 `Coder: ollama_code` 自动归一化为 `[ollama_code]`。
3720
3898
  * 数组形式 `Coder: [ollama_code, openai]` 表示该角色的候选 LLM 池;
3721
- * 实际选择顺序由 `llm.scores` 评分降序决定;用户显式设置评分=0 的 provider 直接跳过。
3899
+ * 实际选择顺序由 ScoreStore 有效评分降序决定;有效评分为用户覆盖优先,否则使用动态评分。
3722
3900
  */
3723
3901
  roles: z4.record(z4.string(), z4.union([z4.string(), z4.array(z4.string())])).default({}).transform((obj) => {
3724
3902
  const out = {};
@@ -3732,9 +3910,8 @@ var LlmSchema = z4.object({
3732
3910
  /** 可选:按角色指定 fallback 链(覆盖全局) */
3733
3911
  role_fallbacks: z4.record(z4.string(), z4.array(z4.string())).default({}),
3734
3912
  /**
3735
- * Provider 评分快照(启动时从 sidecar 文件加载,运行时由 ScoreStore 维护并自动落盘)。
3736
- * 配置文件里也可以手工设置初始值;不存在的 provider 默认评分 = 1。
3737
- * 用户手工设置评分 = 0 表示禁用;运行时自动评分只在 0.1~1.0 范围内调整。
3913
+ * Provider 兼容初值。运行时动态评分由 ScoreStore 写入 llm_scores.yaml;
3914
+ * 用户手动覆盖应写入 llm_scores_user.yaml。旧配置里显式 0 仍表示用户禁用。
3738
3915
  */
3739
3916
  scores: z4.record(z4.string(), z4.number().min(0)).default({}),
3740
3917
  /**
@@ -3754,65 +3931,107 @@ var LlmSchema = z4.object({
3754
3931
  });
3755
3932
  }
3756
3933
  });
3934
+ var AgentSchema = z4.object({
3935
+ /** @deprecated Target project language is inferred from topic/baseline. Kept for legacy configs only. */
3936
+ language: TargetLanguageSchema.optional(),
3937
+ max_steps: z4.number().int().positive().default(50),
3938
+ max_debug_retries: z4.number().int().positive().default(3),
3939
+ /** Debugger 滑动窗口的硬上限(默认 = max(max_debug_retries*4, 10))。 */
3940
+ max_debug_retries_cap: z4.number().int().positive().optional(),
3941
+ max_rounds_per_step: z4.number().int().positive().default(6),
3942
+ max_debug_rounds_per_step: z4.number().int().positive().optional(),
3943
+ max_edit_lines_per_step: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3944
+ max_write_chunk_bytes: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3945
+ /** @deprecated Use agent.sandboxes.<language>.mode. */
3946
+ sandbox: SandboxModeSchema.optional(),
3947
+ /** @deprecated Use agent.sandboxes.<language>.<local|docker>.limits. */
3948
+ sandbox_limits: SandboxLimitsSchema.optional(),
3949
+ /** @deprecated Use agent.sandboxes.<language>.docker. */
3950
+ sandbox_docker: DockerSandboxSchema.optional(),
3951
+ sandboxes: SandboxesSchema
3952
+ }).transform((agent) => {
3953
+ const legacyLanguage = agent.language ?? "python";
3954
+ const legacyMode = agent.sandbox ?? "subprocess";
3955
+ const legacyLimits = agent.sandbox_limits ?? defaultSandboxLimits();
3956
+ const defaults = {
3957
+ python: defaultLanguageSandbox("python", legacyMode, legacyLimits),
3958
+ typescript: defaultLanguageSandbox("typescript", legacyMode, legacyLimits)
3959
+ };
3960
+ const sandboxes = {
3961
+ python: mergeLanguageSandbox(
3962
+ defaults.python,
3963
+ agent.sandboxes.python,
3964
+ legacyLanguage === "python" ? agent.sandbox_docker : void 0
3965
+ ),
3966
+ typescript: mergeLanguageSandbox(
3967
+ defaults.typescript,
3968
+ agent.sandboxes.typescript,
3969
+ legacyLanguage === "typescript" ? agent.sandbox_docker : void 0
3970
+ )
3971
+ };
3972
+ return {
3973
+ ...agent,
3974
+ language: legacyLanguage,
3975
+ sandbox: sandboxes.python.mode,
3976
+ sandbox_limits: sandboxes.python.local.limits,
3977
+ sandbox_docker: sandboxes.python.docker,
3978
+ sandboxes
3979
+ };
3980
+ });
3757
3981
  var ConfigSchema = z4.object({
3758
3982
  /** CLI / prompt locale. Accepts 'en' (default) or 'zh'. */
3759
3983
  locale: LocaleSchema.optional(),
3760
3984
  /** @deprecated use `locale` instead. Kept as a backwards-compatible alias. */
3761
3985
  ui_language: LocaleSchema.optional(),
3762
3986
  llm: LlmSchema,
3763
- agent: z4.object({
3764
- language: z4.enum(["python", "typescript"]).default("python"),
3765
- max_steps: z4.number().int().positive().default(50),
3766
- max_debug_retries: z4.number().int().positive().default(3),
3767
- /** Debugger 滑动窗口的硬上限(默认 = max(max_debug_retries*4, 10))。 */
3768
- max_debug_retries_cap: z4.number().int().positive().optional(),
3769
- max_rounds_per_step: z4.number().int().positive().default(6),
3770
- max_debug_rounds_per_step: z4.number().int().positive().optional(),
3771
- max_edit_lines_per_step: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3772
- max_write_chunk_bytes: z4.union([z4.literal("auto"), z4.number().int().positive()]).default("auto"),
3773
- sandbox: z4.enum(["subprocess", "docker", "firejail"]).default("subprocess"),
3774
- sandbox_limits: z4.object({
3775
- cpu: z4.number().positive().default(1),
3776
- memory_mb: z4.number().int().positive().default(1024),
3777
- wall_seconds: z4.number().int().positive().default(60),
3778
- /**
3779
- * Sandbox network policy.
3780
- * - `off` — no network at all (`docker --network none`).
3781
- * - `download-only` — outbound traffic allowed, no inbound port publishing
3782
- * (default; lets python pip-install / fetch web pages).
3783
- * - `pypi-only` — legacy value; rejected at sandbox creation rather than silently
3784
- * allowing unrestricted outbound traffic.
3785
- * - `full` — outbound + every port in `expose_ports` is published
3786
- * to `127.0.0.1` so host-side tests can reach the app.
3787
- */
3788
- network: z4.enum(["off", "pypi-only", "download-only", "full"]).default("download-only"),
3789
- /** Container ports to publish to 127.0.0.1 when `network=full`. */
3790
- expose_ports: z4.array(z4.number().int().min(1).max(65535)).default([])
3791
- }).default({
3792
- cpu: 1,
3793
- memory_mb: 1024,
3794
- wall_seconds: 60,
3795
- network: "download-only",
3796
- expose_ports: []
3797
- }),
3798
- sandbox_docker: z4.object({
3799
- image: z4.string().default("python:3.11-slim"),
3800
- workdir: z4.string().default("/workspace"),
3801
- pull: z4.boolean().default(false),
3802
- docker_bin: z4.string().default("docker"),
3803
- extra_run_args: z4.array(z4.string()).default([])
3804
- }).default({
3805
- image: "python:3.11-slim",
3806
- workdir: "/workspace",
3807
- pull: false,
3808
- docker_bin: "docker",
3809
- extra_run_args: []
3810
- })
3811
- })
3987
+ agent: AgentSchema
3812
3988
  }).transform(({ locale, ui_language, ...rest }) => ({
3813
3989
  locale: locale ?? ui_language ?? "en",
3814
3990
  ...rest
3815
3991
  }));
3992
+ function defaultSandboxLimits() {
3993
+ return {
3994
+ cpu: 1,
3995
+ memory_mb: 1024,
3996
+ wall_seconds: 60,
3997
+ network: "download-only",
3998
+ expose_ports: []
3999
+ };
4000
+ }
4001
+ function defaultLanguageSandbox(language, mode, limits) {
4002
+ return {
4003
+ mode,
4004
+ local: {
4005
+ sandbox_dir: `.sandbox/${language}`,
4006
+ limits: { ...limits, expose_ports: [...limits.expose_ports ?? []] }
4007
+ },
4008
+ docker: {
4009
+ image: language === "typescript" ? "node:24-slim" : "python:3.11-slim",
4010
+ workdir: "/workspace",
4011
+ pull: false,
4012
+ docker_bin: "docker",
4013
+ extra_run_args: [],
4014
+ sandbox_dir: `.sandbox/${language}`,
4015
+ limits: { ...limits, expose_ports: [...limits.expose_ports ?? []] }
4016
+ }
4017
+ };
4018
+ }
4019
+ function mergeLanguageSandbox(defaults, override, legacyDocker) {
4020
+ const dockerOverride = legacyDocker ?? override?.docker;
4021
+ return {
4022
+ mode: override?.mode ?? defaults.mode,
4023
+ local: {
4024
+ ...defaults.local,
4025
+ ...override?.local ?? {},
4026
+ limits: override?.local?.limits ?? defaults.local.limits
4027
+ },
4028
+ docker: {
4029
+ ...defaults.docker,
4030
+ ...dockerOverride ?? {},
4031
+ limits: dockerOverride?.limits ?? defaults.docker.limits
4032
+ }
4033
+ };
4034
+ }
3816
4035
  function getXCompilerPath() {
3817
4036
  const env = xcEnv("PATH");
3818
4037
  if (env && env.trim()) return path5.resolve(env.trim());
@@ -3834,7 +4053,7 @@ async function loadConfigWithPath(explicitPath) {
3834
4053
  for (const abs of candidates) {
3835
4054
  tried.push(abs);
3836
4055
  try {
3837
- const raw = await fs3.readFile(abs, "utf8");
4056
+ const raw = await fs4.readFile(abs, "utf8");
3838
4057
  const expanded = expandEnv(raw);
3839
4058
  const data = YAML.parse(expanded);
3840
4059
  return { config: ConfigSchema.parse(data), path: abs };
@@ -3876,6 +4095,11 @@ var LOOP_REPEATS = 12;
3876
4095
  var LOOP_MIN_LEN = 1500;
3877
4096
  var LOOP_MIN_WINDOW = 96;
3878
4097
  var LOOP_MAX_PERIOD = 256;
4098
+ var TEXT_LOOP_MIN_LEN = 6e3;
4099
+ var TEXT_LOOP_TAIL = 14e3;
4100
+ var TEXT_LOOP_SAMPLE_LEN = 48;
4101
+ var TEXT_LOOP_STRIDE = 8;
4102
+ var TEXT_LOOP_REPEATS = 5;
3879
4103
  function detectCyclicTokenLoop(aggregate) {
3880
4104
  if (aggregate.length < LOOP_MIN_LEN) return false;
3881
4105
  const maxPeriod = Math.min(LOOP_MAX_PERIOD, Math.floor(aggregate.length / LOOP_REPEATS));
@@ -3897,6 +4121,35 @@ function detectCyclicTokenLoop(aggregate) {
3897
4121
  }
3898
4122
  return false;
3899
4123
  }
4124
+ function detectRepeatedTextLoop(aggregate) {
4125
+ if (aggregate.length < TEXT_LOOP_MIN_LEN) return false;
4126
+ const tail = aggregate.slice(-TEXT_LOOP_TAIL).replace(/\s+/gu, " ").trim();
4127
+ if (tail.length < TEXT_LOOP_MIN_LEN) return false;
4128
+ const seen = /* @__PURE__ */ new Map();
4129
+ for (let i = 0; i + TEXT_LOOP_SAMPLE_LEN <= tail.length; i += TEXT_LOOP_STRIDE) {
4130
+ const sample = tail.slice(i, i + TEXT_LOOP_SAMPLE_LEN);
4131
+ if (!isHighSignalLoopSample(sample)) continue;
4132
+ const prior = seen.get(sample);
4133
+ if (!prior) {
4134
+ seen.set(sample, { count: 1, first: i, last: i });
4135
+ continue;
4136
+ }
4137
+ const next = { count: prior.count + 1, first: prior.first, last: i };
4138
+ seen.set(sample, next);
4139
+ if (next.count >= TEXT_LOOP_REPEATS && next.last - next.first >= TEXT_LOOP_SAMPLE_LEN * 3) {
4140
+ return true;
4141
+ }
4142
+ }
4143
+ return false;
4144
+ }
4145
+ function isHighSignalLoopSample(sample) {
4146
+ const chars = [...sample];
4147
+ const signalChars = chars.filter((ch) => /[\p{L}\p{N}]/u.test(ch));
4148
+ if (signalChars.length < 20) return false;
4149
+ if (signalChars.length / Math.max(1, chars.length) < 0.4) return false;
4150
+ const unique = new Set(signalChars.map((ch) => ch.toLowerCase())).size;
4151
+ return unique >= 10;
4152
+ }
3900
4153
  var RepeatTokenDetector = class {
3901
4154
  constructor(threshold = 40) {
3902
4155
  this.threshold = threshold;
@@ -4075,6 +4328,12 @@ function streamPostNdjson(url, body, watchdog, onLine, shouldStopWhen) {
4075
4328
  );
4076
4329
  return;
4077
4330
  }
4331
+ if (detectRepeatedTextLoop(aggregate)) {
4332
+ fail(
4333
+ new Error("detected repeated text loop in stream (semantic tail repetition); aborting")
4334
+ );
4335
+ return;
4336
+ }
4078
4337
  }
4079
4338
  if (obj.error) {
4080
4339
  fail(new Error(`Ollama error: ${obj.error}`));
@@ -4103,6 +4362,10 @@ function streamPostNdjson(url, body, watchdog, onLine, shouldStopWhen) {
4103
4362
  fail(new Error("detected token loop in stream; aborting"));
4104
4363
  return;
4105
4364
  }
4365
+ if (detectRepeatedTextLoop(aggregate)) {
4366
+ fail(new Error("detected repeated text loop in stream; aborting"));
4367
+ return;
4368
+ }
4106
4369
  }
4107
4370
  });
4108
4371
  res.on("end", () => {
@@ -4373,6 +4636,9 @@ var OpenAIClient = class {
4373
4636
  if (detectCyclicTokenLoop(aggregate)) {
4374
4637
  throw new Error("detected cyclic token loop in OpenAI stream (periodic tail); aborting");
4375
4638
  }
4639
+ if (detectRepeatedTextLoop(aggregate)) {
4640
+ throw new Error("detected repeated text loop in OpenAI stream (semantic tail repetition); aborting");
4641
+ }
4376
4642
  if (maxOutputChars > 0 && expectsJsonObject && aggregate.length > maxOutputChars && hasInvalidJsonPrefix(aggregate)) {
4377
4643
  throw new Error(`OpenAI stream exceeded ${maxOutputChars} chars without a valid JSON prefix; aborting`);
4378
4644
  }
@@ -4909,22 +5175,28 @@ async function reportRoleModelAdvice(router, audit, reporter = (message) => {
4909
5175
  }
4910
5176
 
4911
5177
  // src/llm/scores.ts
4912
- import { promises as fs4 } from "fs";
5178
+ import { promises as fs5 } from "fs";
4913
5179
  import path6 from "path";
4914
5180
  import YAML2 from "yaml";
4915
5181
  var CLUSTER_PROVIDER_TAG = "cluster";
5182
+ var LLM_DYNAMIC_SCORES_FILE = "llm_scores.yaml";
5183
+ var LLM_USER_SCORES_FILE = "llm_scores_user.yaml";
4916
5184
  var ScoreStore = class _ScoreStore {
4917
5185
  constructor(configPath, initial = {}, audit, opts = {}) {
4918
5186
  this.audit = audit;
4919
- this.sidecarPath = path6.join(path6.dirname(configPath), "llm_scores.yaml");
5187
+ const configDir = path6.dirname(configPath);
5188
+ this.sidecarPath = path6.join(configDir, LLM_DYNAMIC_SCORES_FILE);
5189
+ this.userScoresPath = path6.join(configDir, LLM_USER_SCORES_FILE);
4920
5190
  this.clusterProviders = new Set(opts.clusterProviderNames ?? []);
4921
5191
  const bounds = normalizeClusterBounds(opts.clusterScoreMin, opts.clusterScoreMax);
4922
5192
  this.clusterMin = bounds.min;
4923
5193
  this.clusterMax = bounds.max;
4924
5194
  for (const [k, v] of Object.entries(initial)) {
4925
- const score = this.clampConfigured(k, v);
4926
- this.scores.set(k, score);
4927
- if (score === _ScoreStore.DISABLED) this.userDisabled.add(k);
5195
+ if (v === _ScoreStore.DISABLED) {
5196
+ this.userScores.set(k, _ScoreStore.DISABLED);
5197
+ } else {
5198
+ this.dynamicScores.set(k, this.clampDynamic(k, v));
5199
+ }
4928
5200
  }
4929
5201
  }
4930
5202
  audit;
@@ -4936,24 +5208,28 @@ var ScoreStore = class _ScoreStore {
4936
5208
  static CLUSTER_MAX = 0.5;
4937
5209
  static DECAY = 0.5;
4938
5210
  static BOOST = 0.1;
4939
- scores = /* @__PURE__ */ new Map();
4940
- userDisabled = /* @__PURE__ */ new Set();
5211
+ dynamicScores = /* @__PURE__ */ new Map();
5212
+ userScores = /* @__PURE__ */ new Map();
4941
5213
  clusterProviders;
4942
5214
  clusterMin;
4943
5215
  clusterMax;
4944
5216
  dirty = false;
4945
5217
  writeQueue = Promise.resolve();
4946
5218
  sidecarPath;
4947
- /** 异步加载 sidecar 文件;失败/不存在不抛错,使用 ctor 提供的初值。 */
5219
+ userScoresPath;
5220
+ /** 异步加载动态 sidecar 与用户覆盖文件;失败/不存在不抛错,使用 ctor 提供的初值。 */
4948
5221
  async load() {
5222
+ await this.loadDynamicScores();
5223
+ await this.loadUserScores();
5224
+ }
5225
+ async loadDynamicScores() {
4949
5226
  try {
4950
- const raw = await fs4.readFile(this.sidecarPath, "utf8");
5227
+ const raw = await fs5.readFile(this.sidecarPath, "utf8");
4951
5228
  const parsed = YAML2.parse(raw);
4952
5229
  if (parsed && typeof parsed === "object") {
4953
5230
  for (const [k, v] of Object.entries(parsed)) {
4954
5231
  if (typeof v === "number" && Number.isFinite(v)) {
4955
- if (this.userDisabled.has(k)) continue;
4956
- this.scores.set(k, this.clampDynamic(k, v));
5232
+ this.dynamicScores.set(k, this.clampPersistedDynamic(k, v));
4957
5233
  }
4958
5234
  }
4959
5235
  }
@@ -4966,44 +5242,73 @@ var ScoreStore = class _ScoreStore {
4966
5242
  }
4967
5243
  }
4968
5244
  }
5245
+ async loadUserScores() {
5246
+ try {
5247
+ const raw = await fs5.readFile(this.userScoresPath, "utf8");
5248
+ const parsed = YAML2.parse(raw);
5249
+ if (parsed && typeof parsed === "object") {
5250
+ for (const [k, v] of Object.entries(parsed)) {
5251
+ if (typeof v === "number" && Number.isFinite(v)) {
5252
+ this.userScores.set(k, this.clampUserScore(v));
5253
+ }
5254
+ }
5255
+ }
5256
+ } catch (err) {
5257
+ if (err.code !== "ENOENT") {
5258
+ await this.audit?.event("llm.error", t().llm.scoreReadFailed(this.userScoresPath, err.message), {
5259
+ messageId: "llm.user_score_read_failed",
5260
+ sidecarPath: this.userScoresPath
5261
+ });
5262
+ }
5263
+ }
5264
+ }
4969
5265
  get(name) {
4970
- return this.scores.has(name) ? this.scores.get(name) : this.defaultFor(name);
5266
+ if (this.userScores.has(name)) return this.userScores.get(name);
5267
+ return this.dynamicScores.has(name) ? this.dynamicScores.get(name) : this.defaultFor(name);
4971
5268
  }
4972
- /** 主动设置评分;value=0 仅用于显式禁用,其它值会归一到动态范围。 */
5269
+ /** 主动设置动态评分;用户覆盖只从配置或 llm_scores_user.yaml 读取。 */
4973
5270
  set(name, value, reason) {
4974
- if (this.userDisabled.has(name) && value !== _ScoreStore.DISABLED) return;
4975
5271
  const v = value === _ScoreStore.DISABLED ? _ScoreStore.DISABLED : this.clampDynamic(name, value);
4976
- const prev = this.scores.get(name);
5272
+ const prev = this.dynamicScores.get(name);
4977
5273
  if (prev === v) return;
4978
- this.scores.set(name, v);
5274
+ this.dynamicScores.set(name, v);
4979
5275
  this.dirty = true;
4980
- void this.audit?.event("llm.score", t().llm.scoreChanged(name, v.toFixed(2), (prev ?? _ScoreStore.DEFAULT).toFixed(2)), {
5276
+ void this.audit?.event("llm.score", t().llm.scoreChanged(name, v.toFixed(2), (prev ?? this.defaultFor(name)).toFixed(2)), {
4981
5277
  messageId: "llm.score_changed",
4982
5278
  provider: name,
4983
5279
  score: v,
4984
- previous: prev ?? _ScoreStore.DEFAULT,
5280
+ previous: prev ?? this.defaultFor(name),
4985
5281
  reason: reason ?? "set"
4986
5282
  });
4987
5283
  this.scheduleSave();
4988
5284
  }
4989
5285
  decay(name, reason) {
4990
- if (this.get(name) === _ScoreStore.DISABLED) return;
4991
- this.set(name, Math.max(this.minFor(name), this.get(name) - _ScoreStore.DECAY), reason);
5286
+ if (this.isUserDisabled(name)) return;
5287
+ const cur = this.dynamicScore(name);
5288
+ if (cur === _ScoreStore.DISABLED) return;
5289
+ this.set(name, Math.max(this.minFor(name), cur - _ScoreStore.DECAY), reason);
4992
5290
  }
4993
5291
  boost(name, reason) {
4994
- const cur = this.get(name);
5292
+ if (this.isUserDisabled(name)) return;
5293
+ const cur = this.dynamicScore(name);
4995
5294
  if (cur === _ScoreStore.DISABLED) return;
4996
5295
  if (cur >= this.maxFor(name)) return;
4997
5296
  this.set(name, Math.min(this.maxFor(name), cur + _ScoreStore.BOOST), reason);
4998
5297
  }
4999
- /** True only for providers explicitly disabled by user config (`llm.scores.<provider>: 0`). */
5298
+ /** True only for providers explicitly disabled by user config or llm_scores_user.yaml. */
5000
5299
  isUserDisabled(name) {
5001
- return this.userDisabled.has(name);
5300
+ return this.userScores.get(name) === _ScoreStore.DISABLED;
5002
5301
  }
5003
- /** 全量快照(用于测试与 audit 输出)。 */
5302
+ /** 动态评分快照(用于持久化、测试与 audit 输出;不包含用户覆盖)。 */
5004
5303
  snapshot() {
5005
5304
  const out = {};
5006
- for (const [k, v] of this.scores) out[k] = v;
5305
+ for (const [k, v] of this.dynamicScores) out[k] = v;
5306
+ return out;
5307
+ }
5308
+ /** 用户覆盖快照(用于测试/诊断;运行时不会改写用户文件)。 */
5309
+ userSnapshot() {
5310
+ const out = {};
5311
+ for (const [k, v] of this.userScores) out[k] = v;
5007
5312
  return out;
5008
5313
  }
5009
5314
  /** 等待待写入完成(CLI 退出前调用,确保评分已落盘)。 */
@@ -5026,8 +5331,8 @@ var ScoreStore = class _ScoreStore {
5026
5331
  ${t().llm.scoreFileSemantics}
5027
5332
  ` + YAML2.stringify(data);
5028
5333
  const tmp = `${this.sidecarPath}.tmp`;
5029
- await fs4.writeFile(tmp, yaml, "utf8");
5030
- await fs4.rename(tmp, this.sidecarPath);
5334
+ await fs5.writeFile(tmp, yaml, "utf8");
5335
+ await fs5.rename(tmp, this.sidecarPath);
5031
5336
  }
5032
5337
  defaultFor(name) {
5033
5338
  return this.isClusterProvider(name) ? this.clusterMax : _ScoreStore.DEFAULT;
@@ -5041,10 +5346,8 @@ ${t().llm.scoreFileSemantics}
5041
5346
  isClusterProvider(name) {
5042
5347
  return this.clusterProviders.has(name);
5043
5348
  }
5044
- clampConfigured(name, v) {
5045
- if (!Number.isFinite(v)) return this.defaultFor(name);
5046
- if (v === _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5047
- return this.clampDynamic(name, v);
5349
+ dynamicScore(name) {
5350
+ return this.dynamicScores.has(name) ? this.dynamicScores.get(name) : this.defaultFor(name);
5048
5351
  }
5049
5352
  clampDynamic(name, v) {
5050
5353
  if (!Number.isFinite(v)) return this.defaultFor(name);
@@ -5053,6 +5356,18 @@ ${t().llm.scoreFileSemantics}
5053
5356
  if (v > this.maxFor(name)) return this.maxFor(name);
5054
5357
  return v;
5055
5358
  }
5359
+ clampPersistedDynamic(name, v) {
5360
+ if (!Number.isFinite(v)) return this.defaultFor(name);
5361
+ if (v === _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5362
+ return this.clampDynamic(name, v);
5363
+ }
5364
+ clampUserScore(v) {
5365
+ if (!Number.isFinite(v)) return _ScoreStore.DEFAULT;
5366
+ if (v <= _ScoreStore.DISABLED) return _ScoreStore.DISABLED;
5367
+ if (v < _ScoreStore.MIN) return _ScoreStore.MIN;
5368
+ if (v > _ScoreStore.MAX) return _ScoreStore.MAX;
5369
+ return v;
5370
+ }
5056
5371
  };
5057
5372
  function normalizeClusterBounds(rawMin, rawMax) {
5058
5373
  const min = clampBound(rawMin ?? ScoreStore.CLUSTER_MIN);
@@ -5232,7 +5547,7 @@ async function defaultFetchTags(baseUrl, timeoutMs) {
5232
5547
  }
5233
5548
 
5234
5549
  // src/workspace/workspace.ts
5235
- import { promises as fs5 } from "fs";
5550
+ import { promises as fs6 } from "fs";
5236
5551
  import path7 from "path";
5237
5552
  var Workspace = class {
5238
5553
  constructor(root) {
@@ -5243,31 +5558,31 @@ var Workspace = class {
5243
5558
  return path7.resolve(this.root, ...p);
5244
5559
  }
5245
5560
  async ensure(dir) {
5246
- await fs5.mkdir(this.abs(dir), { recursive: true });
5561
+ await fs6.mkdir(this.abs(dir), { recursive: true });
5247
5562
  }
5248
5563
  async writeFile(rel, content) {
5249
5564
  const full = this.abs(rel);
5250
- await fs5.mkdir(path7.dirname(full), { recursive: true });
5251
- await fs5.writeFile(full, content, "utf8");
5565
+ await fs6.mkdir(path7.dirname(full), { recursive: true });
5566
+ await fs6.writeFile(full, content, "utf8");
5252
5567
  }
5253
5568
  async readFile(rel) {
5254
- return fs5.readFile(this.abs(rel), "utf8");
5569
+ return fs6.readFile(this.abs(rel), "utf8");
5255
5570
  }
5256
5571
  async exists(rel) {
5257
5572
  try {
5258
- await fs5.stat(this.abs(rel));
5573
+ await fs6.stat(this.abs(rel));
5259
5574
  return true;
5260
5575
  } catch {
5261
5576
  return false;
5262
5577
  }
5263
5578
  }
5264
5579
  async remove(rel) {
5265
- await fs5.rm(this.abs(rel), { recursive: true, force: true });
5580
+ await fs6.rm(this.abs(rel), { recursive: true, force: true });
5266
5581
  }
5267
5582
  };
5268
5583
 
5269
5584
  // src/workspace/doc_archive.ts
5270
- import { promises as fs6 } from "fs";
5585
+ import { promises as fs7 } from "fs";
5271
5586
  import path8 from "path";
5272
5587
  async function archiveIfExists(ws, rel, audit) {
5273
5588
  const norm = rel.replaceAll("\\", "/");
@@ -5280,7 +5595,7 @@ async function archiveIfExists(ws, rel, audit) {
5280
5595
  const target = `docs/history/${base}-${ts}${ext}`;
5281
5596
  try {
5282
5597
  await ws.ensure("docs/history");
5283
- await fs6.rename(ws.abs(norm), ws.abs(target));
5598
+ await fs7.rename(ws.abs(norm), ws.abs(target));
5284
5599
  await audit?.event("plan.persist", t().audit.documentArchived(norm, target), {
5285
5600
  messageId: "audit.document_archived",
5286
5601
  from: norm,
@@ -5482,6 +5797,18 @@ var DOC_PATH_ALIASES = {
5482
5797
  "docs/integration-test.md": DOC_NAMES.integrationTest,
5483
5798
  "docs/module-test.md": DOC_NAMES.moduleTest,
5484
5799
  "docs/functional-test.md": DOC_NAMES.functionalTest,
5800
+ "docs/unit-test-plan.md": DOC_NAMES.unitTestPlan,
5801
+ "docs/unit_test_plan.md": DOC_NAMES.unitTestPlan,
5802
+ "docs/tests/unit_test_plan.md": DOC_NAMES.unitTestPlan,
5803
+ "docs/integration-test-plan.md": DOC_NAMES.integrationTestPlan,
5804
+ "docs/integration_test_plan.md": DOC_NAMES.integrationTestPlan,
5805
+ "docs/tests/integration_test_plan.md": DOC_NAMES.integrationTestPlan,
5806
+ "docs/module-test-plan.md": DOC_NAMES.moduleTestPlan,
5807
+ "docs/module_test_plan.md": DOC_NAMES.moduleTestPlan,
5808
+ "docs/tests/module_test_plan.md": DOC_NAMES.moduleTestPlan,
5809
+ "docs/functional-test-plan.md": DOC_NAMES.functionalTestPlan,
5810
+ "docs/functional_test_plan.md": DOC_NAMES.functionalTestPlan,
5811
+ "docs/tests/functional_test_plan.md": DOC_NAMES.functionalTestPlan,
5485
5812
  "docs/quick-start.md": DOC_NAMES.quickstart,
5486
5813
  "docs/quick_start.md": DOC_NAMES.quickstart,
5487
5814
  "docs/quickstart.md": DOC_NAMES.quickstart,
@@ -5854,11 +6181,11 @@ function renderDebugSuggestions(sugs) {
5854
6181
  var normalizePythonRequirements = calibratePythonRequirements;
5855
6182
 
5856
6183
  // src/core/incremental.ts
5857
- import { promises as fs8 } from "fs";
6184
+ import { promises as fs9 } from "fs";
5858
6185
  import path10 from "path";
5859
6186
 
5860
6187
  // src/core/project_memory.ts
5861
- import { promises as fs7 } from "fs";
6188
+ import { promises as fs8 } from "fs";
5862
6189
  import path9 from "path";
5863
6190
  var PROJECT_MEMORY_PATH = ".xcompiler/project_memory.json";
5864
6191
  async function refreshProjectMemory(ws, opts = {}) {
@@ -6016,7 +6343,7 @@ async function defaultPlanMetadataPath(root) {
6016
6343
  }
6017
6344
  async function fileExists(filePath) {
6018
6345
  try {
6019
- await fs7.stat(filePath);
6346
+ await fs8.stat(filePath);
6020
6347
  return true;
6021
6348
  } catch {
6022
6349
  return false;
@@ -6044,7 +6371,7 @@ async function readWorkspaceText(ws, rel, maxChars) {
6044
6371
  }
6045
6372
  async function readAbsoluteText(fullPath, maxChars) {
6046
6373
  try {
6047
- const raw = await fs7.readFile(fullPath, "utf8");
6374
+ const raw = await fs8.readFile(fullPath, "utf8");
6048
6375
  return truncate(raw, maxChars);
6049
6376
  } catch {
6050
6377
  return "";
@@ -6066,7 +6393,7 @@ async function walk(abs, rel, out, depth) {
6066
6393
  if (depth > 6 || out.length >= 160) return;
6067
6394
  let entries;
6068
6395
  try {
6069
- entries = await fs7.readdir(abs, { withFileTypes: true });
6396
+ entries = await fs8.readdir(abs, { withFileTypes: true });
6070
6397
  } catch {
6071
6398
  return;
6072
6399
  }
@@ -6293,20 +6620,20 @@ function extractImportedStems(text) {
6293
6620
  function normalizeImportStem(value) {
6294
6621
  return value.replace(/^\.\//u, "").replace(/^\.\.\//u, "").replace(/\.(js|ts|tsx|py)$/u, "").replace(/\/index$/u, "").trim();
6295
6622
  }
6296
- function extractContractsFromDoc(path30, text) {
6623
+ function extractContractsFromDoc(path31, text) {
6297
6624
  const contracts = [];
6298
6625
  for (const rawLine of text.split("\n")) {
6299
6626
  const line = rawLine.replace(/^[-*#\d.\s]+/u, "").trim();
6300
6627
  if (line.length < 18) continue;
6301
6628
  const lower = line.toLowerCase();
6302
6629
  if (INVARIANT_RE.test(lower)) {
6303
- contracts.push({ kind: "invariant", subject: path30, path: path30, detail: trimContractLine(line) });
6630
+ contracts.push({ kind: "invariant", subject: path31, path: path31, detail: trimContractLine(line) });
6304
6631
  } else if (LIMITATION_RE.test(lower)) {
6305
- contracts.push({ kind: "limitation", subject: path30, path: path30, detail: trimContractLine(line) });
6632
+ contracts.push({ kind: "limitation", subject: path31, path: path31, detail: trimContractLine(line) });
6306
6633
  } else if (EXTENSION_RE.test(lower)) {
6307
- contracts.push({ kind: "extension-point", subject: path30, path: path30, detail: trimContractLine(line) });
6634
+ contracts.push({ kind: "extension-point", subject: path31, path: path31, detail: trimContractLine(line) });
6308
6635
  } else if (INTEGRATION_RE.test(lower)) {
6309
- contracts.push({ kind: "integration", subject: path30, path: path30, detail: trimContractLine(line) });
6636
+ contracts.push({ kind: "integration", subject: path31, path: path31, detail: trimContractLine(line) });
6310
6637
  }
6311
6638
  }
6312
6639
  return contracts;
@@ -6331,7 +6658,7 @@ function dedupContracts(contracts) {
6331
6658
  }
6332
6659
 
6333
6660
  // src/audit/audit.ts
6334
- import { promises as fs9, appendFileSync, mkdirSync, existsSync, writeFileSync } from "fs";
6661
+ import { promises as fs10, appendFileSync, mkdirSync, existsSync, writeFileSync } from "fs";
6335
6662
  import { createHash } from "crypto";
6336
6663
  import path11 from "path";
6337
6664
  var AuditLogger = class {
@@ -6488,8 +6815,8 @@ var AuditLogger = class {
6488
6815
  });
6489
6816
  }
6490
6817
  /**
6491
- * 记录一轮 Executor 思考:thoughts 文本、计划调用的 actions、是否完成。
6492
- * 写入 jsonl + markdown 折叠块,交付时可作为"AI 思考过程完整记录"。
6818
+ * 记录一轮 Executor 执行摘要:简短 intent、issue 处理方案、计划调用的 actions、是否完成。
6819
+ * 写入 jsonl + markdown 折叠块,交付时可追溯每轮决策和动作。
6493
6820
  */
6494
6821
  async executorTurn(stepId, role, round, payload) {
6495
6822
  const storedPayload = protectAuditContent(payload, this.contentMode);
@@ -6561,8 +6888,8 @@ ${t().audit.processLogPreamble}
6561
6888
  }
6562
6889
  async appendMd(text) {
6563
6890
  this.mdQueue = this.mdQueue.then(
6564
- () => fs9.appendFile(this.mdAbs, text, "utf8"),
6565
- () => fs9.appendFile(this.mdAbs, text, "utf8")
6891
+ () => fs10.appendFile(this.mdAbs, text, "utf8"),
6892
+ () => fs10.appendFile(this.mdAbs, text, "utf8")
6566
6893
  ).catch((err) => {
6567
6894
  console.warn(t().audit.markdownAppendFailed(err.message));
6568
6895
  });
@@ -6644,7 +6971,7 @@ function redactText(value) {
6644
6971
  }
6645
6972
 
6646
6973
  // src/core/lock.ts
6647
- import { promises as fs10, unlinkSync } from "fs";
6974
+ import { promises as fs11, unlinkSync } from "fs";
6648
6975
  import path12 from "path";
6649
6976
  import os from "os";
6650
6977
  var LockError = class extends Error {
@@ -6671,7 +6998,7 @@ function isAlive(pid) {
6671
6998
  }
6672
6999
  async function readLock(file) {
6673
7000
  try {
6674
- const raw = await fs10.readFile(file, "utf8");
7001
+ const raw = await fs11.readFile(file, "utf8");
6675
7002
  return JSON.parse(raw);
6676
7003
  } catch {
6677
7004
  return null;
@@ -6679,7 +7006,7 @@ async function readLock(file) {
6679
7006
  }
6680
7007
  async function acquireLock(workspace, command, options = {}) {
6681
7008
  const file = lockPath(workspace);
6682
- await fs10.mkdir(path12.dirname(file), { recursive: true });
7009
+ await fs11.mkdir(path12.dirname(file), { recursive: true });
6683
7010
  const info = {
6684
7011
  pid: process.pid,
6685
7012
  host: os.hostname(),
@@ -6689,7 +7016,7 @@ async function acquireLock(workspace, command, options = {}) {
6689
7016
  const payload = JSON.stringify(info, null, 2);
6690
7017
  const tryCreate = async () => {
6691
7018
  try {
6692
- const fh = await fs10.open(file, "wx");
7019
+ const fh = await fs11.open(file, "wx");
6693
7020
  await fh.writeFile(payload, "utf8");
6694
7021
  await fh.close();
6695
7022
  return true;
@@ -6702,14 +7029,14 @@ async function acquireLock(workspace, command, options = {}) {
6702
7029
  return makeReleaser(file);
6703
7030
  }
6704
7031
  if (options.force) {
6705
- await fs10.writeFile(file, payload, "utf8");
7032
+ await fs11.writeFile(file, payload, "utf8");
6706
7033
  return makeReleaser(file);
6707
7034
  }
6708
7035
  const existing = await readLock(file);
6709
7036
  if (existing) {
6710
7037
  const sameHost = existing.host === info.host;
6711
7038
  if (sameHost && !isAlive(existing.pid)) {
6712
- await fs10.writeFile(file, payload, "utf8");
7039
+ await fs11.writeFile(file, payload, "utf8");
6713
7040
  return makeReleaser(file);
6714
7041
  }
6715
7042
  throw new LockError(
@@ -6718,7 +7045,7 @@ async function acquireLock(workspace, command, options = {}) {
6718
7045
  existing
6719
7046
  );
6720
7047
  }
6721
- await fs10.writeFile(file, payload, "utf8");
7048
+ await fs11.writeFile(file, payload, "utf8");
6722
7049
  return makeReleaser(file);
6723
7050
  }
6724
7051
  function makeReleaser(file) {
@@ -6727,7 +7054,7 @@ function makeReleaser(file) {
6727
7054
  if (released) return;
6728
7055
  released = true;
6729
7056
  try {
6730
- await fs10.unlink(file);
7057
+ await fs11.unlink(file);
6731
7058
  } catch {
6732
7059
  }
6733
7060
  };
@@ -6752,7 +7079,7 @@ function makeReleaser(file) {
6752
7079
  }
6753
7080
 
6754
7081
  // src/version.ts
6755
- var XCOMPILER_VERSION = "0.2.3";
7082
+ var XCOMPILER_VERSION = "0.2.4";
6756
7083
  var XCOMPILER_PLUGIN_API_VERSION = 1;
6757
7084
 
6758
7085
  // src/plugins/compatibility.ts
@@ -7054,10 +7381,10 @@ function runtimeResult(io, command, status, data) {
7054
7381
  }
7055
7382
 
7056
7383
  // src/runtime/run.ts
7057
- import path27 from "path";
7384
+ import path28 from "path";
7058
7385
 
7059
7386
  // src/workspace/git.ts
7060
- import { promises as fs12 } from "fs";
7387
+ import { promises as fs13 } from "fs";
7061
7388
  import path14 from "path";
7062
7389
  import { simpleGit } from "simple-git";
7063
7390
  var RUNTIME_EXCLUDE_PATTERNS = [
@@ -7109,14 +7436,14 @@ var GitService = class {
7109
7436
  const excludePath = this.ws.abs(".git/info/exclude");
7110
7437
  let current2 = "";
7111
7438
  try {
7112
- current2 = await fs12.readFile(excludePath, "utf8");
7439
+ current2 = await fs13.readFile(excludePath, "utf8");
7113
7440
  } catch {
7114
7441
  return;
7115
7442
  }
7116
7443
  const missing = RUNTIME_EXCLUDE_PATTERNS.filter((pattern) => !current2.split(/\r?\n/u).includes(pattern));
7117
7444
  if (missing.length === 0) return;
7118
7445
  const prefix = current2.endsWith("\n") ? "\n" : "\n\n";
7119
- await fs12.appendFile(
7446
+ await fs13.appendFile(
7120
7447
  excludePath,
7121
7448
  `${prefix}# XCompiler runtime artifacts
7122
7449
  ${missing.join("\n")}
@@ -7161,7 +7488,7 @@ import { existsSync as existsSync2, readFileSync } from "fs";
7161
7488
 
7162
7489
  // src/sandbox/subprocess.ts
7163
7490
  import { spawn } from "child_process";
7164
- import { promises as fs13 } from "fs";
7491
+ import { promises as fs14 } from "fs";
7165
7492
  import path15 from "path";
7166
7493
  import crypto from "crypto";
7167
7494
 
@@ -7215,6 +7542,9 @@ function formatCommand(cmd, argv) {
7215
7542
  }
7216
7543
 
7217
7544
  // src/sandbox/subprocess.ts
7545
+ var INSTALL_IDLE_TIMEOUT_FLOOR_MS = 15 * 6e4;
7546
+ var INSTALL_IDLE_TIMEOUT_MULTIPLIER = 10;
7547
+ var INSTALL_PROGRESS_CHECK_INTERVAL_MS = 15e3;
7218
7548
  var SubprocessSandbox = class {
7219
7549
  constructor(opts) {
7220
7550
  this.opts = opts;
@@ -7268,12 +7598,13 @@ var SubprocessSandbox = class {
7268
7598
  * Python → requirements.txt (venv + pip);TypeScript → package.json (npm install)。
7269
7599
  */
7270
7600
  async build(manifestFile) {
7271
- await fs13.mkdir(this.sandboxAbs, { recursive: true });
7601
+ await fs14.mkdir(this.sandboxAbs, { recursive: true });
7272
7602
  if (this.opts.inheritEnv === false) {
7273
7603
  await Promise.all([
7274
- fs13.mkdir(path15.join(this.sandboxAbs, "home"), { recursive: true }),
7275
- fs13.mkdir(path15.join(this.sandboxAbs, "tmp"), { recursive: true }),
7276
- fs13.mkdir(path15.join(this.sandboxAbs, "npm-cache"), { recursive: true })
7604
+ fs14.mkdir(path15.join(this.sandboxAbs, "home"), { recursive: true }),
7605
+ fs14.mkdir(path15.join(this.sandboxAbs, "tmp"), { recursive: true }),
7606
+ fs14.mkdir(path15.join(this.sandboxAbs, "npm-cache"), { recursive: true }),
7607
+ fs14.mkdir(path15.join(this.sandboxAbs, "pip-cache"), { recursive: true })
7277
7608
  ]);
7278
7609
  }
7279
7610
  if (this.language === "typescript") {
@@ -7283,10 +7614,10 @@ var SubprocessSandbox = class {
7283
7614
  }
7284
7615
  async buildPython(requirementsTxt) {
7285
7616
  const reqAbs = this.opts.ws.abs(requirementsTxt);
7286
- const reqContent = await fs13.readFile(reqAbs, "utf8").catch(() => "");
7617
+ const reqContent = await fs14.readFile(reqAbs, "utf8").catch(() => "");
7287
7618
  const sig = crypto.createHash("sha256").update(reqContent).digest("hex");
7288
- const cached = await fs13.readFile(this.cacheFile, "utf8").catch(() => "");
7289
- const venvExists = await fs13.stat(this.pythonInVenv).then(() => true).catch(() => false);
7619
+ const cached = await fs14.readFile(this.cacheFile, "utf8").catch(() => "");
7620
+ const venvExists = await fs14.stat(this.pythonInVenv).then(() => true).catch(() => false);
7290
7621
  if (venvExists && cached === sig) {
7291
7622
  return { rebuilt: false, reason: "cache hit" };
7292
7623
  }
@@ -7303,7 +7634,7 @@ var SubprocessSandbox = class {
7303
7634
  }
7304
7635
  }
7305
7636
  const pyVenv = this.pythonInVenv;
7306
- const pyVenvExists = await fs13.stat(pyVenv).then(() => true).catch(() => false);
7637
+ const pyVenvExists = await fs14.stat(pyVenv).then(() => true).catch(() => false);
7307
7638
  if (!pyVenvExists) {
7308
7639
  throw new Error(`venv python missing after creation: ${pyVenv} (install system package python3-venv / python3-virtualenv)`);
7309
7640
  }
@@ -7320,17 +7651,26 @@ Install on the host: apt-get install python3-venv python3-pip or yum install
7320
7651
  }
7321
7652
  }
7322
7653
  if (reqContent.trim().length > 0) {
7654
+ const progressWatch = createInstallProgressWatch(
7655
+ [this.venvAbs, path15.join(this.sandboxAbs, "pip-cache")],
7656
+ this.opts.limits,
7657
+ "pip install"
7658
+ );
7659
+ await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `pip install -r ${reqAbs}`), {
7660
+ messageId: "sandbox.command",
7661
+ cwd: this.opts.ws.root,
7662
+ progressIdleTimeoutMs: progressWatch.idleTimeoutMs,
7663
+ progressPaths: progressWatch.paths
7664
+ });
7323
7665
  const r = await execRaw(pyVenv, ["-m", "pip", "install", "-r", reqAbs, "--quiet", "--disable-pip-version-check"], {
7324
- timeoutMs: this.opts.limits.wall_seconds * 1e3 * 5
7666
+ env: this.baseEnvironment(),
7667
+ progressWatch
7325
7668
  });
7326
7669
  if (r.exitCode !== 0) {
7327
- throw new Error(
7328
- `pip install failed (venv=${this.venvAbs}, requirements=${reqAbs}):
7329
- ${r.stderr || r.stdout}`
7330
- );
7670
+ throw new Error(formatExecFailure(`pip install failed (venv=${this.venvAbs}, requirements=${reqAbs})`, r));
7331
7671
  }
7332
7672
  }
7333
- await fs13.writeFile(this.cacheFile, sig, "utf8");
7673
+ await fs14.writeFile(this.cacheFile, sig, "utf8");
7334
7674
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.subprocessBuilt(!!reqContent), {
7335
7675
  messageId: "sandbox.subprocess_built"
7336
7676
  });
@@ -7338,29 +7678,39 @@ ${r.stderr || r.stdout}`
7338
7678
  }
7339
7679
  async buildNode(manifestFile) {
7340
7680
  const pkgAbs = this.opts.ws.abs(manifestFile);
7341
- const pkgContent = await fs13.readFile(pkgAbs, "utf8").catch(() => "");
7681
+ const pkgContent = await fs14.readFile(pkgAbs, "utf8").catch(() => "");
7342
7682
  if (!pkgContent) {
7343
7683
  return { rebuilt: false, reason: "no package.json yet" };
7344
7684
  }
7345
7685
  const lockAbs = this.opts.ws.abs("package-lock.json");
7346
- const lockContent = await fs13.readFile(lockAbs, "utf8").catch(() => "");
7686
+ const lockContent = await fs14.readFile(lockAbs, "utf8").catch(() => "");
7347
7687
  const sig = crypto.createHash("sha256").update(pkgContent + "\n" + lockContent).digest("hex");
7348
- const cached = await fs13.readFile(this.cacheFile, "utf8").catch(() => "");
7349
- const modulesExist = await fs13.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
7688
+ const cached = await fs14.readFile(this.cacheFile, "utf8").catch(() => "");
7689
+ const modulesExist = await fs14.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
7350
7690
  if (modulesExist && cached === sig) {
7351
7691
  return { rebuilt: false, reason: "cache hit" };
7352
7692
  }
7353
7693
  const installArgs = lockContent.trim() ? ["ci", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--ignore-scripts", "--no-audit", "--no-fund"];
7694
+ const progressWatch = createInstallProgressWatch(
7695
+ [this.opts.ws.abs("node_modules"), path15.join(this.sandboxAbs, "npm-cache")],
7696
+ this.opts.limits,
7697
+ "npm install"
7698
+ );
7699
+ await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `npm ${installArgs.join(" ")}`), {
7700
+ messageId: "sandbox.command",
7701
+ cwd: this.opts.ws.root,
7702
+ progressIdleTimeoutMs: progressWatch.idleTimeoutMs,
7703
+ progressPaths: progressWatch.paths
7704
+ });
7354
7705
  const r = await execRaw("npm", installArgs, {
7355
7706
  cwd: this.opts.ws.root,
7356
7707
  env: this.baseEnvironment(),
7357
- timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10
7708
+ progressWatch
7358
7709
  });
7359
7710
  if (r.exitCode !== 0) {
7360
- throw new Error(`npm dependency install failed (cwd=${this.opts.ws.root}):
7361
- ${r.stderr || r.stdout}`);
7711
+ throw new Error(formatExecFailure(`npm dependency install failed (cwd=${this.opts.ws.root})`, r));
7362
7712
  }
7363
- await fs13.writeFile(this.cacheFile, sig, "utf8");
7713
+ await fs14.writeFile(this.cacheFile, sig, "utf8");
7364
7714
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.subprocessNodeBuilt, {
7365
7715
  messageId: "sandbox.subprocess_node_built"
7366
7716
  });
@@ -7384,9 +7734,11 @@ ${r.stderr || r.stdout}`);
7384
7734
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.command("subprocess", `${cmd} ${argv.join(" ")}`), {
7385
7735
  messageId: "sandbox.command",
7386
7736
  cwd,
7387
- timeoutMs
7737
+ timeoutMs,
7738
+ progressIdleTimeoutMs: extra?.progressWatch?.idleTimeoutMs,
7739
+ progressPaths: extra?.progressWatch?.paths
7388
7740
  });
7389
- const r = await execRaw(cmd, argv, { cwd, env, timeoutMs });
7741
+ const r = await execRaw(cmd, argv, { cwd, env, timeoutMs, progressWatch: extra?.progressWatch });
7390
7742
  return r;
7391
7743
  }
7392
7744
  baseEnvironment() {
@@ -7420,16 +7772,55 @@ ${r.stderr || r.stdout}`);
7420
7772
  }
7421
7773
  /** 安装额外依赖(不会写入依赖清单,需要由调用方自行回写)。 */
7422
7774
  async installDeps(packages) {
7775
+ const progressWatch = createInstallProgressWatch(
7776
+ [
7777
+ this.language === "typescript" ? this.opts.ws.abs("node_modules") : this.venvAbs,
7778
+ path15.join(this.sandboxAbs, this.language === "typescript" ? "npm-cache" : "pip-cache")
7779
+ ],
7780
+ this.opts.limits,
7781
+ this.language === "typescript" ? "npm install" : "pip install"
7782
+ );
7423
7783
  if (this.language === "typescript") {
7424
- return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages]);
7784
+ return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages], {
7785
+ timeoutMs: 0,
7786
+ progressWatch
7787
+ });
7425
7788
  }
7426
- return this.exec(this.pythonInVenv, ["-m", "pip", "install", ...packages, "--quiet", "--disable-pip-version-check"]);
7789
+ return this.exec(this.pythonInVenv, ["-m", "pip", "install", ...packages, "--quiet", "--disable-pip-version-check"], {
7790
+ timeoutMs: 0,
7791
+ progressWatch
7792
+ });
7427
7793
  }
7428
7794
  };
7429
7795
  function sanitizeVenvName(name) {
7430
7796
  const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
7431
7797
  return cleaned.length > 0 ? cleaned : "venv";
7432
7798
  }
7799
+ function createInstallProgressWatch(paths, limits, label = "dependency install") {
7800
+ const idleTimeoutMs = Math.max(
7801
+ limits.wall_seconds * 1e3 * INSTALL_IDLE_TIMEOUT_MULTIPLIER,
7802
+ INSTALL_IDLE_TIMEOUT_FLOOR_MS
7803
+ );
7804
+ return {
7805
+ paths: dedupPaths(paths),
7806
+ idleTimeoutMs,
7807
+ checkIntervalMs: INSTALL_PROGRESS_CHECK_INTERVAL_MS,
7808
+ label
7809
+ };
7810
+ }
7811
+ function formatExecFailure(label, r) {
7812
+ const parts = [
7813
+ `${label} (exit=${r.exitCode}, timedOut=${r.timedOut ? "true" : "false"}, durationMs=${r.durationMs}${r.timeoutReason ? `, reason=${r.timeoutReason}` : ""}):`
7814
+ ];
7815
+ const stdout = clipExecOutput(r.stdout);
7816
+ const stderr = clipExecOutput(r.stderr);
7817
+ if (stdout) parts.push(`stdout:
7818
+ ${stdout}`);
7819
+ if (stderr) parts.push(`stderr:
7820
+ ${stderr}`);
7821
+ if (!stdout && !stderr) parts.push("(no stdout/stderr captured)");
7822
+ return parts.join("\n");
7823
+ }
7433
7824
  async function execRaw(cmd, argv, opts = {}) {
7434
7825
  const start = Date.now();
7435
7826
  return new Promise((resolve) => {
@@ -7441,10 +7832,46 @@ async function execRaw(cmd, argv, opts = {}) {
7441
7832
  let stdout = "";
7442
7833
  let stderr = "";
7443
7834
  let timedOut = false;
7835
+ let timeoutReason;
7836
+ let settled = false;
7837
+ let progressTimer = null;
7838
+ let progressStopped = false;
7444
7839
  const t2 = opts.timeoutMs ? setTimeout(() => {
7445
7840
  timedOut = true;
7841
+ timeoutReason = `wall-clock timeout after ${opts.timeoutMs}ms`;
7446
7842
  child.kill("SIGKILL");
7447
7843
  }, opts.timeoutMs) : null;
7844
+ const finish = (result) => {
7845
+ if (settled) return;
7846
+ settled = true;
7847
+ progressStopped = true;
7848
+ if (t2) clearTimeout(t2);
7849
+ if (progressTimer) clearTimeout(progressTimer);
7850
+ resolve(result);
7851
+ };
7852
+ if (opts.progressWatch?.paths.length) {
7853
+ const watch = opts.progressWatch;
7854
+ let lastProgressAt = start;
7855
+ let lastObservedSize = null;
7856
+ const checkProgress = async () => {
7857
+ if (progressStopped) return;
7858
+ const observedAt = Date.now();
7859
+ const currentSize = await directoryTreeSize(watch.paths);
7860
+ if (progressStopped) return;
7861
+ if (lastObservedSize !== null && currentSize > lastObservedSize) {
7862
+ lastProgressAt = observedAt;
7863
+ }
7864
+ lastObservedSize = currentSize;
7865
+ if (observedAt - lastProgressAt >= watch.idleTimeoutMs) {
7866
+ timedOut = true;
7867
+ timeoutReason = `${watch.label ?? "process"} progress idle for ${watch.idleTimeoutMs}ms; watched paths did not grow: ${watch.paths.join(", ")}`;
7868
+ child.kill("SIGKILL");
7869
+ return;
7870
+ }
7871
+ progressTimer = setTimeout(checkProgress, watch.checkIntervalMs ?? INSTALL_PROGRESS_CHECK_INTERVAL_MS);
7872
+ };
7873
+ progressTimer = setTimeout(checkProgress, watch.checkIntervalMs ?? INSTALL_PROGRESS_CHECK_INTERVAL_MS);
7874
+ }
7448
7875
  child.stdout.on("data", (b) => {
7449
7876
  stdout += b.toString();
7450
7877
  });
@@ -7452,31 +7879,69 @@ async function execRaw(cmd, argv, opts = {}) {
7452
7879
  stderr += b.toString();
7453
7880
  });
7454
7881
  child.on("error", (err) => {
7455
- if (t2) clearTimeout(t2);
7456
- resolve({
7882
+ finish({
7457
7883
  exitCode: -1,
7458
7884
  stdout,
7459
7885
  stderr: stderr + `
7460
7886
  [spawn error] ${err.message}`,
7461
7887
  timedOut,
7888
+ ...timeoutReason ? { timeoutReason } : {},
7462
7889
  durationMs: Date.now() - start
7463
7890
  });
7464
7891
  });
7465
7892
  child.on("close", (code) => {
7466
- if (t2) clearTimeout(t2);
7467
- resolve({
7893
+ finish({
7468
7894
  exitCode: code ?? -1,
7469
7895
  stdout,
7470
7896
  stderr,
7471
7897
  timedOut,
7898
+ ...timeoutReason ? { timeoutReason } : {},
7472
7899
  durationMs: Date.now() - start
7473
7900
  });
7474
7901
  });
7475
7902
  });
7476
7903
  }
7904
+ async function directoryTreeSize(paths) {
7905
+ let total = 0;
7906
+ for (const p of dedupPaths(paths)) {
7907
+ total += await pathTreeSize(p);
7908
+ }
7909
+ return total;
7910
+ }
7911
+ async function pathTreeSize(abs) {
7912
+ let stat;
7913
+ try {
7914
+ stat = await fs14.lstat(abs);
7915
+ } catch {
7916
+ return 0;
7917
+ }
7918
+ if (!stat.isDirectory()) {
7919
+ return stat.isFile() ? stat.size : 0;
7920
+ }
7921
+ let total = stat.size;
7922
+ let entries;
7923
+ try {
7924
+ entries = await fs14.readdir(abs, { withFileTypes: true });
7925
+ } catch {
7926
+ return total;
7927
+ }
7928
+ for (const entry of entries) {
7929
+ total += await pathTreeSize(path15.join(abs, entry.name));
7930
+ }
7931
+ return total;
7932
+ }
7933
+ function dedupPaths(paths) {
7934
+ return Array.from(new Set(paths.map((p) => path15.resolve(p)).filter(Boolean)));
7935
+ }
7936
+ function clipExecOutput(text, max = 6e3) {
7937
+ const normalized = String(text ?? "").trim();
7938
+ if (normalized.length <= max) return normalized;
7939
+ return `${normalized.slice(0, max)}
7940
+ ...[truncated ${normalized.length - max} chars]`;
7941
+ }
7477
7942
 
7478
7943
  // src/sandbox/docker.ts
7479
- import { promises as fs14 } from "fs";
7944
+ import { promises as fs15 } from "fs";
7480
7945
  import path16 from "path";
7481
7946
  import crypto2 from "crypto";
7482
7947
  var DockerSandbox = class {
@@ -7486,7 +7951,7 @@ var DockerSandbox = class {
7486
7951
  throw new Error(t().system.unsupportedPypiOnlyNetwork);
7487
7952
  }
7488
7953
  this.language = opts.language ?? "python";
7489
- this.image = opts.image ?? (this.language === "typescript" ? "node:20-slim" : "python:3.11-slim");
7954
+ this.image = opts.image ?? (this.language === "typescript" ? "node:24-slim" : "python:3.11-slim");
7490
7955
  this.workdir = opts.workdir ?? "/workspace";
7491
7956
  this.sandboxRel = (opts.sandboxDir ?? ".sandbox").replaceAll("\\", "/");
7492
7957
  this.cacheRel = `${this.sandboxRel}/${this.language === "typescript" ? "package.sha256" : "requirements.sha256"}`;
@@ -7535,7 +8000,7 @@ var DockerSandbox = class {
7535
8000
  async build(manifestFile) {
7536
8001
  await this.assertDocker();
7537
8002
  const sandboxAbs = this.opts.ws.abs(this.sandboxRel);
7538
- await fs14.mkdir(sandboxAbs, { recursive: true });
8003
+ await fs15.mkdir(sandboxAbs, { recursive: true });
7539
8004
  if (this.language === "typescript") {
7540
8005
  return this.buildNode(manifestFile ?? "package.json");
7541
8006
  }
@@ -7544,11 +8009,11 @@ var DockerSandbox = class {
7544
8009
  async buildPython(requirementsTxt) {
7545
8010
  const sandboxAbs = this.opts.ws.abs(this.sandboxRel);
7546
8011
  const reqAbs = this.opts.ws.abs(requirementsTxt);
7547
- const reqContent = await fs14.readFile(reqAbs, "utf8").catch(() => "");
8012
+ const reqContent = await fs15.readFile(reqAbs, "utf8").catch(() => "");
7548
8013
  const sig = crypto2.createHash("sha256").update(this.image + "\n" + reqContent).digest("hex");
7549
8014
  const cacheAbs = this.opts.ws.abs(this.cacheRel);
7550
- const cached = await fs14.readFile(cacheAbs, "utf8").catch(() => "");
7551
- const venvExists = await fs14.stat(path16.join(sandboxAbs, this.venvName, "bin", "python")).then(() => true).catch(() => false);
8015
+ const cached = await fs15.readFile(cacheAbs, "utf8").catch(() => "");
8016
+ const venvExists = await fs15.stat(path16.join(sandboxAbs, this.venvName, "bin", "python")).then(() => true).catch(() => false);
7552
8017
  if (venvExists && cached === sig) {
7553
8018
  return { rebuilt: false, reason: "cache hit" };
7554
8019
  }
@@ -7560,6 +8025,11 @@ var DockerSandbox = class {
7560
8025
  }
7561
8026
  const reqInContainer = `${this.workdir}/${requirementsTxt}`;
7562
8027
  const installCmd = reqContent.trim().length > 0 ? `python -m venv ${this.venvInContainer} && ${this.pipInContainer} install --quiet -r ${reqInContainer}` : `python -m venv ${this.venvInContainer}`;
8028
+ const progressWatch = createInstallProgressWatch(
8029
+ [path16.join(sandboxAbs, this.venvName)],
8030
+ this.opts.limits,
8031
+ "docker pip install"
8032
+ );
7563
8033
  const r = await execRaw(
7564
8034
  this.dockerBin,
7565
8035
  [
@@ -7575,15 +8045,12 @@ var DockerSandbox = class {
7575
8045
  "-lc",
7576
8046
  installCmd
7577
8047
  ],
7578
- { timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10 }
8048
+ { progressWatch }
7579
8049
  );
7580
8050
  if (r.exitCode !== 0) {
7581
- throw new Error(
7582
- `docker sandbox build failed (exit=${r.exitCode}):
7583
- ${r.stderr || r.stdout}`
7584
- );
8051
+ throw new Error(formatExecFailure(`docker sandbox build failed (image=${this.image}, cwd=${this.opts.ws.root})`, r));
7585
8052
  }
7586
- await fs14.writeFile(cacheAbs, sig, "utf8");
8053
+ await fs15.writeFile(cacheAbs, sig, "utf8");
7587
8054
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.dockerBuilt(!!reqContent), {
7588
8055
  messageId: "sandbox.docker_built",
7589
8056
  image: this.image
@@ -7592,15 +8059,15 @@ ${r.stderr || r.stdout}`
7592
8059
  }
7593
8060
  async buildNode(manifestFile) {
7594
8061
  const pkgAbs = this.opts.ws.abs(manifestFile);
7595
- const pkgContent = await fs14.readFile(pkgAbs, "utf8").catch(() => "");
8062
+ const pkgContent = await fs15.readFile(pkgAbs, "utf8").catch(() => "");
7596
8063
  if (!pkgContent) {
7597
8064
  return { rebuilt: false, reason: "no package.json yet" };
7598
8065
  }
7599
- const lockContent = await fs14.readFile(this.opts.ws.abs("package-lock.json"), "utf8").catch(() => "");
8066
+ const lockContent = await fs15.readFile(this.opts.ws.abs("package-lock.json"), "utf8").catch(() => "");
7600
8067
  const sig = crypto2.createHash("sha256").update(this.image + "\n" + pkgContent + "\n" + lockContent).digest("hex");
7601
8068
  const cacheAbs = this.opts.ws.abs(this.cacheRel);
7602
- const cached = await fs14.readFile(cacheAbs, "utf8").catch(() => "");
7603
- const modulesExist = await fs14.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
8069
+ const cached = await fs15.readFile(cacheAbs, "utf8").catch(() => "");
8070
+ const modulesExist = await fs15.stat(this.opts.ws.abs("node_modules")).then(() => true).catch(() => false);
7604
8071
  if (modulesExist && cached === sig) {
7605
8072
  return { rebuilt: false, reason: "cache hit" };
7606
8073
  }
@@ -7611,6 +8078,11 @@ ${r.stderr || r.stdout}`
7611
8078
  }
7612
8079
  }
7613
8080
  const installCommand = lockContent.trim() ? "npm ci --ignore-scripts --no-audit --no-fund" : "npm install --ignore-scripts --no-audit --no-fund";
8081
+ const progressWatch = createInstallProgressWatch(
8082
+ [this.opts.ws.abs("node_modules")],
8083
+ this.opts.limits,
8084
+ "docker npm install"
8085
+ );
7614
8086
  const r = await execRaw(
7615
8087
  this.dockerBin,
7616
8088
  [
@@ -7626,13 +8098,12 @@ ${r.stderr || r.stdout}`
7626
8098
  "-lc",
7627
8099
  installCommand
7628
8100
  ],
7629
- { timeoutMs: this.opts.limits.wall_seconds * 1e3 * 10 }
8101
+ { progressWatch }
7630
8102
  );
7631
8103
  if (r.exitCode !== 0) {
7632
- throw new Error(`docker sandbox build failed (exit=${r.exitCode}):
7633
- ${r.stderr || r.stdout}`);
8104
+ throw new Error(formatExecFailure(`docker sandbox build failed (image=${this.image}, cwd=${this.opts.ws.root})`, r));
7634
8105
  }
7635
- await fs14.writeFile(cacheAbs, sig, "utf8");
8106
+ await fs15.writeFile(cacheAbs, sig, "utf8");
7636
8107
  await this.opts.audit?.event("sandbox.exec", t().sandboxLog.dockerNodeBuilt, {
7637
8108
  messageId: "sandbox.docker_node_built",
7638
8109
  image: this.image
@@ -7685,7 +8156,7 @@ ${r.stderr || r.stdout}`);
7685
8156
  image: this.image,
7686
8157
  network: this.opts.limits.network
7687
8158
  });
7688
- return execRaw(this.dockerBin, dockerArgs, { timeoutMs });
8159
+ return execRaw(this.dockerBin, dockerArgs, { timeoutMs, progressWatch: extra?.progressWatch });
7689
8160
  }
7690
8161
  async runProgram(args, extra) {
7691
8162
  if (this.language === "typescript") {
@@ -7703,10 +8174,23 @@ ${r.stderr || r.stdout}`);
7703
8174
  return this.exec(this.pythonInContainer, ["-m", "pytest", ...args], extra);
7704
8175
  }
7705
8176
  async installDeps(packages) {
8177
+ const progressWatch = createInstallProgressWatch(
8178
+ [
8179
+ this.language === "typescript" ? this.opts.ws.abs("node_modules") : this.opts.ws.abs(`${this.sandboxRel}/${this.venvName}`)
8180
+ ],
8181
+ this.opts.limits,
8182
+ this.language === "typescript" ? "docker npm install" : "docker pip install"
8183
+ );
7706
8184
  if (this.language === "typescript") {
7707
- return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages]);
8185
+ return this.exec("npm", ["install", "--no-audit", "--no-fund", ...packages], {
8186
+ timeoutMs: 0,
8187
+ progressWatch
8188
+ });
7708
8189
  }
7709
- return this.exec(this.pipInContainer, ["install", "--quiet", ...packages]);
8190
+ return this.exec(this.pipInContainer, ["install", "--quiet", ...packages], {
8191
+ timeoutMs: 0,
8192
+ progressWatch
8193
+ });
7710
8194
  }
7711
8195
  /** 将宿主机绝对路径映射回容器路径;相对路径或 undefined 原样返回(相对则拼到 workdir)。 */
7712
8196
  toContainerPath(p) {
@@ -7737,12 +8221,11 @@ function isRunningInContainer() {
7737
8221
  }
7738
8222
  return false;
7739
8223
  }
7740
- function createSandbox(cfg, ws, audit, language = cfg.agent.language) {
7741
- const profile = getLanguageProfile(language);
7742
- const configuredImage = cfg.agent.sandbox_docker.image;
7743
- const image = language === cfg.agent.language ? configuredImage : profile.defaultDockerImage;
7744
- const kind = cfg.agent.sandbox;
7745
- if (cfg.agent.sandbox_limits.network === "pypi-only") {
8224
+ function createSandbox(cfg, ws, audit, language = "python") {
8225
+ const languageSandbox = cfg.agent.sandboxes?.[language] ?? legacyLanguageSandbox(cfg, language);
8226
+ const kind = languageSandbox.mode;
8227
+ const activeLimits = kind === "docker" ? languageSandbox.docker.limits : languageSandbox.local.limits;
8228
+ if (activeLimits.network === "pypi-only") {
7746
8229
  throw new Error(t().system.unsupportedPypiOnlyNetwork);
7747
8230
  }
7748
8231
  if (kind === "docker") {
@@ -7751,24 +8234,60 @@ function createSandbox(cfg, ws, audit, language = cfg.agent.language) {
7751
8234
  }
7752
8235
  return new DockerSandbox({
7753
8236
  ws,
7754
- limits: cfg.agent.sandbox_limits,
8237
+ limits: languageSandbox.docker.limits,
7755
8238
  audit,
7756
8239
  language,
7757
- image,
7758
- workdir: cfg.agent.sandbox_docker.workdir,
7759
- pull: cfg.agent.sandbox_docker.pull,
7760
- dockerBin: cfg.agent.sandbox_docker.docker_bin,
7761
- extraRunArgs: cfg.agent.sandbox_docker.extra_run_args
8240
+ image: languageSandbox.docker.image ?? getLanguageProfile(language).defaultDockerImage,
8241
+ workdir: languageSandbox.docker.workdir,
8242
+ pull: languageSandbox.docker.pull,
8243
+ dockerBin: languageSandbox.docker.docker_bin,
8244
+ extraRunArgs: languageSandbox.docker.extra_run_args,
8245
+ sandboxDir: languageSandbox.docker.sandbox_dir
7762
8246
  });
7763
8247
  }
7764
8248
  if (kind === "firejail") {
7765
8249
  throw new Error(t().system.firejailUnsupported);
7766
8250
  }
7767
- return new SubprocessSandbox({ ws, limits: cfg.agent.sandbox_limits, audit, language });
8251
+ return new SubprocessSandbox({
8252
+ ws,
8253
+ limits: languageSandbox.local.limits,
8254
+ audit,
8255
+ language,
8256
+ sandboxDir: languageSandbox.local.sandbox_dir,
8257
+ pythonBin: languageSandbox.local.python_bin,
8258
+ inheritEnv: languageSandbox.local.inherit_env
8259
+ });
8260
+ }
8261
+ function legacyLanguageSandbox(cfg, language) {
8262
+ const legacyAgent = cfg.agent;
8263
+ const limits = legacyAgent.sandbox_limits ?? {
8264
+ cpu: 1,
8265
+ memory_mb: 1024,
8266
+ wall_seconds: 60,
8267
+ network: "download-only",
8268
+ expose_ports: []
8269
+ };
8270
+ const useLegacyDocker = legacyAgent.language === language ? legacyAgent.sandbox_docker : void 0;
8271
+ return {
8272
+ mode: legacyAgent.sandbox ?? "subprocess",
8273
+ local: {
8274
+ sandbox_dir: `.sandbox/${language}`,
8275
+ limits
8276
+ },
8277
+ docker: {
8278
+ image: useLegacyDocker?.image ?? getLanguageProfile(language).defaultDockerImage,
8279
+ workdir: useLegacyDocker?.workdir ?? "/workspace",
8280
+ pull: useLegacyDocker?.pull ?? false,
8281
+ docker_bin: useLegacyDocker?.docker_bin ?? "docker",
8282
+ extra_run_args: useLegacyDocker?.extra_run_args ?? [],
8283
+ sandbox_dir: useLegacyDocker?.sandbox_dir ?? `.sandbox/${language}`,
8284
+ limits: useLegacyDocker?.limits ?? limits
8285
+ }
8286
+ };
7768
8287
  }
7769
8288
 
7770
8289
  // src/core/engine.ts
7771
- import path26 from "path";
8290
+ import path27 from "path";
7772
8291
  import chalk2 from "chalk";
7773
8292
 
7774
8293
  // src/util/spinner.ts
@@ -7854,10 +8373,10 @@ init_types();
7854
8373
  // src/tools/fs.ts
7855
8374
  init_types();
7856
8375
  import path18 from "path";
7857
- import { promises as fs16 } from "fs";
8376
+ import { promises as fs17 } from "fs";
7858
8377
 
7859
8378
  // src/tools/path_guard.ts
7860
- import { promises as fs15 } from "fs";
8379
+ import { promises as fs16 } from "fs";
7861
8380
  import path17 from "path";
7862
8381
  async function resolveWorkspacePath(ws, rawPath, operation, opts = {}) {
7863
8382
  const raw = rawPath && rawPath.trim() ? rawPath : ".";
@@ -7866,19 +8385,19 @@ async function resolveWorkspacePath(ws, rawPath, operation, opts = {}) {
7866
8385
  if (!isInside(root, abs)) return deny(operation, raw);
7867
8386
  if (opts.mustExist) {
7868
8387
  try {
7869
- const real = await fs15.realpath(abs);
8388
+ const real = await fs16.realpath(abs);
7870
8389
  if (!isInside(root, real)) return deny(operation, raw);
7871
8390
  } catch (err) {
7872
8391
  return { ok: false, error: `${operation} failed: ${err.message}` };
7873
8392
  }
7874
8393
  } else if (opts.forWrite) {
7875
- const existingTarget = await fs15.realpath(abs).catch(() => void 0);
8394
+ const existingTarget = await fs16.realpath(abs).catch(() => void 0);
7876
8395
  if (existingTarget && !isInside(root, existingTarget)) return deny(operation, raw);
7877
8396
  const parent = await nearestExistingParent(path17.dirname(abs));
7878
- const realParent = await fs15.realpath(parent).catch(() => parent);
8397
+ const realParent = await fs16.realpath(parent).catch(() => parent);
7879
8398
  if (!isInside(root, realParent)) return deny(operation, raw);
7880
8399
  } else {
7881
- const existing = await fs15.realpath(abs).catch(() => void 0);
8400
+ const existing = await fs16.realpath(abs).catch(() => void 0);
7882
8401
  if (existing && !isInside(root, existing)) return deny(operation, raw);
7883
8402
  }
7884
8403
  return { ok: true, abs };
@@ -7888,13 +8407,13 @@ function isInside(root, candidate) {
7888
8407
  return rel === "" || !!rel && !rel.startsWith("..") && !path17.isAbsolute(rel);
7889
8408
  }
7890
8409
  async function realpathOrResolve(p) {
7891
- return fs15.realpath(p).catch(() => path17.resolve(p));
8410
+ return fs16.realpath(p).catch(() => path17.resolve(p));
7892
8411
  }
7893
8412
  async function nearestExistingParent(start) {
7894
8413
  let current2 = path17.resolve(start);
7895
8414
  while (true) {
7896
8415
  try {
7897
- const stat = await fs15.stat(current2);
8416
+ const stat = await fs16.stat(current2);
7898
8417
  if (stat.isDirectory()) return current2;
7899
8418
  } catch {
7900
8419
  }
@@ -7920,9 +8439,9 @@ var readFileTool = {
7920
8439
  const resolved = await resolveWorkspacePath(ctx.ws, args.path, "read_file", { mustExist: true });
7921
8440
  if (!resolved.ok) return { ok: false, error: resolved.error };
7922
8441
  const abs = resolved.abs;
7923
- const stat = await fs16.stat(abs);
8442
+ const stat = await fs17.stat(abs);
7924
8443
  if (!stat.isFile()) return { ok: false, error: "not a file" };
7925
- const buf = await fs16.readFile(abs);
8444
+ const buf = await fs17.readFile(abs);
7926
8445
  const limit = args.maxBytes ?? 2e5;
7927
8446
  const content = buf.byteLength > limit ? buf.subarray(0, limit).toString("utf8") + `
7928
8447
  ... [truncated ${buf.byteLength - limit} bytes]` : buf.toString("utf8");
@@ -7983,8 +8502,8 @@ var writeFileTool = {
7983
8502
  }
7984
8503
  try {
7985
8504
  const abs = resolved.abs;
7986
- await fs16.mkdir(path18.dirname(abs), { recursive: true });
7987
- await fs16.writeFile(abs, args.content, "utf8");
8505
+ await fs17.mkdir(path18.dirname(abs), { recursive: true });
8506
+ await fs17.writeFile(abs, args.content, "utf8");
7988
8507
  return {
7989
8508
  ok: true,
7990
8509
  data: { bytes: size },
@@ -8020,11 +8539,11 @@ var appendFileTool = {
8020
8539
  }
8021
8540
  try {
8022
8541
  const abs = resolved.abs;
8023
- await fs16.mkdir(path18.dirname(abs), { recursive: true });
8024
- await fs16.appendFile(abs, args.content, "utf8");
8542
+ await fs17.mkdir(path18.dirname(abs), { recursive: true });
8543
+ await fs17.appendFile(abs, args.content, "utf8");
8025
8544
  let total = size;
8026
8545
  try {
8027
- total = (await fs16.stat(abs)).size;
8546
+ total = (await fs17.stat(abs)).size;
8028
8547
  } catch {
8029
8548
  }
8030
8549
  return {
@@ -8057,7 +8576,7 @@ var listDirTool = {
8057
8576
  const resolved = await resolveWorkspacePath(ctx.ws, args.path ?? ".", "list_dir", { mustExist: true });
8058
8577
  if (!resolved.ok) return { ok: false, error: resolved.error };
8059
8578
  const abs = resolved.abs;
8060
- const entries = await fs16.readdir(abs, { withFileTypes: true });
8579
+ const entries = await fs17.readdir(abs, { withFileTypes: true });
8061
8580
  return {
8062
8581
  ok: true,
8063
8582
  data: { entries: entries.map((e) => e.isDirectory() ? e.name + "/" : e.name) },
@@ -8072,7 +8591,7 @@ var listDirTool = {
8072
8591
  // src/tools/patch.ts
8073
8592
  init_types();
8074
8593
  import path19 from "path";
8075
- import { promises as fs17 } from "fs";
8594
+ import { promises as fs18 } from "fs";
8076
8595
  var applyPatchTool = {
8077
8596
  name: "apply_patch",
8078
8597
  description: "\u5E94\u7528 unified diff \u8865\u4E01\uFF1B\u76EE\u6807\u6587\u4EF6\u5FC5\u987B\u5728\u5F53\u524D Step writable allowlist \u5185\u3002",
@@ -8090,14 +8609,14 @@ var applyPatchTool = {
8090
8609
  const abs = resolved.abs;
8091
8610
  let original = "";
8092
8611
  try {
8093
- original = await fs17.readFile(abs, "utf8");
8612
+ original = await fs18.readFile(abs, "utf8");
8094
8613
  } catch {
8095
8614
  if (!fd.isNewFile) return { ok: false, error: `target file missing: ${fd.target}` };
8096
8615
  }
8097
8616
  const next = applyHunks(original, fd.hunks);
8098
8617
  if (next.error) return { ok: false, error: `${fd.target}: ${next.error}` };
8099
- await fs17.mkdir(path19.dirname(abs), { recursive: true });
8100
- await fs17.writeFile(abs, next.content, "utf8");
8618
+ await fs18.mkdir(path19.dirname(abs), { recursive: true });
8619
+ await fs18.writeFile(abs, next.content, "utf8");
8101
8620
  changed.push(fd.target);
8102
8621
  }
8103
8622
  return { ok: true, data: { changedFiles: changed }, summary: `patched ${changed.join(", ")}` };
@@ -8313,7 +8832,7 @@ var pipInstallTool = {
8313
8832
  // src/tools/edit.ts
8314
8833
  init_types();
8315
8834
  import path20 from "path";
8316
- import { promises as fs18 } from "fs";
8835
+ import { promises as fs19 } from "fs";
8317
8836
  var replaceInFileTool = {
8318
8837
  name: "replace_in_file",
8319
8838
  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",
@@ -8334,7 +8853,7 @@ var replaceInFileTool = {
8334
8853
  const abs = resolved.abs;
8335
8854
  let original;
8336
8855
  try {
8337
- original = await fs18.readFile(abs, "utf8");
8856
+ original = await fs19.readFile(abs, "utf8");
8338
8857
  } catch {
8339
8858
  return { ok: false, error: `file not found: ${args.path}` };
8340
8859
  }
@@ -8366,8 +8885,8 @@ var replaceInFileTool = {
8366
8885
  return { ok: false, error: hint.join("\n") };
8367
8886
  }
8368
8887
  const next = parts.join(args.replace);
8369
- await fs18.mkdir(path20.dirname(abs), { recursive: true });
8370
- await fs18.writeFile(abs, next, "utf8");
8888
+ await fs19.mkdir(path20.dirname(abs), { recursive: true });
8889
+ await fs19.writeFile(abs, next, "utf8");
8371
8890
  return {
8372
8891
  ok: true,
8373
8892
  data: { occurrences },
@@ -8384,7 +8903,7 @@ var codeSearchTool = {
8384
8903
  const resolved = await resolveWorkspacePath(ctx.ws, args.root ?? ".", "code_search", { mustExist: true });
8385
8904
  if (!resolved.ok) return { ok: false, error: resolved.error };
8386
8905
  const root = resolved.abs;
8387
- const workspaceRoot = await fs18.realpath(ctx.ws.root).catch(() => ctx.ws.root);
8906
+ const workspaceRoot = await fs19.realpath(ctx.ws.root).catch(() => ctx.ws.root);
8388
8907
  const max = args.maxResults ?? 50;
8389
8908
  const exts = args.ext && args.ext.length > 0 ? new Set(args.ext.map((e) => e.startsWith(".") ? e : "." + e)) : null;
8390
8909
  const matches = [];
@@ -8393,9 +8912,9 @@ var codeSearchTool = {
8393
8912
  if (/(?:^|\/)(node_modules|\.git|\.sandbox|\.xcompiler|dist|__pycache__)\//.test(abs)) return;
8394
8913
  let content;
8395
8914
  try {
8396
- const stat = await fs18.stat(abs);
8915
+ const stat = await fs19.stat(abs);
8397
8916
  if (stat.size > 512e3) return;
8398
- content = await fs18.readFile(abs, "utf8");
8917
+ content = await fs19.readFile(abs, "utf8");
8399
8918
  } catch {
8400
8919
  return;
8401
8920
  }
@@ -8418,7 +8937,7 @@ var codeSearchTool = {
8418
8937
  async function walk2(dir, onFile) {
8419
8938
  let entries;
8420
8939
  try {
8421
- entries = await fs18.readdir(dir, { withFileTypes: true });
8940
+ entries = await fs19.readdir(dir, { withFileTypes: true });
8422
8941
  } catch {
8423
8942
  return;
8424
8943
  }
@@ -8472,7 +8991,7 @@ var analyzeErrorTool = {
8472
8991
  };
8473
8992
 
8474
8993
  // src/tools/deps.ts
8475
- import { promises as fs19 } from "fs";
8994
+ import { promises as fs20 } from "fs";
8476
8995
  var addDependencyTool = {
8477
8996
  name: "add_dependency",
8478
8997
  description: "\u5411\u4F9D\u8D56\u6E05\u5355\u8FFD\u52A0\u4F9D\u8D56\uFF08python: requirements.txt\uFF1Btypescript: package.json\uFF09\u5E76\u91CD\u5EFA\u6C99\u76D2\u3002",
@@ -8492,7 +9011,7 @@ var addDependencyTool = {
8492
9011
  const added = [];
8493
9012
  let final;
8494
9013
  if (ctx.language === "typescript") {
8495
- const pkg = await fs19.readFile(abs, "utf8").then((text) => JSON.parse(text)).catch(() => ({}));
9014
+ const pkg = await fs20.readFile(abs, "utf8").then((text) => JSON.parse(text)).catch(() => ({}));
8496
9015
  const existingDeps = pkg.dependencies && typeof pkg.dependencies === "object" && !Array.isArray(pkg.dependencies) ? { ...pkg.dependencies } : {};
8497
9016
  const before = new Set(Object.keys(existingDeps));
8498
9017
  for (const name of normalized) {
@@ -8501,11 +9020,11 @@ var addDependencyTool = {
8501
9020
  }
8502
9021
  final = Object.keys(existingDeps).sort();
8503
9022
  pkg.dependencies = Object.fromEntries(final.map((name) => [name, existingDeps[name] ?? "*"]));
8504
- await fs19.writeFile(abs, JSON.stringify(pkg, null, 2) + "\n", "utf8");
9023
+ await fs20.writeFile(abs, JSON.stringify(pkg, null, 2) + "\n", "utf8");
8505
9024
  } else {
8506
9025
  let existing = "";
8507
9026
  try {
8508
- existing = await fs19.readFile(abs, "utf8");
9027
+ existing = await fs20.readFile(abs, "utf8");
8509
9028
  } catch {
8510
9029
  }
8511
9030
  const set = /* @__PURE__ */ new Set();
@@ -8519,7 +9038,7 @@ var addDependencyTool = {
8519
9038
  set.add(p);
8520
9039
  }
8521
9040
  final = [...set].sort();
8522
- await fs19.writeFile(abs, final.join("\n") + "\n", "utf8");
9041
+ await fs20.writeFile(abs, final.join("\n") + "\n", "utf8");
8523
9042
  }
8524
9043
  try {
8525
9044
  await ctx.sandbox.build(manifestPath);
@@ -8538,7 +9057,7 @@ var addDependencyTool = {
8538
9057
  };
8539
9058
 
8540
9059
  // src/tools/net.ts
8541
- import { promises as fs20 } from "fs";
9060
+ import { promises as fs21 } from "fs";
8542
9061
  import path21 from "path";
8543
9062
  var DEFAULT_TIMEOUT_MS = 15e3;
8544
9063
  var DEFAULT_MAX_BYTES = 256 * 1024;
@@ -8600,8 +9119,8 @@ var httpFetchTool = {
8600
9119
  if (timer) clearTimeout(timer);
8601
9120
  const trimmed = buf.length > maxBytes ? buf.subarray(0, maxBytes) : buf;
8602
9121
  const abs = saveAsAbs;
8603
- await fs20.mkdir(path21.dirname(abs), { recursive: true });
8604
- await fs20.writeFile(abs, trimmed);
9122
+ await fs21.mkdir(path21.dirname(abs), { recursive: true });
9123
+ await fs21.writeFile(abs, trimmed);
8605
9124
  await ctx.audit?.event("tool.call", t().audit.httpFetchSaved(method, args.url, args.saveAs, trimmed.length), {
8606
9125
  messageId: "audit.http_fetch_saved",
8607
9126
  stepId: ctx.stepId,
@@ -8659,7 +9178,7 @@ var httpFetchTool = {
8659
9178
  init_types();
8660
9179
 
8661
9180
  // src/tools/guard.ts
8662
- import { promises as fs21 } from "fs";
9181
+ import { promises as fs22 } from "fs";
8663
9182
  import path22 from "path";
8664
9183
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "append_file", "apply_patch", "replace_in_file", "add_dependency"]);
8665
9184
  var DEFAULT_EDIT_LINES_PER_STEP = 400;
@@ -8742,8 +9261,8 @@ var EditGuard = class {
8742
9261
  async record(partial) {
8743
9262
  const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), stepId: this.opts.stepId, ...partial };
8744
9263
  try {
8745
- await fs21.mkdir(path22.dirname(this.logAbs), { recursive: true });
8746
- await fs21.appendFile(this.logAbs, JSON.stringify(rec) + "\n", "utf8");
9264
+ await fs22.mkdir(path22.dirname(this.logAbs), { recursive: true });
9265
+ await fs22.appendFile(this.logAbs, JSON.stringify(rec) + "\n", "utf8");
8747
9266
  } catch {
8748
9267
  }
8749
9268
  }
@@ -8799,8 +9318,9 @@ function buildDefaultRegistry() {
8799
9318
 
8800
9319
  // src/agents/executor.ts
8801
9320
  import path23 from "path";
8802
- import { promises as fs22 } from "fs";
9321
+ import { promises as fs23 } from "fs";
8803
9322
  import { jsonrepair } from "jsonrepair";
9323
+ var MISSING_OUTPUT_STALL_ROUND_LIMIT = 3;
8804
9324
  var StepExecutor = class {
8805
9325
  constructor(opts) {
8806
9326
  this.opts = opts;
@@ -8824,8 +9344,12 @@ var StepExecutor = class {
8824
9344
  ];
8825
9345
  const calls = [];
8826
9346
  let finalThought;
8827
- const initialMissing = (await verifyOutputs(inp)).missing.length;
9347
+ let issueResolutionPlan;
9348
+ const initialVerify = await verifyOutputs(inp);
9349
+ const initialMissing = initialVerify.missing.length;
8828
9350
  const hardRoundLimit = Math.max(maxRounds, maxRounds + Math.min(12, Math.max(4, initialMissing * 2)));
9351
+ let lastMissingCount = initialMissing;
9352
+ let missingOutputStallRounds = 0;
8829
9353
  let parseFailures = 0;
8830
9354
  let repeatedTurns = 0;
8831
9355
  let lastActionsKey = null;
@@ -8874,11 +9398,12 @@ var StepExecutor = class {
8874
9398
  } catch (err) {
8875
9399
  rep.done("failed");
8876
9400
  const errMsg = err.message;
9401
+ actualRounds = round;
8877
9402
  const dumpRel = `.xcompiler/llm-stream/${inp.step.id}-${role}-r${round}.txt`;
8878
9403
  try {
8879
9404
  const dumpAbs = inp.ctx.ws.abs(dumpRel);
8880
- await fs22.mkdir(path23.dirname(dumpAbs), { recursive: true });
8881
- await fs22.writeFile(
9405
+ await fs23.mkdir(path23.dirname(dumpAbs), { recursive: true });
9406
+ await fs23.writeFile(
8882
9407
  dumpAbs,
8883
9408
  `${t().audit.partialFailureHeader(errMsg)}
8884
9409
  ${t().audit.streamLength(rawAggregate.length)}
@@ -8907,11 +9432,33 @@ ${rawAggregate}`,
8907
9432
  partialBytes: rawAggregate.length
8908
9433
  }
8909
9434
  );
9435
+ if (isLowQualityLLMResponseError(errMsg)) {
9436
+ repeatedTurns++;
9437
+ const verify2 = await verifyOutputs(inp);
9438
+ const metrics2 = computeMetrics({
9439
+ rounds: actualRounds,
9440
+ parseFailures,
9441
+ repeatedTurns,
9442
+ calls,
9443
+ initialMissing,
9444
+ currentMissing: verify2.missing.length
9445
+ });
9446
+ return {
9447
+ success: false,
9448
+ rounds: round,
9449
+ toolCalls: calls,
9450
+ finalThought,
9451
+ issueResolutionPlan,
9452
+ error: errMsg,
9453
+ metrics: metrics2
9454
+ };
9455
+ }
8910
9456
  throw err;
8911
9457
  }
8912
9458
  rep.done();
8913
9459
  const turn = parseTurn(text);
8914
9460
  finalThought = turn.thoughts;
9461
+ issueResolutionPlan = extractIssueResolutionPlan(turn) ?? issueResolutionPlan;
8915
9462
  const normalizedActions = normalizeActions(turn.actions);
8916
9463
  const actions = normalizedActions.actions;
8917
9464
  if (normalizedActions.invalid.length > 0) {
@@ -8961,6 +9508,7 @@ ${rawAggregate}`,
8961
9508
  }
8962
9509
  await inp.ctx.audit?.executorTurn(inp.step.id, role, round, {
8963
9510
  thoughts: turn.thoughts,
9511
+ issueResolutionPlan,
8964
9512
  actions,
8965
9513
  done: turn.done === true,
8966
9514
  raw: text,
@@ -9069,12 +9617,25 @@ ${rawAggregate}`,
9069
9617
  turnResults.push({ ...r, tool: a.tool });
9070
9618
  }
9071
9619
  const verify = await verifyOutputs(inp);
9620
+ const mutationSucceededThisRound = turnResults.some((r) => r.ok && isRepairEvidenceTool(r.tool));
9621
+ if (verify.missing.length < lastMissingCount) {
9622
+ lastMissingCount = verify.missing.length;
9623
+ missingOutputStallRounds = 0;
9624
+ } else if (!verify.ok && initialMissing > 0 && mutationSucceededThisRound && !readOnlyRound && unresolvedToolFailures.size === 0) {
9625
+ missingOutputStallRounds++;
9626
+ }
9072
9627
  if (turnResults.some((r) => r.tool === "run_tests" && !r.ok) && !advisoryFailureTools.has("run_tests")) {
9073
9628
  failedTestRunRounds++;
9074
9629
  }
9075
9630
  const repairGateOk = !repairRequired || repairEvidence || canAcceptOutputCompletionRecovery(inp, initialMissing);
9631
+ const issueResolutionPlanRequired = role === "Debugger" && !!inp.debugContext?.issueId;
9632
+ const issueResolutionPlanOk = !issueResolutionPlanRequired || !!issueResolutionPlan?.trim();
9076
9633
  const verifiedCompletion = !turn.done && hasSuccessfulCompletionVerification(calls);
9077
- if ((turn.done || verifiedCompletion) && verify.ok && unresolvedToolFailures.size === 0 && repairGateOk) {
9634
+ const outputCompletionRecovery = verify.ok && canAcceptOutputCompletionRecovery(inp, initialMissing);
9635
+ const supersededContractFailures = repairEvidence && hasSuccessfulCompletionVerification(calls) && hasOnlySupersededToolContractFailures(unresolvedToolFailures);
9636
+ const unresolvedFailuresOk = unresolvedToolFailures.size === 0 || outputCompletionRecovery && hasOnlyUntargetedToolContractFailures(unresolvedToolFailures) || supersededContractFailures;
9637
+ const completionSignal = turn.done || verifiedCompletion || outputCompletionRecovery;
9638
+ if (completionSignal && verify.ok && unresolvedFailuresOk && repairGateOk && issueResolutionPlanOk) {
9078
9639
  const metrics2 = computeMetrics({
9079
9640
  rounds: actualRounds,
9080
9641
  parseFailures,
@@ -9083,7 +9644,7 @@ ${rawAggregate}`,
9083
9644
  initialMissing,
9084
9645
  currentMissing: verify.missing.length
9085
9646
  });
9086
- return { success: true, rounds: round, toolCalls: calls, finalThought, metrics: metrics2 };
9647
+ return { success: true, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, metrics: metrics2 };
9087
9648
  }
9088
9649
  if (this.opts.maxFailedTestRuns && failedTestRunRounds >= this.opts.maxFailedTestRuns) {
9089
9650
  const metrics2 = computeMetrics({
@@ -9102,7 +9663,27 @@ ${rawAggregate}`,
9102
9663
  failedTestRunRounds,
9103
9664
  maxFailedTestRuns: this.opts.maxFailedTestRuns
9104
9665
  });
9105
- return { success: false, rounds: round, toolCalls: calls, finalThought, error, metrics: metrics2 };
9666
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
9667
+ }
9668
+ if (missingOutputStallRounds >= MISSING_OUTPUT_STALL_ROUND_LIMIT) {
9669
+ repeatedTurns++;
9670
+ const metrics2 = computeMetrics({
9671
+ rounds: actualRounds,
9672
+ parseFailures,
9673
+ repeatedTurns,
9674
+ calls,
9675
+ initialMissing,
9676
+ currentMissing: verify.missing.length
9677
+ });
9678
+ 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.`;
9679
+ await inp.ctx.audit?.event("note", error, {
9680
+ messageId: "audit.executor_missing_output_stall",
9681
+ stepId: inp.step.id,
9682
+ round,
9683
+ missingOutputStallRounds,
9684
+ missingOutputs: verify.missing
9685
+ });
9686
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
9106
9687
  }
9107
9688
  const readOnlyRecoveryViolation = readOnlyRecoveryMode && readOnlyRecoveryRounds >= 2;
9108
9689
  const readOnlyRoundLimit = directRepairMode ? 2 : 3;
@@ -9125,7 +9706,7 @@ ${rawAggregate}`,
9125
9706
  consecutiveReadOnlyRounds,
9126
9707
  actions
9127
9708
  });
9128
- return { success: false, rounds: round, toolCalls: calls, finalThought, error, metrics: metrics2 };
9709
+ return { success: false, rounds: round, toolCalls: calls, finalThought, issueResolutionPlan, error, metrics: metrics2 };
9129
9710
  }
9130
9711
  if (round >= roundLimit && roundLimit < hardRoundLimit && shouldExtendProductiveRun({
9131
9712
  parseFailures,
@@ -9159,8 +9740,13 @@ ${rawAggregate}`,
9159
9740
  rounds: consecutiveReadOnlyRounds,
9160
9741
  targets: actions.flatMap((action) => actionTargetPaths(action.tool, action.args)).join(", ")
9161
9742
  } : void 0,
9743
+ missingOutputStallWarning: missingOutputStallRounds >= MISSING_OUTPUT_STALL_ROUND_LIMIT - 1 && !verify.ok ? {
9744
+ rounds: missingOutputStallRounds,
9745
+ missing: verify.missing.join(", ")
9746
+ } : void 0,
9162
9747
  readOnlyRecoveryWarning: (readOnlyRecoveryMode || directRepairMode) && readOnlyRound,
9163
- repairEvidenceMissing: repairRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !repairEvidence
9748
+ repairEvidenceMissing: repairRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !repairEvidence,
9749
+ issueResolutionPlanMissing: issueResolutionPlanRequired && turn.done === true && verify.ok && unresolvedToolFailures.size === 0 && !issueResolutionPlanOk
9164
9750
  })
9165
9751
  });
9166
9752
  }
@@ -9178,7 +9764,8 @@ ${rawAggregate}`,
9178
9764
  rounds: actualRounds || roundLimit,
9179
9765
  toolCalls: calls,
9180
9766
  finalThought,
9181
- 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",
9767
+ issueResolutionPlan,
9768
+ 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",
9182
9769
  metrics
9183
9770
  };
9184
9771
  }
@@ -9191,6 +9778,9 @@ function isReadOnlyOrProbeAction(action) {
9191
9778
  function isReadOnlyLoopFailure(reason) {
9192
9779
  return /repeated read-only\/probe actions without progress/i.test(reason) || /read-only recovery mode repeated probe actions/i.test(reason);
9193
9780
  }
9781
+ function isLowQualityLLMResponseError(message) {
9782
+ return /low-quality (?:debugger )?response/i.test(message) || /read-only\/probe actions in read-only recovery mode/i.test(message);
9783
+ }
9194
9784
  function hasActionableDebuggerFailure(debugContext) {
9195
9785
  if (!debugContext) return false;
9196
9786
  const text = [
@@ -9206,6 +9796,18 @@ function canAcceptOutputCompletionRecovery(inp, initialMissing) {
9206
9796
  if (initialMissing !== 0) return false;
9207
9797
  return isOutputCompletionFailure(inp.debugContext.reason, inp.debugContext.failureLog);
9208
9798
  }
9799
+ function hasOnlyUntargetedToolContractFailures(unresolved) {
9800
+ if (unresolved.size === 0) return true;
9801
+ return [...unresolved.entries()].every(
9802
+ ([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)
9803
+ );
9804
+ }
9805
+ function hasOnlySupersededToolContractFailures(unresolved) {
9806
+ if (unresolved.size === 0) return true;
9807
+ return [...unresolved.values()].every(
9808
+ (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)
9809
+ );
9810
+ }
9209
9811
  function isOutputCompletionFailure(reason = "", failureLog = "") {
9210
9812
  const text = `${reason}
9211
9813
  ${failureLog}`;
@@ -9280,6 +9882,7 @@ function compactTurnForHistory(turn) {
9280
9882
  const normalized = normalizeActions(turn.actions);
9281
9883
  return JSON.stringify({
9282
9884
  thoughts: truncate2(turn.thoughts ?? "", 500),
9885
+ issueResolutionPlan: truncate2(extractIssueResolutionPlan(turn) ?? "", 900),
9283
9886
  actions: normalized.actions.map((action) => ({
9284
9887
  tool: action.tool,
9285
9888
  args: compactActionArgs(action.tool, action.args)
@@ -9618,8 +10221,8 @@ async function verifyOutputs(inp) {
9618
10221
  const missing = [];
9619
10222
  for (const out of inp.step.outputs) {
9620
10223
  if (out.endsWith("/")) continue;
9621
- const exists = await inp.ctx.ws.exists(out);
9622
- if (!exists) missing.push(out);
10224
+ const exists2 = await inp.ctx.ws.exists(out);
10225
+ if (!exists2) missing.push(out);
9623
10226
  }
9624
10227
  return { ok: missing.length === 0, missing };
9625
10228
  }
@@ -9635,13 +10238,21 @@ function renderUserPrompt(inp, toolDocs) {
9635
10238
  ${truncate2(s.content, s.path === ".xcompiler/architecture-contract.json" ? architectureLimit : snippetLimit)}
9636
10239
  \`\`\``
9637
10240
  ).join("\n\n");
9638
- const dbg = inp.debugContext ? `## debug failure log
10241
+ const dbg = inp.debugContext ? [
10242
+ inp.debugContext.issueId ? `## issue
10243
+ id: ${inp.debugContext.issueId}
10244
+ ` : "",
10245
+ inp.debugContext.debugBrief ? `${inp.debugContext.debugBrief}
10246
+ ` : "",
10247
+ `## compact failure evidence
9639
10248
  \`\`\`
9640
10249
  ${truncate2(inp.debugContext.failureLog, failureLogLimit)}
9641
10250
  \`\`\`
9642
- ` + (inp.debugContext.suggestions ? `
10251
+ `,
10252
+ inp.debugContext.suggestions ? `
9643
10253
  ${inp.debugContext.suggestions}
9644
- ` : "") : "";
10254
+ ` : ""
10255
+ ].join("\n") : "";
9645
10256
  return [
9646
10257
  `# Step ${inp.step.id} \u2014 ${inp.step.title}`,
9647
10258
  `phase: ${inp.step.phase}`,
@@ -9729,6 +10340,11 @@ function renderFeedback(results, verify, turn) {
9729
10340
  )
9730
10341
  );
9731
10342
  }
10343
+ if (turn.missingOutputStallWarning) {
10344
+ lines.push(
10345
+ `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.`
10346
+ );
10347
+ }
9732
10348
  if (turn.readOnlyRecoveryWarning) {
9733
10349
  lines.push(M.executorFeedbackReadOnlyRecoveryRequired);
9734
10350
  if (!verify.ok && verify.missing.length > 0) {
@@ -9737,6 +10353,9 @@ function renderFeedback(results, verify, turn) {
9737
10353
  );
9738
10354
  }
9739
10355
  }
10356
+ if (turn.issueResolutionPlanMissing) {
10357
+ lines.push(M.executorFeedbackIssueResolutionPlanMissing);
10358
+ }
9740
10359
  if (turn.unresolvedFailures && turn.unresolvedFailures.length > 0) {
9741
10360
  lines.push(
9742
10361
  `Unresolved tool failures remain: ${turn.unresolvedFailures.map((failure) => truncate2(failure, 1200)).join("; ")}`
@@ -9947,6 +10566,21 @@ function isTurnObject(value) {
9947
10566
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
9948
10567
  return value;
9949
10568
  }
10569
+ function extractIssueResolutionPlan(turn) {
10570
+ const candidates = [
10571
+ turn.issueResolutionPlan,
10572
+ turn.issue_resolution_plan,
10573
+ turn.resolutionPlan,
10574
+ turn.handlingPlan,
10575
+ turn.fixPlan
10576
+ ];
10577
+ for (const value of candidates) {
10578
+ if (typeof value === "string" && value.trim().length > 0) {
10579
+ return truncate2(value.trim(), 2400);
10580
+ }
10581
+ }
10582
+ return void 0;
10583
+ }
9950
10584
  function normalizeJsonLikeStrings(text) {
9951
10585
  let out = "";
9952
10586
  let inStr = false;
@@ -10084,8 +10718,301 @@ function buildDefaultSkills() {
10084
10718
  }
10085
10719
 
10086
10720
  // src/core/debug_cache.ts
10087
- import { promises as fs23 } from "fs";
10721
+ import { promises as fs24 } from "fs";
10088
10722
  import path24 from "path";
10723
+
10724
+ // src/core/debug_brief.ts
10725
+ var MAX_EVIDENCE = 8;
10726
+ var MAX_EVIDENCE_LINE = 260;
10727
+ var MAX_TOOL_FAILURES = 6;
10728
+ var MAX_FAILED_TESTS = 8;
10729
+ var MAX_FILES = 10;
10730
+ function buildDebugBrief(input2) {
10731
+ const reason = oneLine(input2.reason ?? "");
10732
+ const raw = `${reason}
10733
+ ${input2.failureLog ?? ""}`.trim();
10734
+ const sections = splitFailureSections(raw);
10735
+ const rootSignals = extractSignals(sections.root || raw);
10736
+ const latestSignals = sections.latest ? extractSignals(sections.latest) : void 0;
10737
+ const chosen = choosePrimarySignals(rootSignals, latestSignals);
10738
+ const category = chosen.category;
10739
+ const primaryError = chosen.primaryError || reason || "Unknown failure";
10740
+ const failedTests = dedup3([...rootSignals.failedTests ?? [], ...latestSignals?.failedTests ?? []]).slice(0, MAX_FAILED_TESTS);
10741
+ const files = dedup3([...rootSignals.files ?? [], ...latestSignals?.files ?? []]).slice(0, MAX_FILES);
10742
+ const toolFailures = dedup3([...rootSignals.toolFailures ?? [], ...latestSignals?.toolFailures ?? []]).slice(0, MAX_TOOL_FAILURES);
10743
+ const statusCodes = dedup3([...rootSignals.statusCodes ?? [], ...latestSignals?.statusCodes ?? []]).slice(0, 6);
10744
+ const evidence = selectEvidenceLines(raw, category, primaryError, failedTests, files, toolFailures);
10745
+ return {
10746
+ version: 1,
10747
+ category,
10748
+ summary: buildSummary({ category, reason, primaryError, failedTests, files, phase: input2.phase, targetPhase: input2.targetPhase }),
10749
+ primaryError,
10750
+ debugDemand: buildDebugDemand(category, input2.targetPhase ?? input2.phase, statusCodes),
10751
+ failedTests,
10752
+ files,
10753
+ toolFailures,
10754
+ statusCodes,
10755
+ evidence: evidence.lines,
10756
+ omittedEvidenceLines: evidence.omitted
10757
+ };
10758
+ }
10759
+ function renderDebugBriefForPrompt(brief) {
10760
+ const lines = [
10761
+ "## debug brief",
10762
+ `- category: ${brief.category}`,
10763
+ `- summary: ${brief.summary}`,
10764
+ `- primaryError: ${brief.primaryError}`,
10765
+ `- debugDemand: ${brief.debugDemand}`
10766
+ ];
10767
+ if (brief.failedTests.length > 0) lines.push(`- failedTests: ${brief.failedTests.join(", ")}`);
10768
+ if (brief.files.length > 0) lines.push(`- likelyFiles: ${brief.files.join(", ")}`);
10769
+ if (brief.toolFailures.length > 0) lines.push(`- toolFailures: ${brief.toolFailures.join(" | ")}`);
10770
+ if (brief.statusCodes.length > 0) lines.push(`- httpStatus: ${brief.statusCodes.join(", ")}`);
10771
+ if (brief.evidence.length > 0) {
10772
+ lines.push("- keyEvidence:");
10773
+ for (const line of brief.evidence) lines.push(` - ${line}`);
10774
+ }
10775
+ if (brief.omittedEvidenceLines > 0) {
10776
+ lines.push(`- omittedEvidenceLines: ${brief.omittedEvidenceLines}`);
10777
+ }
10778
+ return lines.join("\n");
10779
+ }
10780
+ function compactFailureEvidence(input2) {
10781
+ const maxChars = input2.maxChars ?? 2400;
10782
+ const maxLines = input2.maxLines ?? 50;
10783
+ const reason = shouldSuppressReasonInEvidence(input2.reason, input2.failureLog) ? "" : input2.reason ?? "";
10784
+ const raw = `${reason}
10785
+ ${input2.failureLog ?? ""}`.trim();
10786
+ if (!raw) return "";
10787
+ const brief = buildDebugBrief({ ...input2, reason });
10788
+ const important = selectEvidenceLines(
10789
+ raw,
10790
+ brief.category,
10791
+ brief.primaryError,
10792
+ brief.failedTests,
10793
+ brief.files,
10794
+ brief.toolFailures,
10795
+ Math.min(MAX_EVIDENCE + 4, 12)
10796
+ ).lines;
10797
+ 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));
10798
+ const lines = dedup3([...important, ...tail]).slice(-maxLines);
10799
+ const joined = lines.join("\n");
10800
+ if (joined.length <= maxChars) return joined;
10801
+ const head = joined.slice(0, Math.floor(maxChars * 0.45));
10802
+ const tailText2 = joined.slice(-Math.floor(maxChars * 0.45));
10803
+ return `${head}
10804
+ ... [debug evidence truncated ${joined.length - head.length - tailText2.length} chars]
10805
+ ${tailText2}`;
10806
+ }
10807
+ function splitFailureSections(text) {
10808
+ const marker = /\n##\s+latest Debugger attempt failure\b/u.exec(text);
10809
+ if (!marker) return { root: text };
10810
+ return {
10811
+ root: text.slice(0, marker.index).trim(),
10812
+ latest: text.slice(marker.index + 1).trim()
10813
+ };
10814
+ }
10815
+ function choosePrimarySignals(root, latest) {
10816
+ if (!latest) return root;
10817
+ if (isProcessNoise(latest.category) && !isProcessNoise(root.category)) return root;
10818
+ if (latest.category !== "unknown") return latest;
10819
+ return root.category !== "unknown" ? root : latest;
10820
+ }
10821
+ function extractSignals(text) {
10822
+ const lines = normalizedLines(text);
10823
+ const failedTests = extractFailedTests(text);
10824
+ const files = extractFiles(text);
10825
+ const toolFailures = extractToolFailures(lines);
10826
+ const statusCodes = extractStatusCodes(text);
10827
+ const category = classify(text, lines, failedTests, toolFailures);
10828
+ return {
10829
+ category,
10830
+ primaryError: findPrimaryError(text, lines, category, failedTests, toolFailures),
10831
+ failedTests,
10832
+ files,
10833
+ toolFailures,
10834
+ statusCodes
10835
+ };
10836
+ }
10837
+ function classify(text, lines, failedTests, toolFailures) {
10838
+ const lower = text.toLowerCase();
10839
+ 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)) {
10840
+ return "llm_provider";
10841
+ }
10842
+ if (/repeated read-only\/probe actions|read-only recovery mode|low-quality debugger response/u.test(lower)) {
10843
+ return "tool_loop";
10844
+ }
10845
+ if (/permission denied/u.test(lower)) return "permission_denied";
10846
+ 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)) {
10847
+ if (!/openai|ollama|openrouter|llm provider/u.test(lower)) return "network_api_failure";
10848
+ }
10849
+ if (/modulenotfounderror|importerror/u.test(lower)) return "import_error";
10850
+ if (/could not find a version|no matching distribution|pip install|add_dependency/u.test(lower)) return "dependency_error";
10851
+ if (/syntaxerror|indentationerror|taberror/u.test(lower)) return "syntax_error";
10852
+ if (/outputs? (?:still )?missing|missing required outputs?|outputs? \S*缺失|仍缺失/u.test(lower)) return "missing_output";
10853
+ 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)) {
10854
+ return "test_failure";
10855
+ }
10856
+ if (toolFailures.length > 0) return "exception";
10857
+ if (lines.some((line) => /error|exception|traceback|failed/i.test(line))) return "exception";
10858
+ return "unknown";
10859
+ }
10860
+ function findPrimaryError(text, lines, category, failedTests, toolFailures) {
10861
+ if (failedTests.length > 0 && category === "test_failure") return `failed test: ${failedTests[0]}`;
10862
+ if (toolFailures.length > 0 && (category === "tool_loop" || category === "exception")) return toolFailures[0];
10863
+ const patterns = [
10864
+ /(?:SyntaxError|IndentationError|TabError|ModuleNotFoundError|ImportError|AssertionError|TypeError|ValueError|FileNotFoundError|AttributeError|RuntimeError):[^\n]+/u,
10865
+ /\bFAILED\s+[^\n]+/u,
10866
+ /\b(?:pytest|vitest)[^\n]*(?:exit|failed|FAIL)[^\n]*/iu,
10867
+ /\bHTTP\s+(?:401|403|404|408|409|410|422|429|5\d\d)[^\n]*/iu,
10868
+ /Network API failure detected[^\n]*/iu,
10869
+ /outputs?[^\n]*(?:missing|缺失|仍缺失)[^\n]*/iu,
10870
+ /repeated read-only\/probe actions[^\n]*/iu
10871
+ ];
10872
+ for (const pattern of patterns) {
10873
+ const match = text.match(pattern)?.[0];
10874
+ if (match) return oneLine(match);
10875
+ }
10876
+ const lastMeaningful = [...lines].reverse().find((line) => /error|exception|failed|exit=\s*[1-9]|缺失/i.test(line));
10877
+ return oneLine(lastMeaningful ?? lines.at(-1) ?? "Unknown failure");
10878
+ }
10879
+ function buildSummary(args) {
10880
+ const scope = args.targetPhase || args.phase ? ` in ${args.targetPhase ?? args.phase}` : "";
10881
+ if (args.failedTests.length > 0) return `${args.category}${scope}: ${args.failedTests[0]} failed`;
10882
+ if (args.files.length > 0) return `${args.category}${scope}: ${args.primaryError} (${args.files[0]})`;
10883
+ return `${args.category}${scope}: ${args.primaryError || args.reason || "failure"}`;
10884
+ }
10885
+ function buildDebugDemand(category, phase, statusCodes = []) {
10886
+ const phaseHint = phase ? ` for ${phase}` : "";
10887
+ switch (category) {
10888
+ case "test_failure":
10889
+ 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.`;
10890
+ case "syntax_error":
10891
+ return `Read the referenced file, patch the syntax/indentation at the failing location, then run tests.`;
10892
+ case "import_error":
10893
+ return `Resolve the real import/module path or dependency. Do not add fake fallback modules or swallow ImportError in production code.`;
10894
+ case "dependency_error":
10895
+ return `Replace hallucinated dependency names with real package names and update the manifest via add_dependency.`;
10896
+ case "network_api_failure":
10897
+ return networkDemand(statusCodes);
10898
+ case "missing_output":
10899
+ return `Create or repair the declared output files. Do not mark done=true until verify outputs passes.`;
10900
+ case "tool_loop":
10901
+ return `Stop repeating read-only/probe actions. Use the current evidence to make a patch/write/dependency change or run a concrete verification command.`;
10902
+ case "permission_denied":
10903
+ return `Treat the denied operation as a real blocker unless an allowed alternative exists; do not bypass the permission gate.`;
10904
+ case "llm_provider":
10905
+ return `This is provider/context infrastructure, not a project code bug. Reduce prompt/debug context or fix provider config before retrying.`;
10906
+ case "exception":
10907
+ return `Localize the exception to a file or tool call, make the smallest allowed repair, then verify.`;
10908
+ case "unknown":
10909
+ return `Read the most relevant files and produce a concrete diagnosis before making a minimal allowed repair.`;
10910
+ }
10911
+ }
10912
+ function networkDemand(statusCodes) {
10913
+ if (statusCodes.some((code) => code === "401" || code === "403")) {
10914
+ 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.";
10915
+ }
10916
+ if (statusCodes.some((code) => code === "404" || code === "410")) {
10917
+ return "The API URL/resource is unavailable. Stop retrying the same URL; switch to a maintained endpoint and verify response shape.";
10918
+ }
10919
+ if (statusCodes.includes("429")) {
10920
+ return "The API is rate-limited. Switch to a suitable fallback API or implement explicit retry/cache behaviour and tests.";
10921
+ }
10922
+ if (statusCodes.some((code) => /^5/u.test(code))) {
10923
+ return "The API server failed. Use a stable fallback endpoint or fail closed with a clear user-visible error path.";
10924
+ }
10925
+ 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.";
10926
+ }
10927
+ function isProcessNoise(category) {
10928
+ return ["tool_loop", "llm_provider", "permission_denied", "exception"].includes(category);
10929
+ }
10930
+ function shouldSuppressReasonInEvidence(reason, failureLog) {
10931
+ if (!reason || !failureLog?.trim()) return false;
10932
+ const lowerReason = reason.toLowerCase();
10933
+ const lowerLog = failureLog.toLowerCase();
10934
+ 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);
10935
+ if (!hasRootSignal) return false;
10936
+ 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);
10937
+ }
10938
+ function extractFailedTests(text) {
10939
+ const patterns = [
10940
+ /\bFAILED\s+([^\s]+(?:\.py|\.ts|\.tsx|\.js|\.jsx)(?:::[^\s]+)?)/gu,
10941
+ /([^\s]+(?:\.py|\.ts|\.tsx|\.js|\.jsx)::[A-Za-z0-9_:[\].-]+)/gu,
10942
+ /[×x]\s+([^\n]+?>\s+[^\n]+)/gu
10943
+ ];
10944
+ const out = [];
10945
+ for (const pattern of patterns) {
10946
+ for (const match of text.matchAll(pattern)) {
10947
+ const value = oneLine(match[1] ?? "");
10948
+ if (value) out.push(value);
10949
+ }
10950
+ }
10951
+ return dedup3(out);
10952
+ }
10953
+ function extractFiles(text) {
10954
+ const out = [];
10955
+ const patterns = [
10956
+ /\b((?:src|tests?|docs)\/[A-Za-z0-9_./-]+\.(?:py|ts|tsx|js|jsx|json|md|dbc|csv|xlsx?))/gu,
10957
+ /File\s+["']([^"']+)["']/gu
10958
+ ];
10959
+ for (const pattern of patterns) {
10960
+ for (const match of text.matchAll(pattern)) {
10961
+ const file = normalizePath2(match[1] ?? "");
10962
+ if (file && !file.includes("node_modules/")) out.push(file);
10963
+ }
10964
+ }
10965
+ return dedup3(out);
10966
+ }
10967
+ function extractToolFailures(lines) {
10968
+ 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);
10969
+ }
10970
+ function extractStatusCodes(text) {
10971
+ const out = [];
10972
+ for (const match of text.matchAll(/\b(?:HTTP\s*)?(401|403|404|408|409|410|422|429|5\d\d)\b/giu)) {
10973
+ out.push(match[1]);
10974
+ }
10975
+ return dedup3(out);
10976
+ }
10977
+ function selectEvidenceLines(text, category, primaryError, failedTests, files, toolFailures, maxEvidence = MAX_EVIDENCE) {
10978
+ const lines = normalizedLines(text);
10979
+ const needles = [
10980
+ primaryError,
10981
+ ...failedTests,
10982
+ ...files,
10983
+ ...toolFailures,
10984
+ category === "network_api_failure" ? "http" : "",
10985
+ category === "missing_output" ? "missing" : "",
10986
+ category === "tool_loop" ? "read-only" : ""
10987
+ ].map((item) => item.toLowerCase()).filter(Boolean);
10988
+ const selected = [];
10989
+ for (const line of lines) {
10990
+ const lower = line.toLowerCase();
10991
+ 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)) {
10992
+ selected.push(truncateLine(line, MAX_EVIDENCE_LINE));
10993
+ }
10994
+ }
10995
+ const compact = dedup3(selected).slice(0, maxEvidence);
10996
+ return { lines: compact, omitted: Math.max(0, selected.length - compact.length) };
10997
+ }
10998
+ function normalizedLines(text) {
10999
+ 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));
11000
+ }
11001
+ function oneLine(text) {
11002
+ return truncateLine(text.replace(/\s+/gu, " ").trim(), MAX_EVIDENCE_LINE);
11003
+ }
11004
+ function truncateLine(text, max) {
11005
+ if (text.length <= max) return text;
11006
+ return `${text.slice(0, Math.max(0, max - 24))}... [truncated ${text.length - max + 24} chars]`;
11007
+ }
11008
+ function normalizePath2(file) {
11009
+ return file.replace(/\\/g, "/").replace(/^.*?((?:src|tests?|docs)\/)/u, "$1");
11010
+ }
11011
+ function dedup3(items) {
11012
+ return [...new Set(items.filter((item) => String(item ?? "").length > 0))];
11013
+ }
11014
+
11015
+ // src/core/debug_cache.ts
10089
11016
  var EMPTY = { version: 1, steps: {} };
10090
11017
  function sanitizeDebugFailureLogForPrompt(log) {
10091
11018
  const lines = log.split(/\r?\n/u);
@@ -10097,19 +11024,23 @@ function sanitizeDebugFailureLogForPrompt(log) {
10097
11024
  mode = "skip-history";
10098
11025
  continue;
10099
11026
  }
11027
+ if (/^##\s+(?:debug brief|root issue brief|current retry brief)\b/u.test(line)) {
11028
+ mode = "skip-brief";
11029
+ continue;
11030
+ }
10100
11031
  if (/^##\s+修复建议/u.test(line)) {
10101
11032
  mode = "skip-suggestions";
10102
11033
  continue;
10103
11034
  }
10104
11035
  if (mode === "skip-history") {
10105
- if (/^(原因:|Reason:|##\s+debug failure log\b)/u.test(line)) {
11036
+ if (/^(原因:|Reason:|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
10106
11037
  mode = "keep";
10107
11038
  } else {
10108
11039
  continue;
10109
11040
  }
10110
11041
  }
10111
11042
  if (mode === "skip-suggestions") {
10112
- if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+历史\s+DEBUG\s+尝试|##\s+debug failure log\b)/u.test(line)) {
11043
+ if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+历史\s+DEBUG\s+尝试|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
10113
11044
  mode = "keep";
10114
11045
  if (/^##\s+历史\s+DEBUG\s+尝试/u.test(line)) {
10115
11046
  mode = "skip-history";
@@ -10119,6 +11050,13 @@ function sanitizeDebugFailureLogForPrompt(log) {
10119
11050
  continue;
10120
11051
  }
10121
11052
  }
11053
+ if (mode === "skip-brief") {
11054
+ if (/^(原因:|Reason:|轮次:|工具调用:|---\s+|##\s+修复建议|##\s+历史\s+DEBUG\s+尝试|##\s+(?:debug failure log|compact failure evidence)\b)/u.test(line)) {
11055
+ mode = "keep";
11056
+ } else {
11057
+ continue;
11058
+ }
11059
+ }
10122
11060
  if (/^\s*suggestions:\s/u.test(line)) continue;
10123
11061
  if (/^(原因:|Reason:)/u.test(line)) {
10124
11062
  const normalized = line.trim();
@@ -10156,7 +11094,7 @@ var DebugCache = class {
10156
11094
  if (this.loaded) return;
10157
11095
  this.loaded = true;
10158
11096
  try {
10159
- const raw = await fs23.readFile(this.file, "utf8");
11097
+ const raw = await fs24.readFile(this.file, "utf8");
10160
11098
  const parsed = JSON.parse(raw);
10161
11099
  if (parsed && parsed.version === 1 && parsed.steps && typeof parsed.steps === "object") {
10162
11100
  this.data = { version: 1, steps: parsed.steps };
@@ -10184,11 +11122,24 @@ var DebugCache = class {
10184
11122
  const cleanedFailureLog = stripNestedLatestDebuggerFailures(
10185
11123
  sanitizeDebugFailureLogForPrompt(entry.failureLogTail ?? "")
10186
11124
  );
11125
+ const debugBrief = buildDebugBrief({
11126
+ reason: entry.reason,
11127
+ failureLog: cleanedFailureLog
11128
+ });
11129
+ const compactFailureLog = compactFailureEvidence({
11130
+ reason: entry.reason,
11131
+ failureLog: cleanedFailureLog,
11132
+ maxChars: 3600,
11133
+ maxLines: this.maxLogLines
11134
+ });
10187
11135
  const e = {
10188
11136
  attempt: entry.attempt,
10189
11137
  ts: entry.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
10190
11138
  reason: (entry.reason ?? "").slice(0, 500),
10191
- failureLogTail: tail(cleanedFailureLog),
11139
+ failureLogTail: tail(compactFailureLog),
11140
+ debugBrief,
11141
+ contextMode: entry.contextMode,
11142
+ testScopeArgs: entry.testScopeArgs,
10192
11143
  suggestions: entry.suggestions,
10193
11144
  snapshotSha: entry.snapshotSha,
10194
11145
  metrics: entry.metrics
@@ -10253,8 +11204,10 @@ var DebugCache = class {
10253
11204
  const m = e.metrics ? ` [health=${e.metrics.healthScore.toFixed(2)} parseFail=${e.metrics.parseFailures} repeat=${e.metrics.repeatedTurns}]` : "";
10254
11205
  const sugg = e.suggestions && e.suggestions.length > 0 ? `
10255
11206
  prior suggestions: omitted (${e.suggestions.length}) to avoid stale guidance; use the current failure log first.` : "";
11207
+ const brief = e.debugBrief ? `
11208
+ brief: ${e.debugBrief.category} \u2014 ${e.debugBrief.summary}` : "";
10256
11209
  return `- attempt #${e.attempt} @ ${e.ts}${m}
10257
- reason: ${e.reason}${sugg}`;
11210
+ reason: ${e.reason}${brief}${sugg}`;
10258
11211
  });
10259
11212
  const omitted = [
10260
11213
  noisyCount > 0 ? `- omitted ${noisyCount} noisy provider/read-only/recovery attempt(s); keep focus on the current actionable failure.` : "",
@@ -10270,18 +11223,609 @@ var DebugCache = class {
10270
11223
  ].join("\n");
10271
11224
  }
10272
11225
  async save() {
10273
- await fs23.mkdir(path24.dirname(this.file), { recursive: true });
10274
- await fs23.writeFile(this.file, JSON.stringify(this.data, null, 2), "utf8");
11226
+ await fs24.mkdir(path24.dirname(this.file), { recursive: true });
11227
+ await fs24.writeFile(this.file, JSON.stringify(this.data, null, 2), "utf8");
10275
11228
  }
10276
11229
  };
10277
11230
  function isNoisyDebugReason(reason) {
10278
11231
  const text = reason.toLowerCase();
10279
- 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);
11232
+ 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);
10280
11233
  }
10281
11234
 
10282
- // src/core/project_audit.ts
10283
- import { promises as fs24 } from "fs";
11235
+ // src/core/debug_wiki.ts
11236
+ import { promises as fs25 } from "fs";
10284
11237
  import path25 from "path";
11238
+ import { fileURLToPath } from "url";
11239
+ import YAML3 from "yaml";
11240
+ var DEFAULT_DEBUG_WIKI_REL_PATH = ".xcompiler/debug-wiki";
11241
+ var BUNDLED_DEBUG_WIKI_REL_PATH = "debug-wiki";
11242
+ var DEBUG_WIKI_VERSION = 1;
11243
+ var LAYERS = ["system", "agent", "external"];
11244
+ var EMPTY_STATS = { uses: 0, successes: 0, failures: 0 };
11245
+ function defaultDebugWikiPath() {
11246
+ const configured = xcEnv("PATH")?.trim();
11247
+ const base = configured ? path25.resolve(configured) : path25.resolve(path25.dirname(fileURLToPath(import.meta.url)), "..", "..");
11248
+ return path25.join(base, DEFAULT_DEBUG_WIKI_REL_PATH);
11249
+ }
11250
+ function bundledDebugWikiPath() {
11251
+ return path25.join(path25.resolve(path25.dirname(fileURLToPath(import.meta.url)), "..", ".."), BUNDLED_DEBUG_WIKI_REL_PATH);
11252
+ }
11253
+ var DebugWiki = class {
11254
+ loaded = false;
11255
+ entries = [];
11256
+ rootPath;
11257
+ filePath;
11258
+ bundledPath;
11259
+ constructor(rootPath, opts = {}) {
11260
+ this.rootPath = path25.resolve(rootPath);
11261
+ this.filePath = this.rootPath;
11262
+ this.bundledPath = opts.bundledPath ?? bundledDebugWikiPath();
11263
+ }
11264
+ async load() {
11265
+ if (this.loaded) return;
11266
+ this.loaded = true;
11267
+ await this.ensureLayout();
11268
+ await this.ensureRootReadme();
11269
+ await this.ensureOperationLog();
11270
+ await this.copyBundledLayers();
11271
+ this.entries = [];
11272
+ for (const layer of LAYERS) {
11273
+ this.entries.push(...await this.readLayer(layer));
11274
+ }
11275
+ await this.applyFeedbackLog();
11276
+ await this.writeIndex();
11277
+ }
11278
+ async search(brief, opts = {}) {
11279
+ await this.load();
11280
+ const limit = opts.limit ?? 3;
11281
+ return this.rank(brief, opts.language).filter((match) => match.score >= 4).slice(0, limit);
11282
+ }
11283
+ async recordUse(entryIds, input2) {
11284
+ await this.load();
11285
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11286
+ const feedback = this.feedbackFrom(entryIds, input2, now, "used");
11287
+ if (feedback.length === 0) return;
11288
+ for (const item of feedback) {
11289
+ const entry = this.byId(item.entryId);
11290
+ if (!entry) continue;
11291
+ entry.stats.uses += 1;
11292
+ entry.lastUsedAt = now;
11293
+ entry.updatedAt = now;
11294
+ pushFeedback(entry, item);
11295
+ }
11296
+ await this.appendLayerFeedback(feedback);
11297
+ await this.persistExternalEntries();
11298
+ await this.writeIndex(now);
11299
+ await this.appendOperationLog({
11300
+ at: now,
11301
+ action: "use",
11302
+ entryIds: feedback.map((item) => item.entryId).filter(Boolean),
11303
+ issueId: input2.issueId,
11304
+ stepId: input2.stepId,
11305
+ phase: input2.phase,
11306
+ summary: input2.brief.summary
11307
+ });
11308
+ }
11309
+ async recordFailure(entryIds, input2) {
11310
+ await this.load();
11311
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11312
+ const feedback = this.feedbackFrom(entryIds, input2, now, "failure", input2.reason);
11313
+ if (feedback.length === 0) return;
11314
+ for (const item of feedback) {
11315
+ const entry = this.byId(item.entryId);
11316
+ if (!entry) continue;
11317
+ entry.stats.failures += 1;
11318
+ entry.status = "needs_review";
11319
+ entry.updatedAt = now;
11320
+ pushFeedback(entry, item);
11321
+ }
11322
+ await this.appendLayerFeedback(feedback);
11323
+ await this.persistExternalEntries();
11324
+ await this.writeIndex(now);
11325
+ await this.appendOperationLog({
11326
+ at: now,
11327
+ action: "failure",
11328
+ entryIds: feedback.map((item) => item.entryId).filter(Boolean),
11329
+ issueId: input2.issueId,
11330
+ stepId: input2.stepId,
11331
+ phase: input2.phase,
11332
+ summary: input2.brief.summary,
11333
+ reason: input2.reason
11334
+ });
11335
+ }
11336
+ async recordResolution(input2) {
11337
+ await this.load();
11338
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11339
+ const used = this.byIds(input2.usedEntryIds ?? []);
11340
+ const externalTargets = used.filter((entry) => entry.layer === "external");
11341
+ const target = externalTargets.length > 0 ? externalTargets : this.rank(input2.brief, input2.language).filter((match) => match.entry.layer === "external" && match.score >= 8).slice(0, 1).map((m) => m.entry);
11342
+ const updated = [];
11343
+ let createdId;
11344
+ for (const entry of target) {
11345
+ this.applyResolution(entry, input2, now, entry.stats.failures > 0 ? "corrected" : "success");
11346
+ updated.push(entry.id);
11347
+ }
11348
+ if (updated.length === 0) {
11349
+ const created = createEntry(input2, now, this.nextExternalId(now), "external");
11350
+ created.supersedes = used.length > 0 ? used.map((entry) => entry.id) : void 0;
11351
+ this.entries.push(created);
11352
+ createdId = created.id;
11353
+ }
11354
+ const correctedFeedback = this.feedbackFrom(
11355
+ used.filter((entry) => entry.layer !== "external").map((entry) => entry.id),
11356
+ input2,
11357
+ now,
11358
+ "corrected"
11359
+ );
11360
+ for (const item of correctedFeedback) {
11361
+ const entry = this.byId(item.entryId);
11362
+ if (!entry) continue;
11363
+ entry.stats.successes += 1;
11364
+ entry.status = "active";
11365
+ entry.updatedAt = now;
11366
+ pushFeedback(entry, item);
11367
+ }
11368
+ await this.appendLayerFeedback(correctedFeedback);
11369
+ await this.persistExternalEntries();
11370
+ await this.writeIndex(now);
11371
+ await this.appendOperationLog({
11372
+ at: now,
11373
+ action: createdId ? "resolution_created" : "resolution_updated",
11374
+ entryIds: createdId ? [createdId] : updated,
11375
+ issueId: input2.issueId,
11376
+ stepId: input2.stepId,
11377
+ phase: input2.phase,
11378
+ summary: input2.brief.summary
11379
+ });
11380
+ return createdId ? { created: createdId, updated: [] } : { updated };
11381
+ }
11382
+ async ensureLayout() {
11383
+ for (const layer of LAYERS) {
11384
+ await fs25.mkdir(this.layerDir(layer), { recursive: true });
11385
+ }
11386
+ }
11387
+ async ensureRootReadme() {
11388
+ const to = path25.join(this.rootPath, "README.md");
11389
+ if (await exists(to)) return;
11390
+ const from = path25.join(this.bundledPath, "README.md");
11391
+ const fallback = defaultDebugWikiReadme();
11392
+ const text = await fs25.readFile(from, "utf8").catch(() => fallback);
11393
+ await fs25.writeFile(to, text.endsWith("\n") ? text : `${text}
11394
+ `, "utf8");
11395
+ }
11396
+ async ensureOperationLog() {
11397
+ const file = this.operationLogPath();
11398
+ if (await exists(file)) return;
11399
+ await fs25.writeFile(file, "# XCompiler Debug Wiki Log\n\nAppend-only operational notes for retrieval, failed reuse, and confirmed repairs.\n", "utf8");
11400
+ }
11401
+ async copyBundledLayers() {
11402
+ if (path25.resolve(this.bundledPath) === this.rootPath) return;
11403
+ for (const layer of ["system", "agent"]) {
11404
+ const from = path25.join(this.bundledPath, "wiki", layer);
11405
+ const to = this.layerDir(layer);
11406
+ if (!await exists(from)) continue;
11407
+ await fs25.cp(from, to, { recursive: true, force: true });
11408
+ }
11409
+ }
11410
+ async readLayer(layer) {
11411
+ const dir = this.layerDir(layer);
11412
+ const files = (await fs25.readdir(dir).catch(() => [])).filter((file) => file.endsWith(".md")).sort();
11413
+ const entries = [];
11414
+ for (const file of files) {
11415
+ const abs = path25.join(dir, file);
11416
+ const page = parseWikiPage(await fs25.readFile(abs, "utf8"));
11417
+ const entry = normalizeEntry({ ...page.data, layer, solution: page.data.solution ?? page.body }, layer);
11418
+ entry.sourcePath = path25.relative(this.rootPath, abs).replace(/\\/g, "/");
11419
+ entries.push(entry);
11420
+ }
11421
+ return entries;
11422
+ }
11423
+ async applyFeedbackLog() {
11424
+ const log = await fs25.readFile(this.feedbackPath(), "utf8").catch(() => "");
11425
+ for (const line of log.split(/\r?\n/u)) {
11426
+ if (!line.trim()) continue;
11427
+ const item = JSON.parse(line);
11428
+ const entry = this.byId(item.entryId);
11429
+ if (!entry) continue;
11430
+ if (item.kind === "used") entry.stats.uses += 1;
11431
+ if (item.kind === "failure") {
11432
+ entry.stats.failures += 1;
11433
+ entry.status = "needs_review";
11434
+ }
11435
+ if (item.kind === "success" || item.kind === "corrected") {
11436
+ entry.stats.successes += 1;
11437
+ if (item.kind === "corrected") entry.status = "active";
11438
+ }
11439
+ entry.updatedAt = item.at;
11440
+ pushFeedback(entry, item);
11441
+ }
11442
+ }
11443
+ applyResolution(entry, input2, now, kind) {
11444
+ entry.status = "active";
11445
+ entry.updatedAt = now;
11446
+ entry.summary = input2.brief.summary;
11447
+ entry.primaryError = input2.brief.primaryError;
11448
+ entry.debugDemand = input2.brief.debugDemand;
11449
+ entry.fingerprints = dedup4([...entry.fingerprints, ...fingerprints(input2.brief)]);
11450
+ entry.symptoms = dedup4([...input2.brief.evidence, ...entry.symptoms]).slice(0, 12);
11451
+ entry.evidence = dedup4([...input2.evidence ?? [], ...entry.evidence]).slice(0, 12);
11452
+ if (input2.resolutionPlan?.trim()) entry.resolutionPlan = input2.resolutionPlan.trim();
11453
+ entry.solution = mergeSolution(entry.solution, input2.solution);
11454
+ entry.repairFiles = dedup4([...input2.repairFiles ?? [], ...entry.repairFiles ?? []]).slice(0, 12);
11455
+ entry.stats.successes += 1;
11456
+ pushFeedback(entry, {
11457
+ at: now,
11458
+ kind,
11459
+ entryId: entry.id,
11460
+ issueId: input2.issueId,
11461
+ stepId: input2.stepId,
11462
+ phase: input2.phase,
11463
+ summary: input2.brief.summary
11464
+ });
11465
+ }
11466
+ async persistExternalEntries() {
11467
+ for (const entry of this.entries.filter((item) => item.layer === "external")) {
11468
+ const abs = path25.join(this.rootPath, entry.sourcePath ?? this.externalEntryPath(entry));
11469
+ entry.sourcePath = path25.relative(this.rootPath, abs).replace(/\\/g, "/");
11470
+ await fs25.mkdir(path25.dirname(abs), { recursive: true });
11471
+ await fs25.writeFile(abs, renderWikiPage(entry), "utf8");
11472
+ }
11473
+ }
11474
+ async appendFeedback(feedback) {
11475
+ if (feedback.length === 0) return;
11476
+ await fs25.mkdir(path25.dirname(this.feedbackPath()), { recursive: true });
11477
+ await fs25.appendFile(this.feedbackPath(), feedback.map((item) => JSON.stringify(item)).join("\n") + "\n", "utf8");
11478
+ }
11479
+ async appendLayerFeedback(feedback) {
11480
+ await this.appendFeedback(feedback.filter((item) => this.byId(item.entryId)?.layer !== "external"));
11481
+ }
11482
+ async appendOperationLog(entry) {
11483
+ await fs25.mkdir(path25.dirname(this.operationLogPath()), { recursive: true });
11484
+ await fs25.appendFile(this.operationLogPath(), renderOperationLogEntry(entry), "utf8");
11485
+ }
11486
+ async writeIndex(now = (/* @__PURE__ */ new Date()).toISOString()) {
11487
+ const layerCounts = Object.fromEntries(LAYERS.map((layer) => [
11488
+ layer,
11489
+ { entries: this.entries.filter((entry) => entry.layer === layer).length, writable: layer === "external" }
11490
+ ]));
11491
+ const index = {
11492
+ version: DEBUG_WIKI_VERSION,
11493
+ updatedAt: now,
11494
+ root: this.rootPath,
11495
+ layers: layerCounts,
11496
+ entries: this.entries.map((entry) => ({
11497
+ id: entry.id,
11498
+ layer: entry.layer,
11499
+ status: entry.status,
11500
+ category: entry.category,
11501
+ summary: entry.summary,
11502
+ updatedAt: entry.updatedAt,
11503
+ sourcePath: entry.sourcePath
11504
+ }))
11505
+ };
11506
+ await fs25.writeFile(path25.join(this.rootPath, "index.json"), `${JSON.stringify(index, null, 2)}
11507
+ `, "utf8");
11508
+ await fs25.writeFile(path25.join(this.rootPath, "index.md"), renderReadableIndex(index, this.entries), "utf8");
11509
+ }
11510
+ feedbackFrom(ids, input2, now, kind, reason) {
11511
+ return dedup4(ids).filter((id) => this.byId(id)).map((id) => ({
11512
+ at: now,
11513
+ kind,
11514
+ entryId: id,
11515
+ issueId: input2.issueId,
11516
+ stepId: input2.stepId,
11517
+ phase: input2.phase,
11518
+ summary: input2.brief.summary,
11519
+ reason
11520
+ }));
11521
+ }
11522
+ rank(brief, language) {
11523
+ const queryTokens = new Set(tokensForBrief(brief));
11524
+ const queryFingerprints = fingerprints(brief);
11525
+ return this.entries.filter((entry) => entry.status !== "superseded").map((entry) => {
11526
+ const reasons = [];
11527
+ let score = entry.layer === "agent" ? 1 : entry.layer === "system" ? 0.5 : 0;
11528
+ if (entry.category === brief.category) {
11529
+ score += 4;
11530
+ reasons.push(`category:${entry.category}`);
11531
+ }
11532
+ if (language && entry.language === language) score += 1;
11533
+ const exact = entry.fingerprints.filter((fp) => queryFingerprints.includes(fp));
11534
+ if (exact.length > 0) {
11535
+ score += exact.length * 3;
11536
+ reasons.push(`fingerprint:${exact.length}`);
11537
+ }
11538
+ const entryTokens = new Set(tokensForEntry(entry));
11539
+ let overlap = 0;
11540
+ for (const token of queryTokens) if (entryTokens.has(token)) overlap++;
11541
+ score += Math.min(6, overlap);
11542
+ if (overlap > 0) reasons.push(`tokens:${overlap}`);
11543
+ const confidence = confidenceFor(entry);
11544
+ return { entry, score: score * confidence, confidence, reasons };
11545
+ }).sort((a, b) => b.score - a.score);
11546
+ }
11547
+ byId(id) {
11548
+ return this.entries.find((entry) => entry.id === id);
11549
+ }
11550
+ byIds(ids) {
11551
+ const wanted = new Set(ids);
11552
+ return this.entries.filter((entry) => wanted.has(entry.id));
11553
+ }
11554
+ layerDir(layer) {
11555
+ return path25.join(this.rootPath, "wiki", layer);
11556
+ }
11557
+ feedbackPath() {
11558
+ return path25.join(this.rootPath, "wiki", "external", "feedback.jsonl");
11559
+ }
11560
+ operationLogPath() {
11561
+ return path25.join(this.rootPath, "log.md");
11562
+ }
11563
+ nextExternalId(now) {
11564
+ const stamp = now.replace(/[-:.TZ]/g, "").slice(0, 14);
11565
+ const count = this.entries.filter((entry) => entry.layer === "external").length + 1;
11566
+ return `external.${stamp}.${String(count).padStart(4, "0")}`;
11567
+ }
11568
+ externalEntryPath(entry) {
11569
+ return path25.join("wiki", "external", `${slugify(entry.id)}.md`);
11570
+ }
11571
+ };
11572
+ function renderDebugWikiMatchesForPrompt(matches) {
11573
+ if (matches.length === 0) return "";
11574
+ const lines = [
11575
+ "## debug wiki matches",
11576
+ "LLM-wiki layered retrieval. Treat entries as hypotheses, verify against current files/tests, and stop using any entry that current evidence disproves."
11577
+ ];
11578
+ for (const match of matches) {
11579
+ const entry = match.entry;
11580
+ lines.push(
11581
+ `- ${entry.id} layer=${entry.layer} score=${match.score.toFixed(2)} confidence=${match.confidence.toFixed(2)} status=${entry.status}`,
11582
+ ` problem: [${entry.category}] ${entry.summary}`,
11583
+ ` symptoms: ${entry.symptoms.slice(0, 4).join(" | ") || entry.primaryError}`,
11584
+ entry.resolutionPlan ? ` priorPlan: ${entry.resolutionPlan}` : "",
11585
+ ` confirmedSolution: ${entry.solution}`,
11586
+ ` feedback: uses=${entry.stats.uses} successes=${entry.stats.successes} failures=${entry.stats.failures}`
11587
+ );
11588
+ if (entry.repairFiles?.length) lines.push(` repairFiles: ${entry.repairFiles.join(", ")}`);
11589
+ if (entry.supersedes?.length) lines.push(` supersedes: ${entry.supersedes.join(", ")}`);
11590
+ if (match.reasons.length) lines.push(` matchedBy: ${match.reasons.join(", ")}`);
11591
+ }
11592
+ return lines.filter(Boolean).join("\n");
11593
+ }
11594
+ function createEntry(input2, now, id, layer) {
11595
+ return normalizeEntry({
11596
+ id,
11597
+ layer,
11598
+ createdAt: now,
11599
+ updatedAt: now,
11600
+ status: "active",
11601
+ category: input2.brief.category,
11602
+ summary: input2.brief.summary,
11603
+ primaryError: input2.brief.primaryError,
11604
+ debugDemand: input2.brief.debugDemand,
11605
+ fingerprints: fingerprints(input2.brief),
11606
+ symptoms: input2.brief.evidence.slice(0, 12),
11607
+ resolutionPlan: input2.resolutionPlan?.trim(),
11608
+ solution: input2.solution,
11609
+ evidence: (input2.evidence ?? input2.brief.evidence).slice(0, 12),
11610
+ sourceIssueId: input2.issueId,
11611
+ sourceStepId: input2.stepId,
11612
+ sourcePhase: input2.phase,
11613
+ targetPhase: input2.targetPhase,
11614
+ language: input2.language,
11615
+ repairFiles: input2.repairFiles?.slice(0, 12),
11616
+ stats: { uses: 0, successes: 1, failures: 0 },
11617
+ feedback: [{ at: now, kind: "success", entryId: id, issueId: input2.issueId, stepId: input2.stepId, phase: input2.phase, summary: input2.brief.summary }]
11618
+ }, layer);
11619
+ }
11620
+ function normalizeEntry(raw, layer) {
11621
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11622
+ return {
11623
+ id: String(raw.id ?? `${layer}.${slugify(raw.summary ?? raw.primaryError ?? "entry")}`),
11624
+ layer: raw.layer ?? layer,
11625
+ createdAt: raw.createdAt ?? now,
11626
+ updatedAt: raw.updatedAt ?? raw.createdAt ?? now,
11627
+ status: raw.status ?? "active",
11628
+ category: raw.category ?? "unknown",
11629
+ summary: raw.summary ?? raw.primaryError ?? "Debug wiki entry",
11630
+ primaryError: raw.primaryError ?? raw.summary ?? "",
11631
+ debugDemand: raw.debugDemand ?? "",
11632
+ fingerprints: raw.fingerprints ?? [],
11633
+ symptoms: raw.symptoms ?? [],
11634
+ resolutionPlan: raw.resolutionPlan,
11635
+ solution: raw.solution ?? "",
11636
+ evidence: raw.evidence ?? [],
11637
+ sourceIssueId: raw.sourceIssueId,
11638
+ sourceStepId: raw.sourceStepId,
11639
+ sourcePhase: raw.sourcePhase,
11640
+ targetPhase: raw.targetPhase,
11641
+ language: raw.language,
11642
+ repairFiles: raw.repairFiles ?? [],
11643
+ supersedes: raw.supersedes ?? [],
11644
+ stats: { ...EMPTY_STATS, ...raw.stats ?? {} },
11645
+ lastUsedAt: raw.lastUsedAt,
11646
+ feedback: (raw.feedback ?? []).slice(-20),
11647
+ sourcePath: raw.sourcePath
11648
+ };
11649
+ }
11650
+ function renderWikiPage(entry) {
11651
+ const frontmatter = { ...entry, sourcePath: void 0 };
11652
+ return [
11653
+ "---",
11654
+ YAML3.stringify(frontmatter).trim(),
11655
+ "---",
11656
+ "",
11657
+ `# ${entry.summary}`,
11658
+ "",
11659
+ "## Problem",
11660
+ "",
11661
+ `- category: ${entry.category}`,
11662
+ `- status: ${entry.status}`,
11663
+ `- primaryError: ${entry.primaryError || "n/a"}`,
11664
+ `- debugDemand: ${entry.debugDemand || "n/a"}`,
11665
+ "",
11666
+ "## Resolution Plan",
11667
+ "",
11668
+ entry.resolutionPlan?.trim() || "No explicit plan recorded.",
11669
+ "",
11670
+ "## Confirmed Solution",
11671
+ "",
11672
+ entry.solution.trim() || "No confirmed solution recorded.",
11673
+ "",
11674
+ "## Evidence",
11675
+ "",
11676
+ renderMarkdownList(entry.evidence),
11677
+ "",
11678
+ "## Retrieval",
11679
+ "",
11680
+ `- fingerprints: ${entry.fingerprints.join(", ") || "n/a"}`,
11681
+ `- repairFiles: ${(entry.repairFiles ?? []).join(", ") || "n/a"}`,
11682
+ `- stats: uses=${entry.stats.uses} successes=${entry.stats.successes} failures=${entry.stats.failures}`,
11683
+ "",
11684
+ "## Feedback",
11685
+ "",
11686
+ renderMarkdownList(entry.feedback.slice(-8).map((item) => `${item.at} ${item.kind}: ${item.summary}`)),
11687
+ ""
11688
+ ].join("\n");
11689
+ }
11690
+ function parseWikiPage(text) {
11691
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u);
11692
+ if (!match) return { data: {}, body: text.trim() };
11693
+ return { data: YAML3.parse(match[1] ?? "") ?? {}, body: (match[2] ?? "").trim() };
11694
+ }
11695
+ function fingerprints(brief) {
11696
+ return dedup4([
11697
+ `cat:${brief.category}`,
11698
+ brief.primaryError ? `err:${normalize(brief.primaryError)}` : "",
11699
+ ...brief.failedTests.map((test) => `test:${normalize(test)}`),
11700
+ ...brief.files.map((file) => `file:${normalize(file)}`),
11701
+ ...brief.statusCodes.map((code) => `http:${code}`)
11702
+ ]);
11703
+ }
11704
+ function tokensForBrief(brief) {
11705
+ return tokenize2([brief.summary, brief.primaryError, brief.debugDemand, ...brief.failedTests, ...brief.files, ...brief.evidence, ...brief.statusCodes].join(" "));
11706
+ }
11707
+ function tokensForEntry(entry) {
11708
+ return tokenize2([entry.id, entry.summary, entry.primaryError, entry.debugDemand, entry.resolutionPlan ?? "", entry.solution, ...entry.symptoms, ...entry.evidence, ...entry.repairFiles ?? []].join(" "));
11709
+ }
11710
+ function tokenize2(text) {
11711
+ return dedup4(text.toLowerCase().split(/[^a-z0-9_./:-]+/u).filter((token) => token.length >= 3));
11712
+ }
11713
+ function normalize(text) {
11714
+ return tokenize2(text).slice(0, 24).join(" ");
11715
+ }
11716
+ function renderReadableIndex(index, entries) {
11717
+ const lines = [
11718
+ "# XCompiler Debug Wiki Index",
11719
+ "",
11720
+ `Updated: ${index.updatedAt}`,
11721
+ "",
11722
+ "This file is regenerated from wiki pages and feedback overlays. Edit knowledge pages under `wiki/`, not this index.",
11723
+ "",
11724
+ "## Layers",
11725
+ "",
11726
+ "| Layer | Entries | Writable | Purpose |",
11727
+ "| --- | ---: | --- | --- |"
11728
+ ];
11729
+ for (const layer of LAYERS) {
11730
+ const info = index.layers[layer];
11731
+ lines.push(`| ${layer} | ${info.entries} | ${info.writable ? "yes" : "no"} | ${layerPurpose(layer)} |`);
11732
+ }
11733
+ for (const layer of LAYERS) {
11734
+ const layerEntries = entries.filter((entry) => entry.layer === layer);
11735
+ lines.push("", `## ${layer}`, "");
11736
+ if (layerEntries.length === 0) {
11737
+ lines.push("No entries.");
11738
+ continue;
11739
+ }
11740
+ lines.push("| ID | Status | Category | Summary | Source |", "| --- | --- | --- | --- | --- |");
11741
+ for (const entry of layerEntries) {
11742
+ const source = entry.sourcePath ? `[${entry.sourcePath}](${entry.sourcePath})` : "";
11743
+ lines.push(`| ${escapeTable(entry.id)} | ${entry.status} | ${entry.category} | ${escapeTable(entry.summary)} | ${source} |`);
11744
+ }
11745
+ }
11746
+ return `${lines.join("\n")}
11747
+ `;
11748
+ }
11749
+ function renderOperationLogEntry(entry) {
11750
+ const lines = [
11751
+ "",
11752
+ `- ${entry.at} ${entry.action}: ${entry.entryIds.join(", ") || "none"}`,
11753
+ ` - issue: ${entry.issueId ?? "n/a"}; step: ${entry.stepId ?? "n/a"}; phase: ${entry.phase ?? "n/a"}`,
11754
+ ` - summary: ${entry.summary}`
11755
+ ];
11756
+ if (entry.reason) lines.push(` - reason: ${entry.reason}`);
11757
+ return `${lines.join("\n")}
11758
+ `;
11759
+ }
11760
+ function defaultDebugWikiReadme() {
11761
+ return [
11762
+ "# XCompiler Debug Wiki",
11763
+ "",
11764
+ "This directory is an LLM-wiki style knowledge base for Debugger repair.",
11765
+ "",
11766
+ "- `wiki/system/` contains system-level debug policies and safety rules.",
11767
+ "- `wiki/agent/` contains agent-level calibration knowledge derived from recurring LLM failure patterns.",
11768
+ "- `wiki/external/` stores real project issue resolutions and feedback.",
11769
+ "- `index.md` is a human-readable regenerated catalog.",
11770
+ "- `index.json` is the machine-readable retrieval cache.",
11771
+ "- `log.md` is an append-only operational log.",
11772
+ ""
11773
+ ].join("\n");
11774
+ }
11775
+ function layerPurpose(layer) {
11776
+ switch (layer) {
11777
+ case "system":
11778
+ return "bundled system debug policies";
11779
+ case "agent":
11780
+ return "bundled agent calibration knowledge";
11781
+ case "external":
11782
+ return "local project issue resolutions";
11783
+ }
11784
+ }
11785
+ function renderMarkdownList(items) {
11786
+ const compact = items.map((item) => item.trim()).filter(Boolean);
11787
+ if (compact.length === 0) return "- n/a";
11788
+ return compact.map((item) => `- ${item}`).join("\n");
11789
+ }
11790
+ function escapeTable(text) {
11791
+ return text.replace(/\|/gu, "\\|").replace(/\r?\n/gu, " ");
11792
+ }
11793
+ function confidenceFor(entry) {
11794
+ const total = entry.stats.uses + entry.stats.successes + entry.stats.failures;
11795
+ const base = (entry.stats.successes + 1) / Math.max(2, total + 2);
11796
+ const statusFactor = entry.status === "needs_review" ? 0.45 : 1;
11797
+ const layerFactor = entry.layer === "system" ? 0.9 : 1;
11798
+ return Math.max(0.1, Math.min(1, base * statusFactor * layerFactor));
11799
+ }
11800
+ function mergeSolution(previous, next) {
11801
+ const trimmed = next.trim();
11802
+ if (!trimmed || previous.includes(trimmed)) return previous;
11803
+ if (!previous.trim()) return trimmed;
11804
+ return `${previous.trim()}
11805
+ Corrected/confirmed resolution: ${trimmed}`;
11806
+ }
11807
+ function pushFeedback(entry, feedback) {
11808
+ entry.feedback.push(feedback);
11809
+ if (entry.feedback.length > 20) entry.feedback.splice(0, entry.feedback.length - 20);
11810
+ }
11811
+ function slugify(text) {
11812
+ return text.toLowerCase().replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 96) || "entry";
11813
+ }
11814
+ async function exists(filePath) {
11815
+ try {
11816
+ await fs25.stat(filePath);
11817
+ return true;
11818
+ } catch {
11819
+ return false;
11820
+ }
11821
+ }
11822
+ function dedup4(items) {
11823
+ return [...new Set(items.filter((item) => String(item ?? "").length > 0))];
11824
+ }
11825
+
11826
+ // src/core/project_audit.ts
11827
+ import { promises as fs26 } from "fs";
11828
+ import path26 from "path";
10285
11829
  function renderProjectAuditFailureLog(result) {
10286
11830
  const failed = result.checks.filter((check) => !check.ok);
10287
11831
  const interesting = failed.length > 0 ? failed : result.checks;
@@ -10330,12 +11874,12 @@ async function checkDocumentationBundle(ws, projectType, iterationId) {
10330
11874
  const docs = iterationId ? deliveryDocsForIteration(projectType, iterationId) : deliveryDocsForProjectType(projectType);
10331
11875
  const checks = [];
10332
11876
  for (const doc of docs) {
10333
- const exists = await ws.exists(doc);
11877
+ const exists2 = await ws.exists(doc);
10334
11878
  checks.push({
10335
11879
  name: docCheckName(doc),
10336
- severity: exists ? "info" : "error",
10337
- ok: exists,
10338
- summary: exists ? t().execute.auditDocPresent(doc) : t().execute.auditDocMissing(doc)
11880
+ severity: exists2 ? "info" : "error",
11881
+ ok: exists2,
11882
+ summary: exists2 ? t().execute.auditDocPresent(doc) : t().execute.auditDocMissing(doc)
10339
11883
  });
10340
11884
  }
10341
11885
  return checks;
@@ -10456,14 +12000,14 @@ async function listFiles(ws, dir) {
10456
12000
  async function walk3(abs, rel, out) {
10457
12001
  let entries;
10458
12002
  try {
10459
- entries = await fs24.readdir(abs, { withFileTypes: true });
12003
+ entries = await fs26.readdir(abs, { withFileTypes: true });
10460
12004
  } catch {
10461
12005
  return;
10462
12006
  }
10463
12007
  for (const entry of entries) {
10464
12008
  if (entry.name.startsWith(".")) continue;
10465
12009
  const childRel = `${rel}/${entry.name}`;
10466
- const childAbs = path25.join(abs, entry.name);
12010
+ const childAbs = path26.join(abs, entry.name);
10467
12011
  if (entry.isDirectory()) {
10468
12012
  await walk3(childAbs, childRel, out);
10469
12013
  } else {
@@ -10483,6 +12027,7 @@ var PhaseEngine = class {
10483
12027
  this.skills = opts.skills ?? buildDefaultSkills();
10484
12028
  this.plugins = opts.plugins ?? new PluginHost();
10485
12029
  this.debugCache = new DebugCache(opts.ws.abs(".xcompiler/debug_cache.json"));
12030
+ this.debugWiki = new DebugWiki(opts.debugWikiPath ?? defaultDebugWikiPath());
10486
12031
  }
10487
12032
  opts;
10488
12033
  registry;
@@ -10491,6 +12036,8 @@ var PhaseEngine = class {
10491
12036
  pluginExtensionsApplied = false;
10492
12037
  /** 跨 xcompiler run 持久化的 debug 历史(`<workspace>/.xcompiler/debug_cache.json`)。 */
10493
12038
  debugCache;
12039
+ /** 跨项目 debug 知识库,记录错误摘要、解决方案和正/负反馈。 */
12040
+ debugWiki;
10494
12041
  /** 当前 Plan 的语言 profile(在 run() 起始处按 plan.language 解析)。 */
10495
12042
  profile = getLanguageProfile("python");
10496
12043
  /** 当前 workspace 的项目记忆,用于给执行阶段注入更稳定的跨轮上下文。 */
@@ -10510,6 +12057,21 @@ var PhaseEngine = class {
10510
12057
  spin(text, options) {
10511
12058
  return this.terminalOutput ? spinner(text, options).start() : null;
10512
12059
  }
12060
+ async withDebugWiki(operation, action, fallback) {
12061
+ try {
12062
+ return await action();
12063
+ } catch (err) {
12064
+ const message = err.message;
12065
+ await this.opts.audit.event("note", `debug wiki ${operation} failed: ${message}`, {
12066
+ messageId: "engine.debug_wiki_failed",
12067
+ operation,
12068
+ path: this.debugWiki.filePath,
12069
+ error: message
12070
+ });
12071
+ if (this.opts.debugWikiStrict) throw err;
12072
+ return fallback;
12073
+ }
12074
+ }
10513
12075
  async requestEnginePermission(request) {
10514
12076
  if (!this.opts.requestPermission) return { approved: true };
10515
12077
  const decision = await this.opts.requestPermission(request);
@@ -10534,6 +12096,7 @@ var PhaseEngine = class {
10534
12096
  }
10535
12097
  async run(plan) {
10536
12098
  await this.plugins.initialize();
12099
+ await this.withDebugWiki("load", () => this.debugWiki.load(), void 0);
10537
12100
  if (!this.pluginExtensionsApplied) {
10538
12101
  this.plugins.applyExtensions({ tools: this.registry, skills: this.skills });
10539
12102
  this.pluginExtensionsApplied = true;
@@ -10741,7 +12304,23 @@ var PhaseEngine = class {
10741
12304
  const now = (/* @__PURE__ */ new Date()).toISOString();
10742
12305
  this.issueSeq += 1;
10743
12306
  const id = `ISSUE-${now.replace(/[-:.TZ]/g, "").slice(0, 14)}-${String(this.issueSeq).padStart(3, "0")}`;
10744
- const failureLog = cleanFailureLogForDebugContext(input2.failureLog);
12307
+ const rawFailureLog = input2.failureLog ?? "";
12308
+ const cleanedFailureLog = cleanFailureLogForDebugContext(rawFailureLog);
12309
+ const debugBrief = buildDebugBrief({
12310
+ reason: input2.reason,
12311
+ failureLog: cleanedFailureLog,
12312
+ phase: step?.phase
12313
+ });
12314
+ const failureLog = compactFailureEvidence({
12315
+ reason: input2.reason,
12316
+ failureLog: cleanedFailureLog,
12317
+ phase: step?.phase,
12318
+ maxChars: 6e3,
12319
+ maxLines: 90
12320
+ });
12321
+ const rawFailureLogPath = `.xcompiler/issues/${id}/failure.raw.log`;
12322
+ await this.opts.ws.writeFile(rawFailureLogPath, rawFailureLog.endsWith("\n") ? rawFailureLog : `${rawFailureLog}
12323
+ `);
10745
12324
  const issue = {
10746
12325
  id,
10747
12326
  createdAt: now,
@@ -10759,6 +12338,9 @@ var PhaseEngine = class {
10759
12338
  title: step?.title,
10760
12339
  reason: input2.reason,
10761
12340
  failureLog,
12341
+ failureLogBytes: Buffer.byteLength(rawFailureLog, "utf8"),
12342
+ rawFailureLogPath,
12343
+ debugBrief,
10762
12344
  metrics: input2.metrics,
10763
12345
  evidence: input2.evidence
10764
12346
  };
@@ -10776,6 +12358,12 @@ var PhaseEngine = class {
10776
12358
  issue.status = "routed";
10777
12359
  issue.targetStepId = target.id;
10778
12360
  issue.targetPhase = target.phase;
12361
+ issue.debugBrief = buildDebugBrief({
12362
+ reason: issue.reason,
12363
+ failureLog: issue.failureLog,
12364
+ phase: issue.phase,
12365
+ targetPhase: target.phase
12366
+ });
10779
12367
  issue.routedAt = (/* @__PURE__ */ new Date()).toISOString();
10780
12368
  issue.updatedAt = issue.routedAt;
10781
12369
  await this.persistIssue(issue, "routed", { routingReason: reason });
@@ -10794,13 +12382,27 @@ var PhaseEngine = class {
10794
12382
  issue.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10795
12383
  await this.persistIssue(issue, "unresolved", { reason });
10796
12384
  }
10797
- async markIssueResolved(issueId, step, repair) {
12385
+ async markIssueResolved(issueId, step, repair, issueResolutionPlan) {
10798
12386
  const issue = issueId ? this.findIssue(issueId) : void 0;
10799
12387
  if (!issue) return;
10800
12388
  issue.status = "resolved";
10801
12389
  issue.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
10802
12390
  issue.updatedAt = issue.resolvedAt;
10803
12391
  if (repair) issue.repair = repair;
12392
+ if (issueResolutionPlan?.trim()) {
12393
+ issue.issueResolutionPlan = issueResolutionPlan.trim();
12394
+ issue.resolutionPlanHistory = [
12395
+ ...issue.resolutionPlanHistory ?? [],
12396
+ {
12397
+ at: issue.resolvedAt,
12398
+ stepId: step.id,
12399
+ phase: step.phase,
12400
+ plan: issue.issueResolutionPlan,
12401
+ outcome: "accepted"
12402
+ }
12403
+ ].slice(-8);
12404
+ }
12405
+ await this.recordDebugWikiResolution(issue, step, repair);
10804
12406
  await this.persistIssue(issue, "resolved");
10805
12407
  await this.opts.audit.event("issue.resolve", `${issue.id} resolved by ${step.id} ${step.phase}`, {
10806
12408
  messageId: "engine.issue_resolved",
@@ -10810,6 +12412,82 @@ var PhaseEngine = class {
10810
12412
  repair
10811
12413
  });
10812
12414
  }
12415
+ async recordDebugWikiFailure(step, debug, outcome) {
12416
+ const entryIds = dedup5(debug.debugWikiEntryIds ?? []);
12417
+ if (entryIds.length === 0) return;
12418
+ const issue = debug.issueId ? this.findIssue(debug.issueId) : void 0;
12419
+ const brief = buildDebugBrief({
12420
+ reason: outcome.reason ?? debug.reason,
12421
+ failureLog: cleanFailureLogForDebugContext(outcome.failureLog),
12422
+ phase: issue?.phase ?? step.phase,
12423
+ targetPhase: issue?.targetPhase ?? step.phase
12424
+ });
12425
+ await this.withDebugWiki(
12426
+ "record-failure",
12427
+ () => this.debugWiki.recordFailure(entryIds, {
12428
+ brief,
12429
+ issueId: issue?.id,
12430
+ stepId: step.id,
12431
+ phase: step.phase,
12432
+ targetPhase: issue?.targetPhase,
12433
+ language: this.profile.id,
12434
+ solution: "retrieved wiki solution did not resolve this attempt",
12435
+ reason: outcome.reason ?? debug.reason
12436
+ }),
12437
+ void 0
12438
+ );
12439
+ await this.opts.audit.event("note", `debug wiki marked ${entryIds.join(", ")} for review`, {
12440
+ messageId: "engine.debug_wiki_feedback",
12441
+ kind: "failure",
12442
+ entryIds,
12443
+ issueId: issue?.id,
12444
+ stepId: step.id,
12445
+ reason: outcome.reason ?? debug.reason
12446
+ });
12447
+ }
12448
+ async recordDebugWikiResolution(issue, step, repair) {
12449
+ const brief = issue.debugBrief ?? buildDebugBrief({
12450
+ reason: issue.reason,
12451
+ failureLog: issue.failureLog,
12452
+ phase: issue.phase,
12453
+ targetPhase: issue.targetPhase
12454
+ });
12455
+ const repairFiles = repair?.changedFiles?.length ? repair.changedFiles : step.outputs;
12456
+ const evidenceSummary = [
12457
+ `Resolved ${issue.kind} by Debugger in ${step.id}/${step.phase}.`,
12458
+ `Mode: ${repair?.mode ?? "verification"}.`,
12459
+ repairFiles.length > 0 ? `Changed/verified files: ${repairFiles.join(", ")}.` : "",
12460
+ repair?.patchPath ? `Patch: ${repair.patchPath}.` : "",
12461
+ `Demand: ${brief.debugDemand}`
12462
+ ].filter(Boolean).join(" ");
12463
+ const solution = issue.issueResolutionPlan ? `${issue.issueResolutionPlan}
12464
+ Resolution evidence: ${evidenceSummary}` : evidenceSummary;
12465
+ const result = await this.withDebugWiki(
12466
+ "record-resolution",
12467
+ () => this.debugWiki.recordResolution({
12468
+ brief,
12469
+ issueId: issue.id,
12470
+ stepId: step.id,
12471
+ phase: step.phase,
12472
+ targetPhase: issue.targetPhase,
12473
+ language: this.profile.id,
12474
+ resolutionPlan: issue.issueResolutionPlan,
12475
+ solution,
12476
+ evidence: brief.evidence,
12477
+ repairFiles,
12478
+ usedEntryIds: issue.debugWikiEntryIds
12479
+ }),
12480
+ { updated: [] }
12481
+ );
12482
+ if (result.created || result.updated.length > 0) {
12483
+ await this.opts.audit.event("note", `debug wiki updated after ${issue.id}`, {
12484
+ messageId: "engine.debug_wiki_updated",
12485
+ issueId: issue.id,
12486
+ created: result.created,
12487
+ updated: result.updated
12488
+ });
12489
+ }
12490
+ }
10813
12491
  findIssue(issueId) {
10814
12492
  return this.issues.find((issue) => issue.id === issueId);
10815
12493
  }
@@ -10829,6 +12507,10 @@ var PhaseEngine = class {
10829
12507
  targetStepId: issue.targetStepId,
10830
12508
  targetPhase: issue.targetPhase,
10831
12509
  reason: issue.reason,
12510
+ debugSummary: issue.debugBrief?.summary,
12511
+ debugDemand: issue.debugBrief?.debugDemand,
12512
+ debugWikiEntryIds: issue.debugWikiEntryIds,
12513
+ issueResolutionPlan: issue.issueResolutionPlan,
10832
12514
  ...extra
10833
12515
  });
10834
12516
  await this.opts.ws.writeFile(eventPath, `${existing}${line}
@@ -10848,6 +12530,7 @@ var PhaseEngine = class {
10848
12530
  const diff = await this.opts.git.raw().diff([beforeRef, "--"]).catch((err) => `# git diff failed: ${err.message}
10849
12531
  `);
10850
12532
  const mode = inferRepairMode(toolCalls);
12533
+ const changedFiles = parsePatchChangedFiles(diff);
10851
12534
  await this.opts.ws.writeFile(patchPath, diff || "# No textual diff captured.\n");
10852
12535
  await this.opts.ws.writeFile(
10853
12536
  summaryPath,
@@ -10871,7 +12554,8 @@ var PhaseEngine = class {
10871
12554
  completedBeforeDebug,
10872
12555
  mode,
10873
12556
  patchPath,
10874
- summaryPath
12557
+ summaryPath,
12558
+ changedFiles
10875
12559
  };
10876
12560
  }
10877
12561
  async completedPhaseRepairViolation(toolCalls) {
@@ -10888,13 +12572,13 @@ var PhaseEngine = class {
10888
12572
  return void 0;
10889
12573
  }
10890
12574
  async repairChangedFiles() {
10891
- const planPath = normalizeGitPath(path26.relative(this.opts.ws.root, path26.resolve(this.opts.planPath)));
12575
+ const planPath = normalizeGitPath(path27.relative(this.opts.ws.root, path27.resolve(this.opts.planPath)));
10892
12576
  const status = await this.opts.git.raw().status();
10893
12577
  return status.files.map((file) => normalizeGitPath(file.path)).filter((file) => file.length > 0).filter((file) => !isRuntimeOnlyChange(file, planPath));
10894
12578
  }
10895
12579
  testGateArgsForStep(step) {
10896
12580
  if (step.phase === "FUNCTIONAL_TEST") return [];
10897
- return dedup3(
12581
+ return dedup5(
10898
12582
  step.outputs.filter((out) => typeof out === "string" && !out.endsWith("/")).map((out) => normalizeGitPath(out)).filter(isTestFilePath)
10899
12583
  );
10900
12584
  }
@@ -11062,7 +12746,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11062
12746
  initialDebug: {
11063
12747
  reason,
11064
12748
  failureLog: debugFailureLog,
11065
- contextPaths: dedup3([
12749
+ contextPaths: dedup5([
11066
12750
  ...sourceStep.inputs,
11067
12751
  ...sourceStep.outputs,
11068
12752
  ...routedTest.inputs,
@@ -11070,7 +12754,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11070
12754
  ...failedTest.inputs,
11071
12755
  ...failedTest.outputs
11072
12756
  ]),
11073
- extraAllowedWrites: dedup3([...routedTest.outputs]),
12757
+ extraAllowedWrites: dedup5([...routedTest.outputs]),
11074
12758
  contextMode: "test-rollback",
11075
12759
  testScopeArgs: this.testGateArgsForStep(routedTest),
11076
12760
  issueId: issue?.id,
@@ -11211,6 +12895,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11211
12895
  await this.debugCache.load();
11212
12896
  const initialDebug = opts.initialDebug;
11213
12897
  const inheritedExtraAllowedWrites = initialDebug?.extraAllowedWrites;
12898
+ const inheritedContextMode = initialDebug?.contextMode;
12899
+ const inheritedTestScopeArgs = initialDebug?.testScopeArgs;
11214
12900
  const completedBeforeDebug = initialDebug?.completedBeforeDebug ?? step.status === "DONE";
11215
12901
  const rootDebugFailureLog = initialDebug ? cleanFailureLogForDebugContext(initialDebug.failureLog) : void 0;
11216
12902
  const includeRootDebugFailureLog = (failureLog, reason) => rootDebugFailureLog ? composeDebugRetryFailureLog(rootDebugFailureLog, failureLog, reason) : failureLog;
@@ -11244,7 +12930,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11244
12930
  priorAttemptsPrompt: priorPrompt,
11245
12931
  contextPaths: initialDebug.contextPaths,
11246
12932
  extraAllowedWrites: inheritedExtraAllowedWrites,
11247
- contextMode: initialDebug.contextMode,
12933
+ contextMode: inheritedContextMode,
12934
+ testScopeArgs: inheritedTestScopeArgs,
11248
12935
  issueId: activeIssueId,
11249
12936
  completedBeforeDebug
11250
12937
  });
@@ -11252,6 +12939,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11252
12939
  const attempts = this.debugCache.attempts(step.id);
11253
12940
  const last = attempts.slice(-1)[0];
11254
12941
  const resume = latestActionableDebugAttempt(attempts) ?? last;
12942
+ const cachedTestScopeArgs = inferCachedTestScopeArgs(resume);
12943
+ const cachedContextMode = resume.contextMode ?? (cachedTestScopeArgs.length > 0 ? "test-rollback" : void 0);
11255
12944
  this.log(
11256
12945
  chalk2.yellow(
11257
12946
  t().engine.debugResumeNotice(step.id, attempts.length)
@@ -11279,6 +12968,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11279
12968
  reason: resume.reason,
11280
12969
  priorAttemptsPrompt: priorPrompt,
11281
12970
  extraAllowedWrites: inheritedExtraAllowedWrites,
12971
+ contextMode: cachedContextMode,
12972
+ testScopeArgs: cachedTestScopeArgs,
11282
12973
  issueId: activeIssueId,
11283
12974
  completedBeforeDebug
11284
12975
  });
@@ -11307,6 +12998,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11307
12998
  reason: resume.reason,
11308
12999
  priorAttemptsPrompt: priorPrompt,
11309
13000
  extraAllowedWrites: inheritedExtraAllowedWrites,
13001
+ contextMode: cachedContextMode,
13002
+ testScopeArgs: cachedTestScopeArgs,
11310
13003
  issueId: activeIssueId,
11311
13004
  completedBeforeDebug
11312
13005
  });
@@ -11357,6 +13050,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11357
13050
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(initialFailureLogForRecord), initialReason).map(
11358
13051
  (s) => `[${s.code}] ${s.hint}`
11359
13052
  ),
13053
+ contextMode: inheritedContextMode,
13054
+ testScopeArgs: inheritedTestScopeArgs,
11360
13055
  metrics: initial.metrics ? {
11361
13056
  healthScore: initial.metrics.healthScore,
11362
13057
  parseFailures: initial.metrics.parseFailures,
@@ -11428,6 +13123,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11428
13123
  reason: lastReason,
11429
13124
  priorAttemptsPrompt: priorPrompt,
11430
13125
  extraAllowedWrites: inheritedExtraAllowedWrites,
13126
+ contextMode: inheritedContextMode,
13127
+ testScopeArgs: inheritedTestScopeArgs,
11431
13128
  issueId: activeIssueId,
11432
13129
  completedBeforeDebug
11433
13130
  });
@@ -11483,6 +13180,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11483
13180
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(recordedFailureLog2), rollbackReason).map(
11484
13181
  (s) => `[${s.code}] ${s.hint}`
11485
13182
  ),
13183
+ contextMode: inheritedContextMode,
13184
+ testScopeArgs: inheritedTestScopeArgs,
11486
13185
  metrics: r.metrics ? {
11487
13186
  healthScore: r.metrics.healthScore,
11488
13187
  parseFailures: r.metrics.parseFailures,
@@ -11502,9 +13201,11 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11502
13201
  }
11503
13202
  const m = r.metrics;
11504
13203
  const readOnlyProbeLoop = isReadOnlyProbeLoopFailure(r.reason);
13204
+ const missingOutputStall = isMissingOutputStallFailure(r.reason);
13205
+ const lowQualityDebuggerResponse = isLowQualityDebuggerResponseFailure(r.reason);
11505
13206
  const repairEvidenceMissing = isRepairEvidenceMissingFailure(r.reason);
11506
- const healthy = !readOnlyProbeLoop && !repairEvidenceMissing && !!m && m.parseFailures === 0 && m.repeatedTurns <= 1 && m.healthScore >= 0.6;
11507
- const bad = readOnlyProbeLoop || repairEvidenceMissing || !!m && (m.healthScore < 0.3 || m.parseFailures + m.repeatedTurns >= Math.max(2, Math.ceil(m.rounds / 2)));
13207
+ const healthy = !readOnlyProbeLoop && !missingOutputStall && !lowQualityDebuggerResponse && !repairEvidenceMissing && !!m && m.parseFailures === 0 && m.repeatedTurns <= 1 && m.healthScore >= 0.6;
13208
+ const bad = readOnlyProbeLoop || missingOutputStall || lowQualityDebuggerResponse || repairEvidenceMissing || !!m && (m.healthScore < 0.3 || m.parseFailures + m.repeatedTurns >= Math.max(2, Math.ceil(m.rounds / 2)));
11508
13209
  const tag = m ? `health=${m.healthScore.toFixed(2)} parseFail=${m.parseFailures} repeat=${m.repeatedTurns} progress=${m.progressRatio.toFixed(2)}` : "";
11509
13210
  if (healthy) {
11510
13211
  const before = budget;
@@ -11555,6 +13256,8 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11555
13256
  suggestions: calibrateDebugSuggestions(cleanFailureLogForDebugContext(recordedFailureLog), r.reason ?? "").map(
11556
13257
  (s) => `[${s.code}] ${s.hint}`
11557
13258
  ),
13259
+ contextMode: inheritedContextMode,
13260
+ testScopeArgs: inheritedTestScopeArgs,
11558
13261
  metrics: m ? {
11559
13262
  healthScore: m.healthScore,
11560
13263
  parseFailures: m.parseFailures,
@@ -11674,9 +13377,75 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11674
13377
  `${iterationPrefix}/api-guide.md`
11675
13378
  ] : []
11676
13379
  ];
11677
- if (failedNames.has("entrypoint")) return dedup3([...codeAndTests, ...docs]);
11678
- if (failedNames.has("tests") || failedNames.has("test-files")) return dedup3([...codeAndTests, ...docs]);
11679
- return dedup3([...codeAndTests, ...step.inputs, ...docs]);
13380
+ if (failedNames.has("entrypoint")) return dedup5([...codeAndTests, ...docs]);
13381
+ if (failedNames.has("tests") || failedNames.has("test-files")) return dedup5([...codeAndTests, ...docs]);
13382
+ return dedup5([...codeAndTests, ...step.inputs, ...docs]);
13383
+ }
13384
+ async buildDebugPromptPayload(step, debug, failureLog) {
13385
+ const issue = debug.issueId ? this.findIssue(debug.issueId) : void 0;
13386
+ const currentBrief = buildDebugBrief({
13387
+ reason: debug.reason,
13388
+ failureLog,
13389
+ phase: issue?.phase ?? step.phase,
13390
+ targetPhase: issue?.targetPhase ?? step.phase
13391
+ });
13392
+ const rootBrief = issue?.debugBrief;
13393
+ const briefBlocks = rootBrief && rootBrief.summary !== currentBrief.summary ? [
13394
+ "## root issue brief",
13395
+ renderDebugBriefForPrompt(rootBrief),
13396
+ "",
13397
+ "## current retry brief",
13398
+ renderDebugBriefForPrompt(currentBrief)
13399
+ ] : [renderDebugBriefForPrompt(currentBrief)];
13400
+ const evidence = compactFailureEvidence({
13401
+ reason: debug.reason,
13402
+ failureLog,
13403
+ phase: issue?.phase ?? step.phase,
13404
+ targetPhase: issue?.targetPhase ?? step.phase,
13405
+ maxChars: 2600,
13406
+ maxLines: 50
13407
+ });
13408
+ const suggestions = renderDebugSuggestions(
13409
+ calibrateDebugSuggestions(failureLog, debug.reason)
13410
+ );
13411
+ const wikiMatches = await this.withDebugWiki(
13412
+ "search",
13413
+ () => this.debugWiki.search(currentBrief, { language: this.profile.id, limit: 3 }),
13414
+ []
13415
+ );
13416
+ const debugWikiEntryIds = wikiMatches.map((match) => match.entry.id);
13417
+ if (debugWikiEntryIds.length > 0) {
13418
+ debug.debugWikiEntryIds = dedup5([...debug.debugWikiEntryIds ?? [], ...debugWikiEntryIds]);
13419
+ if (issue) {
13420
+ issue.debugWikiEntryIds = dedup5([...issue.debugWikiEntryIds ?? [], ...debugWikiEntryIds]);
13421
+ }
13422
+ await this.withDebugWiki(
13423
+ "record-use",
13424
+ () => this.debugWiki.recordUse(debugWikiEntryIds, {
13425
+ brief: currentBrief,
13426
+ issueId: issue?.id,
13427
+ stepId: step.id,
13428
+ phase: step.phase,
13429
+ targetPhase: issue?.targetPhase,
13430
+ language: this.profile.id,
13431
+ solution: "retrieved for Debugger prompt"
13432
+ }),
13433
+ void 0
13434
+ );
13435
+ await this.opts.audit.event("note", `debug wiki matched ${debugWikiEntryIds.join(", ")}`, {
13436
+ messageId: "engine.debug_wiki_matched",
13437
+ entryIds: debugWikiEntryIds,
13438
+ stepId: step.id,
13439
+ phase: step.phase
13440
+ });
13441
+ }
13442
+ const wikiPrompt = renderDebugWikiMatchesForPrompt(wikiMatches);
13443
+ return {
13444
+ debugBrief: [briefBlocks.join("\n"), wikiPrompt].filter(Boolean).join("\n\n"),
13445
+ failureLog: evidence,
13446
+ suggestions,
13447
+ debugWikiEntryIds
13448
+ };
11680
13449
  }
11681
13450
  /** 一次执行尝试:可选 debug 模式(使用 Debugger 角色 + 注入失败日志)。 */
11682
13451
  async runOneAttempt(plan, step, debug) {
@@ -11690,6 +13459,9 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11690
13459
  });
11691
13460
  try {
11692
13461
  const outcome = await this.runOneAttemptCore(plan, step, debug);
13462
+ if (debug && !outcome.ok) {
13463
+ await this.recordDebugWikiFailure(step, debug, outcome);
13464
+ }
11693
13465
  await this.plugins.emit("step.attempt.after", {
11694
13466
  plan,
11695
13467
  step,
@@ -11705,6 +13477,9 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11705
13477
  failureLog: error instanceof Error ? error.stack ?? error.message : String(error),
11706
13478
  reason: error instanceof Error ? error.message : String(error)
11707
13479
  };
13480
+ if (debug) {
13481
+ await this.recordDebugWikiFailure(step, debug, outcome);
13482
+ }
11708
13483
  await this.plugins.emit("step.attempt.after", {
11709
13484
  plan,
11710
13485
  step,
@@ -11728,10 +13503,10 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11728
13503
  hints.push(`[debugger] ${dbg.prompt}`);
11729
13504
  }
11730
13505
  }
11731
- const allNames = dedup3([...resolvedToolNames, ...extraNames]);
13506
+ const allNames = dedup5([...resolvedToolNames, ...extraNames]);
11732
13507
  const baseTools = this.registry.pick(allNames);
11733
- const allowedWrites = debug ? dedup3([...this.computeDebugAllowedWrites(plan, step), ...debug.extraAllowedWrites ?? []]) : this.computeStepAllowedWrites(plan, step);
11734
- const augmentedWrites = this.isVModelTestPhase(step.phase) || step.phase === "DEBUG" || debug ? dedup3([...allowedWrites, "tests/fixtures"]) : allowedWrites;
13508
+ const allowedWrites = debug ? dedup5([...this.computeDebugAllowedWrites(plan, step), ...debug.extraAllowedWrites ?? []]) : this.computeStepAllowedWrites(plan, step);
13509
+ const augmentedWrites = this.isVModelTestPhase(step.phase) || step.phase === "DEBUG" || debug ? dedup5([...allowedWrites, "tests/fixtures"]) : allowedWrites;
11735
13510
  const budgetContext = {
11736
13511
  phase: step.phase,
11737
13512
  role,
@@ -11818,6 +13593,7 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11818
13593
  { animate: false }
11819
13594
  );
11820
13595
  const debugFailureLog = debug ? cleanFailureLogForDebugContext(debug.failureLog) : void 0;
13596
+ const debugPayload = debug ? await this.buildDebugPromptPayload(step, debug, debugFailureLog ?? debug.failureLog) : void 0;
11821
13597
  try {
11822
13598
  const r = await executor.run({
11823
13599
  step,
@@ -11827,14 +13603,15 @@ ${cleanFailureLogForDebugContext(sourceFailureLog)}` : ""
11827
13603
  contextSnippets: ctxSnippets,
11828
13604
  skillHints: hints,
11829
13605
  debugContext: debug ? {
13606
+ issueId: debug.issueId,
11830
13607
  reason: debug.reason,
11831
- failureLog: debugFailureLog ?? debug.failureLog,
13608
+ failureLog: debugPayload?.failureLog ?? debugFailureLog ?? debug.failureLog,
13609
+ debugBrief: debugPayload?.debugBrief,
11832
13610
  repairRequired: !debug.completedBeforeDebug,
11833
- suggestions: renderDebugSuggestions(
11834
- calibrateDebugSuggestions(debugFailureLog ?? debug.failureLog, debug.reason)
11835
- ) + (debug.priorAttemptsPrompt ? `
11836
-
11837
- ${debug.priorAttemptsPrompt}` : "")
13611
+ suggestions: [
13612
+ debugPayload?.suggestions,
13613
+ debug.priorAttemptsPrompt
13614
+ ].filter(Boolean).join("\n\n")
11838
13615
  } : void 0,
11839
13616
  globalPrompt: plan.globalPrompt,
11840
13617
  languageProfile: this.profile
@@ -12087,7 +13864,7 @@ ${pt.stderr}`);
12087
13864
  r.toolCalls
12088
13865
  ) : void 0;
12089
13866
  if (debug?.issueId) {
12090
- await this.markIssueResolved(debug.issueId, step, repair);
13867
+ await this.markIssueResolved(debug.issueId, step, repair, r.issueResolutionPlan);
12091
13868
  }
12092
13869
  await this.opts.git.snapshot(step.id, step.retries, debug ? "debug done" : "done");
12093
13870
  spin?.succeed(t().engine.phaseDone(step.id, r.rounds));
@@ -12096,9 +13873,12 @@ ${pt.stderr}`);
12096
13873
  rounds: r.rounds,
12097
13874
  retry: step.retries
12098
13875
  });
12099
- return { ok: true, failureLog: "" };
13876
+ return { ok: true, failureLog: "", issueResolutionPlan: r.issueResolutionPlan };
13877
+ }
13878
+ let reason = r.error ?? t().engine.outputsMissing(verify.missing.join(", "));
13879
+ if (debug?.completedBeforeDebug && !hasSuccessfulRepairMutation(r.toolCalls) && hasFailedVerificationEvidence(r.toolCalls)) {
13880
+ reason = "completed phase debug finished with failed verification but without a successful repair mutation";
12100
13881
  }
12101
- const reason = r.error ?? t().engine.outputsMissing(verify.missing.join(", "));
12102
13882
  const m = r.metrics;
12103
13883
  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;
12104
13884
  const failureLog = [
@@ -12132,7 +13912,7 @@ ${pt.stderr}`);
12132
13912
  } else {
12133
13913
  await this.opts.git.revertTo(sha);
12134
13914
  }
12135
- return { ok: false, failureLog, reason, metrics: m, issueKind: "phase" };
13915
+ return { ok: false, failureLog, reason, metrics: m, issueKind: "phase", issueResolutionPlan: r.issueResolutionPlan };
12136
13916
  } catch (err) {
12137
13917
  const msg = err.message;
12138
13918
  const stack = err.stack ?? msg;
@@ -12146,7 +13926,7 @@ ${pt.stderr}`);
12146
13926
  });
12147
13927
  return { ok: false, failureLog: stack, reason: msg, issueKind: "exception" };
12148
13928
  } finally {
12149
- void path26;
13929
+ void path27;
12150
13930
  }
12151
13931
  }
12152
13932
  async buildContextSnippets(plan, step, debug) {
@@ -12203,7 +13983,7 @@ ${pt.stderr}`);
12203
13983
  if (downstream) {
12204
13984
  out.set(`.xcompiler/downstream/${step.id}.md`, downstream);
12205
13985
  }
12206
- return [...out.entries()].map(([path30, content]) => ({ path: path30, content }));
13986
+ return [...out.entries()].map(([path31, content]) => ({ path: path31, content }));
12207
13987
  }
12208
13988
  findOwningTestStepForFailure(plan, currentStep, failureText) {
12209
13989
  const failedPaths = extractFailedTestPaths(failureText);
@@ -12326,7 +14106,7 @@ ${pt.stderr}`);
12326
14106
  ].join("\n").length;
12327
14107
  }
12328
14108
  };
12329
- function dedup3(arr) {
14109
+ function dedup5(arr) {
12330
14110
  return [...new Set(arr)];
12331
14111
  }
12332
14112
  function isDesignSourcePhase(phase) {
@@ -12358,6 +14138,9 @@ var REPAIR_VERIFICATION_TOOLS = /* @__PURE__ */ new Set([
12358
14138
  function hasSuccessfulVerificationEvidence(toolCalls) {
12359
14139
  return toolCalls.some((call) => call.ok && REPAIR_VERIFICATION_TOOLS.has(call.tool));
12360
14140
  }
14141
+ function hasFailedVerificationEvidence(toolCalls) {
14142
+ return toolCalls.some((call) => !call.ok && REPAIR_VERIFICATION_TOOLS.has(call.tool));
14143
+ }
12361
14144
  function shouldRollbackTestPhaseFromToolFailures(step, toolCalls) {
12362
14145
  let unresolvedTestFailure = false;
12363
14146
  for (const call of toolCalls) {
@@ -12477,32 +14260,65 @@ function extractFailedTestPaths(text) {
12477
14260
  if (file && isTestFilePath(file)) found.push(file);
12478
14261
  }
12479
14262
  }
12480
- return dedup3(found);
14263
+ return dedup5(found);
12481
14264
  }
12482
14265
  function isNonDebuggableInfrastructureFailure(reason, failureLog) {
12483
14266
  const text = `${reason ?? ""}
12484
14267
  ${failureLog ?? ""}`.toLowerCase();
12485
- 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);
14268
+ if (/low-quality debugger response/u.test(text)) return false;
14269
+ 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);
12486
14270
  }
12487
14271
  function isReadOnlyProbeLoopFailure(reason) {
12488
14272
  return /repeated read-only\/probe actions without progress/u.test(reason ?? "") || /read-only recovery mode repeated probe actions/u.test(reason ?? "");
12489
14273
  }
14274
+ function isMissingOutputStallFailure(reason) {
14275
+ return /write\/progress actions did not reduce missing outputs/u.test(reason ?? "");
14276
+ }
14277
+ function isLowQualityDebuggerResponseFailure(reason) {
14278
+ return /low-quality debugger response/u.test(reason ?? "");
14279
+ }
12490
14280
  function isRepairEvidenceMissingFailure(reason) {
12491
- return /without repair evidence/u.test(reason ?? "") || /without a successful repair mutation or verification tool call/u.test(reason ?? "");
14281
+ 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 ?? "");
12492
14282
  }
12493
14283
  function shouldRollbackTestPhaseFailure(reason, failureLog) {
12494
14284
  const text = `${reason ?? ""}
12495
14285
  ${failureLog ?? ""}`.toLowerCase();
12496
14286
  if (isReadOnlyProbeLoopFailure(reason) || isRepairEvidenceMissingFailure(reason)) return false;
14287
+ if (/tool verification failed.*rolling back to paired/u.test(text)) return true;
12497
14288
  if (isCachedTestArtifactDiscoveryFailure(text)) return false;
12498
14289
  if (/missing outputs|outputs? 校验|verify outputs|declared outputs|产物/u.test(text)) return false;
12499
14290
  if (/invalid action|args must be an object|invalid json|parse failure/u.test(text)) return false;
12500
- if (/tool verification failed.*rolling back to paired/u.test(text)) return true;
12501
- if (/test gate|functional gate|functional entry probe|entry probe|delivery gate/u.test(text)) return true;
14291
+ if (/test gate|functional gate|functional entry probe|entry probe|delivery gate|测试门禁|功能门禁|功能入口探测|入口探测|交付门禁/u.test(text)) return true;
12502
14292
  if (/pytest exit=\s*[1-9]\d*|exit code\s*[1-9]\d*|failed tests?|test failures?/u.test(text)) return true;
12503
14293
  if (/assertionerror|failed:\s+did not|traceback \(most recent call last\)/u.test(text)) return true;
12504
14294
  return false;
12505
14295
  }
14296
+ function inferCachedTestScopeArgs(entry) {
14297
+ const explicit = (entry.testScopeArgs ?? []).map(normalizeGitPath).filter(isTestFilePath);
14298
+ if (explicit.length > 0) return dedup5(explicit);
14299
+ const text = [
14300
+ entry.failureLogTail,
14301
+ entry.debugBrief?.toolFailures?.join("\n") ?? "",
14302
+ entry.debugBrief?.evidence?.join("\n") ?? ""
14303
+ ].filter(Boolean).join("\n");
14304
+ const fromRunTestsArgs = extractRunTestsArgs(text);
14305
+ if (fromRunTestsArgs.length > 0) return fromRunTestsArgs;
14306
+ const failedPaths = extractFailedTestPaths(text);
14307
+ if (failedPaths.length > 0) return failedPaths;
14308
+ return dedup5((entry.debugBrief?.files ?? []).map(normalizeGitPath).filter(isTestFilePath));
14309
+ }
14310
+ function extractRunTestsArgs(text) {
14311
+ const out = [];
14312
+ for (const match of text.matchAll(/\brun_tests[^\n]*\bargs=([^\n]+)/giu)) {
14313
+ const raw = match[1] ?? "";
14314
+ for (const token of raw.split(/\s+/u)) {
14315
+ const cleaned = token.replace(/^["'`]+|["'`,;]+$/gu, "");
14316
+ const normalized = normalizeGitPath(cleaned);
14317
+ if (isTestFilePath(normalized)) out.push(normalized);
14318
+ }
14319
+ }
14320
+ return dedup5(out);
14321
+ }
12506
14322
  function isCachedTestArtifactDiscoveryFailure(text) {
12507
14323
  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);
12508
14324
  }
@@ -12558,6 +14374,13 @@ function inferRepairMode(toolCalls) {
12558
14374
  if (usedRewrite) return "rewrite";
12559
14375
  return "verification";
12560
14376
  }
14377
+ function parsePatchChangedFiles(diff) {
14378
+ const files = [];
14379
+ for (const match of diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gmu)) {
14380
+ files.push(normalizeGitPath(match[2] ?? match[1] ?? ""));
14381
+ }
14382
+ return dedup5(files.filter(Boolean));
14383
+ }
12561
14384
 
12562
14385
  // src/runtime/run.ts
12563
14386
  async function runExecute(opts) {
@@ -12566,7 +14389,7 @@ async function runExecute(opts) {
12566
14389
  io.interaction?.pauseStdin?.();
12567
14390
  } catch {
12568
14391
  }
12569
- const ws = new Workspace(path27.resolve(opts.workspace));
14392
+ const ws = new Workspace(path28.resolve(opts.workspace));
12570
14393
  const { config: cfg, path: cfgPath } = await loadConfigWithPath(opts.configPath);
12571
14394
  if (!hasXcEnv("LANG")) setLocale(cfg.locale);
12572
14395
  let lock;
@@ -12736,6 +14559,8 @@ async function runExecute(opts) {
12736
14559
  maxEditLinesPerStep: cfg.agent.max_edit_lines_per_step,
12737
14560
  maxWriteChunkBytes: cfg.agent.max_write_chunk_bytes,
12738
14561
  terminalOutput: opts.terminalOutput ?? true,
14562
+ debugWikiPath: opts.debugWikiPath ? path28.resolve(opts.debugWikiPath) : void 0,
14563
+ debugWikiStrict: !!opts.debugWikiPath,
12739
14564
  requestPermission: io.requestPermission ? async (request) => {
12740
14565
  await io.emit({ type: "permission", status: "requested", request });
12741
14566
  const decision = await io.requestPermission(request);
@@ -12980,15 +14805,15 @@ async function emitProjectAudit(io, result) {
12980
14805
  }
12981
14806
 
12982
14807
  // src/runtime/workspace.ts
12983
- import path28 from "path";
12984
- import { promises as fs25 } from "fs";
14808
+ import path29 from "path";
14809
+ import { promises as fs27 } from "fs";
12985
14810
 
12986
14811
  // src/runtime/commands.ts
12987
14812
  async function runRunCommand(opts) {
12988
14813
  const cwd = opts.cwd ?? process.cwd();
12989
14814
  const explicit = opts.output ?? opts.workspace;
12990
- const workspace = explicit ? path29.resolve(explicit) : opts.planArg ? path29.dirname(path29.resolve(opts.planArg)) : cwd;
12991
- const planPath = opts.planArg ? path29.resolve(opts.planArg) : await defaultRunnablePlanPath(workspace);
14815
+ const workspace = explicit ? path30.resolve(explicit) : opts.planArg ? path30.dirname(path30.resolve(opts.planArg)) : cwd;
14816
+ const planPath = opts.planArg ? path30.resolve(opts.planArg) : await defaultRunnablePlanPath(workspace);
12992
14817
  return runExecute({
12993
14818
  ...opts,
12994
14819
  workspace,
@@ -12997,15 +14822,15 @@ async function runRunCommand(opts) {
12997
14822
  });
12998
14823
  }
12999
14824
  async function defaultRunnablePlanPath(workspace) {
13000
- const phasePlanPath = path29.join(workspace, DEFAULT_PHASE_PLAN_FILE);
14825
+ const phasePlanPath = path30.join(workspace, DEFAULT_PHASE_PLAN_FILE);
13001
14826
  if (await fileExists2(phasePlanPath)) return phasePlanPath;
13002
- const legacyPlanPath = path29.join(workspace, DEFAULT_PLAN_FILE);
14827
+ const legacyPlanPath = path30.join(workspace, DEFAULT_PLAN_FILE);
13003
14828
  if (await fileExists2(legacyPlanPath)) return legacyPlanPath;
13004
14829
  return phasePlanPath;
13005
14830
  }
13006
14831
  async function fileExists2(filePath) {
13007
14832
  try {
13008
- await fs26.stat(filePath);
14833
+ await fs28.stat(filePath);
13009
14834
  return true;
13010
14835
  } catch {
13011
14836
  return false;
@@ -13184,7 +15009,7 @@ configureLocalizedHelp(program);
13184
15009
  program.name("xcompiler_run").description(t().cli.runDescription).version(XCOMPILER_VERSION, "-V, --version", t().cli.versionOption).option("--lang <code>", t().cli.optLang, parseLocale).hook("preAction", (cmd) => {
13185
15010
  const l = cmd.opts().lang;
13186
15011
  if (l) setLocale(l);
13187
- }).argument("[plan]", t().cli.argPlan).option("-o, --output <dir>", t().cli.optOutput).option("-w, --workspace <dir>", t().cli.optWorkspace).option("-c, --config <file>", t().cli.optConfig).option("--dry-run", t().cli.optDryRun, false).option("--from <stepId>", t().cli.optFrom, parseStepId).option("--phase <phase>", t().cli.optPhase, parsePhase).option("--reset", t().cli.optReset, false).option("--force", t().cli.optForce, false).option("--project-file <file>", t().cli.optProjectFile).action(async (planArg, opts) => {
15012
+ }).argument("[plan]", t().cli.argPlan).option("-o, --output <dir>", t().cli.optOutput).option("-w, --workspace <dir>", t().cli.optWorkspace).option("-c, --config <file>", t().cli.optConfig).option("--dry-run", t().cli.optDryRun, false).option("--from <stepId>", t().cli.optFrom, parseStepId).option("--phase <phase>", t().cli.optPhase, parsePhase).option("--reset", t().cli.optReset, false).option("--force", t().cli.optForce, false).option("--project-file <file>", t().cli.optProjectFile).option("--debug-wiki-path <dir>", t().cli.optDebugWikiPath).action(async (planArg, opts) => {
13188
15013
  const result = await runRunCommand({
13189
15014
  planArg,
13190
15015
  output: opts.output,
@@ -13196,6 +15021,7 @@ program.name("xcompiler_run").description(t().cli.runDescription).version(XCOMPI
13196
15021
  resetStatus: !!opts.reset,
13197
15022
  force: !!opts.force,
13198
15023
  projectFilePath: opts.projectFile,
15024
+ debugWikiPath: opts.debugWikiPath,
13199
15025
  cwd: process.cwd(),
13200
15026
  io: createCliRuntimeIO()
13201
15027
  });