wogiflow 2.26.2 → 2.29.0

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.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -16,22 +16,38 @@
16
16
  # interleaved with ANSI color codes, so the literal phrase rarely arrived
17
17
  # in a single buffer and the regex missed. Workers deadlocked.
18
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.
19
+ # v2.26.2 rewrite (DEPRECATED): rolling buffer + ANSI strip + timed window.
20
+ # Fixed the per-chunk brittleness but introduced a WORSE bug: the
21
+ # `expect { -re ".+" {...} }` watch-loop OWNS stdin while it runs. If the
22
+ # dialog title text didn't match (e.g., Ink rendered differently in a new
23
+ # Claude Code version, terminal line-wrapped mid-phrase, or the user's
24
+ # terminal injected extra escape sequences), user keystrokes during the
25
+ # 30-second watch window were captured by expect and NEVER forwarded to
26
+ # claude. The dialog appeared frozen Enter did nothing, letters went to
27
+ # expect's buffer and were echoed to stray terminal positions. Reported
28
+ # 2026-04-22.
28
29
  #
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.
30
+ # v2.26.3 (this file) solves both failure modes by using expect's
31
+ # `interact -o` pattern instead of watch-then-handoff:
32
+ #
33
+ # 1. `interact` starts immediately after spawn. From second zero, user
34
+ # stdin is forwarded to claude ZERO stdin capture.
35
+ # 2. The `-o -re <pattern>` trigger watches CHILD output for the dialog
36
+ # phrase while stdin flows freely. When matched, we inject \r to
37
+ # accept the default-highlighted "I am using this for local
38
+ # development" option.
39
+ # 3. Graceful fallback: if the pattern never matches (new CC dialog
40
+ # text, terminal weirdness), the user simply sees the dialog and
41
+ # presses 1+Enter themselves. No 30-second stdin black hole. Worst
42
+ # case = same as running claude without this wrapper. Best case =
43
+ # zero-click dismiss.
44
+ # 4. A `_wogi_dismissed` flag prevents re-firing Enter if the phrase
45
+ # reappears later in chat output (paranoid safety).
46
+ #
47
+ # Pattern tolerance: `Loading.{0,20}development.{0,20}channels` matches
48
+ # the phrase with up to 20 arbitrary bytes between each word, covering
49
+ # ANSI color codes Ink inserts mid-rendering without requiring explicit
50
+ # ANSI stripping (interact doesn't natively support it).
35
51
  #
36
52
  # Usage (invoked from lib/wogi-claude):
37
53
  # expect wogi-claude-expect.exp /absolute/path/to/claude [args...]
@@ -44,6 +60,23 @@ if {[info exists env(WOGI_EXPECT_TIMEOUT)]} {
44
60
  set timeout $env(WOGI_EXPECT_TIMEOUT)
45
61
  }
46
62
 
63
+ # wf-8294d960: env-guarded boot-latency instrumentation. No effect unless
64
+ # WOGI_DEBUG_BOOT=1. Timestamps print to stderr so users can capture with 2>&1.
65
+ set _wogi_boot_debug 0
66
+ if {[info exists env(WOGI_DEBUG_BOOT)] && $env(WOGI_DEBUG_BOOT) eq "1"} {
67
+ set _wogi_boot_debug 1
68
+ }
69
+ set _wogi_boot_t0 [clock milliseconds]
70
+ proc _wogi_boot_mark {label} {
71
+ global _wogi_boot_debug _wogi_boot_t0
72
+ if {$_wogi_boot_debug} {
73
+ set ms [expr {[clock milliseconds] - $_wogi_boot_t0}]
74
+ puts stderr [format "\[boot-latency\] +%6dms expect:%s" $ms $label]
75
+ }
76
+ }
77
+
78
+ _wogi_boot_mark "expect script start"
79
+
47
80
  # Mirror claude's output to the user's terminal during dialog watch
48
81
  log_user 1
49
82
 
@@ -61,78 +94,82 @@ set claude_args [lrange $argv 1 end]
61
94
  # arguments as Tcl script, which lets an argument containing bracket syntax
62
95
  # (e.g. `[exec attacker-cmd]`) escape to command execution. The splice form
63
96
  # expands the list without reparsing.
97
+ _wogi_boot_mark "before spawn"
64
98
  spawn $claude_bin {*}$claude_args
99
+ _wogi_boot_mark "after spawn (pid=$spawn_id)"
65
100
 
