@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1

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 (101) hide show
  1. package/CHANGELOG.md +67 -16
  2. package/dist/application/task-lifecycle/advance.js +309 -9
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  6. package/dist/commands/init-upgrade.js +32 -1
  7. package/dist/commands/init.js +94 -3
  8. package/dist/executors/dag-pi-executor.js +18 -3
  9. package/dist/executors/shell-executor.js +4 -91
  10. package/dist/executors/shell-write-guard.js +26 -8
  11. package/dist/shared/operator/capabilities.js +98 -44
  12. package/dist/shared/resilient-git.js +133 -0
  13. package/dist/task/source-prepare/artifact-meta.js +137 -0
  14. package/dist/task/source-prepare/index.js +2 -0
  15. package/dist/task/source-prepare/parse-intent.js +58 -10
  16. package/dist/task/source-prepare/prepare.js +229 -18
  17. package/dist/task/source-prepare/reference-integrity.js +18 -2
  18. package/dist/task/source-prepare/semantic-intake.js +404 -0
  19. package/dist/worker/console/app-data.js +2 -0
  20. package/dist/worker/console/chat/chat-event-store.js +190 -25
  21. package/dist/worker/console/chat/model-resolver.js +17 -0
  22. package/dist/worker/console/chat/pi-console-config.js +250 -32
  23. package/dist/worker/console/chat/pi-runtime.js +1007 -188
  24. package/dist/worker/console/chat/resource-loader.js +5 -4
  25. package/dist/worker/console/chat/routes.js +495 -157
  26. package/dist/worker/console/chat/runtime-context.js +48 -12
  27. package/dist/worker/console/chat/runtime-selection.js +59 -0
  28. package/dist/worker/console/chat/session-store.js +39 -0
  29. package/dist/worker/console/chat/shortcuts.js +1 -0
  30. package/dist/worker/console/chat/tool-adapter.js +9 -3
  31. package/dist/worker/console/chat/tools.js +5 -1
  32. package/dist/worker/console/dag-execution-receipt.js +380 -0
  33. package/dist/worker/console/interview/grill-me.js +7 -6
  34. package/dist/worker/console/operator-actions.js +785 -85
  35. package/dist/worker/console/prd-intake-bridge.js +393 -0
  36. package/dist/worker/console/recovery-cta.js +35 -6
  37. package/dist/worker/console/server.js +8 -15
  38. package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
  39. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +1 -0
  42. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  45. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  46. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  47. package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
  48. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  49. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  50. package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
  51. package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
  52. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
  53. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
  54. package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
  55. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  56. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  57. package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
  58. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  59. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  60. package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
  61. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
  62. package/dist/worker/delivery/final-verification.js +13 -5
  63. package/dist/worker/delivery/package.js +31 -19
  64. package/dist/worker/delivery/verification-bundle.js +6 -4
  65. package/dist/worker/observability/read-model.js +3 -0
  66. package/dist/worker/observe/static/operator-chrome.css +5 -2
  67. package/dist/worker/observe/static/operator-chrome.js +6 -1
  68. package/dist/worker/observe/static/styles.css +39 -9
  69. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
  70. package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
  71. package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
  72. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  73. package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
  74. package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
  75. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  76. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  77. package/dist/workflows/dag/failure-category.js +3 -0
  78. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  79. package/dist/workflows/dag/init-hybrid.js +49 -55
  80. package/dist/workflows/dag/node-execution.js +3 -2
  81. package/dist/workflows/dag/retry-policy.js +44 -11
  82. package/dist/workflows/dag/runner.js +6 -0
  83. package/dist/workflows/dag/scheduler.js +49 -1
  84. package/dist/workflows/dag/types.js +0 -7
  85. package/dist/workflows/dag/validate.js +16 -2
  86. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  87. package/docs/templates/agent-dag.schema.json +4 -4
  88. package/docs/templates/backend-test-dag.json +29 -32
  89. package/harness.json +1 -1
  90. package/package.json +1 -1
  91. package/skills/local-jacoco-coverage/SKILL.md +281 -0
  92. package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
  93. package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
  94. package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
  95. package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
  96. package/skills/loop-agent/references/command-reference.md +3 -1
  97. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  98. package/skills/loop-agent/references/task-workflow.md +4 -0
  99. package/dist/worker/console/chat/instruction-skills.js +0 -217
  100. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  101. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -5,7 +5,7 @@ import { logicalRevisionForState, observeTaskContractState, } from "../../task/c
