@tea-agent/loop-agent 0.16.6-beta.0 → 0.16.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 CHANGED
@@ -9,15 +9,18 @@
9
9
 
10
10
  ### 修复
11
11
 
12
- - 前端实现计划/修订节点会注入当前包内权威 `frontend-implementation-contract-v1` Schema 与固定 source binding,避免模型猜测字段导致契约门禁失败。
13
- - 前端 Mock 策略节点的 canonical 输出会把首条 `MOCK_STRATEGY:` 协议行提升为第一行,避免解释性前言触发 `first-non-empty` 门禁误判。
14
- - 前端规范回退目录统一为本地 `openspec/`,DAG 能力发现、提示词、Skill 与验证证据检查不再查找大小写不一致的旧目录名。
15
12
  - 后端测试复合 Shell pipeline 现在与普通 Shell 节点共享 Git write guard;即使命令退出成功,只要越过 `read-only`、`allowedPaths` 或 `forbiddenPaths` 边界,节点仍会 fail-closed。
16
13
  - Observe 现使用实际的 repair safety 节点,并只在修复节点真正开始执行后计入一次 attempt;条件跳过不再误报已修复,安全门禁失败会显示为 `rejected`。
17
14
  - Pi SDK 对缺少响应 ID 的累计 Token 生命周期事件改为取本次执行最大快照,避免同一响应的匿名 usage 被重复累加。
18
15
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
19
16
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为 initial/final Result、repair eligibility、traceability 与 Observe 投影保留结构化运行证据。
20
17
 
18
+ ## [0.16.6] - 2026-07-20
19
+
20
+ ### 修复
21
+
22
+ - write guard 忽略 `__pycache__` / `.pytest_cache` 等 ephemeral 解释器缓存,避免 backend-test 生成节点本地冒烟 pytest 时被误拦;并明确 generate-pytest 节点不负责执行测试。
23
+
21
24
  ## [0.16.5] - 2026-07-20
22
25
 
23
26
  ### 修复
@@ -272,7 +272,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
272
272
  persona,
273
273
  step,
274
274
  });
275
- const mapped = mapPiResultToDagNodeResult(result, input.task.firstProtocolLine);
275
+ const mapped = mapPiResultToDagNodeResult(result);
276
276
  if (!isWriteTask) {
277
277
  return mapped;
278
278
  }
@@ -315,57 +315,17 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
315
315
  durationMs: mapped.durationMs || Date.now() - started,
316
316
  };
317
317
  }
318
- export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
319
- const assistantText = canonicalizeProtocolFirstLine(result.assistantText, firstProtocolLine);
318
+ export function mapPiResultToDagNodeResult(result) {
320
319
  return {
321
320
  ok: result.ok,
322
- stdout: assistantText || result.stdout,
321
+ stdout: result.assistantText || result.stdout,
323
322
  stderr: result.stderr,
324
323
  failureCategory: result.failureCategory,
325
324
  durationMs: result.durationMs,
326
- assistantText,
325
+ assistantText: result.assistantText,
327
326
  backend: result.backend,
328
327
  sdkAttempted: result.sdkAttempted,
329
328
  tokensUsed: result.tokensUsed,
330
329
  parsedEvents: result.parsedEvents,
331
330
  };
332
331
  }
333
- function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
334
- if (!assistantText || !firstProtocolLine)
335
- return assistantText;
336
- const lines = assistantText.split(/\r?\n/);
337
- let protocolIndex = -1;
338
- let protocolLine = "";
339
- for (const [index, line] of lines.entries()) {
340
- const normalized = normalizeProtocolLine(line);
341
- if (normalized.startsWith(firstProtocolLine)) {
342
- protocolIndex = index;
343
- protocolLine = normalized;
344
- break;
345
- }
346
- }
347
- if (protocolIndex < 0)
348
- return assistantText;
349
- if (protocolIndex === 0) {
350
- return [protocolLine, ...lines.slice(1)].join("\n");
351
- }
352
- const before = lines.slice(0, protocolIndex);
353
- const after = lines.slice(protocolIndex + 1);
354
- while (before.at(-1)?.trim() === "" &&
355
- after.at(0)?.trim() === "") {
356
- after.shift();
357
- }
358
- const bodyLines = [...before, ...after];
359
- while (bodyLines.at(0)?.trim() === "")
360
- bodyLines.shift();
361
- while (bodyLines.at(-1)?.trim() === "")
362
- bodyLines.pop();
363
- return bodyLines.length > 0
364
- ? `${protocolLine}\n\n${bodyLines.join("\n")}`
365
- : protocolLine;
366
- }
367
- function normalizeProtocolLine(line) {
368
- const trimmed = line.trim();
369
- const emphasized = trimmed.match(/^(\*{1,3})\s*(.*?)\s*\1$/);
370
- return (emphasized?.[2] ?? trimmed).trim();
371
- }
@@ -308,7 +308,9 @@ export async function executeSingleSdkAttempt(options) {
308
308
  return;
309
309
  const line = serializeSessionEvent(event);
310
310
  stdoutLines.push(line);
311
- sessionEventAppender?.append(line, event);
311
+ if (sessionEventAppender) {
312
+ sessionEventAppender.append(line, event);
313
+ }
312
314
  });
