Files
pbakaus_impeccable/skill/scripts/picker-doc-poll.mjs
T
2026-09-01 10:02:40 +05:00

100 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/** Agent poll CLI for the design-document edit session.
*
* The picker forks picker-doc-session.mjs on submit; this is how the agent
* hears from it, on the live-poll.mjs contract: one-shot by default, block
* until one event arrives, print it as JSON on stdout, exit.
*
* node picker-doc-poll.mjs # block, print one event
* node picker-doc-poll.mjs --timeout=600000 # total budget in ms
* node picker-doc-poll.mjs --reply <id> <status> [message]
*
* Events printed: {"type":"edit_request","id","kind","prompt","category",
* "payload"} for work, {"type":"timeout"} when the budget runs out (poll
* again), {"type":"exit"} when the session ended (stop polling).
*
* Reply statuses: done (change applied; message shown to the user in the
* document), error (could not apply; message explains), retry (release the
* request back to pending).
*
* Session discovery: .impeccable/design-interview/doc-session.json, written
* by the session process and removed when it exits; a missing file prints
* {"type":"exit"} so a finished session never hangs the loop.
*/
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const sessionPath = path.resolve(process.cwd(), '.impeccable/design-interview/doc-session.json');
/* Sliced under undici's 300s header timeout, same as live-poll. */
const PER_REQUEST_MS = 270_000;
const DEFAULT_TOTAL_MS = 600_000;
async function session() {
try {
return JSON.parse(await readFile(sessionPath, 'utf8'));
} catch {
return null;
}
}
const args = process.argv.slice(2);
function readFlag(name, fallback) {
const exact = args.find((arg) => arg.startsWith(`${name}=`));
if (exact) return exact.slice(name.length + 1);
const at = args.indexOf(name);
if (at !== -1 && args[at + 1]) return args[at + 1];
return fallback;
}
const info = await session();
if (!info) {
console.log(JSON.stringify({ type: 'exit', reason: 'no-session' }));
process.exit(0);
}
const base = `http://127.0.0.1:${info.port}`;
if (args.includes('--reply')) {
const at = args.indexOf('--reply');
const [id, status, ...rest] = args.slice(at + 1).filter((arg) => !arg.startsWith('--'));
if (!id || !status) {
console.error('usage: picker-doc-poll.mjs --reply <id> <done|error|retry> [message]');
process.exit(1);
}
const response = await fetch(`${base}/doc/reply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: info.token, id, status, message: rest.join(' ') }),
}).catch(() => null);
if (!response?.ok) {
console.error(`Reply failed: ${response ? response.status : 'session unreachable'}`);
process.exit(1);
}
console.log(JSON.stringify(await response.json()));
process.exit(0);
}
const totalBudget = Number(readFlag('--timeout', DEFAULT_TOTAL_MS));
const deadline = Date.now() + (Number.isFinite(totalBudget) && totalBudget > 0 ? totalBudget : DEFAULT_TOTAL_MS);
for (;;) {
const slice = Math.min(deadline - Date.now(), PER_REQUEST_MS);
if (slice <= 0) {
console.log(JSON.stringify({ type: 'timeout' }));
process.exit(0);
}
let payload;
try {
const response = await fetch(`${base}/doc/poll?token=${encodeURIComponent(info.token)}&timeout=${slice}`);
payload = await response.json();
} catch {
/* The session process exited between polls. */
console.log(JSON.stringify({ type: 'exit', reason: 'session-gone' }));
process.exit(0);
}
if (payload.type === 'timeout') continue;
console.log(JSON.stringify(payload));
process.exit(0);
}