@tea-agent/loop-agent 0.42.0 → 0.43.0-next.2

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 (78) hide show
  1. package/CHANGELOG.md +34 -7
  2. package/dist/application/evaluation/budget.js +19 -1
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +762 -252
  5. package/dist/executors/pi-executor.js +20 -1
  6. package/dist/executors/pi-sdk-executor.js +64 -1
  7. package/dist/executors/shell-executor.js +5 -2
  8. package/dist/shared/frontend-execution-policy.js +22 -0
  9. package/dist/task/config-types.js +4 -0
  10. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  11. package/dist/task/source-prepare/ledger-review.js +6 -9
  12. package/dist/task/source-prepare/semantic-intake.js +16 -26
  13. package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
  14. package/dist/worker/observe/node-transparency.js +81 -72
  15. package/dist/worker/observe/routes.js +20 -1
  16. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  17. package/dist/worker/observe/static/dom.js +20 -1
  18. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  19. package/dist/worker/observe/static/format-pool.js +6 -0
  20. package/dist/worker/observe/static/format.js +7 -0
  21. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  22. package/dist/worker/observe/static/inspector-submission.js +32 -0
  23. package/dist/worker/observe/static/kpi.js +1 -0
  24. package/dist/worker/observe/static/relations.js +2 -0
  25. package/dist/worker/observe/static/router.js +13 -0
  26. package/dist/worker/observe/static/run-processing.js +2 -0
  27. package/dist/worker/observe/static/shell-chrome.js +36 -3
  28. package/dist/worker/observe/static/state.js +35 -2
  29. package/dist/worker/observe/static/styles.css +260 -39
  30. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  31. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  32. package/dist/worker/observe/static/task-history.js +12 -0
  33. package/dist/worker/observe/static/views/batch.js +6 -13
  34. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  35. package/dist/worker/observe/static/views/dag-inspector.js +746 -265
  36. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  37. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  38. package/dist/worker/observe/static/views/dag.js +48 -10
  39. package/dist/worker/observe/static/views/dags.js +2 -0
  40. package/dist/worker/observe/static/views/dashboard.js +21 -12
  41. package/dist/worker/observe/static/views/failures.js +21 -11
  42. package/dist/worker/observe/static/views/feature.js +11 -29
  43. package/dist/worker/observe/static/views/pool.js +37 -28
  44. package/dist/worker/observe/static/views/run.js +48 -5
  45. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  46. package/dist/worker/observe/static/views/task.js +81 -62
  47. package/dist/workflows/dag/budget-enforcement.js +53 -3
  48. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  49. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  50. package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
  51. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  52. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  53. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  54. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  55. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  56. package/dist/workflows/dag/frontend-review-scopes.js +117 -0
  57. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  58. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  59. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  60. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  61. package/dist/workflows/dag/init-hybrid.js +16 -10
  62. package/dist/workflows/dag/node-execution.js +32 -155
  63. package/dist/workflows/dag/prompt.js +4 -0
  64. package/dist/workflows/dag/rerun-plan.js +7 -1
  65. package/dist/workflows/dag/runner.js +26 -1
  66. package/dist/workflows/dag/types.js +6 -0
  67. package/docs/operations/README.md +1 -0
  68. package/docs/templates/frontend-design-contract.md +4 -4
  69. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  70. package/docs/templates/frontend-implementation-dag.json +5 -5
  71. package/package.json +1 -1
  72. package/skills/frontend-contract/SKILL.md +2 -1
  73. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  74. package/skills/frontend-design-review/SKILL.md +12 -11
  75. package/skills/frontend-plan/SKILL.md +2 -2
  76. package/skills/frontend-plan/references/decision-contract.md +18 -5
  77. package/skills/frontend-review/SKILL.md +10 -11
  78. package/skills/frontend-scout/references/scout-evidence.md +4 -0
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { frontendExecutionPolicySchema } from "../../shared/frontend-execution-policy.js";
2
3
  import { access, readdir, readFile, realpath } from "node:fs/promises";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { deflateRawSync } from "node:zlib";
@@ -3061,7 +3062,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3061
3062
  .filter(Boolean)
3062
3063
  .join("\n\n"),
3063
3064
  scout: [
3064
- buildSourceContextBlock(sources, { includeReferenceDocuments: false }),
3065
+ buildSourceContextBlock(sources, { includeRequirementExcerpt: false, includeConstraintExcerpt: false, includeReferenceDocuments: false }),
3065
3066
  capabilityContextBlock,
3066
3067
  ]
3067
3068
  .filter(Boolean)
