@peterxiaoyang/superspec 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,6 +82,8 @@ superspec init --scope project
82
82
 
83
83
  这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
84
84
 
85
+ 初始化还会安装托管的 `.codex/hooks.json`。它会让 Codex 在写文件前、工具执行后、子智能体启动和停止时调用 SuperSpec,做检查和记录。
86
+
85
87
  Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
86
88
 
87
89
  ```powershell
@@ -157,6 +159,18 @@ openspec/changes/<变更ID>/.superspec/
157
159
  `.superspec/` 要不要提交到 git,由你的团队决定。
158
160
  如果不提交,删掉后就没有 git 历史可以恢复。
159
161
 
162
+ ## Hook 会做什么
163
+
164
+ SuperSpec 安装的 hook 会在几个关键时机运行:
165
+
166
+ - 写文件前:检查是否会改到 SuperSpec 的过程记录、提前归档、绕过任务检查,或写到当前任务不该写的地方
167
+ - 工具执行后:如果刚跑的是测试或验证命令,就记录这次结果
168
+ - 子智能体启动和停止时:记录这次子智能体运行的基本信息
169
+
170
+ 这些 hook 的默认超时时间是 `120` 秒。这个时间限制的是 hook 自己的检查过程,不限制 `npm test`、构建命令或子智能体本身能运行多久。
171
+
172
+ hook 不是安全沙箱。它能减少误操作、拦住一部分明显会破坏流程记录的写入,并留下审计线索;但不能保证阻止所有绕过,也不能把记录变成不可伪造的安全证明。
173
+
160
174
  ## 重要边界
161
175
 
162
176
  SuperSpec 能让流程更规范,但它不是安全锁。
@@ -175,7 +189,7 @@ SuperSpec 能让流程更规范,但它不是安全锁。
175
189
  - 阻止恶意伪造记录
176
190
  - 替代正式的安全审计、合规审计或法律证明
177
191
 
178
- 也就是说,SuperSpec v1 是“流程纪律工具”,不是“强制安全系统”。
192
+ 也就是说,SuperSpec 目前是“流程纪律 + 审计辅助工具”,不是“强制安全系统”。hook 会增强可见性和一部分写入检查,但它仍然不能替代正式的安全控制。
179
193
 
180
194
  ## 常用命令
181
195
 
@@ -223,6 +237,7 @@ superspec doctor
223
237
 
224
238
  ```text
225
239
  .codex/
240
+ hooks.json
226
241
  skills/superspec-explore/
227
242
  skills/superspec-propose/
228
243
  skills/superspec-apply/
@@ -7,26 +7,26 @@ const EXPECTED_HOOK_MATRIX = [
7
7
  {
8
8
  eventName: "PreToolUse",
9
9
  matcher: "Bash|apply_patch|Edit|Write|mcp__.*",
10
- timeout: 10,
11
- statusMessage: "SuperSpec hook write policy",
10
+ timeout: 120,
11
+ statusMessage: "SuperSpec 写入策略检查",
12
12
  },
13
13
  {
14
14
  eventName: "PostToolUse",
15
15
  matcher: "Bash",
16
- timeout: 30,
17
- statusMessage: "SuperSpec hook runtime evidence",
16
+ timeout: 120,
17
+ statusMessage: "SuperSpec 运行证据记录",
18
18
  },
19
19
  {
20
20
  eventName: "SubagentStart",
21
21
  matcher: ".*",
22
- timeout: 30,
23
- statusMessage: "SuperSpec hook subagent start",
22
+ timeout: 120,
23
+ statusMessage: "SuperSpec 子智能体启动记录",
24
24
  },
25
25
  {
26
26
  eventName: "SubagentStop",
27
27
  matcher: ".*",
28
- timeout: 30,
29
- statusMessage: "SuperSpec hook subagent stop",
28
+ timeout: 120,
29
+ statusMessage: "SuperSpec 子智能体停止记录",
30
30
  },
31
31
  ];
