Skip to content

Commit 267b228

Browse files
NagyViktNagyViktOmX
authored
Make OMX summaries acceptable to Colony (#12)
Colony rejects the runtime summary file unless the payload declares the colony-runtime-summary-v1 schema, so the writer now emits the required schema fields and validates the payload before the atomic rename. Constraint: Current Colony health parser requires schema=colony-runtime-summary-v1 and uses timestamp for freshness. Rejected: Patch Colony to accept the older OMX version-only shape | malformed summaries should be fixed at the producer. Confidence: high Scope-risk: narrow Directive: Keep last_seen_at and timestamp aligned until Colony drops the timestamp freshness input. Tested: npm run build; node --test dist/runtime/__tests__/colony-runtime-summary.test.js; npm run lint; pnpm --filter @colony/core test -- test/omx-runtime-summary.test.ts; pnpm --filter @imdeadpool/colony-cli test -- test/health.test.ts -t 'shows OMX runtime bridge available when a fresh v1 summary exists' Not-tested: Full oh-my-codex npm test suite Co-authored-by: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent c1946cf commit 267b228

3 files changed

Lines changed: 126 additions & 2 deletions

File tree

src/runtime/__tests__/colony-runtime-summary.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ import { join } from 'node:path';
55
import { describe, it } from 'node:test';
66

77
import {
8+
COLONY_RUNTIME_SUMMARY_SCHEMA,
9+
type ColonyRuntimeSummaryV1,
810
colonyRuntimeSummaryPath,
911
extractRecentEditPathsFromHookPayload,
1012
extractRecentToolsFromHookPayload,
13+
validateColonyRuntimeSummary,
1114
writeColonyRuntimeSummary,
1215
} from '../colony-runtime-summary.js';
16+
import { colonyRuntimeSummaryV1Fixture } from './fixtures/colony-runtime-summary-v1.js';
1317
import { writeSessionEnd, writeSessionStart } from '../../hooks/session.js';
1418

1519
async function withTempRepo(fn: (cwd: string) => Promise<void>): Promise<void> {
@@ -37,20 +41,76 @@ describe('colony runtime summary', () => {
3741
await readFile(colonyRuntimeSummaryPath(cwd), 'utf-8'),
3842
) as typeof summary;
3943

44+
assert.equal(persisted.schema, COLONY_RUNTIME_SUMMARY_SCHEMA);
4045
assert.equal(persisted.version, 1);
4146
assert.equal(persisted.runtime, 'omx');
4247
assert.equal(persisted.session_id, 'sess-active');
4348
assert.equal(persisted.agent, 'codex');
4449
assert.equal(persisted.repo_root, cwd);
50+
assert.equal(persisted.cwd, cwd);
4551
assert.equal(persisted.worktree_path, cwd);
4652
assert.equal(persisted.active, true);
4753
assert.equal(persisted.last_seen_at, '2026-05-01T10:00:00.000Z');
54+
assert.equal(persisted.timestamp, '2026-05-01T10:00:00.000Z');
4855
assert.deepEqual(persisted.recent_tools, ['Bash']);
4956
assert.deepEqual(persisted.recent_edit_paths, ['src/runtime/run-state.ts']);
5057
assert.deepEqual(persisted.warnings, []);
5158
});
5259
});
5360

