@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.
Files changed (72) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +7 -51
  3. package/README.md +1 -1
  4. package/dist/application/evaluation/candidate.js +6 -23
  5. package/dist/application/evaluation/types.js +0 -522
  6. package/dist/cli/command-definitions.js +3 -19
  7. package/dist/commands/eval.js +13 -1176
  8. package/dist/commands/init.js +1 -4
  9. package/dist/executors/dag-pi-executor.js +44 -4
  10. package/dist/worker/delivery/verification-bundle.js +2 -13
  11. package/dist/worker/feature/review.js +2 -3
  12. package/dist/worker/outcomes/adapters.js +1 -4
  13. package/dist/worker/outcomes/gate.js +11 -10
  14. package/dist/worker/outcomes/projector.js +1 -10
  15. package/dist/worker/outcomes/types.js +0 -3
  16. package/dist/worker/run-task/run-task.js +4 -23
  17. package/dist/worker/task-graph/ready-planner.js +8 -14
  18. package/dist/worker/task-graph/task-graph-schema.js +3 -5
  19. package/dist/workflows/dag/frontend-implementation-contract.js +77 -0
  20. package/dist/workflows/dag/init-hybrid.js +47 -3
  21. package/dist/workflows/dag/knowledge-curator.js +0 -3
  22. package/dist/workflows/dag/node-execution.js +4 -11
  23. package/dist/workflows/dag/prompt.js +1 -1
  24. package/dist/workflows/dag/runner.js +16 -43
  25. package/dist/workflows/dag/skill-snapshot.js +7 -11
  26. package/dist/workflows/dag/types.js +1 -18
  27. package/docs/init-surface.manifest.json +0 -3
  28. package/docs/templates/agent-dag.schema.json +5 -0
  29. package/docs/templates/product-line/AGENTS.md +0 -1
  30. package/docs/templates/product-line/README.md +0 -17
  31. package/docs/templates/product-line/acceptance.yaml +0 -9
  32. package/docs/templates/product-line/task-graph.yaml +0 -8
  33. package/docs/templates/product-line/task.yaml +0 -4
  34. package/package.json +1 -1
  35. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  36. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  37. package/dist/application/evaluation/alias.js +0 -184
  38. package/dist/application/evaluation/budget.js +0 -192
  39. package/dist/application/evaluation/campaign-hash.js +0 -47
  40. package/dist/application/evaluation/campaign-matrix.js +0 -372
  41. package/dist/application/evaluation/campaign-scorecard.js +0 -135
  42. package/dist/application/evaluation/campaign.js +0 -370
  43. package/dist/application/evaluation/corpus-hash.js +0 -38
  44. package/dist/application/evaluation/corpus.js +0 -56
  45. package/dist/application/evaluation/experiment.js +0 -294
  46. package/dist/application/evaluation/ignition.js +0 -198
  47. package/dist/application/evaluation/integrity-audit.js +0 -162
  48. package/dist/application/evaluation/outer-loop.js +0 -132
  49. package/dist/application/evaluation/pi-cell-executor.js +0 -39
  50. package/dist/application/evaluation/private-verifier.js +0 -46
  51. package/dist/application/evaluation/promotion-policy.js +0 -151
  52. package/dist/application/evaluation/proposer.js +0 -98
  53. package/dist/infrastructure/evaluation/alias-store.js +0 -199
  54. package/dist/infrastructure/evaluation/campaign-store.js +0 -154
  55. package/dist/infrastructure/evaluation/corpus-store.js +0 -181
  56. package/dist/infrastructure/evaluation/experiment-store.js +0 -124
  57. package/dist/infrastructure/evaluation/ignition-store.js +0 -82
  58. package/dist/infrastructure/evaluation/private-verifier-store.js +0 -145
  59. package/dist/infrastructure/evaluation/proposer-store.js +0 -78
  60. package/dist/worker/outcomes/evidence-tokens.js +0 -29
  61. package/dist/workflows/dag/budget-enforcement.js +0 -67
  62. package/dist/workflows/dag/context-policy.js +0 -137
  63. package/docs/templates/evaluation/campaign-budget-v1.json +0 -12
  64. package/docs/templates/evaluation/campaign-dogfood-v0.json +0 -24
  65. package/docs/templates/evaluation/campaign-evidence-v1.json +0 -44
  66. package/docs/templates/evaluation/context-policy-baseline-v1.json +0 -17
  67. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +0 -28
  68. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +0 -118
  69. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +0 -21
  70. package/docs/templates/evaluation/matrix-fixture-v1.json +0 -10
  71. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +0 -16
  72. package/docs/templates/product-line/feature.yaml +0 -11
@@ -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, evaluation, live, cache, init-surface.json, .harness/task-pool, legacy .task-pool residue) while keeping prompts and directory placeholders shareable.",
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: result.assistantText || result.stdout,
320
+ stdout: assistantText || result.stdout,
320
321
  stderr: result.stderr,
321
322
  failureCategory: result.failureCategory,
322
323
  durationMs: result.durationMs,
323
- assistantText: result.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
- 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));
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((token) => !verEnvelopes.some(({ envelope }) => isShellVerificationToken(token)
594
+ if (requiredEvidence.some((kind) => !verEnvelopes.some(({ envelope }) => kind === "shell-verification"
596
595
  ? envelope.shellVerification?.exitZero === true
597
- : envelope.artifacts?.some((artifact) => artifactMatchesEvidenceToken(artifact, token))))) {
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: identity.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 known `outputs.required` token against produced
8
- * artifact kinds (and the shell-verification exit-zero fact).
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
- * 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.
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 (isShellVerificationToken(token)) {
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
- let envelope = projection.envelope;
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 if (envelope.outcomeFailure) {
234
- outcomeFailure = {
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
- * Envelope-level binding only: Feature / producer task / kind / optional
159
- * schemaId / optional sourceBinding must match the injected Outcome. The
160
- * planner is pure and synchronous it does **not** re-read artifact files or
161
- * recompute sha256. Callers must inject envelopes via `readVerifiedOutcome`
162
- * (path/hash of the outcome JSON). File-level artifact sha256 is enforced at
163
- * `projectOutcome` and again at Feature Verification Bundle / verify-final /
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
- const artifactSchemaId = artifact.schemaId ?? artifact.kind;
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. Ready Planner validates envelope-level
13
- * Feature / producer / kind / optional schemaId / optional sourceBinding.
14
- * File-level path+sha256 of artifact bytes is enforced at Outcome projection
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
- outputContract: "Plain Markdown whose first non-empty line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
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 non-empty line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked.",
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 = buildDagSourceBinding(sources).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: policy.resolveSkills(spec, task),
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 policy = resolveContextPolicy(spec);
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: budget.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
- "Pi is the only governed Agent DAG writer; cursor-prompt is an explicit manual one-shot sidecar and must not enter Loop auto-execute or Delegate writers.",
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.",