32
32
  function isRecord(value) {
@@ -4,12 +4,36 @@ import { install_workflow, uninstall_workflow, update_workflow } from "./install
4
4
  import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
- import { system_failure_zh } from "./i18n.js";
7
+ import { action_detail_zh, action_status_zh, reason_message_zh, system_failure_zh } from "./i18n.js";
8
8
  function usage() {
9
9
  return "usage: superspec init [-h] [--scope {project,user}] [--path PATH] [--codex-home PATH] [--format {json,agent,user}] [--create] [--update] [--uninstall] [--dry-run] [--force]\n";
10
10
  }
11
11
  function help() {
12
- return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME 或 ~/.codex)\n --format {json,agent,user} 输出格式;json 用于诊断,agent/user 用于安全展示\n --user-facing 等价于 --format user\n --create 兼容参数;init 默认就会创建缺失内容\n --update 按 manifest 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall 按 manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
12
+ return `${usage()}\n可选参数:\n -h, --help 显示帮助并退出\n --scope {project,user} 安装到当前项目的 .codex 目录,或安装到用户级 Codex 目录(默认:project)\n --project 等价于 --scope project\n --user 等价于 --scope user\n --global 兼容别名,等价于 --user\n --path PATH --scope project 时使用的项目根目录(默认:当前目录)\n --codex-home PATH --scope user 时使用的 Codex 用户目录(默认:$CODEX_HOME 或 ~/.codex)\n --format {json,agent,user} 输出格式(默认:user);json 用于诊断或自动化,agent 用于工作流消费\n --user-facing 等价于 --format user\n --create 兼容参数;init 默认就会创建缺失内容\n --update 按 manifest 更新 SuperSpec 管理的文件;用户改动文件保留,新的版本写入 *.new\n --uninstall 按 manifest 卸载 SuperSpec 管理的文件;.superspec 数据与既有/用户改动文件会保留\n --dry-run 配合 --uninstall 时只预览将删除的文件,不实际修改\n --force 安装时覆盖已有且内容不同的文件,并保留 *.bak 备份\n`;
13
+ }
14
+ function requestedOutputFormat(argv) {
15
+ if (argv.includes("--user-facing"))
16
+ return "user";
17
+ let selected = null;
18
+ for (let idx = 0; idx < argv.length; idx += 1) {
19
+ if (argv[idx] !== "--format")
20
+ continue;
21
+ const value = argv[idx + 1];
22
+ if (value === undefined || value.startsWith("--"))
23
+ return null;
24
+ selected = value;
25
+ }
26
+ if (selected === null)
27
+ return null;
28
+ try {
29
+ return parseDecisionOutputFormat(selected);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function fallbackOutputFormat(argv) {
36
+ return requestedOutputFormat(argv) ?? "user";
13
37
  }
14
38
  function parse_init_argv(argv) {
15
39
  const getValues = (flag) => {
@@ -45,7 +69,7 @@ function parse_init_argv(argv) {
45
69
  parseDecisionOutputFormat(value);
46
70
  const selectedFormat = argv.includes("--user-facing")
47
71
  ? "user"
48
- : (formatValues.length > 0 ? formatValues[formatValues.length - 1] : "json");
72
+ : (formatValues.length > 0 ? formatValues[formatValues.length - 1] : "user");
49
73
  const format = parseDecisionOutputFormat(selectedFormat);
50
74
  return {
51
75
  path: resolve(getValue("--path") ?? process.cwd()),
@@ -60,6 +84,118 @@ function parse_init_argv(argv) {
60
84
  function joinHomeCodex() {
61
85
  return `${homedir()}/.codex`;
62
86
  }
87
+ function scopeLabel(scope) {
88
+ return scope === "user" ? "用户级" : "项目级";
89
+ }
90
+ function modeLabel(mode, dryRun) {
91
+ if (mode === "update")
92
+ return "更新";
93
+ if (mode === "uninstall")
94
+ return dryRun ? "卸载预览" : "卸载";
95
+ return "初始化";
96
+ }
97
+ function actionSummary(actions) {
98
+ if (!Array.isArray(actions) || actions.length === 0)
99
+ return "";
100
+ const counts = new Map();
101
+ for (const item of actions) {
102
+ if (!item || typeof item !== "object")
103
+ continue;
104
+ const status = String(item.status ?? "");
105
+ if (!status)
106
+ continue;
107
+ counts.set(status, (counts.get(status) ?? 0) + 1);
108
+ }
109
+ const changedOrder = ["created", "updated", "removed", "would_remove", "skipped", "failed"];
110
+ const parts = changedOrder
111
+ .map((status) => [status, counts.get(status) ?? 0])
112
+ .filter(([, count]) => count > 0)
113
+ .map(([status, count]) => `${action_status_zh(status)} ${count} 项`);
114
+ if (parts.length > 0)
115
+ return parts.join(",");
116
+ const okCount = counts.get("ok") ?? 0;
117
+ return okCount > 0 ? `已检查 ${okCount} 项,无需改动` : "";
118
+ }
119
+ function actionTarget(action) {
120
+ const match = action.match(/^(?:install|update|uninstall(?: rmdir)?)\s+(.+)$/u);
121
+ return match?.[1] ?? "";
122
+ }
123
+ function skippedActionNotes(actions) {
124
+ if (!Array.isArray(actions))
125
+ return [];
126
+ const groups = new Map();
127
+ for (const item of actions) {
128
+ if (!item || typeof item !== "object")
129
+ continue;
130
+ const action = String(item.action ?? "");
131
+ const status = String(item.status ?? "");
132
+ if (status !== "skipped")
133
+ continue;
134
+ const detail = String(item.detail ?? "");
135
+ const note = detail ? action_detail_zh(detail) : "有文件被保留未改动;需要完整列表时添加 --format json。";
136
+ const target = actionTarget(action);
137
+ const group = groups.get(note) ?? { count: 0, targets: [] };
138
+ group.count += 1;
139
+ if (target && group.targets.length < 3)
140
+ group.targets.push(target);
141
+ groups.set(note, group);
142
+ }
143
+ return [...groups.entries()].map(([note, group]) => {
144
+ const hiddenCount = group.count - group.targets.length;
145
+ if (group.targets.length === 0)
146
+ return `${note}(${group.count} 项)`;
147
+ const suffix = hiddenCount > 0 ? ` 等 ${group.count} 项` : "";
148
+ return `${note}(${group.targets.join(",")}${suffix})`;
149
+ });
150
+ }
151
+ function knownInitProblemMessageZh(message) {
152
+ if (/install manifest missing; run superspec init before --update/iu.test(message)) {
153
+ return "缺少安装清单;请先运行 superspec init,再执行 update。";
154
+ }
155
+ if (/install manifest missing; nothing to uninstall/iu.test(message)) {
156
+ return "缺少安装清单;当前没有可卸载的 SuperSpec 管理文件。";
157
+ }
158
+ return null;
159
+ }
160
+ function reasonTextZh(item) {
161
+ const code = String(item.code ?? "");
162
+ const message = String(item.message ?? "");
163
+ const refs = Array.isArray(item.refs) ? item.refs.map((ref) => String(ref)) : [];
164
+ return knownInitProblemMessageZh(message) ?? reason_message_zh(code, message, refs);
165
+ }
166
+ function renderInitUserDecision(decision, opts = {}) {
167
+ const lines = [];
168
+ const op = modeLabel(opts.mode, opts.dryRun);
169
+ lines.push(`SuperSpec ${op}${decision.allowed ? "完成" : "未完成"}。`);
170
+ lines.push(`范围:${scopeLabel(opts.scope)}。`);
171
+ const root = opts.targetRoot ?? String(decision.install_root ?? decision.project_root ?? "");
172
+ if (root)
173
+ lines.push(`位置:${root}`);
174
+ const summary = actionSummary(decision.actions);
175
+ if (decision.allowed && summary)
176
+ lines.push(`处理:${summary}。`);
177
+ const skippedNotes = skippedActionNotes(decision.actions);
178
+ if (decision.allowed && skippedNotes.length > 0) {
179
+ lines.push("跳过项:");
180
+ for (const note of skippedNotes)
181
+ lines.push(`- ${note}`);
182
+ }
183
+ const reasons = Array.isArray(decision.block_reasons) ? decision.block_reasons : [];
184
+ if (!decision.allowed && reasons.length > 0) {
185
+ lines.push("原因:");
186
+ for (const item of reasons)
187
+ lines.push(`- ${reasonTextZh(item)}`);
188
+ lines.push("修正后重新运行同一命令;需要完整诊断时添加 --format json。");
189
+ }
190
+ return `${lines.join("\n")}\n`;
191
+ }
192
+ function printInitDecision(decision, opts) {
193
+ if (opts.format === "user") {
194
+ process.stdout.write(renderInitUserDecision(decision, opts));
195
+ return;
196
+ }
197
+ printDecision(decision, { command: opts.command ?? "init", format: opts.format });
198
+ }
63
199
  function commandFailure(proc) {
64
200
  const output = (proc.error?.message ?? (proc.stderr || proc.stdout)).trim();
65
201
  if (output)
@@ -156,12 +292,19 @@ function openspecPreflightBlocked(args, scope, installResult) {
156
292
  const decision = block("project", "openspec_preflight", [reason(code, message)], {
157
293
  next_actions: [`install or upgrade @fission-ai/openspec >= ${REQUIRED_OPENSPEC_MIN_VERSION}, then rerun \`superspec init --scope ${scope}\``],
158
294
  });
159
- printDecision({
295
+ printInitDecision({
160
296
  ...decision,
161
297
  project_root: args.path,
162
298
  install_scope: scope,
163
299
  install_root: targetRoot,
164
- }, { command: "init", format: args.format });
300
+ }, {
301
+ command: "init",
302
+ format: args.format,
303
+ mode: args.mode,
304
+ scope,
305
+ targetRoot,
306
+ dryRun: args.dryRun,
307
+ });
165
308
  return 1;
166
309
  }
167
310
  function run_init(args, scope) {
@@ -188,7 +331,14 @@ function run_init(args, scope) {
188
331
  }
189
332
  summary.install_scope = scope;
190
333
  summary.install_root = targetRoot;
191
- printDecision(summary, { command: "init", format: args.format });
334
+ printInitDecision(summary, {
335
+ command: "init",
336
+ format: args.format,
337
+ mode: args.mode,
338
+ scope,
339
+ targetRoot,
340
+ dryRun: args.dryRun,
341
+ });
192
342
  return summary.allowed ? 0 : 1;
193
343
  }
194
344
  export function main_init(argv = process.argv.slice(2)) {
@@ -208,7 +358,14 @@ export function main_init(argv = process.argv.slice(2)) {
208
358
  catch (err) {
209
359
  const change = "project";
210
360
  const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
211
- printDecision(block(change, "guard_error", [errReason]), { command: "init", format: args?.format });
361
+ printInitDecision(block(change, "guard_error", [errReason]), {
362
+ command: "init",
363
+ format: args?.format ?? fallbackOutputFormat(argv),
364
+ mode: args?.mode,
365
+ scope: args?.scope ?? "project",
366
+ targetRoot: args?.path,
367
+ dryRun: args?.dryRun,
368
+ });
212
369
  return 2;
213
370
  }
214
371
  }
@@ -229,7 +386,14 @@ export async function main_init_async(argv = process.argv.slice(2)) {
229
386
  catch (err) {
230
387
  const change = "project";
231
388
  const errReason = err instanceof GuardError ? reason("guard_error", err.message) : reason("guard_internal_error", `${err.name}: ${err.message}`);
232
- printDecision(block(change, "guard_error", [errReason]), { command: "init", format: args?.format });
389
+ printInitDecision(block(change, "guard_error", [errReason]), {
390
+ command: "init",
391
+ format: args?.format ?? fallbackOutputFormat(argv),
392
+ mode: args?.mode,
393
+ scope: args?.scope ?? "project",
394
+ targetRoot: args?.path,
395
+ dryRun: args?.dryRun,
396
+ });
233
397
  return 2;
234
398
  }
235
399
  }
package/dist/superspec.js CHANGED
@@ -30,7 +30,7 @@ function help() {
30
30
  " version print SuperSpec CLI version",
31
31
  "",
32
32
  "examples:",
33
- " superspec init --scope project --format agent",
33
+ " superspec init --scope project",
34
34
  " superspec init --scope user",
35
35
  " superspec guard check-init --change <change> --format agent",
36
36
  " superspec doctor",
@@ -49,7 +49,7 @@ function updateHelp() {
49
49
  " --user, --global equivalent to --scope user",
50
50
  " --path PATH project root for project scope (default: current directory)",
51
51
  " --codex-home PATH Codex user home for user scope (default: $CODEX_HOME or ~/.codex)",
52
- " --format {json,agent,user} output format; use agent for workflow consumption",
52
+ " --format {json,agent,user} output format (default: user); use json for diagnostics/automation",
53
53
  " --local-only skip npm self-update and use the currently installed package",
54
54
  " -h, --help show this help",
55
55
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,8 +12,8 @@
12
12
  {
13
13
  "type": "command",
14
14
  "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
15
- "timeout": 10,
16
- "statusMessage": "SuperSpec hook write policy"
15
+ "timeout": 120,
16
+ "statusMessage": "SuperSpec 写入策略检查"
17
17
  }
18
18
  ]
19
19
  }
@@ -25,8 +25,8 @@
25
25
  {
26
26
  "type": "command",
27
27
  "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
28
- "timeout": 30,
29
- "statusMessage": "SuperSpec hook runtime evidence"
28
+ "timeout": 120,
29
+ "statusMessage": "SuperSpec 运行证据记录"
30
30
  }
31
31
  ]
32
32
  }
@@ -38,8 +38,8 @@
38
38
  {
39
39
  "type": "command",
40
40
  "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
41
- "timeout": 30,
42
- "statusMessage": "SuperSpec hook subagent start"
41
+ "timeout": 120,
42
+ "statusMessage": "SuperSpec 子智能体启动记录"
43
43
  }
44
44
  ]
45
45
  }
@@ -51,8 +51,8 @@
51
51
  {
52
52
  "type": "command",
53
53
  "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
54
- "timeout": 30,
55
- "statusMessage": "SuperSpec hook subagent stop"
54
+ "timeout": 120,
55
+ "statusMessage": "SuperSpec 子智能体停止记录"
56
56
  }
57
57
  ]
58
58
  }