@tea-agent/loop-agent 0.33.6-beta.0 → 0.33.6

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 (64) hide show
  1. package/CHANGELOG.md +29 -4
  2. package/dist/application/task-lifecycle/advance.js +254 -4
  3. package/dist/application/task-lifecycle/gates.js +50 -0
  4. package/dist/application/task-lifecycle/observe.js +11 -2
  5. package/dist/commands/init-upgrade.js +32 -1
  6. package/dist/commands/init.js +94 -3
  7. package/dist/executors/shell-write-guard.js +26 -8
  8. package/dist/shared/operator/capabilities.js +72 -42
  9. package/dist/shared/resilient-git.js +133 -0
  10. package/dist/task/source-prepare/artifact-meta.js +137 -0
  11. package/dist/task/source-prepare/index.js +2 -0
  12. package/dist/task/source-prepare/parse-intent.js +58 -10
  13. package/dist/task/source-prepare/prepare.js +180 -16
  14. package/dist/task/source-prepare/reference-integrity.js +18 -2
  15. package/dist/task/source-prepare/semantic-intake.js +404 -0
  16. package/dist/worker/console/app-data.js +2 -0
  17. package/dist/worker/console/chat/chat-event-store.js +190 -25
  18. package/dist/worker/console/chat/pi-console-config.js +250 -32
  19. package/dist/worker/console/chat/pi-runtime.js +625 -71
  20. package/dist/worker/console/chat/resource-loader.js +5 -4
  21. package/dist/worker/console/chat/routes.js +324 -146
  22. package/dist/worker/console/chat/runtime-context.js +48 -12
  23. package/dist/worker/console/chat/runtime-selection.js +59 -0
  24. package/dist/worker/console/chat/shortcuts.js +1 -0
  25. package/dist/worker/console/chat/tool-adapter.js +9 -3
  26. package/dist/worker/console/chat/tools.js +5 -1
  27. package/dist/worker/console/dag-execution-receipt.js +380 -0
  28. package/dist/worker/console/operator-actions.js +559 -68
  29. package/dist/worker/console/server.js +8 -15
  30. package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
  31. package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
  32. package/dist/worker/console/static/index.html +2 -2
  33. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
  34. package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
  35. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
  36. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
  37. package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
  38. package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
  39. package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
  40. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
  41. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
  42. package/dist/worker/delivery/final-verification.js +13 -5
  43. package/dist/worker/delivery/package.js +31 -19
  44. package/dist/worker/delivery/verification-bundle.js +6 -4
  45. package/dist/worker/observe/static/operator-chrome.css +5 -2
  46. package/dist/worker/observe/static/operator-chrome.js +6 -1
  47. package/dist/worker/observe/static/styles.css +39 -9
  48. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +2 -27
  49. package/dist/workflows/dag/backend-test-module-stem.js +0 -5
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
  51. package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
  52. package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
  53. package/dist/workflows/dag/init-hybrid.js +12 -20
  54. package/dist/workflows/dag/types.js +0 -7
  55. package/dist/workflows/dag/workspace-checkpoint.js +8 -27
  56. package/docs/templates/backend-test-dag.json +10 -10
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +3 -1
  60. package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
  61. package/skills/loop-agent/references/task-workflow.md +4 -0
  62. package/dist/worker/console/chat/instruction-skills.js +0 -217
  63. package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
  64. package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
