wogiflow 2.17.0 → 2.18.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 (111) hide show
  1. package/.claude/commands/wogi-audit.md +212 -17
  2. package/.claude/commands/wogi-research.md +37 -0
  3. package/.claude/commands/wogi-review.md +200 -22
  4. package/.claude/commands/wogi-start.md +45 -0
  5. package/.claude/docs/claude-code-compatibility.md +46 -1
  6. package/.claude/docs/intent-grounded-review.md +209 -0
  7. package/.claude/settings.json +34 -1
  8. package/.workflow/agents/logic-adversary.md +8 -0
  9. package/.workflow/templates/claude-md.hbs +18 -0
  10. package/lib/installer.js +22 -0
  11. package/lib/utils.js +29 -3
  12. package/lib/workspace-changelog.js +2 -1
  13. package/lib/workspace-channel-server.js +4 -6
  14. package/lib/workspace-contracts.js +5 -4
  15. package/lib/workspace-events.js +8 -7
  16. package/lib/workspace-gates.js +4 -3
  17. package/lib/workspace-integration-tests.js +2 -1
  18. package/lib/workspace-intelligence.js +3 -2
  19. package/lib/workspace-locks.js +2 -1
  20. package/lib/workspace-messages.js +7 -6
  21. package/lib/workspace-routing.js +14 -26
  22. package/lib/workspace-session.js +7 -6
  23. package/lib/workspace-sync.js +9 -8
  24. package/package.json +4 -2
  25. package/scripts/base-workflow-step.js +1 -1
  26. package/scripts/flow +19 -0
  27. package/scripts/flow-adaptive-learning.js +1 -1
  28. package/scripts/flow-aggregate.js +2 -1
  29. package/scripts/flow-architect-pass.js +3 -3
  30. package/scripts/flow-archive-runs.js +372 -0
  31. package/scripts/flow-ask.js +1 -1
  32. package/scripts/flow-ast-grep.js +216 -0
  33. package/scripts/flow-audit-gates.js +1 -1
  34. package/scripts/flow-auto-learn.js +8 -11
  35. package/scripts/flow-bug.js +2 -2
  36. package/scripts/flow-capture-gate.js +644 -0
  37. package/scripts/flow-capture.js +4 -3
  38. package/scripts/flow-cli-flags.js +95 -0
  39. package/scripts/flow-community-sync.js +2 -1
  40. package/scripts/flow-community.js +6 -6
  41. package/scripts/flow-conclusion-classifier.js +310 -0
  42. package/scripts/flow-config-defaults.js +3 -3
  43. package/scripts/flow-constants.js +8 -11
  44. package/scripts/flow-context-scoring.js +1 -0
  45. package/scripts/flow-correction-detector.js +344 -3
  46. package/scripts/flow-damage-control.js +1 -1
  47. package/scripts/flow-decisions-merge.js +1 -0
  48. package/scripts/flow-done-gates.js +20 -0
  49. package/scripts/flow-done-report.js +2 -2
  50. package/scripts/flow-done.js +4 -4
  51. package/scripts/flow-epics.js +5 -11
  52. package/scripts/flow-health.js +145 -1
  53. package/scripts/flow-id.js +92 -0
  54. package/scripts/flow-io.js +15 -5
  55. package/scripts/flow-knowledge-router.js +2 -1
  56. package/scripts/flow-links.js +1 -1
  57. package/scripts/flow-log-manager.js +2 -1
  58. package/scripts/flow-logic-adversary.js +4 -4
  59. package/scripts/flow-long-input-cli.js +6 -0
  60. package/scripts/flow-long-input-stories.js +1 -1
  61. package/scripts/flow-loop-retry-learning.js +1 -1
  62. package/scripts/flow-mcp-capabilities.js +2 -3
  63. package/scripts/flow-mcp-docs.js +2 -1
  64. package/scripts/flow-memory-blocks.js +2 -1
  65. package/scripts/flow-memory-sync.js +1 -1
  66. package/scripts/flow-memory.js +767 -0
  67. package/scripts/flow-migrate-igr.js +1 -1
  68. package/scripts/flow-migrate.js +2 -1
  69. package/scripts/flow-model-adapter.js +1 -1
  70. package/scripts/flow-model-config.js +5 -1
  71. package/scripts/flow-model-profile.js +2 -1
  72. package/scripts/flow-orchestrate.js +3 -3
  73. package/scripts/flow-output.js +29 -0
  74. package/scripts/flow-parallel.js +10 -9
  75. package/scripts/flow-pattern-enforcer.js +2 -1
  76. package/scripts/flow-permissions-audit.js +124 -0
  77. package/scripts/flow-plugin-registry.js +2 -2
  78. package/scripts/flow-progress.js +5 -1
  79. package/scripts/flow-project-analyzer.js +1 -1
  80. package/scripts/flow-promote.js +510 -0
  81. package/scripts/flow-registries.js +86 -0
  82. package/scripts/flow-request-log.js +133 -0
  83. package/scripts/flow-research-protocol.js +0 -1
  84. package/scripts/flow-revision-tracker.js +2 -1
  85. package/scripts/flow-roadmap.js +2 -1
  86. package/scripts/flow-rules-sync.js +3 -7
  87. package/scripts/flow-session-end.js +3 -1
  88. package/scripts/flow-session-learning.js +6 -13
  89. package/scripts/flow-session-state.js +2 -2
  90. package/scripts/flow-setup-hooks.js +2 -1
  91. package/scripts/flow-skill-create.js +1 -1
  92. package/scripts/flow-skill-freshness.js +6 -7
  93. package/scripts/flow-skill-learn.js +1 -1
  94. package/scripts/flow-step-coverage.js +1 -1
  95. package/scripts/flow-step-security.js +1 -1
  96. package/scripts/flow-story.js +58 -10
  97. package/scripts/flow-sys.js +204 -0
  98. package/scripts/flow-task-hierarchy.js +88 -0
  99. package/scripts/flow-tech-debt.js +2 -1
  100. package/scripts/flow-test-api.js +1 -1
  101. package/scripts/flow-utils.js +60 -890
  102. package/scripts/hooks/core/bugfix-scope-gate.js +5 -4
  103. package/scripts/hooks/core/deploy-gate.js +1 -1
  104. package/scripts/hooks/core/pre-tool-helpers.js +72 -0
  105. package/scripts/hooks/core/pre-tool-orchestrator.js +442 -0
  106. package/scripts/hooks/core/routing-gate.js +8 -0
  107. package/scripts/hooks/core/session-end.js +28 -0
  108. package/scripts/hooks/entry/claude-code/pre-tool-use.js +48 -492
  109. package/scripts/hooks/entry/shared/hook-runner.js +1 -1
  110. package/scripts/registries/schema-registry.js +1 -1
  111. package/scripts/registries/service-registry.js +1 -1
