terminalhire 0.26.2 → 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.
@@ -9162,10 +9162,19 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
9162
9162
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
9163
9163
  console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
9164
9164
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
9165
- console.log("\n Next:");
9166
- console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
9167
- console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
9168
- 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
+ }
9169
9178
  }
9170
9179
  async function cmdPreview(arg, { json } = {}) {
9171
9180
  if (!arg) {
@@ -9362,6 +9371,179 @@ async function cmdAttach(id, worktree, branch) {
9362
9371
  claims.updateClaim(id, { worktreePath: toplevel, branch });
9363
9372
  console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
9364
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
+ }
9365
9547
  async function cmdSubmit(id, flags = {}) {
9366
9548
  const worktreeOverride = flags.worktree;
9367
9549
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -9376,10 +9558,10 @@ async function cmdSubmit(id, flags = {}) {
9376
9558
  console.error(`terminalhire claim: no claim with id '${id}'.`);
9377
9559
  process.exit(1);
9378
9560
  }
9379
- if (claim.state !== "ready") {
9561
+ if (claim.state !== "working" && claim.state !== "ready") {
9380
9562
  console.error(
9381
- `terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
9382
- 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}`
9383
9565
  );
9384
9566
  process.exit(1);
9385
9567
  }
@@ -9971,6 +10153,9 @@ async function run() {
9971
10153
  case "record":
9972
10154
  await cmdRecord(positional[0], flags);
9973
10155
  break;
10156
+ case "start":
10157
+ await cmdStart(positional[0], flags);
10158
+ break;
9974
10159
  case "list":
9975
10160
  await cmdList(active);
9976
10161
  break;
@@ -9990,7 +10175,7 @@ async function run() {
9990
10175
  await cmdRelease(positional[0]);
9991
10176
  break;
9992
10177
  default:
9993
- 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`);
9994
10179
  process.exit(1);
9995
10180
  }
9996
10181
  } catch (err) {
@@ -10006,6 +10191,7 @@ export {
10006
10191
  cmdRecord,
10007
10192
  countOpenPRsReferencingIssue,
10008
10193
  diffContention,
10194
+ explicitForkConsent,
10009
10195
  findClaimableByShortRef,
10010
10196
  findClaimableInCache,
10011
10197
  fmtAge,
@@ -10021,7 +10207,9 @@ export {
10021
10207
  resolveSubmitWorktree,
10022
10208
  revokeFailureAction,
10023
10209
  run,
10024
- selectPushRemote
10210
+ selectPushRemote,
10211
+ startBranchFor,
10212
+ workDirFor
10025
10213
  };
10026
10214
  /*! Bundled license information:
10027
10215
 
@@ -11039,6 +11039,7 @@ __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,
@@ -11054,7 +11055,9 @@ __export(jpi_claim_exports, {
11054
11055
  resolveSubmitWorktree: () => resolveSubmitWorktree,
11055
11056
  revokeFailureAction: () => revokeFailureAction,
11056
11057
  run: () => run7,
11057
- selectPushRemote: () => selectPushRemote
11058
+ selectPushRemote: () => selectPushRemote,
11059
+ startBranchFor: () => startBranchFor,
11060
+ workDirFor: () => workDirFor
11058
11061
  });
11059
11062
  import { readFileSync as readFileSync17, writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync10, rmSync as rmSync4 } from "fs";
11060
11063
  import { join as join17 } from "path";
@@ -11565,10 +11568,19 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
11565
11568
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
11566
11569
  console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
11567
11570
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
11568
- console.log("\n Next:");
11569
- console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
11570
- console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
11571
- 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
+ }
11572
11584
  }
11573
11585
  async function cmdPreview(arg, { json } = {}) {
11574
11586
  if (!arg) {
@@ -11765,6 +11777,179 @@ async function cmdAttach(id, worktree, branch) {
11765
11777
  claims.updateClaim(id, { worktreePath: toplevel, branch });
11766
11778
  console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
11767
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
+ }
11768
11953
  async function cmdSubmit(id, flags = {}) {
11769
11954
  const worktreeOverride = flags.worktree;
11770
11955
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -11779,10 +11964,10 @@ async function cmdSubmit(id, flags = {}) {
11779
11964
  console.error(`terminalhire claim: no claim with id '${id}'.`);
11780
11965
  process.exit(1);
11781
11966
  }
11782
- if (claim.state !== "ready") {
11967
+ if (claim.state !== "working" && claim.state !== "ready") {
11783
11968
  console.error(
11784
- `terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
11785
- 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}`
11786
11971
  );
11787
11972
  process.exit(1);
11788
11973
  }
@@ -12374,6 +12559,9 @@ async function run7() {
12374
12559
  case "record":
12375
12560
  await cmdRecord(positional[0], flags);
12376
12561
  break;
12562
+ case "start":
12563
+ await cmdStart(positional[0], flags);
12564
+ break;
12377
12565
  case "list":
12378
12566
  await cmdList(active);
12379
12567
  break;
@@ -12393,7 +12581,7 @@ async function run7() {
12393
12581
  await cmdRelease(positional[0]);
12394
12582
  break;
12395
12583
  default:
12396
- 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`);
12397
12585
  process.exit(1);
12398
12586
  }
12399
12587
  } catch (err) {
@@ -42869,6 +43057,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
42869
43057
  console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
42870
43058
  console.log(" terminalhire contribute Open issues where a merged PR counts toward your r\xE9sum\xE9");
42871
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)");
42872
43061
  console.log(" terminalhire claim list [--active] List your claims + accepted-PR rate");
42873
43062
  console.log(" terminalhire claim status [<id>] Poll source PR merge state (updates the metric)");
42874
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.2",
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",