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.
@@ -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 };