@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.
Files changed (71) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +3 -55
  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/init-hybrid.js +3 -2
  20. package/dist/workflows/dag/knowledge-curator.js +0 -3
  21. package/dist/workflows/dag/node-execution.js +4 -11
  22. package/dist/workflows/dag/prompt.js +1 -1
  23. package/dist/workflows/dag/runner.js +16 -43
  24. package/dist/workflows/dag/skill-snapshot.js +7 -11
  25. package/dist/workflows/dag/types.js +1 -18
  26. package/docs/init-surface.manifest.json +0 -3
  27. package/docs/templates/agent-dag.schema.json +5 -0
  28. package/docs/templates/product-line/AGENTS.md +0 -1
  29. package/docs/templates/product-line/README.md +0 -17
  30. package/docs/templates/product-line/acceptance.yaml +0 -9
  31. package/docs/templates/product-line/task-graph.yaml +0 -8
  32. package/docs/templates/product-line/task.yaml +0 -4
  33. package/package.json +1 -1
  34. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  35. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  36. package/dist/application/evaluation/alias.js +0 -184
  37. package/dist/application/evaluation/budget.js +0 -192
  38. package/dist/application/evaluation/campaign-hash.js +0 -47
  39. package/dist/application/evaluation/campaign-matrix.js +0 -372
  40. package/dist/application/evaluation/campaign-scorecard.js +0 -135
  41. package/dist/application/evaluation/campaign.js +0 -370
  42. package/dist/application/evaluation/corpus-hash.js +0 -38
  43. package/dist/application/evaluation/corpus.js +0 -56
  44. package/dist/application/evaluation/experiment.js +0 -294
  45. package/dist/application/evaluation/ignition.js +0 -198
  46. package/dist/application/evaluation/integrity-audit.js +0 -162
  47. package/dist/application/evaluation/outer-loop.js +0 -132
  48. package/dist/application/evaluation/pi-cell-executor.js +0 -39
  49. package/dist/application/evaluation/private-verifier.js +0 -46
  50. package/dist/application/evaluation/promotion-policy.js +0 -151
  51. package/dist/application/evaluation/proposer.js +0 -98
  52. package/dist/infrastructure/evaluation/alias-store.js +0 -199
  53. package/dist/infrastructure/evaluation/campaign-store.js +0 -154
  54. package/dist/infrastructure/evaluation/corpus-store.js +0 -181
  55. package/dist/infrastructure/evaluation/experiment-store.js +0 -124
  56. package/dist/infrastructure/evaluation/ignition-store.js +0 -82
  57. package/dist/infrastructure/evaluation/private-verifier-store.js +0 -145
  58. package/dist/infrastructure/evaluation/proposer-store.js +0 -78
  59. package/dist/worker/outcomes/evidence-tokens.js +0 -29
  60. package/dist/workflows/dag/budget-enforcement.js +0 -67
  61. package/dist/workflows/dag/context-policy.js +0 -137
  62. package/docs/templates/evaluation/campaign-budget-v1.json +0 -12
  63. package/docs/templates/evaluation/campaign-dogfood-v0.json +0 -24
  64. package/docs/templates/evaluation/campaign-evidence-v1.json +0 -44
  65. package/docs/templates/evaluation/context-policy-baseline-v1.json +0 -17
  66. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +0 -28
  67. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +0 -118
  68. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +0 -21
  69. package/docs/templates/evaluation/matrix-fixture-v1.json +0 -10
  70. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +0 -16
  71. 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({
@@ -1338,10 +1338,11 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
1338
1338
  allowedPaths: readOnlyPaths,
1339
1339
  forbiddenPaths,
1340
1340
  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.",
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 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.",
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: 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.",
@@ -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
- const state = {
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 { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
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 { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
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: async (nodeId, executeNode, onPause) => {
419
- if (isHardBudgetBreached(state.budgetLedger))
420
- return;
421
- const preBreach = preflightBudgetOrBreach(state);
422
- if (preBreach && isHardBudgetBreached(state.budgetLedger)) {
423
- await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
424
- await persistState();
425
- return;
426
- }
427
- await executeDagNode({
428
- nodeId,
429
- tasksById,
430
- state,
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: [...policy.resolveSkills(spec, task)],
124
- includeLearnedPatterns: budget.includeLearnedPatterns,
125
- perSkillMaxChars: budget.perSkillMaxChars,
126
- totalMaxChars: budget.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 policy = resolveContextPolicy(input.spec);
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
  };