wogiflow 2.26.0 → 2.26.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.
@@ -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. Move task to recentlyCompleted in ready.json
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.26.0",
3
+ "version": "2.26.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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 { consumeAndTriggerRestart, hasPendingMarker } = require('../../core/task-boundary-reset');
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