@event4u/agent-config 9.16.0 → 9.17.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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/architecture.md +2 -2
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +1 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3194 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +31 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +23 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +13 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +22 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +30 -0
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +9 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +325 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +23 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff generator (road-to-agent-handoff-resume Phase 2). Takes a session
|
|
3
|
+
* picked by `handoff_sessions.ts`, reads its FULL transcript, and emits the
|
|
4
|
+
* handoff document with the exact section set from
|
|
5
|
+
* `src/domains/meta/agent-handoff/command.md` step 2 — deterministically:
|
|
6
|
+
* no LLM call, reproducible, privacy-floored per line.
|
|
7
|
+
*
|
|
8
|
+
* - User messages are preserved VERBATIM (the template's load-bearing
|
|
9
|
+
* rule). A prompt containing any line the low-impact redactor refuses
|
|
10
|
+
* is withheld whole (drop, never rewrite) and counted in the
|
|
11
|
+
* `[N line(s) withheld by privacy floor]` marker.
|
|
12
|
+
* - Assistant/tool content is extracted with the hot-context heuristics:
|
|
13
|
+
* last intents, tool-result changes, failure-pattern regex for errors.
|
|
14
|
+
* - Output lands atomically in `agents/runtime/state/handoff-context.md`
|
|
15
|
+
* with `Generated:` / `Source-Session:` / `Branch:` parse anchors for
|
|
16
|
+
* the session_start hook, hard-capped at ~1200 words (priority-trim
|
|
17
|
+
* like `build_hot_context`).
|
|
18
|
+
* - `--llm` polish is a seam only in v1: requesting it raises a clear
|
|
19
|
+
* "not implemented" error. No spend without the flag, and none with it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
|
|
27
|
+
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
28
|
+
import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
|
|
29
|
+
import { read_entries } from '../chat_history.js';
|
|
30
|
+
import { HANDOFF_CONTEXT_REL } from '../handoff_context_hook.js';
|
|
31
|
+
import type { HandoffSession } from './handoff_sessions.js';
|
|
32
|
+
|
|
33
|
+
export const WORD_CAP = 1200;
|
|
34
|
+
|
|
35
|
+
const SNIPPET_CHARS = 200;
|
|
36
|
+
const DONE_SNIPPET_CHARS = 400;
|
|
37
|
+
const MAX_ERRORS = 5;
|
|
38
|
+
const MAX_DECISIONS = 5;
|
|
39
|
+
const MAX_FILES = 15;
|
|
40
|
+
|
|
41
|
+
const FAILURE_RE = /\b(fail(ing|ed|ure)?|error|exit[= ][1-9]|red\b|broken)\b/i;
|
|
42
|
+
const DECISION_RE = /\b(decided|decision|chose|choosing|locked|we'll use|we will use)\b/i;
|
|
43
|
+
const FILE_PATH_RE = /(?:src|tests|docs|agents|scripts|config)\/[\w./-]+\.\w{1,8}/g;
|
|
44
|
+
const ROADMAP_RE = /agents\/roadmaps\/[\w.-]+\.md/g;
|
|
45
|
+
|
|
46
|
+
export class LlmPolishNotImplementedError extends Error {
|
|
47
|
+
constructor() {
|
|
48
|
+
super(
|
|
49
|
+
'--llm polish is a v1 seam and not implemented yet — ' +
|
|
50
|
+
'the deterministic handoff (default) is the supported path.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------
|
|
56
|
+
// unified transcript model
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export interface TranscriptTurn {
|
|
60
|
+
kind: 'user' | 'assistant' | 'tool';
|
|
61
|
+
text: string;
|
|
62
|
+
ts: string | null;
|
|
63
|
+
tool?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TranscriptData {
|
|
67
|
+
turns: TranscriptTurn[];
|
|
68
|
+
branch: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _read_lines(file: string): string[] {
|
|
72
|
+
try {
|
|
73
|
+
return fs.readFileSync(file, 'utf-8').split('\n');
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _parse_json(line: string): Record<string, unknown> | null {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (!trimmed) return null;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
84
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
85
|
+
return parsed as Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// fail-open
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _from_chat_history(sessionId: string, chatHistoryPath: string | null): TranscriptData {
|
|
94
|
+
let entries: Array<Record<string, unknown>>;
|
|
95
|
+
try {
|
|
96
|
+
entries = read_entries({ session: sessionId, path: chatHistoryPath, last: null });
|
|
97
|
+
} catch {
|
|
98
|
+
entries = [];
|
|
99
|
+
}
|
|
100
|
+
const turns: TranscriptTurn[] = [];
|
|
101
|
+
for (const e of entries) {
|
|
102
|
+
const text = typeof e.text === 'string' ? e.text : '';
|
|
103
|
+
if (!text) continue;
|
|
104
|
+
const t = String(e.t ?? '');
|
|
105
|
+
const kind: TranscriptTurn['kind'] = t === 'user' ? 'user' : t === 'tool' ? 'tool' : 'assistant';
|
|
106
|
+
turns.push({
|
|
107
|
+
kind,
|
|
108
|
+
text,
|
|
109
|
+
ts: typeof e.ts === 'string' ? e.ts : null,
|
|
110
|
+
...(typeof e.tool === 'string' && e.tool ? { tool: e.tool } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { turns, branch: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _cc_text_from_content(content: unknown): string {
|
|
117
|
+
if (typeof content === 'string') return content;
|
|
118
|
+
if (Array.isArray(content)) {
|
|
119
|
+
return content
|
|
120
|
+
.map((block) => {
|
|
121
|
+
if (block && typeof block === 'object' && !Array.isArray(block)) {
|
|
122
|
+
const b = block as Record<string, unknown>;
|
|
123
|
+
if (b.type === 'text' && typeof b.text === 'string') return b.text;
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
})
|
|
127
|
+
.filter((t) => t)
|
|
128
|
+
.join('\n');
|
|
129
|
+
}
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _from_claude_transcript(file: string): TranscriptData {
|
|
134
|
+
const turns: TranscriptTurn[] = [];
|
|
135
|
+
let branch: string | null = null;
|
|
136
|
+
for (const line of _read_lines(file)) {
|
|
137
|
+
const rec = _parse_json(line);
|
|
138
|
+
if (!rec || rec.isSidechain === true) continue;
|
|
139
|
+
if (!branch && typeof rec.gitBranch === 'string' && rec.gitBranch) branch = rec.gitBranch;
|
|
140
|
+
const ts = typeof rec.timestamp === 'string' ? rec.timestamp : null;
|
|
141
|
+
const msg =
|
|
142
|
+
rec.message && typeof rec.message === 'object' && !Array.isArray(rec.message)
|
|
143
|
+
? (rec.message as Record<string, unknown>)
|
|
144
|
+
: null;
|
|
145
|
+
if (!msg) continue;
|
|
146
|
+
if (rec.type === 'user') {
|
|
147
|
+
// real prompts have string content; tool_results arrive as lists
|
|
148
|
+
if (typeof msg.content === 'string') {
|
|
149
|
+
const text = msg.content.trim();
|
|
150
|
+
if (text && !text.startsWith('<') && !text.startsWith('Caveat:')) {
|
|
151
|
+
turns.push({ kind: 'user', text, ts });
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
const text = _cc_text_from_content(msg.content).trim();
|
|
155
|
+
if (text) turns.push({ kind: 'tool', text, ts });
|
|
156
|
+
}
|
|
157
|
+
} else if (rec.type === 'assistant') {
|
|
158
|
+
const text = _cc_text_from_content(msg.content).trim();
|
|
159
|
+
if (text) turns.push({ kind: 'assistant', text, ts });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { turns, branch };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _from_codex_session(file: string): TranscriptData {
|
|
166
|
+
const turns: TranscriptTurn[] = [];
|
|
167
|
+
let branch: string | null = null;
|
|
168
|
+
for (const line of _read_lines(file)) {
|
|
169
|
+
const rec = _parse_json(line);
|
|
170
|
+
if (!rec) continue;
|
|
171
|
+
const ts = typeof rec.timestamp === 'string' ? rec.timestamp : null;
|
|
172
|
+
const p =
|
|
173
|
+
rec.payload && typeof rec.payload === 'object' && !Array.isArray(rec.payload)
|
|
174
|
+
? (rec.payload as Record<string, unknown>)
|
|
175
|
+
: null;
|
|
176
|
+
if (!p) continue;
|
|
177
|
+
if (rec.type === 'session_meta') {
|
|
178
|
+
const git =
|
|
179
|
+
p.git && typeof p.git === 'object' && !Array.isArray(p.git)
|
|
180
|
+
? (p.git as Record<string, unknown>)
|
|
181
|
+
: {};
|
|
182
|
+
if (typeof git.branch === 'string' && git.branch) branch = git.branch;
|
|
183
|
+
} else if (rec.type === 'event_msg' && p.type === 'user_message') {
|
|
184
|
+
const text = typeof p.message === 'string' ? p.message.trim() : '';
|
|
185
|
+
if (text && !text.startsWith('<')) turns.push({ kind: 'user', text, ts });
|
|
186
|
+
} else if (rec.type === 'event_msg' && p.type === 'agent_message') {
|
|
187
|
+
const text = typeof p.message === 'string' ? p.message.trim() : '';
|
|
188
|
+
if (text) turns.push({ kind: 'assistant', text, ts });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { turns, branch };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function load_transcript(
|
|
195
|
+
session: Pick<HandoffSession, 'id' | 'source' | 'transcriptPath'>,
|
|
196
|
+
options: { chatHistoryPath?: string | null } = {},
|
|
197
|
+
): TranscriptData {
|
|
198
|
+
if (session.source === 'chat-history') {
|
|
199
|
+
return _from_chat_history(session.id, options.chatHistoryPath ?? null);
|
|
200
|
+
}
|
|
201
|
+
if (!session.transcriptPath) return { turns: [], branch: null };
|
|
202
|
+
return session.source === 'claude-transcript'
|
|
203
|
+
? _from_claude_transcript(session.transcriptPath)
|
|
204
|
+
: _from_codex_session(session.transcriptPath);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------
|
|
208
|
+
// deterministic section extraction
|
|
209
|
+
// ---------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
function _snippet(text: string, max: number): string {
|
|
212
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
213
|
+
return oneLine.length <= max ? oneLine : oneLine.slice(0, max - 1) + '…';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _word_count(text: string): number {
|
|
217
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Privacy floor for NON-verbatim items: drop refused lines, count them. */
|
|
221
|
+
function _redact_items(items: string[]): { kept: string[]; dropped: number } {
|
|
222
|
+
const kept: string[] = [];
|
|
223
|
+
let dropped = 0;
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
try {
|
|
226
|
+
if (redact_low_impact_entry(item).ok) kept.push(item);
|
|
227
|
+
else dropped += 1;
|
|
228
|
+
} catch {
|
|
229
|
+
dropped += 1; // fail-closed per line
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { kept, dropped };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Verbatim items: a prompt is included ONLY when every one of its lines
|
|
237
|
+
* passes the redactor — otherwise it is withheld whole and its failing
|
|
238
|
+
* lines are counted (drop, never rewrite).
|
|
239
|
+
*/
|
|
240
|
+
function _redact_verbatim(prompts: string[]): { kept: string[]; withheldLines: number } {
|
|
241
|
+
const kept: string[] = [];
|
|
242
|
+
let withheldLines = 0;
|
|
243
|
+
for (const prompt of prompts) {
|
|
244
|
+
const lines = prompt.split('\n');
|
|
245
|
+
let failing = 0;
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
try {
|
|
248
|
+
if (!redact_low_impact_entry(line).ok) failing += 1;
|
|
249
|
+
} catch {
|
|
250
|
+
failing += 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (failing === 0) kept.push(prompt);
|
|
254
|
+
else withheldLines += failing;
|
|
255
|
+
}
|
|
256
|
+
return { kept, withheldLines };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _current_head(root: string): string | null {
|
|
260
|
+
try {
|
|
261
|
+
const proc = spawnSync('git', ['log', '--oneline', '-1'], {
|
|
262
|
+
cwd: root,
|
|
263
|
+
encoding: 'utf-8',
|
|
264
|
+
timeout: 5000,
|
|
265
|
+
env: hardenedSpawnEnv(),
|
|
266
|
+
});
|
|
267
|
+
if (proc.status === 0) return (proc.stdout || '').trim() || null;
|
|
268
|
+
} catch {
|
|
269
|
+
// fall through
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _current_branch(root: string): string {
|
|
275
|
+
try {
|
|
276
|
+
const proc = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
277
|
+
cwd: root,
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
timeout: 5000,
|
|
280
|
+
env: hardenedSpawnEnv(),
|
|
281
|
+
});
|
|
282
|
+
if (proc.status === 0) return (proc.stdout || '').trim() || 'unknown';
|
|
283
|
+
} catch {
|
|
284
|
+
// fall through
|
|
285
|
+
}
|
|
286
|
+
return 'unknown';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface BuildOptions {
|
|
290
|
+
cwd?: string;
|
|
291
|
+
chatHistoryPath?: string | null;
|
|
292
|
+
now?: Date;
|
|
293
|
+
wordCap?: number;
|
|
294
|
+
/** v1 seam — requesting it throws LlmPolishNotImplementedError. */
|
|
295
|
+
llm?: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function build_handoff(
|
|
299
|
+
session: Pick<HandoffSession, 'id' | 'source' | 'transcriptPath' | 'branch'>,
|
|
300
|
+
options: BuildOptions = {},
|
|
301
|
+
): string {
|
|
302
|
+
if (options.llm) throw new LlmPolishNotImplementedError();
|
|
303
|
+
|
|
304
|
+
const root = options.cwd ?? process.cwd();
|
|
305
|
+
const now = options.now ?? new Date();
|
|
306
|
+
const wordCap = options.wordCap ?? WORD_CAP;
|
|
307
|
+
const transcript = load_transcript(session, { chatHistoryPath: options.chatHistoryPath ?? null });
|
|
308
|
+
|
|
309
|
+
const users = transcript.turns.filter((t) => t.kind === 'user');
|
|
310
|
+
const assistants = transcript.turns.filter((t) => t.kind === 'assistant');
|
|
311
|
+
const toolish = transcript.turns.filter((t) => t.kind !== 'user');
|
|
312
|
+
|
|
313
|
+
// User instructions — verbatim or withheld, never rewritten.
|
|
314
|
+
const verbatim = _redact_verbatim(users.map((t) => t.text));
|
|
315
|
+
|
|
316
|
+
// Done — the session's final assistant text.
|
|
317
|
+
const lastAssistant = assistants[assistants.length - 1]?.text ?? '';
|
|
318
|
+
const done = _redact_items(lastAssistant ? [_snippet(lastAssistant, DONE_SNIPPET_CHARS)] : []);
|
|
319
|
+
|
|
320
|
+
// Open + Resume pointer — the last user ask is what was in flight.
|
|
321
|
+
const lastUser = users[users.length - 1]?.text ?? '';
|
|
322
|
+
const open = _redact_items(lastUser ? [`in-flight ask: ${_snippet(lastUser, SNIPPET_CHARS)}`] : []);
|
|
323
|
+
const resume = _redact_items(
|
|
324
|
+
lastUser ? [`continue from the last user ask: ${_snippet(lastUser, SNIPPET_CHARS)}`] : [],
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Errors + fixes — failure-pattern turns, oldest→newest, last 5.
|
|
328
|
+
const errors = _redact_items(
|
|
329
|
+
toolish
|
|
330
|
+
.filter((t) => FAILURE_RE.test(t.text))
|
|
331
|
+
.slice(-MAX_ERRORS)
|
|
332
|
+
.map((t) => _snippet(`${t.tool ? `${t.tool}: ` : ''}${t.text}`, SNIPPET_CHARS)),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Key decisions — decision-pattern assistant turns, last 5.
|
|
336
|
+
const decisions = _redact_items(
|
|
337
|
+
assistants
|
|
338
|
+
.filter((t) => DECISION_RE.test(t.text))
|
|
339
|
+
.slice(-MAX_DECISIONS)
|
|
340
|
+
.map((t) => _snippet(t.text, SNIPPET_CHARS)),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Relevant files — path mentions across the whole transcript, deduped.
|
|
344
|
+
const fileSet = new Set<string>();
|
|
345
|
+
for (const t of transcript.turns) {
|
|
346
|
+
for (const m of t.text.match(FILE_PATH_RE) ?? []) fileSet.add(m);
|
|
347
|
+
}
|
|
348
|
+
const files = _redact_items([...fileSet].slice(-MAX_FILES));
|
|
349
|
+
|
|
350
|
+
// Roadmap — last roadmap-path mention, when present.
|
|
351
|
+
let roadmap: string | null = null;
|
|
352
|
+
for (const t of transcript.turns) {
|
|
353
|
+
for (const m of t.text.match(ROADMAP_RE) ?? []) roadmap = m;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const branch = session.branch ?? transcript.branch ?? 'unknown';
|
|
357
|
+
const droppedTotal =
|
|
358
|
+
done.dropped + open.dropped + resume.dropped + errors.dropped + decisions.dropped + files.dropped;
|
|
359
|
+
|
|
360
|
+
const sections: Array<{ title: string; items: string[]; verbatim?: boolean }> = [
|
|
361
|
+
{ title: 'User instructions (VERBATIM)', items: verbatim.kept, verbatim: true },
|
|
362
|
+
{ title: 'Done', items: done.kept },
|
|
363
|
+
{ title: 'Open', items: open.kept },
|
|
364
|
+
{ title: 'Resume pointer', items: resume.kept },
|
|
365
|
+
{ title: 'Errors + fixes', items: errors.kept },
|
|
366
|
+
{ title: 'Key decisions', items: decisions.kept },
|
|
367
|
+
{ title: 'Relevant files', items: files.kept },
|
|
368
|
+
];
|
|
369
|
+
// trim lowest-priority first; the verbatim record survives longest
|
|
370
|
+
const trimOrder = [
|
|
371
|
+
'Relevant files',
|
|
372
|
+
'Key decisions',
|
|
373
|
+
'Errors + fixes',
|
|
374
|
+
'Done',
|
|
375
|
+
'Open',
|
|
376
|
+
'Resume pointer',
|
|
377
|
+
'User instructions (VERBATIM)',
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const head = branch !== 'unknown' && branch === _current_branch(root) ? _current_head(root) : null;
|
|
381
|
+
|
|
382
|
+
const render = (): string => {
|
|
383
|
+
const lines: string[] = [
|
|
384
|
+
'# Handoff',
|
|
385
|
+
'',
|
|
386
|
+
`Generated: ${now.toISOString()}`,
|
|
387
|
+
`Source-Session: ${session.id}`,
|
|
388
|
+
`Branch: ${branch}`,
|
|
389
|
+
];
|
|
390
|
+
if (head) lines.push(`Last commit: ${head}`);
|
|
391
|
+
if (roadmap) lines.push(`Roadmap: ${roadmap}`);
|
|
392
|
+
if (verbatim.withheldLines > 0 || droppedTotal > 0) {
|
|
393
|
+
lines.push(
|
|
394
|
+
`Privacy floor: [${verbatim.withheldLines + droppedTotal} line(s) withheld by privacy floor]`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (const s of sections) {
|
|
398
|
+
lines.push('', `## ${s.title}`, '');
|
|
399
|
+
if (s.items.length === 0) {
|
|
400
|
+
if (s.verbatim && verbatim.withheldLines > 0) {
|
|
401
|
+
lines.push(`- [${verbatim.withheldLines} line(s) withheld by privacy floor]`);
|
|
402
|
+
} else {
|
|
403
|
+
lines.push('- (none extracted)');
|
|
404
|
+
}
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for (const item of s.items) {
|
|
408
|
+
if (s.verbatim) {
|
|
409
|
+
// verbatim: keep internal newlines, indent continuations
|
|
410
|
+
const [first, ...rest] = item.split('\n');
|
|
411
|
+
lines.push(`- ${first ?? ''}`);
|
|
412
|
+
for (const cont of rest) lines.push(` ${cont}`);
|
|
413
|
+
} else {
|
|
414
|
+
lines.push(`- ${item}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
lines.push('');
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
let text = render();
|
|
423
|
+
for (const title of trimOrder) {
|
|
424
|
+
if (_word_count(text) <= wordCap) break;
|
|
425
|
+
const section = sections.find((s) => s.title === title);
|
|
426
|
+
if (!section) continue;
|
|
427
|
+
while (section.items.length > 0 && _word_count(text) > wordCap) {
|
|
428
|
+
// verbatim: drop the OLDEST prompt first (shift), keep the latest asks
|
|
429
|
+
if (section.verbatim) section.items.shift();
|
|
430
|
+
else section.items.pop();
|
|
431
|
+
text = render();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return text;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------
|
|
438
|
+
// atomic write
|
|
439
|
+
// ---------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
export function handoff_target_path(root: string): string {
|
|
442
|
+
return process.env.AGENT_HANDOFF_CONTEXT_FILE || path.join(root, HANDOFF_CONTEXT_REL);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function write_handoff(text: string, options: { cwd?: string } = {}): string {
|
|
446
|
+
const root = options.cwd ?? process.cwd();
|
|
447
|
+
const target = handoff_target_path(root);
|
|
448
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
449
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
450
|
+
fs.writeFileSync(tmp, text, { encoding: 'utf-8' });
|
|
451
|
+
fs.renameSync(tmp, target);
|
|
452
|
+
return target;
|
|
453
|
+
}
|