gru-ai 0.1.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 (143) hide show
  1. package/.claude/skills/brainstorm/SKILL.md +340 -0
  2. package/.claude/skills/code-review-excellence/SKILL.md +198 -0
  3. package/.claude/skills/directive/SKILL.md +121 -0
  4. package/.claude/skills/directive/docs/pipeline/00-delegation-and-triage.md +181 -0
  5. package/.claude/skills/directive/docs/pipeline/01-checkpoint.md +34 -0
  6. package/.claude/skills/directive/docs/pipeline/02-read-directive.md +38 -0
  7. package/.claude/skills/directive/docs/pipeline/03-read-context.md +15 -0
  8. package/.claude/skills/directive/docs/pipeline/04-challenge.md +38 -0
  9. package/.claude/skills/directive/docs/pipeline/05-planning.md +64 -0
  10. package/.claude/skills/directive/docs/pipeline/06-technical-audit.md +88 -0
  11. package/.claude/skills/directive/docs/pipeline/07-plan-approval.md +145 -0
  12. package/.claude/skills/directive/docs/pipeline/07b-project-brainstorm.md +85 -0
  13. package/.claude/skills/directive/docs/pipeline/08-worktree-and-state.md +50 -0
  14. package/.claude/skills/directive/docs/pipeline/09-execute-projects.md +709 -0
  15. package/.claude/skills/directive/docs/pipeline/10-wrapup.md +242 -0
  16. package/.claude/skills/directive/docs/pipeline/11-completion-gate.md +75 -0
  17. package/.claude/skills/directive/docs/reference/rules/casting-rules.md +78 -0
  18. package/.claude/skills/directive/docs/reference/rules/failure-handling.md +20 -0
  19. package/.claude/skills/directive/docs/reference/rules/phase-definitions.md +42 -0
  20. package/.claude/skills/directive/docs/reference/rules/scope-and-dod.md +30 -0
  21. package/.claude/skills/directive/docs/reference/schemas/audit-output.md +44 -0
  22. package/.claude/skills/directive/docs/reference/schemas/brainstorm-output.md +52 -0
  23. package/.claude/skills/directive/docs/reference/schemas/challenger-output.md +13 -0
  24. package/.claude/skills/directive/docs/reference/schemas/checkpoint.md +18 -0
  25. package/.claude/skills/directive/docs/reference/schemas/current-json.md +5 -0
  26. package/.claude/skills/directive/docs/reference/schemas/directive-json.md +143 -0
  27. package/.claude/skills/directive/docs/reference/schemas/investigation-output.md +37 -0
  28. package/.claude/skills/directive/docs/reference/schemas/plan-schema.md +103 -0
  29. package/.claude/skills/directive/docs/reference/templates/architect-prompt.md +66 -0
  30. package/.claude/skills/directive/docs/reference/templates/auditor-prompt.md +53 -0
  31. package/.claude/skills/directive/docs/reference/templates/brainstorm-prompt.md +68 -0
  32. package/.claude/skills/directive/docs/reference/templates/challenger-prompt.md +35 -0
  33. package/.claude/skills/directive/docs/reference/templates/digest.md +134 -0
  34. package/.claude/skills/directive/docs/reference/templates/investigator-prompt.md +51 -0
  35. package/.claude/skills/directive/docs/reference/templates/planner-prompt.md +130 -0
  36. package/.claude/skills/frontend-design/SKILL.md +42 -0
  37. package/.claude/skills/gruai-agents/SKILL.md +161 -0
  38. package/.claude/skills/gruai-config/SKILL.md +61 -0
  39. package/.claude/skills/healthcheck/SKILL.md +216 -0
  40. package/.claude/skills/report/SKILL.md +380 -0
  41. package/.claude/skills/scout/SKILL.md +452 -0
  42. package/.claude/skills/seo-audit/SKILL.md +107 -0
  43. package/.claude/skills/walkthrough/SKILL.md +274 -0
  44. package/.claude/skills/webapp-testing/SKILL.md +96 -0
  45. package/LICENSE +21 -0
  46. package/README.md +206 -0
  47. package/cli/templates/CLAUDE.md.template +57 -0
  48. package/cli/templates/agent-roles/backend.md +47 -0
  49. package/cli/templates/agent-roles/cmo.md +52 -0
  50. package/cli/templates/agent-roles/content.md +48 -0
  51. package/cli/templates/agent-roles/coo.md +66 -0
  52. package/cli/templates/agent-roles/cpo.md +52 -0
  53. package/cli/templates/agent-roles/cto.md +63 -0
  54. package/cli/templates/agent-roles/data.md +46 -0
  55. package/cli/templates/agent-roles/design.md +46 -0
  56. package/cli/templates/agent-roles/frontend.md +47 -0
  57. package/cli/templates/agent-roles/fullstack.md +47 -0
  58. package/cli/templates/agent-roles/qa.md +46 -0
  59. package/cli/templates/backlog.json.template +3 -0
  60. package/cli/templates/directive.json.template +9 -0
  61. package/cli/templates/directive.md.template +23 -0
  62. package/cli/templates/goals-index.md +21 -0
  63. package/cli/templates/gruai.config.json.template +12 -0
  64. package/cli/templates/lessons.md +16 -0
  65. package/cli/templates/vision.md +35 -0
  66. package/cli/templates/welcome-directive/directive.json +9 -0
  67. package/cli/templates/welcome-directive/directive.md +53 -0
  68. package/dist/assets/GamePage-C5XQQOQH.js +49 -0
  69. package/dist/assets/README.md +17 -0
  70. package/dist/assets/characters/char_0.png +0 -0
  71. package/dist/assets/characters/char_1.png +0 -0
  72. package/dist/assets/characters/char_10.png +0 -0
  73. package/dist/assets/characters/char_11.png +0 -0
  74. package/dist/assets/characters/char_2.png +0 -0
  75. package/dist/assets/characters/char_3.png +0 -0
  76. package/dist/assets/characters/char_4.png +0 -0
  77. package/dist/assets/characters/char_5.png +0 -0
  78. package/dist/assets/characters/char_6.png +0 -0
  79. package/dist/assets/characters/char_7.png +0 -0
  80. package/dist/assets/characters/char_8.png +0 -0
  81. package/dist/assets/characters/char_9.png +0 -0
  82. package/dist/assets/index-CnTPDqpP.js +12 -0
  83. package/dist/assets/index-gR5q7ikB.css +1 -0
  84. package/dist/assets/office/furniture.png +0 -0
  85. package/dist/assets/office/room-builder.png +0 -0
  86. package/dist/index.html +16 -0
  87. package/dist-server/scripts/intelligence-trends.d.ts +100 -0
  88. package/dist-server/scripts/intelligence-trends.js +365 -0
  89. package/dist-server/server/actions/cleanup.d.ts +4 -0
  90. package/dist-server/server/actions/cleanup.js +30 -0
  91. package/dist-server/server/actions/send-input.d.ts +6 -0
  92. package/dist-server/server/actions/send-input.js +147 -0
  93. package/dist-server/server/actions/terminal.d.ts +4 -0
  94. package/dist-server/server/actions/terminal.js +427 -0
  95. package/dist-server/server/config.d.ts +9 -0
  96. package/dist-server/server/config.js +217 -0
  97. package/dist-server/server/db.d.ts +7 -0
  98. package/dist-server/server/db.js +79 -0
  99. package/dist-server/server/hooks/event-receiver.d.ts +11 -0
  100. package/dist-server/server/hooks/event-receiver.js +36 -0
  101. package/dist-server/server/index.d.ts +1 -0
  102. package/dist-server/server/index.js +552 -0
  103. package/dist-server/server/notifications/macos.d.ts +5 -0
  104. package/dist-server/server/notifications/macos.js +22 -0
  105. package/dist-server/server/notifications/notifier.d.ts +17 -0
  106. package/dist-server/server/notifications/notifier.js +110 -0
  107. package/dist-server/server/parsers/process-discovery.d.ts +39 -0
  108. package/dist-server/server/parsers/process-discovery.js +776 -0
  109. package/dist-server/server/parsers/session-scanner.d.ts +56 -0
  110. package/dist-server/server/parsers/session-scanner.js +390 -0
  111. package/dist-server/server/parsers/session-state.d.ts +68 -0
  112. package/dist-server/server/parsers/session-state.js +696 -0
  113. package/dist-server/server/parsers/session-state.test.d.ts +1 -0
  114. package/dist-server/server/parsers/session-state.test.js +950 -0
  115. package/dist-server/server/parsers/task-parser.d.ts +10 -0
  116. package/dist-server/server/parsers/task-parser.js +97 -0
  117. package/dist-server/server/parsers/team-parser.d.ts +3 -0
  118. package/dist-server/server/parsers/team-parser.js +67 -0
  119. package/dist-server/server/platform/__tests__/claude-code.test.d.ts +1 -0
  120. package/dist-server/server/platform/__tests__/claude-code.test.js +311 -0
  121. package/dist-server/server/platform/claude-code.d.ts +34 -0
  122. package/dist-server/server/platform/claude-code.js +94 -0
  123. package/dist-server/server/platform/index.d.ts +5 -0
  124. package/dist-server/server/platform/index.js +1 -0
  125. package/dist-server/server/platform/types.d.ts +190 -0
  126. package/dist-server/server/platform/types.js +9 -0
  127. package/dist-server/server/state/aggregator.d.ts +42 -0
  128. package/dist-server/server/state/aggregator.js +1080 -0
  129. package/dist-server/server/state/work-item-types.d.ts +555 -0
  130. package/dist-server/server/state/work-item-types.js +168 -0
  131. package/dist-server/server/types.d.ts +237 -0
  132. package/dist-server/server/types.js +1 -0
  133. package/dist-server/server/watchers/claude-watcher.d.ts +17 -0
  134. package/dist-server/server/watchers/claude-watcher.js +130 -0
  135. package/dist-server/server/watchers/context-watcher.d.ts +22 -0
  136. package/dist-server/server/watchers/context-watcher.js +125 -0
  137. package/dist-server/server/watchers/directive-watcher.d.ts +46 -0
  138. package/dist-server/server/watchers/directive-watcher.js +497 -0
  139. package/dist-server/server/watchers/session-watcher.d.ts +18 -0
  140. package/dist-server/server/watchers/session-watcher.js +126 -0
  141. package/dist-server/server/watchers/state-watcher.d.ts +36 -0
  142. package/dist-server/server/watchers/state-watcher.js +369 -0
  143. package/package.json +68 -0
