@ref0/cli 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +144 -2
  2. package/dist/main.mjs +791 -555
  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
  /**
@@ -6661,12 +6660,15 @@ const RequestOrganization = Schema.Struct({
6661
6660
  /**
6662
6661
  * The result collected with the machine's two proofs: each included folder's identity and grant,
6663
6662
  * its agent's owner name and organizations, and excluded paths. Confirmation checks each saved
6664
- * grant's live deadline in the Owner DO.
6663
+ * grant's live deadline in the Owner DO. Once a yes connected and the machine hasn't sent its
6664
+ * answer code, waiting says until when: `confirmBy`, when ref0 revokes each grant the machine
6665
+ * hasn't confirmed.
6665
6666
  */
6666
6667
  const RequestState = Schema.Union([
6667
6668
  Schema.Struct({
6668
6669
  state: Schema.Literal("waiting"),
6669
- expiresAt: Timestamp
6670
+ expiresAt: Timestamp,
6671
+ confirmBy: Schema.optionalKey(Timestamp)
6670
6672
  }),
6671
6673
  Schema.Struct({
6672
6674
  state: Schema.Literal("connected"),
@@ -13242,7 +13244,7 @@ Schema.Struct({
13242
13244
  actor: ActorRef,
13243
13245
  /** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
13244
13246
  attachment: Schema.NullOr(CredentialRef),
13245
- scopes: Schema.Array(Scope$1),
13247
+ scopes: Schema.Array(Scope$2),
13246
13248
  ownerName: OwnerName,
13247
13249
  number: ThreadNumber
13248
13250
  });
@@ -19109,271 +19111,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
19109
19111
  /** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
19110
19112
  const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
19111
19113
  //#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
19114
  //#region src/output.ts
19378
19115
  /**
19379
19116
  * What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
@@ -19399,12 +19136,88 @@ const renderLine = (line, terminal) => {
19399
19136
  if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
19400
19137
  return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
19401
19138
  };
19402
- const renderBlocks = (blocks, stream) => {
19139
+ /** The most columns a printed line takes, however wide the terminal. */
19140
+ const mostColumns = 100;
19141
+ /** A printed text without the styles this printer adds. */
19142
+ const unstyled = (text) => text.replaceAll(/\u001b\[\d+m/g, "");
19143
+ const graphemes = new Intl.Segmenter();
19144
+ /** The columns a printed text takes, a character each as the person sees it: styles take none. */
19145
+ const columnsOf = (text) => [...graphemes.segment(unstyled(text))].length;
19146
+ /** A word that says an inline command follows: `run ref0 connect`, `Read a text: ref0 text …`. */
19147
+ const introducesCommand = (word) => word === "run" || word === "Run" || word.endsWith(":");
19148
+ /**
19149
+ * Inline `ref0` commands made single words while they fit in `room`: `ref0` and the word after it
19150
+ * always, and a command after `run` or a colon to its clause's end, so no line breaks inside one.
19151
+ */
19152
+ const withCommandsWhole = (words, room) => {
19153
+ const joined = [];
19154
+ for (let at = 0; at < words.length; at += 1) {
19155
+ const first = words[at];
19156
+ if (first === void 0) continue;
19157
+ if (unstyled(first.word) !== "ref0" || at + 1 >= words.length) {
19158
+ joined.push(first);
19159
+ continue;
19160
+ }
19161
+ const before = words[at - 1];
19162
+ let end = at + 1;
19163
+ if (before !== void 0 && introducesCommand(unstyled(before.word))) while (end + 1 < words.length && !/[.,;:]$/.test(unstyled(words[end]?.word ?? ""))) end += 1;
19164
+ const word = words.slice(at, end + 1).map((each, index) => index === 0 ? each.word : `${each.space}${each.word}`).join("");
19165
+ if (columnsOf(word) > room) {
19166
+ joined.push(first);
19167
+ continue;
19168
+ }
19169
+ joined.push({
19170
+ space: first.space,
19171
+ word
19172
+ });
19173
+ at = end;
19174
+ }
19175
+ return joined;
19176
+ };
19177
+ /**
19178
+ * A line broken between words to fit `width` columns, each line after the first indented as the
19179
+ * first is. A line that fits stays as it is, and a word wider than the width stays whole, on a
19180
+ * line of its own.
19181
+ */
19182
+ const wrapLine = (line, width) => {
19183
+ if (columnsOf(line) <= width) return line;
19184
+ const indent = /^\s*/.exec(line)?.[0] ?? "";
19185
+ const room = Math.max(1, width - columnsOf(indent));
19186
+ const words = withCommandsWhole(Array.from(line.slice(indent.length).matchAll(/(\s*)(\S+)/g), ([, space = "", word = ""]) => ({
19187
+ space,
19188
+ word
19189
+ })), room);
19190
+ const lines = [];
19191
+ let current = "";
19192
+ for (const { space, word } of words) if (current !== "" && columnsOf(current) + columnsOf(space) + columnsOf(word) > room) {
19193
+ lines.push(current);
19194
+ current = word;
19195
+ } else current += current === "" ? word : `${space}${word}`;
19196
+ return [...lines, current].map((each) => `${indent}${each}`).join("\n");
19197
+ };
19198
+ /**
19199
+ * How wide a printed line may be: the terminal's width, at most 100 columns, or none when the
19200
+ * stream isn't a terminal, as for an agent or a script, whose own reader wraps.
19201
+ */
19202
+ const widthOf = (stream) => {
19203
+ if (process[stream].isTTY !== true) return void 0;
19204
+ const columns = process[stream].columns;
19205
+ return columns > 0 ? Math.min(columns, mostColumns) : mostColumns;
19206
+ };
19207
+ const renderBlocks = (blocks, stream, width) => {
19403
19208
  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");
19209
+ const shown = (line) => {
19210
+ const rendered = renderLine(line, terminal);
19211
+ if (width === void 0 || !Predicate.isString(line) && "command" in line) return rendered;
19212
+ return rendered.split("\n").map((each) => wrapLine(each, width)).join("\n");
19213
+ };
19214
+ return blocks.flatMap((block) => block.length === 0 ? [] : [block.map(shown).join("\n")]).join("\n\n");
19405
19215
  };
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)));
19216
+ /**
19217
+ * Words go to stderr by default; a command's answer goes to stdout. In a terminal, each line but a
19218
+ * command's is broken between words to fit it.
19219
+ */
19220
+ 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
19221
  /**
19409
19222
  * A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
19410
19223
  * yes. The cursor stays after it, for their answer.
@@ -19474,6 +19287,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
19474
19287
  };
19475
19288
  /** Says what went wrong, once, on stderr, and fails the command. */
19476
19289
  const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] : words).pipe(Effect.andThen(Effect.fail(new Said())));
19290
+ //#endregion
19291
+ //#region src/session.ts
19292
+ /**
19293
+ * An agent's command in a harness session that has no ref0 session, though an agent is connected
19294
+ * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19295
+ * person instead.
19296
+ */
19297
+ var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19298
+ /**
19299
+ * The token of the session this command runs in, if it runs in one: an agent's, which its
19300
+ * harness bound when the session started. Where it's found is the harness's to say. Outside a
19301
+ * session, there's none, and every command is the person's, asked by approval.
19302
+ */
19303
+ var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
19304
+ /** No session: until a harness binds one, the person asks, by approval. */
19305
+ static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
19306
+ };
19307
+ /**
19308
+ * The thread the session this command runs in is on, as its start kept it beside the session's
19309
+ * token. Outside a session there's none, and in one whose start kept none.
19310
+ */
19311
+ var SessionThread = class SessionThread extends Context.Service()("ref0/cli/SessionThread") {
19312
+ /** No session, so no thread. */
19313
+ static none = Layer.succeed(SessionThread, SessionThread.of({ current: Effect.succeedNone }));
19314
+ };
19315
+ //#endregion
19316
+ //#region src/call.ts
19317
+ /**
19318
+ * Every command's one way to ref0: an endpoint's request, sent with the session's token when
19319
+ * the command runs in a session, or else asked of the person, by approval, from this terminal.
19320
+ * Either way the answer is the endpoint's own, as JSON.
19321
+ */
19322
+ /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
19323
+ var CallFailed = class extends Schema.TaggedError()("CallFailed", {
19324
+ status: Schema.Int,
19325
+ kind: Schema.String,
19326
+ nextStep: Schema.String
19327
+ }) {};
19328
+ const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
19329
+ const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
19330
+ const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
19331
+ const decodeRefusal = Schema.decodeUnknownOption(AnyError);
19332
+ /** How often a terminal asks whether its person has answered. */
19333
+ const pollMillis$1 = 2e3;
19334
+ /** How long one request may take before it's asked again, or the call gives up. */
19335
+ const requestTimeout$1 = Duration.seconds(15);
19336
+ /**
19337
+ * How long a session's request may take for the commands that read and compile before they
19338
+ * answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
19339
+ * lacks the check, one paced read at a time, up to 256 files each.
19340
+ */
19341
+ const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
19342
+ /** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
19343
+ const giveUpAfterMillis$1 = 3e5;
19344
+ /** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
19345
+ const outageMillis = 3e5;
19346
+ /** What a change that failed partway says: ref0 may have made it all the same. */
19347
+ const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
19348
+ /**
19349
+ * An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
19350
+ * 5xx, may have been made before it failed, so it isn't said to try again.
19351
+ */
19352
+ const failedWith$16 = (status, body, change) => Option.match(decodeRefusal(body), {
19353
+ onNone: () => new CallFailed({
19354
+ status,
19355
+ kind: "Unexpected",
19356
+ nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
19357
+ }),
19358
+ onSome: (refusal) => new CallFailed({
19359
+ status,
19360
+ kind: refusal._tag,
19361
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19362
+ })
19363
+ });
19364
+ /**
19365
+ * A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
19366
+ * command's failure: a change that failed at a 5xx may have been made all the same.
19367
+ */
19368
+ const refusedBy = (status, refusal, change) => new CallFailed({
19369
+ status,
19370
+ kind: refusal._tag,
19371
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19372
+ });
19373
+ /**
19374
+ * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19375
+ * the CLI made. A change it answered may have run.
19376
+ */
19377
+ const unreadable = (endpoint, change) => new CallFailed({
19378
+ status: 0,
19379
+ kind: "Unexpected",
19380
+ 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.`
19381
+ });
19382
+ const unreachable = new CallFailed({
19383
+ status: 0,
19384
+ kind: "Unreachable",
19385
+ nextStep: "Couldn’t reach ref0. Try again."
19386
+ });
19387
+ /**
19388
+ * An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
19389
+ * change may have run, and asking again could make it twice.
19390
+ */
19391
+ const gone = (change, nextStep) => new CallFailed({
19392
+ status: 404,
19393
+ kind: "ApprovalNotFound",
19394
+ nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
19395
+ });
19396
+ /** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
19397
+ const unanswered$1 = new CallFailed({
19398
+ status: 0,
19399
+ kind: "Unreachable",
19400
+ nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
19401
+ });
19402
+ /**
19403
+ * Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
19404
+ * stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
19405
+ * What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
19406
+ */
19407
+ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
19408
+ const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
19409
+ let approved = false;
19410
+ let heardAt = yield* Clock.currentTimeMillis;
19411
+ while (true) {
19412
+ const answered = yield* collect;
19413
+ const now = yield* Clock.currentTimeMillis;
19414
+ if (Option.isNone(answered)) {
19415
+ if (now - heardAt > outageMillis) return yield* new CallFailed({
19416
+ status: 0,
19417
+ kind: "Unreachable",
19418
+ 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."
19419
+ });
19420
+ yield* Effect.sleep(pollMillis$1);
19421
+ continue;
19422
+ }
19423
+ const state = answered.value;
19424
+ heardAt = now;
19425
+ if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19426
+ onNone: () => Effect.fail(unreadable(endpoint, change)),
19427
+ onSome: Result.match({
19428
+ onSuccess: () => Effect.succeed(state.result),
19429
+ onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
19430
+ })
19431
+ });
19432
+ if (state.state === "refused") return yield* new CallFailed({
19433
+ status: 403,
19434
+ kind: "Refused",
19435
+ nextStep: "It was refused."
19436
+ });
19437
+ if (state.state === "approved") {
19438
+ if (!approved) yield* printBlocks([["Approved. Waiting for it to run."]]);
19439
+ approved = true;
19440
+ yield* Effect.sleep(pollMillis$1);
19441
+ continue;
19442
+ }
19443
+ if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
19444
+ status: 408,
19445
+ kind: "Expired",
19446
+ nextStep: "Nobody approved it in time. Run it again."
19447
+ });
19448
+ yield* Effect.sleep(pollMillis$1);
19449
+ }
19450
+ });
19451
+ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19452
+ static layer = Layer.effect(Ref0, Effect.gen(function* () {
19453
+ const config = yield* Ref0Config;
19454
+ const session = yield* SessionToken;
19455
+ const http = yield* HttpClient.HttpClient;
19456
+ const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
19457
+ const timeout = timeoutOf(request.command.endpoint);
19458
+ const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
19459
+ const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
19460
+ const change = request.method !== "GET";
19461
+ const lost = change ? unanswered$1 : unreachable;
19462
+ const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
19463
+ response,
19464
+ text
19465
+ }))), Effect.timeoutOrElse({
19466
+ duration: timeout,
19467
+ orElse: () => Effect.fail(lost)
19468
+ }), Effect.mapError(() => lost));
19469
+ const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
19470
+ if (response.status === 202) {
19471
+ const needed = decodeNeeded(body);
19472
+ if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
19473
+ yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
19474
+ return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19475
+ }
19476
+ return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
19477
+ });
19478
+ const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19479
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
19480
+ const refused = (status) => (error) => Effect.fail(new CallFailed({
19481
+ status,
19482
+ kind: error._tag,
19483
+ nextStep: error.nextStep
19484
+ }));
19485
+ return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19486
+ duration: requestTimeout$1,
19487
+ orElse: () => Effect.succeedNone
19488
+ }), Effect.catchTags({
19489
+ ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19490
+ Unauthenticated: refused(401),
19491
+ SessionHasEnded: refused(410)
19492
+ }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19493
+ });
19494
+ const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
19495
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
19496
+ const { secret, hash } = yield* freshSecret;
19497
+ const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
19498
+ const started = yield* client.terminalApprovals.start({
19499
+ headers: { "idempotency-key": yield* newRequestId },
19500
+ payload: {
19501
+ command,
19502
+ secretHash
19503
+ }
19504
+ }).pipe(Effect.timeoutOrElse({
19505
+ duration: requestTimeout$1,
19506
+ orElse: () => Effect.fail(unreachable)
19507
+ }), Effect.catchTags({
19508
+ CommandRefused: (refused) => Effect.fail(new CallFailed({
19509
+ status: 400,
19510
+ kind: refused._tag,
19511
+ nextStep: refused.nextStep
19512
+ })),
19513
+ SlowDown: (slow) => Effect.fail(new CallFailed({
19514
+ status: 429,
19515
+ kind: slow._tag,
19516
+ nextStep: slow.nextStep
19517
+ }))
19518
+ }), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
19519
+ yield* printBlocks([[`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`]]);
19520
+ const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
19521
+ 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({
19522
+ duration: requestTimeout$1,
19523
+ orElse: () => Effect.succeedNone
19524
+ }), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19525
+ return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19526
+ });
19527
+ const call = Effect.fn("Ref0.call")(function* (command) {
19528
+ const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19529
+ status: 0,
19530
+ kind: error._tag,
19531
+ nextStep: error.nextStep
19532
+ })));
19533
+ const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19534
+ status: 400,
19535
+ kind: refused._tag,
19536
+ nextStep: refused.nextStep
19537
+ })));
19538
+ const change = request.method !== "GET";
19539
+ const answer = yield* Option.match(token, {
19540
+ onNone: () => asPerson(request.command, change, command.endpoint),
19541
+ onSome: (bearer) => asSession(bearer, request, command.endpoint)
19542
+ });
19543
+ if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19544
+ return answer;
19545
+ });
19546
+ return Ref0.of({ call });
19547
+ }));
19548
+ };
19549
+ /**
19550
+ * One endpoint's call, typed from the API: the compiler checks its command, and its answer is
19551
+ * read by the endpoint's own success schemas, so what a command shows of it is checked too.
19552
+ */
19553
+ const callEndpoint = (command) => Effect.gen(function* () {
19554
+ const json = yield* (yield* Ref0).call(command);
19555
+ return {
19556
+ json,
19557
+ value: yield* Option.match(successOf(command.endpoint, json), {
19558
+ onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19559
+ onSome: Effect.succeed
19560
+ })
19561
+ };
19562
+ });
19477
19563
  /** The account's commands. */
19478
19564
  const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
19479
19565
  const answer = yield* callEndpoint({ endpoint: "me.get" });
@@ -20417,7 +20503,7 @@ const waitsFor = (each) => {
20417
20503
  * A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
20418
20504
  * when it's in another of `owner`'s, then its title and stage.
20419
20505
  */
20420
- const threadWords = (thread, owner = "") => {
20506
+ const threadWords$1 = (thread, owner = "") => {
20421
20507
  return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
20422
20508
  };
20423
20509
  /** What holds a stack back below the thread, as a line says it, to follow "and". */
@@ -20435,13 +20521,13 @@ const stackHeld = (stack) => {
20435
20521
  */
20436
20522
  const planLine = (wait, number, owner) => {
20437
20523
  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)}`;
20524
+ 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
20525
  return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
20440
20526
  }
20441
20527
  if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
20442
20528
  if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
20443
20529
  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}`;
20530
+ return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
20445
20531
  };
20446
20532
  /** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
20447
20533
  const blockedBy$1 = (list) => {
@@ -20646,7 +20732,7 @@ const browserOpener = Effect.gen(function* () {
20646
20732
  });
20647
20733
  /** Starts the browser independently; a missing or failing desktop never stops the paste. */
20648
20734
  const openBrowser = (program, link) => Effect.sync(() => {
20649
- const failed = () => Effect.runFork(Console.error("Couldn’t open the browser. Open the printed link yourself."));
20735
+ const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
20650
20736
  try {
20651
20737
  const child = spawn(program, [link], {
20652
20738
  detached: true,
@@ -21317,7 +21403,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21317
21403
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21318
21404
  * and only as a default export.
21319
21405
  */
21320
- const cliVersion = "0.11.0";
21406
+ const cliVersion = "0.12.1";
21321
21407
  //#endregion
21322
21408
  //#region src/harness/hook-protocol.ts
21323
21409
  /**
@@ -21635,7 +21721,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21635
21721
  const link = yield* freshLinkSecret;
21636
21722
  const pasted = yield* Ref.make(void 0);
21637
21723
  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)));
21724
+ 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
21725
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21640
21726
  const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
21641
21727
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
@@ -21646,7 +21732,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21646
21732
  ...folder,
21647
21733
  shown: shownConfigFolder(home, folder.configFolder),
21648
21734
  grant: yield* freshTokenSecret,
21649
- replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
21735
+ replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said)),
21736
+ before: Option.isSome(held) ? yield* hostGrant.get(folder.configFolder).pipe(Effect.catchTag("KeychainError", (error) => error.unreadable === true ? Effect.succeedNone : said(error))) : Option.none()
21650
21737
  };
21651
21738
  }));
21652
21739
  const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
@@ -21701,10 +21788,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21701
21788
  const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
21702
21789
  const collector = yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http));
21703
21790
  const params = { approval: started.approval };
21704
- const collect = Effect.flatMap(Ref.get(pasted), (answerCode) => collector.machineAttachments.collect({
21791
+ const collect = (answerCode) => collector.machineAttachments.collect({
21705
21792
  params,
21706
21793
  payload: answerCode === void 0 ? {} : { answerCode }
21707
- })).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
21794
+ }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
21708
21795
  duration: requestTimeout,
21709
21796
  orElse: () => Effect.succeedNone
21710
21797
  }), Effect.catchTags({
@@ -21714,29 +21801,28 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21714
21801
  HttpClientError: () => Effect.succeedNone,
21715
21802
  SchemaError: () => Effect.succeedNone
21716
21803
  }));
21804
+ const lapsing = 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.";
21717
21805
  const giveUpAt = DateTime.toEpochMillis(started.expiresAt) + giveUpAfterMillis;
21806
+ let confirmBy;
21718
21807
  const answered = Effect.gen(function* () {
21719
21808
  while (true) {
21720
- const state = yield* collect;
21721
- if (Option.isSome(state) && isEnded(state.value)) return state.value;
21722
- if ((yield* Clock.currentTimeMillis) > giveUpAt) {
21809
+ const sent = yield* Ref.get(pasted);
21810
+ const state = Option.getOrUndefined(yield* collect(sent));
21811
+ if (state !== void 0 && isEnded(state)) return state;
21812
+ if (state?.confirmBy !== void 0) confirmBy = DateTime.toEpochMillis(state.confirmBy);
21813
+ if ((yield* Ref.get(pasted)) !== sent) continue;
21814
+ if ((yield* Clock.currentTimeMillis) > (confirmBy ?? giveUpAt)) {
21723
21815
  yield* endWait;
21816
+ if (confirmBy !== void 0 && sent === void 0) return yield* sayFailed(`It was approved, but no code was pasted in time, so this machine kept nothing. ${lapsing} To connect this machine, run: ${again$1}`);
21724
21817
  return yield* sayFailed(`ref0 hasn’t said how it ended, so this machine kept nothing. If it was approved, ref0 revokes its grants 10 minutes after it connected. To connect this machine, run: ${again$1}`);
21725
21818
  }
21726
21819
  yield* Effect.sleep(pollMillis);
21727
21820
  }
21728
21821
  });
21729
- const keepOne = Effect.fnUntraced(function* (folder, connection, keepBy) {
21822
+ const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
21730
21823
  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({
21824
+ if (left <= 0) return Option.some(late);
21825
+ const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
21740
21826
  duration: Duration.min(keepTimeout, Duration.millis(left)),
21741
21827
  orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
21742
21828
  }), Effect.retry({
@@ -21745,10 +21831,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21745
21831
  }), Effect.result);
21746
21832
  return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
21747
21833
  });
21834
+ const keepOne = (folder, connection, keepBy) => keepTrying(folder.configFolder, {
21835
+ harness: folder.harness,
21836
+ owner: connection.owner,
21837
+ agent: connection.handle,
21838
+ agentId: connection.agent.id,
21839
+ token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
21840
+ }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21748
21841
  const finish = Effect.fnUntraced(function* (state) {
21749
21842
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
21750
21843
  const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21751
21844
  const kept = [];
21845
+ const keptFrom = [];
21752
21846
  const grants = [];
21753
21847
  const notKept = [];
21754
21848
  for (const folder of asking) {
@@ -21778,9 +21872,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21778
21872
  handle: connection.handle,
21779
21873
  organizations: connection.organizations
21780
21874
  });
21875
+ keptFrom.push(folder);
21781
21876
  grants.push(connection.credential.id);
21782
21877
  }
21783
- yield* oldGrants.retire(kept);
21784
21878
  const [first, ...rest] = grants;
21785
21879
  if (first === void 0) return yield* sayFailed([[
21786
21880
  "It connected, but this machine couldn’t keep what it needs to act:",
@@ -21813,9 +21907,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21813
21907
  times: 2,
21814
21908
  schedule: Schedule.spaced("1 second")
21815
21909
  }), 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}`);
