@unpolarize/code-sessions 0.1.0 → 0.2.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/dist/{chunk-ZJG2DWAK.js → chunk-HV6FQJPS.js} +301 -35
- package/dist/cli.js +17 -1
- package/dist/index.js +21 -1
- package/package.json +15 -5
- package/src/cli.ts +16 -0
- package/src/cliargs.ts +2 -0
- package/src/commands.ts +34 -2
- package/src/fork.test.ts +80 -0
- package/src/fork.ts +91 -0
- package/src/index.ts +2 -0
- package/src/index_store/db.ts +39 -8
- package/src/index_store/sync.ts +6 -0
- package/src/insights/heuristics.test.ts +23 -1
- package/src/insights/heuristics.ts +48 -1
- package/src/insights/labeler.test.ts +4 -1
- package/src/insights/labeler.ts +18 -5
- package/src/insights/llm.test.ts +1 -1
- package/src/insights/llm.ts +13 -5
- package/src/insights/provider.ts +7 -2
- package/src/skills/index.ts +2 -0
- package/src/skills/install.ts +52 -0
- package/src/skills/skills.test.ts +42 -0
- package/src/skills/templates.ts +48 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Signal, Turn } from '@unpolarize/code-sessions-schema';
|
|
1
|
+
import type { Intent, Signal, Turn } from '@unpolarize/code-sessions-schema';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Deterministic, LLM-free signal derivation. Runs regardless of provider so the
|
|
@@ -104,3 +104,50 @@ export function deriveTags(turns: Turn[]): string[] {
|
|
|
104
104
|
for (const t of turns) for (const c of t.tool_calls) tags.add(c.name);
|
|
105
105
|
return [...tags].slice(0, 12);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/** Map an edited file path to a coarse project id (…/projects/<id>, …/docs → docs). */
|
|
109
|
+
export function projectIdFromPath(p: string): string | null {
|
|
110
|
+
const segs = p.split('/').filter(Boolean);
|
|
111
|
+
const i = segs.indexOf('projects');
|
|
112
|
+
if (i >= 0 && segs[i + 1] === 'ai' && segs[i + 2]) return `ai/${segs[i + 2]}`;
|
|
113
|
+
if (i >= 0 && segs[i + 1]) return segs[i + 1]!;
|
|
114
|
+
if (segs.includes('docs')) return 'docs';
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Projects the session touched, from Edit/Write/Read tool file paths. */
|
|
119
|
+
export function deriveProjects(turns: Turn[]): string[] {
|
|
120
|
+
const set = new Set<string>();
|
|
121
|
+
for (const t of turns) {
|
|
122
|
+
for (const c of t.tool_calls) {
|
|
123
|
+
const fp = (c.input as { file_path?: string; path?: string } | undefined)?.file_path
|
|
124
|
+
?? (c.input as { path?: string } | undefined)?.path;
|
|
125
|
+
if (typeof fp === 'string') {
|
|
126
|
+
const id = projectIdFromPath(fp);
|
|
127
|
+
if (id) set.add(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...set].sort().slice(0, 12);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const INTENT_PATTERNS: [Intent, RegExp][] = [
|
|
135
|
+
['bugfix', /\b(fix|bug|broken|error|crash|regression|failing|stack ?trace)\b/i],
|
|
136
|
+
['feature', /\b(add|implement|build|create|feature|support|introduce|new )\b/i],
|
|
137
|
+
['refactor', /\b(refactor|clean ?up|simplify|rename|restructure|extract|dedupe)\b/i],
|
|
138
|
+
['research', /\b(research|investigate|explore|compare|evaluate|find out|how (do|does|to)|why)\b/i],
|
|
139
|
+
['docs', /\b(document|docs|readme|write[ -]?up|notes|comment)\b/i],
|
|
140
|
+
['review', /\b(review|audit|critique|check|inspect)\b/i],
|
|
141
|
+
['ops', /\b(deploy|release|publish|install|configure|ci\/?cd|pipeline|infra)\b/i],
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/** Classify the session's intent from the first substantive user prompt. */
|
|
145
|
+
export function deriveIntent(turns: Turn[]): Intent | undefined {
|
|
146
|
+
const firstUser = turns.find((t) => t.role === 'user' && t.text.trim().length > 0);
|
|
147
|
+
if (!firstUser) return undefined;
|
|
148
|
+
const text = firstUser.text;
|
|
149
|
+
for (const [intent, re] of INTENT_PATTERNS) {
|
|
150
|
+
if (re.test(text)) return intent;
|
|
151
|
+
}
|
|
152
|
+
return 'other';
|
|
153
|
+
}
|
|
@@ -52,8 +52,11 @@ describe('labelSession', () => {
|
|
|
52
52
|
expect(ins!.generated_at).toBe('2026-06-20T09:00:00Z');
|
|
53
53
|
expect(existsSync(insightsFile(dir))).toBe(true);
|
|
54
54
|
|
|
55
|
+
expect(ins!.intent).toBe('bugfix');
|
|
56
|
+
expect(ins!.tags).toContain('Edit'); // tags live on the insights record
|
|
55
57
|
const env = JSON.parse(readFileSync(envelopeFile(dir), 'utf8'));
|
|
56
|
-
expect(env.labels).toContain('
|
|
58
|
+
expect(env.labels).toContain('intent:bugfix'); // envelope labels = intent/topic/projects
|
|
59
|
+
expect(env.labels.some((l: string) => l.startsWith('intent:'))).toBe(true);
|
|
57
60
|
});
|
|
58
61
|
});
|
|
59
62
|
|
package/src/insights/labeler.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { CodeSessionsConfig } from '../config';
|
|
|
11
11
|
import { envelopeFile, insightsFile } from '../store/paths';
|
|
12
12
|
import { listSessionDirs } from '../store/scan';
|
|
13
13
|
import { readTurns } from '../store/writer';
|
|
14
|
-
import { deriveSignals, deriveTags, guessTopic } from './heuristics';
|
|
14
|
+
import { deriveIntent, deriveProjects, deriveSignals, deriveTags, guessTopic } from './heuristics';
|
|
15
15
|
import { FakeProvider, type LabelResult, type Provider } from './provider';
|
|
16
16
|
import { LlmProvider, claudeRunner, grokRunner, ollamaRunner } from './llm';
|
|
17
17
|
|
|
@@ -73,7 +73,7 @@ export async function labelSession(
|
|
|
73
73
|
if (turns.length === 0) return undefined;
|
|
74
74
|
|
|
75
75
|
const heuristicSignals = deriveSignals(turns);
|
|
76
|
-
let provided: LabelResult = { tags: [], signals: [] };
|
|
76
|
+
let provided: LabelResult = { tags: [], projects: [], signals: [] };
|
|
77
77
|
try {
|
|
78
78
|
provided = await provider.label({ sessionId: identity.sessionId, host: identity.host, turns });
|
|
79
79
|
} catch {
|
|
@@ -81,7 +81,9 @@ export async function labelSession(
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const topic = provided.topic ?? guessTopic(turns);
|
|
84
|
+
const intent = provided.intent ?? deriveIntent(turns);
|
|
84
85
|
const tags = [...new Set([...provided.tags, ...deriveTags(turns)])].slice(0, 16);
|
|
86
|
+
const projects = [...new Set([...provided.projects, ...deriveProjects(turns)])].slice(0, 16);
|
|
85
87
|
const signals = dedupeSignals([...heuristicSignals, ...provided.signals]);
|
|
86
88
|
|
|
87
89
|
const insights: Insights = {
|
|
@@ -91,17 +93,22 @@ export async function labelSession(
|
|
|
91
93
|
generated_at: opts.now ?? new Date().toISOString(),
|
|
92
94
|
provider: provider.name,
|
|
93
95
|
tags,
|
|
96
|
+
projects,
|
|
94
97
|
signals,
|
|
95
98
|
};
|
|
96
99
|
if (topic) insights.topic = topic;
|
|
100
|
+
if (intent) insights.intent = intent;
|
|
97
101
|
if (provided.summary) insights.summary = provided.summary;
|
|
98
102
|
|
|
99
103
|
writeJsonAtomic(insightsFile(sessionDir), insights);
|
|
100
|
-
updateEnvelopeLabels(sessionDir, topic,
|
|
104
|
+
updateEnvelopeLabels(sessionDir, { topic, intent, projects });
|
|
101
105
|
return insights;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
function updateEnvelopeLabels(
|
|
108
|
+
function updateEnvelopeLabels(
|
|
109
|
+
sessionDir: string,
|
|
110
|
+
l: { topic?: string; intent?: string; projects: string[] },
|
|
111
|
+
): void {
|
|
105
112
|
const path = envelopeFile(sessionDir);
|
|
106
113
|
if (!existsSync(path)) return;
|
|
107
114
|
let env: SessionEnvelope;
|
|
@@ -110,7 +117,13 @@ function updateEnvelopeLabels(sessionDir: string, topic: string | undefined, tag
|
|
|
110
117
|
} catch {
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
|
-
env.labels = [
|
|
120
|
+
env.labels = [
|
|
121
|
+
...new Set([
|
|
122
|
+
...(l.intent ? [`intent:${l.intent}`] : []),
|
|
123
|
+
...(l.topic ? [l.topic] : []),
|
|
124
|
+
...l.projects.map((p) => `project:${p}`),
|
|
125
|
+
]),
|
|
126
|
+
].slice(0, 16);
|
|
114
127
|
writeJsonAtomic(path, env);
|
|
115
128
|
}
|
|
116
129
|
|
package/src/insights/llm.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('parseLabelJson', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('returns empty on non-JSON', () => {
|
|
57
|
-
expect(parseLabelJson('no json here')).toEqual({ tags: [], signals: [] });
|
|
57
|
+
expect(parseLabelJson('no json here')).toEqual({ tags: [], projects: [], signals: [] });
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
|
package/src/insights/llm.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { SIGNAL_KINDS, type Signal } from '@unpolarize/code-sessions-schema';
|
|
2
|
+
import { INTENTS, SIGNAL_KINDS, type Signal } from '@unpolarize/code-sessions-schema';
|
|
3
3
|
import type { LabelRequest, LabelResult, Provider } from './provider';
|
|
4
4
|
|
|
5
5
|
/** Runs a prompt against some agent CLI/API and returns its raw text output. */
|
|
@@ -27,10 +27,12 @@ export function buildPrompt(req: LabelRequest): string {
|
|
|
27
27
|
|
|
28
28
|
return [
|
|
29
29
|
'You label a coding-agent session. Respond with ONLY a JSON object, no prose:',
|
|
30
|
-
'{"topic": string, "
|
|
30
|
+
'{"topic": string, "intent": one of ' +
|
|
31
|
+
INTENTS.join('|') +
|
|
32
|
+
', "tags": string[], "projects": string[], "summary": string, "signals": [{"kind": one of ' +
|
|
31
33
|
SIGNAL_KINDS.join('|') +
|
|
32
34
|
', "severity": "info"|"warn"|"critical", "note": string}]}',
|
|
33
|
-
'topic: 3-6 words. tags: tools/themes. summary: <=1 sentence. signals: only notable ones.',
|
|
35
|
+
'topic: 3-6 words. intent: what the user wanted. tags: tools/themes. projects: repo/dir names touched. summary: <=1 sentence. signals: only notable ones.',
|
|
34
36
|
'',
|
|
35
37
|
'Transcript:',
|
|
36
38
|
transcript,
|
|
@@ -42,19 +44,25 @@ const KIND_SET = new Set<string>(SIGNAL_KINDS);
|
|
|
42
44
|
export function parseLabelJson(out: string): LabelResult {
|
|
43
45
|
const start = out.indexOf('{');
|
|
44
46
|
const end = out.lastIndexOf('}');
|
|
45
|
-
if (start < 0 || end <= start) return { tags: [], signals: [] };
|
|
47
|
+
if (start < 0 || end <= start) return { tags: [], projects: [], signals: [] };
|
|
46
48
|
let obj: Record<string, unknown>;
|
|
47
49
|
try {
|
|
48
50
|
obj = JSON.parse(out.slice(start, end + 1)) as Record<string, unknown>;
|
|
49
51
|
} catch {
|
|
50
|
-
return { tags: [], signals: [] };
|
|
52
|
+
return { tags: [], projects: [], signals: [] };
|
|
51
53
|
}
|
|
52
54
|
const result: LabelResult = {
|
|
53
55
|
tags: Array.isArray(obj.tags) ? obj.tags.filter((t): t is string => typeof t === 'string') : [],
|
|
56
|
+
projects: Array.isArray(obj.projects)
|
|
57
|
+
? obj.projects.filter((t): t is string => typeof t === 'string')
|
|
58
|
+
: [],
|
|
54
59
|
signals: coerceSignals(obj.signals),
|
|
55
60
|
};
|
|
56
61
|
if (typeof obj.topic === 'string') result.topic = obj.topic;
|
|
57
62
|
if (typeof obj.summary === 'string') result.summary = obj.summary;
|
|
63
|
+
if (typeof obj.intent === 'string' && (INTENTS as readonly string[]).includes(obj.intent)) {
|
|
64
|
+
result.intent = obj.intent as LabelResult['intent'];
|
|
65
|
+
}
|
|
58
66
|
return result;
|
|
59
67
|
}
|
|
60
68
|
|
package/src/insights/provider.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Signal, Turn } from '@unpolarize/code-sessions-schema';
|
|
2
|
-
import { deriveTags, guessTopic } from './heuristics';
|
|
1
|
+
import type { Intent, Signal, Turn } from '@unpolarize/code-sessions-schema';
|
|
2
|
+
import { deriveIntent, deriveProjects, deriveTags, guessTopic } from './heuristics';
|
|
3
3
|
|
|
4
4
|
export interface LabelRequest {
|
|
5
5
|
sessionId: string;
|
|
@@ -9,7 +9,9 @@ export interface LabelRequest {
|
|
|
9
9
|
|
|
10
10
|
export interface LabelResult {
|
|
11
11
|
topic?: string;
|
|
12
|
+
intent?: Intent;
|
|
12
13
|
tags: string[];
|
|
14
|
+
projects: string[];
|
|
13
15
|
signals: Signal[];
|
|
14
16
|
summary?: string;
|
|
15
17
|
}
|
|
@@ -26,10 +28,13 @@ export class FakeProvider implements Provider {
|
|
|
26
28
|
async label(req: LabelRequest): Promise<LabelResult> {
|
|
27
29
|
const result: LabelResult = {
|
|
28
30
|
tags: deriveTags(req.turns),
|
|
31
|
+
projects: deriveProjects(req.turns),
|
|
29
32
|
signals: [],
|
|
30
33
|
};
|
|
31
34
|
const topic = guessTopic(req.turns);
|
|
32
35
|
if (topic) result.topic = topic;
|
|
36
|
+
const intent = deriveIntent(req.turns);
|
|
37
|
+
if (intent) result.intent = intent;
|
|
33
38
|
const assistantText = req.turns.find((t) => t.role === 'assistant')?.text;
|
|
34
39
|
if (assistantText) result.summary = assistantText.slice(0, 160);
|
|
35
40
|
return result;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { buildClaudeSkill, buildPromptFile } from './templates';
|
|
5
|
+
|
|
6
|
+
export type SkillAgent = 'claude' | 'codex' | 'grok' | 'all';
|
|
7
|
+
|
|
8
|
+
export interface SkillInstallResult {
|
|
9
|
+
installed: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Resolve where each agent expects user skills/prompts. */
|
|
13
|
+
function targetsFor(agent: SkillAgent, home: string): { agent: string; path: string; content: string }[] {
|
|
14
|
+
const out: { agent: string; path: string; content: string }[] = [];
|
|
15
|
+
const want = (a: string) => agent === 'all' || agent === a;
|
|
16
|
+
if (want('claude')) {
|
|
17
|
+
out.push({
|
|
18
|
+
agent: 'claude',
|
|
19
|
+
path: join(home, '.claude', 'skills', 'cs-label-session', 'SKILL.md'),
|
|
20
|
+
content: buildClaudeSkill(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (want('codex')) {
|
|
24
|
+
out.push({
|
|
25
|
+
agent: 'codex',
|
|
26
|
+
path: join(home, '.codex', 'prompts', 'cs-label-session.md'),
|
|
27
|
+
content: buildPromptFile(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (want('grok')) {
|
|
31
|
+
out.push({
|
|
32
|
+
agent: 'grok',
|
|
33
|
+
path: join(home, '.grok', 'prompts', 'cs-label-session.md'),
|
|
34
|
+
content: buildPromptFile(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Install the CS labeling skill into the requested agents' skill/prompt dirs. */
|
|
41
|
+
export function installSkills(
|
|
42
|
+
opts: { agent?: SkillAgent; home?: string } = {},
|
|
43
|
+
): SkillInstallResult {
|
|
44
|
+
const home = opts.home ?? homedir();
|
|
45
|
+
const installed: string[] = [];
|
|
46
|
+
for (const t of targetsFor(opts.agent ?? 'all', home)) {
|
|
47
|
+
mkdirSync(dirname(t.path), { recursive: true });
|
|
48
|
+
writeFileSync(t.path, t.content);
|
|
49
|
+
installed.push(t.path);
|
|
50
|
+
}
|
|
51
|
+
return { installed };
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { withTempDir } from '../test/tmp';
|
|
5
|
+
import { buildClaudeSkill, buildLabelSkillBody } from './templates';
|
|
6
|
+
import { installSkills } from './install';
|
|
7
|
+
|
|
8
|
+
describe('skill templates', () => {
|
|
9
|
+
it('embeds the intent + signal taxonomy', () => {
|
|
10
|
+
const body = buildLabelSkillBody();
|
|
11
|
+
expect(body).toContain('feature | bugfix | refactor');
|
|
12
|
+
expect(body).toContain('stuck-loop');
|
|
13
|
+
expect(body).toContain('"projects"');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('claude skill carries frontmatter', () => {
|
|
17
|
+
const s = buildClaudeSkill();
|
|
18
|
+
expect(s).toMatch(/^---\nname: cs-label-session/);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('installSkills', () => {
|
|
23
|
+
it('installs into all agents under the given home', () => {
|
|
24
|
+
withTempDir((home) => {
|
|
25
|
+
const res = installSkills({ agent: 'all', home });
|
|
26
|
+
expect(res.installed).toHaveLength(3);
|
|
27
|
+
expect(existsSync(join(home, '.claude', 'skills', 'cs-label-session', 'SKILL.md'))).toBe(true);
|
|
28
|
+
expect(existsSync(join(home, '.codex', 'prompts', 'cs-label-session.md'))).toBe(true);
|
|
29
|
+
expect(existsSync(join(home, '.grok', 'prompts', 'cs-label-session.md'))).toBe(true);
|
|
30
|
+
const claude = readFileSync(join(home, '.claude', 'skills', 'cs-label-session', 'SKILL.md'), 'utf8');
|
|
31
|
+
expect(claude).toContain('intent');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('can target a single agent', () => {
|
|
36
|
+
withTempDir((home) => {
|
|
37
|
+
const res = installSkills({ agent: 'claude', home });
|
|
38
|
+
expect(res.installed).toHaveLength(1);
|
|
39
|
+
expect(res.installed[0]).toContain('.claude');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { INTENTS, SIGNAL_KINDS } from '@unpolarize/code-sessions-schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canonical "label a session" skill, generated from the CS labels taxonomy
|
|
5
|
+
* so it never drifts from the schema. Installed into each agent so it (or the
|
|
6
|
+
* daemon's configured provider) can label topics, intent, projects-touched, and
|
|
7
|
+
* signals, emitting the exact JSON the CS insights pipeline ingests.
|
|
8
|
+
*/
|
|
9
|
+
export function buildLabelSkillBody(): string {
|
|
10
|
+
return `You are labeling a coding-agent session for the code-sessions (CS) store.
|
|
11
|
+
|
|
12
|
+
Read the provided session transcript and emit ONLY a single JSON object — no prose, no code fence:
|
|
13
|
+
|
|
14
|
+
\`\`\`json
|
|
15
|
+
{
|
|
16
|
+
"topic": "3-6 word summary of what the session was about",
|
|
17
|
+
"intent": "one of: ${INTENTS.join(' | ')}",
|
|
18
|
+
"tags": ["short", "themes", "or", "tool", "names"],
|
|
19
|
+
"projects": ["repo-or-dir-names-touched"],
|
|
20
|
+
"signals": [
|
|
21
|
+
{ "kind": "one of: ${SIGNAL_KINDS.join(' | ')}", "severity": "info|warn|critical", "note": "why" }
|
|
22
|
+
],
|
|
23
|
+
"summary": "one sentence"
|
|
24
|
+
}
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
Guidance:
|
|
28
|
+
- **intent** = what the user wanted (a feature, a bug fixed, a refactor, research, docs, ops, a review, a chore).
|
|
29
|
+
- **projects** = the projects/repos the session actually edited (from file paths it wrote to), not everything mentioned.
|
|
30
|
+
- **signals** = only notable ones: stuck loops, error-recovery, unusually high cost, very long sessions, tool-heavy stretches, strong negative/positive affect.
|
|
31
|
+
- Keep it terse and machine-parseable. Output the JSON and nothing else.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Claude SKILL.md with frontmatter. */
|
|
35
|
+
export function buildClaudeSkill(): string {
|
|
36
|
+
return `---
|
|
37
|
+
name: cs-label-session
|
|
38
|
+
description: Label a coding session (topic, intent, tags, projects touched, signals) as JSON for the code-sessions store. Use when asked to classify or summarize a session.
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
${buildLabelSkillBody()}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Codex / Grok plain prompt file (no frontmatter). */
|
|
46
|
+
export function buildPromptFile(): string {
|
|
47
|
+
return `# cs-label-session\n\n${buildLabelSkillBody()}\n`;
|
|
48
|
+
}
|