@outerlayer/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +226 -59
- package/dist/agent-setup-IFH3FB5X.js +197 -0
- package/dist/build-TZUUTRGH.js +9 -0
- package/dist/build-info.json +1 -1
- package/dist/check-WQP2CZDW.js +94 -0
- package/dist/chunk-2E54Z3P5.js +232 -0
- package/dist/chunk-3TT7RQYB.js +55 -0
- package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
- package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
- package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
- package/dist/chunk-5Y3QQRUH.js +300 -0
- package/dist/chunk-6Y63SXFO.js +383 -0
- package/dist/chunk-774HEQL6.js +178 -0
- package/dist/chunk-7A6RVXPZ.js +2233 -0
- package/dist/chunk-7YIOOFVJ.js +71 -0
- package/dist/chunk-ABIWDUMS.js +104 -0
- package/dist/chunk-AUFG23AA.js +534 -0
- package/dist/chunk-B2G7JHHB.js +42 -0
- package/dist/chunk-BCJNHMZT.js +34 -0
- package/dist/chunk-BCLJSQCV.js +56 -0
- package/dist/chunk-BFESLKPP.js +61 -0
- package/dist/chunk-BJ3KTMDH.js +63 -0
- package/dist/chunk-BKO6JEZI.js +47 -0
- package/dist/chunk-BOLTI6LR.js +37 -0
- package/dist/chunk-CBPPJSAR.js +89 -0
- package/dist/chunk-CBZB6XY6.js +102 -0
- package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
- package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
- package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
- package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
- package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
- package/dist/chunk-F6GFZXZ3.js +60 -0
- package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
- package/dist/chunk-FAJSS2WN.js +1357 -0
- package/dist/chunk-FS2VYGZT.js +1764 -0
- package/dist/chunk-FV2CCGXK.js +9 -0
- package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
- package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
- package/dist/chunk-HE2D4EY5.js +83 -0
- package/dist/chunk-L2DBRKRA.js +94 -0
- package/dist/chunk-LF6MLJCY.js +38 -0
- package/dist/chunk-M5POMKKW.js +1051 -0
- package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
- package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
- package/dist/chunk-N6LURUEF.js +64 -0
- package/dist/chunk-NYC54VBD.js +1084 -0
- package/dist/chunk-OGFGZQA3.js +23 -0
- package/dist/chunk-QOZKPLVJ.js +226 -0
- package/dist/chunk-RA3O54FC.js +30 -0
- package/dist/chunk-TBT347UY.js +41 -0
- package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
- package/dist/chunk-USO2DKBD.js +421 -0
- package/dist/chunk-W4SQCOZU.js +206 -0
- package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
- package/dist/chunk-WO2BXCTQ.js +83 -0
- package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
- package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
- package/dist/chunk-YYYXRJUW.js +101 -0
- package/dist/chunk-ZM2IMMYN.js +76 -0
- package/dist/chunk-ZMYPLFG3.js +71 -0
- package/dist/chunk-ZNA27WEV.js +470 -0
- package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
- package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
- package/dist/config-XVJYZ4IQ.js +9 -0
- package/dist/connect-cmd-OOG4TPU7.js +16 -0
- package/dist/context-adopt-UMA4O5NY.js +105 -0
- package/dist/context-materialize-G2FUFC72.js +19 -0
- package/dist/dist-RW4UPPC2.js +6 -0
- package/dist/docker-NEGDLU6D.js +7 -0
- package/dist/doctor-53F2PYY7.js +43 -0
- package/dist/doctor-UFVL4PZY.js +426 -0
- package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
- package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
- package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
- package/dist/exec-client-4XGXV3XN.js +8 -0
- package/dist/guest-init-GENRHNP7.js +8 -0
- package/dist/hook-fast-EYENPK6F.js +9 -0
- package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
- package/dist/host-key-KDYZ5ECC.js +7 -0
- package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
- package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/init-DXBCOAJK.js +36 -0
- package/dist/init-YEXK35CF.js +221 -0
- package/dist/init-cmd-NHR7PRSQ.js +135 -0
- package/dist/install-cmd-7WXOIFDO.js +55 -0
- package/dist/lima-XN65D7GN.js +55 -0
- package/dist/login-browser-7XGSV7MA.js +10 -0
- package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
- package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
- package/dist/loop-VZOLL4WQ.js +43 -0
- package/dist/machine-WSG52J75.js +35 -0
- package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
- package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
- package/dist/paths-OYKMVYJP.js +6 -0
- package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
- package/dist/real-deps-SU24ZA2K.js +21 -0
- package/dist/relay-L76HDX72.js +46 -0
- package/dist/settings-J2652U5N.js +7 -0
- package/dist/starter-pack-LIZYMKYQ.js +10 -0
- package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
- package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
- package/dist/sync-cmd-VPPYCNNM.js +28 -0
- package/dist/version-BWM6VLDI.js +6 -0
- package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
- package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
- package/dist/work-cmd-UOQO2AD4.js +17 -0
- package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
- package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
- package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
- package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
- package/package.json +13 -3
- package/skill-pack/maintained/amend/SKILL.md +104 -0
- package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
- package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
- package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
- package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
- package/skill-pack/template/build/SKILL.md +193 -0
- package/skill-pack/template/build/references/agent-briefs.md +243 -0
- package/skill-pack/template/build/references/criteria-judge.md +91 -0
- package/skill-pack/template/build/references/evidence.md +42 -0
- package/skill-pack/template/build/references/release.md +74 -0
- package/skill-pack/template/build/references/review-briefs.md +275 -0
- package/skill-pack/template/build/references/review-loop.md +158 -0
- package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
- package/skill-pack/template/spec/SKILL.md +84 -0
- package/skill-pack/template/writing-specs/SKILL.md +134 -0
- package/dist/chunk-DCNOXRMV.js +0 -589
- package/dist/chunk-JJP7YLMN.js +0 -25
- package/dist/chunk-OZ7C3XUE.js +0 -34
- package/dist/chunk-WQ6VGRGZ.js +0 -150
- package/dist/hook-fast-J5LCDHUJ.js +0 -8
- package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
- package/dist/init-PTBITAUO.js +0 -103
- package/dist/login-cmd-IRX6LZT7.js +0 -62
- package/dist/loop-ASZRX3CZ.js +0 -648
- package/dist/sync-cmd-BBAUZ5JD.js +0 -17
- 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 {
|
|
3
|
+
import { applyContextProvenance, installScrubContext, prepareSessionForUpload, wholeSessionCounts, sliceTurnIo } from './chunk-HE2D4EY5.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-6Y63SXFO.js';
|
|
9
|
+
import { tailTranscript, enrichSessionRepo, CAPTURE_TIERS, scanAll, setRepoRoot, resolveRepoIdentity, inferArtifactKind, bannedPathsForTier, sessionIdFromPath, SOURCE_ADAPTERS, EVENT_TYPES } from './chunk-G7FSV7JX.js';
|
|
4
10
|
import { C } from './chunk-YRYMNBGQ.js';
|
|
5
|
-
import { readLaunchSpoolState,
|
|
11
|
+
import { readLaunchSpoolState, launchStatusIsDecisive, launchedItemsBySession, launchesPath } from './chunk-DVDEBNQJ.js';
|
|
6
12
|
import { readLatestProvenanceForSession, readContextProvenance } from './chunk-ZJSNLVCE.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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,
|
|
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",
|
|
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,
|
|
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
|
|
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: {
|
|
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, "");
|
|
@@ -1464,21 +904,26 @@ async function runSync(opts = {}) {
|
|
|
1464
904
|
throw err;
|
|
1465
905
|
}
|
|
1466
906
|
}
|
|
907
|
+
function parseWorkItemNumber(raw) {
|
|
908
|
+
const match = /^\d+$/.exec(raw ?? "");
|
|
909
|
+
return match ? Number(match[0]) : void 0;
|
|
910
|
+
}
|
|
1467
911
|
async function runSyncInner(opts, stats) {
|
|
1468
912
|
const envVars = opts.env ?? process.env;
|
|
1469
913
|
const home = opts.home ?? homeFromEnv(envVars);
|
|
1470
914
|
const fileConfig = readCloudConfig(home);
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
stats.url = url;
|
|
1475
|
-
stats.appId = appId;
|
|
915
|
+
const resolved = resolveGatewayCredentials({ ...opts, home, env: envVars });
|
|
916
|
+
stats.url = resolved.url;
|
|
917
|
+
stats.appId = resolved.appId;
|
|
1476
918
|
const tier = resolveTier(opts.tier ?? envVars.OUTERLAYER_TIER ?? fileConfig.tier);
|
|
1477
919
|
const dryRun = opts.dryRun ?? false;
|
|
1478
920
|
const repoFilter = new RepoFilter(fileConfig.repos);
|
|
1479
921
|
const launchSpool = readLaunchSpoolState(home);
|
|
1480
922
|
const launchedSessionIds = launchSpool.ids;
|
|
1481
923
|
const launchFilter = new LaunchFilter(launchedSessionIds);
|
|
924
|
+
const itemScope = resolved.credential !== void 0 && isItemKey(resolved.credential) ? { item: parseWorkItemNumber(envVars.OUTERLAYER_WORK), sessionItems: launchedItemsBySession(home) } : void 0;
|
|
925
|
+
const inItemScope = (sessionId) => itemScope === void 0 || itemScope.item !== void 0 && itemScope.sessionItems.get(sessionId) === itemScope.item;
|
|
926
|
+
const connectionScoped = resolved.kind === "login" && opts.appId === void 0 && !envVars.OUTERLAYER_APP_ID;
|
|
1482
927
|
const runNowMs = Date.now();
|
|
1483
928
|
const launchSkipVerdict = (mtimeMs) => unlaunchedVerdict({ launchStatusDecisive: launchStatusIsDecisive(launchSpool), idleMs: runNowMs - mtimeMs });
|
|
1484
929
|
const launchSkipReason = () => {
|
|
@@ -1489,7 +934,9 @@ async function runSyncInner(opts, stats) {
|
|
|
1489
934
|
return "not launched with OUTERLAYER_WORK";
|
|
1490
935
|
};
|
|
1491
936
|
const artifactHoldReason = (sessionId) => {
|
|
1492
|
-
if (launchedSessionIds.has(sessionId))
|
|
937
|
+
if (launchedSessionIds.has(sessionId)) {
|
|
938
|
+
return inItemScope(sessionId) ? void 0 : "its session was launched for another work item";
|
|
939
|
+
}
|
|
1493
940
|
if (launchSpool.unreadable !== void 0) {
|
|
1494
941
|
return "the launch spool could not be read this run, so no artifact may leave the machine";
|
|
1495
942
|
}
|
|
@@ -1515,12 +962,8 @@ async function runSyncInner(opts, stats) {
|
|
|
1515
962
|
);
|
|
1516
963
|
}
|
|
1517
964
|
installScrubContext(home, fileConfig.scrub);
|
|
1518
|
-
if (!
|
|
1519
|
-
|
|
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
|
-
}
|
|
965
|
+
if (!resolved.ok) throw new SyncConfigError(resolved.message);
|
|
966
|
+
const { url, credential: apiKey, appId } = resolved;
|
|
1524
967
|
{
|
|
1525
968
|
const sinceMs = opts.all ? 0 : readWatermark(url, appId, home);
|
|
1526
969
|
const hookPlan = planHookExecMerge({ home });
|
|
@@ -1559,7 +1002,7 @@ async function runSyncInner(opts, stats) {
|
|
|
1559
1002
|
}
|
|
1560
1003
|
const liveUploads = readLiveUploadState(home).sessions;
|
|
1561
1004
|
const collected = [];
|
|
1562
|
-
const observedRepos = /* @__PURE__ */ new
|
|
1005
|
+
const observedRepos = /* @__PURE__ */ new Set();
|
|
1563
1006
|
let heldMinMtimeMs = Number.POSITIVE_INFINITY;
|
|
1564
1007
|
let passableMaxMtimeMs = 0;
|
|
1565
1008
|
const neverShipSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -1588,11 +1031,23 @@ async function runSyncInner(opts, stats) {
|
|
|
1588
1031
|
noteSkipped(session.id, entry.mtimeMs, launchSkipVerdict(entry.mtimeMs));
|
|
1589
1032
|
return;
|
|
1590
1033
|
}
|
|
1034
|
+
if (connectionScoped) {
|
|
1035
|
+
const sessionFactory = readRepoConnection(session.env.cwd ?? "/")?.appId;
|
|
1036
|
+
if (sessionFactory !== appId) {
|
|
1037
|
+
const cwd = session.env.cwd ?? "/";
|
|
1038
|
+
const connectable = sessionFactory === void 0 && findGitDirs(cwd) !== void 0;
|
|
1039
|
+
noteSkipped(session.id, entry.mtimeMs, connectable ? "unknown" : "reversible");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const scopeKey = session.subagent?.parentSessionId ?? session.id;
|
|
1044
|
+
if (!inItemScope(scopeKey)) {
|
|
1045
|
+
noteSkipped(session.id, entry.mtimeMs, "unknown");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1591
1048
|
{
|
|
1592
1049
|
const identity = resolveRepoIdentity(session.env.cwd);
|
|
1593
|
-
if (identity.repoRoot)
|
|
1594
|
-
observedRepos.set(identity.repoRoot, identity.gitRepo ?? "");
|
|
1595
|
-
}
|
|
1050
|
+
if (identity.repoRoot) observedRepos.add(identity.repoRoot);
|
|
1596
1051
|
}
|
|
1597
1052
|
const hookEvents = hookPlan.eventsBySession.get(session.id);
|
|
1598
1053
|
if (hookEvents && hookEvents.length > 0) {
|
|
@@ -1600,6 +1055,7 @@ async function runSyncInner(opts, stats) {
|
|
|
1600
1055
|
mergedHookEndKeys.set(session.id, session.subagent?.parentSessionId ?? session.id);
|
|
1601
1056
|
}
|
|
1602
1057
|
applyContextProvenance(session, contextSourceBySessionId.get(session.id));
|
|
1058
|
+
applyReportedCost(session, home);
|
|
1603
1059
|
const gated = prepareSessionForUpload(session, tier, envVars);
|
|
1604
1060
|
const liveEntry = liveUploads[gated.id];
|
|
1605
1061
|
const daemonIsStreaming = liveEntry !== void 0 && !liveEntry.ended && !liveEntry.failed;
|
|
@@ -1651,8 +1107,7 @@ async function runSyncInner(opts, stats) {
|
|
|
1651
1107
|
if (launchStatusIsDecisive(launchSpool)) stats.skippedNotLaunched = launchFilter.skippedTotal;
|
|
1652
1108
|
else stats.held = launchFilter.skippedTotal;
|
|
1653
1109
|
if (!opts.dryRun) {
|
|
1654
|
-
|
|
1655
|
-
for (const repoRoot of observedRepos.keys()) removeJournalHooks(repoRoot);
|
|
1110
|
+
for (const repoRoot of observedRepos) removeJournalHooks(repoRoot);
|
|
1656
1111
|
}
|
|
1657
1112
|
collected.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1658
1113
|
const items = collected.flatMap((item) => chunkOversizedSession(item));
|
|
@@ -1814,7 +1269,7 @@ async function runSyncInner(opts, stats) {
|
|
|
1814
1269
|
output: output2
|
|
1815
1270
|
};
|
|
1816
1271
|
}
|
|
1817
|
-
const fetchImpl = opts.fetchImpl ??
|
|
1272
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
1818
1273
|
const endpoint = new URL("/v1/agents/sync", url).toString();
|
|
1819
1274
|
let synced = 0;
|
|
1820
1275
|
let blobsSent = 0;
|
|
@@ -1896,7 +1351,7 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} hit ${cause
|
|
|
1896
1351
|
if (response.status === 401) {
|
|
1897
1352
|
failureContext();
|
|
1898
1353
|
throw new SyncTransportError(
|
|
1899
|
-
|
|
1354
|
+
unauthorizedMessage(apiKey),
|
|
1900
1355
|
401
|
|
1901
1356
|
);
|
|
1902
1357
|
}
|
|
@@ -2082,7 +1537,7 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
|
|
|
2082
1537
|
}
|
|
2083
1538
|
if (status === 401) {
|
|
2084
1539
|
unauthorized = new SyncTransportError(
|
|
2085
|
-
|
|
1540
|
+
unauthorizedMessage(apiKey),
|
|
2086
1541
|
401
|
|
2087
1542
|
);
|
|
2088
1543
|
heldCount += 1;
|
|
@@ -2163,7 +1618,10 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
|
|
|
2163
1618
|
);
|
|
2164
1619
|
}
|
|
2165
1620
|
} else {
|
|
2166
|
-
writeCloudConfig(
|
|
1621
|
+
writeCloudConfig(
|
|
1622
|
+
{ url, apiKey: resolved.kind === "key" ? apiKey : void 0, appId: resolved.kind === "key" ? appId : void 0 },
|
|
1623
|
+
home
|
|
1624
|
+
);
|
|
2167
1625
|
}
|
|
2168
1626
|
}
|
|
2169
1627
|
let output;
|
|
@@ -2222,4 +1680,4 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
|
|
|
2222
1680
|
}
|
|
2223
1681
|
}
|
|
2224
1682
|
|
|
2225
|
-
export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES,
|
|
1683
|
+
export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES, SyncConfigError, SyncTransportError, isLoopbackUrl, resolveTier, runSync, staleBuildRefusal, staleBuildWarning };
|