21910
+ const backBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21911
+ const notBack = [];
21912
+ for (const folder of keptFrom) {
21913
+ if (Option.isNone(folder.before)) {
21914
+ yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
21915
+ continue;
21916
+ }
21917
+ const failed = yield* keepTrying(folder.configFolder, folder.before.value, backBy, "The keychain took too long with the folders before it.");
21918
+ if (Option.isSome(failed)) notBack.push(`${shownConfigFolder(home, folder.configFolder)}: ${failed.value}`);
21919
+ }
21920
+ return yield* sayFailed([[
21921
+ "ref0 revoked the new grants, since this machine didn’t confirm in time that it kept them.",
21922
+ ...notBack.length === 0 ? ["Each folder has its connection from before back, if it had one. To connect this machine, run:"] : [
21923
+ "These folders hold a grant that works nowhere, since the keychain didn’t take back their records:",
21924
+ ...notBack,
21925
+ "Each other folder has its connection from before back, if it had one. To connect this machine, run:"
21926
+ ],
21927
+ { command: again$1 }
21928
+ ]]);
21818
21929
  }
21930
+ yield* oldGrants.retire(kept);
21819
21931
  return {
21820
21932
  owner: state.owner,
21821
21933
  kept,
@@ -21827,7 +21939,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21827
21939
  };
