@tea-agent/loop-agent 0.16.1-beta.2 → 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.
- package/AGENTS.md +4 -8
- package/CHANGELOG.md +55 -18
- package/README.md +76 -299
- package/dist/application/evaluation/alias.js +184 -0
- package/dist/application/evaluation/budget.js +192 -0
- package/dist/application/evaluation/campaign-hash.js +47 -0
- package/dist/application/evaluation/campaign-matrix.js +372 -0
- package/dist/application/evaluation/campaign-scorecard.js +135 -0
- package/dist/application/evaluation/campaign.js +370 -0
- package/dist/application/evaluation/candidate.js +23 -6
- package/dist/application/evaluation/corpus-hash.js +38 -0
- package/dist/application/evaluation/corpus.js +56 -0
- package/dist/application/evaluation/experiment.js +294 -0
- package/dist/application/evaluation/ignition.js +198 -0
- package/dist/application/evaluation/integrity-audit.js +162 -0
- package/dist/application/evaluation/outer-loop.js +132 -0
- package/dist/application/evaluation/pi-cell-executor.js +39 -0
- package/dist/application/evaluation/private-verifier.js +46 -0
- package/dist/application/evaluation/promotion-policy.js +151 -0
- package/dist/application/evaluation/proposer.js +98 -0
- package/dist/application/evaluation/types.js +522 -0
- package/dist/cli/command-definitions.js +19 -3
- package/dist/commands/dag-reconcile-run.js +3 -116
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +7 -1
- package/dist/executors/dag-pi-executor.js +4 -44
- package/dist/executors/pi-sdk-executor.js +3 -3
- package/dist/executors/shell-executor.js +1 -1
- package/dist/infrastructure/evaluation/alias-store.js +199 -0
- package/dist/infrastructure/evaluation/campaign-store.js +154 -0
- package/dist/infrastructure/evaluation/corpus-store.js +181 -0
- package/dist/infrastructure/evaluation/experiment-store.js +124 -0
- package/dist/infrastructure/evaluation/ignition-store.js +82 -0
- package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
- package/dist/infrastructure/evaluation/proposer-store.js +78 -0
- package/dist/records/promotion.js +3 -1
- package/dist/worker/cli.js +83 -0
- package/dist/worker/delivery/git-transaction.js +75 -0
- package/dist/worker/delivery/verification-bundle.js +13 -2
- package/dist/worker/feature/review.js +3 -2
- package/dist/worker/observe/static/dag-helpers.js +0 -62
- package/dist/worker/observe/static/styles.css +18 -55
- package/dist/worker/observe/static/views/dag.js +13 -5
- package/dist/worker/outcomes/adapters.js +4 -1
- package/dist/worker/outcomes/declared-artifacts.js +103 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +10 -11
- package/dist/worker/outcomes/projector.js +30 -4
- package/dist/worker/outcomes/types.js +3 -0
- package/dist/worker/pool/reconcile.js +285 -0
- package/dist/worker/run-task/run-task.js +81 -4
- package/dist/worker/runner/run-ready.js +25 -2
- package/dist/worker/task-graph/ready-planner.js +14 -8
- package/dist/worker/task-graph/task-graph-schema.js +5 -3
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/failure-routing.js +7 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +0 -77
- package/dist/workflows/dag/init-hybrid.js +33 -53
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/reconcile-run.js +121 -0
- package/dist/workflows/dag/report.js +12 -0
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +18 -1
- package/dist/workflows/dag/validate.js +15 -1
- package/docs/README.md +3 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/init-surface.manifest.json +4 -0
- package/docs/local-development-environment.md +52 -0
- package/docs/templates/agent-dag.schema.json +0 -5
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/branch-merge-report.md +14 -0
- package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
- package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
- package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
- package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
- package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
- package/docs/templates/product-line/AGENTS.md +1 -0
- package/docs/templates/product-line/README.md +17 -0
- package/docs/templates/product-line/acceptance.yaml +9 -0
- package/docs/templates/product-line/feature.yaml +11 -0
- package/docs/templates/product-line/task-graph.yaml +8 -0
- package/docs/templates/product-line/task.yaml +4 -0
- package/package.json +2 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/command-reference.md +5 -0
- 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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
"
|
|
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
|
}
|