5
5
  import { assessDagRunLiveness, listAllDagRunEntries, readDagRunSpec, readDagRunState, } from "../../workflows/dag/lifecycle.js";
6
6
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
7
7
  import { loadLifecycleRecord } from "./record.js";
8
- import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
8
+ import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
9
9
  async function exists(filePath) {
10
10
  try {
11
11
  await access(filePath);
@@ -263,6 +263,15 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
263
263
  : null),
264
264
  })
265
265
  : null;
266
+ const gateDelta = gateBound != null
267
+ ? buildWriteSetGateDelta({
268
+ writeSet: gateBound.writeSet,
269
+ contractAllowedPaths: taskConfigAllowed,
270
+ writers: (parsedSpec?.tasks ?? [])
271
+ .filter((task) => task.writePolicy === "exclusive")
272
+ .map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] })),
273
+ })
274
+ : null;
266
275
  const lastGate = record?.lastGate;
267
276
  const currentDigest = gateBound != null
268
277
  ? buildWriteSetGate({ taskId, bound: gateBound }).digest
@@ -277,7 +286,7 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
277
286
  // not already approved/rejected. Rejected same-digest gates stay closed
278
287
  // until contract/source/DAG/writeSet/controller binding changes.
279
288
  const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
280
- ? buildWriteSetGate({ taskId, bound: gateBound })
289
+ ? buildWriteSetGate({ taskId, bound: gateBound, delta: gateDelta })
281
290
  : null;
282
291
  const gateOpen = Boolean(gate);
283
292
  let legacyState;
@@ -62,17 +62,21 @@ export function planTransitions(input) {
62
62
  expectedTransitions.push("contract-managed");
63
63
  }
