@kognai/build 0.6.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.
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * kognai — interactive REPL for the Kognai sovereign orchestrator runtime.
5
+ *
6
+ * Type `kognai` from any directory. Banner, workspace info, then a prompt
7
+ * where you submit goals or tasks. Each command spawns `kognai-build` as
8
+ * a subprocess so you see the full canonical orchestrator UI for every run.
9
+ *
10
+ * Slash commands:
11
+ * /goal <text> decompose into 1-5 tasks, run each through pipeline
12
+ * /task <text> single-task triple-supervisor pipeline
13
+ * /no-compliance ... append to either command to bypass Sup3
14
+ * /help show this
15
+ * /clear clear screen
16
+ * /quit, /exit, Ctrl-D leave
17
+ *
18
+ * Bare text (no slash) defaults to /goal — builders mostly think in goals.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ const node_child_process_1 = require("node:child_process");
22
+ const node_readline_1 = require("node:readline");
23
+ const node_path_1 = require("node:path");
24
+ const node_fs_1 = require("node:fs");
25
+ // TICKET-202: mandate lifecycle commands run in-process against the cwd's
26
+ // .swarm-state/mandates/ (the same store kognai-build --mandate persists to).
27
+ const mandate_1 = require("../lib/mandate");
28
+ // ─── Visual primitives (mirrors kognai-build.ts) ─────────────────────────────
29
+ const C = {
30
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
31
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', gray: '\x1b[90m',
32
+ jade: '\x1b[38;5;79m',
33
+ };
34
+ const out = (s = '') => process.stdout.write(s + '\n');
35
+ const W = 72;
36
+ const VERSION = (() => {
37
+ try {
38
+ const pj = (0, node_path_1.join)(__dirname, '..', 'package.json');
39
+ return (0, node_fs_1.existsSync)(pj) ? JSON.parse((0, node_fs_1.readFileSync)(pj, 'utf-8')).version : '?';
40
+ }
41
+ catch {
42
+ return '?';
43
+ }
44
+ })();
45
+ function visLen(s) { return s.replace(/\x1b\[[0-9;]*m/g, '').length; }
46
+ function sessionId() { return Math.floor(Date.now() / 1000).toString(36).slice(-4); }
47
+ // ─── Welcome banner (entry only — runs once per shell session) ───────────────
48
+ function welcomeBanner(workspace, sid) {
49
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
50
+ const left = `${C.jade}●${C.reset} ${C.bold}KOGNAI RUNTIME${C.reset} ${C.gray}·${C.reset} ${C.jade}${C.bold}READY${C.reset}`;
51
+ const right = `${C.gray}${ts}${C.reset}`;
52
+ const pad = W - visLen(left) - visLen(right) - 4;
53
+ const subLine1 = ` ${C.gray}sovereign orchestrator · v${VERSION} · session #${sid}${C.reset}`;
54
+ const subLine2 = ` ${C.gray}workspace: ${workspace}${C.reset}`;
55
+ out('');
56
+ out(` ${C.jade}╔${'═'.repeat(W - 2)}╗${C.reset}`);
57
+ out(` ${C.jade}║${C.reset} ${left}${' '.repeat(Math.max(2, pad))}${right} ${C.jade}║${C.reset}`);
58
+ out(` ${C.jade}║${C.reset}${subLine1}${' '.repeat(Math.max(0, W - visLen(subLine1) - 2))}${C.jade}║${C.reset}`);
59
+ out(` ${C.jade}║${C.reset}${subLine2}${' '.repeat(Math.max(0, W - visLen(subLine2) - 2))}${C.jade}║${C.reset}`);
60
+ out(` ${C.jade}╚${'═'.repeat(W - 2)}╝${C.reset}`);
61
+ out('');
62
+ out(` ${C.gray}Type ${C.reset}${C.bold}/help${C.reset}${C.gray} for commands, or just describe what you want to build.${C.reset}`);
63
+ out(` ${C.gray}Bare text is treated as a goal. Use ${C.reset}${C.bold}/task${C.reset}${C.gray} for a single deliverable.${C.reset}`);
64
+ out('');
65
+ }
66
+ function showHelp() {
67
+ out('');
68
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}Commands${C.reset}`);
69
+ out('');
70
+ out(` ${C.bold}/goal${C.reset} ${C.gray}<text>${C.reset} decompose into 1-5 tasks, run each through the pipeline`);
71
+ out(` ${C.bold}/task${C.reset} ${C.gray}<text>${C.reset} single-deliverable triple-supervisor pipeline`);
72
+ out(` ${C.bold}/mandate${C.reset} ${C.gray}<goal>${C.reset} sign a PACT mandate (template + cost envelope), then execute`);
73
+ out(` ${C.bold}/mandates${C.reset} list signed mandates + burn/status`);
74
+ out(` ${C.bold}/revoke${C.reset} ${C.gray}<id>${C.reset} revoke a mandate (halts its autonomous run)`);
75
+ out(` ${C.bold}--profile <axis>${C.reset} TICKET-135 optimization axis. Cascades to all tasks;`);
76
+ out(` supervisor lens matches.`);
77
+ out(` ${C.gray}security · reliability · performance · maintainability${C.reset}`);
78
+ out(` ${C.gray}compatibility · usability · portability · compliance${C.reset}`);
79
+ out(` ${C.gray}cost-efficiency · time-to-market · sovereignty${C.reset}`);
80
+ out(` ${C.bold}/no-compliance${C.reset} ${C.gray}...${C.reset} bypass Sup3 compliance review (trusted tasks only)`);
81
+ out(` ${C.bold}/help${C.reset} this`);
82
+ out(` ${C.bold}/clear${C.reset} clear screen`);
83
+ out(` ${C.bold}/version${C.reset} version info`);
84
+ out(` ${C.bold}/quit${C.reset} ${C.gray}or${C.reset} ${C.bold}/exit${C.reset} ${C.gray}or Ctrl-D${C.reset} leave`);
85
+ out('');
86
+ out(` ${C.gray}Examples${C.reset}`);
87
+ out(` ${C.gray}>${C.reset} TypeScript SDK for a TODO REST API`);
88
+ out(` ${C.gray}>${C.reset} /task Stripe webhook handler with idempotency`);
89
+ out(` ${C.gray}>${C.reset} /goal Express CRUD service with Postgres`);
90
+ out('');
91
+ }
92
+ // ─── Locate kognai-build (sibling in same /bin/ directory) ───────────────────
93
+ function locateKognaiBuild() {
94
+ const here = __dirname;
95
+ const local = (0, node_path_1.join)(here, 'kognai-build.js');
96
+ if ((0, node_fs_1.existsSync)(local))
97
+ return local;
98
+ return 'kognai-build'; // fallback to PATH lookup
99
+ }
100
+ const KOGNAI_BUILD = locateKognaiBuild();
101
+ // ─── Mandate lifecycle (TICKET-202) ──────────────────────────────────────────
102
+ function shortId(id) { return id.slice(0, 8); }
103
+ function statusColor(status) {
104
+ return status === 'active' ? C.green
105
+ : status === 'completed' ? C.gray
106
+ : status === 'breached' ? C.red
107
+ : C.yellow; // revoked
108
+ }
109
+ function listMandatesCmd() {
110
+ let mandates = [];
111
+ try {
112
+ mandates = (0, mandate_1.listMandates)();
113
+ }
114
+ catch (e) {
115
+ out(` ${C.red}error:${C.reset} ${e?.message || String(e)}`);
116
+ return;
117
+ }
118
+ out('');
119
+ out(` ${C.bold}${C.jade}◇${C.reset} ${C.bold}Mandates${C.reset} ${C.gray}(.swarm-state/mandates/)${C.reset}`);
120
+ out('');
121
+ if (mandates.length === 0) {
122
+ out(` ${C.gray}none yet — sign one with ${C.reset}${C.bold}/mandate <goal>${C.reset}`);
123
+ out('');
124
+ return;
125
+ }
126
+ for (const m of mandates) {
127
+ const col = statusColor(m.status);
128
+ const burn = `$${m.burn.usdc_consumed.toFixed(4)}/$${m.cost_envelope.max_usdc.toFixed(4)}`;
129
+ out(` ${C.bold}${shortId(m.mandate_id)}${C.reset} ${col}${m.status.padEnd(9)}${C.reset} ${C.gray}${burn} · ${m.burn.tasks_completed} task(s)${C.reset}`);
130
+ out(` ${C.gray}${m.objective.slice(0, 60)}${m.objective.length > 60 ? '…' : ''}${C.reset}`);
131
+ }
132
+ out('');
133
+ out(` ${C.gray}Revoke with ${C.reset}${C.bold}/revoke <id>${C.reset}${C.gray} (8-char prefix is fine).${C.reset}`);
134
+ out('');
135
+ }
136
+ function revokeCmd(rest) {
137
+ const parts = rest.trim().split(/\s+/);
138
+ const idArg = (parts.shift() || '').replace(/[^a-fA-F0-9]/g, ''); // strip 0x / ellipsis
139
+ if (!idArg) {
140
+ out(` ${C.yellow}usage:${C.reset} /revoke <mandate-id> [reason]`);
141
+ return;
142
+ }
143
+ const reason = parts.join(' ').trim() || 'founder revoked via REPL';
144
+ let mandates = [];
145
+ try {
146
+ mandates = (0, mandate_1.listMandates)();
147
+ }
148
+ catch {
149
+ mandates = [];
150
+ }
151
+ const matches = mandates.filter((m) => m.mandate_id.startsWith(idArg));
152
+ if (matches.length === 0) {
153
+ out(` ${C.yellow}no mandate matches${C.reset} ${idArg} ${C.gray}— /mandates to list${C.reset}`);
154
+ return;
155
+ }
156
+ if (matches.length > 1) {
157
+ out(` ${C.yellow}ambiguous id${C.reset} ${idArg} — matches ${matches.length}; use more characters`);
158
+ return;
159
+ }
160
+ const target = matches[0];
161
+ if (target.status === 'revoked') {
162
+ out(` ${C.gray}mandate ${shortId(target.mandate_id)} is already revoked.${C.reset}`);
163
+ return;
164
+ }
165
+ try {
166
+ const m = (0, mandate_1.revokeMandate)(target.mandate_id, reason);
167
+ out(` ${C.jade}◇${C.reset} Mandate ${C.bold}${shortId(m.mandate_id)}${C.reset} ${C.yellow}revoked${C.reset} ${C.gray}— ${reason}${C.reset}`);
168
+ out(` ${C.gray}Autonomous run halts at the next task boundary.${C.reset}`);
169
+ }
170
+ catch (e) {
171
+ out(` ${C.red}error:${C.reset} ${e?.message || String(e)}`);
172
+ }
173
+ }
174
+ // ─── Command dispatch ────────────────────────────────────────────────────────
175
+ function runSubcommand(args) {
176
+ // spawnSync with stdio:'inherit' streams the subprocess output live —
177
+ // user sees the canonical banner + sections + verdict for each run.
178
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [KOGNAI_BUILD, ...args], {
179
+ stdio: 'inherit',
180
+ env: process.env,
181
+ });
182
+ if (r.status !== 0 && r.status !== 1) {
183
+ // Exit 0 = ship, 1 = reject (both expected). Anything else = real error.
184
+ out(` ${C.yellow}(subcommand exited with code ${r.status})${C.reset}`);
185
+ }
186
+ }
187
+ function parseAndDispatch(line) {
188
+ const trimmed = line.trim();
189
+ if (!trimmed)
190
+ return;
191
+ // Slash commands
192
+ if (trimmed === '/help' || trimmed === '/h' || trimmed === '/?') {
193
+ showHelp();
194
+ return;
195
+ }
196
+ if (trimmed === '/clear') {
197
+ process.stdout.write('\x1b[2J\x1b[H');
198
+ return;
199
+ }
200
+ if (trimmed === '/version' || trimmed === '/v') {
201
+ out(` ${C.bold}@kognai/build${C.reset} v${VERSION}`);
202
+ out(` ${C.gray}bin: ${KOGNAI_BUILD}${C.reset}`);
203
+ return;
204
+ }
205
+ if (trimmed === '/quit' || trimmed === '/exit' || trimmed === '/q') {
206
+ out(` ${C.gray}goodbye${C.reset}`);
207
+ process.exit(0);
208
+ }
209
+ // Mandate lifecycle (TICKET-202) — in-process, no subcommand spawn.
210
+ if (trimmed === '/mandates') {
211
+ listMandatesCmd();
212
+ return;
213
+ }
214
+ if (trimmed === '/revoke' || trimmed.startsWith('/revoke ')) {
215
+ revokeCmd(trimmed.replace(/^\/revoke\s*/, ''));
216
+ return;
217
+ }
218
+ // Strip --no-compliance + --profile <axis> from anywhere in the input.
219
+ // --profile maps to TICKET-135 optimization axes (security / reliability / ...).
220
+ const noCompliance = /(^|\s)\/?no-compliance(\s|$)/.test(trimmed);
221
+ let profile;
222
+ const profileMatch = trimmed.match(/(?:^|\s)--?profile\s+(\S+)/);
223
+ if (profileMatch)
224
+ profile = profileMatch[1];
225
+ const cleaned = trimmed
226
+ .replace(/(^|\s)\/?no-compliance(\s|$)/g, ' ')
227
+ .replace(/(?:^|\s)--?profile\s+\S+/g, ' ')
228
+ .trim();
229
+ // /task <text> — single deliverable
230
+ if (cleaned.startsWith('/task ') || cleaned === '/task') {
231
+ const taskText = cleaned.replace(/^\/task\s*/, '').trim();
232
+ if (!taskText) {
233
+ out(` ${C.yellow}usage:${C.reset} /task <description> [--profile <axis>] [--no-compliance]`);
234
+ return;
235
+ }
236
+ const args = [taskText];
237
+ if (profile)
238
+ args.push('--profile', profile);
239
+ if (noCompliance)
240
+ args.push('--no-compliance');
241
+ runSubcommand(args);
242
+ return;
243
+ }
244
+ // /goal <text> — multi-task decomposition
245
+ if (cleaned.startsWith('/goal ') || cleaned === '/goal') {
246
+ const goalText = cleaned.replace(/^\/goal\s*/, '').trim();
247
+ if (!goalText) {
248
+ out(` ${C.yellow}usage:${C.reset} /goal <description> [--profile <axis>] [--no-compliance]`);
249
+ return;
250
+ }
251
+ const args = ['--goal', goalText];
252
+ if (profile)
253
+ args.push('--profile', profile);
254
+ if (noCompliance)
255
+ args.push('--no-compliance');
256
+ runSubcommand(args);
257
+ return;
258
+ }
259
+ // /mandate <text> — decompose, sign a PACT mandate, then execute within envelope
260
+ if (cleaned.startsWith('/mandate ') || cleaned === '/mandate') {
261
+ const goalText = cleaned.replace(/^\/mandate\s*/, '').trim();
262
+ if (!goalText) {
263
+ out(` ${C.yellow}usage:${C.reset} /mandate <goal> [--profile <axis>] [--no-compliance]`);
264
+ return;
265
+ }
266
+ const args = ['--mandate', goalText];
267
+ if (profile)
268
+ args.push('--profile', profile);
269
+ if (noCompliance)
270
+ args.push('--no-compliance');
271
+ runSubcommand(args);
272
+ return;
273
+ }
274
+ // Unknown slash command
275
+ if (cleaned.startsWith('/')) {
276
+ out(` ${C.yellow}unknown command:${C.reset} ${cleaned.split(/\s+/)[0]}`);
277
+ out(` ${C.gray}try${C.reset} /help`);
278
+ return;
279
+ }
280
+ // Bare text → default to /goal
281
+ const args = ['--goal', cleaned];
282
+ if (profile)
283
+ args.push('--profile', profile);
284
+ if (noCompliance)
285
+ args.push('--no-compliance');
286
+ runSubcommand(args);
287
+ }
288
+ // ─── Main REPL ───────────────────────────────────────────────────────────────
289
+ function main() {
290
+ // ANTHROPIC_API_KEY check upfront (subcommands also check, but fail fast here)
291
+ if (!process.env.ANTHROPIC_API_KEY) {
292
+ out('');
293
+ out(` ${C.red}${C.bold}error:${C.reset} ANTHROPIC_API_KEY environment variable not set.`);
294
+ out(` ${C.gray}Run${C.reset} ${C.bold}export ANTHROPIC_API_KEY=sk-ant-...${C.reset} ${C.gray}and retry.${C.reset}`);
295
+ out('');
296
+ process.exit(2);
297
+ }
298
+ welcomeBanner(process.cwd(), sessionId());
299
+ const rl = (0, node_readline_1.createInterface)({
300
+ input: process.stdin,
301
+ output: process.stdout,
302
+ prompt: ` ${C.jade}›${C.reset} `,
303
+ terminal: true,
304
+ });
305
+ rl.prompt();
306
+ rl.on('line', (line) => {
307
+ try {
308
+ parseAndDispatch(line);
309
+ }
310
+ catch (e) {
311
+ out(` ${C.red}error:${C.reset} ${e?.message || String(e)}`);
312
+ }
313
+ out('');
314
+ rl.prompt();
315
+ });
316
+ rl.on('close', () => {
317
+ out('');
318
+ out(` ${C.gray}goodbye${C.reset}`);
319
+ out('');
320
+ process.exit(0);
321
+ });
322
+ }
323
+ main();
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * boot-env.ts — side-effect module that MUST be imported before
5
+ * @kognai/orchestrator-core (TICKET-233 Phase 1).
6
+ *
7
+ * The engine's KSL record-writer captures its write root from KOGNAI_ROOT at
8
+ * module-load time. To keep the CLI's KSL measurement scoped to a bound
9
+ * `.kognai/` workspace (and out of arbitrary builder cwds), we resolve the
10
+ * workspace root here — before the engine loads — and set KOGNAI_ROOT to it.
11
+ *
12
+ * No `.kognai/` found → KOGNAI_ROOT stays unset and the CLI does not tap KSL
13
+ * (see runOneTask), so a plain `npx @kognai/build "..."` writes nothing extra.
14
+ */
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ if (!process.env.KOGNAI_ROOT) {
18
+ let dir = process.cwd();
19
+ // Walk up looking for <dir>/.kognai/config.yaml (mirrors discoverWorkspace).
20
+ for (;;) {
21
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.kognai', 'config.yaml'))) {
22
+ process.env.KOGNAI_ROOT = dir;
23
+ break;
24
+ }
25
+ const parent = (0, node_path_1.dirname)(dir);
26
+ if (parent === dir)
27
+ break; // filesystem root
28
+ dir = parent;
29
+ }
30
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.estimateCost = estimateCost;
4
+ const tool_layer_1 = require("../lib/tool-layer");
5
+ const TIER_RATES = {
6
+ local: 0.0,
7
+ 'cloud-code': 0.17,
8
+ 'cloud-exec': 0.015,
9
+ apex: 0.85,
10
+ };
11
+ const DEFAULT_EXPECTED_TOOL_CALLS = 3;
12
+ const SAFETY_MARGIN = 1.5;
13
+ /**
14
+ * Produce a heuristic cost envelope for a planned set of tasks under a
15
+ * chosen template. This is a pure function with no IO; it composes per-task
16
+ * model baselines with tool-layer cost estimates.
17
+ *
18
+ * Phase 3 (TICKET-135 Accountant via x402) will replace these baselines with
19
+ * a real CFO database query. Until then, the rates here serve as the
20
+ * authoritative envelope for routing decisions.
21
+ */
22
+ function estimateCost(tasks, template) {
23
+ if (!Array.isArray(tasks)) {
24
+ throw new TypeError('estimateCost: tasks must be an array');
25
+ }
26
+ if (!template || typeof template !== 'object') {
27
+ throw new TypeError('estimateCost: template must be a Template object');
28
+ }
29
+ const breakdown = tasks.map((task) => {
30
+ const tierRate = TIER_RATES[task.task_target];
31
+ if (tierRate === undefined) {
32
+ throw new RangeError(`estimateCost: unknown task_target "${task.task_target}" on task ${task.id}`);
33
+ }
34
+ const expectedToolCalls = task.expected_tool_calls ?? DEFAULT_EXPECTED_TOOL_CALLS;
35
+ const model_usdc = tierRate;
36
+ const tool_usdc = (0, tool_layer_1.estimateToolCost)(template.tools, expectedToolCalls);
37
+ const total_usdc = model_usdc + tool_usdc;
38
+ return {
39
+ task_id: task.id,
40
+ model_usdc,
41
+ tool_usdc,
42
+ total_usdc,
43
+ };
44
+ });
45
+ const estimated_usdc = breakdown.reduce((sum, entry) => sum + entry.total_usdc, 0);
46
+ const max_usdc = estimated_usdc * SAFETY_MARGIN;
47
+ const per_task_budget = tasks.length > 0 ? estimated_usdc / tasks.length : 0;
48
+ return {
49
+ estimated_usdc,
50
+ max_usdc,
51
+ per_task_budget,
52
+ breakdown,
53
+ };
54
+ }
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadKnowledge = loadKnowledge;
37
+ exports.renderKnowledgePrefix = renderKnowledgePrefix;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const DEFAULT_MAX_BYTES = 200_000;
41
+ const KNOWLEDGE_EXTENSIONS = ['.md', '.txt'];
42
+ /**
43
+ * Check if a filename has a knowledge-eligible extension.
44
+ */
45
+ function hasKnowledgeExtension(filename) {
46
+ const lower = filename.toLowerCase();
47
+ return KNOWLEDGE_EXTENSIONS.some((ext) => lower.endsWith(ext));
48
+ }
49
+ /**
50
+ * Recursively walk a directory and collect files matching the extension filter.
51
+ */
52
+ function walkDirectory(dirAbs, extensionFilter) {
53
+ const results = [];
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(dirAbs, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ return results;
60
+ }
61
+ for (const entry of entries) {
62
+ const full = path.join(dirAbs, entry.name);
63
+ if (entry.isDirectory()) {
64
+ results.push(...walkDirectory(full, extensionFilter));
65
+ }
66
+ else if (entry.isFile()) {
67
+ if (extensionFilter) {
68
+ if (entry.name.toLowerCase().endsWith(extensionFilter.toLowerCase())) {
69
+ results.push(full);
70
+ }
71
+ }
72
+ else if (hasKnowledgeExtension(entry.name)) {
73
+ results.push(full);
74
+ }
75
+ }
76
+ }
77
+ return results;
78
+ }
79
+ /**
80
+ * Resolve a single knowledge_ref pattern into a list of absolute file paths.
81
+ *
82
+ * Supported patterns:
83
+ * - 'path/to/file.md' — single file
84
+ * - 'path/to/dir' — recursive walk, collect *.md and *.txt
85
+ * - 'path/to/dir/**\/*.md' — explicit recursive glob, ext-filtered
86
+ * - Absolute paths honored as-is; relative paths resolve from workspaceRoot
87
+ */
88
+ function resolvePattern(pattern, workspaceRoot) {
89
+ // Detect explicit recursive glob: <prefix>/**/*.<ext>
90
+ const globMatch = pattern.match(/^(.*?)\/\*\*\/\*(\.[A-Za-z0-9]+)$/);
91
+ if (globMatch) {
92
+ const prefix = globMatch[1];
93
+ const ext = globMatch[2];
94
+ const baseDir = path.isAbsolute(prefix)
95
+ ? prefix
96
+ : path.resolve(workspaceRoot, prefix);
97
+ if (!fs.existsSync(baseDir))
98
+ return [];
99
+ let stat;
100
+ try {
101
+ stat = fs.statSync(baseDir);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ if (!stat.isDirectory())
107
+ return [];
108
+ return walkDirectory(baseDir, ext);
109
+ }
110
+ // Reject other glob syntax in Phase 1 — fall through silently (no match).
111
+ if (pattern.includes('*') || pattern.includes('?')) {
112
+ return [];
113
+ }
114
+ const resolved = path.isAbsolute(pattern)
115
+ ? pattern
116
+ : path.resolve(workspaceRoot, pattern);
117
+ let stat;
118
+ try {
119
+ stat = fs.statSync(resolved);
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ if (stat.isFile()) {
125
+ return [resolved];
126
+ }
127
+ if (stat.isDirectory()) {
128
+ return walkDirectory(resolved);
129
+ }
130
+ return [];
131
+ }
132
+ /**
133
+ * Compute a relative path from workspace root, falling back to absolute
134
+ * when the file lives outside the workspace tree.
135
+ */
136
+ function relativeToWorkspace(absPath, workspaceRoot) {
137
+ const rel = path.relative(workspaceRoot, absPath);
138
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
139
+ return absPath;
140
+ }
141
+ return rel;
142
+ }
143
+ /**
144
+ * Load knowledge from workspace.knowledge_refs.
145
+ *
146
+ * Reads all *.md and *.txt files matching the patterns, concatenates with
147
+ * boundary markers, and caps the total payload at maxBytes. When the cap
148
+ * is hit, the OLDEST files (by mtime) are skipped first so the newest
149
+ * knowledge always wins.
150
+ */
151
+ function loadKnowledge(workspace, maxBytes = DEFAULT_MAX_BYTES) {
152
+ const empty = {
153
+ text: '',
154
+ files_read: [],
155
+ files_skipped: [],
156
+ total_bytes: 0,
157
+ };
158
+ const refs = workspace.knowledge_refs;
159
+ if (!refs || refs.length === 0) {
160
+ return empty;
161
+ }
162
+ const workspaceRoot = workspace.root;
163
+ // 1. Resolve all patterns into a deduplicated absolute path list.
164
+ const seen = new Set();
165
+ const candidates = [];
166
+ for (const pattern of refs) {
167
+ for (const abs of resolvePattern(pattern, workspaceRoot)) {
168
+ if (!seen.has(abs)) {
169
+ seen.add(abs);
170
+ candidates.push(abs);
171
+ }
172
+ }
173
+ }
174
+ if (candidates.length === 0) {
175
+ return empty;
176
+ }
177
+ // 2. Stat each file to capture mtime and size; drop unreadable entries.
178
+ const entries = [];
179
+ for (const absPath of candidates) {
180
+ let stat;
181
+ try {
182
+ stat = fs.statSync(absPath);
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ if (!stat.isFile())
188
+ continue;
189
+ entries.push({
190
+ absPath,
191
+ relPath: relativeToWorkspace(absPath, workspaceRoot),
192
+ mtimeMs: stat.mtimeMs,
193
+ size: stat.size,
194
+ });
195
+ }
196
+ if (entries.length === 0) {
197
+ return empty;
198
+ }
199
+ // 3. Sort newest-first so the most recent knowledge wins under the cap.
200
+ // Older files get skipped when we exceed maxBytes.
201
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
202
+ const includedParts = [];
203
+ const filesRead = [];
204
+ const filesSkipped = [];
205
+ let totalBytes = 0;
206
+ for (const entry of entries) {
207
+ let content;
208
+ try {
209
+ content = fs.readFileSync(entry.absPath, 'utf8');
210
+ }
211
+ catch {
212
+ filesSkipped.push(entry.absPath);
213
+ continue;
214
+ }
215
+ const block = `\n# === ${entry.relPath} ===\n${content}\n`;
216
+ const blockBytes = Buffer.byteLength(block, 'utf8');
217
+ if (totalBytes + blockBytes > maxBytes) {
218
+ filesSkipped.push(entry.absPath);
219
+ continue;
220
+ }
221
+ includedParts.push(block);
222
+ filesRead.push(entry.absPath);
223
+ totalBytes += blockBytes;
224
+ }
225
+ if (filesRead.length === 0) {
226
+ return {
227
+ text: '',
228
+ files_read: [],
229
+ files_skipped: filesSkipped,
230
+ total_bytes: 0,
231
+ };
232
+ }
233
+ return {
234
+ text: includedParts.join(''),
235
+ files_read: filesRead,
236
+ files_skipped: filesSkipped,
237
+ total_bytes: totalBytes,
238
+ };
239
+ }
240
+ /**
241
+ * Render knowledge as a planner-prompt prefix.
242
+ *
243
+ * Returns a system-context string ready to prepend to the planner system
244
+ * prompt. Returns the empty string when no files were read so callers can
245
+ * safely concatenate without guarding.
246
+ */
247
+ function renderKnowledgePrefix(block) {
248
+ if (!block.files_read || block.files_read.length === 0) {
249
+ return '';
250
+ }
251
+ return ('# Workspace Knowledge Base\n\n' +
252
+ 'You are operating in a workspace with the following knowledge. ' +
253
+ 'Read it carefully before decomposing the goal into tasks.\n\n' +
254
+ block.text +
255
+ '\n# End of Workspace Knowledge\n\n');
256
+ }