wogiflow 2.29.9 → 2.30.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.
- package/.claude/commands/wogi-decide.md +28 -0
- package/.claude/commands/wogi-learn.md +1 -0
- package/package.json +2 -2
- package/scripts/flow-architect-pass.js +14 -0
- package/scripts/flow-standards-checker.js +161 -14
- package/scripts/hooks/core/architect-required-gate.js +175 -0
- package/scripts/hooks/core/pre-tool-deps.js +10 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +45 -0
|
@@ -105,6 +105,34 @@ Options:
|
|
|
105
105
|
|
|
106
106
|
Use `AskUserQuestion` to present these options.
|
|
107
107
|
|
|
108
|
+
### Step 2.5: Mechanical-Enforcement Check (wf-037f8d66)
|
|
109
|
+
|
|
110
|
+
**Rules without mechanical enforcement are vibes.** Before writing any new rule, classify how it will be enforced:
|
|
111
|
+
|
|
112
|
+
| Enforcement type | Examples | Reliability |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| **Mechanical gate** (PreToolUse/Stop hook, type system, lint rule, standards-checker pattern) | deferral-gate, research-evidence-gate, phase-read-gate, hook-three-layer LOC check, forbidden-patterns | High — agent cannot skip |
|
|
115
|
+
| **Test assertion** (unit/integration test that fails on violation) | architectural test asserting no hardcoded paths | High — fails CI |
|
|
116
|
+
| **Schema constraint** (config schema, JSON schema, Go type) | required fields, enum values | High — fails parse/compile |
|
|
117
|
+
| **Vibes-only** (rule text in CLAUDE.md, decisions.md, or rule file with no mechanical check) | "use kebab-case", "prefer composition over inheritance", "always X / never Y" | **Low — agent skips it** |
|
|
118
|
+
|
|
119
|
+
**The rule must declare its enforcement type.** If "vibes-only," call it out explicitly:
|
|
120
|
+
|
|
121
|
+
> ⚠ This rule has no mechanical gate. It will be inconsistently enforced. To make it stick, propose one of:
|
|
122
|
+
> - A standards-checker pattern in `.workflow/state/forbidden-patterns.json`
|
|
123
|
+
> - A new gate in `scripts/hooks/core/`
|
|
124
|
+
> - A unit test in `tests/`
|
|
125
|
+
> - A type-system constraint
|
|
126
|
+
>
|
|
127
|
+
> Without one of those, expect the same class of violation to recur.
|
|
128
|
+
|
|
129
|
+
**Anti-rationalization**:
|
|
130
|
+
- "I'll just trust the AI to follow it" → WRONG. The reason this rule exists is the AI didn't follow it last time.
|
|
131
|
+
- "It's documented in CLAUDE.md" → Documentation ≠ enforcement. Both can coexist.
|
|
132
|
+
- "We can add the gate later" → "Later" is "never" for vibes-rules. Either commit to a gate now, or accept inconsistent enforcement.
|
|
133
|
+
|
|
134
|
+
If user accepts vibes-only, proceed with the rule but tag it `enforcementType: "vibes-only"` in decisions.md so future audits surface it.
|
|
135
|
+
|
|
108
136
|
### Step 3: Assess Clarity
|
|
109
137
|
|
|
110
138
|
Evaluate if the rule needs clarification. **Skip questions if the rule is already clear and specific.**
|
|
@@ -174,6 +174,7 @@ Given a pattern to promote:
|
|
|
174
174
|
- Correction examples → Verification criteria
|
|
175
175
|
|
|
176
176
|
3. **Delegate duplicate checking to `/wogi-decide --from-pattern`** which handles duplicate detection and the full rule-writing flow. This ensures a single source of truth for all rule-creation logic.
|
|
177
|
+
- **Note (wf-037f8d66)**: `/wogi-decide` Step 2.5 will require declaring the rule's enforcement type (mechanical gate / test / schema constraint / vibes-only). Patterns being promoted to rules MUST answer "what's the gate?" — if no mechanical enforcement exists, the rule is vibes-only and will not reliably enforce. The fact that this pattern recurred enough to be promoted is itself evidence that vibes-only didn't work.
|
|
177
178
|
|
|
178
179
|
4. **Ask user for any additions:**
|
|
179
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wogiflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.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-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.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 tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-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-audit-gates.test.js tests/flow-standards-hook-three-layer.test.js tests/flow-correction-detector-reconcile.test.js tests/flow-correction-backfill.test.js tests/flow-audit-gates-feature-output-health.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 tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js tests/workspace-channel-tracking.test.js tests/flow-hooks-deletion-log.test.js tests/flow-task-boundary-reset.test.js tests/flow-deferral-gate.test.js tests/flow-research-required-gate.test.js tests/flow-standards-forbidden-patterns.test.js tests/flow-hooks-architect-required-gate.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",
|
|
@@ -479,6 +479,20 @@ function recordTelemetry({ taskId, parseResult, gateResult, runCtx = {} }) {
|
|
|
479
479
|
sessionId: runCtx.sessionId ?? null,
|
|
480
480
|
},
|
|
481
481
|
});
|
|
482
|
+
|
|
483
|
+
// wf-037f8d66: Write Architect-run marker for the architect-required PreToolUse gate.
|
|
484
|
+
// Marker proves Architect ran for this task — gate consults it before allowing
|
|
485
|
+
// Edit/Write/Bash in coding phase for L1+ tasks. Fail-open on any error.
|
|
486
|
+
if (taskId && (telemetryVerdict === 'PASS' || telemetryVerdict === 'PASS_WITH_NOTES')) {
|
|
487
|
+
try {
|
|
488
|
+
const archGate = require('./hooks/core/architect-required-gate');
|
|
489
|
+
archGate.writeArchitectRunMarker({
|
|
490
|
+
taskId,
|
|
491
|
+
model: runCtx.model || null,
|
|
492
|
+
plan: parseResult?.plan ? { sections: sectionsPresent } : null,
|
|
493
|
+
});
|
|
494
|
+
} catch (_err) { /* fail-open */ }
|
|
495
|
+
}
|
|
482
496
|
}
|
|
483
497
|
|
|
484
498
|
// ============================================================
|
|
@@ -77,23 +77,23 @@ const MATCH_LEVEL_SEVERITY = {
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// Task type to check type mapping for smart scoping
|
|
80
|
-
// wf-00c5067b: 'hook-three-layer' added
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
80
|
+
// wf-00c5067b: 'hook-three-layer' added — entry-file LOC + import-count rule.
|
|
81
|
+
// wf-037f8d66: 'forbidden-patterns' added — declarative project-specific
|
|
82
|
+
// patterns (agnosticism rules, no-hardcoding rules, etc.) loaded from
|
|
83
|
+
// .workflow/state/forbidden-patterns.json. Empty file = no-op.
|
|
84
84
|
const TASK_CHECK_MAP = {
|
|
85
|
-
'component': ['naming', 'components', 'security', 'hook-three-layer'],
|
|
86
|
-
'utility': ['naming', 'functions', 'security', 'hook-three-layer'],
|
|
87
|
-
'api': ['naming', 'api', 'security', 'hook-three-layer'],
|
|
88
|
-
'feature': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
|
|
89
|
-
'bugfix': ['naming', 'security', 'hook-three-layer'],
|
|
90
|
-
'refactor': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
|
|
91
|
-
'story': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'],
|
|
92
|
-
'default': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer']
|
|
85
|
+
'component': ['naming', 'components', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
86
|
+
'utility': ['naming', 'functions', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
87
|
+
'api': ['naming', 'api', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
88
|
+
'feature': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
89
|
+
'bugfix': ['naming', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
90
|
+
'refactor': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
91
|
+
'story': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer', 'forbidden-patterns'],
|
|
92
|
+
'default': ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer', 'forbidden-patterns']
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
// All available check types
|
|
96
|
-
const ALL_CHECK_TYPES = ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer'];
|
|
96
|
+
const ALL_CHECK_TYPES = ['naming', 'components', 'functions', 'api', 'schemas', 'services', 'security', 'hook-three-layer', 'forbidden-patterns'];
|
|
97
97
|
|
|
98
98
|
// ============================================================================
|
|
99
99
|
// Parse Standards Files
|
|
@@ -639,6 +639,140 @@ function checkHookThreeLayer(file, hookThreeLayerConfig = {}) {
|
|
|
639
639
|
return violations;
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/**
|
|
643
|
+
* wf-037f8d66 — Check forbidden patterns from project's declarative rule pack.
|
|
644
|
+
*
|
|
645
|
+
* Projects declare their forbidden patterns in `.workflow/state/forbidden-patterns.json`.
|
|
646
|
+
* This is the GENERIC, DATA-DRIVEN counterpart to checkSecurityPatterns (which is
|
|
647
|
+
* source-coded). Projects can encode "agnosticism" rules, "no-hardcoding" rules,
|
|
648
|
+
* "no-claude-code-literal-outside-docs" rules, etc., without modifying the checker.
|
|
649
|
+
*
|
|
650
|
+
* Format (forbidden-patterns.json):
|
|
651
|
+
* [
|
|
652
|
+
* {
|
|
653
|
+
* "id": "no-claude-code-literal",
|
|
654
|
+
* "pattern": "['\"]claude-code['\"]", // RegExp source string
|
|
655
|
+
* "flags": "g", // optional, defaults to 'g'
|
|
656
|
+
* "exemptions": ["docs/**", "*.md", "reference/**"],
|
|
657
|
+
* "severity": "must-fix", // 'must-fix' | 'warning'
|
|
658
|
+
* "message": "wogiflow-cli is agnostic; ..."
|
|
659
|
+
* }
|
|
660
|
+
* ]
|
|
661
|
+
*
|
|
662
|
+
* @param {Object} file — { path, content }
|
|
663
|
+
* @param {Object[]} patterns — array of pattern entries
|
|
664
|
+
* @returns {Object[]} violations
|
|
665
|
+
*/
|
|
666
|
+
function checkForbiddenPatterns(file, patterns) {
|
|
667
|
+
const violations = [];
|
|
668
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return violations;
|
|
669
|
+
|
|
670
|
+
const content = file.content || '';
|
|
671
|
+
const relPath = file.path.startsWith('/')
|
|
672
|
+
? path.relative(PATHS.root, file.path)
|
|
673
|
+
: file.path;
|
|
674
|
+
|
|
675
|
+
for (const entry of patterns) {
|
|
676
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
677
|
+
if (!entry.pattern || typeof entry.pattern !== 'string') continue;
|
|
678
|
+
|
|
679
|
+
// Exemption check (glob match against relative path)
|
|
680
|
+
const exemptions = Array.isArray(entry.exemptions) ? entry.exemptions : [];
|
|
681
|
+
const exempted = exemptions.some(glob => globMatch(relPath, glob));
|
|
682
|
+
if (exempted) continue;
|
|
683
|
+
|
|
684
|
+
// Compile regex
|
|
685
|
+
let re;
|
|
686
|
+
try {
|
|
687
|
+
const flags = entry.flags && typeof entry.flags === 'string' ? entry.flags : 'g';
|
|
688
|
+
re = new RegExp(entry.pattern, flags);
|
|
689
|
+
} catch (_err) {
|
|
690
|
+
// Invalid regex — skip (don't crash check)
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Match
|
|
695
|
+
let match;
|
|
696
|
+
re.lastIndex = 0;
|
|
697
|
+
while ((match = re.exec(content)) !== null) {
|
|
698
|
+
const before = content.substring(0, match.index);
|
|
699
|
+
const lineNumber = (before.match(/\n/g) || []).length + 1;
|
|
700
|
+
violations.push({
|
|
701
|
+
type: 'forbidden-pattern',
|
|
702
|
+
severity: entry.severity === 'warning' ? 'warning' : 'must-fix',
|
|
703
|
+
file: file.path,
|
|
704
|
+
line: lineNumber,
|
|
705
|
+
message: entry.message || `Forbidden pattern matched: ${entry.id || entry.pattern}`,
|
|
706
|
+
rule: `forbidden-patterns.json: ${entry.id || '(unnamed)'}`
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Avoid infinite loop on zero-length matches
|
|
710
|
+
if (match.index === re.lastIndex) re.lastIndex++;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return violations;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Minimal glob-to-regex matcher for forbidden-pattern exemptions.
|
|
719
|
+
* Supports: `**` (any depth), `*` (no path separator), exact match.
|
|
720
|
+
* Per security-patterns.md §4: use `[^/]*` not `.*` to prevent path
|
|
721
|
+
* separator matching in `*` (only `**` should cross directories).
|
|
722
|
+
*/
|
|
723
|
+
function globMatch(filePath, glob) {
|
|
724
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
725
|
+
// Build regex from glob
|
|
726
|
+
let re = '^';
|
|
727
|
+
let i = 0;
|
|
728
|
+
while (i < glob.length) {
|
|
729
|
+
const c = glob[i];
|
|
730
|
+
if (c === '*' && glob[i + 1] === '*') {
|
|
731
|
+
// ** matches anything including separators
|
|
732
|
+
re += '.*';
|
|
733
|
+
i += 2;
|
|
734
|
+
// Skip a following / so '**/foo' matches both 'foo' and 'a/b/foo'
|
|
735
|
+
if (glob[i] === '/') i++;
|
|
736
|
+
} else if (c === '*') {
|
|
737
|
+
re += '[^/]*';
|
|
738
|
+
i++;
|
|
739
|
+
} else if (c === '?') {
|
|
740
|
+
re += '[^/]';
|
|
741
|
+
i++;
|
|
742
|
+
} else if ('.^$+(){}[]|\\'.includes(c)) {
|
|
743
|
+
re += '\\' + c;
|
|
744
|
+
i++;
|
|
745
|
+
} else {
|
|
746
|
+
re += c;
|
|
747
|
+
i++;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
re += '$';
|
|
751
|
+
try {
|
|
752
|
+
return new RegExp(re).test(normalized);
|
|
753
|
+
} catch (_err) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Load forbidden-patterns.json from project state dir.
|
|
760
|
+
* @returns {Object[]} pattern entries (empty array if missing/invalid)
|
|
761
|
+
*/
|
|
762
|
+
function loadForbiddenPatterns(projectRoot) {
|
|
763
|
+
const root = projectRoot || PATHS.root;
|
|
764
|
+
const filePath = path.join(root, '.workflow', 'state', 'forbidden-patterns.json');
|
|
765
|
+
if (!fs.existsSync(filePath)) return [];
|
|
766
|
+
try {
|
|
767
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
768
|
+
const parsed = JSON.parse(raw);
|
|
769
|
+
if (!Array.isArray(parsed)) return [];
|
|
770
|
+
return parsed;
|
|
771
|
+
} catch (_err) {
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
642
776
|
function checkApiDuplication(file, existingEndpoints, matchConfig) {
|
|
643
777
|
const violations = [];
|
|
644
778
|
const content = file.content || '';
|
|
@@ -1090,6 +1224,8 @@ function runStandardsCheck(files, options = {}) {
|
|
|
1090
1224
|
const functions = checksToRun.includes('functions') ? parseFunctionMap() : [];
|
|
1091
1225
|
const endpoints = checksToRun.includes('api') ? parseApiMap() : [];
|
|
1092
1226
|
const rulesFiles = checksToRun.includes('security') ? loadRulesDir() : [];
|
|
1227
|
+
// wf-037f8d66: load forbidden-patterns.json (empty array if missing)
|
|
1228
|
+
const forbiddenPatterns = checksToRun.includes('forbidden-patterns') ? loadForbiddenPatterns() : [];
|
|
1093
1229
|
|
|
1094
1230
|
// Load schema/service registries if needed
|
|
1095
1231
|
const schemas = checksToRun.includes('schemas') ? parseSchemaMap() : [];
|
|
@@ -1115,7 +1251,8 @@ function runStandardsCheck(files, options = {}) {
|
|
|
1115
1251
|
'service-map.md': { checked: checksToRun.includes('services') && services.length > 0, violations: 0 },
|
|
1116
1252
|
'naming-conventions': { checked: checksToRun.includes('naming'), violations: 0 },
|
|
1117
1253
|
'security-patterns': { checked: checksToRun.includes('security'), violations: 0 },
|
|
1118
|
-
'hook-three-layer': { checked: checksToRun.includes('hook-three-layer'), violations: 0 }
|
|
1254
|
+
'hook-three-layer': { checked: checksToRun.includes('hook-three-layer'), violations: 0 },
|
|
1255
|
+
'forbidden-patterns': { checked: checksToRun.includes('forbidden-patterns') && forbiddenPatterns.length > 0, violations: 0 }
|
|
1119
1256
|
};
|
|
1120
1257
|
|
|
1121
1258
|
for (const file of files) {
|
|
@@ -1181,6 +1318,13 @@ function runStandardsCheck(files, options = {}) {
|
|
|
1181
1318
|
allViolations.push(...hookViolations);
|
|
1182
1319
|
checksSummary['hook-three-layer'].violations += hookViolations.length;
|
|
1183
1320
|
}
|
|
1321
|
+
|
|
1322
|
+
// Forbidden patterns from project rule pack (wf-037f8d66)
|
|
1323
|
+
if (checksToRun.includes('forbidden-patterns') && forbiddenPatterns.length > 0) {
|
|
1324
|
+
const fpViolations = checkForbiddenPatterns(file, forbiddenPatterns);
|
|
1325
|
+
allViolations.push(...fpViolations);
|
|
1326
|
+
checksSummary['forbidden-patterns'].violations += fpViolations.length;
|
|
1327
|
+
}
|
|
1184
1328
|
}
|
|
1185
1329
|
|
|
1186
1330
|
// Count must-fix violations
|
|
@@ -1415,6 +1559,9 @@ module.exports = {
|
|
|
1415
1559
|
checkRegistryDuplication,
|
|
1416
1560
|
checkSecurityPatterns,
|
|
1417
1561
|
checkHookThreeLayer,
|
|
1562
|
+
checkForbiddenPatterns, // wf-037f8d66: declarative project rule pack
|
|
1563
|
+
loadForbiddenPatterns, // wf-037f8d66: exposed for tests + external use
|
|
1564
|
+
globMatch, // wf-037f8d66: exposed for unit testing
|
|
1418
1565
|
extractDeclaredNames,
|
|
1419
1566
|
discoverAllRegistries,
|
|
1420
1567
|
collectReuseCandidates,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Architect-Required Gate (wf-037f8d66)
|
|
5
|
+
*
|
|
6
|
+
* Closes the methodology gap where the IGR Architect/Adversary pass at
|
|
7
|
+
* spec_review IS specced (per .claude/docs/phases/02-spec.md) but enforcement
|
|
8
|
+
* is prompt-only — the agent can skip Architect and go straight to coding.
|
|
9
|
+
*
|
|
10
|
+
* This gate fires on Edit/Write during the `coding` phase for L1+ tasks when
|
|
11
|
+
* config.intentGroundedReasoning.enabled is true and no evidence of an
|
|
12
|
+
* Architect run exists for the current task.
|
|
13
|
+
*
|
|
14
|
+
* Evidence marker: `.workflow/state/architect-runs/<task-id>.json` written
|
|
15
|
+
* by flow-architect-pass.js on successful completion.
|
|
16
|
+
*
|
|
17
|
+
* Scope:
|
|
18
|
+
* - L0 (epic) / L1 (story) tasks: Architect required → gate enforces
|
|
19
|
+
* - L2 / L3 tasks: skip spec_review entirely (correctly) → gate is a no-op
|
|
20
|
+
*
|
|
21
|
+
* Fail-open: any error reading state/config → allow tool call. Same pattern
|
|
22
|
+
* as research-evidence-gate.js.
|
|
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.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} payload — { taskId, completedAt, model, plan }
|
|
44
|
+
* @returns {{ written: boolean, path: string|null }}
|
|
45
|
+
*/
|
|
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
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check whether an Architect run is recorded for a given task.
|
|
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
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Determine whether the current task requires Architect (L1+ only).
|
|
86
|
+
* @param {Object} taskMeta — task record from ready.json (has level, type)
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function requiresArchitect(taskMeta) {
|
|
90
|
+
if (!taskMeta || typeof taskMeta !== 'object') return false;
|
|
91
|
+
const level = (taskMeta.level || '').toUpperCase();
|
|
92
|
+
// L0 (epic) and L1 (story) require Architect.
|
|
93
|
+
// L2 (task) / L3 (subtask) bypass spec_review correctly.
|
|
94
|
+
return level === 'L0' || level === 'L1';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read whether the gate is enabled from config.
|
|
99
|
+
* Default: enabled when IGR is enabled.
|
|
100
|
+
*/
|
|
101
|
+
function isGateEnabled(config) {
|
|
102
|
+
const igr = config?.intentGroundedReasoning;
|
|
103
|
+
if (!igr || igr.enabled === false) return false;
|
|
104
|
+
// Explicit toggle on the gate itself overrides
|
|
105
|
+
if (config?.architectRequiredGate?.enabled === false) return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Main gate check.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} ctx — { phase, taskId, taskMeta, config, toolName }
|
|
113
|
+
* @returns {{ blocked: boolean, reason?: string, message?: string }}
|
|
114
|
+
*/
|
|
115
|
+
function checkArchitectRequired(ctx) {
|
|
116
|
+
const { phase, taskId, taskMeta, config, toolName } = ctx || {};
|
|
117
|
+
|
|
118
|
+
// Only fires on tools that mutate state (Edit/Write/Bash/TodoWrite)
|
|
119
|
+
const mutationTools = new Set(['Edit', 'Write', 'TodoWrite', 'Bash']);
|
|
120
|
+
if (!toolName || !mutationTools.has(toolName)) {
|
|
121
|
+
return { blocked: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Only fires during coding phase
|
|
125
|
+
if (phase !== 'coding') {
|
|
126
|
+
return { blocked: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Gate disabled (or IGR off)
|
|
130
|
+
if (!isGateEnabled(config)) {
|
|
131
|
+
return { blocked: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// No active task → not in scope (gate doesn't apply)
|
|
135
|
+
if (!taskId) {
|
|
136
|
+
return { blocked: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// L2/L3 tasks bypass spec_review correctly — gate is a no-op
|
|
140
|
+
if (!requiresArchitect(taskMeta)) {
|
|
141
|
+
return { blocked: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for evidence marker
|
|
145
|
+
if (hasArchitectRun(taskId)) {
|
|
146
|
+
return { blocked: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Block: Architect required but no evidence
|
|
150
|
+
return {
|
|
151
|
+
blocked: true,
|
|
152
|
+
reason: 'architect-required',
|
|
153
|
+
message: [
|
|
154
|
+
`ARCHITECT-REQUIRED GATE: task ${taskId} is L1+ in coding phase but no `,
|
|
155
|
+
`Architect run is recorded at ${getArchitectRunPath(taskId)}.\n\n`,
|
|
156
|
+
`Per .claude/docs/phases/02-spec.md Step 1.55, L1+ tasks must run an `,
|
|
157
|
+
`Architect pass before coding. Invoke:\n\n`,
|
|
158
|
+
` node scripts/flow-architect-pass.js run --task=${taskId}\n\n`,
|
|
159
|
+
`Then retry your edit. To opt out for this task only, set `,
|
|
160
|
+
`config.architectRequiredGate.enabled = false (project-level), or use `,
|
|
161
|
+
`\`flow architect-skip --task=${taskId} --reason="..."\` (single-task escape; `,
|
|
162
|
+
`not yet implemented — opens follow-up wf if needed).`
|
|
163
|
+
].join('')
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
checkArchitectRequired,
|
|
169
|
+
writeArchitectRunMarker,
|
|
170
|
+
hasArchitectRun,
|
|
171
|
+
requiresArchitect,
|
|
172
|
+
getArchitectRunPath,
|
|
173
|
+
isGateEnabled,
|
|
174
|
+
ARCHITECT_RUNS_DIR
|
|
175
|
+
};
|
|
@@ -58,6 +58,15 @@ function loadGateDeps() {
|
|
|
58
58
|
if (process.env.DEBUG) console.error(`[Hook] Phase-read gate not loaded: ${_err.message}`);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// wf-037f8d66: Architect-required gate (mechanical Layer 2 enforcement)
|
|
62
|
+
let checkArchitectRequired = () => ({ blocked: false });
|
|
63
|
+
try {
|
|
64
|
+
const arg = require('./architect-required-gate');
|
|
65
|
+
checkArchitectRequired = arg.checkArchitectRequired;
|
|
66
|
+
} catch (_err) {
|
|
67
|
+
if (process.env.DEBUG) console.error(`[Hook] Architect-required gate not loaded: ${_err.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
let recordEvidenceRead = () => {};
|
|
62
71
|
let checkSpecWriteGate = _phaseNoop;
|
|
63
72
|
let clearResearchEvidence = () => {};
|
|
@@ -168,6 +177,7 @@ function loadGateDeps() {
|
|
|
168
177
|
checkRoutingGate, clearRoutingPending, hasActiveTask,
|
|
169
178
|
checkPhaseGate, checkCommitLogGate,
|
|
170
179
|
recordPhaseRead, checkPhaseReadGate, clearPhaseReads,
|
|
180
|
+
checkArchitectRequired, // wf-037f8d66
|
|
171
181
|
recordEvidenceRead, checkSpecWriteGate, clearResearchEvidence,
|
|
172
182
|
checkDeployGate, checkWriteBlock,
|
|
173
183
|
checkStrikeGate, checkBugfixScope, checkScopeMutation,
|
|
@@ -118,6 +118,51 @@ function runPreToolGates(ctx, deps) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Architect-required gate (wf-037f8d66)
|
|
122
|
+
// L1+ tasks in coding phase must have run Architect/Adversary before any Edit/Write/Bash.
|
|
123
|
+
// L2/L3 tasks bypass spec_review entirely (correctly), so the gate is a no-op there.
|
|
124
|
+
// Fail-open on any error.
|
|
125
|
+
if (typeof deps.checkArchitectRequired === 'function' &&
|
|
126
|
+
(toolName === 'Edit' || toolName === 'Write' || toolName === 'Bash' || toolName === 'TodoWrite')) {
|
|
127
|
+
try {
|
|
128
|
+
// Resolve current phase + task meta from active state
|
|
129
|
+
const flowUtils = require('../../flow-utils');
|
|
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
|
+
|
|
150
|
+
const archResult = deps.checkArchitectRequired({
|
|
151
|
+
phase, taskId, taskMeta, config, toolName
|
|
152
|
+
});
|
|
153
|
+
if (archResult.blocked) {
|
|
154
|
+
return {
|
|
155
|
+
allowed: false,
|
|
156
|
+
blocked: true,
|
|
157
|
+
reason: archResult.reason || 'architect-required',
|
|
158
|
+
message: archResult.message,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
if (process.env.DEBUG) console.error(`[Hook] Architect-required gate error (fail-open): ${_err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
// wf-f9912af6: Deferral-authorization gate. Blocks Write/Edit to
|
|
122
167
|
// last-review.json / last-audit.json when the new content introduces
|
|
123
168
|
// `status: deferred*` on findings without explicit user authorization.
|