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,275 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Worker Tool-First Turn Gate — G1 + G4 + G6 (epic wf-34290000, Workstream G)
5
+ *
6
+ * In workspace worker mode, every turn that follows a UserPromptSubmit
7
+ * (channel dispatch from the manager) MUST contain at least one tool call,
8
+ * and — in strict mode — the first assistant content block MUST be a tool
9
+ * call, not text.
10
+ *
11
+ * Why this exists
12
+ * ----------------
13
+ * Workers communicate with the manager via tool calls (channel dispatches,
14
+ * file edits, test runs) and structured `## Results` payloads. A pure-text
15
+ * response from a worker is invisible to the user (who only sees the manager
16
+ * terminal) and disqualifies the worker from the three-state end-of-turn
17
+ * contract (ACTION | ESCALATION | IDLE — see CLAUDE.md rule "Workspace
18
+ * Autonomous-Mode Action-After-Completion Contract").
19
+ *
20
+ * Three violations this gate detects
21
+ * ----------------------------------
22
+ * G1 — silent halt: zero tool_use blocks in the turn
23
+ * G4 — text-before-tool-call: first content block is text, not tool_use
24
+ * (strict mode only)
25
+ * G6 — documented contract: the named "worker-tool-first-turn" rule
26
+ * referenced in block messages, so the worker
27
+ * sees one coherent contract, not three gates.
28
+ *
29
+ * Fail-open throughout: missing transcript, parse errors, config errors all
30
+ * return `{ blocked: false }`. Silent-halt false-negatives are recoverable;
31
+ * blocking legitimate stops on a gate bug is not.
32
+ *
33
+ * Scope: worker mode only. Main-mode (non-workspace) turns are unaffected —
34
+ * the caller must gate on `isWorkerMode()` before invoking this check.
35
+ */
36
+
37
+ const fs = require('node:fs');
38
+
39
+ const MAX_TRANSCRIPT_BYTES = 4 * 1024 * 1024; // 4MB cap — large transcripts read last-N lines only
40
+
41
+ /**
42
+ * Inspect the current turn in a worker transcript and determine whether it
43
+ * violates the tool-first contract.
44
+ *
45
+ * @param {Object} opts
46
+ * @param {string} opts.transcriptPath - absolute path to Claude Code JSONL transcript
47
+ * @param {boolean} [opts.strict=true] - when true, also flag text-before-tool-call
48
+ * @returns {{ blocked: boolean, reason?: string, violation?: 'silent-halt'|'text-before-tool-call', ruleId?: string }}
49
+ */
50
+ function checkWorkerToolFirstTurn(opts) {
51
+ const { transcriptPath, strict = true } = opts || {};
52
+ if (!transcriptPath || typeof transcriptPath !== 'string') {
53
+ return { blocked: false, reason: 'no-transcript-path' };
54
+ }
55
+
56
+ const events = readTranscript(transcriptPath);
57
+ if (!events) {
58
+ return { blocked: false, reason: 'transcript-unreadable' };
59
+ }
60
+
61
+ const turn = extractCurrentTurn(events);
62
+ if (!turn) {
63
+ return { blocked: false, reason: 'no-current-turn' };
64
+ }
65
+
66
+ // G1 — zero tool_use blocks across the entire turn.
67
+ if (turn.toolUseCount === 0) {
68
+ return {
69
+ blocked: true,
70
+ violation: 'silent-halt',
71
+ ruleId: 'worker-tool-first-turn',
72
+ reason: 'worker turn after UserPromptSubmit had zero tool calls (silent-halt / text-only response)'
73
+ };
74
+ }
75
+
76
+ // G4 — first content block is text, not tool_use (strict mode only).
77
+ if (strict && turn.firstBlockType === 'text') {
78
+ return {
79
+ blocked: true,
80
+ violation: 'text-before-tool-call',
81
+ ruleId: 'worker-tool-first-turn',
82
+ reason: 'worker turn began with a text block before any tool call (text-before-tool-call)'
83
+ };
84
+ }
85
+
86
+ return { blocked: false };
87
+ }
88
+
89
+ /**
90
+ * Read a JSONL transcript and return parsed event objects. Large transcripts
91
+ * are truncated to the last MAX_TRANSCRIPT_BYTES by reading the file size
92
+ * and, if oversized, reading only the tail. This bounds per-Stop overhead.
93
+ *
94
+ * Returns null on any IO failure (fail-open signal to the caller).
95
+ */
96
+ function readTranscript(p) {
97
+ let raw;
98
+ try {
99
+ const stat = fs.statSync(p);
100
+ if (stat.size > MAX_TRANSCRIPT_BYTES) {
101
+ // Read the last MAX_TRANSCRIPT_BYTES — the first partial line will be
102
+ // dropped by JSON.parse failure (we skip unparseable lines).
103
+ const fd = fs.openSync(p, 'r');
104
+ try {
105
+ const buf = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
106
+ fs.readSync(fd, buf, 0, MAX_TRANSCRIPT_BYTES, stat.size - MAX_TRANSCRIPT_BYTES);
107
+ raw = buf.toString('utf-8');
108
+ } finally {
109
+ fs.closeSync(fd);
110
+ }
111
+ } else {
112
+ raw = fs.readFileSync(p, 'utf-8');
113
+ }
114
+ } catch (_err) {
115
+ return null;
116
+ }
117
+ if (!raw) return [];
118
+
119
+ const events = [];
120
+ const lines = raw.split('\n');
121
+ for (const line of lines) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed) continue;
124
+ try {
125
+ events.push(JSON.parse(trimmed));
126
+ } catch (_err) {
127
+ // Unparseable line (likely the truncated first line when we tailed the
128
+ // file, or a mid-write line) — skip.
129
+ }
130
+ }
131
+ return events;
132
+ }
133
+
134
+ /**
135
+ * From a list of transcript events, isolate the current turn: everything
136
+ * AFTER the most recent user message. Returns turn-level summary:
137
+ *
138
+ * {
139
+ * toolUseCount: number, // total tool_use blocks in the turn
140
+ * firstBlockType: 'text'|'tool_use'|null, // first assistant block type
141
+ * assistantEventCount: number
142
+ * }
143
+ *
144
+ * Returns null if no user message is found (pre-dispatch — not a worker turn
145
+ * we should gate).
146
+ */
147
+ function extractCurrentTurn(events) {
148
+ if (!Array.isArray(events) || events.length === 0) return null;
149
+
150
+ // Find the last user message index. Scan from end.
151
+ let lastUserIdx = -1;
152
+ for (let i = events.length - 1; i >= 0; i--) {
153
+ if (isUserEvent(events[i])) {
154
+ lastUserIdx = i;
155
+ break;
156
+ }
157
+ }
158
+ if (lastUserIdx === -1) return null;
159
+
160
+ // Collect assistant blocks after the last user message, in order.
161
+ let toolUseCount = 0;
162
+ let firstBlockType = null;
163
+ let assistantEventCount = 0;
164
+
165
+ for (let i = lastUserIdx + 1; i < events.length; i++) {
166
+ const entry = events[i];
167
+ if (!isAssistantEvent(entry)) continue;
168
+ assistantEventCount++;
169
+ const blocks = extractContentBlocks(entry);
170
+ for (const block of blocks) {
171
+ if (!block || typeof block !== 'object') continue;
172
+ const t = block.type;
173
+ if (!firstBlockType && (t === 'text' || t === 'tool_use')) {
174
+ firstBlockType = t;
175
+ }
176
+ if (t === 'tool_use') {
177
+ toolUseCount++;
178
+ }
179
+ }
180
+ }
181
+
182
+ return { toolUseCount, firstBlockType, assistantEventCount };
183
+ }
184
+
185
+ function isUserEvent(entry) {
186
+ if (!entry || typeof entry !== 'object') return false;
187
+ return (
188
+ entry.role === 'user' ||
189
+ entry.type === 'user' ||
190
+ (entry.message && entry.message.role === 'user')
191
+ );
192
+ }
193
+
194
+ function isAssistantEvent(entry) {
195
+ if (!entry || typeof entry !== 'object') return false;
196
+ return (
197
+ entry.role === 'assistant' ||
198
+ entry.type === 'assistant' ||
199
+ (entry.message && entry.message.role === 'assistant')
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Extract the content-blocks array from a transcript event. Claude Code's
205
+ * transcript format has evolved, so accept any of:
206
+ * { content: [ {...} ] }
207
+ * { message: { content: [ {...} ] } }
208
+ * { content: 'string' } → [{ type: 'text', text: 'string' }]
209
+ * { message: { content: '...' } } same
210
+ */
211
+ function extractContentBlocks(entry) {
212
+ const content = entry.content ?? entry.message?.content;
213
+ if (typeof content === 'string') {
214
+ return [{ type: 'text', text: content }];
215
+ }
216
+ if (Array.isArray(content)) {
217
+ return content;
218
+ }
219
+ return [];
220
+ }
221
+
222
+ /**
223
+ * Render the standard block message the Stop hook returns when a violation
224
+ * is detected. Centralised so G6 (contract name) stays consistent across
225
+ * violations.
226
+ */
227
+ function renderBlockMessage({ violation, reason }) {
228
+ const port = process.env.WOGI_MANAGER_PORT || '8800';
229
+ const repo = process.env.WOGI_REPO_NAME || '<worker>';
230
+ const head = violation === 'text-before-tool-call'
231
+ ? 'WORKER CONTRACT VIOLATION: text-before-tool-call'
232
+ : 'WORKER CONTRACT VIOLATION: silent-halt (zero tool calls)';
233
+ return [
234
+ head,
235
+ '',
236
+ `Rule: worker-tool-first-turn`,
237
+ `Why: ${reason}`,
238
+ '',
239
+ 'The worker start-of-turn contract requires every turn after a UserPromptSubmit to',
240
+ 'have at least one tool call, with the first content block being a tool call in',
241
+ 'strict mode. Pure-text responses are invisible to the user and disqualify the',
242
+ 'three-state end-of-turn contract (ACTION | ESCALATION | IDLE).',
243
+ '',
244
+ 'Do ONE of these NOW:',
245
+ ' (a) ACTION — invoke the tool you intended to use',
246
+ ' (b) ESCALATE — channel-dispatch a "## QUESTION:" to the manager:',
247
+ ` curl -s -X POST http://127.0.0.1:${port} \\`,
248
+ ` -H "X-Wogi-From: ${repo}" \\`,
249
+ ` --data-binary "## QUESTION: <your question>"`,
250
+ ' (c) REPLY with ## Results — channel-dispatch the structured task reply to manager',
251
+ '',
252
+ 'Then end the turn.'
253
+ ].join('\n');
254
+ }
255
+
256
+ /**
257
+ * Convenience: determine worker mode from env. Exported so callers don\'t
258
+ * have to duplicate the env-check pattern.
259
+ */
260
+ function isWorkerMode() {
261
+ return Boolean(
262
+ process.env.WOGI_WORKSPACE_ROOT &&
263
+ process.env.WOGI_REPO_NAME &&
264
+ process.env.WOGI_REPO_NAME !== 'manager'
265
+ );
266
+ }
267
+
268
+ module.exports = {
269
+ checkWorkerToolFirstTurn,
270
+ renderBlockMessage,
271
+ isWorkerMode,
272
+ // Exported for tests
273
+ extractCurrentTurn,
274
+ readTranscript
275
+ };
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  const { runValidation } = require('../../core/validation');
14
- const { captureObservation } = require('../../core/observation-capture');
14
+ const { captureObservation, selectDuration } = require('../../core/observation-capture');
15
15
  const { runHook } = require('../shared/hook-runner');
16
16
 
17
17
  function extractErrorMessage(toolResponse) {
@@ -47,7 +47,7 @@ runHook('PostToolUse', async ({ parsedInput }) => {
47
47
  toolName,
48
48
  toolInput,
49
49
  toolResponse,
50
- duration: Date.now() - startTime,
50
+ duration: selectDuration(parsedInput, Date.now() - startTime),
51
51
  explorationStatus: toolFailed ? 'rejected' : undefined,
52
52
  rejectionReason: toolFailed ? extractErrorMessage(toolResponse) : undefined
53
53
  });
@@ -63,7 +63,12 @@ try { checkGitSafety = require('../../core/git-safety-gate').checkGitSafety; } c
63
63
  let checkManagerBoundary = _noop;
64
64
  try { checkManagerBoundary = require('../../core/manager-boundary-gate').checkManagerBoundary; } catch (_err) { if (process.env.DEBUG) console.error(`[Hook] Manager boundary gate not loaded: ${_err.message}`); }
65
65
  let checkWorkerBoundary = _noop;
66
- try { checkWorkerBoundary = require('../../core/worker-boundary-gate').checkWorkerBoundary; } catch (_err) { if (process.env.DEBUG) console.error(`[Hook] Worker boundary gate not loaded: ${_err.message}`); }
66
+ let checkPathDiscipline = _noop;
67
+ try {
68
+ const wbg = require('../../core/worker-boundary-gate');
69
+ checkWorkerBoundary = wbg.checkWorkerBoundary;
70
+ checkPathDiscipline = wbg.checkPathDiscipline;
71
+ } catch (_err) { if (process.env.DEBUG) console.error(`[Hook] Worker boundary gate not loaded: ${_err.message}`); }
67
72
 
68
73
  const { claudeCodeAdapter } = require('../../adapters/claude-code');
69
74
  const { markSkillPending } = require('../../../flow-durable-session');
@@ -101,7 +106,7 @@ runHook('PreToolUse', async ({ input, parsedInput }) => {
101
106
  recordEvidenceRead, checkSpecWriteGate, clearResearchEvidence,
102
107
  checkDeployGate, checkWriteBlock,
103
108
  checkStrikeGate, checkBugfixScope, checkScopeMutation,
104
- checkGitSafety, checkManagerBoundary, checkWorkerBoundary,
109
+ checkGitSafety, checkManagerBoundary, checkWorkerBoundary, checkPathDiscipline,
105
110
  // Side-effect helpers
106
111
  markSkillPending,
107
112
  // Config + runtime
@@ -14,6 +14,39 @@ const { setRoutingPending } = require('../../core/routing-gate');
14
14
  const { getConfig } = require('../../../flow-utils');
15
15
  const { runHook } = require('../shared/hook-runner');
16
16
 
17
+ // wf-8294d960: env-guarded boot-latency instrumentation. No effect unless WOGI_DEBUG_BOOT=1.
18
+ // Claude Code suppresses hook process stderr — we write to an append-only log file instead.
19
+ const BOOT_DEBUG = process.env.WOGI_DEBUG_BOOT === '1';
20
+ const _bootT0 = BOOT_DEBUG ? Date.now() : 0;
21
+ const _bootLogFile = BOOT_DEBUG
22
+ ? require('path').join(require('os').tmpdir(), 'wogi-boot-latency.log')
23
+ : null;
24
+ let _bootSep = false;
25
+ function _bootWrite(line) {
26
+ if (!BOOT_DEBUG) return;
27
+ try {
28
+ if (!_bootSep) {
29
+ require('fs').appendFileSync(_bootLogFile, `\n=== SessionStart pid=${process.pid} @ ${new Date().toISOString()} ===\n`);
30
+ _bootSep = true;
31
+ }
32
+ require('fs').appendFileSync(_bootLogFile, line + '\n');
33
+ } catch (_err) { /* non-blocking */ }
34
+ }
35
+ function _bootMark(label) {
36
+ if (!BOOT_DEBUG) return;
37
+ const ms = Date.now() - _bootT0;
38
+ _bootWrite(`[boot-latency] +${String(ms).padStart(6)}ms ${label}`);
39
+ }
40
+ async function _bootTime(label, fn) {
41
+ if (!BOOT_DEBUG) return fn();
42
+ const t = Date.now();
43
+ try {
44
+ return await fn();
45
+ } finally {
46
+ _bootWrite(`[boot-latency] (${String(Date.now() - t).padStart(6)}ms) ${label}`);
47
+ }
48
+ }
49
+
17
50
  // Lazy-load bridge state to avoid circular dependencies
18
51
  let autoSyncBridge = null;
19
52
  function getAutoSyncBridge() {
@@ -28,8 +61,9 @@ function getAutoSyncBridge() {
28
61
  }
29
62
 
30
63
  runHook('SessionStart', async ({ parsedInput }) => {
64
+ _bootMark('SessionStart hook entered');
31
65
  // Start bridge auto-sync in parallel with other init work
32
- const bridgeSyncPromise = (async () => {
66
+ const bridgeSyncPromise = _bootTime('bridge auto-sync', async () => {
33
67
  try {
34
68
  const syncFn = getAutoSyncBridge();
35
69
  await syncFn('claude-code', { silent: true });
@@ -38,10 +72,11 @@ runHook('SessionStart', async ({ parsedInput }) => {
38
72
  console.error(`[session-start] Bridge auto-sync failed: ${err.message}`);
39
73
  }
40
74
  }
41
- })();
75
+ });
42
76
 
43
77
  // Wait for bridge sync to complete
44
78
  await bridgeSyncPromise;
79
+ _bootMark('after bridge sync');
45
80
 
46
81
  // CLAUDE.md drift detection — check if manually edited since last sync
47
82
  let driftDetected = false;
@@ -70,19 +105,22 @@ runHook('SessionStart', async ({ parsedInput }) => {
70
105
  // --- Version compatibility checks (parallelized) ---
71
106
  let versionWarning = null;
72
107
  let updateWarning = null;
73
- try {
74
- const { checkClaudeCodeVersionOnce, checkWogiFlowUpdateOnce } = require('../../../flow-version-check');
75
- const [vw, uw] = await Promise.all([
76
- (async () => { try { return await checkClaudeCodeVersionOnce(); } catch (_err) { return null; } })(),
77
- (async () => { try { return await checkWogiFlowUpdateOnce(); } catch (_err) { return null; } })()
78
- ]);
79
- versionWarning = vw;
80
- updateWarning = uw;
81
- } catch (err) {
82
- if (process.env.DEBUG) {
83
- console.error(`[session-start] Version check failed: ${err.message}`);
108
+ await _bootTime('version checks', async () => {
109
+ try {
110
+ const { checkClaudeCodeVersionOnce, checkWogiFlowUpdateOnce } = require('../../../flow-version-check');
111
+ const [vw, uw] = await Promise.all([
112
+ (async () => { try { return await checkClaudeCodeVersionOnce(); } catch (_err) { return null; } })(),
113
+ (async () => { try { return await checkWogiFlowUpdateOnce(); } catch (_err) { return null; } })()
114
+ ]);
115
+ versionWarning = vw;
116
+ updateWarning = uw;
117
+ } catch (err) {
118
+ if (process.env.DEBUG) {
119
+ console.error(`[session-start] Version check failed: ${err.message}`);
120
+ }
84
121
  }
85
- }
122
+ });
123
+ _bootMark('after version checks');
86
124
 
87
125
  // --- Batch 1: Independent pre-context operations (async + sync) ---
88
126
  let scriptWarnings = [];
@@ -173,14 +211,16 @@ runHook('SessionStart', async ({ parsedInput }) => {
173
211
  );
174
212
 
175
213
  // Gather session context concurrently with the async pre-ops
214
+ _bootMark('before gatherSessionContext + asyncPreOps');
176
215
  const [, coreResult] = await Promise.all([
177
216
  Promise.all(asyncPreOps),
178
- gatherSessionContext({
217
+ _bootTime('gatherSessionContext', () => gatherSessionContext({
179
218
  includeSuspended: true,
180
219
  includeDecisions: true,
181
220
  includeActivity: true
182
- })
221
+ }))
183
222
  ]);
223
+ _bootMark('after gatherSessionContext + asyncPreOps');
184
224
 
185
225
  // --- Batch 2: Post-context operations (plugin scan + community pull) ---
186
226
  const postContextOps = [];
@@ -260,7 +300,8 @@ runHook('SessionStart', async ({ parsedInput }) => {
260
300
  }
261
301
  })());
262
302
 
263
- await Promise.all(postContextOps);
303
+ await _bootTime('postContextOps (plugin-scan + community-pull)', () => Promise.all(postContextOps));
304
+ _bootMark('after postContextOps');
264
305
 
265
306
  // Inject script warnings into context (if any)
266
307
  if (scriptWarnings.length > 0 && coreResult && coreResult.context) {
@@ -309,21 +350,24 @@ runHook('SessionStart', async ({ parsedInput }) => {
309
350
  // if the queue is truly empty, announce worker-ready so the manager can
310
351
  // reconcile against its dispatch log and re-dispatch anything lost during
311
352
  // the restart window. See scripts/hooks/core/session-start-worker.js.
312
- try {
313
- const { handleWorkerSessionStart } = require('../../core/session-start-worker');
314
- const workerResult = handleWorkerSessionStart();
315
- if (workerResult.context && coreResult && coreResult.context) {
316
- if (workerResult.branch === 'auto-resume') {
317
- coreResult.context.workerAutoResume = workerResult.context;
318
- } else if (workerResult.branch === 'announce-ready') {
319
- coreResult.context.workerReadyAnnounce = workerResult.context;
353
+ await _bootTime('worker session-start handler', async () => {
354
+ try {
355
+ const { handleWorkerSessionStart } = require('../../core/session-start-worker');
356
+ const workerResult = handleWorkerSessionStart();
357
+ if (workerResult.context && coreResult && coreResult.context) {
358
+ if (workerResult.branch === 'auto-resume') {
359
+ coreResult.context.workerAutoResume = workerResult.context;
360
+ } else if (workerResult.branch === 'announce-ready') {
361
+ coreResult.context.workerReadyAnnounce = workerResult.context;
362
+ }
363
+ }
364
+ } catch (err) {
365
+ if (process.env.DEBUG) {
366
+ console.error(`[session-start] Worker session-start handler failed: ${err.message}`);
320
367
  }
321
368
  }
322
- } catch (err) {
323
- if (process.env.DEBUG) {
324
- console.error(`[session-start] Worker session-start handler failed: ${err.message}`);
325
- }
326
- }
369
+ });
370
+ _bootMark('SessionStart hook returning');
327
371
 
328
372
  return coreResult;
329
373
  }, { failMode: 'warn' });
@@ -220,7 +220,9 @@ runHook('Stop', async ({ parsedInput }) => {
220
220
  }
221
221
  }
222
222
 
223
- const restartResult = consumeAndTriggerRestart();
223
+ const restartResult = await consumeAndTriggerRestart({
224
+ transcriptPath: parsedInput?.transcriptPath
225
+ });
224
226
  if (restartResult.triggered) {
225
227
  if (process.env.DEBUG) {
226
228
  console.error(`[Stop] Task-boundary restart triggered — claude will exit, wrapper will relaunch`);
@@ -313,6 +315,50 @@ runHook('Stop', async ({ parsedInput }) => {
313
315
  }
314
316
  }
315
317
 
318
+ // Worker Tool-First Turn Gate (G1 + G4 + G6 — epic wf-34290000, Workstream G).
319
+ //
320
+ // In worker mode, every turn after a UserPromptSubmit (channel dispatch)
321
+ // MUST have at least one tool call. Strict mode also requires the first
322
+ // assistant content block to be a tool call, not text. Pure-text worker
323
+ // responses are invisible to the user and violate the three-state
324
+ // end-of-turn contract.
325
+ //
326
+ // Gates in order: G1 (zero tool_use = silent-halt) → G4 (text-first block =
327
+ // text-before-tool-call). Both share the rule name "worker-tool-first-turn"
328
+ // (G6). Fail-open — missing transcript / parse errors / config errors
329
+ // return no-block.
330
+ try {
331
+ const { isWorkerMode, checkWorkerToolFirstTurn, renderBlockMessage } =
332
+ require('../../core/worker-tool-first-gate');
333
+ if (isWorkerMode() && parsedInput?.transcriptPath) {
334
+ const { getConfig } = require('../../../flow-utils');
335
+ const config = getConfig();
336
+ const gateCfg = config.workspace?.toolFirstTurnGate;
337
+ const enabled = gateCfg?.enabled !== false; // default true
338
+ if (enabled) {
339
+ const strict = gateCfg?.strict !== false; // default true
340
+ const result = checkWorkerToolFirstTurn({
341
+ transcriptPath: parsedInput.transcriptPath,
342
+ strict
343
+ });
344
+ if (result.blocked) {
345
+ return {
346
+ __raw: true,
347
+ continue: true,
348
+ stopReason: renderBlockMessage(result)
349
+ };
350
+ }
351
+ }
352
+ }
353
+ } catch (err) {
354
+ // Fail-OPEN — any error in the tool-first gate must not block legitimate
355
+ // stops. Silent-halt / text-first false-negatives are recoverable; a
356
+ // false-positive block on every turn is not.
357
+ if (process.env.DEBUG) {
358
+ console.error(`[Stop] Worker tool-first gate error (fail-open): ${err.message}`);
359
+ }
360
+ }
361
+
316
362
  // G3 (v2.21.0) — AI-based worker-question classifier.
317
363
  //
318
364
  // If the worker ends a turn with a question to the user in free text (no tool
@@ -13,6 +13,7 @@ const { checkResearchRequirement } = require('../../core/research-gate');
13
13
  const { setRoutingPending, clearRoutingPending, ROUTING_CLEARED_PATH } = require('../../core/routing-gate');
14
14
  const { getPhaseContextPrompt } = require('../../core/phase-gate');
15
15
  const { buildOverdueContext } = require('../../core/overdue-dispatches');
16
+ const { getDossierInjection } = require('../../core/feature-dossier-gate');
16
17
  const { markSkillPending, loadDurableSession } = require('../../../flow-durable-session');
17
18
  const { captureCurrentPrompt } = require('../../../flow-prompt-capture');
18
19
  const { spawnBackgroundDetection } = require('../../../flow-correction-detector');
@@ -135,6 +136,22 @@ runHook('UserPromptSubmit', async ({ input, parsedInput }) => {
135
136
  }
136
137
  }
137
138
 
139
+ // wf-557cf08a — Feature dossier + logic rules auto-injection.
140
+ // Surfaces canonical per-feature knowledge and cross-cutting logic rules
141
+ // into the phase prompt so Claude doesn't have to fetch them under token
142
+ // pressure. Fail-open: returns null on any error.
143
+ let dossierPrompt = null;
144
+ try {
145
+ dossierPrompt = getDossierInjection();
146
+ } catch (err) {
147
+ if (process.env.DEBUG) {
148
+ console.error(`[Hook] Dossier injection failed: ${err.message}`);
149
+ }
150
+ }
151
+ if (dossierPrompt) {
152
+ phasePrompt = phasePrompt ? `${phasePrompt}\n\n${dossierPrompt}` : dossierPrompt;
153
+ }
154
+
138
155
  // Check research gate first (before implementation gate)
139
156
  const researchResult = checkResearchRequirement({
140
157
  prompt,
@@ -1,20 +0,0 @@
1
- ## User Commands
2
-
3
- | To Do This | Say This |
4
- |------------|----------|
5
- | Start a task | "start task wf-XXX" or describe what you want |
6
- | Code review | "code review" or "review what we did" |
7
- | Morning briefing | "morning briefing" or "what should I work on" |
8
- | End session | "wrap up" or "end session" |
9
- | Peer review | "peer review" |
10
- | Enable hybrid | "enable hybrid mode" |
11
- | Show tasks | "show tasks" or "what's ready" |
12
- | Project status | "project status" or "where are we" |
13
- | Create a rule | "from now on always..." or "let's make it a rule" |
14
- | Learn from patterns | "let's learn from this" or "promote pattern" |
15
- | Session retro | "retro" or "what went well" |
16
- | Rescan project | "rescan project" or "things changed" or "out of sync" |
17
- | Project audit | "audit project" or "full analysis" |
18
- | Register plugin | "register plugin" or "/wogi-register <name>" |
19
-
20
- `/wogi-start` is the universal fallback router — it classifies any request and routes to the right action. Detailed per-command docs live in each skill's `.md` file under `.claude/commands/`.