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.
@@ -1,107 +1,84 @@
1
1
  /**
2
- * Final Report — PM-UX Layer 3 · MVP (v0.5.0-alpha.3+).
2
+ * Final Report — the headline deliverable. At task exit, produce a plain-English
3
+ * summary of what the agent did.
3
4
  *
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.
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
- * 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).
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
- * 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.
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
- import { spawn } from 'child_process';
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 that indicate the wrapped agent's session terminated
90
- * because of a network / auth / quota failure rather than a real task.
91
- * When we see these in the would-be summary text, we prepend a plain-English
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. Returns null if no provider is reachable.
119
- * Never throws — failure modes return null + log a debug line.
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
- // CODEHERE_MOCK — synthesize a grounded report with zero API so the full
128
- // demo flow (incl. this headline deliverable + the verify cross-check) runs
129
- // under mock. Errors still get the deterministic recovery report below.
130
- if (isMockMode() && input.status !== 'error') {
131
- return mockFinalReport(input);
132
- }
133
- // Short-circuit on errored sessions — don't waste tokens summarizing nothing.
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
- const prompt = PROMPT_TEMPLATE(input);
138
- for (const binary of selectProviderBinary()) {
139
- const result = await tryProvider(binary, prompt, input.sessionId);
140
- if (result) {
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
- return null;
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
- const followup = spawn('aider', ['--restore-chat-history', '--message', text, ...HEADLESS_FLAGS], {
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
- const followup = spawn('claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], {
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
- const followup = spawn('codex', [
460
- 'exec',
461
- '--json',
462
- '--skip-git-repo-check',
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
- triggers: ['scaffold', 'generate', 'create new', 'bootstrap', 'set up'],
57
- preferred: ['opencode', 'claude-code', 'codex'],
58
- reason: 'greenfield generation favors OpenCode',
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): compile on spawn if graph doesn't exist
189
- * - "force": always compile, even if pack exists
190
- * - "off": skip entirely
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
- // Check for existing pack to avoid recompiling on every spawn
197
- const existingPack = loadRecentContextPack(cwd);
198
- const graphPath = getGraphPath(cwd);
199
- const graphExists = existsSync(graphPath);
200
- if (!force && existingPack && graphExists) {
201
- // Reuse existing pack — just enrich the prompt with a compact reference
202
- const reference = `[graphify-ts context pack: ${existingPack.nodeCount} nodes, ${existingPack.edgeCount} edges, pack=${existingPack.packId}]`;
203
- return `[graphify context: ${reference} — agent should call retrieve() for full pack]\n\n${prompt}`;
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, isAbsolute, basename } from 'path';
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 claims to edit — derived from the prompt, used for both
24
- * the emitted step AND the real on-disk edit so they stay consistent. */
25
- function targetFile(p) {
26
- return /(\b[\w./-]+\.(ts|tsx|js|py|go|rs|md))\b/.exec(p)?.[1] || 'src/feature.ts';
27
- }
28
- /** CODEHERE_MOCK fidelity: actually apply a small, safe edit so the REAL
29
- * artifacts pipeline (git diff → trust score → Final Report → verify) has a
30
- * genuine change to report. The demo's "1 file changed · trust" stays honest —
31
- * it is a real diff on disk, just authored by the mock, not a model. */
32
- function materializeEdit(file) {
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 cwd = process.cwd();
35
- let target = isAbsolute(file) ? file : join(cwd, file);
36
- if (!target.startsWith(cwd))
37
- target = join(cwd, basename(file)); // no traversal
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 = targetFile(p);
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(targetFile(prompt));
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
- res.end(JSON.stringify({ status: 'ok', server: 'codehere-mcp', version: getVersion(), tools: TOOLS.length }));
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.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.auditor .verifier-panel { box-shadow: 0 0 0 2px rgba(40,200,64,0.18); }
670
- body.auditor .hook-badge { transform: scale(1.05); }
671
- body.auditor .file-chip .trust { font-weight: 700; padding: 2px 7px; }
672
- body.auditor .panel:has(p) { opacity: 0.7; }
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 pause" onclick="event.stopPropagation();a(\\'pause\\',\\''+s+'\\')">Pause</button><button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
1673
- paused:'<button class="card-btn" onclick="event.stopPropagation();a(\\'resume\\',\\''+s+'\\')">Resume</button><button class="card-btn kill" onclick="event.stopPropagation();a(\\'kill\\',\\''+s+'\\')">Kill</button>',
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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">&#128274; 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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
- for(const g of STAT_ORDER){
1787
- const items=rest.filter(x=>aStat(x.status)===g||(g==='running'&&['running','queued'].includes(x.status))||(g==='failed'&&['crashed','killed','error'].includes(x.status)));
1788
- if(!items.length)continue;
1789
- const label={running:'Running',queued:'Running',paused:'Paused',blocked:'Blocked',done:'Done',failed:'Failed',crashed:'Failed',killed:'Failed'}[g]||g;
1790
- html+='<div class="section-label">'+label+'</div>';
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
- // F-04 · time-gap separator · inject before this card when there's a
1794
- // > 2h gap between its startedAt and the previous card's. Skips for
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 > 2*3600*1000){
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 tasks yet. Type a task below to get started.',running:'No running tasks.',done:'No completed tasks.',failed:'No failed tasks.'};
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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,'&quot;').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">&#8862; 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.0",
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",