@polylogicai/polycode 1.1.0 → 1.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/bin/polycode.mjs CHANGED
@@ -15,10 +15,15 @@ import { computeAgencyReceipt, formatReceipt } from '../lib/agency-receipt.mjs';
15
15
  import { fireHook } from '../lib/hooks.mjs';
16
16
  import { compilePacket } from '../lib/compiler.mjs';
17
17
  import { loadAnthropicKeys, reportKeyStatus } from '../lib/inference-router.mjs';
18
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
18
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
19
19
  import { homedir } from 'node:os';
20
- import { join } from 'node:path';
21
- import { randomUUID } from 'node:crypto';
20
+ import { join, dirname, resolve } from 'node:path';
21
+ import { randomUUID, createHash } from 'node:crypto';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
22
27
  import * as readline from 'node:readline/promises';
23
28
  import { stdin, stdout, exit, argv, env, cwd as getCwd } from 'node:process';
24
29
  import 'dotenv/config';
@@ -137,7 +142,7 @@ function loadRules() {
137
142
  return {};
138
143
  }
139
144
 
140
- const VERSION = '1.1.0';
145
+ const VERSION = PACKAGE_JSON.version;
141
146
  const DOCS_URL = 'https://polylogicai.com/polycode';
142
147
 
143
148
  const BANNER = `${C.bold}${C.amber}polycode v${VERSION}${C.reset}
@@ -173,7 +178,57 @@ function resolveConfigDir() {
173
178
  return dir;
174
179
  }
175
180
 
176
- async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionId, hookDir }) {
181
+ // Per-working-directory session log scoping. Running polycode from different
182
+ // directories produces different session logs, so history from one project
183
+ // does not contaminate another. The cwd is hashed and the short prefix used
184
+ // as a directory name under ~/.polycode/canon. A cwd.txt file in the scope
185
+ // directory preserves the original path for debugging.
186
+ function resolveScopedCanonFile(configDir, cwd) {
187
+ const cwdHash = createHash('sha256').update(cwd).digest('hex').slice(0, 12);
188
+ const scopeDir = join(configDir, 'canon', cwdHash);
189
+ if (!existsSync(scopeDir)) mkdirSync(scopeDir, { recursive: true });
190
+ const cwdPointerPath = join(scopeDir, 'cwd.txt');
191
+ if (!existsSync(cwdPointerPath)) {
192
+ try { writeFileSync(cwdPointerPath, cwd + '\n'); } catch {}
193
+ }
194
+ return join(scopeDir, `${new Date().toISOString().slice(0, 10)}.jsonl`);
195
+ }
196
+
197
+ // POLYCODE.md project context. On startup, walk up from cwd looking for a
198
+ // project-context file. If found, its content is injected into the agent's
199
+ // system prompt as project context. Supported names in priority order.
200
+ function findPolycodeMd(startDir) {
201
+ const NAMES = ['POLYCODE.md', 'polycode.md', '.polycode.md'];
202
+ let dir = resolve(startDir);
203
+ for (let depth = 0; depth < 20; depth++) {
204
+ for (const name of NAMES) {
205
+ const candidate = join(dir, name);
206
+ try {
207
+ if (statSync(candidate).isFile()) return candidate;
208
+ } catch {
209
+ // not present, try next
210
+ }
211
+ }
212
+ const parent = dirname(dir);
213
+ if (parent === dir) break;
214
+ dir = parent;
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function loadProjectContext(cwd) {
220
+ const path = findPolycodeMd(cwd);
221
+ if (!path) return { content: null, path: null };
222
+ try {
223
+ const content = readFileSync(path, 'utf8').slice(0, 8000);
224
+ return { content, path };
225
+ } catch {
226
+ return { content: null, path: null };
227
+ }
228
+ }
229
+
230
+ async function runOneShot(message, opts) {
231
+ const { loop, canon, cwd, renderer, state, sessionId, hookDir } = opts;
177
232
  await fireHook('UserPromptSubmit', {
178
233
  session_id: sessionId, cwd, hook_event_name: 'UserPromptSubmit', prompt: message,
179
234
  }, hookDir);
@@ -188,12 +243,14 @@ async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionI
188
243
  state.lastTurnTokens = result.promptTokensUsed;
189
244
  state.lastCompiler = result.compilerProvider;
190
245
 
191
- const receipt = computeAgencyReceipt({
192
- primitivesList: result.primitivesList,
193
- wallClockMs: result.durationMs,
194
- iterations: result.iterations,
195
- });
196
- stdout.write(`${C.dim}${formatReceipt(receipt)} . ${canon.size()} rows in log${C.reset}\n`);
246
+ if (opts.verbose) {
247
+ const receipt = computeAgencyReceipt({
248
+ primitivesList: result.primitivesList,
249
+ wallClockMs: result.durationMs,
250
+ iterations: result.iterations,
251
+ });
252
+ stdout.write(`${C.dim}${formatReceipt(receipt)} . ${canon.size()} rows in log${C.reset}\n`);
253
+ }
197
254
 
198
255
  await fireHook('Stop', { session_id: sessionId, cwd, hook_event_name: 'Stop' }, hookDir);
199
256
  return result;
@@ -202,7 +259,12 @@ async function runOneShot(message, { loop, canon, cwd, renderer, state, sessionI
202
259
  async function runRepl(opts) {
203
260
  const rl = readline.createInterface({ input: stdin, output: stdout });
204
261
  stdout.write(BANNER + '\n');
205
- stdout.write(`${C.dim}session log: ${opts.canon.size()} rows . ${opts.canon.lastHash().slice(0, 12)}...${C.reset}\n`);
262
+ if (opts.projectContextPath) {
263
+ stdout.write(`${C.dim}project context: ${opts.projectContextPath}${C.reset}\n`);
264
+ }
265
+ if (opts.canon.size() > 0) {
266
+ stdout.write(`${C.dim}session log: ${opts.canon.size()} rows${C.reset}\n`);
267
+ }
206
268
  stdout.write(`${C.dim}type /help for commands. ctrl+c or /exit to leave.${C.reset}\n\n`);
207
269
 
208
270
  try {
@@ -246,9 +308,11 @@ async function main() {
246
308
  const model = env.POLYCODE_MODEL || 'moonshotai/kimi-k2-instruct';
247
309
  const cwd = env.POLYCODE_CWD || getCwd();
248
310
  const hookDir = env.POLYCODE_HOOK_DIR || join(configDir, 'hooks');
249
- const canonFile = env.POLYCODE_CANON_FILE || join(configDir, 'canon', `${new Date().toISOString().slice(0, 10)}.jsonl`);
311
+ const canonFile = env.POLYCODE_CANON_FILE || resolveScopedCanonFile(configDir, cwd);
312
+ const verbose = args.includes('--verbose') || args.includes('-V');
250
313
 
251
314
  const canon = createCanon(canonFile);
315
+ const { content: projectContext, path: projectContextPath } = loadProjectContext(cwd);
252
316
 
253
317
  if (args.includes('--history') || args.includes('--log')) {
254
318
  stdout.write(`session log: ${canonFile}\nrows: ${canon.size()}\nlast_hash: ${canon.lastHash()}\n`);
@@ -292,10 +356,10 @@ async function main() {
292
356
  canon_path: canonFile,
293
357
  }, hookDir);
294
358
 
295
- const loop = new AgenticLoop({ apiKey, model, rules });
296
- const renderer = createRenderer(stdout);
359
+ const loop = new AgenticLoop({ apiKey, model, rules, projectContext });
360
+ const renderer = createRenderer(stdout, { verbose });
297
361
  const state = { lastTurnTokens: 0, lastCompiler: null };
298
- const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir };
362
+ const opts = { loop, canon, cwd, renderer, state, sessionId, hookDir, verbose, projectContextPath };
299
363
 
300
364
  const positional = args.filter((a) => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--packet');
301
365
  if (positional.length > 0) {
package/lib/agentic.mjs CHANGED
@@ -23,22 +23,60 @@ const execAsync = promisify(exec);
23
23
 
24
24
  const DEFAULT_MODEL = 'moonshotai/kimi-k2-instruct';
25
25
  const FALLBACK_MODEL = 'llama-3.3-70b-versatile';
26
- const DEFAULT_MAX_ITERATIONS = 12;
26
+ const DEFAULT_MAX_ITERATIONS = 8;
27
27
  const TEMPERATURE = 0.2;
28
28
  const MAX_TOKENS = 4096;
29
29
  const MAX_BASH_TIMEOUT_MS = 30_000;
30
30
  const MAX_OUTPUT_BYTES = 3200;
31
31
 
32
- const SYSTEM_PROMPT = `You are polycode, a terminal coding agent. Each turn you receive the current user message along with a small set of context rows selected from the session log by a separate selection step. You do not need to hold conversation history in your own memory. Produce a short plan and the tool calls needed to address the current message, then call task_done with a one or two sentence summary.
32
+ const SYSTEM_PROMPT_BASE = `You are polycode, a coding assistant that runs in the user's terminal. You help users in three distinct modes. Pick the right mode based on the current user message.
33
+
34
+ Mode 1: CONVERSATIONAL. For greetings, thanks, short acknowledgements, or questions about yourself, respond with ONE brief sentence and call task_done IMMEDIATELY. Do not use any tools.
35
+ Examples:
36
+ User: hello
37
+ You: Hi. I am polycode. What can I help you with?
38
+ (call task_done with exactly that text)
39
+
40
+ User: thanks
41
+ You: You're welcome.
42
+ (call task_done with exactly that text)
43
+
44
+ User: who are you
45
+ You: I am polycode, a coding assistant that runs on your machine with your API keys.
46
+ (call task_done with exactly that text)
47
+
48
+ Mode 2: KNOWLEDGE QUESTION. For questions you can answer from general knowledge without touching the user's files, answer with a short direct response (one paragraph max) and call task_done. Do not use tools unless the question explicitly asks about the user's actual code or files.
49
+ Example:
50
+ User: what is the difference between let and const in javascript?
51
+ You: let allows reassignment, const does not. Both are block-scoped. const still allows mutation of object contents.
52
+ (call task_done with that explanation)
53
+
54
+ Mode 3: CODE TASK. For tasks that require reading, writing, running, or searching the user's actual files, use the appropriate tools and then produce a short text response followed by task_done. Available tools: bash, read_file, write_file, edit_file, glob, grep.
55
+ Example:
56
+ User: read package.json and tell me the main entry
57
+ You: (call read_file on package.json, then respond with the answer, then task_done)
58
+
59
+ Hard rules:
60
+ - Always produce a text message in your response. Never call task_done without first saying something to the user.
61
+ - Never loop more than 3 iterations without producing text. If you are not sure what to do, ask the user and call task_done.
62
+ - Do not explore the filesystem unless the user's current message explicitly asks about their files.
63
+ - Do not assume there is an ongoing task from prior turns unless the current message continues it.
64
+ - If a tool call fails, acknowledge the failure in your text response, do not retry the same operation.
65
+ - Use periods, commas, colons. Not em dashes. No hype words.
66
+
67
+ Verification: every tool call is checked by a deterministic layer before it lands in the session log. Failures are marked REFUTED. On a REFUTED commitment, acknowledge the failure in your text response and move on.`;
68
+
69
+ function buildSystemPrompt(projectContext) {
70
+ if (!projectContext || typeof projectContext !== 'string' || !projectContext.trim()) {
71
+ return SYSTEM_PROMPT_BASE;
72
+ }
73
+ return `${SYSTEM_PROMPT_BASE}
33
74
 
34
- Discipline:
35
- - Use periods, commas, or colons. Not em dashes.
36
- - No hype words: no "revolutionary", "game-changer", "unprecedented".
37
- - Read files before asserting their content. Test before claiming something works.
38
- - Tools available: task_done, bash, read_file, write_file, edit_file, glob, grep.
39
- - When the current user message has been addressed, call task_done.
75
+ PROJECT CONTEXT (from POLYCODE.md in the user's project):
76
+ ${projectContext.slice(0, 6000)}
40
77
 
41
- Every tool call you make is checked by a deterministic verification layer before it is written to the session log. Checks include content grounding, file existence, rule compliance against a forbidden list, and secret scrubbing on tool output. If a check fails, the record is marked REFUTED and you should acknowledge and correct rather than retry the same action.`;
78
+ When answering questions about the project, prefer the information in the PROJECT CONTEXT over reading files, unless the user explicitly asks you to read a file.`;
79
+ }
42
80
 
43
81
  const TOOL_SCHEMAS = [
44
82
  {
@@ -241,11 +279,12 @@ function recoverInlineToolCalls(content) {
241
279
  }
242
280
 
243
281
  export class AgenticLoop {
244
- constructor({ apiKey, model, logger, rules } = {}) {
282
+ constructor({ apiKey, model, logger, rules, projectContext } = {}) {
245
283
  this.apiKey = apiKey;
246
284
  this.defaultModel = model || DEFAULT_MODEL;
247
285
  this.logger = logger || console;
248
286
  this.rules = rules || {};
287
+ this.systemPrompt = buildSystemPrompt(projectContext);
249
288
  }
250
289
 
251
290
  async runTurn({ canon, userMessage, cwd, onEvent, maxIterations }) {
@@ -318,7 +357,7 @@ export class AgenticLoop {
318
357
 
319
358
  // Phase 3 + Phase 4: DISPATCH and ACT
320
359
  const messages = [
321
- { role: 'system', content: SYSTEM_PROMPT },
360
+ { role: 'system', content: this.systemPrompt },
322
361
  { role: 'user', content: ctx.prompt },
323
362
  ];
324
363
 
package/lib/compiler.mjs CHANGED
@@ -18,6 +18,74 @@ const COMPILER_MODEL = 'claude-haiku-4-5-20251001';
18
18
  const COMPILER_MAX_TOKENS = 600;
19
19
  const SUMMARY_ROW_LIMIT = 400;
20
20
 
21
+ // Conversational fast-path. Short user messages that look like greetings,
22
+ // acknowledgements, or self-questions skip the LLM compile step entirely
23
+ // and use a minimal pure-Node packet. Cuts per-turn latency from ~3s to
24
+ // sub-second for these cases and keeps the LLM from being told to continue
25
+ // any prior task when the user clearly is not asking about one.
26
+ const CONVERSATIONAL_MAX_LEN = 80;
27
+ const CONVERSATIONAL_PATTERNS = [
28
+ /^hi+\b/i,
29
+ /^hello\b/i,
30
+ /^hey\b/i,
31
+ /^yo\b/i,
32
+ /^howdy\b/i,
33
+ /^greetings\b/i,
34
+ /^thanks?\b/i,
35
+ /^thank you\b/i,
36
+ /^thx\b/i,
37
+ /^ty\b/i,
38
+ /^ok(ay)?\b/i,
39
+ /^cool\b/i,
40
+ /^nice\b/i,
41
+ /^great\b/i,
42
+ /^got it\b/i,
43
+ /^sure\b/i,
44
+ /^sounds good\b/i,
45
+ /^good\b/i,
46
+ /^how are you\b/i,
47
+ /^how('s| is) it going\b/i,
48
+ /^what's up\b/i,
49
+ /^sup\b/i,
50
+ /^who are you\b/i,
51
+ /^what are you\b/i,
52
+ /^what is polycode\b/i,
53
+ /^what can you do\b/i,
54
+ /^tell me about yourself\b/i,
55
+ /^bye\b/i,
56
+ /^goodbye\b/i,
57
+ /^see you\b/i,
58
+ /^later\b/i,
59
+ ];
60
+
61
+ function isConversational(message) {
62
+ if (!message || typeof message !== 'string') return false;
63
+ const trimmed = message.trim();
64
+ if (trimmed.length === 0 || trimmed.length > CONVERSATIONAL_MAX_LEN) return false;
65
+ return CONVERSATIONAL_PATTERNS.some((re) => re.test(trimmed));
66
+ }
67
+
68
+ function buildConversationalPacket(userMessage, cwd) {
69
+ const lines = [
70
+ `[polycode conversational-fast-path]`,
71
+ `working_directory: ${cwd || process.cwd()}`,
72
+ '',
73
+ `CURRENT USER MESSAGE:`,
74
+ userMessage,
75
+ '',
76
+ `Respond with one short sentence and call task_done immediately. Do not use any tools.`,
77
+ ];
78
+ const prompt = lines.join('\n');
79
+ return {
80
+ prompt,
81
+ estimatedTokens: Math.ceil(prompt.length / 4),
82
+ selectedRows: [],
83
+ compilerProvider: 'conversational-fast-path',
84
+ compilerUsage: null,
85
+ fallback: false,
86
+ };
87
+ }
88
+
21
89
  const COMPILER_SYSTEM_PROMPT = `You are a context selection helper. You receive a summary of a user's append-only session log (each row has an index, a type, and a short preview) and the user's current message. Your job is to return a JSON list of the row indices most relevant to the current message. You never write prose.
22
90
 
23
91
  Selection rules:
@@ -83,6 +151,14 @@ async function selectRelevantRows(canon, userMessage) {
83
151
  }
84
152
 
85
153
  export async function compilePacket(canon, userMessage, cwd) {
154
+ // Fast-path: trivial conversational messages skip the LLM compile step and
155
+ // get a minimal packet that tells the generator to respond briefly and stop.
156
+ // This structurally prevents prior-session contamination from poisoning
157
+ // short greetings and acknowledgements.
158
+ if (isConversational(userMessage)) {
159
+ return buildConversationalPacket(userMessage, cwd);
160
+ }
161
+
86
162
  const selection = await selectRelevantRows(canon, userMessage);
87
163
  const selectedIndices = new Set(selection.selected || []);
88
164
 
package/lib/repl-ui.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  // lib/repl-ui.mjs
2
- // Streaming terminal UI with ANSI colors. Zero external deps.
3
- // The renderer subscribes to events emitted by AgenticLoop.runTurn() and
4
- // prints each phase with a colored prefix. The verdict pill is rendered
5
- // when a commitment is recorded.
2
+ // REPL output renderer. Default mode is compact: show a single collapsed
3
+ // line per tool call, show the agent's text response prominently, hide the
4
+ // internal phase trace. Verbose mode (--verbose) shows every phase, every
5
+ // tool call argument, every tool result preview, every record event.
6
+ // Zero external dependencies; ANSI colors only.
6
7
 
7
8
  export const C = {
8
9
  reset: '\x1b[0m',
@@ -50,10 +51,48 @@ function phasePrefix(name) {
50
51
  return map[name] || `${C.gray}. ${name}${C.reset}`;
51
52
  }
52
53
 
53
- export function createRenderer(stdout) {
54
+ // Compact-mode tool call renderer. Produces a single line per tool call
55
+ // that names the tool and its most-useful argument, no JSON dump.
56
+ function compactToolCallLine(name, args) {
57
+ const a = args || {};
58
+ let arg = '';
59
+ if (name === 'read_file' || name === 'write_file' || name === 'edit_file') {
60
+ arg = a.path || '';
61
+ } else if (name === 'bash') {
62
+ arg = String(a.command || '').slice(0, 60);
63
+ } else if (name === 'glob') {
64
+ arg = a.pattern || '';
65
+ } else if (name === 'grep') {
66
+ arg = a.pattern || '';
67
+ if (a.glob) arg += ` in ${a.glob}`;
68
+ } else if (name === 'task_done') {
69
+ return null; // task_done is implicit in the final response, not shown as an action
70
+ }
71
+ return `${C.dim}${C.gray}·${C.reset} ${C.bold}${name}${C.reset}${C.dim}${arg ? ` ${arg}` : ''}${C.reset}`;
72
+ }
73
+
74
+ // Compact-mode tool result renderer. A single short line per result.
75
+ function compactToolResultLine(name, result) {
76
+ if (name === 'task_done') return null;
77
+ const r = String(result || '');
78
+ if (r.startsWith('error:')) {
79
+ return `${C.dim}${C.red} ${r.slice(0, 140)}${C.reset}`;
80
+ }
81
+ if (r.startsWith('ok:')) {
82
+ return `${C.dim}${C.green} ${r.slice(0, 140)}${C.reset}`;
83
+ }
84
+ // Default: one-line truncated preview
85
+ const preview = r.slice(0, 140).replace(/\n/g, ' ');
86
+ return `${C.dim} ${preview}${C.reset}`;
87
+ }
88
+
89
+ export function createRenderer(stdout, opts = {}) {
90
+ const verbose = Boolean(opts.verbose);
91
+
54
92
  function line(s) { stdout.write(s + '\n'); }
55
93
 
56
- function onEvent(ev) {
94
+ // Verbose event handler: shows every phase, tool call, result, record.
95
+ function onEventVerbose(ev) {
57
96
  if (ev.phase === 'intent') {
58
97
  line(`${phasePrefix('intent')} ${C.dim}active intent resolved${C.reset}`);
59
98
  } else if (ev.phase === 'ground_complete') {
@@ -87,5 +126,30 @@ export function createRenderer(stdout) {
87
126
  }
88
127
  }
89
128
 
90
- return { onEvent, C, verdictPill };
129
+ // Compact event handler: shows tool calls as single-line actions, shows
130
+ // the agent's text response, hides all internal tracing.
131
+ function onEventCompact(ev) {
132
+ if (ev.phase === 'act' && ev.kind === 'message') {
133
+ const content = String(ev.content || '').trim();
134
+ if (content) line(`${C.cyan}${content}${C.reset}`);
135
+ } else if (ev.phase === 'act' && ev.kind === 'tool_call') {
136
+ const out = compactToolCallLine(ev.name, ev.args);
137
+ if (out) line(out);
138
+ } else if (ev.phase === 'act' && ev.kind === 'tool_result') {
139
+ const out = compactToolResultLine(ev.name, ev.result);
140
+ if (out) line(out);
141
+ } else if (ev.phase === 'scrub_blocked') {
142
+ line(`${C.red}refusing to send tool output to the model: contains a recognized secret pattern${C.reset}`);
143
+ } else if (ev.phase === 'error') {
144
+ line(`${C.red}error: ${ev.message}${C.reset}`);
145
+ }
146
+ // All other phases hidden in compact mode.
147
+ }
148
+
149
+ return {
150
+ onEvent: verbose ? onEventVerbose : onEventCompact,
151
+ verbose,
152
+ C,
153
+ verdictPill,
154
+ };
91
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polylogicai/polycode",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "An agentic coding CLI. Runs on your machine with your keys. Every turn is appended to a SHA-256 chained session log, so your history is auditable, replayable, and portable.",
5
5
  "type": "module",
6
6
  "main": "bin/polycode.mjs",