Row 137 round 4, Darkwing's candidate (agents/darkwing/work/queue-137/r4/, build.patch 0cabe175, manifest 548ee477, 31 paths, base63c82f1c4). Filbert approved round 4 (comment 28809) and Dewey approved it (28816) after round 3 changes (28801, 28805). Darkwing's round 4 ran 9/9 mutant kills, bus 317, seat 150, land-work 86. Round 5 is a rebase only (agents/sage/work/queue-137-r5/, build.patch d6cbaaa8, manifest f73598fe). Row 150 changed docs/TOOLS.md, broker.mjs and cli/README.md under it. The round 4 patch applied unchanged onto435d1c616, and with index and @@ lines removed the two patches are byte-identical. Filbert checked the interdiff (comment 28818). A landing moves refs/heads/main in <dataRoot>/working/<business>/ <project>.git, never the project root or canonical refactor. The seed now fetches the root as refs/heads/root and fast-forwards main only when it is an ancestor, so a landed main survives the next push. Only a cross-role merge decision lands. Its squash names the real channel: "via the CLI" for the human, and "as its arbiter, via the broker" for an arbiter's decision.resolve. Every other class refuses not-authorized before the human check. This carries row 139's two items. REQ-LAND-1: a gated merge decision never lands. REQ-DEC-2: the host land op accepts an arbiter-resolved merge. land-work's read.mjs stays human-only, and land-work.sh still accepts only a human CLI resolution. Sage's landing gate ran on435d1c616plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 20:51:00Z to 20:55:34Z, network sage-r137_default removed. Cherry-picked onto5d98fb194, which added only a docs/SESSIONS.md line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
108 lines
4.3 KiB
JavaScript
108 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// land-work's read of the bus (row 116, #1610). The checks themselves are in
|
|
// packages/bus/src/landing.mjs, which the broker's agent landings share
|
|
// (row 137, #1631); every query there is a SELECT.
|
|
//
|
|
// node scripts/land-work/read.mjs <dataRoot> <business-file> <decision-id> <base>
|
|
//
|
|
// It opens <dataRoot>/bus/bus.sqlite with readOnly: true and checks the
|
|
// schema the way the Store does before reading anything. Output is one
|
|
// "key<TAB>value" line per fact land-work needs, or one
|
|
// "refuse<TAB>reason<TAB>detail" line and exit 2. A missing or unsafe
|
|
// database or business file exits 3.
|
|
//
|
|
// Schema version and digest follow the Store exports; a schema bump has one authority.
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { createHash } from 'node:crypto';
|
|
import { schemaDigest, SCHEMA_SHA256, SCHEMA_VERSION } from '../../packages/bus/src/store.mjs';
|
|
import { LandingRefusal, readLanding as shared } from '../../packages/bus/src/landing.mjs';
|
|
|
|
const refuse = (reason, detail) => {
|
|
throw new LandingRefusal(reason, detail);
|
|
};
|
|
|
|
export function openBus(dataRoot) {
|
|
const path = join(dataRoot, 'bus', 'bus.sqlite');
|
|
let s;
|
|
try {
|
|
s = lstatSync(path);
|
|
} catch {
|
|
throw Object.assign(new Error(`no bus database at ${path}`), { exit: 3 });
|
|
}
|
|
if (s.isSymbolicLink() || !s.isFile() || s.uid !== process.getuid())
|
|
throw Object.assign(new Error(`unsafe bus database ${path}`), { exit: 3 });
|
|
const db = new DatabaseSync(path, { readOnly: true, timeout: 5000 });
|
|
try {
|
|
checkSchema(db);
|
|
} catch (e) {
|
|
db.close();
|
|
throw e;
|
|
}
|
|
return db;
|
|
}
|
|
|
|
// The Store's own open check (store.mjs), against a reference built from the
|
|
// same schema.sql the import has already verified against SCHEMA_SHA256.
|
|
export function checkSchema(db) {
|
|
const ddl = readFileSync(new URL('../../packages/bus/schema.sql', import.meta.url), 'utf8');
|
|
if (createHash('sha256').update(ddl).digest('hex') !== SCHEMA_SHA256)
|
|
refuse('bus-schema', 'packages/bus/schema.sql does not match SCHEMA_SHA256');
|
|
const reference = new DatabaseSync(':memory:');
|
|
reference.exec(ddl);
|
|
const expected = schemaDigest(reference);
|
|
reference.close();
|
|
const meta = (key) => db.prepare('SELECT value FROM meta WHERE key = ?').get(key)?.value;
|
|
let digest;
|
|
try {
|
|
digest = schemaDigest(db);
|
|
} catch (e) {
|
|
refuse('bus-schema', `cannot read the schema: ${e.message}`);
|
|
}
|
|
if (digest !== expected) refuse('bus-schema', `schema digest ${digest.slice(0, 12)}, the store expects ${expected.slice(0, 12)}`);
|
|
if (meta('schema_version') !== SCHEMA_VERSION || meta('schema_digest') !== expected)
|
|
refuse('bus-schema', `meta says version ${meta('schema_version')}, digest ${String(meta('schema_digest')).slice(0, 12)}`);
|
|
}
|
|
|
|
// land-work lands only on refactor (#1610): the base it was given must be
|
|
// refactor, and the raise must target it.
|
|
export function readLanding(db, business, id, base) {
|
|
if (base !== 'refactor') refuse('target-branch', `the base is ${base}, land-work lands only on refactor`);
|
|
const q = { get: (sql, ...p) => db.prepare(sql).get(...p), all: (sql, ...p) => db.prepare(sql).all(...p) };
|
|
return shared(q, business, id, { into: 'refactor' });
|
|
}
|
|
|
|
function main([dataRoot, businessFile, id, base]) {
|
|
const out = (k, v) => process.stdout.write(`${k}\t${String(v ?? '').replace(/[\t\n\r]/g, ' ')}\n`);
|
|
let business;
|
|
try {
|
|
business = JSON.parse(readFileSync(businessFile, 'utf8'));
|
|
} catch (e) {
|
|
process.stderr.write(`land-work: cannot read the business file ${businessFile}: ${e.message}\n`);
|
|
return 3;
|
|
}
|
|
if (typeof business?.id !== 'string' || typeof business?.human !== 'string' || !business.human) {
|
|
process.stderr.write(`land-work: ${businessFile} names no business id and human\n`);
|
|
return 3;
|
|
}
|
|
let db;
|
|
try {
|
|
db = openBus(dataRoot);
|
|
for (const [k, v] of Object.entries(readLanding(db, business, id, base))) out(k, v);
|
|
return 0;
|
|
} catch (e) {
|
|
if (e instanceof LandingRefusal) {
|
|
out('refuse', `${e.reason}\t${e.message}`);
|
|
return 2;
|
|
}
|
|
process.stderr.write(`land-work: ${e.message}\n`);
|
|
return e.exit ?? 1;
|
|
} finally {
|
|
db?.close();
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = main(process.argv.slice(2));
|