66
- # --- Dialog dismissal watch ---
67
- #
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]
101
+ # ============================================================================
102
+ # Test-mode branch (WOGI_EXPECT_NO_INTERACT=1): use the v2.26.2 expect +
103
+ # rolling-buffer + ANSI-strip approach, followed by `expect eof`. Behavioural
104
+ # tests can observe the child receiving our \r keystroke via a fake claude
105
+ # that reads stdin and logs; interact would require a real TTY and CI stdin
106
+ # pipes break it. Production users MUST NOT set this env var.
107
+ # ============================================================================
108
+ if {[info exists env(WOGI_EXPECT_NO_INTERACT)] && $env(WOGI_EXPECT_NO_INTERACT) eq "1"} {
109
+ set dialog_buf ""
110
+ set start_ts [clock seconds]
75
111
 
76
- expect {
77
- -re "(.+)" {
78
- append dialog_buf $expect_out(1,string)
112
+ expect {
113
+ -re "(.+)" {
114
+ append dialog_buf $expect_out(1,string)
79
115
 
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
116
+ # Strip ANSI CSI sequences (colors, cursor moves): ESC [ ... letter
117
+ regsub -all {\x1b\[[0-9;?]*[a-zA-Z]} $dialog_buf "" plain
118
+ # Strip 8-bit CSI form (0x9B byte instead of ESC [), same terminator
119
+ regsub -all {\x9b[0-9;?]*[a-zA-Z]} $plain "" plain
120
+ # Strip OSC sequences (titles, hyperlinks): ESC ] ... BEL
121
+ regsub -all {\x1b\][^\x07]*\x07} $plain "" plain
122
+ # Strip ISO 2022 charset-selection sequences: ESC ( B, ESC ) 0, etc.
123
+ regsub -all {\x1b[\(\)\*\+\-\.\/][\x20-\x7e]} $plain "" plain
124
+ # Strip bare ESC that didn't belong to a recognized sequence
125
+ regsub -all {\x1b} $plain "" plain
90
126
 
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]
127
+ if {[string first "Loading development channels" $plain] >= 0} {
128
+ after 250
129
+ send "\r"
130
+ } else {
131
+ set elapsed [expr {[clock seconds] - $start_ts}]
132
+ if {$elapsed < $timeout} {
133
+ if {[string length $dialog_buf] > 65536} {
134
+ set dialog_buf [string range $dialog_buf 32768 end]
135
+ }
136
+ exp_continue
110
137
  }
111
- exp_continue
112
138
  }
113
- # Elapsed >= timeout: fall through to interact without
114
- # dismissing. Same failure mode as running claude without
115
- # this wrapper.
116
139
  }
140
+ timeout { }
141
+ eof { exit }
117
142
  }
118
- timeout { }
119
- eof { exit }
143
+ expect eof
144
+ exit
120
145
  }
121
146
 
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
147
+ # ============================================================================
148
+ # Production branch: interact with output trigger. Stdin flows to claude
149
+ # from second zero — user keystrokes are NEVER captured by expect. When the
150
+ # dialog phrase appears in child output, we inject Enter. Graceful fallback
151
+ # on mismatch: user sees the dialog, presses 1+Enter themselves, no harm.
152
+ # ============================================================================
153
+ set _wogi_dismissed 0
154
+
155
+ _wogi_boot_mark "entering interact"
156
+ interact {
157
+ -o
158
+ -re "Loading.{0,20}development.{0,20}channels" {
159
+ global _wogi_dismissed
160
+ if {!$_wogi_dismissed} {
161
+ set _wogi_dismissed 1
162
+ _wogi_boot_mark "dialog pattern matched"
163
+ # Let Ink finish rendering the SelectInput component and bind its
164
+ # keyboard listener before injecting Enter. 500ms is generous —
165
+ # typical Ink reconciliation is <50ms but cold startups can be
166
+ # slower. If this fires before the listener binds, the user's
167
+ # own Enter keystroke (which we no longer capture) still works.
168
+ after 500
169
+ _wogi_boot_mark "auto-Enter injected (after 500ms wait)"
170
+ send "\r"
171
+ }
172
+ }
136
173
  }
137
174
 
138
175
  # Pass claude's exit status — wrapper cares about the restart flag file,
@@ -272,6 +272,25 @@ function handleRequest(msg) {
272
272
  const msgPath = require('node:path').join(messagesDir, `${msgId}.json`);
273
273
  fs.writeFileSync(msgPath, JSON.stringify(msgObj, null, 2));
274
274
 
275
+ // wf-3635574e / G3: populate the SQLite IPC index (best effort).
276
+ // JSON above remains authoritative. Index enables atomic consume
277
+ // for hot-path readers. AC5 fallback: silent if sql.js unavailable.
278
+ (async () => {
279
+ try {
280
+ const ipc = require('./workspace-ipc-sqlite');
281
+ if (!(await ipc.isAvailable())) return;
282
+ const route = ipc.routeMessageForIndex(msgObj);
283
+ if (!route) return;
284
+ await ipc.indexMessage(WORKSPACE_ROOT, route.repoName, route.direction, {
285
+ id: msgObj.id,
286
+ kind: msgObj.type,
287
+ payload: msgObj,
288
+ createdAt: msgObj.timestamp,
289
+ consumedAt: null
290
+ });
291
+ } catch (_err) { /* best effort */ }
292
+ })();
293
+
275
294
  // Also POST to manager's channel port for real-time notification
276
295
  const managerPort = process.env.WOGI_MANAGER_PORT;
277
296
  if (managerPort) {
@@ -696,7 +696,7 @@ function checkTypeSyncCompliance(workspaceRoot, manifest) {
696
696
  try {
697
697
  const config = safeReadJson(configPath);
698
698
 
699
- for (const [name, memberConfig] of Object.entries(config.members || {})) {
699
+ for (const [name, _memberConfig] of Object.entries(config.members || {})) {
700
700
  const member = manifest.members?.[name];
701
701
  if (!member || member.role === 'provider') continue; // Only check consumers
702
702
 
@@ -163,6 +163,144 @@ function getOverdueDispatches(workspaceRoot, now) {
163
163
  });
164
164
  }
165
165
 
166
+ // ============================================================
167
+ // SQLite Index (wf-3635574e / G3, Path B)
168
+ // ============================================================
169
+ // JSON ring buffer above remains authoritative. The async helpers below
170
+ // use the SQLite IPC index for atomic queries at scale. Falls back
171
+ // transparently to the JSON path if sql.js is unavailable (AC5).
172
+
173
+ /**
174
+ * Record a dispatch and also index it in the worker's inbound SQLite DB.
175
+ * Preserves the sync recordDispatch path for existing callers; new callers
176
+ * that want the SQLite index entry call this async variant.
177
+ *
178
+ * @returns {Promise<{record: Object, indexed: boolean}>}
179
+ */
180
+ async function recordDispatchIndexed(workspaceRoot, params) {
181
+ const record = recordDispatch(workspaceRoot, params);
182
+ let indexed = false;
183
+ try {
184
+ const ipc = require('./workspace-ipc-sqlite');
185
+ if (await ipc.isAvailable()) {
186
+ const id = `disp-${record.taskId}-${Date.parse(record.dispatchedAt) || 0}`.substring(0, 80);
187
+ indexed = await ipc.indexMessage(workspaceRoot, record.repoName, 'inbound', {
188
+ id,
189
+ kind: 'task-dispatch',
190
+ payload: record,
191
+ createdAt: record.dispatchedAt,
192
+ consumedAt: null
193
+ });
194
+ }
195
+ } catch (_err) { /* AC5 fallback */ }
196
+ return { record, indexed };
197
+ }
198
+
199
+ /**
200
+ * Return overdue dispatches by querying the SQLite index across all known
201
+ * workers. Falls back to the sync JSON ring buffer if SQLite is unavailable.
202
+ *
203
+ * Overdue = inbound row with consumed_at NULL and created_at older than
204
+ * expectedDurationMs (read from payload).
205
+ */
206
+ async function getOverdueDispatchesIndexed(workspaceRoot, now) {
207
+ const ts = Number.isFinite(now) ? now : Date.now();
208
+ try {
209
+ const ipc = require('./workspace-ipc-sqlite');
210
+ if (!(await ipc.isAvailable())) {
211
+ return getOverdueDispatches(workspaceRoot, ts);
212
+ }
213
+ const repos = ipc.listIndexedRepos(workspaceRoot);
214
+ const overdue = [];
215
+ for (const repo of repos) {
216
+ const rows = await ipc.listUnconsumed(workspaceRoot, repo, 'inbound', { kind: 'task-dispatch' });
217
+ for (const r of rows) {
218
+ const p = r.payload || {};
219
+ const deadline = Date.parse(p.expectedDeadline || '');
220
+ if (Number.isFinite(deadline) && deadline < ts && p.status === 'pending') {
221
+ overdue.push(p);
222
+ }
223
+ }
224
+ }
225
+ return overdue;
226
+ } catch (_err) {
227
+ return getOverdueDispatches(workspaceRoot, ts);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Attach a completion summary to the most recent dispatch record for a task
233
+ * (Story B / wf-ab59f0e4). Sets status to 'completed-with-summary' so the
234
+ * silent-halt detector skips it. The summary itself is stored on the
235
+ * record under .completionSummary; the manager surfaces unseen summaries
236
+ * via readPendingCompletionSummaries().
237
+ *
238
+ * Single-writer invariant: this function MUST be called only by the
239
+ * manager process (workers POST the message via channel-dispatch; the
240
+ * manager's HTTP server invokes this).
241
+ *
242
+ * @param {string} workspaceRoot
243
+ * @param {string} taskId
244
+ * @param {Object} summary - validated payload from flow-workspace-summary
245
+ * @param {string} [workerId] - falls back to summary.workerId or the
246
+ * matched record's repoName
247
+ * @returns {Object|null} updated record, or null if no matching dispatch
248
+ */
249
+ function attachCompletionSummary(workspaceRoot, taskId, summary, workerId) {
250
+ if (!summary || typeof summary !== 'object') {
251
+ throw new Error('attachCompletionSummary: summary must be an object');
252
+ }
253
+ const state = loadState(workspaceRoot);
254
+ for (let i = state.dispatches.length - 1; i >= 0; i--) {
255
+ const r = state.dispatches[i];
256
+ if (r && r.taskId === taskId && r.status === 'pending') {
257
+ r.status = 'completed-with-summary';
258
+ r.reconciledAt = new Date().toISOString();
259
+ r.completionSummary = {
260
+ ...summary,
261
+ workerId: workerId || summary.workerId || r.repoName || null,
262
+ receivedAt: r.reconciledAt,
263
+ seenByManager: false
264
+ };
265
+ saveState(workspaceRoot, state);
266
+ return r;
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Return completion summaries that the manager has not yet surfaced to the
274
+ * user. Caller (UserPromptSubmit hook) should mark them seen via
275
+ * markCompletionSummariesSeen() after rendering.
276
+ */
277
+ function readPendingCompletionSummaries(workspaceRoot) {
278
+ const state = loadState(workspaceRoot);
279
+ const out = [];
280
+ for (const r of state.dispatches) {
281
+ if (r && r.completionSummary && r.completionSummary.seenByManager === false) {
282
+ out.push({ taskId: r.taskId, repoName: r.repoName, summary: r.completionSummary });
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+
288
+ function markCompletionSummariesSeen(workspaceRoot, taskIds) {
289
+ if (!Array.isArray(taskIds) || taskIds.length === 0) return 0;
290
+ const set = new Set(taskIds);
291
+ const state = loadState(workspaceRoot);
292
+ let n = 0;
293
+ for (const r of state.dispatches) {
294
+ if (r && r.completionSummary && set.has(r.taskId) && r.completionSummary.seenByManager === false) {
295
+ r.completionSummary.seenByManager = true;
296
+ r.completionSummary.seenAt = new Date().toISOString();
297
+ n++;
298
+ }
299
+ }
300
+ if (n > 0) saveState(workspaceRoot, state);
301
+ return n;
302
+ }
303
+
166
304
  module.exports = {
167
305
  DEFAULT_DURATION_MS,
168
306
  MAX_ACTIVE,
@@ -170,6 +308,12 @@ module.exports = {
170
308
  reconcileDispatch,
171
309
  readDispatches,
172
310
  getOverdueDispatches,
311
+ attachCompletionSummary,
312
+ readPendingCompletionSummaries,
313
+ markCompletionSummariesSeen,
314
+ // wf-3635574e SQLite-backed variants (async, opt-in):
315
+ recordDispatchIndexed,
316
+ getOverdueDispatchesIndexed,
173
317
  stateFilePath,
174
318
  archiveFilePath
175
319
  };
@@ -470,7 +470,7 @@ function broadcastPostChange(workspaceRoot, fromRepo, context, options = {}) {
470
470
  * @param {Object} taskMeta
471
471
  * @returns {{ passed: boolean, message: string, severity: string }}
472
472
  */
473
- function gateDeploymentReadiness(workspaceRoot, _context, taskMeta) {
473
+ function gateDeploymentReadiness(workspaceRoot, _context, _taskMeta) {
474
474
  const { execFileSync } = require('node:child_process');
475
475
 
476
476
  try {