@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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/agent/api-bridge.d.ts +8 -0
  4. package/dist/agent/api-bridge.js +83 -0
  5. package/dist/agent/index.d.ts +2 -0
  6. package/dist/agent/index.js +3 -0
  7. package/dist/agent/pi-adapter.d.ts +35 -0
  8. package/dist/agent/pi-adapter.js +251 -0
  9. package/dist/agent/registry.d.ts +4 -0
  10. package/dist/agent/registry.js +40 -0
  11. package/dist/asset-health.d.ts +50 -0
  12. package/dist/asset-health.js +267 -0
  13. package/dist/asset-pack/loader.d.ts +47 -0
  14. package/dist/asset-pack/loader.js +121 -0
  15. package/dist/asset-pack/manifest.d.ts +23 -0
  16. package/dist/asset-pack/manifest.js +96 -0
  17. package/dist/asset-pack/md-file-pack.d.ts +41 -0
  18. package/dist/asset-pack/md-file-pack.js +153 -0
  19. package/dist/asset-pack/validate.d.ts +55 -0
  20. package/dist/asset-pack/validate.js +131 -0
  21. package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  22. package/dist/builtin/assets/domains/agent-info.md +15 -0
  23. package/dist/builtin/assets/domains/authoring.md +201 -0
  24. package/dist/builtin/assets/domains/pack-repair.md +47 -0
  25. package/dist/builtin/assets/domains/project-analysis.md +52 -0
  26. package/dist/builtin/assets/domains/usage.md +81 -0
  27. package/dist/builtin/assets/domains/user-info.md +15 -0
  28. package/dist/builtin/assets/profiles/guide.profile.md +28 -0
  29. package/dist/commands.d.ts +69 -0
  30. package/dist/commands.js +344 -0
  31. package/dist/compile/agent-context.d.ts +48 -0
  32. package/dist/compile/agent-context.js +423 -0
  33. package/dist/compile/index.d.ts +1 -0
  34. package/dist/compile/index.js +5 -0
  35. package/dist/compile/resolve-use.d.ts +36 -0
  36. package/dist/compile/resolve-use.js +171 -0
  37. package/dist/compile/type-guards.d.ts +29 -0
  38. package/dist/compile/type-guards.js +107 -0
  39. package/dist/config.d.ts +31 -0
  40. package/dist/config.js +133 -0
  41. package/dist/constants.d.ts +52 -0
  42. package/dist/constants.js +81 -0
  43. package/dist/diagnostics.d.ts +14 -0
  44. package/dist/diagnostics.js +29 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +838 -0
  47. package/dist/injection-status.d.ts +45 -0
  48. package/dist/injection-status.js +134 -0
  49. package/dist/log.d.ts +64 -0
  50. package/dist/log.js +139 -0
  51. package/dist/manual-session.d.ts +24 -0
  52. package/dist/manual-session.js +152 -0
  53. package/dist/manual-track.d.ts +35 -0
  54. package/dist/manual-track.js +107 -0
  55. package/dist/parse/blueprint.d.ts +5 -0
  56. package/dist/parse/blueprint.js +59 -0
  57. package/dist/parse/domain-renderers.d.ts +8 -0
  58. package/dist/parse/domain-renderers.js +115 -0
  59. package/dist/parse/domain.d.ts +7 -0
  60. package/dist/parse/domain.js +69 -0
  61. package/dist/parse/index.d.ts +9 -0
  62. package/dist/parse/index.js +223 -0
  63. package/dist/parse/profile.d.ts +18 -0
  64. package/dist/parse/profile.js +117 -0
  65. package/dist/parse/ref-resolver.d.ts +63 -0
  66. package/dist/parse/ref-resolver.js +174 -0
  67. package/dist/parse/shared.d.ts +76 -0
  68. package/dist/parse/shared.js +273 -0
  69. package/dist/profile-persist.d.ts +22 -0
  70. package/dist/profile-persist.js +53 -0
  71. package/dist/render/cache.d.ts +15 -0
  72. package/dist/render/cache.js +135 -0
  73. package/dist/render/index.d.ts +3 -0
  74. package/dist/render/index.js +14 -0
  75. package/dist/render/session-inject.d.ts +5 -0
  76. package/dist/render/session-inject.js +24 -0
  77. package/dist/render/turn-inject.d.ts +36 -0
  78. package/dist/render/turn-inject.js +248 -0
  79. package/dist/schema.d.ts +448 -0
  80. package/dist/schema.js +45 -0
  81. package/dist/session.d.ts +82 -0
  82. package/dist/session.js +102 -0
  83. package/dist/slog.d.ts +3 -0
  84. package/dist/slog.js +27 -0
  85. package/dist/transpile.d.ts +34 -0
  86. package/dist/transpile.js +220 -0
  87. package/dist/verify/file-hash.d.ts +2 -0
  88. package/dist/verify/file-hash.js +30 -0
  89. package/dist/verify/fs-content-match.d.ts +2 -0
  90. package/dist/verify/fs-content-match.js +26 -0
  91. package/dist/verify/fs-exists.d.ts +2 -0
  92. package/dist/verify/fs-exists.js +13 -0
  93. package/dist/verify/fs-not-exists.d.ts +2 -0
  94. package/dist/verify/fs-not-exists.js +13 -0
  95. package/dist/verify/git-status-clean.d.ts +2 -0
  96. package/dist/verify/git-status-clean.js +19 -0
  97. package/dist/verify/index.d.ts +7 -0
  98. package/dist/verify/index.js +46 -0
  99. package/dist/verify/lint-check.d.ts +2 -0
  100. package/dist/verify/lint-check.js +17 -0
  101. package/dist/verify/read-stderr.d.ts +5 -0
  102. package/dist/verify/read-stderr.js +20 -0
  103. package/dist/verify/ref-check.d.ts +13 -0
  104. package/dist/verify/ref-check.js +88 -0
  105. package/dist/verify/test-pass.d.ts +2 -0
  106. package/dist/verify/test-pass.js +17 -0
  107. package/dist/verify/ts-compiles.d.ts +2 -0
  108. package/dist/verify/ts-compiles.js +17 -0
  109. package/package.json +62 -0
  110. package/src/agent/api-bridge.ts +105 -0
  111. package/src/agent/index.ts +4 -0
  112. package/src/agent/pi-adapter.ts +326 -0
  113. package/src/agent/registry.ts +44 -0
  114. package/src/asset-health.ts +327 -0
  115. package/src/asset-pack/loader.ts +141 -0
  116. package/src/asset-pack/manifest.ts +118 -0
  117. package/src/asset-pack/md-file-pack.ts +202 -0
  118. package/src/asset-pack/validate.ts +186 -0
  119. package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
  120. package/src/builtin/assets/domains/agent-info.md +15 -0
  121. package/src/builtin/assets/domains/authoring.md +201 -0
  122. package/src/builtin/assets/domains/pack-repair.md +47 -0
  123. package/src/builtin/assets/domains/project-analysis.md +52 -0
  124. package/src/builtin/assets/domains/usage.md +81 -0
  125. package/src/builtin/assets/domains/user-info.md +15 -0
  126. package/src/builtin/assets/profiles/guide.profile.md +28 -0
  127. package/src/commands.ts +405 -0
  128. package/src/compile/agent-context.ts +487 -0
  129. package/src/compile/index.ts +5 -0
  130. package/src/compile/resolve-use.ts +208 -0
  131. package/src/compile/type-guards.ts +132 -0
  132. package/src/config.ts +154 -0
  133. package/src/constants.ts +104 -0
  134. package/src/diagnostics.ts +36 -0
  135. package/src/index.ts +1028 -0
  136. package/src/injection-status.ts +155 -0
  137. package/src/log.ts +173 -0
  138. package/src/manual-session.ts +190 -0
  139. package/src/manual-track.ts +127 -0
  140. package/src/parse/blueprint.ts +82 -0
  141. package/src/parse/domain-renderers.ts +120 -0
  142. package/src/parse/domain.ts +78 -0
  143. package/src/parse/index.ts +266 -0
  144. package/src/parse/profile.ts +139 -0
  145. package/src/parse/ref-resolver.ts +198 -0
  146. package/src/parse/shared.ts +331 -0
  147. package/src/profile-persist.ts +60 -0
  148. package/src/render/cache.ts +150 -0
  149. package/src/render/index.ts +14 -0
  150. package/src/render/session-inject.ts +26 -0
  151. package/src/render/turn-inject.ts +278 -0
  152. package/src/schema.ts +542 -0
  153. package/src/session.ts +190 -0
  154. package/src/slog.ts +32 -0
  155. package/src/transpile.ts +302 -0
  156. package/src/verify/file-hash.ts +29 -0
  157. package/src/verify/fs-content-match.ts +28 -0
  158. package/src/verify/fs-exists.ts +14 -0
  159. package/src/verify/fs-not-exists.ts +17 -0
  160. package/src/verify/git-status-clean.ts +23 -0
  161. package/src/verify/index.ts +57 -0
  162. package/src/verify/lint-check.ts +21 -0
  163. package/src/verify/read-stderr.ts +24 -0
  164. package/src/verify/ref-check.ts +118 -0
  165. package/src/verify/test-pass.ts +18 -0
  166. package/src/verify/ts-compiles.ts +21 -0
