wogiflow 2.20.1 → 2.22.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 (255) hide show
  1. package/.claude/commands/wogi-finalize.md +83 -0
  2. package/.claude/rules/_internal/self-maintenance.md +1 -1
  3. package/.claude/settings.json +1 -1
  4. package/lib/commands/login.js +1 -1
  5. package/lib/installer.js +5 -5
  6. package/lib/release-channel.js +1 -1
  7. package/lib/skill-registry.js +3 -3
  8. package/lib/workspace-events.js +1 -1
  9. package/lib/workspace-gates.js +2 -2
  10. package/lib/workspace-intelligence.js +1 -1
  11. package/lib/workspace-routing.js +1 -1
  12. package/lib/workspace.js +16 -17
  13. package/package.json +2 -2
  14. package/scripts/base-workflow-step.js +2 -2
  15. package/scripts/flow-adaptive-learning.js +6 -6
  16. package/scripts/flow-api-index.js +2 -2
  17. package/scripts/flow-architect-pass.js +1 -1
  18. package/scripts/flow-ask.js +1 -1
  19. package/scripts/flow-assumption-detector.js +1 -1
  20. package/scripts/flow-audit-gates.js +38 -12
  21. package/scripts/flow-audit.js +4 -4
  22. package/scripts/flow-auto-context.js +3 -3
  23. package/scripts/flow-background.js +1 -1
  24. package/scripts/flow-best-of-n.js +7 -7
  25. package/scripts/flow-bridge.js +3 -3
  26. package/scripts/flow-bug.js +2 -2
  27. package/scripts/flow-bulk-loop.js +7 -7
  28. package/scripts/flow-cascade-completion.js +2 -2
  29. package/scripts/flow-cascade.js +1 -1
  30. package/scripts/flow-checkpoint.js +2 -2
  31. package/scripts/flow-clarifying-questions.js +2 -2
  32. package/scripts/flow-cli.js +2 -2
  33. package/scripts/flow-code-intelligence.js +4 -4
  34. package/scripts/flow-community-sync.js +6 -6
  35. package/scripts/flow-community.js +1 -1
  36. package/scripts/flow-completion-truth-gate.js +161 -5
  37. package/scripts/flow-complexity.js +1 -1
  38. package/scripts/flow-config-defaults.js +16 -4
  39. package/scripts/flow-config-interactive.js +2 -2
  40. package/scripts/flow-config-loader.js +1 -1
  41. package/scripts/flow-config-migrate.js +5 -6
  42. package/scripts/flow-consistency-check.js +5 -5
  43. package/scripts/flow-context-compact/expander.js +1 -1
  44. package/scripts/flow-context-compact/index.js +2 -2
  45. package/scripts/flow-context-compact/section-extractor.js +3 -3
  46. package/scripts/flow-context-compact/summary-tree.js +1 -1
  47. package/scripts/flow-context-estimator.js +1 -1
  48. package/scripts/flow-context-gatherer.js +6 -6
  49. package/scripts/flow-context-generator.js +6 -6
  50. package/scripts/flow-context-init.js +2 -2
  51. package/scripts/flow-context-manager.js +1 -1
  52. package/scripts/flow-context-manifest.js +1 -1
  53. package/scripts/flow-context-monitor.js +5 -5
  54. package/scripts/flow-context-orchestrator.js +2 -2
  55. package/scripts/flow-context-scoring.js +4 -4
  56. package/scripts/flow-contract-scan.js +1 -1
  57. package/scripts/flow-correct.js +3 -3
  58. package/scripts/flow-damage-control.js +2 -2
  59. package/scripts/flow-deploy-gate.js +2 -2
  60. package/scripts/flow-deploy-history.js +1 -1
  61. package/scripts/flow-diff.js +3 -3
  62. package/scripts/flow-done-gates.js +1 -1
  63. package/scripts/flow-done.js +7 -7
  64. package/scripts/flow-durable-session.js +1 -1
  65. package/scripts/flow-entropy-monitor.js +3 -3
  66. package/scripts/flow-epics.js +5 -5
  67. package/scripts/flow-error-recovery.js +4 -4
  68. package/scripts/flow-eval-judge.js +5 -5
  69. package/scripts/flow-eval.js +7 -7
  70. package/scripts/flow-export-scanner.js +5 -5
  71. package/scripts/flow-extraction-review.js +1 -1
  72. package/scripts/flow-failure-learning.js +9 -9
  73. package/scripts/flow-feature.js +5 -5
  74. package/scripts/flow-figma-confirm.js +1 -1
  75. package/scripts/flow-figma-extract.js +2 -2
  76. package/scripts/flow-figma-index.js +2 -2
  77. package/scripts/flow-figma-match.js +1 -1
  78. package/scripts/flow-figma-mcp-server.js +3 -3
  79. package/scripts/flow-figma-orchestrator.js +1 -1
  80. package/scripts/flow-figma-registry.js +2 -2
  81. package/scripts/flow-function-index.js +2 -2
  82. package/scripts/flow-gate-confidence.js +2 -2
  83. package/scripts/flow-gate-telemetry.js +1 -1
  84. package/scripts/flow-gitignore.js +1 -1
  85. package/scripts/flow-guided-edit.js +3 -3
  86. package/scripts/flow-health.js +95 -8
  87. package/scripts/flow-hooks.js +3 -3
  88. package/scripts/flow-hybrid-detect.js +2 -2
  89. package/scripts/flow-hybrid-interactive.js +1 -1
  90. package/scripts/flow-hybrid-test.js +1 -1
  91. package/scripts/flow-hypothesis-generator.js +4 -4
  92. package/scripts/flow-instruction-richness.js +11 -11
  93. package/scripts/flow-intent-bootstrap.js +1 -1
  94. package/scripts/flow-intent-framing.js +1 -1
  95. package/scripts/flow-item-link.js +2 -2
  96. package/scripts/flow-knowledge-router.js +7 -7
  97. package/scripts/flow-knowledge-sync.js +3 -3
  98. package/scripts/flow-learning-orchestrator.js +1 -1
  99. package/scripts/flow-links.js +2 -2
  100. package/scripts/flow-log-manager.js +2 -2
  101. package/scripts/flow-logic-adversary.js +5 -4
  102. package/scripts/flow-long-input-chunking.js +1 -1
  103. package/scripts/flow-long-input-cli.js +3 -3
  104. package/scripts/flow-long-input.js +18 -18
  105. package/scripts/flow-loop-retry-learning.js +2 -2
  106. package/scripts/flow-lsp.js +4 -4
  107. package/scripts/flow-mcp-docs.js +1 -1
  108. package/scripts/flow-memory-blocks.js +5 -5
  109. package/scripts/flow-memory-compactor.js +3 -3
  110. package/scripts/flow-memory-db.js +4 -4
  111. package/scripts/flow-memory-sync.js +3 -3
  112. package/scripts/flow-metrics.js +2 -2
  113. package/scripts/flow-migrate-igr.js +2 -2
  114. package/scripts/flow-migrate.js +2 -2
  115. package/scripts/flow-model-adapter.js +4 -4
  116. package/scripts/flow-model-caller.js +8 -8
  117. package/scripts/flow-model-config.js +5 -5
  118. package/scripts/flow-model-profile.js +7 -7
  119. package/scripts/flow-model-router.js +5 -5
  120. package/scripts/flow-model-types.js +3 -3
  121. package/scripts/flow-models.js +8 -8
  122. package/scripts/flow-morning.js +1 -1
  123. package/scripts/flow-multi-approach.js +1 -1
  124. package/scripts/flow-orchestrate-context.js +2 -2
  125. package/scripts/flow-orchestrate-llm.js +4 -4
  126. package/scripts/flow-orchestrate-rollback.js +1 -1
  127. package/scripts/flow-orchestrate-state.js +6 -6
  128. package/scripts/flow-orchestrate-templates.js +1 -1
  129. package/scripts/flow-orchestrate-validation.js +2 -2
  130. package/scripts/flow-orchestrate-validator.js +1 -1
  131. package/scripts/flow-orchestrate.js +25 -25
  132. package/scripts/flow-parallel.js +1 -1
  133. package/scripts/flow-pattern-enforcer.js +7 -7
  134. package/scripts/flow-pattern-extractor.js +3 -3
  135. package/scripts/flow-peer-review.js +8 -8
  136. package/scripts/flow-pending.js +1 -1
  137. package/scripts/flow-permissions.js +1 -1
  138. package/scripts/flow-phased-task.js +1 -1
  139. package/scripts/flow-plan.js +1 -1
  140. package/scripts/flow-prd-manager.js +2 -2
  141. package/scripts/flow-product-scanner.js +2 -2
  142. package/scripts/flow-progress-tracker.js +2 -2
  143. package/scripts/flow-progress.js +1 -1
  144. package/scripts/flow-project-analyzer.js +3 -3
  145. package/scripts/flow-prompt-capture.js +2 -2
  146. package/scripts/flow-prompt-composer.js +3 -3
  147. package/scripts/flow-prompt-template.js +4 -4
  148. package/scripts/flow-providers.js +31 -23
  149. package/scripts/flow-queue.js +1 -1
  150. package/scripts/flow-registry-manager.js +4 -4
  151. package/scripts/flow-regression.js +1 -1
  152. package/scripts/flow-response-parser.js +1 -1
  153. package/scripts/flow-resume.js +1 -1
  154. package/scripts/flow-review-passes/index.js +2 -2
  155. package/scripts/flow-review-passes/integration.js +3 -3
  156. package/scripts/flow-review-passes/logic.js +3 -3
  157. package/scripts/flow-review-passes/security.js +2 -2
  158. package/scripts/flow-review-passes/structure.js +1 -1
  159. package/scripts/flow-review.js +11 -11
  160. package/scripts/flow-revision-tracker.js +2 -2
  161. package/scripts/flow-roadmap.js +2 -2
  162. package/scripts/flow-run-trace.js +1 -1
  163. package/scripts/flow-safety.js +3 -3
  164. package/scripts/flow-scanner-base.js +1 -1
  165. package/scripts/flow-scenario-engine.js +7 -7
  166. package/scripts/flow-schema-drift.js +4 -3
  167. package/scripts/flow-section-index.js +2 -2
  168. package/scripts/flow-section-resolver.js +4 -4
  169. package/scripts/flow-semantic-match.js +3 -3
  170. package/scripts/flow-session-end.js +56 -0
  171. package/scripts/flow-session-learning.js +2 -2
  172. package/scripts/flow-setup-hooks.js +1 -1
  173. package/scripts/flow-skill-create.js +3 -3
  174. package/scripts/flow-skill-freshness.js +2 -2
  175. package/scripts/flow-skill-generator.js +6 -6
  176. package/scripts/flow-skill-learn.js +7 -7
  177. package/scripts/flow-skill-matcher.js +2 -2
  178. package/scripts/flow-solution-optimizer.js +1 -1
  179. package/scripts/flow-spec-generator.js +5 -5
  180. package/scripts/flow-spec-verifier.js +2 -2
  181. package/scripts/flow-stack-wizard.js +6 -6
  182. package/scripts/flow-standards-checker.js +8 -8
  183. package/scripts/flow-standards-gate.js +4 -4
  184. package/scripts/flow-standards-learner.js +2 -2
  185. package/scripts/flow-start.js +9 -9
  186. package/scripts/flow-stats-collector.js +2 -2
  187. package/scripts/flow-status.js +1 -1
  188. package/scripts/flow-step-changelog.js +3 -3
  189. package/scripts/flow-step-complexity.js +1 -1
  190. package/scripts/flow-step-coverage.js +3 -3
  191. package/scripts/flow-step-knowledge.js +2 -2
  192. package/scripts/flow-step-pr-tests.js +2 -2
  193. package/scripts/flow-step-regression.js +3 -3
  194. package/scripts/flow-step-review.js +5 -5
  195. package/scripts/flow-story.js +2 -2
  196. package/scripts/flow-strict-adherence.js +2 -2
  197. package/scripts/flow-structure-sensor.js +283 -0
  198. package/scripts/flow-sync-anonymizer.js +3 -3
  199. package/scripts/flow-task-checkpoint.js +2 -2
  200. package/scripts/flow-task-classifier.js +2 -2
  201. package/scripts/flow-task-completion-summary.js +1 -1
  202. package/scripts/flow-task-enforcer.js +5 -5
  203. package/scripts/flow-tech-debt.js +3 -3
  204. package/scripts/flow-template-extractor.js +3 -3
  205. package/scripts/flow-templates.js +1 -1
  206. package/scripts/flow-test-api.js +12 -12
  207. package/scripts/flow-test-discovery.js +9 -9
  208. package/scripts/flow-test-generate.js +5 -5
  209. package/scripts/flow-test-integrity.js +3 -3
  210. package/scripts/flow-test-ui.js +8 -8
  211. package/scripts/flow-testing-deps.js +4 -4
  212. package/scripts/flow-tiered-learning.js +3 -3
  213. package/scripts/flow-todowrite-sync.js +1 -1
  214. package/scripts/flow-trap-zone.js +1 -1
  215. package/scripts/flow-verification-profile.js +9 -9
  216. package/scripts/flow-verify.js +2 -2
  217. package/scripts/flow-version-check.js +2 -2
  218. package/scripts/flow-webmcp-generator.js +3 -3
  219. package/scripts/flow-wiring-verifier.js +13 -13
  220. package/scripts/flow-worker-question-classifier.js +256 -0
  221. package/scripts/flow-workflow-steps.js +3 -3
  222. package/scripts/flow-workflow.js +1 -1
  223. package/scripts/flow-worktree.js +1 -1
  224. package/scripts/hooks/adapters/base-adapter.js +2 -2
  225. package/scripts/hooks/core/commit-log-gate.js +2 -2
  226. package/scripts/hooks/core/component-check.js +3 -3
  227. package/scripts/hooks/core/config-change.js +1 -1
  228. package/scripts/hooks/core/deploy-gate.js +2 -1
  229. package/scripts/hooks/core/git-safety-gate.js +1 -1
  230. package/scripts/hooks/core/instructions-loaded.js +1 -1
  231. package/scripts/hooks/core/loop-check.js +1 -1
  232. package/scripts/hooks/core/manager-boundary-gate.js +3 -2
  233. package/scripts/hooks/core/observation-capture.js +6 -6
  234. package/scripts/hooks/core/phase-gate.js +4 -4
  235. package/scripts/hooks/core/pre-compact.js +1 -1
  236. package/scripts/hooks/core/pre-tool-orchestrator.js +1 -1
  237. package/scripts/hooks/core/routing-gate.js +2 -84
  238. package/scripts/hooks/core/session-context.js +1 -1
  239. package/scripts/hooks/core/session-end.js +3 -3
  240. package/scripts/hooks/core/session-history.js +1 -1
  241. package/scripts/hooks/core/setup-handler.js +1 -1
  242. package/scripts/hooks/core/task-boundary-reset.js +2 -4
  243. package/scripts/hooks/core/task-completed.js +13 -7
  244. package/scripts/hooks/core/task-created.js +1 -1
  245. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  246. package/scripts/hooks/entry/claude-code/permission-denied.js +4 -2
  247. package/scripts/hooks/entry/claude-code/stop.js +60 -0
  248. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +1 -1
  249. package/scripts/hooks/git/post-commit.js +1 -1
  250. package/scripts/postinstall.js +7 -7
  251. package/scripts/preuninstall.js +5 -5
  252. package/scripts/registries/component-registry.js +2 -2
  253. package/scripts/registries/contract-scanner.js +11 -11
  254. package/scripts/registries/schema-registry.js +5 -5
  255. package/scripts/registries/service-registry.js +9 -9
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Flow — Worker Question Classifier (v2.21.0+)
5
+ *
6
+ * AI-based semantic detector that runs at Stop-hook time in workspace worker
7
+ * mode and answers ONE question about the AI's final assistant message:
8
+ *
9
+ * "Does this message end by asking the user a question that expects
10
+ * a user response?"
11
+ *
12
+ * If YES + confidence >= threshold + worker mode → Stop hook blocks the turn
13
+ * with instructions to channel-dispatch `## QUESTION:` to the manager instead.
14
+ *
15
+ * Why AI, not regex: the hedging vocabulary is infinite ("let me know",
16
+ * "should I", "which option", "?", "thoughts?", "any preference?"). User
17
+ * explicitly requested AI logic over regex in the 2026-04-16 session.
18
+ *
19
+ * Design mirrors flow-conclusion-classifier.js / flow-correction-detector.js:
20
+ * - Uses existing flow-model-caller.js infrastructure (same plan tokens
21
+ * Claude Code already uses)
22
+ * - ANTHROPIC_API_KEY absent → returns `{ classified: false, reason:
23
+ * 'no-credentials' }` — Stop hook treats as no-op, does NOT block.
24
+ * This matches the established fail-open pattern.
25
+ * - Transcript parsing is defensive — missing, empty, or malformed
26
+ * transcript returns `{ classified: false, reason: <specific> }`.
27
+ * - JSON response from Haiku validated for shape + prototype-pollution.
28
+ * - Fail-open on model error — if the classifier breaks, legitimate
29
+ * stops are not affected. A silent-stall false-negative is recoverable;
30
+ * a false-positive block on every turn is not.
31
+ */
32
+
33
+ const fs = require('node:fs');
34
+
35
+ const DEFAULT_MIN_CONFIDENCE = 70;
36
+ const DEFAULT_MODEL = 'anthropic:claude-3-5-haiku-latest';
37
+ const MAX_MESSAGE_CHARS = 8000; // Classifier input cap
38
+ const MAX_TOKENS = 300; // Classifier output cap (tiny JSON)
39
+ const TEMPERATURE = 0.0; // Deterministic classification
40
+
41
+ // Shared prototype-pollution guard (same as flow-conclusion-classifier).
42
+ const { DANGEROUS_KEYS } = require('./flow-io');
43
+
44
+ /**
45
+ * Extract the final assistant message from a Claude Code transcript JSONL file.
46
+ *
47
+ * Claude Code writes one JSON object per line. Each object represents an event
48
+ * (user message, assistant message, tool call, tool result, etc.). We scan
49
+ * backward for the last event where the content resembles an assistant-authored
50
+ * text block (has `role: 'assistant'` OR `type: 'assistant'` shape).
51
+ *
52
+ * Defensive: any IO / parse error returns null. Caller treats null as no-op.
53
+ *
54
+ * @param {string} transcriptPath - Absolute path to the JSONL transcript
55
+ * @returns {string|null} The text of the last assistant message, or null
56
+ */
57
+ function extractLastAssistantMessage(transcriptPath) {
58
+ if (!transcriptPath || typeof transcriptPath !== 'string') return null;
59
+ let raw;
60
+ try {
61
+ raw = fs.readFileSync(transcriptPath, 'utf-8');
62
+ } catch (_err) {
63
+ return null;
64
+ }
65
+ if (!raw || raw.length === 0) return null;
66
+
67
+ // Scan from end for lines we can parse as assistant messages.
68
+ const lines = raw.split('\n');
69
+ for (let i = lines.length - 1; i >= 0; i--) {
70
+ const line = lines[i].trim();
71
+ if (!line) continue;
72
+ let entry;
73
+ try {
74
+ entry = JSON.parse(line);
75
+ } catch (_err) {
76
+ continue; // Skip unparseable lines — transcript may be mid-write.
77
+ }
78
+ const text = extractAssistantText(entry);
79
+ if (text) return text.slice(0, MAX_MESSAGE_CHARS);
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Pull assistant-authored text out of a single transcript entry. The transcript
86
+ * format has evolved across Claude Code versions — we accept any of:
87
+ * - { role: 'assistant', content: 'string' }
88
+ * - { role: 'assistant', content: [{ type: 'text', text: '...' }] }
89
+ * - { type: 'assistant', message: { content: [{ type: 'text', text: '...' }] } }
90
+ * - { type: 'assistant', text: '...' }
91
+ */
92
+ function extractAssistantText(entry) {
93
+ if (!entry || typeof entry !== 'object') return null;
94
+
95
+ // Direct role/type check.
96
+ const isAssistant =
97
+ entry.role === 'assistant' ||
98
+ entry.type === 'assistant' ||
99
+ (entry.message && entry.message.role === 'assistant');
100
+ if (!isAssistant) return null;
101
+
102
+ // Pull the content wherever it lives.
103
+ const content = entry.content ?? entry.message?.content ?? entry.text;
104
+ if (typeof content === 'string') return content;
105
+ if (Array.isArray(content)) {
106
+ // Collect text blocks only — skip tool_use / tool_result blocks.
107
+ const texts = content
108
+ .filter(b => b && typeof b === 'object' && (b.type === 'text' || typeof b.text === 'string'))
109
+ .map(b => String(b.text || ''))
110
+ .filter(Boolean);
111
+ if (texts.length > 0) return texts.join('\n').trim();
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Build the Haiku classifier prompt. Kept as a pure string for testability.
118
+ *
119
+ * Designed to minimize false positives:
120
+ * - Rhetorical questions ("Did the tests pass? Yes.") → NO
121
+ * - Narration with trailing "?" ("the question is how to proceed") → NO
122
+ * - Actual open-ended ask ("Should I use A or B?" with no answer given) → YES
123
+ */
124
+ function buildClassifierPrompt(lastMessage) {
125
+ return `You classify whether an AI assistant's final message to a WORKSPACE WORKER session ends by asking the USER a question that expects a user response.
126
+
127
+ Context: in workspace mode, workers are autonomous and MUST NOT prompt the user directly — the user only sees the manager terminal, so direct questions stall silently. Workers that need user input MUST channel-dispatch "## QUESTION:" to the manager instead.
128
+
129
+ Your job: classify YES only when the worker's final message contains an OPEN question the worker is waiting on the user to answer. Classify NO for rhetorical questions that the worker answers itself, narrative descriptions, or questions that have an accompanying decision.
130
+
131
+ [MESSAGE_START]
132
+ ${String(lastMessage || '').slice(0, MAX_MESSAGE_CHARS)}
133
+ [MESSAGE_END]
134
+
135
+ Return JSON only, no prose, no markdown fences:
136
+ {"isUserQuestion": true|false, "confidence": 0-100, "reason": "one short sentence"}
137
+
138
+ Examples:
139
+ - "Should I proceed with A or B? Let me know." → {"isUserQuestion": true, "confidence": 95, "reason": "open choice awaiting user decision"}
140
+ - "Did the tests pass? Yes, all 12 passed." → {"isUserQuestion": false, "confidence": 90, "reason": "rhetorical, answered inline"}
141
+ - "I finished the task. Awaiting your signal." → {"isUserQuestion": true, "confidence": 85, "reason": "hedging terminal state awaiting user"}
142
+ - "Task complete. Next: wf-abc12345 — starting now." → {"isUserQuestion": false, "confidence": 95, "reason": "action statement, no question"}`;
143
+ }
144
+
145
+ /**
146
+ * Guard parsed JSON response against prototype pollution. Mirrors
147
+ * flow-conclusion-classifier.hasDangerousKeys.
148
+ */
149
+ function hasDangerousKeys(value) {
150
+ if (!value || typeof value !== 'object') return false;
151
+ if (Array.isArray(value)) return value.some(hasDangerousKeys);
152
+ for (const key of Object.keys(value)) {
153
+ if (DANGEROUS_KEYS.has(key)) return true;
154
+ if (hasDangerousKeys(value[key])) return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Classify the worker's last assistant message.
161
+ *
162
+ * @param {Object} opts
163
+ * @param {string} opts.transcriptPath - Absolute path to session JSONL transcript
164
+ * @param {number} [opts.minConfidence] - Confidence threshold (default 70)
165
+ * @param {string} [opts.model] - Model override (default haiku)
166
+ * @returns {Promise<{
167
+ * classified: boolean,
168
+ * isUserQuestion?: boolean,
169
+ * confidence?: number,
170
+ * reason?: string,
171
+ * lastMessage?: string
172
+ * }>}
173
+ */
174
+ async function classifyWorkerQuestion(opts = {}) {
175
+ const minConfidence = Number.isFinite(opts.minConfidence) ? opts.minConfidence : DEFAULT_MIN_CONFIDENCE;
176
+ const model = opts.model || DEFAULT_MODEL;
177
+
178
+ // Fail-open gates — any reason to skip returns { classified: false }.
179
+ if (!process.env.ANTHROPIC_API_KEY) {
180
+ return { classified: false, reason: 'no-credentials' };
181
+ }
182
+ if (!opts.transcriptPath) {
183
+ return { classified: false, reason: 'no-transcript-path' };
184
+ }
185
+
186
+ const lastMessage = extractLastAssistantMessage(opts.transcriptPath);
187
+ if (!lastMessage || lastMessage.length < 10) {
188
+ return { classified: false, reason: 'no-last-message', lastMessage };
189
+ }
190
+
191
+ let callModel;
192
+ try {
193
+ ({ callModel } = require('./flow-model-caller'));
194
+ } catch (_err) {
195
+ return { classified: false, reason: 'no-model-caller' };
196
+ }
197
+
198
+ let result;
199
+ try {
200
+ result = await callModel(model, buildClassifierPrompt(lastMessage), {
201
+ temperature: TEMPERATURE,
202
+ maxTokens: MAX_TOKENS
203
+ });
204
+ } catch (err) {
205
+ if (process.env.DEBUG) {
206
+ console.error(`[worker-question-classifier] model call failed: ${err.message}`);
207
+ }
208
+ return { classified: false, reason: 'model-error' };
209
+ }
210
+
211
+ // flow-model-caller returns { success, response, ... } where `response` is the text.
212
+ // Earlier classifiers read `.content`; accept both shapes for resilience.
213
+ const raw = String(result?.response ?? result?.content ?? '').trim();
214
+ if (!raw) return { classified: false, reason: 'empty-response' };
215
+
216
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
217
+ if (!jsonMatch) return { classified: false, reason: 'non-json-response' };
218
+
219
+ let parsed;
220
+ try {
221
+ parsed = JSON.parse(jsonMatch[0]);
222
+ } catch (_err) {
223
+ return { classified: false, reason: 'json-parse-error' };
224
+ }
225
+
226
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
227
+ return { classified: false, reason: 'bad-shape' };
228
+ }
229
+ if (hasDangerousKeys(parsed)) {
230
+ return { classified: false, reason: 'dangerous-keys' };
231
+ }
232
+
233
+ const isUserQuestion = Boolean(parsed.isUserQuestion);
234
+ const confidence = Number.isFinite(parsed.confidence) ? Math.round(parsed.confidence) : 0;
235
+ const reason = typeof parsed.reason === 'string' ? parsed.reason.slice(0, 240) : '';
236
+
237
+ return {
238
+ classified: true,
239
+ isUserQuestion,
240
+ confidence,
241
+ reason,
242
+ lastMessage,
243
+ blocked: isUserQuestion && confidence >= minConfidence,
244
+ minConfidence
245
+ };
246
+ }
247
+
248
+ module.exports = {
249
+ classifyWorkerQuestion,
250
+ extractLastAssistantMessage,
251
+ extractAssistantText,
252
+ buildClassifierPrompt,
253
+ hasDangerousKeys,
254
+ DEFAULT_MIN_CONFIDENCE,
255
+ DEFAULT_MODEL
256
+ };
@@ -11,9 +11,9 @@
11
11
  * await runSteps('afterTask', { taskId: 'TASK-001', files: [...] });
12
12
  */
13
13
 
14
- const fs = require('node:fs');
15
- const path = require('node:path');
16
- const { getProjectRoot, colors, getConfig, invalidateConfigCache, writeJson, PATHS } = require('./flow-utils');
14
+ const _fs = require('node:fs');
15
+ const _path = require('node:path');
16
+ const { colors, getConfig, invalidateConfigCache, writeJson, PATHS } = require('./flow-utils');
17
17
 
18
18
  // ============================================================
19
19
  // Step Registry - All available workflow steps
@@ -24,7 +24,7 @@
24
24
  const fs = require('node:fs');
25
25
  const path = require('node:path');
26
26
  const { spawn } = require('node:child_process');
27
- const { getProjectRoot, colors: c, readJson, PATHS } = require('./flow-utils');
27
+ const { colors: c, readJson, PATHS } = require('./flow-utils');
28
28
  const { success: printSuccess, error: printError } = require('./flow-output');
29
29
  const { detectPackageManager } = require('./flow-script-resolver');
30
30
 
@@ -22,7 +22,7 @@
22
22
  * await discardWorktree(worktree);
23
23
  */
24
24
 
25
- const { execFileSync, spawn } = require('node:child_process');
25
+ const { execFileSync } = require('node:child_process');
26
26
  const fs = require('node:fs');
27
27
  const path = require('node:path');
28
28
  const os = require('node:os');
@@ -38,7 +38,7 @@ class BaseAdapter {
38
38
  * @param {Object} coreResult - Result from core module
39
39
  * @returns {Object} CLI-specific format
40
40
  */
41
- transformResult(event, coreResult) {
41
+ transformResult(_event, coreResult) {
42
42
  throw new Error('transformResult() must be implemented by subclass');
43
43
  }
44
44
 
@@ -58,7 +58,7 @@ class BaseAdapter {
58
58
  * @param {Object} [transportConfig] - Transport config: { transport, url, headers, allowedEnvVars }
59
59
  * @returns {Object} CLI-specific hook configuration
60
60
  */
61
- generateConfig(rules, projectRoot, transportConfig) {
61
+ generateConfig(_rules, projectRoot, transportConfig) {
62
62
  throw new Error('generateConfig() must be implemented by subclass');
63
63
  }
64
64
 
@@ -80,7 +80,7 @@ function checkCommitLogGate(command, config) {
80
80
 
81
81
  // Load config if not provided
82
82
  if (!config) {
83
- try { config = getConfig(); } catch (err) { config = {}; }
83
+ try { config = getConfig(); } catch (_err) { config = {}; }
84
84
  }
85
85
 
86
86
  // Check if gate is enabled (default: enabled when enforcement section exists)
@@ -92,7 +92,7 @@ function checkCommitLogGate(command, config) {
92
92
  let readyData;
93
93
  try {
94
94
  readyData = getReadyData();
95
- } catch (err) {
95
+ } catch (_err) {
96
96
  // Can't read ready.json → fail-open (don't block work)
97
97
  return { allowed: true, blocked: false };
98
98
  }
@@ -191,7 +191,7 @@ function loadComponentIndex() {
191
191
  }
192
192
  }
193
193
  }
194
- } catch (err) {
194
+ } catch (_err) {
195
195
  // Skip unreadable map files
196
196
  }
197
197
  }
@@ -199,7 +199,7 @@ function loadComponentIndex() {
199
199
 
200
200
  if (components.length === 0) return null;
201
201
  return { components, source: 'fallback-from-maps' };
202
- } catch (err) {
202
+ } catch (_err) {
203
203
  return null;
204
204
  }
205
205
  }
@@ -238,7 +238,7 @@ function parseAppMap() {
238
238
  }
239
239
 
240
240
  return components;
241
- } catch (err) {
241
+ } catch (_err) {
242
242
  return [];
243
243
  }
244
244
  }
@@ -65,7 +65,7 @@ function handleConfigChange(options = {}) {
65
65
  let bridgeState = null;
66
66
  try {
67
67
  bridgeState = require('../../flow-bridge-state');
68
- const { PATHS } = require('../../flow-utils');
68
+ const { } = require('../../flow-utils');
69
69
  } catch (_err) {
70
70
  // Bridge state module unavailable
71
71
  }
@@ -264,7 +264,8 @@ function findLatestArtifact(options) {
264
264
  for (const file of files) {
265
265
  const filePath = path.join(VERIFICATION_DIR, file);
266
266
  try {
267
- const artifact = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
267
+ const artifact = safeJsonParse(filePath, null);
268
+ if (!artifact) continue;
268
269
 
269
270
  // Verify HMAC signature
270
271
  const sigResult = verifyArtifactSignature(artifact);
@@ -18,7 +18,7 @@
18
18
 
19
19
  'use strict';
20
20
 
21
- const path = require('node:path');
21
+ const _path = require('node:path');
22
22
  const { getConfig, PATHS } = require('../../flow-utils');
23
23
 
24
24
  // ============================================================
@@ -14,7 +14,7 @@
14
14
  const fs = require('node:fs');
15
15
  const path = require('node:path');
16
16
 
17
- const { safeJsonParse, PATHS } = require('../../flow-utils');
17
+ const { safeJsonParse } = require('../../flow-utils');
18
18
 
19
19
  /**
20
20
  * Check if new packages have been added since last scan.
@@ -56,7 +56,7 @@ function getActiveLoopSession() {
56
56
  }
57
57
 
58
58
  return session;
59
- } catch (err) {
59
+ } catch (_err) {
60
60
  return null;
61
61
  }
62
62
  }
@@ -27,6 +27,7 @@
27
27
 
28
28
  const path = require('node:path');
29
29
  const fs = require('node:fs');
30
+ const { safeJsonParse } = require('../../flow-io');
30
31
 
31
32
  // ============================================================
32
33
  // Member Path Resolution
@@ -50,7 +51,7 @@ function getMemberPaths() {
50
51
  if (!fs.existsSync(manifestPath)) return [];
51
52
 
52
53
  try {
53
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
54
+ const manifest = safeJsonParse(manifestPath, {});
54
55
  const members = manifest.members || {};
55
56
  const paths = [];
56
57
 
@@ -141,7 +142,7 @@ function getMemberPort(memberName) {
141
142
 
142
143
  try {
143
144
  const manifestPath = path.join(workspaceRoot, '.workspace', 'state', 'workspace-manifest.json');
144
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
145
+ const manifest = safeJsonParse(manifestPath, {});
145
146
  const member = manifest.members?.[memberName];
146
147
  return member?.port ?? member?.channelPort ?? null;
147
148
  } catch (_err) {
@@ -161,7 +161,7 @@ function summarizeInput(toolName, toolInput) {
161
161
  const inputStr = JSON.stringify(toolInput);
162
162
  return `${toolName}: ${inputStr.slice(0, 60)}${inputStr.length > 60 ? '...' : ''}`;
163
163
  }
164
- } catch (err) {
164
+ } catch (_err) {
165
165
  return `${toolName}: (summarization failed)`;
166
166
  }
167
167
  }
@@ -232,7 +232,7 @@ function summarizeOutput(toolName, toolResponse, success) {
232
232
  default:
233
233
  return `Completed: ${responseStr.slice(0, 60)}${responseStr.length > 60 ? '...' : ''}`;
234
234
  }
235
- } catch (err) {
235
+ } catch (_err) {
236
236
  return 'Completed (summarization failed)';
237
237
  }
238
238
  }
@@ -298,7 +298,7 @@ async function captureObservation(options) {
298
298
  const blocks = getMemoryBlocks();
299
299
  const currentTask = blocks.getCurrentTask();
300
300
  contextTaskId = currentTask?.id || null;
301
- } catch (err) {
301
+ } catch (_err) {
302
302
  // Ignore - task context is optional
303
303
  }
304
304
 
@@ -316,7 +316,7 @@ async function captureObservation(options) {
316
316
  if (fullInput.length > maxInputSize) {
317
317
  fullInput = fullInput.slice(0, maxInputSize) + '...[truncated]';
318
318
  }
319
- } catch (err) {
319
+ } catch (_err) {
320
320
  fullInput = '[serialization failed]';
321
321
  }
322
322
 
@@ -328,7 +328,7 @@ async function captureObservation(options) {
328
328
  if (fullOutput.length > maxOutputSize) {
329
329
  fullOutput = fullOutput.slice(0, maxOutputSize) + '...[truncated]';
330
330
  }
331
- } catch (err) {
331
+ } catch (_err) {
332
332
  fullOutput = '[serialization failed]';
333
333
  }
334
334
 
@@ -356,7 +356,7 @@ async function captureObservation(options) {
356
356
  inputSummary || `${toolName} call`,
357
357
  outputSummary || 'unknown error'
358
358
  ).catch(() => { /* non-blocking */ });
359
- } catch (err) {
359
+ } catch (_err) {
360
360
  // Non-critical - memory pipeline may not be available
361
361
  }
362
362
  }
@@ -82,7 +82,7 @@ function isPhaseGateEnabled(config) {
82
82
  try {
83
83
  if (!config) config = getConfig();
84
84
  return config.hooks?.rules?.phaseGate?.enabled === true;
85
- } catch (err) {
85
+ } catch (_err) {
86
86
  return false; // Fail-open
87
87
  }
88
88
  }
@@ -104,7 +104,7 @@ function getCurrentPhase() {
104
104
  updatedAt: data.updatedAt || null,
105
105
  previousPhase: data.previousPhase || null
106
106
  };
107
- } catch (err) {
107
+ } catch (_err) {
108
108
  return defaults;
109
109
  }
110
110
  }
@@ -287,7 +287,7 @@ function checkPhaseGate(toolName, toolInput, config) {
287
287
  /**
288
288
  * Get guidance text for a blocked tool in a phase
289
289
  */
290
- function getPhaseGuidance(phase, toolName) {
290
+ function getPhaseGuidance(phase, _toolName) {
291
291
  const guidance = {
292
292
  idle: 'Route your request through a /wogi-* command first.',
293
293
  routing: 'Wait for /wogi-start to finish routing.',
@@ -381,7 +381,7 @@ function checkAndResetStalePhase() {
381
381
  return true;
382
382
  }
383
383
  return false;
384
- } catch (err) {
384
+ } catch (_err) {
385
385
  return false;
386
386
  }
387
387
  }
@@ -21,7 +21,7 @@
21
21
 
22
22
  const path = require('node:path');
23
23
  const fs = require('node:fs');
24
- const { PATHS, safeJsonParse, getConfig } = require('../../flow-utils');
24
+ const { PATHS, safeJsonParse } = require('../../flow-utils');
25
25
 
26
26
  /**
27
27
  * Phases where compaction should be BLOCKED because interruption
@@ -177,7 +177,7 @@ function runPreToolGates(ctx, deps) {
177
177
  ]);
178
178
  if (!skipRoutingGateForSubagent && !skipRoutingGateForReadOnlyGit && GATED_TOOLS.has(toolName)) {
179
179
  try {
180
- const routingResult = deps.checkRoutingGate(toolName, config, toolInput);
180
+ const routingResult = deps.checkRoutingGate(toolName, config);
181
181
  if (routingResult.blocked) {
182
182
  return {
183
183
  allowed: false,
@@ -250,7 +250,7 @@ function isRoutingPending() {
250
250
  const age = Date.now() - new Date(data.timestamp).getTime();
251
251
  if (age > ROUTING_FLAG_TTL_MS) {
252
252
  // Flag is stale — clean it up and return false
253
- try { fs.unlinkSync(ROUTING_FLAG_PATH); } catch (err) { /* ignore cleanup failure */ }
253
+ try { fs.unlinkSync(ROUTING_FLAG_PATH); } catch (_err) { /* ignore cleanup failure */ }
254
254
  if (process.env.DEBUG) {
255
255
  console.error(`[routing-gate] Cleaned stale flag (${Math.round(age / 1000)}s old)`);
256
256
  }
@@ -285,10 +285,9 @@ function isRoutingPending() {
285
285
  *
286
286
  * @param {string} toolName - The tool being called (e.g., 'Bash')
287
287
  * @param {Object} [config] - Pre-loaded config (optional, falls back to getConfig())
288
- * @param {Object} [toolInput] - Tool input (optional, used for v2.20.0 diagnostic bypass)
289
288
  * @returns {{ allowed: boolean, blocked: boolean, reason: string, message: string|null }}
290
289
  */
291
- function checkRoutingGate(toolName, config, toolInput) {
290
+ function checkRoutingGate(toolName, config) {
292
291
  // Gate ALL tools that allow the AI to act without routing through /wogi-start.
293
292
  // Edit/Write/NotebookEdit were the critical gap: AI could edit ready.json (exempt
294
293
  // from task gate) to create a fake active task, then edit anything freely.
@@ -318,26 +317,6 @@ function checkRoutingGate(toolName, config, toolInput) {
318
317
  // This meant any in-progress task from a prior turn bypassed routing entirely.
319
318
  // The only way to clear routing-pending is to invoke a /wogi-* skill.
320
319
 
321
- // Gap D (v2.20.0) — diagnostic curl bypass for workspace workers.
322
- // When a manager sends an INTROSPECTION/DIAGNOSTIC channel message, the
323
- // worker needs to curl-reply to localhost:8800 with a structured "## " body.
324
- // Without this bypass, answering diagnostic questions forces the worker to
325
- // create a fake task just to satisfy routing — which is itself an
326
- // anti-pattern. Narrow allowlist: Bash + curl + localhost:manager-port +
327
- // body starts with "## " + config flag enabled.
328
- try {
329
- if (toolName === 'Bash' && isDiagnosticCurlBypass(toolInput, config)) {
330
- return {
331
- allowed: true,
332
- blocked: false,
333
- reason: 'diagnostic_curl_bypass',
334
- message: null
335
- };
336
- }
337
- } catch (_err) {
338
- // Fail-closed — if bypass check errors, default to the normal block path.
339
- }
340
-
341
320
  // Block: routing is pending and no /wogi-* command has been invoked this turn
342
321
  // NOTE: This message is shown to the AI as permissionDecisionReason.
343
322
  // It must be prescriptive enough that the AI invokes /wogi-start instead of
@@ -358,66 +337,6 @@ function checkRoutingGate(toolName, config, toolInput) {
358
337
  };
359
338
  }
360
339
 
361
- /**
362
- * Gap D — recognize a narrow curl-to-manager bypass for diagnostic replies.
363
- *
364
- * Allowed iff ALL hold:
365
- * - config.workspace.diagnosticCurlBypass !== false
366
- * - Tool is Bash and command contains a single curl to
367
- * http(s)://(127\\.0\\.0\\.1|localhost):{managerPort} (default 8800)
368
- * - The curl body (`-d`, `--data`, `--data-binary`, `--data-raw`) starts
369
- * with "## " (structured channel reply marker)
370
- * - Body contains one of the diagnostic markers: "INTROSPECTION",
371
- * "DIAGNOSTIC", "## QUESTION:", or "## ANSWER:" (so generic curl-to-8800
372
- * doesn't escape routing — only diagnostic/question/answer replies do)
373
- *
374
- * This bypass is specifically NARROW by design — we want to unblock diagnostic
375
- * round-trips without opening a back door. Generic curl to any URL, curl to a
376
- * different port, or curl with a non-"## " body all still hit the normal block.
377
- *
378
- * @param {Object} toolInput - Bash tool input ({ command: string, ... })
379
- * @param {Object} config - Loaded config
380
- * @returns {boolean}
381
- */
382
- function isDiagnosticCurlBypass(toolInput, config) {
383
- if (!toolInput || typeof toolInput !== 'object') return false;
384
- if (config?.workspace?.diagnosticCurlBypass === false) return false;
385
-
386
- const command = String(toolInput.command || '');
387
- if (!command.includes('curl')) return false;
388
-
389
- // Must target localhost or 127.0.0.1 on the manager port.
390
- const managerPort = process.env.WOGI_MANAGER_PORT ||
391
- String(config?.workspace?.managerPort || '8800');
392
- // Validate port shape first — prevents regex injection.
393
- if (!/^\d{2,5}$/.test(String(managerPort))) return false;
394
- const portPattern = new RegExp(
395
- `https?://(?:127\\.0\\.0\\.1|localhost):${managerPort}(?:[/\\s"'\\\\]|$)`
396
- );
397
- if (!portPattern.test(command)) return false;
398
-
399
- // Extract the body argument. Recognized flags: -d, --data, --data-binary,
400
- // --data-raw, --data-urlencode. The body can be:
401
- // (a) literal string: -d "## ANSWER: ..."
402
- // (b) @- (from stdin — we can't inspect)
403
- // (c) @filename
404
- const bodyMatch = command.match(
405
- /--data(?:-binary|-raw|-urlencode)?\s+(['"])([\s\S]*?)\1|-d\s+(['"])([\s\S]*?)\3/
406
- );
407
- const literalBody = bodyMatch ? (bodyMatch[2] || bodyMatch[4] || '') : '';
408
-
409
- // Stdin / file bodies (@-) cannot be inspected — we conservatively reject
410
- // them for this bypass. The worker should use literal `-d "## ..."` instead.
411
- if (/--data(?:-binary|-raw|-urlencode)?\s+@|-d\s+@/.test(command) && !literalBody) {
412
- return false;
413
- }
414
-
415
- if (!literalBody.startsWith('## ')) return false;
416
-
417
- // Final marker check — body must contain one of the diagnostic markers.
418
- const markers = ['INTROSPECTION', 'DIAGNOSTIC', '## QUESTION:', '## ANSWER:'];
419
- return markers.some(m => literalBody.includes(m));
420
- }
421
340
 
422
341
  /**
423
342
  * Increment the stop-attempt counter in the routing flag.
@@ -467,7 +386,6 @@ function incrementStopAttempts(maxAttempts = 10) {
467
386
  }
468
387
 
469
388
  module.exports = {
470
- isDiagnosticCurlBypass,
471
389
  isRoutingGateEnabled,
472
390
  hasActiveTask,
473
391
  setRoutingPending,
@@ -809,7 +809,7 @@ async function gatherSessionContext(options = {}) {
809
809
  console.error(`[session-context] Community sync-down failed: ${err.message}`);
810
810
  }
811
811
  });
812
- } catch (err) {
812
+ } catch (_err) {
813
813
  // Non-critical — community sync module may not be available
814
814
  }
815
815