21828
21940
  });
21829
21941
  const stopped = Effect.gen(function* () {
21830
- yield* Console.error("Stopping: aborting the request.");
21942
+ yield* printBlocks([["Stopping: aborting the request."]]);
21831
21943
  const abortId = yield* newRequestId;
21832
21944
  const state = yield* collector.machineAttachments.abort({
21833
21945
  params,
@@ -21846,18 +21958,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21846
21958
  times: 2
21847
21959
  }), 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
21960
  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.");
21961
+ if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", lapsing]]);
21962
+ yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
21851
21963
  }
21852
21964
  const connected = yield* finish(isEnded(state) ? state : yield* answered);
21853
21965
  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
21966
  });
21855
- const typedCode = Effect.gen(function* () {
21967
+ const readAnswer = Effect.gen(function* () {
21968
+ if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21856
21969
  const keys = yield* terminal.readInput;
21857
21970
  let typed = "";
21858
21971
  while (true) {
21859
21972
  const pressed = yield* Effect.option(Queue.take(keys));
21860
21973
  if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
21974
+ if ((yield* Ref.get(pasted)) !== void 0) continue;
21861
21975
  const { input, key } = pressed.value;
21862
21976
  if (key.name === "backspace") {
21863
21977
  typed = takeBack(typed);
@@ -21871,17 +21985,14 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21871
21985
  const line = typed.trim();
21872
21986
  typed = "";
21873
21987
  if (line === "") continue;
21874
- if (isAnswerCode(line)) return line;
21875
- yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21988
+ if (!isAnswerCode(line)) {
21989
+ yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21990
+ continue;
21991
+ }
21992
+ yield* Ref.update(pasted, (first) => first ?? line);
21993
+ if (listener !== void 0) yield* listener.stop;
21876
21994
  }
21877
21995
  }).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
21996
  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
21997
  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
21998
  }, Effect.scoped);
@@ -22861,6 +22972,111 @@ function parse(toml, options = {}) {
22861
22972
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22862
22973
  */
22863
22974
  //#endregion
22975
+ //#region src/harness/empty-folder.ts
22976
+ /**
22977
+ * The folder one of a harness's own commands runs in: made empty for it in the system's temporary
22978
+ * folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
22979
+ * runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
22980
+ *
22981
+ * Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
22982
+ * from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
22983
+ * `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
22984
+ * ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
22985
+ * 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
22986
+ * a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
22987
+ */
22988
+ const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
22989
+ //#endregion
22990
+ //#region src/harness/codex-app-server.ts
22991
+ /**
22992
+ * Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
22993
+ * from an empty folder of its own, the requests written a line each, the answers read until each
22994
+ * request has one, then stopped. Each answer is matched to its request by ID, and read by that
22995
+ * request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
22996
+ * didn't send, is passed over.
22997
+ */
22998
+ /** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
22999
+ var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
23000
+ /** Codex's app server answered a request with an error. */
23001
+ var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
23002
+ method: Schema.String,
23003
+ error: CodexError
23004
+ }) {};
23005
+ /** Codex's app server answered a request with a result its method's schema doesn't read. */
23006
+ var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
23007
+ method: Schema.String,
23008
+ issue: Schema.String
23009
+ }) {};
23010
+ const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
23011
+ const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
23012
+ /** The IDs of the exchange's two requests: the opening's, and the one asked. */
23013
+ const initializeId = "ref0-initialize";
23014
+ const askedId = "ref0-asked";
23015
+ /** The exchange's opening: who's asking, then that it's ready. */
23016
+ const opening = [{
23017
+ jsonrpc: "2.0",
23018
+ id: initializeId,
23019
+ method: codexMethods.initialize.method,
23020
+ params: { clientInfo: {
23021
+ name: "ref0",
23022
+ title: null,
23023
+ version: cliVersion
23024
+ } }
23025
+ }, {
23026
+ jsonrpc: "2.0",
23027
+ method: codexMethods.initialized.method
23028
+ }];
23029
+ /**
23030
+ * Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
23031
+ * answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
23032
+ */
23033
+ const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
23034
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23035
+ const fs = yield* FileSystem.FileSystem;
23036
+ const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
23037
+ cwd: yield* emptyFolder(fs),
23038
+ env,
23039
+ extendEnv: true,
23040
+ forceKillAfter: "2 seconds",
23041
+ stdin: {
23042
+ stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
23043
+ endOnDone: false
23044
+ }
23045
+ }))).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);
23046
+ return Option.filter(answered, (answers) => answers.size === ids.length);
23047
+ })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
23048
+ /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
23049
+ const resultOf = (method, answer) => {
23050
+ if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
23051
+ if ("error" in answer) return Effect.fail(new CodexRefused({
23052
+ method: method.method,
23053
+ error: answer.error
23054
+ }));
23055
+ const { result } = method;
23056
+ return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
23057
+ method: method.method,
23058
+ issue: error.message
23059
+ })));
23060
+ };
23061
+ /**
23062
+ * One exchange with Codex's app server under the person's Codex home: the opening, then `method`
23063
+ * asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
23064
+ * its request's ID, and its result is read by the method's result schema.
23065
+ */
23066
+ const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
23067
+ const asked = {
23068
+ jsonrpc: "2.0",
23069
+ id: askedId,
23070
+ method: method.method,
23071
+ params
23072
+ };
23073
+ const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
23074
+ const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
23075
+ if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
23076
+ yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
23077
+ return yield* resultOf(method, answers.value.get(askedId));
23078
+ });
23079
+ //#endregion
22864
23080
  //#region src/harness/codex-checkouts.ts
