blun-king-cli 9.1.564 → 9.1.566

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.
Files changed (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,333 +1,333 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { runAudit } from "./audit.js";
6
- import { deleteAttention, loadAttention } from "./attention.js";
7
- import { configureContinuity, purgeContinuity } from "./continuity.js";
8
- import { createTask } from "./coordination.js";
9
- import { linkEntities, upsertEntity } from "./graph.js";
10
- import { loadLearning, rollbackLearning } from "./learning.js";
11
- import { configurePreflightPolicy } from "./preflight.js";
12
- import {
13
- grantExecution, loadSelfstarter, registerJob
14
- } from "./selfstarter.js";
15
- import { runHook } from "../hook.js";
16
- import { VERSION } from "../version.js";
17
-
18
- const SCHEMA = "agentspine.acceptance/v1";
19
- const SCENARIO = "acceptance:multilingual-team-v1";
20
- const FIXED_TIME = "2031-04-05T09:00:00.000Z";
21
-
22
- function digest(value) {
23
- return createHash("sha256").update(value).digest("hex");
24
- }
25
-
26
- function packet(result) {
27
- return JSON.parse(result.context);
28
- }
29
-
30
- function requireCondition(condition, detail) {
31
- if (!condition) throw new Error(detail);
32
- }
33
-
34
- function receipt(id, evidence) {
35
- return digest(`${SCHEMA}\0${id}\0${JSON.stringify(evidence)}`);
36
- }
37
-
38
- function addCheck(checks, id, label, detail, evidence) {
39
- checks.push({ id, label, status: "PASS", detail, receipt: receipt(id, evidence) });
40
- }
41
-
42
- function sourceHashes(sources) {
43
- return Object.fromEntries(Object.entries(sources).map(([name, content]) => [name, digest(content)]));
44
- }
45
-
46
- async function currentSourceHashes(root, names) {
47
- return Object.fromEntries(await Promise.all(names.map(async (name) => [name, digest(await readFile(join(root, name)))])));
48
- }
49
-
50
- function styleClaims(context) {
51
- return context.briefing.learning.map((item) => item.claim);
52
- }
53
-
54
- function attentionKinds(context) {
55
- return context.briefing.attention.items.map((item) => item.kind);
56
- }
57
-
58
- export function renderAcceptanceReport(report) {
59
- const lines = report.checks.map((check) => `[${check.status}] ${check.label} — ${check.detail} · receipt=${check.receipt.slice(0, 16)}`);
60
- lines.push(`[PASS] Gesamt — ${report.passed}/${report.total} Gates · receipt=${report.receiptDigest}`);
61
- return `${lines.join("\n")}\n`;
62
- }
63
-
64
- export async function runVisibleAcceptance() {
65
- const projectRoot = await mkdtemp(join(tmpdir(), "agentspine-visible-acceptance-project-"));
66
- const stateRoot = await mkdtemp(join(tmpdir(), "agentspine-visible-acceptance-state-"));
67
- const previousState = process.env.AGENTSPINE_STATE_DIR;
68
- process.env.AGENTSPINE_STATE_DIR = stateRoot;
69
- const checks = [];
70
- const sources = {
71
- "AGENTS.md": "# Team rules\n\nThe current request and explicit stops win.\n",
72
- "CLAUDE.md": "# Claude host\n\nKeep the native host hierarchy.\n",
73
- "SOUL.md": "# Soul\n\nBe warm, precise, and honest.\n"
74
- };
75
- const expectedSources = sourceHashes(sources);
76
- try {
77
- for (const [name, content] of Object.entries(sources)) await writeFile(join(projectRoot, name), content, "utf8");
78
- const adapterPath = join(projectRoot, "mnemo-acceptance.mjs");
79
- const adapterSource = `let body = "";
80
- process.stdin.setEncoding("utf8");
81
- process.stdin.on("data", (chunk) => { body += chunk; });
82
- process.stdin.on("end", () => {
83
- const query = JSON.parse(body);
84
- process.stdout.write(JSON.stringify({
85
- schema: "agentspine.retrieval-result/v1", providerId: query.providerId,
86
- queryDigest: query.queryDigest, status: "ok", rejected: 1,
87
- items: [{ id: "mnemo:aurora-finding", revision: "1", claim: "Prüfe die bestehenden Findings vor neuen Änderungen.",
88
- source: "mnemo", scope: { agentId: query.agentId, userId: query.userId, tenantId: query.tenantId,
89
- projectId: query.projectId }, validity: "current", confidence: 1, whyLoaded: "exact project scope" }]
90
- }));
91
- });
92
- `;
93
- await writeFile(adapterPath, adapterSource, "utf8");
94
- await configurePreflightPolicy({ confirmation: "local-owner-confirmed", env: process.env, profile: {
95
- id: "preflight-policy:acceptance-freja", agentId: "person:freja", host: "claude", profileId: "default",
96
- tenantId: "local-tenant", enabled: true, providers: [{ schema: "agentspine.retrieval-provider/v1",
97
- id: "mnemo:acceptance", adapter: "mnemo-command/v1", required: true, failClosed: true,
98
- timeoutMs: 2000, command: process.execPath, args: [adapterPath], credentialEnv: [] }]
99
- } });
100
- const entities = [
101
- ["person:freja", "person", "Freja Åström"],
102
- ["person:lucia", "person", "Lucía Ortega"],
103
- ["group:nord", "group", "Nordgruppen"],
104
- ["group:sol", "group", "Equipo Sol"],
105
- ["project:aurora", "project", "Aurora"],
106
- ["project:brisa", "project", "Brisa"]
107
- ];
108
- for (const [id, kind, displayName] of entities) {
109
- await upsertEntity({ root: projectRoot, id, kind, displayName, privacy: "shared", confidence: 1 });
110
- }
111
- await linkEntities({ root: projectRoot, from: "person:freja", to: "group:nord", relation: "member-of", privacy: "group", confidence: 1 });
112
- await linkEntities({ root: projectRoot, from: "person:lucia", to: "group:sol", relation: "member-of", privacy: "group", confidence: 1 });
113
- for (const task of [
114
- { id: "task:aurora", actorId: "person:freja", assigneeId: "person:freja", projectId: "project:aurora", title: "Verifiera Aurora", privacy: "private" },
115
- { id: "task:brisa", actorId: "person:lucia", assigneeId: "person:lucia", projectId: "project:brisa", title: "Revisar Brisa", privacy: "private" },
116
- { id: "task:nord", actorId: "person:freja", assigneeId: "person:freja", projectId: "project:aurora", groupId: "group:nord", title: "Granska nordisk leverans", privacy: "group" },
117
- { id: "task:sol", actorId: "person:lucia", assigneeId: "person:lucia", projectId: "project:brisa", groupId: "group:sol", title: "Revisar entrega del equipo", privacy: "group" }
118
- ]) await createTask({ root: projectRoot, ...task });
119
- addCheck(checks, "identity", "Kanonische Identitäten", "Freja und Lucía sowie beide Gruppen besitzen getrennte stabile IDs.", entities.map(([id]) => id));
120
-
121
- await configureContinuity({ root: projectRoot, config: { enabled: true }, confirmation: "local-user-opt-in", now: FIXED_TIME });
122
- const swedish = await runHook({
123
- hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
124
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
125
- session_id: "session:freja:prompt", event_id: "prompt:freja:style", timestamp: FIXED_TIME,
126
- prompt: "Svara alltid kortfattat och varmt."
127
- });
128
- const spanish = await runHook({
129
- hook_event_name: "UserPromptSubmit", host: "codex", cwd: projectRoot,
130
- entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
131
- session_id: "session:lucia:prompt", event_id: "prompt:lucia:style", timestamp: FIXED_TIME,
132
- prompt: "Responde siempre de forma clara y breve."
133
- });
134
- requireCondition(swedish.signal?.accepted && spanish.signal?.accepted, "multilingual style signals were not accepted");
135
- requireCondition(swedish.preflight?.briefing.instructions.some((item) => item.content === sources["CLAUDE.md"]),
136
- "mandatory CLAUDE.md bytes were not present before the turn");
137
- requireCondition(swedish.preflight?.briefing.retrieval[0]?.items[0]?.id === "mnemo:aurora-finding",
138
- "required Mnemo retrieval was not present before the turn");
139
- await rm(adapterPath);
140
- const blockedRecall = await runHook({
141
- hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
142
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
143
- session_id: "session:freja:blocked", event_id: "prompt:freja:blocked", timestamp: "2031-04-05T09:00:30.000Z",
144
- prompt: "Det här svaret får inte skapas utan minne."
145
- });
146
- requireCondition(blockedRecall.blocked && blockedRecall.failedClosed, "missing required recall did not block the turn");
147
- await writeFile(adapterPath, adapterSource, "utf8");
148
- addCheck(checks, "pre-answer-recall", "Verpflichtender Pre-Answer-Recall",
149
- "Vollständige CLAUDE.md und Mnemo wurden vor dem Turn geladen; fehlendes Mnemo blockierte sichtbar.",
150
- [swedish.preflight.receipt.id, swedish.preflight.receipt.briefingDigest, blockedRecall.blocked]);
151
- addCheck(checks, "languages", "Mehrsprachige Stilkontinuität", "Schwedische und spanische Stilwünsche wurden nach Opt-in minimal und belegt angenommen.", [swedish.signal.learningId, spanish.signal.learningId]);
152
-
153
- await runHook({
154
- hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
155
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
156
- session_id: "session:freja:promise", event_id: "prompt:freja:promise", timestamp: "2031-04-05T09:01:00.000Z",
157
- prompt: "Jag lovar att kontrollera den synliga leveransen."
158
- });
159
- await runHook({
160
- hook_event_name: "PostToolUse", host: "claude", cwd: projectRoot,
161
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
162
- session_id: "session:freja:heartbeat", tool_use_id: "tool:heartbeat:freja", timestamp: "2031-04-05T09:02:00.000Z"
163
- });
164
- await runHook({
165
- hook_event_name: "PostToolUse", host: "codex", cwd: projectRoot,
166
- entity_id: "person:freja", group_id: "group:nord", project_id: "project:aurora", task_id: "task:nord",
167
- session_id: "session:nord:blocker", tool_use_id: "tool:blocker:nord", timestamp: "2031-04-05T09:03:00.000Z",
168
- agent_spine_attention: { id: "event:blocker:nord", kind: "blocker", summary: "Blockerad av den syntetiska granskningen.", privacy: "group" }
169
- });
170
- const attentionState = (await loadAttention(projectRoot)).attention;
171
- requireCondition(new Set(attentionState.events.map((item) => item.kind)).size === 3, "heartbeat, promise, and blocker were not all retained");
172
- addCheck(checks, "attention", "Heartbeat, Zusage und Blocker", "Alle drei Ereignisarten sind dauerhaft, dedupliziert und mit Provenienz gespeichert.", attentionState.events.map((item) => [item.id, item.kind, item.provenance.digest]));
173
-
174
- const frejaRestart = packet(await runHook({
175
- hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
176
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
177
- session_id: "session:freja:restart", timestamp: "2031-04-05T09:04:00.000Z"
178
- }));
179
- const luciaCompact = packet(await runHook({
180
- hook_event_name: "PostCompact", host: "codex", cwd: projectRoot,
181
- entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
182
- session_id: "session:lucia:compact", timestamp: "2031-04-05T09:04:00.000Z"
183
- }));
184
- requireCondition(styleClaims(frejaRestart).some((claim) => claim.includes("kortfattat och varmt")), "Swedish style did not survive restart");
185
- requireCondition(styleClaims(luciaCompact).some((claim) => claim.includes("clara y breve")), "Spanish style did not survive compaction");
186
- requireCondition(attentionKinds(frejaRestart).includes("promise"), "promise did not survive restart");
187
- addCheck(checks, "restart", "Echter Neustart", "Claude erhielt Frejas Stil und Zusage automatisch ohne MCP-Aufruf.", [frejaRestart.event, styleClaims(frejaRestart), attentionKinds(frejaRestart)]);
188
- addCheck(checks, "compaction", "Echte Kompaktierungsgrenze", "Codex erhielt Lucías spanischen Stil automatisch nach PostCompact.", [luciaCompact.event, styleClaims(luciaCompact)]);
189
-
190
- const wrongPerson = packet(await runHook({
191
- hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
192
- entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
193
- session_id: "session:wrong-person", timestamp: "2031-04-05T09:05:00.000Z"
194
- }));
195
- const correctGroup = packet(await runHook({
196
- hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
197
- entity_id: "person:freja", group_id: "group:nord", project_id: "project:aurora", task_id: "task:nord",
198
- session_id: "session:group:nord", timestamp: "2031-04-05T09:05:00.000Z"
199
- }));
200
- const wrongGroup = packet(await runHook({
201
- hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
202
- entity_id: "person:lucia", group_id: "group:sol", project_id: "project:brisa", task_id: "task:sol",
203
- session_id: "session:group:sol", timestamp: "2031-04-05T09:05:00.000Z"
204
- }));
205
- requireCondition(!styleClaims(wrongPerson).some((claim) => claim.includes("kortfattat")), "person isolation leaked Freja's style");
206
- requireCondition(correctGroup.briefing.attention.items.some((item) => item.key === "event:event:blocker:nord"), "exact group did not receive its blocker");
207
- requireCondition(!wrongGroup.briefing.attention.items.some((item) => item.key === "event:event:blocker:nord"), "group isolation leaked the Nordic blocker");
208
- addCheck(checks, "isolation", "Personen- und Gruppenisolation", "Fremde Personen und Equipo Sol sahen weder Frejas Stil noch den Nordgruppen-Blocker.", [styleClaims(wrongPerson), correctGroup.briefing.attention.items.map((item) => item.key), wrongGroup.briefing.attention.items.map((item) => item.key)]);
209
-
210
- const correction = await runHook({
211
- hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
212
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
213
- session_id: "session:freja:correction", event_id: "prompt:freja:correction", timestamp: "2031-04-05T09:06:00.000Z",
214
- prompt: "Korrigering: Vid säkerhetsfrågor ska svaret vara utförligt."
215
- });
216
- const corrected = packet(await runHook({
217
- hook_event_name: "PostCompact", host: "claude", cwd: projectRoot,
218
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
219
- session_id: "session:freja:corrected", timestamp: "2031-04-05T09:07:00.000Z"
220
- }));
221
- requireCondition(styleClaims(corrected).some((claim) => claim.includes("säkerhetsfrågor")), "correction was not applied after compaction");
222
- await rollbackLearning({ root: projectRoot, id: correction.signal.learningId, reason: "Reproduzierbarer synthetischer Rollback.", now: "2031-04-05T09:08:00.000Z" });
223
- const rolledBack = packet(await runHook({
224
- hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
225
- entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
226
- session_id: "session:freja:rollback", timestamp: "2031-04-05T09:09:00.000Z"
227
- }));
228
- requireCondition(!styleClaims(rolledBack).some((claim) => claim.includes("säkerhetsfrågor")), "rolled-back correction remained active");
229
- requireCondition(styleClaims(rolledBack).some((claim) => claim.includes("kortfattat")), "prior style was not restored after rollback");
230
- addCheck(checks, "correction", "Korrektur und Historie", "Die schwedische Korrektur wirkte nach Kompaktierung und ersetzte keine frühere Antwort oder Historie.", correction.signal.learningId);
231
- addCheck(checks, "rollback", "Atomarer Rollback", "Rollback entfernte die Korrektur aus aktivem Kontext und stellte den vorherigen Stil wieder her.", styleClaims(rolledBack));
232
-
233
- await grantExecution({
234
- root: projectRoot, id: "execution-grant:acceptance", jobId: "job:acceptance", actorId: "person:freja",
235
- taskId: "task:aurora", targetId: "person:lucia", projectId: "project:aurora", host: "claude",
236
- capabilities: ["tool:Write"], reason: "Owner approved one exact synthetic acceptance artifact.",
237
- confirmation: "local-owner-confirmed", now: "2031-04-05T09:10:00.000Z"
238
- });
239
- await registerJob({
240
- root: projectRoot, id: "job:acceptance", grantId: "execution-grant:acceptance",
241
- confirmation: "local-owner-confirmed", now: "2031-04-05T09:10:01.000Z"
242
- });
243
- const executionScope = {
244
- cwd: projectRoot, host: "claude", entity_id: "person:freja",
245
- project_id: "project:aurora", task_id: "task:aurora"
246
- };
247
- const started = packet(await runHook({
248
- ...executionScope, hook_event_name: "SessionStart", session_id: "session:job:one", timestamp: "2031-04-05T09:10:02.000Z"
249
- }));
250
- requireCondition(started.selfstarter?.action === "start", "authorized job did not start");
251
- const authorized = await runHook({
252
- ...executionScope, hook_event_name: "PreToolUse", session_id: "session:job:one",
253
- tool_name: "Write", tool_use_id: "tool:acceptance:one", timestamp: "2031-04-05T09:10:03.000Z",
254
- tool_input: { file_path: join(projectRoot, "acceptance-output.txt"), content: "checkpointed\n" }
255
- });
256
- requireCondition(!authorized.blocked && authorized.selfstarter?.allowed, "current exact effect was denied");
257
- await writeFile(join(projectRoot, "acceptance-output.txt"), "checkpointed\n", "utf8");
258
- await runHook({
259
- ...executionScope, hook_event_name: "PostToolUse", session_id: "session:job:one",
260
- tool_name: "Write", tool_use_id: "tool:acceptance:one", timestamp: "2031-04-05T09:10:04.000Z",
261
- success: true, tool_result: { ok: true }
262
- });
263
- await runHook({
264
- ...executionScope, hook_event_name: "Stop", session_id: "session:job:one", event_id: "stop:job:one",
265
- timestamp: "2031-04-05T09:10:05.000Z"
266
- });
267
- const resumed = packet(await runHook({
268
- ...executionScope, hook_event_name: "SessionStart", session_id: "session:job:two", timestamp: "2031-04-05T09:10:06.000Z"
269
- }));
270
- requireCondition(resumed.selfstarter?.action === "resume" && resumed.selfstarter.checkpointSequence === 1, "durable checkpoint did not resume");
271
- const denied = await runHook({
272
- ...executionScope, hook_event_name: "PreToolUse", entity_id: "person:lucia", session_id: "session:job:two",
273
- tool_name: "Write", tool_use_id: "tool:acceptance:denied", timestamp: "2031-04-05T09:10:07.000Z",
274
- tool_input: { file_path: join(projectRoot, "denied.txt"), content: "must not exist\n" }
275
- });
276
- requireCondition(denied.blocked && /actor scope mismatch/.test(denied.reason), "wrong actor was not visibly denied");
277
- await runHook({
278
- ...executionScope, hook_event_name: "Stop", session_id: "session:job:two", event_id: "stop:job:two",
279
- timestamp: "2031-04-05T09:10:08.000Z"
280
- });
281
- const durable = (await loadSelfstarter(projectRoot)).state;
282
- const job = durable.jobs.find((item) => item.id === "job:acceptance");
283
- requireCondition(job.checkpoint.sequence === 1 && durable.receipts.some((item) => item.event === "effect-succeeded"), "checkpoint receipt was not durable");
284
- addCheck(checks, "authorized-resume", "Berechtigtes Resume", "Der zweite Claude-Hoststart nahm Checkpoint 1 ohne MCP-Aufruf wieder auf.", [resumed.selfstarter.receiptId, job.checkpoint.workspaceDigest]);
285
- addCheck(checks, "denied-resume", "Verweigerte Fremdwirkung", "Lucía konnte Frejas laufende Lease trotz bekannter Aufgabe nicht verwenden.", denied.reason);
286
- addCheck(checks, "checkpoint", "Dauerhaftes Checkpointing", "Effekt, Ergebnis-Digest, Stop und Resume besitzen idempotente externe Receipts.", durable.receipts.map((item) => [item.id, item.event, item.digest]));
287
-
288
- await purgeContinuity({ root: projectRoot, subjectId: "person:lucia", confirmation: "local-user-confirmed", now: "2031-04-05T09:11:00.000Z" });
289
- await deleteAttention({ root: projectRoot, entityId: "person:lucia" });
290
- const purged = packet(await runHook({
291
- hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
292
- entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
293
- session_id: "session:lucia:purged", timestamp: "2031-04-05T09:12:00.000Z"
294
- }));
295
- const remainingLearning = (await loadLearning(projectRoot)).learning.candidates.filter((item) => item.subjectId === "person:lucia");
296
- requireCondition(remainingLearning.length === 0 && styleClaims(purged).length === 0, "person purge left accepted context behind");
297
- addCheck(checks, "purge", "Vollständiger Personen-Purge", "Lucías Stil-, Signal- und Aufmerksamkeitszustand blieb nach neuem Codex-Start gelöscht.", [remainingLearning.length, styleClaims(purged).length]);
298
-
299
- const actualSources = await currentSourceHashes(projectRoot, Object.keys(sources));
300
- requireCondition(JSON.stringify(actualSources) === JSON.stringify(expectedSources), "protected source bytes changed during acceptance");
301
- requireCondition((await readFile(join(projectRoot, "acceptance-output.txt"), "utf8")) === "checkpointed\n", "authorized checkpoint artifact is missing");
302
- let deniedExists = true;
303
- try { await readFile(join(projectRoot, "denied.txt")); } catch (error) { if (error.code === "ENOENT") deniedExists = false; else throw error; }
304
- requireCondition(!deniedExists, "denied effect changed the workspace");
305
- addCheck(checks, "source-bytes", "Bytegenaue Quellerhaltung", "AGENTS.md, CLAUDE.md und SOUL.md blieben über alle Hooks unverändert.", actualSources);
306
-
307
- const audit = await runAudit(projectRoot);
308
- requireCondition(audit.ok && audit.passed === audit.total, "final ten-gate audit failed");
309
- addCheck(checks, "audit", "Abschluss-Audit", "Alle zehn Sicherheits-, Privacy-, Authority- und Erhaltungsgates sind grün.", [audit.passed, audit.total]);
310
-
311
- const report = {
312
- schema: SCHEMA,
313
- version: VERSION,
314
- scenarioId: SCENARIO,
315
- ok: true,
316
- passed: checks.length,
317
- total: checks.length,
318
- hosts: ["claude", "codex"],
319
- languages: ["sv-SE", "es-ES"],
320
- mcpCalls: 0,
321
- checks,
322
- sourceHashes: actualSources,
323
- receiptDigest: digest(checks.map((check) => `${check.id}:${check.receipt}`).join("\n")),
324
- authority: "acceptance-evidence-only"
325
- };
326
- return report;
327
- } finally {
328
- if (previousState === undefined) delete process.env.AGENTSPINE_STATE_DIR;
329
- else process.env.AGENTSPINE_STATE_DIR = previousState;
330
- await rm(projectRoot, { recursive: true, force: true });
331
- await rm(stateRoot, { recursive: true, force: true });
332
- }
333
- }
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runAudit } from "./audit.js";
6
+ import { deleteAttention, loadAttention } from "./attention.js";
7
+ import { configureContinuity, purgeContinuity } from "./continuity.js";
8
+ import { createTask } from "./coordination.js";
9
+ import { linkEntities, upsertEntity } from "./graph.js";
10
+ import { loadLearning, rollbackLearning } from "./learning.js";
11
+ import { configurePreflightPolicy } from "./preflight.js";
12
+ import {
13
+ grantExecution, loadSelfstarter, registerJob
14
+ } from "./selfstarter.js";
15
+ import { runHook } from "../hook.js";
16
+ import { VERSION } from "../version.js";
17
+
18
+ const SCHEMA = "agentspine.acceptance/v1";
19
+ const SCENARIO = "acceptance:multilingual-team-v1";
20
+ const FIXED_TIME = "2031-04-05T09:00:00.000Z";
21
+
22
+ function digest(value) {
23
+ return createHash("sha256").update(value).digest("hex");
24
+ }
25
+
26
+ function packet(result) {
27
+ return JSON.parse(result.context);
28
+ }
29
+
30
+ function requireCondition(condition, detail) {
31
+ if (!condition) throw new Error(detail);
32
+ }
33
+
34
+ function receipt(id, evidence) {
35
+ return digest(`${SCHEMA}\0${id}\0${JSON.stringify(evidence)}`);
36
+ }
37
+
38
+ function addCheck(checks, id, label, detail, evidence) {
39
+ checks.push({ id, label, status: "PASS", detail, receipt: receipt(id, evidence) });
40
+ }
41
+
42
+ function sourceHashes(sources) {
43
+ return Object.fromEntries(Object.entries(sources).map(([name, content]) => [name, digest(content)]));
44
+ }
45
+
46
+ async function currentSourceHashes(root, names) {
47
+ return Object.fromEntries(await Promise.all(names.map(async (name) => [name, digest(await readFile(join(root, name)))])));
48
+ }
49
+
50
+ function styleClaims(context) {
51
+ return context.briefing.learning.map((item) => item.claim);
52
+ }
53
+
54
+ function attentionKinds(context) {
55
+ return context.briefing.attention.items.map((item) => item.kind);
56
+ }
57
+
58
+ export function renderAcceptanceReport(report) {
59
+ const lines = report.checks.map((check) => `[${check.status}] ${check.label} — ${check.detail} · receipt=${check.receipt.slice(0, 16)}`);
60
+ lines.push(`[PASS] Gesamt — ${report.passed}/${report.total} Gates · receipt=${report.receiptDigest}`);
61
+ return `${lines.join("\n")}\n`;
62
+ }
63
+
64
+ export async function runVisibleAcceptance() {
65
+ const projectRoot = await mkdtemp(join(tmpdir(), "agentspine-visible-acceptance-project-"));
66
+ const stateRoot = await mkdtemp(join(tmpdir(), "agentspine-visible-acceptance-state-"));
67
+ const previousState = process.env.AGENTSPINE_STATE_DIR;
68
+ process.env.AGENTSPINE_STATE_DIR = stateRoot;
69
+ const checks = [];
70
+ const sources = {
71
+ "AGENTS.md": "# Team rules\n\nThe current request and explicit stops win.\n",
72
+ "CLAUDE.md": "# Claude host\n\nKeep the native host hierarchy.\n",
73
+ "SOUL.md": "# Soul\n\nBe warm, precise, and honest.\n"
74
+ };
75
+ const expectedSources = sourceHashes(sources);
76
+ try {
77
+ for (const [name, content] of Object.entries(sources)) await writeFile(join(projectRoot, name), content, "utf8");
78
+ const adapterPath = join(projectRoot, "mnemo-acceptance.mjs");
79
+ const adapterSource = `let body = "";
80
+ process.stdin.setEncoding("utf8");
81
+ process.stdin.on("data", (chunk) => { body += chunk; });
82
+ process.stdin.on("end", () => {
83
+ const query = JSON.parse(body);
84
+ process.stdout.write(JSON.stringify({
85
+ schema: "agentspine.retrieval-result/v1", providerId: query.providerId,
86
+ queryDigest: query.queryDigest, status: "ok", rejected: 1,
87
+ items: [{ id: "mnemo:aurora-finding", revision: "1", claim: "Prüfe die bestehenden Findings vor neuen Änderungen.",
88
+ source: "mnemo", scope: { agentId: query.agentId, userId: query.userId, tenantId: query.tenantId,
89
+ projectId: query.projectId }, validity: "current", confidence: 1, whyLoaded: "exact project scope" }]
90
+ }));
91
+ });
92
+ `;
93
+ await writeFile(adapterPath, adapterSource, "utf8");
94
+ await configurePreflightPolicy({ confirmation: "local-owner-confirmed", env: process.env, profile: {
95
+ id: "preflight-policy:acceptance-freja", agentId: "person:freja", host: "claude", profileId: "default",
96
+ tenantId: "local-tenant", enabled: true, providers: [{ schema: "agentspine.retrieval-provider/v1",
97
+ id: "mnemo:acceptance", adapter: "mnemo-command/v1", required: true, failClosed: true,
98
+ timeoutMs: 2000, command: process.execPath, args: [adapterPath], credentialEnv: [] }]
99
+ } });
100
+ const entities = [
101
+ ["person:freja", "person", "Freja Åström"],
102
+ ["person:lucia", "person", "Lucía Ortega"],
103
+ ["group:nord", "group", "Nordgruppen"],
104
+ ["group:sol", "group", "Equipo Sol"],
105
+ ["project:aurora", "project", "Aurora"],
106
+ ["project:brisa", "project", "Brisa"]
107
+ ];
108
+ for (const [id, kind, displayName] of entities) {
109
+ await upsertEntity({ root: projectRoot, id, kind, displayName, privacy: "shared", confidence: 1 });
110
+ }
111
+ await linkEntities({ root: projectRoot, from: "person:freja", to: "group:nord", relation: "member-of", privacy: "group", confidence: 1 });
112
+ await linkEntities({ root: projectRoot, from: "person:lucia", to: "group:sol", relation: "member-of", privacy: "group", confidence: 1 });
113
+ for (const task of [
114
+ { id: "task:aurora", actorId: "person:freja", assigneeId: "person:freja", projectId: "project:aurora", title: "Verifiera Aurora", privacy: "private" },
115
+ { id: "task:brisa", actorId: "person:lucia", assigneeId: "person:lucia", projectId: "project:brisa", title: "Revisar Brisa", privacy: "private" },
116
+ { id: "task:nord", actorId: "person:freja", assigneeId: "person:freja", projectId: "project:aurora", groupId: "group:nord", title: "Granska nordisk leverans", privacy: "group" },
117
+ { id: "task:sol", actorId: "person:lucia", assigneeId: "person:lucia", projectId: "project:brisa", groupId: "group:sol", title: "Revisar entrega del equipo", privacy: "group" }
118
+ ]) await createTask({ root: projectRoot, ...task });
119
+ addCheck(checks, "identity", "Kanonische Identitäten", "Freja und Lucía sowie beide Gruppen besitzen getrennte stabile IDs.", entities.map(([id]) => id));
120
+
121
+ await configureContinuity({ root: projectRoot, config: { enabled: true }, confirmation: "local-user-opt-in", now: FIXED_TIME });
122
+ const swedish = await runHook({
123
+ hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
124
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
125
+ session_id: "session:freja:prompt", event_id: "prompt:freja:style", timestamp: FIXED_TIME,
126
+ prompt: "Svara alltid kortfattat och varmt."
127
+ });
128
+ const spanish = await runHook({
129
+ hook_event_name: "UserPromptSubmit", host: "codex", cwd: projectRoot,
130
+ entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
131
+ session_id: "session:lucia:prompt", event_id: "prompt:lucia:style", timestamp: FIXED_TIME,
132
+ prompt: "Responde siempre de forma clara y breve."
133
+ });
134
+ requireCondition(swedish.signal?.accepted && spanish.signal?.accepted, "multilingual style signals were not accepted");
135
+ requireCondition(swedish.preflight?.briefing.instructions.some((item) => item.content === sources["CLAUDE.md"]),
136
+ "mandatory CLAUDE.md bytes were not present before the turn");
137
+ requireCondition(swedish.preflight?.briefing.retrieval[0]?.items[0]?.id === "mnemo:aurora-finding",
138
+ "required Mnemo retrieval was not present before the turn");
139
+ await rm(adapterPath);
140
+ const blockedRecall = await runHook({
141
+ hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
142
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
143
+ session_id: "session:freja:blocked", event_id: "prompt:freja:blocked", timestamp: "2031-04-05T09:00:30.000Z",
144
+ prompt: "Det här svaret får inte skapas utan minne."
145
+ });
146
+ requireCondition(blockedRecall.blocked && blockedRecall.failedClosed, "missing required recall did not block the turn");
147
+ await writeFile(adapterPath, adapterSource, "utf8");
148
+ addCheck(checks, "pre-answer-recall", "Verpflichtender Pre-Answer-Recall",
149
+ "Vollständige CLAUDE.md und Mnemo wurden vor dem Turn geladen; fehlendes Mnemo blockierte sichtbar.",
150
+ [swedish.preflight.receipt.id, swedish.preflight.receipt.briefingDigest, blockedRecall.blocked]);
151
+ addCheck(checks, "languages", "Mehrsprachige Stilkontinuität", "Schwedische und spanische Stilwünsche wurden nach Opt-in minimal und belegt angenommen.", [swedish.signal.learningId, spanish.signal.learningId]);
152
+
153
+ await runHook({
154
+ hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
155
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
156
+ session_id: "session:freja:promise", event_id: "prompt:freja:promise", timestamp: "2031-04-05T09:01:00.000Z",
157
+ prompt: "Jag lovar att kontrollera den synliga leveransen."
158
+ });
159
+ await runHook({
160
+ hook_event_name: "PostToolUse", host: "claude", cwd: projectRoot,
161
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
162
+ session_id: "session:freja:heartbeat", tool_use_id: "tool:heartbeat:freja", timestamp: "2031-04-05T09:02:00.000Z"
163
+ });
164
+ await runHook({
165
+ hook_event_name: "PostToolUse", host: "codex", cwd: projectRoot,
166
+ entity_id: "person:freja", group_id: "group:nord", project_id: "project:aurora", task_id: "task:nord",
167
+ session_id: "session:nord:blocker", tool_use_id: "tool:blocker:nord", timestamp: "2031-04-05T09:03:00.000Z",
168
+ agent_spine_attention: { id: "event:blocker:nord", kind: "blocker", summary: "Blockerad av den syntetiska granskningen.", privacy: "group" }
169
+ });
170
+ const attentionState = (await loadAttention(projectRoot)).attention;
171
+ requireCondition(new Set(attentionState.events.map((item) => item.kind)).size === 3, "heartbeat, promise, and blocker were not all retained");
172
+ addCheck(checks, "attention", "Heartbeat, Zusage und Blocker", "Alle drei Ereignisarten sind dauerhaft, dedupliziert und mit Provenienz gespeichert.", attentionState.events.map((item) => [item.id, item.kind, item.provenance.digest]));
173
+
174
+ const frejaRestart = packet(await runHook({
175
+ hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
176
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
177
+ session_id: "session:freja:restart", timestamp: "2031-04-05T09:04:00.000Z"
178
+ }));
179
+ const luciaCompact = packet(await runHook({
180
+ hook_event_name: "PostCompact", host: "codex", cwd: projectRoot,
181
+ entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
182
+ session_id: "session:lucia:compact", timestamp: "2031-04-05T09:04:00.000Z"
183
+ }));
184
+ requireCondition(styleClaims(frejaRestart).some((claim) => claim.includes("kortfattat och varmt")), "Swedish style did not survive restart");
185
+ requireCondition(styleClaims(luciaCompact).some((claim) => claim.includes("clara y breve")), "Spanish style did not survive compaction");
186
+ requireCondition(attentionKinds(frejaRestart).includes("promise"), "promise did not survive restart");
187
+ addCheck(checks, "restart", "Echter Neustart", "Claude erhielt Frejas Stil und Zusage automatisch ohne MCP-Aufruf.", [frejaRestart.event, styleClaims(frejaRestart), attentionKinds(frejaRestart)]);
188
+ addCheck(checks, "compaction", "Echte Kompaktierungsgrenze", "Codex erhielt Lucías spanischen Stil automatisch nach PostCompact.", [luciaCompact.event, styleClaims(luciaCompact)]);
189
+
190
+ const wrongPerson = packet(await runHook({
191
+ hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
192
+ entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
193
+ session_id: "session:wrong-person", timestamp: "2031-04-05T09:05:00.000Z"
194
+ }));
195
+ const correctGroup = packet(await runHook({
196
+ hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
197
+ entity_id: "person:freja", group_id: "group:nord", project_id: "project:aurora", task_id: "task:nord",
198
+ session_id: "session:group:nord", timestamp: "2031-04-05T09:05:00.000Z"
199
+ }));
200
+ const wrongGroup = packet(await runHook({
201
+ hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
202
+ entity_id: "person:lucia", group_id: "group:sol", project_id: "project:brisa", task_id: "task:sol",
203
+ session_id: "session:group:sol", timestamp: "2031-04-05T09:05:00.000Z"
204
+ }));
205
+ requireCondition(!styleClaims(wrongPerson).some((claim) => claim.includes("kortfattat")), "person isolation leaked Freja's style");
206
+ requireCondition(correctGroup.briefing.attention.items.some((item) => item.key === "event:event:blocker:nord"), "exact group did not receive its blocker");
207
+ requireCondition(!wrongGroup.briefing.attention.items.some((item) => item.key === "event:event:blocker:nord"), "group isolation leaked the Nordic blocker");
208
+ addCheck(checks, "isolation", "Personen- und Gruppenisolation", "Fremde Personen und Equipo Sol sahen weder Frejas Stil noch den Nordgruppen-Blocker.", [styleClaims(wrongPerson), correctGroup.briefing.attention.items.map((item) => item.key), wrongGroup.briefing.attention.items.map((item) => item.key)]);
209
+
210
+ const correction = await runHook({
211
+ hook_event_name: "UserPromptSubmit", host: "claude", cwd: projectRoot,
212
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
213
+ session_id: "session:freja:correction", event_id: "prompt:freja:correction", timestamp: "2031-04-05T09:06:00.000Z",
214
+ prompt: "Korrigering: Vid säkerhetsfrågor ska svaret vara utförligt."
215
+ });
216
+ const corrected = packet(await runHook({
217
+ hook_event_name: "PostCompact", host: "claude", cwd: projectRoot,
218
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
219
+ session_id: "session:freja:corrected", timestamp: "2031-04-05T09:07:00.000Z"
220
+ }));
221
+ requireCondition(styleClaims(corrected).some((claim) => claim.includes("säkerhetsfrågor")), "correction was not applied after compaction");
222
+ await rollbackLearning({ root: projectRoot, id: correction.signal.learningId, reason: "Reproduzierbarer synthetischer Rollback.", now: "2031-04-05T09:08:00.000Z" });
223
+ const rolledBack = packet(await runHook({
224
+ hook_event_name: "SessionStart", host: "claude", cwd: projectRoot,
225
+ entity_id: "person:freja", project_id: "project:aurora", task_id: "task:aurora",
226
+ session_id: "session:freja:rollback", timestamp: "2031-04-05T09:09:00.000Z"
227
+ }));
228
+ requireCondition(!styleClaims(rolledBack).some((claim) => claim.includes("säkerhetsfrågor")), "rolled-back correction remained active");
229
+ requireCondition(styleClaims(rolledBack).some((claim) => claim.includes("kortfattat")), "prior style was not restored after rollback");
230
+ addCheck(checks, "correction", "Korrektur und Historie", "Die schwedische Korrektur wirkte nach Kompaktierung und ersetzte keine frühere Antwort oder Historie.", correction.signal.learningId);
231
+ addCheck(checks, "rollback", "Atomarer Rollback", "Rollback entfernte die Korrektur aus aktivem Kontext und stellte den vorherigen Stil wieder her.", styleClaims(rolledBack));
232
+
233
+ await grantExecution({
234
+ root: projectRoot, id: "execution-grant:acceptance", jobId: "job:acceptance", actorId: "person:freja",
235
+ taskId: "task:aurora", targetId: "person:lucia", projectId: "project:aurora", host: "claude",
236
+ capabilities: ["tool:Write"], reason: "Owner approved one exact synthetic acceptance artifact.",
237
+ confirmation: "local-owner-confirmed", now: "2031-04-05T09:10:00.000Z"
238
+ });
239
+ await registerJob({
240
+ root: projectRoot, id: "job:acceptance", grantId: "execution-grant:acceptance",
241
+ confirmation: "local-owner-confirmed", now: "2031-04-05T09:10:01.000Z"
242
+ });
243
+ const executionScope = {
244
+ cwd: projectRoot, host: "claude", entity_id: "person:freja",
245
+ project_id: "project:aurora", task_id: "task:aurora"
246
+ };
247
+ const started = packet(await runHook({
248
+ ...executionScope, hook_event_name: "SessionStart", session_id: "session:job:one", timestamp: "2031-04-05T09:10:02.000Z"
249
+ }));
250
+ requireCondition(started.selfstarter?.action === "start", "authorized job did not start");
251
+ const authorized = await runHook({
252
+ ...executionScope, hook_event_name: "PreToolUse", session_id: "session:job:one",
253
+ tool_name: "Write", tool_use_id: "tool:acceptance:one", timestamp: "2031-04-05T09:10:03.000Z",
254
+ tool_input: { file_path: join(projectRoot, "acceptance-output.txt"), content: "checkpointed\n" }
255
+ });
256
+ requireCondition(!authorized.blocked && authorized.selfstarter?.allowed, "current exact effect was denied");
257
+ await writeFile(join(projectRoot, "acceptance-output.txt"), "checkpointed\n", "utf8");
258
+ await runHook({
259
+ ...executionScope, hook_event_name: "PostToolUse", session_id: "session:job:one",
260
+ tool_name: "Write", tool_use_id: "tool:acceptance:one", timestamp: "2031-04-05T09:10:04.000Z",
261
+ success: true, tool_result: { ok: true }
262
+ });
263
+ await runHook({
264
+ ...executionScope, hook_event_name: "Stop", session_id: "session:job:one", event_id: "stop:job:one",
265
+ timestamp: "2031-04-05T09:10:05.000Z"
266
+ });
267
+ const resumed = packet(await runHook({
268
+ ...executionScope, hook_event_name: "SessionStart", session_id: "session:job:two", timestamp: "2031-04-05T09:10:06.000Z"
269
+ }));
270
+ requireCondition(resumed.selfstarter?.action === "resume" && resumed.selfstarter.checkpointSequence === 1, "durable checkpoint did not resume");
271
+ const denied = await runHook({
272
+ ...executionScope, hook_event_name: "PreToolUse", entity_id: "person:lucia", session_id: "session:job:two",
273
+ tool_name: "Write", tool_use_id: "tool:acceptance:denied", timestamp: "2031-04-05T09:10:07.000Z",
274
+ tool_input: { file_path: join(projectRoot, "denied.txt"), content: "must not exist\n" }
275
+ });
276
+ requireCondition(denied.blocked && /actor scope mismatch/.test(denied.reason), "wrong actor was not visibly denied");
277
+ await runHook({
278
+ ...executionScope, hook_event_name: "Stop", session_id: "session:job:two", event_id: "stop:job:two",
279
+ timestamp: "2031-04-05T09:10:08.000Z"
280
+ });
281
+ const durable = (await loadSelfstarter(projectRoot)).state;
282
+ const job = durable.jobs.find((item) => item.id === "job:acceptance");
283
+ requireCondition(job.checkpoint.sequence === 1 && durable.receipts.some((item) => item.event === "effect-succeeded"), "checkpoint receipt was not durable");
284
+ addCheck(checks, "authorized-resume", "Berechtigtes Resume", "Der zweite Claude-Hoststart nahm Checkpoint 1 ohne MCP-Aufruf wieder auf.", [resumed.selfstarter.receiptId, job.checkpoint.workspaceDigest]);
285
+ addCheck(checks, "denied-resume", "Verweigerte Fremdwirkung", "Lucía konnte Frejas laufende Lease trotz bekannter Aufgabe nicht verwenden.", denied.reason);
286
+ addCheck(checks, "checkpoint", "Dauerhaftes Checkpointing", "Effekt, Ergebnis-Digest, Stop und Resume besitzen idempotente externe Receipts.", durable.receipts.map((item) => [item.id, item.event, item.digest]));
287
+
288
+ await purgeContinuity({ root: projectRoot, subjectId: "person:lucia", confirmation: "local-user-confirmed", now: "2031-04-05T09:11:00.000Z" });
289
+ await deleteAttention({ root: projectRoot, entityId: "person:lucia" });
290
+ const purged = packet(await runHook({
291
+ hook_event_name: "SessionStart", host: "codex", cwd: projectRoot,
292
+ entity_id: "person:lucia", project_id: "project:brisa", task_id: "task:brisa",
293
+ session_id: "session:lucia:purged", timestamp: "2031-04-05T09:12:00.000Z"
294
+ }));
295
+ const remainingLearning = (await loadLearning(projectRoot)).learning.candidates.filter((item) => item.subjectId === "person:lucia");
296
+ requireCondition(remainingLearning.length === 0 && styleClaims(purged).length === 0, "person purge left accepted context behind");
297
+ addCheck(checks, "purge", "Vollständiger Personen-Purge", "Lucías Stil-, Signal- und Aufmerksamkeitszustand blieb nach neuem Codex-Start gelöscht.", [remainingLearning.length, styleClaims(purged).length]);
298
+
299
+ const actualSources = await currentSourceHashes(projectRoot, Object.keys(sources));
300
+ requireCondition(JSON.stringify(actualSources) === JSON.stringify(expectedSources), "protected source bytes changed during acceptance");
301
+ requireCondition((await readFile(join(projectRoot, "acceptance-output.txt"), "utf8")) === "checkpointed\n", "authorized checkpoint artifact is missing");
302
+ let deniedExists = true;
303
+ try { await readFile(join(projectRoot, "denied.txt")); } catch (error) { if (error.code === "ENOENT") deniedExists = false; else throw error; }
304
+ requireCondition(!deniedExists, "denied effect changed the workspace");
305
+ addCheck(checks, "source-bytes", "Bytegenaue Quellerhaltung", "AGENTS.md, CLAUDE.md und SOUL.md blieben über alle Hooks unverändert.", actualSources);
306
+
307
+ const audit = await runAudit(projectRoot);
308
+ requireCondition(audit.ok && audit.passed === audit.total, "final ten-gate audit failed");
309
+ addCheck(checks, "audit", "Abschluss-Audit", "Alle zehn Sicherheits-, Privacy-, Authority- und Erhaltungsgates sind grün.", [audit.passed, audit.total]);
310
+
311
+ const report = {
312
+ schema: SCHEMA,
313
+ version: VERSION,
314
+ scenarioId: SCENARIO,
315
+ ok: true,
316
+ passed: checks.length,
317
+ total: checks.length,
318
+ hosts: ["claude", "codex"],
319
+ languages: ["sv-SE", "es-ES"],
320
+ mcpCalls: 0,
321
+ checks,
322
+ sourceHashes: actualSources,
323
+ receiptDigest: digest(checks.map((check) => `${check.id}:${check.receipt}`).join("\n")),
324
+ authority: "acceptance-evidence-only"
325
+ };
326
+ return report;
327
+ } finally {
328
+ if (previousState === undefined) delete process.env.AGENTSPINE_STATE_DIR;
329
+ else process.env.AGENTSPINE_STATE_DIR = previousState;
330
+ await rm(projectRoot, { recursive: true, force: true });
331
+ await rm(stateRoot, { recursive: true, force: true });
332
+ }
333
+ }