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,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 };
@@ -1,21 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Wogi Flow - Claude Code SessionStart Hook
4
+ * Wogi Flow - Claude Code SessionStart Hook (thin entry)
5
5
  *
6
- * Called when a Claude Code session starts.
7
- * Injects context (suspended tasks, decisions, recent activity).
6
+ * All SessionStart business logic lives in
7
+ * scripts/hooks/core/session-start-orchestrator.js. This entry dispatches.
8
+ *
9
+ * Per .claude/rules/architecture/hook-three-layer.md: entry files ≤ 120 LOC,
10
+ * ≤ 2 core/ imports, no inline business logic. wf-6e31850e A-3 extracted
11
+ * the prior 387-LOC body into core/session-start-orchestrator.js.
12
+ *
13
+ * Boot-latency instrumentation (env-guarded, no effect unless WOGI_DEBUG_BOOT=1)
14
+ * stays here because it wraps the call.
8
15
  */
9
16
 
10
- const { gatherSessionContext } = require('../../core/session-context');
11
- const { setCliSessionId, clearStaleCurrentTaskAsync, resetSessionTaskCounter } = require('../../../flow-session-state');
12
- const { checkAndResetStalePhase } = require('../../core/phase-gate');
13
- const { setRoutingPending } = require('../../core/routing-gate');
14
- const { getConfig } = require('../../../flow-utils');
17
+ const { orchestrateSessionStart } = require('../../core/session-start-orchestrator');
15
18
  const { runHook } = require('../shared/hook-runner');
16
19
 
17
- // wf-8294d960: env-guarded boot-latency instrumentation. No effect unless WOGI_DEBUG_BOOT=1.
18
- // Claude Code suppresses hook process stderr — we write to an append-only log file instead.
20
+ // wf-8294d960: env-guarded boot-latency instrumentation.
19
21
  const BOOT_DEBUG = process.env.WOGI_DEBUG_BOOT === '1';
20
22
  const _bootT0 = BOOT_DEBUG ? Date.now() : 0;
21
23
  const _bootLogFile = BOOT_DEBUG
@@ -34,354 +36,19 @@ function _bootWrite(line) {
34
36
  }
35
37
  function _bootMark(label) {
36
38
  if (!BOOT_DEBUG) return;
37
- const ms = Date.now() - _bootT0;
38
- _bootWrite(`[boot-latency] +${String(ms).padStart(6)}ms ${label}`);
39
+ _bootWrite(`[boot-latency] +${String(Date.now() - _bootT0).padStart(6)}ms ${label}`);
39
40
  }
40
41
  async function _bootTime(label, fn) {
41
42
  if (!BOOT_DEBUG) return fn();
42
43
  const t = Date.now();
43
- try {
44
- return await fn();
45
- } finally {
46
- _bootWrite(`[boot-latency] (${String(Date.now() - t).padStart(6)}ms) ${label}`);
47
- }
48
- }
49
-
50
- // Lazy-load bridge state to avoid circular dependencies
51
- let autoSyncBridge = null;
52
- function getAutoSyncBridge() {
53
- if (!autoSyncBridge) {
54
- try {
55
- autoSyncBridge = require('../../../flow-bridge-state').autoSyncBridge;
56
- } catch (_err) {
57
- autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
58
- }
59
- }
60
- return autoSyncBridge;
44
+ try { return await fn(); }
45
+ finally { _bootWrite(`[boot-latency] (${String(Date.now() - t).padStart(6)}ms) ${label}`); }
61
46
  }
62
47
 
