wogiflow 2.22.0 → 2.22.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.
@@ -180,18 +180,18 @@ When epic creation adds 2+ stories to ready.json and `config.bulkOrchestrator.en
180
180
 
181
181
  Non-blocking if transition fails.
182
182
 
183
- ### Effort Level Optimization (Claude Code 2.1.72+)
183
+ ### Effort Level Optimization (Claude Code 2.1.72+, xhigh added 2.1.111+)
184
184
 
185
185
  After task level classification (L0-L3), set the reasoning effort level to optimize token usage:
186
186
 
187
187
  | Task Level | Effort | Rationale |
188
188
  |------------|--------|-----------|
189
- | L0 (Epic) | high | Complex planning, multi-file architecture |
189
+ | L0 (Epic) | high (xhigh on Opus 4.7 for deep architectural reasoning) | Complex planning, multi-file architecture |
190
190
  | L1 (Story) | high | Multi-criteria implementation |
191
191
  | L2 (Task) | medium | Standard 1-5 file changes |
192
192
  | L3 (Subtask) | low | Single file, trivial change |
193
193
 
194
- This is advisory Claude Code 2.1.72 simplified effort to low/medium/high (removed "max"). The AI should adjust its reasoning depth accordingly during implementation phases.
194
+ This is advisory. Claude Code's effort levels: `low` / `medium` / `high` are universal. Claude Code 2.1.111+ added `xhigh` (between high and max) and `max` as Opus 4.7-only levels — other models fall back to `high`. Use `/effort` interactively (slider as of 2.1.111) to switch mid-session. The AI should adjust reasoning depth during implementation phases accordingly.
195
195
 
196
196
  ### Task Checkpoints (when `config.proactiveCompaction.enabled`)
197
197
 
@@ -8,11 +8,38 @@ Run `./scripts/flow story "<title>"` to create a story.
8
8
 
9
9
  Load `agents/story-writer.md` for the full story format.
10
10
 
11
+ ## Anti-Deferral Rule (MANDATORY)
12
+
13
+ **Every item the user provides MUST become a work item** (criterion or sub-task). Never silently filter items. If you believe an item should be deferred, **ASK the user** — do not decide autonomously.
14
+
15
+ For multi-item inputs, the command output MUST include: **"All {N} items captured as {criteria|sub-tasks}."** If any item cannot be mapped, the "Unmapped" warning must be surfaced, not suppressed.
16
+
17
+ This rule applies equally to deep-decomposition mode and flat stories.
18
+
19
+ ## Specification-Quality Gates (wf-63c0f4cc)
20
+
21
+ Five P0 gates run automatically at creation time (all fail-open):
22
+
23
+ | Gate | Fires When | Effect |
24
+ |------|-----------|--------|
25
+ | 1. Long Input | input ≥40 lines OR ≥5 discrete items | routes to `/wogi-extract-review` |
26
+ | 2. Item Reconciliation | input has ≥3 discrete items | writes manifest, verifies coverage |
27
+ | 3. Consumer Impact | input contains refactor/rename/migrate/etc. | greps consumers, flags phased migration at ≥5 breaking |
28
+ | 4. Scope-Confidence | input mentions "new X" / "existing Y" / "the Z service" | audits assumptions → "Pending Clarifications" block |
29
+ | 5. Intent Bootstrap | IGR artifacts missing + not already scheduled | schedules background bootstrap via session-state.json |
30
+
31
+ Gates enforce **specification quality at creation time**; runtime-quality gates (wiring, typecheck, tests) remain `/wogi-start`'s job.
32
+
33
+ Config: `storyFlow.consumerImpactAnalysis`, `storyFlow.scopeConfidenceAudit`, `storyFlow.itemReconciliation`. All default-true.
34
+
11
35
  ## Options
12
36
 
13
37
  - `--deep` - Enable deep decomposition mode (auto-generate granular sub-tasks)
14
38
  - `--priority <P>` - Set priority P0-P4 (default: P2 from config)
15
39
  - `--json` - Output JSON for programmatic access
40
+ - `--skip-gates` - Skip all P0 gates (testing/debug only)
41
+ - `--bypass-long-input` - Skip Gate 1 (set by `/wogi-start` when it already routed long input)
42
+ - `--full-input <txt>` - Full user input for gates (when title is a summary)
16
43
 
17
44
  Examples:
18
45
  ```bash
@@ -408,6 +408,37 @@ await cancelTask('wf-123', 'superseded', false);
408
408
 