@@ -3342,11 +3343,11 @@ async function buildFrontendHybridDagFromTask(sources) {
3342
3343
  allowedPaths: readOnlyPaths,
3343
3344
  forbiddenPaths,
3344
3345
  skills: FRONTEND_CONTRACT_SKILLS,
3345
- outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then call finalize_contract exactly once. record_requirement takes only the canonical ledger requirement id — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
3346
+ outputContract: "Incremental typed requirement facts; narrative is display-only. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then complete each input scope with complete_contract_scope, then call finalize_contract; correct rejected calls until one successful terminal. record_requirement takes the canonical ledger requirement id and optional execution:{groupId,kind,summary} — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
3346
3347
  subtask_prompt: [
3347
- "OUTPUT BUDGET DISCIPLINE (hard requirement, extreme-environment safe): the provider output window is small — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
3348
- "Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
3349
- "Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes ONLY the canonical id; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
3348
+ "OUTPUT BUDGET DISCIPLINE: provider capacity is discovered at runtime; use small records — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
3349
+ "Consume the complete injected input scope and produce a concise frontend implementation contract as typed requirement facts from complete injected scopes.",
3350
+ "Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes the canonical id and optional execution:{groupId,kind,summary}; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
3350
3351
  "Requirement semantics, source spans, dispositions, and fragment bindings are ledger/runtime-owned. If a canonical requirement is genuinely blocked, say so in the Markdown contract narrative and finalize with the matching disposition instead of trying to encode it in the requirement fact.",
3351
3352
  "Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
3352
3353
  'Use record_evidence_expectation with {requirementId,evidence:{static,behavior,mock,"real-integration"}}; every lane is required | optional | not-applicable. Requirement text and provenance remain runtime-owned.',
@@ -3406,17 +3407,17 @@ async function buildFrontendHybridDagFromTask(sources) {
3406
3407
  retryOnInvalid: true,
3407
3408
  skeleton: frontendContractSkeleton,
3408
3409
  },
3409
- outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan exactly once. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
3410
+ outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan; correct rejected facts until one successful terminal. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
3410
3411
  subtask_prompt: [
3411
3412
  "Plan only the delta between the frozen frontend-contract-pi facts and frontend-scout-pi target surface. Do not reinterpret the task, repeat requirements, search the repository, or choose implementation order.",
3412
3413
  "Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
3413
3414
  "Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
3414
3415
  `Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
3415
- "UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Coverage slices by requirement; UX does not. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
3416
+ "UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Details consume complete execution-group scopes and reuse the same global names across scopes. Constraints/exclusions must not manufacture UI. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
3416
3417
  ...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
3417
- "Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
3418
- "TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
3419
- "Output budget protocol (hard, max output <=16K per turn): never enumerate-reason the whole requirement list before your first record_* call that reasoning burns the entire output budget and the attempt dies with zero committed facts. Process requirements in order: think about ONE requirement briefly, immediately emit its record calls (up to 5 per message), then move to the next. If your budget runs low, stop recording and call finalize_plan with what is committed the retry ladder continues the remainder in a fresh session.",
3418
+ "Call finalize_plan; correct rejected facts until one successful terminal after the necessary typed facts. Return no Markdown narrative.",
3419
+ "Group members retain full source text and independent ACs. Use record_plan_group_coverage only for actually shared references; do not create components for exclusions. TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
3420
+ "Incremental output protocol: process the current complete scope, committing small records immediately. Runtime packs full input and estimated work without assuming a model capacity from its name. Use record_plan_group_coverage for shared references, record_mock_endpoint per endpoint, and finalize only when all coverage is complete. On exhaustion, durable progress survives and remaining work is reduced; never omit source conditions or repeatedly retry the same exhausted scope.",
3420
3421
  fixedVerificationContext,
3421
3422
  scopedOpenspecContext,
3422
3423
  mockContextBlock,
@@ -3500,6 +3501,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3500
3501
  skills: FRONTEND_DESIGN_REVIEW_SKILLS,
3501
3502
  outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
3502
3503
  subtask_prompt: [
3504
+ "Submit findings individually with record_design_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
3503
3505
  "Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
3504
3506
  "Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
3505
3507
  "request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
@@ -3665,6 +3667,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3665
3667
  skills: FRONTEND_REVIEW_SKILLS,
3666
3668
  outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
3667
3669
  subtask_prompt: [
3670
+ "Submit findings individually with record_review_finding and stable IDs; terminal tools aggregate saved findings. Correct rejected calls, stop after a successful terminal. Check execution groups against every member source outcome, including permission, threshold and failure-path differences; shared verification is valid only when it proves each independent AC.",
3668
3671
  "Review the frontend implementation and verification evidence.",
3669
3672
  "Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
3670
3673
  "approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
@@ -3751,6 +3754,9 @@ async function buildFrontendHybridDagFromTask(sources) {
3751
3754
  // runner can bound M6 auto-recovery without re-reading the task config.
3752
3755
  const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
3753
3756
  spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
3757
+ // Configurable execution safety quota, independent of model context/output capacity.
3758
+ spec.budget = { schemaVersion: 1, mode: "hard", limits: { maxProviderRequests: sources.taskConfig.frontendRecovery?.maxProviderRequests ?? 1024 } };
3759
+ spec.frontendExecutionPolicy = frontendExecutionPolicySchema.parse(sources.taskConfig.frontendExecutionPolicy ?? {});
3754
3760
  applyDefaultReadOnlyRetryPolicy(spec);
3755
3761
  stampGeneratedArtifactBindings(spec);
3756
3762
  parseDagSpec(spec);
@@ -1,3 +1,5 @@
1
+ import { collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
2
+ import { reserveDagProviderRequest } from "./budget-enforcement.js";
1
3
  import { createHash } from "node:crypto";
2
4
  import { existsSync } from "node:fs";
3
5
  import { readFile } from "node:fs/promises";
@@ -380,27 +382,14 @@ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
380
382
  function planInputRecord(value) {
381
383
  return typeof value === "object" && value !== null && !Array.isArray(value);
382
384
  }
383
- function planInputText(value, maxChars = 240) {
384
- if (typeof value !== "string" || value.trim().length === 0)
385
- return undefined;
386
- const normalized = value.trim();
387
- return normalized.length <= maxChars
388
- ? normalized
389
- : `${normalized.slice(0, maxChars - 1)}…`;
385
+ function planInputText(value, _maxChars) {
386
+ return typeof value === "string" && value.trim().length ? value.trim() : undefined;
390
387
  }
391
388
  function planInputStrings(value) {
392
389
  return Array.isArray(value)
393
390
  ? value.filter((item) => typeof item === "string")
394
391
  : [];
395
392
  }
396
- /** Input bound for the planner evidence block; protects the model input budget. */
397
- const FRONTEND_PLAN_INPUT_MAX_CHARS = 12_000;
398
- const FRONTEND_PLAN_INPUT_CAP_LADDER = [
399
- { text: 240, array: 40 },
400
- { text: 120, array: 20 },
401
- { text: 60, array: 10 },
402
- { text: 24, array: 4 },
403
- ];
404
393
  /**
405
394
  * Frozen requirement→PRD citation map for the plan review checklist: lets the
406
395
  * model declare sourceRequirementIds whose section/line match the component
@@ -446,78 +435,19 @@ async function resolveComponentSourceCitations(spec, cwd) {
446
435
  return new Map();
447
436
  }
448
437
  }
449
- /** Input bound for the contract node's compiled ledger block. */
450
- const FRONTEND_CONTRACT_INPUT_MAX_CHARS = 12_000;
451
438
  /**
452
- * Render the contract node's complete-but-bounded ledger handoff. The
439
+ * Render the contract node's complete semantic ledger handoff. The
453
440
  * source-fidelity ledger already extracted canonical requirements with source
454
441
  * spans; the contract node confirms and commits them incrementally through
455
442
  * record_requirement instead of re-reading the raw source (extreme-environment:
456
443
  * a small output window cannot absorb a full source re-read).
457
444
  *
458
- * Same shape guarantees as the plan input block: always valid JSON under the
459
- * char bound, ids never drop, texts degrade through the cap ladder.
445
+ * Whole obligations are retained here; the executor selects complete scopes
446
+ * before dispatch. References and conditions are never clipped.
460
447
  */
461
448
  export function renderFrontendContractInputContext(input) {
462
- // Fragment bindings are ids, not prose: they are the one thing the contract
463
- // must never lose. r6 regression the last-resort degradation dropped
464
- // sourceFragmentIds entirely, the model (correctly refusing to invent ids)
465
- // committed empty bindings, and the plan compile failed the ledger-binding
466
- // gate for every requirement. Bindings therefore bypass the cap ladder and
467
- // every degradation level; only requirement TEXTS and fragment CONTEXT
468
- // (path/headingPath) may degrade. Fragment context is rendered only for
469
- // fragments actually referenced by a requirement and shrinks first.
470
- const referencedFragmentIds = new Set(input.canonicalRequirements.flatMap((requirement) => planInputStrings(requirement.sourceFragmentIds)));
471
- const referencedFragments = input.fragments.filter((fragment) => referencedFragmentIds.has(fragment.id));
472
- const serializeAtCap = (cap) => JSON.stringify({
473
- requirements: input.canonicalRequirements.map((requirement) => ({
474
- id: requirement.id,
475
- text: planInputText(requirement.text, cap.text),
476
- sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds),
477
- })),
478
- fragments: referencedFragments.map((fragment) => ({
479
- id: fragment.id,
480
- path: planInputText(fragment.path, 200),
481
- headingPath: planInputText(fragment.headingPath, 120),
482
- lineRange: fragment.lineRange,
483
- })),
484
- });
485
- let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
486
- for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
487
- if (serialized.length <= FRONTEND_CONTRACT_INPUT_MAX_CHARS)
488
- break;
489
- serialized = serializeAtCap(cap);
490
- }
491
- if (serialized.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS) {
492
- // Last resort: keep every requirement id AND its fragment bindings,
493
- // degrade texts, and shrink referenced fragment context first (halve,
494
- // then drop context fields, then drop the fragment list entirely).
495
- // Requirement ids and sourceFragmentIds are never dropped.
496
- let fragments = referencedFragments.map((fragment) => ({
497
- id: fragment.id,
498
- path: planInputText(fragment.path, 120),
499
- }));
500
- let requirements = input.canonicalRequirements.map((requirement) => {
501
- const sourceFragmentIds = planInputStrings(requirement.sourceFragmentIds);
502
- return {
503
- id: requirement.id,
504
- text: "(truncated)",
505
- // Empty bindings carry no information; omit them so the payload
506
- // stays inside the char bound when no requirement is bound.
507
- ...(sourceFragmentIds.length > 0 ? { sourceFragmentIds } : {}),
508
- };
509
- });
510
- let bounded = JSON.stringify({ degraded: "requirement-texts-truncated", requirements, fragments });
511
- while (bounded.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS && fragments.length > 0) {
512
- fragments = fragments.slice(0, Math.floor(fragments.length / 2));
513
- bounded = JSON.stringify({
514
- degraded: "requirement-texts-truncated",
515
- requirements,
516
- fragments,
517
- });
518
- }
519
- serialized = bounded;
520
- }
449
+ const referenced = new Set(input.canonicalRequirements.flatMap(r => r.sourceFragmentIds));
450
+ const serialized = JSON.stringify({ requirements: input.canonicalRequirements, fragments: input.fragments.filter(f => referenced.has(f.id)) });
521
451
  return [
522
452
  "<frontend_contract_input>",
523
453
  "Canonical requirements extracted by the source-fidelity ledger, compiled by the runner. Treat them as the authoritative requirement inventory: confirm and commit each requirement through record_requirement (one per tool call); the ledger already binds source fragments, so do NOT re-read the raw source files.",
@@ -542,16 +472,13 @@ export async function buildFrontendContractInputContext(input) {
542
472
  });
543
473
  }
544
474
  /**
545
- * Render the planner's complete-but-bounded evidence handoff from committed
475
+ * Render the planner's complete semantic evidence handoff from committed
546
476
  * typed facts. It deliberately excludes upstream response prose and artifact
547
477
  * paths: Contract and Scout have already established these facts, so Plan
548
478
  * should decide and commit rather than spend another model turn reading them.
549
479
  *
550
- * The block is always valid JSON under the char bound: field texts shrink
551
- * through a cap ladder before any fact is dropped, and the last-resort
552
- * fallback keeps every requirement id (with `text: "(truncated)"`) while
553
- * declaring the degradation, so the planner records targeted evidence gaps
554
- * instead of receiving a silently corrupted tail.
480
+ * Whole obligations are retained. Session packing and scoped projection happen
481
+ * in the executor before dispatch, without dropping text or source identities.
555
482
  */
556
483
  export function renderFrontendPlanInputContext(input) {
557
484
  const committedFacts = (records) => records.flatMap((record) => record.phase === "committed" && planInputRecord(record.fact)
@@ -610,77 +537,12 @@ export function renderFrontendPlanInputContext(input) {
610
537
  const behaviorRequiredIds = requirementFacts
611
538
  .filter((requirement) => requirement.evidence.behavior === "required")
612
539
  .map((requirement) => requirement.id);
613
- const serializeAtCap = (cap) => JSON.stringify({
614
- requirements: requirements.map((requirement) => ({
615
- id: requirement.id,
616
- text: planInputText(requirement.text, cap.text),
617
- sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
618
- })),
619
- requiredDeliverables,
620
- targetSurface: targetSurface.map((surface) => ({
621
- completeness: planInputText(surface.completeness, 32),
622
- entrypoint: planInputText(surface.entrypoint, cap.text),
623
- routeOrMount: planInputText(surface.routeOrMount, cap.text),
624
- implementationPaths: planInputStrings(surface.implementationPaths).slice(0, cap.array),
625
- testPaths: planInputStrings(surface.testPaths).slice(0, cap.array),
626
- dataSource: planInputText(surface.dataSource, cap.text),
627
- allowedPathConflicts: planInputStrings(surface.allowedPathConflicts).slice(0, cap.array),
628
- unresolvedPaths: planInputStrings(surface.unresolvedPaths).slice(0, cap.array),
629
- })),
630
- designEvidence: designEvidence.map((evidence) => ({
631
- source: planInputText(evidence.source, cap.text),
632
- paths: planInputStrings(evidence.paths).slice(0, cap.array),
633
- conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
634
- })),
635
- declaredUiStates: declaredUiStates.map((state) => ({
636
- id: state.id,
637
- trigger: planInputText(state.trigger, cap.text),
638
- observableOutcome: planInputText(state.observableOutcome, cap.text),
639
- })),
640
- committedUx: committedUiStateNames.length > 0 ||
641
- committedInteractionNames.length > 0
642
- ? {
643
- uiStateNames: committedUiStateNames,
644
- interactionNames: committedInteractionNames,
645
- }
646
- : undefined,
540
+ const serialized = JSON.stringify({
541
+ requirements, requiredDeliverables, targetSurface, designEvidence, declaredUiStates,
542
+ executionGroups: collectFrontendExecutionGroups(contractFacts.filter(f => f.kind === "requirement").map(f => ({ id: String(f.id), execution: f.execution }))),
543
+ constraints: contractFacts.filter(f => ["constraint", "open-question", "split-proposal", "handoff-intent"].includes(String(f.kind))),
544
+ committedUx: committedUiStateNames.length || committedInteractionNames.length ? { uiStateNames: committedUiStateNames, interactionNames: committedInteractionNames } : undefined,
647
545
  });
648
- let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
649
- for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
650
- if (serialized.length <= FRONTEND_PLAN_INPUT_MAX_CHARS)
651
- break;
652
- serialized = serializeAtCap(cap);
653
- }
654
- if (serialized.length > FRONTEND_PLAN_INPUT_MAX_CHARS) {
655
- // Last resort: keep every requirement id (ids are short and the plan
656
- // prompt separately lists them) but drop their texts, shrink scout facts
657
- // to the minimum, and declare the degradation instead of corrupting JSON.
658
- let fallback = {
659
- degraded: "requirement-texts-truncated",
660
- requiredDeliverables,
661
- requirements: requirements.map((requirement) => ({
662
- id: requirement.id,
663
- text: "(truncated)",
664
- })),
665
- targetSurface: targetSurface.map((surface) => ({
666
- completeness: planInputText(surface.completeness, 32),
667
- implementationPaths: planInputStrings(surface.implementationPaths).slice(0, FRONTEND_PLAN_INPUT_CAP_LADDER[3].array),
668
- })),
669
- designEvidence: [],
670
- };
671
- let bounded = JSON.stringify(fallback);
672
- let keep = fallback.requirements.length;
673
- while (bounded.length > FRONTEND_PLAN_INPUT_MAX_CHARS &&
674
- keep > 0) {
675
- keep = Math.max(0, Math.floor(keep / 2));
676
- fallback = { ...fallback, requirements: fallback.requirements.slice(0, keep) };
677
- bounded = JSON.stringify({
678
- ...fallback,
679
- requirementIdsTruncated: keep < requirements.length,
680
- });
681
- }
682
- serialized = bounded;
683
- }
684
546
  const checklistLines = [
685
547
  "1. Record the GLOBAL UX vocabulary with record_state_registry BEFORE any record_state_flow: one stable behavior-domain name per state/interaction (e.g. planner-task-edit), never one name per AC number, and never a rename of an already-recorded concept. Coverage slices by AC; UX does not.",
686
548
  "2. Every recorded interaction/uiState name must be in that registry, and uiState names must use the contract's declared authoritative ids (declaredUiStates below) when present.",
@@ -1583,6 +1445,20 @@ export async function executeDagNode(input) {
1583
1445
  return;
1584
1446
  }
1585
1447
  }
1448
+ if (task.id === "frontend-scout-pi") {
1449
+ try {
1450
+ const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
1451
+ const facts = (await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"))).filter(r => r.phase === "committed").map(r => r.fact);
1452
+ const requirements = [...new Map(facts.filter(f => f.kind === "requirement").map(f => [String(f.id), f])).values()];
1453
+ if (!requirements.length)
1454
+ throw Error("FRONTEND_INPUT_MISSING: Scout needs committed Contract obligations");
1455
+ prompt += `\n<frontend_scout_input>\n${JSON.stringify({ requirements, sharedFacts: facts.filter(f => !["requirement", "contract-scope-completed", "contract-finalized"].includes(String(f.kind))) })}\n</frontend_scout_input>`;
1456
+ }
1457
+ catch (error) {
1458
+ await failBeforePrompt(error, "frontend-scout-input-unavailable");
1459
+ return;
1460
+ }
1461
+ }
1586
1462
  if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
1587
1463
  // Design-review findings are not reliable in the provider's prose output
1588
1464
  // (typed terminal nodes commonly return an empty assistant message). Inject
@@ -1770,6 +1646,7 @@ export async function executeDagNode(input) {
1770
1646
  attemptPrompt,
1771
1647
  });
1772
1648
  result = await executeNode({
1649
+ ...(state.budgetLedger?.mode === "hard" && state.budgetLedger.limits.maxProviderRequests !== undefined ? { reserveProviderRequest: () => reserveDagProviderRequest({ state, nodeId, attempt: attemptNumber, persist: input.persistState }) } : {}),
1773
1650
  task,
1774
1651
  cwd,
1775
1652
  model,
@@ -136,6 +136,10 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
136
136
  const consumedArtifactIds = new Set((task.consumesArtifacts ?? []).map((binding) => binding.artifactId));
137
137
  for (const depId of task.depends_on) {
138
138
  const record = upstream[depId];
139
+ if (task.id === "frontend-scout-pi" && depId === "frontend-contract-pi" && record?.status === "FINISHED") {
140
+ sections.push(`## Upstream output: ${depId}\nCommitted Contract obligations are supplied in frontend_scout_input. Do not reconstruct them from prose or reread the source.`);
141
+ continue;
142
+ }
139
143
  const stdout = record?.stdout?.trim() ? record.stdout : "";
140
144
  const assistantText = record?.assistantText?.trim()
141
145
  ? record.assistantText
@@ -46,6 +46,7 @@ const highRiskContinuationAuthorityV1Schema = z
46
46
  tokens: z.number().int().nonnegative().nullable(),
47
47
  wallTimeMs: z.number().nonnegative(),
48
48
  executorCalls: z.number().int().nonnegative(),
49
+ providerRequests: z.number().int().nonnegative().optional(),
49
50
  repairPasses: z.number().int().nonnegative(),
50
51
  peakContextChars: z.number().int().nonnegative(),
51
52
  missingTokenNodeIds: z.array(z.string()),
@@ -1300,7 +1301,12 @@ export async function evaluateDagRerunPlan(input) {
1300
1301
  const task = tasksByIdForPlan.get(nodeId);
1301
1302
  return task?.readSet ?? [];
1302
1303
  });
1303
- const scopedDrift = driftedPaths !== undefined &&
1304
+ // An omitted or empty readSet is unrestricted, not proof that an
1305
+ // imported node read nothing. Localize drift only when every reused
1306
+ // node has an explicit reading boundary.
1307
+ const importedReadSetsKnown = importedNodeIds.every((nodeId) => (tasksByIdForPlan.get(nodeId)?.readSet?.length ?? 0) > 0);
1308
+ const scopedDrift = importedReadSetsKnown &&
1309
+ driftedPaths !== undefined &&
1304
1310
  driftedPaths.length > 0 &&
1305
1311
  driftedPaths.every((driftPath) => !importedReadPaths.some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern)) &&
