@paths.design/caws-cli 8.2.0 → 8.2.3
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/dist/budget-derivation.js +10 -10
- package/dist/commands/archive.js +22 -22
- package/dist/commands/burnup.js +7 -7
- package/dist/commands/diagnose.js +25 -25
- package/dist/commands/evaluate.js +20 -20
- package/dist/commands/init.js +71 -72
- package/dist/commands/iterate.js +21 -21
- package/dist/commands/mode.js +11 -11
- package/dist/commands/plan.js +5 -5
- package/dist/commands/provenance.js +86 -86
- package/dist/commands/quality-gates.js +4 -4
- package/dist/commands/quality-monitor.js +17 -17
- package/dist/commands/session.js +312 -0
- package/dist/commands/specs.js +44 -44
- package/dist/commands/status.js +43 -43
- package/dist/commands/templates.js +14 -14
- package/dist/commands/tool.js +1 -1
- package/dist/commands/troubleshoot.js +11 -11
- package/dist/commands/tutorial.js +119 -119
- package/dist/commands/validate.js +6 -6
- package/dist/commands/waivers.js +93 -60
- package/dist/commands/workflow.js +17 -17
- package/dist/commands/worktree.js +13 -13
- package/dist/config/index.js +5 -5
- package/dist/config/modes.js +7 -7
- package/dist/constants/spec-types.js +5 -5
- package/dist/error-handler.js +4 -4
- package/dist/generators/jest-config-generator.js +3 -3
- package/dist/generators/working-spec.js +4 -4
- package/dist/index.js +79 -27
- package/dist/minimal-cli.js +9 -9
- package/dist/policy/PolicyManager.js +1 -1
- package/dist/scaffold/claude-hooks.js +7 -7
- package/dist/scaffold/cursor-hooks.js +8 -8
- package/dist/scaffold/git-hooks.js +152 -152
- package/dist/scaffold/index.js +48 -48
- package/dist/session/session-manager.js +548 -0
- package/dist/test-analysis.js +20 -20
- package/dist/utils/command-wrapper.js +8 -8
- package/dist/utils/detection.js +7 -7
- package/dist/utils/finalization.js +21 -21
- package/dist/utils/git-lock.js +3 -3
- package/dist/utils/gitignore-updater.js +1 -1
- package/dist/utils/project-analysis.js +7 -7
- package/dist/utils/quality-gates-utils.js +35 -35
- package/dist/utils/spec-resolver.js +8 -8
- package/dist/utils/typescript-detector.js +5 -5
- package/dist/utils/yaml-validation.js +1 -1
- package/dist/validation/spec-validation.js +4 -4
- package/dist/worktree/worktree-manager.js +11 -5
- package/package.json +1 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Session Capsule Manager
|
|
3
|
+
* Manages session lifecycle and capsule persistence for multi-agent coordination.
|
|
4
|
+
* Each session produces a structured capsule that captures baseline state on entry
|
|
5
|
+
* and work summary + verification evidence on exit.
|
|
6
|
+
* @author @darianrosebrook
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
const SESSIONS_DIR = '.caws/sessions';
|
|
15
|
+
const REGISTRY_FILE = '.caws/sessions.json';
|
|
16
|
+
const CAPSULE_SCHEMA_VERSION = 'caws.capsule.v1';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the git repository root
|
|
20
|
+
* @returns {string} Absolute path to repo root
|
|
21
|
+
*/
|
|
22
|
+
function getRepoRoot() {
|
|
23
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
}).trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get current HEAD revision (short hash)
|
|
30
|
+
* @param {string} cwd - Working directory
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function getHeadRev(cwd) {
|
|
34
|
+
try {
|
|
35
|
+
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
cwd,
|
|
38
|
+
}).trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get current branch name
|
|
46
|
+
* @param {string} cwd - Working directory
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function getCurrentBranch(cwd) {
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
cwd,
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return 'detached';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get dirty files in working tree
|
|
62
|
+
* @param {string} cwd - Working directory
|
|
63
|
+
* @returns {{ paths: string[], dirty: boolean }}
|
|
64
|
+
*/
|
|
65
|
+
function getWorkspaceFingerprint(cwd) {
|
|
66
|
+
try {
|
|
67
|
+
const output = execFileSync('git', ['status', '--porcelain'], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
cwd,
|
|
70
|
+
});
|
|
71
|
+
const paths = output
|
|
72
|
+
.split('\n')
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.map((line) => line.substring(3).trim());
|
|
75
|
+
return { paths_touched: paths, dirty: paths.length > 0 };
|
|
76
|
+
} catch {
|
|
77
|
+
return { paths_touched: [], dirty: false };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get project name from working spec or directory
|
|
83
|
+
* @param {string} root - Repository root
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function getProjectName(root) {
|
|
87
|
+
try {
|
|
88
|
+
const yaml = require('js-yaml');
|
|
89
|
+
const specPath = path.join(root, '.caws/working-spec.yaml');
|
|
90
|
+
if (fs.existsSync(specPath)) {
|
|
91
|
+
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
92
|
+
return spec.title || spec.id || path.basename(root);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Fall through
|
|
96
|
+
}
|
|
97
|
+
return path.basename(root);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get skein ID from working spec
|
|
102
|
+
* @param {string} root - Repository root
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
function getSkeinId(root) {
|
|
106
|
+
try {
|
|
107
|
+
const yaml = require('js-yaml');
|
|
108
|
+
const specPath = path.join(root, '.caws/working-spec.yaml');
|
|
109
|
+
if (fs.existsSync(specPath)) {
|
|
110
|
+
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
111
|
+
return spec.id || 'unknown';
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Fall through
|
|
115
|
+
}
|
|
116
|
+
return 'unknown';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load the session registry
|
|
121
|
+
* @param {string} root - Repository root
|
|
122
|
+
* @returns {Object} Registry object
|
|
123
|
+
*/
|
|
124
|
+
function loadRegistry(root) {
|
|
125
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(registryPath)) {
|
|
128
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Corrupted registry, start fresh
|
|
132
|
+
}
|
|
133
|
+
return { version: 1, sessions: {} };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Save the session registry
|
|
138
|
+
* @param {string} root - Repository root
|
|
139
|
+
* @param {Object} registry - Registry object
|
|
140
|
+
*/
|
|
141
|
+
function saveRegistry(root, registry) {
|
|
142
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
143
|
+
fs.ensureDirSync(path.dirname(registryPath));
|
|
144
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate a deterministic session ID
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
function generateSessionId() {
|
|
152
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
153
|
+
const suffix = crypto.randomBytes(4).toString('hex');
|
|
154
|
+
return `${timestamp}__${suffix}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Start a new session, creating the initial capsule with baseline state
|
|
159
|
+
* @param {Object} options - Session options
|
|
160
|
+
* @param {string} [options.role] - Agent role (worker, integrator, qa)
|
|
161
|
+
* @param {string} [options.specId] - Associated feature spec ID
|
|
162
|
+
* @param {string[]} [options.allowedGlobs] - Allowed file patterns
|
|
163
|
+
* @param {string[]} [options.forbiddenGlobs] - Forbidden file patterns
|
|
164
|
+
* @param {string} [options.intent] - What this session intends to accomplish
|
|
165
|
+
* @returns {Object} Created capsule
|
|
166
|
+
*/
|
|
167
|
+
function startSession(options = {}) {
|
|
168
|
+
const root = getRepoRoot();
|
|
169
|
+
const registry = loadRegistry(root);
|
|
170
|
+
const sessionId = generateSessionId();
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
role = 'worker',
|
|
174
|
+
specId,
|
|
175
|
+
allowedGlobs = [],
|
|
176
|
+
forbiddenGlobs = [],
|
|
177
|
+
intent = '',
|
|
178
|
+
} = options;
|
|
179
|
+
|
|
180
|
+
// Build scope from spec if available and no explicit globs provided
|
|
181
|
+
let scope = {
|
|
182
|
+
allowed_globs: allowedGlobs,
|
|
183
|
+
forbidden_globs: forbiddenGlobs,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (specId && allowedGlobs.length === 0) {
|
|
187
|
+
try {
|
|
188
|
+
const yaml = require('js-yaml');
|
|
189
|
+
const specPath = path.join(root, `.caws/specs/${specId}.yaml`);
|
|
190
|
+
if (fs.existsSync(specPath)) {
|
|
191
|
+
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
192
|
+
if (spec.scope) {
|
|
193
|
+
scope.allowed_globs = spec.scope.in || [];
|
|
194
|
+
scope.forbidden_globs = spec.scope.out || [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Non-fatal: scope stays as provided
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const capsule = {
|
|
203
|
+
schema: CAPSULE_SCHEMA_VERSION,
|
|
204
|
+
project: getProjectName(root),
|
|
205
|
+
skein_id: getSkeinId(root),
|
|
206
|
+
session_id: sessionId,
|
|
207
|
+
role,
|
|
208
|
+
spec_id: specId || null,
|
|
209
|
+
scope,
|
|
210
|
+
base_state: {
|
|
211
|
+
head_rev: getHeadRev(root),
|
|
212
|
+
branch: getCurrentBranch(root),
|
|
213
|
+
workspace_fingerprint: getWorkspaceFingerprint(root),
|
|
214
|
+
},
|
|
215
|
+
started_at: new Date().toISOString(),
|
|
216
|
+
ended_at: null,
|
|
217
|
+
work_summary: {
|
|
218
|
+
intent: intent || '',
|
|
219
|
+
paths_touched: [],
|
|
220
|
+
artifacts_written: [],
|
|
221
|
+
commits: [],
|
|
222
|
+
},
|
|
223
|
+
verification: {
|
|
224
|
+
tests_run: [],
|
|
225
|
+
determinism_checks: [],
|
|
226
|
+
},
|
|
227
|
+
known_issues: [],
|
|
228
|
+
handoff: {
|
|
229
|
+
next_actions: [],
|
|
230
|
+
risk_notes: [],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Persist capsule
|
|
235
|
+
const sessionsDir = path.join(root, SESSIONS_DIR);
|
|
236
|
+
fs.ensureDirSync(sessionsDir);
|
|
237
|
+
const capsulePath = path.join(sessionsDir, `${sessionId}.json`);
|
|
238
|
+
fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
|
|
239
|
+
|
|
240
|
+
// Update registry
|
|
241
|
+
registry.sessions[sessionId] = {
|
|
242
|
+
path: `${sessionId}.json`,
|
|
243
|
+
role,
|
|
244
|
+
spec_id: specId || null,
|
|
245
|
+
status: 'active',
|
|
246
|
+
started_at: capsule.started_at,
|
|
247
|
+
ended_at: null,
|
|
248
|
+
head_rev: capsule.base_state.head_rev,
|
|
249
|
+
branch: capsule.base_state.branch,
|
|
250
|
+
};
|
|
251
|
+
saveRegistry(root, registry);
|
|
252
|
+
|
|
253
|
+
return capsule;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add a checkpoint to the current (most recent active) session
|
|
258
|
+
* @param {Object} data - Checkpoint data
|
|
259
|
+
* @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
|
|
260
|
+
* @param {string[]} [data.pathsTouched] - Files changed
|
|
261
|
+
* @param {string[]} [data.artifactsWritten] - Generated artifacts
|
|
262
|
+
* @param {Object[]} [data.testsRun] - Test results { name, status, evidence }
|
|
263
|
+
* @param {Object[]} [data.determinismChecks] - Determinism checks { name, status, total }
|
|
264
|
+
* @param {Object[]} [data.knownIssues] - Issues discovered { type, description }
|
|
265
|
+
* @param {string} [data.intent] - Updated intent description
|
|
266
|
+
* @returns {Object} Updated capsule
|
|
267
|
+
*/
|
|
268
|
+
function checkpointSession(data = {}) {
|
|
269
|
+
const root = getRepoRoot();
|
|
270
|
+
const registry = loadRegistry(root);
|
|
271
|
+
|
|
272
|
+
// Find session
|
|
273
|
+
const sessionId = data.sessionId || findActiveSession(registry);
|
|
274
|
+
if (!sessionId) {
|
|
275
|
+
throw new Error('No active session found. Start one with: caws session start');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
|
|
279
|
+
if (!fs.existsSync(capsulePath)) {
|
|
280
|
+
throw new Error(`Session capsule not found: ${sessionId}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const capsule = JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
|
|
284
|
+
|
|
285
|
+
// Merge checkpoint data
|
|
286
|
+
if (data.intent) {
|
|
287
|
+
capsule.work_summary.intent = data.intent;
|
|
288
|
+
}
|
|
289
|
+
if (data.pathsTouched) {
|
|
290
|
+
const existing = new Set(capsule.work_summary.paths_touched);
|
|
291
|
+
for (const p of data.pathsTouched) existing.add(p);
|
|
292
|
+
capsule.work_summary.paths_touched = [...existing];
|
|
293
|
+
}
|
|
294
|
+
if (data.artifactsWritten) {
|
|
295
|
+
const existing = new Set(capsule.work_summary.artifacts_written);
|
|
296
|
+
for (const a of data.artifactsWritten) existing.add(a);
|
|
297
|
+
capsule.work_summary.artifacts_written = [...existing];
|
|
298
|
+
}
|
|
299
|
+
if (data.testsRun) {
|
|
300
|
+
capsule.verification.tests_run.push(...data.testsRun);
|
|
301
|
+
}
|
|
302
|
+
if (data.determinismChecks) {
|
|
303
|
+
capsule.verification.determinism_checks.push(...data.determinismChecks);
|
|
304
|
+
}
|
|
305
|
+
if (data.knownIssues) {
|
|
306
|
+
capsule.known_issues.push(...data.knownIssues);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Record current commit as a checkpoint
|
|
310
|
+
const currentRev = getHeadRev(root);
|
|
311
|
+
if (currentRev !== capsule.base_state.head_rev) {
|
|
312
|
+
capsule.work_summary.commits.push({
|
|
313
|
+
rev: currentRev,
|
|
314
|
+
checkpoint_at: new Date().toISOString(),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Write updated capsule
|
|
319
|
+
fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
|
|
320
|
+
|
|
321
|
+
return capsule;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* End a session, finalizing the capsule with handoff information
|
|
326
|
+
* @param {Object} data - End session data
|
|
327
|
+
* @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
|
|
328
|
+
* @param {string[]} [data.nextActions] - What the next session should do
|
|
329
|
+
* @param {string[]} [data.riskNotes] - Risk notes for handoff
|
|
330
|
+
* @returns {Object} Finalized capsule
|
|
331
|
+
*/
|
|
332
|
+
function endSession(data = {}) {
|
|
333
|
+
const root = getRepoRoot();
|
|
334
|
+
const registry = loadRegistry(root);
|
|
335
|
+
|
|
336
|
+
const sessionId = data.sessionId || findActiveSession(registry);
|
|
337
|
+
if (!sessionId) {
|
|
338
|
+
throw new Error('No active session found.');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
|
|
342
|
+
if (!fs.existsSync(capsulePath)) {
|
|
343
|
+
throw new Error(`Session capsule not found: ${sessionId}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const capsule = JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
|
|
347
|
+
|
|
348
|
+
// Finalize
|
|
349
|
+
capsule.ended_at = new Date().toISOString();
|
|
350
|
+
|
|
351
|
+
// Capture final workspace state
|
|
352
|
+
const fingerprint = getWorkspaceFingerprint(root);
|
|
353
|
+
capsule.work_summary.paths_touched = [
|
|
354
|
+
...new Set([...capsule.work_summary.paths_touched, ...fingerprint.paths_touched]),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
// Record final commit
|
|
358
|
+
const finalRev = getHeadRev(root);
|
|
359
|
+
if (
|
|
360
|
+
finalRev !== capsule.base_state.head_rev &&
|
|
361
|
+
!capsule.work_summary.commits.some((c) => c.rev === finalRev)
|
|
362
|
+
) {
|
|
363
|
+
capsule.work_summary.commits.push({
|
|
364
|
+
rev: finalRev,
|
|
365
|
+
checkpoint_at: new Date().toISOString(),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Handoff
|
|
370
|
+
if (data.nextActions) {
|
|
371
|
+
capsule.handoff.next_actions = data.nextActions;
|
|
372
|
+
}
|
|
373
|
+
if (data.riskNotes) {
|
|
374
|
+
capsule.handoff.risk_notes = data.riskNotes;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Flag if dirty
|
|
378
|
+
if (fingerprint.dirty) {
|
|
379
|
+
capsule.known_issues.push({
|
|
380
|
+
type: 'warning',
|
|
381
|
+
description: `Session ended with ${fingerprint.paths_touched.length} uncommitted file(s).`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Write finalized capsule
|
|
386
|
+
fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
|
|
387
|
+
|
|
388
|
+
// Update registry
|
|
389
|
+
registry.sessions[sessionId].status = 'completed';
|
|
390
|
+
registry.sessions[sessionId].ended_at = capsule.ended_at;
|
|
391
|
+
saveRegistry(root, registry);
|
|
392
|
+
|
|
393
|
+
return capsule;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* List all sessions
|
|
398
|
+
* @param {Object} [options] - List options
|
|
399
|
+
* @param {string} [options.status] - Filter by status (active, completed)
|
|
400
|
+
* @param {number} [options.limit] - Max entries to return
|
|
401
|
+
* @returns {Object[]} Session entries
|
|
402
|
+
*/
|
|
403
|
+
function listSessions(options = {}) {
|
|
404
|
+
const root = getRepoRoot();
|
|
405
|
+
const registry = loadRegistry(root);
|
|
406
|
+
|
|
407
|
+
let entries = Object.entries(registry.sessions).map(([id, meta]) => ({
|
|
408
|
+
id,
|
|
409
|
+
...meta,
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
if (options.status) {
|
|
413
|
+
entries = entries.filter((e) => e.status === options.status);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Sort by started_at descending (most recent first)
|
|
417
|
+
entries.sort((a, b) => new Date(b.started_at) - new Date(a.started_at));
|
|
418
|
+
|
|
419
|
+
if (options.limit) {
|
|
420
|
+
entries = entries.slice(0, options.limit);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return entries;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Show a specific session's full capsule
|
|
428
|
+
* @param {string} sessionId - Session ID (or "latest" for most recent)
|
|
429
|
+
* @returns {Object} Full capsule
|
|
430
|
+
*/
|
|
431
|
+
function showSession(sessionId) {
|
|
432
|
+
const root = getRepoRoot();
|
|
433
|
+
|
|
434
|
+
if (sessionId === 'latest') {
|
|
435
|
+
const registry = loadRegistry(root);
|
|
436
|
+
const active = findActiveSession(registry);
|
|
437
|
+
if (active) {
|
|
438
|
+
sessionId = active;
|
|
439
|
+
} else {
|
|
440
|
+
// Find most recent completed
|
|
441
|
+
const entries = Object.entries(registry.sessions).sort(
|
|
442
|
+
(a, b) => new Date(b[1].started_at) - new Date(a[1].started_at)
|
|
443
|
+
);
|
|
444
|
+
if (entries.length === 0) throw new Error('No sessions found.');
|
|
445
|
+
sessionId = entries[0][0];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
|
|
450
|
+
if (!fs.existsSync(capsulePath)) {
|
|
451
|
+
throw new Error(`Session capsule not found: ${sessionId}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Briefing output for session start hooks - returns structured text
|
|
459
|
+
* @returns {string} Briefing text
|
|
460
|
+
*/
|
|
461
|
+
function getBriefing() {
|
|
462
|
+
const root = getRepoRoot();
|
|
463
|
+
const registry = loadRegistry(root);
|
|
464
|
+
|
|
465
|
+
const lines = [];
|
|
466
|
+
lines.push('--- CAWS Session Briefing ---');
|
|
467
|
+
|
|
468
|
+
// Git baseline
|
|
469
|
+
const headRev = getHeadRev(root);
|
|
470
|
+
const branch = getCurrentBranch(root);
|
|
471
|
+
const fingerprint = getWorkspaceFingerprint(root);
|
|
472
|
+
lines.push(`Git: ${branch} @ ${headRev} (${fingerprint.paths_touched.length} dirty files)`);
|
|
473
|
+
|
|
474
|
+
if (fingerprint.dirty) {
|
|
475
|
+
lines.push('WARNING: Working tree has uncommitted changes from a prior session.');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Active sessions
|
|
479
|
+
const activeSessions = Object.entries(registry.sessions)
|
|
480
|
+
.filter(([, meta]) => meta.status === 'active')
|
|
481
|
+
.map(([id, meta]) => ({ id, ...meta }));
|
|
482
|
+
|
|
483
|
+
if (activeSessions.length > 0) {
|
|
484
|
+
lines.push(`Active sessions: ${activeSessions.length}`);
|
|
485
|
+
for (const s of activeSessions) {
|
|
486
|
+
lines.push(` - ${s.id} (${s.role}, spec: ${s.spec_id || 'none'})`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Last completed session handoff
|
|
491
|
+
const completedSessions = Object.entries(registry.sessions)
|
|
492
|
+
.filter(([, meta]) => meta.status === 'completed')
|
|
493
|
+
.sort((a, b) => new Date(b[1].ended_at) - new Date(a[1].ended_at));
|
|
494
|
+
|
|
495
|
+
if (completedSessions.length > 0) {
|
|
496
|
+
const [lastId] = completedSessions[0];
|
|
497
|
+
try {
|
|
498
|
+
const capsule = showSession(lastId);
|
|
499
|
+
if (capsule.handoff.next_actions.length > 0) {
|
|
500
|
+
lines.push('Handoff from prior session:');
|
|
501
|
+
for (const action of capsule.handoff.next_actions) {
|
|
502
|
+
lines.push(` - ${action}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (capsule.known_issues.length > 0) {
|
|
506
|
+
lines.push('Known issues from prior session:');
|
|
507
|
+
for (const issue of capsule.known_issues) {
|
|
508
|
+
lines.push(` - [${issue.type}] ${issue.description}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch {
|
|
512
|
+
// Non-fatal
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
lines.push('---');
|
|
517
|
+
lines.push("Run 'caws session start' to begin a tracked session.");
|
|
518
|
+
lines.push('--- End CAWS Briefing ---');
|
|
519
|
+
|
|
520
|
+
return lines.join('\n');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Find the most recent active session
|
|
525
|
+
* @param {Object} registry - Session registry
|
|
526
|
+
* @returns {string|null} Session ID or null
|
|
527
|
+
*/
|
|
528
|
+
function findActiveSession(registry) {
|
|
529
|
+
const active = Object.entries(registry.sessions)
|
|
530
|
+
.filter(([, meta]) => meta.status === 'active')
|
|
531
|
+
.sort((a, b) => new Date(b[1].started_at) - new Date(a[1].started_at));
|
|
532
|
+
|
|
533
|
+
return active.length > 0 ? active[0][0] : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
module.exports = {
|
|
537
|
+
startSession,
|
|
538
|
+
checkpointSession,
|
|
539
|
+
endSession,
|
|
540
|
+
listSessions,
|
|
541
|
+
showSession,
|
|
542
|
+
getBriefing,
|
|
543
|
+
loadRegistry,
|
|
544
|
+
getRepoRoot,
|
|
545
|
+
SESSIONS_DIR,
|
|
546
|
+
REGISTRY_FILE,
|
|
547
|
+
CAPSULE_SCHEMA_VERSION,
|
|
548
|
+
};
|
package/dist/test-analysis.js
CHANGED
|
@@ -607,7 +607,7 @@ async function testAnalysisCommand(subcommand, options = []) {
|
|
|
607
607
|
case 'find-similar':
|
|
608
608
|
return await handleFindSimilar(options);
|
|
609
609
|
default:
|
|
610
|
-
console.log(chalk.red('
|
|
610
|
+
console.log(chalk.red('Unknown test-analysis subcommand'));
|
|
611
611
|
console.log('Available commands:');
|
|
612
612
|
console.log(' assess-budget - Analyze budget needs for current spec');
|
|
613
613
|
console.log(' analyze-patterns - Show waiver pattern analysis');
|
|
@@ -615,7 +615,7 @@ async function testAnalysisCommand(subcommand, options = []) {
|
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
617
|
} catch (error) {
|
|
618
|
-
console.error(chalk.red('
|
|
618
|
+
console.error(chalk.red('Test analysis failed:'), error.message);
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
621
|
|
|
@@ -639,8 +639,8 @@ async function handleAssessBudget(options) {
|
|
|
639
639
|
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
640
640
|
const spec = yaml.load(specContent);
|
|
641
641
|
|
|
642
|
-
console.log(chalk.cyan(
|
|
643
|
-
console.log('
|
|
642
|
+
console.log(chalk.cyan(`Budget Assessment for ${spec.id}`));
|
|
643
|
+
console.log('==============================================');
|
|
644
644
|
|
|
645
645
|
const result = predictor.assessBudget(spec);
|
|
646
646
|
|
|
@@ -650,14 +650,14 @@ async function handleAssessBudget(options) {
|
|
|
650
650
|
`Historical Analysis: ${assessment.similar_projects_analyzed} similar projects analyzed`
|
|
651
651
|
);
|
|
652
652
|
console.log(
|
|
653
|
-
|
|
653
|
+
`Recommended Budget: ${assessment.recommended_budget.files} files, ${assessment.recommended_budget.loc} LOC (+${assessment.buffer_applied.files_percent}% buffer)`
|
|
654
654
|
);
|
|
655
|
-
console.log(
|
|
655
|
+
console.log(`Rationale: ${assessment.rationale.join('; ')}`);
|
|
656
656
|
|
|
657
657
|
if (assessment.risk_factors.length > 0) {
|
|
658
658
|
console.log(
|
|
659
659
|
chalk.yellow(
|
|
660
|
-
|
|
660
|
+
`Risk Factors: ${assessment.risk_factors.map((f) => f.description).join('; ')}`
|
|
661
661
|
)
|
|
662
662
|
);
|
|
663
663
|
}
|
|
@@ -666,15 +666,15 @@ async function handleAssessBudget(options) {
|
|
|
666
666
|
assessment.confidence > 0.8 ? 'High' : assessment.confidence > 0.6 ? 'Medium' : 'Low';
|
|
667
667
|
console.log(
|
|
668
668
|
chalk.green(
|
|
669
|
-
|
|
669
|
+
`Confidence: ${confidenceLevel} (${Math.round(assessment.confidence * 100)}%)`
|
|
670
670
|
)
|
|
671
671
|
);
|
|
672
672
|
} else {
|
|
673
|
-
console.log(chalk.yellow(
|
|
674
|
-
console.log('
|
|
673
|
+
console.log(chalk.yellow(`${result.message}`));
|
|
674
|
+
console.log('Consider using default tier-based budgeting for now');
|
|
675
675
|
}
|
|
676
676
|
} catch (error) {
|
|
677
|
-
console.error(chalk.red('
|
|
677
|
+
console.error(chalk.red('Failed to load spec:'), error.message);
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
|
|
@@ -685,8 +685,8 @@ async function handleAnalyzePatterns(options) {
|
|
|
685
685
|
const chalk = (await import('chalk')).default;
|
|
686
686
|
const learner = new WaiverPatternLearner();
|
|
687
687
|
|
|
688
|
-
console.log(chalk.cyan('
|
|
689
|
-
console.log('
|
|
688
|
+
console.log(chalk.cyan('Analyzing Waiver Patterns'));
|
|
689
|
+
console.log('==============================================');
|
|
690
690
|
|
|
691
691
|
const result = learner.analyzePatterns();
|
|
692
692
|
|
|
@@ -696,7 +696,7 @@ async function handleAnalyzePatterns(options) {
|
|
|
696
696
|
console.log(`Total waivers analyzed: ${patterns.total_waivers}`);
|
|
697
697
|
|
|
698
698
|
if (patterns.budget_overruns) {
|
|
699
|
-
console.log('\
|
|
699
|
+
console.log('\nBudget Overrun Patterns:');
|
|
700
700
|
console.log(
|
|
701
701
|
` Average overrun: ${patterns.budget_overruns.average_overrun_files} files, ${patterns.budget_overruns.average_overrun_loc} LOC`
|
|
702
702
|
);
|
|
@@ -712,7 +712,7 @@ async function handleAnalyzePatterns(options) {
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
if (patterns.common_reasons.length > 0) {
|
|
715
|
-
console.log('\
|
|
715
|
+
console.log('\nMost Common Waiver Reasons:');
|
|
716
716
|
patterns.common_reasons.slice(0, 5).forEach((reason) => {
|
|
717
717
|
console.log(
|
|
718
718
|
` ${reason.reason}: ${reason.count} times (${Math.round(reason.frequency * 100)}%)`
|
|
@@ -720,7 +720,7 @@ async function handleAnalyzePatterns(options) {
|
|
|
720
720
|
});
|
|
721
721
|
}
|
|
722
722
|
} else {
|
|
723
|
-
console.log(chalk.yellow(
|
|
723
|
+
console.log(chalk.yellow(`${result.message}`));
|
|
724
724
|
}
|
|
725
725
|
}
|
|
726
726
|
|
|
@@ -744,8 +744,8 @@ async function handleFindSimilar(options) {
|
|
|
744
744
|
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
745
745
|
const spec = yaml.load(specContent);
|
|
746
746
|
|
|
747
|
-
console.log(chalk.cyan(
|
|
748
|
-
console.log('
|
|
747
|
+
console.log(chalk.cyan(`Finding projects similar to ${spec.id}`));
|
|
748
|
+
console.log('==============================================');
|
|
749
749
|
|
|
750
750
|
const similar = matcher.findSimilarProjects(spec);
|
|
751
751
|
|
|
@@ -758,10 +758,10 @@ async function handleFindSimilar(options) {
|
|
|
758
758
|
);
|
|
759
759
|
});
|
|
760
760
|
} else {
|
|
761
|
-
console.log(chalk.yellow('
|
|
761
|
+
console.log(chalk.yellow('No similar projects found'));
|
|
762
762
|
}
|
|
763
763
|
} catch (error) {
|
|
764
|
-
console.error(chalk.red('
|
|
764
|
+
console.error(chalk.red('Failed to load spec:'), error.message);
|
|
765
765
|
}
|
|
766
766
|
}
|
|
767
767
|
|