@tea-agent/loop-agent 0.16.13 → 0.16.15-beta.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 CHANGED
@@ -18,42 +18,6 @@
18
18
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
19
19
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为 initial/final Result、repair eligibility、traceability 与 Observe 投影保留结构化运行证据。
20
20
 
21
- ## [0.16.13] - 2026-07-20
22
-
23
- ### 修复
24
-
25
- - backend-test 的 `finalize-effective-result` 管道在节点完成后绑定 `contracts/backend-test-result.json` 为 structured artifact,使 Outcome 投影出 `backend-test-result`,解除 FE-TEST Ready Planner 对 BE-TEST 产物门的误阻断。
26
-
27
- ## [0.16.12] - 2026-07-20
28
-
29
- ### 修复
30
-
31
- - `backend-test` materialize / Git checkpoint 自动并入 `testcase/**`(与 `docs/test-reports/**`)运行时写根,避免 TaskSpec 仍写 `tests/api/**` 时绿跑无法 promote、任务卡在 Running。
32
-
33
- ## [0.16.11] - 2026-07-20
34
-
35
- ### 修复
36
-
37
- - Worker 报告决策不再把条件分支的 `SKIPPED` 节点当成失败;backend-test 等混合 DAG 在 revise/repair 未选中时,只要 ERROR 不存在且 run finished,即可 `report-completed` 并进入 Outcome/promote。
38
-
39
- ## [0.16.10] - 2026-07-20
40
-
41
- ### 修复
42
-
43
- - 后端测试执行合同物化会清理 `managedCommand` 中的空字符串(如 `stop: ""`),避免 near-schema 侦察输出在 strict parse 下误拦 in-process BE-TEST。
44
-
45
- ## [0.16.9] - 2026-07-20
46
-
47
- ### 修复
48
-
49
- - Outcome 结构化产物声明不再绑定节点 mint-time sha256;同一路径若被后续 gate(如 backend-test traceability)就地改写,投影时以磁盘重算 hash 为准,消除 BE-TEST 完成态仍被 `artifact sha256 mismatch` 误拦。
50
-
51
- ## [0.16.8] - 2026-07-20
52
-
53
- ### 修复
54
-
55
- - Outcome 投影对同一 run-owned 结构化产物路径的多次 rewrite(如 backend-test case manifest initial→final)只保留最后一次 hash 声明,避免 `artifact sha256 mismatch` 误拦已完成的 BE-TEST。
56
-
57
21
  ## [0.16.7] - 2026-07-20
58
22
 
59
23
  ### 修复
@@ -186,6 +186,10 @@ function resolvePiCliPath() {
186
186
  path.join(nvmBase, nodeVersion, 'lib', 'node_modules'),
187
187
  ].flatMap((nodeModulesRoot) => PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(nodeModulesRoot, packageName)));
188
188
  const otherCandidates = [
189
+ // Prefer the project-local Pi dependency. `npm run` adds node_modules/.bin
190
+ // to PATH, but direct invocations from the DAG runner may not, so resolve
191
+ // the package from the current workspace explicitly.
192
+ ...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.resolve(process.cwd(), 'node_modules'), packageName)),
189
193
  // Global npm installation (non-nvm)
190
194
  ...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot('/usr/local/lib/node_modules', packageName)),
191
195
  // Bun installation
@@ -6,8 +6,6 @@ import path from "node:path";
6
6
  import { z } from "zod";
7
7
  import { getTaskPoolRoot } from "../pool/run-store.js";
8
8
  import { assertSafeRuntimeId } from "../follow-up/paths.js";
9
- import { expandAllowedPathsForWorkflow } from "../materialize/harness-task-materializer.js";
10
- import { resolveWorkflow } from "../task-spec/workflow-routing.js";
11
9
  const execFileAsync = promisify(execFile);