1306
1312
  resetNodeIds.some((nodeId) => {
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../application/evaluation/budget.js";
7
7
  import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
8
8
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
9
- import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
9
+ import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, validateCumulativeBudgetSuccessor, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
10
10
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, readDagRunState, requireActiveDagRun, } from "./lifecycle.js";
11
11
  import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
12
12
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
@@ -887,6 +887,31 @@ async function executeDagCheckpoint(input) {
887
887
  observeOnly: created.kind === "observed",
888
888
  deps: {
889
889
  ...input.recovery,
890
+ budget: input.recovery.budget ?? state.budget,
891
+ readLedgers: input.recovery.readLedgers ?? (async () => {
892
+ if (!state.budget)
893
+ return [];
894
+ const location = await locateDagRun(cwd, state.runId);
895
+ let current = await readDagRunState(location?.runDir ?? runDir);
896
+ const visited = new Set([current.runId]);
897
+ while (current.frontendRecoveryState?.parentRunId === current.runId && current.frontendRecoveryState.childRunId && current.frontendRecoveryState.childRunId !== current.runId) {
898
+ if (["child-staging", "child-activating"].includes(current.frontendRecoveryState.phase))
899
+ break;
900
+ const childId = current.frontendRecoveryState.childRunId;
901
+ if (visited.has(childId))
902
+ throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: cyclic recovery");
903
+ visited.add(childId);
904
+ const childLocation = await locateDagRun(cwd, childId);
905
+ if (!childLocation)
906
+ throw Error("FRONTEND_PROVIDER_BUDGET_LINEAGE_INVALID: missing recovery child");
907
+ const child = await readDagRunState(childLocation.runDir);
908
+ validateCumulativeBudgetSuccessor(current, child);
909
+ current = child;
910
+ }
911
+ if (!current.budgetLedger)
912
+ throw Error("FRONTEND_PROVIDER_BUDGET_INVALID: recovery ledger missing");
913
+ return [current.budgetLedger];
914
+ }),
890
915
  failureOwner: input.recovery.failureOwner ??
891
916
  recoveryTrigger.failureOwner,
892
917
  protocolFailureReason: input.recovery.protocolFailureReason ??
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { frontendExecutionPolicySchema } from "../../shared/frontend-execution-policy.js";
2
3
  import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, OPENSPEC_SPEC_EXT_RE, } from "../../shared/openspec-spec.js";
3
4
  import { campaignBudgetSchema, } from "../../application/evaluation/budget.js";
4
5
  import { assertDagPromptSourceRule } from "./prompt-source.js";
@@ -1385,6 +1386,7 @@ export const dagSpecSchema = z
1385
1386
  evaluation: dagEvaluationBindingSchema.optional(),
1386
1387
  /** Optional hard/record-only budget; requires version 3 or 4. */
1387
1388
  budget: dagBudgetSchema.optional(),
1389
+ frontendExecutionPolicy: frontendExecutionPolicySchema.optional(),
1388
1390
  sourceBinding: dagSourceBindingSchema.optional(),
1389
1391
  /** v4 managed Task Contract binding; only valid on version 4. */
1390
1392
  taskContractBinding: dagTaskContractBindingSchema.optional(),
@@ -1567,6 +1569,10 @@ export const frontendRecoveryStateSchema = z
1567
1569
  recoveryRootRunId: z.string().min(1),
1568
1570
  parentRunId: z.string().min(1),
1569
1571
  childRunId: z.string().min(1).optional(),
1572
+ budgetLineage: z.object({
1573
+ parentLedgerSha256: z.string().regex(/^[a-f0-9]{64}$/), inheritedAt: z.string().datetime(),
1574
+ inheritedConsumed: z.object({ tokens: z.number().nonnegative().nullable(), wallTimeMs: z.number().nonnegative(), executorCalls: z.number().int().nonnegative(), providerRequests: z.number().int().nonnegative().optional(), repairPasses: z.number().int().nonnegative(), peakContextChars: z.number().int().nonnegative(), missingTokenNodeIds: z.array(z.string()) }).strict(),
1575
+ }).strict().optional(),
1570
1576
  attemptId: z.string().min(1),
1571
1577
  attemptIndex: z.number().int().min(0).max(5),
1572
1578
  continuationCount: z.number().int().min(0).max(5),
@@ -9,6 +9,7 @@
9
9
  - [`github-collaboration.md`](github-collaboration.md):仓库内 GitHub 协作约定及发布通道。
10
10
  - [`version-and-release-management.md`](version-and-release-management.md):版本发布管理——next / release-x.y / latest 渠道、移动式 `npm-next` ref、`inspect next` 查询入口、workflow 与 promote 手册。
11
11
  - [`production-readiness.md`](production-readiness.md):交付前的生产就绪判断。
12
+ - [`frontend-reliability-canary-runbook.md`](frontend-reliability-canary-runbook.md):frontend 可靠性修复的 live canary 内网执行手册(协议冻结自 exec-plan §17;配对矩阵、证据注册与人工晋升门)。
12
13
  - [`agent-worker-production-readiness.md`](agent-worker-production-readiness.md):agent-worker Feature/Task Pool 外层编排的 Production Readiness v1 操作合同。
13
14
  - [`backend-test-jacoco-coverage.md`](backend-test-jacoco-coverage.md):Java 被测服务挂载 JaCoCo tcpserver agent 的 Maven、`java -jar` 与 Docker 操作手册。
14
15
  - [`console-chat-dogfood-playbook.md`](console-chat-dogfood-playbook.md):Operator Chat(含 Explore 子 Agent)真实模型 dogfood 的 CLI 驱动步骤、mutation 双 token / SSE 超时 / question 应答等已知陷阱与验证方式。
@@ -61,8 +61,8 @@
61
61
  - **DAG 如何消费**:进入 `evidenceGaps[]` 与 Non-goals,`frontend-review-pi`/closeout 据它保留风险与后续项。
62
62
  - **缺失/冲突时 fail-closed**:范围与需求非目标冲突、或风险被隐藏时阻塞。
63
63
 
64
- ## OpenSpec 引用块(openspec-citations)
64
+ ## OpenSpec 来源与引用
65
65
 
66
- - **要写什么**:在 fenced `json` 契约块之后追加**恰好一个** fenced `openspec-citations` 块,每行一个 JSON `{"path","section","line"}`(`section` 可空串、`line` 为 int 或 null),逐条列出本计划实际读取并应用的每个 openspec 规范文件。
67
- - **DAG 如何消费**:`frontend-prewrite-gate-shell` fence 语言标签解析该块,并与生效 plan/review 节点的成功 read 事件核验;`cited` 模式下候选未引用 `openspec-not-cited`,引用无 read 背书 → `openspec-citation-not-read`,块缺失/不可解析 → `openspec-citation-block-unparseable`。
68
- - **缺失/冲突时 fail-closed**:候选非空而引用块缺失/不可解析、候选未引用、或引用未真实读取时,prewrite gate 以 `retryable-invalid` fail-closed;不要引用未读取的路径,也不要遗漏已读取的 openspec 文件。
66
+ - Contract `record_openspec_selection` 声明实际使用的 required/relevant 来源,未提及候选不自动升级为 required。
67
+ - Plan 通过 typed tools 引用冻结来源;runtime 从绑定来源与真实读取证据物化引用和 canonical JSON。模型不再输出 JSON 契约块或 `openspec-citations` fence。
68
+ - 来源未绑定、引用不可解析或缺少真实读取背书时,由确定性 gate 阻断;不得伪造读取或引用。
@@ -115,6 +115,32 @@
115
115
  "items": {
116
116
  "$ref": "#/$defs/path"
117
117
  }
118
+ },
119
+ "execution": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "required": [
123
+ "groupId",
124
+ "kind",
125
+ "summary"
126
+ ],
127
+ "properties": {
128
+ "groupId": {
129
+ "type": "string",
130
+ "minLength": 1
131
+ },
132
+ "kind": {
133
+ "enum": [
134
+ "behavior",
135
+ "constraint",
136
+ "exclusion"
137
+ ]
138
+ },
139
+ "summary": {
140
+ "type": "string",
141
+ "minLength": 1
142
+ }
143
+ }
118
144
  }
