blun-king-cli 9.1.563 → 9.1.565
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/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- 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
|
+
}
|