@ref0/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +144 -2
  2. package/dist/main.mjs +775 -548
  3. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -798,7 +798,7 @@ const ScopeKind = Schema.Literals([
798
798
  "session",
799
799
  "agent"
800
800
  ]);
801
- const Scope$1 = Schema.Struct({
801
+ const Scope$2 = Schema.Struct({
802
802
  kind: ScopeKind,
803
803
  id: Schema.String.check(Schema.isMinLength(1)),
804
804
  action: ScopeAction
@@ -971,7 +971,7 @@ const SessionDepth = Schema.Int.check(Schema.isBetween({
971
971
  * attachment's authorization there and the agent's target there.
972
972
  */
973
973
  const AskingHeld = Schema.Struct({
974
- scopes: Schema.Array(Scope$1),
974
+ scopes: Schema.Array(Scope$2),
975
975
  pins: Schema.Struct({
976
976
  agent: Generation,
977
977
  attachment: Generation,
@@ -1010,7 +1010,7 @@ const CredentialFacts = Schema.Struct({
1010
1010
  }),
1011
1011
  expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
1012
1012
  /** What it may do, and where. Rows a reader can't decode were left out: they grant nothing. */
1013
- scopes: Schema.Array(Scope$1),
1013
+ scopes: Schema.Array(Scope$2),
1014
1014
  /**
1015
1015
  * For a session's token: its project's owner, which answers what its agent holds there; the
1016
1016
  * session, whose lease its project's Project DO holds; and how deep it is in its chain, 1 for
@@ -5186,7 +5186,7 @@ const commandFields = {
5186
5186
  * What the credential may do, and where, as its person's Owner DO said: the writer checks
5187
5187
  * the command's action against it on what it holds, such as which thread a number is.
5188
5188
  */
5189
- scopes: Schema.Array(Scope$1),
5189
+ scopes: Schema.Array(Scope$2),
5190
5190
  /** For a command a person approved: their `approval.answered`, which its events name as their cause. */
5191
5191
  cause: Schema.optionalKey(EventRef),
5192
5192
  /**
@@ -5705,7 +5705,7 @@ const Me = Schema.Struct({
5705
5705
  session: SessionId,
5706
5706
  depth: SessionDepth
5707
5707
  })),
5708
- scopes: Schema.Array(Scope$1),
5708
+ scopes: Schema.Array(Scope$2),
5709
5709
  expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
5710
5710
  });
5711
5711
  /**
@@ -6075,7 +6075,7 @@ const Reader = Schema.Struct({
6075
6075
  agent: Schema.optionalKey(AgentRef),
6076
6076
  /** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
6077
6077
  attachment: Schema.NullOr(CredentialRef),
6078
- scopes: Schema.Array(Scope$1)
6078
+ scopes: Schema.Array(Scope$2)
6079
6079
  });
6080
6080
  Schema.Struct({
6081
6081
  reader: Reader,
@@ -6589,14 +6589,13 @@ const ConnectOrganization = Schema.Struct({
6589
6589
  organization: OrgId,
6590
6590
  name: OwnerName
6591
6591
  });
6592
- /** Whether admission replaced a live host grant, or skipped one that no longer belongs here. */
6592
+ /**
6593
+ * A folder's host grant from before: on and the person's, so the new grant's confirm turns it off,
6594
+ * or skipped, off already or not theirs, which a yes leaves as it is.
6595
+ */
6593
6596
  const GrantReplacement = Schema.Struct({
6594
6597
  grant: TokenId,
6595
- state: Schema.Literals([
6596
- "replaces",
6597
- "revoked",
6598
- "skipped"
6599
- ]),
6598
+ state: Schema.Literals(["replaces", "skipped"]),
6600
6599
  attachedAt: Schema.optionalKey(Timestamp)
6601
6600
  });
6602
6601
  /**
@@ -13242,7 +13241,7 @@ Schema.Struct({
13242
13241
  actor: ActorRef,
13243
13242
  /** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
13244
13243
  attachment: Schema.NullOr(CredentialRef),
13245
- scopes: Schema.Array(Scope$1),
13244
+ scopes: Schema.Array(Scope$2),
13246
13245
  ownerName: OwnerName,
13247
13246
  number: ThreadNumber
13248
13247
  });
@@ -19109,271 +19108,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
19109
19108
  /** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
19110
19109
  const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
19111
19110
  //#endregion
19112
- //#region src/session.ts
19113
- /**
19114
- * An agent's command in a harness session that has no ref0 session, though an agent is connected
19115
- * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19116
- * person instead.
19117
- */
19118
- var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19119
- /**
19120
- * The token of the session this command runs in, if it runs in one: an agent's, which its
19121
- * harness bound when the session started. Where it's found is the harness's to say. Outside a
19122
- * session, there's none, and every command is the person's, asked by approval.
19123
- */
19124
- var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
19125
- /** No session: until a harness binds one, the person asks, by approval. */
19126
- static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
19127
- };
19128
- //#endregion
19129
- //#region src/call.ts
19130
- /**
19131
- * Every command's one way to ref0: an endpoint's request, sent with the session's token when
19132
- * the command runs in a session, or else asked of the person, by approval, from this terminal.
19133
- * Either way the answer is the endpoint's own, as JSON.
19134
- */
19135
- /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
19136
- var CallFailed = class extends Schema.TaggedError()("CallFailed", {
19137
- status: Schema.Int,
19138
- kind: Schema.String,
19139
- nextStep: Schema.String
19140
- }) {};
19141
- const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
19142
- const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
19143
- const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
19144
- const decodeRefusal = Schema.decodeUnknownOption(AnyError);
19145
- /** How often a terminal asks whether its person has answered. */
19146
- const pollMillis$1 = 2e3;
19147
- /** How long one request may take before it's asked again, or the call gives up. */
19148
- const requestTimeout$1 = Duration.seconds(15);
19149
- /**
19150
- * How long a session's request may take for the commands that read and compile before they
19151
- * answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
19152
- * lacks the check, one paced read at a time, up to 256 files each.
19153
- */
19154
- const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
19155
- /** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
19156
- const giveUpAfterMillis$1 = 3e5;
19157
- /** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
19158
- const outageMillis = 3e5;
19159
- /** What a change that failed partway says: ref0 may have made it all the same. */
19160
- const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
19161
- /**
19162
- * An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
19163
- * 5xx, may have been made before it failed, so it isn't said to try again.
19164
- */
19165
- const failedWith$15 = (status, body, change) => Option.match(decodeRefusal(body), {
19166
- onNone: () => new CallFailed({
19167
- status,
19168
- kind: "Unexpected",
19169
- nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
19170
- }),
19171
- onSome: (refusal) => new CallFailed({
19172
- status,
19173
- kind: refusal._tag,
19174
- nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19175
- })
19176
- });
19177
- /**
19178
- * A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
19179
- * command's failure: a change that failed at a 5xx may have been made all the same.
19180
- */
19181
- const refusedBy = (status, refusal, change) => new CallFailed({
19182
- status,
19183
- kind: refusal._tag,
19184
- nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19185
- });
19186
- /**
19187
- * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19188
- * the CLI made. A change it answered may have run.
19189
- */
19190
- const unreadable = (endpoint, change) => new CallFailed({
19191
- status: 0,
19192
- kind: "Unexpected",
19193
- nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
19194
- });
19195
- const unreachable = new CallFailed({
19196
- status: 0,
19197
- kind: "Unreachable",
19198
- nextStep: "Couldn’t reach ref0. Try again."
19199
- });
19200
- /**
19201
- * An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
19202
- * change may have run, and asking again could make it twice.
19203
- */
19204
- const gone = (change, nextStep) => new CallFailed({
19205
- status: 404,
19206
- kind: "ApprovalNotFound",
19207
- nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
19208
- });
19209
- /** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
19210
- const unanswered$1 = new CallFailed({
19211
- status: 0,
19212
- kind: "Unreachable",
19213
- nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
19214
- });
19215
- /**
19216
- * Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
19217
- * stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
19218
- * What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
19219
- */
19220
- const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
19221
- const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
19222
- let approved = false;
19223
- let heardAt = yield* Clock.currentTimeMillis;
19224
- while (true) {
19225
- const answered = yield* collect;
19226
- const now = yield* Clock.currentTimeMillis;
19227
- if (Option.isNone(answered)) {
19228
- if (now - heardAt > outageMillis) return yield* new CallFailed({
19229
- status: 0,
19230
- kind: "Unreachable",
19231
- nextStep: approved ? "It was approved, and ref0 stopped answering before it said it ran. Check whether it did before you run it again." : "ref0 stopped answering before it said how it ended. Check whether it ran before you run it again."
19232
- });
19233
- yield* Effect.sleep(pollMillis$1);
19234
- continue;
19235
- }
19236
- const state = answered.value;
19237
- heardAt = now;
19238
- if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19239
- onNone: () => Effect.fail(unreadable(endpoint, change)),
19240
- onSome: Result.match({
19241
- onSuccess: () => Effect.succeed(state.result),
19242
- onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
19243
- })
19244
- });
19245
- if (state.state === "refused") return yield* new CallFailed({
19246
- status: 403,
19247
- kind: "Refused",
19248
- nextStep: "It was refused."
19249
- });
19250
- if (state.state === "approved") {
19251
- if (!approved) yield* Console.error("Approved. Waiting for it to run.");
19252
- approved = true;
19253
- yield* Effect.sleep(pollMillis$1);
19254
- continue;
19255
- }
19256
- if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
19257
- status: 408,
19258
- kind: "Expired",
19259
- nextStep: "Nobody approved it in time. Run it again."
19260
- });
19261
- yield* Effect.sleep(pollMillis$1);
19262
- }
19263
- });
19264
- var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19265
- static layer = Layer.effect(Ref0, Effect.gen(function* () {
19266
- const config = yield* Ref0Config;
19267
- const session = yield* SessionToken;
19268
- const http = yield* HttpClient.HttpClient;
19269
- const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
19270
- const timeout = timeoutOf(request.command.endpoint);
19271
- const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
19272
- const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
19273
- const change = request.method !== "GET";
19274
- const lost = change ? unanswered$1 : unreachable;
19275
- const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
19276
- response,
19277
- text
19278
- }))), Effect.timeoutOrElse({
19279
- duration: timeout,
19280
- orElse: () => Effect.fail(lost)
19281
- }), Effect.mapError(() => lost));
19282
- const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
19283
- if (response.status === 202) {
19284
- const needed = decodeNeeded(body);
19285
- if (Option.isNone(needed)) return yield* failedWith$15(202, body, change);
19286
- yield* Console.error(`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`);
19287
- return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19288
- }
19289
- return response.status < 300 ? body : yield* failedWith$15(response.status, body, change);
19290
- });
19291
- const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19292
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
19293
- const refused = (status) => (error) => Effect.fail(new CallFailed({
19294
- status,
19295
- kind: error._tag,
19296
- nextStep: error.nextStep
19297
- }));
19298
- return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19299
- duration: requestTimeout$1,
19300
- orElse: () => Effect.succeedNone
19301
- }), Effect.catchTags({
19302
- ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19303
- Unauthenticated: refused(401),
19304
- SessionHasEnded: refused(410)
19305
- }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19306
- });
19307
- const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
19308
- const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
19309
- const { secret, hash } = yield* freshSecret;
19310
- const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
19311
- const started = yield* client.terminalApprovals.start({
19312
- headers: { "idempotency-key": yield* newRequestId },
19313
- payload: {
19314
- command,
19315
- secretHash
19316
- }
19317
- }).pipe(Effect.timeoutOrElse({
19318
- duration: requestTimeout$1,
19319
- orElse: () => Effect.fail(unreachable)
19320
- }), Effect.catchTags({
19321
- CommandRefused: (refused) => Effect.fail(new CallFailed({
19322
- status: 400,
19323
- kind: refused._tag,
19324
- nextStep: refused.nextStep
19325
- })),
19326
- SlowDown: (slow) => Effect.fail(new CallFailed({
19327
- status: 429,
19328
- kind: slow._tag,
19329
- nextStep: slow.nextStep
19330
- }))
19331
- }), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
19332
- yield* Console.error(`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`);
19333
- const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
19334
- const collect = (yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http))).terminalApprovals.result({ params: { approval: started.approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19335
- duration: requestTimeout$1,
19336
- orElse: () => Effect.succeedNone
19337
- }), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19338
- return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19339
- });
19340
- const call = Effect.fn("Ref0.call")(function* (command) {
19341
- const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19342
- status: 0,
19343
- kind: error._tag,
19344
- nextStep: error.nextStep
19345
- })));
19346
- const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19347
- status: 400,
19348
- kind: refused._tag,
19349
- nextStep: refused.nextStep
19350
- })));
19351
- const change = request.method !== "GET";
19352
- const answer = yield* Option.match(token, {
19353
- onNone: () => asPerson(request.command, change, command.endpoint),
19354
- onSome: (bearer) => asSession(bearer, request, command.endpoint)
19355
- });
19356
- if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19357
- return answer;
19358
- });
19359
- return Ref0.of({ call });
19360
- }));
19361
- };
19362
- /**
19363
- * One endpoint's call, typed from the API: the compiler checks its command, and its answer is
19364
- * read by the endpoint's own success schemas, so what a command shows of it is checked too.
19365
- */
19366
- const callEndpoint = (command) => Effect.gen(function* () {
19367
- const json = yield* (yield* Ref0).call(command);
19368
- return {
19369
- json,
19370
- value: yield* Option.match(successOf(command.endpoint, json), {
19371
- onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19372
- onSome: Effect.succeed
19373
- })
19374
- };
19375
- });
19376
- //#endregion
19377
19111
  //#region src/output.ts