@@ -0,0 +1,209 @@
1
+ # Intent-Grounded Review (IGR v6.0) — Reference
2
+
3
+ Detailed reference for the IGR phases added to `/wogi-review` in v2.17.3. The main `/wogi-review` skill file contains the mandatory execution flow; this doc holds the full rationale, examples, and background each phase depends on.
4
+
5
+ Read this when:
6
+ - You're debugging unexpected review-skill behavior
7
+ - You're tuning `config.review.*` and want to understand each key's impact
8
+ - You're extending the review pipeline (adding a new phase, new adversary, new tier)
9
+ - A review surfaced a false-positive finding and you want to know which phase should have caught it
10
+
11
+ The four IGR additions to the review pipeline:
12
+
13
+ | Phase | What it adds |
14
+ |-------|---|
15
+ | Phase 0 — Framing | Scope + assumptions surfaced BEFORE agents launch |
16
+ | Phase 2 — Evidence Tiers | Every finding carries `evidenceTier` + `evidenceNote`; severity capped by tier |
17
+ | Phase 2.8 — Findings Adversary | Different-model critique of the findings themselves |
18
+ | Phase 5 — Completion Truth Gate | "Fixed" claims require INTERACTIVE evidence |
19
+
20
+ ---
21
+
22
+ ## Phase 0: Review Framing Pass — Reference Detail
23
+
24
+ **Problem this solves**: "Review" means different things in different invocations. "Review what we just did" is bounded to the session diff; "review the auth flow" is bounded to a module; "review before ship" expects a final-sign-off posture. Without explicit framing, the AI picks its own scope and grades findings against its own mental rubric, producing a different answer than the user asked for.
25
+
26
+ ### The Five Framing Fields
27
+
28
+ | Field | What it captures |
29
+ |---|---|
30
+ | `interpretation` | One sentence: "I understand this as: review X with posture Y" |
31
+ | `scopeIn` | Explicit list: which files, commits, or modules are in scope |
32
+ | `scopeOut` | Explicit list: what this review will NOT cover (by design, not omission) |
33
+ | `assumptions` | 2–5 review-model assumptions (e.g., "a refactor review must verify behavior preservation, not just test pass") |
34
+ | `posture` | `pre-ship` / `session-review` / `security-focused` / `exploratory` — adjusts agent emphasis |
35
+
36
+ ### Posture → Agent Weight Adjustment
37
+
38
+ - `pre-ship` → boost security + integration agents, require Phase 2.8 adversary pass
39
+ - `session-review` → balanced across all agents
40
+ - `security-focused` → security agent mandatory, injection/authn checks emphasized
41
+ - `exploratory` → logic + architecture agents; adversary pass OPTIONAL (via `config.review.framingPass.adversaryInExploratory`)
42
+
43
+ ### Item Reconciliation (Anti-Deferral Guard)
44
+
45
+ When the user's request enumerated multiple focus areas ("review X, Y, Z"), each named item MUST appear in `scopeIn`. If the count shrank (user named 5, framing has 3), the framing pass FAILS — display which items were dropped and require the user to confirm before proceeding.
46
+
47
+ Ported from `/wogi-start`'s anti-deferral rule. The AI cannot silently drop items.
48
+
49
+ ---
50
+
51
+ ## Phase 2: Evidence Tiers — Reference Detail
52
+
53
+ Every finding returned by any review agent MUST carry two additional fields: `evidenceTier` (0–4) and `evidenceNote` (one-line string citing what produced the evidence).
54
+
55
+ ### Tier Definitions
56
+
57
+ | Tier | Name | What it means for findings |
58
+ |------|------|----------------------------|
59
+ | 0 | STATIC | AI inferred from the source alone — no grep, no execution. Weakest. |
60
+ | 1 | STRUCTURAL | AI grepped / globbed / counted instances across the codebase. |
61
+ | 2 | OBSERVATIONAL | AI ran a tool (lint, typecheck, npm audit) and read its output. |
62
+ | 3 | INTERACTIVE | AI executed code or tests and observed the behavior. |
63
+ | 4 | AUTOMATED | A quality gate or test suite produces this finding deterministically on every run. |
64
+
65
+ ### Severity Cap Rules
66
+
67
+ - **Tier 0** findings: severity MUST be LOW (and flagged UNVERIFIED in the report).
68
+ - **Tier 1** findings: severity capped at MEDIUM unless grep returned ≥5 instances.
69
+ - **Tier 2+** findings: severity stands as the agent assigned.
70
+
71
+ ### Why Tiers Matter (Real Incident)
72
+
73
+ During the v2.17.3 self-review (session 2026-04-15), a `code-reviewer` agent reported an F1 finding as "Critical — broken require path" without citing evidence. Manual verification via `require.resolve()` showed the path was correct — the agent's path math was flawed.
74
+
75
+ With tier enforcement, F1 would have been Tier 0 (no grep, no execution), capped at LOW, and flagged UNVERIFIED — alerting the reader to verify before acting. The evidence-tier requirement is the single most powerful rubber-stamp-prevention mechanism in the IGR toolkit.
76
+
77
+ ---
78
+
79
+ ## Phase 2.8: Findings Adversary Critique — Reference Detail
80
+
81
+ This is the review analogue of the `/wogi-audit` Adversary Pass (Step 3.5) and the IGR Logic Adversary (wf-3975a001). Same pattern: different model, separate context, looking for specific defect classes.
82
+
83
+ ### Adversary Model Selection Rule (CRITICAL)
84
+
85
+ The `adversaryPass.adversaryModel` is a mapping, NOT a static string. The AI resolves it at runtime by inspecting which model the review agents used.
86
+
87
+ ```json
88
+ "adversaryModel": {
89
+ "whenAgentOnSonnet": "opus",
90
+ "whenAgentOnOpus": "sonnet",
91
+ "whenAgentOnHaiku": "sonnet",
92
+ "default": "sonnet"
93
+ }
94
+ ```
95
+
96
+ **Override-always rule**: if the resolved value equals the agent model (e.g., legacy plain-string config set to `sonnet` when agents ran on Sonnet), pick a different model instead. Same-model adversary = rubber-stamp, which defeats the entire purpose of the adversary pass.
97
+
98
+ ### Specific Defect Classes to Hunt
99
+
100
+ The adversary prompt includes HUNT instructions for these patterns:
101
+
102
+ 1. Findings where `evidenceTier=0` but severity ≥ HIGH
103
+ 2. Findings that cite line numbers without quoting the surrounding code
104
+ 3. "Broken require path" / "missing import" / "wrong type" claims without `require.resolve` / `tsc` / `grep` verification
105
+ 4. Findings that contradict the framing's `scopeIn` / `scopeOut` declarations
106
+
107
+ ### Applied Adjustments
108
+
109
+ The orchestrator applies the adversary's recommendations automatically:
110
+
111
+ - `severityAdjustments` rewrite findings' severity in the consolidated report (mark `[ADVERSARY-ADJUSTED]`)
112
+ - `scopeDrift` moves findings out of the main report into an "Out-of-Scope Findings" appendix (not dropped — user still sees them)
113
+ - `falsePositives` get marked `[DISPUTED]` in the report body (not removed — user sees both the finding AND the dispute)
114
+ - `missedIssues` get appended as new Tier-0 findings labeled `[ADVERSARY-FOUND]`
115
+ - `evidenceChallenges` downgrade the `evidenceTier` on challenged findings and re-apply the severity cap
116
+
117
+ ### Verdict Semantics
118
+
119
+ - `ACCEPT` — no adjustments needed; findings are well-grounded
120
+ - `ACCEPT_WITH_ADJUSTMENTS` — severity caps/scope fixes applied, report still ships
121
+ - `REVISE_SCOPE` — framing was wrong; reviewer should restart with corrected scope
122
+ - `BLOCK` — adversary found a critical false positive or missed issue that makes the report untrustworthy; user must acknowledge before Phase 3 proceeds
123
+
124
+ **One pass only** — no iteration loop. If the adversary BLOCKS, the user calls it out and we re-review with adjusted scope.
125
+
126
+ ### Archival
127
+
128
+ Every adversary run is archived to `.workflow/state/adversary-runs/review-{timestamp}.json` — same directory as IGR + audit adversary runs. This feeds the `flow promote` pipeline: recurring review-adversary findings graduate to `feedback-patterns.md`.
129
+
130
+ ---
131
+
132
+ ## Phase 5: Completion Truth Gate — Reference Detail
133
+
134
+ **Problem this solves**: A review's "fixed" claim is only as good as the evidence behind it. A finding marked `fixed` because the AI applied an edit is NOT the same as a finding verified to work. Without a truth gate, the sign-off rubber-stamps whatever the agent says.
135
+
136
+ ### Downgrade Rules
137
+
138
+ For every finding now marked `status: fixed`:
139
+
140
+ | Fix evidence tier | New status |
141
+ |---|---|
142
+ | Tier ≥ 3 (INTERACTIVE) | stays `fixed` |
143
+ | Tier 4 (AUTOMATED quality gate) | stays `fixed` |
144
+ | Tier 2 (OBSERVATIONAL — lint/typecheck pass only) | downgraded to `fixed-unverified` |
145
+ | Tier ≤ 1 (STATIC / STRUCTURAL) | downgraded to `implemented-unverified` |
146
+
147
+ ### Persistence
148
+
149
+ Downgraded statuses are persisted to `last-review.json` — NOT silently dropped back to `fixed`. The user should consciously accept unverified fixes, not have them hidden.
150
+
151
+ ### Self-Incident (v2.17.4)
152
+
153
+ In v2.17.4 I claimed to "fix all review findings." The truth gate (applied manually) caught:
154
+
155
+ - F1, F2, F3 — fixed with Tier 2+ evidence (OK)
156
+ - F4 — doc update only, Tier 0, should have been `implemented-unverified`
157
+ - M1 — deferred, but the release notes said "fix all" — promise/delivery mismatch
158
+ - M3 — dropped entirely, never mentioned in the commit
159
+
160
+ User correction: "You're not supposed to defer any fixes. It's up to the user to defer, not you." → Anti-Deferral Guard added to feedback-patterns + decisions.
161
+
162
+ ---
163
+
164
+ ## Config Enforcement Model — Reference Detail
165
+
166
+ All `config.review.*` toggles are AI-honored, not runtime-enforced. No JavaScript reads `config.review.framingPass`, `config.review.evidenceTiers`, `config.review.adversaryPass`, or `config.review.completionTruthGate`.
167
+
168
+ The AI executing `/wogi-review` is responsible for reading these keys via `getConfig()` and honoring them. This matches `/wogi-audit`'s docs-driven model.
169
+
170
+ **Practical implication**: a user who sets `review.adversaryPass.enabled: false` will have the pass skipped ONLY if the AI respects the config. As a reviewer, always load config first and print the toggle states before launching phases.
171
+
172
+ ---
173
+
174
+ ## Config Reference (all IGR keys)
175
+
176
+ ```json
177
+ {
178
+ "review": {
179
+ "minFindings": 3,
180
+ "requireJustificationIfClean": true,
181
+ "framingPass": {
182
+ "enabled": true,
183
+ "itemReconciliation": true,
184
+ "adversaryInExploratory": false
185
+ },
186
+ "evidenceTiers": {
187
+ "enabled": true,
188
+ "capByTier": true
189
+ },
190
+ "adversaryPass": {
191
+ "enabled": true,
192
+ "adversaryModel": {
193
+ "whenAgentOnSonnet": "opus",
194
+ "whenAgentOnOpus": "sonnet",
195
+ "whenAgentOnHaiku": "sonnet",
196
+ "default": "sonnet"
197
+ },
198
+ "applySeverityAdjustments": true,
199
+ "applyScopeDrift": true,
200
+ "blockOnBlockVerdict": true,
201
+ "archiveRuns": true
202
+ },
203
+ "completionTruthGate": {
204
+ "enabled": true,
205
+ "requireInteractiveForFixed": true
206
+ }
207
+ }
208
+ }
209
+ ```
@@ -133,10 +133,43 @@
133
133
  }
