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.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +84 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- 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 (
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
set
|
|
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
|
-
|
|
78
|
-
|
|
112
|
+
expect {
|
|
113
|
+
-re "(.+)" {
|
|
114
|
+
append dialog_buf $expect_out(1,string)
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
119
|
-
|
|
143
|
+
expect eof
|
|
144
|
+
exit
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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,
|
|
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
|
};
|
package/lib/workspace-gates.js
CHANGED
|
@@ -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,
|
|
473
|
+
function gateDeploymentReadiness(workspaceRoot, _context, _taskMeta) {
|
|
474
474
|
const { execFileSync } = require('node:child_process');
|
|
475
475
|
|
|
476
476
|
try {
|