@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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
2
|
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { isResilientGitCommandError, runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
6
5
|
import YAML from "yaml";
|
|
7
6
|
import { z } from "zod";
|
|
8
7
|
import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
|
|
@@ -13,7 +12,6 @@ import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
|
13
12
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
14
13
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
15
14
|
import { readVerifiedBundle, verifyBundleCurrentStateBindings, verifyBundleOutcomes, verifyBundleTaskSpecBindings } from "./verification-bundle.js";
|
|
16
|
-
const execFileAsync = promisify(execFile);
|
|
17
15
|
const hashedRefSchema = z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
18
16
|
export const acceptanceCoverageArtifactSchema = z.object({
|
|
19
17
|
schemaVersion: z.literal(1),
|
|
@@ -91,9 +89,9 @@ export async function prepareFeatureDelivery(input) {
|
|
|
91
89
|
const now = input.now ?? new Date();
|
|
92
90
|
const record = await readTransaction(repoRoot, featureId, blockers);
|
|
93
91
|
if (record) {
|
|
94
|
-
const branch = await git(repoRoot, ["branch", "--show-current"]).catch((
|
|
95
|
-
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch((
|
|
96
|
-
const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch((
|
|
92
|
+
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(rethrowInfrastructureOr(""));
|
|
93
|
+
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
|
|
94
|
+
const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
|
|
97
95
|
if (branch !== record.branch)
|
|
98
96
|
blockers.push(`current branch must be ${record.branch}`);
|
|
99
97
|
if (head !== record.lastCheckpoint)
|
|
@@ -202,11 +200,11 @@ export async function validateDeliveryPackage(input) {
|
|
|
202
200
|
blockers.push("Delivery Feature ownership mismatch");
|
|
203
201
|
if (manifest.branch !== record.branch || manifest.baseBranch !== record.baseBranch || manifest.baseSha !== record.baseCommit || manifest.headSha !== record.lastCheckpoint)
|
|
204
202
|
blockers.push("Delivery manifest does not match the Git transaction");
|
|
205
|
-
const branch = await git(repoRoot, ["branch", "--show-current"]).catch((
|
|
206
|
-
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch((
|
|
203
|
+
const branch = await git(repoRoot, ["branch", "--show-current"]).catch(rethrowInfrastructureOr(""));
|
|
204
|
+
const head = await git(repoRoot, ["rev-parse", "HEAD"]).catch(rethrowInfrastructureOr(""));
|
|
207
205
|
if (branch !== record.branch || head !== record.lastCheckpoint)
|
|
208
206
|
blockers.push("current Git position does not match Delivery");
|
|
209
|
-
const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch((
|
|
207
|
+
const rawCloseoutStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).catch(rethrowInfrastructureOr("git-error"));
|
|
210
208
|
const allowedDirtyPaths = new Set((input.allowedDirtyPaths ?? []).map((item) => item.replace(/\\/g, "/")));
|
|
211
209
|
const closeoutStatus = rawCloseoutStatus.split("\n").filter(Boolean).filter((line) => !allowedDirtyPaths.has(line.slice(3).replace(/\\/g, "/"))).join("\n").trim();
|
|
212
210
|
if (closeoutStatus)
|
|
@@ -495,23 +493,37 @@ async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
|
|
|
495
493
|
}
|
|
496
494
|
}
|
|
497
495
|
function renderPr(manifest) { return `# Summary\n\nFeature ${manifest.featureId} local Delivery Package.\n\n## Feature/base/head\n\n- Branch: ${manifest.branch}\n- Base: ${manifest.baseBranch} @ ${manifest.baseSha}\n- Head: ${manifest.headSha}\n\n## Completed Tasks\n\n${manifest.tasks.map((task) => `- ${task.taskId}: ${task.commit} (${task.changedFiles.join(", ")})`).join("\n")}\n\n## Acceptance Coverage\n\n- Required: ${manifest.acceptanceCoverage.requiredTotal}\n- Covered: ${manifest.acceptanceCoverage.covered}\n- Waived: ${manifest.acceptanceCoverage.waived}\n- Blocked: ${manifest.acceptanceCoverage.blocked}\n\n## Verification\n\n- QA: ${manifest.qa.evidence.path}\n- Final: ${manifest.finalVerification.path}\n\n## Changed Files\n\n${[...new Set(manifest.tasks.flatMap((task) => task.changedFiles))].map((file) => `- ${file}`).join("\n")}\n\n## Risks\n\n- high=${manifest.riskSummary.high}, medium=${manifest.riskSummary.medium}, low=${manifest.riskSummary.low}\n\n## Human Review Checklist\n\n- [ ] Review diff and commits\n- [ ] Confirm required AC coverage and waivers\n- [ ] Confirm QA and final verification evidence\n- [ ] Review risks\n- [ ] Decide whether to push or create a remote PR manually\n`; }
|
|
498
|
-
async function git(repoRoot, args) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return
|
|
496
|
+
async function git(repoRoot, args) {
|
|
497
|
+
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
498
|
+
if (result.code !== 0)
|
|
499
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
500
|
+
return result.stdout.trim();
|
|
501
|
+
}
|
|
502
|
+
async function gitRaw(repoRoot, args) {
|
|
503
|
+
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
504
|
+
if (result.code !== 0)
|
|
505
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
506
|
+
return result.stdout;
|
|
507
|
+
}
|
|
508
|
+
async function gitOk(repoRoot, args) {
|
|
509
|
+
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
510
|
+
return result.code === 0;
|
|
511
|
+
}
|
|
512
|
+
function rethrowInfrastructureOr(fallback) {
|
|
513
|
+
return (error) => {
|
|
514
|
+
if (isResilientGitCommandError(error))
|
|
515
|
+
throw error;
|
|
516
|
+
return fallback;
|
|
517
|
+
};
|
|
503
518
|
}
|
|
504
|
-
catch {
|
|
505
|
-
return false;
|
|
506
|
-
} }
|
|
507
519
|
async function checkpointMatches(repoRoot, head, checkpoint, spec) {
|
|
508
520
|
if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", checkpoint.commit, head]))
|
|
509
521
|
return false;
|
|
510
|
-
const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch((
|
|
522
|
+
const messageText = await gitRaw(repoRoot, ["show", "-s", "--format=%B", checkpoint.commit]).catch(rethrowInfrastructureOr(""));
|
|
511
523
|
for (const trailer of [`Feature: ${spec.feature_id}`, `Task: ${spec.id}`, `Acceptance: ${spec.acceptance_refs.join(", ")}`, `Worker-Run: ${checkpoint.workerRunId}`])
|
|
512
524
|
if (!messageText.split(/\r?\n/).includes(trailer))
|
|
513
525
|
return false;
|
|
514
|
-
const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch((
|
|
526
|
+
const actual = (await gitRaw(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", checkpoint.commit]).catch(rethrowInfrastructureOr(""))).split(/\r?\n/).filter(Boolean).sort();
|
|
515
527
|
return JSON.stringify(actual) === JSON.stringify([...checkpoint.changedFiles].sort());
|
|
516
528
|
}
|
|
517
529
|
async function hashedRefValid(repoRoot, ref) { try {
|
|
@@ -7,6 +7,7 @@ import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outc
|
|
|
7
7
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
8
8
|
import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
|
|
9
9
|
import { workflowSchema } from "../task-spec/workflow-routing.js";
|
|
10
|
+
import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
10
11
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
11
12
|
/**
|
|
12
13
|
* Feature Verification Bundle v1 — a hash-bound aggregate of completed typed
|
|
@@ -533,10 +534,11 @@ async function readGitRecord(repoRoot, featureId) {
|
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
async function defaultGit(repoRoot, args) {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
537
|
+
const result = await runResilientGitCommand({ cwd: repoRoot, args, readOnly: true });
|
|
538
|
+
if (result.code !== 0) {
|
|
539
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
|
|
540
|
+
}
|
|
541
|
+
return result.stdout.trim();
|
|
540
542
|
}
|
|
541
543
|
// Re-export the workflow schema so consumers can validate typed workflows.
|
|
542
544
|
export { workflowSchema };
|
|
@@ -353,7 +353,12 @@ export function mountOperatorChrome(root, config = {}) {
|
|
|
353
353
|
|
|
354
354
|
const brand = el("a", "och-brand");
|
|
355
355
|
brand.href = model.homeHref;
|
|
356
|
-
|
|
356
|
+
brand.setAttribute("aria-label", `${model.brand} 首页`);
|
|
357
|
+
const brandMark = el("img", "och-brand-mark");
|
|
358
|
+
brandMark.src = "/inspect/favicon.svg";
|
|
359
|
+
brandMark.alt = "";
|
|
360
|
+
brandMark.setAttribute("aria-hidden", "true");
|
|
361
|
+
brand.appendChild(brandMark);
|
|
357
362
|
brand.appendChild(el("span", null, model.brand));
|
|
358
363
|
global.appendChild(brand);
|
|
359
364
|
|
|
@@ -1617,7 +1617,11 @@ body.is-resizing-dag-graph {
|
|
|
1617
1617
|
/* 历史失败已恢复/已覆盖:独立中性样式(不占用当前终态失败红色,不洗白事实)。 */
|
|
1618
1618
|
.dag-trajectory-occurrence.is-recovered {
|
|
1619
1619
|
border-color: color-mix(in srgb, var(--amber, #b58900) 55%, var(--hairline));
|
|
1620
|
-
background: color-mix(
|
|
1620
|
+
background: color-mix(
|
|
1621
|
+
in srgb,
|
|
1622
|
+
var(--amber-soft, #fff7d6) 55%,
|
|
1623
|
+
var(--surface)
|
|
1624
|
+
);
|
|
1621
1625
|
}
|
|
1622
1626
|
.dag-trajectory-recovered {
|
|
1623
1627
|
font-size: 11px;
|
|
@@ -1647,9 +1651,14 @@ body.is-resizing-dag-graph {
|
|
|
1647
1651
|
}
|
|
1648
1652
|
.dag-trajectory-banner {
|
|
1649
1653
|
padding: 8px 12px;
|
|
1650
|
-
border: 1px solid
|
|
1654
|
+
border: 1px solid
|
|
1655
|
+
color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
|
|
1651
1656
|
border-radius: var(--radius-lg);
|
|
1652
|
-
background: color-mix(
|
|
1657
|
+
background: color-mix(
|
|
1658
|
+
in srgb,
|
|
1659
|
+
var(--amber-soft, #fff7d6) 60%,
|
|
1660
|
+
var(--surface)
|
|
1661
|
+
);
|
|
1653
1662
|
color: var(--body);
|
|
1654
1663
|
font-size: 12px;
|
|
1655
1664
|
}
|
|
@@ -3050,7 +3059,11 @@ button.copy-command:focus-visible {
|
|
|
3050
3059
|
}
|
|
3051
3060
|
.dags-copy-btn:active {
|
|
3052
3061
|
border-color: var(--hairline-strong, #cfcdc4);
|
|
3053
|
-
background: color-mix(
|
|
3062
|
+
background: color-mix(
|
|
3063
|
+
in srgb,
|
|
3064
|
+
var(--surface-muted, #f0efe9) 80%,
|
|
3065
|
+
var(--hairline, #e6e5e0)
|
|
3066
|
+
);
|
|
3054
3067
|
}
|
|
3055
3068
|
/* Mouse focus must not leave residual orange chrome; orange is :focus-visible only. */
|
|
3056
3069
|
.dags-copy-btn:focus:not(:focus-visible) {
|
|
@@ -3201,12 +3214,20 @@ button.copy-command:focus-visible {
|
|
|
3201
3214
|
}
|
|
3202
3215
|
.dags-page-num:hover:not(:disabled),
|
|
3203
3216
|
.dags-page-nav:hover:not(:disabled) {
|
|
3204
|
-
background: color-mix(
|
|
3217
|
+
background: color-mix(
|
|
3218
|
+
in srgb,
|
|
3219
|
+
var(--orange-soft, #fff1ea) 70%,
|
|
3220
|
+
var(--surface-muted, #f0efe9)
|
|
3221
|
+
);
|
|
3205
3222
|
color: var(--ink, #26251e);
|
|
3206
3223
|
}
|
|
3207
3224
|
.dags-page-num:active:not(:disabled),
|
|
3208
3225
|
.dags-page-nav:active:not(:disabled) {
|
|
3209
|
-
background: color-mix(
|
|
3226
|
+
background: color-mix(
|
|
3227
|
+
in srgb,
|
|
3228
|
+
var(--orange, #f54e00) 14%,
|
|
3229
|
+
var(--surface-muted, #f0efe9)
|
|
3230
|
+
);
|
|
3210
3231
|
}
|
|
3211
3232
|
.dags-page-num:focus-visible,
|
|
3212
3233
|
.dags-page-nav:focus-visible,
|
|
@@ -3224,7 +3245,8 @@ button.copy-command:focus-visible {
|
|
|
3224
3245
|
border-color: var(--orange, #f54e00);
|
|
3225
3246
|
color: #fff;
|
|
3226
3247
|
opacity: 1;
|
|
3227
|
-
box-shadow: 0 2px 8px
|
|
3248
|
+
box-shadow: 0 2px 8px
|
|
3249
|
+
color-mix(in srgb, var(--orange, #f54e00) 35%, transparent);
|
|
3228
3250
|
cursor: default;
|
|
3229
3251
|
}
|
|
3230
3252
|
.dags-page-nav:disabled,
|
|
@@ -3320,7 +3342,11 @@ button.copy-command:focus-visible {
|
|
|
3320
3342
|
background: var(--surface-muted, #f0efe9);
|
|
3321
3343
|
}
|
|
3322
3344
|
.dags-refresh-btn:active:not(:disabled) {
|
|
3323
|
-
background: color-mix(
|
|
3345
|
+
background: color-mix(
|
|
3346
|
+
in srgb,
|
|
3347
|
+
var(--surface-muted, #f0efe9) 80%,
|
|
3348
|
+
var(--hairline, #e6e5e0)
|
|
3349
|
+
);
|
|
3324
3350
|
}
|
|
3325
3351
|
.dags-pagination.is-busy {
|
|
3326
3352
|
opacity: 0.92;
|
|
@@ -3516,7 +3542,11 @@ button.copy-command:focus-visible {
|
|
|
3516
3542
|
}
|
|
3517
3543
|
.custom-select-option.is-selected.is-active,
|
|
3518
3544
|
.custom-select-option.is-selected:hover {
|
|
3519
|
-
background: color-mix(
|
|
3545
|
+
background: color-mix(
|
|
3546
|
+
in srgb,
|
|
3547
|
+
var(--orange-soft, #fff1ea) 70%,
|
|
3548
|
+
var(--surface-muted, #f0efe9)
|
|
3549
|
+
);
|
|
3520
3550
|
}
|
|
3521
3551
|
.custom-select-option-label {
|
|
3522
3552
|
flex: 1 1 auto;
|
|
@@ -909,10 +909,6 @@ function symbolCaseId(symbol) {
|
|
|
909
909
|
const match = symbol.match(/^test_(BE(?:_[A-Z0-9]+)+?_\d{2,3})(?:_|$)/i);
|
|
910
910
|
return match ? canonicalCaseId(match[1].replaceAll("_", "-")) : undefined;
|
|
911
911
|
}
|
|
912
|
-
function metadataCaseIds(region) {
|
|
913
|
-
return orderedUnique([...region.matchAll(/^\s*Case-ID\s*:\s*(BE-[A-Z0-9]+(?:-[A-Z0-9]+)*)\s*$/gmi)]
|
|
914
|
-
.map((match) => canonicalCaseId(match[1])));
|
|
915
|
-
}
|
|
916
912
|
function metadataTestPoints(region, label) {
|
|
917
913
|
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
918
914
|
const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
|
|
@@ -932,27 +928,6 @@ function pytestFunctionRegionStart(source, functionIndex) {
|
|
|
932
928
|
}
|
|
933
929
|
return regionStart;
|
|
934
930
|
}
|
|
935
|
-
function pytestFunctionBodyEnd(source, signatureEnd, functionIndent) {
|
|
936
|
-
let cursor = source.indexOf("\n", signatureEnd);
|
|
937
|
-
if (cursor < 0)
|
|
938
|
-
return source.length;
|
|
939
|
-
cursor += 1;
|
|
940
|
-
const baseIndent = functionIndent.replaceAll("\t", " ").length;
|
|
941
|
-
while (cursor < source.length) {
|
|
942
|
-
const nextLine = source.indexOf("\n", cursor);
|
|
943
|
-
const lineEnd = nextLine < 0 ? source.length : nextLine;
|
|
944
|
-
const line = source.slice(cursor, lineEnd).replace(/\r$/, "");
|
|
945
|
-
if (line.trim()) {
|
|
946
|
-
const indentation = line.match(/^[ \t]*/)?.[0]?.replaceAll("\t", " ").length ?? 0;
|
|
947
|
-
if (indentation <= baseIndent)
|
|
948
|
-
return cursor;
|
|
949
|
-
}
|
|
950
|
-
if (nextLine < 0)
|
|
951
|
-
return source.length;
|
|
952
|
-
cursor = nextLine + 1;
|
|
953
|
-
}
|
|
954
|
-
return source.length;
|
|
955
|
-
}
|
|
956
931
|
function pytestParameterCollections(source) {
|
|
957
932
|
const collections = new Map();
|
|
958
933
|
for (const match of source.matchAll(/^([A-Z][A-Z0-9_]*)\s*=\s*\[([\s\S]*?)^\]/gm)) {
|
|
@@ -966,10 +941,10 @@ function pytestSymbols(script, source) {
|
|
|
966
941
|
const parameterCollections = pytestParameterCollections(source);
|
|
967
942
|
return matches.map((match, index) => {
|
|
968
943
|
const regionStart = regionStarts[index];
|
|
969
|
-
const regionEnd =
|
|
944
|
+
const regionEnd = regionStarts[index + 1] ?? source.length;
|
|
970
945
|
const region = source.slice(match.index, regionEnd);
|
|
971
946
|
const decorators = source.slice(regionStart, match.index);
|
|
972
|
-
const ids =
|
|
947
|
+
const ids = caseIds(region);
|
|
973
948
|
const fromSymbol = symbolCaseId(match[2]);
|
|
974
949
|
if (fromSymbol)
|
|
975
950
|
ids.unshift(fromSymbol);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
const PRIORITY_ONLY_MODULE_STEMS = new Set(["p0", "p1", "p2"]);
|
|
3
|
-
const OPAQUE_HASH_MODULE_STEM = /^[a-f][a-f0-9]{6,63}$/;
|
|
4
|
-
export const BACKEND_TEST_MAX_MODULE_COUNT = 8;
|
|
5
3
|
export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
6
4
|
return path.basename(raw)
|
|
7
5
|
.replace(/\.(?:md|py)$/i, "")
|
|
@@ -13,9 +11,6 @@ export function normalizeBackendTestModuleStemCandidate(raw) {
|
|
|
13
11
|
export function isPriorityOnlyBackendTestModuleStem(raw) {
|
|
14
12
|
return PRIORITY_ONLY_MODULE_STEMS.has(normalizeBackendTestModuleStemCandidate(raw));
|
|
15
13
|
}
|
|
16
|
-
export function isOpaqueHashBackendTestModuleStem(raw) {
|
|
17
|
-
return OPAQUE_HASH_MODULE_STEM.test(normalizeBackendTestModuleStemCandidate(raw));
|
|
18
|
-
}
|
|
19
14
|
export function isPriorityOnlyBackendPytestScript(script) {
|
|
20
15
|
const basename = path.posix.basename(script.replaceAll("\\", "/"));
|
|
21
16
|
const match = /^test_(.+)\.py$/i.exec(basename);
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
|
|
6
6
|
/**
|
|
7
7
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
8
8
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -103,8 +103,6 @@ function moduleStemInvalidReason(raw) {
|
|
|
103
103
|
const stem = normalizeBackendTestModuleStem(raw);
|
|
104
104
|
if (isPriorityOnlyBackendTestModuleStem(stem))
|
|
105
105
|
return "priority-only-module-stem";
|
|
106
|
-
if (isOpaqueHashBackendTestModuleStem(stem))
|
|
107
|
-
return "opaque-hash-module-stem";
|
|
108
106
|
if (stem.toLowerCase() === "readme")
|
|
109
107
|
return "reserved-module-stem";
|
|
110
108
|
if (!/^[a-z][a-z0-9_]*$/.test(stem))
|
|
@@ -116,40 +114,28 @@ function moduleStemInvalidReason(raw) {
|
|
|
116
114
|
function looksLikeValidModuleStem(raw) {
|
|
117
115
|
return moduleStemInvalidReason(raw) === undefined;
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
const lines = readme
|
|
121
|
-
.replaceAll("\r\n", "\n")
|
|
122
|
-
.replaceAll("\r", "\n")
|
|
123
|
-
.split("\n");
|
|
124
|
-
const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
|
|
125
|
-
if (headings.length === 0) {
|
|
126
|
-
return { section: "", structureIssues: ["missing-module-index"] };
|
|
127
|
-
}
|
|
128
|
-
if (headings.length > 1) {
|
|
129
|
-
return { section: "", structureIssues: ["duplicate-module-index"] };
|
|
130
|
-
}
|
|
131
|
-
const start = headings[0] + 1;
|
|
132
|
-
const relativeEnd = lines.slice(start).findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
133
|
-
const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
|
|
134
|
-
return { section: lines.slice(start, end).join("\n"), structureIssues: [] };
|
|
135
|
-
}
|
|
136
|
-
/** Inspect only candidates inside the unique exact `## Module Index` section. */
|
|
117
|
+
/** Inspect only authoritative README module-index candidates. */
|
|
137
118
|
export function inspectModuleStemsFromReadme(readme) {
|
|
138
|
-
const extracted = extractCanonicalModuleIndexSection(readme);
|
|
139
119
|
const candidates = [];
|
|
140
|
-
const tableRowLines =
|
|
120
|
+
const tableRowLines = readme
|
|
121
|
+
.replaceAll("\r\n", "\n")
|
|
122
|
+
.replaceAll("\r", "\n")
|
|
123
|
+
.split("\n")
|
|
124
|
+
.filter((line) => line.includes("|"));
|
|
141
125
|
for (const line of tableRowLines) {
|
|
142
126
|
for (const match of line.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
|
|
143
127
|
candidates.push(match[1]);
|
|
144
128
|
}
|
|
145
129
|
for (const match of line.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
// This compatibility shape can also capture a numeric count column.
|
|
131
|
+
// Keep valid stems and the priority-only defect we diagnose, while
|
|
132
|
+
// ignoring non-stem incidental cells such as `1`.
|
|
133
|
+
if (looksLikeValidModuleStem(match[1]) || isPriorityOnlyBackendTestModuleStem(match[1])) {
|
|
134
|
+
candidates.push(match[1]);
|
|
149
135
|
}
|
|
150
136
|
}
|
|
151
137
|
}
|
|
152
|
-
for (const match of
|
|
138
|
+
for (const match of readme.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
|
|
153
139
|
candidates.push(match[1]);
|
|
154
140
|
}
|
|
155
141
|
const uniqueCandidates = orderedUnique(candidates);
|
|
@@ -159,18 +145,12 @@ export function inspectModuleStemsFromReadme(readme) {
|
|
|
159
145
|
? [{ raw, normalized: normalizeBackendTestModuleStem(raw), reasonCode }]
|
|
160
146
|
: [];
|
|
161
147
|
});
|
|
162
|
-
const validStems = orderedUnique(uniqueCandidates
|
|
163
|
-
.filter(looksLikeValidModuleStem)
|
|
164
|
-
.map((stem) => normalizeBackendTestModuleStem(stem)));
|
|
165
|
-
const structureIssues = [...extracted.structureIssues];
|
|
166
|
-
if (validStems.length > BACKEND_TEST_MAX_MODULE_COUNT) {
|
|
167
|
-
structureIssues.push(`excessive-module-count: ${validStems.length} > ${BACKEND_TEST_MAX_MODULE_COUNT}`);
|
|
168
|
-
}
|
|
169
148
|
return {
|
|
170
149
|
candidates: uniqueCandidates,
|
|
171
|
-
validStems
|
|
150
|
+
validStems: orderedUnique(uniqueCandidates
|
|
151
|
+
.filter(looksLikeValidModuleStem)
|
|
152
|
+
.map((stem) => normalizeBackendTestModuleStem(stem))),
|
|
172
153
|
invalid,
|
|
173
|
-
structureIssues,
|
|
174
154
|
};
|
|
175
155
|
}
|
|
176
156
|
/**
|
|
@@ -937,17 +917,6 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
|
|
|
937
917
|
// are produced by map_agent children and are NOT required here.
|
|
938
918
|
const stemInspection = inspectModuleStemsFromReadme(readme);
|
|
939
919
|
const indexedStems = stemInspection.validStems;
|
|
940
|
-
for (const structureIssue of stemInspection.structureIssues) {
|
|
941
|
-
if (!brokenPaths.includes("testcase/md/README.md")) {
|
|
942
|
-
brokenPaths.push("testcase/md/README.md");
|
|
943
|
-
}
|
|
944
|
-
issues.push({
|
|
945
|
-
code: "T5",
|
|
946
|
-
path: "testcase/md/README.md",
|
|
947
|
-
detail: structureIssue,
|
|
948
|
-
recoverable: true,
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
920
|
for (const invalid of stemInspection.invalid) {
|
|
952
921
|
if (!brokenPaths.includes("testcase/md/README.md")) {
|
|
953
922
|
brokenPaths.push("testcase/md/README.md");
|
|
@@ -367,26 +367,19 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
const budgetBlockedIds = new Set(blockedChildren.map((entry) => entry.nodeId));
|
|
370
|
-
const failOnTokenBudgetExhaustion = input.expansion.failOnTokenBudgetExhaustion === true;
|
|
371
370
|
const failedChildren = tolerateChildFailures
|
|
372
371
|
? []
|
|
373
372
|
: childNodeIds.filter((nodeId) => {
|
|
374
373
|
const record = input.state.nodes[nodeId];
|
|
375
374
|
if (!record || record.status === "FINISHED")
|
|
376
375
|
return false;
|
|
377
|
-
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
378
|
-
return
|
|
379
|
-
}
|
|
376
|
+
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
377
|
+
return false;
|
|
380
378
|
return true;
|
|
381
379
|
});
|
|
382
|
-
const finishedItemCount = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status === "FINISHED").length;
|
|
383
|
-
const blockedItemCount = budgetBlockedIds.size;
|
|
384
380
|
const aggregate = {
|
|
385
381
|
workflowNodeId: input.expansion.workflowNodeId,
|
|
386
382
|
itemCount: items.length,
|
|
387
|
-
expectedItemCount: items.length,
|
|
388
|
-
finishedItemCount,
|
|
389
|
-
blockedItemCount,
|
|
390
383
|
children: childNodeIds.map((nodeId, index) => ({
|
|
391
384
|
nodeId,
|
|
392
385
|
item: items[index],
|
|
@@ -406,30 +399,21 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
406
399
|
?.reason,
|
|
407
400
|
})),
|
|
408
401
|
tokensUsed: totalTokensUsed,
|
|
409
|
-
maxTotalTokens: tokenBudget?.maxTotalTokens,
|
|
410
402
|
caseOutcomeNotes: caseOutcomeNotes.length > 0 ? caseOutcomeNotes : undefined,
|
|
411
403
|
};
|
|
412
|
-
const tokenBudgetFailure = failOnTokenBudgetExhaustion && blockedItemCount > 0;
|
|
413
404
|
return {
|
|
414
|
-
// With tolerateChildFailures: barrier succeeds
|
|
415
|
-
// case evidence.
|
|
416
|
-
|
|
417
|
-
ok: failedChildren.length === 0 && !tokenBudgetFailure,
|
|
405
|
+
// With tolerateChildFailures: barrier always succeeds; product outcomes live in
|
|
406
|
+
// case evidence + result materialization. Without it: real child failures fail-close.
|
|
407
|
+
ok: failedChildren.length === 0,
|
|
418
408
|
stdout: JSON.stringify(aggregate),
|
|
419
409
|
stderr: caseOutcomeNotes.length > 0
|
|
420
410
|
? `map children recorded as case outcomes: ${caseOutcomeNotes
|
|
421
411
|
.map((entry) => `${entry.nodeId}=${entry.status}:${entry.reason}`)
|
|
422
412
|
.join(", ")}`
|
|
423
|
-
: tokenBudgetFailure
|
|
424
|
-
? `dynamic map token budget exhausted: expected=${items.length} finished=${finishedItemCount} blocked=${blockedItemCount} tokensUsed=${totalTokensUsed} maxTotalTokens=${tokenBudget?.maxTotalTokens ?? "unbounded"}`
|
|
425
|
-
: failedChildren.length > 0
|
|
426
|
-
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
427
|
-
: "",
|
|
428
|
-
failureCategory: tokenBudgetFailure
|
|
429
|
-
? "dynamic-expansion-token-budget-exhausted"
|
|
430
413
|
: failedChildren.length > 0
|
|
431
|
-
?
|
|
432
|
-
: "
|
|
414
|
+
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
415
|
+
: "",
|
|
416
|
+
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
433
417
|
durationMs: Date.now() - started,
|
|
434
418
|
};
|
|
435
419
|
}
|
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
|
|
8
8
|
export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
|
|
9
|
-
function runGit(cwd, args) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cwd,
|
|
13
|
-
env: process.env,
|
|
14
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
15
|
-
});
|
|
16
|
-
let stdout = "";
|
|
17
|
-
let stderr = "";
|
|
18
|
-
child.stdout.on("data", (chunk) => {
|
|
19
|
-
stdout += String(chunk);
|
|
20
|
-
});
|
|
21
|
-
child.stderr.on("data", (chunk) => {
|
|
22
|
-
stderr += String(chunk);
|
|
23
|
-
});
|
|
24
|
-
child.on("error", reject);
|
|
25
|
-
child.on("close", (code) => {
|
|
26
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
27
|
-
});
|
|
28
|
-
});
|
|
9
|
+
async function runGit(cwd, args) {
|
|
10
|
+
const result = await runResilientGitCommand({ cwd, args, readOnly: true });
|
|
11
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
29
12
|
}
|
|
30
13
|
function splitLines(text) {
|
|
31
14
|
return text
|
|
@@ -52,8 +35,9 @@ async function sha256File(filePath) {
|
|
|
52
35
|
}
|
|
53
36
|
}
|
|
54
37
|
export async function captureFrontendWorktreeBaseline(input) {
|
|
38
|
+
const run = input.dependencies?.runGit ?? runGit;
|
|
55
39
|
const root = path.resolve(input.workspaceRoot);
|
|
56
|
-
const status = await
|
|
40
|
+
const status = await run(root, [
|
|
57
41
|
"status",
|
|
58
42
|
"--porcelain=v1",
|
|
59
43
|
"--untracked-files=all",
|
|
@@ -84,6 +68,7 @@ export async function captureFrontendWorktreeBaseline(input) {
|
|
|
84
68
|
* patch + inventory under the current run directory before review runs.
|
|
85
69
|
*/
|
|
86
70
|
export async function runFrontendWorktreeDiffGate(input) {
|
|
71
|
+
const run = input.dependencies?.runGit ?? runGit;
|
|
87
72
|
const root = path.resolve(input.workspaceRoot);
|
|
88
73
|
let baseline;
|
|
89
74
|
try {
|
|
@@ -101,16 +86,16 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
101
86
|
if (input.requireBaseline && !baseline) {
|
|
102
87
|
throw new Error("frontend worktree baseline is required for this review context");
|
|
103
88
|
}
|
|
104
|
-
const revParse = await
|
|
89
|
+
const revParse = await run(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
105
90
|
if (revParse.code !== 0 || revParse.stdout.trim() !== "true") {
|
|
106
91
|
throw new Error(`frontend worktree diff gate requires a git worktree: ${revParse.stderr.trim() || revParse.stdout.trim() || "not a git repository"}`);
|
|
107
92
|
}
|
|
108
93
|
// Match failure capture semantics: binary-capable tracked diff vs HEAD.
|
|
109
|
-
const diff = await
|
|
94
|
+
const diff = await run(root, ["diff", "--binary", "HEAD"]);
|
|
110
95
|
if (diff.code !== 0) {
|
|
111
96
|
throw new Error(`frontend worktree diff gate git diff failed: ${diff.stderr.trim() || diff.stdout.trim()}`);
|
|
112
97
|
}
|
|
113
|
-
const nameOnly = await
|
|
98
|
+
const nameOnly = await run(root, [
|
|
114
99
|
"diff",
|
|
115
100
|
"--name-only",
|
|
116
101
|
"--diff-filter=ACDMRTUXB",
|
|
@@ -119,7 +104,7 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
119
104
|
if (nameOnly.code !== 0) {
|
|
120
105
|
throw new Error(`frontend worktree diff gate git name-only failed: ${nameOnly.stderr.trim() || nameOnly.stdout.trim()}`);
|
|
121
106
|
}
|
|
122
|
-
const untracked = await
|
|
107
|
+
const untracked = await run(root, [
|
|
123
108
|
"ls-files",
|
|
124
109
|
"--others",
|
|
125
110
|
"--exclude-standard",
|
|
@@ -152,7 +137,7 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
152
137
|
});
|
|
153
138
|
const patchSource = baseline
|
|
154
139
|
? changedFiles.length > 0
|
|
155
|
-
? await
|
|
140
|
+
? await run(root, ["diff", "--binary", "HEAD", "--", ...changedFiles])
|
|
156
141
|
: { code: 0, stdout: "", stderr: "" }
|
|
157
142
|
: diff;
|
|
158
143
|
if (patchSource.code !== 0) {
|