wogiflow 2.26.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -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, coreResult) {
41
+ transformResult(_event, _coreResult) {
42
42
  throw new Error('transformResult() must be implemented by subclass');
43
43
  }
44
44
 
@@ -58,7 +58,7 @@ class BaseAdapter {
58
58
  * @param {Object} [transportConfig] - Transport config: { transport, url, headers, allowedEnvVars }
59
59
  * @returns {Object} CLI-specific hook configuration
60
60
  */
61
- generateConfig(_rules, projectRoot, transportConfig) {
61
+ generateConfig(_rules, _projectRoot, _transportConfig) {
62
62
  throw new Error('generateConfig() must be implemented by subclass');
63
63
  }
64
64
 
@@ -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
- if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock) return null;
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
- return writePhaseState({
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
- // Setting routing-pending ensures the next tool use goes through routing checks.
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 {