@north-light/crouter 0.3.289 → 0.3.290

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.
@@ -172,7 +172,7 @@ function makePushLeaf(kind) {
172
172
  ...(kind === 'final'
173
173
  ? [
174
174
  'Marks the node done (status + intent) server-side; its engine shuts down on next stop.',
175
- 'When this node owns a clean managed worktree already contained in its local base, the server auto-drops it instead of blocking on `node worktree close`. A clean worktree whose exact branch tip is already on origin may also finalize while retaining its branch and checkout; uncommitted changes or a branch tip not on origin still block with open_managed_worktree.',
175
+ 'When this node owns a clean managed worktree already contained in its local base, the server auto-drops it instead of blocking on `node worktree close`. A clean worktree whose exact tip exists on any branch on origin may also finalize while retaining its branch and checkout; uncommitted changes or a tip absent from origin still block with open_managed_worktree.',
176
176
  ]
177
177
  : []),
178
178
  ],
@@ -970,18 +970,22 @@ export async function branchIsPushedAtCurrentTip(wt) {
970
970
  const initialTip = cleanCurrentBranchTip(wt);
971
971
  if (initialTip === null)
972
972
  return false;
973
- // A cached origin/<branch> can be stale in either direction. Query origin's
974
- // branch ref directly and require exact-tip equality: a remote that has an
975
- // older commit would still leave newer local work stranded at finalization.
973
+ // A cached origin branch can be stale in either direction. Query origin's
974
+ // heads directly and require exact-tip equality: a remote that has an older
975
+ // commit would still leave newer local work stranded at finalization. The
976
+ // branch name is deliberately irrelevant because PR workflows publish this
977
+ // managed checkout's tip under a human-readable branch name.
976
978
  // This runs asynchronously with a bounded, non-interactive Git invocation so
977
979
  // an unreachable remote never stalls crtrd's event loop.
978
980
  const ssh = process.env['GIT_SSH_COMMAND'] ?? process.env['GIT_SSH'] ?? 'ssh';
979
- const remote = await gitAsync(['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${wt.branch}`], wt.path, undefined, { timeoutMs: 10_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_SSH_COMMAND: `${ssh} -o BatchMode=yes` } });
981
+ const remote = await gitAsync(['ls-remote', '--exit-code', '--heads', 'origin'], wt.path, undefined, { timeoutMs: 10_000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_SSH_COMMAND: `${ssh} -o BatchMode=yes` } });
980
982
  if (remote.status !== 0)
981
983
  return false;
982
- const expectedRef = `refs/heads/${wt.branch}`;
983
- const matchingLine = remote.stdout.split('\n').find((line) => line.endsWith(`\t${expectedRef}`));
984
- if (matchingLine?.slice(0, matchingLine.indexOf('\t')) !== initialTip)
984
+ const remotelyPushed = remote.stdout.split('\n').some((line) => {
985
+ const [sha, ref] = line.split('\t', 2);
986
+ return sha === initialTip && ref?.startsWith('refs/heads/') === true;
987
+ });
988
+ if (!remotelyPushed)
985
989
  return false;
986
990
  // The remote proof is useful only if the local checkout is still the exact
987
991
  // clean tip it inspected when finalization begins.
@@ -187,6 +187,34 @@ test('re-homed guard: `crtr push final` on a node with a managed worktree holdin
187
187
  rmSync(repo, { recursive: true, force: true });
188
188
  }
189
189
  });
