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.
- package/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +84 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- 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 [
|
|
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, ...
|
|
448
|
+
const [,, input, ..._args] = process.argv;
|
|
449
449
|
|
|
450
450
|
const extractor = new FigmaExtractor();
|
|
451
451
|
|
|
@@ -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,
|
|
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(', ')}`);
|
package/scripts/flow-health.js
CHANGED
|
@@ -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 };
|
package/scripts/flow-hooks.js
CHANGED
|
@@ -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,
|
|
168
|
+
const { target, _force } = options;
|
|
169
169
|
|
|
170
170
|
console.log(color('cyan', '🪝 Setting Up CLI Hooks'));
|
|
171
171
|
console.log('');
|
package/scripts/flow-id.js
CHANGED
|
@@ -55,19 +55,35 @@ function generatePlanId(title) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* Check if a string is a valid task ID
|
|
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
|
-
//
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
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
|
+
};
|