codehere 0.5.0-alpha.2 → 0.5.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/use-cases/generate-final-report.js +144 -0
- package/dist/index.js +60 -5
- package/dist/infrastructure/orchestration/claude-code-adapter.js +84 -0
- package/dist/infrastructure/orchestration/session-manager.js +65 -2
- package/dist/infrastructure/protocols/mcp-server-impl.js +61 -11
- package/dist/infrastructure/protocols/ui-page.js +191 -14
- package/dist/infrastructure/storage/project-thread-migration.js +232 -0
- package/dist/presentation/cli/commands/init-command.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final Report — PM-UX Layer 3 · MVP (v0.5.0-alpha.3+).
|
|
3
|
+
*
|
|
4
|
+
* At task exit, generates a 3-paragraph plain-English summary of what the
|
|
5
|
+
* agent did, what was touched, and what the user should look at. Prints to
|
|
6
|
+
* stdout so the dogfood-while-building loop sees the artifact immediately.
|
|
7
|
+
*
|
|
8
|
+
* Provider strategy (MVP — try free first, fall back to paid):
|
|
9
|
+
* - Tries `claude-free --bare -p "<prompt>"` first. The `claude-free`
|
|
10
|
+
* wrapper is what actually routes through the FCC proxy at :8082
|
|
11
|
+
* (verified 2026-05-02 dogfood: plain `claude --bare` does NOT route
|
|
12
|
+
* through FCC — it hits paid Claude. The May 1 memory rule was wrong.)
|
|
13
|
+
* - Falls back to `claude --bare -p "<prompt>"` if `claude-free` is not
|
|
14
|
+
* installed (paid path; will fail gracefully if no credit).
|
|
15
|
+
* - If neither binary is installed, returns null silently (no report).
|
|
16
|
+
*
|
|
17
|
+
* Env override: set CODEHERE_REPORT_BINARY=<binary-name> to force a
|
|
18
|
+
* specific provider (e.g. for testing or non-default setups).
|
|
19
|
+
*
|
|
20
|
+
* Iterations 5-9 (2026-05-02) refuted the "we own the supervisory layer"
|
|
21
|
+
* thesis. Final Report is the keystone delivery moment per the v3 chain
|
|
22
|
+
* (claim_market_position) — it's the artifact the dogfood loop produces
|
|
23
|
+
* that none of the surveyed competitors (ComposioHQ, Overstory, Praktor,
|
|
24
|
+
* Claude Squad, AI-Agents-Orchestrator) advertise.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
const PROMPT_TEMPLATE = (i) => `You are a PM reporting back to a developer about what their AI coding agent
|
|
28
|
+
just did. Write 3 short paragraphs in plain English. No headers, no bullet
|
|
29
|
+
lists, no markdown — just three paragraphs separated by blank lines.
|
|
30
|
+
|
|
31
|
+
Paragraph 1: What the agent DID. Verb-led. Name files specifically. 1-3 sentences.
|
|
32
|
+
Paragraph 2: What was TOUCHED. Files modified, line counts, anything that looks risky. 1-3 sentences.
|
|
33
|
+
Paragraph 3: What the developer should LOOK AT. Open concerns, things to verify.
|
|
34
|
+
If everything looks clean, say so explicitly. 1-2 sentences.
|
|
35
|
+
|
|
36
|
+
Be specific. Don't pad. Don't editorialize. Don't praise the agent.
|
|
37
|
+
|
|
38
|
+
The task was: ${i.prompt}
|
|
39
|
+
The agent used: ${i.agent}
|
|
40
|
+
Duration: ${i.durationMs}ms
|
|
41
|
+
Status: ${i.status}
|
|
42
|
+
|
|
43
|
+
Steps the agent took:
|
|
44
|
+
${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') : '(no structured steps captured)'}
|
|
45
|
+
|
|
46
|
+
Files changed:
|
|
47
|
+
${i.artifacts?.files?.length
|
|
48
|
+
? i.artifacts.files.map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`).join('\n')
|
|
49
|
+
: '(no file artifacts captured)'}
|
|
50
|
+
|
|
51
|
+
Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
|
|
52
|
+
|
|
53
|
+
Now write the 3 paragraphs.`;
|
|
54
|
+
const REPORT_TIMEOUT_MS = 30_000;
|
|
55
|
+
/** Provider chain: try free first, then paid. Override with CODEHERE_REPORT_BINARY. */
|
|
56
|
+
function selectProviderBinary() {
|
|
57
|
+
const override = process.env.CODEHERE_REPORT_BINARY;
|
|
58
|
+
if (override)
|
|
59
|
+
return [override];
|
|
60
|
+
return ['claude-free', 'claude'];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate a Final Report. Returns null if no provider is reachable.
|
|
64
|
+
* Never throws — failure modes return null + log a debug line.
|
|
65
|
+
*/
|
|
66
|
+
export async function generateFinalReport(input) {
|
|
67
|
+
const prompt = PROMPT_TEMPLATE(input);
|
|
68
|
+
for (const binary of selectProviderBinary()) {
|
|
69
|
+
const result = await tryProvider(binary, prompt, input.sessionId);
|
|
70
|
+
if (result)
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function tryProvider(binary, prompt, sessionId) {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let stdout = '';
|
|
79
|
+
let stderr = '';
|
|
80
|
+
let settled = false;
|
|
81
|
+
const finish = (result) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
resolve(result);
|
|
86
|
+
};
|
|
87
|
+
const child = spawn(binary, ['--bare', '-p', prompt], {
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
});
|
|
90
|
+
const timeout = setTimeout(() => {
|
|
91
|
+
child.kill('SIGTERM');
|
|
92
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
93
|
+
console.error(`[final-report] ${binary} timeout after ${REPORT_TIMEOUT_MS}ms for ${sessionId}`);
|
|
94
|
+
}
|
|
95
|
+
finish(null);
|
|
96
|
+
}, REPORT_TIMEOUT_MS);
|
|
97
|
+
child.on('error', (err) => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
// ENOENT = binary not installed. Silent fallback to next provider in chain.
|
|
100
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
101
|
+
console.error(`[final-report] ${binary} unavailable: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
finish(null);
|
|
104
|
+
});
|
|
105
|
+
child.stdout?.on('data', (chunk) => {
|
|
106
|
+
stdout += chunk.toString('utf8');
|
|
107
|
+
});
|
|
108
|
+
child.stderr?.on('data', (chunk) => {
|
|
109
|
+
stderr += chunk.toString('utf8');
|
|
110
|
+
});
|
|
111
|
+
child.on('exit', (code) => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
if (code !== 0) {
|
|
114
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
115
|
+
console.error(`[final-report] ${binary} exited ${code}: ${stderr.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
finish(null);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const summary = stdout.trim();
|
|
121
|
+
if (!summary) {
|
|
122
|
+
finish(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
finish({ summary, generatedAt: Date.now(), durationMs: Date.now() - startedAt });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pretty-print to terminal. Used by session-manager when a spawned task exits.
|
|
131
|
+
* Wrapped in try/catch upstream — never let a print failure surface.
|
|
132
|
+
*/
|
|
133
|
+
export function printFinalReportToTerminal(input, output) {
|
|
134
|
+
const banner = '─'.repeat(60);
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
process.stdout.write(`${banner}\n`);
|
|
137
|
+
process.stdout.write(`Final Report · ${input.agent} · ${input.sessionId.slice(0, 8)}\n`);
|
|
138
|
+
process.stdout.write(`${banner}\n`);
|
|
139
|
+
process.stdout.write(`${output.summary}\n`);
|
|
140
|
+
process.stdout.write(`${banner}\n`);
|
|
141
|
+
process.stdout.write(`(generated in ${output.durationMs}ms)\n`);
|
|
142
|
+
process.stdout.write('\n');
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=generate-final-report.js.map
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Command } from 'commander';
|
|
|
14
14
|
import { config } from 'dotenv';
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
+
import { homedir } from 'os';
|
|
17
18
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
18
19
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
19
20
|
import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
|
|
@@ -139,7 +140,7 @@ program
|
|
|
139
140
|
// trust-scorer pattern bank to .codehere/protocols/hook_patterns.json so
|
|
140
141
|
// auditors / cross-tool consumers can read codehere's safety surface.
|
|
141
142
|
program
|
|
142
|
-
.command('protocols')
|
|
143
|
+
.command('protocols', { hidden: true }) // hidden from --help (May 2 cut: power-user F3 export, see docs/MEMORY.md)
|
|
143
144
|
.description('F3 protocols layer · export (write hook_patterns.json from trust-scorer bank)')
|
|
144
145
|
.argument('[action]', 'Action: export, help')
|
|
145
146
|
.option('--rebuild', 'Refresh built-in patterns (project patterns are preserved)')
|
|
@@ -345,7 +346,7 @@ program
|
|
|
345
346
|
// ── serve (MCP server) ───────────────────────────────────────────────────────
|
|
346
347
|
program
|
|
347
348
|
.command('serve')
|
|
348
|
-
.description('
|
|
349
|
+
.description('Run codehere as a background MCP server (no web UI) — for Claude Desktop / Cursor integration')
|
|
349
350
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
350
351
|
.option('--host <host>', 'Host to bind to (default: 127.0.0.1)', '127.0.0.1')
|
|
351
352
|
.option('--token <token>', 'Require Bearer token for authentication')
|
|
@@ -406,7 +407,7 @@ program
|
|
|
406
407
|
// ── start (Mission Control: daemon + web UI) ─────────────────────────────────
|
|
407
408
|
program
|
|
408
409
|
.command('start')
|
|
409
|
-
.description('Start
|
|
410
|
+
.description('Start codehere — interactive PM dashboard at localhost:3773 (delegate tasks, watch agents work)')
|
|
410
411
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
411
412
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
412
413
|
.option('--no-open', "Don't auto-open the browser")
|
|
@@ -428,6 +429,25 @@ program
|
|
|
428
429
|
}
|
|
429
430
|
};
|
|
430
431
|
try {
|
|
432
|
+
// v0.6.0 M1+M7 — migrate sessions.jsonl → ~/.codehere/projects/<slug>/thread.jsonl
|
|
433
|
+
// on daemon start. Idempotent: skips projects whose thread.jsonl already exists,
|
|
434
|
+
// so this is safe to run on every boot. Opt-out via CODEHERE_DISABLE_THREAD_MIGRATION=1.
|
|
435
|
+
// Failures are non-blocking — back-compat to v0.5 sessions.jsonl is preserved.
|
|
436
|
+
if (process.env.CODEHERE_DISABLE_THREAD_MIGRATION !== '1') {
|
|
437
|
+
try {
|
|
438
|
+
const { migrate } = await import('./infrastructure/storage/project-thread-migration.js');
|
|
439
|
+
const result = migrate({
|
|
440
|
+
sourceJsonl: join(homedir(), '.codehere/sessions.jsonl'),
|
|
441
|
+
targetDir: join(homedir(), '.codehere/projects'),
|
|
442
|
+
});
|
|
443
|
+
if (result.threadsWritten > 0) {
|
|
444
|
+
console.log(colors.dim(` v0.6 migration: created ${result.threadsWritten} project thread(s) from ${result.sessionsProcessed} sessions (${result.durationMs}ms)`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
console.warn(colors.dim(` v0.6 migration skipped: ${err instanceof Error ? err.message : String(err)}`));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
431
451
|
const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
|
|
432
452
|
const server = startMCPServer({ port, host });
|
|
433
453
|
// Wait for the server to either start listening or fail. The default
|
|
@@ -597,9 +617,44 @@ program
|
|
|
597
617
|
r.end();
|
|
598
618
|
});
|
|
599
619
|
});
|
|
620
|
+
// ── migrate-threads — v0.6.0 schema migration sessions.jsonl → projects/<slug>/thread.jsonl
|
|
621
|
+
program
|
|
622
|
+
.command('migrate-threads', { hidden: true }) // hidden from --help (v0.6.0 internal — auto-runs on daemon start once wired)
|
|
623
|
+
.description('Migrate sessions.jsonl into per-project thread.jsonl files (v0.6.0 schema)')
|
|
624
|
+
.option('--source <path>', 'Source sessions.jsonl', `${homedir()}/.codehere/sessions.jsonl`)
|
|
625
|
+
.option('--target <dir>', 'Target projects dir', `${homedir()}/.codehere/projects`)
|
|
626
|
+
.option('--dry-run', 'Report what migrate WOULD do, write nothing', false)
|
|
627
|
+
.option('--rollback', 'Remove the projects dir; sessions.jsonl untouched', false)
|
|
628
|
+
.option('--keep-temp-dirs', 'Include /tmp + /var/folders cwds (default skips them)', false)
|
|
629
|
+
.action(async (options) => {
|
|
630
|
+
const { migrate, dryRun, rollback } = await import('./infrastructure/storage/project-thread-migration.js');
|
|
631
|
+
if (options.rollback) {
|
|
632
|
+
const r = rollback(options.target);
|
|
633
|
+
console.log(`Rollback: removed ${r.removedDirs} project dir(s) from ${options.target}`);
|
|
634
|
+
console.log(`Source ${options.source} untouched.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const fn = options.dryRun ? dryRun : migrate;
|
|
638
|
+
const label = options.dryRun ? 'DRY RUN' : 'MIGRATION';
|
|
639
|
+
const result = fn({
|
|
640
|
+
sourceJsonl: options.source,
|
|
641
|
+
targetDir: options.target,
|
|
642
|
+
skipTempDirs: !options.keepTempDirs,
|
|
643
|
+
});
|
|
644
|
+
console.log(`=== ${label} ===`);
|
|
645
|
+
console.log(` source: ${options.source}`);
|
|
646
|
+
console.log(` target: ${options.target}`);
|
|
647
|
+
console.log(` sessions processed: ${result.sessionsProcessed}`);
|
|
648
|
+
console.log(` threads ${options.dryRun ? 'would write' : 'written'}: ${result.threadsWritten}`);
|
|
649
|
+
console.log(` threads skipped (exist): ${result.threadsSkippedExisting}`);
|
|
650
|
+
console.log(` unknown-cwd sessions: ${result.unknownCwdSessions}`);
|
|
651
|
+
console.log(` abandoned sessions: ${result.abandonedSessions}`);
|
|
652
|
+
console.log(` temp-dir sessions skipped: ${result.tempDirSessionsSkipped}`);
|
|
653
|
+
console.log(` duration: ${result.durationMs}ms`);
|
|
654
|
+
});
|
|
600
655
|
// ── scan-tool (called by Claude Code hooks system, not users directly) ───────
|
|
601
656
|
program
|
|
602
|
-
.command('scan-tool')
|
|
657
|
+
.command('scan-tool', { hidden: true }) // hidden from --help (May 2 cut: internal hook handler, called by Claude Code via stdin)
|
|
603
658
|
.description('Hook handler: reads Claude Code tool event from stdin, outputs block/allow (used by hook system)')
|
|
604
659
|
.option('--log-only', 'Never block, log only')
|
|
605
660
|
.option('--block-all', 'Block on any finding, not just critical')
|
|
@@ -613,7 +668,7 @@ program
|
|
|
613
668
|
});
|
|
614
669
|
// ── completion (shell completion script generator) ──────────────────────────
|
|
615
670
|
program
|
|
616
|
-
.command('completion')
|
|
671
|
+
.command('completion', { hidden: true }) // hidden from --help (May 2 cut: shell-completion is npm convention, not product surface)
|
|
617
672
|
.description('Generate shell completion script')
|
|
618
673
|
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
619
674
|
.action(async (shell) => {
|
|
@@ -221,6 +221,12 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
221
221
|
}
|
|
222
222
|
// ── stdout: newline-delimited JSON from `--output-format stream-json` ────────
|
|
223
223
|
let stdoutBuf = '';
|
|
224
|
+
// Iter 13 — capture the claude-internal session_id from the init event so
|
|
225
|
+
// appendMessage can use `--resume <session-id>` (reliable) instead of `-c`
|
|
226
|
+
// (which picks "most recent in cwd" and can collide with other concurrent
|
|
227
|
+
// claude sessions OR with conversations containing thinking blocks the
|
|
228
|
+
// headless API can't replay).
|
|
229
|
+
let claudeSessionId;
|
|
224
230
|
child.stdout?.on('data', (chunk) => {
|
|
225
231
|
stdoutBuf += chunk.toString('utf-8');
|
|
226
232
|
let idx;
|
|
@@ -231,6 +237,12 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
231
237
|
continue;
|
|
232
238
|
try {
|
|
233
239
|
const data = JSON.parse(line);
|
|
240
|
+
if (claudeSessionId === undefined &&
|
|
241
|
+
data.type === 'system' &&
|
|
242
|
+
data.subtype === 'init' &&
|
|
243
|
+
typeof data.session_id === 'string') {
|
|
244
|
+
claudeSessionId = data.session_id;
|
|
245
|
+
}
|
|
234
246
|
emit({ type: data.type ?? 'message', timestamp: Date.now(), data });
|
|
235
247
|
}
|
|
236
248
|
catch {
|
|
@@ -284,6 +296,7 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
284
296
|
startedAt,
|
|
285
297
|
prompt,
|
|
286
298
|
agent: 'claude-code',
|
|
299
|
+
canAppendMessage: true,
|
|
287
300
|
memoryInjection: injected.record,
|
|
288
301
|
get status() {
|
|
289
302
|
return statusValue;
|
|
@@ -317,6 +330,77 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
317
330
|
}
|
|
318
331
|
}, 3000);
|
|
319
332
|
},
|
|
333
|
+
/**
|
|
334
|
+
* Iter 12 (with iter-13 fix) — follow-up message to a completed session.
|
|
335
|
+
*
|
|
336
|
+
* Iter 12 used `claude -p "<text>" -c` which picks "most recent
|
|
337
|
+
* conversation in current directory". Iter 13 dogfood proved this
|
|
338
|
+
* fragile: in a cwd with multiple concurrent claude sessions OR with
|
|
339
|
+
* conversations containing thinking blocks, -c either picks the wrong
|
|
340
|
+
* session or hits an API-replay error ("Invalid signature in thinking
|
|
341
|
+
* block"). Fix: capture the claude session_id from this session's init
|
|
342
|
+
* event (above) and use `--resume <session-id>` for follow-ups —
|
|
343
|
+
* unambiguous and replay-safe.
|
|
344
|
+
*
|
|
345
|
+
* Scope: still one-shot Q&A. No streaming. Returns assistant's response
|
|
346
|
+
* text. If we never captured a session_id (init event never fired —
|
|
347
|
+
* shouldn't happen in healthy claude runs), reject explicitly.
|
|
348
|
+
*/
|
|
349
|
+
appendMessage(text) {
|
|
350
|
+
const startedAt = Date.now();
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
if (!claudeSessionId) {
|
|
353
|
+
reject(new Error('Cannot reply: original claude session_id was never captured (init event missed)'));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
let stdout = '';
|
|
357
|
+
let stderr = '';
|
|
358
|
+
const followup = spawn('claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], {
|
|
359
|
+
cwd,
|
|
360
|
+
env: spawnEnv,
|
|
361
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
362
|
+
});
|
|
363
|
+
followup.on('error', (err) => reject(err));
|
|
364
|
+
followup.stdout?.on('data', (chunk) => {
|
|
365
|
+
stdout += chunk.toString('utf-8');
|
|
366
|
+
});
|
|
367
|
+
followup.stderr?.on('data', (chunk) => {
|
|
368
|
+
stderr += chunk.toString('utf-8');
|
|
369
|
+
});
|
|
370
|
+
followup.on('exit', (code) => {
|
|
371
|
+
if (code !== 0) {
|
|
372
|
+
reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// Parse stream-json — extract assistant message text from the
|
|
376
|
+
// 'assistant' typed lines and the final 'result' line.
|
|
377
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
378
|
+
const fragments = [];
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
try {
|
|
381
|
+
const obj = JSON.parse(line);
|
|
382
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
383
|
+
for (const block of obj.message.content) {
|
|
384
|
+
if (block.type === 'text' && block.text)
|
|
385
|
+
fragments.push(block.text);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
389
|
+
fragments.push(obj.result);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Non-JSON line — skip
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Dedupe trivially: if 'result' contained the same text as the
|
|
397
|
+
// last assistant block, take just one. Conservative: prefer 'result'
|
|
398
|
+
// if present (it's the final summary), else join assistants.
|
|
399
|
+
const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
|
|
400
|
+
resolve({ result, durationMs: Date.now() - startedAt });
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
},
|
|
320
404
|
};
|
|
321
405
|
}
|
|
322
406
|
// ── AgentAdapter implementation ─────────────────────────────────────────────
|
|
@@ -23,6 +23,7 @@ import { buildConductorAdapter } from './conductor-adapter.js';
|
|
|
23
23
|
import { seedAgentsConfig } from './agents-config-seed.js';
|
|
24
24
|
import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
|
|
25
25
|
import { appendSpawned, appendCompleted, loadRecent } from './session-persistence.js';
|
|
26
|
+
import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
|
|
26
27
|
// ── Agent registry ─────────────────────────────────────────────────────────────
|
|
27
28
|
// All backends codehere can spawn. Order here drives the default UI dropdown
|
|
28
29
|
// order. Built-in adapters first; user-config-driven ones appended from
|
|
@@ -96,6 +97,18 @@ export class SessionManager {
|
|
|
96
97
|
* These subprocesses are long-dead; we surface them so the user doesn't lose
|
|
97
98
|
* their Final reports across restarts. */
|
|
98
99
|
persistedCache = loadRecent(20);
|
|
100
|
+
/** When a spawned session exits, we capture the completion timestamp here
|
|
101
|
+
* so the API layer can show duration on still-in-memory sessions (the
|
|
102
|
+
* TaskSession interface intentionally has no completedAt; only PersistedSession
|
|
103
|
+
* does, so anything reading session.completedAt got undefined pre-fix).
|
|
104
|
+
* Surfaced May 2 by the cold-start dogfood — `duration: NaNs` in /api/tasks
|
|
105
|
+
* for completed-but-not-yet-restarted sessions. Bug 2 sister-fix. */
|
|
106
|
+
completedAtBySession = new Map();
|
|
107
|
+
/** PM-UX Layer 3 · Final Report cache (iteration 11 MVP, UI-integration iter 12).
|
|
108
|
+
* Populated when generateFinalReport resolves in the exit handler. Web UI
|
|
109
|
+
* reads via getFinalReport() through the /api/tasks serializer so non-technical
|
|
110
|
+
* users see the prose summary without opening a terminal. */
|
|
111
|
+
finalReportBySession = new Map();
|
|
99
112
|
spawn(prompt, agentId = 'claude-code', options) {
|
|
100
113
|
const adapter = getAdapter(agentId);
|
|
101
114
|
if (!adapter) {
|
|
@@ -121,14 +134,50 @@ export class SessionManager {
|
|
|
121
134
|
if (event.type !== 'exit')
|
|
122
135
|
return;
|
|
123
136
|
const data = event.data;
|
|
137
|
+
const completedAt = Date.now();
|
|
138
|
+
// Surface completedAt to API consumers reading the still-in-memory
|
|
139
|
+
// session (Bug 2 sister-fix, May 2). Without this, /api/tasks shows
|
|
140
|
+
// status=completed but no completedAt → NaN duration in the UI.
|
|
141
|
+
this.completedAtBySession.set(session.sessionId, completedAt);
|
|
124
142
|
appendCompleted({
|
|
125
143
|
id: session.sessionId,
|
|
126
144
|
status: session.status,
|
|
127
|
-
durationMs: Number(data?.durationMs ??
|
|
128
|
-
completedAt
|
|
145
|
+
durationMs: Number(data?.durationMs ?? completedAt - session.startedAt),
|
|
146
|
+
completedAt,
|
|
129
147
|
steps: session.steps ? [...session.steps] : undefined,
|
|
130
148
|
artifacts: session.artifacts,
|
|
131
149
|
});
|
|
150
|
+
// PM-UX Layer 3 · Final Report MVP. Fire-and-forget; the report appears
|
|
151
|
+
// in the terminal a few seconds after exit. Failure modes return null
|
|
152
|
+
// silently (claude CLI not installed, timeout, non-zero exit). No SSE,
|
|
153
|
+
// no UI integration in this MVP — terminal output only so the dogfood
|
|
154
|
+
// loop sees the artifact immediately. See `generate-final-report.ts`.
|
|
155
|
+
const reportInput = {
|
|
156
|
+
sessionId: session.sessionId,
|
|
157
|
+
prompt: session.prompt,
|
|
158
|
+
agent: session.agent,
|
|
159
|
+
status: session.status,
|
|
160
|
+
durationMs: completedAt - session.startedAt,
|
|
161
|
+
steps: session.steps ? [...session.steps] : [],
|
|
162
|
+
artifacts: session.artifacts,
|
|
163
|
+
};
|
|
164
|
+
generateFinalReport(reportInput)
|
|
165
|
+
.then((report) => {
|
|
166
|
+
if (report) {
|
|
167
|
+
// Cache for web UI consumption via /api/tasks (iter 12 — non-technical
|
|
168
|
+
// users live in the web UI, not the terminal).
|
|
169
|
+
this.finalReportBySession.set(session.sessionId, report);
|
|
170
|
+
try {
|
|
171
|
+
printFinalReportToTerminal(reportInput, report);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Don't let a print failure crash the daemon.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {
|
|
179
|
+
// generateFinalReport never throws by contract; defensive only.
|
|
180
|
+
});
|
|
132
181
|
unsubscribe();
|
|
133
182
|
});
|
|
134
183
|
return session;
|
|
@@ -137,6 +186,20 @@ export class SessionManager {
|
|
|
137
186
|
listPersisted() {
|
|
138
187
|
return this.persistedCache;
|
|
139
188
|
}
|
|
189
|
+
/** Completion timestamp for an in-memory session, if it has exited.
|
|
190
|
+
* Returns undefined if the session is still running OR was never spawned
|
|
191
|
+
* through this manager. Bug 2 sister-fix (May 2): TaskSession itself has
|
|
192
|
+
* no completedAt field; this method lets the API layer compute durations. */
|
|
193
|
+
getCompletedAt(sessionId) {
|
|
194
|
+
return this.completedAtBySession.get(sessionId);
|
|
195
|
+
}
|
|
196
|
+
/** PM-UX Layer 3 · prose Final Report for an in-memory exited session.
|
|
197
|
+
* Returns undefined while generation is in flight OR if the provider chain
|
|
198
|
+
* failed (no claude-free, no claude, no credit, etc). The web UI reads this
|
|
199
|
+
* through the /api/tasks serializer to render the prose summary inline. */
|
|
200
|
+
getFinalReport(sessionId) {
|
|
201
|
+
return this.finalReportBySession.get(sessionId);
|
|
202
|
+
}
|
|
140
203
|
get(sessionId) {
|
|
141
204
|
return this.sessions.get(sessionId);
|
|
142
205
|
}
|
|
@@ -538,6 +538,11 @@ export function startMCPServer(options = {}) {
|
|
|
538
538
|
startedAt: session.startedAt,
|
|
539
539
|
status: session.status,
|
|
540
540
|
agent: session.agent,
|
|
541
|
+
// Iter 13 — include canAppendMessage so the freshly-attached
|
|
542
|
+
// session card can correctly enable/disable reply input on the
|
|
543
|
+
// first render. Without this, ALL just-spawned sessions render
|
|
544
|
+
// with reply disabled even when the adapter supports it.
|
|
545
|
+
canAppendMessage: session.canAppendMessage === true,
|
|
541
546
|
}));
|
|
542
547
|
}
|
|
543
548
|
catch (e) {
|
|
@@ -548,16 +553,29 @@ export function startMCPServer(options = {}) {
|
|
|
548
553
|
return;
|
|
549
554
|
}
|
|
550
555
|
if (req.method === 'GET' && req.url === '/api/tasks') {
|
|
551
|
-
const spawned = manager.list().map((s) =>
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
556
|
+
const spawned = manager.list().map((s) => {
|
|
557
|
+
// Bug 2 sister-fix (May 2): include completedAt for in-memory exited
|
|
558
|
+
// sessions so the UI can compute duration. TaskSession interface
|
|
559
|
+
// intentionally lacks completedAt; SessionManager tracks it separately
|
|
560
|
+
// in completedAtBySession map (populated when exit event fires).
|
|
561
|
+
const completedAt = manager.getCompletedAt(s.sessionId);
|
|
562
|
+
const finalReport = manager.getFinalReport(s.sessionId);
|
|
563
|
+
return {
|
|
564
|
+
sessionId: s.sessionId,
|
|
565
|
+
prompt: s.prompt,
|
|
566
|
+
startedAt: s.startedAt,
|
|
567
|
+
status: s.status,
|
|
568
|
+
agent: s.agent,
|
|
569
|
+
steps: s.steps ?? [],
|
|
570
|
+
artifacts: s.artifacts,
|
|
571
|
+
...(completedAt !== undefined ? { completedAt, durationMs: completedAt - s.startedAt } : {}),
|
|
572
|
+
...(finalReport !== undefined ? { finalReport } : {}),
|
|
573
|
+
// Iter 12 — surface canAppendMessage so the UI knows whether to
|
|
574
|
+
// render the reply input or a disabled "follow-up unsupported" state.
|
|
575
|
+
canAppendMessage: s.canAppendMessage === true,
|
|
576
|
+
provenance: 'spawned',
|
|
577
|
+
};
|
|
578
|
+
});
|
|
561
579
|
const activeAgents = manager.listActiveAgents();
|
|
562
580
|
const persisted = manager.listPersisted();
|
|
563
581
|
// Layer 7: timestamp of most recent hook-audit line (= "last edit on this machine")
|
|
@@ -581,7 +599,7 @@ export function startMCPServer(options = {}) {
|
|
|
581
599
|
return;
|
|
582
600
|
}
|
|
583
601
|
// /api/task/:id/events or /api/task/:id/kill
|
|
584
|
-
const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill)$/);
|
|
602
|
+
const m = req.url.match(/^\/api\/task\/([^/]+)\/(events|kill|message)$/);
|
|
585
603
|
if (m) {
|
|
586
604
|
const [, sessionId, verb] = m;
|
|
587
605
|
const session = manager.get(sessionId);
|
|
@@ -612,6 +630,38 @@ export function startMCPServer(options = {}) {
|
|
|
612
630
|
res.end(JSON.stringify({ sessionId, status: session.status }));
|
|
613
631
|
return;
|
|
614
632
|
}
|
|
633
|
+
// Iter 12 — POST /api/task/:id/message — follow-up message to a
|
|
634
|
+
// completed session. The reply-to-thread surface in the web UI calls
|
|
635
|
+
// this. Body: { text }. Response: { result, durationMs }.
|
|
636
|
+
if (verb === 'message' && req.method === 'POST') {
|
|
637
|
+
if (!session.canAppendMessage || !session.appendMessage) {
|
|
638
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
639
|
+
res.end(JSON.stringify({ error: `Agent ${session.agent} does not support follow-up messages yet.` }));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
let body = '';
|
|
643
|
+
req.on('data', (c) => { body += c.toString('utf-8'); });
|
|
644
|
+
req.on('end', async () => {
|
|
645
|
+
try {
|
|
646
|
+
const parsed = JSON.parse(body || '{}');
|
|
647
|
+
const text = (parsed.text ?? '').trim();
|
|
648
|
+
if (!text) {
|
|
649
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
650
|
+
res.end(JSON.stringify({ error: 'text is required' }));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const out = await session.appendMessage(text);
|
|
654
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
655
|
+
res.end(JSON.stringify({ sessionId, ...out }));
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
659
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
660
|
+
res.end(JSON.stringify({ error: msg }));
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
615
665
|
}
|
|
616
666
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
617
667
|
res.end(JSON.stringify({ error: 'Method not allowed for this path' }));
|
|
@@ -253,6 +253,31 @@ const SHARED_STYLES = `
|
|
|
253
253
|
color: var(--fg-40); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px;
|
|
254
254
|
margin-bottom: 8px;
|
|
255
255
|
}
|
|
256
|
+
/* Iter 12 — prose summary block. Plain English for non-technical users.
|
|
257
|
+
Sans-serif breaks the monospace context so it reads as narrative, not data. */
|
|
258
|
+
.final-report .fr-summary {
|
|
259
|
+
font-family: var(--sans);
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
line-height: 1.55;
|
|
262
|
+
color: var(--fg);
|
|
263
|
+
margin-bottom: 12px;
|
|
264
|
+
padding-bottom: 12px;
|
|
265
|
+
border-bottom: 1px solid var(--border-soft);
|
|
266
|
+
}
|
|
267
|
+
.final-report .fr-summary p {
|
|
268
|
+
margin: 0 0 10px 0;
|
|
269
|
+
}
|
|
270
|
+
.final-report .fr-summary p:last-child { margin-bottom: 0; }
|
|
271
|
+
.final-report .fr-summary-pending p {
|
|
272
|
+
color: var(--fg-50);
|
|
273
|
+
font-style: italic;
|
|
274
|
+
animation: shimmer 1.6s linear infinite;
|
|
275
|
+
background: linear-gradient(90deg, var(--fg-30) 0%, var(--fg-70) 50%, var(--fg-30) 100%);
|
|
276
|
+
background-size: 200% auto;
|
|
277
|
+
-webkit-background-clip: text;
|
|
278
|
+
background-clip: text;
|
|
279
|
+
color: transparent;
|
|
280
|
+
}
|
|
256
281
|
.final-report .fr-files { display: flex; flex-direction: column; gap: 3px; }
|
|
257
282
|
.final-report .fr-file {
|
|
258
283
|
display: grid;
|
|
@@ -1091,8 +1116,30 @@ ${SHARED_STYLES}
|
|
|
1091
1116
|
color: var(--fg-50); font-size: 11px; font-family: var(--mono);
|
|
1092
1117
|
display: flex; align-items: center; gap: 8px;
|
|
1093
1118
|
}
|
|
1094
|
-
.
|
|
1095
|
-
.thread-input
|
|
1119
|
+
/* Iter 12 — reply surface now functional. Wraps textarea + send button. */
|
|
1120
|
+
.thread-input { padding: 8px; flex-direction: column; align-items: stretch; gap: 6px; }
|
|
1121
|
+
.thread-input::before { display: none; }
|
|
1122
|
+
.thread-input .reply-text {
|
|
1123
|
+
width: 100%; min-height: 38px; resize: vertical;
|
|
1124
|
+
background: transparent; border: 0; outline: none;
|
|
1125
|
+
color: var(--fg); font-family: var(--sans); font-size: 12px; line-height: 1.5;
|
|
1126
|
+
padding: 4px 0;
|
|
1127
|
+
}
|
|
1128
|
+
.thread-input .reply-text::placeholder { color: var(--fg-30); }
|
|
1129
|
+
.thread-input .reply-send {
|
|
1130
|
+
align-self: flex-end;
|
|
1131
|
+
padding: 5px 14px; border-radius: 6px;
|
|
1132
|
+
background: var(--accent-wash); color: var(--accent);
|
|
1133
|
+
border: 1px solid rgba(255,154,118,0.25);
|
|
1134
|
+
font-family: var(--sans); font-size: 11px; font-weight: 600;
|
|
1135
|
+
cursor: pointer;
|
|
1136
|
+
}
|
|
1137
|
+
.thread-input .reply-send:hover { background: rgba(255,154,118,0.18); }
|
|
1138
|
+
.thread-input .reply-send:disabled { opacity: 0.4; cursor: default; }
|
|
1139
|
+
.thread-input .reply-status { color: var(--fg-50); font-size: 10px; font-family: var(--mono); }
|
|
1140
|
+
.thread-input .reply-status.error { color: var(--block); }
|
|
1141
|
+
.thread-input.disabled .reply-text,
|
|
1142
|
+
.thread-input.disabled .reply-send { opacity: 0.5; cursor: not-allowed; }
|
|
1096
1143
|
|
|
1097
1144
|
.view-toggle {
|
|
1098
1145
|
display: inline-flex; gap: 0;
|
|
@@ -1165,7 +1212,7 @@ ${SHARED_STYLES}
|
|
|
1165
1212
|
</div>
|
|
1166
1213
|
</div>
|
|
1167
1214
|
</div>
|
|
1168
|
-
|
|
1215
|
+
<!-- intentionally empty: multi-project sidebar arrives in v0.6.x -->
|
|
1169
1216
|
</aside>
|
|
1170
1217
|
|
|
1171
1218
|
<section class="thread-pane">
|
|
@@ -1459,11 +1506,37 @@ ${SHARED_STYLES}
|
|
|
1459
1506
|
list.innerHTML = all.map((a) => {
|
|
1460
1507
|
const cls = presenceCls(a.state);
|
|
1461
1508
|
const glyph = presenceGlyph(a.state);
|
|
1462
|
-
|
|
1509
|
+
// Iter 12 — members are now clickable. Non-technical user expects
|
|
1510
|
+
// clicking an agent in the right pane to pick that agent for the
|
|
1511
|
+
// next task. Click handler below sets the agent <select> + scrolls
|
|
1512
|
+
// intake into view + focuses the prompt textarea.
|
|
1513
|
+
const stateTitle = a.state === 'ready' ? 'ready · click to select' :
|
|
1514
|
+
a.state === 'needs_install' ? 'needs install · click for setup' :
|
|
1515
|
+
a.state === 'needs_auth' ? 'needs auth · click for setup' :
|
|
1516
|
+
(a.state || 'unknown');
|
|
1517
|
+
return '<div class="member" data-agent-id="' + escape(a.id) + '" title="' + escape(stateTitle) + '">' +
|
|
1463
1518
|
'<span class="member-presence ' + cls + '">' + glyph + '</span>' +
|
|
1464
1519
|
'<span class="member-label">' + escape(a.label || a.id) + '</span>' +
|
|
1465
1520
|
'</div>';
|
|
1466
1521
|
}).join('');
|
|
1522
|
+
// Wire click → select-this-agent. One delegated handler is cheaper than
|
|
1523
|
+
// attaching one per item, and survives re-renders without leak.
|
|
1524
|
+
list.onclick = (ev) => {
|
|
1525
|
+
const item = ev.target && ev.target.closest && ev.target.closest('.member');
|
|
1526
|
+
if (!item) return;
|
|
1527
|
+
const id = item.getAttribute('data-agent-id');
|
|
1528
|
+
if (!id) return;
|
|
1529
|
+
// Set the agent dropdown to this agent. Trigger 'change' so the
|
|
1530
|
+
// updateSetupPanel code (existing) fires and shows install/auth help
|
|
1531
|
+
// for needs_install / needs_auth states.
|
|
1532
|
+
if (agentSelect) {
|
|
1533
|
+
agentSelect.value = id;
|
|
1534
|
+
try { agentSelect.dispatchEvent(new Event('change')); } catch { /* old browsers */ }
|
|
1535
|
+
}
|
|
1536
|
+
// Scroll intake into view + focus the prompt textarea.
|
|
1537
|
+
try { promptEl && promptEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* noop */ }
|
|
1538
|
+
if (promptEl) promptEl.focus();
|
|
1539
|
+
};
|
|
1467
1540
|
}
|
|
1468
1541
|
|
|
1469
1542
|
fetch('/api/readiness/agents').then(r => r.json()).then(d => {
|
|
@@ -1496,6 +1569,11 @@ ${SHARED_STYLES}
|
|
|
1496
1569
|
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1497
1570
|
});
|
|
1498
1571
|
|
|
1572
|
+
// Iter 13 — sidebar 'add project' button + handler removed (was iter 12 work).
|
|
1573
|
+
// The handler cleared cwd input + scrolled + focused, but the label promised
|
|
1574
|
+
// a new sidebar entry that we do not ship until v0.6.x multi-project work.
|
|
1575
|
+
// Mismatch caused real founder confusion in iter-13 tasting. Removed cleanly.
|
|
1576
|
+
|
|
1499
1577
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1500
1578
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
1501
1579
|
const starterPromptsEl = document.getElementById('starter-prompts');
|
|
@@ -1731,7 +1809,15 @@ ${SHARED_STYLES}
|
|
|
1731
1809
|
// seed prompt, breaking chronological order in thread view.
|
|
1732
1810
|
'<div class="thread-messages"></div>' +
|
|
1733
1811
|
'<div class="final-report" style="display:none"></div>' +
|
|
1734
|
-
|
|
1812
|
+
// Iter 12 — reply surface now functional; replaced the prior dead
|
|
1813
|
+
// placeholder. Active for adapters with canAppendMessage=true
|
|
1814
|
+
// (claude-code today); disabled state for adapters that don't yet
|
|
1815
|
+
// implement appendMessage.
|
|
1816
|
+
'<div class="thread-input">' +
|
|
1817
|
+
'<textarea class="reply-text" placeholder="Reply to thread… (Cmd+Enter to send)" rows="2"></textarea>' +
|
|
1818
|
+
'<button class="reply-send" type="button">Send</button>' +
|
|
1819
|
+
'<div class="reply-status" style="display:none"></div>' +
|
|
1820
|
+
'</div>' +
|
|
1735
1821
|
'<div class="event-log"></div>' +
|
|
1736
1822
|
'</div>';
|
|
1737
1823
|
|
|
@@ -1746,6 +1832,57 @@ ${SHARED_STYLES}
|
|
|
1746
1832
|
const finalReportEl = card.querySelector('.final-report');
|
|
1747
1833
|
const threadEl = card.querySelector('.thread-messages');
|
|
1748
1834
|
const bodyEl = card.querySelector('.session-body');
|
|
1835
|
+
// Iter 12 — reply-to-thread refs + handler.
|
|
1836
|
+
const threadInputEl = card.querySelector('.thread-input');
|
|
1837
|
+
const replyTextEl = card.querySelector('.reply-text');
|
|
1838
|
+
const replySendBtn = card.querySelector('.reply-send');
|
|
1839
|
+
const replyStatusEl = card.querySelector('.reply-status');
|
|
1840
|
+
if (!session.canAppendMessage) {
|
|
1841
|
+
threadInputEl.classList.add('disabled');
|
|
1842
|
+
replyTextEl.placeholder = 'Reply unsupported for ' + agentLabel(session.agent) + ' yet';
|
|
1843
|
+
replyTextEl.disabled = true;
|
|
1844
|
+
replySendBtn.disabled = true;
|
|
1845
|
+
} else {
|
|
1846
|
+
const sendReply = async () => {
|
|
1847
|
+
const text = (replyTextEl.value || '').trim();
|
|
1848
|
+
if (!text) return;
|
|
1849
|
+
replySendBtn.disabled = true;
|
|
1850
|
+
replySendBtn.textContent = '...';
|
|
1851
|
+
replyStatusEl.style.display = 'none';
|
|
1852
|
+
replyStatusEl.classList.remove('error');
|
|
1853
|
+
// Optimistic: render the user's message immediately in the thread.
|
|
1854
|
+
appendThreadMessage(threadMessage({
|
|
1855
|
+
cls: 'user', author: 'You', time: new Date(), content: text,
|
|
1856
|
+
}));
|
|
1857
|
+
replyTextEl.value = '';
|
|
1858
|
+
try {
|
|
1859
|
+
const r = await fetch('/api/task/' + session.sessionId + '/message', {
|
|
1860
|
+
method: 'POST',
|
|
1861
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1862
|
+
body: JSON.stringify({ text }),
|
|
1863
|
+
});
|
|
1864
|
+
const data = await r.json();
|
|
1865
|
+
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
|
1866
|
+
// Render assistant response in the thread.
|
|
1867
|
+
appendThreadMessage(threadMessage({
|
|
1868
|
+
cls: 'agent', author: agentLabel(session.agent), time: new Date(),
|
|
1869
|
+
content: String(data.result || '(empty response)'),
|
|
1870
|
+
}));
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
replyStatusEl.textContent = 'Reply failed: ' + (err && err.message ? err.message : String(err));
|
|
1873
|
+
replyStatusEl.classList.add('error');
|
|
1874
|
+
replyStatusEl.style.display = 'block';
|
|
1875
|
+
} finally {
|
|
1876
|
+
replySendBtn.disabled = false;
|
|
1877
|
+
replySendBtn.textContent = 'Send';
|
|
1878
|
+
replyTextEl.focus();
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
replySendBtn.addEventListener('click', sendReply);
|
|
1882
|
+
replyTextEl.addEventListener('keydown', (ev) => {
|
|
1883
|
+
if ((ev.metaKey || ev.ctrlKey) && ev.key === 'Enter') { ev.preventDefault(); sendReply(); }
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1749
1886
|
const safetyPillEl = card.querySelector('.safety-pill');
|
|
1750
1887
|
const safetySafeEl = safetyPillEl.querySelector('.safe');
|
|
1751
1888
|
const safetyBlockedEl = safetyPillEl.querySelector('.blocked');
|
|
@@ -1939,12 +2076,35 @@ ${SHARED_STYLES}
|
|
|
1939
2076
|
// Layer 3: render the final report block when the task completes.
|
|
1940
2077
|
// Principles #2 + #7 — density (files/tests/trust in one view) and
|
|
1941
2078
|
// agent-provenance (the report attributes to a specific agent).
|
|
1942
|
-
|
|
1943
|
-
|
|
2079
|
+
// Iter 12 — prose summary (3 paragraphs, plain English) prepended above
|
|
2080
|
+
// the artifact list. Non-technical users see prose first; raw artifact
|
|
2081
|
+
// detail (files/tests/tokens) below as supporting context.
|
|
2082
|
+
function renderArtifacts(artifacts, finalReport, status) {
|
|
2083
|
+
// Show the block if either prose summary OR artifacts exist OR if a
|
|
2084
|
+
// completed task is still generating its report (placeholder state).
|
|
2085
|
+
const isCompleted = status === 'completed' || status === 'killed' || status === 'error';
|
|
2086
|
+
const hasContent = artifacts || finalReport;
|
|
2087
|
+
const isAwaitingReport = isCompleted && !finalReport;
|
|
2088
|
+
if (!hasContent && !isAwaitingReport) { finalReportEl.style.display = 'none'; return; }
|
|
1944
2089
|
finalReportEl.style.display = 'block';
|
|
1945
|
-
const files = Array.isArray(artifacts
|
|
1946
|
-
const tests = artifacts
|
|
2090
|
+
const files = Array.isArray(artifacts?.files) ? artifacts.files : [];
|
|
2091
|
+
const tests = artifacts?.tests;
|
|
1947
2092
|
let html = '<div class="fr-label">Final report · ' + escape(agentLabel(session.agent)) + '</div>';
|
|
2093
|
+
if (finalReport && finalReport.summary) {
|
|
2094
|
+
// Plain-English prose. Split paragraphs on blank lines, render each as <p>.
|
|
2095
|
+
// NOTE: this code is inside a TS template literal — backslashes in
|
|
2096
|
+
// regex literals must be doubled so the served JS gets the right
|
|
2097
|
+
// sequence (test/cli/ui-served-html-syntax.test.ts guards this).
|
|
2098
|
+
const paragraphs = String(finalReport.summary).split(/\\n\\s*\\n/).filter(p => p.trim());
|
|
2099
|
+
html += '<div class="fr-summary">' +
|
|
2100
|
+
paragraphs.map(p => '<p>' + escape(p.trim()) + '</p>').join('') +
|
|
2101
|
+
'</div>';
|
|
2102
|
+
} else if (isAwaitingReport) {
|
|
2103
|
+
// Placeholder — task done, report still generating (or provider failed silently).
|
|
2104
|
+
// After ~30s with no report, the placeholder persists. Iter 13 will replace
|
|
2105
|
+
// with explicit "report unavailable — start FCC proxy or set ANTHROPIC_API_KEY".
|
|
2106
|
+
html += '<div class="fr-summary fr-summary-pending"><p>Generating report…</p></div>';
|
|
2107
|
+
}
|
|
1948
2108
|
if (files.length > 0) {
|
|
1949
2109
|
html += '<div class="fr-files">' + files.map(f => {
|
|
1950
2110
|
const lines = '<span class="add">+' + (f.added || 0) + '</span> <span class="rem">−' + (f.removed || 0) + '</span>';
|
|
@@ -1978,7 +2138,11 @@ ${SHARED_STYLES}
|
|
|
1978
2138
|
|
|
1979
2139
|
// Hydrate from initial snapshot (late-joiner support + persisted sessions)
|
|
1980
2140
|
if (Array.isArray(session.steps) && session.steps.length) renderSteps(session.steps);
|
|
1981
|
-
if (
|
|
2141
|
+
// Render Final Report block if there's anything to show (prose summary,
|
|
2142
|
+
// artifacts, or "generating…" placeholder for a freshly-completed session).
|
|
2143
|
+
if (session.artifacts || session.finalReport || session.status === 'completed' || session.status === 'killed' || session.status === 'error') {
|
|
2144
|
+
renderArtifacts(session.artifacts, session.finalReport, session.status);
|
|
2145
|
+
}
|
|
1982
2146
|
|
|
1983
2147
|
// Persisted sessions have no live event stream and no subprocess to kill.
|
|
1984
2148
|
if (isPersisted) {
|
|
@@ -1994,7 +2158,7 @@ ${SHARED_STYLES}
|
|
|
1994
2158
|
|
|
1995
2159
|
// Layer 2/3 synthetic events — handle before renderEvent (so they don't leak into the raw log)
|
|
1996
2160
|
if (event.type === 'codehere:steps') { renderSteps(event.data?.steps); return; }
|
|
1997
|
-
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data); return; }
|
|
2161
|
+
if (event.type === 'codehere:artifacts') { renderArtifacts(event.data, undefined, session.status); return; }
|
|
1998
2162
|
|
|
1999
2163
|
// Any structured error flips the status pill immediately — don't wait for
|
|
2000
2164
|
// the subprocess to exit. Many CLIs print a logical error to stdout and
|
|
@@ -2032,7 +2196,10 @@ ${SHARED_STYLES}
|
|
|
2032
2196
|
killBtn.textContent = 'Kill';
|
|
2033
2197
|
});
|
|
2034
2198
|
|
|
2035
|
-
|
|
2199
|
+
// Iter 12 — expose renderArtifacts so the polling loop can refresh the
|
|
2200
|
+
// Final Report block when the prose summary arrives async (~2-5s after
|
|
2201
|
+
// task exit). Without this, non-technical users would have to refresh.
|
|
2202
|
+
sessions.set(session.sessionId, { card, logEl, es, renderArtifacts });
|
|
2036
2203
|
}
|
|
2037
2204
|
|
|
2038
2205
|
async function assignTask() {
|
|
@@ -2051,7 +2218,7 @@ ${SHARED_STYLES}
|
|
|
2051
2218
|
});
|
|
2052
2219
|
const data = await res.json();
|
|
2053
2220
|
if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
|
|
2054
|
-
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor' });
|
|
2221
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor', canAppendMessage: data.canAppendMessage === true });
|
|
2055
2222
|
promptEl.value = '';
|
|
2056
2223
|
} catch (e) {
|
|
2057
2224
|
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
@@ -2181,7 +2348,10 @@ ${SHARED_STYLES}
|
|
|
2181
2348
|
}).catch(() => { /* fine */ });
|
|
2182
2349
|
}
|
|
2183
2350
|
loadSessions();
|
|
2184
|
-
// Refresh detected-agents list + banner every 5s
|
|
2351
|
+
// Refresh detected-agents list + banner every 5s. Iter 12 — also patches
|
|
2352
|
+
// existing session cards with new finalReport prose when it arrives async.
|
|
2353
|
+
// Cheap re-render (innerHTML on a small div); non-technical user sees the
|
|
2354
|
+
// report appear on its own, no manual refresh.
|
|
2185
2355
|
setInterval(() => {
|
|
2186
2356
|
fetch('/api/tasks').then(r => r.json()).then(d => {
|
|
2187
2357
|
const seen = new Set();
|
|
@@ -2191,6 +2361,13 @@ ${SHARED_STYLES}
|
|
|
2191
2361
|
}
|
|
2192
2362
|
pruneStaleAgents(seen);
|
|
2193
2363
|
updateBanner(d);
|
|
2364
|
+
// Patch existing session cards with finalReport when it arrives.
|
|
2365
|
+
for (const s of (d.spawned || d.sessions || [])) {
|
|
2366
|
+
const entry = sessions.get(s.sessionId);
|
|
2367
|
+
if (entry && entry.renderArtifacts) {
|
|
2368
|
+
entry.renderArtifacts(s.artifacts, s.finalReport, s.status);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2194
2371
|
}).catch(() => { /* fine */ });
|
|
2195
2372
|
}, 5000);
|
|
2196
2373
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project thread migration — sessions.jsonl → projects/<slug>/thread.jsonl.
|
|
3
|
+
*
|
|
4
|
+
* v0.6.0 M1+M7 foundation. The new schema groups sessions by their cwd
|
|
5
|
+
* (project) so the persistent project thread shape (one continuous chat per
|
|
6
|
+
* project) has a place to live. The original sessions.jsonl is preserved
|
|
7
|
+
* for one release as back-compat.
|
|
8
|
+
*
|
|
9
|
+
* Built from Spike-1 prototype (agent/spike/v0.6.0-migrate-sessions-to-threads.mjs).
|
|
10
|
+
* Spike validated lossless + reversible on 612 real sessions in 5ms.
|
|
11
|
+
*
|
|
12
|
+
* Production additions over the spike:
|
|
13
|
+
* - Atomic per-project writes (.tmp + rename) so partial failures don't
|
|
14
|
+
* leave half-written thread.jsonl files
|
|
15
|
+
* - Idempotent: skips writing if target thread.jsonl already exists
|
|
16
|
+
* (re-run on daemon start is safe)
|
|
17
|
+
* - Temp-dir filter: skips /tmp/, /var/folders/, /private/var/folders/ by
|
|
18
|
+
* default (configurable) — these are test artifacts, not real projects
|
|
19
|
+
* - Abandoned-session marker: sessions with no completed event get
|
|
20
|
+
* status='abandoned' instead of silent null
|
|
21
|
+
* - Configurable input/output paths so tests can use temp fixtures
|
|
22
|
+
*
|
|
23
|
+
* NOT in scope for this module (handled by callers):
|
|
24
|
+
* - CLI interface (codehere migrate-threads — TBD subcommand)
|
|
25
|
+
* - Auto-run on daemon start
|
|
26
|
+
* - Forward-mirroring (writing to thread.jsonl alongside sessions.jsonl
|
|
27
|
+
* during normal daemon operation)
|
|
28
|
+
*
|
|
29
|
+
* See docs/research/v0.6.0-spike-1-migration.md for the validation history.
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, renameSync } from 'fs';
|
|
32
|
+
import { join } from 'path';
|
|
33
|
+
import { createHash } from 'crypto';
|
|
34
|
+
/** Path patterns considered ephemeral and skipped from project list. */
|
|
35
|
+
const DEFAULT_TEMP_DIR_PATTERNS = [
|
|
36
|
+
/^\/tmp\//,
|
|
37
|
+
/^\/var\/folders\//,
|
|
38
|
+
/^\/private\/var\/folders\//,
|
|
39
|
+
/^\/private\/tmp\//,
|
|
40
|
+
];
|
|
41
|
+
/** sha1(cwd)[:12] — stable, short, filesystem-safe. */
|
|
42
|
+
function slugForCwd(cwd, unknownLabel) {
|
|
43
|
+
if (!cwd)
|
|
44
|
+
return unknownLabel;
|
|
45
|
+
return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
|
|
46
|
+
}
|
|
47
|
+
function isTempDir(cwd) {
|
|
48
|
+
if (!cwd)
|
|
49
|
+
return false;
|
|
50
|
+
return DEFAULT_TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
|
|
51
|
+
}
|
|
52
|
+
/** Read JSONL events; tolerate malformed lines (count + skip). */
|
|
53
|
+
function readEvents(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return { events: [], malformed: 0 };
|
|
56
|
+
const text = readFileSync(path, 'utf-8');
|
|
57
|
+
const lines = text.split('\n').filter((l) => l.trim());
|
|
58
|
+
const events = [];
|
|
59
|
+
let malformed = 0;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
try {
|
|
62
|
+
events.push(JSON.parse(line));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
malformed++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { events, malformed };
|
|
69
|
+
}
|
|
70
|
+
/** Roll up event-log entries into one record per session. */
|
|
71
|
+
function rollupSessions(events) {
|
|
72
|
+
const byId = new Map();
|
|
73
|
+
const completedIds = new Set();
|
|
74
|
+
for (const e of events) {
|
|
75
|
+
const id = (e.id ?? e.sessionId);
|
|
76
|
+
if (!id)
|
|
77
|
+
continue;
|
|
78
|
+
if (e.t === 'completed' || e.completedAt !== undefined)
|
|
79
|
+
completedIds.add(id);
|
|
80
|
+
const existing = byId.get(id) ?? { id, abandoned: true };
|
|
81
|
+
const existingAny = existing;
|
|
82
|
+
for (const k of ['agent', 'prompt', 'cwd', 'startedAt', 'status', 'durationMs', 'completedAt', 'steps', 'artifacts']) {
|
|
83
|
+
if (e[k] !== undefined && existingAny[k] === undefined) {
|
|
84
|
+
existingAny[k] = e[k];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
byId.set(id, existing);
|
|
88
|
+
}
|
|
89
|
+
// Mark abandoned (sessions with no completed event)
|
|
90
|
+
for (const s of byId.values()) {
|
|
91
|
+
s.abandoned = !completedIds.has(s.id);
|
|
92
|
+
if (s.abandoned && !s.status)
|
|
93
|
+
s.status = 'abandoned';
|
|
94
|
+
}
|
|
95
|
+
return [...byId.values()];
|
|
96
|
+
}
|
|
97
|
+
function groupByCwd(sessions, unknownLabel) {
|
|
98
|
+
const groups = new Map();
|
|
99
|
+
for (const s of sessions) {
|
|
100
|
+
const cwd = s.cwd ?? null;
|
|
101
|
+
const slug = slugForCwd(cwd, unknownLabel);
|
|
102
|
+
const group = groups.get(slug) ?? { slug, cwd, sessions: [] };
|
|
103
|
+
group.sessions.push(s);
|
|
104
|
+
groups.set(slug, group);
|
|
105
|
+
}
|
|
106
|
+
return [...groups.values()];
|
|
107
|
+
}
|
|
108
|
+
/** Atomic write: write to .tmp then rename. Caller ensures dir exists. */
|
|
109
|
+
function atomicWriteFile(path, content) {
|
|
110
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
111
|
+
writeFileSync(tmp, content);
|
|
112
|
+
renameSync(tmp, path);
|
|
113
|
+
}
|
|
114
|
+
function buildThreadJsonl(group) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(JSON.stringify({
|
|
117
|
+
_meta: 'project',
|
|
118
|
+
slug: group.slug,
|
|
119
|
+
cwd: group.cwd,
|
|
120
|
+
sessionCount: group.sessions.length,
|
|
121
|
+
migratedFrom: 'sessions.jsonl',
|
|
122
|
+
migratedAt: Date.now(),
|
|
123
|
+
}));
|
|
124
|
+
const sorted = [...group.sessions].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
|
125
|
+
for (const s of sorted) {
|
|
126
|
+
lines.push(JSON.stringify({
|
|
127
|
+
type: 'task',
|
|
128
|
+
sessionId: s.id,
|
|
129
|
+
agent: s.agent,
|
|
130
|
+
prompt: s.prompt,
|
|
131
|
+
cwd: s.cwd,
|
|
132
|
+
startedAt: s.startedAt,
|
|
133
|
+
status: s.status,
|
|
134
|
+
durationMs: s.durationMs,
|
|
135
|
+
completedAt: s.completedAt,
|
|
136
|
+
steps: s.steps,
|
|
137
|
+
artifacts: s.artifacts,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
return lines.join('\n') + '\n';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run the migration. Idempotent: skips projects whose thread.jsonl already
|
|
144
|
+
* exists (so daemon-startup auto-runs are safe).
|
|
145
|
+
*/
|
|
146
|
+
export function migrate(options) {
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
const skipTempDirs = options.skipTempDirs ?? true;
|
|
149
|
+
const unknownLabel = options.unknownCwdLabel ?? 'unknown';
|
|
150
|
+
const { events } = readEvents(options.sourceJsonl);
|
|
151
|
+
const sessions = rollupSessions(events);
|
|
152
|
+
const groups = groupByCwd(sessions, unknownLabel);
|
|
153
|
+
let threadsWritten = 0;
|
|
154
|
+
let threadsSkippedExisting = 0;
|
|
155
|
+
let unknownCwdSessions = 0;
|
|
156
|
+
let abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
157
|
+
let tempDirSessionsSkipped = 0;
|
|
158
|
+
for (const group of groups) {
|
|
159
|
+
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
160
|
+
tempDirSessionsSkipped += group.sessions.length;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (group.cwd === null)
|
|
164
|
+
unknownCwdSessions += group.sessions.length;
|
|
165
|
+
const dir = join(options.targetDir, group.slug);
|
|
166
|
+
const threadFile = join(dir, 'thread.jsonl');
|
|
167
|
+
if (existsSync(threadFile)) {
|
|
168
|
+
threadsSkippedExisting++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
mkdirSync(dir, { recursive: true });
|
|
172
|
+
atomicWriteFile(threadFile, buildThreadJsonl(group));
|
|
173
|
+
threadsWritten++;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
sessionsProcessed: sessions.length,
|
|
177
|
+
threadsWritten,
|
|
178
|
+
threadsSkippedExisting,
|
|
179
|
+
unknownCwdSessions,
|
|
180
|
+
abandonedSessions,
|
|
181
|
+
tempDirSessionsSkipped,
|
|
182
|
+
durationMs: Date.now() - startedAt,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Dry-run: same as migrate but writes nothing. Returns what WOULD happen. */
|
|
186
|
+
export function dryRun(options) {
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
const skipTempDirs = options.skipTempDirs ?? true;
|
|
189
|
+
const unknownLabel = options.unknownCwdLabel ?? 'unknown';
|
|
190
|
+
const { events } = readEvents(options.sourceJsonl);
|
|
191
|
+
const sessions = rollupSessions(events);
|
|
192
|
+
const groups = groupByCwd(sessions, unknownLabel);
|
|
193
|
+
let threadsWritten = 0;
|
|
194
|
+
let threadsSkippedExisting = 0;
|
|
195
|
+
let unknownCwdSessions = 0;
|
|
196
|
+
let abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
197
|
+
let tempDirSessionsSkipped = 0;
|
|
198
|
+
for (const group of groups) {
|
|
199
|
+
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
200
|
+
tempDirSessionsSkipped += group.sessions.length;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (group.cwd === null)
|
|
204
|
+
unknownCwdSessions += group.sessions.length;
|
|
205
|
+
const threadFile = join(options.targetDir, group.slug, 'thread.jsonl');
|
|
206
|
+
if (existsSync(threadFile))
|
|
207
|
+
threadsSkippedExisting++;
|
|
208
|
+
else
|
|
209
|
+
threadsWritten++;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
sessionsProcessed: sessions.length,
|
|
213
|
+
threadsWritten,
|
|
214
|
+
threadsSkippedExisting,
|
|
215
|
+
unknownCwdSessions,
|
|
216
|
+
abandonedSessions,
|
|
217
|
+
tempDirSessionsSkipped,
|
|
218
|
+
durationMs: Date.now() - startedAt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Rollback: removes the projects directory. Source sessions.jsonl is never
|
|
223
|
+
* touched. Returns the count of removed project subdirectories.
|
|
224
|
+
*/
|
|
225
|
+
export function rollback(targetDir) {
|
|
226
|
+
if (!existsSync(targetDir))
|
|
227
|
+
return { removedDirs: 0 };
|
|
228
|
+
const subdirs = readdirSync(targetDir);
|
|
229
|
+
rmSync(targetDir, { recursive: true });
|
|
230
|
+
return { removedDirs: subdirs.length };
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=project-thread-migration.js.map
|
|
@@ -550,8 +550,11 @@ export function executeInitCommand(options = {}) {
|
|
|
550
550
|
console.log(` 1. Open ${colors.cyan('.codehere/CONTEXT.md')} and fill in what this repo is`);
|
|
551
551
|
console.log(` 2. Run ${colors.cyan('codehere start')} and delegate your first task`);
|
|
552
552
|
console.log(` 3. Track in-flight work in ${colors.cyan('.codehere/HANDOVER.md')} as you go`);
|
|
553
|
-
|
|
554
|
-
|
|
553
|
+
// Power-user / advanced layout entry points moved to docs/advanced/ per the
|
|
554
|
+
// "one canonical entry" rule (May 2 CUT pass). Used to print two extra
|
|
555
|
+
// dim lines pointing at `codehere memory show injection` and the F3
|
|
556
|
+
// canonical layout migration — both confused new users in real-task
|
|
557
|
+
// dogfood. Power users find them in docs/MEMORY.md.
|
|
555
558
|
newline();
|
|
556
559
|
}
|
|
557
560
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.4",
|
|
4
4
|
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|