22865
23081
  const codexGitFolder = (configFolder, pluginId) => {
22866
23082
  const at = pluginId.indexOf("@");
@@ -22977,111 +23193,6 @@ const codexSignOff = [
22977
23193
  `perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
22978
23194
  ];
22979
23195
  //#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
23196
  //#region src/harness/commit-hooks.ts
23086
23197
  /**
23087
23198
  * ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
@@ -23391,21 +23502,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
23391
23502
  parameters: shellWord(`include.path=${file}`)
23392
23503
  };
23393
23504
  });
23505
+ /** Only the single ref0 include installed by this CLI belongs to ref0. */
23506
+ const selectedInclude = (set) => {
23507
+ const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
23508
+ const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
23509
+ 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;
23510
+ };
23511
+ const decodePolicy = Schema.decodeUnknownEffect(Schema.Struct({ shell_environment_policy: CodexConfigRead.fields.config.fields.shell_environment_policy }));
23512
+ /**
23513
+ * The agent's include a Codex config folder's `config.toml` selects, if it's ref0's: its file, read
23514
+ * as TOML for its `shell_environment_policy` alone.
23515
+ */
23516
+ const selectedCodexInclude = Effect.fnUntraced(function* (configFolder) {
23517
+ const fs = yield* FileSystem.FileSystem;
23518
+ const configFile = `${configFolder}/config.toml`;
23519
+ if (!(yield* fs.exists(configFile))) return Option.none();
23520
+ const text = yield* fs.readFileString(configFile);
23521
+ const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
23522
+ const read = yield* decodePolicy(document);
23523
+ return Option.fromNullishOr(selectedInclude(read.shell_environment_policy?.set ?? {}));
23524
+ });
23525
+ /**
23526
+ * Takes the author out of `selected`, the include a folder's `config.toml` selects, unless it's
23527
+ * this origin's include for the folder's agent, so a folder switched to another agent never names
23528
+ * the one before. Its commits name the person until an install selects the agent's own. Whether it
23529
+ * took one out.
23530
+ */
23531
+ const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, selected, options) {
23532
+ const name = yield* originPluginName((yield* Ref0Config).api);
23533
+ if (selected === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return false;
23534
+ const oldRoot = selected.slice(0, selected.lastIndexOf("/agents/"));
23535
+ const fallback = yield* codexAgentConfig(agent, oldRoot, options.home);
23536
+ yield* writeCodexCheckout({
23537
+ gitFolder: oldRoot,
23538
+ common: "",
23539
+ folder: "",
23540
+ signsOff: false,
23541
+ clearAuthor: selected,
23542
+ author: fallback
23543
+ });
23544
+ return true;
23545
+ });
23546
+ /** What the agent reads at its session's start when its folder's `config.toml` names another. */
23547
+ 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
23548
  /**
23395
23549
  * 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`.
23550
+ * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`. An include
23551
+ * `config.toml` selects for another agent, after a switch whose install never finished or another
23552
+ * origin's install, has its author taken out first, and the agent hears so.
23397
23553
  */
23398
23554
  const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
23399
23555
  const folders = yield* ConfigFolders;
23400
23556
  const fs = yield* FileSystem.FileSystem;
23401
23557
  const path = yield* Path.Path;
23402
23558
  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));
23559
+ const configFolder = yield* folders.folder("codex");
23560
+ const selected = yield* selectedCodexInclude(configFolder).pipe(Effect.mapError(() => new CodexCommitFailed({ detail: `Couldn’t read Codex’s config, ${configFolder}/config.toml.` })), Effect.result);
23561
+ const agent = yield* (yield* HostGrant).get(configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23404
23562
  if (Option.isNone(agent)) return false;
23563
+ const home = yield* Config.String("HOME");
23564
+ if (yield* Option.match(yield* Effect.fromResult(selected), {
23565
+ onNone: () => Effect.succeed(false),
23566
+ onSome: (file) => clearOtherCodexAuthor(agent.value, file, {
23567
+ configFolder,
23568
+ home
23569
+ })
23570
+ })) yield* Console.log(otherAgentWords(configFolder));
23405
23571
  const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
23406
23572
  if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
23407
23573
  const gitFolder = path.join(path.dirname(pluginRoot), "@git");
23408
- const home = yield* Config.String("HOME");
23409
23574
  const author = yield* codexAgentConfig(agent.value, gitFolder, home);
23410
23575
  const common = (yield* spawner.string(ChildProcess.make("git", [
23411
23576
  "-C",
@@ -23761,35 +23926,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
23761
23926
  //#region src/harness/codex-install-commits.ts
23762
23927
  const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
23763
23928
  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;
23929
+ /**
23930
+ * What the person reads when an install that never finished took the agent before out of a
23931
+ * folder's commits: what stopped it, in its own words, a conflict only when there is one, then
23932
+ * what to run.
23933
+ */
23934
+ const unfinishedWords = (stopped, configFolder) => {
23935
+ 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, {
23936
+ onNone: () => `The install stopped before it finished. To finish it, run: ${installLine(configFolder)}`,
23937
+ onSome: (failed) => `${failed.says} Then run: ${repairOf(failed, configFolder)}`
23938
+ })}`;
23769
23939
  };
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)}`);
23940
+ /**
23941
+ * After an install into a Codex folder fails or is interrupted, the agent before is taken out of
23942
+ * the include `config.toml` still selects, so the folder's commits never name an agent other than
23943
+ * its grant's, and the person hears what stopped the install: `stopped`, or none when interrupted.
23944
+ */
23945
+ const clearUnfinishedCodexInstall = Effect.fnUntraced(function* (agent, stopped, options) {
23946
+ const selected = yield* selectedCodexInclude(options.configFolder);
23947
+ if (Option.isNone(selected)) return;
23948
+ if (!(yield* clearOtherCodexAuthor(agent, selected.value, options))) return;
23949
+ yield* printBlocks([[unfinishedWords(stopped, options.configFolder)]]);
23793
23950
  });
23794
23951
  /** Install the connected agent's include and shell policy. */
23795
23952
  const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
@@ -23798,6 +23955,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23798
23955
  CODEX_HOME: options.configFolder
23799
23956
  };
23800
23957
  const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
23958
+ 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
23959
  return yield* Effect.gen(function* () {
23802
23960
  const fs = yield* FileSystem.FileSystem;
23803
23961
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -23825,8 +23983,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23825
23983
  ].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
23826
23984
  const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
23827
23985
  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");
23986
+ 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.`]]);
23987
+ if (own) return yield* conflicted;
23830
23988
  return;