313
315
  const filePrefix = options.attachedFiles.map((file) => `@${file}`).join(' ');
314
316
  const promptMessage = filePrefix
@@ -30,6 +30,33 @@ export function pathsChangedDuringRun(before, after) {
30
30
  return Array.from(changed).sort();
31
31
  }
32
32
  const DAG_RUNS_PREFIX = ".harness/dag-runs/";
33
+ /**
34
+ * Ephemeral interpreter/tool caches that writers may create while drafting tests.
35
+ * They are not product evidence and must not fail exclusive write guards.
36
+ * Intentional report/product writes remain governed by writePolicy/writeSet.
37
+ */
38
+ export function isEphemeralToolCachePath(filePath) {
39
+ const normalized = normalizePath(filePath);
40
+ if (!normalized)
41
+ return false;
42
+ if (normalized === ".pytest_cache" || normalized.startsWith(".pytest_cache/")) {
43
+ return true;
44
+ }
45
+ if (normalized === ".mypy_cache" || normalized.startsWith(".mypy_cache/")) {
46
+ return true;
47
+ }
48
+ if (normalized === ".ruff_cache" || normalized.startsWith(".ruff_cache/")) {
49
+ return true;
50
+ }
51
+ if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
52
+ return true;
53
+ }
54
+ if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
55
+ return true;
56
+ if (/\.(?:pyc|pyo)$/.test(normalized))
57
+ return true;
58
+ return false;
59
+ }
33
60
  /** MVP post-run guard: only paths that changed during the shell node are checked. */
