wogiflow 2.26.2 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AC Scope-Preservation Checklist (wf-fe8ef64d / B1).
5
+ *
6
+ * Extends the existing Item Reconciliation Gate (scripts/flow-story-gates.js):
7
+ * - Reconciliation verifies items → criteria at CREATION time only.
8
+ * - Scope-preservation snapshots the originally-stated ACs at creation,
9
+ * then re-verifies at task CLOSE time that none were silently dropped,
10
+ * merged, or reshaped during implementation.
11
+ *
12
+ * Data at rest:
13
+ * .workflow/state/ac-snapshots/<taskId>.json
14
+ *
15
+ * Consumer wiring:
16
+ * - scripts/flow-story.js calls snapshotCriteria() after generating the
17
+ * acceptance criteria list.
18
+ * - scripts/flow-bug.js does the same.
19
+ * - scripts/flow-done.js calls verifyScopePreservation() as a quality gate
20
+ * before moving a task to completed.
21
+ *
22
+ * Story: wf-fe8ef64d (B1)
23
+ * Epic: wf-34290000
24
+ */
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const { PATHS } = require('./flow-paths');
30
+ const { fileExists, safeJsonParse } = require('./flow-io');
31
+
32
+ const SNAPSHOT_DIR = path.join(PATHS.state, 'ac-snapshots');
33
+
34
+ function _ensureDir() {
35
+ if (!fs.existsSync(SNAPSHOT_DIR)) {
36
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
37
+ }
38
+ }
39
+
40
+ function _snapshotPath(taskId) {
41
+ if (!/^wf-[a-f0-9]{8}(-\d{2})?$/i.test(String(taskId || ''))) {
42
+ throw new TypeError(`invalid taskId: ${taskId}`);
43
+ }
44
+ return path.join(SNAPSHOT_DIR, `${taskId}.json`);
45
+ }
46
+
47
+ /**
48
+ * Persist the original acceptance criteria as a baseline for close-time verification.
49
+ * Idempotent: a second call for the same taskId with unchanged content is a no-op.
50
+ *
51
+ * @param {string} taskId
52
+ * @param {string[]} criteria - array of criterion strings
53
+ * @param {object} [meta] - optional metadata (title, createdAt, rawInput)
54
+ * @returns {{ok: boolean, path?: string, skipped?: boolean, reason?: string}}
55
+ */
56
+ function snapshotCriteria(taskId, criteria, meta = {}) {
57
+ if (!Array.isArray(criteria) || criteria.length === 0) {
58
+ return { ok: false, reason: 'criteria must be a non-empty array' };
59
+ }
60
+ _ensureDir();
61
+ const p = _snapshotPath(taskId);
62
+ const payload = {
63
+ taskId,
64
+ snapshotAt: new Date().toISOString(),
65
+ criteria: criteria.map((c, i) => ({ id: `ac-${i + 1}`, text: String(c).trim() })),
66
+ meta,
67
+ };
68
+ // Idempotency: if an existing snapshot has the same texts in the same order, skip.
69
+ if (fileExists(p)) {
70
+ const prev = safeJsonParse(p, null);
71
+ if (prev && Array.isArray(prev.criteria) && prev.criteria.length === payload.criteria.length) {
72
+ const same = prev.criteria.every((c, i) => c.text === payload.criteria[i].text);
73
+ if (same) return { ok: true, path: p, skipped: true, reason: 'identical snapshot exists' };
74
+ }
75
+ }
76
+ fs.writeFileSync(p, JSON.stringify(payload, null, 2));
77
+ return { ok: true, path: p };
78
+ }
79
+
80
+ /**
81
+ * Load a prior snapshot.
82
+ * @param {string} taskId
83
+ * @returns {object|null}
84
+ */
85
+ function loadSnapshot(taskId) {
86
+ const p = _snapshotPath(taskId);
87
+ if (!fileExists(p)) return null;
88
+ return safeJsonParse(p, null);
89
+ }
90
+
91
+ /**
92
+ * Token-overlap coverage check. Reuses the heuristic from flow-story-gates.js
93
+ * but returns a per-criterion verdict instead of a single boolean.
94
+ *
95
+ * @param {string} original - original criterion text
96
+ * @param {string[]} current - current criterion texts
97
+ * @returns {{ status: 'preserved'|'modified'|'dropped', matchIdx: number, overlap: number }}
98
+ */
99
+ function _matchCriterion(original, current) {
100
+ const STOPWORDS = new Set([
101
+ 'with', 'from', 'that', 'this', 'have', 'make', 'been', 'were', 'their',
102
+ 'they', 'them', 'will', 'should', 'would', 'could', 'there', 'into',
103
+ 'when', 'then', 'than', 'which', 'what', 'your', 'given',
104
+ // User-story boilerplate — every criterion contains these, so they dilute signal
105
+ 'user', 'users', 'admin', 'admins', 'administrator', 'administrators',
106
+ 'able', 'must', 'shall', 'action',
107
+ ]);
108
+ const tokenize = (s) => new Set(
109
+ String(s).toLowerCase().match(/\b[a-z]{4,}\b/g)?.filter((w) => !STOPWORDS.has(w)) || []
110
+ );
111
+ const origTokens = tokenize(original);
112
+ if (origTokens.size === 0) return { status: 'dropped', matchIdx: -1, overlap: 0 };
113
+
114
+ let best = { idx: -1, overlap: 0 };
115
+ current.forEach((c, idx) => {
116
+ const ct = tokenize(c);
117
+ let overlap = 0;
118
+ for (const t of origTokens) {
119
+ if (ct.has(t)) overlap++;
120
+ }
121
+ if (overlap > best.overlap) best = { idx, overlap };
122
+ });
123
+
124
+ const orig = origTokens.size;
125
+ if (best.overlap === 0) return { status: 'dropped', matchIdx: -1, overlap: 0 };
126
+
127
+ // Strong match: ≥ 75% overlap AND ≥ 2 absolute tokens (or ≥ orig when orig < 2) → preserved.
128
+ // Weak match: any non-zero overlap that didn't reach the strong bar → modified.
129
+ // Rationale: uploading a photo vs. deleting a photo share "profile|photo" at 67%,
130
+ // but the verb difference makes it a distinct criterion — flag as modified for review.
131
+ const ratio = best.overlap / orig;
132
+ const minAbsolute = Math.min(2, orig);
133
+ const preserved = ratio >= 0.75 && best.overlap >= minAbsolute;
134
+ return {
135
+ status: preserved ? 'preserved' : 'modified',
136
+ matchIdx: best.idx,
137
+ overlap: best.overlap,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Verify scope preservation by comparing current criteria to the original snapshot.
143
+ *
144
+ * @param {string} taskId
145
+ * @param {string[]} currentCriteria
146
+ * @returns {{ok: boolean, reason?: string, preserved: Array, modified: Array, dropped: Array, added: Array}}
147
+ */
148
+ function verifyScopePreservation(taskId, currentCriteria) {
149
+ const snap = loadSnapshot(taskId);
150
+ if (!snap) return { ok: false, reason: `no snapshot for ${taskId}`, preserved: [], modified: [], dropped: [], added: [] };
151
+ if (!Array.isArray(currentCriteria)) {
152
+ return { ok: false, reason: 'currentCriteria must be an array', preserved: [], modified: [], dropped: [], added: [] };
153
+ }
154
+
155
+ const preserved = [];
156
+ const modified = [];
157
+ const dropped = [];
158
+ const matchedCurrentIdxs = new Set();
159
+
160
+ for (const orig of snap.criteria) {
161
+ const m = _matchCriterion(orig.text, currentCriteria);
162
+ if (m.status === 'preserved') {
163
+ preserved.push({ id: orig.id, text: orig.text, currentIdx: m.matchIdx });
164
+ matchedCurrentIdxs.add(m.matchIdx);
165
+ } else if (m.status === 'modified') {
166
+ modified.push({ id: orig.id, text: orig.text, currentIdx: m.matchIdx, currentText: currentCriteria[m.matchIdx], overlap: m.overlap });
167
+ matchedCurrentIdxs.add(m.matchIdx);
168
+ } else {
169
+ dropped.push({ id: orig.id, text: orig.text });
170
+ }
171
+ }
172
+
173
+ const added = currentCriteria
174
+ .map((text, idx) => ({ idx, text }))
175
+ .filter(({ idx }) => !matchedCurrentIdxs.has(idx))
176
+ .map(({ text }) => ({ text }));
177
+
178
+ // Detect collapse-merges: when two originals matched the same current index,
179
+ // the second original is effectively dropped (merged into the first).
180
+ const currentIdxCounts = {};
181
+ for (const p of preserved) currentIdxCounts[p.currentIdx] = (currentIdxCounts[p.currentIdx] || 0) + 1;
182
+ for (const m of modified) currentIdxCounts[m.currentIdx] = (currentIdxCounts[m.currentIdx] || 0) + 1;
183
+ const collapseDetected = Object.values(currentIdxCounts).some((n) => n > 1);
184
+
185
+ return {
186
+ ok: dropped.length === 0 && !collapseDetected,
187
+ collapseDetected,
188
+ preserved,
189
+ modified,
190
+ dropped,
191
+ added,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Format verification result as a human-readable checklist.
197
+ * @param {object} result - from verifyScopePreservation
198
+ * @param {string} [taskId]
199
+ * @returns {string}
200
+ */
201
+ function formatChecklist(result, taskId = '') {
202
+ const lines = [];
203
+ lines.push(`━━━ AC Scope-Preservation Checklist${taskId ? ' — ' + taskId : ''} ━━━`);
204
+ for (const p of result.preserved) lines.push(` ✓ ${p.id}: ${p.text.slice(0, 80)}`);
205
+ for (const m of result.modified) lines.push(` ~ ${m.id}: ${m.text.slice(0, 80)}\n → became: ${String(m.currentText).slice(0, 80)}`);
206
+ for (const d of result.dropped) lines.push(` ✗ ${d.id}: ${d.text.slice(0, 80)} [DROPPED — violates anti-deferral]`);
207
+ for (const a of result.added) lines.push(` + new: ${String(a.text).slice(0, 80)}`);
208
+ lines.push(result.ok ? ' Status: OK — no criteria dropped' : ` Status: BLOCKED — ${result.dropped.length} criterion/criteria dropped`);
209
+ return lines.join('\n');
210
+ }
211
+
212
+ module.exports = {
213
+ snapshotCriteria,
214
+ loadSnapshot,
215
+ verifyScopePreservation,
216
+ formatChecklist,
217
+ SNAPSHOT_DIR,
218
+ };
219
+
220
+ // CLI
221
+ if (require.main === module) {
222
+ const cmd = process.argv[2];
223
+ if (cmd === 'snapshot') {
224
+ const taskId = process.argv[3];
225
+ const criteria = process.argv.slice(4);
226
+ console.log(JSON.stringify(snapshotCriteria(taskId, criteria), null, 2));
227
+ } else if (cmd === 'verify') {
228
+ const taskId = process.argv[3];
229
+ const criteria = process.argv.slice(4);
230
+ const r = verifyScopePreservation(taskId, criteria);
231
+ console.log(formatChecklist(r, taskId));
232
+ process.exit(r.ok ? 0 : 1);
233
+ } else {
234
+ console.error('usage: flow-ac-scope-preservation snapshot <taskId> <criterion...>');
235
+ console.error(' flow-ac-scope-preservation verify <taskId> <criterion...>');
236
+ process.exit(2);
237
+ }
238
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Auto-Review Worker (detached child invoked by lib/worktree-review.js).
6
+ *
7
+ * Responsibilities:
8
+ * 1. Create an isolated worktree via flow-worktree.createWorktree.
9
+ * 2. Invoke the runReview() core on the worktree (heuristic reviewer).
10
+ * 3. Discard the worktree.
11
+ *
12
+ * Exit code is always 0 — findings are communicated via
13
+ * `.workflow/state/auto-review-findings.json`, not via exit status. The
14
+ * parent process has already unref()'d this child; nothing is watching stderr.
15
+ */
16
+
17
+ const { runReview, writeFindings } = require('../lib/worktree-review');
18
+
19
+ function parseArgs(argv) {
20
+ const out = {};
21
+ for (let i = 0; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (a.startsWith('--')) {
24
+ const key = a.slice(2);
25
+ out[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+
31
+ (async () => {
32
+ const args = parseArgs(process.argv.slice(2));
33
+ const taskId = args.task;
34
+ const repoRoot = args.repoRoot || process.cwd();
35
+
36
+ if (!taskId) {
37
+ process.exit(0);
38
+ }
39
+
40
+ let worktree = null;
41
+ try {
42
+ const { createWorktree, discardWorktree } = require('./flow-worktree');
43
+ worktree = await createWorktree({
44
+ taskId: `review-${taskId}`,
45
+ baseBranch: args.baseBranch || undefined,
46
+ repoRoot,
47
+ });
48
+
49
+ // Use the base branch for diff so the reviewer sees the task's changes.
50
+ const baseBranch = worktree.baseBranch || 'HEAD~1';
51
+ await runReview({
52
+ taskId,
53
+ worktreePath: worktree.path,
54
+ baseBranch,
55
+ });
56
+
57
+ try { await discardWorktree(worktree, { force: true }); } catch (_err) { /* noop */ }
58
+ } catch (err) {
59
+ writeFindings({
60
+ taskId,
61
+ status: 'error',
62
+ startedAt: new Date().toISOString(),
63
+ completedAt: new Date().toISOString(),
64
+ findings: [],
65
+ error: String(err.message || err),
66
+ });
67
+ if (worktree) {
68
+ try {
69
+ const { discardWorktree } = require('./flow-worktree');
70
+ await discardWorktree(worktree, { force: true });
71
+ } catch (_err) { /* noop */ }
72
+ }
73
+ }
74
+ // Normal exit.
75
+ })();
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Wogi Flow — Auto-Review CLI (wf-8d635d0e / E1)
6
+ *
7
+ * Usage:
8
+ * flow-auto-review.js start --task <id> [--repoRoot <path>]
9
+ * flow-auto-review.js status --task <id>
10
+ * flow-auto-review.js await --task <id> [--timeoutMs 90000]
11
+ */
12
+
13
+ const { startReview, readFindings, awaitFindings, DEFAULT_TIMEOUT_MS } = require('../lib/worktree-review');
14
+ const { getConfig } = require('./flow-utils');
15
+
16
+ function parseArgs(argv) {
17
+ const out = {};
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (a.startsWith('--')) {
21
+ const key = a.slice(2);
22
+ out[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function autoReviewEnabled() {
29
+ try {
30
+ const cfg = getConfig();
31
+ return cfg?.autoReview?.enabled === true;
32
+ } catch (_err) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function getTimeoutMs() {
38
+ try {
39
+ const cfg = getConfig();
40
+ const v = cfg?.autoReview?.timeoutMs;
41
+ return typeof v === 'number' && v > 0 ? v : DEFAULT_TIMEOUT_MS;
42
+ } catch (_err) {
43
+ return DEFAULT_TIMEOUT_MS;
44
+ }
45
+ }
46
+
47
+ async function main() {
48
+ const [command, ...rest] = process.argv.slice(2);
49
+ const args = parseArgs(rest);
50
+
51
+ if (!command) {
52
+ console.error('Usage: flow-auto-review.js <start|status|await> --task <id> [opts]');
53
+ process.exit(1);
54
+ }
55
+
56
+ if (command === 'start') {
57
+ if (!autoReviewEnabled()) {
58
+ // Silent no-op when disabled — matches flow-phase.js convention.
59
+ process.exit(0);
60
+ }
61
+ const taskId = args.task;
62
+ if (!taskId) {
63
+ console.error('flow-auto-review start requires --task <id>');
64
+ process.exit(1);
65
+ }
66
+ const repoRoot = args.repoRoot || process.cwd();
67
+ const handle = startReview({ taskId, repoRoot });
68
+ console.log(JSON.stringify({ ok: true, ...handle }));
69
+ return;
70
+ }
71
+
72
+ if (command === 'status') {
73
+ const taskId = args.task;
74
+ if (!taskId) {
75
+ console.error('flow-auto-review status requires --task <id>');
76
+ process.exit(1);
77
+ }
78
+ const rec = readFindings(taskId);
79
+ console.log(JSON.stringify(rec || { taskId, status: 'none' }, null, 2));
80
+ return;
81
+ }
82
+
83
+ if (command === 'await') {
84
+ const taskId = args.task;
85
+ if (!taskId) {
86
+ console.error('flow-auto-review await requires --task <id>');
87
+ process.exit(1);
88
+ }
89
+ const timeoutMs = args.timeoutMs ? parseInt(args.timeoutMs, 10) : getTimeoutMs();
90
+ const rec = await awaitFindings(taskId, timeoutMs);
91
+ console.log(JSON.stringify(rec, null, 2));
92
+ return;
93
+ }
94
+
95
+ console.error(`Unknown subcommand: ${command}`);
96
+ process.exit(1);
97
+ }
98
+
99
+ main().catch((err) => {
100
+ console.error(String(err.message || err));
101
+ process.exit(1);
102
+ });
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Autonomous-Mode Trigger Detector (Story C / wf-d712002e)
5
+ *
6
+ * Decides whether a user message activates autonomous walk-away mode.
7
+ *
8
+ * Strategy:
9
+ * 1. Phrase-match (fast, deterministic, free).
10
+ * 2. Optional Haiku classifier fallback for novel phrasings.
11
+ * 3. Fail-closed: classifier failure → NOT autonomous (interactive is the
12
+ * safer default; users can re-trigger explicitly).
13
+ *
14
+ * Programmatic:
15
+ * const d = require('./flow-autonomous-detector');
16
+ * const r = d.detect(userMessage); // { autonomous, trigger, source }
17
+ * const r = await d.detectAsync(userMessage); // includes classifier fallback
18
+ */
19
+
20
+ const TRIGGER_PHRASES = [
21
+ 'go until you finish',
22
+ 'go until done',
23
+ 'go until complete',
24
+ 'work on all these',
25
+ 'work through all of these',
26
+ 'run this epic autonomously',
27
+ 'run this autonomously',
28
+ "don't bother me, just do it",
29
+ "don't bother me",
30
+ 'walk-away mode',
31
+ 'walk away mode',
32
+ 'autonomous mode',
33
+ 'go ahead until done',
34
+ 'just keep going until you finish',
35
+ 'finish all of them',
36
+ 'do them all without asking'
37
+ ];
38
+
39
+ const STOP_PHRASES = [
40
+ 'stop',
41
+ 'pause',
42
+ 'hold on',
43
+ 'wait',
44
+ 'cancel autonomous',
45
+ 'exit autonomous',
46
+ 'leave autonomous mode'
47
+ ];
48
+
49
+ function normalize(s) {
50
+ return String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
51
+ }
52
+
53
+ function detect(message) {
54
+ const text = normalize(message);
55
+ if (!text) return { autonomous: false, source: 'empty' };
56
+
57
+ for (const phrase of TRIGGER_PHRASES) {
58
+ if (text.includes(phrase)) {
59
+ return { autonomous: true, trigger: phrase, source: 'phrase-match' };
60
+ }
61
+ }
62
+
63
+ return { autonomous: false, source: 'no-match' };
64
+ }
65
+
66
+ function detectStop(message) {
67
+ const text = normalize(message);
68
+ if (!text) return false;
69
+ return STOP_PHRASES.some(p => text === p || text.startsWith(p + ' ') || text.endsWith(' ' + p));
70
+ }
71
+
72
+ /**
73
+ * Async variant with optional Haiku classifier fallback.
74
+ * Caller injects the classifier (function returning {autonomous: boolean, confidence: number}).
75
+ * Fail-closed on any error.
76
+ */
77
+ async function detectAsync(message, { aiClassifier = null, minConfidence = 0.7 } = {}) {
78
+ const fast = detect(message);
79
+ if (fast.autonomous) return fast;
80
+
81
+ if (typeof aiClassifier !== 'function') return fast;
82
+
83
+ try {
84
+ const result = await aiClassifier(message);
85
+ if (
86
+ result &&
87
+ typeof result === 'object' &&
88
+ result.autonomous === true &&
89
+ typeof result.confidence === 'number' &&
90
+ result.confidence >= minConfidence
91
+ ) {
92
+ return { autonomous: true, trigger: 'classifier-match', source: 'classifier', confidence: result.confidence };
93
+ }
94
+ return { autonomous: false, source: 'classifier-no-match' };
95
+ } catch (err) {
96
+ return { autonomous: false, source: 'classifier-error', error: err && err.message };
97
+ }
98
+ }
99
+
100
+ module.exports = {
101
+ TRIGGER_PHRASES,
102
+ STOP_PHRASES,
103
+ detect,
104
+ detectAsync,
105
+ detectStop,
106
+ normalize
107
+ };
108
+
109
+ if (require.main === module) {
110
+ const [,, ...rest] = process.argv;
111
+ const msg = rest.join(' ');
112
+ if (!msg) {
113
+ console.log('Usage: flow-autonomous-detector <message>');
114
+ process.exit(1);
115
+ }
116
+ const r = detect(msg);
117
+ console.log(JSON.stringify(r, null, 2));
118
+ }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow — Autonomous Walk-Away Mode Orchestrator (Story C / wf-d712002e)
5
+ *
6
+ * Thin composition layer that ties together:
7
+ * - flow-autonomous-detector (NL trigger detection)
8
+ * - flow-session-state (disk-canonical activation flag)
9
+ * - flow-question-queue (queued questions + skipped tasks)
10
+ * - flow-completion-summary (terminal + JSON output)
11
+ *
12
+ * Responsibilities live in the underlying modules — this file only
13
+ * wires them. Hot-path hooks (PreToolUse) should NOT import this file;
14
+ * they should read the cache directly via flow-session-state.
15
+ *
16
+ * Programmatic:
17
+ * const am = require('./flow-autonomous-mode');
18
+ * const r = am.maybeActivateFromMessage(userMessage);
19
+ * if (am.shouldDeactivate(userMessage)) am.finalize({ endReason: 'user-interrupt' });
20
+ * am.finalize({ endReason: 'queue-drained' });
21
+ */
22
+
23
+ const detector = require('./flow-autonomous-detector');
24
+ const sessionState = require('./flow-session-state');
25
+ const queue = require('./flow-question-queue');
26
+ const completionSummary = require('./flow-completion-summary');
27
+
28
+ /**
29
+ * Inspect a user message and activate autonomous mode if it matches a
30
+ * trigger phrase. No-op when already active. Returns the activation record
31
+ * (or `null` when no trigger).
32
+ */
33
+ function maybeActivateFromMessage(message) {
34
+ if (sessionState.isAutonomousActive()) return sessionState.getAutonomousMode();
35
+ const r = detector.detect(message);
36
+ if (!r.autonomous) return null;
37
+ return sessionState.activateAutonomousMode({ trigger: r.trigger });
38
+ }
39
+
40
+ /**
41
+ * Check whether a user message should deactivate the active run
42
+ * (stop/pause/cancel etc.). Returns false when no autonomous run is
43
+ * active, regardless of message content.
44
+ */
45
+ function shouldDeactivate(message) {
46
+ if (!sessionState.isAutonomousActive()) return false;
47
+ return detector.detectStop(message);
48
+ }
49
+
50
+ /**
51
+ * Render the completion summary, persist the JSON payload, then clear
52
+ * autonomous-mode state (cache + disk). The queue itself is preserved so
53
+ * the user can resolve queued questions in a later session — only the
54
+ * autonomous flag is cleared.
55
+ *
56
+ * @param {object} input
57
+ * @param {string} input.endReason - queue-drained | user-interrupt | fatal-error
58
+ * @param {Array} [input.completed]
59
+ */
60
+ function finalize({ endReason = 'queue-drained', completed = [] } = {}) {
61
+ const mode = sessionState.getAutonomousMode();
62
+ const queueData = queue.loadQueue();
63
+ const startedAt = mode?.activatedAt || new Date().toISOString();
64
+ const adv = mode?.adversaryInvocations || { used: 0 };
65
+ const cap = sessionState.getAutonomousConfig().maxAdversaryInvocations;
66
+ const result = completionSummary.renderSummary({
67
+ runId: mode?.runId || `auto-finalize-${Date.now().toString(36)}`,
68
+ startedAt,
69
+ trigger: mode?.trigger || 'unspecified',
70
+ completed,
71
+ queuedQuestions: queueData.questions.filter(q => !q.answered),
72
+ skippedTasks: queueData.skippedTasks,
73
+ adversaryInvocations: { used: adv.used || 0, cap, breakdown: adv.breakdown || {} },
74
+ endReason
75
+ });
76
+
77
+ // Story B / wf-ab59f0e4: when the worker finalizes an autonomous run in
78
+ // workspace worker mode, post the COMPLETION-SUMMARY message to the
79
+ // manager channel. Best-effort — the run already finalized locally; if
80
+ // the manager is unreachable, the summary file on disk is still
81
+ // recoverable and the worker can be re-dispatched later.
82
+ if (process.env.WOGI_WORKSPACE_ROOT && process.env.WOGI_REPO_NAME && process.env.WOGI_REPO_NAME !== 'manager') {
83
+ try {
84
+ postSummaryToManager(result.payload);
85
+ result.posted = true;
86
+ } catch (err) {
87
+ result.posted = false;
88
+ result.postError = err.message;
89
+ }
90
+ }
91
+
92
+ if (mode) sessionState.deactivateAutonomousMode();
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * POST one or more COMPLETION-SUMMARY lines to the manager's channel-dispatch
98
+ * HTTP bus. Synchronous + best-effort — finalize() must not throw if the
99
+ * manager is unreachable.
100
+ */
101
+ function postSummaryToManager(payload) {
102
+ const { execFileSync } = require('node:child_process');
103
+ const ws = require('./flow-workspace-summary');
104
+ const taskId = payload.completed?.[0]?.taskId || 'unknown';
105
+ const enriched = { ...payload, workerId: process.env.WOGI_REPO_NAME };
106
+ const lines = ws.encodeMessage(enriched);
107
+ const port = process.env.WOGI_MANAGER_PORT || '8800';
108
+ const repo = process.env.WOGI_REPO_NAME;
109
+ for (const line of lines) {
110
+ execFileSync('curl', [
111
+ '-s', '-X', 'POST',
112
+ `http://127.0.0.1:${port}`,
113
+ '-H', `X-Wogi-From: ${repo}`,
114
+ '-H', `X-Wogi-TaskId: ${taskId}`,
115
+ '--data-binary', line
116
+ ], { stdio: 'ignore', timeout: 5000 });
117
+ }
118
+ }
119
+
120
+ module.exports = {
121
+ maybeActivateFromMessage,
122
+ shouldDeactivate,
123
+ finalize
124
+ };
125
+
126
+ if (require.main === module) {
127
+ const [,, cmd, ...rest] = process.argv;
128
+ switch (cmd) {
129
+ case 'activate': {
130
+ const msg = rest.join(' ');
131
+ const r = maybeActivateFromMessage(msg);
132
+ console.log(JSON.stringify(r, null, 2));
133
+ break;
134
+ }
135
+ case 'finalize': {
136
+ const r = finalize({ endReason: rest[0] || 'queue-drained' });
137
+ console.log(r.terminal);
138
+ if (r.jsonPath) console.log(`\nJSON written: ${r.jsonPath}`);
139
+ break;
140
+ }
141
+ case 'status': {
142
+ const mode = sessionState.getAutonomousMode();
143
+ console.log(JSON.stringify({
144
+ active: sessionState.isAutonomousActive(),
145
+ mode,
146
+ config: sessionState.getAutonomousConfig()
147
+ }, null, 2));
148
+ break;
149
+ }
150
+ default:
151
+ console.log('Usage: flow-autonomous-mode <activate "<msg>"|finalize <reason>|status>');
152
+ }
153
+ }
@@ -19,7 +19,7 @@ const {
19
19
  getConfig,
20
20
  PATHS: _PATHS,
21
21
  readJson: _readJson,
22
- validateTaskId
22
+ _validateTaskId
23
23
  } = require('./flow-utils');
24
24
  const { analyzeComplexity } = require('./flow-task-analyzer');
25
25