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,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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (workerResult.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
}
|
|
323
|
-
|
|
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/`.
|