wogiflow 2.26.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -23,7 +23,7 @@ const {
23
23
  runStandardsCheck,
24
24
  formatStandardsResults,
25
25
  collectReuseCandidates,
26
- TASK_CHECK_MAP
26
+ _TASK_CHECK_MAP
27
27
  } = require('./flow-standards-checker');
28
28
 
29
29
  // Gate telemetry — records every invocation for self-assessment.
@@ -42,6 +42,11 @@ const FORMATS = {
42
42
  name: 'Detailed',
43
43
  description: 'Full info including skill and worktree',
44
44
  format: '{{#if workspace.git_worktree}}[WT] {{/if}}{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}% used{{#if skill}} | {{skill}}{{/if}}'
45
+ },
46
+ advanced: {
47
+ name: 'Advanced',
48
+ description: 'Detailed + effort level and thinking state (Claude Code 2.1.119+)',
49
+ format: '{{#if workspace.git_worktree}}[WT] {{/if}}{{#if task}}[{{task.id}}] {{task.title}} | {{/if}}{{model}} | {{context_window.used_percentage}}%{{#if effort.level}} | {{effort.level}}{{/if}}{{#if thinking.enabled}} | thinking{{/if}}{{#if skill}} | {{skill}}{{/if}}'
45
50
  }
46
51
  };
47
52
 
