@volter/supercode-interchange 0.1.22 → 0.1.24

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 (129) hide show
  1. package/LICENSE-diffy-MIT +23 -0
  2. package/README.md +422 -2
  3. package/channel-records.mjs +71 -0
  4. package/channel-session-counts.mjs +81 -0
  5. package/channels-read.mjs +90 -0
  6. package/corpus-audit-records.mjs +192 -0
  7. package/corpus-audit-render.mjs +47 -0
  8. package/corpus-audit-schema.mjs +89 -0
  9. package/corpus-audit.d.ts +16 -0
  10. package/corpus-audit.mjs +132 -0
  11. package/file-io.mjs +21 -2
  12. package/harness-settings.mjs +19 -0
  13. package/hermes-fire-links.mjs +135 -0
  14. package/hermes-fires.mjs +106 -0
  15. package/hermes-read-home.d.ts +3 -0
  16. package/hermes-read-home.mjs +235 -0
  17. package/hermes-read-store.mjs +49 -0
  18. package/hermes-read-validation.mjs +170 -0
  19. package/hermes-read-workflow.mjs +164 -0
  20. package/hermes-read-yaml.mjs +68 -0
  21. package/hermes-store.mjs +1 -1
  22. package/homes.mjs +2 -2
  23. package/index.d.ts +14 -1
  24. package/index.mjs +1 -1
  25. package/inspection-builtin-tools.json +261 -0
  26. package/inspection-config-fields.json +765 -0
  27. package/inspection-schemas.d.ts +4 -0
  28. package/inspection-schemas.mjs +117 -0
  29. package/job-notepad-read.mjs +97 -0
  30. package/jobs-control.mjs +250 -0
  31. package/jobs-read.mjs +198 -0
  32. package/jsonl.mjs +4 -3
  33. package/memory-show.mjs +187 -0
  34. package/metadata.mjs +45 -3
  35. package/native-json.mjs +46 -3
  36. package/ontology-contracts.d.ts +5 -0
  37. package/ontology-contracts.mjs +29 -0
  38. package/ontology-operations.json +198 -0
  39. package/ontology-schemas.json +7946 -0
  40. package/openclaw-jobs-read.mjs +117 -0
  41. package/openclaw-profile-home.mjs +142 -0
  42. package/orchestrator-read-records.mjs +112 -0
  43. package/orchestrator-store.mjs +21 -9
  44. package/package.json +59 -4
  45. package/parity-ledger.json +6984 -0
  46. package/parity-orchestration.json +5538 -0
  47. package/parity-report.d.ts +7 -0
  48. package/parity-report.mjs +46 -0
  49. package/preset-profiles.mjs +27 -0
  50. package/profile-command.mjs +32 -0
  51. package/profile-orchestrator-door.mjs +104 -0
  52. package/profile-paths.mjs +25 -0
  53. package/profiles-control.mjs +98 -0
  54. package/profiles-read.mjs +119 -0
  55. package/reduction-attribution.mjs +54 -0
  56. package/reduction-diff.mjs +165 -0
  57. package/reduction-log.mjs +177 -0
  58. package/reduction-view.mjs +145 -0
  59. package/retained-projection.mjs +9 -0
  60. package/routes-read.mjs +103 -0
  61. package/runs-read.mjs +224 -0
  62. package/session-activity.d.ts +23 -0
  63. package/session-activity.mjs +124 -0
  64. package/session-append-scan.mjs +27 -17
  65. package/session-branch-handoff.mjs +115 -0
  66. package/session-claude-graph.mjs +29 -7
  67. package/session-claude-index.mjs +732 -114
  68. package/session-claude-runtime.d.ts +28 -0
  69. package/session-claude-runtime.mjs +171 -6
  70. package/session-claude-scan-store.mjs +164 -0
  71. package/session-claude.mjs +9 -0
  72. package/session-codex-activity.d.ts +6 -0
  73. package/session-codex-activity.mjs +120 -0
  74. package/session-codex-window.mjs +24 -4
  75. package/session-config-scan.mjs +141 -0
  76. package/session-convert.d.ts +33 -0
  77. package/session-convert.mjs +179 -0
  78. package/session-emission-version.json +1 -0
  79. package/session-file.mjs +11 -1
  80. package/session-follower.mjs +17 -5
  81. package/session-fresh-id.mjs +15 -0
  82. package/session-hermes-import.mjs +61 -0
  83. package/session-hermes.mjs +1 -1
  84. package/session-import.mjs +37 -0
  85. package/session-inspect.d.ts +18 -0
  86. package/session-inspect.mjs +122 -0
  87. package/session-machine-tree.d.ts +33 -0
  88. package/session-machine-tree.mjs +274 -0
  89. package/session-materialize-files.mjs +78 -0
  90. package/session-materialize-opencode.mjs +137 -0
  91. package/session-materialize.mjs +186 -0
  92. package/session-message.mjs +85 -0
  93. package/session-model.d.ts +9 -0
  94. package/session-model.mjs +1 -0
  95. package/session-opencode-store.mjs +39 -3
  96. package/session-path.mjs +6 -3
  97. package/session-pi-content.mjs +7 -2
  98. package/session-read.d.ts +134 -2
  99. package/session-read.mjs +86 -9
  100. package/session-recorded-config.mjs +3 -2
  101. package/session-records.mjs +12 -6
  102. package/session-residue-store.mjs +90 -0
  103. package/session-residue.mjs +150 -0
  104. package/session-resume-instructions.mjs +1 -1
  105. package/session-saved-read.mjs +105 -0
  106. package/session-transfer.mjs +141 -0
  107. package/session-views.mjs +1 -0
  108. package/session-write-claude.mjs +135 -0
  109. package/session-write-codex.mjs +146 -0
  110. package/session-write-gemini.mjs +77 -0
  111. package/session-write-goose.mjs +99 -0
  112. package/session-write-grok.mjs +70 -0
  113. package/session-write-native.mjs +41 -0
  114. package/session-write-opencode.mjs +197 -0
  115. package/session-write-pi.mjs +130 -0
  116. package/session-write-residue.mjs +19 -0
  117. package/session-write-utils.mjs +77 -0
  118. package/session-write.d.ts +10 -0
  119. package/session-write.mjs +37 -0
  120. package/skill-toml.mjs +80 -0
  121. package/skills-control.mjs +193 -0
  122. package/skills-list.mjs +237 -0
  123. package/source-files.mjs +12 -2
  124. package/sqlite-reader.mjs +134 -18
  125. package/support-registry.json +402 -0
  126. package/support-report.mjs +69 -0
  127. package/trigger-records.mjs +77 -0
  128. package/triggers-read.mjs +67 -0
  129. package/workflow-read.mjs +165 -0