134
134
  ]
135
135
  }
136
+ ],
137
+ "TaskCreated": [
138
+ {
139
+ "hooks": [
140
+ {
141
+ "type": "command",
142
+ "command": "node scripts/hooks/entry/claude-code/task-created.js",
143
+ "timeout": 5
144
+ }
145
+ ]
146
+ }
147
+ ],
148
+ "PermissionDenied": [
149
+ {
150
+ "hooks": [
151
+ {
152
+ "type": "command",
153
+ "command": "node scripts/hooks/entry/claude-code/permission-denied.js",
154
+ "timeout": 5
155
+ }
156
+ ]
157
+ }
158
+ ],
159
+ "PreCompact": [
160
+ {
161
+ "hooks": [
162
+ {
163
+ "type": "command",
164
+ "command": "node scripts/hooks/entry/claude-code/pre-compact.js",
165
+ "timeout": 5
166
+ }
167
+ ]
168
+ }
136
169
  ]
137
170
  },
138
171
  "_comment_dynamicHooks": "TaskCreated (2.1.84+) and PermissionDenied (2.1.88+) are added by postinstall.js when the CC version supports them. They must NOT be committed statically — CC rejects the entire settings file if it encounters an unknown hook event name.",
139
172
  "_wogiFlowManaged": true,