19378
19112
  /**
19379
19113
  * What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
@@ -19399,12 +19133,88 @@ const renderLine = (line, terminal) => {
19399
19133
  if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
19400
19134
  return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
19401
19135
  };
19402
- const renderBlocks = (blocks, stream) => {
19136
+ /** The most columns a printed line takes, however wide the terminal. */
19137
+ const mostColumns = 100;
19138
+ /** A printed text without the styles this printer adds. */
19139
+ const unstyled = (text) => text.replaceAll(/\u001b\[\d+m/g, "");
19140
+ const graphemes = new Intl.Segmenter();
19141
+ /** The columns a printed text takes, a character each as the person sees it: styles take none. */
19142
+ const columnsOf = (text) => [...graphemes.segment(unstyled(text))].length;
19143
+ /** A word that says an inline command follows: `run ref0 connect`, `Read a text: ref0 text …`. */
19144
+ const introducesCommand = (word) => word === "run" || word === "Run" || word.endsWith(":");
19145
+ /**
19146
+ * Inline `ref0` commands made single words while they fit in `room`: `ref0` and the word after it
19147
+ * always, and a command after `run` or a colon to its clause's end, so no line breaks inside one.
19148
+ */
19149
+ const withCommandsWhole = (words, room) => {
19150
+ const joined = [];
19151
+ for (let at = 0; at < words.length; at += 1) {
19152
+ const first = words[at];
19153
+ if (first === void 0) continue;
19154
+ if (unstyled(first.word) !== "ref0" || at + 1 >= words.length) {
19155
+ joined.push(first);
19156
+ continue;
19157
+ }
19158
+ const before = words[at - 1];
19159
+ let end = at + 1;
19160
+ if (before !== void 0 && introducesCommand(unstyled(before.word))) while (end + 1 < words.length && !/[.,;:]$/.test(unstyled(words[end]?.word ?? ""))) end += 1;
19161
+ const word = words.slice(at, end + 1).map((each, index) => index === 0 ? each.word : `${each.space}${each.word}`).join("");
19162
+ if (columnsOf(word) > room) {
19163
+ joined.push(first);
19164
+ continue;
19165
+ }
19166
+ joined.push({
19167
+ space: first.space,
19168
+ word
19169
+ });
19170
+ at = end;
19171
+ }
19172
+ return joined;
19173
+ };
19174
+ /**
19175
+ * A line broken between words to fit `width` columns, each line after the first indented as the
19176
+ * first is. A line that fits stays as it is, and a word wider than the width stays whole, on a
19177
+ * line of its own.
19178
+ */
19179
+ const wrapLine = (line, width) => {
19180
+ if (columnsOf(line) <= width) return line;
19181
+ const indent = /^\s*/.exec(line)?.[0] ?? "";
19182
+ const room = Math.max(1, width - columnsOf(indent));
19183
+ const words = withCommandsWhole(Array.from(line.slice(indent.length).matchAll(/(\s*)(\S+)/g), ([, space = "", word = ""]) => ({
19184
+ space,
19185
+ word
19186
+ })), room);
19187
+ const lines = [];
19188
+ let current = "";
19189
+ for (const { space, word } of words) if (current !== "" && columnsOf(current) + columnsOf(space) + columnsOf(word) > room) {
19190
+ lines.push(current);
19191
+ current = word;
19192
+ } else current += current === "" ? word : `${space}${word}`;
19193
+ return [...lines, current].map((each) => `${indent}${each}`).join("\n");
19194
+ };
19195
+ /**
19196
+ * How wide a printed line may be: the terminal's width, at most 100 columns, or none when the
19197
+ * stream isn't a terminal, as for an agent or a script, whose own reader wraps.
19198
+ */
19199
+ const widthOf = (stream) => {
19200
+ if (process[stream].isTTY !== true) return void 0;
19201
+ const columns = process[stream].columns;
19202
+ return columns > 0 ? Math.min(columns, mostColumns) : mostColumns;
19203
+ };
19204
+ const renderBlocks = (blocks, stream, width) => {
19403
19205
  const terminal = process[stream].isTTY === true && process.env["NO_COLOR"] === void 0;
19404
- return blocks.flatMap((block) => block.length === 0 ? [] : [block.map((line) => renderLine(line, terminal)).join("\n")]).join("\n\n");
19206
+ const shown = (line) => {
19207
+ const rendered = renderLine(line, terminal);
19208
+ if (width === void 0 || !Predicate.isString(line) && "command" in line) return rendered;
19209
+ return rendered.split("\n").map((each) => wrapLine(each, width)).join("\n");
19210
+ };
19211
+ return blocks.flatMap((block) => block.length === 0 ? [] : [block.map(shown).join("\n")]).join("\n\n");
19405
19212
  };
19406
- /** Words go to stderr by default; a command's answer goes to stdout. */
19407
- const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream)).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
19213
+ /**
19214
+ * Words go to stderr by default; a command's answer goes to stdout. In a terminal, each line but a
19215
+ * command's is broken between words to fit it.
19216
+ */
19217
+ const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream, widthOf(stream))).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
19408
19218
  /**
19409
19219
  * A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
19410
19220
  * yes. The cursor stays after it, for their answer.
@@ -19474,6 +19284,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
19474
19284
  };
19475
19285
  /** Says what went wrong, once, on stderr, and fails the command. */
19476
19286
  const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] : words).pipe(Effect.andThen(Effect.fail(new Said())));
