sidecar-cli 0.1.4 → 0.1.5-beta.2
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 +257 -17
- package/dist/cli.js +673 -68
- package/dist/lib/banner.js +17 -1
- package/dist/lib/color.js +30 -0
- package/dist/lib/format.js +7 -1
- package/dist/lib/table.js +97 -0
- package/dist/prompts/packet-sections.js +203 -0
- package/dist/prompts/prompt-compiler.js +102 -72
- package/dist/prompts/prompt-service.js +16 -4
- package/dist/prompts/prompt-spec.js +128 -0
- package/dist/prompts/sections.js +194 -0
- package/dist/runners/claude-runner.js +7 -28
- package/dist/runners/codex-runner.js +7 -28
- package/dist/runners/config.js +75 -0
- package/dist/runners/runner-exec.js +152 -0
- package/dist/runs/capture.js +429 -0
- package/dist/runs/run-record.js +58 -0
- package/dist/runs/run-repository.js +2 -0
- package/dist/services/hook-service.js +130 -0
- package/dist/services/run-orchestrator-service.js +210 -11
- package/dist/services/run-review-service.js +1 -1
- package/dist/tasks/task-packet.js +18 -1
- package/dist/tasks/task-service.js +4 -1
- package/dist/templates/hooks.js +34 -0
- package/package.json +2 -1
package/dist/lib/banner.js
CHANGED
|
@@ -3,7 +3,23 @@ export const SIDECAR_TAGLINE = 'project memory for your work';
|
|
|
3
3
|
export function bannerDisabled(argv = process.argv) {
|
|
4
4
|
return process.env.SIDECAR_NO_BANNER === '1' || argv.includes('--no-banner');
|
|
5
5
|
}
|
|
6
|
-
export function renderBanner(includeTagline = true) {
|
|
6
|
+
export function renderBanner(variant = 'compact', includeTagline = true) {
|
|
7
|
+
if (variant === 'block') {
|
|
8
|
+
const blockLines = [
|
|
9
|
+
' [■]─[▪]',
|
|
10
|
+
' ███████╗██╗██████╗ ███████╗ ██████╗ █████╗ ██████╗',
|
|
11
|
+
' ██╔════╝██║██╔══██╗██╔════╝██╔════╝██╔══██╗██╔══██╗',
|
|
12
|
+
' ███████╗██║██║ ██║█████╗ ██║ ███████║██████╔╝',
|
|
13
|
+
' ╚════██║██║██║ ██║██╔══╝ ██║ ██╔══██║██╔══██╗',
|
|
14
|
+
' ███████║██║██████╔╝███████╗╚██████╗██║ ██║██║ ██║',
|
|
15
|
+
' ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝',
|
|
16
|
+
];
|
|
17
|
+
if (includeTagline) {
|
|
18
|
+
blockLines.push('');
|
|
19
|
+
blockLines.push(SIDECAR_TAGLINE);
|
|
20
|
+
}
|
|
21
|
+
return blockLines.join('\n');
|
|
22
|
+
}
|
|
7
23
|
const lines = [SIDECAR_MARK, ''];
|
|
8
24
|
if (includeTagline) {
|
|
9
25
|
lines.push(SIDECAR_TAGLINE);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function isColorEnabled() {
|
|
2
|
+
const forced = process.env.FORCE_COLOR === '1' || process.env.FORCE_COLOR === 'true';
|
|
3
|
+
const hasTty = Boolean(process.stdout.isTTY) || forced;
|
|
4
|
+
return hasTty && !process.env.NO_COLOR && !process.argv.includes('--json');
|
|
5
|
+
}
|
|
6
|
+
const codes = {
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
red: '\x1b[31m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
magenta: '\x1b[35m',
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
};
|
|
16
|
+
// Evaluate isColorEnabled() on every call so late changes to NO_COLOR,
|
|
17
|
+
// FORCE_COLOR, or --json (e.g. tests flipping env vars) take effect.
|
|
18
|
+
// Cheap: two env-var reads plus an argv scan.
|
|
19
|
+
function wrap(code) {
|
|
20
|
+
return (s) => (isColorEnabled() ? code + s + codes.reset : s);
|
|
21
|
+
}
|
|
22
|
+
export const c = {
|
|
23
|
+
bold: wrap(codes.bold),
|
|
24
|
+
dim: wrap(codes.dim),
|
|
25
|
+
red: wrap(codes.red),
|
|
26
|
+
green: wrap(codes.green),
|
|
27
|
+
yellow: wrap(codes.yellow),
|
|
28
|
+
cyan: wrap(codes.cyan),
|
|
29
|
+
magenta: wrap(codes.magenta),
|
|
30
|
+
};
|
package/dist/lib/format.js
CHANGED
|
@@ -2,7 +2,13 @@ export function nowIso() {
|
|
|
2
2
|
return new Date().toISOString();
|
|
3
3
|
}
|
|
4
4
|
export function humanTime(iso) {
|
|
5
|
-
|
|
5
|
+
const date = new Date(iso);
|
|
6
|
+
const year = date.getFullYear();
|
|
7
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
9
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
10
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
11
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
6
12
|
}
|
|
7
13
|
export function splitCsv(input) {
|
|
8
14
|
if (!input)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { isColorEnabled, c } from './color.js';
|
|
2
|
+
function stripAnsi(s) {
|
|
3
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
4
|
+
}
|
|
5
|
+
function visibleLength(s) {
|
|
6
|
+
return stripAnsi(s).length;
|
|
7
|
+
}
|
|
8
|
+
function ellipsizeMiddle(s, maxLen) {
|
|
9
|
+
const stripped = stripAnsi(s);
|
|
10
|
+
if (stripped.length <= maxLen)
|
|
11
|
+
return s;
|
|
12
|
+
const half = Math.floor((maxLen - 1) / 2);
|
|
13
|
+
return stripped.slice(0, half) + '…' + stripped.slice(stripped.length - half);
|
|
14
|
+
}
|
|
15
|
+
function pad(s, width, align = 'left') {
|
|
16
|
+
const visLen = visibleLength(s);
|
|
17
|
+
const padding = Math.max(0, width - visLen);
|
|
18
|
+
if (align === 'left') {
|
|
19
|
+
return s + ' '.repeat(padding);
|
|
20
|
+
}
|
|
21
|
+
return ' '.repeat(padding) + s;
|
|
22
|
+
}
|
|
23
|
+
export function renderTable(columns, rows, opts = {}) {
|
|
24
|
+
const indent = opts.indent || '';
|
|
25
|
+
const isTty = isColorEnabled();
|
|
26
|
+
if (rows.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Compute natural widths
|
|
30
|
+
const widths = {};
|
|
31
|
+
for (const col of columns) {
|
|
32
|
+
let w = col.label.length;
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const val = row[col.key] || '';
|
|
35
|
+
w = Math.max(w, visibleLength(val));
|
|
36
|
+
}
|
|
37
|
+
widths[col.key] = w;
|
|
38
|
+
}
|
|
39
|
+
// Clamp by min/max
|
|
40
|
+
for (const col of columns) {
|
|
41
|
+
if (col.minWidth)
|
|
42
|
+
widths[col.key] = Math.max(widths[col.key], col.minWidth);
|
|
43
|
+
if (col.maxWidth)
|
|
44
|
+
widths[col.key] = Math.min(widths[col.key], col.maxWidth);
|
|
45
|
+
}
|
|
46
|
+
// If total > terminal width, reduce widest flexible column via middle-ellipsis
|
|
47
|
+
const terminalWidth = process.stdout.columns || 120;
|
|
48
|
+
const maxContentWidth = Math.max(0, terminalWidth - indent.length - (columns.length - 1) * 2);
|
|
49
|
+
let totalWidth = columns.reduce((sum, col) => sum + widths[col.key], 0) + (columns.length - 1) * 2;
|
|
50
|
+
if (totalWidth > maxContentWidth) {
|
|
51
|
+
// Find widest flexible column
|
|
52
|
+
let widestIdx = -1;
|
|
53
|
+
let widestWidth = -1;
|
|
54
|
+
for (let i = 0; i < columns.length; i++) {
|
|
55
|
+
const col = columns[i];
|
|
56
|
+
if (!col.maxWidth || widths[col.key] < col.maxWidth) {
|
|
57
|
+
if (widths[col.key] > widestWidth) {
|
|
58
|
+
widestWidth = widths[col.key];
|
|
59
|
+
widestIdx = i;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (widestIdx >= 0) {
|
|
64
|
+
const col = columns[widestIdx];
|
|
65
|
+
const reductionNeeded = totalWidth - maxContentWidth;
|
|
66
|
+
widths[col.key] = Math.max(col.minWidth || 1, widths[col.key] - reductionNeeded);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Render header
|
|
70
|
+
const headerParts = columns.map((col) => pad(col.label, widths[col.key], col.align));
|
|
71
|
+
const headerLine = indent + headerParts.join(' ');
|
|
72
|
+
if (isTty) {
|
|
73
|
+
console.log(c.bold(headerLine));
|
|
74
|
+
const ruleWidth = visibleLength(stripAnsi(headerLine));
|
|
75
|
+
console.log(indent + '─'.repeat(ruleWidth));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(headerLine);
|
|
79
|
+
const ruleWidth = visibleLength(stripAnsi(headerLine));
|
|
80
|
+
console.log(indent + '-'.repeat(ruleWidth));
|
|
81
|
+
}
|
|
82
|
+
// Render rows
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const parts = columns.map((col) => {
|
|
85
|
+
let val = row[col.key] || '';
|
|
86
|
+
if (col.format) {
|
|
87
|
+
val = col.format(val, row);
|
|
88
|
+
}
|
|
89
|
+
// Apply ellipsis if needed
|
|
90
|
+
if (visibleLength(val) > widths[col.key]) {
|
|
91
|
+
val = ellipsizeMiddle(val, widths[col.key]);
|
|
92
|
+
}
|
|
93
|
+
return pad(val, widths[col.key], col.align);
|
|
94
|
+
});
|
|
95
|
+
console.log(indent + parts.join(' '));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Adapter from TaskPacket → CompileSectionsInput. Mirrors the legacy packet
|
|
2
|
+
// layout exactly so `sidecar run <task-id>` produces byte-identical prompts
|
|
3
|
+
// after the compiler refactor. Snapshot-guarded in `prompts.compat.test`.
|
|
4
|
+
import { nowIso } from '../lib/format.js';
|
|
5
|
+
import { PROMPT_PREFERENCE_DEFAULTS } from '../runners/config.js';
|
|
6
|
+
function finalResponseFormat(runner) {
|
|
7
|
+
if (runner === 'codex') {
|
|
8
|
+
return [
|
|
9
|
+
'- Start with a one-line outcome summary.',
|
|
10
|
+
'- List files changed with concise reasons.',
|
|
11
|
+
'- Include validation commands run and their results.',
|
|
12
|
+
'- Note risks, blockers, or follow-up tasks.',
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
'- Use a brief plan -> implementation -> summary structure.',
|
|
17
|
+
'- Call out assumptions and tradeoffs explicitly.',
|
|
18
|
+
'- List changed files and validation results.',
|
|
19
|
+
'- End with remaining risks and next steps if any.',
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function runnerGuidance(runner) {
|
|
23
|
+
if (runner === 'codex') {
|
|
24
|
+
return [
|
|
25
|
+
'Work directly in this repository and keep changes tightly scoped to the task.',
|
|
26
|
+
'Prefer existing project helpers and patterns over introducing new abstractions.',
|
|
27
|
+
'Keep final reporting concise and implementation-focused.',
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
'Begin with a short plan, then execute changes in small coherent steps.',
|
|
32
|
+
'Explain implementation choices and tradeoffs briefly as you go.',
|
|
33
|
+
'Provide a clear summary with validation and follow-up notes at the end.',
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function textSection(id, title, content) {
|
|
37
|
+
return { id, title, kind: 'text', content, trim: 'keep' };
|
|
38
|
+
}
|
|
39
|
+
function listSection(id, title, items, options) {
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
title,
|
|
43
|
+
kind: 'list',
|
|
44
|
+
items,
|
|
45
|
+
...(options?.empty_placeholder ? { empty_placeholder: options.empty_placeholder } : {}),
|
|
46
|
+
...(options?.trim ? { trim: options.trim } : { trim: { policy: 'keep' } }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function validationLine(v) {
|
|
50
|
+
const label = v.name ? `${v.kind}:${v.name}` : v.kind;
|
|
51
|
+
return v.kind === 'custom' ? v.command : `${label} — \`${v.command}\``;
|
|
52
|
+
}
|
|
53
|
+
// Linked context uses two sub-lists (decisions + notes) under one heading. The
|
|
54
|
+
// legacy layout merged them so we render as a single `text` section whose
|
|
55
|
+
// content is the pre-formatted bullet list, and trim by hand before handing it
|
|
56
|
+
// to the core. That keeps byte-identical output without teaching the core
|
|
57
|
+
// about multi-list sections.
|
|
58
|
+
function renderLinkedContext(relatedDecisions, relatedNotes, mode) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const decisions = mode === 'full'
|
|
61
|
+
? relatedDecisions
|
|
62
|
+
: mode === 'trim'
|
|
63
|
+
? sliceWithOverflow(relatedDecisions, 3, 'decisions')
|
|
64
|
+
: sliceWithOverflow(relatedDecisions, 1, 'decisions');
|
|
65
|
+
const notes = mode === 'full'
|
|
66
|
+
? relatedNotes
|
|
67
|
+
: mode === 'trim'
|
|
68
|
+
? sliceWithOverflow(relatedNotes, 2, 'notes')
|
|
69
|
+
: sliceWithOverflow(relatedNotes, 0, 'notes');
|
|
70
|
+
if (decisions.length === 0)
|
|
71
|
+
lines.push('- no related decisions');
|
|
72
|
+
else
|
|
73
|
+
for (const d of decisions)
|
|
74
|
+
lines.push(`- ${d}`);
|
|
75
|
+
if (notes.length === 0)
|
|
76
|
+
lines.push('- no related notes');
|
|
77
|
+
else
|
|
78
|
+
for (const n of notes)
|
|
79
|
+
lines.push(`- ${n}`);
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
function sliceWithOverflow(items, limit, label) {
|
|
83
|
+
if (items.length <= limit)
|
|
84
|
+
return items;
|
|
85
|
+
const kept = items.slice(0, limit);
|
|
86
|
+
kept.push(`+ ${items.length - limit} more ${label} (see task packet for full list)`);
|
|
87
|
+
return kept;
|
|
88
|
+
}
|
|
89
|
+
function renderPreviousRuns(runs) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
runs.forEach((prev, idx) => {
|
|
92
|
+
if (idx > 0)
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(`### ${prev.run_id} — ${prev.runner} (${prev.agent_role})`);
|
|
95
|
+
lines.push(`- Status: ${prev.status}`);
|
|
96
|
+
if (prev.validation_summary)
|
|
97
|
+
lines.push(`- Validation: ${prev.validation_summary}`);
|
|
98
|
+
if (prev.summary)
|
|
99
|
+
lines.push(`- Summary: ${prev.summary}`);
|
|
100
|
+
if (prev.changed_files && prev.changed_files.length > 0) {
|
|
101
|
+
const limited = prev.changed_files.slice(0, 12);
|
|
102
|
+
lines.push(`- Changed files (${prev.changed_files.length}):`);
|
|
103
|
+
for (const f of limited)
|
|
104
|
+
lines.push(` - ${f}`);
|
|
105
|
+
if (prev.changed_files.length > limited.length) {
|
|
106
|
+
lines.push(` - + ${prev.changed_files.length - limited.length} more (see run record)`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (prev.log_tail) {
|
|
110
|
+
lines.push('- Log tail:');
|
|
111
|
+
lines.push('```');
|
|
112
|
+
for (const line of prev.log_tail.split('\n'))
|
|
113
|
+
lines.push(line);
|
|
114
|
+
lines.push('```');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
function renderConstraints(technical, design) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
if (technical.length === 0)
|
|
122
|
+
lines.push('- no technical constraints');
|
|
123
|
+
else
|
|
124
|
+
for (const t of technical)
|
|
125
|
+
lines.push(`- ${t}`);
|
|
126
|
+
if (design.length === 0)
|
|
127
|
+
lines.push('- no design constraints');
|
|
128
|
+
else
|
|
129
|
+
for (const d of design)
|
|
130
|
+
lines.push(`- ${d}`);
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
export function packetToCompileInput(input) {
|
|
134
|
+
const { task, run, runner, agentRole, linkedContext, budget } = input;
|
|
135
|
+
const pref = budget ?? PROMPT_PREFERENCE_DEFAULTS;
|
|
136
|
+
const header = [
|
|
137
|
+
'# Sidecar Execution Brief',
|
|
138
|
+
'',
|
|
139
|
+
`Runner: ${runner}`,
|
|
140
|
+
`Agent role: ${agentRole}`,
|
|
141
|
+
`Run id: ${run.run_id}`,
|
|
142
|
+
`Task id: ${task.task_id}`,
|
|
143
|
+
`Compiled at: ${nowIso()}`,
|
|
144
|
+
];
|
|
145
|
+
const relatedDecisions = linkedContext?.related_decisions ?? task.context.related_decisions;
|
|
146
|
+
const relatedNotes = linkedContext?.related_notes ?? task.context.related_notes;
|
|
147
|
+
const sections = [
|
|
148
|
+
textSection('task', 'Task', [
|
|
149
|
+
`- ${task.title}`,
|
|
150
|
+
`- Type: ${task.type}`,
|
|
151
|
+
`- Priority: ${task.priority}`,
|
|
152
|
+
`- Status: ${task.status}`,
|
|
153
|
+
]),
|
|
154
|
+
textSection('objective', 'Objective', [task.goal]),
|
|
155
|
+
textSection('why', 'Why this matters', [task.summary]),
|
|
156
|
+
listSection('in_scope', 'In scope', task.scope.in_scope, {
|
|
157
|
+
trim: { policy: 'trim-last', limit: 8, limit_strict: 8, overflow_label: 'in-scope items' },
|
|
158
|
+
}),
|
|
159
|
+
listSection('out_of_scope', 'Out of scope', task.scope.out_of_scope, {
|
|
160
|
+
trim: { policy: 'trim-last', limit: 5, limit_strict: 3, overflow_label: 'out-of-scope items' },
|
|
161
|
+
}),
|
|
162
|
+
listSection('files_to_read', 'Read these first', task.implementation.files_to_read, {
|
|
163
|
+
trim: { policy: 'trim-last', limit: 10, limit_strict: 10, overflow_label: 'read-first files' },
|
|
164
|
+
}),
|
|
165
|
+
listSection('files_to_avoid', 'Avoid changing', task.implementation.files_to_avoid, {
|
|
166
|
+
trim: { policy: 'trim-last', limit: 5, limit_strict: 3, overflow_label: 'avoid files' },
|
|
167
|
+
}),
|
|
168
|
+
// Linked context stays a text section so the "no related X" placeholders stay where they were.
|
|
169
|
+
textSection('linked_context', 'Linked context', renderLinkedContext(relatedDecisions, relatedNotes, 'full')),
|
|
170
|
+
// Previous runner context — only when this run is a later step in a pipeline.
|
|
171
|
+
...(linkedContext?.previous_runs && linkedContext.previous_runs.length > 0
|
|
172
|
+
? [textSection('previous_runs', 'Previous runner context', renderPreviousRuns(linkedContext.previous_runs))]
|
|
173
|
+
: []),
|
|
174
|
+
textSection('constraints', 'Constraints', renderConstraints(task.constraints.technical, task.constraints.design)),
|
|
175
|
+
listSection('validation', 'Validation', task.execution.commands.validation.map(validationLine)),
|
|
176
|
+
listSection('definition_of_done', 'Definition of done', task.definition_of_done),
|
|
177
|
+
textSection('runner_guidance', 'Runner guidance', runnerGuidance(runner)),
|
|
178
|
+
textSection('final_response_format', 'Final response format', finalResponseFormat(runner)),
|
|
179
|
+
];
|
|
180
|
+
return {
|
|
181
|
+
header,
|
|
182
|
+
sections,
|
|
183
|
+
budget: { target: pref.budget_target, max: pref.budget_max },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Legacy metadata expects `trimmed_sections: string[]` using the historical names.
|
|
187
|
+
// Map the new core metadata back for back-compat.
|
|
188
|
+
export const LEGACY_TRIM_IDS = [
|
|
189
|
+
'in_scope',
|
|
190
|
+
'out_of_scope',
|
|
191
|
+
'files_to_read',
|
|
192
|
+
'files_to_avoid',
|
|
193
|
+
'related_decisions',
|
|
194
|
+
'related_notes',
|
|
195
|
+
];
|
|
196
|
+
// Rebuild linked_context lines under a trim mode. The core compileSections()
|
|
197
|
+
// can't partially trim a text section, so we run the full pipeline twice in
|
|
198
|
+
// prompt-compiler.ts: first with full linked_context, and again with trimmed
|
|
199
|
+
// linked_context if the baseline is over budget. See prompt-compiler.ts for
|
|
200
|
+
// the wrapper that orchestrates this.
|
|
201
|
+
export function linkedContextForMode(relatedDecisions, relatedNotes, mode) {
|
|
202
|
+
return renderLinkedContext(relatedDecisions, relatedNotes, mode);
|
|
203
|
+
}
|
|
@@ -1,83 +1,113 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { nowIso } from '../lib/format.js';
|
|
4
3
|
import { getSidecarPaths } from '../lib/paths.js';
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { PROMPT_PREFERENCE_DEFAULTS } from '../runners/config.js';
|
|
5
|
+
import { compileSections } from './sections.js';
|
|
6
|
+
import { linkedContextForMode, packetToCompileInput, } from './packet-sections.js';
|
|
7
|
+
// Rebuild the packet adapter output with a specific linked_context trim mode.
|
|
8
|
+
// Linked context mixes two sub-lists under a single heading, which the core
|
|
9
|
+
// compiler treats as a text section (can't partial-trim), so the wrapper owns
|
|
10
|
+
// the two-pass escalation the legacy compiler used.
|
|
11
|
+
function packetInputForMode(adapterInput, mode) {
|
|
12
|
+
const baseline = packetToCompileInput(adapterInput);
|
|
13
|
+
if (mode === 'full')
|
|
14
|
+
return baseline;
|
|
15
|
+
const relatedDecisions = adapterInput.linkedContext?.related_decisions ?? adapterInput.task.context.related_decisions;
|
|
16
|
+
const relatedNotes = adapterInput.linkedContext?.related_notes ?? adapterInput.task.context.related_notes;
|
|
17
|
+
const linkedLines = linkedContextForMode(relatedDecisions, relatedNotes, mode);
|
|
18
|
+
const sections = baseline.sections.map((section) => {
|
|
19
|
+
if (section.id !== 'linked_context')
|
|
20
|
+
return section;
|
|
21
|
+
return { ...section, content: linkedLines };
|
|
22
|
+
});
|
|
23
|
+
return { ...baseline, sections };
|
|
7
24
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
// Preserve the legacy trimmed_sections names (include related_decisions /
|
|
26
|
+
// related_notes) since downstream run records use these keys.
|
|
27
|
+
function buildLegacyTrimmed(adapterInput, listTrimmed, mode) {
|
|
28
|
+
const out = new Set(listTrimmed);
|
|
29
|
+
if (mode !== 'full') {
|
|
30
|
+
const decisions = adapterInput.linkedContext?.related_decisions ?? adapterInput.task.context.related_decisions;
|
|
31
|
+
const notes = adapterInput.linkedContext?.related_notes ?? adapterInput.task.context.related_notes;
|
|
32
|
+
if (mode === 'trim') {
|
|
33
|
+
if (decisions.length > 3)
|
|
34
|
+
out.add('related_decisions');
|
|
35
|
+
if (notes.length > 2)
|
|
36
|
+
out.add('related_notes');
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (decisions.length > 1)
|
|
40
|
+
out.add('related_decisions');
|
|
41
|
+
if (notes.length > 0)
|
|
42
|
+
out.add('related_notes');
|
|
43
|
+
}
|
|
21
44
|
}
|
|
22
|
-
return [
|
|
23
|
-
'- Use a brief plan -> implementation -> summary structure.',
|
|
24
|
-
'- Call out assumptions and tradeoffs explicitly.',
|
|
25
|
-
'- List changed files and validation results.',
|
|
26
|
-
'- End with remaining risks and next steps if any.',
|
|
27
|
-
];
|
|
45
|
+
return [...out];
|
|
28
46
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'Explain implementation choices and tradeoffs briefly as you go.',
|
|
40
|
-
'Provide a clear summary with validation and follow-up notes at the end.',
|
|
41
|
-
];
|
|
47
|
+
// `policy_overrides` with every list id → 'keep' forces compileSections to
|
|
48
|
+
// render the untrimmed baseline. The legacy compiler chose to trim based on
|
|
49
|
+
// THIS baseline, not the partially-trimmed first pass, so we do the same to
|
|
50
|
+
// preserve byte-identical output for downstream consumers.
|
|
51
|
+
function allKeepOverrides(fullInput) {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const s of fullInput.sections)
|
|
54
|
+
if (s.kind === 'list')
|
|
55
|
+
out[s.id] = 'keep';
|
|
56
|
+
return out;
|
|
42
57
|
}
|
|
43
58
|
export function compilePromptMarkdown(input) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
const pref = input.budget ?? PROMPT_PREFERENCE_DEFAULTS;
|
|
60
|
+
const adapterInput = {
|
|
61
|
+
task: input.task,
|
|
62
|
+
run: input.run,
|
|
63
|
+
runner: input.runner,
|
|
64
|
+
agentRole: input.agentRole,
|
|
65
|
+
...(input.linkedContext ? { linkedContext: input.linkedContext } : {}),
|
|
66
|
+
...(input.budget ? { budget: input.budget } : {}),
|
|
67
|
+
};
|
|
68
|
+
const fullInput = packetInputForMode(adapterInput, 'full');
|
|
69
|
+
const untrimmed = compileSections({ ...fullInput, policy_overrides: allKeepOverrides(fullInput) });
|
|
70
|
+
// Fast path — the fully untrimmed render fits within the target budget.
|
|
71
|
+
if (untrimmed.metadata.estimated_tokens_after <= pref.budget_target) {
|
|
72
|
+
return {
|
|
73
|
+
markdown: untrimmed.markdown,
|
|
74
|
+
metadata: {
|
|
75
|
+
estimated_tokens_before: untrimmed.metadata.estimated_tokens_before,
|
|
76
|
+
estimated_tokens_after: untrimmed.metadata.estimated_tokens_after,
|
|
77
|
+
budget_target: pref.budget_target,
|
|
78
|
+
budget_max: pref.budget_max,
|
|
79
|
+
trimmed_sections: [],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Target pass — trim lists + linked_context.
|
|
84
|
+
const trimInput = packetInputForMode(adapterInput, 'trim');
|
|
85
|
+
const trim = compileSections(trimInput);
|
|
86
|
+
// Strict pass (safety valve) if still over max.
|
|
87
|
+
if (trim.metadata.estimated_tokens_after > pref.budget_max) {
|
|
88
|
+
const strictInput = packetInputForMode(adapterInput, 'strict');
|
|
89
|
+
const strict = compileSections(strictInput);
|
|
90
|
+
return {
|
|
91
|
+
markdown: strict.markdown,
|
|
92
|
+
metadata: {
|
|
93
|
+
estimated_tokens_before: untrimmed.metadata.estimated_tokens_before,
|
|
94
|
+
estimated_tokens_after: strict.metadata.estimated_tokens_after,
|
|
95
|
+
budget_target: pref.budget_target,
|
|
96
|
+
budget_max: pref.budget_max,
|
|
97
|
+
trimmed_sections: buildLegacyTrimmed(adapterInput, strict.metadata.trimmed_sections, 'strict'),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
markdown: trim.markdown,
|
|
103
|
+
metadata: {
|
|
104
|
+
estimated_tokens_before: untrimmed.metadata.estimated_tokens_before,
|
|
105
|
+
estimated_tokens_after: trim.metadata.estimated_tokens_after,
|
|
106
|
+
budget_target: pref.budget_target,
|
|
107
|
+
budget_max: pref.budget_max,
|
|
108
|
+
trimmed_sections: buildLegacyTrimmed(adapterInput, trim.metadata.trimmed_sections, 'trim'),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
81
111
|
}
|
|
82
112
|
export function saveCompiledPrompt(rootPath, runId, markdown) {
|
|
83
113
|
const promptsPath = getSidecarPaths(rootPath).promptsPath;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadPromptPreferences } from '../runners/config.js';
|
|
1
2
|
import { createRunRecordEntry, updateRunRecordEntry } from '../runs/run-service.js';
|
|
2
3
|
import { getTaskPacket } from '../tasks/task-service.js';
|
|
3
4
|
import { compilePromptMarkdown, saveCompiledPrompt } from './prompt-compiler.js';
|
|
@@ -10,19 +11,29 @@ export function compileTaskPrompt(input) {
|
|
|
10
11
|
status: 'preparing',
|
|
11
12
|
branch: task.tracking.branch,
|
|
12
13
|
worktree: task.tracking.worktree,
|
|
14
|
+
...(input.parentRunId ? { parent_run_id: input.parentRunId } : {}),
|
|
15
|
+
...(input.replayReason ? { replay_reason: input.replayReason } : {}),
|
|
16
|
+
...(input.pipelineId ? { pipeline_id: input.pipelineId } : {}),
|
|
17
|
+
...(input.pipelineStep ? { pipeline_step: input.pipelineStep } : {}),
|
|
18
|
+
...(input.pipelineTotal ? { pipeline_total: input.pipelineTotal } : {}),
|
|
13
19
|
});
|
|
14
|
-
const
|
|
20
|
+
const compiledPrompt = compilePromptMarkdown({
|
|
15
21
|
task,
|
|
16
22
|
run: created.run,
|
|
17
23
|
runner: input.runner,
|
|
18
24
|
agentRole: input.agentRole,
|
|
19
25
|
linkedContext: input.linkedContext,
|
|
26
|
+
budget: loadPromptPreferences(input.rootPath),
|
|
20
27
|
});
|
|
21
|
-
const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id,
|
|
28
|
+
const promptPath = saveCompiledPrompt(input.rootPath, created.run.run_id, compiledPrompt.markdown);
|
|
22
29
|
updateRunRecordEntry(input.rootPath, created.run.run_id, {
|
|
23
30
|
status: 'queued',
|
|
24
31
|
prompt_path: promptPath,
|
|
25
|
-
|
|
32
|
+
prompt_tokens_estimated_before: compiledPrompt.metadata.estimated_tokens_before,
|
|
33
|
+
prompt_tokens_estimated_after: compiledPrompt.metadata.estimated_tokens_after,
|
|
34
|
+
prompt_budget_target: compiledPrompt.metadata.budget_target,
|
|
35
|
+
prompt_trimmed_sections: compiledPrompt.metadata.trimmed_sections,
|
|
36
|
+
summary: `Compiled prompt for task ${task.task_id} (${compiledPrompt.metadata.estimated_tokens_before} -> ${compiledPrompt.metadata.estimated_tokens_after} est. tokens)`,
|
|
26
37
|
});
|
|
27
38
|
return {
|
|
28
39
|
run_id: created.run.run_id,
|
|
@@ -30,6 +41,7 @@ export function compileTaskPrompt(input) {
|
|
|
30
41
|
runner_type: input.runner,
|
|
31
42
|
agent_role: input.agentRole,
|
|
32
43
|
prompt_path: promptPath,
|
|
33
|
-
prompt_markdown:
|
|
44
|
+
prompt_markdown: compiledPrompt.markdown,
|
|
45
|
+
prompt_optimization: compiledPrompt.metadata,
|
|
34
46
|
};
|
|
35
47
|
}
|