terminalhire 0.26.1 → 0.27.0

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.
@@ -8796,6 +8796,9 @@ function looksLikeShortRef(arg) {
8796
8796
  function isVerblessShortRefClaim(verb, positional) {
8797
8797
  return looksLikeShortRef(verb) && positional.length === 0;
8798
8798
  }
8799
+ function isStrayArgShortRefClaim(verb, positional) {
8800
+ return looksLikeShortRef(verb) && positional.length > 0;
8801
+ }
8799
8802
  function findClaimableByShortRef(ref) {
8800
8803
  try {
8801
8804
  return findByShortRefInPool(readClaimablePool(), ref);
@@ -9159,10 +9162,19 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
9159
9162
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
9160
9163
  console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
9161
9164
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
9162
- console.log("\n Next:");
9163
- console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
9164
- console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
9165
- console.log(" 3. publish (pushes to your fork + opens the PR): terminalhire claim submit " + claim.id);
9165
+ console.log("\n Next \u2014 start work (forks + clones into an isolated worktree; your terminal stays put):");
9166
+ console.log(" terminalhire claim start " + claim.id);
9167
+ console.log(" Then publish when it is done (the only step that pushes + opens the PR):");
9168
+ console.log(" terminalhire claim submit " + claim.id);
9169
+ if (flags.start) {
9170
+ await cmdStart(claim.id, { start: true });
9171
+ } else if (process.stdin.isTTY && !flags["no-start"]) {
9172
+ const go = await confirm(`
9173
+ Fork ${claim.repoFullName} and start now? (y/N) `);
9174
+ if (go) await cmdStart(claim.id, { start: true });
9175
+ else console.log(`
9176
+ Saved. Start anytime: terminalhire claim start ${claim.id}`);
9177
+ }
9166
9178
  }
9167
9179
  async function cmdPreview(arg, { json } = {}) {
9168
9180
  if (!arg) {
@@ -9359,6 +9371,179 @@ async function cmdAttach(id, worktree, branch) {
9359
9371
  claims.updateClaim(id, { worktreePath: toplevel, branch });
9360
9372
  console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
9361
9373
  }
9374
+ function workDirFor(repoFullName, issueNumber) {
9375
+ const [owner, repo] = String(repoFullName).split("/");
9376
+ const suffix = issueNumber ? `-${issueNumber}` : "";
9377
+ return join7(homedir6(), "terminalhire", "work", `${owner}-${repo}${suffix}`);
9378
+ }
9379
+ function startBranchFor(repoFullName, issueNumber) {
9380
+ const repo = String(repoFullName).split("/")[1] || "claim";
9381
+ return `th/${repo}-${issueNumber || "work"}`;
9382
+ }
9383
+ function explicitForkConsent(flags = {}) {
9384
+ return Boolean(flags.start) || Boolean(flags.fork);
9385
+ }
9386
+ async function ensureForkExists(repoFullName, ghUser) {
9387
+ const repoShort = repoFullName.split("/")[1];
9388
+ const forkFullName = `${ghUser}/${repoShort}`;
9389
+ await sh("gh", ["repo", "fork", repoFullName, "--clone=false"]);
9390
+ let isFork = false;
9391
+ try {
9392
+ isFork = await sh("gh", ["api", `repos/${forkFullName}`, "--jq", ".fork"]) === "true";
9393
+ } catch {
9394
+ isFork = false;
9395
+ }
9396
+ if (!isFork) throw new Error(`fork ${forkFullName} created but could not be verified as a fork`);
9397
+ return forkFullName;
9398
+ }
9399
+ async function cmdStart(id, flags = {}) {
9400
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
9401
+ if (!id) {
9402
+ const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
9403
+ if (startable.length === 0) {
9404
+ console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
9405
+ return;
9406
+ }
9407
+ console.log(`
9408
+ ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
9409
+ `);
9410
+ for (const c of startable) {
9411
+ console.log(` ${c.title}`);
9412
+ console.log(` terminalhire claim start ${c.id}`);
9413
+ }
9414
+ console.log("\nRun the command under the one you want to start.");
9415
+ return;
9416
+ }
9417
+ const claim = claims.findClaim(id);
9418
+ if (!claim) {
9419
+ console.error(`terminalhire claim: no claim with id '${id}'.`);
9420
+ process.exit(1);
9421
+ }
9422
+ if (claim.worktreePath) {
9423
+ let stillThere = false;
9424
+ try {
9425
+ stillThere = await sh("git", ["-C", claim.worktreePath, "rev-parse", "--show-toplevel"]) === claim.worktreePath;
9426
+ } catch {
9427
+ stillThere = false;
9428
+ }
9429
+ if (stillThere) {
9430
+ console.log("Already started \u2014 your worktree is ready (your terminal stays put):");
9431
+ console.log(` cd ${claim.worktreePath}`);
9432
+ if (claim.branch) console.log(` branch: ${claim.branch}`);
9433
+ console.log(`
9434
+ When it's done: terminalhire claim submit ${id}`);
9435
+ return;
9436
+ }
9437
+ }
9438
+ if (flags.here) {
9439
+ await cmdStartHere(claims, claim);
9440
+ return;
9441
+ }
9442
+ let ghUser;
9443
+ try {
9444
+ ghUser = await sh("gh", ["api", "user", "-q", ".login"]);
9445
+ } catch {
9446
+ console.error("terminalhire claim: 'gh' CLI not available or not authenticated. Run 'gh auth login'.");
9447
+ process.exit(1);
9448
+ }
9449
+ let consented = explicitForkConsent(flags);
9450
+ if (!consented && process.stdin.isTTY) {
9451
+ consented = await confirm(`
9452
+ Fork ${claim.repoFullName} to @${ghUser} and clone it to start? (y/N) `);
9453
+ }
9454
+ if (!consented) {
9455
+ console.error(
9456
+ `
9457
+ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to your GitHub account (a network write).
9458
+ Re-run in a terminal to confirm interactively, or pass --fork to consent non-interactively.`
9459
+ );
9460
+ process.exit(1);
9461
+ }
9462
+ const issueNumber = (parseGitHubUrl(claim.issueUrl) || {}).number;
9463
+ const destDir = workDirFor(claim.repoFullName, issueNumber);
9464
+ if (existsSync6(destDir)) {
9465
+ console.error(
9466
+ `terminalhire claim: ${destDir} already exists \u2014 refusing to clobber it.
9467
+ Remove it and retry, or attach it: terminalhire claim attach ${id} --worktree ${destDir} --branch <branch>`
9468
+ );
9469
+ process.exit(1);
9470
+ }
9471
+ mkdirSync6(join7(homedir6(), "terminalhire", "work"), { recursive: true });
9472
+ let forkFullName;
9473
+ try {
9474
+ forkFullName = await ensureForkExists(claim.repoFullName, ghUser);
9475
+ } catch (err) {
9476
+ console.error(`terminalhire claim: could not create your fork of ${claim.repoFullName}. ${err.message ?? err}`);
9477
+ process.exit(1);
9478
+ }
9479
+ try {
9480
+ await sh("gh", ["repo", "clone", forkFullName, destDir]);
9481
+ } catch (err) {
9482
+ try {
9483
+ rmSync3(destDir, { recursive: true, force: true });
9484
+ } catch {
9485
+ }
9486
+ console.error(`terminalhire claim: clone of ${forkFullName} failed. ${err.stderr || err.message || err}`);
9487
+ process.exit(1);
9488
+ }
9489
+ try {
9490
+ await sh("git", ["-C", destDir, "remote", "get-url", "upstream"]);
9491
+ } catch {
9492
+ try {
9493
+ await sh("git", ["-C", destDir, "remote", "add", "upstream", `https://github.com/${claim.repoFullName}.git`]);
9494
+ } catch {
9495
+ }
9496
+ }
9497
+ const branch = startBranchFor(claim.repoFullName, issueNumber);
9498
+ await sh("git", ["-C", destDir, "checkout", "-b", branch]);
9499
+ const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
9500
+ claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
9501
+ console.log(`
9502
+ \u2713 Started: ${claim.title}`);
9503
+ console.log(` fork: ${forkFullName}`);
9504
+ console.log(` branch: ${branch}`);
9505
+ console.log("\n Your isolated worktree is ready \u2014 your current terminal is untouched:");
9506
+ console.log(` cd ${toplevel}`);
9507
+ console.log("\n When the work is done (the only step that pushes + opens the PR):");
9508
+ console.log(` terminalhire claim submit ${id}`);
9509
+ }
9510
+ async function cmdStartHere(claims, claim) {
9511
+ let toplevel;
9512
+ try {
9513
+ toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
9514
+ } catch {
9515
+ console.error("terminalhire claim: --here must be run inside a git repository.");
9516
+ process.exit(1);
9517
+ }
9518
+ const remotesOut = await sh("git", ["-C", toplevel, "remote", "-v"]).catch(() => "");
9519
+ const repos = new Set(
9520
+ remotesOut.split("\n").map((l) => parseRepoFromRemote(l.split(/\s+/)[1])).filter(Boolean).map((r) => r.toLowerCase())
9521
+ );
9522
+ if (!repos.has(claim.repoFullName.toLowerCase())) {
9523
+ console.error(
9524
+ `terminalhire claim: --here expects a clone of ${claim.repoFullName}, but no remote here points there.
9525
+ Use \`terminalhire claim start ${claim.id}\` (no --here) to fork + clone it into a fresh worktree.`
9526
+ );
9527
+ process.exit(1);
9528
+ }
9529
+ const dirty = await sh("git", ["-C", toplevel, "status", "--porcelain", "--untracked-files=no"]).catch(() => "");
9530
+ if (dirty) {
9531
+ console.error(
9532
+ "terminalhire claim: --here needs a clean working tree (claim work must not mix with your current changes).\n Commit or stash first, or use `terminalhire claim start` for an isolated worktree."
9533
+ );
9534
+ process.exit(1);
9535
+ }
9536
+ const issueNumber = (parseGitHubUrl(claim.issueUrl) || {}).number;
9537
+ const branch = startBranchFor(claim.repoFullName, issueNumber);
9538
+ await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
9539
+ claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
9540
+ console.log(`
9541
+ \u2713 Started here: ${claim.title}`);
9542
+ console.log(` worktree: ${toplevel}`);
9543
+ console.log(` branch: ${branch}`);
9544
+ console.log(`
9545
+ When the work is done: terminalhire claim submit ${claim.id}`);
9546
+ }
9362
9547
  async function cmdSubmit(id, flags = {}) {
9363
9548
  const worktreeOverride = flags.worktree;
9364
9549
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -9373,10 +9558,10 @@ async function cmdSubmit(id, flags = {}) {
9373
9558
  console.error(`terminalhire claim: no claim with id '${id}'.`);
9374
9559
  process.exit(1);
9375
9560
  }
9376
- if (claim.state !== "ready") {
9561
+ if (claim.state !== "working" && claim.state !== "ready") {
9377
9562
  console.error(
9378
- `terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
9379
- terminalhire claim update ${id} ready`
9563
+ `terminalhire claim: ${id} is '${claim.state}'. Submit runs once work has started ('working', via 'claim start') or the review gate cleared it ('ready'). Start it first:
9564
+ terminalhire claim start ${id}`
9380
9565
  );
9381
9566
  process.exit(1);
9382
9567
  }
@@ -9954,6 +10139,12 @@ async function run() {
9954
10139
  await cmdRecord(verb, flags);
9955
10140
  return;
9956
10141
  }
10142
+ if (isStrayArgShortRefClaim(verb, positional)) {
10143
+ console.error(
10144
+ `terminalhire claim: '${verb}' is a short ref and takes no extra arguments (got: ${positional.join(" ")}). Did you mean \`terminalhire claim ${verb}\`?`
10145
+ );
10146
+ process.exit(1);
10147
+ }
9957
10148
  try {
9958
10149
  switch (verb) {
9959
10150
  case "preview":
@@ -9962,6 +10153,9 @@ async function run() {
9962
10153
  case "record":
9963
10154
  await cmdRecord(positional[0], flags);
9964
10155
  break;
10156
+ case "start":
10157
+ await cmdStart(positional[0], flags);
10158
+ break;
9965
10159
  case "list":
9966
10160
  await cmdList(active);
9967
10161
  break;
@@ -9981,7 +10175,7 @@ async function run() {
9981
10175
  await cmdRelease(positional[0]);
9982
10176
  break;
9983
10177
  default:
9984
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | attach | list | status | update | submit | release`);
10178
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | start | attach | list | status | update | submit | release`);
9985
10179
  process.exit(1);
9986
10180
  }
9987
10181
  } catch (err) {
@@ -9997,11 +10191,13 @@ export {
9997
10191
  cmdRecord,
9998
10192
  countOpenPRsReferencingIssue,
9999
10193
  diffContention,
10194
+ explicitForkConsent,
10000
10195
  findClaimableByShortRef,
10001
10196
  findClaimableInCache,
10002
10197
  fmtAge,
10003
10198
  fmtContestedWarning,
10004
10199
  isContested,
10200
+ isStrayArgShortRefClaim,
10005
10201
  isVerblessShortRefClaim,
10006
10202
  listOpenPRsReferencingIssue,
10007
10203
  matchReferencingPrs,
@@ -10011,7 +10207,9 @@ export {
10011
10207
  resolveSubmitWorktree,
10012
10208
  revokeFailureAction,
10013
10209
  run,
10014
- selectPushRemote
10210
+ selectPushRemote,
10211
+ startBranchFor,
10212
+ workDirFor
10015
10213
  };
10016
10214
  /*! Bundled license information:
10017
10215
 
@@ -11039,11 +11039,13 @@ __export(jpi_claim_exports, {
11039
11039
  cmdRecord: () => cmdRecord,
11040
11040
  countOpenPRsReferencingIssue: () => countOpenPRsReferencingIssue,
11041
11041
  diffContention: () => diffContention,
11042
+ explicitForkConsent: () => explicitForkConsent,
11042
11043
  findClaimableByShortRef: () => findClaimableByShortRef,
11043
11044
  findClaimableInCache: () => findClaimableInCache,
11044
11045
  fmtAge: () => fmtAge,
11045
11046
  fmtContestedWarning: () => fmtContestedWarning,
11046
11047
  isContested: () => isContested,
11048
+ isStrayArgShortRefClaim: () => isStrayArgShortRefClaim,
11047
11049
  isVerblessShortRefClaim: () => isVerblessShortRefClaim,
11048
11050
  listOpenPRsReferencingIssue: () => listOpenPRsReferencingIssue,
11049
11051
  matchReferencingPrs: () => matchReferencingPrs,
@@ -11053,7 +11055,9 @@ __export(jpi_claim_exports, {
11053
11055
  resolveSubmitWorktree: () => resolveSubmitWorktree,
11054
11056
  revokeFailureAction: () => revokeFailureAction,
11055
11057
  run: () => run7,
11056
- selectPushRemote: () => selectPushRemote
11058
+ selectPushRemote: () => selectPushRemote,
11059
+ startBranchFor: () => startBranchFor,
11060
+ workDirFor: () => workDirFor
11057
11061
  });
11058
11062
  import { readFileSync as readFileSync17, writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync10, rmSync as rmSync4 } from "fs";
11059
11063
  import { join as join17 } from "path";
@@ -11198,6 +11202,9 @@ function looksLikeShortRef(arg) {
11198
11202
  function isVerblessShortRefClaim(verb, positional) {
11199
11203
  return looksLikeShortRef(verb) && positional.length === 0;
11200
11204
  }
11205
+ function isStrayArgShortRefClaim(verb, positional) {
11206
+ return looksLikeShortRef(verb) && positional.length > 0;
11207
+ }
11201
11208
  function findClaimableByShortRef(ref) {
11202
11209
  try {
11203
11210
  return findByShortRefInPool(readClaimablePool(), ref);
@@ -11561,10 +11568,19 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
11561
11568
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
11562
11569
  console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
11563
11570
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
11564
- console.log("\n Next:");
11565
- console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
11566
- console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
11567
- console.log(" 3. publish (pushes to your fork + opens the PR): terminalhire claim submit " + claim.id);
11571
+ console.log("\n Next \u2014 start work (forks + clones into an isolated worktree; your terminal stays put):");
11572
+ console.log(" terminalhire claim start " + claim.id);
11573
+ console.log(" Then publish when it is done (the only step that pushes + opens the PR):");
11574
+ console.log(" terminalhire claim submit " + claim.id);
11575
+ if (flags.start) {
11576
+ await cmdStart(claim.id, { start: true });
11577
+ } else if (process.stdin.isTTY && !flags["no-start"]) {
11578
+ const go = await confirm(`
11579
+ Fork ${claim.repoFullName} and start now? (y/N) `);
11580
+ if (go) await cmdStart(claim.id, { start: true });
11581
+ else console.log(`
11582
+ Saved. Start anytime: terminalhire claim start ${claim.id}`);
11583
+ }
11568
11584
  }
11569
11585
  async function cmdPreview(arg, { json } = {}) {
11570
11586
  if (!arg) {
@@ -11761,6 +11777,179 @@ async function cmdAttach(id, worktree, branch) {
11761
11777
  claims.updateClaim(id, { worktreePath: toplevel, branch });
11762
11778
  console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
11763
11779
  }
11780
+ function workDirFor(repoFullName, issueNumber) {
11781
+ const [owner, repo] = String(repoFullName).split("/");
11782
+ const suffix = issueNumber ? `-${issueNumber}` : "";
11783
+ return join17(homedir15(), "terminalhire", "work", `${owner}-${repo}${suffix}`);
11784
+ }
11785
+ function startBranchFor(repoFullName, issueNumber) {
11786
+ const repo = String(repoFullName).split("/")[1] || "claim";
11787
+ return `th/${repo}-${issueNumber || "work"}`;
11788
+ }
11789
+ function explicitForkConsent(flags = {}) {
11790
+ return Boolean(flags.start) || Boolean(flags.fork);
11791
+ }
11792
+ async function ensureForkExists(repoFullName, ghUser) {
11793
+ const repoShort = repoFullName.split("/")[1];
11794
+ const forkFullName = `${ghUser}/${repoShort}`;
11795
+ await sh("gh", ["repo", "fork", repoFullName, "--clone=false"]);
11796
+ let isFork = false;
11797
+ try {
11798
+ isFork = await sh("gh", ["api", `repos/${forkFullName}`, "--jq", ".fork"]) === "true";
11799
+ } catch {
11800
+ isFork = false;
11801
+ }
11802
+ if (!isFork) throw new Error(`fork ${forkFullName} created but could not be verified as a fork`);
11803
+ return forkFullName;
11804
+ }
11805
+ async function cmdStart(id, flags = {}) {
11806
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
11807
+ if (!id) {
11808
+ const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
11809
+ if (startable.length === 0) {
11810
+ console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
11811
+ return;
11812
+ }
11813
+ console.log(`
11814
+ ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
11815
+ `);
11816
+ for (const c of startable) {
11817
+ console.log(` ${c.title}`);
11818
+ console.log(` terminalhire claim start ${c.id}`);
11819
+ }
11820
+ console.log("\nRun the command under the one you want to start.");
11821
+ return;
11822
+ }
11823
+ const claim = claims.findClaim(id);
11824
+ if (!claim) {
11825
+ console.error(`terminalhire claim: no claim with id '${id}'.`);
11826
+ process.exit(1);
11827
+ }
11828
+ if (claim.worktreePath) {
11829
+ let stillThere = false;
11830
+ try {
11831
+ stillThere = await sh("git", ["-C", claim.worktreePath, "rev-parse", "--show-toplevel"]) === claim.worktreePath;
11832
+ } catch {
11833
+ stillThere = false;
11834
+ }
11835
+ if (stillThere) {
11836
+ console.log("Already started \u2014 your worktree is ready (your terminal stays put):");
11837
+ console.log(` cd ${claim.worktreePath}`);
11838
+ if (claim.branch) console.log(` branch: ${claim.branch}`);
11839
+ console.log(`
11840
+ When it's done: terminalhire claim submit ${id}`);
11841
+ return;
11842
+ }
11843
+ }
11844
+ if (flags.here) {
11845
+ await cmdStartHere(claims, claim);
11846
+ return;
11847
+ }
11848
+ let ghUser;
11849
+ try {
11850
+ ghUser = await sh("gh", ["api", "user", "-q", ".login"]);
11851
+ } catch {
11852
+ console.error("terminalhire claim: 'gh' CLI not available or not authenticated. Run 'gh auth login'.");
11853
+ process.exit(1);
11854
+ }
11855
+ let consented = explicitForkConsent(flags);
11856
+ if (!consented && process.stdin.isTTY) {
11857
+ consented = await confirm(`
11858
+ Fork ${claim.repoFullName} to @${ghUser} and clone it to start? (y/N) `);
11859
+ }
11860
+ if (!consented) {
11861
+ console.error(
11862
+ `
11863
+ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to your GitHub account (a network write).
11864
+ Re-run in a terminal to confirm interactively, or pass --fork to consent non-interactively.`
11865
+ );
11866
+ process.exit(1);
11867
+ }
11868
+ const issueNumber = (parseGitHubUrl(claim.issueUrl) || {}).number;
11869
+ const destDir = workDirFor(claim.repoFullName, issueNumber);
11870
+ if (existsSync10(destDir)) {
11871
+ console.error(
11872
+ `terminalhire claim: ${destDir} already exists \u2014 refusing to clobber it.
11873
+ Remove it and retry, or attach it: terminalhire claim attach ${id} --worktree ${destDir} --branch <branch>`
11874
+ );
11875
+ process.exit(1);
11876
+ }
11877
+ mkdirSync11(join17(homedir15(), "terminalhire", "work"), { recursive: true });
11878
+ let forkFullName;
11879
+ try {
11880
+ forkFullName = await ensureForkExists(claim.repoFullName, ghUser);
11881
+ } catch (err) {
11882
+ console.error(`terminalhire claim: could not create your fork of ${claim.repoFullName}. ${err.message ?? err}`);
11883
+ process.exit(1);
11884
+ }
11885
+ try {
11886
+ await sh("gh", ["repo", "clone", forkFullName, destDir]);
11887
+ } catch (err) {
11888
+ try {
11889
+ rmSync4(destDir, { recursive: true, force: true });
11890
+ } catch {
11891
+ }
11892
+ console.error(`terminalhire claim: clone of ${forkFullName} failed. ${err.stderr || err.message || err}`);
11893
+ process.exit(1);
11894
+ }
11895
+ try {
11896
+ await sh("git", ["-C", destDir, "remote", "get-url", "upstream"]);
11897
+ } catch {
11898
+ try {
11899
+ await sh("git", ["-C", destDir, "remote", "add", "upstream", `https://github.com/${claim.repoFullName}.git`]);
11900
+ } catch {
11901
+ }
11902
+ }
11903
+ const branch = startBranchFor(claim.repoFullName, issueNumber);
11904
+ await sh("git", ["-C", destDir, "checkout", "-b", branch]);
11905
+ const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
11906
+ claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
11907
+ console.log(`
11908
+ \u2713 Started: ${claim.title}`);
11909
+ console.log(` fork: ${forkFullName}`);
11910
+ console.log(` branch: ${branch}`);
11911
+ console.log("\n Your isolated worktree is ready \u2014 your current terminal is untouched:");
11912
+ console.log(` cd ${toplevel}`);
11913
+ console.log("\n When the work is done (the only step that pushes + opens the PR):");
11914
+ console.log(` terminalhire claim submit ${id}`);
11915
+ }
11916
+ async function cmdStartHere(claims, claim) {
11917
+ let toplevel;
11918
+ try {
11919
+ toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
11920
+ } catch {
11921
+ console.error("terminalhire claim: --here must be run inside a git repository.");
11922
+ process.exit(1);
11923
+ }
11924
+ const remotesOut = await sh("git", ["-C", toplevel, "remote", "-v"]).catch(() => "");
11925
+ const repos = new Set(
11926
+ remotesOut.split("\n").map((l) => parseRepoFromRemote(l.split(/\s+/)[1])).filter(Boolean).map((r) => r.toLowerCase())
11927
+ );
11928
+ if (!repos.has(claim.repoFullName.toLowerCase())) {
11929
+ console.error(
11930
+ `terminalhire claim: --here expects a clone of ${claim.repoFullName}, but no remote here points there.
11931
+ Use \`terminalhire claim start ${claim.id}\` (no --here) to fork + clone it into a fresh worktree.`
11932
+ );
11933
+ process.exit(1);
11934
+ }
11935
+ const dirty = await sh("git", ["-C", toplevel, "status", "--porcelain", "--untracked-files=no"]).catch(() => "");
11936
+ if (dirty) {
11937
+ console.error(
11938
+ "terminalhire claim: --here needs a clean working tree (claim work must not mix with your current changes).\n Commit or stash first, or use `terminalhire claim start` for an isolated worktree."
11939
+ );
11940
+ process.exit(1);
11941
+ }
11942
+ const issueNumber = (parseGitHubUrl(claim.issueUrl) || {}).number;
11943
+ const branch = startBranchFor(claim.repoFullName, issueNumber);
11944
+ await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
11945
+ claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
11946
+ console.log(`
11947
+ \u2713 Started here: ${claim.title}`);
11948
+ console.log(` worktree: ${toplevel}`);
11949
+ console.log(` branch: ${branch}`);
11950
+ console.log(`
11951
+ When the work is done: terminalhire claim submit ${claim.id}`);
11952
+ }
11764
11953
  async function cmdSubmit(id, flags = {}) {
11765
11954
  const worktreeOverride = flags.worktree;
11766
11955
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -11775,10 +11964,10 @@ async function cmdSubmit(id, flags = {}) {
11775
11964
  console.error(`terminalhire claim: no claim with id '${id}'.`);
11776
11965
  process.exit(1);
11777
11966
  }
11778
- if (claim.state !== "ready") {
11967
+ if (claim.state !== "working" && claim.state !== "ready") {
11779
11968
  console.error(
11780
- `terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
11781
- terminalhire claim update ${id} ready`
11969
+ `terminalhire claim: ${id} is '${claim.state}'. Submit runs once work has started ('working', via 'claim start') or the review gate cleared it ('ready'). Start it first:
11970
+ terminalhire claim start ${id}`
11782
11971
  );
11783
11972
  process.exit(1);
11784
11973
  }
@@ -12356,6 +12545,12 @@ async function run7() {
12356
12545
  await cmdRecord(verb, flags);
12357
12546
  return;
12358
12547
  }
12548
+ if (isStrayArgShortRefClaim(verb, positional)) {
12549
+ console.error(
12550
+ `terminalhire claim: '${verb}' is a short ref and takes no extra arguments (got: ${positional.join(" ")}). Did you mean \`terminalhire claim ${verb}\`?`
12551
+ );
12552
+ process.exit(1);
12553
+ }
12359
12554
  try {
12360
12555
  switch (verb) {
12361
12556
  case "preview":
@@ -12364,6 +12559,9 @@ async function run7() {
12364
12559
  case "record":
12365
12560
  await cmdRecord(positional[0], flags);
12366
12561
  break;
12562
+ case "start":
12563
+ await cmdStart(positional[0], flags);
12564
+ break;
12367
12565
  case "list":
12368
12566
  await cmdList(active);
12369
12567
  break;
@@ -12383,7 +12581,7 @@ async function run7() {
12383
12581
  await cmdRelease(positional[0]);
12384
12582
  break;
12385
12583
  default:
12386
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | attach | list | status | update | submit | release`);
12584
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | start | attach | list | status | update | submit | release`);
12387
12585
  process.exit(1);
12388
12586
  }
12389
12587
  } catch (err) {
@@ -42859,6 +43057,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
42859
43057
  console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
42860
43058
  console.log(" terminalhire contribute Open issues where a merged PR counts toward your r\xE9sum\xE9");
42861
43059
  console.log(" terminalhire claim record <id|issueUrl> Claim a bounty locally + print the executor brief");
43060
+ console.log(" terminalhire claim start [<id>] Fork + clone + branch into an isolated worktree (never cd's you)");
42862
43061
  console.log(" terminalhire claim list [--active] List your claims + accepted-PR rate");
42863
43062
  console.log(" terminalhire claim status [<id>] Poll source PR merge state (updates the metric)");
42864
43063
  console.log(" terminalhire claim --push Opt-in: show your claims on your dashboard (typed-yes + browser confirm)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",