@outerlayer/cli 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +229 -60
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-7MFH6HNC.js +94 -0
  7. package/dist/chunk-3TFPDVTI.js +94 -0
  8. package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
  9. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  10. package/dist/chunk-5Y3QQRUH.js +300 -0
  11. package/dist/chunk-774HEQL6.js +178 -0
  12. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
  13. package/dist/chunk-ABIWDUMS.js +104 -0
  14. package/dist/chunk-AUFG23AA.js +534 -0
  15. package/dist/chunk-B2G7JHHB.js +42 -0
  16. package/dist/chunk-BCJNHMZT.js +34 -0
  17. package/dist/chunk-BCLJSQCV.js +56 -0
  18. package/dist/chunk-BFESLKPP.js +61 -0
  19. package/dist/chunk-BJ3KTMDH.js +63 -0
  20. package/dist/chunk-BKO6JEZI.js +47 -0
  21. package/dist/chunk-BOLTI6LR.js +37 -0
  22. package/dist/chunk-CBPPJSAR.js +89 -0
  23. package/dist/chunk-DM3VFDS3.js +1461 -0
  24. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  25. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  26. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  27. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  28. package/dist/chunk-F6GFZXZ3.js +60 -0
  29. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  30. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  31. package/dist/chunk-HZRNMGLF.js +2233 -0
  32. package/dist/chunk-LF6MLJCY.js +38 -0
  33. package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
  34. package/dist/chunk-LYCDNMOH.js +83 -0
  35. package/dist/chunk-M5POMKKW.js +1051 -0
  36. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  37. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  38. package/dist/chunk-N6LURUEF.js +64 -0
  39. package/dist/chunk-OGFGZQA3.js +23 -0
  40. package/dist/chunk-QDQEUVUF.js +9 -0
  41. package/dist/chunk-QOZKPLVJ.js +226 -0
  42. package/dist/chunk-QY22NZUW.js +1764 -0
  43. package/dist/chunk-RA3O54FC.js +30 -0
  44. package/dist/chunk-RFUPN6KX.js +71 -0
  45. package/dist/chunk-SY4GK4SP.js +55 -0
  46. package/dist/chunk-TBT347UY.js +41 -0
  47. package/dist/chunk-TWNWNS2Q.js +1084 -0
  48. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  49. package/dist/chunk-USO2DKBD.js +421 -0
  50. package/dist/chunk-W4SQCOZU.js +206 -0
  51. package/dist/{chunk-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
  52. package/dist/chunk-WFJ65NUD.js +383 -0
  53. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  54. package/dist/chunk-WO2BXCTQ.js +83 -0
  55. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  56. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  57. package/dist/chunk-Y7KJLXYN.js +232 -0
  58. package/dist/chunk-YFHIIH4B.js +102 -0
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-W62AFRSW.js} +616 -878
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-ZTJONP37.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-44CQVWGT.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-ZLGZYMDK.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-55Y4LHWF.js +36 -0
  87. package/dist/init-XXEKWZQP.js +221 -0
  88. package/dist/init-cmd-WLST2F7G.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-JFIBP4B7.js +43 -0
  95. package/dist/machine-ZBPT2J3R.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-UJXWZRN5.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-4G4A7EVN.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
  110. package/dist/work-cmd-QAUZ7MTD.js +17 -0
  111. package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
  114. package/dist/work-pr-cmd-PV32ZLSI.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +108 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -1,96 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { tailTranscript, enrichSessionRepo, CAPTURE_TIERS, scanAll, setRepoRoot, resolveRepoIdentity, inferArtifactKind, bannedPathsForTier, sessionIdFromPath, setCustomScrubbers, setLocalIdentity, scrubDeep, downconvertSession, isHumanUserTurn, turnCountBound, SOURCE_ADAPTERS, EVENT_TYPES, ArtifactTombstoneRecordSchema, ArtifactSpoolRecordSchema } from './chunk-4TNZMO7V.js';
3
+ import { applyContextProvenance, installScrubContext, prepareSessionForUpload, wholeSessionCounts, sliceTurnIo } from './chunk-LYCDNMOH.js';
4
+ import { LaunchFilter } from './chunk-BCJNHMZT.js';
5
+ import { RepoFilter } from './chunk-TBT347UY.js';
6
+ import { hookExecSpoolPath } from './chunk-F7CU5ABH.js';
7
+ import { applyReportedCost } from './chunk-BKO6JEZI.js';
8
+ import { migrateLegacyArtifactSidecars, planArtifactUpload, artifactsStatePath, sanitizeTerminal, artifactBlobsDir, resolveArtifactTurnIndex, isArtifactTombstoned, ARTIFACT_RETRY_MAX_AGE_MS, writeArtifactSpoolState, reclaimUnneededArtifactBlobs, neededArtifactShas } from './chunk-WFJ65NUD.js';
9
+ import { tailTranscript, enrichSessionRepo, CAPTURE_TIERS, scanAll, setRepoRoot, resolveRepoIdentity, inferArtifactKind, bannedPathsForTier, sessionIdFromPath, SOURCE_ADAPTERS, EVENT_TYPES } from './chunk-4C4THABW.js';
4
10
  import { C } from './chunk-YRYMNBGQ.js';
5
- import { readLaunchSpoolState, launchesPath, launchStatusIsDecisive } from './chunk-65WXAANR.js';
11
+ import { readLaunchSpoolState, launchStatusIsDecisive, launchedItemsBySession, launchesPath } from './chunk-DVDEBNQJ.js';
6
12
  import { readLatestProvenanceForSession, readContextProvenance } from './chunk-ZJSNLVCE.js';
7
- import { writeSyncStatus, spoolDir } from './chunk-NXLLURA4.js';
8
- import { removeJournalHooks } from './chunk-I3ETLSNE.js';
9
- import { hookExecSpoolPath } from './chunk-YOOSBOKS.js';
10
- import { readCloudConfig, cloudConfigPath, writeCloudConfig } from './chunk-TFUIDMOB.js';
13
+ import { defaultFetch } from './chunk-BCLJSQCV.js';
14
+ import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
15
+ import { readCloudConfig, cloudConfigPath, writeCloudConfig } from './chunk-EABW6AJQ.js';
16
+ import { signerFor, signRunnerRequest } from './chunk-M5POMKKW.js';
17
+ import { writeSyncStatus, spoolDir } from './chunk-UFFXNXLQ.js';
18
+ import { removeJournalHooks } from './chunk-XCCLFVXM.js';
19
+ import { readRepoConnection, findGitDirs } from './chunk-BJ3KTMDH.js';
11
20
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
12
21
  import { dirname, basename, join } from 'path';
