@peterxiaoyang/superspec 0.1.6 → 0.1.8
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/adapters/codex/install-map.json +5 -0
- package/bin/superspec-hook.js +4 -0
- package/dist/src/apply_worker_chain_lifecycle.d.ts +19 -0
- package/dist/src/apply_worker_chain_lifecycle.js +278 -0
- package/dist/src/archive.js +2 -0
- package/dist/src/cli_args.d.ts +5 -0
- package/dist/src/cli_args.js +41 -0
- package/dist/src/core.js +36 -0
- package/dist/src/evidence.js +3 -0
- package/dist/src/gates.js +8 -258
- package/dist/src/hooks/adapter.d.ts +5 -0
- package/dist/src/hooks/adapter.js +311 -0
- package/dist/src/hooks/guard_api.d.ts +12 -0
- package/dist/src/hooks/guard_api.js +797 -0
- package/dist/src/hooks/health.d.ts +4 -0
- package/dist/src/hooks/health.js +98 -0
- package/dist/src/hooks/policy_event.d.ts +41 -0
- package/dist/src/hooks/policy_event.js +2624 -0
- package/dist/src/hooks/types.d.ts +72 -0
- package/dist/src/hooks/types.js +1 -0
- package/dist/src/hooks/validation.d.ts +3 -0
- package/dist/src/hooks/validation.js +70 -0
- package/dist/src/packet_measure.js +67 -102
- package/dist/src/packet_render.js +4 -85
- package/dist/superspec_hook.d.ts +4 -0
- package/dist/superspec_hook.js +35 -0
- package/package.json +7 -2
- package/schemas/hook-event.schema.json +27 -0
- package/templates/hooks/codex-hooks.json +61 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +7 -0
- package/templates/workflow/skills/superspec-archive/SKILL.md +8 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +7 -0
- package/templates/workflow/skills/superspec-propose/SKILL.md +7 -0
- package/templates/workflow/skills/superspec-review/SKILL.md +7 -0
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"source": "templates/workflow/skills/superspec-archive/SKILL.md",
|
|
28
28
|
"target": ".codex/skills/superspec-archive/SKILL.md"
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"kind": "hook",
|
|
32
|
+
"source": "templates/hooks/codex-hooks.json",
|
|
33
|
+
"target": ".codex/hooks.json"
|
|
34
|
+
},
|
|
30
35
|
{
|
|
31
36
|
"kind": "prompt",
|
|
32
37
|
"source": "templates/workflow/prompts/architect.md",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JsonMap, Reason } from "./util.ts";
|
|
2
|
+
export type ApplyWorkerChainLifecycleState = {
|
|
3
|
+
completionReasons: Reason[];
|
|
4
|
+
packetBlockers: Reason[];
|
|
5
|
+
activeBlockingReasons: Reason[];
|
|
6
|
+
activePresence: "none" | "single_open" | "blocked";
|
|
7
|
+
active: JsonMap | null;
|
|
8
|
+
openActiveChainIds: Set<string>;
|
|
9
|
+
validTerminalChainIds: Set<string>;
|
|
10
|
+
validClosedChainIds: Set<string>;
|
|
11
|
+
validClosedGreenEvidenceIds: Set<string>;
|
|
12
|
+
takeoverBaselines: {
|
|
13
|
+
fingerprint: unknown;
|
|
14
|
+
successorGreenRefs: string[];
|
|
15
|
+
}[];
|
|
16
|
+
};
|
|
17
|
+
export declare function serial_completion_green(ev: JsonMap): boolean;
|
|
18
|
+
export declare function apply_worker_chain_lifecycle_state(repoRoot: string, changeRoot: string, evidences: JsonMap[], taskId: string): ApplyWorkerChainLifecycleState;
|
|
19
|
+
export declare function apply_worker_chain_lifecycle_reasons(repoRoot: string, changeRoot: string, evidences: JsonMap[], taskId: string): Reason[];
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { isObject, reason, renderList, repr } from "./util.js";
|
|
2
|
+
import { find_pass, live_pass } from "./evidence.js";
|
|
3
|
+
import { apply_worker_implementation_fingerprint, apply_worker_executor_input_ref_digest, compute_apply_worker_freshness, fingerprint_matches, pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, pre_edit_evidence_ref_reasons, read_pinned_artifact_json, worker_input_ref_digest, worker_test_run_reasons, } from "./apply_worker_chain.js";
|
|
4
|
+
function pinned_artifact_ref_matches(changeRoot, refItem, role, taskId, chainId, kind = "worker_report") {
|
|
5
|
+
return shared_pinned_artifact_ref_reasons(changeRoot, refItem, `${role}_ref`, { kind, role, taskId, chainId }).length === 0;
|
|
6
|
+
}
|
|
7
|
+
function executor_worker_test_run_refs_valid(changeRoot, ev, taskId, chainId) {
|
|
8
|
+
return worker_test_run_reasons(changeRoot, ev, taskId, chainId).length === 0;
|
|
9
|
+
}
|
|
10
|
+
function report_origin_matches(refItem, expected) {
|
|
11
|
+
return isObject(refItem) && typeof expected === "string" && refItem.origin_packet_fingerprint === expected;
|
|
12
|
+
}
|
|
13
|
+
function report_input_refs_match(refItem, refs) {
|
|
14
|
+
return isObject(refItem) && refItem.input_ref_digest === worker_input_ref_digest(refs);
|
|
15
|
+
}
|
|
16
|
+
function report_input_digest_matches(refItem, expectedDigest) {
|
|
17
|
+
return isObject(refItem) && refItem.input_ref_digest === expectedDigest;
|
|
18
|
+
}
|
|
19
|
+
function chain_green_ids(ev) {
|
|
20
|
+
if (Array.isArray(ev.green_test_run_evidence_refs)) {
|
|
21
|
+
return ev.green_test_run_evidence_refs.map(String).filter(Boolean).sort();
|
|
22
|
+
}
|
|
23
|
+
const greenId = isObject(ev.green_test_run_evidence_ref)
|
|
24
|
+
? String(ev.green_test_run_evidence_ref.evidence_id ?? "")
|
|
25
|
+
: String(ev.green_test_run_evidence_ref ?? "");
|
|
26
|
+
return greenId ? [greenId] : [];
|
|
27
|
+
}
|
|
28
|
+
function active_pre_edit_input_refs(evidences, active) {
|
|
29
|
+
const preIds = Array.isArray(active.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String) : [];
|
|
30
|
+
return preIds.map((evidenceId) => {
|
|
31
|
+
const ev = live_pass(evidences, { kind: "test_run" }).find((item) => String(item.evidence_id ?? "") === evidenceId);
|
|
32
|
+
return ev
|
|
33
|
+
? { kind: "test_run", evidence_id: evidenceId, phase: ev.phase, semantic_status: ev.semantic_status }
|
|
34
|
+
: { kind: "test_run", evidence_id: evidenceId };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId) {
|
|
38
|
+
const active = live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
|
|
39
|
+
.find((item) => item.chain_state === "active" && item.apply_worker_chain_id === chainId);
|
|
40
|
+
if (!active)
|
|
41
|
+
return false;
|
|
42
|
+
if (pre_edit_evidence_ref_reasons(evidences, active, taskId).length > 0)
|
|
43
|
+
return false;
|
|
44
|
+
if (!pinned_artifact_ref_matches(changeRoot, ev.executor_report_ref, "executor", taskId, chainId))
|
|
45
|
+
return false;
|
|
46
|
+
if (!pinned_artifact_ref_matches(changeRoot, ev.task_code_review_report_ref, "code-reviewer", taskId, chainId))
|
|
47
|
+
return false;
|
|
48
|
+
if (!pinned_artifact_ref_matches(changeRoot, ev.verifier_report_ref, "verifier", taskId, chainId))
|
|
49
|
+
return false;
|
|
50
|
+
if (!report_origin_matches(ev.executor_report_ref, active.executor_packet_fingerprint))
|
|
51
|
+
return false;
|
|
52
|
+
if (!report_input_digest_matches(ev.executor_report_ref, apply_worker_executor_input_ref_digest(evidences, active)))
|
|
53
|
+
return false;
|
|
54
|
+
if (!report_input_refs_match(ev.task_code_review_report_ref, [ev.executor_report_ref]))
|
|
55
|
+
return false;
|
|
56
|
+
if (typeof ev.observed_freshness_fingerprint !== "string" && !(isObject(ev.observed_freshness_fingerprint) && typeof ev.observed_freshness_fingerprint.fingerprint_digest === "string"))
|
|
57
|
+
return false;
|
|
58
|
+
const verifierReport = read_pinned_artifact_json(changeRoot, ev.verifier_report_ref);
|
|
59
|
+
const verifierObserved = verifierReport?.observed_freshness_fingerprint ?? (isObject(ev.verifier_report_ref) ? ev.verifier_report_ref.observed_freshness_fingerprint : undefined);
|
|
60
|
+
if (!fingerprint_matches(verifierObserved, ev.observed_freshness_fingerprint))
|
|
61
|
+
return false;
|
|
62
|
+
const greenIds = chain_green_ids(ev);
|
|
63
|
+
if (greenIds.length === 0)
|
|
64
|
+
return false;
|
|
65
|
+
const greenRuns = greenIds.map((greenId) => live_pass(evidences, { gate: "task_complete", kind: "test_run", task_id: taskId }).find((item) => (String(item.evidence_id ?? "") === greenId
|
|
66
|
+
&& item.semantic_status === "expected_success"
|
|
67
|
+
&& (item.phase === undefined || item.phase === "green")
|
|
68
|
+
&& item.apply_execution_chain === "executor_worker"
|
|
69
|
+
&& item.apply_worker_chain_id === chainId
|
|
70
|
+
&& executor_worker_test_run_refs_valid(changeRoot, item, taskId, chainId)))).filter((item) => isObject(item));
|
|
71
|
+
if (greenRuns.length !== greenIds.length)
|
|
72
|
+
return false;
|
|
73
|
+
if (!report_input_refs_match(ev.verifier_report_ref, [
|
|
74
|
+
ev.executor_report_ref,
|
|
75
|
+
ev.task_code_review_report_ref,
|
|
76
|
+
...greenRuns.map((green) => ({ kind: "test_run", evidence_id: String(green.evidence_id ?? ""), phase: green.phase, semantic_status: green.semantic_status })),
|
|
77
|
+
...active_pre_edit_input_refs(evidences, active),
|
|
78
|
+
]))
|
|
79
|
+
return false;
|
|
80
|
+
const currentFreshness = compute_apply_worker_freshness(repoRoot, changeRoot, evidences, taskId, chainId, {
|
|
81
|
+
executor_report_ref: ev.executor_report_ref,
|
|
82
|
+
task_code_review_report_ref: ev.task_code_review_report_ref,
|
|
83
|
+
green_test_run_evidence_ref: greenIds[0],
|
|
84
|
+
green_test_run_evidence_refs: greenIds,
|
|
85
|
+
verifier_report_ref: ev.verifier_report_ref,
|
|
86
|
+
});
|
|
87
|
+
if (Array.isArray(currentFreshness.unexpected_guard_owned_dirty_paths) && currentFreshness.unexpected_guard_owned_dirty_paths.length > 0)
|
|
88
|
+
return false;
|
|
89
|
+
if (Array.isArray(currentFreshness.protected_dirty_paths) && currentFreshness.protected_dirty_paths.length > 0)
|
|
90
|
+
return false;
|
|
91
|
+
if (!greenRuns.every((green) => fingerprint_matches(green.implementation_fingerprint, currentFreshness.implementation_fingerprint)))
|
|
92
|
+
return false;
|
|
93
|
+
if (!fingerprint_matches(isObject(ev.task_code_review_report_ref) ? ev.task_code_review_report_ref.observed_implementation_fingerprint : undefined, currentFreshness.implementation_fingerprint))
|
|
94
|
+
return false;
|
|
95
|
+
return fingerprint_matches(currentFreshness, verifierObserved);
|
|
96
|
+
}
|
|
97
|
+
function pinned_takeover_baseline_valid(changeRoot, refItem, taskId, chainId) {
|
|
98
|
+
return shared_pinned_artifact_ref_reasons(changeRoot, refItem, "serial_takeover_baseline_ref", { kind: "status_report", role: "verifier", taskId, chainId }).length === 0;
|
|
99
|
+
}
|
|
100
|
+
function abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, active, taskId, chainId) {
|
|
101
|
+
if (!active)
|
|
102
|
+
return false;
|
|
103
|
+
if ("restored_implementation_fingerprint" in ev) {
|
|
104
|
+
const activeScope = Array.isArray(active.declared_task_write_scope) ? active.declared_task_write_scope.map(String).filter(Boolean) : [];
|
|
105
|
+
const current = apply_worker_implementation_fingerprint(repoRoot, changeRoot, [], { declaredTaskWriteScope: activeScope });
|
|
106
|
+
if (fingerprint_matches(ev.restored_implementation_fingerprint, active.source_implementation_fingerprint)
|
|
107
|
+
&& fingerprint_matches(current, active.source_implementation_fingerprint))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if ("serial_takeover_baseline_ref" in ev) {
|
|
111
|
+
return pinned_takeover_baseline_valid(changeRoot, ev.serial_takeover_baseline_ref, taskId, chainId)
|
|
112
|
+
&& Array.isArray(ev.successor_green_evidence_refs)
|
|
113
|
+
&& ev.successor_green_evidence_refs.length > 0
|
|
114
|
+
&& ev.successor_green_evidence_refs.every((item) => typeof item === "string" && item.length > 0);
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
export function serial_completion_green(ev) {
|
|
119
|
+
return (ev.runner_origin === undefined || ev.runner_origin === "main-thread")
|
|
120
|
+
&& ev.apply_execution_chain !== "executor_worker"
|
|
121
|
+
&& typeof ev.apply_worker_chain_id !== "string";
|
|
122
|
+
}
|
|
123
|
+
export function apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidences, taskId) {
|
|
124
|
+
const chains = find_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId });
|
|
125
|
+
const activeByChain = new Map();
|
|
126
|
+
const activeCounts = new Map();
|
|
127
|
+
for (const ev of chains) {
|
|
128
|
+
if (String(ev.chain_state ?? "") !== "active")
|
|
129
|
+
continue;
|
|
130
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
131
|
+
if (!chainId)
|
|
132
|
+
continue;
|
|
133
|
+
const bucket = activeCounts.get(chainId) ?? [];
|
|
134
|
+
bucket.push(ev);
|
|
135
|
+
activeCounts.set(chainId, bucket);
|
|
136
|
+
if (!activeByChain.has(chainId))
|
|
137
|
+
activeByChain.set(chainId, ev);
|
|
138
|
+
}
|
|
139
|
+
const chainIdsWithActive = new Set([...activeByChain.keys()]);
|
|
140
|
+
const terminalValidity = new Map();
|
|
141
|
+
const chainIdsWithTerminal = new Set(chains
|
|
142
|
+
.filter((ev) => {
|
|
143
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
144
|
+
if (!chainId)
|
|
145
|
+
return false;
|
|
146
|
+
let valid = false;
|
|
147
|
+
if (String(ev.chain_state ?? "") === "closed")
|
|
148
|
+
valid = closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId);
|
|
149
|
+
if (String(ev.chain_state ?? "") === "abandoned")
|
|
150
|
+
valid = abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), taskId, chainId);
|
|
151
|
+
terminalValidity.set(ev, valid);
|
|
152
|
+
return valid;
|
|
153
|
+
})
|
|
154
|
+
.map((ev) => String(ev.apply_worker_chain_id ?? ""))
|
|
155
|
+
.filter(Boolean));
|
|
156
|
+
const openActiveChainIds = new Set([...activeByChain.keys()].filter((chainId) => !chainIdsWithTerminal.has(chainId)));
|
|
157
|
+
const validClosedChainIds = new Set(chains
|
|
158
|
+
.filter((ev) => {
|
|
159
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
160
|
+
const valid = String(ev.chain_state ?? "") === "closed" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
|
|
161
|
+
return chainId && valid;
|
|
162
|
+
})
|
|
163
|
+
.map((ev) => String(ev.apply_worker_chain_id ?? ""))
|
|
164
|
+
.filter(Boolean));
|
|
165
|
+
const validClosedGreenEvidenceIds = new Set(chains
|
|
166
|
+
.filter((ev) => {
|
|
167
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
168
|
+
const valid = String(ev.chain_state ?? "") === "closed" && (terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
|
|
169
|
+
return chainId && valid;
|
|
170
|
+
})
|
|
171
|
+
.flatMap(chain_green_ids));
|
|
172
|
+
const takeoverBaselines = [];
|
|
173
|
+
const completionReasons = [];
|
|
174
|
+
const duplicateActives = [...activeCounts.entries()]
|
|
175
|
+
.filter(([chainId]) => openActiveChainIds.has(chainId))
|
|
176
|
+
.map(([, items]) => items)
|
|
177
|
+
.filter((items) => items.length > 1)
|
|
178
|
+
.flatMap((items) => items.map((ev) => String(ev.evidence_id ?? ev.apply_worker_chain_id ?? "")))
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.sort();
|
|
181
|
+
const openActiveValues = [...openActiveChainIds].map((chainId) => activeByChain.get(chainId)).filter((ev) => isObject(ev));
|
|
182
|
+
const parallelActives = openActiveValues.length > 1
|
|
183
|
+
? openActiveValues.map((ev) => String(ev.evidence_id ?? ev.apply_worker_chain_id ?? "")).filter(Boolean).sort()
|
|
184
|
+
: [];
|
|
185
|
+
const activeConflicts = [...new Set([...duplicateActives, ...parallelActives])].sort();
|
|
186
|
+
if (activeConflicts.length > 0) {
|
|
187
|
+
completionReasons.push(reason("apply_worker_chain_active_conflict", `task ${taskId} has conflicting active apply worker chain markers: ${renderList(activeConflicts)}`, activeConflicts));
|
|
188
|
+
}
|
|
189
|
+
const terminalsByChain = new Map();
|
|
190
|
+
for (const ev of chains) {
|
|
191
|
+
const state = String(ev.chain_state ?? "");
|
|
192
|
+
if (state !== "closed" && state !== "abandoned")
|
|
193
|
+
continue;
|
|
194
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
195
|
+
if (!chainId)
|
|
196
|
+
continue;
|
|
197
|
+
const bucket = terminalsByChain.get(chainId) ?? [];
|
|
198
|
+
bucket.push(ev);
|
|
199
|
+
terminalsByChain.set(chainId, bucket);
|
|
200
|
+
}
|
|
201
|
+
const duplicateTerminals = [...terminalsByChain.values()]
|
|
202
|
+
.filter((items) => items.length > 1)
|
|
203
|
+
.flatMap((items) => items.map((ev) => String(ev.evidence_id ?? ev.apply_worker_chain_id ?? "")))
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.sort();
|
|
206
|
+
if (duplicateTerminals.length > 0) {
|
|
207
|
+
completionReasons.push(reason("apply_worker_chain_terminal_conflict", `task ${taskId} has duplicate apply worker chain terminal markers: ${renderList(duplicateTerminals)}`, duplicateTerminals));
|
|
208
|
+
}
|
|
209
|
+
const invalidTerminals = chains
|
|
210
|
+
.filter((ev) => String(ev.chain_state ?? "") === "closed" || String(ev.chain_state ?? "") === "abandoned")
|
|
211
|
+
.filter((ev) => {
|
|
212
|
+
const chainId = String(ev.apply_worker_chain_id ?? "");
|
|
213
|
+
if (!chainId)
|
|
214
|
+
return true;
|
|
215
|
+
if (String(ev.chain_state ?? "") === "closed")
|
|
216
|
+
return !(terminalValidity.get(ev) ?? closed_apply_worker_chain_valid(repoRoot, changeRoot, ev, evidences, taskId, chainId));
|
|
217
|
+
const valid = terminalValidity.get(ev) ?? abandoned_apply_worker_chain_valid(repoRoot, changeRoot, ev, activeByChain.get(chainId), taskId, chainId);
|
|
218
|
+
if (valid && isObject(ev.serial_takeover_baseline_ref)) {
|
|
219
|
+
const baseline = read_pinned_artifact_json(changeRoot, ev.serial_takeover_baseline_ref);
|
|
220
|
+
if (baseline?.implementation_fingerprint !== undefined) {
|
|
221
|
+
takeoverBaselines.push({
|
|
222
|
+
fingerprint: baseline.implementation_fingerprint,
|
|
223
|
+
successorGreenRefs: Array.isArray(ev.successor_green_evidence_refs) ? ev.successor_green_evidence_refs.map(String).filter(Boolean) : [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return !valid;
|
|
228
|
+
})
|
|
229
|
+
.map((ev) => String(ev.evidence_id ?? ev.apply_worker_chain_id ?? ""))
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.sort();
|
|
232
|
+
if (invalidTerminals.length > 0) {
|
|
233
|
+
completionReasons.push(reason("apply_worker_chain_terminal_invalid", `task ${taskId} has invalid apply worker chain terminal markers: ${renderList(invalidTerminals)}`, invalidTerminals));
|
|
234
|
+
}
|
|
235
|
+
for (const chainId of [...openActiveChainIds].sort()) {
|
|
236
|
+
if (!chainIdsWithTerminal.has(chainId)) {
|
|
237
|
+
completionReasons.push(reason("apply_worker_chain_active", `task ${taskId} has active apply worker chain and cannot use non-chain task completion: ${chainId}`, [chainId]));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const ev of evidences) {
|
|
241
|
+
if (!isObject(ev) || ev._invalid || ev.status !== "pass" || ev.task_id !== taskId)
|
|
242
|
+
continue;
|
|
243
|
+
const chainId = typeof ev.apply_worker_chain_id === "string" ? ev.apply_worker_chain_id : "";
|
|
244
|
+
if (!chainId)
|
|
245
|
+
continue;
|
|
246
|
+
if (!chainIdsWithActive.has(chainId)) {
|
|
247
|
+
completionReasons.push(reason("apply_worker_chain_missing_active", `task ${taskId} references apply_worker_chain_id=${repr(chainId)} without a matching active marker`, [chainId]));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (duplicateTerminals.length > 0 || activeConflicts.length > 0) {
|
|
251
|
+
validClosedChainIds.clear();
|
|
252
|
+
validClosedGreenEvidenceIds.clear();
|
|
253
|
+
}
|
|
254
|
+
const activeBlockingReasons = completionReasons.filter((item) => (item.code === "apply_worker_chain_active_conflict"
|
|
255
|
+
|| item.code === "apply_worker_chain_terminal_conflict"
|
|
256
|
+
|| item.code === "apply_worker_chain_terminal_invalid"
|
|
257
|
+
|| item.code === "apply_worker_chain_missing_active"));
|
|
258
|
+
const activePresence = activeBlockingReasons.length > 0
|
|
259
|
+
? "blocked"
|
|
260
|
+
: openActiveValues.length === 1
|
|
261
|
+
? "single_open"
|
|
262
|
+
: "none";
|
|
263
|
+
return {
|
|
264
|
+
completionReasons,
|
|
265
|
+
packetBlockers: activeBlockingReasons,
|
|
266
|
+
activeBlockingReasons,
|
|
267
|
+
activePresence,
|
|
268
|
+
active: activePresence === "single_open" ? openActiveValues[0] : null,
|
|
269
|
+
openActiveChainIds,
|
|
270
|
+
validTerminalChainIds: chainIdsWithTerminal,
|
|
271
|
+
validClosedChainIds,
|
|
272
|
+
validClosedGreenEvidenceIds,
|
|
273
|
+
takeoverBaselines,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
export function apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId) {
|
|
277
|
+
return apply_worker_chain_lifecycle_state(repoRoot, changeRoot, evidences, taskId).completionReasons;
|
|
278
|
+
}
|
package/dist/src/archive.js
CHANGED
|
@@ -35,6 +35,8 @@ function write_text(path, text) {
|
|
|
35
35
|
function excluded_manifest_entry(relPath) {
|
|
36
36
|
if (relPath.endsWith(STATE_LOCK_FILENAME) || relPath.endsWith("superspec-state.tmp"))
|
|
37
37
|
return true;
|
|
38
|
+
if (relPath === ".superspec/hook-runtime" || relPath.startsWith(".superspec/hook-runtime/"))
|
|
39
|
+
return true;
|
|
38
40
|
if (relPath === ".superspec/artifacts/archive-preservation.json")
|
|
39
41
|
return true;
|
|
40
42
|
return false;
|
package/dist/src/cli_args.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export type ParsedArgs = {
|
|
|
9
9
|
task_id?: string;
|
|
10
10
|
test_id?: string;
|
|
11
11
|
phase?: "red" | "characterization" | "green";
|
|
12
|
+
event_ref?: string;
|
|
13
|
+
workflow?: string;
|
|
14
|
+
entrypoint_token?: string;
|
|
15
|
+
end_reason?: string;
|
|
16
|
+
lifecycle_token?: string;
|
|
12
17
|
role?: string;
|
|
13
18
|
evidence_kind?: string;
|
|
14
19
|
round?: number;
|
package/dist/src/cli_args.js
CHANGED
|
@@ -11,6 +11,8 @@ const SIMPLE_COMMANDS = [
|
|
|
11
11
|
"check-verify-ready",
|
|
12
12
|
"check-archive-ready",
|
|
13
13
|
"check-archived",
|
|
14
|
+
"hook-health",
|
|
15
|
+
"hook-session-status",
|
|
14
16
|
];
|
|
15
17
|
const COMMANDS = [
|
|
16
18
|
"init",
|
|
@@ -27,6 +29,13 @@ const COMMANDS = [
|
|
|
27
29
|
"apply-code-review-packet",
|
|
28
30
|
"apply-verify-packet",
|
|
29
31
|
"ledger-render",
|
|
32
|
+
"hook-check-write",
|
|
33
|
+
"hook-check-command",
|
|
34
|
+
"hook-record-test",
|
|
35
|
+
"hook-record-subagent-start",
|
|
36
|
+
"hook-record-subagent-stop",
|
|
37
|
+
"hook-session-begin",
|
|
38
|
+
"hook-session-end",
|
|
30
39
|
];
|
|
31
40
|
const COMMAND_LIST = COMMANDS.join(",");
|
|
32
41
|
const COMMAND_CHOICES = COMMANDS.map((item) => `'${item}'`).join(", ");
|
|
@@ -61,6 +70,13 @@ function requiredValueFlags(command) {
|
|
|
61
70
|
flags.push("--task-id", "--executor-report-ref", "--task-code-review-report-ref", "--green-test-run-evidence-ref");
|
|
62
71
|
if (command === "ledger-render")
|
|
63
72
|
flags.push("--gate");
|
|
73
|
+
if (command === "hook-check-write" || command === "hook-check-command" || command === "hook-record-test"
|
|
74
|
+
|| command === "hook-record-subagent-start" || command === "hook-record-subagent-stop")
|
|
75
|
+
flags.push("--event-ref");
|
|
76
|
+
if (command === "hook-session-begin")
|
|
77
|
+
flags.push("--workflow", "--entrypoint-token");
|
|
78
|
+
if (command === "hook-session-end")
|
|
79
|
+
flags.push("--reason");
|
|
64
80
|
return flags;
|
|
65
81
|
}
|
|
66
82
|
function requiredBooleanFlags(command) {
|
|
@@ -84,6 +100,8 @@ function optionalValueFlags(command) {
|
|
|
84
100
|
return ["--red-test-run-evidence-ref", "--characterization-test-run-evidence-ref"];
|
|
85
101
|
if (command === "ledger-render")
|
|
86
102
|
return ["--round"];
|
|
103
|
+
if (command === "hook-session-end")
|
|
104
|
+
return ["--lifecycle-token"];
|
|
87
105
|
return [];
|
|
88
106
|
}
|
|
89
107
|
function formatUsage(command) {
|
|
@@ -376,6 +394,29 @@ export function parse_argv(argv) {
|
|
|
376
394
|
throw new GuardError("--round 必须是大于等于 1 的整数");
|
|
377
395
|
return { command, change, gate, round };
|
|
378
396
|
}
|
|
397
|
+
if (command === "hook-check-write" || command === "hook-check-command" || command === "hook-record-test"
|
|
398
|
+
|| command === "hook-record-subagent-start" || command === "hook-record-subagent-stop") {
|
|
399
|
+
const eventRef = getValue("--event-ref");
|
|
400
|
+
if (!eventRef)
|
|
401
|
+
throw new Error("缺少必填参数 --event-ref");
|
|
402
|
+
return { command, change, format, event_ref: eventRef };
|
|
403
|
+
}
|
|
404
|
+
if (command === "hook-session-begin") {
|
|
405
|
+
const workflow = getValue("--workflow");
|
|
406
|
+
const entrypointToken = getValue("--entrypoint-token");
|
|
407
|
+
if (!workflow)
|
|
408
|
+
throw new Error("缺少必填参数 --workflow");
|
|
409
|
+
if (!entrypointToken)
|
|
410
|
+
throw new Error("缺少必填参数 --entrypoint-token");
|
|
411
|
+
return { command, change, format, workflow, entrypoint_token: entrypointToken };
|
|
412
|
+
}
|
|
413
|
+
if (command === "hook-session-end") {
|
|
414
|
+
const endReason = getValue("--reason");
|
|
415
|
+
const lifecycleToken = getValue("--lifecycle-token");
|
|
416
|
+
if (!endReason)
|
|
417
|
+
throw new Error("缺少必填参数 --reason");
|
|
418
|
+
return { command, change, format, end_reason: endReason, lifecycle_token: lifecycleToken ?? "" };
|
|
419
|
+
}
|
|
379
420
|
if (command === "check-task-reopen" || command === "check-task-edit" || command === "check-task-complete") {
|
|
380
421
|
const taskId = getValue("--task-id");
|
|
381
422
|
if (!taskId)
|
package/dist/src/core.js
CHANGED
|
@@ -23,6 +23,7 @@ import { dirty_worktree_paths, dirty_worktree_reasons, dirty_write_scope_red_rea
|
|
|
23
23
|
import { append_ledger, load_state, compute_fingerprints, prepare_recomputed_state_write, read_ledger_text, record_supersede_ledger_events, restore_state_snapshot_locked, state_corrupt_reasons, state_file, state_file_corrupt, state_stale_reasons, force_unlock_state, with_state_lock, write_prepared_state_locked, } from "./state.js";
|
|
24
24
|
import { archive_manifest_path, begin_archive_preservation_bundle, check_archived, preset_upgrade_reasons, preset_upgrade_required_from_context, write_archive_preservation_bundle, } from "./archive.js";
|
|
25
25
|
import { check_archive_ready, check_apply_ready, check_artifact, check_init, check_superspec_gate, check_review_complete, check_review_ready, check_task_complete, check_task_edit, check_task_reopen, check_verify_complete, evidence_schema_guard, superspec_agent_reasons, superspec_workflow_skill_reasons, openspec_cli_capability_reasons, openspec_init_reasons, } from "./gates.js";
|
|
26
|
+
import { hookCheckCommand, hookCheckWrite, hookHealth, hookRecordSubagentStart, hookRecordSubagentStop, hookRecordTest, hookSessionBegin, hookSessionEnd, hookSessionStatus, } from "./hooks/guard_api.js";
|
|
26
27
|
const RETRY_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
27
28
|
const DEFAULT_MAX_STATE_WRITE_RETRIES = 5;
|
|
28
29
|
function sleep_ms(ms) {
|
|
@@ -42,6 +43,39 @@ export function load_context(change) {
|
|
|
42
43
|
const evidences = index_evidence(changeRoot);
|
|
43
44
|
return [status, repoRoot, changeRoot, evidences];
|
|
44
45
|
}
|
|
46
|
+
function is_hook_command(command) {
|
|
47
|
+
return command === "hook-check-write"
|
|
48
|
+
|| command === "hook-check-command"
|
|
49
|
+
|| command === "hook-record-test"
|
|
50
|
+
|| command === "hook-record-subagent-start"
|
|
51
|
+
|| command === "hook-record-subagent-stop"
|
|
52
|
+
|| command === "hook-health"
|
|
53
|
+
|| command === "hook-session-begin"
|
|
54
|
+
|| command === "hook-session-status"
|
|
55
|
+
|| command === "hook-session-end";
|
|
56
|
+
}
|
|
57
|
+
function dispatch_hook_command(args) {
|
|
58
|
+
const change = args.change;
|
|
59
|
+
if (args.command === "hook-check-write")
|
|
60
|
+
return [hookCheckWrite(change, args.event_ref ?? ""), "hook"];
|
|
61
|
+
if (args.command === "hook-check-command")
|
|
62
|
+
return [hookCheckCommand(change, args.event_ref ?? ""), "hook"];
|
|
63
|
+
if (args.command === "hook-record-test")
|
|
64
|
+
return [hookRecordTest(change, args.event_ref ?? ""), "hook"];
|
|
65
|
+
if (args.command === "hook-record-subagent-start")
|
|
66
|
+
return [hookRecordSubagentStart(change, args.event_ref ?? ""), "hook"];
|
|
67
|
+
if (args.command === "hook-record-subagent-stop")
|
|
68
|
+
return [hookRecordSubagentStop(change, args.event_ref ?? ""), "hook"];
|
|
69
|
+
if (args.command === "hook-health")
|
|
70
|
+
return [hookHealth(change), "hook"];
|
|
71
|
+
if (args.command === "hook-session-begin")
|
|
72
|
+
return [hookSessionBegin(change, args.workflow ?? "", args.entrypoint_token ?? ""), "hook"];
|
|
73
|
+
if (args.command === "hook-session-status")
|
|
74
|
+
return [hookSessionStatus(change), "hook"];
|
|
75
|
+
if (args.command === "hook-session-end")
|
|
76
|
+
return [hookSessionEnd(change, args.end_reason ?? "", args.lifecycle_token ?? ""), "hook"];
|
|
77
|
+
throw new GuardError(`unknown hook command: ${args.command}`);
|
|
78
|
+
}
|
|
45
79
|
export function cmd_status(change) {
|
|
46
80
|
const [status, repoRoot, changeRoot] = runtime.load_context(change);
|
|
47
81
|
const amap = artifact_status_map(status);
|
|
@@ -117,6 +151,8 @@ export function cmd_init_summary(change, changeRoot, decision) {
|
|
|
117
151
|
function dispatch_once(args) {
|
|
118
152
|
const change = args.change;
|
|
119
153
|
const cmd = args.command;
|
|
154
|
+
if (is_hook_command(cmd))
|
|
155
|
+
return dispatch_hook_command(args);
|
|
120
156
|
if (cmd === "check-archived")
|
|
121
157
|
return [check_archived(change, repo_root_from_cwd()), "archive"];
|
|
122
158
|
const [status, repoRoot, changeRoot, evidences] = runtime.load_context(change);
|
package/dist/src/evidence.js
CHANGED
|
@@ -5,6 +5,7 @@ import { normalize_gate } from "./openspec.js";
|
|
|
5
5
|
import { superspec_dir } from "./paths.js";
|
|
6
6
|
import { findings_schema_reasons, review_digest_schema_reasons, standing_authorization_schema_reasons, user_decision_schema_reasons, } from "./disclosure.js";
|
|
7
7
|
import { pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, worker_test_run_reasons, } from "./apply_worker_chain.js";
|
|
8
|
+
import { strictRoleRunlogReasons, strictRuntimeEvidenceReasons } from "./hooks/validation.js";
|
|
8
9
|
function file_ref_reasons(baseRoot, ev, field, code) {
|
|
9
10
|
const problems = [];
|
|
10
11
|
const raw = ev[field];
|
|
@@ -509,6 +510,7 @@ export function validate_evidence_schema(ev, change, changeRoot, repoRoot) {
|
|
|
509
510
|
problems.push(...human_confirmation_reasons(ev));
|
|
510
511
|
if (ev.kind === "test_run")
|
|
511
512
|
problems.push(...test_run_reasons(ev, changeRoot));
|
|
513
|
+
problems.push(...strictRuntimeEvidenceReasons(ev));
|
|
512
514
|
if (ev.kind === "apply_worker_chain")
|
|
513
515
|
problems.push(...apply_worker_chain_reasons(ev, changeRoot));
|
|
514
516
|
// DISC Phase 1: disclosure evidence kinds and reviewer findings[] are schema-checked fail-closed.
|
|
@@ -556,6 +558,7 @@ export function validate_evidence_schema(ev, change, changeRoot, repoRoot) {
|
|
|
556
558
|
const targetRoot = ev.kind === "source_guidance" ? repoRoot : changeRoot;
|
|
557
559
|
problems.push(...role_target_ref_reasons(ev, targetRoot));
|
|
558
560
|
problems.push(...output_ref_target_overlap_reasons(ev, changeRoot, targetRoot));
|
|
561
|
+
problems.push(...strictRoleRunlogReasons(changeRoot, ev));
|
|
559
562
|
}
|
|
560
563
|
if (ev.kind === "source_guidance") {
|
|
561
564
|
if (!REVIEW_GUIDANCE_ROLES.includes(String(ev.agent_role))) {
|