@tea-agent/loop-agent 0.22.0 → 0.24.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/AGENTS.md +42 -108
- package/CHANGELOG.md +85 -0
- package/README.md +8 -5
- package/bin/agent-worker.js +0 -0
- package/dist/application/context-usage/skill-resolution-stats.js +263 -0
- package/dist/application/dag/generate-task-dag.js +17 -3
- package/dist/cli/command-definitions.js +8 -7
- package/dist/cli/program.js +17 -15
- package/dist/commands/doctor.js +269 -18
- package/dist/commands/init.js +101 -86
- package/dist/commands/stats.js +40 -11
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +486 -3
- package/dist/worker/console/app-data.js +6 -0
- package/dist/worker/console/chat/artifact-card.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +495 -0
- package/dist/worker/console/chat/chat-ui-policy.js +25 -0
- package/dist/worker/console/chat/composer-draft-store.js +45 -0
- package/dist/worker/console/chat/context-panel.js +54 -0
- package/dist/worker/console/chat/contract-apply-receipt-store.js +174 -0
- package/dist/worker/console/chat/explore-tools.js +299 -0
- package/dist/worker/console/chat/human-gate-card.js +37 -0
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/interview-adapter.js +136 -0
- package/dist/worker/console/chat/model-resolver.js +106 -0
- package/dist/worker/console/chat/operation-card.js +23 -0
- package/dist/worker/console/chat/pi-console-config.js +158 -0
- package/dist/worker/console/chat/pi-runtime.js +1143 -0
- package/dist/worker/console/chat/repo-browser.js +140 -0
- package/dist/worker/console/chat/repo-walk.js +116 -0
- package/dist/worker/console/chat/resource-loader.js +67 -0
- package/dist/worker/console/chat/routes.js +1646 -0
- package/dist/worker/console/chat/runtime-context.js +24 -0
- package/dist/worker/console/chat/runtime-selection.js +37 -0
- package/dist/worker/console/chat/session-store.js +437 -0
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +125 -0
- package/dist/worker/console/chat/tools.js +195 -0
- package/dist/worker/console/chat/usage.js +37 -0
- package/dist/worker/console/chat/workspace-landing.js +56 -0
- package/dist/worker/console/dag-confirmation.js +42 -8
- package/dist/worker/console/human-gate-token.js +130 -0
- package/dist/worker/console/mutation-gate-receipt-store.js +184 -0
- package/dist/worker/console/operation-runner.js +6 -2
- package/dist/worker/console/operation-sse.js +26 -0
- package/dist/worker/console/operator-actions.js +420 -7
- package/dist/worker/console/server.js +68 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +1 -0
- package/dist/worker/console/static/assets/index-D9qLevoP.js +27 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/feature/profile-schema.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -5
- package/dist/workflows/dag/backend-test-result-contract.js +229 -0
- package/dist/workflows/dag/frontend-lint-baseline.js +4 -4
- package/dist/workflows/dag/init-hybrid.js +71 -22
- package/dist/workflows/dag/node-execution.js +38 -1
- package/dist/workflows/dag/output-protocol.js +89 -0
- package/dist/workflows/dag/prompt.js +35 -1
- package/dist/workflows/dag/recovery-recommendation.js +45 -0
- package/dist/workflows/dag/report.js +28 -1
- package/dist/workflows/dag/rerun-task.js +1 -1
- package/dist/workflows/dag/scheduler.js +9 -0
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +55 -0
- package/docs/README.md +73 -156
- package/docs/architecture/README.md +7 -6
- package/docs/architecture/dag-execution.md +2 -2
- package/docs/architecture/evolution.md +16 -14
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/governance/README.md +15 -0
- package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
- package/docs/init-surface.manifest.json +22 -4
- package/docs/operations/README.md +12 -0
- package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
- package/docs/skills/vetted-skill-registry.md +23 -3
- package/docs/templates/README.md +55 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/progress-log.md +9 -2
- package/harness.json +4 -4
- package/package.json +5 -5
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/grill-with-docs/SKILL.md +44 -52
- package/skills/grill-with-docs/adr-format.md +37 -26
- package/skills/grill-with-docs/context-format.md +18 -26
- package/skills/loop-agent/SKILL.md +28 -112
- package/skills/loop-agent/references/command-reference.md +9 -3
- package/skills/loop-agent/references/harness-policy.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/task-workflow.md +2 -0
- package/skills/systematic-debugging/SKILL.md +20 -4
- package/skills/test-driven-development/SKILL.md +10 -3
- package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
- package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
- /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
- /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loop 操作台 · Operator Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-D9qLevoP.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BTbrEHnO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
* `fullstack-v1` is NOT a new DAG or taskKind. It is `agent-worker`'s
|
|
6
6
|
* deterministic structural validation + read-model projection policy over a
|
|
7
7
|
* Feature Packet. See
|
|
8
|
-
* `docs/design/agent-worker-fullstack-workflow-integration.md` §9.
|
|
8
|
+
* `docs/design/active/agent-worker-fullstack-workflow-integration.md` §9.
|
|
9
9
|
*
|
|
10
10
|
* A Feature Packet declares its profile via an optional `feature.yaml` next to
|
|
11
11
|
* `acceptance.yaml`. A packet without `feature.yaml` (or one that does not set
|
|
@@ -97,13 +97,19 @@ export async function inspectBackendTestEnvironment(input) {
|
|
|
97
97
|
}
|
|
98
98
|
fixtures.push(...extractFixtures(content));
|
|
99
99
|
}
|
|
100
|
-
const
|
|
100
|
+
const hasPytestHtml = /--html\b/.test(input.pytestHelp);
|
|
101
|
+
const htmlRenderer = hasPytestHtml
|
|
101
102
|
? "pytest-html --self-contained-html"
|
|
102
|
-
: "
|
|
103
|
+
: "missing pytest-html (required: pip install pytest-html)";
|
|
103
104
|
const warnings = [
|
|
104
105
|
...(pytestConfigs.length === 0
|
|
105
106
|
? ["No explicit pytest configuration file was found."]
|
|
106
107
|
: []),
|
|
108
|
+
...(!hasPytestHtml
|
|
109
|
+
? [
|
|
110
|
+
"pytest-html is not installed; the backend-test pipeline requires it to render the self-contained HTML report. Install with: pip install pytest-html",
|
|
111
|
+
]
|
|
112
|
+
: []),
|
|
107
113
|
...(conftestFiles.length === 0
|
|
108
114
|
? ["No bounded conftest.py candidate was found."]
|
|
109
115
|
: []),
|
|
@@ -843,7 +849,7 @@ export function renderBackendTestFacts(input) {
|
|
|
843
849
|
`| ${input.pytestExitCode === 0 ? "通过" : "未通过"} | ${input.parsed.tests} | ${input.parsed.passed} | ${input.parsed.failed} | ${input.parsed.errors} | ${input.parsed.skipped} | ${(passRate * 100).toFixed(2)}% | ${formatDuration(input.parsed.durationMs)} |`,
|
|
844
850
|
"",
|
|
845
851
|
`- Pytest exit code: ${input.pytestExitCode}`,
|
|
846
|
-
"-
|
|
852
|
+
"- Pytest-html report: valid",
|
|
847
853
|
"- HTML valid: yes",
|
|
848
854
|
"",
|
|
849
855
|
"## 质量校验",
|
|
@@ -872,8 +878,6 @@ export function renderBackendTestFacts(input) {
|
|
|
872
878
|
"",
|
|
873
879
|
"## 证据与完整校验原文",
|
|
874
880
|
"",
|
|
875
|
-
`- JUnit: ${input.junitRelativePath}`,
|
|
876
|
-
`- JUnit SHA-256: ${createHash("sha256").update(input.junitContent).digest("hex")}`,
|
|
877
881
|
`- HTML: ${input.htmlRelativePath}`,
|
|
878
882
|
`- HTML SHA-256: ${createHash("sha256").update(input.htmlContent).digest("hex")}`,
|
|
879
883
|
"",
|
|
@@ -354,6 +354,235 @@ export function parseJunitXml(xml) {
|
|
|
354
354
|
failures: failures.slice(0, MAX_FAILURES),
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
|
+
const HTML_ENTITY_MAP = {
|
|
358
|
+
"&": "&",
|
|
359
|
+
"<": "<",
|
|
360
|
+
">": ">",
|
|
361
|
+
""": '"',
|
|
362
|
+
""": '"',
|
|
363
|
+
"'": "'",
|
|
364
|
+
"'": "'",
|
|
365
|
+
};
|
|
366
|
+
function decodeHtmlEntities(value) {
|
|
367
|
+
return value.replace(/&(?:amp|lt|gt|quot|#34|#39|apos);/g, (entity) => HTML_ENTITY_MAP[entity] ?? entity);
|
|
368
|
+
}
|
|
369
|
+
function parseDurationLabelMs(raw) {
|
|
370
|
+
if (!raw)
|
|
371
|
+
return undefined;
|
|
372
|
+
const value = Number.parseFloat(raw);
|
|
373
|
+
if (!Number.isFinite(value))
|
|
374
|
+
return undefined;
|
|
375
|
+
if (/\bms\b/i.test(raw))
|
|
376
|
+
return Math.round(value);
|
|
377
|
+
if (/\bsec|s\b/i.test(raw))
|
|
378
|
+
return Math.round(value * 1000);
|
|
379
|
+
// pytest-html commonly emits bare milliseconds without a unit suffix.
|
|
380
|
+
return Math.round(value);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Extract the pytest-html 4.x JSON island from a self-contained HTML report.
|
|
384
|
+
*
|
|
385
|
+
* pytest-html 4.x embeds the full report payload as an HTML-entity-escaped
|
|
386
|
+
* JSON string in `<div id="data-container" data-jsonblob="...">`. This helper
|
|
387
|
+
* locates that attribute, decodes entities and parses the JSON. Fail-closed on
|
|
388
|
+
* a non-pytest-html document so the pipeline never silently drops test facts.
|
|
389
|
+
*/
|
|
390
|
+
export function extractPytestHtmlReportData(html) {
|
|
391
|
+
const match = html.match(/id=["']data-container["'][^>]*data-jsonblob=["']([^"']*)["']/i);
|
|
392
|
+
if (!match || !match[1]) {
|
|
393
|
+
throw new Error("invalid pytest-html report: missing data-container data-jsonblob island");
|
|
394
|
+
}
|
|
395
|
+
const decoded = decodeHtmlEntities(match[1]);
|
|
396
|
+
try {
|
|
397
|
+
return JSON.parse(decoded);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
throw new Error(`invalid pytest-html report: data-jsonblob is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Parse a self-contained pytest-html 4.x report into the same ParsedJunit
|
|
405
|
+
* shape used by the HTML/facts renderers, so the downstream rendering path
|
|
406
|
+
* stays uniform regardless of the evidence source.
|
|
407
|
+
*
|
|
408
|
+
* Unlike JUnit, pytest-html stores per-test captured stdout in a `log` field
|
|
409
|
+
* (with section markers like "Captured stdout call"). We surface that as the
|
|
410
|
+
* case `stdout` so HTTP_REQUEST/HTTP_RESPONSE lines remain visible in the
|
|
411
|
+
* per-case cards.
|
|
412
|
+
*/
|
|
413
|
+
export function parsePytestHtmlReport(html) {
|
|
414
|
+
const data = extractPytestHtmlReportData(html);
|
|
415
|
+
const entries = Object.entries(data.tests ?? {});
|
|
416
|
+
if (entries.length === 0) {
|
|
417
|
+
throw new Error("invalid pytest-html report: no test entries in data-jsonblob");
|
|
418
|
+
}
|
|
419
|
+
const cases = [];
|
|
420
|
+
const failures = [];
|
|
421
|
+
let passed = 0;
|
|
422
|
+
let failed = 0;
|
|
423
|
+
let errors = 0;
|
|
424
|
+
let skipped = 0;
|
|
425
|
+
for (const [nodeId, records] of entries) {
|
|
426
|
+
const record = records[0];
|
|
427
|
+
if (!record)
|
|
428
|
+
continue;
|
|
429
|
+
const rawResult = (record.result ?? "").toLowerCase();
|
|
430
|
+
const testId = record.testId ?? nodeId;
|
|
431
|
+
const { classname, name } = splitPytestNodeId(testId);
|
|
432
|
+
const durationMs = parseDurationLabelMs(record.duration);
|
|
433
|
+
const capturedLog = record.log ?? "";
|
|
434
|
+
// pytest-html collapses captured stdout/stderr into a single `log` field
|
|
435
|
+
// annotated with section markers. Preserve the whole log as stdout so the
|
|
436
|
+
// HTTP_REQUEST/HTTP_RESPONSE lines stay reachable for the per-case card.
|
|
437
|
+
const stdout = splitPytestHtmlLogSections(capturedLog).stdout || undefined;
|
|
438
|
+
const stderr = splitPytestHtmlLogSections(capturedLog).stderr || undefined;
|
|
439
|
+
if (rawResult === "passed") {
|
|
440
|
+
passed += 1;
|
|
441
|
+
cases.push({
|
|
442
|
+
classname,
|
|
443
|
+
name,
|
|
444
|
+
durationMs,
|
|
445
|
+
status: "passed",
|
|
446
|
+
...(stdout ? { stdout } : {}),
|
|
447
|
+
...(stderr ? { stderr } : {}),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else if (rawResult === "failed") {
|
|
451
|
+
failed += 1;
|
|
452
|
+
const message = extractPytestHtmlFailureMessage(capturedLog) || "failure";
|
|
453
|
+
const summary = truncate(message);
|
|
454
|
+
failures.push({ classname, name, message: summary, kind: "failure" });
|
|
455
|
+
cases.push({
|
|
456
|
+
classname,
|
|
457
|
+
name,
|
|
458
|
+
durationMs,
|
|
459
|
+
status: "failure",
|
|
460
|
+
message: summary,
|
|
461
|
+
details: capturedLog || summary,
|
|
462
|
+
...(stdout ? { stdout } : {}),
|
|
463
|
+
...(stderr ? { stderr } : {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
else if (rawResult === "error") {
|
|
467
|
+
errors += 1;
|
|
468
|
+
const message = extractPytestHtmlFailureMessage(capturedLog) || "error";
|
|
469
|
+
const summary = truncate(message);
|
|
470
|
+
failures.push({ classname, name, message: summary, kind: "error" });
|
|
471
|
+
cases.push({
|
|
472
|
+
classname,
|
|
473
|
+
name,
|
|
474
|
+
durationMs,
|
|
475
|
+
status: "error",
|
|
476
|
+
message: summary,
|
|
477
|
+
details: capturedLog || summary,
|
|
478
|
+
...(stdout ? { stdout } : {}),
|
|
479
|
+
...(stderr ? { stderr } : {}),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
else if (rawResult === "skipped" || rawResult === "xfailed") {
|
|
483
|
+
skipped += 1;
|
|
484
|
+
cases.push({
|
|
485
|
+
classname,
|
|
486
|
+
name,
|
|
487
|
+
durationMs,
|
|
488
|
+
status: "skipped",
|
|
489
|
+
...(stdout ? { stdout } : {}),
|
|
490
|
+
...(stderr ? { stderr } : {}),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Unknown outcome label: treat as error to stay fail-safe.
|
|
495
|
+
errors += 1;
|
|
496
|
+
const message = `unexpected pytest-html result label: ${record.result ?? "(empty)"}`;
|
|
497
|
+
const summary = truncate(message);
|
|
498
|
+
failures.push({ classname, name, message: summary, kind: "error" });
|
|
499
|
+
cases.push({
|
|
500
|
+
classname,
|
|
501
|
+
name,
|
|
502
|
+
durationMs,
|
|
503
|
+
status: "error",
|
|
504
|
+
message: summary,
|
|
505
|
+
details: capturedLog || summary,
|
|
506
|
+
...(stdout ? { stdout } : {}),
|
|
507
|
+
...(stderr ? { stderr } : {}),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const tests = cases.length;
|
|
512
|
+
return {
|
|
513
|
+
tests,
|
|
514
|
+
passed,
|
|
515
|
+
failed,
|
|
516
|
+
errors,
|
|
517
|
+
skipped,
|
|
518
|
+
durationMs: undefined,
|
|
519
|
+
cases,
|
|
520
|
+
failures: failures.slice(0, MAX_FAILURES),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function splitPytestNodeId(testId) {
|
|
524
|
+
// pytest-html uses node ids like "path/to/test_x.py::TestClass::test_name".
|
|
525
|
+
const sepIndex = testId.lastIndexOf("::");
|
|
526
|
+
if (sepIndex < 0) {
|
|
527
|
+
return { classname: "unknown", name: testId };
|
|
528
|
+
}
|
|
529
|
+
const prefix = testId.slice(0, sepIndex);
|
|
530
|
+
const name = testId.slice(sepIndex + 2);
|
|
531
|
+
const lastParen = name.indexOf("(");
|
|
532
|
+
const cleanName = lastParen >= 0 ? name.slice(0, lastParen) : name;
|
|
533
|
+
const moduleSep = prefix.lastIndexOf("::");
|
|
534
|
+
const filePart = moduleSep >= 0 ? prefix.slice(moduleSep + 2) : prefix;
|
|
535
|
+
const moduleWithoutExt = filePart.replace(/\.py$/i, "");
|
|
536
|
+
return { classname: moduleWithoutExt.replace(/\//g, ".") || "unknown", name: cleanName };
|
|
537
|
+
}
|
|
538
|
+
function splitPytestHtmlLogSections(log) {
|
|
539
|
+
if (!log)
|
|
540
|
+
return { stdout: "", stderr: "" };
|
|
541
|
+
// pytest-html interleaves captured stdout/stderr with section markers like
|
|
542
|
+
// "----------------------------- Captured stdout call -----------------------------".
|
|
543
|
+
// String.split includes capture-group matches in the result array, so the
|
|
544
|
+
// layout is [beforeText, label, body, label, body, ...]. Iterate label/body
|
|
545
|
+
// pairs starting at index 1.
|
|
546
|
+
const sections = log.split(/^-+ Captured (stdout|stderr|log|call|setup|teardown)(?: call| setup| teardown)? -+$/m);
|
|
547
|
+
let stdout = "";
|
|
548
|
+
let stderr = "";
|
|
549
|
+
for (let index = 1; index < sections.length - 1; index += 2) {
|
|
550
|
+
const label = sections[index]?.toLowerCase() ?? "";
|
|
551
|
+
const body = sections[index + 1] ?? "";
|
|
552
|
+
if (label.includes("stderr")) {
|
|
553
|
+
stderr = `${stderr}${body}`.trim();
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
stdout = `${stdout}${body}`.trim();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (!stdout && !stderr) {
|
|
560
|
+
// No recognizable section markers: treat the whole log as stdout so HTTP
|
|
561
|
+
// request/response lines remain visible.
|
|
562
|
+
stdout = log.trim();
|
|
563
|
+
}
|
|
564
|
+
return { stdout, stderr };
|
|
565
|
+
}
|
|
566
|
+
function extractPytestHtmlFailureMessage(log) {
|
|
567
|
+
if (!log)
|
|
568
|
+
return "";
|
|
569
|
+
// pytest-html failure logs contain assertion lines prefixed with "E " and a
|
|
570
|
+
// trailing location line like "test_x.py:N: AssertionError". Prefer the
|
|
571
|
+
// explicit AssertionError/Error line; fall back to the last "E " line.
|
|
572
|
+
const assertionLine = log
|
|
573
|
+
.split(/\r?\n/)
|
|
574
|
+
.map((line) => line.trim())
|
|
575
|
+
.find((line) => /:\s*AssertionError/.test(line));
|
|
576
|
+
if (assertionLine)
|
|
577
|
+
return assertionLine.replace(/^.*?:\s*/, "");
|
|
578
|
+
const eLines = log
|
|
579
|
+
.split(/\r?\n/)
|
|
580
|
+
.filter((line) => /^E\s+/.test(line))
|
|
581
|
+
.map((line) => line.replace(/^E\s+/, "").trim());
|
|
582
|
+
if (eLines.length > 0)
|
|
583
|
+
return eLines[eLines.length - 1];
|
|
584
|
+
return "";
|
|
585
|
+
}
|
|
357
586
|
export function deriveBackendTestResult(input) {
|
|
358
587
|
assertNoSecrets("commandSummary", input.commandSummary);
|
|
359
588
|
const emptyJunitMeta = {
|
|
@@ -87,14 +87,14 @@ function stripAnsi(value) {
|
|
|
87
87
|
function normalizeRepoPath(workspaceRoot, candidate) {
|
|
88
88
|
const clean = stripAnsi(candidate).trim().replace(/^file:\/\//, "");
|
|
89
89
|
const windowsAbsolute = /^[A-Za-z]:[\\/]/.test(clean);
|
|
90
|
-
const
|
|
90
|
+
const posixAbsolute = path.posix.isAbsolute(clean);
|
|
91
91
|
let relative;
|
|
92
|
-
if (windowsAbsolute
|
|
93
|
-
if (
|
|
92
|
+
if (windowsAbsolute) {
|
|
93
|
+
if (!/^[A-Za-z]:[\\/]/.test(workspaceRoot))
|
|
94
94
|
return null;
|
|
95
95
|
relative = path.win32.relative(workspaceRoot, clean);
|
|
96
96
|
}
|
|
97
|
-
else if (
|
|
97
|
+
else if (posixAbsolute) {
|
|
98
98
|
relative = path.posix.relative(workspaceRoot.replaceAll("\\", "/"), clean.replaceAll("\\", "/"));
|
|
99
99
|
}
|
|
100
100
|
else {
|
|
@@ -18,7 +18,8 @@ import { discoverProjectGovernancePresence } from "./project-governance-context.
|
|
|
18
18
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
19
19
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
20
20
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
21
|
-
import {
|
|
21
|
+
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
22
|
+
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
22
23
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
23
24
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
24
25
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
@@ -556,7 +557,7 @@ export function hasApiDependency(sources) {
|
|
|
556
557
|
/**
|
|
557
558
|
* Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
|
|
558
559
|
*
|
|
559
|
-
* Decision matrix (from docs/design/frontend-mock-data-workflow.md):
|
|
560
|
+
* Decision matrix (from docs/design/active/frontend-mock-data-workflow.md):
|
|
560
561
|
*
|
|
561
562
|
* | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
|
|
562
563
|
* |---|---|---|---|
|
|
@@ -871,21 +872,29 @@ function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
|
|
|
871
872
|
function verifyCommandKey(command) {
|
|
872
873
|
return `${command.cwd}\0${command.args.join("\0")}`;
|
|
873
874
|
}
|
|
874
|
-
function resolveDagVerifyStrategy(taskConfig) {
|
|
875
|
+
function resolveDagVerifyStrategy(taskConfig, defaultIntermediateQuotaWhenFull = "full") {
|
|
876
|
+
const explicitIntermediateQuota = taskConfig.dagVerifyStrategy?.intermediateQuota;
|
|
875
877
|
return {
|
|
876
|
-
intermediateQuota:
|
|
878
|
+
intermediateQuota: explicitIntermediateQuota ??
|
|
879
|
+
(taskConfig.verifyQuota === "full"
|
|
880
|
+
? defaultIntermediateQuotaWhenFull
|
|
881
|
+
: taskConfig.verifyQuota),
|
|
877
882
|
finalQuota: "full",
|
|
878
883
|
focusedCommandSource: taskConfig.dagVerifyStrategy?.focusedCommandSource ?? "adapter",
|
|
879
884
|
};
|
|
880
885
|
}
|
|
881
886
|
function buildVerifyEvidence(input) {
|
|
887
|
+
const selectedCommands = input.commands && input.commands.length > 0 ? input.commands : undefined;
|
|
888
|
+
const commandCount = selectedCommands?.length ?? input.fallbackCommands.length;
|
|
882
889
|
return {
|
|
883
890
|
phase: input.phase,
|
|
884
891
|
quota: input.quota,
|
|
885
892
|
commandSource: input.commandSource,
|
|
886
|
-
commandCount
|
|
887
|
-
commandLabels:
|
|
893
|
+
commandCount,
|
|
894
|
+
commandLabels: selectedCommands?.map((command) => command.label) ?? input.fallbackCommands,
|
|
888
895
|
commandTexts: input.commandTexts ?? [],
|
|
896
|
+
commandTimeoutMs: input.commandTimeoutMs,
|
|
897
|
+
totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
|
|
889
898
|
finalFullRequired: input.finalFullRequired,
|
|
890
899
|
};
|
|
891
900
|
}
|
|
@@ -1369,9 +1378,10 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1369
1378
|
commands: finalVerifyCommands,
|
|
1370
1379
|
fallbackCommands: [],
|
|
1371
1380
|
finalFullRequired: true,
|
|
1381
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1372
1382
|
}),
|
|
1373
1383
|
cwd: ".",
|
|
1374
|
-
timeoutMs:
|
|
1384
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1375
1385
|
},
|
|
1376
1386
|
},
|
|
1377
1387
|
]
|
|
@@ -1677,9 +1687,10 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
1677
1687
|
commands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
1678
1688
|
fallbackCommands: [],
|
|
1679
1689
|
commandTexts: commands,
|
|
1690
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1680
1691
|
}),
|
|
1681
1692
|
cwd: ".",
|
|
1682
|
-
timeoutMs:
|
|
1693
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1683
1694
|
},
|
|
1684
1695
|
};
|
|
1685
1696
|
}
|
|
@@ -2017,6 +2028,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2017
2028
|
commands: partitionedStaticVerifyCommands.static.commands,
|
|
2018
2029
|
fallbackCommands: staticFallbackCommands,
|
|
2019
2030
|
commandTexts: staticShellCommands,
|
|
2031
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2020
2032
|
});
|
|
2021
2033
|
const lintVerifyEvidence = lintShellCommands.length > 0
|
|
2022
2034
|
? buildVerifyEvidence({
|
|
@@ -2026,6 +2038,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2026
2038
|
commands: partitionedStaticVerifyCommands.lint.commands,
|
|
2027
2039
|
fallbackCommands: [],
|
|
2028
2040
|
commandTexts: lintShellCommands,
|
|
2041
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2029
2042
|
})
|
|
2030
2043
|
: undefined;
|
|
2031
2044
|
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
@@ -2036,6 +2049,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2036
2049
|
fallbackCommands: behaviorFallbackCommands,
|
|
2037
2050
|
commandTexts: behaviorShellCommands,
|
|
2038
2051
|
finalFullRequired: true,
|
|
2052
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2039
2053
|
});
|
|
2040
2054
|
const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
|
|
2041
2055
|
? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
|
|
@@ -2273,7 +2287,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2273
2287
|
lintEvidence: lintVerifyEvidence,
|
|
2274
2288
|
},
|
|
2275
2289
|
cwd: ".",
|
|
2276
|
-
timeoutMs:
|
|
2290
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2277
2291
|
},
|
|
2278
2292
|
},
|
|
2279
2293
|
]
|
|
@@ -2345,7 +2359,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2345
2359
|
mode: "initial",
|
|
2346
2360
|
},
|
|
2347
2361
|
cwd: ".",
|
|
2348
|
-
timeoutMs:
|
|
2362
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2349
2363
|
},
|
|
2350
2364
|
},
|
|
2351
2365
|
{
|
|
@@ -2406,7 +2420,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2406
2420
|
mode: "repair",
|
|
2407
2421
|
},
|
|
2408
2422
|
cwd: ".",
|
|
2409
|
-
timeoutMs:
|
|
2423
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2410
2424
|
},
|
|
2411
2425
|
},
|
|
2412
2426
|
{
|
|
@@ -3122,9 +3136,10 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
|
|
|
3122
3136
|
commandSource: "inline",
|
|
3123
3137
|
fallbackCommands: [pytestCommand],
|
|
3124
3138
|
finalFullRequired: true,
|
|
3139
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3125
3140
|
}),
|
|
3126
3141
|
cwd: ".",
|
|
3127
|
-
timeoutMs:
|
|
3142
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3128
3143
|
},
|
|
3129
3144
|
};
|
|
3130
3145
|
}
|
|
@@ -3241,6 +3256,7 @@ function buildBackendTestOutcomeGateNode(sources) {
|
|
|
3241
3256
|
commandSource: "inline",
|
|
3242
3257
|
fallbackCommands: [gateCommand],
|
|
3243
3258
|
finalFullRequired: true,
|
|
3259
|
+
commandTimeoutMs: 60_000,
|
|
3244
3260
|
}),
|
|
3245
3261
|
cwd: ".",
|
|
3246
3262
|
timeoutMs: 60000,
|
|
@@ -3333,10 +3349,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3333
3349
|
executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive",
|
|
3334
3350
|
writeSet: ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"],
|
|
3335
3351
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])), forbiddenPaths: forbidden,
|
|
3336
|
-
outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring;
|
|
3352
|
+
outputContract: "Convert every final automatable Markdown case into pytest assets whose actual test function region contains the exact Case ID, preferably in the function name or docstring. Each testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
|
|
3337
3353
|
subtask_prompt: [
|
|
3338
3354
|
"Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3339
3355
|
"Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from 前置条件 plus any optional 测试数据/自动化映射 or their legacy English aliases.",
|
|
3356
|
+
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3340
3357
|
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
3341
3358
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
3342
3359
|
"Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
|
|
@@ -3981,6 +3998,7 @@ function buildMultiPerspectiveReviewNodes(input) {
|
|
|
3981
3998
|
commandSource: "inline",
|
|
3982
3999
|
fallbackCommands: [aggregateScript],
|
|
3983
4000
|
finalFullRequired: true,
|
|
4001
|
+
commandTimeoutMs: 60_000,
|
|
3984
4002
|
}),
|
|
3985
4003
|
cwd: ".",
|
|
3986
4004
|
timeoutMs: 60000,
|
|
@@ -4165,6 +4183,7 @@ function buildKnowledgeSyncValidateNode(sources, featureId) {
|
|
|
4165
4183
|
commandSource: "inline",
|
|
4166
4184
|
fallbackCommands: [validateScript],
|
|
4167
4185
|
finalFullRequired: true,
|
|
4186
|
+
commandTimeoutMs: 120_000,
|
|
4168
4187
|
}),
|
|
4169
4188
|
cwd: ".",
|
|
4170
4189
|
timeoutMs: 120000,
|
|
@@ -4385,6 +4404,7 @@ function buildKgBootstrapPreflightNode(sources) {
|
|
|
4385
4404
|
quota: "full",
|
|
4386
4405
|
commandSource: "inline",
|
|
4387
4406
|
fallbackCommands: [script],
|
|
4407
|
+
commandTimeoutMs: 60_000,
|
|
4388
4408
|
}),
|
|
4389
4409
|
cwd: ".",
|
|
4390
4410
|
timeoutMs: 60000,
|
|
@@ -4432,6 +4452,7 @@ function buildKgBootstrapInventoryNode(sources) {
|
|
|
4432
4452
|
quota: "full",
|
|
4433
4453
|
commandSource: "inline",
|
|
4434
4454
|
fallbackCommands: [script],
|
|
4455
|
+
commandTimeoutMs: 120_000,
|
|
4435
4456
|
}),
|
|
4436
4457
|
cwd: ".",
|
|
4437
4458
|
timeoutMs: 120000,
|
|
@@ -4526,6 +4547,7 @@ function buildKgBootstrapValidateNode(sources) {
|
|
|
4526
4547
|
commandSource: "inline",
|
|
4527
4548
|
fallbackCommands: [script],
|
|
4528
4549
|
finalFullRequired: true,
|
|
4550
|
+
commandTimeoutMs: 120_000,
|
|
4529
4551
|
}),
|
|
4530
4552
|
cwd: ".",
|
|
4531
4553
|
timeoutMs: 120000,
|
|
@@ -4653,6 +4675,7 @@ function buildKgBootstrapPromoteNode(sources) {
|
|
|
4653
4675
|
commandSource: "inline",
|
|
4654
4676
|
fallbackCommands: [script],
|
|
4655
4677
|
finalFullRequired: true,
|
|
4678
|
+
commandTimeoutMs: 120_000,
|
|
4656
4679
|
}),
|
|
4657
4680
|
cwd: ".",
|
|
4658
4681
|
timeoutMs: 120000,
|
|
@@ -4718,6 +4741,7 @@ function buildKgBootstrapMaterializeNode(sources) {
|
|
|
4718
4741
|
commandSource: "inline",
|
|
4719
4742
|
fallbackCommands: [script],
|
|
4720
4743
|
finalFullRequired: true,
|
|
4744
|
+
commandTimeoutMs: 120_000,
|
|
4721
4745
|
}),
|
|
4722
4746
|
cwd: ".",
|
|
4723
4747
|
timeoutMs: 120000,
|
|
@@ -4774,7 +4798,22 @@ export async function requireManagedTaskContractBinding(input) {
|
|
|
4774
4798
|
taskId: input.taskId,
|
|
4775
4799
|
});
|
|
4776
4800
|
if (state.effectiveStatus !== "managed" || !state.ref) {
|
|
4777
|
-
const
|
|
4801
|
+
const expectedRevision = state.ref?.revision ?? 0;
|
|
4802
|
+
const observedCanonicalHash = state.observedCanonicalHash ?? "<observedCanonicalHash-from-show>";
|
|
4803
|
+
const recovery = state.effectiveStatus === "transaction-incomplete"
|
|
4804
|
+
? `Recover the unfinished transaction first: loop-agent task contract recover --task ${input.taskId} --json`
|
|
4805
|
+
: [
|
|
4806
|
+
"After reviewing the show JSON, either adopt the current on-disk facts:",
|
|
4807
|
+
`loop-agent task contract adopt --task ${input.taskId} --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-this-adopt-request> --json`,
|
|
4808
|
+
"or apply a reviewed draft:",
|
|
4809
|
+
`loop-agent task contract apply --task ${input.taskId} --input <reviewed-draft.json> --expected-revision ${expectedRevision} --expected-observed-hash ${observedCanonicalHash} --request-id <unique-request-id> --request-payload-sha256 <sha256-of-input-file> --json`,
|
|
4810
|
+
].join(" ");
|
|
4811
|
+
const err = new Error([
|
|
4812
|
+
`dag generate requires managed Task Contract for task ${input.taskId} (effectiveStatus=${state.effectiveStatus})`,
|
|
4813
|
+
`Inspect current state: loop-agent task contract show ${input.taskId} --json`,
|
|
4814
|
+
recovery,
|
|
4815
|
+
"Do not auto-adopt or auto-apply before confirming the Task Contract facts and concurrency fields.",
|
|
4816
|
+
].join("; "));
|
|
4778
4817
|
err.code =
|
|
4779
4818
|
state.effectiveStatus === "externally-modified"
|
|
4780
4819
|
? "EXTERNALLY_MODIFIED"
|
|
@@ -4828,10 +4867,6 @@ function synthesizeInMemoryTaskContractBinding(sources) {
|
|
|
4828
4867
|
};
|
|
4829
4868
|
}
|
|
4830
4869
|
async function resolveTaskContractBindingForGenerate(sources) {
|
|
4831
|
-
// Real CLI generation freezes the managed ref. Direct builder unit tests
|
|
4832
|
-
// may provide repoRoot-shaped fixture paths without a persisted task; keep
|
|
4833
|
-
// their deterministic synthetic binding path so topology/prompt tests remain
|
|
4834
|
-
// isolated from contract storage.
|
|
4835
4870
|
if (sources.repoRoot) {
|
|
4836
4871
|
const state = await observeTaskContract({
|
|
4837
4872
|
repoRoot: sources.repoRoot,
|
|
@@ -4986,7 +5021,12 @@ function buildReviewVerdictRecoveryNode(sources) {
|
|
|
4986
5021
|
return {
|
|
4987
5022
|
id: "review-verdict-recovery-pi",
|
|
4988
5023
|
depends_on: ["review-pi"],
|
|
5024
|
+
// AC3: recovery is a read-only failure-aware node. It runs after review-pi
|
|
5025
|
+
// settles, consuming either normal FINISHED output or an explicitly tolerated
|
|
5026
|
+
// ERROR (for example protocol-invalid after retries). It must not bypass the
|
|
5027
|
+
// writer, write guard, or final shell gate, which remain fail-closed.
|
|
4989
5028
|
role: "reviewer",
|
|
5029
|
+
failureAwareDependsOn: ["review-pi"],
|
|
4990
5030
|
executor: "pi",
|
|
4991
5031
|
complexity: "LOW",
|
|
4992
5032
|
writePolicy: "read-only",
|
|
@@ -5271,7 +5311,7 @@ function buildWriteSetGateNode(sources) {
|
|
|
5271
5311
|
}
|
|
5272
5312
|
function buildSoftVerifyNode(sources) {
|
|
5273
5313
|
const implementId = implementationNodeId();
|
|
5274
|
-
const strategy = resolveDagVerifyStrategy(sources.taskConfig);
|
|
5314
|
+
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
5275
5315
|
const fallbackCommands = ["npm run typecheck"];
|
|
5276
5316
|
const commands = buildVerifyShellCommands({
|
|
5277
5317
|
repoRoot: sources.repoRoot,
|
|
@@ -5297,9 +5337,10 @@ function buildSoftVerifyNode(sources) {
|
|
|
5297
5337
|
commandSource: sources.verifyCommands ? "adapter" : "inline",
|
|
5298
5338
|
commands: sources.verifyCommands?.intermediate,
|
|
5299
5339
|
fallbackCommands,
|
|
5340
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5300
5341
|
}),
|
|
5301
5342
|
cwd: ".",
|
|
5302
|
-
timeoutMs:
|
|
5343
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5303
5344
|
},
|
|
5304
5345
|
};
|
|
5305
5346
|
}
|
|
@@ -5309,6 +5350,7 @@ function buildProcessSupervisorNode(sources) {
|
|
|
5309
5350
|
return {
|
|
5310
5351
|
id: "process-supervisor-pi",
|
|
5311
5352
|
depends_on: ["soft-verify-shell", implementId],
|
|
5353
|
+
failureAwareDependsOn: ["soft-verify-shell"],
|
|
5312
5354
|
role: "supervisor",
|
|
5313
5355
|
executor: "pi",
|
|
5314
5356
|
complexity: "HIGH",
|
|
@@ -5415,9 +5457,10 @@ function buildHardVerifyNode(sources) {
|
|
|
5415
5457
|
commands: sources.verifyCommands?.final,
|
|
5416
5458
|
fallbackCommands,
|
|
5417
5459
|
finalFullRequired: true,
|
|
5460
|
+
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5418
5461
|
}),
|
|
5419
5462
|
cwd: ".",
|
|
5420
|
-
timeoutMs:
|
|
5463
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
5421
5464
|
},
|
|
5422
5465
|
};
|
|
5423
5466
|
}
|
|
@@ -5466,7 +5509,7 @@ function buildSupervisedHybridDag(standard, sources) {
|
|
|
5466
5509
|
objective: `${standard.objective ?? ""}\n\nRoute: supervised implementation DAG selected by workflowPolicy/governanceProfile or explicit CLI profile.`.trim(),
|
|
5467
5510
|
globalConstraints: supervisedConstraints,
|
|
5468
5511
|
convergence: sources.taskConfig.convergence,
|
|
5469
|
-
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
5512
|
+
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig, "1"),
|
|
5470
5513
|
tasks: [
|
|
5471
5514
|
cloneTask(contract),
|
|
5472
5515
|
cloneTask(scoutSrc),
|
|
@@ -5536,6 +5579,12 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
5536
5579
|
? await prepareFrontendMockSources(sources)
|
|
5537
5580
|
: sources;
|
|
5538
5581
|
const spec = await buildHybridDagForTemplate(preparedSources, template);
|
|
5582
|
+
if (preparedSources.repoRoot && dagHasWriterExecution(spec)) {
|
|
5583
|
+
await requireManagedTaskContractBinding({
|
|
5584
|
+
repoRoot: preparedSources.repoRoot,
|
|
5585
|
+
taskId: preparedSources.taskId,
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5539
5588
|
await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
|
|
5540
5589
|
return {
|
|
5541
5590
|
taskId: sources.taskId,
|