@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -17
- 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-executor.js +4 -91
- 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/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- 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/session-store.js +39 -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-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-CMHovlqG.js +32 -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/landing-density.js +23 -0
- 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/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -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/useOverlayFocus.js +84 -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/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -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 +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- 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 +34 -46
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- 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({
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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",
|