@tea-agent/loop-agent 0.16.0 → 0.16.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +3 -55
- package/README.md +1 -1
- package/dist/application/evaluation/candidate.js +6 -23
- package/dist/application/evaluation/types.js +0 -522
- package/dist/cli/command-definitions.js +3 -19
- package/dist/commands/eval.js +13 -1176
- package/dist/commands/init.js +1 -4
- package/dist/executors/dag-pi-executor.js +44 -4
- package/dist/worker/delivery/verification-bundle.js +2 -13
- package/dist/worker/feature/review.js +2 -3
- package/dist/worker/outcomes/adapters.js +1 -4
- package/dist/worker/outcomes/gate.js +11 -10
- package/dist/worker/outcomes/projector.js +1 -10
- package/dist/worker/outcomes/types.js +0 -3
- package/dist/worker/run-task/run-task.js +4 -23
- package/dist/worker/task-graph/ready-planner.js +8 -14
- package/dist/worker/task-graph/task-graph-schema.js +3 -5
- package/dist/workflows/dag/init-hybrid.js +3 -2
- package/dist/workflows/dag/knowledge-curator.js +0 -3
- package/dist/workflows/dag/node-execution.js +4 -11
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/runner.js +16 -43
- package/dist/workflows/dag/skill-snapshot.js +7 -11
- package/dist/workflows/dag/types.js +1 -18
- package/docs/init-surface.manifest.json +0 -3
- package/docs/templates/agent-dag.schema.json +5 -0
- package/docs/templates/product-line/AGENTS.md +0 -1
- package/docs/templates/product-line/README.md +0 -17
- package/docs/templates/product-line/acceptance.yaml +0 -9
- package/docs/templates/product-line/task-graph.yaml +0 -8
- package/docs/templates/product-line/task.yaml +0 -4
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/application/evaluation/alias.js +0 -184
- package/dist/application/evaluation/budget.js +0 -192
- package/dist/application/evaluation/campaign-hash.js +0 -47
- package/dist/application/evaluation/campaign-matrix.js +0 -372
- package/dist/application/evaluation/campaign-scorecard.js +0 -135
- package/dist/application/evaluation/campaign.js +0 -370
- package/dist/application/evaluation/corpus-hash.js +0 -38
- package/dist/application/evaluation/corpus.js +0 -56
- package/dist/application/evaluation/experiment.js +0 -294
- package/dist/application/evaluation/ignition.js +0 -198
- package/dist/application/evaluation/integrity-audit.js +0 -162
- package/dist/application/evaluation/outer-loop.js +0 -132
- package/dist/application/evaluation/pi-cell-executor.js +0 -39
- package/dist/application/evaluation/private-verifier.js +0 -46
- package/dist/application/evaluation/promotion-policy.js +0 -151
- package/dist/application/evaluation/proposer.js +0 -98
- package/dist/infrastructure/evaluation/alias-store.js +0 -199
- package/dist/infrastructure/evaluation/campaign-store.js +0 -154
- package/dist/infrastructure/evaluation/corpus-store.js +0 -181
- package/dist/infrastructure/evaluation/experiment-store.js +0 -124
- package/dist/infrastructure/evaluation/ignition-store.js +0 -82
- package/dist/infrastructure/evaluation/private-verifier-store.js +0 -145
- package/dist/infrastructure/evaluation/proposer-store.js +0 -78
- package/dist/worker/outcomes/evidence-tokens.js +0 -29
- package/dist/workflows/dag/budget-enforcement.js +0 -67
- package/dist/workflows/dag/context-policy.js +0 -137
- package/docs/templates/evaluation/campaign-budget-v1.json +0 -12
- package/docs/templates/evaluation/campaign-dogfood-v0.json +0 -24
- package/docs/templates/evaluation/campaign-evidence-v1.json +0 -44
- package/docs/templates/evaluation/context-policy-baseline-v1.json +0 -17
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +0 -28
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +0 -118
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +0 -21
- package/docs/templates/evaluation/matrix-fixture-v1.json +0 -10
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +0 -16
- package/docs/templates/product-line/feature.yaml +0 -11
package/dist/commands/init.js
CHANGED
|
@@ -859,9 +859,6 @@ export function buildManagedGitignoreBlock() {
|
|
|
859
859
|
".harness/*.log",
|
|
860
860
|
".harness/init-surface.json",
|
|
861
861
|
"",
|
|
862
|
-
"# Eval Lab runtime (candidates, campaigns, scorecards, dogfood scratch)",
|
|
863
|
-
".harness/evaluation/",
|
|
864
|
-
"",
|
|
865
862
|
"# worker task pool",
|
|
866
863
|
".harness/task-pool/*",
|
|
867
864
|
".task-pool/",
|
|
@@ -2092,7 +2089,7 @@ export function buildInitInstructions(input) {
|
|
|
2092
2089
|
"- Enrich the root `README.md`: keep the deterministic project title and the loop-agent managed block intact, and fill the human-authored sections (项目概览, 技术栈与目录结构, 开发与验证) from the target project's actual files. The root README must serve both as a human-first project entry and as an agent work entry; replace the initialization-model supplement comments when the project files provide the information.",
|
|
2093
2090
|
`- Populate \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands derived from its real language and toolchain, keeping the governance rows intact.`,
|
|
2094
2091
|
"- Project repo-local skills live in `.agents/skills/`. Do not create a root `skills/` directory in the target project; the package's bundled `skills/` remains the built-in fallback.",
|
|
2095
|
-
"- Merge a loop-agent managed block into `.gitignore` that ignores harness runtime facts (tasks, dag-runs, runs,
|
|
2092
|
+
"- Merge a loop-agent managed block into `.gitignore` that ignores harness runtime facts (tasks, dag-runs, runs, live, cache, init-surface.json, .harness/task-pool, legacy .task-pool residue) while keeping prompts and directory placeholders shareable.",
|
|
2096
2093
|
"- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
|
|
2097
2094
|
"- Add or update a loop-agent managed block in AGENTS.md.",
|
|
2098
2095
|
"- The generated AGENTS.md must include documentation convergence and structured DAG write-boundary rules so target projects keep the same working discipline as this repository.",
|
|
@@ -272,7 +272,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
272
272
|
persona,
|
|
273
273
|
step,
|
|
274
274
|
});
|
|
275
|
-
const mapped = mapPiResultToDagNodeResult(result);
|
|
275
|
+
const mapped = mapPiResultToDagNodeResult(result, input.task.firstProtocolLine);
|
|
276
276
|
if (!isWriteTask) {
|
|
277
277
|
return mapped;
|
|
278
278
|
}
|
|
@@ -313,17 +313,57 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
313
313
|
durationMs: mapped.durationMs || Date.now() - started,
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
-
export function mapPiResultToDagNodeResult(result) {
|
|
316
|
+
export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
|
|
317
|
+
const assistantText = canonicalizeProtocolFirstLine(result.assistantText, firstProtocolLine);
|
|
317
318
|
return {
|
|
318
319
|
ok: result.ok,
|
|
319
|
-
stdout:
|
|
320
|
+
stdout: assistantText || result.stdout,
|
|
320
321
|
stderr: result.stderr,
|
|
321
322
|
failureCategory: result.failureCategory,
|
|
322
323
|
durationMs: result.durationMs,
|
|
323
|
-
assistantText
|
|
324
|
+
assistantText,
|
|
324
325
|
backend: result.backend,
|
|
325
326
|
sdkAttempted: result.sdkAttempted,
|
|
326
327
|
tokensUsed: result.tokensUsed,
|
|
327
328
|
parsedEvents: result.parsedEvents,
|
|
328
329
|
};
|
|
329
330
|
}
|
|
331
|
+
function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
332
|
+
if (!assistantText || !firstProtocolLine)
|
|
333
|
+
return assistantText;
|
|
334
|
+
const lines = assistantText.split(/\r?\n/);
|
|
335
|
+
let protocolIndex = -1;
|
|
336
|
+
let protocolLine = "";
|
|
337
|
+
for (const [index, line] of lines.entries()) {
|
|
338
|
+
const normalized = normalizeProtocolLine(line);
|
|
339
|
+
if (normalized.startsWith(firstProtocolLine)) {
|
|
340
|
+
protocolIndex = index;
|
|
341
|
+
protocolLine = normalized;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (protocolIndex < 0)
|
|
346
|
+
return assistantText;
|
|
347
|
+
if (protocolIndex === 0) {
|
|
348
|
+
return [protocolLine, ...lines.slice(1)].join("\n");
|
|
349
|
+
}
|
|
350
|
+
const before = lines.slice(0, protocolIndex);
|
|
351
|
+
const after = lines.slice(protocolIndex + 1);
|
|
352
|
+
while (before.at(-1)?.trim() === "" &&
|
|
353
|
+
after.at(0)?.trim() === "") {
|
|
354
|
+
after.shift();
|
|
355
|
+
}
|
|
356
|
+
const bodyLines = [...before, ...after];
|
|
357
|
+
while (bodyLines.at(0)?.trim() === "")
|
|
358
|
+
bodyLines.shift();
|
|
359
|
+
while (bodyLines.at(-1)?.trim() === "")
|
|
360
|
+
bodyLines.pop();
|
|
361
|
+
return bodyLines.length > 0
|
|
362
|
+
? `${protocolLine}\n\n${bodyLines.join("\n")}`
|
|
363
|
+
: protocolLine;
|
|
364
|
+
}
|
|
365
|
+
function normalizeProtocolLine(line) {
|
|
366
|
+
const trimmed = line.trim();
|
|
367
|
+
const emphasized = trimmed.match(/^(\*{1,3})\s*(.*?)\s*\1$/);
|
|
368
|
+
return (emphasized?.[2] ?? trimmed).trim();
|
|
369
|
+
}
|
|
@@ -3,7 +3,6 @@ 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";
|
|
7
6
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
8
7
|
import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
|
|
9
8
|
import { workflowSchema } from "../task-spec/workflow-routing.js";
|
|
@@ -31,12 +30,7 @@ const bundleOutcomeSchema = z
|
|
|
31
30
|
outcomePath: z.string().min(1),
|
|
32
31
|
outcomeSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
33
32
|
acceptanceCoverage: z.array(z.string().min(1)),
|
|
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()),
|
|
33
|
+
artifacts: z.array(z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), kind: z.string().min(1).optional() }).strict()),
|
|
40
34
|
integrationStatus: z.object({ mock: z.boolean(), real: z.boolean() }).strict(),
|
|
41
35
|
outcomeStatus: z.enum(["succeeded", "failed"]),
|
|
42
36
|
controllerIdentity: z
|
|
@@ -249,12 +243,7 @@ function projectBundleAcceptance(item, backendTests, frontendTests, implementati
|
|
|
249
243
|
return { acId: item.id, status: "covered", priority: item.priority };
|
|
250
244
|
}
|
|
251
245
|
function evidenceKindPresent(entry, kind) {
|
|
252
|
-
|
|
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));
|
|
246
|
+
return entry.artifacts.some((artifact) => artifact.kind === kind);
|
|
258
247
|
}
|
|
259
248
|
function isTypedVerificationWorkflow(workflow) {
|
|
260
249
|
return TYPED_VERIFICATION_WORKFLOWS.includes(workflow);
|
|
@@ -4,7 +4,6 @@ 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";
|
|
8
7
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
9
8
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
10
9
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
@@ -592,9 +591,9 @@ export function projectDualCoverage(item, states, outcomes, failed, done, expect
|
|
|
592
591
|
return "awaiting-verification";
|
|
593
592
|
}
|
|
594
593
|
const requiredEvidence = item.verification.required_evidence ?? [];
|
|
595
|
-
if (requiredEvidence.some((
|
|
594
|
+
if (requiredEvidence.some((kind) => !verEnvelopes.some(({ envelope }) => kind === "shell-verification"
|
|
596
595
|
? envelope.shellVerification?.exitZero === true
|
|
597
|
-
: envelope.artifacts?.some((artifact) =>
|
|
596
|
+
: envelope.artifacts?.some((artifact) => artifact.kind === kind)))) {
|
|
598
597
|
return "awaiting-verification";
|
|
599
598
|
}
|
|
600
599
|
// 3. integration policy: real-required demands real evidence on every
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { splitStructuredArtifactIdentity } from "./evidence-tokens.js";
|
|
3
2
|
/**
|
|
4
3
|
* Shared artifact extraction. Every successful worker run produces a canonical
|
|
5
4
|
* `run_record` (the worker-run-record.json) and a `dag` (the generated DAG
|
|
@@ -44,12 +43,10 @@ function structuredArtifactsFromReport(run) {
|
|
|
44
43
|
if (node.structuredArtifactPath &&
|
|
45
44
|
node.structuredArtifactSha256 &&
|
|
46
45
|
node.structuredArtifactSchemaId) {
|
|
47
|
-
const identity = splitStructuredArtifactIdentity(node.structuredArtifactSchemaId);
|
|
48
46
|
artifacts.push({
|
|
49
47
|
path: node.structuredArtifactPath,
|
|
50
48
|
sha256: node.structuredArtifactSha256,
|
|
51
|
-
kind:
|
|
52
|
-
schemaId: identity.schemaId,
|
|
49
|
+
kind: node.structuredArtifactSchemaId,
|
|
53
50
|
});
|
|
54
51
|
}
|
|
55
52
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { isShellVerificationToken, } from "./evidence-tokens.js";
|
|
2
|
-
export { artifactMatchesEvidenceToken, isShellVerificationToken, } from "./evidence-tokens.js";
|
|
3
1
|
/**
|
|
4
2
|
* Deterministic required-output gate.
|
|
5
3
|
*
|
|
6
4
|
* The gate runs *after* a successful DAG report. It performs a **literal
|
|
7
|
-
* string match** of each
|
|
8
|
-
* artifact
|
|
5
|
+
* string match** of each `outputs.required` token against the `kind` of every
|
|
6
|
+
* artifact the adapter produced (and the run-record/dag canonical kinds). A
|
|
7
|
+
* required token with no matching produced artifact fails closed — the run is
|
|
8
|
+
* downgraded from `succeeded` to `failed` with `ContractMismatch`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* never
|
|
10
|
+
* - No regex / substring matching: a token matches only when an artifact
|
|
11
|
+
* `kind` equals it exactly.
|
|
12
|
+
* - Unknown tokens (no adapter mapping) are simply unmatched → missing. The
|
|
13
|
+
* gate never invents a mapping and never passes a run missing a known
|
|
14
|
+
* required output.
|
|
15
|
+
* - An empty `required` list always passes (nothing to enforce).
|
|
14
16
|
*
|
|
15
17
|
* This gate is the success-criteria guardrail: "DAG report succeeded but a
|
|
16
18
|
* known required output is missing ⇒ the Task must not be marked Done."
|
|
@@ -24,7 +26,7 @@ export function checkRequiredOutputs(envelope, requiredOutputs) {
|
|
|
24
26
|
.filter((value) => Boolean(value)));
|
|
25
27
|
const missing = [];
|
|
26
28
|
for (const token of requiredOutputs) {
|
|
27
|
-
if (
|
|
29
|
+
if (token === "shell_verification") {
|
|
28
30
|
if (!envelope.shellVerification?.exitZero)
|
|
29
31
|
missing.push(token);
|
|
30
32
|
continue;
|
|
@@ -32,7 +34,6 @@ export function checkRequiredOutputs(envelope, requiredOutputs) {
|
|
|
32
34
|
if (KNOWN_ARTIFACT_OUTPUTS.has(token) && !producedKinds.has(token)) {
|
|
33
35
|
missing.push(token);
|
|
34
36
|
}
|
|
35
|
-
// Unknown tokens: intentionally ignored (legacy compatibility).
|
|
36
37
|
}
|
|
37
38
|
return { passed: missing.length === 0, missing };
|
|
38
39
|
}
|
|
@@ -97,7 +97,6 @@ export async function projectOutcome(input) {
|
|
|
97
97
|
path: artifact.path,
|
|
98
98
|
sha256: recomputed,
|
|
99
99
|
...(artifact.kind ? { kind: artifact.kind } : {}),
|
|
100
|
-
...(artifact.schemaId ? { schemaId: artifact.schemaId } : {}),
|
|
101
100
|
});
|
|
102
101
|
}
|
|
103
102
|
// 5. Shell verification from report decision (exit-zero = report succeeded).
|
|
@@ -114,13 +113,6 @@ export async function projectOutcome(input) {
|
|
|
114
113
|
}
|
|
115
114
|
: undefined;
|
|
116
115
|
const sourceBinding = input.sourceBinding ?? deriveSourceBinding(input);
|
|
117
|
-
const outcomeStatus = input.reportDecision.succeeded ? "succeeded" : "failed";
|
|
118
|
-
const outcomeFailure = outcomeStatus === "failed"
|
|
119
|
-
? {
|
|
120
|
-
category: "EnvFailure",
|
|
121
|
-
reason: input.reportDecision.reason || "DAG report did not succeed",
|
|
122
|
-
}
|
|
123
|
-
: undefined;
|
|
124
116
|
const envelope = {
|
|
125
117
|
schemaVersion: 1,
|
|
126
118
|
identity: {
|
|
@@ -139,8 +131,7 @@ export async function projectOutcome(input) {
|
|
|
139
131
|
integrationStatus: adapterProjection.integrationStatus,
|
|
140
132
|
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
141
133
|
createdAt: input.now.toISOString(),
|
|
142
|
-
outcomeStatus,
|
|
143
|
-
...(outcomeFailure ? { outcomeFailure } : {}),
|
|
134
|
+
outcomeStatus: input.reportDecision.succeeded ? "succeeded" : "failed",
|
|
144
135
|
};
|
|
145
136
|
// 6. Strict schema parse.
|
|
146
137
|
const parsed = taskOutcomeEnvelopeV1Schema.safeParse(envelope);
|
|
@@ -30,10 +30,7 @@ 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`). */
|
|
34
33
|
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(),
|
|
37
34
|
});
|
|
38
35
|
export const shellVerificationSchema = z.object({
|
|
39
36
|
exitZero: z.boolean(),
|
|
@@ -211,34 +211,15 @@ export async function runTaskSpec(options) {
|
|
|
211
211
|
progress.step(`outcome projection failed: ${projection.category} (${projection.reason})`);
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
|
-
|
|
215
|
-
const gate = checkRequiredOutputs(envelope, options.taskSpec.outputs.required);
|
|
214
|
+
const gate = checkRequiredOutputs(projection.envelope, options.taskSpec.outputs.required);
|
|
216
215
|
if (reportDecision.succeeded && !gate.passed) {
|
|
217
216
|
status = "failed";
|
|
218
|
-
outcomeFailure = {
|
|
219
|
-
category: "ContractMismatch",
|
|
220
|
-
reason: `missing required outputs: ${gate.missing.join(", ")}`,
|
|
221
|
-
};
|
|
222
|
-
envelope = {
|
|
223
|
-
...envelope,
|
|
224
|
-
outcomeStatus: "failed",
|
|
225
|
-
outcomeFailure: {
|
|
226
|
-
category: "ContractMismatch",
|
|
227
|
-
reason: `missing required outputs: ${gate.missing.join(", ")}`,
|
|
228
|
-
missingOutputs: [...gate.missing],
|
|
229
|
-
},
|
|
230
|
-
};
|
|
217
|
+
outcomeFailure = { category: "ContractMismatch", reason: `missing required outputs: ${gate.missing.join(", ")}` };
|
|
231
218
|
progress.step(`required output gate failed: ${gate.missing.join(", ")}`);
|
|
232
219
|
}
|
|
233
|
-
else
|
|
234
|
-
|
|
235
|
-
category: envelope.outcomeFailure.category,
|
|
236
|
-
reason: envelope.outcomeFailure.reason,
|
|
237
|
-
};
|
|
220
|
+
else {
|
|
221
|
+
outcome = await writeOutcome(options.repoRoot, projection.envelope);
|
|
238
222
|
}
|
|
239
|
-
// Persist succeeded and failed envelopes so read models can inspect
|
|
240
|
-
// outcomeFailure without re-parsing run-record / report decision.
|
|
241
|
-
outcome = await writeOutcome(options.repoRoot, envelope);
|
|
242
223
|
}
|
|
243
224
|
if (status === "succeeded" && !options.skipSuccessFinalization) {
|
|
244
225
|
await runObservedStep(eventCtx, "promote-run", async () => {
|
|
@@ -155,15 +155,12 @@ export function planReadyTasks(input) {
|
|
|
155
155
|
* Evaluate artifact-gate eligibility for a node whose dependencies are all
|
|
156
156
|
* `Done`. Returns the first blocking projection, or `undefined` when eligible.
|
|
157
157
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* Delivery / Closeout (`verifyArtifactRefs`).
|
|
165
|
-
*
|
|
166
|
-
* When `outcomes` is omitted, this gate is skipped (legacy callers unchanged).
|
|
158
|
+
* Fail-closed: any unverified producer path/hash/schema/feature/producer/source
|
|
159
|
+
* binding blocks the consumer as `required-artifact-missing` and never becomes
|
|
160
|
+
* Ready. When `outcomes` is not provided, eligibility is skipped (M3 keeps
|
|
161
|
+
* callers that have not wired disk injection unchanged). Hash byte-level
|
|
162
|
+
* verification is the caller's responsibility before injecting an envelope;
|
|
163
|
+
* the planner trusts `envelope.artifacts[].sha256` as a literal.
|
|
167
164
|
*/
|
|
168
165
|
function evaluateArtifactGate(consumes, featureId, states, outcomes, producerKinds) {
|
|
169
166
|
if (!outcomes || !consumes || consumes.length === 0)
|
|
@@ -233,11 +230,8 @@ function evaluateArtifactRef(ref, featureId, states, outcomes) {
|
|
|
233
230
|
if (!artifact) {
|
|
234
231
|
return projection("artifact-kind-absent", `no artifact with kind ${ref.kind}`);
|
|
235
232
|
}
|
|
236
|
-
if (ref.schemaId !== undefined) {
|
|
237
|
-
|
|
238
|
-
if (artifactSchemaId !== ref.schemaId) {
|
|
239
|
-
return projection("schema-mismatch", `artifact schemaId ${String(artifactSchemaId)} != ${ref.schemaId}`);
|
|
240
|
-
}
|
|
233
|
+
if (ref.schemaId !== undefined && artifact.kind !== ref.schemaId) {
|
|
234
|
+
return projection("schema-mismatch", `artifact kind ${String(artifact.kind)} != schemaId ${ref.schemaId}`);
|
|
241
235
|
}
|
|
242
236
|
if (ref.sourceBinding) {
|
|
243
237
|
const envBinding = envelope.sourceBinding;
|
|
@@ -9,11 +9,9 @@ export const artifactDeclSchema = z
|
|
|
9
9
|
})
|
|
10
10
|
.strict();
|
|
11
11
|
/**
|
|
12
|
-
* Consumer-side artifact reference.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and Feature Verification Bundle / Delivery / Closeout — not inside the
|
|
16
|
-
* synchronous Ready Planner.
|
|
12
|
+
* Consumer-side artifact reference. A node may only trust an upstream Task
|
|
13
|
+
* Outcome artifact after deterministic path, hash, schema, feature,
|
|
14
|
+
* producer-task and source-binding validation (see ready-planner gate).
|
|
17
15
|
*/
|
|
18
16
|
export const artifactRefSchema = z
|
|
19
17
|
.object({
|
|
@@ -1338,10 +1338,11 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
|
|
|
1338
1338
|
allowedPaths: readOnlyPaths,
|
|
1339
1339
|
forbiddenPaths,
|
|
1340
1340
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1341
|
-
|
|
1341
|
+
firstProtocolLine: "MOCK_STRATEGY:",
|
|
1342
|
+
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.",
|
|
1342
1343
|
subtask_prompt: [
|
|
1343
1344
|
"Perform read-only Mock assessment and select one safe frontend data strategy.",
|
|
1344
|
-
"The first
|
|
1345
|
+
"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.",
|
|
1345
1346
|
"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.",
|
|
1346
1347
|
autoMaySkipMissingMock
|
|
1347
1348
|
? "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."
|
|
@@ -103,7 +103,6 @@ export async function curateKnowledgePatterns(input) {
|
|
|
103
103
|
ok: true,
|
|
104
104
|
patternsPath,
|
|
105
105
|
patternCount: 0,
|
|
106
|
-
patterns: [],
|
|
107
106
|
safetyFindings: [],
|
|
108
107
|
message: "no patterns.jsonl found; no proposal generated",
|
|
109
108
|
};
|
|
@@ -140,7 +139,6 @@ export async function curateKnowledgePatterns(input) {
|
|
|
140
139
|
patternsPath,
|
|
141
140
|
outputPath,
|
|
142
141
|
patternCount: patterns.length,
|
|
143
|
-
patterns,
|
|
144
142
|
proposalMarkdown,
|
|
145
143
|
safetyFindings,
|
|
146
144
|
message: "proposal failed skill safety audit",
|
|
@@ -155,7 +153,6 @@ export async function curateKnowledgePatterns(input) {
|
|
|
155
153
|
patternsPath,
|
|
156
154
|
outputPath,
|
|
157
155
|
patternCount: patterns.length,
|
|
158
|
-
patterns,
|
|
159
156
|
proposalMarkdown,
|
|
160
157
|
safetyFindings,
|
|
161
158
|
message: patterns.length === 0
|
|
@@ -3,35 +3,29 @@ 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";
|
|
7
6
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
8
7
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
9
8
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
10
9
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
11
10
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
12
11
|
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);
|
|
17
16
|
return buildDagNodePromptEnvelope({
|
|
18
17
|
spec,
|
|
19
18
|
task,
|
|
20
19
|
upstream,
|
|
21
|
-
resolvedSkills:
|
|
22
|
-
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
20
|
+
resolvedSkills: resolveDagNodeSkills(spec, task),
|
|
23
21
|
});
|
|
24
22
|
}
|
|
25
23
|
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd) {
|
|
26
|
-
const
|
|
27
|
-
const skillNames = policy.resolveSkills(spec, task);
|
|
28
|
-
const budget = policy.resolveSkillInstructionBudget(task);
|
|
24
|
+
const skillNames = resolveDagNodeSkills(spec, task);
|
|
29
25
|
const resolvedSkillInstructions = task.executor === "pi"
|
|
30
26
|
? await resolveDagSkillInstructions(skillNames, {
|
|
31
27
|
cwd,
|
|
32
|
-
includeLearnedPatterns:
|
|
33
|
-
perSkillMaxChars: budget.perSkillMaxChars,
|
|
34
|
-
totalMaxChars: budget.totalMaxChars,
|
|
28
|
+
includeLearnedPatterns: task.role === "implementer",
|
|
35
29
|
})
|
|
36
30
|
: [];
|
|
37
31
|
return {
|
|
@@ -41,7 +35,6 @@ export async function buildNodePromptWithResolvedSkillInstructions(spec, task, u
|
|
|
41
35
|
upstream,
|
|
42
36
|
resolvedSkills: skillNames,
|
|
43
37
|
resolvedSkillInstructions,
|
|
44
|
-
maxUpstreamChars: policy.resolveMaxUpstreamChars(task),
|
|
45
38
|
}),
|
|
46
39
|
resolvedSkills: skillInstructionMetadata(resolvedSkillInstructions),
|
|
47
40
|
};
|
|
@@ -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
|
+
"Cursor remains a first-class executor for scouts and exclusive implementers when it fits the task.",
|
|
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.",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../application/evaluation/budget.js";
|
|
5
4
|
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
6
5
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
7
|
-
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
8
6
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
9
7
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
10
8
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
@@ -122,7 +120,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
122
120
|
: {}),
|
|
123
121
|
};
|
|
124
122
|
}
|
|
125
|
-
|
|
123
|
+
return {
|
|
126
124
|
version: 1,
|
|
127
125
|
title: spec.title,
|
|
128
126
|
runId,
|
|
@@ -149,8 +147,6 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
149
147
|
}
|
|
150
148
|
: {}),
|
|
151
149
|
};
|
|
152
|
-
initRunBudgetLedger(state, spec.budget);
|
|
153
|
-
return state;
|
|
154
150
|
}
|
|
155
151
|
export function assertFrozenEvaluationBinding(spec, state) {
|
|
156
152
|
const declared = spec.evaluation;
|
|
@@ -187,7 +183,7 @@ export async function runDag(spec, opts) {
|
|
|
187
183
|
controllerVersion: runningIdentity.packageVersion,
|
|
188
184
|
});
|
|
189
185
|
const { ranks } = topoSortToRanks(spec);
|
|
190
|
-
const
|
|
186
|
+
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
191
187
|
let runId = opts.runId;
|
|
192
188
|
if (runId) {
|
|
193
189
|
for (const warning of validateDagRunIdFormat(runId).warnings) {
|
|
@@ -272,7 +268,6 @@ export async function resumeDagRun(opts) {
|
|
|
272
268
|
throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
|
|
273
269
|
}
|
|
274
270
|
assertFrozenEvaluationBinding(spec, state);
|
|
275
|
-
assertFrozenBudget(spec.budget, state.budget, state.runId);
|
|
276
271
|
// Runtime contract + controller identity must be re-verified before executing
|
|
277
272
|
// any remaining node on resume; drift fails closed.
|
|
278
273
|
const runningIdentity = resolveRunningControllerIdentity();
|
|
@@ -316,7 +311,7 @@ export async function resumeDagRun(opts) {
|
|
|
316
311
|
catch (error) {
|
|
317
312
|
throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
313
|
}
|
|
319
|
-
const
|
|
314
|
+
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
320
315
|
state.status = "running";
|
|
321
316
|
const resumedAt = new Date().toISOString();
|
|
322
317
|
state.runner = {
|
|
@@ -415,43 +410,23 @@ async function executeDagCheckpoint(input) {
|
|
|
415
410
|
tasksById,
|
|
416
411
|
meta: { runDir, runId: state.runId, spec },
|
|
417
412
|
}),
|
|
418
|
-
executeScheduledNode:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
spec,
|
|
432
|
-
cwd,
|
|
433
|
-
runDir,
|
|
434
|
-
executeNode,
|
|
435
|
-
executeDynamicNode,
|
|
436
|
-
observer: input.observer,
|
|
437
|
-
persistState,
|
|
438
|
-
onPause,
|
|
439
|
-
});
|
|
440
|
-
const node = state.nodes[nodeId];
|
|
441
|
-
if (node &&
|
|
442
|
-
(node.status === "FINISHED" || node.status === "ERROR")) {
|
|
443
|
-
recordFinishedNodeBudget(state, node);
|
|
444
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
445
|
-
await persistState();
|
|
446
|
-
}
|
|
447
|
-
},
|
|
413
|
+
executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
|
|
414
|
+
nodeId,
|
|
415
|
+
tasksById,
|
|
416
|
+
state,
|
|
417
|
+
spec,
|
|
418
|
+
cwd,
|
|
419
|
+
runDir,
|
|
420
|
+
executeNode,
|
|
421
|
+
executeDynamicNode,
|
|
422
|
+
observer: input.observer,
|
|
423
|
+
persistState,
|
|
424
|
+
onPause,
|
|
425
|
+
}),
|
|
448
426
|
});
|
|
449
427
|
if (pausedByNodeId) {
|
|
450
428
|
break;
|
|
451
429
|
}
|
|
452
|
-
if (isHardBudgetBreached(state.budgetLedger)) {
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
430
|
const convergenceDecision = await runConvergencePassController({
|
|
456
431
|
spec,
|
|
457
432
|
state,
|
|
@@ -471,7 +446,6 @@ async function executeDagCheckpoint(input) {
|
|
|
471
446
|
}
|
|
472
447
|
state.finishedAt = new Date().toISOString();
|
|
473
448
|
const runDirBeforeTransfer = runDir;
|
|
474
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
475
449
|
if (pausedByNodeId) {
|
|
476
450
|
state.status = "paused";
|
|
477
451
|
await persistState();
|
|
@@ -492,7 +466,6 @@ async function executeDagCheckpoint(input) {
|
|
|
492
466
|
if (state.convergence) {
|
|
493
467
|
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
494
468
|
}
|
|
495
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
496
469
|
await persistState({ allowCompletedFactsWrite: true });
|
|
497
470
|
return {
|
|
498
471
|
title: spec.title,
|
|
@@ -3,9 +3,9 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
|
-
import { resolveContextPolicy } from "./context-policy.js";
|
|
7
6
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
8
|
-
import { resolveDagSkillInstructions, } from "./skill-instructions.js";
|
|
7
|
+
import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, resolveDagSkillInstructions, } from "./skill-instructions.js";
|
|
8
|
+
import { resolveDagNodeSkills } from "./skills.js";
|
|
9
9
|
export const SKILL_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
10
10
|
export const SKILL_SNAPSHOT_RESOLVER_VERSION = 1;
|
|
11
11
|
export const SKILL_SNAPSHOT_REL_PATH = ".runtime/skill-snapshot.json";
|
|
@@ -117,13 +117,11 @@ function profileId(input) {
|
|
|
117
117
|
export function buildDagSkillSnapshotProfileRequest(spec, task) {
|
|
118
118
|
if (task.executor !== "pi")
|
|
119
119
|
return undefined;
|
|
120
|
-
const policy = resolveContextPolicy(spec);
|
|
121
|
-
const budget = policy.resolveSkillInstructionBudget(task);
|
|
122
120
|
const request = {
|
|
123
|
-
skills: [...
|
|
124
|
-
includeLearnedPatterns:
|
|
125
|
-
perSkillMaxChars:
|
|
126
|
-
totalMaxChars:
|
|
121
|
+
skills: [...resolveDagNodeSkills(spec, task)],
|
|
122
|
+
includeLearnedPatterns: task.role === "implementer",
|
|
123
|
+
perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
|
|
124
|
+
totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
|
|
127
125
|
};
|
|
128
126
|
return { id: profileId(request), ...request };
|
|
129
127
|
}
|
|
@@ -513,8 +511,7 @@ function stripPromptText(instruction) {
|
|
|
513
511
|
return metadata;
|
|
514
512
|
}
|
|
515
513
|
export function buildNodePromptFromSnapshot(input) {
|
|
516
|
-
const
|
|
517
|
-
const skillNames = policy.resolveSkills(input.spec, input.task);
|
|
514
|
+
const skillNames = resolveDagNodeSkills(input.spec, input.task);
|
|
518
515
|
const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
|
|
519
516
|
return {
|
|
520
517
|
prompt: buildDagNodePromptEnvelope({
|
|
@@ -523,7 +520,6 @@ export function buildNodePromptFromSnapshot(input) {
|
|
|
523
520
|
upstream: input.upstream,
|
|
524
521
|
resolvedSkills: skillNames,
|
|
525
522
|
resolvedSkillInstructions,
|
|
526
|
-
maxUpstreamChars: policy.resolveMaxUpstreamChars(input.task),
|
|
527
523
|
}),
|
|
528
524
|
resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
|
|
529
525
|
};
|