12
10
  const checkpointSchema = z.object({
13
11
  taskId: z.string().min(1),
@@ -277,14 +275,13 @@ function auditChangedPaths(changes, taskSpec) {
277
275
  throw new Error(audit.violations[0]);
278
276
  }
279
277
  function auditChangedPathsReport(changes, taskSpec) {
280
- const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(taskSpec).workflow, taskSpec.constraints.allowed_paths);
281
278
  const violations = [];
282
279
  for (const changed of changes) {
283
280
  if (/(^|\/)(\.env(?:\.|$)|[^/]*\.(?:pem|key|p12|pfx))$/i.test(changed))
284
281
  violations.push(`changed path may contain sensitive material: ${changed}`);
285
282
  else if (taskSpec.constraints.forbidden_paths.some((glob) => matchesGlob(changed, glob)))
286
283
  violations.push(`changed path is forbidden for ${taskSpec.id}: ${changed}`);
287
- else if (!allowedPaths.some((glob) => matchesGlob(changed, glob)))
284
+ else if (!taskSpec.constraints.allowed_paths.some((glob) => matchesGlob(changed, glob)))
288
285
  violations.push(`changed path is outside allowed_paths for ${taskSpec.id}: ${changed}`);
289
286
  }
290
287
  return { ok: violations.length === 0, violations };
@@ -34,17 +34,13 @@ export async function materializeTaskSpec(options) {
34
34
  }
35
35
  const paths = getTaskPaths(options.repoRoot, harnessTaskId);
36
36
  const baseConfig = await loadTaskConfig(options.repoRoot, harnessTaskId);
37
- // backend-test hybrid DAG writes under testcase/** (pytest + md cases + helpers).
38
- // TaskSpecs often list legacy tests/api/** surfaces; union runtime write roots so
39
- // git checkpoint / path guard can promote green BE-TEST runs.
40
- const allowedPaths = expandAllowedPathsForWorkflow(resolvedWorkflow.workflow, options.taskSpec.constraints.allowed_paths);
41
37
  const taskConfig = {
42
38
  ...baseConfig,
43
39
  taskId: harnessTaskId,
44
40
  title: options.taskSpec.title,
45
41
  taskKind: resolvedWorkflow.taskKind,
46
42
  featureId: options.taskSpec.feature_id,
47
- allowedPaths,
43
+ allowedPaths: options.taskSpec.constraints.allowed_paths,
48
44
  forbiddenPaths: options.taskSpec.constraints.forbidden_paths,
49
45
  hardConstraints: options.taskSpec.constraints.hard_constraints,
50
46
  complexity: mapRiskLevelToComplexity(options.taskSpec.risk_level),
@@ -316,13 +312,6 @@ function bulletLines(values) {
316
312
  return ["- None"];
317
313
  return values.map((value) => `- ${value}`);
318
314
  }
319
- /** Union TaskSpec allowed_paths with workflow-owned write roots. */
320
- export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
321
- const extras = workflow === "backend-test"
322
- ? ["testcase/**", "docs/test-reports/**"]
323
- : [];
324
- return Array.from(new Set([...allowedPaths, ...extras]));
325
- }
326
315
  async function loadExistingTaskConfig(repoRoot, taskId) {
327
316
  try {
328
317
  return await loadTaskConfig(repoRoot, taskId);
@@ -39,29 +39,21 @@ function toRepoRelativePath(repoRoot, candidate) {
39
39
  function structuredArtifactsFromReport(run) {
40
40
  if (!run?.nodes)
41
41
  return [];
42
- // Multiple gates may rewrite the same run-owned path (e.g. backend-test
43
- // case manifest initial → final). Keep the last claim in report node order
44
- // so projector validates against the file on disk after all rewrites.
45
- const byPath = new Map();
42
+ const artifacts = [];
46
43
  for (const node of run.nodes) {
47
- if (!node.structuredArtifactPath ||
48
- !node.structuredArtifactSha256 ||
49
- !node.structuredArtifactSchemaId) {
50
- continue;
44
+ if (node.structuredArtifactPath &&
45
+ node.structuredArtifactSha256 &&
46
+ node.structuredArtifactSchemaId) {
47
+ const identity = splitStructuredArtifactIdentity(node.structuredArtifactSchemaId);
48
+ artifacts.push({
49
+ path: node.structuredArtifactPath,
50
+ sha256: node.structuredArtifactSha256,
51
+ kind: identity.kind,
52
+ schemaId: identity.schemaId,
53
+ });
51
54
  }
52
- const identity = splitStructuredArtifactIdentity(node.structuredArtifactSchemaId);
53
- const pathKey = node.structuredArtifactPath.replace(/\\/g, "/");
54
- // Leave sha256 empty: later DAG nodes (e.g. backend-test traceability)
55
- // may rewrite the same run-owned path in place, invalidating the mint-time
56
- // structuredArtifactSha256. Projector re-hashes the file on disk.
57
- byPath.set(pathKey, {
58
- path: pathKey,
59
- sha256: "",
60
- kind: identity.kind,
61
- schemaId: identity.schemaId,
62
- });
63
55
  }
64
- return Array.from(byPath.values());
56
+ return artifacts;
65
57
  }
66
58
  /**
67
59
  * `agent-dag` — the default standard agent DAG. No dedicated test/integration
@@ -455,8 +455,7 @@ function isPathInside(root, target) {
455
455
  const relative = path.relative(root, target);
456
456
  return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
457
457
  }
458
- /** Exported for unit tests: map DAG report JSON into Worker report decision. */
459
- export function decideFromReport(workerRunId, result) {
458
+ function decideFromReport(workerRunId, result) {
460
459
  if (!result.ok || !result.json) {
461
460
  return {
462
461
  succeeded: false,
@@ -474,16 +473,11 @@ export function decideFromReport(workerRunId, result) {
474
473
  }
475
474
  const runStatus = readString(run, "status");
476
475
  const nodes = readObjectArray(run, "nodes");
477
- // Condition-branch SKIPPED nodes are expected on green backend-test / hybrid
478
- // paths (e.g. revise/repair not selected). Do not treat SKIPPED as failure.
479
- // ERROR and non-success failureCategory on executed nodes still fail closed.
480
476
  const failedNode = nodes.find((node) => {
481
477
  const status = readString(node, "status");
482
478
  const failureCategory = readString(node, "failureCategory");
483
- if (status === "SKIPPED") {
484
- return false;
485
- }
486
479
  return (status === "ERROR" ||
480
+ status === "SKIPPED" ||
487
481
  (Boolean(failureCategory) && failureCategory !== "success"));
488
482
  });
489
483
  if (runStatus !== "completed" && runStatus !== "finished") {
@@ -334,50 +334,22 @@ function asRecord(value) {
334
334
  * Prefer exact schema payloads; otherwise map common discovery shapes onto the
335
335
  * pytest-centric runtime contract without inventing secrets or managed commands.
336
336
  */
337
- /** Drop empty managedCommand strings; omit block when not managed-command. */
338
- export function sanitizeBackendTestExecutionInput(value) {
339
- const record = asRecord(value);
340
- if (!record)
341
- return value;
342
- const next = { ...record };
343
- const managed = asRecord(record.managedCommand);
344
- if (!managed)
345
- return next;
346
- const cleaned = {};
347
- for (const key of ["start", "stop", "sourceRef"]) {
348
- const raw = managed[key];
349
- if (typeof raw === "string" && raw.trim())
350
- cleaned[key] = raw.trim();
351
- }
352
- if (next.targetMode === "managed-command") {
353
- next.managedCommand = cleaned;
354
- }
355
- else if (Object.keys(cleaned).length === 0) {
356
- delete next.managedCommand;
357
- }
358
- else {
359
- // in-process / external: optional managedCommand must not carry empty strings
360
- next.managedCommand = cleaned;
361
- }
362
- return next;
363
- }
364
337
  export function coerceBackendTestExecutionInput(value) {
365
- const sanitized = sanitizeBackendTestExecutionInput(value);
366
- const direct = backendTestExecutionContractSchema.safeParse(sanitized);
338
+ const direct = backendTestExecutionContractSchema.safeParse(value);
367
339
  if (direct.success)
368
340
  return direct.data;
369
- const record = asRecord(sanitized);
341
+ const record = asRecord(value);
370
342
  if (!record)
371
- return sanitized;
343
+ return value;
372
344
  // Near-schema payloads (string framework + runner + testRoot) must stay fail-closed.
373
345
  // Only free-form discovery envelopes are rewritten onto the pytest contract.
374
346
  const looksSchemaShaped = typeof record.framework === "string" &&
375
347
  asRecord(record.runner) !== null &&
376
348
  typeof record.testRoot === "string";
377
349
  if (looksSchemaShaped)
378
- return sanitized;
350
+ return value;
379
351
  if (typeof record.framework === "string" && record.framework !== "pytest") {
380
- return sanitized;
352
+ return value;
381
353
  }
382
354
  const frameworkObj = asRecord(record.framework);
383
355
  const discovered = asRecord(record.discoveredFixtures);
@@ -532,10 +504,7 @@ export async function materializeBackendTestExecutionContract(input) {
532
504
  catch (error) {
533
505
  throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
534
506
  }
535
- const candidates = [
536
- sanitizeBackendTestExecutionInput(parsed),
537
- coerceBackendTestExecutionInput(parsed),
538
- ];
507
+ const candidates = [parsed, coerceBackendTestExecutionInput(parsed)];
539
508
  let accepted = null;
540
509
  let lastSchemaError = "invalid execution contract";
541
510
  let lastSecretError = "";
@@ -1,5 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
2
  import { readFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
@@ -422,19 +421,6 @@ export async function executeDagNode(input) {
422
421
  node.structuredArtifactSha256 = createHash("sha256").update(bytes).digest("hex");
423
422
  node.structuredArtifactSchemaId = task.shell.jsonArtifactGate.schemaId;
424
423
  }
425
- else if (task.shell?.backendTestPipeline === "finalize-effective-result") {
426
- // Pipeline materializes contracts/backend-test-result.json without jsonArtifactGate.
427
- // Bind it so Outcome adapters project kind=backend-test-result for Ready Planner.
428
- const artifactPath = path.join(runDir, "contracts", "backend-test-result.json");
429
- if (existsSync(artifactPath)) {
430
- const bytes = await readFile(artifactPath);
431
- node.structuredArtifactPath = artifactPath;
432
- node.structuredArtifactSha256 = createHash("sha256")
433
- .update(bytes)
434
- .digest("hex");
435
- node.structuredArtifactSchemaId = "backend-test-result-v1";
436
- }
437
- }
438
424
  }
439
425
  state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
440
426
  await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.16.13",
3
+ "version": "0.16.15-beta.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",