@zhushanwen/pi-plan 0.5.2 → 1.0.0
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 +9 -9
- package/package.json +6 -4
- package/skills/plan-ext-config/SKILL.md +33 -18
- package/src/__tests__/active-path.test.ts +204 -0
- package/src/__tests__/command.test.ts +55 -32
- package/src/__tests__/enter.test.ts +58 -20
- package/src/__tests__/exec-options-alignment.test.ts +56 -0
- package/src/__tests__/execution-notice.test.ts +197 -0
- package/src/__tests__/{compact.test.ts → extract-plan-steps.test.ts} +24 -15
- package/src/__tests__/fsm-consumption.test.ts +388 -0
- package/src/__tests__/index.test.ts +175 -75
- package/src/__tests__/prompts.test.ts +66 -0
- package/src/__tests__/review-contract-boundary.test.ts +123 -0
- package/src/__tests__/review.test.ts +264 -97
- package/src/__tests__/skills.test.ts +14 -5
- package/src/__tests__/state.test.ts +254 -52
- package/src/__tests__/{compact-criteria-array.test.ts → success-criteria-array.test.ts} +29 -70
- package/src/__tests__/templates.test.ts +8 -25
- package/src/__tests__/tool.test.ts +269 -121
- package/src/command.ts +26 -29
- package/src/enter.ts +31 -50
- package/src/{compact.ts → execution-notice.ts} +29 -117
- package/src/index.ts +100 -88
- package/src/prompts.ts +42 -12
- package/src/state.ts +290 -87
- package/src/templates.ts +0 -15
- package/src/tool.ts +663 -255
- package/src/__tests__/compact-handler.test.ts +0 -348
- package/src/__tests__/exec-skills.test.ts +0 -397
- package/src/exec-skills.ts +0 -407
package/src/command.ts
CHANGED
|
@@ -7,9 +7,9 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
7
7
|
import { activatePlanMode, resolveSkills } from "./enter.js";
|
|
8
8
|
import type { SkillResolution } from "./enter.js";
|
|
9
9
|
import type { SkillRef } from "./prompts.js";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { exitPlanMode } from "./tool.js";
|
|
11
|
+
import type { PlanCtx, PlanState } from "./state.js";
|
|
12
|
+
import { PLAN_CONTEXT_CUSTOM_TYPE, getPlanState } from "./state.js";
|
|
13
13
|
|
|
14
14
|
/** /plan 参数解析产物:requirement = 最早 flag 标记前的自由文本;skills / templatePath 为 undefined = 未提供对应 flag */
|
|
15
15
|
export interface ParsedPlanArgs {
|
|
@@ -96,8 +96,7 @@ export function resolveTemplateFile(raw: string, projectDir: string): TemplateFi
|
|
|
96
96
|
|
|
97
97
|
export function registerPlanCommand(
|
|
98
98
|
pi: ExtensionAPI,
|
|
99
|
-
|
|
100
|
-
controllers: PlanAbortControllers,
|
|
99
|
+
planCtx: PlanCtx,
|
|
101
100
|
): void {
|
|
102
101
|
pi.registerCommand("plan", {
|
|
103
102
|
description:
|
|
@@ -118,11 +117,11 @@ export function registerPlanCommand(
|
|
|
118
117
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
119
118
|
const trimmed = args.trim();
|
|
120
119
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
121
|
-
const state = getPlanState(
|
|
120
|
+
const state = getPlanState(planCtx.states, sessionId, ctx);
|
|
122
121
|
|
|
123
122
|
// Subcommand: abort
|
|
124
123
|
if (trimmed === "abort") {
|
|
125
|
-
await handleAbort(pi,
|
|
124
|
+
await handleAbort(pi, planCtx, sessionId, ctx, state);
|
|
126
125
|
return;
|
|
127
126
|
}
|
|
128
127
|
|
|
@@ -164,35 +163,26 @@ export function registerPlanCommand(
|
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
// Enter plan mode
|
|
167
|
-
handleEnterPlanMode(pi,
|
|
166
|
+
handleEnterPlanMode(pi, planCtx, sessionId, ctx, state, args);
|
|
168
167
|
},
|
|
169
168
|
});
|
|
170
169
|
}
|
|
171
170
|
|
|
172
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Handle /plan abort subcommand——exit 链单入口(D-B1-1)的 command 路投影:
|
|
173
|
+
* 动作清单(解散挂起/转移落盘/widget/工具集恢复/反馈文案)全在 exitPlanMode 内,
|
|
174
|
+
* 本函数只把 ExitResult 投影为 notify。非活跃格全谱(idle/终态)统一 warn 纠偏
|
|
175
|
+
* 文案(原 isActive 前置守卫的 info no-op 已由单入口守卫极性取代,D-B1-1 行为变更)。
|
|
176
|
+
*/
|
|
173
177
|
async function handleAbort(
|
|
174
178
|
pi: ExtensionAPI,
|
|
175
|
-
|
|
176
|
-
controllers: PlanAbortControllers,
|
|
179
|
+
planCtx: PlanCtx,
|
|
177
180
|
sessionId: string,
|
|
178
181
|
ctx: ExtensionContext,
|
|
179
182
|
state: PlanState,
|
|
180
183
|
): Promise<void> {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
// E10 顺序不可反(因果链):controller.abort() → 挂起 select resolve undefined →
|
|
186
|
-
// tool execute 走「已取消」分支返回 → turn 正常结束 → agent_settled 到达 →
|
|
187
|
-
// busy defer 队列恢复投递。若先 reset 后 abort,挂起 select 无人 resolve 且
|
|
188
|
-
// runtime 不超时 → pi turn 永占用 → 后续消息永 defer → session 只能 forceQuit。
|
|
189
|
-
controllers.get(sessionId)?.abort();
|
|
190
|
-
controllers.delete(sessionId);
|
|
191
|
-
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
192
|
-
updatePlanWidget(ctx, updatedState);
|
|
193
|
-
// Restore full tool set (SDK does NOT support undefined)
|
|
194
|
-
pi.setActiveTools(pi.getAllTools().map((t: { name: string }) => t.name));
|
|
195
|
-
ctx.ui.notify("Plan mode aborted.", "info");
|
|
184
|
+
const result = exitPlanMode(pi, planCtx, sessionId, ctx, state, "command");
|
|
185
|
+
ctx.ui.notify(result.message, result.level);
|
|
196
186
|
}
|
|
197
187
|
|
|
198
188
|
/** Handle /plan status subcommand */
|
|
@@ -270,7 +260,7 @@ function reportTemplateFlagError(pi: ExtensionAPI, problem: string): void {
|
|
|
270
260
|
/** Handle entering plan mode */
|
|
271
261
|
function handleEnterPlanMode(
|
|
272
262
|
pi: ExtensionAPI,
|
|
273
|
-
|
|
263
|
+
planCtx: PlanCtx,
|
|
274
264
|
sessionId: string,
|
|
275
265
|
ctx: ExtensionContext,
|
|
276
266
|
state: PlanState,
|
|
@@ -312,7 +302,14 @@ function handleEnterPlanMode(
|
|
|
312
302
|
// --skills E1 校验(互斥已判过后,走到这里 templatePath 必为 undefined)
|
|
313
303
|
const requested = parsed.skills ?? [];
|
|
314
304
|
if (parsed.skills !== undefined && requested.length === 0) {
|
|
315
|
-
|
|
305
|
+
// 空 --skills 报错复用 resolveSkills 单源(C1 去重:空请求 → ok:false + 全量
|
|
306
|
+
// available + 空 missing,报错文案不变)。TS 无法静态收窄「空请求恒 ok:false」,
|
|
307
|
+
// 运行时 fail-fast 承载该不变量(tool.ts switch default 不可达断言同风格)
|
|
308
|
+
const failed = resolveSkills(pi, requested);
|
|
309
|
+
if (failed.ok) {
|
|
310
|
+
throw new Error("plan: empty --skills must fail skill resolution (invariant)");
|
|
311
|
+
}
|
|
312
|
+
reportUnknownSkills(pi, failed);
|
|
316
313
|
return;
|
|
317
314
|
}
|
|
318
315
|
let resolved: SkillRef[] = [];
|
|
@@ -329,7 +326,7 @@ function handleEnterPlanMode(
|
|
|
329
326
|
// 进入核心收敛到 enter.ts(plan(enter) tool 与 slash 命令共用);本入口只负责
|
|
330
327
|
// flag 解析/校验(上方)与提示词投递(下方 sendMessage custom message 注入)。
|
|
331
328
|
// state 由 activatePlanMode 就地改 + persist(getPlanState 缓存同一对象)。
|
|
332
|
-
const { prompt } = activatePlanMode(pi,
|
|
329
|
+
const { prompt } = activatePlanMode(pi, planCtx.states, sessionId, ctx, {
|
|
333
330
|
requirement,
|
|
334
331
|
skills: resolved,
|
|
335
332
|
projectDir: ctx.cwd,
|
package/src/enter.ts
CHANGED
|
@@ -3,53 +3,21 @@ import * as path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
|
+
// 技能执行门禁(resolveSkills + SkillResolution)单源在 ADR-0074 共享包
|
|
7
|
+
// @zhushanwen/pi-exec-skills(plan-mode-audit-remediation 批次 4②b 迁入,含空请求
|
|
8
|
+
// fail-fast 契约);经此处 re-export 维持既有消费路径(tool.ts / command.ts / 测试
|
|
9
|
+
// import "./enter.js")不动
|
|
10
|
+
export { resolveSkills } from "@zhushanwen/pi-exec-skills";
|
|
11
|
+
export type { SkillResolution } from "@zhushanwen/pi-exec-skills";
|
|
12
|
+
|
|
6
13
|
import { buildPlanModePrompt } from "./prompts.js";
|
|
7
14
|
import type { SkillRef } from "./prompts.js";
|
|
8
15
|
import type { PlanSessionMap, PlanState } from "./state.js";
|
|
9
|
-
import { capPlanRequirement, getPlanState, persistPlanState, PLAN_MODE_TOOLS } from "./state.js";
|
|
16
|
+
import { applyPlanEvent, capPlanRequirement, clearRoundFields, getPlanState, persistPlanState, PLAN_MODE_TOOLS } from "./state.js";
|
|
10
17
|
import { updatePlanWidget } from "./widget.js";
|
|
11
18
|
|
|
12
19
|
export const MAX_SLUG_LENGTH = 30;
|
|
13
20
|
|
|
14
|
-
/** E1 技能解析结果:ok=false 时携带缺失项与可用清单(fail-fast 回复的材料) */
|
|
15
|
-
export type SkillResolution =
|
|
16
|
-
| { ok: true; resolved: SkillRef[] }
|
|
17
|
-
| { ok: false; available: string[]; missing: string[] };
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 技能名归一:剥掉 pi 命令命名空间前导 `skill:` 前缀,得到自然技能名。
|
|
21
|
-
* pi.getCommands() 枚举 skill 类命令时 name 带 `skill:` 前缀(如 `skill:tech-design`),
|
|
22
|
-
* 这是 pi 的实现细节,不得泄漏到输入面——slash `--skills` 与 plan(enter) 的 skills 参数
|
|
23
|
-
* 都用自然技能名。枚举侧与输入侧双向剥前缀后比对,兼容两种形态。
|
|
24
|
-
*/
|
|
25
|
-
export function normalizeSkillName(name: string): string {
|
|
26
|
-
return name.startsWith("skill:") ? name.slice("skill:".length) : name;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* E1 校验:pi.getCommands() 过滤 source === "skill" 枚举比对(技能枚举与路径
|
|
31
|
-
* 经 pi 取得,不自扫描目录——D2)。比对前双向剥 `skill:` 前缀归一:resolved/missing
|
|
32
|
-
* 用归一后的短名;available 维持枚举原形态(错误信息里可直接复制为 pi 命令)。
|
|
33
|
-
* slash 命令与 plan(enter) tool 两入口共用(自 command.ts 迁入 enter.ts)。
|
|
34
|
-
*/
|
|
35
|
-
export function resolveSkills(pi: ExtensionAPI, requested: string[]): SkillResolution {
|
|
36
|
-
const skillCommands = pi.getCommands().filter((c) => c.source === "skill");
|
|
37
|
-
const byShortName = new Map(skillCommands.map((c) => [normalizeSkillName(c.name), c.sourceInfo.path]));
|
|
38
|
-
const available = skillCommands.map((c) => c.name);
|
|
39
|
-
const resolved: SkillRef[] = [];
|
|
40
|
-
const missing: string[] = [];
|
|
41
|
-
for (const raw of requested) {
|
|
42
|
-
const name = normalizeSkillName(raw);
|
|
43
|
-
const skillPath = byShortName.get(name);
|
|
44
|
-
if (skillPath === undefined) {
|
|
45
|
-
missing.push(name);
|
|
46
|
-
} else {
|
|
47
|
-
resolved.push({ name, skillPath });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return missing.length > 0 ? { ok: false, available, missing } : { ok: true, resolved };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
21
|
/** 进入 plan 模式的归一入参(slash 命令与 plan(enter) tool 两入口共用) */
|
|
54
22
|
export interface ActivatePlanModeInput {
|
|
55
23
|
requirement: string;
|
|
@@ -80,7 +48,8 @@ export interface ActivatePlanModeOutcome {
|
|
|
80
48
|
* 副作用顺序钉死:状态字段 → persistPlanState(entry 落盘)→ updatePlanWidget →
|
|
81
49
|
* setActiveTools → 构造 prompt。persist 先于工具收拢,保证「已持久 isActive 但工具未收」
|
|
82
50
|
* 的半进入态不可达(崩溃窗口内重开 session 经 entry 恢复 isActive,session_start hook
|
|
83
|
-
* 会补 setActiveTools
|
|
51
|
+
* 会补 setActiveTools)。生命周期写唯一通道 = applyPlanEvent('enter')(D1:idle|终态
|
|
52
|
+
* --enter--> planning 新一轮)。
|
|
84
53
|
*/
|
|
85
54
|
export function activatePlanMode(
|
|
86
55
|
pi: ExtensionAPI,
|
|
@@ -104,17 +73,29 @@ export function activatePlanMode(
|
|
|
104
73
|
state.planFilePath = planFilePath;
|
|
105
74
|
// state/entry/plan 帧侧 requirement 64KB 封顶(帧有界前提);prompt 仍用未封顶全文直达模型
|
|
106
75
|
state.requirement = capPlanRequirement(requirement);
|
|
107
|
-
// --template 直传:templateName = 去扩展名 basename(GUI / /plan status
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
76
|
+
// --template 直传:templateName = 去扩展名 basename(GUI / /plan status 展示,值域不变);
|
|
77
|
+
// 直传事实落独立布尔字段 templateProvided(select-template 防御的判定信号,D7——
|
|
78
|
+
// 双字段合并后的形态,双向版本错配登记见 PlanState.templateProvided 注释)
|
|
79
|
+
if (input.template) {
|
|
80
|
+
state.templateName = path.basename(input.template.absPath, ".md");
|
|
81
|
+
state.templateProvided = true;
|
|
82
|
+
} else {
|
|
83
|
+
state.templateName = "";
|
|
84
|
+
delete state.templateProvided;
|
|
85
|
+
}
|
|
111
86
|
state.skills = skills.map((s) => s.name);
|
|
112
87
|
state.docs = [];
|
|
113
|
-
// 新轮次重置:
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
88
|
+
// 新轮次重置:per-round 字段随进入失效(D4 单函数出口,与 resetPlanState 同源——
|
|
89
|
+
// 跨轮残留会误触新鲜度门 / 渲染上一轮降级文案(C-U2 同型残留))
|
|
90
|
+
clearRoundFields(state);
|
|
91
|
+
|
|
92
|
+
// 状态写走 transition()(D1 'enter' 边:idle|终态
|
|
93
|
+
// --enter--> planning 新一轮)。不一致格 isActive=false 且 state 落活跃族
|
|
94
|
+
//(仅坏数据/旧映射残留可达,P-6 单写入方下正常不可达)按 ok:false 落穿:不归一、不重试,
|
|
95
|
+
// 后续副作用照常(persist 携带现值)——原「归一 idle 再进」自愈分支已随
|
|
96
|
+
// plan-mode-audit-remediation 批次 3 删除(行为变更仅坏数据格可达,V5 对账归垃圾格口径);
|
|
97
|
+
// 正常路径恒单通道 applyPlanEvent。
|
|
98
|
+
applyPlanEvent(state, "enter");
|
|
118
99
|
|
|
119
100
|
persistPlanState(pi, state);
|
|
120
101
|
updatePlanWidget(ctx, state);
|
|
@@ -1,65 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan complete 后的执行通知投递 + goal 桥。
|
|
3
|
+
*
|
|
4
|
+
* execute 档经 goal 桥 slot 注册执行跟踪(失败显式化五出口,不阻断执行),
|
|
5
|
+
* steer 指令承载执行方式裁决(goal 工作流 / skill / 直接执行三形态)。
|
|
6
|
+
* 压缩/分叉时的 plan 摘要由 pi 内置压缩机制承载(确定性文件操作清单 +
|
|
7
|
+
* keepRecentTokens 近端原文保留),本扩展不再接管压缩摘要。
|
|
8
|
+
*/
|
|
1
9
|
import * as fs from "node:fs";
|
|
2
|
-
import { basename } from "node:path";
|
|
10
|
+
import { basename, dirname } from "node:path";
|
|
3
11
|
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext
|
|
5
|
-
import {
|
|
12
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
6
14
|
import type { GoalInitFn } from "@zhushanwen/pi-goal";
|
|
7
15
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
8
16
|
|
|
9
17
|
import { t } from "./i18n.js";
|
|
10
|
-
import type {
|
|
11
|
-
import { PLAN_CONTEXT_CUSTOM_TYPE
|
|
18
|
+
import type { PlanState } from "./state.js";
|
|
19
|
+
import { PLAN_CONTEXT_CUSTOM_TYPE } from "./state.js";
|
|
12
20
|
|
|
13
21
|
const logger = getLogger("pi-plan");
|
|
14
22
|
|
|
15
|
-
export function registerPlanEventHandlers(
|
|
16
|
-
pi: ExtensionAPI,
|
|
17
|
-
sessions: PlanSessionMap,
|
|
18
|
-
): void {
|
|
19
|
-
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx: ExtensionContext) => {
|
|
20
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
21
|
-
const state = getPlanState(sessions, sessionId, ctx);
|
|
22
|
-
if (!state.isActive) return {};
|
|
23
|
-
|
|
24
|
-
const prep = event.preparation;
|
|
25
|
-
|
|
26
|
-
// Read plan file content for recovery after compact
|
|
27
|
-
const planContent = readPlanFileSafe(state.planFilePath);
|
|
28
|
-
|
|
29
|
-
// handler 已有 isActive 门——能走到这里的 plan 必然进行中(D6:phase 删除,原 phase="complete" 分支为死状态)
|
|
30
|
-
const progressNote = "\nPlan was in progress — review and continue.";
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
compaction: {
|
|
34
|
-
summary:
|
|
35
|
-
`Plan mode active. Plan file: ${state.planFilePath}\n\n` +
|
|
36
|
-
`## Plan Content\n${planContent}\n\n` +
|
|
37
|
-
`Requirement: ${state.requirement}` +
|
|
38
|
-
progressNote,
|
|
39
|
-
firstKeptEntryId: prep?.firstKeptEntryId,
|
|
40
|
-
tokensBefore: prep?.tokensBefore,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
pi.on("session_before_tree", async (_event: SessionBeforeTreeEvent, ctx: ExtensionContext) => {
|
|
46
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
47
|
-
const state = getPlanState(sessions, sessionId, ctx);
|
|
48
|
-
if (!state.isActive) return {};
|
|
49
|
-
|
|
50
|
-
const planContent = readPlanFileSafe(state.planFilePath);
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
summary: {
|
|
54
|
-
summary:
|
|
55
|
-
`Plan mode active. Plan file: ${state.planFilePath}\n\n` +
|
|
56
|
-
`## Plan Content\n${planContent}\n\n` +
|
|
57
|
-
`Read the plan file and execute the implementation.`,
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
23
|
/** Read plan file: ok=false 是显式信号(GoalBridgeOutcome 的 plan-unreadable 出口消费),消除哨兵字符串比较 */
|
|
64
24
|
type PlanFileContent = { ok: true; content: string } | { ok: false };
|
|
65
25
|
|
|
@@ -71,12 +31,6 @@ function readPlanFile(planFilePath: string): PlanFileContent {
|
|
|
71
31
|
}
|
|
72
32
|
}
|
|
73
33
|
|
|
74
|
-
/** Read plan file, return content or human-readable marker (for prompt embedding) */
|
|
75
|
-
function readPlanFileSafe(planFilePath: string): string {
|
|
76
|
-
const result = readPlanFile(planFilePath);
|
|
77
|
-
return result.ok ? result.content : "(plan file could not be read)";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
34
|
/**
|
|
81
35
|
* goalInit slot key——goal 扩展的跨扩展编程式入口(goal-bridge-cross-extension.md)。
|
|
82
36
|
* ⚠️ 必须与 `extensions/universal/goal/src/index.ts` 的 GOAL_INIT_SLOT_KEY 字符串完全一致:
|
|
@@ -87,7 +41,6 @@ const GOAL_INIT_SLOT_KEY = Symbol.for("@zhushanwen/pi-goal.goalInit");
|
|
|
87
41
|
|
|
88
42
|
/**
|
|
89
43
|
* goal 桥的单一断言点:goal 扩展挂在 globalThis slot 上的编程式接口(发现 7——
|
|
90
|
-
* 此前 detectGoalCapability / tryGoalInit 两处 inline 断言收敛于此;
|
|
91
44
|
* 桥通道从 pi API 对象挂载迁到 slot:pi 0.84.4 per-extension API 隔离使
|
|
92
45
|
* pi.__goalInit 形态跨扩展恒不可见,slot 是 C-ext-06 惯例的进程级共享形态)。
|
|
93
46
|
*/
|
|
@@ -96,18 +49,16 @@ function getGoalInit(): GoalInitFn | undefined {
|
|
|
96
49
|
return typeof fn === "function" ? (fn as GoalInitFn) : undefined;
|
|
97
50
|
}
|
|
98
51
|
|
|
99
|
-
/** Detect whether goal extension is available via its programming interface */
|
|
100
|
-
export function detectGoalCapability(): boolean {
|
|
101
|
-
return getGoalInit() !== undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
52
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
53
|
+
* goal 桥侧 slug(仅 widget 标题 + history 展示用,不注入 prompt)。生产输入恒为
|
|
54
|
+
* <project>/.tmp/plans/<slug>/plan.md(enter.ts 构造)——展示名取目录名(requirement
|
|
55
|
+
* slug 段,2026-09-27 用户裁决;已归档缺陷档案(plan-cjk-slug-data-loss) 缺陷 3 同源修复)。
|
|
56
|
+
* kebab 链对 enter slug 幂等,只防 planFilePath 非生产形态的通用输入;提取为空回
|
|
57
|
+
* "plan-execution"。中文需求且缺陷 2(enter 期目录唯一性)未修的窗口内,plan.md 直落
|
|
58
|
+
* .tmp/plans 使本函数取到骨架名 "plans"——该形态随缺陷 2 修复消除。
|
|
107
59
|
*/
|
|
108
60
|
function buildPlanSlug(planFilePath: string): string {
|
|
109
|
-
const stem = basename(planFilePath)
|
|
110
|
-
.replace(/\.md$/i, "")
|
|
61
|
+
const stem = basename(dirname(planFilePath))
|
|
111
62
|
.toLowerCase()
|
|
112
63
|
.replace(/[^a-z0-9]+/g, "-")
|
|
113
64
|
.replace(/^-+|-+$/g, "");
|
|
@@ -137,12 +88,12 @@ function truncatePreview(text: string): string {
|
|
|
137
88
|
* 从 plan 步骤构造可检查的 successCriteria(plan 完成 = 所有步骤执行并验证)。
|
|
138
89
|
* goal 的 complete 判定会对照本字段逐条做证据审计。
|
|
139
90
|
*
|
|
140
|
-
* 形态固定:1 条总述 `All N steps of <
|
|
91
|
+
* 形态固定:1 条总述 `All N steps of <plan slug> executed and verified`
|
|
141
92
|
* + 前 PREVIEW_COUNT 条 step preview(编号前缀、单条截断 ≤PREVIEW_MAX_CHARS),
|
|
142
93
|
* 合计 ≤4 条(goal schema maxItems:8),每条单行不含 \r\n。
|
|
143
94
|
*/
|
|
144
95
|
export function buildPlanSuccessCriteria(planFilePath: string, tasks: string[]): string[] {
|
|
145
|
-
const planName = toSingleLine(
|
|
96
|
+
const planName = toSingleLine(buildPlanSlug(planFilePath));
|
|
146
97
|
const items = [`All ${tasks.length} steps of ${planName} executed and verified`];
|
|
147
98
|
const previews = tasks
|
|
148
99
|
.slice(0, PREVIEW_COUNT)
|
|
@@ -260,7 +211,7 @@ function deliverExecutionNotice(
|
|
|
260
211
|
skillEntryPath?: string,
|
|
261
212
|
): GoalBridgeOutcome | undefined {
|
|
262
213
|
// execute 档整合 goal 桥:tryGoalInit 内部含 goal-unavailable gate(goal 未挂载走
|
|
263
|
-
// started:false
|
|
214
|
+
// started:false 降级),无需前置探测
|
|
264
215
|
const outcome = execMode === "execute" ? tryGoalInit(planFilePath, ctx) : undefined;
|
|
265
216
|
|
|
266
217
|
let modeHint: string;
|
|
@@ -297,58 +248,19 @@ function deliverExecutionNotice(
|
|
|
297
248
|
}
|
|
298
249
|
|
|
299
250
|
/**
|
|
300
|
-
* complete
|
|
251
|
+
* complete 后的执行通知投递(plan-mode-audit-remediation D-B1-6:执行前压缩档
|
|
252
|
+
* 砍除——原 compact | direct 两档分发退化为单一直接投递路径)。
|
|
301
253
|
*
|
|
302
|
-
* 返回值:execMode=execute
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* entry 须在压缩后的世界里创建,提前到 compact 前有被压缩边界丢弃的风险,时序
|
|
306
|
-
* 不动——设计 §6.2 D2),该档 result 已返回,失败报告走 steer + notify 通道。
|
|
254
|
+
* 返回值:execMode=execute 时同步返回 goalInit 的 outcome(executeComplete 写进
|
|
255
|
+
* result content 与 details);skill 档无 goalInit,返回 undefined——该档 result
|
|
256
|
+
* 已返回,失败报告走 steer + notify 通道。
|
|
307
257
|
*/
|
|
308
258
|
export function handlePlanComplete(
|
|
309
259
|
pi: ExtensionAPI,
|
|
310
260
|
ctx: ExtensionContext,
|
|
311
261
|
state: PlanState,
|
|
312
|
-
isolation: string,
|
|
313
262
|
execMode: string,
|
|
314
263
|
skillEntryPath?: string,
|
|
315
264
|
): GoalBridgeOutcome | undefined {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
switch (isolation) {
|
|
319
|
-
case "compact": {
|
|
320
|
-
ctx.compact({
|
|
321
|
-
customInstructions: `Plan file: ${planFilePath}. Read plan and execute implementation.`,
|
|
322
|
-
// E1 同构崩溃点(crash-resilience D1):onComplete/onError 由 compact 内部
|
|
323
|
-
// Promise 链异步调用、不在 pi runner emit() 的 try/catch 内——压缩进行中用户
|
|
324
|
-
// 切换/重载 session 后,回调触碰捕获的 pi/ctx 命中 stale 同步抛错即杀 pi 进程。
|
|
325
|
-
// 守卫 stale 静默降级(执行消息不投递,用户可手动 Read plan 文件执行),非
|
|
326
|
-
// stale 错误原样上抛。plan 未注入代际计数器(低频路径),分诊依赖 PS-30 门禁
|
|
327
|
-
// 守卫的 stale 文案兜底(D1 降级语义声明的合法形态)。
|
|
328
|
-
onComplete: () => {
|
|
329
|
-
guardStaleCtx(() => {
|
|
330
|
-
deliverExecutionNotice(pi, ctx, planFilePath, execMode, skillEntryPath);
|
|
331
|
-
}, {
|
|
332
|
-
label: "plan:compact-onComplete",
|
|
333
|
-
onStale: (error) => logger.warn("plan execution notice delivery skipped (stale ctx)", { error: toErrorMessage(error) }),
|
|
334
|
-
});
|
|
335
|
-
},
|
|
336
|
-
onError: (_error: Error) => {
|
|
337
|
-
guardStaleCtx(() => {
|
|
338
|
-
ctx.ui.notify("Compact failed, continuing without isolation.", "warning");
|
|
339
|
-
deliverExecutionNotice(pi, ctx, planFilePath, execMode, skillEntryPath);
|
|
340
|
-
}, {
|
|
341
|
-
label: "plan:compact-onError",
|
|
342
|
-
onStale: (error) => logger.warn("plan execution notice delivery skipped (stale ctx)", { error: toErrorMessage(error) }),
|
|
343
|
-
});
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
case "direct":
|
|
350
|
-
default: {
|
|
351
|
-
return deliverExecutionNotice(pi, ctx, planFilePath, execMode, skillEntryPath);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
265
|
+
return deliverExecutionNotice(pi, ctx, state.planFilePath, execMode, skillEntryPath);
|
|
354
266
|
}
|