@@ -0,0 +1,170 @@
1
+ // Validation dependencies of the Hermes activity home projection. These
2
+ // validators interpret the complete files before a ledger can prove activity.
3
+ import { nativeInteger, nativeNumberKind, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
4
+ import { hermesRowText } from './hermes-fires.mjs';
5
+
6
+ export const hermesObject = value => value != null && typeof value === 'object' && !Array.isArray(value);
7
+ export const hermesRefuse = (file, key, message) => { throw new Error(`${file}${key ? ` [${key}]` : ''}: ${message}`); };
8
+ // Serde's derived structs also admit positional sequences. Preserve numeric
9
+ // facts when addressing their fields, instead of coercing a float to integer.
10
+ export function hermesStructValue(file, key, value, fields) {
11
+ if (!Array.isArray(value)) return value;
12
+ if (value.length > fields.length) hermesRefuse(file, key, 'too many struct fields');
13
+ return parseNativeJson(`{${value.map((_, index) => `${JSON.stringify(fields[index])}:${stringifyNativeJsonMember(value, String(index))}`).join(',')}}`);
14
+ }
15
+ const required = (file, key, value) => typeof value === 'string' && value !== '' ? value : hermesRefuse(file, key, 'expected a non-empty string');
16
+ const optional = (file, key, value) => value == null ? null : typeof value === 'string' ? value : hermesRefuse(file, key, 'expected a string');
17
+ const map = (file, key, value) => hermesObject(value) ? value : hermesRefuse(file, key, 'expected an object');
18
+ const boolean = (file, key, value) => value == null || typeof value === 'boolean' ? value : hermesRefuse(file, key, 'expected a boolean');
19
+ const unsigned = (parent, key) => nativeInteger(parent, key)
20
+ ?? (nativeNumberKind(parent, key) == null && Number.isSafeInteger(parent[key]) && parent[key] >= 0 ? BigInt(parent[key]) : null);
21
+ const keys = (file, key, value, allowed) => {
22
+ map(file, key, value);
23
+ for (const name of Object.keys(value)) if (!allowed.includes(name)) hermesRefuse(file, `${key}.${name}`, 'unknown key');
24
+ };
25
+
26
+ export function validateHermesWorker(file, worker) {
27
+ if (worker == null) return;
28
+ keys(file, 'worker', worker, ['harness', 'model', 'reasoning_effort', 'preset', 'cwd', 'env', 'permission', 'home', 'surface', 'capacity']);
29
+ required(file, 'worker.harness', worker.harness);
30
+ for (const name of ['model', 'reasoning_effort', 'preset']) optional(file, `worker.${name}`, worker[name]);
31
+ const cwd = Object.hasOwn(worker, 'cwd') ? required(file, 'worker.cwd', worker.cwd) : '.';
32
+ if (cwd.startsWith('/') || cwd.split('/').includes('..')) hermesRefuse(file, 'worker.cwd', 'must be a relative path inside the profile');
33
+ if (Object.hasOwn(worker, 'env')) for (const [name, value] of Object.entries(map(file, 'worker.env', worker.env))) {
34
+ if (typeof value !== 'string') hermesRefuse(file, `worker.env.${name}`, 'expected a string (a secret is written `${NAME}`)');
35
+ }
36
+ if (Object.hasOwn(worker, 'permission')) {
37
+ const permission = worker.permission;
38
+ keys(file, 'worker.permission', permission, ['timeout_seconds', 'default', 'unattended']);
39
+ if (Object.hasOwn(permission, 'timeout_seconds')) {
40
+ const value = unsigned(permission, 'timeout_seconds');
41
+ if (value == null || value < 1n) hermesRefuse(file, 'worker.permission.timeout_seconds', 'expected an integer >= 1');
42
+ }
43
+ if (Object.hasOwn(permission, 'default') && !['deny', 'allow'].includes(permission.default)) hermesRefuse(file, 'worker.permission.default', 'expected deny|allow');
44
+ if (Object.hasOwn(permission, 'unattended') && !['deny', 'approve'].includes(permission.unattended)) hermesRefuse(file, 'worker.permission.unattended', 'expected deny|approve');
45
+ }
46
+ for (const [name, allowed] of [['home', ['profile', 'user']], ['surface', ['headless', 'pane']]]) {
47
+ const value = optional(file, `worker.${name}`, worker[name]);
48
+ if (value != null && !allowed.includes(value)) hermesRefuse(file, `worker.${name}`, `expected ${allowed.join('|')}`);
49
+ }
50
+ if (Object.hasOwn(worker, 'capacity')) {
51
+ const value = unsigned(worker, 'capacity');
52
+ if (value == null || value < 1n || value > 0xffffffffn) hermesRefuse(file, 'worker.capacity', 'expected an integer >= 1');
53
+ }
54
+ }
55
+
56
+ export function hermesDelivery(file, key, word, extra = null) {
57
+ if (word == null) return null;
58
+ if (typeof word !== 'string' || word === '') hermesRefuse(file, key, 'expected a delivery word');
59
+ if (['origin', 'local', 'home'].includes(word)) return { kind: word };
60
+ const parts = word.split(':'), additional = hermesObject(extra) ? extra : {};
61
+ return { kind: 'explicit', platform: parts[0],
62
+ chat_id: (parts[1] ?? hermesRowText(additional, 'chat_id')) || null,
63
+ thread_id: (parts[2] ?? hermesRowText(additional, 'thread_id')) || null };
64
+ }
65
+
66
+ function validateRepeat(file, key, value) {
67
+ if (value == null || typeof value === 'boolean' || typeof value === 'number') return;
68
+ map(file, key, value);
69
+ if (value.times != null) {
70
+ const times = unsigned(value, 'times');
71
+ if (times == null || times < 1n) hermesRefuse(file, `${key}.times`, 'expected null or an integer >= 1');
72
+ }
73
+ if (Object.hasOwn(value, 'completed') && unsigned(value, 'completed') == null) hermesRefuse(file, `${key}.completed`, 'expected an integer >= 0');
74
+ }
75
+
76
+ function decodeJob(file, raw) {
77
+ map(file, '', raw);
78
+ const id = required(file, 'id', raw.id), key = name => `${id}.${name}`;
79
+ const schedule = map(file, key('schedule'), raw.schedule);
80
+ if (schedule.kind === 'once') required(file, key('schedule.run_at'), schedule.run_at);
81
+ else if (schedule.kind === 'interval') {
82
+ if (typeof schedule.minutes !== 'number' || !(schedule.minutes > 0)) hermesRefuse(file, key('schedule.minutes'), 'expected a positive number');
83
+ } else if (schedule.kind === 'cron') {
84
+ required(file, key('schedule.expr'), schedule.expr); optional(file, key('schedule.tz'), schedule.tz);
85
+ } else hermesRefuse(file, key('schedule.kind'), 'expected once|interval|cron');
86
+ for (const name of ['prompt', 'workdir', 'model', 'machine', 'next_run_at', 'last_run_at', 'last_status', 'created_at']) optional(file, key(name), raw[name]);
87
+ if (raw.context_from != null && typeof raw.context_from !== 'string' && !Array.isArray(raw.context_from)) hermesRefuse(file, key('context_from'), 'expected a job id, a list of job ids, or "self"');
88
+ const origin = hermesObject(raw.origin) ? {
89
+ platform: required(file, key('origin.platform'), raw.origin.platform),
90
+ chat_id: optional(file, key('origin.chat_id'), raw.origin.chat_id),
91
+ thread_id: optional(file, key('origin.thread_id'), raw.origin.thread_id),
92
+ } : null;
93
+ const deliver = hermesDelivery(file, key('deliver'), raw.deliver) ?? { kind: 'local' };
94
+ hermesDelivery(file, key('failure_deliver'), raw.failure_deliver);
95
+ boolean(file, key('attach_to_session'), raw.attach_to_session); boolean(file, key('enabled'), raw.enabled);
96
+ const target = optional(file, key('session_target'), raw.session_target);
97
+ if (target != null && !['main', 'current', 'isolated'].includes(target) && !target.startsWith('session:')) hermesRefuse(file, key('session_target'), 'expected main|current|isolated|session:<id>');
98
+ validateRepeat(file, key('repeat'), raw.repeat);
99
+ return { id, origin, deliver };
100
+ }
101
+
102
+ export function hermesJobValues(file, value) {
103
+ let rows;
104
+ if (Array.isArray(value)) rows = value;
105
+ else if (hermesObject(value) && Array.isArray(value.jobs)) rows = value.jobs;
106
+ else if (hermesObject(value) && hermesObject(value.jobs)) rows = Object.keys(value.jobs).map(id => {
107
+ const raw = hermesObject(value.jobs[id]) ? value.jobs[id] : {};
108
+ // Reparse rather than spread so integral REAL and exact integer facts stay
109
+ // distinct when a keyed-envelope id replaces the record's id.
110
+ const members = Object.keys(raw).filter(key => key !== 'id').map(key => `${JSON.stringify(key)}:${stringifyNativeJsonMember(raw, key)}`);
111
+ return parseNativeJson(`{${[`"id":${JSON.stringify(id)}`, ...members].join(',')}}`);
112
+ });
113
+ else hermesRefuse(file, '', 'expected an array of jobs or {"jobs": [...]}');
114
+ return rows;
115
+ }
116
+
117
+ export function hermesJobRecords(file, value) {
118
+ const jobs = new Map();
119
+ const rows = hermesJobValues(file, value);
120
+ for (const row of rows) {
121
+ const job = decodeJob(file, row);
122
+ if (jobs.has(job.id)) hermesRefuse(file, job.id, 'duplicate job id');
123
+ jobs.set(job.id, job);
124
+ }
125
+ return jobs;
126
+ }
127
+
128
+ export function validateHermesRoutes(file, value) {
129
+ if (!Array.isArray(value)) return;
130
+ value.forEach((route, index) => {
131
+ const key = `profile_routes[${index}]`;
132
+ map(file, key, route); optional(file, `${key}.name`, route.name);
133
+ required(file, `${key}.platform`, route.platform);
134
+ required(file, `${key}.${route.agent == null ? 'profile' : 'agent'}`, route.agent ?? route.profile);
135
+ });
136
+ }
137
+
138
+ export function validateHermesChannels(file, value) {
139
+ if (value === undefined) return;
140
+ for (const [platform, channel] of Object.entries(map(file, 'platforms', value))) {
141
+ map(file, `platforms.${platform}`, channel); boolean(file, `platforms.${platform}.enabled`, channel.enabled);
142
+ }
143
+ }
144
+
145
+ export function validateHermesSubscriptions(file, value) {
146
+ for (const [name, subscription] of Object.entries(map(file, '', value))) {
147
+ map(file, name, subscription);
148
+ for (const field of ['prompt', 'description', 'created_at', 'secret']) optional(file, `${name}.${field}`, subscription[field]);
149
+ hermesDelivery(file, `${name}.deliver`, subscription.deliver, subscription.deliver_extra);
150
+ }
151
+ }
152
+
153
+ export function validateHermesBudgets(file, value) {
154
+ if (value === undefined) return;
155
+ if (!Array.isArray(value)) hermesRefuse(file, 'budgets', 'expected an array');
156
+ value.forEach((raw, index) => {
157
+ const key = `budgets[${index}]`;
158
+ const budget = hermesStructValue(file, key, raw, ['period', 'limit']);
159
+ map(file, key, budget);
160
+ if (!['run', 'day', 'month'].includes(budget.period)) hermesRefuse(file, `${key}.period`, 'expected run|day|month');
161
+ const limit = map(file, `${key}.limit`, hermesStructValue(file, `${key}.limit`, budget.limit, ['tokens', 'cost']));
162
+ if (limit.tokens != null && unsigned(limit, 'tokens') == null) hermesRefuse(file, `${key}.limit.tokens`, 'expected an unsigned integer');
163
+ if (limit.cost != null) {
164
+ const cost = map(file, `${key}.limit.cost`, hermesStructValue(file, `${key}.limit.cost`, limit.cost, ['amount', 'currency', 'source']));
165
+ if (typeof cost.amount !== 'number') hermesRefuse(file, `${key}.limit.cost.amount`, 'expected a number');
166
+ if (typeof cost.currency !== 'string') hermesRefuse(file, `${key}.limit.cost.currency`, 'expected a string');
167
+ optional(file, `${key}.limit.cost.source`, cost.source);
168
+ }
169
+ });
170
+ }
@@ -0,0 +1,164 @@
1
+ // The workflow dependency of a Hermes home read. It validates the committed
2
+ // statechart as data; it never dispatches an actor or evaluates an expression.
3
+ import { hermesObject, hermesRefuse, hermesStructValue } from './hermes-read-validation.mjs';
4
+
5
+ const own = (object, key) => Object.hasOwn(object, key);
6
+ const fail = (file, key, message) => hermesRefuse(file, key, message);
7
+ const object = (file, key, value) => hermesObject(value) ? value : fail(file, key, 'expected an object');
8
+ const string = (file, key, value) => typeof value === 'string' ? value : fail(file, key, 'expected a string');
9
+ const expr = (file, key, value) => value === null || ['string', 'number', 'boolean'].includes(typeof value) ? value : fail(file, key, 'an expression is a string or a literal');
10
+ const required = (file, key, parent, field, check) => own(parent, field) ? check(file, `${key}.${field}`, parent[field]) : fail(file, `${key}.${field}`, 'missing field');
11
+ const optional = (file, key, parent, field, check) => parent[field] == null ? null : check(file, `${key}.${field}`, parent[field]);
12
+ const defaulted = (file, key, parent, field, check) => own(parent, field) ? check(file, `${key}.${field}`, parent[field]) : null;
13
+ function strict(file, key, value, allowed) {
14
+ object(file, key, value);
15
+ for (const name of Object.keys(value)) if (!allowed.includes(name)) fail(file, `${key}.${name}`, 'unknown field');
16
+ }
17
+ function array(file, key, value, check) {
18
+ if (!Array.isArray(value)) fail(file, key, 'expected an array');
19
+ value.forEach((item, index) => check(file, `${key}[${index}]`, item));
20
+ }
21
+ function map(file, key, value, check) {
22
+ object(file, key, value);
23
+ for (const [name, item] of Object.entries(value)) check(file, `${key}.${name}`, item);
24
+ }
25
+ const strings = (file, key, value) => array(file, key, value, string);
26
+ const expressions = (file, key, value) => array(file, key, value, expr);
27
+ const expressionMap = (file, key, value) => map(file, key, value, expr);
28
+ const valueMap = (file, key, value) => object(file, key, value);
29
+
30
+ function action(file, key, value) {
31
+ if (typeof value !== 'string' && (!hermesObject(value) || Object.keys(value).length !== 1)) fail(file, key, 'an action is a name or a one-key map');
32
+ }
33
+ const actions = (file, key, value) => array(file, key, value, action);
34
+ function validation(file, key, value) {
35
+ object(file, key, value); required(file, key, value, 'expr', expr); required(file, key, value, 'message', string);
36
+ }
37
+ function transition(file, key, value) {
38
+ object(file, key, value);
39
+ for (const field of ['from', 'by']) defaulted(file, key, value, field, strings);
40
+ optional(file, key, value, 'guard', expr);
41
+ defaulted(file, key, value, 'validate', (f, k, v) => array(f, k, v, validation));
42
+ for (const field of ['target', 'description']) optional(file, key, value, field, string);
43
+ defaulted(file, key, value, 'actions', actions);
44
+ }
45
+ const transitions = (file, key, value) => array(file, key, value, transition);
46
+ const transitionMap = (file, key, value) => map(file, key, value, transitions);
47
+ function delayed(file, key, value) { transition(file, key, value); required(file, key, value, 'after', expr); }
48
+ function run(file, key, value) {
49
+ object(file, key, value);
50
+ for (const field of ['role', 'session']) required(file, key, value, field, string);
51
+ defaulted(file, key, value, 'prompt', string); optional(file, key, value, 'prompt_file', string);
52
+ defaulted(file, key, value, 'skills', strings); defaulted(file, key, value, 'limits', expressionMap);
53
+ }
54
+ function status(file, key, value) {
55
+ object(file, key, value);
56
+ for (const field of ['stored_as', 'description', 'run_role', 'resume_as']) optional(file, key, value, field, string);
57
+ optional(file, key, value, 'run', run);
58
+ for (const field of ['entry', 'exit']) defaulted(file, key, value, field, actions);
59
+ defaulted(file, key, value, 'on', transitionMap); defaulted(file, key, value, 'always', transitions);
60
+ defaulted(file, key, value, 'after', (f, k, v) => array(f, k, v, delayed));
61
+ }
62
+ const statusMap = (file, key, value) => map(file, key, value, status);
63
+ // CapFilter (crates/interchange/src/orchestration/workflow.rs) in its declaration order, which a sequence-form struct
64
+ // follows: `dispatch` an optional boolean, every other key a name or a list of names.
65
+ const filterNames = ['card', 'model', 'harness', 'assignee', 'status', 'stored', 'machine', 'board'];
66
+ const filterFields = ['dispatch', ...filterNames];
67
+ function filter(file, key, value) {
68
+ strict(file, key, value, filterFields);
69
+ optional(file, key, value, 'dispatch', (f, k, v) => typeof v === 'boolean' ? v : fail(f, k, 'expected a boolean'));
70
+ for (const field of filterNames) defaulted(file, key, value, field, (f, k, v) => typeof v === 'string' ? v : strings(f, k, v));
71
+ }
72
+ function reserve(file, key, value) {
73
+ strict(file, key, value, ['for', 'slots']); required(file, key, value, 'for', filter); required(file, key, value, 'slots', expr);
74
+ }
75
+ function capacity(file, key, value) {
76
+ strict(file, key, value, ['name', 'scope', 'counts', 'where', 'unless', 'max', 'reserve']);
77
+ required(file, key, value, 'name', string);
78
+ required(file, key, value, 'scope', (f, k, v) => ['global', 'board', 'machine', 'profile'].includes(v) ? v : fail(f, k, 'unknown scope'));
79
+ required(file, key, value, 'counts', (f, k, v) => ['runs', 'cards', 'sessions', 'turns', 'starts'].includes(v) ? v : fail(f, k, 'unknown count'));
80
+ for (const field of ['where', 'unless']) defaulted(file, key, value, field, filter);
81
+ required(file, key, value, 'max', expr); optional(file, key, value, 'reserve', reserve);
82
+ }
83
+ function dispatch(file, key, value) {
84
+ strict(file, key, value, ['tick', 'start', 'eligible', 'order', 'capacity']);
85
+ required(file, key, value, 'tick', expr); required(file, key, value, 'start', string); optional(file, key, value, 'eligible', expr);
86
+ defaulted(file, key, value, 'order', expressions); defaulted(file, key, value, 'capacity', (f, k, v) => array(f, k, v, capacity));
87
+ }
88
+ function runMachine(file, key, value) {
89
+ object(file, key, value); optional(file, key, value, 'recurring_role', string);
90
+ defaulted(file, key, value, 'limits', expressionMap); defaulted(file, key, value, 'roles', (f, k, v) => map(f, k, v, run));
91
+ defaulted(file, key, value, 'states', statusMap); defaulted(file, key, value, 'initial', string);
92
+ for (const field of ['on', 'outcomes']) defaulted(file, key, value, field, transitionMap);
93
+ }
94
+ function storageRule(file, key, value) { object(file, key, value); required(file, key, value, 'guard', expr); required(file, key, value, 'status', string); }
95
+
96
+ const listShape = child => ({ list: child }), mapShape = child => ({ map: child });
97
+ const validationShape = { fields: ['expr', 'message'] };
98
+ const transitionShape = { fields: ['from', 'by', 'guard', 'validate', 'target', 'actions', 'description'], children: { validate: listShape(validationShape) } };
99
+ const transitionMapShape = mapShape(listShape(transitionShape));
100
+ const runShape = { fields: ['role', 'session', 'prompt', 'prompt_file', 'skills', 'limits'] };
101
+ const statusShape = { fields: ['stored_as', 'description', 'run', 'run_role', 'resume_as', 'entry', 'exit', 'on', 'always', 'after'], children: {
102
+ run: runShape, on: transitionMapShape, always: listShape(transitionShape),
103
+ // A flattened transition has only a mapping deserializer.
104
+ after: listShape({ children: { validate: listShape(validationShape) } }),
105
+ } };
106
+ const filterShape = { fields: filterFields };
107
+ const capacityShape = { fields: ['name', 'scope', 'counts', 'where', 'unless', 'max', 'reserve'], children: {
108
+ where: filterShape, unless: filterShape, reserve: { fields: ['for', 'slots'], children: { for: filterShape } },
109
+ } };
110
+ const workflowShape = { fields: ['run', 'verbs', 'derived', 'relations', 'flags', 'stored_as', 'losses', 'description', 'params', 'roles', 'initial', 'statuses', 'on', 'dispatch', 'notify'], children: {
111
+ run: { fields: ['recurring_role', 'limits', 'roles', 'states', 'initial', 'on', 'outcomes'], children: { roles: mapShape(runShape), states: mapShape(statusShape), on: transitionMapShape, outcomes: transitionMapShape } },
112
+ verbs: transitionMapShape, initial: listShape(transitionShape), statuses: mapShape(statusShape), on: transitionMapShape,
113
+ stored_as: listShape({ fields: ['guard', 'status'] }),
114
+ dispatch: { fields: ['tick', 'start', 'eligible', 'order', 'capacity'], children: { capacity: listShape(capacityShape) } },
115
+ } };
116
+ function normalize(file, key, value, shape) {
117
+ if (value == null) return value;
118
+ if (shape.list) return Array.isArray(value) ? value.map((item, index) => normalize(file, `${key}[${index}]`, item, shape.list)) : value;
119
+ if (shape.map) {
120
+ if (hermesObject(value)) for (const name of Object.keys(value)) value[name] = normalize(file, `${key}.${name}`, value[name], shape.map);
121
+ return value;
122
+ }
123
+ if (shape.fields) value = hermesStructValue(file, key, value, shape.fields);
124
+ if (hermesObject(value)) for (const [name, child] of Object.entries(shape.children ?? {})) if (own(value, name)) value[name] = normalize(file, `${key}.${name}`, value[name], child);
125
+ return value;
126
+ }
127
+
128
+ export function validateHermesWorkflow(file, value) {
129
+ value = normalize(file, '', value, workflowShape);
130
+ object(file, '', value); optional(file, '', value, 'run', runMachine);
131
+ for (const field of ['verbs', 'on']) defaulted(file, '', value, field, transitionMap);
132
+ for (const field of ['derived', 'roles']) defaulted(file, '', value, field, expressionMap);
133
+ for (const field of ['relations', 'flags', 'params']) defaulted(file, '', value, field, valueMap);
134
+ defaulted(file, '', value, 'stored_as', (f, k, v) => array(f, k, v, storageRule)); defaulted(file, '', value, 'losses', strings);
135
+ optional(file, '', value, 'description', string);
136
+ required(file, '', value, 'initial', transitions); required(file, '', value, 'statuses', statusMap); required(file, '', value, 'dispatch', dispatch);
137
+ defaulted(file, '', value, 'notify', (f, k, v) => map(f, k, v, (ff, kk, vv) => map(ff, kk, vv, string)));
138
+
139
+ const known = (target, statuses, message) => {
140
+ if (target != null && !target.startsWith('{{') && !own(statuses, target)) fail(file, '', `${message} ${target}`);
141
+ };
142
+ const check = rows => { for (const row of rows ?? []) known(row.target, value.statuses, 'no status'); };
143
+ check(value.initial);
144
+ for (const rows of Object.values(value.verbs ?? {})) {
145
+ check(rows);
146
+ for (const row of rows) for (const source of row.from ?? []) if (!own(value.statuses, source)) fail(file, '', `no source status ${source}`);
147
+ }
148
+ const machine = value.run;
149
+ if (machine != null) {
150
+ const states = machine.states ?? {};
151
+ if (!own(states, machine.initial ?? '')) fail(file, '', `no initial run state ${machine.initial ?? ''}`);
152
+ for (const rows of Object.values(machine.outcomes ?? {})) check(rows);
153
+ const rules = [...Object.values(machine.on ?? {}), ...Object.values(states).flatMap(state => Object.values(state.on ?? {}))];
154
+ for (const rows of rules) for (const row of rows) known(row.target, states, 'no run state');
155
+ for (const [name, spec] of Object.entries(machine.roles ?? {})) if (!own(value.roles ?? {}, spec.role)) fail(file, '', `run ${name}: no role ${spec.role}`);
156
+ }
157
+ for (const rows of Object.values(value.on ?? {})) check(rows);
158
+ for (const [name, state] of Object.entries(value.statuses)) {
159
+ if (state.run_role != null && !own(machine?.roles ?? {}, state.run_role)) fail(file, '', `${name}: no run specification ${state.run_role}`);
160
+ for (const rows of Object.values(state.on ?? {})) check(rows);
161
+ check(state.always); check(state.after);
162
+ if (state.run != null && !own(value.roles ?? {}, state.run.role)) fail(file, '', `${name}: its run names no role ${state.run.role}`);
163
+ }
164
+ }
@@ -0,0 +1,68 @@
1
+ // YAML 1.2 file values enter the Hermes read projection with integer and
2
+ // float facts intact. A tagged value remains a tagged one-key JSON mapping.
3
+ import { isAlias, isMap, isScalar, isSeq, parseDocument } from 'yaml';
4
+ import { setNativeFloat, setNativeInteger, stringifyNativeJsonMember } from './native-json.mjs';
5
+ import { hermesRefuse } from './hermes-read-validation.mjs';
6
+
7
+ const standard = new Set(['str', 'null', 'bool', 'int', 'float', 'seq', 'map'].map(name => `tag:yaml.org,2002:${name}`));
8
+ const memberOrder = new WeakMap();
9
+ export const hermesYamlKeys = value => memberOrder.get(value) ?? Object.keys(value);
10
+ const set = (object, key, entry) => {
11
+ if (entry.kind === 'integer') setNativeInteger(object, key, entry.whole);
12
+ else if (entry.kind === 'float') setNativeFloat(object, key, entry.value);
13
+ else Object.defineProperty(object, key, { value: entry.value, enumerable: true, writable: true, configurable: true });
14
+ };
15
+ function scalar(value) {
16
+ if (typeof value === 'bigint') {
17
+ if (value >= -(1n << 63n) && value <= (1n << 64n) - 1n) return { kind: 'integer', whole: value, value: Number(value) };
18
+ const number = Number(value); return { kind: 'float', value: Number.isFinite(number) ? number : null };
19
+ }
20
+ return typeof value === 'number' ? { kind: 'float', value: Number.isFinite(value) ? value : null } : { value };
21
+ }
22
+
23
+ export function readHermesYaml(file, text) {
24
+ const document = parseDocument(text, { version: '1.2', schema: 'core', intAsBigInt: true, uniqueKeys: true, logLevel: 'silent' });
25
+ if (document.errors.length) hermesRefuse(file, '', `YAML: ${document.errors[0].message}`);
26
+ const active = new Set();
27
+ const visit = (node, depth = 0) => {
28
+ if (node == null) return { value: null };
29
+ if (depth >= 128 || active.has(node)) hermesRefuse(file, '', 'YAML: recursive value');
30
+ if (isAlias(node)) {
31
+ const resolved = node.resolve(document);
32
+ if (!resolved) hermesRefuse(file, '', 'YAML: unresolved alias');
33
+ active.add(node);
34
+ try { return visit(resolved, depth + 1); } finally { active.delete(node); }
35
+ }
36
+ active.add(node);
37
+ try {
38
+ let entry;
39
+ if (isScalar(node)) entry = scalar(node.value);
40
+ else if (isSeq(node)) {
41
+ const value = [];
42
+ node.items.forEach((item, index) => set(value, String(index), visit(item, depth + 1)));
43
+ entry = { value };
44
+ } else if (isMap(node)) {
45
+ const value = Object.create(null), members = new Map();
46
+ for (const pair of node.items) {
47
+ const key = visit(pair.key, depth + 1);
48
+ let name;
49
+ if (typeof key.value === 'string' || typeof key.value === 'boolean') name = String(key.value);
50
+ else if (typeof key.value === 'number' && Number.isFinite(key.value)) {
51
+ const holder = {}; set(holder, 'key', key); name = stringifyNativeJsonMember(holder, 'key');
52
+ } else hermesRefuse(file, '', 'YAML: key must be a string');
53
+ members.set(name, visit(pair.value, depth + 1));
54
+ }
55
+ for (const [name, member] of members) set(value, name, member);
56
+ memberOrder.set(value, [...members.keys()]);
57
+ entry = { value };
58
+ } else hermesRefuse(file, '', 'YAML: unsupported value');
59
+ if (node.tag && !standard.has(node.tag)) {
60
+ const value = Object.create(null), tag = node.tag.startsWith('!') ? node.tag : `!${node.tag}`;
61
+ set(value, tag, entry); entry = { value };
62
+ }
63
+ return entry;
64
+ } finally { active.delete(node); }
65
+ };
66
+ const value = visit(document.contents).value;
67
+ return value == null ? Object.create(null) : value;
68
+ }
package/hermes-store.mjs CHANGED
@@ -72,7 +72,7 @@ export { lineage as hermesLineageKind, nouns as hermesSessionNouns };
72
72
  export async function readHermesStore(path, { metrics = {} } = {}) {
73
73
  return readStore(path, (db) => {
74
74
  statement(db, 'SELECT count(*) FROM sessions').get();
75
- const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
75
+ const tables = new Set(statement(db, "SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
76
76
  if (!['sessions', 'messages', 'schema_version'].every((table) => tables.has(table))) return { rows: [], mirrors: new Map() };
77
77
  const columns = tableColumns(db, 'sessions');
78
78
  const extendedSchema = extendedColumns.every((column) => columns.has(column));
package/homes.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { userInfo } from 'node:os';
2
- import { join } from 'node:path';
2
+ import { join as nativeDefaultJoin } from 'node:path';
3
3
 
4
4
  /** Native HarnessHomes defaults. An empty selected harness home is present;
5
5
  * only HOME itself uses the nonempty rule from interchange::user_home. */
6
- export function resolveHarnessHomes(env = process.env) {
6
+ export function resolveHarnessHomes(env = process.env, { joinPath: join = nativeDefaultJoin } = {}) {
7
7
  const accountHome = () => { try { return userInfo().homedir || '.'; } catch { return '.'; } };
8
8
  const home = env.HOME || (process.platform === 'win32' && env.USERPROFILE) || accountHome();
9
9
  const data = env.XDG_DATA_HOME ?? join(home, '.local', 'share');
package/index.d.ts CHANGED
@@ -23,7 +23,11 @@ export interface SessionDescriptor {
23
23
  recurrence?: { job_id: string; kind: string };
24
24
  cross_surface?: { state: string; platform?: string; error?: string };
25
25
  workspace?: { kind: 'repo' | 'channel' | 'none'; value?: string };
26
+ /** Read-only live display facts, never permission to answer a native request. */
27
+ pending_request?: Record<string, unknown> | null;
28
+ pending_request_unavailable?: SessionReadFailure;
26
29
  }
30
+ export interface SessionReadFailure { code: string | null; message: string; errors?: SessionReadFailure[] }
27
31
  export interface DiscoveryQuery {
28
32
  homes?: Partial<HarnessHomes>; harnesses?: HarnessId[];
29
33
  workspace?: string | null; workspace_subtree?: boolean; workspace_family?: string | null;
@@ -69,6 +73,13 @@ export function projectMetadataPage(query: DiscoveryQuery, descriptors: Iterable
69
73
  * of SDK clients or mutate a foreign harness's store. */
70
74
  export type IndexMetrics = Record<string, number>;
71
75
  export interface FileFingerprint { device: string; inode: string; size: string; mtime_ns: string }
76
+ export interface SessionFileReader {
77
+ path: string; size: number; fingerprint: FileFingerprint; modified_ms: number | null;
78
+ read(position: number, length: number, options?: { appended?: boolean }): Promise<Buffer>;
79
+ }
80
+ /** Checked nonblocking regular-file admission, fixed-size ranges and final stamps;
81
+ * native symlinks and append-only growth remain supported. Close is awaited. */
82
+ export function withSessionFile<T>(path: string, work: (reader: SessionFileReader) => T | Promise<T>, metrics?: IndexMetrics): Promise<{ result: T; fingerprint: FileFingerprint; size: number; modified_ms: number | null }>;
72
83
  export interface SourceStamp { path: string; missing?: boolean; fingerprint?: FileFingerprint }
73
84
  export interface FileReadCache {
74
85
  fingerprint?: FileFingerprint; size?: number; modified_ms?: number | null;
@@ -94,7 +105,8 @@ export function sameFingerprint(left?: FileFingerprint | null, right?: FileFinge
94
105
  export function verifySources(sources: SourceStamp[]): Promise<void>;
95
106
  export function storeFingerprints(path: string): Promise<SourceStamp[]>;
96
107
  export function readGeminiProjects(root: string, metrics?: IndexMetrics): Promise<{ source: SourceStamp; projects: Map<string, string> }>;
97
- export function readSourceJson(path: string, metrics?: IndexMetrics): Promise<SourceStamp & { result: unknown }>;
108
+ /** `previous` must be the in-process result of an earlier call; deserialized copies are reread. */
109
+ export function readSourceJson(path: string, metrics?: IndexMetrics, previous?: SourceStamp & { result: unknown }): Promise<SourceStamp & { result: unknown }>;
98
110
  export interface StoreRead<T> { sources: SourceStamp[]; result: T }
99
111
  export function readForeignStore(harness: HarnessId, path: string, options?: { previousMarks?: Map<string, string>; metrics?: IndexMetrics }): Promise<StoreRead<{ marks: Map<string, string>; rows: SessionDescriptor[]; removed: string[] }>>;
100
112
  export function readHermesStore(path: string, options?: { metrics?: IndexMetrics }): Promise<StoreRead<{ rows: SessionDescriptor[]; mirrors: NonNullable<MetadataProjectionOptions['hermesMirrors']> }>>;
@@ -125,6 +137,7 @@ export function filedUnderOwnKey(descriptor: SessionDescriptor): boolean;
125
137
  export class RetainedMetadataProjection {
126
138
  constructor(query: DiscoveryQuery, sources: string[], records: Iterable<IndexMetadataRecord>, metrics?: IndexMetrics);
127
139
  updatePart(source: string, part: string, records: Iterable<IndexMetadataRecord>): void;
140
+ keysFor(source: string, part: string): Set<string>;
128
141
  partsFor(key: string): Set<string>;
129
142
  page(query: DiscoveryQuery): DiscoveryPage;
130
143
  }
package/index.mjs CHANGED
@@ -7,7 +7,7 @@ export {
7
7
  } from './query.mjs';
8
8
  export { FILE_HARNESSES, STORE_HARNESSES, normalizedSourcePath, pathUnder, fileRoot, sessionPartFor, listSessionParts, readFileMetadata, metadataReaderVersion } from './source-layout.mjs';
9
9
  export { sourceStat, sourceEntries, selectStoreFiles, storeRegistryDirectories } from './store-registry.mjs';
10
- export { fileFingerprint, sameFingerprint } from './file-io.mjs';
10
+ export { fileFingerprint, sameFingerprint, withSessionFile } from './file-io.mjs';
11
11
  export { verifySources, readGeminiProjects, readSourceJson } from './source-files.mjs';
12
12
  export { storeFingerprints } from './sqlite-reader.mjs';
13
13
  export { readForeignStore } from './foreign-store.mjs';