Skip to content

Ship Nod in the app bundle and make it launchable where its ramp is on - #600

Merged
scgopi merged 3 commits into
mainfrom
feat/nod-integration
Oct 2, 2026
Merged

scgopi merged 3 commits into
mainfrom
feat/nod-integration

Conversation

@scgopi

@scgopi scgopi commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Final integration for GraphCode Nod, after #583 and #588–#592: Nod now ships in the app bundle and is launchable on installs where its beta ramp is on.

Change Detail
Bundling make build-nod packages NodRuntime; release-dmg copies it to Contents/Helpers/nod and signs graphcode-nod, copilot-runtime and runtime.node with NodRuntime/packaging/entitlements.plist (JIT)
Launchable .nod.isSpiked = this process could launch it (ramp flag + runtime present). App, daemon and CLI install the check at startup; the test host does not, so hosting tests don't depend on the Mac's own install
Ramp at launch The app publishes nod/ramp.on from the cached ramp, so a fresh beta install doesn't wait on the fetch
Lineage --inherit <brief> on fresh launches only; composite children launch --unattended
One state dir The chat pane uses NodRuntimeLocator.stateDirectory; the app's duplicate helper is removed

Still owed, in parallel loops: mounting the graphcode MCP server in both engines (NodMcpMount), and the chat pane's graph slots, delegate actions and card wiring (NodWiring).

Evidence

RED: NodLaunchTests.theLineageBriefRidesAFreshLaunchOnly on origin/main -> compile failure, nodArguments has no fresh: parameter and never passes the lineage brief
GREEN: xcodebuild test (private DerivedData) -> exit 0, 2192 tests in 251 suites passed
GREEN: make build-nod, then bun scripts/smoke.ts copilot with the packaged graphcode-nod and both SDK platform packages hidden -> exit 0, hunk accepted, ask allowed, steer, goal check holds 2/2
REGRESSION: graphcode-cli and graphcoded build -> exit 0; make check -> 0 errors; swift-portable test -> exit 0

- `make build-nod` packages NodRuntime; `release-dmg` copies it to
  Contents/Helpers/nod and signs it with the runtime's JIT entitlements.
- `.nod` is spiked wherever this process could launch it: the ramp flag is set
  and a runtime is present. The app, daemon and CLI install that check at
  startup (`NodRuntimeLocator.installAvailability`); the test host does not, so
  hosting assertions never depend on the Mac's own install.
- The app publishes the ramp flag at launch from the cached ramp, so a fresh
  beta install need not wait on the network fetch.
- A loop's lineage brief rides `--inherit` on a fresh launch only, and a
  composite child launches `--unattended`.
- The chat pane reads its state directory from `NodRuntimeLocator`; the app's
  duplicate helper is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>

@scgopi scgopi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review — ✅ no blockers (would approve; GitHub won't let the author's account approve its own PR, so this is posted as a comment)

Reviewed at 9539b55f against origin/main 9ff1442c. Nothing here blocks the merge. The notes below are follow-ups.

What I checked

