@peterxiaoyang/superspec 0.1.13 → 0.1.15-alpha
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +302 -0
- package/dist/format.d.ts +29 -0
- package/dist/format.js +141 -0
- package/dist/next.d.ts +3 -0
- package/dist/next.js +161 -0
- package/dist/openspec.d.ts +9 -0
- package/dist/openspec.js +41 -0
- package/dist/record.d.ts +20 -0
- package/dist/record.js +211 -0
- package/dist/skill_loop.d.ts +23 -0
- package/dist/skill_loop.js +58 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.js +213 -0
- package/dist/sync.d.ts +3 -0
- package/dist/sync.js +135 -0
- package/dist/task.d.ts +7 -0
- package/dist/task.js +44 -0
- package/dist/transition.d.ts +34 -0
- package/dist/transition.js +400 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.js +3 -0
- package/package.json +8 -53
- package/templates/workflow/skills/superspec-apply/SKILL.md +46 -118
- package/templates/workflow/skills/superspec-archive/SKILL.md +22 -47
- package/templates/workflow/skills/superspec-explore/SKILL.md +38 -69
- package/templates/workflow/skills/superspec-propose/SKILL.md +58 -57
- package/templates/workflow/skills/superspec-review/SKILL.md +36 -81
- package/adapters/codex/agents/architect.toml +0 -13
- package/adapters/codex/agents/code-reviewer.toml +0 -13
- package/adapters/codex/agents/critic.toml +0 -13
- package/adapters/codex/agents/executor.toml +0 -13
- package/adapters/codex/agents/explore.toml +0 -13
- package/adapters/codex/agents/test-engineer.toml +0 -13
- package/adapters/codex/agents/test-runner.toml +0 -13
- package/adapters/codex/agents/verifier.toml +0 -13
- package/adapters/codex/install-map.json +0 -116
- package/bin/launch.js +0 -37
- package/bin/superspec-check.js +0 -4
- package/bin/superspec-hook.js +0 -4
- package/bin/superspec-init.js +0 -4
- package/bin/superspec.js +0 -4
- package/dist/src/apply_worker_chain.d.ts +0 -60
- package/dist/src/apply_worker_chain.js +0 -1283
- package/dist/src/apply_worker_chain_lifecycle.d.ts +0 -21
- package/dist/src/apply_worker_chain_lifecycle.js +0 -409
- package/dist/src/archive.d.ts +0 -23
- package/dist/src/archive.js +0 -430
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -33
- package/dist/src/cli_args.d.ts +0 -32
- package/dist/src/cli_args.js +0 -454
- package/dist/src/core.d.ts +0 -20
- package/dist/src/core.js +0 -394
- package/dist/src/disclosure.d.ts +0 -35
- package/dist/src/disclosure.js +0 -671
- package/dist/src/doctor.d.ts +0 -44
- package/dist/src/doctor.js +0 -230
- package/dist/src/evidence.d.ts +0 -29
- package/dist/src/evidence.js +0 -1107
- package/dist/src/gates.d.ts +0 -19
- package/dist/src/gates.js +0 -1631
- package/dist/src/git.d.ts +0 -8
- package/dist/src/git.js +0 -112
- package/dist/src/hooks/adapter.d.ts +0 -5
- package/dist/src/hooks/adapter.js +0 -347
- package/dist/src/hooks/guard_api.d.ts +0 -12
- package/dist/src/hooks/guard_api.js +0 -797
- package/dist/src/hooks/health.d.ts +0 -4
- package/dist/src/hooks/health.js +0 -86
- package/dist/src/hooks/policy_event.d.ts +0 -41
- package/dist/src/hooks/policy_event.js +0 -2634
- package/dist/src/hooks/types.d.ts +0 -72
- package/dist/src/hooks/types.js +0 -1
- package/dist/src/hooks/validation.d.ts +0 -3
- package/dist/src/hooks/validation.js +0 -70
- package/dist/src/i18n.d.ts +0 -21
- package/dist/src/i18n.js +0 -720
- package/dist/src/init_cli.d.ts +0 -28
- package/dist/src/init_cli.js +0 -399
- package/dist/src/install_engine.d.ts +0 -71
- package/dist/src/install_engine.js +0 -538
- package/dist/src/invariants.d.ts +0 -16
- package/dist/src/invariants.js +0 -363
- package/dist/src/openspec.d.ts +0 -37
- package/dist/src/openspec.js +0 -274
- package/dist/src/packet_render.d.ts +0 -5
- package/dist/src/packet_render.js +0 -1759
- package/dist/src/packet_schema.d.ts +0 -58
- package/dist/src/packet_schema.js +0 -1
- package/dist/src/paths.d.ts +0 -22
- package/dist/src/paths.js +0 -203
- package/dist/src/project_init.d.ts +0 -31
- package/dist/src/project_init.js +0 -167
- package/dist/src/self_update.d.ts +0 -14
- package/dist/src/self_update.js +0 -56
- package/dist/src/state.d.ts +0 -37
- package/dist/src/state.js +0 -464
- package/dist/src/tasks.d.ts +0 -38
- package/dist/src/tasks.js +0 -373
- package/dist/src/util.d.ts +0 -163
- package/dist/src/util.js +0 -792
- package/dist/superspec.d.ts +0 -6
- package/dist/superspec.js +0 -97
- package/dist/superspec_guard.d.ts +0 -4
- package/dist/superspec_guard.js +0 -19
- package/dist/superspec_hook.d.ts +0 -4
- package/dist/superspec_hook.js +0 -35
- package/dist/superspec_init.d.ts +0 -2
- package/dist/superspec_init.js +0 -17
- package/schemas/hook-event.schema.json +0 -27
- package/schemas/install-manifest.schema.json +0 -97
- package/templates/hooks/codex-hooks.json +0 -35
- package/templates/sidecar/archive-preservation.json +0 -11
- package/templates/sidecar/business-invariants.md +0 -38
- package/templates/sidecar/config.yaml +0 -13
- package/templates/sidecar/discovery.md +0 -33
- package/templates/sidecar/test-contract.md +0 -27
- package/templates/workflow/prompts/architect.md +0 -27
- package/templates/workflow/prompts/code-reviewer.md +0 -34
- package/templates/workflow/prompts/critic.md +0 -30
- package/templates/workflow/prompts/executor.md +0 -32
- package/templates/workflow/prompts/explore.md +0 -27
- package/templates/workflow/prompts/test-engineer.md +0 -28
- package/templates/workflow/prompts/test-runner.md +0 -35
- package/templates/workflow/prompts/verifier.md +0 -35
package/dist/src/git.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { JsonMap, Reason, TaskInfo } from "./util.ts";
|
|
2
|
-
export declare function file_blob_sha(filePath: string): string;
|
|
3
|
-
export declare function git_lines(repoRoot: string, ...args: string[]): string[];
|
|
4
|
-
export declare function dirty_worktree_paths(repoRoot: string): string[];
|
|
5
|
-
export declare function dirty_worktree_reasons(repoRoot: string, changeRoot: string, evidences: JsonMap[]): Reason[];
|
|
6
|
-
export declare function dirty_write_scope_red_reasons(repoRoot: string, tasks: Record<string, TaskInfo>, evidences: JsonMap[]): Reason[];
|
|
7
|
-
export declare function review_diff_paths(repoRoot: string, baseRef: string, headRef: string): string[];
|
|
8
|
-
export declare function review_diff_coverage_reasons(repoRoot: string, ev: JsonMap): Reason[];
|
package/dist/src/git.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, relative } from "node:path";
|
|
3
|
-
import { GuardError, reason, renderList, runCommand, runtime } from "./util.js";
|
|
4
|
-
import { splitList, task_test_evidence } from "./tasks.js";
|
|
5
|
-
import { live_user_confirmations } from "./evidence.js";
|
|
6
|
-
export function file_blob_sha(filePath) {
|
|
7
|
-
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
8
|
-
throw new GuardError(`git_blob_inspection_failure: reviewed target missing: ${filePath}`);
|
|
9
|
-
const proc = runCommand("git", ["hash-object", "--no-filters", filePath], { cwd: dirname(filePath), timeout: 30_000 });
|
|
10
|
-
if (proc.error || proc.status !== 0)
|
|
11
|
-
throw new GuardError(`git_blob_inspection_failure: git hash-object failed: ${proc.stderr.trim()}`);
|
|
12
|
-
return proc.stdout.trim();
|
|
13
|
-
}
|
|
14
|
-
export function git_lines(repoRoot, ...args) {
|
|
15
|
-
const proc = runCommand("git", args, { cwd: repoRoot, timeout: 30_000 });
|
|
16
|
-
if (proc.error || proc.status !== 0)
|
|
17
|
-
throw new GuardError(`git_blob_inspection_failure: git ${args.join(" ")} exit ${proc.status}: ${proc.stderr.trim()}`);
|
|
18
|
-
return proc.stdout.split(/\r?\n/).filter((line) => line.trim());
|
|
19
|
-
}
|
|
20
|
-
export function dirty_worktree_paths(repoRoot) {
|
|
21
|
-
const paths = new Set();
|
|
22
|
-
const proc = runCommand("git", ["status", "--porcelain=v1", "-z", "--no-renames"], { cwd: repoRoot, timeout: 30_000 });
|
|
23
|
-
if (proc.error || proc.status !== 0) {
|
|
24
|
-
throw new GuardError(`git_blob_inspection_failure: git status --porcelain=v1 -z --no-renames exit ${proc.status}: ${proc.stderr.trim()}`);
|
|
25
|
-
}
|
|
26
|
-
const entries = proc.stdout.split("\0").filter(Boolean);
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (entry.length < 4)
|
|
29
|
-
continue;
|
|
30
|
-
const item = entry.slice(3);
|
|
31
|
-
if (item)
|
|
32
|
-
paths.add(item);
|
|
33
|
-
}
|
|
34
|
-
return [...paths].sort();
|
|
35
|
-
}
|
|
36
|
-
export function dirty_worktree_reasons(repoRoot, changeRoot, evidences) {
|
|
37
|
-
let dirty;
|
|
38
|
-
try {
|
|
39
|
-
dirty = runtime.dirty_worktree_paths(repoRoot);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
return [reason("dirty_worktree_unavailable", `dirty worktree paths could not be inspected: ${String(err.message ?? err)}`)];
|
|
43
|
-
}
|
|
44
|
-
if (dirty.length === 0)
|
|
45
|
-
return [];
|
|
46
|
-
let changeRel = "";
|
|
47
|
-
const rel = relative(repoRoot, changeRoot);
|
|
48
|
-
if (rel && !rel.startsWith("..") && !isAbsolute(rel))
|
|
49
|
-
changeRel = rel;
|
|
50
|
-
const unknown = dirty.filter((item) => !(changeRel && item.startsWith(`${changeRel}/`)));
|
|
51
|
-
if (unknown.length === 0)
|
|
52
|
-
return [];
|
|
53
|
-
const confirmations = live_user_confirmations(evidences, "branch_handling");
|
|
54
|
-
const confirmedScopes = confirmations.flatMap((ev) => (Array.isArray(ev.confirmed_paths) ? ev.confirmed_paths.map((item) => String(item)).filter(Boolean) : []));
|
|
55
|
-
const unconfirmed = unknown.filter((item) => !confirmedScopes.some((scope) => pathInScope(item, scope)));
|
|
56
|
-
if (unconfirmed.length === 0)
|
|
57
|
-
return [];
|
|
58
|
-
return [reason("dirty_worktree_unattributed", `dirty/untracked files require branch/scope handling evidence with confirmed_paths before review_ready: ${renderList(unconfirmed.slice(0, 20))}`)];
|
|
59
|
-
}
|
|
60
|
-
function pathInScope(pathValue, scope) {
|
|
61
|
-
const normalized = scope.replace(/\/+$/, "");
|
|
62
|
-
return pathValue === normalized || pathValue.startsWith(`${normalized}/`);
|
|
63
|
-
}
|
|
64
|
-
export function dirty_write_scope_red_reasons(repoRoot, tasks, evidences) {
|
|
65
|
-
let dirty;
|
|
66
|
-
try {
|
|
67
|
-
dirty = runtime.dirty_worktree_paths(repoRoot);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
return [reason("dirty_worktree_unavailable", `dirty worktree paths could not be inspected: ${String(err.message ?? err)}`)];
|
|
71
|
-
}
|
|
72
|
-
const reasons = [];
|
|
73
|
-
for (const [taskId, task] of Object.entries(tasks)) {
|
|
74
|
-
const scopes = splitList(task.attrs.write_scope ?? "");
|
|
75
|
-
if (scopes.length === 0)
|
|
76
|
-
continue;
|
|
77
|
-
const touched = dirty.filter((item) => scopes.some((scope) => pathInScope(item, scope)));
|
|
78
|
-
if (touched.length === 0)
|
|
79
|
-
continue;
|
|
80
|
-
const tddRequired = (task.attrs.tdd_required ?? "true").toLowerCase() !== "false";
|
|
81
|
-
const tddMode = task.attrs.tdd_mode ?? "new-behavior";
|
|
82
|
-
if (!tddRequired || tddMode === "behavior-preserving-refactor")
|
|
83
|
-
continue;
|
|
84
|
-
if (task_test_evidence(evidences, taskId, "expected_failure", "task_edit").length === 0) {
|
|
85
|
-
reasons.push(reason("missing_red_evidence", `task ${taskId} write_scope changed without RED evidence: ${renderList(touched.slice(0, 20))}`, touched.slice(0, 20)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return reasons;
|
|
89
|
-
}
|
|
90
|
-
export function review_diff_paths(repoRoot, baseRef, headRef) {
|
|
91
|
-
return runtime.git_lines(repoRoot, "diff", "--name-only", `${baseRef}..${headRef}`);
|
|
92
|
-
}
|
|
93
|
-
export function review_diff_coverage_reasons(repoRoot, ev) {
|
|
94
|
-
const { base_ref: baseRef, head_ref: headRef, reviewed_files: reviewedFiles } = ev;
|
|
95
|
-
if (typeof baseRef !== "string" || !baseRef || typeof headRef !== "string" || !headRef) {
|
|
96
|
-
return [reason("review_evidence_incomplete", `${ev._path}: base_ref and head_ref must be non-empty strings`)];
|
|
97
|
-
}
|
|
98
|
-
if (!Array.isArray(reviewedFiles) || reviewedFiles.length === 0)
|
|
99
|
-
return [];
|
|
100
|
-
let diffFiles;
|
|
101
|
-
try {
|
|
102
|
-
diffFiles = new Set(runtime.review_diff_paths(repoRoot, baseRef, headRef));
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
return [reason("review_diff_unavailable", String(err.message ?? err))];
|
|
106
|
-
}
|
|
107
|
-
const reviewed = new Set(reviewedFiles.map((item) => String(item)));
|
|
108
|
-
const missing = [...diffFiles].filter((item) => !reviewed.has(item)).sort();
|
|
109
|
-
if (missing.length === 0)
|
|
110
|
-
return [];
|
|
111
|
-
return [reason("review_diff_not_covered", `${ev._path}: reviewed_files missing diff paths: ${renderList(missing.slice(0, 20))}`, missing.slice(0, 20))];
|
|
112
|
-
}
|
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { isObject } from "../util.js";
|
|
6
|
-
import { hookCheckCommand, hookCheckWrite, hookRecordSubagentStart, hookRecordSubagentStop, hookRecordTest, } from "./guard_api.js";
|
|
7
|
-
import { commandLooksLikeTestValidation, extractWriteIntent, isSuperSpecTrustRootPath } from "./policy_event.js";
|
|
8
|
-
function parseArgs(argv) {
|
|
9
|
-
let change = process.env.SUPERSPEC_CHANGE || null;
|
|
10
|
-
for (let idx = 0; idx < argv.length; idx += 1) {
|
|
11
|
-
if (argv[idx] === "--change")
|
|
12
|
-
change = argv[idx + 1] || null;
|
|
13
|
-
}
|
|
14
|
-
return { change };
|
|
15
|
-
}
|
|
16
|
-
function isDir(path) {
|
|
17
|
-
try {
|
|
18
|
-
return statSync(path).isDirectory();
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function readJson(path) {
|
|
25
|
-
try {
|
|
26
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
27
|
-
return isObject(parsed) ? parsed : null;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function validReasonArray(value) {
|
|
34
|
-
return Array.isArray(value) && value.every((item) => (isObject(item)
|
|
35
|
-
&& typeof item.code === "string"
|
|
36
|
-
&& item.code.length > 0
|
|
37
|
-
&& typeof item.message === "string"
|
|
38
|
-
&& item.message.length > 0
|
|
39
|
-
&& Array.isArray(item.refs)
|
|
40
|
-
&& item.refs.every((ref) => typeof ref === "string")));
|
|
41
|
-
}
|
|
42
|
-
function validActiveSessionRecord(path, change, repoRoot, event) {
|
|
43
|
-
const record = readJson(path);
|
|
44
|
-
if (!record)
|
|
45
|
-
return false;
|
|
46
|
-
if (record.schema_version !== 2 || record.kind !== "hook_active_session")
|
|
47
|
-
return false;
|
|
48
|
-
if (record.trust !== "audit-only" && record.trust !== "trusted")
|
|
49
|
-
return false;
|
|
50
|
-
if (record.strict_profile !== "available" && record.strict_profile !== "unavailable")
|
|
51
|
-
return false;
|
|
52
|
-
if (record.change_id !== change)
|
|
53
|
-
return false;
|
|
54
|
-
if (resolve(String(record.repo_root ?? "")) !== resolve(repoRoot))
|
|
55
|
-
return false;
|
|
56
|
-
if (typeof record.session_id !== "string" || !record.session_id)
|
|
57
|
-
return false;
|
|
58
|
-
if (!validReasonArray(record.audit_only_reasons))
|
|
59
|
-
return false;
|
|
60
|
-
const eventSessionId = typeof event.session_id === "string" && event.session_id ? event.session_id : "";
|
|
61
|
-
const auditRepoChangeLeaseFallback = record.trust === "audit-only"
|
|
62
|
-
&& record.strict_profile === "unavailable"
|
|
63
|
-
&& record.session_id === "manual-cli-session";
|
|
64
|
-
if (eventSessionId && record.session_id !== eventSessionId && !auditRepoChangeLeaseFallback)
|
|
65
|
-
return false;
|
|
66
|
-
const expiresAt = Date.parse(String(record.expires_at ?? ""));
|
|
67
|
-
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now())
|
|
68
|
-
return false;
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
function inferChangeFromActiveSessions(event) {
|
|
72
|
-
const repoRoot = findRepoRootFromCwd(event);
|
|
73
|
-
const changesDir = join(repoRoot, "openspec", "changes");
|
|
74
|
-
if (!isDir(changesDir))
|
|
75
|
-
return { state: "none" };
|
|
76
|
-
const candidates = readdirSync(changesDir).filter((change) => {
|
|
77
|
-
const runtimeDir = join(changesDir, change, ".superspec", "hook-runtime");
|
|
78
|
-
const activeDir = join(changesDir, change, ".superspec", "hook-runtime", "active-sessions");
|
|
79
|
-
if (isDir(activeDir) && readdirSync(activeDir).some((name) => (name.endsWith(".json") && validActiveSessionRecord(join(activeDir, name), change, repoRoot, event))))
|
|
80
|
-
return true;
|
|
81
|
-
return isDir(runtimeDir);
|
|
82
|
-
});
|
|
83
|
-
const unique = [...new Set(candidates)];
|
|
84
|
-
if (unique.length === 1)
|
|
85
|
-
return { state: "unique", change: unique[0] };
|
|
86
|
-
if (unique.length > 1)
|
|
87
|
-
return { state: "ambiguous", changes: unique.sort() };
|
|
88
|
-
return { state: "none" };
|
|
89
|
-
}
|
|
90
|
-
function cwdRoot(event) {
|
|
91
|
-
return typeof event.cwd === "string" && event.cwd ? resolve(event.cwd) : process.cwd();
|
|
92
|
-
}
|
|
93
|
-
function findRepoRootFromCwd(event) {
|
|
94
|
-
let dir = cwdRoot(event);
|
|
95
|
-
while (true) {
|
|
96
|
-
if ((isDir(join(dir, "openspec")) || isDir(join(dir, ".codex"))) && statMaybe(join(dir, "package.json")) === "file")
|
|
97
|
-
return dir;
|
|
98
|
-
if (isDir(join(dir, "openspec", "changes")) || isDir(join(dir, ".codex")))
|
|
99
|
-
return dir;
|
|
100
|
-
if (statMaybe(join(dir, ".git")))
|
|
101
|
-
return dir;
|
|
102
|
-
const parent = dirname(dir);
|
|
103
|
-
if (parent === dir)
|
|
104
|
-
return cwdRoot(event);
|
|
105
|
-
dir = parent;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function passThrough(message) {
|
|
109
|
-
return {
|
|
110
|
-
hookSpecificOutput: {
|
|
111
|
-
hookEventName: "PreToolUse",
|
|
112
|
-
additionalContext: message,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function statMaybe(path) {
|
|
117
|
-
try {
|
|
118
|
-
const st = statSync(path);
|
|
119
|
-
if (st.isFile())
|
|
120
|
-
return "file";
|
|
121
|
-
if (st.isDirectory())
|
|
122
|
-
return "dir";
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function denyPreToolUse(reason) {
|
|
130
|
-
return {
|
|
131
|
-
hookSpecificOutput: {
|
|
132
|
-
hookEventName: "PreToolUse",
|
|
133
|
-
permissionDecision: "deny",
|
|
134
|
-
permissionDecisionReason: reason,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function isTestValidationCommand(event) {
|
|
139
|
-
if (String(event.tool_name ?? "") !== "Bash")
|
|
140
|
-
return false;
|
|
141
|
-
const toolInput = isObject(event.tool_input) ? event.tool_input : {};
|
|
142
|
-
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
143
|
-
return commandLooksLikeTestValidation(command);
|
|
144
|
-
}
|
|
145
|
-
function inertPostToolUseOutput() {
|
|
146
|
-
return {
|
|
147
|
-
systemMessage: "SuperSpec hook ignored non-test command; no test telemetry was recorded.",
|
|
148
|
-
hookSpecificOutput: {
|
|
149
|
-
hookEventName: "PostToolUse",
|
|
150
|
-
additionalContext: "SuperSpec runtime telemetry is inert for non-test commands.",
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
function noChangeFallback(event, inference) {
|
|
155
|
-
if (String(event.hook_event_name ?? "PreToolUse") !== "PreToolUse") {
|
|
156
|
-
return { systemMessage: "SuperSpec hook inert: SUPERSPEC_CHANGE/--change not set" };
|
|
157
|
-
}
|
|
158
|
-
const intent = extractWriteIntent(event, findRepoRootFromCwd(event));
|
|
159
|
-
const writeCapable = intent.target_paths.length > 0
|
|
160
|
-
|| intent.archive_command
|
|
161
|
-
|| intent.internal_hook_writer_command
|
|
162
|
-
|| intent.unsupported_write_surface
|
|
163
|
-
|| intent.shell_write_command
|
|
164
|
-
|| intent.reasons.length > 0
|
|
165
|
-
|| ["apply_patch", "Edit", "Write"].includes(String(event.tool_name ?? ""));
|
|
166
|
-
if (inference.state === "ambiguous" && writeCapable) {
|
|
167
|
-
return denyPreToolUse(`blocked by SuperSpec hook policy: multiple active SuperSpec sessions (${inference.changes.join(", ")}); set SUPERSPEC_CHANGE or close stale leases`);
|
|
168
|
-
}
|
|
169
|
-
if (intent.internal_hook_writer_command) {
|
|
170
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: internal hook writer invocation requires Guard-owned hook authority");
|
|
171
|
-
}
|
|
172
|
-
if (intent.unsafe_lifecycle_termination_command) {
|
|
173
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: hook session cancellation requires trusted terminal authority");
|
|
174
|
-
}
|
|
175
|
-
if (intent.reasons.some((item) => item.code === "target_path_outside_repo")) {
|
|
176
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: write target escapes the current repository root");
|
|
177
|
-
}
|
|
178
|
-
if (intent.reasons.some((item) => item.code === "protected_trust_root_link_source")) {
|
|
179
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: link source points at a SuperSpec trust root");
|
|
180
|
-
}
|
|
181
|
-
if (intent.reasons.some((item) => item.code === "shell_path_may_touch_trust_root")) {
|
|
182
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: shell-expanded path may touch SuperSpec trust roots");
|
|
183
|
-
}
|
|
184
|
-
if (intent.reasons.some((item) => item.code === "curl_config_write_target_unknown")) {
|
|
185
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: curl config-driven writes have unknown targets");
|
|
186
|
-
}
|
|
187
|
-
if (intent.reasons.some((item) => item.code === "curl_write_target_unknown")) {
|
|
188
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: curl write options have unknown targets");
|
|
189
|
-
}
|
|
190
|
-
if (intent.target_paths.some((path) => isSuperSpecTrustRootPath(path))) {
|
|
191
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: direct writes to SuperSpec trust roots require an active Guard context");
|
|
192
|
-
}
|
|
193
|
-
if (intent.shell_write_command && intent.trust_root_text_matches.length > 0) {
|
|
194
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: pathless write-capable command appears to touch SuperSpec trust roots");
|
|
195
|
-
}
|
|
196
|
-
if (intent.archive_command) {
|
|
197
|
-
return denyPreToolUse("blocked by SuperSpec hook policy: openspec archive requires an active Guard context and archive_ready allow");
|
|
198
|
-
}
|
|
199
|
-
return passThrough("SuperSpec hook inert: SUPERSPEC_CHANGE/--change not set");
|
|
200
|
-
}
|
|
201
|
-
function preToolUseOutput(decision) {
|
|
202
|
-
if (!decision.allowed) {
|
|
203
|
-
return {
|
|
204
|
-
hookSpecificOutput: {
|
|
205
|
-
hookEventName: "PreToolUse",
|
|
206
|
-
permissionDecision: "deny",
|
|
207
|
-
permissionDecisionReason: decision.block_reasons.map((item) => item.message).join("; ") || "blocked by SuperSpec hook policy",
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
hookSpecificOutput: {
|
|
213
|
-
hookEventName: "PreToolUse",
|
|
214
|
-
additionalContext: `SuperSpec hook ${decision.enforcement}; strict_profile=${decision.strict_profile}; trust=${decision.trust}`,
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function postToolUseOutput(decision) {
|
|
219
|
-
return {
|
|
220
|
-
systemMessage: `SuperSpec hook telemetry recorded as ${decision.trust}; strict_profile=${decision.strict_profile}`,
|
|
221
|
-
hookSpecificOutput: {
|
|
222
|
-
hookEventName: "PostToolUse",
|
|
223
|
-
additionalContext: `SuperSpec runtime evidence is ${decision.strict_profile === "available" ? "runtime-verified" : "audit-only"}.`,
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
function isSubagentHookEvent(eventName) {
|
|
228
|
-
return eventName === "SubagentStart" || eventName === "SubagentStop";
|
|
229
|
-
}
|
|
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
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
systemMessage: `SuperSpec ${eventName} runlog recorded as ${decision.trust}; strict_profile=${decision.strict_profile}`,
|
|
242
|
-
};
|
|
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
|
-
}
|
|
253
|
-
function writeTempEvent(event) {
|
|
254
|
-
const path = join(tmpdir(), `superspec-hook-event-${randomUUID()}.json`);
|
|
255
|
-
writeFileSync(path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
256
|
-
return path;
|
|
257
|
-
}
|
|
258
|
-
function validateHookEvent(event) {
|
|
259
|
-
const eventName = typeof event.hook_event_name === "string" ? event.hook_event_name : "";
|
|
260
|
-
if (!eventName)
|
|
261
|
-
return "hook event missing required hook_event_name";
|
|
262
|
-
if (!["PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop"].includes(eventName)) {
|
|
263
|
-
return `unsupported hook_event_name: ${eventName}`;
|
|
264
|
-
}
|
|
265
|
-
if ((eventName === "PreToolUse" || eventName === "PostToolUse") && (typeof event.cwd !== "string" || !event.cwd)) {
|
|
266
|
-
return "hook event missing required cwd";
|
|
267
|
-
}
|
|
268
|
-
if (eventName === "PreToolUse") {
|
|
269
|
-
if (typeof event.tool_name !== "string" || !event.tool_name)
|
|
270
|
-
return "PreToolUse hook event missing required tool_name";
|
|
271
|
-
if (!isObject(event.tool_input))
|
|
272
|
-
return "PreToolUse hook event missing required tool_input object";
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
export function runHookAdapter(argv = process.argv.slice(2), stdin = readFileSync(0, "utf8")) {
|
|
277
|
-
const parsedArgs = parseArgs(argv);
|
|
278
|
-
let event;
|
|
279
|
-
let eventNameForFailure = "";
|
|
280
|
-
try {
|
|
281
|
-
if (!stdin.trim())
|
|
282
|
-
throw new Error("hook stdin is empty");
|
|
283
|
-
const parsed = JSON.parse(stdin);
|
|
284
|
-
if (!isObject(parsed))
|
|
285
|
-
throw new Error("hook stdin must be a JSON object");
|
|
286
|
-
event = parsed;
|
|
287
|
-
eventNameForFailure = typeof event.hook_event_name === "string" ? event.hook_event_name : "";
|
|
288
|
-
const validationError = validateHookEvent(event);
|
|
289
|
-
if (validationError)
|
|
290
|
-
throw new Error(validationError);
|
|
291
|
-
}
|
|
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
|
-
}
|
|
297
|
-
return { code: 2, stdout: "", stderr: `SuperSpec hook event parse failed: ${err.message}\n` };
|
|
298
|
-
}
|
|
299
|
-
const inference = parsedArgs.change ? { state: "unique", change: parsedArgs.change } : inferChangeFromActiveSessions(event);
|
|
300
|
-
const change = inference.state === "unique" ? inference.change : null;
|
|
301
|
-
if (!change) {
|
|
302
|
-
const payload = noChangeFallback(event, inference);
|
|
303
|
-
return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
|
|
304
|
-
}
|
|
305
|
-
let eventRef = null;
|
|
306
|
-
try {
|
|
307
|
-
eventRef = writeTempEvent(event);
|
|
308
|
-
const eventName = String(event.hook_event_name ?? "");
|
|
309
|
-
let payload;
|
|
310
|
-
if (eventName === "PreToolUse") {
|
|
311
|
-
const toolName = String(event.tool_name ?? "");
|
|
312
|
-
const decision = toolName === "Bash" ? hookCheckCommand(change, eventRef) : hookCheckWrite(change, eventRef);
|
|
313
|
-
payload = preToolUseOutput(decision);
|
|
314
|
-
}
|
|
315
|
-
else if (eventName === "PostToolUse") {
|
|
316
|
-
payload = isTestValidationCommand(event) ? postToolUseOutput(hookRecordTest(change, eventRef)) : inertPostToolUseOutput();
|
|
317
|
-
}
|
|
318
|
-
else if (eventName === "SubagentStart") {
|
|
319
|
-
payload = subagentOutput(hookRecordSubagentStart(change, eventRef), "SubagentStart");
|
|
320
|
-
}
|
|
321
|
-
else if (eventName === "SubagentStop") {
|
|
322
|
-
payload = subagentOutput(hookRecordSubagentStop(change, eventRef), "SubagentStop");
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
payload = { systemMessage: `SuperSpec hook ignored unsupported event ${eventName}` };
|
|
326
|
-
}
|
|
327
|
-
return { code: 0, stdout: `${JSON.stringify(payload)}\n`, stderr: "" };
|
|
328
|
-
}
|
|
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
|
-
}
|
|
335
|
-
return { code: 2, stdout: "", stderr: `SuperSpec hook failed closed: ${err.message}\n` };
|
|
336
|
-
}
|
|
337
|
-
finally {
|
|
338
|
-
if (eventRef) {
|
|
339
|
-
try {
|
|
340
|
-
unlinkSync(resolve(eventRef));
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// best effort temp cleanup
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { Reason } from "../util.ts";
|
|
2
|
-
import type { HookDecision } from "./types.ts";
|
|
3
|
-
export declare function hookCheckWrite(change: string, eventRef: string): HookDecision;
|
|
4
|
-
export declare function hookCheckCommand(change: string, eventRef: string): HookDecision;
|
|
5
|
-
export declare function hookHealth(change: string): HookDecision;
|
|
6
|
-
export declare function hookSessionBegin(change: string, workflow: string, _entrypointToken: string): HookDecision;
|
|
7
|
-
export declare function hookSessionStatus(change: string): HookDecision;
|
|
8
|
-
export declare function hookSessionEnd(change: string, endReason: string, _lifecycleToken: string): HookDecision;
|
|
9
|
-
export declare function hookRecordTest(change: string, eventRef: string): HookDecision;
|
|
10
|
-
export declare function hookRecordSubagentStart(change: string, eventRef: string): HookDecision;
|
|
11
|
-
export declare function hookRecordSubagentStop(change: string, eventRef: string): HookDecision;
|
|
12
|
-
export declare function hookInitReasons(repoRoot: string): Reason[];
|