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,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
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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' });
|