Area Verdict Evidence
make build-nod ✅ package.sh <out> bun-darwin-arm64 writes graphcode-nod, copilot-runtime, runtime.node and claude flat into .build/nod, which matches the three names the signing loop expects. Arm64-only is consistent with release-dmg
Bundling into Contents/Helpers/nod ✅ ditto keeps modes and mtimes. DaemonBootstrap.bundledNodDirectory resolves Contents/Helpers/nod from Resources/bin, and installNodRuntime copies the whole directory and clears quarantine
Signing (Developer ID branch) ✅ The three Nod binaries are signed with hardened runtime, a timestamp and the JIT entitlements before the outer app is sealed. In the ad-hoc branch, --deep re-signs them without hardened runtime, so JIT isn't needed there
Bundled Claude binary ✅ (not run) claude-agent-sdk-darwin-arm64/claude is already Developer ID Application: Anthropic PBC (Q6L2SF6YDW) with the runtime flag and a secure timestamp, so leaving Anthropic's signature in place should pass notarization. No signed or notarized release-dmg run is in the evidence, so this part is still unproven
isSpiked hook ✅ Installed in all three shipped processes, in the right order: app init (after SupportDirectory.prepare, before the lazy static store), graphcoded (straight after prepare) and the CLI (before parse). The check runs lazily on every call, so the daemon picks up a ramp flag or runtime that appears after it starts. No other executable hosts loops. The test host skips it through XCTestConfigurationFilePath
Ramp flag at launch ✅ publishNodFlag(isEnabled(.nod)) reads the cached configuration, or the default (beta 100 / stable 0), synchronously. It also removes the flag when the ramp is off, so the kill switch still applies from the next launch
--inherit fresh-only, --unattended for composite children ✅ resumeArguments passes fresh: false. The no-prompt and prompt paths both default to fresh. The runtime also drops --inherit under --resume (main.ts:112, runtime.ts:109)
NodStateDirectory removed ✅ No references are left. The pane and the launcher now share NodRuntimeLocator.stateDirectory, with the same uppercase uuidString path
bun test + typecheck ❌ Not this PR src/mcp/daemon.ts(103) TS2322 came in on main with the graph-layer merge (7bdb150f). This PR doesn't touch NodRuntime/, and #601 fixes it (event[key]!)

Do the tests prove what they claim?

Test Fails on main? Notes
theLineageBriefRidesAFreshLaunchOnly ✅ Yes It doesn't compile on main (there's no fresh: parameter), and its logic would fail anyway because main never reads lineage.briefPath. It covers both the fresh-only rule and the composite-child --unattended
aTimedLoopIsUnattended… (edited) ✅ It now gets the brief from lineage, not an argument
nodHostsNothingWhereItCannotLaunch ⚠️ Passes on main too It pins the uninstalled default, which is correct. No test exercises installAvailability() or its truth table (ramp on/off × runtime present/absent). That's understandable, since it's process-global state in a parallel suite, but the PR's core switch has no test
theStateDirectoryIsTheUppercaseNodeID Passes on main It's a refactor guard and doesn't claim RED

Non-blocking follow-ups

  1. The pane's own launch skips the new rules. GhosttyTerminalView.nodLaunchPrefix builds argv without lineage: no --inherit, and unattended only for .timeBased. Its doc says it uses "the same nodArguments the daemon launches with", but that's no longer true. Any Nod loop the pane starts itself won't get the brief. That includes turn-based forks, which only the pane ever starts, and a pane launch that beats the daemon's launch. It was already missing on main for --inherit; the composite-child --unattended difference is new. The pane is the NodWiring work, so it's probably best fixed there.
  2. Size. .build/nod is about 360 MB (claude alone is 228 MB). Every DMG and update carries it, including stable, where the ramp is 0%. DaemonBootstrap also copies it into each workspace's bin. It's worth deciding this on purpose before stable.
  3. Prove the release path once with make release-dmg SIGN_ID=… NOTARIZE=0, then codesign --verify --deep --strict and a notarytool submit, before the next beta is cut. That's the only way to confirm the claim about the Anthropic-signed nested binary.
  4. macos and the Windows jobs were still pending when I posted this.

scgopi and others added 2 commits October 2, 2026 09:57
A Mac with Nod installed has nod/ramp.on and bin/nod/graphcode-nod in the
~/.graphcode the test host shares, so launch-prefix assertions changed with
the machine (CopilotVersionTests.otherBackendsAreUnchanged(.nod) failed).
Under XCTest only the explicit overrides count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>
Signed-off-by: scgopi <scgopireddy@gmail.com>

# Conflicts:
#	graphcode/Sources/GraphcodeApp.swift
@scgopi
scgopi merged commit fc6d40c into main Oct 2, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant