@istuen/pt 0.1.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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/agent/api-bridge.d.ts +8 -0
- package/dist/agent/api-bridge.js +83 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +3 -0
- package/dist/agent/pi-adapter.d.ts +35 -0
- package/dist/agent/pi-adapter.js +251 -0
- package/dist/agent/registry.d.ts +4 -0
- package/dist/agent/registry.js +40 -0
- package/dist/asset-health.d.ts +50 -0
- package/dist/asset-health.js +267 -0
- package/dist/asset-pack/loader.d.ts +47 -0
- package/dist/asset-pack/loader.js +121 -0
- package/dist/asset-pack/manifest.d.ts +23 -0
- package/dist/asset-pack/manifest.js +96 -0
- package/dist/asset-pack/md-file-pack.d.ts +41 -0
- package/dist/asset-pack/md-file-pack.js +153 -0
- package/dist/asset-pack/validate.d.ts +55 -0
- package/dist/asset-pack/validate.js +131 -0
- package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/dist/builtin/assets/domains/agent-info.md +15 -0
- package/dist/builtin/assets/domains/authoring.md +201 -0
- package/dist/builtin/assets/domains/pack-repair.md +47 -0
- package/dist/builtin/assets/domains/project-analysis.md +52 -0
- package/dist/builtin/assets/domains/usage.md +81 -0
- package/dist/builtin/assets/domains/user-info.md +15 -0
- package/dist/builtin/assets/profiles/guide.profile.md +28 -0
- package/dist/commands.d.ts +69 -0
- package/dist/commands.js +344 -0
- package/dist/compile/agent-context.d.ts +48 -0
- package/dist/compile/agent-context.js +423 -0
- package/dist/compile/index.d.ts +1 -0
- package/dist/compile/index.js +5 -0
- package/dist/compile/resolve-use.d.ts +36 -0
- package/dist/compile/resolve-use.js +171 -0
- package/dist/compile/type-guards.d.ts +29 -0
- package/dist/compile/type-guards.js +107 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +133 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.js +81 -0
- package/dist/diagnostics.d.ts +14 -0
- package/dist/diagnostics.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +838 -0
- package/dist/injection-status.d.ts +45 -0
- package/dist/injection-status.js +134 -0
- package/dist/log.d.ts +64 -0
- package/dist/log.js +139 -0
- package/dist/manual-session.d.ts +24 -0
- package/dist/manual-session.js +152 -0
- package/dist/manual-track.d.ts +35 -0
- package/dist/manual-track.js +107 -0
- package/dist/parse/blueprint.d.ts +5 -0
- package/dist/parse/blueprint.js +59 -0
- package/dist/parse/domain-renderers.d.ts +8 -0
- package/dist/parse/domain-renderers.js +115 -0
- package/dist/parse/domain.d.ts +7 -0
- package/dist/parse/domain.js +69 -0
- package/dist/parse/index.d.ts +9 -0
- package/dist/parse/index.js +223 -0
- package/dist/parse/profile.d.ts +18 -0
- package/dist/parse/profile.js +117 -0
- package/dist/parse/ref-resolver.d.ts +63 -0
- package/dist/parse/ref-resolver.js +174 -0
- package/dist/parse/shared.d.ts +76 -0
- package/dist/parse/shared.js +273 -0
- package/dist/profile-persist.d.ts +22 -0
- package/dist/profile-persist.js +53 -0
- package/dist/render/cache.d.ts +15 -0
- package/dist/render/cache.js +135 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.js +14 -0
- package/dist/render/session-inject.d.ts +5 -0
- package/dist/render/session-inject.js +24 -0
- package/dist/render/turn-inject.d.ts +36 -0
- package/dist/render/turn-inject.js +248 -0
- package/dist/schema.d.ts +448 -0
- package/dist/schema.js +45 -0
- package/dist/session.d.ts +82 -0
- package/dist/session.js +102 -0
- package/dist/slog.d.ts +3 -0
- package/dist/slog.js +27 -0
- package/dist/transpile.d.ts +34 -0
- package/dist/transpile.js +220 -0
- package/dist/verify/file-hash.d.ts +2 -0
- package/dist/verify/file-hash.js +30 -0
- package/dist/verify/fs-content-match.d.ts +2 -0
- package/dist/verify/fs-content-match.js +26 -0
- package/dist/verify/fs-exists.d.ts +2 -0
- package/dist/verify/fs-exists.js +13 -0
- package/dist/verify/fs-not-exists.d.ts +2 -0
- package/dist/verify/fs-not-exists.js +13 -0
- package/dist/verify/git-status-clean.d.ts +2 -0
- package/dist/verify/git-status-clean.js +19 -0
- package/dist/verify/index.d.ts +7 -0
- package/dist/verify/index.js +46 -0
- package/dist/verify/lint-check.d.ts +2 -0
- package/dist/verify/lint-check.js +17 -0
- package/dist/verify/read-stderr.d.ts +5 -0
- package/dist/verify/read-stderr.js +20 -0
- package/dist/verify/ref-check.d.ts +13 -0
- package/dist/verify/ref-check.js +88 -0
- package/dist/verify/test-pass.d.ts +2 -0
- package/dist/verify/test-pass.js +17 -0
- package/dist/verify/ts-compiles.d.ts +2 -0
- package/dist/verify/ts-compiles.js +17 -0
- package/package.json +62 -0
- package/src/agent/api-bridge.ts +105 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/pi-adapter.ts +326 -0
- package/src/agent/registry.ts +44 -0
- package/src/asset-health.ts +327 -0
- package/src/asset-pack/loader.ts +141 -0
- package/src/asset-pack/manifest.ts +118 -0
- package/src/asset-pack/md-file-pack.ts +202 -0
- package/src/asset-pack/validate.ts +186 -0
- package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/src/builtin/assets/domains/agent-info.md +15 -0
- package/src/builtin/assets/domains/authoring.md +201 -0
- package/src/builtin/assets/domains/pack-repair.md +47 -0
- package/src/builtin/assets/domains/project-analysis.md +52 -0
- package/src/builtin/assets/domains/usage.md +81 -0
- package/src/builtin/assets/domains/user-info.md +15 -0
- package/src/builtin/assets/profiles/guide.profile.md +28 -0
- package/src/commands.ts +405 -0
- package/src/compile/agent-context.ts +487 -0
- package/src/compile/index.ts +5 -0
- package/src/compile/resolve-use.ts +208 -0
- package/src/compile/type-guards.ts +132 -0
- package/src/config.ts +154 -0
- package/src/constants.ts +104 -0
- package/src/diagnostics.ts +36 -0
- package/src/index.ts +1028 -0
- package/src/injection-status.ts +155 -0
- package/src/log.ts +173 -0
- package/src/manual-session.ts +190 -0
- package/src/manual-track.ts +127 -0
- package/src/parse/blueprint.ts +82 -0
- package/src/parse/domain-renderers.ts +120 -0
- package/src/parse/domain.ts +78 -0
- package/src/parse/index.ts +266 -0
- package/src/parse/profile.ts +139 -0
- package/src/parse/ref-resolver.ts +198 -0
- package/src/parse/shared.ts +331 -0
- package/src/profile-persist.ts +60 -0
- package/src/render/cache.ts +150 -0
- package/src/render/index.ts +14 -0
- package/src/render/session-inject.ts +26 -0
- package/src/render/turn-inject.ts +278 -0
- package/src/schema.ts +542 -0
- package/src/session.ts +190 -0
- package/src/slog.ts +32 -0
- package/src/transpile.ts +302 -0
- package/src/verify/file-hash.ts +29 -0
- package/src/verify/fs-content-match.ts +28 -0
- package/src/verify/fs-exists.ts +14 -0
- package/src/verify/fs-not-exists.ts +17 -0
- package/src/verify/git-status-clean.ts +23 -0
- package/src/verify/index.ts +57 -0
- package/src/verify/lint-check.ts +21 -0
- package/src/verify/read-stderr.ts +24 -0
- package/src/verify/ref-check.ts +118 -0
- package/src/verify/test-pass.ts +18 -0
- package/src/verify/ts-compiles.ts +21 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
// src/index.ts — Pi 扩展入口(v9)
|
|
2
|
+
//
|
|
3
|
+
// v9 用户面命令:--pt-profile(flag)/ /pt-profile(命令),对应"激活 Profile → 编译 AgentContext"。
|
|
4
|
+
// "profile" 在 v9 是配置层概念(引用 Blueprint + 选 Domains),用户面命令强调产物是 AgentContext。
|
|
5
|
+
//
|
|
6
|
+
// 注入用 AgentAdapter(默认 Pi)封装 before_agent_start + input 事件。
|
|
7
|
+
//
|
|
8
|
+
// Tech Debt T11: per-session 状态收拢到 SessionState(src/session.ts),不再 10 个模块级 let。
|
|
9
|
+
//
|
|
10
|
+
// v10.x(issue pt-context-persist-lost 修复):
|
|
11
|
+
// - session_start fallback 链加第四源:session JSONL(`pi.appendEntry()` 持久化)。
|
|
12
|
+
// 优先级:flag > settings > session > auto。session 优先于 auto,保留用户上次选择。
|
|
13
|
+
// - switchProfile / session_start 加载成功后调 `pi.appendEntry()` 把 activeProfile 写回 session。
|
|
14
|
+
// session entry 与 Pi Session 生命周期对齐(resume/--session/--fork 继承;/new/ephemeral 不继承)。
|
|
15
|
+
//
|
|
16
|
+
// v12.x(issue pt-session-singleton-pi-web-pollution 修复):
|
|
17
|
+
// - 移除 module-level `session` 单例访问,state 容器改为 Map<sessionId, SessionState>
|
|
18
|
+
// - 所有 handler 入口从 `ctx.sessionManager.getSessionId()` 拿 sessionId,传给 per-session 函数
|
|
19
|
+
// - `getAgentAdapter(pi, ...)` 返回 per-pi adapter(每个 session 一个 PiAdapter 实例)
|
|
20
|
+
// - tool handler 通过 `ctx.sessionManager.getSessionId()` 取 per-session state
|
|
21
|
+
// - `session_shutdown` 调 `clearSessionById(sessionId)` 精确清本 session
|
|
22
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Type } from "typebox";
|
|
24
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { FULL_DIR, MANUAL_DIR, PROFILES_DIR, RAW_DIR } from "./constants.js";
|
|
29
|
+
import { toAgentAPI } from "./agent/api-bridge.js";
|
|
30
|
+
import { getAgentAdapter } from "./agent/index.js";
|
|
31
|
+
import { scanProjectHealth } from "./asset-health.js";
|
|
32
|
+
import { applyProjectPackDegrade, getGlobalPackDir, loadBuiltinPack, loadGlobalPack, loadProjectPack, loadSettingsPacks, } from "./asset-pack/loader.js";
|
|
33
|
+
import { shouldPromptGlobalPackGuide, validatePack } from "./asset-pack/validate.js";
|
|
34
|
+
import { detectDefaultProfile, detectSingleProfile, formatProfileLabels, listProfiles, listProfilesWithTagline, readProjectSetting, } from "./config.js";
|
|
35
|
+
import { errMsg } from "./diagnostics.js";
|
|
36
|
+
import { readProfileFromSession, persistProfileToSession } from "./profile-persist.js";
|
|
37
|
+
import { LOG_DIR, PtLogger } from "./log.js";
|
|
38
|
+
import { clearSessionById, getSessionById, resetSessionState, } from "./session.js";
|
|
39
|
+
import { buildFullPrompt, buildManualDoc, checkText, flowsText, packsText, statusText, } from "./commands.js";
|
|
40
|
+
import { loadAndTranspile } from "./transpile.js";
|
|
41
|
+
import { persistManualToSession, refreshInjectionFooter, refreshManualWidget, tryRestoreManual, } from "./manual-session.js";
|
|
42
|
+
import { slog } from "./slog.js";
|
|
43
|
+
/** 从 ExtensionContext 拿 sessionId(tool / command handler ctx 形态)。 */
|
|
44
|
+
function getSessionIdFromCtx(ctx) {
|
|
45
|
+
return ctx.sessionManager?.getSessionId?.() ?? "";
|
|
46
|
+
}
|
|
47
|
+
/** 当前编译产物就绪时注册 Adapter 注入;session_start 与手动切换共用。
|
|
48
|
+
* v12.x:传 pi + sessionId,按 sessionId 拿 per-session state,按 pi 拿 per-pi adapter。 */
|
|
49
|
+
function registerInjectionIfReady(pi, ctx, sessionId) {
|
|
50
|
+
if (!sessionId)
|
|
51
|
+
return false;
|
|
52
|
+
const sessionState = getSessionById(sessionId);
|
|
53
|
+
const adapter = sessionState.activeAdapter;
|
|
54
|
+
const context = sessionState.cachedAgentContext;
|
|
55
|
+
const blueprint = sessionState.cachedBlueprint;
|
|
56
|
+
if (!adapter || !context || !blueprint)
|
|
57
|
+
return false;
|
|
58
|
+
adapter.registerInject(toAgentAPI(pi, ctx), context, blueprint, sessionState.cachedDomains, sessionState.cachedProfile);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** 转译当前选定的 Profile,结果写入 per-session state;失败降级。
|
|
62
|
+
* v12.x:pi + sessionId 参数,按 sessionId 写 per-session state,按 pi 拿 per-pi adapter。 */
|
|
63
|
+
async function transpileActive(pi, cwd, profileName, sessionId, notify) {
|
|
64
|
+
const t0 = Date.now();
|
|
65
|
+
// v15.x PR1(§6.7.3):project pack 降级时强制回 guide——必须前置覆盖,
|
|
66
|
+
// 否则用户请求的 profile 会先加载失败才降级(前置覆盖,不是失败后兜底)。
|
|
67
|
+
const sForDegrade = getSessionById(sessionId);
|
|
68
|
+
const originalProfile = profileName;
|
|
69
|
+
profileName = applyProjectPackDegrade(sForDegrade.projectPackDegraded, profileName);
|
|
70
|
+
if (profileName !== originalProfile) {
|
|
71
|
+
slog(sessionId, "warn", "transpileActive:project-pack-degraded", {
|
|
72
|
+
requested: originalProfile,
|
|
73
|
+
forced: profileName,
|
|
74
|
+
});
|
|
75
|
+
notify(`Pt: project pack 降级中,强制使用 builtin guide(请求的 "${originalProfile}" 被覆盖)。修复后重启。`, "warning");
|
|
76
|
+
}
|
|
77
|
+
slog(sessionId, "info", "transpileActive:start", { profileName });
|
|
78
|
+
try {
|
|
79
|
+
const result = await loadAndTranspile(cwd, profileName, {
|
|
80
|
+
notify,
|
|
81
|
+
log: getSessionById(sessionId).logger?.toWriter(),
|
|
82
|
+
});
|
|
83
|
+
const s = getSessionById(sessionId);
|
|
84
|
+
s.cachedSegment = result.segment;
|
|
85
|
+
s.cachedBundles = result.bundles;
|
|
86
|
+
s.cachedAgentContext = result.agentContext;
|
|
87
|
+
s.cachedBlueprint = result.blueprint;
|
|
88
|
+
s.cachedDomains = result.domains;
|
|
89
|
+
s.cachedProfile = result.profile;
|
|
90
|
+
// v15.x PR6(fix pt-active-profile-fallback-mismatch):若 loadAndTranspile fallback 了
|
|
91
|
+
// (user 请求的 profile 找不到),同步改写 s.activeProfile + notify。
|
|
92
|
+
// 这样 s.activeProfile / loadedFrom / 注入路径 三者与产物保持一致。
|
|
93
|
+
if (result.activeProfileOrigin === "fallback" &&
|
|
94
|
+
result.originalProfileName &&
|
|
95
|
+
result.profile.name !== profileName) {
|
|
96
|
+
slog(sessionId, "warn", "transpileActive:profile-fallback", {
|
|
97
|
+
requested: result.originalProfileName,
|
|
98
|
+
forced: result.profile.name,
|
|
99
|
+
});
|
|
100
|
+
notify(`Pt: profile "${result.originalProfileName}" not found, fallback to "${result.profile.name}". 运行 /pt-profile <correct> 修复。`, "warning");
|
|
101
|
+
profileName = result.profile.name;
|
|
102
|
+
}
|
|
103
|
+
s.activeProfile = profileName;
|
|
104
|
+
s.lastCacheHit = result.cacheHit;
|
|
105
|
+
// v12.x:per-pi adapter——registry.ts 给每个 pi 一个新 PiAdapter 实例,
|
|
106
|
+
// 单例字段 this.segment 不会被其他 session 覆盖。
|
|
107
|
+
// Phase term-P4.1:Blueprint.agent 字段移除,暂硬编码 "pi";待 OpenCodeAdapter 后改 transpile(profile, agent)
|
|
108
|
+
s.activeAdapter = getAgentAdapter(pi, "pi");
|
|
109
|
+
s.activeAdapter.setAgentContext(result.agentContext, result.blueprint, result.domains, result.profile);
|
|
110
|
+
slog(sessionId, "info", "transpileActive:done", {
|
|
111
|
+
profileName,
|
|
112
|
+
agent: "pi", // P4.1:硬编码,待 §11 多 Adapter 后改成参数化
|
|
113
|
+
domainCount: result.domains.length,
|
|
114
|
+
segmentLen: result.segment.length,
|
|
115
|
+
cacheHit: result.cacheHit,
|
|
116
|
+
origin: result.activeProfileOrigin, // v15.x PR6:记录 fallback 由来便于 debug
|
|
117
|
+
durationMs: Date.now() - t0,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
slog(sessionId, "error", "transpileActive:failed", {
|
|
122
|
+
err: errMsg(e),
|
|
123
|
+
profileName,
|
|
124
|
+
durationMs: Date.now() - t0,
|
|
125
|
+
});
|
|
126
|
+
// v13.x(issue pt-no-agent-context-reset-session-state 修复):
|
|
127
|
+
// throw 前重置编译产物字段,避免 stale state 让后续 /pt flows 返回旧 Profile 手册
|
|
128
|
+
resetSessionState(getSessionById(sessionId));
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 切换 Profile:重转译 + 通知 + 持久化。
|
|
133
|
+
* v12.x:sessionId 参数。 */
|
|
134
|
+
async function switchProfile(pi, ctx, name) {
|
|
135
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
136
|
+
slog(sessionId, "info", "command:switchProfile start", { profileName: name });
|
|
137
|
+
try {
|
|
138
|
+
await transpileActive(pi, ctx.cwd, name, sessionId, (msg, level) => ctx.ui.notify(msg, level));
|
|
139
|
+
const s = getSessionById(sessionId);
|
|
140
|
+
// v15.x PR6(fix pt-active-profile-fallback-mismatch):transpileActive 可能 fallback
|
|
141
|
+
// (s.activeProfile 已被改写)。此时 s.loadedFrom = "fallback" 表达"用户手动请求但
|
|
142
|
+
// 实际 fallback",比 null 更准确反映状态。
|
|
143
|
+
s.loadedFrom = s.activeProfile !== name ? "fallback" : null;
|
|
144
|
+
persistProfileToSession(pi, s.activeProfile ?? name); // v10.x:session 持久化(issue pt-context-persist-lost)
|
|
145
|
+
// ^ v15.x PR6:持久化用 s.activeProfile(fallback 名)而非原请求名,下次不再 stale
|
|
146
|
+
const injected = registerInjectionIfReady(pi, ctx, sessionId);
|
|
147
|
+
// v11.x:切换后立即标 pending,等下一轮 before_agent_start 翻成 injected
|
|
148
|
+
s.injectionState = "pending";
|
|
149
|
+
s.injectionError = null;
|
|
150
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
151
|
+
await refreshManualWidget(ctx.ui, s);
|
|
152
|
+
slog(sessionId, "info", "command:switchProfile inject", { profileName: name, injected });
|
|
153
|
+
const hint = s.lastCacheHit ? "(缓存命中)" : "(已重编译)";
|
|
154
|
+
ctx.ui.notify(`已切换到 ${name},下一轮生效 ${hint}`, "info");
|
|
155
|
+
slog(sessionId, "info", "command:switchProfile done", {
|
|
156
|
+
profileName: name,
|
|
157
|
+
cacheHit: s.lastCacheHit,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
ctx.ui.notify(`切换失败:${errMsg(e)}`, "error");
|
|
162
|
+
// v13.x(issue pt-no-agent-context-reset-session-state 修复):
|
|
163
|
+
// 重置编译产物 + injection 状态,避免 stale state 让后续 /pt flows 返回旧 Profile 手册
|
|
164
|
+
const s2 = getSessionById(sessionId);
|
|
165
|
+
resetSessionState(s2);
|
|
166
|
+
s2.injectionState = "failed";
|
|
167
|
+
s2.injectionError = errMsg(e);
|
|
168
|
+
refreshInjectionFooter(ctx.ui, s2);
|
|
169
|
+
slog(sessionId, "error", "command:switchProfile failed", {
|
|
170
|
+
profileName: name,
|
|
171
|
+
err: errMsg(e),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export default function (pi) {
|
|
176
|
+
// 启动时 flag(CLI 优先)
|
|
177
|
+
// Phase term-P2:pt-context → pt-profile(命令参数是 Profile 名,名该匹配操作目标)。
|
|
178
|
+
// 向后兼容:--pt-context(flag)和 pt.pt-context/au.pt-context(settings key)作为 fallback 保留——
|
|
179
|
+
// 用户升级 Pt 后旧配置仍能工作,新配置优先。
|
|
180
|
+
pi.registerFlag("pt-profile", {
|
|
181
|
+
description: "启动时激活的 Profile 名(编译成 AgentContext 注入 Session Inject)",
|
|
182
|
+
type: "string",
|
|
183
|
+
});
|
|
184
|
+
// ========== session_start:生成 sessionId + 创 logger + 读默认 profile + 转译 + 注册 adapter ==========
|
|
185
|
+
// v10.x(issue pt-context-persist-lost 修复):fallback 链加第四源(session JSONL)。
|
|
186
|
+
// 优先级:flag > settings > session > auto。
|
|
187
|
+
// session 优先于 auto——保留用户上次选择,避免项目级 auto(>1 project profile 时)抹除用户偏好。
|
|
188
|
+
// 加载成功后调 `persistProfileToSession(pi, picked)` 把来源同步到 JSONL
|
|
189
|
+
// (flag/settings/session 任意来源加载的 profile 都写回 session,作为下次 fallback 的首选)。
|
|
190
|
+
// v11.x:fallback 链额外加 manual 恢复(独立于 profile 链——profile 失败不影响 manual 恢复)。
|
|
191
|
+
// manual 读出后校验文件存在 + status !== completed;满足才挂载 widget。
|
|
192
|
+
// v12.x:从 ctx.sessionManager.getSessionId() 拿 sessionId,按 sessionId 写 per-session state。
|
|
193
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
194
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
195
|
+
if (!sessionId) {
|
|
196
|
+
ctx.ui.notify("Pt:无法获取 session id(pi 版本不兼容)", "error");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const s = getSessionById(sessionId);
|
|
200
|
+
s.sessionId = randomUUID().slice(0, 8); // 短期 ID for logger
|
|
201
|
+
s.logger = new PtLogger(ctx.cwd, "", sessionId);
|
|
202
|
+
s.logger.info("session:start", { sessionId: s.sessionId, cwd: ctx.cwd });
|
|
203
|
+
s.lastCwd = ctx.cwd;
|
|
204
|
+
// v15.x PR4(§6.7.1 + §6.7.5 + §7.5):pack 校验 + settings pack 接通
|
|
205
|
+
// 两段独立 try/catch 兑底——任一异常都不能阻塞 session_start。
|
|
206
|
+
try {
|
|
207
|
+
const projectPack = await loadProjectPack(ctx.cwd);
|
|
208
|
+
const settingsPacks = await loadSettingsPacks(ctx.cwd); // PR4 接通
|
|
209
|
+
const globalPack = await loadGlobalPack();
|
|
210
|
+
const builtinPack = await loadBuiltinPack();
|
|
211
|
+
// settings 包保持声明顺序(不 reverse)——校验顺序不影响结果(每个 pack 独立校验)
|
|
212
|
+
const packsForValidate = [projectPack, ...settingsPacks, globalPack, builtinPack];
|
|
213
|
+
const results = await Promise.all(packsForValidate.map(validatePack));
|
|
214
|
+
s.packValidation = results;
|
|
215
|
+
// project pack 降级(§6.7.3)—— 警告 + 强制回 guide
|
|
216
|
+
const projectResult = results.find((r) => r.source === "project");
|
|
217
|
+
if (projectResult && !projectResult.ok) {
|
|
218
|
+
s.projectPackDegraded = true;
|
|
219
|
+
const firstErr = projectResult.errors[0];
|
|
220
|
+
ctx.ui.notify(`⚠ Pt: project pack 校验失败(${firstErr?.msg ?? "未知错误"})。已降级到 builtin guide。`, "warning");
|
|
221
|
+
ctx.ui.notify(` 修复:/manual:pack-repair`, "info");
|
|
222
|
+
}
|
|
223
|
+
// v15.x PR4(§6.7.5):settings pack 校验失败预警——跳过该 pack,不阻断其他
|
|
224
|
+
for (const r of results) {
|
|
225
|
+
if (r.source === "settings" && !r.ok) {
|
|
226
|
+
const firstErr = r.errors[0];
|
|
227
|
+
ctx.ui.notify(`⚠ Pt: settings pack [@${r.pack}] 校验失败(${firstErr?.msg ?? "未知"})。已跳过该 pack。`, "warning");
|
|
228
|
+
ctx.ui.notify(` 修复:/manual:pack-repair`, "info");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
s.logger?.info("session:pack validation", {
|
|
232
|
+
projectOk: projectResult?.ok ?? false,
|
|
233
|
+
results: results.map((r) => ({
|
|
234
|
+
pack: r.pack,
|
|
235
|
+
source: r.source,
|
|
236
|
+
ok: r.ok,
|
|
237
|
+
errorCount: r.errors.length,
|
|
238
|
+
})),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
// 校验异常仅 log,不阻塞 session_start(陷阱 3:不能阻塞)
|
|
243
|
+
s.logger?.warn("session:pack validation failed", { err: errMsg(e) });
|
|
244
|
+
}
|
|
245
|
+
// v15.x PR1(§7.5 + §7.5.1):全局 Pack 初始化引导——一次性、非交互兼容
|
|
246
|
+
try {
|
|
247
|
+
const globalPackDir = getGlobalPackDir();
|
|
248
|
+
if (shouldPromptGlobalPackGuide({
|
|
249
|
+
isTTY: process.stdout.isTTY === true,
|
|
250
|
+
globalPackExists: existsSync(globalPackDir),
|
|
251
|
+
isFirstRun: !s.globalPackGuideShown,
|
|
252
|
+
isCi: !!process.env.CI,
|
|
253
|
+
guideDisabled: !!process.env.PT_NO_GUIDE,
|
|
254
|
+
})) {
|
|
255
|
+
ctx.ui.notify(`Pt: 全局 Pack 目录不存在: ${globalPackDir}\n 提示: 你可以把通用的 Domain/Blueprint/Profile 放这里跨项目共享\n 创建目录: mkdir -p ${globalPackDir}`, "info");
|
|
256
|
+
s.globalPackGuideShown = true;
|
|
257
|
+
s.logger?.info("session:global pack guide shown", { globalPackDir });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
s.logger?.warn("session:global pack guide failed", { err: errMsg(e) });
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
// Phase term-P2:flag/settings 链主读新名(pt-profile),旧名(pt-context)作 fallback 兼容。
|
|
265
|
+
const flag = pi.getFlag("pt-profile") ?? pi.getFlag("pt-context");
|
|
266
|
+
const flagVal = typeof flag === "string" && flag.trim() ? flag.trim() : undefined;
|
|
267
|
+
const fromSettings = (await readProjectSetting(ctx.cwd, "pt.pt-profile")) ??
|
|
268
|
+
(await readProjectSetting(ctx.cwd, "au.pt-profile")) ??
|
|
269
|
+
(await readProjectSetting(ctx.cwd, "pt.pt-context")) ?? // 向后兼容:旧 settings key
|
|
270
|
+
(await readProjectSetting(ctx.cwd, "au.pt-context"));
|
|
271
|
+
const fromSession = readProfileFromSession(ctx.sessionManager); // v10.x
|
|
272
|
+
const auto = await detectSingleProfile(ctx.cwd);
|
|
273
|
+
const defaultProfile = auto ?? (await detectDefaultProfile(ctx.cwd));
|
|
274
|
+
// 显式分支记录来源(便于 /pt status 展示 + trace)
|
|
275
|
+
let picked;
|
|
276
|
+
let pickedFrom;
|
|
277
|
+
if (flagVal) {
|
|
278
|
+
picked = flagVal;
|
|
279
|
+
pickedFrom = "flag";
|
|
280
|
+
}
|
|
281
|
+
else if (fromSettings) {
|
|
282
|
+
picked = fromSettings;
|
|
283
|
+
pickedFrom = "settings";
|
|
284
|
+
}
|
|
285
|
+
else if (fromSession) {
|
|
286
|
+
picked = fromSession;
|
|
287
|
+
pickedFrom = "session";
|
|
288
|
+
}
|
|
289
|
+
else if (auto) {
|
|
290
|
+
picked = auto;
|
|
291
|
+
pickedFrom = "auto";
|
|
292
|
+
}
|
|
293
|
+
else if (defaultProfile) {
|
|
294
|
+
picked = defaultProfile;
|
|
295
|
+
pickedFrom = "default";
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
picked = undefined;
|
|
299
|
+
pickedFrom = null;
|
|
300
|
+
}
|
|
301
|
+
if (!picked) {
|
|
302
|
+
s.injectionState = "idle";
|
|
303
|
+
s.injectionError = null;
|
|
304
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
305
|
+
ctx.ui.notify("Pt:未找到 Profile。用 /pt-profile <name> 选择,或在 .pi/settings.json 设 pt.pt-profile。", "info");
|
|
306
|
+
s.loadedFrom = null;
|
|
307
|
+
s.logger.info("session:no profile picked", {
|
|
308
|
+
flagVal,
|
|
309
|
+
fromSettings,
|
|
310
|
+
fromSession,
|
|
311
|
+
auto,
|
|
312
|
+
});
|
|
313
|
+
// v11.x:manual fallback 即使无 profile 也要试(手动追踪可独立于 profile)
|
|
314
|
+
await tryRestoreManual(ctx, s);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await transpileActive(pi, ctx.cwd, picked, sessionId, (msg, level) => ctx.ui.notify(msg, level));
|
|
318
|
+
// v15.x PR6(fix pt-active-profile-fallback-mismatch):transpileActive 可能 fallback
|
|
319
|
+
// (s.activeProfile 已被改写为 fallback 名)。此处用 s.activeProfile 同步 downstream。
|
|
320
|
+
// - s.loadedFrom 设为 "fallback" 覆盖原 pickedFrom(如 "session")——用户能看到
|
|
321
|
+
// - persistProfileToSession 持久化 fallback 名(避免下次仍走 stale 路径)
|
|
322
|
+
const finalProfileName = s.activeProfile ?? picked;
|
|
323
|
+
s.loadedFrom = s.activeProfile !== picked ? "fallback" : pickedFrom; // v10.x:可观测性
|
|
324
|
+
persistProfileToSession(pi, finalProfileName); // v10.x:把当前来源同步到 JSONL(下次进程默认走 session)
|
|
325
|
+
// 注册 AgentAdapter 注入(封装 before_agent_start + input)
|
|
326
|
+
const injected = registerInjectionIfReady(pi, ctx, sessionId);
|
|
327
|
+
// v11.x:profile 已加载但还没轮到下一轮 before_agent_start → pending
|
|
328
|
+
s.injectionState = "pending";
|
|
329
|
+
s.injectionError = null;
|
|
330
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
331
|
+
s.logger.info("session:profile loaded", {
|
|
332
|
+
profileName: finalProfileName,
|
|
333
|
+
requested: picked, // v15.x PR6:用户原始请求(与 finalProfileName 不同 = fallback 发生)
|
|
334
|
+
loadedFrom: s.loadedFrom,
|
|
335
|
+
injected,
|
|
336
|
+
});
|
|
337
|
+
// v14.x(issue pt-asset-migration-visibility Layer 2):
|
|
338
|
+
// session_start 末尾批量体检项目所有 profile——主动告知存量项目 schema 错误,
|
|
339
|
+
// 避免"切换才暴露"。失败降级(不阻塞 session 启动)——scan 内部已 try/catch。
|
|
340
|
+
const bundles = s.cachedBundles ?? [];
|
|
341
|
+
const healthBundle = bundles[0];
|
|
342
|
+
if (healthBundle) {
|
|
343
|
+
const report = await scanProjectHealth(ctx.cwd, healthBundle.profiles, healthBundle.blueprints, healthBundle.domains, healthBundle.packs, healthBundle.activeProfilePack, { log: s.logger?.toWriter() });
|
|
344
|
+
s.assetHealthIssues = report.issues;
|
|
345
|
+
if (report.errors > 0 || report.warnings > 0) {
|
|
346
|
+
const summary = report.issues.length === 1
|
|
347
|
+
? `[pt] 项目有 1 个配置问题:${report.issues[0]?.msg ?? ""}(运行 /pt check 查看详情)`
|
|
348
|
+
: `[pt] 项目有 ${report.errors} errors + ${report.warnings} warnings(运行 /pt check 查看详情)`;
|
|
349
|
+
ctx.ui.notify(summary, "warning");
|
|
350
|
+
// 体检结果变化了,刷新 footer 染色
|
|
351
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
352
|
+
}
|
|
353
|
+
s.logger?.info("session:health scan done", {
|
|
354
|
+
issueCount: report.issues.length,
|
|
355
|
+
errors: report.errors,
|
|
356
|
+
warnings: report.warnings,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// v11.x:profile 加载后试恢复 manual(独立于 profile 链)
|
|
360
|
+
await tryRestoreManual(ctx, s);
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
ctx.ui.notify(`Pt 加载失败:${errMsg(e)}`, "error");
|
|
364
|
+
// v13.x(issue pt-no-agent-context-reset-session-state 修复):
|
|
365
|
+
// 统一调 resetSessionState 清编译产物 + loadedFrom;保留 activeProfile(便于用户重试)
|
|
366
|
+
resetSessionState(s);
|
|
367
|
+
s.loadedFrom = null;
|
|
368
|
+
s.injectionState = "failed";
|
|
369
|
+
s.injectionError = errMsg(e);
|
|
370
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
371
|
+
s.logger?.error("session:start failed", { err: errMsg(e) });
|
|
372
|
+
// v11.x:profile 失败但 manual 仍可能独立恢复(手动追踪不依赖 profile)
|
|
373
|
+
await tryRestoreManual(ctx, s);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// ========== session_shutdown:flush logger + 清内存态 ==========
|
|
377
|
+
// v10.x:先 flush 避免丢尾,再 reset 清状态
|
|
378
|
+
// v11.x:resetSession 覆盖 injectionState / activeManual / cachedManualProgress(widget 不持久)
|
|
379
|
+
// v12.x:调 clearSessionById(sessionId) 精确清本 session state,pi-web 多 session 互不污染
|
|
380
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
381
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
382
|
+
if (!sessionId)
|
|
383
|
+
return;
|
|
384
|
+
const s = getSessionById(sessionId);
|
|
385
|
+
if (s.logger) {
|
|
386
|
+
s.logger.info("session:shutdown");
|
|
387
|
+
await s.logger.flush();
|
|
388
|
+
}
|
|
389
|
+
// 同一运行时保留 Pi handler 绑定,但清除旧 session 的 segment/context,
|
|
390
|
+
// 避免新 session 在尚未重新选择 Profile 时继续注入旧内容。
|
|
391
|
+
s.activeAdapter?.resetInjection?.();
|
|
392
|
+
clearSessionById(sessionId);
|
|
393
|
+
});
|
|
394
|
+
// ========== turn 级 trace(P2: 覆盖 turn 生命周期) ==========
|
|
395
|
+
// 任何 turn 异常都能从日志反查;不写入主要因为 UI 噪音,只到 file log。
|
|
396
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
397
|
+
slog(getSessionIdFromCtx(ctx), "debug", "turn:start");
|
|
398
|
+
});
|
|
399
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
400
|
+
// v10.x:event 形态可能包含 token 用量,先取几个字段塞进 ctx
|
|
401
|
+
const e = event;
|
|
402
|
+
slog(getSessionIdFromCtx(ctx), "debug", "turn:end", {
|
|
403
|
+
reason: e.reason,
|
|
404
|
+
messageCount: e.messageCount,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
408
|
+
slog(getSessionIdFromCtx(ctx), "debug", "agent:settled");
|
|
409
|
+
});
|
|
410
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
411
|
+
const e = event;
|
|
412
|
+
slog(getSessionIdFromCtx(ctx), "debug", "tool:call", { name: e.name ?? e.toolName });
|
|
413
|
+
});
|
|
414
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
415
|
+
const e = event;
|
|
416
|
+
slog(getSessionIdFromCtx(ctx), "debug", "tool:result", {
|
|
417
|
+
name: e.name ?? e.toolName,
|
|
418
|
+
isError: e.isError,
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
// ========== /pt-profile 命令:即时切换 ==========
|
|
422
|
+
// Phase term-P2:/pt-context → /pt-profile(命令参数是 Profile 名,名该匹配操作目标)。
|
|
423
|
+
pi.registerCommand("pt-profile", {
|
|
424
|
+
description: "切换当前 Profile(编译成 AgentContext 注入 Session Inject),即时重转译(无参则弹出选择器)",
|
|
425
|
+
getArgumentCompletions: async (prefix) => {
|
|
426
|
+
const sessionId = getSessionIdFromCtx({
|
|
427
|
+
sessionManager: undefined,
|
|
428
|
+
});
|
|
429
|
+
const cwd = (sessionId && getSessionById(sessionId).lastCwd) || process.cwd(); // Q1 修复:fallback 到 process.cwd()
|
|
430
|
+
const names = await listProfiles(cwd);
|
|
431
|
+
const items = names.map((n) => ({ value: n, label: n }));
|
|
432
|
+
const hit = items.filter((i) => i.value.startsWith(prefix));
|
|
433
|
+
return hit.length > 0 ? hit : null;
|
|
434
|
+
},
|
|
435
|
+
handler: async (args, ctx) => {
|
|
436
|
+
const name = args.trim();
|
|
437
|
+
if (!name) {
|
|
438
|
+
const names = await listProfiles(ctx.cwd);
|
|
439
|
+
if (names.length === 0) {
|
|
440
|
+
ctx.ui.notify(`未找到任何 Profile(${PROFILES_DIR}/*.profile.md)`, "warning");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!ctx.hasUI) {
|
|
444
|
+
ctx.ui.notify("/pt-profile(无参)在非交互模式不可用,请指定名称", "warning");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// v14.x(tagline):选择器展示 `name — tagline`,返回 label → 反查 name 走 switchProfile
|
|
448
|
+
const profiles = await listProfilesWithTagline(ctx.cwd);
|
|
449
|
+
const labels = formatProfileLabels(profiles);
|
|
450
|
+
const pickedLabel = await ctx.ui.select("选择 Profile", labels);
|
|
451
|
+
if (!pickedLabel)
|
|
452
|
+
return;
|
|
453
|
+
// 反查:精确匹配 profile 的 label 拿 name;fallback 到 split " — " 取首段(防格式漂移)
|
|
454
|
+
const matched = profiles.find((p) => formatProfileLabels([p])[0] === pickedLabel);
|
|
455
|
+
const picked = matched?.name ?? pickedLabel.split(" — ")[0] ?? pickedLabel;
|
|
456
|
+
await switchProfile(pi, ctx, picked);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await switchProfile(pi, ctx, name);
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
// ========== /pt 命令:查看 Pt 编译产物 ==========
|
|
463
|
+
pi.registerCommand("pt", {
|
|
464
|
+
description: "查看 Pt 转译产物 / 状态(无参=显示当前 segment)",
|
|
465
|
+
handler: async (args, ctx) => {
|
|
466
|
+
// v11.x 修复:原来 `sub = args.trim()` 会把整个 args 作为 sub,导致 `/pt manual <proc>` 时
|
|
467
|
+
// `sub === "manual"` 永远不成立。改为:sub = 第一词,subArgs = 剩余。
|
|
468
|
+
// 兼容现有 logs:clear / status / flows 等单子命令(不带额外参数)行为不变。
|
|
469
|
+
const firstSpace = args.indexOf(" ");
|
|
470
|
+
const head = firstSpace === -1 ? args : args.slice(0, firstSpace);
|
|
471
|
+
const tail = firstSpace === -1 ? "" : args.slice(firstSpace + 1);
|
|
472
|
+
const sub = head.trim().toLowerCase();
|
|
473
|
+
const subArgs = tail;
|
|
474
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
475
|
+
slog(sessionId, "info", "command:/pt invoked", { sub }); // v10.x: P4 子命令 trace
|
|
476
|
+
if (!sessionId) {
|
|
477
|
+
ctx.ui.notify("Pt:无法获取 session id(pi 版本不兼容)", "error");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const s = getSessionById(sessionId);
|
|
481
|
+
if (sub === "status" || sub === "") {
|
|
482
|
+
ctx.ui.notify(statusText(s), "info");
|
|
483
|
+
if (sub === "" && s.cachedSegment) {
|
|
484
|
+
ctx.ui.notify(s.cachedSegment, "info");
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// v15.x §4.4.4(缺口 5):/pt packs 详情命令——pack 诊断信息
|
|
489
|
+
if (sub === "packs") {
|
|
490
|
+
ctx.ui.notify(packsText(s), "info");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (sub === "flows") {
|
|
494
|
+
ctx.ui.notify(flowsText(s), "info");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (sub === "logs") {
|
|
498
|
+
// v10.x:默认查当前 session 的日志(sessionId = 8-hex 短 id)。
|
|
499
|
+
// 如无 session_id(极早期未走 session_start),fallback 到 pt.log。
|
|
500
|
+
const files = await PtLogger.list(ctx.cwd);
|
|
501
|
+
if (files.length === 0) {
|
|
502
|
+
ctx.ui.notify(`无日志(${LOG_DIR}/ 不存在)`, "info");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// 优先当前 session,其次最近修改的 session file,最后 fallback 主文件
|
|
506
|
+
let targetFile = `pt-${s.sessionId}.log`;
|
|
507
|
+
if (!files.includes(targetFile)) {
|
|
508
|
+
if (files.includes("pt.log"))
|
|
509
|
+
targetFile = "pt.log";
|
|
510
|
+
else
|
|
511
|
+
targetFile = files[files.length - 1];
|
|
512
|
+
}
|
|
513
|
+
// 简化: 直接读 targetFile
|
|
514
|
+
const lines = await PtLogger.tail(ctx.cwd, 50, targetFile === "pt.log" ? undefined : targetFile.slice(3, -4));
|
|
515
|
+
if (lines.length === 0) {
|
|
516
|
+
ctx.ui.notify(`日志为空(${targetFile})`, "info");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const formatted = lines
|
|
520
|
+
.map((e) => {
|
|
521
|
+
const stamp = e.ts.slice(11, 23); // HH:MM:SS.mmm
|
|
522
|
+
const lvl = e.level.toUpperCase().padEnd(7);
|
|
523
|
+
const ctxStr = e.ctx && Object.keys(e.ctx).length > 0 ? ` ${JSON.stringify(e.ctx)}` : "";
|
|
524
|
+
return `[${stamp}] [${lvl}] ${e.msg}${ctxStr}`;
|
|
525
|
+
})
|
|
526
|
+
.join("\n");
|
|
527
|
+
const header = `(${targetFile},最近 ${lines.length} 条; 共 ${files.length} 个 session 文件)`;
|
|
528
|
+
ctx.ui.notify(`${header}\n${formatted}`, "info");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (sub === "logs:clear") {
|
|
532
|
+
await PtLogger.clear(ctx.cwd, s.sessionId || undefined);
|
|
533
|
+
ctx.ui.notify(`已清空 .pt/logs/${s.sessionId ? `pt-${s.sessionId}.log` : "pt.log"}`, "info");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (sub === "sessions") {
|
|
537
|
+
// v10.x:列出所有 session 的日志文件(多并发 pi 调试用)。
|
|
538
|
+
const files = await PtLogger.list(ctx.cwd);
|
|
539
|
+
if (files.length === 0) {
|
|
540
|
+
ctx.ui.notify("无 session 日志文件", "info");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
ctx.ui.notify(`已存在的 session 日志:\n${files.map((f) => ` ${f}${f === `pt-${s.sessionId}.log` ? " (current)" : ""}`).join("\n")}`, "info");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (sub === "raw") {
|
|
547
|
+
if (!s.cachedSegment) {
|
|
548
|
+
ctx.ui.notify("无 segment 可显示", "warning");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const dir = join(ctx.cwd, RAW_DIR);
|
|
552
|
+
await mkdir(dir, { recursive: true });
|
|
553
|
+
const file = join(dir, `segment-${Date.now()}.md`);
|
|
554
|
+
await writeFile(file, s.cachedSegment, "utf8");
|
|
555
|
+
ctx.ui.notify(`已写入 ${file}(${s.cachedSegment.length} chars)`, "info");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (sub === "full") {
|
|
559
|
+
// v10.x(fix pt-full-duplicate-segment):用 lastBuiltPrompt 作 canonical source
|
|
560
|
+
// 第一轮之后 = LLM 实际看到的;第一轮之前 fallback 到模拟注入(保留旧版语义)
|
|
561
|
+
const full = buildFullPrompt(ctx.getSystemPrompt(), s.cachedSegment, s.lastBuiltPrompt);
|
|
562
|
+
if (!s.cachedSegment) {
|
|
563
|
+
ctx.ui.notify("警告:无 cachedSegment(未加载 Profile)。用 /pt-profile <name> 选择", "warning");
|
|
564
|
+
}
|
|
565
|
+
const dir = join(ctx.cwd, FULL_DIR);
|
|
566
|
+
await mkdir(dir, { recursive: true });
|
|
567
|
+
const file = join(dir, `prompt-${Date.now()}.md`);
|
|
568
|
+
await writeFile(file, full, "utf8");
|
|
569
|
+
ctx.ui.notify(`完整 systemPrompt 已写入 ${file}(${full.length} chars)`, "info");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (sub === "manual") {
|
|
573
|
+
// v11.x 修复:subArgs 才是 procedure 参数(原 code 会拿到 "manual")
|
|
574
|
+
const procedureParts = subArgs.trim().split(/\s+/);
|
|
575
|
+
const procedureName = procedureParts[0] ?? "";
|
|
576
|
+
const procedureArgs = procedureParts.slice(1).join(" ");
|
|
577
|
+
const r = buildManualDoc(ctx.cwd, s, procedureName, procedureArgs);
|
|
578
|
+
if (r.error) {
|
|
579
|
+
ctx.ui.notify(r.error, "warning");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
await mkdir(join(ctx.cwd, MANUAL_DIR), { recursive: true });
|
|
583
|
+
await writeFile(r.filePath, r.content, "utf8");
|
|
584
|
+
// v11.x:手动跟踪实例 + widget + footer + 持久化
|
|
585
|
+
s.activeManual = {
|
|
586
|
+
filePath: r.filePath,
|
|
587
|
+
procedure: procedureName,
|
|
588
|
+
args: procedureArgs,
|
|
589
|
+
activatedAt: Date.now(),
|
|
590
|
+
};
|
|
591
|
+
persistManualToSession(pi, s.activeManual);
|
|
592
|
+
await refreshManualWidget(ctx.ui, s);
|
|
593
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
594
|
+
ctx.ui.notify(`手册实例已创建: ${r.filePath}`, "info");
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (sub === "check") {
|
|
598
|
+
// v14.x(issue pt-asset-migration-visibility Layer 3):
|
|
599
|
+
// /pt check [--profile X] [--fix]
|
|
600
|
+
// 从 session.assetHealthIssues 格式化(session_start 已扫;不重复扫描)
|
|
601
|
+
// 支持三种写法:/pt check my-profile | /pt check --profile my-profile | /pt check --profile=my-profile
|
|
602
|
+
const checkParts = subArgs
|
|
603
|
+
.trim()
|
|
604
|
+
.split(/\s+/)
|
|
605
|
+
.filter((s) => s.length > 0);
|
|
606
|
+
let profileName;
|
|
607
|
+
let fix = false;
|
|
608
|
+
for (let i = 0; i < checkParts.length; i++) {
|
|
609
|
+
const p = checkParts[i];
|
|
610
|
+
if (p === undefined)
|
|
611
|
+
continue;
|
|
612
|
+
if (p === "--fix") {
|
|
613
|
+
fix = true;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (p === "--profile" || p === "-p") {
|
|
617
|
+
const next = checkParts[i + 1];
|
|
618
|
+
if (next && !next.startsWith("--")) {
|
|
619
|
+
profileName = next;
|
|
620
|
+
i++;
|
|
621
|
+
}
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (p.startsWith("--profile=")) {
|
|
625
|
+
profileName = p.slice("--profile=".length);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!profileName) {
|
|
629
|
+
profileName = p; // 简写:/pt check my-profile
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const r = checkText(s, { profileName, fix });
|
|
633
|
+
ctx.ui.notify(r.output, r.errors > 0 ? "warning" : "info");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
ctx.ui.notify("用法: /pt [status|flows|raw|full|manual|check|packs|logs|logs:clear|sessions]", "warning");
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
// ========== tool 壳:LLM 可调(与 command 共享纯函数内核,.pt/docs/designs/pt-command-tool-dual-registration.md) ==========
|
|
640
|
+
// 只读查询 + 手册实例化做 tool;pt-profile(改 system prompt)不做 tool(见设计文档 §2.4)
|
|
641
|
+
pi.registerTool({
|
|
642
|
+
name: "pt_status",
|
|
643
|
+
label: "Pt Status",
|
|
644
|
+
description: "Show Pt compilation status: active profile, domain/flow counts, segment length, cache hit. Read-only.",
|
|
645
|
+
promptSnippet: "Show Pt status (profile, counts, cache)",
|
|
646
|
+
parameters: Type.Object({}),
|
|
647
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
648
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
649
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: s ? statusText(s) : "no session" }],
|
|
652
|
+
details: {},
|
|
653
|
+
};
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
pi.registerTool({
|
|
657
|
+
name: "pt_packs",
|
|
658
|
+
label: "Pt Packs",
|
|
659
|
+
description: "Show Pt pack details: name/version/desc/asset counts for each loaded pack. Read-only.",
|
|
660
|
+
promptSnippet: "Show Pt pack details (name/version/desc/counts)",
|
|
661
|
+
parameters: Type.Object({}),
|
|
662
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
663
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
664
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
665
|
+
return {
|
|
666
|
+
content: [{ type: "text", text: s ? packsText(s) : "no session" }],
|
|
667
|
+
details: {},
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
pi.registerTool({
|
|
672
|
+
name: "pt_flows",
|
|
673
|
+
label: "Pt Flows",
|
|
674
|
+
description: "List available FlowTemplate manuals in the active Profile. Call before starting a procedure to see what's available. Read-only.",
|
|
675
|
+
promptSnippet: "List available Pt manuals (FlowTemplates)",
|
|
676
|
+
promptGuidelines: [
|
|
677
|
+
"Use pt_flows when you need to know which Pt manuals are available before starting a multi-step procedure.",
|
|
678
|
+
],
|
|
679
|
+
parameters: Type.Object({}),
|
|
680
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
681
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
682
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
683
|
+
return {
|
|
684
|
+
content: [{ type: "text", text: s ? flowsText(s) : "no session" }],
|
|
685
|
+
details: {},
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
pi.registerTool({
|
|
690
|
+
name: "pt_manual",
|
|
691
|
+
label: "Pt Manual",
|
|
692
|
+
description: "Create a manual instance document (.pt/manuals/<procedure>-<ts>.md) with checklist + artifact log. Use when starting a multi-step procedure like feature-lifecycle. Returns the file path.",
|
|
693
|
+
promptSnippet: "Instantiate a Pt manual document with checklist for tracking",
|
|
694
|
+
promptGuidelines: [
|
|
695
|
+
"Use pt_manual when starting a multi-step procedure (e.g., feature-lifecycle, issue-lifecycle, regression-verify) to get a persistent checklist + artifact log.",
|
|
696
|
+
],
|
|
697
|
+
parameters: Type.Object({
|
|
698
|
+
procedure: Type.String({
|
|
699
|
+
description: "FlowTemplate name, e.g. feature-lifecycle, issue-lifecycle, regression-verify",
|
|
700
|
+
}),
|
|
701
|
+
args: Type.Optional(Type.String({
|
|
702
|
+
description: "Arguments for the procedure, e.g. 'req-001' or 'term my-concept'",
|
|
703
|
+
})),
|
|
704
|
+
}),
|
|
705
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
706
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
707
|
+
if (!sessionId) {
|
|
708
|
+
return {
|
|
709
|
+
content: [{ type: "text", text: "no session" }],
|
|
710
|
+
details: { error: "no session" },
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const s = getSessionById(sessionId);
|
|
714
|
+
const r = buildManualDoc(ctx.cwd, s, params.procedure, params.args ?? "");
|
|
715
|
+
if (r.error) {
|
|
716
|
+
return { content: [{ type: "text", text: r.error }], details: { error: r.error } };
|
|
717
|
+
}
|
|
718
|
+
return withFileMutationQueue(r.filePath, async () => {
|
|
719
|
+
await mkdir(join(ctx.cwd, MANUAL_DIR), { recursive: true });
|
|
720
|
+
await writeFile(r.filePath, r.content, "utf8");
|
|
721
|
+
// v11.x:手动跟踪实例 + widget + footer + 持久化
|
|
722
|
+
s.activeManual = {
|
|
723
|
+
filePath: r.filePath,
|
|
724
|
+
procedure: params.procedure,
|
|
725
|
+
args: params.args ?? "",
|
|
726
|
+
activatedAt: Date.now(),
|
|
727
|
+
};
|
|
728
|
+
persistManualToSession(pi, s.activeManual);
|
|
729
|
+
await refreshManualWidget(ctx.ui, s);
|
|
730
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
731
|
+
return {
|
|
732
|
+
content: [{ type: "text", text: `手册实例已创建: ${r.filePath}` }],
|
|
733
|
+
details: { path: r.filePath },
|
|
734
|
+
};
|
|
735
|
+
});
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
pi.registerTool({
|
|
739
|
+
name: "pt_verify",
|
|
740
|
+
label: "Pt Verify",
|
|
741
|
+
description: "Run a verification probe to check if a Manual step was executed correctly. Returns COMPLETED/DEVIATED/INCONCLUSIVE. Use after completing a step that has an observe field.",
|
|
742
|
+
promptSnippet: "Verify a Manual step execution result",
|
|
743
|
+
promptGuidelines: [
|
|
744
|
+
"Use pt_verify after completing a Manual step that has an observe field, to verify the execution result.",
|
|
745
|
+
],
|
|
746
|
+
parameters: Type.Object({
|
|
747
|
+
probe: Type.String({
|
|
748
|
+
description: "Probe name from observe field (e.g. fs-content-match, ts-compiles, test-pass, git-status-clean)",
|
|
749
|
+
}),
|
|
750
|
+
params: Type.Optional(Type.Record(Type.String(), Type.String(), {
|
|
751
|
+
description: "Probe parameters, e.g. { path: 'src/foo.ts', pattern: 'export' }",
|
|
752
|
+
})),
|
|
753
|
+
}),
|
|
754
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
755
|
+
const { runVerify } = await import("./verify/index.js");
|
|
756
|
+
const probeParams = (params.params ?? {});
|
|
757
|
+
const result = await runVerify(ctx.cwd, params.probe, probeParams);
|
|
758
|
+
const text = result.outcome === "COMPLETED"
|
|
759
|
+
? `✓ ${result.message}`
|
|
760
|
+
: result.outcome === "DEVIATED"
|
|
761
|
+
? `✗ ${result.message}${result.actual ? `\n${result.actual}` : ""}`
|
|
762
|
+
: `? ${result.message}`;
|
|
763
|
+
// v11.x:verify 后重读文件刷新 widget(用户可能手动 tick 了 checklist)
|
|
764
|
+
// 不改 session.activeManual,只 refresh 派生数据(widget + cachedManualProgress)
|
|
765
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
766
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
767
|
+
if (s?.activeManual) {
|
|
768
|
+
await refreshManualWidget(ctx.ui, s);
|
|
769
|
+
refreshInjectionFooter(ctx.ui, s);
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
content: [{ type: "text", text }],
|
|
773
|
+
details: result,
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
pi.registerTool({
|
|
778
|
+
name: "pt_check_refs",
|
|
779
|
+
label: "Pt Check Refs",
|
|
780
|
+
description: "Check Profile→Blueprint→Domain reference integrity. Detects dangling references (Profile references non-existent Blueprint or Domain). Read-only.",
|
|
781
|
+
promptSnippet: "Check Pt reference integrity",
|
|
782
|
+
promptGuidelines: [
|
|
783
|
+
"Use pt_check_refs to detect dangling references in Profile/Blueprint/Domain before committing asset changes.",
|
|
784
|
+
],
|
|
785
|
+
parameters: Type.Object({}),
|
|
786
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
787
|
+
const { checkAllRefs, formatRefCheckResult } = await import("./verify/ref-check.js");
|
|
788
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
789
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
790
|
+
const r = await loadAndTranspile(ctx.cwd, s?.activeProfile ?? "");
|
|
791
|
+
const b = r.bundles[0];
|
|
792
|
+
const result = checkAllRefs(b.profiles, b.blueprints, b.domains);
|
|
793
|
+
return {
|
|
794
|
+
content: [{ type: "text", text: formatRefCheckResult(result) }],
|
|
795
|
+
details: result,
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
// v14.x(issue pt-asset-migration-visibility Layer 3):pt_check tool
|
|
800
|
+
// LLM 可主动体检项目配置——尤其在接手陌生项目 / 改资产前调用
|
|
801
|
+
pi.registerTool({
|
|
802
|
+
name: "pt_check",
|
|
803
|
+
label: "Pt Check",
|
|
804
|
+
description: "Scan project assets for known misconfigurations (missing ### Modules, dangling blueprint refs, orphan H2, empty segments, unknown modnames). Read-only.",
|
|
805
|
+
promptSnippet: "Scan Pt project for configuration issues",
|
|
806
|
+
promptGuidelines: [
|
|
807
|
+
"Use pt_check when you suspect a project has stale Pt assets (e.g., after upgrading Pt, before committing Profile changes).",
|
|
808
|
+
"Pair with pt_status to see health count, then pt_check for the detailed list.",
|
|
809
|
+
],
|
|
810
|
+
parameters: Type.Object({
|
|
811
|
+
profile: Type.Optional(Type.String({
|
|
812
|
+
description: "Limit scan to a single Profile name (e.g. 'ysl-developer').",
|
|
813
|
+
})),
|
|
814
|
+
fix: Type.Optional(Type.Boolean({
|
|
815
|
+
description: "Reserved for v2. Currently always false; check output shows `fix:` hints but does not modify files.",
|
|
816
|
+
})),
|
|
817
|
+
}),
|
|
818
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
819
|
+
const sessionId = getSessionIdFromCtx(ctx);
|
|
820
|
+
const s = sessionId ? getSessionById(sessionId) : null;
|
|
821
|
+
if (!s) {
|
|
822
|
+
return {
|
|
823
|
+
content: [{ type: "text", text: "no session" }],
|
|
824
|
+
details: { error: "no session" },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const r = checkText(s, { profileName: params.profile, fix: params.fix ?? false });
|
|
828
|
+
return {
|
|
829
|
+
content: [{ type: "text", text: r.output }],
|
|
830
|
+
details: {
|
|
831
|
+
issueCount: r.issueCount,
|
|
832
|
+
errors: r.errors,
|
|
833
|
+
warnings: r.warnings,
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
}
|