wogiflow 2.30.4 → 2.31.1
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/.claude/commands/wogi-self-adversary.md +130 -0
- package/.claude/docs/config-schema.md +219 -0
- package/package.json +2 -2
- package/scripts/flow-defer-auth.js +41 -10
- package/scripts/flow-deferral-classifier-ai.js +3 -1
- package/scripts/flow-impl-question-classifier.js +178 -0
- package/scripts/flow-self-adversary-loop.js +422 -0
- package/scripts/flow-standards-gate.js +3 -1
- package/scripts/hooks/core/deferral-classifier.js +3 -0
- package/scripts/hooks/core/deferral-gate.js +6 -3
- package/scripts/hooks/core/gate-orchestrator.js +26 -1
- package/scripts/hooks/core/pre-tool-deps.js +11 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/self-adversary-gate.js +295 -0
- package/scripts/hooks/core/session-start-orchestrator.js +269 -0
- package/scripts/hooks/core/stop-orchestrator.js +123 -0
- package/scripts/hooks/core/task-boundary-restart-coordinator.js +84 -0
- package/scripts/hooks/core/user-prompt-orchestrator.js +201 -0
- package/scripts/hooks/core/workspace-stop-gates.js +133 -0
- package/scripts/hooks/core/workspace-stop-notify.js +76 -0
- package/scripts/hooks/entry/claude-code/session-start.js +19 -352
- package/scripts/hooks/entry/claude-code/stop.js +10 -485
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +9 -277
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Task-boundary restart coordinator (wf-6e31850e A-3 / extracted from stop.js).
|
|
5
|
+
*
|
|
6
|
+
* Coordinates session restart at task boundary. Calls into
|
|
7
|
+
* task-boundary-reset for the actual SIGTERM, but handles the surrounding
|
|
8
|
+
* concerns: Phase 1 fallback marking, session-history recording.
|
|
9
|
+
*
|
|
10
|
+
* Returns `{ shouldReturn: true, result: {...} }` when the entry should
|
|
11
|
+
* short-circuit; otherwise `null` to continue.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function handleTaskBoundaryRestart({ parsedInput }) {
|
|
15
|
+
try {
|
|
16
|
+
const {
|
|
17
|
+
consumeAndTriggerRestart,
|
|
18
|
+
hasPendingMarker,
|
|
19
|
+
ensurePhase1MarkedIfRecentlyCompleted
|
|
20
|
+
} = require('./task-boundary-reset');
|
|
21
|
+
|
|
22
|
+
// Phase 1 fallback
|
|
23
|
+
try {
|
|
24
|
+
const fallback = ensurePhase1MarkedIfRecentlyCompleted();
|
|
25
|
+
if (fallback.marked && process.env.DEBUG) {
|
|
26
|
+
console.error(`[Stop] Phase 1 fallback marked ${fallback.taskId}`);
|
|
27
|
+
} else if (!fallback.marked && fallback.reason !== 'marker-already-present' &&
|
|
28
|
+
fallback.reason !== 'no-fresh-completion' &&
|
|
29
|
+
fallback.reason !== 'stale-completion' &&
|
|
30
|
+
fallback.reason !== 'already-triggered-for-this-task' &&
|
|
31
|
+
process.env.DEBUG) {
|
|
32
|
+
console.error(`[Stop] Phase 1 fallback skipped: ${fallback.reason}`);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (process.env.DEBUG) console.error(`[Stop] Phase 1 fallback error (fail-open): ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (hasPendingMarker()) {
|
|
39
|
+
try {
|
|
40
|
+
const { recordSessionEnd } = require('./session-history');
|
|
41
|
+
let cliSessionId = parsedInput?.sessionId || null;
|
|
42
|
+
if (!cliSessionId) {
|
|
43
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
44
|
+
const path = require('node:path');
|
|
45
|
+
const ss = safeJsonParse(path.join(PATHS.state, 'session-state.json'), {});
|
|
46
|
+
cliSessionId = ss.cliSessionId || null;
|
|
47
|
+
}
|
|
48
|
+
if (cliSessionId) {
|
|
49
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
50
|
+
const path = require('node:path');
|
|
51
|
+
const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), {});
|
|
52
|
+
const recent = ready.recentlyCompleted || [];
|
|
53
|
+
const lastCompleted = recent[0] || null;
|
|
54
|
+
recordSessionEnd({
|
|
55
|
+
cliSessionId,
|
|
56
|
+
endReason: 'task-boundary-restart',
|
|
57
|
+
tasksCompletedInSession: recent.slice(0, 5).map(t => t.id).filter(Boolean),
|
|
58
|
+
lastActiveTaskTitle: lastCompleted?.title || null
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (process.env.DEBUG) console.error(`[Stop] Session history record failed (non-fatal): ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const restartResult = await consumeAndTriggerRestart({
|
|
67
|
+
transcriptPath: parsedInput?.transcriptPath
|
|
68
|
+
});
|
|
69
|
+
if (restartResult.triggered) {
|
|
70
|
+
if (process.env.DEBUG) {
|
|
71
|
+
console.error(`[Stop] Task-boundary restart triggered — claude will exit, wrapper will relaunch`);
|
|
72
|
+
}
|
|
73
|
+
return { shouldReturn: true, result: { __raw: true, continue: false } };
|
|
74
|
+
}
|
|
75
|
+
if (restartResult.reason !== 'no-pending-marker' && process.env.DEBUG) {
|
|
76
|
+
console.error(`[Stop] Task-boundary restart check: ${restartResult.reason}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (process.env.DEBUG) console.error(`[Stop] Task-boundary restart module error (fail-open): ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { handleTaskBoundaryRestart };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — UserPromptSubmit Orchestrator (wf-6e31850e A-3)
|
|
5
|
+
*
|
|
6
|
+
* Extracted from scripts/hooks/entry/claude-code/user-prompt-submit.js to
|
|
7
|
+
* bring that entry file under the 120-LOC budget per
|
|
8
|
+
* .claude/rules/architecture/hook-three-layer.md.
|
|
9
|
+
*
|
|
10
|
+
* Same control flow as before. Entry user-prompt-submit.js is now a thin
|
|
11
|
+
* pass-through. Returns the coreResult that the entry forwards to the
|
|
12
|
+
* adapter.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const { checkImplementationGate } = require('./implementation-gate');
|
|
17
|
+
const { checkResearchRequirement } = require('./research-gate');
|
|
18
|
+
const { setRoutingPending, clearRoutingPending, ROUTING_CLEARED_PATH } = require('./routing-gate');
|
|
19
|
+
const { getPhaseContextPrompt } = require('./phase-gate');
|
|
20
|
+
const { buildOverdueContext } = require('./overdue-dispatches');
|
|
21
|
+
const { getDossierInjection } = require('./feature-dossier-gate');
|
|
22
|
+
const {
|
|
23
|
+
shouldForceExtractReview,
|
|
24
|
+
buildEnforcementMessage,
|
|
25
|
+
markLongInputPending
|
|
26
|
+
} = require('./long-input-enforcement');
|
|
27
|
+
const { markSkillPending, loadDurableSession } = require('../../flow-durable-session');
|
|
28
|
+
const { captureCurrentPrompt } = require('../../flow-prompt-capture');
|
|
29
|
+
const { spawnBackgroundDetection } = require('../../flow-correction-detector');
|
|
30
|
+
const { getConfig } = require('../../flow-utils');
|
|
31
|
+
|
|
32
|
+
async function orchestrateUserPromptSubmit({ input, parsedInput }) {
|
|
33
|
+
if (!input || Object.keys(input).length === 0) {
|
|
34
|
+
return { __raw: true, continue: true, hookSpecificOutput: { hookEventName: 'UserPromptSubmit' } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const prompt = parsedInput.prompt;
|
|
38
|
+
const source = parsedInput.source;
|
|
39
|
+
|
|
40
|
+
// wf-729ab5c0 follow-up — clear pending-question marker on user response.
|
|
41
|
+
try {
|
|
42
|
+
const { clearPendingQuestion } = require('../../flow-ask');
|
|
43
|
+
const r = clearPendingQuestion();
|
|
44
|
+
if (r.wasPresent && process.env.DEBUG) {
|
|
45
|
+
console.error(`[UserPromptSubmit] Cleared pending-question marker — restart deferral released`);
|
|
46
|
+
}
|
|
47
|
+
} catch (_err) { /* non-fatal */ }
|
|
48
|
+
|
|
49
|
+
// v4.1: Detect skill commands that need execution tracking
|
|
50
|
+
if (typeof prompt === 'string') {
|
|
51
|
+
const skillMatch = prompt.match(/^\/(wogi-bulk|wogi-start)\b/i);
|
|
52
|
+
if (skillMatch) {
|
|
53
|
+
const skillName = skillMatch[1].toLowerCase();
|
|
54
|
+
markSkillPending(skillName, { prompt });
|
|
55
|
+
if (process.env.DEBUG) console.error(`[Hook] Marked /${skillName} as pending execution`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let hookConfig;
|
|
60
|
+
try { hookConfig = getConfig(); } catch (_err) { hookConfig = {}; }
|
|
61
|
+
|
|
62
|
+
// v5.0: Capture prompt for learning system (non-blocking)
|
|
63
|
+
if (hookConfig.hooks?.rules?.intelligence?.promptCapture?.enabled !== false) {
|
|
64
|
+
if (typeof prompt === 'string' && prompt.trim().length > 0) {
|
|
65
|
+
setImmediate(() => {
|
|
66
|
+
try { captureCurrentPrompt(prompt); } catch (err) {
|
|
67
|
+
if (process.env.DEBUG) console.error(`[Hook] Prompt capture failed: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// wf-5cd71b1f: Research-required classifier
|
|
74
|
+
if (typeof prompt === 'string' && prompt.trim().length > 0) {
|
|
75
|
+
try {
|
|
76
|
+
const { applyClassification: applyResearchClassification } = require('./research-required-classifier');
|
|
77
|
+
const r = applyResearchClassification(prompt, hookConfig);
|
|
78
|
+
if (r.applied && process.env.DEBUG) {
|
|
79
|
+
console.error(`[Hook] Research-required classifier: category=${r.category}, match="${r.match}"`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (process.env.DEBUG) console.error(`[Hook] Research-required classifier failed: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// wf-b8839d99: AI-based deferral classifier
|
|
87
|
+
if (typeof prompt === 'string' && prompt.trim().length > 0) {
|
|
88
|
+
try {
|
|
89
|
+
const { applyClassification } = require('./deferral-classifier');
|
|
90
|
+
const r = await applyClassification(prompt, hookConfig);
|
|
91
|
+
if (r.applied && process.env.DEBUG) {
|
|
92
|
+
console.error(`[Hook] Deferral classifier (AI): intent=${r.intent}, confidence=${r.confidence}, standing=${r.standing}, scope=${JSON.stringify(r.scope)}`);
|
|
93
|
+
} else if (process.env.DEBUG && r.reason) {
|
|
94
|
+
console.error(`[Hook] Deferral classifier (AI): no-op — ${r.reason}`);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (process.env.DEBUG) console.error(`[Hook] Deferral classifier failed: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Correction detection (background)
|
|
102
|
+
if (hookConfig.hooks?.rules?.intelligence?.correctionDetection?.enabled !== false) {
|
|
103
|
+
if (typeof prompt === 'string' && prompt.trim().length > 0) {
|
|
104
|
+
try {
|
|
105
|
+
const session = loadDurableSession();
|
|
106
|
+
spawnBackgroundDetection(prompt, session?.taskId || '');
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (process.env.DEBUG) console.error(`[Hook] Correction detection spawn failed: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// v6.0: Routing-pending flag set/clear
|
|
114
|
+
const isWogiCommand = typeof prompt === 'string' && /^\/wogi-[a-z0-9-]+\b/i.test(prompt.trim());
|
|
115
|
+
if (!isWogiCommand) {
|
|
116
|
+
try { fs.unlinkSync(ROUTING_CLEARED_PATH); }
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err.code !== 'ENOENT' && process.env.DEBUG) console.error(`[Hook] Failed to delete cleared marker: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
try { setRoutingPending(); }
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (process.env.DEBUG) console.error(`[Hook] Routing gate set failed: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
try {
|
|
126
|
+
clearRoutingPending();
|
|
127
|
+
if (process.env.DEBUG) console.error(`[Hook] Cleared routing flag — prompt is a /wogi-* command`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (process.env.DEBUG) console.error(`[Hook] Routing gate clear failed: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Phase context + dossier injection
|
|
134
|
+
let phasePrompt = null;
|
|
135
|
+
try {
|
|
136
|
+
const phaseContext = getPhaseContextPrompt();
|
|
137
|
+
if (phaseContext.inject && phaseContext.prompt) phasePrompt = phaseContext.prompt;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (process.env.DEBUG) console.error(`[Hook] Phase context injection failed: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
let dossierPrompt = null;
|
|
142
|
+
try { dossierPrompt = getDossierInjection(); } catch (err) {
|
|
143
|
+
if (process.env.DEBUG) console.error(`[Hook] Dossier injection failed: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
if (dossierPrompt) {
|
|
146
|
+
phasePrompt = phasePrompt ? `${phasePrompt}\n\n${dossierPrompt}` : dossierPrompt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Research + implementation gates
|
|
150
|
+
const researchResult = checkResearchRequirement({ prompt, source });
|
|
151
|
+
let coreResult = checkImplementationGate({ prompt, source });
|
|
152
|
+
|
|
153
|
+
if (researchResult.injectProtocol && researchResult.protocolSteps) {
|
|
154
|
+
coreResult = {
|
|
155
|
+
...coreResult,
|
|
156
|
+
systemReminder: researchResult.protocolSteps,
|
|
157
|
+
researchTriggered: true,
|
|
158
|
+
questionType: researchResult.questionType,
|
|
159
|
+
suggestedDepth: researchResult.suggestedDepth
|
|
160
|
+
};
|
|
161
|
+
} else if (researchResult.warning && coreResult.allowed) {
|
|
162
|
+
coreResult = {
|
|
163
|
+
...coreResult,
|
|
164
|
+
warning: true,
|
|
165
|
+
researchWarning: researchResult.message,
|
|
166
|
+
suggestedCommand: researchResult.suggestedCommand
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (phasePrompt) coreResult = { ...coreResult, phasePrompt };
|
|
171
|
+
|
|
172
|
+
// wf-d3e67abe — overdue workspace dispatches
|
|
173
|
+
try {
|
|
174
|
+
const overduePrompt = buildOverdueContext();
|
|
175
|
+
if (overduePrompt) coreResult = { ...coreResult, overduePrompt };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (process.env.DEBUG) console.error(`[Hook] Overdue dispatches check failed: ${err.message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// P11.5 mechanical enforcement — long-form prompts without source-link
|
|
181
|
+
try {
|
|
182
|
+
const enforce = shouldForceExtractReview({ text: prompt, source });
|
|
183
|
+
if (enforce.forced) {
|
|
184
|
+
const msg = buildEnforcementMessage(enforce.reason, enforce.level);
|
|
185
|
+
coreResult = { ...coreResult, longInputEnforcement: msg };
|
|
186
|
+
markLongInputPending({
|
|
187
|
+
level: enforce.level,
|
|
188
|
+
reason: enforce.reason,
|
|
189
|
+
promptPreview: typeof prompt === 'string' ? prompt.slice(0, 200) : '(non-string)',
|
|
190
|
+
source: source || null,
|
|
191
|
+
repoName: process.env.WOGI_REPO_NAME || null
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (process.env.DEBUG) console.error(`[Hook] Long-input enforcement check failed: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return coreResult;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { orchestrateUserPromptSubmit };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace Stop-hook gates (wf-6e31850e A-3 / extracted from stop.js).
|
|
5
|
+
*
|
|
6
|
+
* Three gates in sequence, all fail-open:
|
|
7
|
+
* 1. Gap B — block end-of-turn when worker has queued dispatches but no
|
|
8
|
+
* task in progress.
|
|
9
|
+
* 2. Worker Tool-First Turn Gate (G1+G4+G6) — every worker turn after a
|
|
10
|
+
* UserPromptSubmit must contain at least one tool call.
|
|
11
|
+
* 3. AI Worker Question Classifier (G3) — Haiku classifier blocks
|
|
12
|
+
* turns ending with an open user-facing question in worker mode.
|
|
13
|
+
*
|
|
14
|
+
* Returns `{ shouldReturn: true, result: {...} }` to short-circuit, or null.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
async function checkWorkspaceStopGates({ parsedInput }) {
|
|
18
|
+
// Gap B
|
|
19
|
+
try {
|
|
20
|
+
const isWorker = process.env.WOGI_WORKSPACE_ROOT &&
|
|
21
|
+
process.env.WOGI_REPO_NAME &&
|
|
22
|
+
process.env.WOGI_REPO_NAME !== 'manager';
|
|
23
|
+
if (isWorker) {
|
|
24
|
+
const { getConfig, PATHS, safeJsonParse } = require('../../flow-utils');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const config = getConfig();
|
|
27
|
+
const gateEnabled = config.workspace?.autoPickupChannelDispatches !== false;
|
|
28
|
+
if (gateEnabled) {
|
|
29
|
+
const ready = safeJsonParse(path.join(PATHS.state, 'ready.json'), { ready: [], inProgress: [] });
|
|
30
|
+
const inProgressCount = (ready.inProgress || []).length;
|
|
31
|
+
const queued = (ready.ready || []).filter(t => {
|
|
32
|
+
if (!t || typeof t !== 'object') return false;
|
|
33
|
+
return t.channelSource === 'wogi-workspace-channel' ||
|
|
34
|
+
t.dispatchedBy === 'workspace-manager' ||
|
|
35
|
+
(typeof t.source === 'string' && t.source.startsWith('workspace:'));
|
|
36
|
+
});
|
|
37
|
+
if (inProgressCount === 0 && queued.length > 0) {
|
|
38
|
+
const nextId = queued[0].id;
|
|
39
|
+
const msg = [
|
|
40
|
+
`AUTONOMOUS MODE VIOLATION: ${queued.length} channel dispatch(es) queued, no task in progress.`,
|
|
41
|
+
'',
|
|
42
|
+
`You are a workspace worker — "awaiting your signal" / "let me know" / "or will proceed" is NOT a valid terminal state.`,
|
|
43
|
+
'',
|
|
44
|
+
'Exactly one of these must be true at end-of-turn:',
|
|
45
|
+
' (a) You started the next pre-approved dispatch (ACTION), or',
|
|
46
|
+
' (b) You channel-dispatched a "## QUESTION:" to manager (ESCALATION), or',
|
|
47
|
+
' (c) Zero queued and zero in-progress (IDLE — not your current state).',
|
|
48
|
+
'',
|
|
49
|
+
`ACT NOW: Invoke Skill(skill="wogi-start", args="${nextId}")`,
|
|
50
|
+
'',
|
|
51
|
+
`Or escalate: curl -s -X POST http://127.0.0.1:${process.env.WOGI_MANAGER_PORT || '8800'} \\`,
|
|
52
|
+
` -H "X-Wogi-From: ${process.env.WOGI_REPO_NAME}" \\`,
|
|
53
|
+
` --data-binary "## QUESTION: <your blocker>"`
|
|
54
|
+
].join('\n');
|
|
55
|
+
return { shouldReturn: true, result: { __raw: true, continue: true, stopReason: msg } };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (process.env.DEBUG) console.error(`[Stop] Workspace autopickup gate error (fail-open): ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Worker Tool-First Turn Gate
|
|
64
|
+
try {
|
|
65
|
+
const { isWorkerMode, checkWorkerToolFirstTurn, renderBlockMessage } = require('./worker-tool-first-gate');
|
|
66
|
+
if (isWorkerMode() && parsedInput?.transcriptPath) {
|
|
67
|
+
const { getConfig } = require('../../flow-utils');
|
|
68
|
+
const config = getConfig();
|
|
69
|
+
const gateCfg = config.workspace?.toolFirstTurnGate;
|
|
70
|
+
const enabled = gateCfg?.enabled !== false;
|
|
71
|
+
if (enabled) {
|
|
72
|
+
const strict = gateCfg?.strict !== false;
|
|
73
|
+
const result = checkWorkerToolFirstTurn({ transcriptPath: parsedInput.transcriptPath, strict });
|
|
74
|
+
if (result.blocked) {
|
|
75
|
+
return { shouldReturn: true, result: { __raw: true, continue: true, stopReason: renderBlockMessage(result) } };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (process.env.DEBUG) console.error(`[Stop] Worker tool-first gate error (fail-open): ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// AI Worker Question Classifier
|
|
84
|
+
try {
|
|
85
|
+
const isWorker = process.env.WOGI_WORKSPACE_ROOT &&
|
|
86
|
+
process.env.WOGI_REPO_NAME &&
|
|
87
|
+
process.env.WOGI_REPO_NAME !== 'manager';
|
|
88
|
+
if (isWorker) {
|
|
89
|
+
const { getConfig } = require('../../flow-utils');
|
|
90
|
+
const config = getConfig();
|
|
91
|
+
const clf = config.workspace?.aiWorkerQuestionClassifier;
|
|
92
|
+
const enabled = clf?.enabled !== false;
|
|
93
|
+
if (enabled && parsedInput?.transcriptPath) {
|
|
94
|
+
const { classifyWorkerQuestion } = require('../../flow-worker-question-classifier');
|
|
95
|
+
const result = await classifyWorkerQuestion({
|
|
96
|
+
transcriptPath: parsedInput.transcriptPath,
|
|
97
|
+
minConfidence: Number.isFinite(clf?.minConfidence) ? clf.minConfidence : 70,
|
|
98
|
+
model: typeof clf?.model === 'string' ? clf.model : undefined
|
|
99
|
+
});
|
|
100
|
+
if (result?.blocked) {
|
|
101
|
+
const port = process.env.WOGI_MANAGER_PORT || '8800';
|
|
102
|
+
const repo = process.env.WOGI_REPO_NAME;
|
|
103
|
+
const msg = [
|
|
104
|
+
`WORKER→USER QUESTION DETECTED (confidence ${result.confidence}%, threshold ${result.minConfidence}%):`,
|
|
105
|
+
` "${String(result.reason || '').slice(0, 200)}"`,
|
|
106
|
+
'',
|
|
107
|
+
'In workspace mode, workers CANNOT ask the user directly — the user only sees',
|
|
108
|
+
'the manager terminal. Your question will stall silently.',
|
|
109
|
+
'',
|
|
110
|
+
'Channel-dispatch to the manager instead, THEN end the turn:',
|
|
111
|
+
'',
|
|
112
|
+
` curl -s -X POST http://127.0.0.1:${port} \\`,
|
|
113
|
+
` -H "X-Wogi-From: ${repo}" \\`,
|
|
114
|
+
` --data-binary "## QUESTION: <your question>"`,
|
|
115
|
+
'',
|
|
116
|
+
'The manager will relay to the user, capture the answer, and dispatch a',
|
|
117
|
+
'follow-up task to you with the resolved context.',
|
|
118
|
+
'',
|
|
119
|
+
'If you don\'t actually need the user — make a reasonable autonomous decision',
|
|
120
|
+
'and note it in your ## Results reply to the manager. Then end the turn.'
|
|
121
|
+
].join('\n');
|
|
122
|
+
return { shouldReturn: true, result: { __raw: true, continue: true, stopReason: msg } };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (process.env.DEBUG) console.error(`[Stop] Worker question classifier error (fail-open): ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { checkWorkspaceStopGates };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace worker-stopped notification (wf-6e31850e A-3 / extracted from stop.js).
|
|
5
|
+
*
|
|
6
|
+
* Writes a structured `worker-stopped` message to the workspace message bus
|
|
7
|
+
* so the manager's overdue-check can distinguish "graceful stop" from
|
|
8
|
+
* "silent death" vs "task-complete". Original: wf-d3e67abe.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
async function notifyWorkerStopped() {
|
|
12
|
+
if (!process.env.WOGI_REPO_NAME || process.env.WOGI_REPO_NAME === 'manager') return;
|
|
13
|
+
try {
|
|
14
|
+
const nodePath = require('node:path');
|
|
15
|
+
const childProcess = require('node:child_process');
|
|
16
|
+
const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
17
|
+
const repoName = process.env.WOGI_REPO_NAME;
|
|
18
|
+
if (!VALID_NAME.test(repoName)) throw new Error('Invalid WOGI_REPO_NAME');
|
|
19
|
+
|
|
20
|
+
const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
|
|
21
|
+
if (!workspaceRoot) return;
|
|
22
|
+
|
|
23
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
24
|
+
const ready = safeJsonParse(nodePath.join(PATHS.state, 'ready.json'), {});
|
|
25
|
+
const recentTask = (ready.recentlyCompleted || [])[0];
|
|
26
|
+
const inProgressTask = (ready.inProgress || [])[0];
|
|
27
|
+
const mostRecent = recentTask || inProgressTask;
|
|
28
|
+
|
|
29
|
+
const hasInProgress = Boolean(inProgressTask);
|
|
30
|
+
const state = hasInProgress ? 'mid-work' : 'idle';
|
|
31
|
+
const taskInProgress = hasInProgress ? inProgressTask.id : null;
|
|
32
|
+
|
|
33
|
+
let lastSha = null;
|
|
34
|
+
try {
|
|
35
|
+
lastSha = childProcess.execSync('git rev-parse --short HEAD 2>/dev/null || true', {
|
|
36
|
+
cwd: PATHS.root,
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
timeout: 2000
|
|
39
|
+
}).trim() || null;
|
|
40
|
+
} catch (_err) { /* non-critical */ }
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const libMessages = nodePath.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-messages');
|
|
44
|
+
const { createMessage, saveMessage } = require(libMessages);
|
|
45
|
+
const msg = createMessage({
|
|
46
|
+
from: repoName,
|
|
47
|
+
to: 'manager',
|
|
48
|
+
type: 'worker-stopped',
|
|
49
|
+
subject: hasInProgress
|
|
50
|
+
? `Worker stopped mid-work on ${taskInProgress}`
|
|
51
|
+
: `Worker stopped (idle)`,
|
|
52
|
+
body: [
|
|
53
|
+
`Worker "${repoName}" is stopping.`,
|
|
54
|
+
`State: ${state}`,
|
|
55
|
+
taskInProgress ? `Task in progress: ${taskInProgress}` : null,
|
|
56
|
+
mostRecent?.title ? `Most recent task: ${mostRecent.title}` : null,
|
|
57
|
+
lastSha ? `Last commit: ${lastSha}` : null
|
|
58
|
+
].filter(Boolean).join('\n'),
|
|
59
|
+
priority: hasInProgress ? 'high' : 'medium',
|
|
60
|
+
actionRequired: hasInProgress
|
|
61
|
+
});
|
|
62
|
+
msg.taskId = taskInProgress;
|
|
63
|
+
msg.reason = 'graceful';
|
|
64
|
+
msg.state = state;
|
|
65
|
+
msg.taskInProgress = taskInProgress;
|
|
66
|
+
msg.lastSha = lastSha;
|
|
67
|
+
saveMessage(workspaceRoot, msg);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (process.env.DEBUG) console.error(`[Stop] Workspace message write failed: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (process.env.DEBUG) console.error(`[Stop] Workspace notification failed: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { notifyWorkerStopped };
|