@tea-agent/loop-agent 0.38.0-next.1 → 0.38.1
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 +95 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +97 -19
- package/dist/commands/init.js +14 -2
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/governance/manifest-types.js +2 -0
- package/dist/infrastructure/harness/atomic-write.js +2 -1
- package/dist/shared/openspec-spec.js +76 -0
- package/dist/shared/operator/capabilities.js +65 -8
- package/dist/task/config-types.js +21 -0
- package/dist/task/frontend-project-capability.js +292 -38
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +155 -78
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +19 -8
- package/dist/worker/console/chat/semantic-activity.js +17 -5
- package/dist/worker/console/chat/turn-process.js +17 -30
- package/dist/worker/console/harness-pi-model-matrix.js +146 -0
- package/dist/worker/console/operator-actions.js +123 -0
- package/dist/worker/console/operator-user-error.js +19 -0
- package/dist/worker/console/pi-readiness.js +7 -1
- package/dist/worker/console/security.js +20 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-Hfsot6rP.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-YGwEASin.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-CmOM0F92.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DDLuaHC5.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-B0Um43W4.js} +1 -1
- package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DvBN4JZc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B1UVgW4E.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Cf0GgXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-DbNA24sj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-Bi_0Be06.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-C8pPGvRd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BitQY8Z-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-U67GCiEo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-IoJwXPBA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-YDZ_UHgE.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-CUZgsiRC.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-COmCh4pT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-Ta6FSm68.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-UdcR-VW9.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DybcO3Et.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-wErDFWLw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-DkVHZWqz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DdqWs7QN.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-C9dMVmDo.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-B2T2WcBK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-Bt9rjDFq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-U8ic_tvi.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BfQJdQoO.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CpMftH4O.js} +1 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +1 -0
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +325 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-DzqWXNO_.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-ChPtKFLz.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-PRe7wB5u.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DpKX6QNp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-Dnl76b7Y.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-S9PCXc3J.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Du8e-FSX.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CY6OmHTc.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-C65GPhpj.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-U6IUiTCL.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D-I_9Qhi.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-B57Fhed0.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CJOtiw21.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-FyX8zxSh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-eF-pbs9t.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-CcfkA-Dq.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DH8FjnOK.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-beoTagJg.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CQSfw4cK.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-CQgzQ6jn.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +51 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
- 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/useChatSessions.js +15 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +158 -0
- package/dist/workflows/dag/frontend-plan-render.js +90 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
- package/dist/workflows/dag/init-hybrid.js +272 -27
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +51 -9
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/retry-policy.js +15 -0
- package/dist/workflows/dag/types.js +50 -4
- package/dist/workflows/dag/validate.js +7 -2
- 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/docs/templates/harness.schema.json +8 -0
- package/harness.json +11 -2
- 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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only MCP inventory for Operator Chat.
|
|
3
|
+
*
|
|
4
|
+
* Pi has no built-in MCP catalog. Console projects two real facts:
|
|
5
|
+
* 1. mcp.json server specs (configured, may or may not have registered tools)
|
|
6
|
+
* 2. tools already on the live session that look like MCP registrations
|
|
7
|
+
*
|
|
8
|
+
* Never fabricate servers or tools. Missing files stay empty.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
const MCP_CONFIG_NAMES = ["mcp.json"];
|
|
13
|
+
export function isMcpToolName(name) {
|
|
14
|
+
const lower = name.trim().toLowerCase();
|
|
15
|
+
return lower === "mcp" || lower.startsWith("mcp_") || lower.startsWith("mcp-");
|
|
16
|
+
}
|
|
17
|
+
export function inferMcpServerName(toolName, knownServers = []) {
|
|
18
|
+
const name = toolName.trim();
|
|
19
|
+
if (/^mcp$/i.test(name))
|
|
20
|
+
return undefined;
|
|
21
|
+
const rest = name.replace(/^mcp[_-]/i, "");
|
|
22
|
+
if (!rest || rest === name || knownServers.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
const ranked = [...knownServers].sort((left, right) => right.length - left.length);
|
|
25
|
+
const lower = rest.toLowerCase();
|
|
26
|
+
return ranked.find((server) => {
|
|
27
|
+
const key = server.toLowerCase();
|
|
28
|
+
return lower === key || lower.startsWith(`${key}_`) || lower.startsWith(`${key}-`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function looksLikeMcpTool(tool) {
|
|
32
|
+
const name = typeof tool.name === "string" ? tool.name : "";
|
|
33
|
+
if (!name)
|
|
34
|
+
return false;
|
|
35
|
+
if (isMcpToolName(name))
|
|
36
|
+
return true;
|
|
37
|
+
const source = `${tool.sourceInfo?.source ?? ""} ${tool.sourceInfo?.path ?? ""}`.toLowerCase();
|
|
38
|
+
return source.includes("mcp");
|
|
39
|
+
}
|
|
40
|
+
function asRecord(value) {
|
|
41
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
42
|
+
? value
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
function readEnabled(entry) {
|
|
46
|
+
return entry.enabled !== false;
|
|
47
|
+
}
|
|
48
|
+
function readTransport(entry) {
|
|
49
|
+
if (typeof entry.transport === "string" && entry.transport.trim()) {
|
|
50
|
+
return entry.transport.trim();
|
|
51
|
+
}
|
|
52
|
+
if (typeof entry.type === "string" && entry.type.trim()) {
|
|
53
|
+
return entry.type.trim();
|
|
54
|
+
}
|
|
55
|
+
if (typeof entry.url === "string")
|
|
56
|
+
return "http";
|
|
57
|
+
if (Array.isArray(entry.command) || typeof entry.command === "string") {
|
|
58
|
+
return "stdio";
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function readCommand(entry) {
|
|
63
|
+
if (typeof entry.command === "string" && entry.command.trim()) {
|
|
64
|
+
const args = Array.isArray(entry.args)
|
|
65
|
+
? entry.args.filter((item) => typeof item === "string").join(" ")
|
|
66
|
+
: "";
|
|
67
|
+
return args ? `${entry.command} ${args}` : entry.command;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(entry.command)) {
|
|
70
|
+
return entry.command
|
|
71
|
+
.filter((item) => typeof item === "string")
|
|
72
|
+
.join(" ")
|
|
73
|
+
.trim();
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function extractServerMap(parsed) {
|
|
78
|
+
const root = asRecord(parsed);
|
|
79
|
+
if (!root)
|
|
80
|
+
return {};
|
|
81
|
+
const mcpServers = asRecord(root.mcpServers);
|
|
82
|
+
if (mcpServers)
|
|
83
|
+
return mcpServers;
|
|
84
|
+
const mcp = asRecord(root.mcp);
|
|
85
|
+
if (mcp)
|
|
86
|
+
return mcp;
|
|
87
|
+
const servers = asRecord(root.servers);
|
|
88
|
+
if (servers)
|
|
89
|
+
return servers;
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
export function parseMcpConfig(raw, scope, configPath) {
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const servers = [];
|
|
101
|
+
for (const [name, value] of Object.entries(extractServerMap(parsed))) {
|
|
102
|
+
if (!name.trim())
|
|
103
|
+
continue;
|
|
104
|
+
const entry = asRecord(value) ?? {};
|
|
105
|
+
const command = readCommand(entry);
|
|
106
|
+
const url = typeof entry.url === "string" ? entry.url.trim() : undefined;
|
|
107
|
+
servers.push({
|
|
108
|
+
name: name.trim(),
|
|
109
|
+
scope,
|
|
110
|
+
enabled: readEnabled(entry),
|
|
111
|
+
...(readTransport(entry) ? { transport: readTransport(entry) } : {}),
|
|
112
|
+
...(command ? { command } : {}),
|
|
113
|
+
...(url ? { url } : {}),
|
|
114
|
+
configPath,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return servers;
|
|
118
|
+
}
|
|
119
|
+
export function projectSessionMcpTools(tools, activeToolNames = [], knownServers = []) {
|
|
120
|
+
const active = new Set(activeToolNames.map((name) => name.toLowerCase()));
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const projected = [];
|
|
123
|
+
for (const tool of tools) {
|
|
124
|
+
if (!looksLikeMcpTool(tool) || typeof tool.name !== "string")
|
|
125
|
+
continue;
|
|
126
|
+
const name = tool.name.trim();
|
|
127
|
+
if (!name || seen.has(name))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(name);
|
|
130
|
+
const server = inferMcpServerName(name, knownServers);
|
|
131
|
+
projected.push({
|
|
132
|
+
name,
|
|
133
|
+
description: typeof tool.description === "string" ? tool.description : "",
|
|
134
|
+
...(server ? { server } : {}),
|
|
135
|
+
source: tool.sourceInfo?.source ?? "",
|
|
136
|
+
scope: tool.sourceInfo?.scope ?? "session",
|
|
137
|
+
...(tool.sourceInfo?.path ? { path: tool.sourceInfo.path } : {}),
|
|
138
|
+
active: active.has(name.toLowerCase()),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return projected;
|
|
142
|
+
}
|
|
143
|
+
export function assembleMcpInventory(servers, tools) {
|
|
144
|
+
const byServer = new Map();
|
|
145
|
+
const unassigned = [];
|
|
146
|
+
for (const tool of tools) {
|
|
147
|
+
const key = tool.server?.toLowerCase();
|
|
148
|
+
if (!key) {
|
|
149
|
+
unassigned.push(tool);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const list = byServer.get(key) ?? [];
|
|
153
|
+
list.push(tool);
|
|
154
|
+
byServer.set(key, list);
|
|
155
|
+
}
|
|
156
|
+
const views = servers.map((server) => {
|
|
157
|
+
const matched = byServer.get(server.name.toLowerCase()) ?? [];
|
|
158
|
+
byServer.delete(server.name.toLowerCase());
|
|
159
|
+
const status = !server.enabled
|
|
160
|
+
? "disabled"
|
|
161
|
+
: matched.length > 0
|
|
162
|
+
? "loaded"
|
|
163
|
+
: "configured";
|
|
164
|
+
return {
|
|
165
|
+
name: server.name,
|
|
166
|
+
scope: server.scope,
|
|
167
|
+
status,
|
|
168
|
+
enabled: server.enabled,
|
|
169
|
+
...(server.transport ? { transport: server.transport } : {}),
|
|
170
|
+
...(server.command ? { command: server.command } : {}),
|
|
171
|
+
...(server.url ? { url: server.url } : {}),
|
|
172
|
+
configPath: server.configPath,
|
|
173
|
+
tools: matched,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
for (const [name, matched] of byServer) {
|
|
177
|
+
views.push({
|
|
178
|
+
name,
|
|
179
|
+
scope: "session",
|
|
180
|
+
status: "loaded",
|
|
181
|
+
enabled: true,
|
|
182
|
+
tools: matched,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return { servers: views, unassignedTools: unassigned };
|
|
186
|
+
}
|
|
187
|
+
async function readConfigFile(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
return await readFile(filePath, "utf8");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export async function loadMcpServerSpecs(options) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
{ path: path.join(options.agentDir, ...MCP_CONFIG_NAMES), scope: "user" },
|
|
198
|
+
{ path: path.join(options.cwd, ".pi", ...MCP_CONFIG_NAMES), scope: "project" },
|
|
199
|
+
{ path: path.join(options.cwd, ".mcp.json"), scope: "project" },
|
|
200
|
+
];
|
|
201
|
+
const servers = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const raw = await readConfigFile(candidate.path);
|
|
205
|
+
if (!raw)
|
|
206
|
+
continue;
|
|
207
|
+
for (const server of parseMcpConfig(raw, candidate.scope, candidate.path)) {
|
|
208
|
+
const key = `${server.scope}:${server.name.toLowerCase()}`;
|
|
209
|
+
if (seen.has(key))
|
|
210
|
+
continue;
|
|
211
|
+
seen.add(key);
|
|
212
|
+
servers.push(server);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return servers;
|
|
216
|
+
}
|
|
217
|
+
export async function readMcpInventory(options) {
|
|
218
|
+
const servers = await loadMcpServerSpecs(options);
|
|
219
|
+
const tools = projectSessionMcpTools(options.tools ?? [], options.activeToolNames ?? [], servers.map((server) => server.name));
|
|
220
|
+
return assembleMcpInventory(servers, tools);
|
|
221
|
+
}
|