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
|
@@ -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
|
|
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,
|
|
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,
|
|
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 = {},
|
|
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 {
|
|
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 = {},
|
|
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 [
|
|
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,
|
|
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 [
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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;
|
package/scripts/flow-utils.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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(`[
|
|
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 };
|