import * as fs from 'node:fs';
import * as path from 'node:path';
import { createHash } from 'node:crypto';
export interface PlanFloorTargetDelivery {
status: 'missing' | 'ready' | 'error';
reason?: string;
sessionId: string;
command: string;
targetPath: string;
targetSha256: string;
acknowledgedAt?: string;
}
/** The first owned native user command must name the already-seeded plan. */
export function readPlanFloorTarget(configDir: string | null, cwd: string, opts: {
seed: string; sessionId: string; slashCommand: string; startedAt: number; now: number;
}): PlanFloorTargetDelivery {
const delivery: PlanFloorTargetDelivery = {
status: 'missing', sessionId: opts.sessionId, command: `${opts.slashCommand} PLAN.md`,
targetPath: path.join(cwd, 'PLAN.md'), targetSha256: createHash('sha256').update(opts.seed).digest('hex'),
};
try {
if (!configDir || !/^[\da-f-]{36}$/i.test(opts.sessionId) || !/^\/[\w-]+$/.test(opts.slashCommand) ||
!Number.isFinite(opts.startedAt) || !Number.isFinite(opts.now) || opts.now < opts.startedAt) return delivery;
if (!fs.lstatSync(delivery.targetPath).isFile() || fs.readFileSync(delivery.targetPath, 'utf8') !== opts.seed) {
return { ...delivery, status: 'error', reason: 'Owned seed plan is missing or changed' };
}
const projects = path.join(configDir, 'projects');
if (!fs.existsSync(projects)) return delivery;
if (!fs.lstatSync(projects).isDirectory()) throw Error('Native project root is not a directory');
const dirs = fs.readdirSync(projects, { withFileTypes: true }).filter(dir => dir.isDirectory());
if (dirs.length > 128) throw Error('Too many native project directories');
const files = dirs.map(dir => path.join(projects, dir.name, `${opts.sessionId}.jsonl`)).filter(file => fs.existsSync(file));
if (files.length === 0) return delivery;
if (files.length !== 1 || !fs.lstatSync(files[0]!).isFile()) throw Error('Ambiguous or nonregular native session');
if (fs.statSync(files[0]!).size > 32 * 1024 * 1024) throw Error('Native session exceeds 32 MiB');
const raw = fs.readFileSync(files[0]!, 'utf8');
const expected = `${opts.slashCommand.slice(1)}\n` +
`${opts.slashCommand}\nPLAN.md`;
for (const line of raw.slice(0, raw.lastIndexOf('\n') + 1).split('\n')) {
if (!line.trim()) continue;
const record = JSON.parse(line), at = Date.parse(record.timestamp ?? '');
if (record.type !== 'user' || record.isSidechain !== false || record.cwd !== cwd ||
record.sessionId !== opts.sessionId || record.parent_tool_use_id || record.message?.role !== 'user' ||
!Number.isFinite(at) || at < opts.startedAt || at > opts.now) continue;
const content = record.message.content;
const text = typeof content === 'string' ? content : Array.isArray(content) && content.length === 1 &&
content[0]?.type === 'text' && typeof content[0].text === 'string' ? content[0].text : undefined;
if (text === undefined) {
if (Array.isArray(content) && content.some(block => block?.type === 'text')) {
return { ...delivery, status: 'error', reason: 'First owned native user text was not one target command' };
}
continue; // Tool-result-only user envelopes are not submitted commands.
}
// A later queued target cannot repair a skill that already started bare.
return text === expected
? { ...delivery, status: 'ready', acknowledgedAt: record.timestamp }
: { ...delivery, status: 'error', reason: 'First owned native user input did not name the seeded plan' };
}
return delivery;
} catch (error) {
return { ...delivery, status: 'error', reason: error instanceof Error ? error.message : String(error) };
}
}