@tea-agent/loop-agent 0.41.0-next.3 → 0.41.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 (200) hide show
  1. package/CHANGELOG.md +264 -32
  2. package/dist/application/dag/generate-task-dag.js +75 -19
  3. package/dist/application/task-lifecycle/advance.js +24 -5
  4. package/dist/application/task-lifecycle/observe.js +171 -17
  5. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +7 -0
  9. package/dist/cli/program.js +5 -0
  10. package/dist/commands/client-recovery.js +3 -0
  11. package/dist/commands/dag-follow-up.js +138 -0
  12. package/dist/commands/init.js +27 -1
  13. package/dist/commands/task-advance.js +19 -0
  14. package/dist/executors/dag-pi-executor.js +3784 -231
  15. package/dist/executors/pi-executor.js +15 -4
  16. package/dist/executors/pi-read-budget-policy.js +239 -0
  17. package/dist/executors/shell-executor.js +945 -217
  18. package/dist/executors/shell-write-guard.js +7 -0
  19. package/dist/shared/dag-failure-category.js +12 -0
  20. package/dist/shared/openspec-spec.js +70 -4
  21. package/dist/shared/operator/capabilities.js +7 -0
  22. package/dist/task/config-types.js +34 -7
  23. package/dist/task/contract/adopt.js +4 -0
  24. package/dist/task/contract/import-revision.js +4 -0
  25. package/dist/task/contract/project.js +3 -0
  26. package/dist/task/contract/schema.js +2 -1
  27. package/dist/task/frontend-project-capability.js +203 -20
  28. package/dist/task/runtime.js +5 -2
  29. package/dist/task/source-prepare/build-draft.js +3 -3
  30. package/dist/task/source-prepare/fragment-inventory.js +15 -9
  31. package/dist/task/source-prepare/prepare.js +89 -1
  32. package/dist/task/source-prepare/semantic-intake.js +6 -2
  33. package/dist/task/source-references.js +22 -1
  34. package/dist/worker/console/chat/pi-runtime.js +2 -3
  35. package/dist/worker/console/chat/routes.js +3 -12
  36. package/dist/worker/console/chat/semantic-activity.js +11 -12
  37. package/dist/worker/console/chat/session-stats.js +0 -96
  38. package/dist/worker/console/chat/turn-process.js +4 -28
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +169 -7
  44. package/dist/worker/console/pi-plugins.js +1 -3
  45. package/dist/worker/console/server.js +3 -0
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
  78. package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
  79. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  105. package/dist/worker/console/static-src/app/useRunProgress.js +2 -9
  106. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
  107. package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
  108. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
  109. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
  110. package/dist/worker/console/workspace-context.js +111 -1
  111. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  112. package/dist/worker/observability/read-model.js +0 -4
  113. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  114. package/dist/worker/observe/static/views/dag-inspector.js +7 -2
  115. package/dist/worker/observe/static/views/session-timeline.js +15 -46
  116. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  117. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  118. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  119. package/dist/workflows/dag/frontend-closeout.js +221 -0
  120. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  121. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  122. package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
  123. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
  125. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  126. package/dist/workflows/dag/frontend-read-budgets.js +12 -0
  127. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  128. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  129. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  130. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  131. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  132. package/dist/workflows/dag/frontend-repair.js +1 -432
  133. package/dist/workflows/dag/frontend-review-context.js +261 -15
  134. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  135. package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
  136. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  137. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  138. package/dist/workflows/dag/frontend-shape.js +427 -0
  139. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  140. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  141. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  142. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  143. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  144. package/dist/workflows/dag/frontend-verification-trace.js +249 -24
  145. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  146. package/dist/workflows/dag/frontend-writer-admission.js +285 -0
  147. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  148. package/dist/workflows/dag/init-hybrid.js +685 -494
  149. package/dist/workflows/dag/interrupt-request.js +7 -0
  150. package/dist/workflows/dag/node-execution.js +732 -4
  151. package/dist/workflows/dag/prompt.js +34 -1
  152. package/dist/workflows/dag/recovery-lease.js +80 -0
  153. package/dist/workflows/dag/report.js +37 -1
  154. package/dist/workflows/dag/rerun-feedback.js +256 -1
  155. package/dist/workflows/dag/rerun-plan.js +10 -2
  156. package/dist/workflows/dag/rerun-run.js +23 -0
  157. package/dist/workflows/dag/rerun-task.js +29 -0
  158. package/dist/workflows/dag/retry-policy.js +214 -104
  159. package/dist/workflows/dag/runner.js +429 -122
  160. package/dist/workflows/dag/scheduler.js +114 -16
  161. package/dist/workflows/dag/types.js +239 -12
  162. package/dist/workflows/dag/validate.js +28 -12
  163. package/docs/init-surface.manifest.json +30 -12
  164. package/docs/skills/vetted-skill-registry.md +4 -2
  165. package/docs/templates/README.md +2 -0
  166. package/docs/templates/agent-dag-report.schema.json +8 -2
  167. package/docs/templates/agent-dag.schema.json +1 -1
  168. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  169. package/docs/templates/frontend-implementation-dag.json +89 -0
  170. package/docs/templates/spec-registry.schema.json +45 -0
  171. package/harness.json +3 -3
  172. package/package.json +1 -1
  173. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  174. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  175. package/skills/frontend-contract/SKILL.md +23 -0
  176. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  177. package/skills/frontend-design-review/SKILL.md +22 -41
  178. package/skills/frontend-plan/SKILL.md +26 -0
  179. package/skills/frontend-plan/references/decision-contract.md +37 -0
  180. package/skills/frontend-plan/references/design-decisions.md +17 -0
  181. package/skills/frontend-review/SKILL.md +20 -15
  182. package/skills/frontend-review/references/review-findings.md +6 -7
  183. package/skills/frontend-scout/SKILL.md +25 -0
  184. package/skills/frontend-scout/references/design-evidence.md +16 -0
  185. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  186. package/skills/frontend-verification/SKILL.md +1 -1
  187. package/skills/loop-agent/references/command-reference.md +1 -0
  188. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  189. package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
  190. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
  191. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
  192. package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
  193. package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
  194. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
  195. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
  196. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
  197. package/skills/frontend-implementation/SKILL.md +0 -52
  198. package/skills/frontend-implementation/references/code-standards.md +0 -33
  199. package/skills/frontend-implementation/references/design-spec.md +0 -56
  200. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -0,0 +1,285 @@
