wogiflow 2.26.0 → 2.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -33,7 +33,7 @@ Reflection: "Have I introduced any bugs or regressions?"
|
|
|
33
33
|
|
|
34
34
|
1. Reflection: "Does this match what the user asked for?"
|
|
35
35
|
2. Close out all TodoWrite items for this task
|
|
36
|
-
3.
|
|
36
|
+
3. **Run `node node_modules/wogiflow/scripts/flow-done.js <taskId>`** — this is the ONLY supported way to complete a task. It runs quality gates, moves the task from `inProgress` → `recentlyCompleted`, writes the gate latch, and fires the task-boundary-restart Phase 1 marker. **Do NOT hand-edit `ready.json` to move the task** — that bypasses the CLI and silently disables: quality-gate verification, gate latch, and the task-boundary session restart. If `flow` is not on PATH in this environment, invoke it as `node node_modules/wogiflow/scripts/flow-done.js <taskId>` directly.
|
|
37
37
|
4. Registry maps auto-updated by `registryUpdate` quality gate (runs `flow registry-manager scan` on all active registries — app-map, function-map, api-map, schema-map, service-map)
|
|
38
38
|
5. If `config.webmcp.enabled` and UI files created: run `node node_modules/wogiflow/scripts/flow-webmcp-generator.js scan`
|
|
39
39
|
6. Commit: `feat: Complete wf-XXXXXXXX - [title]`
|
package/lib/wogi-claude
CHANGED
|
@@ -41,27 +41,68 @@ set -u
|
|
|
41
41
|
WOGI_CLAUDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
42
|
WOGI_EXPECT_SCRIPT="$WOGI_CLAUDE_DIR/wogi-claude-expect.exp"
|
|
43
43
|
|
|
44
|
-
# Detect whether to use the expect wrapper
|
|
45
|
-
#
|
|
46
|
-
# 1. WOGI_USE_EXPECT=1 is explicitly set (opt-in)
|
|
47
|
-
# 2. WOGI_NO_EXPECT is NOT set (legacy escape hatch still honored)
|
|
48
|
-
# 3. `expect` is on PATH and the wogi-claude-expect.exp script exists
|
|
49
|
-
# 4. The args include --dangerously-load-development-channels (the only
|
|
50
|
-
# flag that triggers the dialog we want to auto-dismiss)
|
|
44
|
+
# Detect whether to use the expect wrapper for auto-dismissing the
|
|
45
|
+
# --dangerously-load-development-channels modal.
|
|
51
46
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# opt-in
|
|
47
|
+
# Precedence (highest to lowest):
|
|
48
|
+
# 1. WOGI_NO_EXPECT=1 → always OFF (kill switch)
|
|
49
|
+
# 2. Workspace worker mode → ON automatically (headless, cannot Enter by hand)
|
|
50
|
+
# 3. WOGI_USE_EXPECT=1 → ON (explicit opt-in for interactive users)
|
|
51
|
+
# 4. Default → OFF (interactive users get the native Claude Code dialog)
|
|
52
|
+
#
|
|
53
|
+
# Worker auto-enable (v2.26.2): WOGI_WORKSPACE_ROOT + WOGI_REPO_NAME (worker
|
|
54
|
+
# side) are set by `flow workspace start` before spawning this wrapper, so
|
|
55
|
+
# detection here is reliable. Interactive users never set these vars, so their
|
|
56
|
+
# default remains opt-in — the v2.22.3 regression (expect's text match miss on
|
|
57
|
+
# Ink ANSI output) is bounded to users who explicitly asked for expect.
|
|
58
|
+
#
|
|
59
|
+
# The rewritten wogi-claude-expect.exp (v2.26.2) replaces the old brittle
|
|
60
|
+
# per-chunk text match with: rolling buffer + ANSI strip + bounded elapsed-time
|
|
61
|
+
# window. Misses fall back to the same failure mode as running claude without
|
|
62
|
+
# the wrapper (dialog stays up until someone presses Enter) — no unsafe blind
|
|
63
|
+
# keystrokes injected into server-mode Claude.
|
|
64
|
+
|
|
65
|
+
__wogi_is_worker=0
|
|
66
|
+
if [ -n "${WOGI_WORKSPACE_ROOT:-}" ] && [ -n "${WOGI_REPO_NAME:-}" ] && \
|
|
67
|
+
[ "${WOGI_REPO_NAME}" != "manager" ]; then
|
|
68
|
+
__wogi_is_worker=1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
__wogi_wants_expect=0
|
|
72
|
+
if [ -z "${WOGI_NO_EXPECT:-}" ]; then
|
|
73
|
+
if [ "$__wogi_is_worker" -eq 1 ] || [ "${WOGI_USE_EXPECT:-}" = "1" ]; then
|
|
74
|
+
__wogi_wants_expect=1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
56
78
|
__wogi_use_expect=0
|
|
57
|
-
if [ "$
|
|
58
|
-
|
|
79
|
+
if [ "$__wogi_wants_expect" -eq 1 ]; then
|
|
80
|
+
# The dialog only fires when --dangerously-load-development-channels is in
|
|
81
|
+
# argv; skip the expect dance otherwise.
|
|
82
|
+
__wogi_has_flag=0
|
|
59
83
|
for arg in "$@"; do
|
|
60
84
|
if [ "$arg" = "--dangerously-load-development-channels" ]; then
|
|
61
|
-
|
|
85
|
+
__wogi_has_flag=1
|
|
62
86
|
break
|
|
63
87
|
fi
|
|
64
88
|
done
|
|
89
|
+
if [ "$__wogi_has_flag" -eq 1 ]; then
|
|
90
|
+
if command -v expect >/dev/null 2>&1 && [ -x "$WOGI_EXPECT_SCRIPT" ]; then
|
|
91
|
+
__wogi_use_expect=1
|
|
92
|
+
if [ "$__wogi_is_worker" -eq 1 ]; then
|
|
93
|
+
echo "[wogi-claude] worker mode detected — auto-enabled expect-based dialog dismissal" >&2
|
|
94
|
+
fi
|
|
95
|
+
elif [ "$__wogi_is_worker" -eq 1 ]; then
|
|
96
|
+
# Headless worker + missing expect = the dialog WILL deadlock this
|
|
97
|
+
# worker on restart. Warn loudly so the operator can install expect,
|
|
98
|
+
# but still start claude (better than failing the worker outright).
|
|
99
|
+
echo "[wogi-claude] WARNING: worker mode detected (repo '${WOGI_REPO_NAME}') but 'expect' is not installed." >&2
|
|
100
|
+
echo "[wogi-claude] The --dangerously-load-development-channels dialog will block this worker on the next restart." >&2
|
|
101
|
+
echo "[wogi-claude] Install expect to enable headless auto-dismiss:" >&2
|
|
102
|
+
echo "[wogi-claude] macOS: brew install expect" >&2
|
|
103
|
+
echo "[wogi-claude] Debian/Ubuntu: apt install expect" >&2
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
65
106
|
fi
|
|
66
107
|
|
|
67
108
|
# run_claude — invoke claude, routing through expect when we can auto-dismiss
|
|
@@ -8,21 +8,38 @@
|
|
|
8
8
|
# - The CLI is launched with --dangerously-load-development-channels
|
|
9
9
|
#
|
|
10
10
|
# There's no Claude Code setting that persists an "accepted" state
|
|
11
|
-
# (verified via decompiled source 2026-04-17)
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# (verified via decompiled source 2026-04-17) and no --accept-dev-channels
|
|
12
|
+
# flag exists in `claude --help`.
|
|
13
|
+
#
|
|
14
|
+
# v2.22.x implementation (DEPRECATED): `-re "Loading development channels"`
|
|
15
|
+
# matched per-chunk output. Ink paints the dialog in fragmented writes
|
|
16
|
+
# interleaved with ANSI color codes, so the literal phrase rarely arrived
|
|
17
|
+
# in a single buffer and the regex missed. Workers deadlocked.
|
|
18
|
+
#
|
|
19
|
+
# v2.26.2 rewrite (this file) solves it with three properties:
|
|
20
|
+
# 1. Rolling buffer — accumulate every chunk into one growing string,
|
|
21
|
+
# match against the whole buffer on each iteration, not per-chunk.
|
|
22
|
+
# 2. ANSI strip — remove CSI / OSC escape sequences before matching,
|
|
23
|
+
# so the color-interleaved Ink output normalizes to plain text.
|
|
24
|
+
# 3. Bounded elapsed-time window — stop accumulating after
|
|
25
|
+
# WOGI_EXPECT_TIMEOUT seconds (default 30). Without this, a very
|
|
26
|
+
# chatty claude startup with no dialog would keep exp_continue'ing
|
|
27
|
+
# forever until the per-iteration timeout.
|
|
28
|
+
#
|
|
29
|
+
# NO BLIND FALLBACK. If the window elapses without matching, we hand off
|
|
30
|
+
# to interact unchanged. Sending a speculative \r to claude in server:
|
|
31
|
+
# mode mid-startup is not safe (server-mode input handling is not a
|
|
32
|
+
# standard REPL and could corrupt the handshake). Miss = same failure
|
|
33
|
+
# mode as running claude without this wrapper. Worker retries via
|
|
34
|
+
# wogi-claude's restart loop.
|
|
16
35
|
#
|
|
17
36
|
# Usage (invoked from lib/wogi-claude):
|
|
18
37
|
# expect wogi-claude-expect.exp /absolute/path/to/claude [args...]
|
|
19
38
|
#
|
|
20
39
|
# Disable at runtime: set WOGI_NO_EXPECT=1. The wrapper then execs claude
|
|
21
|
-
# directly and the user sees the dialog as before
|
|
40
|
+
# directly and the user sees the dialog as before.
|
|
22
41
|
|
|
23
42
|
set timeout 30
|
|
24
|
-
|
|
25
|
-
# Allow WOGI_EXPECT_TIMEOUT override (rarely needed)
|
|
26
43
|
if {[info exists env(WOGI_EXPECT_TIMEOUT)]} {
|
|
27
44
|
set timeout $env(WOGI_EXPECT_TIMEOUT)
|
|
28
45
|
}
|
|
@@ -40,34 +57,84 @@ set claude_bin [lindex $argv 0]
|
|
|
40
57
|
set claude_args [lrange $argv 1 end]
|
|
41
58
|
|
|
42
59
|
# Spawn claude in a pseudo-TTY so its Ink UI renders normally.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
60
|
+
# Use {*} list-splice rather than `eval spawn` — `eval` reparses its
|
|
61
|
+
# arguments as Tcl script, which lets an argument containing bracket syntax
|
|
62
|
+
# (e.g. `[exec attacker-cmd]`) escape to command execution. The splice form
|
|
63
|
+
# expands the list without reparsing.
|
|
64
|
+
spawn $claude_bin {*}$claude_args
|
|
46
65
|
|
|
47
|
-
#
|
|
48
|
-
# the default-highlighted "I am using this for local development" option.
|
|
66
|
+
# --- Dialog dismissal watch ---
|
|
49
67
|
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
68
|
+
# dialog_buf accumulates raw stdout chunks. After each chunk we strip ANSI
|
|
69
|
+
# escapes into `plain` and substring-search for the dialog title text. If
|
|
70
|
+
# found, we send Enter (accepts the default-highlighted "I am using this
|
|
71
|
+
# for local development" option). Otherwise we exp_continue until either
|
|
72
|
+
# the total elapsed time exceeds $timeout or EOF.
|
|
73
|
+
set dialog_buf ""
|
|
74
|
+
set start_ts [clock seconds]
|
|
75
|
+
|
|
53
76
|
expect {
|
|
54
|
-
-re "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
-re "(.+)" {
|
|
78
|
+
append dialog_buf $expect_out(1,string)
|
|
79
|
+
|
|
80
|
+
# Strip ANSI CSI sequences (colors, cursor moves): ESC [ ... letter
|
|
81
|
+
regsub -all {\x1b\[[0-9;?]*[a-zA-Z]} $dialog_buf "" plain
|
|
82
|
+
# Strip 8-bit CSI form (0x9B byte instead of ESC [), same terminator
|
|
83
|
+
regsub -all {\x9b[0-9;?]*[a-zA-Z]} $plain "" plain
|
|
84
|
+
# Strip OSC sequences (titles, hyperlinks): ESC ] ... BEL
|
|
85
|
+
regsub -all {\x1b\][^\x07]*\x07} $plain "" plain
|
|
86
|
+
# Strip ISO 2022 charset-selection sequences: ESC ( B, ESC ) 0, etc.
|
|
87
|
+
regsub -all {\x1b[\(\)\*\+\-\.\/][\x20-\x7e]} $plain "" plain
|
|
88
|
+
# Strip bare ESC that didn't belong to a recognized sequence
|
|
89
|
+
regsub -all {\x1b} $plain "" plain
|
|
90
|
+
|
|
91
|
+
if {[string first "Loading development channels" $plain] >= 0} {
|
|
92
|
+
# Let Ink finish rendering the select-input component before
|
|
93
|
+
# sending Enter — without this the keystroke can land before
|
|
94
|
+
# the keyboard listener binds and gets dropped.
|
|
95
|
+
after 250
|
|
96
|
+
send "\r"
|
|
97
|
+
# Fall through to interact — dialog is dismissed, user drives
|
|
98
|
+
# from here.
|
|
99
|
+
} else {
|
|
100
|
+
# Bound total accumulation by elapsed wall-clock time so we
|
|
101
|
+
# don't exp_continue forever on a chatty startup with no
|
|
102
|
+
# dialog.
|
|
103
|
+
set elapsed [expr {[clock seconds] - $start_ts}]
|
|
104
|
+
if {$elapsed < $timeout} {
|
|
105
|
+
# Cap buffer at 64KB to prevent runaway memory on a very
|
|
106
|
+
# long startup that never shows the dialog. Keep the tail
|
|
107
|
+
# half so any late-arriving title text still matches.
|
|
108
|
+
if {[string length $dialog_buf] > 65536} {
|
|
109
|
+
set dialog_buf [string range $dialog_buf 32768 end]
|
|
110
|
+
}
|
|
111
|
+
exp_continue
|
|
112
|
+
}
|
|
113
|
+
# Elapsed >= timeout: fall through to interact without
|
|
114
|
+
# dismissing. Same failure mode as running claude without
|
|
115
|
+
# this wrapper.
|
|
116
|
+
}
|
|
60
117
|
}
|
|
61
118
|
timeout { }
|
|
62
119
|
eof { exit }
|
|
63
120
|
}
|
|
64
121
|
|
|
65
|
-
# Hand off: user's keystrokes flow to claude, claude's output flows
|
|
66
|
-
#
|
|
67
|
-
|
|
122
|
+
# Hand off: user's keystrokes flow to claude, claude's output flows to
|
|
123
|
+
# the user's terminal. interact blocks until claude exits.
|
|
124
|
+
#
|
|
125
|
+
# Test hook: set WOGI_EXPECT_NO_INTERACT=1 to substitute `expect eof`.
|
|
126
|
+
# `interact` requires a real TTY on stdin; under node:test / CI with
|
|
127
|
+
# pipe-backed stdin it closes the PTY before our sent \r flushes to the
|
|
128
|
+
# child. The test harness sets this env var so the behavioral tests (dialog
|
|
129
|
+
# dismissal, ANSI fragmentation) can actually observe the child receiving
|
|
130
|
+
# the keystroke. Production callers MUST NOT set this — users need
|
|
131
|
+
# interact to drive claude after the dialog dismisses.
|
|
132
|
+
if {[info exists env(WOGI_EXPECT_NO_INTERACT)] && $env(WOGI_EXPECT_NO_INTERACT) eq "1"} {
|
|
133
|
+
expect eof
|
|
134
|
+
} else {
|
|
135
|
+
interact
|
|
136
|
+
}
|
|
68
137
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# after `interact`, but a plain exit is sufficient since the wrapper
|
|
72
|
-
# only cares about the restart flag file, not exit code).
|
|
138
|
+
# Pass claude's exit status — wrapper cares about the restart flag file,
|
|
139
|
+
# not exit code, so a plain exit suffices.
|
|
73
140
|
exit
|
package/package.json
CHANGED
|
@@ -50,6 +50,12 @@ const { getConfig, PATHS } = require('../../flow-utils');
|
|
|
50
50
|
const { safeJsonParse } = require('../../flow-io');
|
|
51
51
|
|
|
52
52
|
const PENDING_MARKER_FILE = 'task-just-completed';
|
|
53
|
+
const LAST_TRIGGERED_FILE = 'task-boundary-last-triggered';
|
|
54
|
+
// Window during which a recentlyCompleted[0] entry is considered "fresh
|
|
55
|
+
// enough" to retro-mark Phase 1 from the Stop hook. Large enough to cover
|
|
56
|
+
// a slow quality-gate run; small enough that a session opened hours later
|
|
57
|
+
// doesn't trigger a bogus restart.
|
|
58
|
+
const FRESHNESS_WINDOW_MS = 5 * 60 * 1000;
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
61
|
* Locate the pending-marker file path inside .workflow/state/.
|
|
@@ -59,6 +65,26 @@ function getPendingMarkerPath() {
|
|
|
59
65
|
return path.join(PATHS.state, PENDING_MARKER_FILE);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
function getLastTriggeredPath() {
|
|
69
|
+
return path.join(PATHS.state, LAST_TRIGGERED_FILE);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readLastTriggered() {
|
|
73
|
+
try {
|
|
74
|
+
return safeJsonParse(getLastTriggeredPath(), null);
|
|
75
|
+
} catch (_err) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeLastTriggered(taskId) {
|
|
81
|
+
try {
|
|
82
|
+
const p = getLastTriggeredPath();
|
|
83
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
84
|
+
fs.writeFileSync(p, JSON.stringify({ taskId, at: new Date().toISOString() }));
|
|
85
|
+
} catch (_err) { /* best effort — anti-replay is defense-in-depth */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
/**
|
|
63
89
|
* Phase 1 — mark that a task just completed and a restart is desired at the
|
|
64
90
|
* next Stop-hook boundary. Safe to call even when the feature is disabled;
|
|
@@ -196,6 +222,13 @@ function consumeAndTriggerRestart() {
|
|
|
196
222
|
return { triggered: false, reason: `sigterm-failed: ${err.message}` };
|
|
197
223
|
}
|
|
198
224
|
|
|
225
|
+
// Record anti-replay sentinel so the Stop-hook fallback in the NEW session
|
|
226
|
+
// (post-restart) doesn't retro-mark the same recentlyCompleted[0] and
|
|
227
|
+
// trigger a second restart.
|
|
228
|
+
if (markerPayload?.taskId) {
|
|
229
|
+
writeLastTriggered(markerPayload.taskId);
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
return {
|
|
200
233
|
triggered: true,
|
|
201
234
|
flagPath: pre.flagPath,
|
|
@@ -203,6 +236,72 @@ function consumeAndTriggerRestart() {
|
|
|
203
236
|
};
|
|
204
237
|
}
|
|
205
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Phase 1 fallback — called from the Stop hook BEFORE
|
|
241
|
+
* consumeAndTriggerRestart. Detects a freshly-completed task in
|
|
242
|
+
* recentlyCompleted and writes the pending marker if neither of the primary
|
|
243
|
+
* Phase 1 paths fired.
|
|
244
|
+
*
|
|
245
|
+
* Why this exists: the primary Phase 1 writers are (a) flow-done.js:604 when
|
|
246
|
+
* `flow done <taskId>` runs, and (b) task-completed.js:522 driven by Claude
|
|
247
|
+
* Code's TaskCompleted hook. Path (b) does not fire for /wogi-start workflow
|
|
248
|
+
* completions (TaskCompleted fires for Task-tool sub-agents only — the reason
|
|
249
|
+
* for the two-phase redesign above). Path (a) only fires if the agent runs
|
|
250
|
+
* `flow done`. Older phase docs quietly encouraged "move task to
|
|
251
|
+
* recentlyCompleted in ready.json" as a substitute for `flow done`, which
|
|
252
|
+
* silently disables the restart. This fallback catches that case: if a fresh
|
|
253
|
+
* completion is visible in ready.json but no marker exists, we write one so
|
|
254
|
+
* Phase 2 can do its job.
|
|
255
|
+
*
|
|
256
|
+
* Anti-replay: recentlyCompleted[0] survives the SIGTERM + wrapper restart
|
|
257
|
+
* cycle, so without a guard the Stop hook in the NEW session would see the
|
|
258
|
+
* same fresh completion and trigger a second restart. The
|
|
259
|
+
* task-boundary-last-triggered sentinel prevents that — it records the last
|
|
260
|
+
* taskId we triggered on, and we skip if the current fresh completion
|
|
261
|
+
* matches.
|
|
262
|
+
*
|
|
263
|
+
* @returns {{ marked: boolean, taskId?: string, reason?: string }}
|
|
264
|
+
*/
|
|
265
|
+
function ensurePhase1MarkedIfRecentlyCompleted() {
|
|
266
|
+
try {
|
|
267
|
+
if (hasPendingMarker()) {
|
|
268
|
+
return { marked: false, reason: 'marker-already-present' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const readyPath = path.join(PATHS.state, 'ready.json');
|
|
272
|
+
const ready = safeJsonParse(readyPath, null);
|
|
273
|
+
const recent = ready && Array.isArray(ready.recentlyCompleted)
|
|
274
|
+
? ready.recentlyCompleted[0]
|
|
275
|
+
: null;
|
|
276
|
+
if (!recent || typeof recent !== 'object' || !recent.id || !recent.completedAt) {
|
|
277
|
+
return { marked: false, reason: 'no-fresh-completion' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const completedTs = new Date(recent.completedAt).getTime();
|
|
281
|
+
if (!Number.isFinite(completedTs)) {
|
|
282
|
+
return { marked: false, reason: 'unparseable-completedAt' };
|
|
283
|
+
}
|
|
284
|
+
const ageMs = Date.now() - completedTs;
|
|
285
|
+
if (ageMs < 0 || ageMs > FRESHNESS_WINDOW_MS) {
|
|
286
|
+
return { marked: false, reason: 'stale-completion' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const lastTriggered = readLastTriggered();
|
|
290
|
+
if (lastTriggered?.taskId === recent.id) {
|
|
291
|
+
return { marked: false, reason: 'already-triggered-for-this-task' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = markRestartPending({
|
|
295
|
+
taskId: recent.id,
|
|
296
|
+
taskTitle: recent.title,
|
|
297
|
+
source: 'stop-hook-fallback'
|
|
298
|
+
});
|
|
299
|
+
return { marked: result.marked, taskId: recent.id, reason: result.reason };
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return { marked: false, reason: `fallback-error: ${err.message}` };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
206
305
|
/**
|
|
207
306
|
* Convenience: whether a pending marker currently exists. Diagnostic only.
|
|
208
307
|
* @returns {boolean}
|
|
@@ -219,6 +318,10 @@ module.exports = {
|
|
|
219
318
|
// Phase 1 — called from task-completion code paths
|
|
220
319
|
markRestartPending,
|
|
221
320
|
|
|
321
|
+
// Phase 1 fallback — called from the Stop hook entry BEFORE Phase 2,
|
|
322
|
+
// catches the case where flow-done didn't run and TaskCompleted didn't fire
|
|
323
|
+
ensurePhase1MarkedIfRecentlyCompleted,
|
|
324
|
+
|
|
222
325
|
// Phase 2 — called from the Stop hook entry
|
|
223
326
|
consumeAndTriggerRestart,
|
|
224
327
|
|
|
@@ -155,7 +155,33 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
155
155
|
// No-op unless task-just-completed marker exists AND feature is enabled
|
|
156
156
|
// AND wogi-claude wrapper env is present.
|
|
157
157
|
try {
|
|
158
|
-
const {
|
|
158
|
+
const {
|
|
159
|
+
consumeAndTriggerRestart,
|
|
160
|
+
hasPendingMarker,
|
|
161
|
+
ensurePhase1MarkedIfRecentlyCompleted
|
|
162
|
+
} = require('../../core/task-boundary-reset');
|
|
163
|
+
|
|
164
|
+
// Phase 1 fallback: if the task completed via a path that didn't write the
|
|
165
|
+
// marker (e.g., agent edited ready.json directly instead of running
|
|
166
|
+
// `flow done`, or TaskCompleted hook didn't fire), retro-mark here so
|
|
167
|
+
// Phase 2 below can consume it. Anti-replay sentinel prevents double-firing
|
|
168
|
+
// across the SIGTERM + wrapper restart cycle.
|
|
169
|
+
try {
|
|
170
|
+
const fallback = ensurePhase1MarkedIfRecentlyCompleted();
|
|
171
|
+
if (fallback.marked && process.env.DEBUG) {
|
|
172
|
+
console.error(`[Stop] Phase 1 fallback marked ${fallback.taskId}`);
|
|
173
|
+
} else if (!fallback.marked && fallback.reason !== 'marker-already-present' &&
|
|
174
|
+
fallback.reason !== 'no-fresh-completion' &&
|
|
175
|
+
fallback.reason !== 'stale-completion' &&
|
|
176
|
+
fallback.reason !== 'already-triggered-for-this-task' &&
|
|
177
|
+
process.env.DEBUG) {
|
|
178
|
+
console.error(`[Stop] Phase 1 fallback skipped: ${fallback.reason}`);
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (process.env.DEBUG) {
|
|
182
|
+
console.error(`[Stop] Phase 1 fallback error (fail-open): ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
159
185
|
|
|
160
186
|
// If we're about to restart, record the session in history FIRST so the
|
|
161
187
|
// new session can find the prior session's resume token. Use parsedInput
|