abelworkflow 0.9.2 → 1.0.0-rc.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/README.md +55 -182
- package/bin/abelworkflow.mjs +5 -3
- package/lib/cli/args.mjs +76 -0
- package/lib/cli/main.mjs +387 -0
- package/lib/cli/prompts.mjs +119 -0
- package/lib/config/dotenv.mjs +102 -0
- package/lib/config/jsonc.mjs +80 -0
- package/lib/config/store.mjs +310 -0
- package/lib/config/toml.mjs +638 -0
- package/lib/installer/assets.mjs +326 -0
- package/lib/installer/install.mjs +167 -0
- package/lib/installer/links.mjs +427 -0
- package/lib/installer/render.mjs +46 -0
- package/lib/installer/state.mjs +182 -0
- package/lib/paths.mjs +99 -0
- package/lib/providers/claude.mjs +388 -0
- package/lib/providers/codex.mjs +433 -0
- package/lib/providers/pi.mjs +362 -0
- package/lib/providers/skills.mjs +164 -0
- package/lib/templates/codex/agents/default.toml +7 -0
- package/lib/templates/codex/agents/explorer.toml +7 -0
- package/lib/templates/codex/agents/planner.toml +7 -0
- package/lib/templates/codex/agents/reviewer.toml +7 -0
- package/lib/templates/codex/agents/worker.toml +7 -0
- package/lib/templates/codex/config-base.toml +11 -3
- package/{AGENTS.md → lib/templates/workflow/AGENTS.md} +8 -3
- package/{commands → lib/templates/workflow/commands}/abel-diagnose.md +5 -5
- package/{commands → lib/templates/workflow/commands}/abel-implement.md +3 -8
- package/{commands → lib/templates/workflow/commands}/abel-init.md +1 -1
- package/{commands → lib/templates/workflow/commands}/abel-plan.md +1 -7
- package/{commands → lib/templates/workflow/commands}/abel-research.md +1 -1
- package/{.gitignore → lib/templates/workflow/gitignore.template} +0 -2
- package/lib/tools/cli-installer.mjs +277 -0
- package/package.json +43 -10
- package/skills/context7-auto-research/SKILL.md +5 -5
- package/skills/context7-auto-research/{context7-api.js → context7-api.cjs} +5 -8
- package/skills/dev-browser/SKILL.md +53 -33
- package/skills/dev-browser/dist/scripts/start.d.ts +2 -0
- package/skills/dev-browser/dist/scripts/start.d.ts.map +1 -0
- package/skills/dev-browser/dist/scripts/start.js +118 -0
- package/skills/dev-browser/dist/scripts/start.js.map +1 -0
- package/skills/dev-browser/dist/src/client.d.ts +93 -0
- package/skills/dev-browser/dist/src/client.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/client.js +310 -0
- package/skills/dev-browser/dist/src/client.js.map +1 -0
- package/skills/dev-browser/dist/src/entrypoint.d.ts +29 -0
- package/skills/dev-browser/dist/src/entrypoint.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/entrypoint.js +106 -0
- package/skills/dev-browser/dist/src/entrypoint.js.map +1 -0
- package/skills/dev-browser/dist/src/index.d.ts +4 -0
- package/skills/dev-browser/dist/src/index.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/index.js +2 -0
- package/skills/dev-browser/dist/src/index.js.map +1 -0
- package/skills/dev-browser/dist/src/page-api.d.ts +25 -0
- package/skills/dev-browser/dist/src/page-api.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/page-api.js +104 -0
- package/skills/dev-browser/dist/src/page-api.js.map +1 -0
- package/skills/dev-browser/dist/src/relay.d.ts +27 -0
- package/skills/dev-browser/dist/src/relay.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/relay.js +521 -0
- package/skills/dev-browser/dist/src/relay.js.map +1 -0
- package/skills/dev-browser/dist/src/runtime.d.ts +45 -0
- package/skills/dev-browser/dist/src/runtime.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/runtime.js +54 -0
- package/skills/dev-browser/dist/src/runtime.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +23 -0
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/browser-script.ts → dist/src/snapshot/browser-script.js} +21 -30
- package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/index.d.ts +14 -0
- package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/index.ts → dist/src/snapshot/index.js} +2 -2
- package/skills/dev-browser/dist/src/snapshot/index.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts +13 -0
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/inject.ts → dist/src/snapshot/inject.js} +2 -2
- package/skills/dev-browser/dist/src/snapshot/inject.js.map +1 -0
- package/skills/dev-browser/dist/src/standalone.d.ts +32 -0
- package/skills/dev-browser/dist/src/standalone.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/standalone.js +174 -0
- package/skills/dev-browser/dist/src/standalone.js.map +1 -0
- package/skills/dev-browser/dist/src/startup.d.ts +55 -0
- package/skills/dev-browser/dist/src/startup.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/startup.js +81 -0
- package/skills/dev-browser/dist/src/startup.js.map +1 -0
- package/skills/dev-browser/dist/src/target-registry.d.ts +29 -0
- package/skills/dev-browser/dist/src/target-registry.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/target-registry.js +135 -0
- package/skills/dev-browser/dist/src/target-registry.js.map +1 -0
- package/skills/dev-browser/dist/src/types.d.ts +27 -0
- package/skills/dev-browser/dist/src/types.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/types.js +2 -0
- package/skills/dev-browser/dist/src/types.js.map +1 -0
- package/skills/dev-browser/package-lock.json +67 -1510
- package/skills/dev-browser/package.json +18 -14
- package/skills/dev-browser/references/scraping.md +94 -105
- package/skills/grok-search/defaults.json +3 -0
- package/skills/grok-search/gitignore.template +4 -0
- package/skills/grok-search/scripts/_dotenv.py +5 -1
- package/skills/grok-search/scripts/groksearch_cli.py +15 -33
- package/skills/prompt-enhancer/TEMPLATE.md +0 -4
- package/skills/prompt-enhancer/requirements.txt +0 -1
- package/skills/prompt-enhancer/scripts/_dotenv.py +5 -1
- package/skills/prompt-enhancer/scripts/enhance.py +4 -51
- package/skills/prompt-enhancer/scripts/prompt_enhancer_entry.py +1 -16
- package/.skill-lock.json +0 -29
- package/lib/cli/logic.mjs +0 -213
- package/lib/cli.mjs +0 -3016
- package/skills/confidence-check/SKILL.md +0 -34
- package/skills/confidence-check/confidence.ts +0 -276
- package/skills/dev-browser/bun.lock +0 -460
- package/skills/dev-browser/scripts/start.ts +0 -280
- package/skills/dev-browser/src/client.ts +0 -474
- package/skills/dev-browser/src/entrypoint.ts +0 -157
- package/skills/dev-browser/src/index.ts +0 -289
- package/skills/dev-browser/src/relay.ts +0 -731
- package/skills/dev-browser/src/runtime.test.ts +0 -60
- package/skills/dev-browser/src/runtime.ts +0 -171
- package/skills/dev-browser/src/startup.test.ts +0 -95
- package/skills/dev-browser/src/startup.ts +0 -153
- package/skills/dev-browser/src/types.ts +0 -35
- package/skills/dev-browser/tsconfig.json +0 -36
- package/skills/dev-browser/vitest.config.ts +0 -12
- package/skills/sequential-think/SKILL.md +0 -198
- package/skills/sequential-think/scripts/.env.example +0 -5
- package/skills/sequential-think/scripts/sequential_think_cli.py +0 -253
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { piInsecureTlsHeader } from "../../extensions/pi-gpt-responses-compat/tls-fetch.mjs";
|
|
5
|
+
import {
|
|
6
|
+
readJsonFileSafe,
|
|
7
|
+
readJsoncFileSafe,
|
|
8
|
+
updateLockedJson,
|
|
9
|
+
writeJsonFileWithBackup,
|
|
10
|
+
writeText
|
|
11
|
+
} from "../config/store.mjs";
|
|
12
|
+
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
13
|
+
|
|
14
|
+
const piProviderId = "gpt";
|
|
15
|
+
const piDefaultApi = "openai-completions";
|
|
16
|
+
const piDefaultBaseUrl = "https://api.openai.com/v1";
|
|
17
|
+
const piDefaultModel = "gpt-5.5";
|
|
18
|
+
const minimumPiVersion = [0, 80, 0];
|
|
19
|
+
|
|
20
|
+
async function updatePiAuthFile(path, apiKey) {
|
|
21
|
+
return updateLockedJson(path, (auth) => {
|
|
22
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
|
|
23
|
+
throw new TypeError("Pi auth.json must contain a JSON object");
|
|
24
|
+
}
|
|
25
|
+
return buildPiAuthConfig(auth, apiKey);
|
|
26
|
+
}, {
|
|
27
|
+
sensitive: true,
|
|
28
|
+
retries: {
|
|
29
|
+
retries: 10,
|
|
30
|
+
factor: 2,
|
|
31
|
+
minTimeout: 100,
|
|
32
|
+
maxTimeout: 10000,
|
|
33
|
+
randomize: true
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parsePiVersion(value) {
|
|
39
|
+
const match = String(value || "").match(/(?:^|\D)v?(\d+)\.(\d+)\.(\d+)(?:\D|$)/u);
|
|
40
|
+
return match ? match.slice(1).map(Number) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assertSupportedPiVersion(value) {
|
|
44
|
+
const version = parsePiVersion(value);
|
|
45
|
+
if (!version) {
|
|
46
|
+
throw new Error("无法检测 Pi 版本;配置自定义 Provider 需要 Pi 0.80.0 或更高版本。");
|
|
47
|
+
}
|
|
48
|
+
for (let index = 0; index < minimumPiVersion.length; index += 1) {
|
|
49
|
+
if (version[index] > minimumPiVersion[index]) return version;
|
|
50
|
+
if (version[index] < minimumPiVersion[index]) {
|
|
51
|
+
throw new Error(`当前 Pi ${version.join(".")} 不支持 auth-only 自定义 Provider;请升级到 Pi 0.80.0 或更高版本。`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return version;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detectPiVersion() {
|
|
58
|
+
const result = spawnSync("pi", ["--version"], {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
shell: process.platform === "win32",
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
62
|
+
});
|
|
63
|
+
return result.status === 0 ? `${result.stdout || ""} ${result.stderr || ""}`.trim() : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parsePiModelIds(value) {
|
|
67
|
+
return [...new Set(String(value || "")
|
|
68
|
+
.split(/[\n,]+/u)
|
|
69
|
+
.map((item) => item.trim())
|
|
70
|
+
.filter(Boolean))];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOpenAiUrlPathname(value) {
|
|
74
|
+
const input = String(value || "").trim();
|
|
75
|
+
try {
|
|
76
|
+
return new URL(input).pathname.replace(/\/+$/u, "");
|
|
77
|
+
} catch {
|
|
78
|
+
return input.split(/[?#]/u)[0].replace(/\/+$/u, "");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inferPiApiFromBaseUrl(value) {
|
|
83
|
+
const pathname = getOpenAiUrlPathname(value);
|
|
84
|
+
if (pathname.endsWith("/v1/chat/completions")) {
|
|
85
|
+
return "openai-completions";
|
|
86
|
+
}
|
|
87
|
+
if (pathname.endsWith("/v1/responses")) {
|
|
88
|
+
return "openai-responses";
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeOpenAiBaseUrl(value) {
|
|
94
|
+
const input = String(value || "").trim();
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(input);
|
|
97
|
+
let pathname = url.pathname.replace(/\/+$/u, "")
|
|
98
|
+
.replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
|
|
99
|
+
if (!pathname.endsWith("/v1")) {
|
|
100
|
+
pathname = `${pathname}/v1`;
|
|
101
|
+
}
|
|
102
|
+
url.pathname = pathname;
|
|
103
|
+
return url.toString();
|
|
104
|
+
} catch {
|
|
105
|
+
const baseUrl = input.replace(/\/+$/u, "")
|
|
106
|
+
.replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
|
|
107
|
+
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getPiApiPromptOptions() {
|
|
112
|
+
return [
|
|
113
|
+
{ value: "openai-completions", label: "OpenAI Chat Completions(推荐)" },
|
|
114
|
+
{ value: "openai-responses", label: "OpenAI Responses API" }
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {}) {
|
|
119
|
+
const provider = modelsConfig.providers?.[piProviderId] && typeof modelsConfig.providers[piProviderId] === "object"
|
|
120
|
+
? modelsConfig.providers[piProviderId]
|
|
121
|
+
: {};
|
|
122
|
+
const credential = auth[piProviderId] && typeof auth[piProviderId] === "object"
|
|
123
|
+
? auth[piProviderId]
|
|
124
|
+
: {};
|
|
125
|
+
const authApiKey = credential.type === "api_key" && typeof credential.key === "string"
|
|
126
|
+
? credential.key
|
|
127
|
+
: "";
|
|
128
|
+
const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
|
|
129
|
+
return {
|
|
130
|
+
baseUrl: provider.baseUrl || piDefaultBaseUrl,
|
|
131
|
+
api: provider.api || piDefaultApi,
|
|
132
|
+
apiKey: authApiKey || provider.apiKey || "",
|
|
133
|
+
modelIds: models.map((model) => model.id),
|
|
134
|
+
defaultModel: settings.defaultProvider === piProviderId && settings.defaultModel
|
|
135
|
+
? settings.defaultModel
|
|
136
|
+
: models[0]?.id || piDefaultModel
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildPiModelConfig(modelId, existingModel = {}) {
|
|
141
|
+
return {
|
|
142
|
+
...existingModel,
|
|
143
|
+
id: modelId,
|
|
144
|
+
name: existingModel.name || modelId,
|
|
145
|
+
reasoning: existingModel.reasoning ?? true,
|
|
146
|
+
input: Array.isArray(existingModel.input) ? existingModel.input : ["text", "image"],
|
|
147
|
+
contextWindow: existingModel.contextWindow ?? 262144,
|
|
148
|
+
maxTokens: existingModel.maxTokens ?? 64000
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasPiInsecureTlsSetting(modelsConfig = {}) {
|
|
153
|
+
const headers = modelsConfig.providers?.[piProviderId]?.headers;
|
|
154
|
+
return headers && typeof headers === "object"
|
|
155
|
+
? Object.keys(headers).some((key) => key.toLowerCase() === piInsecureTlsHeader)
|
|
156
|
+
: false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, modelIds, insecureTls = false }) {
|
|
160
|
+
const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
|
|
161
|
+
const currentProvider = providers[piProviderId] && typeof providers[piProviderId] === "object" ? providers[piProviderId] : {};
|
|
162
|
+
const headers = currentProvider.headers && typeof currentProvider.headers === "object"
|
|
163
|
+
? { ...currentProvider.headers }
|
|
164
|
+
: {};
|
|
165
|
+
for (const key of Object.keys(headers)) {
|
|
166
|
+
if (key.toLowerCase() === piInsecureTlsHeader) {
|
|
167
|
+
delete headers[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (insecureTls) {
|
|
171
|
+
headers[piInsecureTlsHeader] = new URL(baseUrl).origin;
|
|
172
|
+
}
|
|
173
|
+
const existingModels = new Map(
|
|
174
|
+
(Array.isArray(currentProvider.models) ? currentProvider.models : [])
|
|
175
|
+
.filter((model) => model?.id)
|
|
176
|
+
.map((model) => [model.id, model])
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const provider = {
|
|
180
|
+
...currentProvider,
|
|
181
|
+
baseUrl,
|
|
182
|
+
api,
|
|
183
|
+
compat: {
|
|
184
|
+
...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
|
|
185
|
+
supportsDeveloperRole: false
|
|
186
|
+
},
|
|
187
|
+
models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
|
|
188
|
+
};
|
|
189
|
+
delete provider.apiKey;
|
|
190
|
+
if (Object.keys(headers).length) {
|
|
191
|
+
provider.headers = headers;
|
|
192
|
+
} else {
|
|
193
|
+
delete provider.headers;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...modelsConfig,
|
|
198
|
+
providers: {
|
|
199
|
+
...providers,
|
|
200
|
+
[piProviderId]: provider
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildPiAuthConfig(auth = {}, apiKey) {
|
|
206
|
+
const credential = auth[piProviderId] && typeof auth[piProviderId] === "object" ? auth[piProviderId] : {};
|
|
207
|
+
return {
|
|
208
|
+
...auth,
|
|
209
|
+
[piProviderId]: {
|
|
210
|
+
...credential,
|
|
211
|
+
type: "api_key",
|
|
212
|
+
key: apiKey
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildPiSettingsConfig(settings = {}, defaultModel) {
|
|
218
|
+
return {
|
|
219
|
+
...settings,
|
|
220
|
+
defaultProvider: piProviderId,
|
|
221
|
+
defaultModel,
|
|
222
|
+
defaultThinkingLevel: settings.defaultThinkingLevel || "high",
|
|
223
|
+
enableSkillCommands: settings.enableSkillCommands ?? true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function readExistingPiConfiguration(paths, operations = {}) {
|
|
228
|
+
const readAuth = operations.readAuth ?? ((path) => readJsonFileSafe(path, {}, { sensitive: true }));
|
|
229
|
+
const readModels = operations.readModels ?? ((path) => readJsoncFileSafe(path, {}, { sensitive: true }));
|
|
230
|
+
const readSettings = operations.readSettings ?? ((path) => readJsonFileSafe(path, {}));
|
|
231
|
+
const auth = await readAuth(paths.piAuthPath);
|
|
232
|
+
const models = await readModels(paths.piModelsPath);
|
|
233
|
+
const settings = await readSettings(paths.piSettingsPath);
|
|
234
|
+
return { auth, models, settings };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildPiConfiguration({ auth, models, settings }, {
|
|
238
|
+
apiKey,
|
|
239
|
+
baseUrl,
|
|
240
|
+
api,
|
|
241
|
+
modelIds,
|
|
242
|
+
insecureTls,
|
|
243
|
+
defaultModel
|
|
244
|
+
}) {
|
|
245
|
+
return {
|
|
246
|
+
apiKey,
|
|
247
|
+
auth: buildPiAuthConfig(auth, apiKey),
|
|
248
|
+
models: buildPiModelsConfig(models, { baseUrl, api, modelIds, insecureTls }),
|
|
249
|
+
settings: buildPiSettingsConfig(settings, defaultModel)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function persistPiConfiguration(paths, configuration, operations = {}) {
|
|
254
|
+
const updateAuth = operations.updateAuth ?? updatePiAuthFile;
|
|
255
|
+
const writeModels = operations.writeModels ?? ((path, value) => writeText(
|
|
256
|
+
path,
|
|
257
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
258
|
+
{ sensitive: true }
|
|
259
|
+
));
|
|
260
|
+
const writeSettings = operations.writeSettings ?? ((path, value) => writeJsonFileWithBackup(path, value));
|
|
261
|
+
await updateAuth(paths.piAuthPath, configuration.apiKey);
|
|
262
|
+
await writeModels(paths.piModelsPath, configuration.models);
|
|
263
|
+
await writeSettings(paths.piSettingsPath, configuration.settings);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = async () => {}, promptApi, runtime = {}) {
|
|
267
|
+
const {
|
|
268
|
+
assertNotCancelled,
|
|
269
|
+
confirmOrCancel,
|
|
270
|
+
required,
|
|
271
|
+
requiredUnlessExisting,
|
|
272
|
+
resolvePasswordValue,
|
|
273
|
+
selectOrCancel
|
|
274
|
+
} = promptApi;
|
|
275
|
+
assertSupportedPiVersion(await (runtime.getPiVersion ?? detectPiVersion)());
|
|
276
|
+
const {
|
|
277
|
+
auth,
|
|
278
|
+
models: modelsConfig,
|
|
279
|
+
settings
|
|
280
|
+
} = await readExistingPiConfiguration(paths);
|
|
281
|
+
const existing = resolveExistingPiApiConfig(modelsConfig, settings, auth);
|
|
282
|
+
|
|
283
|
+
const baseUrlInput = await p.text({
|
|
284
|
+
message: "Pi gpt Base URL",
|
|
285
|
+
defaultValue: existing.baseUrl,
|
|
286
|
+
validate: required()
|
|
287
|
+
});
|
|
288
|
+
assertNotCancelled(baseUrlInput);
|
|
289
|
+
const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
|
|
290
|
+
const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
|
|
291
|
+
|
|
292
|
+
const insecureTls = await confirmOrCancel({
|
|
293
|
+
message: "是否仅为 Pi gpt 中转请求跳过 TLS 证书校验?仅证书无法修复时启用",
|
|
294
|
+
initialValue: hasPiInsecureTlsSetting(modelsConfig)
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const piApiOptions = getPiApiPromptOptions();
|
|
298
|
+
const api = inferredApi || await selectOrCancel({
|
|
299
|
+
message: "Pi gpt API 类型",
|
|
300
|
+
options: piApiOptions,
|
|
301
|
+
initialValue: piApiOptions.some((option) => option.value === existing.api) ? existing.api : piDefaultApi
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const apiKey = await p.password({
|
|
305
|
+
message: "Pi gpt API Key(输入 - 清除)",
|
|
306
|
+
mask: "*",
|
|
307
|
+
defaultValue: existing.apiKey || undefined,
|
|
308
|
+
validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
|
|
309
|
+
});
|
|
310
|
+
assertNotCancelled(apiKey);
|
|
311
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
|
|
312
|
+
|
|
313
|
+
const modelIdsText = await p.text({
|
|
314
|
+
message: "Pi gpt 模型 ID(多个用逗号分隔)",
|
|
315
|
+
defaultValue: (existing.modelIds.length ? existing.modelIds : [existing.defaultModel]).join(","),
|
|
316
|
+
validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
|
|
317
|
+
});
|
|
318
|
+
assertNotCancelled(modelIdsText);
|
|
319
|
+
const modelIds = parsePiModelIds(modelIdsText);
|
|
320
|
+
|
|
321
|
+
const defaultModel = await p.text({
|
|
322
|
+
message: "Pi 默认模型",
|
|
323
|
+
defaultValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
|
|
324
|
+
validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
|
|
325
|
+
});
|
|
326
|
+
assertNotCancelled(defaultModel);
|
|
327
|
+
|
|
328
|
+
const finalDefaultModel = String(defaultModel).trim();
|
|
329
|
+
const configuration = buildPiConfiguration({ auth, models: modelsConfig, settings }, {
|
|
330
|
+
apiKey: finalApiKey,
|
|
331
|
+
baseUrl,
|
|
332
|
+
api,
|
|
333
|
+
modelIds,
|
|
334
|
+
insecureTls,
|
|
335
|
+
defaultModel: finalDefaultModel
|
|
336
|
+
});
|
|
337
|
+
await ensurePiResourcesLinked(paths);
|
|
338
|
+
await persistPiConfiguration(paths, configuration);
|
|
339
|
+
|
|
340
|
+
p.log.step(`已更新 ${pathToLabel(paths.piModelsPath, paths.homeDir)} (${piProviderId}, ${baseUrl})`);
|
|
341
|
+
p.log.step(`已更新 ${pathToLabel(paths.piSettingsPath, paths.homeDir)} (默认模型: ${finalDefaultModel})`);
|
|
342
|
+
p.log.step(`已更新 ${pathToLabel(paths.piAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
|
|
343
|
+
p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(paths.piAgentDir, "extensions"), paths.homeDir)}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export {
|
|
347
|
+
assertSupportedPiVersion,
|
|
348
|
+
buildPiAuthConfig,
|
|
349
|
+
buildPiConfiguration,
|
|
350
|
+
buildPiModelsConfig,
|
|
351
|
+
buildPiSettingsConfig,
|
|
352
|
+
configurePiApi,
|
|
353
|
+
getPiApiPromptOptions,
|
|
354
|
+
hasPiInsecureTlsSetting,
|
|
355
|
+
inferPiApiFromBaseUrl,
|
|
356
|
+
normalizeOpenAiBaseUrl,
|
|
357
|
+
parsePiModelIds,
|
|
358
|
+
persistPiConfiguration,
|
|
359
|
+
readExistingPiConfiguration,
|
|
360
|
+
resolveExistingPiApiConfig,
|
|
361
|
+
updatePiAuthFile
|
|
362
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { readDotenvFile, updateDotenvFile } from "../config/store.mjs";
|
|
5
|
+
import { pathToLabel } from "../paths.mjs";
|
|
6
|
+
|
|
7
|
+
const grokDefaults = Object.freeze(JSON.parse(readFileSync(
|
|
8
|
+
new URL("../../skills/grok-search/defaults.json", import.meta.url),
|
|
9
|
+
"utf8"
|
|
10
|
+
)));
|
|
11
|
+
|
|
12
|
+
function readSkillEnvFile(path) {
|
|
13
|
+
return readDotenvFile(path, { sensitive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function updateSkillEnvFile(path, updates) {
|
|
17
|
+
return updateDotenvFile(path, updates, { sensitive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function configureGrokSearchEnv(paths, ensureWorkflowPresent = async () => {}, promptApi) {
|
|
21
|
+
const { assertNotCancelled, confirmOrCancel, required, requiredUnlessExisting, resolvePasswordValue } = promptApi;
|
|
22
|
+
await ensureWorkflowPresent(paths);
|
|
23
|
+
const envPath = join(paths.agentsDir, "skills", "grok-search", ".env");
|
|
24
|
+
const existing = await readSkillEnvFile(envPath);
|
|
25
|
+
const baseUrl = await p.text({
|
|
26
|
+
message: "Grok API URL",
|
|
27
|
+
defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1",
|
|
28
|
+
validate: required()
|
|
29
|
+
});
|
|
30
|
+
assertNotCancelled(baseUrl);
|
|
31
|
+
|
|
32
|
+
const apiKey = await p.password({
|
|
33
|
+
message: "Grok API Key(输入 - 清除)",
|
|
34
|
+
mask: "*",
|
|
35
|
+
defaultValue: existing.GROK_API_KEY || undefined,
|
|
36
|
+
validate: requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
|
|
37
|
+
});
|
|
38
|
+
assertNotCancelled(apiKey);
|
|
39
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
|
|
40
|
+
|
|
41
|
+
const model = await p.text({
|
|
42
|
+
message: "Grok 默认模型",
|
|
43
|
+
defaultValue: existing.GROK_MODEL || grokDefaults.model,
|
|
44
|
+
validate: required()
|
|
45
|
+
});
|
|
46
|
+
assertNotCancelled(model);
|
|
47
|
+
|
|
48
|
+
const useTavily = await confirmOrCancel({
|
|
49
|
+
message: "是否同时配置 Tavily 作为额外搜索源?",
|
|
50
|
+
initialValue: Boolean(existing.TAVILY_API_KEY)
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const tavilyKey = useTavily
|
|
54
|
+
? await p.password({
|
|
55
|
+
message: "Tavily API Key(输入 - 清除)",
|
|
56
|
+
mask: "*",
|
|
57
|
+
defaultValue: existing.TAVILY_API_KEY || undefined,
|
|
58
|
+
validate: requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
|
|
59
|
+
})
|
|
60
|
+
: "";
|
|
61
|
+
if (useTavily) assertNotCancelled(tavilyKey);
|
|
62
|
+
const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
|
|
63
|
+
|
|
64
|
+
await updateSkillEnvFile(envPath, {
|
|
65
|
+
GROK_API_URL: baseUrl,
|
|
66
|
+
GROK_API_KEY: finalApiKey,
|
|
67
|
+
GROK_MODEL: model,
|
|
68
|
+
TAVILY_API_KEY: finalTavilyKey,
|
|
69
|
+
TAVILY_ENABLED: useTavily ? "true" : null
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
p.log.step(`已写入 ${pathToLabel(envPath)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function configureContext7Env(paths, ensureWorkflowPresent = async () => {}, promptApi) {
|
|
76
|
+
const { assertNotCancelled, resolvePasswordValue } = promptApi;
|
|
77
|
+
await ensureWorkflowPresent(paths);
|
|
78
|
+
const envPath = join(paths.agentsDir, "skills", "context7-auto-research", ".env");
|
|
79
|
+
const existing = await readSkillEnvFile(envPath);
|
|
80
|
+
const apiKey = await p.password({
|
|
81
|
+
message: "Context7 API Key (可选,输入 - 清除)",
|
|
82
|
+
mask: "*",
|
|
83
|
+
defaultValue: existing.CONTEXT7_API_KEY || undefined
|
|
84
|
+
});
|
|
85
|
+
assertNotCancelled(apiKey);
|
|
86
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
|
|
87
|
+
|
|
88
|
+
await updateSkillEnvFile(envPath, {
|
|
89
|
+
CONTEXT7_API_KEY: finalApiKey
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
p.log.step(`已写入 ${pathToLabel(envPath)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasPromptEnhancerApiConfig(config) {
|
|
96
|
+
return [config.PE_API_URL, config.PE_API_KEY, config.PE_MODEL]
|
|
97
|
+
.every((value) => typeof value === "string" && value.trim() !== "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolvePromptEnhancerMode(existing) {
|
|
101
|
+
return hasPromptEnhancerApiConfig(existing) ? "openai-compatible" : "agent";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildPromptEnhancerEnvUpdates(apiUrl = null, apiKey = null, model = null) {
|
|
105
|
+
return { PE_API_URL: apiUrl, PE_API_KEY: apiKey, PE_MODEL: model };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function configurePromptEnhancerEnv(paths, ensureWorkflowPresent = async () => {}, promptApi) {
|
|
109
|
+
const { assertNotCancelled, required, requiredUnlessExisting, resolvePasswordValue, selectOrCancel } = promptApi;
|
|
110
|
+
await ensureWorkflowPresent(paths);
|
|
111
|
+
const envPath = join(paths.agentsDir, "skills", "prompt-enhancer", ".env");
|
|
112
|
+
const existing = await readSkillEnvFile(envPath);
|
|
113
|
+
const mode = await selectOrCancel({
|
|
114
|
+
message: "请选择 prompt-enhancer 的运行方式",
|
|
115
|
+
options: [
|
|
116
|
+
{ value: "openai-compatible", label: "第三方 OpenAI 兼容接口" },
|
|
117
|
+
{ value: "agent", label: "直接使用当前 Agent" }
|
|
118
|
+
],
|
|
119
|
+
initialValue: resolvePromptEnhancerMode(existing)
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (mode === "openai-compatible") {
|
|
123
|
+
const apiUrl = await p.text({
|
|
124
|
+
message: "PE_API_URL",
|
|
125
|
+
defaultValue: existing.PE_API_URL || undefined,
|
|
126
|
+
validate: required()
|
|
127
|
+
});
|
|
128
|
+
assertNotCancelled(apiUrl);
|
|
129
|
+
|
|
130
|
+
const apiKey = await p.password({
|
|
131
|
+
message: "PE_API_KEY(输入 - 清除)",
|
|
132
|
+
mask: "*",
|
|
133
|
+
defaultValue: existing.PE_API_KEY || undefined,
|
|
134
|
+
validate: requiredUnlessExisting(existing.PE_API_KEY, "PE_API_KEY 不能为空")
|
|
135
|
+
});
|
|
136
|
+
assertNotCancelled(apiKey);
|
|
137
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.PE_API_KEY);
|
|
138
|
+
|
|
139
|
+
const model = await p.text({
|
|
140
|
+
message: "PE_MODEL",
|
|
141
|
+
defaultValue: existing.PE_MODEL || undefined,
|
|
142
|
+
validate: required()
|
|
143
|
+
});
|
|
144
|
+
assertNotCancelled(model);
|
|
145
|
+
|
|
146
|
+
await updateSkillEnvFile(envPath, buildPromptEnhancerEnvUpdates(apiUrl, finalApiKey, model));
|
|
147
|
+
} else {
|
|
148
|
+
await updateSkillEnvFile(envPath, buildPromptEnhancerEnvUpdates());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
p.log.step(`已写入 ${pathToLabel(envPath)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
buildPromptEnhancerEnvUpdates,
|
|
156
|
+
configureContext7Env,
|
|
157
|
+
configureGrokSearchEnv,
|
|
158
|
+
configurePromptEnhancerEnv,
|
|
159
|
+
grokDefaults,
|
|
160
|
+
hasPromptEnhancerApiConfig,
|
|
161
|
+
readSkillEnvFile,
|
|
162
|
+
resolvePromptEnhancerMode,
|
|
163
|
+
updateSkillEnvFile
|
|
164
|
+
};
|
|
@@ -21,6 +21,13 @@ You are the general-purpose fallback sub-agent. Be useful quickly, stay within
|
|
|
21
21
|
scope, and avoid impersonating a specialist role when the task clearly belongs
|
|
22
22
|
to one.
|
|
23
23
|
|
|
24
|
+
## Development Context
|
|
25
|
+
|
|
26
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
27
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
28
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
29
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
30
|
+
|
|
24
31
|
## Priorities (in order)
|
|
25
32
|
|
|
26
33
|
1. Role fit — identify whether the request truly belongs to default
|
|
@@ -20,6 +20,13 @@ developer_instructions = """
|
|
|
20
20
|
Your only job is to map. You produce a structured evidence report that the
|
|
21
21
|
orchestrator or a worker agent uses to plan safe changes.
|
|
22
22
|
|
|
23
|
+
## Development Context
|
|
24
|
+
|
|
25
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
26
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
27
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
28
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
29
|
+
|
|
23
30
|
## Rules
|
|
24
31
|
|
|
25
32
|
- Do not edit any file, ever.
|
|
@@ -25,6 +25,13 @@ safely, not like someone writing a design essay.
|
|
|
25
25
|
Your job is to turn an implementation request into an execution-ready plan
|
|
26
26
|
that minimizes ambiguity, unnecessary searching, and accidental scope growth.
|
|
27
27
|
|
|
28
|
+
## Development Context
|
|
29
|
+
|
|
30
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
31
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
32
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
33
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
34
|
+
|
|
28
35
|
## Priorities (in order)
|
|
29
36
|
|
|
30
37
|
1. Scope clarity — what is actually being changed and what is not
|
|
@@ -20,6 +20,13 @@ developer_instructions = """
|
|
|
20
20
|
Review code like an owner who will be on-call for what ships.
|
|
21
21
|
Your job is to find real problems, not to demonstrate thoroughness.
|
|
22
22
|
|
|
23
|
+
## Development Context
|
|
24
|
+
|
|
25
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
26
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
27
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
28
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
29
|
+
|
|
23
30
|
|
|
24
31
|
## Priorities (in order)
|
|
25
32
|
|
|
@@ -18,6 +18,13 @@ developer_instructions = """
|
|
|
18
18
|
You are a local implementation sub-agent. Execute bounded tasks with
|
|
19
19
|
precision and minimal footprint. You are not alone in the codebase.
|
|
20
20
|
|
|
21
|
+
## Development Context
|
|
22
|
+
|
|
23
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
24
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
25
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
26
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
27
|
+
|
|
21
28
|
## Boundaries
|
|
22
29
|
|
|
23
30
|
- Work only within the file set and module boundary authorized by the
|
|
@@ -12,10 +12,17 @@ developer_instructions = """
|
|
|
12
12
|
Act as the default orchestrator for specialized subagents.
|
|
13
13
|
|
|
14
14
|
This is a fallback orchestration policy, not the primary workflow definition.
|
|
15
|
-
If an active custom command or workflow prompt is present (for example `/
|
|
15
|
+
If an active custom command or workflow prompt is present (for example `/abel-*`),
|
|
16
16
|
its phase rules, guardrails, write scope, tool requirements, and stop/go gates
|
|
17
17
|
take precedence over the default routing below.
|
|
18
18
|
|
|
19
|
+
## Development Context
|
|
20
|
+
|
|
21
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
22
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
23
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
24
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
25
|
+
|
|
19
26
|
## Global Precedence
|
|
20
27
|
|
|
21
28
|
- Active workflow or command instructions override default orchestration
|
|
@@ -42,8 +49,8 @@ take precedence over the default routing below.
|
|
|
42
49
|
- During research or discovery phases, do not dispatch implementation workers.
|
|
43
50
|
- During planning phases, do not generate or apply code changes.
|
|
44
51
|
- During implementation or diagnose phases, respect command-specific
|
|
45
|
-
requirements such as TDD,
|
|
46
|
-
report format.
|
|
52
|
+
requirements such as TDD, root-cause verification, verification order, and
|
|
53
|
+
patch or report format.
|
|
47
54
|
- Respect command-specific write boundaries and required tools.
|
|
48
55
|
- If the active workflow requires explicit user confirmation before advancing
|
|
49
56
|
to the next phase, do not advance automatically.
|
|
@@ -55,6 +62,7 @@ take precedence over the default routing below.
|
|
|
55
62
|
- Prefer parallel subagents only when they do not violate workflow ordering or
|
|
56
63
|
gates.
|
|
57
64
|
- Assign concrete ownership with explicit scope and disjoint write sets.
|
|
65
|
+
- Copy this development context into every subagent task.
|
|
58
66
|
- Do not duplicate work across agents. Avoid overlapping exploration,
|
|
59
67
|
planning, implementation, or review tasks.
|
|
60
68
|
- Do not hand off blocked or underspecified work without enough context for
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
- Minimal comments/docs (only when necessary)
|
|
12
12
|
- Targeted changes only; never touch unrelated functionality
|
|
13
13
|
|
|
14
|
+
## Development Context
|
|
15
|
+
|
|
16
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
17
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
18
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
19
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
20
|
+
|
|
14
21
|
## Context Rules
|
|
15
22
|
|
|
16
23
|
- {{CODEBASE_RETRIEVAL_POLICY}}
|
|
@@ -27,17 +34,15 @@
|
|
|
27
34
|
## Universal Constraints
|
|
28
35
|
|
|
29
36
|
1. Use `unified diff patch` format for proposed/applied changes
|
|
30
|
-
2.
|
|
37
|
+
2. Before applying changes, state assumptions and unknowns explicitly; stop and ask the user on any critical unknown
|
|
31
38
|
|
|
32
39
|
## Stage Skill Matrix
|
|
33
40
|
|
|
34
41
|
| Skill | Research | Plan | Implement | Diagnose | Capability & Triggers |
|
|
35
42
|
| --- | :---: | :---: | :---: | :---: | --- |
|
|
36
|
-
| /sequential-think | ✅ | ✅ | ❌ | ✅ | Multi-step reasoning, hypothesis testing. Trigger: 3+ interconnected components |
|
|
37
43
|
| /grok-search | ✅ | ○ | ❌ | ✅ | Deep research, concept understanding. Trigger: architectural patterns, best practices |
|
|
38
44
|
| /context7-auto-research | ✅ | ✅ | ✅ | ✅ | Official docs retrieval. Trigger: framework/library usage, APIs |
|
|
39
45
|
| /dev-browser | ○ | ○ | ✅ | ✅ | Browser automation. Trigger: E2E testing, UI verification |
|
|
40
|
-
| /confidence-check | ❌ | ✅ | ✅ | ✅ | Readiness verification. Trigger: before implementation/fix (≥90%) |
|
|
41
46
|
| /time | ○ | ✅ | ✅ | ○ | Time/timezone operations. Trigger: scheduling logic |
|
|
42
47
|
|
|
43
48
|
Legend: ✅ Primary, ○ Optional, ❌ Forbidden
|