@volter/supercode-orchestrator 0.5.228 → 0.5.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +18 -2
  2. package/activation.mjs +1 -1
  3. package/adapters/agent-mailbox.mjs +10 -0
  4. package/adapters/api-server.mjs +6 -0
  5. package/agents.mjs +149 -40
  6. package/apply/doors.mjs +3 -1
  7. package/bin/agent-channel.mjs +7 -2
  8. package/bin/connection-runner.mjs +60 -19
  9. package/bin/hermes-worker.mjs +2 -1
  10. package/bin/orchestrator.mjs +3 -3
  11. package/bin/runtime.mjs +3 -2
  12. package/board/agent-current.mjs +82 -0
  13. package/board/agent-declarations.mjs +10 -3
  14. package/board/args.mjs +137 -6
  15. package/board/call.mjs +16 -1
  16. package/board/caller-session.mjs +3 -1
  17. package/board/capacity.mjs +59 -45
  18. package/board/card-state.mjs +14 -5
  19. package/board/ci.mjs +521 -178
  20. package/board/cli.mjs +614 -209
  21. package/board/client.mjs +64 -21
  22. package/board/close-current.mjs +36 -0
  23. package/board/current-records.mjs +44 -0
  24. package/board/custody.mjs +2 -1
  25. package/board/dispatch.mjs +1973 -846
  26. package/board/doors.mjs +2 -2
  27. package/board/engine.mjs +623 -85
  28. package/board/event-stream.mjs +10 -5
  29. package/board/fence.mjs +2 -0
  30. package/board/files.mjs +93 -62
  31. package/board/generation-owner.mjs +188 -53
  32. package/board/git-document.mjs +3 -1
  33. package/board/github-rest.mjs +29 -7
  34. package/board/incidents.mjs +5 -17
  35. package/board/listing.mjs +14 -8
  36. package/board/local-admission.mjs +59 -0
  37. package/board/lock-holder.mjs +18 -6
  38. package/board/machine-fleet.mjs +66 -13
  39. package/board/owner-door-worker.mjs +3 -3
  40. package/board/owner-door.mjs +17 -7
  41. package/board/owner-health.mjs +12 -15
  42. package/board/owner-interface.mjs +6 -0
  43. package/board/publication-reader.mjs +50 -0
  44. package/board/published.mjs +40 -12
  45. package/board/queries.mjs +24 -4
  46. package/board/record-view.mjs +30 -3
  47. package/board/request-mail.mjs +37 -6
  48. package/board/resting-admission.mjs +318 -0
  49. package/board/resting-codec-qualifications.json +33 -0
  50. package/board/resting-protocol.mjs +71 -0
  51. package/board/resting-state-protocol.json +120 -0
  52. package/board/resting-state.mjs +113 -0
  53. package/board/runtime-door.mjs +2 -2
  54. package/board/session-continuity.mjs +127 -0
  55. package/board/session-work.mjs +25 -13
  56. package/board/standalone-declaration.mjs +24 -0
  57. package/board/storage-contract.mjs +129 -7
  58. package/board/store.mjs +73 -11
  59. package/board/tools.mjs +56 -13
  60. package/board/wake.mjs +18 -5
  61. package/board/workflow.mjs +284 -82
  62. package/board/workspace.mjs +86 -9
  63. package/board/ztrack.mjs +344 -91
  64. package/connection.mjs +72 -17
  65. package/gates.mjs +2 -1
  66. package/hand-off.mjs +4 -3
  67. package/hermes-cli.mjs +5 -3
  68. package/hermes-locks.mjs +5 -2
  69. package/hosted.mjs +2 -1
  70. package/index.d.ts +2 -0
  71. package/index.mjs +1 -1
  72. package/kanban-worker.mjs +63 -13
  73. package/loop.mjs +33 -0
  74. package/mcp/hermes-kanban-tools.json +109 -50
  75. package/mcp/server.mjs +7 -1
  76. package/ontology.d.ts +26 -0
  77. package/orchestration.mjs +15 -4
  78. package/package.json +19 -12
  79. package/session-recovery.mjs +2 -1
  80. package/worker-route.mjs +37 -19
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": 1,
3
+ "codec": "sources/ztrack",
4
+ "canonicalContract": "UTF-8 JSON.stringify(contract, null, 2) followed by LF",
5
+ "qualified": {
6
+ "c81532c89e45bf29c74f0a3a62be7e112df72c9f": {
7
+ "contractSha256": "6de082ba7ea63f1a5fc7529d81664a926e46a5770b044d5a3737e972c22a11d0",
8
+ "evidence": [
9
+ "VO-3783 attachment 25595: pinned Bun 1.2.20 build/pack and public-door proof index",
10
+ "VO-3783 attachment 25596: exact raw/artifact hash manifest",
11
+ "VO-3783 attachment 25592: c81532c applicability; all 388 source files and 15 packed public artifacts match the recorded 8133a03 build",
12
+ "https://github.com/volter-ai/ztrack/pull/77 at c81532c89e45bf29c74f0a3a62be7e112df72c9f: independent PASS, VO-3783 report 25611 and audit 25608"
13
+ ]
14
+ },
15
+ "6bb0ecb81aa0d6ac91dc51ee28fb572df0d382c6": {
16
+ "contractSha256": "6de082ba7ea63f1a5fc7529d81664a926e46a5770b044d5a3737e972c22a11d0",
17
+ "evidence": [
18
+ "https://github.com/volter-ai/ztrack/pull/79 at 4cbed24c07d08dc1df0b6d0ff07dc14accd2d4e1: independent PASS; merge 6bb0ecb81aa0d6ac91dc51ee28fb572df0d382c6 changes only build invocation and CONTRIBUTING",
19
+ "VO-3758 attachment 29417: exact 6bb0ecb Windows build with frozen dependencies and pinned Bun 1.2.20",
20
+ "VO-3758 attachment 29415: exact 6bb0ecb macOS build with frozen dependencies and pinned Bun 1.2.20",
21
+ "VO-3758 attachment 29416: all 15 public artifact records and canonical contract hash match qualified c81532c89; existing VO-3783 product-door evidence remains applicable to identical artifacts"
22
+ ]
23
+ },
24
+ "15244d929d317facaef3d1580b19b80be338f872": {
25
+ "contractSha256": "6de082ba7ea63f1a5fc7529d81664a926e46a5770b044d5a3737e972c22a11d0",
26
+ "evidence": [
27
+ "https://github.com/volter-ai/ztrack/pull/83 at 0fac4b2fff3f6ca8d526760efadddabae52fb3b5: independent PASS; build-node-cli.mjs places boilerplates/presets at dist/boilerplates/presets (no codec artifact)",
28
+ "https://github.com/volter-ai/ztrack/pull/84 at c63037efe9124abc2ab2d6f2cd0b1a88c3d60975: independent PASS; visualizerPresentation.ts packageRoot() looks for @volter/ztrack (no codec artifact); merge 15244d929d317facaef3d1580b19b80be338f872",
29
+ "exact 15244d9 Linux (WSL2 x64) build with frozen dependencies and pinned Bun 1.2.20 (node scripts/pinned-bun.mjs pnpm --filter @volter/ztrack build): all 15 public artifact records and the canonical contract hash match qualified c81532c89 and 6bb0ecb8"
30
+ ]
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,71 @@
1
+ // The source-bound reader declaration is checked against shipped modules and the public codec artifacts.
2
+ // A source or codec edit invalidates it until regeneration and independent review qualify the new head.
3
+ import { createHash } from 'node:crypto';
4
+ import { createRequire } from 'node:module';
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, join, resolve, sep } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { nativeJsonEqual } from '@volter/supercode-interchange/native-json';
9
+
10
+ const RESTING_V1_CARD_FILES = ['store.mjs', 'files.mjs', 'ztrack.mjs', 'published.mjs', 'engine.mjs', 'workflow.mjs',
11
+ 'capacity.mjs', 'card-state.mjs', 'dispatch.mjs', 'cli.mjs', 'generation-owner.mjs', 'fence.mjs', 'storage-contract.mjs',
12
+ 'agent-declarations.mjs', 'listing.mjs', 'incidents.mjs', 'args.mjs', '../agents.mjs', '../kanban-worker.mjs',
13
+ 'resting-state.mjs', 'session-continuity.mjs', 'resting-admission.mjs', 'resting-protocol.mjs'];
14
+ export const RESTING_PROTOCOL_FILES = [...RESTING_V1_CARD_FILES, 'event-stream.mjs', 'publication-reader.mjs'];
15
+ // The serving predecessor's exact inventory (4d2208fe): its declaration also bound execution.mjs, ci.mjs and tools.mjs.
16
+ // This writer reads it to admit that reader as its rollback (ADR 0080); it declares the set above.
17
+ const RESTING_PREDECESSOR_FILES = [...RESTING_PROTOCOL_FILES, 'execution.mjs', 'ci.mjs', 'tools.mjs'];
18
+ const ownEntry = fileURLToPath(new URL('../bin/orchestrator.mjs', import.meta.url));
19
+ export function restingProtocolSources(entry = ownEntry, files = RESTING_PROTOCOL_FILES) {
20
+ const directory = resolve(dirname(entry), '../board');
21
+ return Object.fromEntries(files.map(file => [file, createHash('sha256').update(readFileSync(join(directory, file))).digest('hex')]));
22
+ }
23
+ export function restingCodecContract(entry = ownEntry) {
24
+ const resolver = createRequire(entry);
25
+ const codec = resolver('@volter/ztrack/board-protocol');
26
+ if (typeof codec.boardDocumentContract !== 'function') throw new Error('resting-state reader has no bound public codec contract');
27
+ const contract = codec.boardDocumentContract();
28
+ if (contract?.formatFeatures != null && (!Array.isArray(contract.formatFeatures)
29
+ || contract.formatFeatures.some(feature => typeof feature !== 'string')))
30
+ throw new Error('invalid public codec format feature inventory');
31
+ if (contract?.version !== 1 || !Array.isArray(contract.readModes) || !contract.readModes.includes('legacy-holds') ||
32
+ !contract.readModes.includes('backlog-bounded-pause-v1') || !['legacy-holds', 'backlog-bounded-pause-v1'].includes(contract.writer) ||
33
+ contract.preservation?.mode !== 'backlog-bounded-pause-v1' || contract.preservation?.requiresCompatibleAdmission !== true ||
34
+ !contract.artifacts || typeof contract.artifacts !== 'object') throw new Error('invalid public resting-state codec contract');
35
+ const directory = dirname(resolver.resolve('@volter/ztrack/board-protocol'));
36
+ const packageRoot = dirname(resolver.resolve('@volter/ztrack/package.json'));
37
+ const required = ['./boardProtocol.js', './boardMode.js', './boardDocument.js', './documentParser.js', './documentWriteBack.js',
38
+ './markdownDocument.js', './legacyBoard/boardDocument.js', './legacyBoard/documentParser.js', './legacyBoard/documentWriteBack.js',
39
+ './legacyBoard/markdownDocument.js', './legacyBoard/kanban.js', '../kanban-preset.cjs', '../preset-kit.cjs', '../../package.json'];
40
+ for (const name of required) if (!Object.hasOwn(contract.artifacts, name)) throw new Error(`codec contract omits ${name}`);
41
+ for (const [name, receipt] of Object.entries(contract.artifacts)) {
42
+ const path = resolve(directory, name);
43
+ if (!path.startsWith(packageRoot + sep)) throw new Error('codec artifact escapes its selected package');
44
+ const bytes = readFileSync(path);
45
+ if (receipt.bytes !== bytes.length || receipt.sha256 !== createHash('sha256').update(bytes).digest('hex'))
46
+ throw new Error(`codec artifact differs from its declared bytes: ${name}`);
47
+ }
48
+ for (const [name, path] of [['board-document', './boardDocument.js'], ['kanban-preset', '../kanban-preset.cjs'], ['preset-kit', '../preset-kit.cjs']])
49
+ if (resolver.resolve(`@volter/ztrack/${name}`) !== resolve(directory, path)) throw new Error(`codec public export ${name} changed`);
50
+ return contract;
51
+ }
52
+ export function restingContract(entry = ownEntry, { optional = false } = {}) {
53
+ let manifest;
54
+ try { manifest = JSON.parse(readFileSync(resolve(dirname(entry), '../board/resting-state-protocol.json'), 'utf8')); }
55
+ catch (error) { if (optional && error.code === 'ENOENT') return null; throw error; }
56
+ const stateSource = readFileSync(resolve(dirname(entry), '../board/resting-state.mjs'), 'utf8');
57
+ // The first v1 readers already read the complete card format. Adding a pure publication reader to later source
58
+ // declarations must not invalidate their retained card-reader capability. Accept only these exact known source
59
+ // sets, checking every declared byte; the current writer's generated declaration always includes the newer pair.
60
+ const keys = Object.keys(manifest.sources ?? {}).sort();
61
+ const files = [RESTING_PROTOCOL_FILES, RESTING_PREDECESSOR_FILES, RESTING_V1_CARD_FILES].find(files => nativeJsonEqual([...files].sort(), keys));
62
+ const sourceWriter = /export const RESTING_WRITE_MODE = '(legacy-holds|backlog-bounded-pause-v1)';/.exec(stateSource)?.[1];
63
+ if (!sourceWriter || manifest.protocol?.writer !== sourceWriter || manifest.version !== 1 || manifest.protocol?.version !== 1 ||
64
+ !Array.isArray(manifest.protocol.readModes) || !manifest.protocol.readModes.includes('legacy-holds') ||
65
+ !manifest.protocol.readModes.includes('backlog-bounded-pause-v1') ||
66
+ !['legacy-holds', 'backlog-bounded-pause-v1'].includes(manifest.protocol.writer) ||
67
+ !files || !nativeJsonEqual(manifest.sources, restingProtocolSources(entry, files)) ||
68
+ !nativeJsonEqual(manifest.codec, restingCodecContract(entry)))
69
+ throw new Error('resting-state reader contract does not bind the selected source and public codec artifacts');
70
+ return manifest;
71
+ }
@@ -0,0 +1,120 @@
1
+ {
2
+ "version": 1,
3
+ "protocol": {
4
+ "version": 1,
5
+ "readModes": [
6
+ "legacy-holds",
7
+ "backlog-bounded-pause-v1"
8
+ ],
9
+ "writer": "backlog-bounded-pause-v1"
10
+ },
11
+ "sources": {
12
+ "store.mjs": "bd9b997c6f9edb43a68487ab1cd4986dbe2d17d059fb308df30f10663337bca6",
13
+ "files.mjs": "73a523dc68e20cf6ffaae9c153dd050f5a8f5372d7e9f9ba25be3ed5476a62e8",
14
+ "ztrack.mjs": "ef2c61b3edf82c95301f0ac131e3ac5421141e494d6d64067643e0d524f3a4d9",
15
+ "published.mjs": "3036ec76f6cb12e6e9de422ad9fe034b0d38bd4dfe27072467c1242305884f5c",
16
+ "engine.mjs": "d71281edb326a62d639dce4be25a159d93a373a3132146dd0b543171925dfb52",
17
+ "workflow.mjs": "68d2c94a2a5094b7941d8150fb760ba0f5955df7d6e0e0f0047e300855341b84",
18
+ "capacity.mjs": "67c10c856c0705a9d39b79a86f0548cd7d4fbf1c4ab1682f8c0ad33e01d8211a",
19
+ "card-state.mjs": "1228f54a0ec6e2bd84954bb6a93acf9324214aec462a651bf0b03a1d91965ef2",
20
+ "dispatch.mjs": "019feb1a3f2b6739401d1c671bbfb921923a096b51db5fb5de32e00a74ac1794",
21
+ "cli.mjs": "b5ffebe93d70a49997039b50008494d5af5b38714edfcd1d02d8c2daa80f271e",
22
+ "generation-owner.mjs": "074c99e565e03bd6ace918aadc5eb7b353bbfcb0ed8d4d7f68c839eabb7016ed",
23
+ "fence.mjs": "e2fc0811e94634cd653d3d436611f67dc3f1068752c1d04617a8f9f1e35435f0",
24
+ "storage-contract.mjs": "581a059151b5443a95e38e5ee7dab9c77cddd59bdd407a16e0348ae5b7960c86",
25
+ "agent-declarations.mjs": "f44327c3ffaf71c91534030c62d4fa2e51717669eba91f33d7fb8269c1f0d101",
26
+ "listing.mjs": "546bde7cf7aaac5b9b198f31ea9c2fe8baf1d7256b398184bd2ea734c3614cd3",
27
+ "incidents.mjs": "4181db056fbf8dab8592984b20130fc7fb6dd316c54fba8e1cf76b9a15d7e224",
28
+ "args.mjs": "9f3e3707dc54d934f2326d12f000dc882f5a97980612413377f396b39fc755f4",
29
+ "../agents.mjs": "40eb279c5b0c618f103c2d28c9a777f344c6168ac6be0833b1e84d92b13e183f",
30
+ "../kanban-worker.mjs": "19e9f20ba9d95329b600516c350b1feaf8931dd62021596ded9f8be6f8fb4532",
31
+ "resting-state.mjs": "9c37266217ffcaa0d133118e3223a4ed28df5973c8b253cc1d3bd4df8dfb111c",
32
+ "session-continuity.mjs": "79adebb63c7851be6f95cf622df705afda8a9c0e6a9295315e8b8128c1c31518",
33
+ "resting-admission.mjs": "636d96991ec3aeb62cf2f415854f7ba2e85b435f28728677e18bb5ce6d3112ea",
34
+ "resting-protocol.mjs": "f25e6efe4e5dd31ccae92393d91b4d10f786aacececc43ae8680ffcf644e826c",
35
+ "event-stream.mjs": "23adb977d7123b90d151d35979138dfe7980dcbb523f738bb93a2fd01acfeda1",
36
+ "publication-reader.mjs": "1ffd82970ba927e2807bb27015c9a8a3f812157ac68c15612b8cd94ab26f0ef6"
37
+ },
38
+ "codec": {
39
+ "version": 1,
40
+ "readModes": [
41
+ "legacy-holds",
42
+ "backlog-bounded-pause-v1"
43
+ ],
44
+ "formatFeatures": [
45
+ "acceptance-layout-v1"
46
+ ],
47
+ "defaultReader": "legacy-holds",
48
+ "writer": "legacy-holds",
49
+ "legacySource": "be866b90b7b36ebf36ff998b510291a167a3ff2e",
50
+ "preservation": {
51
+ "option": {
52
+ "resting": "preserve"
53
+ },
54
+ "mode": "backlog-bounded-pause-v1",
55
+ "requiresCompatibleAdmission": true
56
+ },
57
+ "artifacts": {
58
+ "../../package.json": {
59
+ "sha256": "d552f041606f781d1e91c9ffe18aa80ccefac17b405e6cef2a3904f63672af4e",
60
+ "bytes": 5071
61
+ },
62
+ "./boardProtocol.js": {
63
+ "sha256": "a42bf525c129aacfab137743e8f35fddf1daafc1f6179705e26228266a50f0e1",
64
+ "bytes": 1688
65
+ },
66
+ "./boardMode.js": {
67
+ "sha256": "7605bc7f4184ee8c0a0df117cb9d1c88dced8901edad91de08e0e278fea5ee68",
68
+ "bytes": 625
69
+ },
70
+ "./boardDocument.js": {
71
+ "sha256": "4c7aed341de930dfa96c110e602aee47f0ab36d60c41777ddafee66dca68fb01",
72
+ "bytes": 14130
73
+ },
74
+ "./documentParser.js": {
75
+ "sha256": "6bb69da8c381ef6695fd27d63c88483dcadc7f506519d64a1d621e2eab07e906",
76
+ "bytes": 12228
77
+ },
78
+ "./documentWriteBack.js": {
79
+ "sha256": "5350ade8b30acaf48367751835eb761662d3a1f74ddf5c6a548894c9c56e9905",
80
+ "bytes": 23534
81
+ },
82
+ "./markdownDocument.js": {
83
+ "sha256": "1ac23c01b066b1f5aece80a6e6d7c5d74959759bbe3fa4934d0751adc1daefb4",
84
+ "bytes": 24064
85
+ },
86
+ "./legacyBoard/boardDocument.js": {
87
+ "sha256": "c0caac097a65fa6e7c8ba87bcb11aedeb9a5bdc31e9c3c295fd9ab7e44dd5029",
88
+ "bytes": 10213
89
+ },
90
+ "./legacyBoard/documentParser.js": {
91
+ "sha256": "3f1d5742ae8c0eaf311ea0564da5736607843233a00702c94b3a9c500a939579",
92
+ "bytes": 11431
93
+ },
94
+ "./legacyBoard/documentWriteBack.js": {
95
+ "sha256": "bd43dc7d32122b78f1fbbd73eee0a5b6bc38b65b2d23bf753b4492dfb806dc86",
96
+ "bytes": 17272
97
+ },
98
+ "./legacyBoard/markdownDocument.js": {
99
+ "sha256": "829848e792e95f4e6a24f9c7312db35c2a8ad6c1727447b4766c9600e8c2d705",
100
+ "bytes": 24242
101
+ },
102
+ "./legacyBoard/kanban.js": {
103
+ "sha256": "d583dc1890d9ad178ef9df34493db793e4a1fb93a4fc68c95a12f590701dcfcc",
104
+ "bytes": 26471
105
+ },
106
+ "../kanban-preset.cjs": {
107
+ "sha256": "47bb32dbbb11b3ab6aaadcd48abd6f5cd0840ad39c1351e8a69f467a23b6004c",
108
+ "bytes": 39901
109
+ },
110
+ "../preset-kit.cjs": {
111
+ "sha256": "729a85b3d9b19fe6f93bbb262f3d7ab56c4d07f6c3c4ea18b1f3b0f2aac8a8d6",
112
+ "bytes": 586547
113
+ },
114
+ "../../boilerplates/presets/kanban.ts": {
115
+ "sha256": "da48456687b655f0f8504cd54315ad5a99ca8833416b5efad03957d917a70e62",
116
+ "bytes": 52461
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,113 @@
1
+ // Resting cards retain conversations, never unresolved execution holdings (ADR 0248).
2
+ // These checks also run at import boundaries; a raw flag is not proof that a session stopped.
3
+ const parse = value => { try { return typeof value === 'string' ? JSON.parse(value) : value; } catch { return null; } };
4
+
5
+ // This is a reviewed source decision, never an environment toggle. The second release changes it only after
6
+ // the selected predecessor has demonstrated the complete native and codec reader contract.
7
+ export const RESTING_WRITE_MODE = 'backlog-bounded-pause-v1';
8
+ export const NEW_HOLD_SQL = "(backlog IS NOT NULL AND backlog != 'null') OR (json_type(CASE WHEN json_valid(paused) THEN paused END,'$.until') IS NOT NULL OR json_type(CASE WHEN json_valid(paused) THEN paused END,'$.generation') IS NOT NULL)";
9
+ let compatibleReader = false, readerBound = false;
10
+ export function bindRestingReader(compatible) {
11
+ if (readerBound) throw new Error('the native resting-state reader admission is already bound');
12
+ compatibleReader = compatible === true; readerBound = true;
13
+ }
14
+ export const restingReaderBound = () => readerBound;
15
+ export const restingSerializationOptions = () => compatibleReader ? { resting: 'preserve' } : { resting: 'legacy' };
16
+ export function assertRestingInput(pause, backlog, { source = 'store' } = {}) {
17
+ if (compatibleReader) return;
18
+ const held = parse(pause);
19
+ if ((backlog != null && backlog !== 'null') || (held && (Object.hasOwn(held, 'until') || Object.hasOwn(held, 'generation'))))
20
+ throw new Error(`${source}: new hold metadata has no compatible incumbent reader; predecessor retained`);
21
+ }
22
+ export function assertRestingRows(db, source = 'store') {
23
+ if (compatibleReader) return;
24
+ const row = db.prepare(`SELECT task_id,paused,backlog FROM supercode_cards WHERE ${NEW_HOLD_SQL} LIMIT 1`).get();
25
+ if (row) assertRestingInput(row.paused, row.backlog, { source: `${source}:${row.task_id}` });
26
+ }
27
+ export function assertRestingWrite(name, value, previous = null) {
28
+ if (!value) return;
29
+ if (RESTING_WRITE_MODE === 'legacy-holds') {
30
+ if (name === 'backlog' || value.until != null || value.generation != null || previous?.until != null || previous?.generation != null)
31
+ throw new Error('the reader bridge cannot create or extend bounded pauses or backlog; adopt the reviewed field writer first');
32
+ } else if (!compatibleReader) throw new Error('native hold writes require an actually selected compatible reader bridge');
33
+ }
34
+
35
+ export const PAUSE_DEADLINE = "json_extract(CASE WHEN json_valid(paused) THEN paused END,'$.until')";
36
+
37
+ /** Normalize both waiting verbs on the board's clock. A pause requires an explicit future UTC deadline;
38
+ * a heartbeat may omit its deadline and retains invalid input as missing detail. */
39
+ export function deadlineUntil(value, clock, verb) {
40
+ const pause = verb === 'pause';
41
+ if (!pause && (value == null || value === true || typeof value === 'string' && !value.trim())) return null;
42
+ let deadline;
43
+ if (typeof value === 'number') deadline = value;
44
+ else {
45
+ const text = String(value ?? '').trim();
46
+ const duration = /^(\d+(?:\.\d+)?)(\s*)([smhdw])$/i.exec(text);
47
+ if (duration && (!pause || !duration[2] && duration[3].toLowerCase() !== 'w'))
48
+ deadline = clock + Number(duration[1]) * { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[duration[3].toLowerCase()];
49
+ else if (!pause && /^\d+(?:\.\d+)?$/.test(text)) deadline = Number(text);
50
+ else if (!pause || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|\+00:00)$/i.test(text)) {
51
+ deadline = Date.parse(text) / 1000;
52
+ // Date.parse normalizes impossible month days; those are typos, not authorization.
53
+ if (pause && Number.isFinite(deadline) && new Date(deadline * 1000).toISOString().slice(0, 10) !== text.slice(0, 10)) deadline = NaN;
54
+ }
55
+ }
56
+ const rounded = Math.ceil(deadline);
57
+ if (!Number.isSafeInteger(rounded) || !Number.isFinite(new Date(rounded * 1000).getTime()) || pause && deadline <= clock) {
58
+ if (pause) throw new Error('pause requires a finite future --until in UTC (2026-10-30T12:00Z) or a duration (2h)');
59
+ return NaN;
60
+ }
61
+ return rounded;
62
+ }
63
+
64
+ /** Old attributed pauses are readable; a new deadline always travels with its generation. */
65
+ export function assertPauseRecord(pause, { id = 'card', source = 'store' } = {}) {
66
+ if (pause == null) return;
67
+ if (!pause || typeof pause !== 'object' || Array.isArray(pause) || typeof pause.by !== 'string' || !pause.by.trim() || typeof pause.reason !== 'string' || !pause.reason.trim()
68
+ || Object.keys(pause).some(key => !['by', 'reason', 'until', 'generation'].includes(key))
69
+ || ((Object.hasOwn(pause, 'until') || Object.hasOwn(pause, 'generation')) && (!Number.isSafeInteger(pause.until) || pause.until <= 0 || !Number.isFinite(new Date(pause.until * 1000).getTime()) || typeof pause.generation !== 'string' || !pause.generation.trim())))
70
+ throw new Error(`${id}: ${source} has malformed pause metadata`);
71
+ }
72
+
73
+ /** Current stopped/released evidence, including launches that never bound a native id. */
74
+ export function backlogQualification(db, id) {
75
+ const task = db.prepare('SELECT current_run_id FROM tasks WHERE id=?').get(id);
76
+ if (!task) return { qualified: false, reason: `no card ${id}` };
77
+ if (task.current_run_id != null) return { qualified: false, reason: `current run ${task.current_run_id} must end before backlog entry` };
78
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_side_sessions'").get()
79
+ && db.prepare('SELECT 1 FROM supercode_side_sessions WHERE task_id=? LIMIT 1').get(id))
80
+ return { qualified: false, reason: 'attached side sessions must be explicitly released before backlog entry' };
81
+ const custody = new Set(db.prepare('SELECT address FROM supercode_session_custody WHERE task_id=?').all(id).map(row => row.address));
82
+ for (const row of db.prepare('SELECT id,ended_at,worker_pid,metadata FROM task_runs WHERE task_id=? ORDER BY id DESC').all(id)) {
83
+ if (row.ended_at == null) return { qualified: false, reason: `run ${row.id} has not ended` };
84
+ const session = parse(row.metadata)?.supercode;
85
+ if (!session) {
86
+ if (row.worker_pid) return { qualified: false, reason: `run ${row.id}'s worker has no confirmed stop receipt` };
87
+ continue;
88
+ }
89
+ if (session.close_transferred_to) {
90
+ const successor = db.prepare('SELECT task_id,metadata FROM task_runs WHERE id=?').get(session.close_transferred_to);
91
+ const next = parse(successor?.metadata)?.supercode;
92
+ if (next && next.machine === session.machine && ((next.session_id && next.session_id === session.session_id) || (next.pane && next.pane === session.pane))) continue;
93
+ return { qualified: false, reason: `run ${row.id}'s execution transfer has no matching successor` };
94
+ }
95
+ const address = session.address ?? (session.machine && session.session_id ? `sc:${session.machine}:${session.harness ?? 'claude-code'}:${session.session_id}` : null);
96
+ if (session.detached_at || (address && custody.has(address))) continue;
97
+ if (session.close_confirmed_at) continue; // includes an adopted session's explicit release receipt
98
+ if (session.pane || session.launch_key || session.session_id || session.pid || session.launch_pending || row.worker_pid) return {
99
+ qualified: false, reason: `run ${row.id}'s ${session.slot ?? 'worker'} execution has no confirmed close/release receipt; unknown liveness is not absence`,
100
+ };
101
+ }
102
+ return { qualified: true, reason: null };
103
+ }
104
+
105
+ /** Importers call this after staging rows, before publication or dispatch can observe them. */
106
+ export function assertBacklogQualified(db, id, { backlog, source = 'backlog' } = {}) {
107
+ if (backlog == null) return;
108
+ if (!backlog || typeof backlog !== 'object' || Array.isArray(backlog) || typeof backlog.by !== 'string' || !backlog.by.trim() || typeof backlog.reason !== 'string' || !backlog.reason.trim()
109
+ || Object.keys(backlog).some(key => !['by', 'reason', 'since'].includes(key))
110
+ || !Number.isSafeInteger(backlog.since) || backlog.since < 0) throw new Error(`${id}: ${source} has malformed backlog attribution`);
111
+ const result = backlogQualification(db, id);
112
+ if (!result.qualified) throw new Error(`${id}: ${source} refused: ${result.reason}`);
113
+ }
@@ -1,4 +1,4 @@
1
- // A machine's public door (`harness.v1.*`), called in this process through the held channel to its daemon (doors.mjs):
1
+ // A machine's public doors (`harness.v1.*`, or `files.stat` for physical sources), called through the held channel to its daemon (doors.mjs):
2
2
  // no helper process, nothing that waits on this process's event loop. Answered as the helper it replaces answered,
3
3
  // `{code, stdout, stderr}`, so a caller reads a door's refusal as it always did.
4
4
  import { machineCall } from './doors.mjs';
@@ -11,7 +11,7 @@ import { machineCall } from './doors.mjs';
11
11
  */
12
12
  export async function runtimeDoor(machine, method, params = {}, { timeout = 60_000, as = null, signal = null } = {}) {
13
13
  try {
14
- if (!method.startsWith('harness.v1.')) throw Object.assign(new Error('the runtime door accepts harness.v1 methods'), { code: 'invalid_request' });
14
+ if (!method.startsWith('harness.v1.') && method !== 'files.stat') throw Object.assign(new Error('the runtime door accepts harness.v1 methods or files.stat'), { code: 'invalid_request' });
15
15
  const acting = as && !method.startsWith('harness.v1.sessions.');
16
16
  const named = !acting ? params
17
17
  : machine ? { ...params, caller: as }
@@ -0,0 +1,127 @@
1
+ // An established role slot is continued unless a retained deliberate act authorizes its successor (ADR 0248).
2
+ // Authorization lives with the old execution references, not in a second scheduling database.
3
+ import { randomUUID } from 'node:crypto';
4
+ import { actorOf, event, json, now, runField } from './store.mjs';
5
+
6
+ const slotSql = () => `COALESCE(${runField('slot')},'worker')`;
7
+ const identified = () => `(${runField('session_id')} IS NOT NULL OR ${runField('launch_key')} IS NOT NULL OR ${runField('pid')} IS NOT NULL OR ${runField('pane')} IS NOT NULL)
8
+ AND NOT (COALESCE(${runField('launch_not_issued')},0)=1 AND ${runField('mode')}='fresh' AND ${runField('close_confirmed_at')} IS NOT NULL)`;
9
+ export const slotGeneration = (session, run) => session?.slot_generation
10
+ ?? `${session?.harness ?? 'claude-code'}:${session?.session_id ?? session?.launch_key ?? `run-${run}`}`;
11
+
12
+ /** The latest execution reference, including launches whose native id was never returned. */
13
+ export function establishedSlot(db, task, slot = 'worker', beforeRun = null) {
14
+ const row = db.prepare(`SELECT id,metadata FROM task_runs WHERE task_id=? AND ${slotSql()}=? AND ${identified()}
15
+ ${beforeRun == null ? '' : 'AND id<?'} ORDER BY id DESC LIMIT 1`).get(task, slot, ...(beforeRun == null ? [] : [beforeRun]));
16
+ if (!row) return null;
17
+ const session = json(row.metadata, {})?.supercode;
18
+ return { run: row.id, session, generation: slotGeneration(session, row.id) };
19
+ }
20
+
21
+ /** Called only by an admitted workflow action or an authenticated assignment-change receipt. */
22
+ export function authorizeReplacement(db, task, slot, { source, target, reason = null }, beforeRun = null) {
23
+ const prior = establishedSlot(db, task, slot, beforeRun);
24
+ if (!prior) return null;
25
+ const existing = prior.session.replacement_authorization;
26
+ if (existing?.source?.id != null && existing.source.id === source.id
27
+ && existing.generation === prior.generation && JSON.stringify(existing.target) === JSON.stringify(target)) return existing;
28
+ const authorization = { id: randomUUID(), slot, generation: prior.generation, prior_run: prior.run,
29
+ prior_session: prior.session.session_id ?? null, prior_agent: prior.session.agent_id ?? null,
30
+ source, target, reason, by: actorOf(), at: now(), issued: null };
31
+ authorization.event = event(db, task, 'session_replacement_authorized', authorization);
32
+ for (const row of db.prepare(`SELECT id,metadata FROM task_runs WHERE task_id=? AND ${slotSql()}=? AND ${identified()}
33
+ ${beforeRun == null ? '' : 'AND id<?'}`).all(task, slot, ...(beforeRun == null ? [] : [beforeRun]))) {
34
+ const whole = json(row.metadata, {});
35
+ whole.supercode = { ...whole.supercode, replacement_authorization: authorization };
36
+ db.prepare('UPDATE task_runs SET metadata=? WHERE id=?').run(JSON.stringify(whole), row.id);
37
+ }
38
+ return authorization;
39
+ }
40
+
41
+ export function continuationRefusal(prior, why) {
42
+ const session = prior?.session?.session_id ?? prior?.session?.launch_key ?? `run ${prior?.run}`;
43
+ const error = new Error(`session ${session} (${prior?.session?.slot ?? 'worker'}) could not be continued: ${why}. A retained deliberate manager decision is required before creating a fresh conversation`);
44
+ error.resumeFailed = { session_id: prior?.session?.session_id ?? null, slot: prior?.session?.slot ?? 'worker', why };
45
+ error.continuationRequired = true;
46
+ return error;
47
+ }
48
+
49
+ /** An old forgotten marker alone grants nothing. Recheck the current intended configuration on every use. */
50
+ export function replacementFor(db, task, slot, { harness, cardMachine, agent = undefined, assignment = null, beforeRun = null } = {}) {
51
+ const prior = establishedSlot(db, task, slot, beforeRun);
52
+ const authorization = prior?.session?.replacement_authorization;
53
+ if (!authorization || authorization.generation !== prior.generation) return { prior, authorization: null };
54
+ const target = authorization.target ?? {};
55
+ if (target.harness != null && target.harness !== harness) return { prior, authorization: null };
56
+ if (Object.hasOwn(target, 'machine') && (target.machine ?? null) !== (cardMachine ?? null)) return { prior, authorization: null };
57
+ if (agent !== undefined && target.agent != null && target.agent !== agent) return { prior, authorization: null };
58
+ if (target.assignment != null && (!assignment || assignment.id !== target.assignment || assignment.agent_id !== target.agent
59
+ || assignment.state !== 'active')) return { prior, authorization: null };
60
+ if (target.assignment_event != null) {
61
+ const latest = db.prepare("SELECT id FROM task_events WHERE task_id=? AND kind='agent_assignment_reassigned' AND json_extract(payload,'$.assignment')=? ORDER BY id DESC LIMIT 1").get(task, target.assignment);
62
+ if (latest?.id !== target.assignment_event) return { prior, authorization: null };
63
+ }
64
+ // An issued authorization is never reinterpreted as permission for the next run.
65
+ if (authorization.issued && authorization.issued.run !== beforeRun) return { prior, authorization: null };
66
+ return { prior, authorization };
67
+ }
68
+
69
+ /** Commit before the launch door. A retry may recover this one key, never choose another conversation. */
70
+ export function issueReplacement(db, task, slot, authorization, { run, key, session, harness, cardMachine, agent, assignment }) {
71
+ const current = replacementFor(db, task, slot, { harness, cardMachine, agent, assignment, beforeRun: run });
72
+ if (!current.authorization || current.authorization.id !== authorization.id) throw continuationRefusal(current.prior, 'the replacement decision changed before issuance');
73
+ const issued = current.authorization.issued;
74
+ if (issued && (issued.key !== key || issued.session !== session)) throw continuationRefusal(current.prior, 'this replacement decision already issued a different launch');
75
+ if (issued) return issued;
76
+ const next = { ...current.authorization, issued: { run, key, session, at: now() } };
77
+ for (const row of db.prepare(`SELECT id,metadata FROM task_runs WHERE task_id=? AND ${slotSql()}=? AND id<?`).all(task, slot, run)) {
78
+ const whole = json(row.metadata, {});
79
+ if (whole.supercode?.replacement_authorization?.id !== authorization.id) continue;
80
+ whole.supercode.replacement_authorization = next;
81
+ db.prepare('UPDATE task_runs SET metadata=? WHERE id=?').run(JSON.stringify(whole), row.id);
82
+ }
83
+ event(db, task, 'session_replacement_issued', { authorization: authorization.id, source: authorization.source, ...next.issued }, run);
84
+ return next.issued;
85
+ }
86
+
87
+ /** A close the machine refused for good (not deferred): the board will not try it again, and the session runs on. */
88
+ export const closeRefused = (s) => Boolean(s?.close_failed_at && s.close_error && !/^close deferred/.test(s.close_error) && !s.close_confirmed_at);
89
+
90
+ /** No previous board-owned process may survive beside a replacement; adopted references release rather than close.
91
+ * A close under way is waited for (deferred, retried next round); only one never requested, or refused for good, refuses. */
92
+ export function requireReplacementTeardown(db, task, slot, run) {
93
+ for (const row of db.prepare(`SELECT id,metadata FROM task_runs WHERE task_id=? AND ${slotSql()}=? AND id<? AND ${identified()}`).all(task, slot, run)) {
94
+ const session = json(row.metadata, {})?.supercode;
95
+ if (session?.close_confirmed_at || session?.close_transferred_to) continue;
96
+ if (session?.close_requested && !closeRefused(session))
97
+ throw Object.assign(new Error(`run ${row.id}'s execution is still closing; its close must be confirmed before its replacement starts`), { launchDeferred: true });
98
+ throw continuationRefusal({ run: row.id, session }, 'its previous execution has no confirmed close or release');
99
+ }
100
+ }
101
+
102
+ /** Consume an assignment owner's durable manager decision. Automatic identity generations carry no such decision. */
103
+ export function authorizeAgentHandoff(db, task, change, sourceEvent, assignment, requestClose) {
104
+ if (change.decision?.kind !== 'reassign_agent' || change.from === change.to || !assignment
105
+ || assignment.agent_id !== change.to || assignment.state !== 'active' || assignment.id !== change.assignment) return;
106
+ const fallback = { implementer: 'worker', reviewer: 'reviewer', auditor: 'auditor' }[change.role] ?? change.role;
107
+ const slots = db.prepare(`SELECT DISTINCT ${slotSql()} AS slot FROM task_runs WHERE task_id=?
108
+ AND COALESCE(${runField('role')},CASE ${slotSql()} WHEN 'worker' THEN 'implementer' ELSE ${slotSql()} END)=?
109
+ AND ${identified()}`).all(task, change.role).map(row => row.slot);
110
+ if (!slots.length) slots.push(fallback);
111
+ const machine = db.prepare('SELECT machine FROM supercode_cards WHERE task_id=?').get(task)?.machine ?? null;
112
+ for (const slot of slots) {
113
+ const previous = establishedSlot(db, task, slot);
114
+ if (!previous) continue;
115
+ const authorization = authorizeReplacement(db, task, slot, { source: { kind: 'reassign_agent', id: sourceEvent, receipt: change.receipt },
116
+ target: { harness: previous.session.harness ?? 'claude-code', machine, agent: change.to, assignment: change.assignment,
117
+ assignment_event: sourceEvent }, reason: `role reassigned from ${change.from} to ${change.to}` });
118
+ for (const row of db.prepare(`SELECT id,metadata FROM task_runs WHERE task_id=? AND ${slotSql()}=? AND ${identified()}`).all(task, slot)) {
119
+ requestClose(db, task, { runId: row.id });
120
+ const whole = json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(row.id)?.metadata, {});
121
+ whole.supercode = { ...whole.supercode, forgotten: now(), resumable: false, replacement_authorization: authorization };
122
+ db.prepare("UPDATE task_runs SET metadata=?,ended_at=COALESCE(ended_at,?),status=CASE WHEN ended_at IS NULL THEN 'reconfigured' ELSE status END,outcome=CASE WHEN ended_at IS NULL THEN 'reconfigured' ELSE outcome END,claim_lock=NULL,claim_expires=NULL,worker_pid=NULL WHERE id=?").run(JSON.stringify(whole), now(), row.id);
123
+ db.prepare('UPDATE tasks SET current_run_id=NULL,claim_lock=NULL,claim_expires=NULL,worker_pid=NULL WHERE id=? AND current_run_id=?').run(task, row.id);
124
+ }
125
+ event(db, task, 'dispatch_released', { reason: 'deliberate role owner reassignment', source: sourceEvent, authorization: authorization.id });
126
+ }
127
+ }
@@ -1,5 +1,6 @@
1
1
  // The dispatcher observes native turns and explicit session delegation. A
2
2
  // transcript write, a reminder reply, or sharing a profile proves no progress.
3
+ import { dispatchWait } from './queries.mjs';
3
4
 
4
5
  const parsed = (value) => {
5
6
  if (typeof value !== 'string') return value ?? null;
@@ -7,6 +8,11 @@ const parsed = (value) => {
7
8
  };
8
9
  const iso = (seconds) => Number(seconds) > 0 ? `${new Date(Number(seconds) * 1000).toISOString().slice(0, 16)}Z` : null;
9
10
  const oneLine = (text, max = 200) => String(text ?? '').trim().split('\n')[0].slice(0, max);
11
+ /** How long a session's presence reading stays current after it was observed: the dispatcher renews an unchanged
12
+ * reading once a minute (dispatch.mjs supervise), so one older than this was not renewed by a running dispatcher. */
13
+ export const PRESENCE_FRESH_SECONDS = 90;
14
+ /** Whether a presence reading (`{ observed_at }`, in seconds) is still current at `now` (in seconds). */
15
+ export const presenceFresh = (reading, now = Date.now() / 1000) => now <= Number(reading?.observed_at) + PRESENCE_FRESH_SECONDS;
10
16
 
11
17
  /**
12
18
  * A retained reading of the card's session, beside the card's workflow lane and apart from its launch receipt (ADR 0024).
@@ -15,13 +21,14 @@ const oneLine = (text, max = 200) => String(text ?? '').trim().split('\n')[0].sl
15
21
  * heartbeat `--until`), waiting on its user, or idle since a time. An ended attempt is never a live session; an unread
16
22
  * live attempt is unknown.
17
23
  * `heartbeat` is the current run's latest heartbeat event on the card ({ payload: { note, until }, at, run_id }) beside the
18
- * run's own, `last` its latest run when it has no current one; both are optional (readingContext reads them).
24
+ * run's own, `last` its latest run when it has no current one, and `queue` the dispatcher's applicable wait
25
+ * ({ reason, at }); each is optional (readingContext reads them).
19
26
  */
20
- export function executionOf(run, { heartbeat = null, last = null } = {}) {
27
+ export function executionOf(run, { heartbeat = null, last = null, queue = null, now = Date.now() / 1000 } = {}) {
21
28
  if (!run) {
22
29
  const ended = last?.ended_at != null ? last : null;
23
- return { state: 'awaiting_session', alive: false, observed_at: null, since: ended ? Number(ended.ended_at) : null,
24
- reason: ended ? `no session works it; its last run ended${ended.outcome ?? ended.status ? ` (${ended.outcome ?? ended.status})` : ''} at ${iso(ended.ended_at)}${ended.error ? `: ${oneLine(ended.error)}` : ''}`
30
+ return { state: 'awaiting_session', alive: false, observed_at: null, since: queue?.reason ? Number(queue.at) : ended ? Number(ended.ended_at) : null,
31
+ reason: queue?.reason ? oneLine(queue.reason, 300) : ended ? `no session works it; its last run ended${ended.outcome ?? ended.status ? ` (${ended.outcome ?? ended.status})` : ''} at ${iso(ended.ended_at)}${ended.error ? `: ${oneLine(ended.error)}` : ''}`
25
32
  : 'no session works it' };
26
33
  }
27
34
  const session = parsed(run.metadata)?.supercode;
@@ -31,12 +38,17 @@ export function executionOf(run, { heartbeat = null, last = null } = {}) {
31
38
  return { state: 'ended', alive: false, observed_at: session?.presence_observation?.observed_at ?? null, since: at == null ? null : Number(at),
32
39
  reason: `its session ended${at ? ` at ${iso(at)}` : ''}` };
33
40
  }
41
+ if (session?.launch_deferred && !session.session_id && !session.pane && !session.pid && !session.launch_key) {
42
+ return { state: 'awaiting_session', alive: false, observed_at: null, since: Number(session.launched_at ?? run.started_at) || null,
43
+ reason: session.launch_deferred_error ? oneLine(session.launch_deferred_error, 300) : 'its launch is deferred' };
44
+ }
34
45
  const reading = session?.presence_observation;
35
46
  if (!reading) return { state: 'unknown', alive: null, observed_at: null, reason: 'the board has not read its session yet' };
36
- const freshUntil = Number(reading.observed_at) + 90;
47
+ const freshUntil = Number(reading.observed_at) + PRESENCE_FRESH_SECONDS;
37
48
  // A stopped dispatcher cannot keep advertising its last reading, its machine wait included: every round renews the
38
- // observation (supervise), so one older than its freshness is unknown. Cached snapshot consumers use the same expiry.
39
- if (Date.now() / 1000 > freshUntil) return { state: 'unknown', alive: null, observed_at: reading.observed_at, fresh_until: freshUntil,
49
+ // observation (supervise), so one older than its freshness is unknown. A card's published snapshot carries neither
50
+ // time (published.mjs publishedExecution); the reading's time and freshness are the board's own doors' (listing.mjs).
51
+ if (!presenceFresh(reading, now)) return { state: 'unknown', alive: null, observed_at: reading.observed_at, fresh_until: freshUntil,
40
52
  reason: `the board has not read its session since ${iso(reading.observed_at)}` };
41
53
  const read = { ...reading, fresh_until: freshUntil };
42
54
  // the run's machine wait: the board read the session's machine as unlinked from the team (supervise, machine_offline)
@@ -66,7 +78,7 @@ export function executionOf(run, { heartbeat = null, last = null } = {}) {
66
78
  const card = heartbeat && run.id != null && heartbeat.run_id != null && Number(heartbeat.run_id) === Number(run.id) ? heartbeat : null;
67
79
  const beat = own && !(Number(card?.at) > Number(own.at)) ? own : card;
68
80
  const wait = parsed(beat?.payload);
69
- if (Number(wait?.until) > Date.now() / 1000) {
81
+ if (Number(wait?.until) > now) {
70
82
  return { ...read, state: 'waiting', until: Number(wait.until), since: Number(beat.at) || null,
71
83
  reason: `waiting until ${iso(wait.until)}${wait.note ? `: ${oneLine(wait.note)}` : ''}` };
72
84
  }
@@ -79,15 +91,15 @@ export function executionOf(run, { heartbeat = null, last = null } = {}) {
79
91
  reason: reading.turn ? null : `its session's turn is not known${unknownSince ? ` since ${iso(unknownSince)}` : ''}` };
80
92
  }
81
93
 
82
- /** What executionOf reads beside a card's current run, for a card in a working lane (running or review) only, as the
83
- * listing reads it (listing.mjs READING_CONTEXT): the current run's latest heartbeat event, and the latest run when
84
- * none is current. Any other lane reads none. */
94
+ /** What executionOf reads beside a card's current run, as the listing reads it (listing.mjs READING_CONTEXT): the
95
+ * dispatcher's wait when none is current, and in a working lane the latest heartbeat and run. */
85
96
  export function readingContext(db, taskId, currentRunId) {
97
+ const queue = currentRunId ? null : dispatchWait(db, taskId);
86
98
  const lane = db.prepare('SELECT COALESCE(c.stage,t.status) AS lane FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id WHERE t.id=?').get(taskId)?.lane;
87
- if (!['running', 'review'].includes(lane)) return { heartbeat: null, last: null };
99
+ if (!['running', 'review'].includes(lane)) return { heartbeat: null, last: null, queue };
88
100
  const heartbeat = currentRunId ? db.prepare("SELECT payload,created_at AS at,run_id FROM task_events WHERE task_id=? AND kind='heartbeat' AND run_id=? ORDER BY id DESC LIMIT 1").get(taskId, currentRunId) ?? null : null;
89
101
  const last = currentRunId ? null : db.prepare('SELECT status,outcome,ended_at,error FROM task_runs WHERE task_id=? ORDER BY id DESC LIMIT 1').get(taskId) ?? null;
90
- return { heartbeat, last };
102
+ return { heartbeat, last, queue };
91
103
  }
92
104
 
93
105
  /** Sessions this session started, transitively. Cycles never imply work. */
@@ -0,0 +1,24 @@
1
+ // A standalone home's command routing follows its committed native generation. Kits keep their own declaration.
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { readSourceLock, enforceSourceLock } from '@volter/supercode-teams/source-lock';
5
+ import { readSourceRecord, saveSourceRecord, sourceProgramBinding } from '@volter/supercode-teams/source-selection';
6
+
7
+ export function publishStandaloneDeclaration(root, selected, env = process.env) {
8
+ if (!selected?.candidate || existsSync(join(root, 'running-kit.json'))) return;
9
+ // A generation's scoped candidate may be activated before source commit. Only the primary selection may change
10
+ // the durable command route; the same reading repairs a committed route after its owner restarts.
11
+ const primaryEnv = { ...env };
12
+ delete primaryEnv.SUPERCODE_SOURCE_CANDIDATE;
13
+ const primary = readSourceLock(primaryEnv);
14
+ if (!primary || realpathSync(selected.entry) !== realpathSync(join(primary.root, 'sdk/orchestrator/bin/orchestrator.mjs'))
15
+ || realpathSync(selected.native) !== sourceProgramBinding(primary).program) return;
16
+ enforceSourceLock(selected.entry, 'the standalone board declaration', primaryEnv);
17
+ enforceSourceLock(selected.native, 'the standalone board program', primaryEnv);
18
+ const path = join(root, 'orchestrator.json');
19
+ const held = existsSync(path) ? readSourceRecord(path) : {};
20
+ if (held.entry === selected.entry && held.supercode?.entry === selected.native) return;
21
+ const version = entry => readSourceRecord(join(dirname(entry), '..', 'package.json')).version ?? null;
22
+ saveSourceRecord(path, { ...held, package: '@volter/supercode-orchestrator', version: version(selected.entry),
23
+ entry: selected.entry, supercode: { ...held.supercode, entry: selected.native, version: version(selected.native) } });
24
+ }