34
61
  export function validateShellWriteGuard(input) {
35
62
  const violations = [];
@@ -42,6 +69,9 @@ export function validateShellWriteGuard(input) {
42
69
  if (filePath.startsWith(DAG_RUNS_PREFIX)) {
43
70
  continue;
44
71
  }
72
+ if (isEphemeralToolCachePath(filePath)) {
73
+ continue;
74
+ }
45
75
  if (matchesAnyPattern(filePath, forbiddenPaths)) {
46
76
  violations.push(filePath);
47
77
  continue;
@@ -18,7 +18,7 @@ const KB_CONNECTOR_TOOLS = new Set([
18
18
  ]);
19
19
  /**
20
20
  * Pattern for detecting spec-related files:
21
- * - openspec/** files
21
+ * - openSpec/** files
22
22
  * - *.spec.md / *.spec.ts / *.spec.tsx
23
23
  * - project-specs/**
24
24
  * - design-spec.md, code-standards.md, review-checklist.md, etc.
@@ -27,6 +27,7 @@ const KB_CONNECTOR_TOOLS = new Set([
27
27
  */
28
28
  const SPEC_FILE_PATTERNS = [
29
29
  /openspec\//i,
30
+ /\/openSpec\//i,
30
31
  /\/project-specs\//i,
31
32
  /\/spec\//i,
32
33
  /\.spec\.(md|tsx?|jsx?)$/i,
@@ -1,86 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
2
  import { readFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
4
  import { z } from "zod";
7
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
8
- import { findPackageRoot } from "../../shared/package-metadata.js";
9
6
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
10
- /**
11
- * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
12
- * installed loop-agent package docs/templates/ path. Package-root discovery
13
- * works from both the source module and the compiled dist module without
14
- * relying on CommonJS globals in the ESM runtime.
15
- *
16
- * Validation is fail-closed: missing file, malformed JSON, mismatched $id,
17
- * missing additionalProperties: false, or incomplete top-level required keys
18
- * all throw before any DAG prompt is assembled.
19
- */
20
- export function loadFrontendImplementationContractJsonSchema(startDir = path.dirname(fileURLToPath(import.meta.url))) {
21
- const packageRoot = findPackageRoot(startDir);
22
- if (!packageRoot) {
23
- throw new Error(`cannot locate loop-agent package root from ${path.resolve(startDir)}`);
24
- }
25
- const schemaPath = path.join(packageRoot, "docs", "templates", "frontend-implementation-contract.schema.json");
26
- let content;
27
- try {
28
- content = readFileSync(schemaPath, "utf-8");
29
- }
30
- catch (error) {
31
- throw new Error(`cannot load frontend-implementation-contract.schema.json from current loop-agent package at ${schemaPath}: ${error.code ?? String(error)}`);
32
- }
33
- let parsed;
34
- try {
35
- parsed = JSON.parse(content);
36
- }
37
- catch (error) {
38
- throw new Error(`frontend-implementation-contract.schema.json is not valid JSON: ${error.message}`);
39
- }
40
- if (parsed === null || typeof parsed !== "object") {
41
- throw new Error("frontend-implementation-contract.schema.json root is not a JSON object");
42
- }
43
- const schema = parsed;
44
- if (schema.$id !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
45
- throw new Error(`schema $id mismatch: expected ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID}, got ${String(schema.$id)}`);
46
- }
47
- if (schema.additionalProperties !== false) {
48
- throw new Error("schema must have additionalProperties: false at top level");
49
- }
50
- const expectedRequired = [
51
- "schemaVersion",
52
- "sourceBinding",
53
- "riskLevel",
54
- "targets",
55
- "requirements",
56
- "uiStates",
57
- "interactions",
58
- "mockApi",
59
- "designEvidence",
60
- "verificationTargets",
61
- "evidenceGaps",
62
- ];
63
- const actualRequired = Array.isArray(schema.required) ? schema.required : [];
64
- const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
65
- if (missing.length > 0) {
66
- throw new Error(`schema required fields missing: ${missing.join(", ")}`);
67
- }
68
- const properties = schema.properties && typeof schema.properties === "object"
69
- ? schema.properties
70
- : {};
71
- const missingProperties = expectedRequired.filter((key) => !Object.hasOwn(properties, key));
72
- if (missingProperties.length > 0) {
73
- throw new Error(`schema properties missing: ${missingProperties.join(", ")}`);
74
- }
75
- const schemaVersion = properties.schemaVersion;
76
- const mockApi = properties.mockApi;
77
- const mockApiProperties = mockApi?.properties;
78
- const productionDefaultOff = mockApiProperties?.productionDefaultOff;
79
- if (schemaVersion?.const !== 1 || productionDefaultOff?.const !== true) {
80
- throw new Error("schema fixed values are incomplete: schemaVersion.const must be 1 and mockApi.productionDefaultOff.const must be true");
81
- }
82
- return JSON.stringify(parsed);
83
- }
84
7
  const id = z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/);
85
8
  const safePath = z
86
9
  .string()
@@ -32,8 +32,8 @@ function allDeps(pkg) {
32
32
  function hasDep(deps, name) {
33
33
  return Object.hasOwn(deps, name);
34
34
  }
35
- async function listOpenspec(repoRoot) {
36
- const root = path.join(repoRoot, "openspec");
35
+ async function listOpenSpec(repoRoot) {
36
+ const root = path.join(repoRoot, "openSpec");
37
37
  if (!(await exists(root)))
38
38
  return [];
39
39
  const out = [];
@@ -70,7 +70,7 @@ export function buildAdapterGuidance(capability) {
70
70
  "## Frontend project capability (generation-time)",
71
71
  `Framework: ${capability.framework}${capability.frameworkVersion ? `@${capability.frameworkVersion}` : ""}`,
72
72
  `Evidence: ${capability.evidencePaths.join(", ") || "(none)"}`,
73
- "Rules: openspec/** and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
73
+ "Rules: openSpec/** and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
74
74
  ];
75
75
  if (capability.framework === "react") {
76
76
  lines.push("React adapter: prefer function components + hooks; reuse existing Testing Library / Vitest patterns; do not introduce new state libs without authorization.");
@@ -276,14 +276,14 @@ export async function discoverFrontendProjectCapability(repoRoot) {
276
276
  router = router ?? "next-router";
277
277
  if (hasDep(deps, "vue-router"))
278
278
  router = "vue-router";
279
- const openspec = await listOpenspec(repoRoot);
279
+ const openSpec = await listOpenSpec(repoRoot);
280
280
  const designEvidence = {
281
- normativePaths: openspec,
281
+ normativePaths: openSpec,
282
282
  advisoryPaths: [],
283
283
  conflicts: [],
284
284
  };
285
- if (openspec.length)
286
- evidencePaths.push(...openspec.slice(0, 5));
285
+ if (openSpec.length)
286
+ evidencePaths.push(...openSpec.slice(0, 5));
287
287
  const base = {
288
288
  schemaVersion: 1,
289
289
  framework,
@@ -33,7 +33,7 @@ const HIGH_RISK_PATTERNS = [
33
33
  },
34
34
  {
35
35
  id: "design-conflict",
36
- re: /design\s+conflict|规范冲突|openspec\s+conflict/i,
36
+ re: /design\s+conflict|规范冲突|openSpec\s+conflict/i,
37
37
  },
38
38
  {
39
39
  id: "cross-domain-paths",
@@ -22,7 +22,6 @@ import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRe
22
22
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
23
23
  import { classifyFrontendRisk, } from "./frontend-risk.js";
24
24
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
25
- import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
26
25
  const REQUIREMENT_FILE = "需求.md";
27
26
  const CONSTRAINT_FILE = "执行约束.md";
28
27
  const REFERENCE_DIRECTORY = "references";
@@ -1356,11 +1355,10 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
1356
1355
  allowedPaths: readOnlyPaths,
1357
1356
  forbiddenPaths,
1358
1357
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
1359
- firstProtocolLine: "MOCK_STRATEGY:",
1360
- outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
1358
+ outputContract: "Plain Markdown whose first non-empty line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
1361
1359
  subtask_prompt: [
1362
1360
  "Perform read-only Mock assessment and select one safe frontend data strategy.",
1363
- "The first line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked. Do not emit blank lines, headings, or explanatory preamble before it.",
1361
+ "The first non-empty line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked.",
1364
1362
  "Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
1365
1363
  autoMaySkipMissingMock
1366
1364
  ? "Auto mode may skip Mock when no project Mock capability is confirmed. Select not-needed with positive evidence from contract/scout that no project Mock capability is confirmed, continue without adding Mock files or dependencies, run the fixed verification entrypoints, and record any unproved real API behavior in Real Integration Gap. Do not block solely because no project Mock capability, browser interception harness, or request adapter exists."
@@ -1618,7 +1616,7 @@ function resolveFrontendCapabilityContextBlock(sources) {
1618
1616
  if (capability) {
1619
1617
  parts.push("", capability.adapterGuidance);
1620
1618
  if (capability.designEvidence.normativePaths.length > 0) {
1621
- parts.push(`openspec normative candidates: ${capability.designEvidence.normativePaths.slice(0, 12).join(", ")}`);
1619
+ parts.push(`openSpec normative candidates: ${capability.designEvidence.normativePaths.slice(0, 12).join(", ")}`);
1622
1620
  }
1623
1621
  parts.push(`A11y capability: ${capability.a11y.status}` +
1624
1622
  (capability.a11y.tools.length
@@ -1751,46 +1749,6 @@ function buildFrontendHybridDagFromTask(sources) {
1751
1749
  allowedPaths: taskConfig.allowedPaths,
1752
1750
  complexity: taskConfig.complexity,
1753
1751
  });
1754
- const frontendSourceBinding = buildDagSourceBinding(sources);
1755
- const frontendContractSchemaBlock = (() => {
1756
- const schema = loadFrontendImplementationContractJsonSchema();
1757
- const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
1758
- if (!requirement) {
1759
- throw new Error("frontend implementation contract context requires a bound requirement source");
1760
- }
1761
- const referencePaths = frontendSourceBinding.sources
1762
- .filter((s) => s.kind === "reference")
1763
- .map((s) => s.path);
1764
- const fixedFields = {
1765
- schemaVersion: 1,
1766
- sourceBinding: {
1767
- taskId: frontendSourceBinding.taskId,
1768
- requirementPath: requirement.path,
1769
- requirementSha256: requirement.sha256,
1770
- referencePaths,
1771
- requirementIds: frontendSourceBinding.requirementIds,
1772
- },
1773
- riskLevel: frontendRisk.selectedRisk,
1774
- targets: { files: implementPaths.writeSet },
1775
- };
1776
- return [
1777
- `## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative; do not guess fields)`,
1778
- schema,
1779
- "",
1780
- "## Fixed contract fields (deterministic; copy exactly and do not modify)",
1781
- JSON.stringify(fixedFields),
1782
- "",
1783
- "## Forbidden fields (these are NOT in the schema; do not emit)",
1784
- "- schemaId",
1785
- "- targetFiles",
1786
- "- requirementCoverage",
1787
- "",
1788
- "## Critical rules",
1789
- "- verificationTargets is a TOP-LEVEL required array",
1790
- "- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
1791
- "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
1792
- ].join("\n");
1793
- })();
1794
1752
  const sourceContext = [
1795
1753
  buildSourceContextBlock(sources),
1796
1754
  capabilityContextBlock,
@@ -1799,7 +1757,7 @@ function buildFrontendHybridDagFromTask(sources) {
1799
1757
  .join("\n\n");
1800
1758
  const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
1801
1759
  mockCapability.verifyCommands.length > 0;
1802
- const requirementIds = frontendSourceBinding.requirementIds;
1760
+ const requirementIds = buildDagSourceBinding(sources).requirementIds;
1803
1761
  const requirementCoverageInstruction = requirementIds.length > 0
1804
1762
  ? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
1805
1763
  : "";
@@ -1958,7 +1916,6 @@ function buildFrontendHybridDagFromTask(sources) {
1958
1916
  fixedVerificationContext,
1959
1917
  sourceContext,
1960
1918
  mockContextBlock,
1961
- frontendContractSchemaBlock,
1962
1919
  ].join("\n\n"),
1963
1920
  },
1964
1921
  {
@@ -2035,7 +1992,6 @@ function buildFrontendHybridDagFromTask(sources) {
2035
1992
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2036
1993
  "End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
2037
1994
  sourceContext,
2038
- frontendContractSchemaBlock,
2039
1995
  ].join("\n\n"),
2040
1996
  },
2041
1997
  ...(requirementIds.length > 0
@@ -3052,6 +3008,7 @@ function buildGenerateBackendPytestNode(sources) {
3052
3008
  "- If a test filename exists, add suffix: test_order.py → test_order_01.py",
3053
3009
  "- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
3054
3010
  "- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
3011
+ "- Do NOT execute pytest/python -m pytest or npm test in this node; initial/final execution is owned by dedicated shell nodes. Local smoke runs create __pycache__/.pytest_cache and are unnecessary here.",
3055
3012
  ].join("\n\n"),
3056
3013
  };
3057
3014
  }
@@ -306,7 +306,6 @@ export const dagTaskSchema = z.object({
306
306
  shell: dagShellConfigSchema.optional(),
307
307
  static: dagStaticConfigSchema.optional(),
308
308
  outputContract: z.string().optional(),
309
- firstProtocolLine: z.string().min(1).optional(),
310
309
  allowedPaths: z.array(z.string()).optional().default([]),
311
310
  forbiddenPaths: z.array(z.string()).optional().default([]),
312
311
  decisionGate: dagDecisionGateSchema.optional(),
@@ -389,11 +389,6 @@
389
389
  "type": "string",
390
390
  "minLength": 1
391
391
  },
392
- "firstProtocolLine": {
393
- "type": "string",
394
- "minLength": 1,
395
- "description": "Optional protocol prefix whose first matching Pi assistant-output line is promoted to the canonical first line. Missing matches are not synthesized."
396
- },
397
392
  "allowedPaths": {
398
393
  "type": "array",
399
394
  "items": { "type": "string", "minLength": 1 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.16.6-beta.0",
3
+ "version": "0.16.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -12,7 +12,7 @@ references:
12
12
  For first/final design review nodes. Read the checklist, then audit contract, scout,
13
13
  mock strategy, plan/revision, task constraints/bounds, and traceable design evidence. The knowledge-
14
14
  base connector is TODO: never invent results. If absent/failed/unmatched, require
15
- `<repoRoot>/openspec/**` search/read evidence before repo conventions.
15
+ `<repoRoot>/openSpec/**` search/read evidence before repo conventions.
16
16
 
17
17
  ## Verdict Contract
18
18
 
@@ -27,7 +27,7 @@ remaining, incomplete, or newly introduced gaps.
27
27
 
28
28
  - Any criterion lacks implementation/verification; UI states lack reasons; a
29
29
  dependency lacks permission; confirmed primitives/rules are ignored; design claims
30
- lack knowledge-base or required `openspec/` evidence; paths cross write bounds;
30
+ lack knowledge-base or required `openSpec/` evidence; paths cross write bounds;
31
31
  commands are missing/non-deterministic; or interaction, responsive, accessibility,
32
32
  data, or failure behavior requires guessing.
33
33
  - `MOCK_STRATEGY: blocked`; missing permitted target paths, endpoint/schema-to-fixture
@@ -35,7 +35,7 @@ remaining, incomplete, or newly introduced gaps.
35
35
  inline fake data; commented real requests; Mock-on production defaults; test-only
36
36
  production imports; or Mock evidence reported as real integration.
37
37
 
38
- Knowledge-base absence is advisory if relevant `openspec/` rules were searched/read
38
+ Knowledge-base absence is advisory if relevant `openSpec/` rules were searched/read
39
39
  and applied. Block skipped fallback, unresolved conflict, or unresolved UI decisions.
40
40
 
41
41
  ## Method And Output
@@ -48,7 +48,7 @@ Advisory, and never edit files.
48
48
  Run `grep`/`find`, then explicit `read` calls for applicable specs and checklist.
49
49
  Only successful paired reads count as “已读取规范文件”; summaries do not. List each
50
50
  read path/section in `Checked Items`. If the connector is unavailable, search/read
51
- `openspec/` before accepting repository conventions.
51
+ `openSpec/` before accepting repository conventions.
52
52
 
53
53
  ```markdown
54
54
  VERDICT: pass
@@ -9,7 +9,7 @@
9
9
  ## Project Fit
10
10
 
11
11
  - Reuse components, hooks, API helpers, mocks, schemas, router patterns, tokens, and theme rules.
12
- - Cite knowledge-base or `openspec/`; failed/empty knowledge queries must search `<repoRoot>/openspec/**`.
12
+ - Cite knowledge-base or `openSpec/`; failed/empty knowledge queries must search `<repoRoot>/openSpec/**`.
13
13
  - Record source status, query terms, paths/headings, conflicts, authorized deps, and allowed paths.
14
14
 
15
15
  ## Interaction / Quality
@@ -35,6 +35,6 @@
35
35
 
36
36
  ## Verdict Matrix
37
37
 
38
- - Request revision for coverage gaps, unsafe scope, unauthorized deps, unresolved required interaction, missing required verification, skipped `openspec/` fallback, unsafe/missing Mock strategy, or Mock evidence presented as real integration.
39
- - Knowledge-base unavailable but relevant `openspec/` rules applied is advisory only.
38
+ - Request revision for coverage gaps, unsafe scope, unauthorized deps, unresolved required interaction, missing required verification, skipped `openSpec/` fallback, unsafe/missing Mock strategy, or Mock evidence presented as real integration.
39
+ - Knowledge-base unavailable but relevant `openSpec/` rules applied is advisory only.
40
40
  - Optional cleanup that cannot affect acceptance is advisory.
@@ -23,7 +23,7 @@ Read all required references before running any listed frontend node.
23
23
  ## Source And Evidence Rules
24
24
 
25
25
  Use task sources/references, constraints, then `task.json`. Follow `design-spec.md`:
26
- knowledge base; `<repoRoot>/openspec/**` after unavailable/failed/empty; then repo
26
+ knowledge base; `<repoRoot>/openSpec/**` after unavailable/failed/empty; then repo
27
27
  evidence. Cite tight paths/symbols, label gaps/conflicts, and never invent APIs,
28
28
  rules, commands, or retrievals. Scout/planners locate and explicitly read applicable
29
29
  specs; only successful paired reads count. Lockfile-only, fixture-only, or unread
@@ -2,7 +2,7 @@
2
2
 
3
3
  Discover rules from task constraints, `design-spec.md` source order, config, code,
4
4
  tests, manifests, and generated types. After knowledge-base failure, applicable
5
- `<repoRoot>/openspec/**` rules are normative. Preferences are not rules, and docs do
5
+ `<repoRoot>/openSpec/**` rules are normative. Preferences are not rules, and docs do
6
6
  not override installed APIs without an explicit compatibility decision.
7
7
 
8
8
  ## Discover And Cite
@@ -3,13 +3,13 @@
3
3
  ## Required Source Sequence
4
4
 
5
5
  1. Attempt the configured component/design knowledge-base query first.
6
- 2. If unavailable, failed, timed out, or unmatched, recursively search the project root's exact `openspec/` directory.
6
+ 2. If unavailable, failed, timed out, or unmatched, recursively search the project root's exact `openSpec/` directory.
7
7
  3. Treat relevant matches as the current project's specification for this run.
8
8
  4. Only then use component source, tokens, stories, tests, and pages as non-normative repository fallback.
9
9
 
10
- Never skip `openspec/` directly to neighboring-code conventions. Report source
10
+ Never skip `openSpec/` directly to neighboring-code conventions. Report source
11
11
  conflicts instead of combining them. Explicit task requirements remain the contract;
12
- flag conflicts with knowledge-base or `openspec/` rules.
12
+ flag conflicts with knowledge-base or `openSpec/` rules.
13
13
 
14
14
  ## Knowledge Base Connection — TODO
15
15
 
@@ -17,11 +17,11 @@ Request format is undecided. TODO: define connector/owner, namespaces, secret-fr
17
17
  auth, query fields, result identity/version/time, and failure behavior.
18
18
 
19
19
  Attempt only a connector actually available in the execution environment. Otherwise
20
- record `not-configured` and run the `openspec/` fallback; never invent a connection.
20
+ record `not-configured` and run the `openSpec/` fallback; never invent a connection.
21
21
 
22
- ## `openspec/` Fallback Procedure
22
+ ## `openSpec/` Fallback Procedure
23
23
 
24
- - Confirm whether `<repoRoot>/openspec/` exists and enumerate its files recursively.
24
+ - Confirm whether `<repoRoot>/openSpec/` exists and enumerate its files recursively.
25
25
  - Read indexes first, then search names/content using task, route, component, interaction, theme, token, and state terms.
26
26
  - Read relevant matches in context; do not treat a filename-only hit as a rule.
27
27
  - Record search terms, inspected/matched paths, headings or tight line ranges, applied rules, and conflicts.
@@ -29,9 +29,9 @@ record `not-configured` and run the `openspec/` fallback; never invent a connect
29
29
 
30
30
  ## Retrieval Evidence
31
31
 
32
- Record source as `knowledge-base`, `openspec fallback`, `repository fallback`, or
32
+ Record source as `knowledge-base`, `openSpec fallback`, `repository fallback`, or
33
33
  `unavailable`. Knowledge-base evidence includes query, source ID/version/time, rules,
34
- and conflicts. `openspec fallback` includes terms, paths/headings/lines, rules, and conflicts.
34
+ and conflicts. `openSpec fallback` includes terms, paths/headings/lines, rules, and conflicts.
35
35
 
36
36
  ## Rules To Retrieve Or Discover
37
37
 
@@ -42,5 +42,5 @@ and conflicts. `openspec fallback` includes terms, paths/headings/lines, rules,
42
42
 
43
43
  - Reuse confirmed primitives unless a new pattern is authorized.
44
44
  - Define applicable states and responsive behavior before implementation.
45
- - Cite knowledge-base or `openspec/` evidence for component/token choices; label weaker repository fallback.
45
+ - Cite knowledge-base or `openSpec/` evidence for component/token choices; label weaker repository fallback.
46
46
  - Make deviations and unresolved gaps explicit.
@@ -5,11 +5,11 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
5
5
  ## Core nodes
6
6
 
7
7
  - **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`. No guessed requirements.
8
- - **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Knowledge base first; else search+read `<repoRoot>/openspec/**` before repo fallback. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
9
- - **`frontend-mock-assess-pi` + gate**: declares `firstProtocolLine: "MOCK_STRATEGY:"`; canonical output first line
8
+ - **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Knowledge base first; else search+read `<repoRoot>/openSpec/**` before repo fallback. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
9
+ - **`frontend-mock-assess-pi` + gate**: first non-empty line
10
10
  `MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked`
11
- Pi output mapping promotes the first matching protocol line ahead of any preamble without inventing or replacing its value; missing, malformed, or blocked strategies still fail closed. Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
12
- - **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix). The plan-pi prompt now includes the complete `frontend-implementation-contract-v1` JSON Schema loaded from the loop-agent package `docs/templates/` path, plus deterministic source binding, risk level, and allowed implementation targets. The model does not need to search or guess contract fields; `schemaId`, `targetFiles`, `requirementCoverage` are explicitly forbidden.
11
+ Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
12
+ - **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix).
13
13
  - **`frontend-implement-pi`**: sole exclusive writer. Stay in `writeSet`; real requests default-on; Mock reversible, dev/test-only, production-off. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths or guesses. Output changed files, behavior, UI states, styling notes, verification attempted, residual risks. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. Skipped-Mock `not-needed` keeps real integration pending unless the real backend path has fresh evidence.
14
14
 
15
15
  ## Contract / trace / stages (M1–M2)
@@ -24,4 +24,4 @@ static/behavior/trace may `nonZeroExitPolicy: record`. Assess → `contracts/fro
24
24
 
25
25
  ## Risk & capability (M4–M6)
26
26
 
27
- Deterministic risk (no model); high-risk beats small; supervised never small. Small may drop first design gate + plan-revision; contract shell retargets to `frontend-plan-pi`. Capability seed injects adapters; openspec/task sources outrank. A11y: static/component tools only when present; Browser a11y always not-run.
27
+ Deterministic risk (no model); high-risk beats small; supervised never small. Small may drop first design gate + plan-revision; contract shell retargets to `frontend-plan-pi`. Capability seed injects adapters; openSpec/task sources outrank. A11y: static/component tools only when present; Browser a11y always not-run.
@@ -37,7 +37,7 @@ required check, forbidden write, or unmet acceptance criterion forces revision.
37
37
  and no false real-integration claim. `not-needed` needs applicable real/no-remote
38
38
  evidence, or an explicit default-auto skipped-Mock rationale with the Real
39
39
  Integration Gap preserved when no project Mock capability is confirmed.
40
- - Component/design claims require traceable knowledge-base evidence or, after connection/query failure or no match, relevant `<repoRoot>/openspec/**` evidence. The connector format is TODO; never claim a query or fallback search without evidence. Execute explicit `grep`/`find` to locate spec files and `read` to load them before referencing their rules. Only successful `read` tool calls are observable as "已读取规范文件" in the spec-evidence inspector.
40
+ - Component/design claims require traceable knowledge-base evidence or, after connection/query failure or no match, relevant `<repoRoot>/openSpec/**` evidence. The connector format is TODO; never claim a query or fallback search without evidence. Execute explicit `grep`/`find` to locate spec files and `read` to load them before referencing their rules. Only successful `read` tool calls are observable as "已读取规范文件" in the spec-evidence inspector.
41
41
  - Treat shell exit status as authoritative. Do not edit files.
42
42
 
43
43
  ## Evidence And Output
@@ -11,7 +11,7 @@
11
11
  - Cite tight file locations, exact commands/results, or named DAG artifacts.
12
12
  - Never invent evidence; name the missing check. An implementation summary is not the actual diff.
13
13
  - Failed required static/behavior verification is at least Important unless proven unrelated.
14
- - A knowledge-base claim records connector/query, source ID/version, and retrieval time. If absent, failed, or unmatched, review evidence must show `<repoRoot>/openspec/**` search terms and matched paths/headings; label `openspec fallback`, `repository fallback`, or `unavailable` accurately.
14
+ - A knowledge-base claim records connector/query, source ID/version, and retrieval time. If absent, failed, or unmatched, review evidence must show `<repoRoot>/openSpec/**` search terms and matched paths/headings; label `openSpec fallback`, `repository fallback`, or `unavailable` accurately.
15
15
 
16
16
  ## Review Sequence
17
17
 
@@ -25,7 +25,7 @@
25
25
  6. Check component/design evidence, responsive/accessibility behavior, dependencies, and maintenance fit when applicable.
26
26
  7. Classify findings and derive the verdict mechanically.
27
27
 
28
- Skipping the required `openspec/` search after knowledge-base failure is Important
28
+ Skipping the required `openSpec/` search after knowledge-base failure is Important
29
29
  when component/design compliance affects acceptance or implementation choices.
30
30
 
31
31
  Use one issue per finding:
@@ -24,9 +24,9 @@ verdict/findings, and required browser, visual, manual, or knowledge evidence.
24
24
  - Mock-backed behavior proves frontend rendering and state transitions only. It never
25
25
  proves backend readiness, transport compatibility, or real API integration.
26
26
  - Unavailable commands remain gaps.
27
- - Resolve design evidence via knowledge base, then `<repoRoot>/openspec/**` after
27
+ - Resolve design evidence via knowledge base, then `<repoRoot>/openSpec/**` after
28
28
  failure/no match. Its connector format remains TODO; never invent it. An applied
29
- `openspec fallback` is available project evidence.
29
+ `openSpec fallback` is available project evidence.
30
30
  - Separate Mock service/handler checks from page consumption and record the
31
31
  dev/test-only boundary; handler tests alone do not prove page use.
32
32
 
@@ -12,9 +12,9 @@
12
12
 
13
13
  ## Design And Component Evidence
14
14
 
15
- - Claims cite knowledge-base retrieval or `<repoRoot>/openspec/**` fallback.
15
+ - Claims cite knowledge-base retrieval or `<repoRoot>/openSpec/**` fallback.
16
16
  - Evidence records query/source/time or fallback search terms, paths, headings, and applied rules.
17
- - Relevant `openspec/` matches satisfy source availability; missing both sources blocks explicit compliance or required design decisions.
17
+ - Relevant `openSpec/` matches satisfy source availability; missing both sources blocks explicit compliance or required design decisions.
18
18
 
19
19
  ## Status
20
20
 
@@ -120,7 +120,7 @@ contract-pi → scout-src ∥ scout-tests → plan-pi → write-set-audit-pi
120
120
  | `authority-surface-audit-pi` + `authority-surface-gate-shell` | 可选 permission/state/tool-exposure audit;仅 authority signal 或显式 `authority-surface-audit` marker 时插入;gate 仅接受 `VERDICT: pass` |
121
121
  | `review-pi` + `review-verdict-recovery-pi` + `review-gate-shell` | Critical/Important → `request-revision`;recovery 只规范化 VERDICT 协议(不得从自然语言猜 pass);gate 只认 `review-verdict-recovery-pi` 的 `VERDICT: pass` |
122
122
 
123
- **Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty` 以兼容;需要固定非 `VERDICT:` 协议首行的 Pi 节点可显式声明 `firstProtocolLine`,executor 会把第一条匹配前缀的行提升为 canonical 首行,不匹配时不伪造。`frontend-mock-assess-pi` 用它固定 `MOCK_STRATEGY:`,gate 仍保持 `first-non-empty` exact-match。supervised gate 用 `first-verdict-line` 选 Pi 在 preamble 或常见整行 Markdown emphasis(如 `**VERDICT: pass**`)后第一条 normalized `VERDICT:` line。勿用 `result.summary.md`、grep VERDICT、latest-active-run discovery 或 multi-command stateful gate。`--strict-governance` 对 anti-pattern fail。supervisor 仍为 `executor: pi` 上的 `role: supervisor`。
123
+ **Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty` 以兼容;supervised gate 用 `first-verdict-line` 选 Pi 在 preamble 或常见整行 Markdown emphasis(如 `**VERDICT: pass**`)后第一条 normalized `VERDICT:` line。勿用 `result.summary.md`、grep VERDICT、latest-active-run discovery 或 multi-command stateful gate。`--strict-governance` 对 anti-pattern fail。supervisor 仍为 `executor: pi` 上的 `role: supervisor`。
124
124
 
125
125
  **Repair artifact gate contract(`shell.repairArtifactGate`)**:声明 `fromNodeId`(supervisor artifact 节点)与 `repairNodeId`(承接修订的 Pi 修复节点)。runner **不再**按节点名(历史 `repair-cursor` / `repair-pi`)猜测 repair 节点:显式 `repairNodeId` 必须存在、直接 `depends_on` gate、且是受治理 Pi writer(`executor: pi`、`toolProfile: write`、`writePolicy: exclusive`、`allowedPaths`+`writeSet` 非空且 `writeSet` 不与 `forbiddenPaths` 冲突)。新生成的 supervised DAG 总是写入 `repairNodeId`;旧 DAG 缺失时只在能唯一、安全地推导出下游 Pi writer 时兼容,零个或多个候选、或候选不满足契约都在执行前 fail closed。validation 覆盖存在性、直接下游、writer 属性与路径边界。
126
126