@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.
- package/AGENTS.md +4 -8
- package/CHANGELOG.md +59 -16
- 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/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
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/pr
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { controllerIdentitiesMatch } from "../loop-agent/loop-agent-client.js";
|
|
6
|
+
import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outcomes/evidence-tokens.js";
|
|
6
7
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
7
8
|
import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
|
|
8
9
|
import { workflowSchema } from "../task-spec/workflow-routing.js";
|
|
@@ -30,7 +31,12 @@ const bundleOutcomeSchema = z
|
|
|
30
31
|
outcomePath: z.string().min(1),
|
|
31
32
|
outcomeSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
32
33
|
acceptanceCoverage: z.array(z.string().min(1)),
|
|
33
|
-
artifacts: z.array(z.object({
|
|
34
|
+
artifacts: z.array(z.object({
|
|
35
|
+
path: z.string().min(1),
|
|
36
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
37
|
+
kind: z.string().min(1).optional(),
|
|
38
|
+
schemaId: z.string().min(1).optional(),
|
|
39
|
+
}).strict()),
|
|
34
40
|
integrationStatus: z.object({ mock: z.boolean(), real: z.boolean() }).strict(),
|
|
35
41
|
outcomeStatus: z.enum(["succeeded", "failed"]),
|
|
36
42
|
controllerIdentity: z
|
|
@@ -243,7 +249,12 @@ function projectBundleAcceptance(item, backendTests, frontendTests, implementati
|
|
|
243
249
|
return { acId: item.id, status: "covered", priority: item.priority };
|
|
244
250
|
}
|
|
245
251
|
function evidenceKindPresent(entry, kind) {
|
|
246
|
-
|
|
252
|
+
if (isShellVerificationToken(kind)) {
|
|
253
|
+
// Bundle outcomes do not embed shellVerification; shell evidence is
|
|
254
|
+
// already implied by outcomeStatus === "succeeded" for typed tests.
|
|
255
|
+
return entry.outcomeStatus === "succeeded";
|
|
256
|
+
}
|
|
257
|
+
return entry.artifacts.some((artifact) => artifactMatchesEvidenceToken(artifact, kind));
|
|
247
258
|
}
|
|
248
259
|
function isTypedVerificationWorkflow(workflow) {
|
|
249
260
|
return TYPED_VERIFICATION_WORKFLOWS.includes(workflow);
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
7
|
+
import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outcomes/evidence-tokens.js";
|
|
7
8
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
8
9
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
9
10
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
@@ -591,9 +592,9 @@ export function projectDualCoverage(item, states, outcomes, failed, done, expect
|
|
|
591
592
|
return "awaiting-verification";
|
|
592
593
|
}
|
|
593
594
|
const requiredEvidence = item.verification.required_evidence ?? [];
|
|
594
|
-
if (requiredEvidence.some((
|
|
595
|
+
if (requiredEvidence.some((token) => !verEnvelopes.some(({ envelope }) => isShellVerificationToken(token)
|
|
595
596
|
? envelope.shellVerification?.exitZero === true
|
|
596
|
-
: envelope.artifacts?.some((artifact) => artifact
|
|
597
|
+
: envelope.artifacts?.some((artifact) => artifactMatchesEvidenceToken(artifact, token))))) {
|
|
597
598
|
return "awaiting-verification";
|
|
598
599
|
}
|
|
599
600
|
// 3. integration policy: real-required demands real evidence on every
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
import { UI_TEXT } from "./constants.js";
|
|
3
3
|
import { el } from "./dom.js";
|
|
4
4
|
import {
|
|
5
|
-
badge,
|
|
6
5
|
dagStatusBadge,
|
|
7
6
|
formatDuration,
|
|
8
7
|
formatAgo,
|
|
9
|
-
formatMs,
|
|
10
8
|
} from "./format.js";
|
|
11
9
|
import { navigate } from "./router.js";
|
|
12
10
|
|
|
@@ -139,63 +137,3 @@ export function getActiveNodes(dag) {
|
|
|
139
137
|
card.appendChild(link);
|
|
140
138
|
return card;
|
|
141
139
|
}
|
|
142
|
-
|
|
143
|
-
export function renderRankLanes(dag) {
|
|
144
|
-
const container = el("div", "rank-lanes");
|
|
145
|
-
const nodes = dag.nodes ?? [];
|
|
146
|
-
const ranks = dag.ranks;
|
|
147
|
-
const rankGroups = new Map();
|
|
148
|
-
|
|
149
|
-
if (ranks && ranks.length > 0) {
|
|
150
|
-
for (let i = 0; i < ranks.length; i++) {
|
|
151
|
-
rankGroups.set(String(i), ranks[i]);
|
|
152
|
-
}
|
|
153
|
-
for (const node of nodes) {
|
|
154
|
-
const rank = node.rank ?? "—";
|
|
155
|
-
if (!rankGroups.has(rank)) {
|
|
156
|
-
rankGroups.set(rank, [node.nodeId]);
|
|
157
|
-
} else if (!rankGroups.get(rank).includes(node.nodeId)) {
|
|
158
|
-
rankGroups.get(rank).push(node.nodeId);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
for (const node of nodes) {
|
|
163
|
-
const rank = node.rank ?? "—";
|
|
164
|
-
const list = rankGroups.get(rank) ?? [];
|
|
165
|
-
list.push(node.nodeId);
|
|
166
|
-
rankGroups.set(rank, list);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const sortedRanks = [...rankGroups.keys()].sort((a, b) => {
|
|
171
|
-
const na = Number(a);
|
|
172
|
-
const nb = Number(b);
|
|
173
|
-
if (!Number.isNaN(na) && !Number.isNaN(nb)) return na - nb;
|
|
174
|
-
return a.localeCompare(b);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const nodeById = Object.fromEntries(nodes.map((n) => [n.nodeId, n]));
|
|
178
|
-
|
|
179
|
-
for (const rank of sortedRanks) {
|
|
180
|
-
const lane = el("div", "rank-lane");
|
|
181
|
-
lane.appendChild(el("div", "rank-lane-label", `第 ${rank} 层`));
|
|
182
|
-
const chips = el("div", "rank-lane-nodes");
|
|
183
|
-
for (const nodeId of rankGroups.get(rank) ?? []) {
|
|
184
|
-
const node = nodeById[nodeId] ?? { nodeId };
|
|
185
|
-
const chip = el("div", "rank-node-chip");
|
|
186
|
-
if (isNodeActive(node.status)) chip.classList.add("rank-node-active");
|
|
187
|
-
if (isNodeFailed(node.status)) chip.classList.add("rank-node-error");
|
|
188
|
-
chip.appendChild(el("span", "rank-node-id", nodeId));
|
|
189
|
-
if (node.status) chip.appendChild(badge(node.status));
|
|
190
|
-
if (node.durationMs !== undefined) {
|
|
191
|
-
chip.appendChild(
|
|
192
|
-
el("span", "rank-node-dur", formatMs(node.durationMs)),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
chips.appendChild(chip);
|
|
196
|
-
}
|
|
197
|
-
lane.appendChild(chips);
|
|
198
|
-
container.appendChild(lane);
|
|
199
|
-
}
|
|
200
|
-
return container;
|
|
201
|
-
}
|
|
@@ -70,10 +70,10 @@ a {
|
|
|
70
70
|
code,
|
|
71
71
|
pre,
|
|
72
72
|
.output-block,
|
|
73
|
-
.rank-node-id,
|
|
74
73
|
.dag-node-label,
|
|
75
74
|
.dag-node-meta,
|
|
76
75
|
.dag-node-status,
|
|
76
|
+
.dag-node-table td:nth-child(2),
|
|
77
77
|
td:first-child,
|
|
78
78
|
.meta-item dd,
|
|
79
79
|
.header-refresh,
|
|
@@ -1462,35 +1462,43 @@ body.is-resizing-dag-graph {
|
|
|
1462
1462
|
}
|
|
1463
1463
|
.dag-node-table th:nth-child(1),
|
|
1464
1464
|
.dag-node-table td:nth-child(1) {
|
|
1465
|
-
width:
|
|
1465
|
+
width: 5%;
|
|
1466
1466
|
}
|
|
1467
1467
|
.dag-node-table th:nth-child(2),
|
|
1468
1468
|
.dag-node-table td:nth-child(2) {
|
|
1469
|
-
width:
|
|
1469
|
+
width: 17%;
|
|
1470
1470
|
}
|
|
1471
1471
|
.dag-node-table th:nth-child(3),
|
|
1472
1472
|
.dag-node-table td:nth-child(3) {
|
|
1473
|
-
width:
|
|
1473
|
+
width: 7%;
|
|
1474
1474
|
}
|
|
1475
1475
|
.dag-node-table th:nth-child(4),
|
|
1476
1476
|
.dag-node-table td:nth-child(4) {
|
|
1477
|
-
width:
|
|
1477
|
+
width: 11%;
|
|
1478
1478
|
}
|
|
1479
1479
|
.dag-node-table th:nth-child(5),
|
|
1480
1480
|
.dag-node-table td:nth-child(5) {
|
|
1481
|
-
width:
|
|
1481
|
+
width: 15%;
|
|
1482
1482
|
}
|
|
1483
1483
|
.dag-node-table th:nth-child(6),
|
|
1484
1484
|
.dag-node-table td:nth-child(6) {
|
|
1485
|
-
width:
|
|
1485
|
+
width: 11%;
|
|
1486
1486
|
}
|
|
1487
1487
|
.dag-node-table th:nth-child(7),
|
|
1488
1488
|
.dag-node-table td:nth-child(7) {
|
|
1489
|
+
width: 9%;
|
|
1490
|
+
}
|
|
1491
|
+
.dag-node-table th:nth-child(8),
|
|
1492
|
+
.dag-node-table td:nth-child(8) {
|
|
1489
1493
|
width: 25%;
|
|
1490
1494
|
}
|
|
1491
|
-
.dag-node-table
|
|
1492
|
-
.dag-node-table td:
|
|
1493
|
-
|
|
1495
|
+
.dag-node-table th:first-child,
|
|
1496
|
+
.dag-node-table td:first-child {
|
|
1497
|
+
text-align: center;
|
|
1498
|
+
}
|
|
1499
|
+
.dag-node-table td:nth-child(2),
|
|
1500
|
+
.dag-node-table td:nth-child(5),
|
|
1501
|
+
.dag-node-table td:nth-child(8) {
|
|
1494
1502
|
overflow-wrap: anywhere;
|
|
1495
1503
|
}
|
|
1496
1504
|
.dag-active-node {
|
|
@@ -1503,51 +1511,6 @@ body.is-resizing-dag-graph {
|
|
|
1503
1511
|
box-shadow: inset 3px 0 0 var(--orange);
|
|
1504
1512
|
}
|
|
1505
1513
|
|
|
1506
|
-
.rank-lanes {
|
|
1507
|
-
display: grid;
|
|
1508
|
-
gap: var(--space-3);
|
|
1509
|
-
}
|
|
1510
|
-
.rank-lane {
|
|
1511
|
-
display: grid;
|
|
1512
|
-
grid-template-columns: 56px minmax(0, 1fr);
|
|
1513
|
-
gap: var(--space-3);
|
|
1514
|
-
align-items: center;
|
|
1515
|
-
}
|
|
1516
|
-
.rank-lane-label {
|
|
1517
|
-
color: var(--muted);
|
|
1518
|
-
font-size: 11px;
|
|
1519
|
-
font-weight: 700;
|
|
1520
|
-
}
|
|
1521
|
-
.rank-lane-nodes {
|
|
1522
|
-
display: flex;
|
|
1523
|
-
flex-wrap: wrap;
|
|
1524
|
-
gap: 8px;
|
|
1525
|
-
}
|
|
1526
|
-
.rank-node-chip {
|
|
1527
|
-
display: inline-flex;
|
|
1528
|
-
align-items: center;
|
|
1529
|
-
gap: 7px;
|
|
1530
|
-
min-height: 32px;
|
|
1531
|
-
padding: 5px 8px;
|
|
1532
|
-
border: 1px solid var(--hairline);
|
|
1533
|
-
border-radius: var(--radius-sm);
|
|
1534
|
-
background: var(--canvas-soft);
|
|
1535
|
-
}
|
|
1536
|
-
.rank-node-active {
|
|
1537
|
-
border-color: var(--orange);
|
|
1538
|
-
}
|
|
1539
|
-
.rank-node-error {
|
|
1540
|
-
border-color: var(--red);
|
|
1541
|
-
}
|
|
1542
|
-
.rank-node-id {
|
|
1543
|
-
color: var(--ink);
|
|
1544
|
-
font-size: 11px;
|
|
1545
|
-
}
|
|
1546
|
-
.rank-node-dur {
|
|
1547
|
-
color: var(--muted);
|
|
1548
|
-
font-size: 11px;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
1514
|
.process-timeline-note {
|
|
1552
1515
|
margin: 0 0 var(--space-2);
|
|
1553
1516
|
color: var(--muted);
|
|
@@ -82,7 +82,6 @@ import {
|
|
|
82
82
|
isNodeFailed,
|
|
83
83
|
isNodeFinished,
|
|
84
84
|
isNodeActive,
|
|
85
|
-
renderRankLanes,
|
|
86
85
|
} from "../dag-helpers.js";
|
|
87
86
|
import { findBatchRunIdForTask } from "../run-processing.js";
|
|
88
87
|
import { renderDagGraph } from "./dag-graph.js";
|
|
@@ -275,7 +274,6 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
275
274
|
clearNode(ranksEl);
|
|
276
275
|
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
277
276
|
ranksEl.appendChild(renderDagGraph(dag, dagRunId, null));
|
|
278
|
-
ranksEl.appendChild(renderRankLanes(dag));
|
|
279
277
|
}
|
|
280
278
|
|
|
281
279
|
const nodesScrollLeft = nodesEl.scrollLeft;
|
|
@@ -298,7 +296,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
298
296
|
return a.nodeId.localeCompare(b.nodeId);
|
|
299
297
|
});
|
|
300
298
|
|
|
301
|
-
const rows = sortedNodes.map((node) => {
|
|
299
|
+
const rows = sortedNodes.map((node, index) => {
|
|
302
300
|
const active = isNodeActive(node.status);
|
|
303
301
|
const failed = isNodeFailed(node.status);
|
|
304
302
|
let className = "";
|
|
@@ -313,6 +311,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
313
311
|
className,
|
|
314
312
|
onClick: () => selectDagNode(dagRunId, node.nodeId),
|
|
315
313
|
cells: [
|
|
314
|
+
index + 1,
|
|
316
315
|
node.nodeId,
|
|
317
316
|
node.rank ?? "—",
|
|
318
317
|
node.executor ?? "—",
|
|
@@ -324,12 +323,21 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
324
323
|
};
|
|
325
324
|
});
|
|
326
325
|
const nodeTable = buildTable(
|
|
327
|
-
[
|
|
326
|
+
[
|
|
327
|
+
"序号",
|
|
328
|
+
"节点 ID",
|
|
329
|
+
"依赖层",
|
|
330
|
+
"执行方式",
|
|
331
|
+
"模型",
|
|
332
|
+
"状态",
|
|
333
|
+
"耗时",
|
|
334
|
+
"备注",
|
|
335
|
+
],
|
|
328
336
|
rows,
|
|
329
337
|
);
|
|
330
338
|
nodeTable.classList.add("dag-node-table");
|
|
331
339
|
for (const row of nodeTable.tBodies[0]?.rows ?? []) {
|
|
332
|
-
for (const columnIndex of [
|
|
340
|
+
for (const columnIndex of [1, 4, 7]) {
|
|
333
341
|
const cell = row.cells[columnIndex];
|
|
334
342
|
if (cell?.textContent) cell.title = cell.textContent;
|
|
335
343
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { splitStructuredArtifactIdentity } from "./evidence-tokens.js";
|
|
2
3
|
/**
|
|
3
4
|
* Shared artifact extraction. Every successful worker run produces a canonical
|
|
4
5
|
* `run_record` (the worker-run-record.json) and a `dag` (the generated DAG
|
|
@@ -43,10 +44,12 @@ function structuredArtifactsFromReport(run) {
|
|
|
43
44
|
if (node.structuredArtifactPath &&
|
|
44
45
|
node.structuredArtifactSha256 &&
|
|
45
46
|
node.structuredArtifactSchemaId) {
|
|
47
|
+
const identity = splitStructuredArtifactIdentity(node.structuredArtifactSchemaId);
|
|
46
48
|
artifacts.push({
|
|
47
49
|
path: node.structuredArtifactPath,
|
|
48
50
|
sha256: node.structuredArtifactSha256,
|
|
49
|
-
kind:
|
|
51
|
+
kind: identity.kind,
|
|
52
|
+
schemaId: identity.schemaId,
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Identity key for domain artifact uniqueness within one Outcome. */
|
|
2
|
+
export function artifactIdentityKey(kind, schemaId) {
|
|
3
|
+
return `${kind}::${schemaId ?? ""}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Minimal glob matcher aligned with Git write-boundary audit.
|
|
7
|
+
* Supports `*` (one path segment) and `**` (any depth).
|
|
8
|
+
*/
|
|
9
|
+
export function matchesPathGlob(filePath, glob) {
|
|
10
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
11
|
+
const normalizedGlob = glob.replace(/\\/g, "/");
|
|
12
|
+
let pattern = "^";
|
|
13
|
+
for (let index = 0; index < normalizedGlob.length; index += 1) {
|
|
14
|
+
const char = normalizedGlob[index];
|
|
15
|
+
if (char === "*" && normalizedGlob[index + 1] === "*") {
|
|
16
|
+
pattern += ".*";
|
|
17
|
+
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === "*") {
|
|
21
|
+
pattern += "[^/]*";
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
pattern += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
}
|
|
26
|
+
return new RegExp(`${pattern}$`).test(normalizedPath);
|
|
27
|
+
}
|
|
28
|
+
export function isPathAllowedForTask(filePath, allowedPaths, forbiddenPaths) {
|
|
29
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
30
|
+
if (normalized.startsWith("/") || normalized.includes("..")) {
|
|
31
|
+
return { ok: false, reason: `artifact path escapes repo or uses traversal: ${filePath}` };
|
|
32
|
+
}
|
|
33
|
+
if (forbiddenPaths.some((glob) => matchesPathGlob(normalized, glob))) {
|
|
34
|
+
return { ok: false, reason: `artifact path is forbidden: ${filePath}` };
|
|
35
|
+
}
|
|
36
|
+
if (allowedPaths.length > 0 &&
|
|
37
|
+
!allowedPaths.some((glob) => matchesPathGlob(normalized, glob))) {
|
|
38
|
+
return { ok: false, reason: `artifact path is outside allowed_paths: ${filePath}` };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project path-bound TaskGraph `produces[]` into Outcome artifact claims.
|
|
44
|
+
* Entries without `path` are skipped (structured DAG artifacts may satisfy them).
|
|
45
|
+
* Empty sha256 is filled by the projector after on-disk hash recomputation.
|
|
46
|
+
*/
|
|
47
|
+
export function projectDeclaredPathArtifacts(declared) {
|
|
48
|
+
if (!declared || declared.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
const artifacts = [];
|
|
51
|
+
for (const entry of declared) {
|
|
52
|
+
if (!entry.path)
|
|
53
|
+
continue;
|
|
54
|
+
artifacts.push({
|
|
55
|
+
path: entry.path.replace(/\\/g, "/"),
|
|
56
|
+
sha256: "",
|
|
57
|
+
kind: entry.kind,
|
|
58
|
+
...(entry.schemaId ? { schemaId: entry.schemaId } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return artifacts;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge adapter artifacts with declared path bindings.
|
|
65
|
+
* Same `{kind,schemaId}` must resolve to one path; conflicts fail closed.
|
|
66
|
+
*/
|
|
67
|
+
export function mergeOutcomeArtifacts(input) {
|
|
68
|
+
const byIdentity = new Map();
|
|
69
|
+
const withoutIdentity = [];
|
|
70
|
+
const ingest = (artifact) => {
|
|
71
|
+
if (!artifact.kind) {
|
|
72
|
+
withoutIdentity.push(artifact);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const key = artifactIdentityKey(artifact.kind, artifact.schemaId);
|
|
76
|
+
const existing = byIdentity.get(key);
|
|
77
|
+
if (!existing) {
|
|
78
|
+
byIdentity.set(key, artifact);
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const existingPath = existing.path.replace(/\\/g, "/");
|
|
82
|
+
const nextPath = artifact.path.replace(/\\/g, "/");
|
|
83
|
+
if (existingPath !== nextPath) {
|
|
84
|
+
return `duplicate artifact identity ${key} with conflicting paths: ${existingPath} vs ${nextPath}`;
|
|
85
|
+
}
|
|
86
|
+
// Prefer the entry that already carries a declared sha256.
|
|
87
|
+
if (!existing.sha256 && artifact.sha256) {
|
|
88
|
+
byIdentity.set(key, artifact);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
for (const artifact of input.base) {
|
|
93
|
+
const error = ingest(artifact);
|
|
94
|
+
if (error)
|
|
95
|
+
return { ok: false, reason: error };
|
|
96
|
+
}
|
|
97
|
+
for (const artifact of input.declared) {
|
|
98
|
+
const error = ingest(artifact);
|
|
99
|
+
if (error)
|
|
100
|
+
return { ok: false, reason: error };
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, artifacts: [...withoutIdentity, ...byIdentity.values()] };
|
|
103
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared evidence / artifact identity helpers for Outcome gates, Feature review,
|
|
3
|
+
* and Feature Verification Bundle projection.
|
|
4
|
+
*/
|
|
5
|
+
/** Canonical required-output / required_evidence token for shell exit-zero. */
|
|
6
|
+
export const SHELL_VERIFICATION_TOKEN = "shell_verification";
|
|
7
|
+
/**
|
|
8
|
+
* Accept the canonical underscore form and the legacy hyphen form used in early
|
|
9
|
+
* fullstack packets so dual-coverage and bundle evidence stay aligned.
|
|
10
|
+
*/
|
|
11
|
+
export function isShellVerificationToken(token) {
|
|
12
|
+
return token === SHELL_VERIFICATION_TOKEN || token === "shell-verification";
|
|
13
|
+
}
|
|
14
|
+
/** Match an evidence token against artifact `kind` or `schemaId`. */
|
|
15
|
+
export function artifactMatchesEvidenceToken(artifact, token) {
|
|
16
|
+
return artifact.kind === token || artifact.schemaId === token;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Derive stable `kind` + versioned `schemaId` from a DAG structured-artifact
|
|
20
|
+
* schema id (e.g. `backend-test-result-v1` → kind `backend-test-result`).
|
|
21
|
+
* Unversioned ids keep kind === schemaId.
|
|
22
|
+
*/
|
|
23
|
+
export function splitStructuredArtifactIdentity(schemaId) {
|
|
24
|
+
const match = /^(.*)-v\d+$/.exec(schemaId);
|
|
25
|
+
if (match?.[1]) {
|
|
26
|
+
return { kind: match[1], schemaId };
|
|
27
|
+
}
|
|
28
|
+
return { kind: schemaId, schemaId };
|
|
29
|
+
}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
+
import { isShellVerificationToken, } from "./evidence-tokens.js";
|
|
2
|
+
export { artifactMatchesEvidenceToken, isShellVerificationToken, } from "./evidence-tokens.js";
|
|
1
3
|
/**
|
|
2
4
|
* Deterministic required-output gate.
|
|
3
5
|
*
|
|
4
6
|
* The gate runs *after* a successful DAG report. It performs a **literal
|
|
5
|
-
* string match** of each `outputs.required` token against
|
|
6
|
-
* artifact
|
|
7
|
-
* required token with no matching produced artifact fails closed — the run is
|
|
8
|
-
* downgraded from `succeeded` to `failed` with `ContractMismatch`.
|
|
7
|
+
* string match** of each known `outputs.required` token against produced
|
|
8
|
+
* artifact kinds (and the shell-verification exit-zero fact).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* required output.
|
|
15
|
-
* - An empty `required` list always passes (nothing to enforce).
|
|
10
|
+
* Known tokens (`run_record`, `dag_json`, `shell_verification` / legacy
|
|
11
|
+
* `shell-verification`) fail closed when absent. Unknown legacy tokens are
|
|
12
|
+
* intentionally ignored for compatibility — they never invent a mapping and
|
|
13
|
+
* never block promotion. An empty `required` list always passes.
|
|
16
14
|
*
|
|
17
15
|
* This gate is the success-criteria guardrail: "DAG report succeeded but a
|
|
18
16
|
* known required output is missing ⇒ the Task must not be marked Done."
|
|
@@ -26,7 +24,7 @@ export function checkRequiredOutputs(envelope, requiredOutputs) {
|
|
|
26
24
|
.filter((value) => Boolean(value)));
|
|
27
25
|
const missing = [];
|
|
28
26
|
for (const token of requiredOutputs) {
|
|
29
|
-
if (token
|
|
27
|
+
if (isShellVerificationToken(token)) {
|
|
30
28
|
if (!envelope.shellVerification?.exitZero)
|
|
31
29
|
missing.push(token);
|
|
32
30
|
continue;
|
|
@@ -34,6 +32,7 @@ export function checkRequiredOutputs(envelope, requiredOutputs) {
|
|
|
34
32
|
if (KNOWN_ARTIFACT_OUTPUTS.has(token) && !producedKinds.has(token)) {
|
|
35
33
|
missing.push(token);
|
|
36
34
|
}
|
|
35
|
+
// Unknown tokens: intentionally ignored (legacy compatibility).
|
|
37
36
|
}
|
|
38
37
|
return { passed: missing.length === 0, missing };
|
|
39
38
|
}
|
|
@@ -3,6 +3,7 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { sha256File } from "../pool/run-store.js";
|
|
4
4
|
import { taskOutcomeEnvelopeV1Schema } from "./types.js";
|
|
5
5
|
import { getOutcomeAdapter } from "./adapters.js";
|
|
6
|
+
import { isPathAllowedForTask, mergeOutcomeArtifacts, projectDeclaredPathArtifacts, } from "./declared-artifacts.js";
|
|
6
7
|
/**
|
|
7
8
|
* Deterministic path-containment guard. Mirrors run-task's `isPathInside`: a
|
|
8
9
|
* path must resolve *inside* the repo root and must not escape it. Absolute
|
|
@@ -66,11 +67,27 @@ export async function projectOutcome(input) {
|
|
|
66
67
|
if (!adapter) {
|
|
67
68
|
return contractError(`no outcome adapter for workflow "${workflow}"`);
|
|
68
69
|
}
|
|
69
|
-
// 3. Adapter projection (pure; no FS).
|
|
70
|
+
// 3. Adapter projection (pure; no FS) + frozen graph `produces.path` bindings.
|
|
70
71
|
const adapterProjection = adapter.project(input);
|
|
72
|
+
const declaredPathArtifacts = projectDeclaredPathArtifacts(input.declaredArtifacts);
|
|
73
|
+
const allowedPaths = input.taskSpec.constraints?.allowed_paths ?? [];
|
|
74
|
+
const forbiddenPaths = input.taskSpec.constraints?.forbidden_paths ?? [];
|
|
75
|
+
for (const artifact of declaredPathArtifacts) {
|
|
76
|
+
const containment = isPathAllowedForTask(artifact.path, allowedPaths, forbiddenPaths);
|
|
77
|
+
if (!containment.ok) {
|
|
78
|
+
return contractError(containment.reason);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const merged = mergeOutcomeArtifacts({
|
|
82
|
+
base: adapterProjection.artifacts,
|
|
83
|
+
declared: declaredPathArtifacts,
|
|
84
|
+
});
|
|
85
|
+
if (!merged.ok) {
|
|
86
|
+
return contractError(merged.reason);
|
|
87
|
+
}
|
|
71
88
|
// 4. Artifact path + hash validation.
|
|
72
89
|
const validatedArtifacts = [];
|
|
73
|
-
for (const artifact of
|
|
90
|
+
for (const artifact of merged.artifacts) {
|
|
74
91
|
if (!isRepoRelativePath(input.repoRoot, artifact.path)) {
|
|
75
92
|
return contractError(`artifact path is not repo-relative: ${artifact.path}`);
|
|
76
93
|
}
|
|
@@ -94,9 +111,10 @@ export async function projectOutcome(input) {
|
|
|
94
111
|
return contractError(`artifact sha256 mismatch for ${artifact.path}: declared=${artifact.sha256} actual=${recomputed}`);
|
|
95
112
|
}
|
|
96
113
|
validatedArtifacts.push({
|
|
97
|
-
path: artifact.path,
|
|
114
|
+
path: artifact.path.replace(/\\/g, "/"),
|
|
98
115
|
sha256: recomputed,
|
|
99
116
|
...(artifact.kind ? { kind: artifact.kind } : {}),
|
|
117
|
+
...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
|
|
100
118
|
});
|
|
101
119
|
}
|
|
102
120
|
// 5. Shell verification from report decision (exit-zero = report succeeded).
|
|
@@ -113,6 +131,13 @@ export async function projectOutcome(input) {
|
|
|
113
131
|
}
|
|
114
132
|
: undefined;
|
|
115
133
|
const sourceBinding = input.sourceBinding ?? deriveSourceBinding(input);
|
|
134
|
+
const outcomeStatus = input.reportDecision.succeeded ? "succeeded" : "failed";
|
|
135
|
+
const outcomeFailure = outcomeStatus === "failed"
|
|
136
|
+
? {
|
|
137
|
+
category: "EnvFailure",
|
|
138
|
+
reason: input.reportDecision.reason || "DAG report did not succeed",
|
|
139
|
+
}
|
|
140
|
+
: undefined;
|
|
116
141
|
const envelope = {
|
|
117
142
|
schemaVersion: 1,
|
|
118
143
|
identity: {
|
|
@@ -131,7 +156,8 @@ export async function projectOutcome(input) {
|
|
|
131
156
|
integrationStatus: adapterProjection.integrationStatus,
|
|
132
157
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
133
158
|
createdAt: input.now.toISOString(),
|
|
134
|
-
outcomeStatus
|
|
159
|
+
outcomeStatus,
|
|
160
|
+
...(outcomeFailure ? { outcomeFailure } : {}),
|
|
135
161
|
};
|
|
136
162
|
// 6. Strict schema parse.
|
|
137
163
|
const parsed = taskOutcomeEnvelopeV1Schema.safeParse(envelope);
|
|
@@ -30,7 +30,10 @@ export const OUTCOME_WORKFLOWS = [
|
|
|
30
30
|
export const outcomeArtifactSchema = z.object({
|
|
31
31
|
path: z.string().min(1),
|
|
32
32
|
sha256: z.string().regex(/^[a-f0-9]{64}$/, "sha256 must be lowercase hex"),
|
|
33
|
+
/** Stable artifact family (e.g. `backend-test-result`). */
|
|
33
34
|
kind: z.string().min(1).optional(),
|
|
35
|
+
/** Versioned schema id (e.g. `backend-test-result-v1`); orthogonal to kind. */
|
|
36
|
+
schemaId: z.string().min(1).optional(),
|
|
34
37
|
});
|
|
35
38
|
export const shellVerificationSchema = z.object({
|
|
36
39
|
exitZero: z.boolean(),
|