wogiflow 2.26.2 → 2.29.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.
Files changed (164) hide show
  1. package/.claude/commands/wogi-bug.md +30 -0
  2. package/.claude/commands/wogi-debug-hypothesis.md +33 -0
  3. package/.claude/commands/wogi-morning.md +1 -2
  4. package/.claude/commands/wogi-review.md +31 -2
  5. package/.claude/commands/wogi-start.md +32 -0
  6. package/.claude/commands/wogi-statusline-setup.md +12 -0
  7. package/.claude/commands/wogi-story.md +3 -2
  8. package/.claude/docs/claude-code-compatibility.md +40 -0
  9. package/.claude/docs/phases/01-explore.md +2 -1
  10. package/.claude/docs/phases/03-implement.md +4 -0
  11. package/.claude/docs/phases/04-verify.md +45 -0
  12. package/.claude/rules/README.md +36 -0
  13. package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
  14. package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
  15. package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
  16. package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
  17. package/.claude/rules/alternative-short-name.md +12 -0
  18. package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
  19. package/.claude/rules/architecture/hook-three-layer.md +68 -0
  20. package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
  21. package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
  22. package/.claude/settings.json +1 -1
  23. package/.workflow/agents/logic-adversary.md +2 -1
  24. package/.workflow/agents/personas/README.md +48 -0
  25. package/.workflow/agents/personas/platform-rigor.md +38 -0
  26. package/.workflow/agents/personas/scale-skeptic.md +28 -0
  27. package/.workflow/agents/personas/security-hawk.md +34 -0
  28. package/.workflow/agents/personas/simplicity-champion.md +37 -0
  29. package/.workflow/agents/personas/user-advocate.md +36 -0
  30. package/.workflow/bridges/base-bridge.js +46 -23
  31. package/.workflow/templates/claude-md.hbs +44 -122
  32. package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
  33. package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
  34. package/.workflow/templates/partials/methodology-rules.hbs +85 -79
  35. package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
  36. package/lib/fuzzy-patch.js +251 -0
  37. package/lib/installer.js +8 -0
  38. package/lib/memory-proposal-store.js +458 -0
  39. package/lib/mode-schema.js +255 -0
  40. package/lib/skill-proposal-store.js +432 -0
  41. package/lib/skill-registry.js +1 -1
  42. package/lib/wogi-claude +84 -9
  43. package/lib/wogi-claude-expect.exp +113 -76
  44. package/lib/workspace-channel-server.js +19 -0
  45. package/lib/workspace-contracts.js +1 -1
  46. package/lib/workspace-dispatch-tracking.js +144 -0
  47. package/lib/workspace-gates.js +1 -1
  48. package/lib/workspace-ipc-sqlite.js +550 -0
  49. package/lib/workspace-messages.js +92 -0
  50. package/lib/workspace-routing.js +1 -1
  51. package/lib/workspace-task-injector.js +223 -0
  52. package/lib/workspace.js +23 -0
  53. package/lib/worktree-review.js +315 -0
  54. package/package.json +2 -2
  55. package/scripts/base-workflow-step.js +1 -1
  56. package/scripts/flow +28 -4
  57. package/scripts/flow-ac-scope-preservation.js +238 -0
  58. package/scripts/flow-auto-review-worker.js +75 -0
  59. package/scripts/flow-auto-review.js +102 -0
  60. package/scripts/flow-autonomous-detector.js +118 -0
  61. package/scripts/flow-autonomous-mode.js +153 -0
  62. package/scripts/flow-best-of-n.js +1 -1
  63. package/scripts/flow-bulk-loop.js +1 -1
  64. package/scripts/flow-checkpoint.js +2 -6
  65. package/scripts/flow-community-sync.js +1 -1
  66. package/scripts/flow-completion-summary.js +176 -0
  67. package/scripts/flow-completion-truth-gate.js +343 -4
  68. package/scripts/flow-config-defaults.js +52 -5
  69. package/scripts/flow-context-compact/expander.js +1 -1
  70. package/scripts/flow-context-compact/section-extractor.js +2 -2
  71. package/scripts/flow-context-gatherer.js +1 -1
  72. package/scripts/flow-context-generator.js +1 -1
  73. package/scripts/flow-context-scoring.js +1 -1
  74. package/scripts/flow-correct.js +1 -1
  75. package/scripts/flow-decision-authority.js +66 -15
  76. package/scripts/flow-done.js +33 -1
  77. package/scripts/flow-epic-cascade.js +171 -0
  78. package/scripts/flow-epics.js +2 -7
  79. package/scripts/flow-eval-judge.js +1 -1
  80. package/scripts/flow-eval.js +1 -1
  81. package/scripts/flow-export-scanner.js +2 -6
  82. package/scripts/flow-failure-learning.js +1 -1
  83. package/scripts/flow-feature-dossier.js +787 -0
  84. package/scripts/flow-figma-extract.js +2 -2
  85. package/scripts/flow-figma-generate.js +1 -1
  86. package/scripts/flow-gate-confidence.js +1 -1
  87. package/scripts/flow-health.js +52 -1
  88. package/scripts/flow-hooks.js +1 -1
  89. package/scripts/flow-id.js +19 -3
  90. package/scripts/flow-instruction-richness.js +1 -1
  91. package/scripts/flow-knowledge-router.js +1 -1
  92. package/scripts/flow-knowledge-sync.js +1 -1
  93. package/scripts/flow-logic-adversary.js +76 -1
  94. package/scripts/flow-logic-rules.js +380 -0
  95. package/scripts/flow-long-input.js +5 -5
  96. package/scripts/flow-memory-sync.js +1 -1
  97. package/scripts/flow-memory.js +78 -7
  98. package/scripts/flow-migrate.js +1 -1
  99. package/scripts/flow-model-caller.js +1 -1
  100. package/scripts/flow-models.js +2 -2
  101. package/scripts/flow-morning.js +0 -17
  102. package/scripts/flow-multi-approach.js +1 -1
  103. package/scripts/flow-orchestrate-context.js +4 -4
  104. package/scripts/flow-orchestrate-templates.js +1 -1
  105. package/scripts/flow-orchestrate.js +8 -8
  106. package/scripts/flow-peer-review.js +1 -1
  107. package/scripts/flow-phase.js +9 -0
  108. package/scripts/flow-proactive-compact.js +1 -1
  109. package/scripts/flow-providers.js +1 -1
  110. package/scripts/flow-question-queue.js +255 -0
  111. package/scripts/flow-repo-map.js +312 -0
  112. package/scripts/flow-review-passes/index.js +1 -1
  113. package/scripts/flow-review-passes/integration.js +1 -1
  114. package/scripts/flow-review-passes/structure.js +1 -1
  115. package/scripts/flow-revision-tracker.js +1 -1
  116. package/scripts/flow-section-resolver.js +1 -1
  117. package/scripts/flow-session-end.js +74 -5
  118. package/scripts/flow-session-state.js +103 -1
  119. package/scripts/flow-setup-hooks.js +1 -1
  120. package/scripts/flow-skeptical-evaluator.js +274 -0
  121. package/scripts/flow-skill-generator.js +3 -3
  122. package/scripts/flow-skill-learn.js +3 -6
  123. package/scripts/flow-skill-manage.js +248 -0
  124. package/scripts/flow-spec-verifier.js +1 -1
  125. package/scripts/flow-standards-checker.js +75 -0
  126. package/scripts/flow-standards-gate.js +1 -1
  127. package/scripts/flow-statusline-setup.js +8 -2
  128. package/scripts/flow-step-changelog.js +2 -2
  129. package/scripts/flow-step-coverage.js +1 -1
  130. package/scripts/flow-step-knowledge.js +1 -1
  131. package/scripts/flow-step-regression.js +1 -1
  132. package/scripts/flow-step-simplifier.js +1 -1
  133. package/scripts/flow-task-analyzer.js +1 -1
  134. package/scripts/flow-task-classifier.js +1 -1
  135. package/scripts/flow-task-enforcer.js +1 -1
  136. package/scripts/flow-template-extractor.js +1 -1
  137. package/scripts/flow-trap-zone.js +1 -1
  138. package/scripts/flow-utils.js +4 -0
  139. package/scripts/flow-worker-question-classifier.js +51 -5
  140. package/scripts/flow-workspace-migrate-ipc.js +216 -0
  141. package/scripts/flow-workspace-summary.js +256 -0
  142. package/scripts/hooks/adapters/base-adapter.js +2 -2
  143. package/scripts/hooks/core/feature-dossier-gate.js +194 -0
  144. package/scripts/hooks/core/observation-capture.js +24 -0
  145. package/scripts/hooks/core/overdue-dispatches.js +20 -1
  146. package/scripts/hooks/core/phase-gate.js +15 -1
  147. package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
  148. package/scripts/hooks/core/post-compact.js +5 -2
  149. package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
  150. package/scripts/hooks/core/routing-gate.js +58 -0
  151. package/scripts/hooks/core/session-context.js +108 -0
  152. package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
  153. package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
  154. package/scripts/hooks/core/session-end.js +25 -0
  155. package/scripts/hooks/core/setup-handler.js +1 -1
  156. package/scripts/hooks/core/task-boundary-reset.js +110 -4
  157. package/scripts/hooks/core/worker-boundary-gate.js +71 -0
  158. package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
  159. package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
  160. package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
  161. package/scripts/hooks/entry/claude-code/session-start.js +74 -30
  162. package/scripts/hooks/entry/claude-code/stop.js +47 -1
  163. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
  164. package/.workflow/templates/partials/user-commands.hbs +0 -20
