From 5708fdf057febcfba5c7ebc38bead4cf0976be2b Mon Sep 17 00:00:00 2001 From: Eve Date: Wed, 22 Apr 2026 20:33:26 +0800 Subject: [PATCH] Add long-task governor wrapper MVP skeleton --- .../long_task_governor_wrapper_example.json | 17 +++ ...vernor_wrapper_invalid_silent_example.json | 17 +++ scripts/long_task_governor_wrapper.mjs | 123 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 docs/_artifacts/long_task_governor_wrapper_example.json create mode 100644 docs/_artifacts/long_task_governor_wrapper_invalid_silent_example.json create mode 100644 scripts/long_task_governor_wrapper.mjs diff --git a/docs/_artifacts/long_task_governor_wrapper_example.json b/docs/_artifacts/long_task_governor_wrapper_example.json new file mode 100644 index 0000000..9e203cb --- /dev/null +++ b/docs/_artifacts/long_task_governor_wrapper_example.json @@ -0,0 +1,17 @@ +{ + "requestText": "Research OpenClawGovernor and report back later.", + "hasFilesOrSystems": true, + "needsWaiting": true, + "needsSubagent": false, + "needsOwnerDecision": false, + "canReplyNow": false, + "taskName": "Research OpenClawGovernor concepts", + "currentStep": "Reading source material", + "nextStep": "Extract reusable governance ideas", + "nextReportCondition": "After the first source review is complete", + "waitingOn": "none", + "blocker": "none", + "checkpointTrigger": "after initial source review completes", + "externalizedTrigger": "cron reminder", + "triggerKind": "cron" +} diff --git a/docs/_artifacts/long_task_governor_wrapper_invalid_silent_example.json b/docs/_artifacts/long_task_governor_wrapper_invalid_silent_example.json new file mode 100644 index 0000000..52be488 --- /dev/null +++ b/docs/_artifacts/long_task_governor_wrapper_invalid_silent_example.json @@ -0,0 +1,17 @@ +{ + "requestText": "Investigate the Telegram closure issue and report back later.", + "hasFilesOrSystems": true, + "needsWaiting": true, + "needsSubagent": false, + "needsOwnerDecision": true, + "canReplyNow": false, + "taskName": "Investigate Telegram closure issue", + "currentStep": "Reviewing recent failures", + "nextStep": "Narrow the root cause", + "nextReportCondition": "After next meaningful milestone", + "waitingOn": "none", + "blocker": "none", + "checkpointTrigger": "", + "externalizedTrigger": "", + "triggerKind": "" +} diff --git a/scripts/long_task_governor_wrapper.mjs b/scripts/long_task_governor_wrapper.mjs new file mode 100644 index 0000000..f5a2b02 --- /dev/null +++ b/scripts/long_task_governor_wrapper.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +import fs from 'fs'; + +function parseArgs(argv) { + const args = { input: '', pretty: true }; + for (let i = 2; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--input') args.input = argv[++i] || ''; + else if (arg === '--compact') args.pretty = false; + } + return args; +} + +function readInput(path) { + if (!path || path === '-') { + return fs.readFileSync(0, 'utf8'); + } + return fs.readFileSync(path, 'utf8'); +} + +function normalizeRequest(raw) { + const data = JSON.parse(raw); + 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 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 validateSilentLaunch(input, classificationResult) { + if (!classificationResult.silentCandidate) { + return { ok: true, reason: 'not a silent long-task' }; + } + + if (!input.checkpointTrigger) { + return { ok: false, reason: 'missing first forced checkpoint trigger' }; + } + + if (!input.externalizedTrigger) { + return { ok: false, reason: 'missing externalized checkpoint path' }; + } + + return { + ok: true, + reason: `${input.triggerKind || 'externalized'} trigger is defined`, + }; +} + +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 = normalizeRequest(raw); + const classificationResult = classify(input); + const taskRecord = bootstrapTaskState(input, classificationResult); + 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, + silentLaunchOk: silentLaunch.ok, + silentLaunchReason: silentLaunch.reason, + handoff, + }; + + process.stdout.write(JSON.stringify(output, null, args.pretty ? 2 : 0) + '\n'); +} + +main();