@ascenda-one/github-collector 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -200,6 +200,102 @@ var require_workMilestoneClassifier = __commonJS({
200
200
  }
201
201
  });
202
202
 
203
+ // ../packages/tool-kit/out/autonomyBand.js
204
+ var require_autonomyBand = __commonJS({
205
+ "../packages/tool-kit/out/autonomyBand.js"(exports) {
206
+ "use strict";
207
+ Object.defineProperty(exports, "__esModule", { value: true });
208
+ exports.autonomyBand = autonomyBand;
209
+ function autonomyBand(mode) {
210
+ if (typeof mode !== "string")
211
+ return "unknown";
212
+ return BAND_BY_MODE[mode] ?? "unknown";
213
+ }
214
+ var BAND_BY_MODE = {
215
+ plan: "planning",
216
+ default: "supervised",
217
+ accept_edits: "edits_auto",
218
+ // Two tokens, one band — and the reason the tokens stayed two. They differ
219
+ // in how the user arrived at the posture rather than in how much the agent
220
+ // may then do unasked, so today they read the same. If that ever stops being
221
+ // true, this line changes and the whole corpus re-reads correctly, because
222
+ // the wire never collapsed them.
223
+ auto: "delegated",
224
+ dont_ask: "delegated",
225
+ bypass_permissions: "unsupervised"
226
+ };
227
+ }
228
+ });
229
+
230
+ // ../packages/tool-kit/out/modelClassifier.js
231
+ var require_modelClassifier = __commonJS({
232
+ "../packages/tool-kit/out/modelClassifier.js"(exports) {
233
+ "use strict";
234
+ Object.defineProperty(exports, "__esModule", { value: true });
235
+ exports.classifyModelClass = classifyModelClass;
236
+ function classifyModelClass(raw) {
237
+ const candidate = raw;
238
+ if (candidate === void 0 || candidate === null)
239
+ return void 0;
240
+ if (typeof candidate !== "string")
241
+ return "unknown";
242
+ const value = candidate.trim().toLowerCase();
243
+ if (!value)
244
+ return void 0;
245
+ if (ROUTER_SENTINEL.test(value))
246
+ return "router:auto";
247
+ const vendor = readModelVendor(value);
248
+ if (vendor === void 0)
249
+ return "unknown";
250
+ for (const [pattern, modelClass] of TIER_PATTERNS_BY_VENDOR[vendor]) {
251
+ if (pattern.test(value))
252
+ return modelClass;
253
+ }
254
+ return UNKNOWN_TIER_BY_VENDOR[vendor];
255
+ }
256
+ function readModelVendor(value) {
257
+ for (const [pattern, vendor] of VENDOR_PATTERNS) {
258
+ if (pattern.test(value))
259
+ return vendor;
260
+ }
261
+ return void 0;
262
+ }
263
+ var ROUTER_SENTINEL = /^(?:[a-z0-9][a-z0-9._-]*\/)?(?:auto|default)$/;
264
+ var VENDOR_PATTERNS = [
265
+ [/\b(anthropic|claude|opus|sonnet|haiku|fable)\b/, "anthropic"],
266
+ [/\b(openai|gpt|o[1-9])\b/, "openai"],
267
+ [/\b(google|gemini|vertex)\b/, "google"],
268
+ // xAI carries no corporate prefix in any observed id — the family name is
269
+ // the whole marker, exactly as `claude` and `gemini` are for theirs.
270
+ [/\b(xai|grok)\b/, "xai"],
271
+ [/\b(ollama|llamacpp|on[-_]?device|local)\b/, "local"]
272
+ ];
273
+ var TIER_PATTERNS_BY_VENDOR = {
274
+ anthropic: [
275
+ [/\bopus\b/, "anthropic:opus"],
276
+ [/\bsonnet\b/, "anthropic:sonnet"],
277
+ [/\bhaiku\b/, "anthropic:haiku"],
278
+ [/\bfable\b/, "anthropic:fable"]
279
+ ],
280
+ openai: [[/\bgpt\b/, "openai:gpt"]],
281
+ google: [[/\bgemini\b/, "google:gemini"]],
282
+ // One tier for now. The line's coding variants (`grok-code-fast-1`) are the
283
+ // same tier word plus a suffix, and splitting them off would be inventing a
284
+ // distinction the ids do not yet draw — `<vendor>:unknown` is waiting for
285
+ // the day one does.
286
+ xai: [[/\bgrok\b/, "xai:grok"]],
287
+ local: [[/\b(ollama|llamacpp|on[-_]?device)\b/, "local:on_device"]]
288
+ };
289
+ var UNKNOWN_TIER_BY_VENDOR = {
290
+ anthropic: "anthropic:unknown",
291
+ openai: "openai:unknown",
292
+ google: "google:unknown",
293
+ xai: "xai:unknown",
294
+ local: "local:unknown"
295
+ };
296
+ }
297
+ });
298
+
203
299
  // ../packages/tool-kit/out/buckets.js