@@ -0,0 +1,46 @@
1
+ import type { Aggregator } from '../state/aggregator.js';
2
+ import type { DirectiveState } from '../types.js';
3
+ export declare class DirectiveWatcher {
4
+ private directivesWatcher;
5
+ private aggregator;
6
+ private directivesDir;
7
+ private debounceTimer;
8
+ private pollTimer;
9
+ private _ready;
10
+ /** Snapshot of last emitted state hash for change detection in poll fallback */
11
+ private lastStateHash;
12
+ /** mtime-based cache: dirId -> { mtimeMs, state } */
13
+ private historyCache;
14
+ constructor(aggregator: Aggregator, _claudeHome: string);
15
+ start(): void;
16
+ get ready(): boolean;
17
+ stop(): Promise<void>;
18
+ /**
19
+ * Find the active directive (status = in_progress or awaiting_completion)
20
+ * and build DirectiveState from directive.json + project.json files.
21
+ */
22
+ readCurrentState(): DirectiveState | null;
23
+ /**
24
+ * Return DirectiveState[] for all active directives (in_progress, awaiting_completion, reopened).
25
+ * Filters from readAllDirectiveStates() to get only actionable ones.
26
+ */
27
+ readActiveDirectives(): DirectiveState[];
28
+ /**
29
+ * Build DirectiveState[] for ALL directives (completed, failed, in_progress, etc.).
30
+ * Uses mtime-based caching so we only re-parse directive.json when it changes.
31
+ */
32
+ readAllDirectiveStates(): DirectiveState[];
33
+ private buildStateFromDirective;
34
+ private mapProjectStatus;
35
+ private readDirectiveJson;
36
+ private readJson;
37
+ private readTextFile;
38
+ private listDirs;
39
+ /**
40
+ * Poll fallback: check if any directive.json or project.json mtimes changed
41
+ * since the last update. Only triggers readAndUpdate if changes detected.
42
+ */
43
+ private pollForChanges;
44
+ private handleChange;
45
+ private readAndUpdate;
46
+ }
@@ -0,0 +1,497 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { watch } from 'chokidar';
4
+ // Pipeline steps by weight class. Steps not in a weight's list are skipped.
5
+ const FULL_PIPELINE_STEPS = [
6
+ { id: 'triage', label: 'Triage' },
7
+ { id: 'read', label: 'Read' },
8
+ { id: 'context', label: 'Context' },
9
+ { id: 'challenge', label: 'Challenge' },
10
+ { id: 'brainstorm', label: 'Brainstorm' },
11
+ { id: 'plan', label: 'Plan' },
12
+ { id: 'audit', label: 'Audit' },
13
+ { id: 'approve', label: 'Approve' },
14
+ { id: 'project-brainstorm', label: 'Project Brainstorm' },
15
+ { id: 'setup', label: 'Setup' },
16
+ { id: 'execute', label: 'Execute' },
17
+ { id: 'review-gate', label: 'Review Gate' },
18
+ { id: 'wrapup', label: 'Wrapup' },
19
+ { id: 'completion', label: 'Completion' },
20
+ ];
21
+ const SKIPPED_STEPS = {
22
+ lightweight: new Set(['challenge', 'brainstorm', 'approve']),
23
+ medium: new Set(['challenge']),
24
+ heavyweight: new Set([]),
25
+ strategic: new Set([]),
26
+ };
27
+ // ---------------------------------------------------------------------------
28
+ // Build pipeline steps from directive.json's pipeline{} object
29
+ // ---------------------------------------------------------------------------
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ function buildPipelineFromDirective(directive) {
32
+ const weight = directive.weight ?? 'medium';
33
+ const skipped = SKIPPED_STEPS[weight];
34
+ const pipeline = directive.pipeline ?? {};
35
+ // Build an ordered index so we can infer completed steps from current position
36
+ const currentStepId = directive.current_step ?? '';
37
+ const currentIdx = FULL_PIPELINE_STEPS.findIndex(s => s.id === currentStepId);
38
+ return FULL_PIPELINE_STEPS
39
+ .map((def, idx) => {
40
+ const stepData = pipeline[def.id];
41
+ const isSkipped = skipped?.has(def.id) && !stepData;
42
+ // Infer status: if no explicit data but directive is past this step, treat as completed
43
+ let inferredStatus = stepData?.status ?? 'pending';
44
+ if (!stepData && !isSkipped && currentIdx > idx) {
45
+ inferredStatus = 'completed';
46
+ }
47
+ const step = {
48
+ id: def.id,
49
+ label: def.label,
50
+ status: isSkipped ? 'skipped' : inferredStatus,
51
+ };
52
+ // Build artifacts from step output + agent
53
+ const artifacts = {};
54
+ if (stepData?.agent)
55
+ artifacts['Agent'] = stepData.agent;
56
+ if (stepData?.reviewers?.length > 0) {
57
+ artifacts['Reviewers'] = stepData.reviewers
58
+ .map((r) => r.charAt(0).toUpperCase() + r.slice(1))
59
+ .join(', ');
60
+ }
61
+ if (stepData?.output && typeof stepData.output === 'object') {
62
+ for (const [k, v] of Object.entries(stepData.output)) {
63
+ if (typeof v === 'string' && v) {
64
+ artifacts[k.charAt(0).toUpperCase() + k.slice(1)] = v;
65
+ }
66
+ }
67
+ }
68
+ if (stepData?.artifacts?.length > 0) {
69
+ artifacts['Files'] = stepData.artifacts
70
+ .map((p) => String(p).split('/').pop())
71
+ .join(', ');
72
+ }
73
+ if (Object.keys(artifacts).length > 0)
74
+ step.artifacts = artifacts;
75
+ if (step.status === 'active' && def.id === 'approve')
76
+ step.needsAction = true;
77
+ if (step.status === 'active' && def.id === 'completion')
78
+ step.needsAction = true;
79
+ if (directive.status === 'awaiting_completion' && def.id === 'completion') {
80
+ step.status = 'active';
81
+ step.needsAction = true;
82
+ }
83
+ if (step.status === 'active' && directive.updated_at)
84
+ step.startedAt = directive.updated_at;
85
+ return step;
86
+ });
87
+ }
88
+ // Derive pipeline steps when directive.json has no pipeline{} (legacy or simple)
89
+ function derivePipelineSteps(weight, directiveStatus) {
90
+ const skipped = SKIPPED_STEPS[weight];
91
+ const isCompleted = directiveStatus === 'completed';
92
+ const isFailed = directiveStatus === 'failed';
93
+ return FULL_PIPELINE_STEPS.map((def) => {
94
+ const isSkipped = skipped?.has(def.id);
95
+ let status;
96
+ if (isSkipped)
97
+ status = 'skipped';
98
+ else if (isCompleted)
99
+ status = 'completed';
100
+ else if (isFailed)
101
+ status = def.id === 'wrapup' || def.id === 'completion' ? 'failed' : 'completed';
102
+ else
103
+ status = 'pending';
104
+ return { id: def.id, label: def.label, status };
105
+ });
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Map directive.json status to DirectiveState status
109
+ // ---------------------------------------------------------------------------
110
+ function mapStatus(status) {
111
+ switch (status) {
112
+ case 'in_progress': return 'in_progress';
113
+ case 'awaiting_completion': return 'awaiting_completion';
114
+ case 'completed': return 'completed';
115
+ case 'failed': return 'failed';
116
+ case 'cancelled': return 'completed';
117
+ case 'pending': return 'pending';
118
+ case 'triaged': return 'pending';
119
+ default: return 'pending';
120
+ }
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // DirectiveWatcher — watches directive.json files directly
124
+ // No more current.json dependency — derives everything from directive.json
125
+ // + project.json files in the directive's projects/ subdirectory.
126
+ // ---------------------------------------------------------------------------
127
+ export class DirectiveWatcher {
128
+ directivesWatcher = null;
129
+ aggregator;
130
+ directivesDir;
131
+ debounceTimer = null;
132
+ pollTimer = null;
133
+ _ready = false;
134
+ /** Snapshot of last emitted state hash for change detection in poll fallback */
135
+ lastStateHash = '';
136
+ /** mtime-based cache: dirId -> { mtimeMs, state } */
137
+ historyCache = new Map();
138
+ constructor(aggregator, _claudeHome) {
139
+ this.aggregator = aggregator;
140
+ this.directivesDir = path.join(process.cwd(), '.context', 'directives');
141
+ }
142
+ start() {
143
+ // Read initial state
144
+ this.readAndUpdate();
145
+ // Watch .context/directives/ for directive.json and project.json changes
146
+ if (!fs.existsSync(this.directivesDir)) {
147
+ try {
148
+ fs.mkdirSync(this.directivesDir, { recursive: true });
149
+ }
150
+ catch {
151
+ console.log(`[directive-watcher] Could not create directives dir, skipping`);
152
+ this._ready = true;
153
+ return;
154
+ }
155
+ }
156
+ console.log(`[directive-watcher] Watching directives at ${this.directivesDir}`);
157
+ this.directivesWatcher = watch(this.directivesDir, {
158
+ ignoreInitial: true,
159
+ persistent: true,
160
+ depth: 4, // Deep enough for {id}/projects/{proj-id}/project.json
161
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
162
+ });
163
+ this.directivesWatcher.on('all', (_event, filePath) => {
164
+ if (!filePath.endsWith('.json'))
165
+ return;
166
+ this.handleChange();
167
+ });
168
+ this.directivesWatcher.on('ready', () => {
169
+ this._ready = true;
170
+ console.log(`[directive-watcher] Ready`);
171
+ });
172
+ this.directivesWatcher.on('error', (err) => {
173
+ console.error(`[directive-watcher] Error:`, err);
174
+ });
175
+ // Periodic poll fallback — catches missed chokidar events (macOS FSEvents limits, awaitWriteFinish stalls)
176
+ this.pollTimer = setInterval(() => {
177
+ this.pollForChanges();
178
+ }, 5000);
179
+ }
180
+ get ready() {
181
+ return this._ready;
182
+ }
183
+ async stop() {
184
+ if (this.debounceTimer) {
185
+ clearTimeout(this.debounceTimer);
186
+ this.debounceTimer = null;
187
+ }
188
+ if (this.pollTimer) {
189
+ clearInterval(this.pollTimer);
190
+ this.pollTimer = null;
191
+ }
192
+ if (this.directivesWatcher) {
193
+ await this.directivesWatcher.close();
194
+ this.directivesWatcher = null;
195
+ }
196
+ }
197
+ /**
198
+ * Find the active directive (status = in_progress or awaiting_completion)
199
+ * and build DirectiveState from directive.json + project.json files.
200
+ */
201
+ readCurrentState() {
202
+ try {
203
+ const dirIds = this.listDirs(this.directivesDir);
204
+ // Find all active directives, pick the most recently updated
205
+ let best = null;
206
+ for (const dirId of dirIds) {
207
+ const directive = this.readDirectiveJson(dirId);
208
+ if (!directive)
209
+ continue;
210
+ const status = String(directive.status ?? '');
211
+ if (status !== 'in_progress' && status !== 'awaiting_completion')
212
+ continue;
213
+ const updatedAt = String(directive.updated_at ?? directive.started_at ?? directive.created ?? '');
214
+ if (!best || updatedAt > best.updatedAt) {
215
+ best = { dirId, directive, updatedAt };
216
+ }
217
+ }
218
+ if (best) {
219
+ return this.buildStateFromDirective(best.dirId, best.directive);
220
+ }
221
+ return null;
222
+ }
223
+ catch (err) {
224
+ console.error(`[directive-watcher] readCurrentState error:`, err);
225
+ return null;
226
+ }
227
+ }
228
+ /**
229
+ * Return DirectiveState[] for all active directives (in_progress, awaiting_completion, reopened).
230
+ * Filters from readAllDirectiveStates() to get only actionable ones.
231
+ */
232
+ readActiveDirectives() {
233
+ const all = this.readAllDirectiveStates();
234
+ const activeStatuses = new Set(['in_progress', 'awaiting_completion']);
235
+ return all.filter((d) => activeStatuses.has(d.status));
236
+ }
237
+ /**
238
+ * Build DirectiveState[] for ALL directives (completed, failed, in_progress, etc.).
239
+ * Uses mtime-based caching so we only re-parse directive.json when it changes.
240
+ */
241
+ readAllDirectiveStates() {
242
+ try {
243
+ const dirIds = this.listDirs(this.directivesDir);
244
+ const results = [];
245
+ const seenIds = new Set();
246
+ for (const dirId of dirIds) {
247
+ seenIds.add(dirId);
248
+ const filePath = path.join(this.directivesDir, dirId, 'directive.json');
249
+ // Check mtime for cache validity
250
+ let mtimeMs;
251
+ try {
252
+ const stat = fs.statSync(filePath);
253
+ mtimeMs = stat.mtimeMs;
254
+ }
255
+ catch {
256
+ // No directive.json in this dir — skip
257
+ continue;
258
+ }
259
+ const cached = this.historyCache.get(dirId);
260
+ if (cached && cached.mtimeMs === mtimeMs && cached.state.status !== 'in_progress' && cached.state.status !== 'awaiting_completion') {
261
+ results.push(cached.state);
262
+ continue;
263
+ }
264
+ // Cache miss — re-parse
265
+ const directive = this.readJson(filePath);
266
+ if (!directive)
267
+ continue;
268
+ const state = this.buildStateFromDirective(dirId, directive);
269
+ this.historyCache.set(dirId, { mtimeMs, state });
270
+ results.push(state);
271
+ }
272
+ // Prune deleted directives from cache
273
+ for (const cachedId of this.historyCache.keys()) {
274
+ if (!seenIds.has(cachedId)) {
275
+ this.historyCache.delete(cachedId);
276
+ }
277
+ }
278
+ return results;
279
+ }
280
+ catch (err) {
281
+ console.error(`[directive-watcher] readAllDirectiveStates error:`, err);
282
+ return [];
283
+ }
284
+ }
285
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
286
+ buildStateFromDirective(dirId, directive) {
287
+ // Read projects from the directive's projects/ subdirectory
288
+ const projectsDir = path.join(this.directivesDir, dirId, 'projects');
289
+ const projects = [];
290
+ if (fs.existsSync(projectsDir)) {
291
+ const projIds = this.listDirs(projectsDir);
292
+ for (const projId of projIds) {
293
+ const projJsonPath = path.join(projectsDir, projId, 'project.json');
294
+ const projJson = this.readJson(projJsonPath);
295
+ if (!projJson)
296
+ continue;
297
+ const tasks = Array.isArray(projJson.tasks) ? projJson.tasks : [];
298
+ const totalTasks = tasks.length;
299
+ const completedTasks = tasks.filter((t) => t.status === 'completed' || t.status === 'done').length;
300
+ // Determine phase from current task status
301
+ let phase = null;
302
+ if (projJson.status === 'in_progress') {
303
+ const activeTask = tasks.find((t) => t.status === 'in_progress');
304
+ if (activeTask)
305
+ phase = 'build';
306
+ }
307
+ projects.push({
308
+ id: projId,
309
+ title: String(projJson.title ?? projId),
310
+ status: this.mapProjectStatus(String(projJson.status ?? 'pending')),
311
+ phase,
312
+ totalTasks,
313
+ completedTasks,
314
+ tasks: tasks.map((t) => ({
315
+ title: String(t.title ?? ''),
316
+ status: String(t.status ?? 'pending'),
317
+ agent: t.agent ? String(t.agent) : undefined,
318
+ dod: Array.isArray(t.dod) ? t.dod.map((d) => ({
319
+ criterion: String(d.criterion ?? ''),
320
+ met: !!d.met,
321
+ })) : undefined,
322
+ })),
323
+ });
324
+ }
325
+ }
326
+ // Also check produced_projects for projects stored elsewhere
327
+ if (Array.isArray(directive.produced_projects)) {
328
+ for (const prodPath of directive.produced_projects) {
329
+ const prodId = String(prodPath).split('/').pop() ?? '';
330
+ // Skip if already found in directive's projects/ dir
331
+ if (projects.some(p => p.id === prodId))
332
+ continue;
333
+ // Projects are now always under directive's projects/ dir
334
+ }
335
+ }
336
+ const completedCount = projects.filter(p => p.status === 'completed').length;
337
+ const executeOutput = directive.pipeline?.execute?.output;
338
+ const currentStep = directive.current_step ?? '';
339
+ // Determine current phase from pipeline state (named step IDs)
340
+ let currentPhase = 'unknown';
341
+ if (currentStep === 'execute' || currentStep === 'review-gate')
342
+ currentPhase = 'executing';
343
+ else if (currentStep === 'wrapup')
344
+ currentPhase = 'wrapup';
345
+ else if (currentStep === 'completion')
346
+ currentPhase = 'completion';
347
+ else if (['triage', 'read', 'context', 'challenge', 'brainstorm', 'plan', 'audit', 'approve', 'project-brainstorm', 'setup'].includes(currentStep))
348
+ currentPhase = 'planning';
349
+ else if (directive.pipeline?.execute?.status === 'completed')
350
+ currentPhase = 'wrapup';
351
+ // Build pipeline steps
352
+ let pipelineSteps;
353
+ if (directive.pipeline && Object.keys(directive.pipeline).length > 0) {
354
+ pipelineSteps = buildPipelineFromDirective(directive);
355
+ }
356
+ else {
357
+ pipelineSteps = derivePipelineSteps(directive.weight ?? 'medium', directive.status);
358
+ }
359
+ return {
360
+ directiveName: dirId,
361
+ title: directive.title ?? dirId,
362
+ status: mapStatus(directive.status),
363
+ totalProjects: projects.length,
364
+ currentProject: completedCount,
365
+ currentPhase,
366
+ projects,
367
+ startedAt: directive.started_at ?? directive.created ?? new Date().toISOString(),
368
+ lastUpdated: directive.updated_at ?? new Date().toISOString(),
369
+ pipelineSteps,
370
+ currentStepId: currentStep,
371
+ weight: directive.weight,
372
+ category: directive.category,
373
+ triageRationale: directive.triage?.rationale,
374
+ approvalStatus: directive.planning?.ceo_approval?.status,
375
+ brainstormSummary: directive.pipeline?.brainstorm?.output?.summary,
376
+ planSummary: directive.pipeline?.plan?.output?.summary ?? directive.pipeline?.plan?.output?.projects,
377
+ brainstormContent: this.readTextFile(path.join(this.directivesDir, dirId, 'brainstorm.md')),
378
+ directiveBrief: this.readTextFile(path.join(this.directivesDir, dirId, 'directive.md')),
379
+ };
380
+ }
381
+ mapProjectStatus(status) {
382
+ switch (status) {
383
+ case 'completed': return 'completed';
384
+ case 'in_progress': return 'in_progress';
385
+ case 'failed': return 'failed';
386
+ case 'skipped': return 'skipped';
387
+ default: return 'pending';
388
+ }
389
+ }
390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
+ readDirectiveJson(dirId) {
392
+ const filePath = path.join(this.directivesDir, dirId, 'directive.json');
393
+ return this.readJson(filePath);
394
+ }
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
+ readJson(filePath) {
397
+ try {
398
+ const raw = fs.readFileSync(filePath, 'utf-8');
399
+ return JSON.parse(raw);
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ readTextFile(filePath) {
406
+ try {
407
+ return fs.readFileSync(filePath, 'utf-8');
408
+ }
409
+ catch {
410
+ return undefined;
411
+ }
412
+ }
413
+ listDirs(dirPath) {
414
+ try {
415
+ return fs.readdirSync(dirPath).filter((name) => {
416
+ if (name.startsWith('.') || name.startsWith('_'))
417
+ return false;
418
+ try {
419
+ return fs.statSync(path.join(dirPath, name)).isDirectory();
420
+ }
421
+ catch {
422
+ return false;
423
+ }
424
+ });
425
+ }
426
+ catch {
427
+ return [];
428
+ }
429
+ }
430
+ /**
431
+ * Poll fallback: check if any directive.json or project.json mtimes changed
432
+ * since the last update. Only triggers readAndUpdate if changes detected.
433
+ */
434
+ pollForChanges() {
435
+ try {
436
+ const dirIds = this.listDirs(this.directivesDir);
437
+ // Build a lightweight hash from mtimes of all directive.json + project.json files
438
+ const parts = [];
439
+ for (const dirId of dirIds) {
440
+ try {
441
+ const stat = fs.statSync(path.join(this.directivesDir, dirId, 'directive.json'));
442
+ parts.push(`${dirId}:${stat.mtimeMs}`);
443
+ }
444
+ catch {
445
+ continue;
446
+ }
447
+ // Also check project.json files
448
+ const projDir = path.join(this.directivesDir, dirId, 'projects');
449
+ try {
450
+ const projIds = fs.readdirSync(projDir);
451
+ for (const pId of projIds) {
452
+ try {
453
+ const pStat = fs.statSync(path.join(projDir, pId, 'project.json'));
454
+ parts.push(`${dirId}/${pId}:${pStat.mtimeMs}`);
455
+ }
456
+ catch {
457
+ continue;
458
+ }
459
+ }
460
+ }
461
+ catch { /* no projects dir */ }
462
+ }
463
+ const hash = parts.join('|');
464
+ if (hash !== this.lastStateHash) {
465
+ this.lastStateHash = hash;
466
+ this.readAndUpdate();
467
+ }
468
+ }
469
+ catch {
470
+ // Poll failure is non-critical
471
+ }
472
+ }
473
+ handleChange() {
474
+ if (this.debounceTimer) {
475
+ clearTimeout(this.debounceTimer);
476
+ }
477
+ this.debounceTimer = setTimeout(() => {
478
+ this.debounceTimer = null;
479
+ this.readAndUpdate();
480
+ }, 300);
481
+ }
482
+ readAndUpdate() {
483
+ // Single pass: read all directives once, derive active + best from the result
484
+ const history = this.readAllDirectiveStates();
485
+ const activeStatuses = new Set(['in_progress', 'awaiting_completion']);
486
+ const activeDirectives = history.filter((d) => activeStatuses.has(d.status));
487
+ // Pick the most recently updated active directive as the singular state (backward compat)
488
+ let state = null;
489
+ for (const d of activeDirectives) {
490
+ if (!state || d.lastUpdated > state.lastUpdated) {
491
+ state = d;
492
+ }
493
+ }
494
+ console.log(`[directive-watcher] Directive state: ${state ? `${state.directiveName} (${state.status}, ${state.currentProject}/${state.totalProjects})` : 'none'} | history: ${history.length} directives | active: ${activeDirectives.length}`);
495
+ this.aggregator.updateDirectiveState(state, history, activeDirectives);
496
+ }
497
+ }
@@ -0,0 +1,18 @@
1
+ import type { AggregatorHandle } from '../platform/types.js';
2
+ import type { PlatformAdapter } from '../platform/types.js';
3
+ export declare class SessionWatcher {
4
+ private watcher;
5
+ private aggregator;
6
+ private claudeHome;
7
+ private projectFilter?;
8
+ private adapter;
9
+ private activityTimers;
10
+ private sessionRefreshTimer;
11
+ private _ready;
12
+ constructor(aggregator: AggregatorHandle, claudeHome: string, projectFilter?: string, adapter?: PlatformAdapter);
13
+ start(): void;
14
+ get ready(): boolean;
15
+ stop(): Promise<void>;
16
+ private scheduleSessionRefresh;
17
+ private handleActivityChange;
18
+ }
@@ -0,0 +1,126 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { watch } from 'chokidar';
4
+ import { processFileUpdate as processFileUpdateRaw, getOrBootstrap as getOrBootstrapRaw, removeFileState as removeFileStateRaw, toSessionActivity as toSessionActivityRaw, } from '../parsers/session-state.js';
5
+ export class SessionWatcher {
6
+ watcher = null;
7
+ aggregator;
8
+ claudeHome;
9
+ projectFilter;
10
+ adapter;
11
+ activityTimers = new Map();
12
+ sessionRefreshTimer = null;
13
+ _ready = false;
14
+ constructor(aggregator, claudeHome, projectFilter, adapter) {
15
+ this.aggregator = aggregator;
16
+ this.claudeHome = claudeHome;
17
+ this.projectFilter = projectFilter;
18
+ this.adapter = adapter ?? null;
19
+ }
20
+ start() {
21
+ // When a project filter is set, watch only that specific project directory
22
+ const projectsDir = this.projectFilter
23
+ ? path.join(this.claudeHome, 'projects', this.projectFilter)
24
+ : path.join(this.claudeHome, 'projects');
25
+ if (!fs.existsSync(projectsDir)) {
26
+ console.log(`[session-watcher] Projects directory not found: ${projectsDir}, skipping watch`);
27
+ this._ready = true;
28
+ return;
29
+ }
30
+ console.log(`[session-watcher] Watching ${projectsDir}`);
31
+ this.watcher = watch(projectsDir, {
32
+ ignoreInitial: true,
33
+ persistent: true,
34
+ });
35
+ this.watcher.on('all', (event, filePath) => {
36
+ // Only care about JSONL files
37
+ if (!filePath.endsWith('.jsonl'))
38
+ return;
39
+ if (event === 'add' || event === 'unlink') {
40
+ // Session file added or deleted — refresh session list (1s debounce)
41
+ if (event === 'unlink') {
42
+ if (this.adapter) {
43
+ this.adapter.removeFileState(filePath);
44
+ }
45
+ else {
46
+ removeFileStateRaw(filePath);
47
+ }
48
+ }
49
+ this.scheduleSessionRefresh();
50
+ }
51
+ if (event === 'change' || event === 'add') {
52
+ // Activity update (500ms debounce per file)
53
+ this.handleActivityChange(filePath);
54
+ }
55
+ });
56
+ this.watcher.on('ready', () => {
57
+ this._ready = true;
58
+ console.log(`[session-watcher] Ready`);
59
+ });
60
+ this.watcher.on('error', (err) => {
61
+ console.error(`[session-watcher] Error:`, err);
62
+ });
63
+ }
64
+ get ready() {
65
+ return this._ready;
66
+ }
67
+ async stop() {
68
+ for (const timer of this.activityTimers.values()) {
69
+ clearTimeout(timer);
70
+ }
71
+ this.activityTimers.clear();
72
+ if (this.sessionRefreshTimer) {
73
+ clearTimeout(this.sessionRefreshTimer);
74
+ this.sessionRefreshTimer = null;
75
+ }
76
+ if (this.watcher) {
77
+ await this.watcher.close();
78
+ this.watcher = null;
79
+ }
80
+ }
81
+ scheduleSessionRefresh() {
82
+ if (this.sessionRefreshTimer) {
83
+ clearTimeout(this.sessionRefreshTimer);
84
+ }
85
+ this.sessionRefreshTimer = setTimeout(() => {
86
+ this.sessionRefreshTimer = null;
87
+ console.log('[session-watcher] Refreshing sessions (new/deleted file detected)');
88
+ this.aggregator.refreshSessions();
89
+ }, 1000);
90
+ }
91
+ handleActivityChange(filePath) {
92
+ const existing = this.activityTimers.get(filePath);
93
+ if (existing) {
94
+ clearTimeout(existing);
95
+ }
96
+ this.activityTimers.set(filePath, setTimeout(() => {
97
+ this.activityTimers.delete(filePath);
98
+ // Incremental update: reads only new bytes since last offset
99
+ const state = this.adapter
100
+ ? this.adapter.processFileUpdate(filePath)
101
+ : processFileUpdateRaw(filePath);
102
+ if (state) {
103
+ const activity = this.adapter
104
+ ? this.adapter.toSessionActivity(state)
105
+ : toSessionActivityRaw(state);
106
+ if (activity && activity.active) {
107
+ console.log(`[session-watcher] Activity for session ${activity.sessionId}: ${activity.tool ?? (activity.thinking ? 'thinking' : 'idle')}`);
108
+ this.aggregator.updateSessionFromFileState(filePath, state);
109
+ }
110
+ else {
111
+ // File changed but activity not active — still update state
112
+ this.aggregator.updateSessionFromFileState(filePath, state);
113
+ }
114
+ }
115
+ else {
116
+ // No new data or bootstrap failed — try bootstrap for new files
117
+ const bootstrapped = this.adapter
118
+ ? this.adapter.getOrBootstrap(filePath)
119
+ : getOrBootstrapRaw(filePath);
120
+ if (bootstrapped) {
121
+ this.aggregator.updateSessionFromFileState(filePath, bootstrapped);
122
+ }
123
+ }
124
+ }, 500));
125
+ }
126
+ }