@thispointon/kondi-chat 0.1.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/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline System: Barrel Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './types';
|
|
6
|
+
export { pipelineStore, PipelineStore } from './store';
|
|
7
|
+
export { PipelineExecutor, type PipelineExecutorCallbacks, type PlatformAdapter } from './executor';
|
|
8
|
+
export * from './memory-store';
|
|
9
|
+
export * from './run-output';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install Command Auto-Detection
|
|
3
|
+
* Scans project files to determine the appropriate dependency install command.
|
|
4
|
+
* Reuses ReadFileFn type from test-detect.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReadFileFn } from './test-detect';
|
|
8
|
+
|
|
9
|
+
export interface DetectedInstall {
|
|
10
|
+
command: string;
|
|
11
|
+
framework: string;
|
|
12
|
+
confidence: 'high' | 'medium' | 'low';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect the appropriate install command for a project directory.
|
|
17
|
+
* Checks common project files in priority order.
|
|
18
|
+
*
|
|
19
|
+
* @param workingDir Absolute path to the project root
|
|
20
|
+
* @param readFile Optional callback that reads a file and returns its content
|
|
21
|
+
* (or null if not found). When omitted, detection is skipped.
|
|
22
|
+
*/
|
|
23
|
+
export async function detectInstallCommand(
|
|
24
|
+
workingDir: string,
|
|
25
|
+
readFile?: ReadFileFn,
|
|
26
|
+
): Promise<DetectedInstall | null> {
|
|
27
|
+
if (!readFile) return null;
|
|
28
|
+
|
|
29
|
+
const fileExists = async (path: string): Promise<string | null> => {
|
|
30
|
+
try {
|
|
31
|
+
return await readFile(path);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 1. Node.js — lockfile-first detection
|
|
38
|
+
const pnpmLock = await fileExists(`${workingDir}/pnpm-lock.yaml`);
|
|
39
|
+
if (pnpmLock) {
|
|
40
|
+
return { command: 'pnpm install', framework: 'pnpm', confidence: 'high' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const yarnLock = await fileExists(`${workingDir}/yarn.lock`);
|
|
44
|
+
if (yarnLock) {
|
|
45
|
+
return { command: 'yarn install', framework: 'yarn', confidence: 'high' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const packageLock = await fileExists(`${workingDir}/package-lock.json`);
|
|
49
|
+
if (packageLock) {
|
|
50
|
+
return { command: 'npm install', framework: 'npm', confidence: 'high' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const packageJson = await fileExists(`${workingDir}/package.json`);
|
|
54
|
+
if (packageJson) {
|
|
55
|
+
return { command: 'npm install', framework: 'npm', confidence: 'medium' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Python — requirements.txt or pyproject.toml
|
|
59
|
+
const requirements = await fileExists(`${workingDir}/requirements.txt`);
|
|
60
|
+
if (requirements) {
|
|
61
|
+
return { command: 'pip install -r requirements.txt', framework: 'pip', confidence: 'high' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pyprojectToml = await fileExists(`${workingDir}/pyproject.toml`);
|
|
65
|
+
if (pyprojectToml && (pyprojectToml.includes('[project.dependencies]') || pyprojectToml.includes('dependencies'))) {
|
|
66
|
+
return { command: 'pip install -e .', framework: 'pip', confidence: 'medium' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Rust — skip (cargo build handles deps implicitly)
|
|
70
|
+
|
|
71
|
+
// 4. Go — go.mod
|
|
72
|
+
const goMod = await fileExists(`${workingDir}/go.mod`);
|
|
73
|
+
if (goMod) {
|
|
74
|
+
return { command: 'go mod download', framework: 'go', confidence: 'high' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Makefile with install target
|
|
78
|
+
const makefile = await fileExists(`${workingDir}/Makefile`);
|
|
79
|
+
if (makefile && /^install\s*:/m.test(makefile)) {
|
|
80
|
+
return { command: 'make install', framework: 'make', confidence: 'medium' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Memory Store
|
|
3
|
+
* Reads/writes JSONL memory files and patterns from disk via PlatformAdapter.
|
|
4
|
+
* Memory lives at {workingDir}/.kondi/memory/{pipelineId}.jsonl
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MemoryEntry } from './types';
|
|
8
|
+
import type { PlatformAdapter } from './executor';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Path Helpers
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export function getMemoryDir(workingDir: string): string {
|
|
15
|
+
return `${workingDir.replace(/\/$/, '')}/.kondi/memory`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getMemoryFilePath(workingDir: string, pipelineId: string): string {
|
|
19
|
+
return `${getMemoryDir(workingDir)}/${pipelineId}.jsonl`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getPatternsFilePath(workingDir: string, pipelineId: string): string {
|
|
23
|
+
return `${getMemoryDir(workingDir)}/${pipelineId}_patterns.json`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Read Operations
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export async function readAllEntries(
|
|
31
|
+
platform: PlatformAdapter,
|
|
32
|
+
workingDir: string,
|
|
33
|
+
pipelineId: string
|
|
34
|
+
): Promise<MemoryEntry[]> {
|
|
35
|
+
if (!platform.readFile) return [];
|
|
36
|
+
const filePath = getMemoryFilePath(workingDir, pipelineId);
|
|
37
|
+
try {
|
|
38
|
+
const content = await platform.readFile(filePath);
|
|
39
|
+
if (!content) return [];
|
|
40
|
+
return content
|
|
41
|
+
.split('\n')
|
|
42
|
+
.filter((line) => line.trim())
|
|
43
|
+
.map((line) => JSON.parse(line) as MemoryEntry);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readLastEntries(
|
|
50
|
+
platform: PlatformAdapter,
|
|
51
|
+
workingDir: string,
|
|
52
|
+
pipelineId: string,
|
|
53
|
+
n: number
|
|
54
|
+
): Promise<MemoryEntry[]> {
|
|
55
|
+
const all = await readAllEntries(platform, workingDir, pipelineId);
|
|
56
|
+
return all.slice(-n);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function readPatterns(
|
|
60
|
+
platform: PlatformAdapter,
|
|
61
|
+
workingDir: string,
|
|
62
|
+
pipelineId: string
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
if (!platform.readFile) return '';
|
|
65
|
+
const filePath = getPatternsFilePath(workingDir, pipelineId);
|
|
66
|
+
try {
|
|
67
|
+
const content = await platform.readFile(filePath);
|
|
68
|
+
if (!content) return '';
|
|
69
|
+
const data = JSON.parse(content);
|
|
70
|
+
if (!Array.isArray(data.patterns)) return '';
|
|
71
|
+
return data.patterns.map((p: string) => `- ${p}`).join('\n');
|
|
72
|
+
} catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Write Operations
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
export async function appendEntry(
|
|
82
|
+
platform: PlatformAdapter,
|
|
83
|
+
workingDir: string,
|
|
84
|
+
pipelineId: string,
|
|
85
|
+
entry: MemoryEntry
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const filePath = getMemoryFilePath(workingDir, pipelineId);
|
|
88
|
+
const line = JSON.stringify(entry);
|
|
89
|
+
|
|
90
|
+
let existing = '';
|
|
91
|
+
if (platform.readFile) {
|
|
92
|
+
try {
|
|
93
|
+
existing = (await platform.readFile(filePath)) || '';
|
|
94
|
+
} catch {
|
|
95
|
+
// File doesn't exist yet
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newContent = existing
|
|
100
|
+
? (existing.endsWith('\n') ? existing : existing + '\n') + line + '\n'
|
|
101
|
+
: line + '\n';
|
|
102
|
+
|
|
103
|
+
await platform.writeFile(filePath, newContent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Formatting for Templates
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
function formatDate(iso: string): string {
|
|
111
|
+
try {
|
|
112
|
+
return new Date(iso).toISOString().split('T')[0];
|
|
113
|
+
} catch {
|
|
114
|
+
return iso;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatEntriesForTemplate(entries: MemoryEntry[]): string {
|
|
119
|
+
if (entries.length === 0) return '';
|
|
120
|
+
return entries.map((entry) => {
|
|
121
|
+
const header = `--- Run #${entry.runNumber} (${formatDate(entry.runDate)})${entry.compressed ? ' [compressed]' : ''} ---`;
|
|
122
|
+
const captures = Object.entries(entry.captures)
|
|
123
|
+
.map(([stepName, content]) => `[${stepName}]:\n${content}`)
|
|
124
|
+
.join('\n\n');
|
|
125
|
+
return `${header}\n${captures}`;
|
|
126
|
+
}).join('\n\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatEntryCapture(entry: MemoryEntry, stepName: string): string {
|
|
130
|
+
// Try exact match first, then case-insensitive
|
|
131
|
+
if (entry.captures[stepName]) return entry.captures[stepName];
|
|
132
|
+
const key = Object.keys(entry.captures).find(
|
|
133
|
+
(k) => k.toLowerCase() === stepName.toLowerCase()
|
|
134
|
+
);
|
|
135
|
+
return key ? entry.captures[key] : '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Run Numbering
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
export async function getNextRunNumber(
|
|
143
|
+
platform: PlatformAdapter,
|
|
144
|
+
workingDir: string,
|
|
145
|
+
pipelineId: string
|
|
146
|
+
): Promise<number> {
|
|
147
|
+
const entries = await readAllEntries(platform, workingDir, pipelineId);
|
|
148
|
+
if (entries.length === 0) return 1;
|
|
149
|
+
const maxRun = Math.max(...entries.map((e) => e.runNumber));
|
|
150
|
+
return maxRun + 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Memory Context Builder (for template rendering)
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
export interface MemoryContext {
|
|
158
|
+
all: string;
|
|
159
|
+
last: string;
|
|
160
|
+
lastN: (n: number) => string;
|
|
161
|
+
lastCapture: (stepName: string) => string;
|
|
162
|
+
patterns: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function buildMemoryContext(
|
|
166
|
+
platform: PlatformAdapter,
|
|
167
|
+
workingDir: string,
|
|
168
|
+
pipelineId: string
|
|
169
|
+
): Promise<MemoryContext> {
|
|
170
|
+
const allEntries = await readAllEntries(platform, workingDir, pipelineId);
|
|
171
|
+
const patternsStr = await readPatterns(platform, workingDir, pipelineId);
|
|
172
|
+
const lastEntry = allEntries.length > 0 ? allEntries[allEntries.length - 1] : null;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
all: formatEntriesForTemplate(allEntries),
|
|
176
|
+
last: lastEntry ? formatEntriesForTemplate([lastEntry]) : '',
|
|
177
|
+
lastN: (n: number) => formatEntriesForTemplate(allEntries.slice(-n)),
|
|
178
|
+
lastCapture: (stepName: string) =>
|
|
179
|
+
lastEntry ? formatEntryCapture(lastEntry, stepName) : '',
|
|
180
|
+
patterns: patternsStr,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Output Parsers
|
|
3
|
+
*
|
|
4
|
+
* Parsing functions for Claude CLI stream-json and Codex CLI JSONL output.
|
|
5
|
+
* Used by both the GUI caller (Tauri invoke) and CLI caller (Node.js spawn).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ParsedOutput {
|
|
9
|
+
text: string;
|
|
10
|
+
tokensUsed: number;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a model ID corresponds to an OpenAI model (routes to Codex CLI).
|
|
16
|
+
*/
|
|
17
|
+
export function isOpenAIModel(model: string): boolean {
|
|
18
|
+
return /^(gpt-|o[1-9]|codex|davinci|chatgpt)/.test(model);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse raw stream-json output from Claude CLI.
|
|
23
|
+
* Handles the `--output-format stream-json` JSONL stream.
|
|
24
|
+
*/
|
|
25
|
+
export function parseStreamJsonOutput(rawOutput: string): ParsedOutput {
|
|
26
|
+
if (!rawOutput.includes('{"type":')) {
|
|
27
|
+
return { text: rawOutput, tokensUsed: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = rawOutput.split('\n').filter(l => l.trim());
|
|
31
|
+
let resultText = '';
|
|
32
|
+
let isErrorResult = false;
|
|
33
|
+
const textChunks: string[] = [];
|
|
34
|
+
const errorChunks: string[] = [];
|
|
35
|
+
let inputTokens = 0;
|
|
36
|
+
let outputTokens = 0;
|
|
37
|
+
let sessionId: string | undefined;
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
try {
|
|
41
|
+
const json = JSON.parse(line);
|
|
42
|
+
|
|
43
|
+
// Extract session_id from init event or result
|
|
44
|
+
if (json.session_id && !sessionId) {
|
|
45
|
+
sessionId = json.session_id;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (json.type === 'result' && json.result) {
|
|
49
|
+
resultText = typeof json.result === 'string' ? json.result : JSON.stringify(json.result);
|
|
50
|
+
if (json.is_error) isErrorResult = true;
|
|
51
|
+
if (json.session_id) sessionId = json.session_id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (json.type === 'error') {
|
|
55
|
+
const errText = json.error || json.body || JSON.stringify(json);
|
|
56
|
+
errorChunks.push(typeof errText === 'string' ? errText : JSON.stringify(errText));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (json.type === 'assistant' && json.message?.content) {
|
|
60
|
+
for (const block of json.message.content) {
|
|
61
|
+
if (block.type === 'text' && block.text) {
|
|
62
|
+
textChunks.push(block.text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (json.message?.usage) {
|
|
66
|
+
inputTokens += json.message.usage.input_tokens || 0;
|
|
67
|
+
outputTokens += json.message.usage.output_tokens || 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (json.type === 'content_block_delta' && json.delta?.type === 'text_delta' && json.delta.text) {
|
|
72
|
+
textChunks.push(json.delta.text);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Not valid JSON, skip
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parts: string[] = [];
|
|
80
|
+
if (textChunks.length > 0) parts.push(textChunks.join(''));
|
|
81
|
+
if (resultText) {
|
|
82
|
+
const combined = parts.join('');
|
|
83
|
+
if (!combined.endsWith(resultText)) {
|
|
84
|
+
parts.push(isErrorResult ? '\n\nError: ' + resultText : '\n\n' + resultText);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (errorChunks.length > 0) {
|
|
88
|
+
const errText = errorChunks.join('; ');
|
|
89
|
+
if (!parts.some(p => p.includes(errText))) {
|
|
90
|
+
parts.push('\n\nError: ' + errText);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const text = parts.length > 0 ? parts.join('').trim() : rawOutput;
|
|
95
|
+
return { text, tokensUsed: inputTokens + outputTokens, sessionId };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse JSONL output from `codex exec --json`.
|
|
100
|
+
*
|
|
101
|
+
* Event types:
|
|
102
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
103
|
+
* {"type":"turn.started"}
|
|
104
|
+
* {"type":"item.completed","item":{"id":"...","type":"agent_message","text":"..."}}
|
|
105
|
+
* {"type":"item.completed","item":{"id":"...","type":"reasoning","text":"..."}}
|
|
106
|
+
* {"type":"turn.completed","usage":{"input_tokens":N,"cached_input_tokens":N,"output_tokens":N}}
|
|
107
|
+
*/
|
|
108
|
+
export function parseCodexJsonOutput(rawOutput: string): ParsedOutput {
|
|
109
|
+
const lines = rawOutput.split('\n').filter(l => l.trim());
|
|
110
|
+
const textChunks: string[] = [];
|
|
111
|
+
let inputTokens = 0;
|
|
112
|
+
let outputTokens = 0;
|
|
113
|
+
let sessionId: string | undefined;
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
try {
|
|
117
|
+
const json = JSON.parse(line);
|
|
118
|
+
|
|
119
|
+
if (json.type === 'thread.started' && json.thread_id) {
|
|
120
|
+
sessionId = json.thread_id;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (json.type === 'item.completed' && json.item) {
|
|
124
|
+
if (json.item.type === 'agent_message' && json.item.text) {
|
|
125
|
+
textChunks.push(json.item.text);
|
|
126
|
+
}
|
|
127
|
+
// Skip reasoning items — they're internal thought, not output
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (json.type === 'turn.completed' && json.usage) {
|
|
131
|
+
inputTokens += json.usage.input_tokens || 0;
|
|
132
|
+
outputTokens += json.usage.output_tokens || 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (json.type === 'error') {
|
|
136
|
+
const errText = json.message || json.error || JSON.stringify(json);
|
|
137
|
+
textChunks.push(`\n\nError: ${errText}`);
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Not valid JSON, skip
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const text = textChunks.join('\n').trim() || rawOutput;
|
|
145
|
+
return { text, tokensUsed: inputTokens + outputTokens, sessionId };
|
|
146
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Run Output Isolation
|
|
3
|
+
* Structured per-run/per-stage/per-step output directories.
|
|
4
|
+
* Runs live at {workingDir}/kondi-runs/{pipelineName}_run_NNN_date_time/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StepArtifact, StepMeta, RunManifest, OutputType } from './types';
|
|
8
|
+
import type { PlatformAdapter } from './executor';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Name Sanitization
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export function sanitizeFolderName(name: string): string {
|
|
15
|
+
return name
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
18
|
+
.replace(/_+/g, '_')
|
|
19
|
+
.replace(/^_|_$/g, '')
|
|
20
|
+
.slice(0, 40);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Directory Name Builders
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export function buildRunDirName(pipelineName: string, runNumber: number, date: Date): string {
|
|
28
|
+
const safeName = sanitizeFolderName(pipelineName);
|
|
29
|
+
const num = String(runNumber).padStart(3, '0');
|
|
30
|
+
const y = date.getFullYear();
|
|
31
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
32
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
33
|
+
const h = String(date.getHours()).padStart(2, '0');
|
|
34
|
+
const min = String(date.getMinutes()).padStart(2, '0');
|
|
35
|
+
return `${safeName}_run_${num}_${y}-${m}-${d}_${h}${min}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildStageDirName(stageIndex: number, stageName: string): string {
|
|
39
|
+
return `stage_${stageIndex + 1}_${sanitizeFolderName(stageName)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildStepDirName(stepIndex: number, stepName: string): string {
|
|
43
|
+
return `step_${stepIndex + 1}_${sanitizeFolderName(stepName)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRunsBaseDir(workingDir: string): string {
|
|
47
|
+
return `${workingDir.replace(/\/$/, '')}/kondi-runs`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildStepOutputDir(
|
|
51
|
+
runDir: string,
|
|
52
|
+
stageIndex: number,
|
|
53
|
+
stageName: string,
|
|
54
|
+
stepIndex: number,
|
|
55
|
+
stepName: string
|
|
56
|
+
): string {
|
|
57
|
+
const stageDir = buildStageDirName(stageIndex, stageName);
|
|
58
|
+
const stepDir = buildStepDirName(stepIndex, stepName);
|
|
59
|
+
return `${runDir}/${stageDir}/${stepDir}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// File Extension Mapping
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export function getOutputExtension(outputType: OutputType): string {
|
|
67
|
+
switch (outputType) {
|
|
68
|
+
case 'json': return '.json';
|
|
69
|
+
default: return '.md';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Write Operations
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export async function writeStepOutput(
|
|
78
|
+
platform: PlatformAdapter,
|
|
79
|
+
stepDir: string,
|
|
80
|
+
artifact: StepArtifact,
|
|
81
|
+
meta: StepMeta
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
const ext = getOutputExtension(meta.outputType);
|
|
84
|
+
const outputPath = `${stepDir}/output${ext}`;
|
|
85
|
+
|
|
86
|
+
await platform.writeFile(outputPath, artifact.content);
|
|
87
|
+
await platform.writeFile(`${stepDir}/_meta.json`, JSON.stringify(meta, null, 2));
|
|
88
|
+
|
|
89
|
+
return outputPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeDeliberationFiles(
|
|
93
|
+
platform: PlatformAdapter,
|
|
94
|
+
stepDir: string,
|
|
95
|
+
deliberationMd: string,
|
|
96
|
+
decisionMd?: string
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
await platform.writeFile(`${stepDir}/deliberation.md`, deliberationMd);
|
|
99
|
+
if (decisionMd) {
|
|
100
|
+
await platform.writeFile(`${stepDir}/decision.md`, decisionMd);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function writeRunManifest(
|
|
105
|
+
platform: PlatformAdapter,
|
|
106
|
+
runDir: string,
|
|
107
|
+
manifest: RunManifest
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
await platform.writeFile(
|
|
110
|
+
`${runDir}/_manifest.json`,
|
|
111
|
+
JSON.stringify(manifest, null, 2)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Run Pruning
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
export async function pruneOldRuns(
|
|
120
|
+
platform: PlatformAdapter,
|
|
121
|
+
runsBaseDir: string,
|
|
122
|
+
pipelineName: string,
|
|
123
|
+
maxRetained: number
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
if (maxRetained <= 0 || !platform.runCommand) return;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const prefix = sanitizeFolderName(pipelineName);
|
|
129
|
+
const result = await platform.runCommand(
|
|
130
|
+
`ls -1d ${prefix}_run_* 2>/dev/null | sort`,
|
|
131
|
+
runsBaseDir
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!result.success || !result.stdout.trim()) return;
|
|
135
|
+
|
|
136
|
+
const dirs = result.stdout.trim().split('\n').filter(Boolean);
|
|
137
|
+
if (dirs.length <= maxRetained) return;
|
|
138
|
+
|
|
139
|
+
const toDelete = dirs.slice(0, dirs.length - maxRetained);
|
|
140
|
+
for (const dir of toDelete) {
|
|
141
|
+
if (new RegExp(`^${prefix}_run_\\d{3}_`).test(dir)) {
|
|
142
|
+
await platform.runCommand(`rm -rf "${dir}"`, runsBaseDir);
|
|
143
|
+
console.log(`[RunOutput] Pruned old run: ${dir}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn('[RunOutput] Failed to prune old runs:', err);
|
|
148
|
+
}
|
|
149
|
+
}
|