@@ -157,7 +162,7 @@ async function interactiveSetup() {
157
162
  showCurrentConfig();
158
163
  showFormats();
159
164
 
160
- const format = await question(`\nChoose format (minimal/compact/standard/detailed) [standard]: `);
165
+ const format = await question(`\nChoose format (minimal/compact/standard/detailed/advanced) [standard]: `);
161
166
  const selectedFormat = format.trim() || 'standard';
162
167
 
163
168
  if (!FORMATS[selectedFormat]) {
@@ -226,6 +231,7 @@ Formats:
226
231
  compact - Task ID + model + context %
227
232
  standard - Task ID + model + labeled context (recommended)
228
233
  detailed - Worktree + task + model + context % + skill
234
+ advanced - Detailed + effort level + thinking state (Claude Code 2.1.119+)
229
235
 
230
236
  Refresh interval (Claude Code 2.1.97+):
231
237
  Re-runs the status line every N seconds so live values like task ID,
@@ -281,7 +287,7 @@ Examples:
281
287
  if (formatIndex >= 0) {
282
288
  const format = args[formatIndex + 1];
283
289
  if (!format || !FORMATS[format]) {
284
- errorMsg('Invalid format. Use: minimal, compact, standard, or detailed');
290
+ errorMsg('Invalid format. Use: minimal, compact, standard, detailed, or advanced');
285
291
  process.exit(1);
286
292
  }
287
293
 
@@ -26,7 +26,7 @@ const CHANGELOG_PATH = path.join(PATHS.root, 'CHANGELOG.md');
26
26
  * @returns {object} - { passed: boolean, message: string, entry?: string }
27
27
  */
28
28
  async function run(options = {}) {
29
- const { taskId, taskTitle, taskType, files = [], mode, stepConfig = {} } = options;
29
+ const { taskId, taskTitle, taskType, files = [], mode, _stepConfig = {} } = options;
30
30
 
31
31
  // Determine changelog category
32
32
  const category = getChangelogCategory(taskType, taskTitle, files);
@@ -133,7 +133,7 @@ function getChangelogCategory(taskType, taskTitle, _files) {
133
133
  /**
134
134
  * Generate a changelog entry
135
135
  */
136
- function generateEntry(taskId, taskTitle, _category, files) {
136
+ function generateEntry(taskId, taskTitle, _category, _files) {
137
137
  // Clean up title
138
138
  let entry = taskTitle || 'Update';
139
139
 
@@ -31,7 +31,7 @@ const COVERAGE_PATHS = [
31
31
  * @returns {object} - { passed: boolean, message: string, coverage?: object }
32
32
  */
33
33
  async function run(options = {}) {
34
- const { files = [], stepConfig = {}, mode } = options;
34
+ const { files = [], stepConfig = {}, _mode } = options;
35
35
  const minCoverage = stepConfig.minCoverage || 80;
36
36
  const checkFiles = stepConfig.checkModifiedOnly ?? true;
37
37
 
@@ -26,7 +26,7 @@ const KNOWLEDGE_DIR = path.join(PATHS.root, '.claude', 'docs', 'knowledge-base')
26
26
  * @returns {object} - { passed: boolean, message: string, suggestion?: string }
27
27
  */
28
28
  async function run(options = {}) {
29
- const { taskId, taskTitle, files = [], mode, stepConfig = {}, learnings } = options;
29
+ const { _taskId, taskTitle, files = [], mode, _stepConfig = {}, learnings } = options;
30
30
 
31
31
  // Ensure knowledge base directory exists
32
32
  if (!fs.existsSync(KNOWLEDGE_DIR)) {
@@ -21,7 +21,7 @@ const { PATHS } = require('./flow-utils');
21
21
  * @returns {object} - { passed: boolean, message: string, details?: object }
22
22
  */
23
23
  async function run(options = {}) {
24
- const { stepConfig = {}, mode } = options;
24
+ const { stepConfig = {}, _mode } = options;
25
25
  const sampleSize = stepConfig.sampleSize || 3;
26
26
 
27
27
  try {
@@ -308,7 +308,7 @@ function findDuplicationPatterns(content, fileName) {
308
308
  }
309
309
 
310
310
  // Report lines that appear 3+ times
311
- for (const [pattern, lineNumbers] of Object.entries(patterns)) {
311
+ for (const [_pattern, lineNumbers] of Object.entries(patterns)) {
312
312
  if (lineNumbers.length >= 3) {
313
313
  suggestions.push({
314
314
  file: fileName,
@@ -355,7 +355,7 @@ function determineCapabilities(text, complexity) {
355
355
  * @returns {Object} Token estimates
356
356
  */
357
357
  function estimateTaskTokens(analysis) {
358
- const { complexity, domains, languages } = analysis;
358
+ const { complexity, domains, _languages } = analysis;
359
359
 
360
360
  const multiplier = TOKEN_FACTORS.COMPLEXITY_MULTIPLIER[complexity.level];
361
361
  const baseInput = TOKEN_FACTORS.BASE_INPUT;
@@ -176,7 +176,7 @@ function classifyTask(taskDescription, affectedFiles = [], _options = {}) {
176
176
  .sort((a, b) => b[1] - a[1]);
177
177
 
178
178
  const [topType, topScore] = sortedTypes[0];
179
- const [secondType, secondScore] = sortedTypes[1] || ['none', 0];
179
+ const [_secondType, secondScore] = sortedTypes[1] || ['none', 0];
180
180
 
181
181
  // Calculate confidence
182
182
  let confidence = 'high';
@@ -752,7 +752,7 @@ function getLoopStats() {
752
752
  */
753
753
  function verifyCriterion(criterion, context = {}) {
754
754
  const { execSync, execFileSync } = require('node:child_process');
755
- const { changedFiles = [], testResults = null, lintResults = null } = context;
755
+ const { _changedFiles = [], testResults = null, lintResults = null } = context;
756
756
  const _config = getConfig();
757
757
  const taskConfig = getTaskConfig();
758
758
  const desc = criterion.description;
@@ -461,7 +461,7 @@ function getIndent(line) {
461
461
  async function extractTemplates(projectRoot, options = {}) {
462
462
  const {
463
463
  types = Object.keys(FILE_TYPES),
464
- outputDir = path.join(projectRoot, '.workflow', 'templates', 'extracted')
464
+ _outputDir = path.join(projectRoot, '.workflow', 'templates', 'extracted')
465
465
  } = options;
466
466
 
467
467
  const startTime = Date.now();
@@ -264,7 +264,7 @@ function parseTypeScript(file) {
264
264
  /(?:^|\n)\s*(?:export\s+)?(interface|class|type)\s+([A-Z]\w*)\b\s*(?:extends[^{]*|implements[^{]*|<[^>]*>\s*)?(?:=\s*)?(\{)/g;
265
265
  let m;
266
266
  while ((m = headerRegex.exec(content)) !== null) {
267
- const [, kind, name, openBrace] = m;
267
+ const [, kind, name, _openBrace] = m;
268
268
  const bodyStart = m.index + m[0].length;
269
269
  const body = extractBalancedBlock(content, bodyStart - 1); // include the opening brace
270
270
  if (!body) continue;
@@ -262,6 +262,10 @@ function isValidWogiId(id) {
262
262
  if (/^wf-rv-[a-f0-9]{8}$/i.test(id)) return true;
263
263
  // Epic, feature, plan IDs
264
264
  if (/^(ep|ft|pl)-[a-f0-9]{8}$/i.test(id)) return true;
265
+ // Slug format: wf-<alphanum>[<alphanum or hyphen>]*<alphanum>, 5-64 chars.
266
+ // For manager-dispatched descriptive IDs. Path-safe (no dots/separators).
267
+ // Keep this in sync with validateTaskId() 'slug' branch in flow-id.js.
268
+ if (/^wf-[a-z0-9][a-z0-9-]{0,60}[a-z0-9]$/i.test(id)) return true;
265
269
  // Legacy format
266
270
  if (/^(TASK|BUG)-\d{3,}$/i.test(id)) return true;
267
271
  return false;
@@ -142,6 +142,36 @@ Examples:
142
142
  - "Task complete. Next: wf-abc12345 — starting now." → {"isUserQuestion": false, "confidence": 95, "reason": "action statement, no question"}`;
143
143
  }
144
144
 
145
+ /**
146
+ * Build the main-mode classifier prompt. Used by the task-boundary-reset path
147
+ * in solo/main-mode sessions to detect when the AI forgot to call `flow ask`
148
+ * before ending a turn with a user-facing question. A YES classification
149
+ * auto-writes the pending-question marker and defers the restart.
150
+ *
151
+ * Same shape as buildClassifierPrompt — only the contextual framing differs.
152
+ */
153
+ function buildMainModePrompt(lastMessage) {
154
+ return `You classify whether an AI assistant's final message to a SOLO (non-workspace) session ends by asking the USER a question that expects a user response.
155
+
156
+ Context: in solo mode the session has a task-boundary session-restart feature. Before the restart fires, this classifier checks whether the AI is waiting on a user answer. If YES, the restart is deferred so the user's next reply lands in the same session context. The safety net exists because the AI should have called \`flow ask\` manually but sometimes forgets.
157
+
158
+ Your job: classify YES only when the AI's final message contains an OPEN question the AI is waiting on the user to answer. Classify NO for rhetorical questions the AI answers itself, narrative descriptions, "here are your options" menus with the AI continuing after, or questions that have an accompanying decision.
159
+
160
+ [MESSAGE_START]
161
+ ${String(lastMessage || '').slice(0, MAX_MESSAGE_CHARS)}
162
+ [MESSAGE_END]
163
+
164
+ Return JSON only, no prose, no markdown fences:
165
+ {"isUserQuestion": true|false, "confidence": 0-100, "reason": "one short sentence"}
166
+
167
+ Examples:
168
+ - "Confirm this approach, or want a different split?" → {"isUserQuestion": true, "confidence": 95, "reason": "open confirm/alternate awaiting user"}
169
+ - "Option 1 (rule only) or option 2 (classifier)?" → {"isUserQuestion": true, "confidence": 95, "reason": "binary choice awaiting user"}
170
+ - "Did the tests pass? Yes, all 12 passed." → {"isUserQuestion": false, "confidence": 90, "reason": "rhetorical, answered inline"}
171
+ - "Task complete. Moving to next task now." → {"isUserQuestion": false, "confidence": 95, "reason": "action statement, no question"}
172
+ - "Implementation done — committing and pushing." → {"isUserQuestion": false, "confidence": 95, "reason": "action statement, no question"}`;
173
+ }
174
+
145
175
  /**
146
176
  * Guard parsed JSON response against prototype pollution. Mirrors
147
177
  * flow-conclusion-classifier.hasDangerousKeys.
@@ -157,10 +187,11 @@ function hasDangerousKeys(value) {
157
187
  }
158
188
 
159
189
  /**
160
- * Classify the worker's last assistant message.
190
+ * Classify the assistant's last message in either worker or main mode.
161
191
  *
162
192
  * @param {Object} opts
163
193
  * @param {string} opts.transcriptPath - Absolute path to session JSONL transcript
194
+ * @param {'worker'|'main'} [opts.mode='worker'] - Which prompt framing to use
164
195
  * @param {number} [opts.minConfidence] - Confidence threshold (default 70)
165
196
  * @param {string} [opts.model] - Model override (default haiku)
166
197
  * @returns {Promise<{
@@ -168,10 +199,13 @@ function hasDangerousKeys(value) {
168
199
  * isUserQuestion?: boolean,
169
200
  * confidence?: number,
170
201
  * reason?: string,
171
- * lastMessage?: string
202
+ * lastMessage?: string,
203
+ * blocked?: boolean,
204
+ * minConfidence?: number
172
205
  * }>}
173
206
  */
174
- async function classifyWorkerQuestion(opts = {}) {
207
+ async function classifyQuestion(opts = {}) {
208
+ const mode = opts.mode === 'main' ? 'main' : 'worker';
175
209
  const minConfidence = Number.isFinite(opts.minConfidence) ? opts.minConfidence : DEFAULT_MIN_CONFIDENCE;
176
210
  const model = opts.model || DEFAULT_MODEL;
177
211
 
@@ -195,15 +229,19 @@ async function classifyWorkerQuestion(opts = {}) {
195
229
  return { classified: false, reason: 'no-model-caller' };
196
230
  }
197
231
 
232
+ const prompt = mode === 'main'
233
+ ? buildMainModePrompt(lastMessage)
234
+ : buildClassifierPrompt(lastMessage);
235
+
198
236
  let result;
199
237
  try {
200
- result = await callModel(model, buildClassifierPrompt(lastMessage), {
238
+ result = await callModel(model, prompt, {
201
239
  temperature: TEMPERATURE,
202
240
  maxTokens: MAX_TOKENS
203
241
  });
204
242
  } catch (err) {
205
243
  if (process.env.DEBUG) {
206
- console.error(`[worker-question-classifier] model call failed: ${err.message}`);
244
+ console.error(`[question-classifier:${mode}] model call failed: ${err.message}`);
207
245
  }
208
246
  return { classified: false, reason: 'model-error' };
209
247
  }
@@ -245,11 +283,19 @@ async function classifyWorkerQuestion(opts = {}) {
245
283
  };
246
284
  }
247
285
 
286
+ // Preserve the original worker-mode export name for zero signature break.
287
+ // New callers should prefer classifyQuestion({ mode }) directly.
288
+ async function classifyWorkerQuestion(opts = {}) {
289
+ return classifyQuestion({ ...opts, mode: 'worker' });
290
+ }
291
+
248
292
  module.exports = {
293
+ classifyQuestion,
249
294
  classifyWorkerQuestion,
250
295
  extractLastAssistantMessage,
251
296
  extractAssistantText,
252
297
  buildClassifierPrompt,
298
+ buildMainModePrompt,
253
299
  hasDangerousKeys,
254
300
  DEFAULT_MIN_CONFIDENCE,
255
301
  DEFAULT_MODEL
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Workspace — IPC Migration / Re-Index (wf-3635574e / G3, AC4)
5
+ *
6
+ * One-shot + idempotent script that rebuilds the per-worker SQLite IPC index
7
+ * (under `.workspace/state/ipc/<repoName>/{inbound,outbound}.db`) from the
8
+ * authoritative JSON sources:
9
+ *
10
+ * - `.workspace/messages/msg-*.json` (message bus)
11
+ * - `.workspace/state/dispatched-tasks.json` (ring buffer — optional)
12
+ *
13
+ * Routing per message `from`/`to`:
14
+ * from == 'manager' → <to>/inbound.db
15
+ * to == 'manager' → <from>/outbound.db
16
+ * to == 'all' → <from>/outbound.db (broadcast)
17
+ * worker → worker → <to>/inbound.db (manager-brokered semantics)
18
+ *
19
+ * Idempotent: UPSERT on message id. Re-running scans all JSON again and
20
+ * re-writes, which is safe — consumed_at is preserved via COALESCE in the
21
+ * UPSERT path (see workspace-ipc-sqlite.js).
22
+ *
23
+ * Usage:
24
+ * node scripts/flow-workspace-migrate-ipc.js <workspaceRoot> [--quiet]
25
+ * node scripts/flow-workspace-migrate-ipc.js --help
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+ const ipc = require('../lib/workspace-ipc-sqlite');
33
+
34
+ const USAGE = `Usage: flow-workspace-migrate-ipc.js <workspaceRoot> [--quiet]
35
+
36
+ Rebuilds the SQLite IPC index from existing JSON message bus + dispatch
37
+ tracking files. Safe to run repeatedly; does not delete JSON files.
38
+
39
+ Options:
40
+ --quiet Suppress per-file log output.
41
+ --help Show this help.
42
+ `;
43
+
44
+ function log(quiet, ...args) {
45
+ if (!quiet) console.log(...args);
46
+ }
47
+
48
+ function parseArgs(argv) {
49
+ const args = { workspaceRoot: null, quiet: false, help: false };
50
+ for (const a of argv.slice(2)) {
51
+ if (a === '--help' || a === '-h') { args.help = true; continue; }
52
+ if (a === '--quiet' || a === '-q') { args.quiet = true; continue; }
53
+ if (!args.workspaceRoot) { args.workspaceRoot = a; continue; }
54
+ }
55
+ return args;
56
+ }
57
+
58
+ function safeReadJson(filePath) {
59
+ try {
60
+ const raw = fs.readFileSync(filePath, 'utf-8');
61
+ return JSON.parse(raw);
62
+ } catch (_err) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Decide which repo's DB and which direction an existing JSON message belongs to.
69
+ */
70
+ function routeMessage(msg) {
71
+ const from = typeof msg.from === 'string' ? msg.from : '';
72
+ const to = typeof msg.to === 'string' ? msg.to : '';
73
+
74
+ if (from === 'manager' && to && to !== 'all' && to !== 'manager') {
75
+ return { repoName: to, direction: 'inbound' };
76
+ }
77
+ if (to === 'manager' && from) {
78
+ return { repoName: from, direction: 'outbound' };
79
+ }
80
+ if (to === 'all' && from && from !== 'manager') {
81
+ return { repoName: from, direction: 'outbound' };
82
+ }
83
+ if (from && to && from !== to) {
84
+ return { repoName: to, direction: 'inbound' };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function inferConsumedAt(msg) {
90
+ if (typeof msg.consumed_at === 'string') return msg.consumed_at;
91
+ if (typeof msg.consumedAt === 'string') return msg.consumedAt;
92
+ if (msg.status && msg.status !== 'pending') {
93
+ return msg.updatedAt || msg.resolvedAt || null;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ async function migrateMessages(workspaceRoot, quiet) {
99
+ const messagesDir = path.join(workspaceRoot, '.workspace', 'messages');
100
+ if (!fs.existsSync(messagesDir)) {
101
+ log(quiet, `[migrate-ipc] No messages dir at ${messagesDir} — skipping.`);
102
+ return { scanned: 0, indexed: 0, skipped: 0 };
103
+ }
104
+
105
+ const files = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
106
+ let indexed = 0;
107
+ let skipped = 0;
108
+
109
+ for (const file of files) {
110
+ const filePath = path.join(messagesDir, file);
111
+ const msg = safeReadJson(filePath);
112
+ if (!msg || !msg.id) { skipped++; continue; }
113
+
114
+ const route = routeMessage(msg);
115
+ if (!route) { skipped++; continue; }
116
+
117
+ const kind = typeof msg.type === 'string' ? msg.type : 'unknown';
118
+ const ok = await ipc.indexMessage(workspaceRoot, route.repoName, route.direction, {
119
+ id: msg.id,
120
+ kind,
121
+ payload: msg,
122
+ createdAt: msg.timestamp || new Date().toISOString(),
123
+ consumedAt: inferConsumedAt(msg)
124
+ });
125
+
126
+ if (ok) {
127
+ indexed++;
128
+ log(quiet, `[migrate-ipc] indexed ${msg.id} → ${route.repoName}/${route.direction}.db (${kind})`);
129
+ } else {
130
+ skipped++;
131
+ }
132
+ }
133
+
134
+ return { scanned: files.length, indexed, skipped };
135
+ }
136
+
137
+ async function migrateDispatches(workspaceRoot, quiet) {
138
+ const dispatchPath = path.join(workspaceRoot, '.workspace', 'state', 'dispatched-tasks.json');
139
+ if (!fs.existsSync(dispatchPath)) {
140
+ log(quiet, `[migrate-ipc] No dispatched-tasks.json — skipping dispatch index.`);
141
+ return { scanned: 0, indexed: 0, skipped: 0 };
142
+ }
143
+
144
+ const state = safeReadJson(dispatchPath);
145
+ if (!state || !Array.isArray(state.dispatches)) {
146
+ return { scanned: 0, indexed: 0, skipped: 0 };
147
+ }
148
+
149
+ let indexed = 0;
150
+ let skipped = 0;
151
+
152
+ for (const rec of state.dispatches) {
153
+ if (!rec || !rec.taskId || !rec.repoName) { skipped++; continue; }
154
+
155
+ // Synthesize a stable message id from taskId + dispatchedAt.
156
+ const id = `disp-${rec.taskId}-${Date.parse(rec.dispatchedAt || '') || 0}`.substring(0, 80);
157
+
158
+ const ok = await ipc.indexMessage(workspaceRoot, rec.repoName, 'inbound', {
159
+ id,
160
+ kind: 'task-dispatch',
161
+ payload: rec,
162
+ createdAt: rec.dispatchedAt || new Date().toISOString(),
163
+ consumedAt: rec.reconciledAt || (rec.status && rec.status !== 'pending' ? new Date().toISOString() : null)
164
+ });
165
+
166
+ if (ok) indexed++;
167
+ else skipped++;
168
+ }
169
+
170
+ return { scanned: state.dispatches.length, indexed, skipped };
171
+ }
172
+
173
+ async function main() {
174
+ const args = parseArgs(process.argv);
175
+ if (args.help) { console.log(USAGE); process.exit(0); }
176
+
177
+ const workspaceRoot = args.workspaceRoot
178
+ || process.env.WOGI_WORKSPACE_ROOT
179
+ || process.cwd();
180
+
181
+ if (!fs.existsSync(workspaceRoot)) {
182
+ console.error(`[migrate-ipc] Workspace root does not exist: ${workspaceRoot}`);
183
+ process.exit(2);
184
+ }
185
+
186
+ if (!(await ipc.isAvailable())) {
187
+ console.error(`[migrate-ipc] SQLite (sql.js) unavailable: ${ipc.unavailableReason()}`);
188
+ console.error(`[migrate-ipc] AC5 fallback active — JSON message bus will continue to serve reads/writes.`);
189
+ console.error(`[migrate-ipc] No migration performed.`);
190
+ process.exit(3);
191
+ }
192
+
193
+ log(args.quiet, `[migrate-ipc] workspaceRoot: ${workspaceRoot}`);
194
+ log(args.quiet, `[migrate-ipc] SQLite ready.`);
195
+
196
+ const msgRes = await migrateMessages(workspaceRoot, args.quiet);
197
+ log(args.quiet, `[migrate-ipc] messages: scanned=${msgRes.scanned} indexed=${msgRes.indexed} skipped=${msgRes.skipped}`);
198
+
199
+ const dispRes = await migrateDispatches(workspaceRoot, args.quiet);
200
+ log(args.quiet, `[migrate-ipc] dispatches: scanned=${dispRes.scanned} indexed=${dispRes.indexed} skipped=${dispRes.skipped}`);
201
+
202
+ await ipc.closeAll();
203
+
204
+ const repos = ipc.listIndexedRepos(workspaceRoot);
205
+ log(args.quiet, `[migrate-ipc] indexed repos: ${repos.join(', ') || '(none)'}`);
206
+ log(args.quiet, `[migrate-ipc] done.`);
207
+ }
208
+
209
+ if (require.main === module) {
210
+ main().catch(err => {
211
+ console.error('[migrate-ipc] FAILED:', err && err.stack ? err.stack : err);
212
+ process.exit(1);
213
+ });
214
+ }
215
+
216
+ module.exports = { routeMessage, migrateMessages, migrateDispatches };