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.
- package/.claude/skills/brainstorm/SKILL.md +340 -0
- package/.claude/skills/code-review-excellence/SKILL.md +198 -0
- package/.claude/skills/directive/SKILL.md +121 -0
- package/.claude/skills/directive/docs/pipeline/00-delegation-and-triage.md +181 -0
- package/.claude/skills/directive/docs/pipeline/01-checkpoint.md +34 -0
- package/.claude/skills/directive/docs/pipeline/02-read-directive.md +38 -0
- package/.claude/skills/directive/docs/pipeline/03-read-context.md +15 -0
- package/.claude/skills/directive/docs/pipeline/04-challenge.md +38 -0
- package/.claude/skills/directive/docs/pipeline/05-planning.md +64 -0
- package/.claude/skills/directive/docs/pipeline/06-technical-audit.md +88 -0
- package/.claude/skills/directive/docs/pipeline/07-plan-approval.md +145 -0
- package/.claude/skills/directive/docs/pipeline/07b-project-brainstorm.md +85 -0
- package/.claude/skills/directive/docs/pipeline/08-worktree-and-state.md +50 -0
- package/.claude/skills/directive/docs/pipeline/09-execute-projects.md +709 -0
- package/.claude/skills/directive/docs/pipeline/10-wrapup.md +242 -0
- package/.claude/skills/directive/docs/pipeline/11-completion-gate.md +75 -0
- package/.claude/skills/directive/docs/reference/rules/casting-rules.md +78 -0
- package/.claude/skills/directive/docs/reference/rules/failure-handling.md +20 -0
- package/.claude/skills/directive/docs/reference/rules/phase-definitions.md +42 -0
- package/.claude/skills/directive/docs/reference/rules/scope-and-dod.md +30 -0
- package/.claude/skills/directive/docs/reference/schemas/audit-output.md +44 -0
- package/.claude/skills/directive/docs/reference/schemas/brainstorm-output.md +52 -0
- package/.claude/skills/directive/docs/reference/schemas/challenger-output.md +13 -0
- package/.claude/skills/directive/docs/reference/schemas/checkpoint.md +18 -0
- package/.claude/skills/directive/docs/reference/schemas/current-json.md +5 -0
- package/.claude/skills/directive/docs/reference/schemas/directive-json.md +143 -0
- package/.claude/skills/directive/docs/reference/schemas/investigation-output.md +37 -0
- package/.claude/skills/directive/docs/reference/schemas/plan-schema.md +103 -0
- package/.claude/skills/directive/docs/reference/templates/architect-prompt.md +66 -0
- package/.claude/skills/directive/docs/reference/templates/auditor-prompt.md +53 -0
- package/.claude/skills/directive/docs/reference/templates/brainstorm-prompt.md +68 -0
- package/.claude/skills/directive/docs/reference/templates/challenger-prompt.md +35 -0
- package/.claude/skills/directive/docs/reference/templates/digest.md +134 -0
- package/.claude/skills/directive/docs/reference/templates/investigator-prompt.md +51 -0
- package/.claude/skills/directive/docs/reference/templates/planner-prompt.md +130 -0
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/.claude/skills/gruai-agents/SKILL.md +161 -0
- package/.claude/skills/gruai-config/SKILL.md +61 -0
- package/.claude/skills/healthcheck/SKILL.md +216 -0
- package/.claude/skills/report/SKILL.md +380 -0
- package/.claude/skills/scout/SKILL.md +452 -0
- package/.claude/skills/seo-audit/SKILL.md +107 -0
- package/.claude/skills/walkthrough/SKILL.md +274 -0
- package/.claude/skills/webapp-testing/SKILL.md +96 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/cli/templates/CLAUDE.md.template +57 -0
- package/cli/templates/agent-roles/backend.md +47 -0
- package/cli/templates/agent-roles/cmo.md +52 -0
- package/cli/templates/agent-roles/content.md +48 -0
- package/cli/templates/agent-roles/coo.md +66 -0
- package/cli/templates/agent-roles/cpo.md +52 -0
- package/cli/templates/agent-roles/cto.md +63 -0
- package/cli/templates/agent-roles/data.md +46 -0
- package/cli/templates/agent-roles/design.md +46 -0
- package/cli/templates/agent-roles/frontend.md +47 -0
- package/cli/templates/agent-roles/fullstack.md +47 -0
- package/cli/templates/agent-roles/qa.md +46 -0
- package/cli/templates/backlog.json.template +3 -0
- package/cli/templates/directive.json.template +9 -0
- package/cli/templates/directive.md.template +23 -0
- package/cli/templates/goals-index.md +21 -0
- package/cli/templates/gruai.config.json.template +12 -0
- package/cli/templates/lessons.md +16 -0
- package/cli/templates/vision.md +35 -0
- package/cli/templates/welcome-directive/directive.json +9 -0
- package/cli/templates/welcome-directive/directive.md +53 -0
- package/dist/assets/GamePage-C5XQQOQH.js +49 -0
- package/dist/assets/README.md +17 -0
- package/dist/assets/characters/char_0.png +0 -0
- package/dist/assets/characters/char_1.png +0 -0
- package/dist/assets/characters/char_10.png +0 -0
- package/dist/assets/characters/char_11.png +0 -0
- package/dist/assets/characters/char_2.png +0 -0
- package/dist/assets/characters/char_3.png +0 -0
- package/dist/assets/characters/char_4.png +0 -0
- package/dist/assets/characters/char_5.png +0 -0
- package/dist/assets/characters/char_6.png +0 -0
- package/dist/assets/characters/char_7.png +0 -0
- package/dist/assets/characters/char_8.png +0 -0
- package/dist/assets/characters/char_9.png +0 -0
- package/dist/assets/index-CnTPDqpP.js +12 -0
- package/dist/assets/index-gR5q7ikB.css +1 -0
- package/dist/assets/office/furniture.png +0 -0
- package/dist/assets/office/room-builder.png +0 -0
- package/dist/index.html +16 -0
- package/dist-server/scripts/intelligence-trends.d.ts +100 -0
- package/dist-server/scripts/intelligence-trends.js +365 -0
- package/dist-server/server/actions/cleanup.d.ts +4 -0
- package/dist-server/server/actions/cleanup.js +30 -0
- package/dist-server/server/actions/send-input.d.ts +6 -0
- package/dist-server/server/actions/send-input.js +147 -0
- package/dist-server/server/actions/terminal.d.ts +4 -0
- package/dist-server/server/actions/terminal.js +427 -0
- package/dist-server/server/config.d.ts +9 -0
- package/dist-server/server/config.js +217 -0
- package/dist-server/server/db.d.ts +7 -0
- package/dist-server/server/db.js +79 -0
- package/dist-server/server/hooks/event-receiver.d.ts +11 -0
- package/dist-server/server/hooks/event-receiver.js +36 -0
- package/dist-server/server/index.d.ts +1 -0
- package/dist-server/server/index.js +552 -0
- package/dist-server/server/notifications/macos.d.ts +5 -0
- package/dist-server/server/notifications/macos.js +22 -0
- package/dist-server/server/notifications/notifier.d.ts +17 -0
- package/dist-server/server/notifications/notifier.js +110 -0
- package/dist-server/server/parsers/process-discovery.d.ts +39 -0
- package/dist-server/server/parsers/process-discovery.js +776 -0
- package/dist-server/server/parsers/session-scanner.d.ts +56 -0
- package/dist-server/server/parsers/session-scanner.js +390 -0
- package/dist-server/server/parsers/session-state.d.ts +68 -0
- package/dist-server/server/parsers/session-state.js +696 -0
- package/dist-server/server/parsers/session-state.test.d.ts +1 -0
- package/dist-server/server/parsers/session-state.test.js +950 -0
- package/dist-server/server/parsers/task-parser.d.ts +10 -0
- package/dist-server/server/parsers/task-parser.js +97 -0
- package/dist-server/server/parsers/team-parser.d.ts +3 -0
- package/dist-server/server/parsers/team-parser.js +67 -0
- package/dist-server/server/platform/__tests__/claude-code.test.d.ts +1 -0
- package/dist-server/server/platform/__tests__/claude-code.test.js +311 -0
- package/dist-server/server/platform/claude-code.d.ts +34 -0
- package/dist-server/server/platform/claude-code.js +94 -0
- package/dist-server/server/platform/index.d.ts +5 -0
- package/dist-server/server/platform/index.js +1 -0
- package/dist-server/server/platform/types.d.ts +190 -0
- package/dist-server/server/platform/types.js +9 -0
- package/dist-server/server/state/aggregator.d.ts +42 -0
- package/dist-server/server/state/aggregator.js +1080 -0
- package/dist-server/server/state/work-item-types.d.ts +555 -0
- package/dist-server/server/state/work-item-types.js +168 -0
- package/dist-server/server/types.d.ts +237 -0
- package/dist-server/server/types.js +1 -0
- package/dist-server/server/watchers/claude-watcher.d.ts +17 -0
- package/dist-server/server/watchers/claude-watcher.js +130 -0
- package/dist-server/server/watchers/context-watcher.d.ts +22 -0
- package/dist-server/server/watchers/context-watcher.js +125 -0
- package/dist-server/server/watchers/directive-watcher.d.ts +46 -0
- package/dist-server/server/watchers/directive-watcher.js +497 -0
- package/dist-server/server/watchers/session-watcher.d.ts +18 -0
- package/dist-server/server/watchers/session-watcher.js +126 -0
- package/dist-server/server/watchers/state-watcher.d.ts +36 -0
- package/dist-server/server/watchers/state-watcher.js +369 -0
- 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
|
+
}
|