@tea-agent/loop-agent 0.16.1-beta.1 → 0.16.1

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.
Files changed (93) hide show
  1. package/AGENTS.md +4 -8
  2. package/CHANGELOG.md +59 -16
  3. package/README.md +76 -299
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/dag-reconcile-run.js +3 -116
  24. package/dist/commands/eval.js +1176 -13
  25. package/dist/commands/init.js +7 -1
  26. package/dist/executors/dag-pi-executor.js +4 -44
  27. package/dist/executors/shell-executor.js +1 -1
  28. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  29. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  30. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  31. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  32. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  33. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  34. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  35. package/dist/records/promotion.js +3 -1
  36. package/dist/worker/cli.js +83 -0
  37. package/dist/worker/delivery/git-transaction.js +75 -0
  38. package/dist/worker/delivery/verification-bundle.js +13 -2
  39. package/dist/worker/feature/review.js +3 -2
  40. package/dist/worker/observe/static/dag-helpers.js +0 -62
  41. package/dist/worker/observe/static/styles.css +18 -55
  42. package/dist/worker/observe/static/views/dag.js +13 -5
  43. package/dist/worker/outcomes/adapters.js +4 -1
  44. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  45. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  46. package/dist/worker/outcomes/gate.js +10 -11
  47. package/dist/worker/outcomes/projector.js +30 -4
  48. package/dist/worker/outcomes/types.js +3 -0
  49. package/dist/worker/pool/reconcile.js +285 -0
  50. package/dist/worker/run-task/run-task.js +81 -4
  51. package/dist/worker/runner/run-ready.js +25 -2
  52. package/dist/worker/task-graph/ready-planner.js +14 -8
  53. package/dist/worker/task-graph/task-graph-schema.js +5 -3
  54. package/dist/workflows/dag/budget-enforcement.js +67 -0
  55. package/dist/workflows/dag/context-policy.js +137 -0
  56. package/dist/workflows/dag/failure-routing.js +7 -0
  57. package/dist/workflows/dag/frontend-implementation-contract.js +0 -77
  58. package/dist/workflows/dag/init-hybrid.js +33 -53
  59. package/dist/workflows/dag/knowledge-curator.js +3 -0
  60. package/dist/workflows/dag/node-execution.js +11 -4
  61. package/dist/workflows/dag/prompt.js +1 -1
  62. package/dist/workflows/dag/reconcile-run.js +121 -0
  63. package/dist/workflows/dag/report.js +12 -0
  64. package/dist/workflows/dag/runner.js +43 -16
  65. package/dist/workflows/dag/skill-snapshot.js +11 -7
  66. package/dist/workflows/dag/types.js +18 -1
  67. package/dist/workflows/dag/validate.js +15 -1
  68. package/docs/README.md +3 -1
  69. package/docs/architecture/runtime-boundaries.md +3 -2
  70. package/docs/init-surface.manifest.json +4 -0
  71. package/docs/local-development-environment.md +52 -0
  72. package/docs/templates/agent-dag.schema.json +0 -5
  73. package/docs/templates/agent-dag.supervised-implementation.json +23 -4
  74. package/docs/templates/branch-merge-report.md +14 -0
  75. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  76. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  77. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  78. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  79. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  80. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  81. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  82. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  83. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  84. package/docs/templates/product-line/AGENTS.md +1 -0
  85. package/docs/templates/product-line/README.md +17 -0
  86. package/docs/templates/product-line/acceptance.yaml +9 -0
  87. package/docs/templates/product-line/feature.yaml +11 -0
  88. package/docs/templates/product-line/task-graph.yaml +8 -0
  89. package/docs/templates/product-line/task.yaml +4 -0
  90. package/package.json +2 -1
  91. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  92. package/skills/loop-agent/references/command-reference.md +5 -0
  93. package/skills/loop-agent/references/hybrid-dag.md +3 -3
