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