fix continuity clean-room install verification
This commit is contained in:
261
scripts/long_task_governor_wrapper.mjs
Normal file
261
scripts/long_task_governor_wrapper.mjs
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function fail(code, message) {
|
||||
process.stderr.write(`${code}: ${message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { input: '', pretty: true };
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--input') {
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith('--')) fail('CLI_ERROR', '--input requires a value');
|
||||
args.input = value;
|
||||
i += 1;
|
||||
} else if (arg === '--compact') {
|
||||
args.pretty = false;
|
||||
} else {
|
||||
fail('CLI_ERROR', `unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function readInput(path) {
|
||||
if (!path || path === '-') return fs.readFileSync(0, 'utf8');
|
||||
return fs.readFileSync(path, 'utf8');
|
||||
}
|
||||
|
||||
function normalizeRequest(raw) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
fail('INVALID_JSON', 'input must be valid JSON');
|
||||
}
|
||||
|
||||
return {
|
||||
requestText: data.requestText || '',
|
||||
hasFilesOrSystems: Boolean(data.hasFilesOrSystems),
|
||||
needsWaiting: Boolean(data.needsWaiting),
|
||||
needsSubagent: Boolean(data.needsSubagent),
|
||||
needsOwnerDecision: Boolean(data.needsOwnerDecision),
|
||||
canReplyNow: Boolean(data.canReplyNow),
|
||||
taskName: data.taskName || 'Untitled long-task',
|
||||
currentStep: data.currentStep || 'Classifying request',
|
||||
nextStep: data.nextStep || 'Define next actionable step',
|
||||
nextReportCondition: data.nextReportCondition || 'After next meaningful milestone',
|
||||
waitingOn: data.waitingOn || 'none',
|
||||
blocker: data.blocker || 'none',
|
||||
checkpointTrigger: data.checkpointTrigger || '',
|
||||
externalizedTrigger: data.externalizedTrigger || '',
|
||||
triggerKind: data.triggerKind || '',
|
||||
};
|
||||
}
|
||||
|
||||
function inferFromRequestText(input) {
|
||||
const text = (input.requestText || '').toLowerCase();
|
||||
const inferred = { ...input };
|
||||
|
||||
if (!input.canReplyNow && /\b(can( not|'t)? use|check|inspect|investigate|review|verify|fix|debug|analyze|analyse|compare|deploy|run)\b/.test(text)) {
|
||||
inferred.hasFilesOrSystems = true;
|
||||
}
|
||||
if (!input.needsOwnerDecision && /\b(accept|reject|approve|decision|choose|pick|verdict)\b/.test(text)) {
|
||||
inferred.needsOwnerDecision = true;
|
||||
}
|
||||
if (!input.needsWaiting && /\b(wait|later|after|async|background|follow up|follow-up)\b/.test(text)) {
|
||||
inferred.needsWaiting = true;
|
||||
}
|
||||
if (!input.needsSubagent && /\bsubagent\b/.test(text)) {
|
||||
inferred.needsSubagent = true;
|
||||
}
|
||||
if (!input.checkpointTrigger && inferred.needsSubagent) {
|
||||
inferred.checkpointTrigger = 'when delegated work returns or the next checkpoint fires';
|
||||
}
|
||||
if (!input.externalizedTrigger && inferred.needsSubagent) {
|
||||
inferred.externalizedTrigger = 'wrapper-derived checkpoint artifact';
|
||||
}
|
||||
if (!input.triggerKind && inferred.needsSubagent) {
|
||||
inferred.triggerKind = 'artifact';
|
||||
}
|
||||
|
||||
return inferred;
|
||||
}
|
||||
|
||||
function classify(input) {
|
||||
const classification = input.canReplyNow && !input.hasFilesOrSystems && !input.needsWaiting && !input.needsSubagent && !input.needsOwnerDecision
|
||||
? 'general_chat'
|
||||
: 'long_task';
|
||||
|
||||
const silentCandidate = classification === 'long_task' && (input.needsWaiting || input.needsSubagent || Boolean(input.checkpointTrigger));
|
||||
const needsCheckpoint = classification === 'long_task';
|
||||
|
||||
return {
|
||||
classification,
|
||||
silentCandidate,
|
||||
needsOwnerDecision: input.needsOwnerDecision,
|
||||
needsCheckpoint,
|
||||
needsSubagent: input.needsSubagent,
|
||||
};
|
||||
}
|
||||
|
||||
function bootstrapTaskState(input, classificationResult) {
|
||||
if (classificationResult.classification !== 'long_task') return null;
|
||||
return {
|
||||
task_name: input.taskName,
|
||||
status: input.blocker !== 'none' ? 'blocked' : (input.waitingOn !== 'none' ? 'waiting_user' : 'active'),
|
||||
current_step: input.currentStep,
|
||||
next_step: input.nextStep,
|
||||
next_report_condition: input.nextReportCondition,
|
||||
waiting_on: input.waitingOn,
|
||||
blocker: input.blocker,
|
||||
silent: classificationResult.silentCandidate,
|
||||
};
|
||||
}
|
||||
|
||||
function toSlug(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 48);
|
||||
}
|
||||
|
||||
function ensureCheckpointArtifact(externalizedCheckpointPath, input, classificationResult) {
|
||||
if (classificationResult.classification !== 'long_task') return null;
|
||||
if (!classificationResult.silentCandidate) return null;
|
||||
if (!externalizedCheckpointPath) return null;
|
||||
|
||||
const artifactPath = path.resolve(process.cwd(), externalizedCheckpointPath);
|
||||
const artifact = {
|
||||
kind: 'long_task_checkpoint',
|
||||
triggerKind: input.triggerKind || 'artifact',
|
||||
checkpointTrigger: input.checkpointTrigger || '',
|
||||
currentStep: input.currentStep || '',
|
||||
nextStep: input.nextStep || '',
|
||||
waitingOn: input.waitingOn || '',
|
||||
blocker: input.blocker || '',
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
||||
fs.writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
||||
|
||||
const stats = fs.statSync(artifactPath);
|
||||
const readable = fs.readFileSync(artifactPath, 'utf8');
|
||||
|
||||
return {
|
||||
absolutePath: artifactPath,
|
||||
bytes: stats.size,
|
||||
readable: readable.trim().length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildExternalizedCheckpointPath(input, classificationResult) {
|
||||
if (classificationResult.classification !== 'long_task') return '';
|
||||
if (!classificationResult.silentCandidate) return '';
|
||||
if (!input.externalizedTrigger) return '';
|
||||
|
||||
const taskSeed = [input.currentStep, input.nextStep, input.waitingOn, input.blocker]
|
||||
.map((value) => toSlug(value))
|
||||
.filter(Boolean)
|
||||
.join('-');
|
||||
const stableSeed = taskSeed || 'long-task';
|
||||
|
||||
return `checkpoints/${stableSeed}.json`;
|
||||
}
|
||||
|
||||
function buildProgressEvidence(input, classificationResult, externalizedCheckpointPath, checkpointArtifact) {
|
||||
if (classificationResult.classification !== 'long_task') return null;
|
||||
if (!classificationResult.silentCandidate) return null;
|
||||
if (!externalizedCheckpointPath) return null;
|
||||
if (!checkpointArtifact || checkpointArtifact.readable !== true) return null;
|
||||
|
||||
return {
|
||||
sessionKey: toSlug([input.currentStep, input.waitingOn, input.nextStep].filter(Boolean).join('-')) || 'long-task-session',
|
||||
checkpointPath: externalizedCheckpointPath,
|
||||
verificationResult: `checkpoint artifact readable at ${externalizedCheckpointPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
function validateSilentLaunch(input, classificationResult) {
|
||||
if (!classificationResult.silentCandidate) {
|
||||
return {
|
||||
ok: true,
|
||||
reason: 'not a silent long-task',
|
||||
recommendedFallback: 'none',
|
||||
requiredNextAction: 'proceed_with_normal_long_task_flow',
|
||||
};
|
||||
}
|
||||
|
||||
if (!input.checkpointTrigger) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'missing first forced checkpoint trigger',
|
||||
recommendedFallback: 'non_silent_follow_up',
|
||||
requiredNextAction: 'define_first_checkpoint_trigger_before_silent_launch',
|
||||
};
|
||||
}
|
||||
|
||||
if (!input.externalizedTrigger) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'missing externalized checkpoint path',
|
||||
recommendedFallback: 'non_silent_follow_up',
|
||||
requiredNextAction: 'bind_externalized_checkpoint_path_or_abort_silent_launch',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
reason: `${input.triggerKind || 'externalized'} trigger is defined`,
|
||||
recommendedFallback: 'none',
|
||||
requiredNextAction: 'proceed_with_silent_launch',
|
||||
};
|
||||
}
|
||||
|
||||
function planHandoff(classificationResult) {
|
||||
if (classificationResult.needsOwnerDecision) return { mode: 'button_path' };
|
||||
return { mode: 'direct_reply' };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
const raw = readInput(args.input);
|
||||
const input = inferFromRequestText(normalizeRequest(raw));
|
||||
const classificationResult = classify(input);
|
||||
const taskRecord = bootstrapTaskState(input, classificationResult);
|
||||
const externalizedCheckpointPath = buildExternalizedCheckpointPath(input, classificationResult);
|
||||
const checkpointArtifact = ensureCheckpointArtifact(externalizedCheckpointPath, input, classificationResult);
|
||||
const progressEvidence = buildProgressEvidence(input, classificationResult, externalizedCheckpointPath, checkpointArtifact);
|
||||
const silentLaunch = validateSilentLaunch(input, classificationResult);
|
||||
const handoff = planHandoff(classificationResult);
|
||||
|
||||
const output = {
|
||||
classification: classificationResult.classification,
|
||||
silentCandidate: classificationResult.silentCandidate,
|
||||
needsOwnerDecision: classificationResult.needsOwnerDecision,
|
||||
needsCheckpoint: classificationResult.needsCheckpoint,
|
||||
needsSubagent: classificationResult.needsSubagent,
|
||||
taskRecord,
|
||||
progressEvidence,
|
||||
externalizedCheckpointPath,
|
||||
checkpointArtifact,
|
||||
silentLaunchOk: silentLaunch.ok,
|
||||
silentLaunchReason: silentLaunch.reason,
|
||||
recommendedFallback: silentLaunch.recommendedFallback,
|
||||
requiredNextAction: silentLaunch.requiredNextAction,
|
||||
handoff,
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(output, null, args.pretty ? 2 : 0) + '\n');
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
fail('CLI_ERROR', error && error.message ? error.message : 'unexpected error');
|
||||
}
|
||||
Reference in New Issue
Block a user