23831
23989
  }
23832
23990
  const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
@@ -23851,8 +24009,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23851
24009
  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
24010
  const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
23853
24011
  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");
24012
+ 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.`]]);
24013
+ if (own) return yield* conflicted;
23856
24014
  return;
23857
24015
  }
23858
24016
  if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
@@ -23940,9 +24098,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23940
24098
  }, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
23941
24099
  }).pipe(Effect.onError(() => Effect.gen(function* () {
23942
24100
  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.");
24101
+ 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
24102
  }).pipe(Effect.orDie)));
23945
- }).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
24103
+ }).pipe(Effect.scoped, Effect.mapError((error) => error instanceof InstallFailed ? error : failed("set Codex’s commit environment")));
23946
24104
  }, Effect.scoped);
23947
24105
  //#endregion
23948
24106
  //#region src/harness/install-claude-code.ts
@@ -24106,7 +24264,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
24106
24264
  trusted,
24107
24265
  profile
24108
24266
  };
24109
- }).pipe(Effect.onError(() => clearOtherCodexAuthor(agent, options).pipe(Effect.orDie)));
24267
+ }).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
24110
24268
  });
24111
24269
  //#endregion
24112
24270
  //#region src/harness/install.ts
@@ -24212,7 +24370,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24212
24370
  for (const folder of into) answers.set(folder, Result.fail(failed));
24213
24371
  };
24214
24372
  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, {
24373
+ 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
24374
  configFolder: folder.configFolder,
24217
24375
  home
24218
24376
  }).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
@@ -24227,8 +24385,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24227
24385
  if (into.length === 0) continue;
24228
24386
  const command = yield* onPath(harness === "codex" ? "codex" : "claude");
24229
24387
  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);
24388
+ const missing = new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` });
24389
+ failAll(into, missing);
24390
+ yield* clearUnstarted(into, Option.some(missing));
24232
24391
  continue;
24233
24392
  }
24234
24393
  yield* Effect.scoped(Effect.gen(function* () {
@@ -24249,9 +24408,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
24249
24408
  })).pipe(Effect.result);
24250
24409
  answers.set(folder, installed);
24251
24410
  }
24252
- }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
24411
+ }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into, Option.some(failed)))))));
24253
24412
  }
