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
|
@@ -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
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Feature Dossier Gate (Core Module)
|
|
5
|
+
*
|
|
6
|
+
* Auto-injects matching feature dossiers + cross-cutting logic rules into
|
|
7
|
+
* the phase context so Claude doesn't have to fetch them under token pressure.
|
|
8
|
+
*
|
|
9
|
+
* Problem this solves (2026-04-24 workspace failure catalog):
|
|
10
|
+
* - Claude doesn't proactively fetch context — grabs one thing, ignores rest
|
|
11
|
+
* - Feature/workflow knowledge is lost between sessions
|
|
12
|
+
* - Owner corrections stop being remembered
|
|
13
|
+
* - "I know the rule exists" ≠ "I consulted it before acting"
|
|
14
|
+
*
|
|
15
|
+
* Two enforcement surfaces:
|
|
16
|
+
* 1. buildPhaseInjection() — called by UserPromptSubmit. Returns a
|
|
17
|
+
* markdown block with the top matching dossiers' canonical content.
|
|
18
|
+
* Injected into the phase prompt alongside the phase-context injection.
|
|
19
|
+
*
|
|
20
|
+
* 2. validateSpecContradictions() — called from /wogi-story spec-review.
|
|
21
|
+
* Scans a spec file against every matching dossier and returns
|
|
22
|
+
* blocking issues (spec mentions rejected alternative, spec reintroduces
|
|
23
|
+
* removed element). Returns { blocked: boolean, issues: [...] }.
|
|
24
|
+
*
|
|
25
|
+
* Fail-open throughout. Missing dossiers, unreadable files, grep failures —
|
|
26
|
+
* all fail-open. The gate is an aid, not a hard stopgap; the core invariant
|
|
27
|
+
* is "don't break existing workflow if dossiers aren't set up yet."
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const { PATHS, safeJsonParse, getConfig } = require('../../flow-utils');
|
|
33
|
+
|
|
34
|
+
function isEnabled() {
|
|
35
|
+
try {
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
return config.featureDossier?.enabled !== false;
|
|
38
|
+
} catch (_err) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getCurrentTaskInfo() {
|
|
44
|
+
try {
|
|
45
|
+
const ready = safeJsonParse(PATHS.ready, null);
|
|
46
|
+
if (!ready || !Array.isArray(ready.inProgress) || ready.inProgress.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const task = ready.inProgress[0];
|
|
50
|
+
const files = [];
|
|
51
|
+
if (task.specPath && fs.existsSync(path.join(PATHS.root, task.specPath))) {
|
|
52
|
+
files.push(task.specPath);
|
|
53
|
+
}
|
|
54
|
+
const changeFiles = listRecentlyChangedFiles();
|
|
55
|
+
return {
|
|
56
|
+
id: task.id,
|
|
57
|
+
title: task.title || '',
|
|
58
|
+
description: task.notes || task.description || '',
|
|
59
|
+
criteria: task.criteria || [],
|
|
60
|
+
files: [...new Set([...files, ...changeFiles])]
|
|
61
|
+
};
|
|
62
|
+
} catch (_err) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listRecentlyChangedFiles() {
|
|
68
|
+
try {
|
|
69
|
+
const { execSync } = require('node:child_process');
|
|
70
|
+
const out = execSync('git diff --name-only HEAD 2>/dev/null; git status --porcelain 2>/dev/null | awk \'{print $2}\' ', {
|
|
71
|
+
cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
|
|
72
|
+
});
|
|
73
|
+
return out.split('\n').map(s => s.trim()).filter(Boolean).slice(0, 100);
|
|
74
|
+
} catch (_err) { return []; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the phase-injection block for the current task.
|
|
79
|
+
* Returns null if no matches, dossier system is disabled, or on any error.
|
|
80
|
+
*/
|
|
81
|
+
function getDossierInjection() {
|
|
82
|
+
if (!isEnabled()) return null;
|
|
83
|
+
let dossier, logicRules;
|
|
84
|
+
try {
|
|
85
|
+
dossier = require('../../flow-feature-dossier');
|
|
86
|
+
logicRules = require('../../flow-logic-rules');
|
|
87
|
+
} catch (_err) { return null; }
|
|
88
|
+
|
|
89
|
+
const taskInfo = getCurrentTaskInfo();
|
|
90
|
+
if (!taskInfo) return null;
|
|
91
|
+
|
|
92
|
+
const criteriaText = (taskInfo.criteria || []).map(c =>
|
|
93
|
+
typeof c === 'string' ? c : (c && c.text) || ''
|
|
94
|
+
).join('\n');
|
|
95
|
+
const matchInput = {
|
|
96
|
+
title: taskInfo.title,
|
|
97
|
+
description: `${taskInfo.description}\n${criteriaText}`,
|
|
98
|
+
files: taskInfo.files
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let featureBlock = null, rulesBlock = null;
|
|
102
|
+
try {
|
|
103
|
+
const featureMatches = dossier.matchFeatures(matchInput);
|
|
104
|
+
const config = getConfig();
|
|
105
|
+
const minScore = config.featureDossier?.autoMatchConfidence ?? 1;
|
|
106
|
+
featureBlock = dossier.buildPhaseInjection(featureMatches, { minScore, maxDossiers: 3 });
|
|
107
|
+
} catch (_err) { /* non-blocking */ }
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const ruleMatches = logicRules.matchRulesForFiles(
|
|
111
|
+
taskInfo.files,
|
|
112
|
+
[taskInfo.title, taskInfo.description].filter(Boolean)
|
|
113
|
+
);
|
|
114
|
+
rulesBlock = logicRules.buildRulesInjection(ruleMatches);
|
|
115
|
+
} catch (_err) { /* non-blocking */ }
|
|
116
|
+
|
|
117
|
+
const parts = [featureBlock, rulesBlock].filter(Boolean);
|
|
118
|
+
if (parts.length === 0) return null;
|
|
119
|
+
return parts.join('\n\n---\n\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate a spec file against all matching dossiers.
|
|
124
|
+
* Returns { blocked, issues } — blocked=true if any blocker-severity issue.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} specContent
|
|
127
|
+
* @param {Object} [taskInfo] — optional pre-computed task info; otherwise detected
|
|
128
|
+
*/
|
|
129
|
+
function validateSpecContradictions(specContent, taskInfo) {
|
|
130
|
+
if (!isEnabled()) return { blocked: false, issues: [] };
|
|
131
|
+
let dossier;
|
|
132
|
+
try { dossier = require('../../flow-feature-dossier'); }
|
|
133
|
+
catch (_err) { return { blocked: false, issues: [] }; }
|
|
134
|
+
|
|
135
|
+
const info = taskInfo || getCurrentTaskInfo();
|
|
136
|
+
if (!info) return { blocked: false, issues: [] };
|
|
137
|
+
|
|
138
|
+
const matches = dossier.matchFeatures({
|
|
139
|
+
title: info.title,
|
|
140
|
+
description: info.description,
|
|
141
|
+
files: info.files
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const allIssues = [];
|
|
145
|
+
for (const m of matches) {
|
|
146
|
+
const d = dossier.loadDossier(m.slug);
|
|
147
|
+
if (!d) continue;
|
|
148
|
+
const issues = dossier.validateSpecAgainstDossier(specContent, d);
|
|
149
|
+
for (const issue of issues) {
|
|
150
|
+
allIssues.push({ ...issue, dossier: m.slug });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let blockOnContradiction = true;
|
|
155
|
+
try {
|
|
156
|
+
const config = getConfig();
|
|
157
|
+
blockOnContradiction = config.featureDossier?.blockOnContradiction !== false;
|
|
158
|
+
} catch (_err) { /* default true */ }
|
|
159
|
+
|
|
160
|
+
const blockers = allIssues.filter(i => i.severity === 'blocker');
|
|
161
|
+
return {
|
|
162
|
+
blocked: blockOnContradiction && blockers.length > 0,
|
|
163
|
+
issues: allIssues
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format contradictions as a block message the caller can surface to the user.
|
|
169
|
+
*/
|
|
170
|
+
function formatContradictionMessage(issues) {
|
|
171
|
+
if (!issues || issues.length === 0) return '';
|
|
172
|
+
const lines = ['## Feature Dossier Contradiction Gate', ''];
|
|
173
|
+
lines.push(`${issues.length} contradiction(s) found between the proposed spec and one or more active feature dossiers.`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('Dossiers capture owner-rejected alternatives and removed elements. A spec that reintroduces any of these would re-introduce a bug the owner already corrected.');
|
|
176
|
+
lines.push('');
|
|
177
|
+
for (const issue of issues) {
|
|
178
|
+
lines.push(`- **[${issue.severity}]** (${issue.dossier} / ${issue.kind}) ${issue.detail}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('**Resolution**:');
|
|
182
|
+
lines.push('1. Read the referenced dossier section in full.');
|
|
183
|
+
lines.push('2. If the owner has actually changed their mind, update the dossier (move the item out of Rejected / Removed) before proceeding.');
|
|
184
|
+
lines.push('3. Otherwise, revise the spec to not reintroduce the rejected/removed item.');
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
isEnabled,
|
|
190
|
+
getCurrentTaskInfo,
|
|
191
|
+
getDossierInjection,
|
|
192
|
+
validateSpecContradictions,
|
|
193
|
+
formatContradictionMessage
|
|
194
|
+
};
|
|
@@ -376,6 +376,27 @@ async function captureObservation(options) {
|
|
|
376
376
|
// Exports
|
|
377
377
|
// ============================================================
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Pick the authoritative tool-execution duration.
|
|
381
|
+
*
|
|
382
|
+
* Claude Code 2.1.119+ attaches a numeric `duration_ms` to PostToolUse /
|
|
383
|
+
* PostToolUseFailure payloads — real tool execution time, excluding
|
|
384
|
+
* permission prompts and PreToolUse hooks. Older CC versions omit it, so
|
|
385
|
+
* callers pass a locally-computed fallback (typically near-zero because
|
|
386
|
+
* the hook only measures the gap between adjacent statements, not the
|
|
387
|
+
* tool itself). Prefer native when numeric; fall back otherwise.
|
|
388
|
+
*
|
|
389
|
+
* @param {object} parsedInput - hook payload
|
|
390
|
+
* @param {number} fallbackMs - local duration to use when native is absent
|
|
391
|
+
* @returns {number}
|
|
392
|
+
*/
|
|
393
|
+
function selectDuration(parsedInput, fallbackMs) {
|
|
394
|
+
if (parsedInput && typeof parsedInput.duration_ms === 'number') {
|
|
395
|
+
return parsedInput.duration_ms;
|
|
396
|
+
}
|
|
397
|
+
return fallbackMs;
|
|
398
|
+
}
|
|
399
|
+
|
|
379
400
|
module.exports = {
|
|
380
401
|
// Configuration
|
|
381
402
|
isObservationCaptureEnabled,
|
|
@@ -387,6 +408,9 @@ module.exports = {
|
|
|
387
408
|
summarizeInput,
|
|
388
409
|
summarizeOutput,
|
|
389
410
|
|
|
411
|
+
// Duration source selection (CC 2.1.119+ native, fallback otherwise)
|
|
412
|
+
selectDuration,
|
|
413
|
+
|
|
390
414
|
// Main capture function
|
|
391
415
|
captureObservation
|
|
392
416
|
};
|
|
@@ -259,10 +259,29 @@ function buildOverdueContext(opts = {}) {
|
|
|
259
259
|
return lostBlock;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
// Worker completion summaries (Story B / wf-ab59f0e4): surface unseen
|
|
263
|
+
// summaries from any worker that finished an autonomous epic. Render via
|
|
264
|
+
// flow-workspace-summary.renderMultiWorker for the multi-worker block.
|
|
265
|
+
let summariesBlock = null;
|
|
266
|
+
let seenTaskIds = [];
|
|
267
|
+
try {
|
|
268
|
+
const libPath = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-dispatch-tracking.js');
|
|
269
|
+
const { readPendingCompletionSummaries, markCompletionSummariesSeen } = require(libPath);
|
|
270
|
+
const pending = readPendingCompletionSummaries(workspaceRoot);
|
|
271
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
272
|
+
const wsSummary = require(path.resolve(__dirname, '..', '..', 'flow-workspace-summary.js'));
|
|
273
|
+
const payloads = pending.map(p => p.summary);
|
|
274
|
+
summariesBlock = wsSummary.renderMultiWorker(payloads);
|
|
275
|
+
seenTaskIds = pending.map(p => p.taskId);
|
|
276
|
+
markCompletionSummariesSeen(workspaceRoot, seenTaskIds);
|
|
277
|
+
}
|
|
278
|
+
} catch (_err) { /* fail-open — surface what we can */ }
|
|
279
|
+
|
|
280
|
+
if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock && !summariesBlock) return null;
|
|
263
281
|
|
|
264
282
|
const sections = [];
|
|
265
283
|
|
|
284
|
+
if (summariesBlock) sections.push(summariesBlock);
|
|
266
285
|
if (lostBlock) sections.push(lostBlock);
|
|
267
286
|
|
|
268
287
|
if (Array.isArray(overdue) && overdue.length > 0) {
|
|
@@ -186,12 +186,26 @@ function transitionPhase(from, to, taskId) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
const wrote = writePhaseState({
|
|
190
190
|
phase: to,
|
|
191
191
|
taskId: taskId || current.taskId,
|
|
192
192
|
updatedAt: new Date().toISOString(),
|
|
193
193
|
previousPhase: from
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
// Clear any stale routing-pending flag left over from a prior turn when
|
|
197
|
+
// crossing a phase boundary. Uses removeRoutingFlag() (not clearRoutingPending)
|
|
198
|
+
// so the cleared-marker is preserved — we don't want to open a 15s bypass
|
|
199
|
+
// window just because a phase transitioned. Fail-open if the module is
|
|
200
|
+
// unavailable (e.g. future CLI adapter without routing-gate wired).
|
|
201
|
+
if (wrote) {
|
|
202
|
+
try {
|
|
203
|
+
const { removeRoutingFlag } = require('./routing-gate');
|
|
204
|
+
removeRoutingFlag();
|
|
205
|
+
} catch (_err) { /* fail-open */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return wrote;
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase-transition Auto-Review trigger (wf-8d635d0e / E1).
|
|
5
|
+
*
|
|
6
|
+
* Called after a successful phase transition. When the transition is
|
|
7
|
+
* `coding → validating` and `config.autoReview.enabled`, fire a detached
|
|
8
|
+
* background review worker. Non-blocking by design (AC5) — parent returns
|
|
9
|
+
* immediately; the worker writes findings asynchronously.
|
|
10
|
+
*
|
|
11
|
+
* Failure-mode: any error from the spawn path is swallowed and logged via
|
|
12
|
+
* DEBUG only. Auto-review is an advisory signal layered on top of the
|
|
13
|
+
* existing Completion Truth Gate — a crash here must never fail the
|
|
14
|
+
* primary phase transition.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { getConfig } = require('../../flow-utils');
|
|
18
|
+
|
|
19
|
+
function isAutoReviewEnabled(cfg) {
|
|
20
|
+
try {
|
|
21
|
+
const c = cfg || getConfig();
|
|
22
|
+
return c?.autoReview?.enabled === true;
|
|
23
|
+
} catch (_err) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Trigger a background auto-review when appropriate.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} from - prior phase
|
|
32
|
+
* @param {string} to - new phase
|
|
33
|
+
* @param {string} taskId
|
|
34
|
+
* @param {Object} [opts]
|
|
35
|
+
* @param {Function} [opts.starter] — injectable startReview (tests)
|
|
36
|
+
* @returns {{ started:boolean, reason?:string, handle?:Object }}
|
|
37
|
+
*/
|
|
38
|
+
function maybeStartAutoReview(from, to, taskId, opts = {}) {
|
|
39
|
+
if (from !== 'coding' || to !== 'validating') {
|
|
40
|
+
return { started: false, reason: 'not-validating-transition' };
|
|
41
|
+
}
|
|
42
|
+
if (!taskId) {
|
|
43
|
+
return { started: false, reason: 'no-task-id' };
|
|
44
|
+
}
|
|
45
|
+
if (!isAutoReviewEnabled(opts.config)) {
|
|
46
|
+
return { started: false, reason: 'disabled' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const starter = opts.starter || require('../../../lib/worktree-review').startReview;
|
|
50
|
+
try {
|
|
51
|
+
const handle = starter({ taskId, repoRoot: opts.repoRoot });
|
|
52
|
+
return { started: true, handle };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (process.env.DEBUG) {
|
|
55
|
+
console.error(`[auto-review] startReview failed: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
return { started: false, reason: 'spawn-error', error: String(err.message || err) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { maybeStartAutoReview, isAutoReviewEnabled };
|
|
@@ -184,10 +184,13 @@ function handlePostCompact() {
|
|
|
184
184
|
|
|
185
185
|
// 3. Re-set routing-pending flag
|
|
186
186
|
// After compaction, the AI has fresh context and may try to act without routing.
|
|
187
|
-
//
|
|
187
|
+
// resetRoutingState() first clears any stale cleared-marker from the pre-compact
|
|
188
|
+
// turn — otherwise setRoutingPending() would short-circuit on the 15s skill-chain
|
|
189
|
+
// suppression window and leave the flag unset, creating a bypass.
|
|
188
190
|
let routingReArmed = false;
|
|
189
191
|
try {
|
|
190
|
-
const { setRoutingPending } = require('./routing-gate');
|
|
192
|
+
const { setRoutingPending, resetRoutingState } = require('./routing-gate');
|
|
193
|
+
resetRoutingState();
|
|
191
194
|
setRoutingPending();
|
|
192
195
|
routingReArmed = true;
|
|
193
196
|
} catch (err) {
|
|
@@ -261,6 +261,27 @@ function runPreToolGates(ctx, deps) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Path-discipline gate (Story B / wf-ab59f0e4): runs in BOTH manager and
|
|
265
|
+
// worker mode (different rules each side). Cross-process state writes are
|
|
266
|
+
// blocked fail-loud so file corruption is impossible to ignore.
|
|
267
|
+
if (process.env.WOGI_WORKSPACE_ROOT) {
|
|
268
|
+
try {
|
|
269
|
+
if (typeof deps.checkPathDiscipline === 'function') {
|
|
270
|
+
const pathResult = deps.checkPathDiscipline(toolName, toolInput);
|
|
271
|
+
if (pathResult.blocked) {
|
|
272
|
+
return {
|
|
273
|
+
allowed: false,
|
|
274
|
+
blocked: true,
|
|
275
|
+
reason: pathResult.reason,
|
|
276
|
+
message: pathResult.message,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (process.env.DEBUG) console.error(`[Hook] Path discipline gate error (fail-open): ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
264
285
|
// Commit log gate
|
|
265
286
|
if (toolName === 'Bash' && toolInput.command) {
|
|
266
287
|
try {
|