@tea-agent/loop-agent 0.37.0 → 0.37.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +71 -144
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/shared/operator/capabilities.js +13 -200
  18. package/dist/shared/package-metadata.js +0 -42
  19. package/dist/task/contract/project.js +0 -4
  20. package/dist/task/source-prepare/completeness.js +0 -53
  21. package/dist/task/source-prepare/parse-intent.js +12 -82
  22. package/dist/task/source-prepare/prepare.js +5 -102
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -152
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -111
  29. package/dist/worker/console/chat/routes.js +10 -43
  30. package/dist/worker/console/chat/session-store.js +1 -9
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -204
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +14 -391
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -114
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -67
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +3 -10
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  70. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  71. package/dist/workflows/dag/frontend-repair.js +18 -219
  72. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  73. package/dist/workflows/dag/init-hybrid.js +82 -165
  74. package/dist/workflows/dag/node-execution.js +0 -89
  75. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  76. package/dist/workflows/dag/runner.js +15 -333
  77. package/dist/workflows/dag/scheduler.js +3 -257
  78. package/dist/workflows/dag/types.js +9 -130
  79. package/docs/README.md +3 -3
  80. package/docs/architecture/evolution.md +67 -102
  81. package/docs/templates/backend-test-dag.json +50 -32
  82. package/docs/templates/init-managed-agents.md +2 -5
  83. package/harness.json +2 -2
  84. package/package.json +3 -9
  85. package/skills/loop-agent/SKILL.md +0 -1
  86. package/skills/loop-agent/references/command-reference.md +0 -4
  87. package/dist/build-stamp.json +0 -6
  88. package/dist/commands/dag-request-interrupt.js +0 -20
  89. package/dist/worker/console/chat/assistant-content.js +0 -121
  90. package/dist/worker/console/chat/semantic-activity.js +0 -477
  91. package/dist/worker/console/operation-run-facts.js +0 -190
  92. package/dist/worker/console/operation-wait.js +0 -355
  93. package/dist/worker/console/recovery-error-copy.js +0 -198
  94. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +0 -1
  95. package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +0 -1
  96. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +0 -36
  97. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +0 -132
  98. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +0 -10
  99. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  100. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +0 -1
  101. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +0 -1
  102. package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +0 -231
  103. package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +0 -15
  104. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +0 -88
  105. package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +0 -206
  106. package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +0 -1
  107. package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +0 -1
  108. package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +0 -1
  109. package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +0 -1
  110. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  112. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +0 -1
  113. package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +0 -166
  114. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +0 -62
  115. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +0 -321
  116. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +0 -4
  117. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +0 -1
  118. package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +0 -30
  119. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +0 -3
  120. package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +0 -24
  121. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +0 -24
  122. package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +0 -41
  123. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +0 -1
  124. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +0 -85
  125. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +0 -156
  126. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +0 -292
  127. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +0 -106
  128. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +0 -1
  129. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  130. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  131. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +0 -2
  132. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +0 -1
  133. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +0 -70
  134. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +0 -139
  135. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +0 -89
  136. package/dist/worker/console/static/assets/layout-D-LzfAck.js +0 -1
  137. package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +0 -1
  138. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +0 -1
  139. package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +0 -308
  140. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +0 -96
  141. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +0 -1
  142. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +0 -1
  143. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +0 -39
  144. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +0 -7
  145. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +0 -1
  146. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +0 -84
  147. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +0 -40
  148. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +0 -162
  149. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +0 -1
  150. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +0 -1
  151. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  152. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +0 -2
  153. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
  154. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +0 -120
  155. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +0 -34
  156. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +0 -78
  157. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +0 -7
  158. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  159. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  160. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  161. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  162. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  163. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  164. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  165. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  166. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  167. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  168. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  169. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  170. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  171. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  172. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  173. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  174. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  175. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  176. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  177. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  178. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  179. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  180. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  181. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  182. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  183. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  184. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  185. package/dist/workflows/dag/contract-output-registry.js +0 -14
  186. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  187. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  188. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  189. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  190. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  191. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  192. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -1,6 +1,5 @@
1
1
  import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
2
2
  import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
3
- import { createRunDagProgressObserver, validateRunDagProgressIntervalMs, } from "./run-dag-progress.js";
4
3
  const COMMAND = "task advance";
