wogiflow 2.26.1 → 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 +134 -18
  43. package/lib/wogi-claude-expect.exp +137 -33
  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
@@ -8,25 +8,75 @@
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). So we intercept the dialog
12
- # at the wrapper level: spawn claude in an expect-managed PTY, watch for
13
- # the dialog title text, send Enter to accept the already-highlighted
14
- # "I am using this for local development" option, then hand off control
15
- # to the user via `interact`.
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 (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.
29
+ #
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).
16
51
  #
17
52
  # Usage (invoked from lib/wogi-claude):
18
53
  # expect wogi-claude-expect.exp /absolute/path/to/claude [args...]
19
54
  #
20
55
  # Disable at runtime: set WOGI_NO_EXPECT=1. The wrapper then execs claude
21
- # directly and the user sees the dialog as before (manual single-click).
56
+ # directly and the user sees the dialog as before.
22
57
 
23
58
  set timeout 30
24
-
25
- # Allow WOGI_EXPECT_TIMEOUT override (rarely needed)
26
59
  if {[info exists env(WOGI_EXPECT_TIMEOUT)]} {
27
60
  set timeout $env(WOGI_EXPECT_TIMEOUT)
28
61
  }
29
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
+
30
80
  # Mirror claude's output to the user's terminal during dialog watch
31
81
  log_user 1
32
82
 
@@ -40,34 +90,88 @@ set claude_bin [lindex $argv 0]
40
90
  set claude_args [lrange $argv 1 end]
41
91
 
42
92
  # Spawn claude in a pseudo-TTY so its Ink UI renders normally.
43
- # eval is needed because claude_args is a list and spawn expects a
44
- # flattened command line.
45
- eval spawn $claude_bin $claude_args
93
+ # Use {*} list-splice rather than `eval spawn` `eval` reparses its
94
+ # arguments as Tcl script, which lets an argument containing bracket syntax
95
+ # (e.g. `[exec attacker-cmd]`) escape to command execution. The splice form
96
+ # expands the list without reparsing.
97
+ _wogi_boot_mark "before spawn"
98
+ spawn $claude_bin {*}$claude_args
99
+ _wogi_boot_mark "after spawn (pid=$spawn_id)"
46
100
 
47
- # Watch for the DevChannels dialog title, then press Enter to accept
48
- # the default-highlighted "I am using this for local development" option.
49
- #
50
- # On timeout or EOF: fall through to `interact`. If the dialog appears
51
- # AFTER our timeout window, the user can still answer it manually
52
- # same failure mode as running claude directly.
53
- expect {
54
- -re "Loading development channels" {
55
- # Let Ink finish rendering the dialog before sending Enter.
56
- # Without this, the select-input component may not have bound
57
- # its keyboard listener yet and the keystroke is dropped.
58
- after 250
59
- send "\r"
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]
111
+
112
+ expect {
113
+ -re "(.+)" {
114
+ append dialog_buf $expect_out(1,string)
115
+
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
126
+
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
137
+ }
138
+ }
139
+ }
140
+ timeout { }
141
+ eof { exit }
60
142
  }
61
- timeout { }
62
- eof { exit }
143
+ expect eof
144
+ exit
63
145
  }
64
146
 
65
- # Hand off: user's keystrokes flow to claude, claude's output flows
66
- # to the user's terminal. interact blocks until claude exits.
67
- interact
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
+ }
173
+ }
68
174
 
69
- # After claude exits, let the bash wrapper decide whether to restart.
70
- # Pass through claude's exit status (expect sets it in $expect_out(-code)
71
- # after `interact`, but a plain exit is sufficient since the wrapper
72
- # only cares about the restart flag file, not exit code).
175
+ # Pass claude's exit status wrapper cares about the restart flag file,
176
+ # not exit code, so a plain exit suffices.
73
177
  exit
@@ -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 {