@peterxiaoyang/superspec 0.1.10 → 0.1.11

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,7 +82,7 @@ superspec init --scope project
82
82
 
83
83
  这条命令的意思是:把 SuperSpec 当前可用的工作流入口安装到项目里。
84
84
 
85
- 初始化还会安装托管的 `.codex/hooks.json`。它会让 Codex 在写文件前、工具执行后、子智能体启动和停止时调用 SuperSpec,做检查和记录。
85
+ 初始化还会安装托管的 `.codex/hooks.json`。默认 manifest 只在子智能体启动和停止时调用 SuperSpec,记录 best-effort 审计信息;它不会拦截普通文件写入,也不会在测试命令后自动记录结果。
86
86
 
87
87
  Windows PowerShell 如果拦截 npm 的 `.ps1` 脚本,请改用:
88
88
 
@@ -161,15 +161,13 @@ openspec/changes/<变更ID>/.superspec/
161
161
 
162
162
  ## Hook 会做什么
163
163
 
164
- SuperSpec 安装的 hook 会在几个关键时机运行:
164
+ SuperSpec 默认安装的 hook 只在子智能体启动和停止时运行:
165
165
 
166
- - 写文件前:检查是否会改到 SuperSpec 的过程记录、提前归档、绕过任务检查,或写到当前任务不该写的地方
167
- - 工具执行后:如果刚跑的是测试或验证命令,就记录这次结果
168
166
  - 子智能体启动和停止时:记录这次子智能体运行的基本信息
169
167
 
170
168
  这些 hook 的默认超时时间是 `120` 秒。这个时间限制的是 hook 自己的检查过程,不限制 `npm test`、构建命令或子智能体本身能运行多久。
171
169
 
172
- hook 不是安全沙箱。它能减少误操作、拦住一部分明显会破坏流程记录的写入,并留下审计线索;但不能保证阻止所有绕过,也不能把记录变成不可伪造的安全证明。
170
+ hook 不是安全沙箱。默认 hook 只留下子智能体审计线索;它不会机械阻止写入,也不能把记录变成不可伪造的安全证明。
173
171
 
174
172
  ## 重要边界
175
173
 
@@ -189,7 +187,7 @@ SuperSpec 能让流程更规范,但它不是安全锁。
189
187
  - 阻止恶意伪造记录
190
188
  - 替代正式的安全审计、合规审计或法律证明
191
189
 
192
- 也就是说,SuperSpec 目前是“流程纪律 + 审计辅助工具”,不是“强制安全系统”。hook 会增强可见性和一部分写入检查,但它仍然不能替代正式的安全控制。
190
+ 也就是说,SuperSpec 目前是“流程纪律 + 审计辅助工具”,不是“强制安全系统”。默认 hook 只增强子智能体活动的可见性;显式/manual `PreToolUse` 才会进入保守写入策略,但仍然不能替代正式的安全控制。
193
191
 
194
192
  ## 常用命令
195
193
 
@@ -224,11 +224,32 @@ function postToolUseOutput(decision) {
224
224
  },
225
225
  };
226
226
  }
227
+ function isSubagentHookEvent(eventName) {
228
+ return eventName === "SubagentStart" || eventName === "SubagentStop";
229
+ }
227
230
  function subagentOutput(decision, eventName) {
231
+ if (!decision.allowed) {
232
+ return {
233
+ systemMessage: `SuperSpec ${eventName} runlog skipped; ${decision.block_reasons.map((item) => item.message).join("; ") || "audit telemetry unavailable"}`,
234
+ hookSpecificOutput: {
235
+ hookEventName: eventName,
236
+ additionalContext: "SuperSpec subagent telemetry is best-effort and did not block the workflow.",
237
+ },
238
+ };
239
+ }
228
240
  return {
229
241
  systemMessage: `SuperSpec ${eventName} runlog recorded as ${decision.trust}; strict_profile=${decision.strict_profile}`,
230
242
  };
231
243
  }
