agileflow 3.1.0 → 3.2.1
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/CHANGELOG.md +10 -0
- package/README.md +57 -85
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +409 -434
- package/scripts/claude-tmux.sh +80 -2
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/browser-qa-evidence.js +409 -0
- package/scripts/lib/browser-qa-status.js +192 -0
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +295 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +7 -2
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/browser-qa.md +328 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/browser-qa.md +240 -0
- package/src/core/commands/configure.md +8 -8
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/browser-qa-spec.yaml +94 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -69
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -105
- package/scripts/tmux-task-watcher.sh +0 -344
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-qa-status.js - Agentic test status integration for status.json
|
|
3
|
+
*
|
|
4
|
+
* Adds and manages `agentic_test_status` field on stories in status.json.
|
|
5
|
+
* This field is separate from `test_status` (which tracks deterministic Jest tests).
|
|
6
|
+
*
|
|
7
|
+
* Status values:
|
|
8
|
+
* - "validated" : >=80% pass rate in agentic browser tests
|
|
9
|
+
* - "warning" : 70-79% pass rate (needs investigation)
|
|
10
|
+
* - "failed" : <70% pass rate (potential bug)
|
|
11
|
+
* - "not_run" : No agentic tests executed yet
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const { updateAgenticTestStatus, getAgenticTestStatus } = require('./lib/browser-qa-status');
|
|
15
|
+
*
|
|
16
|
+
* // Update a story's agentic test status
|
|
17
|
+
* updateAgenticTestStatus(projectRoot, 'US-0050', {
|
|
18
|
+
* status: 'validated',
|
|
19
|
+
* pass_rate: 0.87,
|
|
20
|
+
* scenarios_run: 3,
|
|
21
|
+
* last_run: '2026-02-16T14:30:00Z',
|
|
22
|
+
* evidence_path: '.agileflow/ui-review/runs/2026-02-16_14-30-00/'
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Read a story's agentic test status
|
|
26
|
+
* const result = getAgenticTestStatus(projectRoot, 'US-0050');
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const STATUS_FILE = 'docs/09-agents/status.json';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read status.json safely
|
|
36
|
+
* @param {string} projectRoot - Project root directory
|
|
37
|
+
* @returns {object|null} Parsed status.json or null on error
|
|
38
|
+
*/
|
|
39
|
+
function readStatusJson(projectRoot) {
|
|
40
|
+
const statusPath = path.join(projectRoot, STATUS_FILE);
|
|
41
|
+
if (!fs.existsSync(statusPath)) return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write status.json safely
|
|
51
|
+
* @param {string} projectRoot - Project root directory
|
|
52
|
+
* @param {object} data - Status data to write
|
|
53
|
+
* @returns {boolean} Success status
|
|
54
|
+
*/
|
|
55
|
+
function writeStatusJson(projectRoot, data) {
|
|
56
|
+
const statusPath = path.join(projectRoot, STATUS_FILE);
|
|
57
|
+
try {
|
|
58
|
+
data.updated = new Date().toISOString();
|
|
59
|
+
fs.writeFileSync(statusPath, JSON.stringify(data, null, 2) + '\n');
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update a story's agentic test status
|
|
68
|
+
* @param {string} projectRoot - Project root directory
|
|
69
|
+
* @param {string} storyId - Story ID (e.g., 'US-0050')
|
|
70
|
+
* @param {object} result - Agentic test result
|
|
71
|
+
* @param {'validated'|'warning'|'failed'|'not_run'} result.status - Overall status
|
|
72
|
+
* @param {number} result.pass_rate - Pass rate (0-1)
|
|
73
|
+
* @param {number} result.scenarios_run - Number of scenarios executed
|
|
74
|
+
* @param {string} result.last_run - ISO timestamp of last run
|
|
75
|
+
* @param {string} result.evidence_path - Path to evidence directory
|
|
76
|
+
* @returns {boolean} Success status
|
|
77
|
+
*/
|
|
78
|
+
function updateAgenticTestStatus(projectRoot, storyId, result) {
|
|
79
|
+
const status = readStatusJson(projectRoot);
|
|
80
|
+
if (!status) return false;
|
|
81
|
+
|
|
82
|
+
if (!status.stories || !status.stories[storyId]) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
status.stories[storyId].agentic_test_status = result.status;
|
|
87
|
+
status.stories[storyId].agentic_test_details = {
|
|
88
|
+
pass_rate: result.pass_rate,
|
|
89
|
+
scenarios_run: result.scenarios_run,
|
|
90
|
+
last_run: result.last_run,
|
|
91
|
+
evidence_path: result.evidence_path,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return writeStatusJson(projectRoot, status);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a story's agentic test status
|
|
99
|
+
* @param {string} projectRoot - Project root directory
|
|
100
|
+
* @param {string} storyId - Story ID (e.g., 'US-0050')
|
|
101
|
+
* @returns {object|null} Agentic test status or null
|
|
102
|
+
*/
|
|
103
|
+
function getAgenticTestStatus(projectRoot, storyId) {
|
|
104
|
+
const status = readStatusJson(projectRoot);
|
|
105
|
+
if (!status || !status.stories || !status.stories[storyId]) return null;
|
|
106
|
+
|
|
107
|
+
const story = status.stories[storyId];
|
|
108
|
+
return {
|
|
109
|
+
status: story.agentic_test_status || 'not_run',
|
|
110
|
+
details: story.agentic_test_details || null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update multiple stories' agentic test statuses from a run summary
|
|
116
|
+
* @param {string} projectRoot - Project root directory
|
|
117
|
+
* @param {Array} scenarioResults - Array of scenario results
|
|
118
|
+
* @param {string} scenarioResults[].story_id - Story ID
|
|
119
|
+
* @param {'validated'|'warning'|'failed'} scenarioResults[].status - Status
|
|
120
|
+
* @param {number} scenarioResults[].pass_rate - Pass rate
|
|
121
|
+
* @param {string} evidencePath - Path to run evidence directory
|
|
122
|
+
* @returns {{ updated: number, skipped: number }}
|
|
123
|
+
*/
|
|
124
|
+
function updateBatchAgenticStatus(projectRoot, scenarioResults, evidencePath) {
|
|
125
|
+
const status = readStatusJson(projectRoot);
|
|
126
|
+
if (!status) return { updated: 0, skipped: 0 };
|
|
127
|
+
|
|
128
|
+
let updated = 0;
|
|
129
|
+
let skipped = 0;
|
|
130
|
+
const timestamp = new Date().toISOString();
|
|
131
|
+
|
|
132
|
+
for (const result of scenarioResults) {
|
|
133
|
+
if (!result.story_id) {
|
|
134
|
+
skipped++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!status.stories || !status.stories[result.story_id]) {
|
|
139
|
+
skipped++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
status.stories[result.story_id].agentic_test_status = result.status;
|
|
144
|
+
status.stories[result.story_id].agentic_test_details = {
|
|
145
|
+
pass_rate: result.pass_rate,
|
|
146
|
+
scenarios_run: 1,
|
|
147
|
+
last_run: timestamp,
|
|
148
|
+
evidence_path: evidencePath,
|
|
149
|
+
};
|
|
150
|
+
updated++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (updated > 0) {
|
|
154
|
+
writeStatusJson(projectRoot, status);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { updated, skipped };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get summary of all agentic test statuses across stories
|
|
162
|
+
* @param {string} projectRoot - Project root directory
|
|
163
|
+
* @returns {{ validated: number, warning: number, failed: number, not_run: number, total: number }}
|
|
164
|
+
*/
|
|
165
|
+
function getAgenticTestSummary(projectRoot) {
|
|
166
|
+
const status = readStatusJson(projectRoot);
|
|
167
|
+
const summary = { validated: 0, warning: 0, failed: 0, not_run: 0, total: 0 };
|
|
168
|
+
|
|
169
|
+
if (!status || !status.stories) return summary;
|
|
170
|
+
|
|
171
|
+
for (const story of Object.values(status.stories)) {
|
|
172
|
+
summary.total++;
|
|
173
|
+
const agenticStatus = story.agentic_test_status || 'not_run';
|
|
174
|
+
if (summary[agenticStatus] !== undefined) {
|
|
175
|
+
summary[agenticStatus]++;
|
|
176
|
+
} else {
|
|
177
|
+
summary.not_run++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return summary;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
updateAgenticTestStatus,
|
|
186
|
+
getAgenticTestStatus,
|
|
187
|
+
updateBatchAgenticStatus,
|
|
188
|
+
getAgenticTestSummary,
|
|
189
|
+
readStatusJson,
|
|
190
|
+
writeStatusJson,
|
|
191
|
+
STATUS_FILE,
|
|
192
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* command-prereqs.js
|
|
4
|
+
*
|
|
5
|
+
* Declarative prerequisite checker for AgileFlow commands.
|
|
6
|
+
* Validates that environment signals are met before command execution
|
|
7
|
+
* and provides actionable warnings with fix instructions.
|
|
8
|
+
*
|
|
9
|
+
* Uses:
|
|
10
|
+
* - resolveSignalPath() from feature-catalog.js for signal checking
|
|
11
|
+
* - mtime-based caching pattern from damage-control-utils.js
|
|
12
|
+
* - safeLoad() from yaml-utils.js for YAML parsing
|
|
13
|
+
*
|
|
14
|
+
* All functions fail-open: errors return empty/safe defaults.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// Import resolveSignalPath from feature-catalog
|
|
23
|
+
const { resolveSignalPath } = require('./feature-catalog');
|
|
24
|
+
|
|
25
|
+
// Lazy-load yaml-utils (may not be available in all environments)
|
|
26
|
+
let safeLoad;
|
|
27
|
+
try {
|
|
28
|
+
safeLoad = require('../../lib/yaml-utils').safeLoad;
|
|
29
|
+
} catch {
|
|
30
|
+
// Fallback: no YAML parsing available
|
|
31
|
+
safeLoad = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Inline colors (standalone - no dependency on colors.js for hook compat)
|
|
35
|
+
const c = {
|
|
36
|
+
coral: '\x1b[38;5;203m',
|
|
37
|
+
amber: '\x1b[38;5;215m',
|
|
38
|
+
mintGreen: '\x1b[38;5;158m',
|
|
39
|
+
skyBlue: '\x1b[38;5;117m',
|
|
40
|
+
dim: '\x1b[2m',
|
|
41
|
+
bold: '\x1b[1m',
|
|
42
|
+
reset: '\x1b[0m',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Severity display config
|
|
46
|
+
const SEVERITY = {
|
|
47
|
+
critical: { icon: '\u2718', color: c.coral, label: 'CRITICAL' },
|
|
48
|
+
high: { icon: '!', color: c.amber, label: 'HIGH' },
|
|
49
|
+
medium: { icon: '\u25CB', color: c.dim, label: 'MEDIUM' },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Config Cache (mtime-based invalidation, same pattern as damage-control-utils)
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
const _prereqCache = {
|
|
57
|
+
/** @type {string|null} */ filePath: null,
|
|
58
|
+
/** @type {number} */ mtime: 0,
|
|
59
|
+
/** @type {object|null} */ config: null,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clear the prereq config cache (for testing or forced reload).
|
|
64
|
+
*/
|
|
65
|
+
function clearPrereqCache() {
|
|
66
|
+
_prereqCache.filePath = null;
|
|
67
|
+
_prereqCache.mtime = 0;
|
|
68
|
+
_prereqCache.config = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Config search paths (installed location, then source template)
|
|
72
|
+
const CONFIG_PATHS = [
|
|
73
|
+
'.agileflow/templates/command-prerequisites.yaml',
|
|
74
|
+
'.agileflow/templates/command-prerequisites.yml',
|
|
75
|
+
'.agileflow/config/command-prerequisites.yaml',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load prerequisite configuration from YAML with mtime-based caching.
|
|
80
|
+
* Returns safe defaults on any error (fail-open).
|
|
81
|
+
*
|
|
82
|
+
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
83
|
+
* @returns {{ commands: Object, settings: Object }} Parsed config
|
|
84
|
+
*/
|
|
85
|
+
function loadPrereqConfig(projectRoot) {
|
|
86
|
+
const root = projectRoot || process.cwd();
|
|
87
|
+
const defaultConfig = { commands: {}, settings: { fail_open: true, max_warnings: 5 } };
|
|
88
|
+
|
|
89
|
+
if (!safeLoad) {
|
|
90
|
+
return defaultConfig;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const configPath of CONFIG_PATHS) {
|
|
94
|
+
const fullPath = path.join(root, configPath);
|
|
95
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const stat = fs.statSync(fullPath);
|
|
99
|
+
const mtime = stat.mtimeMs;
|
|
100
|
+
|
|
101
|
+
// Return cached if file hasn't changed
|
|
102
|
+
if (
|
|
103
|
+
_prereqCache.filePath === fullPath &&
|
|
104
|
+
_prereqCache.mtime === mtime &&
|
|
105
|
+
_prereqCache.config
|
|
106
|
+
) {
|
|
107
|
+
return _prereqCache.config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
111
|
+
const parsed = safeLoad(content);
|
|
112
|
+
|
|
113
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const config = {
|
|
118
|
+
commands: parsed.commands || {},
|
|
119
|
+
settings: { ...defaultConfig.settings, ...(parsed.settings || {}) },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Store in cache
|
|
123
|
+
_prereqCache.filePath = fullPath;
|
|
124
|
+
_prereqCache.mtime = mtime;
|
|
125
|
+
_prereqCache.config = config;
|
|
126
|
+
|
|
127
|
+
return config;
|
|
128
|
+
} catch {
|
|
129
|
+
// Continue to next path
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return defaultConfig;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// Prerequisite Checking (pure function)
|
|
138
|
+
// =============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check prerequisites for a specific command against current signals.
|
|
142
|
+
* Pure function - no I/O, no side effects.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} commandName - Command to check (e.g. 'deploy', 'babysit')
|
|
145
|
+
* @param {Object} signals - Extracted signals from smart-detect
|
|
146
|
+
* @param {{ commands: Object, settings: Object }} config - Loaded prereq config
|
|
147
|
+
* @returns {{
|
|
148
|
+
* command: string,
|
|
149
|
+
* hasPrereqs: boolean,
|
|
150
|
+
* allMet: boolean,
|
|
151
|
+
* results: Array<{ signal: string, description: string, fix: string, severity: string, met: boolean }>,
|
|
152
|
+
* unmet: Array<{ signal: string, description: string, fix: string, severity: string }>,
|
|
153
|
+
* criticalUnmet: number,
|
|
154
|
+
* highUnmet: number
|
|
155
|
+
* }}
|
|
156
|
+
*/
|
|
157
|
+
function checkCommandPrereqs(commandName, signals, config) {
|
|
158
|
+
const result = {
|
|
159
|
+
command: commandName,
|
|
160
|
+
hasPrereqs: false,
|
|
161
|
+
allMet: true,
|
|
162
|
+
results: [],
|
|
163
|
+
unmet: [],
|
|
164
|
+
criticalUnmet: 0,
|
|
165
|
+
highUnmet: 0,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!commandName || !config || !config.commands) {
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const commandConfig = config.commands[commandName];
|
|
173
|
+
if (!commandConfig || !Array.isArray(commandConfig.prerequisites)) {
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
result.hasPrereqs = true;
|
|
178
|
+
const prereqs = commandConfig.prerequisites;
|
|
179
|
+
|
|
180
|
+
for (const prereq of prereqs) {
|
|
181
|
+
if (!prereq.signal) continue;
|
|
182
|
+
|
|
183
|
+
const value = resolveSignalPath(signals || {}, prereq.signal);
|
|
184
|
+
// Evaluate signal: empty arrays and empty strings are "unmet"
|
|
185
|
+
// (e.g. git.filesChanged=[] means no changes, which is unmet)
|
|
186
|
+
const met = Array.isArray(value) ? value.length > 0 : !!value;
|
|
187
|
+
|
|
188
|
+
const entry = {
|
|
189
|
+
signal: prereq.signal,
|
|
190
|
+
description: prereq.description || prereq.signal,
|
|
191
|
+
fix: prereq.fix || '',
|
|
192
|
+
severity: prereq.severity || 'medium',
|
|
193
|
+
met,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
result.results.push(entry);
|
|
197
|
+
|
|
198
|
+
if (!met) {
|
|
199
|
+
result.allMet = false;
|
|
200
|
+
result.unmet.push(entry);
|
|
201
|
+
|
|
202
|
+
if (entry.severity === 'critical') {
|
|
203
|
+
result.criticalUnmet++;
|
|
204
|
+
} else if (entry.severity === 'high') {
|
|
205
|
+
result.highUnmet++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Warning Formatting
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Format unmet prerequisite warnings with ANSI colors.
|
|
219
|
+
* Returns empty string if all prerequisites are met.
|
|
220
|
+
*
|
|
221
|
+
* @param {{ allMet: boolean, command: string, unmet: Array, criticalUnmet: number }} checkResult
|
|
222
|
+
* @param {{ max_warnings?: number }} [settings] - Display settings
|
|
223
|
+
* @returns {string} Formatted warning string (with trailing newline) or empty string
|
|
224
|
+
*/
|
|
225
|
+
function formatPrereqWarnings(checkResult, settings) {
|
|
226
|
+
if (!checkResult || checkResult.allMet || checkResult.unmet.length === 0) {
|
|
227
|
+
return '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const maxWarnings = (settings && settings.max_warnings) || 5;
|
|
231
|
+
const { command, unmet, criticalUnmet } = checkResult;
|
|
232
|
+
|
|
233
|
+
const lines = [];
|
|
234
|
+
const headerColor = criticalUnmet > 0 ? c.coral : c.amber;
|
|
235
|
+
const headerIcon = criticalUnmet > 0 ? '\u26A0\uFE0F' : '\u2139\uFE0F';
|
|
236
|
+
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(
|
|
239
|
+
`${headerColor}${c.bold}\u2501\u2501\u2501 ${headerIcon} Command Prerequisites: /${command} \u2501\u2501\u2501${c.reset}`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (criticalUnmet > 0) {
|
|
243
|
+
lines.push(
|
|
244
|
+
`${c.coral}${criticalUnmet} critical prerequisite(s) not met - command may fail${c.reset}`
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
lines.push(
|
|
248
|
+
`${c.amber}${unmet.length} prerequisite(s) not met - results may be suboptimal${c.reset}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lines.push('');
|
|
253
|
+
|
|
254
|
+
const displayed = unmet.slice(0, maxWarnings);
|
|
255
|
+
for (const prereq of displayed) {
|
|
256
|
+
const sev = SEVERITY[prereq.severity] || SEVERITY.medium;
|
|
257
|
+
lines.push(` ${sev.color}${sev.icon} [${sev.label}]${c.reset} ${prereq.description}`);
|
|
258
|
+
if (prereq.fix) {
|
|
259
|
+
lines.push(` ${c.dim}\u2192 Fix: ${prereq.fix}${c.reset}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (unmet.length > maxWarnings) {
|
|
264
|
+
lines.push(` ${c.dim}... and ${unmet.length - maxWarnings} more${c.reset}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push('');
|
|
268
|
+
|
|
269
|
+
return lines.join('\n');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
loadPrereqConfig,
|
|
274
|
+
checkCommandPrereqs,
|
|
275
|
+
formatPrereqWarnings,
|
|
276
|
+
clearPrereqCache,
|
|
277
|
+
// Exported for testing
|
|
278
|
+
CONFIG_PATHS,
|
|
279
|
+
SEVERITY,
|
|
280
|
+
};
|
|
@@ -83,6 +83,13 @@ function detectConfig(version) {
|
|
|
83
83
|
level: null,
|
|
84
84
|
patternCount: 0,
|
|
85
85
|
},
|
|
86
|
+
noaiattribution: {
|
|
87
|
+
enabled: false,
|
|
88
|
+
valid: true,
|
|
89
|
+
issues: [],
|
|
90
|
+
version: null,
|
|
91
|
+
outdated: false,
|
|
92
|
+
},
|
|
86
93
|
askuserquestion: {
|
|
87
94
|
enabled: false,
|
|
88
95
|
valid: true,
|
|
@@ -98,6 +105,22 @@ function detectConfig(version) {
|
|
|
98
105
|
version: null,
|
|
99
106
|
outdated: false,
|
|
100
107
|
},
|
|
108
|
+
browserqa: {
|
|
109
|
+
enabled: false,
|
|
110
|
+
valid: true,
|
|
111
|
+
issues: [],
|
|
112
|
+
version: null,
|
|
113
|
+
outdated: false,
|
|
114
|
+
playwright_detected: false,
|
|
115
|
+
},
|
|
116
|
+
contextverbosity: {
|
|
117
|
+
enabled: false,
|
|
118
|
+
valid: true,
|
|
119
|
+
issues: [],
|
|
120
|
+
version: null,
|
|
121
|
+
outdated: false,
|
|
122
|
+
mode: 'full',
|
|
123
|
+
},
|
|
101
124
|
},
|
|
102
125
|
metadata: { exists: false, version: null },
|
|
103
126
|
currentVersion: version,
|
|
@@ -225,7 +248,7 @@ function detectStopHooks(hook, status) {
|
|
|
225
248
|
}
|
|
226
249
|
|
|
227
250
|
/**
|
|
228
|
-
* Detect PreToolUse hooks (damage control)
|
|
251
|
+
* Detect PreToolUse hooks (damage control, no AI attribution)
|
|
229
252
|
*/
|
|
230
253
|
function detectPreToolUseHooks(hooks, status) {
|
|
231
254
|
if (!Array.isArray(hooks) || hooks.length === 0) return;
|
|
@@ -248,6 +271,17 @@ function detectPreToolUseHooks(hooks, status) {
|
|
|
248
271
|
status.features.damagecontrol.issues.push(`Only ${hookCount}/3 hooks configured`);
|
|
249
272
|
}
|
|
250
273
|
}
|
|
274
|
+
|
|
275
|
+
// Detect no AI attribution hook
|
|
276
|
+
const hasNoAiHook = hooks.some(
|
|
277
|
+
h =>
|
|
278
|
+
h.matcher === 'Bash' &&
|
|
279
|
+
Array.isArray(h.hooks) &&
|
|
280
|
+
h.hooks.some(hk => hk.command?.includes('strip-ai-attribution'))
|
|
281
|
+
);
|
|
282
|
+
if (hasNoAiHook) {
|
|
283
|
+
status.features.noaiattribution.enabled = true;
|
|
284
|
+
}
|
|
251
285
|
}
|
|
252
286
|
|
|
253
287
|
/**
|
|
@@ -303,9 +337,31 @@ function detectMetadata(status, version) {
|
|
|
303
337
|
status.features.tmuxautospawn.enabled = true; // Default enabled
|
|
304
338
|
}
|
|
305
339
|
|
|
340
|
+
// No AI attribution metadata
|
|
341
|
+
if (meta.features?.noaiattribution?.enabled) {
|
|
342
|
+
status.features.noaiattribution.enabled = true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Browser QA metadata
|
|
346
|
+
if (meta.features?.browserqa?.enabled) {
|
|
347
|
+
status.features.browserqa.enabled = true;
|
|
348
|
+
status.features.browserqa.playwright_detected =
|
|
349
|
+
meta.features.browserqa.playwright_detected || false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Context verbosity metadata
|
|
353
|
+
if (meta.features?.contextVerbosity?.enabled) {
|
|
354
|
+
status.features.contextverbosity.enabled = true;
|
|
355
|
+
status.features.contextverbosity.mode = meta.features.contextVerbosity.mode || 'full';
|
|
356
|
+
}
|
|
357
|
+
|
|
306
358
|
// Read feature versions and check if outdated (content-based)
|
|
307
359
|
if (meta.features) {
|
|
308
|
-
const featureKeyMap = {
|
|
360
|
+
const featureKeyMap = {
|
|
361
|
+
askUserQuestion: 'askuserquestion',
|
|
362
|
+
tmuxAutoSpawn: 'tmuxautospawn',
|
|
363
|
+
contextVerbosity: 'contextverbosity',
|
|
364
|
+
};
|
|
309
365
|
const packageScriptDir = findPackageScriptDir();
|
|
310
366
|
|
|
311
367
|
Object.entries(meta.features).forEach(([feature, data]) => {
|
|
@@ -434,6 +490,14 @@ function printStatus(status) {
|
|
|
434
490
|
log(` Damage Control: disabled`, c.dim);
|
|
435
491
|
}
|
|
436
492
|
|
|
493
|
+
// No AI Attribution
|
|
494
|
+
const naa = status.features.noaiattribution;
|
|
495
|
+
if (naa.enabled) {
|
|
496
|
+
log(` No AI Attribution: enabled`, c.green);
|
|
497
|
+
} else {
|
|
498
|
+
log(` No AI Attribution: disabled`, c.dim);
|
|
499
|
+
}
|
|
500
|
+
|
|
437
501
|
// AskUserQuestion
|
|
438
502
|
const auq = status.features.askuserquestion;
|
|
439
503
|
if (auq.enabled) {
|
|
@@ -452,6 +516,32 @@ function printStatus(status) {
|
|
|
452
516
|
log(` Tmux Auto-Spawn: disabled`, c.dim);
|
|
453
517
|
}
|
|
454
518
|
|
|
519
|
+
// Browser QA
|
|
520
|
+
const bqa = status.features.browserqa;
|
|
521
|
+
if (bqa) {
|
|
522
|
+
if (bqa.enabled) {
|
|
523
|
+
let bqaText = 'enabled';
|
|
524
|
+
if (bqa.playwright_detected) bqaText += ' (Playwright detected)';
|
|
525
|
+
else bqaText += ' (Playwright not found)';
|
|
526
|
+
log(
|
|
527
|
+
` ${bqa.playwright_detected ? '' : ''} Browser QA: ${bqaText}`,
|
|
528
|
+
bqa.playwright_detected ? c.green : c.yellow
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
log(` Browser QA: disabled`, c.dim);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Context Verbosity
|
|
536
|
+
const cv = status.features.contextverbosity;
|
|
537
|
+
if (cv) {
|
|
538
|
+
if (cv.enabled) {
|
|
539
|
+
log(` Context Verbosity: ${cv.mode}`, c.green);
|
|
540
|
+
} else {
|
|
541
|
+
log(` Context Verbosity: full (default)`, c.dim);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
455
545
|
// Metadata version
|
|
456
546
|
if (status.metadata.exists) {
|
|
457
547
|
log(`\nMetadata: v${status.metadata.version}`, c.dim);
|