19287
+ //#endregion
19288
+ //#region src/session.ts
19289
+ /**
19290
+ * An agent's command in a harness session that has no ref0 session, though an agent is connected
19291
+ * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19292
+ * person instead.
19293
+ */
19294
+ var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19295
+ /**
19296
+ * The token of the session this command runs in, if it runs in one: an agent's, which its
19297
+ * harness bound when the session started. Where it's found is the harness's to say. Outside a
19298
+ * session, there's none, and every command is the person's, asked by approval.
19299
+ */
19300
+ var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
19301
+ /** No session: until a harness binds one, the person asks, by approval. */
19302
+ static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
19303
+ };
19304
+ /**
19305
+ * The thread the session this command runs in is on, as its start kept it beside the session's
19306
+ * token. Outside a session there's none, and in one whose start kept none.
19307
+ */
19308
+ var SessionThread = class SessionThread extends Context.Service()("ref0/cli/SessionThread") {
19309
+ /** No session, so no thread. */
19310
+ static none = Layer.succeed(SessionThread, SessionThread.of({ current: Effect.succeedNone }));
19311
+ };
19312
+ //#endregion
19313
+ //#region src/call.ts
19314
+ /**
19315
+ * Every command's one way to ref0: an endpoint's request, sent with the session's token when
19316
+ * the command runs in a session, or else asked of the person, by approval, from this terminal.
19317
+ * Either way the answer is the endpoint's own, as JSON.
19318
+ */
19319
+ /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
19320
+ var CallFailed = class extends Schema.TaggedError()("CallFailed", {
19321
+ status: Schema.Int,
19322
+ kind: Schema.String,
19323
+ nextStep: Schema.String
19324
+ }) {};
19325
+ const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
19326
+ const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
19327
+ const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
19328
+ const decodeRefusal = Schema.decodeUnknownOption(AnyError);
19329
+ /** How often a terminal asks whether its person has answered. */
19330
+ const pollMillis$1 = 2e3;
19331
+ /** How long one request may take before it's asked again, or the call gives up. */
19332
+ const requestTimeout$1 = Duration.seconds(15);
19333
+ /**
19334
+ * How long a session's request may take for the commands that read and compile before they
19335
+ * answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
19336
+ * lacks the check, one paced read at a time, up to 256 files each.
19337
+ */
19338
+ const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
19339
+ /** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
19340
+ const giveUpAfterMillis$1 = 3e5;
19341
+ /** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
19342
+ const outageMillis = 3e5;
19343
+ /** What a change that failed partway says: ref0 may have made it all the same. */
19344
+ const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
19345
+ /**
19346
+ * An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
19347
+ * 5xx, may have been made before it failed, so it isn't said to try again.
19348
+ */
19349
+ const failedWith$16 = (status, body, change) => Option.match(decodeRefusal(body), {
19350
+ onNone: () => new CallFailed({
19351
+ status,
19352
+ kind: "Unexpected",
19353
+ nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
19354
+ }),
19355
+ onSome: (refusal) => new CallFailed({
19356
+ status,
19357
+ kind: refusal._tag,
19358
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19359
+ })
19360
+ });
19361
+ /**
19362
+ * A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
19363
+ * command's failure: a change that failed at a 5xx may have been made all the same.
19364
+ */
19365
+ const refusedBy = (status, refusal, change) => new CallFailed({
19366
+ status,
19367
+ kind: refusal._tag,
19368
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19369
+ });
19370
+ /**
19371
+ * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19372
+ * the CLI made. A change it answered may have run.
19373
+ */
19374
+ const unreadable = (endpoint, change) => new CallFailed({
19375
+ status: 0,
19376
+ kind: "Unexpected",
19377
+ nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
19378
+ });
19379
+ const unreachable = new CallFailed({
19380
+ status: 0,
19381
+ kind: "Unreachable",
19382
+ nextStep: "Couldn’t reach ref0. Try again."
19383
+ });
19384
+ /**
19385
+ * An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
19386
+ * change may have run, and asking again could make it twice.
19387
+ */
19388
+ const gone = (change, nextStep) => new CallFailed({
19389
+ status: 404,
19390
+ kind: "ApprovalNotFound",
19391
+ nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
19392
+ });
19393
+ /** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
19394
+ const unanswered$1 = new CallFailed({
19395
+ status: 0,
19396
+ kind: "Unreachable",
19397
+ nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
19398
+ });
19399
+ /**
19400
+ * Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
19401
+ * stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
19402
+ * What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
19403
+ */
19404
+ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
19405
+ const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
19406
+ let approved = false;
19407
+ let heardAt = yield* Clock.currentTimeMillis;
19408
+ while (true) {
19409
+ const answered = yield* collect;
19410
+ const now = yield* Clock.currentTimeMillis;
19411
+ if (Option.isNone(answered)) {
19412
+ if (now - heardAt > outageMillis) return yield* new CallFailed({
19413
+ status: 0,
19414
+ kind: "Unreachable",
19415
+ nextStep: approved ? "It was approved, and ref0 stopped answering before it said it ran. Check whether it did before you run it again." : "ref0 stopped answering before it said how it ended. Check whether it ran before you run it again."
19416
+ });
19417
+ yield* Effect.sleep(pollMillis$1);
19418
+ continue;
19419
+ }
19420
+ const state = answered.value;
19421
+ heardAt = now;
19422
+ if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19423
+ onNone: () => Effect.fail(unreadable(endpoint, change)),
19424
+ onSome: Result.match({
19425
+ onSuccess: () => Effect.succeed(state.result),
19426
+ onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
19427
+ })
19428
+ });
19429
+ if (state.state === "refused") return yield* new CallFailed({
19430
+ status: 403,
19431
+ kind: "Refused",
19432
+ nextStep: "It was refused."
19433
+ });
19434
+ if (state.state === "approved") {
19435
+ if (!approved) yield* printBlocks([["Approved. Waiting for it to run."]]);
19436
+ approved = true;
19437
+ yield* Effect.sleep(pollMillis$1);
19438
+ continue;
19439
+ }
19440
+ if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
19441
+ status: 408,
19442
+ kind: "Expired",
19443
+ nextStep: "Nobody approved it in time. Run it again."
19444
+ });
19445
+ yield* Effect.sleep(pollMillis$1);
19446
+ }
19447
+ });
19448
+ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19449
+ static layer = Layer.effect(Ref0, Effect.gen(function* () {
19450
+ const config = yield* Ref0Config;
19451
+ const session = yield* SessionToken;
19452
+ const http = yield* HttpClient.HttpClient;
19453
+ const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
19454
+ const timeout = timeoutOf(request.command.endpoint);
19455
+ const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
19456
+ const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
19457
+ const change = request.method !== "GET";
19458
+ const lost = change ? unanswered$1 : unreachable;
19459
+ const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
19460
+ response,
19461
+ text
19462
+ }))), Effect.timeoutOrElse({
19463
+ duration: timeout,
19464
+ orElse: () => Effect.fail(lost)
19465
+ }), Effect.mapError(() => lost));
19466
+ const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
19467
+ if (response.status === 202) {
19468
+ const needed = decodeNeeded(body);
19469
+ if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
19470
+ yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
19471
+ return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19472
+ }
19473
+ return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
19474
+ });
19475
+ const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19476
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
19477
+ const refused = (status) => (error) => Effect.fail(new CallFailed({
19478
+ status,
19479
+ kind: error._tag,
19480
+ nextStep: error.nextStep
19481
+ }));
19482
+ return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19483
+ duration: requestTimeout$1,
19484
+ orElse: () => Effect.succeedNone
19485
+ }), Effect.catchTags({
19486
+ ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19487
+ Unauthenticated: refused(401),
19488
+ SessionHasEnded: refused(410)
19489
+ }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19490
+ });
19491
+ const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
19492
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
19493
+ const { secret, hash } = yield* freshSecret;
19494
+ const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
19495
+ const started = yield* client.terminalApprovals.start({
19496
+ headers: { "idempotency-key": yield* newRequestId },
19497
+ payload: {
19498
+ command,
19499
+ secretHash
19500
+ }
19501
+ }).pipe(Effect.timeoutOrElse({
19502
+ duration: requestTimeout$1,
19503
+ orElse: () => Effect.fail(unreachable)
19504
+ }), Effect.catchTags({
19505
+ CommandRefused: (refused) => Effect.fail(new CallFailed({
19506
+ status: 400,
19507
+ kind: refused._tag,
19508
+ nextStep: refused.nextStep
19509
+ })),
19510
+ SlowDown: (slow) => Effect.fail(new CallFailed({
19511
+ status: 429,
19512
+ kind: slow._tag,
19513
+ nextStep: slow.nextStep
19514
+ }))
19515
+ }), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
19516
+ yield* printBlocks([[`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`]]);
19517
+ const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
19518
+ const collect = (yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http))).terminalApprovals.result({ params: { approval: started.approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19519
+ duration: requestTimeout$1,
19520
+ orElse: () => Effect.succeedNone
19521
+ }), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19522
+ return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19523
+ });
19524
+ const call = Effect.fn("Ref0.call")(function* (command) {
19525
+ const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19526
+ status: 0,
19527
+ kind: error._tag,
19528
+ nextStep: error.nextStep
19529
+ })));
19530
+ const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19531
+ status: 400,
19532
+ kind: refused._tag,
19533
+ nextStep: refused.nextStep
19534
+ })));
19535
+ const change = request.method !== "GET";
19536
+ const answer = yield* Option.match(token, {
19537
+ onNone: () => asPerson(request.command, change, command.endpoint),
19538
+ onSome: (bearer) => asSession(bearer, request, command.endpoint)
19539
+ });
19540
+ if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19541
+ return answer;
19542
+ });
19543
+ return Ref0.of({ call });
19544
+ }));
19545
+ };
19546
+ /**
19547
+ * One endpoint's call, typed from the API: the compiler checks its command, and its answer is
19548
+ * read by the endpoint's own success schemas, so what a command shows of it is checked too.
19549
+ */
19550
+ const callEndpoint = (command) => Effect.gen(function* () {
19551
+ const json = yield* (yield* Ref0).call(command);
19552
+ return {
19553
+ json,
19554
+ value: yield* Option.match(successOf(command.endpoint, json), {
19555
+ onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19556
+ onSome: Effect.succeed
19557
+ })
19558
+ };
19559
+ });
19477
19560
  /** The account's commands. */
19478
19561
  const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
19479
19562
  const answer = yield* callEndpoint({ endpoint: "me.get" });