119
145
  }
120
146
  }
@@ -644,10 +670,16 @@
644
670
  "allOf": [
645
671
  {
646
672
  "if": {
647
- "required": ["ledgerPath"]
673
+ "required": [
674
+ "ledgerPath"
675
+ ]
648
676
  },
649
677
  "then": {
650
- "required": ["ledgerSha256", "inputDigest", "requirementToFragments"]
678
+ "required": [
679
+ "ledgerSha256",
680
+ "inputDigest",
681
+ "requirementToFragments"
682
+ ]
651
683
  }
652
684
  }
653
685
  ],
@@ -27,17 +27,17 @@
27
27
  "tasks": [
28
28
  {
29
29
  "id":"frontend-contract-pi", "depends_on":[], "complexity":"MED", "executor":"pi", "role":"planner", "writePolicy":"read-only", "allowedPaths":["**"], "forbiddenPaths":[".harness/**","artifacts/**"],
30
- "outputContract":"Plain Markdown frontend contract: required behavior, states, interactions, non-goals, concrete verification expectations, and unresolved gaps; no file writes.",
30
+ "outputContract":"Incremental Contract typed facts from complete input scopes; complete_contract_scope after all decisions, then one successful finalize_contract. Runtime owns canonical text and source bindings. No file writes.",
31
31
  "subtask_prompt":"Read the task inputs. Produce a concise frontend contract that maps each required behavior, UI state, and interaction to observable outcomes. Identify missing or conflicting requirements as blockers; do not infer them and do not edit files."
32
32
  },
33
33
  {
34
34
  "id":"frontend-scout-pi", "depends_on":["frontend-contract-pi"], "complexity":"LOW", "executor":"pi", "role":"scout", "writePolicy":"read-only", "allowedPaths":["REPLACE/WITH/FRONTEND/SOURCE/PATH/**","REPLACE/WITH/FRONTEND/TEST/PATH/**"], "forbiddenPaths":[".harness/**","artifacts/**"],
35
- "outputContract":"Plain Markdown source and verification reconnaissance: relevant routes, components, styles, state patterns, focused tests, and risks; no file writes.",
35
+ "outputContract":"Scout typed target-surface and design-evidence facts backed by fresh read evidence; durable receipts only. No file writes.",
36
36
  "subtask_prompt":"Inspect only the declared frontend source and test paths. Report reusable component, styling, accessibility, state-management, and verification patterns that satisfy the contract. Do not edit files or broaden the allowed paths."
37
37
  },
38
38
  {
39
39
  "id":"frontend-plan-pi", "depends_on":["frontend-scout-pi"], "complexity":"MED", "executor":"pi", "role":"planner", "writePolicy":"read-only", "allowedPaths":["REPLACE/WITH/FRONTEND/SOURCE/PATH/**","REPLACE/WITH/FRONTEND/TEST/PATH/**"], "forbiddenPaths":[".harness/**","artifacts/**"],
40
- "outputContract":"Plain Markdown implementation plan: ordered changes, concrete target files, behavior/state/interaction coverage, and focused verification mapping; no file writes.",
40
+ "outputContract":"Plan record_* tools for scoped coverage, shared execution groups and UX, one mock endpoint per record, then one successful finalize_plan. Runtime derives canonical JSON and Markdown. No file writes.",
41
41
  "subtask_prompt":"Turn the contract and reconnaissance into a minimal implementation plan. Every target file and verification step must be concrete. Preserve existing design and accessibility conventions. If any behavior lacks a verifiable target, report it as blocked instead of guessing."
42
42
  },
43
43
  {
@@ -48,7 +48,7 @@
48
48
  },
49
49
  {
50
50
  "id":"frontend-design-review-pi", "depends_on":["frontend-design-policy-shell"], "complexity":"MED", "executor":"pi", "role":"reviewer", "writePolicy":"read-only", "allowedPaths":["REPLACE/WITH/FRONTEND/SOURCE/PATH/**","REPLACE/WITH/FRONTEND/TEST/PATH/**"], "forbiddenPaths":[".harness/**","artifacts/**"],
51
- "outputContract":"Plain Markdown design review: approved plan or explicit blocking findings for behavior, state, interaction, design-system, and accessibility coverage; no file writes.",
51
+ "outputContract":"record_design_finding for individual findings; approve_design or request_design_changes provides the sole committed terminal. Independently review execution-group member differences. No file writes.",
52
52
  "subtask_prompt":"Review the approved plan against the frontend contract and design-policy result. Reject missing visible states, interaction behavior, accessibility obligations, or unsupported design claims. Do not edit files."
53
53
  },
54
54
  {
@@ -76,7 +76,7 @@
76
76
  },
77
77
  {
78
78
  "id":"frontend-review-pi", "depends_on":["frontend-review-context-shell"], "complexity":"MED", "executor":"pi", "role":"reviewer", "writePolicy":"read-only", "allowedPaths":["REPLACE/WITH/FRONTEND/TARGET/PATH/**"], "forbiddenPaths":[".harness/**","artifacts/**"],
79
- "outputContract":"Plain Markdown final review: approved result or explicit findings for contract coverage, regression risk, accessibility, and verification gaps; no file writes.",
79
+ "outputContract":"record_review_finding for individual findings; approve_review or request_review_changes provides the sole committed terminal. No file writes.",
80
80
  "subtask_prompt":"Review the actual diff and archived verification evidence against the frontend contract. Do not treat missing, weak, mock-only, or not-run evidence as passed. Report explicit residual risks; do not edit files."
81
81
  },
82
82
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.42.0",
3
+ "version": "0.43.0-next.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -15,7 +15,8 @@ language. Do not inspect implementation files or choose components, routes, Mock
15
15
  mechanisms, dependencies, or target code as requirement facts.
16
16
 
17
17
  Submit confirmed facts incrementally with the contract `record_*` tools and finish
18
- with exactly one `finalize_contract`. Start recording as soon as a requirement or
18
+ with one successful `finalize_contract`. Complete each input scope with
19
+ `complete_contract_scope` after all its decisions; correct rejected calls and retry. Start recording as soon as a requirement or
19
20
  constraint is confirmed; do not accumulate the whole contract in one response.
20
21
 
21
22
  Missing product semantics may be explicit assumptions only when they do not change