@tea-agent/loop-agent 0.39.0-next.7 → 0.39.0-next.9

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.
package/CHANGELOG.md CHANGED
@@ -11,14 +11,23 @@
11
11
  - `design-review` / `final-design-review` 提示改为消费契约 JSON(注入契约字段摘要),不再要求读取 plan prose
12
12
  - Console「模型与认证」页新增模型矩阵编辑器:展示并允许编辑 `harness.json executors.pi` 的 LOW/MED/HIGH 三档模型;输入框从 readiness 结构化投影 `piModelMatrix` 预填(provider/model 或裸 model id),本地内联校验(空串 / `default` 哨兵 / 畸形 `provider/`、`/model`、`a//b`)非法时禁用保存;保存经既有 `POST /api/operator/v1/operations` 派发 `savePiModelMatrix`,复用 HTTP 层 `evaluateMutationGate`(boot capability cookie + `x-loop-console-confirmation` 头 + Origin/Host/Sec-Fetch-Site),读全文档后原子写 tmp+fsync+rename(保留 `$schema` 与其余字段),成功后自动重测 readiness 并对活动 Chat session 触发 `/sessions/:id/reload`(409 `PI_SESSION_BUSY` 给出可恢复提示)
13
13
  - `GET /api/operator/v1/readiness` 的 probe 新增 `piModelMatrix` 结构化投影(向后兼容,不改动既有 `modelsMessage`)
14
+ - 组件选型规范合规(ADR 0016):契约 schema 新增可选 `uiComponentChoices[]`(`{ purpose, component, decision(specified|reuse-existing|new), specReference{path,section,line}|null, rationale }`),`specified` 必须给非空 `specReference`、`new` 不得携带、空串拒绝;`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
15
+ - `frontend-prewrite-gate` 新增三个 `retryable-invalid` failureCode:`component-spec-reference-invalid`(specReference.path 非受支持规范路径)、`component-spec-not-cited`(未在 openspec-citations 引用块引用或未真实读取)、`component-choices-missing`(组件/主题候选桶非空且契约有 UI 可见工作而 `uiComponentChoices` 缺失/空)
16
+ - gate 配置新增可选 `componentSpecCandidatePaths`(旧 DAG 缺字段兼容);`init-hybrid` 从 `designEvidence.classified.component ∪ theme ∪ rule.components` 冻结注入,capability context 强调组件/主题桶语义,plan/revision/design-review/final-design-review 提示加入硬规则与 BAD/GOOD 示例
17
+ - `frontend-plan.md` 新增 `Component Selection (spec conformance)` 章节(纯函数确定性渲染,缺省 `_(not specified)_`)
14
18
 
15
19
  - `frontend-prewrite-gate` 的 OpenSpec 读取校验支持 `task.json.frontendOpenspec` 配置:`policy` 缺省 `cited`(候选 = `requiredReadPaths` ∪ 任务源显式引用的 openspec 路径),`scan-strict` 保留止血任务后的全量必读语义;`requiredReadPaths` 中非 openspec 支持路径在生成期确定性失败
16
20
  - 确定性引用块协议:生效 plan 输出在 fenced `json` 契约块后追加 `openspec-citations` fenced 块(每行一个 JSON `{"path","section","line"}`);gate 解析并与生效 plan/review 节点的成功 read 事件核验,新增 `openspec-citation-block-unparseable` / `openspec-not-cited` / `openspec-citation-not-read` 三个 retryable-invalid failureCode(防捏造、防遗漏)
17
21
  - `src/shared/openspec-spec.ts` 新增 `extractTaskSourceOpenspecPaths` 纯函数(反引号内联、markdown 链接目标、裸路径 token → 归一化 → spec 过滤 → 治理子树排除 → 去重排序)
18
22
  - 生成期把 `openspecPolicy` 与候选来源元数据(`openspecCandidateSources.declared` / `taskSourceCited` / `scanStrict`)冻结进 prewrite gate 配置;`task advance` 的 advisories 说明当前模式与候选来源
19
23
 
24
+ ### 修复
25
+
26
+ - `frontend-implementation-contract.schema.json` 的 `uiComponentChoices` items 补 `if/then` 决策语义(`specified` ⇒ `specReference` 必填非 null;`new` ⇒ `specReference` 为 null/缺失);`frontend-plan-render.ts` 空 `section` 不再渲染尾随 `#`;ADR 0016 增补「门禁边界」小节
27
+
20
28
  ### 文档
21
29
 
30
+ - 新增 `docs/decisions/0016-frontend-component-conformance.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-implementation-contract.schema.json`、`skills/frontend-implementation/references/design-spec.md`、`skills/frontend-implementation/references/node-contracts.md`、`skills/frontend-design-review/references/review-checklist.md`、`CONTEXT.md`
22
31
  - 新增 `docs/decisions/0015-frontend-plan-json-only-contract.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-implementation-contract.schema.json`、`skills/frontend-implementation/references/node-contracts.md`
23
32
  - 新增 `docs/decisions/0014-openspec-gate-claim-verification.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-task-constraints.md`、`docs/templates/frontend-design-contract.md`、`skills/frontend-implementation/references/node-contracts.md`