63
48
  runHook('SessionStart', async ({ parsedInput }) => {
64
- _bootMark('SessionStart hook entered');
65
- // Start bridge auto-sync in parallel with other init work
66
- const bridgeSyncPromise = _bootTime('bridge auto-sync', async () => {
67
- try {
68
- const syncFn = getAutoSyncBridge();
69
- await syncFn('claude-code', { silent: true });
70
- } catch (err) {
71
- if (process.env.DEBUG) {
72
- console.error(`[session-start] Bridge auto-sync failed: ${err.message}`);
73
- }
74
- }
49
+ return await orchestrateSessionStart({
50
+ parsedInput,
51
+ bootMark: _bootMark,
52
+ bootTime: _bootTime
75
53
  });
76
-
77
- // Wait for bridge sync to complete
78
- await bridgeSyncPromise;
79
- _bootMark('after bridge sync');
80
-
81
- // wf-b8839d99: Refresh standing no-defer pin from decisions.md if a policy
82
- // section is present. Fail-open — never blocks session start.
83
- try {
84
- const { refreshFromPolicy } = require('../../core/no-defer-policy');
85
- const r = refreshFromPolicy();
86
- if (r.refreshed && process.env.DEBUG) {
87
- console.error(`[session-start] Refreshed no-defer pin from policy: ${r.header}`);
88
- }
89
- } catch (err) {
90
- if (process.env.DEBUG) {
91
- console.error(`[session-start] no-defer policy refresh failed: ${err.message}`);
92
- }
93
- }
94
-
95
- // CLAUDE.md drift detection — check if manually edited since last sync
96
- let driftDetected = false;
97
- let driftMarkerMissing = false;
98
- try {
99
- const { checkClaudeMdDrift } = require('../../../flow-bridge-state');
100
- const drift = checkClaudeMdDrift();
101
- if (drift.drifted && drift.reason === 'content-changed') {
102
- if (process.env.DEBUG) {
103
- console.error('[session-start] CLAUDE.md drift detected — content changed since last sync');
104
- }
105
- driftDetected = true;
106
- } else if (drift.drifted && drift.reason === 'marker-missing') {
107
- if (process.env.DEBUG) {
108
- console.error('[session-start] CLAUDE.md appears manually maintained (no generation marker)');
109
- }
110
- driftDetected = true;
111
- driftMarkerMissing = true;
112
- }
113
- } catch (err) {
114
- if (process.env.DEBUG) {
115
- console.error(`[session-start] Drift detection failed: ${err.message}`);
116
- }
117
- }
118
-
119
- // --- Version compatibility checks (parallelized) ---
120
- let versionWarning = null;
121
- let updateWarning = null;
122
- await _bootTime('version checks', async () => {
123
- try {
124
- const { checkClaudeCodeVersionOnce, checkWogiFlowUpdateOnce } = require('../../../flow-version-check');
125
- const [vw, uw] = await Promise.all([
126
- (async () => { try { return await checkClaudeCodeVersionOnce(); } catch (_err) { return null; } })(),
127
- (async () => { try { return await checkWogiFlowUpdateOnce(); } catch (_err) { return null; } })()
128
- ]);
129
- versionWarning = vw;
130
- updateWarning = uw;
131
- } catch (err) {
132
- if (process.env.DEBUG) {
133
- console.error(`[session-start] Version check failed: ${err.message}`);
134
- }
135
- }
136
- });
137
- _bootMark('after version checks');
138
-
139
- // --- Batch 1: Independent pre-context operations (async + sync) ---
140
- let scriptWarnings = [];
141
- try {
142
- const wasReset = checkAndResetStalePhase();
143
- if (wasReset && process.env.DEBUG) {
144
- console.error('[session-start] Reset stale workflow phase to idle');
145
- }
146
- } catch (err) {
147
- if (process.env.DEBUG) {
148
- console.error(`[session-start] Failed to check stale phase: ${err.message}`);
149
- }
150
- }
151
-
152
- // Reset session task counter so first task uses full prompt
153
- try {
154
- resetSessionTaskCounter();
155
- } catch (_err) {
156
- // Non-blocking
157
- }
158
-
159
- try {
160
- const routingResult = setRoutingPending();
161
- if (process.env.DEBUG) {
162
- console.error(`[session-start] Set routing-pending: ${routingResult.reason}`);
163
- }
164
- } catch (err) {
165
- if (process.env.DEBUG) {
166
- console.error(`[session-start] Failed to set routing-pending: ${err.message}`);
167
- }
168
- }
169
-
170
- try {
171
- const { validateScripts } = require('../../../flow-script-resolver');
172
- scriptWarnings = validateScripts();
173
- } catch (err) {
174
- if (process.env.DEBUG) {
175
- console.error(`[session-start] Script validation failed: ${err.message}`);
176
- }
177
- }
178
-
179
- // BUG-005 fix: Create durable-session.json for active tasks on session start.
180
- try {
181
- const { getReadyData } = require('../../../flow-utils');
182
- const readyData = getReadyData();
183
- if (Array.isArray(readyData.inProgress) && readyData.inProgress.length > 0) {
184
- const task = readyData.inProgress[0];
185
- const taskId = task && task.id;
186
- if (taskId) {
187
- const { loadDurableSession, createDurableSession } = require('../../../flow-durable-session');
188
- const existing = loadDurableSession();
189
- if (!existing || existing.taskId !== taskId) {
190
- const criteria = task.acceptanceCriteria || task.scenarios || [];
191
- const steps = Array.isArray(criteria) ? criteria : [];
192
- const sessionSteps = steps.length > 0 ? steps : [task.title || taskId];
193
- createDurableSession(taskId, 'task', sessionSteps);
194
- if (process.env.DEBUG) {
195
- console.error(`[session-start] Created durable session for active task ${taskId}`);
196
- }
197
- }
198
- }
199
- }
200
- } catch (err) {
201
- if (process.env.DEBUG) {
202
- console.error(`[session-start] Durable session init failed: ${err.message}`);
203
- }
204
- }
205
-
206
- // Async operations — batch with Promise.all
207
- const asyncPreOps = [];
208
-
209
- if (parsedInput.sessionId) {
210
- asyncPreOps.push(
211
- setCliSessionId(parsedInput.sessionId).catch(err => {
212
- if (process.env.DEBUG) {
213
- console.error(`[session-start] Failed to store session ID: ${err.message}`);
214
- }
215
- })
216
- );
217
- }
218
-
219
- asyncPreOps.push(
220
- clearStaleCurrentTaskAsync().catch(err => {
221
- if (process.env.DEBUG) {
222
- console.error(`[session-start] Failed to clear stale task: ${err.message}`);
223
- }
224
- })
225
- );
226
-
227
- // Gather session context concurrently with the async pre-ops
228
- _bootMark('before gatherSessionContext + asyncPreOps');
229
- const [, coreResult] = await Promise.all([
230
- Promise.all(asyncPreOps),
231
- _bootTime('gatherSessionContext', () => gatherSessionContext({
232
- includeSuspended: true,
233
- includeDecisions: true,
234
- includeActivity: true
235
- }))
236
- ]);
237
- _bootMark('after gatherSessionContext + asyncPreOps');
238
-
239
- // --- Batch 2: Post-context operations (plugin scan + community pull) ---
240
- const postContextOps = [];
241
-
242
- // Plugin auto-scan (non-blocking)
243
- postContextOps.push((async () => {
244
- try {
245
- const config = getConfig();
246
- if (config.plugins?.enabled && config.plugins?.autoScanOnSessionStart) {
247
- const { scanUnregisteredMcpServers, registerPlugin, deactivateStaleMcpPlugins, listPlugins } = require('../../../flow-plugin-registry');
248
-
249
- const unregistered = scanUnregisteredMcpServers();
250
- for (const server of unregistered) {
251
- registerPlugin({
252
- name: server.serverName,
253
- description: `Auto-discovered MCP server: ${server.serverName}`,
254
- source: 'auto-scan',
255
- triggers: [`use ${server.serverName}`, `send to ${server.serverName}`, server.serverName],
256
- capabilities: [],
257
- metadata: { mcpServer: server.serverName }
258
- });
259
- if (process.env.DEBUG) {
260
- console.error(`[session-start] Auto-registered plugin: ${server.serverName}`);
261
- }
262
- }
263
-
264
- const deactivated = deactivateStaleMcpPlugins();
265
- if (deactivated.length > 0 && process.env.DEBUG) {
266
- console.error(`[session-start] Deactivated ${deactivated.length} stale plugin(s): ${deactivated.join(', ')}`);
267
- }
268
-
269
- if (coreResult && coreResult.context) {
270
- const activePlugins = listPlugins({ activeOnly: true });
271
- if (unregistered.length > 0 || activePlugins.length > 0) {
272
- coreResult.context.pluginScan = {
273
- newlyRegistered: unregistered.map(s => s.serverName),
274
- activePlugins: activePlugins.map(p => ({ name: p.name, capabilities: (p.capabilities || []).length }))
275
- };
276
- }
277
- }
278
- }
279
- } catch (err) {
280
- if (process.env.DEBUG) {
281
- console.error(`[session-start] Plugin auto-scan failed: ${err.message}`);
282
- }
283
- }
284
- })());
285
-
286
- // Community knowledge pull + suggestion retry (non-blocking)
287
- postContextOps.push((async () => {
288
- try {
289
- const communityConfig = getConfig();
290
- if (communityConfig.community?.enabled) {
291
- const community = require('../../../flow-community');
292
-
293
- community.retryPendingSuggestions(communityConfig).catch(() => {});
294
-
295
- if (communityConfig.community?.pullOnSessionStart !== false) {
296
- const knowledge = await community.pullFromServer(communityConfig);
297
- if (knowledge && coreResult && coreResult.context) {
298
- coreResult.context.communityKnowledge = knowledge;
299
-
300
- try {
301
- community.mergeCommunityKnowledge(knowledge, communityConfig);
302
- } catch (err) {
303
- if (process.env.DEBUG) {
304
- console.error(`[session-start] Community merge failed: ${err.message}`);
305
- }
306
- }
307
- }
308
- }
309
- }
310
- } catch (err) {
311
- if (process.env.DEBUG) {
312
- console.error(`[session-start] Community pull failed: ${err.message}`);
313
- }
314
- }
315
- })());
316
-
317
- await _bootTime('postContextOps (plugin-scan + community-pull)', () => Promise.all(postContextOps));
318
- _bootMark('after postContextOps');
319
-
320
- // Inject script warnings into context (if any)
321
- if (scriptWarnings.length > 0 && coreResult && coreResult.context) {
322
- coreResult.context.scriptWarnings = scriptWarnings.map(w => w.message);
323
- }
324
-
325
- // Inject version compatibility warning (if any)
326
- if (versionWarning && coreResult && coreResult.context) {
327
- coreResult.context.versionWarning = versionWarning;
328
- }
329
-
330
- // Inject WogiFlow update warning (if any)
331
- if (updateWarning && coreResult && coreResult.context) {
332
- coreResult.context.updateWarning = updateWarning;
333
- }
334
-
335
- // Inject drift detection results (if any)
336
- if (driftDetected && coreResult && coreResult.context) {
337
- if (driftMarkerMissing) {
338
- coreResult.context.driftWarning = '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.';
339
- } else {
340
- coreResult.context.driftWarning = '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.';
341
- }
342
- }
343
-
344
- // State file drift detection (Claude Code 2.1.105+ — also works on older versions)
345
- // Detects when .workflow/state/ files were modified externally between sessions
346
- try {
347
- const { detectDrift, saveSnapshot, formatDriftReport } = require('../../../flow-state-drift-detector');
348
- const driftResult = detectDrift();
349
- if (driftResult.hasDrift && coreResult && coreResult.context) {
350
- coreResult.context.stateDriftWarning = formatDriftReport(driftResult);
351
- }
352
- // Always save a fresh snapshot at session start for next comparison
353
- saveSnapshot();
354
- } catch (_err) {
355
- // State drift detection failure is non-fatal
356
- if (process.env.DEBUG) {
357
- console.error(`[session-start] State drift detection failed: ${_err.message}`);
358
- }
359
- }
360
-
361
- // Workspace worker restart-handoff (wf-restart-handoff / 2.22.2).
362
- // When the wogi-claude wrapper restarts a worker (via task-boundary-reset),
363
- // queued dispatches from the PRIOR session are picked up by auto-resume;
364
- // if the queue is truly empty, announce worker-ready so the manager can
365
- // reconcile against its dispatch log and re-dispatch anything lost during
366
- // the restart window. See scripts/hooks/core/session-start-worker.js.
367
- await _bootTime('worker session-start handler', async () => {
368
- try {
369
- const { handleWorkerSessionStart } = require('../../core/session-start-worker');
370
- const workerResult = handleWorkerSessionStart();
371
- if (workerResult.context && coreResult && coreResult.context) {
372
- if (workerResult.branch === 'auto-resume') {
373
- coreResult.context.workerAutoResume = workerResult.context;
374
- } else if (workerResult.branch === 'announce-ready') {
375
- coreResult.context.workerReadyAnnounce = workerResult.context;
376
- }
377
- }
378
- } catch (err) {
379
- if (process.env.DEBUG) {
380
- console.error(`[session-start] Worker session-start handler failed: ${err.message}`);
381
- }
382
- }
383
- });
384
- _bootMark('SessionStart hook returning');
385
-
386
- return coreResult;
387
54
  }, { failMode: 'warn' });