@cmetech/otto 1.3.1 → 1.3.3
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/ollama/model-capabilities.js +15 -4
- package/dist/resources/extensions/ollama/ollama-discovery.js +29 -15
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +26 -2
- package/dist/resources/extensions/workflow/auto-verification.js +9 -2
- package/dist/resources/extensions/workflow/bootstrap/crash-log.js +3 -4
- package/dist/resources/extensions/workflow/worktree-state-projection.js +29 -0
- package/package.json +6 -6
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.test.js +43 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/config-selector.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/config-selector.test.ts +58 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +1 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/terminal-image.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/terminal-image.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/terminal-image.test.js +57 -0
- package/packages/pi-tui/dist/__tests__/terminal-image.test.js.map +1 -0
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +4 -0
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +1 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +24 -1
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/src/__tests__/terminal-image.test.ts +57 -0
- package/packages/pi-tui/src/terminal-image.ts +5 -0
- package/packages/pi-tui/src/tui.ts +26 -2
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install.js +8 -1
- package/src/resources/extensions/ollama/model-capabilities.ts +15 -4
- package/src/resources/extensions/ollama/ollama-discovery.ts +28 -12
- package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +47 -2
- package/src/resources/extensions/ollama/tests/ollama-discovery-priority.test.ts +93 -0
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +26 -2
- package/src/resources/extensions/workflow/auto-verification.ts +11 -2
- package/src/resources/extensions/workflow/bootstrap/crash-log.ts +3 -4
- package/src/resources/extensions/workflow/tests/crash-handler-secondary.test.ts +22 -0
- package/src/resources/extensions/workflow/tests/post-exec-retry-bypass.test.ts +23 -10
- package/src/resources/extensions/workflow/tests/worktree-state-projection.test.ts +41 -1
- package/src/resources/extensions/workflow/worktree-state-projection.ts +33 -0
|
@@ -33,13 +33,37 @@ export interface ReleaseNotesManifest {
|
|
|
33
33
|
|
|
34
34
|
export const RELEASE_NOTES_MANIFEST: ReleaseNotesManifest = {
|
|
35
35
|
truncated: false,
|
|
36
|
-
total:
|
|
36
|
+
total: 19,
|
|
37
37
|
oldestBundled: '1.0.0',
|
|
38
|
-
newestBundled: '1.3.
|
|
38
|
+
newestBundled: '1.3.3',
|
|
39
39
|
historyUrl: 'https://github.com/cmetech/otto-cli/blob/main/CHANGELOG.md',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export const RELEASE_NOTES: ReleaseNote[] = [
|
|
43
|
+
{
|
|
44
|
+
version: '1.3.3',
|
|
45
|
+
date: '2026-06-08',
|
|
46
|
+
headline: 'Maintenance patch rolling up six upstream-ported fixes that landed on `main` since 1.3.2: TUI rendering on JetBrains terminals, pattern-resolution basedir, project-root artifact placement when running inside worktrees, verification-pause diagnostics, per-PID crash-log isolation, and Ollama context-window trust.',
|
|
47
|
+
fixed: [
|
|
48
|
+
'**JetBrains terminal capabilities.** TUI rendering now provides the correct capability set when running under JetBrains\' embedded terminal (`packages/pi-tui`), eliminating layout glitches reported on IntelliJ / WebStorm / GoLand. Closes #31 (ported via PR #77).',
|
|
49
|
+
'**Pattern basedir resolution.** Pattern lookups now resolve against the correct base directory, restoring expected matching behavior for relative glob patterns. Closes #53 (ported via PR #74).',
|
|
50
|
+
'**Project root artifacts in worktrees.** Workflow runs invoked from a `git worktree` now project root-level artifacts (lockfile, configs, generated files) into the worktree itself instead of leaking into the primary checkout. Closes #90 (PR #370).',
|
|
51
|
+
'**Verification pause message shows failing check.** When a workflow pauses after an execution step, the message now surfaces *which* check failed instead of a generic pause string, dramatically shortening the debug loop. Closes #99 (PR #371).',
|
|
52
|
+
'**Crash logs append to single per-PID file.** Crash diagnostics now append to one file per process rather than fragmenting across multiple files, making post-mortem inspection coherent. Closes #343 (PR #374).',
|
|
53
|
+
'**Ollama `/api/show` context + `num_ctx` sync.** The Ollama integration now trusts the model\'s reported context window from `/api/show`, keeps `num_ctx` in lockstep, and corrects `KNOWN_MODELS` drift — preventing silent truncation when a model\'s real context exceeds the hard-coded table. Closes #345 (PR #375).',
|
|
54
|
+
],
|
|
55
|
+
notes: [
|
|
56
|
+
'Internal: upstream-swarm orchestrator skill + autonomy hardening (PRs #75, #76, #78, #79, #80, #81, #82) landed in this window but are tooling-only and have no runtime impact for end users.',
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
version: '1.3.2',
|
|
61
|
+
date: '2026-06-04',
|
|
62
|
+
headline: 'Windows-TUI hotfix on top of 1.3.1: launching `otto` no longer paints three stacked welcome banners on Windows. The CLI rendered correctly all along — it just looked broken because cursor-relative differential updates drifted out of alignment.',
|
|
63
|
+
fixed: [
|
|
64
|
+
'**Stacked TUI frames on Windows launch.** `otto` on Windows (PowerShell inside Windows Terminal) painted the welcome banner three times in scrollback instead of updating one frame in place. Root cause traced to `packages/pi-tui/src/tui.ts`: the differential render path uses *relative* cursor movement (`\\x1b[N A/B`) which depends on `hardwareCursorRow` matching the actual terminal cursor row. The welcome screen\'s full-width yellow rule (`\'─\'.repeat(termWidth)` at `src/welcome-screen.ts:192`) is the canonical auto-wrap edge case; Windows ConPTY\'s cursor-state after writing exactly `termWidth` chars differs from xterm\'s "pending wrap" semantic, so each render drifts the tracked cursor by one row. After ~3 async-driven re-renders during cold-start (workflow\'s `session_start` handler has 5+ `await` boundaries, each potentially scheduling a render), drift compounds and subsequent updates write to wrong rows — the previous frame stays visible. Two pi-tui changes resolve it: (1) re-enable DEC 2026 synchronized output on Windows — modern Windows Terminal supports it and the prior `platform !== "win32"` guard predated that; (2) force `fullRender(true)` (absolute-positioned clear + repaint) for all non-first renders on Windows, bypassing the cursor-relative differential path entirely. Trade-off: tiny additional write traffic per render, hidden by synchronized output. Opt-outs: `PI_DISABLE_SYNC_OUTPUT=1` and `PI_DISABLE_WIN32_FULL_REDRAW=1` for terminals that mis-handle either change.',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
43
67
|
{
|
|
44
68
|
version: '1.3.1',
|
|
45
69
|
date: '2026-06-04',
|
|
@@ -600,6 +600,7 @@ export async function runPostUnitVerification(
|
|
|
600
600
|
// ── Post-execution checks (run after main verification passes for execute-task units) ──
|
|
601
601
|
let postExecChecks: PostExecutionCheckJSON[] | undefined;
|
|
602
602
|
let postExecBlockingFailure = false;
|
|
603
|
+
let postExecFailureSummary: string | null = null;
|
|
603
604
|
|
|
604
605
|
if (result.passed && mid && sid && tid) {
|
|
605
606
|
// Check preferences — respect enhanced_verification and enhanced_verification_post
|
|
@@ -696,6 +697,13 @@ export async function runPostUnitVerification(
|
|
|
696
697
|
const blockingCount = postExecResult.checks.filter(
|
|
697
698
|
(c) => !c.passed && c.blocking
|
|
698
699
|
).length;
|
|
700
|
+
const firstBlockingFailure = postExecResult.checks.find(
|
|
701
|
+
(c) => !c.passed && c.blocking
|
|
702
|
+
);
|
|
703
|
+
if (firstBlockingFailure) {
|
|
704
|
+
postExecFailureSummary =
|
|
705
|
+
`[${firstBlockingFailure.category}] ${firstBlockingFailure.target}: ${firstBlockingFailure.message}`;
|
|
706
|
+
}
|
|
699
707
|
ctx.ui.notify(
|
|
700
708
|
`Post-execution checks failed: ${blockingCount} blocking issue${blockingCount === 1 ? "" : "s"} found`,
|
|
701
709
|
"error"
|
|
@@ -810,12 +818,13 @@ export async function runPostUnitVerification(
|
|
|
810
818
|
s.verificationRetryCount.delete(retryKey);
|
|
811
819
|
s.verificationRetryFailureHashes.delete(retryKey);
|
|
812
820
|
s.pendingVerificationRetry = null;
|
|
821
|
+
const failureDetail = postExecFailureSummary ?? "unknown post-execution check failure";
|
|
813
822
|
ctx.ui.notify(
|
|
814
|
-
`Post-execution checks failed —
|
|
823
|
+
`Post-execution checks failed (${failureDetail}) — pausing for human review`,
|
|
815
824
|
"error",
|
|
816
825
|
);
|
|
817
826
|
await pauseAuto(ctx, pi, {
|
|
818
|
-
message:
|
|
827
|
+
message: `Post-execution checks failed: ${failureDetail}.`,
|
|
819
828
|
category: "unknown",
|
|
820
829
|
});
|
|
821
830
|
return "pause";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* crash-log.ts — Write crash diagnostics to ~/.otto/crash
|
|
2
|
+
* crash-log.ts — Write crash diagnostics to ~/.otto/crash/pid-<pid>.log
|
|
3
3
|
*
|
|
4
4
|
* Zero cross-dependencies: only uses Node.js built-ins so it can be imported
|
|
5
5
|
* safely from uncaughtException / unhandledRejection handlers and from tests
|
|
@@ -11,15 +11,14 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Write a crash log to ~/.otto/crash
|
|
14
|
+
* Write a crash log to ~/.otto/crash/pid-<pid>.log (or $OTTO_HOME/crash/).
|
|
15
15
|
* Never throws — must be safe to call from any error handler.
|
|
16
16
|
*/
|
|
17
17
|
export function writeCrashLog(err: Error, source: string): void {
|
|
18
18
|
try {
|
|
19
19
|
const crashDir = join((process.env.OTTO_HOME ?? process.env.OTTO_HOME) ?? join(homedir(), ".otto"), "crash");
|
|
20
20
|
mkdirSync(crashDir, { recursive: true });
|
|
21
|
-
const
|
|
22
|
-
const logPath = join(crashDir, `${ts}.log`);
|
|
21
|
+
const logPath = join(crashDir, `pid-${process.pid}.log`);
|
|
23
22
|
const lines = [
|
|
24
23
|
`[otto] ${source}: ${err.message}`,
|
|
25
24
|
`timestamp: ${new Date().toISOString()}`,
|
|
@@ -49,6 +49,28 @@ describe('register-extension crash handler secondary fixes (#3348)', () => {
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
test('writeCrashLog appends repeated crashes from one process to a single file', async () => {
|
|
53
|
+
const tmpHome = join(tmpdir(), `otto-crash-test-${randomUUID()}`);
|
|
54
|
+
const origHome = process.env.OTTO_HOME;
|
|
55
|
+
process.env.OTTO_HOME = tmpHome;
|
|
56
|
+
try {
|
|
57
|
+
const { writeCrashLog } = await import('../bootstrap/crash-log.ts');
|
|
58
|
+
writeCrashLog(new Error('first crash'), 'uncaughtException');
|
|
59
|
+
writeCrashLog(new Error('second crash'), 'unhandledRejection');
|
|
60
|
+
|
|
61
|
+
const crashDir = join(tmpHome, 'crash');
|
|
62
|
+
const logs = readdirSync(crashDir).filter((f) => f.endsWith('.log'));
|
|
63
|
+
assert.equal(logs.length, 1, 'repeated writes in one process should share one crash log');
|
|
64
|
+
|
|
65
|
+
const content = readFileSync(join(crashDir, logs[0]), 'utf-8');
|
|
66
|
+
assert.ok(content.includes('first crash'), 'log should contain first error message');
|
|
67
|
+
assert.ok(content.includes('second crash'), 'log should contain second error message');
|
|
68
|
+
} finally {
|
|
69
|
+
process.env.OTTO_HOME = origHome;
|
|
70
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
52
74
|
test('_gsdRejectionGuard is registered for unhandledRejection', () => {
|
|
53
75
|
installEpipeGuard();
|
|
54
76
|
const listener = process.listeners("unhandledRejection").find((candidate) =>
|
|
@@ -359,13 +359,8 @@ describe("Post-execution blocking failure retry bypass", () => {
|
|
|
359
359
|
assert.ok(messages.some((m: string) => m.includes("Verification failed") && m.includes("auto-fix attempt 1/2")));
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
-
test("post-exec failure notification
|
|
363
|
-
|
|
364
|
-
// the appropriate message about cross-task consistency issues.
|
|
365
|
-
// The actual post-exec failure would require specific file/output state
|
|
366
|
-
// that's harder to set up in a unit test, but we can verify the code path exists.
|
|
367
|
-
|
|
368
|
-
createBasicTask();
|
|
362
|
+
test("post-exec failure notification includes failing check details", async () => {
|
|
363
|
+
createPostExecFailureTask();
|
|
369
364
|
writePreferences({
|
|
370
365
|
enhanced_verification: true,
|
|
371
366
|
enhanced_verification_post: true,
|
|
@@ -381,9 +376,27 @@ describe("Post-execution blocking failure retry bypass", () => {
|
|
|
381
376
|
const vctx: VerificationContext = { s, ctx, pi };
|
|
382
377
|
const result = await runPostUnitVerification(vctx, pauseAutoMock);
|
|
383
378
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
assert.equal(result, "pause");
|
|
380
|
+
assert.equal(pauseAutoMock.mock.callCount(), 1);
|
|
381
|
+
const notifyMessages = ctx.ui.notify.mock.calls.map((c: { arguments: unknown[] }) =>
|
|
382
|
+
String(c.arguments[0])
|
|
383
|
+
);
|
|
384
|
+
assert.ok(
|
|
385
|
+
notifyMessages.some(
|
|
386
|
+
(m: string) =>
|
|
387
|
+
m.includes("Post-execution checks failed ([import] src/broken.ts:1") &&
|
|
388
|
+
m.includes("pausing for human review")
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const pauseCallArgs = (pauseAutoMock.mock.calls[0]?.arguments as unknown as unknown[])?.[2] as
|
|
393
|
+
| { message?: string }
|
|
394
|
+
| undefined;
|
|
395
|
+
assert.ok(
|
|
396
|
+
pauseCallArgs?.message?.includes(
|
|
397
|
+
"Post-execution checks failed: [import] src/broken.ts:1"
|
|
398
|
+
)
|
|
399
|
+
);
|
|
387
400
|
});
|
|
388
401
|
|
|
389
402
|
test("uok gate runner persists post-execution gate failures when enabled", async () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// File Purpose: Worktree State Projection Module — typed-Interface contract tests for projectRootToWorktree (ADR-016).
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { WorktreeStateProjection } from "../worktree-state-projection.js";
|
|
@@ -60,6 +60,46 @@ test("projectRootToWorktree is idempotent — repeated calls do not throw", () =
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("projectRootToWorktree forwards root PROJECT.md into isolated worktrees", () => {
|
|
64
|
+
const { dir, cleanup } = makeProjectRoot();
|
|
65
|
+
try {
|
|
66
|
+
const worktree = join(dir, ".otto/workflow/worktrees/M001");
|
|
67
|
+
mkdirSync(join(dir, ".otto/workflow/milestones/M001"), { recursive: true });
|
|
68
|
+
mkdirSync(join(worktree, ".otto/workflow"), { recursive: true });
|
|
69
|
+
|
|
70
|
+
const projectContent = [
|
|
71
|
+
"# Project",
|
|
72
|
+
"",
|
|
73
|
+
"## Milestone Sequence",
|
|
74
|
+
"",
|
|
75
|
+
"- [ ] M001: Foundation — Establish the runnable slice.",
|
|
76
|
+
"",
|
|
77
|
+
].join("\n");
|
|
78
|
+
writeFileSync(join(dir, ".otto/workflow/PROJECT.md"), projectContent);
|
|
79
|
+
writeFileSync(join(dir, ".otto/workflow/REQUIREMENTS.md"), "# Requirements\n");
|
|
80
|
+
writeFileSync(
|
|
81
|
+
join(dir, ".otto/workflow/milestones/M001/M001-ROADMAP.md"),
|
|
82
|
+
"# M001\n",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const workspace = createWorkspace(worktree);
|
|
86
|
+
const scope = scopeMilestone(workspace, "M001");
|
|
87
|
+
const projection = new WorktreeStateProjection();
|
|
88
|
+
|
|
89
|
+
projection.projectRootToWorktree(scope);
|
|
90
|
+
|
|
91
|
+
const projectedProject = join(worktree, ".otto/workflow/PROJECT.md");
|
|
92
|
+
assert.ok(existsSync(projectedProject), "PROJECT.md is available to worktree-bound units");
|
|
93
|
+
assert.equal(readFileSync(projectedProject, "utf-8"), projectContent);
|
|
94
|
+
assert.ok(
|
|
95
|
+
existsSync(join(worktree, ".otto/workflow/milestones/M001/M001-ROADMAP.md")),
|
|
96
|
+
"milestone artifacts still project into the worktree",
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
cleanup();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
63
103
|
// ─── projectWorktreeToRoot — Module contract ────────────────────────────────
|
|
64
104
|
|
|
65
105
|
test("projectWorktreeToRoot exists and accepts a MilestoneScope", () => {
|
|
@@ -172,6 +172,35 @@ const ROOT_DIAGNOSTIC_FILES = [
|
|
|
172
172
|
"metrics.json",
|
|
173
173
|
] as const;
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Root-level .otto/workflow/ projections copied from project root into worktrees for
|
|
177
|
+
* compatibility reads. Project root remains authoritative; copy-back still
|
|
178
|
+
* excludes these markdown projections.
|
|
179
|
+
*/
|
|
180
|
+
const ROOT_FORWARD_PROJECTION_FILES = [
|
|
181
|
+
"DECISIONS.md",
|
|
182
|
+
"REQUIREMENTS.md",
|
|
183
|
+
"PROJECT.md",
|
|
184
|
+
"KNOWLEDGE.md",
|
|
185
|
+
"OVERRIDES.md",
|
|
186
|
+
"QUEUE.md",
|
|
187
|
+
"completed-units.json",
|
|
188
|
+
"metrics.json",
|
|
189
|
+
"mcp.json",
|
|
190
|
+
] as const;
|
|
191
|
+
|
|
192
|
+
function syncRootProjectionFilesToWorktree(prGsd: string, wtGsd: string): void {
|
|
193
|
+
mkdirSync(wtGsd, { recursive: true });
|
|
194
|
+
|
|
195
|
+
for (const file of ROOT_FORWARD_PROJECTION_FILES) {
|
|
196
|
+
const src = join(prGsd, file);
|
|
197
|
+
const dst = join(wtGsd, file);
|
|
198
|
+
if (!existsSync(src) || existsSync(dst)) continue;
|
|
199
|
+
|
|
200
|
+
safeCopy(src, dst, { force: false });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
// ─── Implementation cores ────────────────────────────────────────────────
|
|
176
205
|
//
|
|
177
206
|
// The `_*Impl` exports take raw paths so the deprecated path-string
|
|
@@ -204,6 +233,10 @@ export function _projectRootToWorktreeImpl(
|
|
|
204
233
|
// Compare realpaths and skip when they resolve to the same physical path (#2184).
|
|
205
234
|
if (isSamePath(prGsd, wtGsd)) return;
|
|
206
235
|
|
|
236
|
+
// Root PROJECT/REQUIREMENTS/DECISIONS projections must be readable from a
|
|
237
|
+
// worktree-bound unit; the project root remains authoritative.
|
|
238
|
+
syncRootProjectionFilesToWorktree(prGsd, wtGsd);
|
|
239
|
+
|
|
207
240
|
// Copy milestone directory from project root to worktree — additive only.
|
|
208
241
|
// force:false prevents cpSync from overwriting existing worktree files.
|
|
209
242
|
// Without this, worktree-local files (e.g. VALIDATION.md written
|