codehere 0.7.0 → 0.7.1
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 +86 -191
- package/dist/infrastructure/orchestration/aider-adapter.js +4 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +5 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +4 -9
- package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
- package/dist/infrastructure/orchestration/graphify-context.js +18 -13
- package/dist/infrastructure/orchestration/memory-injector.js +46 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +21 -24
- package/dist/infrastructure/orchestration/session-manager.js +18 -0
- package/dist/infrastructure/orchestration/session-persistence.js +2 -2
- package/dist/infrastructure/protocols/mcp-server-impl.js +3 -1
- package/dist/infrastructure/protocols/ui-page-v2.js +162 -30
- package/package.json +2 -1
|
@@ -1,107 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Final Report —
|
|
2
|
+
* Final Report — the headline deliverable. At task exit, produce a plain-English
|
|
3
|
+
* summary of what the agent did.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Engine (locked 2026-06-03, after the real dogfood found NO report on a real
|
|
6
|
+
* conductor run): REUSE THE AGENT SESSION. The agent already narrates what it
|
|
7
|
+
* did (kind:'say' steps — e.g. "Added subtract(a,b) to utils.js:5"); we use that
|
|
8
|
+
* directly. No fresh summarizer spawn.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - If neither binary is installed, returns null silently (no report).
|
|
10
|
+
* Why the old path was replaced: it spawned a SEPARATE provider binary chain
|
|
11
|
+
* (`claude-free` → `claude`) to summarize. `claude-free` (the FCC/NIM proxy
|
|
12
|
+
* binary) is on PATH but its proxy was down → 30s hang → null; the `claude`
|
|
13
|
+
* fallback also returned null in the conductor exit-handler. Net: a completed
|
|
14
|
+
* real task showed "⚠ Final Report unavailable" — the headline deliverable
|
|
15
|
+
* missing. Reusing the agent's own words is cheaper, has full context, and can't
|
|
16
|
+
* be taken down by an external proxy.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
18
|
+
* Fallback (locked): when the agent left no usable summary (or it looks like a
|
|
19
|
+
* provider failure), build a DETERMINISTIC report from the recorded steps +
|
|
20
|
+
* artifacts. It states ONLY verified facts, says plainly that no written summary
|
|
21
|
+
* was available, and NEVER implies success — so a completed task always shows a
|
|
22
|
+
* deliverable, and it's honest about which kind it is (see `source`).
|
|
25
23
|
*/
|
|
26
|
-
|
|
27
|
-
import { isMockMode } from '../../infrastructure/orchestration/mocks/resolve-agent-spawn.js';
|
|
28
|
-
import { mockFinalReport } from '../../infrastructure/orchestration/mocks/mock-synthesis.js';
|
|
29
|
-
const PROMPT_TEMPLATE = (i) => `You are a PM reporting back to a developer about what their AI coding agent
|
|
30
|
-
just did. Write 3 short paragraphs in plain English. No headers, no bullet
|
|
31
|
-
lists, no markdown — just three paragraphs separated by blank lines.
|
|
32
|
-
|
|
33
|
-
CRITICAL anti-hallucination rule: ground every claim in the structured data
|
|
34
|
-
below. You CANNOT see the contents of files, the output of commands, or
|
|
35
|
-
anything beyond what's listed under "Steps the agent took" and "Files
|
|
36
|
-
changed". If the data doesn't show specific filenames, do NOT invent any.
|
|
37
|
-
If a step ran a command but its output isn't shown to you, describe what
|
|
38
|
-
the step ATTEMPTED, not what it found. Saying "I don't have the output to
|
|
39
|
-
report on" is correct; making up plausible-sounding details is wrong and
|
|
40
|
-
the developer will catch the lie.
|
|
41
|
-
|
|
42
|
-
Paragraph 1: What the agent ATTEMPTED. Verb-led. Describe steps as listed.
|
|
43
|
-
Mention specific filenames ONLY if they appear in the data
|
|
44
|
-
below. 1-3 sentences.
|
|
45
|
-
Paragraph 2: What was TOUCHED. Reference ONLY the "Files changed" list
|
|
46
|
-
below. If the list is empty AND "File tracking" says git is
|
|
47
|
-
available, say "No files were changed." If "File tracking" says
|
|
48
|
-
git is NOT available, say codehere could not track file changes
|
|
49
|
-
because this folder is not a git repository — do NOT say no files
|
|
50
|
-
were changed, because the agent may well have changed files
|
|
51
|
-
codehere cannot see. 1-2 sentences.
|
|
52
|
-
Paragraph 3: What the developer should LOOK AT. If steps + files data show
|
|
53
|
-
nothing risky, say so explicitly ("nothing to verify; the
|
|
54
|
-
session was read-only" or similar). 1-2 sentences.
|
|
55
|
-
|
|
56
|
-
Be specific where the data lets you. Don't pad. Don't editorialize. Don't
|
|
57
|
-
praise the agent. Don't fabricate file names, line counts, or content
|
|
58
|
-
descriptions you cannot derive from the data below.
|
|
59
|
-
|
|
60
|
-
The task was: ${i.prompt}
|
|
61
|
-
The agent used: ${i.agent}
|
|
62
|
-
Duration: ${i.durationMs}ms
|
|
63
|
-
Status: ${i.status}
|
|
64
|
-
|
|
65
|
-
Steps the agent took:
|
|
66
|
-
${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') : '(no structured steps captured)'}
|
|
67
|
-
|
|
68
|
-
File tracking: ${i.artifacts?.noGitTracking ? 'git NOT available (this folder is not a git repository — file changes cannot be tracked)' : 'git available'}
|
|
69
|
-
|
|
70
|
-
Files changed:
|
|
71
|
-
${i.artifacts?.files?.length
|
|
72
|
-
? i.artifacts.files
|
|
73
|
-
.map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`)
|
|
74
|
-
.join('\n')
|
|
75
|
-
: '(no file artifacts captured)'}
|
|
76
|
-
|
|
77
|
-
Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
|
|
78
|
-
|
|
79
|
-
Now write the 3 paragraphs, grounded ONLY in the data above.`;
|
|
80
|
-
const REPORT_TIMEOUT_MS = 30_000;
|
|
81
|
-
/** Provider chain: try free first, then paid. Override with CODEHERE_REPORT_BINARY. */
|
|
82
|
-
function selectProviderBinary() {
|
|
83
|
-
const override = process.env.CODEHERE_REPORT_BINARY;
|
|
84
|
-
if (override)
|
|
85
|
-
return [override];
|
|
86
|
-
return ['claude-free', 'claude'];
|
|
87
|
-
}
|
|
24
|
+
const MAX_SUMMARY_CHARS = 1600;
|
|
88
25
|
/**
|
|
89
|
-
* UAT-003 — patterns
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* recovery hint instead of letting the raw provider error echo through.
|
|
93
|
-
*
|
|
94
|
-
* Pre-fix: bare `exit` in REPL → Conductor spawned claude-code → real API
|
|
95
|
-
* call failed with "All connection attempts failed Request ID: req_...";
|
|
96
|
-
* Final Report quoted that text verbatim. Per Core Values #4 + #6, this is
|
|
97
|
-
* unacceptable — the user sees a Request ID that means nothing.
|
|
26
|
+
* UAT-003 — patterns indicating the agent's own text is actually a network /
|
|
27
|
+
* auth / quota failure, not a real summary. Such "summaries" must NOT be shown
|
|
28
|
+
* as the report; they fall through to the deterministic fallback.
|
|
98
29
|
*/
|
|
99
30
|
const NETWORK_FAILURE_PATTERNS = /(?:all connection attempts failed|ECONNREFUSED|ENOTFOUND|fetch failed|network error|request id:\s*req_|connection timed out|getaddrinfo)/i;
|
|
100
31
|
function isLikelyNetworkFailureSummary(text) {
|
|
101
32
|
return NETWORK_FAILURE_PATTERNS.test(text);
|
|
102
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* ENGINE: the agent's own narration. kind:'say' steps are the words the agent
|
|
36
|
+
* already emitted (reasoning + wrap-up). Joined newest-context-last, they ARE
|
|
37
|
+
* the summary — no extra spawn. Returns null if the agent said nothing usable.
|
|
38
|
+
*/
|
|
39
|
+
function extractAgentSummary(steps) {
|
|
40
|
+
const says = steps
|
|
41
|
+
.filter((s) => s.kind === 'say' && typeof s.label === 'string' && s.label.trim().length > 0)
|
|
42
|
+
.map((s) => s.label.trim());
|
|
43
|
+
if (!says.length)
|
|
44
|
+
return null;
|
|
45
|
+
const text = says.join('\n\n').slice(0, MAX_SUMMARY_CHARS);
|
|
46
|
+
// Too short to be a real summary (e.g. a one-word ack) → let the fallback give facts.
|
|
47
|
+
return text.length >= 12 ? text : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* FALLBACK: deterministic, honest. Facts only, from steps + artifacts. Says
|
|
51
|
+
* plainly that no written summary was available. Never implies success.
|
|
52
|
+
*/
|
|
53
|
+
function buildDeterministicReport(input) {
|
|
54
|
+
const files = input.artifacts?.files ?? [];
|
|
55
|
+
const toolSteps = input.steps.filter((s) => s.kind !== 'say');
|
|
56
|
+
const noGit = input.artifacts?.noGitTracking;
|
|
57
|
+
const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
|
|
58
|
+
const parts = [
|
|
59
|
+
`No plain-English summary was available for this run — ${agentName} didn't leave a written summary, and codehere doesn't fabricate one. Here are the recorded facts.`,
|
|
60
|
+
`The session ran ${input.steps.length} step${input.steps.length === 1 ? '' : 's'}` +
|
|
61
|
+
(toolSteps.length
|
|
62
|
+
? ` (${toolSteps.slice(0, 5).map((s) => s.label).join(', ')}${toolSteps.length > 5 ? ', …' : ''})`
|
|
63
|
+
: '') +
|
|
64
|
+
`.`,
|
|
65
|
+
];
|
|
66
|
+
if (noGit) {
|
|
67
|
+
parts.push(`This folder isn't a git repository, so codehere couldn't track which files changed — check the working tree yourself.`);
|
|
68
|
+
}
|
|
69
|
+
else if (files.length) {
|
|
70
|
+
parts.push(`Files changed: ` +
|
|
71
|
+
files
|
|
72
|
+
.map((f) => `${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust ${f.trust}` : ''})`)
|
|
73
|
+
.join('; ') +
|
|
74
|
+
`.`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
parts.push(`No files were changed.`);
|
|
78
|
+
}
|
|
79
|
+
return { summary: parts.join('\n\n'), generatedAt: Date.now(), durationMs: 0, source: 'deterministic' };
|
|
80
|
+
}
|
|
103
81
|
function buildErrorRecoveryReport(input, durationMs) {
|
|
104
|
-
// Tailor the hint to the specific failure shape we can detect from input.
|
|
105
82
|
const agentName = input.agent === 'conductor' ? 'the agent codehere chose' : input.agent;
|
|
106
83
|
return {
|
|
107
84
|
summary: `The ${agentName} session ended without completing the task. ` +
|
|
@@ -112,114 +89,32 @@ function buildErrorRecoveryReport(input, durationMs) {
|
|
|
112
89
|
`codehere couldn't confirm any file changes from this attempt.`,
|
|
113
90
|
generatedAt: Date.now(),
|
|
114
91
|
durationMs,
|
|
92
|
+
source: 'error-recovery',
|
|
115
93
|
};
|
|
116
94
|
}
|
|
117
95
|
/**
|
|
118
|
-
* Generate a Final Report.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* UAT-003 — when the input.status is 'error', we skip the LLM call entirely
|
|
122
|
-
* and return a deterministic recovery hint. Saves tokens on a known-failed
|
|
123
|
-
* task AND gives the user a plain-English next step instead of waiting for
|
|
124
|
-
* a possibly-unhelpful LLM summary.
|
|
96
|
+
* Generate a Final Report. Reuses the agent's own summary; falls back to a
|
|
97
|
+
* deterministic, honest facts report. A completed task ALWAYS gets a report
|
|
98
|
+
* (never null for non-error terminal states) — no external dependency.
|
|
125
99
|
*/
|
|
126
100
|
export async function generateFinalReport(input) {
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
101
|
+
// No mock short-circuit: under CODEHERE_MOCK the SAME engine runs, so the
|
|
102
|
+
// narration gap is reproduced faithfully — claude/aider (emit `say`) get a
|
|
103
|
+
// rich agent-summary; codex/gemini/opencode (no `say`) get the honest
|
|
104
|
+
// deterministic report, exactly as on a real run. (A prior mock-only
|
|
105
|
+
// short-circuit synthesized a report from steps for every agent, hiding the
|
|
106
|
+
// cross-vendor gap — you couldn't dogfood "codex's report is thin" under mock.)
|
|
107
|
+
// Errored session — deterministic recovery hint, no summarization of nothing.
|
|
134
108
|
if (input.status === 'error') {
|
|
135
109
|
return buildErrorRecoveryReport(input, 0);
|
|
136
110
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Defense-in-depth: even on 'completed' status, if the generated
|
|
142
|
-
// summary leaks a provider network-failure string (e.g., the wrapped
|
|
143
|
-
// agent exited 0 but its assistant text was an error message), wrap
|
|
144
|
-
// the summary with a recovery preamble.
|
|
145
|
-
if (isLikelyNetworkFailureSummary(result.summary)) {
|
|
146
|
-
return {
|
|
147
|
-
summary: `Couldn't reach the provider cleanly — the agent's session text looks like a network or auth failure.\n\n` +
|
|
148
|
-
`What to try: check your internet, run \`codehere health\`, or rerun in a minute. ` +
|
|
149
|
-
`If it persists, switch agents from the dropdown.\n\n` +
|
|
150
|
-
`— Raw agent message —\n${result.summary}`,
|
|
151
|
-
generatedAt: result.generatedAt,
|
|
152
|
-
durationMs: result.durationMs,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
111
|
+
// ENGINE: reuse the agent's own narration (no fresh spawn).
|
|
112
|
+
const agentSummary = extractAgentSummary(input.steps);
|
|
113
|
+
if (agentSummary && !isLikelyNetworkFailureSummary(agentSummary)) {
|
|
114
|
+
return { summary: agentSummary, generatedAt: Date.now(), durationMs: 0, source: 'agent-summary' };
|
|
157
115
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
function tryProvider(binary, prompt, sessionId) {
|
|
161
|
-
const startedAt = Date.now();
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
let stdout = '';
|
|
164
|
-
let stderr = '';
|
|
165
|
-
let settled = false;
|
|
166
|
-
const finish = (result) => {
|
|
167
|
-
if (settled)
|
|
168
|
-
return;
|
|
169
|
-
settled = true;
|
|
170
|
-
resolve(result);
|
|
171
|
-
};
|
|
172
|
-
// Provider args per-binary:
|
|
173
|
-
// - `claude-free` (FCC proxy binary) needs `--bare` to skip session
|
|
174
|
-
// setup that the proxy doesn't implement.
|
|
175
|
-
// - Standard `claude` (the Claude Code Max CLI) MUST NOT use `--bare`
|
|
176
|
-
// because `--bare` explicitly rejects OAuth and keychain auth — and
|
|
177
|
-
// Claude Code Max users authenticate via OAuth. With `--bare`, the
|
|
178
|
-
// fallback path returned empty silently, no Final Report ever
|
|
179
|
-
// reached the UI, and the dashboard showed `✓ Final Report` on a
|
|
180
|
-
// missing artifact. (May 21, 2026 — root cause of CRIT #1.)
|
|
181
|
-
const args = binary === 'claude-free' ? ['--bare', '-p', prompt] : ['-p', prompt];
|
|
182
|
-
const child = spawn(binary, args, {
|
|
183
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
-
});
|
|
185
|
-
const timeout = setTimeout(() => {
|
|
186
|
-
child.kill('SIGTERM');
|
|
187
|
-
if (process.env.CODEHERE_DEBUG === '1') {
|
|
188
|
-
console.error(`[final-report] ${binary} timeout after ${REPORT_TIMEOUT_MS}ms for ${sessionId}`);
|
|
189
|
-
}
|
|
190
|
-
finish(null);
|
|
191
|
-
}, REPORT_TIMEOUT_MS);
|
|
192
|
-
child.on('error', (err) => {
|
|
193
|
-
clearTimeout(timeout);
|
|
194
|
-
// ENOENT = binary not installed. Silent fallback to next provider in chain.
|
|
195
|
-
if (process.env.CODEHERE_DEBUG === '1') {
|
|
196
|
-
console.error(`[final-report] ${binary} unavailable: ${err.message}`);
|
|
197
|
-
}
|
|
198
|
-
finish(null);
|
|
199
|
-
});
|
|
200
|
-
child.stdout?.on('data', (chunk) => {
|
|
201
|
-
stdout += chunk.toString('utf8');
|
|
202
|
-
});
|
|
203
|
-
child.stderr?.on('data', (chunk) => {
|
|
204
|
-
stderr += chunk.toString('utf8');
|
|
205
|
-
});
|
|
206
|
-
child.on('exit', (code) => {
|
|
207
|
-
clearTimeout(timeout);
|
|
208
|
-
if (code !== 0) {
|
|
209
|
-
if (process.env.CODEHERE_DEBUG === '1') {
|
|
210
|
-
console.error(`[final-report] ${binary} exited ${code}: ${stderr.slice(0, 200)}`);
|
|
211
|
-
}
|
|
212
|
-
finish(null);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const summary = stdout.trim();
|
|
216
|
-
if (!summary) {
|
|
217
|
-
finish(null);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
finish({ summary, generatedAt: Date.now(), durationMs: Date.now() - startedAt });
|
|
221
|
-
});
|
|
222
|
-
});
|
|
116
|
+
// FALLBACK: deterministic, honest — always a deliverable, never a fake ✓.
|
|
117
|
+
return buildDeterministicReport(input);
|
|
223
118
|
}
|
|
224
119
|
/**
|
|
225
120
|
* Pretty-print to terminal. Used by session-manager when a spawned task exits.
|
|
@@ -342,7 +342,10 @@ export function spawnAider(prompt, options = {}) {
|
|
|
342
342
|
return new Promise((resolve, reject) => {
|
|
343
343
|
let stdout = '';
|
|
344
344
|
let stderr = '';
|
|
345
|
-
|
|
345
|
+
// Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
|
|
346
|
+
// hits the mock REPL, not real aider. (Was a real-spend leak.)
|
|
347
|
+
const sp2 = resolveAgentSpawn('aider', 'aider', ['--restore-chat-history', '--message', text, ...HEADLESS_FLAGS], text);
|
|
348
|
+
const followup = spawn(sp2.command, sp2.args, {
|
|
346
349
|
cwd,
|
|
347
350
|
env: spawnEnv,
|
|
348
351
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -384,7 +384,11 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
384
384
|
}
|
|
385
385
|
let stdout = '';
|
|
386
386
|
let stderr = '';
|
|
387
|
-
|
|
387
|
+
// Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
|
|
388
|
+
// hits the mock REPL, not the real binary. (Was a real-spend leak:
|
|
389
|
+
// appendMessage spawned `claude` directly regardless of mock mode.)
|
|
390
|
+
const sp2 = resolveAgentSpawn('claude-code', 'claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], text);
|
|
391
|
+
const followup = spawn(sp2.command, sp2.args, {
|
|
388
392
|
cwd,
|
|
389
393
|
env: spawnEnv,
|
|
390
394
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -456,15 +456,10 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
456
456
|
let stdout = '';
|
|
457
457
|
let stderr = '';
|
|
458
458
|
// codex exec resume <SESSION_ID> - → reads prompt from stdin
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
'--dangerously-bypass-approvals-and-sandbox',
|
|
464
|
-
'resume',
|
|
465
|
-
codexThreadId,
|
|
466
|
-
'-',
|
|
467
|
-
], {
|
|
459
|
+
// Route follow-ups through the mock seam too — under CODEHERE_MOCK=1 this
|
|
460
|
+
// hits the mock REPL, not real codex. (Was a real-spend leak.)
|
|
461
|
+
const sp2 = resolveAgentSpawn('codex', 'codex', ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', 'resume', codexThreadId, '-'], text);
|
|
462
|
+
const followup = spawn(sp2.command, sp2.args, {
|
|
468
463
|
cwd,
|
|
469
464
|
env: spawnEnv,
|
|
470
465
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -53,9 +53,15 @@ const ROUTING_RULES = [
|
|
|
53
53
|
reason: 'targeted fixes favor Aider',
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// Narrowed 2026-06-02 (root-cause fix for "randomly uses opencode"): bare
|
|
57
|
+
// 'generate'/'set up' are far too common ("generate a report", "set up the
|
|
58
|
+
// config") and were silently routing ordinary tasks to opencode. AND
|
|
59
|
+
// claude-code now leads — opencode has NO mid-stream visibility (buffered
|
|
60
|
+
// output), so it must never be the surprise default for a "watch it work"
|
|
61
|
+
// product; it stays available via the explicit agent picker.
|
|
62
|
+
triggers: ['scaffold', 'bootstrap', 'create a new project', 'greenfield', 'new project from scratch'],
|
|
63
|
+
preferred: ['claude-code', 'codex', 'opencode'],
|
|
64
|
+
reason: 'greenfield scaffolding — claude-code default (visible); opencode opt-in only',
|
|
59
65
|
},
|
|
60
66
|
{
|
|
61
67
|
triggers: ['add tests', 'write tests', 'test coverage', 'unit test', 'integration test'],
|
|
@@ -185,24 +185,29 @@ export function loadRecentContextPack(cwd) {
|
|
|
185
185
|
* Returns the original prompt if graphify-ts fails or is unavailable.
|
|
186
186
|
*
|
|
187
187
|
* Respects CODEHERE_GRAPHIFY env var:
|
|
188
|
-
* - unset or "auto" (default):
|
|
189
|
-
*
|
|
190
|
-
*
|
|
188
|
+
* - unset or "auto" (default): enrich ONLY from a pre-existing graph/pack;
|
|
189
|
+
* NEVER compile on the spawn hot path (root-cause fix 2026-06-02 for
|
|
190
|
+
* "every task calls graphify" — `npx graphify-ts pack` was shelling out
|
|
191
|
+
* synchronously on every spawn, slowing crucial tasks + prepending a
|
|
192
|
+
* verbose context blob the user never asked for).
|
|
193
|
+
* - "force": compile a fresh pack on spawn (explicit opt-in for the cost).
|
|
194
|
+
* - "off": skip entirely.
|
|
191
195
|
*/
|
|
192
196
|
export function enrichPromptWithPackSync(prompt, cwd, { force = false } = {}) {
|
|
193
197
|
const mode = process.env.CODEHERE_GRAPHIFY ?? 'auto';
|
|
194
198
|
if (mode === 'off')
|
|
195
199
|
return prompt;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Compile a fresh pack
|
|
200
|
+
const forceCompile = force || mode === 'force';
|
|
201
|
+
// 'auto' must NEVER touch the user's prompt — not by compiling (npx subprocess
|
|
202
|
+
// on the hot path) AND not by prepending an existing pack's "[graphify context:
|
|
203
|
+
// …]" reference blob. That blob leaked into the visible prompt, the Final
|
|
204
|
+
// Report summary, and the conversation ("On it — [graphify context: 1017
|
|
205
|
+
// nodes…]") — user-reported 2026-06-02 with a graph present. Structural
|
|
206
|
+
// context enrichment is now 'force'-only opt-in; a normal task gets a clean prompt.
|
|
207
|
+
if (!forceCompile)
|
|
208
|
+
return prompt;
|
|
209
|
+
// Compile a fresh pack (force only)
|
|
210
|
+
// Compile a fresh pack (force only)
|
|
206
211
|
const pack = compileContextPackSync(prompt, cwd, { task: 'explain', budget: 2500 });
|
|
207
212
|
if (!pack) {
|
|
208
213
|
return prompt;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* AGENTS.md / session.md). Same byte-equality-based detection pattern as
|
|
30
30
|
* memory-command.ts (per feedback_implementation_patterns_v05.md Pattern #1).
|
|
31
31
|
*/
|
|
32
|
-
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
32
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'fs';
|
|
33
33
|
import { join, dirname, resolve } from 'path';
|
|
34
34
|
const DEFAULT_MAX_TOKENS = 3000;
|
|
35
35
|
const DEFAULT_MAX_LESSONS = 10;
|
|
@@ -329,4 +329,49 @@ export function applyMemoryPrefix(prompt, cwd, options) {
|
|
|
329
329
|
record: { included: r.included, tokens: r.tokens, truncated: r.truncated },
|
|
330
330
|
};
|
|
331
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Write-back side of the shared-room hand-off: append a finished turn's outcome
|
|
334
|
+
* to the project's `working/session.md` so the NEXT agent spawned in this
|
|
335
|
+
* project reads it via buildMemoryPrefix() — closing the loop ("agents take
|
|
336
|
+
* turns in a shared room, each building on the last").
|
|
337
|
+
*
|
|
338
|
+
* Resolves the SAME project root as the read path (findProjectRoot), so it only
|
|
339
|
+
* writes when the project is codehere-initialized — it never creates `.codehere/`
|
|
340
|
+
* in an uninitialized repo. The scratch is capped (the injector also budgets it).
|
|
341
|
+
* Best-effort: a memory-write failure must never break a run.
|
|
342
|
+
*/
|
|
343
|
+
export function recordTurnToWorkingMemory(cwd, entry) {
|
|
344
|
+
try {
|
|
345
|
+
const projectRoot = findProjectRoot(cwd);
|
|
346
|
+
if (projectRoot === null)
|
|
347
|
+
return; // not a codehere-initialized project
|
|
348
|
+
const dir = join(projectRoot, '.codehere', 'memory', 'working');
|
|
349
|
+
if (!existsSync(dir))
|
|
350
|
+
return;
|
|
351
|
+
const p = join(dir, 'session.md');
|
|
352
|
+
const HEADER = '# Working session log - codehere auto-records each agent turn so the next one picks up here\n';
|
|
353
|
+
const agent = (entry.agent || 'agent').replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
354
|
+
const prompt = (entry.prompt || '').replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
355
|
+
const summary = (entry.summary || '').replace(/\s+/g, ' ').trim().slice(0, 600);
|
|
356
|
+
const files = entry.files ? ` - ${entry.files} file${entry.files === 1 ? '' : 's'} changed` : '';
|
|
357
|
+
const block = `\n## ${agent}${prompt ? ' - ' + prompt : ''}${files}\n${summary || '(no written summary)'}\n`;
|
|
358
|
+
let cur = '';
|
|
359
|
+
try {
|
|
360
|
+
cur = existsSync(p) ? readFileSync(p, 'utf8') : '';
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
cur = '';
|
|
364
|
+
}
|
|
365
|
+
if (!cur.trim() || looksLikeInitSeed(cur, 'session'))
|
|
366
|
+
cur = HEADER;
|
|
367
|
+
cur += block;
|
|
368
|
+
const CAP = 8000;
|
|
369
|
+
if (cur.length > CAP)
|
|
370
|
+
cur = HEADER + cur.slice(cur.length - (CAP - HEADER.length));
|
|
371
|
+
writeFileSync(p, cur, 'utf8');
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
/* never break a run on a memory-write failure */
|
|
375
|
+
}
|
|
376
|
+
}
|
|
332
377
|
//# sourceMappingURL=memory-injector.js.map
|
|
@@ -15,33 +15,30 @@
|
|
|
15
15
|
* runs end-to-end. For replaying REAL captured runs, that's the cassette system.
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
-
import { join
|
|
18
|
+
import { join } from 'path';
|
|
19
19
|
function argVal(flag) {
|
|
20
20
|
const i = process.argv.indexOf(flag);
|
|
21
21
|
return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : undefined;
|
|
22
22
|
}
|
|
23
|
-
/** The file the mock
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
|
|
23
|
+
/** The single dedicated file the mock edits — claimed in the step AND written on
|
|
24
|
+
* disk, so they stay consistent. ALWAYS a mock-only file, NEVER an existing real
|
|
25
|
+
* source file. The prior version derived a path from the prompt and appended to
|
|
26
|
+
* it; when the daemon's cwd was a real repo and the prompt carried injected
|
|
27
|
+
* memory (which lists repo paths like `src/ui.ts`), it corrupted real tracked
|
|
28
|
+
* source. gitignored so it never rides along. */
|
|
29
|
+
const MOCK_EDIT_FILE = 'codehere-mock-change.md';
|
|
30
|
+
/** CODEHERE_MOCK fidelity: apply a small, safe on-disk edit so the run has a
|
|
31
|
+
* genuine change — WITHOUT touching real source. Writes only to the dedicated
|
|
32
|
+
* MOCK_EDIT_FILE. Note: that file is gitignored, so INSIDE a git repo the
|
|
33
|
+
* artifacts extractor (git ls-files --others --exclude-standard) won't count it
|
|
34
|
+
* — but verify-all runs the mock in a NON-git sandbox where file counts don't
|
|
35
|
+
* apply, and the polished demo uses seeded sessions, not live mock edits. */
|
|
36
|
+
function materializeEdit() {
|
|
33
37
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const marker = `\n// codehere(mock): change applied for "${prompt.slice(0, 60).replace(/"/g, "'")}"\n`;
|
|
39
|
-
if (existsSync(target)) {
|
|
40
|
-
writeFileSync(target, readFileSync(target, 'utf-8') + marker);
|
|
41
|
-
}
|
|
42
|
-
else if (!file.includes('/')) {
|
|
43
|
-
writeFileSync(target, marker.trimStart()); // top-level only — never mkdir
|
|
44
|
-
}
|
|
38
|
+
const target = join(process.cwd(), MOCK_EDIT_FILE);
|
|
39
|
+
const header = '# codehere mock scratch — synthetic edits under CODEHERE_MOCK (safe to delete)\n\n';
|
|
40
|
+
const marker = `codehere(mock): change applied for "${prompt.slice(0, 60).replace(/"/g, "'")}"\n`;
|
|
41
|
+
writeFileSync(target, (existsSync(target) ? readFileSync(target, 'utf-8') : header) + marker);
|
|
45
42
|
}
|
|
46
43
|
catch {
|
|
47
44
|
/* best-effort — mock fidelity must never crash the run */
|
|
@@ -74,7 +71,7 @@ const out = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
|
74
71
|
// for the execution path to produce real steps/artifacts without an API call.
|
|
75
72
|
function plannedSteps(p) {
|
|
76
73
|
const lc = p.toLowerCase();
|
|
77
|
-
const file =
|
|
74
|
+
const file = MOCK_EDIT_FILE;
|
|
78
75
|
const steps = [
|
|
79
76
|
{ tool: 'Read', input: { file_path: 'REPO_MAP.md' }, result: 'context loaded · 84 files indexed' },
|
|
80
77
|
{ tool: 'Edit', input: { file_path: file }, result: '+' + (20 + (p.length % 90)) + ' lines · change applied' },
|
|
@@ -204,7 +201,7 @@ async function main() {
|
|
|
204
201
|
prompt = (await readStdin()) || 'task';
|
|
205
202
|
// Apply the real on-disk edit before emitting the run so the artifacts
|
|
206
203
|
// extractor (which diffs at exit) sees a genuine change.
|
|
207
|
-
materializeEdit(
|
|
204
|
+
materializeEdit();
|
|
208
205
|
switch (agent) {
|
|
209
206
|
case 'claude-code':
|
|
210
207
|
case 'claude':
|
|
@@ -20,6 +20,7 @@ import { OpenCodeAdapter } from './opencode-adapter.js';
|
|
|
20
20
|
import { GeminiAdapter } from './gemini-adapter.js';
|
|
21
21
|
import { buildGenericAdapter, loadAgentsConfig } from './generic-cli-adapter.js';
|
|
22
22
|
import { buildConductorAdapter } from './conductor-adapter.js';
|
|
23
|
+
import { recordTurnToWorkingMemory } from './memory-injector.js';
|
|
23
24
|
import { seedAgentsConfig } from './agents-config-seed.js';
|
|
24
25
|
import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
|
|
25
26
|
import { appendSpawned, appendCompleted, appendFinalReport, loadRecent, } from './session-persistence.js';
|
|
@@ -308,7 +309,22 @@ export class SessionManager {
|
|
|
308
309
|
summary: report.summary,
|
|
309
310
|
generatedAt: report.generatedAt,
|
|
310
311
|
durationMs: report.durationMs,
|
|
312
|
+
source: report.source,
|
|
311
313
|
});
|
|
314
|
+
// Shared-room hand-off: record this turn's outcome to the project's
|
|
315
|
+
// working memory so the NEXT agent spawned here reads it (closes the
|
|
316
|
+
// loop with applyMemoryPrefix). Best-effort; only initialized projects.
|
|
317
|
+
try {
|
|
318
|
+
recordTurnToWorkingMemory(cwd, {
|
|
319
|
+
agent: session.agent,
|
|
320
|
+
prompt: session.prompt,
|
|
321
|
+
summary: report.summary,
|
|
322
|
+
files: session.artifacts?.files?.length,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
/* memory write must never break the run */
|
|
327
|
+
}
|
|
312
328
|
try {
|
|
313
329
|
printFinalReportToTerminal(reportInput, report);
|
|
314
330
|
}
|
|
@@ -473,6 +489,7 @@ export class SessionManager {
|
|
|
473
489
|
summary: report.summary,
|
|
474
490
|
generatedAt: report.generatedAt,
|
|
475
491
|
durationMs: report.durationMs,
|
|
492
|
+
source: report.source,
|
|
476
493
|
});
|
|
477
494
|
try {
|
|
478
495
|
printFinalReportToTerminal(reportInput, report);
|
|
@@ -669,6 +686,7 @@ export class SessionManager {
|
|
|
669
686
|
summary: report.summary,
|
|
670
687
|
generatedAt: report.generatedAt,
|
|
671
688
|
durationMs: report.durationMs,
|
|
689
|
+
source: report.source,
|
|
672
690
|
});
|
|
673
691
|
try {
|
|
674
692
|
printFinalReportToTerminal(reportInput, report);
|
|
@@ -151,7 +151,7 @@ export function loadSessionById(idOrPrefix) {
|
|
|
151
151
|
steps: c.steps,
|
|
152
152
|
artifacts: c.artifacts,
|
|
153
153
|
...(c.turns ? { turns: c.turns } : {}),
|
|
154
|
-
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
|
|
154
|
+
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs, source: fr.source } } : {}),
|
|
155
155
|
...(failoverFrom ? { failoverFrom } : {}),
|
|
156
156
|
...(failoverTo ? { failoverTo } : {}),
|
|
157
157
|
...(failoverReason ? { failoverReason } : {}),
|
|
@@ -222,7 +222,7 @@ export function loadRecent(limit = 20) {
|
|
|
222
222
|
steps: c.steps,
|
|
223
223
|
artifacts: c.artifacts,
|
|
224
224
|
...(c.turns ? { turns: c.turns } : {}),
|
|
225
|
-
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs } } : {}),
|
|
225
|
+
...(fr ? { finalReport: { summary: fr.summary, generatedAt: fr.generatedAt, durationMs: fr.durationMs, source: fr.source } } : {}),
|
|
226
226
|
...((c.failoverFrom ?? s.failoverFrom) ? { failoverFrom: c.failoverFrom ?? s.failoverFrom } : {}),
|
|
227
227
|
...((c.failoverTo ?? s.failoverTo) ? { failoverTo: c.failoverTo ?? s.failoverTo } : {}),
|
|
228
228
|
...((c.failoverReason ?? s.failoverReason) ? { failoverReason: c.failoverReason ?? s.failoverReason } : {}),
|
|
@@ -444,7 +444,9 @@ export function startMCPServer(options = {}) {
|
|
|
444
444
|
// Health check
|
|
445
445
|
if (req.method === 'GET' && req.url === '/health') {
|
|
446
446
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
447
|
-
|
|
447
|
+
// `mock` lets a no-spend tool (verify-all) confirm a daemon is mock BEFORE
|
|
448
|
+
// attaching — so it can refuse to drive a LIVE daemon while claiming "no spend".
|
|
449
|
+
res.end(JSON.stringify({ status: 'ok', server: 'codehere-mcp', version: getVersion(), tools: TOOLS.length, mock: process.env.CODEHERE_MOCK === '1' }));
|
|
448
450
|
return;
|
|
449
451
|
}
|
|
450
452
|
// v2 Task-Centric PM UI (primary — served at /)
|
|
@@ -665,11 +665,11 @@ const SHARED_STYLES = `
|
|
|
665
665
|
/* auditor persona · audit-chain + verifier + hook-badges amplified,
|
|
666
666
|
summary muted (per demo v12 line 2462: "amplify audit-lines +
|
|
667
667
|
hook-badges + trust-grades") */
|
|
668
|
-
body
|
|
669
|
-
body
|
|
670
|
-
body
|
|
671
|
-
body
|
|
672
|
-
body
|
|
668
|
+
body[data-persona="auditor"] .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; color: var(--fg-70); font-size: 11.5px; padding: 8px 14px; }
|
|
669
|
+
body[data-persona="auditor"] .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
|
|
670
|
+
body[data-persona="auditor"] .hook-badge { transform: scale(1.05); }
|
|
671
|
+
body[data-persona="auditor"] .file-chip .trust { font-weight: 700; padding: 2px 7px; }
|
|
672
|
+
body[data-persona="auditor"] .panel:has(p) { opacity: 0.7; }
|
|
673
673
|
|
|
674
674
|
/* SHAPE: velocity · hide tool-call cards, summary prose, tokens, verifier · keep outcome + files */
|
|
675
675
|
body.shape-velocity .tc-card,
|
|
@@ -683,6 +683,13 @@ const SHARED_STYLES = `
|
|
|
683
683
|
body.shape-security .verifier-panel { box-shadow: 0 0 0 2px rgba(255,95,87,0.18); padding: 14px; }
|
|
684
684
|
body.shape-security .hook-block-card { box-shadow: 0 0 0 2px rgba(255,95,87,0.3); }
|
|
685
685
|
|
|
686
|
+
/* Reshape shapes recompose the goal room too (slice 4): "just what shipped"
|
|
687
|
+
* / velocity hide per-turn meta; the audit/security shape amplifies the
|
|
688
|
+
* room's audit line. Reuses the existing body.shape-* + .p-eng/.audit-line. */
|
|
689
|
+
body.shape-ship-only #task-list .card-row2 .p-eng,
|
|
690
|
+
body.shape-velocity #task-list .card-row2 .p-eng { display: none !important; }
|
|
691
|
+
body.shape-security #task-list .audit-line { background: rgba(74,144,226,0.06); border-left-color: #4a90e2; font-size: 11.5px; padding: 8px 14px; }
|
|
692
|
+
|
|
686
693
|
/* v0.8 S2.5 · Settings drawer · ported from demo v12 lines 178-192 + A-09
|
|
687
694
|
* in the backlog. Right-side off-canvas panel · holds Context Sources +
|
|
688
695
|
* future section-visibility toggles. */
|
|
@@ -936,6 +943,29 @@ const SHARED_STYLES = `
|
|
|
936
943
|
box-shadow: 0 0 0 3px rgba(255,154,118,0.18);
|
|
937
944
|
}
|
|
938
945
|
|
|
946
|
+
/* Goal-first home · each project (goal) is a group; its tasks live inside.
|
|
947
|
+
* The home is organized by what you're working toward, not by status. */
|
|
948
|
+
.goal-group { margin: 0 0 22px; }
|
|
949
|
+
.goal-head {
|
|
950
|
+
display: flex; align-items: baseline; gap: 10px;
|
|
951
|
+
padding: 0 0 8px; margin: 20px 0 10px;
|
|
952
|
+
border-bottom: 1px solid var(--border);
|
|
953
|
+
}
|
|
954
|
+
.goal-group:first-child .goal-head { margin-top: 0; }
|
|
955
|
+
.goal-title { font-size: 15px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
956
|
+
.goal-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-40); margin-left: auto; }
|
|
957
|
+
.goal-head { cursor: pointer; }
|
|
958
|
+
.goal-head:hover .goal-title { color: var(--accent); }
|
|
959
|
+
.goal-head:hover .goal-meta { color: var(--fg-70); }
|
|
960
|
+
|
|
961
|
+
/* The goal room · a single goal opened to its multi-agent turns, in order. */
|
|
962
|
+
.room-head { margin: 4px 0 14px; }
|
|
963
|
+
.room-back { background: transparent; border: 0; color: var(--fg-50); font-family: var(--mono); font-size: 11px; cursor: pointer; padding: 0; margin-bottom: 12px; }
|
|
964
|
+
.room-back:hover { color: var(--fg); }
|
|
965
|
+
.room-title { font-size: 20px; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
|
966
|
+
.room-sub { font-family: var(--mono); font-size: 11px; color: var(--fg-40); margin-top: 5px; }
|
|
967
|
+
.turns-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; color: var(--fg-40); text-transform: uppercase; margin: 18px 0 12px; }
|
|
968
|
+
|
|
939
969
|
/* Task card · glass surface · matches demo v12 .glass-card pattern.
|
|
940
970
|
* Faint white-on-dark fill + 1px low-contrast border · sits on top of
|
|
941
971
|
* the ambient dot-grid. Hover lifts 2px with a bigger shadow + inner
|
|
@@ -1536,7 +1566,7 @@ const SHARED_STYLES = `
|
|
|
1536
1566
|
`;
|
|
1537
1567
|
const APP_SCRIPT = `
|
|
1538
1568
|
const _cached = {};
|
|
1539
|
-
let _filter = 'all', _detail = null, _offline = false;
|
|
1569
|
+
let _filter = 'all', _detail = null, _room = null, _offline = false, _autoOpened = false;
|
|
1540
1570
|
const ICONS = { queued:'\\u23F3', running:'\\u25CF', paused:'\\u23F8', blocked:'\\uD83D\\uDD12', done:'\\u2713', failed:'\\u2715', crashed:'!', killed:'\\u2298' };
|
|
1541
1571
|
const STAT_ORDER = ['running','queued','paused','blocked','done','failed','crashed','killed'];
|
|
1542
1572
|
const FILTERS = { all: s=>true, running: s=>['running','queued','paused','blocked'].includes(s.status), done: s=>['done','completed'].includes(s.status), failed: s=>['failed','crashed','killed','error'].includes(s.status) };
|
|
@@ -1668,12 +1698,14 @@ function codeCard(path, snippet, lang) {
|
|
|
1668
1698
|
}
|
|
1669
1699
|
function allS(d) { const sp=d.spawned||d.sessions||[], pe=d.persisted||[], seen=new Set(sp.map(s=>s.sessionId)); return [...sp,...pe.filter(p=>!seen.has(p.sessionId))]; }
|
|
1670
1700
|
function cardActions(s) {
|
|
1701
|
+
// Only 'kill' is wired (POST /api/task/:id/kill). Pause/Resume/Restart had NO
|
|
1702
|
+
// backend endpoint — they rendered as live buttons that silently no-op'd (R11
|
|
1703
|
+
// honest-state lie, 2026-06-03). Removed until real process control lands; Kill
|
|
1704
|
+
// and Cancel both map to the wired kill, so every rendered control here works.
|
|
1671
1705
|
const m={
|
|
1672
|
-
running:'<button class="card-btn
|
|
1673
|
-
paused:'<button class="card-btn
|
|
1706
|
+
running:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
1707
|
+
paused:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
|
|
1674
1708
|
queued:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
1675
|
-
crashed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
|
|
1676
|
-
failed:'<button class="card-btn restart" onclick="event.stopPropagation();a(\\'restart\\',\\''+s+'\\')">Restart</button>',
|
|
1677
1709
|
blocked:'<button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Cancel</button>',
|
|
1678
1710
|
};
|
|
1679
1711
|
return m[mapped(s)]||'';
|
|
@@ -1691,11 +1723,55 @@ function mapped(s) { return s==='queued'?'running':s==='blocked'?'paused':aStat(
|
|
|
1691
1723
|
function findS(id) { const s=allS(_cached.tasks||{}); return s.find(x=>x.sessionId===id)||null; }
|
|
1692
1724
|
|
|
1693
1725
|
function render() {
|
|
1694
|
-
if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; renderL(); } }
|
|
1726
|
+
if(_detail){ const t=findS(_detail); if(t)renderD(t); else{ _detail=null; if(_room)renderRoom(); else renderL(); } }
|
|
1727
|
+
else if(_room) renderRoom();
|
|
1695
1728
|
else renderL();
|
|
1696
1729
|
upCnt(); upBud(); upSq(); upWk(); upIdx();
|
|
1697
1730
|
}
|
|
1698
1731
|
|
|
1732
|
+
// The goal room — one goal opened to its tasks as a chronological thread of
|
|
1733
|
+
// agent turns. Renders into #task-list (same surface as the goals list).
|
|
1734
|
+
function openRoom(enc){
|
|
1735
|
+
_room=decodeURIComponent(enc||''); _detail=null;
|
|
1736
|
+
document.getElementById('task-list-view').style.display='';
|
|
1737
|
+
document.getElementById('task-detail-view').style.display='none';
|
|
1738
|
+
document.body.classList.remove('detail-open');
|
|
1739
|
+
render();
|
|
1740
|
+
}
|
|
1741
|
+
function closeRoom(){ _room=null; render(); }
|
|
1742
|
+
function renderRoom(){
|
|
1743
|
+
const c=document.getElementById('task-list');
|
|
1744
|
+
const _hs=document.getElementById('host-sessions-strip'); if(_hs){ _hs.style.display='none'; }
|
|
1745
|
+
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
1746
|
+
const cwd=_room;
|
|
1747
|
+
const items=allS(_cached.tasks||{}).filter(x=>!/^demo-/.test(x.sessionId||'') && (x.cwd||'')===cwd);
|
|
1748
|
+
items.sort((a,b)=>(a.startedAt||0)-(b.startedAt||0)); // turns in the order they happened
|
|
1749
|
+
const projects=_cached.projects||[];
|
|
1750
|
+
const p=projects.find(x=>x.cwd===cwd);
|
|
1751
|
+
const title=(p&&p.title)||(cwd?(cwd.split('/').filter(Boolean).pop()||'Goal'):'Goal');
|
|
1752
|
+
const agents=[...new Set(items.map(x=>x.agent).filter(Boolean))];
|
|
1753
|
+
let html='<div class="room-head"><button class="room-back" onclick="closeRoom()">\\u2190 all goals</button>';
|
|
1754
|
+
html+='<div class="room-title">'+esc(title)+'</div>';
|
|
1755
|
+
// Persona-adaptive (direction B): everyone sees the plain-English outcome;
|
|
1756
|
+
// the technical path is engineer-only (.p-eng · hidden for PM/designer via
|
|
1757
|
+
// the existing body[data-persona] CSS — recomposes live, no re-render).
|
|
1758
|
+
const _done=items.filter(x=>x.status==='completed'||x.status==='done').length;
|
|
1759
|
+
const _outcome=items.length?(_done===items.length?('\\u2713 all '+items.length+' shipped'):(_done+' of '+items.length+' shipped')):'no tasks yet';
|
|
1760
|
+
html+='<div class="room-sub">'+esc(_outcome)+(agents.length?' \\u00b7 '+agents.map(esc).join(', '):'')+'<span class="p-eng"> \\u00b7 '+esc(cwd||'')+'</span></div></div>';
|
|
1761
|
+
if(items.length){
|
|
1762
|
+
html+='<div class="turns-label">How your agents worked this goal, in order</div>';
|
|
1763
|
+
for(const t of items) html+=cardH(t);
|
|
1764
|
+
// Auditor signal · subtle for everyone, amplified for the Auditor persona
|
|
1765
|
+
// via body[data-persona="auditor"] .audit-line. Honest: real step count,
|
|
1766
|
+
// and every step IS in the local hash-chained audit log.
|
|
1767
|
+
const _stepsTotal=items.reduce((n,t)=>n+((t.steps||[]).length),0);
|
|
1768
|
+
html+='<div class="audit-line allow" style="margin-top:14px">🔒 every step in this goal is in the local audit chain \\u00b7 <b>'+_stepsTotal+' steps</b> \\u00b7 verify \\u21bb</div>';
|
|
1769
|
+
} else {
|
|
1770
|
+
html+='<div class="empty-state"><h2>No tasks in this goal yet.</h2></div>';
|
|
1771
|
+
}
|
|
1772
|
+
c.innerHTML=html;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1699
1775
|
// C-01 · first-run indexing banner · auto-show when session count < 3 +
|
|
1700
1776
|
// only once per browser session (sessionStorage tracks the dismissal).
|
|
1701
1777
|
function upIdx(){
|
|
@@ -1773,6 +1849,7 @@ function fmtGap(ms){
|
|
|
1773
1849
|
|
|
1774
1850
|
function renderL() {
|
|
1775
1851
|
const c=document.getElementById('task-list'), all=allS(_cached.tasks||{});
|
|
1852
|
+
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));
|
|
1776
1853
|
const fl=all.filter(FILTERS[_filter]||FILTERS.all);
|
|
1777
1854
|
let html='';
|
|
1778
1855
|
// Pin demo-shape sessions (id prefix 'demo-') to the very top — they
|
|
@@ -1783,29 +1860,54 @@ function renderL() {
|
|
|
1783
1860
|
html+='<div class="section-label featured">Featured</div>';
|
|
1784
1861
|
for(const t of demo) html+=cardH(t);
|
|
1785
1862
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1863
|
+
// Goal-first home: group the rest by project (the goal/room), not by status.
|
|
1864
|
+
// cwd → project title (from /api/projects); fall back to the folder name.
|
|
1865
|
+
const projects=_cached.projects||[];
|
|
1866
|
+
const titleFor=cwd=>{
|
|
1867
|
+
if(!cwd) return 'Ungrouped';
|
|
1868
|
+
const p=projects.find(x=>x.cwd===cwd);
|
|
1869
|
+
if(p&&p.title) return p.title;
|
|
1870
|
+
const parts=String(cwd).split('/').filter(Boolean);
|
|
1871
|
+
return parts.length?parts[parts.length-1]:'Ungrouped';
|
|
1872
|
+
};
|
|
1873
|
+
const groups=new Map();
|
|
1874
|
+
for(const t of rest){
|
|
1875
|
+
const key=t.cwd||'ungrouped';
|
|
1876
|
+
if(!groups.has(key)) groups.set(key,{cwd:t.cwd,items:[]});
|
|
1877
|
+
groups.get(key).items.push(t);
|
|
1878
|
+
}
|
|
1879
|
+
const lastTs=g=>g.items.reduce((m,x)=>Math.max(m,x.startedAt||0),0);
|
|
1880
|
+
const ordered=[...groups.values()].sort((a,b)=>lastTs(b)-lastTs(a));
|
|
1881
|
+
let prevGroupTs=null;
|
|
1882
|
+
for(const g of ordered){
|
|
1883
|
+
// Time-gap separator BETWEEN goals (ordered by recency) — a big gap means
|
|
1884
|
+
// these goals are from a different stretch of work ("last week").
|
|
1885
|
+
const gts=lastTs(g);
|
|
1886
|
+
if(prevGroupTs!==null && gts>0 && prevGroupTs>0){
|
|
1887
|
+
const gg=Math.abs(prevGroupTs-gts);
|
|
1888
|
+
if(gg>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gg)+'</span><span class="line"></span></div>'; }
|
|
1889
|
+
}
|
|
1890
|
+
prevGroupTs=gts;
|
|
1891
|
+
g.items.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
1892
|
+
const running=g.items.filter(x=>['running','queued','paused','blocked'].includes(x.status)).length;
|
|
1893
|
+
const meta=g.items.length+' task'+(g.items.length===1?'':'s')+(running?' · '+running+' running':'');
|
|
1894
|
+
html+='<div class="goal-group"><div class="goal-head" onclick="openRoom(\\''+encodeURIComponent(g.cwd||'')+'\\')"><span class="goal-title">'+esc(titleFor(g.cwd))+'</span><span class="goal-meta">'+esc(meta)+' \\u00b7 open \\u2192</span></div>';
|
|
1895
|
+
// Time-gap separator within a goal · a big gap between consecutive tasks
|
|
1896
|
+
// in the same goal is meaningful ("picked this back up 3 days later").
|
|
1791
1897
|
let prevTs=null;
|
|
1792
|
-
for(const t of items){
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
// 'running' (still alive, no gap) and the first card (no predecessor).
|
|
1796
|
-
const ts = t.startedAt || 0;
|
|
1797
|
-
if(g!=='running' && prevTs!==null && ts>0 && prevTs>0){
|
|
1898
|
+
for(const t of g.items){
|
|
1899
|
+
const ts=t.startedAt||0;
|
|
1900
|
+
if(prevTs!==null && ts>0 && prevTs>0){
|
|
1798
1901
|
const gap=Math.abs(prevTs-ts);
|
|
1799
|
-
if(gap
|
|
1800
|
-
html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>';
|
|
1801
|
-
}
|
|
1902
|
+
if(gap>2*3600*1000){ html+='<div class="time-gap"><span class="line"></span><span class="label">'+fmtGap(gap)+'</span><span class="line"></span></div>'; }
|
|
1802
1903
|
}
|
|
1803
1904
|
html+=cardH(t);
|
|
1804
1905
|
prevTs=ts;
|
|
1805
1906
|
}
|
|
1907
|
+
html+='</div>';
|
|
1806
1908
|
}
|
|
1807
1909
|
if(!html){
|
|
1808
|
-
const h={all:'No
|
|
1910
|
+
const h={all:'No goals yet. Type a task below to start one.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
|
|
1809
1911
|
html='<div class="empty-state"><h2>'+(h[_filter]||'No tasks found.')+'</h2></div>';
|
|
1810
1912
|
}
|
|
1811
1913
|
c.innerHTML=html;
|
|
@@ -1814,6 +1916,8 @@ function renderL() {
|
|
|
1814
1916
|
function renderHostSessions(data){
|
|
1815
1917
|
const strip=document.getElementById('host-sessions-strip');
|
|
1816
1918
|
if(!strip)return;
|
|
1919
|
+
// The observed-sessions strip belongs to the goals home, not inside a room.
|
|
1920
|
+
if(_room){ strip.style.display='none'; strip.innerHTML=''; return; }
|
|
1817
1921
|
const sessions=(data&&data.sessions)||[];
|
|
1818
1922
|
if(!sessions.length){ strip.style.display='none'; strip.innerHTML=''; return; }
|
|
1819
1923
|
function he(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
@@ -1880,7 +1984,7 @@ function cardH(t) {
|
|
|
1880
1984
|
const stalledHtml = t.stalledReason ? '<div class="card-stalled" title="the watchdog stopped this task because the agent went silent">'+he(t.stalledReason)+'</div>' : '';
|
|
1881
1985
|
return '<div class="task-card '+agentClass+pinClass+'" onclick="showD(\\''+t.sessionId+'\\')">'
|
|
1882
1986
|
+'<div class="card-row1">'+statIcon(s)+'<span class="card-title" title="'+displayPrompt(t.prompt).replace(/"/g,'"').slice(0,400)+'">'+title(t.prompt)+'</span>'+tagHtml(t.tags||[])+agentChip+'</div>'
|
|
1883
|
-
+'<div class="card-row2">'+metaParts.join('')+verB(t.verification)+outcomeHtml+'</div>'
|
|
1987
|
+
+'<div class="card-row2"><span class="p-eng">'+metaParts.join('')+'</span>'+verB(t.verification)+outcomeHtml+'</div>'
|
|
1884
1988
|
+stalledHtml
|
|
1885
1989
|
+(cardActions(t.sessionId)?'<div class="card-row3">'+cardActions(t.sessionId)+'</div>':'')+'</div>';
|
|
1886
1990
|
}
|
|
@@ -2401,6 +2505,12 @@ function renderD(t) {
|
|
|
2401
2505
|
// Empty finalReport object or null/empty summary => honest "unavailable" outcome,
|
|
2402
2506
|
// not a green check. Composer must offer a re-spawn affordance for these cases.
|
|
2403
2507
|
const hasReportText = !!(t.finalReport && typeof t.finalReport.summary === 'string' && t.finalReport.summary.trim());
|
|
2508
|
+
// §G R8 — green ✓ "Final Report" ONLY for a verified live-agent summary
|
|
2509
|
+
// (source === 'agent-summary'). Every other source — deterministic fallback,
|
|
2510
|
+
// error-recovery, mock synthesis — renders an honest ⚠, never a fake ✓.
|
|
2511
|
+
// POSITIVE allowlist (not deny-one) so a new/unknown source can't sneak a ✓.
|
|
2512
|
+
const reportSource = (t.finalReport && t.finalReport.source) || '';
|
|
2513
|
+
const isVerifiedAgentReport = reportSource === 'agent-summary';
|
|
2404
2514
|
const isTerminal = ['completed','done','killed','failed','error','crashed'].includes(t.status);
|
|
2405
2515
|
// The headline "N files changed" number. Normally derived from the actual
|
|
2406
2516
|
// artifact list, so claim === rendered count, always. The optional
|
|
@@ -2415,8 +2525,13 @@ function renderD(t) {
|
|
|
2415
2525
|
} else {
|
|
2416
2526
|
oHtml+=(claimedFiles?'<div class="outcome-item"><span class="check">'+ms(true)+'</span> '+claimedFiles+' file'+(claimedFiles===1?'':'s')+' changed</div>':'');
|
|
2417
2527
|
oHtml+='<div class="outcome-item"><span class="'+(nStep>0?'check':'pending')+'">'+(nStep>0?ms(true):'\\u2022')+'</span> '+nStep+' step'+(nStep===1?'':'s')+'</div>';
|
|
2418
|
-
if(hasReportText) {
|
|
2528
|
+
if(hasReportText && isVerifiedAgentReport) {
|
|
2419
2529
|
oHtml+='<div class="outcome-item"><span class="check">'+ms(true)+'</span> Final Report</div>';
|
|
2530
|
+
} else if(hasReportText) {
|
|
2531
|
+
const _rl = reportSource==='deterministic' ? 'No written report \\u2014 recorded facts only'
|
|
2532
|
+
: reportSource==='error-recovery' ? 'Final Report \\u2014 task did not complete'
|
|
2533
|
+
: 'Final Report \\u2014 unverified (not a live-agent summary)';
|
|
2534
|
+
oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> '+_rl+'</div>';
|
|
2420
2535
|
} else if(isTerminal) {
|
|
2421
2536
|
oHtml+='<div class="outcome-item"><span class="pending">\\u26A0</span> Final Report unavailable</div>';
|
|
2422
2537
|
}
|
|
@@ -3190,9 +3305,25 @@ function al(m){const e=document.getElementById('toast');if(e){e.textContent=m;e.
|
|
|
3190
3305
|
async function fetchAll() {
|
|
3191
3306
|
const sk=document.getElementById('skeleton');if(sk){const p=sk.parentNode;if(p)p.removeChild(sk);}
|
|
3192
3307
|
try {
|
|
3193
|
-
const [t,r]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json())]);
|
|
3194
|
-
_cached.tasks=t;_cached.readiness=r;
|
|
3308
|
+
const [t,r,pj]=await Promise.all([fetch('/api/tasks').then(x=>x.json()),fetch('/api/readiness/agents').then(x=>x.json()),fetch('/api/projects').then(x=>x.json()).catch(()=>({projects:[]}))]);
|
|
3309
|
+
_cached.tasks=t;_cached.readiness=r;_cached.projects=(pj&&pj.projects)||[];
|
|
3195
3310
|
if(t.pricing)_cached.pricing=t.pricing;
|
|
3311
|
+
// Auto-open the most relevant thread on first load (running > most-recent) —
|
|
3312
|
+
// landing on a bare list "did nothing" feel (Ega 2026-06-02). Once only, and
|
|
3313
|
+
// only if the user hasn't already navigated; Back still returns to the list.
|
|
3314
|
+
if(!_autoOpened){
|
|
3315
|
+
const _all=allS(t||{});
|
|
3316
|
+
if(_all.length){
|
|
3317
|
+
_autoOpened=true;
|
|
3318
|
+
if(_detail===null){
|
|
3319
|
+
const _act=_all.filter(x=>['running','queued','paused','blocked'].includes(x.status));
|
|
3320
|
+
const _pool=_act.length?_act:_all.slice();
|
|
3321
|
+
_pool.sort((a,b)=>(b.startedAt||0)-(a.startedAt||0));
|
|
3322
|
+
const _pick=_pool[0];
|
|
3323
|
+
if(_pick&&_pick.sessionId){ showD(_pick.sessionId); }
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3196
3327
|
render();
|
|
3197
3328
|
}catch(e){console.error('fetch error',e);}
|
|
3198
3329
|
try {
|
|
@@ -3946,6 +4077,7 @@ export const TASK_HTML_V2 = `<!doctype html>
|
|
|
3946
4077
|
<option value="pm">PM view</option>
|
|
3947
4078
|
<option value="designer">Designer view</option>
|
|
3948
4079
|
<option value="eng">Engineer view</option>
|
|
4080
|
+
<option value="auditor">Auditor view</option>
|
|
3949
4081
|
</select>
|
|
3950
4082
|
<button class="density-toggle" id="density-toggle" title="Toggle compact / standard density">⊞ Compact</button>
|
|
3951
4083
|
<button class="reshape-btn" id="reshape-open" title="Describe the view you want · agent composes from primitives">⌘ Reshape view</button>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
+
"prepare": "bash ../scripts/install-hooks.sh || true",
|
|
20
21
|
"prebuild": "rm -rf dist",
|
|
21
22
|
"build": "tsc",
|
|
22
23
|
"postbuild": "chmod +x dist/index.js",
|