24
33
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "version": "0.37.0",
4
- "gitSha": "21814008c0dd62e8ff8de012c1e66143600e093e",
5
- "builtAt": "2026-08-17T17:17:00.338Z"
4
+ "gitSha": "aaa51e1fe0a1b8731731152ae34de25ed4c8ff0d",
5
+ "builtAt": "2026-08-18T06:52:28.616Z"
6
6
  }
@@ -191,6 +191,42 @@ const gap = z
191
191
  blocking: z.boolean(),
192
192
  })
193
193
  .strict();
194
+ /**
195
+ * Structured component/token selection contract (ADR 0016). Every user-visible
196
+ * UI purpose the plan touches must be declared here so the prewrite gate can
197
+ * deterministically cross-check that the selection comes from the frontend
198
+ * specification (openspec/ai_workspace component + theme specs) instead of
199
+ * relying on design-review's soft model judgment.
200
+ *
201
+ * - `specified`: the frontend spec mandates this component for the purpose →
202
+ * `specReference` is required (path/section/line hit).
203
+ * - `reuse-existing`: reuse an existing repo component/convention not named by
204
+ * the spec → `specReference` may be null, rationale explains the basis.
205
+ * - `new`: neither spec nor existing code fits → `specReference` must be null,
206
+ * rationale must declare the deviation (design-review approves it).
207
+ *
208
+ * `specReference` is nullable + optional because `stripNullValuesDeep` strips
209
+ * null object values before validation, so a `new` choice's `specReference:
210
+ * null` becomes an absent field; the superRefine below treats absent and null
211
+ * identically.
212
+ */
213
+ const uiComponentSpecReferenceSchema = z
214
+ .object({
215
+ path: safePath,
216
+ section: z.string(),
217
+ line: z.number().int().nullable().optional(),
218
+ })
219
+ .strict();
220
+ const uiComponentChoiceSchema = z
221
+ .object({
222
+ purpose: z.string().min(1),
223
+ component: z.string().min(1),
224
+ decision: z.enum(["specified", "reuse-existing", "new"]),
225
+ specReference: uiComponentSpecReferenceSchema.nullable().optional(),
226
+ rationale: z.string().min(1),
227
+ })
228
+ .strict();
229
+ const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
194
230
  /**
195
231
  * Normalize unstable model emissions before the strict schema sees them:
196
232
  * recursively drop null values (object entries and array elements). A null for
@@ -318,6 +354,7 @@ export const frontendImplementationContractSchema = z
318
354
  evidenceGaps: z.array(gap),
319
355
  implementationSteps: z.array(z.string().min(1)).optional(),
320
356
  stylingStrategy: z.string().min(1).optional(),
357
+ uiComponentChoices: uiComponentChoicesSchema.optional(),
321
358
  dependencyPolicy: z.string().min(1).optional(),
322
359
  residualRisks: z.array(z.string().min(1)).optional(),
323
360
  realIntegrationGap: z.string().min(1).optional(),
@@ -396,6 +433,26 @@ export const frontendImplementationContractSchema = z
396
433
  path: ["uiStates"],
397
434
  });
398
435
  }
436
+ for (const [index, choice] of (value.uiComponentChoices ?? []).entries()) {
437
+ if (choice.decision === "specified") {
438
+ if (!choice.specReference || !choice.specReference.path) {
439
+ ctx.addIssue({
440
+ code: "custom",
441
+ message: "specified component choice requires a non-empty specReference.path",
442
+ path: ["uiComponentChoices", index, "specReference"],
443
+ });
444
+ }
445
+ }
446
+ else if (choice.decision === "new") {
447
+ if (choice.specReference) {
448
+ ctx.addIssue({
449
+ code: "custom",
450
+ message: "new component choice must not carry a specReference",
451
+ path: ["uiComponentChoices", index, "specReference"],
452
+ });
453
+ }
454
+ }
455
+ }
399
456
  if (value.mockApi.strategy !== "not-needed") {
400
457
  if (value.mockApi.endpoints.length === 0) {
401
458
  ctx.addIssue({
@@ -691,6 +748,18 @@ function asStringArray(value) {
691
748
  .map((item) => asString(item))
692
749
  .filter((item) => item.length > 0);
693
750
  }
751
+ /**
752
+ * Pass structured component choices through the free-form compatibility
753
+ * branch. The strict-shape branch already spreads unknown fields, so only this
754
+ * explicit constructor needs to re-emit `uiComponentChoices`; a missing/empty
755
+ * array normalizes to undefined (absent) so the optional schema field stays
756
+ * compatible with legacy free-form contracts.
757
+ */
758
+ function normalizeUiComponentChoices(value) {
759
+ if (!Array.isArray(value) || value.length === 0)
760
+ return undefined;
761
+ return value;
762
+ }
694
763
  function normalizeFrontendRoute(value) {
695
764
  const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
696
765
  if (!route || route === "." || route.split("/").includes(".."))
@@ -1379,6 +1448,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1379
1448
  dependencyPolicy: asString(record.dependencyPolicy) || undefined,
1380
1449
  residualRisks: asStringArray(record.residualRisks),
1381
1450
  realIntegrationGap: asString(record.realIntegrationGap) || undefined,
1451
+ uiComponentChoices: normalizeUiComponentChoices(record.uiComponentChoices),
1382
1452
  };
