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,223 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Workspace — Manager-side Task Injector (wf-2f49b292 / G5)
5
+ *
6
+ * Writes a task record into a worker's `.workflow/state/ready.json` from the
7
+ * manager, so the manager can dispatch `/wogi-start <taskId>` for a brand-new
8
+ * task that the worker doesn't yet know about.
9
+ *
10
+ * Before G5: manager could only dispatch tasks that already existed in the
11
+ * worker's ready.json (worker had to create them first). G5 closes that gap.
12
+ *
13
+ * Atomicity: write-to-temp + rename — atomic at the filesystem layer.
14
+ * Concurrent injects from two manager turns serialize via rename semantics;
15
+ * the last writer wins only on the rename, so we re-read before every write
16
+ * to avoid losing a concurrent append.
17
+ */
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const crypto = require('node:crypto');
22
+
23
+ const VALID_REPO_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
24
+ const VALID_TASK_ID = /^wf-[0-9a-f]{8}$/i;
25
+ const REQUIRED_FIELDS = ['id', 'title', 'type'];
26
+ const MAX_RETRIES = 5;
27
+
28
+ /**
29
+ * Resolve the filesystem path to a worker's ready.json.
30
+ * Reads wogi-workspace.json manifest to find the worker's repo path.
31
+ *
32
+ * @param {string} workspaceRoot
33
+ * @param {string} repoName
34
+ * @returns {string} absolute path to ready.json (may not yet exist)
35
+ */
36
+ function getWorkerReadyPath(workspaceRoot, repoName) {
37
+ if (!workspaceRoot || typeof workspaceRoot !== 'string') {
38
+ throw new Error('workspaceRoot must be a non-empty string');
39
+ }
40
+ if (!VALID_REPO_NAME.test(repoName)) {
41
+ throw new Error(`Invalid repoName: "${repoName}" — must match ${VALID_REPO_NAME}`);
42
+ }
43
+
44
+ const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
45
+ let manifest;
46
+ try {
47
+ manifest = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
48
+ } catch (err) {
49
+ throw new Error(`Cannot read workspace manifest at ${configPath}: ${err.message}`);
50
+ }
51
+
52
+ const member = manifest.members?.[repoName];
53
+ if (!member) {
54
+ throw new Error(`Unknown repo "${repoName}" in workspace manifest`);
55
+ }
56
+
57
+ const memberPath = member.path || `./${repoName}`;
58
+ const repoPath = path.resolve(workspaceRoot, memberPath);
59
+
60
+ const resolvedRoot = path.resolve(workspaceRoot);
61
+ if (!repoPath.startsWith(resolvedRoot + path.sep) && repoPath !== resolvedRoot) {
62
+ throw new Error(`Worker repo path escapes workspace root: ${repoPath}`);
63
+ }
64
+
65
+ return path.join(repoPath, '.workflow', 'state', 'ready.json');
66
+ }
67
+
68
+ /**
69
+ * Validate a task record has the shape needed for ready.json entries.
70
+ *
71
+ * @param {Object} taskRecord
72
+ * @throws {Error} if invalid
73
+ */
74
+ function validateTaskRecord(taskRecord) {
75
+ if (!taskRecord || typeof taskRecord !== 'object' || Array.isArray(taskRecord)) {
76
+ throw new Error('taskRecord must be a plain object');
77
+ }
78
+ for (const field of REQUIRED_FIELDS) {
79
+ if (!taskRecord[field] || typeof taskRecord[field] !== 'string') {
80
+ throw new Error(`taskRecord.${field} is required and must be a string`);
81
+ }
82
+ }
83
+ if (!VALID_TASK_ID.test(taskRecord.id)) {
84
+ throw new Error(`Invalid task ID "${taskRecord.id}" — expected wf-XXXXXXXX (8 hex)`);
85
+ }
86
+ if (taskRecord.title.length > 500) {
87
+ throw new Error('taskRecord.title exceeds 500 chars');
88
+ }
89
+ }
90
+
91
+ function atomicWriteJson(filePath, data) {
92
+ const dir = path.dirname(filePath);
93
+ fs.mkdirSync(dir, { recursive: true });
94
+ const tmp = path.join(dir, `.${path.basename(filePath)}.${crypto.randomBytes(6).toString('hex')}.tmp`);
95
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
96
+ fs.renameSync(tmp, filePath);
97
+ }
98
+
99
+ function readReadyJson(readyPath) {
100
+ if (!fs.existsSync(readyPath)) {
101
+ return {
102
+ lastUpdated: new Date().toISOString(),
103
+ inProgress: [],
104
+ ready: [],
105
+ blocked: [],
106
+ recentlyCompleted: []
107
+ };
108
+ }
109
+ const raw = fs.readFileSync(readyPath, 'utf-8');
110
+ const parsed = JSON.parse(raw);
111
+ if (!parsed || typeof parsed !== 'object') {
112
+ throw new Error(`ready.json is not a valid object at ${readyPath}`);
113
+ }
114
+ parsed.ready = Array.isArray(parsed.ready) ? parsed.ready : [];
115
+ parsed.inProgress = Array.isArray(parsed.inProgress) ? parsed.inProgress : [];
116
+ parsed.blocked = Array.isArray(parsed.blocked) ? parsed.blocked : [];
117
+ parsed.recentlyCompleted = Array.isArray(parsed.recentlyCompleted) ? parsed.recentlyCompleted : [];
118
+ return parsed;
119
+ }
120
+
121
+ function taskExistsAnywhere(data, taskId) {
122
+ const lists = ['inProgress', 'ready', 'blocked', 'recentlyCompleted'];
123
+ for (const k of lists) {
124
+ if (data[k].some(t => t && t.id === taskId)) return k;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Inject a task record into a worker's ready.json `ready[]` array.
131
+ *
132
+ * Idempotent: if a task with the same id already exists anywhere in the file,
133
+ * returns `{ ok: true, alreadyPresent: <list-name> }` without modifying the file.
134
+ *
135
+ * @param {string} workspaceRoot
136
+ * @param {string} repoName
137
+ * @param {Object} taskRecord — must have id (wf-XXXXXXXX), title, type
138
+ * @returns {{ ok: boolean, path?: string, taskId?: string, alreadyPresent?: string, message?: string }}
139
+ */
140
+ function injectTask(workspaceRoot, repoName, taskRecord) {
141
+ try {
142
+ validateTaskRecord(taskRecord);
143
+ } catch (err) {
144
+ return { ok: false, message: err.message };
145
+ }
146
+
147
+ let readyPath;
148
+ try {
149
+ readyPath = getWorkerReadyPath(workspaceRoot, repoName);
150
+ } catch (err) {
151
+ return { ok: false, message: err.message };
152
+ }
153
+
154
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
155
+ let data;
156
+ try {
157
+ data = readReadyJson(readyPath);
158
+ } catch (err) {
159
+ return { ok: false, message: `Read failed: ${err.message}` };
160
+ }
161
+
162
+ const existingList = taskExistsAnywhere(data, taskRecord.id);
163
+ if (existingList) {
164
+ return { ok: true, path: readyPath, taskId: taskRecord.id, alreadyPresent: existingList };
165
+ }
166
+
167
+ const enriched = {
168
+ ...taskRecord,
169
+ status: taskRecord.status || 'ready',
170
+ created: taskRecord.created || new Date().toISOString(),
171
+ injectedBy: process.env.WOGI_REPO_NAME || 'manager',
172
+ injectedAt: new Date().toISOString()
173
+ };
174
+ data.ready.push(enriched);
175
+ data.lastUpdated = new Date().toISOString();
176
+
177
+ try {
178
+ atomicWriteJson(readyPath, data);
179
+ return { ok: true, path: readyPath, taskId: taskRecord.id };
180
+ } catch (err) {
181
+ if (attempt === MAX_RETRIES - 1) {
182
+ return { ok: false, message: `Write failed after ${MAX_RETRIES} attempts: ${err.message}` };
183
+ }
184
+ }
185
+ }
186
+
187
+ return { ok: false, message: 'Unreachable retry exhaustion' };
188
+ }
189
+
190
+ /**
191
+ * Inject a task into the worker's ready.json, then dispatch /wogi-start.
192
+ *
193
+ * If injection reports `alreadyPresent`, still proceeds to dispatch — the
194
+ * task exists, so dispatch is the right next step.
195
+ *
196
+ * @param {string} workspaceRoot
197
+ * @param {string} repoName
198
+ * @param {Object} taskRecord
199
+ * @param {Object} [opts] — forwarded to dispatchToChannel
200
+ * @returns {Promise<{ inject: Object, dispatch: Object, ok: boolean }>}
201
+ */
202
+ async function injectAndDispatch(workspaceRoot, repoName, taskRecord, opts = {}) {
203
+ const injectResult = injectTask(workspaceRoot, repoName, taskRecord);
204
+ if (!injectResult.ok) {
205
+ return { ok: false, inject: injectResult, dispatch: null };
206
+ }
207
+
208
+ const { dispatchToChannel } = require('./workspace-routing');
209
+ const dispatchResult = await dispatchToChannel(workspaceRoot, repoName, taskRecord.id, opts);
210
+
211
+ return {
212
+ ok: injectResult.ok && dispatchResult.ok,
213
+ inject: injectResult,
214
+ dispatch: dispatchResult
215
+ };
216
+ }
217
+
218
+ module.exports = {
219
+ injectTask,
220
+ injectAndDispatch,
221
+ getWorkerReadyPath,
222
+ validateTaskRecord
223
+ };
package/lib/workspace.js CHANGED
@@ -1216,6 +1216,29 @@ You are a workspace worker. There is NO human watching your terminal. You MUST o
1216
1216
 
1217
1217
  The \`TaskCompleted\` hook will inject an auto-pickup directive when channel dispatches are queued (v2.20.0+). The \`Stop\` hook will BLOCK end-of-turn if you try to stop while dispatches are queued and no task is in progress. These enforcements exist because the silent-stall pattern was incident-worthy (2026-04-16).
1218
1218
 
1219
+ ### Tool-First Turn Contract (v2.27.0+)
1220
+
1221
+ **Every worker turn after a UserPromptSubmit MUST contain at least one tool call. In strict mode (default), the FIRST content block must be a tool call, not text.**
1222
+
1223
+ Rule name: \`worker-tool-first-turn\`. The \`Stop\` hook blocks end-of-turn when this contract is violated:
1224
+
1225
+ - **silent-halt**: zero tool calls across the entire turn (pure-text response)
1226
+ - **text-before-tool-call**: first content block is text, not tool_use (strict mode only)
1227
+
1228
+ Why: pure-text worker responses are invisible to the user — the user only sees the manager terminal. Worker text disappears into the transcript with no downstream consumer. It also disqualifies the three-state end-of-turn contract (ACTION | ESCALATION | IDLE) above.
1229
+
1230
+ **Allowed turn shapes**:
1231
+ - \`tool_use → tool_use → end\`
1232
+ - \`tool_use → text → tool_use → end\` (narrate after acting)
1233
+ - \`tool_use (## QUESTION: dispatch) → end\` (escalation)
1234
+ - \`tool_use (## Results: dispatch) → end\` (reply)
1235
+
1236
+ **Blocked turn shapes**:
1237
+ - \`text → end\` (silent-halt)
1238
+ - \`text → tool_use → end\` (text-before-tool-call — strict mode)
1239
+
1240
+ Config: \`workspace.toolFirstTurnGate.{enabled, strict}\` in \`.workflow/config.json\`. Default \`enabled: true, strict: true\`.
1241
+
1219
1242
  ### CRITICAL: Stop, Don't Degrade
1220
1243
 
1221
1244
  **If you cannot verify your work to the required evidence tier, you may NOT mark the task as complete.** Report it as BLOCKED with the specific verification gap.
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Flow — Parallel-Worktree Auto Review (wf-8d635d0e / E1)
5
+ *
6
+ * On `coding → validating` phase transition, a background "review worker"
7
+ * scans the task's changes in an isolated git worktree and writes findings
8
+ * to `.workflow/state/auto-review-findings.json`. The Completion Truth Gate
9
+ * reads the file and downgrades completion claims when high-severity findings
10
+ * are present.
11
+ *
12
+ * Architecture:
13
+ * - `startReview({ taskId })` — detached background spawn (fire-and-forget)
14
+ * - `runReview({ taskId, reviewer, ... })` — core scan loop (used in-process
15
+ * by the detached child; injectable `reviewer` for tests)
16
+ * - `writeFindings / readFindings / awaitFindings` — JSON file I/O helpers.
17
+ *
18
+ * The "workspace worker" per the spec is a detached Node child process — not
19
+ * an MCP-channel-hosted Claude session. Per-task model selection is a Claude
20
+ * Code feature we don't yet own (AC6 — documented known limitation); the
21
+ * review therefore defaults to a heuristic static scan of the worktree diff
22
+ * so it's useful today without a running Claude Code child.
23
+ */
24
+
25
+ const path = require('node:path');
26
+ const { spawn, execFileSync } = require('node:child_process');
27
+
28
+ const { PATHS } = require('../scripts/flow-paths');
29
+ const { readJson, writeJson } = require('../scripts/flow-io');
30
+
31
+ const FINDINGS_FILE = path.join(PATHS.state, 'auto-review-findings.json');
32
+ const DEFAULT_TIMEOUT_MS = 90000;
33
+ const POLL_INTERVAL_MS = 250;
34
+
35
+ // ============================================================
36
+ // File I/O — { schemaVersion, records: [...] } object wrapping the records
37
+ // array. Object-at-root is required by safeJsonParse (prototype-pollution
38
+ // protection). Semantics per AC3 remain "array; last-write-wins per task":
39
+ // `records` is the array, one entry per taskId after last-write dedupe.
40
+ // ============================================================
41
+
42
+ const SCHEMA_VERSION = 1;
43
+
44
+ function readAll() {
45
+ const data = readJson(FINDINGS_FILE, { schemaVersion: SCHEMA_VERSION, records: [] });
46
+ if (data && Array.isArray(data.records)) return data.records;
47
+ return [];
48
+ }
49
+
50
+ function readFindings(taskId) {
51
+ if (!taskId) return null;
52
+ const all = readAll();
53
+ // Latest record wins (iterate reverse).
54
+ for (let i = all.length - 1; i >= 0; i--) {
55
+ if (all[i] && all[i].taskId === taskId) return all[i];
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Upsert a record for `taskId`. Last-write-wins per AC3: the existing record
62
+ * (if any) is replaced by `record`.
63
+ */
64
+ function writeFindings(record) {
65
+ if (!record || !record.taskId) {
66
+ throw new Error('writeFindings requires { taskId, ... }');
67
+ }
68
+ const all = readAll();
69
+ const filtered = all.filter((r) => r && r.taskId !== record.taskId);
70
+ filtered.push(record);
71
+ writeJson(FINDINGS_FILE, { schemaVersion: SCHEMA_VERSION, records: filtered });
72
+ return record;
73
+ }
74
+
75
+ /**
76
+ * Poll for a terminal record (status !== 'in-progress') for `taskId` within
77
+ * `timeoutMs`. Returns either the completed record or a synthetic
78
+ * `unverified-review-timeout` record. Does NOT modify the findings file on
79
+ * timeout — that's the caller's call.
80
+ */
81
+ async function awaitFindings(taskId, timeoutMs = DEFAULT_TIMEOUT_MS) {
82
+ const deadline = Date.now() + Math.max(0, timeoutMs);
83
+ for (;;) {
84
+ const rec = readFindings(taskId);
85
+ if (rec && rec.status && rec.status !== 'in-progress') return rec;
86
+ if (Date.now() >= deadline) {
87
+ return {
88
+ taskId,
89
+ status: 'unverified-review-timeout',
90
+ startedAt: rec?.startedAt || null,
91
+ completedAt: null,
92
+ findings: [],
93
+ timeoutMs,
94
+ };
95
+ }
96
+ await sleep(POLL_INTERVAL_MS);
97
+ }
98
+ }
99
+
100
+ function sleep(ms) {
101
+ return new Promise((r) => setTimeout(r, ms));
102
+ }
103
+
104
+ // ============================================================
105
+ // Heuristic default reviewer (no Claude dependency)
106
+ // ============================================================
107
+
108
+ const FINDING_PATTERNS = [
109
+ { re: /\bTODO\b/, severity: 'low', claim: 'TODO left in changed code' },
110
+ { re: /\bFIXME\b/, severity: 'medium', claim: 'FIXME left in changed code' },
111
+ { re: /\bXXX\b/, severity: 'medium', claim: 'XXX marker left in changed code' },
112
+ { re: /^<<<<<<<|^=======$|^>>>>>>>/, severity: 'high', claim: 'Unresolved merge conflict marker' },
113
+ { re: /console\.log\(/, severity: 'low', claim: 'console.log left in code' },
114
+ { re: /debugger;/, severity: 'high', claim: 'debugger statement left in code' },
115
+ ];
116
+
117
+ /**
118
+ * Default reviewer — runs in the worktree and reports findings on changed
119
+ * lines only (git diff scope). Pure static scan; no AI. Returns findings[].
120
+ *
121
+ * This is intentionally cheap so the feature is useful without per-task
122
+ * Claude model selection (AC6). When that Claude Code feature lands, a
123
+ * richer reviewer can be plugged in via the `reviewer` option of runReview.
124
+ */
125
+ function heuristicReviewer({ worktreePath, baseBranch }) {
126
+ const findings = [];
127
+ let diff = '';
128
+ try {
129
+ diff = execFileSync('git', ['diff', `${baseBranch}...HEAD`, '--unified=0'], {
130
+ cwd: worktreePath,
131
+ encoding: 'utf-8',
132
+ maxBuffer: 16 * 1024 * 1024,
133
+ });
134
+ } catch (_err) {
135
+ return findings;
136
+ }
137
+ if (!diff) return findings;
138
+
139
+ let currentFile = null;
140
+ let currentLine = 0;
141
+ for (const raw of diff.split('\n')) {
142
+ if (raw.startsWith('+++ b/')) {
143
+ currentFile = raw.slice(6);
144
+ currentLine = 0;
145
+ continue;
146
+ }
147
+ if (raw.startsWith('@@')) {
148
+ // @@ -a,b +c,d @@
149
+ const m = raw.match(/\+(\d+)/);
150
+ currentLine = m ? parseInt(m[1], 10) : 0;
151
+ continue;
152
+ }
153
+ if (!raw.startsWith('+') || raw.startsWith('+++')) continue;
154
+ const added = raw.slice(1);
155
+ for (const pat of FINDING_PATTERNS) {
156
+ if (pat.re.test(added)) {
157
+ findings.push({
158
+ severity: pat.severity,
159
+ file: currentFile,
160
+ line: currentLine,
161
+ claim: pat.claim,
162
+ evidence: added.trim().slice(0, 200),
163
+ });
164
+ }
165
+ }
166
+ currentLine += 1;
167
+ }
168
+ return findings;
169
+ }
170
+
171
+ // ============================================================
172
+ // Run (in-process) — the detached child calls this
173
+ // ============================================================
174
+
175
+ /**
176
+ * Run a review for `taskId` against `worktreePath`. Updates the findings file
177
+ * with `in-progress` → `complete|error` status transitions. Returns the final
178
+ * record.
179
+ *
180
+ * @param {Object} opts
181
+ * @param {string} opts.taskId
182
+ * @param {string} opts.worktreePath
183
+ * @param {string} [opts.baseBranch='HEAD~1']
184
+ * @param {Function} [opts.reviewer] — (ctx) => findings[] | Promise<findings[]>.
185
+ * Injectable for tests. Default = heuristicReviewer.
186
+ */
187
+ async function runReview({ taskId, worktreePath, baseBranch = 'HEAD~1', reviewer = heuristicReviewer }) {
188
+ if (!taskId) throw new Error('runReview requires taskId');
189
+ const startedAt = new Date().toISOString();
190
+ writeFindings({
191
+ taskId,
192
+ status: 'in-progress',
193
+ startedAt,
194
+ completedAt: null,
195
+ findings: [],
196
+ });
197
+
198
+ try {
199
+ const findings = await Promise.resolve(reviewer({ taskId, worktreePath, baseBranch }));
200
+ const record = {
201
+ taskId,
202
+ status: 'complete',
203
+ startedAt,
204
+ completedAt: new Date().toISOString(),
205
+ findings: Array.isArray(findings) ? findings : [],
206
+ };
207
+ writeFindings(record);
208
+ return record;
209
+ } catch (err) {
210
+ const record = {
211
+ taskId,
212
+ status: 'error',
213
+ startedAt,
214
+ completedAt: new Date().toISOString(),
215
+ findings: [],
216
+ error: String(err.message || err),
217
+ };
218
+ writeFindings(record);
219
+ return record;
220
+ }
221
+ }
222
+
223
+ // ============================================================
224
+ // startReview — the public entry (spawn detached child)
225
+ // ============================================================
226
+
227
+ /**
228
+ * Fire-and-forget background review. Creates a worktree, spawns a detached
229
+ * child that runs `runReview`, and returns a handle the caller can poll via
230
+ * `awaitFindings(taskId, timeoutMs)`. The child is unref()d so it never
231
+ * keeps the parent process alive.
232
+ *
233
+ * Non-blocking per AC5: the parent writes an `in-progress` marker and
234
+ * returns immediately. If the child never completes within `timeoutMs`,
235
+ * `awaitFindings` returns an `unverified-review-timeout` record.
236
+ *
237
+ * @param {Object} opts
238
+ * @param {string} opts.taskId
239
+ * @param {string} [opts.repoRoot] — default = process.cwd()
240
+ * @param {string} [opts.childScript] — override the worker entry (tests)
241
+ * @returns {{ taskId, pid, worktreePath }}
242
+ */
243
+ function startReview({ taskId, repoRoot, childScript }) {
244
+ if (!taskId) throw new Error('startReview requires taskId');
245
+ const root = repoRoot || process.cwd();
246
+
247
+ // Write the in-progress marker synchronously so consumers see it immediately.
248
+ writeFindings({
249
+ taskId,
250
+ status: 'in-progress',
251
+ startedAt: new Date().toISOString(),
252
+ completedAt: null,
253
+ findings: [],
254
+ });
255
+
256
+ // Worktree creation happens inside the child process (the worker script
257
+ // is responsible for createWorktree / discardWorktree — keeping this
258
+ // parent call synchronous and fire-and-forget).
259
+ const worktreePath = null;
260
+
261
+ const entry = childScript || path.join(__dirname, '..', 'scripts', 'flow-auto-review-worker.js');
262
+ const child = spawn(process.execPath, [entry, '--task', taskId, '--repoRoot', root], {
263
+ cwd: root,
264
+ detached: true,
265
+ stdio: 'ignore',
266
+ env: { ...process.env, WOGI_AUTO_REVIEW_CHILD: '1' },
267
+ });
268
+ child.unref();
269
+
270
+ return { taskId, pid: child.pid, worktreePath };
271
+ }
272
+
273
+ // ============================================================
274
+ // Findings → audit incorporation (consumed by completion-truth-gate)
275
+ // ============================================================
276
+
277
+ /**
278
+ * Summarize findings for a task into a shape the truth gate can fold into its
279
+ * existing `audit` object. High-severity findings trigger a soft-warn or block
280
+ * (depending on configuration) without the gate having to re-implement any
281
+ * downgrade logic — it just treats this like another "insufficient" signal.
282
+ */
283
+ function summarizeFindingsForAudit(taskId) {
284
+ const rec = readFindings(taskId);
285
+ if (!rec) return { present: false };
286
+
287
+ const findings = Array.isArray(rec.findings) ? rec.findings : [];
288
+ const counts = { low: 0, medium: 0, high: 0 };
289
+ for (const f of findings) {
290
+ const sev = (f && f.severity) || 'low';
291
+ if (counts[sev] !== undefined) counts[sev] += 1;
292
+ }
293
+
294
+ return {
295
+ present: true,
296
+ status: rec.status || 'unknown',
297
+ timedOut: rec.status === 'unverified-review-timeout',
298
+ counts,
299
+ highSeverityCount: counts.high,
300
+ topHighSeverity: findings.filter((f) => f && f.severity === 'high').slice(0, 5),
301
+ };
302
+ }
303
+
304
+ module.exports = {
305
+ FINDINGS_FILE,
306
+ DEFAULT_TIMEOUT_MS,
307
+ readFindings,
308
+ writeFindings,
309
+ awaitFindings,
310
+ runReview,
311
+ startReview,
312
+ heuristicReviewer,
313
+ summarizeFindingsForAudit,
314
+ _internal: { readAll },
315
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.26.2",
3
+ "version": "2.29.0",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "flow": "./scripts/flow",
13
- "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
13
+ "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
14
14
  "test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
15
15
  "lint": "eslint scripts/ lib/ tests/",
16
16
  "lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
@@ -77,7 +77,7 @@ class BaseWorkflowStep {
77
77
  * @param {object} options - Remaining options (stepConfig, mode, taskType, etc.)
78
78
  * @returns {Promise<{passed: boolean, message: string, details?: any}>}
79
79
  */
80
- async execute(_files, options) {
80
+ async execute(_files, _options) {
81
81
  throw new Error(`${this.name}: execute() must be overridden`);
82
82
  }
83
83
 
package/scripts/flow CHANGED
@@ -541,6 +541,12 @@ case "${1:-}" in
541
541
  memory)
542
542
  node "$SCRIPT_DIR/flow-memory.js" "${@:2}"
543
543
  ;;
544
+ feature-dossier|dossier)
545
+ node "$SCRIPT_DIR/flow-feature-dossier.js" "${@:2}"
546
+ ;;
547
+ logic-rules)
548
+ node "$SCRIPT_DIR/flow-logic-rules.js" "${@:2}"
549
+ ;;
544
550
  skill-create)
545
551
  node "$SCRIPT_DIR/flow-skill-create.js" "${@:2}"
546
552
  ;;
@@ -670,8 +676,13 @@ case "${1:-}" in
670
676
  node -e "require('$SCRIPT_DIR/../lib/skill-registry').skill(['add', '$3', ...process.argv.slice(2)])" -- "${@:4}"
671
677
  ;;
672
678
  remove)
673
- # Remove installed skill
674
- node -e "require('$SCRIPT_DIR/../lib/skill-registry').skill(['remove', '$3'])"
679
+ # If --name is present, this is a staged-removal proposal (flow-skill-manage).
680
+ # Otherwise, legacy registry removal.
681
+ if [[ " $* " == *" --name "* ]]; then
682
+ node "$SCRIPT_DIR/flow-skill-manage.js" remove "${@:3}"
683
+ else
684
+ node -e "require('$SCRIPT_DIR/../lib/skill-registry').skill(['remove', '$3'])"
685
+ fi
675
686
  ;;
676
687
  update)
677
688
  # Update skills
@@ -685,8 +696,12 @@ case "${1:-}" in
685
696
  # List skills from registry
686
697
  node -e "require('$SCRIPT_DIR/../lib/skill-registry').skill(['list'])"
687
698
  ;;
699
+ propose|patch|promote|reject|archive|pending)
700
+ # Agent proposal CLI (staged, user-approved at session-end)
701
+ node "$SCRIPT_DIR/flow-skill-manage.js" "${@:2}"
702
+ ;;
688
703
  *)
689
- echo "Usage: flow skill [detect|list|create|add|remove|update|info|registry]"
704
+ echo "Usage: flow skill [detect|list|create|add|remove|update|info|registry|propose|patch|promote|reject|archive|pending]"
690
705
  echo ""
691
706
  echo "Local Skills:"
692
707
  echo " detect Detect frameworks in project"
@@ -696,9 +711,18 @@ case "${1:-}" in
696
711
  echo "Registry Skills:"
697
712
  echo " registry List available skills from registry"
698
713
  echo " add <name> Install skill from registry"
699
- echo " remove <name> Remove installed skill"
714
+ echo " remove <name> Remove installed skill (or stage proposal with --name)"
700
715
  echo " update [name] Update skill(s)"
701
716
  echo " info <name> Show skill details"
717
+ echo ""
718
+ echo "Agent Proposals (staged, user-approved at session-end):"
719
+ echo " propose --name <n> --content <f> Stage new skill"
720
+ echo " patch --name <n> --content <f> Stage edit to existing skill"
721
+ echo " remove --name <n> Stage removal of existing skill"
722
+ echo " promote <name> Apply pending proposal"
723
+ echo " reject <name> Discard pending proposal"
724
+ echo " archive <name> Direct archive (no staging)"
725
+ echo " pending [--json] List pending proposals"
702
726
  ;;
703
727
  esac
704
728
  ;;