@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.
- package/CHANGELOG.md +29 -4
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +625 -71
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +324 -146
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
- package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +2 -27
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +12 -20
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +10 -10
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
package/dist/commands/init.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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: "
|
|
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: "
|
|
947
|
+
name: "executionId",
|
|
918
948
|
type: "string",
|
|
919
949
|
required: true,
|
|
920
|
-
description: "
|
|
950
|
+
description: "execution id (from prepareDagExecution) — raw dag path is never accepted from the model",
|
|
921
951
|
},
|
|
922
952
|
],
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
|
|
926
|
-
|
|
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.
|
|
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
|
-
|
|
994
|
-
|
|
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
|
|
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:
|
|
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: "
|
|
1311
|
+
name: "profile",
|
|
1278
1312
|
type: "string",
|
|
1279
|
-
required:
|
|
1280
|
-
description: "
|
|
1313
|
+
required: false,
|
|
1314
|
+
description: "DAG profile (defaults to auto)",
|
|
1281
1315
|
},
|
|
1282
1316
|
{
|
|
1283
|
-
name: "
|
|
1284
|
-
type: "
|
|
1285
|
-
required:
|
|
1286
|
-
description: "
|
|
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
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
+
}
|