@@ -0,0 +1,137 @@
1
+ import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, } from "./skill-instructions.js";
2
+ import { MAX_UPSTREAM_CHARS } from "./prompt.js";
3
+ import { resolveDagNodeSkills } from "./skills.js";
4
+ export const CONTEXT_POLICY_IDS = [
5
+ "baseline-v1",
6
+ "role-specialized-v1",
7
+ ];
8
+ export const DEFAULT_CONTEXT_POLICY_ID = "baseline-v1";
9
+ function roleOrUndefined(task) {
10
+ return task.role;
11
+ }
12
+ function pickByRole(table, role) {
13
+ if (role && table[role] !== undefined)
14
+ return table[role];
15
+ return table.default;
16
+ }
17
+ class BaselineContextPolicy {
18
+ id = "baseline-v1";
19
+ description = "Current DAG context assembly: shared upstream char budget, role skill defaults, learned patterns only for implementer.";
20
+ resolveSkills(spec, task) {
21
+ return resolveDagNodeSkills(spec, task);
22
+ }
23
+ resolveSkillInstructionBudget(task) {
24
+ return {
25
+ includeLearnedPatterns: task.role === "implementer",
26
+ perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
27
+ totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
28
+ };
29
+ }
30
+ resolveMaxUpstreamChars(_task) {
31
+ return MAX_UPSTREAM_CHARS;
32
+ }
33
+ toManifest() {
34
+ return {
35
+ schemaVersion: 1,
36
+ policyId: this.id,
37
+ description: this.description,
38
+ knobs: {
39
+ maxUpstreamCharsByRole: { default: MAX_UPSTREAM_CHARS },
40
+ includeLearnedPatternsRoles: ["implementer"],
41
+ perSkillMaxCharsByRole: {
42
+ default: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
43
+ },
44
+ totalMaxCharsByRole: {
45
+ default: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
46
+ },
47
+ },
48
+ };
49
+ }
50
+ }
51
+ /**
52
+ * First A/B challenger: keep skill name resolution identical to baseline, but
53
+ * specialize upstream / skill-instruction budgets by role so scouts see less
54
+ * noise and implementers retain more upstream + learned patterns surface.
55
+ */
56
+ class RoleSpecializedContextPolicy {
57
+ id = "role-specialized-v1";
58
+ description = "Role-specialized upstream and skill-instruction budgets; skill names still resolve via baseline merge order.";
59
+ maxUpstreamCharsByRole = {
60
+ default: MAX_UPSTREAM_CHARS,
61
+ scout: 1_200,
62
+ reviewer: 1_200,
63
+ implementer: 3_000,
64
+ verifier: 1_600,
65
+ closeout: 1_600,
66
+ planner: MAX_UPSTREAM_CHARS,
67
+ supervisor: MAX_UPSTREAM_CHARS,
68
+ };
69
+ perSkillMaxCharsByRole = {
70
+ default: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
71
+ scout: 2_500,
72
+ implementer: 3_500,
73
+ };
74
+ totalMaxCharsByRole = {
75
+ default: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
76
+ scout: 10_000,
77
+ implementer: 14_000,
78
+ };
79
+ learnedPatternRoles = new Set([
80
+ "implementer",
81
+ "closeout",
82
+ ]);
83
+ resolveSkills(spec, task) {
84
+ return resolveDagNodeSkills(spec, task);
85
+ }
86
+ resolveSkillInstructionBudget(task) {
87
+ const role = roleOrUndefined(task);
88
+ return {
89
+ includeLearnedPatterns: role
90
+ ? this.learnedPatternRoles.has(role)
91
+ : false,
92
+ perSkillMaxChars: pickByRole(this.perSkillMaxCharsByRole, role),
93
+ totalMaxChars: pickByRole(this.totalMaxCharsByRole, role),
94
+ };
95
+ }
96
+ resolveMaxUpstreamChars(task) {
97
+ return pickByRole(this.maxUpstreamCharsByRole, roleOrUndefined(task));
98
+ }
99
+ toManifest() {
100
+ return {
101
+ schemaVersion: 1,
102
+ policyId: this.id,
103
+ description: this.description,
104
+ knobs: {
105
+ maxUpstreamCharsByRole: this.maxUpstreamCharsByRole,
106
+ includeLearnedPatternsRoles: [...this.learnedPatternRoles],
107
+ perSkillMaxCharsByRole: this.perSkillMaxCharsByRole,
108
+ totalMaxCharsByRole: this.totalMaxCharsByRole,
109
+ },
110
+ };
111
+ }
112
+ }
113
+ const POLICIES = {
114
+ "baseline-v1": new BaselineContextPolicy(),
115
+ "role-specialized-v1": new RoleSpecializedContextPolicy(),
116
+ };
117
+ export function isContextPolicyId(value) {
118
+ return CONTEXT_POLICY_IDS.includes(value);
119
+ }
120
+ export function getContextPolicy(policyId) {
121
+ return POLICIES[policyId];
122
+ }
123
+ export function listContextPolicies() {
124
+ return CONTEXT_POLICY_IDS.map((id) => POLICIES[id]);
125
+ }
126
+ export function resolveContextPolicyId(spec) {
127
+ const raw = spec.defaults?.contextPolicyId;
128
+ if (!raw)
129
+ return DEFAULT_CONTEXT_POLICY_ID;
130
+ if (!isContextPolicyId(raw)) {
131
+ throw new Error(`unknown contextPolicyId "${raw}"; expected one of ${CONTEXT_POLICY_IDS.join(", ")}`);
132
+ }
133
+ return raw;
134
+ }
135
+ export function resolveContextPolicy(spec) {
136
+ return getContextPolicy(resolveContextPolicyId(spec));
137
+ }
@@ -46,6 +46,13 @@ function routeToProductLine(input) {
46
46
  case "skipped":
47
47
  return "DependencyFailure";
48
48
  case "shell-command":
49
+ if (/\b(ENOENT|PATH|command not found|No such file|not found in PATH|bash: .*: No such file)\b/i.test(raw)) {
50
+ return "EnvFailure";
51
+ }
52
+ if (/\b(missing VERDICT line|verdict gate blocked|VERDICT)\b/i.test(raw) &&
53
+ /\b(missing|malformed|unexpected|blocked)\b/i.test(raw)) {
54
+ return "ContractMismatch";
55
+ }
49
56
  if (raw.includes("flaky"))
50
57
  return "FlakyTest";
51
58
  if (nodeId.includes("test") || raw.includes("test-bug")) {
@@ -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()
@@ -21,7 +21,6 @@ import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPref
21
21
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
22
22
  import { classifyFrontendRisk, } from "./frontend-risk.js";
23
23
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
24
- import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
25
24
  const REQUIREMENT_FILE = "需求.md";
26
25
  const CONSTRAINT_FILE = "执行约束.md";
27
26
  const REFERENCE_DIRECTORY = "references";
@@ -1339,11 +1338,10 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
1339
1338
  allowedPaths: readOnlyPaths,
1340
1339
  forbiddenPaths,
1341
1340
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
1342
- firstProtocolLine: "MOCK_STRATEGY:",
1343
- 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.",
1341
+ 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.",
1344
1342
  subtask_prompt: [
1345
1343
  "Perform read-only Mock assessment and select one safe frontend data strategy.",
1346
- "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.",
1344
+ "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.",
1347
1345
  "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.",
1348
1346
  autoMaySkipMissingMock
1349
1347
  ? "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."
@@ -1734,46 +1732,6 @@ function buildFrontendHybridDagFromTask(sources) {
1734
1732
  allowedPaths: taskConfig.allowedPaths,
1735
1733
  complexity: taskConfig.complexity,
1736
1734
  });
1737
- const frontendSourceBinding = buildDagSourceBinding(sources);
1738
- const frontendContractSchemaBlock = (() => {
1739
- const schema = loadFrontendImplementationContractJsonSchema();
1740
- const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
1741
- if (!requirement) {
1742
- throw new Error("frontend implementation contract context requires a bound requirement source");
1743
- }
1744
- const referencePaths = frontendSourceBinding.sources
1745
- .filter((s) => s.kind === "reference")
1746
- .map((s) => s.path);
1747
- const fixedFields = {
1748
- schemaVersion: 1,
1749
- sourceBinding: {
1750
- taskId: frontendSourceBinding.taskId,
1751
- requirementPath: requirement.path,
1752
- requirementSha256: requirement.sha256,
1753
- referencePaths,
1754
- requirementIds: frontendSourceBinding.requirementIds,
1755
- },
1756
- riskLevel: frontendRisk.selectedRisk,
1757
- targets: { files: implementPaths.writeSet },
1758
- };
1759
- return [
1760
- `## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative; do not guess fields)`,
1761
- schema,
1762
- "",
1763
- "## Fixed contract fields (deterministic; copy exactly and do not modify)",
1764
- JSON.stringify(fixedFields),
1765
- "",
1766
- "## Forbidden fields (these are NOT in the schema; do not emit)",
1767
- "- schemaId",
1768
- "- targetFiles",
1769
- "- requirementCoverage",
1770
- "",
1771
- "## Critical rules",
1772
- "- verificationTargets is a TOP-LEVEL required array",
1773
- "- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
1774
- "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
1775
- ].join("\n");
1776
- })();
1777
1735
  const sourceContext = [
1778
1736
  buildSourceContextBlock(sources),
1779
1737
  capabilityContextBlock,
@@ -1782,7 +1740,7 @@ function buildFrontendHybridDagFromTask(sources) {
1782
1740
  .join("\n\n");
1783
1741
  const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
1784
1742
  mockCapability.verifyCommands.length > 0;
1785
- const requirementIds = frontendSourceBinding.requirementIds;
1743
+ const requirementIds = buildDagSourceBinding(sources).requirementIds;
1786
1744
  const requirementCoverageInstruction = requirementIds.length > 0
1787
1745
  ? `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.`
1788
1746
  : "";
@@ -1941,7 +1899,6 @@ function buildFrontendHybridDagFromTask(sources) {
1941
1899
  fixedVerificationContext,
1942
1900
  sourceContext,
1943
1901
  mockContextBlock,
1944
- frontendContractSchemaBlock,
1945
1902
  ].join("\n\n"),
1946
1903
  },
1947
1904
  {
@@ -2018,7 +1975,6 @@ function buildFrontendHybridDagFromTask(sources) {
2018
1975
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2019
1976
  "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.",
2020
1977
  sourceContext,
2021
- frontendContractSchemaBlock,
2022
1978
  ].join("\n\n"),
2023
1979
  },
2024
1980
  ...(requirementIds.length > 0
@@ -4356,22 +4312,45 @@ function buildReviewNode(sources) {
4356
4312
  ].join("\n\n"),
4357
4313
  };
4358
4314
  }
4315
+ function buildReviewVerdictRecoveryNode(sources) {
4316
+ return {
4317
+ id: "review-verdict-recovery-pi",
4318
+ depends_on: ["review-pi"],
4319
+ role: "reviewer",
4320
+ executor: "pi",
4321
+ complexity: "LOW",
4322
+ writePolicy: "read-only",
4323
+ allowedPaths: commonReadOnlyPaths(sources),
4324
+ forbiddenPaths: commonForbiddenPaths(sources),
4325
+ outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original review findings without substantive changes. No file writes.",
4326
+ subtask_prompt: [
4327
+ "Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol.",
4328
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
4329
+ "If review-pi already contains a valid VERDICT line, preserve that verdict exactly and keep the original findings.",
4330
+ "If it omitted or malformed the VERDICT line but states an unambiguous request-revision conclusion, emit VERDICT: request-revision and preserve the findings.",
4331
+ "Do not invent VERDICT: pass from natural-language phrases such as 通过、PASS、✅, or general approval prose.",
4332
+ "If the upstream conclusion is ambiguous or cannot be preserved safely, emit VERDICT: request-revision and report the format ambiguity.",
4333
+ "Do not re-review code, expand task allowedPaths, or edit files.",
4334
+ buildSourceContextBlock(sources),
4335
+ ].join("\n\n"),
4336
+ };
4337
+ }
4359
4338
  function buildReviewGateNode(sources) {
4360
4339
  return {
4361
4340
  id: "review-gate-shell",
4362
- depends_on: ["review-pi"],
4341
+ depends_on: ["review-verdict-recovery-pi"],
4363
4342
  role: "verifier",
4364
4343
  executor: "shell",
4365
4344
  complexity: "LOW",
4366
4345
  writePolicy: "read-only",
4367
4346
  allowedPaths: commonReadOnlyPaths(sources),
4368
4347
  forbiddenPaths: commonForbiddenPaths(sources),
4369
- outputContract: "Deterministic review verdict gate: exit 0 only when review-pi first non-empty assistant output line is pass.",
4370
- subtask_prompt: "Deterministic gate: block downstream closeout unless review-pi emitted VERDICT: pass.",
4348
+ outputContract: "Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi first VERDICT line is pass.",
4349
+ subtask_prompt: "Deterministic gate: block downstream closeout unless review-verdict-recovery-pi emitted VERDICT: pass.",
4371
4350
  shell: {
4372
4351
  commands: [],
4373
4352
  verdictGate: {
4374
- fromNodeId: "review-pi",
4353
+ fromNodeId: "review-verdict-recovery-pi",
4375
4354
  accept: ["VERDICT: pass"],
4376
4355
  label: "review",
4377
4356
  lineMode: "first-verdict-line",
@@ -4388,13 +4367,13 @@ function buildReviewGatedHybridDag(standard, sources) {
4388
4367
  objective: `${standard.objective ?? ""}\n\nRoute: review-gated DAG selected by workflowPolicy/governanceProfile.`.trim(),
4389
4368
  globalConstraints: [
4390
4369
  ...(standard.globalConstraints ?? []),
4391
- "Review-gated DAGs must block closeout unless review-pi emits first-line VERDICT: pass.",
4370
+ "Review-gated DAGs must block closeout unless review-verdict-recovery-pi emits first-line VERDICT: pass.",
4392
4371
  ],
4393
4372
  tasks: standard.tasks.map((task) => ({ ...task })),
4394
4373
  };
4395
4374
  const closeout = getTaskOrThrow(spec, "closeout-pi");
4396
4375
  replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
4397
- spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewGateNode(sources));
4376
+ spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
4398
4377
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
4399
4378
  applyDefaultReadOnlyRetryPolicy(spec);
4400
4379
  parseDagSpec(spec);
@@ -4778,6 +4757,7 @@ function buildSupervisedHybridDag(standard, sources) {
4778
4757
  cloneTask(buildReviewNode(sources), {
4779
4758
  depends_on: reviewDependsOn,
4780
4759
  }),
4760
+ buildReviewVerdictRecoveryNode(sources),
4781
4761
  buildReviewGateNode(sources),
4782
4762
  buildDecisionNode(sources),
4783
4763
  cloneTask(closeout, { depends_on: ["decision-pi"] }),
@@ -103,6 +103,7 @@ export async function curateKnowledgePatterns(input) {
103
103
  ok: true,
104
104
  patternsPath,
105
105
  patternCount: 0,
106
+ patterns: [],
106
107
  safetyFindings: [],
107
108
  message: "no patterns.jsonl found; no proposal generated",
108
109
  };
@@ -139,6 +140,7 @@ export async function curateKnowledgePatterns(input) {
139
140
  patternsPath,
140
141
  outputPath,
141
142
  patternCount: patterns.length,
143
+ patterns,
142
144
  proposalMarkdown,
143
145
  safetyFindings,
144
146
  message: "proposal failed skill safety audit",
@@ -153,6 +155,7 @@ export async function curateKnowledgePatterns(input) {
153
155
  patternsPath,
154
156
  outputPath,
155
157
  patternCount: patterns.length,
158
+ patterns,
156
159
  proposalMarkdown,
157
160
  safetyFindings,
158
161
  message: patterns.length === 0
@@ -3,29 +3,35 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
5
5
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
6
+ import { resolveContextPolicy } from "./context-policy.js";
6
7
  import { buildDagNodePromptEnvelope } from "./prompt.js";
7
8
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
8
9
  import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
9
10
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
10
11
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
11
12
  import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
12
- import { resolveDagNodeSkills } from "./skills.js";
13
13
  import { parseRepairArtifactFromText, resolveRepairTaskForGate, validateRepairArtifactScope, } from "./repair-artifact.js";
14
14
  import { resolveModelForTask, } from "./types.js";
15
15
  export function buildNodePrompt(spec, task, upstream) {
16
+ const policy = resolveContextPolicy(spec);
16
17
  return buildDagNodePromptEnvelope({
17
18
  spec,
18
19
  task,
19
20
  upstream,
20
- resolvedSkills: resolveDagNodeSkills(spec, task),
21
+ resolvedSkills: policy.resolveSkills(spec, task),
22
+ maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
21
23
  });
22
24
  }
23
25
  export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
24
- const skillNames = resolveDagNodeSkills(spec, task);
26
+ const policy = resolveContextPolicy(spec);
27
+ const skillNames = policy.resolveSkills(spec, task);
28
+ const budget = policy.resolveSkillInstructionBudget(task);
25
29
  const resolvedSkillInstructions = task.executor === "pi"
26
30
  ? await resolveDagSkillInstructions(skillNames, {
27
31
  cwd,
28
- includeLearnedPatterns: task.role === "implementer",
32
+ includeLearnedPatterns: budget.includeLearnedPatterns,
33
+ perSkillMaxChars: budget.perSkillMaxChars,
34
+ totalMaxChars: budget.totalMaxChars,
29
35
  })
30
36
  : [];
31
37
  return {
@@ -35,6 +41,7 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
35
41
  upstream,
36
42
  resolvedSkills: skillNames,
37
43
  resolvedSkillInstructions,
44
+ maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
38
45
  }),
39
46
  resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
40
47
  };
@@ -6,7 +6,7 @@ export const DAG_AUTHORING_GUIDANCE = [
6
6
  "Prefer same-rank parallel read-only scouts over unnecessary serial depends_on chains.",
7
7
  "Add depends_on only when a child truly needs upstream output; default to independent ranks.",
8
8
  "Every task must explicitly declare executor; defaults.executor is schema-only, not a runtime fallback.",
9
- "Cursor remains a first-class executor for scouts and exclusive implementers when it fits the task.",
9
+ "Pi is the only governed Agent DAG writer; cursor-prompt is an explicit manual one-shot sidecar and must not enter Loop auto-execute or Delegate writers.",
10
10
  "exclusive nodes require narrow, concrete, disjoint writeSet paths; never use ** or repo root.",
11
11
  "Read-only nodes must not write repository files, including root artifacts/**; return findings in node output only.",
12
12
  "If the DAG is a single linear chain, challenge whether read-only work can run in parallel ranks.",
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
3
+ import { assessDagRunLiveness, assessDagRunRecoveryEligibility, assertDagRunTransferTargetAvailable, getDagRunDir, locateDagRun, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
4
+ export function parseDagReconcileRunArgs(args) {
5
+ let runId;
6
+ let action;
7
+ let reason;
8
+ for (let index = 0; index < args.length; index += 1) {
9
+ const arg = args[index];
10
+ if (arg === "--run-id")
11
+ runId = args[++index];
12
+ else if (arg.startsWith("--run-id="))
13
+ runId = arg.slice("--run-id=".length);
14
+ else if (arg === "--action")
15
+ action = parseAction(args[++index]);
16
+ else if (arg.startsWith("--action="))
17
+ action = parseAction(arg.slice("--action=".length));
18
+ else if (arg === "--reason")
19
+ reason = args[++index];
20
+ else if (arg.startsWith("--reason="))
21
+ reason = arg.slice("--reason=".length);
22
+ else if (arg.startsWith("-"))
23
+ throw new Error(`unknown dag reconcile-run flag: ${arg}`);
24
+ else
25
+ throw new Error(`unexpected positional argument: ${arg}`);
26
+ }
27
+ if (!runId)
28
+ throw new Error("dag reconcile-run requires --run-id <id>");
29
+ if (action && !reason?.trim()) {
30
+ throw new Error("dag reconcile-run mutation requires --reason <text>");
31
+ }
32
+ return { runId, action, ...(reason?.trim() ? { reason: reason.trim() } : {}) };
33
+ }
34
+ function parseAction(value) {
35
+ if (value === "supersede" || value === "abandon")
36
+ return value;
37
+ throw new Error("dag reconcile-run --action must be supersede or abandon");
38
+ }
39
+ /**
40
+ * DAG-kernel reconcile for a single run. Lives in workflows so Worker outer-loop
41
+ * recovery can call it without importing the CLI/commands layer.
42
+ */
43
+ export async function executeDagReconcileRun(repoRoot, rawArgs) {
44
+ const parsed = parseDagReconcileRunArgs(rawArgs);
45
+ const located = await locateDagRun(repoRoot, parsed.runId);
46
+ if (!located)
47
+ throw new Error(`dag run not found: ${parsed.runId}`);
48
+ const state = await readDagRunState(located.runDir);
49
+ const liveness = assessDagRunLiveness({ state });
50
+ const eligibility = assessDagRunRecoveryEligibility({
51
+ lifecycle: located.lifecycle,
52
+ state,
53
+ liveness: liveness.status,
54
+ });
55
+ if (!parsed.action) {
56
+ return {
57
+ action: "inspect",
58
+ runId: state.runId,
59
+ lifecycle: located.lifecycle,
60
+ status: state.status,
61
+ liveness: liveness.status,
62
+ eligibility,
63
+ nextRecommendedAction: eligibility.canReconcile
64
+ ? "Re-run with --action supersede|abandon and --reason <text>."
65
+ : "Do not reconcile this run; inspect runner and lifecycle evidence first.",
66
+ };
67
+ }
68
+ if (!eligibility.canReconcile || !eligibility.allowedActions.includes(parsed.action)) {
69
+ throw new Error(`dag run ${state.runId} is not eligible for reconciliation: ${eligibility.reasons.join(", ") || "unknown reason"}`);
70
+ }
71
+ const reconciledAt = new Date().toISOString();
72
+ const completedRunDir = getDagRunDir(repoRoot, "completed", state.runId);
73
+ if (located.lifecycle !== "completed") {
74
+ await assertDagRunTransferTargetAvailable(completedRunDir);
75
+ }
76
+ const artifactRelativePath = path.join(".harness", "dag-runs", "completed", state.runId, "reconciliation.json").replace(/\\/g, "/");
77
+ await writeJsonAtomic(path.join(located.runDir, "reconciliation.json"), {
78
+ schemaVersion: 1,
79
+ runId: state.runId,
80
+ action: parsed.action,
81
+ reason: parsed.reason,
82
+ reconciledAt,
83
+ previousLifecycle: located.lifecycle,
84
+ previousStatus: state.status,
85
+ liveness: liveness.status,
86
+ originalState: state,
87
+ });
88
+ const previousStatus = state.status;
89
+ for (const node of Object.values(state.nodes)) {
90
+ if (node.status !== "RUNNING")
91
+ continue;
92
+ node.status = "ERROR";
93
+ node.finishedAt = reconciledAt;
94
+ node.failureCategory = `operator-${parsed.action}`;
95
+ }
96
+ state.status = parsed.action === "supersede" ? "superseded" : "abandoned";
97
+ state.finishedAt = reconciledAt;
98
+ state.failureCategory = `operator-${parsed.action}`;
99
+ state.reconciliation = {
100
+ action: parsed.action,
101
+ reason: parsed.reason,
102
+ reconciledAt,
103
+ previousStatus,
104
+ previousLifecycle: located.lifecycle,
105
+ liveness: liveness.status,
106
+ artifactPath: artifactRelativePath,
107
+ };
108
+ await writeDagRunState(located.runDir, state);
109
+ const runDir = located.lifecycle === "completed"
110
+ ? located.runDir
111
+ : await transferDagRunDir(located.runDir, completedRunDir);
112
+ return {
113
+ action: parsed.action,
114
+ runId: state.runId,
115
+ status: state.status,
116
+ lifecycle: "completed",
117
+ reason: parsed.reason,
118
+ reconciliationArtifactPath: artifactRelativePath,
119
+ runDir,
120
+ };
121
+ }
@@ -148,6 +148,9 @@ const dagNodeReportRowSchema = z
148
148
  finishedAt: z.string().optional(),
149
149
  decisionEnvelope: dagNodeDecisionEnvelopeSchema.optional(),
150
150
  artifacts: dagNodeArtifactsReportSchema,
151
+ structuredArtifactPath: z.string().min(1).optional(),
152
+ structuredArtifactSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
153
+ structuredArtifactSchemaId: z.string().min(1).optional(),
151
154
  })
152
155
  .strict();
153
156
  const dagRunReportEntrySchema = z
@@ -460,6 +463,15 @@ export async function buildDagRunReportEntry(input) {
460
463
  finishedAt: node.finishedAt,
461
464
  decisionEnvelope: node.decisionEnvelope,
462
465
  artifacts: await buildNodeArtifactsReport(input.runDir, nodeId),
466
+ ...(node.structuredArtifactPath
467
+ ? { structuredArtifactPath: node.structuredArtifactPath }
468
+ : {}),
469
+ ...(node.structuredArtifactSha256
470
+ ? { structuredArtifactSha256: node.structuredArtifactSha256 }
471
+ : {}),
472
+ ...(node.structuredArtifactSchemaId
473
+ ? { structuredArtifactSchemaId: node.structuredArtifactSchemaId }
474
+ : {}),
463
475
  });
464
476
  }
465
477
  }