@allocator-one/harness-cli 1.16.0 → 1.17.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.
package/dist/cli.js CHANGED
@@ -1,57 +1,44 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ UPDATE_CHECK_COMMAND,
4
+ guardBrokenPipes,
5
+ operatorDeprecationNotice,
6
+ performUpdateCheck,
7
+ selfHeal,
8
+ updateCheck
9
+ } from "./chunk-A4WZAC7L.js";
2
10
  import {
3
11
  ACTIVITY_MAX_LIMIT,
4
- Client,
5
- CommandError,
6
- DEFAULT_BASE_URL,
7
12
  EXIT_INTERRUPTED,
8
13
  EXIT_JOB_ENDED,
9
14
  EXIT_WAITING_FOR_PERSON,
10
- EXPERT_ASSIGNMENTS_PATH,
11
- EXPERT_LAPSED,
12
- HttpError,
13
15
  JOBS_MAX_LIMIT,
14
16
  JOBS_MAX_PAGES,
15
17
  JOB_STATES,
16
- LocalError,
17
18
  PLAN_LIST_MAX_LIMIT,
18
19
  PLAN_STATUSES,
19
20
  RELATION_CREATE_TYPES,
20
21
  RELATION_UPDATE_TYPES,
21
- SESSION_PATH,
22
22
  STEER_MAX_CHARACTERS,
23
- TransportError,
24
- UpgradeRequiredError,
25
- UsageError,
26
23
  activity,
27
24
  archive,
28
25
  assignments,
29
26
  attachImage,
30
27
  blocked,
31
- clean,
32
28
  close,
33
29
  comment,
34
30
  comments,
35
31
  conditionList,
36
32
  create,
37
- defaultCredentialsPath,
38
33
  deleteComment,
39
- deleteCredentials,
40
34
  dep,
41
35
  docCreate,
42
36
  docList,
43
37
  docShow,
44
38
  docUpdate,
45
39
  editComment,
46
- errorDetails,
47
- expertSummary,
48
- explainExpertRefusal,
49
- findRepoConfig,
50
40
  gateSet,
51
41
  instructionFrom,
52
- isExpertToken,
53
- isHostTrusted,
54
- isLoginCredential,
55
42
  jobCancel,
56
43
  jobContinue,
57
44
  jobPause,
@@ -59,7 +46,6 @@ import {
59
46
  jobSteer,
60
47
  jobWatch,
61
48
  jobsList,
62
- json,
63
49
  labelCreate,
64
50
  labelDelete,
65
51
  labelList,
@@ -69,13 +55,11 @@ import {
69
55
  linkRm,
70
56
  linkUpdate,
71
57
  list,
72
- loginHelp,
73
58
  memberAdd,
74
59
  memberList,
75
60
  memberSetRole,
76
61
  milestoneCreate,
77
62
  milestoneList,
78
- orgCredentialsPath,
79
63
  planCreate,
80
64
  planEdit,
81
65
  planHold,
@@ -91,7 +75,6 @@ import {
91
75
  projectList,
92
76
  projectShow,
93
77
  projectUpdate,
94
- readCredentials,
95
78
  readPlanFile,
96
79
  ready,
97
80
  relationAdd,
@@ -113,8 +96,6 @@ import {
113
96
  requireSearchQuery,
114
97
  requireToolsOpts,
115
98
  requiredTeam,
116
- resolveCacheHome,
117
- resolveConfig,
118
99
  resolveTextSources,
119
100
  run,
120
101
  search,
@@ -134,28 +115,50 @@ import {
134
115
  toolsDomains,
135
116
  toolsList,
136
117
  toolsSearch,
137
- trustHost,
138
118
  update,
139
119
  upload,
140
120
  userList,
141
- validOrgId,
142
- validateBaseUrl,
143
121
  validateIdempotencyKey,
144
122
  version,
145
123
  watchStopSignal,
146
- workflowStates,
147
- writeCredentials
148
- } from "./chunk-GVTZKAXC.js";
124
+ workflowStates
125
+ } from "./chunk-CEEQW6ZV.js";
126
+ import {
127
+ login,
128
+ logout,
129
+ whoami
130
+ } from "./chunk-WSDFLMO6.js";
131
+ import {
132
+ Client,
133
+ CommandError,
134
+ DEFAULT_BASE_URL,
135
+ HttpError,
136
+ LocalError,
137
+ TransportError,
138
+ UpgradeRequiredError,
139
+ UsageError,
140
+ clean,
141
+ defaultCredentialsPath,
142
+ errorDetails,
143
+ explainExpertRefusal,
144
+ findRepoConfig,
145
+ isHostTrusted,
146
+ json,
147
+ loginHelp,
148
+ readCredentials,
149
+ resolveCacheHome,
150
+ resolveConfig,
151
+ trustHost
152
+ } from "./chunk-BHBEIT5I.js";
149
153
  import {
150
154
  CLI_VERSION,
151
- OPERATOR_PACKAGE,
152
155
  invokedName,
153
156
  layoutOperatorHelp,
154
157
  named
155
- } from "./chunk-FQWV5MIC.js";
158
+ } from "./chunk-5THYIKLW.js";
156
159
 
157
160
  // src/cli.ts
158
- import { createInterface as createInterface2 } from "readline";
161
+ import { createInterface } from "readline";
159
162
  import { Command, CommanderError, InvalidArgumentError } from "commander";
160
163
 
161
164
  // src/commands/keys.ts
@@ -209,9 +212,9 @@ function selectSlugs(stored, only) {
209
212
  function unknownSlugs(only) {
210
213
  return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
211
214
  }
212
- function withholdOthers(base, requested) {
215
+ function withholdOthers(base2, requested) {
213
216
  const keep = new Set(requested.map((slug) => envVarFor(slug)));
214
- const env = { ...base };
217
+ const env = { ...base2 };
215
218
  for (const name of KEY_ENV_VARS.values()) {
216
219
  if (!keep.has(name)) delete env[name];
217
220
  }
@@ -229,9 +232,9 @@ function toEnv(stored) {
229
232
  }
230
233
  return env;
231
234
  }
232
- function childEnv(base, fetched, override) {
235
+ function childEnv(base2, fetched, override) {
233
236
  const env = {};
234
- for (const [name, value] of Object.entries(base)) {
237
+ for (const [name, value] of Object.entries(base2)) {
235
238
  if (value !== void 0) env[name] = value;
236
239
  }
237
240
  for (const [name, value] of Object.entries(fetched)) {
@@ -240,8 +243,8 @@ function childEnv(base, fetched, override) {
240
243
  }
241
244
  return env;
242
245
  }
243
- function exitCodeFor(code, signal) {
244
- if (code !== null) return code;
246
+ function exitCodeFor(code2, signal) {
247
+ if (code2 !== null) return code2;
245
248
  if (signal !== null) {
246
249
  const number = constants.signals[signal];
247
250
  if (typeof number === "number") return 128 + number;
@@ -295,13 +298,13 @@ async function exec(ctx, opts) {
295
298
  opts.override
296
299
  );
297
300
  const name = clean(program2);
298
- const exitCode = await new Promise((resolve, reject) => {
301
+ const exitCode = await new Promise((resolve3, reject) => {
299
302
  let child;
300
303
  try {
301
304
  child = spawn(program2, args, { stdio: "inherit", env });
302
305
  } catch (error) {
303
- const code = error.code ?? "invalid arguments";
304
- reject(new CommandError(`cannot start ${name}: ${code}`));
306
+ const code2 = error.code ?? "invalid arguments";
307
+ reject(new CommandError(`cannot start ${name}: ${code2}`));
305
308
  return;
306
309
  }
307
310
  const handlers = [];
@@ -331,12 +334,12 @@ async function exec(ctx, opts) {
331
334
  });
332
335
  child.on("error", (error) => {
333
336
  settle(() => {
334
- const code = error.code ?? error.message;
335
- reject(new CommandError(spawned ? `${name} failed: ${code}` : `cannot run ${name}: ${code}`));
337
+ const code2 = error.code ?? error.message;
338
+ reject(new CommandError(spawned ? `${name} failed: ${code2}` : `cannot run ${name}: ${code2}`));
336
339
  });
337
340
  });
338
- child.on("exit", (code, signal) => {
339
- settle(() => resolve(exitCodeFor(code, signal)));
341
+ child.on("exit", (code2, signal) => {
342
+ settle(() => resolve3(exitCodeFor(code2, signal)));
340
343
  });
341
344
  });
342
345
  return { json: { exit_code: exitCode }, human: "", exitCode };
@@ -490,8 +493,8 @@ async function retry(ctx, pr, reason) {
490
493
  response = await ctx.client.post(`${gatePath(pr)}/retry`, body, { actor: false });
491
494
  } catch (error) {
492
495
  if (error instanceof HttpError && error.status === 409) {
493
- const code = error.message.trim().split(/[\s:]+/).pop() ?? "";
494
- throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code] ?? clean(error.message)}`);
496
+ const code2 = error.message.trim().split(/[\s:]+/).pop() ?? "";
497
+ throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code2] ?? clean(error.message)}`);
495
498
  }
496
499
  if (error instanceof HttpError && error.status === 403) {
497
500
  throw new CommandError(`${error.message} \u2014 a retry needs a login with reviews:write`);
@@ -508,1192 +511,1657 @@ async function retry(ctx, pr, reason) {
508
511
  };
509
512
  }
510
513
 
511
- // src/core/auth.ts
512
- import { createHash, randomBytes } from "crypto";
513
- import { execFile } from "child_process";
514
- import { accessSync, constants as fsConstants } from "fs";
515
- import { createServer } from "http";
516
- import { delimiter, join } from "path";
517
- import { createInterface } from "readline";
518
- var DEFAULT_TIMEOUT_MS = 18e4;
519
- var RESPONSE_DRAIN_GRACE_MS = 100;
520
- var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
521
- var MAX_PAGE_RESPONSES = 4;
522
- var loginPageImport;
523
- function loadLoginPage() {
524
- loginPageImport ??= import("./login-page-KGEJFFXR.js");
525
- return loginPageImport;
526
- }
527
- var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
528
- function generateState() {
529
- return randomBytes(24).toString("base64url");
530
- }
531
- function generateVerifier() {
532
- return randomBytes(32).toString("base64url");
533
- }
534
- function challengeFor(verifier) {
535
- return createHash("sha256").update(verifier).digest("base64url");
536
- }
537
- async function login(baseUrl, options = {}) {
538
- const validated = validateBaseUrl(baseUrl);
539
- if (!validated.ok) return { ok: false, message: validated.message };
540
- if (options.org !== void 0 && !validOrgId(options.org)) {
541
- return { ok: false, message: "org must be an organization UUID" };
542
- }
543
- const base = baseUrl.replace(/\/+$/, "");
544
- if (options.noBrowser === true || !browserAvailable(options)) {
545
- return manualLogin(base, options);
546
- }
547
- return loopbackLogin(base, options);
548
- }
549
- function browserAvailable(options) {
550
- if ("openBrowser" in options) return typeof options.openBrowser === "function";
551
- return browserOpener() !== null && displayAvailable(options.env ?? process.env);
552
- }
553
- function findExecutable(name, pathVar = process.env["PATH"]) {
554
- for (const dir of (pathVar ?? "").split(delimiter)) {
555
- if (dir === "") continue;
556
- const candidate = join(dir, name);
557
- try {
558
- accessSync(candidate, fsConstants.X_OK);
559
- return candidate;
560
- } catch {
561
- }
562
- }
563
- return null;
564
- }
565
- function browserOpener() {
566
- const name = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
567
- if (name === null) return null;
568
- return findExecutable(name);
569
- }
570
- function displayAvailable(env) {
571
- const display = env["DISPLAY"];
572
- const remote = isSet(env["SSH_CONNECTION"]) || isSet(env["SSH_TTY"]);
573
- const hasDisplay = isSet(display) || isSet(env["WAYLAND_DISPLAY"]);
574
- const forwarded = typeof display === "string" && display.startsWith("localhost:");
575
- if (process.platform === "darwin") return !remote;
576
- if (process.platform === "linux") return hasDisplay && (!remote || forwarded);
577
- return !remote;
578
- }
579
- function isSet(value) {
580
- return value !== void 0 && value !== "";
581
- }
582
- async function loopbackLogin(baseUrl, options) {
583
- const notify = options.notify ?? defaultNotify;
584
- const state2 = generateState();
585
- const verifier = generateVerifier();
586
- const challenge = challengeFor(verifier);
587
- const server = createServer();
588
- const port = await new Promise((resolve) => {
589
- server.once("error", () => resolve(null));
590
- server.listen(0, "127.0.0.1", () => {
591
- const address = server.address();
592
- resolve(typeof address === "object" && address !== null ? address.port : null);
593
- });
594
- });
595
- if (port === null) {
596
- return {
597
- ok: false,
598
- message: "could not open a loopback port for the browser handoff"
599
- };
600
- }
514
+ // src/commands/code/index.ts
515
+ import { resolve as resolve2 } from "path";
516
+ import { statSync } from "fs";
517
+
518
+ // src/core/code/http.ts
519
+ import { setTimeout as delay } from "timers/promises";
520
+ import { z } from "zod";
521
+ var UUID = z.string().uuid();
522
+ var HASH = z.string().regex(/^[a-f0-9]{64}$/);
523
+ var view = z.object({
524
+ view_id: UUID,
525
+ view_key: z.string(),
526
+ repository_id: UUID,
527
+ active: z.object({
528
+ version: z.number().int(),
529
+ base_commit: z.string(),
530
+ overlay_digest: HASH,
531
+ indexed_at: z.string().nullable().optional()
532
+ }).nullable(),
533
+ pending: z.object({
534
+ version: z.number().int(),
535
+ state: z.string(),
536
+ progress: z.object({ done: z.number().int().nonnegative(), total: z.number().int().nonnegative() }).nullable().optional(),
537
+ error: z.unknown().optional()
538
+ }).nullable()
539
+ });
540
+ var policy = z.object({
541
+ org_id: UUID,
542
+ policy_digest: z.string().min(1).max(256),
543
+ max_file_bytes: z.number().int().positive(),
544
+ max_batch_bytes: z.number().int().positive(),
545
+ include_extensions: z.array(z.string().max(32)).max(100),
546
+ deny_globs: z.array(z.string().max(1024)).max(1e3),
547
+ deny_glob_case: z.literal("ascii_insensitive").optional(),
548
+ content_patterns: z.array(z.string().max(4096)).max(100),
549
+ org_name: z.string().optional()
550
+ });
551
+ var base = z.object({
552
+ state: z.enum(["fetching", "complete", "ready", "failed"]),
553
+ policy_digest: z.string().optional(),
554
+ file_count: z.number().optional(),
555
+ excluded_count: z.number().optional(),
556
+ skipped: z.array(z.object({ path: z.string(), reason: z.string() })).optional(),
557
+ error: z.unknown().optional(),
558
+ retry_after_ms: z.number().nonnegative().optional()
559
+ });
560
+ var opened = z.object({
561
+ sync_id: UUID,
562
+ state: z.string(),
563
+ missing: z.array(HASH),
564
+ rejected: z.array(z.object({ path: z.string(), error: z.string() }))
565
+ });
566
+ var activated = z.object({
567
+ pending_version: z.number().int().positive(),
568
+ state: z.string()
569
+ });
570
+ var syncStatus = z.object({
571
+ id: UUID,
572
+ state: z.enum(["receiving", "queued", "indexing", "active", "superseded", "failed"]),
573
+ version: z.number().int().nonnegative().nullable()
574
+ });
575
+ function decode(schema, data) {
576
+ const result = schema.safeParse(data);
577
+ if (!result.success)
578
+ throw new CommandError(
579
+ "The code index API returned an invalid response; no additional source will be uploaded."
580
+ );
581
+ return result.data;
582
+ }
583
+ function retryAfterMs(value, now = Date.now()) {
584
+ if (value === null) return void 0;
585
+ const seconds = Number(value);
586
+ if (/^\d+(?:\.\d+)?$/.test(value) && Number.isFinite(seconds))
587
+ return seconds * 1e3;
588
+ const date = Date.parse(value);
589
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
590
+ }
591
+ function describeCodeError(error) {
592
+ const code2 = error.body["error"];
593
+ let message = error.message;
594
+ if (code2 === "repository_not_connected")
595
+ message = "repository_not_connected: share this repository through your organization\u2019s Harness GitHub connection before syncing.";
596
+ if (code2 === "code_index_disabled")
597
+ message = "code_index_disabled: code indexing is disabled for this organization; ask a Harness administrator to enable it.";
598
+ if (code2 === "quota_exceeded") {
599
+ const raw = error.body["details"];
600
+ const d = raw && typeof raw === "object" ? raw : {};
601
+ message = `quota_exceeded: your organization\u2019s code index limit ${typeof d["quota"] === "string" ? d["quota"] : "quota"} is reached (limit ${typeof d["limit"] === "number" ? d["limit"] : "unknown"}). Ask a Harness administrator.`;
602
+ }
603
+ if (code2 === "invalid_sync")
604
+ message = "invalid_sync: the client sync request does not match the server contract. Upgrade the client and retry code sync.";
605
+ if (code2 === "overlay_too_large") {
606
+ const raw = error.body["details"];
607
+ const details = raw && typeof raw === "object" ? raw : {};
608
+ const limit = typeof details["limit"] === "number" ? details["limit"] : 2e4;
609
+ message = `overlay_too_large: the overlay exceeds ${limit} paths. Sync against a newer base commit.`;
610
+ }
611
+ if (code2 === "unexpected_blob")
612
+ message = "unexpected_blob: no open sync names this upload. Retry code sync to reconcile the overlay; base blobs are never uploaded.";
613
+ if (code2 === "stale_policy")
614
+ message = "stale_policy: the upload policy kept changing; retry code sync to fetch the policy and rescan.";
615
+ if (code2 === "not_view_owner")
616
+ message = "not_view_owner: this code view belongs to another member; sync your own worktree.";
617
+ if (code2 === "insufficient_scope" || code2 === "missing_scope")
618
+ message = "Code commands need harness:read; sync and base status also need harness:write.";
619
+ return new HttpError(error.status, message, error.body, error.retryAfter);
620
+ }
621
+ async function responseText(response, maxBytes) {
622
+ if (maxBytes === void 0) return response.text();
623
+ if (!response.body) return "";
624
+ const reader = response.body.getReader();
625
+ const chunks = [];
626
+ let size = 0;
601
627
  try {
602
- const query = new URLSearchParams({ port: String(port), state: state2, challenge });
603
- const url = `${baseUrl}/cli/login?${query.toString()}`;
604
- const callbackPromise = awaitCallback(server, state2, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
605
- void loadLoginPage().catch(() => {
606
- loginPageImport = void 0;
607
- });
608
- const openBrowser = options.openBrowser ?? defaultOpenBrowser;
609
- try {
610
- openBrowser(url);
611
- } catch (error) {
612
- const detail = error instanceof Error ? error.message : String(error);
613
- return { ok: false, message: `could not open a browser: ${detail}` };
614
- }
615
- if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
616
- notify(`Opening ${url}`);
617
- notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
618
- const callback = await callbackPromise;
619
- if (!callback.ok) return callback;
620
- try {
621
- const token = await exchangeCode(baseUrl, callback.code, verifier, options);
622
- if (!token.ok) {
623
- await callback.respond(
624
- 400,
625
- "Harness could not exchange the browser authorization. Return to the terminal for details.",
626
- "error"
627
- );
628
- return token;
629
- }
630
- const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
631
- if (result.ok) {
632
- await callback.respond(200, "You can close this browser and return to the terminal.", "success");
633
- } else {
634
- await callback.respond(
635
- 400,
636
- "Harness could not verify and store this login. Return to the terminal for details.",
637
- "error"
638
- );
628
+ while (true) {
629
+ const part = await reader.read();
630
+ if (part.done) break;
631
+ size += part.value.byteLength;
632
+ if (size > maxBytes) {
633
+ void reader.cancel().catch(() => {
634
+ });
635
+ throw new CommandError("The code query response exceeds the 16 MiB size limit; narrow the query.");
639
636
  }
640
- return result;
641
- } catch (error) {
642
- await callback.respond(500, "Harness could not finish the login. Return to the terminal for details.", "error");
643
- const detail = error instanceof Error ? error.message : String(error);
644
- return { ok: false, message: `could not finish the browser login: ${detail}` };
637
+ chunks.push(Buffer.from(part.value));
645
638
  }
646
639
  } finally {
647
- server.close();
648
- const forceClose = setTimeout(
649
- () => server.closeAllConnections(),
650
- RESPONSE_FLUSH_TIMEOUT_MS + RESPONSE_DRAIN_GRACE_MS
651
- );
652
- forceClose.unref();
653
- }
654
- }
655
- function awaitCallback(server, state2, timeoutMs) {
656
- return new Promise((resolve) => {
657
- let phase = "waiting";
658
- let finalResponse = null;
659
- let callbackPageResponses = 0;
660
- const rootPageResponses = /* @__PURE__ */ new Map();
661
- const acceptedResponders = [];
662
- const inFlightResponses = /* @__PURE__ */ new Set();
663
- const trackResponse = (responsePromise) => {
664
- inFlightResponses.add(responsePromise);
665
- void responsePromise.finally(() => inFlightResponses.delete(responsePromise));
666
- return responsePromise;
667
- };
668
- const drainTrackedResponses = async () => {
669
- await new Promise((settle) => setTimeout(settle, RESPONSE_DRAIN_GRACE_MS));
670
- const deadline = Date.now() + RESPONSE_FLUSH_TIMEOUT_MS;
671
- while (inFlightResponses.size > 0 && Date.now() < deadline) {
672
- const remaining = deadline - Date.now();
673
- await Promise.race([
674
- Promise.allSettled([...inFlightResponses]),
675
- new Promise((settle) => setTimeout(settle, remaining))
676
- ]);
677
- }
678
- };
679
- const timer = setTimeout(() => {
680
- if (phase !== "waiting") return;
681
- const timeoutResponse = {
682
- status: 410,
683
- message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
684
- pageState: "error"
685
- };
686
- phase = "terminal";
687
- finalResponse = timeoutResponse;
688
- resolve({
689
- ok: false,
690
- message: "timed out waiting for the browser authorization"
691
- });
692
- void drainTrackedResponses();
693
- }, timeoutMs);
694
- server.once("close", () => {
695
- clearTimeout(timer);
696
- if (phase !== "waiting") return;
697
- phase = "terminal";
698
- resolve({ ok: false, message: "the browser login listener closed before authorization completed" });
699
- });
700
- server.on("request", (request, response) => {
701
- const respond = callbackResponder(response);
702
- const respondWithPage = (status2, message, pageState) => {
703
- if (callbackPageResponses >= MAX_PAGE_RESPONSES) {
704
- respondEmpty(response, 429);
705
- return Promise.resolve();
706
- }
707
- callbackPageResponses += 1;
708
- return respond(status2, message, pageState);
709
- };
710
- const respondAtRoot = (status2, message, pageState) => {
711
- const rootPhase = phase === "terminal" ? "terminal" : "waiting";
712
- const responses = rootPageResponses.get(rootPhase) ?? 0;
713
- if (responses >= MAX_PAGE_RESPONSES) {
714
- respondEmpty(response, 429);
715
- return Promise.resolve();
716
- }
717
- rootPageResponses.set(rootPhase, responses + 1);
718
- return respond(status2, message, pageState);
719
- };
720
- let url;
640
+ reader.releaseLock();
641
+ }
642
+ try {
643
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
644
+ } catch {
645
+ throw new CommandError("The code query response is not valid UTF-8.");
646
+ }
647
+ }
648
+ var HttpCodeApi = class {
649
+ constructor(config, options = {}) {
650
+ this.config = config;
651
+ this.origin = config.baseUrl;
652
+ this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
653
+ this.sleep = options.sleep ?? delay;
654
+ }
655
+ config;
656
+ origin;
657
+ fetchImpl;
658
+ sleep;
659
+ async request(method, path, body, maxResponseBytes) {
660
+ for (let attempt = 0; attempt < 3; attempt++) {
721
661
  try {
722
- url = new URL(request.url ?? "/", "http://127.0.0.1");
723
- } catch {
724
- respondEmpty(response, 404);
725
- return;
726
- }
727
- const path = url.pathname.replace(/\/+$/, "") || "/";
728
- if (path !== "/callback") {
729
- if (path === "/") {
730
- const rootResponse = phase === "terminal" && finalResponse ? finalResponse : {
731
- status: 200,
732
- message: "Waiting for the Harness browser authorization to finish.",
733
- pageState: "waiting"
734
- };
735
- void trackResponse(
736
- respondAtRoot(rootResponse.status, rootResponse.message, rootResponse.pageState)
737
- );
738
- } else {
739
- respondEmpty(response, path === "/favicon.ico" ? 204 : 404);
740
- }
741
- return;
742
- }
743
- const matchingState = url.searchParams.get("state") === state2;
744
- if (!matchingState) {
745
- respondEmpty(response, 400);
746
- return;
747
- }
748
- if (phase === "terminal") {
749
- if (!url.searchParams.has("code") && !url.searchParams.has("error")) {
750
- respondEmpty(response, 400);
751
- return;
752
- }
753
- const result = finalResponse ?? {
754
- status: 410,
755
- message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
756
- pageState: "error"
662
+ const headers = {
663
+ authorization: `Bearer ${this.config.token}`,
664
+ "x-harness-cli-version": CLI_VERSION
757
665
  };
758
- void trackResponse(respondWithPage(result.status, result.message, result.pageState));
759
- return;
760
- }
761
- if (phase === "processing") {
762
- if (url.searchParams.has("error")) {
763
- respondEmpty(response, 409);
764
- return;
765
- }
766
- const duplicateCode = url.searchParams.get("code");
767
- if (duplicateCode === null || duplicateCode === "") {
768
- respondEmpty(response, 400);
769
- return;
770
- }
771
- if (acceptedResponders.length >= MAX_PAGE_RESPONSES) {
772
- respondEmpty(response, 429);
773
- return;
774
- }
775
- acceptedResponders.push(respondWithPage);
776
- return;
777
- }
778
- if (url.searchParams.has("error")) {
779
- const denialResponse = {
780
- status: 400,
781
- message: "Browser authorization was denied. Return to the terminal to try again.",
782
- pageState: "error"
783
- };
784
- phase = "terminal";
785
- finalResponse = denialResponse;
786
- clearTimeout(timer);
787
- void (async () => {
788
- await trackResponse(respondWithPage(denialResponse.status, denialResponse.message, denialResponse.pageState));
789
- await drainTrackedResponses();
790
- resolve({ ok: false, message: "browser authorization was denied" });
791
- })();
792
- return;
793
- }
794
- const code = url.searchParams.get("code");
795
- if (code !== null && code !== "") {
796
- phase = "processing";
797
- acceptedResponders.push(respondWithPage);
798
- clearTimeout(timer);
799
- resolve({
800
- ok: true,
801
- code,
802
- respond: async (status2, message, pageState) => {
803
- finalResponse = { status: status2, message, pageState };
804
- phase = "terminal";
805
- const responders = acceptedResponders.splice(0);
806
- await Promise.all(
807
- responders.map((acceptedRespond) => trackResponse(acceptedRespond(status2, message, pageState)))
808
- );
809
- await drainTrackedResponses();
666
+ const encoded = body === void 0 ? void 0 : Buffer.isBuffer(body) ? body : JSON.stringify(body);
667
+ if (body !== void 0)
668
+ headers["content-type"] = Buffer.isBuffer(body) ? "application/octet-stream" : "application/json";
669
+ const response = await this.fetchImpl(
670
+ new URL(this.origin + "/api/v1/code" + path),
671
+ {
672
+ method,
673
+ headers,
674
+ body: encoded,
675
+ signal: AbortSignal.timeout(3e4),
676
+ redirect: "manual"
677
+ }
678
+ );
679
+ const retryAfter = retryAfterMs(response.headers.get("retry-after"));
680
+ const text = await responseText(response, maxResponseBytes);
681
+ let json2 = {};
682
+ if (text) {
683
+ try {
684
+ const parsed = JSON.parse(text);
685
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
686
+ json2 = parsed;
687
+ else if (response.ok)
688
+ throw new CommandError(
689
+ "The code index API returned an invalid JSON envelope."
690
+ );
691
+ } catch (error) {
692
+ if (response.ok)
693
+ throw error instanceof CommandError ? error : new CommandError(
694
+ "The code index API returned a non-JSON body."
695
+ );
810
696
  }
811
- });
812
- } else {
813
- respondEmpty(response, 400);
814
- }
815
- });
816
- });
817
- }
818
- function callbackResponder(response) {
819
- return async (status2, message, state2) => {
820
- if (response.writableEnded || response.destroyed) return;
821
- return new Promise((resolve) => {
822
- let flushTimer;
823
- const armFlushTimeout = () => {
824
- if (flushTimer !== void 0) clearTimeout(flushTimer);
825
- flushTimer = setTimeout(() => {
826
- response.destroy();
827
- settle();
828
- }, RESPONSE_FLUSH_TIMEOUT_MS);
829
- flushTimer.unref();
830
- };
831
- const settle = () => {
832
- if (flushTimer !== void 0) clearTimeout(flushTimer);
833
- response.off("finish", settle);
834
- response.off("close", settle);
835
- response.off("error", settle);
836
- response.off("drain", armFlushTimeout);
837
- resolve();
838
- };
839
- response.once("finish", settle);
840
- response.once("close", settle);
841
- response.once("error", settle);
842
- response.on("drain", armFlushTimeout);
843
- void loadLoginPage().then(({ loginPage }) => {
844
- if (response.writableEnded || response.destroyed) {
845
- settle();
846
- return;
847
697
  }
848
- try {
849
- const html = loginPage(message, state2);
850
- response.writeHead(status2, {
851
- "cache-control": "no-store",
852
- connection: "close",
853
- "content-length": String(Buffer.byteLength(html)),
854
- "content-type": "text/html; charset=utf-8",
855
- "x-content-type-options": "nosniff"
856
- });
857
- armFlushTimeout();
858
- response.end(html);
859
- } catch {
860
- response.destroy();
861
- settle();
698
+ if (!response.ok) {
699
+ const message = typeof json2["message"] === "string" ? json2["message"] : `Code index request refused (HTTP ${response.status}).`;
700
+ if (response.status === 426)
701
+ throw new UpgradeRequiredError(message, json2);
702
+ throw new HttpError(
703
+ response.status,
704
+ message,
705
+ json2,
706
+ response.headers.get("retry-after")
707
+ );
862
708
  }
863
- }).catch(() => {
864
- response.destroy();
865
- settle();
866
- });
867
- });
868
- };
869
- }
870
- function respondEmpty(response, status2) {
871
- response.once("error", () => void 0);
872
- try {
873
- response.writeHead(status2, {
874
- "cache-control": "no-store",
875
- connection: "close",
876
- "x-content-type-options": "nosniff"
877
- });
878
- response.end();
879
- } catch {
880
- response.destroy();
881
- }
882
- }
883
- async function exchangeCode(baseUrl, code, verifier, options) {
884
- try {
885
- const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
886
- const token = body.data?.token;
887
- const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
888
- if (typeof token === "string" && token !== "") return { ok: true, token, kind };
889
- return {
890
- ok: false,
891
- message: "the login-code exchange returned no token \u2014 aborting"
892
- };
893
- } catch (error) {
894
- if (error instanceof HttpError) {
895
- return {
896
- ok: false,
897
- message: `the login-code exchange failed: ${error.message}`
898
- };
899
- }
900
- if (error instanceof TransportError) {
901
- return {
902
- ok: false,
903
- message: `the login-code exchange failed: cannot reach the API (${error.message})`
904
- };
905
- }
906
- throw error;
907
- }
908
- }
909
- async function manualLogin(baseUrl, options) {
910
- const notify = options.notify ?? defaultNotify;
911
- const prompt = options.prompt ?? defaultPrompt;
912
- if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
913
- notify(
914
- `Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below (an outside expert pastes the pxc_ credential minted in their expert view instead).`
915
- );
916
- const token = (await prompt()).trim();
917
- if (token === "") return { ok: false, message: "no token entered" };
918
- return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
919
- }
920
- async function verifyAndStore(baseUrl, token, options, received) {
921
- const defaultPath = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
922
- const path = options.org === void 0 ? defaultPath : orgCredentialsPath(defaultPath, baseUrl, options.org);
923
- const client = bareClient(baseUrl, token, options);
924
- const expert = isExpertToken(token);
925
- let kind = received.kind;
926
- try {
927
- if (expert) {
928
- if (options.org !== void 0) return { ok: false, message: "cannot verify an outside expert credential against a repo org \u2014 nothing was stored" };
929
- await client.get(EXPERT_ASSIGNMENTS_PATH);
930
- } else {
931
- const described = isLoginCredential(token) ? await describeSession(client) : null;
932
- if (described !== null) kind = described.session.kind ?? kind;
933
- if (options.org !== void 0 && (typeof described?.session.org?.["id"] !== "string" || described.session.org["id"].toLowerCase() !== options.org.toLowerCase())) {
934
- const revoked = received.minted && await revokeSession(client, token);
935
- return { ok: false, message: `the received token does not verify organization ${options.org} \u2014 nothing was stored` + (revoked ? ", and the token was revoked" : "") };
709
+ return { data: json2["data"], meta: json2["meta"], retryAfter };
710
+ } catch (error) {
711
+ if (error instanceof UpgradeRequiredError || error instanceof CommandError)
712
+ throw error;
713
+ const transient = error instanceof HttpError ? [429, 500, 502, 503, 504].includes(error.status) : true;
714
+ if (!transient || attempt === 2) {
715
+ if (error instanceof HttpError) throw error;
716
+ throw new TransportError(
717
+ "Code index request did not complete. Retry this command with the same worktree to resume."
718
+ );
719
+ }
720
+ const backoff = error instanceof HttpError ? retryAfterMs(error.retryAfter) : void 0;
721
+ const wait = backoff ?? 250 * 2 ** attempt;
722
+ if (wait > 3e5)
723
+ throw error instanceof HttpError ? error : new TransportError(
724
+ "The code index retry backoff is too long; retry later."
725
+ );
726
+ await this.sleep(wait);
936
727
  }
937
- if (described === null) await client.get("/api/v1/teams");
938
728
  }
939
- } catch (error) {
940
- if (error instanceof HttpError) {
941
- const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
942
- const revoked = received.minted && await revokeSession(client, token);
943
- return {
944
- ok: false,
945
- message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
946
- };
729
+ throw new CommandError("Code index retry limit exceeded.");
730
+ }
731
+ async query(id, operation, p) {
732
+ const root = `/views/${encodeURIComponent(id)}`;
733
+ if (operation === "affected") {
734
+ const result2 = await this.request("POST", root + "/affected", {
735
+ symbol_ids: p.symbols ?? [],
736
+ paths: p.files ?? [],
737
+ ...p.depth !== void 0 ? { depth: p.depth } : {},
738
+ ...p.includeAmbiguous !== void 0 ? { include_ambiguous: p.includeAmbiguous } : {},
739
+ ...p.includeTypes !== void 0 ? { include_types: p.includeTypes } : {}
740
+ }, 16 * 1024 * 1024);
741
+ return { data: result2.data, meta: result2.meta };
947
742
  }
948
- if (error instanceof TransportError) {
949
- return {
950
- ok: false,
951
- message: `could not verify the token: cannot reach the API (${error.message}) \u2014 nothing was stored`
952
- };
743
+ const path = operation === "explain" ? root + `/symbols/${encodeURIComponent(p.symbol)}` : root + (operation === "search" ? "/symbols" : "/context");
744
+ const params = new URLSearchParams();
745
+ for (const [key, value] of Object.entries(operation === "search" ? { q: p.q, kind: p.kind, path_prefix: p.pathPrefix, limit: p.limit } : operation === "explain" ? { budget_tokens: p.budgetTokens } : { q: p.q, symbol_id: p.symbol, budget_tokens: p.budgetTokens })) {
746
+ if (value !== void 0) params.set(key, String(value));
953
747
  }
954
- throw error;
748
+ const result = await this.request("GET", path + (params.size ? "?" + params.toString() : ""), void 0, 16 * 1024 * 1024);
749
+ return { data: result.data, meta: result.meta };
955
750
  }
956
- const stored = readCredentials(path);
957
- const superseded = stored !== null && stored.token !== token ? stored : null;
958
- try {
959
- writeCredentials(path, baseUrl, token);
960
- } catch (error) {
961
- if (received.minted) await revokeSession(client, token);
962
- const detail = error instanceof Error ? error.message : String(error);
963
- return {
964
- ok: false,
965
- message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : received.minted ? "" : " (the pasted token was not revoked and stays valid)")
966
- };
751
+ async getPolicy() {
752
+ return decode(policy, (await this.request("GET", "/policy")).data);
967
753
  }
968
- let supersededNote = "";
969
- if (superseded !== null) {
970
- await revokeSession(bareClient(superseded.url, superseded.token, options), superseded.token);
971
- if (isExpertToken(superseded.token)) {
972
- supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
973
- }
754
+ async putRepository(repoKey) {
755
+ return decode(
756
+ z.object({ repository_id: UUID, repo_key: z.string() }),
757
+ (await this.request("PUT", "/repositories", { repo_key: repoKey })).data
758
+ );
974
759
  }
975
- const name = invokedName();
976
- return {
977
- ok: true,
978
- json: { url: baseUrl, credentials: path, kind },
979
- human: (expert ? `Stored an outside expert's pxc_ credential for ${baseUrl} at ${path} (it lasts at most 30 days; revoke it in your expert view \u2014 ${name} logout only forgets it). Commands now act as the expert: only assignments, conditions and gate work until you log in again.
980
- ` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; ${name} logout revokes it).
981
- `) + kindNote(kind) + supersededNote
982
- };
983
- }
984
- function kindNote(kind) {
985
- if (kind === "operator-cli") {
986
- return "This credential is Operator-only (operator-cli): Operator commands work; issue, plan and gate commands are refused.\n";
760
+ async getBase(repositoryId, commit, skipped) {
761
+ const r = await this.request(
762
+ "GET",
763
+ `/repositories/${encodeURIComponent(repositoryId)}/bases/${encodeURIComponent(commit)}${skipped ? "?skipped=true" : ""}`
764
+ );
765
+ const data = decode(base, r.data);
766
+ const wait = Math.max(data.retry_after_ms ?? 0, r.retryAfter ?? 0);
767
+ return wait ? { ...data, retry_after_ms: wait } : data;
768
+ }
769
+ async putView(repositoryId, key) {
770
+ return decode(
771
+ view,
772
+ (await this.request(
773
+ "PUT",
774
+ `/repositories/${encodeURIComponent(repositoryId)}/views/${encodeURIComponent(key)}`,
775
+ {}
776
+ )).data
777
+ );
987
778
  }
988
- if (kind === "harness-cli") return "This credential has full access (harness-cli).\n";
989
- return "";
990
- }
991
- async function describeSession(client) {
992
- try {
993
- const { envelope, apiVersion } = await client.getVersioned(SESSION_PATH);
994
- return { session: sessionOf(envelope.data), apiVersion };
995
- } catch (error) {
996
- if (error instanceof HttpError && (error.status === 404 || error.status === 405)) return null;
997
- throw error;
779
+ async getView(id) {
780
+ return decode(
781
+ view,
782
+ (await this.request("GET", `/views/${encodeURIComponent(id)}`)).data
783
+ );
998
784
  }
999
- }
1000
- function sessionOf(data) {
1001
- if (!isRecord3(data)) return NO_SESSION;
1002
- const text = (value) => typeof value === "string" ? value : null;
1003
- return {
1004
- kind: text(data["kind"]),
1005
- expires_at: text(data["expires_at"]),
1006
- scopes: Array.isArray(data["scopes"]) ? data["scopes"].filter((scope) => typeof scope === "string") : null,
1007
- user: isRecord3(data["user"]) ? data["user"] : null,
1008
- org: isRecord3(data["org"]) ? data["org"] : null
1009
- };
1010
- }
1011
- async function revokeSession(client, token) {
1012
- if (isExpertToken(token)) return false;
1013
- return client.delete("/api/v1/session").then(() => true).catch(() => false);
1014
- }
1015
- async function logout(options = {}) {
1016
- const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
1017
- const stored = readCredentials(path);
1018
- if (stored === null) {
1019
- const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
1020
- return {
1021
- ok: false,
1022
- message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 " + (isExpertToken(envToken.trim()) ? "unset it, or revoke the pxc_ credential in your expert view." : "unset it, or disable the token in the admin UI.")
1023
- };
785
+ async openSync(id, body) {
786
+ return decode(
787
+ opened,
788
+ (await this.request("POST", `/views/${encodeURIComponent(id)}/syncs`, {
789
+ ...body
790
+ })).data
791
+ );
1024
792
  }
1025
- const expert = isExpertToken(stored.token);
1026
- const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
1027
- const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
793
+ async putBlob(hash2, bytes) {
794
+ HASH.parse(hash2);
795
+ await this.request("PUT", `/blobs/${hash2}`, bytes);
796
+ }
797
+ async getSync(viewId, syncId) {
798
+ return decode(syncStatus, (await this.request(
799
+ "GET",
800
+ `/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}`
801
+ )).data);
802
+ }
803
+ async activate(viewId, syncId) {
804
+ return decode(
805
+ activated,
806
+ (await this.request(
807
+ "POST",
808
+ `/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}/activate`,
809
+ {}
810
+ )).data
811
+ );
812
+ }
813
+ };
814
+
815
+ // src/core/code/sync.ts
816
+ import { randomUUID as randomUUID2 } from "crypto";
817
+ import { setTimeout as delay2 } from "timers/promises";
818
+
819
+ // src/core/code/git.ts
820
+ import { execFile } from "child_process";
821
+ import { promisify } from "util";
822
+ import { realpath } from "fs/promises";
823
+ var run2 = promisify(execFile);
824
+ var MAX_GIT_BYTES = 16 * 1024 * 1024;
825
+ async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES) {
1028
826
  try {
1029
- deleteCredentials(path);
827
+ return (await run2(
828
+ "git",
829
+ [
830
+ "--no-optional-locks",
831
+ "--no-pager",
832
+ "-c",
833
+ "core.fsmonitor=false",
834
+ ...args
835
+ ],
836
+ { cwd, maxBuffer, timeout: 3e4, encoding: "buffer" }
837
+ )).stdout;
1030
838
  } catch (error) {
1031
- const detail = error instanceof Error ? error.message : String(error);
1032
- return {
1033
- ok: false,
1034
- message: `could not remove ${path} (${detail}) \u2014 delete it manually.` + (revoked ? " The token itself was revoked." : "")
1035
- };
839
+ const code2 = error.code;
840
+ if (code2 === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || error instanceof Error && /maxBuffer/i.test(error.message))
841
+ throw new LocalError(
842
+ "Git returned more than 16 MiB of metadata; narrow the worktree before code sync."
843
+ );
844
+ throw new LocalError(
845
+ "Cannot read this Git working tree. Run code commands inside a committed repository with an origin remote."
846
+ );
1036
847
  }
1037
- return {
1038
- ok: true,
1039
- json: { logged_out: true, revoked },
1040
- human: expert ? `Forgot the outside expert's pxc_ credential for ${stored.url}. It stays valid until it expires: revoke it in your expert view to end it now.
1041
- ` : `Logged out of ${stored.url}${note}.
1042
- `
1043
- };
1044
848
  }
1045
- async function whoami(config, credentialSource) {
1046
- const client = new Client(config);
1047
- const expert = isExpertToken(config.token);
1048
- let probe;
1049
- let session = null;
1050
- let serverVersion;
1051
- try {
1052
- const described2 = isLoginCredential(config.token) ? await describeSession(client) : null;
1053
- if (described2 !== null) {
1054
- session = described2.session;
1055
- serverVersion = described2.apiVersion;
1056
- } else {
1057
- const answered = await client.getVersioned(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats");
1058
- probe = answered.envelope.data;
1059
- serverVersion = answered.apiVersion;
1060
- }
1061
- } catch (error) {
1062
- if (error instanceof HttpError) {
1063
- return {
1064
- ok: false,
1065
- message: expert ? `signed in to ${config.baseUrl} with an outside expert's pxc_ credential (${credentialSource}), but it was refused: ${error.message} (HTTP ${error.status})${error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : ""}` : `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
1066
- };
849
+ function decodeUtf8(value, message) {
850
+ const decoded = value.toString("utf8");
851
+ if (!Buffer.from(decoded, "utf8").equals(value)) throw new LocalError(message);
852
+ return decoded;
853
+ }
854
+ function decodeGitPaths(outputs) {
855
+ const paths = [];
856
+ for (const output of outputs) {
857
+ if (output.length > 0 && output.at(-1) !== 0)
858
+ throw new LocalError("Code sync received an incomplete Git path record.");
859
+ let start = 0;
860
+ for (let end = output.indexOf(0, start); end !== -1; end = output.indexOf(0, start)) {
861
+ if (end === start)
862
+ throw new LocalError("Code sync received an empty Git path record.");
863
+ const path = decodeUtf8(
864
+ output.subarray(start, end),
865
+ "Code sync cannot safely index a Git path with invalid UTF-8."
866
+ );
867
+ if (/[\x00-\x1f\x7f-\x9f]/.test(path))
868
+ throw new LocalError(
869
+ "Code sync cannot safely index a Git path with control characters."
870
+ );
871
+ paths.push(path);
872
+ start = end + 1;
1067
873
  }
1068
- throw error;
1069
874
  }
1070
- let seat = null;
1071
- if (expert) {
1072
- seat = expertSeat(probe);
1073
- if (seat === null) {
1074
- return {
1075
- ok: false,
1076
- message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
1077
- };
875
+ return paths;
876
+ }
877
+ function decodeGitLine(output, message) {
878
+ if (output.length === 0 || output.at(-1) !== 10)
879
+ throw new LocalError(message);
880
+ return decodeUtf8(output.subarray(0, -1), message);
881
+ }
882
+ function assertDistinctPathIdentities(paths) {
883
+ const known = /* @__PURE__ */ new Map();
884
+ for (const path of paths) {
885
+ let original = "";
886
+ let folded = "";
887
+ for (const part of path.split("/")) {
888
+ original += `/${part}`;
889
+ folded += `/${part.normalize("NFC").toLocaleLowerCase("en-US").toLocaleUpperCase("en-US").normalize("NFC")}`;
890
+ const previous = known.get(folded);
891
+ if (previous !== void 0 && previous !== original)
892
+ throw new LocalError(
893
+ "Code sync cannot safely distinguish colliding Git path identities."
894
+ );
895
+ known.set(folded, original);
1078
896
  }
1079
897
  }
1080
- serverVersion ??= "unknown";
1081
- const hostSourceLabels = {
1082
- env: "HARNESS_API_URL",
1083
- "repo-config": ".harness-cli/config.json",
1084
- "stored-login": "stored login",
1085
- default: "workspace default"
1086
- };
1087
- const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
1088
- const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
1089
- Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
1090
- `;
1091
- const described = session ?? NO_SESSION;
1092
- return {
1093
- ok: true,
1094
- json: {
1095
- url: config.baseUrl,
1096
- source: credentialSource,
1097
- verified: true,
1098
- server_version: serverVersion,
1099
- host_source: config.hostSource,
1100
- kind: described.kind,
1101
- expires_at: described.expires_at,
1102
- scopes: described.scopes,
1103
- user: described.user,
1104
- org: described.org,
1105
- expert: seat
1106
- },
1107
- human: `Host: ${config.baseUrl}${via}
1108
- Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
1109
- ` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
1110
- `
1111
- };
1112
898
  }
1113
- function kindLabel(kind) {
1114
- if (kind === null) return "";
1115
- if (kind === "harness-cli") return ", harness-cli (full access)";
1116
- if (kind === "operator-cli") return ", operator-cli (Operator only)";
1117
- return `, ${clean(kind)}`;
1118
- }
1119
- function sessionLines(session) {
1120
- let lines = "";
1121
- const user2 = session.user;
1122
- if (user2 !== null) {
1123
- const name = clean(user2["name"]);
1124
- const email = clean(user2["email"]);
1125
- lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
1126
- `;
1127
- }
1128
- const org = session.org;
1129
- if (org !== null) {
1130
- const name = clean(org["name"]);
1131
- const slug = clean(org["slug"]);
1132
- lines += `Org: ${slug === "" || slug === name ? name : `${name} (${slug})`}
1133
- `;
1134
- }
1135
- if (session.expires_at !== null) lines += `Expires: ${clean(session.expires_at)}
1136
- `;
1137
- if (session.kind === "operator-cli") {
1138
- lines += "Reaches: Operator commands only; issue, plan and gate commands are refused\n";
899
+ async function identity(cwd) {
900
+ const rootRecord = decodeGitLine(
901
+ await gitBytes(cwd, ["rev-parse", "--show-toplevel"]),
902
+ "Cannot resolve the Git working tree root safely."
903
+ );
904
+ if (/[\x00-\x1f\x7f-\x9f]/.test(rootRecord))
905
+ throw new LocalError("Cannot resolve the Git working tree root safely.");
906
+ const root = await realpath(rootRecord);
907
+ const commit = decodeGitLine(
908
+ await gitBytes(root, ["rev-parse", "--verify", "HEAD^{commit}"]),
909
+ "Cannot resolve the repository HEAD commit."
910
+ );
911
+ const remote = decodeGitLine(
912
+ await gitBytes(root, ["remote", "get-url", "origin"]),
913
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
914
+ );
915
+ if (/[\x00-\x1f\x7f-\x9f]/.test(remote))
916
+ throw new LocalError(
917
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
918
+ );
919
+ const scp = /^(?:git@)?github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(remote);
920
+ let parts;
921
+ if (scp) parts = [scp[1], scp[2]];
922
+ else {
923
+ try {
924
+ const url = new URL(remote);
925
+ if (["https:", "ssh:", "git:"].includes(url.protocol) && url.hostname.toLowerCase() === "github.com" && !url.password && !url.search && !url.hash && (!url.port || url.protocol === "ssh:")) {
926
+ parts = url.pathname.replace(/^\//, "").replace(/\/$/, "").replace(/\.git$/, "").split("/");
927
+ }
928
+ } catch {
929
+ }
1139
930
  }
1140
- return lines;
1141
- }
1142
- function expertSeat(data) {
1143
- if (!isRecord3(data) || !isRecord3(data["expert"])) return null;
1144
- const plan2 = isRecord3(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
1145
- const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord3).length : 0;
1146
- return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
1147
- }
1148
- function isRecord3(value) {
1149
- return typeof value === "object" && value !== null && !Array.isArray(value);
1150
- }
1151
- function bareClient(baseUrl, token, options) {
1152
- const config = {
1153
- baseUrl: baseUrl.replace(/\/+$/, ""),
1154
- token,
1155
- actor: null,
1156
- team: null,
1157
- cacheDir: ""
931
+ if (!parts || parts.length !== 2 || parts.some((p) => !p || !/^[\w.-]+$/.test(p) || p === "." || p === ".."))
932
+ throw new LocalError(
933
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
934
+ );
935
+ if (!/^[0-9a-f]{40,64}$/.test(commit))
936
+ throw new LocalError("Cannot resolve the repository HEAD commit.");
937
+ return {
938
+ root,
939
+ commit,
940
+ repoKey: `github.com/${parts.join("/").toLowerCase()}`
1158
941
  };
1159
- if (options.fetchImpl !== void 0) config.fetchImpl = options.fetchImpl;
1160
- return new Client(config);
1161
942
  }
1162
- function defaultNotify(line) {
1163
- process.stderr.write(named(line + "\n"));
1164
- }
1165
- function defaultPrompt() {
1166
- const readline = createInterface({
1167
- input: process.stdin,
1168
- output: process.stderr
1169
- });
1170
- return new Promise((resolve) => {
1171
- readline.question("Token: ", (answer) => {
1172
- readline.close();
1173
- resolve(answer);
1174
- });
1175
- });
1176
- }
1177
- function defaultOpenBrowser(url) {
1178
- const opener = browserOpener();
1179
- if (opener !== null) {
1180
- execFile(opener, [url], () => void 0);
1181
- }
943
+ async function changedPaths(root, commit) {
944
+ const changed = await gitBytes(root, [
945
+ "diff",
946
+ "--name-only",
947
+ "-z",
948
+ "--no-renames",
949
+ "--no-ext-diff",
950
+ "--no-textconv",
951
+ commit,
952
+ "--"
953
+ ]);
954
+ const untracked = await gitBytes(root, [
955
+ "ls-files",
956
+ "--others",
957
+ "--exclude-standard",
958
+ "-z"
959
+ ]);
960
+ return [
961
+ ...new Set(decodeGitPaths([changed, untracked]))
962
+ ].sort(
963
+ (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
964
+ );
1182
965
  }
1183
966
 
1184
- // src/core/stdio.ts
1185
- var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
1186
- function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
1187
- for (const stream of streams) {
1188
- stream.on("error", (error) => {
1189
- if (!BROKEN_PIPE_CODES.has(error.code ?? "")) throw error;
1190
- });
1191
- }
1192
- }
967
+ // src/core/code/scan.ts
968
+ import { constants as constants2 } from "fs";
969
+ import { open } from "fs/promises";
970
+ import { join } from "path";
971
+ import { createHash } from "crypto";
972
+ import { release } from "os";
1193
973
 
1194
- // src/core/upgrade.ts
1195
- import { spawn as spawn2, spawnSync } from "child_process";
1196
- import {
1197
- accessSync as accessSync2,
1198
- constants as constants2,
1199
- existsSync,
1200
- linkSync,
1201
- mkdirSync,
1202
- readFileSync,
1203
- realpathSync,
1204
- renameSync,
1205
- rmSync,
1206
- statSync,
1207
- writeFileSync
1208
- } from "fs";
1209
- import { basename, dirname, join as join2, parse, sep } from "path";
1210
- import { fileURLToPath } from "url";
1211
- var PACKAGE_NAME = "@allocator-one/harness-cli";
1212
- var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
1213
- var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
1214
- function upgradeInstallSpec(install) {
1215
- return `${install.packageName ?? PACKAGE_NAME}@latest`;
1216
- }
1217
- var UPDATE_CHECK_COMMAND = "__update-check";
1218
- var REEXEC_SENTINEL = "HARNESS_UPGRADED";
1219
- function latestUrl(packageName = PACKAGE_NAME) {
1220
- return `https://registry.npmjs.org/${packageName}/latest`;
1221
- }
1222
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1223
- var REGISTRY_TIMEOUT_MS = 5e3;
1224
- var NPM_ROOT_TIMEOUT_MS = 15e3;
1225
- var INSTALL_TIMEOUT_MS = 12e4;
1226
- var LOCK_STALE_MS = NPM_ROOT_TIMEOUT_MS + INSTALL_TIMEOUT_MS + 6e4;
1227
- function detectInstall(from = fileURLToPath(import.meta.url)) {
1228
- const unknown = {
1229
- kind: "unknown",
1230
- packageRoot: null,
1231
- nodeModules: null
1232
- };
1233
- if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
1234
- const found = findPackageRoot(from);
1235
- if (found === null) return unknown;
1236
- const { packageRoot, packageName } = found;
1237
- if (existsSync(join2(packageRoot, ".git"))) {
1238
- return { kind: "checkout", packageRoot, packageName, nodeModules: null };
1239
- }
1240
- const nodeModules = enclosingNodeModules(packageRoot);
1241
- if (nodeModules === null) return { ...unknown, packageRoot, packageName };
1242
- const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
1243
- return { kind, packageRoot, packageName, nodeModules };
1244
- }
1245
- function findPackageRoot(from) {
1246
- let dir = dirname(from);
1247
- const { root } = parse(dir);
1248
- for (; ; ) {
1249
- const name = packageNameAt(join2(dir, "package.json"));
1250
- if (name !== null && PACKAGE_NAMES.includes(name))
1251
- return { packageRoot: dir, packageName: name };
1252
- if (dir === root) return null;
1253
- dir = dirname(dir);
1254
- }
1255
- }
1256
- function packageNameAt(path) {
1257
- try {
1258
- const parsed = JSON.parse(readFileSync(path, "utf8"));
1259
- if (typeof parsed !== "object" || parsed === null) return null;
1260
- const name = parsed["name"];
1261
- return typeof name === "string" ? name : null;
1262
- } catch {
974
+ // src/core/code/policy.ts
975
+ import { isUtf8 } from "buffer";
976
+ import { extname } from "path";
977
+
978
+ // src/core/code/glob.ts
979
+ var MAX_PATTERN_BYTES = 256;
980
+ var MAX_PATTERN_STARS = 8;
981
+ var MAX_PATTERNS = 1e3;
982
+ function compileIgnorePatterns(lines, warnings) {
983
+ const patterns = [];
984
+ for (const [line, raw] of lines.entries()) {
985
+ const trimmed = raw.trim();
986
+ if (!trimmed || trimmed.startsWith("#")) continue;
987
+ if (trimmed.startsWith("!")) {
988
+ warnings.push(
989
+ `Ignoring .harnesscodeignore negation on line ${line + 1}; code-index patterns do not support !.`
990
+ );
991
+ continue;
992
+ }
993
+ if (trimmed.includes("\\")) {
994
+ warnings.push(
995
+ `The .harnesscodeignore backslash on line ${line + 1} is literal and never matches a normalized source path.`
996
+ );
997
+ }
998
+ if (Buffer.byteLength(trimmed) > MAX_PATTERN_BYTES || (trimmed.match(/\*/g)?.length ?? 0) > MAX_PATTERN_STARS) {
999
+ warnings.push(
1000
+ "Ignoring .harnesscodeignore pattern outside the code-index size limits."
1001
+ );
1002
+ continue;
1003
+ }
1004
+ if (patterns.length === MAX_PATTERNS) {
1005
+ warnings.push(
1006
+ "Ignoring .harnesscodeignore patterns after the first 1,000 valid entries."
1007
+ );
1008
+ break;
1009
+ }
1010
+ const pattern = compilePattern(trimmed);
1011
+ if (pattern) patterns.push(pattern);
1012
+ }
1013
+ return patterns;
1014
+ }
1015
+ function patternLimitError() {
1016
+ return new Error("Code upload policy contains an invalid path pattern.");
1017
+ }
1018
+ function escapeRegex(pattern) {
1019
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1020
+ }
1021
+ function globBody(pattern) {
1022
+ return escapeRegex(pattern).replaceAll("\\*\\*/", "(?:.*/)?").replaceAll("/\\*\\*", "/.*").replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]");
1023
+ }
1024
+ function compilePattern(raw, {
1025
+ caseInsensitive = false,
1026
+ mode = "ignore",
1027
+ warnings,
1028
+ maxPatternBytes = MAX_PATTERN_BYTES,
1029
+ maxStars = MAX_PATTERN_STARS,
1030
+ rejectUnsupported = false
1031
+ } = {}) {
1032
+ let pattern = mode === "served" ? raw : raw.trim();
1033
+ if (pattern === "") return null;
1034
+ if (pattern.startsWith("#")) {
1035
+ if (rejectUnsupported) throw patternLimitError();
1263
1036
  return null;
1264
1037
  }
1265
- }
1266
- function versionAt(packageRoot) {
1267
- try {
1268
- const parsed = JSON.parse(
1269
- readFileSync(join2(packageRoot, "package.json"), "utf8")
1038
+ if (Buffer.byteLength(pattern) > maxPatternBytes || (pattern.match(/\*/g)?.length ?? 0) > maxStars)
1039
+ throw patternLimitError();
1040
+ if (pattern.startsWith("!")) {
1041
+ if (rejectUnsupported) throw patternLimitError();
1042
+ warnings?.push(
1043
+ "Ignoring .harnesscodeignore negation; code-index patterns do not support !."
1270
1044
  );
1271
- if (typeof parsed !== "object" || parsed === null) return null;
1272
- const version2 = parsed["version"];
1273
- return typeof version2 === "string" ? version2 : null;
1274
- } catch {
1275
1045
  return null;
1276
1046
  }
1277
- }
1278
- function enclosingNodeModules(packageRoot) {
1279
- let dir = dirname(packageRoot);
1280
- const { root } = parse(dir);
1281
- for (; ; ) {
1282
- if (basename(dir) === "node_modules") return dir;
1283
- if (dir === root) return null;
1284
- dir = dirname(dir);
1047
+ if (pattern.includes("\\")) {
1048
+ if (rejectUnsupported) throw patternLimitError();
1049
+ warnings?.push(
1050
+ "A .harnesscodeignore backslash is literal, not an escape."
1051
+ );
1285
1052
  }
1286
- }
1287
- var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
1288
- function parseVersion(value) {
1289
- const match = SEMVER.exec(value.trim());
1290
- if (match === null) return null;
1053
+ if (rejectUnsupported && /[\[\]{}]/.test(pattern)) throw patternLimitError();
1054
+ const anchored = pattern.includes("/");
1055
+ if (mode === "ignore") pattern = pattern.replace(/^\/+|\/+$/g, "");
1056
+ const prefix = anchored ? "^" : "^(?:.*/)?";
1057
+ const suffix = mode === "ignore" ? "(?:/.*)?$" : "$";
1291
1058
  return {
1292
- parts: [Number(match[1]), Number(match[2]), Number(match[3])],
1293
- pre: match[4] ?? null
1059
+ source: mode === "served" ? raw : raw.trim(),
1060
+ negated: false,
1061
+ regex: new RegExp(
1062
+ prefix + globBody(pattern) + suffix,
1063
+ caseInsensitive ? "i" : ""
1064
+ )
1294
1065
  };
1295
1066
  }
1296
- function isNewer(candidate, current) {
1297
- const a = parseVersion(candidate);
1298
- const b = parseVersion(current);
1299
- if (a === null || b === null) return false;
1300
- for (let index = 0; index < 3; index++) {
1301
- if (a.parts[index] !== b.parts[index])
1302
- return a.parts[index] > b.parts[index];
1303
- }
1304
- if (a.pre === b.pre) return false;
1305
- if (a.pre === null) return true;
1306
- if (b.pre === null) return false;
1307
- return comparePrerelease(a.pre, b.pre) > 0;
1308
- }
1309
- function comparePrerelease(left, right) {
1310
- const a = left.split(".");
1311
- const b = right.split(".");
1312
- for (let index = 0; index < Math.max(a.length, b.length); index++) {
1313
- const x = a[index];
1314
- const y = b[index];
1315
- if (x === void 0) return -1;
1316
- if (y === void 0) return 1;
1317
- const xNumeric = /^\d+$/.test(x);
1318
- const yNumeric = /^\d+$/.test(y);
1319
- if (xNumeric && yNumeric) {
1320
- if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
1321
- } else if (xNumeric !== yNumeric) {
1322
- return xNumeric ? -1 : 1;
1323
- } else if (x !== y) {
1324
- return x < y ? -1 : 1;
1325
- }
1326
- }
1327
- return 0;
1067
+ function compilePatterns(lines, options = {}) {
1068
+ const maxPatterns = options.maxPatterns ?? MAX_PATTERNS;
1069
+ if (lines.length > maxPatterns) throw patternLimitError();
1070
+ return lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null);
1328
1071
  }
1329
- function stampPath(env = process.env, packageName = PACKAGE_NAME) {
1330
- const file = packageName === PACKAGE_NAME ? "update-check.json" : `update-check-${packageName.replace(/^@[^/]+\//, "")}.json`;
1331
- return join2(resolveCacheHome(env), "harness", file);
1072
+ function matchingPattern(path, patterns) {
1073
+ let decided = null;
1074
+ for (const pattern of patterns) if (pattern.regex.test(path)) decided = pattern;
1075
+ return decided;
1332
1076
  }
1333
- function readStamp(env = process.env, packageName = PACKAGE_NAME) {
1334
- try {
1335
- const parsed = JSON.parse(
1336
- readFileSync(stampPath(env, packageName), "utf8")
1077
+
1078
+ // src/core/code/policy.ts
1079
+ var MAX_FILE_BYTES = 1048576;
1080
+ var EXTENSIONS = [
1081
+ ".ex",
1082
+ ".exs",
1083
+ ".js",
1084
+ ".mjs",
1085
+ ".cjs",
1086
+ ".jsx",
1087
+ ".ts",
1088
+ ".tsx",
1089
+ ".swift"
1090
+ ];
1091
+ var DENY = [
1092
+ ".env",
1093
+ ".env.*",
1094
+ "secrets/**",
1095
+ "**/secrets/**",
1096
+ "*.pem",
1097
+ "*.key",
1098
+ "*.p12",
1099
+ "*.pfx",
1100
+ "*.jks",
1101
+ "*.keystore",
1102
+ "id_rsa*",
1103
+ "id_ecdsa*",
1104
+ "id_ed25519*",
1105
+ "*.kdbx",
1106
+ ".npmrc",
1107
+ ".netrc",
1108
+ "credentials.json",
1109
+ "**/.aws/**",
1110
+ ".git/**",
1111
+ "node_modules/**",
1112
+ "_build/**",
1113
+ "deps/**",
1114
+ "dist/**",
1115
+ "*.lock",
1116
+ "package-lock.json",
1117
+ "*.min.js",
1118
+ "*.map",
1119
+ "priv/static/**",
1120
+ "src/generated/**",
1121
+ "DerivedData/**",
1122
+ "**/DerivedData/**",
1123
+ ".build/**",
1124
+ "**/.build/**",
1125
+ "Pods/**",
1126
+ "Carthage/Build/**",
1127
+ "xcuserdata/**",
1128
+ "**/xcuserdata/**",
1129
+ "*.xcuserstate",
1130
+ "*.xcresult/**",
1131
+ "*.xcarchive/**",
1132
+ "*.dSYM/**",
1133
+ "*.ipa",
1134
+ "*.secret.exs"
1135
+ ];
1136
+ var SECRET_PATTERN_SOURCES = [
1137
+ "-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----",
1138
+ "\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b",
1139
+ "\\baone_[A-Za-z0-9_-]{20,}",
1140
+ "\\bgh[opsur]_[A-Za-z0-9]{36}\\b",
1141
+ "\\b[rs]k_live_[A-Za-z0-9]{20,}",
1142
+ "\\bAIza[0-9A-Za-z_-]{35}\\b",
1143
+ "\\bxapp-\\d-[A-Za-z0-9-]{20,}",
1144
+ "hooks\\.slack\\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+",
1145
+ "\\bgithub_pat_[A-Za-z0-9_]{22,}",
1146
+ "\\bxox[baprs]-\\d{8,}-[A-Za-z0-9-]{10,}",
1147
+ "\\bsk-(?:proj-|ant-[a-z0-9]+-)?[A-Za-z0-9_-]{32,}"
1148
+ ];
1149
+ var SECRETS = SECRET_PATTERN_SOURCES.map((source) => new RegExp(source));
1150
+ function contentMatchers(patterns) {
1151
+ const literalMarkers = [];
1152
+ for (const pattern of patterns) {
1153
+ if (SECRET_PATTERN_SOURCES.includes(pattern)) continue;
1154
+ if (!pattern || /[\\^$.*+?()[\]{}|]/.test(pattern))
1155
+ throw new Error("unknown regular expression");
1156
+ literalMarkers.push(pattern);
1157
+ }
1158
+ return literalMarkers;
1159
+ }
1160
+ function servedCase(served) {
1161
+ const value = served.deny_glob_case;
1162
+ if (value !== void 0 && value !== "ascii_insensitive")
1163
+ throw new Error("unknown deny glob case mode");
1164
+ return value;
1165
+ }
1166
+ function buildPolicy(served, ignore, includeFloor) {
1167
+ const warnings = [];
1168
+ const unsupported = [...new Set(
1169
+ served.include_extensions.filter((extension) => !EXTENSIONS.includes(extension))
1170
+ )].sort();
1171
+ if (unsupported.length)
1172
+ warnings.push(
1173
+ `This harness-cli does not send ${unsupported.join(", ")} files; upgrade the client.`
1337
1174
  );
1338
- if (typeof parsed !== "object" || parsed === null) return null;
1339
- const record = parsed;
1340
- const checkedAt = record["checkedAt"];
1341
- if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt))
1342
- return null;
1343
- const latest = record["latest"];
1344
- return { checkedAt, latest: typeof latest === "string" ? latest : null };
1175
+ const insensitiveServed = servedCase(served) === "ascii_insensitive";
1176
+ const literalMarkers = contentMatchers(served.content_patterns);
1177
+ return {
1178
+ limit: Math.min(served.max_file_bytes, MAX_FILE_BYTES),
1179
+ deny: [
1180
+ ...includeFloor ? compilePatterns(DENY, {
1181
+ caseInsensitive: true,
1182
+ maxPatterns: Infinity,
1183
+ maxPatternBytes: Infinity,
1184
+ maxStars: Infinity
1185
+ }) : [],
1186
+ ...compilePatterns(served.deny_globs, {
1187
+ caseInsensitive: includeFloor || insensitiveServed,
1188
+ mode: "served",
1189
+ maxPatternBytes: 1024,
1190
+ maxStars: Infinity,
1191
+ rejectUnsupported: true
1192
+ })
1193
+ ],
1194
+ ignore: compileIgnorePatterns(ignore.split(/\r?\n/), warnings),
1195
+ patterns: SECRETS,
1196
+ literalMarkers,
1197
+ extensions: new Set(
1198
+ EXTENSIONS.filter((extension) => served.include_extensions.includes(extension))
1199
+ ),
1200
+ warnings
1201
+ };
1202
+ }
1203
+ function policyFor(served, ignore) {
1204
+ try {
1205
+ return buildPolicy(served, ignore, true);
1345
1206
  } catch {
1346
- return null;
1207
+ throw new CommandError(
1208
+ "The server returned an invalid code upload policy; upgrade the client before code sync."
1209
+ );
1347
1210
  }
1348
1211
  }
1349
- function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
1350
- const path = stampPath(env, packageName);
1351
- const temporary = `${path}.${process.pid}.tmp`;
1352
- const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
1212
+ function basePolicyFor(served, ignore) {
1353
1213
  try {
1354
- mkdirSync(dirname(path), { recursive: true });
1355
- writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
1356
- renameSync(temporary, path);
1214
+ return buildPolicy(served, ignore, false);
1357
1215
  } catch {
1358
- try {
1359
- rmSync(temporary, { force: true });
1360
- } catch {
1361
- }
1216
+ throw new CommandError(
1217
+ "The server returned an invalid code upload policy; upgrade the client before code sync."
1218
+ );
1362
1219
  }
1363
1220
  }
1364
- function updateCheck(options = {}) {
1365
- const env = options.env ?? process.env;
1366
- const now = options.now ?? Date.now();
1367
- if (!checksEnabled(env)) return null;
1368
- const install = options.install ?? detectInstall();
1369
- if (install.kind !== "global" && install.kind !== "local") return null;
1370
- const packageName = install.packageName ?? PACKAGE_NAME;
1371
- const stamp = readStamp(env, packageName);
1372
- if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
1373
- writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
1374
- (options.refresh ?? spawnRefresh)();
1375
- }
1376
- if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
1377
- return `harness: ${stamp.latest} is available (you have ${CLI_VERSION}) \u2014 ${upgradeHint(install)}
1378
- `;
1379
- }
1380
- return null;
1221
+ function pathReason(path, p) {
1222
+ if (!path || Buffer.byteLength(path) > 1024 || /[\\\x00-\x1f\x7f-\x9f]/.test(path) || Buffer.from(path, "utf8").toString("utf8") !== path || path.split("/").some((segment) => ["", ".", ".."].includes(segment)))
1223
+ return "invalid_path";
1224
+ if (p.deny.some((pattern) => pattern.regex.test(path))) return "excluded_path";
1225
+ if (matchingPattern(path, p.ignore)) return "ignored";
1226
+ return p.extensions.has(extname(path)) ? null : "unsupported_language";
1381
1227
  }
1382
- function upgradeHint(install) {
1383
- return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
1228
+ function extensionWarning(path, p) {
1229
+ const extension = extname(path);
1230
+ return !p.extensions.has(extension) && p.extensions.has(extension.toLowerCase()) ? `The server indexes ${extension.toLowerCase()} exactly; ${path} was skipped.` : null;
1384
1231
  }
1385
- function spawnRefresh() {
1386
- const self = process.argv[1];
1387
- if (self === void 0 || self === "") return;
1388
- try {
1389
- const child = spawn2(process.execPath, [self, UPDATE_CHECK_COMMAND], {
1390
- detached: true,
1391
- stdio: "ignore"
1392
- });
1393
- child.on("error", () => {
1394
- });
1395
- child.unref();
1396
- } catch {
1397
- }
1232
+ function contentReason(bytes, p) {
1233
+ if (bytes.length > p.limit) return "too_large";
1234
+ if (bytes.includes(0) || !isUtf8(bytes)) return "binary";
1235
+ const text = bytes.toString("utf8");
1236
+ return p.patterns.some((pattern) => pattern.test(text)) || p.literalMarkers.some((marker) => text.includes(marker)) ? "secret_content" : null;
1398
1237
  }
1399
- async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
1400
- if (!checksEnabled(env)) return;
1401
- writeStamp(
1402
- { checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
1403
- env,
1404
- packageName
1238
+
1239
+ // src/core/code/scan.ts
1240
+ var MAX_OVERLAY_BYTES = 64 * 1024 * 1024;
1241
+ var MAX_OVERLAY_CHANGES = 2e4;
1242
+ var O_NOFOLLOW_ANY = 536870912;
1243
+ function sha256(data) {
1244
+ return createHash("sha256").update(data).digest("hex");
1245
+ }
1246
+ function supportsDarwinNoFollowAny(osRelease = release()) {
1247
+ const major = Number(osRelease.split(".")[0]);
1248
+ return Number.isInteger(major) && major >= 20;
1249
+ }
1250
+ async function committedIgnore(root, commit) {
1251
+ const entry = decodeUtf8(
1252
+ await gitBytes(root, [
1253
+ "ls-tree",
1254
+ "-z",
1255
+ commit,
1256
+ "--",
1257
+ ".harnesscodeignore"
1258
+ ]),
1259
+ "Cannot read the committed .harnesscodeignore safely."
1260
+ );
1261
+ if (!entry) return "";
1262
+ const match = /^(\d+) blob ([0-9a-f]+)\t\.harnesscodeignore\0$/.exec(entry);
1263
+ if (!match || !["100644", "100755"].includes(match[1]))
1264
+ throw new LocalError(
1265
+ "The committed .harnesscodeignore must be a regular file before code sync."
1266
+ );
1267
+ return decodeUtf8(
1268
+ await gitBytes(root, ["cat-file", "blob", match[2]], 65537),
1269
+ "The committed .harnesscodeignore must be valid UTF-8 before code sync."
1405
1270
  );
1406
1271
  }
1407
- async function fetchLatestVersion(env, packageName) {
1408
- const configured = env["HARNESS_REGISTRY_URL"];
1409
- const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
1272
+ var directoryFlags = constants2.O_RDONLY | constants2.O_DIRECTORY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK;
1273
+ async function openLinuxRoot(root) {
1274
+ let handle;
1410
1275
  try {
1411
- const response = await fetch(url, {
1412
- headers: { accept: "application/json" },
1413
- signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
1414
- });
1415
- if (!response.ok) return null;
1416
- const parsed = await response.json();
1417
- if (typeof parsed !== "object" || parsed === null) return null;
1418
- const version2 = parsed["version"];
1419
- if (typeof version2 !== "string" || parseVersion(version2) === null)
1420
- return null;
1421
- return version2;
1276
+ if (!root.startsWith("/"))
1277
+ throw new Error("repository root is not absolute");
1278
+ handle = await open("/", directoryFlags);
1279
+ for (const part of root.split("/").filter(Boolean)) {
1280
+ if (part === "." || part === "..")
1281
+ throw new Error("repository root is invalid");
1282
+ const next = await open(
1283
+ `/proc/self/fd/${handle.fd}/${part}`,
1284
+ directoryFlags
1285
+ );
1286
+ await handle.close();
1287
+ handle = next;
1288
+ }
1289
+ return handle;
1422
1290
  } catch {
1423
- return null;
1291
+ await handle?.close().catch(() => {
1292
+ });
1293
+ throw new LocalError(
1294
+ "Code sync cannot establish a safe source root on this platform."
1295
+ );
1424
1296
  }
1425
1297
  }
1426
- function selfHeal(options = {}) {
1427
- const env = options.env ?? process.env;
1428
- const argv2 = options.argv ?? process.argv;
1429
- if (truthy(env[REEXEC_SENTINEL])) {
1430
- return {
1431
- kind: "skipped",
1432
- reason: "already upgraded once for this command"
1433
- };
1434
- }
1435
- if (!autoUpgradeEnabled(env))
1436
- return { kind: "skipped", reason: "automatic upgrades are off" };
1437
- if (argv2[2] === "mcp")
1438
- return {
1439
- kind: "skipped",
1440
- reason: "an MCP session must not be replaced mid-stream"
1441
- };
1442
- const install = options.install ?? detectInstall();
1443
- if (install.kind !== "global" || install.packageRoot === null) {
1444
- return { kind: "skipped", reason: describeInstall(install.kind) };
1298
+ async function openLinuxBelowRoot(root, path) {
1299
+ let handle = await openLinuxRoot(root);
1300
+ try {
1301
+ const parts = path.split("/");
1302
+ for (const [index, part] of parts.entries()) {
1303
+ if (!part || part === "." || part === "..")
1304
+ throw new LocalError(
1305
+ "Cannot read an overlay file safely; invalid path."
1306
+ );
1307
+ const next = await open(
1308
+ `/proc/self/fd/${handle.fd}/${part}`,
1309
+ constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK | (index === parts.length - 1 ? 0 : constants2.O_DIRECTORY)
1310
+ );
1311
+ await handle.close();
1312
+ handle = next;
1313
+ }
1314
+ return handle;
1315
+ } catch (error) {
1316
+ await handle.close().catch(() => {
1317
+ });
1318
+ throw error;
1445
1319
  }
1446
- const packageRoot = install.packageRoot;
1447
- const installed = versionAt(packageRoot);
1448
- if (installed !== null && isNewer(installed, CLI_VERSION)) {
1449
- return { kind: "upgraded", code: reexec(options, env, argv2) };
1320
+ }
1321
+ async function openSafe(root, path) {
1322
+ if (process.platform === "darwin") {
1323
+ if (!supportsDarwinNoFollowAny())
1324
+ throw new LocalError(
1325
+ "Code sync requires macOS 11 or later for safe source reads."
1326
+ );
1327
+ return open(
1328
+ join(root, path),
1329
+ constants2.O_RDONLY | O_NOFOLLOW_ANY | constants2.O_NONBLOCK
1330
+ );
1450
1331
  }
1451
- const lock = takeLock(env);
1452
- if (lock === null)
1453
- return {
1454
- kind: "skipped",
1455
- reason: "another harness process is already upgrading"
1456
- };
1332
+ if (process.platform === "linux") return openLinuxBelowRoot(root, path);
1333
+ throw new LocalError(
1334
+ "Code sync cannot safely read source files on this platform."
1335
+ );
1336
+ }
1337
+ async function readSafe(root, path, limit) {
1457
1338
  try {
1458
- const stillHeld = () => readHolder(lock.path)?.pid === lock.pid;
1459
- const result = (options.runInstall ?? npmInstallGlobal)(install, stillHeld);
1460
- if (!result.ok) return { kind: "failed", reason: result.detail };
1461
- const after = versionAt(packageRoot);
1462
- if (after !== null && after === installed) {
1339
+ const handle = await openSafe(root, path);
1340
+ try {
1341
+ const before = await handle.stat();
1342
+ if (!before.isFile()) return { reason: "not_regular_file" };
1343
+ if (before.size > limit) return { reason: "too_large" };
1344
+ const bytes = Buffer.allocUnsafe(Math.min(before.size, limit) + 1);
1345
+ let total = 0;
1346
+ while (total < bytes.length) {
1347
+ const { bytesRead } = await handle.read(
1348
+ bytes,
1349
+ total,
1350
+ bytes.length - total,
1351
+ null
1352
+ );
1353
+ if (bytesRead === 0) break;
1354
+ total += bytesRead;
1355
+ }
1356
+ const after = await handle.stat();
1357
+ if (total > limit) return { reason: "too_large" };
1358
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || total !== after.size)
1359
+ throw new LocalError(
1360
+ "A source file changed during scanning; retry code sync."
1361
+ );
1463
1362
  return {
1464
- kind: "failed",
1465
- reason: `still ${after} after upgrading \u2014 no newer release is published`
1363
+ bytes: bytes.subarray(0, total),
1364
+ mode: before.mode & 64 ? "100755" : "100644"
1466
1365
  };
1366
+ } finally {
1367
+ await handle.close();
1467
1368
  }
1468
- if (after !== null && installed !== null && !isNewer(after, installed)) {
1469
- return {
1470
- kind: "failed",
1471
- reason: `npm left ${after} installed, which is not newer than ${installed}`
1472
- };
1369
+ } catch (error) {
1370
+ const code2 = error.code;
1371
+ if (code2 === "ENOENT") return { reason: "deleted" };
1372
+ if (code2 === "ELOOP" || code2 === "ENOTDIR") return { reason: "symlink" };
1373
+ if (error instanceof LocalError) throw error;
1374
+ throw new LocalError(
1375
+ "Cannot read an overlay file safely; check permissions and retry code sync."
1376
+ );
1377
+ }
1378
+ }
1379
+ async function scanOverlay(id, served) {
1380
+ policyFor(served, "");
1381
+ const ignore = await readSafe(id.root, ".harnesscodeignore", 65536);
1382
+ if ("reason" in ignore && ignore.reason !== "deleted")
1383
+ throw new LocalError(
1384
+ "Cannot read .harnesscodeignore safely; fix or remove it before code sync."
1385
+ );
1386
+ const localIgnore = "bytes" in ignore ? decodeUtf8(
1387
+ ignore.bytes,
1388
+ "The local .harnesscodeignore must be valid UTF-8 before code sync."
1389
+ ) : "";
1390
+ const baseIgnore = await committedIgnore(id.root, id.commit);
1391
+ if (Buffer.byteLength(baseIgnore) > 65536)
1392
+ throw new LocalError(
1393
+ "The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."
1394
+ );
1395
+ const policy2 = policyFor(served, localIgnore);
1396
+ const basePolicy = basePolicyFor(served, baseIgnore);
1397
+ const paths = await changedPaths(id.root, id.commit);
1398
+ const basePaths = new Set(
1399
+ decodeGitPaths([
1400
+ await gitBytes(id.root, ["ls-tree", "-r", "--name-only", "-z", id.commit])
1401
+ ])
1402
+ );
1403
+ assertDistinctPathIdentities([...basePaths, ...paths]);
1404
+ const changes = [];
1405
+ const deleted = /* @__PURE__ */ new Set();
1406
+ const blobs = /* @__PURE__ */ new Map();
1407
+ const excluded = [];
1408
+ const baseWouldHold = (path) => basePaths.has(path) && pathReason(path, basePolicy) === null;
1409
+ const deleteBasePath = (path) => {
1410
+ if (baseWouldHold(path) && !deleted.has(path)) {
1411
+ if (changes.length >= MAX_OVERLAY_CHANGES)
1412
+ throw new LocalError(
1413
+ `overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
1414
+ );
1415
+ deleted.add(path);
1416
+ changes.push({ op: "delete", path });
1473
1417
  }
1474
- } finally {
1475
- releaseLock(lock);
1418
+ };
1419
+ const candidates = new Set(paths);
1420
+ const warnings = /* @__PURE__ */ new Set([...policy2.warnings, ...basePolicy.warnings]);
1421
+ for (const path of basePaths) {
1422
+ const baseReason = pathReason(path, basePolicy);
1423
+ const localReason = pathReason(path, policy2);
1424
+ if (baseReason === null && (localReason === "excluded_path" || localReason === "ignored"))
1425
+ deleteBasePath(path);
1426
+ if (baseReason === "ignored" && localReason === null) candidates.add(path);
1427
+ }
1428
+ let total = 0;
1429
+ for (const path of [...candidates].sort(
1430
+ (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
1431
+ )) {
1432
+ const reason = pathReason(path, policy2);
1433
+ if (reason) {
1434
+ excluded.push({ path, reason });
1435
+ if (reason === "ignored" || reason === "excluded_path") deleteBasePath(path);
1436
+ const warning = extensionWarning(path, policy2);
1437
+ if (warning) warnings.add(warning);
1438
+ continue;
1439
+ }
1440
+ const read = await readSafe(id.root, path, policy2.limit);
1441
+ if ("reason" in read) {
1442
+ if (read.reason !== "deleted")
1443
+ excluded.push({ path, reason: read.reason });
1444
+ deleteBasePath(path);
1445
+ continue;
1446
+ }
1447
+ const content = contentReason(read.bytes, policy2);
1448
+ if (content) {
1449
+ excluded.push({ path, reason: content });
1450
+ deleteBasePath(path);
1451
+ continue;
1452
+ }
1453
+ const hash2 = sha256(read.bytes);
1454
+ if (!blobs.has(hash2)) {
1455
+ total += read.bytes.length;
1456
+ if (total > MAX_OVERLAY_BYTES)
1457
+ throw new LocalError(
1458
+ "The overlay exceeds the 64 MiB client upload limit; split the change before syncing."
1459
+ );
1460
+ blobs.set(hash2, read.bytes);
1461
+ }
1462
+ if (changes.length >= MAX_OVERLAY_CHANGES)
1463
+ throw new LocalError(
1464
+ `overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
1465
+ );
1466
+ changes.push({
1467
+ op: "put",
1468
+ path,
1469
+ sha256: hash2,
1470
+ size: read.bytes.length,
1471
+ mode: read.mode
1472
+ });
1476
1473
  }
1477
- return { kind: "upgraded", code: reexec(options, env, argv2) };
1474
+ changes.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
1475
+ const digest = sha256(
1476
+ changes.map(
1477
+ (c) => c.op === "delete" ? `${c.path}\0-\0-
1478
+ ` : `${c.path}\0${c.sha256}\0${c.mode}
1479
+ `
1480
+ ).join("")
1481
+ );
1482
+ return {
1483
+ changes,
1484
+ blobs,
1485
+ excluded,
1486
+ warnings: [...warnings],
1487
+ digest
1488
+ };
1478
1489
  }
1479
- function reexec(options, env, argv2) {
1480
- return (options.reexec ?? reexecSelf)(
1481
- { ...env, [REEXEC_SENTINEL]: "1" },
1482
- argv2
1490
+
1491
+ // src/core/code/state.ts
1492
+ import {
1493
+ readFile,
1494
+ mkdir,
1495
+ writeFile,
1496
+ rename,
1497
+ unlink,
1498
+ rmdir
1499
+ } from "fs/promises";
1500
+ import { join as join2, resolve, isAbsolute } from "path";
1501
+ import { randomUUID } from "crypto";
1502
+ function statePath(server, org, root, repoKey, directory) {
1503
+ const url = new URL(server);
1504
+ const canonical = url.origin + url.pathname.replace(/\/+$/, "");
1505
+ const base2 = directory ?? process.env["HARNESS_CODE_STATE_DIR"] ?? join2(resolveCacheHome(), "harness", "code");
1506
+ if (!isAbsolute(base2))
1507
+ throw new LocalError("HARNESS_CODE_STATE_DIR must be an absolute path.");
1508
+ return join2(
1509
+ resolve(base2),
1510
+ sha256(JSON.stringify([canonical, org.toLowerCase()])),
1511
+ sha256(JSON.stringify([root, repoKey])) + ".json"
1483
1512
  );
1484
1513
  }
1485
- function describeInstall(kind) {
1486
- switch (kind) {
1487
- case "checkout":
1488
- return "this is a git checkout, not an npm install";
1489
- case "local":
1490
- return "this copy is a project dependency, not a global install";
1491
- case "npx":
1492
- return "npx runs a throwaway copy";
1493
- default:
1494
- return "cannot tell how this CLI was installed";
1495
- }
1496
- }
1497
- function npmInstallGlobal(install, stillHeld) {
1498
- const globalRoot = npmGlobalRoot();
1499
- if (globalRoot === null)
1500
- return { ok: false, detail: "npm did not say where its global root is" };
1501
- if (globalRoot.includes("***")) {
1502
- return {
1503
- ok: false,
1504
- detail: "npm redacted its own global root, so this copy's location cannot be confirmed"
1505
- };
1514
+ async function readState(path, server, org, root, repoKey) {
1515
+ try {
1516
+ const s = JSON.parse(await readFile(path, "utf8"));
1517
+ if (typeof s !== "object" || s === null) throw new Error();
1518
+ const state2 = s;
1519
+ if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey || new URL(state2.server).href !== new URL(server).href || typeof state2.viewKey !== "string" || !/^[\da-f-]{36}$/.test(state2.viewKey))
1520
+ throw new Error();
1521
+ return { ...state2, org: state2.org.toLowerCase() };
1522
+ } catch (error) {
1523
+ if (error.code === "ENOENT") return null;
1524
+ throw new LocalError(
1525
+ "The code view state is invalid or belongs to another scope. Check HARNESS_CODE_STATE_DIR; no source was uploaded."
1526
+ );
1506
1527
  }
1507
- if (install.nodeModules === null || !samePath(globalRoot, install.nodeModules)) {
1508
- return {
1509
- ok: false,
1510
- detail: `this copy lives in ${install.nodeModules ?? "an unrecognized place"}, not npm's global root (${globalRoot})`
1511
- };
1528
+ }
1529
+ async function initialState(path, server, org, root, repoKey) {
1530
+ const previous = await readState(path, server, org, root, repoKey);
1531
+ if (previous) return previous;
1532
+ const state2 = { server, org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
1533
+ await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
1534
+ try {
1535
+ await writeFile(path, JSON.stringify(state2) + "\n", {
1536
+ flag: "wx",
1537
+ mode: 384
1538
+ });
1539
+ return state2;
1540
+ } catch (error) {
1541
+ if (error.code === "EEXIST") {
1542
+ const raced = await readState(path, server, org, root, repoKey);
1543
+ if (raced) return raced;
1544
+ }
1545
+ throw new LocalError("Cannot save isolated code view state.");
1512
1546
  }
1547
+ }
1548
+ async function saveState(path, state2) {
1549
+ const tmp = `${path}.${randomUUID()}.tmp`;
1513
1550
  try {
1514
- accessSync2(globalRoot, constants2.W_OK);
1551
+ await writeFile(tmp, JSON.stringify({ ...state2, org: state2.org.toLowerCase() }) + "\n", {
1552
+ flag: "wx",
1553
+ mode: 384
1554
+ });
1555
+ await rename(tmp, path);
1515
1556
  } catch {
1516
- return {
1517
- ok: false,
1518
- detail: `${globalRoot} is not writable \u2014 upgrade it yourself`
1519
- };
1520
- }
1521
- if (!stillHeld()) {
1522
- return {
1523
- ok: false,
1524
- detail: "lost the upgrade lock while preparing \u2014 another process took over"
1525
- };
1526
- }
1527
- process.stderr.write(
1528
- named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
1529
- `)
1530
- );
1531
- const args = [
1532
- "install",
1533
- "--global",
1534
- "--ignore-scripts",
1535
- "--no-audit",
1536
- "--no-fund",
1537
- "--no-update-notifier",
1538
- "--loglevel=error",
1539
- upgradeInstallSpec(install)
1540
- ];
1541
- const result = spawnSync(npmCommand(), args, {
1542
- // npm's diagnostics belong on stderr with ours; stdout stays the
1543
- // command's own, so its summary is dropped rather than piped into jq.
1544
- stdio: ["ignore", "ignore", "inherit"],
1545
- timeout: INSTALL_TIMEOUT_MS,
1546
- ...npmSpawnOptions()
1547
- });
1548
- if (result.error !== void 0)
1549
- return { ok: false, detail: result.error.message };
1550
- if (result.status !== 0) {
1551
- return {
1552
- ok: false,
1553
- detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}`
1554
- };
1557
+ throw new LocalError(
1558
+ "Cannot save code sync progress; retry with the same worktree and state directory."
1559
+ );
1560
+ } finally {
1561
+ await unlink(tmp).catch(() => {
1562
+ });
1555
1563
  }
1556
- return { ok: true, detail: "" };
1557
1564
  }
1558
- function npmGlobalRoot() {
1565
+ async function withSyncLock(path, action) {
1566
+ const lock = path + ".lock";
1567
+ await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
1559
1568
  try {
1560
- const result = spawnSync(npmCommand(), ["root", "--global"], {
1561
- encoding: "utf8",
1562
- timeout: NPM_ROOT_TIMEOUT_MS,
1563
- ...npmSpawnOptions()
1569
+ await mkdir(lock, { mode: 448 });
1570
+ } catch (error) {
1571
+ if (error.code === "EEXIST")
1572
+ throw new LocalError(
1573
+ `A code sync is already running for this view. Wait for it to finish. If it was interrupted, confirm no sync is running before removing its lock directory: ${lock}`
1574
+ );
1575
+ throw new LocalError(
1576
+ "Cannot lock code sync progress; check the state directory permissions."
1577
+ );
1578
+ }
1579
+ try {
1580
+ await writeFile(join2(lock, "owner"), `${process.pid}
1581
+ `, {
1582
+ flag: "wx",
1583
+ mode: 384
1564
1584
  });
1565
- if (result.status !== 0 || typeof result.stdout !== "string") return null;
1566
- const path = result.stdout.trim();
1567
- return path === "" ? null : path;
1568
- } catch {
1569
- return null;
1585
+ return await action();
1586
+ } finally {
1587
+ await unlink(join2(lock, "owner")).catch(() => {
1588
+ });
1589
+ try {
1590
+ await rmdir(lock);
1591
+ } catch {
1592
+ try {
1593
+ process.stderr.write(named(
1594
+ `harness: Warning: code sync lock cleanup failed at ${clean(lock)}. If the lock remains, confirm no sync is running before removing it.
1595
+ `
1596
+ ));
1597
+ } catch {
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ // src/core/code/sync.ts
1604
+ async function baseReady(api, repo, commit, options) {
1605
+ const timeout = options.pollTimeoutMs ?? 6e5;
1606
+ const sleep = options.sleep ?? delay2;
1607
+ let waited = 0;
1608
+ let interval = 1e3;
1609
+ const started = Date.now();
1610
+ for (let attempts = 0; attempts < 600; attempts++) {
1611
+ const base2 = await api.getBase(repo, commit, options.skipped === true);
1612
+ if (base2.state === "complete" || base2.state === "ready") return base2;
1613
+ if (base2.state === "failed")
1614
+ throw new CommandError(
1615
+ "The server could not build this commit\u2019s base. Check code status --skipped, then retry after the server backoff."
1616
+ );
1617
+ const backoff = base2.retry_after_ms === 0 ? interval : base2.retry_after_ms ?? interval;
1618
+ if (!Number.isFinite(backoff) || backoff < 0 || Math.max(waited, Date.now() - started) + backoff > timeout)
1619
+ throw new CommandError(
1620
+ "Timed out waiting for the server-built base; retry code sync to resume."
1621
+ );
1622
+ await sleep(backoff);
1623
+ waited += backoff;
1624
+ interval = Math.min(interval * 2, 1e4);
1570
1625
  }
1626
+ throw new CommandError("The base is still fetching; retry code sync later.");
1571
1627
  }
1572
- function npmCommand() {
1573
- return process.platform === "win32" ? "npm.cmd" : "npm";
1628
+ function stale(error) {
1629
+ return error instanceof HttpError && error.status === 409 && error.body["error"] === "stale_policy";
1574
1630
  }
1575
- function npmSpawnOptions() {
1576
- return process.platform === "win32" ? { shell: true } : {};
1631
+ function withinActivationRetention(attemptedAt) {
1632
+ const elapsed = typeof attemptedAt === "number" ? Date.now() - attemptedAt : NaN;
1633
+ return Number.isFinite(elapsed) && elapsed >= 0 && elapsed < 60 * 6e4;
1577
1634
  }
1578
- function samePath(left, right) {
1579
- const resolve = (path) => {
1635
+ var ReplacedSyncError = class extends CommandError {
1636
+ constructor() {
1637
+ super(
1638
+ "The server replaced this sync before activation. Retry code sync with no other sync running for this view."
1639
+ );
1640
+ }
1641
+ };
1642
+ function policyScope(policy2) {
1643
+ if (!/^[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}$/i.test(policy2.org_id) || !policy2.policy_digest || !Number.isSafeInteger(policy2.max_file_bytes) || policy2.max_file_bytes < 1)
1644
+ throw new CommandError(
1645
+ "The server did not provide a valid organization and upload policy; no source was uploaded."
1646
+ );
1647
+ policyFor(policy2, "");
1648
+ }
1649
+ async function syncCode(api, options) {
1650
+ const id = await identity(options.cwd);
1651
+ let policyRestarts = 0;
1652
+ let syncRestarts = 0;
1653
+ for (let restart = 0; restart < 3; restart++) {
1654
+ const policy2 = await api.getPolicy();
1655
+ policyScope(policy2);
1656
+ const path = statePath(
1657
+ api.origin,
1658
+ policy2.org_id,
1659
+ id.root,
1660
+ id.repoKey,
1661
+ options.stateDir
1662
+ );
1580
1663
  try {
1581
- return realpathSync(path);
1582
- } catch {
1583
- return path;
1664
+ return await withSyncLock(path, async () => {
1665
+ const state2 = await initialState(
1666
+ path,
1667
+ api.origin,
1668
+ policy2.org_id,
1669
+ id.root,
1670
+ id.repoKey
1671
+ );
1672
+ const repo = await api.putRepository(id.repoKey);
1673
+ if (repo.repo_key !== id.repoKey)
1674
+ throw new CommandError(
1675
+ "The server resolved a different repository; no source was uploaded."
1676
+ );
1677
+ if (state2.repositoryId && state2.repositoryId !== repo.repository_id) {
1678
+ delete state2.viewId;
1679
+ delete state2.pending;
1680
+ delete state2.activated;
1681
+ }
1682
+ state2.repositoryId = repo.repository_id;
1683
+ state2.baseCommit = id.commit;
1684
+ await saveState(path, state2);
1685
+ const base2 = await baseReady(
1686
+ api,
1687
+ repo.repository_id,
1688
+ id.commit,
1689
+ options
1690
+ );
1691
+ if (base2.policy_digest && base2.policy_digest !== policy2.policy_digest)
1692
+ throw new HttpError(409, "The upload policy changed", {
1693
+ error: "stale_policy"
1694
+ });
1695
+ const overlay = await scanOverlay(id, policy2);
1696
+ const view2 = await api.putView(repo.repository_id, state2.viewKey);
1697
+ if (state2.viewId !== void 0 && state2.viewId !== view2.view_id && view2.repository_id === repo.repository_id && view2.view_key === state2.viewKey) {
1698
+ let removed = false;
1699
+ try {
1700
+ await api.getView(state2.viewId);
1701
+ } catch (error) {
1702
+ if (error instanceof HttpError && error.status === 404)
1703
+ removed = true;
1704
+ else throw error;
1705
+ }
1706
+ if (removed) {
1707
+ delete state2.viewId;
1708
+ delete state2.pending;
1709
+ delete state2.activated;
1710
+ }
1711
+ }
1712
+ if (view2.repository_id !== repo.repository_id || view2.view_key !== state2.viewKey || state2.viewId !== void 0 && state2.viewId !== view2.view_id)
1713
+ throw new CommandError(
1714
+ "The server view does not match this repository/worktree; no source was uploaded."
1715
+ );
1716
+ state2.viewId = view2.view_id;
1717
+ const forgetReplaced = async () => {
1718
+ delete state2.pending;
1719
+ await saveState(path, state2);
1720
+ throw new ReplacedSyncError();
1721
+ };
1722
+ const receipt = async (syncId) => {
1723
+ try {
1724
+ const previous = await api.getSync(view2.view_id, syncId);
1725
+ if (previous.id !== syncId)
1726
+ throw new CommandError("The server returned a different sync; activation remains unverified.");
1727
+ if (previous.state === "receiving") return null;
1728
+ if (!Number.isSafeInteger(previous.version) || previous.version === null || previous.version < 1)
1729
+ throw new CommandError("The server did not return an activation version; retry code status.");
1730
+ return { pending_version: previous.version, state: previous.state };
1731
+ } catch (error) {
1732
+ if (error instanceof HttpError && error.status === 404) {
1733
+ if (withinActivationRetention(state2.pending?.activationAttemptedAt))
1734
+ return forgetReplaced();
1735
+ throw new CommandError("The previous activation can no longer be verified after retention. Check code status before syncing again.");
1736
+ }
1737
+ throw error;
1738
+ }
1739
+ };
1740
+ let current = state2.pending;
1741
+ const matchesCurrent = current && current.digest === overlay.digest && current.request?.mode === "full" && current.request.base_commit === id.commit && current.request.policy_digest === policy2.policy_digest && typeof current.request.client_sync_id === "string" && JSON.stringify(current.request.changes) === JSON.stringify(overlay.changes);
1742
+ if (current?.syncId && current.activationAttemptedAt !== void 0 && !matchesCurrent) {
1743
+ const prior = current;
1744
+ const originalSyncId = current.syncId;
1745
+ let recovered = await receipt(originalSyncId);
1746
+ if (!recovered) {
1747
+ prior.activationAttemptedAt = Date.now();
1748
+ await saveState(path, state2);
1749
+ try {
1750
+ recovered = await api.activate(view2.view_id, originalSyncId);
1751
+ } catch (error) {
1752
+ recovered = await receipt(originalSyncId);
1753
+ if (!recovered) throw error;
1754
+ }
1755
+ }
1756
+ state2.activated = {
1757
+ baseCommit: prior.request.base_commit,
1758
+ overlayDigest: prior.digest,
1759
+ pendingVersion: recovered.pending_version,
1760
+ policyDigest: prior.request.policy_digest
1761
+ };
1762
+ delete state2.pending;
1763
+ await saveState(path, state2);
1764
+ current = void 0;
1765
+ }
1766
+ const request = matchesCurrent && current ? current.request : {
1767
+ client_sync_id: randomUUID2(),
1768
+ base_commit: id.commit,
1769
+ mode: "full",
1770
+ changes: overlay.changes,
1771
+ policy_digest: policy2.policy_digest
1772
+ };
1773
+ state2.pending = current && current.request === request ? current : { digest: overlay.digest, request };
1774
+ await saveState(path, state2);
1775
+ const finish = async (syncId, activated3, uploadedBlobs) => {
1776
+ state2.activated = {
1777
+ baseCommit: id.commit,
1778
+ overlayDigest: overlay.digest,
1779
+ pendingVersion: activated3.pending_version,
1780
+ policyDigest: policy2.policy_digest
1781
+ };
1782
+ delete state2.pending;
1783
+ await saveState(path, state2);
1784
+ return {
1785
+ repository_id: repo.repository_id,
1786
+ repo_key: id.repoKey,
1787
+ org_id: policy2.org_id,
1788
+ view_id: view2.view_id,
1789
+ view_key: state2.viewKey,
1790
+ sync_id: syncId,
1791
+ base_commit: id.commit,
1792
+ base: base2,
1793
+ overlay_digest: overlay.digest,
1794
+ changed_files: overlay.changes.length,
1795
+ uploaded_blobs: uploadedBlobs,
1796
+ excluded: overlay.excluded,
1797
+ warnings: overlay.warnings,
1798
+ policy_restarts: policyRestarts,
1799
+ sync_restarts: syncRestarts,
1800
+ ...activated3
1801
+ };
1802
+ };
1803
+ if (state2.pending.syncId && state2.pending.activationAttemptedAt !== void 0) {
1804
+ const recovered = await receipt(state2.pending.syncId);
1805
+ if (recovered) return finish(state2.pending.syncId, recovered, 0);
1806
+ }
1807
+ const opened2 = await api.openSync(view2.view_id, request);
1808
+ if (opened2.rejected.length)
1809
+ throw new CommandError(
1810
+ `The server refused ${opened2.rejected.length} overlay entries; no sync was activated. Refetch the policy and check the skipped-files report.`
1811
+ );
1812
+ state2.pending.syncId = opened2.sync_id;
1813
+ await saveState(path, state2);
1814
+ if (opened2.state !== "receiving") {
1815
+ const recovered = await receipt(opened2.sync_id);
1816
+ if (recovered) return finish(opened2.sync_id, recovered, 0);
1817
+ throw new CommandError("The server returned an inconsistent sync state; no new version was requested.");
1818
+ }
1819
+ const missing = [...new Set(opened2.missing)];
1820
+ for (const hash2 of missing)
1821
+ if (!overlay.blobs.has(hash2))
1822
+ throw new CommandError(
1823
+ "The server requested an unexpected blob not present in this overlay; no blobs were uploaded."
1824
+ );
1825
+ let activating = false;
1826
+ let activated2;
1827
+ try {
1828
+ let next = 0;
1829
+ let failure;
1830
+ await Promise.all(Array.from({ length: Math.min(4, missing.length) }, async () => {
1831
+ while (!failure && next < missing.length) {
1832
+ const hash2 = missing[next++];
1833
+ try {
1834
+ await api.putBlob(hash2, overlay.blobs.get(hash2));
1835
+ } catch (error) {
1836
+ failure ??= { error };
1837
+ }
1838
+ }
1839
+ }));
1840
+ if (failure) throw failure.error;
1841
+ state2.pending.activationAttemptedAt = Date.now();
1842
+ await saveState(path, state2);
1843
+ activating = true;
1844
+ activated2 = await api.activate(view2.view_id, opened2.sync_id);
1845
+ } catch (error) {
1846
+ const replaced = error instanceof HttpError && (activating ? error.status === 404 : error.status === 409 && error.body["error"] === "unexpected_blob");
1847
+ if (replaced) {
1848
+ if (activating && !withinActivationRetention(state2.pending?.activationAttemptedAt))
1849
+ throw new CommandError("The activation retry exceeded its retention window. Check code status before syncing again.");
1850
+ return forgetReplaced();
1851
+ }
1852
+ const ambiguous = activating && (!(error instanceof HttpError) || error.status >= 500 || error.status === 429);
1853
+ if (ambiguous) {
1854
+ const recovered = await receipt(opened2.sync_id);
1855
+ if (recovered) return finish(opened2.sync_id, recovered, missing.length);
1856
+ }
1857
+ throw error;
1858
+ }
1859
+ return finish(opened2.sync_id, activated2, missing.length);
1860
+ });
1861
+ } catch (error) {
1862
+ if (stale(error) && restart < 2) {
1863
+ policyRestarts++;
1864
+ continue;
1865
+ }
1866
+ if (error instanceof ReplacedSyncError && restart < 2) {
1867
+ syncRestarts++;
1868
+ continue;
1869
+ }
1870
+ throw error;
1584
1871
  }
1585
- };
1586
- return resolve(left) === resolve(right);
1587
- }
1588
- function reexecSelf(env, argv2) {
1589
- const self = argv2[1];
1590
- if (self === void 0) return 1;
1591
- const result = spawnSync(process.execPath, [self, ...argv2.slice(2)], {
1592
- stdio: "inherit",
1593
- env
1594
- });
1595
- if (result.error !== void 0) {
1596
- process.stderr.write(
1597
- named(`harness: upgraded, but re-running failed: ${result.error.message}
1598
- `)
1872
+ }
1873
+ throw new CommandError(
1874
+ "The upload policy kept changing. Retry code sync after it settles."
1875
+ );
1876
+ }
1877
+ async function statusCode(api, options) {
1878
+ const id = await identity(options.cwd);
1879
+ const policy2 = await api.getPolicy();
1880
+ policyScope(policy2);
1881
+ const path = statePath(
1882
+ api.origin,
1883
+ policy2.org_id,
1884
+ id.root,
1885
+ id.repoKey,
1886
+ options.stateDir
1887
+ );
1888
+ const state2 = await readState(
1889
+ path,
1890
+ api.origin,
1891
+ policy2.org_id,
1892
+ id.root,
1893
+ id.repoKey
1894
+ );
1895
+ const repository = options.skipped ? await api.putRepository(id.repoKey) : void 0;
1896
+ if (repository && repository.repo_key !== id.repoKey)
1897
+ throw new CommandError(
1898
+ "The server resolved a different repository; its base status cannot be trusted."
1599
1899
  );
1600
- return 1;
1900
+ const repositoryId = repository?.repository_id ?? state2?.repositoryId;
1901
+ if (!state2?.viewId || repository && repositoryId !== state2.repositoryId) {
1902
+ const base3 = options.skipped && repositoryId ? await api.getBase(repositoryId, id.commit, true) : void 0;
1903
+ return {
1904
+ state: base3?.state === "failed" ? "failed" : base3?.state === "fetching" ? "fetching" : "not_synced",
1905
+ org_id: policy2.org_id,
1906
+ repo_key: id.repoKey,
1907
+ base_commit: id.commit,
1908
+ ...base3 ? { base: base3 } : {}
1909
+ };
1601
1910
  }
1602
- return result.status ?? 1;
1911
+ const view2 = await api.getView(state2.viewId);
1912
+ if (view2.repository_id !== state2.repositoryId || view2.view_id !== state2.viewId || view2.view_key !== state2.viewKey)
1913
+ throw new CommandError(
1914
+ "The server view does not match this repository/worktree; its status cannot be trusted."
1915
+ );
1916
+ const overlay = await scanOverlay(id, policy2);
1917
+ const current = view2.active?.base_commit === id.commit && view2.active.overlay_digest === overlay.digest && state2.activated?.baseCommit === view2.active.base_commit && state2.activated.overlayDigest === view2.active.overlay_digest && state2.activated.pendingVersion === view2.active.version && state2.activated.policyDigest === policy2.policy_digest;
1918
+ const base2 = options.skipped && state2.repositoryId ? await api.getBase(state2.repositoryId, id.commit, true) : void 0;
1919
+ return {
1920
+ state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : "stale",
1921
+ org_id: policy2.org_id,
1922
+ repo_key: id.repoKey,
1923
+ base_commit: id.commit,
1924
+ ...view2,
1925
+ local_overlay_digest: overlay.digest,
1926
+ excluded: overlay.excluded,
1927
+ warnings: overlay.warnings,
1928
+ ...base2 ? { base: base2 } : {}
1929
+ };
1603
1930
  }
1604
- function takeLock(env) {
1605
- const path = join2(resolveCacheHome(env), "harness", "upgrade.lock");
1606
- const temporary = `${path}.${process.pid}.tmp`;
1931
+
1932
+ // src/core/code/query.ts
1933
+ import { z as z2 } from "zod";
1934
+ var hash = z2.string().regex(/^[0-9a-f]{64}$/);
1935
+ var queryView = z2.object({
1936
+ view_id: z2.string().uuid(),
1937
+ repository_id: z2.string().uuid(),
1938
+ view_key: z2.string().min(1),
1939
+ active: z2.object({ version: z2.number().int().positive(), base_commit: z2.string().regex(/^[0-9a-f]{40}$/), overlay_digest: hash }).passthrough(),
1940
+ pending: z2.object({ version: z2.number().int(), state: z2.string(), progress: z2.object({ done: z2.number().int().nonnegative(), total: z2.number().int().nonnegative() }).nullable().optional() }).passthrough().nullable()
1941
+ }).passthrough();
1942
+ var envelopeSchema = z2.object({
1943
+ data: z2.unknown(),
1944
+ meta: z2.object({ contract: z2.literal("1.3"), view: queryView, truncation: z2.object({ truncated: z2.boolean() }).passthrough() }).passthrough()
1945
+ }).passthrough();
1946
+ var citationSchema = z2.object({ path: z2.string().min(1), start_line: z2.number().int().positive(), end_line: z2.number().int().positive(), sha256: hash, version: z2.number().int().positive(), from: z2.enum(["base", "overlay"]), snippet: z2.string().optional() }).passthrough();
1947
+ var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1948
+ var shell = (text) => "'" + text.replaceAll("'", "'\\''") + "'";
1949
+ function fail() {
1950
+ throw new CommandError("The code query API returned an invalid contract 1.3 response; no result was presented.");
1951
+ }
1952
+ function integer(value, name, min, max) {
1953
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < min || value > max)) throw new UsageError(`${name} must be an integer from ${min} to ${max}.`);
1954
+ }
1955
+ function validateQuery(operation, p) {
1956
+ const texts = [p.q, p.symbol, p.kind, p.pathPrefix, ...p.symbols ?? [], ...p.files ?? []];
1957
+ if (texts.some((s) => s !== void 0 && (typeof s !== "string" || s.length === 0 || Buffer.byteLength(s) > 8192 || /[\x00-\x1f\x7f-\x9f]/.test(s) || Buffer.from(s).toString("utf8") !== s))) throw new UsageError("Code query arguments must be nonempty UTF-8 text without control characters (at most 8 KiB each).");
1958
+ if (operation === "search" && !p.q?.trim()) throw new UsageError("usage: harness code search <query>");
1959
+ if (operation === "explain" && !p.symbol) throw new UsageError("usage: harness code explain <symbol-id>");
1960
+ if (operation === "context" && Number(Boolean(p.symbol)) + Number(Boolean(p.q?.trim())) !== 1) throw new UsageError("code context requires exactly one query or --symbol.");
1961
+ if (operation === "affected") {
1962
+ const seeds = (p.symbols?.length ?? 0) + (p.files?.length ?? 0);
1963
+ if (seeds < 1 || seeds > 200) throw new UsageError("code affected needs 1 to 200 --symbol/--file seeds.");
1964
+ if (Buffer.byteLength(JSON.stringify(p)) > 24e4) throw new UsageError("Code affected seeds exceed the request size limit; narrow the selection.");
1965
+ }
1966
+ integer(p.limit, "limit", 1, 100);
1967
+ integer(p.depth, "depth", 1, 6);
1968
+ integer(p.budgetTokens, "budget-tokens", 100, operation === "context" ? 32e3 : 16e3);
1969
+ }
1970
+ function command(op, p) {
1971
+ const parts = ["harness", "code", op];
1972
+ if (p.q !== void 0) parts.push(shell(p.q));
1973
+ if (op === "explain" && p.symbol) parts.push(shell(p.symbol));
1974
+ else if (p.symbol) parts.push("--symbol", shell(p.symbol));
1975
+ for (const id of p.symbols ?? []) parts.push("--symbol", shell(id));
1976
+ for (const file of p.files ?? []) parts.push("--file", shell(file));
1977
+ for (const [key, value] of [["limit", p.limit], ["depth", p.depth], ["budget-tokens", p.budgetTokens], ["kind", p.kind], ["path-prefix", p.pathPrefix]])
1978
+ if (value !== void 0) parts.push(`--${key}`, typeof value === "number" ? String(value) : shell(value));
1979
+ if (p.includeAmbiguous) parts.push("--include-ambiguous");
1980
+ if (p.includeTypes) parts.push("--include-types");
1981
+ if (p.worktree) parts.push("--path", shell(p.worktree));
1982
+ return named(parts.join(" "));
1983
+ }
1984
+ function nextCommand(op, p, next) {
1985
+ if (!record(next)) return null;
1986
+ const updated = { ...p };
1987
+ let changed = false;
1988
+ for (const [key, field] of [["limit", "limit"], ["depth", "depth"], ["budget_tokens", "budgetTokens"]]) {
1989
+ if (key === "limit" && op !== "search" || key === "depth" && op !== "affected" || key === "budget_tokens" && op !== "explain" && op !== "context") continue;
1990
+ if (typeof next[key] === "number") {
1991
+ updated[field] = next[key];
1992
+ changed = true;
1993
+ }
1994
+ }
1995
+ if (!changed) return null;
1607
1996
  try {
1608
- mkdirSync(dirname(path), { recursive: true });
1609
- writeFileSync(temporary, `${process.pid}
1610
- `);
1997
+ validateQuery(op, updated);
1611
1998
  } catch {
1612
1999
  return null;
1613
2000
  }
2001
+ return command(op, updated);
2002
+ }
2003
+ function renderQuery(operation, params, raw) {
2004
+ const parsed = envelopeSchema.safeParse(raw);
2005
+ if (!parsed.success) fail();
2006
+ const response = parsed.data;
2007
+ if (operation === "search" ? !Array.isArray(response.data) : !record(response.data)) fail();
2008
+ const { view: view2, truncation } = response.meta;
2009
+ const guidance = [];
2010
+ const lines = [`View ${view2.view_id}: version ${view2.active.version}; base ${view2.active.base_commit}; overlay ${view2.active.overlay_digest}`];
2011
+ if (view2.pending) lines.push(`Pending version ${view2.pending.version}: ${clean(view2.pending.state)}${view2.pending.progress ? ` (${view2.pending.progress.done}/${view2.pending.progress.total})` : ""}`);
2012
+ let visited = 0;
2013
+ const buckets = {};
2014
+ const walk = (value, label3, depth, enclosingSymbol, countBuckets = true) => {
2015
+ if (++visited > 1e5 || depth > 32) fail();
2016
+ if (record(value)) {
2017
+ const symbol = typeof value["symbol_id"] === "string" ? value["symbol_id"] : enclosingSymbol;
2018
+ if ("path" in value && "start_line" in value && "sha256" in value) {
2019
+ const citation = citationSchema.safeParse(value);
2020
+ if (!citation.success || citation.data.end_line < citation.data.start_line || citation.data.version !== view2.active.version) fail();
2021
+ const c = citation.data;
2022
+ lines.push(`${label3}: ${clean(c.path)}:${c.start_line}${c.end_line === c.start_line ? "" : `-${c.end_line}`}@${c.sha256} [${c.from}, version ${c.version}]`);
2023
+ const context = command("context", { ...symbol || params.symbol ? { symbol: symbol ?? params.symbol } : { q: params.q ?? "symbol name" }, worktree: params.worktree });
2024
+ lines.push(c.snippet !== void 0 ? c.snippet.split("\n").map(clean).join("\n") : `Snippet not supplied; retrieve indexed text: ${context}`);
2025
+ for (const [key, extra] of Object.entries(value)) if (!["path", "start_line", "end_line", "sha256", "version", "from", "snippet"].includes(key)) walk(extra, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
2026
+ } else for (const [key, child] of Object.entries(value)) walk(child, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
2027
+ } else if (Array.isArray(value)) {
2028
+ if (countBuckets && label3 !== "data") buckets[label3.slice(5)] = value.length;
2029
+ if (!value.length) lines.push(`${label3}: []`);
2030
+ value.forEach((v, i) => walk(v, `${label3}[${i}]`, depth + 1, enclosingSymbol, false));
2031
+ } else lines.push(`${label3}: ${clean(typeof value === "string" ? value : JSON.stringify(value))}`);
2032
+ };
2033
+ walk(response.data, "data", 0);
2034
+ const counts = Array.isArray(response.data) ? response.data.length : buckets;
2035
+ const total = typeof truncation["total"] === "number" ? truncation["total"] : response.meta["total"];
2036
+ lines.push(`Truncated: ${truncation.truncated ? "yes" : "no"}; returned: ${JSON.stringify(counts)}; total: ${typeof total === "number" ? total : "unknown (not supplied by server)"}`);
2037
+ if (truncation.truncated) {
2038
+ lines.push(`Truncation metadata: ${JSON.stringify(truncation)}`);
2039
+ const next = nextCommand(operation, params, truncation["next"]);
2040
+ guidance.push(next ? `Next: ${next}` : `Next: narrow the query or use rg -n; ${typeof truncation["hint"] === "string" ? clean(truncation["hint"]) : "the server supplied no larger request."}`);
2041
+ }
2042
+ const coverage = record(response.data) ? response.data["not_tracked"] : void 0;
2043
+ if (Array.isArray(coverage) && coverage.length) {
2044
+ for (const entry of coverage) if (record(entry)) guidance.push(`Coverage${entry["partial"] === true ? " (partial)" : ""}: ${clean(entry["reason"])}`);
2045
+ }
2046
+ if (Array.isArray(coverage) && coverage.length || Array.isArray(response.data) && response.data.length === 0) {
2047
+ const symbol = record(response.data) && record(response.data["symbol"]) ? response.data["symbol"]["name"] : void 0;
2048
+ guidance.push(`Search locally: rg -n --fixed-strings -- ${shell(params.q ?? (typeof symbol === "string" ? symbol : "symbol name"))} .`);
2049
+ }
2050
+ return { json: { ...response, guidance }, human: lines.concat(guidance).join("\n") + "\n" };
2051
+ }
2052
+ async function queryCode(api, operation, params, options) {
2053
+ validateQuery(operation, params);
2054
+ const id = await identity(options.cwd);
2055
+ const policy2 = await api.getPolicy();
2056
+ if (!z2.string().uuid().safeParse(policy2.org_id).success) fail();
2057
+ const path = statePath(api.origin, policy2.org_id, id.root, id.repoKey, options.stateDir);
2058
+ const state2 = await readState(path, api.origin, policy2.org_id, id.root, id.repoKey);
2059
+ if (!state2?.viewId || !state2.repositoryId) throw new CommandError(`This worktree has no code view. Run harness code sync --path ${shell(id.root)} first.`);
2060
+ const view2 = await api.getView(state2.viewId);
2061
+ const matches = (v) => v.view_id === state2.viewId && v.repository_id === state2.repositoryId && v.view_key === state2.viewKey;
2062
+ if (!matches(view2)) throw new CommandError("The code view does not match this repository/worktree; no query was sent.");
2063
+ if (!view2.active) throw new CommandError(`The view has no indexed version yet. Check harness code status --path ${shell(id.root)} until indexing finishes.`);
1614
2064
  try {
1615
- for (let attempt = 0; attempt < 2; attempt++) {
1616
- try {
1617
- linkSync(temporary, path);
1618
- return { path, pid: process.pid };
1619
- } catch {
1620
- const holder = readHolder(path);
1621
- if (holder === null) continue;
1622
- const aged = Date.now() - holder.mtimeMs > LOCK_STALE_MS;
1623
- const abandoned = holder.pid === null ? aged : !running(holder.pid) || aged;
1624
- if (!abandoned) return null;
1625
- try {
1626
- rmSync(path, { force: true });
1627
- } catch {
1628
- return null;
1629
- }
1630
- }
1631
- }
1632
- return null;
1633
- } finally {
1634
- try {
1635
- rmSync(temporary, { force: true });
1636
- } catch {
1637
- }
2065
+ const raw = await api.query(state2.viewId, operation, params);
2066
+ const result = renderQuery(operation, { ...params, worktree: id.root }, raw);
2067
+ if (!matches(result.json.meta.view)) throw new CommandError("The query view does not match this repository/worktree; no result was presented.");
2068
+ return result;
2069
+ } catch (error) {
2070
+ if (error instanceof HttpError && error.status === 404 && operation !== "search") throw new CommandError(`The code symbol or view was not found. Search again with ${command("search", { q: "symbol name", worktree: id.root })}; symbol IDs are opaque and can change after edits. Check code status for a removed view.`);
2071
+ throw error;
1638
2072
  }
1639
2073
  }
1640
- function releaseLock(lock) {
2074
+
2075
+ // src/commands/code/index.ts
2076
+ function enterWorktree(options) {
2077
+ const cwd = process.cwd();
2078
+ const before = statSync(".");
2079
+ const declared = statSync(cwd);
2080
+ if (before.dev !== declared.dev || before.ino !== declared.ino)
2081
+ throw new LocalError("Cannot enter the code worktree safely.");
2082
+ const supplied = options.path ?? cwd;
2083
+ if (/[\x00-\x1f\x7f-\x9f]/.test(supplied) || Buffer.from(supplied, "utf8").toString("utf8") !== supplied || options.path !== void 0 && supplied.includes("\uFFFD"))
2084
+ throw new LocalError("Cannot enter the code worktree safely.");
2085
+ const path = resolve2(supplied);
1641
2086
  try {
1642
- if (readHolder(lock.path)?.pid !== lock.pid) return;
1643
- rmSync(lock.path, { force: true });
2087
+ const target = statSync(path);
2088
+ if (options.path === void 0 && (before.dev !== target.dev || before.ino !== target.ino))
2089
+ throw new Error("current directory identity changed");
2090
+ process.chdir(path);
2091
+ const entered = statSync(".");
2092
+ if (entered.dev !== target.dev || entered.ino !== target.ino)
2093
+ throw new Error("worktree identity changed");
1644
2094
  } catch {
2095
+ throw new LocalError("Cannot enter the code worktree; check --path.");
1645
2096
  }
2097
+ return { ...options, path };
1646
2098
  }
1647
- function readHolder(path) {
2099
+ async function sync(ctx, options = {}) {
1648
2100
  try {
1649
- const mtimeMs = statSync(path).mtimeMs;
1650
- const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
1651
- return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
1652
- } catch {
1653
- return null;
2101
+ const result = await syncCode(new HttpCodeApi(ctx.config), {
2102
+ cwd: options.path ?? process.cwd(),
2103
+ skipped: options.skipped
2104
+ });
2105
+ return {
2106
+ json: result,
2107
+ exitCode: result.state === "failed" ? 1 : 0,
2108
+ human: `${clean(result.repo_key)}: ${clean(result.state)} version ${result.pending_version}; uploaded ${result.uploaded_blobs} overlay blobs.
2109
+ View: ${result.view_id}
2110
+ ${result.excluded.length} overlay files skipped; ${result.base.excluded_count ?? 0} base files skipped.${options.skipped ? "\n" + (result.base.skipped ?? []).map((s) => `${clean(s.path)}: ${clean(s.reason)}`).join("\n") : ""}
2111
+ ${result.warnings.map((w) => `Warning: ${clean(w)}
2112
+ `).join("")}`
2113
+ };
2114
+ } catch (error) {
2115
+ throw error instanceof HttpError ? describeCodeError(error) : error;
1654
2116
  }
1655
2117
  }
1656
- function running(pid) {
1657
- if (pid === null) return false;
2118
+ async function status2(ctx, options = {}) {
1658
2119
  try {
1659
- process.kill(pid, 0);
1660
- return true;
2120
+ const result = await statusCode(new HttpCodeApi(ctx.config), {
2121
+ cwd: options.path ?? process.cwd(),
2122
+ skipped: options.skipped
2123
+ });
2124
+ const report = "base" in result ? result.base : void 0;
2125
+ const pending = "pending" in result ? result.pending : null;
2126
+ const serverError = pending?.error ?? report?.error;
2127
+ const failure = serverError && typeof serverError === "object" && "message" in serverError && typeof serverError.message === "string" ? clean(serverError.message) : "";
2128
+ return {
2129
+ json: result,
2130
+ exitCode: result.state === "failed" ? 1 : 0,
2131
+ human: `${clean(result.repo_key)}: ${result.state.replace("_", " ")}${failure ? ": " + failure : ""}
2132
+ ${report?.skipped?.map((s) => `${clean(s.path)}: ${clean(s.reason)}
2133
+ `).join("") ?? ""}${("warnings" in result ? result.warnings : [])?.map((w) => `Warning: ${clean(w)}
2134
+ `).join("") ?? ""}`
2135
+ };
1661
2136
  } catch (error) {
1662
- return error.code === "EPERM";
2137
+ throw error instanceof HttpError ? describeCodeError(error) : error;
1663
2138
  }
1664
2139
  }
1665
- function truthy(value) {
1666
- if (value === void 0) return false;
1667
- const normalized = value.trim().toLowerCase();
1668
- return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
1669
- }
1670
- function checksEnabled(env) {
1671
- return !truthy(env["CI"]) && !truthy(env["HARNESS_NO_UPDATE_CHECK"]);
1672
- }
1673
- function autoUpgradeEnabled(env) {
1674
- if (!checksEnabled(env)) return false;
1675
- const explicit = env["HARNESS_AUTO_UPGRADE"];
1676
- if (explicit !== void 0 && explicit.trim() !== "") return truthy(explicit);
1677
- return true;
2140
+ async function query(ctx, operation, params, options = {}) {
2141
+ try {
2142
+ return await queryCode(new HttpCodeApi(ctx.config), operation, params, { cwd: options.path ?? process.cwd() });
2143
+ } catch (error) {
2144
+ throw error instanceof HttpError ? describeCodeError(error) : error;
2145
+ }
1678
2146
  }
1679
2147
 
1680
2148
  // src/cli.ts
1681
2149
  function promptLine(question) {
1682
- const readline = createInterface2({
2150
+ const readline = createInterface({
1683
2151
  input: process.stdin,
1684
2152
  output: process.stderr
1685
2153
  });
1686
- return new Promise((resolve) => {
2154
+ return new Promise((resolve3) => {
1687
2155
  readline.question(question, (answer) => {
1688
2156
  readline.close();
1689
- resolve(answer);
2157
+ resolve3(answer);
1690
2158
  });
1691
2159
  });
1692
2160
  }
1693
- function exitWith(code) {
1694
- const drain = (stream) => new Promise((resolve) => stream.write("", () => resolve()));
2161
+ function exitWith(code2) {
2162
+ const drain = (stream) => new Promise((resolve3) => stream.write("", () => resolve3()));
1695
2163
  return Promise.all([drain(process.stdout), drain(process.stderr)]).then(
1696
- () => process.exit(code)
2164
+ () => process.exit(code2)
1697
2165
  );
1698
2166
  }
1699
2167
  guardBrokenPipes();
@@ -1708,13 +2176,13 @@ program.name(invokedAs).description(
1708
2176
  writeOut: (text) => process.stdout.write(named(text, invokedAs)),
1709
2177
  writeErr: (text) => writeStderr(text)
1710
2178
  });
1711
- function integer(value) {
2179
+ function integer2(value) {
1712
2180
  if (!/^\d+$/.test(value))
1713
2181
  throw new InvalidArgumentError("expected a non-negative integer");
1714
2182
  return Number.parseInt(value, 10);
1715
2183
  }
1716
2184
  function activityLimit(value) {
1717
- const limit = integer(value);
2185
+ const limit = integer2(value);
1718
2186
  if (limit < 1 || limit > ACTIVITY_MAX_LIMIT) {
1719
2187
  throw new InvalidArgumentError(
1720
2188
  `expected an integer from 1 to ${ACTIVITY_MAX_LIMIT}`
@@ -1763,7 +2231,7 @@ async function confirmRepoLogin(baseUrl, source, path) {
1763
2231
  trustHost(baseUrl);
1764
2232
  return true;
1765
2233
  }
1766
- async function withConfig(jsonFlag, mutating, run2) {
2234
+ async function withConfig(jsonFlag, mutating, run3) {
1767
2235
  const resolved = resolveConfig();
1768
2236
  for (const warning of resolved.warnings) writeStderr(warning + "\n");
1769
2237
  if (resolved.kind === "error") {
@@ -1796,25 +2264,25 @@ async function withConfig(jsonFlag, mutating, run2) {
1796
2264
  );
1797
2265
  return exitWith(1);
1798
2266
  }
1799
- return execute(again.config, jsonFlag, mutating, run2);
2267
+ return execute(again.config, jsonFlag, mutating, run3);
1800
2268
  }
1801
- return execute(resolved.config, jsonFlag, mutating, run2);
2269
+ return execute(resolved.config, jsonFlag, mutating, run3);
1802
2270
  }
1803
- async function execute(config, jsonFlag, mutating, run2) {
2271
+ async function execute(config, jsonFlag, mutating, run3) {
1804
2272
  const client = new Client(config);
1805
2273
  const hostNotice = mutating ? `\u2192 ${config.baseUrl}
1806
2274
  ` : "";
1807
2275
  try {
1808
- const output = await run2({ client, config });
2276
+ const output = await run3({ client, config });
1809
2277
  writeStderr(hostNotice);
1810
2278
  if (output.notice !== void 0) writeStderr(output.notice);
1811
2279
  process.stdout.write(jsonFlag ? json(output.json) : output.human);
1812
2280
  return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
1813
2281
  } catch (error) {
1814
- return fail(explainExpertRefusal(error, config.token), hostNotice);
2282
+ return fail2(explainExpertRefusal(error, config.token), hostNotice);
1815
2283
  }
1816
2284
  }
1817
- async function fail(error, hostNotice) {
2285
+ async function fail2(error, hostNotice) {
1818
2286
  if (error instanceof UsageError) {
1819
2287
  writeStderr(error.message + "\n");
1820
2288
  return exitWith(2);
@@ -1858,7 +2326,7 @@ async function fail(error, hostNotice) {
1858
2326
  program.command("ready").description("issues ready to work, highest priority first").option(
1859
2327
  "--claim",
1860
2328
  "claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
1861
- ).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2329
+ ).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1862
2330
  if (opts.claim === true) requireReadyClaimArgs(opts);
1863
2331
  await withConfig(
1864
2332
  opts.json === true,
@@ -1866,7 +2334,7 @@ program.command("ready").description("issues ready to work, highest priority fir
1866
2334
  (ctx) => ready(ctx, opts)
1867
2335
  );
1868
2336
  });
1869
- program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2337
+ program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1870
2338
  await withConfig(
1871
2339
  opts.json === true,
1872
2340
  false,
@@ -1879,7 +2347,7 @@ program.command("list").description("list issues with filters").option("--status
1879
2347
  ).option("--created-by <who>", "me or a creator user id").option(
1880
2348
  "--involved <who>",
1881
2349
  "me for issues created by, assigned to, or mentioning you"
1882
- ).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2350
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
1883
2351
  await withConfig(
1884
2352
  opts.json === true,
1885
2353
  false,
@@ -1888,16 +2356,16 @@ program.command("list").description("list issues with filters").option("--status
1888
2356
  });
1889
2357
  program.command("search").description(
1890
2358
  "search by meaning \u2014 ranked, exact identifier first; use list --search for paginated filtering"
1891
- ).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer).option(
2359
+ ).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer2).option(
1892
2360
  "--page <n>",
1893
2361
  "(deprecated, ignored \u2014 results are ranked and capped)",
1894
- integer
2362
+ integer2
1895
2363
  ).option("--all", "(deprecated, ignored \u2014 results are ranked and capped)").option("--json", "emit JSON").action(
1896
- async (query, opts) => {
2364
+ async (query2, opts) => {
1897
2365
  await withConfig(
1898
2366
  opts.json === true,
1899
2367
  false,
1900
- (ctx) => search(ctx, query, opts)
2368
+ (ctx) => search(ctx, query2, opts)
1901
2369
  );
1902
2370
  }
1903
2371
  );
@@ -2117,7 +2585,7 @@ plan.command("create").description("create a plan from a JSON or YAML file (the
2117
2585
  return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
2118
2586
  });
2119
2587
  });
2120
- plan.command("list").description("a team's plans, newest first: root, status, held, revision and title").option("--team <key>", "the team whose issues root the plans (or HARNESS_TEAM)").option("--status <status>", PLAN_STATUSES.join(" | ")).option("--limit <n>", `maximum plans (default 50, at most ${PLAN_LIST_MAX_LIMIT}; there are no pages)`, integer).option("--json", "emit JSON").action(async (opts) => {
2588
+ plan.command("list").description("a team's plans, newest first: root, status, held, revision and title").option("--team <key>", "the team whose issues root the plans (or HARNESS_TEAM)").option("--status <status>", PLAN_STATUSES.join(" | ")).option("--limit <n>", `maximum plans (default 50, at most ${PLAN_LIST_MAX_LIMIT}; there are no pages)`, integer2).option("--json", "emit JSON").action(async (opts) => {
2121
2589
  requirePlanListArgs(opts);
2122
2590
  await withConfig(opts.json === true, false, (ctx) => planList(ctx, opts));
2123
2591
  });
@@ -2137,7 +2605,7 @@ plan.command("edit").description("change step executors and keys, and budgets, f
2137
2605
  return planEdit(ctx, id, readPlanFile(opts.file));
2138
2606
  });
2139
2607
  });
2140
- plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[plan]", PLAN_ARGUMENT).option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer).option("--json", "emit JSON").action(
2608
+ plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[plan]", PLAN_ARGUMENT).option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer2).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer2).option("--json", "emit JSON").action(
2141
2609
  async (id, opts) => {
2142
2610
  await withConfig(opts.json === true, true, (ctx) => {
2143
2611
  if (id === void 0) throw new UsageError("usage: harness plan release <plan-id-or-root>");
@@ -2178,7 +2646,7 @@ var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which th
2178
2646
  program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
2179
2647
  "--status <states>",
2180
2648
  `comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
2181
- ).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
2649
+ ).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer2).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
2182
2650
  ${NO_OBJECTIVE_TEXT}
2183
2651
  `).action(async (opts) => {
2184
2652
  requireJobsArgs(opts);
@@ -2195,7 +2663,7 @@ ${NO_OBJECTIVE_TEXT}
2195
2663
  (ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
2196
2664
  );
2197
2665
  });
2198
- program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
2666
+ program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer2).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
2199
2667
  "after",
2200
2668
  `
2201
2669
  Polls the job's events every 2 s, and every 10 s while the job is paused or
@@ -2416,7 +2884,7 @@ program.command("attach-image").description("upload an image; prints markdown to
2416
2884
  });
2417
2885
  });
2418
2886
  var project = program.command("project").description("projects");
2419
- project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2887
+ project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2420
2888
  await withConfig(
2421
2889
  opts.json === true,
2422
2890
  false,
@@ -2464,7 +2932,7 @@ milestone.command("create").option("--project <id>", "project id (required)").op
2464
2932
  );
2465
2933
  });
2466
2934
  var doc = program.command("doc").description("documents");
2467
- doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2935
+ doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2468
2936
  await withConfig(
2469
2937
  opts.json === true,
2470
2938
  false,
@@ -2648,14 +3116,14 @@ var tools = program.command("tools").description(
2648
3116
  requireToolsOpts(opts);
2649
3117
  await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
2650
3118
  });
2651
- tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query, opts) => {
3119
+ tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query2, opts) => {
2652
3120
  const merged = { ...tools.opts(), ...opts };
2653
- if (query.length === 0) {
3121
+ if (query2.length === 0) {
2654
3122
  throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
2655
3123
  }
2656
- requireSearchQuery(query.join(" "));
3124
+ requireSearchQuery(query2.join(" "));
2657
3125
  requireToolsOpts(merged);
2658
- await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query.join(" "), merged));
3126
+ await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query2.join(" "), merged));
2659
3127
  });
2660
3128
  tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
2661
3129
  const parent = tools.opts();
@@ -2674,7 +3142,7 @@ program.command("skills").description("the organization's skills assigned to Ope
2674
3142
  await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
2675
3143
  });
2676
3144
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
2677
- const { startMcpServer } = await import("./server-F6YRKNC2.js");
3145
+ const { startMcpServer } = await import("./server-TRXBTJSA.js");
2678
3146
  await startMcpServer(invokedAs);
2679
3147
  });
2680
3148
  var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
@@ -2688,14 +3156,48 @@ keys.command("exec").description(
2688
3156
  "pass only these provider slugs (comma-separated, repeatable); default: everything stored",
2689
3157
  collect,
2690
3158
  []
2691
- ).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command, opts) => {
3159
+ ).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command2, opts) => {
2692
3160
  const only = opts.only.flatMap((value) => value.split(","));
2693
3161
  await withConfig(
2694
3162
  false,
2695
3163
  false,
2696
- (ctx) => exec(ctx, { command, override: opts.override === true, only })
3164
+ (ctx) => exec(ctx, { command: command2, override: opts.override === true, only })
2697
3165
  );
2698
3166
  });
3167
+ var code = program.command("code").description("sync and inspect a code-index worktree overlay");
3168
+ code.command("sync").description("sync an overlay on a server-built GitHub base (needs harness:read and harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
3169
+ const target = enterWorktree(opts);
3170
+ await withConfig(opts.json === true, true, (ctx) => sync(ctx, target));
3171
+ });
3172
+ code.command("status").description("inspect this worktree view; --skipped also asks for base status (needs harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
3173
+ const target = enterWorktree(opts);
3174
+ await withConfig(opts.json === true, opts.skipped === true, (ctx) => status2(ctx, target));
3175
+ });
3176
+ for (const operation of ["search", "explain", "affected", "context"]) {
3177
+ const query2 = code.command(operation).description({ search: "find indexed declarations", explain: "explain an opaque symbol ID and its callers", affected: "find impact from symbol IDs or file paths", context: "retrieve indexed source snippets within a token budget" }[operation]);
3178
+ if (operation === "search" || operation === "explain" || operation === "context") query2.argument("[value]");
3179
+ if (operation === "search") query2.option("--kind <kind>").option("--path-prefix <prefix>").option("--limit <n>", "1 to 100", integer2);
3180
+ if (operation === "context") query2.option("--symbol <id>", "opaque ID from search");
3181
+ if (operation === "context" || operation === "explain") query2.option("--budget-tokens <n>", "indexed snippet token budget", integer2);
3182
+ if (operation === "affected") query2.option("--symbol <id>", "repeat for multiple IDs", (value, all) => [...all, value], []).option("--file <path>", "repeat for multiple paths", (value, all) => [...all, value], []).option("--depth <n>", "1 to 6", integer2).option("--include-ambiguous", "follow possible bindings too").option("--include-types", "follow type dependents too");
3183
+ query2.option("--path <directory>", "Git worktree").option("--json", "emit response data, meta and guidance");
3184
+ const run3 = async (value, opts) => {
3185
+ const target = enterWorktree({ path: opts["path"] });
3186
+ const params = {
3187
+ ...operation === "search" || operation === "context" ? { q: value } : {},
3188
+ ...operation === "explain" ? { symbol: value } : operation === "context" ? { symbol: opts["symbol"] } : {},
3189
+ ...operation === "affected" ? { symbols: opts["symbol"], files: opts["file"], depth: opts["depth"], includeAmbiguous: opts["includeAmbiguous"], includeTypes: opts["includeTypes"] } : {},
3190
+ kind: opts["kind"],
3191
+ pathPrefix: opts["pathPrefix"],
3192
+ limit: opts["limit"],
3193
+ budgetTokens: opts["budgetTokens"]
3194
+ };
3195
+ validateQuery(operation, params);
3196
+ await withConfig(opts["json"] === true, false, (ctx) => query(ctx, operation, params, target));
3197
+ };
3198
+ if (operation === "affected") query2.action(async (opts) => run3(void 0, opts));
3199
+ else query2.action(run3);
3200
+ }
2699
3201
  var review = program.command("review").description("a pull request's enforced Review Council gate, and a retry of its latest attempt");
2700
3202
  review.command("status").description("the gate's enforced and advisory status, and a failed latest attempt with what comes next").argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--json", "emit JSON").action(async (pr, opts) => {
2701
3203
  const ref = parsePullRequest(pr, opts.repo);
@@ -2710,7 +3212,7 @@ review.command("retry").description(
2710
3212
  program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
2711
3213
  "--timeout <ms>",
2712
3214
  "how long to wait for the browser callback",
2713
- integer
3215
+ integer2
2714
3216
  ).option("--json", "emit JSON").action(
2715
3217
  async (opts) => {
2716
3218
  const repoLookup = findRepoConfig();
@@ -2801,6 +3303,10 @@ try {
2801
3303
  if (notice !== null) writeStderr(notice);
2802
3304
  } catch {
2803
3305
  }
3306
+ if (invokedAs === "operator") {
3307
+ const deprecation = operatorDeprecationNotice();
3308
+ if (deprecation !== null) writeStderr(deprecation);
3309
+ }
2804
3310
  if (invokedAs === "operator") layoutOperatorHelp(program);
2805
3311
  if (argv.length <= 2) {
2806
3312
  program.outputHelp({ error: true });