@@ -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({
@@ -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();
@@ -699,6 +699,36 @@ export function buildOperatorCapabilitiesDocument() {
699
699
  modelCallable: "always",
700
700
  humanConfirmation: "none",
701
701
  },
702
+ {
703
+ action: "prepareDagExecution",
704
+ cli: "console aggregate execution receipt (server-side G2 assessment)",
705
+ kind: "read",
706
+ inputSchemaVersion: 1,
707
+ resultSchemaVersion: 1,
708
+ envelopeSchemaVersion: 1,
709
+ requiredErrorCodes: [
710
+ "INVALID_INPUT",
711
+ "BINDING_DRIFT",
712
+ "HUMAN_CONFIRMATION_REQUIRED",
713
+ ],
714
+ description: "Issue a session-bound single-use execution receipt after server-side G2 assessment (bounded writeSet ⊆ allowedPaths, no forbidden overlap, no broad/destructive risk, structured verification). The model never passes a raw DAG path.",
715
+ inputParams: [
716
+ {
717
+ name: "taskId",
718
+ type: "string",
719
+ required: true,
720
+ description: "task id",
721
+ },
722
+ {
723
+ name: "profile",
724
+ type: "string",
725
+ required: false,
726
+ description: "DAG profile (defaults to auto)",
727
+ },
728
+ ],
729
+ modelCallable: "always",
730
+ humanConfirmation: "none",
731
+ },
702
732
  {
703
733
  action: "confirmDagConfirmation",
704
734
  cli: "console aggregate confirmation human challenge (server-side)",
@@ -901,7 +931,7 @@ export function buildOperatorCapabilitiesDocument() {
901
931
  },
902
932
  {
903
933
  action: "runDag",
904
- cli: "loop-agent task advance <taskId> --approve-gate <id:digest> --json | dag execute --dag <path>",
934
+ cli: "loop-agent task advance <taskId> --approve-gate <id:digest> --json | dag execute --dag <staged-path>",
905
935
  kind: "long-running",
906
936
  inputSchemaVersion: 1,
907
937
  resultSchemaVersion: 1,
@@ -911,19 +941,20 @@ export function buildOperatorCapabilitiesDocument() {
911
941
  "CONTROLLER_MISMATCH",
912
942
  "INVALID_INPUT",
913
943
  ],
914
- description: "Execute reviewed DAG after human confirmation (prefer task advance --approve-gate when gate token present).",
944
+ description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor.",
915
945
  inputParams: [
916
946
  {
917
- name: "confirmationId",
947
+ name: "executionId",
918
948
  type: "string",
919
949
  required: true,
920
- description: "confirmation id (from prepareDagConfirmation) — raw dag path is never accepted from the model",
950
+ description: "execution id (from prepareDagExecution) — raw dag path is never accepted from the model",
921
951
  },
922
952
  ],
923
- // prepare-only: the model can invoke runDag but it MUST carry a
924
- // confirmationId that was consumed by a human-origin confirm (M0-B).
925
- modelCallable: "prepare-only",
926
- humanConfirmation: "required",
953
+ // always: the model can invoke runDag with a server-issued executionId.
954
+ // The server re-validates staged bytes/hash + bindings at consume time
955
+ // (2026-08-11 autonomous DAG supervision; receipt replaces browser gate).
956
+ modelCallable: "always",
957
+ humanConfirmation: "none",
927
958
  },
928
959
  {
929
960
  action: "dagRerunPlan",
@@ -963,7 +994,7 @@ export function buildOperatorCapabilitiesDocument() {
963
994
  "BINDING_DRIFT",
964
995
  "INVALID_INPUT",
965
996
  ],
966
- description: "Execute R1 continuation from effective node. Requires planHash from a fresh dagRerunPlan (eligible=true). Prefer this over new task / standaloneTaskRerun for provider flake and safe downstream failures. Human Gate required.",
997
+ description: "Execute R1 continuation from effective node. Requires planHash from a fresh dagRerunPlan (eligible=true). Prefer this over new task / standaloneTaskRerun for provider flake and safe downstream failures. Server enforces planHash/binding/fingerprint freshness.",
967
998
  inputParams: [
968
999
  {
969
1000
  name: "runId",
@@ -990,8 +1021,11 @@ export function buildOperatorCapabilitiesDocument() {
990
1021
  description: "why continue from this node (e.g. provider flake; resume from review-pi)",
991
1022
  },
992
1023
  ],
993
- modelCallable: "prepare-only",
994
- humanConfirmation: "required",
1024
+ // always: fresh eligible dagRerun plans are model-callable (2026-08-11).
1025
+ // The CLI keeps enforcing parent lifecycle, fromNode, planHash,
1026
+ // binding/fingerprint and request idempotency.
1027
+ modelCallable: "always",
1028
+ humanConfirmation: "none",
995
1029
  },
996
1030
  {
997
1031
  action: "prepareMutationGate",
@@ -1001,13 +1035,13 @@ export function buildOperatorCapabilitiesDocument() {
1001
1035
  resultSchemaVersion: 1,
1002
1036
  envelopeSchemaVersion: 1,
1003
1037
  requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1004
- description: "Prepare a one-shot Human Gate receipt for standaloneTaskRerun / workerTaskRetry / Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume.",
1038
+ description: "Prepare a one-shot Human Gate receipt for Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume.",
1005
1039
  inputParams: [
1006
1040
  {
1007
1041
  name: "action",
1008
1042
  type: "string",
1009
1043
  required: true,
1010
- description: "target action: standaloneTaskRerun | workerTaskRetry | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard",
1044
+ description: "target action: workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard",
1011
1045
  },
1012
1046
  {
1013
1047
  name: "actionParams",
@@ -1259,7 +1293,7 @@ export function buildOperatorCapabilitiesDocument() {
1259
1293
  "INVALID_INPUT",
1260
1294
  "HUMAN_CONFIRMATION_REQUIRED",
1261
1295
  ],
1262
- description: "Full standalone task regenerate → validate → execute with parent lineage.",
1296
+ description: "Full standalone task regenerate → validate → execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting.",
1263
1297
  inputParams: [
1264
1298
  {
1265
1299
  name: "runId",
@@ -1274,20 +1308,22 @@ export function buildOperatorCapabilitiesDocument() {
1274
1308
  description: "rerun reason",
1275
1309
  },
1276
1310
  {
1277
- name: "confirmationId",
1311
+ name: "profile",
1278
1312
  type: "string",
1279
- required: true,
1280
- description: "mutation-gate receipt id from prepareMutationGate",
1313
+ required: false,
1314
+ description: "DAG profile (defaults to auto)",
1281
1315
  },
1282
1316
  {
1283
- name: "humanGateToken",
1284
- type: "object",
1285
- required: true,
1286
- description: "server-signed human-gate token from prepareMutationGate",
1317
+ name: "taskId",
1318
+ type: "string",
1319
+ required: false,
1320
+ description: "original task id (must match the run's task binding; never invent a new task-id)",
1287
1321
  },
1288
1322
  ],
1289
- modelCallable: "prepare-only",
1290
- humanConfirmation: "required",
1323
+ // always: the server gate is the read-only run-facts check (2026-08-11);
1324
+ // no browser Human Gate token is required for safe same-task rerun.
1325
+ modelCallable: "always",
1326
+ humanConfirmation: "none",
1291
1327
  },
1292
1328
  {
1293
1329
  action: "workerTaskRetry",
@@ -1301,7 +1337,7 @@ export function buildOperatorCapabilitiesDocument() {
1301
1337
  "INVALID_INPUT",
1302
1338
  "HUMAN_CONFIRMATION_REQUIRED",
1303
1339
  ],
1304
- description: "Requeue failed Task Pool task (Failed → Ready) via in-package pool store.",
1340
+ description: "Requeue failed Task Pool task (Failed → Ready) via in-package pool store. Only when read-only validation confirms the task exists in Failed state; the server checks this before accepting.",
1305
1341
  inputParams: [
1306
1342
  {
1307
1343
  name: "taskId",
@@ -1321,21 +1357,11 @@ export function buildOperatorCapabilitiesDocument() {
1321
1357
  required: true,
1322
1358
  description: "retry reason",
1323
1359
  },
1324
- {
1325
- name: "confirmationId",
1326
- type: "string",
1327
- required: true,
1328
- description: "mutation-gate receipt id from prepareMutationGate",
1329
- },
1330
- {
1331
- name: "humanGateToken",
1332
- type: "object",
1333
- required: true,
1334
- description: "server-signed human-gate token from prepareMutationGate",
1335
- },
1336
1360
  ],
1337
- modelCallable: "prepare-only",
1338
- humanConfirmation: "required",
1361
+ // always: the server gate is the read-only worker task facts check
1362
+ // (2026-08-11); no browser Human Gate token is required.
1363
+ modelCallable: "always",
1364
+ humanConfirmation: "none",
1339
1365
  },
1340
1366
  ...OFFICIAL_ACTIONS,
1341
1367
  ];
@@ -1411,9 +1437,10 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
1411
1437
  },
1412
1438
  {
1413
1439
  command: "loop-agent dag execute",
1414
- coverage: "human-gated-required",
1440
+ coverage: "model-callable",
1415
1441
  action: "runDag",
1416
1442
  source: "loop-agent",
1443
+ note: "runDag consumes a server-issued single-use execution receipt (prepareDagExecution); browser Human Gate no longer required for bounded DAGs (2026-08-11).",
1417
1444
  },
1418
1445
  {
1419
1446
  command: "loop-agent cursor-prompt",
@@ -1829,15 +1856,17 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
1829
1856
  },
1830
1857
  {
1831
1858
  command: "loop-agent dag rerun",
1832
- coverage: "human-gated-required",
1859
+ coverage: "model-callable",
1833
1860
  action: "dagRerun",
1834
1861
  source: "loop-agent",
1862
+ note: "fresh eligible dagRerunPlan (planHash) is model-callable; the CLI still enforces plan/binding/fingerprint freshness (2026-08-11).",
1835
1863
  },
1836
1864
  {
1837
1865
  command: "loop-agent dag rerun-task",
1838
- coverage: "human-gated-required",
1866
+ coverage: "model-callable",
1839
1867
  action: "standaloneTaskRerun",
1840
1868
  source: "loop-agent",
1869
+ note: "server-side read-only run-facts check (ineligible plan / explicit rerun-task recommendation) replaces the browser Human Gate (2026-08-11).",
1841
1870
  },
1842
1871
  {
1843
1872
  command: "loop-agent dag reconcile-tasks",
@@ -2069,9 +2098,10 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
2069
2098
  },
2070
2099
  {
2071
2100
  command: "agent-worker task retry",
2072
- coverage: "human-gated-required",
2101
+ coverage: "model-callable",
2073
2102
  action: "workerTaskRetry",
2074
2103
  source: "agent-worker",
2104
+ note: "server-side read-only pool doctor facts check (Failed state) replaces the browser Human Gate (2026-08-11).",
2075
2105
  },
2076
2106
  {
2077
2107
  command: "agent-worker task draft-followup",
@@ -0,0 +1,133 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access, constants } from "node:fs/promises";
3
+ import path from "node:path";
4
+ /** Thrown only for spawn/infrastructure failures or exhausted eligible startup retries. */
5
+ export class ResilientGitCommandError extends Error {
6
+ diagnostics;
7
+ constructor(input) {
8
+ const last = input.attempts.at(-1);
9
+ super(`git ${input.command} unavailable after ${input.attempts.length} attempts (exit code=${last?.exitCode ?? "unavailable"}, exit code hex=${last?.exitCodeHex ?? "unavailable"}, signal=${last?.signal ?? "unavailable"}, cwd=${path.resolve(input.cwd)}): ${last?.detail ?? "unknown infrastructure error"}`);
10
+ this.name = "ResilientGitCommandError";
11
+ this.diagnostics = { schemaVersion: 1, ...input };
12
+ }
13
+ }
14
+ export function isResilientGitCommandError(error) {
15
+ return error instanceof ResilientGitCommandError;
16
+ }
17
+ export function formatWindowsExitCode(exitCode) {
18
+ return exitCode === undefined
19
+ ? undefined
20
+ : `0x${(exitCode >>> 0).toString(16).padStart(8, "0").toUpperCase()}`;
21
+ }
22
+ export function isWindowsDllInitializationFailure(platform, exitCode) {
23
+ return platform === "win32" && exitCode !== undefined && (exitCode >>> 0) === 0xc0000142;
24
+ }
25
+ /** Only returns the original executable and its sibling within the same Git installation. */
26
+ export function deriveSameInstallationGitCandidates(primary) {
27
+ const normalized = path.win32.normalize(primary);
28
+ const lower = normalized.toLowerCase();
29
+ let sibling;
30
+ if (lower.endsWith("\\mingw64\\bin\\git.exe")) {
31
+ const root = path.win32.resolve(path.win32.dirname(normalized), "..", "..");
32
+ sibling = path.win32.join(root, "cmd", "git.exe");
33
+ }
34
+ else if (lower.endsWith("\\cmd\\git.exe")) {
35
+ const root = path.win32.resolve(path.win32.dirname(normalized), "..");
36
+ sibling = path.win32.join(root, "mingw64", "bin", "git.exe");
37
+ }
38
+ return [...new Set([normalized, ...(sibling ? [sibling] : [])])];
39
+ }
40
+ const windowsCandidateCache = new Map();
41
+ export async function resolveGitExecutableCandidates(platform, env) {
42
+ if (platform !== "win32")
43
+ return ["git"];
44
+ const pathValue = env.PATH ?? env.Path ?? "";
45
+ const cached = windowsCandidateCache.get(pathValue);
46
+ if (cached)
47
+ return cached;
48
+ const resolved = resolveWindowsGitExecutableCandidates(pathValue);
49
+ windowsCandidateCache.set(pathValue, resolved);
50
+ return resolved;
51
+ }
52
+ async function resolveWindowsGitExecutableCandidates(pathValue) {
53
+ for (const raw of pathValue.split(path.delimiter)) {
54
+ const entry = raw.trim().replace(/^"|"$/g, "");
55
+ if (!entry)
56
+ continue;
57
+ const candidate = path.win32.join(entry, "git.exe");
58
+ try {
59
+ await access(candidate, constants.X_OK);
60
+ const candidates = [];
61
+ for (const executable of deriveSameInstallationGitCandidates(candidate)) {
62
+ try {
63
+ await access(executable, constants.X_OK);
64
+ candidates.push(executable);
65
+ }
66
+ catch { /* optional sibling */ }
67
+ }
68
+ if (candidates.length)
69
+ return candidates;
70
+ }
71
+ catch { /* continue PATH search */ }
72
+ }
73
+ return ["git"];
74
+ }
75
+ const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
76
+ const bounded = (text) => text.length <= 1000 ? text : `${text.slice(0, 1000)}...[truncated]`;
77
+ /**
78
+ * Execute a read-only Git command with bounded recovery for Windows startup failures.
79
+ * Completed non-zero commands are deliberately returned to preserve caller semantics.
80
+ */
81
+ export async function runResilientGitCommand(options, dependencies = {}) {
82
+ const platform = dependencies.platform ?? process.platform;
83
+ const env = { ...(dependencies.env ?? process.env), GIT_OPTIONAL_LOCKS: "0" };
84
+ const candidates = await (dependencies.resolveExecutableCandidates ?? resolveGitExecutableCandidates)(platform, env);
85
+ const executables = candidates.length ? candidates : ["git"];
86
+ const maxAttempts = Math.max(1, options.attempts ?? 3);
87
+ const delay = Math.max(0, options.retryDelayMs ?? 250);
88
+ const execute = dependencies.runAttempt ?? spawnGitAttempt;
89
+ const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
90
+ const now = dependencies.now ?? Date.now;
91
+ const attemptDiagnostics = [];
92
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
93
+ const executable = executables[(attempt - 1) % executables.length];
94
+ const startedAt = now();
95
+ try {
96
+ const result = await execute({ executable, args: options.args, cwd: options.cwd, env, windowsHide: true, maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER });
97
+ if (result.code === 0)
98
+ return result;
99
+ const dllFailure = isWindowsDllInitializationFailure(platform, result.code);
100
+ const emptyStartupFailure = result.stdout.length === 0 && result.stderr.length === 0;
101
+ const eligible = platform === "win32" && (dllFailure || emptyStartupFailure);
102
+ if (!eligible)
103
+ return result;
104
+ attemptDiagnostics.push({
105
+ attempt, executable, exitCode: result.code, ...(formatWindowsExitCode(result.code) ? { exitCodeHex: formatWindowsExitCode(result.code) } : {}), signal: result.signal,
106
+ durationMs: Math.max(0, now() - startedAt), detail: bounded("no stdout/stderr"),
107
+ transientKind: dllFailure ? "windows-dll-init-failed" : "empty-output-startup-failure",
108
+ });
109
+ }
110
+ catch (error) {
111
+ const detail = bounded(error instanceof Error ? error.message : String(error));
112
+ attemptDiagnostics.push({ attempt, executable, signal: null, durationMs: Math.max(0, now() - startedAt), detail });
113
+ }
114
+ if (attempt < maxAttempts && delay > 0)
115
+ await sleep(Math.min(4_000, delay * 2 ** (attempt - 1)));
116
+ }
117
+ throw new ResilientGitCommandError({ cwd: options.cwd, command: options.args.join(" "), platform, executableCandidates: executables, attempts: attemptDiagnostics });
118
+ }
119
+ function spawnGitAttempt(input) {
120
+ return new Promise((resolve, reject) => {
121
+ const child = spawn(input.executable, input.args, { cwd: input.cwd, env: input.env, windowsHide: input.windowsHide, stdio: ["ignore", "pipe", "pipe"] });
122
+ let stdout = "";
123
+ let stderr = "";
124
+ const append = (current, chunk) => {
125
+ const next = current + String(chunk);
126
+ return next.length > input.maxBuffer ? next.slice(0, input.maxBuffer) : next;
127
+ };
128
+ child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
129
+ child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
130
+ child.on("error", reject);
131
+ child.on("close", (code, signal) => resolve({ code: code ?? 1, signal, stdout, stderr }));
132
+ });
133
+ }