@tea-agent/loop-agent 0.38.0 → 0.38.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 (147) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/init-upgrade.js +97 -19
  5. package/dist/executors/dag-pi-executor.js +42 -4
  6. package/dist/executors/shell-executor.js +269 -15
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +149 -27
  10. package/dist/task/config-types.js +43 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/deferred-turn.js +91 -0
  13. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  14. package/dist/worker/console/chat/pi-runtime.js +165 -83
  15. package/dist/worker/console/chat/repo-walk.js +10 -1
  16. package/dist/worker/console/chat/routes.js +477 -55
  17. package/dist/worker/console/chat/semantic-activity.js +20 -8
  18. package/dist/worker/console/chat/session-store.js +146 -2
  19. package/dist/worker/console/chat/turn-process.js +17 -30
  20. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  21. package/dist/worker/console/operator-actions.js +128 -4
  22. package/dist/worker/console/operator-user-error.js +19 -0
  23. package/dist/worker/console/pi-readiness.js +8 -1
  24. package/dist/worker/console/recovery-cta.js +4 -4
  25. package/dist/worker/console/recovery-error-copy.js +4 -4
  26. package/dist/worker/console/security.js +20 -1
  27. package/dist/worker/console/server.js +6 -1
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  60. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  61. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/app/console-types.js +39 -0
  87. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  88. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  91. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  93. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  95. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  96. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  97. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  99. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  100. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  101. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  102. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  103. package/dist/workflows/dag/backend-test-layout.js +133 -0
  104. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  105. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  106. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  107. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  108. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  109. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  110. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  111. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  112. package/dist/workflows/dag/convergence/controller.js +141 -0
  113. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  114. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  115. package/dist/workflows/dag/failure-routing.js +9 -4
  116. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  117. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
  119. package/dist/workflows/dag/init-hybrid.js +920 -110
  120. package/dist/workflows/dag/lifecycle.js +4 -0
  121. package/dist/workflows/dag/node-execution.js +77 -8
  122. package/dist/workflows/dag/prompt.js +45 -0
  123. package/dist/workflows/dag/report.js +6 -0
  124. package/dist/workflows/dag/rerun-feedback.js +212 -0
  125. package/dist/workflows/dag/rerun-plan.js +93 -27
  126. package/dist/workflows/dag/rerun-run.js +3 -0
  127. package/dist/workflows/dag/rerun-task.js +61 -2
  128. package/dist/workflows/dag/retry-policy.js +33 -0
  129. package/dist/workflows/dag/types.js +148 -4
  130. package/dist/workflows/dag/validate.js +7 -2
  131. package/docs/templates/backend-test-dag.json +60 -35
  132. package/docs/templates/frontend-design-contract.md +40 -14
  133. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  134. package/docs/templates/frontend-task-constraints.md +40 -13
  135. package/docs/templates/frontend-task-requirement.md +54 -20
  136. package/harness.json +3 -1
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  139. package/skills/frontend-implementation/references/design-spec.md +16 -19
  140. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  141. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  142. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  144. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  145. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -5,7 +5,73 @@
5
5
  */
6
6
  import { readFile, readdir, stat } from "node:fs/promises";
7
7
  import path from "node:path";