13
- import { statSync, mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, openSync, fstatSync, readSync, closeSync, writeFileSync, renameSync, unlinkSync, rmSync } from 'fs';
14
- import { execFileSync } from 'child_process';
15
- import { hostname, userInfo } from 'os';
22
+ import { statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, existsSync, rmSync } from 'fs';
16
23
  import { fileURLToPath } from 'url';
17
24
  import { createRequire as createRequire$1 } from 'module';
18
25
 
19
26
  createRequire(import.meta.url);
20
- function detectCi(env = process.env) {
21
- return env.CI === "true" || env.CI === "1" || !!env.JENKINS_URL;
22
- }
23
- function gitGlobalConfig(key) {
24
- try {
25
- return execFileSync("git", ["config", "--global", key], { encoding: "utf8", timeout: 2e3 }).trim() || void 0;
26
- } catch {
27
- return void 0;
28
- }
29
- }
30
- function installScrubContext(home, scrub) {
31
- setCustomScrubbers(scrub);
32
- setLocalIdentity({
33
- name: gitGlobalConfig("user.name"),
34
- email: gitGlobalConfig("user.email"),
35
- username: (() => {
36
- try {
37
- return userInfo().username;
38
- } catch {
39
- return void 0;
40
- }
41
- })(),
42
- hostname: (() => {
43
- try {
44
- return hostname();
45
- } catch {
46
- return void 0;
47
- }
48
- })(),
49
- home
50
- });
51
- }
52
- function applyContextProvenance(session, record) {
53
- if (!record) return;
54
- if (record.repository !== void 0) session.env.contextSourceRepo = record.repository;
55
- if (record.ref !== void 0) session.env.contextSourceRef = record.ref;
56
- if (record.sha !== null) session.env.contextSourceSha = record.sha;
57
- session.env.contextSourceVerification = record.verification;
58
- }
59
- function prepareSessionForUpload(session, tier, envVars = process.env) {
60
- setRepoRoot(resolveRepoIdentity(session.env.cwd).repoRoot);
61
- scrubDeep(session);
62
- session.workerKind ??= detectCi(envVars) ? "ci" : "seat";
63
- return downconvertSession(session, tier);
64
- }
65
- var MAX_TOOL_IO_CHARS = 64e3;
66
- function sliceToolIo(value) {
67
- const text = typeof value === "string" ? value : JSON.stringify(value);
68
- if (typeof text !== "string" || text.length <= MAX_TOOL_IO_CHARS) return value;
69
- return `${text.slice(0, MAX_TOOL_IO_CHARS)}\u2026[sliced: ${text.length} chars exceeded the sync per-call cap]`;
70
- }
71
- function sliceTurnIo(turn) {
72
- return {
73
- ...turn,
74
- toolCalls: (turn.toolCalls ?? []).map((call) => ({
75
- ...call,
76
- ...call.input !== void 0 ? { input: sliceToolIo(call.input) } : {},
77
- ...call.output !== void 0 ? { output: sliceToolIo(call.output) } : {}
78
- }))
79
- };
80
- }
81
- function wholeSessionCounts(turns) {
82
- const allCalls = turns.flatMap((t) => t.toolCalls ?? []);
83
- return {
84
- // An index BOUND (`1 + max(turn.index)`), not the shipped turn count —
85
- // turn indexes are not guaranteed dense from zero, and a late artifact
86
- // anchored at its real turn index must still validate against the
87
- // summary row this produces (see turnCountBound).
88
- turns: turnCountBound(turns),
89
- toolCalls: allCalls.length,
90
- errors: allCalls.filter((c) => c.status === "error").length,
91
- userTurns: turns.filter(isHumanUserTurn).length
92
- };
93
- }
94
27
  var IRRELEVANT = /* @__PURE__ */ new Set(["__tests__", "__mocks__", "node_modules"]);
95
28
  function newestSourceMs(dir) {
96
29
  let newest = 0;
@@ -198,121 +131,6 @@ function readSqliteWatermark(url, appId, home) {
198
131
  return null;
199
132
  }
200
133
  }
201
-
202
- // src/repo-filter.ts
203
- function toMatcher(pattern) {
204
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
205
- return new RegExp(`^${escaped}$`, "i");
206
- }
207
- var RepoFilter = class {
208
- constructor(config) {
209
- /** Sessions dropped, keyed by repo (or "(no repo)") — for the sync summary. */
210
- this.skipped = /* @__PURE__ */ new Map();
211
- this.include = (config?.include ?? []).map(toMatcher);
212
- this.exclude = (config?.exclude ?? []).map(toMatcher);
213
- }
214
- get active() {
215
- return this.include.length > 0 || this.exclude.length > 0;
216
- }
217
- /** True when the session should sync. Records skips for reporting. */
218
- allows(repo) {
219
- const label = repo || "(no repo)";
220
- if (repo && this.exclude.some((m) => m.test(repo))) {
221
- this.skipped.set(label, (this.skipped.get(label) ?? 0) + 1);
222
- return false;
223
- }
224
- if (this.include.length > 0 && !(repo && this.include.some((m) => m.test(repo)))) {
225
- this.skipped.set(label, (this.skipped.get(label) ?? 0) + 1);
226
- return false;
227
- }
228
- return true;
229
- }
230
- get skippedTotal() {
231
- let n = 0;
232
- for (const c of this.skipped.values()) n += c;
233
- return n;
234
- }
235
- };
236
- var SESSION_DETECT_TAIL_BYTES = 64 * 1024;
237
- var SESSION_DETECT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
238
- var readTail = (path, maxBytes) => {
239
- try {
240
- const fd = openSync(path, "r");
241
- try {
242
- const size = fstatSync(fd).size;
243
- const length = Math.min(size, maxBytes);
244
- const data = Buffer.alloc(length);
245
- readSync(fd, data, 0, length, size - length);
246
- return { data, truncated: size > length };
247
- } finally {
248
- closeSync(fd);
249
- }
250
- } catch {
251
- return void 0;
252
- }
253
- };
254
- function detectActiveSession(opts) {
255
- if (!opts.env.CLAUDECODE) return void 0;
256
- const read = opts.readTailImpl ?? readTail;
257
- const tail = read(join(opts.home, ".outerlayer", "spool", "events.jsonl"), SESSION_DETECT_TAIL_BYTES);
258
- if (!tail || tail.data.length === 0) return void 0;
259
- let lines = tail.data.toString("utf8").split("\n");
260
- if (tail.truncated) lines = lines.slice(1);
261
- const records = [];
262
- for (const line of lines) {
263
- const text = line.trim();
264
- if (!text) continue;
265
- try {
266
- records.push(JSON.parse(text));
267
- } catch {
268
- continue;
269
- }
270
- }
271
- const ended = /* @__PURE__ */ new Set();
272
- for (const record of records) {
273
- if (record.event === "SessionEnd" && typeof record.sessionId === "string" && record.sessionId !== "") {
274
- ended.add(record.sessionId);
275
- }
276
- }
277
- let newest;
278
- for (const record of records) {
279
- if (typeof record.sessionId !== "string" || record.sessionId === "") continue;
280
- if (record.event === "SessionEnd" || ended.has(record.sessionId)) continue;
281
- if (typeof record.cwd !== "string" || !opts.cwds.includes(record.cwd)) continue;
282
- if (typeof record.t !== "string") continue;
283
- const tMs = Date.parse(record.t);
284
- if (!Number.isFinite(tMs) || opts.nowMs - tMs > SESSION_DETECT_MAX_AGE_MS) continue;
285
- newest = record.sessionId;
286
- }
287
- return newest;
288
- }
289
-
290
- // src/launch-filter.ts
291
- function launchRefusalForSession(sessionId, home) {
292
- const spool = readLaunchSpoolState(home);
293
- if (spool.ids.has(sessionId)) return void 0;
294
- if (spool.unreadable !== void 0) {
295
- return `the launch spool at ${launchesPath(home)} could not be read (${spool.unreadable}), so this session's launch could not be checked and nothing it produces may leave the machine. Fix the file's permissions and re-run.`;
296
- }
297
- if (spool.malformedLines !== void 0) {
298
- return `the launch spool at ${launchesPath(home)} has ${spool.malformedLines} damaged line(s), so this session's launch could not be checked and nothing it produces may leave the machine.`;
299
- }
300
- return `this session was not launched with OUTERLAYER_WORK naming a work item, so nothing it produces may leave the machine. Start the session with OUTERLAYER_WORK set (for example OUTERLAYER_WORK=42), or run this from CI.`;
301
- }
302
- var LaunchFilter = class {
303
- constructor(launchedSessionIds) {
304
- this.launchedSessionIds = launchedSessionIds;
305
- /** Sessions dropped for carrying no launch record — for the sync summary. */
306
- this.skippedTotal = 0;
307
- }
308
- /** True when the session should sync. Records skips for reporting. */
309
- allows(session) {
310
- const key = session.subagent?.parentSessionId ?? session.id;
311
- const allowed = typeof key === "string" && this.launchedSessionIds.has(key);
312
- if (!allowed) this.skippedTotal += 1;
313
- return allowed;
314
- }
315
- };
316
134
  var HOOK_TIMEOUT_MS = 6e5;
