chati-dev 4.1.6 → 4.2.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/README.md +6 -4
- package/framework/agents/build/dev.md +54 -1
- package/framework/agents/plan/architect.md +88 -25
- package/framework/agents/plan/detail.md +44 -0
- package/framework/agents/plan/ux.md +109 -34
- package/framework/agents/quality/qa-implementation.md +162 -2
- package/framework/config.yaml +9 -3
- package/framework/constitution.md +269 -2
- package/framework/hooks/license-guard.js +13 -6
- package/framework/hooks/prism-engine.js +31 -1
- package/framework/hooks/settings.json +8 -0
- package/framework/hooks/team-quality-gate.js +145 -0
- package/framework/orchestrator/chati.md +172 -2
- package/framework/schemas/session.schema.json +160 -0
- package/framework/templates/team-build-tasks.yaml +56 -0
- package/framework/templates/team-planning-tasks.yaml +73 -0
- package/package.json +1 -1
- package/src/autonomy/safety-net.js +32 -0
- package/src/orchestrator/cli.js +404 -7
- package/src/terminal/run-team.js +349 -0
- package/src/terminal/team-task-list.js +226 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI runner for Agent Team execution (Article XXI).
|
|
4
|
+
*
|
|
5
|
+
* Called by the orchestrator via the Bash tool to spawn a team of agents
|
|
6
|
+
* that communicate via a shared task list and mailbox.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node run-team.js --team-id TM-20260411-pln \
|
|
10
|
+
* --team-type planning \
|
|
11
|
+
* --project-dir /path/to/project \
|
|
12
|
+
* --previous-agent brief \
|
|
13
|
+
* --provider claude \
|
|
14
|
+
* --timeout 1800000
|
|
15
|
+
*
|
|
16
|
+
* Outputs consolidated JSON to stdout for the orchestrator to parse.
|
|
17
|
+
* Same output shape as run-parallel.js + team_id + echo_events.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { buildAgentPrompt } from './prompt-builder.js';
|
|
24
|
+
import { spawnParallelGroup, spawnTerminal } from './spawner.js';
|
|
25
|
+
import { TerminalMonitor } from './monitor.js';
|
|
26
|
+
import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
|
|
27
|
+
import { parseAgentOutput } from './handoff-parser.js';
|
|
28
|
+
import { estimateTokens, COST_PER_1K } from './cost-tracker.js';
|
|
29
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
30
|
+
import {
|
|
31
|
+
readTaskList, getTeamProgress, writeMailboxMessage, computeEchoSimilarity,
|
|
32
|
+
} from './team-task-list.js';
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Constants
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const TEAM_MEMBERS = {
|
|
39
|
+
planning: ['detail', 'architect', 'ux'],
|
|
40
|
+
build: ['dev', 'qa-implementation'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Task IDs match the Shared Task List template (team-planning-tasks.yaml / team-build-tasks.yaml)
|
|
44
|
+
const TEAM_TASK_IDS = {
|
|
45
|
+
planning: ['TT-PLN-001', 'TT-PLN-002', 'TT-PLN-003'],
|
|
46
|
+
build: ['TT-BLD-001-DEV', 'TT-BLD-001-QA'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Team-level echo threshold: compares task OUTPUT similarity between consecutive
|
|
50
|
+
// outputs from the same agent. Higher threshold (0.92) because task outputs are
|
|
51
|
+
// structurally similar by nature — only near-identical outputs indicate a stuck loop.
|
|
52
|
+
// This is DISTINCT from the QA-level echo threshold (0.85 per Article XXII §4)
|
|
53
|
+
// which compares DEFECT DESCRIPTIONS against the Decision Trail.
|
|
54
|
+
const ECHO_THRESHOLD = 0.92;
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// CLI argument parsing
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function parseArgs(argv) {
|
|
61
|
+
const args = {};
|
|
62
|
+
for (let i = 2; i < argv.length; i++) {
|
|
63
|
+
const arg = argv[i];
|
|
64
|
+
if (arg.startsWith('--')) {
|
|
65
|
+
const key = arg.slice(2);
|
|
66
|
+
const next = argv[i + 1];
|
|
67
|
+
if (next && !next.startsWith('--')) {
|
|
68
|
+
args[key] = next;
|
|
69
|
+
i++;
|
|
70
|
+
} else {
|
|
71
|
+
args[key] = 'true';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Main
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const args = parseArgs(process.argv);
|
|
84
|
+
|
|
85
|
+
const teamId = args['team-id'];
|
|
86
|
+
const teamType = args['team-type'];
|
|
87
|
+
const projectDir = args['project-dir'] || process.cwd();
|
|
88
|
+
const previousAgent = args['previous-agent'] || null;
|
|
89
|
+
const provider = args.provider || 'claude';
|
|
90
|
+
const timeout = parseInt(args.timeout, 10) || 1_800_000; // default 30 minutes
|
|
91
|
+
|
|
92
|
+
if (!teamId || !teamType) {
|
|
93
|
+
outputError('Missing required arguments: --team-id and --team-type');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const members = TEAM_MEMBERS[teamType];
|
|
98
|
+
const taskIds = TEAM_TASK_IDS[teamType];
|
|
99
|
+
|
|
100
|
+
if (!members) {
|
|
101
|
+
outputError(`Unknown team type: ${teamType}. Valid: planning, build`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const teamDir = join(projectDir, '.chati', 'teams', teamId);
|
|
106
|
+
const mailboxDir = join(teamDir, 'mailbox');
|
|
107
|
+
const taskListPath = join(teamDir, 'tasks.yaml');
|
|
108
|
+
|
|
109
|
+
// Load session state (minimal parse)
|
|
110
|
+
let sessionState = {};
|
|
111
|
+
try {
|
|
112
|
+
const sessionPath = join(projectDir, '.chati', 'session.yaml');
|
|
113
|
+
if (existsSync(sessionPath)) {
|
|
114
|
+
const raw = readFileSync(sessionPath, 'utf-8');
|
|
115
|
+
for (const line of raw.split('\n')) {
|
|
116
|
+
const m = line.trim().match(/^([\w_-]+):\s*(.+)/);
|
|
117
|
+
if (m) sessionState[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch { /* optional */ }
|
|
121
|
+
|
|
122
|
+
// Build prompts for all team members
|
|
123
|
+
const configs = [];
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < members.length; i++) {
|
|
127
|
+
try {
|
|
128
|
+
const promptResult = buildAgentPrompt({
|
|
129
|
+
agent: members[i],
|
|
130
|
+
taskId: taskIds[i],
|
|
131
|
+
projectDir,
|
|
132
|
+
previousAgent,
|
|
133
|
+
sessionState,
|
|
134
|
+
provider,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
configs.push({
|
|
138
|
+
agent: members[i],
|
|
139
|
+
taskId: taskIds[i],
|
|
140
|
+
model: promptResult.model,
|
|
141
|
+
provider: promptResult.provider,
|
|
142
|
+
prompt: promptResult.prompt,
|
|
143
|
+
workingDir: projectDir,
|
|
144
|
+
timeout,
|
|
145
|
+
// Team-specific env vars injected into the spawned process
|
|
146
|
+
contextPayload: {
|
|
147
|
+
CHATI_TEAM_ID: teamId,
|
|
148
|
+
CHATI_TEAM_MEMBER: members[i],
|
|
149
|
+
CHATI_TEAM_TASK_LIST: taskListPath,
|
|
150
|
+
CHATI_TEAM_MAILBOX: mailboxDir,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
outputError(`Failed to build prompt for ${members[i]}: ${err.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check rate limit
|
|
160
|
+
const limiter = getRateLimiter(provider);
|
|
161
|
+
const rateStats = limiter.getStats();
|
|
162
|
+
const availableSlots = rateStats.limit - rateStats.used;
|
|
163
|
+
if (availableSlots < configs.length) {
|
|
164
|
+
console.error(`[chati] Rate limiter: ${availableSlots} slots for ${configs.length} team members. May throttle.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Spawn team members
|
|
168
|
+
let group;
|
|
169
|
+
let fallbackUsed = false;
|
|
170
|
+
const echoEvents = [];
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
group = spawnParallelGroup(configs);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[chati] Team spawn failed: ${err.message}. Signaling fallback.`);
|
|
176
|
+
process.stdout.write(JSON.stringify({
|
|
177
|
+
status: 'spawn_failed',
|
|
178
|
+
fallback_required: true,
|
|
179
|
+
error: err.message,
|
|
180
|
+
team_id: teamId,
|
|
181
|
+
}, null, 2) + '\n');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Monitor until all members complete
|
|
186
|
+
const monitor = new TerminalMonitor({ pollInterval: 2000, timeout });
|
|
187
|
+
|
|
188
|
+
for (const terminal of group.terminals) {
|
|
189
|
+
monitor.addTerminal(terminal);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await new Promise((resolve) => {
|
|
193
|
+
const safetyTimer = setTimeout(() => {
|
|
194
|
+
monitor.stopMonitoring();
|
|
195
|
+
resolve();
|
|
196
|
+
}, timeout + 10_000);
|
|
197
|
+
|
|
198
|
+
monitor.onComplete(() => {
|
|
199
|
+
clearTimeout(safetyTimer);
|
|
200
|
+
monitor.stopMonitoring();
|
|
201
|
+
resolve();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
monitor.startMonitoring();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const elapsed = Date.now() - startTime;
|
|
208
|
+
|
|
209
|
+
// Echo detection: compare task outputs for cyclical patterns
|
|
210
|
+
try {
|
|
211
|
+
const taskList = readTaskList(taskListPath);
|
|
212
|
+
if (taskList && taskList.tasks) {
|
|
213
|
+
const completedTasks = taskList.tasks.filter(t => t.status === 'done');
|
|
214
|
+
for (let i = 1; i < completedTasks.length; i++) {
|
|
215
|
+
for (let j = 0; j < i; j++) {
|
|
216
|
+
const t1 = completedTasks[i];
|
|
217
|
+
const t2 = completedTasks[j];
|
|
218
|
+
if (t1.assigned_to === t2.assigned_to) {
|
|
219
|
+
const desc1 = JSON.stringify(t1);
|
|
220
|
+
const desc2 = JSON.stringify(t2);
|
|
221
|
+
const similarity = computeEchoSimilarity(desc1, desc2);
|
|
222
|
+
if (similarity >= ECHO_THRESHOLD) {
|
|
223
|
+
echoEvents.push({
|
|
224
|
+
detected_at: new Date().toISOString(),
|
|
225
|
+
member: t1.assigned_to,
|
|
226
|
+
similarity: Math.round(similarity * 100) / 100,
|
|
227
|
+
action_taken: 'echo_logged',
|
|
228
|
+
});
|
|
229
|
+
// Notify member via mailbox
|
|
230
|
+
writeMailboxMessage(mailboxDir, 'orchestrator', t1.assigned_to, 'echo_detected', {
|
|
231
|
+
similarity,
|
|
232
|
+
message: `You have submitted nearly identical outputs. Apply Article XX: diagnose root cause, change approach, do not repeat previous attempt.`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch { /* echo detection is non-critical */ }
|
|
240
|
+
|
|
241
|
+
// Cost estimation
|
|
242
|
+
const costEstimates = configs.map((cfg, i) => {
|
|
243
|
+
const terminal = group.terminals[i];
|
|
244
|
+
const inputTokens = estimateTokens(cfg.prompt || '');
|
|
245
|
+
const outputTokens = estimateTokens((terminal?.stdout || []).join(''));
|
|
246
|
+
const modelKey = cfg.model || 'sonnet';
|
|
247
|
+
const rate = COST_PER_1K[modelKey] || COST_PER_1K.sonnet || 0.015;
|
|
248
|
+
return {
|
|
249
|
+
agent: cfg.agent,
|
|
250
|
+
model: modelKey,
|
|
251
|
+
provider: cfg.provider || 'claude',
|
|
252
|
+
inputTokens,
|
|
253
|
+
outputTokens,
|
|
254
|
+
estimatedCost: ((inputTokens + outputTokens) / 1000) * rate,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Collect and merge results
|
|
259
|
+
const rawResults = collectResults(group.groupId, group.terminals);
|
|
260
|
+
|
|
261
|
+
const agentResults = rawResults.results.map(r => {
|
|
262
|
+
const parsed = parseAgentOutput(r.stdout);
|
|
263
|
+
return {
|
|
264
|
+
...r,
|
|
265
|
+
handoff: parsed.found ? parsed.handoff : null,
|
|
266
|
+
handoffFound: parsed.found,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const mergeInput = agentResults.map(r => ({
|
|
271
|
+
agent: r.agent,
|
|
272
|
+
status: r.handoff?.status || (r.exitCode === 0 ? 'complete' : 'failed'),
|
|
273
|
+
outputs: r.handoff?.outputs || [],
|
|
274
|
+
decisions: r.handoff?.decisions || {},
|
|
275
|
+
blockers: r.handoff?.blockers || [],
|
|
276
|
+
summary: r.handoff?.summary || '',
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const merged = mergeHandoffs(mergeInput);
|
|
280
|
+
const nextAgent = determineNextAgent(teamType);
|
|
281
|
+
const consolidated = buildConsolidatedHandoff(merged, nextAgent);
|
|
282
|
+
|
|
283
|
+
// Team progress
|
|
284
|
+
const progress = getTeamProgress(taskListPath);
|
|
285
|
+
|
|
286
|
+
// Output consolidated result
|
|
287
|
+
const output = {
|
|
288
|
+
status: rawResults.summary.failed === 0 ? 'complete' : 'partial',
|
|
289
|
+
team_id: teamId,
|
|
290
|
+
team_type: teamType,
|
|
291
|
+
groupId: group.groupId,
|
|
292
|
+
agents: members.map((a, i) => ({
|
|
293
|
+
agent: a,
|
|
294
|
+
model: configs[i].model,
|
|
295
|
+
status: agentResults[i]?.handoff?.status || (agentResults[i]?.exitCode === 0 ? 'complete' : 'failed'),
|
|
296
|
+
score: agentResults[i]?.handoff?.score || null,
|
|
297
|
+
exitCode: agentResults[i]?.exitCode,
|
|
298
|
+
handoffFound: agentResults[i]?.handoffFound,
|
|
299
|
+
})),
|
|
300
|
+
mergedHandoff: consolidated,
|
|
301
|
+
team_progress: progress,
|
|
302
|
+
echo_events: echoEvents,
|
|
303
|
+
summary: rawResults.summary,
|
|
304
|
+
elapsed,
|
|
305
|
+
performance: {
|
|
306
|
+
sequentialEstimate: elapsed * members.length,
|
|
307
|
+
parallelActual: elapsed,
|
|
308
|
+
timeSaved: elapsed * (members.length - 1),
|
|
309
|
+
},
|
|
310
|
+
costEstimate: costEstimates,
|
|
311
|
+
fallbackUsed,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
315
|
+
process.exit(rawResults.summary.failed > 0 ? 1 : 0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Helpers
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Determine the next sequential agent after a team completes.
|
|
324
|
+
* Planning Team → phases
|
|
325
|
+
* Build Team → devops
|
|
326
|
+
*/
|
|
327
|
+
function determineNextAgent(teamType) {
|
|
328
|
+
if (teamType === 'planning') return 'phases';
|
|
329
|
+
if (teamType === 'build') return 'devops';
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function outputError(message) {
|
|
334
|
+
process.stdout.write(JSON.stringify({
|
|
335
|
+
status: 'error',
|
|
336
|
+
error: message,
|
|
337
|
+
fallback_required: true,
|
|
338
|
+
}) + '\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Guard pattern
|
|
342
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
343
|
+
main().catch(err => {
|
|
344
|
+
outputError(err.message);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { parseArgs, determineNextAgent };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Team Task List and Mailbox utilities.
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for managing shared task lists and
|
|
5
|
+
* inter-agent mailbox communication within Agent Teams (Article XXI).
|
|
6
|
+
*
|
|
7
|
+
* Uses file-lock.js for atomic writes to prevent race conditions
|
|
8
|
+
* when multiple team members update concurrently.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
import { withLock } from '../utils/file-lock.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Task List Management
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize a team task list from a template.
|
|
22
|
+
* @param {string} destPath - Destination path for the task list YAML
|
|
23
|
+
* @param {string} templatePath - Source template path
|
|
24
|
+
* @param {object} overrides - Optional field overrides (team_id, etc.)
|
|
25
|
+
* @returns {{ created: boolean, path: string }}
|
|
26
|
+
*/
|
|
27
|
+
export function initTaskList(destPath, templatePath, overrides = {}) {
|
|
28
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
29
|
+
|
|
30
|
+
if (!existsSync(templatePath)) {
|
|
31
|
+
throw new Error(`Team task template not found: ${templatePath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const template = yaml.load(readFileSync(templatePath, 'utf-8'));
|
|
35
|
+
const taskList = {
|
|
36
|
+
...template,
|
|
37
|
+
...overrides,
|
|
38
|
+
created_at: new Date().toISOString(),
|
|
39
|
+
last_updated: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
writeFileSync(destPath, yaml.dump(taskList, { lineWidth: -1, noRefs: true }), 'utf-8');
|
|
43
|
+
return { created: true, path: destPath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read and parse a team task list.
|
|
48
|
+
* @param {string} path - Path to task list YAML
|
|
49
|
+
* @returns {object|null} Parsed task list or null if not found
|
|
50
|
+
*/
|
|
51
|
+
export function readTaskList(path) {
|
|
52
|
+
if (!existsSync(path)) return null;
|
|
53
|
+
return yaml.load(readFileSync(path, 'utf-8'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Update a task's status atomically using file locking.
|
|
58
|
+
* @param {string} path - Path to task list YAML
|
|
59
|
+
* @param {string} taskId - Task ID to update
|
|
60
|
+
* @param {object} updates - Fields to merge (status, score, etc.)
|
|
61
|
+
* @returns {Promise<{ updated: boolean, task: object|null }>}
|
|
62
|
+
*/
|
|
63
|
+
export async function updateTaskStatus(path, taskId, updates) {
|
|
64
|
+
return withLock(path, () => {
|
|
65
|
+
const taskList = readTaskList(path);
|
|
66
|
+
if (!taskList || !taskList.tasks) return { updated: false, task: null };
|
|
67
|
+
|
|
68
|
+
const task = taskList.tasks.find(t => t.id === taskId);
|
|
69
|
+
if (!task) return { updated: false, task: null };
|
|
70
|
+
|
|
71
|
+
Object.assign(task, updates, { last_updated: new Date().toISOString() });
|
|
72
|
+
if (updates.status === 'done' && !task.completed_at) {
|
|
73
|
+
task.completed_at = new Date().toISOString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
taskList.last_updated = new Date().toISOString();
|
|
77
|
+
writeFileSync(path, yaml.dump(taskList, { lineWidth: -1, noRefs: true }), 'utf-8');
|
|
78
|
+
return { updated: true, task };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get team progress summary from task list.
|
|
84
|
+
* @param {string} path - Path to task list YAML
|
|
85
|
+
* @returns {{ total: number, done: number, pending: number, blocked: number, inProgress: number, memberProgress: object }}
|
|
86
|
+
*/
|
|
87
|
+
export function getTeamProgress(path) {
|
|
88
|
+
const taskList = readTaskList(path);
|
|
89
|
+
if (!taskList || !taskList.tasks) {
|
|
90
|
+
return { total: 0, done: 0, pending: 0, blocked: 0, inProgress: 0, memberProgress: {} };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tasks = taskList.tasks;
|
|
94
|
+
const memberProgress = {};
|
|
95
|
+
|
|
96
|
+
for (const task of tasks) {
|
|
97
|
+
const assignee = task.assigned_to;
|
|
98
|
+
if (!assignee) continue;
|
|
99
|
+
if (!memberProgress[assignee]) {
|
|
100
|
+
memberProgress[assignee] = { status: 'pending', score: null, tasks: 0, completed: 0 };
|
|
101
|
+
}
|
|
102
|
+
memberProgress[assignee].tasks++;
|
|
103
|
+
if (task.status === 'done') {
|
|
104
|
+
memberProgress[assignee].completed++;
|
|
105
|
+
memberProgress[assignee].score = task.score;
|
|
106
|
+
} else if (task.status === 'in_progress') {
|
|
107
|
+
memberProgress[assignee].status = 'in_progress';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Post-loop: derive correct status for each member
|
|
112
|
+
for (const member of Object.values(memberProgress)) {
|
|
113
|
+
if (member.completed === member.tasks && member.tasks > 0) {
|
|
114
|
+
member.status = 'done';
|
|
115
|
+
} else if (member.completed > 0 && member.completed < member.tasks) {
|
|
116
|
+
member.status = 'in_progress';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
total: tasks.length,
|
|
122
|
+
done: tasks.filter(t => t.status === 'done').length,
|
|
123
|
+
pending: tasks.filter(t => t.status === 'pending').length,
|
|
124
|
+
blocked: tasks.filter(t => t.status === 'blocked').length,
|
|
125
|
+
inProgress: tasks.filter(t => t.status === 'in_progress').length,
|
|
126
|
+
memberProgress,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Mailbox Management
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write a message to the team mailbox.
|
|
136
|
+
* Messages are JSON files named {timestamp}-{from}-{type}.json
|
|
137
|
+
* @param {string} mailboxDir - Path to team mailbox directory
|
|
138
|
+
* @param {string} from - Sender agent name
|
|
139
|
+
* @param {string} to - Recipient agent name (or 'all' for broadcast)
|
|
140
|
+
* @param {string} type - Message type (cross_review_request, task_ready_for_review, etc.)
|
|
141
|
+
* @param {object} payload - Message content
|
|
142
|
+
* @returns {{ written: boolean, path: string }}
|
|
143
|
+
*/
|
|
144
|
+
export function writeMailboxMessage(mailboxDir, from, to, type, payload) {
|
|
145
|
+
mkdirSync(mailboxDir, { recursive: true });
|
|
146
|
+
|
|
147
|
+
const timestamp = Date.now();
|
|
148
|
+
const filename = `${timestamp}-${from}-${type}.json`;
|
|
149
|
+
const messagePath = join(mailboxDir, filename);
|
|
150
|
+
|
|
151
|
+
const message = {
|
|
152
|
+
id: `MSG-${timestamp}-${from.slice(0, 3)}`,
|
|
153
|
+
from,
|
|
154
|
+
to,
|
|
155
|
+
type,
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
payload,
|
|
158
|
+
read: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
writeFileSync(messagePath, JSON.stringify(message, null, 2), 'utf-8');
|
|
162
|
+
return { written: true, path: messagePath };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Read all inbox messages for a specific agent.
|
|
167
|
+
* @param {string} mailboxDir - Path to team mailbox directory
|
|
168
|
+
* @param {string} member - Agent name to read messages for
|
|
169
|
+
* @returns {object[]} Array of message objects addressed to this member (or 'all')
|
|
170
|
+
*/
|
|
171
|
+
export function readInbox(mailboxDir, member) {
|
|
172
|
+
if (!existsSync(mailboxDir)) return [];
|
|
173
|
+
|
|
174
|
+
const files = readdirSync(mailboxDir).filter(f => f.endsWith('.json')).sort();
|
|
175
|
+
const messages = [];
|
|
176
|
+
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
try {
|
|
179
|
+
const msg = JSON.parse(readFileSync(join(mailboxDir, file), 'utf-8'));
|
|
180
|
+
if (msg.to === member || msg.to === 'all') {
|
|
181
|
+
messages.push(msg);
|
|
182
|
+
}
|
|
183
|
+
} catch { /* skip malformed messages */ }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return messages;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Echo Detection
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Compute trigram-based Jaccard similarity between two texts.
|
|
195
|
+
* Used for Echo Detection (Article XXII) to detect cyclical defects.
|
|
196
|
+
* @param {string} text1 - First text (normalized)
|
|
197
|
+
* @param {string} text2 - Second text (normalized)
|
|
198
|
+
* @returns {number} Similarity ratio 0.0 - 1.0
|
|
199
|
+
*/
|
|
200
|
+
export function computeEchoSimilarity(text1, text2) {
|
|
201
|
+
if (!text1 || !text2) return 0;
|
|
202
|
+
|
|
203
|
+
const normalize = (t) => t.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
204
|
+
const trigrams = (t) => {
|
|
205
|
+
const n = normalize(t);
|
|
206
|
+
const set = new Set();
|
|
207
|
+
for (let i = 0; i <= n.length - 3; i++) {
|
|
208
|
+
set.add(n.slice(i, i + 3));
|
|
209
|
+
}
|
|
210
|
+
return set;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const a = trigrams(text1);
|
|
214
|
+
const b = trigrams(text2);
|
|
215
|
+
|
|
216
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
217
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
218
|
+
|
|
219
|
+
let intersection = 0;
|
|
220
|
+
for (const gram of a) {
|
|
221
|
+
if (b.has(gram)) intersection++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const union = a.size + b.size - intersection;
|
|
225
|
+
return union === 0 ? 0 : intersection / union;
|
|
226
|
+
}
|