From f244d1b21a4acb88be8493fc8f0bb15a938c3a8c Mon Sep 17 00:00:00 2001 From: Zheng Weihao Date: Thu, 13 Aug 2026 19:37:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20read=20paths=20never=20fetch=20=E2=80=94?= =?UTF-8?q?=20info=20and=20session-start=20hook=20go=20purely=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #29's touchpoint fetch made every `orbit info` and every session start with worktrees pay N serial remote round-trips (default branch + each tracked branch, one fetch each), multiplying with scoped-branch residue; measured baselines: `orbit info` 4.83s wall / ~0.4s CPU on a repo with 4 tracked branches, 19.7s on a pool carrying more scoped-branch residue, `context --startup` 24.5s with two repos added - ruling: without an async daemon, auto-fetch on a main path taxes a synchronous caller for advisory freshness — low friction outranks it (cf. VSCode's background autofetch never blocking startup); auto-fetch may return only off the main path - info and the context --startup reignite block drop the touchpoint fetch and keep config maintenance (local writes); remoteAhead now reads last-fetched refs, refreshed by sync / prune or the user's own fetch/pull; fetching touchpoints narrow to sync / prune - skill contract (SKILL.md + CONSTRAINTS.md): steps 3/5 state the last-fetched-refs semantics; cold-start sync becomes a two-trigger rule (remoteAhead > 0, or a stale last fetch when the task depends on fresh code) - spec-docs, USAGE, ROADMAP, CHANGELOG and the two info-fetch bats cases sync to the new contract; full suite 367/367 on latest main Signed-off-by: Zheng Weihao --- CHANGELOG.md | 3 ++- ROADMAP.md | 2 +- USAGE.md | 2 +- docs/spec-commands.md | 14 +++++++------- docs/spec-hooks.md | 5 +++-- docs/spec-knowledge.md | 9 ++++----- docs/spec-warnings.md | 2 +- docs/spec-worktree.md | 2 +- orbit.sh | 26 +++++++++++++++----------- skills/CONSTRAINTS.md | 4 ++-- skills/orbit/SKILL.md | 4 ++-- tests/12_context.bats | 4 ++-- tests/18_sync.bats | 19 ++++++++++++++----- 13 files changed, 55 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6515c39..cbd42cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma - **BREAKING:** `prune` left the skill's action surface — agents report the need, humans run it. - **BREAKING:** `prune --verify` removed. PR evidence is now used automatically whenever the workspace recorded `pr.url` entries and `gh` is available — no flag, and no `gh` call at all when nothing was recorded. The old flag gated a per-workspace "all PRs merged" check that force-deleted every branch in the workspace; verdicts are now per-branch, and a merged PR only clears the branch it covers — [`docs/spec-lifecycle.md`](docs/spec-lifecycle.md) → Prune Safety Guards → Branch Verdicts. - Prune's messages changed — stderr diagnostics and the stdout report shape alike (worktree counts, residue groups, closing block) — contract in [`docs/spec-warnings.md`](docs/spec-warnings.md) → Refusals and skips. -- **BREAKING:** pool fetch config is now orbit-maintained state. Pools converge to the full wildcard map `+refs/heads/*:refs/remotes/origin/*` plus `fetch.prune=true` — written at clone, re-asserted at every `sync`/`info`/session-start/`prune` touchpoint, **removing any other `remote.origin.fetch` mapping** (per-branch entries from older orbit versions, hand edits, emptied configs all converge; each convergence is reported on stderr as it happens). To keep a custom refspec layout, set `orbit config git.fetchAllBranches once` (write the baseline at birth, never correct it) or `never` (fully self-managed); `git.fetchPrune` takes the same three modes. Visible consequences: `@{u}` / `git status` upstream lines work for every branch with upstream config — scoped or raw, no registration step — and a push materializes the tracking ref on the spot; tracking refs self-clean as branches are deleted upstream; and a bare `git fetch` or `git pull` in any worktree now pulls every branch's objects (a one-time step onto full-clone footing on huge repos — orbit's own commands still fetch named branches only, so agent/headless paths never trigger it). +- **BREAKING:** pool fetch config is now orbit-maintained state. Pools converge to the full wildcard map `+refs/heads/*:refs/remotes/origin/*` plus `fetch.prune=true` — written at clone, re-asserted at every `sync`/`info`/session-start/`prune` touchpoint, **removing any other `remote.origin.fetch` mapping** (per-branch entries from older orbit versions, hand edits, emptied configs all converge; each convergence is reported on stderr as it happens). To keep a custom refspec layout, set `orbit config git.fetchAllBranches once` (write the baseline at birth, never correct it) or `never` (fully self-managed); `git.fetchPrune` takes the same three modes. Visible consequences: `@{u}` / `git status` upstream lines work for every branch with upstream config — scoped or raw, no registration step — and a push materializes the tracking ref on the spot; tracking refs self-clean as branches are deleted upstream (the cleaning runs at the fetching touchpoints — since narrowed to `sync` / `prune`); and a bare `git fetch` or `git pull` in any worktree now pulls every branch's objects (a one-time step onto full-clone footing on huge repos — orbit's own commands still fetch named branches only, so agent/headless paths never trigger it). - **BREAKING:** the `removed stale fetch refspec` / `added fetch refspec` / `would remove` / `would add` output lines are gone, and prune's `pool maintenance:` section no longer carries refspec content — the per-branch registration/reconciliation machinery was deleted outright. In their place, config convergence reports fixed per-key steering lines (`orbit: : fetch config converged: …` / `orbit: : push routing converged: …`) — contract in [`docs/spec-warnings.md`](docs/spec-warnings.md) → Config convergence lines. - **BREAKING:** `push.default=upstream` joins the maintained set — re-asserted at the same touchpoints (scoped local names differ from remote names, so git's default `simple` would refuse a bare `git push`); escape with `orbit config git.pushUpstreamByDefault once` or `never`. And `push.autoSetupRemote` is gone: its only beneficiary was raw-mode bare push, and raw mode's contract is plain git — a fresh raw branch now gets git's native "no upstream" error naming `git push -u`, while the documented explicit `git push origin ` needs no config at all. Scoped mode is unaffected (its upstream is wired by `switch` up front), and the git ≥ 2.37 soft gate drops with the key. - The prune recovery narrative is stated precisely everywhere (spec-lifecycle Recoverability, USAGE, spec-warnings): recovery is the report's `(was )` plus object survival until gc (`gc.pruneExpire`, two weeks by default) — a deleted branch's own reflog is deleted with it, so the 90-day reflog window never applied post-prune. `core.logAllRefUpdates` / `gc.*` are declared premise-only in the dependency closure — user policy orbit deliberately does not manage. @@ -62,6 +62,7 @@ Hardens the destructive surface: `prune` and `sync --force`/`--branch` become ma - `orbit clone` rejects a URL whose basename violates the pool-name contract (e.g. `.github`), pointing at `--name`. - Workspace/repo inference compares physical paths — commands work through symlinked cwds. - Session guard warns when process ancestry is unreadable, instead of failing silently open. +- `orbit info` and the `orbit context --startup` reignite block no longer fetch — read paths are purely local again (zero network): #29's touchpoint fetch made every `info` and every session start with worktrees pay N serial remote round-trips (the default branch plus each tracked branch, one fetch each), multiplying with pool residue. Ruling: without an async daemon, auto-fetch on a main path taxes a synchronous caller for advisory freshness, and low friction outranks it — auto-fetch may return only off the main path. Layer-1 staleness (`remoteAhead`) now reads last-fetched refs, refreshed by the remaining fetching touchpoints (`orbit sync` / `orbit prune`) or the user's own fetch/pull; fetch-config maintenance (a local write) stays. #### Removal diff --git a/ROADMAP.md b/ROADMAP.md index d37d9cf..7697677 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,7 @@ - `orbit context`: output complete context of current workspace (goal + repo brief/memo + background info), agent gets all needed context with one command after entering workspace - `orbit repos --json` output adds `memoBehind` field: agent can judge memo freshness at Level 0 - `orbit sync [repo...] [--force] [--branch ]`: sync pool repo to upstream latest (fast-forward / force reset / switch tracking branch) -- `orbit info` auto-fetch + two-layer staleness detection (remoteAhead / memoBehind) +- `orbit info` two-layer staleness detection (remoteAhead / memoBehind), reading last-fetched refs - `orbit doctor`: environment health check (git ≥2.20 / bash ≥3.2 / jq+gh optional dependencies / `.repos/` structural integrity diagnostics) - `orbit jot`: lightweight discovery queue (push/pop) for recording knowledge during work, aggregated into memo at natural breakpoints — reduces per-discovery cost from ~500 tokens to ~20 tokens - Deterministic session-start context injection: the plugin ships a `SessionStart` hook (startup / resume / compact) that runs `orbit context` to keep the agent aware it is inside a workspace — proven on **Claude Code** and **Qoder**, zero user effort (prompts to install the runtime when `orbit` is missing). diff --git a/USAGE.md b/USAGE.md index 49bb4dc..793054c 100644 --- a/USAGE.md +++ b/USAGE.md @@ -113,7 +113,7 @@ View detailed memo of a specific repo: orbit info backend ``` -`orbit info` automatically fetches upstream and detects two layers of staleness: +`orbit info` is purely local (never fetches) and detects two layers of staleness against last-fetched refs — refreshed by `orbit sync` / `orbit prune`, or your own fetch/pull: - **Pool behind upstream**: stderr shows `N new commits on origin/main`, run `orbit sync` to synchronize - **Memo behind pool HEAD**: stderr shows `memo is N commits behind HEAD`, consider updating memo diff --git a/docs/spec-commands.md b/docs/spec-commands.md index 90a4aae..b5515e8 100644 --- a/docs/spec-commands.md +++ b/docs/spec-commands.md @@ -223,23 +223,23 @@ Jot entries are real discoveries only — orbit writes no system placeholders in - **`--startup` (session-start block)**: used by the `SessionStart` hook and by the skill's startup detection; one call doubles as workspace detection (fails fast outside a workspace). Routes internally on worktree presence: empty → **prime**, populated → **reignite**. `--prime` / `--reignite` select the block explicitly (human/debug use; the skill exposes only `--startup` and the bare form). - **prime** (cold start, empty workspace): outputs `path` / `goal` (if set) / `state` (with a DONE banner when the workspace is `done`), then the **pool roster** — the "add menu": `available in pool (orbit add ...):` followed by ` ` per pool repo. Briefs resolve via the shared fallback (index → memo → README; [spec-metadata](spec-metadata.md) "Fallback Rules"), so a freshly cloned repo shows its README brief rather than `-`. The steering that `orbit repos` sends to stderr is inlined as trailing stdout sections — pool repos without a memo are listed under `no memo (write the card via orbit memo ; …):`, and repos whose memo exists but whose index entry drifted under `index out of sync (repair via orbit memo --refresh):` — because hook injection carries only stdout. Level-0 briefs only (no memo dumps) — full memo on demand via `orbit info ` (progressive loading). Empty pool prints `pool is empty — clone a repo into the pool first: orbit clone `. - - **reignite** (worktrees present): outputs the same durables, then per worktree repo: `--- (branch: ) ---` + two-layer staleness (memoBehind + remoteAhead — fetches like `orbit info`, advisory only; sync stays on-demand) + conditional `status:` line (jot count and level / commits behind upstream / `memo thin` / `memo over budget` — only repos with something pending) + small jot queues inlined (up to `jot.bufferSize` entries; larger queues collapse to a count + pop hint) + the full memo card. No roster, no source. + - **reignite** (worktrees present): outputs the same durables, then per worktree repo: `--- (branch: ) ---` + two-layer staleness (memoBehind + remoteAhead — purely local, read from last-fetched refs; advisory only, sync stays on-demand) + conditional `status:` line (jot count and level / commits behind upstream / `memo thin` / `memo over budget` — only repos with something pending) + small jot queues inlined (up to `jot.bufferSize` entries; larger queues collapse to a count + pop hint) + the full memo card. No roster, no source. - **Bare `orbit context` (cruise block)**: the in-session counterpart of the startup block, for compact/resume recovery — cheap durables (`path` / `goal` if set / `state`, with DONE banner) plus **conditional per-repo status** (only repos with pending jots, commits behind upstream, an untracked raw-mode branch, or a non-ok memo state), one line each: `repo : 3 jots (building) | 2 behind upstream | memo thin`. Never fetches (uses local refs; behind is measured against the worktree branch's `@{upstream}` — a raw-mode branch without upstream shows `no upstream (fetch origin to track)` instead of a count, so the agent knows to materialize the ref). Does **not** dump memos — pull a repo's memo on demand with `orbit info `. - **Single key query**: `orbit context ` outputs a single value and exits. Supported keys: `workspace` (name), `path` (absolute path), `goal`, `state` (active/done). - Must be executed within a workspace (inferred from CWD); errors at project root or outside a project — hooks treat failure as a silent no-op. - A key cannot be combined with `--startup`/`--prime`/`--reignite`; the three mode flags are mutually exclusive. - `--json`: structured output for debug/scripting (format in the "JSON Output Format" section below). The bare form's JSON mirrors the cruise block (no memos); `--startup --json` follows the same worktree routing — empty workspace emits the prime JSON shape, populated workspace emits the reignite JSON shape. -## orbit info Auto-fetch +## orbit info staleness -`orbit info ` maintains the repo's fetch config and fetches on execution (the touchpoint discipline of [`spec-worktree.md`](./spec-worktree.md#touchpoint-fetch-discipline): the default branch plus every tracked remote branch, one explicit refspec per fetch, never a bare fetch), enabling two-layer staleness detection: +`orbit info ` is a screening command and stays purely local — zero network. It maintains the repo's fetch config (a local write; [`spec-worktree.md`](./spec-worktree.md#config-ownership)) but never fetches, and detects two-layer staleness against last-fetched refs: -- **Layer 1 (pool ← upstream)**: After fetch, compares pool repo's local branch with `origin/`; if behind, outputs to stderr: `orbit: : N new commits on origin/` +- **Layer 1 (pool ← upstream)**: Compares pool repo's local branch with the last-fetched `origin/`; if behind, outputs to stderr: `orbit: : N new commits on origin/` - **Layer 2 (memo ← pool HEAD)**: Existing `orbit_staleness_check`, compares HEAD at memo write time with pool repo's current HEAD -Both layer warnings output to stderr, not affecting stdout. A config convergence (see [`spec-worktree.md`](./spec-worktree.md#config-ownership)) prints its `orbit: : … converged: …` line to stderr at the moment it happens. Fetch failures are silently tolerated (network unavailability does not block viewing); a remote-deleted tracked branch's ref is converged by the closing `git remote prune origin`, and only the default branch's failure can surface — as an explicit WARNING when the remote answers. +Both layer warnings output to stderr, not affecting stdout. A config convergence (see [`spec-worktree.md`](./spec-worktree.md#config-ownership)) prints its `orbit: : … converged: …` line to stderr at the moment it happens. -The same maintain-then-named-fetch pair runs per repo in the `orbit context --startup` reignite block (the session-start hook path). Tracking refs also materialize without any touchpoint: under the wildcard map a push updates `origin/` on the spot. `orbit repos` and bare `orbit context` (cruise block) do not fetch (stay purely local and fast), only show Layer 2. +Layer 1 is only as fresh as the last fetch. The fetching touchpoints are `orbit sync` and `orbit prune` (the touchpoint discipline of [`spec-worktree.md`](./spec-worktree.md#touchpoint-fetch-discipline)); the user's own `git fetch` / `git pull` refreshes the same refs. The `orbit context --startup` reignite block (the session-start hook path) maintains config per repo but never fetches either — session start is the session's main path and stays zero-network. Tracking refs also materialize without any touchpoint: under the wildcard map a push updates `origin/` on the spot. `orbit repos` and bare `orbit context` (cruise block) do not fetch either (stay purely local and fast), only show Layer 2. ## orbit sync @@ -497,6 +497,6 @@ Field descriptions: ``` - `worktrees`: array, one entry per worktree repo (unfiltered, unlike the bare form) - - `memoBehind` / `remoteAhead`: numbers, two-layer staleness distances (reignite fetches, like `orbit info`) + - `memoBehind` / `remoteAhead`: numbers, two-layer staleness distances (purely local — reignite reads last-fetched refs, like `orbit info`) - `jotEntries`: array of string, unpopped jot entries (full list; the markdown block inlines only up to `jot.bufferSize`) - `memo`: string, full memo content (`""` when no memo) diff --git a/docs/spec-hooks.md b/docs/spec-hooks.md index 94d1eb3..5b96342 100644 --- a/docs/spec-hooks.md +++ b/docs/spec-hooks.md @@ -44,8 +44,9 @@ - **startup** (`orbit context --startup`): cold start (empty workspace) → durables (`path` / `goal` / `state`, DONE banner when done) + the pool roster; populated workspace → durables + each repo's memo card + two-layer - staleness (memoBehind + remoteAhead — fetches like `orbit info`, advisory - only; sync stays on-demand) + conditional per-repo status + small jot + staleness (memoBehind + remoteAhead — purely local, read from + last-fetched refs, advisory only; the hook is the session's main path + and never fetches; sync stays on-demand) + conditional per-repo status + small jot queues inlined (up to `jot.bufferSize`). - **cruise** (bare `orbit context`): cheap durables + conditional per-repo status only (pending jots with level / commits behind upstream / diff --git a/docs/spec-knowledge.md b/docs/spec-knowledge.md index 6a2b5d1..4e9949d 100644 --- a/docs/spec-knowledge.md +++ b/docs/spec-knowledge.md @@ -10,7 +10,7 @@ Agents fetch repo information on demand, drilling down level by level, autonomou ``` Level 0: orbit repos <- global index: name + url + brief + memoBehind (purely local) -Level 1: orbit info <- per-repo markdown full text + fetch + two-layer staleness detection +Level 1: orbit info <- per-repo markdown full text + two-layer staleness detection (purely local) Level 2: orbit add <- worktree enters workspace Level 3: agent reads/writes code <- memo describes pool repo's stable branch (main branch) state ``` @@ -44,10 +44,9 @@ fi ### Layer 1: Pool Behind Upstream (`remoteAhead`) -When `orbit info` executes, it automatically fetches the repo's tracking branch, then compares the local branch with `origin/`: +`orbit info` never fetches — a screening command stays purely local. It compares the local branch with the **last-fetched** `origin/`; the refs are refreshed by the fetching touchpoints (`orbit sync` / `orbit prune`) or the user's own `git fetch` / `git pull`: ```bash -git -C .repos/backend fetch origin main 2>/dev/null local_head=$(git -C .repos/backend rev-parse refs/heads/main) remote_head=$(git -C .repos/backend rev-parse refs/remotes/origin/main) if [ "$local_head" != "$remote_head" ]; then @@ -59,10 +58,10 @@ fi ### Design Points - Both layers output hints to **stderr**, avoiding stdout pollution -- `orbit repos` only checks Layer 2 (purely local, fast); `orbit info` checks both layers (triggers fetch) +- `orbit repos` only checks Layer 2 (purely local, fast); `orbit info` checks both layers (purely local — Layer 1 reads last-fetched refs) - No hard thresholds are set; only distance numbers are reported, leaving the agent/human to decide whether to update - Layer 2 precondition: `head` field exists **and** per-repo `.md` file exists (no .md triggers the fallback path) -- Layer 1 silently skips on fetch failure (network unavailability does not block viewing) +- Layer 1 freshness is bounded by the last fetching touchpoint (network unavailability never blocks viewing) - `orbit clone` writes basic index fields (url + head) but does not generate per-repo `.md` ## Sync and Memo Cascading Relationship diff --git a/docs/spec-warnings.md b/docs/spec-warnings.md index aa658d9..cf4d1ff 100644 --- a/docs/spec-warnings.md +++ b/docs/spec-warnings.md @@ -80,7 +80,7 @@ orbit: : push routing converged: git config push.default upstream (stop co | Warning | Trigger (command + condition) | Source (function) | Named next action | Backstops | |:--------|:------------------------------|:------------------|:------------------|:----------| -| config converged (per key, forms above) | any fetching touchpoint (`orbit sync` / `orbit info` / `orbit context --startup` / `orbit prune`) that converges a non-standard pool config value, per managed key, under the default `always` maintenance mode | `orbit_maintain_pool_config` | the parenthesized opt-out: `config git.fetchAllBranches once` / `config git.fetchPrune once` / `config git.pushUpstreamByDefault once` stops future convergence; re-applying the custom value is the user's step | — (one-shot: fires only when a write actually happens; `once`/`never` pools are untouched and unreported) | +| config converged (per key, forms above) | any config touchpoint (`orbit sync` / `orbit info` / `orbit context --startup` / `orbit prune`) that converges a non-standard pool config value, per managed key, under the default `always` maintenance mode | `orbit_maintain_pool_config` | the parenthesized opt-out: `config git.fetchAllBranches once` / `config git.fetchPrune once` / `config git.pushUpstreamByDefault once` stops future convergence; re-applying the custom value is the user's step | — (one-shot: fires only when a write actually happens; `once`/`never` pools are untouched and unreported) | The lines deviate from the two-colon shape on purpose: their first duty is *reporting a mutation orbit just made* (silent config rewrites are forbidden), and the extra colons belong diff --git a/docs/spec-worktree.md b/docs/spec-worktree.md index 092ca3b..ecbd8e4 100644 --- a/docs/spec-worktree.md +++ b/docs/spec-worktree.md @@ -71,7 +71,7 @@ A config key orbit depends on is **managed state**, not a one-time write. Manage ## Touchpoint Fetch Discipline -Every orbit command that fetches — `orbit sync`, `orbit info`, the `orbit context --startup` reignite block, `orbit prune` — follows the same discipline: +Every orbit command that fetches — `orbit sync`, `orbit prune` — follows the same discipline. The list is deliberately short: auto-fetch without an async daemon taxes a synchronous caller for advisory freshness, so read paths never fetch — `orbit info` (a screening command) and the `orbit context --startup` reignite block (the session's main path; cf. VSCode's background autofetch, which never blocks startup either) stay purely local and read last-fetched refs. A future daemon that fetches off the main path is the only shape under which auto-fetch may return: - **Maintain, then fetch named branches only**: config maintenance ([Config Ownership](#config-ownership)) runs first; then the default branch plus every remote branch a local branch tracks (`branch.*.merge`, deduped — several local branches can share one upstream), one explicit `+refs/heads/:refs/remotes/origin/` refspec per fetch. Never a bare fetch: under the wildcard map it would pull every branch's objects, defeating the single-branch economy — that pull is reserved for the user's own `git fetch` / `git pull`, a one-time step onto full-clone footing. - **A tracked branch that does not fetch is tolerated**: git's `couldn't find remote ref` fatal is swallowed — the touchpoint must never leak the error this model exists to eliminate — and one closing `git remote prune origin` converges the refs of remote-deleted branches (online it cleans; offline it fails and deletes nothing). The prune runs only while the full wildcard is in place — a shape judgment, so a user-narrowed layout is exempt by construction. diff --git a/orbit.sh b/orbit.sh index 1f351d3..b1c1fde 100755 --- a/orbit.sh +++ b/orbit.sh @@ -432,7 +432,7 @@ orbit_maintain_pool_config() { fi } -# The touchpoint fetch discipline (sync / info / context --startup / prune): +# The touchpoint fetch discipline (sync / prune): # fetch the default branch plus every remote branch a local branch tracks # (branch.*.merge, deduped — several local branches can share one upstream), # one explicit refspec per fetch, never a bare fetch: under the wildcard map @@ -447,8 +447,8 @@ orbit_maintain_pool_config() { # user's territory). The default branch is the one loud failure: if it does # not fetch while the remote answers, the remote may have lost its default # branch — a repo-level event, reported. Returns non-zero iff the default -# branch would not fetch (sync treats this as fetch failure; advisory -# touchpoints ignore it). +# branch would not fetch (sync treats this as fetch failure; prune ignores +# it). orbit_touchpoint_fetch() { local repo="$1" default_br lb merge b failed=0 default_failed=0 default_br=$(orbit_default_branch "$repo" 2>/dev/null || true) @@ -2234,11 +2234,11 @@ orbit_info() { local default_branch default_branch=$(orbit_default_branch "$repo_dir" 2>/dev/null) || true if [ -n "$default_branch" ]; then - # Config maintenance + the touchpoint fetch discipline: named branches - # only (default + tracked, deduped), advisory — a fetch failure here - # never fails info. + # Config maintenance only — info is a screening command and stays purely + # local (zero network, like `orbit repos` and the cruise block). The + # staleness checks below read last-fetched refs, refreshed by the + # fetching touchpoints (sync / prune) or the user's own fetch/pull. orbit_maintain_pool_config "$repo_dir" "$root" 0 - orbit_touchpoint_fetch "$repo_dir" || true fi orbit_upstream_check "$repo_name" "$root" @@ -4038,7 +4038,9 @@ orbit_context_prime() { # reignite: session (re)start with worktrees present — rebuilds what the ignite # phase had read: each repo's memo card + two-layer staleness (memoBehind + -# remoteAhead; fetches like `orbit info`, advisory only — sync stays on-demand), +# remoteAhead; read from last-fetched refs — the startup hook is the session's +# main path and never fetches; without an async daemon, auto-fetch here would +# tax every session start for an advisory hint, and sync stays on-demand), # conditional per-repo status, and small jot queues inlined. No roster, no source. orbit_context_reignite() { local root="$1" ws="$2" ws_dir="$3" goal="$4" state_val="$5" index="$6" json_mode="$7" @@ -4063,8 +4065,11 @@ orbit_context_reignite() { default_branch=$(orbit_default_branch "$root/.repos/$name" 2>/dev/null || true) memo_behind=0 remote_ahead=0 if [ -n "$default_branch" ]; then + # Config maintenance only — the startup hook is the session's main + # path and stays purely local (zero network). remoteAhead below reads + # last-fetched refs, refreshed by sync / prune or the user's own + # fetch/pull. orbit_maintain_pool_config "$root/.repos/$name" "$root" 0 - orbit_touchpoint_fetch "$root/.repos/$name" || true local_head=$(git -C "$root/.repos/$name" rev-parse "refs/heads/$default_branch" 2>/dev/null || true) remote_head=$(git -C "$root/.repos/$name" rev-parse "refs/remotes/origin/$default_branch" 2>/dev/null || true) if [ -n "$local_head" ] && [ -n "$remote_head" ] && [ "$local_head" != "$remote_head" ]; then @@ -4130,12 +4135,11 @@ orbit_context_reignite() { name=$(basename "$d") branch=$(git -C "$d" branch --show-current 2>/dev/null || echo "detached") - # Two-layer staleness (fetch like orbit info; advisory only, no sync). + # Two-layer staleness (purely local, last-fetched refs; no sync). default_branch=$(orbit_default_branch "$root/.repos/$name" 2>/dev/null || true) memo_behind=0 remote_ahead=0 if [ -n "$default_branch" ]; then orbit_maintain_pool_config "$root/.repos/$name" "$root" 0 - orbit_touchpoint_fetch "$root/.repos/$name" || true local_head=$(git -C "$root/.repos/$name" rev-parse "refs/heads/$default_branch" 2>/dev/null || true) remote_head=$(git -C "$root/.repos/$name" rev-parse "refs/remotes/origin/$default_branch" 2>/dev/null || true) if [ -n "$local_head" ] && [ -n "$remote_head" ] && [ "$local_head" != "$remote_head" ]; then diff --git a/skills/CONSTRAINTS.md b/skills/CONSTRAINTS.md index fb7d916..3628c3c 100644 --- a/skills/CONSTRAINTS.md +++ b/skills/CONSTRAINTS.md @@ -224,12 +224,12 @@ Every skill must guide the agent to discover before acting: 1. `orbit context goal` — understand the workspace objective (skip it when a held block already carries the goal; bare `orbit goal` never reads: a TTY opens an editor, non-TTY sets from stdin) 2. `orbit repos` — screen: view available repos (name + url + brief), identify potentially relevant candidates. Skip when the startup block's pool roster already answers it (cold start); re-run to refresh when the pool may have changed or a needed field (URL) is beyond the brief -3. `orbit info ` — assess: read the memo card for candidate repos (roles: when/why to add; entry points: where to start), also detects upstream freshness and memo staleness +3. `orbit info ` — assess: read the memo card for candidate repos (roles: when/why to add; entry points: where to start), also detects upstream freshness (against last-fetched refs — info never fetches) and memo staleness - **README fallback = no memo.** When `orbit info` falls back to the README, no memo exists. The README is the repo's unprocessed façade, not decision context — it must not be treated as "enough" to skip `orbit add` or the step 7 exploration - Mid-work self-check: bare `orbit context` shows goal + per-repo status (jots / behind / memo state), not memos — it does not replace steps 1–3 4. Decide: based on info, determine whether to add — card answers your question (which repo, where to start) → don't add; need to grep source, trace call chains, modify code → `orbit add`; not in pool → `orbit clone` then add; only need docs → web search. A README fallback is **not** "sufficient" and never justifies "don't add" - **Task type does not exempt exploration.** Release, ops, and pure-research tasks explore first too — the default is not "editing code". If full source is genuinely not needed, the agent states that reason explicitly at this step rather than skipping exploration by default -5. **Cold-start sync** — if step 3 showed remoteAhead > 0, run `orbit sync ` now (before add). Agent hasn't started relying on the code yet, so sync cost is lowest. This ensures `orbit add` creates the worktree from the latest pool HEAD +5. **Cold-start sync** — sync the pool repo before add when either trigger holds: step 3 showed remoteAhead > 0, or the last sync/fetch is old and the task depends on fresh code (remoteAhead is measured against last-fetched refs — info/session start never fetch — so 0 proves "no drift since the last fetch", not "pool is current"). Run it now, before the agent starts relying on the code, so `orbit add` creates the worktree from the latest pool HEAD 6. `orbit add ` — bring into workspace only repos confirmed in step 4 as needing full source. Worktree starts from pool's current HEAD (latest after sync). `-s` suppresses the memo echo only when context is already held (from step 3 `orbit info`, the startup block, or a prior session). **Hard rule:** if step 3 showed **no memo** (README fallback), `-s` is forbidden — no memo means zero inherited context, so add without `-s` and explore in step 7 - **No/low-memo nudge at add:** when the added repo's memo is missing or thin, `orbit add` prints a one-shot stderr naming the scope to explore — explore and write the card before done. The skill must guide the agent to act on it in step 7 (the same state resurfaces via per-repo status in bare `orbit context` and at `orbit done`) 7. **Memo check** — if the startup block (hook-injected or self-run `orbit context --startup`) reported pending jots for this repo, pop them first (`orbit jot --pop`) and merge them into the same write. The startup moment is the discriminator: the session has not worked yet, so those entries are a prior session's by construction — their capturing context is gone, and this memo write is their only survival path. Same-session jots aggregate at wrap-up or the overflow warning (capture/aggregate split, [docs/spec-knowledge.md](../docs/spec-knowledge.md)). Then, based on staleness info from step 3 (recalculated after sync): diff --git a/skills/orbit/SKILL.md b/skills/orbit/SKILL.md index c804d79..977f0c9 100644 --- a/skills/orbit/SKILL.md +++ b/skills/orbit/SKILL.md @@ -67,12 +67,12 @@ These steps describe the work itself, independent of who performs it. Run them y 1. **Read goal first.** It's already in any block you hold (startup or cruise) — don't re-fetch. Run `orbit context goal` when you hold no block or the goal may have changed. 2. **Screen.** Cold-start startup blocks already carry the pool roster (name + brief) — screen from it. Otherwise (populated workspace, the pool may have changed, you need the URL) run `orbit repos`. Identify candidates relevant to your goal. -3. **Assess.** Run `orbit info ` for each candidate — the memo card: the repo's roles (when/why to add) and entry points (where to start). Also detects upstream freshness and memo staleness. +3. **Assess.** Run `orbit info ` for each candidate — the memo card: the repo's roles (when/why to add) and entry points (where to start). Also detects upstream freshness (against last-fetched refs — info never fetches) and memo staleness. - **README fallback = no memo.** When `orbit info` falls back to showing the README, it means **no memo exists**. The README is the repo's unprocessed façade, not decision context — never treat it as "enough" to justify skipping `orbit add` or the step 7 exploration. - **Mid-work self-check:** bare `orbit context` shows goal + per-repo status (jots / behind / memo state), not memos — it does not replace steps 1–3. 4. **Decide.** Based on info: memo card gives enough context (the repo's roles and entry points answer your need) → don't add. Need to grep source, trace call chains, or modify code → `orbit add`. Repo not in pool → `orbit clone` then add. Only need docs → web search. A README fallback (step 3) is **not** "enough context" — it never justifies "don't add". - **Task type doesn't exempt you from exploring.** Release, ops, and pure-research tasks explore first too — the default mental model is not "editing code". If you judge full source truly isn't needed, state that reason explicitly here rather than skipping exploration by default. -5. **Cold-start sync.** If step 3 showed remoteAhead > 0, run `orbit sync ` now — before add, while nothing depends on the old code yet — so the worktree starts from the latest pool HEAD. `sync` moves the pool repo only, never an existing worktree (catch one up with native git if wanted; scope note also in the command map). +5. **Cold-start sync.** Sync the pool repo before add when either trigger holds: step 3 showed remoteAhead > 0, or the last sync/fetch is old and the task depends on fresh code (remoteAhead is measured against last-fetched refs — info and session start never fetch — so 0 proves "no drift since the last fetch", not "pool is current"). Run it now, while nothing depends on the old code yet, so the worktree starts from the latest pool HEAD. `sync` moves the pool repo only, never an existing worktree (catch one up with native git if wanted; scope note also in the command map). 6. **Add repos.** Run `orbit add ` only for repos that need full source (from inside a workspace directory). Worktree starts from pool's current HEAD (latest after sync). Pass `-s` when you already hold enough context to justify the add — from `orbit info` in step 3, the memo surfaced in the startup block, or a prior session: `orbit add -s`. Plain `orbit add` (no `-s`) echoes the memo as a safety net — reach for it only when adding without that context; seeing the memo dump means you added blind and should confirm you actually need the full source. **Hard rule:** if step 3's `orbit info` showed **no memo** (README fallback), `-s` is forbidden — no memo means zero inherited context, so add without `-s` and explore in step 7. - **No/low-memo nudge at add.** When you add a repo whose memo is missing or thin, `orbit add` prints a one-shot stderr naming the scope to explore — explore and write the card before done. It is an *instruction to you*: act on it in step 7. The same state resurfaces via per-repo status in bare `orbit context` and at `orbit done`. 7. **Memo check.** If your startup block (hook-injected or your own `orbit context --startup`) reported pending jots for this repo, pop them first (`orbit jot --pop`) and fold them into a memo write this turn — the write below if it happens, a fold-only write otherwise. Then, based on staleness info from step 3 (recalculated after sync): diff --git a/tests/12_context.bats b/tests/12_context.bats index 6d23d98..f57ceb4 100644 --- a/tests/12_context.bats +++ b/tests/12_context.bats @@ -254,8 +254,8 @@ teardown() { # spot, before any touchpoint runs. git rev-parse --verify --quiet origin/feat-x >/dev/null - # The startup block fetches the named branches only: no registration - # output, no config change, @{u} resolves. + # The startup block never fetches (session start is the main path): no + # registration output, no fatal leak, no config change, @{u} resolves. run bash -c "cd '$proj/ws1/myrepo' && ORBIT_ROOT='$proj' bash '$ORBIT_CMD' context --startup 2>&1" [ "$status" -eq 0 ] refute_contains "$output" "fetch refspec" diff --git a/tests/18_sync.bats b/tests/18_sync.bats index 5e60308..7d609fd 100644 --- a/tests/18_sync.bats +++ b/tests/18_sync.bats @@ -274,9 +274,9 @@ _push_update_to() { ! assert_contains "$output" "untouched by sync" } -# --- info fetch behavior --- +# --- info behavior (purely local — no fetch touchpoint) --- -@test "info: shows upstream behind warning after fetch" { +@test "info: never fetches — upstream warning reflects last-fetched refs" { local proj="$SANDBOX/info-fetch" local remote="$REMOTES/info-fetch.git" clone_remote "$remote" @@ -295,8 +295,16 @@ _push_update_to() { ) rm -rf "$tmp" + # purely local: the push is invisible to info until some fetch refreshes + # the tracking refs local stderr_output stderr_output=$(cd "$proj" && orbit info myrepo 2>&1 >/dev/null || true) + refute_contains "$stderr_output" "new commits on origin/main" + + # after any fetch (a fetching touchpoint, or the user's own), info reads + # the refreshed refs + git -C "$proj/.repos/myrepo" fetch origin main >/dev/null 2>&1 + stderr_output=$(cd "$proj" && orbit info myrepo 2>&1 >/dev/null || true) assert_contains "$stderr_output" "1 new commits on origin/main" } @@ -529,7 +537,7 @@ _push_update_to() { [ "$output" = "+refs/heads/*:refs/remotes/origin/*" ] } -@test "info: a tracked branch the remote lost does not leak git's fatal" { +@test "info: purely local — a remote-lost tracked branch's stale ref stays put" { local proj="$SANDBOX/info-gone" clone_project "$proj" git -C "$proj/.repos/myrepo" branch demo main @@ -541,9 +549,10 @@ _push_update_to() { run bash -c "cd '$proj' && ORBIT_ROOT='$proj' bash '$ORBIT_CMD' info myrepo 2>&1" [ "$status" -eq 0 ] refute_contains "$output" "couldn't find remote ref" - # the touchpoint really ran: the wildcard-shaped prune converged the stale ref + # no fetch touchpoint here: converging the stale ref is the fetching + # touchpoints' business (the sync variant above), not a screening command's run git -C "$proj/.repos/myrepo" rev-parse --verify --quiet refs/remotes/origin/demo-gone - [ "$status" -ne 0 ] + [ "$status" -eq 0 ] } # --- Pool-wide scope: --branch is root-only ---