190
+ test('`crtr push final` concludes a clean managed worktree whose tip exists on origin only under a different branch name (#912)', async () => {
191
+ const repo = initRepo();
192
+ const remote = mkdtempSync(join(tmpdir(), 'crtr-leafparity-remote-'));
193
+ try {
194
+ git(['init', '--bare'], remote);
195
+ git(['remote', 'add', 'origin', remote], repo);
196
+ git(['push', '-u', 'origin', 'main'], repo);
197
+ const id = 'wt-912-differently-named-remote-branch';
198
+ const wt = createManagedWorktree(repo, id);
199
+ writeFileSync(join(wt.path, 'delivered-via-pr.txt'), 'delivered via a PR branch\n');
200
+ git(['add', '.'], wt.path);
201
+ git(['commit', '-m', 'delivered via differently named branch'], wt.path);
202
+ const tip = gitSync(['rev-parse', 'HEAD'], wt.path).stdout.trim();
203
+ const remoteBranch = 'fix/issue-912-worktree-final';
204
+ git(['push', 'origin', `HEAD:refs/heads/${remoteBranch}`], wt.path);
205
+ assert.equal(gitSync(['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${wt.branch}`], wt.path).status, 2, 'the recorded crtr branch is absent from origin');
206
+ assert.equal(gitSync(['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${remoteBranch}`], wt.path).stdout.split('\t')[0], tip, 'origin carries the exact tip under the PR branch name');
207
+ createNode(worktreeNode(id, wt.path, wt));
208
+ const out = await runPushAs(id, 'final', 'delivered through a differently named PR branch');
209
+ assert.equal(out['worktree_auto_dropped'], undefined, 'the remote-tip allowance retains the open worktree');
210
+ assert.equal(getNode(id)?.managed_worktree?.state, 'open', 'the remote-tip allowance does not close the worktree');
211
+ assert.equal(getNode(id)?.status, 'done', 'the node finalizes when origin carries its exact tip under the PR branch name');
212
+ }
213
+ finally {
214
+ rmSync(repo, { recursive: true, force: true });
215
+ rmSync(remote, { recursive: true, force: true });
216
+ }
217
+ });
190
218
  test('auto-drop: `crtr push final` on a node with a CLEAN, zero-commit managed worktree auto-drops it server-side, surfaces worktree_auto_dropped(+path) on the leaf output, and finalizes normally', async () => {
191
219
  const repo = initRepo();
192
220
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.289",
3
+ "version": "0.3.290",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -94,7 +94,7 @@
94
94
  "test": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --import ./scripts/test-env-scrub.mjs --test --test-force-exit --test-concurrency=$(node -p 'Math.max(4, require(`node:os`).availableParallelism() - 1)') --test-timeout=10000 'src/**/__tests__/*.test.ts'",
95
95
  "test:integration": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --import ./scripts/test-env-scrub.mjs --test --test-concurrency=$(node -p 'Math.min(8, Math.max(2, require(`node:os`).availableParallelism() - 1))') --test-timeout=60000 'src/**/__tests__/integration/*.test.ts'",
96
96
  "test:seam": "npm run build && npm run test:seam:built",
97
- "test:seam:built": "CRTR_NO_DAEMON_AUTOSTART=1 node --import ./scripts/test-env-scrub.mjs --test --test-concurrency=4 --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'",
97
+ "test:seam:built": "CRTR_NO_DAEMON_AUTOSTART=1 node --import ./scripts/test-env-scrub.mjs --test --test-concurrency=$(node -p 'Math.min(4, Math.max(2, require(`node:os`).availableParallelism() - 2))') --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'",
98
98
  "test:all": "npm test && npm run test:integration && npm run test:seam"
99
99
  },
100
100
  "overrides": {
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.289",
3
+ "version": "0.3.290",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.289",
9
+ "version": "0.3.290",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
@@ -4244,17 +4244,17 @@
4244
4244
  },
4245
4245
  "packages/crouter-api": {
4246
4246
  "name": "@north-light/crouter-api",
4247
- "version": "0.3.289",
4247
+ "version": "0.3.290",
4248
4248
  "license": "UNLICENSED"
4249
4249
  },
4250
4250
  "packages/crouter-env-docker": {
4251
4251
  "name": "@north-light/crouter-env-docker",
4252
- "version": "0.3.289",
4252
+ "version": "0.3.290",
4253
4253
  "license": "UNLICENSED"
4254
4254
  },
4255
4255
  "packages/crouter-sdk": {
4256
4256
  "name": "@north-light/crouter-sdk",
4257
- "version": "0.3.289",
4257
+ "version": "0.3.290",
4258
4258
  "license": "UNLICENSED",
4259
4259
  "dependencies": {
4260
4260
  "@north-light/crouter-api": "*"