1
+ import { deterministicSha256, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
2
+ import { expandFrontendGlobPaths } from "./frontend-worktree-diff.js";
3
+ import { z } from "zod";
4
+ /** Runtime writer admission evidence. This is the sole production authorization type. */
5
+ export const frontendWriterAdmissionSchema = z
6
+ .object({
7
+ schemaVersion: z.literal(1),
8
+ writerNodeId: z.literal("frontend-implement-pi"),
9
+ decision: z.enum(["authorized", "denied"]),
10
+ sourceArtifact: z.string().min(1),
11
+ artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
12
+ checkedAt: z.string().min(1),
13
+ reason: z.string().nullable(),
14
+ })
15
+ .strict();
16
+ /** A+B (AC-006): four-state writer admission classification. `accepted` is the
17
+ * only authorized state; `requires-human-approval` is the High-topology
18
+ * high-admission-receipt gate; `blocked` is deterministic denial; `stale` is
19
+ * digest drift (source refresh / re-contract). */
20
+ export const frontendWriterAdmissionClassificationSchema = z.enum([
21
+ "accepted",
22
+ "requires-human-approval",
23
+ "blocked",
24
+ "stale",
25
+ // Legacy fixture alias: writeSet-external test helpers still emit `admitted`.
26
+ // Canonical producers always emit one of the four values above.
27
+ "admitted",
28
+ ]);
29
+ /** A+B (AC-006): Micro topology uses `requirement-to-target-and-evidence`
30
+ * coverage; Small/Standard/High use `requirement-to-decision`. The two schemas
31
+ * are deliberately separated so a Micro evidence coverage can never be read as
32
+ * a decision coverage. */
33
+ export const frontendAdmissionCoverageSchema = z.discriminatedUnion("coverageId", [
34
+ z
35
+ .object({
36
+ coverageId: z.literal("requirement-to-target-and-evidence"),
37
+ requirementId: z.string().min(1),
38
+ targetPaths: z.array(z.string().min(1)),
39
+ evidenceRefs: z.array(z.string().min(1)),
40
+ })
41
+ .strict(),
42
+ z
43
+ .object({
44
+ coverageId: z.literal("requirement-to-decision"),
45
+ requirementId: z.string().min(1),
46
+ decisionRefs: z.array(z.string().min(1)),
47
+ })
48
+ .strict(),
49
+ ]);
50
+ /** Failure-source values shared with frontend recovery planning (M8). */
51
+ export const frontendAdmissionFailureSourceSchema = z.enum([
52
+ "frontend-plan-pi",
53
+ "frontend-design-policy-shell",
54
+ "frontend-writer-admission-shell",
55
+ "frontend-implement-pi",
56
+ "frontend-verify-shell",
57
+ ]);
58
+ /**
59
+ * Deterministic artifact schema for `contracts/frontend-writer-admission-result.json`.
60
+ * Scheduler admission gate and frontend recovery planning read this artifact
61
+ * (fail-closed); it replaces the legacy frontend-prewrite-result-v1 as the
62
+ * write authorization source.
63
+ */
64
+ export const frontendWriterAdmissionResultV1Schema = z
65
+ .object({
66
+ schemaVersion: z.literal(1),
67
+ schemaId: z.literal("frontend-writer-admission-shell-v1"),
68
+ classification: frontendWriterAdmissionClassificationSchema,
69
+ writeSet: z.array(z.string().min(1)),
70
+ writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/),
71
+ baselineDigest: z.string().regex(/^[a-f0-9]{64}$/),
72
+ admissionDigest: z.string().regex(/^[a-f0-9]{64}$/),
73
+ baselineDebtPolicy: z.string().min(1).optional(),
74
+ findings: z.array(z
75
+ .object({
76
+ code: z.string().min(1),
77
+ message: z.string().min(1),
78
+ path: z.string().optional(),
79
+ })
80
+ .strict()),
81
+ // r12 policy change: a request_design_changes verdict no longer blocks
82
+ // writer admission. The verdict and its findings ride along as
83
+ // advisory context for the implement node; blocking is owned by the
84
+ // deterministic gates (prewrite, write guard) and the final code
85
+ // review.
86
+ designReviewAdvisory: z
87
+ .object({
88
+ verdict: z.enum(["approve_design", "request_design_changes"]),
89
+ restartPhase: z.string().min(1).optional(),
90
+ findingCount: z.number().int().nonnegative(),
91
+ })
92
+ .strict()
93
+ .optional(),
94
+ failureSource: frontendAdmissionFailureSourceSchema.optional(),
95
+ })
96
+ .strict();
97
+ /** Artifact relative path written by the frontend-writer-admission-shell. */
98
+ export const FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT = "contracts/frontend-writer-admission-result.json";
99
+ function sortFindings(findings) {
100
+ return [...findings].sort((a, b) => {
101
+ if (a.code !== b.code)
102
+ return a.code < b.code ? -1 : 1;
103
+ if (a.message !== b.message)
104
+ return a.message < b.message ? -1 : 1;
105
+ const pathA = a.path ?? "";
106
+ const pathB = b.path ?? "";
107
+ if (pathA !== pathB)
108
+ return pathA < pathB ? -1 : 1;
109
+ return 0;
110
+ });
111
+ }
112
+ /** Reject non-concrete writeSet entries: absolute paths, traversal, globstar,
113
+ * repo-root, and empty/duplicate-slash segments. */
114
+ export function isConcreteWriteSetPath(value) {
115
+ const path = value.trim();
116
+ if (path === "" || path === "." || path === "./")
117
+ return false;
118
+ if (path.startsWith("/") || path.includes("\\"))
119
+ return false;
120
+ const segments = path.split("/");
121
+ if (segments.some((segment) => segment === ".." || segment === "**" || segment === "")) {
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /** Derive the concrete writeSet from implementation targets ∪ non-static
127
+ * verification target files. Entries are de-duplicated and lexicographically
128
+ * sorted (frozen). */
129
+ export function deriveWriteSet(contract, options) {
130
+ const findings = [];
131
+ const candidates = [
132
+ ...contract.targets.files,
133
+ ...contract.verificationTargets
134
+ .filter((target) => target.type !== "static")
135
+ .map((target) => target.file),
136
+ ];
137
+ // Expand glob entries (e.g. `src/**` inherited from the skeleton) against
138
+ // the enumerated workspace files so the derived writeSet contains only
139
+ // concrete paths. Without a file list, globs are treated as concrete and
140
+ // rejected by isConcreteWriteSetPath below (fail-closed).
141
+ const expanded = options?.workspaceFiles
142
+ ? expandFrontendGlobPaths({
143
+ candidates,
144
+ workspaceFiles: options.workspaceFiles,
145
+ })
146
+ : candidates;
147
+ const writeSet = new Set();
148
+ for (const candidate of expanded) {
149
+ if (!isConcreteWriteSetPath(candidate)) {
150
+ findings.push({
151
+ code: "write-set-path-not-concrete",
152
+ message: `writeSet entry is not a concrete path: ${candidate}`,
153
+ path: candidate,
154
+ });
155
+ continue;
156
+ }
157
+ writeSet.add(candidate);
158
+ }
159
+ return { writeSet: [...writeSet].sort(), findings };
160
+ }
161
+ /** Sort and freeze the injected baseline; duplicate paths and empty baselines
162
+ * are blocking (collection itself is production wiring, not this shell). */
163
+ export function freezeBaseline(baseline) {
164
+ const findings = [];
165
+ if (baseline.length === 0) {
166
+ findings.push({
167
+ code: "baseline-empty",
168
+ message: "writer admission baseline is empty",
169
+ });
170
+ }
171
+ const seen = new Set();
172
+ for (const entry of baseline) {
173
+ if (seen.has(entry.path)) {
174
+ findings.push({
175
+ code: "baseline-duplicate-path",
176
+ message: `baseline contains duplicate path: ${entry.path}`,
177
+ path: entry.path,
178
+ });
179
+ }
180
+ seen.add(entry.path);
181
+ }
182
+ const sorted = [...baseline].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
183
+ return { baselineDigest: deterministicSha256(sorted), findings };
184
+ }
185
+ /** Combine mock strategy, allowed strategies digest, frozen command digest,
186
+ * production-default-off flag and real-integration expectation into one
187
+ * deterministic admission digest. */
188
+ export function deriveAdmissionDigest(contract, allowedMockStrategies, frozenCommandLabels, baselineDebtPolicy) {
189
+ return deterministicSha256({
190
+ mockApiStrategy: contract.mockApi.strategy,
191
+ allowedMockStrategiesDigest: deterministicSha256([...allowedMockStrategies].sort()),
192
+ frozenCommandLabelsDigest: deterministicSha256([...frozenCommandLabels].sort()),
193
+ productionDefaultOff: contract.mockApi.productionDefaultOff === true,
194
+ hasRealIntegrationGap: Boolean(contract.realIntegrationGap),
195
+ baselineDebtPolicy: baselineDebtPolicy ?? "",
196
+ });
197
+ }
198
+ /** Shape-aware deterministic admission: re-validate the typed contract, honor
199
+ * the design-policy result, derive/freeze the writeSet, digest the baseline,
200
+ * and assemble the admission digest. */
201
+ export function deriveFrontendWriterAdmissionShell(input) {
202
+ const findings = [];
203
+ const shape = frontendImplementationContractSchema.safeParse(input.contract);
204
+ if (!shape.success) {
205
+ findings.push({
206
+ code: "contract-shape-invalid",
207
+ message: `contract fails strict shape validation: ${shape.error.issues[0]?.message ?? "unknown issue"}`,
208
+ path: shape.error.issues[0]?.path.join(".") || undefined,
209
+ });
210
+ }
211
+ if (input.designPolicyResult.classification === "blocked") {
212
+ findings.push({
213
+ code: "design-policy-blocked",
214
+ message: `design policy is blocked with ${input.designPolicyResult.findings.length} finding(s)`,
215
+ });
216
+ }
217
+ const derived = deriveWriteSet(input.contract, {
218
+ workspaceFiles: input.workspaceFiles,
219
+ });
220
+ findings.push(...derived.findings);
221
+ const baseline = freezeBaseline(input.baseline);
222
+ findings.push(...baseline.findings);
223
+ const sortedFindings = sortFindings(findings);
224
+ // Deterministic admission: any finding blocks. High-risk contracts keep the
225
+ // full policy/writeSet/baseline gates but do NOT stop for a human approval
226
+ // step — the DAG runs unattended, so `requires-human-approval` would strand
227
+ // every high-risk writer. The receipt is still materialized for audit; the
228
+ // scheduler authorizes `accepted` directly.
229
+ const classification = sortedFindings.length > 0 ? "blocked" : "accepted";
230
+ const result = {
231
+ schemaId: "frontend-writer-admission-shell-v1",
232
+ classification,
233
+ writeSet: derived.writeSet,
234
+ writeSetDigest: deterministicSha256(derived.writeSet),
235
+ baselineDigest: baseline.baselineDigest,
236
+ admissionDigest: deriveAdmissionDigest(input.contract, input.allowedMockStrategies, input.frozenCommandLabels, input.baselineDebtPolicy),
237
+ ...(input.baselineDebtPolicy !== undefined
238
+ ? { baselineDebtPolicy: input.baselineDebtPolicy }
239
+ : {}),
240
+ findings: sortedFindings,
241
+ };
242
+ return result;
243
+ }
244
+ function normalizeNewVerdict(classification) {
245
+ return classification === "accepted" ||
246
+ classification === "requires-human-approval" ||
247
+ classification === "admitted"
248
+ ? "accepted"
249
+ : "blocked";
250
+ }
251
+ function normalizeLegacyVerdict(classification) {
252
+ return classification === "accepted" || classification === "accepted-normalized"
253
+ ? "accepted"
254
+ : "blocked";
255
+ }
256
+ export function compareAdmissionToLegacyGate(input) {
257
+ const newVerdict = normalizeNewVerdict(input.newClassification);
258
+ const legacyVerdict = normalizeLegacyVerdict(input.legacyClassification);
259
+ return {
260
+ newClassification: input.newClassification,
261
+ legacyClassification: input.legacyClassification,
262
+ newVerdict,
263
+ legacyVerdict,
264
+ match: newVerdict === legacyVerdict,
265
+ };
266
+ }
267
+ /** Render a stable, deterministically ordered mismatch list for the evaluation
268
+ * set. Input order is irrelevant: rows are sorted by a canonical key. */
269
+ export function renderAdmissionShadowDiff(results) {
270
+ const indexed = results.map((result, index) => ({ ...result, index }));
271
+ const sorted = indexed.sort((a, b) => {
272
+ const keyA = `${a.legacyClassification}|${a.newClassification}`;
273
+ const keyB = `${b.legacyClassification}|${b.newClassification}`;
274
+ if (keyA !== keyB)
275
+ return keyA < keyB ? -1 : 1;
276
+ return a.index - b.index;
277
+ });
278
+ const mismatches = sorted.filter((row) => !row.match);
279
+ const lines = [
280
+ "## Admission Shadow Diff",
281
+ `${sorted.length} comparisons, ${mismatches.length} mismatches`,
282
+ ...sorted.map((row) => `- ${row.legacyClassification} -> ${row.newClassification}: legacy=${row.legacyVerdict} new=${row.newVerdict}${row.match ? " match" : " MISMATCH"}`),
283
+ ];
284
+ return lines.join("\n");
285
+ }
@@ -0,0 +1,256 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ /**
4
+ * M4: fact-derived frontend writer status + bounded in-node micro-loop.
5
+ *
6
+ * `frontend-implement-pi` derives its implementation status from mechanical
7
+ * facts (write-tool events, run delta, cumulative diff, write guard, requirement
8
+ * coverage, focused-check failures) instead of parsing the
9
+ * `IMPLEMENTATION_OUTCOME:` first line. The legacy `implementation-outcome-v1`
10
+ * protocol (backend/README/module writers + `frontend-repair-pi`) is untouched.
11
+ *
12
+ * Everything here is either a pure function (deterministically testable) or an
13
+ * IO collector that is only reached through a dynamic import from the Pi
14
+ * executor, avoiding an executor↔workflows static cycle.
15
+ */
16
+ /**
17
+ * Write/edit-style tool names emitted by DAG writer customTools. Kept in sync
18
+ * with the `SDK_WRITE_TOOL_NAMES` set in pi-sdk-executor.ts so the persisted
19
+ * session-event count and the in-memory writeToolCallCount agree.
20
+ */
21
+ export const FRONTEND_WRITE_TOOL_NAMES = new Set([
22
+ "edit",
23
+ "write",
24
+ "str_replace_editor",
25
+ ]);
26
+ /**
27
+ * AC-001/AC-003: derive the writer status from mechanical facts only. The
28
+ * first-line text is never consulted. Ordering:
29
+ * 1. write-guard violation → blocked
30
+ * 2. focused-check failure → needs-repair (including trace preflight gaps)
31
+ * 3. non-empty diff → changed (diff is the mechanical fact source)
32
+ * 4. empty diff + runtime already-satisfied evidence → already-satisfied
33
+ * 5. empty diff, zero writes, zero diff → blocked (no progress)
34
+ * 6. otherwise → empty-diff (fail-closed)
35
+ */
36
+ export function deriveFrontendWriterStatus(facts) {
37
+ if (!facts.writeGuardOk) {
38
+ return {
39
+ status: "blocked",
40
+ reason: `write guard violations: ${facts.writeGuardViolations.join(", ") || "unknown"}`,
41
+ };
42
+ }
43
+ if (facts.focusedCheckFailures.length > 0) {
44
+ const failures = facts.focusedCheckFailures
45
+ .map((failure) => failure.targetId
46
+ ? `${failure.targetId}: ${failure.detail}`
47
+ : failure.detail)
48
+ .join("; ");
49
+ return {
50
+ status: "needs-repair",
51
+ reason: `focused checks require repair: ${failures}`,
52
+ };
53
+ }
54
+ if (facts.changedFiles.length > 0) {
55
+ return {
56
+ status: "changed",
57
+ reason: `non-empty run-attributed diff (${facts.changedFiles.length} file${facts.changedFiles.length === 1 ? "" : "s"})`,
58
+ };
59
+ }
60
+ if (facts.alreadySatisfiedEvidence) {
61
+ return {
62
+ status: "already-satisfied",
63
+ reason: `empty diff with runtime already-satisfied evidence (${facts.alreadySatisfiedEvidence.requirementTargets.length} target${facts.alreadySatisfiedEvidence.requirementTargets.length === 1 ? "" : "s"})`,
64
+ };
65
+ }
66
+ if (facts.writeToolEvents === 0 && facts.changedFiles.length === 0) {
67
+ return {
68
+ status: "blocked",
69
+ reason: "zero write-tool events and zero diff: no implementation progress",
70
+ };
71
+ }
72
+ return {
73
+ status: "empty-diff",
74
+ reason: "empty diff without runtime already-satisfied evidence (fail-closed)",
75
+ };
76
+ }
77
+ /**
78
+ * Deterministic failure fingerprint over the normalized failure signature:
79
+ * category + focused-check failures (target + detail) + guard status. Used for
80
+ * repeated-failure deduplication inside the bounded micro-loop.
81
+ */
82
+ export function computeFailureFingerprint(facts) {
83
+ const signature = normalizeFailureSignature(facts);
84
+ return createHash("sha256").update(signature).digest("hex");
85
+ }
86
+ function normalizeFailureSignature(facts) {
87
+ const parts = [];
88
+ if (!facts.writeGuardOk) {
89
+ parts.push("write-guard");
90
+ }
91
+ for (const failure of facts.focusedCheckFailures) {
92
+ parts.push(`${failure.category}:${failure.targetId ?? ""}:${failure.detail}`);
93
+ }
94
+ if (facts.changedFiles.length === 0 && facts.writeToolEvents === 0) {
95
+ parts.push("no-progress");
96
+ }
97
+ return parts.sort().join("\n");
98
+ }
99
+ export const DEFAULT_FRONTEND_MICRO_LOOP_BUDGET = {
100
+ maxFocusedCheckRounds: 3,
101
+ maxWriteAttempts: 5,
102
+ maxWallTimeMs: 15 * 60 * 1000,
103
+ maxTokens: 200_000,
104
+ maxRepeatedFingerprints: 2,
105
+ };
106
+ /**
107
+ * Pure micro-loop continuation gate. Deterministic: same budget + history +
108
+ * timestamps always yield the same decision. The caller (executor) drives the
109
+ * loop by re-checking this function after each focused-check round.
110
+ */
111
+ export function evaluateMicroLoopContinuation(budget, history, startWallTimeMs, nowWallTimeMs) {
112
+ const rounds = history.length;
113
+ const last = history[rounds - 1];
114
+ if (last) {
115
+ const resolved = (last.status === "changed" &&
116
+ last.writeGuardOk &&
117
+ last.focusedCheckFailures.length === 0) ||
118
+ last.status === "already-satisfied" ||
119
+ last.status === "blocked";
120
+ if (resolved) {
121
+ return { continue: false, reason: "status-resolved" };
122
+ }
123
+ if (last.writeAttempts >= budget.maxWriteAttempts) {
124
+ return { continue: false, reason: "budget-write-attempts" };
125
+ }
126
+ if (last.tokensUsed >= budget.maxTokens) {
127
+ return { continue: false, reason: "budget-tokens" };
128
+ }
129
+ if (nowWallTimeMs - startWallTimeMs >= budget.maxWallTimeMs) {
130
+ return { continue: false, reason: "budget-wall-time" };
131
+ }
132
+ const repeatCount = history.filter((round) => round.fingerprint === last.fingerprint).length;
133
+ if (repeatCount >= budget.maxRepeatedFingerprints) {
134
+ return { continue: false, reason: "budget-repeated-fingerprint" };
135
+ }
136
+ }
137
+ if (rounds >= budget.maxFocusedCheckRounds) {
138
+ return { continue: false, reason: "budget-focused-check-rounds" };
139
+ }
140
+ return { continue: true };
141
+ }
142
+ /**
143
+ * Build a deterministic per-round checkpoint payload. Persistence stays with the
144
+ * caller (executor writes `runDir/<nodeId>/` only, never root `artifacts/**`).
145
+ */
146
+ export function buildFrontendMicroLoopCheckpoint(input) {
147
+ return {
148
+ schemaVersion: 1,
149
+ nodeId: input.nodeId,
150
+ round: input.round,
151
+ status: deriveFrontendWriterStatus(input.facts).status,
152
+ fingerprint: computeFailureFingerprint(input.facts),
153
+ changedFiles: [...input.facts.changedFiles].sort(),
154
+ focusedCheckFailures: input.facts.focusedCheckFailures.map((failure) => ({
155
+ ...failure,
156
+ })),
157
+ writeGuardOk: input.facts.writeGuardOk,
158
+ recordedAt: input.recordedAt,
159
+ wallTimeMs: input.facts.wallTimeMs,
160
+ tokensUsed: input.facts.tokensUsed,
161
+ writeAttempts: input.facts.writeAttempts,
162
+ };
163
+ }
164
+ /**
165
+ * Shadow comparison between the fact-derived status and the legacy
166
+ * `IMPLEMENTATION_OUTCOME:` first-line parse. Never used to decide ok/fail —
167
+ * only persisted for the M4 shadow evaluation.
168
+ */
169
+ export function compareFactStatusToLegacyOutcome(factStatus, legacyOutcome) {
170
+ const factVerdict = factStatus === "changed" || factStatus === "already-satisfied"
171
+ ? "ok"
172
+ : "fail";
173
+ const legacyVerdict = legacyOutcome === "changed" || legacyOutcome === "already-satisfied"
174
+ ? "ok"
175
+ : "fail";
176
+ return {
177
+ factStatus,
178
+ legacyOutcome,
179
+ factVerdict,
180
+ legacyVerdict,
181
+ match: factVerdict === legacyVerdict,
182
+ };
183
+ }
184
+ /**
185
+ * Pure session-event scanner: count write-tool events from the persisted
186
+ * session-events.jsonl content. Fail-closed: an unparseable line never counts
187
+ * as a write, so a corrupt/empty log cannot inflate progress.
188
+ */
189
+ export function countWriteToolEventsFromSessionEvents(content) {
190
+ let count = 0;
191
+ for (const line of content.split("\n")) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed)
194
+ continue;
195
+ let event;
196
+ try {
197
+ event = JSON.parse(trimmed);
198
+ }
199
+ catch {
200
+ continue;
201
+ }
202
+ if (event !== null &&
203
+ typeof event === "object" &&
204
+ event.type === "tool_execution_start" &&
205
+ typeof event.toolName === "string" &&
206
+ FRONTEND_WRITE_TOOL_NAMES.has(event.toolName)) {
207
+ count += 1;
208
+ }
209
+ }
210
+ return count;
211
+ }
212
+ /**
213
+ * IO collector for frontend facts. Reads only the persisted session-events
214
+ * file; every other fact is passed in by the executor so this stays narrow and
215
+ * deterministic under test (tests inject session content by writing to a temp
216
+ * path or call the pure scanner directly).
217
+ */
218
+ export async function collectFrontendWriterFacts(input) {
219
+ let writeToolEvents = 0;
220
+ try {
221
+ const content = await readFile(input.sessionEventsPath, "utf8");
222
+ writeToolEvents = countWriteToolEventsFromSessionEvents(content);
223
+ }
224
+ catch {
225
+ // Missing/unreadable session log → fail-closed at zero, never fail-open.
226
+ writeToolEvents = 0;
227
+ }
228
+ return {
229
+ changedFiles: [...input.changedFiles].sort(),
230
+ writeToolEvents,
231
+ writeGuardOk: input.writeGuard.ok,
232
+ writeGuardViolations: [...input.writeGuard.violations],
233
+ requirementTargets: [...new Set(input.requirementTargets)].sort(),
234
+ coveredRequirementIds: [...new Set(input.coveredRequirementIds)].sort(),
235
+ focusedCheckFailures: input.focusedCheckFailures.map((failure) => ({
236
+ ...failure,
237
+ })),
238
+ ...(input.alreadySatisfiedEvidence
239
+ ? {
240
+ alreadySatisfiedEvidence: {
241
+ requirementTargets: [
242
+ ...input.alreadySatisfiedEvidence.requirementTargets,
243
+ ].sort(),
244
+ proofSha256: input.alreadySatisfiedEvidence.proofSha256,
245
+ },
246
+ }
247
+ : {}),
248
+ tokensUsed: input.tokensUsed ?? 0,
249
+ wallTimeMs: input.wallTimeMs ?? 0,
250
+ rounds: input.rounds ?? 1,
251
+ writeAttempts: input.writeAttempts ?? 1,
252
+ ...(input.firstLineText !== undefined
253
+ ? { firstLineText: input.firstLineText }
254
+ : {}),
255
+ };
256
+ }