wogiflow 2.29.5 → 2.29.6
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/.workflow/templates/partials/methodology-rules.hbs +74 -0
- package/README.md +1 -1
- package/lib/wogi-claude +34 -3
- package/lib/wogi-claude-expect.exp +30 -5
- package/package.json +2 -2
- package/scripts/flow-defer-auth.js +103 -0
- package/scripts/flow-utils.js +52 -0
- package/scripts/hooks/core/deferral-classifier.js +129 -0
- package/scripts/hooks/core/deferral-gate.js +379 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +58 -0
- package/scripts/hooks/core/research-evidence-gate.js +11 -1
- package/scripts/hooks/core/research-required-classifier.js +205 -0
- package/scripts/hooks/core/research-required-gate.js +235 -0
- package/scripts/hooks/core/session-context.js +21 -0
- package/scripts/hooks/core/task-boundary-reset.js +132 -1
- package/scripts/hooks/entry/claude-code/stop.js +26 -0
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +39 -0
|
@@ -213,3 +213,77 @@ The user can dump N items, say "go until you finish" / "autonomous mode" / "run
|
|
|
213
213
|
**Exit conditions**: ready queue drains, user types "stop"/"pause", or fatal error. On exit, render the completion summary (terminal block + JSON payload at `.workflow/state/autonomous-run-summary-<runId>.json`) and clear the flag.
|
|
214
214
|
|
|
215
215
|
Enforced by: `flow-autonomous-detector.js`, `flow-question-queue.js`, `flow-decision-authority.js` (autonomous param + `queue-for-review` + `adversary-loop` buckets), `flow-completion-summary.js`, and the SessionStart context injection in `scripts/hooks/core/session-context.js`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Mechanical Deferral Authorization Gate (wf-f9912af6)
|
|
220
|
+
|
|
221
|
+
The textual "Review-Findings Anti-Deferral" rule above (incident-driven 2026-04-15) is enforced mechanically by the deferral gate. The AI cannot silently mark review/audit findings as `status: deferred*` without explicit user authorization — the PreToolUse hook intercepts every Write/Edit/Bash that targets `.workflow/state/last-review.json` or `.workflow/state/last-audit.json` and BLOCKS the write when:
|
|
222
|
+
|
|
223
|
+
1. The new content introduces one or more findings whose `status` matches `/^deferred(?:[-_].*)?$|^wont-?fix$|^skipped$/i`, AND
|
|
224
|
+
2. No valid authorization marker exists at `.workflow/state/deferral-authorization.json`, AND
|
|
225
|
+
3. The `no-defer-pin.json` is not active (a pin overrides any auth — set when the user says "fix everything" / "no deferrals" / "I don't want tech debt").
|
|
226
|
+
|
|
227
|
+
**Authorization sources** (one of):
|
|
228
|
+
|
|
229
|
+
- **User-prompt classifier** (`scripts/hooks/core/deferral-classifier.js`): regex-detects explicit defer phrases in UserPromptSubmit messages — "defer X", "fix critical only", "ship as-is", "option 2"/"option 4" from the /wogi-review menu, etc. Writes auth marker with TTL 10 min by default.
|
|
230
|
+
- **Explicit CLI**: `node scripts/flow-defer-auth.js grant --scope=all --reason="<verbatim user phrase>"` (or `--findings=F5,F6,...`). Used when the AI needs to record explicit authorization (e.g., user picked option 4).
|
|
231
|
+
|
|
232
|
+
**Negative intent overrides positive**: phrases like "fix everything", "no deferrals", "don't defer", "I don't want tech debt" delete any existing auth and write a `no-defer-pin.json` that hard-blocks deferrals for ~30 minutes.
|
|
233
|
+
|
|
234
|
+
**Bash-mutating commands** that write to the target files AND mention `deferred|wont-fix|skipped|dismissed` are blocked when no auth is active — this catches `node -e "fs.writeFileSync('.workflow/state/last-review.json', ...)"` patterns that bypass Write/Edit. Reads (`cat`/`jq`/`grep`) are not blocked.
|
|
235
|
+
|
|
236
|
+
**Audit trail**: every blocked attempt logs to `.workflow/state/deferral-block-log.json` (last 100 entries) for telemetry.
|
|
237
|
+
|
|
238
|
+
**Why mechanical enforcement matters:** the textual rule has been violated multiple times in incidents — the AI decides "low risk / can wait / pre-existing" and writes `status: deferred` to last-review.json based on its own judgment. The gate makes this structurally impossible without the user's word.
|
|
239
|
+
|
|
240
|
+
**Anti-rationalization** (if any of these thoughts cross your mind, you are about to violate the gate):
|
|
241
|
+
- *"This finding is pre-existing, not introduced by my changes"* → WRONG. Pre-existing is a reason to fix it now (continuous improvement) or to surface it to the user with an explicit "ship / fix / defer" question, not to silently `status: deferred-pre-existing`.
|
|
242
|
+
- *"This is LOW severity, the user won't care"* → WRONG. Severity is the user's call, not yours.
|
|
243
|
+
- *"The adversary already verified it's not a real bug"* → WRONG. If it's not a bug, mark it `dismissed-not-a-bug` only AFTER the user confirms; otherwise leave it `open`.
|
|
244
|
+
- *"I'll batch deferrals into the next review cycle"* → WRONG. There is no "next cycle" — the user reads the findings now.
|
|
245
|
+
|
|
246
|
+
Config: `deferralGate.{enabled,authTtlSeconds,classifyUserPrompts}` in `.workflow/config.json` (defaults: true / 600 / true).
|
|
247
|
+
|
|
248
|
+
Enforced by: `scripts/hooks/core/deferral-gate.js` (core), `scripts/hooks/core/deferral-classifier.js` (intent detection), `scripts/flow-defer-auth.js` (CLI), wired into `scripts/hooks/core/pre-tool-orchestrator.js` (PreToolUse) and `scripts/hooks/entry/claude-code/user-prompt-submit.js` (UserPromptSubmit).
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Mechanical Research-Required Gate (wf-5cd71b1f)
|
|
253
|
+
|
|
254
|
+
The textual rules in CLAUDE.md ("Research Before Propose," Tier 2/3 routing protocol) say the AI must read evidence before answering diagnostic questions. The research-required gate makes this mechanical: it intercepts diagnostic prompts at UserPromptSubmit and re-prompts the AI at Stop hook if the assistant turn produced text without enough Read calls against evidence paths.
|
|
255
|
+
|
|
256
|
+
**How it works**:
|
|
257
|
+
|
|
258
|
+
1. **UserPromptSubmit classifier** (`scripts/hooks/core/research-required-classifier.js`): regex-classifies each prompt into `command` / `factual` / `diagnostic` / `none`.
|
|
259
|
+
- `command` — task IDs, action imperatives ("add X"), follow-ups ("yes", "continue", "option N"), AI's own slash commands
|
|
260
|
+
- `factual` — Tier 1 markers ("what is", "where is", "show me", "list all")
|
|
261
|
+
- `diagnostic` — Tier 2/3 markers ("why", "should I", "what do you think", "is this correct", "explain why", "did you fix")
|
|
262
|
+
- On `diagnostic`: writes `.workflow/state/research-required-this-turn.json` with `{requiredEvidence: 2, attemptCount: 0, classifiedAt}`.
|
|
263
|
+
|
|
264
|
+
2. **Override**: prompt prefix `!` skips the gate entirely. For when the user knows their question is conversational and doesn't need evidence reading.
|
|
265
|
+
|
|
266
|
+
3. **Stop-hook gate** (`scripts/hooks/core/research-required-gate.js`): if marker exists, parses the JSONL transcript for the current turn (since the most recent user entry), counts:
|
|
267
|
+
- `Read` tool calls where `file_path` matches an evidence prefix
|
|
268
|
+
- `Bash` tool calls where the command starts with `cat|head|tail|grep|rg|jq|less|view|awk|sed` and targets an evidence-prefix path
|
|
269
|
+
- `Glob`/`Grep` tool calls (any pattern counts)
|
|
270
|
+
|
|
271
|
+
4. **If count < requiredEvidence**:
|
|
272
|
+
- Increments `attemptCount` in the marker
|
|
273
|
+
- Returns `{continue: true, stopReason: <violation message>}` — Claude Code re-prompts the AI with the message; the AI must redo the turn with reads
|
|
274
|
+
- After `maxAttempts` (default 3): returns `{continue: false, stopReason: <hard-stop message>}` — visible to the user, marker cleared
|
|
275
|
+
|
|
276
|
+
5. **If count ≥ requiredEvidence**: marker is consumed (deleted), Stop proceeds normally.
|
|
277
|
+
|
|
278
|
+
**Evidence prefixes** (shared with `research-evidence-gate.js`): `.workflow/state/`, `.workflow/changes/`, `.workflow/specs/`, `.workflow/epics/`, `lib/`, `scripts/`, `src/`, `tests/`, `app/`. Reading code in answer to "why does X happen" is the legitimate path.
|
|
279
|
+
|
|
280
|
+
**Why mechanical enforcement matters**: the textual Tier 2/3 protocol relies on the AI self-classifying its own question's complexity, which is the rubber-stamp pattern. The gate uses structural markers + Stop-hook redo loop — same proven architecture as `worker-tool-first-gate.js` G1/G4. The AI cannot bypass: UserPromptSubmit fires on every user message, Stop fires on every assistant turn end, and `{continue: true, stopReason}` is honored by Claude Code as a forced redo.
|
|
281
|
+
|
|
282
|
+
**Anti-rationalization**:
|
|
283
|
+
- *"I already know the answer from context"* → WRONG. Confidence is not evidence. The gate fires on the question's structure, not your perceived certainty.
|
|
284
|
+
- *"This question is conversational, doesn't need code reading"* → WRONG. If you genuinely believe that, the user can prefix `!` next time. Within a turn, the gate is final.
|
|
285
|
+
- *"I'll cite the evidence in my next answer instead of reading it now"* → WRONG. Citations require reads in the same turn. The transcript proves it.
|
|
286
|
+
|
|
287
|
+
Config: `researchRequiredGate.{enabled,requiredEvidence,maxAttempts}` in `.workflow/config.json` (defaults: true / 2 / 3). The override prefix `!` is hard-coded.
|
|
288
|
+
|
|
289
|
+
Enforced by: `scripts/hooks/core/research-required-classifier.js` (UserPromptSubmit), `scripts/hooks/core/research-required-gate.js` (Stop), wired into `scripts/hooks/entry/claude-code/user-prompt-submit.js` and `scripts/hooks/entry/claude-code/stop.js`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# WogiFlow
|
|
2
2
|
|
|
3
|
-
A self-improving AI development workflow that learns from your feedback. Currently supports **Claude Code 2.1.33+**.
|
|
3
|
+
A self-improving AI development workflow that learns from your feedback. Currently supports **Claude Code 2.1.33+**. Claude Code **2.1.121+** is recommended for native MCP startup retries (transient channel-server failures auto-recover), Bash resilience to deleted CWDs (worktree cleanup is safe mid-session), and `Always allow` permission persistence across worker restarts.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -D wogiflow
|
package/lib/wogi-claude
CHANGED
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
# WOGI_MAX_RESTARTS — safety cap, default 50 (prevents runaway restart storms)
|
|
22
22
|
# WOGI_WRAPPER_PID — exported to child; hook checks this to confirm wrapper is present
|
|
23
23
|
# WOGI_CLAUDE_BIN — override path to claude binary (default: found via PATH)
|
|
24
|
+
# WOGI_BASH_BIN — (wf-ee4e343b cleanup) override the bash binary used
|
|
25
|
+
# by the PID-alignment subshell trick. Defaults to
|
|
26
|
+
# `bash` on PATH. Useful on minimal containers
|
|
27
|
+
# (Alpine, distroless) where bash lives at a
|
|
28
|
+
# non-standard path or where the shell wrapping the
|
|
29
|
+
# claude CLI is not bash-by-default.
|
|
24
30
|
# WOGI_USE_EXPECT — (EXPERIMENTAL, v2.22.4+) set to 1 to opt IN to the
|
|
25
31
|
# expect-based auto-dismiss of the "Loading development
|
|
26
32
|
# channels" dialog. OFF BY DEFAULT because Ink's
|
|
@@ -99,7 +105,7 @@ if [ "$__wogi_is_worker" -eq 1 ]; then
|
|
|
99
105
|
try {
|
|
100
106
|
const cfg = require(process.cwd() + "/.workflow/config.json");
|
|
101
107
|
process.stdout.write(String(!!(cfg.workspace && cfg.workspace.inheritClaudeAiMcpIntegrations)));
|
|
102
|
-
} catch (
|
|
108
|
+
} catch (_err) { process.stdout.write("false"); }
|
|
103
109
|
' 2>/dev/null)"
|
|
104
110
|
if [ "$__wogi_config_inherit" = "true" ]; then
|
|
105
111
|
__wogi_strip_mcp=0
|
|
@@ -205,7 +211,7 @@ if [ "$__wogi_strip_mcp" -eq 1 ]; then
|
|
|
205
211
|
const ws = cfg && cfg.mcpServers && cfg.mcpServers["wogi-workspace-channel"];
|
|
206
212
|
if (ws) channelEntry = ws;
|
|
207
213
|
}
|
|
208
|
-
} catch (
|
|
214
|
+
} catch (_err) {}
|
|
209
215
|
const payload = channelEntry
|
|
210
216
|
? { mcpServers: { "wogi-workspace-channel": channelEntry } }
|
|
211
217
|
: { mcpServers: {} };
|
|
@@ -284,12 +290,37 @@ __wogi_build_argv() {
|
|
|
284
290
|
|
|
285
291
|
# run_claude — invoke claude, routing through expect when we can auto-dismiss
|
|
286
292
|
# the dev-channels dialog. Preserves stdin/stdout/stderr exactly.
|
|
293
|
+
#
|
|
294
|
+
# wf-ee4e343b: PID-alignment via bash-c-exec trick. The Stop hook's SEC-006
|
|
295
|
+
# check (task-boundary-reset.js:200-206) requires WOGI_WRAPPER_PID === process.ppid
|
|
296
|
+
# in any hook running under claude. Plain `"$CLAUDE_BIN" ...` without `exec`
|
|
297
|
+
# causes bash to fork: claude gets a NEW PID that does not match $$ (this bash
|
|
298
|
+
# wrapper's PID). The check then fails silently, breaking auto-restart for
|
|
299
|
+
# everyone since 2026-04-26.
|
|
300
|
+
#
|
|
301
|
+
# Fix: spawn claude through `bash -c '...'` which forks a fresh bash with its
|
|
302
|
+
# OWN $$, sets WOGI_WRAPPER_PID to that $$, then `exec` replaces the new bash
|
|
303
|
+
# with claude — preserving the same PID. Result: claude's PID equals the
|
|
304
|
+
# WOGI_WRAPPER_PID it inherits, and process.ppid in any hook child of claude
|
|
305
|
+
# equals that same value. The strict-equality SEC-006 check now holds.
|
|
306
|
+
#
|
|
307
|
+
# Why `bash -c` and not a `( ... )` subshell: in bash 3.x (macOS system bash),
|
|
308
|
+
# `$$` inside a `( ... )` subshell returns the OUTER shell's PID, not the
|
|
309
|
+
# subshell's own PID. Bash 4+ adds $BASHPID for that purpose, but we cannot
|
|
310
|
+
# rely on bash 4+ being installed. `bash -c` always returns its own PID via
|
|
311
|
+
# `$$`, regardless of version.
|
|
312
|
+
#
|
|
313
|
+
# Bash -c argv form: `bash -c COMMAND COMMAND_NAME ARG1 ARG2 ...` — COMMAND_NAME
|
|
314
|
+
# becomes $0 inside the script and ARG1..N become $1..$N, so `exec "$0" "$@"`
|
|
315
|
+
# invokes claude with all original args without quoting hazards.
|
|
316
|
+
#
|
|
317
|
+
# For expect mode, the same alignment is performed inside wogi-claude-expect.exp.
|
|
287
318
|
run_claude() {
|
|
288
319
|
__wogi_build_argv "$@"
|
|
289
320
|
if [ "$__wogi_use_expect" -eq 1 ]; then
|
|
290
321
|
expect "$WOGI_EXPECT_SCRIPT" "$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
291
322
|
else
|
|
292
|
-
"$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
323
|
+
"${WOGI_BASH_BIN:-bash}" -c 'export WOGI_WRAPPER_PID=$$; exec "$0" "$@"' "$CLAUDE_BIN" "${__wogi_claude_argv[@]+"${__wogi_claude_argv[@]}"}"
|
|
293
324
|
fi
|
|
294
325
|
}
|
|
295
326
|
|
|
@@ -90,12 +90,37 @@ set claude_bin [lindex $argv 0]
|
|
|
90
90
|
set claude_args [lrange $argv 1 end]
|
|
91
91
|
|
|
92
92
|
# Spawn claude in a pseudo-TTY so its Ink UI renders normally.
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# (
|
|
96
|
-
#
|
|
93
|
+
#
|
|
94
|
+
# wf-ee4e343b PID-alignment: spawn claude through `bash -c` so we can
|
|
95
|
+
# re-export WOGI_WRAPPER_PID=$$ (the subshell's PID) before exec — this
|
|
96
|
+
# makes claude inherit a WOGI_WRAPPER_PID equal to its own PID, satisfying
|
|
97
|
+
# the SEC-006 strict-equality check in task-boundary-reset.js. Without this,
|
|
98
|
+
# expect's spawn gives claude a PID different from WOGI_WRAPPER_PID and the
|
|
99
|
+
# Stop-hook restart trigger silently fails.
|
|
100
|
+
#
|
|
101
|
+
# The bash -c form `bash -c COMMAND COMMAND_NAME ARG1 ARG2 ...` makes
|
|
102
|
+
# COMMAND_NAME = $0 and the remaining args = $1..$N, so we use `exec "$0" "$@"`
|
|
103
|
+
# to invoke claude with all original args — no quoting hazards.
|
|
97
104
|
_wogi_boot_mark "before spawn"
|
|
98
|
-
|
|
105
|
+
|
|
106
|
+
# F4 fix (wf-ee4e343b cleanup): defensive list construction. `lrange $argv 1
|
|
107
|
+
# end` already returns a clean Tcl list, and `{*}$claude_args` splices it
|
|
108
|
+
# without re-parsing element contents — so brace-containing args are
|
|
109
|
+
# preserved as single elements. The Sonnet review flagged this as a quoting
|
|
110
|
+
# hazard; the Opus adversary verified it's safe. We rebuild the list
|
|
111
|
+
# explicitly via [list ...] anyway for defense-in-depth and to make the
|
|
112
|
+
# safety property obvious to future readers (no implicit dependency on
|
|
113
|
+
# lrange's return contract).
|
|
114
|
+
set claude_args_safe [list]
|
|
115
|
+
foreach _arg $claude_args { lappend claude_args_safe $_arg }
|
|
116
|
+
|
|
117
|
+
# Honor WOGI_BASH_BIN (wf-ee4e343b cleanup) for non-standard shell layouts.
|
|
118
|
+
set _wogi_bash_bin "bash"
|
|
119
|
+
if {[info exists env(WOGI_BASH_BIN)] && $env(WOGI_BASH_BIN) ne ""} {
|
|
120
|
+
set _wogi_bash_bin $env(WOGI_BASH_BIN)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
spawn $_wogi_bash_bin -c "export WOGI_WRAPPER_PID=\$\$; exec \"\$0\" \"\$@\"" $claude_bin {*}$claude_args_safe
|
|
99
124
|
_wogi_boot_mark "after spawn (pid=$spawn_id)"
|
|
100
125
|
|
|
101
126
|
# ============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.29.
|
|
3
|
+
"version": "2.29.6",
|
|
4
4
|
"description": "AI-powered development workflow management system with multi-model support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"flow": "./scripts/flow",
|
|
13
|
-
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
13
|
+
"test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
|
|
14
14
|
"test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
|
|
15
15
|
"lint": "eslint scripts/ lib/ tests/",
|
|
16
16
|
"lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Deferral Authorization CLI (wf-f9912af6)
|
|
5
|
+
*
|
|
6
|
+
* Explicit user-authorization helper for the deferral gate. Used when the AI
|
|
7
|
+
* needs to record that the user picked a defer-style menu option in
|
|
8
|
+
* /wogi-review (e.g., "Create tasks for all - fix later in batches").
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* flow defer-auth grant --scope=all --reason="<verbatim user phrase>"
|
|
12
|
+
* flow defer-auth grant --findings=F5,F6,F7 --reason="..."
|
|
13
|
+
* flow defer-auth clear
|
|
14
|
+
* flow defer-auth status
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const gate = require('./hooks/core/deferral-gate');
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const args = {};
|
|
22
|
+
for (const a of argv) {
|
|
23
|
+
if (a.startsWith('--')) {
|
|
24
|
+
const eq = a.indexOf('=');
|
|
25
|
+
if (eq === -1) {
|
|
26
|
+
args[a.slice(2)] = true;
|
|
27
|
+
} else {
|
|
28
|
+
args[a.slice(2, eq)] = a.slice(eq + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cmdGrant(args) {
|
|
36
|
+
let scope = 'all';
|
|
37
|
+
if (args.findings) {
|
|
38
|
+
scope = String(args.findings)
|
|
39
|
+
.split(',')
|
|
40
|
+
.map(s => s.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
if (scope.length === 0) {
|
|
43
|
+
console.error('grant: --findings must be a non-empty comma-separated list');
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
} else if (args.scope === 'all' || args.scope === undefined) {
|
|
47
|
+
scope = 'all';
|
|
48
|
+
} else {
|
|
49
|
+
scope = String(args.scope);
|
|
50
|
+
}
|
|
51
|
+
const reason = args.reason ? String(args.reason) : 'cli-grant';
|
|
52
|
+
const ttlSec = args['ttl-sec'] ? parseInt(args['ttl-sec'], 10) : undefined;
|
|
53
|
+
|
|
54
|
+
const payload = gate.writeAuth({
|
|
55
|
+
scope,
|
|
56
|
+
source: reason,
|
|
57
|
+
grantedBy: 'explicit-cli',
|
|
58
|
+
ttlSec
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!payload) {
|
|
62
|
+
console.error('grant: failed to write authorization marker');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
console.log(JSON.stringify({ status: 'granted', ...payload }, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cmdClear() {
|
|
69
|
+
gate.clearAuth();
|
|
70
|
+
gate.clearNoDeferPin();
|
|
71
|
+
console.log(JSON.stringify({ status: 'cleared' }, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cmdStatus() {
|
|
75
|
+
const auth = gate.loadAuth();
|
|
76
|
+
const pin = gate.loadNoDeferPin();
|
|
77
|
+
console.log(JSON.stringify({
|
|
78
|
+
authorization: auth || null,
|
|
79
|
+
noDeferPin: pin || null,
|
|
80
|
+
authPath: gate.getAuthPath(),
|
|
81
|
+
pinPath: gate.getNoDeferPinPath()
|
|
82
|
+
}, null, 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function usage() {
|
|
86
|
+
console.log('Usage: flow defer-auth <grant|clear|status> [--scope=all|<id>] [--findings=F1,F2] [--reason="..."] [--ttl-sec=600]');
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
92
|
+
const args = parseArgs(rest);
|
|
93
|
+
switch (subcommand) {
|
|
94
|
+
case 'grant': return cmdGrant(args);
|
|
95
|
+
case 'clear': return cmdClear();
|
|
96
|
+
case 'status': return cmdStatus();
|
|
97
|
+
default: return usage();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (require.main === module) main();
|
|
102
|
+
|
|
103
|
+
module.exports = { parseArgs, cmdGrant, cmdClear, cmdStatus };
|
package/scripts/flow-utils.js
CHANGED
|
@@ -336,6 +336,7 @@ function saveReadyData(data) {
|
|
|
336
336
|
const toSave = { ...data, lastUpdated: new Date().toISOString() };
|
|
337
337
|
const result = writeJson(PATHS.ready, toSave);
|
|
338
338
|
invalidateReadyDataCache(); // Invalidate AFTER write completes to avoid stale cache race
|
|
339
|
+
maybeArmTaskBoundaryRestart(previousData, toSave);
|
|
339
340
|
return result;
|
|
340
341
|
}
|
|
341
342
|
|
|
@@ -359,10 +360,61 @@ async function saveReadyDataAsync(data) {
|
|
|
359
360
|
const toSave = { ...data, lastUpdated: new Date().toISOString() };
|
|
360
361
|
const result = writeJson(PATHS.ready, toSave);
|
|
361
362
|
invalidateReadyDataCache(); // Invalidate AFTER write completes
|
|
363
|
+
maybeArmTaskBoundaryRestart(previousData, toSave);
|
|
362
364
|
return result;
|
|
363
365
|
});
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
/**
|
|
369
|
+
* wf-ee4e343b — Phase 1 chokepoint for task-boundary auto-restart.
|
|
370
|
+
*
|
|
371
|
+
* Why this exists: Phase 1 marker writes were previously split across three
|
|
372
|
+
* disjoint paths (`flow done`, `task-completed.js` hook, Stop-hook fallback
|
|
373
|
+
* with a 5-min freshness window). The Stop-hook fallback misses real-world
|
|
374
|
+
* timing (user takes >5min to type next message → fallback rejects) and the
|
|
375
|
+
* other two paths are not always called. By detecting "new entry in
|
|
376
|
+
* recentlyCompleted" right here in saveReadyData — the actual chokepoint
|
|
377
|
+
* every completion goes through — we arm the marker at the moment of
|
|
378
|
+
* completion regardless of who completed the task.
|
|
379
|
+
*
|
|
380
|
+
* Gated on WOGI_WRAPPER_PID so test/CLI/non-wrapper invocations don't
|
|
381
|
+
* write spurious markers. Lazy-required to avoid circular dependency
|
|
382
|
+
* (task-boundary-reset.js → flow-utils.js).
|
|
383
|
+
*/
|
|
384
|
+
function maybeArmTaskBoundaryRestart(previousData, savedData) {
|
|
385
|
+
try {
|
|
386
|
+
if (!process.env.WOGI_WRAPPER_PID) return;
|
|
387
|
+
// First-save guard (F2): when ready.json doesn't yet exist, previousData
|
|
388
|
+
// is null. If savedData arrives pre-populated (fresh install seeded from
|
|
389
|
+
// backup, init script bootstrapping recentlyCompleted, etc.) we MUST NOT
|
|
390
|
+
// arm a restart marker — there's no completion event, just an initial
|
|
391
|
+
// state snapshot. Real completions always have a previousData to diff
|
|
392
|
+
// against because saveReadyData is the only writer.
|
|
393
|
+
if (!previousData) return;
|
|
394
|
+
// F7 fix (wf-ee4e343b cleanup): F2 was asymmetric — readJson returns {}
|
|
395
|
+
// (truthy) on corrupt JSON or missing top-level keys, so the !previousData
|
|
396
|
+
// guard would not catch that case. A corrupt ready.json that recovers as
|
|
397
|
+
// {} followed by a save with populated recentlyCompleted would still
|
|
398
|
+
// false-positive. Require previousData.recentlyCompleted to be an actual
|
|
399
|
+
// array for the diff to be meaningful — anything else is "we don't know
|
|
400
|
+
// the prior state," which is structurally identical to first-save and
|
|
401
|
+
// must NOT arm.
|
|
402
|
+
if (!Array.isArray(previousData.recentlyCompleted)) return;
|
|
403
|
+
const prevTop = previousData.recentlyCompleted[0];
|
|
404
|
+
const curTop = savedData?.recentlyCompleted?.[0];
|
|
405
|
+
if (!curTop || !curTop.id) return;
|
|
406
|
+
if (prevTop && prevTop.id === curTop.id) return; // no new completion
|
|
407
|
+
const { markRestartPending } = require('./hooks/core/task-boundary-reset');
|
|
408
|
+
markRestartPending({
|
|
409
|
+
taskId: curTop.id,
|
|
410
|
+
taskTitle: curTop.title,
|
|
411
|
+
source: 'saveReadyData'
|
|
412
|
+
});
|
|
413
|
+
} catch (_err) {
|
|
414
|
+
// Fail-open — never let an observability/marker write break ready.json save
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
366
418
|
/**
|
|
367
419
|
* Archive overflow completed tasks to a log file (v3.2)
|
|
368
420
|
* When recentlyCompleted exceeds 10 items, archive the overflow
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Deferral Intent Classifier (wf-f9912af6)
|
|
5
|
+
*
|
|
6
|
+
* Regex-based detector for explicit user deferral intent in UserPromptSubmit
|
|
7
|
+
* messages. Cheap (no Haiku call), deterministic, runs every prompt.
|
|
8
|
+
*
|
|
9
|
+
* NEGATIVE intent takes precedence over POSITIVE — if the user says both
|
|
10
|
+
* "fix everything" and "skip Y" in the same message, we assume they want
|
|
11
|
+
* everything fixed (the defer-everything pattern is the dangerous one this
|
|
12
|
+
* gate exists to stop).
|
|
13
|
+
*
|
|
14
|
+
* Negative match → write `no-defer-pin.json` (HARD block, overrides any auth)
|
|
15
|
+
* Positive match → write `deferral-authorization.json` (allows specific scope)
|
|
16
|
+
* Neither → no-op
|
|
17
|
+
*
|
|
18
|
+
* Fail-open: any error in classification falls through silently.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Negative phrases (HIGH PRIORITY — clear auth, write no-defer pin)
|
|
22
|
+
const NEGATIVE_PATTERNS = [
|
|
23
|
+
/\bfix\s+(everything|all\s+of\s+(them|it)|all\s+findings?)\b/i,
|
|
24
|
+
/\bno\s+deferr?als?\b/i,
|
|
25
|
+
/\b(don'?t|do\s+not)\s+defer\b/i,
|
|
26
|
+
/\bi\s+don'?t\s+(want|like)\s+(tech\s*-?\s*debt|technical\s*-?\s*debt|deferr?al)/i,
|
|
27
|
+
/\bnever\s+defer\b/i,
|
|
28
|
+
/\balways\s+fix\s+(what'?s\s+broken|what\s+needs?\s+fixing)/i,
|
|
29
|
+
/\bnothing\s+(should\s+be|gets)\s+deferr?ed\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Positive phrases (MEDIUM PRIORITY — write auth marker)
|
|
33
|
+
// We're conservative: require defer/skip phrasing to be coupled with finding
|
|
34
|
+
// context (this/that/those/it/option N/F\d+/severity word) to avoid catching
|
|
35
|
+
// unrelated mentions like "let's defer the meeting".
|
|
36
|
+
const POSITIVE_PATTERNS = [
|
|
37
|
+
// "defer X" / "skip X" with a referent
|
|
38
|
+
/\b(defer|skip|ignore|drop)\s+(this|that|those|it|them|f\d+|finding\s+\w+)\b/i,
|
|
39
|
+
/\bleave\s+(this|that|those|f\d+|.*?)\s+(for\s+)?later\b/i,
|
|
40
|
+
|
|
41
|
+
// /wogi-review menu options that mean defer
|
|
42
|
+
/\boption\s*[24]\b/i, // option 2 = "fix critical only"; option 4 = "create tasks for all (defer)"
|
|
43
|
+
/\bcreate\s+tasks?\s+for\s+(all|the\s+rest|remaining)\b/i,
|
|
44
|
+
|
|
45
|
+
// Severity-scoped deferrals
|
|
46
|
+
/\bfix\s+(only\s+)?(critical|high)\s*(\s*\/\s*high)?\s+only\b/i,
|
|
47
|
+
/\bfix\s+(critical|high)\s+(only|first)\b/i,
|
|
48
|
+
/\bskip\s+(low|medium|low\s*\/\s*medium)\b/i,
|
|
49
|
+
|
|
50
|
+
// Ship-as-is style
|
|
51
|
+
/\bship\s+(it\s+)?as\s*-?\s*is\b/i,
|
|
52
|
+
/\bgood\s+enough\s+(as\s*-?\s*is|for\s+now)\b/i,
|
|
53
|
+
/\bcall\s+it\s+(done|good)\b/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Classify a user prompt for deferral intent.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} prompt - the user's UserPromptSubmit text
|
|
60
|
+
* @returns {{ intent: 'negative'|'positive'|'none', match?: string, scope?: string|string[] }}
|
|
61
|
+
*/
|
|
62
|
+
function classifyDeferralIntent(prompt) {
|
|
63
|
+
if (!prompt || typeof prompt !== 'string') return { intent: 'none' };
|
|
64
|
+
|
|
65
|
+
// Negative first — overrides positive
|
|
66
|
+
for (const rx of NEGATIVE_PATTERNS) {
|
|
67
|
+
const m = prompt.match(rx);
|
|
68
|
+
if (m) return { intent: 'negative', match: m[0] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Positive
|
|
72
|
+
for (const rx of POSITIVE_PATTERNS) {
|
|
73
|
+
const m = prompt.match(rx);
|
|
74
|
+
if (m) {
|
|
75
|
+
// Try to extract scope — look for F\d+ ids in the prompt
|
|
76
|
+
const findingIds = Array.from(prompt.matchAll(/\bF\d+\b/g)).map(x => x[0]);
|
|
77
|
+
return {
|
|
78
|
+
intent: 'positive',
|
|
79
|
+
match: m[0],
|
|
80
|
+
scope: findingIds.length > 0 ? findingIds : 'all'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { intent: 'none' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Apply classification result to the gate's state files. Wired into
|
|
90
|
+
* UserPromptSubmit. Fail-open throughout.
|
|
91
|
+
*/
|
|
92
|
+
function applyClassification(prompt, config) {
|
|
93
|
+
try {
|
|
94
|
+
if (config?.deferralGate?.classifyUserPrompts === false) return { applied: false, reason: 'classifier-disabled' };
|
|
95
|
+
|
|
96
|
+
const result = classifyDeferralIntent(prompt);
|
|
97
|
+
if (result.intent === 'none') return { applied: false, reason: 'no-match' };
|
|
98
|
+
|
|
99
|
+
// Lazy-require to avoid load-order coupling
|
|
100
|
+
const gate = require('./deferral-gate');
|
|
101
|
+
|
|
102
|
+
if (result.intent === 'negative') {
|
|
103
|
+
gate.writeNoDeferPin({ source: result.match });
|
|
104
|
+
return { applied: true, intent: 'negative', match: result.match };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (result.intent === 'positive') {
|
|
108
|
+
gate.writeAuth({
|
|
109
|
+
scope: result.scope,
|
|
110
|
+
source: result.match,
|
|
111
|
+
grantedBy: 'user-prompt',
|
|
112
|
+
config
|
|
113
|
+
});
|
|
114
|
+
return { applied: true, intent: 'positive', match: result.match, scope: result.scope };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { applied: false, reason: 'unhandled-intent' };
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (process.env.DEBUG) console.error(`[deferral-classifier] applyClassification error (fail-open): ${err.message}`);
|
|
120
|
+
return { applied: false, reason: `error: ${err.message}` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
classifyDeferralIntent,
|
|
126
|
+
applyClassification,
|
|
127
|
+
NEGATIVE_PATTERNS,
|
|
128
|
+
POSITIVE_PATTERNS
|
|
129
|
+
};
|