@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.
- package/CHANGELOG.md +191 -0
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/executors/dag-pi-executor.js +42 -4
- package/dist/executors/shell-executor.js +269 -15
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/shared/openspec-spec.js +76 -0
- package/dist/shared/operator/capabilities.js +149 -27
- package/dist/task/config-types.js +43 -0
- package/dist/task/frontend-project-capability.js +292 -38
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +477 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +137 -0
- package/dist/worker/console/operator-actions.js +128 -4
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +8 -1
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
- package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +39 -0
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
- package/dist/workflows/dag/init-hybrid.js +920 -110
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +77 -8
- package/dist/workflows/dag/prompt.js +45 -0
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +93 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +33 -0
- package/dist/workflows/dag/types.js +148 -4
- package/dist/workflows/dag/validate.js +7 -2
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-design-contract.md +40 -14
- package/docs/templates/frontend-implementation-contract.schema.json +9 -2
- package/docs/templates/frontend-task-constraints.md +40 -13
- package/docs/templates/frontend-task-requirement.md +54 -20
- package/harness.json +3 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/references/review-checklist.md +2 -0
- package/skills/frontend-implementation/references/design-spec.md +16 -19
- package/skills/frontend-implementation/references/node-contracts.md +15 -11
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- 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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
382
|
+
evidencePaths: discovery.paths.slice(0, 5),
|
|
123
383
|
reasons: ["package.json missing or unreadable"],
|
|
124
|
-
designEvidence
|
|
125
|
-
|
|
126
|
-
advisoryPaths: [],
|
|
127
|
-
conflicts: [],
|
|
128
|
-
},
|
|
384
|
+
designEvidence,
|
|
385
|
+
openspecDiscovery: toOpenspecDiscovery(discovery, designEvidence),
|
|
129
386
|
};
|
|
130
|
-
if (
|
|
131
|
-
base.reasons.push(`openspec spec files discovered: ${
|
|
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
|
|
287
|
-
const designEvidence =
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|