zames_pro 2.96.2 → 2.96.4

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/CHANGELOG.md CHANGED
@@ -7,13 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.96.4] - 2026-11-06
11
+
10
12
  ### Fixed
11
13
 
12
- - **info:** keep a /chat and /pwd value on one line
13
- - **status:** flatten a status/doctor value and drop a broken count
14
- - **limits:** never render a negative or NaN counter in /limits
15
- - **restore-history:** keep a plain answer that opens with 'thinking for'
16
- - **commands:** keep a project file/skill/command name on one /context row
14
+ - **tui:** keep a tall suggestion popup from overflowing the layout
15
+ - **scheduler:** scan cron in UTC so a DST fall-back hour is not skipped
16
+ - **context:** parse a YAML block list of command arguments
17
+ - **provider-fallback:** probe a guest-capable provider without the guest flag
18
+ - **cli:** accept the --flag=value spelling of a value flag
19
+
20
+ ## [2.96.3] - 2026-11-06
21
+
22
+ ### Fixed
23
+
24
+ - **banner:** keep a banner field on one line inside the box
25
+ - **scheduler:** reschedule a job whose nextAt is not finite
26
+ - **goal:** keep a condition from injecting its own verdict block
27
+ - **compact:** keep operator and summary text out of the carryover headings
28
+ - **release:** sync the changelog and rebuild its pages inside a release
17
29
 
18
30
  ## [2.96.2] - 2026-11-06
19
31
 
