wogiflow 2.30.0 → 2.30.2
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/.workflow/state/forbidden-patterns.json.template +15 -0
- package/lib/installer.js +30 -0
- package/package.json +2 -2
- package/scripts/flow-architect-pass.js +8 -5
- package/scripts/flow-architect-runs.js +194 -0
- package/scripts/flow-feature-dossier.js +3 -9
- package/scripts/flow-glob.js +104 -0
- package/scripts/flow-logic-rules.js +3 -9
- package/scripts/flow-phase.js +9 -4
- package/scripts/flow-standards-checker.js +87 -45
- package/scripts/flow-standards-gate.js +15 -0
- package/scripts/hooks/adapters/claude-code.js +24 -9
- package/scripts/hooks/core/architect-required-gate.js +55 -106
- package/scripts/hooks/core/gate-orchestrator.js +104 -0
- package/scripts/hooks/core/long-input-enforcement.js +49 -0
- package/scripts/hooks/core/pre-tool-helpers.js +38 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +6 -24
- package/scripts/hooks/core/research-required-classifier.js +5 -1
- package/scripts/hooks/core/session-end.js +12 -0
- package/scripts/hooks/core/task-gate.js +77 -1
- package/scripts/hooks/entry/claude-code/stop.js +24 -1
- package/scripts/postinstall.js +25 -0
|
@@ -7,6 +7,45 @@
|
|
|
7
7
|
* Checks if there's an active task before allowing implementation actions.
|
|
8
8
|
*
|
|
9
9
|
* Returns a standardized result that adapters transform for specific CLIs.
|
|
10
|
+
*
|
|
11
|
+
* --------------------------------------------------------------------------
|
|
12
|
+
* "Active task" state-source contract (wf-c573961f, 2026-05-11)
|
|
13
|
+
* --------------------------------------------------------------------------
|
|
14
|
+
*
|
|
15
|
+
* The task-gate consults FOUR state sources to determine if a task is active.
|
|
16
|
+
* External tooling that mutates task state MUST satisfy this contract or the
|
|
17
|
+
* gate will reject Edits/Writes with `no_active_task` or `task_missing_routing_proof`.
|
|
18
|
+
*
|
|
19
|
+
* 1. `.workflow/state/ready.json` → `inProgress[]`
|
|
20
|
+
* The task object must be the first element of inProgress, with a valid
|
|
21
|
+
* `wf-XXXXXXXX` id (validateTaskId).
|
|
22
|
+
*
|
|
23
|
+
* 2. `.workflow/state/durable-session.json` → `{ taskId, status: 'active' }`
|
|
24
|
+
* Fallback path when inProgress is empty. Also requires routing proof.
|
|
25
|
+
*
|
|
26
|
+
* 3. Task object `routedAt` field (ISO timestamp)
|
|
27
|
+
* Set automatically by `flow start` / `createQuickTask` / `moveTaskAsync`
|
|
28
|
+
* when a task legitimately enters inProgress.
|
|
29
|
+
*
|
|
30
|
+
* 4. `.workflow/state/.routing-receipt-<taskId>` (dotfile, JSON)
|
|
31
|
+
* Anti-bypass receipt written by the same callers that set `routedAt`.
|
|
32
|
+
* The dual mechanism is defense-in-depth: AI editing ready.json directly
|
|
33
|
+
* can spoof `routedAt`, but the receipt file is harder to forge silently.
|
|
34
|
+
*
|
|
35
|
+
* The gate accepts the task if EITHER (3) routedAt is set, OR (4) the
|
|
36
|
+
* receipt file exists, OR the task has a legacy `startedAt` field (pre-
|
|
37
|
+
* routedAt migration). One of these MUST be true; otherwise the task is
|
|
38
|
+
* rejected as "manually inserted".
|
|
39
|
+
*
|
|
40
|
+
* Documented helpers for external tooling:
|
|
41
|
+
* - `writeRoutingReceipt(taskId, options)` — produces a valid receipt for
|
|
42
|
+
* a task that's already in inProgress (e.g., after `flow bug` then a
|
|
43
|
+
* manual move). This is the documented "I know what I'm doing" escape
|
|
44
|
+
* hatch that closes the 2026-05-10 wogiflow-cli bug-report issue.
|
|
45
|
+
*
|
|
46
|
+
* Single source of truth in the future: see the deferred follow-up epic
|
|
47
|
+
* referenced in wf-c573961f's bug spec ("Approach 1 / consolidate to
|
|
48
|
+
* .workflow/state/active-task.json"). That work is out of L2 scope.
|
|
10
49
|
*/
|
|
11
50
|
|
|
12
51
|
const fs = require('node:fs');
|
|
@@ -314,7 +353,7 @@ function checkTaskGate(options = {}, config) {
|
|
|
314
353
|
return {
|
|
315
354
|
allowed: false,
|
|
316
355
|
blocked: true,
|
|
317
|
-
message: `Task ${taskId} is in inProgress but has no routing proof (missing routedAt and no routing receipt file).\n\nThis usually means the task was inserted into ready.json manually instead of through /wogi-start.\n\nTo fix:\n1. Use /wogi-start ${taskId} to properly route this task
|
|
356
|
+
message: `Task ${taskId} is in inProgress but has no routing proof (missing routedAt and no routing receipt file).\n\nThis usually means the task was inserted into ready.json manually instead of through /wogi-start.\n\nTo fix:\n1. RECOMMENDED — Use /wogi-start ${taskId} to properly route this task.\n2. Or remove it from inProgress and start fresh: /wogi-ready\n3. Or, if you know what you're doing (e.g. after \`flow bug\` + manual move), write a routing receipt:\n node -e "require('./scripts/hooks/core/task-gate').writeRoutingReceipt('${taskId}', { via: 'manual' })"\n\nSee scripts/hooks/core/task-gate.js header for the "active task" state-source contract.`,
|
|
318
357
|
reason: 'task_missing_routing_proof'
|
|
319
358
|
};
|
|
320
359
|
}
|
|
@@ -410,6 +449,42 @@ function checkTaskGate(options = {}, config) {
|
|
|
410
449
|
};
|
|
411
450
|
}
|
|
412
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Write a routing receipt for a task. This is the documented external API
|
|
454
|
+
* for satisfying the routing-proof requirement (state source #4) when the
|
|
455
|
+
* task is in inProgress but was placed there by a caller that didn't write
|
|
456
|
+
* a receipt (e.g., manual edit, or `flow bug` then move).
|
|
457
|
+
*
|
|
458
|
+
* Returns `{ ok: true, path }` on success or `{ ok: false, reason }` on
|
|
459
|
+
* failure. Validates taskId format before writing.
|
|
460
|
+
*
|
|
461
|
+
* wf-c573961f: previously this functionality was inlined in createQuickTask
|
|
462
|
+
* and not exported, leaving external tooling no documented way to satisfy
|
|
463
|
+
* the gate. The bug-report author hit exactly this — they updated four
|
|
464
|
+
* state files but couldn't discover the receipt-file requirement.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} taskId - wf-XXXXXXXX format
|
|
467
|
+
* @param {Object} [options]
|
|
468
|
+
* @param {string} [options.via='external'] - audit field stored in the receipt
|
|
469
|
+
* @returns {{ok: boolean, path?: string, reason?: string}}
|
|
470
|
+
*/
|
|
471
|
+
function writeRoutingReceipt(taskId, options = {}) {
|
|
472
|
+
if (typeof taskId !== 'string' || !validateTaskId(taskId).valid) {
|
|
473
|
+
return { ok: false, reason: `invalid taskId format: ${taskId}` };
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
const receiptPath = path.join(PATHS.state, `.routing-receipt-${taskId}`);
|
|
477
|
+
fs.writeFileSync(receiptPath, JSON.stringify({
|
|
478
|
+
taskId,
|
|
479
|
+
routedAt: new Date().toISOString(),
|
|
480
|
+
via: options.via || 'external'
|
|
481
|
+
}, null, 2));
|
|
482
|
+
return { ok: true, path: receiptPath };
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return { ok: false, reason: err.message };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
413
488
|
/**
|
|
414
489
|
* Generate warning message (when not blocking)
|
|
415
490
|
*/
|
|
@@ -523,6 +598,7 @@ module.exports = {
|
|
|
523
598
|
getActiveTask,
|
|
524
599
|
checkTaskGate,
|
|
525
600
|
createQuickTask,
|
|
601
|
+
writeRoutingReceipt,
|
|
526
602
|
generateBlockMessage,
|
|
527
603
|
generateWarningMessage
|
|
528
604
|
};
|
|
@@ -17,12 +17,26 @@ const { isRoutingPending, incrementStopAttempts } = require('../../core/routing-
|
|
|
17
17
|
const { runHook } = require('../shared/hook-runner');
|
|
18
18
|
|
|
19
19
|
runHook('Stop', async ({ parsedInput }) => {
|
|
20
|
+
// wf-35742353 — Gate priority: if long-input-pending is active, it is the
|
|
21
|
+
// top-priority remediation. The UserPromptSubmit hook already surfaced the
|
|
22
|
+
// full long-input message on prompt arrival; firing routing-enforcement
|
|
23
|
+
// and research-required gates now would issue conflicting "do this NOW"
|
|
24
|
+
// instructions in the same turn. Defer the lower-priority Stop-hook gates
|
|
25
|
+
// until long-input-pending is resolved.
|
|
26
|
+
//
|
|
27
|
+
// Fail-open: any error reading the marker falls through to normal gate flow.
|
|
28
|
+
let longInputActive = false;
|
|
29
|
+
try {
|
|
30
|
+
const { isLongInputPending } = require('../../core/long-input-enforcement');
|
|
31
|
+
longInputActive = isLongInputPending();
|
|
32
|
+
} catch (_err) { /* fail-open */ }
|
|
33
|
+
|
|
20
34
|
// v6.2: Routing enforcement check — catches text-only response bypass
|
|
21
35
|
// If routing-pending flag is still set when the AI tries to stop, it means
|
|
22
36
|
// the AI responded to the user's message without ever invoking a /wogi-* command.
|
|
23
37
|
// This is the exact bypass we need to prevent (especially after context compaction).
|
|
24
38
|
try {
|
|
25
|
-
if (isRoutingPending()) {
|
|
39
|
+
if (isRoutingPending() && !longInputActive) {
|
|
26
40
|
// Use counter-based approach instead of clearing immediately.
|
|
27
41
|
// This gives the AI multiple chances to comply before giving up.
|
|
28
42
|
// Gap 4 fix: clearing immediately made this single-shot protection.
|
|
@@ -260,7 +274,15 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
260
274
|
// turn was classified as diagnostic (Tier 2/3 from CLAUDE.md), check that
|
|
261
275
|
// the AI made enough Read calls against evidence paths before answering.
|
|
262
276
|
// If not, re-prompt with a violation message forcing a redo. Fail-open.
|
|
277
|
+
//
|
|
278
|
+
// wf-35742353 — Skip this gate when long-input-pending is active. The user's
|
|
279
|
+
// prompt isn't yet captured, so demanding evidence-reading would issue a
|
|
280
|
+
// conflicting remediation. The diagnostic marker will still be present when
|
|
281
|
+
// long-input resolves; the gate fires correctly then.
|
|
263
282
|
try {
|
|
283
|
+
if (longInputActive) {
|
|
284
|
+
// skip — defer to long-input remediation
|
|
285
|
+
} else {
|
|
264
286
|
const { checkResearchRequiredGate } = require('../../core/research-required-gate');
|
|
265
287
|
const { getConfig } = require('../../../flow-utils');
|
|
266
288
|
const config = getConfig();
|
|
@@ -276,6 +298,7 @@ runHook('Stop', async ({ parsedInput }) => {
|
|
|
276
298
|
// Soft re-prompt: force the AI to redo with reads
|
|
277
299
|
return { __raw: true, continue: true, stopReason: result.message };
|
|
278
300
|
}
|
|
301
|
+
} // end else (wf-35742353 long-input-active skip)
|
|
279
302
|
} catch (err) {
|
|
280
303
|
if (process.env.DEBUG) {
|
|
281
304
|
console.error(`[Stop] Research-required gate error (fail-open): ${err.message}`);
|
package/scripts/postinstall.js
CHANGED
|
@@ -148,6 +148,31 @@ function createMinimalStructure() {
|
|
|
148
148
|
}, null, 2), { mode: FILE_MODE });
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
// wf-2eafdab0 (AC2/H2): Create forbidden-patterns.json from template on fresh
|
|
153
|
+
// installs. Idempotent — never overwrites an existing file. Without this, the
|
|
154
|
+
// declarative-forbidden-pattern feature ships as a silent no-op (the v2.30.0
|
|
155
|
+
// bug this commit fixes).
|
|
156
|
+
const forbiddenPatternsPath = path.join(STATE_DIR, 'forbidden-patterns.json');
|
|
157
|
+
if (!fs.existsSync(forbiddenPatternsPath)) {
|
|
158
|
+
const fpTemplate = path.join(STATE_DIR, 'forbidden-patterns.json.template');
|
|
159
|
+
try {
|
|
160
|
+
const rawContent = fs.existsSync(fpTemplate)
|
|
161
|
+
? fs.readFileSync(fpTemplate, 'utf-8')
|
|
162
|
+
: '[]';
|
|
163
|
+
const parsed = safeJsonParseString(rawContent, []);
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
forbiddenPatternsPath,
|
|
166
|
+
JSON.stringify(parsed, null, 2),
|
|
167
|
+
{ mode: FILE_MODE }
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (process.env.DEBUG) {
|
|
171
|
+
console.error(`[postinstall] forbidden-patterns template error: ${err.message}`);
|
|
172
|
+
}
|
|
173
|
+
fs.writeFileSync(forbiddenPatternsPath, '[]\n', { mode: FILE_MODE });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
/**
|