wogiflow 2.22.1 → 2.22.3
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/wogi-claude +49 -6
- package/lib/wogi-claude-expect.exp +73 -0
- package/lib/workspace-messages.js +1 -0
- package/lib/workspace-worker-ready.js +190 -0
- package/package.json +2 -2
- package/scripts/hooks/core/overdue-dispatches.js +146 -14
- 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/lib/wogi-claude
CHANGED
|
@@ -17,13 +17,52 @@
|
|
|
17
17
|
# Opt-out: pass --no-wogi-restart to run claude once without the restart loop
|
|
18
18
|
#
|
|
19
19
|
# Environment:
|
|
20
|
-
# WOGI_RESTART_FLAG
|
|
21
|
-
# WOGI_MAX_RESTARTS
|
|
22
|
-
# WOGI_WRAPPER_PID
|
|
23
|
-
# WOGI_CLAUDE_BIN
|
|
20
|
+
# WOGI_RESTART_FLAG — path to restart-flag file (default: <cwd>/.workflow/state/restart-requested)
|
|
21
|
+
# WOGI_MAX_RESTARTS — safety cap, default 50 (prevents runaway restart storms)
|
|
22
|
+
# WOGI_WRAPPER_PID — exported to child; hook checks this to confirm wrapper is present
|
|
23
|
+
# WOGI_CLAUDE_BIN — override path to claude binary (default: found via PATH)
|
|
24
|
+
# WOGI_NO_EXPECT — set to 1 to disable expect-based auto-dismiss of the
|
|
25
|
+
# "Loading development channels" dialog (v2.22.3+).
|
|
26
|
+
# With expect enabled (default when expect is on PATH
|
|
27
|
+
# and --dangerously-load-development-channels is in args),
|
|
28
|
+
# the dialog is auto-accepted so workspace worker
|
|
29
|
+
# restarts require zero manual interaction.
|
|
30
|
+
# WOGI_EXPECT_TIMEOUT — override the expect timeout (default 30s) for watching
|
|
31
|
+
# the dialog. After timeout we hand off to the user
|
|
32
|
+
# unconditionally.
|
|
24
33
|
|
|
25
34
|
set -u
|
|
26
35
|
|
|
36
|
+
# --- Resolve helper paths (for expect-based dialog auto-dismiss) ---
|
|
37
|
+
WOGI_CLAUDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
WOGI_EXPECT_SCRIPT="$WOGI_CLAUDE_DIR/wogi-claude-expect.exp"
|
|
39
|
+
|
|
40
|
+
# Detect whether to use the expect wrapper. Two conditions must hold:
|
|
41
|
+
# 1. `expect` is on PATH and the wogi-claude-expect.exp script exists
|
|
42
|
+
# 2. The args include --dangerously-load-development-channels (which is the
|
|
43
|
+
# only flag that triggers the "Loading development channels" dialog we
|
|
44
|
+
# want to auto-dismiss)
|
|
45
|
+
# AND WOGI_NO_EXPECT is NOT set (escape hatch).
|
|
46
|
+
__wogi_use_expect=0
|
|
47
|
+
if [ -z "${WOGI_NO_EXPECT:-}" ] && command -v expect >/dev/null 2>&1 && [ -x "$WOGI_EXPECT_SCRIPT" ]; then
|
|
48
|
+
for arg in "$@"; do
|
|
49
|
+
if [ "$arg" = "--dangerously-load-development-channels" ]; then
|
|
50
|
+
__wogi_use_expect=1
|
|
51
|
+
break
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# run_claude — invoke claude, routing through expect when we can auto-dismiss
|
|
57
|
+
# the dev-channels dialog. Preserves stdin/stdout/stderr exactly.
|
|
58
|
+
run_claude() {
|
|
59
|
+
if [ "$__wogi_use_expect" -eq 1 ]; then
|
|
60
|
+
expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "$@"
|
|
61
|
+
else
|
|
62
|
+
"$CLAUDE_BIN" "$@"
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
# --- Opt-out path: no restart loop, just exec claude once ---
|
|
28
67
|
for arg in "$@"; do
|
|
29
68
|
if [ "$arg" = "--no-wogi-restart" ]; then
|
|
@@ -31,7 +70,11 @@ for arg in "$@"; do
|
|
|
31
70
|
filtered=()
|
|
32
71
|
for a in "$@"; do [ "$a" = "--no-wogi-restart" ] || filtered+=("$a"); done
|
|
33
72
|
CLAUDE_BIN="${WOGI_CLAUDE_BIN:-claude}"
|
|
34
|
-
|
|
73
|
+
if [ "$__wogi_use_expect" -eq 1 ]; then
|
|
74
|
+
exec expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "${filtered[@]}"
|
|
75
|
+
else
|
|
76
|
+
exec "$CLAUDE_BIN" "${filtered[@]}"
|
|
77
|
+
fi
|
|
35
78
|
fi
|
|
36
79
|
done
|
|
37
80
|
|
|
@@ -66,7 +109,7 @@ while true; do
|
|
|
66
109
|
rm -f "$FLAG_FILE"
|
|
67
110
|
fi
|
|
68
111
|
|
|
69
|
-
|
|
112
|
+
run_claude "$@"
|
|
70
113
|
inner_exit=$?
|
|
71
114
|
|
|
72
115
|
# If the user hit Ctrl+C (INT/TERM reached us), don't restart
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env expect
|
|
2
|
+
#
|
|
3
|
+
# wogi-claude-expect.exp — Auto-dismiss Claude Code's "Loading development
|
|
4
|
+
# channels" dialog so workspace worker restarts are truly seamless.
|
|
5
|
+
#
|
|
6
|
+
# Claude Code fires an interactive Ink dialog EVERY session when:
|
|
7
|
+
# - User is OAuth-authenticated to claude.ai
|
|
8
|
+
# - The CLI is launched with --dangerously-load-development-channels
|
|
9
|
+
#
|
|
10
|
+
# There's no Claude Code setting that persists an "accepted" state
|
|
11
|
+
# (verified via decompiled source 2026-04-17). So we intercept the dialog
|
|
12
|
+
# at the wrapper level: spawn claude in an expect-managed PTY, watch for
|
|
13
|
+
# the dialog title text, send Enter to accept the already-highlighted
|
|
14
|
+
# "I am using this for local development" option, then hand off control
|
|
15
|
+
# to the user via `interact`.
|
|
16
|
+
#
|
|
17
|
+
# Usage (invoked from lib/wogi-claude):
|
|
18
|
+
# expect wogi-claude-expect.exp /absolute/path/to/claude [args...]
|
|
19
|
+
#
|
|
20
|
+
# Disable at runtime: set WOGI_NO_EXPECT=1. The wrapper then execs claude
|
|
21
|
+
# directly and the user sees the dialog as before (manual single-click).
|
|
22
|
+
|
|
23
|
+
set timeout 30
|
|
24
|
+
|
|
25
|
+
# Allow WOGI_EXPECT_TIMEOUT override (rarely needed)
|
|
26
|
+
if {[info exists env(WOGI_EXPECT_TIMEOUT)]} {
|
|
27
|
+
set timeout $env(WOGI_EXPECT_TIMEOUT)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Mirror claude's output to the user's terminal during dialog watch
|
|
31
|
+
log_user 1
|
|
32
|
+
|
|
33
|
+
# argv[0] is the claude binary path; rest are claude's args
|
|
34
|
+
if {[llength $argv] < 1} {
|
|
35
|
+
puts stderr "wogi-claude-expect.exp: missing claude binary path"
|
|
36
|
+
exit 2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set claude_bin [lindex $argv 0]
|
|
40
|
+
set claude_args [lrange $argv 1 end]
|
|
41
|
+
|
|
42
|
+
# Spawn claude in a pseudo-TTY so its Ink UI renders normally.
|
|
43
|
+
# eval is needed because claude_args is a list and spawn expects a
|
|
44
|
+
# flattened command line.
|
|
45
|
+
eval spawn $claude_bin $claude_args
|
|
46
|
+
|
|
47
|
+
# Watch for the DevChannels dialog title, then press Enter to accept
|
|
48
|
+
# the default-highlighted "I am using this for local development" option.
|
|
49
|
+
#
|
|
50
|
+
# On timeout or EOF: fall through to `interact`. If the dialog appears
|
|
51
|
+
# AFTER our timeout window, the user can still answer it manually —
|
|
52
|
+
# same failure mode as running claude directly.
|
|
53
|
+
expect {
|
|
54
|
+
-re "Loading development channels" {
|
|
55
|
+
# Let Ink finish rendering the dialog before sending Enter.
|
|
56
|
+
# Without this, the select-input component may not have bound
|
|
57
|
+
# its keyboard listener yet and the keystroke is dropped.
|
|
58
|
+
after 250
|
|
59
|
+
send "\r"
|
|
60
|
+
}
|
|
61
|
+
timeout { }
|
|
62
|
+
eof { exit }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Hand off: user's keystrokes flow to claude, claude's output flows
|
|
66
|
+
# to the user's terminal. interact blocks until claude exits.
|
|
67
|
+
interact
|
|
68
|
+
|
|
69
|
+
# After claude exits, let the bash wrapper decide whether to restart.
|
|
70
|
+
# Pass through claude's exit status (expect sets it in $expect_out(-code)
|
|
71
|
+
# after `interact`, but a plain exit is sufficient since the wrapper
|
|
72
|
+
# only cares about the restart flag file, not exit code).
|
|
73
|
+
exit
|
|
@@ -23,6 +23,7 @@ const MESSAGE_TYPES = [
|
|
|
23
23
|
'bug-report', // "Your endpoint returns 500 when I send Y"
|
|
24
24
|
'task-complete', // "I finished my side of feature Z"
|
|
25
25
|
'worker-stopped', // Graceful Stop hook — worker session ending, not necessarily at task completion
|
|
26
|
+
'worker-ready', // Fresh worker session with empty queue — "got anything for me?" (wf-restart-handoff)
|
|
26
27
|
'needs-help', // "I'm stuck, can you check X on your side?"
|
|
27
28
|
'heads-up', // "I'm about to change Y, just FYI"
|
|
28
29
|
'impact-query', // Pre-dev: "I'm about to change X, will this break you?"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Workspace — Worker Readiness Announce (restart-handoff protocol)
|
|
5
|
+
*
|
|
6
|
+
* Problem this solves:
|
|
7
|
+
* When a worker session restarts (wogi-claude wrapper relaunches claude
|
|
8
|
+
* after a task-boundary), channel dispatches sent by the manager during
|
|
9
|
+
* the restart window can be lost — they arrive either while the old
|
|
10
|
+
* claude is shutting down or before the new claude has wired up its
|
|
11
|
+
* MCP channel, and the notification never reaches a live session.
|
|
12
|
+
*
|
|
13
|
+
* Previous sessions observed: manager dispatches session N, worker
|
|
14
|
+
* completes N and restarts, manager tries to dispatch N+1 during the
|
|
15
|
+
* restart gap, N+1 is lost, worker comes up fresh with empty queue
|
|
16
|
+
* and sits idle until the user notices and the manager re-dispatches.
|
|
17
|
+
*
|
|
18
|
+
* Design:
|
|
19
|
+
* File-based announce via the workspace-messages bus. When a worker
|
|
20
|
+
* SessionStart fires and the worker has zero in-progress tasks and
|
|
21
|
+
* zero queued channel dispatches, write a structured `worker-ready`
|
|
22
|
+
* message to `.workspace/messages/`. The manager's next turn sweeps
|
|
23
|
+
* the bus, cross-references the dispatched-tasks.json (wf-d3e67abe)
|
|
24
|
+
* to see if anything is owed to this worker, and surfaces lost
|
|
25
|
+
* dispatches for re-dispatch.
|
|
26
|
+
*
|
|
27
|
+
* File-based delivery is durable: no timing games, no buffer TTL,
|
|
28
|
+
* no dependency on the MCP channel server being up during the
|
|
29
|
+
* restart gap. Worker writes → manager reads → reconciles.
|
|
30
|
+
*
|
|
31
|
+
* Dedup:
|
|
32
|
+
* If a pending `worker-ready` message already exists for this repo,
|
|
33
|
+
* we skip — no need to stack announcements while the manager hasn't
|
|
34
|
+
* picked up the first one yet.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const path = require('node:path');
|
|
39
|
+
const { safeReadJson } = require('./utils');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect if the current process is a workspace worker (not a manager and not a
|
|
43
|
+
* single-repo session). Mirrors the isWorkspaceWorker detection used in
|
|
44
|
+
* scripts/hooks/core/task-completed.js.
|
|
45
|
+
*
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function isWorker() {
|
|
49
|
+
if (!process.env.WOGI_WORKSPACE_ROOT) return false;
|
|
50
|
+
const repo = process.env.WOGI_REPO_NAME;
|
|
51
|
+
if (!repo || repo === 'manager') return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the workspace root from env (worker mode).
|
|
57
|
+
*
|
|
58
|
+
* @returns {string|null}
|
|
59
|
+
*/
|
|
60
|
+
function getWorkspaceRoot() {
|
|
61
|
+
const root = process.env.WOGI_WORKSPACE_ROOT;
|
|
62
|
+
if (!root || !path.isAbsolute(root)) return null;
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check whether a pending worker-ready message already exists for this repo.
|
|
68
|
+
* Used to dedup — we don't need to stack announcements.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} workspaceRoot
|
|
71
|
+
* @param {string} repoName
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function hasPendingAnnounce(workspaceRoot, repoName) {
|
|
75
|
+
try {
|
|
76
|
+
const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
|
|
77
|
+
if (!fs.existsSync(messagesDir)) return false;
|
|
78
|
+
const files = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
try {
|
|
81
|
+
const msg = safeReadJson(path.join(messagesDir, file));
|
|
82
|
+
if (!msg) continue;
|
|
83
|
+
if (msg.type === 'worker-ready' &&
|
|
84
|
+
msg.from === repoName &&
|
|
85
|
+
msg.status === 'pending') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
} catch (_err) { /* skip malformed */ }
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
} catch (_err) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether this worker should announce ready.
|
|
98
|
+
* Preconditions:
|
|
99
|
+
* - Worker mode (WOGI_WORKSPACE_ROOT + WOGI_REPO_NAME !== 'manager')
|
|
100
|
+
* - ready.json has zero in-progress tasks
|
|
101
|
+
* - ready.json has zero queued channel-dispatched tasks
|
|
102
|
+
* (if it has queued work, the SessionStart hook auto-invokes
|
|
103
|
+
* /wogi-start instead of announcing — different branch)
|
|
104
|
+
* - No pending worker-ready message already exists for this repo
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} [opts] - override knobs for testing
|
|
107
|
+
* @param {string} [opts.workspaceRoot]
|
|
108
|
+
* @param {string} [opts.repoName]
|
|
109
|
+
* @param {Object} [opts.readyData]
|
|
110
|
+
* @returns {{announce: boolean, reason: string, repoName?: string, workspaceRoot?: string}}
|
|
111
|
+
*/
|
|
112
|
+
function shouldAnnounceReady(opts = {}) {
|
|
113
|
+
const workspaceRoot = opts.workspaceRoot || getWorkspaceRoot();
|
|
114
|
+
const repoName = opts.repoName || process.env.WOGI_REPO_NAME;
|
|
115
|
+
|
|
116
|
+
if (!workspaceRoot) return { announce: false, reason: 'no-workspace-root' };
|
|
117
|
+
if (!opts.repoName && !isWorker()) return { announce: false, reason: 'not-worker' };
|
|
118
|
+
if (!repoName || repoName === 'manager') return { announce: false, reason: 'not-worker' };
|
|
119
|
+
|
|
120
|
+
let readyData = opts.readyData;
|
|
121
|
+
if (!readyData) {
|
|
122
|
+
try {
|
|
123
|
+
const { PATHS } = require('../scripts/flow-utils');
|
|
124
|
+
const readyPath = path.join(PATHS.state, 'ready.json');
|
|
125
|
+
readyData = safeReadJson(readyPath, { ready: [], inProgress: [] });
|
|
126
|
+
} catch (_err) {
|
|
127
|
+
readyData = { ready: [], inProgress: [] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const inProgress = Array.isArray(readyData.inProgress) ? readyData.inProgress : [];
|
|
132
|
+
if (inProgress.length > 0) {
|
|
133
|
+
return { announce: false, reason: 'in-progress-not-empty' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const queuedChannel = (Array.isArray(readyData.ready) ? readyData.ready : [])
|
|
137
|
+
.filter(t => t && (
|
|
138
|
+
t.channelSource === 'wogi-workspace-channel' ||
|
|
139
|
+
t.dispatchedBy === 'workspace-manager' ||
|
|
140
|
+
(typeof t.source === 'string' && t.source.startsWith('workspace:'))
|
|
141
|
+
));
|
|
142
|
+
if (queuedChannel.length > 0) {
|
|
143
|
+
return { announce: false, reason: 'queued-channel-work-present' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (hasPendingAnnounce(workspaceRoot, repoName)) {
|
|
147
|
+
return { announce: false, reason: 'already-announced' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { announce: true, reason: 'ok', workspaceRoot, repoName };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Write a worker-ready message to the workspace-messages bus.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} workspaceRoot
|
|
157
|
+
* @param {string} repoName
|
|
158
|
+
* @returns {{written: boolean, messageId?: string, path?: string, reason?: string}}
|
|
159
|
+
*/
|
|
160
|
+
function announceWorkerReady(workspaceRoot, repoName) {
|
|
161
|
+
try {
|
|
162
|
+
const { createMessage, saveMessage } = require('./workspace-messages');
|
|
163
|
+
const msg = createMessage({
|
|
164
|
+
from: repoName,
|
|
165
|
+
to: 'manager',
|
|
166
|
+
type: 'worker-ready',
|
|
167
|
+
subject: `Worker ${repoName} ready — queue empty, awaiting dispatch`,
|
|
168
|
+
body: [
|
|
169
|
+
`Worker "${repoName}" has started a fresh session with an empty task queue.`,
|
|
170
|
+
`If you dispatched any tasks to this worker that were lost during the`,
|
|
171
|
+
`restart window, they can be re-dispatched now. No pending work detected`,
|
|
172
|
+
`in ready.json (zero inProgress, zero queued channel dispatches).`
|
|
173
|
+
].join('\n'),
|
|
174
|
+
priority: 'medium',
|
|
175
|
+
actionRequired: false
|
|
176
|
+
});
|
|
177
|
+
const filePath = saveMessage(workspaceRoot, msg);
|
|
178
|
+
return { written: true, messageId: msg.id, path: filePath };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { written: false, reason: `write-failed: ${err.message}` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
isWorker,
|
|
186
|
+
getWorkspaceRoot,
|
|
187
|
+
hasPendingAnnounce,
|
|
188
|
+
shouldAnnounceReady,
|
|
189
|
+
announceWorkerReady
|
|
190
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.3",
|
|
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-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/flow-story-gates.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-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/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.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",
|
|
@@ -108,10 +108,122 @@ function sweepAndReconcile(workspaceRoot) {
|
|
|
108
108
|
return reconciled;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Reconcile pending `worker-ready` messages (2.22.2 restart-handoff).
|
|
113
|
+
*
|
|
114
|
+
* A worker-ready message signals that a worker session started with an
|
|
115
|
+
* empty queue — possibly because a prior dispatch was lost during the
|
|
116
|
+
* wrapper's restart window. For each pending worker-ready:
|
|
117
|
+
* - Find pending dispatches to that repo in dispatched-tasks.json
|
|
118
|
+
* - If any found: they're likely the lost dispatches. Collect as
|
|
119
|
+
* `lostDispatches` for surface to the manager.
|
|
120
|
+
* - Mark the worker-ready message as acknowledged regardless — once
|
|
121
|
+
* the manager has seen it, there's nothing more to do with the
|
|
122
|
+
* same message. If another restart happens, a fresh worker-ready
|
|
123
|
+
* will be written.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} workspaceRoot
|
|
126
|
+
* @param {Object} [opts]
|
|
127
|
+
* @param {number} [opts.staleGraceMs=30000] — ignore dispatches newer than this
|
|
128
|
+
* (to avoid flagging just-sent dispatches still in flight).
|
|
129
|
+
* @returns {{acknowledged: number, lostDispatches: Array}}
|
|
130
|
+
*/
|
|
131
|
+
function reconcileWorkerReady(workspaceRoot, opts = {}) {
|
|
132
|
+
const staleGraceMs = Number.isFinite(opts.staleGraceMs) ? opts.staleGraceMs : 30000;
|
|
133
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
134
|
+
|
|
135
|
+
let readMessages, updateMessageStatus, readDispatches;
|
|
136
|
+
try {
|
|
137
|
+
const libMessages = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-messages.js');
|
|
138
|
+
const libTracking = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-dispatch-tracking.js');
|
|
139
|
+
const bus = require(libMessages);
|
|
140
|
+
readMessages = bus.readMessages;
|
|
141
|
+
updateMessageStatus = bus.updateMessageStatus;
|
|
142
|
+
const tracking = require(libTracking);
|
|
143
|
+
readDispatches = tracking.readDispatches;
|
|
144
|
+
} catch (_err) {
|
|
145
|
+
return { acknowledged: 0, lostDispatches: [] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let pendingReady = [];
|
|
149
|
+
try {
|
|
150
|
+
pendingReady = readMessages(workspaceRoot, { type: 'worker-ready', status: 'pending' });
|
|
151
|
+
} catch (_err) {
|
|
152
|
+
return { acknowledged: 0, lostDispatches: [] };
|
|
153
|
+
}
|
|
154
|
+
if (pendingReady.length === 0) return { acknowledged: 0, lostDispatches: [] };
|
|
155
|
+
|
|
156
|
+
let dispatches = [];
|
|
157
|
+
try {
|
|
158
|
+
dispatches = readDispatches(workspaceRoot).filter(r => r && r.status === 'pending');
|
|
159
|
+
} catch (_err) {
|
|
160
|
+
dispatches = [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lostDispatches = [];
|
|
164
|
+
let acknowledged = 0;
|
|
165
|
+
|
|
166
|
+
for (const msg of pendingReady) {
|
|
167
|
+
const repoName = msg.from;
|
|
168
|
+
if (!repoName) continue;
|
|
169
|
+
|
|
170
|
+
// Find pending dispatches to this repo that are older than the grace
|
|
171
|
+
// period (avoid race conditions with just-sent dispatches).
|
|
172
|
+
const candidates = dispatches.filter(r => {
|
|
173
|
+
if (r.repoName !== repoName) return false;
|
|
174
|
+
const dispatched = Date.parse(r.dispatchedAt || '');
|
|
175
|
+
if (!Number.isFinite(dispatched)) return false;
|
|
176
|
+
return (now - dispatched) > staleGraceMs;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
for (const c of candidates) {
|
|
180
|
+
lostDispatches.push({ ...c, workerReadyMsgId: msg.id });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Acknowledge the worker-ready message — we've processed it once.
|
|
184
|
+
// If the restart-loss recurs, a fresh worker-ready will be written.
|
|
185
|
+
try {
|
|
186
|
+
if (updateMessageStatus) {
|
|
187
|
+
updateMessageStatus(workspaceRoot, msg.id, 'acknowledged');
|
|
188
|
+
acknowledged++;
|
|
189
|
+
}
|
|
190
|
+
} catch (_err) { /* non-fatal */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { acknowledged, lostDispatches };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Format the lost-dispatches block for manager additionalContext.
|
|
198
|
+
*
|
|
199
|
+
* @param {Array} lost
|
|
200
|
+
* @returns {string|null}
|
|
201
|
+
*/
|
|
202
|
+
function formatLostDispatchesContext(lost) {
|
|
203
|
+
if (!Array.isArray(lost) || lost.length === 0) return null;
|
|
204
|
+
const lines = lost.map(r => {
|
|
205
|
+
const dispatchedAt = r.dispatchedAt || '?';
|
|
206
|
+
return `• ${r.taskId} → ${r.repoName} | dispatched ${dispatchedAt} | still pending after worker restart`;
|
|
207
|
+
});
|
|
208
|
+
return [
|
|
209
|
+
`━━━ LOST DISPATCHES — WORKER RESTARTED WITH EMPTY QUEUE (${lost.length}) ━━━`,
|
|
210
|
+
...lines,
|
|
211
|
+
'',
|
|
212
|
+
'A worker announced fresh readiness but these dispatches are still',
|
|
213
|
+
'pending. Likely lost during the wrapper\'s restart window. Re-dispatch',
|
|
214
|
+
'them now via dispatchToChannel(workspaceRoot, repoName, taskId).',
|
|
215
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
|
216
|
+
].join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
111
219
|
/**
|
|
112
220
|
* Build the overdue-dispatches additionalContext block, or return null
|
|
113
221
|
* when nothing to surface (non-manager, no workspace root, no overdue).
|
|
114
222
|
*
|
|
223
|
+
* Also handles worker-ready reconciliation (2.22.2) — if workers announced
|
|
224
|
+
* readiness and there are matching pending dispatches, include a lost-dispatch
|
|
225
|
+
* section so the manager can re-dispatch.
|
|
226
|
+
*
|
|
115
227
|
* @param {Object} [opts]
|
|
116
228
|
* @param {string} [opts.workspaceRoot] — override (primarily for tests)
|
|
117
229
|
* @param {number} [opts.now=Date.now()]
|
|
@@ -128,32 +240,52 @@ function buildOverdueContext(opts = {}) {
|
|
|
128
240
|
try { sweepAndReconcile(workspaceRoot); }
|
|
129
241
|
catch (_err) { /* fail-open */ }
|
|
130
242
|
|
|
243
|
+
// Reconcile worker-ready announcements. Surface any lost dispatches
|
|
244
|
+
// the manager should re-send.
|
|
245
|
+
let lostBlock = null;
|
|
246
|
+
try {
|
|
247
|
+
const { lostDispatches } = reconcileWorkerReady(workspaceRoot, { now: opts.now });
|
|
248
|
+
lostBlock = formatLostDispatchesContext(lostDispatches);
|
|
249
|
+
} catch (_err) { /* fail-open */ }
|
|
250
|
+
|
|
131
251
|
let overdue;
|
|
132
252
|
try {
|
|
133
253
|
const libPath = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-dispatch-tracking.js');
|
|
134
254
|
const { getOverdueDispatches } = require(libPath);
|
|
135
255
|
overdue = getOverdueDispatches(workspaceRoot, opts.now);
|
|
136
256
|
} catch (_err) {
|
|
137
|
-
|
|
257
|
+
// If dispatch-tracking is missing but we have lost-dispatches from
|
|
258
|
+
// worker-ready, still surface those.
|
|
259
|
+
return lostBlock;
|
|
138
260
|
}
|
|
139
261
|
|
|
140
|
-
if (!Array.isArray(overdue) || overdue.length === 0) return null;
|
|
262
|
+
if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock) return null;
|
|
141
263
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
264
|
+
const sections = [];
|
|
265
|
+
|
|
266
|
+
if (lostBlock) sections.push(lostBlock);
|
|
267
|
+
|
|
268
|
+
if (Array.isArray(overdue) && overdue.length > 0) {
|
|
269
|
+
const now = Number.isFinite(opts.now) ? opts.now : Date.now();
|
|
270
|
+
const lines = overdue.map(r => formatLine(r, now));
|
|
271
|
+
sections.push([
|
|
272
|
+
`━━━ OVERDUE WORKSPACE DISPATCHES (${overdue.length}) ━━━`,
|
|
273
|
+
...lines,
|
|
274
|
+
'',
|
|
275
|
+
'These workers may have died silently. Check worker terminals;',
|
|
276
|
+
'if dead, re-dispatch or mark failed. Records:',
|
|
277
|
+
' .workspace/state/dispatched-tasks.json',
|
|
278
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
|
279
|
+
].join('\n'));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return sections.length > 0 ? sections.join('\n\n') : null;
|
|
153
283
|
}
|
|
154
284
|
|
|
155
285
|
module.exports = {
|
|
156
286
|
isManagerSession,
|
|
157
287
|
buildOverdueContext,
|
|
158
|
-
sweepAndReconcile
|
|
288
|
+
sweepAndReconcile,
|
|
289
|
+
reconcileWorkerReady,
|
|
290
|
+
formatLostDispatchesContext
|
|
159
291
|
};
|
|
@@ -871,6 +871,23 @@ function formatContextForInjection(context) {
|
|
|
871
871
|
// Non-critical — history file may not exist; continue with normal context
|
|
872
872
|
}
|
|
873
873
|
|
|
874
|
+
// Workspace worker auto-resume (wf-restart-handoff / 2.22.2).
|
|
875
|
+
// CRITICAL priority — shown at the top so the model acts on it before
|
|
876
|
+
// anything else. Fires when a worker session starts with queued channel
|
|
877
|
+
// dispatches that were inherited from the prior (restarted) session.
|
|
878
|
+
if (ctx.workerAutoResume) {
|
|
879
|
+
output += `### Workspace Worker Auto-Resume\n`;
|
|
880
|
+
output += ctx.workerAutoResume + '\n\n';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Workspace worker readiness announcement (wf-restart-handoff / 2.22.2).
|
|
884
|
+
// Informational — worker started idle, announced readiness to manager.
|
|
885
|
+
// Manager will reconcile async; no immediate action required from the worker.
|
|
886
|
+
if (ctx.workerReadyAnnounce) {
|
|
887
|
+
output += `### Workspace Worker Ready\n`;
|
|
888
|
+
output += ctx.workerReadyAnnounce + '\n\n';
|
|
889
|
+
}
|
|
890
|
+
|
|
874
891
|
// CRITICAL: CLAUDE_CODE_SIMPLE mode warning (highest priority)
|
|
875
892
|
if (ctx.simpleModeWarning && ctx.simpleModeWarning.active) {
|
|
876
893
|
output += `### CLAUDE_CODE_SIMPLE Mode Detected\n`;
|
|
@@ -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' });
|