@tea-agent/loop-agent 0.16.0 → 0.16.1-beta.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 +1 -1
- package/CHANGELOG.md +7 -51
- 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/frontend-implementation-contract.js +77 -0
- package/dist/workflows/dag/init-hybrid.js +47 -3
- 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 +3 -3
- 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({
|
|
@@ -1,9 +1,86 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
|
+
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
6
9
|
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
|
+
}
|
|
7
84
|
const id = z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/);
|
|
8
85
|
const safePath = z
|
|
9
86
|
.string()
|
|
@@ -21,6 +21,7 @@ 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";
|
|
24
25
|
const REQUIREMENT_FILE = "需求.md";
|
|
25
26
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
26
27
|
const REFERENCE_DIRECTORY = "references";
|
|
@@ -1338,10 +1339,11 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
|
|
|
1338
1339
|
allowedPaths: readOnlyPaths,
|
|
1339
1340
|
forbiddenPaths,
|
|
1340
1341
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1341
|
-
|
|
1342
|
+
firstProtocolLine: "MOCK_STRATEGY:",
|
|
1343
|
+
outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
|
|
1342
1344
|
subtask_prompt: [
|
|
1343
1345
|
"Perform read-only Mock assessment and select one safe frontend data strategy.",
|
|
1344
|
-
"The first
|
|
1346
|
+
"The first line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked. Do not emit blank lines, headings, or explanatory preamble before it.",
|
|
1345
1347
|
"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
1348
|
autoMaySkipMissingMock
|
|
1347
1349
|
? "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."
|
|
@@ -1732,6 +1734,46 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1732
1734
|
allowedPaths: taskConfig.allowedPaths,
|
|
1733
1735
|
complexity: taskConfig.complexity,
|
|
1734
1736
|
});
|
|
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
|
+
})();
|
|
1735
1777
|
const sourceContext = [
|
|
1736
1778
|
buildSourceContextBlock(sources),
|
|
1737
1779
|
capabilityContextBlock,
|
|
@@ -1740,7 +1782,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1740
1782
|
.join("\n\n");
|
|
1741
1783
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
1742
1784
|
mockCapability.verifyCommands.length > 0;
|
|
1743
|
-
const requirementIds =
|
|
1785
|
+
const requirementIds = frontendSourceBinding.requirementIds;
|
|
1744
1786
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
1745
1787
|
? `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.`
|
|
1746
1788
|
: "";
|
|
@@ -1899,6 +1941,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1899
1941
|
fixedVerificationContext,
|
|
1900
1942
|
sourceContext,
|
|
1901
1943
|
mockContextBlock,
|
|
1944
|
+
frontendContractSchemaBlock,
|
|
1902
1945
|
].join("\n\n"),
|
|
1903
1946
|
},
|
|
1904
1947
|
{
|
|
@@ -1975,6 +2018,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1975
2018
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
1976
2019
|
"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.",
|
|
1977
2020
|
sourceContext,
|
|
2021
|
+
frontendContractSchemaBlock,
|
|
1978
2022
|
].join("\n\n"),
|
|
1979
2023
|
},
|
|
1980
2024
|
...(requirementIds.length > 0
|
|
@@ -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.",
|