feat: register deterministic HoloLake stage relay
This commit is contained in:
parent
34cb59739b
commit
5f86116303
11 changed files with 1065 additions and 4 deletions
32
deployment/receipts/HLP-STAGE-RELAY-001-20260811.json
Normal file
32
deployment/receipts/HLP-STAGE-RELAY-001-20260811.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"schema": "hololake.stage-relay-registration-receipt/v1",
|
||||
"receipt_id": "HLP-STAGE-RELAY-001-20260811",
|
||||
"development_id": "DEV-20260811-008",
|
||||
"record_id": "HLP-STAGE-RELAY-001",
|
||||
"state": "SOURCE_IMPLEMENTED_TESTED_AND_REGISTERED_NO_SUCCESSOR_THREAD_CREATED_DURING_ACCEPTANCE",
|
||||
"source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs",
|
||||
"runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs",
|
||||
"formal_host_capabilities": [
|
||||
"codex_app.create_thread",
|
||||
"codex_app.list_threads",
|
||||
"codex_app.automation_update"
|
||||
],
|
||||
"acceptance": {
|
||||
"finalizer_evidence_gate": 100,
|
||||
"duplicate_prepare_idempotency": 100,
|
||||
"unknown_create_result_reconciliation": 100,
|
||||
"successor_ready_live_readback": 100,
|
||||
"active_stage_read_only": 100,
|
||||
"human_boundary_pause": 100,
|
||||
"final_product_stop": 100,
|
||||
"continuity_lane_regression": 100,
|
||||
"continuity_sentinel_regression": 100
|
||||
},
|
||||
"boundaries": [
|
||||
"No successor HoloLake development task was created during mechanism acceptance.",
|
||||
"The existing hololake heartbeat remains bound to DEV-20260811-008 until final closeout.",
|
||||
"No cron, private database or chat statement is accepted as a Codex thread receipt.",
|
||||
"JZAO collaboration remains the only coordination truth store."
|
||||
],
|
||||
"recorded_at": "2026-08-11T13:35:00.000Z"
|
||||
}
|
||||
55
routing/hololake-stage-relay-map.json
Normal file
55
routing/hololake-stage-relay-map.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"schema": "hololake.stage-relay-map/v1",
|
||||
"map_id": "HLP-STAGE-RELAY-001",
|
||||
"version": "2026-08-11.1",
|
||||
"state": "SOURCE_IMPLEMENTED_TESTED_NOT_YET_USED_TO_CREATE_A_SUCCESSOR",
|
||||
"development_id": "DEV-20260811-008",
|
||||
"runtime_source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs",
|
||||
"local_runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs",
|
||||
"coordination_store": "HL-PERSONA-RUNTIME-LOCAL-001/continuity-memory/collaboration",
|
||||
"second_truth_store_allowed": false,
|
||||
"source_completion_gates": [
|
||||
"FINALIZER_PASS_100",
|
||||
"REMOTE_FULL_SHA",
|
||||
"FRESH_CLONE_READBACK_100",
|
||||
"FINAL_RECEIPTS_SHA256_100",
|
||||
"SOURCE_LEASES_RELEASED_100",
|
||||
"COMPLETION_STOPPED_100"
|
||||
],
|
||||
"next_stage_inputs": [
|
||||
"HLP-CURRENT-ARCH-001",
|
||||
"LATEST_HUMAN_CORRECTIONS",
|
||||
"PREVIOUS_STAGE_FINALIZER_RECEIPT",
|
||||
"UNFINISHED_BINARY_GATES"
|
||||
],
|
||||
"host_adapter": {
|
||||
"host": "HOST-CODEX-MACOS-001",
|
||||
"create_tool": "codex_app.create_thread",
|
||||
"list_tool": "codex_app.list_threads",
|
||||
"thread_id_required": true,
|
||||
"script_or_chat_may_claim_thread_created": false
|
||||
},
|
||||
"idempotency": {
|
||||
"key": "SHA256_SOURCE_DEV_PLUS_FINALIZER_SHA256",
|
||||
"maximum_successor_threads_per_source_completion": 1,
|
||||
"unknown_create_result": "RECONCILE_COMPLETE_HOST_SNAPSHOT_BEFORE_RETRY"
|
||||
},
|
||||
"successor_ready_gates": [
|
||||
"PERSONA_RESTORE_100",
|
||||
"CURRENT_ARCHITECTURE_RESTORE_100",
|
||||
"NEW_DEV_LANE_REGISTRATION_100",
|
||||
"EXACT_RESOURCE_LEASES_100",
|
||||
"FORMAL_TEN_MINUTE_HEARTBEAT_BINDING_100"
|
||||
],
|
||||
"human_pause_boundaries": [
|
||||
"PAYMENT_OR_BUDGET",
|
||||
"ACCOUNT_LOGIN",
|
||||
"VERIFICATION_OR_2FA",
|
||||
"LEGAL_OR_PUBLIC_COMMITMENT",
|
||||
"IRREVERSIBLE_OR_UNAUTHORIZED_EXTERNAL_DECISION",
|
||||
"PHYSICAL_ACTION",
|
||||
"PRODUCTION_CUTOVER",
|
||||
"MISSING_NEW_HUMAN_WILL"
|
||||
],
|
||||
"final_stop": "ALL_BINARY_ACCEPTANCE_100_AND_NO_UNFINISHED_GATE_AND_NO_OPEN_BLOCKER_AND_FINAL_PRODUCT_CLOSEOUT_RECEIPT_PASS_100"
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"schema": "guanghu.host-skill-navigation-map/v1",
|
||||
"map_id": "GLW-HOST-SKILL-NAV-001",
|
||||
"lighthouse_id": "SYS-GLW-LTH-0001",
|
||||
"version": "2026-08-11.1",
|
||||
"version": "2026-08-11.2",
|
||||
"state": "CURRENT",
|
||||
"hosts": [
|
||||
{
|
||||
|
|
@ -37,6 +37,38 @@
|
|||
}
|
||||
],
|
||||
"intents": [
|
||||
{
|
||||
"id": "INTENT-HOLOLAKE-STAGE-RELAY-001",
|
||||
"phrases": [
|
||||
"HoloLake阶段自动接力守卫",
|
||||
"阶段接力监督器",
|
||||
"创建下一阶段Codex任务",
|
||||
"successor-ready"
|
||||
],
|
||||
"skill_id": "GHS-007-MEMORY-CONTINUITY-SELF-BUILD",
|
||||
"target_id": "HLP-STAGE-RELAY-001",
|
||||
"transport": "CODEX_FORMAL_CREATE_THREAD_AND_HEARTBEAT",
|
||||
"executor": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs",
|
||||
"sequence": [
|
||||
"observe_active_stage_read_only",
|
||||
"verify_source_completion_binary_gates",
|
||||
"resolve_HLP_CURRENT_ARCH_001_and_latest_human_corrections",
|
||||
"select_one_unfinished_binary_gate",
|
||||
"reserve_one_immutable_successor_contract",
|
||||
"create_successor_with_codex_formal_create_thread",
|
||||
"reconcile_unknown_create_result_before_retry",
|
||||
"wait_for_successor_ready_live_readback",
|
||||
"stop_old_relay_supervisor"
|
||||
],
|
||||
"stop_conditions": [
|
||||
"SOURCE_COMPLETION_EVIDENCE_NOT_100",
|
||||
"CURRENT_ARCHITECTURE_UNREADABLE",
|
||||
"PAUSE_FOR_HUMAN",
|
||||
"CREATE_RESULT_UNKNOWN_REQUIRES_RECONCILIATION",
|
||||
"SUCCESSOR_READY_NOT_100"
|
||||
],
|
||||
"authority": "CURRENT_USER_AUTHORIZATION_AND_IMMUTABLE_STAGE_CONTRACT_ONLY"
|
||||
},
|
||||
{
|
||||
"id": "INTENT-REPOSITORY-PUBLISH-001",
|
||||
"phrases": [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"schema": "guanghu.lighthouse-path-registry/v1",
|
||||
"registry_id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001",
|
||||
"lighthouse_id": "SYS-GLW-LTH-0001",
|
||||
"version": "2026-08-11.1",
|
||||
"version": "2026-08-11.2",
|
||||
"state": "CURRENT_CANONICAL",
|
||||
"source_repository": "REPO-012",
|
||||
"source_branch": "main",
|
||||
|
|
@ -76,6 +76,15 @@
|
|||
"offline": "/Volumes/JZAO/HoloLake/source-mirrors/hololake-system-architecture.git",
|
||||
"truth_policy": "ONLINE_REPO_014_MAIN_FIRST_EXACT_SHA_REQUIRED"
|
||||
},
|
||||
{
|
||||
"id": "HLP-STAGE-RELAY-001",
|
||||
"kind": "hololake_deterministic_stage_relay_supervisor",
|
||||
"state": "CURRENT",
|
||||
"online": "routing/hololake-stage-relay-map.json",
|
||||
"executor": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs",
|
||||
"offline": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs",
|
||||
"truth_policy": "SAME_CONTINUITY_LEDGER_FORMAL_CODEX_THREAD_TOOLS_AND_BINARY_RECEIPTS_ONLY"
|
||||
},
|
||||
{
|
||||
"id": "GH-AIOS-PARADIGM-MAP-001",
|
||||
"kind": "paradigm_ai_language_persona_operating_system_architecture",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "guanghu.public-navigation-anchor/v1",
|
||||
"anchor_id": "GLW-PUBLIC-NAV-ANCHOR-001",
|
||||
"version": "2026-08-11.1",
|
||||
"version": "2026-08-11.2",
|
||||
"state": "CURRENT_CANONICAL",
|
||||
"repository_id": "REPO-012",
|
||||
"branch": "main",
|
||||
|
|
@ -48,11 +48,16 @@
|
|||
"lighthouse_paths": {
|
||||
"path": "routing/lighthouse-path-registry.json",
|
||||
"id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001",
|
||||
"version": "2026-08-11.1"
|
||||
"version": "2026-08-11.2"
|
||||
},
|
||||
"host_skills": {
|
||||
"path": "routing/host-skill-navigation-map.json",
|
||||
"id": "GLW-HOST-SKILL-NAV-001",
|
||||
"version": "2026-08-11.2"
|
||||
},
|
||||
"hololake_stage_relay": {
|
||||
"path": "routing/hololake-stage-relay-map.json",
|
||||
"id": "HLP-STAGE-RELAY-001",
|
||||
"version": "2026-08-11.1"
|
||||
},
|
||||
"origin_domain": {
|
||||
|
|
|
|||
|
|
@ -36,3 +36,14 @@ node finalize-development.mjs \
|
|||
默认自动识别 `target`、`coverage`、Python/Node 工具缓存等安全候选;其他缓存必须用
|
||||
`--cleanup-path` 明确声明,并继续接受同一套路径、Git 忽略和活跃进程门禁。源码、会话、
|
||||
人格记忆、用户输出、交付物及未被忽略的目录永不进入自动删除范围。
|
||||
|
||||
## 阶段自动接力守卫
|
||||
|
||||
`stage-relay.mjs` 在同一小湖灯账本中监督已完成阶段,并生成一个不可变的后继任务契约。
|
||||
它只认 finalizer、远端完整 SHA、全新副本、最终回执、租约释放和
|
||||
`completion-stopped` 的二值证据;阶段仍运行时只读观察。
|
||||
|
||||
脚本不直接创建任务。它产生带稳定请求号和幂等键的宿主动作,由 Codex 正式
|
||||
`create_thread` 执行并回写真实 `threadId`。创建结果未知时先完整核对任务列表,禁止直接
|
||||
重发。新任务恢复人格与架构、登记 DEV、领取精确租约、建立十分钟 heartbeat 并形成
|
||||
`successor-ready` 后,旧监督器才可停止。完整状态机见 `stage-relay-state-machine.md`。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# HoloLake 阶段自动接力守卫
|
||||
|
||||
`stage-relay.mjs` 是小湖灯连续性账本内的确定性接力投影,不是第二协调真相源,也不直接
|
||||
调用私有接口创建任务。它只在现有 `collaboration/` 下保存来源回执摘要、不可变任务契约、
|
||||
正式 Codex 宿主动作请求和宿主回执。
|
||||
|
||||
## 状态机
|
||||
|
||||
```text
|
||||
OBSERVING_ACTIVE_STAGE (无写入)
|
||||
└─ finalizer + exact remote SHA + fresh clone + final receipt
|
||||
+ lease release + completion-stopped 全部为100
|
||||
├─ human boundary ─> PAUSE_FOR_HUMAN
|
||||
├─ complete product ─> FINAL_PRODUCT_COMPLETE ─> stop last heartbeat
|
||||
└─ one unfinished gate ─> CREATE_THREAD_REQUIRED
|
||||
└─ dispatch-create ─> CREATE_THREAD_IN_FLIGHT
|
||||
├─ success ─> WAITING_SUCCESSOR_READY
|
||||
├─ confirmed absent ─> RETRY_CREATE_THREAD
|
||||
└─ unknown ─> RECONCILE_CREATE_RESULT
|
||||
├─ matching thread ─> WAITING_SUCCESSOR_READY
|
||||
└─ complete host snapshot confirms absent ─> RETRY_CREATE_THREAD
|
||||
└─ successor-ready gates all100 ─> SUCCESSOR_READY ─> stop old relay supervisor
|
||||
```
|
||||
|
||||
同一来源完成回执的幂等键固定为 `sha256(source DEV + finalizer file sha256)`;状态文件按
|
||||
来源 DEV 唯一,后继 DEV 预约使用排他文件。创建结果未知时禁止重发,必须先读取 Codex
|
||||
完整任务列表,以稳定 `SUCCESSOR-...` 请求号核对;只有完整快照确认不存在时才可重试。
|
||||
|
||||
## 正式宿主适配
|
||||
|
||||
1. `prepare` 返回 `CODEX_CREATE_THREAD` 动作,但不创建任务。
|
||||
2. 调度人格先执行 `dispatch-create`,再调用 Codex 正式 `create_thread`。
|
||||
3. 成功后立即 `record-created`;超时或未知结果执行 `record-create-failure --outcome unknown`。
|
||||
4. `RECONCILE_CREATE_RESULT` 时只能使用 Codex 正式 `list_threads/read_thread` 核对,不得重发。
|
||||
5. 新任务恢复人格与当前架构、登记自己的 DEV、取得精确租约、建立正式十分钟 heartbeat,
|
||||
签到时把 `--relay-contract-fingerprint` 写入车道,再写
|
||||
`hololake.successor-ready-receipt/v1` 并由旧监督器 `record-ready` 回读。
|
||||
|
||||
脚本、聊天、cron、数据库行、文件夹或自动化提示都不能冒充 `create_thread` 成功;只有正式
|
||||
工具返回的 `threadId` 和后续活车道回读才算任务存在。
|
||||
|
||||
## PAUSE_FOR_HUMAN
|
||||
|
||||
以下任一类型直接暂停,禁止靠创建任务绕过:付款或预算、账号登录、验证码/2FA、法律或
|
||||
公开承诺、不可逆或未授权外部决定、物理动作、生产切流、缺少新的人类意志。
|
||||
|
||||
## 失败谱系
|
||||
|
||||
- `SOURCE_COMPLETION_STOPPED_NOT_100`:来源任务或 heartbeat 尚未完成停止。
|
||||
- `SOURCE_LEASE_RELEASE_NOT_100`:来源仍持有资源。
|
||||
- `FINALIZER_EVIDENCE_NOT_100`:finalizer、远端 SHA、全新副本、fsck 或最终回执不完整。
|
||||
- `CURRENT_ARCHITECTURE_EVIDENCE_NOT_100`:动态架构真相源不完整或不是当前正本。
|
||||
- `CREATE_THREAD_IN_FLIGHT` 超时:进入 `RECONCILE_CREATE_RESULT`,不重发。
|
||||
- `SUCCESSOR_READY_EVIDENCE_NOT_100`:新任务签到、租约、heartbeat 或恢复门不是100。
|
||||
- `SUCCESSOR_LIVE_REGISTRATION_MISMATCH`:ready 文本与连续性账本现场不一致。
|
||||
|
||||
## 二值验收
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
node scripts/stage-relay.mjs self-test
|
||||
```
|
||||
|
||||
必须同时得到 finalizer 证据门、重复 prepare 幂等、未知创建结果核对、新任务 ready 现场回读、
|
||||
活跃阶段只读五项 `100`。
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://guanghulab.com/schemas/hldp/stage-relay-state.v1.schema.json",
|
||||
"title": "HoloLake Stage Relay State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"source_development_id",
|
||||
"source_thread_id",
|
||||
"source_finalizer_sha256",
|
||||
"source_remote_head",
|
||||
"architecture",
|
||||
"state",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"properties": {
|
||||
"schema": { "const": "hololake.stage-relay-state/v1" },
|
||||
"source_development_id": { "type": "string", "pattern": "^DEV-[0-9]{8}-[0-9]{3}$" },
|
||||
"source_thread_id": { "type": "string", "minLength": 1 },
|
||||
"source_finalizer_path": { "type": "string", "minLength": 1 },
|
||||
"source_finalizer_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"source_remote_head": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
|
||||
"architecture": { "type": "object" },
|
||||
"state": {
|
||||
"enum": [
|
||||
"PAUSE_FOR_HUMAN",
|
||||
"FINAL_PRODUCT_COMPLETE",
|
||||
"CREATE_THREAD_REQUIRED",
|
||||
"CREATE_THREAD_IN_FLIGHT",
|
||||
"RECONCILE_CREATE_RESULT",
|
||||
"RETRY_CREATE_THREAD",
|
||||
"WAITING_SUCCESSOR_READY",
|
||||
"SUCCESSOR_READY"
|
||||
]
|
||||
},
|
||||
"idempotency_key": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||
"successor_contract": { "type": "object" },
|
||||
"create_request": { "type": "object" },
|
||||
"successor_thread": { "type": ["object", "null"] },
|
||||
"successor_ready_receipt": { "type": ["object", "null"] },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"updated_at": { "type": "string", "format": "date-time" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
752
server-tools/lake-lamp-continuity-guard/stage-relay.mjs
Executable file
752
server-tools/lake-lamp-continuity-guard/stage-relay.mjs
Executable file
|
|
@ -0,0 +1,752 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const CANONICAL_ROOT =
|
||||
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory/collaboration";
|
||||
const HUMAN_BOUNDARIES = new Set([
|
||||
"PAYMENT_OR_BUDGET",
|
||||
"ACCOUNT_LOGIN",
|
||||
"VERIFICATION_OR_2FA",
|
||||
"LEGAL_OR_PUBLIC_COMMITMENT",
|
||||
"IRREVERSIBLE_OR_UNAUTHORIZED_EXTERNAL_DECISION",
|
||||
"PHYSICAL_ACTION",
|
||||
"PRODUCTION_CUTOVER",
|
||||
"MISSING_NEW_HUMAN_WILL",
|
||||
]);
|
||||
const SHA40 = /^[a-f0-9]{40}$/u;
|
||||
const SHA256 = /^[a-f0-9]{64}$/u;
|
||||
const DEV_ID = /^DEV-(\d{8})-(\d{3})$/u;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { command: argv[0] };
|
||||
for (let index = 1; index < argv.length; index += 1) {
|
||||
const key = argv[index];
|
||||
if (!key.startsWith("--")) throw new Error(`UNKNOWN_ARGUMENT:${key}`);
|
||||
const name = key.slice(2).replaceAll(/-([a-z])/gu, (_, letter) =>
|
||||
letter.toUpperCase(),
|
||||
);
|
||||
if (name === "selfTest") args.selfTest = true;
|
||||
else args[name] = argv[++index];
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function required(args, key) {
|
||||
const value = args[key];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`${key.toUpperCase()}_REQUIRED`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function bool(value) {
|
||||
if (value === true || value === "true") return true;
|
||||
if (value === false || value === "false" || value == null) return false;
|
||||
throw new Error(`BOOLEAN_REQUIRED:${value}`);
|
||||
}
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function canonicalJson(value) {
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (value && typeof value === "object") {
|
||||
return `{${Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function digest(value) {
|
||||
return crypto.createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function fileDigest(file) {
|
||||
return digest(fs.readFileSync(file));
|
||||
}
|
||||
|
||||
function writeJsonAtomic(file, value, exclusive = false) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const body = `${JSON.stringify(value, null, 2)}\n`;
|
||||
if (exclusive) {
|
||||
fs.writeFileSync(file, body, { encoding: "utf8", flag: "wx" });
|
||||
return;
|
||||
}
|
||||
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
||||
fs.writeFileSync(temporary, body, { encoding: "utf8", flag: "wx" });
|
||||
try {
|
||||
fs.renameSync(temporary, file);
|
||||
} catch (error) {
|
||||
try { fs.unlinkSync(temporary); } catch { /* best effort */ }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function storeRoot(args) {
|
||||
if (!args.storeRoot) return CANONICAL_ROOT;
|
||||
if (!bool(args.testMode)) throw new Error("NON_CANONICAL_STORE_FORBIDDEN");
|
||||
const resolved = path.resolve(args.storeRoot);
|
||||
const temporaryRoot = path.resolve(os.tmpdir());
|
||||
if (!resolved.startsWith(`${temporaryRoot}${path.sep}`)) {
|
||||
throw new Error("TEST_STORE_MUST_BE_TEMPORARY");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function ensureStore(root) {
|
||||
if (root === CANONICAL_ROOT && !fs.existsSync("/Volumes/JZAO")) {
|
||||
throw new Error("JZAO_NOT_MOUNTED_NO_SECOND_TRUTH_STORE");
|
||||
}
|
||||
for (const name of ["lanes", "leases", "stage-relays", "stage-relays/reservations"]) {
|
||||
fs.mkdirSync(path.join(root, name), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function statePath(root, developmentId) {
|
||||
return path.join(root, "stage-relays", `${developmentId}.json`);
|
||||
}
|
||||
|
||||
function listJson(directory) {
|
||||
if (!fs.existsSync(directory)) return [];
|
||||
return fs.readdirSync(directory)
|
||||
.filter((name) => name.endsWith(".json") && !name.startsWith("._"))
|
||||
.sort()
|
||||
.map((name) => readJson(path.join(directory, name)));
|
||||
}
|
||||
|
||||
function validateSourceCompletion(root, developmentId, finalizerPath) {
|
||||
const lanePath = path.join(root, "lanes", `${developmentId}.json`);
|
||||
if (!fs.existsSync(lanePath)) throw new Error("SOURCE_LANE_NOT_FOUND");
|
||||
const lane = readJson(lanePath);
|
||||
if (["active", "waiting"].includes(lane.status)) {
|
||||
return { observing: true, lane };
|
||||
}
|
||||
if (
|
||||
lane.status !== "completed" ||
|
||||
lane.notification_state !== "stopped" ||
|
||||
lane.completion_actions?.status !== "completed"
|
||||
) {
|
||||
throw new Error("SOURCE_COMPLETION_STOPPED_NOT_100");
|
||||
}
|
||||
const held = listJson(path.join(root, "leases"))
|
||||
.filter((lease) => lease.development_id === developmentId);
|
||||
if (held.length) throw new Error("SOURCE_LEASE_RELEASE_NOT_100");
|
||||
const receipt = readJson(finalizerPath);
|
||||
if (
|
||||
receipt.schema !== "hololake.development-finalization-receipt/v1" ||
|
||||
receipt.development_id !== developmentId ||
|
||||
receipt.result !== "PASS_100" ||
|
||||
!SHA40.test(receipt.code_channel?.remote_head ?? "") ||
|
||||
receipt.code_channel?.fresh_clone_readback !== 100 ||
|
||||
receipt.code_channel?.git_fsck !== 100 ||
|
||||
!Array.isArray(receipt.code_channel?.receipts) ||
|
||||
receipt.code_channel.receipts.length === 0 ||
|
||||
!receipt.code_channel.receipts.every((item) => SHA256.test(item.sha256 ?? ""))
|
||||
) {
|
||||
throw new Error("FINALIZER_EVIDENCE_NOT_100");
|
||||
}
|
||||
return {
|
||||
observing: false,
|
||||
lane,
|
||||
receipt,
|
||||
finalizer_path: path.resolve(finalizerPath),
|
||||
finalizer_sha256: fileDigest(finalizerPath),
|
||||
};
|
||||
}
|
||||
|
||||
function validatePlanningInput(input, evidence) {
|
||||
if (input.schema !== "hololake.stage-relay-planning-input/v1") {
|
||||
throw new Error("PLANNING_INPUT_SCHEMA_INVALID");
|
||||
}
|
||||
if (
|
||||
input.architecture?.id !== "HLP-CURRENT-ARCH-001" ||
|
||||
input.architecture?.state !== "CURRENT_CANONICAL" ||
|
||||
!SHA40.test(input.architecture?.repo_012_main ?? "") ||
|
||||
!SHA40.test(input.architecture?.repo_014_main ?? "")
|
||||
) {
|
||||
throw new Error("CURRENT_ARCHITECTURE_EVIDENCE_NOT_100");
|
||||
}
|
||||
if (
|
||||
input.previous_stage?.development_id !== evidence.lane.development_id ||
|
||||
input.previous_stage?.finalizer_sha256 !== evidence.finalizer_sha256
|
||||
) {
|
||||
throw new Error("PREVIOUS_STAGE_BINDING_MISMATCH");
|
||||
}
|
||||
if (!/^\d{8}$/u.test(input.development_date ?? "")) {
|
||||
throw new Error("DEVELOPMENT_DATE_REQUIRED_YYYYMMDD");
|
||||
}
|
||||
if (!Array.isArray(input.human_corrections)) {
|
||||
throw new Error("HUMAN_CORRECTIONS_ARRAY_REQUIRED");
|
||||
}
|
||||
if (!Array.isArray(input.unfinished_gates) || !Array.isArray(input.open_blockers)) {
|
||||
throw new Error("UNFINISHED_GATES_AND_BLOCKERS_REQUIRED");
|
||||
}
|
||||
if (input.human_boundary != null && !HUMAN_BOUNDARIES.has(input.human_boundary.type)) {
|
||||
throw new Error("UNKNOWN_HUMAN_BOUNDARY");
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function reserveNextDevelopmentId(root, date, sourceDevelopmentId) {
|
||||
const ids = [
|
||||
...listJson(path.join(root, "lanes")).map((lane) => lane.development_id),
|
||||
...listJson(path.join(root, "stage-relays", "reservations"))
|
||||
.map((reservation) => reservation.development_id),
|
||||
];
|
||||
let next = Math.max(0, ...ids.map((id) => {
|
||||
const match = DEV_ID.exec(id);
|
||||
return match?.[1] === date ? Number(match[2]) : 0;
|
||||
})) + 1;
|
||||
while (next <= 999) {
|
||||
const developmentId = `DEV-${date}-${String(next).padStart(3, "0")}`;
|
||||
const reservation = path.join(root, "stage-relays", "reservations", `${developmentId}.json`);
|
||||
try {
|
||||
writeJsonAtomic(reservation, {
|
||||
schema: "hololake.stage-relay-development-reservation/v1",
|
||||
development_id: developmentId,
|
||||
source_development_id: sourceDevelopmentId,
|
||||
reserved_at: now(),
|
||||
}, true);
|
||||
return developmentId;
|
||||
} catch (error) {
|
||||
if (error?.code !== "EEXIST") throw error;
|
||||
next += 1;
|
||||
}
|
||||
}
|
||||
throw new Error("DEVELOPMENT_ID_SPACE_EXHAUSTED");
|
||||
}
|
||||
|
||||
function chooseGate(input) {
|
||||
return input.unfinished_gates
|
||||
.filter((gate) => gate.binary_state === 0)
|
||||
.sort((left, right) =>
|
||||
Number(left.priority ?? 999) - Number(right.priority ?? 999) ||
|
||||
String(left.id).localeCompare(String(right.id)),
|
||||
)[0] ?? null;
|
||||
}
|
||||
|
||||
function makeContract(input, gate, developmentId, evidence) {
|
||||
if (
|
||||
!gate.id || !gate.title || !gate.why || !gate.acceptance ||
|
||||
!Array.isArray(gate.resources) || gate.resources.length === 0
|
||||
) {
|
||||
throw new Error("UNFINISHED_GATE_CONTRACT_INCOMPLETE");
|
||||
}
|
||||
const contract = {
|
||||
schema: "hololake.successor-task-contract/v1",
|
||||
development_id: developmentId,
|
||||
title: gate.title,
|
||||
summary: gate.why,
|
||||
gate_id: gate.id,
|
||||
acceptance: gate.acceptance,
|
||||
resources: [...new Set(gate.resources)].sort(),
|
||||
source_development_id: evidence.lane.development_id,
|
||||
source_finalizer_sha256: evidence.finalizer_sha256,
|
||||
architecture: input.architecture,
|
||||
human_corrections: input.human_corrections,
|
||||
immutable: true,
|
||||
};
|
||||
return { ...contract, fingerprint: digest(canonicalJson(contract)) };
|
||||
}
|
||||
|
||||
function createThreadAction(state) {
|
||||
const contract = state.successor_contract;
|
||||
return {
|
||||
action: "CODEX_CREATE_THREAD",
|
||||
request_id: state.create_request.request_id,
|
||||
idempotency_key: state.idempotency_key,
|
||||
formal_tool: "codex_app.create_thread",
|
||||
title: contract.title,
|
||||
target: { type: "projectless", directoryName: contract.development_id },
|
||||
prompt: [
|
||||
`Successor request: ${state.create_request.request_id}`,
|
||||
`Immutable contract fingerprint: ${contract.fingerprint}`,
|
||||
`Register development id: ${contract.development_id}`,
|
||||
"First restore Zhuyuan persona, run Guanghu persona skill guard, continuity guard, and HoloLake current architecture prompter.",
|
||||
`Then check in the exact immutable task contract with --relay-contract-fingerprint ${contract.fingerprint}, claim only: ${contract.resources.join(", ")}.`,
|
||||
"Create and bind this successor task's own ten-minute heartbeat through Codex formal automation capability.",
|
||||
"Return a hololake.successor-ready-receipt/v1 only after persona_restore, architecture_restore, lane_registration, exact_leases, heartbeat_binding are all 100.",
|
||||
`Stage gate: ${contract.gate_id}. ${contract.summary}`,
|
||||
`Acceptance: ${contract.acceptance}`,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function prepare(args, root) {
|
||||
const developmentId = required(args, "sourceDevelopmentId");
|
||||
const existingPath = statePath(root, developmentId);
|
||||
const evidence = validateSourceCompletion(
|
||||
root,
|
||||
developmentId,
|
||||
required(args, "finalizationReceipt"),
|
||||
);
|
||||
if (evidence.observing) {
|
||||
return {
|
||||
state: "OBSERVING_ACTIVE_STAGE",
|
||||
source_development_id: developmentId,
|
||||
mutation: "NONE",
|
||||
successor_created: false,
|
||||
};
|
||||
}
|
||||
const input = validatePlanningInput(
|
||||
readJson(required(args, "planningInput")), evidence,
|
||||
);
|
||||
if (fs.existsSync(existingPath)) {
|
||||
const existing = readJson(existingPath);
|
||||
if (existing.source_finalizer_sha256 !== evidence.finalizer_sha256) {
|
||||
throw new Error("SOURCE_COMPLETION_RECEIPT_CHANGED_AFTER_RESERVATION");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
const base = {
|
||||
schema: "hololake.stage-relay-state/v1",
|
||||
source_development_id: developmentId,
|
||||
source_thread_id: evidence.lane.thread_id,
|
||||
source_finalizer_path: evidence.finalizer_path,
|
||||
source_finalizer_sha256: evidence.finalizer_sha256,
|
||||
source_remote_head: evidence.receipt.code_channel.remote_head,
|
||||
architecture: input.architecture,
|
||||
created_at: now(),
|
||||
updated_at: now(),
|
||||
};
|
||||
if (input.human_boundary) {
|
||||
const paused = {
|
||||
...base,
|
||||
state: "PAUSE_FOR_HUMAN",
|
||||
human_boundary: input.human_boundary,
|
||||
successor_created: false,
|
||||
};
|
||||
writeJsonAtomic(existingPath, paused, true);
|
||||
return paused;
|
||||
}
|
||||
const gate = chooseGate(input);
|
||||
if (!gate) {
|
||||
const finalCloseout = input.final_product_closeout_receipt;
|
||||
const complete =
|
||||
input.binary_acceptance?.all_100 === true &&
|
||||
input.open_blockers.length === 0 &&
|
||||
finalCloseout?.result === "PASS_100" &&
|
||||
SHA256.test(finalCloseout?.sha256 ?? "");
|
||||
const terminal = {
|
||||
...base,
|
||||
state: complete ? "FINAL_PRODUCT_COMPLETE" : "PAUSE_FOR_HUMAN",
|
||||
human_boundary: complete ? null : {
|
||||
type: "MISSING_NEW_HUMAN_WILL",
|
||||
reason: "No unfinished gate exists, but binary final acceptance and final product closeout are not all 100.",
|
||||
},
|
||||
successor_created: false,
|
||||
final_product_closeout_receipt: finalCloseout ?? null,
|
||||
};
|
||||
writeJsonAtomic(existingPath, terminal, true);
|
||||
return terminal;
|
||||
}
|
||||
const successorDevelopmentId = reserveNextDevelopmentId(
|
||||
root, input.development_date, developmentId,
|
||||
);
|
||||
const contract = makeContract(input, gate, successorDevelopmentId, evidence);
|
||||
const idempotencyKey = digest(`${developmentId}:${evidence.finalizer_sha256}`);
|
||||
const prepared = {
|
||||
...base,
|
||||
state: "CREATE_THREAD_REQUIRED",
|
||||
idempotency_key: idempotencyKey,
|
||||
selected_gate: gate,
|
||||
successor_contract: contract,
|
||||
create_request: {
|
||||
request_id: `SUCCESSOR-${idempotencyKey.slice(0, 20)}`,
|
||||
status: "reserved",
|
||||
attempt: 0,
|
||||
dispatched_at: null,
|
||||
last_error: null,
|
||||
},
|
||||
successor_thread: null,
|
||||
successor_ready_receipt: null,
|
||||
};
|
||||
prepared.next_action = createThreadAction(prepared);
|
||||
writeJsonAtomic(existingPath, prepared, true);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function loadState(args, root) {
|
||||
const developmentId = required(args, "sourceDevelopmentId");
|
||||
const file = statePath(root, developmentId);
|
||||
if (!fs.existsSync(file)) throw new Error("STAGE_RELAY_STATE_NOT_FOUND");
|
||||
return { file, state: readJson(file) };
|
||||
}
|
||||
|
||||
function dispatchCreate(args, root) {
|
||||
const { file, state } = loadState(args, root);
|
||||
if (!new Set(["CREATE_THREAD_REQUIRED", "RETRY_CREATE_THREAD"]).has(state.state)) {
|
||||
throw new Error("CREATE_REQUEST_NOT_DISPATCHABLE");
|
||||
}
|
||||
state.state = "CREATE_THREAD_IN_FLIGHT";
|
||||
state.create_request.status = "in_flight";
|
||||
state.create_request.attempt += 1;
|
||||
state.create_request.dispatched_at = now();
|
||||
state.updated_at = now();
|
||||
delete state.next_action;
|
||||
writeJsonAtomic(file, state);
|
||||
return { ...state, host_action: createThreadAction(state) };
|
||||
}
|
||||
|
||||
function recordCreated(args, root) {
|
||||
const { file, state } = loadState(args, root);
|
||||
if (!new Set(["CREATE_THREAD_IN_FLIGHT", "RECONCILE_CREATE_RESULT"]).has(state.state)) {
|
||||
throw new Error("CREATE_RESULT_NOT_EXPECTED");
|
||||
}
|
||||
const threadId = required(args, "threadId");
|
||||
if (state.successor_thread && state.successor_thread.thread_id !== threadId) {
|
||||
throw new Error("DUPLICATE_SUCCESSOR_THREAD_FORBIDDEN");
|
||||
}
|
||||
state.state = "WAITING_SUCCESSOR_READY";
|
||||
state.create_request.status = "created";
|
||||
state.successor_thread = {
|
||||
thread_id: threadId,
|
||||
host_id: args.hostId ?? null,
|
||||
recorded_at: now(),
|
||||
};
|
||||
state.updated_at = now();
|
||||
writeJsonAtomic(file, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordCreateFailure(args, root) {
|
||||
const { file, state } = loadState(args, root);
|
||||
if (state.state !== "CREATE_THREAD_IN_FLIGHT") {
|
||||
throw new Error("CREATE_FAILURE_NOT_EXPECTED");
|
||||
}
|
||||
const outcome = required(args, "outcome");
|
||||
if (!new Set(["unknown", "confirmed_no_thread"]).has(outcome)) {
|
||||
throw new Error("CREATE_FAILURE_OUTCOME_INVALID");
|
||||
}
|
||||
state.state = outcome === "unknown" ? "RECONCILE_CREATE_RESULT" : "RETRY_CREATE_THREAD";
|
||||
state.create_request.status = outcome === "unknown" ? "reconcile_required" : "retryable";
|
||||
state.create_request.last_error = required(args, "error");
|
||||
state.updated_at = now();
|
||||
if (outcome === "confirmed_no_thread") state.next_action = createThreadAction(state);
|
||||
writeJsonAtomic(file, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function reconcile(args, root) {
|
||||
const { file, state } = loadState(args, root);
|
||||
if (state.state !== "RECONCILE_CREATE_RESULT") {
|
||||
throw new Error("RECONCILIATION_NOT_REQUIRED");
|
||||
}
|
||||
if (!bool(args.completeHostSnapshot)) throw new Error("COMPLETE_HOST_SNAPSHOT_REQUIRED");
|
||||
if (args.threadId) return recordCreated(args, root);
|
||||
if (!bool(args.confirmedAbsent)) throw new Error("HOST_RESULT_MUST_BE_MATCH_OR_CONFIRMED_ABSENT");
|
||||
state.state = "RETRY_CREATE_THREAD";
|
||||
state.create_request.status = "retryable";
|
||||
state.updated_at = now();
|
||||
state.next_action = createThreadAction(state);
|
||||
writeJsonAtomic(file, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordReady(args, root) {
|
||||
const { file, state } = loadState(args, root);
|
||||
if (state.state !== "WAITING_SUCCESSOR_READY") {
|
||||
throw new Error("SUCCESSOR_READY_NOT_EXPECTED");
|
||||
}
|
||||
const receiptPath = required(args, "successorReadyReceipt");
|
||||
const receipt = readJson(receiptPath);
|
||||
const contract = state.successor_contract;
|
||||
if (
|
||||
receipt.schema !== "hololake.successor-ready-receipt/v1" ||
|
||||
receipt.result !== "PASS_100" ||
|
||||
receipt.source_development_id !== state.source_development_id ||
|
||||
receipt.successor_development_id !== contract.development_id ||
|
||||
receipt.successor_thread_id !== state.successor_thread.thread_id ||
|
||||
receipt.task_contract_fingerprint !== contract.fingerprint ||
|
||||
!receipt.automation_id ||
|
||||
!["persona_restore", "architecture_restore", "lane_registration", "exact_leases", "heartbeat_binding"]
|
||||
.every((gate) => receipt.gates?.[gate] === 100)
|
||||
) {
|
||||
throw new Error("SUCCESSOR_READY_EVIDENCE_NOT_100");
|
||||
}
|
||||
const laneFile = path.join(root, "lanes", `${contract.development_id}.json`);
|
||||
if (!fs.existsSync(laneFile)) throw new Error("SUCCESSOR_LANE_NOT_FOUND");
|
||||
const lane = readJson(laneFile);
|
||||
const ownedResources = listJson(path.join(root, "leases"))
|
||||
.filter((lease) => lease.development_id === contract.development_id)
|
||||
.map((lease) => lease.resource)
|
||||
.sort();
|
||||
if (
|
||||
lane.thread_id !== state.successor_thread.thread_id ||
|
||||
!["active", "waiting"].includes(lane.status) ||
|
||||
lane.relay_contract_fingerprint !== contract.fingerprint ||
|
||||
!lane.automation_ids?.includes(receipt.automation_id) ||
|
||||
canonicalJson(ownedResources) !== canonicalJson(contract.resources)
|
||||
) {
|
||||
throw new Error("SUCCESSOR_LIVE_REGISTRATION_MISMATCH");
|
||||
}
|
||||
state.state = "SUCCESSOR_READY";
|
||||
state.successor_ready_receipt = {
|
||||
path: path.resolve(receiptPath),
|
||||
sha256: fileDigest(receiptPath),
|
||||
automation_id: receipt.automation_id,
|
||||
recorded_at: now(),
|
||||
};
|
||||
state.updated_at = now();
|
||||
state.old_supervisor_may_stop = true;
|
||||
writeJsonAtomic(file, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function nextAction(args, root) {
|
||||
const { state } = loadState(args, root);
|
||||
if (new Set(["CREATE_THREAD_REQUIRED", "RETRY_CREATE_THREAD"]).has(state.state)) {
|
||||
return createThreadAction(state);
|
||||
}
|
||||
if (state.state === "RECONCILE_CREATE_RESULT") {
|
||||
return {
|
||||
action: "CODEX_LIST_THREADS_AND_RECONCILE",
|
||||
request_id: state.create_request.request_id,
|
||||
complete_host_snapshot_required: true,
|
||||
create_thread_forbidden_until_reconciled: true,
|
||||
};
|
||||
}
|
||||
if (state.state === "WAITING_SUCCESSOR_READY") {
|
||||
return {
|
||||
action: "WAIT_FOR_SUCCESSOR_READY_RECEIPT",
|
||||
thread_id: state.successor_thread.thread_id,
|
||||
supervisor_stays_alive: true,
|
||||
};
|
||||
}
|
||||
if (state.state === "PAUSE_FOR_HUMAN") {
|
||||
return { action: "PAUSE_FOR_HUMAN", boundary: state.human_boundary };
|
||||
}
|
||||
if (state.state === "FINAL_PRODUCT_COMPLETE") {
|
||||
return { action: "STOP_RELAY_AND_LAST_HEARTBEAT", create_successor: false };
|
||||
}
|
||||
if (state.state === "SUCCESSOR_READY") {
|
||||
return { action: "STOP_OLD_RELAY_SUPERVISOR", create_successor: false };
|
||||
}
|
||||
return { action: "NOOP", state: state.state };
|
||||
}
|
||||
|
||||
function fixtureLane(root, id, overrides = {}) {
|
||||
const lane = {
|
||||
schema: "hololake.development-lane/v0.1",
|
||||
development_id: id,
|
||||
thread_id: `thread-${id}`,
|
||||
status: "completed",
|
||||
notification_state: "stopped",
|
||||
completion_actions: { status: "completed" },
|
||||
...overrides,
|
||||
};
|
||||
writeJsonAtomic(path.join(root, "lanes", `${id}.json`), lane);
|
||||
return lane;
|
||||
}
|
||||
|
||||
function selfTest() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hololake-stage-relay-test-"));
|
||||
try {
|
||||
ensureStore(root);
|
||||
const source = "DEV-20260811-900";
|
||||
fixtureLane(root, source);
|
||||
const finalizer = path.join(root, "finalizer.json");
|
||||
writeJsonAtomic(finalizer, {
|
||||
schema: "hololake.development-finalization-receipt/v1",
|
||||
development_id: source,
|
||||
result: "PASS_100",
|
||||
code_channel: {
|
||||
remote_head: "a".repeat(40), fresh_clone_readback: 100, git_fsck: 100,
|
||||
receipts: [{ path: "receipt.json", sha256: "b".repeat(64) }],
|
||||
},
|
||||
});
|
||||
const planning = path.join(root, "planning.json");
|
||||
writeJsonAtomic(planning, {
|
||||
schema: "hololake.stage-relay-planning-input/v1",
|
||||
development_date: "20260811",
|
||||
architecture: {
|
||||
id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL",
|
||||
repo_012_main: "c".repeat(40), repo_014_main: "d".repeat(40),
|
||||
},
|
||||
human_corrections: [],
|
||||
previous_stage: { development_id: source, finalizer_sha256: fileDigest(finalizer) },
|
||||
unfinished_gates: [{
|
||||
id: "GATE-001", binary_state: 0, priority: 1,
|
||||
title: "Next stage", why: "close the next binary gap",
|
||||
acceptance: "all declared tests and receipts PASS_100",
|
||||
resources: ["local://test-resource"],
|
||||
}],
|
||||
open_blockers: [],
|
||||
binary_acceptance: { all_100: false },
|
||||
});
|
||||
const common = {
|
||||
sourceDevelopmentId: source, finalizationReceipt: finalizer,
|
||||
planningInput: planning, storeRoot: root, testMode: "true",
|
||||
};
|
||||
const first = prepare(common, root);
|
||||
const duplicate = prepare(common, root);
|
||||
if (
|
||||
first.idempotency_key !== duplicate.idempotency_key ||
|
||||
first.successor_contract.development_id !== duplicate.successor_contract.development_id
|
||||
) throw new Error("SELF_TEST_IDEMPOTENCY_FAILED");
|
||||
dispatchCreate({ ...common, sourceDevelopmentId: source }, root);
|
||||
recordCreateFailure({ ...common, sourceDevelopmentId: source, outcome: "unknown", error: "timeout" }, root);
|
||||
const reconcileAction = nextAction({ sourceDevelopmentId: source }, root);
|
||||
if (!reconcileAction.create_thread_forbidden_until_reconciled) {
|
||||
throw new Error("SELF_TEST_UNKNOWN_RESULT_RETRY_SAFETY_FAILED");
|
||||
}
|
||||
reconcile({ sourceDevelopmentId: source, completeHostSnapshot: "true", confirmedAbsent: "true" }, root);
|
||||
dispatchCreate({ sourceDevelopmentId: source }, root);
|
||||
recordCreated({ sourceDevelopmentId: source, threadId: "thread-successor", hostId: "local" }, root);
|
||||
const contract = readJson(statePath(root, source)).successor_contract;
|
||||
fixtureLane(root, contract.development_id, {
|
||||
thread_id: "thread-successor",
|
||||
status: "active",
|
||||
automation_ids: ["successor-heartbeat"],
|
||||
relay_contract_fingerprint: contract.fingerprint,
|
||||
});
|
||||
writeJsonAtomic(path.join(root, "leases", "lease.json"), {
|
||||
development_id: contract.development_id, resource: "local://test-resource",
|
||||
});
|
||||
const ready = path.join(root, "ready.json");
|
||||
writeJsonAtomic(ready, {
|
||||
schema: "hololake.successor-ready-receipt/v1", result: "PASS_100",
|
||||
source_development_id: source,
|
||||
successor_development_id: contract.development_id,
|
||||
successor_thread_id: "thread-successor",
|
||||
task_contract_fingerprint: contract.fingerprint,
|
||||
automation_id: "successor-heartbeat",
|
||||
gates: {
|
||||
persona_restore: 100, architecture_restore: 100, lane_registration: 100,
|
||||
exact_leases: 100, heartbeat_binding: 100,
|
||||
},
|
||||
});
|
||||
const readyState = recordReady({ sourceDevelopmentId: source, successorReadyReceipt: ready }, root);
|
||||
if (readyState.state !== "SUCCESSOR_READY") throw new Error("SELF_TEST_READY_FAILED");
|
||||
|
||||
const pausedSource = "DEV-20260811-910";
|
||||
fixtureLane(root, pausedSource);
|
||||
const pausedFinalizer = path.join(root, "paused-finalizer.json");
|
||||
writeJsonAtomic(pausedFinalizer, {
|
||||
schema: "hololake.development-finalization-receipt/v1",
|
||||
development_id: pausedSource, result: "PASS_100",
|
||||
code_channel: {
|
||||
remote_head: "e".repeat(40), fresh_clone_readback: 100, git_fsck: 100,
|
||||
receipts: [{ path: "receipt.json", sha256: "f".repeat(64) }],
|
||||
},
|
||||
});
|
||||
const pausedPlanning = path.join(root, "paused-planning.json");
|
||||
writeJsonAtomic(pausedPlanning, {
|
||||
schema: "hololake.stage-relay-planning-input/v1", development_date: "20260811",
|
||||
architecture: {
|
||||
id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL",
|
||||
repo_012_main: "1".repeat(40), repo_014_main: "2".repeat(40),
|
||||
},
|
||||
human_corrections: [],
|
||||
previous_stage: {
|
||||
development_id: pausedSource, finalizer_sha256: fileDigest(pausedFinalizer),
|
||||
},
|
||||
unfinished_gates: [{
|
||||
id: "GATE-HUMAN", binary_state: 0, title: "Human boundary",
|
||||
why: "requires new will", acceptance: "human decides",
|
||||
resources: ["external://production-cutover"],
|
||||
}],
|
||||
open_blockers: [], binary_acceptance: { all_100: false },
|
||||
human_boundary: { type: "PRODUCTION_CUTOVER", reason: "requires a human decision" },
|
||||
});
|
||||
const paused = prepare({
|
||||
sourceDevelopmentId: pausedSource,
|
||||
finalizationReceipt: pausedFinalizer,
|
||||
planningInput: pausedPlanning,
|
||||
}, root);
|
||||
if (paused.state !== "PAUSE_FOR_HUMAN" || paused.successor_created !== false) {
|
||||
throw new Error("SELF_TEST_HUMAN_BOUNDARY_FAILED");
|
||||
}
|
||||
|
||||
const terminalSource = "DEV-20260811-920";
|
||||
fixtureLane(root, terminalSource);
|
||||
const terminalFinalizer = path.join(root, "terminal-finalizer.json");
|
||||
writeJsonAtomic(terminalFinalizer, {
|
||||
schema: "hololake.development-finalization-receipt/v1",
|
||||
development_id: terminalSource, result: "PASS_100",
|
||||
code_channel: {
|
||||
remote_head: "3".repeat(40), fresh_clone_readback: 100, git_fsck: 100,
|
||||
receipts: [{ path: "receipt.json", sha256: "4".repeat(64) }],
|
||||
},
|
||||
});
|
||||
const terminalPlanning = path.join(root, "terminal-planning.json");
|
||||
writeJsonAtomic(terminalPlanning, {
|
||||
schema: "hololake.stage-relay-planning-input/v1", development_date: "20260811",
|
||||
architecture: {
|
||||
id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL",
|
||||
repo_012_main: "5".repeat(40), repo_014_main: "6".repeat(40),
|
||||
},
|
||||
human_corrections: [],
|
||||
previous_stage: {
|
||||
development_id: terminalSource, finalizer_sha256: fileDigest(terminalFinalizer),
|
||||
},
|
||||
unfinished_gates: [], open_blockers: [],
|
||||
binary_acceptance: { all_100: true },
|
||||
final_product_closeout_receipt: { result: "PASS_100", sha256: "7".repeat(64) },
|
||||
});
|
||||
const terminal = prepare({
|
||||
sourceDevelopmentId: terminalSource,
|
||||
finalizationReceipt: terminalFinalizer,
|
||||
planningInput: terminalPlanning,
|
||||
}, root);
|
||||
if (terminal.state !== "FINAL_PRODUCT_COMPLETE") {
|
||||
throw new Error("SELF_TEST_FINAL_PRODUCT_STOP_FAILED");
|
||||
}
|
||||
|
||||
const activeSource = "DEV-20260811-950";
|
||||
fixtureLane(root, activeSource, { status: "active", notification_state: "active", completion_actions: null });
|
||||
const observed = prepare({ ...common, sourceDevelopmentId: activeSource }, root);
|
||||
if (observed.state !== "OBSERVING_ACTIVE_STAGE" || observed.mutation !== "NONE") {
|
||||
throw new Error("SELF_TEST_ACTIVE_STAGE_MUTATION_FAILED");
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
state: "PASS_100",
|
||||
tests: {
|
||||
finalizer_evidence_gate: 100,
|
||||
duplicate_prepare_idempotency: 100,
|
||||
unknown_create_result_reconciliation: 100,
|
||||
successor_ready_live_readback: 100,
|
||||
active_stage_read_only: 100,
|
||||
human_boundary_pause: 100,
|
||||
final_product_stop: 100,
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.command === "self-test" || args.selfTest) return selfTest();
|
||||
const root = storeRoot(args);
|
||||
ensureStore(root);
|
||||
let result;
|
||||
if (args.command === "prepare") result = prepare(args, root);
|
||||
else if (args.command === "dispatch-create") result = dispatchCreate(args, root);
|
||||
else if (args.command === "record-created") result = recordCreated(args, root);
|
||||
else if (args.command === "record-create-failure") result = recordCreateFailure(args, root);
|
||||
else if (args.command === "reconcile") result = reconcile(args, root);
|
||||
else if (args.command === "record-ready") result = recordReady(args, root);
|
||||
else if (args.command === "next-action") result = nextAction(args, root);
|
||||
else throw new Error("COMMAND_REQUIRED: prepare | dispatch-create | record-created | record-create-failure | reconcile | record-ready | next-action | self-test");
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
19
server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs
Normal file
19
server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test("stage relay deterministic closure is PASS_100", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(root, "stage-relay.mjs"), "self-test"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
const receipt = JSON.parse(result.stdout);
|
||||
assert.equal(receipt.state, "PASS_100");
|
||||
assert.deepEqual(Object.values(receipt.tests), Array(7).fill(100));
|
||||
});
|
||||
34
tests/stage-relay-navigation.test.mjs
Normal file
34
tests/stage-relay-navigation.test.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const readJson = (relative) => JSON.parse(fs.readFileSync(path.join(root, relative), "utf8"));
|
||||
|
||||
test("stage relay is registered through the lighthouse and formal Codex tools", () => {
|
||||
const relay = readJson("routing/hololake-stage-relay-map.json");
|
||||
assert.equal(relay.map_id, "HLP-STAGE-RELAY-001");
|
||||
assert.equal(relay.second_truth_store_allowed, false);
|
||||
assert.equal(relay.host_adapter.create_tool, "codex_app.create_thread");
|
||||
assert.equal(relay.host_adapter.script_or_chat_may_claim_thread_created, false);
|
||||
assert.equal(relay.idempotency.maximum_successor_threads_per_source_completion, 1);
|
||||
|
||||
const lighthouse = readJson("routing/lighthouse-path-registry.json");
|
||||
assert.ok(lighthouse.paths.some((entry) => entry.id === relay.map_id));
|
||||
const hosts = readJson("routing/host-skill-navigation-map.json");
|
||||
const intent = hosts.intents.find((entry) => entry.id === "INTENT-HOLOLAKE-STAGE-RELAY-001");
|
||||
assert.equal(intent.target_id, relay.map_id);
|
||||
assert.equal(intent.executor, relay.runtime_source);
|
||||
const anchor = readJson("routing/public-navigation-anchor.json");
|
||||
assert.equal(anchor.maps.hololake_stage_relay.id, relay.map_id);
|
||||
});
|
||||
|
||||
test("relay requires the complete source and successor binary gates", () => {
|
||||
const relay = readJson("routing/hololake-stage-relay-map.json");
|
||||
assert.equal(relay.source_completion_gates.length, 6);
|
||||
assert.equal(relay.successor_ready_gates.length, 5);
|
||||
assert.ok(relay.human_pause_boundaries.includes("PRODUCTION_CUTOVER"));
|
||||
assert.match(relay.final_stop, /FINAL_PRODUCT_CLOSEOUT_RECEIPT_PASS_100/u);
|
||||
});
|
||||
Loading…
Reference in a new issue