agileflow 2.91.0 → 2.92.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 +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +32 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +491 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +50 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +127 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +408 -55
- package/scripts/spawn-parallel.js +666 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +132 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +95 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +113 -2
- package/tools/cli/lib/ui.js +15 -25
package/lib/consent.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consent.js
|
|
3
|
+
*
|
|
4
|
+
* GDPR consent handling for AgileFlow (US-0149)
|
|
5
|
+
*
|
|
6
|
+
* Manages privacy consent during setup:
|
|
7
|
+
* - Prompts user to acknowledge privacy policy
|
|
8
|
+
* - Stores consent timestamp in .agileflow/config/consent.json
|
|
9
|
+
* - Supports --accept-privacy flag for CI environments
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Consent configuration
|
|
18
|
+
*/
|
|
19
|
+
const CONSENT_FILE = '.agileflow/config/consent.json';
|
|
20
|
+
const PRIVACY_POLICY_URL =
|
|
21
|
+
'https://github.com/projectquestorg/AgileFlow/blob/main/packages/cli/PRIVACY.md';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Consent status types
|
|
25
|
+
*/
|
|
26
|
+
const ConsentStatus = {
|
|
27
|
+
ACCEPTED: 'accepted',
|
|
28
|
+
DECLINED: 'declined',
|
|
29
|
+
PENDING: 'pending',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if consent has been given
|
|
34
|
+
* @returns {{ hasConsent: boolean, consent: Object | null }}
|
|
35
|
+
*/
|
|
36
|
+
function checkConsent() {
|
|
37
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(consentPath)) {
|
|
41
|
+
const consent = JSON.parse(fs.readFileSync(consentPath, 'utf8'));
|
|
42
|
+
return {
|
|
43
|
+
hasConsent: consent.status === ConsentStatus.ACCEPTED,
|
|
44
|
+
consent,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore errors, treat as no consent
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { hasConsent: false, consent: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Record consent
|
|
56
|
+
* @param {string} status - 'accepted' or 'declined'
|
|
57
|
+
* @param {Object} options - Additional options
|
|
58
|
+
* @param {string} options.method - How consent was given ('interactive', 'flag', 'api')
|
|
59
|
+
* @param {string} options.version - Privacy policy version
|
|
60
|
+
* @returns {{ ok: boolean, path: string }}
|
|
61
|
+
*/
|
|
62
|
+
function recordConsent(status, options = {}) {
|
|
63
|
+
const { method = 'interactive', version = '1.0.0' } = options;
|
|
64
|
+
|
|
65
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
66
|
+
const consentDir = path.dirname(consentPath);
|
|
67
|
+
|
|
68
|
+
// Ensure directory exists
|
|
69
|
+
if (!fs.existsSync(consentDir)) {
|
|
70
|
+
fs.mkdirSync(consentDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const consent = {
|
|
74
|
+
status,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
method,
|
|
77
|
+
policy_version: version,
|
|
78
|
+
policy_url: PRIVACY_POLICY_URL,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
fs.writeFileSync(consentPath, JSON.stringify(consent, null, 2));
|
|
83
|
+
return { ok: true, path: consentPath };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return { ok: false, error: err.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prompt user for consent interactively
|
|
91
|
+
* @param {Object} options - Prompt options
|
|
92
|
+
* @param {WritableStream} options.output - Output stream (default: process.stdout)
|
|
93
|
+
* @param {ReadableStream} options.input - Input stream (default: process.stdin)
|
|
94
|
+
* @returns {Promise<{ accepted: boolean }>}
|
|
95
|
+
*/
|
|
96
|
+
async function promptConsent(options = {}) {
|
|
97
|
+
const { output = process.stdout, input = process.stdin } = options;
|
|
98
|
+
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input,
|
|
101
|
+
output,
|
|
102
|
+
terminal: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Display privacy notice
|
|
106
|
+
const notice = `
|
|
107
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
108
|
+
┃ Privacy Notice ┃
|
|
109
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
110
|
+
|
|
111
|
+
AgileFlow respects your privacy:
|
|
112
|
+
|
|
113
|
+
• All data is stored locally on your machine
|
|
114
|
+
• No telemetry, analytics, or tracking
|
|
115
|
+
• No data is transmitted to external servers
|
|
116
|
+
• You can delete all data at any time
|
|
117
|
+
|
|
118
|
+
For details, see: ${PRIVACY_POLICY_URL}
|
|
119
|
+
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
output.write(notice);
|
|
123
|
+
|
|
124
|
+
return new Promise(resolve => {
|
|
125
|
+
const question = 'Do you accept the privacy policy? (yes/no): ';
|
|
126
|
+
output.write(question);
|
|
127
|
+
|
|
128
|
+
rl.once('line', answer => {
|
|
129
|
+
rl.close();
|
|
130
|
+
const normalized = answer.toLowerCase().trim();
|
|
131
|
+
const accepted = normalized === 'yes' || normalized === 'y';
|
|
132
|
+
resolve({ accepted });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle consent during setup
|
|
139
|
+
* @param {Object} options - Setup options
|
|
140
|
+
* @param {boolean} options.acceptPrivacy - --accept-privacy flag was passed
|
|
141
|
+
* @param {boolean} options.silent - Silent mode (no prompts)
|
|
142
|
+
* @param {WritableStream} options.output - Output stream
|
|
143
|
+
* @param {ReadableStream} options.input - Input stream
|
|
144
|
+
* @returns {Promise<{ ok: boolean, status: string, skipped: boolean }>}
|
|
145
|
+
*/
|
|
146
|
+
async function handleSetupConsent(options = {}) {
|
|
147
|
+
const {
|
|
148
|
+
acceptPrivacy = false,
|
|
149
|
+
silent = false,
|
|
150
|
+
output = process.stdout,
|
|
151
|
+
input = process.stdin,
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
// Check if consent already given
|
|
155
|
+
const { hasConsent, consent } = checkConsent();
|
|
156
|
+
if (hasConsent) {
|
|
157
|
+
return { ok: true, status: 'already_consented', skipped: false, consent };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If --accept-privacy flag provided
|
|
161
|
+
if (acceptPrivacy) {
|
|
162
|
+
const result = recordConsent(ConsentStatus.ACCEPTED, { method: 'flag' });
|
|
163
|
+
return { ok: result.ok, status: 'accepted_via_flag', skipped: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If silent mode (CI without flag)
|
|
167
|
+
if (silent) {
|
|
168
|
+
return { ok: false, status: 'consent_required', skipped: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Interactive prompt
|
|
172
|
+
const { accepted } = await promptConsent({ output, input });
|
|
173
|
+
|
|
174
|
+
if (accepted) {
|
|
175
|
+
const result = recordConsent(ConsentStatus.ACCEPTED, { method: 'interactive' });
|
|
176
|
+
return { ok: result.ok, status: 'accepted_interactive', skipped: false };
|
|
177
|
+
} else {
|
|
178
|
+
const result = recordConsent(ConsentStatus.DECLINED, { method: 'interactive' });
|
|
179
|
+
return { ok: false, status: 'declined', skipped: false };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get consent status for display
|
|
185
|
+
* @returns {{ status: string, timestamp: string | null, method: string | null }}
|
|
186
|
+
*/
|
|
187
|
+
function getConsentStatus() {
|
|
188
|
+
const { hasConsent, consent } = checkConsent();
|
|
189
|
+
|
|
190
|
+
if (!consent) {
|
|
191
|
+
return { status: ConsentStatus.PENDING, timestamp: null, method: null };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
status: consent.status,
|
|
196
|
+
timestamp: consent.timestamp,
|
|
197
|
+
method: consent.method,
|
|
198
|
+
policyVersion: consent.policy_version,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Revoke consent (delete consent file)
|
|
204
|
+
* @returns {{ ok: boolean }}
|
|
205
|
+
*/
|
|
206
|
+
function revokeConsent() {
|
|
207
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
if (fs.existsSync(consentPath)) {
|
|
211
|
+
fs.unlinkSync(consentPath);
|
|
212
|
+
}
|
|
213
|
+
return { ok: true };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return { ok: false, error: err.message };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
// Constants
|
|
221
|
+
CONSENT_FILE,
|
|
222
|
+
PRIVACY_POLICY_URL,
|
|
223
|
+
ConsentStatus,
|
|
224
|
+
|
|
225
|
+
// Functions
|
|
226
|
+
checkConsent,
|
|
227
|
+
recordConsent,
|
|
228
|
+
promptConsent,
|
|
229
|
+
handleSetupConsent,
|
|
230
|
+
getConsentStatus,
|
|
231
|
+
revokeConsent,
|
|
232
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Correlation ID Management
|
|
3
|
+
*
|
|
4
|
+
* Generates and propagates trace IDs and session IDs for event correlation.
|
|
5
|
+
* Enables filtering and tracing events across complex workflows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Global context for correlation IDs
|
|
13
|
+
let currentContext = {
|
|
14
|
+
traceId: null,
|
|
15
|
+
sessionId: null,
|
|
16
|
+
spanId: null,
|
|
17
|
+
parentSpanId: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a unique trace ID (16 hex chars, like OpenTelemetry standard)
|
|
22
|
+
* @returns {string} Unique trace ID
|
|
23
|
+
*/
|
|
24
|
+
function generateTraceId() {
|
|
25
|
+
return crypto.randomBytes(8).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a span ID (8 hex chars)
|
|
30
|
+
* @returns {string} Unique span ID
|
|
31
|
+
*/
|
|
32
|
+
function generateSpanId() {
|
|
33
|
+
return crypto.randomBytes(4).toString('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a session ID based on timestamp and random component
|
|
38
|
+
* Format: session_YYYYMMDD_HHMMSS_XXXX
|
|
39
|
+
* @returns {string} Session ID
|
|
40
|
+
*/
|
|
41
|
+
function generateSessionId() {
|
|
42
|
+
const now = new Date();
|
|
43
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
44
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
45
|
+
const random = crypto.randomBytes(2).toString('hex');
|
|
46
|
+
return `session_${date}_${time}_${random}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current correlation context
|
|
51
|
+
* @returns {{ traceId: string | null, sessionId: string | null, spanId: string | null }}
|
|
52
|
+
*/
|
|
53
|
+
function getContext() {
|
|
54
|
+
return { ...currentContext };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set correlation context
|
|
59
|
+
* @param {Object} context
|
|
60
|
+
* @param {string} [context.traceId] - Trace ID
|
|
61
|
+
* @param {string} [context.sessionId] - Session ID
|
|
62
|
+
* @param {string} [context.spanId] - Span ID
|
|
63
|
+
* @param {string} [context.parentSpanId] - Parent span ID
|
|
64
|
+
*/
|
|
65
|
+
function setContext(context) {
|
|
66
|
+
if (context.traceId !== undefined) currentContext.traceId = context.traceId;
|
|
67
|
+
if (context.sessionId !== undefined) currentContext.sessionId = context.sessionId;
|
|
68
|
+
if (context.spanId !== undefined) currentContext.spanId = context.spanId;
|
|
69
|
+
if (context.parentSpanId !== undefined) currentContext.parentSpanId = context.parentSpanId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start a new trace (generates new trace_id)
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @param {string} [options.sessionId] - Existing session ID to use
|
|
76
|
+
* @returns {{ traceId: string, sessionId: string, spanId: string }}
|
|
77
|
+
*/
|
|
78
|
+
function startTrace(options = {}) {
|
|
79
|
+
const traceId = generateTraceId();
|
|
80
|
+
const sessionId = options.sessionId || currentContext.sessionId || generateSessionId();
|
|
81
|
+
const spanId = generateSpanId();
|
|
82
|
+
|
|
83
|
+
setContext({
|
|
84
|
+
traceId,
|
|
85
|
+
sessionId,
|
|
86
|
+
spanId,
|
|
87
|
+
parentSpanId: null,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { traceId, sessionId, spanId };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Start a new span within current trace
|
|
95
|
+
* @returns {{ traceId: string, sessionId: string, spanId: string, parentSpanId: string | null }}
|
|
96
|
+
*/
|
|
97
|
+
function startSpan() {
|
|
98
|
+
const parentSpanId = currentContext.spanId;
|
|
99
|
+
const spanId = generateSpanId();
|
|
100
|
+
|
|
101
|
+
setContext({
|
|
102
|
+
spanId,
|
|
103
|
+
parentSpanId,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
traceId: currentContext.traceId,
|
|
108
|
+
sessionId: currentContext.sessionId,
|
|
109
|
+
spanId,
|
|
110
|
+
parentSpanId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Inject correlation IDs into an event object
|
|
116
|
+
* @param {Object} event - Event to inject into
|
|
117
|
+
* @returns {Object} Event with correlation IDs
|
|
118
|
+
*/
|
|
119
|
+
function injectCorrelation(event) {
|
|
120
|
+
const ctx = getContext();
|
|
121
|
+
return {
|
|
122
|
+
...event,
|
|
123
|
+
...(ctx.traceId && { trace_id: ctx.traceId }),
|
|
124
|
+
...(ctx.sessionId && { session_id: ctx.sessionId }),
|
|
125
|
+
...(ctx.spanId && { span_id: ctx.spanId }),
|
|
126
|
+
...(ctx.parentSpanId && { parent_span_id: ctx.parentSpanId }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear correlation context (for testing or new sessions)
|
|
132
|
+
*/
|
|
133
|
+
function clearContext() {
|
|
134
|
+
currentContext = {
|
|
135
|
+
traceId: null,
|
|
136
|
+
sessionId: null,
|
|
137
|
+
spanId: null,
|
|
138
|
+
parentSpanId: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load session ID from session-state.json if available
|
|
144
|
+
* @param {string} projectDir - Project directory
|
|
145
|
+
* @returns {string | null} Session ID or null
|
|
146
|
+
*/
|
|
147
|
+
function loadSessionId(projectDir) {
|
|
148
|
+
try {
|
|
149
|
+
const sessionStatePath = path.join(projectDir, 'docs', '09-agents', 'session-state.json');
|
|
150
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
151
|
+
const data = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
152
|
+
return data.session_id || null;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore errors
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Save session ID to session-state.json
|
|
162
|
+
* @param {string} projectDir - Project directory
|
|
163
|
+
* @param {string} sessionId - Session ID to save
|
|
164
|
+
*/
|
|
165
|
+
function saveSessionId(projectDir, sessionId) {
|
|
166
|
+
try {
|
|
167
|
+
const sessionStatePath = path.join(projectDir, 'docs', '09-agents', 'session-state.json');
|
|
168
|
+
let data = {};
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
171
|
+
data = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
data.session_id = sessionId;
|
|
175
|
+
data.updated_at = new Date().toISOString();
|
|
176
|
+
|
|
177
|
+
const dir = path.dirname(sessionStatePath);
|
|
178
|
+
if (!fs.existsSync(dir)) {
|
|
179
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(data, null, 2) + '\n');
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore errors - session ID is not critical
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Initialize correlation context for a project
|
|
190
|
+
* Loads existing session ID or creates new one
|
|
191
|
+
* @param {string} projectDir - Project directory
|
|
192
|
+
* @param {Object} [options]
|
|
193
|
+
* @param {boolean} [options.newTrace=true] - Start new trace
|
|
194
|
+
* @returns {{ traceId: string, sessionId: string }}
|
|
195
|
+
*/
|
|
196
|
+
function initializeForProject(projectDir, options = {}) {
|
|
197
|
+
const { newTrace = true } = options;
|
|
198
|
+
|
|
199
|
+
// Load or create session ID
|
|
200
|
+
let sessionId = loadSessionId(projectDir);
|
|
201
|
+
if (!sessionId) {
|
|
202
|
+
sessionId = generateSessionId();
|
|
203
|
+
saveSessionId(projectDir, sessionId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setContext({ sessionId });
|
|
207
|
+
|
|
208
|
+
if (newTrace) {
|
|
209
|
+
const trace = startTrace({ sessionId });
|
|
210
|
+
return { traceId: trace.traceId, sessionId };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
traceId: currentContext.traceId || generateTraceId(),
|
|
215
|
+
sessionId,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extract correlation IDs from an event
|
|
221
|
+
* @param {Object} event - Event object
|
|
222
|
+
* @returns {{ traceId: string | null, sessionId: string | null, spanId: string | null }}
|
|
223
|
+
*/
|
|
224
|
+
function extractCorrelation(event) {
|
|
225
|
+
return {
|
|
226
|
+
traceId: event.trace_id || null,
|
|
227
|
+
sessionId: event.session_id || null,
|
|
228
|
+
spanId: event.span_id || null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Filter events by trace ID
|
|
234
|
+
* @param {Array} events - Array of events
|
|
235
|
+
* @param {string} traceId - Trace ID to filter by
|
|
236
|
+
* @returns {Array} Filtered events
|
|
237
|
+
*/
|
|
238
|
+
function filterByTraceId(events, traceId) {
|
|
239
|
+
return events.filter(e => e.trace_id === traceId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Filter events by session ID
|
|
244
|
+
* @param {Array} events - Array of events
|
|
245
|
+
* @param {string} sessionId - Session ID to filter by
|
|
246
|
+
* @returns {Array} Filtered events
|
|
247
|
+
*/
|
|
248
|
+
function filterBySessionId(events, sessionId) {
|
|
249
|
+
return events.filter(e => e.session_id === sessionId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
// ID generation
|
|
254
|
+
generateTraceId,
|
|
255
|
+
generateSpanId,
|
|
256
|
+
generateSessionId,
|
|
257
|
+
|
|
258
|
+
// Context management
|
|
259
|
+
getContext,
|
|
260
|
+
setContext,
|
|
261
|
+
clearContext,
|
|
262
|
+
startTrace,
|
|
263
|
+
startSpan,
|
|
264
|
+
|
|
265
|
+
// Event injection/extraction
|
|
266
|
+
injectCorrelation,
|
|
267
|
+
extractCorrelation,
|
|
268
|
+
|
|
269
|
+
// Project integration
|
|
270
|
+
initializeForProject,
|
|
271
|
+
loadSessionId,
|
|
272
|
+
saveSessionId,
|
|
273
|
+
|
|
274
|
+
// Filtering
|
|
275
|
+
filterByTraceId,
|
|
276
|
+
filterBySessionId,
|
|
277
|
+
};
|
package/lib/error-codes.js
CHANGED
|
@@ -524,6 +524,48 @@ function formatError(error, options = {}) {
|
|
|
524
524
|
return lines.join('\n');
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Create an error result with standardized metadata
|
|
529
|
+
* This factory produces Result objects compatible with result-schema.js
|
|
530
|
+
* @param {string} errorCode - Error code (e.g., 'ENOENT', 'ECONFIG')
|
|
531
|
+
* @param {string} [message] - Optional custom error message
|
|
532
|
+
* @param {Object} [options] - Additional options
|
|
533
|
+
* @param {Object} [options.context] - Additional context about the error
|
|
534
|
+
* @param {Error} [options.cause] - Original error that caused this failure
|
|
535
|
+
* @returns {{ ok: false, error: string, errorCode: string, severity: string, category: string, recoverable: boolean, suggestedFix: string, autoFix: string|null, context?: Object, cause?: Error }}
|
|
536
|
+
*/
|
|
537
|
+
function createErrorResult(errorCode, message, options = {}) {
|
|
538
|
+
const codeData = ErrorCodes[errorCode] || ErrorCodes.EUNKNOWN;
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
error: message || codeData.message,
|
|
543
|
+
errorCode: codeData.code,
|
|
544
|
+
severity: codeData.severity,
|
|
545
|
+
category: codeData.category,
|
|
546
|
+
recoverable: codeData.recoverable,
|
|
547
|
+
suggestedFix: codeData.suggestedFix,
|
|
548
|
+
autoFix: codeData.autoFix || null,
|
|
549
|
+
context: options.context,
|
|
550
|
+
cause: options.cause,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Create a success result
|
|
556
|
+
* @template T
|
|
557
|
+
* @param {T} data - The success data
|
|
558
|
+
* @param {Object} [meta] - Optional metadata
|
|
559
|
+
* @returns {{ ok: true, data: T }}
|
|
560
|
+
*/
|
|
561
|
+
function createSuccessResult(data, meta = {}) {
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
data,
|
|
565
|
+
...meta,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
527
569
|
module.exports = {
|
|
528
570
|
// Enums
|
|
529
571
|
Severity,
|
|
@@ -541,4 +583,8 @@ module.exports = {
|
|
|
541
583
|
getSuggestedFix,
|
|
542
584
|
getAutoFix,
|
|
543
585
|
formatError,
|
|
586
|
+
|
|
587
|
+
// Result factories (for result-schema.js compatibility)
|
|
588
|
+
createErrorResult,
|
|
589
|
+
createSuccessResult,
|
|
544
590
|
};
|
package/lib/errors.js
CHANGED
|
@@ -236,10 +236,13 @@ function getCharName(char) {
|
|
|
236
236
|
* @param {string} filePath - Absolute path to JSON file
|
|
237
237
|
* @param {object} options - Optional settings
|
|
238
238
|
* @param {*} options.defaultValue - Value to return if file doesn't exist (makes missing file not an error)
|
|
239
|
-
* @
|
|
239
|
+
* @param {boolean} options.hidePathInError - Hide file path in error messages (security: default false)
|
|
240
|
+
* @param {string} options.context - Context for error messages (alternative to exposing path)
|
|
241
|
+
* @returns {{ ok: boolean, data?: any, error?: string, errorCode?: string }}
|
|
240
242
|
*/
|
|
241
243
|
function safeReadJSON(filePath, options = {}) {
|
|
242
|
-
const { defaultValue } = options;
|
|
244
|
+
const { defaultValue, hidePathInError = false, context } = options;
|
|
245
|
+
const pathDisplay = hidePathInError ? context || 'configuration file' : filePath;
|
|
243
246
|
|
|
244
247
|
try {
|
|
245
248
|
if (!fs.existsSync(filePath)) {
|
|
@@ -247,19 +250,58 @@ function safeReadJSON(filePath, options = {}) {
|
|
|
247
250
|
debugLog('safeReadJSON', { filePath, status: 'missing, using default' });
|
|
248
251
|
return { ok: true, data: defaultValue };
|
|
249
252
|
}
|
|
250
|
-
const error = `File not found: ${
|
|
253
|
+
const error = `File not found: ${pathDisplay}`;
|
|
251
254
|
debugLog('safeReadJSON', { filePath, error });
|
|
252
|
-
return { ok: false, error };
|
|
255
|
+
return { ok: false, error, errorCode: 'ENOENT' };
|
|
253
256
|
}
|
|
254
257
|
|
|
255
258
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Handle empty files gracefully
|
|
261
|
+
if (!content.trim()) {
|
|
262
|
+
if (defaultValue !== undefined) {
|
|
263
|
+
debugLog('safeReadJSON', { filePath, status: 'empty, using default' });
|
|
264
|
+
return { ok: true, data: defaultValue };
|
|
265
|
+
}
|
|
266
|
+
const error = `File is empty: ${pathDisplay}`;
|
|
267
|
+
debugLog('safeReadJSON', { filePath, error: 'empty file' });
|
|
268
|
+
return { ok: false, error, errorCode: 'EPARSE' };
|
|
269
|
+
}
|
|
270
|
+
|
|
256
271
|
const data = JSON.parse(content);
|
|
257
272
|
debugLog('safeReadJSON', { filePath, status: 'success' });
|
|
258
273
|
return { ok: true, data };
|
|
259
274
|
} catch (err) {
|
|
260
|
-
|
|
275
|
+
// Detect specific JSON parse errors
|
|
276
|
+
let errorCode = 'EPARSE';
|
|
277
|
+
let errorMessage = `Failed to parse JSON`;
|
|
278
|
+
|
|
279
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
280
|
+
errorCode = 'EACCES';
|
|
281
|
+
errorMessage = `Permission denied reading file`;
|
|
282
|
+
} else if (err.message && err.message.includes('Unexpected')) {
|
|
283
|
+
// JSON syntax error - don't expose full content
|
|
284
|
+
errorMessage = `Invalid JSON syntax`;
|
|
285
|
+
if (err.message.includes('position')) {
|
|
286
|
+
// Extract position info but not content
|
|
287
|
+
const posMatch = err.message.match(/position (\d+)/);
|
|
288
|
+
if (posMatch) {
|
|
289
|
+
errorMessage = `Invalid JSON syntax near position ${posMatch[1]}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} else if (err.message && err.message.includes('token')) {
|
|
293
|
+
errorMessage = `Invalid JSON: unexpected token`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Add context if not hiding path
|
|
297
|
+
if (!hidePathInError) {
|
|
298
|
+
errorMessage = `${errorMessage} in ${pathDisplay}`;
|
|
299
|
+
} else if (context) {
|
|
300
|
+
errorMessage = `${errorMessage} in ${context}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
261
303
|
debugLog('safeReadJSON', { filePath, error: err.message });
|
|
262
|
-
return { ok: false, error };
|
|
304
|
+
return { ok: false, error: errorMessage, errorCode };
|
|
263
305
|
}
|
|
264
306
|
}
|
|
265
307
|
|