204
300
  var require_buckets = __commonJS({
205
301
  "../packages/tool-kit/out/buckets.js"(exports) {
@@ -294,89 +390,6 @@ var require_afterHours = __commonJS({
294
390
  }
295
391
  });
296
392
 
297
- // ../packages/tool-kit/out/payload.js
298
- var require_payload = __commonJS({
299
- "../packages/tool-kit/out/payload.js"(exports) {
300
- "use strict";
301
- Object.defineProperty(exports, "__esModule", { value: true });
302
- exports.getString = getString;
303
- exports.getNumber = getNumber;
304
- exports.getNested = getNested;
305
- exports.getNestedString = getNestedString;
306
- exports.getNestedNumber = getNestedNumber;
307
- exports.inferOutcome = inferOutcome;
308
- exports.outcomeForHook = outcomeForHook;
309
- exports.looksLikeCorrection = looksLikeCorrection;
310
- function getString(input, keys) {
311
- for (const key of keys) {
312
- const value = input[key];
313
- if (typeof value === "string" && value.trim())
314
- return value;
315
- }
316
- return void 0;
317
- }
318
- function getNumber(input, keys) {
319
- for (const key of keys) {
320
- const value = input[key];
321
- if (typeof value === "number" && Number.isFinite(value))
322
- return value;
323
- }
324
- return void 0;
325
- }
326
- function getNested(input, path) {
327
- let current = input;
328
- for (const segment of path) {
329
- if (!current || typeof current !== "object")
330
- return void 0;
331
- current = current[segment];
332
- }
333
- return current;
334
- }
335
- function getNestedString(input, paths) {
336
- for (const path of paths) {
337
- const value = getNested(input, path);
338
- if (typeof value === "string" && value.trim())
339
- return value;
340
- }
341
- return void 0;
342
- }
343
- function getNestedNumber(input, paths) {
344
- for (const path of paths) {
345
- const value = getNested(input, path);
346
- if (typeof value === "number" && Number.isFinite(value))
347
- return value;
348
- }
349
- return void 0;
350
- }
351
- function inferOutcome(input) {
352
- const exitCode = getNumber(input, ["exitCode", "exit_code", "status"]) ?? getNestedNumber(input, [["tool_response", "exitCode"], ["tool_response", "exit_code"], ["result", "exitCode"], ["result", "exit_code"]]);
353
- if (typeof exitCode === "number")
354
- return exitCode === 0 ? "success" : "failure";
355
- const error = getString(input, ["error", "errorMessage"]) ?? getNestedString(input, [["tool_response", "error"], ["result", "error"]]);
356
- if (error)
357
- return "failure";
358
- return "unknown";
359
- }
360
- function outcomeForHook(hookName, input) {
361
- if (hookName === "PostToolUseFailure") {
362
- const interrupted = input["is_interrupt"] === true || getNested(input, ["tool_response", "interrupted"]) === true;
363
- return interrupted ? "cancelled" : "failure";
364
- }
365
- if (hookName === "PostToolUse") {
366
- if (getNested(input, ["tool_response", "interrupted"]) === true)
367
- return "cancelled";
368
- return "success";
369
- }
370
- return "unknown";
371
- }
372
- function looksLikeCorrection(text) {
373
- if (!text)
374
- return false;
375
- return /\b(wrong|incorrect|try again|fix|not what i asked|that's not|that is not|redo|regenerate|you missed|doesn't work|does not work)\b/i.test(text);
376
- }
377
- }
378
- });
379
-
380
393
  // ../packages/tool-contract/out/metricKeys.js
381
394
  var require_metricKeys = __commonJS({
382
395
  "../packages/tool-contract/out/metricKeys.js"(exports) {
@@ -416,6 +429,42 @@ var require_metricKeys = __commonJS({
416
429
  linesChangedBucket: { readBy: ["backend"], backendAliases: ["linesChangedBucket", "lines_changed_bucket"] },
417
430
  // ── Read by the local handoff only ──────────────────────────────────────
418
431
  activeMinutes: { readBy: ["handoff"], unit: "minutes" },
432
+ /**
433
+ * The two halves of `activeMinutes`, and deliberately two keys.
434
+ *
435
+ * They partition it exactly, so a reader can add them — but there is no
436
+ * third key holding the sum, because the sum is `activeMinutes` and it
437
+ * already exists. Presenting one combined "active" figure in place of these
438
+ * is the thing the split was added to stop: an hour of typing and an hour of
439
+ * watching an agent work are not the same hour, and a single number says
440
+ * they are.
441
+ */
442
+ handsOnMinutes: {
443
+ readBy: ["handoff"],
444
+ unit: "minutes",
445
+ note: "Active time immediately preceding a human prompt \u2014 the only interval a transcript can show a person present for, because the prompt at its end is the evidence."
446
+ },
447
+ agentSupervisingMinutes: {
448
+ readBy: ["handoff"],
449
+ unit: "minutes",
450
+ note: "The remaining active time: the agent was working and the person was not typing. NOT a claim that anyone watched it \u2014 nothing in a transcript could show that. Never render as attention."
451
+ },
452
+ // The split's honesty counters. Read by neither the backend nor the handoff
453
+ // on purpose: they exist so a thin or posture-blind session can be told from
454
+ // a complete one, and a reader that ignores them is choosing to, rather than
455
+ // being unable to.
456
+ activeSplitInstants: {
457
+ readBy: ["diagnostic"],
458
+ note: "Distinct timestamps the split ran over, after collapsing ties. The denominator: two minutes off four instants and off four hundred are not the same measurement."
459
+ },
460
+ activeSplitUndatedLines: {
461
+ readBy: ["diagnostic"],
462
+ note: "Known lines carrying a timestamp that would not parse. Absent from the timeline, so both halves are short by an unknown amount and only this says so."
463
+ },
464
+ activeSplitUnposturedInstants: {
465
+ readBy: ["diagnostic"],
466
+ note: "Instants reached before any permissionMode had been declared. Their supervising time lands in the unknown band, which is a blind spot rather than a posture."
467
+ },
419
468
  afterHoursRequests: { readBy: ["handoff"] },
420
469
  approximateLintErrorsCount: { readBy: ["handoff"] },
421
470
  canceledCount: { readBy: ["handoff"] },
@@ -525,7 +574,7 @@ var require_out = __commonJS({
525
574
  "../packages/tool-contract/out/index.js"(exports) {
526
575
  "use strict";
527
576
  Object.defineProperty(exports, "__esModule", { value: true });
528
- exports.backendMetricKeys = exports.METRIC_KEYS = exports.ASCENDA_SEMANTIC_PROVENANCE = exports.ASCENDA_HISTORICAL_CONSENT_SCOPE = exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = exports.ASCENDA_PROVENANCE = exports.ASCENDA_CONSENT_SCOPE = exports.EVENT_WORKLOAD_CATEGORY = exports.COLLABORATION_EVENT_TYPES = exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = void 0;
577
+ exports.backendMetricKeys = exports.METRIC_KEYS = exports.ASCENDA_SEMANTIC_PROVENANCE = exports.ASCENDA_HISTORICAL_CONSENT_SCOPE = exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = exports.ASCENDA_PROVENANCE = exports.ASCENDA_CONSENT_SCOPE = exports.EVENT_WORKLOAD_CATEGORY = exports.TOOL_EVENT_DELIVERED_STATUSES = exports.IDEMPOTENCY_KEY_MAX_LENGTH = exports.EVENT_METADATA_FIELDS = exports.COLLABORATION_EVENT_TYPES = exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = void 0;
529
578
  exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = [
530
579
  "approach_churn_detected",
531
580
  "goal_drift_detected",
@@ -539,6 +588,39 @@ var require_out = __commonJS({
539
588
  "review_given",
540
589
  "pull_request_opened"
541
590
  ];
591
+ exports.EVENT_METADATA_FIELDS = [
592
+ "language",
593
+ "fileType",
594
+ "durationBucket",
595
+ "tokenPressureBucket",
596
+ "linesChangedBucket",
597
+ "commandClass",
598
+ "gitAction",
599
+ "milestoneKind",
600
+ "branchHash",
601
+ "autonomyMode",
602
+ "modelClass",
603
+ "modelId",
604
+ "userModified",
605
+ "outcome",
606
+ "trigger",
607
+ "promptClass",
608
+ "reason",
609
+ "afterHours",
610
+ "activity",
611
+ "message",
612
+ "host",
613
+ "toolName",
614
+ "simulated",
615
+ "relatedEventType",
616
+ "skillVersion",
617
+ "taskFingerprint",
618
+ "importKey",
619
+ "extractionId",
620
+ "importSchema"
621
+ ];
622
+ exports.IDEMPOTENCY_KEY_MAX_LENGTH = 128;
623
+ exports.TOOL_EVENT_DELIVERED_STATUSES = ["accepted", "duplicate"];
542
624
  exports.EVENT_WORKLOAD_CATEGORY = {
543
625
  create_focus_session: "creation",
544
626
  ai_prompt_submitted: "creation",
@@ -594,6 +676,98 @@ var require_out = __commonJS({
594
676
  }
595
677
  });
596
678
 
679
+ // ../packages/tool-kit/out/payload.js
680
+ var require_payload = __commonJS({
681
+ "../packages/tool-kit/out/payload.js"(exports) {
682
+ "use strict";
683
+ Object.defineProperty(exports, "__esModule", { value: true });
684
+ exports.mintIdempotencyKey = mintIdempotencyKey;
685
+ exports.getString = getString;
686
+ exports.getNumber = getNumber;
687
+ exports.getNested = getNested;
688
+ exports.getNestedString = getNestedString;
689
+ exports.getNestedNumber = getNestedNumber;
690
+ exports.inferOutcome = inferOutcome;
691
+ exports.outcomeForHook = outcomeForHook;
692
+ exports.looksLikeCorrection = looksLikeCorrection;
693
+ var node_crypto_1 = __require("node:crypto");
694
+ var tool_contract_1 = require_out();
695
+ function mintIdempotencyKey() {
696
+ const key = (0, node_crypto_1.randomUUID)();
697
+ if (key.length > tool_contract_1.IDEMPOTENCY_KEY_MAX_LENGTH)
698
+ throw new Error("idempotency key exceeds the wire limit");
699
+ return key;
700
+ }
701
+ function getString(input, keys) {
702
+ for (const key of keys) {
703
+ const value = input[key];
704
+ if (typeof value === "string" && value.trim())
705
+ return value;
706
+ }
707
+ return void 0;
708
+ }
709
+ function getNumber(input, keys) {
710
+ for (const key of keys) {
711
+ const value = input[key];
712
+ if (typeof value === "number" && Number.isFinite(value))
713
+ return value;
714
+ }
715
+ return void 0;
716
+ }
717
+ function getNested(input, path) {
718
+ let current = input;
719
+ for (const segment of path) {
720
+ if (!current || typeof current !== "object")
721
+ return void 0;
722
+ current = current[segment];
723
+ }
724
+ return current;
725
+ }
726
+ function getNestedString(input, paths) {
727
+ for (const path of paths) {
728
+ const value = getNested(input, path);
729
+ if (typeof value === "string" && value.trim())
730
+ return value;
731
+ }
732
+ return void 0;
733
+ }
734
+ function getNestedNumber(input, paths) {
735
+ for (const path of paths) {
736
+ const value = getNested(input, path);
737
+ if (typeof value === "number" && Number.isFinite(value))
738
+ return value;
739
+ }
740
+ return void 0;
741
+ }
742
+ function inferOutcome(input) {
743
+ const exitCode = getNumber(input, ["exitCode", "exit_code", "status"]) ?? getNestedNumber(input, [["tool_response", "exitCode"], ["tool_response", "exit_code"], ["result", "exitCode"], ["result", "exit_code"]]);
744
+ if (typeof exitCode === "number")
745
+ return exitCode === 0 ? "success" : "failure";
746
+ const error = getString(input, ["error", "errorMessage"]) ?? getNestedString(input, [["tool_response", "error"], ["result", "error"]]);
747
+ if (error)
748
+ return "failure";
749
+ return "unknown";
750
+ }
751
+ function outcomeForHook(hookName, input) {
752
+ if (hookName === "PostToolUseFailure") {
753
+ const interrupted = input["is_interrupt"] === true || getNested(input, ["tool_response", "interrupted"]) === true;
754
+ return interrupted ? "cancelled" : "failure";
755
+ }
756
+ if (hookName === "PostToolUse") {
757
+ if (getNested(input, ["tool_response", "interrupted"]) === true)
758
+ return "cancelled";
759
+ return "success";
760
+ }
761
+ return "unknown";
762
+ }
763
+ function looksLikeCorrection(text) {
764
+ if (!text)
765
+ return false;
766
+ return /\b(wrong|incorrect|try again|fix|not what i asked|that's not|that is not|redo|regenerate|you missed|doesn't work|does not work)\b/i.test(text);
767
+ }
768
+ }
769
+ });
770
+
597
771
  // ../packages/tool-kit/out/eventLog.js
598
772
  var require_eventLog = __commonJS({
599
773
  "../packages/tool-kit/out/eventLog.js"(exports) {
@@ -694,6 +868,7 @@ var require_http = __commonJS({
694
868
  exports.postToolEvent = postToolEvent;
695
869
  exports.postToolEventsBatch = postToolEventsBatch;
696
870
  exports.parseIngestResponse = parseIngestResponse;
871
+ var tool_contract_1 = require_out();
697
872
  var AscendaApiError = class extends Error {
698
873
  status;
699
874
  errorCode;
@@ -736,6 +911,35 @@ var require_http = __commonJS({
736
911
  throw new AscendaApiError(response.status, void 0, await response.text());
737
912
  return await response.json();
738
913
  }
914
+ function isDeliveredStatus(value) {
915
+ return typeof value === "string" && tool_contract_1.TOOL_EVENT_DELIVERED_STATUSES.includes(value);
916
+ }
917
+ function readSuccessBody(body) {
918
+ let parsed;
919
+ try {
920
+ parsed = JSON.parse(body);
921
+ } catch {
922
+ return { duplicates: 0 };
923
+ }
924
+ if (!parsed || typeof parsed !== "object")
925
+ return { duplicates: 0 };
926
+ const single = parsed.status;
927
+ if (isDeliveredStatus(single))
928
+ return { duplicates: single === "duplicate" ? 1 : 0 };
929
+ const raw = parsed.results;
930
+ if (!Array.isArray(raw))
931
+ return { duplicates: 0 };
932
+ const results = [];
933
+ for (const item of raw) {
934
+ if (!item || typeof item !== "object")
935
+ continue;
936
+ const { index, status, reason } = item;
937
+ if (typeof index !== "number" || typeof status !== "string")
938
+ continue;
939
+ results.push({ index, status, ...typeof reason === "string" ? { reason } : {} });
940
+ }
941
+ return { duplicates: results.filter((item) => item.status === "duplicate").length, results };
942
+ }
739
943
  function isRetryableStatus(status) {
740
944
  return status === 408 || status === 429 || status !== void 0 && status >= 500 && status <= 599;
741
945
  }
@@ -762,8 +966,20 @@ var require_http = __commonJS({
762
966
  }
763
967
  }
764
968
  async function parseIngestResponse(response) {
765
- if (response.ok)
766
- return { result: "accepted", httpStatus: response.status };
969
+ if (response.ok) {
970
+ const outcome = { result: "accepted", httpStatus: response.status };
971
+ let read = { duplicates: 0 };
972
+ try {
973
+ read = readSuccessBody(await response.text());
974
+ } catch {
975
+ read = { duplicates: 0 };
976
+ }
977
+ return {
978
+ ...outcome,
979
+ ...read.duplicates > 0 ? { duplicates: read.duplicates } : {},
980
+ ...read.results !== void 0 ? { results: read.results } : {}
981
+ };
982
+ }
767
983
  const body = await response.text();
768
984
  let errorCode;
769
985
  try {
@@ -828,6 +1044,7 @@ var require_tokenStore = __commonJS({
828
1044
  exports.ascendaHome = ascendaHome;
829
1045
  exports.defaultTokenFilePath = defaultTokenFilePath2;
830
1046
  exports.persistEventWriteToken = persistEventWriteToken2;
1047
+ exports.listPersistedToolInstallationIds = listPersistedToolInstallationIds;
831
1048
  exports.readTokenFile = readTokenFile2;
832
1049
  exports.sanitizeFilePart = sanitizeFilePart;
833
1050
  var fs = __importStar(__require("fs"));
@@ -848,6 +1065,32 @@ var require_tokenStore = __commonJS({
848
1065
  fs.chmodSync(tokenFilePath, 384);
849
1066
  }
850
1067
  }
1068
+ function listPersistedToolInstallationIds(toolType) {
1069
+ const prefix = `${sanitizeFilePart(toolType)}_`;
1070
+ const dir = path.join(ascendaHome(), "tokens");
1071
+ let names;
1072
+ try {
1073
+ names = fs.readdirSync(dir);
1074
+ } catch {
1075
+ return [];
1076
+ }
1077
+ const ids = [];
1078
+ for (const name of names.sort()) {
1079
+ if (!name.startsWith(prefix) || name.length === prefix.length)
1080
+ continue;
1081
+ const file = path.join(dir, name);
1082
+ try {
1083
+ if (!fs.statSync(file).isFile())
1084
+ continue;
1085
+ } catch {
1086
+ continue;
1087
+ }
1088
+ if (readTokenFile2(file) === void 0)
1089
+ continue;
1090
+ ids.push(`${toolType}:${name.slice(prefix.length)}`);
1091
+ }
1092
+ return ids;
1093
+ }
851
1094
  function readTokenFile2(tokenFilePath) {
852
1095
  try {
853
1096
  if (!fs.existsSync(tokenFilePath))
@@ -907,8 +1150,11 @@ var require_stateStore = __commonJS({
907
1150
  }();
908
1151
  Object.defineProperty(exports, "__esModule", { value: true });
909
1152
  exports.defaultStateFilePath = defaultStateFilePath;
1153
+ exports.unresolvedToolInstallationId = unresolvedToolInstallationId;
1154
+ exports.unresolvedStateFilePath = unresolvedStateFilePath;
910
1155
  exports.readCollectorState = readCollectorState;
911
1156
  exports.recordSendOutcome = recordSendOutcome;
1157
+ exports.recordOutboxDiscard = recordOutboxDiscard;
912
1158
  exports.shouldAnnounceFailure = shouldAnnounceFailure;
913
1159
  exports.markFailureNotified = markFailureNotified;
914
1160
  var fs = __importStar(__require("fs"));
@@ -920,6 +1166,12 @@ var require_stateStore = __commonJS({
920
1166
  const base2 = dir ? dir : path.join(os.homedir(), ".ascenda", "state");
921
1167
  return path.join(base2, `${(0, tokenStore_1.sanitizeFilePart)(toolInstallationId)}.json`);
922
1168
  }
1169
+ function unresolvedToolInstallationId(toolType) {
1170
+ return `${toolType}:unresolved`;
1171
+ }
1172
+ function unresolvedStateFilePath(toolType) {
1173
+ return defaultStateFilePath(unresolvedToolInstallationId(toolType));
1174
+ }
923
1175
  function readCollectorState(stateFilePath) {
924
1176
  try {
925
1177
  if (!fs.existsSync(stateFilePath))
@@ -952,7 +1204,28 @@ var require_stateStore = __commonJS({
952
1204
  // the one already open. Carrying `notifiedFailingSince` across a
953
1205
  // continuing episode is what keeps the notice to once per outage.
954
1206
  ...accepted ? {} : { failingSince: previous?.failingSince ?? now },
955
- ...accepted ? {} : previous?.notifiedFailingSince !== void 0 ? { notifiedFailingSince: previous.notifiedFailingSince } : {}
1207
+ ...accepted ? {} : previous?.notifiedFailingSince !== void 0 ? { notifiedFailingSince: previous.notifiedFailingSince } : {},
1208
+ // Cumulative by design: a send outcome, success included, never erases
1209
+ // the record of what the outbox had to throw away.
1210
+ ...previous?.outboxDiscarded !== void 0 ? { outboxDiscarded: previous.outboxDiscarded } : {}
1211
+ };
1212
+ writeStateFile(stateFilePath, next);
1213
+ return next;
1214
+ }
1215
+ function recordOutboxDiscard(stateFilePath, toolInstallationId, discard) {
1216
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1217
+ const previous = readCollectorState(stateFilePath);
1218
+ const next = {
1219
+ ...previous ?? { lastAttemptAt: now, consecutiveFailures: 0 },
1220
+ toolInstallationId,
1221
+ lastOutcome: "outbox_discarded",
1222
+ outboxDiscarded: {
1223
+ total: (previous?.outboxDiscarded?.total ?? 0) + discard.count,
1224
+ lastAt: now,
1225
+ lastCount: discard.count,
1226
+ lastReasons: discard.reasons,
1227
+ ...discard.oldestQueuedAt !== void 0 ? { lastOldestQueuedAt: discard.oldestQueuedAt } : {}
1228
+ }
956
1229
  };
957
1230
  writeStateFile(stateFilePath, next);
958
1231
  return next;
@@ -992,19 +1265,241 @@ var require_stateStore = __commonJS({
992
1265
  }
993
1266
  });
994
1267
 
995
- // ../packages/tool-kit/out/eventSender.js
996
- var require_eventSender = __commonJS({
997
- "../packages/tool-kit/out/eventSender.js"(exports) {
1268
+ // ../packages/tool-kit/out/outbox.js
1269
+ var require_outbox = __commonJS({
1270
+ "../packages/tool-kit/out/outbox.js"(exports) {
998
1271
  "use strict";
999
- Object.defineProperty(exports, "__esModule", { value: true });
1000
- exports.AscendaEventSender = exports.AscendaSemanticEventError = void 0;
1001
- exports.buildEventPayload = buildEventPayload;
1002
- var afterHours_1 = require_afterHours();
1003
- var tool_contract_1 = require_out();
1004
- var eventLog_1 = require_eventLog();
1005
- var http_1 = require_http();
1006
- var tokenStore_1 = require_tokenStore();
1272
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
1273
+ if (k2 === void 0) k2 = k;
1274
+ var desc = Object.getOwnPropertyDescriptor(m, k);
1275
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1276
+ desc = { enumerable: true, get: function() {
1277
+ return m[k];
1278
+ } };
1279
+ }
1280
+ Object.defineProperty(o, k2, desc);
1281
+ } : function(o, m, k, k2) {
1282
+ if (k2 === void 0) k2 = k;
1283
+ o[k2] = m[k];
1284
+ });
1285
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
1286
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
1287
+ } : function(o, v) {
1288
+ o["default"] = v;
1289
+ });
1290
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
1291
+ var ownKeys = function(o) {
1292
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
1293
+ var ar = [];
1294
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1295
+ return ar;
1296
+ };
1297
+ return ownKeys(o);
1298
+ };
1299
+ return function(mod) {
1300
+ if (mod && mod.__esModule) return mod;
1301
+ var result = {};
1302
+ if (mod != null) {
1303
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1304
+ }
1305
+ __setModuleDefault(result, mod);
1306
+ return result;
1307
+ };
1308
+ }();
1309
+ Object.defineProperty(exports, "__esModule", { value: true });
1310
+ exports.DEFAULT_OUTBOX_DRAIN_BATCH_SIZE = exports.DEFAULT_OUTBOX_MAX_AGE_MS = exports.DEFAULT_OUTBOX_MAX_ENTRIES = exports.OUTBOX_DRAIN_ENV_VAR = void 0;
1311
+ exports.outboxDrainEnabled = outboxDrainEnabled;
1312
+ exports.defaultOutboxFilePath = defaultOutboxFilePath;
1313
+ exports.appendToOutbox = appendToOutbox;
1314
+ exports.readOutboxSummary = readOutboxSummary;
1315
+ exports.claimOutbox = claimOutbox;
1316
+ exports.enforceOutboxBounds = enforceOutboxBounds;
1317
+ var fs = __importStar(__require("fs"));
1318
+ var path = __importStar(__require("path"));
1319
+ var stateStore_1 = require_stateStore();
1320
+ var tokenStore_1 = require_tokenStore();
1321
+ exports.OUTBOX_DRAIN_ENV_VAR = "ASCENDA_OUTBOX_DRAIN";
1322
+ exports.DEFAULT_OUTBOX_MAX_ENTRIES = 1e4;
1323
+ exports.DEFAULT_OUTBOX_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
1324
+ exports.DEFAULT_OUTBOX_DRAIN_BATCH_SIZE = 100;
1325
+ var ORPHANED_CLAIM_AGE_MS = 6e4;
1326
+ var CLAIM_SUFFIX = ".draining";
1327
+ function outboxDrainEnabled(env = process.env) {
1328
+ const value = env[exports.OUTBOX_DRAIN_ENV_VAR]?.trim().toLowerCase();
1329
+ return value === "1" || value === "true" || value === "yes" || value === "on";
1330
+ }
1331
+ function defaultOutboxFilePath(toolInstallationId) {
1332
+ const dir = path.dirname((0, stateStore_1.defaultStateFilePath)(toolInstallationId));
1333
+ return path.join(dir, `${(0, tokenStore_1.sanitizeFilePart)(toolInstallationId)}.outbox.jsonl`);
1334
+ }
1335
+ function appendToOutbox(outboxFilePath, payload, now = /* @__PURE__ */ new Date()) {
1336
+ try {
1337
+ const dir = path.dirname(outboxFilePath);
1338
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
1339
+ const entry = { queuedAt: now.toISOString(), payload };
1340
+ fs.appendFileSync(outboxFilePath, `${JSON.stringify(entry)}
1341
+ `, { encoding: "utf8", mode: 384 });
1342
+ if (process.platform !== "win32")
1343
+ fs.chmodSync(outboxFilePath, 384);
1344
+ return true;
1345
+ } catch {
1346
+ return false;
1347
+ }
1348
+ }
1349
+ function readOutboxSummary(outboxFilePath) {
1350
+ const files = [outboxFilePath, ...listClaimFiles(outboxFilePath)].filter((file) => fs.existsSync(file));
1351
+ if (files.length === 0)
1352
+ return void 0;
1353
+ let depth = 0;
1354
+ let unreadableLines = 0;
1355
+ let oldestQueuedAt;
1356
+ for (const file of files) {
1357
+ const { entries, unreadable } = readEntries(file);
1358
+ depth += entries.length;
1359
+ unreadableLines += unreadable;
1360
+ for (const entry of entries) {
1361
+ if (oldestQueuedAt === void 0 || entry.queuedAt < oldestQueuedAt)
1362
+ oldestQueuedAt = entry.queuedAt;
1363
+ }
1364
+ }
1365
+ return { depth, unreadableLines, ...oldestQueuedAt !== void 0 ? { oldestQueuedAt } : {} };
1366
+ }
1367
+ function claimOutbox(outboxFilePath, now = Date.now()) {
1368
+ const claimPath = `${outboxFilePath}.${process.pid}${CLAIM_SUFFIX}`;
1369
+ const claimed = [];
1370
+ try {
1371
+ fs.renameSync(outboxFilePath, claimPath);
1372
+ claimed.push(claimPath);
1373
+ } catch {
1374
+ }
1375
+ let orphanIndex = 0;
1376
+ for (const orphan of listClaimFiles(outboxFilePath)) {
1377
+ if (claimed.includes(orphan))
1378
+ continue;
1379
+ try {
1380
+ if (now - fs.statSync(orphan).mtimeMs < ORPHANED_CLAIM_AGE_MS)
1381
+ continue;
1382
+ const mine = `${claimPath}.${orphanIndex++}`;
1383
+ fs.renameSync(orphan, mine);
1384
+ claimed.push(mine);
1385
+ } catch {
1386
+ }
1387
+ }
1388
+ if (claimed.length === 0)
1389
+ return void 0;
1390
+ const entries = [];
1391
+ let unreadable = 0;
1392
+ for (const file of claimed) {
1393
+ const read = readEntries(file);
1394
+ entries.push(...read.entries);
1395
+ unreadable += read.unreadable;
1396
+ }
1397
+ entries.sort((a, b) => a.queuedAt < b.queuedAt ? -1 : a.queuedAt > b.queuedAt ? 1 : 0);
1398
+ let released = false;
1399
+ return {
1400
+ entries,
1401
+ unreadable,
1402
+ release(remainder) {
1403
+ if (released)
1404
+ return;
1405
+ released = true;
1406
+ if (remainder.length > 0) {
1407
+ try {
1408
+ fs.mkdirSync(path.dirname(outboxFilePath), { recursive: true, mode: 448 });
1409
+ fs.appendFileSync(outboxFilePath, remainder.map((entry) => `${JSON.stringify(entry)}
1410
+ `).join(""), { encoding: "utf8", mode: 384 });
1411
+ if (process.platform !== "win32")
1412
+ fs.chmodSync(outboxFilePath, 384);
1413
+ } catch {
1414
+ return;
1415
+ }
1416
+ }
1417
+ for (const file of claimed) {
1418
+ try {
1419
+ fs.unlinkSync(file);
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ }
1424
+ };
1425
+ }
1426
+ function enforceOutboxBounds(entries, bounds, now = Date.now()) {
1427
+ const reasons = {};
1428
+ let oldestQueuedAt;
1429
+ const cutoff = new Date(now - bounds.maxAgeMs).toISOString();
1430
+ const fresh = [];
1431
+ for (const entry of entries) {
1432
+ if (entry.queuedAt < cutoff) {
1433
+ reasons.age = (reasons.age ?? 0) + 1;
1434
+ if (oldestQueuedAt === void 0 || entry.queuedAt < oldestQueuedAt)
1435
+ oldestQueuedAt = entry.queuedAt;
1436
+ } else {
1437
+ fresh.push(entry);
1438
+ }
1439
+ }
1440
+ const excess = Math.max(0, fresh.length - bounds.maxEntries);
1441
+ if (excess > 0) {
1442
+ reasons.count = excess;
1443
+ const first = fresh[0]?.queuedAt;
1444
+ if (first !== void 0 && (oldestQueuedAt === void 0 || first < oldestQueuedAt))
1445
+ oldestQueuedAt = first;
1446
+ }
1447
+ const kept = excess > 0 ? fresh.slice(excess) : fresh;
1448
+ const count = Object.values(reasons).reduce((sum, n) => sum + (n ?? 0), 0);
1449
+ return { kept, discarded: { count, reasons, ...oldestQueuedAt !== void 0 ? { oldestQueuedAt } : {} } };
1450
+ }
1451
+ function listClaimFiles(outboxFilePath) {
1452
+ const dir = path.dirname(outboxFilePath);
1453
+ const prefix = `${path.basename(outboxFilePath)}.`;
1454
+ try {
1455
+ return fs.readdirSync(dir).filter((name) => name.startsWith(prefix) && name.includes(CLAIM_SUFFIX)).map((name) => path.join(dir, name)).sort();
1456
+ } catch {
1457
+ return [];
1458
+ }
1459
+ }
1460
+ function readEntries(file) {
1461
+ let raw;
1462
+ try {
1463
+ raw = fs.readFileSync(file, "utf8");
1464
+ } catch {
1465
+ return { entries: [], unreadable: 0 };
1466
+ }
1467
+ const entries = [];
1468
+ let unreadable = 0;
1469
+ for (const line of raw.split("\n")) {
1470
+ if (!line.trim())
1471
+ continue;
1472
+ try {
1473
+ const parsed = JSON.parse(line);
1474
+ if (!parsed || typeof parsed !== "object" || typeof parsed.queuedAt !== "string" || !parsed.payload || typeof parsed.payload !== "object") {
1475
+ unreadable += 1;
1476
+ continue;
1477
+ }
1478
+ entries.push({ queuedAt: parsed.queuedAt, payload: parsed.payload });
1479
+ } catch {
1480
+ unreadable += 1;
1481
+ }
1482
+ }
1483
+ return { entries, unreadable };
1484
+ }
1485
+ }
1486
+ });
1487
+
1488
+ // ../packages/tool-kit/out/eventSender.js
1489
+ var require_eventSender = __commonJS({
1490
+ "../packages/tool-kit/out/eventSender.js"(exports) {
1491
+ "use strict";
1492
+ Object.defineProperty(exports, "__esModule", { value: true });
1493
+ exports.AscendaEventSender = exports.AscendaSemanticEventError = void 0;
1494
+ exports.buildEventPayload = buildEventPayload;
1495
+ var afterHours_1 = require_afterHours();
1496
+ var tool_contract_1 = require_out();
1497
+ var eventLog_1 = require_eventLog();
1498
+ var http_1 = require_http();
1499
+ var outbox_1 = require_outbox();
1500
+ var tokenStore_1 = require_tokenStore();
1007
1501
  var stateStore_1 = require_stateStore();
1502
+ var payload_1 = require_payload();
1008
1503
  var AscendaSemanticEventError = class extends Error {
1009
1504
  constructor(message) {
1010
1505
  super(message);
@@ -1017,6 +1512,7 @@ var require_eventSender = __commonJS({
1017
1512
  toolInstallationId: identity.toolInstallationId,
1018
1513
  source: identity.source,
1019
1514
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1515
+ idempotencyKey: (0, payload_1.mintIdempotencyKey)(),
1020
1516
  utcOffsetMinutes: (0, afterHours_1.utcOffsetMinutesAt)(/* @__PURE__ */ new Date()),
1021
1517
  sessionId: identity.sessionId ?? void 0,
1022
1518
  workspaceHash: identity.workspaceHash ?? void 0,
@@ -1034,6 +1530,9 @@ var require_eventSender = __commonJS({
1034
1530
  config;
1035
1531
  eventWriteToken;
1036
1532
  lastState;
1533
+ lastDrain;
1534
+ /** One outbox pass per sender, i.e. per hook process. The hook is on the user's critical path. */
1535
+ outboxServiced = false;
1037
1536
  constructor(config) {
1038
1537
  this.config = config;
1039
1538
  this.eventWriteToken = config.eventWriteToken;
@@ -1072,6 +1571,7 @@ var require_eventSender = __commonJS({
1072
1571
  source: this.config.source,
1073
1572
  eventType: mapped.eventType,
1074
1573
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1574
+ idempotencyKey: (0, payload_1.mintIdempotencyKey)(),
1075
1575
  utcOffsetMinutes: (0, afterHours_1.utcOffsetMinutesAt)(/* @__PURE__ */ new Date()),
1076
1576
  severity: "low",
1077
1577
  sessionId: this.config.sessionId ?? void 0,
@@ -1102,6 +1602,7 @@ var require_eventSender = __commonJS({
1102
1602
  source: this.config.source,
1103
1603
  eventType: mapped.eventType,
1104
1604
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1605
+ idempotencyKey: (0, payload_1.mintIdempotencyKey)(),
1105
1606
  utcOffsetMinutes: (0, afterHours_1.utcOffsetMinutesAt)(/* @__PURE__ */ new Date()),
1106
1607
  severity: "low",
1107
1608
  sessionId: this.config.sessionId ?? void 0,
@@ -1120,15 +1621,30 @@ var require_eventSender = __commonJS({
1120
1621
  * deliberate: Claude Code, Codex, the GitHub collector and the MCP server all
1121
1622
  * send through this method, and the defect being fixed showed up in three
1122
1623
  * separate components because each was left to notice its own failures.
1624
+ *
1625
+ * The outbox is serviced first, once per process. If that pass just watched
1626
+ * the ingest door refuse a batch, the live event is not offered to the same
1627
+ * door a second time in the same instant: it inherits the pass's outcome,
1628
+ * and a retryable one puts it straight in the queue. That is what keeps a
1629
+ * hook during an outage to one bounded round trip instead of three.
1123
1630
  */
1124
1631
  async post(payload) {
1125
- const outcome = await this.attempt(payload);
1632
+ const halted = await this.serviceOutbox();
1633
+ let outcome;
1634
+ let queued = false;
1635
+ if (halted) {
1636
+ outcome = halted;
1637
+ queued = this.isRetryable(outcome) && this.enqueue(payload);
1638
+ } else {
1639
+ outcome = await this.attempt(payload);
1640
+ queued = this.isRetryable(outcome) && this.enqueue(payload);
1641
+ }
1126
1642
  this.lastState = (0, stateStore_1.recordSendOutcome)(this.stateFilePath(), this.config.toolInstallationId, outcome.result, {
1127
1643
  httpStatus: outcome.httpStatus,
1128
1644
  errorCode: outcome.errorCode,
1129
- detail: outcome.detail
1645
+ detail: queued ? withNote(outcome.detail, "queued in outbox") : outcome.detail
1130
1646
  });
1131
- this.log(payload, outcome.result);
1647
+ this.log(payload, outcome.result, queued ? "queued" : void 0);
1132
1648
  return outcome.result;
1133
1649
  }
1134
1650
  /**
@@ -1137,6 +1653,15 @@ var require_eventSender = __commonJS({
1137
1653
  * error gets one retry, because the common cases (a restarting instance, a
1138
1654
  * proxy blip, a 429) clear in well under a second and the alternative is
1139
1655
  * losing the event outright.
1656
+ *
1657
+ * Both recoveries resend the same `payload` object, so the `idempotencyKey`
1658
+ * minted at construction is what the server sees on every attempt. That is
1659
+ * what lets a retry of a request the server actually processed (a timeout
1660
+ * after the write, a 502 from a proxy in front of a 200) come back
1661
+ * `duplicate` instead of landing twice. Never rebuild the payload here.
1662
+ *
1663
+ * When the retry fails too, the caller queues the payload: anything longer
1664
+ * than the pause here is the outbox's job, not another in-process wait.
1140
1665
  */
1141
1666
  async attempt(payload) {
1142
1667
  const outcome = await (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
@@ -1145,12 +1670,138 @@ var require_eventSender = __commonJS({
1145
1670
  return outcome;
1146
1671
  return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
1147
1672
  }
1148
- if (outcome.result === "transport_error" && (outcome.httpStatus === void 0 || (0, http_1.isRetryableStatus)(outcome.httpStatus))) {
1673
+ if (this.isRetryable(outcome)) {
1149
1674
  await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
1150
1675
  return (0, http_1.postToolEvent)(this.config.apiBaseUrl, this.eventWriteToken, payload, this.signal());
1151
1676
  }
1152
1677
  return outcome;
1153
1678
  }
1679
+ /** A failure that never reached a verdict. Replaying can change the answer. */
1680
+ isRetryable(outcome) {
1681
+ return outcome.result === "transport_error" && (outcome.httpStatus === void 0 || (0, http_1.isRetryableStatus)(outcome.httpStatus));
1682
+ }
1683
+ /**
1684
+ * Keeps a refused payload for a later drain. Returns whether it is now on
1685
+ * disk; when it is not (read-only home, full disk) the event is lost and the
1686
+ * journal's detail says so instead of implying it was kept.
1687
+ */
1688
+ enqueue(payload) {
1689
+ return (0, outbox_1.appendToOutbox)(this.outboxFilePath(), payload);
1690
+ }
1691
+ /**
1692
+ * One pass over the outbox: claim it, apply the bounds, and — when sending
1693
+ * is enabled — offer one batch, oldest first, to the batch door.
1694
+ *
1695
+ * Entries are deleted on `accepted` or `duplicate`, decided on `status`
1696
+ * alone; `reason` is for a person reading their logs. A per-item `rejected`
1697
+ * is a verdict, and replaying a verdict cannot change it, so those are
1698
+ * discarded and journaled rather than kept forever. A whole-batch
1699
+ * `validation_failed` is the same verdict for every item. Anything else
1700
+ * stops the pass with everything still on disk, and is returned so the live
1701
+ * send can skip a door that just refused.
1702
+ *
1703
+ * Never loops, never backs off, never sends more than one batch: the next
1704
+ * hook invocation is usually seconds away, and a hook sitting in a retry
1705
+ * loop delays the tool call the user is waiting on.
1706
+ */
1707
+ async serviceOutbox() {
1708
+ if (this.outboxServiced)
1709
+ return void 0;
1710
+ this.outboxServiced = true;
1711
+ const sendEnabled = this.config.outboxDrain ?? (0, outbox_1.outboxDrainEnabled)();
1712
+ const claimed = (0, outbox_1.claimOutbox)(this.outboxFilePath());
1713
+ if (!claimed) {
1714
+ this.lastDrain = { found: 0, discarded: 0, delivered: 0, remaining: 0, sendEnabled };
1715
+ return void 0;
1716
+ }
1717
+ const found = claimed.entries.length + claimed.unreadable;
1718
+ const { kept, discarded } = (0, outbox_1.enforceOutboxBounds)(claimed.entries, {
1719
+ maxEntries: this.config.outboxMaxEntries ?? outbox_1.DEFAULT_OUTBOX_MAX_ENTRIES,
1720
+ maxAgeMs: this.config.outboxMaxAgeMs ?? outbox_1.DEFAULT_OUTBOX_MAX_AGE_MS
1721
+ });
1722
+ if (claimed.unreadable > 0) {
1723
+ discarded.count += claimed.unreadable;
1724
+ discarded.reasons.unreadable = claimed.unreadable;
1725
+ }
1726
+ let discardedTotal = this.journalDiscard(discarded);
1727
+ if (!sendEnabled || kept.length === 0) {
1728
+ claimed.release(kept);
1729
+ this.lastDrain = { found, discarded: discardedTotal, delivered: 0, remaining: kept.length, sendEnabled };
1730
+ return void 0;
1731
+ }
1732
+ const batchSize = this.config.outboxDrainBatchSize ?? outbox_1.DEFAULT_OUTBOX_DRAIN_BATCH_SIZE;
1733
+ const batch = kept.slice(0, batchSize);
1734
+ const rest = kept.slice(batchSize);
1735
+ const outcome = await this.attemptBatch(batch.map((entry) => entry.payload));
1736
+ let delivered = [];
1737
+ let rejected = [];
1738
+ let undecided = [];
1739
+ let halted;
1740
+ if (outcome.result === "accepted") {
1741
+ if (outcome.results === void 0) {
1742
+ delivered = batch;
1743
+ } else {
1744
+ const byIndex = new Map(outcome.results.map((item) => [item.index, item.status]));
1745
+ for (const [index, entry] of batch.entries()) {
1746
+ const status = byIndex.get(index);
1747
+ if (status !== void 0 && tool_contract_1.TOOL_EVENT_DELIVERED_STATUSES.includes(status))
1748
+ delivered.push(entry);
1749
+ else if (status === "rejected")
1750
+ rejected.push(entry);
1751
+ else
1752
+ undecided.push(entry);
1753
+ }
1754
+ }
1755
+ } else if (outcome.result === "validation_failed") {
1756
+ rejected = batch;
1757
+ } else {
1758
+ undecided = batch;
1759
+ halted = outcome;
1760
+ }
1761
+ if (rejected.length > 0) {
1762
+ discardedTotal += this.journalDiscard({ count: rejected.length, reasons: { rejected: rejected.length }, oldestQueuedAt: rejected[0]?.queuedAt });
1763
+ }
1764
+ if (!halted) {
1765
+ this.lastState = (0, stateStore_1.recordSendOutcome)(this.stateFilePath(), this.config.toolInstallationId, outcome.result, {
1766
+ httpStatus: outcome.httpStatus,
1767
+ errorCode: outcome.errorCode,
1768
+ detail: withNote(outcome.detail, `outbox drain: ${delivered.length} delivered`)
1769
+ });
1770
+ }
1771
+ for (const entry of delivered)
1772
+ this.log(entry.payload, "accepted", "drained");
1773
+ const remainder = [...undecided, ...rest];
1774
+ claimed.release(remainder);
1775
+ this.lastDrain = {
1776
+ found,
1777
+ discarded: discardedTotal,
1778
+ delivered: delivered.length,
1779
+ remaining: remainder.length,
1780
+ sendEnabled,
1781
+ ...halted ? { halted: halted.result } : {}
1782
+ };
1783
+ return halted;
1784
+ }
1785
+ /** The batch door, with the same single token renewal as the live path and no in-process retry. */
1786
+ async attemptBatch(payloads) {
1787
+ const outcome = await (0, http_1.postToolEventsBatch)(this.config.apiBaseUrl, this.eventWriteToken, payloads, this.signal());
1788
+ if (outcome.result !== "auth_failed")
1789
+ return outcome;
1790
+ if (!await this.renewEventToken())
1791
+ return outcome;
1792
+ return (0, http_1.postToolEventsBatch)(this.config.apiBaseUrl, this.eventWriteToken, payloads, this.signal());
1793
+ }
1794
+ journalDiscard(discard) {
1795
+ if (discard.count === 0)
1796
+ return 0;
1797
+ const reasons = discard.reasons;
1798
+ this.lastState = (0, stateStore_1.recordOutboxDiscard)(this.stateFilePath(), this.config.toolInstallationId, {
1799
+ count: discard.count,
1800
+ reasons,
1801
+ oldestQueuedAt: discard.oldestQueuedAt
1802
+ });
1803
+ return discard.count;
1804
+ }
1154
1805
  /**
1155
1806
  * The state written by the most recent send, so a caller can decide whether
1156
1807
  * to surface a one-time notice without re-reading the journal it just wrote.
@@ -1158,10 +1809,16 @@ var require_eventSender = __commonJS({
1158
1809
  get state() {
1159
1810
  return this.lastState;
1160
1811
  }
1812
+ /** What this sender's one outbox pass did; undefined before the first send. */
1813
+ get drain() {
1814
+ return this.lastDrain;
1815
+ }
1161
1816
  stateFilePath() {
1162
1817
  return this.config.stateFilePath ?? (0, stateStore_1.defaultStateFilePath)(this.config.toolInstallationId);
1163
1818
  }
1164
- /** Never throws: a renewal that errors is a failed renewal, not a failed turn. */
1819
+ outboxFilePath() {
1820
+ return this.config.outboxFilePath ?? (0, outbox_1.defaultOutboxFilePath)(this.config.toolInstallationId);
1821
+ }
1165
1822
  /**
1166
1823
  * Every send path funnels through {@link post}, so semantic and
1167
1824
  * collaboration signals are logged on the same terms as host events — the
@@ -1171,12 +1828,13 @@ var require_eventSender = __commonJS({
1171
1828
  * It is now `transport_error` through the ordinary path, because the
1172
1829
  * transport returns that outcome instead of throwing.
1173
1830
  */
1174
- log(payload, delivery) {
1831
+ log(payload, delivery, outbox) {
1175
1832
  const logFile = this.config.eventLogFile === void 0 ? (0, eventLog_1.resolveEventLogPath)() : this.config.eventLogFile;
1176
1833
  if (!logFile)
1177
1834
  return;
1178
- (0, eventLog_1.appendEventLog)(logFile, { loggedAt: (/* @__PURE__ */ new Date()).toISOString(), delivery, payload });
1835
+ (0, eventLog_1.appendEventLog)(logFile, { loggedAt: (/* @__PURE__ */ new Date()).toISOString(), delivery, payload, ...outbox ? { outbox } : {} });
1179
1836
  }
1837
+ /** Never throws: a renewal that errors is a failed renewal, not a failed turn. */
1180
1838
  async renewEventToken() {
1181
1839
  try {
1182
1840
  const renewed = await (0, http_1.renewToolToken)(this.config.apiBaseUrl, this.eventWriteToken, this.signal());
@@ -1194,6 +1852,9 @@ var require_eventSender = __commonJS({
1194
1852
  }
1195
1853
  };
1196
1854
  exports.AscendaEventSender = AscendaEventSender2;
1855
+ function withNote(detail, note) {
1856
+ return detail ? `${detail} (${note})` : note;
1857
+ }
1197
1858
  }
1198
1859
  });
1199
1860
 
@@ -1272,72 +1933,335 @@ var require_contextRegistry = __commonJS({
1272
1933
  }
1273
1934
  return upsert(updates, options);
1274
1935
  }
1275
- function recordWorkContextAlias(hash2, label, observedPath, options) {
1276
- if (!hash2 || !label)
1936
+ function recordWorkContextAlias(hash, label, observedPath, options) {
1937
+ if (!hash || !label)
1277
1938
  return false;
1278
- return upsert([{ hash: hash2, kind: "alias", label, observedPath: observedPath ?? null }], options);
1939
+ return upsert([{ hash, kind: "alias", label, observedPath: observedPath ?? null }], options);
1279
1940
  }
1280
1941
  function upsert(updates, options) {
1281
1942
  if (updates.length === 0)
1282
1943
  return false;
1283
1944
  try {
1284
- const registryFilePath = options?.registryFilePath ?? workContextRegistryFilePath();
1285
- const nowIso = (options?.now ?? /* @__PURE__ */ new Date()).toISOString();
1286
- const registry = readWorkContextRegistry(registryFilePath);
1287
- let dirty = false;
1288
- for (const update of updates) {
1289
- const existing = registry.contexts[update.hash];
1290
- if (!existing) {
1291
- registry.contexts[update.hash] = {
1292
- kind: update.kind,
1293
- label: update.label,
1294
- paths: update.observedPath ? [update.observedPath] : [],
1295
- firstSeenAt: nowIso,
1296
- lastSeenAt: nowIso
1297
- };
1298
- dirty = true;
1945
+ const registryFilePath = options?.registryFilePath ?? workContextRegistryFilePath();
1946
+ const nowIso = (options?.now ?? /* @__PURE__ */ new Date()).toISOString();
1947
+ const registry = readWorkContextRegistry(registryFilePath);
1948
+ let dirty = false;
1949
+ for (const update of updates) {
1950
+ const existing = registry.contexts[update.hash];
1951
+ if (!existing) {
1952
+ registry.contexts[update.hash] = {
1953
+ kind: update.kind,
1954
+ label: update.label,
1955
+ paths: update.observedPath ? [update.observedPath] : [],
1956
+ firstSeenAt: nowIso,
1957
+ lastSeenAt: nowIso
1958
+ };
1959
+ dirty = true;
1960
+ continue;
1961
+ }
1962
+ if (existing.kind === "alias" && update.kind !== "alias") {
1963
+ existing.kind = update.kind;
1964
+ dirty = true;
1965
+ }
1966
+ if (existing.label !== update.label && update.kind !== "alias") {
1967
+ existing.label = update.label;
1968
+ dirty = true;
1969
+ }
1970
+ if (update.observedPath && !existing.paths.includes(update.observedPath)) {
1971
+ if (existing.paths.length < MAX_PATHS_PER_ENTRY)
1972
+ existing.paths.push(update.observedPath);
1973
+ dirty = true;
1974
+ }
1975
+ if (dayOf(existing.lastSeenAt) !== dayOf(nowIso)) {
1976
+ existing.lastSeenAt = nowIso;
1977
+ dirty = true;
1978
+ }
1979
+ }
1980
+ if (!dirty)
1981
+ return false;
1982
+ writeRegistry(registryFilePath, registry);
1983
+ return true;
1984
+ } catch {
1985
+ return false;
1986
+ }
1987
+ }
1988
+ function dayOf(iso) {
1989
+ return iso.slice(0, 10);
1990
+ }
1991
+ function writeRegistry(registryFilePath, registry) {
1992
+ const dir = path.dirname(registryFilePath);
1993
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
1994
+ const tmp = `${registryFilePath}.${process.pid}.tmp`;
1995
+ fs.writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
1996
+ `, { encoding: "utf8", mode: 384 });
1997
+ fs.renameSync(tmp, registryFilePath);
1998
+ if (process.platform !== "win32") {
1999
+ fs.chmodSync(registryFilePath, 384);
2000
+ }
2001
+ }
2002
+ }
2003
+ });
2004
+
2005
+ // ../packages/tool-kit/out/credentials.js
2006
+ var require_credentials = __commonJS({
2007
+ "../packages/tool-kit/out/credentials.js"(exports) {
2008
+ "use strict";
2009
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
2010
+ if (k2 === void 0) k2 = k;
2011
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2012
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2013
+ desc = { enumerable: true, get: function() {
2014
+ return m[k];
2015
+ } };
2016
+ }
2017
+ Object.defineProperty(o, k2, desc);
2018
+ } : function(o, m, k, k2) {
2019
+ if (k2 === void 0) k2 = k;
2020
+ o[k2] = m[k];
2021
+ });
2022
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
2023
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
2024
+ } : function(o, v) {
2025
+ o["default"] = v;
2026
+ });
2027
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
2028
+ var ownKeys = function(o) {
2029
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
2030
+ var ar = [];
2031
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
2032
+ return ar;
2033
+ };
2034
+ return ownKeys(o);
2035
+ };
2036
+ return function(mod) {
2037
+ if (mod && mod.__esModule) return mod;
2038
+ var result = {};
2039
+ if (mod != null) {
2040
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
2041
+ }
2042
+ __setModuleDefault(result, mod);
2043
+ return result;
2044
+ };
2045
+ }();
2046
+ Object.defineProperty(exports, "__esModule", { value: true });
2047
+ exports.credentialsFilePath = credentialsFilePath;
2048
+ exports.readMachineCredentials = readMachineCredentials;
2049
+ exports.writeMachineCredentials = writeMachineCredentials;
2050
+ exports.writeTopLevelCredentials = writeTopLevelCredentials;
2051
+ exports.readHostCredentials = readHostCredentials;
2052
+ exports.writeHostCredentials = writeHostCredentials;
2053
+ exports.removeHostCredentials = removeHostCredentials;
2054
+ var fs = __importStar(__require("fs"));
2055
+ var path = __importStar(__require("path"));
2056
+ var tokenStore_1 = require_tokenStore();
2057
+ function credentialsFilePath() {
2058
+ return path.join((0, tokenStore_1.ascendaHome)(), "credentials.json");
2059
+ }
2060
+ function readMachineCredentials() {
2061
+ try {
2062
+ const raw = fs.readFileSync(credentialsFilePath(), "utf8").trim();
2063
+ if (!raw)
2064
+ return void 0;
2065
+ const parsed = JSON.parse(raw);
2066
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
2067
+ return void 0;
2068
+ return parsed;
2069
+ } catch {
2070
+ return void 0;
2071
+ }
2072
+ }
2073
+ function writeMachineCredentials(credentials) {
2074
+ const file = credentialsFilePath();
2075
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
2076
+ fs.writeFileSync(file, `${JSON.stringify(credentials, null, 2)}
2077
+ `, { encoding: "utf8", mode: 384 });
2078
+ if (process.platform !== "win32") {
2079
+ fs.chmodSync(path.dirname(file), 448);
2080
+ fs.chmodSync(file, 384);
2081
+ }
2082
+ }
2083
+ function writeTopLevelCredentials(credentials) {
2084
+ const existing = readMachineCredentials();
2085
+ writeMachineCredentials({ ...credentials, ...existing?.tools ? { tools: existing.tools } : {} });
2086
+ }
2087
+ function readHostCredentials(host) {
2088
+ const entry = readMachineCredentials()?.tools?.[host];
2089
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2090
+ return void 0;
2091
+ return entry;
2092
+ }
2093
+ function writeHostCredentials(host, credentials) {
2094
+ const existing = readMachineCredentials() ?? {};
2095
+ writeMachineCredentials({ ...existing, tools: { ...existing.tools ?? {}, [host]: credentials } });
2096
+ }
2097
+ function removeHostCredentials(host) {
2098
+ const existing = readMachineCredentials();
2099
+ if (!existing?.tools || !(host in existing.tools))
2100
+ return;
2101
+ const tools = { ...existing.tools };
2102
+ delete tools[host];
2103
+ const next = { ...existing };
2104
+ if (Object.keys(tools).length)
2105
+ next.tools = tools;
2106
+ else
2107
+ delete next.tools;
2108
+ writeMachineCredentials(next);
2109
+ }
2110
+ }
2111
+ });
2112
+
2113
+ // ../packages/tool-kit/out/forgeProject.js
2114
+ var require_forgeProject = __commonJS({
2115
+ "../packages/tool-kit/out/forgeProject.js"(exports) {
2116
+ "use strict";
2117
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
2118
+ if (k2 === void 0) k2 = k;
2119
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2120
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2121
+ desc = { enumerable: true, get: function() {
2122
+ return m[k];
2123
+ } };
2124
+ }
2125
+ Object.defineProperty(o, k2, desc);
2126
+ } : function(o, m, k, k2) {
2127
+ if (k2 === void 0) k2 = k;
2128
+ o[k2] = m[k];
2129
+ });
2130
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
2131
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
2132
+ } : function(o, v) {
2133
+ o["default"] = v;
2134
+ });
2135
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
2136
+ var ownKeys = function(o) {
2137
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
2138
+ var ar = [];
2139
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
2140
+ return ar;
2141
+ };
2142
+ return ownKeys(o);
2143
+ };
2144
+ return function(mod) {
2145
+ if (mod && mod.__esModule) return mod;
2146
+ var result = {};
2147
+ if (mod != null) {
2148
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
2149
+ }
2150
+ __setModuleDefault(result, mod);
2151
+ return result;
2152
+ };
2153
+ }();
2154
+ Object.defineProperty(exports, "__esModule", { value: true });
2155
+ exports.forgeProjectHash = forgeProjectHash2;
2156
+ exports.parseForgeFullName = parseForgeFullName;
2157
+ exports.readForgeFullName = readForgeFullName;
2158
+ exports.forgeFullNameFromConfig = forgeFullNameFromConfig;
2159
+ exports.recordForgeProjectAlias = recordForgeProjectAlias;
2160
+ var fs = __importStar(__require("fs"));
2161
+ var path = __importStar(__require("path"));
2162
+ var contextRegistry_1 = require_contextRegistry();
2163
+ function forgeProjectHash2(value) {
2164
+ let h = 2166136261;
2165
+ for (let i = 0; i < value.length; i++) {
2166
+ h ^= value.charCodeAt(i);
2167
+ h = Math.imul(h, 16777619) >>> 0;
2168
+ }
2169
+ return h.toString(16).padStart(8, "0");
2170
+ }
2171
+ function parseForgeFullName(remoteUrl) {
2172
+ if (!remoteUrl)
2173
+ return null;
2174
+ const trimmed = remoteUrl.trim();
2175
+ if (!trimmed)
2176
+ return null;
2177
+ const scp = /^(?:[^@/]+@)?([^/:]+):(.+)$/.exec(trimmed);
2178
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\/(?:[^@/]*@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
2179
+ let host;
2180
+ let repoPath;
2181
+ if (scheme) {
2182
+ host = scheme[2];
2183
+ repoPath = scheme[3];
2184
+ } else if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
2185
+ host = scp[1];
2186
+ repoPath = scp[2];
2187
+ } else {
2188
+ return null;
2189
+ }
2190
+ const normalizedHost = host.toLowerCase().replace(/^www\./, "");
2191
+ if (normalizedHost !== "github.com")
2192
+ return null;
2193
+ const segments = repoPath.split("/").filter((segment) => segment.length > 0);
2194
+ if (segments.length < 2)
2195
+ return null;
2196
+ const owner = segments[0];
2197
+ const repo = segments[1].replace(/\.git$/, "");
2198
+ if (!owner || !repo)
2199
+ return null;
2200
+ return `${owner}/${repo}`;
2201
+ }
2202
+ function readForgeFullName(repositoryRoot) {
2203
+ if (!repositoryRoot)
2204
+ return null;
2205
+ let config;
2206
+ try {
2207
+ config = fs.readFileSync(path.join(repositoryRoot, ".git", "config"), "utf8");
2208
+ } catch {
2209
+ return null;
2210
+ }
2211
+ return forgeFullNameFromConfig(config);
2212
+ }
2213
+ function forgeFullNameFromConfig(config) {
2214
+ const remotes = /* @__PURE__ */ new Map();
2215
+ let currentRemote = null;
2216
+ for (const rawLine of config.split(/\r?\n/)) {
2217
+ const line = rawLine.trim();
2218
+ if (!line || line.startsWith("#") || line.startsWith(";"))
2219
+ continue;
2220
+ const section = /^\[([^\]]*)\]$/.exec(line);
2221
+ if (section) {
2222
+ const remote = /^remote\s+"(.*)"$/.exec(section[1].trim());
2223
+ currentRemote = remote ? remote[1] : null;
2224
+ continue;
2225
+ }
2226
+ if (!currentRemote)
2227
+ continue;
2228
+ const entry = /^url\s*=\s*(.*)$/.exec(line);
2229
+ if (entry && !remotes.has(currentRemote))
2230
+ remotes.set(currentRemote, entry[1].trim());
2231
+ }
2232
+ const ordered = [
2233
+ ...remotes.has("origin") ? ["origin"] : [],
2234
+ ...remotes.has("upstream") ? ["upstream"] : [],
2235
+ ...[...remotes.keys()].filter((name) => name !== "origin" && name !== "upstream")
2236
+ ];
2237
+ for (const name of ordered) {
2238
+ const fullName = parseForgeFullName(remotes.get(name));
2239
+ if (fullName)
2240
+ return fullName;
2241
+ }
2242
+ return null;
2243
+ }
2244
+ function recordForgeProjectAlias(context, options) {
2245
+ try {
2246
+ if (!context?.projectHash || !context.projectLabel || !context.projectPath)
2247
+ return false;
2248
+ const fullName = readForgeFullName(context.projectPath);
2249
+ if (!fullName)
2250
+ return false;
2251
+ const variants = [fullName, fullName.toLowerCase()].filter((value, index, all) => all.indexOf(value) === index);
2252
+ let wrote = false;
2253
+ for (const variant of variants) {
2254
+ const hash = forgeProjectHash2(variant);
2255
+ if (hash === context.projectHash || hash === context.workspaceHash)
1299
2256
  continue;
1300
- }
1301
- if (existing.kind === "alias" && update.kind !== "alias") {
1302
- existing.kind = update.kind;
1303
- dirty = true;
1304
- }
1305
- if (existing.label !== update.label && update.kind !== "alias") {
1306
- existing.label = update.label;
1307
- dirty = true;
1308
- }
1309
- if (update.observedPath && !existing.paths.includes(update.observedPath)) {
1310
- if (existing.paths.length < MAX_PATHS_PER_ENTRY)
1311
- existing.paths.push(update.observedPath);
1312
- dirty = true;
1313
- }
1314
- if (dayOf(existing.lastSeenAt) !== dayOf(nowIso)) {
1315
- existing.lastSeenAt = nowIso;
1316
- dirty = true;
1317
- }
2257
+ if ((0, contextRegistry_1.recordWorkContextAlias)(hash, context.projectLabel, context.projectPath, options))
2258
+ wrote = true;
1318
2259
  }
1319
- if (!dirty)
1320
- return false;
1321
- writeRegistry(registryFilePath, registry);
1322
- return true;
2260
+ return wrote;
1323
2261
  } catch {
1324
2262
  return false;
1325
2263
  }
1326
2264
  }
1327
- function dayOf(iso) {
1328
- return iso.slice(0, 10);
1329
- }
1330
- function writeRegistry(registryFilePath, registry) {
1331
- const dir = path.dirname(registryFilePath);
1332
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
1333
- const tmp = `${registryFilePath}.${process.pid}.tmp`;
1334
- fs.writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
1335
- `, { encoding: "utf8", mode: 384 });
1336
- fs.renameSync(tmp, registryFilePath);
1337
- if (process.platform !== "win32") {
1338
- fs.chmodSync(registryFilePath, 384);
1339
- }
1340
- }
1341
2265
  }
1342
2266
  });
1343
2267
 
@@ -1467,6 +2391,10 @@ var require_workContext = __commonJS({
1467
2391
  }();
1468
2392
  Object.defineProperty(exports, "__esModule", { value: true });
1469
2393
  exports.deriveWorkContext = deriveWorkContext;
2394
+ exports.normalizeBranchName = normalizeBranchName;
2395
+ exports.deriveBranchHash = deriveBranchHash;
2396
+ exports.readBranchName = readBranchName;
2397
+ exports.deriveBranchHashForCwd = deriveBranchHashForCwd;
1470
2398
  var fs = __importStar(__require("fs"));
1471
2399
  var path = __importStar(__require("path"));
1472
2400
  var salt_1 = require_salt();
@@ -1481,6 +2409,8 @@ var require_workContext = __commonJS({
1481
2409
  } catch {
1482
2410
  roots = null;
1483
2411
  }
2412
+ if (!roots)
2413
+ roots = inferRootsFromPath(startPath);
1484
2414
  const workspacePath = roots?.checkoutRoot ?? startPath;
1485
2415
  const workspaceLabel = basenameOf(workspacePath);
1486
2416
  if (!workspaceLabel)
@@ -1509,9 +2439,12 @@ var require_workContext = __commonJS({
1509
2439
  stat = null;
1510
2440
  }
1511
2441
  if (stat?.isDirectory())
1512
- return { checkoutRoot: dir, canonicalRoot: dir };
1513
- if (stat?.isFile())
1514
- return { checkoutRoot: dir, canonicalRoot: worktreeParentRoot(dotGit, dir) ?? dir };
2442
+ return { checkoutRoot: dir, canonicalRoot: dir, gitDir: dotGit };
2443
+ if (stat?.isFile()) {
2444
+ const gitDir = readGitdirPointer(dotGit, dir);
2445
+ const canonicalRoot = (gitDir ? worktreeParentRoot(gitDir) : null) ?? dir;
2446
+ return { checkoutRoot: dir, canonicalRoot, gitDir };
2447
+ }
1515
2448
  const parent = path.dirname(dir);
1516
2449
  if (parent === dir)
1517
2450
  return null;
@@ -1519,22 +2452,45 @@ var require_workContext = __commonJS({
1519
2452
  }
1520
2453
  return null;
1521
2454
  }
1522
- function worktreeParentRoot(dotGitFile, containingDir) {
1523
- let gitdir;
2455
+ function readGitdirPointer(dotGitFile, containingDir) {
1524
2456
  try {
1525
2457
  const match = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(dotGitFile, "utf8"));
1526
2458
  if (!match)
1527
2459
  return null;
1528
- gitdir = match[1].trim();
2460
+ return path.resolve(containingDir, match[1].trim());
1529
2461
  } catch {
1530
2462
  return null;
1531
2463
  }
1532
- const resolved = path.resolve(containingDir, gitdir);
2464
+ }
2465
+ function worktreeParentRoot(resolvedGitDir) {
1533
2466
  const marker = `${path.sep}.git${path.sep}worktrees${path.sep}`;
1534
- const idx = resolved.indexOf(marker);
2467
+ const idx = resolvedGitDir.indexOf(marker);
1535
2468
  if (idx === -1)
1536
2469
  return null;
1537
- return resolved.slice(0, idx);
2470
+ return resolvedGitDir.slice(0, idx);
2471
+ }
2472
+ function inferRootsFromPath(startPath) {
2473
+ const sep = startPath.includes("\\") && !startPath.includes("/") ? "\\" : "/";
2474
+ const leading = /^[\\/]/.test(startPath) ? sep : "";
2475
+ const segments = startPath.split(/[\\/]/).filter(Boolean);
2476
+ const join = (count) => leading + segments.slice(0, count).join(sep);
2477
+ for (let i = 0; i + 2 < segments.length; i++) {
2478
+ if (segments[i] === ".claude" && segments[i + 1] === "worktrees") {
2479
+ if (i === 0)
2480
+ return null;
2481
+ return { checkoutRoot: join(i + 3), canonicalRoot: join(i), gitDir: null };
2482
+ }
2483
+ }
2484
+ for (let i = 0; i + 1 < segments.length; i++) {
2485
+ const folder = segments[i];
2486
+ const suffix = ["-worktrees", "-wt"].find((s) => folder.endsWith(s) && folder.length > s.length);
2487
+ if (!suffix)
2488
+ continue;
2489
+ const repoName = folder.slice(0, -suffix.length);
2490
+ const canonicalRoot = leading + [...segments.slice(0, i), repoName].join(sep);
2491
+ return { checkoutRoot: join(i + 2), canonicalRoot, gitDir: null };
2492
+ }
2493
+ return null;
1538
2494
  }
1539
2495
  function stripTrailingSeparators(value) {
1540
2496
  let end = value.length;
@@ -1546,6 +2502,55 @@ var require_workContext = __commonJS({
1546
2502
  const segment = value.split(/[\\/]/).filter(Boolean).pop() ?? null;
1547
2503
  return segment && segment.length > 0 ? segment : null;
1548
2504
  }
2505
+ var REFS_HEADS_PREFIX = "refs/heads/";
2506
+ function normalizeBranchName(branch) {
2507
+ if (!branch)
2508
+ return null;
2509
+ let name = branch.trim();
2510
+ if (name.startsWith(REFS_HEADS_PREFIX))
2511
+ name = name.slice(REFS_HEADS_PREFIX.length).trim();
2512
+ if (!name || name === "HEAD")
2513
+ return null;
2514
+ return name;
2515
+ }
2516
+ function deriveBranchHash(branch, saltFilePath) {
2517
+ const name = normalizeBranchName(branch);
2518
+ if (!name)
2519
+ return null;
2520
+ try {
2521
+ return (0, salt_1.hashWithMachineSalt)(name, saltFilePath);
2522
+ } catch {
2523
+ return null;
2524
+ }
2525
+ }
2526
+ function readBranchName(cwd) {
2527
+ if (!cwd || !cwd.trim())
2528
+ return null;
2529
+ let gitDir = null;
2530
+ try {
2531
+ gitDir = resolveRepositoryRoots(stripTrailingSeparators(cwd.trim()))?.gitDir ?? null;
2532
+ } catch {
2533
+ gitDir = null;
2534
+ }
2535
+ if (!gitDir)
2536
+ return null;
2537
+ let head;
2538
+ try {
2539
+ head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
2540
+ } catch {
2541
+ return null;
2542
+ }
2543
+ const match = /^ref:\s*(.+)$/.exec(head);
2544
+ if (!match)
2545
+ return null;
2546
+ const ref = match[1].trim();
2547
+ if (!ref.startsWith(REFS_HEADS_PREFIX))
2548
+ return null;
2549
+ return normalizeBranchName(ref);
2550
+ }
2551
+ function deriveBranchHashForCwd(cwd, saltFilePath) {
2552
+ return deriveBranchHash(readBranchName(cwd), saltFilePath);
2553
+ }
1549
2554
  }
1550
2555
  });
1551
2556
 
@@ -1554,35 +2559,69 @@ var require_hookAdapter = __commonJS({
1554
2559
  "../packages/tool-kit/out/hookAdapter.js"(exports) {
1555
2560
  "use strict";
1556
2561
  Object.defineProperty(exports, "__esModule", { value: true });
1557
- exports.DEFAULT_API_BASE_URL = void 0;
2562
+ exports.MissingInstallationIdError = exports.DEFAULT_API_BASE_URL = void 0;
2563
+ exports.resolveCliAgentInstallationId = resolveCliAgentInstallationId;
1558
2564
  exports.resolveContextHashes = resolveContextHashes;
1559
2565
  exports.loadCliAgentConfig = loadCliAgentConfig;
1560
2566
  exports.deliverHookEvents = deliverHookEvents;
1561
2567
  var contextRegistry_1 = require_contextRegistry();
2568
+ var credentials_1 = require_credentials();
2569
+ var forgeProject_1 = require_forgeProject();
1562
2570
  var eventLog_1 = require_eventLog();
1563
2571
  var eventSender_1 = require_eventSender();
2572
+ var stateStore_1 = require_stateStore();
1564
2573
  var tokenStore_1 = require_tokenStore();
1565
2574
  var workContext_1 = require_workContext();
1566
2575
  exports.DEFAULT_API_BASE_URL = "https://api.ascenda.one";
2576
+ var MissingInstallationIdError = class extends Error {
2577
+ /** The token files that were considered — none, or too many to pick from. */
2578
+ candidates;
2579
+ toolType;
2580
+ constructor(toolType, candidates, setupCommand) {
2581
+ super(candidates.length === 0 ? `Not configured: no ASCENDA_TOOL_INSTALLATION_ID, no pairing in ~/.ascenda/credentials.json, and no ${toolType} token in ~/.ascenda/tokens/. Run: ${setupCommand}` : `Not configured: no ASCENDA_TOOL_INSTALLATION_ID, no pairing in ~/.ascenda/credentials.json, and ${candidates.length} ${toolType} tokens in ~/.ascenda/tokens/ (${candidates.join(", ")}) \u2014 refusing to guess. Export ASCENDA_TOOL_INSTALLATION_ID to choose one, or run: ${setupCommand}`);
2582
+ this.name = "MissingInstallationIdError";
2583
+ this.toolType = toolType;
2584
+ this.candidates = candidates;
2585
+ }
2586
+ };
2587
+ exports.MissingInstallationIdError = MissingInstallationIdError;
2588
+ function resolveCliAgentInstallationId(toolType, identity = {}) {
2589
+ const fromEnv = process.env.ASCENDA_TOOL_INSTALLATION_ID?.trim();
2590
+ if (fromEnv)
2591
+ return { toolInstallationId: qualify(toolType, fromEnv), source: "env" };
2592
+ const fromCredentials = identity.host ? (0, credentials_1.readHostCredentials)(identity.host)?.toolInstallationId?.trim() : void 0;
2593
+ if (fromCredentials)
2594
+ return { toolInstallationId: qualify(toolType, fromCredentials), source: "credentials" };
2595
+ const candidates = (0, tokenStore_1.listPersistedToolInstallationIds)(toolType);
2596
+ if (candidates.length === 1)
2597
+ return { toolInstallationId: candidates[0], source: "disk" };
2598
+ throw new MissingInstallationIdError(toolType, candidates, identity.setupCommand ?? defaultSetupCommand(identity.host));
2599
+ }
2600
+ function defaultSetupCommand(host) {
2601
+ return host ? `npx @ascenda-one/${host.replace(/_cli$/, "")}-hooks setup` : "the agent's setup command";
2602
+ }
2603
+ function qualify(toolType, value) {
2604
+ return value.includes(":") ? value : `${toolType}:${value}`;
2605
+ }
1567
2606
  function resolveContextHashes(cwd) {
1568
2607
  const workspaceOverride = process.env.ASCENDA_WORKSPACE_HASH?.trim() || null;
1569
2608
  const projectOverride = process.env.ASCENDA_PROJECT_HASH?.trim() || null;
1570
2609
  if (workspaceOverride && projectOverride)
1571
2610
  return { workspaceHash: workspaceOverride, projectHash: projectOverride };
1572
2611
  const context = (0, workContext_1.deriveWorkContext)(cwd ?? process.cwd());
1573
- if (context)
2612
+ if (context) {
1574
2613
  (0, contextRegistry_1.recordWorkContext)(context);
2614
+ (0, forgeProject_1.recordForgeProjectAlias)(context);
2615
+ }
1575
2616
  return {
1576
2617
  workspaceHash: workspaceOverride ?? context?.workspaceHash ?? null,
1577
2618
  projectHash: projectOverride ?? context?.projectHash ?? null
1578
2619
  };
1579
2620
  }
1580
- function loadCliAgentConfig(toolType, sessionIdFromHook, cwd) {
1581
- const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? exports.DEFAULT_API_BASE_URL).replace(/\/$/, "");
1582
- const toolInstallationIdRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
1583
- if (!toolInstallationIdRaw)
1584
- throw new Error("Missing ASCENDA_TOOL_INSTALLATION_ID");
1585
- const toolInstallationId = toolInstallationIdRaw.trim().includes(":") ? toolInstallationIdRaw.trim() : `${toolType}:${toolInstallationIdRaw.trim()}`;
2621
+ function loadCliAgentConfig(toolType, sessionIdFromHook, cwd, identity = {}) {
2622
+ const credentials = identity.host ? (0, credentials_1.readHostCredentials)(identity.host) : void 0;
2623
+ const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? credentials?.apiBaseUrl ?? exports.DEFAULT_API_BASE_URL).replace(/\/$/, "");
2624
+ const { toolInstallationId } = resolveCliAgentInstallationId(toolType, identity);
1586
2625
  const tokenFilePath = process.env.ASCENDA_EVENT_WRITE_TOKEN_FILE ?? (0, tokenStore_1.defaultTokenFilePath)(toolInstallationId);
1587
2626
  const fileToken = (0, tokenStore_1.readTokenFile)(tokenFilePath);
1588
2627
  const eventWriteToken = fileToken ?? process.env.ASCENDA_EVENT_WRITE_TOKEN;
@@ -1609,8 +2648,10 @@ var require_hookAdapter = __commonJS({
1609
2648
  const notice = options.onNotice ?? ((message) => console.error(message));
1610
2649
  let config;
1611
2650
  try {
1612
- config = loadCliAgentConfig(options.toolType, options.sessionId, options.cwd);
2651
+ config = loadCliAgentConfig(options.toolType, options.sessionId, options.cwd, options);
1613
2652
  } catch (error) {
2653
+ if (error instanceof MissingInstallationIdError)
2654
+ journalSkippedSend(options.host, error);
1614
2655
  const logFile = (0, eventLog_1.resolveEventLogPath)();
1615
2656
  if (!logFile)
1616
2657
  throw error;
@@ -1650,13 +2691,19 @@ var require_hookAdapter = __commonJS({
1650
2691
  } else if (result === "auth_failed") {
1651
2692
  notice("Ascenda telemetry paused: connection revoked or expired. Re-pair via an Ascenda IDE extension or pairing-sim.");
1652
2693
  } else if (result === "transport_error") {
1653
- notice("Ascenda telemetry paused: the ingest endpoint could not be reached. Your work is unaffected.");
2694
+ notice("Ascenda telemetry paused: the ingest endpoint could not be reached; the event is kept in the outbox. Your work is unaffected.");
1654
2695
  } else {
1655
2696
  notice(`Ascenda telemetry rejected: ${result}`);
1656
2697
  }
1657
2698
  return;
1658
2699
  }
1659
2700
  }
2701
+ function journalSkippedSend(host, error) {
2702
+ const who = host ? `${host}: ` : "";
2703
+ (0, stateStore_1.recordSendOutcome)((0, stateStore_1.unresolvedStateFilePath)(error.toolType), (0, stateStore_1.unresolvedToolInstallationId)(error.toolType), "skipped_no_installation_id", {
2704
+ detail: error.candidates.length === 0 ? `${who}no ASCENDA_TOOL_INSTALLATION_ID, no credentials.json pairing, no ${error.toolType} token file` : `${who}no ASCENDA_TOOL_INSTALLATION_ID, no credentials.json pairing, ${error.candidates.length} ${error.toolType} token files (${error.candidates.join(", ")})`
2705
+ });
2706
+ }
1660
2707
  function parsePositiveInt(value) {
1661
2708
  const n = Number(value);
1662
2709
  return Number.isInteger(n) && n > 0 ? n : void 0;
@@ -1664,6 +2711,387 @@ var require_hookAdapter = __commonJS({
1664
2711
  }
1665
2712
  });
1666
2713
 
2714
+ // ../packages/tool-kit/out/cliAgentSetup.js
2715
+ var require_cliAgentSetup = __commonJS({
2716
+ "../packages/tool-kit/out/cliAgentSetup.js"(exports) {
2717
+ "use strict";
2718
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
2719
+ if (k2 === void 0) k2 = k;
2720
+ var desc = Object.getOwnPropertyDescriptor(m, k);
2721
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
2722
+ desc = { enumerable: true, get: function() {
2723
+ return m[k];
2724
+ } };
2725
+ }
2726
+ Object.defineProperty(o, k2, desc);
2727
+ } : function(o, m, k, k2) {
2728
+ if (k2 === void 0) k2 = k;
2729
+ o[k2] = m[k];
2730
+ });
2731
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
2732
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
2733
+ } : function(o, v) {
2734
+ o["default"] = v;
2735
+ });
2736
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
2737
+ var ownKeys = function(o) {
2738
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
2739
+ var ar = [];
2740
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
2741
+ return ar;
2742
+ };
2743
+ return ownKeys(o);
2744
+ };
2745
+ return function(mod) {
2746
+ if (mod && mod.__esModule) return mod;
2747
+ var result = {};
2748
+ if (mod != null) {
2749
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
2750
+ }
2751
+ __setModuleDefault(result, mod);
2752
+ return result;
2753
+ };
2754
+ }();
2755
+ Object.defineProperty(exports, "__esModule", { value: true });
2756
+ exports.isCliAgentManagementCommand = isCliAgentManagementCommand;
2757
+ exports.cliAgentHookBinPath = cliAgentHookBinPath;
2758
+ exports.runCliAgentSetup = runCliAgentSetup;
2759
+ exports.writeHookSettings = writeHookSettings;
2760
+ exports.findStaleHookCommands = findStaleHookCommands;
2761
+ var crypto = __importStar(__require("crypto"));
2762
+ var fs = __importStar(__require("fs"));
2763
+ var os = __importStar(__require("os"));
2764
+ var path = __importStar(__require("path"));
2765
+ var credentials_1 = require_credentials();
2766
+ var hookAdapter_1 = require_hookAdapter();
2767
+ var http_1 = require_http();
2768
+ var tokenStore_1 = require_tokenStore();
2769
+ var MANAGEMENT_COMMANDS = /* @__PURE__ */ new Set(["setup", "install", "status", "uninstall", "-h", "--help"]);
2770
+ function isCliAgentManagementCommand(argument) {
2771
+ return argument !== void 0 && MANAGEMENT_COMMANDS.has(argument);
2772
+ }
2773
+ function cliAgentHookBinPath(binaryName) {
2774
+ return path.join((0, tokenStore_1.ascendaHome)(), "bin", binaryName);
2775
+ }
2776
+ function usage(spec) {
2777
+ return `${spec.binaryName} setup \u2014 wire ${spec.displayName} to Ascenda telemetry
2778
+
2779
+ npx ${spec.packageName} setup [options]
2780
+ npx ${spec.packageName} status
2781
+ npx ${spec.packageName} uninstall
2782
+
2783
+ Options
2784
+ --api-base-url <url> ingest host (default ${hookAdapter_1.DEFAULT_API_BASE_URL})
2785
+ --local [port] shorthand for the local dev server (default port 4477)
2786
+ --tool-installation-id <id> reuse an existing pairing instead of creating one
2787
+ --token <eventWriteToken> reuse an existing token (stored 0600, never printed)
2788
+ --scope project|user where hooks are registered (default project)
2789
+ --project-dir <path> project root for --scope project (default cwd)
2790
+ --dry-run print what would change, write nothing
2791
+ -h, --help
2792
+ `;
2793
+ }
2794
+ async function runCliAgentSetup(argv, spec) {
2795
+ let options;
2796
+ try {
2797
+ options = parseArgs(argv, spec);
2798
+ } catch (error) {
2799
+ console.error(error instanceof Error ? error.message : String(error));
2800
+ return 1;
2801
+ }
2802
+ if (options.action === "help") {
2803
+ console.log(usage(spec));
2804
+ return 0;
2805
+ }
2806
+ if (options.action === "status")
2807
+ return printStatus(options, spec);
2808
+ if (options.action === "uninstall")
2809
+ return uninstall(options, spec);
2810
+ const apiBaseUrl = (options.apiBaseUrl ?? (0, credentials_1.readHostCredentials)(spec.host)?.apiBaseUrl ?? hookAdapter_1.DEFAULT_API_BASE_URL).replace(/\/$/, "");
2811
+ console.log(`Ascenda setup for ${spec.displayName} \u2014 ${apiBaseUrl}`);
2812
+ const identity = await resolveIdentity(apiBaseUrl, options, spec);
2813
+ if (!identity)
2814
+ return 1;
2815
+ console.log(` pairing ${identity.toolInstallationId}${identity.paired ? " (new)" : " (existing)"}`);
2816
+ const binary = installBinary(spec, options.dryRun);
2817
+ console.log(` hook binary ${binary}`);
2818
+ if (!options.dryRun) {
2819
+ (0, credentials_1.writeHostCredentials)(spec.host, { apiBaseUrl, toolInstallationId: identity.toolInstallationId, pairedAt: (/* @__PURE__ */ new Date()).toISOString() });
2820
+ }
2821
+ console.log(` credentials ${(0, credentials_1.credentialsFilePath)()} (tools.${spec.host})`);
2822
+ const settingsFile = spec.settings.settingsPath(options.scope, options.projectDir);
2823
+ const written = writeHookSettings(settingsFile, binary, spec, options.dryRun);
2824
+ if (written === null)
2825
+ return 1;
2826
+ console.log(` hooks ${settingsFile} (${spec.hookEvents.length} events${written ? "" : ", already current"})`);
2827
+ if (options.dryRun) {
2828
+ console.log("\nDry run \u2014 nothing was written.");
2829
+ return 0;
2830
+ }
2831
+ console.log(`
2832
+ Done. ${spec.restartHint}`);
2833
+ console.log(`Check anytime: npx ${spec.packageName} status`);
2834
+ return 0;
2835
+ }
2836
+ function parseArgs(argv, spec) {
2837
+ const options = {
2838
+ scope: "project",
2839
+ projectDir: process.cwd(),
2840
+ dryRun: false,
2841
+ action: "install"
2842
+ };
2843
+ for (let i = 0; i < argv.length; i++) {
2844
+ const arg = argv[i];
2845
+ const next = () => {
2846
+ const value = argv[++i];
2847
+ if (value === void 0)
2848
+ throw new Error(`${arg} needs a value`);
2849
+ return value;
2850
+ };
2851
+ switch (arg) {
2852
+ case "setup":
2853
+ case "install":
2854
+ options.action = "install";
2855
+ break;
2856
+ case "status":
2857
+ options.action = "status";
2858
+ break;
2859
+ case "uninstall":
2860
+ options.action = "uninstall";
2861
+ break;
2862
+ case "--api-base-url":
2863
+ options.apiBaseUrl = next();
2864
+ break;
2865
+ case "--local": {
2866
+ const peek = argv[i + 1];
2867
+ const port = peek && /^\d+$/.test(peek) ? argv[++i] : "4477";
2868
+ options.apiBaseUrl = `http://localhost:${port}`;
2869
+ break;
2870
+ }
2871
+ case "--tool-installation-id":
2872
+ options.toolInstallationId = next();
2873
+ break;
2874
+ case "--token":
2875
+ options.token = next();
2876
+ break;
2877
+ case "--scope": {
2878
+ const value = next();
2879
+ if (value !== "project" && value !== "user")
2880
+ throw new Error(`--scope must be project or user, got ${value}`);
2881
+ options.scope = value;
2882
+ break;
2883
+ }
2884
+ case "--project-dir":
2885
+ options.projectDir = path.resolve(next());
2886
+ break;
2887
+ case "--dry-run":
2888
+ options.dryRun = true;
2889
+ break;
2890
+ case "-h":
2891
+ case "--help":
2892
+ options.action = "help";
2893
+ break;
2894
+ default:
2895
+ throw new Error(`unknown argument: ${arg}
2896
+
2897
+ ${usage(spec)}`);
2898
+ }
2899
+ }
2900
+ return options;
2901
+ }
2902
+ async function resolveIdentity(apiBaseUrl, options, spec) {
2903
+ const existingId = options.toolInstallationId ?? (0, credentials_1.readHostCredentials)(spec.host)?.toolInstallationId;
2904
+ if (existingId && options.token) {
2905
+ if (!options.dryRun)
2906
+ (0, tokenStore_1.persistEventWriteToken)((0, tokenStore_1.defaultTokenFilePath)(existingId), options.token);
2907
+ return { toolInstallationId: existingId, paired: false };
2908
+ }
2909
+ if (existingId && (0, tokenStore_1.readTokenFile)((0, tokenStore_1.defaultTokenFilePath)(existingId))) {
2910
+ return { toolInstallationId: existingId, paired: false };
2911
+ }
2912
+ if (options.dryRun) {
2913
+ return { toolInstallationId: existingId ?? `${spec.toolType}:<paired at run time>`, paired: false };
2914
+ }
2915
+ const toolInstallationId = existingId ?? `${spec.toolType}:${crypto.randomUUID()}`;
2916
+ let session;
2917
+ try {
2918
+ session = await (0, http_1.createPairingSession)(apiBaseUrl, toolInstallationId, spec.toolType, `${spec.displayName} on ${os.hostname()}`);
2919
+ } catch (error) {
2920
+ console.error(`
2921
+ Could not reach ${apiBaseUrl} to pair: ${error instanceof Error ? error.message : String(error)}`);
2922
+ console.error("Start the local dev server and use --local, or pass --api-base-url for your backend.");
2923
+ return void 0;
2924
+ }
2925
+ const token = await pollForToken(apiBaseUrl, session.pairingSessionId, session.code, session.expiresAt);
2926
+ if (!token)
2927
+ return void 0;
2928
+ (0, tokenStore_1.persistEventWriteToken)((0, tokenStore_1.defaultTokenFilePath)(toolInstallationId), token);
2929
+ return { toolInstallationId, paired: true };
2930
+ }
2931
+ async function pollForToken(apiBaseUrl, pairingSessionId, code, expiresAt) {
2932
+ const deadline = Math.min(Date.parse(expiresAt) || Date.now() + 3e5, Date.now() + 3e5);
2933
+ let announced = false;
2934
+ while (Date.now() < deadline) {
2935
+ const status = await (0, http_1.getPairingStatus)(apiBaseUrl, pairingSessionId);
2936
+ if (status.status === "paired" && status.eventWriteToken)
2937
+ return status.eventWriteToken;
2938
+ if (status.status === "expired" || status.status === "cancelled") {
2939
+ console.error(`
2940
+ Pairing ${status.status}. Run setup again.`);
2941
+ return void 0;
2942
+ }
2943
+ if (!announced) {
2944
+ console.log(`
2945
+ Confirm in the Ascenda app \u2014 code ${code}`);
2946
+ console.log(" Waiting...");
2947
+ announced = true;
2948
+ }
2949
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2950
+ }
2951
+ console.error("\nPairing timed out. Run setup again.");
2952
+ return void 0;
2953
+ }
2954
+ function installBinary(spec, dryRun) {
2955
+ const target = cliAgentHookBinPath(spec.binaryName);
2956
+ if (dryRun)
2957
+ return target;
2958
+ const source = process.argv[1];
2959
+ fs.mkdirSync(path.dirname(target), { recursive: true });
2960
+ if (path.resolve(source) !== path.resolve(target)) {
2961
+ fs.copyFileSync(source, target);
2962
+ }
2963
+ if (process.platform !== "win32")
2964
+ fs.chmodSync(target, 493);
2965
+ return target;
2966
+ }
2967
+ function writeHookSettings(settingsFile, binary, spec, dryRun) {
2968
+ let settings = { ...spec.settings.scaffold ?? {} };
2969
+ const exists = fs.existsSync(settingsFile);
2970
+ if (exists) {
2971
+ const raw = fs.readFileSync(settingsFile, "utf8").trim();
2972
+ if (raw) {
2973
+ try {
2974
+ settings = JSON.parse(raw);
2975
+ } catch {
2976
+ console.error(`
2977
+ ${settingsFile} is not valid JSON. Fix or move it, then run setup again.`);
2978
+ return null;
2979
+ }
2980
+ }
2981
+ }
2982
+ const command = hookCommand(binary);
2983
+ const hooks = { ...settings.hooks ?? {} };
2984
+ for (const event of spec.hookEvents) {
2985
+ const kept = (hooks[event] ?? []).filter((entry) => !isOurs(entry, spec));
2986
+ hooks[event] = [...kept, spec.settings.entry(command, event)];
2987
+ }
2988
+ const updated = { ...settings, hooks };
2989
+ const serialised = `${JSON.stringify(updated, null, 2)}
2990
+ `;
2991
+ if (exists && fs.readFileSync(settingsFile, "utf8") === serialised)
2992
+ return false;
2993
+ if (dryRun) {
2994
+ console.log(`
2995
+ --- ${settingsFile} (dry run) ---
2996
+ ${serialised}`);
2997
+ return true;
2998
+ }
2999
+ if (exists)
3000
+ fs.copyFileSync(settingsFile, `${settingsFile}.ascenda-backup`);
3001
+ fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
3002
+ fs.writeFileSync(settingsFile, serialised, "utf8");
3003
+ return true;
3004
+ }
3005
+ function hookCommand(binary) {
3006
+ return `"${process.execPath}" "${binary}"`;
3007
+ }
3008
+ function isOurs(entry, spec) {
3009
+ const command = spec.settings.commandOf(entry);
3010
+ return typeof command === "string" && command.includes(spec.binaryName);
3011
+ }
3012
+ function findStaleHookCommands(settings, binary, spec) {
3013
+ const stale = /* @__PURE__ */ new Set();
3014
+ for (const entries of Object.values(settings.hooks ?? {})) {
3015
+ for (const entry of entries ?? []) {
3016
+ const command = spec.settings.commandOf(entry);
3017
+ if (typeof command !== "string")
3018
+ continue;
3019
+ if (!/ascenda/i.test(command) || command.includes(binary))
3020
+ continue;
3021
+ stale.add(command);
3022
+ }
3023
+ }
3024
+ return [...stale];
3025
+ }
3026
+ function readSettings(settingsFile) {
3027
+ try {
3028
+ return JSON.parse(fs.readFileSync(settingsFile, "utf8"));
3029
+ } catch {
3030
+ return {};
3031
+ }
3032
+ }
3033
+ function printStatus(options, spec) {
3034
+ const credentials = (0, credentials_1.readHostCredentials)(spec.host);
3035
+ const settingsFile = spec.settings.settingsPath(options.scope, options.projectDir);
3036
+ const binary = cliAgentHookBinPath(spec.binaryName);
3037
+ const tokenFile = credentials?.toolInstallationId ? (0, tokenStore_1.defaultTokenFilePath)(credentials.toolInstallationId) : void 0;
3038
+ const settings = readSettings(settingsFile);
3039
+ const registered = spec.hookEvents.filter((event) => (settings.hooks?.[event] ?? []).some((entry) => isOurs(entry, spec))).length;
3040
+ const stale = findStaleHookCommands(settings, binary, spec);
3041
+ console.log(`api base url ${credentials?.apiBaseUrl ?? "\u2014 not configured"}`);
3042
+ console.log(`pairing ${credentials?.toolInstallationId ?? "\u2014 not paired"}`);
3043
+ console.log(`token ${tokenFile && (0, tokenStore_1.readTokenFile)(tokenFile) ? "present" : "\u2014 missing"}`);
3044
+ console.log(`hook binary ${fs.existsSync(binary) ? binary : "\u2014 not installed"}`);
3045
+ console.log(`hooks ${registered}/${spec.hookEvents.length} registered in ${settingsFile}`);
3046
+ if (stale.length) {
3047
+ console.log(`stale hooks ${stale.length} not pointing at the installed binary \u2014 each one fails silently per event:`);
3048
+ for (const command of stale)
3049
+ console.log(` ${command}`);
3050
+ console.log(` Remove them from ${settingsFile} by hand; setup cannot tell them from a hook you wrote.`);
3051
+ }
3052
+ const healthy = credentials?.toolInstallationId && registered === spec.hookEvents.length && fs.existsSync(binary) && !stale.length;
3053
+ return healthy ? 0 : 1;
3054
+ }
3055
+ function uninstall(options, spec) {
3056
+ const settingsFile = spec.settings.settingsPath(options.scope, options.projectDir);
3057
+ if (fs.existsSync(settingsFile)) {
3058
+ try {
3059
+ const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
3060
+ const hooks = { ...settings.hooks ?? {} };
3061
+ for (const event of Object.keys(hooks)) {
3062
+ const kept = hooks[event].filter((entry) => !isOurs(entry, spec));
3063
+ if (kept.length)
3064
+ hooks[event] = kept;
3065
+ else
3066
+ delete hooks[event];
3067
+ }
3068
+ const updated = { ...settings, hooks };
3069
+ if (!Object.keys(hooks).length)
3070
+ delete updated.hooks;
3071
+ fs.copyFileSync(settingsFile, `${settingsFile}.ascenda-backup`);
3072
+ fs.writeFileSync(settingsFile, `${JSON.stringify(updated, null, 2)}
3073
+ `, "utf8");
3074
+ console.log(`hooks removed from ${settingsFile}`);
3075
+ } catch {
3076
+ console.error(`could not parse ${settingsFile} \u2014 remove the ascenda hook entries by hand`);
3077
+ return 1;
3078
+ }
3079
+ }
3080
+ const binary = cliAgentHookBinPath(spec.binaryName);
3081
+ if (fs.existsSync(binary)) {
3082
+ fs.rmSync(binary);
3083
+ console.log(`removed ${binary}`);
3084
+ }
3085
+ if ((0, credentials_1.readHostCredentials)(spec.host)) {
3086
+ (0, credentials_1.removeHostCredentials)(spec.host);
3087
+ console.log(`removed tools.${spec.host} from ${(0, credentials_1.credentialsFilePath)()}`);
3088
+ }
3089
+ console.log(`tokens left in ${path.join((0, tokenStore_1.ascendaHome)(), "tokens")} \u2014 revoke in the Ascenda app to invalidate them`);
3090
+ return 0;
3091
+ }
3092
+ }
3093
+ });
3094
+
1667
3095
  // ../packages/tool-kit/out/turnState.js
1668
3096
  var require_turnState = __commonJS({
1669
3097
  "../packages/tool-kit/out/turnState.js"(exports) {
@@ -1877,8 +3305,9 @@ var require_out2 = __commonJS({
1877
3305
  "../packages/tool-kit/out/index.js"(exports) {
1878
3306
  "use strict";
1879
3307
  Object.defineProperty(exports, "__esModule", { value: true });
1880
- exports.readWorkContextRegistry = exports.recordWorkContextAlias = exports.recordWorkContext = exports.deriveWorkContext = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.ascendaHome = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.resolveContextHashes = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.BUSINESS_DAY = exports.isOutsideBusinessHours = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1881
- exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.workContextRegistryFilePath = void 0;
3308
+ exports.consumeTurnDurationMs = exports.writeTopLevelCredentials = exports.writeMachineCredentials = exports.writeHostCredentials = exports.removeHostCredentials = exports.readMachineCredentials = exports.readHostCredentials = exports.credentialsFilePath = exports.writeHookSettings = exports.runCliAgentSetup = exports.isCliAgentManagementCommand = exports.findStaleHookCommands = exports.cliAgentHookBinPath = exports.resolveContextHashes = exports.resolveCliAgentInstallationId = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.MissingInstallationIdError = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.mintIdempotencyKey = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.BUSINESS_DAY = exports.isOutsideBusinessHours = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.classifyModelClass = exports.autonomyBand = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
3309
+ exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.recordForgeProjectAlias = exports.forgeFullNameFromConfig = exports.readForgeFullName = exports.parseForgeFullName = exports.forgeProjectHash = exports.workContextRegistryFilePath = exports.readWorkContextRegistry = exports.recordWorkContextAlias = exports.recordWorkContext = exports.readBranchName = exports.normalizeBranchName = exports.deriveBranchHashForCwd = exports.deriveBranchHash = exports.deriveWorkContext = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.enforceOutboxBounds = exports.claimOutbox = exports.readOutboxSummary = exports.appendToOutbox = exports.defaultOutboxFilePath = exports.outboxDrainEnabled = exports.DEFAULT_OUTBOX_DRAIN_BATCH_SIZE = exports.DEFAULT_OUTBOX_MAX_AGE_MS = exports.DEFAULT_OUTBOX_MAX_ENTRIES = exports.OUTBOX_DRAIN_ENV_VAR = exports.recordOutboxDiscard = exports.unresolvedToolInstallationId = exports.unresolvedStateFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.listPersistedToolInstallationIds = exports.defaultTokenFilePath = exports.ascendaHome = exports.recordTurnStart = void 0;
3310
+ exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = void 0;
1882
3311
  var commandClassifier_1 = require_commandClassifier();
1883
3312
  Object.defineProperty(exports, "classifyCommand", { enumerable: true, get: function() {
1884
3313
  return commandClassifier_1.classifyCommand;
@@ -1900,6 +3329,14 @@ var require_out2 = __commonJS({
1900
3329
  Object.defineProperty(exports, "invitesDebrief", { enumerable: true, get: function() {
1901
3330
  return workMilestoneClassifier_1.invitesDebrief;
1902
3331
  } });
3332
+ var autonomyBand_1 = require_autonomyBand();
3333
+ Object.defineProperty(exports, "autonomyBand", { enumerable: true, get: function() {
3334
+ return autonomyBand_1.autonomyBand;
3335
+ } });
3336
+ var modelClassifier_1 = require_modelClassifier();
3337
+ Object.defineProperty(exports, "classifyModelClass", { enumerable: true, get: function() {
3338
+ return modelClassifier_1.classifyModelClass;
3339
+ } });
1903
3340
  var buckets_1 = require_buckets();
1904
3341
  Object.defineProperty(exports, "bucketLinesChanged", { enumerable: true, get: function() {
1905
3342
  return buckets_1.bucketLinesChanged;
@@ -1948,6 +3385,9 @@ var require_out2 = __commonJS({
1948
3385
  Object.defineProperty(exports, "looksLikeCorrection", { enumerable: true, get: function() {
1949
3386
  return payload_1.looksLikeCorrection;
1950
3387
  } });
3388
+ Object.defineProperty(exports, "mintIdempotencyKey", { enumerable: true, get: function() {
3389
+ return payload_1.mintIdempotencyKey;
3390
+ } });
1951
3391
  var eventSender_1 = require_eventSender();
1952
3392
  Object.defineProperty(exports, "AscendaEventSender", { enumerable: true, get: function() {
1953
3393
  return eventSender_1.AscendaEventSender;
@@ -1975,15 +3415,59 @@ var require_out2 = __commonJS({
1975
3415
  Object.defineProperty(exports, "DEFAULT_API_BASE_URL", { enumerable: true, get: function() {
1976
3416
  return hookAdapter_1.DEFAULT_API_BASE_URL;
1977
3417
  } });
3418
+ Object.defineProperty(exports, "MissingInstallationIdError", { enumerable: true, get: function() {
3419
+ return hookAdapter_1.MissingInstallationIdError;
3420
+ } });
1978
3421
  Object.defineProperty(exports, "deliverHookEvents", { enumerable: true, get: function() {
1979
3422
  return hookAdapter_1.deliverHookEvents;
1980
3423
  } });
1981
3424
  Object.defineProperty(exports, "loadCliAgentConfig", { enumerable: true, get: function() {
1982
3425
  return hookAdapter_1.loadCliAgentConfig;
1983
3426
  } });
3427
+ Object.defineProperty(exports, "resolveCliAgentInstallationId", { enumerable: true, get: function() {
3428
+ return hookAdapter_1.resolveCliAgentInstallationId;
3429
+ } });
1984
3430
  Object.defineProperty(exports, "resolveContextHashes", { enumerable: true, get: function() {
1985
3431
  return hookAdapter_1.resolveContextHashes;
1986
3432
  } });
3433
+ var cliAgentSetup_1 = require_cliAgentSetup();
3434
+ Object.defineProperty(exports, "cliAgentHookBinPath", { enumerable: true, get: function() {
3435
+ return cliAgentSetup_1.cliAgentHookBinPath;
3436
+ } });
3437
+ Object.defineProperty(exports, "findStaleHookCommands", { enumerable: true, get: function() {
3438
+ return cliAgentSetup_1.findStaleHookCommands;
3439
+ } });
3440
+ Object.defineProperty(exports, "isCliAgentManagementCommand", { enumerable: true, get: function() {
3441
+ return cliAgentSetup_1.isCliAgentManagementCommand;
3442
+ } });
3443
+ Object.defineProperty(exports, "runCliAgentSetup", { enumerable: true, get: function() {
3444
+ return cliAgentSetup_1.runCliAgentSetup;
3445
+ } });
3446
+ Object.defineProperty(exports, "writeHookSettings", { enumerable: true, get: function() {
3447
+ return cliAgentSetup_1.writeHookSettings;
3448
+ } });
3449
+ var credentials_1 = require_credentials();
3450
+ Object.defineProperty(exports, "credentialsFilePath", { enumerable: true, get: function() {
3451
+ return credentials_1.credentialsFilePath;
3452
+ } });
3453
+ Object.defineProperty(exports, "readHostCredentials", { enumerable: true, get: function() {
3454
+ return credentials_1.readHostCredentials;
3455
+ } });
3456
+ Object.defineProperty(exports, "readMachineCredentials", { enumerable: true, get: function() {
3457
+ return credentials_1.readMachineCredentials;
3458
+ } });
3459
+ Object.defineProperty(exports, "removeHostCredentials", { enumerable: true, get: function() {
3460
+ return credentials_1.removeHostCredentials;
3461
+ } });
3462
+ Object.defineProperty(exports, "writeHostCredentials", { enumerable: true, get: function() {
3463
+ return credentials_1.writeHostCredentials;
3464
+ } });
3465
+ Object.defineProperty(exports, "writeMachineCredentials", { enumerable: true, get: function() {
3466
+ return credentials_1.writeMachineCredentials;
3467
+ } });
3468
+ Object.defineProperty(exports, "writeTopLevelCredentials", { enumerable: true, get: function() {
3469
+ return credentials_1.writeTopLevelCredentials;
3470
+ } });
1987
3471
  var turnState_1 = require_turnState();
1988
3472
  Object.defineProperty(exports, "consumeTurnDurationMs", { enumerable: true, get: function() {
1989
3473
  return turnState_1.consumeTurnDurationMs;
@@ -1998,6 +3482,9 @@ var require_out2 = __commonJS({
1998
3482
  Object.defineProperty(exports, "defaultTokenFilePath", { enumerable: true, get: function() {
1999
3483
  return tokenStore_1.defaultTokenFilePath;
2000
3484
  } });
3485
+ Object.defineProperty(exports, "listPersistedToolInstallationIds", { enumerable: true, get: function() {
3486
+ return tokenStore_1.listPersistedToolInstallationIds;
3487
+ } });
2001
3488
  Object.defineProperty(exports, "persistEventWriteToken", { enumerable: true, get: function() {
2002
3489
  return tokenStore_1.persistEventWriteToken;
2003
3490
  } });
@@ -2020,6 +3507,46 @@ var require_out2 = __commonJS({
2020
3507
  Object.defineProperty(exports, "markFailureNotified", { enumerable: true, get: function() {
2021
3508
  return stateStore_1.markFailureNotified;
2022
3509
  } });
3510
+ Object.defineProperty(exports, "unresolvedStateFilePath", { enumerable: true, get: function() {
3511
+ return stateStore_1.unresolvedStateFilePath;
3512
+ } });
3513
+ Object.defineProperty(exports, "unresolvedToolInstallationId", { enumerable: true, get: function() {
3514
+ return stateStore_1.unresolvedToolInstallationId;
3515
+ } });
3516
+ Object.defineProperty(exports, "recordOutboxDiscard", { enumerable: true, get: function() {
3517
+ return stateStore_1.recordOutboxDiscard;
3518
+ } });
3519
+ var outbox_1 = require_outbox();
3520
+ Object.defineProperty(exports, "OUTBOX_DRAIN_ENV_VAR", { enumerable: true, get: function() {
3521
+ return outbox_1.OUTBOX_DRAIN_ENV_VAR;
3522
+ } });
3523
+ Object.defineProperty(exports, "DEFAULT_OUTBOX_MAX_ENTRIES", { enumerable: true, get: function() {
3524
+ return outbox_1.DEFAULT_OUTBOX_MAX_ENTRIES;
3525
+ } });
3526
+ Object.defineProperty(exports, "DEFAULT_OUTBOX_MAX_AGE_MS", { enumerable: true, get: function() {
3527
+ return outbox_1.DEFAULT_OUTBOX_MAX_AGE_MS;
3528
+ } });
3529
+ Object.defineProperty(exports, "DEFAULT_OUTBOX_DRAIN_BATCH_SIZE", { enumerable: true, get: function() {
3530
+ return outbox_1.DEFAULT_OUTBOX_DRAIN_BATCH_SIZE;
3531
+ } });
3532
+ Object.defineProperty(exports, "outboxDrainEnabled", { enumerable: true, get: function() {
3533
+ return outbox_1.outboxDrainEnabled;
3534
+ } });
3535
+ Object.defineProperty(exports, "defaultOutboxFilePath", { enumerable: true, get: function() {
3536
+ return outbox_1.defaultOutboxFilePath;
3537
+ } });
3538
+ Object.defineProperty(exports, "appendToOutbox", { enumerable: true, get: function() {
3539
+ return outbox_1.appendToOutbox;
3540
+ } });
3541
+ Object.defineProperty(exports, "readOutboxSummary", { enumerable: true, get: function() {
3542
+ return outbox_1.readOutboxSummary;
3543
+ } });
3544
+ Object.defineProperty(exports, "claimOutbox", { enumerable: true, get: function() {
3545
+ return outbox_1.claimOutbox;
3546
+ } });
3547
+ Object.defineProperty(exports, "enforceOutboxBounds", { enumerable: true, get: function() {
3548
+ return outbox_1.enforceOutboxBounds;
3549
+ } });
2023
3550
  var salt_1 = require_salt();
2024
3551
  Object.defineProperty(exports, "machineSaltFilePath", { enumerable: true, get: function() {
2025
3552
  return salt_1.machineSaltFilePath;
@@ -2034,6 +3561,18 @@ var require_out2 = __commonJS({
2034
3561
  Object.defineProperty(exports, "deriveWorkContext", { enumerable: true, get: function() {
2035
3562
  return workContext_1.deriveWorkContext;
2036
3563
  } });
3564
+ Object.defineProperty(exports, "deriveBranchHash", { enumerable: true, get: function() {
3565
+ return workContext_1.deriveBranchHash;
3566
+ } });
3567
+ Object.defineProperty(exports, "deriveBranchHashForCwd", { enumerable: true, get: function() {
3568
+ return workContext_1.deriveBranchHashForCwd;
3569
+ } });
3570
+ Object.defineProperty(exports, "normalizeBranchName", { enumerable: true, get: function() {
3571
+ return workContext_1.normalizeBranchName;
3572
+ } });
3573
+ Object.defineProperty(exports, "readBranchName", { enumerable: true, get: function() {
3574
+ return workContext_1.readBranchName;
3575
+ } });
2037
3576
  var contextRegistry_1 = require_contextRegistry();
2038
3577
  Object.defineProperty(exports, "recordWorkContext", { enumerable: true, get: function() {
2039
3578
  return contextRegistry_1.recordWorkContext;
@@ -2047,6 +3586,22 @@ var require_out2 = __commonJS({
2047
3586
  Object.defineProperty(exports, "workContextRegistryFilePath", { enumerable: true, get: function() {
2048
3587
  return contextRegistry_1.workContextRegistryFilePath;
2049
3588
  } });
3589
+ var forgeProject_1 = require_forgeProject();
3590
+ Object.defineProperty(exports, "forgeProjectHash", { enumerable: true, get: function() {
3591
+ return forgeProject_1.forgeProjectHash;
3592
+ } });
3593
+ Object.defineProperty(exports, "parseForgeFullName", { enumerable: true, get: function() {
3594
+ return forgeProject_1.parseForgeFullName;
3595
+ } });
3596
+ Object.defineProperty(exports, "readForgeFullName", { enumerable: true, get: function() {
3597
+ return forgeProject_1.readForgeFullName;
3598
+ } });
3599
+ Object.defineProperty(exports, "forgeFullNameFromConfig", { enumerable: true, get: function() {
3600
+ return forgeProject_1.forgeFullNameFromConfig;
3601
+ } });
3602
+ Object.defineProperty(exports, "recordForgeProjectAlias", { enumerable: true, get: function() {
3603
+ return forgeProject_1.recordForgeProjectAlias;
3604
+ } });
2050
3605
  var liveBus_1 = require_liveBus();
2051
3606
  Object.defineProperty(exports, "emitLiveSignal", { enumerable: true, get: function() {
2052
3607
  return liveBus_1.emitLiveSignal;
@@ -2089,7 +3644,7 @@ var require_out2 = __commonJS({
2089
3644
  });
2090
3645
 
2091
3646
  // src/cli.ts
2092
- var import_tool_kit2 = __toESM(require_out2(), 1);
3647
+ var import_tool_kit3 = __toESM(require_out2(), 1);
2093
3648
  import { readFile } from "node:fs/promises";
2094
3649
 
2095
3650
  // src/config.ts
@@ -2119,6 +3674,7 @@ function normalizeToolInstallationId(value) {
2119
3674
  }
2120
3675
 
2121
3676
  // src/mapForgeEvent.ts
3677
+ var import_tool_kit2 = __toESM(require_out2(), 1);
2122
3678
  function mapForgeEvent(eventName, payload, viewerLogin) {
2123
3679
  if (!eventName || !viewerLogin) return [];
2124
3680
  const action = str(payload["action"]);
@@ -2170,20 +3726,20 @@ function base(payload) {
2170
3726
  host: "github",
2171
3727
  // Hashed, never the name. "Is it always the same repository" stays
2172
3728
  // answerable; which repository does not travel.
2173
- ...repo ? { projectHash: hash(repo) } : {}
3729
+ //
3730
+ // The digest is an UNSALTED FNV-1a of `owner/repo`, and this step stays
3731
+ // deliberately salt-free: it runs in CI from a webhook payload, where the
3732
+ // only place a machine salt could come from is a repository secret — which
3733
+ // is to say, from everyone who can read the repository's settings. The
3734
+ // function now lives in tool-kit so a developer's own machine, which holds
3735
+ // both identities, can compute this exact digest and file it beside its
3736
+ // own; nothing about what this step emits has changed.
3737
+ ...repo ? { projectHash: (0, import_tool_kit2.forgeProjectHash)(repo) } : {}
2174
3738
  };
2175
3739
  }
2176
3740
  function reviewState(state) {
2177
3741
  return state?.toLowerCase() === "approved" ? "success" : "unknown";
2178
3742
  }
2179
- function hash(value) {
2180
- let h = 2166136261;
2181
- for (let i = 0; i < value.length; i++) {
2182
- h ^= value.charCodeAt(i);
2183
- h = Math.imul(h, 16777619) >>> 0;
2184
- }
2185
- return h.toString(16).padStart(8, "0");
2186
- }
2187
3743
  function obj(value) {
2188
3744
  return value && typeof value === "object" ? value : {};
2189
3745
  }
@@ -2199,7 +3755,7 @@ async function main() {
2199
3755
  if (!payload) return;
2200
3756
  const events = mapForgeEvent(eventName, payload, config.viewerLogin);
2201
3757
  if (events.length === 0) return;
2202
- const sender = new import_tool_kit2.AscendaEventSender({
3758
+ const sender = new import_tool_kit3.AscendaEventSender({
2203
3759
  apiBaseUrl: config.apiBaseUrl,
2204
3760
  toolInstallationId: config.toolInstallationId,
2205
3761
  source: "code_forge",