agileflow 3.2.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/feature-flags.js +32 -4
  4. package/lib/skill-loader.js +0 -1
  5. package/package.json +1 -1
  6. package/scripts/agileflow-statusline.sh +81 -0
  7. package/scripts/babysit-clear-restore.js +154 -0
  8. package/scripts/claude-tmux.sh +120 -24
  9. package/scripts/claude-watchdog.sh +225 -0
  10. package/scripts/generators/agent-registry.js +14 -1
  11. package/scripts/generators/inject-babysit.js +22 -9
  12. package/scripts/generators/inject-help.js +19 -9
  13. package/scripts/lib/README-portable-tasks.md +424 -0
  14. package/scripts/lib/audit-cleanup.js +250 -0
  15. package/scripts/lib/audit-registry.js +248 -0
  16. package/scripts/lib/configure-detect.js +20 -0
  17. package/scripts/lib/feature-catalog.js +13 -2
  18. package/scripts/lib/gate-enforcer.js +295 -0
  19. package/scripts/lib/model-profiles.js +98 -0
  20. package/scripts/lib/signal-detectors.js +1 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/tdd-phase-manager.js +455 -0
  24. package/scripts/lib/team-events.js +76 -8
  25. package/scripts/lib/tmux-group-colors.js +113 -0
  26. package/scripts/messaging-bridge.js +209 -1
  27. package/scripts/spawn-audit-sessions.js +549 -0
  28. package/scripts/team-manager.js +37 -16
  29. package/scripts/tmux-close-windows.sh +180 -0
  30. package/scripts/tmux-restore-window.sh +67 -0
  31. package/scripts/tmux-save-closed-window.sh +35 -0
  32. package/src/core/agents/ads-audit-budget.md +181 -0
  33. package/src/core/agents/ads-audit-compliance.md +169 -0
  34. package/src/core/agents/ads-audit-creative.md +164 -0
  35. package/src/core/agents/ads-audit-google.md +226 -0
  36. package/src/core/agents/ads-audit-meta.md +183 -0
  37. package/src/core/agents/ads-audit-tracking.md +197 -0
  38. package/src/core/agents/ads-consensus.md +322 -0
  39. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  40. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  41. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  42. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  43. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  44. package/src/core/agents/brainstorm-consensus.md +237 -0
  45. package/src/core/agents/completeness-analyzer-api.md +190 -0
  46. package/src/core/agents/completeness-analyzer-conditional.md +201 -0
  47. package/src/core/agents/completeness-analyzer-handlers.md +159 -0
  48. package/src/core/agents/completeness-analyzer-imports.md +159 -0
  49. package/src/core/agents/completeness-analyzer-routes.md +182 -0
  50. package/src/core/agents/completeness-analyzer-state.md +188 -0
  51. package/src/core/agents/completeness-analyzer-stubs.md +198 -0
  52. package/src/core/agents/completeness-consensus.md +286 -0
  53. package/src/core/agents/perf-consensus.md +2 -2
  54. package/src/core/agents/security-consensus.md +2 -2
  55. package/src/core/agents/seo-analyzer-content.md +167 -0
  56. package/src/core/agents/seo-analyzer-images.md +187 -0
  57. package/src/core/agents/seo-analyzer-performance.md +206 -0
  58. package/src/core/agents/seo-analyzer-schema.md +176 -0
  59. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  60. package/src/core/agents/seo-analyzer-technical.md +144 -0
  61. package/src/core/agents/seo-consensus.md +289 -0
  62. package/src/core/agents/test-consensus.md +2 -2
  63. package/src/core/commands/ads/audit.md +375 -0
  64. package/src/core/commands/ads/budget.md +97 -0
  65. package/src/core/commands/ads/competitor.md +112 -0
  66. package/src/core/commands/ads/creative.md +85 -0
  67. package/src/core/commands/ads/google.md +112 -0
  68. package/src/core/commands/ads/landing.md +119 -0
  69. package/src/core/commands/ads/linkedin.md +112 -0
  70. package/src/core/commands/ads/meta.md +91 -0
  71. package/src/core/commands/ads/microsoft.md +115 -0
  72. package/src/core/commands/ads/plan.md +321 -0
  73. package/src/core/commands/ads/tiktok.md +129 -0
  74. package/src/core/commands/ads/youtube.md +124 -0
  75. package/src/core/commands/ads.md +128 -0
  76. package/src/core/commands/babysit.md +250 -1344
  77. package/src/core/commands/code/completeness.md +466 -0
  78. package/src/core/commands/{audit → code}/legal.md +26 -16
  79. package/src/core/commands/{audit → code}/logic.md +27 -16
  80. package/src/core/commands/{audit → code}/performance.md +30 -20
  81. package/src/core/commands/{audit → code}/security.md +32 -19
  82. package/src/core/commands/{audit → code}/test.md +30 -20
  83. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  84. package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
  85. package/src/core/commands/ideate/features.md +435 -0
  86. package/src/core/commands/seo/audit.md +373 -0
  87. package/src/core/commands/seo/competitor.md +174 -0
  88. package/src/core/commands/seo/content.md +107 -0
  89. package/src/core/commands/seo/geo.md +229 -0
  90. package/src/core/commands/seo/hreflang.md +140 -0
  91. package/src/core/commands/seo/images.md +96 -0
  92. package/src/core/commands/seo/page.md +198 -0
  93. package/src/core/commands/seo/plan.md +163 -0
  94. package/src/core/commands/seo/programmatic.md +131 -0
  95. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  96. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  97. package/src/core/commands/seo/references/quality-gates.md +91 -0
  98. package/src/core/commands/seo/references/schema-types.md +102 -0
  99. package/src/core/commands/seo/schema.md +183 -0
  100. package/src/core/commands/seo/sitemap.md +97 -0
  101. package/src/core/commands/seo/technical.md +100 -0
  102. package/src/core/commands/seo.md +107 -0
  103. package/src/core/commands/skill/list.md +68 -212
  104. package/src/core/commands/skill/recommend.md +216 -0
  105. package/src/core/commands/tdd-next.md +238 -0
  106. package/src/core/commands/tdd.md +210 -0
  107. package/src/core/experts/_core-expertise.yaml +105 -0
  108. package/src/core/experts/analytics/expertise.yaml +5 -99
  109. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  110. package/src/core/experts/compliance/expertise.yaml +6 -72
  111. package/src/core/experts/database/expertise.yaml +9 -52
  112. package/src/core/experts/documentation/expertise.yaml +7 -140
  113. package/src/core/experts/integrations/expertise.yaml +7 -127
  114. package/src/core/experts/mentor/expertise.yaml +8 -35
  115. package/src/core/experts/monitoring/expertise.yaml +7 -49
  116. package/src/core/experts/performance/expertise.yaml +1 -26
  117. package/src/core/experts/security/expertise.yaml +9 -34
  118. package/src/core/experts/ui/expertise.yaml +6 -36
  119. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  120. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  121. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  122. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  123. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  124. package/src/core/templates/agileflow-metadata.json +15 -1
  125. package/tools/cli/installers/ide/_base-ide.js +42 -5
  126. package/tools/cli/installers/ide/claude-code.js +13 -4
  127. package/tools/cli/lib/content-injector.js +160 -12
  128. package/tools/cli/lib/docs-setup.js +1 -1
  129. package/src/core/commands/skill/create.md +0 -698
  130. package/src/core/commands/skill/delete.md +0 -316
  131. package/src/core/commands/skill/edit.md +0 -359
  132. package/src/core/commands/skill/test.md +0 -394
  133. package/src/core/commands/skill/upgrade.md +0 -552
  134. package/src/core/templates/skill-template.md +0 -117