package/dist/banner.js CHANGED
@@ -81,11 +81,16 @@ function clip(s, max) {
81
81
  * (the terminal width minus a safety column).
82
82
  */
83
83
  export function renderStartupBanner(info, contentWidth) {
84
- const title = theme.assistant(' ' + info.title) + theme.dim(' v' + info.version);
85
- const dir = theme.system(' cwd: ') + theme.dir(info.workdir);
86
- const prov = theme.system(' provider: ') + theme.toggleOn(info.provider);
84
+ // Every field is printed INSIDE the box: a newline in the working directory
85
+ // or in the provider label (both come from the config/environment) broke the
86
+ // borders apart, so each value is flattened to one line first.
87
+ const oneLine = (v) => String(v ?? '').replace(/\s+/g, ' ');
88
+ const title = theme.assistant(' ' + oneLine(info.title)) +
89
+ theme.dim(' v' + oneLine(info.version));
90
+ const dir = theme.system(' cwd: ') + theme.dir(oneLine(info.workdir));
91
+ const prov = theme.system(' provider: ') + theme.toggleOn(oneLine(info.provider));
87
92
  const lines = [title, dir, prov];
88
93
  if (info.hint)
89
- lines.push(theme.dim(' ' + info.hint));
94
+ lines.push(theme.dim(' ' + oneLine(info.hint)));
90
95
  return box(lines, contentWidth);
91
96
  }
package/dist/cli-args.js CHANGED
@@ -22,6 +22,14 @@ export const SKIP_VALUE_FLAGS = [
22
22
  export function createCliArgs(argv) {
23
23
  const args = Array.isArray(argv) ? argv : [];
24
24
  const getArg = (flag, fallback = null) => {
25
+ // `--task=fix it` is the common shell spelling: the SEPARATE form below
26
+ // never matched it, so the value was dropped from the task text and the
27
+ // operator's prompt vanished. Check the `=` form first.
28
+ const eq = flag + '=';
29
+ const joined = args.find((a) => a.startsWith(eq));
30
+ if (joined !== undefined && joined.length > eq.length) {
31
+ return joined.slice(eq.length);
32
+ }
25
33
  const i = args.indexOf(flag);
26
34
  // Truthiness, not `!== undefined`: an EMPTY value (`--task ''`) must
27
35
  // fall back too - an empty task is not a task. A following FLAG is not
package/dist/commands.js CHANGED
@@ -580,8 +580,19 @@ export function buildCompactToolResult(todos = [], goal = null) {
580
580
  * they are session state the model-written summary is not guaranteed to keep.
581
581
  */
582
582
  export function buildCompactCarryover(summary, task, todos = [], goal = null) {
583
- const body = String(summary ?? '').trim();
584
- const goalText = String(task ?? '').trim();
583
+ const NL2 = NL + NL;
584
+ // The carried text becomes the FIRST USER TURN of the new chat. The task is
585
+ // OPERATOR text and the summary is MODEL text: a line starting with `## `
586
+ // would fake one of the SESSION blocks this function appends itself (a todo
587
+ // checklist / a goal the agent never had), so such lines are indented out of
588
+ // the heading namespace.
589
+ const noHeadings = (v) => String(v ?? '')
590
+ .split(NL)
591
+ .map((l) => (/^## /.test(l) ? ' ' + l : l))
592
+ .join(NL)
593
+ .trim();
594
+ const body = noHeadings(summary);
595
+ const goalText = noHeadings(task);
585
596
  let out = 'Context carried over from a previous chat (compacted). ' +
586
597
  'Treat it as the history of our work so far and continue from the current state.';
587
598
  out += NL + NL + body;
package/dist/context.js CHANGED
@@ -332,11 +332,15 @@ function parseFrontmatter(raw) {
332
332
  const userInvokable = typeof out['user-invokable'] === 'string'
333
333
  ? out['user-invokable'].toLowerCase() !== 'false'
334
334
  : undefined;
335
+ // A YAML BLOCK LIST (`arguments:` + ` - env`) folds into one string with
336
+ // the list markers, so splitting on whitespace produced `['-', 'env']`: every
337
+ // declared name was 'missing' and the command was refused. Strip the markers
338
+ // first; the inline form (`arguments: env mode`) still works.
335
339
  const argumentsList = typeof out['arguments'] === 'string'
336
340
  ? out['arguments']
337
341
  .split(/[,\s]+/)
338
342
  .map((s) => s.trim())
339
- .filter(Boolean)
343
+ .filter((s) => s && s !== '-')
340
344
  : undefined;
341
345
  return {
342
346
  name: out.name,
package/dist/goal.js CHANGED
@@ -29,6 +29,15 @@ export function clampGoalCondition(condition) {
29
29
  // is requested; no tools.
30
30
  export function buildEvaluatorPrompt(condition) {
31
31
  const NL = String.fromCharCode(10);
32
+ // The condition is OPERATOR text pasted VERBATIM. A line of it that looks
33
+ // like the requested answer format (`Reply with EXACTLY two lines`,
34
+ // `VERDICT: MET`) would let the operator's own words stand in for the
35
+ // judge's verdict and close an unfinished task as done: such a line is
36
+ // quoted instead of being left at the start of a line.
37
+ const safeCondition = String(condition ?? '')
38
+ .split(NL)
39
+ .map((l) => /^\s*(?:reply with|verdict\s*:|reason\s*:)/i.test(l) ? '> ' + l : l)
40
+ .join(NL);
32
41
  return ('You are a strict COMPLETION EVALUATOR. You do NOT do the work and you do NOT call tools. Read ' +
33
42
  'the conversation above and judge - from what the agent has ALREADY surfaced - whether ' +
34
43
  'the completion condition below is satisfied.' +
@@ -36,7 +45,7 @@ export function buildEvaluatorPrompt(condition) {
36
45
  NL +
37
46
  'Completion condition:' +
38
47
  NL +
39
- condition +
48
+ safeCondition +
40
49
  NL +
41
50
  NL +
42
51
  'Reply with EXACTLY two lines, nothing else:' +
@@ -26,7 +26,10 @@ export async function pickFallbackProvider(active, candidates, probeLogin, hasMa
26
26
  continue;
27
27
  // A live GUEST session is not a fallback (it dies in a few turns), but the
28
28
  // provider is still authorizable — signing in makes it a candidate.
29
- if (c.guestAllowed && c.guestSession !== false) {
29
+ // `guestSession` is OPTIONAL: an older caller that predates the field
30
+ // leaves it undefined, and reading that as "a guest" silently refused to
31
+ // fall back to a signed-in provider with a live session.
32
+ if (c.guestAllowed && c.guestSession === true) {
30
33
  authorizable.push(c.id);
31
34
  continue;
32
35
  }
package/dist/scheduler.js CHANGED
@@ -157,18 +157,24 @@ export function nextCronTime(expr, fromMs) {
157
157
  const c = parseCron(expr);
158
158
  if (!c)
159
159
  return null;
160
- const d = new Date(fromMs);
161
- d.setSeconds(0, 0);
162
- d.setMinutes(d.getMinutes() + 1);
160
+ // The scan steps in UTC MILLISECONDS, not local minutes: on the autumn DST
161
+ // FALL-BACK an hour repeats, and `setMinutes(getMinutes()+1)` visits every
162
+ // wall-clock minute TWICE (then jumps over the second occurrence), so a
163
+ // 02:45 job created after the first 02:45 fired the NEXT DAY. A fixed
164
+ // 60-second step visits every real minute exactly once; the field checks
165
+ // still use the LOCAL getters, so the wall-clock match is unchanged.
166
+ const step = 60_000;
167
+ let t = Math.floor(fromMs / step) * step + step;
163
168
  const limit = fromMs + 4 * 366 * 24 * 60 * 60 * 1000;
164
- while (d.getTime() <= limit) {
169
+ while (t <= limit) {
170
+ const d = new Date(t);
165
171
  if (c.minute.has(d.getMinutes()) &&
166
172
  c.hour.has(d.getHours()) &&
167
173
  c.month.has(d.getMonth() + 1) &&
168
174
  dayMatches(c, d)) {
169
- return d.getTime();
175
+ return t;
170
176
  }
171
- d.setMinutes(d.getMinutes() + 1);
177
+ t += step;
172
178
  }
173
179
  return null;
174
180
  }
@@ -256,6 +262,23 @@ export class Scheduler {
256
262
  due(now) {
257
263
  const fired = [];
258
264
  for (const job of this.jobs) {
265
+ // A NON-FINITE `nextAt` (a restored/hand-edited job, a NaN from a broken
266
+ // date) passed every comparison below: `NaN > now` is false, so the job
267
+ // was reported due on EVERY tick and the reschedule arithmetic kept it
268
+ // NaN forever — the busy loop the zero-interval guard exists to stop.
269
+ if (!Number.isFinite(job.nextAt)) {
270
+ if (job.kind === 'loop' && job.intervalMs && job.intervalMs > 0) {
271
+ job.nextAt = now + job.intervalMs;
272
+ }
273
+ else if (job.kind === 'cron' && job.cron) {
274
+ const nxt = nextCronTime(job.cron, now);
275
+ job.nextAt = nxt === null ? Number.POSITIVE_INFINITY : nxt;
276
+ }
277
+ else {
278
+ job.nextAt = Number.POSITIVE_INFINITY;
279
+ }
280
+ continue;
281
+ }
259
282
  if (job.nextAt > now)
260
283
  continue;
261
284
  fired.push({ ...job });
@@ -20,8 +20,15 @@ export function fullScreenLayout(input) {
20
20
  // width — and the pure helper stays consistent with what it renders.
21
21
  const cols = Math.max(1, Math.floor(input.cols) || 80);
22
22
  const box = inputBoxRows(input.prompt, input.buf, input.cursor, cols, input.borderCols ?? 2);
23
- const suggestRows = Math.max(0, Math.floor(input.suggestRows) || 0);
24
- const panelRows = HEADER_ROWS + STATUS_ROWS + box + suggestRows + HINT_ROWS;
23
+ const wanted = Math.max(0, Math.floor(input.suggestRows) || 0);
24
+ // The layout must FIT the terminal: with a tall popup (a project with many
25
+ // custom commands) the reserved rows exceeded `termRows`, `logHeight` was
26
+ // clamped to 1 and the panel still overflowed — the input rows were pushed
27
+ // below the screen. The popup gives way first (it is a hint list), the log
28
+ // keeps at least one row.
29
+ const fixed = HEADER_ROWS + STATUS_ROWS + box + HINT_ROWS;
30
+ const suggestRows = Math.max(0, Math.min(wanted, termRows - fixed - 1));
31
+ const panelRows = fixed + suggestRows;
25
32
  const logHeight = Math.max(1, termRows - panelRows);
26
33
  // The caret counts the SAME viewport the reservation uses.
27
34
  // The suggestion popup is drawn ABOVE the box and the box does NOT move, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.96.2",
3
+ "version": "2.96.4",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {