wogiflow 2.26.2 → 2.29.1

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 (169) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +149 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-config-loader.js +3 -2
  70. package/scripts/flow-context-compact/expander.js +1 -1
  71. package/scripts/flow-context-compact/section-extractor.js +2 -2
  72. package/scripts/flow-context-gatherer.js +1 -1
  73. package/scripts/flow-context-generator.js +1 -1
  74. package/scripts/flow-context-scoring.js +1 -1
  75. package/scripts/flow-correct.js +1 -1
  76. package/scripts/flow-correction-detector.js +3 -2
  77. package/scripts/flow-decision-authority.js +66 -15
  78. package/scripts/flow-done.js +33 -1
  79. package/scripts/flow-epic-cascade.js +171 -0
  80. package/scripts/flow-epics.js +2 -7
  81. package/scripts/flow-eval-judge.js +1 -1
  82. package/scripts/flow-eval.js +1 -1
  83. package/scripts/flow-export-scanner.js +2 -6
  84. package/scripts/flow-failure-learning.js +1 -1
  85. package/scripts/flow-feature-dossier.js +787 -0
  86. package/scripts/flow-figma-extract.js +2 -2
  87. package/scripts/flow-figma-generate.js +1 -1
  88. package/scripts/flow-gate-confidence.js +1 -1
  89. package/scripts/flow-health.js +52 -1
  90. package/scripts/flow-hooks.js +1 -1
  91. package/scripts/flow-id.js +19 -3
  92. package/scripts/flow-instruction-richness.js +1 -1
  93. package/scripts/flow-knowledge-router.js +1 -1
  94. package/scripts/flow-knowledge-sync.js +1 -1
  95. package/scripts/flow-logic-adversary.js +76 -1
  96. package/scripts/flow-logic-rules.js +380 -0
  97. package/scripts/flow-long-input.js +5 -5
  98. package/scripts/flow-memory-sync.js +1 -1
  99. package/scripts/flow-memory.js +78 -7
  100. package/scripts/flow-migrate.js +1 -1
  101. package/scripts/flow-model-caller.js +1 -1
  102. package/scripts/flow-models.js +2 -2
  103. package/scripts/flow-morning.js +0 -17
  104. package/scripts/flow-multi-approach.js +1 -1
  105. package/scripts/flow-orchestrate-context.js +4 -4
  106. package/scripts/flow-orchestrate-templates.js +1 -1
  107. package/scripts/flow-orchestrate.js +8 -8
  108. package/scripts/flow-peer-review.js +1 -1
  109. package/scripts/flow-phase.js +9 -0
  110. package/scripts/flow-proactive-compact.js +1 -1
  111. package/scripts/flow-prompt-composer.js +3 -2
  112. package/scripts/flow-prompt-template.js +3 -2
  113. package/scripts/flow-providers.js +1 -1
  114. package/scripts/flow-question-queue.js +255 -0
  115. package/scripts/flow-repo-map.js +312 -0
  116. package/scripts/flow-review-passes/index.js +1 -1
  117. package/scripts/flow-review-passes/integration.js +1 -1
  118. package/scripts/flow-review-passes/structure.js +1 -1
  119. package/scripts/flow-revision-tracker.js +1 -1
  120. package/scripts/flow-section-resolver.js +1 -1
  121. package/scripts/flow-session-end.js +74 -5
  122. package/scripts/flow-session-state.js +103 -1
  123. package/scripts/flow-setup-hooks.js +1 -1
  124. package/scripts/flow-skeptical-evaluator.js +274 -0
  125. package/scripts/flow-skill-generator.js +3 -3
  126. package/scripts/flow-skill-learn.js +3 -6
  127. package/scripts/flow-skill-manage.js +248 -0
  128. package/scripts/flow-spec-verifier.js +1 -1
  129. package/scripts/flow-standards-checker.js +75 -0
  130. package/scripts/flow-standards-gate.js +1 -1
  131. package/scripts/flow-statusline-setup.js +8 -2
  132. package/scripts/flow-step-changelog.js +2 -2
  133. package/scripts/flow-step-coverage.js +1 -1
  134. package/scripts/flow-step-knowledge.js +1 -1
  135. package/scripts/flow-step-regression.js +1 -1
  136. package/scripts/flow-step-simplifier.js +1 -1
  137. package/scripts/flow-task-analyzer.js +1 -1
  138. package/scripts/flow-task-classifier.js +1 -1
  139. package/scripts/flow-task-enforcer.js +1 -1
  140. package/scripts/flow-template-extractor.js +1 -1
  141. package/scripts/flow-trap-zone.js +1 -1
  142. package/scripts/flow-utils.js +4 -0
  143. package/scripts/flow-worker-mcp-strip.js +122 -0
  144. package/scripts/flow-worker-question-classifier.js +51 -5
  145. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  146. package/scripts/flow-workspace-summary.js +256 -0
  147. package/scripts/hooks/adapters/base-adapter.js +2 -2
  148. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  149. package/scripts/hooks/core/observation-capture.js +24 -0
  150. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  151. package/scripts/hooks/core/phase-gate.js +15 -1
  152. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  153. package/scripts/hooks/core/post-compact.js +5 -2
  154. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  155. package/scripts/hooks/core/routing-gate.js +58 -0
  156. package/scripts/hooks/core/session-context.js +108 -0
  157. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  158. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  159. package/scripts/hooks/core/session-end.js +25 -0
  160. package/scripts/hooks/core/setup-handler.js +1 -1
  161. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  162. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  163. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  164. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  165. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  166. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  167. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  168. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  169. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Feature Dossier Gate (Core Module)
5
+ *
6
+ * Auto-injects matching feature dossiers + cross-cutting logic rules into
7
+ * the phase context so Claude doesn't have to fetch them under token pressure.
8
+ *
9
+ * Problem this solves (2026-04-24 workspace failure catalog):
10
+ * - Claude doesn't proactively fetch context — grabs one thing, ignores rest
11
+ * - Feature/workflow knowledge is lost between sessions
12
+ * - Owner corrections stop being remembered
13
+ * - "I know the rule exists" ≠ "I consulted it before acting"
14
+ *
15
+ * Two enforcement surfaces:
16
+ * 1. buildPhaseInjection() — called by UserPromptSubmit. Returns a
17
+ * markdown block with the top matching dossiers' canonical content.
18
+ * Injected into the phase prompt alongside the phase-context injection.
19
+ *
20
+ * 2. validateSpecContradictions() — called from /wogi-story spec-review.
21
+ * Scans a spec file against every matching dossier and returns
22
+ * blocking issues (spec mentions rejected alternative, spec reintroduces
23
+ * removed element). Returns { blocked: boolean, issues: [...] }.
24
+ *
25
+ * Fail-open throughout. Missing dossiers, unreadable files, grep failures —
26
+ * all fail-open. The gate is an aid, not a hard stopgap; the core invariant
27
+ * is "don't break existing workflow if dossiers aren't set up yet."
28
+ */
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+ const { PATHS, safeJsonParse, getConfig } = require('../../flow-utils');
33
+
34
+ function isEnabled() {
35
+ try {
36
+ const config = getConfig();
37
+ return config.featureDossier?.enabled !== false;
38
+ } catch (_err) {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ function getCurrentTaskInfo() {
44
+ try {
45
+ const ready = safeJsonParse(PATHS.ready, null);
46
+ if (!ready || !Array.isArray(ready.inProgress) || ready.inProgress.length === 0) {
47
+ return null;
48
+ }
49
+ const task = ready.inProgress[0];
50
+ const files = [];
51
+ if (task.specPath && fs.existsSync(path.join(PATHS.root, task.specPath))) {
52
+ files.push(task.specPath);
53
+ }
54
+ const changeFiles = listRecentlyChangedFiles();
55
+ return {
56
+ id: task.id,
57
+ title: task.title || '',
58
+ description: task.notes || task.description || '',
59
+ criteria: task.criteria || [],
60
+ files: [...new Set([...files, ...changeFiles])]
61
+ };
62
+ } catch (_err) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function listRecentlyChangedFiles() {
68
+ try {
69
+ const { execSync } = require('node:child_process');
70
+ const out = execSync('git diff --name-only HEAD 2>/dev/null; git status --porcelain 2>/dev/null | awk \'{print $2}\' ', {
71
+ cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
72
+ });
73
+ return out.split('\n').map(s => s.trim()).filter(Boolean).slice(0, 100);
74
+ } catch (_err) { return []; }
75
+ }
76
+
77
+ /**
78
+ * Build the phase-injection block for the current task.
79
+ * Returns null if no matches, dossier system is disabled, or on any error.
80
+ */
81
+ function getDossierInjection() {
82
+ if (!isEnabled()) return null;
83
+ let dossier, logicRules;
84
+ try {
85
+ dossier = require('../../flow-feature-dossier');
86
+ logicRules = require('../../flow-logic-rules');
87
+ } catch (_err) { return null; }
88
+
89
+ const taskInfo = getCurrentTaskInfo();
90
+ if (!taskInfo) return null;
91
+
92
+ const criteriaText = (taskInfo.criteria || []).map(c =>
93
+ typeof c === 'string' ? c : (c && c.text) || ''
94
+ ).join('\n');
95
+ const matchInput = {
96
+ title: taskInfo.title,
97
+ description: `${taskInfo.description}\n${criteriaText}`,
98
+ files: taskInfo.files
99
+ };
100
+
101
+ let featureBlock = null, rulesBlock = null;
102
+ try {
103
+ const featureMatches = dossier.matchFeatures(matchInput);
104
+ const config = getConfig();
105
+ const minScore = config.featureDossier?.autoMatchConfidence ?? 1;
106
+ featureBlock = dossier.buildPhaseInjection(featureMatches, { minScore, maxDossiers: 3 });
107
+ } catch (_err) { /* non-blocking */ }
108
+
109
+ try {
110
+ const ruleMatches = logicRules.matchRulesForFiles(
111
+ taskInfo.files,
112
+ [taskInfo.title, taskInfo.description].filter(Boolean)
113
+ );
114
+ rulesBlock = logicRules.buildRulesInjection(ruleMatches);
115
+ } catch (_err) { /* non-blocking */ }
116
+
117
+ const parts = [featureBlock, rulesBlock].filter(Boolean);
118
+ if (parts.length === 0) return null;
119
+ return parts.join('\n\n---\n\n');
120
+ }
121
+
122
+ /**
123
+ * Validate a spec file against all matching dossiers.
124
+ * Returns { blocked, issues } — blocked=true if any blocker-severity issue.
125
+ *
126
+ * @param {string} specContent
127
+ * @param {Object} [taskInfo] — optional pre-computed task info; otherwise detected
128
+ */
129
+ function validateSpecContradictions(specContent, taskInfo) {
130
+ if (!isEnabled()) return { blocked: false, issues: [] };
131
+ let dossier;
132
+ try { dossier = require('../../flow-feature-dossier'); }
133
+ catch (_err) { return { blocked: false, issues: [] }; }
134
+
135
+ const info = taskInfo || getCurrentTaskInfo();
136
+ if (!info) return { blocked: false, issues: [] };
137
+
138
+ const matches = dossier.matchFeatures({
139
+ title: info.title,
140
+ description: info.description,
141
+ files: info.files
142
+ });
143
+
144
+ const allIssues = [];
145
+ for (const m of matches) {
146
+ const d = dossier.loadDossier(m.slug);
147
+ if (!d) continue;
148
+ const issues = dossier.validateSpecAgainstDossier(specContent, d);
149
+ for (const issue of issues) {
150
+ allIssues.push({ ...issue, dossier: m.slug });
151
+ }
152
+ }
153
+
154
+ let blockOnContradiction = true;
155
+ try {
156
+ const config = getConfig();
157
+ blockOnContradiction = config.featureDossier?.blockOnContradiction !== false;
158
+ } catch (_err) { /* default true */ }
159
+
160
+ const blockers = allIssues.filter(i => i.severity === 'blocker');
161
+ return {
162
+ blocked: blockOnContradiction && blockers.length > 0,
163
+ issues: allIssues
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Format contradictions as a block message the caller can surface to the user.
169
+ */
170
+ function formatContradictionMessage(issues) {
171
+ if (!issues || issues.length === 0) return '';
172
+ const lines = ['## Feature Dossier Contradiction Gate', ''];
173
+ lines.push(`${issues.length} contradiction(s) found between the proposed spec and one or more active feature dossiers.`);
174
+ lines.push('');
175
+ lines.push('Dossiers capture owner-rejected alternatives and removed elements. A spec that reintroduces any of these would re-introduce a bug the owner already corrected.');
176
+ lines.push('');
177
+ for (const issue of issues) {
178
+ lines.push(`- **[${issue.severity}]** (${issue.dossier} / ${issue.kind}) ${issue.detail}`);
179
+ }
180
+ lines.push('');
181
+ lines.push('**Resolution**:');
182
+ lines.push('1. Read the referenced dossier section in full.');
183
+ lines.push('2. If the owner has actually changed their mind, update the dossier (move the item out of Rejected / Removed) before proceeding.');
184
+ lines.push('3. Otherwise, revise the spec to not reintroduce the rejected/removed item.');
185
+ return lines.join('\n');
186
+ }
187
+
188
+ module.exports = {
189
+ isEnabled,
190
+ getCurrentTaskInfo,
191
+ getDossierInjection,
192
+ validateSpecContradictions,
193
+ formatContradictionMessage
194
+ };
@@ -376,6 +376,27 @@ async function captureObservation(options) {
376
376
  // Exports
377
377
  // ============================================================
378
378
 
379
+ /**
380
+ * Pick the authoritative tool-execution duration.
381
+ *
382
+ * Claude Code 2.1.119+ attaches a numeric `duration_ms` to PostToolUse /
383
+ * PostToolUseFailure payloads — real tool execution time, excluding
384
+ * permission prompts and PreToolUse hooks. Older CC versions omit it, so
385
+ * callers pass a locally-computed fallback (typically near-zero because
386
+ * the hook only measures the gap between adjacent statements, not the
387
+ * tool itself). Prefer native when numeric; fall back otherwise.
388
+ *
389
+ * @param {object} parsedInput - hook payload
390
+ * @param {number} fallbackMs - local duration to use when native is absent
391
+ * @returns {number}
392
+ */
393
+ function selectDuration(parsedInput, fallbackMs) {
394
+ if (parsedInput && typeof parsedInput.duration_ms === 'number') {
395
+ return parsedInput.duration_ms;
396
+ }
397
+ return fallbackMs;
398
+ }
399
+
379
400
  module.exports = {
380
401
  // Configuration
381
402
  isObservationCaptureEnabled,
@@ -387,6 +408,9 @@ module.exports = {
387
408
  summarizeInput,
388
409
  summarizeOutput,
389
410
 
411
+ // Duration source selection (CC 2.1.119+ native, fallback otherwise)
412
+ selectDuration,
413
+
390
414
  // Main capture function
391
415
  captureObservation
392
416
  };
@@ -259,10 +259,29 @@ function buildOverdueContext(opts = {}) {
259
259
  return lostBlock;
260
260
  }
261
261
 
262
- if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock) return null;
262
+ // Worker completion summaries (Story B / wf-ab59f0e4): surface unseen
263
+ // summaries from any worker that finished an autonomous epic. Render via
264
+ // flow-workspace-summary.renderMultiWorker for the multi-worker block.
265
+ let summariesBlock = null;
266
+ let seenTaskIds = [];
267
+ try {
268
+ const libPath = path.resolve(__dirname, '..', '..', '..', 'lib', 'workspace-dispatch-tracking.js');
269
+ const { readPendingCompletionSummaries, markCompletionSummariesSeen } = require(libPath);
270
+ const pending = readPendingCompletionSummaries(workspaceRoot);
271
+ if (Array.isArray(pending) && pending.length > 0) {
272
+ const wsSummary = require(path.resolve(__dirname, '..', '..', 'flow-workspace-summary.js'));
273
+ const payloads = pending.map(p => p.summary);
274
+ summariesBlock = wsSummary.renderMultiWorker(payloads);
275
+ seenTaskIds = pending.map(p => p.taskId);
276
+ markCompletionSummariesSeen(workspaceRoot, seenTaskIds);
277
+ }
278
+ } catch (_err) { /* fail-open — surface what we can */ }
279
+
280
+ if ((!Array.isArray(overdue) || overdue.length === 0) && !lostBlock && !summariesBlock) return null;
263
281
 
264
282
  const sections = [];
265
283
 
284
+ if (summariesBlock) sections.push(summariesBlock);
266
285
  if (lostBlock) sections.push(lostBlock);
267
286
 
268
287
  if (Array.isArray(overdue) && overdue.length > 0) {
@@ -186,12 +186,26 @@ function transitionPhase(from, to, taskId) {
186
186
  }
187
187
  }
188
188
 
189
- return writePhaseState({
189
+ const wrote = writePhaseState({
190
190
  phase: to,
191
191
  taskId: taskId || current.taskId,
192
192
  updatedAt: new Date().toISOString(),
193
193
  previousPhase: from
194
194
  });
195
+
196
+ // Clear any stale routing-pending flag left over from a prior turn when
197
+ // crossing a phase boundary. Uses removeRoutingFlag() (not clearRoutingPending)
198
+ // so the cleared-marker is preserved — we don't want to open a 15s bypass
199
+ // window just because a phase transitioned. Fail-open if the module is
200
+ // unavailable (e.g. future CLI adapter without routing-gate wired).
201
+ if (wrote) {
202
+ try {
203
+ const { removeRoutingFlag } = require('./routing-gate');
204
+ removeRoutingFlag();
205
+ } catch (_err) { /* fail-open */ }
206
+ }
207
+
208
+ return wrote;
195
209
  }
196
210
 
197
211
  /**
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Phase-transition Auto-Review trigger (wf-8d635d0e / E1).
5
+ *
6
+ * Called after a successful phase transition. When the transition is
7
+ * `coding → validating` and `config.autoReview.enabled`, fire a detached
8
+ * background review worker. Non-blocking by design (AC5) — parent returns
9
+ * immediately; the worker writes findings asynchronously.
10
+ *
11
+ * Failure-mode: any error from the spawn path is swallowed and logged via
12
+ * DEBUG only. Auto-review is an advisory signal layered on top of the
13
+ * existing Completion Truth Gate — a crash here must never fail the
14
+ * primary phase transition.
15
+ */
16
+
17
+ const { getConfig } = require('../../flow-utils');
18
+
19
+ function isAutoReviewEnabled(cfg) {
20
+ try {
21
+ const c = cfg || getConfig();
22
+ return c?.autoReview?.enabled === true;
23
+ } catch (_err) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Trigger a background auto-review when appropriate.
30
+ *
31
+ * @param {string} from - prior phase
32
+ * @param {string} to - new phase
33
+ * @param {string} taskId
34
+ * @param {Object} [opts]
35
+ * @param {Function} [opts.starter] — injectable startReview (tests)
36
+ * @returns {{ started:boolean, reason?:string, handle?:Object }}
37
+ */
38
+ function maybeStartAutoReview(from, to, taskId, opts = {}) {
39
+ if (from !== 'coding' || to !== 'validating') {
40
+ return { started: false, reason: 'not-validating-transition' };
41
+ }
42
+ if (!taskId) {
43
+ return { started: false, reason: 'no-task-id' };
44
+ }
45
+ if (!isAutoReviewEnabled(opts.config)) {
46
+ return { started: false, reason: 'disabled' };
47
+ }
48
+
49
+ const starter = opts.starter || require('../../../lib/worktree-review').startReview;
50
+ try {
51
+ const handle = starter({ taskId, repoRoot: opts.repoRoot });
52
+ return { started: true, handle };
53
+ } catch (err) {
54
+ if (process.env.DEBUG) {
55
+ console.error(`[auto-review] startReview failed: ${err.message}`);
56
+ }
57
+ return { started: false, reason: 'spawn-error', error: String(err.message || err) };
58
+ }
59
+ }
60
+
61
+ module.exports = { maybeStartAutoReview, isAutoReviewEnabled };
@@ -184,10 +184,13 @@ function handlePostCompact() {
184
184
 
185
185
  // 3. Re-set routing-pending flag
186
186
  // After compaction, the AI has fresh context and may try to act without routing.
187
- // Setting routing-pending ensures the next tool use goes through routing checks.
187
+ // resetRoutingState() first clears any stale cleared-marker from the pre-compact
188
+ // turn — otherwise setRoutingPending() would short-circuit on the 15s skill-chain
189
+ // suppression window and leave the flag unset, creating a bypass.
188
190
  let routingReArmed = false;
189
191
  try {
190
- const { setRoutingPending } = require('./routing-gate');
192
+ const { setRoutingPending, resetRoutingState } = require('./routing-gate');
193
+ resetRoutingState();
191
194
  setRoutingPending();
192
195
  routingReArmed = true;
193
196
  } catch (err) {
@@ -261,6 +261,27 @@ function runPreToolGates(ctx, deps) {
261
261
  }
262
262
  }
263
263
 
264
+ // Path-discipline gate (Story B / wf-ab59f0e4): runs in BOTH manager and
265
+ // worker mode (different rules each side). Cross-process state writes are
266
+ // blocked fail-loud so file corruption is impossible to ignore.
267
+ if (process.env.WOGI_WORKSPACE_ROOT) {
268
+ try {
269
+ if (typeof deps.checkPathDiscipline === 'function') {
270
+ const pathResult = deps.checkPathDiscipline(toolName, toolInput);
271
+ if (pathResult.blocked) {
272
+ return {
273
+ allowed: false,
274
+ blocked: true,
275
+ reason: pathResult.reason,
276
+ message: pathResult.message,
277
+ };
278
+ }
279
+ }
280
+ } catch (err) {
281
+ if (process.env.DEBUG) console.error(`[Hook] Path discipline gate error (fail-open): ${err.message}`);
282
+ }
283
+ }
284
+
264
285
  // Commit log gate
265
286
  if (toolName === 'Bash' && toolInput.command) {
266
287
  try {
@@ -385,6 +385,62 @@ function incrementStopAttempts(maxAttempts = 10) {
385
385
  }
386
386
  }
387
387
 
388
+ /**
389
+ * Remove the routing-pending flag file WITHOUT writing a cleared-marker.
390
+ *
391
+ * Distinct from clearRoutingPending(): that writes a 15s cleared-marker to
392
+ * suppress re-setting during skill chains. This helper just deletes the flag
393
+ * so stale pending state (e.g. from a crashed prior turn) can't block tools
394
+ * on the next check. The cleared-marker path is left untouched — so if a
395
+ * /wogi-* skill is actively executing, its suppression window is preserved.
396
+ *
397
+ * Used by phase transitions: moving between phases should not leave stale
398
+ * pending flags blocking tools, but must not create a bypass window either.
399
+ *
400
+ * @returns {{ removed: boolean }}
401
+ */
402
+ function removeRoutingFlag() {
403
+ try {
404
+ fs.unlinkSync(ROUTING_FLAG_PATH);
405
+ return { removed: true };
406
+ } catch (err) {
407
+ if (err.code === 'ENOENT') return { removed: true };
408
+ if (process.env.DEBUG) {
409
+ console.error(`[routing-gate] removeRoutingFlag failed: ${err.message}`);
410
+ }
411
+ return { removed: false };
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Reset routing state entirely — delete BOTH the pending flag and the
417
+ * cleared-marker. Leaves a clean slate so the next UserPromptSubmit re-arms
418
+ * the gate normally without any inherited bypass window.
419
+ *
420
+ * Used by PostCompact: compaction produces fresh context, so any in-flight
421
+ * skill-chain suppression or stale pending flag from the pre-compact turn
422
+ * is no longer relevant. Caller is responsible for re-arming via
423
+ * setRoutingPending() if the post-reset state needs a pending flag.
424
+ *
425
+ * @returns {{ reset: boolean }}
426
+ */
427
+ function resetRoutingState() {
428
+ let ok = true;
429
+ for (const p of [ROUTING_FLAG_PATH, ROUTING_CLEARED_PATH]) {
430
+ try {
431
+ fs.unlinkSync(p);
432
+ } catch (err) {
433
+ if (err.code !== 'ENOENT') {
434
+ ok = false;
435
+ if (process.env.DEBUG) {
436
+ console.error(`[routing-gate] resetRoutingState unlink ${p} failed: ${err.message}`);
437
+ }
438
+ }
439
+ }
440
+ }
441
+ return { reset: ok };
442
+ }
443
+
388
444
  module.exports = {
389
445
  isRoutingGateEnabled,
390
446
  hasActiveTask,
@@ -394,6 +450,8 @@ module.exports = {
394
450
  isRoutingRecentlyCleared,
395
451
  checkRoutingGate,
396
452
  incrementStopAttempts,
453
+ removeRoutingFlag,
454
+ resetRoutingState,
397
455
  ROUTING_FLAG_PATH,
398
456
  ROUTING_CLEARED_PATH
399
457
  };
@@ -138,6 +138,12 @@ const KNOWN_STATE_FILES = new Set([
138
138
  '.routing-pending',
139
139
  '.routing-cleared',
140
140
  '.gates-passed.json',
141
+
142
+ // Task-boundary restart machinery (wf-39e9dc09, R-336, wf-f267ea2a)
143
+ 'task-just-completed',
144
+ 'task-boundary-last-triggered',
145
+ 'task-boundary-clean-completion.json',
146
+ 'pending-question.json',
141
147
  ]);
142
148
 
143
149
  /** Known directory names within state/ (not files) */
@@ -871,6 +877,108 @@ function formatContextForInjection(context) {
871
877
  // Non-critical — history file may not exist; continue with normal context
872
878
  }
873
879
 
880
+ // AUTO-PICKUP after clean completion (wf-f267ea2a).
881
+ // When the prior task completed cleanly AND the ready queue is non-empty AND
882
+ // no pending-question marker exists, instruct the AI to immediately invoke
883
+ // /wogi-start <nextReadyId> on the first user message rather than asking
884
+ // "what's next?". This is the main-mode mirror of workspace.autoPickupChannelDispatches.
885
+ //
886
+ // Marker is consumed (deleted) on every SessionStart that observes it,
887
+ // regardless of whether AUTO-PICKUP fires — so a stale marker can't loop
888
+ // across unrelated future restarts. Fail-open throughout: any error or
889
+ // missing config falls back to the default "proceed with next instruction".
890
+ try {
891
+ const cleanMarkerPath = path.join(PATHS.state, 'task-boundary-clean-completion.json');
892
+ if (fs.existsSync(cleanMarkerPath)) {
893
+ const config = getConfig();
894
+ const tbr = config.taskBoundaryReset || {};
895
+ const flagEnabled = tbr.autoPickupNextTask !== false; // default true
896
+ const pendingQuestionPath = path.join(PATHS.state, 'pending-question.json');
897
+ const hasPendingQuestion = fs.existsSync(pendingQuestionPath);
898
+
899
+ // Read the marker for diagnostic context (which task completed)
900
+ const markerPayload = safeJsonParse(cleanMarkerPath, null);
901
+
902
+ // Find next ready task. Prefer the first non-epic task — epics are
903
+ // containers whose work lives in their child stories, and auto-picking an
904
+ // epic produces a restart loop (wogi-start on an epic has no actionable
905
+ // next step when its children are not yet ready-queue tasks). If the
906
+ // queue contains only epics, emit no auto-pickup (safer to stop the loop
907
+ // than to re-enter it).
908
+ //
909
+ // Cascade-after-decomposition (Story E / wf-e28b6cd8): when the marker
910
+ // carries a `nextTaskId` (set by the cascade helper after epic
911
+ // decomposition), use that explicitly. Verify the ID still exists in
912
+ // ready.json before honoring it; if missing/stale, fall back to default
913
+ // first-actionable resolution.
914
+ let nextTaskId = null;
915
+ let nextTaskTitle = null;
916
+ try {
917
+ const ready = getReadyData();
918
+ const queue = Array.isArray(ready?.ready) ? ready.ready : [];
919
+ if (markerPayload && markerPayload.nextTaskId) {
920
+ const explicit = queue.find(t => t && t.id === markerPayload.nextTaskId);
921
+ if (explicit) {
922
+ nextTaskId = explicit.id;
923
+ nextTaskTitle = explicit.title || null;
924
+ }
925
+ }
926
+ if (!nextTaskId) {
927
+ const firstActionable = queue.find(t => t && t.type !== 'epic');
928
+ if (firstActionable) {
929
+ nextTaskId = firstActionable.id || null;
930
+ nextTaskTitle = firstActionable.title || null;
931
+ }
932
+ }
933
+ } catch (_err) { /* fall through — no auto-pickup if ready.json unreadable */ }
934
+
935
+ const shouldInject = flagEnabled && !hasPendingQuestion && nextTaskId;
936
+
937
+ if (shouldInject) {
938
+ output += `### ⚡ AUTO-PICKUP MODE ACTIVE\n`;
939
+ output += `Prior task **${markerPayload?.completedTaskId || 'unknown'}** (${markerPayload?.completedTaskTitle || 'no title'}) completed cleanly. The next ready task is **${nextTaskId}** — ${nextTaskTitle || '(no title)'}.\n\n`;
940
+ output += `**ON THE FIRST USER MESSAGE IN THIS SESSION (regardless of content): immediately invoke \`Skill(skill="wogi-start", args="${nextTaskId}")\`.**\n\n`;
941
+ output += `Do NOT ask "what's next?". Do NOT summarize prior work. Do NOT propose alternatives. The user has authorized autonomous continuation across this epic. If you have a question that genuinely cannot be resolved by self-challenge, use \`flow ask "<question>"\` so the next restart defers correctly (R-336).\n\n`;
942
+ }
943
+
944
+ // Always consume the marker (single-use), regardless of whether we injected
945
+ try { fs.unlinkSync(cleanMarkerPath); } catch (_err) { /* best effort */ }
946
+ }
947
+ } catch (_err) {
948
+ // Non-critical — fall through to default context
949
+ }
950
+
951
+ // Autonomous walk-away mode rehydration (wf-d712002e / Story C).
952
+ // Disk is canonical; cache evaporates at SIGTERM. SessionStart re-hydrates
953
+ // the cache from disk and surfaces the active-mode reminder so the AI
954
+ // continues to honor autonomous routing across task-boundary restarts.
955
+ // Stale-flag detection (older than autonomousMode.stalenessThresholdMs,
956
+ // default 1h) emits an interruption notice and clears the flag — no
957
+ // auto-resume; the user must explicitly say "continue" / "resume".
958
+ try {
959
+ const sessionState = require('../../flow-session-state');
960
+ const result = sessionState.rehydrateAutonomousFromDisk();
961
+ if (result.hydrated && result.mode) {
962
+ const mode = result.mode;
963
+ output += `### ⚡ AUTONOMOUS MODE ACTIVE\n`;
964
+ output += `Walk-away run \`${mode.runId}\` is in progress (trigger: "${mode.trigger}", started ${mode.activatedAt}).\n\n`;
965
+ output += `**Routing rules** for this run:\n`;
966
+ output += `- productBehavior / ux questions → \`queue-for-review\` (do NOT ask the user; \`flow-question-queue\` collects them).\n`;
967
+ output += `- engineering / naming / implementation → decide autonomously.\n`;
968
+ output += `- low-confidence technical → self-adversarial challenge to ≥90% confidence; if still uncertain after the cap, queue.\n`;
969
+ output += `- Blocking errors (typecheck/test/conflict) → fix autonomously; only surface if fundamentally un-fixable.\n\n`;
970
+ output += `**No hedging**. Forbidden phrases (per feedback-patterns.md 2026-04-16): "let me know if", "should I continue", "awaiting your signal", "standing by", "would you like me to". The user is walked away; there is no one to answer until the run ends.\n\n`;
971
+ output += `**Exit conditions**: ready queue drains, user types "stop"/"pause", or fatal error. On exit: render completion summary (terminal block + JSON payload at \`autonomous-run-summary-${mode.runId}.json\`).\n\n`;
972
+ } else if (result.reason === 'stale' && result.staleMode) {
973
+ const stale = result.staleMode;
974
+ output += `### ⚠️ Autonomous Run Interrupted\n`;
975
+ output += `A previous autonomous run (\`${stale.runId}\`, started ${stale.activatedAt}) exceeded the staleness threshold and has been cleared.\n\n`;
976
+ output += `Review \`.workflow/state/autonomous-run-summary-${stale.runId}.json\` (if present) to see what completed before the interruption. The session is now in standard interactive mode. To resume, the user must explicitly say "continue" / "resume autonomous run" — do not auto-restart.\n\n`;
977
+ }
978
+ } catch (_err) {
979
+ // Non-critical — autonomous-mode injection failure must not block session start
980
+ }
981
+
874
982
  // Workspace worker auto-resume (wf-restart-handoff / 2.22.2).
875
983
  // CRITICAL priority — shown at the top so the model acts on it before
876
984
  // anything else. Fires when a worker session starts with queued channel
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Flow — Session End: Memory Proposal Surfacing
5
+ *
6
+ * Reads pending IGR-artifact edit proposals staged by `flow memory propose`
7
+ * and returns a structured summary for the session-end adapter to display.
8
+ *
9
+ * Agent-proposed edits do NOT auto-apply. The user reviews at session-end
10
+ * and runs `flow memory approve <id>` / `flow memory reject <id>`.
11
+ *
12
+ * Story: wf-4434851f (IGR artifact edit proposals).
13
+ */
14
+
15
+ const store = require('../../../lib/memory-proposal-store');
16
+
17
+ function summarizePendingMemoryProposals() {
18
+ let pending;
19
+ try {
20
+ pending = store.listProposals({ status: 'pending' });
21
+ } catch (_err) {
22
+ return null;
23
+ }
24
+ if (!pending || pending.length === 0) return null;
25
+
26
+ const byOp = { append: 0, 'replace-section': 0, 'replace-all': 0 };
27
+ const byBlock = {};
28
+ const previews = [];
29
+ for (const p of pending) {
30
+ byOp[p.op] = (byOp[p.op] || 0) + 1;
31
+ byBlock[p.block] = (byBlock[p.block] || 0) + 1;
32
+ try {
33
+ previews.push(store.previewProposal(p));
34
+ } catch (_err) {
35
+ // Non-fatal — skip a broken preview but keep the rest.
36
+ }
37
+ }
38
+
39
+ return {
40
+ count: pending.length,
41
+ byOp,
42
+ byBlock,
43
+ proposals: pending,
44
+ previews,
45
+ message: formatMessage(pending.length, byOp, previews),
46
+ };
47
+ }
48
+
49
+ function formatMessage(count, byOp, previews) {
50
+ const plural = count !== 1 ? 's' : '';
51
+ const breakdown = Object.entries(byOp)
52
+ .filter(([, n]) => n > 0)
53
+ .map(([op, n]) => `${n} ${op}`)
54
+ .join(', ');
55
+ return [
56
+ `${count} pending memory proposal${plural} (${breakdown}):`,
57
+ ...previews,
58
+ '',
59
+ 'Review: flow memory list',
60
+ 'Approve: flow memory approve <id>',
61
+ 'Reject: flow memory reject <id> [--reason <text>]',
62
+ ].join('\n');
63
+ }
64
+
65
+ module.exports = { summarizePendingMemoryProposals };