@rockysoft/opencode-hybrid-workflow 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/assets/rules.md +18 -14
- package/core/controller.mjs +512 -506
- package/core/planning-escalation.mjs +45 -0
- package/core/planning-research.mjs +2 -1
- package/core/plugin.mjs +104 -104
- package/core/workers.mjs +205 -202
- package/docs/CHANGELOG.md +7 -0
- package/docs/planning-recovery.md +14 -14
- package/lib/continuation.mjs +3 -1
- package/lib/native-result.mjs +2 -1
- package/lib/planning-continuation.mjs +40 -0
- package/package.json +76 -76
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {hash,snapshot,sensitiveEvidence} from './snapshot.mjs';
|
|
2
|
+
import {findingChecklist} from './findings.mjs';
|
|
3
|
+
import {lead} from '../lib/reviewers.mjs';
|
|
4
|
+
|
|
5
|
+
export const recoverySchema={type:['object','null'],additionalProperties:false,properties:{
|
|
6
|
+
kind:{type:'string',enum:['research','resubmit','developer','tooling']},
|
|
7
|
+
instructions:{type:'string'},
|
|
8
|
+
},required:['kind','instructions']};
|
|
9
|
+
export const ESCALATION_POLICY=`Planning corrections belong to the configured premium lead. The implementation agent may collect public facts and relay observations, but must not invent security/architecture contracts, dismiss findings, or decide a closed planning gate is broken. If a requested correction is unclear, disputed, apparently impossible, or blocked by a capability, call hybrid_checkpoint escalate with the concrete observations and evidence. Do not reset, disable the plugin, start a replacement task or ask the developer to bypass findings. Follow planningNext; research directives authorize evidence gathering only. Return collected evidence to plan for premium evaluation. A premium reviewer can explicitly withdraw its own mistaken finding through verifiedResolutions with a concrete argument; a bare pass or another reviewer's opinion cannot silently erase dissent. Specific developer approvals remain required for stack changes and other reserved decisions.`;
|
|
10
|
+
export const RECOVERY_PROMPT=`You are the premium planning lead, evaluating a stalled handoff. Read the current findings, latest observations, plan and supplied evidence. Decide whether each requested correction is real, already supported, mistaken, or needs evidence. Do not leave architectural/security decisions to the implementation model. Retain normal verdict consistency: use blocked when medium/high/critical findings remain, revise for other open corrections, and pass only when no material finding remains. A blocked research directive is actionable work, not a request for user intervention. Return an advisory report and a non-null recovery object: research = exact public facts/questions/excerpts to collect with no invented architectural decision; resubmit = a concrete corrected contract or reasoned rebuttal ready for normal plan validation; developer = a specific question/options/consequences requiring actual developer authority; tooling = the exact missing capability/authentication and repair needed. Resolve what available evidence allows before requesting more. Do not demand proof requiring unbuilt code before scaffolding. No unrestricted web search exists in the native reviewer transport; use captured evidence and request targeted collection where needed. If a finding is mistaken, explain why and tell the originating reviewer to explicitly verify its withdrawal. This advisory call cannot approve the plan, close findings, authorize a stack deviation or override another reviewer's dissent. Do not suggest resetting/disabling the workflow. Include recovery only for this advisory stage; regular reports return recovery:null.`;
|
|
11
|
+
export const researchDigest=state=>hash(JSON.stringify((state.planningResearch??[]).map(r=>[r.kind,r.body])));
|
|
12
|
+
export function escalationKey(state,tree,checks=''){
|
|
13
|
+
return hash(JSON.stringify([state.id,state.brief,state.candidate?.hash,state.planningClarification?.text,tree.fingerprint,
|
|
14
|
+
findingChecklist(state,'plan'),researchDigest(state),checks.trim(),RECOVERY_PROMPT]));
|
|
15
|
+
}
|
|
16
|
+
export function planningNext(state){
|
|
17
|
+
if(!state||!['plan','planning'].includes(state.phase))return null;
|
|
18
|
+
const escalated=state.planningEscalation;
|
|
19
|
+
if(escalated&&!state.planningEscalationConsumed){
|
|
20
|
+
if(['research','resubmit'].includes(escalated.recovery.kind)&&escalated.researchDigest!==researchDigest(state))return {kind:'resubmit',action:'plan',instructions:'New evidence is attached. Submit it to the premium planners; do not evaluate or waive findings yourself.',key:hash(escalated.key+researchDigest(state))};
|
|
21
|
+
return {...escalated.recovery,action:escalated.recovery.kind==='resubmit'?'plan':escalated.recovery.kind==='research'?'research':null,key:escalated.key,reportCall:escalated.call};
|
|
22
|
+
}
|
|
23
|
+
if(state.evidenceRequest||state.pauseReason)return {kind:'escalate',action:'escalate',instructions:'Send the current findings and concrete observations to the premium planning lead. This is recoverable planning work, not permission to reset the gate.',key:hash(JSON.stringify([state.id,state.evidenceRequest,state.pauseReason,findingChecklist(state,'plan')]))};
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export async function escalatePlanning(controller,state,input,signal){
|
|
27
|
+
if(!['plan','planning'].includes(state.phase))throw Error('Escalation is available only for pending planning.');
|
|
28
|
+
const checks=input.checks??'';
|
|
29
|
+
if(typeof checks!=='string')throw Error('Escalation checks must be text.');
|
|
30
|
+
if(sensitiveEvidence('planning-escalation.txt',Buffer.from(checks)))throw Error('Escalation contains potential credentials; supply redacted evidence.');
|
|
31
|
+
const tree=snapshot(controller.directory),key=escalationKey(state,tree,checks);
|
|
32
|
+
if(state.planningEscalation?.key===key)return {...controller.status(),escalationUnchanged:true,message:'The premium lead already evaluated this exact evidence. Read its saved directive; collect new evidence or identify the specific missing capability. Do not repeat this unchanged escalation or reset the workflow.'};
|
|
33
|
+
controller.checkpoint(state,'plan');
|
|
34
|
+
const report=await controller.ask(state,lead(controller.config),'plan-recovery',{
|
|
35
|
+
brief:state.brief,candidate:state.candidate?.text??null,clarification:state.planningClarification??null,
|
|
36
|
+
trackedFindings:findingChecklist(state,'plan'),priorFindings:state.planFeedback??[],researchEvidence:state.planningResearch??[],
|
|
37
|
+
observations:{text:checks,source:'Implementation agent claims, not approval or independent verification'},recoveryContract:RECOVERY_PROMPT,
|
|
38
|
+
},signal,[],tree);
|
|
39
|
+
if(!report.recovery)throw Error('Planning lead omitted its recovery directive; no approval or finding changed. Retry escalation with the report-schema diagnostic.');
|
|
40
|
+
if(signal?.aborted)throw Error('Planning escalation cancelled.');
|
|
41
|
+
if(snapshot(controller.directory).fingerprint!==tree.fingerprint)throw Error('Project changed during escalation; recollect evidence before retrying.');
|
|
42
|
+
state.planningEscalation={key,call:state.reports.at(-1).call,who:lead(controller.config),researchDigest:researchDigest(state),recovery:report.recovery,report:report.report};
|
|
43
|
+
state.planningEscalationConsumed=false;
|
|
44
|
+
controller.save(state);return controller.status();
|
|
45
|
+
}
|
|
@@ -3,8 +3,9 @@ import {hash,sensitiveEvidence} from './snapshot.mjs';
|
|
|
3
3
|
import {runtimePath,writeJSONAtomic} from './state-store.mjs';
|
|
4
4
|
import {findingChecklist} from './findings.mjs';
|
|
5
5
|
import {STACK_POLICY} from './stack-policy.mjs';
|
|
6
|
+
import {ESCALATION_POLICY} from './planning-escalation.mjs';
|
|
6
7
|
|
|
7
|
-
export const PLANNING_POLICY = `${STACK_POLICY}\n\nPlanning is a document checkpoint, not a preimplementation runtime test.
|
|
8
|
+
export const PLANNING_POLICY = `${STACK_POLICY}\n\n${ESCALATION_POLICY}\n\nPlanning is a document checkpoint, not a preimplementation runtime test.
|
|
8
9
|
For each material planning finding, identify the concrete decision that is unsafe now. Distinguish evidence needed to choose the architecture from execution checks that require the approved implementation. Do not demand an installed dependency tree, lockfile, deployed service, working integration or runtime test before permitting the work needed to produce it. Select compatible initial dependencies using registry/docs evidence; verify installation during the first implementation slice. Later integrations need explicit prerequisites and acceptance checks before activation, not premature proof that unbuilt code works.
|
|
9
10
|
A deferred check is not a waiver: the plan must name its owner, the dependent operation that must remain disabled, the evidence required to enable it and the failure/replan path. Do not defer an unresolved fundamental feasibility/security issue when dependent implementation would already be unsafe or require guessing material architecture. Reviewers must verify these boundaries on subsequent implementation reviews before the dependent operation is accepted.
|
|
10
11
|
For an existing finding whose requirement was incorrectly placed at planning, the originating reviewer must explicitly verify the corrected phase boundary and return verifiedResolutions with that argument. Never silently omit or downgrade a finding. One pass and no unresolved medium/high/critical findings still govern the current checkpoint.
|
package/core/plugin.mjs
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import {isReviewerSession} from '../lib/opencode-reviewer.mjs';
|
|
2
|
-
import {roleText} from '../lib/reviewers.mjs';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { WORKFLOW_POLICY } from './workflow-policy.mjs';
|
|
7
|
-
import { Controller } from './controller.mjs';
|
|
8
|
-
import { readDashboard } from './monitor-data.mjs';
|
|
9
|
-
import { DESIGN_POLICY, recordDesignRequest } from './design.mjs';
|
|
10
|
-
import { researchTool } from './research-tools.mjs';
|
|
11
|
-
import {registerImpeccableCommand,IMPECCABLE_POLICY} from '../lib/impeccable.mjs';
|
|
12
|
-
import {registerContinuation,CONTINUATION_POLICY} from '../lib/continuation.mjs';
|
|
13
|
-
import {PLANNING_POLICY} from './planning-research.mjs';
|
|
14
|
-
|
|
15
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
// V2's Plugin.define is an identity function. This plain plugin needs no installed dependencies.
|
|
17
|
-
import { FRONTEND_COMMON, FRONTEND_BUILD } from './frontend-policy.mjs';
|
|
18
|
-
export const readTools = /^(read|glob|grep|list|ls|skill|webfetch|websearch|todowrite|todoread|question|subagent|hybrid_checkpoint|hybrid_activity|context7_.+|serena_(find_symbol|find_referencing_symbols|get_symbols_overview|search_for_pattern|read_file|list_dir|find_file|read_memory|list_memories|initial_instructions|check_onboarding_performed)|playwright_browser_(snapshot|take_screenshot|console_messages|network_requests))$/;
|
|
19
|
-
export function createPlugin(config, storage = config.runtimeDirectory ?? path.join(here, 'runtime')) { return {
|
|
20
|
-
id: 'Hybrid-Workflow',
|
|
21
|
-
async setup(ctx) {
|
|
22
|
-
if (!config.enabled) return; // Inert until explicit later activation.
|
|
23
|
-
const compatible = config.compatibleOpenCode ?? [config.targetOpenCode];
|
|
24
|
-
if (!compatible.includes(ctx.app.version)) throw new Error(`Hybrid plugin compatibility check required: running OpenCode ${ctx.app.version}; verified versions: ${compatible.join(', ')}. Run the isolated compatibility probe before adding this version. Existing task history is preserved.`);
|
|
25
|
-
const controllers = new Map();
|
|
26
|
-
await registerImpeccableCommand(ctx);
|
|
27
|
-
const forSession = async sessionID => {
|
|
28
|
-
const result = await ctx.session.get({ sessionID });
|
|
29
|
-
const session = result.data ?? result;
|
|
30
|
-
const directory = session.location?.directory;
|
|
31
|
-
if (typeof directory !== 'string') throw new Error('Cannot resolve session workspace; refusing to act on plugin-instance directory.');
|
|
32
|
-
if (!controllers.has(directory)) controllers.set(directory, new Controller(directory, config.storageFor?.(directory) ?? storage, config));
|
|
33
|
-
return controllers.get(directory);
|
|
34
|
-
};
|
|
35
|
-
await registerContinuation(ctx,forSession);
|
|
36
|
-
await ctx.session.hook('prompt', async event => {
|
|
37
|
-
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
38
|
-
const response = await ctx.session.get({sessionID:event.sessionID});
|
|
39
|
-
const session = response.data ?? response;
|
|
40
|
-
if (session.parentID) return; // Subagent assignments are not new human design requests.
|
|
41
|
-
const text = event.prompt.text;
|
|
42
|
-
if (typeof text === 'string') recordDesignRequest(await forSession(event.sessionID),text,event.messageID);
|
|
43
|
-
});
|
|
44
|
-
await ctx.session.hook('context', async event => {
|
|
45
|
-
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
46
|
-
event.system.push({type:'text',text:roleText(DESIGN_POLICY)});
|
|
47
|
-
event.system.push({type:'text',text:roleText(WORKFLOW_POLICY)});
|
|
48
|
-
event.system.push({type:'text',text:PLANNING_POLICY});
|
|
49
|
-
event.system.push({type:'text',text:IMPECCABLE_POLICY});
|
|
50
|
-
event.system.push({type:'text',text:CONTINUATION_POLICY});
|
|
51
|
-
event.system.push({type:'text',text:`${FRONTEND_COMMON}\n${FRONTEND_BUILD}`});
|
|
52
|
-
event.system.push({type:'text',text:'Hybrid recovery: completed negative/blocked code reviews allow corrections and evidence collection in fix; continue automatically and re-review, never claim acceptance. action=fix recovers a legacy completed review. New evidence in checks/images resumes paused plan/design; action=replan with concrete changed requirements opens architecture revision while preserving findings and history. action=inspect checks installed tools and CLI logins without model calls and is available during planning. Never edit bridge state or reset history. Playwright research outputs use plain filenames in .playwright-mcp/hybrid-evidence; this generated evidence is excluded from source snapshots. Navigation/resizing and screenshot capture are available while planning/designing. Supply actual PNGs to plan/design/review as images [{path,caption,artifactHash}], using status.currentArtifactHash for the code/reference being inspected. Recapture after code changes and include interaction evidence in checks. Both reviewers receive pixels; never substitute hashes or ask the user to attest images merely because they live outside Git.'});
|
|
53
|
-
});
|
|
54
|
-
await ctx.tool.transform(editor => editor.add({
|
|
55
|
-
name: 'hybrid_checkpoint',
|
|
56
|
-
description: 'High-level checkpoints. begin: mode cosmetic/small/ui/feature, brief, designMode none/existing/supplied/new and reference designEvidence for reuse/supplied designs. Use ui for existing-app visual redesigns preserving architecture/behavior; design directly. Feature plan precedes new design: configured lead authors, configured reviewers validate. At least one pass and zero medium/high/critical findings from all configured reviewers required. Negative review permits corrections; fix recovers legacy completed blocked reviews. plan/design accept new checks/images to resolve evidence pauses. replan accepts changed requirements in checks, retaining history/baseline. inspect checks dependencies/auth without model calls. Actual PNG images [{path,caption,artifactHash}] can accompany plan/design/review; use status.currentArtifactHash. Playwright outputs are in project/.playwright-mcp/hybrid-evidence. Follow-up reviews focus on changes and unresolved findings. Supply resolutions [{id,evidence}] for every blocking ID; incomplete explicit checklists return without spending calls. No code-line or screenshot-count quotas. browser: capture declared package.json hybrid.browser static frontend scenarios with disposable API fixtures; see docs/browser-evidence.md. Results attach to reviews automatically and do not grant approval. Builds need approval; source-only baseline capture can run earlier. For SSR/live backends retain existing integration tests and images/checks. Status is free.',
|
|
57
|
-
options: { codemode: false },
|
|
58
|
-
input: { type: 'object', additionalProperties: false, properties: {
|
|
59
|
-
action: { type: 'string', enum: ['begin', 'plan', 'replan', 'design', 'verify', 'browser', 'review', 'fix', 'inspect', 'research', 'status', 'finish'] },
|
|
60
|
-
kind:{type:'string',enum:['packages','note','report'],description:'Research works before plan approval. Fetch public npm metadata, save attributed checks outside source, or read an existing report by call number. Evidence is automatically attached to planning.'},
|
|
61
|
-
packages:{type:'array',items:{type:'object',additionalProperties:false,properties:{name:{type:'string'},version:{type:'string'}},required:['name','version']}},
|
|
62
|
-
reportCall:{type:'integer',description:'Call number from status.reports. Reads only this task history; no arbitrary file path.'},
|
|
63
|
-
mode: { type: 'string', enum: ['cosmetic', 'small', 'ui', 'feature'] }, brief: { type: 'string' }, checks: { type: 'string' },
|
|
64
|
-
designMode: {type:'string',enum:['none','existing','supplied','new']}, designEvidence:{type:'string'},
|
|
65
|
-
script: { type: 'string', enum: ['typecheck', 'lint', 'test', 'build'] },
|
|
66
|
-
verificationMode:{type:'string',enum:['auto','full'],description:'auto honors a project deterministic Vitest input contract; full always executes the entire script. Targeted checks are partial coverage, never a full-suite pass.'},
|
|
67
|
-
resolutions: {type:'array',items:{type:'object',additionalProperties:false,properties:{id:{type:'string'},evidence:{type:'string'}},required:['id','evidence']}},
|
|
68
|
-
retryReason:{type:'string',description:'After repeated identical reviewer transport/auth/schema failure, describe new diagnostic/recovery evidence. Does not change review evidence, clear findings or grant approval.'},
|
|
69
|
-
images: { type:'array', items:{type:'object',additionalProperties:false,properties:{path:{type:'string'},caption:{type:'string'},artifactHash:{type:'string'}},required:['path','caption','artifactHash']} },
|
|
70
|
-
}, required: ['action'] },
|
|
71
|
-
execute: async (input, context) => {
|
|
72
|
-
const controller = await forSession(context.sessionID);
|
|
73
|
-
let pendingProgress = false;
|
|
74
|
-
const progress = async () => {
|
|
75
|
-
if (!context.progress || pendingProgress) return;
|
|
76
|
-
pendingProgress = true;
|
|
77
|
-
try {
|
|
78
|
-
const data = readDashboard(controller.folder, config), latest = data.calls.at(-1);
|
|
79
|
-
await context.progress({ title: latest ? `${latest.who} · ${latest.label} · ${latest.status}` : 'Hybrid checkpoint',
|
|
80
|
-
output: JSON.stringify({ phase: data.phase, totals: data.totals, latest: latest && { model: latest.resolvedModels, effort: latest.effort, action: latest.currentAction, usage: latest.usage } }) });
|
|
81
|
-
} catch { /* Monitoring does not change checkpoint outcome. */ } finally { pendingProgress = false; }
|
|
82
|
-
};
|
|
83
|
-
const timer = setInterval(progress, 3000); timer.unref();
|
|
84
|
-
try { const result = await controller.operation(input, context.signal, {sessionID:context.sessionID}); await progress(); return { content: JSON.stringify(result) }; }
|
|
85
|
-
finally { clearInterval(timer); }
|
|
86
|
-
},
|
|
87
|
-
}));
|
|
88
|
-
await ctx.tool.transform(editor => editor.add({
|
|
89
|
-
name: 'hybrid_activity', description: 'Read-only reviewer activity, requested/resolved models, effort and CLI-reported token usage. Missing usage is unavailable, not zero. Usage cards are integrated in the sidebar.',
|
|
90
|
-
options: { codemode: false }, input: { type: 'object', properties: {}, additionalProperties: false },
|
|
91
|
-
execute: async (_input, context) => {
|
|
92
|
-
const controller = await forSession(context.sessionID), data = readDashboard(controller.folder, config);
|
|
93
|
-
return { content: JSON.stringify({ phase: data.phase, operationActive: data.operationActive, totals: data.totals,
|
|
94
|
-
calls: data.calls.map(({ sequence, who, label, status, requestedModel, resolvedModels, effort, usage, verdict }) => ({ sequence, who, label, status, requestedModel, resolvedModels, effort, usage, verdict })) }) };
|
|
95
|
-
},
|
|
96
|
-
}));
|
|
97
|
-
await ctx.tool.hook('execute.before', async event => {
|
|
98
|
-
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
99
|
-
const controller=await forSession(event.sessionID);
|
|
100
|
-
if(researchTool(event,controller.directory))return;
|
|
101
|
-
if (!readTools.test(event.tool)) controller.assertWritable();
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
}; }
|
|
1
|
+
import {isReviewerSession} from '../lib/opencode-reviewer.mjs';
|
|
2
|
+
import {roleText} from '../lib/reviewers.mjs';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { WORKFLOW_POLICY } from './workflow-policy.mjs';
|
|
7
|
+
import { Controller } from './controller.mjs';
|
|
8
|
+
import { readDashboard } from './monitor-data.mjs';
|
|
9
|
+
import { DESIGN_POLICY, recordDesignRequest } from './design.mjs';
|
|
10
|
+
import { researchTool } from './research-tools.mjs';
|
|
11
|
+
import {registerImpeccableCommand,IMPECCABLE_POLICY} from '../lib/impeccable.mjs';
|
|
12
|
+
import {registerContinuation,CONTINUATION_POLICY} from '../lib/continuation.mjs';
|
|
13
|
+
import {PLANNING_POLICY} from './planning-research.mjs';
|
|
14
|
+
|
|
15
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// V2's Plugin.define is an identity function. This plain plugin needs no installed dependencies.
|
|
17
|
+
import { FRONTEND_COMMON, FRONTEND_BUILD } from './frontend-policy.mjs';
|
|
18
|
+
export const readTools = /^(read|glob|grep|list|ls|skill|webfetch|websearch|todowrite|todoread|question|subagent|hybrid_checkpoint|hybrid_activity|context7_.+|serena_(find_symbol|find_referencing_symbols|get_symbols_overview|search_for_pattern|read_file|list_dir|find_file|read_memory|list_memories|initial_instructions|check_onboarding_performed)|playwright_browser_(snapshot|take_screenshot|console_messages|network_requests))$/;
|
|
19
|
+
export function createPlugin(config, storage = config.runtimeDirectory ?? path.join(here, 'runtime')) { return {
|
|
20
|
+
id: 'Hybrid-Workflow',
|
|
21
|
+
async setup(ctx) {
|
|
22
|
+
if (!config.enabled) return; // Inert until explicit later activation.
|
|
23
|
+
const compatible = config.compatibleOpenCode ?? [config.targetOpenCode];
|
|
24
|
+
if (!compatible.includes(ctx.app.version)) throw new Error(`Hybrid plugin compatibility check required: running OpenCode ${ctx.app.version}; verified versions: ${compatible.join(', ')}. Run the isolated compatibility probe before adding this version. Existing task history is preserved.`);
|
|
25
|
+
const controllers = new Map();
|
|
26
|
+
await registerImpeccableCommand(ctx);
|
|
27
|
+
const forSession = async sessionID => {
|
|
28
|
+
const result = await ctx.session.get({ sessionID });
|
|
29
|
+
const session = result.data ?? result;
|
|
30
|
+
const directory = session.location?.directory;
|
|
31
|
+
if (typeof directory !== 'string') throw new Error('Cannot resolve session workspace; refusing to act on plugin-instance directory.');
|
|
32
|
+
if (!controllers.has(directory)) controllers.set(directory, new Controller(directory, config.storageFor?.(directory) ?? storage, config));
|
|
33
|
+
return controllers.get(directory);
|
|
34
|
+
};
|
|
35
|
+
await registerContinuation(ctx,forSession);
|
|
36
|
+
await ctx.session.hook('prompt', async event => {
|
|
37
|
+
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
38
|
+
const response = await ctx.session.get({sessionID:event.sessionID});
|
|
39
|
+
const session = response.data ?? response;
|
|
40
|
+
if (session.parentID) return; // Subagent assignments are not new human design requests.
|
|
41
|
+
const text = event.prompt.text;
|
|
42
|
+
if (typeof text === 'string') recordDesignRequest(await forSession(event.sessionID),text,event.messageID);
|
|
43
|
+
});
|
|
44
|
+
await ctx.session.hook('context', async event => {
|
|
45
|
+
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
46
|
+
event.system.push({type:'text',text:roleText(DESIGN_POLICY)});
|
|
47
|
+
event.system.push({type:'text',text:roleText(WORKFLOW_POLICY)});
|
|
48
|
+
event.system.push({type:'text',text:PLANNING_POLICY});
|
|
49
|
+
event.system.push({type:'text',text:IMPECCABLE_POLICY});
|
|
50
|
+
event.system.push({type:'text',text:CONTINUATION_POLICY});
|
|
51
|
+
event.system.push({type:'text',text:`${FRONTEND_COMMON}\n${FRONTEND_BUILD}`});
|
|
52
|
+
event.system.push({type:'text',text:'Hybrid recovery: completed negative/blocked code reviews allow corrections and evidence collection in fix; continue automatically and re-review, never claim acceptance. action=fix recovers a legacy completed review. New evidence in checks/images resumes paused plan/design; action=replan with concrete changed requirements opens architecture revision while preserving findings and history. action=inspect checks installed tools and CLI logins without model calls and is available during planning. Never edit bridge state or reset history. Playwright research outputs use plain filenames in .playwright-mcp/hybrid-evidence; this generated evidence is excluded from source snapshots. Navigation/resizing and screenshot capture are available while planning/designing. Supply actual PNGs to plan/design/review as images [{path,caption,artifactHash}], using status.currentArtifactHash for the code/reference being inspected. Recapture after code changes and include interaction evidence in checks. Both reviewers receive pixels; never substitute hashes or ask the user to attest images merely because they live outside Git.'});
|
|
53
|
+
});
|
|
54
|
+
await ctx.tool.transform(editor => editor.add({
|
|
55
|
+
name: 'hybrid_checkpoint',
|
|
56
|
+
description: 'High-level checkpoints. escalate: send disputed/unclear planning findings or capability failures to the premium lead with checks; follow planningNext. This advisory action never grants approval. begin: mode cosmetic/small/ui/feature, brief, designMode none/existing/supplied/new and reference designEvidence for reuse/supplied designs. Use ui for existing-app visual redesigns preserving architecture/behavior; design directly. Feature plan precedes new design: configured lead authors, configured reviewers validate. At least one pass and zero medium/high/critical findings from all configured reviewers required. Negative review permits corrections; fix recovers legacy completed blocked reviews. plan/design accept new checks/images to resolve evidence pauses. replan accepts changed requirements in checks, retaining history/baseline. inspect checks dependencies/auth without model calls. Actual PNG images [{path,caption,artifactHash}] can accompany plan/design/review; use status.currentArtifactHash. Playwright outputs are in project/.playwright-mcp/hybrid-evidence. Follow-up reviews focus on changes and unresolved findings. Supply resolutions [{id,evidence}] for every blocking ID; incomplete explicit checklists return without spending calls. No code-line or screenshot-count quotas. browser: capture declared package.json hybrid.browser static frontend scenarios with disposable API fixtures; see docs/browser-evidence.md. Results attach to reviews automatically and do not grant approval. Builds need approval; source-only baseline capture can run earlier. For SSR/live backends retain existing integration tests and images/checks. Status is free.',
|
|
57
|
+
options: { codemode: false },
|
|
58
|
+
input: { type: 'object', additionalProperties: false, properties: {
|
|
59
|
+
action: { type: 'string', enum: ['begin', 'plan', 'replan', 'escalate', 'design', 'verify', 'browser', 'review', 'fix', 'inspect', 'research', 'status', 'finish'] },
|
|
60
|
+
kind:{type:'string',enum:['packages','note','report'],description:'Research works before plan approval. Fetch public npm metadata, save attributed checks outside source, or read an existing report by call number. Evidence is automatically attached to planning.'},
|
|
61
|
+
packages:{type:'array',items:{type:'object',additionalProperties:false,properties:{name:{type:'string'},version:{type:'string'}},required:['name','version']}},
|
|
62
|
+
reportCall:{type:'integer',description:'Call number from status.reports. Reads only this task history; no arbitrary file path.'},
|
|
63
|
+
mode: { type: 'string', enum: ['cosmetic', 'small', 'ui', 'feature'] }, brief: { type: 'string' }, checks: { type: 'string' },
|
|
64
|
+
designMode: {type:'string',enum:['none','existing','supplied','new']}, designEvidence:{type:'string'},
|
|
65
|
+
script: { type: 'string', enum: ['typecheck', 'lint', 'test', 'build'] },
|
|
66
|
+
verificationMode:{type:'string',enum:['auto','full'],description:'auto honors a project deterministic Vitest input contract; full always executes the entire script. Targeted checks are partial coverage, never a full-suite pass.'},
|
|
67
|
+
resolutions: {type:'array',items:{type:'object',additionalProperties:false,properties:{id:{type:'string'},evidence:{type:'string'}},required:['id','evidence']}},
|
|
68
|
+
retryReason:{type:'string',description:'After repeated identical reviewer transport/auth/schema failure, describe new diagnostic/recovery evidence. Does not change review evidence, clear findings or grant approval.'},
|
|
69
|
+
images: { type:'array', items:{type:'object',additionalProperties:false,properties:{path:{type:'string'},caption:{type:'string'},artifactHash:{type:'string'}},required:['path','caption','artifactHash']} },
|
|
70
|
+
}, required: ['action'] },
|
|
71
|
+
execute: async (input, context) => {
|
|
72
|
+
const controller = await forSession(context.sessionID);
|
|
73
|
+
let pendingProgress = false;
|
|
74
|
+
const progress = async () => {
|
|
75
|
+
if (!context.progress || pendingProgress) return;
|
|
76
|
+
pendingProgress = true;
|
|
77
|
+
try {
|
|
78
|
+
const data = readDashboard(controller.folder, config), latest = data.calls.at(-1);
|
|
79
|
+
await context.progress({ title: latest ? `${latest.who} · ${latest.label} · ${latest.status}` : 'Hybrid checkpoint',
|
|
80
|
+
output: JSON.stringify({ phase: data.phase, totals: data.totals, latest: latest && { model: latest.resolvedModels, effort: latest.effort, action: latest.currentAction, usage: latest.usage } }) });
|
|
81
|
+
} catch { /* Monitoring does not change checkpoint outcome. */ } finally { pendingProgress = false; }
|
|
82
|
+
};
|
|
83
|
+
const timer = setInterval(progress, 3000); timer.unref();
|
|
84
|
+
try { const result = await controller.operation(input, context.signal, {sessionID:context.sessionID}); await progress(); return { content: JSON.stringify(result) }; }
|
|
85
|
+
finally { clearInterval(timer); }
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
await ctx.tool.transform(editor => editor.add({
|
|
89
|
+
name: 'hybrid_activity', description: 'Read-only reviewer activity, requested/resolved models, effort and CLI-reported token usage. Missing usage is unavailable, not zero. Usage cards are integrated in the sidebar.',
|
|
90
|
+
options: { codemode: false }, input: { type: 'object', properties: {}, additionalProperties: false },
|
|
91
|
+
execute: async (_input, context) => {
|
|
92
|
+
const controller = await forSession(context.sessionID), data = readDashboard(controller.folder, config);
|
|
93
|
+
return { content: JSON.stringify({ phase: data.phase, operationActive: data.operationActive, totals: data.totals,
|
|
94
|
+
calls: data.calls.map(({ sequence, who, label, status, requestedModel, resolvedModels, effort, usage, verdict }) => ({ sequence, who, label, status, requestedModel, resolvedModels, effort, usage, verdict })) }) };
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
await ctx.tool.hook('execute.before', async event => {
|
|
98
|
+
if(await isReviewerSession(ctx,event.sessionID))return;
|
|
99
|
+
const controller=await forSession(event.sessionID);
|
|
100
|
+
if(researchTool(event,controller.directory))return;
|
|
101
|
+
if (!readTools.test(event.tool)) controller.assertWritable();
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
}; }
|