@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. 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
+ }