24254
- }).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
24413
+ }).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
24255
24414
  return folders.map((folder) => ({
24256
24415
  ...folder,
24257
24416
  installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
@@ -24343,16 +24502,13 @@ const mostFolders = 8;
24343
24502
  const isConfigFolderPath = Schema.is(ConfigFolderPath);
24344
24503
  /** The line that asks again. */
24345
24504
  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
24505
  /**
24349
24506
  * 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.
24507
+ * question is said first, and the chooser shows only the folders: once Enter chooses, it leaves
24508
+ * the ticked ones on one line after any question it was given, which the terminal would break
24509
+ * mid-word.
24353
24510
  */
24354
24511
  const choose = Effect.fnUntraced(function* (found, home) {
24355
- const columns = yield* (yield* Terminal.Terminal).columns;
24356
24512
  const choices = found.map((folder) => {
24357
24513
  const shown = shownConfigFolder(home, folder.configFolder);
24358
24514
  const sendable = isConfigFolderPath(shown);
@@ -24369,7 +24525,7 @@ const choose = Effect.fnUntraced(function* (found, home) {
24369
24525
  });
24370
24526
  const ticked = choices.filter((choice) => choice.selected).length;
24371
24527
  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)]]);
24528
+ yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
24373
24529
  return yield* Prompt.run(Prompt.MultiSelect({
24374
24530
  choices,
24375
24531
  max: mostFolders
@@ -24562,7 +24718,7 @@ const signalLines = (answer) => {
24562
24718
  * credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
24563
24719
  * thread in the project sends a signal to one of its connections by name.
24564
24720
  */
24565
- const failedWith$14 = (failed) => sayFailed(failed.nextStep);
24721
+ const failedWith$15 = (failed) => sayFailed(failed.nextStep);
24566
24722
  const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
24567
24723
  const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
24568
24724
  const list$5 = Command.make("list", {
@@ -24574,7 +24730,7 @@ const list$5 = Command.make("list", {
24574
24730
  params: projectParts(address)
24575
24731
  });
24576
24732
  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"));
24733
+ }).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
24734
  /** A change made on the project's page: its link, and what to do there. */
24579
24735
  const onThePage = (address, change) => Effect.gen(function* () {
24580
24736
  const config = yield* Ref0Config;
@@ -24650,14 +24806,14 @@ const send = Command.make("send", {
24650
24806
  })
24651
24807
  });
24652
24808
  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"));
24809
+ }).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
24654
24810
  /** A project's connections, and sending a signal by one. */
24655
24811
  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
24812
  //#endregion
24657
24813
  //#region src/harness/checkout.ts
24658
24814
  const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
24659
24815
  const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
24660
- const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
24816
+ const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
24661
24817
  const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
24662
24818
  var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24663
24819
  static layer = Layer.effect(Checkout, Effect.gen(function* () {
@@ -24696,7 +24852,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24696
24852
  "config",
24697
24853
  "--get",
24698
24854
  "ref0.thread"
24699
- ]), (written) => decodeThread$1(written.trim()));
24855
+ ]), (written) => decodeThread$2(written.trim()));
24700
24856
  const repoAt = (folder) => Effect.map(git(folder, [
24701
24857
  "config",
24702
24858
  "--get",
@@ -25699,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25699
25855
  })).value;
25700
25856
  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
25857
  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.`);
25858
+ 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
25859
  return;
25704
25860
  }
25705
25861
  yield* holdsVersion(root.value, code);
@@ -25708,7 +25864,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25708
25864
  thread: `${thread.value}’s own .ref0/`,
25709
25865
  project: "the project’s checks"
25710
25866
  }[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.`);
25867
+ 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
25868
  const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
25713
25869
  const sandboxes = code.compiled.entry.sandboxes ?? [];
25714
25870
  const held = /* @__PURE__ */ new Set();
@@ -25720,7 +25876,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25720
25876
  const run = yield* newRunId;
25721
25877
  const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
25722
25878
  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}.`);
25879
+ yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
25724
25880
  const ran = yield* runPart(own, {
25725
25881
  params,
25726
25882
  host: treeHost(root.value, code.tree),
@@ -25783,7 +25939,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
25783
25939
  /** Says a waiting start's tree on stderr, as it stands now. */
25784
25940
  const sayTree = Effect.fnUntraced(function* (wait) {
25785
25941
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25786
- yield* Console.error(printable(waitingLines$1(wait, now)));
25942
+ yield* printBlocks([[waitingLines$1(wait, now)]]);
25787
25943
  });
25788
25944
  /**
25789
25945
  * Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
@@ -25829,7 +25985,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
25829
25985
  * reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
25830
25986
  * at a time.
25831
25987
  */
25832
- const failedWith$13 = (failed) => sayFailed(failed.nextStep);
25988
+ const failedWith$14 = (failed) => sayFailed(failed.nextStep);
25833
25989
  const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
25834
25990
  const decodeTraced = Schema.decodeUnknownOption(TracedRun);
25835
25991
  /** A run named by a flag, or a failure that says how to name one. */
@@ -25931,7 +26087,7 @@ const run = Command.make("run", {
25931
26087
  }, asJson, rerunCommand);
25932
26088
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25933
26089
  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"));
26090
+ }).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
26091
  const trace = Command.make("trace", {
25936
26092
  run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
25937
26093
  repo: repoFlag,
@@ -25974,7 +26130,7 @@ const trace = Command.make("trace", {
25974
26130
  ...narrowed,
25975
26131
  `--cursor ${next}`
25976
26132
  ].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"));
26133
+ }).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
26134
  const waiting$1 = Command.make("waiting", {
25979
26135
  start: startArgument,
25980
26136
  repo: repoFlag,
@@ -26009,7 +26165,7 @@ const waiting$1 = Command.make("waiting", {
26009
26165
  }, asJson, rerunCommand);
26010
26166
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
26011
26167
  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"));
26168
+ }).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
26169
  /** What a cancel did, in words. */
26014
26170
  const cancelledText = {
26015
26171
  start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
@@ -26035,7 +26191,7 @@ const cancel = Command.make("cancel", {
26035
26191
  })
26036
26192
  });
26037
26193
  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"));
26194
+ }).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
26195
  /** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
26040
26196
  const runCommands = [run, Command.make("runs", {
26041
26197
  repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
@@ -26077,14 +26233,14 @@ const runCommands = [run, Command.make("runs", {
26077
26233
  ...narrowed,
26078
26234
  `--before ${next}`
26079
26235
  ].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([
26236
+ }).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
26237
  trace,
26082
26238
  waiting$1,
26083
26239
  cancel
26084
26240
  ]))];
26085
26241
  //#endregion
26086
26242
  //#region src/commands/checks.ts
26087
- const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26243
+ const failedWith$13 = (failed) => sayFailed(failed.nextStep);
26088
26244
  const decodeCheck = Schema.decodeUnknownOption(CheckKey);
26089
26245
  const decodeToken = Schema.decodeUnknownOption(TokenId);
26090
26246
  const decodePerson = Schema.decodeUnknownOption(OwnerName);
@@ -26120,7 +26276,7 @@ const restore = Command.make("restore", {
26120
26276
  }
26121
26277
  });
26122
26278
  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"));
26279
+ }).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
26280
  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
26281
  /** `ref0 checks`: a repo's checks. */
26126
26282
  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 +26608,7 @@ const projectGateLines = (view, repo) => {
26452
26608
  };
26453
26609
  //#endregion
26454
26610
  //#region src/commands/gate.ts
26455
- const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26611
+ const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26456
26612
  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
26613
  /** A pull request's gate. */
26458
26614
  const gateCommands = [Command.make("gate", {
@@ -26478,7 +26634,7 @@ const gateCommands = [Command.make("gate", {
26478
26634
  }
26479
26635
  });
26480
26636
  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"))];
26637
+ }).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
26638
  //#endregion
26483
26639
  //#region src/clone.ts
26484
26640
  /** The remote a clone pushes to: its thread's fork path, named as the thread is. */
@@ -26836,7 +26992,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26836
26992
  * with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
26837
26993
  * thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
26838
26994
  */
26839
- const failedWith$10 = (failed) => sayFailed(failed.nextStep);
26995
+ const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26840
26996
  /** `ref0 git-credential`, as Git runs it: see `answerGit`. */
26841
26997
  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
26998
  const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
@@ -26890,7 +27046,7 @@ const gitCommands = [
26890
27046
  value
26891
27047
  ])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
26892
27048
  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")),
27049
+ }).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
27050
  Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
26895
27051
  yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
26896
27052
  const synced = yield* syncBase(".");
@@ -26906,7 +27062,7 @@ const gitCommands = [
26906
27062
  on,
26907
27063
  commit
26908
27064
  }, 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")),
27065
+ }).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
27066
  credential
26911
27067
  ];
26912
27068
  //#endregion
@@ -27319,7 +27475,7 @@ const usageCommands = [personUsage, personMeters];
27319
27475
  * invitations. Each is a person's, asked of them by approval, as every terminal command of
27320
27476
  * theirs is; agents never change members.
27321
27477
  */
27322
- const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27478
+ const failedWith$10 = (failed) => sayFailed(failed.nextStep);
27323
27479
  const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
27324
27480
  const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
27325
27481
  const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
@@ -27349,7 +27505,7 @@ const create$1 = Command.make("create", {
27349
27505
  payload
27350
27506
  });
27351
27507
  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"));
27508
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
27353
27509
  const leave = Command.make("leave", {
27354
27510
  organization: organization$2,
27355
27511
  json
@@ -27359,7 +27515,7 @@ const leave = Command.make("leave", {
27359
27515
  params: { owner: org }
27360
27516
  });
27361
27517
  yield* printAnswer(answer.json, asJson, `Left ${org}.`);
27362
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Leave an organization"));
27518
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
27363
27519
  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
27520
  create$1,
27365
27521
  leave,
@@ -27376,7 +27532,7 @@ const orgs = Command.make("orgs", {
27376
27532
  });
27377
27533
  const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
27378
27534
  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"));
27535
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
27380
27536
  const members = Command.make("members", {
27381
27537
  organization: organization$2,
27382
27538
  cursor: cursor$3,
@@ -27389,7 +27545,7 @@ const members = Command.make("members", {
27389
27545
  });
27390
27546
  const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
27391
27547
  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"));
27548
+ }).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
27549
  const role = Command.make("role", {
27394
27550
  organization: organization$2,
27395
27551
  person,
@@ -27405,7 +27561,7 @@ const role = Command.make("role", {
27405
27561
  payload: { role: chosen }
27406
27562
  });
27407
27563
  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"));
27564
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
27409
27565
  const remove$1 = Command.make("remove", {
27410
27566
  organization: organization$2,
27411
27567
  person,
@@ -27419,7 +27575,7 @@ const remove$1 = Command.make("remove", {
27419
27575
  }
27420
27576
  });
27421
27577
  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"));
27578
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
27423
27579
  const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
27424
27580
  const listInvitations = Command.make("list", {
27425
27581
  organization: organization$2,
@@ -27433,7 +27589,7 @@ const listInvitations = Command.make("list", {
27433
27589
  });
27434
27590
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
27435
27591
  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"));
27592
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
27437
27593
  const withdraw = Command.make("withdraw", {
27438
27594
  organization: organization$2,
27439
27595
  invitation,
@@ -27447,7 +27603,7 @@ const withdraw = Command.make("withdraw", {
27447
27603
  }
27448
27604
  });
27449
27605
  yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
27450
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Take an open invitation back"));
27606
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
27451
27607
  const invite = Command.make("invite", {
27452
27608
  organization: organization$2,
27453
27609
  person,
@@ -27467,7 +27623,7 @@ const invite = Command.make("invite", {
27467
27623
  payload
27468
27624
  });
27469
27625
  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]));
27626
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
27471
27627
  const invitations = Command.make("invitations", {
27472
27628
  cursor: cursor$3,
27473
27629
  json
@@ -27478,7 +27634,7 @@ const invitations = Command.make("invitations", {
27478
27634
  });
27479
27635
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
27480
27636
  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"));
27637
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
27482
27638
  const accept = Command.make("accept", {
27483
27639
  invitation,
27484
27640
  json
@@ -27488,7 +27644,7 @@ const accept = Command.make("accept", {
27488
27644
  params: { invitation: id }
27489
27645
  });
27490
27646
  yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
27491
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Accept an invitation"));
27647
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
27492
27648
  const decline = Command.make("decline", {
27493
27649
  invitation,
27494
27650
  json
@@ -27498,7 +27654,7 @@ const decline = Command.make("decline", {
27498
27654
  params: { invitation: id }
27499
27655
  });
27500
27656
  yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
27501
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Decline an invitation"));
27657
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
27502
27658
  /** Organizations, their members and invitations. */
27503
27659
  const organizationCommands = [
27504
27660
  orgCommand,
@@ -27511,7 +27667,7 @@ const organizationCommands = [
27511
27667
  ];
27512
27668
  //#endregion
27513
27669
  //#region src/commands/grants.ts
27514
- const failedWith$8 = (failed) => sayFailed(failed.nextStep);
27670
+ const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27515
27671
  const isOwnerRef = Schema.is(OwnerRef);
27516
27672
  const isProject$1 = Schema.is(WrittenProject);
27517
27673
  const isThread = Schema.is(WrittenThread);
@@ -27633,7 +27789,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
27633
27789
  onSome: (on) => [on]
27634
27790
  })];
27635
27791
  yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
27636
- }).pipe(Effect.catchTag("CallFailed", failedWith$8)));
27792
+ }).pipe(Effect.catchTag("CallFailed", failedWith$9)));
27637
27793
  const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
27638
27794
  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
27795
  /** An organization's grants. */
@@ -27653,7 +27809,41 @@ const grantCommands = [Command.make("grants", {
27653
27809
  const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
27654
27810
  const next = answer.value.nextAfter;
27655
27811
  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];
27812
+ }).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];
27813
+ //#endregion
27814
+ //#region src/commands/identity-token.ts
27815
+ /**
27816
+ * `ref0 identity-token --audience <origin>`: an identity token for the agent's session it runs
27817
+ * in, for one outside service, which verifies it against ref0's published keys. One request, by
27818
+ * the session's own token, which never starts or renews a session. The token alone goes to
27819
+ * stdout, so it pipes into a verifier; anything else goes to stderr, with no token, and fails.
27820
+ */
27821
+ const decodeAudience = Schema.decodeUnknownOption(IdentityAudience);
27822
+ 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.";
27823
+ const audienceOnce = "Name the audience once, as --audience https://verifier.example: the outside service’s HTTPS origin.";
27824
+ const notAnAudience = "Send the outside service’s HTTPS origin as the audience, such as https://verifier.example, with no path.";
27825
+ 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.";
27826
+ /**
27827
+ * What went wrong, said once, on stderr. An answer lost or failed partway may still have made a
27828
+ * token, as ref0's log records, but nothing to check or undo: a fresh request is the way on.
27829
+ */
27830
+ const failedWith$8 = (failed) => sayFailed(failed.status >= 500 || failed.status === 0 && (failed.kind === "Unreachable" || failed.kind === "Unexpected") ? mayHaveIssued : failed.nextStep);
27831
+ /** Identity tokens, for outside services. */
27832
+ const identityTokenCommands = [Command.make("identity-token", {
27833
+ 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)),
27834
+ json
27835
+ }, ({ audience: named, json: asJson }) => Effect.gen(function* () {
27836
+ if (named.length !== 1) return yield* sayFailed(audienceOnce);
27837
+ const audience = decodeAudience(named[0]);
27838
+ if (Option.isNone(audience)) return yield* sayFailed(notAnAudience);
27839
+ yield* ownSession(outside);
27840
+ const issued = yield* callEndpoint({
27841
+ endpoint: "me.identityToken",
27842
+ payload: { audience: audience.value }
27843
+ });
27844
+ if (asJson) return yield* printAnswer(issued.json, true, "");
27845
+ yield* Console.log(issued.value.token);
27846
+ }).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
27847
  //#endregion
27658
27848
  //#region src/commands/agents.ts
27659
27849
  /**
@@ -27928,27 +28118,39 @@ const repoCommands = [Command.make("repos", {
27928
28118
  //#endregion
27929
28119
  //#region src/decision-lines.ts
27930
28120
  /** 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);
28121
+ /**
28122
+ * A thread as a line read in `scope` names it: `#3` when it's in that scope, else in full, as
28123
+ * `pip/default#3`.
28124
+ */
28125
+ const threadWords = (thread, scope) => thread.startsWith(`${scope}#`) ? thread.slice(scope.length) : thread;
28126
+ /**
28127
+ * What waits, in words: a gate's pull request and version, and the thread it's from, or the thread
28128
+ * an ask is on.
28129
+ */
28130
+ const subjectWords = ({ subject, names }, scope) => subject.kind === "version" && names.pullRequest !== void 0 ? `${names.pullRequest}, version ${subject.version}, from ${threadWords(names.thread, scope)}` : names.thread;
28131
+ /**
28132
+ * A decision in a line, read in `scope`: what waits, its answers or how it ended, then its ID, for
28133
+ * `ref0 decision` and `ref0 answer`.
28134
+ */
28135
+ const decisionLines = (decision, scope) => {
28136
+ const on = subjectWords(decision, scope);
27936
28137
  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"}`;
28138
+ return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
27938
28139
  };