61+
it('matches the Colony-compatible runtime-summary v1 fixture shape', async () => {
62+
validateColonyRuntimeSummary(colonyRuntimeSummaryV1Fixture);
63+
64+
await withTempRepo(async (cwd) => {
65+
await writeColonyRuntimeSummary(cwd, {
66+
sessionId: colonyRuntimeSummaryV1Fixture.session_id,
67+
agent: colonyRuntimeSummaryV1Fixture.agent,
68+
active: colonyRuntimeSummaryV1Fixture.active,
69+
lastSeenAt: colonyRuntimeSummaryV1Fixture.last_seen_at,
70+
recentTools: colonyRuntimeSummaryV1Fixture.recent_tools,
71+
recentEditPaths: colonyRuntimeSummaryV1Fixture.recent_edit_paths,
72+
warnings: colonyRuntimeSummaryV1Fixture.warnings,
73+
});
74+
75+
const persisted = JSON.parse(
76+
await readFile(colonyRuntimeSummaryPath(cwd), 'utf-8'),
77+
) as Record<string, unknown>;
78+
79+
for (const key of Object.keys(colonyRuntimeSummaryV1Fixture)) {
80+
assert.equal(Object.prototype.hasOwnProperty.call(persisted, key), true, key);
81+
}
82+
assert.equal(persisted.schema, colonyRuntimeSummaryV1Fixture.schema);
83+
assert.equal(persisted.runtime, colonyRuntimeSummaryV1Fixture.runtime);
84+
assert.equal(persisted.timestamp, persisted.last_seen_at);
85+
assert.deepEqual(persisted.recent_tools, colonyRuntimeSummaryV1Fixture.recent_tools);
86+
assert.deepEqual(persisted.recent_edit_paths, colonyRuntimeSummaryV1Fixture.recent_edit_paths);
87+
assert.deepEqual(persisted.warnings, colonyRuntimeSummaryV1Fixture.warnings);
88+
});
89+
});
90+
91+
it('rejects malformed summaries before writing', () => {
92+
assert.throws(
93+
() => validateColonyRuntimeSummary({
94+
schema: 'wrong-schema',
95+
version: 1,
96+
runtime: 'omx',
97+
session_id: 'sess-invalid',
98+
agent: 'codex',
99+
repo_root: '/repo',
100+
cwd: '/repo',
101+
branch: 'agent/codex/test',
102+
worktree_path: '/repo',
103+
active: true,
104+
last_seen_at: '2026-05-01T10:00:00.000Z',
105+
timestamp: '2026-05-01T10:00:00.000Z',
106+
recent_tools: [],
107+
recent_edit_paths: [],
108+
warnings: [],
109+
} as unknown as ColonyRuntimeSummaryV1),
110+
/schema must be colony-runtime-summary-v1/,
111+
);
112+
});
113+
54114
it('emits fresh summaries from OMX session lifecycle state', async () => {
55115
await withTempRepo(async (cwd) => {
56116
await writeSessionStart(cwd, 'sess-lifecycle', {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type ColonyRuntimeSummaryV1 } from '../../colony-runtime-summary.js';
2+
3+
export const colonyRuntimeSummaryV1Fixture: ColonyRuntimeSummaryV1 = {
4+
schema: 'colony-runtime-summary-v1',
5+
version: 1,
6+
runtime: 'omx',
7+
session_id: 'codex@fixture',
8+
agent: 'codex',
9+
repo_root: '/repo',
10+
cwd: '/repo',
11+
branch: 'agent/codex/fixture',
12+
worktree_path: '/repo',
13+
active: true,
14+
last_seen_at: '2026-05-01T10:00:00.000Z',
15+
timestamp: '2026-05-01T10:00:00.000Z',
16+
recent_tools: ['Bash'],
17+
recent_edit_paths: ['src/runtime/run-state.ts'],
18+
warnings: [],
19+
};

src/runtime/colony-runtime-summary.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
44
import { dirname, join, resolve } from 'node:path';
55

66
const SUMMARY_FILENAME = 'colony-runtime-summary.json';
7+
export const COLONY_RUNTIME_SUMMARY_SCHEMA = 'colony-runtime-summary-v1';
78
const SUMMARY_VERSION = 1;
89
const MAX_RECENT_ITEMS = 12;
910

1011
export interface ColonyRuntimeSummaryV1 {
12+
schema: typeof COLONY_RUNTIME_SUMMARY_SCHEMA;
1113
version: 1;
1214
runtime: 'omx';
1315
session_id: string;
1416
agent: string;
1517
repo_root: string;
18+
cwd: string;
1619
branch: string;
1720
worktree_path: string;
1821
active: boolean;
1922
last_seen_at: string;
23+
timestamp: string;
2024
recent_tools: string[];
2125
recent_edit_paths: string[];
2226
warnings: string[];
@@ -38,7 +42,7 @@ interface ReadJsonResult {
3842
}
3943

4044
function stateDir(cwd: string): string {
41-
return join(resolve(cwd), '.omx', 'state');
45+
return join(detectWorktreePath(cwd), '.omx', 'state');
4246
}
4347

4448
export function colonyRuntimeSummaryPath(cwd: string): string {
@@ -137,11 +141,48 @@ function buildWarnings(
137141
return warnings;
138142
}
139143

144+
function isIsoTimestamp(value: string): boolean {
145+
return Number.isFinite(Date.parse(value));
146+
}
147+
148+
function validateStringArray(value: unknown): boolean {
149+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
150+
}
151+
152+
export function validateColonyRuntimeSummary(summary: ColonyRuntimeSummaryV1): void {
153+
const errors: string[] = [];
154+
if (summary.schema !== COLONY_RUNTIME_SUMMARY_SCHEMA) {
155+
errors.push(`schema must be ${COLONY_RUNTIME_SUMMARY_SCHEMA}`);
156+
}
157+
if (summary.runtime !== 'omx') errors.push('runtime must be omx');
158+
if (summary.version !== SUMMARY_VERSION) errors.push(`version must be ${SUMMARY_VERSION}`);
159+
for (const key of ['session_id', 'agent', 'repo_root', 'cwd', 'worktree_path'] as const) {
160+
if (!safeString(summary[key])) errors.push(`${key} must be a non-empty string`);
161+
}
162+
if (typeof summary.branch !== 'string') errors.push('branch must be a string');
163+
if (typeof summary.active !== 'boolean') errors.push('active must be a boolean');
164+
if (!safeString(summary.last_seen_at) || !isIsoTimestamp(summary.last_seen_at)) {
165+
errors.push('last_seen_at must be an ISO timestamp');
166+
}
167+
if (summary.timestamp !== summary.last_seen_at) {
168+
errors.push('timestamp must match last_seen_at');
169+
}
170+
if (!validateStringArray(summary.recent_tools)) errors.push('recent_tools must be a string array');
171+
if (!validateStringArray(summary.recent_edit_paths)) {
172+
errors.push('recent_edit_paths must be a string array');
173+
}
174+
if (!validateStringArray(summary.warnings)) errors.push('warnings must be a string array');
175+
if (errors.length > 0) {
176+
throw new Error(`Invalid colony runtime summary: ${errors.join('; ')}`);
177+
}
178+
}
179+
140180
export async function writeColonyRuntimeSummary(
141181
cwd: string,
142182
options: WriteColonyRuntimeSummaryOptions = {},
143183
): Promise<ColonyRuntimeSummaryV1> {
144-
const repoRoot = resolve(cwd);
184+
const resolvedCwd = resolve(cwd);
185+
const repoRoot = detectWorktreePath(resolvedCwd);
145186
const summaryPath = colonyRuntimeSummaryPath(repoRoot);
146187
const [sessionRead, existingRead] = await Promise.all([
147188
readJsonIfExists(sessionPath(repoRoot)),
@@ -160,20 +201,24 @@ export async function writeColonyRuntimeSummary(
160201
: sessionRead.data !== null;
161202

162203
const summary: ColonyRuntimeSummaryV1 = {
204+
schema: COLONY_RUNTIME_SUMMARY_SCHEMA,
163205
version: SUMMARY_VERSION,
164206
runtime: 'omx',
165207
session_id: sessionId,
166208
agent: safeString(options.agent) || safeString(existingRead.data?.agent) || defaultAgent(),
167209
repo_root: repoRoot,
210+
cwd: resolvedCwd,
168211
branch: detectBranch(repoRoot),
169212
worktree_path: detectWorktreePath(repoRoot),
170213
active,
171214
last_seen_at: lastSeenAt,
215+
timestamp: lastSeenAt,
172216
recent_tools: mergeRecent(options.recentTools, existingRead.data?.recent_tools),
173217
recent_edit_paths: mergeRecent(options.recentEditPaths, existingRead.data?.recent_edit_paths),
174218
warnings: buildWarnings(options, sessionRead.malformed, existingRead.malformed),
175219
};
176220

221+
validateColonyRuntimeSummary(summary);
177222
await writeAtomicFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`);
178223
return summary;
179224
}

0 commit comments

Comments
 (0)