1383
1453
  }
1384
1454
  /**
@@ -33,6 +33,20 @@ function verificationPlan(contract) {
33
33
  const lines = contract.verificationTargets.map((target) => `- ${target.id}: ${target.commandLabel} (${target.file})`);
34
34
  return lines.length > 0 ? lines.join("\n") : "_(none)_";
35
35
  }
36
+ function componentSelection(contract) {
37
+ const choices = contract.uiComponentChoices ?? [];
38
+ if (choices.length === 0)
39
+ return "_(not specified)_";
40
+ return choices
41
+ .map((choice) => {
42
+ const specReference = choice.specReference;
43
+ const refText = specReference
44
+ ? ` ${specReference.path}${specReference.section ? `#${specReference.section}` : ""}${specReference.line != null ? `:${specReference.line}` : ""}`
45
+ : "";
46
+ return `- ${choice.purpose} → ${choice.component} (${choice.decision})${refText ? `:${refText}` : ""} — ${choice.rationale}`;
47
+ })
48
+ .join("\n");
49
+ }
36
50
  export function renderFrontendPlanMarkdown(contract) {
37
51
  return [
38
52
  "# Frontend Implementation Plan",
@@ -52,6 +66,9 @@ export function renderFrontendPlanMarkdown(contract) {
52
66
  "## Styling & Component Strategy",
53
67
  contract.stylingStrategy || "_(not specified)_",
54
68
  "",
69
+ "## Component Selection (spec conformance)",
70
+ componentSelection(contract),
71
+ "",
55
72
  "## Interaction Notes",
56
73
  interactionNotes(contract),
57
74
  "",
@@ -35,6 +35,9 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
35
35
  "openspec-citation-block-unparseable",
36
36
  "openspec-not-cited",
37
37
  "openspec-citation-not-read",
38
+ "component-spec-reference-invalid",
39
+ "component-spec-not-cited",
40
+ "component-choices-missing",
38
41
  "contract-too-large",
39
42
  ]);
40
43
  export const frontendPrewriteResultV1Schema = z
@@ -785,6 +788,87 @@ export async function runFrontendPrewriteGate(input) {
785
788
  }
786
789
  }
787
790
  }
791
+ // Component selection spec conformance (ADR 0016). Deterministic cross-check
792
+ // that declared component/token selections trace to the frontend spec
793
+ // (openspec/ai_workspace component + theme) instead of a model's ad-hoc
794
+ // "better" choice. Runs after the openspec read gate so citation + read
795
+ // evidence are already resolved.
796
+ const componentChoices = analysis.canonical.uiComponentChoices ?? [];
797
+ const componentSpecCandidates = input.config.componentSpecCandidatePaths ?? [];
798
+ const hasUiVisibleWork = analysis.canonical.uiStates.some((state) => state.applicable) ||
799
+ analysis.canonical.interactions.length > 0;
800
+ if (componentSpecCandidates.length > 0 &&
801
+ hasUiVisibleWork &&
802
+ componentChoices.length === 0) {
803
+ return finalizePrewrite(input, {
804
+ ...basePending,
805
+ verdict,
806
+ candidateJsonSha256: analysis.candidateJsonSha256,
807
+ normalizationActions: [],
808
+ mockStrategy,
809
+ classification: "retryable-invalid",
810
+ failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
811
+ failureCode: "component-choices-missing",
812
+ openspecReadPaths,
813
+ openspecCandidatePaths: candidatePaths,
814
+ });
815
+ }
816
+ const componentSpecReferences = componentChoices
817
+ .map((choice) => ({
818
+ raw: choice.specReference?.path ?? "",
819
+ normalized: choice.specReference?.path
820
+ ? normalizeCitationPath(choice.specReference.path, candidateRoot)
821
+ : null,
822
+ }))
823
+ .filter((ref) => ref.raw.length > 0);
824
+ if (componentSpecReferences.length > 0) {
825
+ const invalidRefs = [
826
+ ...new Set(componentSpecReferences
827
+ .filter((ref) => !ref.normalized ||
828
+ !isOpenspecSpecFilePath(ref.normalized))
829
+ .map((ref) => ref.raw)),
830
+ ];
831
+ if (invalidRefs.length > 0) {
832
+ return finalizePrewrite(input, {
833
+ ...basePending,
834
+ verdict,
835
+ candidateJsonSha256: analysis.candidateJsonSha256,
836
+ normalizationActions: [],
837
+ mockStrategy,
838
+ classification: "retryable-invalid",
839
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are not supported openspec/ai_workspace spec files: ${invalidRefs.join(", ")}`,
840
+ failureCode: "component-spec-reference-invalid",
841
+ openspecReadPaths,
842
+ openspecCandidatePaths: candidatePaths,
843
+ });
844
+ }
845
+ if (openspecPolicy === "cited") {
846
+ const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
847
+ const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
848
+ const readSet = new Set(openspecReadPaths);
849
+ const uncorroborated = [
850
+ ...new Set(componentSpecReferences
851
+ .filter((ref) => !ref.normalized ||
852
+ !citedPaths.has(ref.normalized) ||
853
+ !readSet.has(ref.normalized))
854
+ .map((ref) => ref.raw)),
855
+ ];
856
+ if (uncorroborated.length > 0) {
857
+ return finalizePrewrite(input, {
858
+ ...basePending,
859
+ verdict,
860
+ candidateJsonSha256: analysis.candidateJsonSha256,
861
+ normalizationActions: [],
862
+ mockStrategy,
863
+ classification: "retryable-invalid",
864
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
865
+ failureCode: "component-spec-not-cited",
866
+ openspecReadPaths,
867
+ openspecCandidatePaths: candidatePaths,
868
+ });
869
+ }
870
+ }
871
+ }
788
872
  // Render the deterministic plan.md from the canonical contract (fail-closed
789
873
  // before any artifact is written: no contract, no writer authorization).
790
874
  let planMarkdown;
@@ -2246,6 +2246,15 @@ const openspecCitationInstruction = [
2246
2246
  '该块内每行一个 JSON 对象 {"path":"<repo 相对 openspec 路径>","section":"<命中章节或空串>","line":<int 或 null>},必须逐条列出你在本计划中实际读取并应用的每个 openspec 规范文件。',
2247
2247
  "prewrite gate 会用真实 read 事件核验每条引用:引用存在但无成功 read 事件 → openspec-citation-not-read;契约冻结的必读候选未被引用 → openspec-not-cited;两者都 fail-closed。不要引用未读取的路径。",
2248
2248
  ].join("\n");
2249
+ const frontendComponentConformanceInstruction = [
2250
+ "## Component Selection conformance (uiComponentChoices; hard rule)",
2251
+ "每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
2252
+ "- decision=specified:前端规范(候选组件/主题桶 + 任务源显式引用)已定义该用途组件 → 必须使用该组件,并给精确 specReference { path, section, line }(path 必须是 openspec/ai_workspace 受支持规范路径)。",
2253
+ "- decision=reuse-existing:复用仓库既有组件/惯例(规范未点名)→ specReference 可为 null,rationale 说明复用的现有组件与依据。",
2254
+ "- decision=new:规范与既有代码均无合适组件 → specReference 必须为 null,rationale 必须说明偏差理由(design-review 审,最终 review 复核)。",
2255
+ "不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
2256
+ "prewrite gate 确定性交叉校验:specReference.path 非法 → component-spec-reference-invalid;未在 openspec-citations 引用块中引用或未真实读取 → component-spec-not-cited;候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
2257
+ ].join("\n");
2249
2258
  function resolveFrontendCapabilityContextBlock(sources) {
2250
2259
  const risk = sources.frontendRisk;
2251
2260
  const capability = sources.frontendProjectCapability;
@@ -2284,6 +2293,7 @@ function resolveFrontendCapabilityContextBlock(sources) {
2284
2293
  else {
2285
2294
  parts.push("(no openspec specification paths discovered — greenfield)");
2286
2295
  }
2296
+ parts.push("component / theme / rule.components 是「组件/主题规范」必读语义桶:规范已定义某用途组件时必须使用它(uiComponentChoices 用 decision=specified + 精确 path/section/line),不得静默替换为自认更合适的组件;无规范定义时才允许 reuse-existing 或 new(new 必须声明偏差 rationale)。这些路径在生成期冻结为 prewrite gate 的 componentSpecCandidatePaths。");
2287
2297
  parts.push("Each consuming node MUST report in its output: applicable rules, the hit path/section/line number for every applied specification, and any conflicts or missing specifications. Missing or conflicting required specifications must fail closed rather than silently substituting nearby repository conventions.");
2288
2298
  parts.push(`A11y capability: ${capability.a11y.status}` +
2289
2299
  (capability.a11y.tools.length
@@ -2492,6 +2502,16 @@ async function buildFrontendHybridDagFromTask(sources) {
2492
2502
  "",
2493
2503
  "### optional plan fields - BAD (present-but-empty strings are rejected):",
2494
2504
  '{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
2505
+ "",
2506
+ "### uiComponentChoices - GOOD (specified with precise spec hit):",
2507
+ '{"purpose":"primary action button","component":"Button","decision":"specified","specReference":{"path":"openspec/schemas/button.md","section":"Variants","line":12},"rationale":"spec mandates Button for primary actions"}',
2508
+ "",
2509
+ "### uiComponentChoices - GOOD (new with deviation rationale; specReference null):",
2510
+ '{"purpose":"loading skeleton","component":"SkeletonCard","decision":"new","specReference":null,"rationale":"no spec or existing component covers skeleton; deviation pending design-review approval"}',
2511
+ "",
2512
+ "### uiComponentChoices - BAD (specified without specReference, or new with specReference):",
2513
+ '{"purpose":"primary action","component":"MyButton","decision":"specified","specReference":null,"rationale":"..."} <-- REJECTED: specified requires specReference',
2514
+ '{"purpose":"primary action","component":"MyButton","decision":"new","specReference":{"path":"openspec/schemas/button.md","section":"","line":null},"rationale":"..."} <-- REJECTED: new must not carry specReference',
2495
2515
  ].join("\n");
2496
2516
  })();
2497
2517
  const frontendContractFieldSummary = [
@@ -2504,7 +2524,8 @@ async function buildFrontendHybridDagFromTask(sources) {
2504
2524
  "- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
2505
2525
  "- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
2506
2526
  "- designEvidence: source, paths, conflicts; evidenceGaps[]",
2507
- "- optional: implementationSteps[], stylingStrategy, dependencyPolicy, residualRisks[], realIntegrationGap",
2527
+ "- optional: implementationSteps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
2528
+ "- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
2508
2529
  "Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
2509
2530
  ].join("\n");
2510
2531
  const sourceContext = [
@@ -2681,6 +2702,17 @@ async function buildFrontendHybridDagFromTask(sources) {
2681
2702
  advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
2682
2703
  }
2683
2704
  const openspecGate = resolveFrontendOpenspecGateConfig(sources);
2705
+ // Generation-frozen component/theme specification bucket (ADR 0016). Derived
2706
+ // from the classified component/theme/rule.components buckets; the prewrite
2707
+ // gate consumes it to enforce uiComponentChoices presence and specReference
2708
+ // cross-checking. `src/task/**` classification logic is intentionally
2709
+ // unchanged — these buckets already partition normativePaths.
2710
+ const classified = sources.frontendProjectCapability?.designEvidence.classified;
2711
+ const componentSpecCandidatePaths = [
2712
+ ...(classified?.component ?? []),
2713
+ ...(classified?.theme ?? []),
2714
+ ...(classified?.rule.components ?? []),
2715
+ ].filter((value, index, array) => array.indexOf(value) === index).sort();
2684
2716
  if (openspecGate.openspecPolicy === "cited") {
2685
2717
  if (openspecGate.openspecCandidatePaths.length === 0) {
2686
2718
  advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
@@ -2787,6 +2819,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2787
2819
  sourceContext,
2788
2820
  mockContextBlock,
2789
2821
  frontendContractSchemaBlock,
2822
+ frontendComponentConformanceInstruction,
2790
2823
  openspecCitationInstruction,
2791
2824
  ].join("\n\n"),
2792
2825
  },
@@ -2807,6 +2840,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2807
2840
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2808
2841
  "Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
2809
2842
  "Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
2843
+ "Component selection conformance is a hard blocking condition: VERDICT: request-revision when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; or when any uiComponentChoices specReference.path is not cited in the openspec-citations block or has no successful read event.",
2810
2844
  "Read-only: do not modify repository files.",
2811
2845
  fixedVerificationContext,
2812
2846
  sourceContext,
@@ -2835,7 +2869,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2835
2869
  subtask_prompt: [
2836
2870
  "Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
2837
2871
  "This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
2838
- "The patch delta may update any of these contract fields: requirements, implementationSteps, targets, uiStates, interactions, mockApi, dependencyPolicy, stylingStrategy, verificationTargets, evidenceGaps, residualRisks, realIntegrationGap. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
2872
+ "The patch delta may update any of these contract fields: requirements, implementationSteps, targets, uiStates, interactions, mockApi, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, realIntegrationGap. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
2839
2873
  requirementCoverageInstruction,
2840
2874
  "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
2841
2875
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
@@ -2847,6 +2881,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2847
2881
  sourceContext,
2848
2882
  frontendContractSchemaBlock,
2849
2883
  mockContextBlock,
2884
+ frontendComponentConformanceInstruction,
2850
2885
  openspecCitationInstruction,
2851
2886
  ].join("\n\n"),
2852
2887
  },
@@ -2875,6 +2910,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2875
2910
  "Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
2876
2911
  "Review every explicit REQ-/BR-/AC- mapping; the downstream prewrite gate also checks identifier retention deterministically.",
2877
2912
  "Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
2913
+ "Also request revision for component selection non-conformance: spec-defined components silently replaced or self-invented without a declared deviation, uiComponentChoices missing for UI-visible work, or a uiComponentChoices specReference.path not cited in the openspec-citations block / not actually read.",
2878
2914
  "Read-only: do not modify repository files.",
2879
2915
  fixedVerificationContext,
2880
2916
  sourceContext,
@@ -2933,6 +2969,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2933
2969
  scanStrict: openspecGate.openspecCandidateSources.scanStrict,
2934
2970
  },
2935
2971
  openspecCandidatePaths: openspecGate.openspecCandidatePaths,
2972
+ componentSpecCandidatePaths,
2936
2973
  },
2937
2974
  cwd: ".",
2938
2975
  timeoutMs: 60000,
@@ -193,6 +193,16 @@ export const dagFrontendPrewriteGateSchema = z.object({
193
193
  requireSourceFreshness: z.literal(true),
194
194
  implementationWriteSet: z.array(z.string().min(1)).min(1).optional(),
195
195
  openspecCandidatePaths: z.array(openspecCandidatePathSchema).default([]),
196
+ /**
197
+ * Generation-frozen component/theme specification bucket. Derived from
198
+ * `designEvidence.classified.component ∪ theme ∪ rule.components` at DAG
199
+ * generation. When non-empty and the canonical contract has UI-visible
200
+ * work, the prewrite gate requires a non-empty `uiComponentChoices` and
201
+ * cross-checks each `specReference.path` against this set + the effective
202
+ * plan openspec-citations block. Legacy DAGs without this field keep the
203
+ * field-absent semantics (component-choices-missing is not enforced).
204
+ */
205
+ componentSpecCandidatePaths: z.array(openspecCandidatePathSchema).optional(),
196
206
  /**
197
207
  * OpenSpec gate policy. Omitted on legacy DAGs: the runtime treats
198
208
  * undefined as scan-strict so pre-existing full-read semantics are
@@ -19,6 +19,7 @@
19
19
  "evidenceGaps": { "type": "array", "items": { "$ref": "#/$defs/gap" } },
20
20
  "implementationSteps": { "type": "array", "items": { "type": "string", "minLength": 1 } },
21
21
  "stylingStrategy": { "type": "string", "minLength": 1 },
22
+ "uiComponentChoices": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["purpose", "component", "decision", "rationale"], "properties": { "purpose": { "type": "string", "minLength": 1 }, "component": { "type": "string", "minLength": 1 }, "decision": { "enum": ["specified", "reuse-existing", "new"] }, "specReference": { "anyOf": [{ "$ref": "#/$defs/uiComponentSpecReference" }, { "type": "null" }] }, "rationale": { "type": "string", "minLength": 1 } }, "allOf": [ { "if": { "properties": { "decision": { "const": "specified" } }, "required": ["decision"] }, "then": { "required": ["specReference"], "properties": { "specReference": { "$ref": "#/$defs/uiComponentSpecReference" } } } }, { "if": { "properties": { "decision": { "const": "new" } }, "required": ["decision"] }, "then": { "properties": { "specReference": { "type": "null" } } } } ] } },
22
23
  "dependencyPolicy": { "type": "string", "minLength": 1 },
23
24
  "residualRisks": { "type": "array", "items": { "type": "string", "minLength": 1 } },
24
25
  "realIntegrationGap": { "type": "string", "minLength": 1 }
@@ -28,6 +29,7 @@
28
29
  "path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" },
29
30
  "requirementId": { "type": "string", "pattern": "^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$" },
30
31
  "gap": { "type": "object", "additionalProperties": false, "required": ["description", "blocking"], "properties": { "requirementId": { "$ref": "#/$defs/requirementId" }, "description": { "type": "string", "minLength": 1 }, "blocking": { "type": "boolean" } } },
31
- "sourceBinding": { "type": "object", "additionalProperties": false, "required": ["taskId", "requirementPath", "requirementSha256", "referencePaths", "requirementIds"], "properties": { "taskId": { "type": "string", "minLength": 1 }, "requirementPath": { "$ref": "#/$defs/path" }, "requirementSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "referencePaths": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } } } }
32
+ "sourceBinding": { "type": "object", "additionalProperties": false, "required": ["taskId", "requirementPath", "requirementSha256", "referencePaths", "requirementIds"], "properties": { "taskId": { "type": "string", "minLength": 1 }, "requirementPath": { "$ref": "#/$defs/path" }, "requirementSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "referencePaths": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } } } },
33
+ "uiComponentSpecReference": { "type": "object", "additionalProperties": false, "required": ["path", "section"], "properties": { "path": { "$ref": "#/$defs/path" }, "section": { "type": "string" }, "line": { "anyOf": [{ "type": "integer" }, { "type": "null" }] } } }
32
34
  }
33
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.39.0-next.7",
3
+ "version": "0.39.0-next.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -12,6 +12,7 @@
12
12
  - Cite knowledge base, `openspec/schemas/`, `openspec/project-specs/`, and `ai_workspace/` as parallel sources; failed or empty
13
13
  knowledge queries must still search `<repoRoot>/openspec/schemas/`, `<repoRoot>/openspec/project-specs/`, and `<repoRoot>/ai_workspace/`.
14
14
  - Record source status, query terms, paths/headings, conflicts, authorized deps, and allowed paths for both.
15
+ - Component selection conformance: every UI purpose must be declared in the contract `uiComponentChoices[]`. `specified` must carry a non-empty `specReference { path, section, line }` hitting a supported openspec/ai_workspace spec; `new` must declare a deviation rationale with `specReference` null; a spec-defined component must not be silently replaced or self-invented without a declared deviation.
15
16
 
16
17
  ## Interaction / Quality
17
18
 
@@ -37,5 +38,6 @@
37
38
  ## Verdict Matrix
38
39
 
39
40
  - Request revision for coverage gaps, unsafe scope, unauthorized deps, unresolved required interaction, missing required verification, skipped local specification fallback (`openspec/schemas/`, `openspec/project-specs/`, or `ai_workspace/`), unsafe/missing Mock strategy, or Mock evidence presented as real integration.
41
+ - Request revision for component selection non-conformance: spec-defined components silently replaced or self-invented without a declared deviation, `uiComponentChoices` missing for UI-visible work, or a `uiComponentChoices` `specReference.path` not cited in the `openspec-citations` block / not actually read.
40
42
  - Knowledge-base unavailable but relevant OpenSpec or `ai_workspace/` rules applied is advisory only.
41
43
  - Optional cleanup that cannot affect acceptance is advisory.
@@ -5,27 +5,21 @@
5
5
  Knowledge base, OpenSpec, and `ai_workspace/` are parallel specification sources:
6
6
 
7
7
  1. Attempt the configured component/design knowledge-base query first when a
8
- connector is available in the execution environment.
9
- 2. Regardless of knowledge-base success, failure, timeout, no match, or no
10
- configuration, also recursively search `openspec/schemas/`,
11
- `openspec/project-specs/`, and `ai_workspace/` for index files and relevant content.
8
+ connector is available.
9
+ 2. Regardless of knowledge-base success/failure/timeout/no-match, also
10
+ recursively search `openspec/schemas/`, `openspec/project-specs/`, and
11
+ `ai_workspace/` for index files and relevant content.
12
12
  3. Treat relevant matches from both sources as the current project's
13
13
  specification for this run.
14
14
  4. Only then use component source, tokens, stories, tests, and pages as
15
15
  non-normative repository fallback.
16
16
 
17
- Never skip local OpenSpec or `ai_workspace/` sources for neighboring-code
18
- conventions, even when a knowledge-base query returned results. Report source
19
- conflicts instead of combining them. Explicit task requirements remain the
20
- contract; flag conflicts with knowledge-base or openspec specification rules.
17
+ Never skip local OpenSpec or `ai_workspace/` for neighboring-code conventions,
18
+ even when a knowledge-base query returned results. Report source conflicts instead
19
+ of combining them. Explicit task requirements remain the contract.
21
20
 
22
- ## Knowledge Base Connection TODO
23
-
24
- Request format is undecided. TODO: define connector/owner, namespaces, secret-free
25
- auth, query fields, result identity/version/time, and failure behavior.
26
-
27
- Attempt only a connector actually available in the execution environment. Otherwise
28
- record `not-configured` and run the openspec fallback; never invent a connection.
21
+ Connector format is TODO; attempt only an available connector, otherwise record
22
+ `not-configured` and run the openspec fallback (never invent a connection).
29
23
 
30
24
  ## openspec Fallback Procedure
31
25
 
@@ -33,10 +27,10 @@ record `not-configured` and run the openspec fallback; never invent a connection
33
27
  and `<repoRoot>/ai_workspace/` when present; enumerate supported files recursively.
34
28
  - Read indexes first, then search names/content using task, route, component,
35
29
  interaction, theme, token, and state terms.
36
- - Read relevant matches in context; do not treat a filename-only hit as a rule.
30
+ - Read matches in context; a filename-only hit is not a rule.
37
31
  - Record search terms, inspected/matched paths, headings or tight line ranges,
38
32
  applied rules, and conflicts.
39
- - If both directories or relevant rules are absent, record that fact before using
33
+ - If both directories or relevant rules are absent, record that before using
40
34
  repository fallback.
41
35
 
42
36
  ## Retrieval Evidence
@@ -54,6 +48,9 @@ and conflicts. `openspec fallback` includes terms, paths/headings/lines, rules,
54
48
 
55
49
  - Reuse confirmed primitives unless a new pattern is authorized.
56
50
  - Define applicable states and responsive behavior before implementation.
57
- - Cite knowledge-base or openspec specification evidence for component/token
58
- choices; label weaker repository fallback.
51
+ - Cite knowledge-base or openspec spec evidence for component/token choices;
52
+ label weaker repo fallback.
59
53
  - Make deviations and unresolved gaps explicit.
54
+ - Contract `uiComponentChoices[]` per UI purpose: `specified` needs
55
+ `specReference {path,section,line}`; `new` needs null ref + rationale; never
56
+ silently replace a spec-defined component.
@@ -5,18 +5,20 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
5
5
  ## Core nodes
6
6
 
7
7
  - **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`; no guessed requirements.
8
- - **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets; fact vs inference vs gap. Search+read `openspec/schemas/`, `openspec/project-specs/`, `ai_workspace/` first. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
9
- - **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; steps, in-bound files, applicable UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap. **JSON-only**: one-line lead-in + **exactly one** fenced `json` block (`frontend-implementation-contract-v1`) + one `openspec-citations` block. No plan prose; plan.md is rendered by the prewrite gate. `frontend-contract-json-pi`/`frontend-contract-json-validate-shell` don't exist. IDs+paths aren't sufficient behavior semantics. Use `uiStates: []` for logic-only changes; don't invent UI states. Applicable states need behavior/implementation/verification; non-applicable need a reason. Non-`not-needed` needs frozen Mock verify commands (`frontendMock.verifyCommands`/`package.json` mock script/capability seed); `auto` with absent/ambiguous capability or no command → `not-needed`; `required` without a command → generation-time blocked. Initial pass uses original plan; only exact `request-revision` runs revision + final review. Small-risk: one design review.
10
- - **`frontend-prewrite-gate-shell`**: sole write authorization. Resolve effective plan (revised if revision ran, else original) + review; require exact pass; retain every REQ/BR/AC id; enforce Mock policy; validate schema/source binding+writeSet containment; materialize `contracts/frontend-implementation-contract.json` from the **single** fenced contract block (multiple candidates fail closed `invalid-output`). Fallback only when conditional primary absent; existing malformed primary fails closed. Generation-time blocked Mock yields one deterministic blocking shell node, no writer.
8
+ - **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets; fact vs inference vs gap. Search+read `openspec/schemas/`, `openspec/project-specs/`, `ai_workspace/` first. Output stack, styling, conventions, state/data, test entry points, reuse, risks.
9
+ - **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; steps, in-bound files, UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap. **JSON-only**: one-line lead-in + **exactly one** fenced `json` block (`frontend-implementation-contract-v1`) + one `openspec-citations` block. No plan prose; plan.md is rendered by the gate. `frontend-contract-json-pi`/`frontend-contract-json-validate-shell` don't exist; IDs+paths behavior semantics. Use `uiStates: []` for logic-only changes; don't invent UI states. Applicable states need behavior/implementation/verification; non-applicable need reasons. Non-`not-needed` needs frozen Mock verify commands (`frontendMock.verifyCommands`/`package.json` mock script/capability seed); `auto` with absent/ambiguous capability or no command → `not-needed`; `required` without a command → generation-time blocked. Initial pass = original plan; only `request-revision` runs revision+final review; small-risk = one review.
10
+ - `uiComponentChoices[]` (optional): `{purpose, component, decision, specReference, rationale}`; `specified` needs `specReference{path,section,line}`; `new` needs null ref+rationale.
11
+ - **`frontend-prewrite-gate-shell`**: sole write authorization. Resolve effective plan (revised if revision ran, else original) + review; require exact pass; retain every REQ/BR/AC id; enforce Mock policy; validate schema/source binding+writeSet containment; materialize `contracts/frontend-implementation-contract.json` from the **single** fenced contract block (multiple candidates fail closed `invalid-output`). Fallback only when conditional primary absent; malformed primary fails closed. Generation-time blocked Mock yields one blocking shell node, no writer.
11
12
  - `verificationTarget.commandLabel` must be a **frozen command label** from `verifyEvidence.commandLabels`; others fail closed (`invalid-output`). Empty frozen set (no `run.json`) skips check.
12
13
  - `mockApi.strategy !== "not-needed"` fails closed when `mockCommandLabels` empty (`no authorized Mock verification commands`).
13
- - OpenSpec policy: `task.json.frontendOpenspec.policy` (default `cited`); candidates = `requiredReadPaths` ∪ task-source citations; effective plan must append one fenced `openspec-citations` block (one JSON `{"path","section","line"}` per line). Candidates non-empty but block missing/unparseable → `openspec-citation-block-unparseable`; candidate not cited → `openspec-not-cited`; cited path without successful read event in plan/review → `openspec-citation-not-read` (anti-fabrication). Empty candidates skip enforcement (generation-time advisory). `scan-strict` keeps auto-discovered full-read semantics (`openspec-not-read`); both modes keep `candidate-missing-drift`.
14
+ - OpenSpec policy: `task.json.frontendOpenspec.policy` (default `cited`); candidates = `requiredReadPaths` ∪ task-source citations; effective plan must append one fenced `openspec-citations` block (one JSON `{"path","section","line"}` per line). Candidates non-empty but block missing/unparseable → `openspec-citation-block-unparseable`; candidate not cited → `openspec-not-cited`; cited path without read event → `openspec-citation-not-read` (anti-fabrication). Empty candidates skip enforcement. `scan-strict` keeps full-read semantics (`openspec-not-read`); both modes keep `candidate-missing-drift`.
15
+ - Component conformance: missing `uiComponentChoices` (frozen bucket non-empty + UI-visible work) → `component-choices-missing`; bad/uncited `specReference.path` → `component-spec-reference-invalid` / `component-spec-not-cited`.
14
16
  - revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original contract node (`planFallbackFromNodeIds[0]`); legacy DAGs keep full-contract semantics. Size guard: canonical serialization >64KB → `retryable-invalid` + `contract-too-large`.
15
- - **`frontend-implement-pi`**: sole regular exclusive writer; uses `frontend-bounded-implement` (not this skill). Stay in `writeSet`; real requests default-on. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths/guesses. First line `IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked`; runtime checks against attributed diff. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. `not-needed` keeps real integration pending unless real backend has fresh evidence.
17
+ - **`frontend-implement-pi`**: sole regular exclusive writer; uses `frontend-bounded-implement` (not this skill). Stay in `writeSet`; real requests default-on. Atomic handler/intercept/adapter with tests. Stop on forbidden paths/guesses. First line `IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked`; runtime checks against attributed diff. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. `not-needed` keeps real integration pending unless real backend has fresh evidence.
16
18
 
17
19
  ## Contract / trace / stages (M1–M2)
18
20
 
19
- - `frontend-verify-assess-shell` runs Mock/static/behavior groups, binds `verificationTargets` to command labels + file/symbol evidence, writes `frontend-verification-trace.json` + `frontend-repair-assessment.json`; browser/visual stay `not-run`.
21
+ - `frontend-verify-assess-shell` runs Mock/static/behavior groups, binds `verificationTargets` to command labels+evidence, writes `frontend-verification-trace.json`+`frontend-repair-assessment.json`; browser/visual stay `not-run`.
20
22
  - `frontend-review-context-shell` captures real diff; combines contract, effective trace, repair assessment, diff into `contracts/frontend-review-context.json`.
21
23
  - Implement stages: (1) contract confirm (2) tests sync (3) component/UI (4) API/Mock (5) frozen checks (6) diff cleanup. Summary: Contract Ref, Changed Files, Requirements, UI States, Tests, Verification Attempts, Deviations, Residual Risks.
22
24