27939
- const nothing = "Nothing waits on you here.";
27940
28140
  /**
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.
28141
+ * What waits in `scope`, as `ref0 waiting` prints it: what waits on the caller, then, for an agent,
28142
+ * what waits on its person, which only they answer, on their own page.
27943
28143
  */
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");
28144
+ const waitingLines = (decisions, scope) => {
28145
+ const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision, scope)] : []);
28146
+ const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision, scope)}`] : []);
28147
+ if (theirs.length === 0) return yours.length === 0 ? `Nothing waits on you in ${scope}.` : yours.join("\n");
28148
+ const them = theirs.length === 1 ? "it" : "them";
27948
28149
  return [
27949
28150
  ...yours,
27950
- "Waits on your person, under Waiting on you on their page:",
27951
- ...theirs
28151
+ `Waits on your person, who alone can answer ${them}, under Waiting on you on their page:`,
28152
+ ...theirs,
28153
+ `ref0 refuses an agent’s answer to ${them}.`
27952
28154
  ].join("\n");
27953
28155
  };
27954
28156
  //#endregion
@@ -27964,7 +28166,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
27964
28166
  const encodePlan = Schema.encodeEffect(PlanInput);
27965
28167
  const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
27966
28168
  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])));
28169
+ /** A project, `acme/billing`, or an organization, `acme`, for its org threads. */
28170
+ const Scope$1 = Schema.Union([WrittenProject, OwnerName]);
28171
+ const decodeScope = Schema.decodeUnknownOption(Scope$1);
28172
+ 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
28173
  const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
27969
28174
  const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
27970
28175
  /** Where a version's approval stands: who accepted it, or its check. */
@@ -28125,25 +28330,37 @@ const comments = Command.make("comments", {
28125
28330
  ].filter((line) => line !== "").join("\n");
28126
28331
  yield* printAnswer(answer.json, asJson, words);
28127
28332
  }).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"));
28333
+ 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([
28334
+ list$4,
28335
+ write,
28336
+ onVersion("show"),
28337
+ onVersion("propose"),
28338
+ comment,
28339
+ comments
28340
+ ]));
28341
+ /**
28342
+ * The project of the thread the session runs on, or its organization for an org thread's. Outside
28343
+ * a session, it says how to name one, and fails before anything's sent.
28344
+ */
28345
+ const sessionScope = Effect.gen(function* () {
28346
+ const thread = yield* (yield* SessionThread).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
28347
+ const scope = Option.flatMap(thread, (written) => decodeScope(projectOf$2(written)));
28348
+ if (Option.isNone(scope)) return yield* sayFailed("Name the project: ref0 waiting pip/shop. Or, for an organization’s org threads: ref0 waiting acme.");
28349
+ return scope.value;
28350
+ });
28128
28351
  /** Plans' and decisions' commands. */
28129
28352
  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
- ])),
28353
+ plan,
28138
28354
  Command.make("waiting", {
28139
- project: project$2,
28355
+ 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
28356
  json
28141
- }, ({ project: address, json: asJson }) => Effect.gen(function* () {
28357
+ }, ({ project: named, json: asJson }) => Effect.gen(function* () {
28358
+ const address = Option.isSome(named) ? named.value : yield* sessionScope;
28142
28359
  const answer = yield* callEndpoint(routed(address, {
28143
28360
  endpoint: "decisions.waiting",
28144
28361
  params: scopeParams(address)
28145
28362
  }));
28146
- yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
28363
+ yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
28147
28364
  }).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
28365
  Command.make("decision", {
28149
28366
  project: project$2,
@@ -28157,7 +28374,7 @@ const planCommands = [
28157
28374
  decision: id
28158
28375
  }
28159
28376
  }));
28160
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28377
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
28161
28378
  }).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
28379
  Command.make("answer", {
28163
28380
  project: project$2,
@@ -28180,7 +28397,7 @@ const planCommands = [
28180
28397
  })
28181
28398
  })
28182
28399
  }));
28183
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28400
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
28184
28401
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
28185
28402
  ];
28186
28403
  //#endregion
@@ -28220,9 +28437,14 @@ const endedLines = (view) => {
28220
28437
  const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
28221
28438
  return [` ${view.state.padEnd(8)} ${at}${why}`];
28222
28439
  };
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}`;
28440
+ /**
28441
+ * When it opened, and by what: a workflow's run, or an agent's session, whose IDs go on a line of
28442
+ * their own, since ref0 answers no name for either.
28443
+ */
28444
+ const openedLines = (view) => {
28445
+ const at = minute(view.openedAt);
28446
+ if ("workflow" in view.openedBy) return [` opened ${at} by workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`];
28447
+ return [` opened ${at} by an agent’s session:`, ` ${view.openedBy.session} of ${view.openedBy.agent.id}`];
28226
28448
  };
