@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/src/session.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/session.ts — Pt Session 状态封装(pt-quality #2 / P2.6)
|
|
2
|
+
//
|
|
3
|
+
// 替代 index.ts 10 个模块级 let 变量——收拢到单 state 对象,便于维护和单元测试。
|
|
4
|
+
//
|
|
5
|
+
// 设计:
|
|
6
|
+
// - Map<sessionId, SessionState>(v12.x)—— key 用 pi SessionManager.getSessionId()
|
|
7
|
+
// 支持 pi-web 多 session 并发:不同 session 各自一份 SessionState。
|
|
8
|
+
// TUI 模式下只有一个 session,Map 只有一个 entry,等同 module-level 单例行为。
|
|
9
|
+
// - 所有 setter 都通过 state 字段赋值(不解构)
|
|
10
|
+
// - clearSessionById 暴露给 session_shutdown / 测试
|
|
11
|
+
//
|
|
12
|
+
// v10.x:session-state 升级为 session-scoped daemon 级——
|
|
13
|
+
// - sessionId:crypto 生成的 8-hex 短 id(多并发 `pi` 进程的日志隔离键)
|
|
14
|
+
// - logger :per-session 单例 PtLogger(替代 per-transpile 实例化)
|
|
15
|
+
//
|
|
16
|
+
// v10.x(issue pt-context-persist-lost 修复,现 /pt-profile):loadedFrom 记录当前 activeProfile 的来源,
|
|
17
|
+
// 用于 /pt status 可观测性 + 排查"为什么没选到我预期的 profile"。
|
|
18
|
+
//
|
|
19
|
+
// v12.x(issue pt-session-singleton-pi-web-pollution 修复):
|
|
20
|
+
// - 移除 module-level `session` 单例(设计假设 per-process = per-session 在 pi-web 下不成立)
|
|
21
|
+
// - state 容器改为 Map<sessionId, SessionState>,key 用 pi SessionManager.getSessionId()
|
|
22
|
+
// - 调用方通过 `getSessionById(ctx.sessionManager.getSessionId())` 取 state
|
|
23
|
+
// - 对应 `src/agent/registry.ts` 把 Adapter 注册表也改成 WeakMap<ExtensionAPI, ...>,
|
|
24
|
+
// 保证 Adapter 实例 per-pi,避免"单例 PiAdapter.this.segment 被其他 session 覆盖"。
|
|
25
|
+
// - `cachedManualProgress` 从 module-level let 搬到 SessionState 字段。
|
|
26
|
+
|
|
27
|
+
import type { AssetHealthIssue } from "./asset-health.js";
|
|
28
|
+
import type { ValidationResult } from "./asset-pack/validate.js";
|
|
29
|
+
import type {
|
|
30
|
+
AgentAdapter,
|
|
31
|
+
AgentContext,
|
|
32
|
+
Blueprint,
|
|
33
|
+
Domain,
|
|
34
|
+
Profile,
|
|
35
|
+
SchemaBundle,
|
|
36
|
+
} from "./schema.js";
|
|
37
|
+
import type { PtLogger } from "./log.js";
|
|
38
|
+
import type { ManualProgress } from "./manual-track.js";
|
|
39
|
+
|
|
40
|
+
/** activeProfile 的来源(session_start fallback 命中点)。 */
|
|
41
|
+
export type ProfileLoadSource =
|
|
42
|
+
| "flag"
|
|
43
|
+
| "settings"
|
|
44
|
+
| "session"
|
|
45
|
+
| "auto"
|
|
46
|
+
| "default"
|
|
47
|
+
// v15.x PR6(fix pt-active-profile-fallback-mismatch):显式 fallback 标识
|
|
48
|
+
| "fallback"
|
|
49
|
+
| null;
|
|
50
|
+
|
|
51
|
+
/** pt 注入到 Session Inject 的状态(自报,非检测 Pi)。
|
|
52
|
+
* - idle:未激活 / 无 segment
|
|
53
|
+
* - pending:Profile 已加载但还没轮到下一轮 before_agent_start
|
|
54
|
+
* - injected:本轮 before_agent_start 成功返回注入结果
|
|
55
|
+
* - failed:本轮 before_agent_start catch 异常 */
|
|
56
|
+
export type InjectionState = "idle" | "pending" | "injected" | "failed";
|
|
57
|
+
|
|
58
|
+
/** 当前追踪的 Manual 实例(LLM 调 pt_manual 写入后触发)。
|
|
59
|
+
* 进度(stepDone/stepTotal)不存 session——文件是 single source of truth,
|
|
60
|
+
* 每次 widget 渲染时 parse 文件重新计算。 */
|
|
61
|
+
export interface ActiveManual {
|
|
62
|
+
filePath: string; // .pt/manuals/<procedure>-<ts>.md
|
|
63
|
+
procedure: string;
|
|
64
|
+
args: string;
|
|
65
|
+
activatedAt: number; // Date.now(),排序/去重用
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Session 全量状态。 */
|
|
69
|
+
export interface SessionState {
|
|
70
|
+
activeProfile: string | null;
|
|
71
|
+
cachedSegment: string | null;
|
|
72
|
+
cachedBundles: SchemaBundle[] | null;
|
|
73
|
+
cachedAgentContext: AgentContext | null;
|
|
74
|
+
cachedBlueprint: Blueprint | null;
|
|
75
|
+
cachedDomains: Domain[];
|
|
76
|
+
cachedProfile: Profile | null;
|
|
77
|
+
lastCwd: string;
|
|
78
|
+
lastBuiltPrompt: string | null;
|
|
79
|
+
lastCacheHit: boolean;
|
|
80
|
+
activeAdapter: AgentAdapter | null;
|
|
81
|
+
/** v10.x:session 唯一短 id,8 hex(4.3B 组合空间,足够区分并发 pi 进程)。 */
|
|
82
|
+
sessionId: string;
|
|
83
|
+
/** v10.x:per-session 单例 logger(替代 per-transpile 实例化)。 */
|
|
84
|
+
logger: PtLogger | null;
|
|
85
|
+
/** v10.x:当前 activeProfile 的来源(可观测性)。null 表示未加载。 */
|
|
86
|
+
loadedFrom: ProfileLoadSource;
|
|
87
|
+
/** pt 注入到 Session Inject 的状态(footer 三态文字 + widget 依据)。 */
|
|
88
|
+
injectionState: InjectionState;
|
|
89
|
+
/** failed 时存错误消息(footer 追加)。其它状态 null。 */
|
|
90
|
+
injectionError: string | null;
|
|
91
|
+
/** 当前追踪的 Manual 实例(widget + footer 后缀 + 持久化恢复)。 */
|
|
92
|
+
activeManual: ActiveManual | null;
|
|
93
|
+
/** v12.x:当前 manual widget 的 async parse 缓存(替代原 module-level `cachedManualProgress`)。 */
|
|
94
|
+
cachedManualProgress: ManualProgress | null;
|
|
95
|
+
/** v14.x(issue pt-asset-migration-visibility Layer 2):
|
|
96
|
+
* session_start 批量体检结果——footer 追加 ⚠ N issues + /pt status 暴露。
|
|
97
|
+
* null = 未扫描(用户加载内置 profile 后才扫描过项目 profile)。 */
|
|
98
|
+
assetHealthIssues: AssetHealthIssue[] | null;
|
|
99
|
+
/** v15.x PR1(§6.7.1):session_start 一次性 pack 校验结果——/pt status 展示用。 */
|
|
100
|
+
packValidation: ValidationResult[] | null;
|
|
101
|
+
/** v15.x PR1(§6.7.3):project pack 校验失败时标 true,transpileActive 强制回 guide。 */
|
|
102
|
+
projectPackDegraded: boolean;
|
|
103
|
+
/** v15.x PR1(§7.5):全局 Pack 初始化引导一次性提示标记。 */
|
|
104
|
+
globalPackGuideShown: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 默认空 SessionState。 */
|
|
108
|
+
export function createSessionState(): SessionState {
|
|
109
|
+
return {
|
|
110
|
+
activeProfile: null,
|
|
111
|
+
cachedSegment: null,
|
|
112
|
+
cachedBundles: null,
|
|
113
|
+
cachedAgentContext: null,
|
|
114
|
+
cachedBlueprint: null,
|
|
115
|
+
cachedDomains: [],
|
|
116
|
+
cachedProfile: null,
|
|
117
|
+
lastCwd: "",
|
|
118
|
+
lastBuiltPrompt: null,
|
|
119
|
+
lastCacheHit: false,
|
|
120
|
+
activeAdapter: null,
|
|
121
|
+
sessionId: "",
|
|
122
|
+
logger: null,
|
|
123
|
+
loadedFrom: null,
|
|
124
|
+
injectionState: "idle",
|
|
125
|
+
injectionError: null,
|
|
126
|
+
activeManual: null,
|
|
127
|
+
cachedManualProgress: null,
|
|
128
|
+
assetHealthIssues: null,
|
|
129
|
+
packValidation: null,
|
|
130
|
+
projectPackDegraded: false,
|
|
131
|
+
globalPackGuideShown: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** v12.x:Map<sessionId, SessionState> 容器,key = pi SessionManager.getSessionId()。
|
|
136
|
+
* 对外不导出(避免外部直接 mutate 绕过懒加载)。 */
|
|
137
|
+
const sessionMap = new Map<string, SessionState>();
|
|
138
|
+
|
|
139
|
+
/** 重置编译产物字段(catch 路径用)。
|
|
140
|
+
* v13.x(issue pt-no-agent-context-reset-session-state 修复):
|
|
141
|
+
* - transpile/session_start/switchProfile 三处 catch 调用本函数
|
|
142
|
+
* - 避免 stale cachedAgentContext/cachedBlueprint/cachedDomains/cachedProfile/activeAdapter
|
|
143
|
+
* 误导 /pt flows / /pt manual 返回旧 Profile 的手册列表(掩盖真实失败)
|
|
144
|
+
* - 不清 sessionId/logger/lastCwd/activeProfile/loadedFrom/activeManual(生命周期不同:
|
|
145
|
+
* session_id 标识当前会话、logger 持续写日志、lastCwd 是项目根、activeProfile 是用户意图、
|
|
146
|
+
* loadedFrom 是来源、activeManual 是手动追踪)
|
|
147
|
+
*/
|
|
148
|
+
export function resetSessionState(s: SessionState): void {
|
|
149
|
+
s.cachedSegment = null;
|
|
150
|
+
s.cachedBundles = null;
|
|
151
|
+
s.cachedAgentContext = null;
|
|
152
|
+
s.cachedBlueprint = null;
|
|
153
|
+
s.cachedDomains = [];
|
|
154
|
+
s.cachedProfile = null;
|
|
155
|
+
s.activeAdapter?.resetInjection?.();
|
|
156
|
+
s.activeAdapter = null;
|
|
157
|
+
s.lastCacheHit = false;
|
|
158
|
+
s.injectionState = "idle";
|
|
159
|
+
s.injectionError = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 取指定 sessionId 的 state(lazy create)。TUI 模式下只有一个 entry。 */
|
|
163
|
+
export function getSessionById(sessionId: string): SessionState {
|
|
164
|
+
let s = sessionMap.get(sessionId);
|
|
165
|
+
if (!s) {
|
|
166
|
+
s = createSessionState();
|
|
167
|
+
sessionMap.set(sessionId, s);
|
|
168
|
+
}
|
|
169
|
+
return s;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 列表所有 session state(debug / 诊断用,如 /pt sessions 列表)。 */
|
|
173
|
+
export function listAllSessions(): SessionState[] {
|
|
174
|
+
return [...sessionMap.values()];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 清空指定 sessionId 的 state(session_shutdown 调用)。 */
|
|
178
|
+
export function clearSessionById(sessionId: string): void {
|
|
179
|
+
sessionMap.delete(sessionId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 清空所有 session state(仅测试 / 重启进程等需要)。 */
|
|
183
|
+
export function clearAllSessions(): void {
|
|
184
|
+
sessionMap.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 测试 / 诊断:Map 的当前大小。 */
|
|
188
|
+
export function sessionCount(): number {
|
|
189
|
+
return sessionMap.size;
|
|
190
|
+
}
|
package/src/slog.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/slog.ts — session-scoped logger 快捷调用
|
|
2
|
+
//
|
|
3
|
+
// 设计动机:
|
|
4
|
+
// - 入口代码(src/index.ts)和 session 交互层(src/manual-session.ts)共享同一 slog 快捷
|
|
5
|
+
// - session.logger 为 null 时静默(session_start 之前不可用)
|
|
6
|
+
// - 不依赖 file IO(持久化走 src/log.ts 的 PtLogger 类),仅作调用快捷
|
|
7
|
+
//
|
|
8
|
+
// v11.x:从 src/index.ts 抽出(与 manual-session 同次提交),统一两个调用点的快捷。
|
|
9
|
+
//
|
|
10
|
+
// v12.x(issue pt-session-singleton-pi-web-pollution 修复):
|
|
11
|
+
// - 移除 module-level `session` 单例访问
|
|
12
|
+
// - `slog` 改接受 `sessionId` 参数,按 sessionId 取 per-session logger
|
|
13
|
+
// - sessionId 为空时静默(与原 "session.logger 为 null 时静默" 行为一致)
|
|
14
|
+
//
|
|
15
|
+
// 类型说明:PtLogLevel 用 "warning"(与 ctx.ui.notify 兼容),PtLogger 方法用 "warn"。
|
|
16
|
+
// 此处使用 PtLogger 的方法名(debug/info/warn/error)作为调用 key,与原 slog 行为一致。
|
|
17
|
+
|
|
18
|
+
import { getSessionById } from "./session.js";
|
|
19
|
+
|
|
20
|
+
/** session-scoped logger 快捷调用(sessionId 空时静默——session_start 之前不可用)。
|
|
21
|
+
* level 用 PtLogger 的方法名(debug/info/warn/error),不是 PtLogLevel 的字面量。 */
|
|
22
|
+
export function slog(
|
|
23
|
+
sessionId: string,
|
|
24
|
+
level: "debug" | "info" | "warn" | "error",
|
|
25
|
+
msg: string,
|
|
26
|
+
ctx?: Record<string, unknown>
|
|
27
|
+
): void {
|
|
28
|
+
if (!sessionId) return;
|
|
29
|
+
const logger = getSessionById(sessionId).logger;
|
|
30
|
+
if (!logger) return;
|
|
31
|
+
logger[level](msg, ctx);
|
|
32
|
+
}
|
package/src/transpile.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/transpile.ts — v9 三段式链路:parse → compile → cache → render
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.7:v9 链路——Profile 用户面 + Blueprint 结构层 + AgentAdapter 注入。
|
|
4
|
+
// parse(profile md) → IR (SchemaBundle with profiles)
|
|
5
|
+
// ↓
|
|
6
|
+
// findProfile → findBlueprint → compileAgentContext(profile, blueprint, domains) → AgentContext IR
|
|
7
|
+
// ↓
|
|
8
|
+
// cache.load? 命中 → 用缓存 : cache.save(AgentContext) → 重编译
|
|
9
|
+
// ↓
|
|
10
|
+
// render.systemPrompt(AgentContext, Blueprint) → 给 AgentAdapter 注入 before_agent_start
|
|
11
|
+
//
|
|
12
|
+
// Phase 11.x / P1:单源线性(YAGNI 多源合并)——取第一个成功的 adapter。
|
|
13
|
+
// 旧版多 bundle 循环 + 5 个 lastXxx 累积已删(P1.1)。bundles 仍返 [bundle]
|
|
14
|
+
// 保留 SchemaBundle[] 形态(index.ts:786 r.bundles[0] 依赖)。无成功 bundle
|
|
15
|
+
// 抛错(P1.2)由 index.ts:286 catch 兜底。
|
|
16
|
+
//
|
|
17
|
+
// Phase term-P1:Context IR → AgentContext 改名同步——
|
|
18
|
+
// - compileContext → compileAgentContext
|
|
19
|
+
// - saveContext/loadContext → saveAgentContext/loadAgentContext
|
|
20
|
+
// - TranspileResult.context → TranspileResult.agentContext(字段改名)
|
|
21
|
+
|
|
22
|
+
import { readdir, unlink } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { CACHE_DIR } from "./constants.js";
|
|
25
|
+
import { errMsg, reportError, reportWarn } from "./diagnostics.js";
|
|
26
|
+
import { mdAdapter } from "./parse/index.js";
|
|
27
|
+
import { compileAgentContext } from "./compile/agent-context.js";
|
|
28
|
+
import { saveAgentContext, loadAgentContext } from "./render/cache.js";
|
|
29
|
+
import { renderSessionInject } from "./render/session-inject.js";
|
|
30
|
+
import { findProfile } from "./schema.js";
|
|
31
|
+
import { parseRef, resolveBlueprint } from "./parse/ref-resolver.js";
|
|
32
|
+
import {
|
|
33
|
+
expandProfile,
|
|
34
|
+
UseTargetNotFound,
|
|
35
|
+
UseChainCycle,
|
|
36
|
+
BlueprintGroupOutOfScope,
|
|
37
|
+
} from "./compile/resolve-use.js";
|
|
38
|
+
import type {
|
|
39
|
+
AgentContext,
|
|
40
|
+
Blueprint,
|
|
41
|
+
Domain,
|
|
42
|
+
Profile,
|
|
43
|
+
SchemaBundle,
|
|
44
|
+
SourceAdapter,
|
|
45
|
+
SourceAdapterContext,
|
|
46
|
+
} from "./schema.js";
|
|
47
|
+
|
|
48
|
+
export interface TranspileResult {
|
|
49
|
+
/** 注入 session 的字符串段(聚合所有 inject=session 的聚合组) */
|
|
50
|
+
segment: string;
|
|
51
|
+
/** 各 adapter 返回的 SchemaBundle(保留给 input handler 找 FlowTemplate / listManuals 用) */
|
|
52
|
+
bundles: SchemaBundle[];
|
|
53
|
+
/** 缓存命中信息 */
|
|
54
|
+
cacheHit: boolean;
|
|
55
|
+
/** v9 新增(给 AgentAdapter 用)。Phase term-P1:context → agentContext(IR 改名同步)。 */
|
|
56
|
+
agentContext: AgentContext;
|
|
57
|
+
blueprint: Blueprint;
|
|
58
|
+
domains: Domain[];
|
|
59
|
+
/** 当前激活的 Profile 名 */
|
|
60
|
+
activeProfile: string;
|
|
61
|
+
/** 当前激活的 Profile(listManuals 范围过滤用) */
|
|
62
|
+
profile: Profile;
|
|
63
|
+
/** v15.x PR6(fix pt-active-profile-fallback-mismatch):bundle.activeProfile 由来
|
|
64
|
+
* caller 层依据 origin 决定是否同步 s.activeProfile / loadedFrom / notify。 */
|
|
65
|
+
activeProfileOrigin: "exact" | "fallback";
|
|
66
|
+
/** origin="fallback" 时记录用户原始请求名(caller 用作 notify 文案 + log) */
|
|
67
|
+
originalProfileName?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** ============== Source Adapter 注册表(MVP 只有 MD) ==============
|
|
71
|
+
* P1.1:保留数组形式作为扩展点(未来加 yamlAdapter / dbAdapter),
|
|
72
|
+
* 但 loadAndTranspile 改为"取第一个成功 adapter"——多源合并未来设计(YAGNI)。 */
|
|
73
|
+
const sourceAdapters: SourceAdapter[] = [
|
|
74
|
+
mdAdapter,
|
|
75
|
+
// 未来:yamlAdapter, dbAdapter, ...
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** 三段式转译:parse → compile → cache → render。 */
|
|
79
|
+
export async function loadAndTranspile(
|
|
80
|
+
cwd: string,
|
|
81
|
+
profileName: string,
|
|
82
|
+
adapterCtx?: SourceAdapterContext
|
|
83
|
+
): Promise<TranspileResult> {
|
|
84
|
+
adapterCtx?.log?.debug("transpile:start", { profileName, adapterCount: sourceAdapters.length });
|
|
85
|
+
|
|
86
|
+
// 1. parse:依次调所有 adapter,取第一个成功的 bundle
|
|
87
|
+
let bundle: SchemaBundle | null = null;
|
|
88
|
+
for (const a of sourceAdapters) {
|
|
89
|
+
try {
|
|
90
|
+
const b = await a.load(cwd, profileName, adapterCtx);
|
|
91
|
+
if (b) {
|
|
92
|
+
bundle = b;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
reportError(adapterCtx, `adapter ${a.name} failed: ${errMsg(e)}`, { adapter: a.name });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!bundle) {
|
|
100
|
+
throw new Error(`transpile: no adapter succeeded for profile "${profileName}"`);
|
|
101
|
+
}
|
|
102
|
+
adapterCtx?.log?.info("transpile:parse done", {
|
|
103
|
+
profileName,
|
|
104
|
+
profileCount: bundle.profiles.length,
|
|
105
|
+
blueprintCount: bundle.blueprints.length,
|
|
106
|
+
domainCount: bundle.domains.length,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 2. compile:profile → blueprint → context
|
|
110
|
+
const rawProfile = findProfile(bundle.profiles, bundle.activeProfile);
|
|
111
|
+
if (!rawProfile) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`transpile: profile "${bundle.activeProfile}" not found in bundle (available: ${bundle.profiles.map((p) => p.name).join(", ") || "<none>"})`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// v15.x PR5(§5.3.2 + §8.2):use 单继承展开
|
|
118
|
+
// - 在 findProfile 之后、findBlueprint 之前——展开后 blueprint 可能被 self 覆盖
|
|
119
|
+
// - 展开后 profile 进 compileAgentContext + computeSourceHash(§8.2 use 链自然进 hash)
|
|
120
|
+
// - profileByQualifiedName = workingSet.profiles 视图转换(key 已是 "pack/name")
|
|
121
|
+
// - blueprintByQualifiedName 直接复用 workingSet.blueprints({pack, asset} 视图)
|
|
122
|
+
// v15.x §4.4.2:profileByQualifiedName 从 workingSet.profiles.identity 迭代(key = pack.name/asset.name)
|
|
123
|
+
const profileByQualifiedName = new Map<string, Profile>();
|
|
124
|
+
for (const [k, v] of bundle.workingSet.profiles.identity) {
|
|
125
|
+
profileByQualifiedName.set(k, v.asset);
|
|
126
|
+
}
|
|
127
|
+
// v15.x §4.4.2:blueprintByQualifiedName 从 workingSet.blueprints.identity 取(与原单索引语义一致)
|
|
128
|
+
const blueprintByQualifiedName = bundle.workingSet.blueprints.identity;
|
|
129
|
+
let profile: Profile;
|
|
130
|
+
try {
|
|
131
|
+
profile = expandProfile(
|
|
132
|
+
rawProfile,
|
|
133
|
+
profileByQualifiedName,
|
|
134
|
+
blueprintByQualifiedName,
|
|
135
|
+
bundle.packs,
|
|
136
|
+
new Set(),
|
|
137
|
+
adapterCtx
|
|
138
|
+
);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// use 链错误:reportWarn + rethrow——让上层报"transpile: failed"并阻断激活
|
|
141
|
+
if (
|
|
142
|
+
e instanceof UseTargetNotFound ||
|
|
143
|
+
e instanceof UseChainCycle ||
|
|
144
|
+
e instanceof BlueprintGroupOutOfScope
|
|
145
|
+
) {
|
|
146
|
+
reportWarn(adapterCtx, e.message, {
|
|
147
|
+
profileName: rawProfile.name,
|
|
148
|
+
errorType: e.name,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
const blueprintEntry = resolveBlueprint(
|
|
154
|
+
profile,
|
|
155
|
+
bundle.workingSet.blueprints,
|
|
156
|
+
bundle.packs.map((p) => p.name)
|
|
157
|
+
);
|
|
158
|
+
const blueprint = blueprintEntry?.asset;
|
|
159
|
+
if (!blueprint) {
|
|
160
|
+
const resolved = (() => {
|
|
161
|
+
const { pack, name } = parseRef(profile.blueprint, profile.sourcePack ?? "");
|
|
162
|
+
return `${pack}/${name}`;
|
|
163
|
+
})();
|
|
164
|
+
reportWarn(adapterCtx, `Profile "${profile.name}" 引用未知 Blueprint "${profile.blueprint}"`, {
|
|
165
|
+
profileName: profile.name,
|
|
166
|
+
referencedBlueprint: profile.blueprint,
|
|
167
|
+
resolvedBlueprint: `@${resolved}`,
|
|
168
|
+
// v15.x §4.4.2:availableBlueprints 从 identity 索引读(manifest.name 视图,与原单索引语义一致)
|
|
169
|
+
availableBlueprints: [...bundle.workingSet.blueprints.identity.keys()],
|
|
170
|
+
});
|
|
171
|
+
throw new Error(
|
|
172
|
+
`transpile: blueprint "${profile.blueprint}" not found for profile "${profile.name}"`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
// v15.x PR2(§8.1 + §8.3)+ PR3(§4.4.3 #2):compileAgentContext 加 packs + profilePack + workingSet
|
|
176
|
+
const ctx = compileAgentContext(
|
|
177
|
+
profile,
|
|
178
|
+
blueprint,
|
|
179
|
+
bundle.domains,
|
|
180
|
+
bundle.packs,
|
|
181
|
+
bundle.activeProfilePack,
|
|
182
|
+
bundle.workingSet
|
|
183
|
+
);
|
|
184
|
+
adapterCtx?.log?.debug("transpile:compile done", {
|
|
185
|
+
profileName: profile.name,
|
|
186
|
+
sourceHashPrefix: ctx.sourceHash.slice(0, 8),
|
|
187
|
+
moduleCount: Object.keys(ctx.modules).length,
|
|
188
|
+
packName: ctx.packName,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 3. cache:load 命中 → 用缓存(跳过写入),未命中 → save
|
|
192
|
+
// Phase term-P4.2:cacheDir 改用 constants.CACHE_DIR 常量,签名删 compilation 参数。
|
|
193
|
+
// v15.x PR2(§8.3):loadAgentContext 加 packName 参数,cache 文件名 <pack>__<profile>。
|
|
194
|
+
const cached = await loadAgentContext(cwd, ctx.packName, ctx.name, ctx.sourceHash);
|
|
195
|
+
let used: AgentContext;
|
|
196
|
+
let cacheHit = false;
|
|
197
|
+
if (cached) {
|
|
198
|
+
cacheHit = true;
|
|
199
|
+
adapterCtx?.log?.info("transpile:cache hit", { agentContextName: ctx.name });
|
|
200
|
+
used = cached;
|
|
201
|
+
} else {
|
|
202
|
+
adapterCtx?.log?.info("transpile:cache miss → save", { agentContextName: ctx.name });
|
|
203
|
+
await saveAgentContext(cwd, ctx);
|
|
204
|
+
used = ctx;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 4. render:剥 asset 分隔注释
|
|
208
|
+
// Phase term-P4.3:renderSystemPrompt → renderSessionInject(inject 语义值 session/turn)。
|
|
209
|
+
const segment = renderSessionInject(used, blueprint)
|
|
210
|
+
.replace(/<!-- =====[^\n]*-->\n?/g, "")
|
|
211
|
+
.trim();
|
|
212
|
+
|
|
213
|
+
// issue pt-status-no-injection-state:segmentLen === 0 几乎总是配置错误
|
|
214
|
+
// (profile 缺 ### Modules / blueprint groups 名错配 / domain 段名不匹配等)。
|
|
215
|
+
// cache hit 路径下特别危险——空 segment 被缓存复用,footer 永远 idle。
|
|
216
|
+
// 主动告警让用户首次切换就发现问题,而不是依赖 `injected: true` 误导
|
|
217
|
+
if (segment.length === 0) {
|
|
218
|
+
reportWarn(
|
|
219
|
+
adapterCtx,
|
|
220
|
+
`Profile「${profile.name}」编译产出空 segment(注入不会生效,footer 会一直 idle)`,
|
|
221
|
+
{
|
|
222
|
+
profileName: profile.name,
|
|
223
|
+
cacheHit,
|
|
224
|
+
hint: "检查 Profile 的 H2 段下 `### Modules` 配置、Blueprint groups 名匹配、Domain H2 段名",
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 5. prune orphan caches(v13.x issue pt-no-agent-context-prune-orphan-caches 修复)
|
|
230
|
+
// Profile 删除/改名后,旧 cache 文件残留——transpile 末尾自动 unlink
|
|
231
|
+
// validProfileNames 来自当前 bundle(项目 + builtin 合并后的全集)
|
|
232
|
+
// Phase term-P4.2:Blueprint.competition 已移除,cacheDir 用 CACHE_DIR 常量
|
|
233
|
+
const pruned = await pruneOrphanCaches(
|
|
234
|
+
cwd,
|
|
235
|
+
CACHE_DIR,
|
|
236
|
+
new Set(bundle.profiles.map((p) => p.name))
|
|
237
|
+
);
|
|
238
|
+
if (pruned.length > 0) {
|
|
239
|
+
adapterCtx?.log?.info("transpile:prune orphan caches", { pruned });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
adapterCtx?.log?.info("transpile:done", {
|
|
243
|
+
profileName: profile.name,
|
|
244
|
+
segmentLen: segment.length,
|
|
245
|
+
cacheHit,
|
|
246
|
+
prunedCount: pruned.length,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
segment,
|
|
251
|
+
bundles: [bundle],
|
|
252
|
+
cacheHit,
|
|
253
|
+
agentContext: used,
|
|
254
|
+
blueprint,
|
|
255
|
+
domains: bundle.domains,
|
|
256
|
+
activeProfile: profile.name,
|
|
257
|
+
profile,
|
|
258
|
+
// v15.x PR6(fix pt-active-profile-fallback-mismatch):透传 origin 给 caller
|
|
259
|
+
activeProfileOrigin: bundle.activeProfileOrigin,
|
|
260
|
+
originalProfileName: bundle.originalProfileName,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 删除孤儿 cache 文件(Profile 已不存在的 cache)。
|
|
265
|
+
* v13.x(issue pt-no-agent-context-prune-orphan-caches):transpile 末尾自动清理,
|
|
266
|
+
* 避免 Profile 删改后旧 cache 文件残留误导排查。
|
|
267
|
+
*
|
|
268
|
+
* @param cwd 项目根目录
|
|
269
|
+
* @param cacheDir 缓存目录(从 Blueprint.compilation.cacheDir 读,默认 `.pt/cache/agent-contexts/`)
|
|
270
|
+
* @param validProfileNames 当前 bundle.profiles 合并后的全集(项目覆盖 builtin)
|
|
271
|
+
* @returns 被 unlink 的 cache 名列表(用于 trace)
|
|
272
|
+
*/
|
|
273
|
+
export async function pruneOrphanCaches(
|
|
274
|
+
cwd: string,
|
|
275
|
+
cacheDir: string,
|
|
276
|
+
validProfileNames: Set<string>
|
|
277
|
+
): Promise<string[]> {
|
|
278
|
+
const dir = join(cwd, cacheDir);
|
|
279
|
+
let files: string[];
|
|
280
|
+
try {
|
|
281
|
+
files = await readdir(dir);
|
|
282
|
+
} catch {
|
|
283
|
+
return []; // 目录不存在(首次加载)返空,不抛错
|
|
284
|
+
}
|
|
285
|
+
const pruned: string[] = [];
|
|
286
|
+
for (const f of files) {
|
|
287
|
+
if (!f.endsWith(".agent-context.md")) continue;
|
|
288
|
+
// v15.x PR2(§8.3):cache 文件名 <pack>__<profile>.agent-context.md——split "__" 拿 profile name
|
|
289
|
+
// PR1 阶段:<profile>.agent-context.md——slice 去后缀。二者需向后兼容。
|
|
290
|
+
const stem = f.slice(0, -".agent-context.md".length);
|
|
291
|
+
const sepIdx = stem.lastIndexOf("__");
|
|
292
|
+
const name = sepIdx >= 0 ? stem.slice(sepIdx + 2) : stem;
|
|
293
|
+
if (validProfileNames.has(name)) continue;
|
|
294
|
+
try {
|
|
295
|
+
await unlink(join(dir, f));
|
|
296
|
+
pruned.push(name);
|
|
297
|
+
} catch {
|
|
298
|
+
// unlink 失败(权限/不存在)跳过——不影响主流程
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return pruned;
|
|
302
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/verify/file-hash.ts — 验证文件 sha256 hash 匹配
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
6
|
+
|
|
7
|
+
export async function fileHash(cwd: string, params: Record<string, string>): Promise<ProbeOutcome> {
|
|
8
|
+
const path = params.path;
|
|
9
|
+
if (!path) return { outcome: "INCONCLUSIVE", message: "缺少参数: path" };
|
|
10
|
+
const expected = params.expected;
|
|
11
|
+
if (!expected) return { outcome: "INCONCLUSIVE", message: "缺少参数: expected (sha256)" };
|
|
12
|
+
try {
|
|
13
|
+
const content = readFileSync(join(cwd, path));
|
|
14
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
15
|
+
const match = hash === expected;
|
|
16
|
+
return {
|
|
17
|
+
outcome: match ? "COMPLETED" : "DEVIATED",
|
|
18
|
+
message: match
|
|
19
|
+
? `hash 匹配: ${path}`
|
|
20
|
+
: `hash 不匹配: ${path}(期望 ${expected.slice(0, 8)}...,实际 ${hash.slice(0, 8)}...)`,
|
|
21
|
+
actual: hash,
|
|
22
|
+
};
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return {
|
|
25
|
+
outcome: "INCONCLUSIVE",
|
|
26
|
+
message: `无法读取 ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/verify/fs-content-match.ts — 验证文件包含某文本
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
5
|
+
|
|
6
|
+
export async function fsContentMatch(
|
|
7
|
+
cwd: string,
|
|
8
|
+
params: Record<string, string>
|
|
9
|
+
): Promise<ProbeOutcome> {
|
|
10
|
+
const path = params.path;
|
|
11
|
+
if (!path) return { outcome: "INCONCLUSIVE", message: "缺少参数: path" };
|
|
12
|
+
const pattern = params.pattern;
|
|
13
|
+
if (!pattern) return { outcome: "INCONCLUSIVE", message: "缺少参数: pattern" };
|
|
14
|
+
try {
|
|
15
|
+
const content = readFileSync(join(cwd, path), "utf8");
|
|
16
|
+
const found = content.includes(pattern);
|
|
17
|
+
return {
|
|
18
|
+
outcome: found ? "COMPLETED" : "DEVIATED",
|
|
19
|
+
message: found ? `文件 ${path} 包含 "${pattern}"` : `文件 ${path} 不包含 "${pattern}"`,
|
|
20
|
+
actual: found ? "found" : "not-found",
|
|
21
|
+
};
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return {
|
|
24
|
+
outcome: "INCONCLUSIVE",
|
|
25
|
+
message: `无法读取 ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/verify/fs-exists.ts — 验证文件存在
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
5
|
+
|
|
6
|
+
export async function fsExists(cwd: string, params: Record<string, string>): Promise<ProbeOutcome> {
|
|
7
|
+
const path = params.path;
|
|
8
|
+
if (!path) return { outcome: "INCONCLUSIVE", message: "缺少参数: path" };
|
|
9
|
+
const exists = existsSync(join(cwd, path));
|
|
10
|
+
return {
|
|
11
|
+
outcome: exists ? "COMPLETED" : "DEVIATED",
|
|
12
|
+
message: exists ? `存在: ${path}` : `不存在: ${path}`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/verify/fs-not-exists.ts — 验证文件不存在
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
5
|
+
|
|
6
|
+
export async function fsNotExists(
|
|
7
|
+
cwd: string,
|
|
8
|
+
params: Record<string, string>
|
|
9
|
+
): Promise<ProbeOutcome> {
|
|
10
|
+
const path = params.path;
|
|
11
|
+
if (!path) return { outcome: "INCONCLUSIVE", message: "缺少参数: path" };
|
|
12
|
+
const exists = existsSync(join(cwd, path));
|
|
13
|
+
return {
|
|
14
|
+
outcome: !exists ? "COMPLETED" : "DEVIATED",
|
|
15
|
+
message: !exists ? `已删除: ${path}` : `仍存在: ${path}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/verify/git-status-clean.ts — 验证 git 工作树干净
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import type { ProbeOutcome } from "../schema.js";
|
|
4
|
+
|
|
5
|
+
export async function gitStatusClean(
|
|
6
|
+
cwd: string,
|
|
7
|
+
_params: Record<string, string>
|
|
8
|
+
): Promise<ProbeOutcome> {
|
|
9
|
+
try {
|
|
10
|
+
const out = execSync("git status --porcelain", { cwd, encoding: "utf8", timeout: 10000 });
|
|
11
|
+
const clean = out.trim() === "";
|
|
12
|
+
return {
|
|
13
|
+
outcome: clean ? "COMPLETED" : "DEVIATED",
|
|
14
|
+
message: clean ? "git 工作树干净" : `git 工作树有变更:\n${out.trim()}`,
|
|
15
|
+
actual: clean ? "clean" : "dirty",
|
|
16
|
+
};
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return {
|
|
19
|
+
outcome: "INCONCLUSIVE",
|
|
20
|
+
message: `git 执行失败: ${e instanceof Error ? e.message : String(e)}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|