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.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +149 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-config-loader.js +3 -2
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-correction-detector.js +3 -2
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-prompt-composer.js +3 -2
- package/scripts/flow-prompt-template.js +3 -2
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-mcp-strip.js +122 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- 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.
|
|
3
|
+
"version": "2.29.1",
|
|
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 tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js && 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,
|
|
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
|
-
#
|
|
674
|
-
|
|
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
|
;;
|