317
135
  var HOOK_INCOMPLETE_SLACK_MS = 6e4;
318
136
  var HOOK_INCOMPLETE_AFTER_MS = HOOK_TIMEOUT_MS + HOOK_INCOMPLETE_SLACK_MS;
@@ -744,8 +562,8 @@ var LiveUploader = class {
744
562
  const priorEntry = state.sessions[key];
745
563
  if (priorEntry?.ended || priorEntry?.failed) return true;
746
564
  const cloudConfig = readCloudConfig(this.home);
747
- if (!cloudConfig.url || !cloudConfig.apiKey || !cloudConfig.appId) return false;
748
565
  if (cloudConfig.autoSync === false) return false;
566
+ if (!cloudConfig.url || !(cloudConfig.apiKey || cloudConfig.accountToken)) return false;
749
567
  const info = subagentInfo(mirrorPath);
750
568
  const tail = tailTranscript(mirrorPath, void 0, {
751
569
  isSubagent: info.isSubagent,
@@ -755,6 +573,8 @@ var LiveUploader = class {
755
573
  if (!tail.session) return false;
756
574
  const session = tail.session;
757
575
  enrichSessionRepo(session);
576
+ const destination = resolveGatewayCredentials({ home: this.home, env: {}, cwd: session.env.cwd ?? "/" });
577
+ if (!destination.ok) return false;
758
578
  const repoFilter = new RepoFilter(cloudConfig.repos);
759
579
  if (!repoFilter.allows(session.env.gitRepo)) return false;
760
580
  const launchSpool = readLaunchSpoolState(this.home);
@@ -771,6 +591,7 @@ var LiveUploader = class {
771
591
  const hookEvents = planHookExecMerge({ home: this.home, fromOffset: 0 }).eventsBySession.get(session.id);
772
592
  if (hookEvents && hookEvents.length > 0) appendHookExecEvents(session, hookEvents);
773
593
  applyContextProvenance(session, readLatestProvenanceForSession(session.id, this.home));
594
+ applyReportedCost(session, this.home);
774
595
  installScrubContext(this.home, cloudConfig.scrub);
775
596
  const prepared = prepareSessionForUpload(session, tier, this.env);
776
597
  const ackedTurnIndex = priorEntry?.ackedTurnIndex ?? -1;
@@ -782,18 +603,19 @@ var LiveUploader = class {
782
603
  const groups = groupTurns(available, open, wholeCounts);
783
604
  const emptyFinalTurn = Math.max(ackedTurnIndex, 0);
784
605
  const toSend = groups.length > 0 ? groups : [{ turns: [], delta: { fromTurn: emptyFinalTurn, toTurn: emptyFinalTurn, open: false, counts: wholeCounts } }];
785
- const endpoint = new URL("/v1/agents/sync", cloudConfig.url).toString();
606
+ const endpoint = new URL("/v1/agents/sync", destination.url).toString();
786
607
  let currentAcked = ackedTurnIndex;
787
608
  for (const group of toSend) {
788
609
  const payload = {
789
610
  schemaVersion: 1,
790
611
  sessions: [{ ...prepared, turns: group.turns, delta: group.delta }]
791
612
  };
792
- const outcome = await this.sendWithRetry(endpoint, cloudConfig.apiKey, cloudConfig.appId, prepared.id, payload);
613
+ const outcome = await this.sendWithRetry(endpoint, destination.credential, destination.appId, prepared.id, payload);
793
614
  if (outcome === "unauthorized") {
794
615
  this.stopped = true;
795
616
  this.stderr(
796
- `live upload stopped: the API key was refused. No session streams from this daemon until it restarts with a valid key.
617
+ destination.credential.startsWith("olu_") ? `live upload stopped: the login was refused (expired or revoked). No session streams from this daemon until you run \`outerlayer login\` and it restarts.
618
+ ` : `live upload stopped: the API key was refused. No session streams from this daemon until it restarts with a valid key.
797
619
  `
798
620
  );
799
621
  return true;
@@ -828,6 +650,20 @@ var LiveUploader = class {
828
650
  const current = readLiveUploadState(this.home);
829
651
  writeLiveUploadState({ v: 1, sessions: { ...current.sessions, [key]: entry } }, this.home);
830
652
  }
653
+ /** The headers that sign a request with this host's key, or none when the host has no readable key. A runner
654
+ * key bound to a host key is refused on every route unless that key signed the request. A login token is
655
+ * never signed: only the saved factory key is bound to a host key. */
656
+ async signatureHeaders(endpoint, body, credential) {
657
+ if (readCloudConfig(this.home).apiKey !== credential) return {};
658
+ let signer;
659
+ try {
660
+ signer = await signerFor(cloudConfigPath(this.home));
661
+ } catch {
662
+ return {};
663
+ }
664
+ if (signer === void 0) return {};
665
+ return { ...await signRunnerRequest({ method: "POST", url: endpoint, body: new TextEncoder().encode(body), signer }) };
666
+ }
831
667
  /** Sends one request, retrying a network error or a retryable 5xx forever
832
668
  * with exponential backoff (2s → 60s cap) — the daemon runs unattended, so
833
669
  * there is no bounded attempt count. A 401 or a permanent 4xx/rejection
@@ -838,9 +674,15 @@ var LiveUploader = class {
838
674
  for (; ; ) {
839
675
  let response;
840
676
  try {
677
+ const signature = await this.signatureHeaders(endpoint, body, apiKey);
841
678
  response = await this.fetchImpl(endpoint, {
842
679
  method: "POST",
843
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}`, "x-outerlayer-app-id": appId },
680
+ headers: {
681
+ "content-type": "application/json",
682
+ authorization: `Bearer ${apiKey}`,
683
+ "x-outerlayer-app-id": appId,
684
+ ...signature
685
+ },
844
686
  body
845
687
  });
846
688
  } catch {
@@ -860,379 +702,6 @@ var LiveUploader = class {
860
702
  }
861
703
  }
862
704
  };
863
- function artifactsSpoolPath(home = homeFromEnv()) {
864
- return join(home, ".outerlayer", "spool", "artifacts.jsonl");
865
- }
866
- function artifactBlobsDir(home = homeFromEnv()) {
867
- return join(home, ".outerlayer", "spool", "artifact-blobs");
868
- }
869
- function artifactsStatePath(home = homeFromEnv()) {
870
- return join(home, ".outerlayer", "spool", "artifacts.state.json");
871
- }
872
- var ARTIFACT_RETRY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
873
- function appendArtifactRecord(record, home = homeFromEnv()) {
874
- const path = artifactsSpoolPath(home);
875
- mkdirSync(dirname(path), { recursive: true });
876
- appendFileSync(path, JSON.stringify(record) + "\n");
877
- }
878
- function appendArtifactTombstone(artifactId, home = homeFromEnv(), now = () => /* @__PURE__ */ new Date()) {
879
- const path = artifactsSpoolPath(home);
880
- mkdirSync(dirname(path), { recursive: true });
881
- const record = { rec: "artifact-tombstone", artifactId, t: now().toISOString() };
882
- appendFileSync(path, JSON.stringify(record) + "\n");
883
- }
884
- function walkArtifactSpoolLines(home, visit) {
885
- let buf;
886
- try {
887
- buf = readFileSync(artifactsSpoolPath(home));
888
- } catch {
889
- return;
890
- }
891
- let pos = 0;
892
- while (pos < buf.length) {
893
- const nl = buf.indexOf(10, pos);
894
- const textEnd = nl === -1 ? buf.length : nl;
895
- const text = buf.subarray(pos, textEnd).toString("utf8").trim();
896
- if (text) visit(text, pos);
897
- if (nl === -1) break;
898
- pos = nl + 1;
899
- }
900
- }
901
- function isArtifactTombstoned(artifactId, home = homeFromEnv()) {
902
- let found = false;
903
- walkArtifactSpoolLines(home, (text) => {
904
- if (found) return;
905
- try {
906
- const parsed = ArtifactTombstoneRecordSchema.safeParse(JSON.parse(text));
907
- if (parsed.success && parsed.data.artifactId === artifactId) found = true;
908
- } catch {
909
- }
910
- });
911
- return found;
912
- }
913
- var KNOWN_SPOOL_RECORD_TYPES = /* @__PURE__ */ new Set(["artifact", "artifact-tombstone"]);
914
- var SHA256_HEX = /^[0-9a-f]{64}$/;
915
- var MAX_DISPLAYED_REC_TYPE_LENGTH = 100;
916
- var CONTROL_CHAR_PATTERN = new RegExp(
917
- `[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
918
- "g"
919
- );
920
- function sanitizeTerminal(text) {
921
- return text.replace(CONTROL_CHAR_PATTERN, "");
922
- }
923
- function sanitizeRecType(rec) {
924
- const stripped = sanitizeTerminal(rec);
925
- return stripped.length > MAX_DISPLAYED_REC_TYPE_LENGTH ? `${stripped.slice(0, MAX_DISPLAYED_REC_TYPE_LENGTH)}\u2026` : stripped;
926
- }
927
- function readUnknownRecordEnvelope(json) {
928
- if (typeof json !== "object" || json === null) return void 0;
929
- const rec = json.rec;
930
- const t = json.t;
931
- if (typeof rec !== "string" || typeof t !== "string") return void 0;
932
- const shaRaw = json.sha256;
933
- const sha256 = typeof shaRaw === "string" && SHA256_HEX.test(shaRaw) ? shaRaw : void 0;
934
- return { rec, t, sha256 };
935
- }
936
- function classifySpoolLine(text) {
937
- let json;
938
- try {
939
- json = JSON.parse(text);
940
- } catch {
941
- return { kind: "skipped", reason: "not valid JSON" };
942
- }
943
- const tombstone = ArtifactTombstoneRecordSchema.safeParse(json);
944
- if (tombstone.success) return { kind: "tombstone", artifactId: tombstone.data.artifactId };
945
- const parsed = ArtifactSpoolRecordSchema.safeParse(json);
946
- if (parsed.success) return { kind: "record", record: parsed.data };
947
- const envelope = readUnknownRecordEnvelope(json);
948
- if (envelope !== void 0 && !KNOWN_SPOOL_RECORD_TYPES.has(envelope.rec)) {
949
- const sanitizedRecType = sanitizeRecType(envelope.rec);
950
- const recType = sanitizedRecType === "" ? "(empty)" : `"${sanitizedRecType}"`;
951
- return {
952
- kind: "held",
953
- reason: `unrecognized record type ${recType} \u2014 this build's schemas do not recognize it`,
954
- ...envelope.sha256 !== void 0 ? { sha256: envelope.sha256 } : {}
955
- };
956
- }
957
- const issue = parsed.error.issues[0];
958
- const issueDetail = issue ? ` (${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${sanitizeTerminal(issue.message)})` : "";
959
- return { kind: "skipped", reason: `does not match the artifact spool record shape${issueDetail}` };
960
- }
961
- function legacySidecarPaths(home) {
962
- const dir = join(home, ".outerlayer", "spool");
963
- return [
964
- join(dir, "artifacts.watermark"),
965
- join(dir, "artifacts.uploaded.json"),
966
- join(dir, "artifacts.dropped.json"),
967
- join(dir, "artifacts.held-offsets.json"),
968
- join(dir, "artifacts.pending-first-seen.json")
969
- ];
970
- }
971
- function readLegacySidecars(home) {
972
- const dir = join(home, ".outerlayer", "spool");
973
- let watermark = 0;
974
- try {
975
- const n = parseInt(readFileSync(join(dir, "artifacts.watermark"), "utf8").trim(), 10);
976
- if (Number.isFinite(n) && n >= 0) watermark = n;
977
- } catch {
978
- }
979
- const readIdSet = (name) => {
980
- try {
981
- const raw = JSON.parse(readFileSync(join(dir, name), "utf8"));
982
- return new Set(Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : []);
983
- } catch {
984
- return /* @__PURE__ */ new Set();
985
- }
986
- };
987
- let pendingFirstSeen = {};
988
- try {
989
- const raw = JSON.parse(readFileSync(join(dir, "artifacts.pending-first-seen.json"), "utf8"));
990
- if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
991
- const out = {};
992
- for (const [key, value] of Object.entries(raw)) {
993
- if (typeof value === "string") out[key] = value;
994
- }
995
- pendingFirstSeen = out;
996
- }
997
- } catch {
998
- }
999
- return {
1000
- watermark,
1001
- uploadedIds: readIdSet("artifacts.uploaded.json"),
1002
- droppedIds: readIdSet("artifacts.dropped.json"),
1003
- pendingFirstSeen
1004
- };
1005
- }
1006
- function computeMigratedState(home) {
1007
- const legacy = readLegacySidecars(home);
1008
- const state = /* @__PURE__ */ new Map();
1009
- const tombstoned = /* @__PURE__ */ new Set();
1010
- const rawRecords = [];
1011
- walkArtifactSpoolLines(home, (text, offset) => {
1012
- const classified = classifySpoolLine(text);
1013
- if (classified.kind === "tombstone") {
1014
- tombstoned.add(classified.artifactId);
1015
- } else if (classified.kind === "record") {
1016
- rawRecords.push({
1017
- artifactId: classified.record.artifactId,
1018
- offset,
1019
- sha256: classified.record.sha256,
1020
- t: classified.record.t
1021
- });
1022
- }
1023
- });
1024
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1025
- for (const { artifactId, offset, sha256, t } of rawRecords) {
1026
- if (offset >= legacy.watermark) continue;
1027
- if (legacy.uploadedIds.has(artifactId)) {
1028
- state.set(artifactId, { state: "uploaded", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
1029
- continue;
1030
- }
1031
- if (legacy.droppedIds.has(artifactId)) {
1032
- state.set(artifactId, {
1033
- state: "dropped",
1034
- attempts: 1,
1035
- firstSeen: legacy.pendingFirstSeen[artifactId] ?? t ?? nowIso,
1036
- sha256,
1037
- reachedServer: false
1038
- });
1039
- continue;
1040
- }
1041
- if (tombstoned.has(artifactId)) continue;
1042
- state.set(artifactId, { state: "dropped", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
1043
- }
1044
- return state;
1045
- }
1046
- function readPersistedArtifactSpoolState(home) {
1047
- let raw;
1048
- try {
1049
- raw = readFileSync(artifactsStatePath(home), "utf8");
1050
- } catch {
1051
- return { state: /* @__PURE__ */ new Map(), corrupt: false };
1052
- }
1053
- try {
1054
- const parsed = JSON.parse(raw);
1055
- const records = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.records) ? parsed.records : void 0;
1056
- if (records === void 0) return { state: /* @__PURE__ */ new Map(), corrupt: true };
1057
- const state = /* @__PURE__ */ new Map();
1058
- for (const entry of records) {
1059
- if (entry === null || typeof entry !== "object") continue;
1060
- const e = entry;
1061
- if (typeof e.artifactId !== "string") continue;
1062
- if (e.state !== "pending" && e.state !== "uploaded" && e.state !== "held" && e.state !== "dropped") continue;
1063
- state.set(e.artifactId, {
1064
- state: e.state,
1065
- attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? e.attempts : 0,
1066
- firstSeen: typeof e.firstSeen === "string" ? e.firstSeen : (/* @__PURE__ */ new Date(0)).toISOString(),
1067
- sha256: typeof e.sha256 === "string" ? e.sha256 : "",
1068
- reachedServer: e.reachedServer === true
1069
- });
1070
- }
1071
- return { state, corrupt: false };
1072
- } catch {
1073
- return { state: /* @__PURE__ */ new Map(), corrupt: true };
1074
- }
1075
- }
1076
- var ARTIFACT_STATE_RESOLVED_CAP = 5e3;
1077
- function writeArtifactSpoolState(state, home = homeFromEnv()) {
1078
- try {
1079
- const path = artifactsStatePath(home);
1080
- mkdirSync(dirname(path), { recursive: true });
1081
- const tmp = `${path}.tmp`;
1082
- const pending = [];
1083
- const resolved = [];
1084
- for (const pair of state.entries()) {
1085
- (pair[1].state === "uploaded" || pair[1].state === "dropped" ? resolved : pending).push(pair);
1086
- }
1087
- const cappedResolved = resolved.length > ARTIFACT_STATE_RESOLVED_CAP ? resolved.slice(-ARTIFACT_STATE_RESOLVED_CAP) : resolved;
1088
- const records = [...pending, ...cappedResolved].map(([artifactId, entry]) => ({ artifactId, ...entry }));
1089
- writeFileSync(tmp, JSON.stringify({ version: 1, records }));
1090
- renameSync(tmp, path);
1091
- return true;
1092
- } catch {
1093
- return false;
1094
- }
1095
- }
1096
- function migrateLegacyArtifactSidecars(home = homeFromEnv()) {
1097
- if (existsSync(artifactsStatePath(home))) return;
1098
- const paths = legacySidecarPaths(home);
1099
- if (!paths.some((p) => existsSync(p))) return;
1100
- writeArtifactSpoolState(computeMigratedState(home), home);
1101
- for (const p of paths) {
1102
- try {
1103
- unlinkSync(p);
1104
- } catch {
1105
- }
1106
- }
1107
- }
1108
- function readEffectiveArtifactSpoolState(home = homeFromEnv()) {
1109
- const { state: persisted, corrupt } = existsSync(artifactsStatePath(home)) ? readPersistedArtifactSpoolState(home) : { state: computeMigratedState(home), corrupt: false };
1110
- const state = new Map(persisted);
1111
- const tombstonedIds = /* @__PURE__ */ new Set();
1112
- walkArtifactSpoolLines(home, (text) => {
1113
- const classified = classifySpoolLine(text);
1114
- if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
1115
- });
1116
- if (tombstonedIds.size > 0) {
1117
- walkArtifactSpoolLines(home, (text) => {
1118
- const classified = classifySpoolLine(text);
1119
- if (classified.kind !== "record") return;
1120
- const { record } = classified;
1121
- if (!tombstonedIds.has(record.artifactId)) return;
1122
- const existing = state.get(record.artifactId);
1123
- if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
1124
- state.set(record.artifactId, {
1125
- state: "dropped",
1126
- attempts: existing?.attempts ?? 0,
1127
- firstSeen: existing?.firstSeen ?? record.t,
1128
- sha256: record.sha256,
1129
- reachedServer: false
1130
- });
1131
- }
1132
- });
1133
- }
1134
- return { state, corrupt };
1135
- }
1136
- function planArtifactUpload(opts = {}) {
1137
- const home = opts.home ?? homeFromEnv();
1138
- const { state: effective, corrupt } = readEffectiveArtifactSpoolState(home);
1139
- const state = new Map(effective);
1140
- const candidates = [];
1141
- const skipped = [];
1142
- const held = [];
1143
- const tombstonedIds = /* @__PURE__ */ new Set();
1144
- walkArtifactSpoolLines(home, (text) => {
1145
- const classified = classifySpoolLine(text);
1146
- if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
1147
- });
1148
- walkArtifactSpoolLines(home, (text, offset) => {
1149
- const classified = classifySpoolLine(text);
1150
- if (classified.kind === "tombstone") return;
1151
- if (classified.kind === "skipped") {
1152
- skipped.push({ offset, reason: classified.reason });
1153
- return;
1154
- }
1155
- if (classified.kind === "held") {
1156
- held.push({ offset, reason: classified.reason, ...classified.sha256 !== void 0 ? { sha256: classified.sha256 } : {} });
1157
- return;
1158
- }
1159
- const { record } = classified;
1160
- if (tombstonedIds.has(record.artifactId)) return;
1161
- const existing = state.get(record.artifactId);
1162
- if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
1163
- candidates.push({ record });
1164
- }
1165
- });
1166
- return { candidates, state, skipped, held, corrupt };
1167
- }
1168
- function neededArtifactShas(home, finalState) {
1169
- const needed = /* @__PURE__ */ new Set();
1170
- walkArtifactSpoolLines(home, (text) => {
1171
- const classified = classifySpoolLine(text);
1172
- if (classified.kind === "held") {
1173
- if (classified.sha256 !== void 0) needed.add(classified.sha256);
1174
- return;
1175
- }
1176
- if (classified.kind !== "record") return;
1177
- const entry = finalState.get(classified.record.artifactId);
1178
- if (entry === void 0 || entry.state === "held" || entry.state === "pending") needed.add(classified.record.sha256);
1179
- });
1180
- return needed;
1181
- }
1182
- function reclaimUnneededArtifactBlobs(home, neededShas) {
1183
- let names;
1184
- try {
1185
- names = readdirSync(artifactBlobsDir(home));
1186
- } catch {
1187
- return 0;
1188
- }
1189
- let reclaimed = 0;
1190
- for (const name of names) {
1191
- if (!SHA256_HEX.test(name) || neededShas.has(name)) continue;
1192
- try {
1193
- unlinkSync(join(artifactBlobsDir(home), name));
1194
- reclaimed += 1;
1195
- } catch {
1196
- }
1197
- }
1198
- return reclaimed;
1199
- }
1200
- function localArtifactIds(home = homeFromEnv()) {
1201
- const ids = /* @__PURE__ */ new Set();
1202
- walkArtifactSpoolLines(home, (text) => {
1203
- const classified = classifySpoolLine(text);
1204
- if (classified.kind === "record") ids.add(classified.record.artifactId);
1205
- });
1206
- return ids;
1207
- }
1208
- function findPendingArtifactRecord(artifactId, home = homeFromEnv()) {
1209
- let found;
1210
- walkArtifactSpoolLines(home, (text) => {
1211
- if (found !== void 0) return;
1212
- const classified = classifySpoolLine(text);
1213
- if (classified.kind === "record" && classified.record.artifactId === artifactId) found = classified.record;
1214
- });
1215
- if (found === void 0) return void 0;
1216
- const { state } = readEffectiveArtifactSpoolState(home);
1217
- const entry = state.get(artifactId);
1218
- if (entry !== void 0 && entry.state !== "held" && entry.state !== "pending") return void 0;
1219
- return { record: found };
1220
- }
1221
- function resolveArtifactTurnIndex(turns, filename) {
1222
- let found;
1223
- for (const turn of turns) {
1224
- for (const call of turn.toolCalls ?? []) {
1225
- const haystack = [
1226
- call.name,
1227
- typeof call.input === "string" ? call.input : call.input === void 0 ? "" : JSON.stringify(call.input),
1228
- typeof call.output === "string" ? call.output : call.output === void 0 ? "" : JSON.stringify(call.output),
1229
- call.file ?? ""
1230
- ].join("\n");
1231
- if (haystack.includes("emit artifact") && haystack.includes(filename)) found = turn.index;
1232
- }
1233
- }
1234
- return found;
1235
- }
1236
705
 
1237
706
  // src/skip-verdict.ts
1238
707
  var RESUME_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
@@ -1246,41 +715,12 @@ function statusUnknown(verdict) {
1246
715
  function mightStillShip(verdict) {
1247
716
  return verdict !== "final";
1248
717
  }
1249
- var MAX_SESSION_REPOS = 100;
1250
- function sessionReposPath(home = homeFromEnv()) {
1251
- return join(home, ".outerlayer", "spool", "session-repos.json");
1252
- }
1253
- function readSessionRepos(home = homeFromEnv()) {
1254
- const path = sessionReposPath(home);
1255
- if (!existsSync(path)) return [];
1256
- try {
1257
- const parsed = JSON.parse(readFileSync(path, "utf8"));
1258
- if (!Array.isArray(parsed)) return [];
1259
- return parsed.filter(
1260
- (r) => typeof r === "object" && r !== null && typeof r.repoRoot === "string" && typeof r.gitRepo === "string" && typeof r.lastSeenAt === "string"
1261
- );
1262
- } catch {
1263
- return [];
1264
- }
1265
- }
1266
- function recordSessionRepos(observed, now = /* @__PURE__ */ new Date(), home = homeFromEnv()) {
1267
- if (observed.length === 0) return;
1268
- const byRoot = new Map(readSessionRepos(home).map((r) => [r.repoRoot, r]));
1269
- const at = now.toISOString();
1270
- for (const { repoRoot, gitRepo } of observed) {
1271
- if (!repoRoot) continue;
1272
- byRoot.set(repoRoot, { repoRoot, gitRepo, lastSeenAt: at });
1273
- }
1274
- const rows = [...byRoot.values()].sort((a, b) => a.lastSeenAt < b.lastSeenAt ? 1 : a.lastSeenAt > b.lastSeenAt ? -1 : 0).slice(0, MAX_SESSION_REPOS);
1275
- try {
1276
- const path = sessionReposPath(home);
1277
- mkdirSync(dirname(path), { recursive: true });
1278
- writeFileSync(path, JSON.stringify(rows, null, 2) + "\n");
1279
- } catch {
1280
- }
1281
- }
1282
718
 
1283
719
  // src/sync-cmd.ts
720
+ var ITEM_KEY_PREFIX = "olitem_";
721
+ function isItemKey(token) {
722
+ return token.startsWith(ITEM_KEY_PREFIX);
723
+ }
1284
724
  function isLoopbackUrl(url) {
1285
725
  try {
1286
726
  const host = new URL(url).hostname.toLowerCase().replace(/^\[|\]$/g, "");
@@ -1346,14 +786,16 @@ var KNOWN_PERMANENT_ARTIFACT_ERROR_CODES = /* @__PURE__ */ new Set([
1346
786
  "unknown_replaces_target",
1347
787
  "nothing_to_attach",
1348
788
  "artifact_content_conflict",
1349
- "payload_too_large"
789
+ "payload_too_large",
790
+ "unreadable_test_results"
1350
791
  ]);
1351
792
  var NEVER_REACHED_SERVER_ERROR_CODES = /* @__PURE__ */ new Set([
1352
793
  "invalid_request_body",
1353
794
  "invalid_field_value",
1354
795
  "session_not_found",
1355
796
  "nothing_to_attach",
1356
- "payload_too_large"
797
+ "payload_too_large",
798
+ "unreadable_test_results"
1357
799
  ]);
1358
800
  function splitPayload(payload) {
1359
801
  const mid = Math.ceil(payload.sessions.length / 2);
@@ -1464,21 +906,26 @@ async function runSync(opts = {}) {
1464
906
  throw err;
1465
907
  }
1466
908
  }
909
+ function parseWorkItemNumber(raw) {
910
+ const match = /^\d+$/.exec(raw ?? "");
911
+ return match ? Number(match[0]) : void 0;
912
+ }
1467
913
  async function runSyncInner(opts, stats) {
1468
914
  const envVars = opts.env ?? process.env;
1469
915
  const home = opts.home ?? homeFromEnv(envVars);
1470
916
  const fileConfig = readCloudConfig(home);
1471
- const url = opts.url ?? envVars.OUTERLAYER_URL ?? fileConfig.url;
1472
- const apiKey = opts.apiKey ?? envVars.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
1473
- const appId = opts.appId ?? envVars.OUTERLAYER_APP_ID ?? fileConfig.appId;
1474
- stats.url = url;
1475
- stats.appId = appId;
917
+ const resolved = resolveGatewayCredentials({ ...opts, home, env: envVars });
918
+ stats.url = resolved.url;
919
+ stats.appId = resolved.appId;
1476
920
  const tier = resolveTier(opts.tier ?? envVars.OUTERLAYER_TIER ?? fileConfig.tier);
1477
921
  const dryRun = opts.dryRun ?? false;
1478
922
  const repoFilter = new RepoFilter(fileConfig.repos);
1479
923
  const launchSpool = readLaunchSpoolState(home);
1480
924
  const launchedSessionIds = launchSpool.ids;
1481
925
  const launchFilter = new LaunchFilter(launchedSessionIds);
926
+ const itemScope = resolved.credential !== void 0 && isItemKey(resolved.credential) ? { item: parseWorkItemNumber(envVars.OUTERLAYER_WORK), sessionItems: launchedItemsBySession(home) } : void 0;
927
+ const inItemScope = (sessionId) => itemScope === void 0 || itemScope.item !== void 0 && itemScope.sessionItems.get(sessionId) === itemScope.item;
928
+ const connectionScoped = resolved.kind === "login" && opts.appId === void 0 && !envVars.OUTERLAYER_APP_ID;
1482
929
  const runNowMs = Date.now();
1483
930
  const launchSkipVerdict = (mtimeMs) => unlaunchedVerdict({ launchStatusDecisive: launchStatusIsDecisive(launchSpool), idleMs: runNowMs - mtimeMs });
1484
931
  const launchSkipReason = () => {
@@ -1489,7 +936,9 @@ async function runSyncInner(opts, stats) {
1489
936
  return "not launched with OUTERLAYER_WORK";
1490
937
  };
1491
938
  const artifactHoldReason = (sessionId) => {
1492
- if (launchedSessionIds.has(sessionId)) return void 0;
939
+ if (launchedSessionIds.has(sessionId)) {
940
+ return inItemScope(sessionId) ? void 0 : "its session was launched for another work item";
941
+ }
1493
942
  if (launchSpool.unreadable !== void 0) {
1494
943
  return "the launch spool could not be read this run, so no artifact may leave the machine";
1495
944
  }
@@ -1515,12 +964,8 @@ async function runSyncInner(opts, stats) {
1515
964
  );
1516
965
  }
1517
966
  installScrubContext(home, fileConfig.scrub);
1518
- if (!url || !apiKey || !appId) {
1519
- const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
1520
- throw new SyncConfigError(
1521
- `missing ${missing}. Run \`outerlayer login --url <your gateway>\` and paste the key at the prompt, or set OUTERLAYER_URL / OUTERLAYER_API_KEY / OUTERLAYER_APP_ID. Mint a key in the dashboard: Settings \u2192 API keys.`
1522
- );
1523
- }
967
+ if (!resolved.ok) throw new SyncConfigError(resolved.message);
968
+ const { url, credential: apiKey, appId } = resolved;
1524
969
  {
1525
970
  const sinceMs = opts.all ? 0 : readWatermark(url, appId, home);
1526
971
  const hookPlan = planHookExecMerge({ home });
@@ -1559,7 +1004,7 @@ async function runSyncInner(opts, stats) {
1559
1004
  }
1560
1005
  const liveUploads = readLiveUploadState(home).sessions;
1561
1006
  const collected = [];
1562
- const observedRepos = /* @__PURE__ */ new Map();
1007
+ const observedRepos = /* @__PURE__ */ new Set();
1563
1008
  let heldMinMtimeMs = Number.POSITIVE_INFINITY;
1564
1009
  let passableMaxMtimeMs = 0;
1565
1010
  const neverShipSessionIds = /* @__PURE__ */ new Set();
@@ -1588,11 +1033,23 @@ async function runSyncInner(opts, stats) {
1588
1033
  noteSkipped(session.id, entry.mtimeMs, launchSkipVerdict(entry.mtimeMs));
1589
1034
  return;
1590
1035
  }
1036
+ if (connectionScoped) {
1037
+ const sessionFactory = readRepoConnection(session.env.cwd ?? "/")?.appId;
1038
+ if (sessionFactory !== appId) {
1039
+ const cwd = session.env.cwd ?? "/";
1040
+ const connectable = sessionFactory === void 0 && findGitDirs(cwd) !== void 0;
1041
+ noteSkipped(session.id, entry.mtimeMs, connectable ? "unknown" : "reversible");
1042
+ return;
1043
+ }
1044
+ }
1045
+ const scopeKey = session.subagent?.parentSessionId ?? session.id;
1046
+ if (!inItemScope(scopeKey)) {
1047
+ noteSkipped(session.id, entry.mtimeMs, "unknown");
1048
+ return;
1049
+ }
1591
1050
  {
1592
1051
  const identity = resolveRepoIdentity(session.env.cwd);
1593
- if (identity.repoRoot) {
1594
- observedRepos.set(identity.repoRoot, identity.gitRepo ?? "");
1595
- }
1052
+ if (identity.repoRoot) observedRepos.add(identity.repoRoot);
1596
1053
  }
1597
1054
  const hookEvents = hookPlan.eventsBySession.get(session.id);
1598
1055
  if (hookEvents && hookEvents.length > 0) {
@@ -1600,6 +1057,7 @@ async function runSyncInner(opts, stats) {
1600
1057
  mergedHookEndKeys.set(session.id, session.subagent?.parentSessionId ?? session.id);
1601
1058
  }
1602
1059
  applyContextProvenance(session, contextSourceBySessionId.get(session.id));
1060
+ applyReportedCost(session, home);
1603
1061
  const gated = prepareSessionForUpload(session, tier, envVars);
1604
1062
  const liveEntry = liveUploads[gated.id];
1605
1063
  const daemonIsStreaming = liveEntry !== void 0 && !liveEntry.ended && !liveEntry.failed;
@@ -1651,8 +1109,7 @@ async function runSyncInner(opts, stats) {
1651
1109
  if (launchStatusIsDecisive(launchSpool)) stats.skippedNotLaunched = launchFilter.skippedTotal;
1652
1110
  else stats.held = launchFilter.skippedTotal;
1653
1111
  if (!opts.dryRun) {
1654
- recordSessionRepos([...observedRepos].map(([repoRoot, gitRepo]) => ({ repoRoot, gitRepo })), /* @__PURE__ */ new Date(), home);
1655
- for (const repoRoot of observedRepos.keys()) removeJournalHooks(repoRoot);
1112
+ for (const repoRoot of observedRepos) removeJournalHooks(repoRoot);
1656
1113
  }
1657
1114
  collected.sort((a, b) => a.mtimeMs - b.mtimeMs);
1658
1115
  const items = collected.flatMap((item) => chunkOversizedSession(item));
@@ -1814,7 +1271,7 @@ async function runSyncInner(opts, stats) {
1814
1271
  output: output2
1815
1272
  };
1816
1273
  }
1817
- const fetchImpl = opts.fetchImpl ?? fetch;
1274
+ const fetchImpl = opts.fetchImpl ?? defaultFetch;
1818
1275
  const endpoint = new URL("/v1/agents/sync", url).toString();
1819
1276
  let synced = 0;
1820
1277
  let blobsSent = 0;
@@ -1896,7 +1353,7 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} hit ${cause
1896
1353
  if (response.status === 401) {
1897
1354
  failureContext();
1898
1355
  throw new SyncTransportError(
1899
- "not authorized \u2014 the API key is unknown, expired, or bound to a different app. Mint a fresh key: dashboard \u2192 Settings \u2192 API keys.",
1356
+ unauthorizedMessage(apiKey),
1900
1357
  401
1901
1358
  );
1902
1359
  }
@@ -2038,6 +1495,7 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
2038
1495
  ...record.commitSha !== void 0 ? { commitSha: record.commitSha } : {},
2039
1496
  ...replaces !== void 0 ? { replaces } : {},
2040
1497
  ...record.replacesTargets !== void 0 ? { replacesTargets: record.replacesTargets } : {},
1498
+ ...record.tests !== void 0 ? { tests: record.tests } : {},
2041
1499
  session: { sessionId: record.sessionId, ...turnIndex !== void 0 ? { turnIndex } : {} }
2042
1500
  },
2043
1501
  blob: { data: blobData.toString("base64") }
@@ -2082,7 +1540,7 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
2082
1540
  }
2083
1541
  if (status === 401) {
2084
1542
  unauthorized = new SyncTransportError(
2085
- "not authorized \u2014 the API key is unknown, expired, or bound to a different app. Mint a fresh key: dashboard \u2192 Settings \u2192 API keys.",
1543
+ unauthorizedMessage(apiKey),
2086
1544
  401
2087
1545
  );
2088
1546
  heldCount += 1;
@@ -2163,7 +1621,10 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
2163
1621
  );
2164
1622
  }
2165
1623
  } else {
2166
- writeCloudConfig({ url, apiKey, appId }, home);
1624
+ writeCloudConfig(
1625
+ { url, apiKey: resolved.kind === "key" ? apiKey : void 0, appId: resolved.kind === "key" ? appId : void 0 },
1626
+ home
1627
+ );
2167
1628
  }
2168
1629
  }
2169
1630
  let output;
@@ -2222,4 +1683,4 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
2222
1683
  }
2223
1684
  }
2224
1685
 
2225
- export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES, RepoFilter, SyncConfigError, SyncTransportError, appendArtifactRecord, appendArtifactTombstone, artifactBlobsDir, artifactsSpoolPath, detectActiveSession, detectCi, findPendingArtifactRecord, isArtifactTombstoned, isLoopbackUrl, launchRefusalForSession, localArtifactIds, readEffectiveArtifactSpoolState, resolveTier, runSync, staleBuildRefusal, staleBuildWarning };
1686
+ export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES, SyncConfigError, SyncTransportError, isLoopbackUrl, resolveTier, runSync, staleBuildRefusal, staleBuildWarning };