wogiflow 2.22.0 → 2.22.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/commands/wogi-start.md +3 -3
- package/.claude/commands/wogi-story.md +27 -0
- package/.claude/docs/claude-code-compatibility.md +32 -1
- package/lib/workspace-dispatch-tracking.js +175 -0
- package/lib/workspace-messages.js +2 -0
- package/lib/workspace-routing.js +17 -0
- package/lib/workspace-worker-ready.js +190 -0
- package/package.json +2 -2
- package/scripts/flow-config-defaults.js +9 -0
- package/scripts/flow-story-gates.js +504 -0
- package/scripts/flow-story.js +205 -7
- package/scripts/hooks/adapters/claude-code.js +18 -37
- package/scripts/hooks/core/overdue-dispatches.js +291 -0
- package/scripts/hooks/core/session-context.js +17 -0
- package/scripts/hooks/core/session-start-worker.js +114 -0
- package/scripts/hooks/entry/claude-code/session-start.js +22 -0
- package/scripts/hooks/entry/claude-code/stop.js +92 -47
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +18 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wogi Flow — Worker SessionStart handler (wf-restart-handoff)
|
|
3
|
+
*
|
|
4
|
+
* Handles the "worker just started" branch of SessionStart:
|
|
5
|
+
*
|
|
6
|
+
* - If worker has queued channel dispatches in ready.json:
|
|
7
|
+
* inject additionalContext telling the model to invoke
|
|
8
|
+
* /wogi-start <nextId> now. Mirrors the existing Stop-hook
|
|
9
|
+
* autopickup flow (task-completed.js::buildAutoPickupContext)
|
|
10
|
+
* but fires at session boundary instead of turn boundary —
|
|
11
|
+
* necessary because restart (wf-d3e67abe/2.22.1) kills the
|
|
12
|
+
* previous claude and the Stop-hook autopickup no longer bridges
|
|
13
|
+
* between tasks.
|
|
14
|
+
*
|
|
15
|
+
* - Else if worker has zero in-progress + zero queued channel
|
|
16
|
+
* dispatches: write a `worker-ready` message to the workspace
|
|
17
|
+
* message bus so the manager can reconcile against its durable
|
|
18
|
+
* dispatched-tasks.json and re-dispatch any work lost during
|
|
19
|
+
* the restart window.
|
|
20
|
+
*
|
|
21
|
+
* Returns a context fragment (or null) that the SessionStart entry
|
|
22
|
+
* merges into the overall hook output.
|
|
23
|
+
*
|
|
24
|
+
* Fail-open: any error returns null and logs in DEBUG mode. Never
|
|
25
|
+
* blocks session startup.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const WORKER_READY_LIB = path.join(__dirname, '..', '..', '..', 'lib', 'workspace-worker-ready.js');
|
|
31
|
+
const TASK_COMPLETED_CORE = path.join(__dirname, 'task-completed.js');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handle worker SessionStart.
|
|
35
|
+
*
|
|
36
|
+
* @returns {{branch: 'auto-resume'|'announce-ready'|'skip', context?: string, announced?: Object, pickup?: Object}}
|
|
37
|
+
*/
|
|
38
|
+
function handleWorkerSessionStart() {
|
|
39
|
+
try {
|
|
40
|
+
const { isWorker, shouldAnnounceReady, announceWorkerReady } = require(WORKER_READY_LIB);
|
|
41
|
+
if (!isWorker()) return { branch: 'skip', reason: 'not-worker' };
|
|
42
|
+
|
|
43
|
+
// Check for queued work first — if any, tell the model to pick it up
|
|
44
|
+
// instead of announcing idle readiness.
|
|
45
|
+
let pickup;
|
|
46
|
+
try {
|
|
47
|
+
const { findQueuedChannelDispatches, buildAutoPickupContext } = require(TASK_COMPLETED_CORE);
|
|
48
|
+
pickup = findQueuedChannelDispatches();
|
|
49
|
+
if (pickup && pickup.count > 0 && pickup.nextTaskId) {
|
|
50
|
+
const base = buildAutoPickupContext(pickup);
|
|
51
|
+
// Adjust the leading line for session-start context — the canonical
|
|
52
|
+
// pickup message starts with "You just completed a task." which isn't
|
|
53
|
+
// true on session start. Re-frame it here.
|
|
54
|
+
const context = [
|
|
55
|
+
`⚡ WORKSPACE SESSION START — ${pickup.count} CHANNEL DISPATCH${pickup.count === 1 ? '' : 'ES'} QUEUED`,
|
|
56
|
+
'',
|
|
57
|
+
`This fresh worker session has ${pickup.count} channel-dispatched task${pickup.count === 1 ? '' : 's'} queued in ready.json.`,
|
|
58
|
+
`The previous session restarted cleanly (wogi-claude wrapper). Pick up the next task now.`,
|
|
59
|
+
'',
|
|
60
|
+
`Next: ${pickup.nextTaskId} — ${pickup.nextTaskTitle || '(no title)'}`,
|
|
61
|
+
'',
|
|
62
|
+
'AUTONOMOUS MODE CONTRACT (workspace worker):',
|
|
63
|
+
' • These dispatches are pre-approved by the manager.',
|
|
64
|
+
' • You MUST start the next one IMMEDIATELY in this same turn.',
|
|
65
|
+
' • Do NOT hedge ("awaiting signal", "let me know"). Forbidden.',
|
|
66
|
+
'',
|
|
67
|
+
`ACT NOW: Invoke Skill(skill="wogi-start", args="${pickup.nextTaskId}")`
|
|
68
|
+
].join('\n');
|
|
69
|
+
// base included for logging/telemetry parity if we ever want to diff them
|
|
70
|
+
void base;
|
|
71
|
+
return { branch: 'auto-resume', context, pickup };
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (process.env.DEBUG) {
|
|
75
|
+
console.error(`[session-start-worker] pickup-check failed (fail-open): ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// No queued work — announce readiness so the manager can reconcile.
|
|
80
|
+
const decision = shouldAnnounceReady();
|
|
81
|
+
if (!decision.announce) {
|
|
82
|
+
return { branch: 'skip', reason: decision.reason };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const announced = announceWorkerReady(decision.workspaceRoot, decision.repoName);
|
|
86
|
+
if (!announced.written) {
|
|
87
|
+
if (process.env.DEBUG) {
|
|
88
|
+
console.error(`[session-start-worker] announce failed: ${announced.reason}`);
|
|
89
|
+
}
|
|
90
|
+
return { branch: 'skip', reason: announced.reason };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Optional context surface — not strictly needed since the manager
|
|
94
|
+
// handles reconciliation asynchronously, but a one-line note helps
|
|
95
|
+
// humans reading worker transcripts understand why the worker is idle.
|
|
96
|
+
const context = [
|
|
97
|
+
`Worker session started with empty queue.`,
|
|
98
|
+
`Announced readiness to manager (msg ${announced.messageId}) —`,
|
|
99
|
+
`manager will reconcile against its dispatch log and re-dispatch`,
|
|
100
|
+
`any work lost during the restart window.`
|
|
101
|
+
].join(' ');
|
|
102
|
+
|
|
103
|
+
return { branch: 'announce-ready', context, announced };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (process.env.DEBUG) {
|
|
106
|
+
console.error(`[session-start-worker] unexpected error (fail-open): ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
return { branch: 'skip', reason: `error: ${err.message}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
handleWorkerSessionStart
|
|
114
|
+
};
|
|
@@ -303,5 +303,27 @@ runHook('SessionStart', async ({ parsedInput }) => {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// Workspace worker restart-handoff (wf-restart-handoff / 2.22.2).
|
|
307
|
+
// When the wogi-claude wrapper restarts a worker (via task-boundary-reset),
|
|
308
|
+
// queued dispatches from the PRIOR session are picked up by auto-resume;
|
|
309
|
+
// if the queue is truly empty, announce worker-ready so the manager can
|
|
310
|
+
// reconcile against its dispatch log and re-dispatch anything lost during
|
|
311
|
+
// the restart window. See scripts/hooks/core/session-start-worker.js.
|
|
312
|
+
try {
|
|
313
|
+
const { handleWorkerSessionStart } = require('../../core/session-start-worker');
|
|
314
|
+
const workerResult = handleWorkerSessionStart();
|
|
315
|
+
if (workerResult.context && coreResult && coreResult.context) {
|
|
316
|
+
if (workerResult.branch === 'auto-resume') {
|
|
317
|
+
coreResult.context.workerAutoResume = workerResult.context;
|
|
318
|
+
} else if (workerResult.branch === 'announce-ready') {
|
|
319
|
+
coreResult.context.workerReadyAnnounce = workerResult.context;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (process.env.DEBUG) {
|
|
324
|
+
console.error(`[session-start] Worker session-start handler failed: ${err.message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
306
328
|
return coreResult;
|
|
307
329
|
}, { failMode: 'warn' });
|
|
@@ -61,57 +61,84 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Workspace worker:
|
|
65
|
-
//
|
|
66
|
-
|
|
64
|
+
// Workspace worker: write a structured `worker-stopped` message to the
|
|
65
|
+
// workspace message bus when stopping. This is the graceful-stop half of
|
|
66
|
+
// silent-halt detection (wf-d3e67abe) — the manager's overdue check uses
|
|
67
|
+
// this (vs. task-complete vs. nothing) to tell "finished" from "gave up
|
|
68
|
+
// gracefully" from "died silently".
|
|
69
|
+
//
|
|
70
|
+
// Replaces the previous plain-text curl POST to the manager channel — that
|
|
71
|
+
// was fire-and-forget with no structure, so manager-side reconciliation
|
|
72
|
+
// couldn't distinguish graceful stops from silent deaths.
|
|
73
|
+
if (process.env.WOGI_REPO_NAME && process.env.WOGI_REPO_NAME !== 'manager') {
|
|
67
74
|
try {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
75
|
+
const nodePath = require('node:path');
|
|
76
|
+
const childProcess = require('node:child_process');
|
|
70
77
|
const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
71
78
|
const repoName = process.env.WOGI_REPO_NAME;
|
|
72
|
-
const managerPort = parseInt(process.env.WOGI_MANAGER_PORT, 10);
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error(`Invalid WOGI_REPO_NAME or WOGI_MANAGER_PORT`);
|
|
80
|
+
if (!VALID_NAME.test(repoName)) {
|
|
81
|
+
throw new Error(`Invalid WOGI_REPO_NAME`);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
|
|
85
|
+
if (workspaceRoot) {
|
|
86
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
87
|
+
const ready = safeJsonParse(nodePath.join(PATHS.state, 'ready.json'), {});
|
|
88
|
+
const recentTask = (ready.recentlyCompleted || [])[0];
|
|
89
|
+
const inProgressTask = (ready.inProgress || [])[0];
|
|
90
|
+
const mostRecent = recentTask || inProgressTask;
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
// Determine worker state at stop-time
|
|
93
|
+
const hasInProgress = Boolean(inProgressTask);
|
|
94
|
+
const state = hasInProgress ? 'mid-work' : 'idle';
|
|
95
|
+
const taskInProgress = hasInProgress ? inProgressTask.id : null;
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
// Best-effort lastSha
|
|
98
|
+
let lastSha = null;
|
|
99
|
+
try {
|
|
100
|
+
lastSha = childProcess.execSync('git rev-parse --short HEAD 2>/dev/null || true', {
|
|
101
|
+
cwd: PATHS.root,
|
|
102
|
+
encoding: 'utf-8',
|
|
103
|
+
timeout: 2000
|
|
104
|
+
}).trim() || null;
|
|
105
|
+
} catch (_err) { /* non-critical */ }
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
// Build structured message and persist via the workspace message bus.
|
|
108
|
+
// The worker-stopped type was added to MESSAGE_TYPES in
|
|
109
|
+
// workspace-messages.js (wf-d3e67abe).
|
|
110
|
+
try {
|
|
111
|
+
const libMessages = nodePath.resolve(__dirname, '..', '..', '..', '..', 'lib', 'workspace-messages');
|
|
112
|
+
const { createMessage, saveMessage } = require(libMessages);
|
|
113
|
+
const msg = createMessage({
|
|
114
|
+
from: repoName,
|
|
115
|
+
to: 'manager',
|
|
116
|
+
type: 'worker-stopped',
|
|
117
|
+
subject: hasInProgress
|
|
118
|
+
? `Worker stopped mid-work on ${taskInProgress}`
|
|
119
|
+
: `Worker stopped (idle)`,
|
|
120
|
+
body: [
|
|
121
|
+
`Worker "${repoName}" is stopping.`,
|
|
122
|
+
`State: ${state}`,
|
|
123
|
+
taskInProgress ? `Task in progress: ${taskInProgress}` : null,
|
|
124
|
+
mostRecent?.title ? `Most recent task: ${mostRecent.title}` : null,
|
|
125
|
+
lastSha ? `Last commit: ${lastSha}` : null
|
|
126
|
+
].filter(Boolean).join('\n'),
|
|
127
|
+
priority: hasInProgress ? 'high' : 'medium',
|
|
128
|
+
actionRequired: hasInProgress
|
|
129
|
+
});
|
|
130
|
+
// Attach structured fields the manager-side reconciler consumes.
|
|
131
|
+
msg.taskId = taskInProgress;
|
|
132
|
+
msg.reason = 'graceful';
|
|
133
|
+
msg.state = state;
|
|
134
|
+
msg.taskInProgress = taskInProgress;
|
|
135
|
+
msg.lastSha = lastSha;
|
|
136
|
+
saveMessage(workspaceRoot, msg);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (process.env.DEBUG) {
|
|
139
|
+
console.error(`[Stop] Workspace message write failed: ${err.message}`);
|
|
140
|
+
}
|
|
99
141
|
}
|
|
100
|
-
} catch (_err) { /* non-critical */ }
|
|
101
|
-
|
|
102
|
-
const body = summaryParts.join('\n') || `Work completed by ${repoName}.`;
|
|
103
|
-
|
|
104
|
-
// execFileSync with array args — no shell interpretation (finding-001 fix)
|
|
105
|
-
try {
|
|
106
|
-
execFileSync('curl', [
|
|
107
|
-
'-s', '-X', 'POST',
|
|
108
|
-
`http://127.0.0.1:${managerPort}`,
|
|
109
|
-
'-H', 'Content-Type: text/plain',
|
|
110
|
-
'-H', `X-Wogi-From: ${repoName}`,
|
|
111
|
-
'--data-binary', '@-'
|
|
112
|
-
], { input: body, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
113
|
-
} catch (_err) {
|
|
114
|
-
// Manager might be offline — that's OK
|
|
115
142
|
}
|
|
116
143
|
} catch (err) {
|
|
117
144
|
if (process.env.DEBUG) {
|
|
@@ -168,14 +195,32 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
const restartResult = consumeAndTriggerRestart();
|
|
171
|
-
if (restartResult.triggered
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
if (restartResult.triggered) {
|
|
199
|
+
if (process.env.DEBUG) {
|
|
200
|
+
console.error(`[Stop] Task-boundary restart triggered — claude will exit, wrapper will relaunch`);
|
|
201
|
+
}
|
|
202
|
+
// CRITICAL: return NOW, short-circuiting subsequent stop-blocking gates.
|
|
203
|
+
//
|
|
204
|
+
// Before this fix (observed 2026-04-17): Phase 2 would SIGTERM claude and
|
|
205
|
+
// write the restart flag, then fall through to the workspace autopickup
|
|
206
|
+
// gate (lines below). For a worker with queued dispatches (the common
|
|
207
|
+
// case), that gate returns `{ continue: true, stopReason: ... }` which
|
|
208
|
+
// Claude Code honours as "don't stop, pick up next dispatch." Result: the
|
|
209
|
+
// SIGTERM + restart flag became a no-op because claude was told to keep
|
|
210
|
+
// running in the SAME session. Symptom: single claude PID survives across
|
|
211
|
+
// N tasks, context accumulates, tokens burn — exactly the complaint this
|
|
212
|
+
// feature was supposed to solve.
|
|
213
|
+
//
|
|
214
|
+
// The restart is our stop path. The next session's SessionStart hook will
|
|
215
|
+
// inject queued-dispatch context, so the worker picks up the next task
|
|
216
|
+
// on RESTART rather than via the autopickup gate's continue-override.
|
|
217
|
+
// __raw skips the adapter transform — we want the literal {continue:false}
|
|
218
|
+
// wire format to reach claude unchanged.
|
|
219
|
+
return { __raw: true, continue: false };
|
|
220
|
+
}
|
|
221
|
+
if (restartResult.reason !== 'no-pending-marker' && process.env.DEBUG) {
|
|
174
222
|
console.error(`[Stop] Task-boundary restart check: ${restartResult.reason}`);
|
|
175
223
|
}
|
|
176
|
-
// If we SIGTERM'd our parent, the process will begin shutting down. Still
|
|
177
|
-
// return the normal Stop-hook result so any in-flight return value flows
|
|
178
|
-
// back to claude before the signal is handled.
|
|
179
224
|
} catch (err) {
|
|
180
225
|
if (process.env.DEBUG) {
|
|
181
226
|
console.error(`[Stop] Task-boundary restart module error (fail-open): ${err.message}`);
|
|
@@ -12,6 +12,7 @@ const { checkImplementationGate } = require('../../core/implementation-gate');
|
|
|
12
12
|
const { checkResearchRequirement } = require('../../core/research-gate');
|
|
13
13
|
const { setRoutingPending, clearRoutingPending, ROUTING_CLEARED_PATH } = require('../../core/routing-gate');
|
|
14
14
|
const { getPhaseContextPrompt } = require('../../core/phase-gate');
|
|
15
|
+
const { buildOverdueContext } = require('../../core/overdue-dispatches');
|
|
15
16
|
const { markSkillPending, loadDurableSession } = require('../../../flow-durable-session');
|
|
16
17
|
const { captureCurrentPrompt } = require('../../../flow-prompt-capture');
|
|
17
18
|
const { spawnBackgroundDetection } = require('../../../flow-correction-detector');
|
|
@@ -172,6 +173,23 @@ runHook('UserPromptSubmit', async ({ input, parsedInput }) => {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
// wf-d3e67abe — surface overdue workspace dispatches (silent worker deaths)
|
|
177
|
+
// to the manager model before it processes the next prompt. Manager-only;
|
|
178
|
+
// fail-open (buildOverdueContext returns null on any error or wrong scope).
|
|
179
|
+
try {
|
|
180
|
+
const overduePrompt = buildOverdueContext();
|
|
181
|
+
if (overduePrompt) {
|
|
182
|
+
coreResult = {
|
|
183
|
+
...coreResult,
|
|
184
|
+
overduePrompt
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (process.env.DEBUG) {
|
|
189
|
+
console.error(`[Hook] Overdue dispatches check failed: ${err.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
return coreResult;
|
|
176
194
|
}, {
|
|
177
195
|
failMode: 'block',
|