ctxpool 0.1.1 → 0.5.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 +107 -99
- package/dist/cli.js +336 -183
- 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 +18 -13
- package/dist/handoff.js +495 -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 +45 -1
- package/dist/session.js +1307 -302
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +14 -6
- package/dist/store.js +90 -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 +92 -7
- package/dist/ui.d.ts +10 -1
- package/dist/ui.js +355 -382
- 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/Geist-Variable-B48wpBRb.woff2 +0 -0
- package/dist-web/assets/GeistMono-Variable-BRXCGrxA.woff2 +0 -0
- package/dist-web/assets/antigravity-Vc0oU6JN.svg +1 -0
- package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
- package/dist-web/assets/index-BztZuBPJ.js +21 -0
- package/dist-web/assets/index-LvoT6mmM.css +2 -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 { cleanTranscriptEntries, 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,111 @@ 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(input) {
|
|
307
|
+
const packet = input.handoffSummary ? input : cleanLegacyPacket(input);
|
|
308
|
+
const repo = {
|
|
309
|
+
...packet.repo,
|
|
310
|
+
fileChanges: packet.repo.fileChanges?.length ? packet.repo.fileChanges : legacyFileChanges(packet.repo.dirtyFiles || [])
|
|
311
|
+
};
|
|
312
|
+
const derived = buildHandoffSummary(packet.task, repo, packet.sourceSession, packet.sections);
|
|
313
|
+
const existing = packet.handoffSummary;
|
|
314
|
+
const handoffSummary = existing
|
|
315
|
+
? {
|
|
316
|
+
...derived,
|
|
317
|
+
...existing,
|
|
318
|
+
fileChanges: existing.fileChanges?.length ? existing.fileChanges : derived.fileChanges,
|
|
319
|
+
verification: existing.verification || derived.verification,
|
|
320
|
+
repoStats: existing.repoStats || derived.repoStats,
|
|
321
|
+
contextSignals: existing.contextSignals?.length ? existing.contextSignals : derived.contextSignals,
|
|
322
|
+
sourceSession: existing.sourceSession || derived.sourceSession,
|
|
323
|
+
repo: existing.repo || derived.repo
|
|
324
|
+
}
|
|
325
|
+
: derived;
|
|
326
|
+
const engineeringMemory = packet.engineeringMemory || buildEngineeringMemory(packet.task, repo, packet.sourceSession, packet.id, handoffSummary);
|
|
327
|
+
return { ...packet, repo, handoffSummary, engineeringMemory };
|
|
328
|
+
}
|
|
329
|
+
/** Packets saved before v0.2 kept injected harness text as user messages; re-read them without it. */
|
|
330
|
+
function cleanLegacyPacket(packet) {
|
|
331
|
+
if (!packet.sourceSession)
|
|
332
|
+
return packet;
|
|
333
|
+
const sourceSession = { ...packet.sourceSession, transcript: cleanTranscriptEntries(packet.sourceSession.transcript) };
|
|
334
|
+
const sections = buildSections(packet.task, packet.repo.dirtyFiles || [], packet.repo.diffStat || "", sourceSession);
|
|
335
|
+
return { ...packet, sourceSession, sections };
|
|
336
|
+
}
|
|
337
|
+
/** The compact shape the web UI lists and charts. */
|
|
338
|
+
export function summarizeHandoff(packet) {
|
|
339
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
340
|
+
const summary = normalized.handoffSummary;
|
|
341
|
+
const commands = summary.verification.commands;
|
|
342
|
+
const plan = summary.plan || [];
|
|
343
|
+
const changes = summary.fileChanges;
|
|
344
|
+
return {
|
|
345
|
+
id: normalized.id,
|
|
346
|
+
task: normalized.task,
|
|
347
|
+
createdAt: normalized.createdAt,
|
|
348
|
+
title: summary.title,
|
|
349
|
+
source: summary.sourceSession?.source,
|
|
350
|
+
sessionId: summary.sourceSession?.id,
|
|
351
|
+
targetAgent: normalized.targetAgent,
|
|
352
|
+
repoPath: normalized.repo.repoPath,
|
|
353
|
+
repoName: basename(normalized.repo.repoPath),
|
|
354
|
+
branch: normalized.repo.branch,
|
|
355
|
+
commit: normalized.repo.commit,
|
|
356
|
+
isGitRepo: normalized.repo.isGitRepo,
|
|
357
|
+
phase: summary.workPhase,
|
|
358
|
+
objective: truncate(oneLine(summary.originalQuestion), 280),
|
|
359
|
+
nextAction: truncate(oneLine(summary.nextAction), 280),
|
|
360
|
+
verification: summary.verification.status,
|
|
361
|
+
checks: {
|
|
362
|
+
passed: commands.filter((command) => command.status === "passed").length,
|
|
363
|
+
failed: commands.filter((command) => command.status === "failed").length,
|
|
364
|
+
unknown: commands.filter((command) => command.status === "unknown").length
|
|
365
|
+
},
|
|
366
|
+
plan: {
|
|
367
|
+
total: plan.length,
|
|
368
|
+
completed: plan.filter((item) => item.status === "completed").length,
|
|
369
|
+
inProgress: plan.filter((item) => item.status === "in_progress").length
|
|
370
|
+
},
|
|
371
|
+
files: {
|
|
372
|
+
count: changes.length,
|
|
373
|
+
additions: changes.reduce((sum, change) => sum + change.additions, 0),
|
|
374
|
+
deletions: changes.reduce((sum, change) => sum + change.deletions, 0),
|
|
375
|
+
paths: changes.slice(0, 50).map((change) => change.path)
|
|
376
|
+
},
|
|
377
|
+
blockers: summary.blockers.length,
|
|
378
|
+
tokens: normalized.tokenEstimate ?? estimateTokens(normalized.markdown),
|
|
379
|
+
transcriptItems: summary.sourceSession?.transcriptItems ?? 0
|
|
380
|
+
};
|
|
601
381
|
}
|
|
602
382
|
export function buildEngineeringMemory(task, repo, sourceSession, handoffId, summary) {
|
|
603
383
|
const sourceLabel = sourceSession ? `${sourceSession.source}:${sourceSession.id}` : "local-capture";
|
|
384
|
+
const verified = summary.verification.commands.length > 0;
|
|
604
385
|
return {
|
|
605
386
|
version: 1,
|
|
606
387
|
identity: {
|
|
@@ -612,19 +393,12 @@ export function buildEngineeringMemory(task, repo, sourceSession, handoffId, sum
|
|
|
612
393
|
sessionId: sourceSession?.id,
|
|
613
394
|
sourceAgent: sourceSession?.source
|
|
614
395
|
},
|
|
615
|
-
positioning: {
|
|
616
|
-
category: "handoff-ledger",
|
|
617
|
-
buyer: "ai-heavy-engineering-teams",
|
|
618
|
-
moat: "provenance-backed-engineering-memory"
|
|
619
|
-
},
|
|
620
396
|
intent: facts("intent", [summary.originalQuestion, summary.latestUserInstruction], "user-authored", "high", sourceLabel),
|
|
621
397
|
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),
|
|
398
|
+
constraints: facts("constraint", summary.constraints || [], "user-authored", "medium", sourceLabel),
|
|
399
|
+
verification: facts("verification", verified ? summary.verification.commands.map((command) => `${command.status}: ${command.command}`) : summary.verification.gaps, "command-derived", verified ? "high" : "low", sourceLabel),
|
|
626
400
|
risks: facts("risk", [...summary.blockers, ...summary.errors], "transcript-derived", "medium", sourceLabel),
|
|
627
|
-
nextActions: facts("next-action", [summary.nextAction, ...summary.remainingWork], "ai-inferred", "medium", sourceLabel),
|
|
401
|
+
nextActions: facts("next-action", [summary.nextAction, ...summary.remainingWork], summary.plan ? "transcript-derived" : "ai-inferred", "medium", sourceLabel),
|
|
628
402
|
changedFiles: facts("changed-file", formatFileChanges(summary.fileChanges), "diff-derived", "high", repo.repoPath),
|
|
629
403
|
teamSync: {
|
|
630
404
|
mode: "local-only",
|
|
@@ -637,232 +411,247 @@ export function buildEngineeringMemory(task, repo, sourceSession, handoffId, sum
|
|
|
637
411
|
}
|
|
638
412
|
function facts(category, values, provenanceKind, confidence, source) {
|
|
639
413
|
return unique(values)
|
|
640
|
-
.filter((value) => !/^None captured\.?$/i.test(value))
|
|
641
414
|
.slice(0, 12)
|
|
642
|
-
.map((text) => ({
|
|
643
|
-
id: stableId(category, text),
|
|
644
|
-
text,
|
|
645
|
-
provenance: {
|
|
646
|
-
kind: provenanceKind,
|
|
647
|
-
confidence,
|
|
648
|
-
source
|
|
649
|
-
}
|
|
650
|
-
}));
|
|
415
|
+
.map((text) => ({ id: stableId(category, text), text, provenance: { kind: provenanceKind, confidence, source } }));
|
|
651
416
|
}
|
|
652
417
|
function stableId(prefix, seed) {
|
|
653
418
|
return `${prefix}_${hashText(seed || prefix)}`;
|
|
654
419
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Rendering
|
|
422
|
+
/**
|
|
423
|
+
* The handoff an agent reads. Ordered by what the next agent needs first; empty sections are omitted.
|
|
424
|
+
*/
|
|
425
|
+
export function renderHandoffMarkdown(packet, includeTranscript = true) {
|
|
426
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
427
|
+
const summary = normalized.handoffSummary;
|
|
428
|
+
const session = normalized.sourceSession;
|
|
429
|
+
const stats = summary.repoStats;
|
|
430
|
+
const meta = [
|
|
431
|
+
basename(normalized.repo.repoPath),
|
|
432
|
+
normalized.repo.isGitRepo ? `${normalized.repo.branch} @ ${normalized.repo.commit}` : "not a git repo",
|
|
433
|
+
stats.dirtyFiles ? `${stats.dirtyFiles} changed ${stats.dirtyFiles === 1 ? "file" : "files"} (+${stats.additions} -${stats.deletions})` : "clean tree",
|
|
434
|
+
session ? `${sourceLabel(session.source)} session` : undefined,
|
|
435
|
+
normalized.createdAt.slice(0, 16).replace("T", " ")
|
|
436
|
+
].filter(Boolean);
|
|
437
|
+
const lines = [
|
|
438
|
+
`# Handoff: ${normalized.task}`,
|
|
439
|
+
"",
|
|
440
|
+
`> ${meta.join(" · ")}`,
|
|
441
|
+
"",
|
|
442
|
+
`${normalized.targetAgent ? `For ${normalized.targetAgent}: c` : "C"}ontinue this task. Check that the repo matches the state below, then start from **Next action**.`
|
|
443
|
+
];
|
|
444
|
+
const section = (title, body) => {
|
|
445
|
+
const content = Array.isArray(body) ? body.filter(Boolean).join("\n") : body?.trim();
|
|
446
|
+
if (content)
|
|
447
|
+
lines.push("", `## ${title}`, content);
|
|
448
|
+
};
|
|
449
|
+
const objective = summary.originalQuestion;
|
|
450
|
+
section("Objective", objective);
|
|
451
|
+
if (summary.latestUserInstruction && summary.latestUserInstruction !== objective)
|
|
452
|
+
section("Latest request", summary.latestUserInstruction);
|
|
453
|
+
section("Next action", summary.nextAction);
|
|
454
|
+
section("Status", [
|
|
673
455
|
`- Phase: ${summary.workPhase}`,
|
|
674
|
-
`-
|
|
675
|
-
`-
|
|
676
|
-
|
|
677
|
-
|
|
456
|
+
`- Verification: ${summary.verification.status}`,
|
|
457
|
+
summary.latestAssistantSummary ? `- Last update from the agent: ${truncate(oneLine(summary.latestAssistantSummary), 600)}` : ""
|
|
458
|
+
]);
|
|
459
|
+
section("Plan", summary.plan?.length ? summary.plan.map(formatPlanItem) : summary.agentPlan && !summary.plan ? truncate(summary.agentPlan, 1_500) : undefined);
|
|
460
|
+
section("Verification", renderVerification(summary.verification));
|
|
461
|
+
section("Changed files", renderFileChanges(summary.fileChanges));
|
|
462
|
+
section("Decisions", bullets(summary.decisions, 6));
|
|
463
|
+
section("Constraints", bullets(summary.constraints || normalized.sections.doNotRepeat, 6));
|
|
464
|
+
section("Blockers", bullets(summary.blockers, 6));
|
|
465
|
+
if (!summary.verification.commands.length)
|
|
466
|
+
section("Errors", bullets(summary.errors, 6));
|
|
467
|
+
section("Earlier context", session?.compactSummary ? truncate(session.compactSummary, 2_000) : undefined);
|
|
468
|
+
section("Links", bullets(session?.links || [], 5));
|
|
469
|
+
if (includeTranscript)
|
|
470
|
+
section("Recent transcript", session?.transcriptExcerpt);
|
|
471
|
+
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}`);
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
/** Import-ready context for an agent: the handoff without the transcript, plus the structured summary. */
|
|
475
|
+
export function buildAgentContext(packet) {
|
|
476
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
477
|
+
return {
|
|
478
|
+
markdown: renderHandoffMarkdown(normalized, false),
|
|
479
|
+
summary: normalized.handoffSummary,
|
|
480
|
+
engineeringMemory: normalized.engineeringMemory
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
export function renderAgentContextMarkdown(packet) {
|
|
484
|
+
return renderHandoffMarkdown(packet, false);
|
|
678
485
|
}
|
|
679
486
|
function renderVerification(verification) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
487
|
+
const glyph = { passed: "✓", failed: "✗", unknown: "?" };
|
|
488
|
+
return [
|
|
489
|
+
...verification.commands.map((command) => {
|
|
490
|
+
const exit = command.status === "failed" && command.exitCode ? ` (exit ${command.exitCode})` : command.status === "unknown" ? " (result not recorded)" : "";
|
|
491
|
+
return `- ${glyph[command.status]} \`${command.command}\`${exit}${command.evidence ? ` — ${command.evidence}` : ""}`;
|
|
492
|
+
}),
|
|
493
|
+
...verification.gaps.map((gap) => `- Gap: ${gap}`)
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
function renderFileChanges(changes) {
|
|
497
|
+
const lines = formatFileChanges(changes.slice(0, 30)).map((line) => `- ${line}`);
|
|
498
|
+
if (changes.length > 30)
|
|
499
|
+
lines.push(`- …and ${changes.length - 30} more`);
|
|
500
|
+
return lines;
|
|
501
|
+
}
|
|
502
|
+
function bullets(values, max) {
|
|
503
|
+
return unique(values)
|
|
504
|
+
.slice(0, max)
|
|
505
|
+
.map((value) => `- ${truncate(oneLine(value), 300)}`);
|
|
506
|
+
}
|
|
507
|
+
function formatPlanItem(item) {
|
|
508
|
+
if (item.status === "completed")
|
|
509
|
+
return `- [x] ${item.text}`;
|
|
510
|
+
return `- [ ] ${item.text}${item.status === "in_progress" ? " (in progress)" : ""}`;
|
|
687
511
|
}
|
|
688
512
|
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
|
-
}));
|
|
513
|
+
return unique(files).map((path) => ({ path, status: "modified", additions: 0, deletions: 0, staged: false, unstaged: true, untracked: false }));
|
|
698
514
|
}
|
|
699
515
|
function formatFileChanges(changes) {
|
|
700
516
|
return changes.map((change) => {
|
|
701
517
|
const stat = change.additions || change.deletions ? ` (+${change.additions} -${change.deletions})` : "";
|
|
702
|
-
const oldPath = change.oldPath ? `${change.oldPath}
|
|
518
|
+
const oldPath = change.oldPath ? `${change.oldPath} → ` : "";
|
|
703
519
|
return `${statusLabel(change)} ${oldPath}${change.path}${stat}`;
|
|
704
520
|
});
|
|
705
521
|
}
|
|
706
522
|
function statusLabel(change) {
|
|
707
523
|
if (change.untracked)
|
|
708
524
|
return "??";
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return "?";
|
|
720
|
-
}
|
|
721
|
-
function dedupeBlocks(blocks) {
|
|
525
|
+
return { added: "A", deleted: "D", renamed: "R", copied: "C", modified: "M", untracked: "??", unknown: "?" }[change.status];
|
|
526
|
+
}
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Context blocks
|
|
529
|
+
export function deriveContextBlocks(packet) {
|
|
530
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
531
|
+
const summary = normalized.handoffSummary;
|
|
532
|
+
const memory = normalized.engineeringMemory;
|
|
533
|
+
const source = `${summary.sourceSession?.source || "handoff"}:${summary.sourceSession?.id || normalized.id}`;
|
|
534
|
+
const blocks = [];
|
|
722
535
|
const seen = new Set();
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
const key = `${
|
|
726
|
-
if (seen.has(key))
|
|
727
|
-
|
|
536
|
+
const push = (category, title, text, provenance, confidence, provenanceSource = source) => {
|
|
537
|
+
const clean = redactSecrets(oneLine(text)).slice(0, 2_000);
|
|
538
|
+
const key = `${category}:${clean}`;
|
|
539
|
+
if (clean.length < 3 || seen.has(key))
|
|
540
|
+
return;
|
|
728
541
|
seen.add(key);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
542
|
+
blocks.push({
|
|
543
|
+
id: stableId(`${normalized.id}:${category}`, `${title}:${clean}`),
|
|
544
|
+
sourceHandoffId: normalized.id,
|
|
545
|
+
sourceTask: normalized.task,
|
|
546
|
+
sourceCreatedAt: normalized.createdAt,
|
|
547
|
+
category,
|
|
548
|
+
title,
|
|
549
|
+
text: clean,
|
|
550
|
+
provenance: { kind: provenance, confidence, source: provenanceSource },
|
|
551
|
+
estimatedChars: clean.length
|
|
552
|
+
});
|
|
739
553
|
};
|
|
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 "";
|
|
554
|
+
push("intent", "Objective", summary.originalQuestion, "user-authored", "high");
|
|
555
|
+
if (summary.latestUserInstruction !== summary.originalQuestion)
|
|
556
|
+
push("instruction", "Latest request", summary.latestUserInstruction, "user-authored", "high");
|
|
557
|
+
push("next-action", "Next action", summary.nextAction, summary.plan ? "transcript-derived" : "ai-inferred", "medium");
|
|
558
|
+
for (const fact of memory.decisions)
|
|
559
|
+
push("decision", "Decision", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
560
|
+
for (const fact of memory.constraints)
|
|
561
|
+
push("constraint", "Constraint", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
562
|
+
for (const fact of memory.verification)
|
|
563
|
+
push("verification", "Verification", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
564
|
+
for (const fact of memory.risks)
|
|
565
|
+
push("risk", "Risk", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
566
|
+
for (const fact of memory.nextActions.slice(1))
|
|
567
|
+
push("next-action", "Remaining work", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
568
|
+
for (const fact of memory.changedFiles)
|
|
569
|
+
push("changed-file", "Changed file", fact.text, fact.provenance.kind, fact.provenance.confidence, fact.provenance.source);
|
|
570
|
+
for (const entry of normalized.sourceSession?.transcript || []) {
|
|
571
|
+
if (entry.role === "tool" || entry.text.length < 20)
|
|
572
|
+
continue;
|
|
573
|
+
push("transcript", entry.role === "user" ? "User" : "Agent", entry.text, "transcript-derived", "medium");
|
|
815
574
|
}
|
|
816
|
-
return
|
|
575
|
+
return blocks.slice(0, 160);
|
|
817
576
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const found = snippet.match(pattern);
|
|
830
|
-
if (!found)
|
|
831
|
-
continue;
|
|
832
|
-
for (const file of found) {
|
|
833
|
-
matches.push(file.replace(/[),.;:!?]+$/g, ""));
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// Team export
|
|
579
|
+
export function buildTeamTimelineExport(packet, options = {}) {
|
|
580
|
+
const normalized = normalizeHandoffPacket(packet);
|
|
581
|
+
const includeRawTranscript = options.includeRawTranscript === true;
|
|
582
|
+
const sourceSession = normalized.sourceSession
|
|
583
|
+
? {
|
|
584
|
+
...normalized.sourceSession,
|
|
585
|
+
transcript: includeRawTranscript ? normalized.sourceSession.transcript : [],
|
|
586
|
+
transcriptExcerpt: includeRawTranscript ? normalized.sourceSession.transcriptExcerpt : "",
|
|
587
|
+
compactSummary: includeRawTranscript ? normalized.sourceSession.compactSummary : undefined
|
|
834
588
|
}
|
|
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
|
-
|
|
589
|
+
: undefined;
|
|
590
|
+
const exportPacket = {
|
|
591
|
+
...normalized,
|
|
592
|
+
sourceSession,
|
|
593
|
+
sources: normalized.sources.map((source) => ({ ...source, snippets: includeRawTranscript ? source.snippets : [] })),
|
|
594
|
+
engineeringMemory: {
|
|
595
|
+
...normalized.engineeringMemory,
|
|
596
|
+
teamSync: {
|
|
597
|
+
mode: "sync-ready",
|
|
598
|
+
rawTranscript: includeRawTranscript ? "included" : "excluded",
|
|
599
|
+
filePreviewRedaction: "enabled",
|
|
600
|
+
secretsRedaction: "enabled",
|
|
601
|
+
userControlledSync: true
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
markdown: ""
|
|
605
|
+
};
|
|
606
|
+
exportPacket.markdown = renderHandoffMarkdown(exportPacket, includeRawTranscript);
|
|
607
|
+
// Packets saved by older versions were not redacted at capture; redact everything on the way out.
|
|
608
|
+
return redactDeep(exportPacket);
|
|
609
|
+
}
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// Optional local model
|
|
612
|
+
async function suggestNextActionWithOllama(task, summary, configuredModel) {
|
|
613
|
+
const host = process.env.OLLAMA_HOST ? normalizeOllamaHost(process.env.OLLAMA_HOST) : "http://127.0.0.1:11434";
|
|
614
|
+
const model = configuredModel || process.env.CTXPOOL_OLLAMA_MODEL || (await firstOllamaModel(host));
|
|
615
|
+
if (!model)
|
|
616
|
+
return undefined;
|
|
617
|
+
const response = await fetch(`${host}/api/generate`, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
signal: AbortSignal.timeout(15_000),
|
|
620
|
+
headers: { "content-type": "application/json" },
|
|
621
|
+
body: JSON.stringify({
|
|
622
|
+
model,
|
|
623
|
+
stream: false,
|
|
624
|
+
think: false,
|
|
625
|
+
options: { temperature: 0.2 },
|
|
626
|
+
prompt: [
|
|
627
|
+
"You write the single next action for a coding agent taking over a task.",
|
|
628
|
+
"Reply with one imperative sentence under 30 words. No preamble.",
|
|
629
|
+
"",
|
|
630
|
+
`Task: ${task}`,
|
|
631
|
+
`Objective: ${truncate(summary.originalQuestion, 400)}`,
|
|
632
|
+
`Latest request: ${truncate(summary.latestUserInstruction, 400)}`,
|
|
633
|
+
`Remaining: ${summary.remainingWork.slice(0, 5).join("; ") || "none recorded"}`,
|
|
634
|
+
`Verification: ${summary.verification.status}${summary.verification.commands.length ? ` (${summary.verification.commands.map((command) => `${command.command}: ${command.status}`).join("; ")})` : ""}`,
|
|
635
|
+
`Blockers: ${summary.blockers.join("; ") || "none"}`,
|
|
636
|
+
`Changed files: ${summary.fileChanges.slice(0, 10).map((change) => change.path).join(", ") || "none"}`
|
|
637
|
+
].join("\n")
|
|
638
|
+
})
|
|
639
|
+
});
|
|
640
|
+
if (!response.ok)
|
|
641
|
+
return undefined;
|
|
642
|
+
const json = (await response.json());
|
|
643
|
+
const sentence = json.response?.replace(/<think>[\s\S]*?<\/think>/g, "").trim().split("\n")[0]?.replace(/^["'“]|["'”]$/g, "");
|
|
644
|
+
return sentence ? truncate(sentence, 400) : undefined;
|
|
645
|
+
}
|
|
646
|
+
async function firstOllamaModel(host) {
|
|
647
|
+
const response = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(1_500) });
|
|
648
|
+
if (!response.ok)
|
|
649
|
+
return undefined;
|
|
650
|
+
const json = (await response.json());
|
|
651
|
+
return json.models?.find((model) => model.name)?.name;
|
|
652
|
+
}
|
|
653
|
+
function normalizeOllamaHost(value) {
|
|
654
|
+
const withScheme = /^https?:\/\//.test(value) ? value : `http://${value}`;
|
|
655
|
+
return withScheme.replace(/\/+$/, "");
|
|
867
656
|
}
|
|
868
657
|
//# sourceMappingURL=handoff.js.map
|