@@ -20417,7 +20500,7 @@ const waitsFor = (each) => {
20417
20500
  * A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
20418
20501
  * when it's in another of `owner`'s, then its title and stage.
20419
20502
  */
20420
- const threadWords = (thread, owner = "") => {
20503
+ const threadWords$1 = (thread, owner = "") => {
20421
20504
  return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
20422
20505
  };
20423
20506
  /** What holds a stack back below the thread, as a line says it, to follow "and". */
@@ -20435,13 +20518,13 @@ const stackHeld = (stack) => {
20435
20518
  */
20436
20519
  const planLine = (wait, number, owner) => {
20437
20520
  if (wait.kind === "stack") {
20438
- const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
20521
+ const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords$1(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
20439
20522
  return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
20440
20523
  }
20441
20524
  if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
20442
20525
  if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
20443
20526
  const holds = wait.child.project === void 0 || wait.child.stage !== "settled" ? `#${number} is settled once it is` : `#${number} is settled once a settle check reads it so`;
20444
- return `${wait.child.thread} required child ${threadWords(wait.child, owner)}: ${holds}`;
20527
+ return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
20445
20528
  };
20446
20529
  /** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
20447
20530
  const blockedBy$1 = (list) => {
@@ -20646,7 +20729,7 @@ const browserOpener = Effect.gen(function* () {
20646
20729
  });
20647
20730
  /** Starts the browser independently; a missing or failing desktop never stops the paste. */
20648
20731
  const openBrowser = (program, link) => Effect.sync(() => {
20649
- const failed = () => Effect.runFork(Console.error("Couldn’t open the browser. Open the printed link yourself."));
20732
+ const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
20650
20733
  try {
20651
20734
  const child = spawn(program, [link], {
20652
20735
  detached: true,
@@ -21317,7 +21400,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21317
21400
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21318
21401
  * and only as a default export.
21319
21402
  */
21320
- const cliVersion = "0.11.0";
21403
+ const cliVersion = "0.12.0";
21321
21404
  //#endregion
21322
21405
  //#region src/harness/hook-protocol.ts
21323
21406
  /**
@@ -21635,7 +21718,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21635
21718
  const link = yield* freshLinkSecret;
21636
21719
  const pasted = yield* Ref.make(void 0);
21637
21720
  const opener = yield* browserOpener;
21638
- const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(Console.error("Couldn’t listen for the browser. Paste the code instead."), void 0)));
21721
+ const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(printBlocks([["Couldn’t listen for the browser. Paste the code instead."]]), void 0)));
21639
21722
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21640
21723
  const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
21641
21724
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
@@ -21646,7 +21729,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21646
21729
  ...folder,
21647
21730
  shown: shownConfigFolder(home, folder.configFolder),
21648
21731
  grant: yield* freshTokenSecret,
21649
- replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
21732
+ replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said)),
21733
+ before: Option.isSome(held) ? yield* hostGrant.get(folder.configFolder).pipe(Effect.catchTag("KeychainError", (error) => error.unreadable === true ? Effect.succeedNone : said(error))) : Option.none()
21650
21734
  };
21651
21735
  }));
21652
21736
  const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
@@ -21726,17 +21810,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21726
21810
  yield* Effect.sleep(pollMillis);
21727
21811
  }
21728
21812
  });
21729
- const keepOne = Effect.fnUntraced(function* (folder, connection, keepBy) {
21813
+ const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
21730
21814
  const left = keepBy - (yield* Clock.currentTimeMillis);
21731
- if (left <= 0) return Option.some("The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21732
- const token = formatToken(connection.credential.person, connection.credential.id, folder.grant.secret);
21733
- const kept = yield* hostGrant.keep(folder.configFolder, {
21734
- harness: folder.harness,
21735
- owner: connection.owner,
21736
- agent: connection.handle,
21737
- agentId: connection.agent.id,
21738
- token: Redacted.make(token)
21739
- }).pipe(Effect.timeoutOrElse({
21815
+ if (left <= 0) return Option.some(late);
21816
+ const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
21740
21817
  duration: Duration.min(keepTimeout, Duration.millis(left)),
21741
21818
  orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
21742
21819
  }), Effect.retry({
@@ -21745,10 +21822,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21745
21822
  }), Effect.result);
21746
21823
  return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
21747
21824
  });
21825
+ const keepOne = (folder, connection, keepBy) => keepTrying(folder.configFolder, {
21826
+ harness: folder.harness,
21827
+ owner: connection.owner,
21828
+ agent: connection.handle,
21829
+ agentId: connection.agent.id,
21830
+ token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
21831
+ }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21748
21832
  const finish = Effect.fnUntraced(function* (state) {
21749
21833
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
21750
21834
  const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21751
21835
  const kept = [];
21836
+ const keptFrom = [];
21752
21837
  const grants = [];
21753
21838
  const notKept = [];
21754
21839
  for (const folder of asking) {
@@ -21778,9 +21863,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21778
21863
  handle: connection.handle,
21779
21864
  organizations: connection.organizations
21780
21865
  });
21866
+ keptFrom.push(folder);
21781
21867
  grants.push(connection.credential.id);
21782
21868
  }
21783
- yield* oldGrants.retire(kept);
21784
21869
  const [first, ...rest] = grants;
21785
21870
  if (first === void 0) return yield* sayFailed([[
21786
21871
  "It connected, but this machine couldn’t keep what it needs to act:",
@@ -21813,9 +21898,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21813
21898
  times: 2,
21814
21899
  schedule: Schedule.spaced("1 second")
21815
21900
  }), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
21816
- for (const folder of kept) yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
21817
- return yield* sayFailed(`ref0 revoked the grants, since this machine didn’t confirm in time that it kept them. They’re deleted here, and nothing is connected. To connect this machine, run: ${again$1}`);
21901
+ const backBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21902
+ const notBack = [];
21903
+ for (const folder of keptFrom) {
21904
+ if (Option.isNone(folder.before)) {
21905
+ yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
21906
+ continue;
21907
+ }
21908
+ const failed = yield* keepTrying(folder.configFolder, folder.before.value, backBy, "The keychain took too long with the folders before it.");
21909
+ if (Option.isSome(failed)) notBack.push(`${shownConfigFolder(home, folder.configFolder)}: ${failed.value}`);
21910
+ }
21911
+ return yield* sayFailed([[
21912
+ "ref0 revoked the new grants, since this machine didn’t confirm in time that it kept them.",
21913
+ ...notBack.length === 0 ? ["Each folder has its connection from before back, if it had one. To connect this machine, run:"] : [
21914
+ "These folders hold a grant that works nowhere, since the keychain didn’t take back their records:",
21915
+ ...notBack,
21916
+ "Each other folder has its connection from before back, if it had one. To connect this machine, run:"
21917
+ ],
21918
+ { command: again$1 }
21919
+ ]]);
21818
21920
  }
21921
+ yield* oldGrants.retire(kept);
21819
21922
  return {
21820
21923
  owner: state.owner,
21821
21924
  kept,
@@ -21827,7 +21930,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21827
21930
  };
21828
21931
  });
21829
21932
  const stopped = Effect.gen(function* () {
21830
- yield* Console.error("Stopping: aborting the request.");
21933
+ yield* printBlocks([["Stopping: aborting the request."]]);
21831
21934
  const abortId = yield* newRequestId;
21832
21935
  const state = yield* collector.machineAttachments.abort({
21833
21936
  params,
@@ -21846,18 +21949,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21846
21949
  times: 2
21847
21950
  }), Effect.catchTag("Unanswered", () => sayFailed("Couldn’t reach ref0 to abort it. If it’s approved anyway, ref0 revokes its grants 10 minutes after they connect, since this machine never kept them.")));
21848
21951
  if (!isEnded(state)) {
21849
- if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed("It was already answered, but no code was pasted. Nothing was kept here; its unconfirmed grants expire.");
21850
- yield* Console.error("It was already answered, so this machine waits to hear how.");
21952
+ if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", asking.some((folder) => Option.isSome(folder.replaces)) ? "Its new grants expire unconfirmed, and each folder’s connection from before still works." : "Its new grants expire unconfirmed."]]);
21953
+ yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
21851
21954
  }
21852
21955
  const connected = yield* finish(isEnded(state) ? state : yield* answered);
21853
21956
  yield* printBlocks([[{ label: `Connected as ${connected.owner}’s agents` }, ...connected.kept.map((folder) => `${shownConfigFolder(home, folder.configFolder)} ${folder.handle}`)], ["Stopped, so nothing was installed. To install ref0’s plugin into each, run:", ...connected.kept.map((folder) => ({ command: installLine(folder.configFolder) }))]]);
21854
21957
  });
21855
- const typedCode = Effect.gen(function* () {
21958
+ const readAnswer = Effect.gen(function* () {
21959
+ if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21856
21960
  const keys = yield* terminal.readInput;
21857
21961
  let typed = "";
21858
21962
  while (true) {
21859
21963
  const pressed = yield* Effect.option(Queue.take(keys));
21860
21964
  if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
21965
+ if ((yield* Ref.get(pasted)) !== void 0) continue;
21861
21966
  const { input, key } = pressed.value;
21862
21967
  if (key.name === "backspace") {
21863
21968
  typed = takeBack(typed);
@@ -21871,17 +21976,14 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21871
21976
  const line = typed.trim();
21872
21977
  typed = "";
21873
21978
  if (line === "") continue;
21874
- if (isAnswerCode(line)) return line;
21875
- yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21979
+ if (!isAnswerCode(line)) {
21980
+ yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21981
+ continue;
21982
+ }
21983
+ yield* Ref.update(pasted, (first) => first ?? line);
21984
+ if (listener !== void 0) yield* listener.stop;
21876
21985
  }
21877
21986
  }).pipe(Effect.scoped);
21878
- const readAnswer = Effect.gen(function* () {
21879
- if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21880
- const code = yield* typedCode;
21881
- yield* Ref.update(pasted, (first) => first ?? code);
21882
- if (listener !== void 0) yield* listener.stop;
21883
- return yield* Effect.never;
21884
- });
21885
21987
  const receiveAnswer = listener === void 0 ? readAnswer : Effect.raceFirst(readAnswer, listener.answer.pipe(Effect.flatMap((code) => Ref.update(pasted, (first) => first ?? code)), Effect.andThen(Effect.never)));
21886
21988
  return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.ensuring(endWait), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
21887
21989
  }, Effect.scoped);
@@ -22861,6 +22963,111 @@ function parse(toml, options = {}) {
22861
22963
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22862
22964
  */
22863
22965
  //#endregion
22966
+ //#region src/harness/empty-folder.ts
22967
+ /**
22968
+ * The folder one of a harness's own commands runs in: made empty for it in the system's temporary
22969
+ * folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
22970
+ * runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
22971
+ *
22972
+ * Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
22973
+ * from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
22974
+ * `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
22975
+ * ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
22976
+ * 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
22977
+ * a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
22978
+ */
22979
+ const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
22980
+ //#endregion
22981
+ //#region src/harness/codex-app-server.ts
22982
+ /**
22983
+ * Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
22984
+ * from an empty folder of its own, the requests written a line each, the answers read until each
22985
+ * request has one, then stopped. Each answer is matched to its request by ID, and read by that
22986
+ * request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
22987
+ * didn't send, is passed over.
22988
+ */
22989
+ /** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
22990
+ var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
22991
+ /** Codex's app server answered a request with an error. */
22992
+ var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
22993
+ method: Schema.String,
22994
+ error: CodexError
22995
+ }) {};
22996
+ /** Codex's app server answered a request with a result its method's schema doesn't read. */
22997
+ var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
22998
+ method: Schema.String,
22999
+ issue: Schema.String
23000
+ }) {};
23001
+ const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
23002
+ const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
23003
+ /** The IDs of the exchange's two requests: the opening's, and the one asked. */
23004
+ const initializeId = "ref0-initialize";
23005
+ const askedId = "ref0-asked";
23006
+ /** The exchange's opening: who's asking, then that it's ready. */
23007
+ const opening = [{
23008
+ jsonrpc: "2.0",
23009
+ id: initializeId,
23010
+ method: codexMethods.initialize.method,
23011
+ params: { clientInfo: {
23012
+ name: "ref0",
23013
+ title: null,
23014
+ version: cliVersion
23015
+ } }
23016
+ }, {
23017
+ jsonrpc: "2.0",
23018
+ method: codexMethods.initialized.method
23019
+ }];
23020
+ /**
23021
+ * Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
23022
+ * answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
23023
+ */
23024
+ const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
23025
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23026
+ const fs = yield* FileSystem.FileSystem;
23027
+ const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
23028
+ cwd: yield* emptyFolder(fs),
23029
+ env,
23030
+ extendEnv: true,
23031
+ forceKillAfter: "2 seconds",
23032
+ stdin: {
23033
+ stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
23034
+ endOnDone: false
23035
+ }
23036
+ }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
23037
+ return Option.filter(answered, (answers) => answers.size === ids.length);
23038
+ })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
23039
+ /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
23040
+ const resultOf = (method, answer) => {
23041
+ if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
23042
+ if ("error" in answer) return Effect.fail(new CodexRefused({
23043
+ method: method.method,
23044
+ error: answer.error
23045
+ }));
23046
+ const { result } = method;
23047
+ return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
23048
+ method: method.method,
23049
+ issue: error.message
23050
+ })));
23051
+ };
23052
+ /**
23053
+ * One exchange with Codex's app server under the person's Codex home: the opening, then `method`
23054
+ * asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
23055
+ * its request's ID, and its result is read by the method's result schema.
23056
+ */
23057
+ const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
23058
+ const asked = {
23059
+ jsonrpc: "2.0",
23060
+ id: askedId,
23061
+ method: method.method,
23062
+ params
23063
+ };
23064
+ const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
23065
+ const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
23066
+ if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
23067
+ yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
23068
+ return yield* resultOf(method, answers.value.get(askedId));
23069
+ });
23070
+ //#endregion
22864
23071
  //#region src/harness/codex-checkouts.ts
22865
23072
  const codexGitFolder = (configFolder, pluginId) => {
22866
23073
  const at = pluginId.indexOf("@");
@@ -22977,111 +23184,6 @@ const codexSignOff = [
22977
23184
  `perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
22978
23185
  ];
22979
23186
  //#endregion
22980
- //#region src/harness/empty-folder.ts
22981
- /**
22982
- * The folder one of a harness's own commands runs in: made empty for it in the system's temporary
22983
- * folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
22984
- * runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
22985
- *
22986
- * Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
22987
- * from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
22988
- * `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
22989
- * ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
22990
- * 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
22991
- * a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
22992
- */
22993
- const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
22994
- //#endregion
22995
- //#region src/harness/codex-app-server.ts
22996
- /**
22997
- * Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
22998
- * from an empty folder of its own, the requests written a line each, the answers read until each
22999
- * request has one, then stopped. Each answer is matched to its request by ID, and read by that
23000
- * request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
23001
- * didn't send, is passed over.
23002
- */
23003
- /** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
23004
- var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
23005
- /** Codex's app server answered a request with an error. */
23006
- var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
23007
- method: Schema.String,
23008
- error: CodexError
23009
- }) {};
23010
- /** Codex's app server answered a request with a result its method's schema doesn't read. */
23011
- var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
23012
- method: Schema.String,
23013
- issue: Schema.String
23014
- }) {};
23015
- const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
23016
- const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
23017
- /** The IDs of the exchange's two requests: the opening's, and the one asked. */
23018
- const initializeId = "ref0-initialize";
23019
- const askedId = "ref0-asked";
23020
- /** The exchange's opening: who's asking, then that it's ready. */
23021
- const opening = [{
23022
- jsonrpc: "2.0",
23023
- id: initializeId,
23024
- method: codexMethods.initialize.method,
23025
- params: { clientInfo: {
23026
- name: "ref0",
23027
- title: null,
23028
- version: cliVersion
23029
- } }
23030
- }, {
23031
- jsonrpc: "2.0",
23032
- method: codexMethods.initialized.method
23033
- }];
23034
- /**
23035
- * Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
23036
- * answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
23037
- */
23038
- const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
23039
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23040
- const fs = yield* FileSystem.FileSystem;
23041
- const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
23042
- cwd: yield* emptyFolder(fs),
23043
- env,
23044
- extendEnv: true,
23045
- forceKillAfter: "2 seconds",
23046
- stdin: {
23047
- stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
23048
- endOnDone: false
23049
- }
23050
- }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
23051
- return Option.filter(answered, (answers) => answers.size === ids.length);
23052
- })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
23053
- /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
23054
- const resultOf = (method, answer) => {
23055
- if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
23056
- if ("error" in answer) return Effect.fail(new CodexRefused({
23057
- method: method.method,
23058
- error: answer.error
23059
- }));
23060
- const { result } = method;
23061
- return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
23062
- method: method.method,
23063
- issue: error.message
23064
- })));
23065
- };
23066
- /**
23067
- * One exchange with Codex's app server under the person's Codex home: the opening, then `method`
23068
- * asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
23069
- * its request's ID, and its result is read by the method's result schema.
23070
- */
23071
- const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
23072
- const asked = {
23073
- jsonrpc: "2.0",
23074
- id: askedId,
23075
- method: method.method,
23076
- params
23077
- };
23078
- const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
23079
- const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
23080
- if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
23081
- yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
23082
- return yield* resultOf(method, answers.value.get(askedId));
23083
- });
23084
- //#endregion
23085
23187
  //#region src/harness/commit-hooks.ts