@@ -0,0 +1,155 @@
1
+ // src/injection-status.ts — pt 注入状态检测(纯函数内核)
2
+ //
3
+ // 配套 .pt/docs/designs/pt-injection-status-manual-track.md §2.3:
4
+ // - footer 文本由 InjectionState × profile 决定
5
+ // - 自报(PiAdapter before_agent_start 三分支写 session.injectionState)
6
+ // - 不依赖 ExtensionAPI,可单测
7
+ //
8
+ // 边界纪律:
9
+ // - 不检测 Pi 的 system prompt——自报更准(ctx.getSystemPrompt() 不含注入后内容)
10
+ // - 不存进度——进度从 manual 实例文件 parse(见 manual-track.ts)
11
+ //
12
+ // v14.x(issue pt-asset-migration-visibility Layer 2 修复):
13
+ // - renderInjectionFooter 加 healthIssueCount 参数 → footer 末尾追加 "⚠ N issue(s)"
14
+ // - useColor() 检测 TTY → TUI 用 ANSI 颜色(状态染色),web 用纯文本 + ⚠ 前缀
15
+ // - pi 的 ui.setStatus 不支持 level 参数,ANSI 烧在 text 字段里
16
+ // - back-compat:healthIssueCount 默认 0,旧 call site 行为不变
17
+
18
+ import type { InjectionState } from "./session.js";
19
+
20
+ /** ANSI 转义码集合。烧进 setStatus text 字段,让 TUI 渲染颜色。 */
21
+ const ANSI = {
22
+ reset: "\x1b[0m",
23
+ dim: "\x1b[90m",
24
+ red: "\x1b[31m",
25
+ yellow: "\x1b[33m",
26
+ green: "\x1b[32m",
27
+ bold: "\x1b[1m",
28
+ } as const;
29
+
30
+ /** TTY 检测:TUI 模式 stdout 是 TTY(颜色可用);web 模式 stdout 通常被 pipe(颜色退化为乱码)。
31
+ * 加 isTTY 检查避免 web 下输出 "[31m..." 字面量。 */
32
+ function useColor(): boolean {
33
+ return process.stdout?.isTTY === true;
34
+ }
35
+
36
+ /** 把字符串截断到 N 字符(用 Array.from 避免 surrogate pair 切坏 emoji)。
37
+ * v14.x(tagline):footer 超长 tagline 裁短用。 */
38
+ function truncate(s: string, n: number): string {
39
+ if (s.length <= n) return s;
40
+ // 防止切到 surrogate pair 中间
41
+ const arr = Array.from(s);
42
+ if (arr.length <= n) return s;
43
+ return `${arr.slice(0, n).join("")}...`;
44
+ }
45
+
46
+ /** v14.x(tagline):对 footer tagline 作脱裁 + separator 拼装。
47
+ * - 缺省 tagline → 返回 ''(外层拼接到 footer 不变)
48
+ * - tagline.length ≤ maxLen → 原样
49
+ * - tagline.length > maxLen → 裁到 maxLen - 1 + '…'
50
+ * 返回 `: <tagline>` 形式(带 separator),便于直拼。
51
+ *
52
+ * maxLen 默认 35(与 health suffix 分享 footer 宽度预算)。
53
+ */
54
+ export function formatTaglineForFooter(tagline: string | undefined, maxLen: number = 35): string {
55
+ if (!tagline) return "";
56
+ const trimmed = tagline.trim();
57
+ if (!trimmed) return "";
58
+ if (trimmed.length <= maxLen) return `: ${trimmed}`;
59
+ // 超长:本地手动截 codepoints + 1-char ellipsis(`…`)— 避开 truncate() 的 `...` 3-char 形态
60
+ const arr = Array.from(trimmed);
61
+ if (arr.length <= maxLen) return `: ${trimmed}`;
62
+ return `: ${arr.slice(0, maxLen - 1).join("")}…`;
63
+ }
64
+
65
+ /** 颜色模式。
66
+ * - "auto"(默认):根据 process.stdout.isTTY 自动判断(TUI 用颜色,web 不用)
67
+ * - "always":强制开启 ANSI(手动 debug / 调试 输出场景)
68
+ * - "never": 强制关闭 ANSI(CI / pipe / 重定向场景)
69
+ *
70
+ * v14.x(issue pt-asset-migration-visibility Layer 2):加 colorMode 让测试与 CLI 可覆盖默认 TTY 检测。
71
+ */
72
+ export type ColorMode = "auto" | "always" | "never";
73
+
74
+ /** 把 InjectionState × profile × error × healthIssueCount × tagline 渲染为 footer 文本。
75
+ *
76
+ * 状态颜色映射:
77
+ * - injected → green
78
+ * - pending → yellow
79
+ * - idle → dim gray
80
+ * - failed → red
81
+ *
82
+ * Health suffix:healthIssueCount > 0 → 末尾追加 ` ⚠ N issue(s)`(emoji 前缀作主视觉信号,颜色补充)
83
+ *
84
+ * Tagline(v14.x):profile 名后追加 `: <tagline>`(≤35 字符),超长裁短 + …:
85
+ * - tagline 缺省 → 不追加(back-compat)
86
+ * - tagline 存在且短 → `pt: pt-dev: Senior dev + QA + Reviewer (3 agents) ok`
87
+ * - tagline 超长 → 裁到 34 + '…'
88
+ *
89
+ * profile=null → "pt: 无 context"(其它状态必先有 profile,统一降级)
90
+ * failed 时追加 ": <error 前 40 字>"
91
+ *
92
+ * TUI / Web 兼容:
93
+ * - TUI(isTTY=true):状态染色 ANSI + ⚠ 后缀(颜色补充)
94
+ * - Web(isTTY=false):纯文本 + ⚠ 前缀(无 ANSI 避免 [31m 字面量)
95
+ * - colorMode='always'|'never':覆盖自动检测(调试 / 测试用)
96
+ *
97
+ * back-compat:healthIssueCount + colorMode + tagline 均有默认值,旧 call site 行为不变。 */
98
+ export function renderInjectionFooter(
99
+ state: InjectionState,
100
+ profile: string | null,
101
+ error: string | null,
102
+ healthIssueCount: number = 0,
103
+ colorMode: ColorMode = "auto",
104
+ tagline: string | null = null
105
+ ): string {
106
+ if (profile === null) {
107
+ return "pt: 无 context";
108
+ }
109
+ // 1. 状态后缀(failed 追加 error,injected 用 "ok",其它用 state 名)
110
+ // failed + null error 仍拼 `: unknown`(与 v13 旧版兼容——存在不传 error 的旧 call site)
111
+ let stateSuffix: string;
112
+ if (state === "failed") {
113
+ const errText = error ? truncate(error, 40) : "unknown";
114
+ stateSuffix = ` failed: ${errText}`;
115
+ } else if (state === "injected") {
116
+ stateSuffix = " ok";
117
+ } else {
118
+ stateSuffix = ` ${state}`;
119
+ }
120
+
121
+ // 2. Health suffix:⚠ 前缀作主视觉信号(即便无 ANSI 也能看到)
122
+ // - 0 issue → 不显示
123
+ // - 1 issue → " ⚠ 1 issue"
124
+ // - N issue → " ⚠ N issues"
125
+ const healthSuffix =
126
+ healthIssueCount > 0 ? ` ⚠ ${healthIssueCount} issue${healthIssueCount > 1 ? "s" : ""}` : "";
127
+
128
+ // 3. v14.x tagline:profile 名后拼 `: <tagline-truncated>`(≤35 字符)
129
+ // 无 tagline 时返空串 → 不影响 back-compat
130
+ const taglineSuffix = formatTaglineForFooter(tagline ?? undefined);
131
+
132
+ const baseText = `pt: ${profile}${taglineSuffix}${stateSuffix}${healthSuffix}`;
133
+
134
+ // 4. 颜色决策:colorMode 覆盖 TTY 默认检测
135
+ const colorEnabled = colorMode === "always" ? true : colorMode === "never" ? false : useColor();
136
+ if (!colorEnabled) return baseText;
137
+
138
+ const colorByState: Record<InjectionState, string> = {
139
+ injected: ANSI.green,
140
+ pending: ANSI.yellow,
141
+ idle: ANSI.dim,
142
+ failed: ANSI.red,
143
+ };
144
+ // health issue 染色优先于 state 颜色(视觉上"严重"更突出)
145
+ if (healthIssueCount > 0) {
146
+ return `${ANSI.red}${ANSI.bold}⚠ ${baseText.replace(/^pt: /, "pt: ")}${ANSI.reset}`;
147
+ }
148
+ return `${colorByState[state]}${baseText}${ANSI.reset}`;
149
+ }
150
+
151
+ /** 把 ANSI 颜色码剥成纯文本(test 工具——便于断言内容而忽略颜色)。 */
152
+ export function stripAnsi(text: string): string {
153
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI CSI 序列故意用 ESC 控制符
154
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
155
+ }
package/src/log.ts ADDED
@@ -0,0 +1,173 @@
1
+ // src/log.ts — Pt 持久化日志(NDJSON, append-only, 串行写链)
2
+ //
3
+ // 设计原则:
4
+ // - 零运行时依赖:只用 node:fs/promises + node:path,std lib 搞定
5
+ // - 异步串行链 (writeChain) 保证写入顺序,避免 race condition
6
+ // - 文件路径:<cwd>/.pt/logs/pt.log(NDJSON 行式 JSON, 崩溃时只丢一行)
7
+ // - 失败静默:日志写盘错误不影响主流程(仅 console.error 保留可见性)
8
+ // - 不做自动 rotate:rollover 留作外部脚本 / 手工处理(Pt 转译是低频事件)
9
+ //
10
+ // 技术债 Tech Debt T-new: 与 pt-quality #9(error-via-notify)配套——
11
+ // notify 走 UI 瞬时反馈,log 走磁盘持久 trace,两者互补。
12
+
13
+ import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+
16
+ /** 日志根目录(项目根下,git 排除)。 */
17
+ export const LOG_DIR = ".pt/logs";
18
+
19
+ /** 主日志文件名(单文件 append)。 */
20
+ export const LOG_FILE = "pt.log";
21
+
22
+ /** Pt 支持的日志级别(与 ctx.ui.notify 的 level 集合兼容 + debug)。 */
23
+ export type PtLogLevel = "debug" | "info" | "warning" | "error";
24
+
25
+ /** 单条日志记录(NDJSON 行)。 */
26
+ export interface PtLogEntry {
27
+ /** ISO 时间戳(毫秒精度) */
28
+ ts: string;
29
+ /** 日志级别 */
30
+ level: PtLogLevel;
31
+ /** 人类可读消息(与 notify msg 一致或更细) */
32
+ msg: string;
33
+ /** 结构化上下文(cwd / profile / 关联数据)。SourceAdapterContext 里 log api 走 unknown 留扩展位 */
34
+ ctx?: Record<string, unknown>;
35
+ }
36
+
37
+ /** 给 SourceAdapterContext.log 用的接口(不带 baseCtx,logger 内部注入)。
38
+ * v10.x 设计:log 接口不暴露 file path, 仅暴露级别方法 → 适配器层无法绕过 logger 写文件。 */
39
+ export interface PtLoggerWriter {
40
+ debug(msg: string, ctx?: Record<string, unknown>): void;
41
+ info(msg: string, ctx?: Record<string, unknown>): void;
42
+ warn(msg: string, ctx?: Record<string, unknown>): void;
43
+ error(msg: string, ctx?: Record<string, unknown>): void;
44
+ }
45
+
46
+ /** PtLogger:per-session 单例(由 SessionState.logger 持有,避免跨 session 串台)。
47
+ *
48
+ * v10.x:构造时传入 sessionId → 多并发 `pi` 进程的日志文件分离:
49
+ * - 主文件:`pt-<sessionId>.log`
50
+ * - 反查:ctx.sessionId 字段注入每条 entry
51
+ *
52
+ * 使用方式(index.ts session_start):
53
+ * session.sessionId = randomUUID().slice(0, 8);
54
+ * session.logger = new PtLogger(cwd, "", session.sessionId);
55
+ */
56
+ export class PtLogger implements PtLoggerWriter {
57
+ private writeChain: Promise<void> = Promise.resolve();
58
+ private readonly baseCtx: Record<string, unknown>;
59
+
60
+ constructor(
61
+ public readonly cwd: string,
62
+ public readonly profile: string = "",
63
+ public readonly sessionId: string = ""
64
+ ) {
65
+ this.baseCtx = { cwd, profile, sessionId };
66
+ }
67
+
68
+ /** 当前 session 的日志文件路径:`pt-<sessionId>.log`。
69
+ * sessionId 为空时 fallback 为 `pt.log`(兼容未走 session_start 的旧调用)。 */
70
+ get filePath(): string {
71
+ const name = this.sessionId ? `pt-${this.sessionId}.log` : LOG_FILE;
72
+ return join(this.cwd, LOG_DIR, name);
73
+ }
74
+
75
+ /** 给 SourceAdapterContext.log 用的适配对象(绑定 baseCtx 的 writer)。 */
76
+ toWriter(): PtLoggerWriter {
77
+ return {
78
+ debug: (m, c) => this.debug(m, c),
79
+ info: (m, c) => this.info(m, c),
80
+ warn: (m, c) => this.warn(m, c),
81
+ error: (m, c) => this.error(m, c),
82
+ };
83
+ }
84
+
85
+ /** 内部:链式串行 + 静默失败(写盘错误不传染到链上下一步)。 */
86
+ private write(level: PtLogLevel, msg: string, ctx?: Record<string, unknown>): void {
87
+ const entry: PtLogEntry = {
88
+ ts: new Date().toISOString(),
89
+ level,
90
+ msg,
91
+ ctx: { ...this.baseCtx, ...ctx },
92
+ };
93
+ this.writeChain = this.writeChain
94
+ .then(async () => {
95
+ try {
96
+ await mkdir(join(this.cwd, LOG_DIR), { recursive: true });
97
+ await appendFile(this.filePath, `${JSON.stringify(entry)}\n`, "utf8");
98
+ } catch (e) {
99
+ console.error("[pt-log] write failed:", e instanceof Error ? e.message : e);
100
+ }
101
+ })
102
+ .catch(() => {
103
+ /* 链上一步失败不传染到下一步 */
104
+ });
105
+ }
106
+
107
+ debug(msg: string, ctx?: Record<string, unknown>): void {
108
+ this.write("debug", msg, ctx);
109
+ }
110
+
111
+ info(msg: string, ctx?: Record<string, unknown>): void {
112
+ this.write("info", msg, ctx);
113
+ }
114
+
115
+ warn(msg: string, ctx?: Record<string, unknown>): void {
116
+ this.write("warning", msg, ctx);
117
+ }
118
+
119
+ error(msg: string, ctx?: Record<string, unknown>): void {
120
+ this.write("error", msg, ctx);
121
+ }
122
+
123
+ /** 等待在飞写入全部落地(session_shutdown 时调,避免丢尾)。 */
124
+ async flush(): Promise<void> {
125
+ await this.writeChain.catch(() => {});
126
+ }
127
+
128
+ /** /pt logs 命令支持:从磁盘读最近 N 条(按文件顺序,NDJSON 逐行解析)。
129
+ * v10.x:默认从 `pt.log`(无 sessionId)读——兼容旧文件;如想读 session 文件,传 sessionId。 */
130
+ static async tail(cwd: string, n = 50, sessionId?: string): Promise<PtLogEntry[]> {
131
+ const file = sessionId
132
+ ? join(cwd, LOG_DIR, `pt-${sessionId}.log`)
133
+ : join(cwd, LOG_DIR, LOG_FILE);
134
+ let raw: string;
135
+ try {
136
+ raw = await readFile(file, "utf8");
137
+ } catch {
138
+ return []; // 文件不存在 → 返空(首次启动)
139
+ }
140
+ const lines = raw.trim().split("\n");
141
+ const slice = lines.slice(-n);
142
+ const out: PtLogEntry[] = [];
143
+ for (const line of slice) {
144
+ try {
145
+ out.push(JSON.parse(line) as PtLogEntry);
146
+ } catch {
147
+ /* 跳过损坏行(半行 / 篡改) */
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+
153
+ /** /pt logs clear 命令。清空日志文件(保留目录)。
154
+ * v10.x:清单一文件(默认 `pt.log` 或 sessionId 指定)。 */
155
+ static async clear(cwd: string, sessionId?: string): Promise<void> {
156
+ const dir = join(cwd, LOG_DIR);
157
+ await mkdir(dir, { recursive: true });
158
+ const file = sessionId ? join(dir, `pt-${sessionId}.log`) : join(dir, LOG_FILE);
159
+ await writeFile(file, "", "utf8");
160
+ }
161
+
162
+ /** /pt logs 命令支持:枚举所有日志文件(多 session 时列出全部)。 */
163
+ static async list(cwd: string): Promise<string[]> {
164
+ const dir = join(cwd, LOG_DIR);
165
+ let entries: string[];
166
+ try {
167
+ entries = await readdir(dir);
168
+ } catch {
169
+ return [];
170
+ }
171
+ return entries.filter((f) => f.startsWith("pt-") && f.endsWith(".log")).sort();
172
+ }
173
+ }
@@ -0,0 +1,190 @@
1
+ // src/manual-session.ts — manual 跟踪的 session/JSONL/widget 交互层
2
+ //
3
+ // v11.x:从 src/index.ts 抽出的 6 个函数 + 模块级缓存 + 常量。
4
+ //
5
+ // 职责分层(vs 已抽出的 manual-track.ts):
6
+ // - manual-track.ts:纯函数渲染(renderManualFooterSuffix / renderManualWidgetLines / isManualActive / parseManualProgress)
7
+ // - manual-session.ts:session JSONL 读写 + widget/footer 交互 + ActiveManual 状态机
8
+ //
9
+ // v12.x(issue pt-session-singleton-pi-web-pollution 修复):
10
+ // - 函数全部接受 `session: SessionState` 参数,调用方传 per-session state
11
+ // - `cachedManualProgress` 从 module-level let 搬到 SessionState 字段(session.cachedManualProgress)
12
+ // - 不再 import module-level `session` 单例
13
+ //
14
+ // 调用方:src/index.ts session_start / session_shutdown / 命令 + tool
15
+ //
16
+ // 依赖梳理:
17
+ // - Pi 类型:ExtensionAPI / ExtensionContext / ExtensionUIContext / AgentUIContext
18
+ // - session 单例 → SessionState 参数
19
+ // - manual-track 纯函数:parseManualProgress / renderManualWidgetLines / renderManualFooterSuffix / isManualActive / ManualProgress
20
+ // - injection-status:renderInjectionFooter
21
+ // - profile-persist:MinimalSessionManager
22
+ // - slog:slog(session-scoped logger 快捷)
23
+ // - diagnostics:errMsg
24
+
25
+ import type {
26
+ ExtensionAPI,
27
+ ExtensionContext,
28
+ ExtensionUIContext,
29
+ } from "@earendil-works/pi-coding-agent";
30
+ import {
31
+ isManualActive,
32
+ parseManualProgress,
33
+ renderManualFooterSuffix,
34
+ renderManualWidgetLines,
35
+ } from "./manual-track.js";
36
+ import { renderInjectionFooter } from "./injection-status.js";
37
+ import type { MinimalSessionManager } from "./profile-persist.js";
38
+ import { errMsg } from "./diagnostics.js";
39
+ import { slog } from "./slog.js";
40
+ import type { ActiveManual, SessionState } from "./session.js";
41
+
42
+ /** session JSONL 中持久化 ActiveManual 的 custom entry customType。 */
43
+ const PT_MANUAL_ENTRY = "pt:active-manual";
44
+
45
+ /** 从 session JSONL 读上次保存的 ActiveManual。读出后由 caller 校验(isManualActive)。
46
+ * 静默 fallback:异常 / 无 entry → undefined。 */
47
+ interface PersistedManualEntry {
48
+ filePath: string;
49
+ procedure: string;
50
+ args: string;
51
+ }
52
+ function readManualFromSession(
53
+ sessionManager: MinimalSessionManager
54
+ ): PersistedManualEntry | undefined {
55
+ try {
56
+ const entries = sessionManager.getEntries();
57
+ for (let i = entries.length - 1; i >= 0; i--) {
58
+ const e = entries[i];
59
+ if (e && e.type === "custom" && e.customType === PT_MANUAL_ENTRY) {
60
+ const data = (e as { data?: unknown }).data;
61
+ if (data && typeof data === "object") {
62
+ const d = data as Record<string, unknown>;
63
+ const filePath = d.filePath;
64
+ const procedure = d.procedure;
65
+ const args = d.args;
66
+ if (typeof filePath === "string" && filePath.trim() && typeof procedure === "string") {
67
+ return {
68
+ filePath: filePath.trim(),
69
+ procedure,
70
+ args: typeof args === "string" ? args : "",
71
+ };
72
+ }
73
+ }
74
+ }
75
+ }
76
+ } catch {}
77
+ return undefined;
78
+ }
79
+
80
+ /** 把当前 ActiveManual 写入 session JSONL。
81
+ * 失败静默(ephemeral session / 旧版 pi 无 appendEntry)——内存中 activeManual 仍可用本进程。 */
82
+ function persistManualToSession(pi: ExtensionAPI, m: ActiveManual): void {
83
+ try {
84
+ if (typeof pi.appendEntry !== "function") return;
85
+ pi.appendEntry(PT_MANUAL_ENTRY, {
86
+ filePath: m.filePath,
87
+ procedure: m.procedure,
88
+ args: m.args,
89
+ });
90
+ } catch (e) {
91
+ slog("", "warn", "persistManualToSession failed", { procedure: m.procedure, err: errMsg(e) });
92
+ }
93
+ }
94
+
95
+ /** 计算 active manual 的 footer 后缀(空字符串 = 无 activeManual 或 completed)。 */
96
+ function renderActiveManualSuffix(session: SessionState): string {
97
+ if (!session.activeManual) return "";
98
+ // 同步快速读(无 IO)—— widget 刷新会走 async parseManualProgress
99
+ // footer 只显示 procedure 名 + done/total,避免 IO 阻塞 setStatus
100
+ // 但 stepDone/total 是派生数据,需要同步可读——
101
+ // 这里走同步取缓存策略:保留 widget 异步 parse 的最新结果
102
+ if (!session.cachedManualProgress) return "";
103
+ return renderManualFooterSuffix(session.cachedManualProgress);
104
+ }
105
+
106
+ /** 刷新 footer 注入状态 + manual 后缀(合并写一次 setStatus)。
107
+ * v14.x(issue pt-asset-migration-visibility Layer 2):assetHealthIssues.length 透传给
108
+ * renderInjectionFooter → footer 末尾追加 ⚠ N issues(染色)。
109
+ * v14.x(tagline):从 cachedProfile.tagline 读,footer 拼 `: <tagline>`。 */
110
+ function refreshInjectionFooter(ui: ExtensionUIContext, session: SessionState): void {
111
+ const suffix = renderActiveManualSuffix(session);
112
+ const healthCount = session.assetHealthIssues?.length ?? 0;
113
+ const tagline = session.cachedProfile?.tagline ?? null;
114
+ const base = renderInjectionFooter(
115
+ session.injectionState,
116
+ session.activeProfile,
117
+ session.injectionError,
118
+ healthCount,
119
+ "auto",
120
+ tagline
121
+ );
122
+ ui.setStatus("pt", suffix ? `${base} ${suffix}` : base);
123
+ }
124
+
125
+ /** 刷新 widget(aboveEditor)。根据 session.activeManual 决定显示/撤掉。
126
+ * - 无 activeManual → 撤 widget
127
+ * - 文件不存在 / 已 completed → 清 activeManual + 撤 widget
128
+ * - in-progress → 渲染 3 行 widget + 更新 cachedManualProgress(footer 同步读)
129
+ * v12.x:state 全部从 session 参数读,不再读写 module-level 单例。 */
130
+ async function refreshManualWidget(ui: ExtensionUIContext, session: SessionState): Promise<void> {
131
+ const m = session.activeManual;
132
+ if (!m) {
133
+ session.cachedManualProgress = null;
134
+ ui.setWidget("pt-manual", undefined);
135
+ return;
136
+ }
137
+ const p = await parseManualProgress(m.filePath);
138
+ if (!p || p.status === "completed") {
139
+ session.activeManual = null;
140
+ session.cachedManualProgress = null;
141
+ ui.setWidget("pt-manual", undefined);
142
+ return;
143
+ }
144
+ session.cachedManualProgress = p;
145
+ ui.setWidget("pt-manual", renderManualWidgetLines(m.filePath, p), {
146
+ placement: "aboveEditor",
147
+ });
148
+ }
149
+
150
+ /** session_start 时试恢复 manual:读 pt:active-manual entry → 校验文件存在 + status !== completed。
151
+ * 独立于 profile 加载链——profile 失败 / 无 profile 也能恢复 manual 追踪。 */
152
+ async function tryRestoreManual(ctx: ExtensionContext, session: SessionState): Promise<void> {
153
+ const entry = readManualFromSession(ctx.sessionManager);
154
+ if (!entry) return;
155
+ const active = await isManualActive(entry.filePath);
156
+ if (!active) {
157
+ session.logger?.debug("manual:restore skipped (inactive)", {
158
+ filePath: entry.filePath,
159
+ });
160
+ return;
161
+ }
162
+ session.activeManual = {
163
+ filePath: entry.filePath,
164
+ procedure: entry.procedure,
165
+ args: entry.args,
166
+ activatedAt: Date.now(),
167
+ };
168
+ await refreshManualWidget(ctx.ui, session);
169
+ // widget 设置后才调 footer(refreshManualWidget 写 cachedManualProgress)
170
+ refreshInjectionFooter(ctx.ui, session);
171
+ session.logger?.info("manual:restored", {
172
+ filePath: entry.filePath,
173
+ procedure: entry.procedure,
174
+ });
175
+ }
176
+
177
+ /** 重置 module-level 缓存(session_shutdown 时调,避免新 session 残留旧 manual 状态)。
178
+ * v12.x:改 no-op——cachedManualProgress / activeManual 都搬到 SessionState,
179
+ * session_shutdown 时由 `clearSessionById(sessionId)` 整体删除 entry 即可。 */
180
+ function resetManualSession(_session: SessionState): void {
181
+ // no-op: state 已在 session_shutdown 时通过 clearSessionById 删除
182
+ }
183
+
184
+ export {
185
+ persistManualToSession,
186
+ refreshInjectionFooter,
187
+ refreshManualWidget,
188
+ resetManualSession,
189
+ tryRestoreManual,
190
+ };
@@ -0,0 +1,127 @@
1
+ // src/manual-track.ts — pt Manual 实例追踪(纯函数内核)
2
+ //
3
+ // 配套 .pt/docs/designs/pt-injection-status-manual-track.md §2.4 / §2.5:
4
+ // - parseManualProgress:读文件 → 抽 frontmatter → 正则数 checklist
5
+ // - renderManualWidgetLines:3 行 string[] 给 setWidget 用
6
+ // - renderManualFooterSuffix:footer 后缀("· manual: <procedure> 3/6")
7
+ // - isManualActive:文件存在 + status !== completed
8
+ //
9
+ // 边界纪律:
10
+ // - manual 实例文件不是 Pt 资产(不是 Domain/Blueprint/Profile)→ 不进 parse/ 层
11
+ // - 进度(stepDone/stepTotal)不存 session——文件是 single source of truth
12
+ // - 不依赖 ExtensionAPI,可单测
13
+
14
+ import { access, readFile } from "node:fs/promises";
15
+
16
+ /** Manual 实例文件 parse 结果。
17
+ * - procedure:从 frontmatter.procedure 取
18
+ * - stepDone / stepTotal:实时正则数 "- [x]" / "- [ ]"
19
+ * - status:frontmatter.status(in-progress / completed)
20
+ * - nextStep:第一个 "- [ ]" 行去前缀的文本(下一步该做的),无则 null */
21
+ export interface ManualProgress {
22
+ procedure: string;
23
+ stepDone: number;
24
+ stepTotal: number;
25
+ status: string;
26
+ nextStep: string | null;
27
+ }
28
+
29
+ /** 从 manual 实例文件 parse 进度。失败返回 null(容错,不抛)。 */
30
+ export async function parseManualProgress(filePath: string): Promise<ManualProgress | null> {
31
+ try {
32
+ const content = await readFile(filePath, "utf8");
33
+ return parseManualProgressFromContent(content);
34
+ } catch {
35
+ // 文件不存在 / 权限不足 / 读取失败
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /** 内部:从字符串内容 parse(便于单测 & 未来内联输入)。
41
+ * 算法:
42
+ * 1. frontmatter:取首段 `---` ... `---` 之间的 YAML
43
+ * 2. procedure / status:从 frontmatter 行解析(`key: value` 形式,宽松)
44
+ * 3. checklist:全文正则 `/^- \[([ x])\]\s+(.+)$/gm` 数 done / total
45
+ * 4. nextStep:第一个未完成项的文本(去前缀) */
46
+ export function parseManualProgressFromContent(content: string): ManualProgress | null {
47
+ const fm = extractFrontmatter(content);
48
+ const procedure = fm?.procedure ?? "(unknown)";
49
+ const status = fm?.status ?? "unknown";
50
+
51
+ let stepDone = 0;
52
+ let stepTotal = 0;
53
+ let nextStep: string | null = null;
54
+ // 多行匹配(m 标志),匹配行首 "- [x]" 或 "- [ ]"
55
+ const re = /^- \[([ x])\]\s+(.+)$/gm;
56
+ for (const m of content.matchAll(re)) {
57
+ const checked = m[1] === "x";
58
+ const text = m[2]?.trim() ?? "";
59
+ stepTotal++;
60
+ if (checked) {
61
+ stepDone++;
62
+ } else if (nextStep === null && text) {
63
+ nextStep = text;
64
+ }
65
+ }
66
+
67
+ return { procedure, stepDone, stepTotal, status, nextStep };
68
+ }
69
+
70
+ /** frontmatter 抽取(极简 YAML:只解 key: value)。 */
71
+ interface SimpleFrontmatter {
72
+ procedure?: string;
73
+ status?: string;
74
+ }
75
+ function extractFrontmatter(content: string): SimpleFrontmatter | null {
76
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
77
+ if (!m?.[1]) return null;
78
+ const out: SimpleFrontmatter = {};
79
+ for (const line of m[1].split(/\r?\n/)) {
80
+ const kv = line.match(/^([a-zA-Z_-]+)\s*:\s*(.*)$/);
81
+ if (!kv?.[1]) continue;
82
+ const key = kv[1];
83
+ const value = (kv[2] ?? "").trim();
84
+ if (key === "procedure") out.procedure = value;
85
+ else if (key === "status") out.status = value;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /** widget 渲染:3 行 string[],给 `ui.setWidget("pt-manual", lines, { placement: "aboveEditor" })` 用。
91
+ * - 第 1 行:`pt ▶ <procedure> step <done>/<total> (<status>)`(completed 加 ` ✓ done`)
92
+ * - 第 2 行:` next: <nextStep 文本>`(无 nextStep 时该行省略)
93
+ * - 第 3 行:` file: <filePath>`
94
+ * status === completed 时仍显示,但 footer 后缀改 done;widget 撤掉由 caller 决定(见 index.ts refreshManualWidget)。 */
95
+ export function renderManualWidgetLines(filePath: string, p: ManualProgress): string[] {
96
+ const statusText = p.status === "completed" ? "✓ done" : `(${p.status})`;
97
+ const lines: string[] = [];
98
+ lines.push(`pt ▶ ${p.procedure} step ${p.stepDone}/${p.stepTotal} ${statusText}`);
99
+ if (p.nextStep && p.status !== "completed") {
100
+ lines.push(` next: ${p.nextStep}`);
101
+ }
102
+ lines.push(` file: ${filePath}`);
103
+ return lines;
104
+ }
105
+
106
+ /** footer 后缀:有 activeManual 时追加。
107
+ * - in-progress → `· manual: <procedure> <done>/<total>`
108
+ * - completed → `· manual: <procedure> done` */
109
+ export function renderManualFooterSuffix(p: ManualProgress): string {
110
+ if (p.status === "completed") {
111
+ return `· manual: ${p.procedure} done`;
112
+ }
113
+ return `· manual: ${p.procedure} ${p.stepDone}/${p.stepTotal}`;
114
+ }
115
+
116
+ /** 文件存在 + status !== completed。配合 widget/footer 决定要不要展示。
117
+ * - 用 fs.access 探测存在(轻量,不读全文)
118
+ * - 真的 parse status 用 parseManualProgress(一次 readFile 取齐) */
119
+ export async function isManualActive(filePath: string): Promise<boolean> {
120
+ try {
121
+ await access(filePath);
122
+ } catch {
123
+ return false;
124
+ }
125
+ const p = await parseManualProgress(filePath);
126
+ return p !== null && p.status !== "completed";
127
+ }