244
+ function inertSubagentOutput(eventName, message) {
245
+ return {
246
+ systemMessage: message,
247
+ hookSpecificOutput: {
248
+ hookEventName: eventName,
249
+ additionalContext: "SuperSpec subagent telemetry is best-effort and did not block the workflow.",
250
+ },
251
+ };
252
+ }
232
253
  function writeTempEvent(event) {
233
254
  const path = join(tmpdir(), `superspec-hook-event-${randomUUID()}.json`);
234
255
  writeFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
@@ -241,8 +262,9 @@ function validateHookEvent(event) {
241
262
  if (!["PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop"].includes(eventName)) {
242
263
  return `unsupported hook_event_name: ${eventName}`;
243
264
  }
244
- if (typeof event.cwd !== "string" || !event.cwd)
265
+ if ((eventName === "PreToolUse" || eventName === "PostToolUse") && (typeof event.cwd !== "string" || !event.cwd)) {
245
266
  return "hook event missing required cwd";
267
+ }
246
268
  if (eventName === "PreToolUse") {
247
269
  if (typeof event.tool_name !== "string" || !event.tool_name)
248
270
  return "PreToolUse hook event missing required tool_name";
@@ -254,6 +276,7 @@ function validateHookEvent(event) {
254
276
  export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSync(0, "utf8")) {
255
277
  const parsedArgs = parseArgs(argv);
256
278
  let event;
279
+ let eventNameForFailure = "";
257
280
  try {
258
281
  if (!stdin.trim())
259
282
  throw new Error("hook stdin is empty");
@@ -261,11 +284,16 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
261
284
  if (!isObject(parsed))
262
285
  throw new Error("hook stdin must be a JSON object");
263
286
  event = parsed;
287
+ eventNameForFailure = typeof event.hook_event_name === "string" ? event.hook_event_name : "";
264
288
  const validationError = validateHookEvent(event);
265
289
  if (validationError)
266
290
  throw new Error(validationError);
267
291
  }
268
292
  catch (err) {
293
+ if (isSubagentHookEvent(eventNameForFailure)) {
294
+ const payload = inertSubagentOutput(eventNameForFailure, `SuperSpec ${eventNameForFailure} runlog skipped: ${err.message}`);
295
+ return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
296
+ }
269
297
  return { code: 2, stdout: "", stderr: `SuperSpec hook event parse failed: ${err.message}\n` };
270
298
  }
271
299
  const inference = parsedArgs.change ? { state: "unique", change: parsedArgs.change } : inferChangeFromActiveSessions(event);
@@ -274,8 +302,9 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
274
302
  const payload = noChangeFallback(event, inference);
275
303
  return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
276
304
  }
277
- const eventRef = writeTempEvent(event);
305
+ let eventRef = null;
278
306
  try {
307
+ eventRef = writeTempEvent(event);
279
308
  const eventName = String(event.hook_event_name ?? "");
280
309
  let payload;
281
310
  if (eventName === "PreToolUse") {
@@ -298,14 +327,21 @@ export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSyn
298
327
  return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
299
328
  }
300
329
  catch (err) {
330
+ if (isSubagentHookEvent(String(event.hook_event_name ?? ""))) {
331
+ const eventName = String(event.hook_event_name ?? "");
332
+ const payload = inertSubagentOutput(eventName, `SuperSpec ${eventName} runlog skipped: ${err.message}`);
333
+ return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
334
+ }
301
335
  return { code: 2, stdout: "", stderr: `SuperSpec hook failed closed: ${err.message}\n` };
302
336
  }
303
337
  finally {
304
- try {
305
- unlinkSync(resolve(eventRef));
306
- }
307
- catch {
308
- // best effort temp cleanup
338
+ if (eventRef) {
339
+ try {
340
+ unlinkSync(resolve(eventRef));
341
+ }
342
+ catch {
343
+ // best effort temp cleanup
344
+ }
309
345
  }
310
346
  }
311
347
  }
@@ -4,18 +4,6 @@ import { reason, sha256_file } from "../util.js";
4
4
  import { HOOK_ADAPTER_VERSION } from "./types.js";
5
5
  const HOOK_COMMAND = 'superspec-hook --change "$SUPERSPEC_CHANGE"';
6
6
  const EXPECTED_HOOK_MATRIX = [
7
- {
8
- eventName: "PreToolUse",
9
- matcher: "Bash|apply_patch|Edit|Write|mcp__.*",
10
- timeout: 120,
11
- statusMessage: "SuperSpec 写入策略检查",
12
- },
13
- {
14
- eventName: "PostToolUse",
15
- matcher: "Bash",
16
- timeout: 120,
17
- statusMessage: "SuperSpec 运行证据记录",
18
- },
19
7
  {
20
8
  eventName: "SubagentStart",
21
9
  matcher: ".*",
@@ -29,6 +29,13 @@ const CODEX_CONFIG_ENTRIES = [
29
29
  { table: "agents", key: "max_threads", value: String(CODEX_NATIVE_AGENT_MAX_THREADS) },
30
30
  { table: "agents", key: "max_depth", value: String(CODEX_NATIVE_AGENT_MAX_DEPTH) },
31
31
  ];
32
+ const HOOK_COMMAND = 'superspec-hook --change "$SUPERSPEC_CHANGE"';
33
+ const LEGACY_FOUR_HOOK_MATRIX = [
34
+ { eventName: "PreToolUse", matcher: "Bash|apply_patch|Edit|Write|mcp__.*", statusMessage: "SuperSpec 写入策略检查" },
35
+ { eventName: "PostToolUse", matcher: "Bash", statusMessage: "SuperSpec 运行证据记录" },
36
+ { eventName: "SubagentStart", matcher: ".*", statusMessage: "SuperSpec 子智能体启动记录" },
37
+ { eventName: "SubagentStop", matcher: ".*", statusMessage: "SuperSpec 子智能体停止记录" },
38
+ ];
32
39
  function package_version(packageRoot) {
33
40
  try {
34
41
  const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
@@ -38,6 +45,51 @@ function package_version(packageRoot) {
38
45
  return "0.0.0";
39
46
  }
40
47
  }
48
+ function has_exact_keys(value, keys) {
49
+ const actual = Object.keys(value).sort();
50
+ const expected = [...keys].sort();
51
+ return actual.length === expected.length && actual.every((key, idx) => key === expected[idx]);
52
+ }
53
+ function legacy_hook_entry_matches(entry, expected) {
54
+ if (!isObject(entry) || !has_exact_keys(entry, ["matcher", "hooks"]))
55
+ return false;
56
+ if (entry.matcher !== expected.matcher || !Array.isArray(entry.hooks) || entry.hooks.length !== 1)
57
+ return false;
58
+ const hook = entry.hooks[0];
59
+ return isObject(hook)
60
+ && has_exact_keys(hook, ["type", "command", "timeout", "statusMessage"])
61
+ && hook.type === "command"
62
+ && hook.command === HOOK_COMMAND
63
+ && hook.timeout === 120
64
+ && hook.statusMessage === expected.statusMessage;
65
+ }
66
+ function is_legacy_managed_four_hook_manifest(parsed) {
67
+ if (!isObject(parsed) || !has_exact_keys(parsed, ["superspec", "hooks"]))
68
+ return false;
69
+ if (!isObject(parsed.superspec) || !has_exact_keys(parsed.superspec, ["managed", "adapter_version", "strict_profile_default"]))
70
+ return false;
71
+ if (parsed.superspec.managed !== true
72
+ || parsed.superspec.adapter_version !== "superspec-hook@2"
73
+ || parsed.superspec.strict_profile_default !== "audit-only-until-r1-provenance-passes") {
74
+ return false;
75
+ }
76
+ if (!isObject(parsed.hooks) || !has_exact_keys(parsed.hooks, LEGACY_FOUR_HOOK_MATRIX.map((entry) => entry.eventName)))
77
+ return false;
78
+ return LEGACY_FOUR_HOOK_MATRIX.every((expected) => {
79
+ const entries = parsed.hooks[expected.eventName];
80
+ return Array.isArray(entries)
81
+ && entries.length === 1
82
+ && legacy_hook_entry_matches(entries[0], expected);
83
+ });
84
+ }
85
+ function target_is_legacy_managed_four_hook_manifest(targetAbs) {
86
+ try {
87
+ return is_legacy_managed_four_hook_manifest(JSON.parse(readFileSync(targetAbs, "utf8")));
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
41
93
  export function load_install_map(packageRoot = PACKAGE_ROOT) {
42
94
  const mapPath = join(packageRoot, INSTALL_MAP_REL);
43
95
  if (!existsSync(mapPath))
@@ -315,6 +367,13 @@ export function install_workflow(repoRoot, opts = {}) {
315
367
  files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
316
368
  actions.push({ action: `install ${mapping.target}`, status: "ok" });
317
369
  }
370
+ else if (mapping.kind === "hook" && target_is_legacy_managed_four_hook_manifest(targetAbs)) {
371
+ // Previous SuperSpec default hooks installed PreToolUse/PostToolUse. Re-init must migrate
372
+ // that unmodified managed baseline so old blocking hooks do not stay in the workflow.
373
+ copyFileSync(sourceAbs, targetAbs);
374
+ files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
375
+ actions.push({ action: `install ${mapping.target}`, status: "updated", detail: "legacy managed four-hook manifest migrated to subagent-only manifest" });
376
+ }
318
377
  else if (opts.force) {
319
378
  copyFileSync(targetAbs, `${targetAbs}.bak`);
320
379
  copyFileSync(sourceAbs, targetAbs);
@@ -323,6 +382,11 @@ export function install_workflow(repoRoot, opts = {}) {
323
382
  files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
324
383
  actions.push({ action: `install ${mapping.target}`, status: "updated", detail: `existing file backed up to ${mapping.target}.bak` });
325
384
  }
385
+ else if (mapping.kind === "hook") {
386
+ copyFileSync(sourceAbs, `${targetAbs}.new`);
387
+ files.push({ path: mapping.target, sha256: targetSha, managed: false, preexisting: true });
388
+ actions.push({ action: `install ${mapping.target}`, status: "skipped", detail: `pre-existing hooks manifest kept; current SuperSpec manifest written to ${mapping.target}.new` });
389
+ }
326
390
  else {
327
391
  // Pre-existing different file: never overwrite, never delete (DISTRIBUTION §5 red line).
328
392
  files.push({ path: mapping.target, sha256: targetSha, managed: false, preexisting: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peterxiaoyang/superspec",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "SuperSpec workflow package: guard runtime, generic workflow templates, and Codex adapter payload.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5,32 +5,6 @@
5
5
  "strict_profile_default": "audit-only-until-r1-provenance-passes"
6
6
  },
7
7
  "hooks": {
8
- "PreToolUse": [
9
- {
10
- "matcher": "Bash|apply_patch|Edit|Write|mcp__.*",
11
- "hooks": [
12
- {
13
- "type": "command",
14
- "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
15
- "timeout": 120,
16
- "statusMessage": "SuperSpec 写入策略检查"
17
- }
18
- ]
19
- }
20
- ],
21
- "PostToolUse": [
22
- {
23
- "matcher": "Bash",
24
- "hooks": [
25
- {
26
- "type": "command",
27
- "command": "superspec-hook --change \"$SUPERSPEC_CHANGE\"",
28
- "timeout": 120,
29
- "statusMessage": "SuperSpec 运行证据记录"
30
- }
31
- ]
32
- }
33
- ],
34
8
  "SubagentStart": [
35
9
  {
36
10
  "matcher": ".*",
@@ -26,13 +26,6 @@ Apply 按 OpenSpec tasks 执行实现,负责 RED/GREEN 证据、任务勾选
26
26
 
27
27
  ## 第一条必跑命令
28
28
 
29
- 先尝试建立当前 change 的 SuperSpec hook session。R-1/provenance 未通过时该命令只会记录 audit-only lease 和降级诊断,不代表 mechanical enforcement 已启用:
30
-
31
- ```text
32
- superspec guard hook-session-begin --change "<change>" --workflow superspec-apply --entrypoint-token "<fresh-entrypoint-token>" --format agent
33
- superspec guard hook-session-status --change "<change>" --format agent
34
- ```
35
-
36
29
  ```text
37
30
  superspec guard workflow-packet --change "<change>" --gate apply_ready --format agent
38
31
  ```
@@ -26,13 +26,6 @@ Archive 在 `review_complete` allowed 后收尾:确认 archive readiness、保
26
26
 
27
27
  ## 第一条必跑命令
28
28
 
29
- 先尝试建立当前 change 的 SuperSpec hook session。R-1/provenance 未通过时该命令只会记录 audit-only lease 和降级诊断,不代表 mechanical enforcement 已启用:
30
-
31
- ```text
32
- superspec guard hook-session-begin --change "<change>" --workflow superspec-archive --entrypoint-token "<fresh-entrypoint-token>" --format agent
33
- superspec guard hook-session-status --change "<change>" --format agent
34
- ```
35
-
36
29
  ```text
37
30
  superspec guard workflow-packet --change "<change>" --gate archive_ready --format agent
38
31
  ```
@@ -67,7 +60,6 @@ openspec archive -y "<change>"
67
60
 
68
61
  ```text
69
62
  superspec guard check-archived --change "<change>" --format agent
70
- superspec guard hook-session-end --change "<change>" --reason archived --format agent
71
63
  ```
72
64
 
73
65
  `.superspec/artifacts/business-invariants.md`、`.superspec/artifacts/test-contract.md`、review/verification evidence、RED/GREEN evidence 和 archive evidence 必须能从 preservation manifest 追溯。
@@ -26,13 +26,6 @@ Explore 只做需求澄清、代码事实调查、范围边界和风险记录。
26
26
 
27
27
  ## 第一条必跑命令
28
28
 
29
- 先尝试建立当前 change 的 SuperSpec hook session。R-1/provenance 未通过时该命令只会记录 audit-only lease 和降级诊断,不代表 mechanical enforcement 已启用:
30
-
31
- ```text
32
- superspec guard hook-session-begin --change "<change>" --workflow superspec-explore --entrypoint-token "<fresh-entrypoint-token>" --format agent
33
- superspec guard hook-session-status --change "<change>" --format agent
34
- ```
35
-
36
29
  ```text
37
30
  superspec init --scope project --format agent
38
31
  ```
@@ -26,13 +26,6 @@ Propose 把 discovery 转成 OpenSpec proposal package,并补 SuperSpec 业务
26
26
 
27
27
  ## 第一条必跑命令
28
28
 
29
- 先尝试建立当前 change 的 SuperSpec hook session。R-1/provenance 未通过时该命令只会记录 audit-only lease 和降级诊断,不代表 mechanical enforcement 已启用:
30
-
31
- ```text
32
- superspec guard hook-session-begin --change "<change>" --workflow superspec-propose --entrypoint-token "<fresh-entrypoint-token>" --format agent
33
- superspec guard hook-session-status --change "<change>" --format agent
34
- ```
35
-
36
29
  ```text
37
30
  superspec guard workflow-packet --change "<change>" --gate explore_complete --format agent
38
31
  ```
@@ -26,13 +26,6 @@ Review 合并实现审查、架构审查、SuperSpec critic、final verification
26
26
 
27
27
  ## 第一条必跑命令
28
28
 
29
- 先尝试建立当前 change 的 SuperSpec hook session。R-1/provenance 未通过时该命令只会记录 audit-only lease 和降级诊断,不代表 mechanical enforcement 已启用:
30
-
31
- ```text
32
- superspec guard hook-session-begin --change "<change>" --workflow superspec-review --entrypoint-token "<fresh-entrypoint-token>" --format agent
33
- superspec guard hook-session-status --change "<change>" --format agent
34
- ```
35
-
36
29
  ```text
37
30
  superspec guard check-init --change "<change>" --format agent
38
31
  ```