64
64
  else if (!sourceReady || !contractManaged) {
65
- if ((shouldImportPrd || alreadyImportedPrd) &&
66
- !input.hasEngineeringBoundary) {
67
- // Console bootstrap / import-only: archive PRD(s), do not prepare contract yet.
68
- if (shouldImportPrd) {
69
- actions.push({ type: "import-prd" });
70
- expectedTransitions.push("prd-imported");
71
- }
65
+ // Console PRD Intake Bridge (2026-08-11), two-phase:
66
+ // 1) hasPrdInput without engineering boundary → import only (archive
67
+ // references). Do NOT prepare yet so multi-file bootstrap can finish
68
+ // all imports before the one-shot Semantic Intake burns.
69
+ // 2) alreadyImportedPrd without new --prd (and/or with boundary)
70
+ // prepare-contract so deterministic parse + Semantic Intake run.
71
+ // prepare soft-stops on EMPTY_ALLOWED_PATHS etc. and never opens the
72
+ // writeSet gate without a complete managed contract.
73
+ if (shouldImportPrd && !input.hasEngineeringBoundary) {
74
+ actions.push({ type: "import-prd" });
75
+ expectedTransitions.push("prd-imported");
72
76
  actions.push({ type: "stop", reason: "blocker" });
73
77
  blockers.push({
74
78
  code: "BOUNDARY_REQUIRED",
75
- message: "PRD imported; provide --allowed-path / --verify (or --from-text with boundaries) to prepare contract and open writeSet gate",
79
+ message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
76
80
  });
77
81
  return {
78
82
  actions,
@@ -80,7 +84,7 @@ export function planTransitions(input) {
80
84
  blockers,
81
85
  next: {
82
86
  kind: "advance",
83
- command: `loop-agent task advance ${snapshot.taskId} --allowed-path <glob> --verify <label:command> --json`,
87
+ command: `loop-agent task advance ${snapshot.taskId} --json`,
84
88
  },
85
89
  expectedTransitions,
86
90
  };
@@ -218,18 +222,6 @@ export function planTransitions(input) {
218
222
  expectedTransitions,
219
223
  };
220
224
  }
221
- function stopWith(actions, expectedTransitions, blockers, snapshot, reason) {
222
- return {
223
- actions: [...actions, { type: "stop", reason }],
224
- gate: snapshot.gate ?? null,
225
- blockers,
226
- next: {
227
- kind: "advance",
228
- command: `loop-agent task advance ${snapshot.taskId} --json`,
229
- },
230
- expectedTransitions,
231
- };
232
- }
233
225
  function deriveNext(snapshot, gate, approveGate) {
234
226
  if (gate && !approveGate) {
235
227
  return { kind: "approve-gate", command: gate.approveCommand };
@@ -1023,6 +1023,7 @@ function assertPendingAcceptance(value, state, task) {
1023
1023
  }
1024
1024
  const pending = value;
1025
1025
  const desiredSha256 = task.evidence.desiredSha256;
1026
+ const sourceAnchorSha256 = task.evidence.sourceAnchorSha256;
1026
1027
  const guard = pending.guard;
1027
1028
  const receipt = pending.receipt;
1028
1029
  const transition = pending.transition;
@@ -1039,6 +1040,9 @@ function assertPendingAcceptance(value, state, task) {
1039
1040
  !isSha256(guard.desiredSha256) ||
1040
1041
  !isSha256(desiredSha256) ||
1041
1042
  guard.desiredSha256 !== desiredSha256 ||
1043
+ (sourceAnchorSha256 === undefined
1044
+ ? guard.sourceAnchorSha256 !== undefined
1045
+ : guard.sourceAnchorSha256 !== sourceAnchorSha256) ||
1042
1046
  !isMergeReceipt(receipt) ||
1043
1047
  receipt.taskId !== guard.taskId ||
1044
1048
  receipt.path !== guard.path ||
@@ -1076,6 +1080,9 @@ function acceptedSurface(input) {
1076
1080
  acceptedEntry.acceptedMerge = {
1077
1081
  currentSha256: input.currentSha256,
1078
1082
  desiredSha256: input.desiredSha256,
1083
+ ...(typeof input.task.evidence.sourceAnchorSha256 === "string"
1084
+ ? { sourceAnchorSha256: input.task.evidence.sourceAnchorSha256 }
1085
+ : {}),
1079
1086
  };
1080
1087
  return { beforeEntry: cloneJson(entry), accepted };
1081
1088
  }
@@ -1210,6 +1217,7 @@ async function recordAcceptedMerge(input) {
1210
1217
  if (!desiredSha256) {
1211
1218
  throw new Error(`init upgrade cannot accept ${input.task.path}: desired hash evidence is missing`);
1212
1219
  }
1220
+ const sourceAnchorSha256 = input.task.evidence.sourceAnchorSha256;
1213
1221
  const canonicalPath = normalizeAllowedPath(input.task.path);
1214
1222
  const surfacePath = path.join(input.state.repoRoot, INIT_SURFACE_STATE_PATH);
1215
1223
  let pending = input.pending;
@@ -1263,6 +1271,9 @@ async function recordAcceptedMerge(input) {
1263
1271
  allowedPaths: [canonicalPath],
1264
1272
  currentSha256: input.currentSha256,
1265
1273
  desiredSha256,
1274
+ ...(sourceAnchorSha256 !== undefined
1275
+ ? { sourceAnchorSha256 }
1276
+ : {}),
1266
1277
  },
1267
1278
  receipt,
1268
1279
  transition: {
@@ -2412,7 +2423,27 @@ async function executeUpgrade(directory, state, input) {
2412
2423
  });
2413
2424
  state.actionReceipts.push(...safe.applied);
2414
2425
  await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
2415
- const post = await checkInitUpdate({ repoRoot: state.repoRoot });
2426
+ let post = await checkInitUpdate({ repoRoot: state.repoRoot });
2427
+ // Converge remaining safe deterministic init actions before the final check.
2428
+ // Project/governance-derived drift (e.g. a personalized project rename after
2429
+ // an accepted merge) settles here instead of failing finalCheck. Model merge
2430
+ // tasks and human decisions are never auto-applied.
2431
+ while (post.deterministicActions.length > 0) {
2432
+ const converged = await applyInitUpdate({
2433
+ repoRoot: state.repoRoot,
2434
+ applySafe: true,
2435
+ clientRecovery: "project",
2436
+ });
2437
+ state.actionReceipts.push(...converged.applied);
2438
+ await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
2439
+ if (converged.applied.length === 0)
2440
+ break;
2441
+ post = await checkInitUpdate({ repoRoot: state.repoRoot });
2442
+ if (post.humanDecisions.length > 0 ||
2443
+ post.modelMergeTasks.length > 0) {
2444
+ break;
2445
+ }
2446
+ }
2416
2447
  state.humanDecisions = post.humanDecisions;
2417
2448
  if (post.humanDecisions.length > 0) {
2418
2449
  state.mergeTasks = [];
@@ -1414,6 +1414,70 @@ async function readExistingSurfaceState(repoRoot) {
1414
1414
  return undefined;
1415
1415
  return parsed;
1416
1416
  }
1417
+ /** The IDE schema reference remains a structural update signal. */
1418
+ const HARNESS_STRUCTURAL_VALUE_KEYS = new Set(["$schema"]);
1419
+ const PI_MODEL_ROUTING_KEYS = new Set(["LOW", "MED", "HIGH", "defaultModel"]);
1420
+ function isPiModelRoutingPath(path) {
1421
+ return path.length === 2 && path[0] === "executors" && path[1] === "pi";
1422
+ }
1423
+ /**
1424
+ * Attribute-set iteration comparison for harness.json: object key shapes must
1425
+ * agree at every level, except that the Pi model-routing fields are opaque
1426
+ * project-owned configuration. Scalars and array contents may otherwise differ
1427
+ * freely unless the key is structurally sensitive.
1428
+ */
1429
+ function harnessJsonShapeMatches(current, desired, path = []) {
1430
+ if (isRecord(current) || isRecord(desired)) {
1431
+ if (!isRecord(current) || !isRecord(desired))
1432
+ return false;
1433
+ const filterKeys = (value) => Object.keys(value)
1434
+ .filter((key) => !isPiModelRoutingPath(path) || !PI_MODEL_ROUTING_KEYS.has(key))
1435
+ .sort();
1436
+ const currentKeys = filterKeys(current);
1437
+ const desiredKeys = filterKeys(desired);
1438
+ if (currentKeys.length !== desiredKeys.length)
1439
+ return false;
1440
+ for (let index = 0; index < currentKeys.length; index += 1) {
1441
+ const key = currentKeys[index];
1442
+ if (key !== desiredKeys[index])
1443
+ return false;
1444
+ if (HARNESS_STRUCTURAL_VALUE_KEYS.has(key)) {
1445
+ if (!isDeepStrictEqual(current[key], desired[key]))
1446
+ return false;
1447
+ continue;
1448
+ }
1449
+ if (!harnessJsonShapeMatches(current[key], desired[key], [...path, key])) {
1450
+ return false;
1451
+ }
1452
+ }
1453
+ return true;
1454
+ }
1455
+ return true;
1456
+ }
1457
+ /**
1458
+ * Stable package/source anchor for generated desired content: sha256 of the
1459
+ * desired content re-rendered through buildDesiredSurfaceContent with fixed
1460
+ * sentinel identity values (PROJECT_NAME_TOKEN / GOVERNANCE_ROOT_TOKEN). The
1461
+ * anchor is therefore deterministic and invariant to the real project name and
1462
+ * governance root: a project rename or governance-root change never invalidates
1463
+ * a previously accepted semantic merge, while package/template content changes
1464
+ * still do. Rendering with sentinels (instead of reverse split/join replacement
1465
+ * on already-rendered bytes) avoids false drift when a real project name is a
1466
+ * common substring such as `init` or `docs`.
1467
+ */
1468
+ async function stableDesiredSourceAnchor(input) {
1469
+ const manifestPath = targetPathToManifestPath(input.entry.path, input.governanceRoot);
1470
+ const sentinel = await buildDesiredSurfaceContent({
1471
+ assetRoot: input.assetRoot,
1472
+ repoRoot: input.repoRoot,
1473
+ projectName: PROJECT_NAME_TOKEN,
1474
+ governanceRoot: GOVERNANCE_ROOT_TOKEN,
1475
+ entry: { ...input.entry, path: manifestPath },
1476
+ });
1477
+ return sentinel.content === undefined
1478
+ ? undefined
1479
+ : sha256Text(sentinel.content);
1480
+ }
1417
1481
  /**
1418
1482
  * Lightweight read-only preflight for the post-upgrade init surface notifier.
1419
1483
  *
@@ -1458,10 +1522,20 @@ async function buildCurrentSurfaceState(input) {
1458
1522
  entry: manifestEntry,
1459
1523
  });
1460
1524
  const sourceSha256 = desired.content === undefined ? undefined : sha256Text(desired.content);
1525
+ const sourceAnchorSha256 = entry.mode !== "generated" || desired.content === undefined
1526
+ ? undefined
1527
+ : await stableDesiredSourceAnchor({
1528
+ assetRoot,
1529
+ repoRoot: input.repoRoot,
1530
+ projectName: input.projectName,
1531
+ governanceRoot: input.governanceRoot,
1532
+ entry,
1533
+ });
1461
1534
  const base = {
1462
1535
  status: targetStat ? "present" : "missing",
1463
1536
  relationship: "missing-from-target",
1464
1537
  sourceSha256,
1538
+ sourceAnchorSha256,
1465
1539
  sourcePath: desired.sourcePath
1466
1540
  ? repoRelative(assetRoot, desired.sourcePath)
1467
1541
  : undefined,
@@ -1510,7 +1584,7 @@ async function buildCurrentSurfaceState(input) {
1510
1584
  if (targetRelativePath === "harness.json" &&
1511
1585
  desired.content !== undefined) {
1512
1586
  try {
1513
- semanticallyMatchesGeneratedJson = isDeepStrictEqual(JSON.parse(current.toString("utf-8")), JSON.parse(desired.content));
1587
+ semanticallyMatchesGeneratedJson = harnessJsonShapeMatches(JSON.parse(current.toString("utf-8")), JSON.parse(desired.content));
1514
1588
  }
1515
1589
  catch {
1516
1590
  // Invalid JSON remains local-existing-unknown for model merge / doctor.
@@ -1563,6 +1637,9 @@ async function writeInitSurfaceState(input) {
1563
1637
  harness.acceptedMerge = {
1564
1638
  currentSha256: harness.currentSha256,
1565
1639
  desiredSha256: harness.sourceSha256,
1640
+ ...(harness.sourceAnchorSha256 !== undefined
1641
+ ? { sourceAnchorSha256: harness.sourceAnchorSha256 }
1642
+ : {}),
1566
1643
  };
1567
1644
  }
1568
1645
  }
@@ -2729,6 +2806,7 @@ function modelMergeTaskFor(pathName, state, allPaths, recordedState) {
2729
2806
  baseSha256: recordedState?.files[pathName]?.currentSha256,
2730
2807
  currentSha256: state.currentSha256,
2731
2808
  desiredSha256: state.sourceSha256,
2809
+ sourceAnchorSha256: state.sourceAnchorSha256,
2732
2810
  },
2733
2811
  };
2734
2812
  }
@@ -2847,8 +2925,16 @@ export async function checkInitUpdate(input) {
2847
2925
  if (state.relationship === "local-existing-unknown") {
2848
2926
  const acceptedMerge = recordedState?.files[pathName]?.acceptedMerge;
2849
2927
  if (acceptedMerge) {
2928
+ // Generated acceptance is bound to the stable source anchor so that
2929
+ // project-derived desired drift (projectName/governanceRoot changes)
2930
+ // cannot invalidate an already accepted merge. Copied files remain
2931
+ // byte-strict; desiredSha256 stays the audit receipt in both cases.
2932
+ const anchorAccepted = state.mode === "generated" &&
2933
+ state.sourceAnchorSha256 !== undefined &&
2934
+ acceptedMerge.sourceAnchorSha256 === state.sourceAnchorSha256;
2850
2935
  if (acceptedMerge.currentSha256 === state.currentSha256 &&
2851
- acceptedMerge.desiredSha256 === state.sourceSha256) {
2936
+ (anchorAccepted ||
2937
+ acceptedMerge.desiredSha256 === state.sourceSha256)) {
2852
2938
  continue;
2853
2939
  }
2854
2940
  // A previously accepted semantic merge is user-preserving ownership.
@@ -2906,7 +2992,12 @@ export async function checkInitUpdate(input) {
2906
2992
  reason: `replace the legacy default governanceRoot docs with ${governanceRoot}`,
2907
2993
  });
2908
2994
  }
2909
- if (isRecord(harness)) {
2995
+ // A harness that already matches the current generated surface except for
2996
+ // opaque Pi routing fields is modern, not a legacy-routing candidate.
2997
+ // Keep the explicit migration assessment for every other harness shape.
2998
+ if (isRecord(harness) &&
2999
+ currentState.files["harness.json"]?.relationship !==
3000
+ "matches-current-generated") {
2910
3001
  const modelMigration = assessHarnessModelMigration(harness);
2911
3002
  if (modelMigration.kind === "safe") {
2912
3003
  deterministicActions.push({
@@ -8,7 +8,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
8
8
  import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
9
9
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
10
10
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
11
- import { isWriterEmptyDiffRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
11
+ import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
12
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
13
13
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
14
14
  /**
@@ -831,6 +831,18 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
831
831
  if (meta.writeGuardAttribution === "best-effort") {
832
832
  stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
833
833
  }
834
+ // Pure provider stall/timeout with zero workspace side effects is eligible
835
+ // for a single writer transport retry (implement-pi flake resilience).
836
+ // Partial writes, tool calls, or write-guard failures keep raw `timeout`.
837
+ const writerCleanTimeout = !mapped.ok &&
838
+ mapped.failureCategory === "timeout" &&
839
+ writeGuardOk &&
840
+ !completenessFailure &&
841
+ !writerThinkingExhausted &&
842
+ isWriterTransportRetryCandidate(input.task) &&
843
+ changeManifestChangedFiles !== undefined &&
844
+ changeManifestChangedFiles.length === 0 &&
845
+ readWriterThinkingExhaustionEvidence(result).writeToolCallCount === 0;
834
846
  return {
835
847
  ...mapped,
836
848
  ok: false,
@@ -851,7 +863,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
851
863
  // the continuation is retryable and carries the embedded target paths.
852
864
  // Provider/transport failures (quota, auth, network, timeout,
853
865
  // rate-limit, unavailable) keep their original category so a provider
854
- // outage is never masked as a recoverable local write problem.
866
+ // outage is never masked as a recoverable local write problem — except
867
+ // clean timeouts with zero attributed writes, remapped below.
855
868
  writeGuardOk &&
856
869
  completenessFailure?.failureCategory ===
857
870
  INCOMPLETE_WRITE_SET_RETRY_CATEGORY &&
@@ -867,7 +880,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
867
880
  // upgrade to incomplete-write-set above.
868
881
  writerThinkingExhausted
869
882
  ? WRITER_THINKING_EXHAUSTED_CATEGORY
870
- : mapped.failureCategory,
883
+ : writerCleanTimeout
884
+ ? WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY
885
+ : mapped.failureCategory,
871
886
  durationMs: mapped.durationMs || Date.now() - started,
872
887
  };
873
888
  }
@@ -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, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
34
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, 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,42 +678,6 @@ 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
- }
717
681
  const artifacts = await writeBackendPytestCollectionArtifacts({
718
682
  runDir: meta.runDir,
719
683
  stem: "initial",
@@ -812,56 +776,12 @@ async function executeBackendTestPipeline(input, meta) {
812
776
  facts: effective,
813
777
  });
814
778
  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}`);
857
779
  const readiness = await materializeBackendTestExecutionReadiness({
858
780
  runDir: meta.runDir,
859
781
  workspaceRoot: input.cwd,
860
782
  effective,
861
783
  scenarioParamStatus: scenarioFacts.overallStatus,
862
784
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
863
- eligibility,
864
- eligibilityInputHashes,
865
785
  });
866
786
  return {
867
787
  ok: true,
@@ -872,8 +792,6 @@ async function executeBackendTestPipeline(input, meta) {
872
792
  collectedItemCount: effective.collectedItemCount,
873
793
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
874
794
  executionReadinessStatus: readiness.status,
875
- eligibleItemCount: readiness.eligibleItemIds.length,
876
- excludedItemCount: readiness.excludedItems.length,
877
795
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
878
796
  readinessPath: "contracts/backend-test-execution-readiness.json",
879
797
  reportPath: artifacts.reportPath,
@@ -972,8 +890,7 @@ async function executeBackendTestPipeline(input, meta) {
972
890
  await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
973
891
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
974
892
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
975
- const eligibleItemIds = executionReadiness.eligibleItemIds;
976
- const pytestTargets = eligibleItemIds.map(shellQuote).join(" ");
893
+ const pytestTargets = mappedScripts.map(shellQuote).join(" ");
977
894
  // Split into short bash -c commands (aligned with markdown-environment).
978
895
  // A single ultra-long compound command has been observed on Windows to
979
896
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -1050,9 +967,7 @@ async function executeBackendTestPipeline(input, meta) {
1050
967
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
1051
968
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
1052
969
  'echo "STEP=pytest-run"',
1053
- `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1054
- `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1055
- `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
970
+ `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1056
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`,
1057
972
  "STATUS=$?",
1058
973
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
@@ -1142,9 +1057,7 @@ async function executeBackendTestPipeline(input, meta) {
1142
1057
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1143
1058
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1144
1059
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
1145
- const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1146
- const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1147
- const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1060
+ const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1148
1061
  const htmlContent = renderBackendTestHtml({
1149
1062
  title: meta.spec.title,
1150
1063
  parsed,
@@ -4,6 +4,7 @@ import { constants, createReadStream } from "node:fs";
4
4
  import { access, lstat, readlink, unlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { pathMatchesPattern } from "../shared/git-progress.js";
7
+ import { ResilientGitCommandError, runResilientGitCommand, } from "../shared/resilient-git.js";
7
8
  async function sha256File(filePath) {
8
9
  const hash = createHash("sha256");
9
10
  for await (const chunk of createReadStream(filePath)) {
@@ -401,14 +402,31 @@ async function resolveGitExecutableCandidates(platform, env) {
401
402
  return ["git"];
402
403
  }
403
404
  export async function readGitStatusPorcelain(cwd, options = {}) {
404
- return readGitStatusPorcelainWithDependencies(cwd, options, {
405
- platform: process.platform,
406
- resolveExecutableCandidates: () => resolveGitExecutableCandidates(process.platform, process.env),
407
- runAttempt: readGitStatusPorcelainOnce,
408
- sleep: async (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
409
- now: Date.now,
410
- env: process.env,
411
- });
405
+ try {
406
+ const result = await runResilientGitCommand({
407
+ cwd,
408
+ args: ["status", "--porcelain=v1", "--untracked-files=all"],
409
+ readOnly: true,
410
+ attempts: options.attempts,
411
+ retryDelayMs: options.retryDelayMs,
412
+ });
413
+ if (result.code === 0)
414
+ return result.stdout;
415
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`);
416
+ }
417
+ catch (error) {
418
+ if (error instanceof ResilientGitCommandError) {
419
+ throw new GitStatusUnavailableError({
420
+ cwd,
421
+ phase: options.phase,
422
+ platform: error.diagnostics.platform,
423
+ executableCandidates: error.diagnostics.executableCandidates,
424
+ attempts: error.diagnostics.attempts,
425
+ requiredWindowsEnvironment: requiredWindowsEnvironment(process.env),
426
+ });
427
+ }
428
+ throw error;
429
+ }
412
430
  }
413
431
  export async function readGitStatusPorcelainWithDependencies(cwd, options, dependencies) {
414
432
  const candidates = await dependencies.resolveExecutableCandidates();