23086
23188
  /**
23087
23189
  * ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
@@ -23391,21 +23493,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
23391
23493
  parameters: shellWord(`include.path=${file}`)
23392
23494
  };
23393
23495
  });
23496
+ /** Only the single ref0 include installed by this CLI belongs to ref0. */
23497
+ const selectedInclude = (set) => {
23498
+ const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
23499
+ const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
23500
+ return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
23501
+ };
23502
+ const decodePolicy = Schema.decodeUnknownEffect(Schema.Struct({ shell_environment_policy: CodexConfigRead.fields.config.fields.shell_environment_policy }));
23503
+ /**
23504
+ * The agent's include a Codex config folder's `config.toml` selects, if it's ref0's: its file, read
23505
+ * as TOML for its `shell_environment_policy` alone.
23506
+ */
23507
+ const selectedCodexInclude = Effect.fnUntraced(function* (configFolder) {
23508
+ const fs = yield* FileSystem.FileSystem;
23509
+ const configFile = `${configFolder}/config.toml`;
23510
+ if (!(yield* fs.exists(configFile))) return Option.none();
23511
+ const text = yield* fs.readFileString(configFile);
23512
+ const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
23513
+ const read = yield* decodePolicy(document);
23514
+ return Option.fromNullishOr(selectedInclude(read.shell_environment_policy?.set ?? {}));
23515
+ });
23516
+ /**
23517
+ * Takes the author out of `selected`, the include a folder's `config.toml` selects, unless it's
23518
+ * this origin's include for the folder's agent, so a folder switched to another agent never names
23519
+ * the one before. Its commits name the person until an install selects the agent's own. Whether it
23520
+ * took one out.
23521
+ */
23522
+ const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, selected, options) {
23523
+ const name = yield* originPluginName((yield* Ref0Config).api);
23524
+ if (selected === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return false;
23525
+ const oldRoot = selected.slice(0, selected.lastIndexOf("/agents/"));
23526
+ const fallback = yield* codexAgentConfig(agent, oldRoot, options.home);
23527
+ yield* writeCodexCheckout({
23528
+ gitFolder: oldRoot,
23529
+ common: "",
23530
+ folder: "",
23531
+ signsOff: false,
23532
+ clearAuthor: selected,
23533
+ author: fallback
23534
+ });
23535
+ return true;
23536
+ });
23537
+ /** What the agent reads at its session's start when its folder's `config.toml` names another. */
23538
+ const otherAgentWords = (configFolder) => `ref0: The config.toml of this Codex config folder, ${configFolder}, names another agent than the one this folder is connected as, so your commits name the person instead of you until they run, in their own terminal: ${installLine(configFolder)}`;
23394
23539
  /**
23395
23540
  * Codex has no environment file: its install selects a file; session start fills its checkout,
23396
- * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`.
23541
+ * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`. An include
23542
+ * `config.toml` selects for another agent, after a switch whose install never finished or another
23543
+ * origin's install, has its author taken out first, and the agent hears so.
23397
23544
  */
23398
23545
  const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
23399
23546
  const folders = yield* ConfigFolders;
23400
23547
  const fs = yield* FileSystem.FileSystem;
23401
23548
  const path = yield* Path.Path;
23402
23549
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23403
- const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex")).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23550
+ const configFolder = yield* folders.folder("codex");
23551
+ const selected = yield* selectedCodexInclude(configFolder).pipe(Effect.mapError(() => new CodexCommitFailed({ detail: `Couldn’t read Codex’s config, ${configFolder}/config.toml.` })), Effect.result);
23552
+ const agent = yield* (yield* HostGrant).get(configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23404
23553
  if (Option.isNone(agent)) return false;
23554
+ const home = yield* Config.String("HOME");
23555
+ if (yield* Option.match(yield* Effect.fromResult(selected), {
23556
+ onNone: () => Effect.succeed(false),
23557
+ onSome: (file) => clearOtherCodexAuthor(agent.value, file, {
23558
+ configFolder,
23559
+ home
23560
+ })
23561
+ })) yield* Console.log(otherAgentWords(configFolder));
23405
23562
  const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
23406
23563
  if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
23407
23564
  const gitFolder = path.join(path.dirname(pluginRoot), "@git");
23408
- const home = yield* Config.String("HOME");
23409
23565
  const author = yield* codexAgentConfig(agent.value, gitFolder, home);
23410
23566
  const common = (yield* spawner.string(ChildProcess.make("git", [
23411
23567
  "-C",
@@ -23761,35 +23917,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
23761
23917
  //#region src/harness/codex-install-commits.ts
23762
23918
  const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
23763
23919
  const sameJson = Schema.toEquivalence(Schema.Json);
23764
- /** Only the single ref0 include installed by this CLI belongs to ref0. */
23765
- const selectedInclude = (set) => {
23766
- const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
23767
- const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
23768
- return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
23920
+ /**
23921
+ * What the person reads when an install that never finished took the agent before out of a
23922
+ * folder's commits: what stopped it, in its own words, a conflict only when there is one, then
23923
+ * what to run.
23924
+ */
23925
+ const unfinishedWords = (stopped, configFolder) => {
23926
+ return `ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. ${Option.match(stopped, {
23927
+ onNone: () => `The install stopped before it finished. To finish it, run: ${installLine(configFolder)}`,
23928
+ onSome: (failed) => `${failed.says} Then run: ${repairOf(failed, configFolder)}`
23929
+ })}`;
23769
23930
  };
23770
- /** A changed grant must never leave the previous agent as author after a failed install. */
23771
- const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, options) {
23772
- const fs = yield* FileSystem.FileSystem;
23773
- const configFile = `${options.configFolder}/config.toml`;
23774
- if (!(yield* fs.exists(configFile))) return;
23775
- const text = yield* fs.readFileString(configFile);
23776
- const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
23777
- const read = yield* decodeProfile({ config: document });
23778
- const file = selectedInclude(read.config.shell_environment_policy?.set ?? {});
23779
- if (file === void 0) return;
23780
- const name = yield* originPluginName((yield* Ref0Config).api);
23781
- if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
23782
- const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
23783
- const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
23784
- yield* writeCodexCheckout({
23785
- gitFolder: oldRoot,
23786
- common: "",
23787
- folder: "",
23788
- signsOff: false,
23789
- clearAuthor: file,
23790
- author: fallback
23791
- });
23792
- yield* Console.error(`ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. Fix the shell_environment_policy conflict in Codex’s config or profile named above: remove conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded. Then run: ${installLine(options.configFolder)}`);
23931
+ /**
23932
+ * After an install into a Codex folder fails or is interrupted, the agent before is taken out of
23933
+ * the include `config.toml` still selects, so the folder's commits never name an agent other than
23934
+ * its grant's, and the person hears what stopped the install: `stopped`, or none when interrupted.
23935
+ */
23936
+ const clearUnfinishedCodexInstall = Effect.fnUntraced(function* (agent, stopped, options) {
23937
+ const selected = yield* selectedCodexInclude(options.configFolder);
23938
+ if (Option.isNone(selected)) return;
23939
+ if (!(yield* clearOtherCodexAuthor(agent, selected.value, options))) return;
23940
+ yield* printBlocks([[unfinishedWords(stopped, options.configFolder)]]);
23793
23941
  });
23794
23942
  /** Install the connected agent's include and shell policy. */
23795
23943
  const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
@@ -23798,6 +23946,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23798
23946
  CODEX_HOME: options.configFolder
23799
23947
  };
23800
23948
  const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
23949
+ const conflicted = new InstallFailed({ says: "Codex’s shell_environment_policy conflicts with ref0’s commit environment, in the config or profile named above. Remove the conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded." });
23801
23950
  return yield* Effect.gen(function* () {
23802
23951
  const fs = yield* FileSystem.FileSystem;
23803
23952
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -23825,8 +23974,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23825
23974
  ].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
23826
23975
  const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
23827
23976
  if (collision !== void 0 || allowlist) {
23828
- yield* Console.error(`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`);
23829
- if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
23977
+ yield* printBlocks([[`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`]]);
23978
+ if (own) return yield* conflicted;
23830
23979
  return;
23831
23980
  }
23832
23981
  const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
@@ -23851,8 +24000,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23851
24000
  const losesAuthors = excluded !== void 0 && excluded !== null && ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].some((name) => !excluded.some((pattern) => pathMatchesWithin(pattern.toLowerCase(), name.toLowerCase(), { left: 1e4 })));
23852
24001
  const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
23853
24002
  if (key === void 0 && !losesAuthors && !allowlist) continue;
23854
- yield* Console.error(`ref0: Codex’s profile ${printable(file)} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`);
23855
- if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
24003
+ yield* printBlocks([[`ref0: Codex’s profile ${file} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`]]);
24004
+ if (own) return yield* conflicted;
23856
24005
  return;
23857
24006
  }
23858
24007
  if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
@@ -23940,9 +24089,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23940
24089
  }, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
23941
24090
  }).pipe(Effect.onError(() => Effect.gen(function* () {
23942
24091
  if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
23943
- else yield* Console.error("ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again.");
24092
+ else yield* printBlocks([["ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again."]]);
23944
24093
  }).pipe(Effect.orDie)));
23945
- }).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
24094
+ }).pipe(Effect.scoped, Effect.mapError((error) => error instanceof InstallFailed ? error : failed("set Codex’s commit environment")));
23946
24095
  }, Effect.scoped);
23947
24096
  //#endregion
23948
24097
  //#region src/harness/install-claude-code.ts
@@ -24106,7 +24255,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
24106
24255
  trusted,
24107
24256
  profile
24108
24257
  };
24109
- }).pipe(Effect.onError(() => clearOtherCodexAuthor(agent, options).pipe(Effect.orDie)));
24258
+ }).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
24110
24259
  });
24111
24260
  //#endregion
24112
24261
  //#region src/harness/install.ts
@@ -24212,7 +24361,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24212
24361
  for (const folder of into) answers.set(folder, Result.fail(failed));
24213
24362
  };
24214
24363
  const started = /* @__PURE__ */ new Set();
24215
- const clearUnstarted = (into) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearOtherCodexAuthor(agent, {
24364
+ const clearUnstarted = (into, stopped) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearUnfinishedCodexInstall(agent, stopped, {
24216
24365
  configFolder: folder.configFolder,
24217
24366
  home
24218
24367
  }).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
@@ -24227,8 +24376,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24227
24376
  if (into.length === 0) continue;
24228
24377
  const command = yield* onPath(harness === "codex" ? "codex" : "claude");
24229
24378
  if (Option.isNone(command)) {
24230
- failAll(into, new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` }));
24231
- yield* clearUnstarted(into);
24379
+ const missing = new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` });
24380
+ failAll(into, missing);
24381
+ yield* clearUnstarted(into, Option.some(missing));
24232
24382
  continue;
24233
24383
  }
24234
24384
  yield* Effect.scoped(Effect.gen(function* () {
@@ -24249,9 +24399,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24249
24399
  })).pipe(Effect.result);
24250
24400
  answers.set(folder, installed);
24251
24401
  }
24252
- }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
24402
+ }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into, Option.some(failed)))))));
24253
24403
  }
