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.
@@ -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.29.9",
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 to all task types — entry-file LOC
81
- // + import-count rule (per .claude/rules/architecture/hook-three-layer.md)
82
- // is universally applicable; the exemption list in config covers known
83
- // pre-extraction violators (see ARCH-001, ARCH-002 in .workflow/state/last-audit.json).
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.