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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Durable Sub-task State (epic-workspace-sustained-exec / S1, wf-e72350bf)
|
|
5
|
+
*
|
|
6
|
+
* Sub-tasks of a decomposed task are normally tracked ONLY in Claude Code's
|
|
7
|
+
* in-context TodoWrite list, which is ephemeral: a Stop hook running in a fresh
|
|
8
|
+
* `node` process can't read it, and a session restart loses it entirely. That's
|
|
9
|
+
* the root cause behind workers re-executing completed sub-tasks after a restart
|
|
10
|
+
* and behind the continuation gate (S2) having no reliable "work remaining"
|
|
11
|
+
* signal.
|
|
12
|
+
*
|
|
13
|
+
* This module mirrors the decomposition to a durable, atomically-written ledger
|
|
14
|
+
* at `.workflow/state/subtask-state.json` so that:
|
|
15
|
+
* - a fresh-process Stop hook can read "N of M sub-tasks remain" (S2),
|
|
16
|
+
* - a restarted session can resume without redoing completed sub-tasks (S5).
|
|
17
|
+
*
|
|
18
|
+
* The ledger holds the CURRENT in-progress task's decomposition. There is one
|
|
19
|
+
* active task per worker at a time, so a single-task shape is sufficient and
|
|
20
|
+
* avoids stale cross-task contamination — read* helpers take an optional taskId
|
|
21
|
+
* and return empty when it doesn't match the ledger's task.
|
|
22
|
+
*
|
|
23
|
+
* Atomic write: tmp + fsync(file) + rename + fsync(dir), mirroring
|
|
24
|
+
* task-boundary-reset.js:writeCleanCompletionMarker so the ledger survives the
|
|
25
|
+
* SIGTERM/relaunch boundary and concurrent readers never see torn JSON.
|
|
26
|
+
*
|
|
27
|
+
* Fail-open throughout: any error degrades to "no durable state" (callers fall
|
|
28
|
+
* back to their prior behavior). Never throws.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const { PATHS } = require('../scripts/flow-utils');
|
|
35
|
+
const { safeJsonParse } = require('../scripts/flow-io');
|
|
36
|
+
|
|
37
|
+
const LEDGER_FILE = 'subtask-state.json';
|
|
38
|
+
const LEDGER_VERSION = 1;
|
|
39
|
+
// Statuses that count as "still needs work" for remaining().
|
|
40
|
+
const OPEN_STATUSES = new Set(['pending', 'in_progress']);
|
|
41
|
+
const TERMINAL_STATUSES = new Set(['completed', 'blocked']);
|
|
42
|
+
|
|
43
|
+
function getLedgerPath() {
|
|
44
|
+
return path.join(PATHS.state, LEDGER_FILE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a free-form sub-task entry (TodoWrite item or plain object/string)
|
|
49
|
+
* into the ledger shape { id, title, status }.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeSubtask(entry, index) {
|
|
52
|
+
if (entry == null) return null;
|
|
53
|
+
if (typeof entry === 'string') {
|
|
54
|
+
return { id: String(index + 1).padStart(2, '0'), title: entry.slice(0, 500), status: 'pending' };
|
|
55
|
+
}
|
|
56
|
+
if (typeof entry !== 'object') return null;
|
|
57
|
+
const rawStatus = typeof entry.status === 'string' ? entry.status.toLowerCase() : 'pending';
|
|
58
|
+
const status = OPEN_STATUSES.has(rawStatus) || TERMINAL_STATUSES.has(rawStatus) ? rawStatus : 'pending';
|
|
59
|
+
const title = entry.title || entry.content || entry.text || entry.description || entry.activeForm || `Sub-task ${index + 1}`;
|
|
60
|
+
const id = entry.id != null ? String(entry.id) : String(index + 1).padStart(2, '0');
|
|
61
|
+
return { id, title: String(title).slice(0, 500), status };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Map a Claude Code TodoWrite toolInput ({ todos: [{ content, status }] })
|
|
66
|
+
* into normalized sub-tasks. Returns [] for anything unparseable.
|
|
67
|
+
*/
|
|
68
|
+
function subtasksFromTodos(toolInput) {
|
|
69
|
+
try {
|
|
70
|
+
const todos = toolInput && Array.isArray(toolInput.todos) ? toolInput.todos : null;
|
|
71
|
+
if (!todos) return [];
|
|
72
|
+
return todos.map(normalizeSubtask).filter(Boolean);
|
|
73
|
+
} catch (_err) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the full ledger object, or null if absent/unreadable.
|
|
80
|
+
* @returns {{version:number, taskId:string, updatedAt:string, subtasks:Array}|null}
|
|
81
|
+
*/
|
|
82
|
+
function readLedger() {
|
|
83
|
+
try {
|
|
84
|
+
const data = safeJsonParse(getLedgerPath(), null);
|
|
85
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.subtasks)) return null;
|
|
86
|
+
return data;
|
|
87
|
+
} catch (_err) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read the sub-tasks for a given task. When taskId is provided and doesn't match
|
|
94
|
+
* the ledger's task, returns [] (the ledger belongs to a different task).
|
|
95
|
+
* @param {string} [taskId]
|
|
96
|
+
* @returns {Array<{id:string,title:string,status:string}>}
|
|
97
|
+
*/
|
|
98
|
+
function read(taskId) {
|
|
99
|
+
const led = readLedger();
|
|
100
|
+
if (!led) return [];
|
|
101
|
+
if (taskId && led.taskId && led.taskId !== taskId) return [];
|
|
102
|
+
return led.subtasks;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write/replace the ledger for taskId atomically.
|
|
107
|
+
* @param {string} taskId
|
|
108
|
+
* @param {Array} subtasks raw or normalized entries
|
|
109
|
+
* @returns {{written:boolean, reason?:string, path?:string}}
|
|
110
|
+
*/
|
|
111
|
+
function write(taskId, subtasks) {
|
|
112
|
+
if (!taskId) return { written: false, reason: 'no-task-id' };
|
|
113
|
+
try {
|
|
114
|
+
const normalized = (Array.isArray(subtasks) ? subtasks : [])
|
|
115
|
+
.map(normalizeSubtask)
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
const payload = {
|
|
118
|
+
version: LEDGER_VERSION,
|
|
119
|
+
taskId,
|
|
120
|
+
updatedAt: new Date().toISOString(),
|
|
121
|
+
subtasks: normalized
|
|
122
|
+
};
|
|
123
|
+
const p = getLedgerPath();
|
|
124
|
+
const dir = path.dirname(p);
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
const tmp = `${p}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
127
|
+
const fd = fs.openSync(tmp, 'w');
|
|
128
|
+
try {
|
|
129
|
+
fs.writeSync(fd, JSON.stringify(payload, null, 2));
|
|
130
|
+
fs.fsyncSync(fd);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
fs.renameSync(tmp, p);
|
|
135
|
+
try {
|
|
136
|
+
const dfd = fs.openSync(dir, 'r');
|
|
137
|
+
try { fs.fsyncSync(dfd); } finally { fs.closeSync(dfd); }
|
|
138
|
+
} catch (_err) { /* directory fsync best-effort (not supported on all FS) */ }
|
|
139
|
+
return { written: true, path: p };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { written: false, reason: `write-failed: ${err.message}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Count sub-tasks still needing work (pending + in_progress) for taskId.
|
|
147
|
+
* blocked and completed do NOT count. Returns 0 when no matching ledger exists
|
|
148
|
+
* (no durable state ⇒ "nothing known to remain"; callers decide what that means).
|
|
149
|
+
* @param {string} [taskId]
|
|
150
|
+
* @returns {number}
|
|
151
|
+
*/
|
|
152
|
+
function remaining(taskId) {
|
|
153
|
+
const subs = read(taskId);
|
|
154
|
+
return subs.filter(s => OPEN_STATUSES.has(s.status)).length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Summary counts for heartbeats / status (S3, S4).
|
|
159
|
+
* @param {string} [taskId]
|
|
160
|
+
* @returns {{total:number, remaining:number, completed:number, blocked:number}}
|
|
161
|
+
*/
|
|
162
|
+
function summary(taskId) {
|
|
163
|
+
const subs = read(taskId);
|
|
164
|
+
return {
|
|
165
|
+
total: subs.length,
|
|
166
|
+
remaining: subs.filter(s => OPEN_STATUSES.has(s.status)).length,
|
|
167
|
+
completed: subs.filter(s => s.status === 'completed').length,
|
|
168
|
+
blocked: subs.filter(s => s.status === 'blocked').length
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Mark one sub-task's status, preserving the rest. No-op if the ledger task
|
|
174
|
+
* doesn't match. Returns the updated count.
|
|
175
|
+
* @param {string} taskId
|
|
176
|
+
* @param {string} subId
|
|
177
|
+
* @param {string} [status='completed']
|
|
178
|
+
*/
|
|
179
|
+
function markStatus(taskId, subId, status = 'completed') {
|
|
180
|
+
const led = readLedger();
|
|
181
|
+
if (!led || (taskId && led.taskId !== taskId)) return { written: false, reason: 'no-matching-ledger' };
|
|
182
|
+
const next = led.subtasks.map(s => (s.id === String(subId) ? { ...s, status } : s));
|
|
183
|
+
return write(led.taskId, next);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Clear the ledger (e.g. on task completion). Best-effort.
|
|
188
|
+
*/
|
|
189
|
+
function clear() {
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(getLedgerPath());
|
|
192
|
+
return { cleared: true };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err && err.code !== 'ENOENT' && process.env.DEBUG) {
|
|
195
|
+
console.error(`[workspace-subtask-state] clear: ${err.code} ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
return { cleared: false };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
getLedgerPath,
|
|
203
|
+
normalizeSubtask,
|
|
204
|
+
subtasksFromTodos,
|
|
205
|
+
readLedger,
|
|
206
|
+
read,
|
|
207
|
+
write,
|
|
208
|
+
remaining,
|
|
209
|
+
summary,
|
|
210
|
+
markStatus,
|
|
211
|
+
clear,
|
|
212
|
+
LEDGER_FILE,
|
|
213
|
+
OPEN_STATUSES,
|
|
214
|
+
TERMINAL_STATUSES
|
|
215
|
+
};
|
package/lib/workspace.js
CHANGED
|
@@ -1209,6 +1209,18 @@ You are a workspace worker. There is NO human watching your terminal. You MUST o
|
|
|
1209
1209
|
|
|
1210
1210
|
The \`TaskCompleted\` hook will inject an auto-pickup directive when channel dispatches are queued (v2.20.0+). The \`Stop\` hook will BLOCK end-of-turn if you try to stop while dispatches are queued and no task is in progress. These enforcements exist because the silent-stall pattern was incident-worthy (2026-04-16).
|
|
1211
1211
|
|
|
1212
|
+
### Sustained Execution — Grind a Dispatched Task to Completion (v2.34.0+)
|
|
1213
|
+
|
|
1214
|
+
**A dispatched task runs to COMPLETION across turns. Do NOT stop to "report progress" after each sub-task.** When you decompose a task into sub-tasks (TodoWrite), work through ALL of them in the same session — the decomposition is mirrored to durable state (\`subtask-state.json\`) so it survives turn boundaries.
|
|
1215
|
+
|
|
1216
|
+
"The work" in *"reply to the manager after completing the work"* means the **entire dispatched task** (all its sub-tasks), not one increment. Reply with \`## Results\` ONLY when:
|
|
1217
|
+
- the task is **complete** (you ran \`flow done <id>\` and it left inProgress), or
|
|
1218
|
+
- you are **escalating** a genuine blocker (\`## QUESTION:\` / \`## BLOCKED:\`).
|
|
1219
|
+
|
|
1220
|
+
The \`Stop\` hook's **in-progress continuation gate** will force you to keep going (\`{continue:true}\`) while a task is in-progress in the coding/validating phase with sub-tasks remaining. If you stop early to "give visibility," it will re-prompt you to continue. Make real progress every turn (an edit, a test, a completed sub-task) — idle turns with no file changes are detected across a few continuations and stop with an escalation.
|
|
1221
|
+
|
|
1222
|
+
**Escalate instead of proceeding** when the next step is destructive, irreversible, touches production, needs external credentials, or genuinely needs a human decision. Otherwise: keep grinding until done.
|
|
1223
|
+
|
|
1212
1224
|
### Tool-First Turn Contract (v2.27.0+)
|
|
1213
1225
|
|
|
1214
1226
|
**Every worker turn after a UserPromptSubmit MUST contain at least one tool call. In strict mode (default), the FIRST content block must be a tool call, not text.**
|
|
@@ -1768,6 +1780,61 @@ function startWorkerSession(cwd) {
|
|
|
1768
1780
|
* Handle workspace subcommands
|
|
1769
1781
|
* @param {string[]} args
|
|
1770
1782
|
*/
|
|
1783
|
+
/**
|
|
1784
|
+
* S5 (wf-ee87a24e): manager-triggered worker restart. Resolves the worker's
|
|
1785
|
+
* channel port from the workspace config and POSTs /restart. The worker's
|
|
1786
|
+
* channel server writes the wrapper restart flag and SIGTERMs claude, so the
|
|
1787
|
+
* wogi-claude wrapper relaunches it with a fresh require cache (reloading any
|
|
1788
|
+
* upgraded wogiflow code). Resume of the in-progress task is handled by the
|
|
1789
|
+
* worker's SessionStart hook (resume-in-progress branch).
|
|
1790
|
+
*/
|
|
1791
|
+
async function restartWorkerSession(cwd, workerName) {
|
|
1792
|
+
// Find workspace root
|
|
1793
|
+
let workspaceRoot = null;
|
|
1794
|
+
let dir = cwd;
|
|
1795
|
+
while (dir !== path.dirname(dir)) {
|
|
1796
|
+
if (fs.existsSync(path.join(dir, WORKSPACE_CONFIG_FILE))) { workspaceRoot = dir; break; }
|
|
1797
|
+
dir = path.dirname(dir);
|
|
1798
|
+
}
|
|
1799
|
+
if (!workspaceRoot) {
|
|
1800
|
+
console.error('Error: Not inside a workspace (no wogi-workspace.json found).');
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
const config = safeJsonParse(path.join(workspaceRoot, WORKSPACE_CONFIG_FILE), null);
|
|
1804
|
+
const member = config?.channels?.members?.[workerName];
|
|
1805
|
+
if (!member || !member.port) {
|
|
1806
|
+
console.error(`Error: unknown worker "${workerName}". Known workers: ${Object.keys(config?.channels?.members || {}).join(', ') || '(none)'}`);
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const http = require('node:http');
|
|
1811
|
+
const result = await new Promise((resolve) => {
|
|
1812
|
+
const req = http.request({
|
|
1813
|
+
hostname: '127.0.0.1', port: member.port, path: '/restart', method: 'POST',
|
|
1814
|
+
headers: { 'X-Wogi-From': 'manager', 'Content-Length': 0 }, timeout: 5000
|
|
1815
|
+
}, (res) => {
|
|
1816
|
+
let data = '';
|
|
1817
|
+
res.on('data', c => { data += c; });
|
|
1818
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
1819
|
+
});
|
|
1820
|
+
req.on('error', (err) => resolve({ error: err.message }));
|
|
1821
|
+
req.on('timeout', () => { req.destroy(); resolve({ error: 'timeout' }); });
|
|
1822
|
+
req.end();
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
if (result.error) {
|
|
1826
|
+
console.error(`✗ Could not reach worker "${workerName}" on port ${member.port}: ${result.error}`);
|
|
1827
|
+
console.error(` Is the worker running? Start it with: cd ${member.path || workerName} && flow workspace start`);
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
if (result.status === 202) {
|
|
1831
|
+
console.log(`✓ Restart signalled to "${workerName}" (port ${member.port}). The worker will relaunch with fresh code and resume its in-progress task.`);
|
|
1832
|
+
} else {
|
|
1833
|
+
console.error(`✗ Worker "${workerName}" returned HTTP ${result.status}: ${result.body}`);
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1771
1838
|
async function workspace(args) {
|
|
1772
1839
|
const subcommand = args[0];
|
|
1773
1840
|
|
|
@@ -1816,6 +1883,18 @@ async function workspace(args) {
|
|
|
1816
1883
|
startWorkerSession(process.cwd());
|
|
1817
1884
|
break;
|
|
1818
1885
|
}
|
|
1886
|
+
case 'restart': {
|
|
1887
|
+
// S5 (wf-ee87a24e): cycle a worker session so it reloads upgraded code.
|
|
1888
|
+
// POSTs /restart to the worker's channel; the channel server writes the
|
|
1889
|
+
// wrapper flag and SIGTERMs claude → wogi-claude relaunches it fresh.
|
|
1890
|
+
const workerName = args[1];
|
|
1891
|
+
if (!workerName) {
|
|
1892
|
+
console.error('Usage: flow workspace restart <worker>');
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
await restartWorkerSession(process.cwd(), workerName);
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1819
1898
|
default:
|
|
1820
1899
|
console.log(`
|
|
1821
1900
|
Wogi Workspace — Multi-Repo Orchestration
|
|
@@ -1829,12 +1908,14 @@ Commands:
|
|
|
1829
1908
|
add Add a member repo to the workspace
|
|
1830
1909
|
remove Remove a member repo from the workspace
|
|
1831
1910
|
start Start a worker session with channel (run from a member repo)
|
|
1911
|
+
restart Cycle a worker session so it reloads upgraded code + resumes its task
|
|
1832
1912
|
|
|
1833
1913
|
Examples:
|
|
1834
1914
|
flow workspace init # Create workspace from subdirectories
|
|
1835
1915
|
flow workspace sync # Refresh after external changes
|
|
1836
1916
|
flow workspace status # Show all repos, tasks, contracts
|
|
1837
1917
|
cd frontend/ && flow workspace start # Start worker session
|
|
1918
|
+
flow workspace restart backend # Restart the 'backend' worker (after npm upgrade)
|
|
1838
1919
|
`);
|
|
1839
1920
|
}
|
|
1840
1921
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.34.1",
|
|
4
4
|
"description": "AI-powered development workflow management system with multi-model support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"flow": "./scripts/flow",
|
|
13
|
-
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js tests/flow-scheduled-runner.test.js tests/flow-schedule-cli.test.js tests/flow-skill-portability.test.js tests/flow-skill-export.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
13
|
+
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.test.js tests/flow-architect-runs.test.js tests/flow-installer-forbidden-patterns.test.js tests/flow-deferral-classifier-ai.test.js tests/flow-no-defer-policy.test.js tests/flow-self-adversary-loop.test.js tests/flow-impl-question-classifier.test.js tests/flow-hooks-self-adversary-gate.test.js tests/flow-scheduled-runner.test.js tests/flow-schedule-cli.test.js tests/flow-skill-portability.test.js tests/flow-skill-export.test.js tests/flow-workspace-subtask-state.test.js tests/flow-worker-continuation-gate.test.js tests/flow-workspace-stop-notify.test.js tests/flow-workspace-channel-status.test.js tests/flow-workspace-restart-resume.test.js tests/flow-workspace-sustained-execution.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
14
14
|
"test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
|
|
15
15
|
"lint": "eslint scripts/ lib/ tests/",
|
|
16
16
|
"lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
|
package/scripts/flow
CHANGED
|
@@ -381,6 +381,23 @@ case "${1:-}" in
|
|
|
381
381
|
ready)
|
|
382
382
|
node "$SCRIPT_DIR/flow-ready.js" "${@:2}"
|
|
383
383
|
;;
|
|
384
|
+
# F11 (R-379): bin/flow (npm-installed dispatcher) had `schedule` wired
|
|
385
|
+
# at line 232, but scripts/flow (dev/repo dispatcher) did not — so
|
|
386
|
+
# `flow schedule install` failed silently inside the dev repo. Wire it
|
|
387
|
+
# here so both dispatchers stay in sync. Same for `skill export`, which
|
|
388
|
+
# ships in v2.33.0 but was only reachable from the npm-installed path.
|
|
389
|
+
schedule)
|
|
390
|
+
node "$SCRIPT_DIR/flow-schedule.js" "${@:2}"
|
|
391
|
+
;;
|
|
392
|
+
skill|skills)
|
|
393
|
+
# Sub-subcommand: `flow skill export ...` → flow-skill-export.js
|
|
394
|
+
# Other `flow skill ...` subcommands → flow-skill-manage.js (existing).
|
|
395
|
+
if [ "${2:-}" = "export" ]; then
|
|
396
|
+
node "$SCRIPT_DIR/flow-skill-export.js" "${@:3}"
|
|
397
|
+
else
|
|
398
|
+
node "$SCRIPT_DIR/flow-skill-manage.js" "${@:2}"
|
|
399
|
+
fi
|
|
400
|
+
;;
|
|
384
401
|
start)
|
|
385
402
|
node "$SCRIPT_DIR/flow-start.js" "${@:2}"
|
|
386
403
|
;;
|
|
@@ -148,7 +148,9 @@ const KNOWN_CONFIG_KEYS = [
|
|
|
148
148
|
// Task-boundary session restart (wf-39e9dc09) — opt-in, experimental
|
|
149
149
|
'taskBoundaryReset',
|
|
150
150
|
// Session hydration recency filter (wf-729ab5c0)
|
|
151
|
-
'sessionHydration'
|
|
151
|
+
'sessionHydration',
|
|
152
|
+
// Workspace (multi-repo manager/worker) — epic-workspace-sustained-exec
|
|
153
|
+
'workspace'
|
|
152
154
|
];
|
|
153
155
|
|
|
154
156
|
module.exports = {
|
package/scripts/flow-schedule.js
CHANGED
|
@@ -100,6 +100,15 @@ function nodeBinary() {
|
|
|
100
100
|
// Unit content generators (pure — no I/O)
|
|
101
101
|
// ============================================================
|
|
102
102
|
|
|
103
|
+
// F8 (R-379): paths that get inlined into shell-interpreted contexts
|
|
104
|
+
// (crontab lines, systemd ExecStart) MUST be single-quoted with embedded
|
|
105
|
+
// single-quotes escaped. Without this, a project at `/Users/Alice Smith/...`
|
|
106
|
+
// breaks the schedule because cron treats the space as an argument boundary.
|
|
107
|
+
function shellQuote(s) {
|
|
108
|
+
// POSIX single-quote escape: replace each ' with '\''
|
|
109
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
function generateCrontabLines(opts = {}) {
|
|
104
113
|
const node = opts.node || nodeBinary();
|
|
105
114
|
const runner = opts.runner || runnerScriptPath();
|
|
@@ -109,9 +118,11 @@ function generateCrontabLines(opts = {}) {
|
|
|
109
118
|
];
|
|
110
119
|
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
111
120
|
const spec = SCHEDULES[jobName];
|
|
121
|
+
const logPath = path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`);
|
|
112
122
|
lines.push(
|
|
113
|
-
`${spec.cronExpr} cd ${projectRoot} &&
|
|
114
|
-
|
|
123
|
+
`${spec.cronExpr} cd ${shellQuote(projectRoot)} && ` +
|
|
124
|
+
`${shellQuote(node)} ${shellQuote(runner)} ${jobName} ` +
|
|
125
|
+
`>> ${shellQuote(logPath)} 2>&1`
|
|
115
126
|
);
|
|
116
127
|
}
|
|
117
128
|
lines.push(`# === wogi-scheduled end ===`);
|
|
@@ -172,14 +183,18 @@ function generateSystemdServiceUnit(jobName, opts = {}) {
|
|
|
172
183
|
const projectRoot = opts.projectRoot || repoRoot();
|
|
173
184
|
if (!SCHEDULES[jobName]) throw new Error(`No schedule defined for "${jobName}"`);
|
|
174
185
|
|
|
186
|
+
// F8 (R-379): systemd ExecStart with a path containing spaces needs each
|
|
187
|
+
// arg surrounded by quotes — systemd's argument parser splits on spaces.
|
|
188
|
+
// WorkingDirectory and Standard{Output,Error} accept literal paths (no
|
|
189
|
+
// splitting), but quoting them too is harmless and consistent.
|
|
175
190
|
return `[Unit]
|
|
176
191
|
Description=Wogi Flow scheduled job: ${jobName}
|
|
177
192
|
After=network-online.target
|
|
178
193
|
|
|
179
194
|
[Service]
|
|
180
195
|
Type=oneshot
|
|
181
|
-
WorkingDirectory
|
|
182
|
-
ExecStart
|
|
196
|
+
WorkingDirectory="${projectRoot}"
|
|
197
|
+
ExecStart="${node}" "${runner}" "${jobName}"
|
|
183
198
|
StandardOutput=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`)}
|
|
184
199
|
StandardError=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.err.log`)}
|
|
185
200
|
`;
|
|
@@ -361,12 +376,14 @@ function getStatus(deps = {}) {
|
|
|
361
376
|
const cfrag = path.join(homeDir, '.config', 'wogi-flow', 'crontab-fragment');
|
|
362
377
|
if (fsx.existsSync(cfrag)) status.cron.push(cfrag);
|
|
363
378
|
|
|
364
|
-
// systemd
|
|
379
|
+
// systemd — both .service and .timer files are tracked under the same key.
|
|
380
|
+
// F21 (R-379): the prior ternary `ext === 'timer' ? 'systemd' : 'systemd'`
|
|
381
|
+
// had identical branches — a meaningless conditional. Just push directly.
|
|
365
382
|
const sdir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
366
383
|
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
367
384
|
for (const ext of ['service', 'timer']) {
|
|
368
385
|
const p = path.join(sdir, `wogi-scheduled-${jobName}.${ext}`);
|
|
369
|
-
if (fsx.existsSync(p)) status
|
|
386
|
+
if (fsx.existsSync(p)) status.systemd.push(p);
|
|
370
387
|
}
|
|
371
388
|
}
|
|
372
389
|
return status;
|
|
@@ -58,6 +58,36 @@ const {
|
|
|
58
58
|
|
|
59
59
|
const { runInWorktree } = require('./flow-worktree');
|
|
60
60
|
|
|
61
|
+
// F16 (R-379): named constant for stderr-truncation cap so the meaning is
|
|
62
|
+
// inspectable and tunable in one place (decisions.md Code Quality §1).
|
|
63
|
+
const MAX_STDERR_BYTES = 4096;
|
|
64
|
+
|
|
65
|
+
// F10 (R-379): redact secret-shaped strings from stderr BEFORE we post it
|
|
66
|
+
// to a public GH Issue. Conservative — only strips the well-known token
|
|
67
|
+
// shapes (Anthropic API keys, GitHub PATs/fine-grained). Real users may
|
|
68
|
+
// have other tokens in their environment; this is best-effort, not a
|
|
69
|
+
// guarantee. The right defense remains "don't echo secrets in error
|
|
70
|
+
// messages in the first place"; this is the defense-in-depth pass.
|
|
71
|
+
const SECRET_REDACTION_PATTERNS = [
|
|
72
|
+
// Anthropic API keys — sk-ant-…
|
|
73
|
+
{ re: /sk-ant-[a-zA-Z0-9_\-]{20,}/g, replacement: '[REDACTED:anthropic-key]' },
|
|
74
|
+
// GitHub PATs — ghp_… (classic), github_pat_… (fine-grained)
|
|
75
|
+
{ re: /ghp_[a-zA-Z0-9]{36}/g, replacement: '[REDACTED:github-pat-classic]' },
|
|
76
|
+
{ re: /github_pat_[a-zA-Z0-9_]{22,}/g, replacement: '[REDACTED:github-pat-fg]' },
|
|
77
|
+
// OAuth-shaped bearer tokens — only when they appear next to the literal
|
|
78
|
+
// "Authorization: Bearer" header (avoid stripping every JWT in stderr).
|
|
79
|
+
{ re: /(Authorization:\s*Bearer\s+)[A-Za-z0-9_\-.]{20,}/gi, replacement: '$1[REDACTED:bearer]' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function redactSecrets(text) {
|
|
83
|
+
if (typeof text !== 'string') return '';
|
|
84
|
+
let out = text;
|
|
85
|
+
for (const { re, replacement } of SECRET_REDACTION_PATTERNS) {
|
|
86
|
+
out = out.replace(re, replacement);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
const { PATHS, getConfig, safeJsonParse } = require('./flow-utils');
|
|
62
92
|
|
|
63
93
|
// ============================================================
|
|
@@ -148,7 +178,7 @@ function recordUsage(jobName, tokens, now = Date.now()) {
|
|
|
148
178
|
const key = new Date(now).toISOString().slice(0, 10);
|
|
149
179
|
const log = readUsageLog();
|
|
150
180
|
if (!log[key]) log[key] = {};
|
|
151
|
-
log[key][jobName] = (log[key][jobName]
|
|
181
|
+
log[key][jobName] = (log[key][jobName] ?? 0) + tokens;
|
|
152
182
|
writeUsageLog(log);
|
|
153
183
|
return log;
|
|
154
184
|
}
|
|
@@ -180,7 +210,10 @@ function listDedupIssues(jobName, repo) {
|
|
|
180
210
|
const r = execSafe('gh', args);
|
|
181
211
|
if (!r.ok) return [];
|
|
182
212
|
try {
|
|
183
|
-
|
|
213
|
+
// F19 (R-379): use safeJsonParse for prototype-pollution guard, even
|
|
214
|
+
// though gh's output is trusted today — consistent with the project
|
|
215
|
+
// convention (security-patterns.md §2).
|
|
216
|
+
const parsed = safeJsonParse(r.stdout || '[]', []);
|
|
184
217
|
if (!Array.isArray(parsed)) return [];
|
|
185
218
|
return parsed.map((x) => x && x.number).filter((n) => Number.isFinite(n));
|
|
186
219
|
} catch (_err) {
|
|
@@ -206,9 +239,9 @@ function openFailureIssue(jobName, summary, stderr, repo) {
|
|
|
206
239
|
`### Summary`,
|
|
207
240
|
summary,
|
|
208
241
|
``,
|
|
209
|
-
`### stderr (last
|
|
242
|
+
`### stderr (last ${MAX_STDERR_BYTES} bytes, secrets redacted)`,
|
|
210
243
|
'```',
|
|
211
|
-
(stderr || '').slice(-
|
|
244
|
+
redactSecrets((stderr || '').slice(-MAX_STDERR_BYTES)),
|
|
212
245
|
'```',
|
|
213
246
|
].join('\n');
|
|
214
247
|
const argv = ['issue', 'create', '--title', title, '--body', body, '--label', FAILURE_LABEL];
|
|
@@ -409,19 +442,31 @@ async function runOnce(jobName, ctx) {
|
|
|
409
442
|
}
|
|
410
443
|
return withTimeout(
|
|
411
444
|
({ signal }) => handler({ ...ctx, signal }),
|
|
412
|
-
ctx.timeoutMs
|
|
445
|
+
ctx.timeoutMs ?? DEFAULT_JOB_TIMEOUT_MS,
|
|
413
446
|
);
|
|
414
447
|
}
|
|
415
448
|
|
|
416
449
|
async function runJobWithRetry(jobName, ctx) {
|
|
417
450
|
const first = await runOnce(jobName, ctx);
|
|
418
|
-
if (first.ok) return first;
|
|
419
451
|
|
|
420
|
-
//
|
|
421
|
-
|
|
452
|
+
// F5 (R-379): handlers catch internally (via execSafe / try-catch) and
|
|
453
|
+
// return `{passed:false, ...}` rather than throwing, so `withTimeout`
|
|
454
|
+
// wraps them as `{ok:true, result:{...}}`. Without this branch, the
|
|
455
|
+
// transient-retry path is unreachable because `first.ok` is almost
|
|
456
|
+
// always true. Look at the INNER result for transient signals too.
|
|
457
|
+
const transientInOuter = !first.ok && first.error && isTransientError(first.error);
|
|
458
|
+
const transientInInner =
|
|
459
|
+
first.ok &&
|
|
460
|
+
first.result &&
|
|
461
|
+
first.result.passed === false &&
|
|
462
|
+
typeof first.result.message === 'string' &&
|
|
463
|
+
isTransientError({ message: first.result.message });
|
|
464
|
+
|
|
465
|
+
if (transientInOuter || transientInInner) {
|
|
422
466
|
await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));
|
|
423
467
|
return runOnce(jobName, ctx);
|
|
424
468
|
}
|
|
469
|
+
|
|
425
470
|
return first;
|
|
426
471
|
}
|
|
427
472
|
|
|
@@ -451,6 +451,43 @@ function checkSecurityPatterns(file, _securityRules) {
|
|
|
451
451
|
|
|
452
452
|
// Hard-coded security checks from security-patterns.md
|
|
453
453
|
|
|
454
|
+
// 0. execSync / execAsync with template-string commands containing
|
|
455
|
+
// interpolated values (R-379 standards-gate hardening).
|
|
456
|
+
// security-patterns.md §8 mandates execFile* with array args for any
|
|
457
|
+
// subprocess that includes dynamic data. Three independent review rounds
|
|
458
|
+
// have caught this pattern in scripts/hooks/core/git-safety-gate.js;
|
|
459
|
+
// making the check mechanical so it can't slip past again.
|
|
460
|
+
//
|
|
461
|
+
// Scoped to scripts/hooks/ and lib/ — these are the places the rule binds.
|
|
462
|
+
// Test files and CLI tools that build complex pipelines often legitimately
|
|
463
|
+
// use template-string shells (e.g. for documented one-off scripts).
|
|
464
|
+
//
|
|
465
|
+
// Match shape: execSync(`...${...}...`) — any backtick literal containing
|
|
466
|
+
// a ${...} expression passed to execSync (or its aliases).
|
|
467
|
+
const inScopeForExecSyncCheck =
|
|
468
|
+
/(?:^|\/)scripts\/hooks\//.test(file.path) ||
|
|
469
|
+
/(?:^|\/)lib\//.test(file.path);
|
|
470
|
+
if (inScopeForExecSyncCheck) {
|
|
471
|
+
const execSyncTemplateRe =
|
|
472
|
+
/\b(?:execSync|execAsync)\s*\(\s*`[^`]*\$\{[^`]*`/g;
|
|
473
|
+
let m;
|
|
474
|
+
while ((m = execSyncTemplateRe.exec(content)) !== null) {
|
|
475
|
+
const beforeMatch = content.substring(0, m.index);
|
|
476
|
+
const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
477
|
+
violations.push({
|
|
478
|
+
type: 'security',
|
|
479
|
+
severity: 'must-fix',
|
|
480
|
+
file: file.path,
|
|
481
|
+
line: lineNumber,
|
|
482
|
+
message:
|
|
483
|
+
'execSync with template-string command (contains ${...} interpolation) — ' +
|
|
484
|
+
'use execFileSync("bin", ["arg1", interpolatedVar]) instead (no shell layer). ' +
|
|
485
|
+
'Three review rounds have caught this exact pattern; mechanical now.',
|
|
486
|
+
rule: 'security-patterns.md §8 (R-379 standards-gate hardening)',
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
454
491
|
// 1. Raw JSON.parse — strengthened by Track B (2026-04-13).
|
|
455
492
|
// Original heuristic only flagged JSON.parse OUTSIDE try blocks. This missed
|
|
456
493
|
// SEC-001 (raw JSON.parse on user-config inside a try block — which loses the
|
|
@@ -191,7 +191,11 @@ class ClaudeCodeAdapter extends BaseAdapter {
|
|
|
191
191
|
case 'PostToolUse':
|
|
192
192
|
return this.transformPostToolUse(coreResult);
|
|
193
193
|
case 'Stop':
|
|
194
|
-
|
|
194
|
+
// SubagentStop intentionally omitted: not in CLAUDE_CODE_EVENTS
|
|
195
|
+
// (commented out at line 70), so generateConfig() never emits a
|
|
196
|
+
// hook entry for it. The fall-through case is unreachable by
|
|
197
|
+
// construction (F12 / R-379). If SubagentStop support is wanted,
|
|
198
|
+
// re-add to CLAUDE_CODE_EVENTS + HOOK_TIMEOUTS + this switch.
|
|
195
199
|
return this.transformStop(coreResult);
|
|
196
200
|
case 'SessionEnd':
|
|
197
201
|
return this.transformSessionEnd(coreResult);
|
|
@@ -537,7 +541,7 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
537
541
|
...(coreResult.message && { systemMessage: coreResult.message }),
|
|
538
542
|
hookSpecificOutput: {
|
|
539
543
|
hookEventName: 'TaskCreated',
|
|
540
|
-
linked: coreResult.linked
|
|
544
|
+
linked: coreResult.linked ?? false,
|
|
541
545
|
wogiTaskId: coreResult.wogiTaskId || null
|
|
542
546
|
}
|
|
543
547
|
};
|