wogiflow 2.26.2 → 2.29.1
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 +149 -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-config-loader.js +3 -2
- 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-correction-detector.js +3 -2
- 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-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- 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-mcp-strip.js +122 -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
|
@@ -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 };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Workspace Autonomous-Run Completion Summary (Story B / wf-ab59f0e4)
|
|
5
|
+
*
|
|
6
|
+
* Workers emit completion summaries to the manager via the channel-dispatch
|
|
7
|
+
* HTTP bus. The wire format is single-line:
|
|
8
|
+
*
|
|
9
|
+
* ## COMPLETION-SUMMARY: <base64-JSON>
|
|
10
|
+
*
|
|
11
|
+
* Where <base64-JSON> is the same payload Story C produces locally, with a
|
|
12
|
+
* `workerId` field added for manager-side aggregation. Base64 was chosen
|
|
13
|
+
* over plain JSON because the channel-dispatch parser splits on `##`
|
|
14
|
+
* line-prefixes — multi-line raw JSON would split mid-payload (Blocker 5
|
|
15
|
+
* from the adversary critique).
|
|
16
|
+
*
|
|
17
|
+
* Chunked variant for >64KB payloads:
|
|
18
|
+
*
|
|
19
|
+
* ## COMPLETION-SUMMARY-CHUNK-1/3: <base64-fragment>
|
|
20
|
+
* ## COMPLETION-SUMMARY-CHUNK-2/3: <base64-fragment>
|
|
21
|
+
* ## COMPLETION-SUMMARY-CHUNK-3/3: <base64-fragment>
|
|
22
|
+
*
|
|
23
|
+
* Programmatic:
|
|
24
|
+
* const ws = require('./flow-workspace-summary');
|
|
25
|
+
* const lines = ws.encodeMessage(payload); // → ['## COMPLETION-SUMMARY: ...']
|
|
26
|
+
* const r = ws.parseMessage(line); // → { ok, payload?, error? }
|
|
27
|
+
* const r = ws.parseChunked(lines); // re-assemble chunks
|
|
28
|
+
* const text = ws.renderMultiWorker(summaries);
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const SINGLE_LINE_PREFIX = '## COMPLETION-SUMMARY: ';
|
|
32
|
+
const CHUNK_PREFIX_REGEX = /^## COMPLETION-SUMMARY-CHUNK-(\d+)\/(\d+):\s+/;
|
|
33
|
+
// Channel-dispatch lines are typically capped well under 64KB; pick a
|
|
34
|
+
// conservative single-line ceiling. Anything larger goes through chunking.
|
|
35
|
+
const SINGLE_LINE_MAX_BYTES = 60 * 1024;
|
|
36
|
+
|
|
37
|
+
const SEP = '━'.repeat(58);
|
|
38
|
+
|
|
39
|
+
function encodeBase64(payload) {
|
|
40
|
+
return Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function decodeBase64(s) {
|
|
44
|
+
try {
|
|
45
|
+
const buf = Buffer.from(s, 'base64');
|
|
46
|
+
const text = buf.toString('utf-8');
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new Error(`base64-JSON decode failed: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Encode a completion-summary payload into one or more channel-dispatch
|
|
55
|
+
* message lines. Always returns at least one line; chunks when the payload
|
|
56
|
+
* exceeds SINGLE_LINE_MAX_BYTES.
|
|
57
|
+
*/
|
|
58
|
+
function encodeMessage(payload) {
|
|
59
|
+
if (!payload || typeof payload !== 'object') {
|
|
60
|
+
throw new Error('encodeMessage: payload must be an object');
|
|
61
|
+
}
|
|
62
|
+
const encoded = encodeBase64(payload);
|
|
63
|
+
if (encoded.length + SINGLE_LINE_PREFIX.length <= SINGLE_LINE_MAX_BYTES) {
|
|
64
|
+
return [`${SINGLE_LINE_PREFIX}${encoded}`];
|
|
65
|
+
}
|
|
66
|
+
const chunkSize = SINGLE_LINE_MAX_BYTES - 64;
|
|
67
|
+
const total = Math.ceil(encoded.length / chunkSize);
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (let i = 0; i < total; i++) {
|
|
70
|
+
const fragment = encoded.slice(i * chunkSize, (i + 1) * chunkSize);
|
|
71
|
+
lines.push(`## COMPLETION-SUMMARY-CHUNK-${i + 1}/${total}: ${fragment}`);
|
|
72
|
+
}
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isCompletionSummaryLine(line) {
|
|
77
|
+
if (typeof line !== 'string') return false;
|
|
78
|
+
if (line.startsWith(SINGLE_LINE_PREFIX)) return true;
|
|
79
|
+
return CHUNK_PREFIX_REGEX.test(line);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseMessage(line) {
|
|
83
|
+
if (typeof line !== 'string') {
|
|
84
|
+
return { ok: false, error: 'line must be a string' };
|
|
85
|
+
}
|
|
86
|
+
if (!line.startsWith(SINGLE_LINE_PREFIX)) {
|
|
87
|
+
return { ok: false, error: 'not a single-line COMPLETION-SUMMARY' };
|
|
88
|
+
}
|
|
89
|
+
const b64 = line.slice(SINGLE_LINE_PREFIX.length).trim();
|
|
90
|
+
try {
|
|
91
|
+
const payload = decodeBase64(b64);
|
|
92
|
+
return validatePayload(payload);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return { ok: false, error: err.message };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseChunked(lines) {
|
|
99
|
+
if (!Array.isArray(lines) || lines.length === 0) {
|
|
100
|
+
return { ok: false, error: 'lines must be a non-empty array' };
|
|
101
|
+
}
|
|
102
|
+
const fragments = [];
|
|
103
|
+
let total = null;
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const m = CHUNK_PREFIX_REGEX.exec(line);
|
|
106
|
+
if (!m) return { ok: false, error: 'non-chunk line in chunked input' };
|
|
107
|
+
const n = parseInt(m[1], 10);
|
|
108
|
+
const t = parseInt(m[2], 10);
|
|
109
|
+
if (total === null) total = t;
|
|
110
|
+
if (t !== total) return { ok: false, error: 'mismatched chunk totals' };
|
|
111
|
+
if (!Number.isInteger(n) || n < 1 || n > total) {
|
|
112
|
+
return { ok: false, error: `invalid chunk index: ${m[1]}` };
|
|
113
|
+
}
|
|
114
|
+
fragments[n - 1] = line.replace(CHUNK_PREFIX_REGEX, '');
|
|
115
|
+
}
|
|
116
|
+
if (fragments.length !== total || fragments.some(f => f === undefined)) {
|
|
117
|
+
return { ok: false, error: `missing chunks (have ${fragments.filter(Boolean).length} of ${total})` };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const payload = decodeBase64(fragments.join(''));
|
|
121
|
+
return validatePayload(payload);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return { ok: false, error: err.message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate the shape of a completion-summary payload. Returns
|
|
129
|
+
* { ok: true, payload } or { ok: false, error }.
|
|
130
|
+
*/
|
|
131
|
+
function validatePayload(payload) {
|
|
132
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
133
|
+
return { ok: false, error: 'payload must be a JSON object' };
|
|
134
|
+
}
|
|
135
|
+
if (typeof payload.runId !== 'string' || !payload.runId) {
|
|
136
|
+
return { ok: false, error: 'missing runId' };
|
|
137
|
+
}
|
|
138
|
+
for (const key of ['completed', 'queuedQuestions', 'skippedTasks']) {
|
|
139
|
+
if (!Array.isArray(payload[key])) {
|
|
140
|
+
return { ok: false, error: `${key} must be an array` };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, payload };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render a multi-worker workspace summary block. `summaries` is an array
|
|
148
|
+
* of validated payloads (each may include `workerId`).
|
|
149
|
+
*
|
|
150
|
+
* Empty-collection rule (decisions.md 2026-04-23): all 3 sections always
|
|
151
|
+
* render per worker. Workers with no work still appear with empty-state
|
|
152
|
+
* placeholders so the user never wonders if a worker is missing.
|
|
153
|
+
*/
|
|
154
|
+
function renderMultiWorker(summaries) {
|
|
155
|
+
const list = Array.isArray(summaries) ? summaries : [];
|
|
156
|
+
const lines = [];
|
|
157
|
+
lines.push(SEP);
|
|
158
|
+
lines.push('WORKSPACE AUTONOMOUS RUN COMPLETE');
|
|
159
|
+
lines.push(SEP);
|
|
160
|
+
lines.push('');
|
|
161
|
+
|
|
162
|
+
if (list.length === 0) {
|
|
163
|
+
lines.push('[no worker summaries received]');
|
|
164
|
+
lines.push(SEP);
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let totalCompleted = 0;
|
|
169
|
+
let totalQuestions = 0;
|
|
170
|
+
let totalSkipped = 0;
|
|
171
|
+
for (const s of list) {
|
|
172
|
+
const workerId = s.workerId || 'unknown';
|
|
173
|
+
const dur = formatDuration(s.startedAt, s.endedAt);
|
|
174
|
+
const completed = Array.isArray(s.completed) ? s.completed : [];
|
|
175
|
+
const queued = Array.isArray(s.queuedQuestions) ? s.queuedQuestions : [];
|
|
176
|
+
const skipped = Array.isArray(s.skippedTasks) ? s.skippedTasks : [];
|
|
177
|
+
totalCompleted += completed.length;
|
|
178
|
+
totalQuestions += queued.length;
|
|
179
|
+
totalSkipped += skipped.length;
|
|
180
|
+
|
|
181
|
+
lines.push(`Worker: ${workerId} (runId: ${s.runId}, duration: ${dur})`);
|
|
182
|
+
lines.push(` ✓ Completed (${completed.length}):`);
|
|
183
|
+
if (completed.length === 0) {
|
|
184
|
+
lines.push(' [none]');
|
|
185
|
+
} else {
|
|
186
|
+
for (const t of completed) lines.push(` - ${t.taskId}: ${t.title || '(no title)'}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(` ? Queued questions (${queued.length}):`);
|
|
189
|
+
if (queued.length === 0) {
|
|
190
|
+
lines.push(' [none]');
|
|
191
|
+
} else {
|
|
192
|
+
for (const q of queued) {
|
|
193
|
+
const blockers = Array.isArray(q.dependencies) && q.dependencies.length
|
|
194
|
+
? ` (blocks: ${q.dependencies.join(', ')})`
|
|
195
|
+
: '';
|
|
196
|
+
lines.push(` - ${q.id}: ${q.text}${blockers}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
lines.push(` ⊘ Skipped tasks (${skipped.length}):`);
|
|
200
|
+
if (skipped.length === 0) {
|
|
201
|
+
lines.push(' [none]');
|
|
202
|
+
} else {
|
|
203
|
+
for (const sk of skipped) {
|
|
204
|
+
const ref = sk.blockingQuestionId ? ` (awaiting ${sk.blockingQuestionId})` : '';
|
|
205
|
+
lines.push(` - ${sk.taskId}: ${sk.reason}${ref}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (s.endReason && s.endReason !== 'queue-drained') {
|
|
209
|
+
lines.push(` ⚠ endReason: ${s.endReason}`);
|
|
210
|
+
}
|
|
211
|
+
lines.push('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
lines.push(`Total: ${totalCompleted} completed, ${totalQuestions} questions queued, ${totalSkipped} skipped across ${list.length} worker${list.length === 1 ? '' : 's'}`);
|
|
215
|
+
lines.push(SEP);
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatDuration(startedAt, endedAt) {
|
|
220
|
+
if (!startedAt || !endedAt) return '0:00';
|
|
221
|
+
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
222
|
+
if (!Number.isFinite(ms) || ms < 0) return '0:00';
|
|
223
|
+
const sec = Math.floor(ms / 1000);
|
|
224
|
+
const m = Math.floor(sec / 60);
|
|
225
|
+
const s = sec % 60;
|
|
226
|
+
if (m >= 60) {
|
|
227
|
+
const h = Math.floor(m / 60);
|
|
228
|
+
return `${h}:${String(m % 60).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
229
|
+
}
|
|
230
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
SINGLE_LINE_PREFIX,
|
|
235
|
+
CHUNK_PREFIX_REGEX,
|
|
236
|
+
SINGLE_LINE_MAX_BYTES,
|
|
237
|
+
encodeMessage,
|
|
238
|
+
parseMessage,
|
|
239
|
+
parseChunked,
|
|
240
|
+
isCompletionSummaryLine,
|
|
241
|
+
validatePayload,
|
|
242
|
+
renderMultiWorker
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
if (require.main === module) {
|
|
246
|
+
const [,, cmd, ...rest] = process.argv;
|
|
247
|
+
if (cmd === 'encode') {
|
|
248
|
+
const payload = JSON.parse(rest.join(' '));
|
|
249
|
+
console.log(encodeMessage(payload).join('\n'));
|
|
250
|
+
} else if (cmd === 'parse') {
|
|
251
|
+
const r = parseMessage(rest.join(' '));
|
|
252
|
+
console.log(JSON.stringify(r, null, 2));
|
|
253
|
+
} else {
|
|
254
|
+
console.log('Usage: flow-workspace-summary <encode <json>|parse <line>>');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -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,
|
|
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,
|
|
61
|
+
generateConfig(_rules, _projectRoot, _transportConfig) {
|
|
62
62
|
throw new Error('generateConfig() must be implemented by subclass');
|
|
63
63
|
}
|
|
64
64
|
|