@harnessforce/cli 0.2.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +39 -11
  2. package/dist/bin.js +824 -465
  3. package/package.json +5 -5
package/dist/bin.js CHANGED
@@ -3,15 +3,15 @@ import { createRequire } from "node:module";
3
3
  import { constants, homedir, tmpdir } from "node:os";
4
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
5
5
  import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
6
- import { createHash, randomBytes, randomUUID } from "node:crypto";
7
- import { access, chmod, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
6
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
7
+ import { access, chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
8
8
  import { execFile, spawn } from "node:child_process";
9
9
  import { constants as constants$1, createReadStream } from "node:fs";
10
10
  import { createServer } from "node:http";
11
- import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
11
+ import { ATTR, INTERVENTION_KINDS, LOOP_KINDS, MAX_WORK_LOCATIONS, PROPOSAL_KINDS, isToken, normalizeRepository } from "@harnessforce/semconv";
12
12
  import { createInterface } from "node:readline";
13
13
  import { isDeepStrictEqual } from "node:util";
14
- //#region ../agent-core/src/config/canonical.ts
14
+ // ../agent-core/src/config/canonical.ts
15
15
  const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
16
16
  function canonicalJson(value) {
17
17
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -28,11 +28,9 @@ function sortComponents(components) {
28
28
  return [...components].sort((a, b) => byCodeUnit$1(a.kind, b.kind) || byCodeUnit$1(a.source, b.source) || byCodeUnit$1(a.id, b.id));
29
29
  }
30
30
  const snapshotId = (components) => hashValue(sortComponents(components));
31
- //#endregion
32
- //#region ../agent-core/src/object.ts
31
+ // ../agent-core/src/object.ts
33
32
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
- //#endregion
35
- //#region ../agent-core/src/config/files.ts
33
+ // ../agent-core/src/config/files.ts
36
34
  const MAX_DEPTH = 8;
37
35
  const MAX_FILE_BYTES = 1048576;
38
36
  var CollectionExpired = class extends Error {};
@@ -93,8 +91,7 @@ async function listDirectories(dir, guard) {
93
91
  for (const entry of await entries(dir, guard)) if (await kindOf(dir, entry, guard) === "directory") names.push(entry.name);
94
92
  return names;
95
93
  }
96
- //#endregion
97
- //#region ../agent-core/src/managed.ts
94
+ // ../agent-core/src/managed.ts
98
95
  const MANAGED_DIRS = {
99
96
  darwin: "/Library/Application Support/ClaudeCode",
100
97
  win32: "C:\\Program Files\\ClaudeCode"
@@ -113,8 +110,7 @@ async function readManagedEnv(managedDir, names) {
113
110
  }
114
111
  return values;
115
112
  }
116
- //#endregion
117
- //#region ../agent-core/src/process/lookup.ts
113
+ // ../agent-core/src/process/lookup.ts
118
114
  const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
119
115
  const NPM_SHIM_MAX_BYTES = 65536;
120
116
  const SHEBANG_MAX_BYTES = 256;
@@ -295,8 +291,7 @@ async function commandLineFor(file, args, context, { quoteArgs = true } = {}) {
295
291
  verbatim: true
296
292
  };
297
293
  }
298
- //#endregion
299
- //#region ../agent-core/src/process/git.ts
294
+ // ../agent-core/src/process/git.ts
300
295
  const GIT_TIMEOUT_MS$1 = 1e3;
301
296
  const execGit = (file, args, env, timeoutMs) => new Promise((resolve) => {
302
297
  execFile(file, args, {
@@ -334,8 +329,7 @@ function createGitRunner({ platform, env, processCwd, excludeTarget, timeoutMs =
334
329
  ], gitEnv, timeoutMs);
335
330
  };
336
331
  }
337
- //#endregion
338
- //#region ../agent-core/src/process/runtime-env.ts
332
+ // ../agent-core/src/process/runtime-env.ts
339
333
  const RUNTIME_VARIABLES = [
340
334
  "NODE_TLS_REJECT_UNAUTHORIZED",
341
335
  "NODE_EXTRA_CA_CERTS",
@@ -412,8 +406,7 @@ function takeStashedRuntimeEnv(env, platform) {
412
406
  if (!isObject(value)) return {};
413
407
  return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && isRuntimeVariable(entry[0], platform)));
414
408
  }
415
- //#endregion
416
- //#region src/shared/messages.ts
409
+ // src/shared/messages.ts
417
410
  const INIT_MESSAGES = {
418
411
  success: "Connected to Harnessforce. Restart Claude Code to apply the settings",
419
412
  pluginAutoUpdateOn: "Automatic updates for the Harnessforce plugin are on. To turn them off, run `harnessforce init --no-plugin-auto-update` or use /plugin",
@@ -432,25 +425,32 @@ const INIT_MESSAGES = {
432
425
  noWorkspace: "You are not a member of any Workspace. Create a Workspace or accept an invitation, then run `harnessforce init` again",
433
426
  loginFailed: "The login failed. Run `harnessforce init` again",
434
427
  viewer: "The view-only role cannot issue an ingest key. Contact a Workspace administrator",
435
- limitOwnerAdmin: "The number of active ingest keys has reached the limit. Revoke unused keys on the ingest keys page",
428
+ limitOwnerAdmin: "The number of active ingest keys has reached the limit. Ask members to revoke unused keys in My ingest keys on their account page",
436
429
  limitMember: "The ingest key limit has been reached. Contact a Workspace administrator",
437
430
  gateUnavailable: "Could not apply the change on the receiving side. Nothing was changed. Try again",
438
431
  ingestUnavailable: "This environment does not receive telemetry, so an ingest key cannot be issued",
439
432
  saveFailed: "Could not save the ingest key. Run `harnessforce init` again",
440
433
  codexSaveFailed: "Could not write the Codex configuration. Run `harnessforce init` again",
441
- contentEnabled: "Sending the prompts you type is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. Within the workspace, only you can read your stored content, and it is deleted after the content retention period (at most 30 days)",
442
- contentNotOptedIn: "This workspace has not opted in to prompts you typed, so sending content was not enabled. Opt in under Data retention in the workspace settings, then run `harnessforce init --send-content` again"
434
+ contentNotStored: "This workspace does not store content, so sending content was not enabled. An owner or admin can turn it on under Collection & retention in the workspace settings, then run `harnessforce init --send-content` again",
435
+ contentNotSent: "Content is not sent from this machine. Run `harnessforce init --send-content` to send it"
443
436
  };
444
437
  const codexNotes = (configPath) => [
445
438
  `Wrote the Codex configuration (${configPath}). Restart Codex to apply it`,
446
- "Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart hook in /hooks",
439
+ "Codex hooks do not run until you trust them in Codex's /hooks. Trust the harnessforce SessionStart, PostToolUse, and Stop hooks in /hooks",
447
440
  "To use the Harnessforce MCP server, log in with `codex mcp login harnessforce`"
448
441
  ].join("\n");
449
442
  const nonDefaultConnectionMessage = (url) => `Connecting to ${url} (not the default production endpoint)`;
450
443
  const listenFailedMessage = (port) => `${INIT_MESSAGES.listenFailed}. Stop the process using port ${port}, or specify another port with \`harnessforce init --port\``;
451
444
  const keychainUnavailableMessage = (keychain, message = INIT_MESSAGES.keychainUnavailable) => keychain.unavailableHint ? `${message}. ${keychain.unavailableHint}` : message;
452
- //#endregion
453
- //#region src/credentials/keychain.ts
445
+ const CONTENT_READERS = {
446
+ all_members: "Everyone in the workspace can read it",
447
+ owners_admins_and_sender: "Owners, admins and you can read it",
448
+ sender_only: "Within the workspace, only you can read it"
449
+ };
450
+ function contentEnabledMessage(content) {
451
+ return `Sending ${[...content.isPromptsStored ? ["the prompts you type"] : [], ...content.isModelAndToolsStored ? ["model responses and tool input and output"] : []].join(", ")} is now enabled. Values that match known patterns for secrets and personal information are masked, but not everything can be detected. ${CONTENT_READERS[content.visibility]}. It is deleted after ${content.retentionDays} days`;
452
+ }
453
+ // src/credentials/keychain.ts
454
454
  async function openSingleStore(store) {
455
455
  if (!await store.isAvailable()) return void 0;
456
456
  return {
@@ -476,8 +476,7 @@ const workspaceAccounts = (workspaceId) => [
476
476
  ];
477
477
  const isIngestKeyAccount = (account) => account.endsWith(INGEST_KEY_SUFFIX);
478
478
  const apiTokenWorkspaceId = (account) => account.endsWith(API_TOKEN_SUFFIX) ? account.slice(0, -10) : void 0;
479
- //#endregion
480
- //#region src/credentials/linux-keychain.ts
479
+ // src/credentials/linux-keychain.ts
481
480
  const SECRET_SERVICE_TIMEOUT_MS = 6e4;
482
481
  const WAITING_NOTICE_DELAY_MS = 2e3;
483
482
  const PROBE_ACCOUNT$1 = "availability-probe";
@@ -631,8 +630,7 @@ async function migrate({ kernel, secretService }, workspaceId) {
631
630
  clearTimeout(timer);
632
631
  }
633
632
  }
634
- //#endregion
635
- //#region src/credentials/os-keychain.ts
633
+ // src/credentials/os-keychain.ts
636
634
  const PROBE_ACCOUNT = "availability-probe";
637
635
  function createOsKeychain({ platform, load, secretService }) {
638
636
  let loaded;
@@ -714,8 +712,7 @@ const unsupported = {
714
712
  withTimeout: () => unsupported,
715
713
  openForInit: async () => void 0
716
714
  };
717
- //#endregion
718
- //#region src/credentials/secret-service-process.ts
715
+ // src/credentials/secret-service-process.ts
719
716
  const CHILD_SCRIPT = `
720
717
  const [op, service, account, timeoutMs, modulePath] = process.argv.slice(1);
721
718
  setTimeout(() => process.kill(process.pid, "SIGKILL"), Number(timeoutMs));
@@ -790,8 +787,7 @@ function createSecretServiceProcess({ execPath, resolveKeyring }) {
790
787
  }
791
788
  };
792
789
  }
793
- //#endregion
794
- //#region src/import/repository.ts
790
+ // src/import/repository.ts
795
791
  const GIT_TIMEOUT_MS = 5e3;
796
792
  const createImportGit = (platform, env, processCwd) => createGitRunner({
797
793
  platform,
@@ -800,8 +796,7 @@ const createImportGit = (platform, env, processCwd) => createGitRunner({
800
796
  excludeTarget: false,
801
797
  timeoutMs: GIT_TIMEOUT_MS
802
798
  });
803
- //#endregion
804
- //#region src/init/browser.ts
799
+ // src/init/browser.ts
805
800
  const STILL_RUNNING_MS = 3e3;
806
801
  function openerFor(platform, url) {
807
802
  if (platform === "darwin") return ["open", [url]];
@@ -838,35 +833,63 @@ async function openBrowser(url, { platform, env, cwd, restoredEnv, spawn: spawnI
838
833
  child.once("exit", (code) => finish(code === 0));
839
834
  });
840
835
  }
841
- //#endregion
842
- //#region src/init/loopback.ts
836
+ // src/init/loopback.ts
843
837
  const HOST = "127.0.0.1";
844
838
  const CALLBACK_PATH = "/callback";
845
- const page = (message) => `<!doctype html><html lang="en"><meta charset="utf-8"><title>Harnessforce</title><p>${message}</p></html>`;
846
- const SUCCESS_PAGE = page("Return to the terminal");
847
- const FAILURE_PAGE = page("The login failed. Check the terminal");
839
+ const COMPLETION_PATH = "/cli/connected";
840
+ function completionUrl(webOrigin, completion) {
841
+ const url = new URL(COMPLETION_PATH, webOrigin);
842
+ url.searchParams.set("status", completion.status);
843
+ if (completion.status === "connected" && completion.workspaceId) url.searchParams.set("workspace", completion.workspaceId);
844
+ return url.href;
845
+ }
846
+ function parseRequestUrl(target) {
847
+ try {
848
+ return new URL(target ?? "/", `http://${HOST}`);
849
+ } catch {
850
+ return;
851
+ }
852
+ }
853
+ function isSameState(received, expected) {
854
+ if (received === null) return false;
855
+ const receivedBytes = Buffer.from(received, "utf8");
856
+ const expectedBytes = Buffer.from(expected, "utf8");
857
+ return receivedBytes.length === expectedBytes.length && timingSafeEqual(receivedBytes, expectedBytes);
858
+ }
848
859
  async function startLoopback(options) {
849
860
  let settle = () => {};
850
861
  const callback = new Promise((resolve) => {
851
862
  settle = resolve;
852
863
  });
864
+ let held;
853
865
  const server = createServer((req, res) => {
854
- const url = new URL(req.url ?? "/", `http://${HOST}`);
866
+ const url = parseRequestUrl(req.url);
867
+ if (url === void 0) {
868
+ res.writeHead(400).end();
869
+ return;
870
+ }
855
871
  if (url.pathname !== CALLBACK_PATH) {
856
872
  res.writeHead(404).end();
857
873
  return;
858
874
  }
859
- const received = {
875
+ if (isStopped) {
876
+ res.writeHead(404).end();
877
+ return;
878
+ }
879
+ if (!isSameState(url.searchParams.get("state"), options.state)) {
880
+ res.writeHead(303, {
881
+ location: completionUrl(options.webOrigin, { status: "failed" }),
882
+ "cache-control": "no-store"
883
+ });
884
+ res.end();
885
+ return;
886
+ }
887
+ held = res;
888
+ stop({
860
889
  code: url.searchParams.get("code") || void 0,
861
890
  state: url.searchParams.get("state") ?? void 0,
862
891
  error: url.searchParams.get("error") ?? void 0
863
- };
864
- const isSuccess = !received.error && Boolean(received.code) && received.state === options.state;
865
- res.writeHead(200, {
866
- "content-type": "text/html; charset=utf-8",
867
- "cache-control": "no-store"
868
892
  });
869
- res.end(isSuccess ? SUCCESS_PAGE : FAILURE_PAGE, () => stop(received));
870
893
  });
871
894
  const timer = setTimeout(() => stop(void 0), options.timeoutMs);
872
895
  let isStopped = false;
@@ -875,9 +898,19 @@ async function startLoopback(options) {
875
898
  isStopped = true;
876
899
  clearTimeout(timer);
877
900
  server.close();
878
- server.closeAllConnections();
901
+ if (!held) server.closeAllConnections();
879
902
  settle(received);
880
903
  }
904
+ function finish(completion) {
905
+ const response = held;
906
+ held = void 0;
907
+ if (!response) return;
908
+ response.writeHead(303, {
909
+ location: completionUrl(options.webOrigin, completion),
910
+ "cache-control": "no-store"
911
+ });
912
+ response.end(() => server.closeAllConnections());
913
+ }
881
914
  await new Promise((resolve, reject) => {
882
915
  server.once("error", reject);
883
916
  server.listen(options.port, HOST, resolve);
@@ -888,11 +921,11 @@ async function startLoopback(options) {
888
921
  const { port } = server.address();
889
922
  return {
890
923
  redirectUri: `http://${HOST}:${port}${CALLBACK_PATH}`,
891
- callback
924
+ callback,
925
+ finish
892
926
  };
893
927
  }
894
- //#endregion
895
- //#region ../agent-core/src/url.ts
928
+ // ../agent-core/src/url.ts
896
929
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
897
930
  "localhost",
898
931
  "127.0.0.1",
@@ -914,8 +947,7 @@ function underBase(base, path) {
914
947
  return url;
915
948
  }
916
949
  const withoutExtras = (url) => `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
917
- //#endregion
918
- //#region ../agent-core/src/client-update.ts
950
+ // ../agent-core/src/client-update.ts
919
951
  const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
920
952
  const UPDATE_COMMANDS = {
921
953
  cli: "npm install -g @harnessforce/cli@latest",
@@ -1074,8 +1106,7 @@ async function recheckClientStatus(options) {
1074
1106
  }
1075
1107
  return await readUpdateState(options.statePath, options.version) ?? state;
1076
1108
  }
1077
- //#endregion
1078
- //#region ../agent-core/src/ingest.ts
1109
+ // ../agent-core/src/ingest.ts
1079
1110
  const SEND_TIMEOUT_MS = 2e3;
1080
1111
  function ingestBaseFrom(endpoint) {
1081
1112
  const base = parseAllowedUrl(endpoint);
@@ -1110,8 +1141,7 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
1110
1141
  };
1111
1142
  }
1112
1143
  }
1113
- //#endregion
1114
- //#region ../agent-core/src/config/hash-input.ts
1144
+ // ../agent-core/src/config/hash-input.ts
1115
1145
  function withoutSecrets(entry) {
1116
1146
  const { env, headers, url, args: _args, ...rest } = entry;
1117
1147
  const result = rest;
@@ -1143,8 +1173,7 @@ function hashInput(kind, value) {
1143
1173
  if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
1144
1174
  return value;
1145
1175
  }
1146
- //#endregion
1147
- //#region ../agent-core/src/config/scope.ts
1176
+ // ../agent-core/src/config/scope.ts
1148
1177
  const MAX_COMPONENTS = 1e3;
1149
1178
  var TooManyComponents = class extends Error {};
1150
1179
  var Sink = class {
@@ -1200,11 +1229,9 @@ function collectMcpServers$1(s, servers) {
1200
1229
  if (!isObject(servers)) return;
1201
1230
  for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
1202
1231
  }
1203
- //#endregion
1204
- //#region ../agent-core/src/plugin-auto-update.ts
1232
+ // ../agent-core/src/plugin-auto-update.ts
1205
1233
  const MARKETPLACE_NAME = "harnessforce-agent";
1206
- //#endregion
1207
- //#region src/shared/settings.ts
1234
+ // src/shared/settings.ts
1208
1235
  const OTEL_HEADERS_HELPER = "harnessforce otel-headers";
1209
1236
  const PLUGIN_ID$1 = "harnessforce@harnessforce-agent";
1210
1237
  const NEW_FILE_MODE = 384;
@@ -1292,8 +1319,7 @@ async function writeUserSettings(path, settings) {
1292
1319
  await rm(temporary, { force: true });
1293
1320
  }
1294
1321
  }
1295
- //#endregion
1296
- //#region src/destinations.ts
1322
+ // src/destinations.ts
1297
1323
  const NAMES = [
1298
1324
  "HARNESSFORCE_URL",
1299
1325
  "HARNESSFORCE_ENDPOINT",
@@ -1314,8 +1340,7 @@ async function resolveCliDestinations(env, settingsPath, defaultUrl) {
1314
1340
  readApiUrl: pick("HARNESSFORCE_URL") ?? defaultUrl
1315
1341
  };
1316
1342
  }
1317
- //#endregion
1318
- //#region src/client-update.ts
1343
+ // src/client-update.ts
1319
1344
  const updateStatePath = (homeDir) => join(homeDir, ".harnessforce", "update-status.json");
1320
1345
  const readCliUpdateState = (homeDir, version) => readUpdateState(updateStatePath(homeDir), version);
1321
1346
  /**
@@ -1348,8 +1373,7 @@ async function noticeUpdate(deps) {
1348
1373
  });
1349
1374
  if (line) deps.stderr(`${line}\n`);
1350
1375
  }
1351
- //#endregion
1352
- //#region src/help.ts
1376
+ // src/help.ts
1353
1377
  const SENDS_NOTHING_LOCAL = "Nothing. It reads local files only and makes no network request.";
1354
1378
  const COMMANDS$1 = {
1355
1379
  init: {
@@ -1366,8 +1390,9 @@ What it does:
1366
1390
 
1367
1391
  Options:
1368
1392
  --port <port> Loopback port for the login callback, 1-65535 (default 8080).
1369
- --send-content Also send the text of the prompts you type. Takes effect only
1370
- when the Workspace has opted in. Off by default.
1393
+ --send-content Also send the prompts you type, model responses and tool
1394
+ input and output. Takes effect only for the categories the
1395
+ Workspace stores. Without it, init turns all of them off.
1371
1396
  --no-plugin-auto-update
1372
1397
  Turn off automatic updates of the Harnessforce plugin in
1373
1398
  Claude Code. On by default; a value you set is kept.
@@ -1377,9 +1402,9 @@ Sends:
1377
1402
  telemetry (token counts, tool names, timings) with that key.
1378
1403
 
1379
1404
  Does not send:
1380
- Prompt text (unless --send-content and the Workspace opt-in), model responses,
1381
- tool input or output, code diffs, and secrets. The key is never written to a
1382
- plain file.
1405
+ Prompt text, model responses and tool input or output (unless --send-content
1406
+ and the Workspace stores that category), code diffs, and secrets. The key is
1407
+ never written to a plain file.
1383
1408
 
1384
1409
  Exit codes:
1385
1410
  0 Connected and configured.
@@ -1420,6 +1445,38 @@ Exit codes:
1420
1445
 
1421
1446
  Examples:
1422
1447
  harnessforce import
1448
+ `
1449
+ },
1450
+ status: {
1451
+ summary: "Show the Workspace setup steps that remain",
1452
+ text: `Usage: harnessforce status [--json]
1453
+
1454
+ When to use:
1455
+ After init, and any time you want to see which setup steps of the connected
1456
+ Workspace remain and whether this repository is connected to the GitHub App.
1457
+
1458
+ What it does:
1459
+ Reads the setup checklist of the connected Workspace and the list of
1460
+ connected repositories, and prints your role, each step (done, to do with its
1461
+ URL, or "?" when it cannot be read), and whether the repository of the current
1462
+ directory (its origin remote) is connected.
1463
+
1464
+ Options:
1465
+ --json Print the checklist and the repository as one JSON object.
1466
+
1467
+ Sends:
1468
+ Read requests with the API token. It writes nothing.
1469
+
1470
+ Does not send:
1471
+ The URL of the origin remote: it is compared on this machine.
1472
+
1473
+ Exit codes:
1474
+ 0 The status was read, whether or not steps remain.
1475
+ 1 Not logged in, the login expired, or communication failed.
1476
+
1477
+ Examples:
1478
+ harnessforce status
1479
+ harnessforce status --json
1423
1480
  `
1424
1481
  },
1425
1482
  "otel-headers": {
@@ -1469,8 +1526,8 @@ Sends:
1469
1526
  The same telemetry as an agent started directly, plus the Issue identifier.
1470
1527
 
1471
1528
  Does not send:
1472
- Anything beyond what init configured. Prompt text is sent only if you chose
1473
- --send-content at init.
1529
+ Anything beyond what init configured. Prompt text, responses and tool input
1530
+ and output are sent only if you chose --send-content at init.
1474
1531
 
1475
1532
  Exit codes:
1476
1533
  The agent's own exit code when it ran.
@@ -1484,6 +1541,8 @@ Examples:
1484
1541
  tune: {
1485
1542
  summary: "Analyze your past sessions and report counts",
1486
1543
  text: `Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json]
1544
+ harnessforce tune --report-only [--all]
1545
+ harnessforce tune --print-schedule
1487
1546
  harnessforce tune record
1488
1547
  harnessforce tune --purge
1489
1548
 
@@ -1503,6 +1562,11 @@ Options:
1503
1562
  --show-report Print the exact values that would be sent.
1504
1563
  --json Print the result as one JSON object on stdout. Messages go to
1505
1564
  stderr.
1565
+ --report-only Analyze and send, writing nothing to stdout (for your OS
1566
+ scheduler). Combines with --all only.
1567
+ --print-schedule
1568
+ Print a cron line that runs --report-only every Monday at
1569
+ 9:00. It changes nothing and sends nothing.
1506
1570
  record Read one proposal as JSON from stdin and store it locally.
1507
1571
  --purge Delete the analysis, proposals, and unsent results.
1508
1572
 
@@ -1524,6 +1588,7 @@ Exit codes:
1524
1588
  Examples:
1525
1589
  harnessforce tune
1526
1590
  harnessforce tune --no-send --json
1591
+ harnessforce tune --print-schedule
1527
1592
  harnessforce tune --purge
1528
1593
  `
1529
1594
  },
@@ -1561,20 +1626,26 @@ Examples:
1561
1626
  `
1562
1627
  },
1563
1628
  hook: {
1564
- summary: "Session-start hook (used by Codex)",
1565
- text: `Usage: harnessforce hook session-start
1629
+ summary: "Session hooks (used by Codex)",
1630
+ text: `Usage: harnessforce hook session-start|post-tool-use|stop
1566
1631
 
1567
1632
  When to use:
1568
- You normally never run it. Codex runs it when a session starts.
1633
+ You normally never run it. Codex runs session-start when a session starts,
1634
+ post-tool-use after it edits files with apply_patch, and stop at the end of
1635
+ each turn.
1569
1636
 
1570
1637
  What it does:
1571
- Reads the hook input from stdin, registers the session with the Workspace, and
1572
- prints context for the agent (the session ID, plus a one-line warning if
1573
- registration failed).
1638
+ session-start reads the hook input from stdin, registers the session with the
1639
+ Workspace, and prints context for the agent (the session ID, plus a one-line
1640
+ warning if registration failed).
1641
+ post-tool-use and stop record the repository and branch of the Git working
1642
+ trees the session worked in, and stop sends them when they changed.
1574
1643
 
1575
1644
  Sends:
1576
1645
  Session registration: agent, session ID, repository, branch, commit, and
1577
1646
  start time.
1647
+ Work locations: agent, session ID, and the repository and branch of each
1648
+ working tree. Paths, commits, and file names are not sent.
1578
1649
 
1579
1650
  Does not send:
1580
1651
  Prompt text, model responses, tool input or output, and code diffs.
@@ -1592,6 +1663,7 @@ Examples:
1592
1663
  const ORDER = [
1593
1664
  "init",
1594
1665
  "import",
1666
+ "status",
1595
1667
  "run",
1596
1668
  "tune",
1597
1669
  "top",
@@ -1631,8 +1703,7 @@ function resolveHelp(argv) {
1631
1703
  const separator = rest.indexOf("--");
1632
1704
  return (separator === -1 ? rest : rest.slice(0, separator)).some(isHelpFlag) ? COMMANDS$1[command].text : void 0;
1633
1705
  }
1634
- //#endregion
1635
- //#region ../agent-core/src/hook-warning.ts
1706
+ // ../agent-core/src/hook-warning.ts
1636
1707
  const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
1637
1708
  const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
1638
1709
  const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
@@ -1643,21 +1714,17 @@ const DESTINATION_MISMATCH_WARNING = {
1643
1714
  claude_code: "Telemetry is not being sent because the Harnessforce destination differs from the one saved by `harnessforce init`. Check this repository's settings or run `harnessforce init` again",
1644
1715
  codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
1645
1716
  };
1646
- //#endregion
1647
- //#region ../agent-core/src/issue.ts
1717
+ // ../agent-core/src/issue.ts
1648
1718
  const isIssueIdentifier = (value) => isToken(value, 300);
1649
- //#endregion
1650
- //#region ../agent-core/src/using-harnessforce.ts
1651
- const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
1652
- //#endregion
1653
- //#region ../agent-core/src/session-context.ts
1719
+ // ../agent-core/src/using-harnessforce.ts
1720
+ const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Work locations: the repository and branch of each Git working tree the session works in. Paths are not sent.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user sends them (`--send-content` or managed settings) and the Workspace stores that category. The members the Workspace chooses can read them. `harnessforce import` never sends them.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- Stop: sends the work locations at the end of a turn in which they changed.\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
1721
+ // ../agent-core/src/session-context.ts
1654
1722
  const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
1655
1723
  const sessionContext = (sessionId) => ({ hookSpecificOutput: {
1656
1724
  hookEventName: "SessionStart",
1657
1725
  additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
1658
1726
  } });
1659
- //#endregion
1660
- //#region ../agent-core/src/vcs.ts
1727
+ // ../agent-core/src/vcs.ts
1661
1728
  async function selectRemote(cwd, git) {
1662
1729
  if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
1663
1730
  const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
@@ -1707,8 +1774,7 @@ async function resolveProjectRoot(cwd, git) {
1707
1774
  const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
1708
1775
  return topLevel && isAbsolute(topLevel) ? topLevel : cwd;
1709
1776
  }
1710
- //#endregion
1711
- //#region src/credentials/user-destination.ts
1777
+ // src/credentials/user-destination.ts
1712
1778
  async function resolveUserDestination(deps) {
1713
1779
  const { workspaceId, ingestEndpoint } = await resolveCliDestinations(deps.env, userSettingsPath(deps.env, deps.homeDir), "");
1714
1780
  if (!workspaceId && !ingestEndpoint) return { kind: "none" };
@@ -1743,14 +1809,13 @@ async function resolveUserDestination(deps) {
1743
1809
  }
1744
1810
  };
1745
1811
  }
1746
- //#endregion
1747
- //#region src/hook/session-start.ts
1748
- const HOOK_USAGE = "Usage: harnessforce hook session-start\n";
1812
+ // src/hook/session-start.ts
1813
+ const HOOK_USAGE = "Usage: harnessforce hook session-start|post-tool-use|stop\n";
1749
1814
  const MAX_INPUT_BYTES = 1048576;
1750
1815
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set(["startup", "clear"]);
1751
1816
  const KEYCHAIN_READ_TIMEOUT_MS = 1e3;
1752
1817
  const REVOKED_MESSAGE = "Your ingest key has been revoked. Run `harnessforce init`";
1753
- function parseInput(raw) {
1818
+ function parseInput$1(raw) {
1754
1819
  let fields;
1755
1820
  try {
1756
1821
  fields = JSON.parse(raw);
@@ -1766,7 +1831,7 @@ function parseInput(raw) {
1766
1831
  source: typeof source === "string" ? source : void 0
1767
1832
  };
1768
1833
  }
1769
- const report$1 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1834
+ const report$2 = (deps, detail) => deps.stderr(`harnessforce: session registration ${detail}\n`);
1770
1835
  const SKIPPED_WARNINGS = {
1771
1836
  "not configured": notConfiguredWarning("codex"),
1772
1837
  "invalid endpoint": INVALID_ENDPOINT_WARNING,
@@ -1794,11 +1859,11 @@ async function resolveDestination(deps) {
1794
1859
  const resolved = await resolveWithinLimit(deps);
1795
1860
  if (resolved.kind === "ok") return { destination: resolved.destination };
1796
1861
  if (resolved.kind === "none") {
1797
- report$1(deps, "skipped (not configured)");
1862
+ report$2(deps, "skipped (not configured)");
1798
1863
  return { warning: notConfiguredWarning("codex") };
1799
1864
  }
1800
1865
  const detail = resolved.reason === "keychain unreadable" ? "no ingest key" : resolved.reason;
1801
- report$1(deps, `skipped (${detail})`);
1866
+ report$2(deps, `skipped (${detail})`);
1802
1867
  return { warning: SKIPPED_WARNINGS[resolved.reason] };
1803
1868
  }
1804
1869
  function registrationSource(env) {
@@ -1821,7 +1886,7 @@ async function register(input, deps) {
1821
1886
  started_at: deps.now().toISOString()
1822
1887
  };
1823
1888
  const outcome = await postItem(destination, "v1/sessions", registration, deps.fetch);
1824
- if (outcome.kind === "failed") report$1(deps, `failed (${outcome.reason})`);
1889
+ if (outcome.kind === "failed") report$2(deps, `failed (${outcome.reason})`);
1825
1890
  return { outcome };
1826
1891
  }
1827
1892
  async function selectWarning(registered, deps) {
@@ -1836,12 +1901,12 @@ async function selectWarning(registered, deps) {
1836
1901
  });
1837
1902
  }
1838
1903
  async function sessionStart(deps) {
1839
- const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1904
+ const input = parseInput$1(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
1840
1905
  if (!input) return 0;
1841
1906
  const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
1842
1907
  const blocked = registers && await isSendingBlocked(deps);
1843
1908
  const registered = registers && !blocked ? await register(input, deps).catch((error) => {
1844
- report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
1909
+ report$2(deps, `failed (${error instanceof Error ? error.name : "unknown"})`);
1845
1910
  return {};
1846
1911
  }) : {};
1847
1912
  const warning = registers ? await selectWarning(registered, deps) : void 0;
@@ -1852,8 +1917,185 @@ async function sessionStart(deps) {
1852
1917
  })}\n`);
1853
1918
  return 0;
1854
1919
  }
1855
- //#endregion
1856
- //#region src/shared/stop.ts
1920
+ // ../agent-core/src/work-location.ts
1921
+ const absolute = (value) => typeof value === "string" && isAbsolute(value) ? value : void 0;
1922
+ const toolInputOf = (fields) => isObject(fields.tool_input) ? fields.tool_input : {};
1923
+ const PATCH_FILE_LINE = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/;
1924
+ function patchedDirectories(command, cwd) {
1925
+ if (typeof command !== "string") return [];
1926
+ return command.split(/\r?\n/).flatMap((line) => {
1927
+ const path = PATCH_FILE_LINE.exec(line)?.[1]?.trim();
1928
+ return path ? [dirname(resolve(cwd, path))] : [];
1929
+ });
1930
+ }
1931
+ /**
1932
+ * eventの入力から、作業位置を求めるdirectoryを返す。読むのは`new_cwd`、`file_path`、patchのfileの行のpathだけで、編集の
1933
+ * 本文を読まない。
1934
+ */
1935
+ function observedDirectories(agent, event, fields, cwd) {
1936
+ if (event === "stop") return [cwd];
1937
+ if (agent === "claude_code" && event === "cwd-changed") {
1938
+ const newCwd = absolute(fields.new_cwd);
1939
+ return newCwd ? [newCwd] : [];
1940
+ }
1941
+ if (event !== "post-tool-use") return [];
1942
+ const input = toolInputOf(fields);
1943
+ if (agent === "codex") return fields.tool_name === "apply_patch" ? [...new Set(patchedDirectories(input.command, cwd))] : [];
1944
+ if (fields.tool_name !== "Write" && fields.tool_name !== "Edit") return [];
1945
+ const filePath = absolute(input.file_path);
1946
+ return filePath ? [dirname(filePath)] : [];
1947
+ }
1948
+ const isDirectory = (path) => stat(path).then((found) => found.isDirectory(), () => false);
1949
+ /** directoryのworking treeの、その時点のrepositoryとbranch。repositoryかbranchを求められなければundefined。 */
1950
+ async function observePair(directory, git) {
1951
+ if (!await isDirectory(directory)) return void 0;
1952
+ const vcs = await resolveVcs(directory, git);
1953
+ return vcs?.repository && vcs.branch ? {
1954
+ repository: vcs.repository,
1955
+ branch: vcs.branch
1956
+ } : void 0;
1957
+ }
1958
+ const emptyLocationState = () => ({
1959
+ pairs: [],
1960
+ sent: [],
1961
+ unauthorized: false
1962
+ });
1963
+ const pairKey = (pair) => JSON.stringify([pair.repository, pair.branch]);
1964
+ /** 組を加える。記録済みの組と、10件に達した後の組は加えない。 */
1965
+ function withObservedPair(state, pair) {
1966
+ if (state.pairs.some((saved) => pairKey(saved) === pairKey(pair)) || state.pairs.length >= MAX_WORK_LOCATIONS) return state;
1967
+ return {
1968
+ ...state,
1969
+ pairs: [...state.pairs, pair]
1970
+ };
1971
+ }
1972
+ /** 記録した組の集合が、最後に送った集合と異なる場合だけ送る組。 */
1973
+ function locationsToSend(state) {
1974
+ if (state.unauthorized || state.pairs.length === 0) return void 0;
1975
+ const sent = new Set(state.sent.map(pairKey));
1976
+ return sent.size === state.pairs.length && state.pairs.every((pair) => sent.has(pairKey(pair))) ? void 0 : state.pairs;
1977
+ }
1978
+ const isPair = (value) => isObject(value) && typeof value.repository === "string" && typeof value.branch === "string";
1979
+ async function readLocationState(path) {
1980
+ try {
1981
+ const saved = JSON.parse(await readFile(path, "utf8"));
1982
+ if (!isObject(saved) || !Array.isArray(saved.pairs) || !Array.isArray(saved.sent)) return emptyLocationState();
1983
+ return {
1984
+ pairs: saved.pairs.filter(isPair),
1985
+ sent: saved.sent.filter(isPair),
1986
+ unauthorized: saved.unauthorized === true
1987
+ };
1988
+ } catch {
1989
+ return emptyLocationState();
1990
+ }
1991
+ }
1992
+ async function writeLocationState(path, state) {
1993
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1994
+ await writeFile(temporary, JSON.stringify(state), { mode: 384 });
1995
+ await rename(temporary, path);
1996
+ }
1997
+ const FILE_SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
1998
+ const LOCATION_DIRECTORY = "harnessforce-locations";
1999
+ async function isPrivateDirectory(path) {
2000
+ const found = await lstat(path).catch(() => void 0);
2001
+ if (!found) return false;
2002
+ const uid = process.getuid?.();
2003
+ return found.isDirectory() && !found.isSymbolicLink() && (uid === void 0 || found.uid === uid) && (found.mode & 511) === 448;
2004
+ }
2005
+ /**
2006
+ * Codexの記録のfile。Codexのhookは`scratchpad_dir`を受け取らないため、OSの一時directoryの下に置く。macOSとLinuxでは
2007
+ * directoryの所有者とmodeを確かめ、満たさなければundefinedを返す(記録も送信もしない)。Windowsの`TEMP`は利用者ごとに分かれる。
2008
+ */
2009
+ async function codexLocationStatePath(temporaryDirectory, sessionId, platform = process.platform) {
2010
+ if (!FILE_SAFE_SESSION_ID.test(sessionId)) return void 0;
2011
+ const directory = join(temporaryDirectory, LOCATION_DIRECTORY);
2012
+ if (platform !== "win32") {
2013
+ await mkdir(directory, { mode: 448 }).catch(() => {});
2014
+ if (!await isPrivateDirectory(directory)) return void 0;
2015
+ } else await mkdir(directory, { recursive: true }).catch(() => {});
2016
+ return join(directory, `codex-${sessionId}.json`);
2017
+ }
2018
+ /** 観測したdirectoryの組を記録のfileへ加える。gitの外のdirectoryは記録しない。 */
2019
+ async function recordObservedPairs(statePath, directories, deps) {
2020
+ const pairs = await Promise.all(directories.map((directory) => observePair(directory, deps.git)));
2021
+ const before = await readLocationState(statePath);
2022
+ const after = pairs.reduce((state, pair) => pair ? withObservedPair(state, pair) : state, before);
2023
+ if (after !== before) await writeLocationState(statePath, after);
2024
+ return after;
2025
+ }
2026
+ /**
2027
+ * 記録した組の集合が最後に送った集合と異なれば`/v1/sessions/locations`へ送る。2xxなら送った集合を記録し、401なら以後送らない
2028
+ * ことを記録する。それ以外の失敗では記録を変えず、次の`Stop`が送り直しを兼ねる。
2029
+ */
2030
+ async function sendLocations(statePath, state, item, destination, fetchImpl) {
2031
+ const locations = locationsToSend(state);
2032
+ if (!locations) return { kind: "unchanged" };
2033
+ const outcome = await postItem(destination, "v1/sessions/locations", {
2034
+ agent: item.agent,
2035
+ session_id: item.sessionId,
2036
+ locations
2037
+ }, fetchImpl);
2038
+ if (outcome.kind === "accepted") await writeLocationState(statePath, {
2039
+ ...state,
2040
+ sent: locations
2041
+ });
2042
+ if (outcome.kind === "unauthorized") await writeLocationState(statePath, {
2043
+ ...state,
2044
+ unauthorized: true
2045
+ });
2046
+ return {
2047
+ kind: "sent",
2048
+ outcome
2049
+ };
2050
+ }
2051
+ // src/hook/work-location.ts
2052
+ function parseInput(raw) {
2053
+ let fields;
2054
+ try {
2055
+ fields = JSON.parse(raw);
2056
+ } catch {
2057
+ return;
2058
+ }
2059
+ if (!isObject(fields)) return void 0;
2060
+ const { session_id: sessionId, cwd } = fields;
2061
+ if (typeof sessionId !== "string" || !isToken(sessionId) || typeof cwd !== "string" || !isAbsolute(cwd)) return void 0;
2062
+ return {
2063
+ sessionId,
2064
+ cwd,
2065
+ fields
2066
+ };
2067
+ }
2068
+ const report$1 = (deps, detail) => deps.stderr(`harnessforce: session location ${detail}\n`);
2069
+ async function observe(event, input, deps) {
2070
+ const statePath = await codexLocationStatePath(deps.hookTmpDir, input.sessionId);
2071
+ if (!statePath) return;
2072
+ const state = await recordObservedPairs(statePath, observedDirectories("codex", event, input.fields, input.cwd), deps);
2073
+ if (event !== "stop") return;
2074
+ if (state.unauthorized) return;
2075
+ if (await isSendingBlocked(deps)) return;
2076
+ const { destination } = await resolveDestination({
2077
+ ...deps,
2078
+ stderr: () => {}
2079
+ });
2080
+ if (!destination) return;
2081
+ const sent = await sendLocations(statePath, state, {
2082
+ agent: "codex",
2083
+ sessionId: input.sessionId
2084
+ }, destination, deps.fetch);
2085
+ if (sent.kind !== "sent" || sent.outcome.kind === "accepted") return;
2086
+ report$1(deps, `failed (${sent.outcome.kind === "unauthorized" ? "unauthorized" : sent.outcome.reason})`);
2087
+ }
2088
+ /**
2089
+ * correlation.md「Codexのhook」「sessionの作業位置」。Codexの`PostToolUse`と`Stop`のhookが起動する。どの失敗でもsessionを
2090
+ * 止めず、常にexit 0で終える。`Stop`はstdoutにJSONを期待するため、空のobjectだけを書く。
2091
+ */
2092
+ async function workLocationHook(event, deps) {
2093
+ const input = parseInput(await deps.readStdin(MAX_INPUT_BYTES).then((buffer) => buffer.toString("utf8")).catch(() => ""));
2094
+ if (input) await observe(event, input, deps).catch((error) => report$1(deps, `failed (${error instanceof Error ? error.name : "unknown"})`));
2095
+ if (event === "stop") deps.stdout("{}\n");
2096
+ return 0;
2097
+ }
2098
+ // src/shared/stop.ts
1857
2099
  var CommandStop = class {
1858
2100
  text;
1859
2101
  constructor(text) {
@@ -1873,8 +2115,7 @@ async function runUntilStop(body, stderr) {
1873
2115
  return 1;
1874
2116
  }
1875
2117
  }
1876
- //#endregion
1877
- //#region src/credentials/api-token.ts
2118
+ // src/credentials/api-token.ts
1878
2119
  const NO_WHITESPACE$1 = /^\S+$/;
1879
2120
  const INSTANT$2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
1880
2121
  const isInstant = (value) => typeof value === "string" && INSTANT$2.test(value) && !Number.isNaN(Date.parse(value));
@@ -1903,8 +2144,7 @@ function parseStoredApiToken(secret, workspaceId) {
1903
2144
  return;
1904
2145
  }
1905
2146
  }
1906
- //#endregion
1907
- //#region src/credentials/access.ts
2147
+ // src/credentials/access.ts
1908
2148
  async function verifyCliAccess(deps, messages) {
1909
2149
  const { keychain } = deps;
1910
2150
  const stop = (message) => stopWith(messages[message]);
@@ -1928,8 +2168,7 @@ async function verifyCliAccess(deps, messages) {
1928
2168
  accessToken: apiToken.accessToken
1929
2169
  };
1930
2170
  }
1931
- //#endregion
1932
- //#region src/shared/http.ts
2171
+ // src/shared/http.ts
1933
2172
  const REQUEST_TIMEOUT_MS = 3e4;
1934
2173
  async function readJsonObjectBody(response) {
1935
2174
  try {
@@ -1939,8 +2178,7 @@ async function readJsonObjectBody(response) {
1939
2178
  return;
1940
2179
  }
1941
2180
  }
1942
- //#endregion
1943
- //#region src/shared/metadata.ts
2181
+ // src/shared/metadata.ts
1944
2182
  const WELL_KNOWN = "/.well-known/oauth-authorization-server";
1945
2183
  const withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
1946
2184
  async function fetchMetadata(base, fetchImpl) {
@@ -1975,8 +2213,7 @@ async function discoverTokenEndpoint(base, fetchImpl) {
1975
2213
  const endpoint = endpointOf(await fetchMetadata(base, fetchImpl), "token_endpoint");
1976
2214
  return endpoint?.origin === base.origin ? endpoint : void 0;
1977
2215
  }
1978
- //#endregion
1979
- //#region src/credentials/token-lock.ts
2216
+ // src/credentials/token-lock.ts
1980
2217
  const tokenLockPath = (homeDir) => join(homeDir, ".harnessforce", "token.lock");
1981
2218
  const LOCK_WAIT_MS$1 = 3e4;
1982
2219
  const STALE_LOCK_MS$1 = 6e4;
@@ -2006,8 +2243,7 @@ async function acquireTokenLock(path, deps) {
2006
2243
  await deps.sleep(RETRY_INTERVAL_MS$1);
2007
2244
  }
2008
2245
  }
2009
- //#endregion
2010
- //#region src/credentials/api-token-session.ts
2246
+ // src/credentials/api-token-session.ts
2011
2247
  const CLIENT_ID$1 = "harnessforce-cli";
2012
2248
  const LOCKED_KEYCHAIN_TIMEOUT_MS = 1e4;
2013
2249
  const isPast = (instant, now) => now >= Date.parse(instant);
@@ -2117,8 +2353,7 @@ function createApiTokenSession(deps) {
2117
2353
  authorizedFetch
2118
2354
  };
2119
2355
  }
2120
- //#endregion
2121
- //#region src/import/read-api.ts
2356
+ // src/import/read-api.ts
2122
2357
  const FAILED$1 = { kind: "failed" };
2123
2358
  async function getJson(url, apiToken, fetchImpl) {
2124
2359
  try {
@@ -2188,8 +2423,49 @@ async function fetchSessionImportDays(base, apiToken, fetchImpl) {
2188
2423
  value: days
2189
2424
  } : FAILED$1;
2190
2425
  }
2191
- //#endregion
2192
- //#region src/import/record-base.ts
2426
+ const ROLES = /* @__PURE__ */ new Set([
2427
+ "owner",
2428
+ "admin",
2429
+ "member",
2430
+ "viewer"
2431
+ ]);
2432
+ const STEP_STATUSES = /* @__PURE__ */ new Set([
2433
+ "done",
2434
+ "todo",
2435
+ "unavailable"
2436
+ ]);
2437
+ function parseSteps(value) {
2438
+ if (!Array.isArray(value)) return void 0;
2439
+ const steps = value.map((item) => {
2440
+ if (typeof item !== "object" || item === null) return void 0;
2441
+ const { step, status, url } = item;
2442
+ return Number.isInteger(step) && step >= 2 && step <= 5 && STEP_STATUSES.has(status) && typeof url === "string" ? {
2443
+ step,
2444
+ status,
2445
+ url
2446
+ } : void 0;
2447
+ });
2448
+ return steps.every((step) => step !== void 0) ? steps : void 0;
2449
+ }
2450
+ async function fetchOnboarding(base, apiToken, fetchImpl) {
2451
+ const outcome = await getJson(underBase(base, "api/v1/onboarding"), apiToken, fetchImpl);
2452
+ if (outcome.kind !== "ok") return outcome;
2453
+ const body = outcome.value;
2454
+ const steps = parseSteps(body.steps);
2455
+ const adminSteps = body.admin_steps === void 0 ? void 0 : parseSteps(body.admin_steps);
2456
+ if (typeof body.workspace_name !== "string" || !ROLES.has(body.role) || typeof body.has_seat !== "boolean" || !steps || body.admin_steps !== void 0 && !adminSteps) return FAILED$1;
2457
+ return {
2458
+ kind: "ok",
2459
+ value: {
2460
+ workspace_name: body.workspace_name,
2461
+ role: body.role,
2462
+ has_seat: body.has_seat,
2463
+ steps,
2464
+ ...adminSteps && { admin_steps: adminSteps }
2465
+ }
2466
+ };
2467
+ }
2468
+ // src/import/record-base.ts
2193
2469
  async function readSettingsEnv(path) {
2194
2470
  const read = await readUserSettings(path);
2195
2471
  const env = read.kind === "ok" ? read.settings.env : void 0;
@@ -2210,8 +2486,7 @@ async function recordBase(deps) {
2210
2486
  const { CLAUDE_CONFIG_DIR: configDir } = await readTrustedEnv(deps, ["CLAUDE_CONFIG_DIR"]);
2211
2487
  return configDir ?? join(deps.homeDir, ".claude");
2212
2488
  }
2213
- //#endregion
2214
- //#region src/import/state.ts
2489
+ // src/import/state.ts
2215
2490
  const STATE_VERSION = 1;
2216
2491
  const DIR_MODE$1 = 448;
2217
2492
  const FILE_MODE$3 = 384;
@@ -2268,8 +2543,7 @@ async function recordSentSessions(path, destination, sessionIds) {
2268
2543
  await rm(temporary, { force: true });
2269
2544
  }
2270
2545
  }
2271
- //#endregion
2272
- //#region src/import/send.ts
2546
+ // src/import/send.ts
2273
2547
  const BATCH_SIZE$1 = 100;
2274
2548
  const MAX_RESENDS = 3;
2275
2549
  const MAX_RETRY_AFTER_SECONDS = 60;
@@ -2486,8 +2760,7 @@ var SessionAccumulator = class {
2486
2760
  };
2487
2761
  }
2488
2762
  };
2489
- //#endregion
2490
- //#region src/import/rollout.ts
2763
+ // src/import/rollout.ts
2491
2764
  const CODEX_PARSER_VERSION = "1.0.0";
2492
2765
  const DETACHED$2 = "HEAD";
2493
2766
  const COMMIT_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
@@ -2624,8 +2897,7 @@ async function parseRollout(path) {
2624
2897
  }
2625
2898
  return accumulator.result(skippedLines);
2626
2899
  }
2627
- //#endregion
2628
- //#region src/import/transcript.ts
2900
+ // src/import/transcript.ts
2629
2901
  const PARSER_VERSION = "1.1.0";
2630
2902
  const isToken$1 = (value, maxLength = 256) => typeof value === "string" && value.length >= 1 && value.length <= maxLength && /^\S+$/.test(value);
2631
2903
  const MAX_TOOL_NAME = 128;
@@ -2902,8 +3174,7 @@ async function parseTranscriptUsage(path) {
2902
3174
  const { usage, isReadable } = await readTranscript(path, new Accumulator());
2903
3175
  return isReadable ? usage : void 0;
2904
3176
  }
2905
- //#endregion
2906
- //#region src/import/sessions.ts
3177
+ // src/import/sessions.ts
2907
3178
  const noneNotImported = () => ({
2908
3179
  outOfRange: 0,
2909
3180
  outsideGit: 0,
@@ -3064,10 +3335,9 @@ async function scanCodexSessions(options) {
3064
3335
  compressed
3065
3336
  };
3066
3337
  }
3067
- //#endregion
3068
- //#region src/import/command.ts
3338
+ // src/import/command.ts
3069
3339
  const DAY_MS$1 = 864e5;
3070
- const MESSAGES = {
3340
+ const MESSAGES$1 = {
3071
3341
  runInit: "Run `harnessforce init`",
3072
3342
  invalidUrl: "The connection URL is invalid",
3073
3343
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
@@ -3084,15 +3354,15 @@ const limitMessage = {
3084
3354
  monthly_event_limit: (count) => `Could not import ${count} session(s) because the monthly event limit was reached. Run \`harnessforce import\` again after the limit is lifted to resume importing`,
3085
3355
  workspace_read_only: (count) => `Could not import ${count} session(s) because the Workspace is view-only. Run \`harnessforce import\` again after view-only mode is lifted to resume importing`
3086
3356
  };
3087
- const ACCESS_MESSAGES$2 = {
3088
- keychainUnavailable: MESSAGES.keychainUnavailable,
3089
- initRequired: MESSAGES.runInit,
3090
- invalidUrl: MESSAGES.invalidUrl,
3091
- apiTokenMissing: MESSAGES.runInit
3357
+ const ACCESS_MESSAGES$3 = {
3358
+ keychainUnavailable: MESSAGES$1.keychainUnavailable,
3359
+ initRequired: MESSAGES$1.runInit,
3360
+ invalidUrl: MESSAGES$1.invalidUrl,
3361
+ apiTokenMissing: MESSAGES$1.runInit
3092
3362
  };
3093
- function unwrap(outcome) {
3363
+ function unwrap$1(outcome) {
3094
3364
  if (outcome.kind === "ok") return outcome.value;
3095
- return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.readFailed);
3365
+ return stopWith(outcome.kind === "unauthorized" ? MESSAGES$1.loginExpired : MESSAGES$1.readFailed);
3096
3366
  }
3097
3367
  const NOT_IMPORTED_LABELS = [
3098
3368
  ["alreadySent", "already sent"],
@@ -3108,8 +3378,8 @@ function reportNotImported(imported, notes, deps) {
3108
3378
  const line = breakdownLine(notes.breakdown);
3109
3379
  if (line) deps.stdout(`${line}\n`);
3110
3380
  if (imported > 0) return;
3111
- if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES.connectNone : MESSAGES.connectOther}\n`);
3112
- else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES.nothingFound}\n`);
3381
+ if (notes.breakdown.notConnected > 0) deps.stdout(`${notes.connectedCount === 0 ? MESSAGES$1.connectNone : MESSAGES$1.connectOther}\n`);
3382
+ else if (!line && !notes.otherNoticeShown) deps.stdout(`${MESSAGES$1.nothingFound}\n`);
3113
3383
  }
3114
3384
  function report(result, notes, deps) {
3115
3385
  switch (result.kind) {
@@ -3125,10 +3395,10 @@ function report(result, notes, deps) {
3125
3395
  deps.stderr(`${limitMessage[result.reason](result.notImported)}\n`);
3126
3396
  return 1;
3127
3397
  case "unauthorized":
3128
- deps.stderr(`${MESSAGES.revoked}\n`);
3398
+ deps.stderr(`${MESSAGES$1.revoked}\n`);
3129
3399
  return 1;
3130
3400
  case "failed":
3131
- deps.stderr(`${MESSAGES.sendFailed}\n`);
3401
+ deps.stderr(`${MESSAGES$1.sendFailed}\n`);
3132
3402
  return 1;
3133
3403
  }
3134
3404
  }
@@ -3138,7 +3408,7 @@ const sumNotImported = (a, b) => ({
3138
3408
  notConnected: a.notConnected + b.notConnected
3139
3409
  });
3140
3410
  async function runImport(deps) {
3141
- const access = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
3411
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES$3);
3142
3412
  const destination = {
3143
3413
  workspaceId: access.workspaceId,
3144
3414
  endpoint: withoutExtras(access.ingest)
@@ -3152,8 +3422,8 @@ async function runImport(deps) {
3152
3422
  sleep: deps.sleep,
3153
3423
  homeDir: deps.homeDir
3154
3424
  }).authorizedFetch(deps.fetch);
3155
- const connected = unwrap(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch));
3156
- const days = unwrap(await fetchSessionImportDays(access.readApiBase, access.accessToken, readFetch));
3425
+ const connected = unwrap$1(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch));
3426
+ const days = unwrap$1(await fetchSessionImportDays(access.readApiBase, access.accessToken, readFetch));
3157
3427
  const sinceMs = deps.now() - days * DAY_MS$1;
3158
3428
  const claude = await scanSessions({
3159
3429
  projectsDir: join(await recordBase(deps), "projects"),
@@ -3182,7 +3452,7 @@ async function runImport(deps) {
3182
3452
  sessions: unsent,
3183
3453
  fetch: deps.fetch,
3184
3454
  sleep: deps.sleep,
3185
- record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES.stateFailed))
3455
+ record: (ids) => recordSentSessions(statePath, destination, ids).catch(() => stopWith(MESSAGES$1.stateFailed))
3186
3456
  }), {
3187
3457
  breakdown: {
3188
3458
  alreadySent: scanned.length - unsent.length,
@@ -3195,9 +3465,8 @@ async function runImport(deps) {
3195
3465
  function importCommand(deps) {
3196
3466
  return runUntilStop(() => runImport(deps), deps.stderr);
3197
3467
  }
3198
- //#endregion
3199
- //#region ../../node_modules/.pnpm/@decimalturn+toml-patch@3.3.0/node_modules/@decimalturn/toml-patch/dist/toml-patch.js
3200
- //! @decimalturn/toml-patch v3.3.0 - https://github.com/DecimalTurn/toml-patch - @license: MIT
3468
+ // ../../node_modules/.pnpm/@decimalturn+toml-patch@3.3.1/node_modules/@decimalturn/toml-patch/dist/toml-patch.js
3469
+ //! @decimalturn/toml-patch v3.3.1 - https://github.com/DecimalTurn/toml-patch - @license: MIT
3201
3470
  function e(e) {
3202
3471
  return e.type === `Document`;
3203
3472
  }
@@ -4573,13 +4842,12 @@ function on(e) {
4573
4842
  if (e[t] !== `\\`) continue;
4574
4843
  let n = e[t + 1];
4575
4844
  if (!n) return null;
4576
- if (n === `\\`) {
4577
- t++;
4578
- continue;
4845
+ if (n === `\\`) t++;
4846
+ else {
4847
+ if (n === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(t + 2, t + 4))) return /[A-F]/.test(e.slice(t + 2, t + 4));
4848
+ if (n === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(t + 2, t + 6))) return /[A-F]/.test(e.slice(t + 2, t + 6));
4849
+ if (n === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(t + 2, t + 10))) return /[A-F]/.test(e.slice(t + 2, t + 10));
4579
4850
  }
4580
- if (n === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(t + 2, t + 4))) return /[A-F]/.test(e.slice(t + 2, t + 4));
4581
- if (n === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(t + 2, t + 6))) return /[A-F]/.test(e.slice(t + 2, t + 6));
4582
- if (n === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(t + 2, t + 10))) return /[A-F]/.test(e.slice(t + 2, t + 10));
4583
4851
  }
4584
4852
  return null;
4585
4853
  }
@@ -4891,14 +5159,11 @@ function qn(e, t) {
4891
5159
  if (e === t) return r;
4892
5160
  if (a(e)) return n(e.value, r);
4893
5161
  if (d(e)) return n(e.item, r);
4894
- if (u(e) || f(e)) {
4895
- for (let t of e.items) {
4896
- let e = n(t, r + 1);
4897
- if (e !== void 0) return e;
4898
- }
4899
- return;
5162
+ if (u(e) || f(e)) for (let t of e.items) {
5163
+ let e = n(t, r + 1);
5164
+ if (e !== void 0) return e;
4900
5165
  }
4901
- if (m(e)) for (let t of e.items) {
5166
+ else if (m(e)) for (let t of e.items) {
4902
5167
  let e = n(t, r);
4903
5168
  if (e !== void 0) return e;
4904
5169
  }
@@ -4910,14 +5175,11 @@ function Jn(e, t) {
4910
5175
  if (e === t) return r;
4911
5176
  if (a(e)) return n(e.value, r);
4912
5177
  if (d(e)) return n(e.item, r);
4913
- if (u(e) || f(e)) {
4914
- for (let t of e.items) {
4915
- let r = n(t, e);
4916
- if (r) return r;
4917
- }
4918
- return;
5178
+ if (u(e) || f(e)) for (let t of e.items) {
5179
+ let r = n(t, e);
5180
+ if (r) return r;
4919
5181
  }
4920
- if (m(e)) for (let t of e.items) {
5182
+ else if (m(e)) for (let t of e.items) {
4921
5183
  let e = n(t, r);
4922
5184
  if (e) return e;
4923
5185
  }
@@ -5617,25 +5879,17 @@ function Nr(e) {
5617
5879
  let r = e[n + 1];
5618
5880
  if (!r) continue;
5619
5881
  let i = kr(r);
5620
- if (i !== null) {
5621
- i !== `
5882
+ if (i !== null) i !== `
5622
5883
  ` && i !== `\r` && !t.has(i) && t.set(i, `\\` + r), n += 1;
5623
- continue;
5624
- }
5625
- if (r === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(n + 2, n + 4))) {
5884
+ else if (r === `x` && /^[0-9A-Fa-f]{2}$/.test(e.slice(n + 2, n + 4))) {
5626
5885
  let r = e.slice(n, n + 4), i = Number.parseInt(e.slice(n + 2, n + 4), 16), a = String.fromCharCode(i);
5627
5886
  t.has(a) || t.set(a, r), n += 3;
5628
- continue;
5629
- }
5630
- if (r === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(n + 2, n + 6))) {
5887
+ } else if (r === `u` && /^[0-9A-Fa-f]{4}$/.test(e.slice(n + 2, n + 6))) {
5631
5888
  let r = e.slice(n, n + 6), i = Number.parseInt(e.slice(n + 2, n + 6), 16), a = String.fromCodePoint(i);
5632
5889
  t.has(a) || t.set(a, r), n += 5;
5633
- continue;
5634
- }
5635
- if (r === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(n + 2, n + 10))) {
5890
+ } else if (r === `U` && /^[0-9A-Fa-f]{8}$/.test(e.slice(n + 2, n + 10))) {
5636
5891
  let r = e.slice(n, n + 10), i = Number.parseInt(e.slice(n + 2, n + 10), 16), a = String.fromCodePoint(i);
5637
5892
  t.has(a) || t.set(a, r), n += 9;
5638
- continue;
5639
5893
  }
5640
5894
  }
5641
5895
  return t;
@@ -5644,11 +5898,7 @@ function Pr(e, t, n, r) {
5644
5898
  let i = ``;
5645
5899
  for (let a of e) {
5646
5900
  let e = t.get(a);
5647
- if (e) {
5648
- i += e;
5649
- continue;
5650
- }
5651
- i += jr(a, n, r);
5901
+ i += e || jr(a, n, r);
5652
5902
  }
5653
5903
  return i;
5654
5904
  }
@@ -6052,7 +6302,7 @@ function oi(e) {
6052
6302
  return !f(e) || e.items.length === 0 ? !1 : e.items[e.items.length - 1].comma === !0;
6053
6303
  }
6054
6304
  function si(e, t) {
6055
- return t.inlineTableStart === 0 ? e : (e.items.filter((e) => {
6305
+ return t.inlineTableStart === 0 || (e.items.filter((e) => {
6056
6306
  if (!a(e)) return !1;
6057
6307
  let n = f(e.value), r = u(e.value) && e.value.items.length > 0 && e.value.items.every((e) => f(e.item));
6058
6308
  if (n || r) {
@@ -6064,7 +6314,7 @@ function si(e, t) {
6064
6314
  V(e, e, n), f(n.value) ? n.value.items.length === 0 ? B(e, e, W(n.key.value)) : B(e, e, ci(n, t.bracketSpacing, t.escapeSequenceUpperCase)) : li(n, t.bracketSpacing, t.escapeSequenceUpperCase).forEach((t) => {
6065
6315
  B(e, e, t);
6066
6316
  });
6067
- }), H(e), e);
6317
+ }), H(e)), e;
6068
6318
  }
6069
6319
  function ci(e, t, n) {
6070
6320
  let r = W(e.key.value, n);
@@ -7101,11 +7351,7 @@ function ia(e, t, n = [], r = {}) {
7101
7351
  let h = 0;
7102
7352
  for (let t = e; t < o.length; t++) o[t] === r && h++;
7103
7353
  let g = p > h;
7104
- if (s && (o.indexOf(a[e]) === -1 || g)) {
7105
- i.push(d(e + l)), a.splice(e, 1), c.splice(e, 1), l++, e--;
7106
- continue;
7107
- }
7108
- if (l > 0 && g) {
7354
+ if (s && (o.indexOf(a[e]) === -1 || g) || l > 0 && g) {
7109
7355
  i.push(d(e + l)), a.splice(e, 1), c.splice(e, 1), l++, e--;
7110
7356
  continue;
7111
7357
  }
@@ -7136,14 +7382,10 @@ function ia(e, t, n = [], r = {}) {
7136
7382
  for (let n = e; n < o.length; n++) o[n] === t && r++;
7137
7383
  h = n > r;
7138
7384
  }
7139
- if (!p && h) {
7140
- Vt(i, $i(c[e], t[e], n.concat(e), r)), a[e] = f, c[e] = t[e];
7141
- continue;
7142
- }
7143
- i.push({
7385
+ !p && h ? (Vt(i, $i(c[e], t[e], n.concat(e), r)), a[e] = f, c[e] = t[e]) : (i.push({
7144
7386
  type: `Add`,
7145
7387
  path: n.concat(e)
7146
- }), u = !0, a.splice(e, 0, f), c.splice(e, 0, t[e]), l > 0 && l--;
7388
+ }), u = !0, a.splice(e, 0, f), c.splice(e, 0, t[e]), l > 0 && l--);
7147
7389
  }
7148
7390
  for (let e = o.length; e < a.length; e++) i.push(d(e + l));
7149
7391
  return i;
@@ -7229,19 +7471,10 @@ function pa(e) {
7229
7471
  let n = !1, r = !1;
7230
7472
  for (let i = t + 1; i < e.raw.length; i++) {
7231
7473
  let t = e.raw[i];
7232
- if (t === `\\`) {
7233
- i++;
7234
- continue;
7235
- }
7236
- if (!r && t === `"`) {
7237
- n = !n;
7238
- continue;
7239
- }
7240
- if (!n && t === `'`) {
7241
- r = !r;
7242
- continue;
7243
- }
7244
- if (!n && !r && t === `#`) return !0;
7474
+ if (t === `\\`) i++;
7475
+ else if (!r && t === `"`) n = !n;
7476
+ else if (!n && t === `'`) r = !r;
7477
+ else if (!n && !r && t === `#`) return !0;
7245
7478
  }
7246
7479
  return !1;
7247
7480
  }
@@ -7509,13 +7742,7 @@ function Ta(t, n, r, i) {
7509
7742
  e && (e.lines = 0, e.columns = 0);
7510
7743
  }
7511
7744
  H(t), p && h && r.loc.end.line > r.loc.start.line && (r.loc.end.column = h.column, d(r) && g && r.item.loc.end.line > r.item.loc.start.line && (r.item.loc.end.column = g.column));
7512
- for (let { node: e, start: t, end: n, endOnly: r, equals: i } of _) {
7513
- if (r) {
7514
- e.loc.end.column = n.column;
7515
- continue;
7516
- }
7517
- e.loc.start.column = t.column, e.loc.end.line === e.loc.start.line && (e.loc.end.column = n.column), i !== void 0 && a(e) && (e.equals = i);
7518
- }
7745
+ for (let { node: e, start: t, end: n, endOnly: r, equals: i } of _) r ? e.loc.end.column = n.column : (e.loc.start.column = t.column, e.loc.end.line === e.loc.start.line && (e.loc.end.column = n.column), i !== void 0 && a(e) && (e.equals = i));
7519
7746
  for (let { owner: t, ownerOriginalStart: n, comments: i } of l) {
7520
7747
  let a;
7521
7748
  t === r ? (a = {
@@ -7640,11 +7867,7 @@ function Ea(e) {
7640
7867
  continue;
7641
7868
  }
7642
7869
  let e = t(i.member) || r(i.member), a = n[n.length - 1];
7643
- if (a && a.kind === `member` && i.key !== void 0 && a.key === i.key) {
7644
- a.groups.push(i), a.endLine = i.endLine, a.items = a.items.concat(i.items);
7645
- continue;
7646
- }
7647
- n.push({
7870
+ a && a.kind === `member` && i.key !== void 0 && a.key === i.key ? (a.groups.push(i), a.endLine = i.endLine, a.items = a.items.concat(i.items)) : n.push({
7648
7871
  kind: `member`,
7649
7872
  key: i.key,
7650
7873
  isSection: e,
@@ -7869,23 +8092,11 @@ function Ua(e, t, n) {
7869
8092
  return r.columns + (i || a ? 2 : 0) + (s ? 1 + (c ? -1 : 0) : 0);
7870
8093
  }
7871
8094
  function Wa(e, n = []) {
7872
- for (let i of e.items) {
7873
- if (p(i)) {
7874
- n.push(i);
7875
- continue;
7876
- }
7877
- (t(i) || r(i)) && Wa(i, n);
7878
- }
8095
+ for (let i of e.items) p(i) ? n.push(i) : (t(i) || r(i)) && Wa(i, n);
7879
8096
  return n;
7880
8097
  }
7881
8098
  function Ga(e, n = /* @__PURE__ */ new Set()) {
7882
- for (let i of e.items) {
7883
- if (a(i) && f(i.value) && i.loc.start.line === i.loc.end.line) {
7884
- n.add(i.loc.end.line);
7885
- continue;
7886
- }
7887
- (t(i) || r(i)) && Ga(i, n);
7888
- }
8099
+ for (let i of e.items) a(i) && f(i.value) && i.loc.start.line === i.loc.end.line ? n.add(i.loc.end.line) : (t(i) || r(i)) && Ga(i, n);
7889
8100
  return n;
7890
8101
  }
7891
8102
  function Ka(e) {
@@ -8257,11 +8468,7 @@ function so(e) {
8257
8468
  let t = [], n = 0, r, i = !1;
8258
8469
  for (let a = 0; a < e.length; a++) {
8259
8470
  let o = e[a];
8260
- if (r) {
8261
- r === `"` && i ? i = !1 : r === `"` && o === `\\` ? i = !0 : o === r && (r = void 0);
8262
- continue;
8263
- }
8264
- o === `"` || o === `'` ? r = o : o === `.` && (t.push(e.slice(n, a)), n = a + 1);
8471
+ r ? r === `"` && i ? i = !1 : r === `"` && o === `\\` ? i = !0 : o === r && (r = void 0) : o === `"` || o === `'` ? r = o : o === `.` && (t.push(e.slice(n, a)), n = a + 1);
8265
8472
  }
8266
8473
  return t.push(e.slice(n)), t;
8267
8474
  }
@@ -9013,11 +9220,20 @@ function yo(n, i, s, c, l = !1, g = /* @__PURE__ */ new WeakSet(), _ = void 0, v
9013
9220
  for (let e of s) Q(n, l, e);
9014
9221
  }
9015
9222
  if ((t(o) || r(o)) && m(l)) {
9016
- let e = o.key.item.value, i = l.items, s = [];
9017
- for (let n of i) {
9018
- if (n === o) continue;
9019
- let i = a(n) ? n.key.value : t(n) || r(n) ? n.key.item.value : void 0;
9020
- i && i.length > e.length && F(i.slice(0, e.length), e) && s.push(n);
9223
+ let e = o.key.item.value, i = l.items, s = [], c = r(o), u = i.indexOf(o), d = i.length;
9224
+ if (c) for (let t = u + 1; t < i.length; t++) {
9225
+ let n = i[t];
9226
+ if (!r(n)) continue;
9227
+ let a = n.key.item.value;
9228
+ if (a.length <= e.length && F(a, e.slice(0, a.length))) {
9229
+ d = t;
9230
+ break;
9231
+ }
9232
+ }
9233
+ for (let [n, l] of i.entries()) {
9234
+ if (l === o || c && (n < u || n >= d)) continue;
9235
+ let i = a(l) ? l.key.value : t(l) || r(l) ? l.key.item.value : void 0;
9236
+ i && i.length > e.length && F(i.slice(0, e.length), e) && s.push(l);
9021
9237
  }
9022
9238
  for (let e of s) Q(n, l, e);
9023
9239
  }
@@ -9403,23 +9619,18 @@ function To(e, n, i, o, s, c, l) {
9403
9619
  let n = s.items.find((e) => a(e) && F(e.key.value, t.key.value));
9404
9620
  n ? pr(e, s, n, t) : B(e, s, t, void 0);
9405
9621
  };
9406
- for (let e of m) {
9407
- if (a(e)) {
9408
- h(e);
9409
- continue;
9410
- }
9411
- if (t(e)) {
9412
- let t = e.key.item.value;
9413
- for (let n of e.items) {
9414
- if (!a(n)) continue;
9415
- let e = n.key.raw, r = t.concat(n.key.value);
9416
- n.key.value = r, n.key.raw = Ur(r, o.escapeSequenceUpperCase).raw;
9417
- let i = n.key.raw.length - e.length;
9418
- n.key.loc.end.column = n.key.loc.start.column + n.key.raw.length, n.equals += i, U(n.value, {
9419
- lines: 0,
9420
- columns: i
9421
- }, { first_line_only: !0 }), n.loc.end.line === n.loc.start.line && (n.loc.end.column += i), h(n);
9422
- }
9622
+ for (let e of m) if (a(e)) h(e);
9623
+ else if (t(e)) {
9624
+ let t = e.key.item.value;
9625
+ for (let n of e.items) {
9626
+ if (!a(n)) continue;
9627
+ let e = n.key.raw, r = t.concat(n.key.value);
9628
+ n.key.value = r, n.key.raw = Ur(r, o.escapeSequenceUpperCase).raw;
9629
+ let i = n.key.raw.length - e.length;
9630
+ n.key.loc.end.column = n.key.loc.start.column + n.key.raw.length, n.equals += i, U(n.value, {
9631
+ lines: 0,
9632
+ columns: i
9633
+ }, { first_line_only: !0 }), n.loc.end.line === n.loc.start.line && (n.loc.end.column += i), h(n);
9423
9634
  }
9424
9635
  }
9425
9636
  return;
@@ -9438,51 +9649,51 @@ function To(e, n, i, o, s, c, l) {
9438
9649
  if (o) {
9439
9650
  for (let t of i.items) B(e, o, t, void 0);
9440
9651
  v ||= o;
9441
- continue;
9442
- }
9443
- if (a(i) && f(i.value)) {
9444
- let o = i.key.value, s = e.items.find((e) => t(e) && F(e.key.item.value, o));
9445
- if (s) {
9446
- for (let t of i.value.items) B(e, s, d(t) ? t.item : t, void 0);
9447
- v ||= s;
9448
- continue;
9449
- }
9450
- if ($(e, o).filter((e) => a(e) || t(e) || r(e)).length > 0) {
9451
- for (let t of i.value.items) {
9452
- let r = d(t) ? t.item : t;
9453
- if (a(r)) {
9454
- let e = r.key.raw, t = o.concat(r.key.value);
9455
- r.key.value = t, r.key.raw = t.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9456
- let n = r.key.raw.length - e.length;
9457
- r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += n, U(r.value, {
9458
- lines: 0,
9459
- columns: n
9460
- }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += n);
9652
+ } else {
9653
+ if (a(i) && f(i.value)) {
9654
+ let o = i.key.value, s = e.items.find((e) => t(e) && F(e.key.item.value, o));
9655
+ if (s) {
9656
+ for (let t of i.value.items) B(e, s, d(t) ? t.item : t, void 0);
9657
+ v ||= s;
9658
+ continue;
9659
+ }
9660
+ if ($(e, o).filter((e) => a(e) || t(e) || r(e)).length > 0) {
9661
+ for (let t of i.value.items) {
9662
+ let r = d(t) ? t.item : t;
9663
+ if (a(r)) {
9664
+ let e = r.key.raw, t = o.concat(r.key.value);
9665
+ r.key.value = t, r.key.raw = t.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9666
+ let n = r.key.raw.length - e.length;
9667
+ r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += n, U(r.value, {
9668
+ lines: 0,
9669
+ columns: n
9670
+ }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += n);
9671
+ }
9672
+ B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9461
9673
  }
9462
- B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9674
+ continue;
9463
9675
  }
9464
- continue;
9465
9676
  }
9466
- }
9467
- if (t(i)) {
9468
- let t = i.key.item.value;
9469
- if ($(e, t).filter((e) => a(e)).length > 0) {
9470
- for (let r of i.items) {
9471
- if (a(r)) {
9472
- let e = r.key.raw, n = t.concat(r.key.value);
9473
- r.key.value = n, r.key.raw = n.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9474
- let i = r.key.raw.length - e.length;
9475
- r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += i, U(r.value, {
9476
- lines: 0,
9477
- columns: i
9478
- }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += i);
9677
+ if (t(i)) {
9678
+ let t = i.key.item.value;
9679
+ if ($(e, t).filter((e) => a(e)).length > 0) {
9680
+ for (let r of i.items) {
9681
+ if (a(r)) {
9682
+ let e = r.key.raw, n = t.concat(r.key.value);
9683
+ r.key.value = n, r.key.raw = n.map((e) => A.test(e) ? e : JSON.stringify(e).replace(/\x7f/g, `\\u007f`)).join(`.`);
9684
+ let i = r.key.raw.length - e.length;
9685
+ r.key.loc.end.column = r.key.loc.start.column + r.key.raw.length, r.equals += i, U(r.value, {
9686
+ lines: 0,
9687
+ columns: i
9688
+ }, { first_line_only: !0 }), r.loc.end.line === r.loc.start.line && (r.loc.end.column += i);
9689
+ }
9690
+ B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9479
9691
  }
9480
- B(e, e, r, n === 0 ? _ : void 0), v ||= r;
9692
+ continue;
9481
9693
  }
9482
- continue;
9483
9694
  }
9695
+ n > 0 && v && t(v) && a(i) ? B(e, v, i, void 0) : B(e, e, i, n === 0 ? _ : void 0), v ||= i;
9484
9696
  }
9485
- n > 0 && v && t(v) && a(i) ? B(e, v, i, void 0) : B(e, e, i, n === 0 ? _ : void 0), v ||= i;
9486
9697
  }
9487
9698
  let y = v;
9488
9699
  t(y) && l.add(y), c.add(y);
@@ -9665,11 +9876,26 @@ function Ko(e, t) {
9665
9876
  temporal: (t == null ? void 0 : t.temporal) ?? !1
9666
9877
  });
9667
9878
  }
9668
- //#endregion
9669
- //#region src/shared/codex-config.ts
9670
- const HOOK_COMMAND = "harnessforce hook session-start";
9671
- const HOOK_COMMAND_WINDOWS = "harnessforce.cmd hook session-start";
9879
+ // src/shared/codex-config.ts
9672
9880
  const HOOK_TIMEOUT_SECONDS = 10;
9881
+ const HOOKS = [
9882
+ {
9883
+ event: "SessionStart",
9884
+ subcommand: "session-start"
9885
+ },
9886
+ {
9887
+ event: "PostToolUse",
9888
+ subcommand: "post-tool-use",
9889
+ matcher: "^apply_patch$",
9890
+ isAsync: true
9891
+ },
9892
+ {
9893
+ event: "Stop",
9894
+ subcommand: "stop",
9895
+ isAsync: true
9896
+ }
9897
+ ];
9898
+ const hookCommand = (spec) => `harnessforce hook ${spec.subcommand}`;
9673
9899
  const FILE_MODE$2 = 384;
9674
9900
  function codexConfigPath(env, homeDir) {
9675
9901
  return join(absoluteEnv(env.CODEX_HOME) ?? join(homeDir, ".codex"), "config.toml");
@@ -9705,11 +9931,14 @@ function withHarnessforce(config, input) {
9705
9931
  headers: { Authorization: `Bearer ${input.ingestKey}` },
9706
9932
  protocol: "binary"
9707
9933
  } };
9708
- if (input.logUserPrompt) otel.log_user_prompt = true;
9934
+ otel.log_user_prompt = input.logUserPrompt;
9935
+ otel.log_agent_responses = input.logAgentResponses;
9709
9936
  const hooks = tableAt(config, "hooks");
9710
- const groups = hooks.SessionStart ?? [];
9711
- if (!Array.isArray(groups)) throw new Error("SessionStart");
9712
- hooks.SessionStart = withHandler(groups);
9937
+ for (const spec of HOOKS) {
9938
+ const groups = hooks[spec.event] ?? [];
9939
+ if (!Array.isArray(groups)) throw new Error(spec.event);
9940
+ hooks[spec.event] = withHandler(groups, spec);
9941
+ }
9713
9942
  const servers = tableAt(config, "mcp_servers");
9714
9943
  const existing = servers.harnessforce;
9715
9944
  if (existing !== void 0 && !isObject(existing)) throw new Error("server");
@@ -9725,16 +9954,21 @@ function tableAt(parent, key) {
9725
9954
  parent[key] = value;
9726
9955
  return value;
9727
9956
  }
9728
- const isOurs = (handler) => isObject(handler) && handler.command === HOOK_COMMAND;
9729
- const hasOurs = (group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOurs);
9730
- function withHandler(groups) {
9957
+ function withHandler(groups, spec) {
9958
+ const command = hookCommand(spec);
9959
+ const isOurs = (entry) => isObject(entry) && entry.command === command;
9960
+ const hasOurs = (group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOurs);
9731
9961
  const handler = {
9732
9962
  type: "command",
9733
- command: HOOK_COMMAND,
9734
- commandWindows: HOOK_COMMAND_WINDOWS,
9735
- timeout: HOOK_TIMEOUT_SECONDS
9736
- };
9737
- if (!groups.some(hasOurs)) return [...groups, { hooks: [handler] }];
9963
+ command,
9964
+ commandWindows: `harnessforce.cmd hook ${spec.subcommand}`,
9965
+ timeout: HOOK_TIMEOUT_SECONDS,
9966
+ ...spec.isAsync ? { async: true } : {}
9967
+ };
9968
+ if (!groups.some(hasOurs)) return [...groups, {
9969
+ ...spec.matcher === void 0 ? {} : { matcher: spec.matcher },
9970
+ hooks: [handler]
9971
+ }];
9738
9972
  return groups.map((group) => hasOurs(group) ? {
9739
9973
  ...group,
9740
9974
  hooks: group.hooks.map((entry) => isOurs(entry) ? handler : entry)
@@ -9754,8 +9988,29 @@ async function writeCodexConfig(path, text) {
9754
9988
  await rm(temporary, { force: true });
9755
9989
  }
9756
9990
  }
9757
- //#endregion
9758
- //#region src/init/credentials-api.ts
9991
+ // src/init/credentials-api.ts
9992
+ const VISIBILITIES = /* @__PURE__ */ new Set([
9993
+ "all_members",
9994
+ "owners_admins_and_sender",
9995
+ "sender_only"
9996
+ ]);
9997
+ const RETENTION_DAYS = /* @__PURE__ */ new Set([
9998
+ 7,
9999
+ 14,
10000
+ 30
10001
+ ]);
10002
+ function parseContent(value) {
10003
+ if (!isObject(value)) return void 0;
10004
+ const { prompts, model_and_tools: modelAndTools, visibility } = value;
10005
+ const retentionDays = value.retention_days;
10006
+ if (typeof prompts !== "boolean" || typeof modelAndTools !== "boolean" || typeof visibility !== "string" || !VISIBILITIES.has(visibility) || typeof retentionDays !== "number" || !RETENTION_DAYS.has(retentionDays)) return void 0;
10007
+ return {
10008
+ isPromptsStored: prompts,
10009
+ isModelAndToolsStored: modelAndTools,
10010
+ visibility,
10011
+ retentionDays
10012
+ };
10013
+ }
9759
10014
  const WORKSPACE_ID = /^[A-Za-z0-9-]{1,128}$/;
9760
10015
  const NO_WHITESPACE = /^\S+$/;
9761
10016
  const LIMIT_ROLES = /* @__PURE__ */ new Set([
@@ -9764,15 +10019,16 @@ const LIMIT_ROLES = /* @__PURE__ */ new Set([
9764
10019
  "member"
9765
10020
  ]);
9766
10021
  function parseIssued(body) {
9767
- const { workspace_id: workspaceId, ingest_key: ingestKey, ingest_endpoint: ingestEndpoint, content_opt_in: contentOptIn } = body;
9768
- if (typeof workspaceId !== "string" || typeof ingestKey !== "string" || typeof ingestEndpoint !== "string" || typeof contentOptIn !== "boolean" || !WORKSPACE_ID.test(workspaceId)) return void 0;
10022
+ const { workspace_id: workspaceId, ingest_key: ingestKey, ingest_endpoint: ingestEndpoint, content } = body;
10023
+ if (typeof workspaceId !== "string" || typeof ingestKey !== "string" || typeof ingestEndpoint !== "string" || !WORKSPACE_ID.test(workspaceId)) return void 0;
9769
10024
  const apiToken = parseTokenPair(body, workspaceId);
9770
- return ingestKey.startsWith(`hf_ik_${workspaceId}_`) && NO_WHITESPACE.test(ingestKey) && apiToken !== void 0 && parseAllowedUrl(ingestEndpoint) !== void 0 && apiToken ? {
10025
+ const parsedContent = parseContent(content);
10026
+ return ingestKey.startsWith(`hf_ik_${workspaceId}_`) && NO_WHITESPACE.test(ingestKey) && apiToken !== void 0 && parsedContent !== void 0 && parseAllowedUrl(ingestEndpoint) !== void 0 && apiToken && parsedContent ? {
9771
10027
  workspaceId,
9772
10028
  ingestKey,
9773
10029
  apiToken,
9774
10030
  ingestEndpoint,
9775
- contentOptIn
10031
+ content: parsedContent
9776
10032
  } : void 0;
9777
10033
  }
9778
10034
  function classify(status, body) {
@@ -9812,8 +10068,7 @@ async function requestCredentials(base, request, fetchImpl) {
9812
10068
  return { kind: "unexpected" };
9813
10069
  }
9814
10070
  }
9815
- //#endregion
9816
- //#region src/init/pkce.ts
10071
+ // src/init/pkce.ts
9817
10072
  const RANDOM_BYTES = 32;
9818
10073
  const codeChallenge = (codeVerifier) => createHash("sha256").update(codeVerifier).digest("base64url");
9819
10074
  function createLoginSecrets() {
@@ -9824,8 +10079,7 @@ function createLoginSecrets() {
9824
10079
  state: randomBytes(RANDOM_BYTES).toString("base64url")
9825
10080
  };
9826
10081
  }
9827
- //#endregion
9828
- //#region src/init/init.ts
10082
+ // src/init/init.ts
9829
10083
  const CLIENT_ID = "harnessforce-cli";
9830
10084
  const DEFAULT_PORT = 8080;
9831
10085
  const MAX_REVOKE_HASHES = 100;
@@ -9856,28 +10110,43 @@ async function runInit(args, deps) {
9856
10110
  if (settings.kind === "invalid" || !hasWritableMarketplaces(settings.settings)) stop$1("settingsUnreadable");
9857
10111
  const authorizationEndpoint = await discoverAuthorizationEndpoint(base, deps.fetch) ?? stop$1("network");
9858
10112
  const secrets = createLoginSecrets();
9859
- const { code, redirectUri } = await logIn(authorizationEndpoint, secrets, port, deps);
9860
- const issued = issuedOrStop(await requestCredentials(base, {
9861
- code,
9862
- code_verifier: secrets.codeVerifier,
9863
- client_id: CLIENT_ID,
9864
- redirect_uri: redirectUri,
9865
- revoke_key_hashes: revoke.keyHashes,
9866
- revoke_api_token_hashes: revoke.apiTokenHashes
9867
- }, deps.fetch));
9868
- const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
9869
- if (isStop(error)) throw error;
9870
- return stop$1("saveFailed");
9871
- });
9872
- const codexPath = codexConfigPath(deps.env, deps.homeDir);
9873
- await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
9874
- if (args.sendContent) deps.stdout(`${issued.contentOptIn ? INIT_MESSAGES.contentEnabled : INIT_MESSAGES.contentNotOptedIn}\n`);
9875
- return {
9876
- codexPath,
9877
- connection: recordedUrl,
9878
- isKernelKeyring: store.isKernelKeyring,
9879
- isPluginAutoUpdateOn
9880
- };
10113
+ const { code, redirectUri, loopback } = await logIn(authorizationEndpoint, secrets, port, new URL(recordedUrl).origin, deps);
10114
+ let completion = "failed";
10115
+ try {
10116
+ const outcome = await requestCredentials(base, {
10117
+ code,
10118
+ code_verifier: secrets.codeVerifier,
10119
+ client_id: CLIENT_ID,
10120
+ redirect_uri: redirectUri,
10121
+ revoke_key_hashes: revoke.keyHashes,
10122
+ revoke_api_token_hashes: revoke.apiTokenHashes
10123
+ }, deps.fetch);
10124
+ if (outcome.kind === "viewer") completion = "viewer";
10125
+ const issued = issuedOrStop(outcome);
10126
+ const isPluginAutoUpdateOn = await save(issued, recordedUrl, settingsPath, args, store).catch((error) => {
10127
+ if (isStop(error)) throw error;
10128
+ return stop$1("saveFailed");
10129
+ });
10130
+ const codexPath = codexConfigPath(deps.env, deps.homeDir);
10131
+ await saveCodexConfig(issued, recordedUrl, codexPath, args.sendContent).catch(() => stop$1("codexSaveFailed"));
10132
+ completion = "connected";
10133
+ loopback.finish({
10134
+ status: completion,
10135
+ workspaceId: issued.workspaceId
10136
+ });
10137
+ return finishInit(issued, args, deps, {
10138
+ codexPath,
10139
+ connection: recordedUrl,
10140
+ isKernelKeyring: store.isKernelKeyring,
10141
+ isPluginAutoUpdateOn
10142
+ });
10143
+ } finally {
10144
+ loopback.finish({ status: completion });
10145
+ }
10146
+ }
10147
+ function finishInit(issued, args, deps, result) {
10148
+ deps.stdout(`${contentNotice(issued.content, args.sendContent)}\n`);
10149
+ return result;
9881
10150
  }
9882
10151
  async function openKeychain(deps, previousWorkspaceId) {
9883
10152
  return await deps.keychain.openForInit({
@@ -9899,11 +10168,12 @@ function revokeHashes({ items }) {
9899
10168
  apiTokenHashes
9900
10169
  };
9901
10170
  }
9902
- async function logIn(authorizationEndpoint, secrets, port, deps) {
10171
+ async function logIn(authorizationEndpoint, secrets, port, webOrigin, deps) {
9903
10172
  const loopback = await deps.startLoopback({
9904
10173
  state: secrets.state,
9905
10174
  timeoutMs: deps.callbackTimeoutMs,
9906
- port
10175
+ port,
10176
+ webOrigin
9907
10177
  }).catch(() => stopWith(listenFailedMessage(port)));
9908
10178
  const authorization = new URL(authorizationEndpoint);
9909
10179
  for (const [name, value] of Object.entries({
@@ -9915,10 +10185,23 @@ async function logIn(authorizationEndpoint, secrets, port, deps) {
9915
10185
  state: secrets.state
9916
10186
  })) authorization.searchParams.set(name, value);
9917
10187
  if (!await deps.openBrowser(authorization.href)) deps.stderr(`${authorization.href}\n`);
9918
- return {
9919
- code: codeOrStop(await loopback.callback ?? stop$1("timeout"), secrets.state),
9920
- redirectUri: loopback.redirectUri
9921
- };
10188
+ const callback = await loopback.callback ?? stop$1("timeout");
10189
+ try {
10190
+ return {
10191
+ code: codeOrStop(callback, secrets.state),
10192
+ redirectUri: loopback.redirectUri,
10193
+ loopback
10194
+ };
10195
+ } catch (error) {
10196
+ loopback.finish({ status: callbackFailure(callback, secrets.state) });
10197
+ throw error;
10198
+ }
10199
+ }
10200
+ function callbackFailure(callback, state) {
10201
+ if (callback.state !== state) return "failed";
10202
+ if (callback.error === "access_denied") return "denied";
10203
+ if (callback.error === "no_workspace") return "no_workspace";
10204
+ return "failed";
9922
10205
  }
9923
10206
  function codeOrStop(callback, state) {
9924
10207
  if (callback.state !== state) return stop$1("loginFailed");
@@ -9961,21 +10244,35 @@ async function save(issued, connection, settingsPath, args, store) {
9961
10244
  OTEL_TRACES_EXPORTER: "otlp",
9962
10245
  OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
9963
10246
  };
9964
- if (args.sendContent && issued.contentOptIn) env.OTEL_LOG_USER_PROMPTS = "1";
10247
+ Object.assign(env, contentEnv(issued.content, args.sendContent));
9965
10248
  const { settings, isOn } = withPluginAutoUpdate(mergeUserSettings(current.settings, env), args.noPluginAutoUpdate);
9966
10249
  await writeUserSettings(settingsPath, settings);
9967
10250
  return isOn;
9968
10251
  }
10252
+ function contentEnv(content, sendContent) {
10253
+ const flag = (isStored) => sendContent && isStored ? "1" : "0";
10254
+ const modelAndTools = flag(content.isModelAndToolsStored);
10255
+ return {
10256
+ OTEL_LOG_USER_PROMPTS: flag(content.isPromptsStored),
10257
+ OTEL_LOG_ASSISTANT_RESPONSES: modelAndTools,
10258
+ OTEL_LOG_TOOL_DETAILS: modelAndTools,
10259
+ OTEL_LOG_TOOL_CONTENT: modelAndTools
10260
+ };
10261
+ }
10262
+ function contentNotice(content, sendContent) {
10263
+ if (!sendContent) return INIT_MESSAGES.contentNotSent;
10264
+ return content.isPromptsStored || content.isModelAndToolsStored ? contentEnabledMessage(content) : INIT_MESSAGES.contentNotStored;
10265
+ }
9969
10266
  async function saveCodexConfig(issued, connection, path, sendContent) {
9970
10267
  await writeCodexConfig(path, mergeCodexConfig(await readCodexConfig(path), {
9971
10268
  connection,
9972
10269
  ingestEndpoint: issued.ingestEndpoint,
9973
10270
  ingestKey: issued.ingestKey,
9974
- logUserPrompt: sendContent && issued.contentOptIn
10271
+ logUserPrompt: sendContent && issued.content.isPromptsStored,
10272
+ logAgentResponses: sendContent && issued.content.isModelAndToolsStored
9975
10273
  }));
9976
10274
  }
9977
- //#endregion
9978
- //#region src/otel-headers.ts
10275
+ // src/otel-headers.ts
9979
10276
  const DESTINATION_VARIABLES = [
9980
10277
  "HARNESSFORCE_ENDPOINT",
9981
10278
  "OTEL_EXPORTER_OTLP_ENDPOINT",
@@ -10029,8 +10326,7 @@ async function selectKey(env, managedDir, unlimited) {
10029
10326
  function isPinnedDestination(env, pinnedOrigin) {
10030
10327
  return DESTINATION_VARIABLES.map((name) => env[name]).filter((value) => Boolean(value)).every((value) => parseAllowedUrl(value)?.origin === pinnedOrigin);
10031
10328
  }
10032
- //#endregion
10033
- //#region ../agent-core/src/config/plugins.ts
10329
+ // ../agent-core/src/config/plugins.ts
10034
10330
  const NON_MARKETPLACE_ORIGINS = /* @__PURE__ */ new Set([
10035
10331
  "inline",
10036
10332
  "skills-dir",
@@ -10071,8 +10367,7 @@ async function collectPlugins(pluginsRoot, settingsByPrecedence, scope, guard) {
10071
10367
  collectMcpServers$1(s, (await readJsonObjectFile(join(root, ".mcp.json"), guard))?.mcpServers);
10072
10368
  }
10073
10369
  }
10074
- //#endregion
10075
- //#region ../agent-core/src/config/collect.ts
10370
+ // ../agent-core/src/config/collect.ts
10076
10371
  const COLLECT_BUDGET_MS = 1e3;
10077
10372
  async function readManagedSettings(managedDir, guard) {
10078
10373
  let merged = await readJsonObjectFile(join(managedDir, "managed-settings.json"), guard);
@@ -10206,8 +10501,7 @@ async function collectConfig(options) {
10206
10501
  components: sortComponents(sink.components)
10207
10502
  };
10208
10503
  }
10209
- //#endregion
10210
- //#region src/run/context.ts
10504
+ // src/run/context.ts
10211
10505
  async function resolveConfigVersion(options) {
10212
10506
  const startedMs = options.now().getTime();
10213
10507
  const result = await collectConfig({
@@ -10227,8 +10521,7 @@ async function resolveLaunchContext(options) {
10227
10521
  ...configVersion === void 0 ? {} : { configVersion }
10228
10522
  };
10229
10523
  }
10230
- //#endregion
10231
- //#region src/run/issues.ts
10524
+ // src/run/issues.ts
10232
10525
  const MAX_CANDIDATES = 10;
10233
10526
  const DOT_SEGMENT = /^\.{1,2}$/;
10234
10527
  async function get(url, apiToken, fetchImpl, timeoutMs) {
@@ -10287,8 +10580,7 @@ async function resolveIssue(base, identifier, apiToken, fetchImpl, timeoutMs = R
10287
10580
  }
10288
10581
  return fetchCandidates(base, identifier, apiToken, fetchImpl, timeoutMs);
10289
10582
  }
10290
- //#endregion
10291
- //#region src/run/launch.ts
10583
+ // src/run/launch.ts
10292
10584
  const STRIPPED_FROM_SHELL = /* @__PURE__ */ new Set(["OTEL_EXPORTER_OTLP_HEADERS", ...[
10293
10585
  "TRACES",
10294
10586
  "LOGS",
@@ -10350,8 +10642,7 @@ function buildLaunch(request) {
10350
10642
  ...isClaudeCode(request.agent) ? { settingsEnv: injectedEnv } : {}
10351
10643
  };
10352
10644
  }
10353
- //#endregion
10354
- //#region src/run/messages.ts
10645
+ // src/run/messages.ts
10355
10646
  const RUN_MESSAGES = {
10356
10647
  keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
10357
10648
  initRequired: "Run `harnessforce init`",
@@ -10364,22 +10655,21 @@ const RUN_MESSAGES = {
10364
10655
  candidatesHeader: "Could not resolve the Issue. Candidates:"
10365
10656
  };
10366
10657
  const launchFailedMessage = (agent) => `Could not start ${agent}`;
10367
- //#endregion
10368
- //#region src/run/run.ts
10658
+ // src/run/run.ts
10369
10659
  const stop = (message) => stopWith(RUN_MESSAGES[message]);
10370
10660
  const printable = (text) => text.replace(/\p{Cc}/gu, "");
10371
10661
  function candidateList(candidates) {
10372
10662
  const lines = candidates.map(({ identifier, title }) => `${printable(identifier)} ${printable(title)}`);
10373
10663
  return [RUN_MESSAGES.candidatesHeader, ...lines].join("\n");
10374
10664
  }
10375
- const ACCESS_MESSAGES$1 = {
10665
+ const ACCESS_MESSAGES$2 = {
10376
10666
  keychainUnavailable: RUN_MESSAGES.keychainUnavailable,
10377
10667
  initRequired: RUN_MESSAGES.initRequired,
10378
10668
  invalidUrl: RUN_MESSAGES.invalidUrl,
10379
10669
  apiTokenMissing: RUN_MESSAGES.issueInitRequired
10380
10670
  };
10381
10671
  async function prepareLaunch(args, deps) {
10382
- const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
10672
+ const { workspaceId, ingestEndpoint, readApiBase, accessToken } = await verifyCliAccess(deps, ACCESS_MESSAGES$2);
10383
10673
  if (!isIssueIdentifier(args.issue)) stop("invalidIssue");
10384
10674
  const readFetch = createApiTokenSession({
10385
10675
  keychain: deps.keychain,
@@ -10444,8 +10734,88 @@ async function runWithoutHarnessforce(args, deps) {
10444
10734
  deps.stderr(`${launchFailedMessage(args.agent)}\n`);
10445
10735
  return 1;
10446
10736
  }
10447
- //#endregion
10448
- //#region src/top/args.ts
10737
+ // src/status/command.ts
10738
+ const STATUS_USAGE = "Usage: harnessforce status [--json]\n";
10739
+ const MESSAGES = {
10740
+ failed: "Could not read the Harnessforce status. Run `harnessforce init` and try again.",
10741
+ loginExpired: "Your login has expired. Run `harnessforce init`"
10742
+ };
10743
+ const ACCESS_MESSAGES$1 = {
10744
+ keychainUnavailable: INIT_MESSAGES.keychainUnavailable,
10745
+ initRequired: MESSAGES.failed,
10746
+ invalidUrl: MESSAGES.failed,
10747
+ apiTokenMissing: MESSAGES.failed
10748
+ };
10749
+ const STEP_TITLES = {
10750
+ 2: "Connect the GitHub App",
10751
+ 3: "Assign repositories to a project",
10752
+ 4: "Set up telemetry",
10753
+ 5: "Wait for your first run"
10754
+ };
10755
+ const STATUS_MARKS = {
10756
+ done: "✓",
10757
+ todo: "・",
10758
+ unavailable: "?"
10759
+ };
10760
+ function unwrap(outcome) {
10761
+ if (outcome.kind === "ok") return outcome.value;
10762
+ return stopWith(outcome.kind === "unauthorized" ? MESSAGES.loginExpired : MESSAGES.failed);
10763
+ }
10764
+ async function originRepository(cwd, git) {
10765
+ const url = await git(cwd, [
10766
+ "remote",
10767
+ "get-url",
10768
+ "origin"
10769
+ ]);
10770
+ return url === void 0 ? void 0 : normalizeRepository(url);
10771
+ }
10772
+ function stepLine(step) {
10773
+ const parts = [STATUS_MARKS[step.status], STEP_TITLES[step.step]];
10774
+ if (step.status === "todo") parts.push(step.url);
10775
+ return parts.join(" ");
10776
+ }
10777
+ async function runStatus(json, deps) {
10778
+ const access = await verifyCliAccess(deps, ACCESS_MESSAGES$1);
10779
+ const readFetch = createApiTokenSession({
10780
+ keychain: deps.keychain,
10781
+ workspaceId: access.workspaceId,
10782
+ readBase: access.readApiBase,
10783
+ fetch: deps.fetch,
10784
+ now: deps.now,
10785
+ sleep: deps.sleep,
10786
+ homeDir: deps.homeDir
10787
+ }).authorizedFetch(deps.fetch);
10788
+ const onboarding = unwrap(await fetchOnboarding(access.readApiBase, access.accessToken, readFetch));
10789
+ const name = await originRepository(deps.cwd, deps.git);
10790
+ const repository = name === void 0 ? null : {
10791
+ name,
10792
+ connected: unwrap(await listConnectedRepositories(access.readApiBase, access.accessToken, readFetch)).has(name)
10793
+ };
10794
+ if (json) {
10795
+ deps.stdout(`${JSON.stringify({
10796
+ ...onboarding,
10797
+ repository
10798
+ })}\n`);
10799
+ return 0;
10800
+ }
10801
+ const lines = [...onboarding.admin_steps ?? [], ...onboarding.steps].sort((a, b) => a.step - b.step).map(stepLine);
10802
+ const repositoryLine = repository && `This repository (${repository.name}) is ${repository.connected ? "connected" : "not connected"} to the GitHub App`;
10803
+ deps.stdout(`${[
10804
+ `${onboarding.workspace_name} (${onboarding.role})`,
10805
+ ...lines,
10806
+ ...repositoryLine ? [repositoryLine] : []
10807
+ ].join("\n")}\n`);
10808
+ return 0;
10809
+ }
10810
+ function statusCommand(args, deps) {
10811
+ const json = args.length === 1 && args[0] === "--json";
10812
+ if (args.length > 0 && !json) {
10813
+ deps.stderr(STATUS_USAGE);
10814
+ return Promise.resolve(1);
10815
+ }
10816
+ return runUntilStop(() => runStatus(json, deps), deps.stderr);
10817
+ }
10818
+ // src/top/args.ts
10449
10819
  const TOP_USAGE = "Usage: harnessforce top [--once] [--json] [--ascii] [--theme <auto|light|dark|ansi>]\n";
10450
10820
  const THEMES = [
10451
10821
  "auto",
@@ -10474,8 +10844,7 @@ function parseTopArgs(args) {
10474
10844
  theme
10475
10845
  };
10476
10846
  }
10477
- //#endregion
10478
- //#region src/top/width.ts
10847
+ // src/top/width.ts
10479
10848
  const SGR = /\x1b\[[0-9;]*m/g;
10480
10849
  const stripSgr = (text) => text.replace(SGR, "");
10481
10850
  const WIDE_RANGES = [
@@ -10540,8 +10909,7 @@ function truncate(text, maxWidth, ambiguousWide, ellipsis) {
10540
10909
  function padEnd(text, width, ambiguousWide) {
10541
10910
  return text + " ".repeat(Math.max(0, width - displayWidth(text, ambiguousWide)));
10542
10911
  }
10543
- //#endregion
10544
- //#region src/top/frame.ts
10912
+ // src/top/frame.ts
10545
10913
  const plainPainter = {
10546
10914
  color: (_token, text) => text,
10547
10915
  bold: (text) => text,
@@ -10741,8 +11109,7 @@ function renderDetail(row, options) {
10741
11109
  ];
10742
11110
  }
10743
11111
  const skippedNotice = (snapshot) => snapshot.skippedLines > 0 || snapshot.skippedFiles > 0 ? [`Skipped ${snapshot.skippedLines} unreadable line(s) and ${snapshot.skippedFiles} unreadable file(s)`] : [];
10744
- //#endregion
10745
- //#region src/top/codex-session.ts
11112
+ // src/top/codex-session.ts
10746
11113
  const DETACHED = "HEAD";
10747
11114
  const SHELL = "shell";
10748
11115
  const COUNTED_CALLS = /* @__PURE__ */ new Set(["function_call", "custom_tool_call"]);
@@ -10860,8 +11227,7 @@ var CodexSessionAccumulator = class {
10860
11227
  };
10861
11228
  }
10862
11229
  };
10863
- //#endregion
10864
- //#region src/top/reader.ts
11230
+ // src/top/reader.ts
10865
11231
  const WINDOW_MS = 864e5;
10866
11232
  const freshState = (newAccumulator) => ({
10867
11233
  offset: 0,
@@ -10986,8 +11352,7 @@ function createTopReader({ projectsDir, codexSessionsDir, git }) {
10986
11352
  };
10987
11353
  } };
10988
11354
  }
10989
- //#endregion
10990
- //#region src/top/color.ts
11355
+ // src/top/color.ts
10991
11356
  const MIN_CONTRAST = 3;
10992
11357
  const TOKEN_VALUES = {
10993
11358
  good: [[
@@ -11126,8 +11491,7 @@ function createPainter(input) {
11126
11491
  dim: (text) => open$1("2", text, "22")
11127
11492
  };
11128
11493
  }
11129
- //#endregion
11130
- //#region src/top/screen.ts
11494
+ // src/top/screen.ts
11131
11495
  const ENTER = "\x1B[?1049h\x1B[?25l";
11132
11496
  const RESTORE = "\x1B[?25h\x1B[?1049l";
11133
11497
  const HOME = "\x1B[H";
@@ -11237,7 +11601,7 @@ async function runScreen(options) {
11237
11601
  };
11238
11602
  const indexOfSelected = (rows) => {
11239
11603
  const index = rows.findIndex((row) => row.sessionId === selectedId);
11240
- return index >= 0 ? index : 0;
11604
+ return Math.max(index, 0);
11241
11605
  };
11242
11606
  const select = (rows, index) => {
11243
11607
  selectedId = rows[Math.max(0, Math.min(index, rows.length - 1))]?.sessionId;
@@ -11347,8 +11711,7 @@ async function runScreen(options) {
11347
11711
  render();
11348
11712
  });
11349
11713
  }
11350
- //#endregion
11351
- //#region src/top/settings.ts
11714
+ // src/top/settings.ts
11352
11715
  const DEFAULT_SETTINGS = {
11353
11716
  ambiguousWide: false,
11354
11717
  braille: false
@@ -11383,8 +11746,7 @@ async function readTopSettings(homeDir) {
11383
11746
  braille: braille === true
11384
11747
  } };
11385
11748
  }
11386
- //#endregion
11387
- //#region src/top/command.ts
11749
+ // src/top/command.ts
11388
11750
  const iso$1 = (ms) => new Date(ms).toISOString();
11389
11751
  function toJson(row, nowMs) {
11390
11752
  return {
@@ -11447,8 +11809,7 @@ async function topCommand(args, deps) {
11447
11809
  deps.stdout(`${lines.join("\n")}\n`);
11448
11810
  return 0;
11449
11811
  }
11450
- //#endregion
11451
- //#region src/tune/mcp-config.ts
11812
+ // src/tune/mcp-config.ts
11452
11813
  const UNLISTED = "unlisted";
11453
11814
  const PLUGIN_SERVER = /^([A-Za-z0-9._-]+)@[A-Za-z0-9._-]+:(.+)$/;
11454
11815
  const toToolName = (name) => name.replace(/[^A-Za-z0-9_-]/g, "_");
@@ -11468,8 +11829,7 @@ function mcpConfigOf(identifiers) {
11468
11829
  }
11469
11830
  };
11470
11831
  }
11471
- //#endregion
11472
- //#region src/tune/vocabulary.ts
11832
+ // src/tune/vocabulary.ts
11473
11833
  const CONTINUE_PHRASES = /* @__PURE__ */ new Set([
11474
11834
  "continue",
11475
11835
  "go",
@@ -11530,8 +11890,7 @@ const EDIT_TOOLS = /* @__PURE__ */ new Set([
11530
11890
  "NotebookEdit"
11531
11891
  ]);
11532
11892
  const ISSUE_TOOL = /^mcp__.+__get_issue$/;
11533
- //#endregion
11534
- //#region src/tune/analyze.ts
11893
+ // src/tune/analyze.ts
11535
11894
  const ANALYZER_VERSION = "1.1.0";
11536
11895
  const NOT_MEASURED_INTERVENTIONS = /* @__PURE__ */ new Set(["approval", "answer"]);
11537
11896
  const emptyProposalCounts = () => Object.fromEntries(PROPOSAL_KINDS.map((kind) => [kind, {
@@ -11720,8 +12079,7 @@ function analyzeSession(input) {
11720
12079
  records_skipped: input.skippedLines
11721
12080
  };
11722
12081
  }
11723
- //#endregion
11724
- //#region src/tune/config-snapshot.ts
12082
+ // src/tune/config-snapshot.ts
11725
12083
  async function collectAt(projectRoot, deps, startedMs = deps.now()) {
11726
12084
  const result = await collectConfig({
11727
12085
  projectRoot,
@@ -11740,8 +12098,7 @@ async function collectMcpServers(cwd, git, deps) {
11740
12098
  if (collected.kind === "fixed_failure") return [];
11741
12099
  return [...new Set(collected.components.filter((c) => c.kind === "mcp_server").map((c) => c.id))];
11742
12100
  }
11743
- //#endregion
11744
- //#region src/tune/applied.ts
12101
+ // src/tune/applied.ts
11745
12102
  async function detectApplied(proposals, snapshot) {
11746
12103
  const byRoot = /* @__PURE__ */ new Map();
11747
12104
  const collect = (root) => {
@@ -11765,8 +12122,7 @@ async function detectApplied(proposals, snapshot) {
11765
12122
  }
11766
12123
  return detected;
11767
12124
  }
11768
- //#endregion
11769
- //#region src/tune/usage.ts
12125
+ // src/tune/usage.ts
11770
12126
  const USAGE_KINDS = [
11771
12127
  "frequent_compaction",
11772
12128
  "low_cache_reuse",
@@ -11837,8 +12193,7 @@ function modelShares(usages) {
11837
12193
  subagent: shares(subagent)
11838
12194
  };
11839
12195
  }
11840
- //#endregion
11841
- //#region src/tune/display.ts
12196
+ // src/tune/display.ts
11842
12197
  const median = (values) => {
11843
12198
  if (values.length === 0) return null;
11844
12199
  const sorted = [...values].sort((a, b) => a - b);
@@ -11906,8 +12261,7 @@ function localIso(ms) {
11906
12261
  const pad = (n) => String(n).padStart(2, "0");
11907
12262
  return `${new Date(ms + offsetMinutes * 6e4).toISOString().slice(0, 19)}${sign}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`;
11908
12263
  }
11909
- //#endregion
11910
- //#region src/tune/followups.ts
12264
+ // src/tune/followups.ts
11911
12265
  const PERIOD_MS = 12096e5;
11912
12266
  const MIN_SESSIONS = 5;
11913
12267
  const ratio = (numerator, denominator) => denominator === 0 ? null : numerator / denominator;
@@ -12011,8 +12365,7 @@ function computeFollowups(proposals, sessions, nowMs, rangeSinceMs) {
12011
12365
  ended
12012
12366
  };
12013
12367
  }
12014
- //#endregion
12015
- //#region src/tune/lock.ts
12368
+ // src/tune/lock.ts
12016
12369
  const LOCK_WAIT_MS = 3e4;
12017
12370
  const STALE_LOCK_MS = 6e4;
12018
12371
  const HEARTBEAT_MS = 1e4;
@@ -12047,8 +12400,7 @@ async function acquireTuneLock(path, deps) {
12047
12400
  await deps.sleep(RETRY_INTERVAL_MS);
12048
12401
  }
12049
12402
  }
12050
- //#endregion
12051
- //#region src/tune/messages.ts
12403
+ // src/tune/messages.ts
12052
12404
  const NOTICE_ORDER = [
12053
12405
  "insufficient",
12054
12406
  "outOfRange",
@@ -12096,7 +12448,7 @@ const TUNE_MESSAGES = {
12096
12448
  unwritable: "Cannot write to `~/.harnessforce/tune/`, so the analysis results are not sent",
12097
12449
  recordFailed: "Could not record the suggestion",
12098
12450
  recordInvalid: (field) => `The suggestion record input is invalid: ${field}`,
12099
- usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune record, or harnessforce tune --purge",
12451
+ usage: "Usage: harnessforce tune [--all] [--no-send] [--show-report] [--json], harnessforce tune --report-only [--all], harnessforce tune --print-schedule, harnessforce tune record, or harnessforce tune --purge",
12100
12452
  purged: "Deleted the analysis results, suggestions and unsent analysis results in `~/.harnessforce/tune/`",
12101
12453
  purgeFailed: "Could not delete some files in `~/.harnessforce/tune/`"
12102
12454
  };
@@ -12122,8 +12474,7 @@ var Notices = class {
12122
12474
  return code;
12123
12475
  }
12124
12476
  };
12125
- //#endregion
12126
- //#region src/tune/store.ts
12477
+ // src/tune/store.ts
12127
12478
  const tuneDir = (homeDir) => join(homeDir, ".harnessforce", "tune");
12128
12479
  const tunePaths = (homeDir) => {
12129
12480
  const dir = tuneDir(homeDir);
@@ -12169,8 +12520,7 @@ async function writeFileAtomic(path, content) {
12169
12520
  }
12170
12521
  }
12171
12522
  const writeJsonFile = (path, value) => writeFileAtomic(path, `${JSON.stringify(value)}\n`);
12172
- //#endregion
12173
- //#region src/tune/proposals.ts
12523
+ // src/tune/proposals.ts
12174
12524
  const isRecord = (value) => isObject(value) && typeof value.proposal_id === "string" && typeof value.change_type === "string" && PROPOSAL_KINDS.includes(value.change_type) && Array.isArray(value.evidence_session_ids) && (value.attribution === "pending" || value.attribution === "decided");
12175
12525
  async function readProposals(path) {
12176
12526
  const value = await readJsonFile(path);
@@ -12209,8 +12559,7 @@ function countProposals(proposals, sessionId) {
12209
12559
  }
12210
12560
  return counts;
12211
12561
  }
12212
- //#endregion
12213
- //#region src/tune/saved-state.ts
12562
+ // src/tune/saved-state.ts
12214
12563
  async function readDestinations(path) {
12215
12564
  const value = await readJsonFile(path);
12216
12565
  return isObject(value) && isObject(value.destinations) ? value.destinations : {};
@@ -12269,8 +12618,7 @@ const writeSessionConfigs = (path, configs) => writeJsonFile(path, {
12269
12618
  transcript_path: config.transcriptPath
12270
12619
  }]))
12271
12620
  });
12272
- //#endregion
12273
- //#region src/tune/send.ts
12621
+ // src/tune/send.ts
12274
12622
  const BATCH_SIZE = 100;
12275
12623
  const READ_ONLY = "workspace_read_only";
12276
12624
  function parseReply(body, size) {
@@ -12371,8 +12719,7 @@ async function sendReports(options) {
12371
12719
  result.invalid.reasons = [...reasons];
12372
12720
  return result;
12373
12721
  }
12374
- //#endregion
12375
- //#region src/tune/sessions.ts
12722
+ // src/tune/sessions.ts
12376
12723
  const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
12377
12724
  async function readSubagentUsages(transcriptPath) {
12378
12725
  const dir = join(dirname(transcriptPath), basename(transcriptPath, ".jsonl"), "subagents");
@@ -12421,8 +12768,7 @@ async function readTuneSessions(options) {
12421
12768
  }
12422
12769
  return [...sessions.values()].sort((a, b) => Date.parse(a.session.startedAt) - Date.parse(b.session.startedAt) || byCodeUnit(a.session.sessionId, b.session.sessionId));
12423
12770
  }
12424
- //#endregion
12425
- //#region src/tune/settings.ts
12771
+ // src/tune/settings.ts
12426
12772
  const tuneConfigPath = (homeDir) => join(homeDir, ".harnessforce", "config.json");
12427
12773
  const isMissing = (error) => error?.code === "ENOENT";
12428
12774
  async function readSendSetting(homeDir) {
@@ -12447,8 +12793,7 @@ async function readSendSetting(homeDir) {
12447
12793
  if (typeof sendReport !== "boolean") return "unreadable";
12448
12794
  return sendReport ? "send" : "no_send";
12449
12795
  }
12450
- //#endregion
12451
- //#region src/tune/command.ts
12796
+ // src/tune/command.ts
12452
12797
  const DAY_MS = 864e5;
12453
12798
  const DEFAULT_RANGE_DAYS = 30;
12454
12799
  const SKIPPED_RATIO = .05;
@@ -12689,7 +13034,7 @@ async function analyzeUnderLock(options, access, paths, deps) {
12689
13034
  ]).then(() => true, () => false);
12690
13035
  if (!isWritten) notices.add("unwritable", TUNE_MESSAGES.unwritable, 1);
12691
13036
  if (options.json) deps.stdout(`${asciiJson(output)}\n`);
12692
- else {
13037
+ else if (!options.reportOnly) {
12693
13038
  deps.stdout(renderAnalysis(analyzed.map((a) => a.report), analyzed.map((a) => a.tune.usage), {
12694
13039
  sessionCount: analyzed.length,
12695
13040
  analyzerVersion: ANALYZER_VERSION,
@@ -12730,8 +13075,7 @@ function analyzeCommand(options, deps) {
12730
13075
  }
12731
13076
  }, deps.stderr);
12732
13077
  }
12733
- //#endregion
12734
- //#region src/tune/purge.ts
13078
+ // src/tune/purge.ts
12735
13079
  async function purgeTuneFiles(paths) {
12736
13080
  const targets = [
12737
13081
  paths.repositories,
@@ -12746,8 +13090,7 @@ async function purgeTuneFiles(paths) {
12746
13090
  force: true
12747
13091
  })))).every((r) => r.status === "fulfilled");
12748
13092
  }
12749
- //#endregion
12750
- //#region src/tune/analysis-file.ts
13093
+ // src/tune/analysis-file.ts
12751
13094
  async function readAnalysisSessions(path) {
12752
13095
  const value = await readJsonFile(path);
12753
13096
  const sessions = isObject(value) && Array.isArray(value.sessions) ? value.sessions : [];
@@ -12762,8 +13105,7 @@ async function readAnalysisSessions(path) {
12762
13105
  }
12763
13106
  return found;
12764
13107
  }
12765
- //#endregion
12766
- //#region src/tune/record.ts
13108
+ // src/tune/record.ts
12767
13109
  const MAX_RECORD_INPUT_BYTES = 1048576;
12768
13110
  const SCOPES = [
12769
13111
  "user",
@@ -12927,33 +13269,41 @@ async function recordProposal(text, paths, now) {
12927
13269
  attributed_session_id: record.attributed_session_id
12928
13270
  };
12929
13271
  }
12930
- //#endregion
12931
- //#region src/tune/tune.ts
13272
+ // src/tune/tune.ts
12932
13273
  const FLAGS = {
12933
13274
  "--all": "all",
12934
13275
  "--no-send": "noSend",
12935
13276
  "--show-report": "showReport",
12936
- "--json": "json"
13277
+ "--json": "json",
13278
+ "--report-only": "reportOnly"
12937
13279
  };
13280
+ const REPORT_ONLY = "--report-only";
12938
13281
  function parseArgs(args) {
12939
13282
  if (args.length === 1 && args[0] === "record") return { kind: "record" };
12940
13283
  if (args.length === 1 && args[0] === "--purge") return { kind: "purge" };
13284
+ if (args.length === 1 && args[0] === "--print-schedule") return { kind: "printSchedule" };
12941
13285
  const options = {
12942
13286
  all: false,
12943
13287
  noSend: false,
12944
13288
  showReport: false,
12945
- json: false
13289
+ json: false,
13290
+ reportOnly: false
12946
13291
  };
12947
13292
  for (const arg of args) {
12948
13293
  const key = FLAGS[arg];
12949
13294
  if (key === void 0 || options[key]) return void 0;
12950
13295
  options[key] = true;
12951
13296
  }
13297
+ if (options.reportOnly && (options.noSend || options.showReport || options.json)) return void 0;
12952
13298
  return {
12953
13299
  kind: "analyze",
12954
13300
  options
12955
13301
  };
12956
13302
  }
13303
+ function cronCommandPath(path) {
13304
+ return (/^[A-Za-z0-9_./-]+$/.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`).replaceAll("%", "\\%");
13305
+ }
13306
+ const scheduleLine = (selfPath) => `0 9 * * 1 ${cronCommandPath(selfPath)} tune ${REPORT_ONLY}\n`;
12957
13307
  function canTuneSend(args) {
12958
13308
  const parsed = parseArgs(args);
12959
13309
  return parsed?.kind === "analyze" && !parsed.options.noSend;
@@ -13003,6 +13353,10 @@ async function tuneCommand(args, deps) {
13003
13353
  return 2;
13004
13354
  }
13005
13355
  if (parsed.kind === "record") return record(deps);
13356
+ if (parsed.kind === "printSchedule") {
13357
+ deps.stdout(scheduleLine(deps.selfPath));
13358
+ return 0;
13359
+ }
13006
13360
  if (parsed.kind === "purge") return underLock(deps, TUNE_MESSAGES.purgeFailed, async (paths) => {
13007
13361
  if (!await purgeTuneFiles(paths)) return stopWith(TUNE_MESSAGES.purgeFailed);
13008
13362
  deps.stderr(`${TUNE_MESSAGES.purged}\n`);
@@ -13010,8 +13364,7 @@ async function tuneCommand(args, deps) {
13010
13364
  });
13011
13365
  return analyzeCommand(parsed.options, deps);
13012
13366
  }
13013
- //#endregion
13014
- //#region src/main.ts
13367
+ // src/main.ts
13015
13368
  const { version } = createRequire(import.meta.url)("../package.json");
13016
13369
  const EXIT_CLIENT_UNSUPPORTED = 5;
13017
13370
  function parseInitArgs(args) {
@@ -13108,17 +13461,19 @@ async function run(argv, deps) {
13108
13461
  git: cli.importGit,
13109
13462
  now: () => cli.now().getTime()
13110
13463
  }));
13464
+ if (command === "status") return guarded(() => statusCommand(rest, {
13465
+ ...cli,
13466
+ git: cli.importGit,
13467
+ now: () => cli.now().getTime()
13468
+ }));
13111
13469
  if (command === "tune") return guarded(() => tuneCommand(rest, {
13112
13470
  ...cli,
13113
13471
  git: cli.importGit,
13114
13472
  now: () => cli.now().getTime()
13115
13473
  }));
13116
13474
  if (command === "hook") {
13117
- if (rest.length !== 1 || rest[0] !== "session-start") {
13118
- cli.stderr(HOOK_USAGE);
13119
- return 1;
13120
- }
13121
- return sessionStart({
13475
+ const [event] = rest;
13476
+ const hookDeps = {
13122
13477
  env: cli.env,
13123
13478
  homeDir: cli.homeDir,
13124
13479
  now: cli.now,
@@ -13130,7 +13485,14 @@ async function run(argv, deps) {
13130
13485
  stderr: cli.stderr,
13131
13486
  client,
13132
13487
  defaultUrl: cli.defaultUrl
13488
+ };
13489
+ if (rest.length === 1 && event === "session-start") return sessionStart(hookDeps);
13490
+ if (rest.length === 1 && (event === "post-tool-use" || event === "stop")) return workLocationHook(event, {
13491
+ ...hookDeps,
13492
+ hookTmpDir: cli.hookTmpDir
13133
13493
  });
13494
+ cli.stderr(HOOK_USAGE);
13495
+ return 1;
13134
13496
  }
13135
13497
  if (command === "top") return topCommand(rest, {
13136
13498
  homeDir: cli.homeDir,
@@ -13156,12 +13518,12 @@ async function run(argv, deps) {
13156
13518
  cli.stderr(UNKNOWN_COMMAND_HINT);
13157
13519
  return 1;
13158
13520
  }
13159
- //#endregion
13160
- //#region src/relaunch.ts
13521
+ // src/relaunch.ts
13161
13522
  const SENDING_COMMANDS = /* @__PURE__ */ new Set([
13162
13523
  "init",
13163
13524
  "run",
13164
13525
  "import",
13526
+ "status",
13165
13527
  "hook"
13166
13528
  ]);
13167
13529
  const isSending = (argv) => SENDING_COMMANDS.has(argv[0] ?? "") || argv[0] === "tune" && canTuneSend(argv.slice(1));
@@ -13185,8 +13547,7 @@ async function relaunchHf(argv, deps) {
13185
13547
  deps.stderr(`${RESTART_FAILED}\n`);
13186
13548
  return 1;
13187
13549
  }
13188
- //#endregion
13189
- //#region src/run/command.ts
13550
+ // src/run/command.ts
13190
13551
  const hasSeparator = (command, platform) => platform === "win32" ? /[\\/]/.test(command) : command.includes("/");
13191
13552
  const contextFor = ({ platform, env, cwd, fs }) => ({
13192
13553
  platform,
@@ -13202,8 +13563,7 @@ async function resolveAgentFile(command, options) {
13202
13563
  function commandLine(file, args, options) {
13203
13564
  return commandLineFor(file, args, contextFor(options));
13204
13565
  }
13205
- //#endregion
13206
- //#region src/run/process.ts
13566
+ // src/run/process.ts
13207
13567
  const IGNORED_WHILE_RUNNING = ["SIGINT", "SIGQUIT"];
13208
13568
  const FORWARDED = ["SIGTERM", "SIGHUP"];
13209
13569
  const FAILED = { kind: "failed" };
@@ -13265,8 +13625,7 @@ function spawnAndWait(command, env) {
13265
13625
  }));
13266
13626
  });
13267
13627
  }
13268
- //#endregion
13269
- //#region src/top/process-io.ts
13628
+ // src/top/process-io.ts
13270
13629
  const EXIT_SIGNALS = ["SIGTERM", "SIGHUP"];
13271
13630
  function createProcessIo(proc = process) {
13272
13631
  const { stdin, stdout } = proc;
@@ -13327,8 +13686,7 @@ function createProcessIo(proc = process) {
13327
13686
  }
13328
13687
  };
13329
13688
  }
13330
- //#endregion
13331
- //#region src/bin.ts
13689
+ // src/bin.ts
13332
13690
  const require = createRequire(import.meta.url);
13333
13691
  const { url: defaultUrl, isDefault } = require("./build-config.json");
13334
13692
  const CALLBACK_TIMEOUT_MS = 3e5;
@@ -13368,6 +13726,7 @@ process.exitCode = relaunched ?? await run(argv, {
13368
13726
  devBuild: !isDefault,
13369
13727
  callbackTimeoutMs: CALLBACK_TIMEOUT_MS,
13370
13728
  cwd: process.cwd(),
13729
+ selfPath: process.argv[1],
13371
13730
  git: createGitRunner({
13372
13731
  platform: process.platform,
13373
13732
  env: process.env,
@@ -13390,6 +13749,7 @@ process.exitCode = relaunched ?? await run(argv, {
13390
13749
  processCwd: process.cwd(),
13391
13750
  excludeTarget: true
13392
13751
  }),
13752
+ hookTmpDir: tmpdir(),
13393
13753
  sleep: (ms) => setTimeout$1(ms),
13394
13754
  top: createProcessIo(),
13395
13755
  readStdin: async (maxBytes) => {
@@ -13403,5 +13763,4 @@ process.exitCode = relaunched ?? await run(argv, {
13403
13763
  return Buffer.concat(chunks);
13404
13764
  }
13405
13765
  });
13406
- //#endregion
13407
13766
  export {};