ctxcarry 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -0
- package/assets/ctxcarry-ascii.svg +39 -0
- package/dist/archive.js +19 -0
- package/dist/capture.js +250 -0
- package/dist/cli.js +213 -0
- package/dist/compile.js +152 -0
- package/dist/content-router.js +93 -0
- package/dist/distill.js +249 -0
- package/dist/git.js +65 -0
- package/dist/learn.js +30 -0
- package/dist/mcp-server.js +94 -0
- package/dist/paths.js +8 -0
- package/dist/redact.js +26 -0
- package/dist/store.js +175 -0
- package/dist/summarizers/openai.js +41 -0
- package/dist/tokens.js +14 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<img src="./assets/ctxcarry-ascii.svg" alt="ctxcarry - Portable project memory for AI coding agents" width="100%">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h2>Move work between Claude Code, Codex, and future agents without losing repo state.</h2>
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<img src="https://img.shields.io/badge/build-passing-22c55e?style=flat-square" />
|
|
11
|
+
<img src="https://img.shields.io/badge/version-v0.3.0-f97316?style=flat-square" />
|
|
12
|
+
<img src="https://img.shields.io/badge/npm-ctxcarry-c026d3?style=flat-square" />
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-3b82f6?style=flat-square" />
|
|
14
|
+
<img src="https://img.shields.io/badge/license-MIT-8b5cf6?style=flat-square" />
|
|
15
|
+
<img src="https://img.shields.io/badge/local--first-yes-111827?style=flat-square" />
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
<a href="#quickstart">Quickstart</a> ·
|
|
20
|
+
<a href="#commands">Commands</a> ·
|
|
21
|
+
<a href="#mcp-retrieval">MCP</a> ·
|
|
22
|
+
<a href="#benchmarks">Benchmarks</a> ·
|
|
23
|
+
<a href="./BENCHMARKS.md">Proof</a> ·
|
|
24
|
+
<a href="#development">Development</a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
<p>
|
|
28
|
+
<strong>AI agents / LLMs:</strong> use ctxcarry to capture repo state, compact session memory, and continue work across agents.
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# ctxcarry
|
|
36
|
+
|
|
37
|
+
**Portable project memory for AI coding agents.**
|
|
38
|
+
|
|
39
|
+
ctxcarry lets developers move long-running coding work between Claude Code, Codex, and future agents without losing the working state of a repo. It captures what happened in an agent session, distills the useful state, and generates the right handoff context for the next agent.
|
|
40
|
+
|
|
41
|
+
The core idea:
|
|
42
|
+
|
|
43
|
+
> Coding agents should not own your context. Your repo should.
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
AI coding agents are powerful, but their context is trapped inside isolated sessions. If you start in Claude Code and then switch to Codex, the next agent usually does not know:
|
|
48
|
+
|
|
49
|
+
- what task was in progress
|
|
50
|
+
- which files changed
|
|
51
|
+
- what tests failed
|
|
52
|
+
- what decisions were made
|
|
53
|
+
- what constraints matter
|
|
54
|
+
- what the next step should be
|
|
55
|
+
|
|
56
|
+
ctxcarry stores that state locally in your repository and compiles compact agent-specific context files.
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g ctxcarry
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Inside a project:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ctxcarry init
|
|
68
|
+
ctxcarry run claude
|
|
69
|
+
ctxcarry switch codex
|
|
70
|
+
codex
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That creates a Claude session, captures before/after git state, asks Claude to write a structured summary, compacts the result, updates `AGENTS.md`, and prepares Codex to continue.
|
|
74
|
+
|
|
75
|
+
## What It Generates
|
|
76
|
+
|
|
77
|
+
ctxcarry stores source-of-truth state under `.ctxcarry/`:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
.ctxcarry/
|
|
81
|
+
events.jsonl
|
|
82
|
+
commands.jsonl
|
|
83
|
+
state.json
|
|
84
|
+
state.md
|
|
85
|
+
learned.md
|
|
86
|
+
sessions/
|
|
87
|
+
ctxcarrys/
|
|
88
|
+
codex.md
|
|
89
|
+
claude.md
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Generated agent files:
|
|
93
|
+
|
|
94
|
+
- `AGENTS.md` for Codex
|
|
95
|
+
- `CLAUDE.md` for Claude Code
|
|
96
|
+
- `.ctxcarry/ctxcarrys/codex.md`
|
|
97
|
+
- `.ctxcarry/ctxcarrys/claude.md`
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ctxcarry init
|
|
103
|
+
ctxcarry run claude
|
|
104
|
+
ctxcarry switch codex
|
|
105
|
+
ctxcarry status
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Useful extras:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
ctxcarry capture
|
|
112
|
+
ctxcarry compact
|
|
113
|
+
ctxcarry compile --agent codex
|
|
114
|
+
ctxcarry compile --agent claude
|
|
115
|
+
handoff tokens
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Add explicit context when needed:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
ctxcarry note --type task --text "Fix Google OAuth redirect bug"
|
|
122
|
+
ctxcarry note --type decision --text "Do not rewrite the auth provider"
|
|
123
|
+
ctxcarry note --type constraint --text "Preserve existing email login behavior"
|
|
124
|
+
ctxcarry note --type failure --text "Production callback returns 400"
|
|
125
|
+
ctxcarry note --type next --text "Check redirect URI construction"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Session Summaries
|
|
129
|
+
|
|
130
|
+
Before launching an agent, ctxcarry writes:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
.ctxcarry/sessions/<session-id>/instructions.md
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The instruction asks the agent to write:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
.ctxcarry/sessions/<session-id>/summary.md
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
with strict headings:
|
|
143
|
+
|
|
144
|
+
```md
|
|
145
|
+
## Current Task
|
|
146
|
+
## Files Changed
|
|
147
|
+
## Decisions
|
|
148
|
+
## Constraints
|
|
149
|
+
## Failures
|
|
150
|
+
## Commands Run
|
|
151
|
+
## Next Step
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If the summary exists, ctxcarry parses it into structured memory. If it does not, ctxcarry falls back to git snapshots, changed files, branch, and diff summary.
|
|
155
|
+
|
|
156
|
+
## MCP Retrieval
|
|
157
|
+
|
|
158
|
+
Serve local project memory to MCP-compatible clients:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
ctxcarry mcp serve
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Available tools:
|
|
165
|
+
|
|
166
|
+
- `get_current_task`
|
|
167
|
+
- `get_latest_ctxcarry`
|
|
168
|
+
- `get_relevant_session_events`
|
|
169
|
+
- `expand_session_artifact`
|
|
170
|
+
- `summarize_latest_failure`
|
|
171
|
+
|
|
172
|
+
## Learning
|
|
173
|
+
|
|
174
|
+
Mine local sessions for durable guidance:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
ctxcarry learn
|
|
178
|
+
ctxcarry learn --apply
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`--apply` writes `.ctxcarry/learned.md` and updates `AGENTS.md` / `CLAUDE.md` with learned guidance.
|
|
182
|
+
|
|
183
|
+
## Benchmarks
|
|
184
|
+
|
|
185
|
+
Run deterministic local fixture benchmarks:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pnpm run bench
|
|
189
|
+
pnpm run bench -- --format json
|
|
190
|
+
pnpm run bench -- --out BENCHMARKS.md
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Current synthetic/local fixture headline:
|
|
194
|
+
|
|
195
|
+
| Metric | Value |
|
|
196
|
+
| --- | ---: |
|
|
197
|
+
| Total raw benchmark tokens | 210,371 |
|
|
198
|
+
| Total handoff tokens | 2,053 |
|
|
199
|
+
| Total saved tokens | 208,324 |
|
|
200
|
+
| Overall compression | 99.0% |
|
|
201
|
+
| Mean critical-fact recall | 100.0% |
|
|
202
|
+
| Mean budget recall | 100.0% |
|
|
203
|
+
| Contradictions found | 0 |
|
|
204
|
+
| Redaction checks | PASS |
|
|
205
|
+
|
|
206
|
+
These are deterministic synthetic/local fixture benchmarks, not production telemetry or real agent-execution results.
|
|
207
|
+
|
|
208
|
+
Run benchmarks on real local sessions:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pnpm run bench:real -- --project /path/to/your/project
|
|
212
|
+
pnpm run bench:continuation -- --project /path/to/your/project
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Real-session benchmarks report observable local artifacts only. They do not fabricate task-success or recall metrics.
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pnpm install
|
|
221
|
+
pnpm run build
|
|
222
|
+
pnpm test
|
|
223
|
+
pnpm run bench
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Link locally:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pnpm link --global
|
|
230
|
+
ctxcarry --help
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Status
|
|
234
|
+
|
|
235
|
+
ctxcarry is early local-first developer tooling. The current focus is reliable Claude Code to Codex handoff, local session capture, compact project memory, and measurable benchmark quality.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<svg width="1400" height="300" viewBox="0 0 1400 300" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="ctxcarryGradient" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0%" stop-color="#22c55e"/>
|
|
5
|
+
<stop offset="35%" stop-color="#38bdf8"/>
|
|
6
|
+
<stop offset="70%" stop-color="#8b5cf6"/>
|
|
7
|
+
<stop offset="100%" stop-color="#f97316"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
|
|
11
|
+
<rect width="1400" height="300" rx="18" fill="#0d1117"/>
|
|
12
|
+
|
|
13
|
+
<text
|
|
14
|
+
x="220"
|
|
15
|
+
y="60"
|
|
16
|
+
font-family="Consolas, Monaco, 'Courier New', monospace"
|
|
17
|
+
font-size="24"
|
|
18
|
+
font-weight="800"
|
|
19
|
+
fill="url(#ctxcarryGradient)"
|
|
20
|
+
xml:space="preserve">
|
|
21
|
+
<tspan x="220" dy="0"> ██████╗████████╗██╗ ██╗ ██████╗ █████╗ ██████╗ ██████╗ ██╗ ██╗</tspan>
|
|
22
|
+
<tspan x="220" dy="34">██╔════╝╚══██╔══╝╚██╗██╔╝██╔════╝██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝</tspan>
|
|
23
|
+
<tspan x="220" dy="34">██║ ██║ ╚███╔╝ ██║ ███████║██████╔╝██████╔╝ ╚████╔╝ </tspan>
|
|
24
|
+
<tspan x="220" dy="34">██║ ██║ ██╔██╗ ██║ ██╔══██║██╔══██╗██╔══██╗ ╚██╔╝ </tspan>
|
|
25
|
+
<tspan x="220" dy="34">╚██████╗ ██║ ██╔╝ ██╗╚██████╗██║ ██║██║ ██║██║ ██║ ██║ </tspan>
|
|
26
|
+
<tspan x="220" dy="34"> ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ </tspan>
|
|
27
|
+
</text>
|
|
28
|
+
|
|
29
|
+
<text
|
|
30
|
+
x="700"
|
|
31
|
+
y="265"
|
|
32
|
+
text-anchor="middle"
|
|
33
|
+
font-family="Consolas, Monaco, 'Courier New', monospace"
|
|
34
|
+
font-size="24"
|
|
35
|
+
font-weight="600"
|
|
36
|
+
fill="#e6edf3">
|
|
37
|
+
Portable project memory for AI coding agents
|
|
38
|
+
</text>
|
|
39
|
+
</svg>
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { redactText } from "./redact.js";
|
|
4
|
+
import { routeContent } from "./content-router.js";
|
|
5
|
+
export function archiveSessionArtifact(sessionDir, name, content) {
|
|
6
|
+
const rawDir = path.join(sessionDir, "raw");
|
|
7
|
+
fs.mkdirSync(rawDir, { recursive: true });
|
|
8
|
+
const artifactId = name.replace(/[^a-z0-9_.-]/gi, "-").toLowerCase();
|
|
9
|
+
const artifactPath = path.join(rawDir, artifactId);
|
|
10
|
+
const redacted = redactText(content);
|
|
11
|
+
fs.writeFileSync(artifactPath, redacted.endsWith("\n") ? redacted : `${redacted}\n`);
|
|
12
|
+
const routed = routeContent(name, redacted);
|
|
13
|
+
return {
|
|
14
|
+
artifactId,
|
|
15
|
+
path: artifactPath,
|
|
16
|
+
kind: routed.kind,
|
|
17
|
+
summary: routed.summary
|
|
18
|
+
};
|
|
19
|
+
}
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { archiveSessionArtifact } from "./archive.js";
|
|
5
|
+
import { ctxcarryPath } from "./paths.js";
|
|
6
|
+
import { redactText } from "./redact.js";
|
|
7
|
+
import { appendEvent, readConfig } from "./store.js";
|
|
8
|
+
import { getGitSnapshot, summarizeSnapshot } from "./git.js";
|
|
9
|
+
export function captureSnapshot(agent) {
|
|
10
|
+
const snapshot = getGitSnapshot();
|
|
11
|
+
appendEvent({
|
|
12
|
+
type: "repo_snapshot",
|
|
13
|
+
agent,
|
|
14
|
+
branch: snapshot.branch,
|
|
15
|
+
isRepo: snapshot.isRepo,
|
|
16
|
+
status: snapshot.status,
|
|
17
|
+
changedFiles: snapshot.changedFiles,
|
|
18
|
+
diffStat: snapshot.diffStat,
|
|
19
|
+
summary: summarizeSnapshot(snapshot)
|
|
20
|
+
});
|
|
21
|
+
return snapshot;
|
|
22
|
+
}
|
|
23
|
+
export async function runAgent(agent) {
|
|
24
|
+
const config = readConfig();
|
|
25
|
+
const agentConfig = config.agents[agent];
|
|
26
|
+
if (!agentConfig?.enabled) {
|
|
27
|
+
throw new Error(`Agent "${agent}" is not enabled in ctxcarry.config.json.`);
|
|
28
|
+
}
|
|
29
|
+
const sessionId = createSessionId(agent);
|
|
30
|
+
const sessionDir = ctxcarryPath("sessions", sessionId);
|
|
31
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
32
|
+
const instructionsPath = path.join(sessionDir, "instructions.md");
|
|
33
|
+
const summaryPath = path.join(sessionDir, "summary.md");
|
|
34
|
+
writeSessionInstructions(agent, sessionId, instructionsPath, summaryPath);
|
|
35
|
+
const instructionArtifact = archiveSessionArtifact(sessionDir, "instructions.md", fs.readFileSync(instructionsPath, "utf8"));
|
|
36
|
+
const command = agentConfig.command ?? agent;
|
|
37
|
+
const startedAtMs = Date.now();
|
|
38
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
39
|
+
const before = getGitSnapshot();
|
|
40
|
+
writeSnapshot(sessionDir, "before", before);
|
|
41
|
+
appendEvent({
|
|
42
|
+
type: "session_started",
|
|
43
|
+
agent,
|
|
44
|
+
sessionId,
|
|
45
|
+
sessionDir,
|
|
46
|
+
instructionsPath,
|
|
47
|
+
summaryPath,
|
|
48
|
+
command,
|
|
49
|
+
startedAt,
|
|
50
|
+
instructionArtifact,
|
|
51
|
+
branch: before.branch,
|
|
52
|
+
changedFiles: before.changedFiles,
|
|
53
|
+
diffStat: before.diffStat
|
|
54
|
+
});
|
|
55
|
+
const exitCode = await spawnInteractive(command, sessionDir, instructionsPath, summaryPath);
|
|
56
|
+
const endedAtMs = Date.now();
|
|
57
|
+
const endedAt = new Date(endedAtMs).toISOString();
|
|
58
|
+
const after = getGitSnapshot();
|
|
59
|
+
writeSnapshot(sessionDir, "after", after);
|
|
60
|
+
const beforeArtifact = archiveSessionArtifact(sessionDir, "before.json", JSON.stringify(before, null, 2));
|
|
61
|
+
const afterArtifact = archiveSessionArtifact(sessionDir, "after.json", JSON.stringify(after, null, 2));
|
|
62
|
+
const changedFiles = unique([...before.changedFiles, ...after.changedFiles]);
|
|
63
|
+
const newFiles = statusFiles(after.status, ["??", "A", "AM"]);
|
|
64
|
+
const deletedFiles = statusFiles(after.status, ["D", "AD"]);
|
|
65
|
+
const durationMs = endedAtMs - startedAtMs;
|
|
66
|
+
appendEvent({
|
|
67
|
+
type: "repo_snapshot",
|
|
68
|
+
agent,
|
|
69
|
+
sessionId,
|
|
70
|
+
branch: after.branch,
|
|
71
|
+
isRepo: after.isRepo,
|
|
72
|
+
status: after.status,
|
|
73
|
+
changedFiles,
|
|
74
|
+
newFiles,
|
|
75
|
+
deletedFiles,
|
|
76
|
+
diffStat: after.diffStat,
|
|
77
|
+
summary: summarizeSnapshot(after)
|
|
78
|
+
});
|
|
79
|
+
appendEvent({
|
|
80
|
+
type: "agent_session",
|
|
81
|
+
agent,
|
|
82
|
+
sessionId,
|
|
83
|
+
sessionDir,
|
|
84
|
+
instructionsPath,
|
|
85
|
+
summaryPath,
|
|
86
|
+
command,
|
|
87
|
+
startedAt,
|
|
88
|
+
endedAt,
|
|
89
|
+
durationMs,
|
|
90
|
+
exitCode,
|
|
91
|
+
branch: after.branch,
|
|
92
|
+
before,
|
|
93
|
+
after,
|
|
94
|
+
changedFiles,
|
|
95
|
+
newFiles,
|
|
96
|
+
deletedFiles,
|
|
97
|
+
diffSummary: after.diffStat
|
|
98
|
+
});
|
|
99
|
+
appendEvent({
|
|
100
|
+
type: "session_artifacts",
|
|
101
|
+
agent,
|
|
102
|
+
sessionId,
|
|
103
|
+
artifacts: [instructionArtifact, beforeArtifact, afterArtifact]
|
|
104
|
+
});
|
|
105
|
+
if (fs.existsSync(summaryPath)) {
|
|
106
|
+
const summaryArtifact = archiveSessionArtifact(sessionDir, "summary.md", fs.readFileSync(summaryPath, "utf8"));
|
|
107
|
+
appendEvent({
|
|
108
|
+
type: "session_artifact",
|
|
109
|
+
agent,
|
|
110
|
+
sessionId,
|
|
111
|
+
artifact: summaryArtifact
|
|
112
|
+
});
|
|
113
|
+
parseSessionSummary(agent, sessionId, summaryPath);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
appendEvent({
|
|
117
|
+
type: "session_fallback",
|
|
118
|
+
agent,
|
|
119
|
+
sessionId,
|
|
120
|
+
content: "No session summary was written. Falling back to git snapshots and changed files.",
|
|
121
|
+
branch: after.branch,
|
|
122
|
+
changedFiles,
|
|
123
|
+
newFiles,
|
|
124
|
+
deletedFiles,
|
|
125
|
+
diffSummary: after.diffStat
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return { sessionId, sessionDir, exitCode, changedFiles };
|
|
129
|
+
}
|
|
130
|
+
function writeSessionInstructions(agent, sessionId, instructionsPath, summaryPath) {
|
|
131
|
+
const relativeSummaryPath = path.relative(process.cwd(), summaryPath);
|
|
132
|
+
const instructions = `# ctxcarry Session Instructions
|
|
133
|
+
|
|
134
|
+
At the end of this session, write a concise ctxcarry summary to:
|
|
135
|
+
|
|
136
|
+
${relativeSummaryPath}
|
|
137
|
+
|
|
138
|
+
Use exactly these Markdown headings:
|
|
139
|
+
|
|
140
|
+
## Current Task
|
|
141
|
+
## Files Changed
|
|
142
|
+
## Decisions
|
|
143
|
+
## Constraints
|
|
144
|
+
## Failures
|
|
145
|
+
## Commands Run
|
|
146
|
+
## Next Step
|
|
147
|
+
|
|
148
|
+
Include only durable state the next coding agent needs to continue.
|
|
149
|
+
`;
|
|
150
|
+
fs.writeFileSync(instructionsPath, instructions);
|
|
151
|
+
}
|
|
152
|
+
function parseSessionSummary(agent, sessionId, summaryPath) {
|
|
153
|
+
const summary = redactText(fs.readFileSync(summaryPath, "utf8"));
|
|
154
|
+
const sections = parseMarkdownSections(summary);
|
|
155
|
+
const task = firstContent(sections["Current Task"]);
|
|
156
|
+
if (task) {
|
|
157
|
+
appendEvent({ type: "note", agent, sessionId, noteType: "task", content: task });
|
|
158
|
+
}
|
|
159
|
+
for (const file of listContent(sections["Files Changed"])) {
|
|
160
|
+
appendEvent({ type: "file_changed", agent, sessionId, path: file, change_summary: `Changed during ${sessionId}` });
|
|
161
|
+
}
|
|
162
|
+
for (const decision of listContent(sections.Decisions)) {
|
|
163
|
+
appendEvent({ type: "note", agent, sessionId, noteType: "decision", content: decision });
|
|
164
|
+
}
|
|
165
|
+
for (const constraint of listContent(sections.Constraints)) {
|
|
166
|
+
appendEvent({ type: "note", agent, sessionId, noteType: "constraint", content: constraint });
|
|
167
|
+
}
|
|
168
|
+
for (const failure of listContent(sections.Failures)) {
|
|
169
|
+
appendEvent({ type: "note", agent, sessionId, noteType: "failure", content: failure });
|
|
170
|
+
}
|
|
171
|
+
for (const command of listContent(sections["Commands Run"])) {
|
|
172
|
+
appendEvent({ type: "command_run", agent, sessionId, command, status: "recorded", summary: `Recorded from ${sessionId}` });
|
|
173
|
+
}
|
|
174
|
+
const next = firstContent(sections["Next Step"]);
|
|
175
|
+
if (next) {
|
|
176
|
+
appendEvent({ type: "note", agent, sessionId, noteType: "next", content: next });
|
|
177
|
+
}
|
|
178
|
+
appendEvent({
|
|
179
|
+
type: "session_summary",
|
|
180
|
+
agent,
|
|
181
|
+
sessionId,
|
|
182
|
+
summaryPath,
|
|
183
|
+
content: summary
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function parseMarkdownSections(markdown) {
|
|
187
|
+
const sections = {};
|
|
188
|
+
let current = null;
|
|
189
|
+
for (const line of markdown.split("\n")) {
|
|
190
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
191
|
+
if (heading) {
|
|
192
|
+
current = heading[1].trim();
|
|
193
|
+
sections[current] = "";
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (current) {
|
|
197
|
+
sections[current] += `${line}\n`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return sections;
|
|
201
|
+
}
|
|
202
|
+
function firstContent(section = "") {
|
|
203
|
+
return listContent(section)[0] ?? section.trim();
|
|
204
|
+
}
|
|
205
|
+
function listContent(section = "") {
|
|
206
|
+
return section
|
|
207
|
+
.split("\n")
|
|
208
|
+
.map((line) => line.trim())
|
|
209
|
+
.map((line) => line.replace(/^[-*]\s+/, "").replace(/^\d+\.\s+/, ""))
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
function spawnInteractive(command, sessionDir, instructionsPath, summaryPath) {
|
|
213
|
+
const [bin, ...args] = splitCommand(command);
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const child = spawn(bin, args, {
|
|
216
|
+
cwd: process.cwd(),
|
|
217
|
+
env: {
|
|
218
|
+
...process.env,
|
|
219
|
+
CTXCARRY_SESSION_DIR: sessionDir,
|
|
220
|
+
CTXCARRY_SESSION_INSTRUCTIONS: instructionsPath,
|
|
221
|
+
CTXCARRY_SESSION_SUMMARY: summaryPath
|
|
222
|
+
},
|
|
223
|
+
stdio: "inherit",
|
|
224
|
+
shell: false
|
|
225
|
+
});
|
|
226
|
+
child.on("error", reject);
|
|
227
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function writeSnapshot(sessionDir, name, snapshot) {
|
|
231
|
+
fs.writeFileSync(path.join(sessionDir, `${name}.json`), JSON.stringify(snapshot, null, 2) + "\n");
|
|
232
|
+
}
|
|
233
|
+
function createSessionId(agent) {
|
|
234
|
+
const safeAgent = agent.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
|
|
235
|
+
return `${new Date().toISOString().replace(/[:.]/g, "-")}-${safeAgent}-${process.pid}`;
|
|
236
|
+
}
|
|
237
|
+
function statusFiles(status, codes) {
|
|
238
|
+
return unique(status
|
|
239
|
+
.filter((line) => codes.some((code) => line.trimStart().startsWith(code)))
|
|
240
|
+
.map((line) => line.slice(3).trim())
|
|
241
|
+
.filter((file) => !file.startsWith(".ctxcarry/"))
|
|
242
|
+
.filter(Boolean));
|
|
243
|
+
}
|
|
244
|
+
function splitCommand(command) {
|
|
245
|
+
const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
246
|
+
return parts.map((part) => part.replace(/^["']|["']$/g, ""));
|
|
247
|
+
}
|
|
248
|
+
function unique(values) {
|
|
249
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
250
|
+
}
|