@istuen/pt 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ import type { ProbeOutcomeKind } from "./schema.js";
2
+ /** findStepForProbe 返回结构(probe 命中的 step 信息) */
3
+ export interface StepMatch {
4
+ /** step 的 1-indexed 编号(对应 manual 文件 ## 执行状态 表的行号) */
5
+ stepIndex: number;
6
+ /** step 的 desc 文本(去 "- [ ] " 前缀) */
7
+ desc: string;
8
+ /** step 的 observe 列表(来自 ` - 验证参照:xxx, yyy` 行) */
9
+ observeNames: string[];
10
+ }
11
+ /** writeProbeResult 返回结构 */
12
+ export interface WritebackResult {
13
+ /** 匹配上的 step 数(0 = probe 名不在任何 step 的 observe 中) */
14
+ matchCount: number;
15
+ /** 写回的 step 索引(1-indexed) */
16
+ stepIndexes: number[];
17
+ /** 因全部 observe 完成而勾选 checklist 的 step 索引(1-indexed) */
18
+ checkedSteps: number[];
19
+ /** 改后的文件内容(changed=false 时返回原 content) */
20
+ content: string;
21
+ /** 是否真的改了文件 */
22
+ changed: boolean;
23
+ /** 改后的 completed-probes 列表(含历史 + 本次) */
24
+ completedProbes: string[];
25
+ }
26
+ /** 从 manual 内容里按 probe 名找匹配的 step。多个 step 有同一 observe 时取首个。
27
+ * 返回 null = probe 名不在任何 step 的 observe 中。 */
28
+ export declare function findStepForProbe(content: string, probeName: string): StepMatch | null;
29
+ /** 写回 probe 结果到 manual 文件内容(同步,不读文件)。
30
+ * - 写执行状态表 `| N | — | |` → `| N | <outcome> | <message> |`
31
+ * - 改 frontmatter 注释维护 completed probe 列表
32
+ * - 多 observe 同步:若 step 的全部 observe 都 in 列表 → checklist `- [ ]` → `- [x]`
33
+ * - 幂等:同一 probe 跑两次 → 用最新覆盖原 message
34
+ * - 边界:
35
+ * - 找不到匹配 step → matchCount=0, changed=false(让 caller 走 warn 分支)
36
+ * - 找不到执行状态表对应行 → 不改(保守:manual 损坏不擅自补) */
37
+ export declare function writeProbeResult(content: string, probeName: string, outcome: ProbeOutcomeKind, message: string): WritebackResult;
@@ -0,0 +1,195 @@
1
+ // src/manual-writeback.ts — pt_verify 结果自动写回 manual 实例(纯函数内核)
2
+ //
3
+ // v15.x(issue pt-verify-result-not-written-back-to-manual):
4
+ // pt_verify tool 跑完 probe 后,自动把结果写回 activeManual 文件的 ## 执行状态 表。
5
+ // 闭环不靠 LLM 自觉——与 pt-collab.md 的 acceptance 原则一致。
6
+ //
7
+ // 设计要点:
8
+ // - probe ↔ step 映射:按 FlowStep.observe 列表匹配 probe 名(首个匹配)
9
+ // - 多 observe 同步:frontmatter 注释维护已完成 probe 列表;该 step 全部 observe 都
10
+ // 完成时 checklist `- [ ]` → `- [x]`
11
+ // - 写回幂等:同一 probe 跑两次 → 用最新 outcome/message 覆盖原行
12
+ // - 纯函数:writeProbeResult 只改字符串,不碰 IO;IO 由 caller 包 withFileMutationQueue
13
+ //
14
+ // 边界纪律:
15
+ // - 不动 parseManualProgress / renderManualWidgetLines / renderManualFooterSuffix
16
+ // (manual-track.ts 保持纯解析语义,写回是上层职责)
17
+ // - 不依赖 ExtensionAPI,可单测
18
+ // - manual 实例文件不是 Pt 资产(不是 Domain/Blueprint/Profile)→ 不进 parse/ 层
19
+ /** 从 manual 内容里扫 step + observe,建立 stepIndex → StepMatch 索引。
20
+ * 算法:
21
+ * 1. 逐行扫描:step 行 `^- \[([ x])\] (.+)$` 记录 stepIndex(1-indexed 累计)——兼容已勾选 `- [x]`;
22
+ * 验证参照行 `^ - 验证参照:(.+)$` 关联到上一个 step。
23
+ * 2. step 行格式兼容:`^- \[ \] \d+\. xxx`(旧版 buildManualDoc)也支持(取 \d+ 后内容)。
24
+ * 失败返回空 Map(不含任何 step)。 */
25
+ function indexSteps(content) {
26
+ const out = new Map();
27
+ let stepIndex = 0;
28
+ let lastStep = null;
29
+ const lines = content.split("\n");
30
+ for (const line of lines) {
31
+ // step 行:兼容 "- [ ] desc" / "- [x] desc" / "- [ ] N. desc" / "- [x] N. desc"
32
+ // 多轮 writeback 场景下,已勾选 step 必须算入索引(避免后续 step 索引错乱)
33
+ const stepMatch = line.match(/^- \[[ x]\]\s+(?:\d+\.\s+)?(.+)$/);
34
+ if (stepMatch) {
35
+ stepIndex++;
36
+ lastStep = {
37
+ stepIndex,
38
+ desc: (stepMatch[1] ?? "").trim(),
39
+ observeNames: [],
40
+ };
41
+ out.set(stepIndex, lastStep);
42
+ continue;
43
+ }
44
+ // observe 行(紧跟 step 的子项)
45
+ const observeMatch = line.match(/^ {2}- 验证参照:(.+)$/);
46
+ if (observeMatch && lastStep) {
47
+ const names = (observeMatch[1] ?? "")
48
+ .split(",")
49
+ .map((s) => s.trim())
50
+ .filter((s) => s.length > 0);
51
+ lastStep.observeNames = names;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ /** 从 manual 内容里按 probe 名找匹配的 step。多个 step 有同一 observe 时取首个。
57
+ * 返回 null = probe 名不在任何 step 的 observe 中。 */
58
+ export function findStepForProbe(content, probeName) {
59
+ if (!probeName)
60
+ return null;
61
+ const steps = indexSteps(content);
62
+ for (const step of steps.values()) {
63
+ if (step.observeNames.includes(probeName)) {
64
+ return step;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ /** 解析 frontmatter 注释里的 completed probes 列表(pt-verify-completed 行)。
70
+ * 格式:<!-- pt-verify-completed: probe1, probe2, probe3 -->
71
+ * 缺省返空 Set。 */
72
+ function parseCompletedProbes(content) {
73
+ const out = new Set();
74
+ const m = content.match(/<!--\s*pt-verify-completed:\s*([\s\S]*?)\s*-->/);
75
+ if (!m?.[1])
76
+ return out;
77
+ for (const name of m[1].split(",")) {
78
+ const trimmed = name.trim();
79
+ if (trimmed.length > 0)
80
+ out.add(trimmed);
81
+ }
82
+ return out;
83
+ }
84
+ /** 把 completed probes Set 序列化成 frontmatter 注释行(含前后空格)。
85
+ * 若 Set 为空,返回空字符串(删除注释行时不主动清,由 caller 决定)。 */
86
+ function renderCompletedProbes(probes) {
87
+ if (probes.size === 0)
88
+ return "";
89
+ const list = [...probes].sort().join(", ");
90
+ return `<!-- pt-verify-completed: ${list} -->`;
91
+ }
92
+ /** 写回 probe 结果到 manual 文件内容(同步,不读文件)。
93
+ * - 写执行状态表 `| N | — | |` → `| N | <outcome> | <message> |`
94
+ * - 改 frontmatter 注释维护 completed probe 列表
95
+ * - 多 observe 同步:若 step 的全部 observe 都 in 列表 → checklist `- [ ]` → `- [x]`
96
+ * - 幂等:同一 probe 跑两次 → 用最新覆盖原 message
97
+ * - 边界:
98
+ * - 找不到匹配 step → matchCount=0, changed=false(让 caller 走 warn 分支)
99
+ * - 找不到执行状态表对应行 → 不改(保守:manual 损坏不擅自补) */
100
+ export function writeProbeResult(content, probeName, outcome, message) {
101
+ const baseResult = {
102
+ matchCount: 0,
103
+ stepIndexes: [],
104
+ checkedSteps: [],
105
+ content,
106
+ changed: false,
107
+ completedProbes: [],
108
+ };
109
+ if (!probeName)
110
+ return baseResult;
111
+ const steps = indexSteps(content);
112
+ if (steps.size === 0)
113
+ return baseResult;
114
+ const match = findStepForProbe(content, probeName);
115
+ if (!match)
116
+ return baseResult;
117
+ baseResult.matchCount = 1;
118
+ baseResult.stepIndexes.push(match.stepIndex);
119
+ // 1. 改执行状态表对应行(幂等覆盖:找 `| N | ... |` 任意中间值)
120
+ // `.+` 贪婪匹配到最后一个 ` |$`,覆盖已填行(COMPLETED|DEVIATED|INCONCLUSIVE)。
121
+ let out = content;
122
+ const statusLineRe = new RegExp(`^\\| ${match.stepIndex} \\| .+ \\|$`, "m");
123
+ if (statusLineRe.test(out)) {
124
+ // 转义 message 里的 | 字符(避免破坏 markdown 表格)+ 反斜杠(避免后续正则误判)
125
+ const safeMsg = message.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
126
+ out = out.replace(statusLineRe, `| ${match.stepIndex} | ${outcome} | ${safeMsg} |`);
127
+ }
128
+ // 2. 维护 frontmatter 注释里的 completed probes 列表
129
+ const completed = parseCompletedProbes(out);
130
+ completed.add(probeName);
131
+ baseResult.completedProbes = [...completed];
132
+ // 写回或追加 frontmatter 注释
133
+ const commentRe = /<!--\s*pt-verify-completed:\s*[\s\S]*?-->/;
134
+ const newComment = renderCompletedProbes(completed);
135
+ if (commentRe.test(out)) {
136
+ out = out.replace(commentRe, newComment);
137
+ }
138
+ else if (newComment) {
139
+ // 追加到 frontmatter 后空行(frontmatter 后第一个空行)
140
+ const fmEndRe = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/m;
141
+ const fmMatch = out.match(fmEndRe);
142
+ if (fmMatch?.index !== undefined) {
143
+ const insertAt = fmMatch.index + fmMatch[0].length;
144
+ out = out.slice(0, insertAt) + `\n${newComment}\n` + out.slice(insertAt);
145
+ }
146
+ else {
147
+ // 无 frontmatter(不应该,但兼容)→ 追加到文件头
148
+ out = `${newComment}\n${out}`;
149
+ }
150
+ }
151
+ // 3. 多 observe 同步:若 step 的全部 observe 都 in completed → checklist 打勾
152
+ const checkedSteps = [];
153
+ const checklistLines = []; // [{lineIdx, newLine}] 延迟到循环外统一 replace
154
+ const lines = out.split("\n");
155
+ // 复用 indexSteps 但要在已更新的 out 里重新解析(completed 变了不影响 step 索引)
156
+ const freshSteps = indexSteps(out);
157
+ for (const [idx, step] of freshSteps) {
158
+ if (step.observeNames.length === 0)
159
+ continue;
160
+ const allCompleted = step.observeNames.every((n) => completed.has(n));
161
+ if (!allCompleted)
162
+ continue;
163
+ // 找该 step 对应的 checklist 行(兼容已勾选 - [x])
164
+ let stepLineIdx = -1;
165
+ let lineNum = 0;
166
+ let sIdx = 0;
167
+ for (const line of lines) {
168
+ if (line.match(/^- \[[ x]\]\s+(?:\d+\.\s+)?.+$/)) {
169
+ sIdx++;
170
+ if (sIdx === idx) {
171
+ stepLineIdx = lineNum;
172
+ break;
173
+ }
174
+ }
175
+ lineNum++;
176
+ }
177
+ if (stepLineIdx === -1)
178
+ continue;
179
+ const oldLine = lines[stepLineIdx];
180
+ if (oldLine === undefined)
181
+ continue;
182
+ const newLine = oldLine.replace(/^- \[ \]/, "- [x]");
183
+ if (newLine !== oldLine) {
184
+ lines[stepLineIdx] = newLine;
185
+ checkedSteps.push(idx);
186
+ }
187
+ }
188
+ if (checkedSteps.length > 0) {
189
+ out = lines.join("\n");
190
+ baseResult.checkedSteps = checkedSteps;
191
+ }
192
+ baseResult.content = out;
193
+ baseResult.changed = out !== content;
194
+ return baseResult;
195
+ }
@@ -2,8 +2,5 @@ import type { SourceAdapter } from "../schema.js";
2
2
  import { parseBlueprint } from "./blueprint.js";
3
3
  import { parseDomain } from "./domain.js";
4
4
  import { parseProfile } from "./profile.js";
5
- /** MD adapter:按目录位置分发到 domain/blueprint/profile adapter,组装 SchemaBundle。
6
- * v9 命名约定:适配的是 MD 文件格式(不再叫 OXN——OXN 是历史名)。
7
- * v15.x PR1(§3.1):内部构造 4 类 AssetPack → loadXxx → N 元 dedupByNameN。 */
8
5
  export declare const mdAdapter: SourceAdapter;
9
6
  export { parseBlueprint, parseDomain, parseProfile };
@@ -20,6 +20,16 @@ import { parseProfile } from "./profile.js";
20
20
  /** MD adapter:按目录位置分发到 domain/blueprint/profile adapter,组装 SchemaBundle。
21
21
  * v9 命名约定:适配的是 MD 文件格式(不再叫 OXN——OXN 是历史名)。
22
22
  * v15.x PR1(§3.1):内部构造 4 类 AssetPack → loadXxx → N 元 dedupByNameN。 */
23
+ /** v15.x §2.4.4:位置 alias 短名 → source(位置指针独立于 pack.name)。
24
+ * 用于 findActiveProfile 查位置 alias(kind="location")时按 source 查 pack,
25
+ * 与 pack.name 解耦——builtin pack 加 manifest.name="pt-builtin" 后 @pt/... 仍能寻址。
26
+ * key 是 parseRef 输出的位置 alias 短名(prj/gbl/pt),value 是 AssetPack.source。
27
+ * 与 LOC_ALIAS(source → alias)是反向表,互不重复定义。 */
28
+ const ALIAS_TO_SOURCE = new Map([
29
+ ["prj", "project"],
30
+ ["gbl", "global"],
31
+ ["pt", "builtin"],
32
+ ]);
23
33
  export const mdAdapter = {
24
34
  name: "md",
25
35
  async load(cwd, profileName, adapterCtx) {
@@ -148,8 +158,15 @@ export const mdAdapter = {
148
158
  * v15.x PR4:settings 倒序逻辑由 mdAdapter.load 构造 packs 时已处理,findActiveProfile 不变。 */
149
159
  function findActiveProfile(packs, packProfiles, profileRef) {
150
160
  if (profileRef.startsWith("@")) {
151
- const { pack, name } = parseRef(profileRef, ""); // 限定 ref 不需要 selfPack
152
- const packIdx = packs.findIndex((p) => p.name === pack);
161
+ const { kind, pack, name } = parseRef(profileRef, ""); // 限定 ref 不需要 selfPack
162
+ // v15.x §2.4.4:位置 alias(@prj/@gbl/@pt,kind="location")按 source 查(固定 3 slot 物理位置指针),
163
+ // 与 pack.name 解耦——builtin pack 加 manifest.name 后,pack.name 变化不影响 @pt/... 寻址。
164
+ // 身份 alias(kind="identity")按 pack.name 查(manifest.name 身份指针)。
165
+ const packIdx = kind === "location"
166
+ ? ALIAS_TO_SOURCE.has(pack)
167
+ ? packs.findIndex((p) => p.source === ALIAS_TO_SOURCE.get(pack))
168
+ : -1
169
+ : packs.findIndex((p) => p.name === pack);
153
170
  if (packIdx < 0) {
154
171
  throw new Error(`active profile "@${pack}/${name}" references unknown pack "${pack}"`);
155
172
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@istuen/pt",
3
- "version": "0.1.0",
4
- "description": "Pt \u2014 Turn domain knowledge into configurable Agent Context.",
3
+ "version": "0.1.2",
4
+ "description": "Pt Turn domain knowledge into configurable Agent Context.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package"
@@ -14,6 +14,7 @@ import { readFile } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
15
  import { parse as parseYaml } from "yaml";
16
16
  import { errMsg } from "../diagnostics.js";
17
+ import type { PackSource } from "../schema.js";
17
18
 
18
19
  /** manifest 解析结果。ok=false 表示文件不存在/解析失败/校验不过——调用方走默认值。 */
19
20
  export interface ParsedManifest {
@@ -55,9 +56,13 @@ const KEBAB_RE = /^[a-z0-9-]{1,64}$/;
55
56
  * - name 非 kebab-case / 保留名冲突 → warnings + name 丢弃(走 basename)
56
57
  * - version 非 semver → warnings + version 丢弃(走 "0.0.0")
57
58
  *
59
+ * v15.x builtin 特例:source="builtin" 时 manifest.name 等于保留名合法
60
+ * (位置 alias @pt = 身份 alias 合一;builtin pack 的"身份"就是"内置")。
61
+ * project/global/settings pack 仍禁用保留名(保护位置 slot)。
62
+ *
58
63
  * 永远不抛异常(§2.2 校验规则:解析失败当无 manifest 处理,不阻断加载)。
59
64
  */
60
- export async function parseManifest(rootDir: string): Promise<ParsedManifest> {
65
+ export async function parseManifest(rootDir: string, source?: PackSource): Promise<ParsedManifest> {
61
66
  const file = join(rootDir, "pt-asset-pack.yaml");
62
67
  let raw: string;
63
68
  try {
@@ -87,9 +92,16 @@ export async function parseManifest(rootDir: string): Promise<ParsedManifest> {
87
92
  if (typeof data.name === "string" && data.name.trim()) {
88
93
  const n = data.name.trim();
89
94
  if (!KEBAB_RE.test(n)) {
90
- warnings.push(`manifest.name "${n}" not kebab-case, falling back to basename`);
91
- } else if (RESERVED_NAMES.has(n)) {
92
- warnings.push(`manifest.name "${n}" is reserved, falling back to basename`);
95
+ warnings.push(
96
+ `[repair-required][name-kebab] manifest.name "${n}" not kebab-case, fallback: basename. Auto-fix: /manual:pack-management#pack-repair`
97
+ );
98
+ } else if (RESERVED_NAMES.has(n) && source !== "builtin") {
99
+ // v15.x builtin 特例:source="builtin" 时保留名(prj/gbl/pt)合法——位置 alias = 身份 alias 合一。
100
+ // 其他 source(project/global/settings)仍禁用保留名:位置 slot 是 reserved pack 的,身份 alias
101
+ // 不能占用。project pack 应该用跨项目身份名(如 pt-internal)走身份寻址,不是用保留名。
102
+ warnings.push(
103
+ `[repair-required][name-reserved] manifest.name "${n}" is reserved, fallback: basename. Auto-fix: /manual:pack-management#pack-repair`
104
+ );
93
105
  } else {
94
106
  name = n;
95
107
  }
@@ -99,7 +111,9 @@ export async function parseManifest(rootDir: string): Promise<ParsedManifest> {
99
111
  if (typeof data.version === "string" && data.version.trim()) {
100
112
  const v = data.version.trim();
101
113
  if (!SEMVER_RE.test(v)) {
102
- warnings.push(`manifest.version "${v}" not semver, treating as "0.0.0"`);
114
+ warnings.push(
115
+ `[repair-required][version-semver] manifest.version "${v}" not semver, default: "0.0.0". Auto-fix: /manual:pack-management#pack-repair`
116
+ );
103
117
  } else {
104
118
  version = v;
105
119
  }
@@ -89,15 +89,31 @@ export class MdFilePack implements AssetPack {
89
89
  source: PackSource;
90
90
  adapterCtx?: SourceAdapterContext;
91
91
  }): Promise<MdFilePack> {
92
- const manifest = await parseManifest(args.rootDir);
92
+ const manifest = await parseManifest(args.rootDir, args.source);
93
93
  const dirName = pathBasename(args.rootDir);
94
94
 
95
95
  // manifest warnings 上抛 notify(不阻断——parseManifest 已容错)
96
- if (manifest.warnings.length > 0 && args.adapterCtx?.notify) {
97
- args.adapterCtx.notify(
98
- `Pt: pack "${dirName}" manifest 警告:${manifest.warnings.join("; ")}`,
99
- "warning"
100
- );
96
+ if (args.adapterCtx?.notify) {
97
+ if (manifest.warnings.length > 0) {
98
+ // 检测 [repair-required] 前缀的 warnings——加 manual hint 引导 LLM 调 /manual:pack-management
99
+ const hasRepairRequired = manifest.warnings.some((w) => w.startsWith("[repair-required]"));
100
+ const manualHint = hasRepairRequired
101
+ ? "\n→ 调 /manual:pack-management 让 LLM 自动修复"
102
+ : "";
103
+ args.adapterCtx.notify(
104
+ `Pt: pack "${dirName}" manifest 警告:${manifest.warnings.join("; ")}${manualHint}`,
105
+ "warning"
106
+ );
107
+ } else if (!manifest.ok && args.source === "settings") {
108
+ // v15.x §2.4.2 + pack-naming:reserved pack(project/global/builtin)无 manifest 是
109
+ // back-compat 退化路径(退到位置别名 prj/gbl/pt),设计预期——silent。
110
+ // 只有 settings pack(用户主动声明)无 manifest 时才通知:basename 兜底"易碎",
111
+ // 建议加 pt-asset-pack.yaml 让 pack 成为自描述实体,/manual:pack-management#pack-create。
112
+ args.adapterCtx.notify(
113
+ `Pt: pack "${dirName}" 无 manifest(basename 兜底)— 建议添加 pt-asset-pack.yaml 让 pack 成为自描述实体。/manual:pack-management#pack-create`,
114
+ "warning"
115
+ );
116
+ }
101
117
  }
102
118
 
103
119
  // name 解析优先级(§2.4.2):
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: pack-management
3
+ ---
4
+
5
+ # pack-management
6
+
7
+ Pack 全生命周期管理 domain——创建 / 调整 / 迭代 / 迁移 / 修复。builtin profile
8
+ `guide` 引用本 domain,用户 `/manual:pack-management` 触发任意 FlowTemplate,
9
+ LLM 跟着 Scene + Rules + Flow + Checklist 走即可。
10
+
11
+ **核心立场**:manifest 是 pack 的**身份证 + 说明书**——没有 manifest 的目录只是
12
+ back-compat 兜底状态(参见 `pt-pack-location-vs-identity-alias`),不是真正的
13
+ pt pack。缺 manifest / 写错时,跟着 `pack-repair` flow 走即可自描述修复。
14
+
15
+ ## Scene
16
+
17
+ ### pack-lifecycle
18
+ - desc: Pack 五态——创建(init)→ 迭代(add/change asset)→ 迁移(schema 升级)→ 废弃(deprecate)→ 修复(manifest 缺失/错误回退到创建态)。每态对应一个 FlowTemplate。
19
+
20
+ ### pack-structure
21
+ - desc: Pt pack 标准目录结构——必须含 `domains/` + `blueprints/` + `profiles/` 三个子目录之**一**(至少一个);可选 `pt-asset-pack.yaml` manifest(**强烈建议始终提供**——manifest 是身份证,不是装饰)
22
+
23
+ ### pack-sources
24
+ - desc: v15.x Pack 有 4 类来源——project(`<cwd>/.pt/assets/`)/ settings(`.pi/settings.json` 的 `pt.asset-packs[]`,PR4 启用)/ global(`~/.pt/assets/`)/ builtin(`src/builtin/assets/`,随 npm 包发布)。每类走相同 MdFilePack 管线,差别只在入口函数和 name fallback 表。
25
+
26
+ ### pack-manifest
27
+ - desc: `pt-asset-pack.yaml` 是 pack 自描述 manifest——三个字段:`name`(kebab-case 身份 alias,跨项目寻址用)/ `version`(semver,缓存失效标识)/ `description`(人类可读说明,UI 展示用)。字段全部可选——但**强烈建议始终提供**(back-compat fallback 是过渡方案)。
28
+
29
+ ### validation-codes
30
+ - desc: validatePack 返回的错误码——`dir-not-found`(路径不存在)/ `no-asset-subdir`(无 asset 子目录)/ `load-failed`(加载抛异常)/ `manifest-warnings`(manifest 缺失/解析失败/字段校验失败,**不阻断**但会走 notify + manual hint)
31
+
32
+ ### pack-naming
33
+ - desc: 寻址双层语义——位置 alias(`@prj`/`@gbl`/`@pt`,固定 3 slot 物理位置指针,reserved pack 用)+ 身份 alias(`@<manifest-name>`,跨项目寻址用,settings pack 用)。manifest.name 走身份 alias,无 manifest 时 reserved pack 退到位置 alias,settings pack 退到 basename(**易碎**,建议始终提供 manifest)。
34
+
35
+ ### pack-reserved-vs-external
36
+ - desc: Pt Pack 体系按"是否有固定位置约定"分两类——
37
+
38
+ **① 固定位置 slot(reserved,3 个 slot,本质是位置约定)**:
39
+ - `@prj` → 项目 cwd 下的 `.pt/assets/`(项目 pack)
40
+ - `@gbl` → 用户 home 下的 `~/.pt/assets/`(用户私有通用 pack)
41
+ - `@pt` → npm 包内嵌 `src/builtin/assets/`(工具内嵌 pack)
42
+
43
+ 三者都是"外部 pack"——它们不在 pt 工具代码本身里,是用户在文件系统 / npm 包里的资产。reserved 不是因为"内置",而是因为**有固定的物理位置约定**——pt 工具预先知道去哪里找它们,不用用户声明路径。
44
+
45
+ **② 用户/外部 Pt Packs(settings,通过 manifest.name 走身份 alias)**:
46
+ - 用户主动声明在 `.pi/settings.json` 的 `pt.asset-packs[]` 中
47
+ - 身份由 manifest.name 决定(自由命名,避开保留名)
48
+ - 没有固定位置约定——可以是任意路径、任意名字
49
+ - 包含第三方 pack(团队 / 公司 / 社区发布的)
50
+
51
+ **关键区别**:① 有固定位置(物理约定)→ 工具自己找;② 有固定身份(manifest.name)→ 用户声明路径找。
52
+
53
+ ### pack-builtin-special
54
+ - desc: `@pt` 是最特殊的 reserved pack——位置 alias + 身份 alias 合一。
55
+
56
+ - builtin pack 的"身份"就是"内置",**不需要跨项目身份寻址**(位置固定 = `pt`)
57
+ - 位置 slot `@pt` 已经是其完整身份表达
58
+ - manifest.name="pt" 合法(保留名作为身份 alias)——位置 alias = 身份 alias 合一
59
+ - working set 双索引 key 重合:`@pt/foo` 的 location 和 identity entry 指向同一份 asset
60
+ - 其他 reserved pack(prj/gbl)不享受合一——项目/全局 pack 仍用跨项目身份名(`pt-internal` 等)
61
+
62
+ 设计动机:v10.x 时期 builtin pack.name="pt"(位置别名退化)是 back-compat 默认行为;本轮修复把这个行为**显式声明**为设计意图,而不是引入 `pt-builtin` 之类冗余名字。保留名规则对 builtin 特例放行(`parseManifest(rootDir, "builtin")`),project/global/settings pack 仍禁用保留名。
63
+
64
+ ## Flows
65
+
66
+ ### pack-create
67
+ - argument-hint: <pack-root>
68
+ - intent: 从零创建新 pack——创建目录结构 + 写 `pt-asset-pack.yaml` 模板(name 走 kebab-case 提示用户输入,version 默认 `0.1.0`)
69
+ - vars: [pack-root]
70
+ - step: 确认 pack-root 不存在或为空(避免覆盖现有 pack)
71
+ - step: `mkdir -p <pack-root>/{domains,blueprints,profiles}`
72
+ - step: 写 `<pack-root>/pt-asset-pack.yaml` 模板:
73
+ - step: ```yaml
74
+ name: <kebab-case-name> # 跨项目身份 alias
75
+ version: 0.1.0 # semver
76
+ description: <一句话说明>
77
+ ```
78
+ - step: 校验:name 满足 `[a-z0-9-]{1,64}` 且非保留字(prj/gbl/pt/project/global/builtin)
79
+ - step: 校验:version 满足 `^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`
80
+ - step: 若作为 settings pack 暴露:在 `.pi/settings.json` 加 `pt.asset-packs: [{ "path": "<pack-root>" }]`
81
+ - step: 验证:`/pt status` → 新 pack 显示在 Pack 健康行
82
+
83
+ ### pack-repair
84
+ - argument-hint: (无)
85
+ - intent: Manifest 缺失/错误时自描述修复——按错误码分类处置后重启 session 验证
86
+ - vars: []
87
+ - step: `/pt status` 查看 pack 健康状态 + manifest warnings(如果有 `[repair-required]` 前缀,说明需要修复)
88
+ - step: 按错误码分类处置:
89
+ - step: - manifest 缺失:走 `pack-create` flow 创建 `pt-asset-pack.yaml`(保留现有 assets 子目录)
90
+ - step: - manifest 解析失败:检查 YAML 语法(top-level 必须是 mapping)
91
+ - step: - name 非 kebab-case:改名满足 `[a-z0-9-]{1,64}`
92
+ - step: - name 是保留字:改用其他身份 alias(不能是 prj/gbl/pt/project/global/builtin)
93
+ - step: - version 非 semver:改成 `^\d+\.\d+\.\d+` 格式
94
+ - step: - dir-not-found:`mkdir -p <pack-root>/{domains,blueprints,profiles}`
95
+ - step: - no-asset-subdir:至少创建一个 asset 子目录
96
+ - step: - load-failed:检查资产文件格式(`.md` frontmatter 合法 / `.blueprint.yaml` 语法正确)
97
+ - step: 修复后重启 pi session(`projectPackDegraded` 在 session_start 重新校验时清零)
98
+ - step: `/pt status` 确认 pack 健康(✅,无 `[repair-required]` warnings)
99
+
100
+ ### pack-iterate
101
+ - argument-hint: <pack-root>
102
+ - intent: Pack 内容迭代——添加 / 修改 / 删除 asset,并按需 bump version
103
+ - vars: [pack-root]
104
+ - step: 修改 asset 文件(`domains/*.md` / `blueprints/*.blueprint.yaml` / `profiles/*.profile.md`)
105
+ - step: 资产改动后删 `.pt/cache/agent-contexts/*.agent-context.md`(强制重编译,sourceHash 自动失效)
106
+ - step: 修改 manifest.version——breaking change 升 major,向后兼容加 feature 升 minor,bug fix 升 patch
107
+ - step: 同步 description(如果 pack 用途变化)
108
+ - step: 验证:`/pt status` → pack version 已更新 → 跑 `npm run verify` 全测试通过
109
+
110
+ ### pack-migrate
111
+ - argument-hint: <pack-root>
112
+ - intent: Pt schema 升级时 pack 内容迁移——按迁移指南更新 asset 格式
113
+ - vars: [pack-root]
114
+ - step: 读迁移指南(`.pt/docs/migrations/<from>-to-<to>-*.md` 或 `/manual:pt-asset-migration`)
115
+ - step: 按指南转换每个 asset 文件(如 v9.0 → v9.1 modules-to-profile-complete:Blueprint `modules` 字段删除,迁到 Profile `### Modules`)
116
+ - step: 跑 `pt_check` LLM 工具检查 pack 健康(missing-modules / dangling-blueprint-ref / empty-segment / orphan-h2 等)
117
+ - step: 修完所有报错后 bump version(breaking change 升 major)
118
+ - step: 验证:`/pt status` 健康 + `npm run verify` 通过
119
+
120
+ ## Rules
121
+
122
+ ### manifest-required-as-identity
123
+ - check: Pack 必须有 `pt-asset-pack.yaml` manifest——没有 manifest 的目录只是 back-compat 兜底状态(reserved pack 退到 prj/gbl/pt,settings pack 退到 basename)。Manifest 是 pack 的身份证 + 说明书,缺它 pack 不是真正的 pt pack
124
+
125
+ ### manifest-name-kebab-case
126
+ - check: `manifest.name` 必须满足 `[a-z0-9-]{1,64}`——kebab-case 格式,1-64 字符。校验失败 → warning + fallback 到 basename(settings pack)或位置 alias(reserved pack)
127
+
128
+ ### manifest-name-not-reserved
129
+ - check: `manifest.name` 不能等于保留字——`prj`/`gbl`/`pt`/`project`/`global`/`builtin` 全部禁用(reserved pack 位置 alias 专用)。冲突 → warning + fallback
130
+
131
+ ### manifest-version-semver
132
+ - check: `manifest.version` 必须满足 `^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$`——semver 格式。校验失败 → warning + 默认 "0.0.0"
133
+
134
+ ### manifest-fallback-is-transitional
135
+ - check: Manifest 缺失时的 fallback 是**过渡方案**,不是设计意图——reserved pack 退到位置 alias、settings pack 退到 basename 都是 back-compat 妥协。Pack 作者应始终提供 manifest,让 pack 成为自描述实体
136
+
137
+ ### reserved-pack-position-agreement
138
+ - check: Reserved pack(`@prj` / `@gbl` / `@pt`)的"reserved"**不是"内置"**——三者都是"外部 pack"(项目 / 用户 home / npm 包里的资产),reserved 是因为有**固定的物理位置约定**(pt 工具预先知道去哪里找)。用户资产 / 第三方 pack 走 settings,通过 manifest.name 走身份 alias(见 `pack-reserved-vs-external`)
139
+
140
+ ### builtin-pack-name-merges-position-and-identity
141
+ - check: builtin pack 是 3 个 reserved slot 里最特殊的——位置 alias `@pt` = 身份 alias `@pt` 合一。`src/builtin/assets/pt-asset-pack.yaml` 的 `name: pt` 合法(保留名作为身份 alias),working set 双索引 key 重合。其他 reserved pack(prj/gbl)仍用跨项目身份名(`pt-internal` 等),不合一(见 `pack-builtin-special`)
142
+
143
+ ### validate-pack-never-throws
144
+ - check: validatePack 失败时返 `ValidationResult` 对象(`ok=false` + `errors[]`),不抛异常——保证加载链不阻断(§6.7.7)。Manifest parse 失败同样不阻断(parseManifest 已容错,warning 进 notify)
145
+
146
+ ### project-pack-degradation
147
+ - check: Project pack 失效时强制激活 builtin `guide` profile(`projectPackDegraded=true`),覆盖用户配置的 `pt.default-profile`——保证 pi 可用让用户走 `pack-repair` flow 修复(§6.7.3)
148
+
149
+ ### restart-after-repair
150
+ - check: 修复 pack 后必须重启 pi session——`projectPackDegraded` 标记在 session_start 重新校验时清零,不重启则继续降级
151
+
152
+ ### global-pack-guide-non-interactive
153
+ - check: 全局 Pack 初始化引导仅在 TTY + 非 CI + 无 `PT_NO_GUIDE` 环境触发(§7.5.1)——避免阻塞 CI / 后台进程
154
+
155
+ ## Checklists
156
+
157
+ ### pack-create-checklist
158
+ - step: pack-root 路径合法(绝对路径或相对 cwd)
159
+ - step: pack-root 不存在或为空(避免覆盖)
160
+ - step: 至少一个 asset 子目录存在(domains/blueprints/profiles)
161
+ - step: `pt-asset-pack.yaml` 存在且字段合法(kebab-case name / semver version / description 可选)
162
+ - step: 若作为 settings pack 暴露,`.pi/settings.json` 的 `pt.asset-packs[]` 已声明
163
+ - step: `/pt status` 显示新 pack 健康(✅)
164
+
165
+ ### pack-repair-checklist
166
+ - step: `/pt status` 列出所有 `[repair-required]` warnings
167
+ - step: 每个 warning 对应 pack-repair flow 的一个 step(manifest 缺失 / 解析失败 / name 校验 / version 校验 / dir-not-found / no-asset-subdir / load-failed)
168
+ - step: 修复后 `/pt status` 无 `[repair-required]` warnings
169
+ - step: 重启 pi session 后 `projectPackDegraded` 标记清零
170
+ - step: `npm run verify` 通过(除 phase9 fixture 错位独立 bug)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: guide
3
3
  blueprint: dev-knowledge
4
- domains: [user-info, agent-info, project-analysis, authoring, usage, pack-repair]
4
+ domains: [user-info, agent-info, project-analysis, authoring, usage, pack-management]
5
5
  ---
6
6
 
7
7
  # guide (profile)
@@ -0,0 +1,22 @@
1
+ # pt-asset-pack.yaml — Pt builtin 资产(随 npm 包发布)
2
+ #
3
+ # 加载位置:src/builtin/assets/(builtin pack 根目录)
4
+ # 加载方式:builtin pack(reserved)→ pack name 走 manifest.name="pt"。
5
+ # 位置 alias @pt + 身份 alias @pt 合一(同一索引条目覆盖同一 key)。
6
+ #
7
+ # 设计意图(§2.4.2 + builtin 特例):
8
+ # - builtin pack 的"身份"就是"内置",位置 slot @pt 已是其完整身份表达
9
+ # - 不需要额外身份别名(如 pt-builtin)——跨项目身份寻址对 npm 内嵌 pack 无意义
10
+ # - 与 back-compat 默认行为一致(无 manifest 时 pack.name=位置别名)
11
+ # - 保留名规则对 builtin 特例放行(manifest.ts:name-validation):builtin 允许用
12
+ # "prj/gbl/pt" 作 manifest.name;project/global/settings pack 仍禁用保留名
13
+ #
14
+ # 加 manifest 的好处(§2.4.2):
15
+ # - /pt status 显示真实 version/description(不再永远 v0.0.0 / undefined)
16
+ # - 跨 builtin 的 manifest 校验走同一条 parseManifest 路径,无双轨
17
+ # - 与 project pack 的 manifest 校验语义对称(同样:reserved pack 读 manifest,
18
+ # 身份 alias 优先;只是 builtin 的"身份"恰好等于位置别名)
19
+
20
+ name: pt
21
+ version: 0.0.0
22
+ description: Pt builtin assets(随 npm 包发布,跨项目复用——guide profile + usage/authoring 等参考文档)
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
24
24
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
25
25
  import { Type } from "typebox";
26
- import { mkdir, writeFile } from "node:fs/promises";
26
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
27
27
  import { existsSync } from "node:fs";
28
28
  import { join } from "node:path";
29
29
  import { randomUUID } from "node:crypto";
@@ -73,6 +73,7 @@ import {
73
73
  refreshManualWidget,
74
74
  tryRestoreManual,
75
75
  } from "./manual-session.js";
76
+ import { writeProbeResult } from "./manual-writeback.js";
76
77
  import { slog } from "./slog.js";
77
78
 
78
79
  type AgentUIContext = NonNullable<AgentAPI["ui"]>;
@@ -943,15 +944,39 @@ export default function (pi: ExtensionAPI): void {
943
944
  : `? ${result.message}`;
944
945
  // v11.x:verify 后重读文件刷新 widget(用户可能手动 tick 了 checklist)
945
946
  // 不改 session.activeManual,只 refresh 派生数据(widget + cachedManualProgress)
947
+ // v15.x(issue pt-verify-result-not-written-back-to-manual):自动写回 manual 文件
948
+ // - 按 probe 名匹配 step 的 observe 列表,定位 ## 执行状态 对应行
949
+ // - 写 outcome + message + 维护 frontmatter 注释的 completed probes 列表
950
+ // - step 全部 observe 都 completed → checklist - [ ] → - [x]
951
+ // - withFileMutationQueue 保证并发安全
952
+ // - 写回失败不阻断 verify 结果返回(异常 catch 走 text 末尾提示)
946
953
  const sessionId = getSessionIdFromCtx(ctx);
947
954
  const s = sessionId ? getSessionById(sessionId) : null;
955
+ let writebackNote = "";
948
956
  if (s?.activeManual) {
957
+ const filePath = s.activeManual.filePath;
958
+ try {
959
+ await withFileMutationQueue(filePath, async () => {
960
+ const before = await readFile(filePath, "utf8");
961
+ const wb = writeProbeResult(before, params.probe, result.outcome, result.message);
962
+ if (wb.changed) {
963
+ await writeFile(filePath, wb.content, "utf8");
964
+ const checked =
965
+ wb.checkedSteps.length > 0 ? `,勾选 ${wb.checkedSteps.length} 个 checklist` : "";
966
+ writebackNote = `\n↳ 已写回 manual step ${wb.stepIndexes.join(", ")}${checked}`;
967
+ } else if (wb.matchCount === 0) {
968
+ writebackNote = `\n? probe "${params.probe}" 未匹配 activeManual 任何 step 的 observe(${filePath})`;
969
+ }
970
+ });
971
+ } catch (e) {
972
+ writebackNote = `\n! 写回 manual 失败: ${errMsg(e)}`;
973
+ }
949
974
  await refreshManualWidget(ctx.ui, s);
950
975
  refreshInjectionFooter(ctx.ui, s);
951
976
  }
952
977
  return {
953
- content: [{ type: "text", text }],
954
- details: result,
978
+ content: [{ type: "text", text: text + writebackNote }],
979
+ details: { ...result, writeback: writebackNote || undefined },
955
980
  };
956
981
  },
957
982
  });