@tea-agent/loop-agent 0.23.1 → 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/CHANGELOG.md +18 -1
- package/README.md +1 -1
- package/bin/agent-worker.js +0 -0
- package/dist/executors/shell-executor.js +20 -7
- package/dist/shared/operator/capabilities.js +475 -2
- package/dist/worker/console/app-data.js +2 -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/interview-adapter.js +136 -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 +581 -43
- 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 +18 -17
- package/dist/worker/console/chat/routes.js +1354 -65
- 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 +210 -11
- package/dist/worker/console/chat/shortcuts.js +15 -0
- package/dist/worker/console/chat/tool-adapter.js +81 -194
- package/dist/worker/console/chat/tools.js +72 -48
- 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 +14 -2
- 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/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 +2 -1
- package/docs/README.md +1 -1
- package/docs/architecture/README.md +5 -5
- package/docs/architecture/evolution.md +4 -4
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/templates/backend-test-dag.json +2 -2
- package/harness.json +1 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +0 -25
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +0 -1
|
@@ -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>
|
|
@@ -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 {
|
|
@@ -3349,10 +3349,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3349
3349
|
executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive",
|
|
3350
3350
|
writeSet: ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"],
|
|
3351
3351
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])), forbiddenPaths: forbidden,
|
|
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;
|
|
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.",
|
|
3353
3353
|
subtask_prompt: [
|
|
3354
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.",
|
|
3355
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.",
|
|
3356
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.",
|
|
3357
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.",
|
|
3358
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.",
|
package/docs/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`docs/` 是 loop-agent 源仓的治理文档根目录。新读者不需要先浏览所有文件,先按任务选择入口,再由目录 README 或 `docs/document-catalog.json` 判断文档身份。
|
|
4
4
|
|
|
5
|
-
**维护日:2026-07-
|
|
5
|
+
**维护日:2026-07-27** — 对齐 `@tea-agent/loop-agent@0.24.0`。版本事实以根 `CHANGELOG.md` 与 `package.json` 为准;能力短摘要见 `reports/current-capability-summary.md`。
|
|
6
6
|
|
|
7
7
|
## 我现在要做什么?
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本目录是 loop-agent 维护者架构文档入口。每篇文档回答一个具体问题,不重复 `runtime-boundaries.md` 的依赖方向表与 governance-hook 表;遇到契约级事实请回到该文件。
|
|
4
4
|
|
|
5
|
-
**维护日:2026-07-
|
|
5
|
+
**维护日:2026-07-27** — 索引与演进叙述对齐仓库头 `@tea-agent/loop-agent@0.24.0`。边界表仍以 `runtime-boundaries.md` 与 `scripts/check-*.sh` 为准。
|
|
6
6
|
|
|
7
7
|
## 阅读路径
|
|
8
8
|
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
1. `runtime-boundaries.md` — runtime 层边界、依赖方向与治理 hook 的机器校验契约(**先读,是其他文档的边界真源**)。
|
|
12
12
|
2. `agents-invariants.md` — `AGENTS.md`(source)与 init managed block 的「什么不能删」不变量契约(短语真源 + 行数 guardrail 语义)。
|
|
13
13
|
3. `system-overview.md` — loop-agent / agent-worker / 治理层 / 外部系统的全景关系。
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
4. `dag-execution.md` — Agent DAG 主调用链、rank 调度、executor、skill snapshot、生命周期。
|
|
15
|
+
5. `worker-and-feature.md` — agent-worker 子进程边界、controller identity、Task Pool、Feature、Observe 与 Console。
|
|
16
|
+
6. `facts-and-state.md` — `.harness/` 各根目录、canonical facts、derived read models 与不可变规则。
|
|
17
|
+
7. `evolution.md` — 当前已实现能力 vs 第 3–6 月未来方向(明确标注规划/未实现)。
|
|
18
18
|
|
|
19
19
|
## 事实与规划的区分
|
|
20
20
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
本页区分 loop-agent **当前已实现**的架构能力与**未来规划**。当前事实以代码、发布 CLI、已完成计划为准;未来能力一律标「规划 / 未实现 / 前瞻」。权威源:`CHANGELOG.md`、`docs/reports/current-capability-summary.md`、ADR 0001–0005、`docs/exec-plans/completed/` 与 `docs/exec-plans/active/`。
|
|
4
4
|
|
|
5
|
-
**对照版本:`@tea-agent/loop-agent@0.
|
|
5
|
+
**对照版本:`@tea-agent/loop-agent@0.24.0`(2026-07-27)**。细节版本条目见根 `CHANGELOG.md`;本表只保留架构层可读摘要。
|
|
6
6
|
|
|
7
|
-
## 当前已实现(累计至 0.
|
|
7
|
+
## 当前已实现(累计至 0.24.0)
|
|
8
8
|
|
|
9
9
|
| 域 | 现状 | 权威入口 |
|
|
10
10
|
| --- | --- | --- |
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| Frontend-test | 默认 short-chain RAG;内容/evidence 质量进入 advisory findings,路径与安全边界仍 fail-closed | `CHANGELOG.md [0.21.0]` / `[0.22.0]` |
|
|
22
22
|
| Frontend-implementation | Contract / trace / repair / Mock assess;`ai_workspace` + 知识库 + `openspec` 规范发现;lint 基线债务隔离 | `CHANGELOG.md [0.22.0]` |
|
|
23
23
|
| Inspect(原 Observe) | 统一 Console 的 `/inspect/` 只读运营面与富时间线;兼容期仍保留独立 `observe serve` | `website/docs/guides/observe-ui.md`、`CHANGELOG.md [Unreleased]` |
|
|
24
|
-
| Local Operator Console | `agent-worker console serve\|doctor`(loopback);Operate + Inspect、Happy Path / Interview / split view / recovery CTA
|
|
24
|
+
| Local Operator Console | `agent-worker console serve\|doctor`(loopback);Operate + Inspect、Happy Path / Interview / split view / recovery CTA;**0.23.x 起含 General Operator Chat**(Pi SDK services session、operator-only tool surface);**非**远端多用户 Console | unify design;ADR 0005;completed `2026-07-25-console-phase-4-general-operator-chat.md` |
|
|
25
25
|
| Task Contract / operator surface | journaled `task contract *`;machine envelope;**DagSpec v4 `taskContractBinding`**(exclusive writer 强制) | `CHANGELOG.md [0.17.0]`–`[0.17.2]`;ADR 0005 |
|
|
26
26
|
| DagSpec / repair | v3 `runtimeContract` + 显式 `repairNodeId`;新 writer 生成默认 v4 binding | `dag-execution.md`、`CHANGELOG.md [0.11.0]` / `[0.17.0]` |
|
|
27
27
|
| 自适应 liveness | runner lease、Provider/tool/output 活动与 meaningful progress 分离;静默 stall 受控终止,退出未确认时禁止自动重试 | `dag-execution.md`、active `2026-07-23-dag-adaptive-liveness.md` |
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
| fullstack Delivery / Final Verification live 闭环 | 进行中(active train) | `docs/exec-plans/active/2026-07-19-fullstack-dogfood-016x-release-train.md` |
|
|
53
53
|
| Dynamic Workflow runtime limits 强执法、更广 profile | 设计输入 | `docs/design/active/dynamic-workflow-dag-engine-roadmap.md` |
|
|
54
54
|
| Loop 与 Dynamic Workflow 更深双向集成 / 自动恢复 | 设计输入 | 同上;当前已有基础 `workflow` action |
|
|
55
|
-
|
|
|
55
|
+
| Operator Chat 作为主操作会话(裸 bash 收敛、真 human gate、durable session/turn、Operation 闭环、完整 CLI action coverage、默认入口切换) | 设计输入 / 未达主会话门槛 | `docs/design/active/operator-chat-primary-session-roadmap.md`;Phase 4 General Operator Chat 本体已实现归档(`docs/exec-plans/completed/2026-07-25-console-phase-4-general-operator-chat.md`) |
|
|
56
56
|
| Eval Lab auto-promote / RSI Level 1 产品声称 | **禁止**写成已交付 | Eval Lab design + active plan |
|
|
57
57
|
|
|
58
58
|
> 本地 Operator Console 统一面(实际首发 0.19.0)已发布:单仓库、loopback、随 `@tea-agent/loop-agent` 同包、canonical mutation 只经 sibling CLI,Inspect 复用 GET-only Observe read model。它不是远端 Web Console,也不能把多租户/云编排需求偷渡进本地 Console。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本页说明 `agent-worker` 如何通过冻结的已发布 `loop-agent` 子进程执行 DAG(不 in-process import runtime kernel),以及其上的产品线 read model:TaskSpec、Task Pool、Feature 与统一 Operator Console(Operate + Inspect)。边界契约权威是 `runtime-boundaries.md` §Worker adapter。
|
|
4
4
|
|
|
5
|
-
**维护校准:2026-07-
|
|
5
|
+
**维护校准:2026-07-27 / `@tea-agent/loop-agent@0.24.0`** — 统一 Operator Console(Operate + Inspect)、恢复操作面与自适应 liveness 已落地;细节以 `CHANGELOG.md` 与对应 active/completed plan 为准。
|
|
6
6
|
|
|
7
7
|
## 核心事实:子进程,非 in-process
|
|
8
8
|
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
".harness/dag-runs/**",
|
|
203
203
|
"artifacts/**"
|
|
204
204
|
],
|
|
205
|
-
"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;
|
|
206
|
-
"subtask_prompt": "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.\n\nEnsure 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.\n\nGenerate 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.\n\nCompare 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.\n\nBefore 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.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`."
|
|
205
|
+
"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.",
|
|
206
|
+
"subtask_prompt": "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.\n\nEnsure 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.\n\nName 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.\n\nGenerate 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.\n\nCompare 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.\n\nBefore 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.\n\nDo not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`."
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"id": "backend-test-traceability-gate-shell",
|
package/harness.json
CHANGED