24254
- }).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
24404
+ }).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
24255
24405
  return folders.map((folder) => ({
24256
24406
  ...folder,
24257
24407
  installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
@@ -24343,16 +24493,13 @@ const mostFolders = 8;
24343
24493
  const isConfigFolderPath = Schema.is(ConfigFolderPath);
24344
24494
  /** The line that asks again. */
24345
24495
  const again = "ref0 connect";
24346
- /** The widest a line of ref0's own words runs, as its help does, in a terminal at least as wide. */
24347
- const mostColumns = 100;
24348
24496
  /**
24349
24497
  * Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. The
24350
- * question is said first, wrapped between words to the terminal, and the chooser shows only the
24351
- * folders: once Enter chooses, it leaves the ticked ones on one line after any question it was
24352
- * given, which the terminal would break mid-word.
24498
+ * question is said first, and the chooser shows only the folders: once Enter chooses, it leaves
24499
+ * the ticked ones on one line after any question it was given, which the terminal would break
24500
+ * mid-word.
24353
24501
  */
24354
24502
  const choose = Effect.fnUntraced(function* (found, home) {
24355
- const columns = yield* (yield* Terminal.Terminal).columns;
24356
24503
  const choices = found.map((folder) => {
24357
24504
  const shown = shownConfigFolder(home, folder.configFolder);
24358
24505
  const sendable = isConfigFolderPath(shown);
@@ -24369,7 +24516,7 @@ const choose = Effect.fnUntraced(function* (found, home) {
24369
24516
  });
24370
24517
  const ticked = choices.filter((choice) => choice.selected).length;
24371
24518
  const keys = "Space ticks or unticks one; Enter connects those ticked.";
24372
- yield* printBlocks([[{ label: "Connect which config folders?" }, ...wrapWords(ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys, columns > 0 ? Math.min(columns, mostColumns) : mostColumns)]]);
24519
+ yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
24373
24520
  return yield* Prompt.run(Prompt.MultiSelect({
24374
24521
  choices,
24375
24522
  max: mostFolders
@@ -24562,7 +24709,7 @@ const signalLines = (answer) => {
24562
24709
  * credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
24563
24710
  * thread in the project sends a signal to one of its connections by name.
24564
24711
  */
24565
- const failedWith$14 = (failed) => sayFailed(failed.nextStep);
24712
+ const failedWith$15 = (failed) => sayFailed(failed.nextStep);
24566
24713
  const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
24567
24714
  const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
24568
24715
  const list$5 = Command.make("list", {
@@ -24574,7 +24721,7 @@ const list$5 = Command.make("list", {
24574
24721
  params: projectParts(address)
24575
24722
  });
24576
24723
  yield* printAnswer(answer.json, asJson, connectionLines(answer.value));
24577
- }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("The project’s connections, their last signal and what they dropped"), Command.withShortDescription("List a project’s connections and their last signals"));
24724
+ }).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("The project’s connections, their last signal and what they dropped"), Command.withShortDescription("List a project’s connections and their last signals"));
24578
24725
  /** A change made on the project's page: its link, and what to do there. */
24579
24726
  const onThePage = (address, change) => Effect.gen(function* () {
24580
24727
  const config = yield* Ref0Config;
@@ -24650,14 +24797,14 @@ const send = Command.make("send", {
24650
24797
  })
24651
24798
  });
24652
24799
  yield* printAnswer(answer.json, asJson, signalLines(answer.value));
24653
- }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
24800
+ }).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
24654
24801
  /** A project's connections, and sending a signal by one. */
24655
24802
  const connectionCommands = [connectionCommand, Command.make("signal").pipe(Command.withDescription("A project’s signals: send one by a connection"), Command.withShortDescription("Send a signal to a project"), Command.withSubcommands([send]))];
24656
24803
  //#endregion
24657
24804
  //#region src/harness/checkout.ts
24658
24805
  const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
24659
24806
  const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
24660
- const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
24807
+ const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
24661
24808
  const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
24662
24809
  var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24663
24810
  static layer = Layer.effect(Checkout, Effect.gen(function* () {
@@ -24696,7 +24843,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24696
24843
  "config",
24697
24844
  "--get",
24698
24845
  "ref0.thread"
24699
- ]), (written) => decodeThread$1(written.trim()));
24846
+ ]), (written) => decodeThread$2(written.trim()));
24700
24847
  const repoAt = (folder) => Effect.map(git(folder, [
24701
24848
  "config",
24702
24849
  "--get",
@@ -25699,7 +25846,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25699
25846
  })).value;
25700
25847
  if (code.compiled.entry.reaches?.browser === true) return yield* sayFailed(`${code.check} browses, and only a run on ref0 browses: run it there, with ref0 run ${check} --thread ${thread.value}.`);
25701
25848
  if (code.runs.length === 0) {
25702
- yield* Console.error(`${code.check} has no part left for a local run on version ${code.version}: each is met, or failed on ref0. See ref0 gate.`);
25849
+ yield* printBlocks([[`${code.check} has no part left for a local run on version ${code.version}: each is met, or failed on ref0. See ref0 gate.`]]);
25703
25850
  return;
25704
25851
  }
25705
25852
  yield* holdsVersion(root.value, code);
@@ -25708,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25708
25855
  thread: `${thread.value}’s own .ref0/`,
25709
25856
  project: "the project’s checks"
25710
25857
  }[code.code.origin];
25711
- yield* Console.error(`Running ${code.check} from ${from}, as ref0 compiled it for version ${code.version}: its code and its sandboxes’ setup run on this machine, as you, outside any container.`);
25858
+ yield* printBlocks([[`Running ${code.check} from ${from}, as ref0 compiled it for version ${code.version}: its code and its sandboxes’ setup run on this machine, as you, outside any container.`]]);
25712
25859
  const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
25713
25860
  const sandboxes = code.compiled.entry.sandboxes ?? [];
25714
25861
  const held = /* @__PURE__ */ new Set();
@@ -25720,7 +25867,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25720
25867
  const run = yield* newRunId;
25721
25868
  const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
25722
25869
  const sandbox = yield* localOpener(root.value, held, sandboxes, deadline);
25723
- yield* Console.error(`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`);
25870
+ yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
25724
25871
  const ran = yield* runPart(own, {
25725
25872
  params,
25726
25873
  host: treeHost(root.value, code.tree),
@@ -25783,7 +25930,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
25783
25930
  /** Says a waiting start's tree on stderr, as it stands now. */
25784
25931
  const sayTree = Effect.fnUntraced(function* (wait) {
25785
25932
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25786
- yield* Console.error(printable(waitingLines$1(wait, now)));
25933
+ yield* printBlocks([[waitingLines$1(wait, now)]]);
25787
25934
  });
25788
25935
  /**
25789
25936
  * Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
@@ -25829,7 +25976,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
25829
25976
  * reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
25830
25977
  * at a time.
25831
25978
  */
25832
- const failedWith$13 = (failed) => sayFailed(failed.nextStep);
25979
+ const failedWith$14 = (failed) => sayFailed(failed.nextStep);
25833
25980
  const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
25834
25981
  const decodeTraced = Schema.decodeUnknownOption(TracedRun);
25835
25982
  /** A run named by a flag, or a failure that says how to name one. */
@@ -25931,7 +26078,7 @@ const run = Command.make("run", {
25931
26078
  }, asJson, rerunCommand);
25932
26079
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25933
26080
  yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25934
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Run a check on main, or on a thread’s head: a run it ran already is answered, not run again, unless the check says reuse: false. A start its check’s start rule makes wait shows what it waits on, and waits up to 9 minutes for it, unless --no-wait. With --local, run a check of the thread’s open version on this machine, and report it"), Command.withShortDescription("Run a check on main or a thread’s head"));
26081
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Run a check on main, or on a thread’s head: a run it ran already is answered, not run again, unless the check says reuse: false. A start its check’s start rule makes wait shows what it waits on, and waits up to 9 minutes for it, unless --no-wait. With --local, run a check of the thread’s open version on this machine, and report it"), Command.withShortDescription("Run a check on main or a thread’s head"));
25935
26082
  const trace = Command.make("trace", {
25936
26083
  run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
25937
26084
  repo: repoFlag,
@@ -25974,7 +26121,7 @@ const trace = Command.make("trace", {
25974
26121
  ...narrowed,
25975
26122
  `--cursor ${next}`
25976
26123
  ].join(" ")));
25977
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A run’s trace, a check run’s or a workflow run’s: its steps and its log lines, a page at a time"), Command.withShortDescription("Show a run’s trace and log lines"));
26124
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A run’s trace, a check run’s or a workflow run’s: its steps and its log lines, a page at a time"), Command.withShortDescription("Show a run’s trace and log lines"));
25978
26125
  const waiting$1 = Command.make("waiting", {
25979
26126
  start: startArgument,
25980
26127
  repo: repoFlag,
@@ -26009,7 +26156,7 @@ const waiting$1 = Command.make("waiting", {
26009
26156
  }, asJson, rerunCommand);
26010
26157
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
26011
26158
  yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
26012
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A start of yours its check’s start rule made wait: what it waits on, as a tree, waiting up to 9 minutes for it to go on or end, unless --no-wait"), Command.withShortDescription("Show what a waiting start waits on"));
26159
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A start of yours its check’s start rule made wait: what it waits on, as a tree, waiting up to 9 minutes for it to go on or end, unless --no-wait"), Command.withShortDescription("Show what a waiting start waits on"));
26013
26160
  /** What a cancel did, in words. */
26014
26161
  const cancelledText = {
26015
26162
  start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
@@ -26035,7 +26182,7 @@ const cancel = Command.make("cancel", {
26035
26182
  })
26036
26183
  });
26037
26184
  yield* printAnswer(answer.json, asJson, cancelledText[answer.value.cancelled]);
26038
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Cancel a start of yours that waits: yours ends it and the workflow runs it waits on; one that joined another’s only leaves it"), Command.withShortDescription("Cancel a waiting start of yours"));
26185
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Cancel a start of yours that waits: yours ends it and the workflow runs it waits on; one that joined another’s only leaves it"), Command.withShortDescription("Cancel a waiting start of yours"));
26039
26186
  /** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
26040
26187
  const runCommands = [run, Command.make("runs", {
26041
26188
  repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
@@ -26077,14 +26224,14 @@ const runCommands = [run, Command.make("runs", {
26077
26224
  ...narrowed,
26078
26225
  `--before ${next}`
26079
26226
  ].join(" ")));
26080
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A repo’s check runs, main’s or a thread’s, the newest first, a page at a time"), Command.withShortDescription("List a repo’s check runs"), Command.withSubcommands([
26227
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A repo’s check runs, main’s or a thread’s, the newest first, a page at a time"), Command.withShortDescription("List a repo’s check runs"), Command.withSubcommands([
26081
26228
  trace,
26082
26229
  waiting$1,
26083
26230
  cancel
26084
26231
  ]))];
26085
26232
  //#endregion
26086
26233
  //#region src/commands/checks.ts
26087
- const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26234
+ const failedWith$13 = (failed) => sayFailed(failed.nextStep);
26088
26235
  const decodeCheck = Schema.decodeUnknownOption(CheckKey);
26089
26236
  const decodeToken = Schema.decodeUnknownOption(TokenId);
26090
26237
  const decodePerson = Schema.decodeUnknownOption(OwnerName);
@@ -26120,7 +26267,7 @@ const restore = Command.make("restore", {
26120
26267
  }
26121
26268
  });
26122
26269
  yield* printAnswer(answer.json, asJson, restoredWords(answer.value, repo));
26123
- }).pipe(Effect.catchTag("CallFailed", failedWith$12))).pipe(Command.withDescription("Restore a signer a miss revoked from answering a check, as an admin: its local runs, or their yes, count again"), Command.withShortDescription("Restore a revoked check signer"));
26270
+ }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Restore a signer a miss revoked from answering a check, as an admin: its local runs, or their yes, count again"), Command.withShortDescription("Restore a revoked check signer"));
26124
26271
  const signers = Command.make("signers").pipe(Command.withDescription("Who signs a check’s local runs or says yes over its runs: restore one a miss revoked"), Command.withShortDescription("Manage a check’s signers"), Command.withSubcommands([restore]));
26125
26272
  /** `ref0 checks`: a repo's checks. */
26126
26273
  const checksCommands = [Command.make("checks").pipe(Command.withDescription("A repo’s checks: the signers of their local runs and yeses"), Command.withShortDescription("Manage a repo’s check signers"), Command.withSubcommands([signers]))];
@@ -26452,7 +26599,7 @@ const projectGateLines = (view, repo) => {
26452
26599
  };
26453
26600
  //#endregion
26454
26601
  //#region src/commands/gate.ts
26455
- const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26602
+ const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26456
26603
  const project$4 = Flag.Boolean("project").pipe(Flag.withDescription("The project gate instead: its active revision, and a pending one with each latch it waits on"), Flag.withDefault(false));
26457
26604
  /** A pull request's gate. */
26458
26605
  const gateCommands = [Command.make("gate", {
@@ -26478,7 +26625,7 @@ const gateCommands = [Command.make("gate", {
26478
26625
  }
26479
26626
  });
26480
26627
  yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
26481
- }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("A pull request’s needs, each with where it came from, ref0’s floor or a line of .ref0/gate.ts, and whether it’s met or on whom it waits; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
26628
+ }).pipe(Effect.catchTag("CallFailed", failedWith$12))).pipe(Command.withDescription("A pull request’s needs, each with where it came from, ref0’s floor or a line of .ref0/gate.ts, and whether it’s met or on whom it waits; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
26482
26629
  //#endregion
26483
26630
  //#region src/clone.ts
26484
26631
  /** The remote a clone pushes to: its thread's fork path, named as the thread is. */
@@ -26836,7 +26983,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26836
26983
  * with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
26837
26984
  * thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
26838
26985
  */
26839
- const failedWith$10 = (failed) => sayFailed(failed.nextStep);
26986
+ const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26840
26987
  /** `ref0 git-credential`, as Git runs it: see `answerGit`. */
26841
26988
  const credential = Command.make("git-credential", { operation: Argument.String("operation").pipe(Argument.withDescription("What Git asks: get, store or erase")) }, ({ operation }) => Effect.gen(function* () {
26842
26989
  const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
@@ -26890,7 +27037,7 @@ const gitCommands = [
26890
27037
  value
26891
27038
  ])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
26892
27039
  yield* printAnswer(fork.json, asJson, `Cloned ${owner}/${name} into ${into}. origin is read-only to agents: git push goes to ${forkRemote(fork.value)}, the fork of whichever session on the thread pushes.`);
26893
- }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
27040
+ }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
26894
27041
  Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
26895
27042
  yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
26896
27043
  const synced = yield* syncBase(".");
@@ -26906,7 +27053,7 @@ const gitCommands = [
26906
27053
  on,
26907
27054
  commit
26908
27055
  }, asJson, `Merged ${at}. Push it when you’re ready: git push.`);
26909
- }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Merge in main, or the thread this one is stacked on, as ref0 holds it now. ref0 mcp says to when what merged touches your work"), Command.withShortDescription("Merge in main or the thread this one is stacked on")),
27056
+ }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Merge in main, or the thread this one is stacked on, as ref0 holds it now. ref0 mcp says to when what merged touches your work"), Command.withShortDescription("Merge in main or the thread this one is stacked on")),
26910
27057
  credential
26911
27058
  ];
26912
27059
  //#endregion
@@ -27319,7 +27466,7 @@ const usageCommands = [personUsage, personMeters];
27319
27466
  * invitations. Each is a person's, asked of them by approval, as every terminal command of
27320
27467
  * theirs is; agents never change members.
27321
27468
  */
27322
- const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27469
+ const failedWith$10 = (failed) => sayFailed(failed.nextStep);
27323
27470
  const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
27324
27471
  const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
27325
27472
  const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
@@ -27349,7 +27496,7 @@ const create$1 = Command.make("create", {
27349
27496
  payload
27350
27497
  });
27351
27498
  yield* printAnswer(answer.json, asJson, answer.value.state === "ready" ? `Made ${answer.value.name}.` : `Made ${answer.value.name}; it’s being set up.`);
27352
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Make an organization, with you as its admin"));
27499
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
27353
27500
  const leave = Command.make("leave", {
27354
27501
  organization: organization$2,
27355
27502
  json
@@ -27359,7 +27506,7 @@ const leave = Command.make("leave", {
27359
27506
  params: { owner: org }
27360
27507
  });
27361
27508
  yield* printAnswer(answer.json, asJson, `Left ${org}.`);
27362
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Leave an organization"));
27509
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
27363
27510
  const orgCommand = Command.make("org").pipe(Command.withDescription("Make an organization, leave one, or read its usage and what ref0 ran and kept for it"), Command.withShortDescription("Manage an organization and read its usage"), Command.withSubcommands([
27364
27511
  create$1,
27365
27512
  leave,
@@ -27376,7 +27523,7 @@ const orgs = Command.make("orgs", {
27376
27523
  });
27377
27524
  const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
27378
27525
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You’re in no organization yet." : [...lines, ...more(answer.value.next)].join("\n"));
27379
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
27526
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
27380
27527
  const members = Command.make("members", {
27381
27528
  organization: organization$2,
27382
27529
  cursor: cursor$3,
@@ -27389,7 +27536,7 @@ const members = Command.make("members", {
27389
27536
  });
27390
27537
  const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
27391
27538
  yield* printAnswer(answer.json, asJson, [...lines, ...more(answer.value.next)].join("\n"));
27392
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("An organization’s members, people and its own agents"), Command.withShortDescription("List an organization’s members"));
27539
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s members, people and its own agents"), Command.withShortDescription("List an organization’s members"));
27393
27540
  const role = Command.make("role", {
27394
27541
  organization: organization$2,
27395
27542
  person,
@@ -27405,7 +27552,7 @@ const role = Command.make("role", {
27405
27552
  payload: { role: chosen }
27406
27553
  });
27407
27554
  yield* printAnswer(answer.json, asJson, `${named} is ${answer.value.role} of ${org} now.`);
27408
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
27555
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
27409
27556
  const remove$1 = Command.make("remove", {
27410
27557
  organization: organization$2,
27411
27558
  person,
@@ -27419,7 +27566,7 @@ const remove$1 = Command.make("remove", {
27419
27566
  }
27420
27567
  });
27421
27568
  yield* printAnswer(answer.json, asJson, `Removed ${named} from ${org}.`);
27422
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
27569
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
27423
27570
  const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
27424
27571
  const listInvitations = Command.make("list", {
27425
27572
  organization: organization$2,
@@ -27433,7 +27580,7 @@ const listInvitations = Command.make("list", {
27433
27580
  });
27434
27581
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
27435
27582
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? `${org} has no open invitations.` : [...lines, ...more(answer.value.next)].join("\n"));
27436
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
27583
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
27437
27584
  const withdraw = Command.make("withdraw", {
27438
27585
  organization: organization$2,
27439
27586
  invitation,
@@ -27447,7 +27594,7 @@ const withdraw = Command.make("withdraw", {
27447
27594
  }
27448
27595
  });
27449
27596
  yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
27450
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Take an open invitation back"));
27597
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
27451
27598
  const invite = Command.make("invite", {
27452
27599
  organization: organization$2,
27453
27600
  person,
@@ -27467,7 +27614,7 @@ const invite = Command.make("invite", {
27467
27614
  payload
27468
27615
  });
27469
27616
  yield* printAnswer(answer.json, asJson, `Invited ${answer.value.invitee.name} to ${org} as ${answer.value.role}: ${answer.value.invitation}.`);
27470
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
27617
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
27471
27618
  const invitations = Command.make("invitations", {
27472
27619
  cursor: cursor$3,
27473
27620
  json
@@ -27478,7 +27625,7 @@ const invitations = Command.make("invitations", {
27478
27625
  });
27479
27626
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
27480
27627
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You have no open invitations." : [...lines, ...more(answer.value.next)].join("\n"));
27481
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
27628
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
27482
27629
  const accept = Command.make("accept", {
27483
27630
  invitation,
27484
27631
  json
@@ -27488,7 +27635,7 @@ const accept = Command.make("accept", {
27488
27635
  params: { invitation: id }
27489
27636
  });
27490
27637
  yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
27491
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Accept an invitation"));
27638
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
27492
27639
  const decline = Command.make("decline", {
27493
27640
  invitation,
27494
27641
  json
@@ -27498,7 +27645,7 @@ const decline = Command.make("decline", {
27498
27645
  params: { invitation: id }
27499
27646
  });
27500
27647
  yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
27501
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Decline an invitation"));
27648
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
27502
27649
  /** Organizations, their members and invitations. */
27503
27650
  const organizationCommands = [
27504
27651
  orgCommand,
@@ -27511,7 +27658,7 @@ const organizationCommands = [
27511
27658
  ];
27512
27659
  //#endregion
27513
27660
  //#region src/commands/grants.ts
27514
- const failedWith$8 = (failed) => sayFailed(failed.nextStep);
27661
+ const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27515
27662
  const isOwnerRef = Schema.is(OwnerRef);
27516
27663
  const isProject$1 = Schema.is(WrittenProject);
27517
27664
  const isThread = Schema.is(WrittenThread);
@@ -27633,7 +27780,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
27633
27780
  onSome: (on) => [on]
27634
27781
  })];
27635
27782
  yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
27636
- }).pipe(Effect.catchTag("CallFailed", failedWith$8)));
27783
+ }).pipe(Effect.catchTag("CallFailed", failedWith$9)));
27637
27784
  const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
27638
27785
  const grant = changeCommand("grant", "grants.set").pipe(Command.withDescription("Give a grant in an organization, as an admin: ref0 grant acme/billing pip/claude write"), Command.withShortDescription("Give a grant in an organization"), Command.withSubcommands([revoke]));
27639
27786
  /** An organization's grants. */
@@ -27653,7 +27800,41 @@ const grantCommands = [Command.make("grants", {
27653
27800
  const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
27654
27801
  const next = answer.value.nextAfter;
27655
27802
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? `You see no grants in ${org}.` : [...lines, ...next === void 0 ? [] : [`More: --cursor ${next}`]].join("\n"));
27656
- }).pipe(Effect.catchTag("CallFailed", failedWith$8))).pipe(Command.withDescription("The grants in an organization you may see"), Command.withShortDescription("List an organization’s grants")), grant];
27803
+ }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The grants in an organization you may see"), Command.withShortDescription("List an organization’s grants")), grant];
27804
+ //#endregion
27805
+ //#region src/commands/identity-token.ts
27806
+ /**
27807
+ * `ref0 identity-token --audience <origin>`: an identity token for the agent's session it runs
27808
+ * in, for one outside service, which verifies it against ref0's published keys. One request, by
27809
+ * the session's own token, which never starts or renews a session. The token alone goes to
27810
+ * stdout, so it pipes into a verifier; anything else goes to stderr, with no token, and fails.
27811
+ */
27812
+ const decodeAudience = Schema.decodeUnknownOption(IdentityAudience);
27813
+ const outside = "ref0 identity-token runs only in an agent’s ref0 session, and none runs here. In Claude Code or Codex, one starts at each prompt once ref0 connect has connected its config folder: ask the agent there.";
27814
+ const audienceOnce = "Name the audience once, as --audience https://verifier.example: the outside service’s HTTPS origin.";
27815
+ const notAnAudience = "Send the outside service’s HTTPS origin as the audience, such as https://verifier.example, with no path.";
27816
+ const mayHaveIssued = "ref0’s answer was lost or failed partway, so it may have made a token that never reached you. Ask again for a fresh one in a moment.";
27817
+ /**
27818
+ * What went wrong, said once, on stderr. An answer lost or failed partway may still have made a
27819
+ * token, as ref0's log records, but nothing to check or undo: a fresh request is the way on.
27820
+ */
27821
+ const failedWith$8 = (failed) => sayFailed(failed.status >= 500 || failed.status === 0 && (failed.kind === "Unreachable" || failed.kind === "Unexpected") ? mayHaveIssued : failed.nextStep);
27822
+ /** Identity tokens, for outside services. */
27823
+ const identityTokenCommands = [Command.make("identity-token", {
27824
+ audience: Flag.String("audience").pipe(Flag.withDescription("The outside service that verifies the token, as its HTTPS origin, such as https://verifier.example"), Flag.atLeast(0)),
27825
+ json
27826
+ }, ({ audience: named, json: asJson }) => Effect.gen(function* () {
27827
+ if (named.length !== 1) return yield* sayFailed(audienceOnce);
27828
+ const audience = decodeAudience(named[0]);
27829
+ if (Option.isNone(audience)) return yield* sayFailed(notAnAudience);
27830
+ yield* ownSession(outside);
27831
+ const issued = yield* callEndpoint({
27832
+ endpoint: "me.identityToken",
27833
+ payload: { audience: audience.value }
27834
+ });
27835
+ if (asJson) return yield* printAnswer(issued.json, true, "");
27836
+ yield* Console.log(issued.value.token);
27837
+ }).pipe(Effect.catchTag("CallFailed", failedWith$8))).pipe(Command.withDescription("Print a short-lived identity token, a signed JWT naming your agent, for one outside service"), Command.withShortDescription("Print an identity token for one outside service"))];
27657
27838
  //#endregion
27658
27839
  //#region src/commands/agents.ts
27659
27840
  /**
@@ -27928,27 +28109,39 @@ const repoCommands = [Command.make("repos", {
27928
28109
  //#endregion
27929
28110
  //#region src/decision-lines.ts
27930
28111
  /** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
27931
- /** What a decision is about, in words: its thread, and a gate's version of its pull request. */
27932
- const subjectWords = ({ subject, names }) => subject.kind === "version" && names.pullRequest !== void 0 ? `version ${subject.version} of ${names.pullRequest}, from ${names.thread}` : names.thread;
27933
- /** A decision in a line. */
27934
- const decisionLines = (decision) => {
27935
- const on = subjectWords(decision);
28112
+ /**
28113
+ * A thread as a line read in `scope` names it: `#3` when it's in that scope, else in full, as
28114
+ * `pip/default#3`.
28115
+ */
28116
+ const threadWords = (thread, scope) => thread.startsWith(`${scope}#`) ? thread.slice(scope.length) : thread;
28117
+ /**
28118
+ * What waits, in words: a gate's pull request and version, and the thread it's from, or the thread
28119
+ * an ask is on.
28120
+ */
28121
+ const subjectWords = ({ subject, names }, scope) => subject.kind === "version" && names.pullRequest !== void 0 ? `${names.pullRequest}, version ${subject.version}, from ${threadWords(names.thread, scope)}` : names.thread;
28122
+ /**
28123
+ * A decision in a line, read in `scope`: what waits, its answers or how it ended, then its ID, for
28124
+ * `ref0 decision` and `ref0 answer`.
28125
+ */
28126
+ const decisionLines = (decision, scope) => {
28127
+ const on = subjectWords(decision, scope);
27936
28128
  const answers = decision.options.map((option) => option.key).join(" or ");
27937
- return decision.state === "open" ? `${decision.decision} on ${on}: ${answers}` : `${decision.decision} on ${on}: ${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`;
28129
+ return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
27938
28130
  };
27939
- const nothing = "Nothing waits on you here.";
27940
28131
  /**
27941
- * What waits, as `ref0 waiting` prints it: what waits on the caller, then, for an agent, what
27942
- * waits on its person, which they answer on their own page.
28132
+ * What waits in `scope`, as `ref0 waiting` prints it: what waits on the caller, then, for an agent,
28133
+ * what waits on its person, which only they answer, on their own page.
27943
28134
  */
27944
- const waitingLines = (decisions) => {
27945
- const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision)] : []);
27946
- const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision)}`] : []);
27947
- if (theirs.length === 0) return yours.length === 0 ? nothing : yours.join("\n");
28135
+ const waitingLines = (decisions, scope) => {
28136
+ const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision, scope)] : []);
28137
+ const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision, scope)}`] : []);
28138
+ if (theirs.length === 0) return yours.length === 0 ? `Nothing waits on you in ${scope}.` : yours.join("\n");
28139
+ const them = theirs.length === 1 ? "it" : "them";
27948
28140
  return [
27949
28141
  ...yours,
27950
- "Waits on your person, under Waiting on you on their page:",
27951
- ...theirs
28142
+ `Waits on your person, who alone can answer ${them}, under Waiting on you on their page:`,
28143
+ ...theirs,
28144
+ `ref0 refuses an agent’s answer to ${them}.`
27952
28145
  ].join("\n");
27953
28146
  };
27954
28147
  //#endregion
@@ -27964,7 +28157,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
27964
28157
  const encodePlan = Schema.encodeEffect(PlanInput);
27965
28158
  const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
27966
28159
  const thread$4 = Argument.String("thread").pipe(Argument.withDescription("The thread, as owner/project#number, or an org thread, as acme#7"), Argument.withSchema(WrittenThread));
27967
- const project$2 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads"), Argument.withSchema(Schema.Union([WrittenProject, OwnerName])));
28160
+ /** A project, `acme/billing`, or an organization, `acme`, for its org threads. */
28161
+ const Scope$1 = Schema.Union([WrittenProject, OwnerName]);
28162
+ const decodeScope = Schema.decodeUnknownOption(Scope$1);
28163
+ const project$2 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads"), Argument.withSchema(Scope$1));
27968
28164
  const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
27969
28165
  const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
27970
28166
  /** Where a version's approval stands: who accepted it, or its check. */
@@ -28125,25 +28321,37 @@ const comments = Command.make("comments", {
28125
28321
  ].filter((line) => line !== "").join("\n");
28126
28322
  yield* printAnswer(answer.json, asJson, words);
28127
28323
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("A plan version’s comments, oldest first"), Command.withShortDescription("List a plan version’s comments"));
28324
+ const plan = Command.make("plan").pipe(Command.withDescription("A thread’s plan: write, propose and read its versions, and comment"), Command.withShortDescription("Write, propose and read a thread’s plan"), Command.withSubcommands([
28325
+ list$4,
28326
+ write,
28327
+ onVersion("show"),
28328
+ onVersion("propose"),
28329
+ comment,
28330
+ comments
28331
+ ]));
28332
+ /**
28333
+ * The project of the thread the session runs on, or its organization for an org thread's. Outside
28334
+ * a session, it says how to name one, and fails before anything's sent.
28335
+ */
28336
+ const sessionScope = Effect.gen(function* () {
28337
+ const thread = yield* (yield* SessionThread).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
28338
+ const scope = Option.flatMap(thread, (written) => decodeScope(projectOf$2(written)));
28339
+ if (Option.isNone(scope)) return yield* sayFailed("Name the project: ref0 waiting pip/shop. Or, for an organization’s org threads: ref0 waiting acme.");
28340
+ return scope.value;
28341
+ });
28128
28342
  /** Plans' and decisions' commands. */
28129
28343
  const planCommands = [
28130
- Command.make("plan").pipe(Command.withDescription("A thread’s plan: write, propose and read its versions, and comment"), Command.withShortDescription("Write, propose and read a thread’s plan"), Command.withSubcommands([
28131
- list$4,
28132
- write,
28133
- onVersion("show"),
28134
- onVersion("propose"),
28135
- comment,
28136
- comments
28137
- ])),
28344
+ plan,
28138
28345
  Command.make("waiting", {
28139
- project: project$2,
28346
+ project: Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads; in an agent’s session, its own if not named"), Argument.withSchema(Scope$1), Argument.optional),
28140
28347
  json
28141
- }, ({ project: address, json: asJson }) => Effect.gen(function* () {
28348
+ }, ({ project: named, json: asJson }) => Effect.gen(function* () {
28349
+ const address = Option.isSome(named) ? named.value : yield* sessionScope;
28142
28350
  const answer = yield* callEndpoint(routed(address, {
28143
28351
  endpoint: "decisions.waiting",
28144
28352
  params: scopeParams(address)
28145
28353
  }));
28146
- yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
28354
+ yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
28147
28355
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("What in a project, or in an organization’s org threads, waits on you"), Command.withShortDescription("Show what waits on you")),
28148
28356
  Command.make("decision", {
28149
28357
  project: project$2,
@@ -28157,7 +28365,7 @@ const planCommands = [
28157
28365
  decision: id
28158
28366
  }
28159
28367
  }));
28160
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28368
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
28161
28369
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("One decision, as it stands"), Command.withShortDescription("Show a decision and where it stands")),
28162
28370
  Command.make("answer", {
28163
28371
  project: project$2,
@@ -28180,7 +28388,7 @@ const planCommands = [
28180
28388
  })
28181
28389
  })
28182
28390
  }));
28183
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28391
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
28184
28392
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
28185
28393
  ];
28186
28394
  //#endregion
@@ -28220,9 +28428,14 @@ const endedLines = (view) => {
28220
28428
  const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
28221
28429
  return [` ${view.state.padEnd(8)} ${at}${why}`];
28222
28430
  };
28223
- const openedByWords = (view) => {
28224
- const by = "session" in view.openedBy ? `session ${view.openedBy.session} of agent ${view.openedBy.agent.id}` : `workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`;
28225
- return `${minute(view.openedAt)} by ${by}`;
28431
+ /**
28432
+ * When it opened, and by what: a workflow's run, or an agent's session, whose IDs go on a line of
28433
+ * their own, since ref0 answers no name for either.
28434
+ */
28435
+ const openedLines = (view) => {
28436
+ const at = minute(view.openedAt);
28437
+ if ("workflow" in view.openedBy) return [` opened ${at} by workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`];
28438
+ return [` opened ${at} by an agent’s session:`, ` ${view.openedBy.session} of ${view.openedBy.agent.id}`];
28226
28439
  };
28227
28440
  /** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
28228
28441
  const pullRequestLines = (view, repo) => {
@@ -28233,7 +28446,7 @@ const pullRequestLines = (view, repo) => {
28233
28446
  `${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
28234
28447
  ` head ${headWords(head)}`,
28235
28448
  ` current ${versionWords(current)}, on ${base}`,
28236
- ` opened ${openedByWords(view)}`,
28449
+ ...openedLines(view),
28237
28450
  ...endedLines(view)
28238
28451
  ].join("\n");
28239
28452
  };
@@ -29219,7 +29432,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
29219
29432
  const { thread, blockers } = first.value;
29220
29433
  if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
29221
29434
  const them = blockers.length === 1 ? "it" : "them";
29222
- yield* Console.error(printable(`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`));
29435
+ yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
29223
29436
  let renewedAt = Number.NEGATIVE_INFINITY;
29224
29437
  for (;;) {
29225
29438
  const now = yield* Clock.currentTimeMillis;
@@ -30112,7 +30325,7 @@ const enable = Command.make("enable", {
30112
30325
  const why = found.why === void 0 ? "" : ` (${found.why})`;
30113
30326
  return yield* sayFailed(found.why === "activation failed" ? `ref0 couldn't read main's .ref0/ for a day, and reads it again each hour. Once ref0 workflows list ${address} shows a version of ${workflow}, turn it on.` : `main has no version of ${workflow} ref0 can run${why}. Fix its code in .ref0/ and merge it, so ref0 compiles a new version, then turn that one on.`);
30114
30327
  }
30115
- yield* Console.error(printable(enablingLines(workflow, found.version, found.manifest, found)));
30328
+ yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
30116
30329
  const answer = yield* callEndpoint({
30117
30330
  endpoint: "workflows.enable",
30118
30331
  params: repoParts(address),
@@ -30145,7 +30358,7 @@ const start = Command.make("start", {
30145
30358
  source: source.value
30146
30359
  }
30147
30360
  });
30148
- yield* Console.error(printable(attachingLines(written, compiled.value)));
30361
+ yield* printBlocks([[attachingLines(written, compiled.value)]]);
30149
30362
  const answer = yield* callEndpoint({
30150
30363
  endpoint: "threadWorkflows.start",
30151
30364
  params,
@@ -33105,11 +33318,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33105
33318
  * started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
33106
33319
  * someone else's words, and nothing here reads it.
33107
33320
  */
33108
- const decodeThread = Schema.decodeUnknownOption(WrittenThread);
33321
+ const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
33109
33322
  const decodeEvent = Schema.decodeUnknownOption(EventId);
33110
33323
  const optional = (name) => Config.String(name).pipe(Config.option);
33111
33324
  const watchedHere = Effect.gen(function* () {
33112
- const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
33325
+ const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
33113
33326
  const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
33114
33327
  return Option.zipWith(thread, event, (on, by) => ({
33115
33328
  thread: on,
@@ -34487,6 +34700,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
34487
34700
  });
34488
34701
  return SessionToken.of({ current });
34489
34702
  }));
34703
+ const decodeThread = Schema.decodeUnknownOption(WrittenThread);
34704
+ /** `SessionThread` from the keychain, as the start of the harness session this runs in kept it. */
34705
+ const harnessSessionThread = Layer.effect(SessionThread, Effect.gen(function* () {
34706
+ const keychain = yield* Keychain;
34707
+ const harness = yield* HarnessProcess;
34708
+ const current = Effect.gen(function* () {
34709
+ const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
34710
+ if (Option.isNone(binding)) return Option.none();
34711
+ const kept = yield* keychain.get(threadAccount(binding.value));
34712
+ return Option.flatMap(kept, (thread) => decodeThread(Redacted.value(thread)));
34713
+ });
34714
+ return SessionThread.of({ current });
34715
+ }));
34490
34716
  //#endregion
34491
34717
  //#region src/harness/server-check.ts
34492
34718
  /**
@@ -34848,7 +35074,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
34848
35074
  ...organizationCommands,
34849
35075
  ...grantCommands,
34850
35076
  ...agentCommands,
34851
- ...attachmentCommands
35077
+ ...attachmentCommands,
35078
+ ...identityTokenCommands
34852
35079
  ]));
34853
35080
  const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
34854
35081
  const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
@@ -34863,7 +35090,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
34863
35090
  Ref0Config.layer,
34864
35091
  sessionToken,
34865
35092
  FetchHttpClient.layer
34866
- ])), sessionToken, FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
35093
+ ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
34867
35094
  //#endregion
34868
35095
  //#region src/main.ts
34869
35096
  ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);