@abelxiaoxing/cadence 1.8.0 → 1.8.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/README.md +44 -0
- package/package.json +2 -1
- package/prompts/abel-design.md +6 -0
- package/prompts/abel-diagnose.md +6 -0
- package/prompts/abel-implement.md +6 -0
- package/skills/_shared/load-config.d.mts +7 -1
- package/skills/_shared/load-config.mjs +58 -5
- package/skills/context7-auto-research/SKILL.md +5 -0
- package/skills/context7-auto-research/context7.mjs +1 -1
- package/skills/grok-search/SKILL.md +5 -0
- package/src/index.ts +220 -28
- package/src/install-config.d.mts +6 -0
- package/src/install-config.mjs +57 -0
- package/src/startup-card.ts +84 -0
package/README.md
CHANGED
|
@@ -14,6 +14,50 @@
|
|
|
14
14
|
4. 首次进入项目先运行 `/abel-init`,然后 `/abel-design <需求>`,确认集中呈现的目标与方案,设计完成后运行 `/abel-implement <change>`。
|
|
15
15
|
5. 暂停时先查看原因和建议,不要靠反复 resume 或重启“碰运气”;未完成的工作会保留。
|
|
16
16
|
|
|
17
|
+
### 每次加载时的配置提示卡
|
|
18
|
+
|
|
19
|
+
每次扩展启动、重载或恢复会话,Cadence 都会在输入框上方显示一张简短研究能力提示卡,不需要额外记住配置或诊断命令。
|
|
20
|
+
卡片只检查本地配置,显示 Context7、Grok 与可选 Tavily 的配置状态及当前生效文件;“已配置”不代表网络、认证或服务能力已验证。
|
|
21
|
+
开始处理任务后卡片收起,下次加载重新显示;不记录“已看过”或“暂时不用”来抑制后续提示。
|
|
22
|
+
安装 Cadence 时会创建用户级模板 `~/.pi/agent/cadence/.env`(已有文件不会覆盖);卡片本身不会联网、调用模型、写入配置、索要密钥或自动激活 Abel 工作流,可选服务未配置不阻塞普通任务。
|
|
23
|
+
未信任的项目只显示未检查提示,不读取研究配置。
|
|
24
|
+
|
|
25
|
+
安装后用户级模板已包含 Grok 配置占位字段;Context7 无配置文件时仍使用默认地址匿名访问,显式配置文件损坏仍会报错。
|
|
26
|
+
联网搜索需要在本地文件中配置以下内容,每项一行(替换为实际服务地址和密钥):
|
|
27
|
+
|
|
28
|
+
```dotenv
|
|
29
|
+
GROK_API_URL=https://api.x.ai/v1
|
|
30
|
+
GROK_API_KEY=<your-api-key>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
安装器会创建用户级 `~/.pi/agent/cadence/.env` 及其父目录,并且只在文件不存在时写入模板;也可手动创建该文件以跨项目复用。
|
|
34
|
+
如果项目已有 `.pi/cadence/.env`,它按整文件优先,**不会与用户级文件合并**;请编辑卡片显示的生效文件,不要覆盖已有无关配置。
|
|
35
|
+
研究 Skills 不读取 shell 中的 API 环境变量,也不读取安装包旁的 `.env`;保存后下次 Skill 调用生效,无需重启。
|
|
36
|
+
可选 `CONTEXT7_API_KEY` 提供认证访问;可选 `TAVILY_API_KEY` 启用扩展检索、抓取和站点映射,`TAVILY_ENABLED=false` 可关闭它。
|
|
37
|
+
完整字段见 [配置模板](config/.env.example),配置文件限普通文件且不超过 64 KiB。
|
|
38
|
+
密钥只在本地编辑,勿发送到聊天或提交 Git;使用项目级文件时应将其加入忽略规则。
|
|
39
|
+
|
|
40
|
+
TUI 显示卡片;RPC 通过非阻塞 `setWidget` 事件发送同一卡片,Web UI 需实现该事件的展示。
|
|
41
|
+
Print / JSON 无交互界面,不额外输出卡片、不等待输入,也不污染标准输出;可按上述路径直接配置。
|
|
42
|
+
本版本提供提示与本地编辑指引,不提供密钥输入向导或自动重试搜索。
|
|
43
|
+
|
|
44
|
+
### Pi / Web UI 阶段启动约定
|
|
45
|
+
|
|
46
|
+
阶段命令需要 Cadence 扩展与包内 prompts 同时加载;只有提示正文可见,不代表 `abel_dispatch` 已注册或已激活。
|
|
47
|
+
Web UI / SDK 集成应在会话空闲时提交原始命令:`session.prompt("/abel-design <需求>", { source: "rpc" })`。
|
|
48
|
+
不要在客户端预展开模板,也不要通过 `steer()`、`followUp()` 或运行中的排队请求启动阶段。
|
|
49
|
+
运行中的显式阶段命令会被拒绝并报告 `abel-stage-requires-idle`,等待当前请求结束后重新提交即可;原有阶段不会因此被退出或丢弃。
|
|
50
|
+
|
|
51
|
+
启动失败有明确错误码:`abel-stage-provenance-invalid`(包来源不匹配)、`abel-stage-unverified-input`(缺少可验证的原始调用)和 `abel-stage-tools-unavailable`(工具未暴露或 Design 工具边界设置失败)。
|
|
52
|
+
扩展会阻止失败请求的工具执行;不能改用 bash、subagent 或 terminal 工具绕过。
|
|
53
|
+
空闲时正常启动会在首次模型请求前启用 `abel_dispatch` 并限制 Design 工具集合,显式退出后恢复进入前的工具集合。
|
|
54
|
+
恢复会话或重载不会从历史提示自动激活阶段。
|
|
55
|
+
|
|
56
|
+
如果扩展根本没有加载,扩展自身无法执行检查;提示中的缺失工具说明只是模型侧兜底,不是宿主级拦截。
|
|
57
|
+
Web UI 必须展示资源加载器的 `getExtensions().errors`,核实当前会话已绑定扩展,并在发送阶段提示前检查 `session.getAllTools()` 中的 `abel_dispatch`;不存在时直接报告配置错误,不发送阶段正文。
|
|
58
|
+
请检查包资源过滤是否禁用了 extensions、实际运行的 Node / Pi 版本,以及是否恢复了旧的工具清单。
|
|
59
|
+
诊断时只记录工具名、命令来源和脱敏错误,不记录密钥或完整需求。
|
|
60
|
+
|
|
17
61
|
### 先检查环境,不先花模型调用
|
|
18
62
|
|
|
19
63
|
在 Cadence 开发仓库中可用:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Four explicit Abel workflow prompts with stage-isolated design and diagnosis, durable resumable implementation, and three package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
+
"postinstall": "node src/install-config.mjs",
|
|
40
41
|
"pack:check": "node scripts/pack-check.mjs",
|
|
41
42
|
"traceability:check": "node scripts/traceability-check.mjs",
|
|
42
43
|
"verify": "bun run check && bun run lint && bun run test && bun run pack:check",
|
package/prompts/abel-design.md
CHANGED
|
@@ -16,6 +16,12 @@ $ARGUMENTS
|
|
|
16
16
|
|
|
17
17
|
<!-- ABEL:PROMPT:abel-design -->
|
|
18
18
|
|
|
19
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
20
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
21
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
22
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
23
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
24
|
+
|
|
19
25
|
This stage is scoped to the invoked task.
|
|
20
26
|
Direct answers and same-task continuations stay in this stage.
|
|
21
27
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
package/prompts/abel-diagnose.md
CHANGED
|
@@ -13,6 +13,12 @@ $ARGUMENTS
|
|
|
13
13
|
|
|
14
14
|
<!-- ABEL:PROMPT:abel-diagnose -->
|
|
15
15
|
|
|
16
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
17
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
18
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
19
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
20
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
21
|
+
|
|
16
22
|
This stage is scoped to the invoked task.
|
|
17
23
|
Direct answers and same-task continuations stay in this stage.
|
|
18
24
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
|
@@ -16,6 +16,12 @@ $ARGUMENTS
|
|
|
16
16
|
|
|
17
17
|
<!-- ABEL:PROMPT:abel-implement -->
|
|
18
18
|
|
|
19
|
+
Before following this procedure, check that `abel_dispatch` is actually callable in this model request.
|
|
20
|
+
If it is missing, stop and report `Cadence configuration error: abel-stage-tools-unavailable`.
|
|
21
|
+
Do not substitute bash, subagent, or terminal tools, and do not claim the stage started.
|
|
22
|
+
Ask the operator to enable the Cadence extension together with its package prompts, inspect extension-load errors and tool filters, then resubmit the original slash command while the session is idle.
|
|
23
|
+
A prompt marker or historical active/inactive message is not evidence that the tool is available.
|
|
24
|
+
|
|
19
25
|
This stage is scoped to the invoked task.
|
|
20
26
|
Direct answers and same-task continuations stay in this stage.
|
|
21
27
|
If the user ends the workflow or requests an unrelated task, first send `{"action":"finish"}` to `abel_dispatch`, then handle the new task normally with the restored tools.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
2
2
|
|
|
3
|
+
export declare function selectConfigPath(options: {
|
|
4
|
+
cwd: string;
|
|
5
|
+
home: string;
|
|
6
|
+
}): string | null;
|
|
7
|
+
|
|
3
8
|
export declare function loadConfig(options: {
|
|
4
9
|
cwd: string;
|
|
5
10
|
home: string;
|
|
6
11
|
required?: readonly string[];
|
|
7
|
-
|
|
12
|
+
allowMissing?: boolean;
|
|
13
|
+
}): { path: string | null; values: Record<string, string> };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
fstatSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readSync,
|
|
8
|
+
} from "node:fs";
|
|
2
9
|
import path from "node:path";
|
|
3
10
|
|
|
4
11
|
export function parseEnvFile(content) {
|
|
@@ -37,22 +44,68 @@ export function parseEnvFile(content) {
|
|
|
37
44
|
return values;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
function pathEntryExists(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
// lstat, rather than existsSync, intentionally retains dangling symlinks.
|
|
50
|
+
// They are an explicit configuration path and must fail closed when opened.
|
|
51
|
+
lstatSync(filePath);
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === "ENOENT") return false;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function selectConfigPath({ cwd, home }) {
|
|
41
60
|
const projectPath = path.join(cwd, ".pi", "cadence", ".env");
|
|
42
61
|
const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
|
|
43
|
-
|
|
62
|
+
return pathEntryExists(projectPath)
|
|
44
63
|
? projectPath
|
|
45
|
-
:
|
|
64
|
+
: pathEntryExists(userPath)
|
|
46
65
|
? userPath
|
|
47
66
|
: null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadConfig({ cwd, home, required = [], allowMissing = false }) {
|
|
70
|
+
const selectedPath = selectConfigPath({ cwd, home });
|
|
71
|
+
if (!selectedPath && allowMissing && required.length === 0) {
|
|
72
|
+
return { path: null, values: {} };
|
|
73
|
+
}
|
|
48
74
|
|
|
49
75
|
if (!selectedPath) {
|
|
76
|
+
const projectPath = path.join(cwd, ".pi", "cadence", ".env");
|
|
77
|
+
const userPath = path.join(home, ".pi", "agent", "cadence", ".env");
|
|
50
78
|
throw new Error(
|
|
51
79
|
`No cadence configuration file found; create ${projectPath} or ${userPath}`,
|
|
52
80
|
);
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
// Configuration inspection also runs at extension startup: never block on
|
|
84
|
+
// a FIFO/device or read an unbounded file. Read one extra byte to detect growth.
|
|
85
|
+
const maxBytes = 64 * 1024;
|
|
86
|
+
const fd = openSync(selectedPath, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
87
|
+
let content;
|
|
88
|
+
try {
|
|
89
|
+
const stat = fstatSync(fd);
|
|
90
|
+
if (!stat.isFile() || stat.size > maxBytes) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Cadence configuration must be a regular file of at most 64 KiB",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
96
|
+
let size = 0;
|
|
97
|
+
while (size < buffer.length) {
|
|
98
|
+
const count = readSync(fd, buffer, size, buffer.length - size, null);
|
|
99
|
+
if (!count) break;
|
|
100
|
+
size += count;
|
|
101
|
+
}
|
|
102
|
+
if (size > maxBytes)
|
|
103
|
+
throw new Error("Cadence configuration exceeds 64 KiB");
|
|
104
|
+
content = buffer.subarray(0, size).toString("utf8");
|
|
105
|
+
} finally {
|
|
106
|
+
closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
const values = parseEnvFile(content);
|
|
56
109
|
const missing = required.filter((name) => !values[name]);
|
|
57
110
|
if (missing.length > 0) {
|
|
58
111
|
throw new Error(
|
|
@@ -30,3 +30,8 @@ Treat a non-zero exit, invalid shape, or empty useful result as a failed lookup;
|
|
|
30
30
|
The CLI selects the project `.pi/cadence/.env` as a whole file when present, otherwise `~/.pi/agent/cadence/.env`.
|
|
31
31
|
It does not merge files, read ambient API configuration, or read `.env` beside the installed package.
|
|
32
32
|
`CONTEXT7_API_KEY` may be empty; `CONTEXT7_API_URL` defaults to `https://context7.com/api/v2`.
|
|
33
|
+
When neither configuration file exists, use those anonymous defaults without creating a file.
|
|
34
|
+
An existing malformed or unreadable file is still an error; never silently fall back.
|
|
35
|
+
Cadence shows a local configuration card on every extension load in TUI/RPC; configured does not mean network-verified.
|
|
36
|
+
Configuration files must be regular files of at most 64 KiB.
|
|
37
|
+
Never ask for API keys in chat.
|
|
@@ -9,7 +9,7 @@ import { loadConfig } from "../_shared/load-config.mjs";
|
|
|
9
9
|
const DEFAULT_API_URL = "https://context7.com/api/v2";
|
|
10
10
|
|
|
11
11
|
export function loadContext7Config({ cwd, home }) {
|
|
12
|
-
const selected = loadConfig({ cwd, home });
|
|
12
|
+
const selected = loadConfig({ cwd, home, allowMissing: true });
|
|
13
13
|
return {
|
|
14
14
|
path: selected.path,
|
|
15
15
|
apiUrl: selected.values.CONTEXT7_API_URL || DEFAULT_API_URL,
|
|
@@ -36,4 +36,9 @@ Treat non-zero exits and invalid result shapes as failed research.
|
|
|
36
36
|
|
|
37
37
|
The CLI selects the project `.pi/cadence/.env` as a whole file when present, otherwise `~/.pi/agent/cadence/.env`.
|
|
38
38
|
`GROK_API_URL` and `GROK_API_KEY` are required.
|
|
39
|
+
Cadence shows the selected configuration path and missing fields on every extension load in TUI/RPC, without network verification.
|
|
40
|
+
If configuration is missing, direct the user to edit that local file (prefer the user file when no project file exists); never ask them to paste a key in chat.
|
|
41
|
+
The CLI does not read shell API variables.
|
|
42
|
+
Save one `NAME=value` per line; the next invocation reads the changes without restarting Pi.
|
|
43
|
+
Configuration files must be regular files of at most 64 KiB.
|
|
39
44
|
`GROK_MODEL` defaults to `grok-4.20-non-reasoning`, `TAVILY_API_URL` defaults to `https://api.tavily.com`, and Tavily is enabled by default once a non-empty `TAVILY_API_KEY` is configured; set `TAVILY_ENABLED=false` to opt out.
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export { executePackageVerification } from "./package-verification.ts";
|
|
|
21
21
|
// workflow routing (abel-design/implement/diagnose provenance) in the prompts
|
|
22
22
|
// integration; abel-init and ordinary prompts never activate dispatch.
|
|
23
23
|
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { homedir } from "node:os";
|
|
24
26
|
import { dirname, join } from "node:path";
|
|
25
27
|
import { fileURLToPath } from "node:url";
|
|
26
28
|
import type {
|
|
@@ -57,7 +59,7 @@ import { canonicalJson } from "./implement-graph.ts";
|
|
|
57
59
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
58
60
|
|
|
59
61
|
import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
|
|
60
|
-
|
|
62
|
+
import { STARTUP_CARD_KEY, startupCardLines } from "./startup-card.ts";
|
|
61
63
|
import { StateRootError } from "./state-root.ts";
|
|
62
64
|
import {
|
|
63
65
|
ACTIVITY_DETAILS_KEY,
|
|
@@ -369,6 +371,27 @@ function hasExpandedPromptMarker(
|
|
|
369
371
|
);
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
// A marker is never authority. Recognize only the expanded entrypoint shape
|
|
375
|
+
// here so quoted documentation and ordinary command discussions stay ordinary.
|
|
376
|
+
function expandedStagePrompt(text: string): EligiblePrompt | undefined {
|
|
377
|
+
return ELIGIBLE_PROMPTS.find(
|
|
378
|
+
(name) =>
|
|
379
|
+
text.startsWith(
|
|
380
|
+
`This procedure applies only when the user explicitly invokes \`/${name}\`.`,
|
|
381
|
+
) && hasExpandedPromptMarker(text, name),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type StageStartupError =
|
|
386
|
+
| "abel-stage-requires-idle"
|
|
387
|
+
| "abel-stage-provenance-invalid"
|
|
388
|
+
| "abel-stage-unverified-input"
|
|
389
|
+
| "abel-stage-tools-unavailable";
|
|
390
|
+
|
|
391
|
+
function startupErrorText(code: StageStartupError): string {
|
|
392
|
+
return `Cadence configuration error: ${code}. Do not execute this workflow or substitute shell/subagent tools for abel_dispatch. Ensure the Cadence extension and its package prompts are enabled, inspect host extension-load errors and tool filters, then submit the original slash command through session.prompt when idle. Do not pre-expand the template or queue it through steer/followUp. No new stage authority was granted by this rejected request.`;
|
|
393
|
+
}
|
|
394
|
+
|
|
372
395
|
function isVerifiedStageInvocation(
|
|
373
396
|
pi: ExtensionAPI,
|
|
374
397
|
activation: Activation,
|
|
@@ -719,6 +742,21 @@ export function registerWorkflowControl(
|
|
|
719
742
|
let exitingStage = false;
|
|
720
743
|
let activePrompt: EligiblePrompt | undefined;
|
|
721
744
|
let designToolSnapshot: string[] | undefined;
|
|
745
|
+
let startupFailure: StageStartupError | undefined;
|
|
746
|
+
let admittedPrompt: string | undefined;
|
|
747
|
+
let latestContextUser: string | undefined;
|
|
748
|
+
const admittedMessages = new Set<string>();
|
|
749
|
+
const reportStartupFailure = (code: StageStartupError) => {
|
|
750
|
+
pi.sendMessage(
|
|
751
|
+
{
|
|
752
|
+
customType: "abel-stage-startup-error",
|
|
753
|
+
content: startupErrorText(code),
|
|
754
|
+
display: true,
|
|
755
|
+
details: { code },
|
|
756
|
+
},
|
|
757
|
+
{ triggerTurn: false },
|
|
758
|
+
);
|
|
759
|
+
};
|
|
722
760
|
const activation = new (class implements Activation {
|
|
723
761
|
state: Activation["state"] = "inactive";
|
|
724
762
|
|
|
@@ -791,6 +829,18 @@ export function registerWorkflowControl(
|
|
|
791
829
|
),
|
|
792
830
|
);
|
|
793
831
|
};
|
|
832
|
+
const stageToolsAvailable = () => {
|
|
833
|
+
if (!activation.isActive()) return true;
|
|
834
|
+
const tools = pi.getActiveTools();
|
|
835
|
+
return (
|
|
836
|
+
tools.includes(DISPATCH_TOOL) &&
|
|
837
|
+
(activePrompt !== "abel-design" ||
|
|
838
|
+
tools.every(
|
|
839
|
+
(name) =>
|
|
840
|
+
name === DISPATCH_TOOL || DESIGN_PARENT_READ_TOOLS.has(name),
|
|
841
|
+
))
|
|
842
|
+
);
|
|
843
|
+
};
|
|
794
844
|
const deactivate = () => {
|
|
795
845
|
activation.drain();
|
|
796
846
|
if (designToolSnapshot) {
|
|
@@ -900,7 +950,6 @@ export function registerWorkflowControl(
|
|
|
900
950
|
|
|
901
951
|
const registerDispatchTool = (kind: "command" | "packet") => {
|
|
902
952
|
if (registeredParameterKind === kind) return;
|
|
903
|
-
registeredParameterKind = kind;
|
|
904
953
|
pi.registerTool({
|
|
905
954
|
name: DISPATCH_TOOL,
|
|
906
955
|
label: "Abel Control",
|
|
@@ -926,6 +975,7 @@ export function registerWorkflowControl(
|
|
|
926
975
|
ctx: ExtensionContext,
|
|
927
976
|
) {
|
|
928
977
|
if (
|
|
978
|
+
startupFailure ||
|
|
929
979
|
exitingStage ||
|
|
930
980
|
!activation.isActive() ||
|
|
931
981
|
activePrompt === undefined
|
|
@@ -1353,6 +1403,7 @@ export function registerWorkflowControl(
|
|
|
1353
1403
|
);
|
|
1354
1404
|
},
|
|
1355
1405
|
} as never);
|
|
1406
|
+
registeredParameterKind = kind;
|
|
1356
1407
|
};
|
|
1357
1408
|
registerDispatchTool("packet");
|
|
1358
1409
|
|
|
@@ -1411,6 +1462,17 @@ export function registerWorkflowControl(
|
|
|
1411
1462
|
// expose the complete workflow instructions to the parent model.
|
|
1412
1463
|
return { action: prompt || init ? "handled" : "continue" };
|
|
1413
1464
|
}
|
|
1465
|
+
if (
|
|
1466
|
+
prompt &&
|
|
1467
|
+
(event.streamingBehavior || !hasPackageProvenance(pi, prompt))
|
|
1468
|
+
) {
|
|
1469
|
+
reportStartupFailure(
|
|
1470
|
+
event.streamingBehavior
|
|
1471
|
+
? "abel-stage-requires-idle"
|
|
1472
|
+
: "abel-stage-provenance-invalid",
|
|
1473
|
+
);
|
|
1474
|
+
return { action: "handled" };
|
|
1475
|
+
}
|
|
1414
1476
|
pendingPrompt = prompt;
|
|
1415
1477
|
pendingImplementChange =
|
|
1416
1478
|
prompt === "abel-implement"
|
|
@@ -1430,6 +1492,9 @@ export function registerWorkflowControl(
|
|
|
1430
1492
|
return { action: "continue" };
|
|
1431
1493
|
});
|
|
1432
1494
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1495
|
+
startupFailure = undefined;
|
|
1496
|
+
admittedPrompt = undefined;
|
|
1497
|
+
latestContextUser = undefined;
|
|
1433
1498
|
const prompt = pendingPrompt;
|
|
1434
1499
|
const implementChange = pendingImplementChange;
|
|
1435
1500
|
const init =
|
|
@@ -1439,43 +1504,143 @@ export function registerWorkflowControl(
|
|
|
1439
1504
|
pendingInit = false;
|
|
1440
1505
|
const verified =
|
|
1441
1506
|
prompt && isVerifiedStageInvocation(pi, activation, prompt, event.prompt);
|
|
1442
|
-
if (verified)
|
|
1443
|
-
|
|
1444
|
-
|
|
1507
|
+
if (prompt && !verified) startupFailure = "abel-stage-unverified-input";
|
|
1508
|
+
if (!prompt && expandedStagePrompt(event.prompt))
|
|
1509
|
+
startupFailure = "abel-stage-unverified-input";
|
|
1510
|
+
const toolsBefore = pi.getActiveTools();
|
|
1511
|
+
const wasActive = activation.isActive();
|
|
1512
|
+
try {
|
|
1513
|
+
if (verified) {
|
|
1514
|
+
if (activePrompt === "abel-design" && prompt !== "abel-design") {
|
|
1515
|
+
restoreDesignTools();
|
|
1516
|
+
}
|
|
1517
|
+
registerDispatchTool(
|
|
1518
|
+
prompt === "abel-implement" ? "command" : "packet",
|
|
1519
|
+
);
|
|
1520
|
+
activePrompt = prompt;
|
|
1521
|
+
if (prompt === "abel-implement") {
|
|
1522
|
+
if (
|
|
1523
|
+
implementChange &&
|
|
1524
|
+
implementPromptBindsChange(event.prompt, implementChange) &&
|
|
1525
|
+
typeof ctx.cwd === "string"
|
|
1526
|
+
)
|
|
1527
|
+
implementContinuation.activate({
|
|
1528
|
+
cwd: ctx.cwd,
|
|
1529
|
+
change: implementChange,
|
|
1530
|
+
});
|
|
1531
|
+
else implementContinuation.deactivate();
|
|
1532
|
+
} else {
|
|
1533
|
+
implementContinuation.deactivate();
|
|
1534
|
+
}
|
|
1445
1535
|
}
|
|
1446
|
-
|
|
1447
|
-
activePrompt
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1536
|
+
if (prompt) activateDispatcher(pi, activation, prompt, event.prompt);
|
|
1537
|
+
if (activation.isActive() && activePrompt === "abel-design")
|
|
1538
|
+
enforceDesignTools();
|
|
1539
|
+
if (!stageToolsAvailable())
|
|
1540
|
+
startupFailure = "abel-stage-tools-unavailable";
|
|
1541
|
+
} catch {
|
|
1542
|
+
startupFailure = "abel-stage-tools-unavailable";
|
|
1543
|
+
}
|
|
1544
|
+
if (
|
|
1545
|
+
startupFailure === "abel-stage-tools-unavailable" &&
|
|
1546
|
+
verified &&
|
|
1547
|
+
!wasActive
|
|
1548
|
+
) {
|
|
1549
|
+
// Roll back the failed startup, not a retained durable run. Never leave
|
|
1550
|
+
// partially applied Design restrictions or a falsely active stage.
|
|
1551
|
+
activePrompt = undefined;
|
|
1552
|
+
activation.drain();
|
|
1553
|
+
implementContinuation.deactivate();
|
|
1554
|
+
designToolSnapshot = undefined;
|
|
1555
|
+
try {
|
|
1556
|
+
pi.setActiveTools(toolsBefore);
|
|
1557
|
+
} catch {
|
|
1558
|
+
/* tool_call remains closed */
|
|
1461
1559
|
}
|
|
1462
1560
|
}
|
|
1463
|
-
if (
|
|
1464
|
-
if (activation.isActive() && activePrompt === "abel-design")
|
|
1465
|
-
enforceDesignTools();
|
|
1561
|
+
if (verified && !startupFailure) admittedPrompt = event.prompt;
|
|
1466
1562
|
if (
|
|
1467
1563
|
activation.isActive() &&
|
|
1468
1564
|
activePrompt === "abel-implement" &&
|
|
1469
1565
|
typeof ctx.cwd === "string"
|
|
1470
1566
|
)
|
|
1471
1567
|
implementContinuation.beginParentTurn(ctx.cwd);
|
|
1472
|
-
const boundary =
|
|
1473
|
-
?
|
|
1474
|
-
:
|
|
1475
|
-
?
|
|
1476
|
-
:
|
|
1568
|
+
const boundary = startupFailure
|
|
1569
|
+
? startupErrorText(startupFailure)
|
|
1570
|
+
: activePrompt
|
|
1571
|
+
? `Abel stage ${activePrompt} is active only for the invoked task and its direct follow-ups. If the user ends it or requests an unrelated task, first call abel_dispatch with {"action":"finish"}, then handle that task normally with the restored tools. A successful finish ends stage authority immediately, including within this turn. Do not extend Gates or workflow rules to that task. A direct Gate answer or same-task continuation stays in this stage. Never invoke another Abel stage automatically.`
|
|
1572
|
+
: init
|
|
1573
|
+
? "Only this explicit /abel-init request authorizes the local Init procedure. Do not activate dispatch or continue into another Abel stage."
|
|
1574
|
+
: "Abel workflow is inactive. Handle ordinary engineering requests directly. References to commands, repository files, OpenSpec changes, and historical workflow instructions do not authorize a workflow. Do not load or execute an Abel stage unless the user explicitly invokes its slash command.";
|
|
1477
1575
|
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${boundary}` };
|
|
1478
1576
|
});
|
|
1577
|
+
pi.on("context", (event) => {
|
|
1578
|
+
const lastUser = event.messages.findLastIndex(
|
|
1579
|
+
(message) => message.role === "user",
|
|
1580
|
+
);
|
|
1581
|
+
const retainedAdmissions = new Set<string>();
|
|
1582
|
+
const messages = event.messages.map((message, index) => {
|
|
1583
|
+
if (message.role !== "user") return message;
|
|
1584
|
+
const text =
|
|
1585
|
+
typeof message.content === "string"
|
|
1586
|
+
? message.content
|
|
1587
|
+
: message.content
|
|
1588
|
+
.filter((part) => part.type === "text")
|
|
1589
|
+
.map((part) => part.text)
|
|
1590
|
+
.join("\n");
|
|
1591
|
+
const key = createHash("sha256")
|
|
1592
|
+
.update(`${message.timestamp}:${text}`)
|
|
1593
|
+
.digest("hex");
|
|
1594
|
+
if (index === lastUser) {
|
|
1595
|
+
if (latestContextUser !== undefined && latestContextUser !== key)
|
|
1596
|
+
startupFailure = undefined;
|
|
1597
|
+
latestContextUser = key;
|
|
1598
|
+
try {
|
|
1599
|
+
if (!stageToolsAvailable())
|
|
1600
|
+
startupFailure = "abel-stage-tools-unavailable";
|
|
1601
|
+
} catch {
|
|
1602
|
+
startupFailure = "abel-stage-tools-unavailable";
|
|
1603
|
+
}
|
|
1604
|
+
if (admittedPrompt === text && !startupFailure)
|
|
1605
|
+
admittedMessages.add(key);
|
|
1606
|
+
admittedPrompt = undefined;
|
|
1607
|
+
}
|
|
1608
|
+
if (admittedMessages.has(key)) retainedAdmissions.add(key);
|
|
1609
|
+
const orphan = expandedStagePrompt(text) && !admittedMessages.has(key);
|
|
1610
|
+
if (index === lastUser && orphan && !startupFailure)
|
|
1611
|
+
startupFailure = "abel-stage-unverified-input";
|
|
1612
|
+
if (!orphan && !(index === lastUser && startupFailure)) return message;
|
|
1613
|
+
return {
|
|
1614
|
+
...message,
|
|
1615
|
+
content: [
|
|
1616
|
+
{
|
|
1617
|
+
type: "text" as const,
|
|
1618
|
+
text: startupErrorText(
|
|
1619
|
+
index === lastUser && startupFailure
|
|
1620
|
+
? startupFailure
|
|
1621
|
+
: "abel-stage-unverified-input",
|
|
1622
|
+
),
|
|
1623
|
+
},
|
|
1624
|
+
],
|
|
1625
|
+
};
|
|
1626
|
+
});
|
|
1627
|
+
// Keep only identities still present after compaction; never retain raw requirements.
|
|
1628
|
+
admittedMessages.clear();
|
|
1629
|
+
for (const key of retainedAdmissions) admittedMessages.add(key);
|
|
1630
|
+
return { messages };
|
|
1631
|
+
});
|
|
1632
|
+
pi.on("tool_call", (event) => {
|
|
1633
|
+
if (!stageToolsAvailable()) startupFailure = "abel-stage-tools-unavailable";
|
|
1634
|
+
if (startupFailure)
|
|
1635
|
+
return { block: true, reason: startupErrorText(startupFailure) };
|
|
1636
|
+
// Defense in depth against a host/extension restoring a stale tool list.
|
|
1637
|
+
if (
|
|
1638
|
+
activePrompt === "abel-design" &&
|
|
1639
|
+
event.toolName !== DISPATCH_TOOL &&
|
|
1640
|
+
!DESIGN_PARENT_READ_TOOLS.has(event.toolName)
|
|
1641
|
+
)
|
|
1642
|
+
return { block: true, reason: "abel-design-tool-boundary" };
|
|
1643
|
+
});
|
|
1479
1644
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1480
1645
|
if (
|
|
1481
1646
|
activePrompt !== "abel-design" ||
|
|
@@ -1495,6 +1660,7 @@ export function registerWorkflowControl(
|
|
|
1495
1660
|
const aborted = () => runSignal?.aborted || ctx.signal?.aborted;
|
|
1496
1661
|
if (
|
|
1497
1662
|
aborted() ||
|
|
1663
|
+
startupFailure ||
|
|
1498
1664
|
exitingStage ||
|
|
1499
1665
|
!activation.isActive() ||
|
|
1500
1666
|
activePrompt !== "abel-implement"
|
|
@@ -1590,6 +1756,10 @@ export function registerWorkflowControl(
|
|
|
1590
1756
|
);
|
|
1591
1757
|
});
|
|
1592
1758
|
pi.on("session_start", async (_event, ctx) => {
|
|
1759
|
+
startupFailure = undefined;
|
|
1760
|
+
admittedPrompt = undefined;
|
|
1761
|
+
latestContextUser = undefined;
|
|
1762
|
+
admittedMessages.clear();
|
|
1593
1763
|
pendingPrompt = undefined;
|
|
1594
1764
|
pendingImplementChange = undefined;
|
|
1595
1765
|
pendingInit = false;
|
|
@@ -1603,6 +1773,10 @@ export function registerWorkflowControl(
|
|
|
1603
1773
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
1604
1774
|
});
|
|
1605
1775
|
pi.on("session_shutdown", async () => {
|
|
1776
|
+
startupFailure = undefined;
|
|
1777
|
+
admittedPrompt = undefined;
|
|
1778
|
+
latestContextUser = undefined;
|
|
1779
|
+
admittedMessages.clear();
|
|
1606
1780
|
pendingPrompt = undefined;
|
|
1607
1781
|
pendingImplementChange = undefined;
|
|
1608
1782
|
pendingInit = false;
|
|
@@ -1619,4 +1793,22 @@ export function registerWorkflowControl(
|
|
|
1619
1793
|
|
|
1620
1794
|
export default function register(pi: ExtensionAPI): void {
|
|
1621
1795
|
registerWorkflowControl(pi);
|
|
1796
|
+
// Presentation only: never persist a dismissal or inject a model message.
|
|
1797
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1798
|
+
if (!ctx.hasUI) return;
|
|
1799
|
+
ctx.ui.setWidget(
|
|
1800
|
+
STARTUP_CARD_KEY,
|
|
1801
|
+
startupCardLines({
|
|
1802
|
+
cwd: ctx.cwd,
|
|
1803
|
+
home: homedir(),
|
|
1804
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
1805
|
+
}),
|
|
1806
|
+
);
|
|
1807
|
+
});
|
|
1808
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
1809
|
+
if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
|
|
1810
|
+
});
|
|
1811
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1812
|
+
if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
|
|
1813
|
+
});
|
|
1622
1814
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const CONFIG_TEMPLATE = `# Cadence research configuration. Keep this file local and never commit it.
|
|
7
|
+
# Context7 works anonymously with its default URL.
|
|
8
|
+
# CONTEXT7_API_URL=https://context7.com/api/v2
|
|
9
|
+
# CONTEXT7_API_KEY=
|
|
10
|
+
|
|
11
|
+
# Required when using the Grok search skill.
|
|
12
|
+
GROK_API_URL=https://api.x.ai/v1
|
|
13
|
+
GROK_API_KEY=
|
|
14
|
+
|
|
15
|
+
# Optional Tavily integration.
|
|
16
|
+
# TAVILY_ENABLED=true
|
|
17
|
+
# TAVILY_API_URL=https://api.tavily.com
|
|
18
|
+
# TAVILY_API_KEY=
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export function ensureUserConfig(home = homedir()) {
|
|
22
|
+
const directory = path.join(home, ".pi", "agent", "cadence");
|
|
23
|
+
const file = path.join(directory, ".env");
|
|
24
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
try {
|
|
26
|
+
const fd = openSync(
|
|
27
|
+
file,
|
|
28
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
29
|
+
0o600,
|
|
30
|
+
);
|
|
31
|
+
try {
|
|
32
|
+
writeSync(fd, CONFIG_TEMPLATE, undefined, "utf8");
|
|
33
|
+
} finally {
|
|
34
|
+
closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
return { created: true, path: file };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code === "EEXIST") return { created: false, path: file };
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
process.argv[1] &&
|
|
45
|
+
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
|
|
46
|
+
) {
|
|
47
|
+
try {
|
|
48
|
+
const result = ensureUserConfig();
|
|
49
|
+
if (result.created)
|
|
50
|
+
process.stderr.write(`Cadence created ${result.path}\n`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`Cadence could not create the user configuration template: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
54
|
+
);
|
|
55
|
+
process.exitCode = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
selectConfigPath,
|
|
5
|
+
} from "../skills/_shared/load-config.mjs";
|
|
6
|
+
|
|
7
|
+
export const STARTUP_CARD_KEY = "cadence-configuration";
|
|
8
|
+
|
|
9
|
+
// Only code-owned labels and paths enter the UI; never values, endpoints,
|
|
10
|
+
// models, parser errors or credentials from the configuration file.
|
|
11
|
+
function displayPath(value: string): string {
|
|
12
|
+
return JSON.stringify(value).slice(1, -1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validUrl(value: string): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(value);
|
|
18
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function startupCardLines(options: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
home: string;
|
|
27
|
+
projectTrusted: boolean;
|
|
28
|
+
}): string[] {
|
|
29
|
+
const lines = ["Cadence · 研究能力(仅本地配置检查,未联网验证)"];
|
|
30
|
+
if (!options.projectTrusted) {
|
|
31
|
+
return [
|
|
32
|
+
...lines,
|
|
33
|
+
"尚未信任当前项目:未读取研究配置;信任项目后重新加载可查看状态。",
|
|
34
|
+
"不自动启动 Abel 工作流;请勿在聊天中发送 API Key。",
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let selectedPath: string | null = null;
|
|
39
|
+
try {
|
|
40
|
+
selectedPath = selectConfigPath(options);
|
|
41
|
+
const { values } = loadConfig({ ...options, allowMissing: true });
|
|
42
|
+
const context7 = !validUrl(
|
|
43
|
+
values.CONTEXT7_API_URL || "https://context7.com/api/v2",
|
|
44
|
+
)
|
|
45
|
+
? "CONTEXT7_API_URL 格式无效"
|
|
46
|
+
: values.CONTEXT7_API_KEY
|
|
47
|
+
? "已配置密钥"
|
|
48
|
+
: "匿名模式,无需密钥";
|
|
49
|
+
const missing = ["GROK_API_URL", "GROK_API_KEY"].filter(
|
|
50
|
+
(name) => !values[name],
|
|
51
|
+
);
|
|
52
|
+
const grok = missing.length
|
|
53
|
+
? `待配置 ${missing.join("、")}`
|
|
54
|
+
: !validUrl(values.GROK_API_URL)
|
|
55
|
+
? "GROK_API_URL 格式无效"
|
|
56
|
+
: "已配置";
|
|
57
|
+
const tavily =
|
|
58
|
+
values.TAVILY_ENABLED === "false"
|
|
59
|
+
? "已关闭"
|
|
60
|
+
: !values.TAVILY_API_KEY
|
|
61
|
+
? "未配置(可选)"
|
|
62
|
+
: !validUrl(values.TAVILY_API_URL || "https://api.tavily.com")
|
|
63
|
+
? "TAVILY_API_URL 格式无效"
|
|
64
|
+
: "已配置(Grok Skill 仍需 Grok 配置)";
|
|
65
|
+
lines.push(
|
|
66
|
+
`文档查询 Context7:${context7}`,
|
|
67
|
+
`联网搜索 Grok:${grok};Tavily:${tavily}`,
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
lines.push("研究配置无法读取或解析:请检查下方文件;未回退到其他配置。");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const userPath = path.join(options.home, ".pi", "agent", "cadence", ".env");
|
|
74
|
+
const projectPath = path.join(options.cwd, ".pi", "cadence", ".env");
|
|
75
|
+
lines.push(
|
|
76
|
+
selectedPath
|
|
77
|
+
? `生效文件:${displayPath(selectedPath)}${selectedPath === projectPath ? "(项目整文件优先,不合并用户配置)" : "(用户级,跨项目复用)"}`
|
|
78
|
+
: `配置位置:${displayPath(userPath)}(用户级,跨项目复用)`,
|
|
79
|
+
"在本地编辑该文件:GROK_API_URL=<服务地址>、GROK_API_KEY=<密钥>,每项一行;可选 TAVILY_API_KEY。",
|
|
80
|
+
"研究 Skills 不读取 shell API 变量;保存后下次调用生效。不要把密钥发到聊天或提交到 Git。",
|
|
81
|
+
"可选服务未配置不阻塞普通任务,也不自动启动 Abel 工作流。",
|
|
82
|
+
);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|