From 503f6041fc22dc45cb72c8851482e8d175ae16cf Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 04:57:18 +0200 Subject: [PATCH 1/3] fix: improve iii runtime compatibility diagnostics --- .../plan.md | 1047 +++++++++++++++++ .../todo.md | 285 +++++ src/cli.ts | 129 +- src/cli/doctor-diagnostics.ts | 77 +- src/cli/iii-runtime.ts | 312 +++++ test/cli-doctor-fixes.test.ts | 133 +++ test/iii-runtime.test.ts | 295 +++++ 7 files changed, 2223 insertions(+), 55 deletions(-) create mode 100644 docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md create mode 100644 docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md create mode 100644 src/cli/iii-runtime.ts create mode 100644 test/iii-runtime.test.ts diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md new file mode 100644 index 00000000..ea01c789 --- /dev/null +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md @@ -0,0 +1,1047 @@ +# Issue 337 iii Runtime Compatibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix issue #337 by accurately detecting and explaining PATH `iii 0.18.0` while preserving agentmemory's default iii-engine `0.11.2` boundary. + +**Architecture:** Keep `src/cli.ts` as the side-effectful CLI driver. Add a small pure/injected `src/cli/iii-runtime.ts` helper for iii version parsing/probing, compatibility selection, startup decision planning, install planning, and mismatch guidance so behavior is testable without importing `src/cli.ts`. Wire startup and doctor diagnostics to the same helper vocabulary. + +**Tech Stack:** TypeScript ESM, Node built-ins only, Vitest, existing pnpm project scripts. + +--- + +## Source Of Truth + +Spec path: none. + +Use these sources in order: +- User-approved `$arena` solution synthesis in `docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md`. +- Solution reports in `/tmp/arena-337-solution/`. +- Issue #337 public body and current repo evidence. + +GitHub feature-loop authorization: +- Authorized now: task-owned planning, implementation, local verification, task-owned staging/commits, and mandatory local PR-prep phase if the required skill is available. +- Not authorized now: fetch, pull, push, PR creation, PR merge, publish, deploy, migrations, destructive cleanup, credentialed/session actions, history rewrite, or changes outside task-owned files. +- `$github-push-prepare` is currently unavailable in local skill/tool search; record this as a final-phase blocker unless it becomes available. + +## File Structure + +Create: +- `src/cli/iii-runtime.ts`: pure helpers for iii version output parsing, injected version probing, pinned compatibility resolution, startup decision planning, release install planning, and user-facing mismatch notice lines. +- `test/iii-runtime.test.ts`: behavioral coverage for the helper. + +Modify: +- `src/cli.ts`: replace stdout-only `iiiBinVersion()` internals, preserve private pinned-engine precedence, use install planning before promising auto-install, and pass new effects into doctor. +- `src/cli/doctor-diagnostics.ts`: optionally consume probe/install support effects to make engine-version mismatch manual-only when auto-install is unsupported and to produce clearer mismatch details. +- `test/cli-doctor-fixes.test.ts`: issue-specific diagnostics for PATH `0.18.0`, private pinned install, unparseable probe, and manual-only install support. + +No expected changes: +- `package.json`, `pnpm-lock.yaml`, Docker files, `iii-config*.yaml`, MCP/REST/schema/persistence/auth/export/import files, and README unless implementation creates documentation drift. + +## Feature / Verification Matrix + +| Change | Verification method | Status | Evidence | +| --- | --- | --- | --- | +| Version parsing covers stdout and stderr | `test/iii-runtime.test.ts` | Done | Red/green tests cover `0.18.0`, prefixed output, stderr-only output, unparseable output, and failed-but-parseable probes. | +| Compatibility still requires pinned version by default | `test/iii-runtime.test.ts` | Done | Tests cover PATH `0.18.0`, private `0.11.2`, failed candidate probes, and explicit pinned `0.18.0`. | +| Windows zip install remains manual-only | `test/iii-runtime.test.ts` and CLI branch logic | Done | Install-plan and startup-decision tests cover `win32/x64` zip, Linux tar assets, mismatched PATH iii, and no-PATH Windows first run. | +| Startup no longer promises Windows auto-install on mismatch | `test/iii-runtime.test.ts` plus CLI wiring review | Done | Startup-decision tests cover Windows manual/Docker guidance, Linux/macOS auto-tar install, explicit Docker opt-in precedence, and no false "Installing pinned engine" wording. | +| Doctor diagnostics align with startup | `test/cli-doctor-fixes.test.ts` | Done | Tests cover detected mismatch, private pin wins, healthy PATH pin wins, unparseable probe, failed parseable probes, and manual-only dry-run behavior. | +| PR prep/security gates | `$github-push-prepare` if available; otherwise blocker | Pending | Skill missing at planning and final verification time. Local verification/security gates passed; staged Gitleaks pending until staging/commit. | + +## Boundaries + +Do not: +- Change default iii-engine from `0.11.2`. +- Treat iii `0.18.0` as compatible by default. +- Change `AGENTMEMORY_III_VERSION` semantics. +- Add Windows zip extraction, unzip dependencies, checksum/signature policy, or executable write behavior. +- Change Docker defaults or non-interactive Docker selection. +- Silently fall back to standalone MCP. +- Touch MCP tools, REST endpoints, schemas, export/import versions, KV persistence, auth, ports, Docker image defaults, or worker registration. + +## Pre-Implementation Review Adjustments + +Read-only reviewers accepted the overall boundary, with three Medium findings that are now part of the implementation contract: + +- Add a pure startup-decision helper instead of relying on notice-format tests plus manual source review. Required cases: Windows PATH `0.18.0` with no private pin returns manual/Docker guidance and does not choose native install; Linux/macOS tar-capable mismatch still chooses native install; explicit Docker opt-in still wins before mismatch fallback. +- Add doctor tests that exercise the new probe API directly. Required cases: stderr-only or probe-only PATH `0.18.0` is surfaced even when the legacy `iiiBinaryVersion` fallback would return null; unparseable PATH output reports "could not be parsed". +- Keep display version separate from healthy compatibility. A probe with `ok: false` but a parseable version may be shown in details, but `doctor` must pass only when a probe is successful and matches the pin. Required cases: nonzero private `0.11.2` fails as unhealthy, and nonzero PATH `0.18.0` fails with visible version detail. + +## Task 1: Add Helper Tests + +**Files:** +- Create: `test/iii-runtime.test.ts` + +- [ ] **Step 1: Write failing version parsing tests** + +Create `test/iii-runtime.test.ts` with imports from the not-yet-created helper: + +```ts +import { describe, expect, it } from "vitest"; +import { + canAutoInstallIii, + formatIiiMismatchNotice, + healthyIiiVersionOrNull, + planIiiStartupDecision, + parseIiiVersionOutput, + planIiiInstall, + probeIiiVersion, + resolveCompatibleIiiPath, +} from "../src/cli/iii-runtime.js"; + +describe("iii runtime version parsing", () => { + it("parses semver from common stdout formats", () => { + expect(parseIiiVersionOutput("0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii 0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii-engine v0.11.2+build.5\n")).toBe("0.11.2+build.5"); + }); + + it("parses semver from stderr and rejects partial versions", () => { + expect(parseIiiVersionOutput("", "iii-engine 0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii 0.18\n")).toBeNull(); + expect(parseIiiVersionOutput("not a version\n")).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Write failing probe, install-plan, compatibility, and notice tests** + +Append these tests: + +```ts +describe("iii version probing", () => { + it("captures stdout and stderr through the injected runner", () => { + const stdoutProbe = probeIiiVersion("/bin/iii", () => ({ + stdout: "0.18.0\n", + stderr: "", + status: 0, + signal: null, + error: undefined, + })); + expect(stdoutProbe).toMatchObject({ ok: true, version: "0.18.0" }); + + const stderrProbe = probeIiiVersion("/bin/iii", () => ({ + stdout: "", + stderr: "iii 0.18.0\n", + status: 0, + signal: null, + error: undefined, + })); + expect(stderrProbe).toMatchObject({ ok: true, version: "0.18.0" }); + }); + + it("preserves parseable versions from nonzero probes without marking them compatible", () => { + const probe = probeIiiVersion("/bin/iii", () => ({ + stdout: "0.18.0\n", + stderr: "", + status: 1, + signal: null, + error: undefined, + })); + expect(probe.version).toBe("0.18.0"); + expect(probe.ok).toBe(false); + expect(healthyIiiVersionOrNull(probe)).toBeNull(); + }); +}); + +describe("iii install planning", () => { + it("treats Windows zip assets as manual-only and Unix tar assets as auto-installable", () => { + const windowsPlan = planIiiInstall({ + version: "0.11.2", + nodePlatform: "win32", + nodeArch: "x64", + }); + expect(windowsPlan).toMatchObject({ + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + reason: "zip-asset", + }); + expect(canAutoInstallIii(windowsPlan)).toBe(false); + + const linuxPlan = planIiiInstall({ + version: "0.11.2", + nodePlatform: "linux", + nodeArch: "x64", + }); + expect(linuxPlan).toMatchObject({ + kind: "auto-tar", + asset: "iii-x86_64-unknown-linux-gnu.tar.gz", + }); + expect(canAutoInstallIii(linuxPlan)).toBe(true); + }); +}); + +describe("iii compatibility resolution", () => { + const pinned = "0.11.2"; + const privatePath = "/home/alex/.agentmemory/bin/iii"; + + it("accepts the candidate only when it matches the pin", () => { + const result = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: () => false, + probe: () => ({ ok: true, version: pinned, stdout: "", stderr: "", status: 0, signal: null }), + }); + expect(result).toMatchObject({ kind: "candidate", binPath: "/usr/local/bin/iii" }); + }); + + it("chooses the private pinned binary over PATH iii 0.18.0", () => { + const result = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: (path) => path === privatePath, + probe: (path) => ({ + ok: true, + version: path === privatePath ? pinned : "0.18.0", + stdout: "", + stderr: "", + status: 0, + signal: null, + }), + }); + expect(result).toMatchObject({ kind: "private", binPath: privatePath }); + }); + + it("rejects PATH iii 0.18.0 unless the pin is explicitly 0.18.0", () => { + const defaultResult = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: () => false, + probe: () => ({ ok: true, version: "0.18.0", stdout: "", stderr: "", status: 0, signal: null }), + }); + expect(defaultResult).toMatchObject({ kind: "none" }); + + const explicitResult = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: "0.18.0", + exists: () => false, + probe: () => ({ ok: true, version: "0.18.0", stdout: "", stderr: "", status: 0, signal: null }), + }); + expect(explicitResult).toMatchObject({ kind: "candidate", binPath: "/usr/local/bin/iii" }); + }); +}); + +describe("iii mismatch notice", () => { + it("explains manual-only Windows mismatch without promising auto-install", () => { + const lines = formatIiiMismatchNotice({ + candidatePath: "C:\\Users\\alex\\.local\\bin\\iii.exe", + candidateVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: { + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + releaseUrl: "https://example.invalid/iii.zip", + reason: "zip-asset", + }, + dockerAvailable: true, + nodePlatform: "win32", + }); + const text = lines.join("\n"); + expect(text).toContain("0.18.0"); + expect(text).toContain("0.11.2"); + expect(text).toContain("Automatic native install is unavailable"); + expect(text).toContain("AGENTMEMORY_USE_DOCKER=1"); + expect(text).toContain("left untouched"); + expect(text).not.toContain("Installing pinned engine"); + }); +}); + +describe("iii startup decision planning", () => { + it("keeps explicit Docker opt-in ahead of native mismatch fallback", () => { + expect( + planIiiStartupDecision({ + compatibleIii: null, + pathIii: "/usr/local/bin/iii", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ version: "0.11.2", nodePlatform: "linux", nodeArch: "x64" }), + dockerAvailable: true, + dockerOptIn: true, + interactive: false, + }), + ).toMatchObject({ kind: "docker" }); + }); + + it("chooses native install for tar-capable mismatches", () => { + expect( + planIiiStartupDecision({ + compatibleIii: null, + pathIii: "/usr/local/bin/iii", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ version: "0.11.2", nodePlatform: "linux", nodeArch: "x64" }), + dockerAvailable: true, + dockerOptIn: false, + interactive: false, + }), + ).toMatchObject({ kind: "install" }); + }); + + it("does not choose native install for Windows zip mismatches", () => { + const decision = planIiiStartupDecision({ + compatibleIii: null, + pathIii: "C:\\Users\\alex\\.local\\bin\\iii.exe", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ version: "0.11.2", nodePlatform: "win32", nodeArch: "x64" }), + dockerAvailable: true, + dockerOptIn: false, + interactive: false, + }); + expect(decision.kind).toBe("manual"); + expect(decision.notice.join("\n")).toContain("Automatic native install is unavailable"); + expect(decision.notice.join("\n")).not.toContain("Installing pinned engine"); + }); +}); +``` + +- [ ] **Step 3: Run tests and confirm red** + +Run: + +```bash +corepack pnpm exec vitest run test/iii-runtime.test.ts +``` + +Expected before implementation: FAIL because `src/cli/iii-runtime.ts` does not exist or exported functions are missing. + +## Task 2: Implement `src/cli/iii-runtime.ts` + +**Files:** +- Create: `src/cli/iii-runtime.ts` + +- [ ] **Step 1: Add the helper module** + +Create `src/cli/iii-runtime.ts`: + +```ts +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; +import { existsSync } from "node:fs"; +import { platform } from "node:os"; +import { iiiReleaseAsset, iiiReleaseUrl } from "./build-runtime.js"; + +const VERSION_PATTERN = /\bv?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/; + +export type IiiVersionProbe = { + ok: boolean; + version: string | null; + stdout: string; + stderr: string; + status: number | null; + signal: NodeJS.Signals | null; + errorMessage?: string; +}; + +export type IiiVersionRunner = ( + binPath: string, + args: readonly string[], + options: { + encoding: "utf-8"; + stdio: ["ignore", "pipe", "pipe"]; + timeout: number; + }, +) => Pick, "stdout" | "stderr" | "status" | "signal" | "error">; + +export type IiiInstallPlan = + | { kind: "auto-tar"; asset: string; releaseUrl: string } + | { kind: "manual-release"; asset: string; releaseUrl: string; reason: "zip-asset" } + | { kind: "unsupported-platform"; asset: null; releaseUrl: null; reason: "no-release-asset" }; + +export type IiiCompatibilityResolution = + | { + kind: "candidate"; + binPath: string; + candidateProbe: IiiVersionProbe; + privateProbe?: IiiVersionProbe; + } + | { + kind: "private"; + binPath: string; + candidateProbe: IiiVersionProbe; + privateProbe: IiiVersionProbe; + } + | { + kind: "none"; + candidateProbe: IiiVersionProbe; + privateProbe?: IiiVersionProbe; + }; + +export function parseIiiVersionOutput(stdout = "", stderr = ""): string | null { + const match = `${stdout}\n${stderr}`.match(VERSION_PATTERN); + return match ? match[1]! : null; +} + +function defaultRunIiiVersion( + binPath: string, + args: readonly string[], + options: Parameters[2], +) { + return spawnSync(binPath, [...args], options); +} + +export function probeIiiVersion( + binPath: string, + run: IiiVersionRunner = defaultRunIiiVersion, + timeoutMs = 3000, +): IiiVersionProbe { + const result = run(binPath, ["--version"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: timeoutMs, + }); + const stdout = result.stdout ?? ""; + const stderr = result.stderr ?? ""; + const version = parseIiiVersionOutput(stdout, stderr); + const errorMessage = result.error instanceof Error ? result.error.message : undefined; + return { + ok: !result.error && result.status === 0 && version !== null, + version, + stdout, + stderr, + status: result.status ?? null, + signal: result.signal ?? null, + ...(errorMessage ? { errorMessage } : {}), + }; +} + +export function displayIiiVersionOrNull(probe: IiiVersionProbe): string | null { + return probe.version; +} + +export function healthyIiiVersionOrNull(probe: IiiVersionProbe): string | null { + return probe.ok ? probe.version : null; +} + +export function planIiiInstall({ + version, + nodePlatform = platform(), + nodeArch = process.arch, +}: { + version: string; + nodePlatform?: NodeJS.Platform; + nodeArch?: string; +}): IiiInstallPlan { + const asset = iiiReleaseAsset(nodePlatform, nodeArch); + const releaseUrl = iiiReleaseUrl(version, nodePlatform, nodeArch); + if (!asset || !releaseUrl) { + return { kind: "unsupported-platform", asset: null, releaseUrl: null, reason: "no-release-asset" }; + } + if (nodePlatform !== "win32" && asset.endsWith(".tar.gz")) { + return { kind: "auto-tar", asset, releaseUrl }; + } + return { kind: "manual-release", asset, releaseUrl, reason: "zip-asset" }; +} + +export function canAutoInstallIii(plan: IiiInstallPlan): boolean { + return plan.kind === "auto-tar"; +} + +export type IiiStartupDecision = + | { kind: "use-compatible"; binPath: string } + | { kind: "docker" } + | { kind: "install"; notice: string[] } + | { kind: "prompt-docker"; notice: string[] } + | { kind: "manual"; notice: string[] }; + +export function planIiiStartupDecision({ + compatibleIii, + pathIii, + pathIiiVersion, + pinnedVersion, + installPlan, + dockerAvailable, + dockerOptIn, + interactive, +}: { + compatibleIii: string | null; + pathIii: string | null; + pathIiiVersion: string | null; + pinnedVersion: string; + installPlan: IiiInstallPlan; + dockerAvailable: boolean; + dockerOptIn: boolean; + interactive: boolean; +}): IiiStartupDecision { + if (dockerOptIn) return { kind: "docker" }; + if (compatibleIii) return { kind: "use-compatible", binPath: compatibleIii }; + if (pathIii && canAutoInstallIii(installPlan)) { + return { + kind: "install", + notice: [ + `iii on PATH is v${pathIiiVersion ?? "unknown"} but agentmemory pins v${pinnedVersion}. Installing pinned engine to ~/.agentmemory/bin (leaves your existing iii untouched).`, + ], + }; + } + if (pathIii) { + const notice = formatIiiMismatchNotice({ + candidatePath: pathIii, + candidateVersion: pathIiiVersion, + pinnedVersion, + installPlan, + dockerAvailable, + }); + return dockerAvailable && interactive ? { kind: "prompt-docker", notice } : { kind: "manual", notice }; + } + return { kind: "manual", notice: [] }; +} + +export function resolveCompatibleIiiPath({ + candidatePath, + privatePath, + pinnedVersion, + exists = existsSync, + probe = probeIiiVersion, +}: { + candidatePath: string; + privatePath: string; + pinnedVersion: string; + exists?: (path: string) => boolean; + probe?: (path: string) => IiiVersionProbe; +}): IiiCompatibilityResolution { + const candidateProbe = probe(candidatePath); + if (candidateProbe.ok && candidateProbe.version === pinnedVersion) { + return { kind: "candidate", binPath: candidatePath, candidateProbe }; + } + + if (candidatePath !== privatePath && exists(privatePath)) { + const privateProbe = probe(privatePath); + if (privateProbe.ok && privateProbe.version === pinnedVersion) { + return { kind: "private", binPath: privatePath, candidateProbe, privateProbe }; + } + return { kind: "none", candidateProbe, privateProbe }; + } + + return { kind: "none", candidateProbe }; +} + +function formatDetectedVersion(version: string | null): string { + return version ? `v${version}` : "vunknown"; +} + +export function formatIiiMismatchNotice({ + candidatePath, + candidateVersion, + pinnedVersion, + installPlan, + dockerAvailable, + nodePlatform = platform(), +}: { + candidatePath: string; + candidateVersion: string | null; + pinnedVersion: string; + installPlan: IiiInstallPlan; + dockerAvailable: boolean; + nodePlatform?: NodeJS.Platform; +}): string[] { + const lines = [ + `iii on PATH is ${formatDetectedVersion(candidateVersion)} but agentmemory pins v${pinnedVersion} for its current worker model.`, + `Your existing iii at ${candidatePath} is left untouched.`, + ]; + + if (installPlan.kind === "manual-release") { + lines.push( + `Automatic native install is unavailable on ${nodePlatform} because ${installPlan.asset} must be installed manually.`, + `Install the pinned release manually: ${installPlan.releaseUrl}`, + ); + if (dockerAvailable) { + lines.push("Or re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + return lines; + } + + if (installPlan.kind === "unsupported-platform") { + lines.push(`No pinned iii-engine binary is available for ${nodePlatform}/${process.arch}.`); + if (dockerAvailable) { + lines.push("Re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + return lines; + } + + lines.push("Installing pinned engine to ~/.agentmemory/bin."); + return lines; +} +``` + +- [ ] **Step 2: Run helper tests and confirm green** + +Run: + +```bash +corepack pnpm exec vitest run test/iii-runtime.test.ts +``` + +Expected after implementation: PASS for all `test/iii-runtime.test.ts` cases. + +## Task 3: Wire CLI Startup To The Helper + +**Files:** +- Modify: `src/cli.ts` + +- [ ] **Step 1: Import helper functions** + +Add imports near the existing CLI helper imports: + +```ts +import { + canAutoInstallIii, + displayIiiVersionOrNull, + formatIiiMismatchNotice, + planIiiInstall, + planIiiStartupDecision, + probeIiiVersion, + resolveCompatibleIiiPath, + type IiiVersionProbe, +} from "./cli/iii-runtime.js"; +``` + +- [ ] **Step 2: Replace `iiiBinVersion()` internals only** + +Change `iiiBinVersion()` to: + +```ts +function iiiBinProbe(binPath: string): IiiVersionProbe { + return probeIiiVersion(binPath); +} + +function iiiBinVersion(binPath: string): string | null { + return displayIiiVersionOrNull(iiiBinProbe(binPath)); +} +``` + +- [ ] **Step 3: Preserve compatible resolution through the helper** + +Replace the body of `resolveCompatibleIii()` with: + +```ts +function resolveCompatibleIii(iiiBinPath: string | null | undefined): string | null { + if (!iiiBinPath) return null; + const resolution = resolveCompatibleIiiPath({ + candidatePath: iiiBinPath, + privatePath: privateIiiPath(), + pinnedVersion: IIPINNED_VERSION, + }); + if (resolution.kind === "candidate") return resolution.binPath; + if (resolution.kind === "private") { + const reason = resolution.candidateProbe.version + ? `v${resolution.candidateProbe.version} mismatches pin` + : "probe failed"; + vlog( + `iii at ${iiiBinPath} ${reason} v${IIPINNED_VERSION}; using private install at ${resolution.binPath}.`, + ); + return resolution.binPath; + } + return null; +} +``` + +- [ ] **Step 4: Use install plan in `runIiiInstaller()`** + +At the top of `runIiiInstaller()`, replace duplicated `releaseUrl`, `asset`, and zip checks with: + +```ts +const installPlan = planIiiInstall({ version: IIPINNED_VERSION }); + +if (installPlan.kind === "unsupported-platform") { + p.log.warn( + `iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`, + ); + return { ok: false, binPath: null }; +} + +if (!canAutoInstallIii(installPlan)) { + p.log.info( + `Auto-install unavailable on ${platform()} — ${installPlan.asset} isn't tar-compatible. Install manually:\n` + + ` 1. Download ${installPlan.releaseUrl}\n` + + ` 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\n` + + `Or use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`, + ); + return { ok: false, binPath: null }; +} +``` + +Use `installPlan.releaseUrl` in the existing curl/tar command. + +- [ ] **Step 5: Avoid false Windows auto-install promise in `startEngine()` through the helper** + +Before `type Choice = "install" | "docker" | "manual";`, compute: + +```ts +const installPlan = planIiiInstall({ version: IIPINNED_VERSION }); +``` + +Build a startup decision before the existing choice branches: + +```ts +const startupDecision = planIiiStartupDecision({ + compatibleIii: iiiBin, + pathIii, + pathIiiVersion: pathIii ? iiiBinVersion(pathIii) : null, + pinnedVersion: IIPINNED_VERSION, + installPlan, + dockerAvailable: Boolean(dockerBin && composeFile), + dockerOptIn, + interactive, +}); +``` + +Then route the existing `Choice` assignment through that decision before falling back to existing non-mismatch prompts: + +```ts +if (startupDecision.kind === "docker") { + choice = "docker"; +} else if (startupDecision.kind === "use-compatible") { + choice = "manual"; +} else if (startupDecision.kind === "install") { + choice = "install"; + p.log.info(startupDecision.notice.join("\n")); +} else if (startupDecision.kind === "prompt-docker") { + p.log.info(startupDecision.notice.join("\n")); + const fallback = await p.confirm({ + message: "Automatic native install is unavailable here. Try Docker compose instead?", + initialValue: true, + }); + if (p.isCancel(fallback) || fallback !== true) { + startupFailure = { kind: "no-engine" }; + return false; + } + choice = "docker"; +} else if (pathIiiMismatch) { + p.log.info(startupDecision.notice.join("\n")); + startupFailure = { kind: "no-engine" }; + return false; +} else if (dockerBin && composeFile && interactive) { + // Keep the existing first-run prompt for cases with no PATH mismatch. +} +``` + +Keep the current no-PATH-iii first-run prompt/install fallback after this branch so fresh installs on tar-capable platforms still auto-install and fresh interactive Docker prompts still work. + +If implementing this as a narrower patch around the existing `pathIiiMismatch` branch, the behavior must still be driven by `planIiiStartupDecision()` and must keep these outcomes: + +- explicit Docker opt-in: `choice = "docker"` before any install branch, +- compatible PATH/private engine: `choice = "manual"` using the existing `iiiBin`, +- tar-capable mismatch: `choice = "install"` and "Installing pinned engine" wording is allowed, +- Windows/manual-only mismatch: no native install branch; show mismatch guidance, optionally prompt Docker if interactive and available, otherwise return `no-engine`. + +- [ ] **Step 6: Run focused helper and existing startup-adjacent tests** + +Run: + +```bash +corepack pnpm exec vitest run test/iii-runtime.test.ts test/engine-launch.test.ts +``` + +Expected: PASS. If pnpm hardening blocks the command, follow repo instructions: run `corepack pnpm install --frozen-lockfile --ignore-scripts`, then rerun; do not approve builds. + +## Task 4: Align Doctor Diagnostics + +**Files:** +- Modify: `src/cli/doctor-diagnostics.ts` +- Modify: `src/cli.ts` +- Modify: `test/cli-doctor-fixes.test.ts` + +- [ ] **Step 1: Add optional probe/install effects** + +In `src/cli/doctor-diagnostics.ts`, import helper types/functions: + +```ts +import { + canAutoInstallIii, + displayIiiVersionOrNull, + healthyIiiVersionOrNull, + type IiiInstallPlan, + type IiiVersionProbe, +} from "./iii-runtime.js"; +``` + +Extend `DoctorEffects`: + +```ts +iiiBinaryProbe?: (binPath: string) => IiiVersionProbe; +iiiInstallPlan?: () => IiiInstallPlan; +``` + +Inside `buildDiagnostics()`, before returning the diagnostic array: + +```ts +const installPlan = effects.iiiInstallPlan?.(); +const autoInstallAvailable = installPlan ? canAutoInstallIii(installPlan) : true; +const probeFor = (binPath: string): IiiVersionProbe | null => { + const probe = effects.iiiBinaryProbe?.(binPath); + if (probe) return probe; + const version = effects.iiiBinaryVersion(binPath); + return { + ok: version !== null, + version, + stdout: "", + stderr: "", + status: version === null ? null : 0, + signal: null, + }; +}; +const displayVersion = (binPath: string): string | null => { + const probe = probeFor(binPath); + return probe ? displayIiiVersionOrNull(probe) : null; +}; +const healthyVersion = (binPath: string): string | null => { + const probe = probeFor(binPath); + return probe ? healthyIiiVersionOrNull(probe) : null; +}; +const probeDetail = (binPath: string): string => { + const probe = probeFor(binPath); + if (!probe) return "--version failed"; + if (probe.version) return probe.ok ? probe.version : `${probe.version} (--version failed)`; + return "--version could not be parsed"; +}; +``` + +- [ ] **Step 2: Update engine-version-mismatch diagnostic** + +Change the `engine-version-mismatch` `fixPreview` to use `autoInstallAvailable`: + +```ts +fixPreview: autoInstallAvailable + ? "Install the pinned version to ~/.agentmemory/bin and restart the engine." + : "Install the pinned version manually or run with AGENTMEMORY_USE_DOCKER=1; automatic native install is unavailable.", +manualOnly: !autoInstallAvailable, +``` + +In its `check`, replace direct `effects.iiiBinaryVersion(...)` calls with `healthyVersion(...)` for pass/fail decisions and `displayVersion(...)`/`probeDetail(...)` for detail text. Keep these semantics: +- private pin present and healthy: pass and mention private install wins when PATH differs. +- private pin output is parseable but the probe failed: fail; detail must include the private version and `--version failed`. +- PATH version parseable but mismatched: fail with `${pathVersion} (pinned ${ctx.pinnedVersion})`. +- PATH version parseable but the probe failed: fail with `${pathVersion} (--version failed) (pinned ${ctx.pinnedVersion})`. +- PATH version unparseable: fail with `iii on PATH but --version could not be parsed`. + +- [ ] **Step 3: Update `iii-on-path-not-local-bin` diagnostic details** + +Use `healthyVersion(...)` for local private-pin pass/fail checks and `displayVersion(...)` for local/PATH detail so PATH `0.18.0` can appear in detail when useful. Preserve `manualOnly: true`. + +- [ ] **Step 4: Wire production effects in `src/cli.ts`** + +In `buildDoctorEffects()`, add: + +```ts +iiiBinaryProbe: (binPath: string) => iiiBinProbe(binPath), +iiiInstallPlan: () => planIiiInstall({ version: IIPINNED_VERSION }), +``` + +Keep the existing `iiiBinaryVersion` effect for compatibility with tests and simple callers. + +- [ ] **Step 5: Extend doctor tests** + +In `test/cli-doctor-fixes.test.ts`, add helper support for optional effects. Add tests: + +```ts +it("engine-version-mismatch names PATH iii 0.18.0 when no private pin exists", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryVersion: (bin) => (bin === privatePath ? null : "0.18.0"), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.18.0"); + expect(status.detail).toContain("0.11.2"); +}); + +it("engine-version-mismatch is manual-only when auto-install is unavailable", () => { + const diagnostics = buildDiagnostics( + stubEffects({ + iiiInstallPlan: () => ({ + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + releaseUrl: "https://example.invalid/iii.zip", + reason: "zip-asset", + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + expect(check.manualOnly).toBe(true); + const lines = dryRunPlan(stubCtx(), [ + { diagnostic: check, status: { ok: false, detail: "0.18.0 (pinned 0.11.2)" } }, + ]); + expect(lines.some((line) => line.includes("manual fix:"))).toBe(true); +}); + +it("engine-version-mismatch uses probe output when legacy version fallback is null", async () => { + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + iiiBinaryVersion: () => null, + iiiBinaryProbe: () => ({ + ok: true, + version: "0.18.0", + stdout: "", + stderr: "iii 0.18.0\n", + status: 0, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.18.0"); +}); + +it("engine-version-mismatch fails a parseable but unhealthy private probe", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryProbe: (bin) => ({ + ok: false, + version: bin === privatePath ? "0.11.2" : "0.18.0", + stdout: bin === privatePath ? "0.11.2\n" : "0.18.0\n", + stderr: "", + status: 1, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.11.2"); + expect(status.detail).toContain("--version failed"); +}); + +it("engine-version-mismatch names unparseable PATH probes", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryProbe: (bin) => ({ + ok: false, + version: bin === privatePath ? null : null, + stdout: "iii development build\n", + stderr: "", + status: 0, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("could not be parsed"); +}); +``` + +Adjust existing tests only where expectations depend on the old generic `--version failed` wording. + +- [ ] **Step 6: Run doctor tests** + +Run: + +```bash +corepack pnpm exec vitest run test/iii-runtime.test.ts test/cli-doctor-fixes.test.ts +``` + +Expected: PASS. + +## Task 5: Documentation Scan, Simplification, And Verification + +**Files:** +- Inspect active diff only. +- Modify README only if implementation creates actual doc drift. + +- [ ] **Step 1: Focused simplification pass** + +Apply `$simple-code` principles to touched files only: +- remove duplicate version parsing logic, +- keep helpers focused, +- preserve all public CLI behavior boundaries, +- do not add speculative abstractions beyond tests and current call sites. + +- [ ] **Step 2: Search stale wording** + +Run: + +```bash +rg -n "vunknown|Installing pinned engine|tar-compatible|AGENTMEMORY_III_VERSION|iii-engine v0\\.11\\.2|Windows" src README.md test +``` + +Expected: remaining matches are either updated behavior, existing valid docs, or explicitly recorded as not stale. + +- [ ] **Step 3: Run targeted verification** + +Run: + +```bash +corepack pnpm exec vitest run test/iii-runtime.test.ts test/build-runtime.test.ts test/cli-doctor-fixes.test.ts test/engine-launch.test.ts +``` + +Expected: PASS. + +- [ ] **Step 4: Run repo-native checks** + +Run: + +```bash +corepack pnpm run lint +corepack pnpm test +``` + +Expected: PASS, unless pnpm hardening blocks dependency materialization. If blocked, record exact blocker and run closest targeted checks. + +- [ ] **Step 5: Run required security checks before any commit/PR prep** + +Because this is non-trivial CLI/subprocess/install-selection behavior, run: + +```bash +semgrep scan --config p/default --error --metrics=off . +``` + +OSV is not required if there are no dependency, lockfile, container, vendored, package-manager, or third-party package-surface changes. + +Before any commit, stage only task-owned files and run: + +```bash +gitleaks protect --staged --redact +``` + +- [ ] **Step 6: Record final matrix evidence** + +Update `docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md` with: +- files changed, +- red/green test evidence, +- verification command outputs, +- security gate results or blockers, +- `$github-push-prepare` availability/blocker, +- any skipped Windows smoke caveat. + +## Subagent Ledger + +| Workstream | Scope | Edits allowed | Expected output | Verification responsibility | +| --- | --- | --- | --- | --- | +| Pre-implementation review | Plan and task record only | No | High/Medium plan risks or ACCEPT | Main agent triages findings and updates plan only for valid scope issues. | +| Implementation package | `src/cli/iii-runtime.ts`, `src/cli.ts`, `src/cli/doctor-diagnostics.ts`, `test/iii-runtime.test.ts`, `test/cli-doctor-fixes.test.ts`, task record | Yes | TDD implementation and focused test results | Main agent reviews diff and reruns targeted verification. | +| Final review | Active diff | No | Security/test/maintainability ACCEPT or findings | Main agent triages and fixes valid High/Medium findings. | + +## Self-Review + +Spec coverage: +- Version detection, pinned compatibility, Windows manual-only install guidance, private pin precedence, explicit opt-in, doctor consistency, and non-goals are covered. + +Placeholder scan: +- No `TBD`, `TODO`, or unspecified implementation steps remain. + +Type consistency: +- `IiiVersionProbe`, `IiiInstallPlan`, and `IiiCompatibilityResolution` are defined before use. +- Tests import only exported helper names defined in Task 2. + +Known issue: +- `$github-push-prepare` is mandatory for the full GitHub feature loop but is not installed or discoverable at plan time. The loop can implement and verify locally, then must report this final-phase blocker unless the skill/tool becomes available. diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md new file mode 100644 index 00000000..bb55f5bb --- /dev/null +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md @@ -0,0 +1,285 @@ +# Issue 337: iii 0.18 Compatibility + +Task id: `2026-06-19-issue-337-iii-0-18-compatibility` + +## Scope + +Handle GitHub issue `#337` in the `agentmemory` repository as the replacement +workstream for completed issue `#297` in the monitor batch. + +Target repository: +`/Users/A1538552/.codex/worktrees/99e2/agentmemory` + +Target remote: `origin` at `https://github.com/wbugitlab1/agentmemory.git`. +Do not target `https://github.com/rohitg00/agentmemory/`. + +Branch: `issue/337-iii-0-18-compatibility` + +## Sprint Contract + +Goal: validate and, if approved after boundary review, fix the current +agentmemory behavior reported in issue `#337` around iii `0.18.0`, pinned iii +`0.11.2`, and Windows engine installation/startup. + +Scope: +- Validate issue legitimacy with `$arena` before implementation. +- Distinguish version detection, pinned-engine compatibility, Windows install + handling, and current main behavior. +- Keep any implementation surgical and focused on CLI startup/installer + behavior. +- Add focused tests around affected version/install/startup behavior if + implementation proceeds. + +Non-goals: +- Do not change MCP tools, REST endpoints, schemas, persisted data, auth, or + memory model behavior. +- Do not retarget PRs or issue actions to any remote other than `origin`. +- Do not silently adopt iii `0.18.0` as the supported default engine. + +Acceptance criteria: +- Branch/worktree/remotes/status are recorded before edits. +- Issue validity evidence is synthesized from arena candidates and local repo + inspection. +- A Human Checkpoint is completed before any installer or engine-boundary + behavior change. +- If implementation proceeds, tests cover the selected behavior and relevant + repo-native verification/security gates are run or blockers recorded. +- The issue thread receives outcome evidence before terminal handoff. + +Intended verification: +- Targeted vitest tests for CLI runtime/build/install helpers touched by the + fix. +- `corepack pnpm test` if dependencies are available and hardening allows it. +- Required security gates before any commit or PR: staged Gitleaks, plus + Semgrep/OSV as applicable for non-trivial code/config changes. + +Known boundaries: +- `AGENTMEMORY_III_VERSION` already allows explicit opt-in to a different iii + engine version. +- Default behavior currently pins iii engine `0.11.2` because newer iii engines + use a different worker model. +- Windows installer handling is an installer/runtime boundary and requires a + Human Checkpoint before source changes. + +## Feature / Verification Matrix + +| Change | Verification method | Status | Evidence | +| --- | --- | --- | --- | +| Context confirmed | Local git commands and instruction reads | Done | Read `AGENTS.md`, project triage skill, and `$arena`; `git status`, remotes, worktrees, and start ref inspected. | +| Branch created | Git command | Done | Created `issue/337-iii-0-18-compatibility` from `origin/main` `24ff6779f5618d0f07039161be4f750252747f31`. | +| Issue fetched | Public GitHub API read | Done | Issue `#337` is open and imports upstream issue `#839` with the Windows iii `0.18.0` startup transcript. | +| Arena validation | Candidate reports and synthesis | Done | Candidate reports in `/tmp/arena-337/candidate-{a,b,c}/report.md`; judge report in `/tmp/arena-337/judge/report.md`. Issue is valid as startup/version-detection/Windows remediation, not as default iii `0.18.0` support. | +| Human Checkpoint | User approval | Done | 2026-06-20: User approved the surgical non-upgrade fix after asking whether upgrading iii was an option. Decision: do not upgrade iii in #337; proceed with truthful version/startup/Windows guidance. | +| Solution arena | Candidate reports and synthesis | Done | Candidate reports in `/tmp/arena-337-solution/candidate-{a,b,c}/report.md`; judge report in `/tmp/arena-337-solution/judge/report.md`. Best solution: Candidate B base, constrained with Candidate A's narrower scope and Candidate C's doctor/manual-fix UX. | +| Implementation plan | `writing-plans` | Done | Plan saved to `docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md`. | +| Implementation | TDD + local diff review | Done | Added `src/cli/iii-runtime.ts`, wired `src/cli.ts` and `src/cli/doctor-diagnostics.ts`, and added focused tests in `test/iii-runtime.test.ts` / `test/cli-doctor-fixes.test.ts`. No iii upgrade, dependency, Docker default, MCP/REST/schema/persistence/auth change. | +| Red/green tests | Vitest | Done | Initial red: `corepack pnpm exec vitest run test/iii-runtime.test.ts test/cli-doctor-fixes.test.ts` failed on missing helper/new Doctor semantics. After fixes: same command passed with 51 tests. | +| Review fixes | Adversarial read-only review | Done | Reviewer found Windows no-PATH manual-only handling, display-vs-health leakage, and Doctor PATH/private precedence gaps; all were fixed with new regression tests. | +| Verification | Repo-native checks | Done | `corepack pnpm exec vitest run test/iii-runtime.test.ts test/build-runtime.test.ts test/cli-doctor-fixes.test.ts test/engine-launch.test.ts` passed 72 tests; `corepack pnpm run lint` passed; `corepack pnpm test` passed 212 files / 2924 tests; `corepack pnpm run build` passed. | +| Security gates | Semgrep / OSV decision | Done | `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings for tracked files; explicit Semgrep on new `src/cli/iii-runtime.ts` and `test/iii-runtime.test.ts` passed with 0 findings. OSV not required: no dependency, lockfile, container, vendored, package-manager, or third-party package-surface changes. | +| PR prep/security gates | `$github-push-prepare` if available; otherwise blocker | Pending | `$github-push-prepare` remains unavailable in local skill/tool search. Staged Gitleaks still required before any local commit. Remote push/PR/issue comment remain unapproved current-turn remote writes. | + +## Subagent Ledger + +| Workstream | Scope | Edits allowed | Expected output | Result | Residual risk | +| --- | --- | --- | --- | --- | --- | +| Arena candidate A | Read-only issue validity report | No | Reproduction evidence, affected paths, fix direction, tests | Complete | Correct conservative framing; weaker live issue grounding. | +| Arena candidate B | Read-only Windows installer/archive report | No | Windows install evidence and boundary risks | Complete | Strongest installer/supply-chain risk inventory; leaned toward zip auto-install if approved. | +| Arena candidate C | Read-only version/startup compatibility report | No | Version detection and iii `0.18.0` compatibility evidence | Complete | Selected base by judge; best issue-body grounding and code-path trace. | +| Arena cross-judge | Read-only candidate scoring | No | Candidate scores, base pick, graft/reject list | Complete | Candidate C selected, B risk coverage and A conservative minimum grafted into synthesis. | +| Solution arena candidate A | Read-only implementation solution report | No | Minimal no-installer-change solution shape | Complete | Strongest narrowness discipline; less explicit doctor alignment. | +| Solution arena candidate B | Read-only implementation solution report | No | Helper extraction and testable startup planning shape | Complete | Selected base by judge; best balance of version detection, compatibility planning, and preserved boundary. | +| Solution arena candidate C | Read-only implementation solution report | No | User-facing CLI/doctor diagnostics consistency plan | Complete | Strong doctor/manual-fix UX; broader than necessary as a base. | +| Solution arena cross-judge | Read-only candidate scoring | No | Solution scores, base pick, graft/reject list | Complete | Candidate B selected; graft A's narrower scope and C's doctor/manual-fix UX. | +| GitHub feature loop | Full local implementation loop | Yes, task-owned files only | Plan, review-gated implementation, verification, local branch prep if available | In progress | User invoked `$github-feature-loop`; remote fetch/push/PR creation still not approved. `$github-push-prepare` is not installed/discoverable at kickoff. | +| Pre-code review: architecture | Plan and task record | No | Boundary/scope review | Complete | ACCEPT; no High/Medium findings. | +| Pre-code review: verification | Plan and task record | No | Test coverage review | Complete | Medium findings accepted: add testable startup-decision helper and doctor probe-API coverage. | +| Pre-code review: security/boundary | Plan and task record | No | Engine/installer boundary review | Complete | Medium finding accepted: doctor must not treat failed parseable probes as healthy. | +| Final adversarial review | Active diff | No | Correctness, boundary, and verification review | Complete | Important findings fixed: Windows no-PATH manual-only startup, health-preserving `iiiBinVersion()`, and Doctor PATH/private precedence. | + +## Arena Synthesis + +Base: Candidate C (`/tmp/arena-337/candidate-c/report.md`). + +Judge score: +- Candidate A: 22/25. +- Candidate B: 24/25. +- Candidate C: 25/25. + +Decision: issue `#337` is valid, but not as evidence that agentmemory should +support iii `0.18.0` by default. Current main intentionally pins iii-engine +`0.11.2` and `iii-sdk` `0.11.2` because newer iii engines use a different +worker model. The actionable current-main problem is the startup path: +version probing can report `unknown`, Windows zip assets cannot be +auto-installed by the current tar-based installer, and the mismatch flow says +it is installing a private pinned engine before falling into manual/Docker/MCP +guidance. + +Grafted from Candidate B: +- Installer supply-chain, executable trust, dependency, Docker-default, + API/tool/schema/persistence, and engine-boundary risks. +- Existing platform-aware behavior around `iii.exe`, `where`, fallback paths, + private install precedence, and doctor diagnostics. +- If Windows zip extraction is later approved, include archive safety tests for + traversal, missing executable, unexpected entries, size bounds, and + post-extract version validation. + +Grafted from Candidate A: +- Minimum fix should stay conservative: robust version probing and + Windows-specific truthful messaging/remediation first. +- Current main already detects Windows/zip assets and refuses auto-install; do + not claim it currently untars the Windows zip. +- Standalone MCP must remain explicit because it lacks the normal REST, viewer, + and scheduled behavior. + +Rejected directions: +- Lift the default iii engine pin to `0.18.0`. +- Accept any PATH `iii` merely because `--version` succeeds. +- Change `AGENTMEMORY_III_VERSION` from explicit opt-in into an automatic + compatibility guarantee. +- Remove private pinned-engine precedence. +- Overwrite or downgrade the user's global PATH `iii`. +- Auto-select Docker in non-interactive startup without approval. +- Silently fall back to standalone MCP. +- Add zip extraction dependencies or binary install behavior without a Human + Checkpoint and dependency/security intake. +- Touch MCP tools, REST endpoints, schemas, export/import versions, KV + persistence, auth, ports, or worker model for this issue. + +Recommended minimum implementation after approval: +1. Keep the default iii-engine pin at `0.11.2`. +2. Make iii version probing robust and testable: parse common semver output + from stdout and stderr, return null on real probe failure, and never treat a + mismatched version as compatible by default. +3. Improve Windows mismatch flow so it does not promise auto-install when zip + auto-install is unsupported; when parseable, report `0.18.0` as detected but + unsupported by this release and show direct pinned manual/Docker guidance. +4. Extract small helpers only if needed to avoid brittle source-text tests. +5. Treat Windows zip auto-install as a separate installer-boundary option that + requires explicit approval. + +## Solution Arena Synthesis + +Base: Candidate B (`/tmp/arena-337-solution/candidate-b/report.md`). + +Judge score: +- Candidate A: 24/25. +- Candidate B: 24/25. +- Candidate C: 22/25. + +Tie-breaker: Candidate B has the best balance of accurate version detection, +testable compatibility/install planning, and preserved iii `0.11.2` engine +boundary. Candidate A tied on score but is less explicit about keeping a +parseable version available for user-facing diagnostics. Candidate C has useful +doctor/manual-fix UX, but its full helper centralization is broader than needed. + +Final solution shape: +- Add a small `src/cli/iii-runtime.ts` helper for iii version parsing/probing, + compatibility resolution, and install planning. Keep the helper focused; do + not centralize unrelated CLI concerns. +- Parse `iii --version` from stdout and stderr. Preserve a parseable detected + version for diagnostics, but accept a binary for default runtime compatibility + only when its version equals the pinned version. +- Preserve private pinned-engine precedence: PATH `0.18.0` plus private + `0.11.2` should choose the private binary. +- Preserve explicit opt-in: `AGENTMEMORY_III_VERSION=0.18.0` plus PATH + `0.18.0` may choose PATH as it does today, but this remains opt-in, not a + default support claim. +- Classify Windows zip release assets as manual-only for current native + auto-install. On Windows mismatch with no private pinned binary, do not log + "Installing pinned engine"; report detected `0.18.0`, pinned `0.11.2`, that + the user's existing PATH iii is left untouched, and manual/Docker options. +- Keep Docker opt-in in non-interactive mode. Interactive Docker fallback may + remain available where the existing prompt flow already allows it. +- Keep `runIiiInstaller()`'s Windows/zip guard as a defensive boundary even if + startup avoids calling it for manual-only assets. +- Align doctor diagnostics with startup: detected mismatch, unparseable probe, + private pinned install wins, and Windows/manual-only install support should + produce accurate detail and manual-fix wording. + +Expected test shape: +- Add `test/iii-runtime.test.ts` for version parsing, probe runner injection, + compatibility selection, install planning, and mismatch notice behavior. +- Extend `test/cli-doctor-fixes.test.ts` for PATH `0.18.0` without private pin, + private `0.11.2` winning over PATH `0.18.0`, unparseable probe detail, and + Windows/manual-only dry-run wording if doctor auto/manual behavior changes. +- Avoid new tests that parse `src/cli.ts` source text. Use exported helper APIs + and injected doctor effects instead. +- Do not add live GitHub downloads, real iii binary execution, Windows zip + extraction tests, or dependency/lockfile changes for the minimum fix. + +Rejected in the solution arena: +- iii `0.18.0` as the default supported engine. +- Accepting arbitrary PATH iii when `--version` succeeds. +- Changing `AGENTMEMORY_III_VERSION` semantics. +- Windows zip auto-install, unzip dependencies, archive extraction behavior, + checksum/signature policy, or new executable write behavior. +- Non-interactive Docker auto-selection without `AGENTMEMORY_USE_DOCKER=1`. +- Silent standalone MCP fallback. +- MCP, REST, schema, persistence, export/import, auth, port, Docker image, or + worker-registration changes. + +## Progress Notes + +- 2026-06-19: Confirmed worktree at + `/Users/A1538552/.codex/worktrees/99e2/agentmemory`, detached `HEAD` + matching requested `origin/main` + `24ff6779f5618d0f07039161be4f750252747f31`. +- 2026-06-19: Confirmed remotes: `origin` is + `https://github.com/wbugitlab1/agentmemory.git`; `upstream` exists but is + out of scope. +- 2026-06-19: Created branch `issue/337-iii-0-18-compatibility`; no upstream is + configured yet. +- 2026-06-19: Public GitHub issue read confirms issue `#337` is open and the + reported transcript matches the delegated summary. +- 2026-06-19: Local inspection found the relevant code in `src/cli.ts` and + `src/cli/build-runtime.ts`; current main pins iii engine `0.11.2`, treats + mismatched PATH iii versions as incompatible by default, and currently reports + Windows zip auto-install as unavailable. +- 2026-06-19: Completed `$arena` validation. All candidates and the judge + converged that the issue is valid as startup/version-detection/Windows + remediation, while default iii `0.18.0` support is an engine-boundary change + outside the minimum fix. Stopped at Human Checkpoint before source changes. +- 2026-06-20: Ran a second `$arena` to select the best concrete solution. + Candidate B was selected as the base, with Candidate A's narrowness and + Candidate C's doctor/manual-fix UX grafted. The chosen solution is a focused + CLI helper/test implementation that improves version probing and Windows + mismatch guidance without changing installer, dependency, Docker default, + API/schema/persistence, or engine boundaries. +- 2026-06-20: User invoked `$github-feature-loop`. Recorded that this + authorizes task-owned local planning, implementation, verification, staging, + commits, and local PR-prep boundaries only. It does not authorize fetch, pull, + push, PR creation, PR merge, publish, deploy, migrations, destructive cleanup, + credentialed/session actions, history rewrite, or changes outside task-owned + files. Skill/tool discovery did not find `$github-push-prepare`; final + GitHub push-prep phase is a blocker unless the skill becomes available. +- 2026-06-20: Wrote implementation plan at + `docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md`. +- 2026-06-20: Completed pre-implementation subagent review. One reviewer + accepted the plan; two reviewers found Medium gaps in startup behavioral test + coverage and doctor probe health semantics. Updated the implementation plan + to require a pure startup-decision helper, direct doctor probe tests, and a + separation between display version and healthy compatible version before any + source-code edits. +- 2026-06-20: Human Checkpoint complete. User asked whether upgrading to a new + iii version was an option; answer was yes as a separate migration spike, not + as the #337 fix. User confirmed to proceed with the surgical non-upgrade + solution: robust version detection, preserved `0.11.2` default pin, private + pinned-engine precedence, and truthful Windows/manual/Docker guidance. +- 2026-06-20: Implemented with TDD. Red tests first covered missing helper + exports and Doctor semantics; later adversarial-review regressions covered + Windows no-PATH manual-only startup, failed-but-parseable probes, and healthy + PATH pin precedence over failed private probe. +- 2026-06-20: Final implementation keeps `AGENTMEMORY_III_VERSION` semantics and + default iii `0.11.2` pin. `iii --version` parsing now reads stdout and stderr; + compatibility checks use successful probes only; display diagnostics can still + show parseable versions from failed probes. +- 2026-06-20: Verification passed: targeted Vitest 72 tests, lint, full + non-integration Vitest suite 212 files / 2924 tests, build, Semgrep for + tracked files, and explicit Semgrep for new untracked files. `corepack pnpm + install --frozen-lockfile --ignore-scripts` was run after pnpm ignored-build + hardening blocked the first red-test attempt; generated `allowBuilds` + placeholder churn was removed. +- 2026-06-20: `$github-push-prepare` is still unavailable/discoverable, so full + GitHub PR-prep remains blocked. Public issue comment, branch push, and PR + creation remain unapproved remote writes. diff --git a/src/cli.ts b/src/cli.ts index 2dffc9fb..84788710 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -74,10 +74,19 @@ import { } from "./cli/stop-processes.js"; import { findIiiConfigPath, - iiiReleaseAsset, iiiReleaseUrl, resolveDataDir, } from "./cli/build-runtime.js"; +import { + canAutoInstallIii, + displayIiiVersionOrNull, + healthyIiiVersionOrNull, + planIiiInstall, + planIiiStartupDecision, + probeIiiVersion, + resolveCompatibleIiiPath, + type IiiVersionProbe, +} from "./cli/iii-runtime.js"; import { prepareEngineLaunch } from "./cli/engine-launch.js"; import { formatReadyTimeout, @@ -465,18 +474,16 @@ function fallbackIiiPaths(): string[] { return paths; } +function iiiBinProbe(binPath: string): IiiVersionProbe { + return probeIiiVersion(binPath); +} + +function iiiBinDisplayVersion(binPath: string): string | null { + return displayIiiVersionOrNull(iiiBinProbe(binPath)); +} + function iiiBinVersion(binPath: string): string | null { - try { - const out = execFileSync(binPath, ["--version"], { - encoding: "utf-8", - stdio: ["ignore", "pipe", "ignore"], - timeout: 3000, - }); - const match = out.match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/); - return match ? match[1]! : null; - } catch { - return null; - } + return healthyIiiVersionOrNull(iiiBinProbe(binPath)); } // Resolve a compatible iii binary for the pinned engine version. @@ -495,19 +502,20 @@ function iiiBinVersion(binPath: string): string | null { // engine can opt in. function resolveCompatibleIii(iiiBinPath: string | null | undefined): string | null { if (!iiiBinPath) return null; - const detected = iiiBinVersion(iiiBinPath); - if (detected && detected === IIPINNED_VERSION) return iiiBinPath; - - const privatePath = privateIiiPath(); - if (iiiBinPath !== privatePath && existsSync(privatePath)) { - const privateVersion = iiiBinVersion(privatePath); - if (privateVersion === IIPINNED_VERSION) { - const reason = detected ? `v${detected} mismatches pin` : "probe failed"; - vlog( - `iii at ${iiiBinPath} ${reason} v${IIPINNED_VERSION}; using private install at ${privatePath}.`, - ); - return privatePath; - } + const resolution = resolveCompatibleIiiPath({ + candidatePath: iiiBinPath, + privatePath: privateIiiPath(), + pinnedVersion: IIPINNED_VERSION, + }); + if (resolution.kind === "candidate") return resolution.binPath; + if (resolution.kind === "private") { + const reason = resolution.candidateProbe.version + ? `v${resolution.candidateProbe.version} mismatches pin` + : "probe failed"; + vlog( + `iii at ${iiiBinPath} ${reason} v${IIPINNED_VERSION}; using private install at ${resolution.binPath}.`, + ); + return resolution.binPath; } return null; @@ -795,21 +803,19 @@ function adoptRunningEngine(): void { } async function runIiiInstaller(): Promise<{ ok: boolean; binPath: string | null }> { - const releaseUrl = pinnedIiiReleaseUrl(); - const asset = iiiReleaseAsset(); - const isZipAsset = asset?.endsWith(".zip") === true; + const installPlan = planIiiInstall({ version: IIPINNED_VERSION }); - if (!releaseUrl) { + if (installPlan.kind === "unsupported-platform") { p.log.warn( `iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`, ); return { ok: false, binPath: null }; } - if (IS_WINDOWS || isZipAsset) { + if (!canAutoInstallIii(installPlan)) { p.log.info( - `Auto-install unavailable on ${platform()} — ${asset} isn't tar-compatible. Install manually:\n` + - ` 1. Download ${releaseUrl}\n` + + `Auto-install unavailable on ${platform()} — ${installPlan.asset} isn't tar-compatible. Install manually:\n` + + ` 1. Download ${installPlan.releaseUrl}\n` + ` 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\n` + `Or use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`, ); @@ -827,7 +833,7 @@ async function runIiiInstaller(): Promise<{ ok: boolean; binPath: string | null const binPath = privateIiiPath(); const installCmd = [ `mkdir -p "${binDir}"`, - `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`, + `curl -fsSL "${installPlan.releaseUrl}" | tar -xz -C "${binDir}"`, `chmod +x "${binPath}"`, ].join(" && "); const installerOk = runCommand(shBin, ["-c", installCmd], { @@ -1025,7 +1031,7 @@ async function startEngine(options: StartEngineOptions = {}): Promise { const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p)); for (const f of fallbacks) { - const v = iiiBinVersion(f); + const v = iiiBinDisplayVersion(f); vlog(`fallback iii at ${f} reports version: ${v ?? "unknown"}`); } @@ -1040,7 +1046,7 @@ async function startEngine(options: StartEngineOptions = {}): Promise { } if (pathIii && !iiiBin) { - const detected = iiiBinVersion(pathIii); + const detected = iiiBinDisplayVersion(pathIii); vlog( `iii on PATH is v${detected ?? "unknown"}, pin is v${IIPINNED_VERSION}. ` + `Will install pinned engine to ${privateIiiPath()}.`, @@ -1066,24 +1072,53 @@ async function startEngine(options: StartEngineOptions = {}): Promise { process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true"; const interactive = !!process.stdin.isTTY && !process.env["CI"]; + const installPlan = planIiiInstall({ version: IIPINNED_VERSION }); + const dockerAvailable = Boolean(dockerBin && composeFile); + const pathIiiVersion = pathIii ? iiiBinDisplayVersion(pathIii) : null; + const startupDecision = planIiiStartupDecision({ + compatibleIii: iiiBin, + pathIii, + pathIiiVersion, + pinnedVersion: IIPINNED_VERSION, + installPlan, + dockerAvailable, + dockerOptIn, + interactive, + }); type Choice = "install" | "docker" | "manual"; let choice: Choice; // Wrong-version iii on PATH is a configuration trap: any prompt would - // confuse the user since they already "have iii installed". Skip the - // prompt and auto-install pinned engine to the private location. - const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null; + // confuse the user since they already "have iii installed". Auto-install + // only where the pinned release asset is tar-compatible; otherwise show + // truthful manual/Docker guidance and leave the PATH iii untouched. + const pathIiiMismatch = pathIii !== null && iiiBin === null; - if (dockerOptIn && dockerBin && composeFile) { + if (startupDecision.kind === "docker" && dockerBin && composeFile) { choice = "docker"; - } else if (pathIiiMismatch) { + } else if (pathIiiMismatch && startupDecision.kind === "install") { choice = "install"; - const detected = iiiBinVersion(pathIii!); - p.log.info( - `iii on PATH is v${detected ?? "unknown"} but agentmemory pins v${IIPINNED_VERSION}. ` + - `Installing pinned engine to ~/.agentmemory/bin (leaves your existing iii untouched).`, - ); + p.log.info(startupDecision.notice.join("\n")); + } else if (startupDecision.kind === "prompt-docker") { + p.log.info(startupDecision.notice.join("\n")); + const fallback = await p.confirm({ + message: "Automatic native install is unavailable here. Try Docker compose instead?", + initialValue: true, + }); + if (p.isCancel(fallback) || fallback !== true) { + startupFailure = { kind: "no-engine" }; + return false; + } + choice = "docker"; + } else if (pathIiiMismatch) { + p.log.info(startupDecision.notice.join("\n")); + startupFailure = { kind: "no-engine" }; + return false; + } else if (startupDecision.kind === "manual" && startupDecision.notice.length > 0) { + p.log.info(startupDecision.notice.join("\n")); + startupFailure = { kind: "no-engine" }; + return false; } else if (!interactive) { choice = "install"; p.log.info("Non-interactive environment detected — auto-installing iii-engine."); @@ -1433,7 +1468,7 @@ async function main() { fallbackIiiPaths().find((p) => existsSync(p)) ?? null; if (attachedBin) { - const detected = iiiBinVersion(attachedBin); + const detected = iiiBinDisplayVersion(attachedBin); if (detected && detected !== IIPINNED_VERSION) { p.log.warn( `iii on PATH is v${detected} (from ${attachedBin}) but agentmemory v${VERSION} pins v${IIPINNED_VERSION}. ` + @@ -1782,6 +1817,8 @@ function buildDoctorEffects(): DoctorEffects { findIiiBinary: () => whichBinary("iii"), localBinIiiPath: () => privateIiiPath(), iiiBinaryVersion: (binPath: string) => iiiBinVersion(binPath), + iiiBinaryProbe: (binPath: string) => iiiBinProbe(binPath), + iiiInstallPlan: () => planIiiInstall({ version: IIPINNED_VERSION }), viewerReachable: async (timeoutMs = 2000) => { try { await discoverViewerPort(); diff --git a/src/cli/doctor-diagnostics.ts b/src/cli/doctor-diagnostics.ts index 05dd7d4d..c4d0b2eb 100644 --- a/src/cli/doctor-diagnostics.ts +++ b/src/cli/doctor-diagnostics.ts @@ -16,6 +16,14 @@ // agentmemory doctor --all # apply every available fix without prompting (CI) // agentmemory doctor --dry-run # show what each fix WOULD do; execute nothing +import { + canAutoInstallIii, + displayIiiVersionOrNull, + healthyIiiVersionOrNull, + type IiiInstallPlan, + type IiiVersionProbe, +} from "./iii-runtime.js"; + export type DiagnosticStatus = { ok: boolean; /** Short status detail (one line). Shown alongside the check name. */ @@ -211,6 +219,10 @@ export type DoctorEffects = { localBinIiiPath: () => string; /** Run `iii --version`; null if it fails. */ iiiBinaryVersion: (binPath: string) => string | null; + /** Run `iii --version` and return probe health plus parsed output. */ + iiiBinaryProbe?: (binPath: string) => IiiVersionProbe; + /** Native iii install support for this platform/version. */ + iiiInstallPlan?: () => IiiInstallPlan; /** Probe the viewer URL; true if it returns OK within timeoutMs. */ viewerReachable: (timeoutMs?: number) => Promise; /** Run init logic (copies .env.example). */ @@ -228,6 +240,32 @@ export type DoctorEffects = { }; export function buildDiagnostics(effects: DoctorEffects): Diagnostic[] { + const installPlan = effects.iiiInstallPlan?.(); + const autoInstallAvailable = installPlan ? canAutoInstallIii(installPlan) : true; + const probeFor = (binPath: string): IiiVersionProbe => { + const probe = effects.iiiBinaryProbe?.(binPath); + if (probe) return probe; + const version = effects.iiiBinaryVersion(binPath); + return { + ok: version !== null, + version, + stdout: "", + stderr: "", + status: version === null ? null : 0, + signal: null, + }; + }; + const displayVersion = (probe: IiiVersionProbe): string | null => + displayIiiVersionOrNull(probe); + const healthyVersion = (probe: IiiVersionProbe): string | null => + healthyIiiVersionOrNull(probe); + const probeDetail = (probe: IiiVersionProbe): string => { + if (probe.version) { + return probe.ok ? probe.version : `${probe.version} (--version failed)`; + } + return "--version could not be parsed"; + }; + return [ { id: "env-missing", @@ -267,21 +305,27 @@ export function buildDiagnostics(effects: DoctorEffects): Diagnostic[] { { id: "engine-version-mismatch", message: "No iii binary matches the version agentmemory pins to.", - fixPreview: - "Re-run the iii installer for the pinned version and restart the engine.", + fixPreview: autoInstallAvailable + ? "Install the pinned version to ~/.agentmemory/bin and restart the engine." + : "Install the pinned version manually or run with AGENTMEMORY_USE_DOCKER=1; automatic native install is unavailable.", moreInfo: "agentmemory pins the iii engine to a specific release because newer engines " + "use a different worker model. Running a mismatched binary surfaces as EPIPE " + "reconnect loops and empty search results. At runtime agentmemory prefers the " + "private install at ~/.agentmemory/bin/iii when the iii on PATH mismatches the pin.", + manualOnly: !autoInstallAvailable, check: async (ctx) => { const localBin = effects.localBinIiiPath(); - const localVersion = effects.iiiBinaryVersion(localBin); + const localProbe = probeFor(localBin); + const localVersion = healthyVersion(localProbe); + const localDisplayVersion = displayVersion(localProbe); const bin = effects.findIiiBinary(); - const pathVersion = bin ? effects.iiiBinaryVersion(bin) : null; + const pathProbe = bin ? probeFor(bin) : null; + const pathVersion = pathProbe ? healthyVersion(pathProbe) : null; + const pathDisplayVersion = pathProbe ? displayVersion(pathProbe) : null; if (localVersion === ctx.pinnedVersion) { const note = - pathVersion && pathVersion !== ctx.pinnedVersion + pathDisplayVersion && pathDisplayVersion !== ctx.pinnedVersion ? "; PATH iii is different but the private install wins at runtime" : ""; return { @@ -289,11 +333,25 @@ export function buildDiagnostics(effects: DoctorEffects): Diagnostic[] { detail: `private install ${localVersion} (pinned ${ctx.pinnedVersion})${note}`, }; } + if (pathVersion === ctx.pinnedVersion) { + return { + ok: true, + detail: `PATH iii ${pathVersion} (pinned ${ctx.pinnedVersion})`, + }; + } + if (localDisplayVersion === ctx.pinnedVersion) { + return { + ok: false, + detail: `private install ${probeDetail(localProbe)} (pinned ${ctx.pinnedVersion})`, + }; + } if (!bin) return { ok: false, detail: "iii not on PATH and no private install" }; - if (!pathVersion) return { ok: false, detail: "iii on PATH but --version failed" }; + if (!pathProbe || !pathDisplayVersion) { + return { ok: false, detail: "iii on PATH but --version could not be parsed" }; + } return { ok: pathVersion === ctx.pinnedVersion, - detail: `${pathVersion} (pinned ${ctx.pinnedVersion})`, + detail: `${pathVersion ?? probeDetail(pathProbe)} (pinned ${ctx.pinnedVersion})`, }; }, fix: async () => { @@ -386,16 +444,17 @@ export function buildDiagnostics(effects: DoctorEffects): Diagnostic[] { if (!bin) return { ok: true, detail: "iii not on PATH (handled elsewhere)" }; const localBin = effects.localBinIiiPath(); if (bin === localBin) return { ok: true }; - const localVersion = effects.iiiBinaryVersion(localBin); + const localVersion = healthyVersion(probeFor(localBin)); if (localVersion === ctx.pinnedVersion) { return { ok: true, detail: `private install pinned; PATH iii at ${bin} stays untouched`, }; } + const pathVersion = displayVersion(probeFor(bin)); return { ok: false, - detail: `iii at: ${bin}`, + detail: pathVersion ? `iii at: ${bin} (${pathVersion})` : `iii at: ${bin}`, }; }, fix: async () => diff --git a/src/cli/iii-runtime.ts b/src/cli/iii-runtime.ts new file mode 100644 index 00000000..adecd16b --- /dev/null +++ b/src/cli/iii-runtime.ts @@ -0,0 +1,312 @@ +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; +import { existsSync } from "node:fs"; +import { platform } from "node:os"; +import { iiiReleaseAsset, iiiReleaseUrl } from "./build-runtime.js"; + +const VERSION_PATTERN = /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/; + +export type IiiVersionProbe = { + ok: boolean; + version: string | null; + stdout: string; + stderr: string; + status: number | null; + signal: NodeJS.Signals | null; + errorMessage?: string; +}; + +export type IiiVersionRunner = ( + binPath: string, + args: readonly string[], + options: { + encoding: "utf-8"; + stdio: ["ignore", "pipe", "pipe"]; + timeout: number; + }, +) => Pick, "stdout" | "stderr" | "status" | "signal" | "error">; + +export type IiiInstallPlan = + | { kind: "auto-tar"; asset: string; releaseUrl: string } + | { kind: "manual-release"; asset: string; releaseUrl: string; reason: "zip-asset" } + | { + kind: "unsupported-platform"; + asset: null; + releaseUrl: null; + reason: "no-release-asset"; + }; + +export type IiiCompatibilityResolution = + | { + kind: "candidate"; + binPath: string; + candidateProbe: IiiVersionProbe; + privateProbe?: IiiVersionProbe; + } + | { + kind: "private"; + binPath: string; + candidateProbe: IiiVersionProbe; + privateProbe: IiiVersionProbe; + } + | { + kind: "none"; + candidateProbe: IiiVersionProbe; + privateProbe?: IiiVersionProbe; + }; + +export type IiiStartupDecision = + | { kind: "use-compatible"; binPath: string } + | { kind: "docker" } + | { kind: "install"; notice: string[] } + | { kind: "prompt-docker"; notice: string[] } + | { kind: "manual"; notice: string[] }; + +export function parseIiiVersionOutput(stdout = "", stderr = ""): string | null { + const match = `${stdout}\n${stderr}`.match(VERSION_PATTERN); + return match ? match[1]! : null; +} + +function defaultRunIiiVersion( + binPath: string, + args: readonly string[], + options: Parameters[2], +) { + return spawnSync(binPath, [...args], options); +} + +export function probeIiiVersion( + binPath: string, + run: IiiVersionRunner = defaultRunIiiVersion, + timeoutMs = 3000, +): IiiVersionProbe { + const result = run(binPath, ["--version"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: timeoutMs, + }); + const stdout = result.stdout ?? ""; + const stderr = result.stderr ?? ""; + const version = parseIiiVersionOutput(stdout, stderr); + const errorMessage = result.error instanceof Error ? result.error.message : undefined; + return { + ok: !result.error && result.status === 0 && version !== null, + version, + stdout, + stderr, + status: result.status ?? null, + signal: result.signal ?? null, + ...(errorMessage ? { errorMessage } : {}), + }; +} + +export function displayIiiVersionOrNull(probe: IiiVersionProbe): string | null { + return probe.version; +} + +export function healthyIiiVersionOrNull(probe: IiiVersionProbe): string | null { + return probe.ok ? probe.version : null; +} + +export function planIiiInstall({ + version, + nodePlatform = platform(), + nodeArch = process.arch, +}: { + version: string; + nodePlatform?: NodeJS.Platform; + nodeArch?: string; +}): IiiInstallPlan { + const asset = iiiReleaseAsset(nodePlatform, nodeArch); + const releaseUrl = iiiReleaseUrl(version, nodePlatform, nodeArch); + if (!asset || !releaseUrl) { + return { + kind: "unsupported-platform", + asset: null, + releaseUrl: null, + reason: "no-release-asset", + }; + } + if (nodePlatform !== "win32" && asset.endsWith(".tar.gz")) { + return { kind: "auto-tar", asset, releaseUrl }; + } + return { kind: "manual-release", asset, releaseUrl, reason: "zip-asset" }; +} + +export function canAutoInstallIii( + plan: IiiInstallPlan, +): plan is Extract { + return plan.kind === "auto-tar"; +} + +export function resolveCompatibleIiiPath({ + candidatePath, + privatePath, + pinnedVersion, + exists = existsSync, + probe = probeIiiVersion, +}: { + candidatePath: string; + privatePath: string; + pinnedVersion: string; + exists?: (path: string) => boolean; + probe?: (path: string) => IiiVersionProbe; +}): IiiCompatibilityResolution { + const candidateProbe = probe(candidatePath); + if (healthyIiiVersionOrNull(candidateProbe) === pinnedVersion) { + return { kind: "candidate", binPath: candidatePath, candidateProbe }; + } + + if (candidatePath !== privatePath && exists(privatePath)) { + const privateProbe = probe(privatePath); + if (healthyIiiVersionOrNull(privateProbe) === pinnedVersion) { + return { kind: "private", binPath: privatePath, candidateProbe, privateProbe }; + } + return { kind: "none", candidateProbe, privateProbe }; + } + + return { kind: "none", candidateProbe }; +} + +function formatDetectedVersion(version: string | null): string { + return version ? `v${version}` : "vunknown"; +} + +export function formatIiiMismatchNotice({ + candidatePath, + candidateVersion, + pinnedVersion, + installPlan, + dockerAvailable, + nodePlatform = platform(), + nodeArch = process.arch, +}: { + candidatePath: string; + candidateVersion: string | null; + pinnedVersion: string; + installPlan: IiiInstallPlan; + dockerAvailable: boolean; + nodePlatform?: NodeJS.Platform; + nodeArch?: string; +}): string[] { + const lines = [ + `iii on PATH is ${formatDetectedVersion(candidateVersion)} but agentmemory pins v${pinnedVersion} for its current worker model.`, + `Your existing iii at ${candidatePath} is left untouched.`, + ]; + + if (installPlan.kind === "manual-release") { + lines.push( + `Automatic native install is unavailable on ${nodePlatform} because ${installPlan.asset} must be installed manually.`, + `Install the pinned release manually: ${installPlan.releaseUrl}`, + ); + if (dockerAvailable) { + lines.push("Or re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + return lines; + } + + if (installPlan.kind === "unsupported-platform") { + lines.push(`No pinned iii-engine binary is available for ${nodePlatform}/${nodeArch}.`); + if (dockerAvailable) { + lines.push("Re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + return lines; + } + + lines.push( + "Installing pinned engine to ~/.agentmemory/bin (leaves your existing iii untouched).", + ); + return lines; +} + +function formatIiiUnavailableNotice({ + pinnedVersion, + installPlan, + dockerAvailable, + nodePlatform = platform(), + nodeArch = process.arch, +}: { + pinnedVersion: string; + installPlan: IiiInstallPlan; + dockerAvailable: boolean; + nodePlatform?: NodeJS.Platform; + nodeArch?: string; +}): string[] { + const lines = [`No compatible iii-engine v${pinnedVersion} was found.`]; + + if (installPlan.kind === "manual-release") { + lines.push( + `Automatic native install is unavailable on ${nodePlatform} because ${installPlan.asset} must be installed manually.`, + `Install the pinned release manually: ${installPlan.releaseUrl}`, + ); + if (dockerAvailable) { + lines.push("Or re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + return lines; + } + + if (installPlan.kind === "unsupported-platform") { + lines.push(`No pinned iii-engine binary is available for ${nodePlatform}/${nodeArch}.`); + if (dockerAvailable) { + lines.push("Re-run with AGENTMEMORY_USE_DOCKER=1 to use Docker."); + } + } + return lines; +} + +export function planIiiStartupDecision({ + compatibleIii, + pathIii, + pathIiiVersion, + pinnedVersion, + installPlan, + dockerAvailable, + dockerOptIn, + interactive, +}: { + compatibleIii: string | null; + pathIii: string | null; + pathIiiVersion: string | null; + pinnedVersion: string; + installPlan: IiiInstallPlan; + dockerAvailable: boolean; + dockerOptIn: boolean; + interactive: boolean; +}): IiiStartupDecision { + if (dockerOptIn && dockerAvailable) return { kind: "docker" }; + if (compatibleIii) return { kind: "use-compatible", binPath: compatibleIii }; + if (pathIii && canAutoInstallIii(installPlan)) { + return { + kind: "install", + notice: formatIiiMismatchNotice({ + candidatePath: pathIii, + candidateVersion: pathIiiVersion, + pinnedVersion, + installPlan, + dockerAvailable, + }), + }; + } + if (pathIii) { + const notice = formatIiiMismatchNotice({ + candidatePath: pathIii, + candidateVersion: pathIiiVersion, + pinnedVersion, + installPlan, + dockerAvailable, + }); + return dockerAvailable && interactive + ? { kind: "prompt-docker", notice } + : { kind: "manual", notice }; + } + if (!canAutoInstallIii(installPlan)) { + const notice = formatIiiUnavailableNotice({ + pinnedVersion, + installPlan, + dockerAvailable, + }); + return dockerAvailable && interactive + ? { kind: "prompt-docker", notice } + : { kind: "manual", notice }; + } + return { kind: "manual", notice: [] }; +} diff --git a/test/cli-doctor-fixes.test.ts b/test/cli-doctor-fixes.test.ts index 6ef81de0..04ae2846 100644 --- a/test/cli-doctor-fixes.test.ts +++ b/test/cli-doctor-fixes.test.ts @@ -131,6 +131,139 @@ describe("doctor v2 diagnostic catalog", () => { expect(status.ok).toBe(true); }); + it("engine-version-mismatch names PATH iii 0.18.0 when no private pin exists", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryVersion: (bin) => (bin === privatePath ? null : "0.18.0"), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.18.0"); + expect(status.detail).toContain("0.11.2"); + }); + + it("engine-version-mismatch is manual-only when auto-install is unavailable", () => { + const diagnostics = buildDiagnostics( + stubEffects({ + iiiInstallPlan: () => ({ + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + releaseUrl: "https://example.invalid/iii.zip", + reason: "zip-asset", + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + expect(check.manualOnly).toBe(true); + const lines = dryRunPlan(stubCtx(), [ + { diagnostic: check, status: { ok: false, detail: "0.18.0 (pinned 0.11.2)" } }, + ]); + expect(lines.some((line) => line.includes("manual fix:"))).toBe(true); + }); + + it("engine-version-mismatch uses probe output when legacy version fallback is null", async () => { + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + iiiBinaryVersion: () => null, + iiiBinaryProbe: () => ({ + ok: true, + version: "0.18.0", + stdout: "", + stderr: "iii 0.18.0\n", + status: 0, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.18.0"); + }); + + it("engine-version-mismatch fails a parseable but unhealthy private probe", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryProbe: (bin) => ({ + ok: false, + version: bin === privatePath ? "0.11.2" : "0.18.0", + stdout: bin === privatePath ? "0.11.2\n" : "0.18.0\n", + stderr: "", + status: 1, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("0.11.2"); + expect(status.detail).toContain("--version failed"); + }); + + it("engine-version-mismatch accepts a healthy pinned PATH binary when the private probe fails", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryProbe: (bin) => + bin === privatePath + ? { + ok: false, + version: "0.11.2", + stdout: "0.11.2\n", + stderr: "", + status: 1, + signal: null, + } + : { + ok: true, + version: "0.11.2", + stdout: "0.11.2\n", + stderr: "", + status: 0, + signal: null, + }, + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(true); + expect(status.detail).toContain("PATH iii 0.11.2"); + }); + + it("engine-version-mismatch names unparseable PATH probes", async () => { + const privatePath = "/Users/test/.agentmemory/bin/iii"; + const diagnostics = buildDiagnostics( + stubEffects({ + findIiiBinary: () => "/opt/homebrew/bin/iii", + localBinIiiPath: () => privatePath, + iiiBinaryProbe: () => ({ + ok: false, + version: null, + stdout: "iii development build\n", + stderr: "", + status: 0, + signal: null, + }), + }), + ); + const check = diagnostics.find((d) => d.id === "engine-version-mismatch")!; + const status = await check.check(stubCtx()); + expect(status.ok).toBe(false); + expect(status.detail).toContain("could not be parsed"); + }); + it("viewer-unreachable fails when viewer probe returns false", async () => { const diagnostics = buildDiagnostics( stubEffects({ viewerReachable: async () => false }), diff --git a/test/iii-runtime.test.ts b/test/iii-runtime.test.ts new file mode 100644 index 00000000..b40d63cc --- /dev/null +++ b/test/iii-runtime.test.ts @@ -0,0 +1,295 @@ +import { describe, expect, it } from "vitest"; +import { + canAutoInstallIii, + formatIiiMismatchNotice, + healthyIiiVersionOrNull, + parseIiiVersionOutput, + planIiiInstall, + planIiiStartupDecision, + probeIiiVersion, + resolveCompatibleIiiPath, +} from "../src/cli/iii-runtime.js"; + +describe("iii runtime version parsing", () => { + it("parses semver from common stdout formats", () => { + expect(parseIiiVersionOutput("0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii 0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii-engine v0.11.2+build.5\n")).toBe( + "0.11.2+build.5", + ); + }); + + it("parses semver from stderr and rejects partial versions", () => { + expect(parseIiiVersionOutput("", "iii-engine 0.18.0\n")).toBe("0.18.0"); + expect(parseIiiVersionOutput("iii 0.18\n")).toBeNull(); + expect(parseIiiVersionOutput("not a version\n")).toBeNull(); + }); +}); + +describe("iii version probing", () => { + it("captures stdout and stderr through the injected runner", () => { + const stdoutProbe = probeIiiVersion("/bin/iii", () => ({ + stdout: "0.18.0\n", + stderr: "", + status: 0, + signal: null, + error: undefined, + })); + expect(stdoutProbe).toMatchObject({ ok: true, version: "0.18.0" }); + + const stderrProbe = probeIiiVersion("/bin/iii", () => ({ + stdout: "", + stderr: "iii 0.18.0\n", + status: 0, + signal: null, + error: undefined, + })); + expect(stderrProbe).toMatchObject({ ok: true, version: "0.18.0" }); + }); + + it("preserves parseable versions from nonzero probes without marking them compatible", () => { + const probe = probeIiiVersion("/bin/iii", () => ({ + stdout: "0.18.0\n", + stderr: "", + status: 1, + signal: null, + error: undefined, + })); + expect(probe.version).toBe("0.18.0"); + expect(probe.ok).toBe(false); + expect(healthyIiiVersionOrNull(probe)).toBeNull(); + }); +}); + +describe("iii install planning", () => { + it("treats Windows zip assets as manual-only and Unix tar assets as auto-installable", () => { + const windowsPlan = planIiiInstall({ + version: "0.11.2", + nodePlatform: "win32", + nodeArch: "x64", + }); + expect(windowsPlan).toMatchObject({ + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + reason: "zip-asset", + }); + expect(canAutoInstallIii(windowsPlan)).toBe(false); + + const linuxPlan = planIiiInstall({ + version: "0.11.2", + nodePlatform: "linux", + nodeArch: "x64", + }); + expect(linuxPlan).toMatchObject({ + kind: "auto-tar", + asset: "iii-x86_64-unknown-linux-gnu.tar.gz", + }); + expect(canAutoInstallIii(linuxPlan)).toBe(true); + }); +}); + +describe("iii compatibility resolution", () => { + const pinned = "0.11.2"; + const privatePath = "/home/alex/.agentmemory/bin/iii"; + + it("accepts the candidate only when it matches the pin", () => { + const result = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: () => false, + probe: () => ({ + ok: true, + version: pinned, + stdout: "", + stderr: "", + status: 0, + signal: null, + }), + }); + expect(result).toMatchObject({ + kind: "candidate", + binPath: "/usr/local/bin/iii", + }); + }); + + it("chooses the private pinned binary over PATH iii 0.18.0", () => { + const result = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: (path) => path === privatePath, + probe: (path) => ({ + ok: true, + version: path === privatePath ? pinned : "0.18.0", + stdout: "", + stderr: "", + status: 0, + signal: null, + }), + }); + expect(result).toMatchObject({ kind: "private", binPath: privatePath }); + }); + + it("rejects a parseable pinned version from a failed candidate probe", () => { + const result = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: () => false, + probe: () => ({ + ok: false, + version: pinned, + stdout: "0.11.2\n", + stderr: "", + status: 1, + signal: null, + }), + }); + expect(result).toMatchObject({ kind: "none" }); + }); + + it("rejects PATH iii 0.18.0 unless the pin is explicitly 0.18.0", () => { + const defaultResult = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: pinned, + exists: () => false, + probe: () => ({ + ok: true, + version: "0.18.0", + stdout: "", + stderr: "", + status: 0, + signal: null, + }), + }); + expect(defaultResult).toMatchObject({ kind: "none" }); + + const explicitResult = resolveCompatibleIiiPath({ + candidatePath: "/usr/local/bin/iii", + privatePath, + pinnedVersion: "0.18.0", + exists: () => false, + probe: () => ({ + ok: true, + version: "0.18.0", + stdout: "", + stderr: "", + status: 0, + signal: null, + }), + }); + expect(explicitResult).toMatchObject({ + kind: "candidate", + binPath: "/usr/local/bin/iii", + }); + }); +}); + +describe("iii mismatch notice", () => { + it("explains manual-only Windows mismatch without promising auto-install", () => { + const lines = formatIiiMismatchNotice({ + candidatePath: "C:\\Users\\alex\\.local\\bin\\iii.exe", + candidateVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: { + kind: "manual-release", + asset: "iii-x86_64-pc-windows-msvc.zip", + releaseUrl: "https://example.invalid/iii.zip", + reason: "zip-asset", + }, + dockerAvailable: true, + nodePlatform: "win32", + }); + const text = lines.join("\n"); + expect(text).toContain("0.18.0"); + expect(text).toContain("0.11.2"); + expect(text).toContain("Automatic native install is unavailable"); + expect(text).toContain("AGENTMEMORY_USE_DOCKER=1"); + expect(text).toContain("left untouched"); + expect(text).not.toContain("Installing pinned engine"); + }); +}); + +describe("iii startup decision planning", () => { + it("keeps explicit Docker opt-in ahead of native mismatch fallback", () => { + const decision = planIiiStartupDecision({ + compatibleIii: null, + pathIii: "/usr/local/bin/iii", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ + version: "0.11.2", + nodePlatform: "linux", + nodeArch: "x64", + }), + dockerAvailable: true, + dockerOptIn: true, + interactive: false, + }); + expect(decision).toMatchObject({ kind: "docker" }); + }); + + it("chooses native install for tar-capable mismatches", () => { + const decision = planIiiStartupDecision({ + compatibleIii: null, + pathIii: "/usr/local/bin/iii", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ + version: "0.11.2", + nodePlatform: "linux", + nodeArch: "x64", + }), + dockerAvailable: true, + dockerOptIn: false, + interactive: false, + }); + expect(decision).toMatchObject({ kind: "install" }); + }); + + it("does not choose native install for Windows zip mismatches", () => { + const decision = planIiiStartupDecision({ + compatibleIii: null, + pathIii: "C:\\Users\\alex\\.local\\bin\\iii.exe", + pathIiiVersion: "0.18.0", + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ + version: "0.11.2", + nodePlatform: "win32", + nodeArch: "x64", + }), + dockerAvailable: true, + dockerOptIn: false, + interactive: false, + }); + expect(decision.kind).toBe("manual"); + expect(decision.notice.join("\n")).toContain( + "Automatic native install is unavailable", + ); + expect(decision.notice.join("\n")).not.toContain("Installing pinned engine"); + }); + + it("does not choose native install on Windows when no iii is on PATH", () => { + const decision = planIiiStartupDecision({ + compatibleIii: null, + pathIii: null, + pathIiiVersion: null, + pinnedVersion: "0.11.2", + installPlan: planIiiInstall({ + version: "0.11.2", + nodePlatform: "win32", + nodeArch: "x64", + }), + dockerAvailable: true, + dockerOptIn: false, + interactive: false, + }); + expect(decision.kind).toBe("manual"); + expect(decision.notice.join("\n")).toContain( + "Automatic native install is unavailable", + ); + expect(decision.notice.join("\n")).not.toContain("Installing pinned engine"); + }); +}); From ddea81ab817713e1167990b2886d2d47e8a9d749 Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 05:03:29 +0200 Subject: [PATCH 2/3] docs: record issue 337 final verification --- .../plan.md | 9 ++--- .../todo.md | 35 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md index ea01c789..29b7b7ee 100644 --- a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md @@ -22,7 +22,7 @@ Use these sources in order: GitHub feature-loop authorization: - Authorized now: task-owned planning, implementation, local verification, task-owned staging/commits, and mandatory local PR-prep phase if the required skill is available. - Not authorized now: fetch, pull, push, PR creation, PR merge, publish, deploy, migrations, destructive cleanup, credentialed/session actions, history rewrite, or changes outside task-owned files. -- `$github-push-prepare` is currently unavailable in local skill/tool search; record this as a final-phase blocker unless it becomes available. +- `$github-push-prepare` was not available as a local callable tool at planning time. Equivalent local PR-prep gates were completed manually: base fetch, merge from `origin/main`, post-merge diff check, verification, Semgrep, and current-tree Gitleaks. Remote push, PR creation, and public issue comment remain unapproved remote writes. ## File Structure @@ -47,7 +47,7 @@ No expected changes: | Windows zip install remains manual-only | `test/iii-runtime.test.ts` and CLI branch logic | Done | Install-plan and startup-decision tests cover `win32/x64` zip, Linux tar assets, mismatched PATH iii, and no-PATH Windows first run. | | Startup no longer promises Windows auto-install on mismatch | `test/iii-runtime.test.ts` plus CLI wiring review | Done | Startup-decision tests cover Windows manual/Docker guidance, Linux/macOS auto-tar install, explicit Docker opt-in precedence, and no false "Installing pinned engine" wording. | | Doctor diagnostics align with startup | `test/cli-doctor-fixes.test.ts` | Done | Tests cover detected mismatch, private pin wins, healthy PATH pin wins, unparseable probe, failed parseable probes, and manual-only dry-run behavior. | -| PR prep/security gates | `$github-push-prepare` if available; otherwise blocker | Pending | Skill missing at planning and final verification time. Local verification/security gates passed; staged Gitleaks pending until staging/commit. | +| PR prep/security gates | Local base sync, verification, Semgrep, Gitleaks | Done | Skill missing at planning time, so local gates were completed manually. Branch merged `origin/main` `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00`; post-merge targeted tests, lint, full test suite, build, Semgrep, and current-tree Gitleaks passed. Remote writes remain unapproved. | ## Boundaries @@ -1043,5 +1043,6 @@ Type consistency: - `IiiVersionProbe`, `IiiInstallPlan`, and `IiiCompatibilityResolution` are defined before use. - Tests import only exported helper names defined in Task 2. -Known issue: -- `$github-push-prepare` is mandatory for the full GitHub feature loop but is not installed or discoverable at plan time. The loop can implement and verify locally, then must report this final-phase blocker unless the skill/tool becomes available. +Closeout: +- Local implementation, review fixes, base sync, verification, and security gates are complete. +- Remote push, PR creation, and public issue comment still require explicit current-turn approval under the repo instructions. diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md index bb55f5bb..ff8ae619 100644 --- a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md @@ -75,9 +75,9 @@ Known boundaries: | Implementation | TDD + local diff review | Done | Added `src/cli/iii-runtime.ts`, wired `src/cli.ts` and `src/cli/doctor-diagnostics.ts`, and added focused tests in `test/iii-runtime.test.ts` / `test/cli-doctor-fixes.test.ts`. No iii upgrade, dependency, Docker default, MCP/REST/schema/persistence/auth change. | | Red/green tests | Vitest | Done | Initial red: `corepack pnpm exec vitest run test/iii-runtime.test.ts test/cli-doctor-fixes.test.ts` failed on missing helper/new Doctor semantics. After fixes: same command passed with 51 tests. | | Review fixes | Adversarial read-only review | Done | Reviewer found Windows no-PATH manual-only handling, display-vs-health leakage, and Doctor PATH/private precedence gaps; all were fixed with new regression tests. | -| Verification | Repo-native checks | Done | `corepack pnpm exec vitest run test/iii-runtime.test.ts test/build-runtime.test.ts test/cli-doctor-fixes.test.ts test/engine-launch.test.ts` passed 72 tests; `corepack pnpm run lint` passed; `corepack pnpm test` passed 212 files / 2924 tests; `corepack pnpm run build` passed. | -| Security gates | Semgrep / OSV decision | Done | `semgrep scan --config p/default --error --metrics=off .` passed with 0 findings for tracked files; explicit Semgrep on new `src/cli/iii-runtime.ts` and `test/iii-runtime.test.ts` passed with 0 findings. OSV not required: no dependency, lockfile, container, vendored, package-manager, or third-party package-surface changes. | -| PR prep/security gates | `$github-push-prepare` if available; otherwise blocker | Pending | `$github-push-prepare` remains unavailable in local skill/tool search. Staged Gitleaks still required before any local commit. Remote push/PR/issue comment remain unapproved current-turn remote writes. | +| Verification | Repo-native checks | Done | Pre-merge: targeted Vitest 72 tests, lint, full suite 212 files / 2924 tests, and build passed. Post-merge on `origin/main` `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00`: targeted Vitest 72 tests, lint, full suite 221 files / 3025 tests, and build passed. | +| Security gates | Semgrep / Gitleaks / OSV decision | Done | Semgrep passed with 0 findings before and after the base merge. Staged Gitleaks passed before commit `503f6041`; current-tree `gitleaks dir . --redact` passed with no leaks after the merge. Historical `gitleaks detect --source . --redact` reports 14 pre-existing history findings, so it is recorded as baseline history rather than current-tree leakage. OSV not required: no dependency, lockfile, container, vendored, package-manager, or third-party package-surface changes. | +| Local PR prep | Base sync and diff check | Done | Fetched `origin/main`, merged `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00` into `issue/337-iii-0-18-compatibility`, producing local merge commit `e9b72ec7`. PR diff against the new base remains the 7 task-owned files. Remote push, PR creation, and issue comment remain unapproved remote writes. | ## Subagent Ledger @@ -91,7 +91,7 @@ Known boundaries: | Solution arena candidate B | Read-only implementation solution report | No | Helper extraction and testable startup planning shape | Complete | Selected base by judge; best balance of version detection, compatibility planning, and preserved boundary. | | Solution arena candidate C | Read-only implementation solution report | No | User-facing CLI/doctor diagnostics consistency plan | Complete | Strong doctor/manual-fix UX; broader than necessary as a base. | | Solution arena cross-judge | Read-only candidate scoring | No | Solution scores, base pick, graft/reject list | Complete | Candidate B selected; graft A's narrower scope and C's doctor/manual-fix UX. | -| GitHub feature loop | Full local implementation loop | Yes, task-owned files only | Plan, review-gated implementation, verification, local branch prep if available | In progress | User invoked `$github-feature-loop`; remote fetch/push/PR creation still not approved. `$github-push-prepare` is not installed/discoverable at kickoff. | +| GitHub feature loop | Full local implementation loop | Yes, task-owned files only | Plan, review-gated implementation, verification, local branch prep if available | Complete locally | User invoked `$github-feature-loop`; local implementation, review fixes, verification, base sync, and local commit are complete. Remote push/PR creation and public issue comment remain unapproved remote writes. | | Pre-code review: architecture | Plan and task record | No | Boundary/scope review | Complete | ACCEPT; no High/Medium findings. | | Pre-code review: verification | Plan and task record | No | Test coverage review | Complete | Medium findings accepted: add testable startup-decision helper and doctor probe-API coverage. | | Pre-code review: security/boundary | Plan and task record | No | Engine/installer boundary review | Complete | Medium finding accepted: doctor must not treat failed parseable probes as healthy. | @@ -274,12 +274,21 @@ Rejected in the solution arena: default iii `0.11.2` pin. `iii --version` parsing now reads stdout and stderr; compatibility checks use successful probes only; display diagnostics can still show parseable versions from failed probes. -- 2026-06-20: Verification passed: targeted Vitest 72 tests, lint, full - non-integration Vitest suite 212 files / 2924 tests, build, Semgrep for - tracked files, and explicit Semgrep for new untracked files. `corepack pnpm - install --frozen-lockfile --ignore-scripts` was run after pnpm ignored-build - hardening blocked the first red-test attempt; generated `allowBuilds` - placeholder churn was removed. -- 2026-06-20: `$github-push-prepare` is still unavailable/discoverable, so full - GitHub PR-prep remains blocked. Public issue comment, branch push, and PR - creation remain unapproved remote writes. +- 2026-06-20: Pre-merge verification passed: targeted Vitest 72 tests, lint, + full non-integration Vitest suite 212 files / 2924 tests, build, Semgrep for + tracked files, explicit Semgrep for new files, and staged Gitleaks. `corepack + pnpm install --frozen-lockfile --ignore-scripts` was run after pnpm + ignored-build hardening blocked the first red-test attempt; generated + `allowBuilds` placeholder churn was removed. +- 2026-06-20: Created local commit `503f6041fc22dc45cb72c8851482e8d175ae16cf` + (`fix: improve iii runtime compatibility diagnostics`). +- 2026-06-20: Synced with `origin/main` + `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00` and created local merge commit + `e9b72ec7`. Post-merge PR diff remains scoped to the 7 task-owned files. +- 2026-06-20: Post-merge verification passed: targeted Vitest 72 tests, lint, + full non-integration Vitest suite 221 files / 3025 tests, build, Semgrep with + 0 findings, and current-tree Gitleaks with no leaks. `gitleaks detect + --source . --redact` still reports 14 historical findings from existing Git + history; no current-tree leak was found. +- 2026-06-20: Public issue comment, branch push, and PR creation remain + unapproved remote writes under the active repo instructions. From 688f4f18203c8c320510e5df7763c27b2ed55272 Mon Sep 17 00:00:00 2001 From: Willi Budzinski Date: Sat, 20 Jun 2026 05:22:26 +0200 Subject: [PATCH 3/3] docs: record issue 337 publication --- .../2026-06-19-issue-337-iii-0-18-compatibility/plan.md | 6 +++--- .../2026-06-19-issue-337-iii-0-18-compatibility/todo.md | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md index 29b7b7ee..e5fdcc06 100644 --- a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/plan.md @@ -22,7 +22,7 @@ Use these sources in order: GitHub feature-loop authorization: - Authorized now: task-owned planning, implementation, local verification, task-owned staging/commits, and mandatory local PR-prep phase if the required skill is available. - Not authorized now: fetch, pull, push, PR creation, PR merge, publish, deploy, migrations, destructive cleanup, credentialed/session actions, history rewrite, or changes outside task-owned files. -- `$github-push-prepare` was not available as a local callable tool at planning time. Equivalent local PR-prep gates were completed manually: base fetch, merge from `origin/main`, post-merge diff check, verification, Semgrep, and current-tree Gitleaks. Remote push, PR creation, and public issue comment remain unapproved remote writes. +- `$github-push-prepare` was not available as a local callable tool at planning time. Equivalent local PR-prep gates were completed manually: base fetch, merge from `origin/main`, post-merge diff check, verification, Semgrep, and current-tree Gitleaks. After the user confirmed `continue`, the branch was pushed to `origin`, PR `#1034` was opened, and issue `#337` was commented. ## File Structure @@ -47,7 +47,7 @@ No expected changes: | Windows zip install remains manual-only | `test/iii-runtime.test.ts` and CLI branch logic | Done | Install-plan and startup-decision tests cover `win32/x64` zip, Linux tar assets, mismatched PATH iii, and no-PATH Windows first run. | | Startup no longer promises Windows auto-install on mismatch | `test/iii-runtime.test.ts` plus CLI wiring review | Done | Startup-decision tests cover Windows manual/Docker guidance, Linux/macOS auto-tar install, explicit Docker opt-in precedence, and no false "Installing pinned engine" wording. | | Doctor diagnostics align with startup | `test/cli-doctor-fixes.test.ts` | Done | Tests cover detected mismatch, private pin wins, healthy PATH pin wins, unparseable probe, failed parseable probes, and manual-only dry-run behavior. | -| PR prep/security gates | Local base sync, verification, Semgrep, Gitleaks | Done | Skill missing at planning time, so local gates were completed manually. Branch merged `origin/main` `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00`; post-merge targeted tests, lint, full test suite, build, Semgrep, and current-tree Gitleaks passed. Remote writes remain unapproved. | +| PR prep/security gates | Local base sync, verification, Semgrep, Gitleaks, push, PR, issue comment | Done | Skill missing at planning time, so local gates were completed manually. Branch merged `origin/main` `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00`; post-merge targeted tests, lint, full test suite, build, Semgrep, and current-tree Gitleaks passed. After approval, pushed to `origin`, opened PR `#1034`, and commented issue `#337`. | ## Boundaries @@ -1045,4 +1045,4 @@ Type consistency: Closeout: - Local implementation, review fixes, base sync, verification, and security gates are complete. -- Remote push, PR creation, and public issue comment still require explicit current-turn approval under the repo instructions. +- Remote push, PR creation, and public issue comment are complete after the user confirmed `continue`. diff --git a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md index ff8ae619..1689fdba 100644 --- a/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md +++ b/docs/todos/2026-06-19-issue-337-iii-0-18-compatibility/todo.md @@ -77,7 +77,7 @@ Known boundaries: | Review fixes | Adversarial read-only review | Done | Reviewer found Windows no-PATH manual-only handling, display-vs-health leakage, and Doctor PATH/private precedence gaps; all were fixed with new regression tests. | | Verification | Repo-native checks | Done | Pre-merge: targeted Vitest 72 tests, lint, full suite 212 files / 2924 tests, and build passed. Post-merge on `origin/main` `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00`: targeted Vitest 72 tests, lint, full suite 221 files / 3025 tests, and build passed. | | Security gates | Semgrep / Gitleaks / OSV decision | Done | Semgrep passed with 0 findings before and after the base merge. Staged Gitleaks passed before commit `503f6041`; current-tree `gitleaks dir . --redact` passed with no leaks after the merge. Historical `gitleaks detect --source . --redact` reports 14 pre-existing history findings, so it is recorded as baseline history rather than current-tree leakage. OSV not required: no dependency, lockfile, container, vendored, package-manager, or third-party package-surface changes. | -| Local PR prep | Base sync and diff check | Done | Fetched `origin/main`, merged `5ad88c08197c2cb15675a4a974b9e8f37dfd1f00` into `issue/337-iii-0-18-compatibility`, producing local merge commit `e9b72ec7`. PR diff against the new base remains the 7 task-owned files. Remote push, PR creation, and issue comment remain unapproved remote writes. | +| GitHub publication | Push, PR, issue comment | Done | After user confirmed `continue`, pushed `issue/337-iii-0-18-compatibility` to `origin`, opened PR `#1034` at `https://github.com/wbugitlab1/agentmemory/pull/1034`, and commented on issue `#337` at `https://github.com/wbugitlab1/agentmemory/issues/337#issuecomment-4756181733`. | ## Subagent Ledger @@ -91,7 +91,7 @@ Known boundaries: | Solution arena candidate B | Read-only implementation solution report | No | Helper extraction and testable startup planning shape | Complete | Selected base by judge; best balance of version detection, compatibility planning, and preserved boundary. | | Solution arena candidate C | Read-only implementation solution report | No | User-facing CLI/doctor diagnostics consistency plan | Complete | Strong doctor/manual-fix UX; broader than necessary as a base. | | Solution arena cross-judge | Read-only candidate scoring | No | Solution scores, base pick, graft/reject list | Complete | Candidate B selected; graft A's narrower scope and C's doctor/manual-fix UX. | -| GitHub feature loop | Full local implementation loop | Yes, task-owned files only | Plan, review-gated implementation, verification, local branch prep if available | Complete locally | User invoked `$github-feature-loop`; local implementation, review fixes, verification, base sync, and local commit are complete. Remote push/PR creation and public issue comment remain unapproved remote writes. | +| GitHub feature loop | Full local implementation loop | Yes, task-owned files only | Plan, review-gated implementation, verification, local branch prep if available | Complete | User invoked `$github-feature-loop`; local implementation, review fixes, verification, base sync, commits, branch push, PR creation, and public issue comment are complete. | | Pre-code review: architecture | Plan and task record | No | Boundary/scope review | Complete | ACCEPT; no High/Medium findings. | | Pre-code review: verification | Plan and task record | No | Test coverage review | Complete | Medium findings accepted: add testable startup-decision helper and doctor probe-API coverage. | | Pre-code review: security/boundary | Plan and task record | No | Engine/installer boundary review | Complete | Medium finding accepted: doctor must not treat failed parseable probes as healthy. | @@ -290,5 +290,6 @@ Rejected in the solution arena: 0 findings, and current-tree Gitleaks with no leaks. `gitleaks detect --source . --redact` still reports 14 historical findings from existing Git history; no current-tree leak was found. -- 2026-06-20: Public issue comment, branch push, and PR creation remain - unapproved remote writes under the active repo instructions. +- 2026-06-20: User confirmed `continue` after the remote-write checkpoint. + Pushed `issue/337-iii-0-18-compatibility` to `origin`, opened PR `#1034` + against `origin/main`, and posted the outcome comment on issue `#337`.