codehere 0.4.1 → 0.5.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -7
- package/dist/application/use-cases/generate-final-report.js +144 -0
- package/dist/index.js +47 -4
- package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
- package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +70 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
- package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
- package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
- package/dist/infrastructure/orchestration/memory-injector.js +337 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
- package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
- package/dist/infrastructure/orchestration/readiness.js +2 -5
- package/dist/infrastructure/orchestration/session-manager.js +65 -2
- package/dist/infrastructure/orchestration/session-persistence.js +13 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +86 -14
- package/dist/infrastructure/protocols/ui-page.js +665 -67
- package/dist/presentation/cli/commands/init-command.js +345 -24
- package/dist/presentation/cli/commands/memory-command.js +1017 -0
- package/dist/presentation/cli/commands/protocols-builder.js +254 -0
- package/dist/presentation/cli/commands/protocols-command.js +84 -0
- package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/codehere)
|
|
6
6
|
[](../LICENSE)
|
|
7
|
-
[]()
|
|
8
8
|
[]()
|
|
9
9
|
|
|
10
10
|
codehere is **a PM for your AI coding agents.** Delegate a task. codehere picks the right specialist (Claude Code, Codex, Aider, OpenCode, or Gemini), spawns it, streams its work back to you, and hands you a plain-English Final Report with a **trust score on every file changed**. Today, Claude Code gets full pre-execution safety-hook enforcement on every tool call; other adapters are session-traced and reported, with per-tool hook parity planned for v0.4.
|
|
@@ -26,8 +26,9 @@ One manager, many specialists, every action recorded.
|
|
|
26
26
|
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
27
27
|
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
28
28
|
| 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
|
|
29
|
-
| 5 | **Scaffold this repo** | `cd /your/repo && codehere init` — creates `.codehere/
|
|
30
|
-
| 6 | **Fill in
|
|
29
|
+
| 5 | **Scaffold this repo** | `cd /your/repo && codehere init` — creates `.codehere/` with the F3 memory tree (working / episodic / semantic / personal / candidates) plus `AGENTS.md` (project briefing) and `skills/` (prompt templates) |
|
|
30
|
+
| 6 | **Fill in AGENTS.md** | 5 minutes. The agent will follow what's written there. |
|
|
31
|
+
| 7 | **Build the symbol map (optional)** | `codehere memory repo-map` — JSON map of every top-level export in your repo, ranked by reference count. Spawned agents read it on entry instead of cold-grepping. |
|
|
31
32
|
|
|
32
33
|
You're done. `codehere start` opens the web UI at `:3773`; you delegate from there.
|
|
33
34
|
|
|
@@ -43,11 +44,20 @@ Context + End State + Constraints
|
|
|
43
44
|
|
|
44
45
|
### Folder convention (what `codehere init` writes)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
The v0.5 layout — F3 project memory:
|
|
48
|
+
|
|
49
|
+
| Path | Purpose | Committed? |
|
|
47
50
|
|---|---|---|
|
|
48
|
-
| `.codehere/CONTEXT.md` | What this repo is. Conventions. Style. Constraints. | yes |
|
|
49
|
-
| `.codehere/HANDOVER.md` |
|
|
50
|
-
| `.codehere/
|
|
51
|
+
| `.codehere/AGENTS.md` *(or legacy `CONTEXT.md`)* | What this repo is. Conventions. Style. Constraints. | yes |
|
|
52
|
+
| `.codehere/memory/working/session.md` *(or legacy `HANDOVER.md`)* | What's in flight in the current session. | **no** |
|
|
53
|
+
| `.codehere/memory/episodic/AGENT_LEARNINGS.jsonl` | Symlink to your full session history at `~/.codehere/sessions.jsonl`. | (symlink) |
|
|
54
|
+
| `.codehere/memory/episodic/repo_map.json` | Ranked symbol map. Built by `codehere memory repo-map`. | yes |
|
|
55
|
+
| `.codehere/memory/semantic/lessons.jsonl` | Project-specific lessons promoted from episodic patterns. | yes |
|
|
56
|
+
| `.codehere/memory/personal/preferences.json` | Per-developer prefs. | **no** |
|
|
57
|
+
| `.codehere/skills/<name>/SKILL.md` *(or legacy flat `PROMPTS.md`)* | Prompt templates — one folder per skill. | yes |
|
|
58
|
+
| `.codehere/protocols/permissions.md` + `hook_patterns.json` | Safety policy for the PreToolUse hook. | yes |
|
|
59
|
+
|
|
60
|
+
Upgrading from v0.4.1? Run `codehere memory init` once — it migrates `CONTEXT/HANDOVER/PROMPTS` into the new layout and leaves symlinks at the old paths so back-compat holds. `--dry-run` shows what would move first.
|
|
51
61
|
|
|
52
62
|
---
|
|
53
63
|
|
|
@@ -75,6 +85,52 @@ codehere status # quick dashboard · sessions, blocks, trust trends
|
|
|
75
85
|
|
|
76
86
|
The hook intercepts every `Bash`, `Edit`, `Write`, `MultiEdit`, and `NotebookEdit` call from any Claude Code session running on your machine, runs it through the trust-scorer pattern bank in under 5 ms, blocks anything matching the destructive / credential / injection patterns, and appends every decision to an append-only JSONL audit log at `~/.codehere/audit/`. (Hook coverage today is Claude Code-only — Codex / Aider / OpenCode adapters spawn through their native CLIs and emit to `~/.codehere/sessions.jsonl`; full hook parity across all adapters is v0.4 work.)
|
|
77
87
|
|
|
88
|
+
## Memory · F3 (v0.5)
|
|
89
|
+
|
|
90
|
+
Once you've run `codehere init` in a project, every spawned agent — Claude Code, Codex, Aider, OpenCode, or Gemini — automatically reads the project's memory on entry. The agent starts warm with the briefing, recent lessons, and the most-referenced symbols in your codebase, instead of cold-grepping every session.
|
|
91
|
+
|
|
92
|
+
**What gets injected, in priority order:**
|
|
93
|
+
1. **`.codehere/AGENTS.md`** — your project briefing (conventions, style, constraints)
|
|
94
|
+
2. **`.codehere/memory/working/session.md`** — what's in flight right now (per-machine, gitignored)
|
|
95
|
+
3. **`.codehere/memory/semantic/lessons.jsonl`** — graduated project-wide lessons
|
|
96
|
+
4. **`.codehere/memory/episodic/repo_map.json`** — top-N most-referenced symbols (run `codehere memory repo-map` to build / refresh)
|
|
97
|
+
|
|
98
|
+
The injection is token-budget-aware (default ~3000 tokens, drops lower-priority sections first to fit; AGENTS.md is never dropped). Pass `--no-memory-prefix` if you want the bare prompt for a one-off task.
|
|
99
|
+
|
|
100
|
+
**Inspect what would inject** before spawning anything:
|
|
101
|
+
```bash
|
|
102
|
+
codehere memory show injection
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Add a lesson** so future spawns learn from it:
|
|
106
|
+
```bash
|
|
107
|
+
codehere memory graduate --add "auth tests live alongside source files"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**See everything codehere has accumulated:**
|
|
111
|
+
```bash
|
|
112
|
+
codehere memory show # one-line summary per layer
|
|
113
|
+
codehere memory show semantic # numbered lesson list
|
|
114
|
+
codehere memory show episodic # session count + top symbols
|
|
115
|
+
codehere memory show protocols # safety patterns by category
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Other memory commands:**
|
|
119
|
+
- `codehere memory init` — migrate the v0.4.1 layout (`CONTEXT.md` / `HANDOVER.md` / `PROMPTS.md`) into the F3 layout (one shot, leaves back-compat symlinks; `--dry-run` previews)
|
|
120
|
+
- `codehere memory repo-map [--rebuild]` — build the symbol map; v0.5 ships a TS-native MVP, v0.6 swaps in tree-sitter
|
|
121
|
+
- `codehere protocols export [--rebuild]` — write the trust-scorer pattern bank to `.codehere/protocols/hook_patterns.json` for inspection / cross-tool ingestion
|
|
122
|
+
|
|
123
|
+
## Going deeper
|
|
124
|
+
|
|
125
|
+
For technical readers — the living documents that describe codehere as a system, not just a CLI:
|
|
126
|
+
|
|
127
|
+
| Doc | What it is |
|
|
128
|
+
|---|---|
|
|
129
|
+
| [`docs/decisions/`](../docs/decisions/) | ADR registry — every locked architectural decision (audit-first moat, supervisor-agent positioning, local-first, F3 memory layout, generic-CLI adapter, ...) numbered, dated, with rejected alternatives + rollback triggers |
|
|
130
|
+
| [`docs/THREAT_MODEL.md`](../docs/THREAT_MODEL.md) | Every attack surface, current mitigation, residual risk, planned hardening — npm supply chain, hook bypass, symlink redirect, audit log tampering, MCP server, prompt injection |
|
|
131
|
+
| [`docs/CAPABILITIES.md`](../docs/CAPABILITIES.md) | Honest matrix of what each adapter can and cannot do today — Claude Code, Codex, Aider, OpenCode, Gemini, Generic CLI — across spawn, stream, per-tool block, audit, F3 memory, auth |
|
|
132
|
+
| [`agent/REPO_MAP.md`](./REPO_MAP.md) | One-line-per-file index of the 54 source files with their top-level exports — generated, regenerated via `npm run repomap` after renames |
|
|
133
|
+
|
|
78
134
|
## Commands
|
|
79
135
|
|
|
80
136
|
```
|
|
@@ -129,6 +185,7 @@ For depth beyond this README:
|
|
|
129
185
|
- **[CODEHERE.md](../CODEHERE.md)** · product spec · positioning, six-ring adoption funnel, F0–F6 foundations, defensibility, GTM, revenue model
|
|
130
186
|
- **[EXPERIENCE.md](../EXPERIENCE.md)** · the human story · how engineering teams, CISOs, and regulated-industry buyers adopt codehere
|
|
131
187
|
- **[AUDIT.md](../AUDIT.md)** · pre-publish audit report · methodology, findings, remediation
|
|
188
|
+
- **[docs/SECURITY_AUDIT_2026-05-01.md](../docs/SECURITY_AUDIT_2026-05-01.md)** · first-party security audit · OWASP Top 10 + STRIDE · 0 critical / 0 high · 2 medium fixed inline · methodology disclosed · reproducible
|
|
132
189
|
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** · how to contribute · setup, commit discipline, testing philosophy
|
|
133
190
|
- **[SECURITY.md](../SECURITY.md)** · responsible disclosure · `security@codehere.uk`
|
|
134
191
|
- **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** · community standards
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final Report — PM-UX Layer 3 · MVP (v0.5.0-alpha.3+).
|
|
3
|
+
*
|
|
4
|
+
* At task exit, generates a 3-paragraph plain-English summary of what the
|
|
5
|
+
* agent did, what was touched, and what the user should look at. Prints to
|
|
6
|
+
* stdout so the dogfood-while-building loop sees the artifact immediately.
|
|
7
|
+
*
|
|
8
|
+
* Provider strategy (MVP — try free first, fall back to paid):
|
|
9
|
+
* - Tries `claude-free --bare -p "<prompt>"` first. The `claude-free`
|
|
10
|
+
* wrapper is what actually routes through the FCC proxy at :8082
|
|
11
|
+
* (verified 2026-05-02 dogfood: plain `claude --bare` does NOT route
|
|
12
|
+
* through FCC — it hits paid Claude. The May 1 memory rule was wrong.)
|
|
13
|
+
* - Falls back to `claude --bare -p "<prompt>"` if `claude-free` is not
|
|
14
|
+
* installed (paid path; will fail gracefully if no credit).
|
|
15
|
+
* - If neither binary is installed, returns null silently (no report).
|
|
16
|
+
*
|
|
17
|
+
* Env override: set CODEHERE_REPORT_BINARY=<binary-name> to force a
|
|
18
|
+
* specific provider (e.g. for testing or non-default setups).
|
|
19
|
+
*
|
|
20
|
+
* Iterations 5-9 (2026-05-02) refuted the "we own the supervisory layer"
|
|
21
|
+
* thesis. Final Report is the keystone delivery moment per the v3 chain
|
|
22
|
+
* (claim_market_position) — it's the artifact the dogfood loop produces
|
|
23
|
+
* that none of the surveyed competitors (ComposioHQ, Overstory, Praktor,
|
|
24
|
+
* Claude Squad, AI-Agents-Orchestrator) advertise.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
const PROMPT_TEMPLATE = (i) => `You are a PM reporting back to a developer about what their AI coding agent
|
|
28
|
+
just did. Write 3 short paragraphs in plain English. No headers, no bullet
|
|
29
|
+
lists, no markdown — just three paragraphs separated by blank lines.
|
|
30
|
+
|
|
31
|
+
Paragraph 1: What the agent DID. Verb-led. Name files specifically. 1-3 sentences.
|
|
32
|
+
Paragraph 2: What was TOUCHED. Files modified, line counts, anything that looks risky. 1-3 sentences.
|
|
33
|
+
Paragraph 3: What the developer should LOOK AT. Open concerns, things to verify.
|
|
34
|
+
If everything looks clean, say so explicitly. 1-2 sentences.
|
|
35
|
+
|
|
36
|
+
Be specific. Don't pad. Don't editorialize. Don't praise the agent.
|
|
37
|
+
|
|
38
|
+
The task was: ${i.prompt}
|
|
39
|
+
The agent used: ${i.agent}
|
|
40
|
+
Duration: ${i.durationMs}ms
|
|
41
|
+
Status: ${i.status}
|
|
42
|
+
|
|
43
|
+
Steps the agent took:
|
|
44
|
+
${i.steps.length ? i.steps.map((s) => `- ${s.label} (${s.state})`).join('\n') : '(no structured steps captured)'}
|
|
45
|
+
|
|
46
|
+
Files changed:
|
|
47
|
+
${i.artifacts?.files?.length
|
|
48
|
+
? i.artifacts.files.map((f) => `- ${f.path} (+${f.added}/-${f.removed}${f.trust !== undefined ? `, trust: ${f.trust}` : ''})`).join('\n')
|
|
49
|
+
: '(no file artifacts captured)'}
|
|
50
|
+
|
|
51
|
+
Token usage: ${i.artifacts?.usage?.inputTokens ?? 0} in / ${i.artifacts?.usage?.outputTokens ?? 0} out
|
|
52
|
+
|
|
53
|
+
Now write the 3 paragraphs.`;
|
|
54
|
+
const REPORT_TIMEOUT_MS = 30_000;
|
|
55
|
+
/** Provider chain: try free first, then paid. Override with CODEHERE_REPORT_BINARY. */
|
|
56
|
+
function selectProviderBinary() {
|
|
57
|
+
const override = process.env.CODEHERE_REPORT_BINARY;
|
|
58
|
+
if (override)
|
|
59
|
+
return [override];
|
|
60
|
+
return ['claude-free', 'claude'];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate a Final Report. Returns null if no provider is reachable.
|
|
64
|
+
* Never throws — failure modes return null + log a debug line.
|
|
65
|
+
*/
|
|
66
|
+
export async function generateFinalReport(input) {
|
|
67
|
+
const prompt = PROMPT_TEMPLATE(input);
|
|
68
|
+
for (const binary of selectProviderBinary()) {
|
|
69
|
+
const result = await tryProvider(binary, prompt, input.sessionId);
|
|
70
|
+
if (result)
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function tryProvider(binary, prompt, sessionId) {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let stdout = '';
|
|
79
|
+
let stderr = '';
|
|
80
|
+
let settled = false;
|
|
81
|
+
const finish = (result) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
resolve(result);
|
|
86
|
+
};
|
|
87
|
+
const child = spawn(binary, ['--bare', '-p', prompt], {
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
});
|
|
90
|
+
const timeout = setTimeout(() => {
|
|
91
|
+
child.kill('SIGTERM');
|
|
92
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
93
|
+
console.error(`[final-report] ${binary} timeout after ${REPORT_TIMEOUT_MS}ms for ${sessionId}`);
|
|
94
|
+
}
|
|
95
|
+
finish(null);
|
|
96
|
+
}, REPORT_TIMEOUT_MS);
|
|
97
|
+
child.on('error', (err) => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
// ENOENT = binary not installed. Silent fallback to next provider in chain.
|
|
100
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
101
|
+
console.error(`[final-report] ${binary} unavailable: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
finish(null);
|
|
104
|
+
});
|
|
105
|
+
child.stdout?.on('data', (chunk) => {
|
|
106
|
+
stdout += chunk.toString('utf8');
|
|
107
|
+
});
|
|
108
|
+
child.stderr?.on('data', (chunk) => {
|
|
109
|
+
stderr += chunk.toString('utf8');
|
|
110
|
+
});
|
|
111
|
+
child.on('exit', (code) => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
if (code !== 0) {
|
|
114
|
+
if (process.env.CODEHERE_DEBUG === '1') {
|
|
115
|
+
console.error(`[final-report] ${binary} exited ${code}: ${stderr.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
finish(null);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const summary = stdout.trim();
|
|
121
|
+
if (!summary) {
|
|
122
|
+
finish(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
finish({ summary, generatedAt: Date.now(), durationMs: Date.now() - startedAt });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pretty-print to terminal. Used by session-manager when a spawned task exits.
|
|
131
|
+
* Wrapped in try/catch upstream — never let a print failure surface.
|
|
132
|
+
*/
|
|
133
|
+
export function printFinalReportToTerminal(input, output) {
|
|
134
|
+
const banner = '─'.repeat(60);
|
|
135
|
+
process.stdout.write('\n');
|
|
136
|
+
process.stdout.write(`${banner}\n`);
|
|
137
|
+
process.stdout.write(`Final Report · ${input.agent} · ${input.sessionId.slice(0, 8)}\n`);
|
|
138
|
+
process.stdout.write(`${banner}\n`);
|
|
139
|
+
process.stdout.write(`${output.summary}\n`);
|
|
140
|
+
process.stdout.write(`${banner}\n`);
|
|
141
|
+
process.stdout.write(`(generated in ${output.durationMs}ms)\n`);
|
|
142
|
+
process.stdout.write('\n');
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=generate-final-report.js.map
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { join, dirname } from 'path';
|
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
18
18
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
19
|
+
import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
|
|
20
|
+
import { executeProtocolsCommand } from './presentation/cli/commands/protocols-command.js';
|
|
19
21
|
import { executeConfigCommand } from './presentation/cli/commands/config-command.js';
|
|
20
22
|
import { executeHelpCommand } from './presentation/cli/commands/help-command.js';
|
|
21
23
|
import { executeStatusCommand } from './presentation/cli/commands/status-command.js';
|
|
@@ -103,6 +105,47 @@ program
|
|
|
103
105
|
.action((options) => {
|
|
104
106
|
executeInitCommand({ force: options.force, noGitignore: !options.gitignore });
|
|
105
107
|
});
|
|
108
|
+
// ── memory ───────────────────────────────────────────────────────────────────
|
|
109
|
+
// F3 v0.5: project memory operations. `memory init` migrates v0.4.1 layout
|
|
110
|
+
// (CONTEXT/HANDOVER/PROMPTS) into the four-layer F3 layout (AGENTS.md +
|
|
111
|
+
// memory/ + skills/). `memory repo-map` builds a JSON symbol map. Future:
|
|
112
|
+
// memory show, memory graduate.
|
|
113
|
+
program
|
|
114
|
+
.command('memory')
|
|
115
|
+
.description('F3 project memory · init / repo-map / show / graduate / help')
|
|
116
|
+
.argument('[action]', 'Action: init, repo-map, show, graduate, help')
|
|
117
|
+
.argument('[layer]', 'Layer for `memory show` (working / episodic / semantic / personal / skills / protocols / injection)')
|
|
118
|
+
.option('-f, --force', 'Overwrite existing target files (memory init)')
|
|
119
|
+
.option('--no-symlinks', 'Delete old paths instead of leaving back-compat symlinks (memory init)')
|
|
120
|
+
.option('--dry-run', 'Print planned moves without touching disk (memory init)')
|
|
121
|
+
.option('--rebuild', 'Force a fresh build (memory repo-map)')
|
|
122
|
+
.option('--add <text>', 'Add a free-text lesson directly (memory graduate)')
|
|
123
|
+
.option('--accept <id>', 'Graduate a candidate by id (memory graduate)')
|
|
124
|
+
.option('--reject <id>', 'Reject a candidate by id without graduating (memory graduate)')
|
|
125
|
+
.action((action, layer, options) => {
|
|
126
|
+
executeMemoryCommand(action, {
|
|
127
|
+
force: options.force,
|
|
128
|
+
noSymlinks: !options.symlinks,
|
|
129
|
+
dryRun: options.dryRun,
|
|
130
|
+
rebuild: options.rebuild,
|
|
131
|
+
layer,
|
|
132
|
+
add: options.add,
|
|
133
|
+
accept: options.accept,
|
|
134
|
+
reject: options.reject,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ── protocols ────────────────────────────────────────────────────────────────
|
|
138
|
+
// F3 v0.5: protocols layer operations. `protocols export` writes the
|
|
139
|
+
// trust-scorer pattern bank to .codehere/protocols/hook_patterns.json so
|
|
140
|
+
// auditors / cross-tool consumers can read codehere's safety surface.
|
|
141
|
+
program
|
|
142
|
+
.command('protocols', { hidden: true }) // hidden from --help (May 2 cut: power-user F3 export, see docs/MEMORY.md)
|
|
143
|
+
.description('F3 protocols layer · export (write hook_patterns.json from trust-scorer bank)')
|
|
144
|
+
.argument('[action]', 'Action: export, help')
|
|
145
|
+
.option('--rebuild', 'Refresh built-in patterns (project patterns are preserved)')
|
|
146
|
+
.action((action, options) => {
|
|
147
|
+
executeProtocolsCommand(action, { rebuild: options.rebuild });
|
|
148
|
+
});
|
|
106
149
|
// ── config ───────────────────────────────────────────────────────────────────
|
|
107
150
|
program
|
|
108
151
|
.command('config')
|
|
@@ -302,7 +345,7 @@ program
|
|
|
302
345
|
// ── serve (MCP server) ───────────────────────────────────────────────────────
|
|
303
346
|
program
|
|
304
347
|
.command('serve')
|
|
305
|
-
.description('
|
|
348
|
+
.description('Run codehere as a background MCP server (no web UI) — for Claude Desktop / Cursor integration')
|
|
306
349
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
307
350
|
.option('--host <host>', 'Host to bind to (default: 127.0.0.1)', '127.0.0.1')
|
|
308
351
|
.option('--token <token>', 'Require Bearer token for authentication')
|
|
@@ -363,7 +406,7 @@ program
|
|
|
363
406
|
// ── start (Mission Control: daemon + web UI) ─────────────────────────────────
|
|
364
407
|
program
|
|
365
408
|
.command('start')
|
|
366
|
-
.description('Start
|
|
409
|
+
.description('Start codehere — interactive PM dashboard at localhost:3773 (delegate tasks, watch agents work)')
|
|
367
410
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
368
411
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
369
412
|
.option('--no-open', "Don't auto-open the browser")
|
|
@@ -556,7 +599,7 @@ program
|
|
|
556
599
|
});
|
|
557
600
|
// ── scan-tool (called by Claude Code hooks system, not users directly) ───────
|
|
558
601
|
program
|
|
559
|
-
.command('scan-tool')
|
|
602
|
+
.command('scan-tool', { hidden: true }) // hidden from --help (May 2 cut: internal hook handler, called by Claude Code via stdin)
|
|
560
603
|
.description('Hook handler: reads Claude Code tool event from stdin, outputs block/allow (used by hook system)')
|
|
561
604
|
.option('--log-only', 'Never block, log only')
|
|
562
605
|
.option('--block-all', 'Block on any finding, not just critical')
|
|
@@ -570,7 +613,7 @@ program
|
|
|
570
613
|
});
|
|
571
614
|
// ── completion (shell completion script generator) ──────────────────────────
|
|
572
615
|
program
|
|
573
|
-
.command('completion')
|
|
616
|
+
.command('completion', { hidden: true }) // hidden from --help (May 2 cut: shell-completion is npm convention, not product surface)
|
|
574
617
|
.description('Generate shell completion script')
|
|
575
618
|
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
576
619
|
.action(async (shell) => {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
15
15
|
*/
|
|
16
|
-
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
16
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
19
|
const PATTERNS = [
|
|
@@ -392,9 +392,24 @@ export function appendAuditLog(entry) {
|
|
|
392
392
|
try {
|
|
393
393
|
const dir = AUDIT_FILE.substring(0, AUDIT_FILE.lastIndexOf('/'));
|
|
394
394
|
if (dir && !existsSync(dir)) {
|
|
395
|
-
|
|
395
|
+
// mode 0o700 — owner-only directory. The audit log records every tool call
|
|
396
|
+
// including bash commands and file paths; on multi-user systems a default
|
|
397
|
+
// 0o755 dir would let other users walk in and read the file. Surfaced by
|
|
398
|
+
// the May 1 2026 /cso security audit (Finding #1, MEDIUM).
|
|
399
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
400
|
+
}
|
|
401
|
+
// mode 0o600 — owner-only file. Same reasoning as the dir above. Only takes
|
|
402
|
+
// effect on first write (file creation); existing files are chmod'd below
|
|
403
|
+
// for backwards compatibility with audit logs created before this fix.
|
|
404
|
+
appendFileSync(AUDIT_FILE, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
405
|
+
// Idempotent: if the file existed pre-fix with 0o644, tighten it now. The
|
|
406
|
+
// chmod is cheap and silent on already-0o600 files.
|
|
407
|
+
try {
|
|
408
|
+
chmodSync(AUDIT_FILE, 0o600);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
/* fall through — chmod failure shouldn't block the audit append */
|
|
396
412
|
}
|
|
397
|
-
appendFileSync(AUDIT_FILE, JSON.stringify(entry) + '\n');
|
|
398
413
|
}
|
|
399
414
|
catch {
|
|
400
415
|
// Audit log failure must not block tool execution
|
|
@@ -16,6 +16,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
16
16
|
import { EventEmitter } from 'events';
|
|
17
17
|
import { randomUUID } from 'crypto';
|
|
18
18
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
19
|
+
import { applyMemoryPrefix } from './memory-injector.js';
|
|
19
20
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
20
21
|
const HEADLESS_FLAGS = [
|
|
21
22
|
'--yes-always', // auto-accept all file-edit confirmations
|
|
@@ -70,8 +71,16 @@ export function spawnAider(prompt, options = {}) {
|
|
|
70
71
|
let statusValue = 'running';
|
|
71
72
|
let killRequested = false;
|
|
72
73
|
const cwd = options.cwd ?? process.cwd();
|
|
74
|
+
// F3 Task 8 — memory injection (same shape as every other adapter).
|
|
75
|
+
const injected = applyMemoryPrefix(prompt, cwd, options);
|
|
76
|
+
const finalPrompt = injected.prompt;
|
|
73
77
|
const snapshot = snapshotCwd(cwd);
|
|
74
|
-
|
|
78
|
+
// ProgressAggregator parses aider's stdout for `Running ...`, `Applied edit
|
|
79
|
+
// to ...`, and `Creating empty file ...` lines and emits Run/Edit/Write
|
|
80
|
+
// ProgressSteps. Observability parity (Path B) — see ingestAider() in
|
|
81
|
+
// progress-aggregator.ts. Blocking parity (Path A) needs the upstream
|
|
82
|
+
// `--pre-action-hook` PR.
|
|
83
|
+
const aggregator = new ProgressAggregator('aider');
|
|
75
84
|
let stepsSnapshot = [];
|
|
76
85
|
let artifactsSnapshot;
|
|
77
86
|
let textAccum = '';
|
|
@@ -139,7 +148,7 @@ export function spawnAider(prompt, options = {}) {
|
|
|
139
148
|
const spawnEnv = { ...process.env, ...(options.env ?? {}) };
|
|
140
149
|
let child;
|
|
141
150
|
try {
|
|
142
|
-
child = spawn('aider', ['--message',
|
|
151
|
+
child = spawn('aider', ['--message', finalPrompt, ...HEADLESS_FLAGS, ...(options.extraArgs ?? [])], {
|
|
143
152
|
cwd,
|
|
144
153
|
env: spawnEnv,
|
|
145
154
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -225,6 +234,7 @@ export function spawnAider(prompt, options = {}) {
|
|
|
225
234
|
startedAt,
|
|
226
235
|
prompt,
|
|
227
236
|
agent: 'aider',
|
|
237
|
+
memoryInjection: injected.record,
|
|
228
238
|
get status() {
|
|
229
239
|
return statusValue;
|
|
230
240
|
},
|
|
@@ -19,6 +19,7 @@ import { EventEmitter } from 'events';
|
|
|
19
19
|
import { randomUUID } from 'crypto';
|
|
20
20
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
21
21
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
22
|
+
import { applyMemoryPrefix } from './memory-injector.js';
|
|
22
23
|
// Runtime-error fingerprints → structured codehere:error events with recovery hints.
|
|
23
24
|
// Matched against any stderr line or assistant-text content we see on the stream.
|
|
24
25
|
const ERROR_PATTERNS = [
|
|
@@ -78,6 +79,12 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
78
79
|
let killRequested = false;
|
|
79
80
|
// PM UX Layer 2+3 state. Shared helper modules; per-session instances.
|
|
80
81
|
const cwd = options.cwd ?? process.cwd();
|
|
82
|
+
// F3 Task 8 — inject project memory (AGENTS.md + lessons + repo_map slice)
|
|
83
|
+
// into the prompt prefix unless the caller opted out. Pure, side-effect-
|
|
84
|
+
// free; returns unchanged prompt + record=undefined when there's no
|
|
85
|
+
// .codehere/ at cwd.
|
|
86
|
+
const injected = applyMemoryPrefix(prompt, cwd, options);
|
|
87
|
+
const finalPrompt = injected.prompt;
|
|
81
88
|
const snapshot = snapshotCwd(cwd);
|
|
82
89
|
const aggregator = new ProgressAggregator('claude-code');
|
|
83
90
|
let stepsSnapshot = [];
|
|
@@ -176,7 +183,7 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
176
183
|
}
|
|
177
184
|
let child;
|
|
178
185
|
try {
|
|
179
|
-
child = spawn('claude', ['-p',
|
|
186
|
+
child = spawn('claude', ['-p', finalPrompt, ...HEADLESS_FLAGS, ...(options.extraArgs ?? [])], {
|
|
180
187
|
cwd,
|
|
181
188
|
env: spawnEnv,
|
|
182
189
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -277,6 +284,8 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
277
284
|
startedAt,
|
|
278
285
|
prompt,
|
|
279
286
|
agent: 'claude-code',
|
|
287
|
+
canAppendMessage: true,
|
|
288
|
+
memoryInjection: injected.record,
|
|
280
289
|
get status() {
|
|
281
290
|
return statusValue;
|
|
282
291
|
},
|
|
@@ -309,6 +318,66 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
309
318
|
}
|
|
310
319
|
}, 3000);
|
|
311
320
|
},
|
|
321
|
+
/**
|
|
322
|
+
* Iter 12 — follow-up message to a completed session. Spawns a fresh
|
|
323
|
+
* `claude -p "<text>" -c` in the same cwd, which uses Claude Code's
|
|
324
|
+
* native -c flag to continue the most recent conversation in that dir.
|
|
325
|
+
*
|
|
326
|
+
* Scope: one-shot Q&A. No streaming, no event piping into the parent
|
|
327
|
+
* emitter. Returns the assistant's response text. Iter 13 may upgrade
|
|
328
|
+
* to a true thread-streaming model if the dogfood signal warrants.
|
|
329
|
+
*/
|
|
330
|
+
appendMessage(text) {
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
let stdout = '';
|
|
334
|
+
let stderr = '';
|
|
335
|
+
const followup = spawn('claude', ['-p', text, '-c', ...HEADLESS_FLAGS], {
|
|
336
|
+
cwd,
|
|
337
|
+
env: spawnEnv,
|
|
338
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
});
|
|
340
|
+
followup.on('error', (err) => reject(err));
|
|
341
|
+
followup.stdout?.on('data', (chunk) => {
|
|
342
|
+
stdout += chunk.toString('utf-8');
|
|
343
|
+
});
|
|
344
|
+
followup.stderr?.on('data', (chunk) => {
|
|
345
|
+
stderr += chunk.toString('utf-8');
|
|
346
|
+
});
|
|
347
|
+
followup.on('exit', (code) => {
|
|
348
|
+
if (code !== 0) {
|
|
349
|
+
reject(new Error(`claude -c exited ${code}: ${stderr.slice(0, 200)}`));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Parse stream-json — extract assistant message text from the
|
|
353
|
+
// 'assistant' typed lines and the final 'result' line.
|
|
354
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
355
|
+
const fragments = [];
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
try {
|
|
358
|
+
const obj = JSON.parse(line);
|
|
359
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
360
|
+
for (const block of obj.message.content) {
|
|
361
|
+
if (block.type === 'text' && block.text)
|
|
362
|
+
fragments.push(block.text);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
366
|
+
fragments.push(obj.result);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Non-JSON line — skip
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Dedupe trivially: if 'result' contained the same text as the
|
|
374
|
+
// last assistant block, take just one. Conservative: prefer 'result'
|
|
375
|
+
// if present (it's the final summary), else join assistants.
|
|
376
|
+
const result = fragments.length ? fragments[fragments.length - 1] : '(no response captured)';
|
|
377
|
+
resolve({ result, durationMs: Date.now() - startedAt });
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
},
|
|
312
381
|
};
|
|
313
382
|
}
|
|
314
383
|
// ── AgentAdapter implementation ─────────────────────────────────────────────
|
|
@@ -18,6 +18,7 @@ import { EventEmitter } from 'events';
|
|
|
18
18
|
import { randomUUID } from 'crypto';
|
|
19
19
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
20
20
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
21
|
+
import { applyMemoryPrefix } from './memory-injector.js';
|
|
21
22
|
const HEADLESS_FLAGS = [
|
|
22
23
|
'exec',
|
|
23
24
|
'--json',
|
|
@@ -73,6 +74,9 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
73
74
|
// PM UX Layer 2+3 state — same shape as claude-code-adapter. Kept in-file (not
|
|
74
75
|
// extracted to a base class) because the two CLIs diverge in more than they share.
|
|
75
76
|
const cwd = options.cwd ?? process.cwd();
|
|
77
|
+
// F3 Task 8 — memory injection (same shape as every other adapter).
|
|
78
|
+
const injected = applyMemoryPrefix(prompt, cwd, options);
|
|
79
|
+
const finalPrompt = injected.prompt;
|
|
76
80
|
const snapshot = snapshotCwd(cwd);
|
|
77
81
|
const aggregator = new ProgressAggregator('codex');
|
|
78
82
|
let stepsSnapshot = [];
|
|
@@ -151,7 +155,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
151
155
|
env: spawnEnv,
|
|
152
156
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
153
157
|
});
|
|
154
|
-
child.stdin?.write(
|
|
158
|
+
child.stdin?.write(finalPrompt);
|
|
155
159
|
child.stdin?.end();
|
|
156
160
|
}
|
|
157
161
|
catch (err) {
|
|
@@ -241,6 +245,7 @@ export function spawnCodex(prompt, options = {}) {
|
|
|
241
245
|
startedAt,
|
|
242
246
|
prompt,
|
|
243
247
|
agent: 'codex',
|
|
248
|
+
memoryInjection: injected.record,
|
|
244
249
|
get status() {
|
|
245
250
|
return statusValue;
|
|
246
251
|
},
|
|
@@ -284,8 +284,17 @@ function spawnConductor(prompt, options, lookup) {
|
|
|
284
284
|
let artifactsSnapshot;
|
|
285
285
|
specialistSession.subscribe((evt) => {
|
|
286
286
|
buffer.push(evt);
|
|
287
|
-
|
|
288
|
-
//
|
|
287
|
+
// Mirror progress + artifacts BEFORE re-emitting so any subscriber that
|
|
288
|
+
// reads `session.status` / `session.steps` / `session.artifacts` from
|
|
289
|
+
// inside its event handler sees the up-to-date conductor state.
|
|
290
|
+
//
|
|
291
|
+
// BUG 2 (zombie RUNNING sessions, May 1 2026, 138/155 records affected):
|
|
292
|
+
// pre-fix this block emitted FIRST and updated status SECOND. session-
|
|
293
|
+
// manager.ts subscribes to the conductor session and persists `session.status`
|
|
294
|
+
// synchronously inside its on-exit handler. With emit-before-update,
|
|
295
|
+
// every conductor session was persisted with status='running' even though
|
|
296
|
+
// the underlying specialist had already finished. The fix reorders so the
|
|
297
|
+
// emit() at the end sees the freshly-mirrored statusValue.
|
|
289
298
|
if (specialistSession.steps)
|
|
290
299
|
stepsSnapshot = specialistSession.steps;
|
|
291
300
|
if (specialistSession.artifacts)
|
|
@@ -298,6 +307,7 @@ function spawnConductor(prompt, options, lookup) {
|
|
|
298
307
|
if (evt.type === 'exit' || evt.type === 'codehere:complete' || evt.type === 'codehere:error') {
|
|
299
308
|
statusValue = specialistSession.status;
|
|
300
309
|
}
|
|
310
|
+
emitter.emit('event', evt);
|
|
301
311
|
});
|
|
302
312
|
return {
|
|
303
313
|
sessionId,
|