140
- "_wogiFlowVersion": "2.4.2",
173
+ "_wogiFlowVersion": "2.17.5",
141
174
  "_comment": "Shared WogiFlow hook configuration. Committed to repo for team use. User-specific overrides go in settings.local.json."
142
175
  }
@@ -35,6 +35,14 @@ Patterns that produce logic failures in practice — seen in real agent session
35
35
 
36
36
  **P11.3 — Also check for EXISTING WOGIFLOW FEATURES that touch the same domain.** Before shipping any new mechanism (hook, wrapper, CLI entry, state file, config key, skill), enumerate the sibling surface: (S1) `grep -r "execSync\|spawn.*claude" lib/ scripts/`, check `.claude/commands/`, check `scripts/flow-constants.js`, check `lib/workspace.js` — does an existing feature already touch this domain? (S2) Show how the new mechanism composes, conflicts, or integrates with each sibling. "Orthogonal" is OK but must be asserted. (S3) If integration work is needed (e.g., the new wrapper needs to be injected into workspace's `execSync('claude')` call), include it in scope OR explicitly file a follow-up story. Silent omission of sibling integration = FAIL. Example violation caught live: `wogi-claude` wrapper initially missed that `lib/workspace.js:1612` spawns claude directly, so workspace-mode workers weren't restart-capable.
37
37
 
38
+ **P11.4 — Generative edge-case taxonomy (5 buckets, always run for any new mechanism).** Go through EACH bucket and demand a sentence of acknowledgment — "addressed by X", "N/A because Y", or "accepted limitation Z documented in spec". Blank buckets = FAIL. The buckets: **B1 Interleaving/concurrency** (TOCTOU, two instances at once, hook-in-hook races), **B2 Partial failure** (step 1 ok, step 2 fails — is the half-done state acceptable?), **B3 Boundary counts** (0x, 1x, 1000x — accumulation, caps, restart-storm), **B4 Execution-environment portability** (which OS/browser/runtime/deploy-target variants does this run in? Which are in scope vs explicitly unsupported?), **B5 Silent-failure observability** (if it breaks silently, will anyone notice? — is there a health-check surface, log line, or telemetry event?). Distinct from P11.1-P11.3 because it's GENERATIVE (force-enumeration) not REACTIVE (critique what the plan says).
39
+
40
+ **Stack-agnostic**: the buckets are universal. Substitute examples with whatever your plan's stack uses. "Windows / non-bash / filesystem" applies to CLI tools; for web frontends B4 means "Safari vs Chrome vs mobile WebView, SSR vs CSR, accessibility modes"; for mobile apps it means "iOS / Android versions, phone vs tablet"; for backends it means "Node/Python/Go runtime spread, containerized vs bare-metal, serverless cold-start". See rubric P11.4 for the full stack-specific mapping.
41
+
42
+ Cost: ~50-100 words added per plan. Value: catches architectural gaps at plan-time that otherwise surface as post-ship fires.
43
+
44
+ Reflex (stack-agnostic): *"for this mechanism — can 2 run at once? can a step half-fail? what happens at 0x and 1000x? which execution-environment variants does this run on? if this breaks silently, what surfaces it?"*
45
+
38
46
  ### What you are NOT looking for
39
47
 
40
48
  - Code style, lint, naming — other gates handle these.
@@ -131,6 +131,7 @@ npm install -D wogiflow && npx flow onboard
131
131
  | `/wogi-register` | Register plugins for /wogi-start routing |
132
132
 
133
133
  See `.claude/docs/commands.md` for complete command reference.
134
+ See `.claude/docs/claude-code-compatibility.md` for Claude Code version features, performance tips, and env vars (incl. **`ENABLE_PROMPT_CACHING_1H=1`** recommended for API-key / Bedrock / Vertex / Foundry users).
134
135
 
135
136
  ## Natural Language Command Detection
136
137
 
@@ -243,6 +244,23 @@ If the user provides N items, ALL N must become tracked work items. No exception
243
244
 
244
245
  **When genuinely unsure the work is still needed**: ask the user explicitly — "Do you still want wf-XXXX to ship this epic, or should we drop it?" Let them decide. Do NOT make that call autonomously.
245
246
 
247
+ ### Review-Findings Anti-Deferral (MANDATORY — INCIDENT-DRIVEN)
248
+
249
+ **Extends Mid-Execution Anti-Deferral to `/wogi-review`, `/wogi-audit`, `/wogi-triage` findings.** If the user asks you to "fix all findings" / "option 1" / any variant that means "address everything," you MUST:
250
+
251
+ 1. **Ship a fix for every finding that carries evidence tier ≥ 1**, regardless of effort estimate.
252
+ 2. **Never silently convert a finding to "deferred"** in the commit or release notes without the user explicitly saying "defer X."
253
+ 3. **If an item is genuinely too large for the current release**, STOP and ask: "Finding X requires ~Y minutes of work. Ship it in this release, split it into its own release, or defer? Your call."
254
+ 4. **Never list a finding in the release description without fixing it.** If v2.17.4 says "fixes F1, F2, F3, M1" and M1 wasn't fixed, that's a promise/delivery mismatch — exactly the rubber-stamp pattern the Completion Truth Gate was designed to prevent.
255
+
256
+ **Anti-Rationalization Checklist for review findings** — if you catch yourself thinking any of these, STOP:
257
+ - "M1 is a restructure, that warrants a separate release" → WRONG. The user said fix all. Ask first if you think it's too big.
258
+ - "This finding is low-risk, it can wait" → WRONG. Low-risk doesn't mean drop-worthy.
259
+ - "The release notes will acknowledge it's deferred" → WRONG. User didn't defer. You are.
260
+ - "I'll mention it in the commit so it's transparent" → WRONG. Transparency ≠ permission. Ship the fix.
261
+
262
+ **Incident that promoted this rule to decisions.md**: 2026-04-15, v2.17.4 release. I claimed "fix all" in the commit message but silently deferred M1 (wogi-review.md bloat) and completely dropped M3 (_fastPath test coverage gap) from the Findings Adversary. User correction: "You're not supposed to defer any fixes. It's up to the user to defer, not you." v2.17.5 fixed M1 + M3 and added this rule.
263
+
246
264
  ### Task ID Format (MANDATORY)
247
265
 
248
266
  All task IDs MUST be generated by `generateTaskId()` from `wogiflow/scripts/flow-utils.js`. **Never manually type a task ID.**
package/lib/installer.js CHANGED
@@ -16,6 +16,7 @@ const readline = require('node:readline');
16
16
 
17
17
  // Shared utilities
18
18
  const { copyDir, safeReadJson } = require('./utils');
19
+ const { getTodayDate } = require('../scripts/flow-output');
19
20
 
20
21
  // Package root (where wogi-flow is installed)
21
22
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
@@ -1029,6 +1030,27 @@ async function init(args) {
1029
1030
  console.log(' /wogi-health - Check workflow health');
1030
1031
  console.log(' /wogi-story - Create a new story');
1031
1032
  console.log('');
1033
+ printPromptCachingTip();
1034
+ }
1035
+
1036
+ /**
1037
+ * Print the ENABLE_PROMPT_CACHING_1H recommendation for non-subscriber Claude Code users.
1038
+ *
1039
+ * Claude Pro/Max/Team/Enterprise (OAuth via claude.ai) already get 1-hour prompt-cache TTL.
1040
+ * API-key, Bedrock, Vertex, and Foundry users default to 5min TTL — any pause longer than
1041
+ * 5min during a WogiFlow session invalidates the cached CLAUDE.md + state-file prefix and
1042
+ * re-pays the full input-token cost on the next turn. Enabling 1h TTL gives substantial
1043
+ * savings on typical multi-hour sessions.
1044
+ *
1045
+ * We surface this exactly once, during `flow init` onboarding. The full rationale lives in
1046
+ * `.claude/docs/claude-code-compatibility.md` under "Features in 2.1.108+".
1047
+ */
1048
+ function printPromptCachingTip() {
1049
+ console.log('💡 Performance tip (API-key / Bedrock / Vertex / Foundry users):');
1050
+ console.log(' Set \x1b[33mENABLE_PROMPT_CACHING_1H=1\x1b[0m in your shell profile for 1-hour');
1051
+ console.log(' prompt-cache TTL (default is 5min). Claude subscribers already get 1h by default.');
1052
+ console.log(' Details: \x1b[36m.claude/docs/claude-code-compatibility.md\x1b[0m (Features in 2.1.108+)');
1053
+ console.log('');
1032
1054
  }
1033
1055
 
1034
1056
  /**
package/lib/utils.js CHANGED
@@ -20,20 +20,46 @@ const REQUEST_TIMEOUT = 10000; // 10 seconds
20
20
  const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
21
21
 
22
22
  /**
23
- * Find the project root by looking for .workflow directory
23
+ * Find the project root with layered strategies:
24
+ * (0) WOGI_PROJECT_ROOT env var if set and valid
25
+ * (1) git rev-parse --show-toplevel (handles worktrees, submodules)
26
+ * (2) filesystem walk for .workflow
27
+ *
28
+ * Matches scripts/flow-paths.getProjectRoot semantics — enhanced 2026-04-15
29
+ * per audit dup-005 (wf-7072d3ac). Kept in lib/ because lib/ cannot require
30
+ * from scripts/ (cross-domain dependency per dual-repo-management.md).
31
+ *
24
32
  * @returns {string|null} Project root path or null if not in a project
25
33
  */
26
34
  function findProjectRoot() {
35
+ // Strategy 0: env var short-circuit
36
+ if (process.env.WOGI_PROJECT_ROOT && fs.existsSync(path.join(process.env.WOGI_PROJECT_ROOT, '.workflow'))) {
37
+ return process.env.WOGI_PROJECT_ROOT;
38
+ }
39
+
40
+ // Strategy 1: git rev-parse (handles worktrees + submodules)
41
+ try {
42
+ const { execSync } = require('node:child_process');
43
+ const gitRoot = execSync('git rev-parse --show-toplevel', {
44
+ encoding: 'utf-8',
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ }).trim();
47
+ if (gitRoot && fs.existsSync(path.join(gitRoot, '.workflow'))) {
48
+ return gitRoot;
49
+ }
50
+ } catch (_err) {
51
+ // Not in git repo or git unavailable — fall through to walk
52
+ }
53
+
54
+ // Strategy 2: filesystem walk (works outside git)
27
55
  let dir = process.cwd();
28
56
  const root = path.parse(dir).root;
29
-
30
57
  while (dir !== root) {
31
58
  if (fs.existsSync(path.join(dir, '.workflow'))) {
32
59
  return dir;
33
60
  }
34
61
  dir = path.dirname(dir);
35
62
  }
36
-
37
63
  return null;
38
64
  }
39
65
 
@@ -11,6 +11,7 @@
11
11
 
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
+ const { safeReadJson } = require('./utils');
14
15
 
15
16
  const { WORKSPACE_CONFIG_FILE } = require('./workspace');
16
17
 
@@ -94,7 +95,7 @@ function aggregateChangelogs(workspaceRoot, options = {}) {
94
95
 
95
96
  let config;
96
97
  try {
97
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
98
+ config = safeReadJson(configPath);
98
99
  } catch (_err) {
99
100
  return { entries: [], memberCount: 0, totalEntries: 0 };
100
101
  }
@@ -20,6 +20,7 @@
20
20
 
21
21
  const http = require('node:http');
22
22
  const readline = require('node:readline');
23
+ const { safeJsonParseContent } = require('./utils');
23
24
 
24
25
  // ============================================================
25
26
  // Constants
@@ -372,12 +373,9 @@ rl.on('line', (line) => {
372
373
  const trimmed = line.trim();
373
374
  if (!trimmed) return;
374
375
 
375
- try {
376
- const msg = JSON.parse(trimmed);
377
- handleRequest(msg);
378
- } catch (_err) {
379
- // Ignore malformed JSON
380
- }
376
+ const msg = safeJsonParseContent(trimmed);
377
+ if (msg) handleRequest(msg);
378
+ // else ignore malformed / prototype-polluting JSON
381
379
  });
382
380
 
383
381
  // ============================================================
@@ -11,6 +11,7 @@
11
11
 
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
+ const { safeReadJson } = require('./utils');
14
15
  const crypto = require('node:crypto');
15
16
 
16
17
  // ============================================================
@@ -410,7 +411,7 @@ function trackContractVersion(workspaceRoot, contractName, content, changedBy, r
410
411
  let versions = { contracts: {} };
411
412
  try {
412
413
  if (fs.existsSync(versionsPath)) {
413
- versions = JSON.parse(fs.readFileSync(versionsPath, 'utf-8'));
414
+ versions = safeReadJson(versionsPath);
414
415
  }
415
416
  } catch (_err) {
416
417
  versions = { contracts: {} };
@@ -468,7 +469,7 @@ function generateContractChangelog(workspaceRoot) {
468
469
 
469
470
  let versions;
470
471
  try {
471
- versions = JSON.parse(fs.readFileSync(versionsPath, 'utf-8'));
472
+ versions = safeReadJson(versionsPath);
472
473
  } catch (_err) {
473
474
  return '# Contract Changelog\n\nError reading contract versions file.\n';
474
475
  }
@@ -514,7 +515,7 @@ function detectContractFormat(filePath) {
514
515
  if (ext === '.ts' || ext === '.d.ts') return 'typescript';
515
516
  if (ext === '.json') {
516
517
  try {
517
- const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
518
+ const content = safeReadJson(filePath);
518
519
  if (content.openapi || content.swagger) return 'openapi';
519
520
  if (content.$schema?.includes('json-schema')) return 'jsonschema';
520
521
  if (content.type || content.properties) return 'jsonschema';
@@ -693,7 +694,7 @@ function checkTypeSyncCompliance(workspaceRoot, manifest) {
693
694
  // Check each consumer's schema-map for duplicates
694
695
  const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
695
696
  try {
696
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
697
+ const config = safeReadJson(configPath);
697
698
 
698
699
  for (const [name, memberConfig] of Object.entries(config.members || {})) {
699
700
  const member = manifest.members?.[name];
@@ -26,6 +26,7 @@
26
26
 
27
27
  const fs = require('node:fs');
28
28
  const path = require('node:path');
29
+ const { safeReadJson, safeJsonParseContent } = require('./utils');
29
30
  const crypto = require('node:crypto');
30
31
 
31
32
  // ============================================================
@@ -150,13 +151,13 @@ function readEvents(workspaceRoot, filter = {}) {
150
151
  const content = fs.readFileSync(eventsPath, 'utf-8');
151
152
  // Support both NDJSON (one JSON per line) and legacy JSON array format
152
153
  if (content.trimStart().startsWith('[')) {
153
- events = JSON.parse(content);
154
+ events = safeJsonParseContent(content, []);
154
155
  if (!Array.isArray(events)) events = [];
155
156
  } else {
156
157
  // NDJSON format
157
- events = content.trim().split('\n').filter(Boolean).map(line => {
158
- try { return JSON.parse(line); } catch (_err) { return null; }
159
- }).filter(Boolean);
158
+ events = content.trim().split('\n').filter(Boolean)
159
+ .map(line => safeJsonParseContent(line))
160
+ .filter(Boolean);
160
161
  }
161
162
  }
162
163
  } catch (_err) {
@@ -229,7 +230,7 @@ function saveSubscription(workspaceRoot, subscription) {
229
230
 
230
231
  try {
231
232
  if (fs.existsSync(subsPath)) {
232
- subs = JSON.parse(fs.readFileSync(subsPath, 'utf-8'));
233
+ subs = safeReadJson(subsPath);
233
234
  if (!Array.isArray(subs)) subs = [];
234
235
  }
235
236
  } catch (_err) {
@@ -254,7 +255,7 @@ function getActiveSubscriptions(workspaceRoot) {
254
255
 
255
256
  try {
256
257
  if (fs.existsSync(subsPath)) {
257
- const subs = JSON.parse(fs.readFileSync(subsPath, 'utf-8'));
258
+ const subs = safeReadJson(subsPath);
258
259
  return (Array.isArray(subs) ? subs : []).filter(s => s.active);
259
260
  }
260
261
  } catch (_err) {
@@ -276,7 +277,7 @@ function removeSubscription(workspaceRoot, subscriptionId) {
276
277
 
277
278
  try {
278
279
  if (!fs.existsSync(subsPath)) return false;
279
- let subs = JSON.parse(fs.readFileSync(subsPath, 'utf-8'));
280
+ let subs = safeReadJson(subsPath);
280
281
  if (!Array.isArray(subs)) return false;
281
282
 
282
283
  const before = subs.length;
@@ -19,6 +19,7 @@
19
19
 
20
20
  const fs = require('node:fs');
21
21
  const path = require('node:path');
22
+ const { safeReadJson } = require('./utils');
22
23
 
23
24
  const { WORKSPACE_CONFIG_FILE, WORKSPACE_DIR } = require('./workspace');
24
25
  const { buildIntegrationMap } = require('./workspace-contracts');
@@ -150,7 +151,7 @@ function identifyCurrentMember(workspaceRoot, cwd) {
150
151
  const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
151
152
 
152
153
  try {
153
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
154
+ const config = safeReadJson(configPath);
154
155
  for (const [name, memberConfig] of Object.entries(config.members || {})) {
155
156
  const memberPath = path.resolve(workspaceRoot, memberConfig.path);
156
157
  if (currentDir === memberPath || currentDir.startsWith(memberPath + path.sep)) {
@@ -187,7 +188,7 @@ function loadWorkspaceContext(workspaceRoot) {
187
188
  // Load config
188
189
  const configPath = path.join(workspaceRoot, WORKSPACE_CONFIG_FILE);
189
190
  try {
190
- context.config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
191
+ context.config = safeReadJson(configPath);
191
192
  } catch (_err) {
192
193
  return context;
193
194
  }
@@ -196,7 +197,7 @@ function loadWorkspaceContext(workspaceRoot) {
196
197
  const manifestPath = path.join(workspaceRoot, WORKSPACE_DIR, 'state', 'workspace-manifest.json');
197
198
  try {
198
199
  if (fs.existsSync(manifestPath)) {
199
- context.manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
200
+ context.manifest = safeReadJson(manifestPath);
200
201
  }
201
202
  } catch (_err) {
202
203
  // Non-critical
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('node:fs');
14
14
  const path = require('node:path');
15
+ const { safeReadJson } = require('./utils');
15
16
 
16
17
  const { buildIntegrationMap } = require('./workspace-contracts');
17
18
 
@@ -190,7 +191,7 @@ function generateAllIntegrationSpecs(workspaceRoot, oldManifest, newManifest) {
190
191
  try {
191
192
  const contractPath = path.join(contractsDir, `${impact.provider}.json`);
192
193
  if (fs.existsSync(contractPath)) {
193
- contract = JSON.parse(fs.readFileSync(contractPath, 'utf-8'));
194
+ contract = safeReadJson(contractPath);
194
195
  }
195
196
  } catch (_err) {
196
197
  // No contract available — generate basic tests
@@ -11,6 +11,7 @@
11
11
 
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
+ const { safeReadJson } = require('./utils');
14
15
 
15
16
  // ============================================================
16
17
  // S5: Contract Drift Detection (Criterion 1)
@@ -35,7 +36,7 @@ function detectContractDrift(workspaceRoot, manifest) {
35
36
 
36
37
  for (const file of files) {
37
38
  try {
38
- const spec = JSON.parse(fs.readFileSync(path.join(contractsDir, file), 'utf-8'));
39
+ const spec = safeReadJson(path.join(contractsDir, file));
39
40
  if (spec.paths) {
40
41
  for (const [routePath, methods] of Object.entries(spec.paths)) {
41
42
  for (const method of Object.keys(methods)) {
@@ -906,7 +907,7 @@ function auditWorkspaceDimension(workspaceRoot, manifest) {
906
907
  try {
907
908
  const memberMetadata = {};
908
909
  const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
909
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
910
+ const config = safeReadJson(configPath);
910
911
 
911
912
  for (const [name, memberConfig] of Object.entries(config.members || {})) {
912
913
  const memberPath = path.resolve(workspaceRoot, memberConfig.path);
@@ -19,6 +19,7 @@
19
19
 
20
20
  const fs = require('node:fs');
21
21
  const path = require('node:path');
22
+ const { safeReadJson } = require('./utils');
22
23
  const crypto = require('node:crypto');
23
24
 
24
25
  // ============================================================
@@ -240,7 +241,7 @@ function listLocks(workspaceRoot) {
240
241
  const files = fs.readdirSync(locksDir).filter(f => f.endsWith('.json'));
241
242
  for (const file of files) {
242
243
  try {
243
- const content = JSON.parse(fs.readFileSync(path.join(locksDir, file), 'utf-8'));
244
+ const content = safeReadJson(path.join(locksDir, file));
244
245
  if (content.id && content.interface && content.owner) {
245
246
  locks.push(content);
246
247
  }