From 0704be48870a83d2b7e21e0665f6ba24bc3097ce Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Sun, 23 Aug 2026 22:57:41 +0530 Subject: [PATCH 1/2] feat(skills): add guarded selfhost upgrade workflow --- .../skills/executor-selfhost-upgrade/SKILL.md | 65 ++++ .../references/deploy.md | 82 ++++ .../references/rebase.md | 75 ++++ .../references/topology.md | 48 +++ .../scripts/preflight.ts | 361 ++++++++++++++++++ 5 files changed, 631 insertions(+) create mode 100644 .agents/skills/executor-selfhost-upgrade/SKILL.md create mode 100644 .agents/skills/executor-selfhost-upgrade/references/deploy.md create mode 100644 .agents/skills/executor-selfhost-upgrade/references/rebase.md create mode 100644 .agents/skills/executor-selfhost-upgrade/references/topology.md create mode 100644 .agents/skills/executor-selfhost-upgrade/scripts/preflight.ts diff --git a/.agents/skills/executor-selfhost-upgrade/SKILL.md b/.agents/skills/executor-selfhost-upgrade/SKILL.md new file mode 100644 index 0000000000..36793d28da --- /dev/null +++ b/.agents/skills/executor-selfhost-upgrade/SKILL.md @@ -0,0 +1,65 @@ +--- +name: executor-selfhost-upgrade +description: Rebase the Executor dev fork onto current upstream/main, promote the reviewed history safely, update the linked selfhost checkout, reconcile the AryaLabsHQ Cloudflare host, and deploy with explicit production gates. Use for full upstream-to-executor.arya.sh upgrades, not ordinary feature PRs. +--- + +# Executor selfhost upgrade + +Upgrade the fork and hosted instance without conflating their three repositories or silently widening authorization. + +## Choose the phase + +Start in the least-mutating phase that satisfies the request: + +- **Inspect** is read-only. Refresh remote-tracking refs when current upstream state is requested, run the preflight helper, and report drift. +- **Prepare** creates an isolated rebase candidate, resolves supported conflicts, and validates it. It does not change `dev`, selfhost, or production. +- **Promote** rewrites `dev` to the reviewed candidate. Require explicit confirmation immediately before the exact force-with-lease. +- **Deploy** updates the detached selfhost checkout, reconciles the host, handles required migrations, deploys, and verifies live. Require explicit confirmation immediately before migrations or production deployment. + +An approval for one phase does not authorize a later phase. A request to discuss or inspect is not permission to mutate refs, files, provider state, or production. + +## Required reading + +Before any phase, read [references/topology.md](references/topology.md). + +- For Prepare or Promote, also read [references/rebase.md](references/rebase.md). +- For Deploy, also read [references/deploy.md](references/deploy.md). + +## Preflight + +From any checkout in the Executor worktree family, run: + +```bash +bun .agents/skills/executor-selfhost-upgrade/scripts/preflight.ts +``` + +Use `--json` when another tool will consume the result. The helper is deliberately read-only: it does not fetch, switch branches, install packages, migrate data, or deploy. + +Treat every reported blocker as a stop condition. Resolve stale remote-tracking refs by fetching deliberately, then rerun preflight. Do not reinterpret a dirty checkout as safe. + +## Shared invariants + +- Keep the canonical `dev` checkout, detached selfhost checkout, and hosted-instance repository distinct. +- Use exact SHAs in reports and mutation commands. Re-resolve them immediately before promotion and deployment. +- Use `wt new` for isolated Executor worktrees. +- Preserve unrelated worktrees and user changes. +- Never resolve semantic conflicts with blanket `ours` or `theirs` choices. +- Never hand-merge `bun.lock`; regenerate it with the repository's Bun version after resolving manifests. +- Do not overwrite the hosted instance with `apps/host-cloudflare`. Reconcile intentional host composition instead. +- Use Wrangler for Cloudflare migrations, deployment, version inspection, and rollback. +- Use Executor only through MCP for live service inspection and verification. Never use the Executor CLI. +- Record the old `dev` SHA, selfhost SHA, hosted-instance SHA, and deployed Worker version before their respective mutations. + +## Checkpoints and stopping conditions + +Stop and ask for direction when: + +- a conflict changes public contracts, storage semantics, authentication, execution runtime behavior, or plugin composition; +- any owned checkout is dirty; +- `origin/dev` changes after the promotion lease is captured; +- a required hosted-instance change cannot be represented by a separate reviewed host PR; +- migration ordering, backup coverage, or reversibility is unclear; +- a required verification gate fails; +- live validation shows a regression or cannot reach the authenticated service. + +Do not merge pull requests. A rebase candidate PR is review-only; promotion uses the exact guarded ref update described in the rebase reference. A host PR is a separate repository change and blocks deployment until reviewed and landed. diff --git a/.agents/skills/executor-selfhost-upgrade/references/deploy.md b/.agents/skills/executor-selfhost-upgrade/references/deploy.md new file mode 100644 index 0000000000..b5038d2d62 --- /dev/null +++ b/.agents/skills/executor-selfhost-upgrade/references/deploy.md @@ -0,0 +1,82 @@ +# Selfhost update and deployment + +Use this procedure only after the promoted `origin/dev` SHA is verified. + +## Update linked package sources + +1. Record the current detached selfhost SHA. +2. Require the selfhost checkout to be clean and detached. +3. Fetch the promoted ref and detach selfhost at its exact SHA. +4. Install with the checked-in lockfile rather than allowing dependency drift. +5. Build generated package output that the host cannot consume as TypeScript source, including `@executor-js/vite-plugin` when present. +6. Verify every hosted `link:@executor-js/*` dependency resolves inside the selfhost worktree and that no expected link is missing. + +Do not make a repair commit directly in the detached selfhost checkout. Package fixes go through the fork history workflow. + +## Reconcile the hosted instance + +Compare the old and new fork host contract with `/Users/aryasaatvik/Developer/AryaLabsHQ/executor`. Do not copy the upstream app over the hosted repository. + +If no host change is required, record the comparison and continue. If a change is required: + +1. Create an isolated branch in the host repository. +2. Implement only required host composition changes. +3. Run the host gates and open a separate conventional PR. +4. Stop until that PR is reviewed and landed. Never deploy uncommitted host changes. + +Preserve intentional hosted behavior including access authentication, service-token actor aliases, QuickJS execution when no Dynamic Worker binding is configured, D1/R2 state, semantic-search indexing, queues, observability, and custom plugin composition unless the upgrade explicitly replaces it. + +## Migration gate + +Diff the previously deployed package and host SHAs against the proposed deployment for storage schemas, migrations, bindings, queues, indexes, and persistent-object contracts. + +If no migration is required, say so explicitly. If one is required, prepare a concrete plan containing: + +- affected D1 tables and migration identifiers; +- pre-migration exports or backups; +- R2 objects or prefixes involved; +- Durable Object compatibility implications; +- queue/index rebuild or reconciliation steps; +- verification queries and rollback limits. + +Require confirmation before executing remote migrations. Use the hosted repository's checked-in migration command and Wrangler configuration. Do not enable per-request schema setup to substitute for a planned one-time migration. + +## Host validation + +From the clean hosted repository, run the relevant checked-in commands, normally including: + +```bash +bun install --frozen-lockfile +bun run typecheck +bun run lint +bun run format:check +bun run build +bunx wrangler deploy --dry-run +``` + +Use the actual package scripts when names differ. A package-only change still requires a host build because linked TypeScript source is compiled at deployment time. + +## Deploy and verify + +Immediately before production deployment: + +1. Confirm the hosted repository is clean and at the reviewed SHA. +2. Confirm selfhost is detached at the promoted `dev` SHA. +3. Capture the current Worker version and relevant binding inventory. +4. Present the migration and deployment commands, expected versions, and rollback target. +5. Obtain explicit deployment confirmation. + +Deploy through the hosted repository's Wrangler-backed command. Capture the resulting Worker version and read it back from Cloudflare. + +Validate the authenticated customer path through Executor MCP, not the Executor CLI: + +- AI Search/index health; +- representative semantic searches, including namespace-filtered searches; +- exact tool description; +- one safe read-only live tool execution; +- service-token actor attribution when relevant; +- Worker errors, latency, and telemetry around the validation window. + +Browser validation may supplement MCP validation for user-visible changes but does not replace it. + +If live validation fails, stop further mutation, gather Worker logs and the exact failing request, and compare with the recorded old versions. Do not automatically roll back unless rollback was explicitly authorized. Use Wrangler rollback only against the recorded previous Worker version. diff --git a/.agents/skills/executor-selfhost-upgrade/references/rebase.md b/.agents/skills/executor-selfhost-upgrade/references/rebase.md new file mode 100644 index 0000000000..3bb24c9ab6 --- /dev/null +++ b/.agents/skills/executor-selfhost-upgrade/references/rebase.md @@ -0,0 +1,75 @@ +# Rebase and promotion + +Use this procedure for the Prepare and Promote phases. + +## Prepare an isolated candidate + +1. Fetch current `origin/dev` and `upstream/main` and record both full SHAs. +2. Require the canonical `dev`, selfhost, upstream, and hosted-instance checkouts to be clean. +3. Create a dedicated candidate from the captured dev SHA with `wt new`. Do not rebase the canonical `dev` checkout. +4. Run `bun run bootstrap` in the fresh candidate as required by the repository. +5. Rebase the fork series onto the captured upstream SHA with rerere enabled for the rebase commands. Do not enable or change global Git configuration. + +The candidate branch name should identify the upstream date or SHA, for example `sync/upstream-2026-08-23`. + +## Conflict policy + +Resolve by ownership and intent, commit by commit: + +- Preserve additive fork packages and plugins unless upstream now supplies an equivalent that is intentionally adopted. +- Prefer current upstream framework contracts, provider migrations, Durable Object/session behavior, and supported runtime seams. +- Reapply fork behavior through the new upstream seam instead of restoring removed upstream structure. +- Preserve service-token aliases, execution history, semantic search, host-specific OAuth health, and branded UI only where they remain deliberate fork features. +- For delete/modify and rename conflicts, inspect the upstream replacement and callers before choosing a destination. +- Resolve package manifests first, then regenerate `bun.lock`. Never edit lockfile conflict markers by hand. +- Search the candidate for conflict markers and inspect commits that became empty or changed scope. + +Stop for an architectural decision when both sides deliberately changed the same contract or when a resolution would alter storage, authentication, execution isolation, or public APIs. + +## Validate the candidate + +Run focused tests after each meaningful conflict cluster. When the tree is settled, run the merge-ready repository gates: + +```bash +bun run format:check +bun run lint +bun run typecheck +bun run test +``` + +Do not silently omit a failing suite. Report an environmental limitation separately from a product failure and get approval for any exception. + +Prepare a review report containing: + +- captured old dev and upstream SHAs; +- new candidate SHA and range-diff; +- empty, dropped, or materially rewritten commits; +- conflict files and resolution rationale; +- upstream-only changes that affect the fork; +- full gate results; +- expected host-contract changes and migration risk. + +## Review-only candidate PR + +When review on GitHub is requested, push the candidate and open a PR against `dev` that explicitly says it is review-only and must not be merged. GitHub merging would retain the old base history instead of performing the intended ref promotion. + +Required checks and reviews must apply to the exact current candidate head. A later force-push invalidates earlier evidence. + +## Promote with an exact lease + +Promotion is a separate destructive phase. Immediately before it: + +1. Fetch `origin/dev` again. +2. Confirm it still equals the captured old dev SHA. Stop if it moved. +3. Create a dated backup ref containing the old dev SHA and push that backup when authorized. +4. Verify the candidate tree and reviewed tree are identical. +5. Update `dev` with an explicit lease tied to the old SHA: + +```bash +git push --force-with-lease=refs/heads/dev: origin :refs/heads/dev +``` + +6. Fetch and verify `origin/dev` is exactly the candidate SHA. +7. Update the canonical checkout without destructive reset commands. + +Report the backup ref, old SHA, new SHA, and remote readback. Do not merge or close the review-only PR unless separately authorized. diff --git a/.agents/skills/executor-selfhost-upgrade/references/topology.md b/.agents/skills/executor-selfhost-upgrade/references/topology.md new file mode 100644 index 0000000000..529d715500 --- /dev/null +++ b/.agents/skills/executor-selfhost-upgrade/references/topology.md @@ -0,0 +1,48 @@ +# Executor upgrade topology + +Use live Git and filesystem state as the authority. Paths below are defaults for Saatvik's current setup; the preflight helper accepts overrides. + +## Checkouts + +| Role | Default path | Ownership | +| ----------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Fork history | `/Users/aryasaatvik/Developer/executor` | `dev`, Git history, ordinary feature PRs, and upstream-rebase promotion | +| Linked packages | `/Users/aryasaatvik/Developer/executor-worktrees/selfhost` | Detached checkout whose `packages/**/src` files are consumed by the hosted instance through `bun link` symlinks | +| Upstream snapshot | `/Users/aryasaatvik/Developer/executor-worktrees/upstream` | Clean detached view of the exact fetched `upstream/main` SHA | +| Hosted instance | `/Users/aryasaatvik/Developer/AryaLabsHQ/executor` | Cloudflare composition deployed at `executor.arya.sh` | + +The hosted repository consumes `@executor-js/*` as TypeScript source through links into the selfhost checkout. A successful build in the fork history checkout does not prove that those links or the hosted composition are correct. + +## Source-of-truth rules + +- Package and plugin changes belong in the Executor monorepo, initially in an isolated candidate and ultimately on `dev`. +- Host-only bindings, routes, secrets wiring, migrations, queues, and deployment configuration belong in `AryaLabsHQ/executor`. +- The selfhost worktree is an exact detached package source. Do not develop unique changes there during an upgrade. +- The upstream worktree is an inspection surface. Do not create fork commits there. + +## Read-only refresh + +When current upstream state is required: + +1. Inspect all worktrees and the four owned checkouts. +2. Fetch `origin` and `upstream`. If local tag conflicts would make an ordinary fetch unsafe, fetch branch tips with `--no-tags` instead of modifying tags. +3. Capture `origin/dev` and `upstream/main` as full SHAs. +4. Run preflight before changing either detached worktree. +5. If the upstream worktree is clean, detach it at the captured upstream SHA. +6. Rerun preflight and preserve its output in the upgrade report. + +Refreshing remote-tracking refs is not permission to rebase, promote, migrate, or deploy. + +## Hosted-instance contract + +Treat `apps/host-cloudflare` as a reference composition, not a directory to copy. Compare it with the hosted repository for changes in: + +- environment and binding contracts; +- Durable Object and hibernation/session wiring; +- execution runtime and QuickJS preload behavior; +- MCP transport and authentication; +- plugin construction and provider presets; +- storage schema and migration machinery; +- queues, AI Search, R2, D1, and observability. + +Classify each difference as upstream-required, fork-required, host-specific, or stale. Only upstream-required changes should be ported automatically into a host change proposal. diff --git a/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts b/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts new file mode 100644 index 0000000000..d6dbdcfea7 --- /dev/null +++ b/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts @@ -0,0 +1,361 @@ +#!/usr/bin/env bun + +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; + +interface CommandResult { + readonly ok: boolean; + readonly stdout: string; + readonly stderr: string; +} + +interface Worktree { + readonly path: string; + readonly branch: string | null; + readonly detached: boolean; +} + +interface RepoState { + readonly path: string; + readonly head: string | null; + readonly branch: string | null; + readonly clean: boolean | null; +} + +interface Check { + readonly severity: "blocker" | "warning" | "ok"; + readonly name: string; + readonly detail: string; +} + +interface Options { + readonly json: boolean; + readonly main?: string; + readonly selfhost?: string; + readonly upstream?: string; + readonly host?: string; +} + +interface PackageJson { + readonly dependencies?: Readonly>; + readonly devDependencies?: Readonly>; +} + +const decoder = new TextDecoder(); + +const run = (command: readonly string[], cwd?: string): CommandResult => { + const result = Bun.spawnSync([...command], { + cwd, + stdout: "pipe", + stderr: "pipe", + }); + + return { + ok: result.exitCode === 0, + stdout: decoder.decode(result.stdout).trim(), + stderr: decoder.decode(result.stderr).trim(), + }; +}; + +const git = (repository: string, ...args: readonly string[]) => + run(["git", "-C", repository, ...args]); + +const parseOptions = (args: readonly string[]): Options => { + const values: { + json: boolean; + main?: string; + selfhost?: string; + upstream?: string; + host?: string; + } = { json: false }; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + if (argument === "--json") { + values.json = true; + continue; + } + + const option = argument.replace(/^--/, "") as keyof Omit; + if (!["main", "selfhost", "upstream", "host"].includes(option)) { + process.stderr.write(`Unknown option: ${argument}\n`); + process.exit(2); + } + + const value = args[index + 1]; + if (!value) { + process.stderr.write(`Missing value for ${argument}\n`); + process.exit(2); + } + values[option] = resolve(value); + index += 1; + } + + return values; +}; + +const parseWorktrees = (output: string): readonly Worktree[] => + output.split(/\n\n+/).flatMap((record): readonly Worktree[] => { + const lines = record.split("\n"); + const path = lines.find((line) => line.startsWith("worktree "))?.slice(9); + if (!path) return []; + const branch = lines.find((line) => line.startsWith("branch "))?.slice(7) ?? null; + return [ + { + path, + branch, + detached: lines.includes("detached"), + }, + ]; + }); + +const resolveCanonicalMain = (override?: string): string => { + if (override) return override; + + const root = run(["git", "rev-parse", "--show-toplevel"]); + if (!root.ok) { + process.stderr.write("Run preflight from an Executor Git checkout or pass --main.\n"); + process.exit(2); + } + + const common = git(root.stdout, "rev-parse", "--path-format=absolute", "--git-common-dir"); + if (!common.ok) return root.stdout; + return basename(common.stdout) === ".git" ? dirname(common.stdout) : root.stdout; +}; + +const repoState = (path: string): RepoState => { + if (!existsSync(path)) { + return { path, head: null, branch: null, clean: null }; + } + + const head = git(path, "rev-parse", "HEAD"); + const branch = git(path, "symbolic-ref", "--quiet", "--short", "HEAD"); + const status = git(path, "status", "--porcelain"); + return { + path, + head: head.ok ? head.stdout : null, + branch: branch.ok ? branch.stdout : null, + clean: status.ok ? status.stdout.length === 0 : null, + }; +}; + +const addRepoChecks = ( + checks: Check[], + label: string, + state: RepoState, + expectedBranch: string | null, +) => { + if (state.head === null) { + checks.push({ severity: "blocker", name: `${label}.exists`, detail: state.path }); + return; + } + checks.push({ severity: "ok", name: `${label}.exists`, detail: state.path }); + + if (state.clean !== true) { + checks.push({ + severity: "blocker", + name: `${label}.clean`, + detail: state.clean === false ? "working tree has changes" : "unable to read status", + }); + } else { + checks.push({ severity: "ok", name: `${label}.clean`, detail: "clean" }); + } + + if (state.branch !== expectedBranch) { + checks.push({ + severity: "blocker", + name: `${label}.branch`, + detail: `expected ${expectedBranch ?? "detached HEAD"}, found ${state.branch ?? "detached HEAD"}`, + }); + } else { + checks.push({ + severity: "ok", + name: `${label}.branch`, + detail: expectedBranch ?? "detached HEAD", + }); + } +}; + +const expectedExecutorPackages = (hostPath: string): readonly string[] => { + const packagePath = join(hostPath, "package.json"); + if (!existsSync(packagePath)) return []; + + // oxlint-disable-next-line executor/no-json-parse -- boundary: package.json is a trusted local tooling manifest + const parsed = JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson; + return Object.entries({ ...parsed.dependencies, ...parsed.devDependencies }) + .filter(([name, value]) => name.startsWith("@executor-js/") && value.startsWith("link:")) + .map(([name]) => name) + .sort(); +}; + +const inspectLinks = (hostPath: string, selfhostPath: string) => { + const expected = expectedExecutorPackages(hostPath); + const missing: string[] = []; + const outsideSelfhost: { name: string; target: string }[] = []; + + for (const name of expected) { + const packageName = name.slice("@executor-js/".length); + const linkPath = join(hostPath, "node_modules", "@executor-js", packageName); + if (!existsSync(linkPath)) { + missing.push(name); + continue; + } + + const target = realpathSync(linkPath); + const relativeTarget = target.startsWith(`${selfhostPath}/`) || target === selfhostPath; + if (!relativeTarget) outsideSelfhost.push({ name, target }); + } + + return { expected, missing, outsideSelfhost }; +}; + +const options = parseOptions(process.argv.slice(2)); +const mainPath = resolveCanonicalMain(options.main); +const worktreeResult = git(mainPath, "worktree", "list", "--porcelain"); +if (!worktreeResult.ok) { + process.stderr.write(`${worktreeResult.stderr || "Unable to list Executor worktrees"}\n`); + process.exit(2); +} + +const worktrees = parseWorktrees(worktreeResult.stdout); +const findWorktree = (name: string) => + worktrees.find((worktree) => basename(worktree.path) === name)?.path; +const selfhostPath = + options.selfhost ?? + findWorktree("selfhost") ?? + join(dirname(mainPath), "executor-worktrees", "selfhost"); +const upstreamPath = + options.upstream ?? + findWorktree("upstream") ?? + join(dirname(mainPath), "executor-worktrees", "upstream"); +const hostPath = options.host ?? join(dirname(mainPath), "AryaLabsHQ", "executor"); + +const main = repoState(mainPath); +const selfhost = repoState(selfhostPath); +const upstream = repoState(upstreamPath); +const host = repoState(hostPath); +const checks: Check[] = []; + +addRepoChecks(checks, "main", main, "dev"); +addRepoChecks(checks, "selfhost", selfhost, null); +addRepoChecks(checks, "upstream", upstream, null); +addRepoChecks(checks, "host", host, "main"); + +const originDevResult = git(mainPath, "rev-parse", "refs/remotes/origin/dev"); +const upstreamMainResult = git(mainPath, "rev-parse", "refs/remotes/upstream/main"); +const originDev = originDevResult.ok ? originDevResult.stdout : null; +const upstreamMain = upstreamMainResult.ok ? upstreamMainResult.stdout : null; + +if (!originDev) + checks.push({ severity: "blocker", name: "refs.origin-dev", detail: "missing origin/dev" }); +if (!upstreamMain) + checks.push({ severity: "blocker", name: "refs.upstream-main", detail: "missing upstream/main" }); + +if (originDev && main.head !== originDev) { + checks.push({ + severity: "blocker", + name: "main.alignment", + detail: `HEAD ${main.head} != origin/dev ${originDev}`, + }); +} else if (originDev) { + checks.push({ severity: "ok", name: "main.alignment", detail: originDev }); +} + +if (originDev && selfhost.head !== originDev) { + checks.push({ + severity: "warning", + name: "selfhost.alignment", + detail: `HEAD ${selfhost.head} != origin/dev ${originDev}`, + }); +} else if (originDev) { + checks.push({ severity: "ok", name: "selfhost.alignment", detail: originDev }); +} + +if (upstreamMain && upstream.head !== upstreamMain) { + checks.push({ + severity: "warning", + name: "upstream.alignment", + detail: `HEAD ${upstream.head} != upstream/main ${upstreamMain}`, + }); +} else if (upstreamMain) { + checks.push({ severity: "ok", name: "upstream.alignment", detail: upstreamMain }); +} + +const divergenceResult = + originDev && upstreamMain + ? git(mainPath, "rev-list", "--left-right", "--count", `${upstreamMain}...${originDev}`) + : { ok: false, stdout: "", stderr: "missing refs" }; +const [upstreamOnly, forkOnly] = divergenceResult.ok + ? divergenceResult.stdout.split(/\s+/).map((value) => Number.parseInt(value, 10)) + : [null, null]; +const mergeBaseResult = + originDev && upstreamMain + ? git(mainPath, "merge-base", originDev, upstreamMain) + : { ok: false, stdout: "", stderr: "missing refs" }; + +const links = inspectLinks(hostPath, selfhostPath); +if (links.expected.length === 0) { + checks.push({ + severity: "warning", + name: "links.expected", + detail: "no link:@executor-js/* dependencies found", + }); +} else { + checks.push({ + severity: "ok", + name: "links.expected", + detail: `${links.expected.length} linked packages declared`, + }); +} +if (links.missing.length > 0) { + checks.push({ severity: "blocker", name: "links.missing", detail: links.missing.join(", ") }); +} +if (links.outsideSelfhost.length > 0) { + checks.push({ + severity: "blocker", + name: "links.targets", + detail: links.outsideSelfhost.map(({ name, target }) => `${name} -> ${target}`).join(", "), + }); +} else if (links.expected.length > 0) { + checks.push({ + severity: "ok", + name: "links.targets", + detail: "all linked packages resolve inside selfhost", + }); +} + +const blockers = checks.filter((check) => check.severity === "blocker"); +const warnings = checks.filter((check) => check.severity === "warning"); +const report = { + status: blockers.length === 0 ? "ready" : "blocked", + generatedAt: new Date().toISOString(), + note: "Read-only local inspection. Remote-tracking refs were not fetched.", + repositories: { main, selfhost, upstream, host }, + refs: { + originDev, + upstreamMain, + mergeBase: mergeBaseResult.ok ? mergeBaseResult.stdout : null, + divergence: { upstreamOnly, forkOnly }, + }, + links, + checks, +}; + +if (options.json) { + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +} else { + process.stdout.write(`Executor selfhost upgrade preflight: ${report.status.toUpperCase()}\n`); + process.stdout.write(`origin/dev: ${originDev ?? "missing"}\n`); + process.stdout.write(`upstream/main: ${upstreamMain ?? "missing"}\n`); + process.stdout.write( + `divergence: ${upstreamOnly ?? "?"} upstream-only, ${forkOnly ?? "?"} fork-only\n`, + ); + process.stdout.write( + `linked source: ${links.expected.length} declared, ${links.missing.length} missing, ${links.outsideSelfhost.length} outside selfhost\n`, + ); + for (const check of [...blockers, ...warnings]) { + process.stdout.write(`${check.severity.toUpperCase()}: ${check.name}: ${check.detail}\n`); + } +} + +process.exit(blockers.length === 0 ? 0 : 1); From 70a16c4bb445f12506f4961f4b6fe560b1326b34 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Sun, 23 Aug 2026 23:20:33 +0530 Subject: [PATCH 2/2] fix(skills): make selfhost preflight portable (greptile) --- .../skills/executor-selfhost-upgrade/SKILL.md | 2 +- .../references/deploy.md | 2 +- .../references/topology.md | 18 +++++++++--------- .../scripts/preflight.ts | 16 ++++++++++++---- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.agents/skills/executor-selfhost-upgrade/SKILL.md b/.agents/skills/executor-selfhost-upgrade/SKILL.md index 36793d28da..8b697aaef7 100644 --- a/.agents/skills/executor-selfhost-upgrade/SKILL.md +++ b/.agents/skills/executor-selfhost-upgrade/SKILL.md @@ -1,6 +1,6 @@ --- name: executor-selfhost-upgrade -description: Rebase the Executor dev fork onto current upstream/main, promote the reviewed history safely, update the linked selfhost checkout, reconcile the AryaLabsHQ Cloudflare host, and deploy with explicit production gates. Use for full upstream-to-executor.arya.sh upgrades, not ordinary feature PRs. +description: Rebase the Executor dev fork onto current upstream/main, promote the reviewed history safely, update the linked selfhost checkout, reconcile its Cloudflare host, and deploy with explicit production gates. Use for full upstream-to-production upgrades, not ordinary feature PRs. --- # Executor selfhost upgrade diff --git a/.agents/skills/executor-selfhost-upgrade/references/deploy.md b/.agents/skills/executor-selfhost-upgrade/references/deploy.md index b5038d2d62..a5995cfa8e 100644 --- a/.agents/skills/executor-selfhost-upgrade/references/deploy.md +++ b/.agents/skills/executor-selfhost-upgrade/references/deploy.md @@ -15,7 +15,7 @@ Do not make a repair commit directly in the detached selfhost checkout. Package ## Reconcile the hosted instance -Compare the old and new fork host contract with `/Users/aryasaatvik/Developer/AryaLabsHQ/executor`. Do not copy the upstream app over the hosted repository. +Compare the old and new fork host contract with the hosted checkout selected by `--host` or `EXECUTOR_HOST_CHECKOUT`. Do not copy the upstream app over the hosted repository. If no host change is required, record the comparison and continue. If a change is required: diff --git a/.agents/skills/executor-selfhost-upgrade/references/topology.md b/.agents/skills/executor-selfhost-upgrade/references/topology.md index 529d715500..1a2973e31e 100644 --- a/.agents/skills/executor-selfhost-upgrade/references/topology.md +++ b/.agents/skills/executor-selfhost-upgrade/references/topology.md @@ -1,22 +1,22 @@ # Executor upgrade topology -Use live Git and filesystem state as the authority. Paths below are defaults for Saatvik's current setup; the preflight helper accepts overrides. +Use live Git and filesystem state as the authority. The preflight helper discovers the Executor worktree family and accepts explicit path overrides. ## Checkouts -| Role | Default path | Ownership | -| ----------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| Fork history | `/Users/aryasaatvik/Developer/executor` | `dev`, Git history, ordinary feature PRs, and upstream-rebase promotion | -| Linked packages | `/Users/aryasaatvik/Developer/executor-worktrees/selfhost` | Detached checkout whose `packages/**/src` files are consumed by the hosted instance through `bun link` symlinks | -| Upstream snapshot | `/Users/aryasaatvik/Developer/executor-worktrees/upstream` | Clean detached view of the exact fetched `upstream/main` SHA | -| Hosted instance | `/Users/aryasaatvik/Developer/AryaLabsHQ/executor` | Cloudflare composition deployed at `executor.arya.sh` | +| Role | Discovery or override | Ownership | +| ----------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | +| Fork history | Worktree on `dev`, or `--main` | Git history, ordinary feature PRs, and upstream-rebase promotion | +| Linked packages | Worktree named `selfhost`, or `--selfhost` | Detached checkout whose `packages/**/src` files are consumed by the hosted instance through `bun link` symlinks | +| Upstream snapshot | Worktree named `upstream`, or `--upstream` | Clean detached view of the exact fetched `upstream/main` SHA | +| Hosted instance | `EXECUTOR_HOST_CHECKOUT`, or `--host` | Cloudflare composition deployed for this fork | The hosted repository consumes `@executor-js/*` as TypeScript source through links into the selfhost checkout. A successful build in the fork history checkout does not prove that those links or the hosted composition are correct. ## Source-of-truth rules - Package and plugin changes belong in the Executor monorepo, initially in an isolated candidate and ultimately on `dev`. -- Host-only bindings, routes, secrets wiring, migrations, queues, and deployment configuration belong in `AryaLabsHQ/executor`. +- Host-only bindings, routes, secrets wiring, migrations, queues, and deployment configuration belong in the configured hosted repository. - The selfhost worktree is an exact detached package source. Do not develop unique changes there during an upgrade. - The upstream worktree is an inspection surface. Do not create fork commits there. @@ -35,7 +35,7 @@ Refreshing remote-tracking refs is not permission to rebase, promote, migrate, o ## Hosted-instance contract -Treat `apps/host-cloudflare` as a reference composition, not a directory to copy. Compare it with the hosted repository for changes in: +Treat `apps/host-cloudflare` as a reference composition, not a directory to copy. Compare it with the configured hosted repository for changes in: - environment and binding contracts; - Durable Object and hibernation/session wiring; diff --git a/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts b/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts index d6dbdcfea7..55e7a39c23 100644 --- a/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts +++ b/.agents/skills/executor-selfhost-upgrade/scripts/preflight.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { existsSync, readFileSync, realpathSync } from "node:fs"; -import { basename, dirname, join, resolve } from "node:path"; +import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; interface CommandResult { readonly ok: boolean; @@ -192,6 +192,9 @@ const inspectLinks = (hostPath: string, selfhostPath: string) => { const expected = expectedExecutorPackages(hostPath); const missing: string[] = []; const outsideSelfhost: { name: string; target: string }[] = []; + const canonicalSelfhostPath = existsSync(selfhostPath) + ? realpathSync(selfhostPath) + : selfhostPath; for (const name of expected) { const packageName = name.slice("@executor-js/".length); @@ -202,8 +205,10 @@ const inspectLinks = (hostPath: string, selfhostPath: string) => { } const target = realpathSync(linkPath); - const relativeTarget = target.startsWith(`${selfhostPath}/`) || target === selfhostPath; - if (!relativeTarget) outsideSelfhost.push({ name, target }); + const relativeTarget = relative(canonicalSelfhostPath, target); + const isInsideSelfhost = + relativeTarget === "" || (!relativeTarget.startsWith("..") && !isAbsolute(relativeTarget)); + if (!isInsideSelfhost) outsideSelfhost.push({ name, target }); } return { expected, missing, outsideSelfhost }; @@ -228,7 +233,10 @@ const upstreamPath = options.upstream ?? findWorktree("upstream") ?? join(dirname(mainPath), "executor-worktrees", "upstream"); -const hostPath = options.host ?? join(dirname(mainPath), "AryaLabsHQ", "executor"); +const configuredHostPath = options.host ?? process.env.EXECUTOR_HOST_CHECKOUT; +const hostPath = configuredHostPath + ? resolve(configuredHostPath) + : join(dirname(mainPath), "executor-host"); const main = repoState(mainPath); const selfhost = repoState(selfhostPath);