ctxpool 0.1.1 → 0.4.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 +94 -103
- package/dist/cli.js +328 -184
- package/dist/cli.js.map +1 -1
- package/dist/clipboard.js +8 -10
- package/dist/clipboard.js.map +1 -1
- package/dist/format.d.ts +32 -0
- package/dist/format.js +110 -0
- package/dist/format.js.map +1 -0
- package/dist/handoff.d.ts +16 -13
- package/dist/handoff.js +441 -706
- package/dist/handoff.js.map +1 -1
- package/dist/mcp.js +99 -221
- package/dist/mcp.js.map +1 -1
- package/dist/pack.d.ts +14 -0
- package/dist/pack.js +121 -0
- package/dist/pack.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.js +35 -5
- package/dist/paths.js.map +1 -1
- package/dist/redact.d.ts +6 -0
- package/dist/redact.js +85 -0
- package/dist/redact.js.map +1 -0
- package/dist/repo.d.ts +6 -1
- package/dist/repo.js +109 -123
- package/dist/repo.js.map +1 -1
- package/dist/session.d.ts +40 -1
- package/dist/session.js +1295 -302
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +13 -6
- package/dist/store.js +88 -115
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +6 -0
- package/dist/text.js +25 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +44 -7
- package/dist/ui.d.ts +10 -1
- package/dist/ui.js +329 -383
- package/dist/ui.js.map +1 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +40 -10
- package/dist/util.js.map +1 -1
- package/dist/verify.d.ts +16 -0
- package/dist/verify.js +74 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
- package/dist-web/assets/index-BBjy_S2c.css +2 -0
- package/dist-web/assets/index-QwGbBEu2.js +17 -0
- package/dist-web/index.html +4 -3
- package/package.json +18 -17
- package/dist/logs.d.ts +0 -9
- package/dist/logs.js +0 -82
- package/dist/logs.js.map +0 -1
- package/dist-web/assets/contextpool-mark-Bu_00i5t.svg +0 -16
- package/dist-web/assets/index-BfHRocKl.js +0 -14
- package/dist-web/assets/index-l1QjYqDb.css +0 -2
package/dist/handoff.js
CHANGED
|
@@ -1,262 +1,148 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { ensureCtxDirs } from "./paths.js";
|
|
4
|
+
import { redactDeep, redactSecrets } from "./redact.js";
|
|
4
5
|
import { captureGitSnapshot, inferTaskName } from "./repo.js";
|
|
5
|
-
import { resolveSessionSource } from "./session.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveSessionSource, sourceLabel } from "./session.js";
|
|
7
|
+
import { estimateTokens, oneLine, truncate } from "./text.js";
|
|
8
|
+
import { canonicalPath, hashText, limit, slugify, unique } from "./util.js";
|
|
9
|
+
import { isVerificationCommand, verificationKey, verificationLabel } from "./verify.js";
|
|
10
|
+
import { VERSION } from "./version.js";
|
|
11
|
+
export { createContextPack, normalizeContextPack, renderContextPackOutput } from "./pack.js";
|
|
12
|
+
// Agents narrate obstacles they then work around; only their latest messages describe the current state.
|
|
13
|
+
const BLOCKER_WINDOW = 2;
|
|
14
|
+
const TODO_WINDOW = 4;
|
|
7
15
|
export async function createHandoff(options = {}) {
|
|
8
|
-
const cwd = options.cwd || process.cwd();
|
|
16
|
+
const cwd = resolve(options.cwd || process.cwd());
|
|
9
17
|
const paths = ensureCtxDirs();
|
|
10
18
|
const repo = captureGitSnapshot(cwd);
|
|
11
19
|
const task = inferTaskName(options.task, repo);
|
|
12
|
-
const sourceSession = resolveSessionSource({
|
|
20
|
+
const sourceSession = withRepoRelativeEdits(redactDeep(resolveSessionSource({
|
|
13
21
|
from: options.from || "auto",
|
|
14
22
|
cwd,
|
|
23
|
+
repoRoot: repo.isGitRepo ? repo.repoPath : undefined,
|
|
15
24
|
session: options.session,
|
|
16
25
|
budget: options.budget
|
|
17
|
-
});
|
|
18
|
-
const sources = sourceSessionToSignals(sourceSession);
|
|
26
|
+
})), repo.repoPath);
|
|
19
27
|
const sections = buildSections(task, repo.dirtyFiles, repo.diffStat, sourceSession);
|
|
20
28
|
const handoffSummary = buildHandoffSummary(task, repo, sourceSession, sections);
|
|
21
29
|
const createdAt = new Date().toISOString();
|
|
22
30
|
const id = `${createdAt.replace(/[:.]/g, "-")}-${hashText(`${task}:${createdAt}`)}`;
|
|
23
|
-
if (options.useOllama
|
|
24
|
-
const improved = await
|
|
31
|
+
if (options.useOllama) {
|
|
32
|
+
const improved = await suggestNextActionWithOllama(task, handoffSummary, options.ollamaModel).catch(() => undefined);
|
|
25
33
|
if (improved) {
|
|
26
34
|
sections.nextBestAction = improved;
|
|
27
35
|
handoffSummary.nextAction = improved;
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
const
|
|
31
|
-
const markdown = renderHandoffMarkdown({
|
|
38
|
+
const packet = {
|
|
32
39
|
id,
|
|
33
40
|
task,
|
|
34
41
|
createdAt,
|
|
35
42
|
targetAgent: options.to,
|
|
36
43
|
repo,
|
|
37
|
-
sources,
|
|
44
|
+
sources: [{ source: sourceSession.source, path: sourceSession.path, modifiedAt: sourceSession.updatedAt, snippets: [] }],
|
|
38
45
|
sourceSession,
|
|
39
46
|
sections,
|
|
40
47
|
handoffSummary,
|
|
41
|
-
engineeringMemory,
|
|
48
|
+
engineeringMemory: buildEngineeringMemory(task, repo, sourceSession, id, handoffSummary),
|
|
42
49
|
markdown: "",
|
|
43
50
|
markdownPath: ""
|
|
44
|
-
}
|
|
51
|
+
};
|
|
52
|
+
packet.markdown = renderHandoffMarkdown(packet, options.includeTranscript !== false);
|
|
53
|
+
packet.tokenEstimate = estimateTokens(packet.markdown);
|
|
45
54
|
const taskDir = join(paths.handoffsDir, slugify(task));
|
|
46
55
|
mkdirSync(taskDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
writeFileSync(markdownPath, markdown, "utf8");
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
sections,
|
|
58
|
-
handoffSummary,
|
|
59
|
-
engineeringMemory,
|
|
60
|
-
markdown,
|
|
61
|
-
markdownPath
|
|
62
|
-
};
|
|
63
|
-
}
|
|
56
|
+
packet.markdownPath = join(taskDir, `${createdAt.replace(/[:.]/g, "-")}.md`);
|
|
57
|
+
writeFileSync(packet.markdownPath, packet.markdown, { encoding: "utf8", mode: 0o600 });
|
|
58
|
+
return packet;
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Extraction
|
|
62
|
+
/**
|
|
63
|
+
* Pull the working state out of a session. Structured agent data (plans, command exit codes, edits)
|
|
64
|
+
* wins; prose heuristics fill gaps and only read the user's and the agent's own words, never tool output.
|
|
65
|
+
*/
|
|
64
66
|
export function buildSections(task, filesTouched, diffStat, sourceSessionOrSources) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const errors =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
67
|
+
const session = Array.isArray(sourceSessionOrSources) ? undefined : sourceSessionOrSources;
|
|
68
|
+
const legacySnippets = Array.isArray(sourceSessionOrSources) ? sourceSessionOrSources.flatMap((source) => source.snippets) : [];
|
|
69
|
+
const userMessages = session ? session.transcript.filter((entry) => entry.role === "user").map((entry) => entry.text) : [];
|
|
70
|
+
const assistantMessages = session ? session.transcript.filter((entry) => entry.role === "assistant").map((entry) => entry.text) : [];
|
|
71
|
+
const allProse = session ? assistantMessages.flatMap(sentences) : legacySnippets.flatMap(sentences);
|
|
72
|
+
const recent = (count) => (session ? assistantMessages.slice(-count).flatMap(sentences) : allProse);
|
|
73
|
+
const plan = session?.plan;
|
|
74
|
+
const completed = plan ? plan.filter((item) => item.status === "completed").map((item) => item.text) : pick(allProse, COMPLETED, 8);
|
|
75
|
+
const inProgress = plan ? plan.filter((item) => item.status === "in_progress").map((item) => item.text) : [];
|
|
76
|
+
const todos = plan ? plan.filter((item) => item.status !== "completed").map((item) => item.text) : pick(recent(TODO_WINDOW), TODO, 10);
|
|
77
|
+
const blockers = pick(recent(BLOCKER_WINDOW), BLOCKER, 6);
|
|
78
|
+
const verification = summarizeVerification(session, legacySnippets);
|
|
79
|
+
const errors = session
|
|
80
|
+
? verification.commands.filter((command) => command.status === "failed").map((command) => `\`${command.command}\` failed${command.evidence ? `: ${command.evidence}` : ""}`)
|
|
81
|
+
: pick(legacySnippets, /\b(error|failed|failure|exception|traceback)\b/i, 8);
|
|
82
|
+
const commandsRun = session ? limit((session.commands || []).map((run) => run.command), 20) : pick(legacySnippets, SHELL_TOOL, 12);
|
|
83
|
+
const editedFiles = (session?.edits || []).map((edit) => edit.path);
|
|
84
|
+
const mentionedFiles = session ? [] : extractFilePaths(legacySnippets);
|
|
85
|
+
const decisions = pick(allProse, DECISION, 8);
|
|
86
|
+
// The first and latest requests are rendered in full, so constraints come from the messages in between.
|
|
87
|
+
const constraints = limit([...userMessages.slice(1, -1).flatMap(sentences).filter((sentence) => CONSTRAINT.test(sentence)), ...pick(allProse, DEAD_END, 4)], 8);
|
|
88
|
+
const changedFiles = limit([...filesTouched, ...editedFiles, ...mentionedFiles], 40);
|
|
83
89
|
return {
|
|
84
|
-
goal: `Continue the ${task} task
|
|
85
|
-
currentStatus:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
? `Exported ${sourceSession.transcript.length} transcript item(s) from ${sourceSession.source} session ${sourceSession.id}.`
|
|
90
|
-
: sources.length > 0 ? `Imported signals from ${sources.length} recent agent log file(s).` : "No recent Codex or Claude log signals were found."
|
|
91
|
-
], 6),
|
|
90
|
+
goal: `Continue the ${task} task.`,
|
|
91
|
+
currentStatus: [
|
|
92
|
+
changedFiles.length ? `${changedFiles.length} changed file(s)${diffStat ? ` (${diffStat})` : ""}.` : "Working tree is clean.",
|
|
93
|
+
`Verification: ${verification.status}.`
|
|
94
|
+
],
|
|
92
95
|
completed,
|
|
93
|
-
inProgress
|
|
96
|
+
inProgress,
|
|
94
97
|
todos,
|
|
95
98
|
blockers,
|
|
96
|
-
filesTouched:
|
|
99
|
+
filesTouched: changedFiles,
|
|
97
100
|
commandsRun,
|
|
98
101
|
errors,
|
|
99
102
|
decisions,
|
|
100
|
-
doNotRepeat:
|
|
101
|
-
nextBestAction:
|
|
103
|
+
doNotRepeat: constraints,
|
|
104
|
+
nextBestAction: chooseNextAction({
|
|
105
|
+
verification,
|
|
106
|
+
blockers,
|
|
107
|
+
plan,
|
|
108
|
+
todos,
|
|
109
|
+
changedFileCount: changedFiles.length,
|
|
110
|
+
latestUserMessage: userMessages.at(-1)
|
|
111
|
+
})
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
|
-
export function renderHandoffMarkdown(packet, includeTranscript = true) {
|
|
105
|
-
const normalized = normalizeHandoffPacket(packet);
|
|
106
|
-
const summary = normalized.handoffSummary;
|
|
107
|
-
const sourceLines = packet.sources.map((source) => `- ${source.source}: ${source.path} (${source.modifiedAt})`);
|
|
108
|
-
const session = packet.sourceSession;
|
|
109
|
-
return [
|
|
110
|
-
`# Handoff: ${packet.task}`,
|
|
111
|
-
"",
|
|
112
|
-
`Generated: ${packet.createdAt}`,
|
|
113
|
-
packet.targetAgent ? `Target agent: ${packet.targetAgent}` : "",
|
|
114
|
-
"",
|
|
115
|
-
"## Original Question",
|
|
116
|
-
summary?.originalQuestion || "Not captured.",
|
|
117
|
-
"",
|
|
118
|
-
"## Latest User Instruction",
|
|
119
|
-
summary?.latestUserInstruction || "Not captured.",
|
|
120
|
-
"",
|
|
121
|
-
"## Current State",
|
|
122
|
-
summary ? renderCurrentState(summary) : "Not captured.",
|
|
123
|
-
"",
|
|
124
|
-
"## AI Plan",
|
|
125
|
-
summary?.agentPlan || "Not captured.",
|
|
126
|
-
"",
|
|
127
|
-
"## Goal",
|
|
128
|
-
packet.sections.goal,
|
|
129
|
-
"",
|
|
130
|
-
"## What Changed",
|
|
131
|
-
list(formatFileChanges(summary?.fileChanges || [])),
|
|
132
|
-
"",
|
|
133
|
-
"## Current Status",
|
|
134
|
-
list(packet.sections.currentStatus),
|
|
135
|
-
"",
|
|
136
|
-
"## Completed",
|
|
137
|
-
list(packet.sections.completed),
|
|
138
|
-
"",
|
|
139
|
-
"## In Progress",
|
|
140
|
-
list(packet.sections.inProgress),
|
|
141
|
-
"",
|
|
142
|
-
"## Leftover Work",
|
|
143
|
-
list(summary?.remainingWork || packet.sections.todos),
|
|
144
|
-
"",
|
|
145
|
-
"## Blockers",
|
|
146
|
-
list(packet.sections.blockers),
|
|
147
|
-
"",
|
|
148
|
-
"## Files Touched",
|
|
149
|
-
list(packet.sections.filesTouched),
|
|
150
|
-
"",
|
|
151
|
-
"## Commands Run",
|
|
152
|
-
list(packet.sections.commandsRun),
|
|
153
|
-
"",
|
|
154
|
-
"## Verification",
|
|
155
|
-
summary ? renderVerification(summary.verification) : "- Unknown.",
|
|
156
|
-
"",
|
|
157
|
-
"## Errors and Test Failures",
|
|
158
|
-
list(packet.sections.errors),
|
|
159
|
-
"",
|
|
160
|
-
"## Important Decisions",
|
|
161
|
-
list(packet.sections.decisions),
|
|
162
|
-
"",
|
|
163
|
-
"## Do Not Repeat",
|
|
164
|
-
list(packet.sections.doNotRepeat),
|
|
165
|
-
"",
|
|
166
|
-
"## Engineering Memory",
|
|
167
|
-
normalized.engineeringMemory ? renderEngineeringMemory(normalized.engineeringMemory) : "- Not captured.",
|
|
168
|
-
"",
|
|
169
|
-
"## Next Best Action",
|
|
170
|
-
summary?.nextAction || packet.sections.nextBestAction,
|
|
171
|
-
"",
|
|
172
|
-
"## Source Session",
|
|
173
|
-
...(session
|
|
174
|
-
? [
|
|
175
|
-
`- Agent: ${session.source}`,
|
|
176
|
-
`- Session ID: ${session.id}`,
|
|
177
|
-
`- Session path: ${session.path}`,
|
|
178
|
-
`- Session cwd: ${session.cwd || "unknown"}`,
|
|
179
|
-
`- Matched cwd: ${session.matchedCwd ? "yes" : "no"}`,
|
|
180
|
-
`- Updated: ${session.updatedAt}`,
|
|
181
|
-
`- Transcript items: ${session.transcript.length}`,
|
|
182
|
-
`- Session lines: ${session.lineCount}`,
|
|
183
|
-
...(session.threadName ? [`- Thread: ${session.threadName}`] : []),
|
|
184
|
-
...(session.supplementalPaths?.length ? [`- Supplemental paths: ${session.supplementalPaths.join(", ")}`] : [])
|
|
185
|
-
]
|
|
186
|
-
: sourceLines.length ? sourceLines : ["- None captured."]),
|
|
187
|
-
"",
|
|
188
|
-
...(includeTranscript
|
|
189
|
-
? [
|
|
190
|
-
"## Exported Transcript Excerpt",
|
|
191
|
-
session?.transcriptExcerpt || "No transcript excerpt captured.",
|
|
192
|
-
""
|
|
193
|
-
]
|
|
194
|
-
: []),
|
|
195
|
-
"",
|
|
196
|
-
"## Source Provenance",
|
|
197
|
-
`- Repo: ${packet.repo.repoPath}`,
|
|
198
|
-
`- Branch: ${packet.repo.branch}`,
|
|
199
|
-
`- Commit: ${packet.repo.commit}`,
|
|
200
|
-
`- Git repo: ${packet.repo.isGitRepo ? "yes" : "no"}`,
|
|
201
|
-
...sourceLines,
|
|
202
|
-
""
|
|
203
|
-
]
|
|
204
|
-
.filter((line) => line !== "")
|
|
205
|
-
.join("\n");
|
|
206
|
-
}
|
|
207
|
-
export function normalizeHandoffPacket(packet) {
|
|
208
|
-
const repo = {
|
|
209
|
-
...packet.repo,
|
|
210
|
-
fileChanges: packet.repo.fileChanges?.length ? packet.repo.fileChanges : legacyFileChanges(packet.repo.dirtyFiles || [])
|
|
211
|
-
};
|
|
212
|
-
const derivedSummary = buildHandoffSummary(packet.task, repo, packet.sourceSession, packet.sections);
|
|
213
|
-
const existingSummary = packet.handoffSummary;
|
|
214
|
-
const handoffSummary = existingSummary
|
|
215
|
-
? {
|
|
216
|
-
...derivedSummary,
|
|
217
|
-
...existingSummary,
|
|
218
|
-
fileChanges: existingSummary.fileChanges?.length ? existingSummary.fileChanges : derivedSummary.fileChanges,
|
|
219
|
-
verification: existingSummary.verification || derivedSummary.verification,
|
|
220
|
-
repoStats: existingSummary.repoStats || derivedSummary.repoStats,
|
|
221
|
-
contextSignals: existingSummary.contextSignals?.length ? existingSummary.contextSignals : derivedSummary.contextSignals,
|
|
222
|
-
sourceSession: existingSummary.sourceSession || derivedSummary.sourceSession,
|
|
223
|
-
repo: existingSummary.repo || derivedSummary.repo
|
|
224
|
-
}
|
|
225
|
-
: derivedSummary;
|
|
226
|
-
const engineeringMemory = packet.engineeringMemory || buildEngineeringMemory(packet.task, repo, packet.sourceSession, packet.id, handoffSummary);
|
|
227
|
-
return { ...packet, repo, handoffSummary, engineeringMemory };
|
|
228
|
-
}
|
|
229
114
|
export function buildHandoffSummary(task, repo, sourceSession, sections) {
|
|
230
115
|
const fileChanges = repo.fileChanges?.length ? repo.fileChanges : legacyFileChanges(repo.dirtyFiles || []);
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
]);
|
|
240
|
-
const inProgressWork = meaningfulList(sections.inProgress.filter((item) => !/^Work appears active around /i.test(item)), fileChanges.length ? [`Active work is concentrated in ${fileChanges.length} changed file(s).`] : []);
|
|
116
|
+
const userMessages = sourceSession?.transcript.filter((entry) => entry.role === "user").map((entry) => entry.text) || [];
|
|
117
|
+
const assistantMessages = sourceSession?.transcript.filter((entry) => entry.role === "assistant").map((entry) => entry.text) || [];
|
|
118
|
+
const verification = summarizeVerification(sourceSession, sections.commandsRun);
|
|
119
|
+
const plan = sourceSession?.plan;
|
|
120
|
+
const agentPlan = sourceSession?.planText || (plan ? plan.map(formatPlanItem).join("\n") : latestPlanMessage(assistantMessages));
|
|
121
|
+
const originalQuestion = truncate(userMessages[0] || "", 1_500);
|
|
122
|
+
const latestUserInstruction = truncate(userMessages.at(-1) || "", 1_500);
|
|
123
|
+
const latestAssistantSummary = truncate(assistantMessages.at(-1) || "", 1_500);
|
|
241
124
|
return {
|
|
242
125
|
version: 1,
|
|
126
|
+
title: sourceSession?.threadName,
|
|
243
127
|
originalQuestion: originalQuestion || `Continue the ${task} task.`,
|
|
244
128
|
latestUserInstruction: latestUserInstruction || originalQuestion || `Continue the ${task} task.`,
|
|
245
|
-
latestAssistantSummary
|
|
246
|
-
agentPlan
|
|
247
|
-
workPhase: inferWorkPhase(sections, verification, agentPlan, fileChanges),
|
|
129
|
+
latestAssistantSummary,
|
|
130
|
+
agentPlan,
|
|
131
|
+
workPhase: inferWorkPhase(sections, verification, plan, agentPlan, fileChanges),
|
|
248
132
|
verification,
|
|
249
|
-
repoStats,
|
|
133
|
+
repoStats: summarizeRepoStats(fileChanges),
|
|
250
134
|
contextSignals: buildContextSignals(sections, latestUserInstruction || originalQuestion, latestAssistantSummary),
|
|
251
|
-
completedWork:
|
|
252
|
-
inProgressWork,
|
|
253
|
-
remainingWork,
|
|
135
|
+
completedWork: sections.completed,
|
|
136
|
+
inProgressWork: sections.inProgress,
|
|
137
|
+
remainingWork: sections.todos,
|
|
254
138
|
blockers: sections.blockers,
|
|
255
139
|
nextAction: sections.nextBestAction,
|
|
256
140
|
fileChanges,
|
|
257
141
|
commandsRun: sections.commandsRun,
|
|
258
142
|
errors: sections.errors,
|
|
259
143
|
decisions: sections.decisions,
|
|
144
|
+
constraints: sections.doNotRepeat,
|
|
145
|
+
plan,
|
|
260
146
|
sourceSession: sourceSession
|
|
261
147
|
? {
|
|
262
148
|
source: sourceSession.source,
|
|
@@ -275,306 +161,108 @@ export function buildHandoffSummary(task, repo, sourceSession, sections) {
|
|
|
275
161
|
}
|
|
276
162
|
};
|
|
277
163
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"
|
|
310
|
-
summary ? renderVerification(summary.verification) : "- Unknown.",
|
|
311
|
-
"",
|
|
312
|
-
"## Blockers",
|
|
313
|
-
list(summary?.blockers || []),
|
|
314
|
-
"",
|
|
315
|
-
"## Files Changed",
|
|
316
|
-
list(formatFileChanges(summary?.fileChanges || [])),
|
|
317
|
-
"",
|
|
318
|
-
"## Commands Run",
|
|
319
|
-
list(summary?.commandsRun || []),
|
|
320
|
-
"",
|
|
321
|
-
"## Errors",
|
|
322
|
-
list(summary?.errors || []),
|
|
323
|
-
"",
|
|
324
|
-
"## Engineering Memory",
|
|
325
|
-
normalized.engineeringMemory ? renderEngineeringMemory(normalized.engineeringMemory) : "- Not captured.",
|
|
326
|
-
"",
|
|
327
|
-
"## Next Action",
|
|
328
|
-
summary?.nextAction || normalized.sections.nextBestAction
|
|
329
|
-
].join("\n");
|
|
330
|
-
}
|
|
331
|
-
export function buildAgentContext(packet) {
|
|
332
|
-
const normalized = normalizeHandoffPacket(packet);
|
|
333
|
-
return {
|
|
334
|
-
markdown: renderAgentContextMarkdown(normalized),
|
|
335
|
-
summary: normalized.handoffSummary,
|
|
336
|
-
engineeringMemory: normalized.engineeringMemory,
|
|
337
|
-
packet: normalized
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
export function deriveContextBlocks(packet) {
|
|
341
|
-
const normalized = normalizeHandoffPacket(packet);
|
|
342
|
-
const summary = normalized.handoffSummary;
|
|
343
|
-
const memory = normalized.engineeringMemory;
|
|
344
|
-
const source = `${summary.sourceSession?.source || "handoff"}:${summary.sourceSession?.id || normalized.id}`;
|
|
345
|
-
const blocks = [];
|
|
346
|
-
const push = (category, title, text, provenance, confidence, provenanceSource = source) => {
|
|
347
|
-
const clean = cleanContextText(text);
|
|
348
|
-
if (!clean || /^None captured\.?$/i.test(clean) || /^Not captured\.?$/i.test(clean))
|
|
349
|
-
return;
|
|
350
|
-
blocks.push({
|
|
351
|
-
id: stableId(`${normalized.id}:${category}`, `${title}:${clean}`),
|
|
352
|
-
sourceHandoffId: normalized.id,
|
|
353
|
-
sourceTask: normalized.task,
|
|
354
|
-
sourceCreatedAt: normalized.createdAt,
|
|
355
|
-
category,
|
|
356
|
-
title,
|
|
357
|
-
text: clean,
|
|
358
|
-
provenance: { kind: provenance, confidence, source: provenanceSource },
|
|
359
|
-
estimatedChars: clean.length
|
|
360
|
-
});
|
|
361
|
-
};
|
|
362
|
-
push("intent", "Original question", summary.originalQuestion, "user-authored", "high");
|
|
363
|
-
push("instruction", "Latest user instruction", summary.latestUserInstruction, "user-authored", "high");
|
|
364
|
-
push("next-action", "Next action", summary.nextAction, "ai-inferred", "medium");
|
|
365
|
-
for (const fact of memory.decisions)
|
|
366
|
-
push("decision", "Decision", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
367
|
-
for (const fact of memory.constraints)
|
|
368
|
-
push("constraint", "Constraint", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
369
|
-
for (const fact of memory.verification)
|
|
370
|
-
push("verification", "Verification", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
371
|
-
for (const fact of memory.risks)
|
|
372
|
-
push("risk", "Risk", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
373
|
-
for (const fact of memory.nextActions.slice(1))
|
|
374
|
-
push("next-action", "Remaining work", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
375
|
-
for (const fact of memory.changedFiles)
|
|
376
|
-
push("changed-file", "Changed file", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
377
|
-
for (const entry of normalized.sourceSession?.transcript || []) {
|
|
378
|
-
if (entry.role === "tool")
|
|
379
|
-
continue;
|
|
380
|
-
const text = cleanContextText(entry.text);
|
|
381
|
-
if (!text || text.length < 20)
|
|
382
|
-
continue;
|
|
383
|
-
push("transcript", `${labelize(entry.role)} transcript`, text, "transcript-derived", "medium", source);
|
|
164
|
+
const COMPLETED = /^(?:done|completed|fixed|implemented|added|created|updated|refactored|removed|shipped|wrote)\b|\b(?:i(?:'ve| have)|we(?:'ve| have)|now)\s+(?:added|fixed|implemented|updated|created|removed|refactored|wired)\b/i;
|
|
165
|
+
const TODO = /\b(?:todo|still need(?:s)? to|left to do|not yet (?:done|implemented|handled)|remaining(?: work)?:|follow[- ]?up:|next step)/i;
|
|
166
|
+
const BLOCKER = /\b(?:blocked|blocker|stuck|cannot proceed|can't proceed|unable to (?:proceed|continue)|waiting (?:on|for) (?:you|the user|approval|access)|need(?:s)? (?:your|user) (?:input|approval|confirmation)|permission denied|is missing|not configured|missing (?:credentials|access|env))\b/i;
|
|
167
|
+
const DECISION = /^(?:decision|decided)\b|\b(?:decided to|chose to|opted to|going with|we(?:'ll| will) use|i(?:'ll| will) use|instead of)\b/i;
|
|
168
|
+
const CONSTRAINT = /\b(?:don't|do not|never|always|must(?! have)|should not|shouldn't|avoid|make sure|only use|without)\b/i;
|
|
169
|
+
const DEAD_END = /\b(?:already tried|didn't work|doesn't work|did not work|reverted|rolled back)\b/i;
|
|
170
|
+
const SHELL_TOOL = /\b(npm|pnpm|yarn|bun|git|node|tsx|tsc|vitest|pytest|cargo|go test)\b/i;
|
|
171
|
+
function pick(snippets, pattern, count) {
|
|
172
|
+
return limit(snippets.filter((snippet) => pattern.test(snippet)).reverse().map((snippet) => truncate(snippet, 300)), count);
|
|
173
|
+
}
|
|
174
|
+
function sentences(text) {
|
|
175
|
+
return text
|
|
176
|
+
.split(/\n+|(?<=[.!?])\s+(?=[A-Z0-9`*])/)
|
|
177
|
+
.map((part) => part.replace(/^[-*>\d.)\s]+/, "").replace(/\*\*/g, "").trim())
|
|
178
|
+
.filter((part) => part.length > 8 && part.length < 600 && !/^```/.test(part));
|
|
179
|
+
}
|
|
180
|
+
function chooseNextAction(input) {
|
|
181
|
+
const failed = input.verification.commands.find((command) => command.status === "failed");
|
|
182
|
+
if (failed)
|
|
183
|
+
return `Fix the failing \`${failed.command}\`${failed.evidence ? ` (${failed.evidence})` : ""}, then re-run it.`;
|
|
184
|
+
if (input.blockers[0])
|
|
185
|
+
return `Resolve the blocker: ${input.blockers[0]}`;
|
|
186
|
+
const active = input.plan?.find((item) => item.status === "in_progress");
|
|
187
|
+
if (active)
|
|
188
|
+
return `Continue: ${active.text}`;
|
|
189
|
+
const pending = input.plan?.find((item) => item.status === "pending");
|
|
190
|
+
if (pending)
|
|
191
|
+
return `Next: ${pending.text}`;
|
|
192
|
+
const unverified = input.verification.gaps.some((gap) => /edited after|No tests/.test(gap));
|
|
193
|
+
if (input.changedFileCount && unverified) {
|
|
194
|
+
const checks = unique(input.verification.commands.map((command) => `\`${command.command}\``)).slice(-2);
|
|
195
|
+
return `Verify the ${input.changedFileCount} changed file(s) with ${checks.length ? checks.join(" and ") : "the project's tests and type checks"}.`;
|
|
384
196
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
items: enabledItems
|
|
423
|
-
},
|
|
424
|
-
estimatedChars,
|
|
425
|
-
warnings
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
export function normalizeContextPack(pack) {
|
|
429
|
-
const includeRawTranscript = pack.includeRawTranscript === true;
|
|
430
|
-
return {
|
|
431
|
-
...pack,
|
|
432
|
-
includeRawTranscript,
|
|
433
|
-
name: pack.name.trim() || "Untitled context pack",
|
|
434
|
-
items: pack.items
|
|
435
|
-
.map((item, index) => ({
|
|
436
|
-
...item,
|
|
437
|
-
order: Number.isFinite(item.order) ? item.order : index,
|
|
438
|
-
enabled: item.enabled !== false,
|
|
439
|
-
block: sanitizeBlockForPack(item.block, includeRawTranscript)
|
|
440
|
-
}))
|
|
441
|
-
.sort((a, b) => a.order - b.order)
|
|
442
|
-
.map((item, index) => ({ ...item, order: index }))
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
export function buildTeamTimelineExport(packet, options = {}) {
|
|
446
|
-
const normalized = normalizeHandoffPacket(packet);
|
|
447
|
-
const includeRawTranscript = options.includeRawTranscript === true;
|
|
448
|
-
const sourceSession = normalized.sourceSession
|
|
449
|
-
? {
|
|
450
|
-
...normalized.sourceSession,
|
|
451
|
-
transcript: includeRawTranscript ? normalized.sourceSession.transcript : [],
|
|
452
|
-
transcriptExcerpt: includeRawTranscript ? normalized.sourceSession.transcriptExcerpt : "Raw transcript excluded from team sync export."
|
|
453
|
-
}
|
|
454
|
-
: undefined;
|
|
455
|
-
const sources = includeRawTranscript
|
|
456
|
-
? normalized.sources
|
|
457
|
-
: normalized.sources.map((source) => ({ ...source, snippets: [] }));
|
|
458
|
-
const engineeringMemory = {
|
|
459
|
-
...normalized.engineeringMemory,
|
|
460
|
-
teamSync: {
|
|
461
|
-
mode: "sync-ready",
|
|
462
|
-
rawTranscript: includeRawTranscript ? "included" : "excluded",
|
|
463
|
-
filePreviewRedaction: "enabled",
|
|
464
|
-
secretsRedaction: "enabled",
|
|
465
|
-
userControlledSync: true
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const exportPacket = {
|
|
469
|
-
...normalized,
|
|
470
|
-
sourceSession,
|
|
471
|
-
sources,
|
|
472
|
-
engineeringMemory,
|
|
473
|
-
markdown: "",
|
|
474
|
-
markdownPath: normalized.markdownPath
|
|
475
|
-
};
|
|
476
|
-
return {
|
|
477
|
-
...exportPacket,
|
|
478
|
-
markdown: renderHandoffMarkdown(exportPacket, includeRawTranscript)
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
function match(snippets, pattern, count) {
|
|
482
|
-
return limit(snippets.filter((snippet) => pattern.test(snippet)).reverse(), count);
|
|
483
|
-
}
|
|
484
|
-
function list(values) {
|
|
485
|
-
const clean = unique(values);
|
|
486
|
-
if (clean.length === 0)
|
|
487
|
-
return "- None captured.";
|
|
488
|
-
return clean.map((value) => `- ${value}`).join("\n");
|
|
489
|
-
}
|
|
490
|
-
function sourceSessionToSignals(session) {
|
|
491
|
-
return [
|
|
492
|
-
{
|
|
493
|
-
source: session.source,
|
|
494
|
-
path: session.path,
|
|
495
|
-
modifiedAt: session.updatedAt,
|
|
496
|
-
snippets: session.transcript.map((entry) => entry.text)
|
|
497
|
-
}
|
|
498
|
-
];
|
|
499
|
-
}
|
|
500
|
-
function firstUsefulUserMessage(session) {
|
|
501
|
-
return cleanLongText(session.transcript.find((entry) => entry.role === "user" && toHandoffSignal(entry.text))?.text || "");
|
|
502
|
-
}
|
|
503
|
-
function latestUsefulUserMessage(session) {
|
|
504
|
-
const candidates = session.transcript
|
|
505
|
-
.filter((entry) => entry.role === "user")
|
|
506
|
-
.map((entry) => cleanLongText(entry.text))
|
|
507
|
-
.filter(Boolean);
|
|
508
|
-
return candidates.at(-1) || "";
|
|
509
|
-
}
|
|
510
|
-
function latestAssistantMessage(session) {
|
|
511
|
-
const candidates = session.transcript
|
|
512
|
-
.filter((entry) => entry.role === "assistant")
|
|
513
|
-
.map((entry) => cleanLongText(entry.text))
|
|
514
|
-
.filter(Boolean);
|
|
515
|
-
return candidates.at(-1) || "";
|
|
516
|
-
}
|
|
517
|
-
function latestPlanMessage(session) {
|
|
518
|
-
const candidates = session.transcript
|
|
519
|
-
.filter((entry) => entry.role === "assistant")
|
|
520
|
-
.map((entry) => cleanLongText(entry.text))
|
|
521
|
-
.filter((text) => /(plan|steps|approach|implementation|todo|next|will|going to|checklist)/i.test(text));
|
|
522
|
-
return candidates.at(-1) || "";
|
|
523
|
-
}
|
|
524
|
-
function cleanLongText(text) {
|
|
525
|
-
const clean = toHandoffSignal(text).replace(/\s+/g, " ").trim();
|
|
526
|
-
return clean.length > 1200 ? `${clean.slice(0, 1200)}...` : clean;
|
|
527
|
-
}
|
|
528
|
-
function meaningfulList(values, fallback) {
|
|
529
|
-
const clean = unique(values.filter((value) => !/None captured/i.test(value)));
|
|
530
|
-
return clean.length ? clean : fallback;
|
|
531
|
-
}
|
|
532
|
-
function inferVerification(sections, sourceSession) {
|
|
533
|
-
const transcriptText = sourceSession?.transcript.map((entry) => entry.text).join("\n") || "";
|
|
534
|
-
const evidence = [...sections.commandsRun, ...sections.errors, transcriptText].join("\n");
|
|
535
|
-
const verificationCommands = unique(sections.commandsRun.filter((command) => /\b(npm (run )?(test|typecheck|build)|pnpm (test|build)|yarn (test|build)|bun test|vitest|jest|tsc|pytest|cargo test|go test)\b/i.test(command)));
|
|
536
|
-
const commands = verificationCommands.map((command) => ({
|
|
197
|
+
if (input.todos[0])
|
|
198
|
+
return input.todos[0];
|
|
199
|
+
if (input.latestUserMessage)
|
|
200
|
+
return `Confirm the latest request is fully handled: “${truncate(oneLine(input.latestUserMessage), 160)}”`;
|
|
201
|
+
return "Review the current diff and continue the task.";
|
|
202
|
+
}
|
|
203
|
+
function summarizeVerification(session, legacyEvidence) {
|
|
204
|
+
const runs = (session?.commands || []).filter((run) => isVerificationCommand(run.command));
|
|
205
|
+
if (!session?.commands)
|
|
206
|
+
return legacyVerification(legacyEvidence);
|
|
207
|
+
const latest = new Map();
|
|
208
|
+
for (const run of runs)
|
|
209
|
+
latest.set(verificationKey(run.command), run);
|
|
210
|
+
const latestRuns = [...latest.values()].sort((a, b) => a.seq - b.seq);
|
|
211
|
+
const commands = latestRuns.map((run) => ({
|
|
212
|
+
command: truncate(verificationLabel(run.command), 160),
|
|
213
|
+
status: run.status,
|
|
214
|
+
exitCode: run.exitCode,
|
|
215
|
+
evidence: run.errorLine
|
|
216
|
+
}));
|
|
217
|
+
const failed = commands.some((command) => command.status === "failed");
|
|
218
|
+
const passed = commands.length > 0 && commands.every((command) => command.status === "passed");
|
|
219
|
+
const lastEdit = Math.max(-1, ...(session.edits || []).map((edit) => edit.seq));
|
|
220
|
+
const lastPass = Math.max(-1, ...latestRuns.filter((run) => run.status === "passed").map((run) => run.seq));
|
|
221
|
+
const stale = passed && lastEdit > lastPass;
|
|
222
|
+
const gaps = [];
|
|
223
|
+
if (!commands.length)
|
|
224
|
+
gaps.push("No tests, type checks, or builds were run in the session.");
|
|
225
|
+
else if (!failed && !passed)
|
|
226
|
+
gaps.push("Some checks ran without a recorded result.");
|
|
227
|
+
if (stale)
|
|
228
|
+
gaps.push("Files were edited after the last passing check.");
|
|
229
|
+
return { status: failed ? "failed" : passed && !stale ? "passed" : "unknown", commands, gaps };
|
|
230
|
+
}
|
|
231
|
+
/** Packets and inputs without structured command data: look for verification commands in text. */
|
|
232
|
+
function legacyVerification(evidence) {
|
|
233
|
+
const commands = unique(evidence.filter(isVerificationCommand)).map((command) => ({
|
|
537
234
|
command,
|
|
538
|
-
status:
|
|
539
|
-
evidence: matchingEvidence(command, evidence)
|
|
235
|
+
status: /\b(failed|failure|error)\b/i.test(command) ? "failed" : /\b(passed|succeeded)\b/i.test(command) ? "passed" : "unknown"
|
|
540
236
|
}));
|
|
541
|
-
const failed = commands.some((command) => command.status === "failed")
|
|
237
|
+
const failed = commands.some((command) => command.status === "failed");
|
|
542
238
|
const passed = commands.length > 0 && commands.every((command) => command.status === "passed");
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
function inferCommandStatus(command, evidence) {
|
|
552
|
-
const commandName = command.replace(/\s+/g, " ").slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
553
|
-
const nearCommand = new RegExp(`${commandName}[\\s\\S]{0,500}`, "i").exec(evidence)?.[0] || evidence;
|
|
554
|
-
if (/(failed|failure|error|exception|exit code [1-9]|Process exited with code [1-9])/i.test(nearCommand))
|
|
555
|
-
return "failed";
|
|
556
|
-
if (/(passed|success|succeeded|0 failed|Process exited with code 0|✓|✔)/i.test(nearCommand))
|
|
557
|
-
return "passed";
|
|
558
|
-
return "unknown";
|
|
559
|
-
}
|
|
560
|
-
function matchingEvidence(command, evidence) {
|
|
561
|
-
const commandWords = command.split(/\s+/).filter((word) => word.length > 2).slice(0, 3);
|
|
562
|
-
const line = evidence.split("\n").find((candidate) => commandWords.some((word) => candidate.includes(word)));
|
|
563
|
-
return line ? cleanLongText(line).slice(0, 240) : undefined;
|
|
564
|
-
}
|
|
565
|
-
function inferWorkPhase(sections, verification, agentPlan, fileChanges) {
|
|
566
|
-
if (sections.blockers.length > 0)
|
|
239
|
+
return {
|
|
240
|
+
status: failed ? "failed" : passed ? "passed" : "unknown",
|
|
241
|
+
commands,
|
|
242
|
+
gaps: commands.length ? (failed || passed ? [] : ["Some checks ran without a recorded result."]) : ["No tests, type checks, or builds were run in the session."]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function inferWorkPhase(sections, verification, plan, agentPlan, fileChanges) {
|
|
246
|
+
if (sections.blockers.length)
|
|
567
247
|
return "blocked";
|
|
568
|
-
if (verification.status === "failed"
|
|
248
|
+
if (verification.status === "failed")
|
|
569
249
|
return "debugging";
|
|
570
|
-
if (
|
|
250
|
+
if (plan?.some((item) => item.status !== "completed"))
|
|
251
|
+
return plan.some((item) => item.status !== "pending") || fileChanges.length ? "implementation" : "planning";
|
|
252
|
+
if (verification.status === "passed")
|
|
253
|
+
return "review";
|
|
254
|
+
if (verification.commands.length)
|
|
571
255
|
return "verification";
|
|
572
|
-
if (sections.todos.length
|
|
256
|
+
if (sections.todos.length || fileChanges.length)
|
|
573
257
|
return "implementation";
|
|
574
258
|
if (agentPlan)
|
|
575
259
|
return "planning";
|
|
576
260
|
return "handoff";
|
|
577
261
|
}
|
|
262
|
+
function latestPlanMessage(assistantMessages) {
|
|
263
|
+
const plan = [...assistantMessages].reverse().find((text) => /^(?:plan|here'?s (?:the|my) plan|steps?|approach)\b\s*:?/im.test(text));
|
|
264
|
+
return plan ? truncate(plan, 1_500) : "";
|
|
265
|
+
}
|
|
578
266
|
function summarizeRepoStats(fileChanges) {
|
|
579
267
|
return fileChanges.reduce((stats, file) => ({
|
|
580
268
|
dirtyFiles: stats.dirtyFiles + 1,
|
|
@@ -589,18 +277,57 @@ function buildContextSignals(sections, latestUserInstruction, latestAssistantSum
|
|
|
589
277
|
const signals = [];
|
|
590
278
|
for (const text of limit(sections.decisions, 4))
|
|
591
279
|
signals.push({ kind: "decision", text });
|
|
592
|
-
for (const text of limit(sections.doNotRepeat,
|
|
280
|
+
for (const text of limit(sections.doNotRepeat, 4))
|
|
593
281
|
signals.push({ kind: "constraint", text });
|
|
594
282
|
for (const text of limit([...sections.blockers, ...sections.errors], 4))
|
|
595
283
|
signals.push({ kind: "risk", text });
|
|
596
284
|
if (latestUserInstruction)
|
|
597
|
-
signals.push({ kind: "anchor", text: `Latest user instruction: ${latestUserInstruction}` });
|
|
285
|
+
signals.push({ kind: "anchor", text: `Latest user instruction: ${truncate(latestUserInstruction, 300)}` });
|
|
598
286
|
if (latestAssistantSummary)
|
|
599
|
-
signals.push({ kind: "anchor", text: `Latest
|
|
600
|
-
return signals.slice(0,
|
|
287
|
+
signals.push({ kind: "anchor", text: `Latest agent update: ${truncate(latestAssistantSummary, 300)}` });
|
|
288
|
+
return signals.slice(0, 12);
|
|
289
|
+
}
|
|
290
|
+
function extractFilePaths(snippets) {
|
|
291
|
+
const pattern = /(?:^|[\s`'"(])((?:\.{0,2}\/)?(?:[\w.-]+\/)*[\w.-]+\.(?:tsx?|jsx?|mjs|cjs|json|md|ya?ml|toml|sql|sh|py|go|rs|rb|java|kt|swift|css|html))(?=$|[\s`'"),.;:!?])/g;
|
|
292
|
+
return unique(snippets.flatMap((snippet) => Array.from(snippet.matchAll(pattern), (match) => match[1])));
|
|
293
|
+
}
|
|
294
|
+
function withRepoRelativeEdits(session, repoPath) {
|
|
295
|
+
const root = canonicalPath(repoPath);
|
|
296
|
+
const edits = (session.edits || []).map((edit) => {
|
|
297
|
+
if (!isAbsolute(edit.path))
|
|
298
|
+
return edit;
|
|
299
|
+
const rel = relative(root, canonicalPath(edit.path));
|
|
300
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? { ...edit, path: rel } : edit;
|
|
301
|
+
});
|
|
302
|
+
return { ...session, edits };
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Normalization (packets saved by older versions)
|
|
306
|
+
export function normalizeHandoffPacket(packet) {
|
|
307
|
+
const repo = {
|
|
308
|
+
...packet.repo,
|
|
309
|
+
fileChanges: packet.repo.fileChanges?.length ? packet.repo.fileChanges : legacyFileChanges(packet.repo.dirtyFiles || [])
|
|
310
|
+
};
|
|
311
|
+
const derived = buildHandoffSummary(packet.task, repo, packet.sourceSession, packet.sections);
|
|
312
|
+
const existing = packet.handoffSummary;
|
|
313
|
+
const handoffSummary = existing
|
|
314
|
+
? {
|
|
315
|
+
...derived,
|
|
316
|
+
...existing,
|
|
317
|
+
fileChanges: existing.fileChanges?.length ? existing.fileChanges : derived.fileChanges,
|
|
318
|
+
verification: existing.verification || derived.verification,
|
|
319
|
+
repoStats: existing.repoStats || derived.repoStats,
|
|
320
|
+
contextSignals: existing.contextSignals?.length ? existing.contextSignals : derived.contextSignals,
|
|
321
|
+
sourceSession: existing.sourceSession || derived.sourceSession,
|
|
322
|
+
repo: existing.repo || derived.repo
|
|
323
|
+
}
|
|
324
|
+
: derived;
|
|
325
|
+
const engineeringMemory = packet.engineeringMemory || buildEngineeringMemory(packet.task, repo, packet.sourceSession, packet.id, handoffSummary);
|
|
326
|
+
return { ...packet, repo, handoffSummary, engineeringMemory };
|
|
601
327
|
}
|
|
602
328
|
export function buildEngineeringMemory(task, repo, sourceSession, handoffId, summary) {
|
|
603
329
|
const sourceLabel = sourceSession ? `${sourceSession.source}:${sourceSession.id}` : "local-capture";
|
|
330
|
+
const verified = summary.verification.commands.length > 0;
|
|
604
331
|
return {
|
|
605
332
|
version: 1,
|
|
606
333
|
identity: {
|
|
@@ -612,19 +339,12 @@ export function buildEngineeringMemory(task, repo, sourceSession, handoffId, sum
|
|
|
612
339
|
sessionId: sourceSession?.id,
|
|
613
340
|
sourceAgent: sourceSession?.source
|
|
614
341
|
},
|
|
615
|
-
positioning: {
|
|
616
|
-
category: "handoff-ledger",
|
|
617
|
-
buyer: "ai-heavy-engineering-teams",
|
|
618
|
-
moat: "provenance-backed-engineering-memory"
|
|
619
|
-
},
|
|
620
342
|
intent: facts("intent", [summary.originalQuestion, summary.latestUserInstruction], "user-authored", "high", sourceLabel),
|
|
621
343
|
decisions: facts("decision", summary.decisions, "transcript-derived", "medium", sourceLabel),
|
|
622
|
-
constraints: facts("constraint", summary.
|
|
623
|
-
verification: facts("verification", summary.verification.commands.
|
|
624
|
-
? summary.verification.commands.map((command) => `${command.status}: ${command.command}`)
|
|
625
|
-
: summary.verification.gaps, "command-derived", summary.verification.commands.length ? "high" : "low", sourceLabel),
|
|
344
|
+
constraints: facts("constraint", summary.constraints || [], "user-authored", "medium", sourceLabel),
|
|
345
|
+
verification: facts("verification", verified ? summary.verification.commands.map((command) => `${command.status}: ${command.command}`) : summary.verification.gaps, "command-derived", verified ? "high" : "low", sourceLabel),
|
|
626
346
|
risks: facts("risk", [...summary.blockers, ...summary.errors], "transcript-derived", "medium", sourceLabel),
|
|
627
|
-
nextActions: facts("next-action", [summary.nextAction, ...summary.remainingWork], "ai-inferred", "medium", sourceLabel),
|
|
347
|
+
nextActions: facts("next-action", [summary.nextAction, ...summary.remainingWork], summary.plan ? "transcript-derived" : "ai-inferred", "medium", sourceLabel),
|
|
628
348
|
changedFiles: facts("changed-file", formatFileChanges(summary.fileChanges), "diff-derived", "high", repo.repoPath),
|
|
629
349
|
teamSync: {
|
|
630
350
|
mode: "local-only",
|
|
@@ -637,232 +357,247 @@ export function buildEngineeringMemory(task, repo, sourceSession, handoffId, sum
|
|
|
637
357
|
}
|
|
638
358
|
function facts(category, values, provenanceKind, confidence, source) {
|
|
639
359
|
return unique(values)
|
|
640
|
-
.filter((value) => !/^None captured\.?$/i.test(value))
|
|
641
360
|
.slice(0, 12)
|
|
642
|
-
.map((text) => ({
|
|
643
|
-
id: stableId(category, text),
|
|
644
|
-
text,
|
|
645
|
-
provenance: {
|
|
646
|
-
kind: provenanceKind,
|
|
647
|
-
confidence,
|
|
648
|
-
source
|
|
649
|
-
}
|
|
650
|
-
}));
|
|
361
|
+
.map((text) => ({ id: stableId(category, text), text, provenance: { kind: provenanceKind, confidence, source } }));
|
|
651
362
|
}
|
|
652
363
|
function stableId(prefix, seed) {
|
|
653
364
|
return `${prefix}_${hashText(seed || prefix)}`;
|
|
654
365
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Rendering
|
|
368
|
+
/**
|
|
369
|
+
* The handoff an agent reads. Ordered by what the next agent needs first; empty sections are omitted.
|
|
370
|
+
*/
|
|
371
|
+
export function renderHandoffMarkdown(packet, includeTranscript = true) {
|
|
372
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
373
|
+
const summary = normalized.handoffSummary;
|
|
374
|
+
const session = normalized.sourceSession;
|
|
375
|
+
const stats = summary.repoStats;
|
|
376
|
+
const meta = [
|
|
377
|
+
basename(normalized.repo.repoPath),
|
|
378
|
+
normalized.repo.isGitRepo ? `${normalized.repo.branch} @ ${normalized.repo.commit}` : "not a git repo",
|
|
379
|
+
stats.dirtyFiles ? `${stats.dirtyFiles} changed ${stats.dirtyFiles === 1 ? "file" : "files"} (+${stats.additions} -${stats.deletions})` : "clean tree",
|
|
380
|
+
session ? `${sourceLabel(session.source)} session` : undefined,
|
|
381
|
+
normalized.createdAt.slice(0, 16).replace("T", " ")
|
|
382
|
+
].filter(Boolean);
|
|
383
|
+
const lines = [
|
|
384
|
+
`# Handoff: ${normalized.task}`,
|
|
385
|
+
"",
|
|
386
|
+
`> ${meta.join(" · ")}`,
|
|
387
|
+
"",
|
|
388
|
+
`${normalized.targetAgent ? `For ${normalized.targetAgent}: c` : "C"}ontinue this task. Check that the repo matches the state below, then start from **Next action**.`
|
|
389
|
+
];
|
|
390
|
+
const section = (title, body) => {
|
|
391
|
+
const content = Array.isArray(body) ? body.filter(Boolean).join("\n") : body?.trim();
|
|
392
|
+
if (content)
|
|
393
|
+
lines.push("", `## ${title}`, content);
|
|
394
|
+
};
|
|
395
|
+
const objective = summary.originalQuestion;
|
|
396
|
+
section("Objective", objective);
|
|
397
|
+
if (summary.latestUserInstruction && summary.latestUserInstruction !== objective)
|
|
398
|
+
section("Latest request", summary.latestUserInstruction);
|
|
399
|
+
section("Next action", summary.nextAction);
|
|
400
|
+
section("Status", [
|
|
673
401
|
`- Phase: ${summary.workPhase}`,
|
|
674
|
-
`-
|
|
675
|
-
`-
|
|
676
|
-
|
|
677
|
-
|
|
402
|
+
`- Verification: ${summary.verification.status}`,
|
|
403
|
+
summary.latestAssistantSummary ? `- Last update from the agent: ${truncate(oneLine(summary.latestAssistantSummary), 600)}` : ""
|
|
404
|
+
]);
|
|
405
|
+
section("Plan", summary.plan?.length ? summary.plan.map(formatPlanItem) : summary.agentPlan && !summary.plan ? truncate(summary.agentPlan, 1_500) : undefined);
|
|
406
|
+
section("Verification", renderVerification(summary.verification));
|
|
407
|
+
section("Changed files", renderFileChanges(summary.fileChanges));
|
|
408
|
+
section("Decisions", bullets(summary.decisions, 6));
|
|
409
|
+
section("Constraints", bullets(summary.constraints || normalized.sections.doNotRepeat, 6));
|
|
410
|
+
section("Blockers", bullets(summary.blockers, 6));
|
|
411
|
+
if (!summary.verification.commands.length)
|
|
412
|
+
section("Errors", bullets(summary.errors, 6));
|
|
413
|
+
section("Earlier context", session?.compactSummary ? truncate(session.compactSummary, 2_000) : undefined);
|
|
414
|
+
section("Links", bullets(session?.links || [], 5));
|
|
415
|
+
if (includeTranscript)
|
|
416
|
+
section("Recent transcript", session?.transcriptExcerpt);
|
|
417
|
+
lines.push("", "---", `Source: ${session ? `${sourceLabel(session.source)} session ${session.id}${session.threadName ? ` (“${session.threadName}”)` : ""}` : "local capture"}`, `Repo: ${normalized.repo.repoPath}`, `Generated by ContextPool ${VERSION} at ${normalized.createdAt}`);
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
420
|
+
/** Import-ready context for an agent: the handoff without the transcript, plus the structured summary. */
|
|
421
|
+
export function buildAgentContext(packet) {
|
|
422
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
423
|
+
return {
|
|
424
|
+
markdown: renderHandoffMarkdown(normalized, false),
|
|
425
|
+
summary: normalized.handoffSummary,
|
|
426
|
+
engineeringMemory: normalized.engineeringMemory
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
export function renderAgentContextMarkdown(packet) {
|
|
430
|
+
return renderHandoffMarkdown(packet, false);
|
|
678
431
|
}
|
|
679
432
|
function renderVerification(verification) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
433
|
+
const glyph = { passed: "✓", failed: "✗", unknown: "?" };
|
|
434
|
+
return [
|
|
435
|
+
...verification.commands.map((command) => {
|
|
436
|
+
const exit = command.status === "failed" && command.exitCode ? ` (exit ${command.exitCode})` : command.status === "unknown" ? " (result not recorded)" : "";
|
|
437
|
+
return `- ${glyph[command.status]} \`${command.command}\`${exit}${command.evidence ? ` — ${command.evidence}` : ""}`;
|
|
438
|
+
}),
|
|
439
|
+
...verification.gaps.map((gap) => `- Gap: ${gap}`)
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
function renderFileChanges(changes) {
|
|
443
|
+
const lines = formatFileChanges(changes.slice(0, 30)).map((line) => `- ${line}`);
|
|
444
|
+
if (changes.length > 30)
|
|
445
|
+
lines.push(`- …and ${changes.length - 30} more`);
|
|
446
|
+
return lines;
|
|
447
|
+
}
|
|
448
|
+
function bullets(values, max) {
|
|
449
|
+
return unique(values)
|
|
450
|
+
.slice(0, max)
|
|
451
|
+
.map((value) => `- ${truncate(oneLine(value), 300)}`);
|
|
452
|
+
}
|
|
453
|
+
function formatPlanItem(item) {
|
|
454
|
+
if (item.status === "completed")
|
|
455
|
+
return `- [x] ${item.text}`;
|
|
456
|
+
return `- [ ] ${item.text}${item.status === "in_progress" ? " (in progress)" : ""}`;
|
|
687
457
|
}
|
|
688
458
|
function legacyFileChanges(files) {
|
|
689
|
-
return unique(files).map((path) => ({
|
|
690
|
-
path,
|
|
691
|
-
status: "modified",
|
|
692
|
-
additions: 0,
|
|
693
|
-
deletions: 0,
|
|
694
|
-
staged: false,
|
|
695
|
-
unstaged: true,
|
|
696
|
-
untracked: false
|
|
697
|
-
}));
|
|
459
|
+
return unique(files).map((path) => ({ path, status: "modified", additions: 0, deletions: 0, staged: false, unstaged: true, untracked: false }));
|
|
698
460
|
}
|
|
699
461
|
function formatFileChanges(changes) {
|
|
700
462
|
return changes.map((change) => {
|
|
701
463
|
const stat = change.additions || change.deletions ? ` (+${change.additions} -${change.deletions})` : "";
|
|
702
|
-
const oldPath = change.oldPath ? `${change.oldPath}
|
|
464
|
+
const oldPath = change.oldPath ? `${change.oldPath} → ` : "";
|
|
703
465
|
return `${statusLabel(change)} ${oldPath}${change.path}${stat}`;
|
|
704
466
|
});
|
|
705
467
|
}
|
|
706
468
|
function statusLabel(change) {
|
|
707
469
|
if (change.untracked)
|
|
708
470
|
return "??";
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return "?";
|
|
720
|
-
}
|
|
721
|
-
function dedupeBlocks(blocks) {
|
|
471
|
+
return { added: "A", deleted: "D", renamed: "R", copied: "C", modified: "M", untracked: "??", unknown: "?" }[change.status];
|
|
472
|
+
}
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Context blocks
|
|
475
|
+
export function deriveContextBlocks(packet) {
|
|
476
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
477
|
+
const summary = normalized.handoffSummary;
|
|
478
|
+
const memory = normalized.engineeringMemory;
|
|
479
|
+
const source = `${summary.sourceSession?.source || "handoff"}:${summary.sourceSession?.id || normalized.id}`;
|
|
480
|
+
const blocks = [];
|
|
722
481
|
const seen = new Set();
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
const key = `${
|
|
726
|
-
if (seen.has(key))
|
|
727
|
-
|
|
482
|
+
const push = (category, title, text, provenance, confidence, provenanceSource = source) => {
|
|
483
|
+
const clean = redactSecrets(oneLine(text)).slice(0, 2_000);
|
|
484
|
+
const key = `${category}:${clean}`;
|
|
485
|
+
if (clean.length < 3 || seen.has(key))
|
|
486
|
+
return;
|
|
728
487
|
seen.add(key);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
488
|
+
blocks.push({
|
|
489
|
+
id: stableId(`${normalized.id}:${category}`, `${title}:${clean}`),
|
|
490
|
+
sourceHandoffId: normalized.id,
|
|
491
|
+
sourceTask: normalized.task,
|
|
492
|
+
sourceCreatedAt: normalized.createdAt,
|
|
493
|
+
category,
|
|
494
|
+
title,
|
|
495
|
+
text: clean,
|
|
496
|
+
provenance: { kind: provenance, confidence, source: provenanceSource },
|
|
497
|
+
estimatedChars: clean.length
|
|
498
|
+
});
|
|
739
499
|
};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
if (!pack.items.some((item) => item.enabled))
|
|
762
|
-
warnings.push("No enabled context blocks are present in this pack.");
|
|
763
|
-
return warnings;
|
|
764
|
-
}
|
|
765
|
-
function renderContextPackMarkdown(pack, items, estimatedChars, warnings) {
|
|
766
|
-
const sections = new Map();
|
|
767
|
-
for (const item of items) {
|
|
768
|
-
const current = sections.get(item.block.category) || [];
|
|
769
|
-
current.push(item);
|
|
770
|
-
sections.set(item.block.category, current);
|
|
771
|
-
}
|
|
772
|
-
const lines = [
|
|
773
|
-
`# Context Pack: ${pack.name}`,
|
|
774
|
-
"",
|
|
775
|
-
`Pack ID: ${pack.id}`,
|
|
776
|
-
`Updated: ${pack.updatedAt}`,
|
|
777
|
-
`Estimated characters: ${estimatedChars}`,
|
|
778
|
-
`Raw transcript: ${pack.includeRawTranscript ? "included" : "excluded/redacted"}`,
|
|
779
|
-
"",
|
|
780
|
-
"## Use This Context",
|
|
781
|
-
"Reuse the selected context below without re-discovering prior decisions, verification, changed files, or next actions.",
|
|
782
|
-
""
|
|
783
|
-
];
|
|
784
|
-
if (warnings.length) {
|
|
785
|
-
lines.push("## Warnings", ...warnings.map((warning) => `- ${warning}`), "");
|
|
786
|
-
}
|
|
787
|
-
for (const [category, categoryItems] of sections) {
|
|
788
|
-
lines.push(`## ${contextCategoryTitle(category)}`);
|
|
789
|
-
for (const item of categoryItems) {
|
|
790
|
-
lines.push(`- ${item.block.text}`);
|
|
791
|
-
lines.push(` - Source: ${item.block.sourceTask} / ${item.block.sourceHandoffId}`);
|
|
792
|
-
lines.push(` - Provenance: ${item.block.provenance.kind}, ${item.block.provenance.confidence}`);
|
|
793
|
-
}
|
|
794
|
-
lines.push("");
|
|
795
|
-
}
|
|
796
|
-
return lines.join("\n").trimEnd();
|
|
797
|
-
}
|
|
798
|
-
function contextCategoryTitle(category) {
|
|
799
|
-
switch (category) {
|
|
800
|
-
case "next-action":
|
|
801
|
-
return "Next Actions";
|
|
802
|
-
case "changed-file":
|
|
803
|
-
return "Changed Files";
|
|
804
|
-
default:
|
|
805
|
-
return labelize(category);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function labelize(value) {
|
|
809
|
-
return value.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
810
|
-
}
|
|
811
|
-
function toHandoffSignal(text) {
|
|
812
|
-
if (/(<permissions instructions>|<collaboration_mode>|<skills_instructions>|<plugins_instructions>|<environment_context>|<image>)/i.test(text) ||
|
|
813
|
-
/Knowledge cutoff:|You are Codex|Filesystem sandboxing defines|Available skills|Skill roots|Tool definitions|Valid channels:|# Handoff:/i.test(text)) {
|
|
814
|
-
return "";
|
|
500
|
+
push("intent", "Objective", summary.originalQuestion, "user-authored", "high");
|
|
501
|
+
if (summary.latestUserInstruction !== summary.originalQuestion)
|
|
502
|
+
push("instruction", "Latest request", summary.latestUserInstruction, "user-authored", "high");
|
|
503
|
+
push("next-action", "Next action", summary.nextAction, summary.plan ? "transcript-derived" : "ai-inferred", "medium");
|
|
504
|
+
for (const fact of memory.decisions)
|
|
505
|
+
push("decision", "Decision", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
506
|
+
for (const fact of memory.constraints)
|
|
507
|
+
push("constraint", "Constraint", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
508
|
+
for (const fact of memory.verification)
|
|
509
|
+
push("verification", "Verification", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
510
|
+
for (const fact of memory.risks)
|
|
511
|
+
push("risk", "Risk", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
512
|
+
for (const fact of memory.nextActions.slice(1))
|
|
513
|
+
push("next-action", "Remaining work", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
514
|
+
for (const fact of memory.changedFiles)
|
|
515
|
+
push("changed-file", "Changed file", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
516
|
+
for (const entry of normalized.sourceSession?.transcript || []) {
|
|
517
|
+
if (entry.role === "tool" || entry.text.length < 20)
|
|
518
|
+
continue;
|
|
519
|
+
push("transcript", entry.role === "user" ? "User" : "Agent", entry.text, "transcript-derived", "medium");
|
|
815
520
|
}
|
|
816
|
-
return
|
|
817
|
-
}
|
|
818
|
-
function segmentText(text) {
|
|
819
|
-
return text
|
|
820
|
-
.split(/\n+|(?<=[.!?])\s+/)
|
|
821
|
-
.map((part) => part.trim())
|
|
822
|
-
.filter((part) => part.length > 8)
|
|
823
|
-
.map((part) => (part.length > 800 ? `${part.slice(0, 800)}...` : part));
|
|
521
|
+
return blocks.slice(0, 160);
|
|
824
522
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Team export
|
|
525
|
+
export function buildTeamTimelineExport(packet, options = {}) {
|
|
526
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
527
|
+
const includeRawTranscript = options.includeRawTranscript === true;
|
|
528
|
+
const sourceSession = normalized.sourceSession
|
|
529
|
+
? {
|
|
530
|
+
...normalized.sourceSession,
|
|
531
|
+
transcript: includeRawTranscript ? normalized.sourceSession.transcript : [],
|
|
532
|
+
transcriptExcerpt: includeRawTranscript ? normalized.sourceSession.transcriptExcerpt : "",
|
|
533
|
+
compactSummary: includeRawTranscript ? normalized.sourceSession.compactSummary : undefined
|
|
834
534
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
return
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
535
|
+
: undefined;
|
|
536
|
+
const exportPacket = {
|
|
537
|
+
...normalized,
|
|
538
|
+
sourceSession,
|
|
539
|
+
sources: normalized.sources.map((source) => ({ ...source, snippets: includeRawTranscript ? source.snippets : [] })),
|
|
540
|
+
engineeringMemory: {
|
|
541
|
+
...normalized.engineeringMemory,
|
|
542
|
+
teamSync: {
|
|
543
|
+
mode: "sync-ready",
|
|
544
|
+
rawTranscript: includeRawTranscript ? "included" : "excluded",
|
|
545
|
+
filePreviewRedaction: "enabled",
|
|
546
|
+
secretsRedaction: "enabled",
|
|
547
|
+
userControlledSync: true
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
markdown: ""
|
|
551
|
+
};
|
|
552
|
+
exportPacket.markdown = renderHandoffMarkdown(exportPacket, includeRawTranscript);
|
|
553
|
+
// Packets saved by older versions were not redacted at capture; redact everything on the way out.
|
|
554
|
+
return redactDeep(exportPacket);
|
|
555
|
+
}
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Optional local model
|
|
558
|
+
async function suggestNextActionWithOllama(task, summary, configuredModel) {
|
|
559
|
+
const host = process.env.OLLAMA_HOST ? normalizeOllamaHost(process.env.OLLAMA_HOST) : "http://127.0.0.1:11434";
|
|
560
|
+
const model = configuredModel || process.env.CTXPOOL_OLLAMA_MODEL || (await firstOllamaModel(host));
|
|
561
|
+
if (!model)
|
|
562
|
+
return undefined;
|
|
563
|
+
const response = await fetch(`${host}/api/generate`, {
|
|
564
|
+
method: "POST",
|
|
565
|
+
signal: AbortSignal.timeout(15_000),
|
|
566
|
+
headers: { "content-type": "application/json" },
|
|
567
|
+
body: JSON.stringify({
|
|
568
|
+
model,
|
|
569
|
+
stream: false,
|
|
570
|
+
think: false,
|
|
571
|
+
options: { temperature: 0.2 },
|
|
572
|
+
prompt: [
|
|
573
|
+
"You write the single next action for a coding agent taking over a task.",
|
|
574
|
+
"Reply with one imperative sentence under 30 words. No preamble.",
|
|
575
|
+
"",
|
|
576
|
+
`Task: ${task}`,
|
|
577
|
+
`Objective: ${truncate(summary.originalQuestion, 400)}`,
|
|
578
|
+
`Latest request: ${truncate(summary.latestUserInstruction, 400)}`,
|
|
579
|
+
`Remaining: ${summary.remainingWork.slice(0, 5).join("; ") || "none recorded"}`,
|
|
580
|
+
`Verification: ${summary.verification.status}${summary.verification.commands.length ? ` (${summary.verification.commands.map((command) => `${command.command}: ${command.status}`).join("; ")})` : ""}`,
|
|
581
|
+
`Blockers: ${summary.blockers.join("; ") || "none"}`,
|
|
582
|
+
`Changed files: ${summary.fileChanges.slice(0, 10).map((change) => change.path).join(", ") || "none"}`
|
|
583
|
+
].join("\n")
|
|
584
|
+
})
|
|
585
|
+
});
|
|
586
|
+
if (!response.ok)
|
|
587
|
+
return undefined;
|
|
588
|
+
const json = (await response.json());
|
|
589
|
+
const sentence = json.response?.replace(/<think>[\s\S]*?<\/think>/g, "").trim().split("\n")[0]?.replace(/^["'“]|["'”]$/g, "");
|
|
590
|
+
return sentence ? truncate(sentence, 400) : undefined;
|
|
591
|
+
}
|
|
592
|
+
async function firstOllamaModel(host) {
|
|
593
|
+
const response = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(1_500) });
|
|
594
|
+
if (!response.ok)
|
|
595
|
+
return undefined;
|
|
596
|
+
const json = (await response.json());
|
|
597
|
+
return json.models?.find((model) => model.name)?.name;
|
|
598
|
+
}
|
|
599
|
+
function normalizeOllamaHost(value) {
|
|
600
|
+
const withScheme = /^https?:\/\//.test(value) ? value : `http://${value}`;
|
|
601
|
+
return withScheme.replace(/\/+$/, "");
|
|
867
602
|
}
|
|
868
603
|
//# sourceMappingURL=handoff.js.map
|