409
409
  - **Hardened "Open in editor" against command injection**: Security hardening for untrusted filenames. **Impact on WogiFlow**: Validates the same pattern in `.claude/rules/security/security-patterns.md` — external inputs going into shell commands must be validated. No WogiFlow code change needed.
410
410
 
411
+ ### Features in 2.1.111+
412
+
413
+ - **`xhigh` effort level for Opus 4.7**: New effort level sitting between `high` and `max`, available via `/effort`, `--effort`, and the model picker. Other models fall back to `high`. `/effort` now opens an interactive slider when called without arguments. **Impact on WogiFlow**: The effort-level mapping in `wogi-start.md` now acknowledges `xhigh`/`max` as Opus 4.7-only. For L0 epics running on Opus 4.7, users may prefer `xhigh` over `high` for deeper architectural reasoning — the mapping table documents this as an option. No code change needed; the mapping is advisory.
414
+
415
+ - **`/ultrareview` built-in command**: Claude Code now ships a native `/ultrareview` that runs parallel multi-agent analysis and critique in the cloud — invoke with no arguments to review the current branch, or `/ultrareview <PR#>` to fetch and review a specific GitHub PR. **Relationship to WogiFlow's review commands**: No collision (`wogi-*` prefix). How to choose:
416
+ - `/ultrareview` — cloud-side parallel multi-agent critique. Zero local setup. Best for standalone branch/PR reviews when you don't have peer models configured.
417
+ - `/wogi-peer-review` — uses the peer models you configured via `/wogi-models-setup` (local/BYO models). Best when you want specific perspectives (e.g., a different vendor's model) or offline/cost-controlled review.
418
+ - `/wogi-review` — single-reviewer code review wired into WogiFlow task state (findings logged to `last-review.json`, triaged via `/wogi-triage`). Best for in-flow review during task execution.
419
+ - `/wogi-review-fix` — auto-applies fixes from `/wogi-review` findings.
420
+ Users can combine them: run `/ultrareview` for a wide-angle cloud critique, then `/wogi-review` for task-linked findings.
421
+
422
+ - **`/less-permission-prompts` built-in skill**: Scans recent transcripts for common read-only Bash and MCP tool calls and proposes a prioritized allowlist for `.claude/settings.json`. **Relationship to WogiFlow**: Complementary to `computeLeanConfig()` in `lib/installer.js` — the installer produces a minimal allowlist at install time, while `/less-permission-prompts` tunes the allowlist based on actual session usage. Suggested workflow: after a few WogiFlow sessions, run `/less-permission-prompts` to prune redundant prompts. Future opportunity: surface this suggestion in `/wogi-health` output.
423
+
424
+ - **Auto-allow for read-only bash with globs and `cd <project-dir> &&` prefix**: Read-only commands like `ls *.ts` and commands starting with `cd <project-dir> &&` no longer trigger a permission prompt. **Impact on WogiFlow**: Reduces prompts during WogiFlow hook-driven validation (lint/typecheck) and user-driven exploration. Allowlist rules in `lib/installer.js` that duplicated these patterns are now redundant — minor cleanup opportunity (tracked, low priority). No action required; the installer's lean-config approach already avoids over-emitting.
425
+
426
+ - **Auto mode for Max subscribers on Opus 4.7**: Auto mode is now available for Max subscribers when using Opus 4.7, and no longer requires `--enable-auto-mode`. 2.1.112 fixed a "claude-opus-4-7 is temporarily unavailable" error in auto mode. **Impact on WogiFlow**: WogiFlow's model registry already lists Opus 4.7 (v2.22.0); auto-mode routing is orthogonal to WogiFlow's hybrid mode. Users on Max with Opus 4.7 benefit automatically.
427
+
428
+ - **`OTEL_LOG_RAW_API_BODIES` env var**: Emits full API request and response bodies as OpenTelemetry log events for debugging. **Impact on WogiFlow**: Useful when debugging hybrid mode (`/wogi-hybrid`) routing and peer-review (`/wogi-peer-review`) model calls — set this env var to see the exact payloads reaching each model. Complements WogiFlow's gate telemetry (`/wogi-gate-stats`) which tracks pass/catch/miss rates at a higher level. Set with: `export OTEL_LOG_RAW_API_BODIES=1`. Note: payloads may contain sensitive data — only enable in development.
429
+
430
+ - **Headless `--output-format stream-json` includes `plugin_errors` on init**: Plugin demotion errors (unsatisfied dependencies, conflicting versions) are now surfaced on the init event in headless mode. **WogiFlow opportunity**: `/wogi-health` could read this stream when running in CI/headless mode to flag plugin-registry issues before they cause silent failures. Tracked as an enhancement.
431
+
432
+ - **Opus 4.7 availability fix (2.1.112)**: Fixed a "claude-opus-4-7 is temporarily unavailable" error in auto mode. Aligned with WogiFlow v2.22.0 registry update. No WogiFlow code change needed.
433
+
434
+ - **Windows improvements**: `CLAUDE_ENV_FILE` and SessionStart hook environment files now apply on Windows (previously a no-op). Permission rules with drive-letter paths are now correctly root-anchored, and paths differing only by drive-letter case are recognized as the same path. **Impact on WogiFlow**: Windows users of WogiFlow's SessionStart hook can now configure environment variables via `CLAUDE_ENV_FILE`. Drive-letter-path permission rules generated by the installer now behave correctly. Automatic improvement after upgrade.
435
+
436
+ - **Miscellaneous UX**: Plan files named after the originating prompt (e.g. `fix-auth-race-snug-otter.md`), `/skills` menu supports sorting by estimated token count (press `t`), Ctrl+U clears the entire input buffer (Ctrl+Y restores), Ctrl+L forces a full redraw, and typo suggestions on near-miss subcommands. Documentation-only for WogiFlow.
437
+
438
+ - **Fixed "Unknown skill: commit" error**: Users without a custom `/commit` skill were seeing this error when Claude Code tried to invoke a non-existent built-in. **Impact on WogiFlow**: No WogiFlow-shipped `/commit` skill (commits go through `/wogi-finalize` and git commit instructions). Users benefit passively from the fix.
439
+
440
+ - **Reliability fixes (all automatic after upgrade)**: Terminal display tearing in iTerm2+tmux, `@`-file suggestions re-scanning entire project in non-git directories, LSP diagnostics from before an edit appearing after it, tab-completing `/resume` behavior, `/context` grid rendering, `/clear` dropping session name, spurious decompression/network/transient errors in the TUI. Reverted v2.1.110 cap on non-streaming fallback retries (now uncapped again). Fixed Bedrock/Vertex/Foundry 429 retries pointing users at the wrong status page, bare URLs unclickable when wrapped in tool output, feedback surveys appearing back-to-back. WogiFlow sessions benefit from all of these automatically.
441
+
411
442
  ### Simple Mode Naming Distinction
412
443
 
413
444
  Claude Code's `CLAUDE_CODE_SIMPLE` environment variable (which enables a simplified tool set) is **unrelated** to WogiFlow's `loops.simpleMode` (a lightweight task completion loop using string detection). They are separate features that happen to share the word "simple":
@@ -542,4 +573,4 @@ Run `/keybindings` in Claude Code to customize your shortcuts.
542
573
 
543
574
  ---
544
575
 
545
- *Last updated: 2026-04-16*
576
+ *Last updated: 2026-04-17*
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Workspace — Dispatch Tracking (wf-d3e67abe)
5
+ *
6
+ * Silent-worker-halt detection via file-based dispatch records.
7
+ *
8
+ * Manager records every dispatch; any pending dispatch past its
9
+ * expectedDeadline without a matching completion/stop message =
10
+ * silent death. Surfaced on the next manager turn via the
11
+ * UserPromptSubmit hook (no background processes).
12
+ *
13
+ * State file: .workspace/state/dispatched-tasks.json
14
+ * Ring buffer of last MAX_ACTIVE records; older overflow to
15
+ * .workspace/state/dispatched-tasks.archive.jsonl (append-only).
16
+ */
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+ const { safeReadJson } = require('./utils');
21
+
22
+ const DEFAULT_DURATION_MS = 30 * 60 * 1000; // 30 min — matches waitForCompletion default
23
+ const MAX_ACTIVE = 100;
24
+ const SCHEMA_VERSION = 1;
25
+
26
+ const VALID_STATUSES = new Set(['pending', 'completed', 'graceful-stop', 'silent-halt']);
27
+
28
+ function stateFilePath(workspaceRoot) {
29
+ return path.join(workspaceRoot, '.workspace', 'state', 'dispatched-tasks.json');
30
+ }
31
+
32
+ function archiveFilePath(workspaceRoot) {
33
+ return path.join(workspaceRoot, '.workspace', 'state', 'dispatched-tasks.archive.jsonl');
34
+ }
35
+
36
+ function loadState(workspaceRoot) {
37
+ const data = safeReadJson(stateFilePath(workspaceRoot), null);
38
+ if (data && typeof data === 'object' && Array.isArray(data.dispatches)) {
39
+ return { version: data.version || SCHEMA_VERSION, dispatches: data.dispatches };
40
+ }
41
+ return { version: SCHEMA_VERSION, dispatches: [] };
42
+ }
43
+
44
+ function saveState(workspaceRoot, state) {
45
+ const filePath = stateFilePath(workspaceRoot);
46
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
47
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
48
+ }
49
+
50
+ function archiveRecord(workspaceRoot, record) {
51
+ const filePath = archiveFilePath(workspaceRoot);
52
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
+ fs.appendFileSync(filePath, JSON.stringify(record) + '\n');
54
+ }
55
+
56
+ /**
57
+ * Record a dispatch. Appends to state and trims ring buffer.
58
+ *
59
+ * @param {string} workspaceRoot
60
+ * @param {Object} params
61
+ * @param {string} params.taskId - wf-XXXXXXXX
62
+ * @param {string} params.repoName
63
+ * @param {number} [params.expectedDurationMs=DEFAULT_DURATION_MS]
64
+ * @param {string} [params.dispatchedBy='manager']
65
+ * @returns {Object} the created record
66
+ */
67
+ function recordDispatch(workspaceRoot, { taskId, repoName, expectedDurationMs, dispatchedBy }) {
68
+ if (!workspaceRoot || typeof workspaceRoot !== 'string') {
69
+ throw new Error('workspaceRoot required');
70
+ }
71
+ if (!/^wf-[0-9a-f]{8}$/i.test(taskId || '')) {
72
+ throw new Error(`Invalid taskId: ${taskId}`);
73
+ }
74
+ if (!repoName || typeof repoName !== 'string') {
75
+ throw new Error('repoName required');
76
+ }
77
+
78
+ const durationMs = Number.isFinite(expectedDurationMs) && expectedDurationMs > 0
79
+ ? expectedDurationMs
80
+ : DEFAULT_DURATION_MS;
81
+ const now = Date.now();
82
+ const dispatchedAt = new Date(now).toISOString();
83
+ const expectedDeadline = new Date(now + durationMs).toISOString();
84
+
85
+ const record = {
86
+ taskId,
87
+ repoName,
88
+ dispatchedAt,
89
+ expectedDeadline,
90
+ expectedDurationMs: durationMs,
91
+ status: 'pending',
92
+ dispatchedBy: dispatchedBy || 'manager',
93
+ reconciledAt: null,
94
+ reconciledReason: null
95
+ };
96
+
97
+ const state = loadState(workspaceRoot);
98
+ state.dispatches.push(record);
99
+
100
+ // Ring buffer: overflow oldest records to archive
101
+ while (state.dispatches.length > MAX_ACTIVE) {
102
+ const overflow = state.dispatches.shift();
103
+ try { archiveRecord(workspaceRoot, overflow); }
104
+ catch (_err) { /* non-fatal — archive is best-effort */ }
105
+ }
106
+
107
+ saveState(workspaceRoot, state);
108
+ return record;
109
+ }
110
+
111
+ /**
112
+ * Reconcile the most recent pending record for a task.
113
+ *
114
+ * @param {string} workspaceRoot
115
+ * @param {string} taskId
116
+ * @param {string} status - 'completed' | 'graceful-stop' | 'silent-halt'
117
+ * @param {string} [reason]
118
+ * @returns {Object|null} updated record, or null if not found
119
+ */
120
+ function reconcileDispatch(workspaceRoot, taskId, status, reason) {
121
+ if (!VALID_STATUSES.has(status) || status === 'pending') {
122
+ throw new Error(`Invalid reconcile status: ${status}`);
123
+ }
124
+ const state = loadState(workspaceRoot);
125
+ // Find most recent pending record for this taskId (last wins — most recent dispatch)
126
+ for (let i = state.dispatches.length - 1; i >= 0; i--) {
127
+ const r = state.dispatches[i];
128
+ if (r && r.taskId === taskId && r.status === 'pending') {
129
+ r.status = status;
130
+ r.reconciledAt = new Date().toISOString();
131
+ r.reconciledReason = reason || null;
132
+ saveState(workspaceRoot, state);
133
+ return r;
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * Read all currently-active dispatch records (not archived).
141
+ *
142
+ * @param {string} workspaceRoot
143
+ * @returns {Array<Object>}
144
+ */
145
+ function readDispatches(workspaceRoot) {
146
+ return loadState(workspaceRoot).dispatches;
147
+ }
148
+
149
+ /**
150
+ * Get dispatches whose expectedDeadline has passed and are still pending.
151
+ *
152
+ * @param {string} workspaceRoot
153
+ * @param {number} [now=Date.now()]
154
+ * @returns {Array<Object>} overdue records
155
+ */
156
+ function getOverdueDispatches(workspaceRoot, now) {
157
+ const ts = Number.isFinite(now) ? now : Date.now();
158
+ const dispatches = readDispatches(workspaceRoot);
159
+ return dispatches.filter(r => {
160
+ if (!r || r.status !== 'pending') return false;
161
+ const deadline = Date.parse(r.expectedDeadline || '');
162
+ return Number.isFinite(deadline) && deadline < ts;
163
+ });
164
+ }
165
+
166
+ module.exports = {
167
+ DEFAULT_DURATION_MS,
168
+ MAX_ACTIVE,
169
+ recordDispatch,
170
+ reconcileDispatch,
171
+ readDispatches,
172
+ getOverdueDispatches,
173
+ stateFilePath,
174
+ archiveFilePath
175
+ };
@@ -22,6 +22,8 @@ const MESSAGE_TYPES = [
22
22
  'question', // "Does your side handle X?"
23
23
  'bug-report', // "Your endpoint returns 500 when I send Y"
24
24
  'task-complete', // "I finished my side of feature Z"
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)
25
27
  'needs-help', // "I'm stuck, can you check X on your side?"
26
28
  'heads-up', // "I'm about to change Y, just FYI"
27
29
  'impact-query', // Pre-dev: "I'm about to change X, will this break you?"
@@ -700,6 +700,8 @@ function checkWorkerHealth(port) {
700
700
  * @param {string} taskId — task ID to start
701
701
  * @param {Object} [opts] — dispatch options
702
702
  * @param {string} [opts.effortLevel] — reasoning effort to propagate ('low'|'medium'|'high')
703
+ * @param {number} [opts.expectedDurationMs] — override deadline for silent-halt detection
704
+ * (wf-d3e67abe). Defaults to DEFAULT_DURATION_MS from workspace-dispatch-tracking.
703
705
  * @returns {Promise<{ ok: boolean, message: string }>}
704
706
  */
705
707
  async function dispatchToChannel(workspaceRoot, repoName, taskId, opts = {}) {
@@ -739,6 +741,21 @@ async function dispatchToChannel(workspaceRoot, repoName, taskId, opts = {}) {
739
741
  const dispatchBody = `${effortPrefix}/wogi-start ${taskId}`;
740
742
  const result = await httpPost('127.0.0.1', port, dispatchBody);
741
743
  if (result.ok) {
744
+ // wf-d3e67abe — record dispatch for silent-halt detection.
745
+ // Fail-open: if tracking write fails, dispatch itself still succeeds.
746
+ try {
747
+ const { recordDispatch } = require('./workspace-dispatch-tracking');
748
+ recordDispatch(workspaceRoot, {
749
+ taskId,
750
+ repoName,
751
+ expectedDurationMs: opts.expectedDurationMs,
752
+ dispatchedBy: process.env.WOGI_REPO_NAME || 'manager'
753
+ });
754
+ } catch (err) {
755
+ if (process.env.DEBUG) {
756
+ console.error(`[dispatchToChannel] Dispatch tracking failed (non-fatal): ${err.message}`);
757
+ }
758
+ }
742
759
  return { ok: true, message: `Dispatched /wogi-start ${taskId} to ${repoName} (port ${port})${effortLevel ? ` [effort: ${effortLevel}]` : ''}` };
743
760
  }
744
761
 
@@ -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.0",
3
+ "version": "2.22.2",
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 && 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 && 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",
@@ -328,6 +328,15 @@ const CONFIG_DEFAULTS = {
328
328
  supportEpics: true,
329
329
  propagateProgress: true
330
330
  },
331
+
332
+ // --- Story Creation Quality Gates (wf-63c0f4cc) ---
333
+ // Specification-quality gates enforced at story CREATION time. Execution-
334
+ // quality gates (runtime wiring, typecheck, tests) remain /wogi-start's job.
335
+ storyFlow: {
336
+ consumerImpactAnalysis: { enabled: true, breakingThreshold: 5 },
337
+ scopeConfidenceAudit: { enabled: true },
338
+ itemReconciliation: { enabled: true, minItems: 3 }
339
+ },
331
340
  epics: { enabled: false },
332
341
 
333
342
  // --- Specification & Questions ---