@@ -0,0 +1,424 @@
1
+ # Portable Task Tracking System
2
+
3
+ A file-based, IDE-agnostic task tracking system for AgileFlow projects. Works with Claude Code, Cursor, Windsurf, Codex, and any IDE that can read/write files.
4
+
5
+ ## Overview
6
+
7
+ Unlike Claude Code's native `TaskCreate`/`TaskUpdate` tools (which only work in Claude Code), this system stores tasks in a simple markdown file (`.agileflow/tasks.md`) that ANY IDE's AI can read and modify.
8
+
9
+ **Key Features:**
10
+ - Pure markdown format - human-readable, git-friendly
11
+ - No dependencies beyond Node.js built-ins
12
+ - Works across all IDEs
13
+ - Supports filtering, custom fields, and blocking relationships
14
+ - Round-trip stable (parse → modify → format → parse)
15
+
16
+ ## Files
17
+
18
+ - **`portable-tasks.js`** - Core module (functions for CRUD operations)
19
+ - **`portable-tasks-cli.js`** - Command-line interface (callable from any IDE)
20
+
21
+ ## Installation
22
+
23
+ Copy both files to your project's `.agileflow/scripts/lib/` directory during setup. They are published with the agileflow npm package.
24
+
25
+ ## File Format
26
+
27
+ Tasks are stored in `.agileflow/tasks.md`:
28
+
29
+ ```markdown
30
+ # AgileFlow Tasks
31
+
32
+ > Auto-managed task list. Edit carefully - format matters.
33
+ > Last updated: 2026-02-20T15:30:00Z
34
+
35
+ ## Active Tasks
36
+
37
+ ### T-001: Implement user authentication [in_progress]
38
+ - **Owner**: AG-API
39
+ - **Created**: 2026-02-20
40
+ - **Story**: US-0042
41
+ - **Description**: Add JWT-based auth to /api/login endpoint
42
+
43
+ ### T-002: Write auth tests [pending]
44
+ - **Owner**: AG-CI
45
+ - **Created**: 2026-02-20
46
+ - **Story**: US-0042
47
+ - **Blocked by**: T-001
48
+ - **Description**: Unit and integration tests for auth flow
49
+
50
+ ## Completed Tasks
51
+
52
+ ### T-003: Setup database schema [completed]
53
+ - **Owner**: AG-DEVOPS
54
+ - **Created**: 2026-02-19
55
+ - **Completed**: 2026-02-20
56
+ - **Story**: US-0041
57
+ - **Description**: Create users and sessions tables
58
+ ```
59
+
60
+ ## API Reference
61
+
62
+ ### Module: `portable-tasks.js`
63
+
64
+ #### `loadTasks(projectDir)`
65
+ Load tasks from `.agileflow/tasks.md`.
66
+
67
+ ```javascript
68
+ const { loadTasks } = require('./portable-tasks');
69
+ const { activeTasks, completedTasks } = loadTasks(process.cwd());
70
+ ```
71
+
72
+ Returns: `{ activeTasks: [], completedTasks: [] }`
73
+
74
+ #### `saveTasks(projectDir, tasksData)`
75
+ Save tasks back to markdown file.
76
+
77
+ ```javascript
78
+ saveTasks(process.cwd(), { activeTasks, completedTasks });
79
+ ```
80
+
81
+ Returns: `boolean` (success/failure)
82
+
83
+ #### `addTask(projectDir, taskData)`
84
+ Create a new task.
85
+
86
+ ```javascript
87
+ const result = addTask(process.cwd(), {
88
+ subject: 'Write API tests',
89
+ description: 'Integration tests for new endpoints',
90
+ owner: 'AG-CI',
91
+ status: 'pending', // pending, in_progress, completed, blocked
92
+ story: 'US-0040',
93
+ blockedBy: 'T-001', // optional
94
+ });
95
+
96
+ // result: { ok: true, taskId: 'T-001' } or { ok: false, error: 'message' }
97
+ ```
98
+
99
+ #### `updateTask(projectDir, taskId, updates)`
100
+ Update an existing task.
101
+
102
+ ```javascript
103
+ updateTask(process.cwd(), 'T-001', {
104
+ status: 'in_progress',
105
+ description: 'Updated description',
106
+ owner: 'AG-DEVOPS',
107
+ });
108
+ ```
109
+
110
+ Returns: `{ ok: boolean, error?: string }`
111
+
112
+ Moves task between active/completed sections automatically when status changes.
113
+
114
+ #### `deleteTask(projectDir, taskId)`
115
+ Remove a task.
116
+
117
+ ```javascript
118
+ deleteTask(process.cwd(), 'T-001');
119
+ ```
120
+
121
+ Returns: `{ ok: boolean, error?: string }`
122
+
123
+ #### `getTask(projectDir, taskId)`
124
+ Retrieve a single task by ID.
125
+
126
+ ```javascript
127
+ const task = getTask(process.cwd(), 'T-001');
128
+ // Returns task object or null if not found
129
+ ```
130
+
131
+ #### `listTasks(projectDir, filters?)`
132
+ List tasks with optional filtering.
133
+
134
+ ```javascript
135
+ // All active tasks (default)
136
+ const tasks = listTasks(process.cwd());
137
+
138
+ // Filter by status
139
+ listTasks(process.cwd(), { status: 'pending' });
140
+ listTasks(process.cwd(), { status: ['pending', 'blocked'] });
141
+
142
+ // Filter by owner
143
+ listTasks(process.cwd(), { owner: 'AG-API' });
144
+
145
+ // Include completed tasks
146
+ listTasks(process.cwd(), { includeCompleted: true });
147
+
148
+ // Combine filters
149
+ listTasks(process.cwd(), {
150
+ status: 'pending',
151
+ owner: 'AG-API',
152
+ includeCompleted: true,
153
+ });
154
+ ```
155
+
156
+ Returns: `Array` of task objects
157
+
158
+ #### `getNextId(tasks)`
159
+ Generate the next sequential task ID.
160
+
161
+ ```javascript
162
+ const nextId = getNextId(allTasks); // 'T-001', 'T-002', etc.
163
+ ```
164
+
165
+ #### Parsing Functions
166
+ - `parseTasksFile(content)` - Parse markdown content into task objects
167
+ - `formatTasksFile(activeTasks, completedTasks)` - Generate markdown from tasks
168
+
169
+ ## CLI: `portable-tasks-cli.js`
170
+
171
+ Command-line interface for task management (works in any IDE's terminal).
172
+
173
+ ### Usage
174
+
175
+ ```bash
176
+ node portable-tasks-cli.js [command] [options]
177
+ ```
178
+
179
+ ### Commands
180
+
181
+ #### List Tasks
182
+ ```bash
183
+ # List active tasks
184
+ node portable-tasks-cli.js list
185
+
186
+ # Filter by status
187
+ node portable-tasks-cli.js list --status=pending
188
+ node portable-tasks-cli.js list --status=in_progress
189
+
190
+ # Filter by owner
191
+ node portable-tasks-cli.js list --owner=AG-API
192
+
193
+ # Include completed tasks
194
+ node portable-tasks-cli.js list --include-completed
195
+
196
+ # Output as JSON
197
+ node portable-tasks-cli.js list --json
198
+
199
+ # Combine filters
200
+ node portable-tasks-cli.js list --status=pending --owner=AG-CI
201
+ ```
202
+
203
+ #### Add Task
204
+ ```bash
205
+ # Minimal (subject required)
206
+ node portable-tasks-cli.js add --subject="Fix login bug"
207
+
208
+ # Full details
209
+ node portable-tasks-cli.js add \
210
+ --subject="Implement auth" \
211
+ --description="Add JWT tokens to API" \
212
+ --owner=AG-API \
213
+ --status=in_progress \
214
+ --story=US-0040 \
215
+ --blocked-by=T-001
216
+ ```
217
+
218
+ #### Update Task
219
+ ```bash
220
+ # Change status
221
+ node portable-tasks-cli.js update T-001 --status=completed
222
+
223
+ # Update multiple fields
224
+ node portable-tasks-cli.js update T-001 \
225
+ --status=in_progress \
226
+ --owner=AG-API \
227
+ --description="Started implementation"
228
+ ```
229
+
230
+ #### Get Task
231
+ ```bash
232
+ # Display task
233
+ node portable-tasks-cli.js get T-001
234
+
235
+ # Output as JSON
236
+ node portable-tasks-cli.js get T-001 --json
237
+ ```
238
+
239
+ #### Delete Task
240
+ ```bash
241
+ node portable-tasks-cli.js delete T-001
242
+ node portable-tasks-cli.js rm T-001 # alias
243
+ ```
244
+
245
+ ### Output Formats
246
+
247
+ **Human-readable (default):**
248
+ ```
249
+ T-001 [in_progress] Implement user auth (AG-API) [blocked by T-002]
250
+ T-002 [pending] Write tests (AG-CI)
251
+ ```
252
+
253
+ **JSON output:**
254
+ ```json
255
+ [
256
+ {
257
+ "id": "T-001",
258
+ "title": "Implement user auth",
259
+ "status": "in_progress",
260
+ "owner": "AG-API",
261
+ "created": "2026-02-20",
262
+ "completed": null,
263
+ "story": "US-0040",
264
+ "blockedBy": "T-002",
265
+ "description": "Add JWT tokens to /api/login endpoint"
266
+ }
267
+ ]
268
+ ```
269
+
270
+ ### Exit Codes
271
+ - **0**: Success
272
+ - **1**: Error (invalid command, missing task, etc.)
273
+
274
+ ## Task Fields
275
+
276
+ Every task has these fields:
277
+
278
+ | Field | Type | Required | Description |
279
+ |-------|------|----------|-------------|
280
+ | `id` | string | Yes | Auto-generated ID (T-001, T-002, etc.) |
281
+ | `title` | string | Yes | Task summary |
282
+ | `status` | string | Yes | One of: `pending`, `in_progress`, `completed`, `blocked` |
283
+ | `owner` | string | No | Responsible agent/team (e.g., AG-API) |
284
+ | `created` | date | No | Creation date (YYYY-MM-DD) |
285
+ | `completed` | date | No | Completion date (auto-set when status → completed) |
286
+ | `story` | string | No | Link to parent story (e.g., US-0040) |
287
+ | `blockedBy` | string | No | Task ID blocking this task (e.g., T-001) |
288
+ | `description` | string | No | Detailed task description |
289
+
290
+ ## Examples
291
+
292
+ ### Python Script Using Module
293
+ ```python
294
+ import subprocess
295
+ import json
296
+
297
+ # List all pending tasks
298
+ result = subprocess.run(
299
+ ['node', '.agileflow/scripts/lib/portable-tasks-cli.js', 'list', '--status=pending', '--json'],
300
+ capture_output=True,
301
+ text=True
302
+ )
303
+ tasks = json.loads(result.stdout)
304
+ for task in tasks:
305
+ print(f"{task['id']}: {task['title']} ({task['owner']})")
306
+ ```
307
+
308
+ ### JavaScript Using Module
309
+ ```javascript
310
+ const { addTask, listTasks, updateTask } = require('./.agileflow/scripts/lib/portable-tasks');
311
+
312
+ // Create task
313
+ const result = addTask(process.cwd(), {
314
+ subject: 'Run integration tests',
315
+ owner: 'AG-CI',
316
+ status: 'pending'
317
+ });
318
+
319
+ // List tasks for an owner
320
+ const tasks = listTasks(process.cwd(), { owner: 'AG-CI' });
321
+
322
+ // Update task
323
+ updateTask(process.cwd(), result.taskId, { status: 'in_progress' });
324
+ ```
325
+
326
+ ### Bash Script
327
+ ```bash
328
+ #!/bin/bash
329
+
330
+ # Add task
331
+ node .agileflow/scripts/lib/portable-tasks-cli.js add \
332
+ --subject="Deploy to staging" \
333
+ --owner=AG-DEVOPS
334
+
335
+ # Get all pending tasks for AG-API
336
+ node .agileflow/scripts/lib/portable-tasks-cli.js list \
337
+ --status=pending \
338
+ --owner=AG-API
339
+
340
+ # Mark task complete
341
+ node .agileflow/scripts/lib/portable-tasks-cli.js update T-001 \
342
+ --status=completed
343
+ ```
344
+
345
+ ## Status Values
346
+
347
+ - **`pending`** - Task not started
348
+ - **`in_progress`** - Task actively being worked on
349
+ - **`blocked`** - Task blocked by another (see `blockedBy` field)
350
+ - **`completed`** - Task finished
351
+
352
+ ## Markdown Format Details
353
+
354
+ The markdown format is strict for correct parsing:
355
+
356
+ **Correct:**
357
+ ```markdown
358
+ ### T-001: Task title [status]
359
+ - **Owner**: Value
360
+ - **Blocked by**: T-002
361
+ - **Description**: Description text
362
+ ```
363
+
364
+ **Incorrect (will not parse):**
365
+ ```markdown
366
+ ### T-001: Task title [status]
367
+ - Owner: Value # Missing **bold** markers
368
+ - **Blocked By**: T-002 # Wrong case (must be "Blocked by")
369
+ - **Description**: Description text
370
+ # Extra blank lines between fields are ok
371
+ ```
372
+
373
+ **Rules:**
374
+ - Task header: `### T-NNN: Title [status]`
375
+ - Field format: `- **Field name**: Value`
376
+ - Field names are case-insensitive (will be normalized)
377
+ - Extra blank lines, comments, headers are ignored
378
+ - Tasks can have any fields beyond the standard ones
379
+
380
+ ## Thread Safety
381
+
382
+ The system uses atomic writes (write to temp file, then rename) to prevent corruption if multiple processes write simultaneously. However, reads are NOT locked, so concurrent reads during writes may see partial data. For production use with high concurrency, implement file locking via `fcntl` or `flock`.
383
+
384
+ ## Testing
385
+
386
+ Run the comprehensive test suite:
387
+
388
+ ```bash
389
+ cd packages/cli
390
+ npm test -- __tests__/scripts/lib/portable-tasks.test.js --no-coverage
391
+ ```
392
+
393
+ Coverage:
394
+ - 47 tests across all functions
395
+ - Parsing, formatting, CRUD operations
396
+ - Round-trip integrity (parse → modify → format → parse)
397
+ - Error handling and edge cases
398
+ - Integration workflows
399
+
400
+ ## Design Decisions
401
+
402
+ 1. **Markdown format** - Human-readable, diff-friendly, git-compatible (vs JSON/YAML)
403
+ 2. **Status-driven sections** - Completed/active sections based on task status, not location
404
+ 3. **Auto-ID generation** - Sequential IDs (T-001) simplify references
405
+ 4. **No dependencies** - Only Node.js built-ins to minimize install footprint
406
+ 5. **Lazy parsing** - Parse on load, format on save (vs streaming)
407
+ 6. **Fail-safe writes** - Atomic writes prevent corruption on crashes
408
+
409
+ ## Limitations
410
+
411
+ - No encryption (store sensitive data elsewhere)
412
+ - No versioning/history (file is current state only)
413
+ - No concurrent write locking (implement if needed)
414
+ - No real-time sync (changes require reload)
415
+ - Task IDs are reassigned if you manually edit the file
416
+
417
+ ## Future Enhancements
418
+
419
+ - [ ] Watch mode (monitor file for external changes)
420
+ - [ ] Priority levels (high/medium/low)
421
+ - [ ] Time tracking (estimated/actual hours)
422
+ - [ ] Comments/notes per task
423
+ - [ ] Subtasks (nested hierarchy)
424
+ - [ ] Recurring tasks
@@ -0,0 +1,250 @@
1
+ /**
2
+ * audit-cleanup.js - Orphan cleanup for ULTRADEEP audit sessions
3
+ *
4
+ * Cleans up abandoned tmux sessions and incomplete sentinel directories
5
+ * from ULTRADEEP audit runs. Designed to be called from Stop hooks or
6
+ * manually for maintenance.
7
+ *
8
+ * Usage:
9
+ * const { cleanupOrphanSessions } = require('./audit-cleanup');
10
+ * cleanupOrphanSessions(rootDir);
11
+ *
12
+ * CLI:
13
+ * node scripts/lib/audit-cleanup.js [--max-age=60] [--dry-run]
14
+ */
15
+
16
+ const { execFileSync } = require('child_process');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const MAX_AGE_MINUTES = 60;
21
+
22
+ /**
23
+ * Find all ultradeep trace directories.
24
+ * @param {string} rootDir - Project root
25
+ * @returns {Array<{ traceId: string, dir: string, status: object|null }>}
26
+ */
27
+ function findTraceDirectories(rootDir) {
28
+ const ultradeepDir = path.join(rootDir, 'docs', '09-agents', 'ultradeep');
29
+
30
+ if (!fs.existsSync(ultradeepDir)) {
31
+ return [];
32
+ }
33
+
34
+ const traces = [];
35
+ try {
36
+ const entries = fs.readdirSync(ultradeepDir, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ if (!entry.isDirectory()) continue;
39
+
40
+ const traceDir = path.join(ultradeepDir, entry.name);
41
+ const statusFile = path.join(traceDir, '_status.json');
42
+ let status = null;
43
+
44
+ try {
45
+ if (fs.existsSync(statusFile)) {
46
+ status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
47
+ }
48
+ } catch (_) {
49
+ // Corrupt status file
50
+ }
51
+
52
+ traces.push({
53
+ traceId: entry.name,
54
+ dir: traceDir,
55
+ status,
56
+ });
57
+ }
58
+ } catch (_) {
59
+ // Directory read failure
60
+ }
61
+
62
+ return traces;
63
+ }
64
+
65
+ /**
66
+ * Check if a trace is stale (older than maxAge).
67
+ * @param {object} trace - Trace info from findTraceDirectories
68
+ * @param {number} maxAgeMinutes - Maximum age in minutes
69
+ * @returns {boolean}
70
+ */
71
+ function isStaleTrace(trace, maxAgeMinutes) {
72
+ if (!trace.status || !trace.status.started_at) {
73
+ // No status = assume stale
74
+ return true;
75
+ }
76
+
77
+ const startedAt = new Date(trace.status.started_at).getTime();
78
+ if (isNaN(startedAt)) return true; // Invalid date = treat as stale
79
+ const age = Date.now() - startedAt;
80
+ return age > maxAgeMinutes * 60 * 1000;
81
+ }
82
+
83
+ /**
84
+ * Check if a trace is incomplete (not all analyzers have findings).
85
+ * @param {object} trace - Trace info from findTraceDirectories
86
+ * @returns {boolean}
87
+ */
88
+ function isIncompleteTrace(trace) {
89
+ if (!trace.status || !trace.status.analyzers) return true;
90
+
91
+ const expected = trace.status.analyzers;
92
+ const completed = trace.status.completed || [];
93
+
94
+ return completed.length < expected.length;
95
+ }
96
+
97
+ /**
98
+ * Find orphaned tmux sessions matching audit pattern.
99
+ * @returns {string[]} Array of session names
100
+ */
101
+ function findOrphanedTmuxSessions() {
102
+ try {
103
+ const output = execFileSync('tmux', ['list-sessions', '-F', '#{session_name}'], {
104
+ encoding: 'utf8',
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ }).trim();
107
+
108
+ if (!output) return [];
109
+
110
+ return output.split('\n').filter(name => name.startsWith('audit-'));
111
+ } catch (_) {
112
+ return [];
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Kill a tmux session by name.
118
+ * @param {string} sessionName - Session name to kill
119
+ * @returns {boolean} True if killed successfully
120
+ */
121
+ function killTmuxSession(sessionName) {
122
+ try {
123
+ execFileSync('tmux', ['kill-session', '-t', sessionName], {
124
+ stdio: 'pipe',
125
+ });
126
+ return true;
127
+ } catch (_) {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Remove a sentinel directory.
134
+ * @param {string} dir - Directory to remove
135
+ * @returns {boolean} True if removed successfully
136
+ */
137
+ function removeSentinelDir(dir) {
138
+ try {
139
+ fs.rmSync(dir, { recursive: true, force: true });
140
+ return true;
141
+ } catch (_) {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Clean up orphaned ULTRADEEP audit sessions and stale sentinel dirs.
148
+ *
149
+ * @param {string} rootDir - Project root directory
150
+ * @param {object} [options] - Options
151
+ * @param {number} [options.maxAgeMinutes] - Max age for stale traces (default: 60)
152
+ * @param {boolean} [options.dryRun] - If true, report but don't delete
153
+ * @returns {{ sessionsKilled: string[], tracesRemoved: string[], errors: string[] }}
154
+ */
155
+ function cleanupOrphanSessions(rootDir, options) {
156
+ const maxAge = (options && options.maxAgeMinutes) || MAX_AGE_MINUTES;
157
+ const dryRun = (options && options.dryRun) || false;
158
+
159
+ const result = {
160
+ sessionsKilled: [],
161
+ tracesRemoved: [],
162
+ errors: [],
163
+ };
164
+
165
+ // 1. Find and kill orphaned tmux sessions
166
+ const orphanedSessions = findOrphanedTmuxSessions();
167
+ for (const session of orphanedSessions) {
168
+ // Extract trace ID from session name: audit-{type}-{traceId}
169
+ const parts = session.split('-');
170
+ if (parts.length < 3) continue; // Malformed session name, skip
171
+ const traceId = parts.slice(2).join('-');
172
+
173
+ if (dryRun) {
174
+ result.sessionsKilled.push(`${session} (dry-run)`);
175
+ continue;
176
+ }
177
+
178
+ if (killTmuxSession(session)) {
179
+ result.sessionsKilled.push(session);
180
+ } else {
181
+ result.errors.push(`Failed to kill session: ${session}`);
182
+ }
183
+ }
184
+
185
+ // 2. Clean up stale sentinel directories
186
+ const traces = findTraceDirectories(rootDir);
187
+ for (const trace of traces) {
188
+ if (isStaleTrace(trace, maxAge) && isIncompleteTrace(trace)) {
189
+ if (dryRun) {
190
+ result.tracesRemoved.push(`${trace.traceId} (dry-run)`);
191
+ continue;
192
+ }
193
+
194
+ if (removeSentinelDir(trace.dir)) {
195
+ result.tracesRemoved.push(trace.traceId);
196
+ } else {
197
+ result.errors.push(`Failed to remove trace dir: ${trace.traceId}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ return result;
203
+ }
204
+
205
+ // CLI
206
+ if (require.main === module) {
207
+ const args = process.argv.slice(2);
208
+ let maxAge = MAX_AGE_MINUTES;
209
+ let dryRun = false;
210
+
211
+ for (const arg of args) {
212
+ if (arg.startsWith('--max-age=')) {
213
+ const parsed = parseInt(arg.split('=')[1], 10);
214
+ maxAge = isNaN(parsed) ? MAX_AGE_MINUTES : parsed;
215
+ }
216
+ if (arg === '--dry-run') dryRun = true;
217
+ }
218
+
219
+ const rootDir = process.cwd();
220
+ const result = cleanupOrphanSessions(rootDir, { maxAgeMinutes: maxAge, dryRun });
221
+
222
+ if (result.sessionsKilled.length > 0) {
223
+ console.log(`Killed ${result.sessionsKilled.length} orphaned session(s):`);
224
+ result.sessionsKilled.forEach(s => console.log(` - ${s}`));
225
+ }
226
+
227
+ if (result.tracesRemoved.length > 0) {
228
+ console.log(`Removed ${result.tracesRemoved.length} stale trace(s):`);
229
+ result.tracesRemoved.forEach(t => console.log(` - ${t}`));
230
+ }
231
+
232
+ if (result.errors.length > 0) {
233
+ console.error(`${result.errors.length} error(s):`);
234
+ result.errors.forEach(e => console.error(` - ${e}`));
235
+ }
236
+
237
+ if (result.sessionsKilled.length === 0 && result.tracesRemoved.length === 0) {
238
+ console.log('No orphaned sessions or stale traces found.');
239
+ }
240
+ }
241
+
242
+ module.exports = {
243
+ findTraceDirectories,
244
+ isStaleTrace,
245
+ isIncompleteTrace,
246
+ findOrphanedTmuxSessions,
247
+ killTmuxSession,
248
+ removeSentinelDir,
249
+ cleanupOrphanSessions,
250
+ };