@@ -83,7 +83,7 @@ class FigmaExtractor {
83
83
  }
84
84
 
85
85
  parseNodes(nodes, parent = null) {
86
- for (const [nodeId, nodeData] of Object.entries(nodes)) {
86
+ for (const [_nodeId, nodeData] of Object.entries(nodes)) {
87
87
  const node = nodeData.document || nodeData;
88
88
  this.parseNode(node, parent);
89
89
  }
@@ -445,7 +445,7 @@ class FigmaExtractor {
445
445
  // ============================================================
446
446
 
447
447
  async function main() {
448
- const [,, input, ...args] = process.argv;
448
+ const [,, input, ..._args] = process.argv;
449
449
 
450
450
  const extractor = new FigmaExtractor();
451
451
 
@@ -21,7 +21,7 @@ const fs = require('node:fs');
21
21
  const path = require('node:path');
22
22
  const {
23
23
  detectFramework } = require('./flow-figma-index');
24
- const { getProjectRoot,
24
+ const { _getProjectRoot,
25
25
  getConfig,
26
26
  addRequestLogEntry,
27
27
  addAppMapComponent,
@@ -523,7 +523,7 @@ const VALID_DECISIONS = ['auto-apply', 'approved', 'blocked'];
523
523
  * @param {Object} params - Decision parameters
524
524
  * @throws {Error} If decision is not a valid type
525
525
  */
526
- function recordDecision({ _analysisId, decision, outcome }) {
526
+ function recordDecision({ _analysisId, decision, _outcome }) {
527
527
  // Validate decision type to prevent silent failures
528
528
  if (!VALID_DECISIONS.includes(decision)) {
529
529
  throw new Error(`Invalid decision type: ${decision}. Must be one of: ${VALID_DECISIONS.join(', ')}`);
@@ -1089,6 +1089,11 @@ function main() {
1089
1089
  }
1090
1090
  }
1091
1091
 
1092
+ // B7 (wf-c3b5afab): Surface gate miss-rate summary — rubber-stamping visibility
1093
+ console.log('');
1094
+ printSection('Checking gate telemetry...');
1095
+ printGateMissRateSummary();
1096
+
1092
1097
  // Summary
1093
1098
  console.log('');
1094
1099
  console.log('========================');
@@ -1106,6 +1111,52 @@ function main() {
1106
1111
  return { issues, warnings };
1107
1112
  }
1108
1113
 
1114
+ // B7 (wf-c3b5afab): One-line surface of gates above the miss-rate threshold.
1115
+ // Uses the same threshold as flow-session-end's watch section (>=10%).
1116
+ const GATE_MISS_RATE_THRESHOLD = 0.10;
1117
+ const GATE_MISS_WINDOW = '7d';
1118
+
1119
+ function loadGateStatsForHealth() {
1120
+ let getGateStats;
1121
+ try {
1122
+ ({ getGateStats } = require('./flow-gate-telemetry'));
1123
+ } catch (err) {
1124
+ if (process.env.DEBUG) console.error(`[DEBUG] Gate telemetry: ${err.message}`);
1125
+ return null;
1126
+ }
1127
+ try {
1128
+ return getGateStats({ since: GATE_MISS_WINDOW });
1129
+ } catch (err) {
1130
+ if (process.env.DEBUG) console.error(`[DEBUG] Gate telemetry stats: ${err.message}`);
1131
+ return null;
1132
+ }
1133
+ }
1134
+
1135
+ function printGateMissRateSummary(stats = loadGateStatsForHealth()) {
1136
+ const perGate = stats && stats.perGate ? stats.perGate : null;
1137
+ const gates = perGate ? Object.keys(perGate) : [];
1138
+ if (!perGate || gates.length === 0) {
1139
+ console.log(` ${color('dim', 'No telemetry yet (baseline)')}`);
1140
+ return;
1141
+ }
1142
+
1143
+ const over = gates.filter(id => {
1144
+ const g = perGate[id];
1145
+ return g.verdicts && g.verdicts.PASS > 0 && g.missRate >= GATE_MISS_RATE_THRESHOLD;
1146
+ });
1147
+
1148
+ if (over.length === 0) {
1149
+ success(`Gate missRate: 0 gates above ${(GATE_MISS_RATE_THRESHOLD * 100).toFixed(0)}% threshold`);
1150
+ return;
1151
+ }
1152
+
1153
+ warn(`Gate missRate: ${over.length} gate(s) above ${(GATE_MISS_RATE_THRESHOLD * 100).toFixed(0)}% threshold (see /wogi-gate-stats)`);
1154
+ for (const id of over.slice(0, 3)) {
1155
+ const g = perGate[id];
1156
+ console.log(` ${color('dim', `${id}: ${(g.missRate * 100).toFixed(1)}% miss (${g.missedAfterPass}/${g.verdicts.PASS})`)}`);
1157
+ }
1158
+ }
1159
+
1109
1160
  // ============================================================
1110
1161
  // Deep Audit (v1.0.4)
1111
1162
  // ============================================================
@@ -1390,4 +1441,4 @@ function checkCompletionClaimHonesty() {
1390
1441
  return hits;
1391
1442
  }
1392
1443
 
1393
- module.exports = { checkMcpScopes, normalizeMcpConfig, checkAntiDeferralCompliance, checkCompletionClaimHonesty };
1444
+ module.exports = { checkMcpScopes, normalizeMcpConfig, checkAntiDeferralCompliance, checkCompletionClaimHonesty, printGateMissRateSummary, GATE_MISS_RATE_THRESHOLD };
@@ -165,7 +165,7 @@ function installClaudeCodeHooks(adapter, hooksConfig) {
165
165
  * Install hooks for all configured targets
166
166
  */
167
167
  function setupHooks(options = {}) {
168
- const { target, force } = options;
168
+ const { target, _force } = options;
169
169
 
170
170
  console.log(color('cyan', '🪝 Setting Up CLI Hooks'));
171
171
  console.log('');
@@ -55,19 +55,35 @@ function generatePlanId(title) {
55
55
  }
56
56
 
57
57
  /**
58
- * Check if a string is a valid task ID (old or new format)
59
- * @returns {{ valid: boolean, format: 'hash' | 'legacy' | null }}
58
+ * Check if a string is a valid task ID.
59
+ * @returns {{ valid: boolean, format: 'hash' | 'slug' | 'legacy' | null }}
60
+ *
61
+ * Accepted formats:
62
+ * - 'hash' — wf-XXXXXXXX (8-char hex, produced by generateTaskId)
63
+ * - 'slug' — wf-<alphanum>[<alphanum or hyphen>]*<alphanum>, 5-64 chars total.
64
+ * For manager-dispatched descriptive IDs (e.g. wf-ttp-gate-2a,
65
+ * wf-auth-me-customer-capabilities). Path-safe: no '.', no '/',
66
+ * no '\\', no whitespace — safe to interpolate into file paths.
67
+ * - 'legacy' — TASK-NNN / BUG-NNN (grandfathered)
60
68
  */
61
69
  function validateTaskId(id) {
62
70
  if (!id || typeof id !== 'string') {
63
71
  return { valid: false, format: null };
64
72
  }
65
73
 
66
- // New hash-based format: wf-XXXXXXXX
74
+ // Hash format: wf-XXXXXXXX
67
75
  if (/^wf-[a-f0-9]{8}$/i.test(id)) {
68
76
  return { valid: true, format: 'hash' };
69
77
  }
70
78
 
79
+ // Slug format: wf-<start-alphanum><0-60 alphanum-or-hyphen><end-alphanum>.
80
+ // Min 5 chars ("wf-ab"), max 64 chars. Start+end must be alphanum so the ID
81
+ // never begins or ends with '-'. No dots or path separators allowed — this
82
+ // keeps `path.join(DIR, `.routing-receipt-${id}`)` safe from traversal.
83
+ if (/^wf-[a-z0-9][a-z0-9-]{0,60}[a-z0-9]$/i.test(id)) {
84
+ return { valid: true, format: 'slug' };
85
+ }
86
+
71
87
  // Legacy formats: TASK-XXX, BUG-XXX
72
88
  if (/^(TASK|BUG)-\d{3,}$/i.test(id)) {
73
89
  return { valid: true, format: 'legacy' };
@@ -834,7 +834,7 @@ this without guessing anything. Local LLM tokens are FREE - don't hold back!
834
834
  * @returns {string|null} Formatted type hints
835
835
  */
836
836
  function generateTypeHints(projectRoot, options = {}) {
837
- const { maxTypes = 10, taskDescription = '' } = options;
837
+ const { maxTypes = 10, _taskDescription = '' } = options;
838
838
  const typeHints = [];
839
839
 
840
840
  // Common type file locations
@@ -280,7 +280,7 @@ async function storeSkillLearning(correction, route, context) {
280
280
  };
281
281
  }
282
282
 
283
- async function storeProjectDecision(correction, _route, context) {
283
+ async function storeProjectDecision(correction, _route, _context) {
284
284
  const decisionsPath = PATHS.decisions;
285
285
 
286
286
  let content = '';
@@ -415,7 +415,7 @@ function printStatus(driftStatus) {
415
415
  { key: 'testing', name: 'Testing (testing.md)', file: getSpecFilePath('testing', { warnOnOld: false, preferNew: true }) }
416
416
  ];
417
417
 
418
- for (const { key, name, file } of categories) {
418
+ for (const { key, name, _file } of categories) {
419
419
  const status = driftStatus.categories[key];
420
420
  printSection(name);
421
421
 
@@ -55,6 +55,21 @@ const gateTelemetry = require('./flow-gate-telemetry');
55
55
  const RUBRIC_DIR = path.join(PATHS.workflow, 'rubrics');
56
56
  const DEFAULT_RUBRIC = 'logic-constitution-v3';
57
57
  const CALIBRATION_PATH = path.join(PATHS.state, 'adversary-calibration.json');
58
+ const PERSONAS_DIR = path.join(PATHS.workflow, 'agents', 'personas');
59
+
60
+ // Persona library — see .workflow/agents/personas/README.md
61
+ // Story: wf-258f558c (A2). Keys map to .md filenames (minus extension).
62
+ const PERSONA_LIBRARY = ['scale-skeptic', 'security-hawk', 'simplicity-champion', 'platform-rigor', 'user-advocate'];
63
+
64
+ // Trigger patterns — if the plan/title/taskId mentions any of these, auto-pick the persona.
65
+ // Order matters: earlier entries win when multiple triggers match.
66
+ const PERSONA_TRIGGERS = [
67
+ { persona: 'security-hawk', patterns: [/\bauth\b/i, /\bsecret\b/i, /\btoken\b/i, /\bcredential/i, /rm\s+-rf/i, /--force/i, /destructive/i, /\bshell\s+inject/i, /\bexecSync\b/, /\.env\b/] },
68
+ { persona: 'platform-rigor', patterns: [/\bPreToolUse\b/, /\bPostToolUse\b/, /\bSessionStart\b/, /\bMCP\b/, /\bsubagent\b/i, /\bvalidator\b/i, /\bvalidateTaskId\b/, /\bconfig\s+key\b/i] },
69
+ { persona: 'scale-skeptic', patterns: [/\bparallel\b/i, /\bconcurrent/i, /\bworktree/i, /\bdispatch/i, /\bqueue\b/i, /\bworker\b/i, /\brace\s+condition/i, /\bTOCTOU\b/i, /\bboundary\b/i] },
70
+ { persona: 'user-advocate', patterns: [/\bUI\b/, /\buser-facing\b/i, /\bonboarding/i, /\bjourney/i, /\berror\s+message/i, /\bempty\s+state/i, /\bcli\s+output/i] },
71
+ { persona: 'simplicity-champion', patterns: [/\bframework\b/i, /\bpluggable/i, /\bfuture-proof/i, /\bextensibility/i, /\bgeneric\b/i, /\babstraction\b/i, /\brefactor\b/i] },
72
+ ];
58
73
 
59
74
  const VALID_OVERALL_VERDICTS = new Set([
60
75
  'PASS',
@@ -106,6 +121,50 @@ function loadCalibration() {
106
121
  return Array.isArray(parsed.examples) ? parsed.examples : [];
107
122
  }
108
123
 
124
+ // ============================================================
125
+ // Persona library (wf-258f558c / A2)
126
+ // ============================================================
127
+
128
+ function _getPersonasConfig() {
129
+ const cfg = getConfig();
130
+ return cfg.intentGroundedReasoning?.logicAdversary?.personas || cfg.adversary?.personas || {};
131
+ }
132
+
133
+ /**
134
+ * Load a persona amplifier file.
135
+ * @param {string} key - persona slug (must be in PERSONA_LIBRARY)
136
+ * @returns {string} markdown content, or empty string when missing
137
+ */
138
+ function loadPersona(key) {
139
+ if (!key || !PERSONA_LIBRARY.includes(key)) return '';
140
+ const p = path.join(PERSONAS_DIR, `${key}.md`);
141
+ return fileExists(p) ? readFile(p) : '';
142
+ }
143
+
144
+ /**
145
+ * Pick a persona based on plan/title content. Falls back to taskId-hash rotation.
146
+ * @param {object} opts
147
+ * @param {string} [opts.taskId]
148
+ * @param {string} [opts.plan]
149
+ * @param {string} [opts.title]
150
+ * @returns {string} persona key (always one of PERSONA_LIBRARY)
151
+ */
152
+ function pickPersona({ taskId, plan, title } = {}) {
153
+ const haystack = [title || '', plan || ''].join('\n').slice(0, 4000);
154
+ if (haystack.trim().length > 0) {
155
+ for (const trigger of PERSONA_TRIGGERS) {
156
+ for (const pattern of trigger.patterns) {
157
+ if (pattern.test(haystack)) return trigger.persona;
158
+ }
159
+ }
160
+ }
161
+ // Rotate by taskId hash to ensure library coverage over time.
162
+ const source = taskId || haystack || Date.now().toString();
163
+ let h = 0;
164
+ for (let i = 0; i < source.length; i++) h = (h * 31 + source.charCodeAt(i)) >>> 0;
165
+ return PERSONA_LIBRARY[h % PERSONA_LIBRARY.length];
166
+ }
167
+
109
168
  // ============================================================
110
169
  // Intent artifact detection
111
170
  // ============================================================
@@ -187,9 +246,21 @@ function buildAdversaryPrompt(opts) {
187
246
  const personaPath = path.join(PATHS.workflow, 'agents', 'logic-adversary.md');
188
247
  const personaContent = fileExists(personaPath) ? readFile(personaPath) : '';
189
248
 
190
- // System prompt persona + rubric + calibration + degraded-mode notes
249
+ // Persona-library pick (wf-258f558c / A2). An amplification layer on top of the base persona.
250
+ // Config toggle: adversary.personas.enabled (default true). Orchestrator may override via opts.persona.
251
+ const personasEnabled = _getPersonasConfig().enabled !== false;
252
+ const personaKey = personasEnabled
253
+ ? (opts.persona || pickPersona({ taskId: opts.taskId, plan: opts.plan, title: opts.title }))
254
+ : null;
255
+ const amplifierContent = personaKey ? loadPersona(personaKey) : '';
256
+
257
+ // System prompt — base persona + persona amplifier + rubric + calibration + degraded-mode notes
191
258
  const systemParts = [];
192
259
  systemParts.push(personaContent || '# Persona\nYou are the Logic Adversary. See the rubric below.');
260
+ if (amplifierContent) {
261
+ systemParts.push('\n# Persona amplifier (stacks on top of base persona)\n');
262
+ systemParts.push(amplifierContent);
263
+ }
193
264
  systemParts.push('\n# Rubric (Logic Constitution)\n');
194
265
  systemParts.push(rubric.content);
195
266
 
@@ -691,4 +762,8 @@ module.exports = {
691
762
  VALID_PRINCIPLE_VERDICTS,
692
763
  DEFAULT_RUBRIC,
693
764
  DEFAULT_MODEL_PAIRING,
765
+ pickPersona,
766
+ loadPersona,
767
+ PERSONA_LIBRARY,
768
+ PERSONA_TRIGGERS,
694
769
  };
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Logic Rules (Cross-Cutting)
5
+ *
6
+ * Cross-cutting business-logic rules that span multiple features/pages.
7
+ *
8
+ * Problem solved (2026-04-24 workspace failure catalog, point 4):
9
+ * Owner says "every person in the system needs a seat — remove contact-person
10
+ * blocks." A dossier captures that for ONE feature. But the rule applies
11
+ * everywhere. Next session, Claude edits a different page and reintroduces
12
+ * the pattern. Feature-scoped memory doesn't catch it.
13
+ *
14
+ * How it works:
15
+ * - Rules live in <dossier-dir>/_logic-rules.md, one "## RULE: <id>" per rule
16
+ * - Each rule declares: statement, applies-to (file globs / keywords),
17
+ * enforcement-grep (regex to detect violations anywhere)
18
+ * - listRules() parses all rules from the canonical file(s)
19
+ * - matchRulesForFiles(files) returns rules scoped to those files
20
+ * - checkPropagation(rule, originFiles) greps the repo for other places
21
+ * the rule should apply but the origin task may have missed
22
+ * - detectViolations() scans the entire repo for grep patterns that should
23
+ * not appear — surfaces drift at session-start or /wogi-health
24
+ *
25
+ * Workspace-mode:
26
+ * Workspace-level rules live at WOGI_WORKSPACE_ROOT/.workspace/dossiers/
27
+ * _logic-rules.md (cross-repo).
28
+ * Per-repo rules live at <repo>/.workflow/dossiers/_logic-rules.md.
29
+ * Both are merged at read time; workspace shadows repo on id collision.
30
+ */
31
+
32
+ const fs = require('node:fs');
33
+ const path = require('node:path');
34
+ const { execSync } = require('node:child_process');
35
+ const { PATHS } = require('./flow-utils');
36
+ const { getDossierRoots, DOSSIER_DIRNAME: _DOSSIER_DIRNAME } = require('./flow-feature-dossier');
37
+
38
+ const LOGIC_RULES_FILENAME = '_logic-rules.md';
39
+
40
+ function getRulesPaths() {
41
+ const out = [];
42
+ for (const root of getDossierRoots()) {
43
+ const p = path.join(root.dir, LOGIC_RULES_FILENAME);
44
+ if (fs.existsSync(p)) out.push({ path: p, root: root.kind });
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Parse a logic-rules markdown file.
51
+ * Format:
52
+ * ## RULE: <id>
53
+ * <!-- id: <id> --> (optional, id taken from heading if absent)
54
+ * <!-- status: active|deprecated -->
55
+ * <!-- created: YYYY-MM-DD -->
56
+ * **Statement**: ...
57
+ * **Why**: ...
58
+ * **Applies to**:
59
+ * - pattern: src/**\/Customer*
60
+ * - keyword: contact person
61
+ * **Enforcement grep**: `regex`
62
+ * **Origin**: wf-xxxxxxxx
63
+ */
64
+ function parseRulesFile(raw, rootKind) {
65
+ const rules = [];
66
+ // Strip fenced code blocks so example rules in docs aren't parsed as real rules.
67
+ const stripped = raw.replace(/```[\s\S]*?```/g, '');
68
+ const sectionRegex = /\n##\s+RULE:\s*([^\n]+)\n([\s\S]*?)(?=\n##\s+RULE:|\n##\s+[^R]|$)/gi;
69
+ let m;
70
+ while ((m = sectionRegex.exec('\n' + stripped)) !== null) {
71
+ const headingId = m[1].trim();
72
+ const body = m[2];
73
+ const idMatch = body.match(/<!--\s*id:\s*([^>]+)-->/);
74
+ const id = (idMatch ? idMatch[1].trim() : headingId).replace(/\s+/g, '-').toLowerCase();
75
+ const statusMatch = body.match(/<!--\s*status:\s*([^>]+)-->/);
76
+ const createdMatch = body.match(/<!--\s*created:\s*([^>]+)-->/);
77
+ const statement = extractBoldField(body, 'Statement');
78
+ const why = extractBoldField(body, 'Why');
79
+ const origin = extractBoldField(body, 'Origin');
80
+ const grepMatch = body.match(/\*\*Enforcement\s*grep\*\*\s*:\s*`([^`]+)`/i);
81
+ const appliesSection = body.match(/\*\*Applies\s*to\*\*\s*:\s*\n([\s\S]*?)(?=\n\*\*|\n$|$)/i);
82
+
83
+ const patterns = [];
84
+ const keywords = [];
85
+ const components = [];
86
+ if (appliesSection) {
87
+ for (const line of appliesSection[1].split('\n')) {
88
+ const kv = line.trim().match(/^-\s*([a-zA-Z-]+):\s*(.+)$/);
89
+ if (!kv) continue;
90
+ const kind = kv[1].toLowerCase();
91
+ const value = kv[2].trim();
92
+ if (kind === 'pattern' || kind === 'file' || kind === 'filepattern') patterns.push(value);
93
+ else if (kind === 'keyword') keywords.push(value.toLowerCase());
94
+ else if (kind === 'component') components.push(value);
95
+ }
96
+ }
97
+
98
+ rules.push({
99
+ id,
100
+ status: statusMatch ? statusMatch[1].trim() : 'active',
101
+ created: createdMatch ? createdMatch[1].trim() : null,
102
+ statement: statement || '',
103
+ why: why || '',
104
+ origin: origin || '',
105
+ enforcementGrep: grepMatch ? grepMatch[1] : null,
106
+ appliesTo: { patterns, keywords, components },
107
+ _root: rootKind
108
+ });
109
+ }
110
+ return rules;
111
+ }
112
+
113
+ function extractBoldField(body, name) {
114
+ const re = new RegExp(`\\*\\*${name}\\*\\*\\s*:\\s*(.+?)(?=\\n\\*\\*|\\n$|$)`, 'is');
115
+ const m = body.match(re);
116
+ if (!m) return '';
117
+ return m[1].trim().replace(/\n+/g, ' ');
118
+ }
119
+
120
+ function listRules() {
121
+ const all = [];
122
+ const seen = new Map();
123
+ for (const { path: p, root } of getRulesPaths()) {
124
+ let raw;
125
+ try { raw = fs.readFileSync(p, 'utf-8'); } catch (_err) { continue; }
126
+ const rules = parseRulesFile(raw, root);
127
+ for (const r of rules) {
128
+ if (seen.has(r.id) && root !== 'workspace') continue;
129
+ seen.set(r.id, r);
130
+ }
131
+ }
132
+ for (const r of seen.values()) all.push(r);
133
+ return all;
134
+ }
135
+
136
+ function globToRegex(pat) {
137
+ const escaped = pat
138
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
139
+ .replace(/\*\*/g, '.__DBLSTAR__')
140
+ .replace(/\*/g, '[^/]*')
141
+ .replace(/\.__DBLSTAR__/g, '.*')
142
+ .replace(/\?/g, '[^/]');
143
+ return new RegExp(`^${escaped}$`, 'i');
144
+ }
145
+
146
+ function matchRulesForFiles(files = [], extraKeywords = []) {
147
+ if (!Array.isArray(files)) files = [files];
148
+ const lowerFiles = files.map(f => String(f).toLowerCase());
149
+ const lowerKw = extraKeywords.map(k => String(k).toLowerCase());
150
+ const rules = listRules();
151
+ const matched = [];
152
+ for (const rule of rules) {
153
+ if (rule.status !== 'active') continue;
154
+ let scoreHit = false;
155
+ const reasons = [];
156
+ for (const pat of rule.appliesTo.patterns) {
157
+ const re = globToRegex(pat);
158
+ for (const f of lowerFiles) {
159
+ if (re.test(f)) {
160
+ scoreHit = true;
161
+ reasons.push(`file-match: ${pat}`);
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ for (const kw of rule.appliesTo.keywords) {
167
+ if (lowerKw.some(k => k.includes(kw))) {
168
+ scoreHit = true;
169
+ reasons.push(`keyword: ${kw}`);
170
+ }
171
+ }
172
+ if (scoreHit) matched.push({ ...rule, reasons });
173
+ }
174
+ return matched;
175
+ }
176
+
177
+ /**
178
+ * Propagation check: given a rule and the files just touched,
179
+ * grep the repo for other places the rule's enforcement pattern appears.
180
+ * Surface hits as "rule applies here too, did you miss it?"
181
+ */
182
+ function checkPropagation(rule, originFiles = []) {
183
+ if (!rule.enforcementGrep) return { rule: rule.id, checked: false, otherHits: [] };
184
+ const normalizedOrigin = new Set(originFiles.map(f => path.normalize(f)));
185
+ try {
186
+ const out = execSync(
187
+ `git grep -lE ${JSON.stringify(rule.enforcementGrep)} -- . ':(exclude).workflow' ':(exclude).workspace' ':(exclude)node_modules' ':(exclude).git' 2>/dev/null || true`,
188
+ { cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
189
+ );
190
+ const files = out.split('\n').filter(Boolean);
191
+ const otherHits = files.filter(f => !normalizedOrigin.has(path.normalize(f)));
192
+ return { rule: rule.id, checked: true, pattern: rule.enforcementGrep, otherHits };
193
+ } catch (_err) {
194
+ return { rule: rule.id, checked: false, otherHits: [] };
195
+ }
196
+ }
197
+
198
+ function detectViolations() {
199
+ const rules = listRules();
200
+ const report = [];
201
+ for (const rule of rules) {
202
+ if (rule.status !== 'active') continue;
203
+ if (!rule.enforcementGrep) continue;
204
+ try {
205
+ const out = execSync(
206
+ `git grep -nE ${JSON.stringify(rule.enforcementGrep)} -- . ':(exclude).workflow' ':(exclude).workspace' ':(exclude)node_modules' ':(exclude).git' 2>/dev/null || true`,
207
+ { cwd: PATHS.root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
208
+ );
209
+ const lines = out.split('\n').filter(Boolean);
210
+ if (lines.length > 0) {
211
+ report.push({
212
+ rule: rule.id,
213
+ statement: rule.statement,
214
+ pattern: rule.enforcementGrep,
215
+ hits: lines.length,
216
+ sample: lines.slice(0, 5)
217
+ });
218
+ }
219
+ } catch (_err) { /* skip */ }
220
+ }
221
+ return report;
222
+ }
223
+
224
+ function buildRulesInjection(matches) {
225
+ if (!matches || matches.length === 0) return null;
226
+ const blocks = matches.slice(0, 6).map(rule => {
227
+ let b = `### Logic Rule: ${rule.id}\n`;
228
+ b += `**Statement**: ${rule.statement}\n`;
229
+ if (rule.why) b += `**Why**: ${rule.why}\n`;
230
+ b += `**Matched via**: ${rule.reasons.join(', ')}\n`;
231
+ if (rule.enforcementGrep) b += `**Enforcement grep**: \`${rule.enforcementGrep}\`\n`;
232
+ if (rule.origin) b += `**Origin**: ${rule.origin}\n`;
233
+ return b;
234
+ });
235
+ return [
236
+ '## Cross-Cutting Logic Rules (auto-loaded)',
237
+ '',
238
+ 'Active logic rules scoped to the files you are touching. Violating these introduces regressions the owner has already corrected. Run propagation check (`flow logic-rules propagate <rule-id>`) if your change implements or reinforces one of these rules.',
239
+ '',
240
+ blocks.join('\n---\n\n')
241
+ ].join('\n');
242
+ }
243
+
244
+ // ============================================================
245
+ // CLI
246
+ // ============================================================
247
+
248
+ function printHelp() {
249
+ console.log(`Usage: flow logic-rules <command> [args]
250
+
251
+ Commands:
252
+ list List all rules with status
253
+ show <id> Show a single rule
254
+ match --files "a,b" [--keywords "k1,k2"]
255
+ Show rules that scope to these files/keywords
256
+ propagate <id> [--origin "a,b"]
257
+ Find other places this rule's pattern appears
258
+ scan Detect violations across the whole repo
259
+ inject --files "a,b" [--keywords "k1,k2"]
260
+ Print phase-injection block
261
+ help Show this help
262
+
263
+ Examples:
264
+ flow logic-rules list
265
+ flow logic-rules match --files "src/pages/Customer.tsx"
266
+ flow logic-rules propagate every-person-needs-seat --origin "src/pages/Customer.tsx"
267
+ flow logic-rules scan
268
+ `);
269
+ }
270
+
271
+ function parseArgs(args) {
272
+ const out = { _: [], flags: {} };
273
+ for (let i = 0; i < args.length; i++) {
274
+ const a = args[i];
275
+ if (a.startsWith('--')) {
276
+ const key = a.slice(2);
277
+ const next = args[i + 1];
278
+ if (next !== undefined && !next.startsWith('--')) { out.flags[key] = next; i++; }
279
+ else { out.flags[key] = true; }
280
+ } else { out._.push(a); }
281
+ }
282
+ return out;
283
+ }
284
+
285
+ function cliMain(argv) {
286
+ const [cmd, ...rest] = argv;
287
+ const { _: positional, flags } = parseArgs(rest);
288
+
289
+ if (!cmd || cmd === 'help' || cmd === '--help') return printHelp();
290
+
291
+ if (cmd === 'list') {
292
+ const rules = listRules();
293
+ if (rules.length === 0) {
294
+ console.log('(no rules — edit .workflow/dossiers/_logic-rules.md to add)');
295
+ return;
296
+ }
297
+ for (const r of rules) {
298
+ console.log(`${r.id} [${r.status}] ${r.statement.slice(0, 80)}`);
299
+ }
300
+ return;
301
+ }
302
+
303
+ if (cmd === 'show') {
304
+ const id = positional[0];
305
+ const rules = listRules();
306
+ const r = rules.find(x => x.id === id);
307
+ if (!r) { console.error(`rule not found: ${id}`); process.exit(1); }
308
+ console.log(JSON.stringify(r, null, 2));
309
+ return;
310
+ }
311
+
312
+ if (cmd === 'match') {
313
+ const files = flags.files ? String(flags.files).split(',').map(s => s.trim()) : [];
314
+ const kw = flags.keywords ? String(flags.keywords).split(',').map(s => s.trim()) : [];
315
+ const matches = matchRulesForFiles(files, kw);
316
+ if (matches.length === 0) { console.log('(no matches)'); return; }
317
+ for (const m of matches) {
318
+ console.log(`${m.id} [${m.reasons.join(', ')}] ${m.statement.slice(0, 80)}`);
319
+ }
320
+ return;
321
+ }
322
+
323
+ if (cmd === 'propagate') {
324
+ const id = positional[0];
325
+ if (!id) { console.error('rule id required'); process.exit(1); }
326
+ const rules = listRules();
327
+ const r = rules.find(x => x.id === id);
328
+ if (!r) { console.error(`rule not found: ${id}`); process.exit(1); }
329
+ const origin = flags.origin ? String(flags.origin).split(',').map(s => s.trim()) : [];
330
+ const report = checkPropagation(r, origin);
331
+ if (!report.checked) { console.log('(no enforcement grep to propagate)'); return; }
332
+ if (report.otherHits.length === 0) {
333
+ console.log(`propagation: clean (pattern /${report.pattern}/ only appears in origin files)`);
334
+ } else {
335
+ console.log(`PROPAGATION: pattern /${report.pattern}/ also appears in:`);
336
+ for (const f of report.otherHits) console.log(` ${f}`);
337
+ process.exit(2);
338
+ }
339
+ return;
340
+ }
341
+
342
+ if (cmd === 'scan') {
343
+ const report = detectViolations();
344
+ if (report.length === 0) { console.log('no violations'); return; }
345
+ console.log(`${report.length} rule violation(s):`);
346
+ for (const v of report) {
347
+ console.log(`\n${v.rule}: ${v.statement}`);
348
+ console.log(` pattern: /${v.pattern}/ hits: ${v.hits}`);
349
+ for (const s of v.sample) console.log(` ${s}`);
350
+ }
351
+ process.exit(2);
352
+ }
353
+
354
+ if (cmd === 'inject') {
355
+ const files = flags.files ? String(flags.files).split(',').map(s => s.trim()) : [];
356
+ const kw = flags.keywords ? String(flags.keywords).split(',').map(s => s.trim()) : [];
357
+ const matches = matchRulesForFiles(files, kw);
358
+ const block = buildRulesInjection(matches);
359
+ if (block) console.log(block);
360
+ return;
361
+ }
362
+
363
+ console.error(`unknown command: ${cmd}`);
364
+ printHelp();
365
+ process.exit(1);
366
+ }
367
+
368
+ if (require.main === module) {
369
+ try { cliMain(process.argv.slice(2)); }
370
+ catch (err) { console.error(err.message); process.exit(1); }
371
+ }
372
+
373
+ module.exports = {
374
+ listRules,
375
+ parseRulesFile,
376
+ matchRulesForFiles,
377
+ checkPropagation,
378
+ detectViolations,
379
+ buildRulesInjection
380
+ };