fix(coding): resolve packaged Pi imports from runtime root
This commit is contained in:
@@ -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.
|
||||
@@ -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')),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user