wogiflow 2.31.0 → 2.31.2
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/docs/config-schema.md +219 -0
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +27 -0
- package/scripts/flow-defer-auth.js +41 -10
- package/scripts/flow-deferral-classifier-ai.js +3 -1
- package/scripts/flow-impl-question-classifier.js +3 -1
- package/scripts/flow-self-adversary-loop.js +81 -14
- package/scripts/flow-standards-gate.js +3 -1
- package/scripts/hooks/core/deferral-classifier.js +3 -0
- package/scripts/hooks/core/deferral-gate.js +8 -3
- package/scripts/hooks/core/gate-orchestrator.js +46 -8
- package/scripts/hooks/core/self-adversary-gate.js +4 -1
- package/scripts/hooks/core/session-start-orchestrator.js +269 -0
- package/scripts/hooks/core/stop-orchestrator.js +126 -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,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — SessionStart Orchestrator (wf-6e31850e A-3)
|
|
5
|
+
*
|
|
6
|
+
* Extracted from scripts/hooks/entry/claude-code/session-start.js to bring
|
|
7
|
+
* 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 session-start.js is now a thin
|
|
11
|
+
* pass-through that imports boot-instrumentation helpers + this orchestrator.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { gatherSessionContext } = require('./session-context');
|
|
15
|
+
const { setCliSessionId, clearStaleCurrentTaskAsync, resetSessionTaskCounter } = require('../../flow-session-state');
|
|
16
|
+
const { checkAndResetStalePhase } = require('./phase-gate');
|
|
17
|
+
const { setRoutingPending } = require('./routing-gate');
|
|
18
|
+
const { getConfig } = require('../../flow-utils');
|
|
19
|
+
|
|
20
|
+
let autoSyncBridge = null;
|
|
21
|
+
function getAutoSyncBridge() {
|
|
22
|
+
if (!autoSyncBridge) {
|
|
23
|
+
try {
|
|
24
|
+
autoSyncBridge = require('../../flow-bridge-state').autoSyncBridge;
|
|
25
|
+
} catch (_err) {
|
|
26
|
+
autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return autoSyncBridge;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function orchestrateSessionStart({ parsedInput, bootMark, bootTime }) {
|
|
33
|
+
bootMark('SessionStart hook entered');
|
|
34
|
+
|
|
35
|
+
const bridgeSyncPromise = bootTime('bridge auto-sync', async () => {
|
|
36
|
+
try {
|
|
37
|
+
const syncFn = getAutoSyncBridge();
|
|
38
|
+
await syncFn('claude-code', { silent: true });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (process.env.DEBUG) console.error(`[session-start] Bridge auto-sync failed: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
await bridgeSyncPromise;
|
|
44
|
+
bootMark('after bridge sync');
|
|
45
|
+
|
|
46
|
+
// wf-b8839d99: Refresh standing no-defer pin from decisions.md policy.
|
|
47
|
+
try {
|
|
48
|
+
const { refreshFromPolicy } = require('./no-defer-policy');
|
|
49
|
+
const r = refreshFromPolicy();
|
|
50
|
+
if (r.refreshed && process.env.DEBUG) {
|
|
51
|
+
console.error(`[session-start] Refreshed no-defer pin from policy: ${r.header}`);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (process.env.DEBUG) console.error(`[session-start] no-defer policy refresh failed: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// CLAUDE.md drift detection
|
|
58
|
+
let driftDetected = false;
|
|
59
|
+
let driftMarkerMissing = false;
|
|
60
|
+
try {
|
|
61
|
+
const { checkClaudeMdDrift } = require('../../flow-bridge-state');
|
|
62
|
+
const drift = checkClaudeMdDrift();
|
|
63
|
+
if (drift.drifted && drift.reason === 'content-changed') {
|
|
64
|
+
if (process.env.DEBUG) console.error('[session-start] CLAUDE.md drift detected — content changed since last sync');
|
|
65
|
+
driftDetected = true;
|
|
66
|
+
} else if (drift.drifted && drift.reason === 'marker-missing') {
|
|
67
|
+
if (process.env.DEBUG) console.error('[session-start] CLAUDE.md appears manually maintained (no generation marker)');
|
|
68
|
+
driftDetected = true;
|
|
69
|
+
driftMarkerMissing = true;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (process.env.DEBUG) console.error(`[session-start] Drift detection failed: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Version compatibility checks (parallelized)
|
|
76
|
+
let versionWarning = null;
|
|
77
|
+
let updateWarning = null;
|
|
78
|
+
await bootTime('version checks', async () => {
|
|
79
|
+
try {
|
|
80
|
+
const { checkClaudeCodeVersionOnce, checkWogiFlowUpdateOnce } = require('../../flow-version-check');
|
|
81
|
+
const [vw, uw] = await Promise.all([
|
|
82
|
+
(async () => { try { return await checkClaudeCodeVersionOnce(); } catch (_err) { return null; } })(),
|
|
83
|
+
(async () => { try { return await checkWogiFlowUpdateOnce(); } catch (_err) { return null; } })()
|
|
84
|
+
]);
|
|
85
|
+
versionWarning = vw;
|
|
86
|
+
updateWarning = uw;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (process.env.DEBUG) console.error(`[session-start] Version check failed: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
bootMark('after version checks');
|
|
92
|
+
|
|
93
|
+
// Batch 1: Independent pre-context operations
|
|
94
|
+
let scriptWarnings = [];
|
|
95
|
+
try {
|
|
96
|
+
const wasReset = checkAndResetStalePhase();
|
|
97
|
+
if (wasReset && process.env.DEBUG) console.error('[session-start] Reset stale workflow phase to idle');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (process.env.DEBUG) console.error(`[session-start] Failed to check stale phase: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
try { resetSessionTaskCounter(); } catch (_err) { /* non-blocking */ }
|
|
102
|
+
try {
|
|
103
|
+
const routingResult = setRoutingPending();
|
|
104
|
+
if (process.env.DEBUG) console.error(`[session-start] Set routing-pending: ${routingResult.reason}`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (process.env.DEBUG) console.error(`[session-start] Failed to set routing-pending: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const { validateScripts } = require('../../flow-script-resolver');
|
|
110
|
+
scriptWarnings = validateScripts();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (process.env.DEBUG) console.error(`[session-start] Script validation failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// BUG-005: Create durable-session.json for active tasks on session start.
|
|
116
|
+
try {
|
|
117
|
+
const { getReadyData } = require('../../flow-utils');
|
|
118
|
+
const readyData = getReadyData();
|
|
119
|
+
if (Array.isArray(readyData.inProgress) && readyData.inProgress.length > 0) {
|
|
120
|
+
const task = readyData.inProgress[0];
|
|
121
|
+
const taskId = task && task.id;
|
|
122
|
+
if (taskId) {
|
|
123
|
+
const { loadDurableSession, createDurableSession } = require('../../flow-durable-session');
|
|
124
|
+
const existing = loadDurableSession();
|
|
125
|
+
if (!existing || existing.taskId !== taskId) {
|
|
126
|
+
const criteria = task.acceptanceCriteria || task.scenarios || [];
|
|
127
|
+
const steps = Array.isArray(criteria) ? criteria : [];
|
|
128
|
+
const sessionSteps = steps.length > 0 ? steps : [task.title || taskId];
|
|
129
|
+
createDurableSession(taskId, 'task', sessionSteps);
|
|
130
|
+
if (process.env.DEBUG) console.error(`[session-start] Created durable session for active task ${taskId}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (process.env.DEBUG) console.error(`[session-start] Durable session init failed: ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Async pre-ops batched with gatherSessionContext
|
|
139
|
+
const asyncPreOps = [];
|
|
140
|
+
if (parsedInput.sessionId) {
|
|
141
|
+
asyncPreOps.push(setCliSessionId(parsedInput.sessionId).catch(err => {
|
|
142
|
+
if (process.env.DEBUG) console.error(`[session-start] Failed to store session ID: ${err.message}`);
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
asyncPreOps.push(clearStaleCurrentTaskAsync().catch(err => {
|
|
146
|
+
if (process.env.DEBUG) console.error(`[session-start] Failed to clear stale task: ${err.message}`);
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
bootMark('before gatherSessionContext + asyncPreOps');
|
|
150
|
+
const [, coreResult] = await Promise.all([
|
|
151
|
+
Promise.all(asyncPreOps),
|
|
152
|
+
bootTime('gatherSessionContext', () => gatherSessionContext({
|
|
153
|
+
includeSuspended: true,
|
|
154
|
+
includeDecisions: true,
|
|
155
|
+
includeActivity: true
|
|
156
|
+
}))
|
|
157
|
+
]);
|
|
158
|
+
bootMark('after gatherSessionContext + asyncPreOps');
|
|
159
|
+
|
|
160
|
+
// Batch 2: Post-context — plugin scan + community pull (parallel, non-blocking)
|
|
161
|
+
await bootTime('postContextOps (plugin-scan + community-pull)', () => Promise.all([
|
|
162
|
+
runPluginAutoScan(coreResult),
|
|
163
|
+
runCommunityPull(coreResult)
|
|
164
|
+
]));
|
|
165
|
+
bootMark('after postContextOps');
|
|
166
|
+
|
|
167
|
+
// Inject warnings into context
|
|
168
|
+
if (scriptWarnings.length > 0 && coreResult?.context) {
|
|
169
|
+
coreResult.context.scriptWarnings = scriptWarnings.map(w => w.message);
|
|
170
|
+
}
|
|
171
|
+
if (versionWarning && coreResult?.context) coreResult.context.versionWarning = versionWarning;
|
|
172
|
+
if (updateWarning && coreResult?.context) coreResult.context.updateWarning = updateWarning;
|
|
173
|
+
if (driftDetected && coreResult?.context) {
|
|
174
|
+
coreResult.context.driftWarning = driftMarkerMissing
|
|
175
|
+
? 'CLAUDE.md appears to have been manually edited (generation marker missing). Was this intentional? If yes, WogiFlow will respect your custom CLAUDE.md. If not, run `flow bridge sync` to regenerate from template.'
|
|
176
|
+
: 'CLAUDE.md content has changed since the last bridge sync. Was this intentional? If yes, WogiFlow will preserve your changes. If not, run `flow bridge sync` to regenerate from template.';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// State file drift detection
|
|
180
|
+
try {
|
|
181
|
+
const { detectDrift, saveSnapshot, formatDriftReport } = require('../../flow-state-drift-detector');
|
|
182
|
+
const driftResult = detectDrift();
|
|
183
|
+
if (driftResult.hasDrift && coreResult?.context) {
|
|
184
|
+
coreResult.context.stateDriftWarning = formatDriftReport(driftResult);
|
|
185
|
+
}
|
|
186
|
+
saveSnapshot();
|
|
187
|
+
} catch (_err) {
|
|
188
|
+
if (process.env.DEBUG) console.error(`[session-start] State drift detection failed: ${_err.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Workspace worker restart-handoff
|
|
192
|
+
await bootTime('worker session-start handler', async () => {
|
|
193
|
+
try {
|
|
194
|
+
const { handleWorkerSessionStart } = require('./session-start-worker');
|
|
195
|
+
const workerResult = handleWorkerSessionStart();
|
|
196
|
+
if (workerResult.context && coreResult?.context) {
|
|
197
|
+
if (workerResult.branch === 'auto-resume') {
|
|
198
|
+
coreResult.context.workerAutoResume = workerResult.context;
|
|
199
|
+
} else if (workerResult.branch === 'announce-ready') {
|
|
200
|
+
coreResult.context.workerReadyAnnounce = workerResult.context;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (process.env.DEBUG) console.error(`[session-start] Worker session-start handler failed: ${err.message}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
bootMark('SessionStart hook returning');
|
|
208
|
+
|
|
209
|
+
return coreResult;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runPluginAutoScan(coreResult) {
|
|
213
|
+
try {
|
|
214
|
+
const config = getConfig();
|
|
215
|
+
if (!config.plugins?.enabled || !config.plugins?.autoScanOnSessionStart) return;
|
|
216
|
+
const { scanUnregisteredMcpServers, registerPlugin, deactivateStaleMcpPlugins, listPlugins } = require('../../flow-plugin-registry');
|
|
217
|
+
|
|
218
|
+
const unregistered = scanUnregisteredMcpServers();
|
|
219
|
+
for (const server of unregistered) {
|
|
220
|
+
registerPlugin({
|
|
221
|
+
name: server.serverName,
|
|
222
|
+
description: `Auto-discovered MCP server: ${server.serverName}`,
|
|
223
|
+
source: 'auto-scan',
|
|
224
|
+
triggers: [`use ${server.serverName}`, `send to ${server.serverName}`, server.serverName],
|
|
225
|
+
capabilities: [],
|
|
226
|
+
metadata: { mcpServer: server.serverName }
|
|
227
|
+
});
|
|
228
|
+
if (process.env.DEBUG) console.error(`[session-start] Auto-registered plugin: ${server.serverName}`);
|
|
229
|
+
}
|
|
230
|
+
const deactivated = deactivateStaleMcpPlugins();
|
|
231
|
+
if (deactivated.length > 0 && process.env.DEBUG) {
|
|
232
|
+
console.error(`[session-start] Deactivated ${deactivated.length} stale plugin(s): ${deactivated.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
if (coreResult?.context) {
|
|
235
|
+
const activePlugins = listPlugins({ activeOnly: true });
|
|
236
|
+
if (unregistered.length > 0 || activePlugins.length > 0) {
|
|
237
|
+
coreResult.context.pluginScan = {
|
|
238
|
+
newlyRegistered: unregistered.map(s => s.serverName),
|
|
239
|
+
activePlugins: activePlugins.map(p => ({ name: p.name, capabilities: (p.capabilities || []).length }))
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (process.env.DEBUG) console.error(`[session-start] Plugin auto-scan failed: ${err.message}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runCommunityPull(coreResult) {
|
|
249
|
+
try {
|
|
250
|
+
const communityConfig = getConfig();
|
|
251
|
+
if (!communityConfig.community?.enabled) return;
|
|
252
|
+
const community = require('../../flow-community');
|
|
253
|
+
community.retryPendingSuggestions(communityConfig).catch(() => {});
|
|
254
|
+
if (communityConfig.community?.pullOnSessionStart !== false) {
|
|
255
|
+
const knowledge = await community.pullFromServer(communityConfig);
|
|
256
|
+
if (knowledge && coreResult?.context) {
|
|
257
|
+
coreResult.context.communityKnowledge = knowledge;
|
|
258
|
+
try { community.mergeCommunityKnowledge(knowledge, communityConfig); }
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (process.env.DEBUG) console.error(`[session-start] Community merge failed: ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (process.env.DEBUG) console.error(`[session-start] Community pull failed: ${err.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = { orchestrateSessionStart };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Stop-Hook Orchestrator (wf-6e31850e A-3)
|
|
5
|
+
*
|
|
6
|
+
* Extracted from scripts/hooks/entry/claude-code/stop.js to bring that entry
|
|
7
|
+
* file under the 120-LOC budget per .claude/rules/architecture/hook-three-layer.md.
|
|
8
|
+
*
|
|
9
|
+
* Same control flow as before; just moved here. Entry stop.js is now a thin
|
|
10
|
+
* pass-through that imports and delegates.
|
|
11
|
+
*
|
|
12
|
+
* Returns the Stop-hook result `{ __raw?, continue?, stopReason?, ... }`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { isRoutingPending, incrementStopAttempts } = require('./routing-gate');
|
|
16
|
+
const { checkLoopExit } = require('./loop-check');
|
|
17
|
+
|
|
18
|
+
async function orchestrateStop({ parsedInput }) {
|
|
19
|
+
const activeGates = {};
|
|
20
|
+
try {
|
|
21
|
+
const { isLongInputPending } = require('./long-input-enforcement');
|
|
22
|
+
activeGates['long-input-pending'] = isLongInputPending();
|
|
23
|
+
} catch (_err) { /* fail-open */ }
|
|
24
|
+
|
|
25
|
+
let recoveryGraceActive = false;
|
|
26
|
+
try {
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const { PATHS } = require('../../flow-utils');
|
|
30
|
+
const gracePath = path.join(PATHS.state, 'routing-recovery-grace.json');
|
|
31
|
+
if (fs.existsSync(gracePath)) {
|
|
32
|
+
const raw = fs.readFileSync(gracePath, 'utf-8');
|
|
33
|
+
const data = JSON.parse(raw);
|
|
34
|
+
if (data?.expiresAt && Date.parse(data.expiresAt) > Date.now()) {
|
|
35
|
+
recoveryGraceActive = true;
|
|
36
|
+
} else {
|
|
37
|
+
try { fs.unlinkSync(gracePath); } catch (_err) { /* fine */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (_err) { /* fail-open */ }
|
|
41
|
+
|
|
42
|
+
let orchestratorTopGate = null;
|
|
43
|
+
try {
|
|
44
|
+
const { pickStopHookGate } = require('./gate-orchestrator');
|
|
45
|
+
// wf-740f47e4 (NULL-CHECK): guard against malformed return shape.
|
|
46
|
+
const result = pickStopHookGate({
|
|
47
|
+
'long-input-pending': activeGates['long-input-pending'] === true
|
|
48
|
+
});
|
|
49
|
+
orchestratorTopGate = (result && typeof result === 'object' && typeof result.topGateId === 'string')
|
|
50
|
+
? result.topGateId
|
|
51
|
+
: null;
|
|
52
|
+
} catch (_err) { /* fail-open */ }
|
|
53
|
+
const longInputActive = orchestratorTopGate === 'long-input-pending';
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (isRoutingPending() && !longInputActive && !recoveryGraceActive) {
|
|
57
|
+
const { cleared, attempts } = incrementStopAttempts(10);
|
|
58
|
+
if (cleared) {
|
|
59
|
+
if (process.env.DEBUG) {
|
|
60
|
+
console.error(`[Stop] Max routing enforcement attempts reached (${attempts}), surfacing to user`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
__raw: true,
|
|
64
|
+
continue: false,
|
|
65
|
+
stopReason: `ROUTING VIOLATION (unrecoverable): max ${attempts} attempts exceeded. The AI failed to call Skill(skill="wogi-start") after ${attempts} stop attempts. Manual review required — this may indicate a stuck routing flag or a session that bypassed /wogi-start through context compaction. To unstick: invoke /wogi-start manually, or check .workflow/state/routing-pending.json.`
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
return {
|
|
69
|
+
__raw: true,
|
|
70
|
+
continue: true,
|
|
71
|
+
stopReason: [
|
|
72
|
+
`ROUTING VIOLATION (attempt ${attempts}/10): You MUST call Skill(skill="wogi-start") before responding.`,
|
|
73
|
+
'',
|
|
74
|
+
'Call Skill(skill="wogi-start", args="<user\'s message>") NOW. No text. No explanation. Just the Skill tool call.'
|
|
75
|
+
].join('\n')
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (process.env.DEBUG) {
|
|
81
|
+
console.error(`[Stop] Routing check error (fail-closed, forcing continue): ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
__raw: true,
|
|
85
|
+
continue: true,
|
|
86
|
+
stopReason: 'Routing enforcement check encountered an error. Please invoke /wogi-start with your request.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const workspaceNotify = require('./workspace-stop-notify');
|
|
91
|
+
await workspaceNotify.notifyWorkerStopped();
|
|
92
|
+
|
|
93
|
+
const restartCoordinator = require('./task-boundary-restart-coordinator');
|
|
94
|
+
const restartResult = await restartCoordinator.handleTaskBoundaryRestart({ parsedInput });
|
|
95
|
+
if (restartResult?.shouldReturn) return restartResult.result;
|
|
96
|
+
|
|
97
|
+
// Research-Required Stop-Hook Gate
|
|
98
|
+
try {
|
|
99
|
+
if (longInputActive) {
|
|
100
|
+
// skip — defer to long-input remediation
|
|
101
|
+
} else {
|
|
102
|
+
const { checkResearchRequiredGate } = require('./research-required-gate');
|
|
103
|
+
const { getConfig } = require('../../flow-utils');
|
|
104
|
+
const config = getConfig();
|
|
105
|
+
const result = checkResearchRequiredGate({
|
|
106
|
+
transcriptPath: parsedInput?.transcriptPath,
|
|
107
|
+
config
|
|
108
|
+
});
|
|
109
|
+
if (result.blocked) {
|
|
110
|
+
if (result.hardStop) return { __raw: true, continue: false, stopReason: result.message };
|
|
111
|
+
return { __raw: true, continue: true, stopReason: result.message };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (process.env.DEBUG) console.error(`[Stop] Research-required gate error (fail-open): ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Workspace + worker gates
|
|
119
|
+
const workspaceGates = require('./workspace-stop-gates');
|
|
120
|
+
const wsResult = await workspaceGates.checkWorkspaceStopGates({ parsedInput });
|
|
121
|
+
if (wsResult?.shouldReturn) return wsResult.result;
|
|
122
|
+
|
|
123
|
+
return await checkLoopExit();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { orchestrateStop };
|
|
@@ -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 };
|