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
|
@@ -169,6 +169,21 @@ function inferTaskType(taskType, changedFiles) {
|
|
|
169
169
|
* @returns {Object} Check results with feedback for retry loop
|
|
170
170
|
*/
|
|
171
171
|
function runTaskStandardsCheck(taskContext, files, options = {}) {
|
|
172
|
+
// Defensive normalization: callers (flow-done-gates.js → ctx.getModifiedFiles())
|
|
173
|
+
// pass a string[] of file paths; this function documents Object[] with
|
|
174
|
+
// {path, content}. Pre-fix, `files.map(f => f.path)` returned [undefined,...]
|
|
175
|
+
// which then crashed downstream `.includes()` on undefined. The crash was
|
|
176
|
+
// swallowed by the caller's try/catch and surfaced as "checker error —
|
|
177
|
+
// degraded to manual check" on every `flow done`. Normalize here so both
|
|
178
|
+
// shapes work; content-dependent checks skip strings gracefully (downstream
|
|
179
|
+
// already does `if (!file.content) continue;`).
|
|
180
|
+
if (!Array.isArray(files)) {
|
|
181
|
+
files = [];
|
|
182
|
+
} else {
|
|
183
|
+
files = files.map(f => (typeof f === 'string' ? { path: f, content: undefined } : f))
|
|
184
|
+
.filter(f => f && typeof f.path === 'string');
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
const config = getConfig();
|
|
173
188
|
const standardsConfig = config.standardsCompliance || {};
|
|
174
189
|
const gateStartMs = Date.now();
|
|
@@ -421,16 +421,31 @@ Run: /wogi-start ${coreResult.nextTaskId}`;
|
|
|
421
421
|
};
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
424
|
+
// wf-35742353 — Gate-orchestrator integration.
|
|
425
|
+
//
|
|
426
|
+
// Previously this concatenated up to five pieces blindly, producing a
|
|
427
|
+
// wall of competing system-reminder text ("invoke /wogi-extract-review"
|
|
428
|
+
// + "research protocol" + phase context, etc.) that the AI/user could
|
|
429
|
+
// not triage. Now: REMEDIATIONS (gates demanding action) go through
|
|
430
|
+
// the gate-orchestrator which picks the highest-priority one and lists
|
|
431
|
+
// the rest as a one-line "queued" footer. INFO pieces (phase context,
|
|
432
|
+
// overdue dispatches) pass through unchanged alongside the top
|
|
433
|
+
// remediation. Fail-open: any error falls back to the prior concat.
|
|
430
434
|
const pieces = [];
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
try {
|
|
436
|
+
const { selectAndRender } = require('../core/gate-orchestrator');
|
|
437
|
+
const remediation = selectAndRender({
|
|
438
|
+
'long-input-pending': coreResult.longInputEnforcement,
|
|
439
|
+
'research-required': coreResult.systemReminder || coreResult.message
|
|
440
|
+
});
|
|
441
|
+
if (remediation) pieces.push(remediation);
|
|
442
|
+
} catch (_err) {
|
|
443
|
+
// Fallback to prior concat shape if orchestrator misfires.
|
|
444
|
+
if (coreResult.longInputEnforcement) pieces.push(coreResult.longInputEnforcement);
|
|
445
|
+
if (coreResult.systemReminder) pieces.push(coreResult.systemReminder);
|
|
446
|
+
else if (coreResult.message) pieces.push(coreResult.message);
|
|
447
|
+
}
|
|
448
|
+
// Info pieces always pass through.
|
|
434
449
|
if (coreResult.phasePrompt) pieces.push(coreResult.phasePrompt);
|
|
435
450
|
if (coreResult.overduePrompt) pieces.push(coreResult.overduePrompt);
|
|
436
451
|
|
|
@@ -1,97 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Wogi Flow - Architect-Required Gate (wf-037f8d66)
|
|
4
|
+
* Wogi Flow - Architect-Required Gate (wf-037f8d66, hardened in wf-2eafdab0)
|
|
5
5
|
*
|
|
6
6
|
* Closes the methodology gap where the IGR Architect/Adversary pass at
|
|
7
7
|
* spec_review IS specced (per .claude/docs/phases/02-spec.md) but enforcement
|
|
8
8
|
* is prompt-only — the agent can skip Architect and go straight to coding.
|
|
9
9
|
*
|
|
10
|
-
* This gate fires on Edit/Write during the `coding` phase for L1+ tasks
|
|
11
|
-
* config.intentGroundedReasoning.enabled is true and no evidence of an
|
|
10
|
+
* This gate fires on Edit/Write/Bash during the `coding` phase for L1+ tasks
|
|
11
|
+
* when config.intentGroundedReasoning.enabled is true and no evidence of an
|
|
12
12
|
* Architect run exists for the current task.
|
|
13
13
|
*
|
|
14
|
-
* Evidence marker:
|
|
15
|
-
*
|
|
14
|
+
* Evidence marker: read from flow-architect-runs.js (the neutral-location
|
|
15
|
+
* module that BOTH this gate and flow-architect-pass.js consume — keeps
|
|
16
|
+
* hooks/core from owning state hooks/core's parents need to write to).
|
|
16
17
|
*
|
|
17
18
|
* Scope:
|
|
18
19
|
* - L0 (epic) / L1 (story) tasks: Architect required → gate enforces
|
|
19
20
|
* - L2 / L3 tasks: skip spec_review entirely (correctly) → gate is a no-op
|
|
21
|
+
* - type=story/epic with MISSING level: fail-closed (treat as L1)
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const path = require('node:path');
|
|
26
|
-
const fs = require('node:fs');
|
|
27
|
-
const { PATHS } = require('../../flow-utils');
|
|
28
|
-
|
|
29
|
-
const ARCHITECT_RUNS_DIR = path.join(PATHS.state, 'architect-runs');
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Compute path to the Architect-run evidence marker for a task.
|
|
33
|
-
*/
|
|
34
|
-
function getArchitectRunPath(taskId) {
|
|
35
|
-
if (!taskId || typeof taskId !== 'string') return null;
|
|
36
|
-
return path.join(ARCHITECT_RUNS_DIR, `${taskId}.json`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Write an Architect-run evidence marker. Called by flow-architect-pass.js
|
|
41
|
-
* on successful completion. Atomic write-temp + rename.
|
|
23
|
+
* Mutation set: Edit, Write, Bash (NOT TodoWrite — review finding M8: blocking
|
|
24
|
+
* planning before coding is chicken-and-egg).
|
|
42
25
|
*
|
|
43
|
-
*
|
|
44
|
-
* @returns {{ written: boolean, path: string|null }}
|
|
26
|
+
* Fail-open: any error reading state/config → allow tool call.
|
|
45
27
|
*/
|
|
46
|
-
function writeArchitectRunMarker(payload) {
|
|
47
|
-
if (!payload || !payload.taskId) {
|
|
48
|
-
return { written: false, path: null };
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
if (!fs.existsSync(ARCHITECT_RUNS_DIR)) {
|
|
52
|
-
fs.mkdirSync(ARCHITECT_RUNS_DIR, { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
const filePath = getArchitectRunPath(payload.taskId);
|
|
55
|
-
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
56
|
-
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
57
|
-
taskId: payload.taskId,
|
|
58
|
-
completedAt: payload.completedAt || new Date().toISOString(),
|
|
59
|
-
model: payload.model || null,
|
|
60
|
-
plan: payload.plan || null
|
|
61
|
-
}, null, 2));
|
|
62
|
-
fs.renameSync(tmpPath, filePath);
|
|
63
|
-
return { written: true, path: filePath };
|
|
64
|
-
} catch (_err) {
|
|
65
|
-
return { written: false, path: null };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
28
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* @param {string} taskId
|
|
72
|
-
* @returns {boolean}
|
|
73
|
-
*/
|
|
74
|
-
function hasArchitectRun(taskId) {
|
|
75
|
-
const p = getArchitectRunPath(taskId);
|
|
76
|
-
if (!p) return false;
|
|
77
|
-
try {
|
|
78
|
-
return fs.existsSync(p);
|
|
79
|
-
} catch (_err) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
29
|
+
const archRuns = require('../../flow-architect-runs');
|
|
30
|
+
const { hasArchitectRun, getArchitectRunPath, writeArchitectRunMarker } = archRuns;
|
|
83
31
|
|
|
84
32
|
/**
|
|
85
|
-
* Determine whether the current task requires Architect
|
|
86
|
-
*
|
|
87
|
-
*
|
|
33
|
+
* Determine whether the current task requires Architect.
|
|
34
|
+
*
|
|
35
|
+
* Returns true for:
|
|
36
|
+
* - L0 (epic), L1 (story)
|
|
37
|
+
* - type=story OR type=epic with MISSING/empty level (fail-closed — review M2)
|
|
38
|
+
*
|
|
39
|
+
* Returns false for:
|
|
40
|
+
* - explicit L2 / L3 (regardless of type)
|
|
41
|
+
* - missing/null taskMeta
|
|
42
|
+
* - unknown type with no level signal
|
|
88
43
|
*/
|
|
89
44
|
function requiresArchitect(taskMeta) {
|
|
90
45
|
if (!taskMeta || typeof taskMeta !== 'object') return false;
|
|
91
46
|
const level = (taskMeta.level || '').toUpperCase();
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
47
|
+
// Explicit level takes precedence
|
|
48
|
+
if (level === 'L0' || level === 'L1') return true;
|
|
49
|
+
if (level === 'L2' || level === 'L3') return false;
|
|
50
|
+
// No explicit level — fail-closed for stories/epics (M2 fix).
|
|
51
|
+
// A task created without a level field is untracked-by-pipeline; treating
|
|
52
|
+
// it as "doesn't need architect" lets bypass slip through. Stories/epics
|
|
53
|
+
// are exactly the work-types Architect is for.
|
|
54
|
+
const type = (taskMeta.type || '').toLowerCase();
|
|
55
|
+
if (type === 'story' || type === 'epic') return true;
|
|
56
|
+
return false;
|
|
95
57
|
}
|
|
96
58
|
|
|
97
59
|
/**
|
|
@@ -101,7 +63,6 @@ function requiresArchitect(taskMeta) {
|
|
|
101
63
|
function isGateEnabled(config) {
|
|
102
64
|
const igr = config?.intentGroundedReasoning;
|
|
103
65
|
if (!igr || igr.enabled === false) return false;
|
|
104
|
-
// Explicit toggle on the gate itself overrides
|
|
105
66
|
if (config?.architectRequiredGate?.enabled === false) return false;
|
|
106
67
|
return true;
|
|
107
68
|
}
|
|
@@ -109,67 +70,55 @@ function isGateEnabled(config) {
|
|
|
109
70
|
/**
|
|
110
71
|
* Main gate check.
|
|
111
72
|
*
|
|
112
|
-
* @param {Object} ctx — { phase, taskId, taskMeta, config, toolName }
|
|
73
|
+
* @param {Object} ctx — { phase, taskId, taskMeta, config, toolName, specPath? }
|
|
113
74
|
* @returns {{ blocked: boolean, reason?: string, message?: string }}
|
|
114
75
|
*/
|
|
115
76
|
function checkArchitectRequired(ctx) {
|
|
116
|
-
const { phase, taskId, taskMeta, config, toolName } = ctx || {};
|
|
77
|
+
const { phase, taskId, taskMeta, config, toolName, specPath } = ctx || {};
|
|
117
78
|
|
|
118
|
-
//
|
|
119
|
-
const mutationTools = new Set(['Edit', 'Write', '
|
|
120
|
-
if (!toolName || !mutationTools.has(toolName)) {
|
|
121
|
-
return { blocked: false };
|
|
122
|
-
}
|
|
79
|
+
// Mutation set: Edit/Write/Bash only. TodoWrite removed (M8).
|
|
80
|
+
const mutationTools = new Set(['Edit', 'Write', 'Bash']);
|
|
81
|
+
if (!toolName || !mutationTools.has(toolName)) return { blocked: false };
|
|
123
82
|
|
|
124
83
|
// Only fires during coding phase
|
|
125
|
-
if (phase !== 'coding') {
|
|
126
|
-
return { blocked: false };
|
|
127
|
-
}
|
|
84
|
+
if (phase !== 'coding') return { blocked: false };
|
|
128
85
|
|
|
129
86
|
// Gate disabled (or IGR off)
|
|
130
|
-
if (!isGateEnabled(config)) {
|
|
131
|
-
return { blocked: false };
|
|
132
|
-
}
|
|
87
|
+
if (!isGateEnabled(config)) return { blocked: false };
|
|
133
88
|
|
|
134
|
-
// No active task → not in scope
|
|
135
|
-
if (!taskId) {
|
|
136
|
-
return { blocked: false };
|
|
137
|
-
}
|
|
89
|
+
// No active task → not in scope
|
|
90
|
+
if (!taskId) return { blocked: false };
|
|
138
91
|
|
|
139
|
-
// L2/L3 tasks bypass spec_review
|
|
140
|
-
if (!requiresArchitect(taskMeta)) {
|
|
141
|
-
return { blocked: false };
|
|
142
|
-
}
|
|
92
|
+
// L2/L3 tasks correctly bypass spec_review — gate is a no-op
|
|
93
|
+
if (!requiresArchitect(taskMeta)) return { blocked: false };
|
|
143
94
|
|
|
144
|
-
// Check
|
|
145
|
-
if (hasArchitectRun(taskId)) {
|
|
146
|
-
return { blocked: false };
|
|
147
|
-
}
|
|
95
|
+
// Check evidence marker (with content validation + optional specHash check)
|
|
96
|
+
if (hasArchitectRun(taskId, specPath)) return { blocked: false };
|
|
148
97
|
|
|
149
|
-
// Block: Architect required but no evidence
|
|
150
98
|
return {
|
|
151
99
|
blocked: true,
|
|
152
100
|
reason: 'architect-required',
|
|
153
101
|
message: [
|
|
154
102
|
`ARCHITECT-REQUIRED GATE: task ${taskId} is L1+ in coding phase but no `,
|
|
155
|
-
`Architect run is recorded at ${getArchitectRunPath(taskId)}.\n\n`,
|
|
103
|
+
`valid Architect run is recorded at ${getArchitectRunPath(taskId)}.\n\n`,
|
|
156
104
|
`Per .claude/docs/phases/02-spec.md Step 1.55, L1+ tasks must run an `,
|
|
157
105
|
`Architect pass before coding. Invoke:\n\n`,
|
|
158
106
|
` node scripts/flow-architect-pass.js run --task=${taskId}\n\n`,
|
|
159
107
|
`Then retry your edit. To opt out for this task only, set `,
|
|
160
|
-
`config.architectRequiredGate.enabled = false (project-level)
|
|
161
|
-
`\`flow architect-skip --task=${taskId} --reason="..."\` (single-task escape; `,
|
|
162
|
-
`not yet implemented — opens follow-up wf if needed).`
|
|
108
|
+
`config.architectRequiredGate.enabled = false (project-level).`
|
|
163
109
|
].join('')
|
|
164
110
|
};
|
|
165
111
|
}
|
|
166
112
|
|
|
167
113
|
module.exports = {
|
|
168
114
|
checkArchitectRequired,
|
|
169
|
-
|
|
170
|
-
|
|
115
|
+
// Re-exports from flow-architect-runs for backward compat with existing
|
|
116
|
+
// test suite + flow-architect-pass.js. These pass-throughs let consumers
|
|
117
|
+
// who already require the gate continue to work; new consumers should
|
|
118
|
+
// prefer require('../../flow-architect-runs') directly.
|
|
171
119
|
requiresArchitect,
|
|
172
|
-
getArchitectRunPath,
|
|
173
120
|
isGateEnabled,
|
|
174
|
-
|
|
121
|
+
hasArchitectRun,
|
|
122
|
+
getArchitectRunPath,
|
|
123
|
+
writeArchitectRunMarker
|
|
175
124
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Gate Orchestrator (wf-35742353)
|
|
5
|
+
*
|
|
6
|
+
* Cross-gate priority and remediation surfacing. Each gate (long-input-pending,
|
|
7
|
+
* routing, research-required, phase-context, overdue-dispatches, etc.) was
|
|
8
|
+
* designed assuming it was the only voice in the room. At the integration
|
|
9
|
+
* point (UserPromptSubmit additionalContext and Stop hook stopReason) they
|
|
10
|
+
* collide and produce conflicting "do this NOW" instructions in the same turn.
|
|
11
|
+
*
|
|
12
|
+
* This module owns the priority order and the picker. The hook entry files
|
|
13
|
+
* and adapters call it instead of stacking messages.
|
|
14
|
+
*
|
|
15
|
+
* Priority (highest first):
|
|
16
|
+
* 1. long-input-pending — user's prompt isn't captured; downstream is
|
|
17
|
+
* suspect. Resolve before anything else.
|
|
18
|
+
* 2. routing — no task assigned; work would be untracked.
|
|
19
|
+
* 3. research-required — diagnostic prompt needs evidence-reading.
|
|
20
|
+
* 4. workspace-overdue — silent worker death surfacing (manager-only).
|
|
21
|
+
* 5. phase-context — informational phase-prompt injection (not a demand).
|
|
22
|
+
*
|
|
23
|
+
* Categories:
|
|
24
|
+
* - "remediation": the AI must take a specific action (resolve before
|
|
25
|
+
* proceeding). At most ONE remediation is surfaced per turn — the
|
|
26
|
+
* highest-priority active one wins; others get a one-line "queued"
|
|
27
|
+
* footer so the AI knows more work follows.
|
|
28
|
+
* - "info": informational, always passes through alongside the top
|
|
29
|
+
* remediation. Examples: phase-context, dossier injection.
|
|
30
|
+
*
|
|
31
|
+
* Fail-open philosophy: if classification or formatting errors, return
|
|
32
|
+
* the original message stack unchanged (caller fallback).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const REMEDIATION_PRIORITY = Object.freeze([
|
|
36
|
+
'long-input-pending',
|
|
37
|
+
'routing',
|
|
38
|
+
'research-required',
|
|
39
|
+
'workspace-overdue'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const REMEDIATION_LABELS = Object.freeze({
|
|
43
|
+
'long-input-pending': 'long-input-pending (invoke /wogi-extract-review or `flow long-input-pending dismiss`)',
|
|
44
|
+
'routing': 'routing (invoke /wogi-start)',
|
|
45
|
+
'research-required': 'research-required (read evidence before answering)',
|
|
46
|
+
'workspace-overdue': 'workspace-overdue (a worker dispatch is past its deadline)'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pick the top-priority active remediation from a set of (gateId, message) pairs.
|
|
51
|
+
*
|
|
52
|
+
* @param {Array<{id: string, message: string}>} active - gates currently demanding action
|
|
53
|
+
* @returns {{ top: {id, message}|null, queued: string[] }}
|
|
54
|
+
* top: the highest-priority active gate (null if none)
|
|
55
|
+
* queued: gateIds of the others (in priority order), for footer rendering
|
|
56
|
+
*/
|
|
57
|
+
function pickTopRemediation(active) {
|
|
58
|
+
if (!Array.isArray(active) || active.length === 0) {
|
|
59
|
+
return { top: null, queued: [] };
|
|
60
|
+
}
|
|
61
|
+
// Filter to valid entries and sort by priority index.
|
|
62
|
+
const valid = active.filter(g => g && typeof g.id === 'string' && typeof g.message === 'string' && g.message.trim().length > 0);
|
|
63
|
+
if (valid.length === 0) return { top: null, queued: [] };
|
|
64
|
+
|
|
65
|
+
const indexed = valid.map(g => ({ ...g, idx: REMEDIATION_PRIORITY.indexOf(g.id) }))
|
|
66
|
+
.map(g => ({ ...g, idx: g.idx === -1 ? Number.POSITIVE_INFINITY : g.idx }));
|
|
67
|
+
indexed.sort((a, b) => a.idx - b.idx);
|
|
68
|
+
|
|
69
|
+
const top = { id: indexed[0].id, message: indexed[0].message };
|
|
70
|
+
const queued = indexed.slice(1).map(g => g.id);
|
|
71
|
+
return { top, queued };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render the top remediation message with a one-line footer listing queued
|
|
76
|
+
* remediations. If no others are queued, returns the top message unchanged.
|
|
77
|
+
*/
|
|
78
|
+
function renderRemediation(top, queued) {
|
|
79
|
+
if (!top || typeof top.message !== 'string') return '';
|
|
80
|
+
if (!Array.isArray(queued) || queued.length === 0) return top.message;
|
|
81
|
+
const labels = queued.map(id => REMEDIATION_LABELS[id] || id).join('; ');
|
|
82
|
+
return `${top.message}\n\n[gate-orchestrator] Also queued (resolve after the above): ${labels}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convenience: take a map of {gateId: message-or-null} and return the rendered
|
|
87
|
+
* top remediation (or empty string when nothing is active).
|
|
88
|
+
*/
|
|
89
|
+
function selectAndRender(gateMap) {
|
|
90
|
+
if (!gateMap || typeof gateMap !== 'object') return '';
|
|
91
|
+
const active = Object.entries(gateMap)
|
|
92
|
+
.filter(([_id, msg]) => typeof msg === 'string' && msg.trim().length > 0)
|
|
93
|
+
.map(([id, message]) => ({ id, message }));
|
|
94
|
+
const { top, queued } = pickTopRemediation(active);
|
|
95
|
+
return renderRemediation(top, queued);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
REMEDIATION_PRIORITY,
|
|
100
|
+
REMEDIATION_LABELS,
|
|
101
|
+
pickTopRemediation,
|
|
102
|
+
renderRemediation,
|
|
103
|
+
selectAndRender
|
|
104
|
+
};
|
|
@@ -94,6 +94,47 @@ function hasSourceLink(text) {
|
|
|
94
94
|
return SOURCE_LINK_PATTERNS.some(re => re.test(text));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Known system-content tag prefixes that arrive via UserPromptSubmit but are
|
|
98
|
+
// NOT user-typed input. Sub-agent task-notifications, system reminders, and
|
|
99
|
+
// slash-command framings all flow through the same hook. Treating them as
|
|
100
|
+
// user prompts is the wf-f7d58760 false-positive shape — sub-agent
|
|
101
|
+
// completions tripped the long-input gate and force-blocked the parent
|
|
102
|
+
// session with no recoverable path (catch-22 documented in the bug).
|
|
103
|
+
const SYSTEM_CONTENT_PREFIXES = [
|
|
104
|
+
'<task-notification>',
|
|
105
|
+
'<system-reminder>',
|
|
106
|
+
'<command-message>',
|
|
107
|
+
'<command-name>',
|
|
108
|
+
'<command-args>',
|
|
109
|
+
'<command-output>',
|
|
110
|
+
'<command-stderr>',
|
|
111
|
+
'<local-command-stdout>',
|
|
112
|
+
'<local-command-stderr>',
|
|
113
|
+
'<user-prompt-submit-hook>',
|
|
114
|
+
'<bash-input>',
|
|
115
|
+
'<bash-stdout>',
|
|
116
|
+
'<bash-stderr>'
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Detect content that originates from the system (tool results, sub-agent
|
|
121
|
+
* notifications, slash-command framings) rather than user typing. These
|
|
122
|
+
* arrive via UserPromptSubmit in some Claude Code paths but should never
|
|
123
|
+
* trip the long-input gate — they aren't requests, and the user can't
|
|
124
|
+
* "preserve their verbatim source" because the user didn't author them.
|
|
125
|
+
*
|
|
126
|
+
* Detection: leading non-whitespace begins with a known system tag prefix.
|
|
127
|
+
* Conservative — only matches if the tag is the FIRST thing in the text.
|
|
128
|
+
*/
|
|
129
|
+
function isSystemOriginatedContent(text) {
|
|
130
|
+
if (typeof text !== 'string') return false;
|
|
131
|
+
const lead = text.trimStart().slice(0, 64);
|
|
132
|
+
for (const prefix of SYSTEM_CONTENT_PREFIXES) {
|
|
133
|
+
if (lead.startsWith(prefix)) return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function hasTaskSignals(text) {
|
|
98
139
|
if (typeof text !== 'string') return false;
|
|
99
140
|
let imperativeHits = 0;
|
|
@@ -129,6 +170,12 @@ function isChannelDispatchInWorker(source, env = process.env) {
|
|
|
129
170
|
* @returns {{forced: boolean, level: 'strict'|'force'|'suggest'|'pass', reason: string}}
|
|
130
171
|
*/
|
|
131
172
|
function shouldForceExtractReview({ text, source, env = process.env } = {}) {
|
|
173
|
+
// wf-f7d58760: system-originated content (sub-agent task-notifications,
|
|
174
|
+
// tool-result echoes, slash-command framings) flows through the same
|
|
175
|
+
// UserPromptSubmit pipe but is NOT a user prompt. Skip the gate entirely.
|
|
176
|
+
if (isSystemOriginatedContent(text)) {
|
|
177
|
+
return { forced: false, level: 'pass', reason: 'system-originated-content' };
|
|
178
|
+
}
|
|
132
179
|
if (!detectLongFormPrompt(text)) {
|
|
133
180
|
return { forced: false, level: 'pass', reason: 'below-long-input-threshold' };
|
|
134
181
|
}
|
|
@@ -297,9 +344,11 @@ module.exports = {
|
|
|
297
344
|
PENDING_PATH,
|
|
298
345
|
LONG_LINE_THRESHOLD,
|
|
299
346
|
LONG_ITEM_THRESHOLD,
|
|
347
|
+
SYSTEM_CONTENT_PREFIXES,
|
|
300
348
|
detectLongFormPrompt,
|
|
301
349
|
hasSourceLink,
|
|
302
350
|
hasTaskSignals,
|
|
351
|
+
isSystemOriginatedContent,
|
|
303
352
|
isChannelDispatchInWorker,
|
|
304
353
|
shouldForceExtractReview,
|
|
305
354
|
buildEnforcementMessage,
|
|
@@ -64,9 +64,47 @@ function isAllGatesDisabled(hookStatus) {
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the current workflow phase + task meta from state files.
|
|
69
|
+
*
|
|
70
|
+
* Reads `.workflow/state/workflow-phase.json` for { phase, taskId }, then
|
|
71
|
+
* looks up the matching task in `ready.json.inProgress` for full taskMeta.
|
|
72
|
+
*
|
|
73
|
+
* Fail-open everywhere: any read/parse error → returns the partial state
|
|
74
|
+
* resolved so far. Multiple gates may need this context; centralizing here
|
|
75
|
+
* stops the inline-block proliferation flagged by review L3.
|
|
76
|
+
*
|
|
77
|
+
* @returns {{phase: string, taskId: string|null, taskMeta: object|null}}
|
|
78
|
+
*/
|
|
79
|
+
function resolveCurrentTaskContext() {
|
|
80
|
+
const path = require('node:path');
|
|
81
|
+
const flowUtils = require('../../flow-utils');
|
|
82
|
+
const flowIo = require('../../flow-io');
|
|
83
|
+
let phase = 'idle';
|
|
84
|
+
let taskId = null;
|
|
85
|
+
let taskMeta = null;
|
|
86
|
+
try {
|
|
87
|
+
const phaseStatePath = path.join(flowUtils.PATHS.state, 'workflow-phase.json');
|
|
88
|
+
const ps = flowIo.safeJsonParse(phaseStatePath, null);
|
|
89
|
+
if (ps) {
|
|
90
|
+
phase = ps.phase || 'idle';
|
|
91
|
+
taskId = ps.taskId || null;
|
|
92
|
+
}
|
|
93
|
+
} catch (_err) { /* fail-open */ }
|
|
94
|
+
if (taskId) {
|
|
95
|
+
try {
|
|
96
|
+
const ready = flowUtils.getReadyData ? flowUtils.getReadyData() : null;
|
|
97
|
+
const inProgress = (ready && Array.isArray(ready.inProgress)) ? ready.inProgress : [];
|
|
98
|
+
taskMeta = inProgress.find(t => t && t.id === taskId) || null;
|
|
99
|
+
} catch (_err) { /* fail-open */ }
|
|
100
|
+
}
|
|
101
|
+
return { phase, taskId, taskMeta };
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
module.exports = {
|
|
68
105
|
VALID_AGENT_TYPES,
|
|
69
106
|
READ_ONLY_AGENT_TYPES,
|
|
70
107
|
parseSubagentContext,
|
|
71
108
|
isAllGatesDisabled,
|
|
109
|
+
resolveCurrentTaskContext,
|
|
72
110
|
};
|
|
@@ -118,35 +118,17 @@ function runPreToolGates(ctx, deps) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// Architect-required gate (wf-037f8d66)
|
|
121
|
+
// Architect-required gate (wf-037f8d66, hardened wf-2eafdab0)
|
|
122
122
|
// L1+ tasks in coding phase must have run Architect/Adversary before any Edit/Write/Bash.
|
|
123
|
+
// TodoWrite removed from gate set (review M8 fix — planning-tool chicken-and-egg).
|
|
123
124
|
// L2/L3 tasks bypass spec_review entirely (correctly), so the gate is a no-op there.
|
|
125
|
+
// Task context resolution extracted to resolveCurrentTaskContext (review L3 fix).
|
|
124
126
|
// Fail-open on any error.
|
|
125
127
|
if (typeof deps.checkArchitectRequired === 'function' &&
|
|
126
|
-
(toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash'
|
|
128
|
+
(toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash')) {
|
|
127
129
|
try {
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const flowIo = require('../../flow-io');
|
|
131
|
-
let phase = 'idle';
|
|
132
|
-
let taskId = null;
|
|
133
|
-
let taskMeta = null;
|
|
134
|
-
try {
|
|
135
|
-
const phaseStatePath = path.join(flowUtils.PATHS.state, 'workflow-phase.json');
|
|
136
|
-
const ps = flowIo.safeJsonParse(phaseStatePath, null);
|
|
137
|
-
if (ps) {
|
|
138
|
-
phase = ps.phase || 'idle';
|
|
139
|
-
taskId = ps.taskId || null;
|
|
140
|
-
}
|
|
141
|
-
} catch (_err) { /* fail-open */ }
|
|
142
|
-
if (taskId) {
|
|
143
|
-
try {
|
|
144
|
-
const ready = flowUtils.getReadyData ? flowUtils.getReadyData() : null;
|
|
145
|
-
const inProgress = (ready && Array.isArray(ready.inProgress)) ? ready.inProgress : [];
|
|
146
|
-
taskMeta = inProgress.find(t => t && t.id === taskId) || null;
|
|
147
|
-
} catch (_err) { /* fail-open */ }
|
|
148
|
-
}
|
|
149
|
-
|
|
130
|
+
const { resolveCurrentTaskContext } = require('./pre-tool-helpers');
|
|
131
|
+
const { phase, taskId, taskMeta } = resolveCurrentTaskContext();
|
|
150
132
|
const archResult = deps.checkArchitectRequired({
|
|
151
133
|
phase, taskId, taskMeta, config, toolName
|
|
152
134
|
});
|
|
@@ -47,7 +47,11 @@ const DIAGNOSTIC_PATTERNS = [
|
|
|
47
47
|
/\bwhich\s+(approach|option|way|one)\s+(is\s+better|should|do\s+you)\b/i,
|
|
48
48
|
/\bis\s+it\s+better\s+to\b/i,
|
|
49
49
|
/\bwhat'?s?\s+the\s+(right|best|correct)\s+(approach|way)\b/i,
|
|
50
|
-
|
|
50
|
+
// Imperative "recommend" — anchored to prompt start or after sentence end.
|
|
51
|
+
// Prior version `/\brecommend\b/i` matched ANY occurrence and false-fired on
|
|
52
|
+
// "the recommendation system is broken" / "I recommend doing X" (statements,
|
|
53
|
+
// not questions). See wf-12271e82.
|
|
54
|
+
/(?:^|[.?!]\s+)\s*(please\s+|can\s+you\s+|could\s+you\s+|would\s+you\s+)?recommend\b/i,
|
|
51
55
|
/\bdid\s+you\s+(fix|address|verify|check|test|handle)\b/i,
|
|
52
56
|
/\bdo\s+you\s+(think|recommend|suggest)\b/i,
|
|
53
57
|
];
|
|
@@ -68,6 +68,18 @@ function handleSessionEnd(input) {
|
|
|
68
68
|
result.logged = false;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// wf-2eafdab0 (AC8): GC stale architect-run markers for completed tasks.
|
|
72
|
+
// Markers in `.workflow/state/architect-runs/` accumulate forever otherwise;
|
|
73
|
+
// task-id collision (re-used fixtures, manual ID re-use) bypasses the gate.
|
|
74
|
+
// Fail-open everywhere — never block session end.
|
|
75
|
+
try {
|
|
76
|
+
const { gcStaleMarkers } = require('../../flow-architect-runs');
|
|
77
|
+
const gc = gcStaleMarkers(); // default 7-day retention for completed tasks
|
|
78
|
+
if (gc && gc.removed && gc.removed.length > 0) {
|
|
79
|
+
result.architectMarkerGc = { removed: gc.removed.length };
|
|
80
|
+
}
|
|
81
|
+
} catch (_err) { /* non-critical */ }
|
|
82
|
+
|
|
71
83
|
// Surface pending skill proposals staged by `flow skill propose|patch|remove`.
|
|
72
84
|
// These await user approval (`flow skill promote|reject`) at session end.
|
|
73
85
|
try {
|