wogiflow 2.33.0 → 2.34.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.
- package/lib/scheduled-mode.js +12 -15
- package/lib/skill-export-claude-plugin.js +41 -1
- package/lib/skill-portability.js +21 -3
- package/lib/workspace-channel-server.js +106 -3
- package/lib/workspace-channel-tracking.js +102 -1
- package/lib/workspace-dispatch-tracking.js +28 -0
- package/lib/workspace-messages.js +32 -4
- package/lib/workspace-subtask-state.js +215 -0
- package/lib/workspace.js +81 -0
- package/package.json +2 -2
- package/scripts/flow +17 -0
- package/scripts/flow-constants.js +3 -1
- package/scripts/flow-schedule.js +23 -6
- package/scripts/flow-scheduled-runner.js +53 -8
- package/scripts/flow-standards-checker.js +37 -0
- package/scripts/hooks/adapters/claude-code.js +6 -2
- package/scripts/hooks/core/git-safety-gate.js +34 -15
- package/scripts/hooks/core/overdue-dispatches.js +28 -6
- package/scripts/hooks/core/session-start-worker.js +52 -0
- package/scripts/hooks/core/stop-orchestrator.js +17 -2
- package/scripts/hooks/core/validation.js +8 -0
- package/scripts/hooks/core/worker-continuation-gate.js +326 -0
- package/scripts/hooks/core/workspace-stop-gates.js +21 -0
- package/scripts/hooks/core/workspace-stop-notify.js +174 -59
- package/scripts/hooks/entry/claude-code/post-tool-use.js +26 -0
|
@@ -1,76 +1,191 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Workspace worker
|
|
4
|
+
* Workspace worker signal emission (epic-workspace-sustained-exec / S3, wf-d3ae1717;
|
|
5
|
+
* originally wf-6e31850e A-3 / wf-d3e67abe).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Two distinct signals so the manager never mistakes a pause for a stop:
|
|
8
|
+
*
|
|
9
|
+
* notifyWorkerProgress() — a HEARTBEAT, emitted when the continuation gate (S2)
|
|
10
|
+
* forces the worker to keep going. "Work ongoing," NOT a stop. Carries the
|
|
11
|
+
* current sub-task progress + git HEAD so the manager can refresh the
|
|
12
|
+
* dispatch deadline instead of polling git.
|
|
13
|
+
*
|
|
14
|
+
* notifyWorkerTerminal() — emitted ONLY on a genuine stop (after all gates
|
|
15
|
+
* decline to continue). Picks a precise terminal type:
|
|
16
|
+
* worker-awaiting-approval — in-progress task sitting in spec_review/exploring
|
|
17
|
+
* (waiting on the manager's GO — NOT done)
|
|
18
|
+
* worker-stopped — in-progress, active phase, but stopping anyway
|
|
19
|
+
* (legacy mid-work stop)
|
|
20
|
+
* worker-idle — nothing in progress and nothing queued
|
|
21
|
+
*
|
|
22
|
+
* The old code emitted `worker-stopped` UNCONDITIONALLY at the top of the Stop
|
|
23
|
+
* sequence — before the gates ran — so the manager saw "stopped mid-work" on
|
|
24
|
+
* every turn boundary, even when the worker was about to be forced to continue.
|
|
25
|
+
* That ordering bug is fixed: emission now happens at the decision point.
|
|
26
|
+
*
|
|
27
|
+
* All emission is best-effort and fail-open; never throws into the Stop path.
|
|
9
28
|
*/
|
|
10
29
|
|
|
11
|
-
|
|
12
|
-
|
|
30
|
+
const VALID_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
31
|
+
const AWAITING_PHASES = new Set(['spec_review', 'exploring', 'routing', 'idle']);
|
|
32
|
+
|
|
33
|
+
function isWorker() {
|
|
34
|
+
return Boolean(process.env.WOGI_REPO_NAME &&
|
|
35
|
+
process.env.WOGI_REPO_NAME !== 'manager' &&
|
|
36
|
+
VALID_NAME.test(process.env.WOGI_REPO_NAME) &&
|
|
37
|
+
process.env.WOGI_WORKSPACE_ROOT);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readState() {
|
|
41
|
+
const nodePath = require('node:path');
|
|
42
|
+
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
43
|
+
const ready = safeJsonParse(nodePath.join(PATHS.state, 'ready.json'), {});
|
|
44
|
+
const phaseData = safeJsonParse(nodePath.join(PATHS.state, 'workflow-phase.json'), {});
|
|
45
|
+
const inProgressTask = (ready.inProgress || [])[0] || null;
|
|
46
|
+
const queued = (ready.ready || []).length;
|
|
47
|
+
let lastSha = null;
|
|
13
48
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
lastSha = require('node:child_process').execSync('git rev-parse --short HEAD 2>/dev/null || true', {
|
|
50
|
+
cwd: PATHS.root, encoding: 'utf-8', timeout: 2000
|
|
51
|
+
}).trim() || null;
|
|
52
|
+
} catch (_err) { /* non-critical */ }
|
|
53
|
+
return { inProgressTask, queued, phase: phaseData?.phase || null, lastSha };
|
|
54
|
+
}
|
|
19
55
|
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
function emit(type, fields) {
|
|
57
|
+
const nodePath = require('node:path');
|
|
58
|
+
const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
|
|
59
|
+
const repoName = process.env.WOGI_REPO_NAME;
|
|
60
|
+
const libMessages = nodePath.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-messages');
|
|
61
|
+
const { createMessage, saveMessage } = require(libMessages);
|
|
62
|
+
const msg = createMessage({
|
|
63
|
+
from: repoName,
|
|
64
|
+
to: 'manager',
|
|
65
|
+
type,
|
|
66
|
+
subject: fields.subject,
|
|
67
|
+
body: fields.body,
|
|
68
|
+
priority: fields.priority || 'medium',
|
|
69
|
+
actionRequired: Boolean(fields.actionRequired)
|
|
70
|
+
});
|
|
71
|
+
Object.assign(msg, fields.extra || {});
|
|
72
|
+
saveMessage(workspaceRoot, msg);
|
|
22
73
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
74
|
+
// Real-time nudge to the manager port (best-effort) so it doesn't have to wait
|
|
75
|
+
// for its own next prompt to read the bus.
|
|
76
|
+
try {
|
|
77
|
+
const managerPort = process.env.WOGI_MANAGER_PORT;
|
|
78
|
+
if (managerPort) {
|
|
79
|
+
const http = require('node:http');
|
|
80
|
+
const payload = `[${type}] ${fields.subject}`;
|
|
81
|
+
const buf = Buffer.from(payload, 'utf-8');
|
|
82
|
+
const req = http.request({
|
|
83
|
+
hostname: '127.0.0.1', port: parseInt(managerPort, 10), path: '/', method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'text/plain', 'Content-Length': buf.byteLength, 'X-Wogi-From': repoName }
|
|
85
|
+
});
|
|
86
|
+
req.on('error', () => {});
|
|
87
|
+
req.write(buf); req.end();
|
|
88
|
+
}
|
|
89
|
+
} catch (_err) { /* best effort */ }
|
|
90
|
+
}
|
|
28
91
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Heartbeat — the continuation gate forced another turn. NOT a stop.
|
|
94
|
+
* @param {Object} info { taskId, remaining, total, attempt }
|
|
95
|
+
*/
|
|
96
|
+
async function notifyWorkerProgress(info = {}) {
|
|
97
|
+
if (!isWorker()) return;
|
|
98
|
+
try {
|
|
99
|
+
const { lastSha } = readState();
|
|
100
|
+
emit('worker-progress', {
|
|
101
|
+
subject: `Worker ${process.env.WOGI_REPO_NAME} working on ${info.taskId || 'task'} (${info.remaining ?? '?'} sub-task(s) left)`,
|
|
102
|
+
body: [
|
|
103
|
+
`Worker "${process.env.WOGI_REPO_NAME}" is continuing (heartbeat).`,
|
|
104
|
+
info.taskId ? `Task: ${info.taskId}` : null,
|
|
105
|
+
(info.total != null) ? `Sub-tasks: ${(info.total - (info.remaining ?? 0))}/${info.total} done, ${info.remaining ?? '?'} remaining` : null,
|
|
106
|
+
info.attempt != null ? `Continuation: ${info.attempt}` : null,
|
|
107
|
+
lastSha ? `HEAD: ${lastSha}` : null
|
|
108
|
+
].filter(Boolean).join('\n'),
|
|
109
|
+
priority: 'low',
|
|
110
|
+
actionRequired: false,
|
|
111
|
+
extra: {
|
|
112
|
+
reason: 'continuation',
|
|
113
|
+
state: 'in-progress',
|
|
114
|
+
taskId: info.taskId || null,
|
|
115
|
+
taskInProgress: info.taskId || null,
|
|
116
|
+
remaining: info.remaining ?? null,
|
|
117
|
+
total: info.total ?? null,
|
|
118
|
+
continuation: info.attempt ?? null,
|
|
119
|
+
lastSha,
|
|
120
|
+
heartbeatAt: new Date().toISOString()
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (process.env.DEBUG) console.error(`[Stop] worker-progress emit failed: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
32
127
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Terminal — emitted at a genuine stop. Picks the precise terminal type.
|
|
130
|
+
*/
|
|
131
|
+
async function notifyWorkerTerminal() {
|
|
132
|
+
if (!isWorker()) return;
|
|
133
|
+
try {
|
|
134
|
+
const { inProgressTask, queued, phase, lastSha } = readState();
|
|
135
|
+
const taskId = inProgressTask?.id || null;
|
|
41
136
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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}`);
|
|
137
|
+
let type, subject, priority, actionRequired, state;
|
|
138
|
+
if (!inProgressTask) {
|
|
139
|
+
type = 'worker-idle';
|
|
140
|
+
state = 'idle';
|
|
141
|
+
subject = `Worker ${process.env.WOGI_REPO_NAME} idle (${queued} queued)`;
|
|
142
|
+
priority = 'medium';
|
|
143
|
+
actionRequired = queued > 0; // queued-but-not-started is worth the manager's attention
|
|
144
|
+
} else if (AWAITING_PHASES.has(phase)) {
|
|
145
|
+
type = 'worker-awaiting-approval';
|
|
146
|
+
state = 'awaiting-approval';
|
|
147
|
+
subject = `Worker ${process.env.WOGI_REPO_NAME} awaiting approval on ${taskId} (phase: ${phase})`;
|
|
148
|
+
priority = 'high';
|
|
149
|
+
actionRequired = true;
|
|
150
|
+
} else {
|
|
151
|
+
type = 'worker-stopped';
|
|
152
|
+
state = 'mid-work';
|
|
153
|
+
subject = `Worker stopped mid-work on ${taskId}`;
|
|
154
|
+
priority = 'high';
|
|
155
|
+
actionRequired = true;
|
|
70
156
|
}
|
|
157
|
+
|
|
158
|
+
emit(type, {
|
|
159
|
+
subject,
|
|
160
|
+
body: [
|
|
161
|
+
`Worker "${process.env.WOGI_REPO_NAME}" stopped.`,
|
|
162
|
+
`State: ${state}`,
|
|
163
|
+
taskId ? `Task in progress: ${taskId}` : `Queued: ${queued}`,
|
|
164
|
+
phase ? `Phase: ${phase}` : null,
|
|
165
|
+
lastSha ? `Last commit: ${lastSha}` : null
|
|
166
|
+
].filter(Boolean).join('\n'),
|
|
167
|
+
priority,
|
|
168
|
+
actionRequired,
|
|
169
|
+
extra: {
|
|
170
|
+
reason: 'graceful',
|
|
171
|
+
state,
|
|
172
|
+
taskId,
|
|
173
|
+
taskInProgress: taskId,
|
|
174
|
+
phase,
|
|
175
|
+
lastSha
|
|
176
|
+
}
|
|
177
|
+
});
|
|
71
178
|
} catch (err) {
|
|
72
|
-
if (process.env.DEBUG) console.error(`[Stop]
|
|
179
|
+
if (process.env.DEBUG) console.error(`[Stop] worker terminal emit failed: ${err.message}`);
|
|
73
180
|
}
|
|
74
181
|
}
|
|
75
182
|
|
|
76
|
-
module.exports = {
|
|
183
|
+
module.exports = {
|
|
184
|
+
notifyWorkerProgress,
|
|
185
|
+
notifyWorkerTerminal,
|
|
186
|
+
isWorker,
|
|
187
|
+
AWAITING_PHASES,
|
|
188
|
+
// Back-compat alias: the old single unconditional emitter. Now routes to the
|
|
189
|
+
// typed terminal notifier (callers that want a terminal stop signal).
|
|
190
|
+
notifyWorkerStopped: notifyWorkerTerminal
|
|
191
|
+
};
|
|
@@ -57,6 +57,32 @@ runHook('PostToolUse', async ({ parsedInput }) => {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// S1 (wf-e72350bf): mirror the worker's TodoWrite decomposition to the durable
|
|
61
|
+
// sub-task ledger so the continuation gate (S2) and restart-resume (S5) have a
|
|
62
|
+
// disk-readable "work remaining" signal. Worker-mode only by default (solo
|
|
63
|
+
// sessions opt in via workspace.subtaskLedger.alsoInSolo). Fail-open.
|
|
64
|
+
if (toolName === 'TodoWrite' && !toolFailed) {
|
|
65
|
+
try {
|
|
66
|
+
const { getConfig, PATHS, safeJsonParse } = require('../../../flow-utils');
|
|
67
|
+
const cfg = (() => { try { return getConfig()?.workspace?.subtaskLedger || {}; } catch (_err) { return {}; } })();
|
|
68
|
+
const ledgerEnabled = cfg.enabled !== false;
|
|
69
|
+
const isWorker = process.env.WOGI_WORKSPACE_ROOT &&
|
|
70
|
+
process.env.WOGI_REPO_NAME &&
|
|
71
|
+
process.env.WOGI_REPO_NAME !== 'manager';
|
|
72
|
+
if (ledgerEnabled && (isWorker || cfg.alsoInSolo === true)) {
|
|
73
|
+
const ready = safeJsonParse(require('node:path').join(PATHS.state, 'ready.json'), { inProgress: [] });
|
|
74
|
+
const taskId = ready.inProgress?.[0]?.id || null;
|
|
75
|
+
if (taskId) {
|
|
76
|
+
const subtaskState = require('../../../../lib/workspace-subtask-state');
|
|
77
|
+
const subs = subtaskState.subtasksFromTodos(toolInput);
|
|
78
|
+
if (subs.length > 0) subtaskState.write(taskId, subs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (process.env.DEBUG) console.error(`[post-tool-use] subtask-ledger mirror: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
60
86
|
// Track B B3 fix (2026-04-13): mark when templates are edited, so
|
|
61
87
|
// session-end and the next /wogi-start can remind to run flow bridge sync.
|
|
62
88
|
// Mechanical enforcement of self-maintenance.md §1.
|