5
4
  const USAGE = `usage:
6
5
  task advance <task-id> [title]
@@ -25,8 +24,6 @@ const USAGE = `usage:
25
24
  [--dag-output <path>]
26
25
  [--skip-finalize]
27
26
  [--no-strict-models]
28
- [--quiet]
29
- [--progress-interval-ms <ms>]
30
27
  [--dry-run]
31
28
  [--json]`;
32
29
  function pushList(target, value) {
@@ -173,19 +170,6 @@ export function parseTaskAdvanceArgs(args) {
173
170
  options.strictModels = false;
174
171
  continue;
175
172
  }
176
- if (token === "--quiet") {
177
- options.quiet = true;
178
- continue;
179
- }
180
- if (token === "--progress-interval-ms") {
181
- const raw = next();
182
- const parsed = Number(raw);
183
- if (!Number.isFinite(parsed)) {
184
- throw new Error(`progress-interval-ms must be an integer >= 1000\n${USAGE}`);
185
- }
186
- options.progressIntervalMs = validateRunDagProgressIntervalMs(parsed);
187
- continue;
188
- }
189
173
  if (token === "--help" || token === "-h") {
190
174
  throw new Error(USAGE);
191
175
  }
@@ -224,7 +208,7 @@ function mapOutcome(result) {
224
208
  return "blocked";
225
209
  return "succeeded";
226
210
  }
227
- export function toUseCaseInput(repoRoot, taskId, options, observer) {
211
+ function toUseCaseInput(repoRoot, taskId, options) {
228
212
  const timeouts = new Map((options.verifyTimeout ?? []).map((entry) => {
229
213
  const parsed = parseVerifyTimeout(entry);
230
214
  return [parsed.label, parsed.timeoutMs];
@@ -287,8 +271,7 @@ export function toUseCaseInput(repoRoot, taskId, options, observer) {
287
271
  dagOutputPath: options.dagOutputPath,
288
272
  skipFinalize: options.skipFinalize,
289
273
  strictModels: options.strictModels,
290
- ...(observer ? { observer } : {}),
291
- onProgress: options.quiet
274
+ onProgress: options.json
292
275
  ? undefined
293
276
  : (message) => {
294
277
  process.stderr.write(`[task advance] ${message}\n`);
@@ -313,18 +296,8 @@ export async function runTaskAdvance(repoRoot, args) {
313
296
  process.exitCode = processExitCodeForOutcome(envelope.outcome);
314
297
  return;
315
298
  }
316
- let progress;
317
299
  try {
318
- // AC-006/AC-007: periodic DAG progress goes to stderr only (stdout stays
319
- // the single final OperatorCommandResultV1 JSON). The observer is created
320
- // ONLY for the approve-gate execution path and disposed on every exit;
321
- // its timer starts only after onRunStart (double guard, no stray timer).
322
- if (options.approveGate && !options.dryRun && !options.quiet) {
323
- progress = createRunDagProgressObserver({
324
- intervalMs: options.progressIntervalMs,
325
- });
326
- }
327
- const result = await advanceTaskLifecycle(toUseCaseInput(repoRoot, taskId, options, progress?.observer));
300
+ const result = await advanceTaskLifecycle(toUseCaseInput(repoRoot, taskId, options));
328
301
  const outcome = mapOutcome(result);
329
302
  const gateStop = result.lifecycleState === "awaiting-write-set-approval" &&
330
303
  result.blockers.length === 0;
@@ -360,7 +333,4 @@ export async function runTaskAdvance(repoRoot, args) {
360
333
  writeOperatorJson(envelope);
361
334
  process.exitCode = processExitCodeForOutcome(envelope.outcome);
362
335
  }
363
- finally {
364
- progress?.dispose();
365
- }
366
336
  }
@@ -194,7 +194,7 @@ export function buildDagPiUserMessage(task, persona, step) {
194
194
  if (isDagPiWriteTask(task)) {
195
195
  const outcomeInstruction = task.writerOutcomePolicy
196
196
  ? [
197
- "The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. The runner measures your diff mechanically from git status snapshots taken before and after this node: code pasted into the response text is NOT an implementation and yields an empty diff. Use changed only after actually calling write/edit tools that persist files to disk; use already-satisfied only when the contract is already met and no file changed; use blocked when implementation cannot proceed. Claiming changed without a persisted diff fails this node as invalid-output.",
197
+ "The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. Use changed only after producing a non-empty bounded diff; use already-satisfied only when the contract is already met and no file changed; use blocked when implementation cannot proceed.",
198
198
  task.writerOutcomePolicy.requireChangedFiles
199
199
  ? "This generation node requires a non-empty bounded diff; already-satisfied cannot complete it successfully."
200
200
  : undefined,
@@ -206,8 +206,6 @@ export function buildDagPiUserMessage(task, persona, step) {
206
206
  `You are executing hybrid DAG node "${task.id}" (role=${role}, piStep=${step}, writePolicy=${writePolicy}).`,
207
207
  "The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
208
208
  "Use write tools only for the bounded implementation requested by this node.",
209
- "The upstream plan node's output is a BLUEPRINT, not a deliverable: reading it never satisfies this node. You MUST materialize the plan into actual working-tree changes by calling write/edit tools before claiming changed. A detailed plan that names every file, test, or snippet still requires you to write those files yourself.",
210
- "Forbidden paths bound the writeSet, not the task: when the task only requires writing inside the writeSet (e.g. adding tests that call existing public APIs), a forbidden src/** does not make the task impossible — do NOT claim blocked merely because some path you never need to write is forbidden. blocked is only for when you genuinely cannot produce the required writeSet changes.",
211
209
  outcomeInstruction,
212
210
  `Allowed paths: ${(task.allowedPaths ?? []).join(", ") || "(none declared)"}.`,
213
211
  `Write set: ${(task.writeSet ?? []).join(", ") || "(none declared)"}.`,
@@ -522,33 +520,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
522
520
  }
523
521
  let result;
524
522
  try {
525
- // Phase 5 (P1-9): capture the writeSet baseline BEFORE the writer provider
526
- // call so a transient partial write can be rolled back and recovered via a
527
- // staged child. Only frontend-implementation writers; a failed capture is a
528
- // hard fail (no baseline → no rollback → no recovery). Dynamic import avoids
529
- // an executor ↔ scheduler static cycle.
530
- if (isWriteTask &&
531
- (input.task.id === "frontend-implement-pi" ||
532
- input.task.id === "frontend-repair-pi") &&
533
- (input.task.writeSet?.length ?? 0) > 0) {
534
- try {
535
- const { captureFrontendWriterAttemptIntent } = await import("../workflows/dag/frontend-writer-recovery.js");
536
- await captureFrontendWriterAttemptIntent({
537
- cwd: input.cwd,
538
- runDir: meta.runDir,
539
- writeSet: input.task.writeSet,
540
- });
541
- }
542
- catch (error) {
543
- return {
544
- ok: false,
545
- stdout: "",
546
- stderr: `frontend writer attempt baseline capture failed: ${error instanceof Error ? error.message : String(error)}`,
547
- failureCategory: "write-guard",
548
- durationMs: Date.now() - started,
549
- };
550
- }
551
- }
552
523
  result = await piStepFn({
553
524
  attachedFiles: [],
554
525
  modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
@@ -720,23 +691,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
720
691
  const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
721
692
  changeManifestAfterStatus = afterStatus;
722
693
  changeManifestChangedFiles = changedFiles;
723
- // Phase 5: record the attempt changed paths/hashes into the rollback
724
- // intent so a transient partial write can be CAS-restored.
725
- if ((input.task.id === "frontend-implement-pi" ||
726
- input.task.id === "frontend-repair-pi") &&
727
- (input.task.writeSet?.length ?? 0) > 0) {
728
- try {
729
- const { recordFrontendWriterAttempt } = await import("../workflows/dag/frontend-writer-recovery.js");
730
- await recordFrontendWriterAttempt({
731
- cwd: input.cwd,
732
- runDir: meta.runDir,
733
- changedPaths: changedFiles,
734
- });
735
- }
736
- catch {
737
- // best-effort: a missing intent (no baseline captured) is a no-op
738
- }
739
- }
740
694
  const guard = validateShellWriteGuardFromDiff({
741
695
  changedFiles,
742
696
  task: input.task,
@@ -977,6 +931,15 @@ function parseWriterImplementationOutcome(text) {
977
931
  const lines = text.split(/\r?\n/);
978
932
  const candidates = [];
979
933
  for (const [lineIndex, line] of lines.entries()) {
934
+ const localizedOutcome = localizedWriterImplementationOutcome(line);
935
+ if (localizedOutcome) {
936
+ candidates.push({
937
+ lineIndex,
938
+ value: localizedOutcome,
939
+ outcome: localizedOutcome,
940
+ });
941
+ continue;
942
+ }
980
943
  const nextLine = lines[lineIndex + 1];
981
944
  const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
982
945
  if (normalized === undefined)
@@ -1005,6 +968,26 @@ function parseWriterImplementationOutcome(text) {
1005
968
  candidates,
1006
969
  };
1007
970
  }
971
+ function localizedWriterImplementationOutcome(line) {
972
+ const normalized = normalizedProtocolValue(line);
973
+ const match = /^(?:实现|实施)结果\s*[::]\s*(已变更|已更改|已修改|已满足|无需变更|已阻塞|阻塞)$/u.exec(normalized);
974
+ if (!match)
975
+ return undefined;
976
+ switch (match[1]) {
977
+ case "已变更":
978
+ case "已更改":
979
+ case "已修改":
980
+ return "changed";
981
+ case "已满足":
982
+ case "无需变更":
983
+ return "already-satisfied";
984
+ case "已阻塞":
985
+ case "阻塞":
986
+ return "blocked";
987
+ default:
988
+ return undefined;
989
+ }
990
+ }
1008
991
  function isWriterImplementationOutcome(value) {
1009
992
  return (value === "changed" || value === "already-satisfied" || value === "blocked");
1010
993
  }
@@ -78,40 +78,6 @@ export async function checkPiSdkAvailability(_repoRoot) {
78
78
  return { ok: false, detail: `pi SDK not available: ${message}` };
79
79
  }
80
80
  }
81
- /**
82
- * Best-effort Pi SDK model catalog (credentialed providers first).
83
- * Empty on SDK/auth/runtime failure — callers must fall back to harness tiers.
84
- */
85
- export async function listPiSdkAvailableModels() {
86
- try {
87
- const imported = sdkImportOverrideForTests
88
- ? await sdkImportOverrideForTests()
89
- : await loadPiSdkModule();
90
- const sdk = imported;
91
- const getAgentDir = sdk.getAgentDir;
92
- const ModelRuntime = sdk.ModelRuntime;
93
- if (typeof getAgentDir !== "function" || typeof ModelRuntime?.create !== "function") {
94
- return [];
95
- }
96
- const agentDir = getAgentDir();
97
- const runtime = await ModelRuntime.create({
98
- authPath: path.join(agentDir, "auth.json"),
99
- modelsPath: path.join(agentDir, "models.json"),
100
- });
101
- const snapshot = runtime.snapshot ?? {};
102
- const configured = snapshot.configuredProviders;
103
- const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
104
- return (snapshot.available ?? []).map((entry) => ({
105
- provider: entry.provider,
106
- id: entry.id,
107
- name: entry.name,
108
- hasCredentials: isConfigured(entry.provider),
109
- }));
110
- }
111
- catch {
112
- return [];
113
- }
114
- }
115
81
  /** Fail-closed probe for the SDK-only structured custom-tool surface. */
116
82
  export async function checkPiSdkCustomToolCapability(_repoRoot) {
117
83
  if (sdkSessionFactoryOverride) {
@@ -31,7 +31,7 @@ import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorresp
31
31
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
32
32
  import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
33
33
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
34
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
34
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
35
35
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
36
36
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
37
37
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -678,6 +678,42 @@ async function executeBackendTestPipeline(input, meta) {
678
678
  fixtureResolution,
679
679
  });
680
680
  }
681
+ try {
682
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
683
+ workspaceRoot: input.cwd,
684
+ taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
685
+ });
686
+ const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
687
+ const contractsDir = path.join(meta.runDir, "contracts");
688
+ await mkdir(contractsDir, { recursive: true });
689
+ const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
690
+ await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
691
+ outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
692
+ if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
693
+ const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
694
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
695
+ .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
696
+ .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
697
+ if (correspondenceRepairPaths.length > 0) {
698
+ facts.status = "REPAIRABLE";
699
+ facts.repairEligible = true;
700
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
701
+ facts.findings.push({
702
+ kind: "markdown-pytest-correspondence",
703
+ classification: "test-asset-defect",
704
+ repairability: "repairable",
705
+ detail: correspondence.facts.entries
706
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
707
+ .flatMap((entry) => entry.findings)
708
+ .join("; ")
709
+ .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
710
+ });
711
+ }
712
+ }
713
+ }
714
+ catch (error) {
715
+ outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
716
+ }
681
717
  const artifacts = await writeBackendPytestCollectionArtifacts({
682
718
  runDir: meta.runDir,
683
719
  stem: "initial",
@@ -776,12 +812,56 @@ async function executeBackendTestPipeline(input, meta) {
776
812
  facts: effective,
777
813
  });
778
814
  const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
815
+ const correspondence = meta.spec.sourceBinding
816
+ ? await analyzeBackendTestMarkdownPytestCorrespondence({
817
+ workspaceRoot: input.cwd,
818
+ taskId: meta.spec.sourceBinding.taskId,
819
+ })
820
+ : undefined;
821
+ const eligibility = correspondence
822
+ ? buildBackendTestItemEligibility(effective.collectedItemIds, {
823
+ correspondenceEntries: correspondence.facts.entries,
824
+ scenarioEntries: scenarioFacts.entries,
825
+ })
826
+ : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
827
+ const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
828
+ const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
829
+ await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
830
+ await writeFile(eligibilityFactsPath, `${JSON.stringify({
831
+ schemaId: "backend-test-execution-eligibility-v1",
832
+ status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
833
+ collectedItemCount: effective.collectedItemIds.length,
834
+ eligibleItemCount: eligibility.eligibleItemIds.length,
835
+ excludedItemCount: eligibility.excludedItems.length,
836
+ eligibleItemIds: eligibility.eligibleItemIds,
837
+ excludedItems: eligibility.excludedItems,
838
+ inputHashes: eligibilityInputHashes,
839
+ }, null, 2)}\n`, "utf8");
840
+ const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
841
+ "# Backend Test Execution Eligibility",
842
+ "",
843
+ "## Status",
844
+ "",
845
+ eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
846
+ "",
847
+ `- Collected items: ${effective.collectedItemIds.length}`,
848
+ `- Eligible items: ${eligibility.eligibleItemIds.length}`,
849
+ `- Excluded items: ${eligibility.excludedItems.length}`,
850
+ "",
851
+ "## Excluded Items",
852
+ "",
853
+ ...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
854
+ "",
855
+ ].join("\n"));
856
+ outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
779
857
  const readiness = await materializeBackendTestExecutionReadiness({
780
858
  runDir: meta.runDir,
781
859
  workspaceRoot: input.cwd,
782
860
  effective,
783
861
  scenarioParamStatus: scenarioFacts.overallStatus,
784
862
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
863
+ eligibility,
864
+ eligibilityInputHashes,
785
865
  });
786
866
  return {
787
867
  ok: true,
@@ -792,6 +872,8 @@ async function executeBackendTestPipeline(input, meta) {
792
872
  collectedItemCount: effective.collectedItemCount,
793
873
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
794
874
  executionReadinessStatus: readiness.status,
875
+ eligibleItemCount: readiness.eligibleItemIds.length,
876
+ excludedItemCount: readiness.excludedItems.length,
795
877
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
796
878
  readinessPath: "contracts/backend-test-execution-readiness.json",
797
879
  reportPath: artifacts.reportPath,
@@ -889,8 +971,11 @@ async function executeBackendTestPipeline(input, meta) {
889
971
  const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
890
972
  await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
891
973
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
892
- const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
893
- const pytestTargets = mappedScripts.map(shellQuote).join(" ");
974
+ const eligibleItemIds = executionReadiness.eligibleItemIds;
975
+ const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
976
+ const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
977
+ await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
978
+ const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
894
979
  // Split into short bash -c commands (aligned with markdown-environment).
895
980
  // A single ultra-long compound command has been observed on Windows to
896
981
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -967,8 +1052,10 @@ async function executeBackendTestPipeline(input, meta) {
967
1052
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
968
1053
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
969
1054
  'echo "STEP=pytest-run"',
970
- `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
971
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
1055
+ `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1056
+ `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1057
+ `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1058
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
972
1059
  "STATUS=$?",
973
1060
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
974
1061
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -1057,7 +1144,9 @@ async function executeBackendTestPipeline(input, meta) {
1057
1144
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1058
1145
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1059
1146
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
1060
- const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1147
+ const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1148
+ const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1149
+ const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1061
1150
  const htmlContent = renderBackendTestHtml({
1062
1151
  title: meta.spec.title,
1063
1152
  parsed,