diff --git a/.project-docs/30-worklog/tasks/20260902-build-unsigned-mac-9d7e4a2c.md b/.project-docs/30-worklog/tasks/20260902-build-unsigned-mac-9d7e4a2c.md new file mode 100644 index 0000000..9c0d65c --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-build-unsigned-mac-9d7e4a2c.md @@ -0,0 +1,70 @@ +# Task: Build unsigned macOS installer + +## Identity + +- Task ID: 20260902-build-unsigned-mac-9d7e4a2c +- Mode: Feature +- Branch: main +- Worktree: /Users/inmanx/Documents/makelore +- Base commit: 301c1496b0a3a1af3b9443a68a67de8dfea45749 +- Owner: codex +- Status: Ready for integration + +## Scope + +- Build a fresh unsigned macOS arm64 installer from the current `main` worktree. +- Produce a local-only DMG artifact under ignored `release/`; do not publish. +- Verify architecture, absence of a trusted code signature, and basic DMG contents. +- Diagnose the installed package's `CODING_RUNTIME_UNAVAILABLE` failure, fix the packaged Agent Server bootstrap, and rebuild a locally usable artifact. + +## Intent And Constraints + +- Use the exact pnpm version pinned by `packageManager` through Corepack. +- Stage the bundled Python, uv, and Pi runtime required by the packaged app. +- Disable macOS signing identity discovery and notarization explicitly. +- Limit the product fix to packaged Agent Server dependency resolution and a focused real-process regression test; do not change release configuration, app identity, backend contracts, or canonical project memory. +- Build only the current Apple Silicon (`arm64`) architecture because the user requested a package for local use on this Mac. + +## Outcome + +- The first unsigned installer opened but its Code module reported `CODING_RUNTIME_UNAVAILABLE`. The installed app log identified `ERR_MODULE_NOT_FOUND` for `@earendil-works/pi-ai` from `Contents/Resources/resources/pi-agent-server.mjs`. +- Root cause: Node 24 did not honor the parent URL passed to `import.meta.resolve`, so the packaged Agent Server resolved the package from the sibling product-resources tree instead of `Contents/Resources/pi-runtime/node_modules`. +- Updated `resources/pi-agent-server.mjs` to locate the package manifest from the configured runtime root with `findPackageJSON`, select its import entry, validate that the entry remains inside the package, and import that explicit file URL. +- Added a real-process regression test that copies the server resource outside the project module tree, reproducing the packaged directory relationship while using the staged runtime root. +- Rebuilt the corrected artifact as both `/Users/inmanx/Documents/makelore/release/Makelore-2.0.0-mac-arm64.dmg` and the identical, user-facing `/Users/inmanx/Documents/makelore/release/Makelore-2.0.0-mac-arm64-fixed.dmg`. +- The corrected installer is 315,653,270 bytes with SHA-256 `6d0216da6c30f7fed537041b37c69811b8e025af3e9cccf85a64c690e29ecb7b`. It supersedes the earlier unusable DMG and checksum recorded by this task. +- The final DMG-only electron-builder run exited successfully with `mac.identity: null`, `mac.notarize: false`, `--publish never`, and explicit `dmg`/`arm64` target selection. No Developer ID identity was used and no notarization or publication occurred. +- Release configuration, app identity, backend contracts, and canonical project-memory files were unchanged. + +## Verification + +- `corepack pnpm exec vitest run tests/unit/pi-agent-server-process-real.test.ts`: passed, 3/3 tests including the packaged-layout regression. +- `corepack pnpm run typecheck`: passed. +- `corepack pnpm run lint:check`: passed with 0 errors and 12 pre-existing warnings. +- `corepack pnpm test`: passed; 212 test files, 1,733 passed plus the separate pressure test, and 3 skipped. +- `corepack pnpm run build:vite`: passed; Vite reported only existing chunk/dynamic-import and Browserslist age warnings. +- `node scripts/bundle-pi-runtime.mjs --target darwin-arm64`: passed with 131 production packages and 7 runtime assets. +- `hdiutil verify release/Makelore-2.0.0-mac-arm64.dmg`: valid checksum for the corrected DMG. +- `file` and `lipo -archs` on the packaged executable: `Mach-O 64-bit executable arm64` / `arm64`. +- Effective builder config: `identity: null` and `notarize: false`; builder logged `skipped macOS code signing`. +- `codesign --verify` returned non-zero for the app and DMG, confirming no valid product/DMG signature. The unsigned Electron app retains upstream binary signature metadata, so this is not a signed or notarized distribution claim. +- Read-only DMG mount: passed; contains `Makelore.app`, the `/Applications` symlink, version `2.0.0`, and executable bundled uv, Python, and Pi runtime entries. +- `corepack pnpm run verify:artifact:pi -- --app-exe .../release/mac-arm64/Makelore.app/Contents/MacOS/Makelore --samples 1 --timeout-ms 10000`: top-level result `pass`; Pi `0.84.2`, target `darwin-arm64`, missing packages `[]`, missing assets `[]`, and the final executable runtime probe completed. The nested release-wide runtime report retains its pre-existing cross-platform and real-Provider waivers and therefore reports `partial-pass` rather than claiming a signed release gate. +- Direct JSONL initialize/shutdown against the unpacked corrected app's bundled Helper, Agent Server resource, and Pi runtime passed with exit code 0. +- Mounted the exact corrected DMG read-only and repeated the bundled Agent Server initialize/shutdown handshake directly from the mounted image: both responses succeeded, exit code 0, empty stderr. +- `corepack pnpm run smoke:pi:real ...` launched the packaged Electron Main and progressed through an active parent Agent Server and active child subagent. Its later macOS process-enumeration assertion remained unsupported (`processes.supported: false`), so this task does not claim that release-wide smoke gate as a full pass. +- `git diff --check`: passed. + +## Follow-ups + +- Replace the previously installed `/Applications/Makelore.app` with the corrected `-fixed.dmg` build before testing Code again; the old mounted installer volume still refers to the superseded artifact. +- This artifact is for local Apple Silicon use only. A signed/notarized macOS release, cross-platform evidence, real-Provider evidence, and macOS process-enumeration support remain outside this task. + +## Promotion Candidates + +- Target: `.project-docs/30-worklog/current-state.md` + Proposal: record that packaged Agent Server package resolution is anchored to the staged Pi runtime and that the unsigned arm64 local DMG passed an exact mounted-image initialize/shutdown handshake, without presenting it as signed-release readiness. + Evidence: `resources/pi-agent-server.mjs`, `tests/unit/pi-agent-server-process-real.test.ts`, and corrected artifact SHA-256 `6d0216da6c30f7fed537041b37c69811b8e025af3e9cccf85a64c690e29ecb7b`. + Future impact: future packaging changes should retain the runtime-root resolution regression test and distinguish local unsigned artifact usability from signed/notarized release readiness. + Semantic-conflict check: no conflict with the accepted packaging ADR because signed/notarized release readiness remains explicitly open. + Human confirmation required: no. diff --git a/resources/pi-agent-server.mjs b/resources/pi-agent-server.mjs index 9c1b368..f903781 100644 --- a/resources/pi-agent-server.mjs +++ b/resources/pi-agent-server.mjs @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; import { readFile } from 'node:fs/promises'; +import { findPackageJSON } from 'node:module'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -14,12 +15,36 @@ const runtimeRoot = runtimeRootFromArgs(process.argv.slice(2)); const runtimeModule = (...segments) => pathToFileURL(path.join(runtimeRoot, ...segments)).href; const runtimePackageUrl = pathToFileURL(path.join(runtimeRoot, 'package.json')).href; +async function runtimePackageModule(packageName) { + const packageJsonPath = findPackageJSON(packageName, runtimePackageUrl); + if (!packageJsonPath) throw new Error(`Runtime package is unavailable: ${packageName}`); + const packageRoot = path.dirname(packageJsonPath); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); + const rootExport = packageJson.exports?.['.']; + const entry = ( + typeof rootExport === 'string' + ? rootExport + : rootExport?.import + ) ?? packageJson.module ?? packageJson.main; + if (typeof entry !== 'string' || !entry.trim()) { + throw new Error(`Runtime package import entry is unavailable: ${packageName}`); + } + const entryPath = path.resolve(packageRoot, entry); + const entryRelative = path.relative(packageRoot, entryPath); + if (!entryRelative || entryRelative === '..' || entryRelative.startsWith(`..${path.sep}`) + || path.isAbsolute(entryRelative)) { + throw new Error(`Runtime package import entry escapes its package: ${packageName}`); + } + return pathToFileURL(entryPath).href; +} + const outputGuard = await import(runtimeModule('dist', 'core', 'output-guard.js')); outputGuard.takeOverStdout(); +const piAiModule = await runtimePackageModule('@earendil-works/pi-ai'); const [pi, piAi, httpDispatcher, jsonEvents, jsonl, themeModule, shellModule] = await Promise.all([ import(runtimeModule('dist', 'index.js')), - import(import.meta.resolve('@earendil-works/pi-ai', runtimePackageUrl)), + import(piAiModule), import(runtimeModule('dist', 'core', 'http-dispatcher.js')), import(runtimeModule('dist', 'modes', 'json-event.js')), import(runtimeModule('dist', 'modes', 'rpc', 'jsonl.js')), diff --git a/tests/unit/pi-agent-server-process-real.test.ts b/tests/unit/pi-agent-server-process-real.test.ts index 4e076bc..d9a1316 100644 --- a/tests/unit/pi-agent-server-process-real.test.ts +++ b/tests/unit/pi-agent-server-process-real.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { copyFile, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { createServer, type ServerResponse } from 'node:http'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -165,6 +165,32 @@ afterEach(async () => { }); describe('Pi Agent Server real process', () => { + it('resolves runtime packages when the server resource is outside the project module tree', async () => { + const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-packaged-layout-')); + roots.push(root); + const resourcesDir = path.join(root, 'product-resources', 'resources'); + const configDir = path.join(root, 'config'); + await Promise.all([ + mkdir(resourcesDir, { recursive: true }), + mkdir(configDir, { recursive: true }), + ]); + const serverPath = path.join(resourcesDir, 'pi-agent-server.mjs'); + await copyFile(path.resolve('resources/pi-agent-server.mjs'), serverPath); + const runtimeRoot = path.resolve('node_modules/@earendil-works/pi-coding-agent'); + const server = new PiAgentServerProcess({ + executablePath: process.execPath, + serverPath, + runtimeRoot, + configDir, + }); + try { + await expect(server.start()).resolves.toBeUndefined(); + expect(server.processId).toBeTypeOf('number'); + } finally { + await server.stop().catch(() => undefined); + } + }, 10_000); + it('executes two write-leased Bash calls without waiting for the HTTP idle timeout', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-agent-server-bash-batch-')); roots.push(root);