28227
28449
  /** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
28228
28450
  const pullRequestLines = (view, repo) => {
@@ -28233,7 +28455,7 @@ const pullRequestLines = (view, repo) => {
28233
28455
  `${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
28234
28456
  ` head ${headWords(head)}`,
28235
28457
  ` current ${versionWords(current)}, on ${base}`,
28236
- ` opened ${openedByWords(view)}`,
28458
+ ...openedLines(view),
28237
28459
  ...endedLines(view)
28238
28460
  ].join("\n");
28239
28461
  };
@@ -29219,7 +29441,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
29219
29441
  const { thread, blockers } = first.value;
29220
29442
  if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
29221
29443
  const them = blockers.length === 1 ? "it" : "them";
29222
- yield* Console.error(printable(`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`));
29444
+ yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
29223
29445
  let renewedAt = Number.NEGATIVE_INFINITY;
29224
29446
  for (;;) {
29225
29447
  const now = yield* Clock.currentTimeMillis;
@@ -30112,7 +30334,7 @@ const enable = Command.make("enable", {
30112
30334
  const why = found.why === void 0 ? "" : ` (${found.why})`;
30113
30335
  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
30336
  }
30115
- yield* Console.error(printable(enablingLines(workflow, found.version, found.manifest, found)));
30337
+ yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
30116
30338
  const answer = yield* callEndpoint({
30117
30339
  endpoint: "workflows.enable",
30118
30340
  params: repoParts(address),
@@ -30145,7 +30367,7 @@ const start = Command.make("start", {
30145
30367
  source: source.value
30146
30368
  }
30147
30369
  });
30148
- yield* Console.error(printable(attachingLines(written, compiled.value)));
30370
+ yield* printBlocks([[attachingLines(written, compiled.value)]]);
30149
30371
  const answer = yield* callEndpoint({
30150
30372
  endpoint: "threadWorkflows.start",
30151
30373
  params,
@@ -33105,11 +33327,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
33105
33327
  * started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
33106
33328
  * someone else's words, and nothing here reads it.
33107
33329
  */
33108
- const decodeThread = Schema.decodeUnknownOption(WrittenThread);
33330
+ const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
33109
33331
  const decodeEvent = Schema.decodeUnknownOption(EventId);
33110
33332
  const optional = (name) => Config.String(name).pipe(Config.option);
33111
33333
  const watchedHere = Effect.gen(function* () {
33112
- const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
33334
+ const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
33113
33335
  const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
33114
33336
  return Option.zipWith(thread, event, (on, by) => ({
33115
33337
  thread: on,
@@ -34487,6 +34709,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
34487
34709
  });
34488
34710
  return SessionToken.of({ current });
34489
34711
  }));
34712
+ const decodeThread = Schema.decodeUnknownOption(WrittenThread);
34713
+ /** `SessionThread` from the keychain, as the start of the harness session this runs in kept it. */
34714
+ const harnessSessionThread = Layer.effect(SessionThread, Effect.gen(function* () {
34715
+ const keychain = yield* Keychain;
34716
+ const harness = yield* HarnessProcess;
34717
+ const current = Effect.gen(function* () {
34718
+ const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
34719
+ if (Option.isNone(binding)) return Option.none();
34720
+ const kept = yield* keychain.get(threadAccount(binding.value));
34721
+ return Option.flatMap(kept, (thread) => decodeThread(Redacted.value(thread)));
34722
+ });
34723
+ return SessionThread.of({ current });
34724
+ }));
34490
34725
  //#endregion
34491
34726
  //#region src/harness/server-check.ts
34492
34727
  /**
@@ -34848,7 +35083,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
34848
35083
  ...organizationCommands,
34849
35084
  ...grantCommands,
34850
35085
  ...agentCommands,
34851
- ...attachmentCommands
35086
+ ...attachmentCommands,
35087
+ ...identityTokenCommands
34852
35088
  ]));
34853
35089
  const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
34854
35090
  const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
@@ -34863,7 +35099,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
34863
35099
  Ref0Config.layer,
34864
35100
  sessionToken,
34865
35101
  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));
35102
+ ])), 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
35103
  //#endregion
34868
35104
  //#region src/main.ts
34869
35105
  ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);