loopat 0.1.50 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopat",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "Self-hosted AI workspace built around context management — works solo, scales to teams",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/simpx/loopat",
@@ -453,8 +453,23 @@ async function ensureRepoMirror(user: string, name: string, sshCommand?: string)
453
453
  if (!spec?.git) return null
454
454
  const dir = personalRepoCacheDir(user, name)
455
455
  const env = sshCommand ? { ...process.env, GIT_SSH_COMMAND: sshCommand } : process.env
456
- // Already mirrored just fetch (incremental, fast). HEAD presence == bare repo.
456
+ // Pin the STANDARD fetch refspec (default branch refs/remotes/origin/<def>)
457
+ // so worktrees off this mirror get an ordinary `origin/<def>` tracking ref —
458
+ // `git rebase origin/<def>`, `git status` ahead/behind, `git log origin/<def>`
459
+ // all work as in any normal clone. Self-healing: re-assert it on EVERY ensure
460
+ // (incl. caches pinned by the old non-standard `+…:refs/heads/<def>` refspec),
461
+ // then fetch so the standard ref materializes.
462
+ const assertStandardRefspec = async () => {
463
+ const def = await execFileP("git", ["-C", dir, "symbolic-ref", "--short", "HEAD"])
464
+ .then((r) => r.stdout.trim()).catch(() => "")
465
+ if (def) {
466
+ await execFileP("git", ["-C", dir, "config", "remote.origin.fetch", `+refs/heads/${def}:refs/remotes/origin/${def}`]).catch(() => {})
467
+ }
468
+ }
469
+ // Already mirrored → re-assert the standard refspec, then fetch (incremental,
470
+ // fast). HEAD presence == bare repo.
457
471
  if (existsSyncBase(join(dir, "HEAD"))) {
472
+ await assertStandardRefspec()
458
473
  await execFileP("git", ["-C", dir, "fetch", "--quiet", "origin"], { env, timeout: 60_000 }).catch(() => {})
459
474
  return dir
460
475
  }
@@ -474,13 +489,13 @@ async function ensureRepoMirror(user: string, name: string, sshCommand?: string)
474
489
  await execFileP("git", ["clone", "--bare", "--single-branch", "--", spec.git, dir], { env, timeout: 300_000 })
475
490
  }
476
491
  // A bare clone sets NO fetch refspec, so `git fetch origin` wouldn't advance
477
- // any ref. Pin one for JUST the default branch so future fetches keep
478
- // refs/heads/<default> (= the worktree start point) fresh and stay small.
479
- const def = await execFileP("git", ["-C", dir, "symbolic-ref", "--short", "HEAD"])
480
- .then((r) => r.stdout.trim()).catch(() => "")
481
- if (def) {
482
- await execFileP("git", ["-C", dir, "config", "remote.origin.fetch", `+refs/heads/${def}:refs/heads/${def}`]).catch(() => {})
483
- }
492
+ // any ref. Pin the STANDARD one for JUST the default branch
493
+ // (+refs/heads/<def>:refs/remotes/origin/<def>) so worktrees off this mirror
494
+ // get an ordinary `origin/<def>` tracking ref and the mirror stays small.
495
+ await assertStandardRefspec()
496
+ // Materialize refs/remotes/origin/<def> now (the clone only wrote
497
+ // refs/heads/<def>), so the very first worktree already tracks origin.
498
+ await execFileP("git", ["-C", dir, "fetch", "--quiet", "origin"], { env, timeout: 60_000 }).catch(() => {})
484
499
  console.log(`[loopat] mirrored ${spec.git} → ${dir}`)
485
500
  return dir
486
501
  } catch (e: any) {
@@ -2059,6 +2074,14 @@ async function remoteDefaultBranch(dir: string): Promise<string> {
2059
2074
  const m = stdout.match(/ref:\s+refs\/heads\/(\S+)\s+HEAD/)
2060
2075
  if (m?.[1]) return m[1]
2061
2076
  } catch {}
2077
+ // Bare mirrors don't carry a refs/remotes/origin/HEAD, but their own HEAD
2078
+ // symbolic-ref names the default branch the clone tracked — cheaper than (and
2079
+ // a fallback for) ls-remote, and correct for `ensureRepoMirror`'s mirrors.
2080
+ try {
2081
+ const { stdout } = await execFileP("git", ["-C", dir, "symbolic-ref", "--short", "HEAD"])
2082
+ const b = stdout.trim().replace(/^origin\//, "")
2083
+ if (b) return b
2084
+ } catch {}
2062
2085
  return "main"
2063
2086
  }
2064
2087
 
@@ -2206,10 +2229,17 @@ export async function createLoop(opts: {
2206
2229
  const branch = `loop/${(await shortBranchSlug(meta.title))}-${id.slice(0, 6)}`
2207
2230
  try {
2208
2231
  // ① pull (docs/context-flow.md): ensureRepoMirror already fetched, so the
2209
- // mirror's HEAD (refs/heads/<default>) is the latest consensus. Worktree
2210
- // the new loop branch off it fresh, and the worktree's `origin` is the
2211
- // real remote, so the loop's pushes go straight upstream.
2212
- await execFileP("git", ["-C", repoPath, "worktree", "add", "-b", branch, loopWorkdir(id)])
2232
+ // mirror's `origin/<default>` tracking ref is the latest consensus. Open
2233
+ // the loop branch off `origin/<default>` (not the bare mirror's HEAD) so
2234
+ // the worktree is an ORDINARY clone: it has a real `origin/<default>`
2235
+ // tracking ref and `git rebase origin/<default>` / `git status`
2236
+ // ahead-behind work with nothing special to learn. Fall back to HEAD only
2237
+ // if origin/<default> can't be resolved (offline / empty remote).
2238
+ const start = await remoteStartPoint(repoPath, userSsh)
2239
+ const addArgs = start
2240
+ ? ["-C", repoPath, "worktree", "add", "-b", branch, loopWorkdir(id), start]
2241
+ : ["-C", repoPath, "worktree", "add", "-b", branch, loopWorkdir(id)]
2242
+ await execFileP("git", addArgs)
2213
2243
  meta.repo = opts.repo
2214
2244
  meta.branch = branch
2215
2245
  } catch (e: any) {
@@ -71,6 +71,7 @@ For team memory: when an insight is genuinely team-relevant (a convention everyo
71
71
  - **Don't echo sensitive values** (API keys, tokens, SSH key material, anything that looks like a credential) to chat. Reference by filename or env var name instead.
72
72
  - **Default to short, direct answers**. Don't announce a plan unless the task is genuinely large.
73
73
  - **Read before Edit on long files**; avoid guessing surrounding context.
74
+ - **`origin` is the source of truth — finishing means pushing to origin.** The workdir is an ordinary git worktree with a normal `origin/<default>` tracking ref, so `git rebase origin/<default>`, `git status` ahead/behind, and `git log origin/<default>` all work as usual. A local commit is NOT "done": work is only preserved and shared once it reaches origin — open a PR, or push directly when that's the team's flow. Don't consider a task complete while it lives only in local commits.
74
75
 
75
76
  ## collaboration
76
77