8
- import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE } from "../shared/openspec-spec.js";
8
+ import { z } from "zod";
9
+ import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, isOpenspecSpecFilePath, } from "../shared/openspec-spec.js";
10
+ const classifiedPathSchema = z
11
+ .string()
12
+ .refine((candidate) => isOpenspecSpecFilePath(candidate), "classified path must be a repo-relative supported spec file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
13
+ /**
14
+ * Stable semantic partition of `designEvidence.normativePaths`. Keys are a
15
+ * generic directory convention (not project-specific file names); the same
16
+ * repository therefore yields the same classification on repeated probes.
17
+ */
18
+ export const frontendDesignClassifiedSchema = z
19
+ .object({
20
+ schemas: z.array(classifiedPathSchema),
21
+ codeTemplate: z.array(classifiedPathSchema),
22
+ rule: z
23
+ .object({
24
+ api: z.array(classifiedPathSchema),
25
+ mock: z.array(classifiedPathSchema),
26
+ router: z.array(classifiedPathSchema),
27
+ hooks: z.array(classifiedPathSchema),
28
+ utils: z.array(classifiedPathSchema),
29
+ components: z.array(classifiedPathSchema),
30
+ other: z.array(classifiedPathSchema),
31
+ })
32
+ .strict(),
33
+ theme: z.array(classifiedPathSchema),
34
+ component: z.array(classifiedPathSchema),
35
+ uiOther: z.array(classifiedPathSchema),
36
+ advisoryOther: z.array(classifiedPathSchema),
37
+ })
38
+ .strict();
39
+ export const frontendDesignEvidenceSchema = z
40
+ .object({
41
+ normativePaths: z.array(z.string()),
42
+ advisoryPaths: z.array(z.string()),
43
+ conflicts: z.array(z.string()),
44
+ classified: frontendDesignClassifiedSchema,
45
+ })
46
+ .strict()
47
+ .superRefine((evidence, ctx) => {
48
+ const classified = [
49
+ ...evidence.classified.schemas,
50
+ ...evidence.classified.codeTemplate,
51
+ ...evidence.classified.rule.api,
52
+ ...evidence.classified.rule.mock,
53
+ ...evidence.classified.rule.router,
54
+ ...evidence.classified.rule.hooks,
55
+ ...evidence.classified.rule.utils,
56
+ ...evidence.classified.rule.components,
57
+ ...evidence.classified.rule.other,
58
+ ...evidence.classified.theme,
59
+ ...evidence.classified.component,
60
+ ...evidence.classified.uiOther,
61
+ ...evidence.classified.advisoryOther,
62
+ ];
63
+ const classifiedSet = new Set(classified);
64
+ const normativeSet = new Set(evidence.normativePaths);
65
+ const onlyClassified = [...classifiedSet].filter((candidate) => !normativeSet.has(candidate));
66
+ const onlyNormative = [...normativeSet].filter((candidate) => !classifiedSet.has(candidate));
67
+ if (onlyClassified.length > 0 || onlyNormative.length > 0) {
68
+ ctx.addIssue({
69
+ code: z.ZodIssueCode.custom,
70
+ message: `classified must partition normativePaths exactly; extra in classified: ${onlyClassified.join(", ") || "(none)"}; missing from classified: ${onlyNormative.join(", ") || "(none)"}`,
71
+ path: ["classified"],
72
+ });
73
+ }
74
+ });
9
75
  async function exists(filePath) {
10
76
  try {
11
77
  await stat(filePath);
@@ -15,39 +81,68 @@ async function exists(filePath) {
15
81
  return false;
16
82
  }
17
83
  }
18
- async function readJson(filePath) {
19
- try {
20
- return JSON.parse(await readFile(filePath, "utf8"));
21
- }
22
- catch {
23
- return null;
84
+ /**
85
+ * Resolve the governance subtree root from harness.json.governanceRoot.
86
+ * Falls back to DEFAULT_OPENSPEC_GOVERNANCE_ROOT when the manifest is missing,
87
+ * unreadable, malformed, or lacks a string governanceRoot. This intentionally
88
+ * does NOT apply init.ts's "docs" -> default migration (init-only behavior).
89
+ */
90
+ export async function resolveOpenspecGovernanceRoot(repoRoot) {
91
+ const manifest = await readJson(path.join(repoRoot, "harness.json"));
92
+ if (manifest && typeof manifest === "object") {
93
+ const candidate = manifest.governanceRoot;
94
+ if (typeof candidate === "string" && candidate.trim()) {
95
+ return candidate.trim().replace(/\\/g, "/").replace(/\/+$/, "");
96
+ }
24
97
  }
98
+ return DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
25
99
  }
26
- function allDeps(pkg) {
27
- return {
28
- ...(pkg.dependencies ?? {}),
29
- ...(pkg.devDependencies ?? {}),
30
- ...(pkg.peerDependencies ?? {}),
100
+ export async function listOpenspecDiscovery(repoRoot, options = {}) {
101
+ const governanceRoot = options.governanceRootOverride ??
102
+ (await resolveOpenspecGovernanceRoot(repoRoot));
103
+ const extraExcludedPrefixes = (options.extraExcludedPrefixes ?? [])
104
+ .map((prefix) => prefix.replace(/\\/g, "/").replace(/\/+$/, ""))
105
+ .filter(Boolean);
106
+ const collected = [];
107
+ let excludedGovernanceCount = 0;
108
+ const isExcluded = (rel) => {
109
+ if (isGovernanceSubtreePath(rel, governanceRoot))
110
+ return true;
111
+ return extraExcludedPrefixes.some((prefix) => rel === prefix || rel.startsWith(prefix + "/"));
31
112
  };
32
- }
33
- function hasDep(deps, name) {
34
- return Object.hasOwn(deps, name);
35
- }
36
- async function listOpenspec(repoRoot) {
37
- const out = [];
38
113
  for (const specDir of OPENSPEC_SPEC_DIRS) {
39
114
  const root = path.join(repoRoot, specDir);
40
115
  if (!(await exists(root)))
41
116
  continue;
42
117
  await walk(root, 0);
43
118
  }
44
- return out.slice(0, 40);
119
+ // openspec/schemas/** and openspec/project-specs/** sort first; remaining
120
+ // ai_workspace/** business files follow. Dedupe preserves discovery order.
121
+ const unique = [...new Set(collected)];
122
+ const openspecPaths = unique
123
+ .filter((candidate) => candidate.startsWith("openspec/"))
124
+ .sort();
125
+ const aiWorkspacePaths = unique
126
+ .filter((candidate) => candidate.startsWith("ai_workspace/"))
127
+ .sort();
128
+ const deduped = [...openspecPaths, ...aiWorkspacePaths];
129
+ const truncated = deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
130
+ const paths = truncated
131
+ ? deduped.slice(0, MAX_OPENSPEC_CANDIDATE_PATHS)
132
+ : deduped;
133
+ return {
134
+ paths,
135
+ governanceRoot,
136
+ excludedGovernanceCount,
137
+ truncated,
138
+ maxCandidates: MAX_OPENSPEC_CANDIDATE_PATHS,
139
+ };
45
140
  async function walk(dir, depth) {
46
141
  if (depth > 4)
47
142
  return;
48
143
  let entries = [];
49
144
  try {
50
- entries = await readdir(dir);
145
+ entries = (await readdir(dir)).sort();
51
146
  }
52
147
  catch {
53
148
  return;
@@ -59,7 +154,13 @@ async function listOpenspec(repoRoot) {
59
154
  if (info.isDirectory())
60
155
  await walk(full, depth + 1);
61
156
  else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
62
- out.push(path.relative(repoRoot, full).replace(/\\/g, "/"));
157
+ const rel = path.relative(repoRoot, full).replace(/\\/g, "/");
158
+ if (isExcluded(rel)) {
159
+ excludedGovernanceCount++;
160
+ }
161
+ else {
162
+ collected.push(rel);
163
+ }
63
164
  }
64
165
  }
65
166
  catch {
@@ -68,6 +169,164 @@ async function listOpenspec(repoRoot) {
68
169
  }
69
170
  }
70
171
  }
172
+ /**
173
+ * Count spec-extension files under a governance subtree that candidate
174
+ * discovery would exclude. Bounded, read-only diagnostic helper used by the
175
+ * prewrite gate to reproduce exclusion stats at runtime.
176
+ */
177
+ export async function countExcludedGovernanceSpecFiles(repoRoot, governanceRoot) {
178
+ const root = path.join(repoRoot, governanceRoot);
179
+ if (!(await exists(root)))
180
+ return 0;
181
+ let count = 0;
182
+ await walkGovernance(root, 0);
183
+ return count;
184
+ async function walkGovernance(dir, depth) {
185
+ if (depth > 4)
186
+ return;
187
+ let entries = [];
188
+ try {
189
+ entries = (await readdir(dir)).sort();
190
+ }
191
+ catch {
192
+ return;
193
+ }
194
+ for (const name of entries) {
195
+ const full = path.join(dir, name);
196
+ try {
197
+ const info = await stat(full);
198
+ if (info.isDirectory())
199
+ await walkGovernance(full, depth + 1);
200
+ else if (OPENSPEC_SPEC_EXT_RE.test(name))
201
+ count++;
202
+ }
203
+ catch {
204
+ // skip
205
+ }
206
+ }
207
+ }
208
+ }
209
+ async function readJson(filePath) {
210
+ try {
211
+ return JSON.parse(await readFile(filePath, "utf8"));
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ }
217
+ function allDeps(pkg) {
218
+ return {
219
+ ...(pkg.dependencies ?? {}),
220
+ ...(pkg.devDependencies ?? {}),
221
+ ...(pkg.peerDependencies ?? {}),
222
+ };
223
+ }
224
+ function hasDep(deps, name) {
225
+ return Object.hasOwn(deps, name);
226
+ }
227
+ function emptyClassified() {
228
+ return {
229
+ schemas: [],
230
+ codeTemplate: [],
231
+ rule: { api: [], mock: [], router: [], hooks: [], utils: [], components: [], other: [] },
232
+ theme: [],
233
+ component: [],
234
+ uiOther: [],
235
+ advisoryOther: [],
236
+ };
237
+ }
238
+ /**
239
+ * Classify discovered openspec paths by generic directory convention. Only
240
+ * `isOpenspecSpecFilePath` paths enter a bucket; the classifier never invents
241
+ * project-specific file names. Basename stems use word-boundary matching so
242
+ * e.g. `*api*` (case-insensitive) maps to `rule.api` without hardcoding names.
243
+ */
244
+ function classifyOpenspecPaths(paths) {
245
+ const classified = emptyClassified();
246
+ const ruleStem = (basename) => {
247
+ const lower = basename.toLowerCase();
248
+ const stems = [
249
+ ["api", /\bapi\b/],
250
+ ["mock", /\bmock\b/],
251
+ ["router", /\brouter\b/],
252
+ ["hooks", /\bhooks?\b/],
253
+ ["utils", /\butils?\b/],
254
+ ["components", /\bcomponents?\b/],
255
+ ];
256
+ for (const [key, re] of stems) {
257
+ if (re.test(lower))
258
+ return key;
259
+ }
260
+ return "other";
261
+ };
262
+ for (const candidate of paths) {
263
+ if (!isOpenspecSpecFilePath(candidate))
264
+ continue;
265
+ const segments = candidate.split("/");
266
+ const basename = segments[segments.length - 1] ?? "";
267
+ if (candidate.startsWith("openspec/schemas/")) {
268
+ classified.schemas.push(candidate);
269
+ }
270
+ else if (candidate.startsWith("openspec/project-specs/templates/")) {
271
+ classified.codeTemplate.push(candidate);
272
+ }
273
+ else if (candidate.startsWith("openspec/project-specs/rules/")) {
274
+ classified.rule[ruleStem(basename)].push(candidate);
275
+ }
276
+ else if (candidate.startsWith("openspec/project-specs/ui/")) {
277
+ const lower = basename.toLowerCase();
278
+ if (/\btheme\b/.test(lower))
279
+ classified.theme.push(candidate);
280
+ else if (/\bcomponents?\b/.test(lower))
281
+ classified.component.push(candidate);
282
+ else
283
+ classified.uiOther.push(candidate);
284
+ }
285
+ else {
286
+ classified.advisoryOther.push(candidate);
287
+ }
288
+ }
289
+ const sortBucket = (bucket) => bucket.sort();
290
+ classified.schemas = sortBucket(classified.schemas);
291
+ classified.codeTemplate = sortBucket(classified.codeTemplate);
292
+ for (const key of [
293
+ "api",
294
+ "mock",
295
+ "router",
296
+ "hooks",
297
+ "utils",
298
+ "components",
299
+ "other",
300
+ ]) {
301
+ classified.rule[key] = sortBucket(classified.rule[key]);
302
+ }
303
+ classified.theme = sortBucket(classified.theme);
304
+ classified.component = sortBucket(classified.component);
305
+ classified.uiOther = sortBucket(classified.uiOther);
306
+ classified.advisoryOther = sortBucket(classified.advisoryOther);
307
+ return classified;
308
+ }
309
+ function buildDesignEvidence(openspec) {
310
+ // Preserve discovery priority order (openspec first, then ai_workspace)
311
+ // instead of re-sorting; the exact-partition schema identity is set-based.
312
+ const normativePaths = [...new Set(openspec)];
313
+ return frontendDesignEvidenceSchema.parse({
314
+ normativePaths,
315
+ advisoryPaths: [],
316
+ conflicts: [],
317
+ classified: classifyOpenspecPaths(normativePaths),
318
+ });
319
+ }
320
+ function toOpenspecDiscovery(discovery, designEvidence) {
321
+ return {
322
+ source: "auto-discovered",
323
+ governanceRoot: discovery.governanceRoot,
324
+ candidateCount: designEvidence.normativePaths.length,
325
+ excludedGovernanceCount: discovery.excludedGovernanceCount,
326
+ truncated: discovery.truncated,
327
+ maxCandidates: discovery.maxCandidates,
328
+ };
329
+ }
71
330
  export function buildAdapterGuidance(capability) {
72
331
  const lines = [
73
332
  "## Frontend project capability (generation-time)",
@@ -108,7 +367,8 @@ export async function discoverFrontendProjectCapability(repoRoot) {
108
367
  const pkgPath = path.join(repoRoot, "package.json");
109
368
  const pkgRaw = await readJson(pkgPath);
110
369
  if (!pkgRaw || typeof pkgRaw !== "object") {
111
- const openspec = await listOpenspec(repoRoot);
370
+ const discovery = await listOpenspecDiscovery(repoRoot);
371
+ const designEvidence = buildDesignEvidence(discovery.paths);
112
372
  const base = {
113
373
  schemaVersion: 1,
114
374
  framework: "unknown",
@@ -119,16 +379,13 @@ export async function discoverFrontendProjectCapability(repoRoot) {
119
379
  testRunner: [],
120
380
  mock: [],
121
381
  a11y: { status: "unknown", tools: [], evidencePaths: [] },
122
- evidencePaths: openspec.slice(0, 5),
382
+ evidencePaths: discovery.paths.slice(0, 5),
123
383
  reasons: ["package.json missing or unreadable"],
124
- designEvidence: {
125
- normativePaths: openspec,
126
- advisoryPaths: [],
127
- conflicts: [],
128
- },
384
+ designEvidence,
385
+ openspecDiscovery: toOpenspecDiscovery(discovery, designEvidence),
129
386
  };
130
- if (openspec.length) {
131
- base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
387
+ if (discovery.paths.length) {
388
+ base.reasons.push(`openspec spec files discovered: ${discovery.paths.slice(0, 5).join(", ")}`);
132
389
  }
133
390
  return { ...base, adapterGuidance: buildAdapterGuidance(base) };
134
391
  }
@@ -283,14 +540,10 @@ export async function discoverFrontendProjectCapability(repoRoot) {
283
540
  router = router ?? "next-router";
284
541
  if (hasDep(deps, "vue-router"))
285
542
  router = "vue-router";
286
- const openspec = await listOpenspec(repoRoot);
287
- const designEvidence = {
288
- normativePaths: openspec,
289
- advisoryPaths: [],
290
- conflicts: [],
291
- };
292
- if (openspec.length)
293
- evidencePaths.push(...openspec.slice(0, 5));
543
+ const discovery = await listOpenspecDiscovery(repoRoot);
544
+ const designEvidence = buildDesignEvidence(discovery.paths);
545
+ if (discovery.paths.length)
546
+ evidencePaths.push(...discovery.paths.slice(0, 5));
294
547
  const base = {
295
548
  schemaVersion: 1,
296
549
  framework,
@@ -311,6 +564,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
311
564
  evidencePaths: [...new Set(evidencePaths)],
312
565
  reasons,
313
566
  designEvidence,
567
+ openspecDiscovery: toOpenspecDiscovery(discovery, designEvidence),
314
568
  };
315
569
  return { ...base, adapterGuidance: buildAdapterGuidance(base) };
316
570
  }
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+ const IMAGE_MIME_TYPES = new Set([
3
+ "image/png",
4
+ "image/jpeg",
5
+ "image/gif",
6
+ "image/webp",
7
+ ]);
8
+ export function hashDeferredTurnPayload(input) {
9
+ return createHash("sha256")
10
+ .update(JSON.stringify({
11
+ text: input.text,
12
+ images: input.images.map(({ type, mimeType, data }) => ({
13
+ type,
14
+ mimeType,
15
+ data,
16
+ })),
17
+ }))
18
+ .digest("hex");
19
+ }
20
+ export function parseDeferredTurn(value) {
21
+ if (!value || typeof value !== "object")
22
+ return undefined;
23
+ const item = value;
24
+ if (item.schemaVersion !== 1 ||
25
+ !Number.isSafeInteger(item.revision) ||
26
+ (item.revision ?? 0) < 1 ||
27
+ typeof item.clientRequestId !== "string" ||
28
+ !item.clientRequestId.trim() ||
29
+ typeof item.payloadHash !== "string" ||
30
+ !/^[a-f0-9]{64}$/.test(item.payloadHash) ||
31
+ typeof item.blockedByTurnId !== "string" ||
32
+ !item.blockedByTurnId.trim() ||
33
+ typeof item.text !== "string" ||
34
+ !Array.isArray(item.images) ||
35
+ !["queued", "dispatching", "accepted", "failed"].includes(item.state) ||
36
+ typeof item.createdAt !== "string" ||
37
+ typeof item.updatedAt !== "string") {
38
+ return undefined;
39
+ }
40
+ const images = [];
41
+ for (const image of item.images) {
42
+ if (!image || typeof image !== "object")
43
+ return undefined;
44
+ const candidate = image;
45
+ if (typeof candidate.name !== "string" ||
46
+ candidate.type !== "image" ||
47
+ typeof candidate.mimeType !== "string" ||
48
+ !IMAGE_MIME_TYPES.has(candidate.mimeType) ||
49
+ typeof candidate.data !== "string") {
50
+ return undefined;
51
+ }
52
+ images.push(candidate);
53
+ }
54
+ return {
55
+ schemaVersion: 1,
56
+ revision: item.revision,
57
+ clientRequestId: item.clientRequestId.trim(),
58
+ payloadHash: item.payloadHash,
59
+ blockedByTurnId: item.blockedByTurnId.trim(),
60
+ text: item.text,
61
+ images,
62
+ state: item.state,
63
+ createdAt: item.createdAt,
64
+ updatedAt: item.updatedAt,
65
+ ...(typeof item.acceptedTurnId === "string" && item.acceptedTurnId
66
+ ? { acceptedTurnId: item.acceptedTurnId }
67
+ : {}),
68
+ ...(item.lastError &&
69
+ typeof item.lastError.code === "string" &&
70
+ typeof item.lastError.message === "string"
71
+ ? { lastError: { code: item.lastError.code, message: item.lastError.message } }
72
+ : {}),
73
+ };
74
+ }
75
+ export function projectDeferredTurnSummary(deferredTurn) {
76
+ if (!deferredTurn)
77
+ return null;
78
+ return {
79
+ revision: deferredTurn.revision,
80
+ clientRequestId: deferredTurn.clientRequestId,
81
+ blockedByTurnId: deferredTurn.blockedByTurnId,
82
+ state: deferredTurn.state,
83
+ updatedAt: deferredTurn.updatedAt,
84
+ textPreview: deferredTurn.text.replace(/\s+/g, " ").trim().slice(0, 120),
85
+ imageCount: deferredTurn.images.length,
86
+ ...(deferredTurn.acceptedTurnId
87
+ ? { acceptedTurnId: deferredTurn.acceptedTurnId }
88
+ : {}),
89
+ ...(deferredTurn.lastError ? { lastError: deferredTurn.lastError } : {}),
90
+ };
91
+ }