@ref0/cli 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +144 -2
  2. package/dist/main.mjs +1623 -965
  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
  /**
@@ -5486,12 +5486,15 @@ const name$1 = ownerNamePattern.source.replace(/^\^|\$$/g, "");
5486
5486
  const projectPattern = new RegExp(`^(${name$1})/(${name$1})$`);
5487
5487
  const threadPattern = new RegExp(`^(${name$1})(?:/(${name$1}))?#([1-9][0-9]*)$`);
5488
5488
  const repoPattern = new RegExp(`^(${name$1})/(${repoNamePattern.source.replace(/^\^|\$$/g, "")})$`);
5489
+ const pullRequestPattern = new RegExp(`${repoPattern.source.replace(/\$$/, "")}#[1-9][0-9]*$`);
5489
5490
  /** A project, `owner/project`. */
5490
5491
  const WrittenProject = Schema.String.check(Schema.isPattern(projectPattern)).pipe(Schema.brand("WrittenProject"));
5491
5492
  /** A thread, `owner/project#number`, or an org thread, `owner#number`. */
5492
5493
  const WrittenThread = Schema.String.check(Schema.isPattern(threadPattern)).pipe(Schema.brand("WrittenThread"));
5493
5494
  /** A repo, `owner/repo`. */
5494
5495
  const WrittenRepo = Schema.String.check(Schema.isPattern(repoPattern)).pipe(Schema.brand("WrittenRepo"));
5496
+ /** A pull request, `owner/repo#number`. */
5497
+ const WrittenPullRequest = Schema.String.check(Schema.isPattern(pullRequestPattern)).pipe(Schema.brand("WrittenPullRequest"));
5495
5498
  /** A written repo's parts: its owner, as an endpoint's path takes it, and its name. */
5496
5499
  const repoParts = (written) => {
5497
5500
  const [, owner = "", repo = ""] = repoPattern.exec(written) ?? [];
@@ -5702,7 +5705,7 @@ const Me = Schema.Struct({
5702
5705
  session: SessionId,
5703
5706
  depth: SessionDepth
5704
5707
  })),
5705
- scopes: Schema.Array(Scope$1),
5708
+ scopes: Schema.Array(Scope$2),
5706
5709
  expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
5707
5710
  });
5708
5711
  /**
@@ -6072,7 +6075,7 @@ const Reader = Schema.Struct({
6072
6075
  agent: Schema.optionalKey(AgentRef),
6073
6076
  /** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
6074
6077
  attachment: Schema.NullOr(CredentialRef),
6075
- scopes: Schema.Array(Scope$1)
6078
+ scopes: Schema.Array(Scope$2)
6076
6079
  });
6077
6080
  Schema.Struct({
6078
6081
  reader: Reader,
@@ -6586,14 +6589,13 @@ const ConnectOrganization = Schema.Struct({
6586
6589
  organization: OrgId,
6587
6590
  name: OwnerName
6588
6591
  });
6589
- /** 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
+ */
6590
6596
  const GrantReplacement = Schema.Struct({
6591
6597
  grant: TokenId,
6592
- state: Schema.Literals([
6593
- "replaces",
6594
- "revoked",
6595
- "skipped"
6596
- ]),
6598
+ state: Schema.Literals(["replaces", "skipped"]),
6597
6599
  attachedAt: Schema.optionalKey(Timestamp)
6598
6600
  });
6599
6601
  /**
@@ -6767,383 +6769,6 @@ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
6767
6769
  attachGrant: Schema.optionalKey(EventId)
6768
6770
  })]);
6769
6771
  //#endregion
6770
- //#region ../contracts/src/approvals.ts
6771
- /**
6772
- * Approvals, as their commands travel: what a terminal, a page or an agent asks its person to
6773
- * say yes to. A command is one endpoint, by its name in the surface table, and its request, as
6774
- * that endpoint encodes it. ref0 decodes each part by the endpoint's own `Schema` before it
6775
- * keeps or shows anything, so a command is exactly what the endpoint would take.
6776
- */
6777
- /**
6778
- * An endpoint by its name in the surface table, `group.endpoint`. Checked against the table
6779
- * where the command is made canonical, since the table is typed from the API this is part of.
6780
- */
6781
- const CommandEndpoint = Schema.String.check(Schema.isPattern(/^[a-zA-Z]+\.[a-zA-Z]+$/));
6782
- /** One command, as asked: its endpoint, and its request's path, query and payload, encoded. */
6783
- const ApprovalCommand = Schema.Struct({
6784
- endpoint: CommandEndpoint,
6785
- params: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
6786
- query: Schema.optionalKey(Schema.JsonObject),
6787
- payload: Schema.optionalKey(Schema.Json)
6788
- });
6789
- /** A command ref0 won't ask anyone about: not an endpoint's request, or not one an approval runs. */
6790
- var CommandRefused = class extends Schema.TaggedError()("CommandRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
6791
- /** A terminal asking: its command, and the hash of the secret it collects the result with. */
6792
- const StartApproval = Schema.Struct({
6793
- command: ApprovalCommand,
6794
- secretHash: SecretHash
6795
- });
6796
- /** A terminal's approval, started: what to show its person, and until when. */
6797
- const ApprovalStarted = Schema.Struct({
6798
- approval: ApprovalId,
6799
- code: ApprovalCode,
6800
- expiresAt: Timestamp
6801
- });
6802
- /**
6803
- * An answer that waits for the person: the call only the person may make, or that agents ask
6804
- * for, became an approval, which the person answers on their own page. A `202`, never a failure
6805
- * of the call: whoever asked collects the result with `GET /v1/approvals/{approval}`.
6806
- */
6807
- var ApprovalNeeded = class extends Schema.TaggedError()("ApprovalNeeded", {
6808
- approval: ApprovalId,
6809
- says: Schema.String,
6810
- expiresAt: Timestamp
6811
- }, { httpApiStatus: 202 }) {};
6812
- const terminalAsker = Schema.Struct({ kind: Schema.Literal("terminal") });
6813
- const pageAsker = Schema.Struct({ kind: Schema.Literal("page") });
6814
- const agentAsker = Schema.Struct({
6815
- kind: Schema.Literal("agent"),
6816
- agent: AgentId,
6817
- handle: AgentHandle,
6818
- session: SessionId,
6819
- /** The attachment its credential was made under, for a person's agent in an organization. */
6820
- attachment: Schema.optionalKey(CredentialRef)
6821
- });
6822
- /**
6823
- * Who asked, as a receipt keeps it: a terminal, one of ref0's pages, an agent, or a machine
6824
- * asking to connect, by its ID and its own word for itself, and from where.
6825
- */
6826
- const ApprovalAsker = Schema.Union([
6827
- terminalAsker,
6828
- pageAsker,
6829
- Schema.Struct({
6830
- kind: Schema.Literal("machine"),
6831
- machine: MachineId,
6832
- machineName: MachineName,
6833
- askedFrom: Schema.optionalKey(Schema.String)
6834
- }),
6835
- agentAsker
6836
- ]);
6837
- /**
6838
- * Who asked, as the person reads a request before they answer: a terminal, a page or an agent.
6839
- * A machine's request is read as an `AttachmentRequestView`, never as a command.
6840
- */
6841
- const ApprovalViewAsker = Schema.Union([
6842
- terminalAsker,
6843
- pageAsker,
6844
- agentAsker
6845
- ]);
6846
- /** A request, as the person reads it before they answer. */
6847
- const ApprovalView = Schema.Struct({
6848
- approval: ApprovalId,
6849
- /** The command as its CLI line. */
6850
- says: Schema.String,
6851
- /** Everything the command carries, each named, in order: nothing it would run is left out. */
6852
- fields: Schema.Array(Schema.Struct({
6853
- name: Schema.String,
6854
- value: Schema.String,
6855
- part: Schema.Literals([
6856
- "path",
6857
- "query",
6858
- "payload"
6859
- ])
6860
- })),
6861
- command: ApprovalCommand,
6862
- asker: ApprovalViewAsker,
6863
- askedFrom: Schema.optionalKey(Schema.String),
6864
- askedAt: Timestamp,
6865
- expiresAt: Timestamp,
6866
- /**
6867
- * An agent's ask: the kind of action its answer may set agents' permission for, and whether a
6868
- * yes may let agents take it without asking. A no with the passkey may always stop them asking.
6869
- */
6870
- agentPermission: Schema.optionalKey(Schema.Struct({
6871
- action: ScopeAction,
6872
- mayAllow: Schema.Boolean
6873
- })),
6874
- /**
6875
- * What the command acts on, drawn as ref0 resolves it for the person reading, where its
6876
- * endpoint names a preview. Without one the page can draw, it offers only no.
6877
- */
6878
- preview: Schema.optionalKey(ApprovalPreview)
6879
- });
6880
- /** A statement's hash: the SHA-256 of its words and quoted values as JSON, as lowercase hex. */
6881
- const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
6882
- /**
6883
- * Who asks for a yes to a statement: a workflow's run, `passkey.run`'s, by the workflow and the
6884
- * run; or a gate's check, `passkey.check`'s, by the check, the pull request's version it gates and
6885
- * the check run its statement is over.
6886
- */
6887
- const StatementAsker = Schema.Union([Schema.Struct({
6888
- kind: Schema.Literal("workflow"),
6889
- workflow: WorkflowName,
6890
- run: WorkflowRunId
6891
- }), Schema.Struct({
6892
- kind: Schema.Literal("gate"),
6893
- check: CheckKey,
6894
- pullRequest: PullRequestNumber,
6895
- version: VersionNumber,
6896
- run: CheckRunId
6897
- })]);
6898
- /**
6899
- * A passkey request, as the person reads it before they answer: the statement, its words as its
6900
- * asker wrote them and each value it quotes, and its hash; who asks; where, a workflow's
6901
- * `owner/repo` or a thread's `owner/project#number`, or a gate's pull request, `owner/repo#number`;
6902
- * whom it asks, the run's accountable person, or for a gate the person behind its run, a person by
6903
- * name, or a permission's holders; and until when.
6904
- */
6905
- const StatementView = Schema.Struct({
6906
- approval: ApprovalId,
6907
- statement: WorkflowText,
6908
- hash: StatementHash,
6909
- asker: StatementAsker,
6910
- home: Schema.String,
6911
- from: Schema.Union([Schema.Literals(["accountable", "person"]), Schema.Struct({ permission: RepoPermissionName })]),
6912
- askedAt: Timestamp,
6913
- expiresAt: Timestamp
6914
- });
6915
- /**
6916
- * The person's own requests, waiting for them; the passkey requests workflows' runs and gates asked
6917
- * them; and each machine's request this browser session is the answerer of, so a reload finds it
6918
- * again without its code.
6919
- */
6920
- const ApprovalsWaiting = Schema.Struct({
6921
- approvals: Schema.Array(ApprovalView),
6922
- statements: Schema.Array(StatementView),
6923
- requests: Schema.Array(AttachmentRequestView)
6924
- });
6925
- /** Finding a terminal's request by the code it showed. */
6926
- const FindApproval = Schema.Union([Schema.Struct({ code: TypedApprovalCode }), Schema.Struct({ link: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/)) })]);
6927
- /**
6928
- * Asking for the passkey's options to answer a request: a terminal's request needs its code
6929
- * again, so its ID alone, which travels where its code doesn't, never answers it. A machine's
6930
- * request needs its answerer, and the agent it chooses to connect, before a yes has options.
6931
- */
6932
- const ApprovalOptionsRequest = Schema.Struct({
6933
- code: Schema.optionalKey(TypedApprovalCode),
6934
- folders: Schema.optionalKey(ConnectFolderChoices)
6935
- });
6936
- /**
6937
- * An answer: yes, with the passkey, or no. For an agent's ask, each answer can set the owner's
6938
- * permission its own way, so a pairing that can't happen can't be sent: yes, and let agents do
6939
- * it without asking; no, and stop agents asking, with the passkey too.
6940
- */
6941
- const AnswerApproval = Schema.Union([
6942
- Schema.Struct({
6943
- answer: Schema.Literal("yes"),
6944
- permission: Schema.optionalKey(Schema.Literal("allow")),
6945
- assertion: PasskeyAsserted,
6946
- code: Schema.optionalKey(TypedApprovalCode),
6947
- /** The preview the page drew, where the command's endpoint names one. */
6948
- preview: Schema.optionalKey(PreviewEcho),
6949
- /** The statement the page showed, by its hash, for a workflow run's or a gate's passkey request. */
6950
- statement: Schema.optionalKey(Schema.Struct({ hash: StatementHash }))
6951
- }),
6952
- Schema.Struct({
6953
- answer: Schema.Literal("no"),
6954
- permission: Schema.Literal("deny"),
6955
- assertion: PasskeyAsserted,
6956
- code: Schema.optionalKey(TypedApprovalCode)
6957
- }),
6958
- Schema.Struct({
6959
- answer: Schema.Literal("no"),
6960
- code: Schema.optionalKey(TypedApprovalCode)
6961
- })
6962
- ]);
6963
- /**
6964
- * What running an approved command came to: its status, and its endpoint's answer or refusal,
6965
- * as JSON. ref0 reads it by the command's endpoint before anyone sees it, and whoever asked reads
6966
- * it the same way, with `ranOf`: this schema is part of the API the endpoints' types come from,
6967
- * so it can't be typed by them itself.
6968
- */
6969
- const Ran = Schema.Struct({
6970
- status: Schema.Int,
6971
- result: Schema.Json
6972
- });
6973
- /**
6974
- * A ran command whose answer its endpoint's schemas no longer read, such as one kept from
6975
- * before they changed: it ran, and what it came to can't be said.
6976
- */
6977
- var ResultUnreadable = class extends Schema.TaggedError()("ResultUnreadable", { nextStep: Schema.String }, { httpApiStatus: 500 }) {};
6978
- /**
6979
- * Where a request stands, for whoever asked: still waiting; approved, and running as soon as
6980
- * ref0 can; refused; expired; or what it ran to, with the endpoint it ran, whose schemas read it.
6981
- */
6982
- const ApprovalState = Schema.Union([
6983
- Schema.Struct({
6984
- state: Schema.Literal("waiting"),
6985
- expiresAt: Timestamp
6986
- }),
6987
- Schema.Struct({
6988
- state: Schema.Literal("approved"),
6989
- /** A machine's answering browser may forget its saved answer once the terminal kept it. */
6990
- collected: Schema.optionalKey(Schema.Literal(true))
6991
- }),
6992
- Schema.Struct({ state: Schema.Literal("refused") }),
6993
- Schema.Struct({ state: Schema.Literal("expired") }),
6994
- Schema.Struct({
6995
- state: Schema.Literal("ran"),
6996
- endpoint: CommandEndpoint,
6997
- ...Ran.fields
6998
- })
6999
- ]);
7000
- /** Only the answering browser receives a machine's answer code; state reads never carry it. */
7001
- const ApprovalAnswer = Schema.Union([
7002
- Schema.Struct({
7003
- ...ApprovalState.members[1].fields,
7004
- answerCode: ApprovalCode
7005
- }),
7006
- Schema.Struct({
7007
- ...ApprovalState.members[4].fields,
7008
- answerCode: ApprovalCode
7009
- }),
7010
- ApprovalState
7011
- ]);
7012
- /** A request that isn't there, or isn't the caller's to see, answers the same. */
7013
- var ApprovalNotFound = class extends Schema.TaggedError()("ApprovalNotFound", { nextStep: Schema.String }, { httpApiStatus: 404 }) {};
7014
- /** Too many asked, or too many codes typed, from here in the last hour. */
7015
- var SlowDown = class extends Schema.TaggedError()("SlowDown", { nextStep: Schema.String }, { httpApiStatus: 429 }) {};
7016
- /** A command's hash: the SHA-256 of its canonical JSON, as lowercase hex. */
7017
- const CommandHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
7018
- Schema.Struct({
7019
- approval: ApprovalId,
7020
- endpoint: CommandEndpoint,
7021
- /** The command's canonical JSON. */
7022
- command: Schema.String,
7023
- commandHash: CommandHash,
7024
- asker: ApprovalAsker,
7025
- /** The browser session the person answered in, for their log. */
7026
- credential: CredentialRef,
7027
- generation: Generation,
7028
- /** When the request expires, in epoch milliseconds. */
7029
- deadline: Schema.Int,
7030
- challenge: Schema.String,
7031
- /** A machine answer retries only from its answering session, with this key. */
7032
- answerKey: Schema.optionalKey(RequestId),
7033
- /** An agent's ask: the credential it asked with, checked again before the answer counts. */
7034
- asking: Schema.optionalKey(TokenId),
7035
- /** The kind of action the person, answering, let agents take without asking from now on. */
7036
- allow: Schema.optionalKey(ScopeAction)
7037
- });
7038
- Schema.Struct({
7039
- approval: ApprovalId,
7040
- action: ScopeAction,
7041
- credential: CredentialRef,
7042
- generation: Generation
7043
- });
7044
- Schema.Union([Schema.Struct({ denied: Schema.Literal(true) }), Schema.Struct({
7045
- denied: Schema.Literal(false),
7046
- reason: Schema.Literals(["generation moved", "not set up"])
7047
- })]);
7048
- Schema.Union([Schema.Struct({
7049
- admitted: Schema.Literal(true),
7050
- admittedAt: Schema.Int,
7051
- answerCode: Schema.optionalKey(ApprovalCode)
7052
- }), Schema.Struct({
7053
- admitted: Schema.Literal(false),
7054
- reason: Schema.Literals([
7055
- "expired",
7056
- "generation moved",
7057
- "challenge used",
7058
- "not set up",
7059
- "asker gone"
7060
- ])
7061
- })]);
7062
- Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
7063
- found: Schema.Literal(true),
7064
- endpoint: CommandEndpoint,
7065
- command: Schema.String,
7066
- commandHash: CommandHash,
7067
- generation: Generation,
7068
- generationNow: Generation,
7069
- /** The `approval.answered` its admission recorded in the person's log. */
7070
- answeredEvent: EventId,
7071
- /** Who asked, as that `approval.answered` says. */
7072
- asker: ApprovalAsker,
7073
- /** What running it came to, once it ran, as `KeptRan` keeps it. */
7074
- result: Schema.optionalKey(Schema.String)
7075
- })]);
7076
- /**
7077
- * A `Ran` as it's kept, on its receipt and on its approval: its JSON, as text. The Owner DO
7078
- * keeps it as it comes and runs no endpoint's schemas, so it's read only by its endpoint, with
7079
- * `keptRan`, which refuses one they don't take.
7080
- */
7081
- const KeptRan = Schema.fromJsonString(Ran);
7082
- Schema.Struct({
7083
- approval: ApprovalId,
7084
- result: Schema.String
7085
- });
7086
- Schema.Struct({ result: Schema.String });
7087
- Schema.Struct({ approval: ApprovalId });
7088
- Schema.Struct({ approval: ApprovalId });
7089
- Schema.Union([Schema.Struct({ expired: Schema.Literal(true) }), Schema.Struct({ expired: Schema.Literal(false) })]);
7090
- Schema.Struct({
7091
- person: PersonId,
7092
- approval: ApprovalId
7093
- });
7094
- //#endregion
7095
- //#region ../contracts/src/browser-sessions.ts
7096
- /**
7097
- * A browser session's cookie: Better Auth's, named for ref0, `Secure` and host-only on the
7098
- * app's one origin. It holds the session's token, signed; the directory and the person's Owner DO hold
7099
- * only the token's SHA-256.
7100
- */
7101
- const browserSessionCookie = "__Secure-ref0.session_token";
7102
- Schema.Struct({
7103
- token: TokenId,
7104
- secretHash: SecretHash,
7105
- expiresAt: Schema.DateTimeUtcFromMillis
7106
- });
7107
- Schema.Struct({ registered: Schema.Boolean });
7108
- /**
7109
- * Where a signed-in person stands: naming, when the name their GitHub login gave was taken or
7110
- * kept back, with that login as a suggestion; setting up, once their owner is made and before
7111
- * it's ready with this session registered, which naming again finishes; or ready.
7112
- */
7113
- const Welcome = Schema.Union([Schema.Struct({
7114
- state: Schema.Literals(["ready", "setting up"]),
7115
- person: PersonId,
7116
- owner: OwnerId,
7117
- name: OwnerName
7118
- }), Schema.Struct({
7119
- state: Schema.Literal("naming"),
7120
- person: PersonId,
7121
- suggestion: Schema.optionalKey(Schema.String)
7122
- })]);
7123
- /**
7124
- * A browser session renewed: its expiry, in its person's Owner DO and in the directory, and on its cookie.
7125
- * Renewed at most once a day, so one asked sooner answers its expiry unchanged.
7126
- */
7127
- const SessionRenewed = Schema.Struct({ expiresAt: Schema.DateTimeUtcFromMillis });
7128
- /**
7129
- * What starting a GitHub sign-in at `POST /auth/sign-in/social` answers: GitHub's address to go
7130
- * to, with the sign-in's state in a cookie set alongside. Better Auth's answer, which ref0 serves.
7131
- */
7132
- const NavigationUrl = Schema.String.check(Schema.isPattern(/^https?:\/\/[^\s/]+(?:\/[^\s]*)?$/), Schema.makeFilter((url) => URL.canParse(url) || "an HTTP(S) URL the platform can parse")).pipe(Schema.brand("NavigationUrl"));
7133
- Schema.Struct({ url: NavigationUrl });
7134
- /**
7135
- * Where signing in sends the browser back to, as `POST /auth/sign-in/social` takes it: a path on
7136
- * ref0's own origin, such as `/welcome`, or a whole URL. Never a socket's.
7137
- */
7138
- const SignInDestination = Schema.String.pipe(Schema.brand("SignInDestination"));
7139
- Schema.Struct({
7140
- provider: Schema.Literal("github"),
7141
- callbackURL: SignInDestination,
7142
- errorCallbackURL: SignInDestination
7143
- });
7144
- /** The name a naming person chose for their owner. */
7145
- const NameOwner = Schema.Struct({ name: OwnerName });
7146
- //#endregion
7147
6772
  //#region ../contracts/src/plan-events.ts
7148
6773
  /**
7149
6774
  * A plan's events, in its thread's project's log, written by the Project DO. A version's
@@ -7282,6 +6907,21 @@ const DecisionView = Schema.Struct({
7282
6907
  closedWhy: Schema.optionalKey(DecisionClosedWhy)
7283
6908
  });
7284
6909
  /**
6910
+ * Where a decision is, as people write it: its thread, `pip/default#3`, or an org thread's,
6911
+ * `acme#7`; and a gate's pull request, `pip/notes#1`. Its Project DO writes them as it answers,
6912
+ * from its own threads and repos and the owner's name the Worker gives it, so each reads as it's
6913
+ * called now.
6914
+ */
6915
+ const DecisionNames = Schema.Struct({
6916
+ thread: WrittenThread,
6917
+ pullRequest: Schema.optionalKey(WrittenPullRequest)
6918
+ });
6919
+ /** A decision as `/v1` answers it: as it stands, and where it is, in words. */
6920
+ const NamedDecision = Schema.Struct({
6921
+ ...DecisionView.fields,
6922
+ names: DecisionNames
6923
+ });
6924
+ /**
7285
6925
  * A version as it stands: its content, its state, and, once proposed, where its approval is: its
7286
6926
  * repos still being checked, or who accepted it.
7287
6927
  */
@@ -7435,7 +7075,7 @@ const GateAnswerRoute = Schema.TaggedStruct("GateAnswerRoute", {
7435
7075
  reason: Schema.optionalKey(TextId)
7436
7076
  });
7437
7077
  Schema.Union([
7438
- DecisionView,
7078
+ NamedDecision,
7439
7079
  GateAnswerRoute,
7440
7080
  DecisionNotFound,
7441
7081
  AnswerRefused,
@@ -7467,14 +7107,29 @@ Schema.Union([
7467
7107
  ]) }),
7468
7108
  DeliveryOutsideWindow
7469
7109
  ]);
7470
- Schema.Struct({
7110
+ /** Someone decisions wait on: a person, or an agent, and the roles the Worker found they hold. */
7111
+ const Waiter = Schema.Struct({
7471
7112
  decider: DeciderRef,
7472
- roles: Schema.optionalKey(Schema.Array(Schema.Struct({
7113
+ roles: Schema.Array(Schema.Struct({
7473
7114
  role: Schema.Literal("admin"),
7474
7115
  owner: OwnerId
7475
- })))
7116
+ }))
7476
7117
  });
7477
- const WaitingPage = Schema.Struct({ decisions: Schema.Array(DecisionView) });
7118
+ Schema.Struct({
7119
+ ownerName: OwnerName,
7120
+ you: Waiter,
7121
+ person: Schema.optionalKey(Waiter),
7122
+ decisions: Schema.optionalKey(Schema.Array(DecisionId).check(Schema.isMaxLength(100)))
7123
+ });
7124
+ /**
7125
+ * An open decision that waits on the caller: on them, or, for an agent, on its person, who
7126
+ * answers it. A gate never takes an agent's answer, so an agent's gate always waits on its person.
7127
+ */
7128
+ const WaitingDecision = Schema.Struct({
7129
+ ...NamedDecision.fields,
7130
+ waitsOn: Schema.Literals(["you", "your person"])
7131
+ });
7132
+ const WaitingPage = Schema.Struct({ decisions: Schema.Array(WaitingDecision) });
7478
7133
  Schema.Struct({
7479
7134
  ownerName: OwnerName,
7480
7135
  number: ThreadNumber
@@ -7492,7 +7147,7 @@ Schema.Struct({
7492
7147
  decision: DecisionId,
7493
7148
  wait: Schema.optionalKey(DecisionWait)
7494
7149
  });
7495
- Schema.Union([DecisionView, DecisionNotFound]);
7150
+ Schema.Union([NamedDecision, DecisionNotFound]);
7496
7151
  /**
7497
7152
  * `POST …/plans`: a version, as a draft unless it's proposed at once, and why it was written.
7498
7153
  * A first version needs no reason, and a later one may leave it out.
@@ -7541,6 +7196,385 @@ const ChildStageView = Schema.Struct({
7541
7196
  });
7542
7197
  Schema.Union([ChildStageView, ThreadNotFound]);
7543
7198
  //#endregion
7199
+ //#region ../contracts/src/approvals.ts
7200
+ /**
7201
+ * Approvals, as their commands travel: what a terminal, a page or an agent asks its person to
7202
+ * say yes to. A command is one endpoint, by its name in the surface table, and its request, as
7203
+ * that endpoint encodes it. ref0 decodes each part by the endpoint's own `Schema` before it
7204
+ * keeps or shows anything, so a command is exactly what the endpoint would take.
7205
+ */
7206
+ /**
7207
+ * An endpoint by its name in the surface table, `group.endpoint`. Checked against the table
7208
+ * where the command is made canonical, since the table is typed from the API this is part of.
7209
+ */
7210
+ const CommandEndpoint = Schema.String.check(Schema.isPattern(/^[a-zA-Z]+\.[a-zA-Z]+$/));
7211
+ /** One command, as asked: its endpoint, and its request's path, query and payload, encoded. */
7212
+ const ApprovalCommand = Schema.Struct({
7213
+ endpoint: CommandEndpoint,
7214
+ params: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
7215
+ query: Schema.optionalKey(Schema.JsonObject),
7216
+ payload: Schema.optionalKey(Schema.Json)
7217
+ });
7218
+ /** A command ref0 won't ask anyone about: not an endpoint's request, or not one an approval runs. */
7219
+ var CommandRefused = class extends Schema.TaggedError()("CommandRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
7220
+ /** A terminal asking: its command, and the hash of the secret it collects the result with. */
7221
+ const StartApproval = Schema.Struct({
7222
+ command: ApprovalCommand,
7223
+ secretHash: SecretHash
7224
+ });
7225
+ /** A terminal's approval, started: what to show its person, and until when. */
7226
+ const ApprovalStarted = Schema.Struct({
7227
+ approval: ApprovalId,
7228
+ code: ApprovalCode,
7229
+ expiresAt: Timestamp
7230
+ });
7231
+ /**
7232
+ * An answer that waits for the person: the call only the person may make, or that agents ask
7233
+ * for, became an approval, which the person answers on their own page. A `202`, never a failure
7234
+ * of the call: whoever asked collects the result with `GET /v1/approvals/{approval}`.
7235
+ */
7236
+ var ApprovalNeeded = class extends Schema.TaggedError()("ApprovalNeeded", {
7237
+ approval: ApprovalId,
7238
+ says: Schema.String,
7239
+ expiresAt: Timestamp
7240
+ }, { httpApiStatus: 202 }) {};
7241
+ const terminalAsker = Schema.Struct({ kind: Schema.Literal("terminal") });
7242
+ const pageAsker = Schema.Struct({ kind: Schema.Literal("page") });
7243
+ const agentAsker = Schema.Struct({
7244
+ kind: Schema.Literal("agent"),
7245
+ agent: AgentId,
7246
+ handle: AgentHandle,
7247
+ session: SessionId,
7248
+ /** The attachment its credential was made under, for a person's agent in an organization. */
7249
+ attachment: Schema.optionalKey(CredentialRef)
7250
+ });
7251
+ /**
7252
+ * Who asked, as a receipt keeps it: a terminal, one of ref0's pages, an agent, or a machine
7253
+ * asking to connect, by its ID and its own word for itself, and from where.
7254
+ */
7255
+ const ApprovalAsker = Schema.Union([
7256
+ terminalAsker,
7257
+ pageAsker,
7258
+ Schema.Struct({
7259
+ kind: Schema.Literal("machine"),
7260
+ machine: MachineId,
7261
+ machineName: MachineName,
7262
+ askedFrom: Schema.optionalKey(Schema.String)
7263
+ }),
7264
+ agentAsker
7265
+ ]);
7266
+ /**
7267
+ * Who asked, as the person reads a request before they answer: a terminal, a page or an agent.
7268
+ * A machine's request is read as an `AttachmentRequestView`, never as a command.
7269
+ */
7270
+ const ApprovalViewAsker = Schema.Union([
7271
+ terminalAsker,
7272
+ pageAsker,
7273
+ agentAsker
7274
+ ]);
7275
+ /** A request, as the person reads it before they answer. */
7276
+ const ApprovalView = Schema.Struct({
7277
+ approval: ApprovalId,
7278
+ /** The command as its CLI line. */
7279
+ says: Schema.String,
7280
+ /** Everything the command carries, each named, in order: nothing it would run is left out. */
7281
+ fields: Schema.Array(Schema.Struct({
7282
+ name: Schema.String,
7283
+ value: Schema.String,
7284
+ part: Schema.Literals([
7285
+ "path",
7286
+ "query",
7287
+ "payload"
7288
+ ])
7289
+ })),
7290
+ command: ApprovalCommand,
7291
+ asker: ApprovalViewAsker,
7292
+ askedFrom: Schema.optionalKey(Schema.String),
7293
+ askedAt: Timestamp,
7294
+ expiresAt: Timestamp,
7295
+ /**
7296
+ * An agent's ask: the kind of action its answer may set agents' permission for, and whether a
7297
+ * yes may let agents take it without asking. A no with the passkey may always stop them asking.
7298
+ */
7299
+ agentPermission: Schema.optionalKey(Schema.Struct({
7300
+ action: ScopeAction,
7301
+ mayAllow: Schema.Boolean
7302
+ })),
7303
+ /**
7304
+ * What the command acts on, drawn as ref0 resolves it for the person reading, where its
7305
+ * endpoint names a preview. Without one the page can draw, it offers only no.
7306
+ */
7307
+ preview: Schema.optionalKey(ApprovalPreview)
7308
+ });
7309
+ /** A statement's hash: the SHA-256 of its words and quoted values as JSON, as lowercase hex. */
7310
+ const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
7311
+ /**
7312
+ * Who asks for a yes to a statement: a workflow's run, `passkey.run`'s, by the workflow and the
7313
+ * run; or a gate's check, `passkey.check`'s, by the check, the pull request's version it gates and
7314
+ * the check run its statement is over.
7315
+ */
7316
+ const StatementAsker = Schema.Union([Schema.Struct({
7317
+ kind: Schema.Literal("workflow"),
7318
+ workflow: WorkflowName,
7319
+ run: WorkflowRunId
7320
+ }), Schema.Struct({
7321
+ kind: Schema.Literal("gate"),
7322
+ check: CheckKey,
7323
+ pullRequest: PullRequestNumber,
7324
+ version: VersionNumber,
7325
+ run: CheckRunId
7326
+ })]);
7327
+ /**
7328
+ * A passkey request, as the person reads it before they answer: the statement, its words as its
7329
+ * asker wrote them and each value it quotes, and its hash; who asks; where, a workflow's
7330
+ * `owner/repo` or a thread's `owner/project#number`, or a gate's pull request, `owner/repo#number`;
7331
+ * whom it asks, the run's accountable person, or for a gate the person behind its run, a person by
7332
+ * name, or a permission's holders; and until when.
7333
+ */
7334
+ const StatementView = Schema.Struct({
7335
+ approval: ApprovalId,
7336
+ statement: WorkflowText,
7337
+ hash: StatementHash,
7338
+ asker: StatementAsker,
7339
+ home: Schema.String,
7340
+ from: Schema.Union([Schema.Literals(["accountable", "person"]), Schema.Struct({ permission: RepoPermissionName })]),
7341
+ askedAt: Timestamp,
7342
+ expiresAt: Timestamp
7343
+ });
7344
+ /**
7345
+ * The person's own requests, waiting for them; the passkey requests workflows' runs and gates asked
7346
+ * them; each machine's request this browser session is the answerer of, so a reload finds it
7347
+ * again without its code; and the gates' decisions that wait on their yes or no, in every project
7348
+ * they see, oldest first.
7349
+ */
7350
+ const ApprovalsWaiting = Schema.Struct({
7351
+ approvals: Schema.Array(ApprovalView),
7352
+ statements: Schema.Array(StatementView),
7353
+ requests: Schema.Array(AttachmentRequestView),
7354
+ decisions: Schema.Array(NamedDecision)
7355
+ });
7356
+ /** Finding a terminal's request by the code it showed. */
7357
+ const FindApproval = Schema.Union([Schema.Struct({ code: TypedApprovalCode }), Schema.Struct({ link: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/)) })]);
7358
+ /**
7359
+ * Asking for the passkey's options to answer a request: a terminal's request needs its code
7360
+ * again, so its ID alone, which travels where its code doesn't, never answers it. A machine's
7361
+ * request needs its answerer, and the agent it chooses to connect, before a yes has options.
7362
+ */
7363
+ const ApprovalOptionsRequest = Schema.Struct({
7364
+ code: Schema.optionalKey(TypedApprovalCode),
7365
+ folders: Schema.optionalKey(ConnectFolderChoices)
7366
+ });
7367
+ /**
7368
+ * An answer: yes, with the passkey, or no. For an agent's ask, each answer can set the owner's
7369
+ * permission its own way, so a pairing that can't happen can't be sent: yes, and let agents do
7370
+ * it without asking; no, and stop agents asking, with the passkey too.
7371
+ */
7372
+ const AnswerApproval = Schema.Union([
7373
+ Schema.Struct({
7374
+ answer: Schema.Literal("yes"),
7375
+ permission: Schema.optionalKey(Schema.Literal("allow")),
7376
+ assertion: PasskeyAsserted,
7377
+ code: Schema.optionalKey(TypedApprovalCode),
7378
+ /** The preview the page drew, where the command's endpoint names one. */
7379
+ preview: Schema.optionalKey(PreviewEcho),
7380
+ /** The statement the page showed, by its hash, for a workflow run's or a gate's passkey request. */
7381
+ statement: Schema.optionalKey(Schema.Struct({ hash: StatementHash }))
7382
+ }),
7383
+ Schema.Struct({
7384
+ answer: Schema.Literal("no"),
7385
+ permission: Schema.Literal("deny"),
7386
+ assertion: PasskeyAsserted,
7387
+ code: Schema.optionalKey(TypedApprovalCode)
7388
+ }),
7389
+ Schema.Struct({
7390
+ answer: Schema.Literal("no"),
7391
+ code: Schema.optionalKey(TypedApprovalCode)
7392
+ })
7393
+ ]);
7394
+ /**
7395
+ * What running an approved command came to: its status, and its endpoint's answer or refusal,
7396
+ * as JSON. ref0 reads it by the command's endpoint before anyone sees it, and whoever asked reads
7397
+ * it the same way, with `ranOf`: this schema is part of the API the endpoints' types come from,
7398
+ * so it can't be typed by them itself.
7399
+ */
7400
+ const Ran = Schema.Struct({
7401
+ status: Schema.Int,
7402
+ result: Schema.Json
7403
+ });
7404
+ /**
7405
+ * A ran command whose answer its endpoint's schemas no longer read, such as one kept from
7406
+ * before they changed: it ran, and what it came to can't be said.
7407
+ */
7408
+ var ResultUnreadable = class extends Schema.TaggedError()("ResultUnreadable", { nextStep: Schema.String }, { httpApiStatus: 500 }) {};
7409
+ /**
7410
+ * Where a request stands, for whoever asked: still waiting; approved, and running as soon as
7411
+ * ref0 can; refused; expired; or what it ran to, with the endpoint it ran, whose schemas read it.
7412
+ */
7413
+ const ApprovalState = Schema.Union([
7414
+ Schema.Struct({
7415
+ state: Schema.Literal("waiting"),
7416
+ expiresAt: Timestamp
7417
+ }),
7418
+ Schema.Struct({
7419
+ state: Schema.Literal("approved"),
7420
+ /** A machine's answering browser may forget its saved answer once the terminal kept it. */
7421
+ collected: Schema.optionalKey(Schema.Literal(true))
7422
+ }),
7423
+ Schema.Struct({ state: Schema.Literal("refused") }),
7424
+ Schema.Struct({ state: Schema.Literal("expired") }),
7425
+ Schema.Struct({
7426
+ state: Schema.Literal("ran"),
7427
+ endpoint: CommandEndpoint,
7428
+ ...Ran.fields
7429
+ })
7430
+ ]);
7431
+ /** Only the answering browser receives a machine's answer code; state reads never carry it. */
7432
+ const ApprovalAnswer = Schema.Union([
7433
+ Schema.Struct({
7434
+ ...ApprovalState.members[1].fields,
7435
+ answerCode: ApprovalCode
7436
+ }),
7437
+ Schema.Struct({
7438
+ ...ApprovalState.members[4].fields,
7439
+ answerCode: ApprovalCode
7440
+ }),
7441
+ ApprovalState
7442
+ ]);
7443
+ /** A request that isn't there, or isn't the caller's to see, answers the same. */
7444
+ var ApprovalNotFound = class extends Schema.TaggedError()("ApprovalNotFound", { nextStep: Schema.String }, { httpApiStatus: 404 }) {};
7445
+ /** Too many asked, or too many codes typed, from here in the last hour. */
7446
+ var SlowDown = class extends Schema.TaggedError()("SlowDown", { nextStep: Schema.String }, { httpApiStatus: 429 }) {};
7447
+ /** A command's hash: the SHA-256 of its canonical JSON, as lowercase hex. */
7448
+ const CommandHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
7449
+ Schema.Struct({
7450
+ approval: ApprovalId,
7451
+ endpoint: CommandEndpoint,
7452
+ /** The command's canonical JSON. */
7453
+ command: Schema.String,
7454
+ commandHash: CommandHash,
7455
+ asker: ApprovalAsker,
7456
+ /** The browser session the person answered in, for their log. */
7457
+ credential: CredentialRef,
7458
+ generation: Generation,
7459
+ /** When the request expires, in epoch milliseconds. */
7460
+ deadline: Schema.Int,
7461
+ challenge: Schema.String,
7462
+ /** A machine answer retries only from its answering session, with this key. */
7463
+ answerKey: Schema.optionalKey(RequestId),
7464
+ /** An agent's ask: the credential it asked with, checked again before the answer counts. */
7465
+ asking: Schema.optionalKey(TokenId),
7466
+ /** The kind of action the person, answering, let agents take without asking from now on. */
7467
+ allow: Schema.optionalKey(ScopeAction)
7468
+ });
7469
+ Schema.Struct({
7470
+ approval: ApprovalId,
7471
+ action: ScopeAction,
7472
+ credential: CredentialRef,
7473
+ generation: Generation
7474
+ });
7475
+ Schema.Union([Schema.Struct({ denied: Schema.Literal(true) }), Schema.Struct({
7476
+ denied: Schema.Literal(false),
7477
+ reason: Schema.Literals(["generation moved", "not set up"])
7478
+ })]);
7479
+ Schema.Union([Schema.Struct({
7480
+ admitted: Schema.Literal(true),
7481
+ admittedAt: Schema.Int,
7482
+ answerCode: Schema.optionalKey(ApprovalCode)
7483
+ }), Schema.Struct({
7484
+ admitted: Schema.Literal(false),
7485
+ reason: Schema.Literals([
7486
+ "expired",
7487
+ "generation moved",
7488
+ "challenge used",
7489
+ "not set up",
7490
+ "asker gone"
7491
+ ])
7492
+ })]);
7493
+ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
7494
+ found: Schema.Literal(true),
7495
+ endpoint: CommandEndpoint,
7496
+ command: Schema.String,
7497
+ commandHash: CommandHash,
7498
+ generation: Generation,
7499
+ generationNow: Generation,
7500
+ /** The `approval.answered` its admission recorded in the person's log. */
7501
+ answeredEvent: EventId,
7502
+ /** Who asked, as that `approval.answered` says. */
7503
+ asker: ApprovalAsker,
7504
+ /** What running it came to, once it ran, as `KeptRan` keeps it. */
7505
+ result: Schema.optionalKey(Schema.String)
7506
+ })]);
7507
+ /**
7508
+ * A `Ran` as it's kept, on its receipt and on its approval: its JSON, as text. The Owner DO
7509
+ * keeps it as it comes and runs no endpoint's schemas, so it's read only by its endpoint, with
7510
+ * `keptRan`, which refuses one they don't take.
7511
+ */
7512
+ const KeptRan = Schema.fromJsonString(Ran);
7513
+ Schema.Struct({
7514
+ approval: ApprovalId,
7515
+ result: Schema.String
7516
+ });
7517
+ Schema.Struct({ result: Schema.String });
7518
+ Schema.Struct({ approval: ApprovalId });
7519
+ Schema.Struct({ approval: ApprovalId });
7520
+ Schema.Union([Schema.Struct({ expired: Schema.Literal(true) }), Schema.Struct({ expired: Schema.Literal(false) })]);
7521
+ Schema.Struct({
7522
+ person: PersonId,
7523
+ approval: ApprovalId
7524
+ });
7525
+ //#endregion
7526
+ //#region ../contracts/src/browser-sessions.ts
7527
+ /**
7528
+ * A browser session's cookie: Better Auth's, named for ref0, `Secure` and host-only on the
7529
+ * app's one origin. It holds the session's token, signed; the directory and the person's Owner DO hold
7530
+ * only the token's SHA-256.
7531
+ */
7532
+ const browserSessionCookie = "__Secure-ref0.session_token";
7533
+ Schema.Struct({
7534
+ token: TokenId,
7535
+ secretHash: SecretHash,
7536
+ expiresAt: Schema.DateTimeUtcFromMillis
7537
+ });
7538
+ Schema.Struct({ registered: Schema.Boolean });
7539
+ /**
7540
+ * Where a signed-in person stands: naming, when the name their GitHub login gave was taken or
7541
+ * kept back, with that login as a suggestion; setting up, once their owner is made and before
7542
+ * it's ready with this session registered, which naming again finishes; or ready.
7543
+ */
7544
+ const Welcome = Schema.Union([Schema.Struct({
7545
+ state: Schema.Literals(["ready", "setting up"]),
7546
+ person: PersonId,
7547
+ owner: OwnerId,
7548
+ name: OwnerName
7549
+ }), Schema.Struct({
7550
+ state: Schema.Literal("naming"),
7551
+ person: PersonId,
7552
+ suggestion: Schema.optionalKey(Schema.String)
7553
+ })]);
7554
+ /**
7555
+ * A browser session renewed: its expiry, in its person's Owner DO and in the directory, and on its cookie.
7556
+ * Renewed at most once a day, so one asked sooner answers its expiry unchanged.
7557
+ */
7558
+ const SessionRenewed = Schema.Struct({ expiresAt: Schema.DateTimeUtcFromMillis });
7559
+ /**
7560
+ * What starting a GitHub sign-in at `POST /auth/sign-in/social` answers: GitHub's address to go
7561
+ * to, with the sign-in's state in a cookie set alongside. Better Auth's answer, which ref0 serves.
7562
+ */
7563
+ const NavigationUrl = Schema.String.check(Schema.isPattern(/^https?:\/\/[^\s/]+(?:\/[^\s]*)?$/), Schema.makeFilter((url) => URL.canParse(url) || "an HTTP(S) URL the platform can parse")).pipe(Schema.brand("NavigationUrl"));
7564
+ Schema.Struct({ url: NavigationUrl });
7565
+ /**
7566
+ * Where signing in sends the browser back to, as `POST /auth/sign-in/social` takes it: a path on
7567
+ * ref0's own origin, such as `/welcome`, or a whole URL. Never a socket's.
7568
+ */
7569
+ const SignInDestination = Schema.String.pipe(Schema.brand("SignInDestination"));
7570
+ Schema.Struct({
7571
+ provider: Schema.Literal("github"),
7572
+ callbackURL: SignInDestination,
7573
+ errorCallbackURL: SignInDestination
7574
+ });
7575
+ /** The name a naming person chose for their owner. */
7576
+ const NameOwner = Schema.Struct({ name: OwnerName });
7577
+ //#endregion
7544
7578
  //#region ../contracts/src/ask-calls.ts
7545
7579
  /**
7546
7580
  * Asks: a question that waits on an answer, from an agent's session or a person, on a thread,
@@ -11659,6 +11693,151 @@ Schema.Struct({
11659
11693
  name: RepoName
11660
11694
  }))
11661
11695
  });
11696
+ /** Where an agent's session asks for an identity token. */
11697
+ const identityTokenPath = "/v1/me/identity-token";
11698
+ /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11699
+ const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11700
+ /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11701
+ const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11702
+ /**
11703
+ * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11704
+ * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11705
+ * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11706
+ */
11707
+ const parsesAsWritten = (audience) => {
11708
+ const parts = audiencePattern.exec(audience);
11709
+ if (parts === null || !URL.canParse(audience)) return false;
11710
+ const url = new URL(audience);
11711
+ const port = parts[2] ?? "";
11712
+ return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11713
+ };
11714
+ /**
11715
+ * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11716
+ * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11717
+ * case and any port too, so `https://verifier.example:443` is another audience than
11718
+ * `https://verifier.example`. ref0 never fetches or resolves it.
11719
+ */
11720
+ const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
11721
+ /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11722
+ const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11723
+ /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11724
+ const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
11725
+ /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11726
+ const IssuedIdentityToken = Schema.Struct({
11727
+ token: IdentityToken,
11728
+ expiresAt: Schema.DateTimeUtcFromMillis
11729
+ });
11730
+ /**
11731
+ * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11732
+ * token, naming its issuance in ref0's log without naming any row or secret.
11733
+ */
11734
+ const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
11735
+ /**
11736
+ * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11737
+ * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11738
+ * audience, different at every other, and carrying no time.
11739
+ */
11740
+ const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11741
+ /** A session, as one audience knows it, made as a person's is. */
11742
+ const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11743
+ /** The accountable person, last in a token's chain: only their pairwise ID. */
11744
+ const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11745
+ /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11746
+ const sessionActing = (act) => Schema.Struct({
11747
+ sub: PairwiseSessionId,
11748
+ act
11749
+ });
11750
+ const oneSession = sessionActing(PersonActor);
11751
+ const twoSessions = sessionActing(oneSession);
11752
+ const threeSessions = sessionActing(twoSessions);
11753
+ /**
11754
+ * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11755
+ * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11756
+ * allows the agent in `sub`, never an actor.
11757
+ */
11758
+ const IdentityActor = Schema.Union([
11759
+ oneSession,
11760
+ twoSessions,
11761
+ threeSessions,
11762
+ sessionActing(threeSessions)
11763
+ ]);
11764
+ /** A time in a token, in whole UTC epoch seconds. */
11765
+ const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11766
+ Schema.Struct({
11767
+ iss: ApiOrigin,
11768
+ sub: AgentId,
11769
+ aud: IdentityAudience,
11770
+ iat: EpochSeconds,
11771
+ exp: EpochSeconds,
11772
+ jti: IdentityTokenId,
11773
+ act: IdentityActor
11774
+ }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11775
+ /**
11776
+ * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11777
+ * never names two keys.
11778
+ */
11779
+ const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11780
+ Schema.Struct({
11781
+ alg: Schema.Literal("ES256"),
11782
+ kid: SigningKeyId,
11783
+ typ: Schema.Literal("JWT")
11784
+ });
11785
+ /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11786
+ const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11787
+ /** One public key in the JWKS: exactly these fields, and never a private one. */
11788
+ const PublicSigningKey = Schema.Struct({
11789
+ kty: Schema.Literal("EC"),
11790
+ crv: Schema.Literal("P-256"),
11791
+ x: Coordinate,
11792
+ y: Coordinate,
11793
+ use: Schema.Literal("sig"),
11794
+ alg: Schema.Literal("ES256"),
11795
+ kid: SigningKeyId
11796
+ });
11797
+ Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11798
+ Schema.Struct({
11799
+ issuer: ApiOrigin,
11800
+ jwks_uri: Schema.String,
11801
+ subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11802
+ id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11803
+ claims_supported: Schema.Tuple([
11804
+ Schema.Literal("iss"),
11805
+ Schema.Literal("sub"),
11806
+ Schema.Literal("aud"),
11807
+ Schema.Literal("exp"),
11808
+ Schema.Literal("iat"),
11809
+ Schema.Literal("jti"),
11810
+ Schema.Literal("act")
11811
+ ])
11812
+ });
11813
+ /**
11814
+ * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11815
+ * `IdentityAudience` says. The refused text is never echoed.
11816
+ */
11817
+ var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11818
+ /**
11819
+ * Less than a whole second is left before the session, a parent or the credential expires, or
11820
+ * the token would expire before it's recorded: the next prompt renews or replaces the session.
11821
+ */
11822
+ var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11823
+ /**
11824
+ * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11825
+ * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11826
+ * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11827
+ * received or used it. A failed append discards the token, so every token has its event.
11828
+ */
11829
+ const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11830
+ session: SessionId,
11831
+ agent: AgentRef,
11832
+ /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11833
+ accountable: PersonId,
11834
+ jti: IdentityTokenId,
11835
+ audience: IdentityAudience,
11836
+ issuer: ApiOrigin,
11837
+ keyId: SigningKeyId,
11838
+ issuedAt: Schema.DateTimeUtcFromMillis,
11839
+ expiresAt: Schema.DateTimeUtcFromMillis
11840
+ }));
11662
11841
  Schema.Literals([
11663
11842
  "info/refs",
11664
11843
  "git-upload-pack",
@@ -11911,6 +12090,71 @@ const GateAddition = Schema.Struct({
11911
12090
  from: NeedFrom
11912
12091
  });
11913
12092
  /**
12093
+ * A yes that met a need: its decider's, given by the person `by` when one gave it, such as an
12094
+ * admin answering as the owner's admins; or, `overridden`, a check ref0 said no to, met by the
12095
+ * yes of each of its overrides.
12096
+ */
12097
+ const NeedYes = Schema.Struct({
12098
+ decider: DeciderRef,
12099
+ by: Schema.optionalKey(PersonId),
12100
+ overridden: Schema.optionalKey(Schema.Literal(true))
12101
+ });
12102
+ /**
12103
+ * An override left of a check ref0 said no to: of a part's failed run, or of the check not running,
12104
+ * and who answers it.
12105
+ */
12106
+ const OverrideLeft = Schema.Struct({
12107
+ of: Schema.Literals(["failed run", "not run"]),
12108
+ by: OverrideAnswerer
12109
+ });
12110
+ /**
12111
+ * A decider a need waits on. For a check ref0 said no to, `overrides` lists each of its overrides
12112
+ * left, every one of whose yeses meets it in a pass's place, none when it has none: one yes to a
12113
+ * gate answers every decider its person may answer as, each override among them.
12114
+ */
12115
+ const NeedWait = Schema.Struct({
12116
+ decider: DeciderRef,
12117
+ overrides: Schema.optionalKey(Schema.Array(OverrideLeft).check(Schema.isMaxLength(maxGateDeciders)))
12118
+ });
12119
+ /**
12120
+ * How one of a version's needs stands under its gate's answers now, as the gate is decided from
12121
+ * them: `met` by each yes in `by`; `met` with nothing to wait for, `as` `no checks`, for `checks`
12122
+ * when none of the repo's checks runs on the change, or `on the tip`, for a check only the merge
12123
+ * queue's tip runs, which judges it before `main` moves; `waiting` on each decider in `on`, when
12124
+ * `all`, or else on a yes from any one of them; `waiting` for ref0 to read which of `main`'s
12125
+ * checks run on the change; or `refused` by a person's no to one of its deciders, or to an
12126
+ * override of one, which decides the whole gate no.
12127
+ */
12128
+ const NeedStanding = Schema.Union([
12129
+ Schema.Struct({
12130
+ state: Schema.Literal("met"),
12131
+ by: Schema.Array(NeedYes).check(Schema.isMaxLength(maxGateDeciders))
12132
+ }),
12133
+ Schema.Struct({
12134
+ state: Schema.Literal("met"),
12135
+ as: Schema.Literals(["no checks", "on the tip"])
12136
+ }),
12137
+ Schema.Struct({
12138
+ state: Schema.Literal("waiting"),
12139
+ on: Schema.Array(NeedWait).check(Schema.isMaxLength(maxGateDeciders)),
12140
+ all: Schema.Boolean
12141
+ }),
12142
+ Schema.Struct({
12143
+ state: Schema.Literal("waiting"),
12144
+ for: Schema.Literal("checks read")
12145
+ }),
12146
+ Schema.Struct({
12147
+ state: Schema.Literal("refused"),
12148
+ decider: DeciderRef,
12149
+ by: PersonId
12150
+ })
12151
+ ]);
12152
+ /** A version's need, with every place it came from, and how it stands once its gate is open. */
12153
+ const GateNeedView = Schema.Struct({
12154
+ ...GateNeed.fields,
12155
+ standing: Schema.optionalKey(NeedStanding)
12156
+ });
12157
+ /**
11914
12158
  * The version's gate decided again in place under `main`'s `.ref0/` now (Q7c): on which `main`
11915
12159
  * and under its tree, `pending` until `main`'s gate answers, then `held` or `grew`; the needs
11916
12160
  * it answered, joined to ref0's floor, absent while pending; what `main`'s gate read of the
@@ -12012,8 +12256,21 @@ const GateView = Schema.Struct({
12012
12256
  state: GateState,
12013
12257
  /** Why the gate failed; null otherwise. */
12014
12258
  why: Schema.NullOr(Schema.String),
12015
- /** None while pending; once answered, the floor's too, sorted as they're spelled. */
12016
- needs: Schema.Array(GateNeed),
12259
+ /**
12260
+ * None while pending; once answered, the floor's too, sorted as they're spelled, each with how
12261
+ * it stands once the version's gate is open.
12262
+ */
12263
+ needs: Schema.Array(GateNeedView),
12264
+ /** The repo's owner, whose admins answer its admins' needs: at home, its person alone. */
12265
+ owner: OwnerId,
12266
+ /**
12267
+ * The names of the owner and of the people its needs' standings name, where the directory has
12268
+ * them.
12269
+ */
12270
+ names: Schema.optionalKey(Schema.Array(Schema.Struct({
12271
+ owner: OwnerId,
12272
+ name: OwnerName
12273
+ })).check(Schema.isMaxLength(maxGateDeciders + 1))),
12017
12274
  /** What the gate read of the change's trees, each read once, in order; none while pending. */
12018
12275
  reads: GateReads,
12019
12276
  /** The project gate revision the version was decided under; none for 0. */
@@ -12984,7 +13241,7 @@ Schema.Struct({
12984
13241
  actor: ActorRef,
12985
13242
  /** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
12986
13243
  attachment: Schema.NullOr(CredentialRef),
12987
- scopes: Schema.Array(Scope$1),
13244
+ scopes: Schema.Array(Scope$2),
12988
13245
  ownerName: OwnerName,
12989
13246
  number: ThreadNumber
12990
13247
  });
@@ -14133,6 +14390,7 @@ const KnownEvent = Schema.Union([
14133
14390
  ThreadListingChanged,
14134
14391
  SessionStarted,
14135
14392
  SessionEnded,
14393
+ IdentityTokenIssued,
14136
14394
  MemberRoleChanged,
14137
14395
  MemberRemoved,
14138
14396
  InvitationSent,
@@ -14994,6 +15252,11 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
14994
15252
  * what to change. Any other part that fails its contract is answered with Effect's empty 400.
14995
15253
  */
14996
15254
  var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
15255
+ /**
15256
+ * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15257
+ * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
15258
+ */
15259
+ var IdentityAudienceRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/IdentityAudienceRefusal", { error: IdentityAudienceRefused }) {};
14997
15260
  const idempotency = { "idempotency-key": RequestId };
14998
15261
  const inProject$1 = {
14999
15262
  owner: OwnerRef,
@@ -15216,7 +15479,18 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
15216
15479
  UsageRangeRefused,
15217
15480
  AuthorityUnavailable
15218
15481
  ]
15219
- })).middleware(Authorization) {};
15482
+ }), HttpApiEndpoint.post("identityToken", identityTokenPath, {
15483
+ payload: IdentityTokenRequest,
15484
+ success: IssuedIdentityToken,
15485
+ error: [
15486
+ IdentityAudienceRefused,
15487
+ IdentityTokenNotReady,
15488
+ NotPermitted,
15489
+ SessionHasEnded,
15490
+ SlowDown,
15491
+ AuthorityUnavailable
15492
+ ]
15493
+ }).middleware(IdentityAudienceRefusal)).middleware(Authorization) {};
15220
15494
  /**
15221
15495
  * A thread's endpoints, as one of a project's threads and an org thread take them alike, by
15222
15496
  * where they are: `at` is a project's path parameters, or the owner's alone for its own scope.
@@ -16152,15 +16426,18 @@ const decisionEndpoints = (at) => {
16152
16426
  HttpApiEndpoint.get("get", "/:decision", {
16153
16427
  params: onDecision,
16154
16428
  query: { wait: Schema.optionalKey(DecisionWait) },
16155
- success: DecisionView,
16429
+ success: NamedDecision,
16156
16430
  error: [...projectErrors, DecisionNotFound]
16157
16431
  }),
16158
16432
  HttpApiEndpoint.post("answer", "/:decision/answers", {
16159
16433
  params: onDecision,
16160
16434
  headers: idempotency,
16161
16435
  payload: AnswerRequest,
16162
- success: DecisionView,
16436
+ success: NamedDecision,
16163
16437
  error: [
16438
+ ApprovalNeeded,
16439
+ CommandRefused,
16440
+ SlowDown,
16164
16441
  ...projectErrors,
16165
16442
  DecisionNotFound,
16166
16443
  AnswerRefused,
@@ -17007,6 +17284,15 @@ const ownSurfaces = {
17007
17284
  cli: "meters",
17008
17285
  mcp: null
17009
17286
  },
17287
+ "me.identityToken": {
17288
+ action: "session:change",
17289
+ actors: "agent",
17290
+ target: "session",
17291
+ stepUp: "never",
17292
+ asks: "never",
17293
+ cli: "identity-token",
17294
+ mcp: null
17295
+ },
17010
17296
  "threads.list": {
17011
17297
  action: "project:read",
17012
17298
  actors: "both",
@@ -18822,271 +19108,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
18822
19108
  /** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
18823
19109
  const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
18824
19110
  //#endregion
18825
- //#region src/session.ts
18826
- /**
18827
- * An agent's command in a harness session that has no ref0 session, though an agent is connected
18828
- * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
18829
- * person instead.
18830
- */
18831
- var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
18832
- /**
18833
- * The token of the session this command runs in, if it runs in one: an agent's, which its
18834
- * harness bound when the session started. Where it's found is the harness's to say. Outside a
18835
- * session, there's none, and every command is the person's, asked by approval.
18836
- */
18837
- var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
18838
- /** No session: until a harness binds one, the person asks, by approval. */
18839
- static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
18840
- };
18841
- //#endregion
18842
- //#region src/call.ts
18843
- /**
18844
- * Every command's one way to ref0: an endpoint's request, sent with the session's token when
18845
- * the command runs in a session, or else asked of the person, by approval, from this terminal.
18846
- * Either way the answer is the endpoint's own, as JSON.
18847
- */
18848
- /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
18849
- var CallFailed = class extends Schema.TaggedError()("CallFailed", {
18850
- status: Schema.Int,
18851
- kind: Schema.String,
18852
- nextStep: Schema.String
18853
- }) {};
18854
- const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
18855
- const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
18856
- const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
18857
- const decodeRefusal = Schema.decodeUnknownOption(AnyError);
18858
- /** How often a terminal asks whether its person has answered. */
18859
- const pollMillis$1 = 2e3;
18860
- /** How long one request may take before it's asked again, or the call gives up. */
18861
- const requestTimeout$1 = Duration.seconds(15);
18862
- /**
18863
- * How long a session's request may take for the commands that read and compile before they
18864
- * answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
18865
- * lacks the check, one paced read at a time, up to 256 files each.
18866
- */
18867
- const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
18868
- /** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
18869
- const giveUpAfterMillis$1 = 3e5;
18870
- /** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
18871
- const outageMillis = 3e5;
18872
- /** What a change that failed partway says: ref0 may have made it all the same. */
18873
- const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
18874
- /**
18875
- * An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
18876
- * 5xx, may have been made before it failed, so it isn't said to try again.
18877
- */
18878
- const failedWith$15 = (status, body, change) => Option.match(decodeRefusal(body), {
18879
- onNone: () => new CallFailed({
18880
- status,
18881
- kind: "Unexpected",
18882
- nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
18883
- }),
18884
- onSome: (refusal) => new CallFailed({
18885
- status,
18886
- kind: refusal._tag,
18887
- nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
18888
- })
18889
- });
18890
- /**
18891
- * A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
18892
- * command's failure: a change that failed at a 5xx may have been made all the same.
18893
- */
18894
- const refusedBy = (status, refusal, change) => new CallFailed({
18895
- status,
18896
- kind: refusal._tag,
18897
- nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
18898
- });
18899
- /**
18900
- * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
18901
- * the CLI made. A change it answered may have run.
18902
- */
18903
- const unreadable = (endpoint, change) => new CallFailed({
18904
- status: 0,
18905
- kind: "Unexpected",
18906
- 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.`
18907
- });
18908
- const unreachable = new CallFailed({
18909
- status: 0,
18910
- kind: "Unreachable",
18911
- nextStep: "Couldn’t reach ref0. Try again."
18912
- });
18913
- /**
18914
- * An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
18915
- * change may have run, and asking again could make it twice.
18916
- */
18917
- const gone = (change, nextStep) => new CallFailed({
18918
- status: 404,
18919
- kind: "ApprovalNotFound",
18920
- nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
18921
- });
18922
- /** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
18923
- const unanswered$1 = new CallFailed({
18924
- status: 0,
18925
- kind: "Unreachable",
18926
- nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
18927
- });
18928
- /**
18929
- * Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
18930
- * stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
18931
- * What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
18932
- */
18933
- const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
18934
- const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
18935
- let approved = false;
18936
- let heardAt = yield* Clock.currentTimeMillis;
18937
- while (true) {
18938
- const answered = yield* collect;
18939
- const now = yield* Clock.currentTimeMillis;
18940
- if (Option.isNone(answered)) {
18941
- if (now - heardAt > outageMillis) return yield* new CallFailed({
18942
- status: 0,
18943
- kind: "Unreachable",
18944
- 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."
18945
- });
18946
- yield* Effect.sleep(pollMillis$1);
18947
- continue;
18948
- }
18949
- const state = answered.value;
18950
- heardAt = now;
18951
- if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
18952
- onNone: () => Effect.fail(unreadable(endpoint, change)),
18953
- onSome: Result.match({
18954
- onSuccess: () => Effect.succeed(state.result),
18955
- onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
18956
- })
18957
- });
18958
- if (state.state === "refused") return yield* new CallFailed({
18959
- status: 403,
18960
- kind: "Refused",
18961
- nextStep: "It was refused."
18962
- });
18963
- if (state.state === "approved") {
18964
- if (!approved) yield* Console.error("Approved. Waiting for it to run.");
18965
- approved = true;
18966
- yield* Effect.sleep(pollMillis$1);
18967
- continue;
18968
- }
18969
- if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
18970
- status: 408,
18971
- kind: "Expired",
18972
- nextStep: "Nobody approved it in time. Run it again."
18973
- });
18974
- yield* Effect.sleep(pollMillis$1);
18975
- }
18976
- });
18977
- var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
18978
- static layer = Layer.effect(Ref0, Effect.gen(function* () {
18979
- const config = yield* Ref0Config;
18980
- const session = yield* SessionToken;
18981
- const http = yield* HttpClient.HttpClient;
18982
- const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
18983
- const timeout = timeoutOf(request.command.endpoint);
18984
- const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
18985
- const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
18986
- const change = request.method !== "GET";
18987
- const lost = change ? unanswered$1 : unreachable;
18988
- const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
18989
- response,
18990
- text
18991
- }))), Effect.timeoutOrElse({
18992
- duration: timeout,
18993
- orElse: () => Effect.fail(lost)
18994
- }), Effect.mapError(() => lost));
18995
- const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
18996
- if (response.status === 202) {
18997
- const needed = decodeNeeded(body);
18998
- if (Option.isNone(needed)) return yield* failedWith$15(202, body, change);
18999
- yield* Console.error(`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`);
19000
- return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19001
- }
19002
- return response.status < 300 ? body : yield* failedWith$15(response.status, body, change);
19003
- });
19004
- const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19005
- const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
19006
- const refused = (status) => (error) => Effect.fail(new CallFailed({
19007
- status,
19008
- kind: error._tag,
19009
- nextStep: error.nextStep
19010
- }));
19011
- return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19012
- duration: requestTimeout$1,
19013
- orElse: () => Effect.succeedNone
19014
- }), Effect.catchTags({
19015
- ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19016
- Unauthenticated: refused(401),
19017
- SessionHasEnded: refused(410)
19018
- }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19019
- });
19020
- const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
19021
- const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
19022
- const { secret, hash } = yield* freshSecret;
19023
- const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
19024
- const started = yield* client.terminalApprovals.start({
19025
- headers: { "idempotency-key": yield* newRequestId },
19026
- payload: {
19027
- command,
19028
- secretHash
19029
- }
19030
- }).pipe(Effect.timeoutOrElse({
19031
- duration: requestTimeout$1,
19032
- orElse: () => Effect.fail(unreachable)
19033
- }), Effect.catchTags({
19034
- CommandRefused: (refused) => Effect.fail(new CallFailed({
19035
- status: 400,
19036
- kind: refused._tag,
19037
- nextStep: refused.nextStep
19038
- })),
19039
- SlowDown: (slow) => Effect.fail(new CallFailed({
19040
- status: 429,
19041
- kind: slow._tag,
19042
- nextStep: slow.nextStep
19043
- }))
19044
- }), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
19045
- yield* Console.error(`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`);
19046
- const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
19047
- 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({
19048
- duration: requestTimeout$1,
19049
- orElse: () => Effect.succeedNone
19050
- }), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19051
- return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19052
- });
19053
- const call = Effect.fn("Ref0.call")(function* (command) {
19054
- const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19055
- status: 0,
19056
- kind: error._tag,
19057
- nextStep: error.nextStep
19058
- })));
19059
- const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19060
- status: 400,
19061
- kind: refused._tag,
19062
- nextStep: refused.nextStep
19063
- })));
19064
- const change = request.method !== "GET";
19065
- const answer = yield* Option.match(token, {
19066
- onNone: () => asPerson(request.command, change, command.endpoint),
19067
- onSome: (bearer) => asSession(bearer, request, command.endpoint)
19068
- });
19069
- if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19070
- return answer;
19071
- });
19072
- return Ref0.of({ call });
19073
- }));
19074
- };
19075
- /**
19076
- * One endpoint's call, typed from the API: the compiler checks its command, and its answer is
19077
- * read by the endpoint's own success schemas, so what a command shows of it is checked too.
19078
- */
19079
- const callEndpoint = (command) => Effect.gen(function* () {
19080
- const json = yield* (yield* Ref0).call(command);
19081
- return {
19082
- json,
19083
- value: yield* Option.match(successOf(command.endpoint, json), {
19084
- onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19085
- onSome: Effect.succeed
19086
- })
19087
- };
19088
- });
19089
- //#endregion
19090
19111
  //#region src/output.ts
19091
19112
  /**
19092
19113
  * What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
@@ -19112,12 +19133,88 @@ const renderLine = (line, terminal) => {
19112
19133
  if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
19113
19134
  return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
19114
19135
  };
19115
- const renderBlocks = (blocks, stream) => {
19136
+ /** The most columns a printed line takes, however wide the terminal. */
19137
+ const mostColumns = 100;
19138
+ /** A printed text without the styles this printer adds. */
19139
+ const unstyled = (text) => text.replaceAll(/\u001b\[\d+m/g, "");
19140
+ const graphemes = new Intl.Segmenter();
19141
+ /** The columns a printed text takes, a character each as the person sees it: styles take none. */
19142
+ const columnsOf = (text) => [...graphemes.segment(unstyled(text))].length;
19143
+ /** A word that says an inline command follows: `run ref0 connect`, `Read a text: ref0 text …`. */
19144
+ const introducesCommand = (word) => word === "run" || word === "Run" || word.endsWith(":");
19145
+ /**
19146
+ * Inline `ref0` commands made single words while they fit in `room`: `ref0` and the word after it
19147
+ * always, and a command after `run` or a colon to its clause's end, so no line breaks inside one.
19148
+ */
19149
+ const withCommandsWhole = (words, room) => {
19150
+ const joined = [];
19151
+ for (let at = 0; at < words.length; at += 1) {
19152
+ const first = words[at];
19153
+ if (first === void 0) continue;
19154
+ if (unstyled(first.word) !== "ref0" || at + 1 >= words.length) {
19155
+ joined.push(first);
19156
+ continue;
19157
+ }
19158
+ const before = words[at - 1];
19159
+ let end = at + 1;
19160
+ if (before !== void 0 && introducesCommand(unstyled(before.word))) while (end + 1 < words.length && !/[.,;:]$/.test(unstyled(words[end]?.word ?? ""))) end += 1;
19161
+ const word = words.slice(at, end + 1).map((each, index) => index === 0 ? each.word : `${each.space}${each.word}`).join("");
19162
+ if (columnsOf(word) > room) {
19163
+ joined.push(first);
19164
+ continue;
19165
+ }
19166
+ joined.push({
19167
+ space: first.space,
19168
+ word
19169
+ });
19170
+ at = end;
19171
+ }
19172
+ return joined;
19173
+ };
19174
+ /**
19175
+ * A line broken between words to fit `width` columns, each line after the first indented as the
19176
+ * first is. A line that fits stays as it is, and a word wider than the width stays whole, on a
19177
+ * line of its own.
19178
+ */
19179
+ const wrapLine = (line, width) => {
19180
+ if (columnsOf(line) <= width) return line;
19181
+ const indent = /^\s*/.exec(line)?.[0] ?? "";
19182
+ const room = Math.max(1, width - columnsOf(indent));
19183
+ const words = withCommandsWhole(Array.from(line.slice(indent.length).matchAll(/(\s*)(\S+)/g), ([, space = "", word = ""]) => ({
19184
+ space,
19185
+ word
19186
+ })), room);
19187
+ const lines = [];
19188
+ let current = "";
19189
+ for (const { space, word } of words) if (current !== "" && columnsOf(current) + columnsOf(space) + columnsOf(word) > room) {
19190
+ lines.push(current);
19191
+ current = word;
19192
+ } else current += current === "" ? word : `${space}${word}`;
19193
+ return [...lines, current].map((each) => `${indent}${each}`).join("\n");
19194
+ };
19195
+ /**
19196
+ * How wide a printed line may be: the terminal's width, at most 100 columns, or none when the
19197
+ * stream isn't a terminal, as for an agent or a script, whose own reader wraps.
19198
+ */
19199
+ const widthOf = (stream) => {
19200
+ if (process[stream].isTTY !== true) return void 0;
19201
+ const columns = process[stream].columns;
19202
+ return columns > 0 ? Math.min(columns, mostColumns) : mostColumns;
19203
+ };
19204
+ const renderBlocks = (blocks, stream, width) => {
19116
19205
  const terminal = process[stream].isTTY === true && process.env["NO_COLOR"] === void 0;
19117
- return blocks.flatMap((block) => block.length === 0 ? [] : [block.map((line) => renderLine(line, terminal)).join("\n")]).join("\n\n");
19206
+ const shown = (line) => {
19207
+ const rendered = renderLine(line, terminal);
19208
+ if (width === void 0 || !Predicate.isString(line) && "command" in line) return rendered;
19209
+ return rendered.split("\n").map((each) => wrapLine(each, width)).join("\n");
19210
+ };
19211
+ return blocks.flatMap((block) => block.length === 0 ? [] : [block.map(shown).join("\n")]).join("\n\n");
19118
19212
  };
19119
- /** Words go to stderr by default; a command's answer goes to stdout. */
19120
- const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream)).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
19213
+ /**
19214
+ * Words go to stderr by default; a command's answer goes to stdout. In a terminal, each line but a
19215
+ * command's is broken between words to fit it.
19216
+ */
19217
+ const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream, widthOf(stream))).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
19121
19218
  /**
19122
19219
  * A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
19123
19220
  * yes. The cursor stays after it, for their answer.
@@ -19187,6 +19284,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
19187
19284
  };
19188
19285
  /** Says what went wrong, once, on stderr, and fails the command. */
19189
19286
  const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] : words).pipe(Effect.andThen(Effect.fail(new Said())));
19287
+ //#endregion
19288
+ //#region src/session.ts
19289
+ /**
19290
+ * An agent's command in a harness session that has no ref0 session, though an agent is connected
19291
+ * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19292
+ * person instead.
19293
+ */
19294
+ var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19295
+ /**
19296
+ * The token of the session this command runs in, if it runs in one: an agent's, which its
19297
+ * harness bound when the session started. Where it's found is the harness's to say. Outside a
19298
+ * session, there's none, and every command is the person's, asked by approval.
19299
+ */
19300
+ var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
19301
+ /** No session: until a harness binds one, the person asks, by approval. */
19302
+ static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
19303
+ };
19304
+ /**
19305
+ * The thread the session this command runs in is on, as its start kept it beside the session's
19306
+ * token. Outside a session there's none, and in one whose start kept none.
19307
+ */
19308
+ var SessionThread = class SessionThread extends Context.Service()("ref0/cli/SessionThread") {
19309
+ /** No session, so no thread. */
19310
+ static none = Layer.succeed(SessionThread, SessionThread.of({ current: Effect.succeedNone }));
19311
+ };
19312
+ //#endregion
19313
+ //#region src/call.ts
19314
+ /**
19315
+ * Every command's one way to ref0: an endpoint's request, sent with the session's token when
19316
+ * the command runs in a session, or else asked of the person, by approval, from this terminal.
19317
+ * Either way the answer is the endpoint's own, as JSON.
19318
+ */
19319
+ /** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
19320
+ var CallFailed = class extends Schema.TaggedError()("CallFailed", {
19321
+ status: Schema.Int,
19322
+ kind: Schema.String,
19323
+ nextStep: Schema.String
19324
+ }) {};
19325
+ const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
19326
+ const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
19327
+ const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
19328
+ const decodeRefusal = Schema.decodeUnknownOption(AnyError);
19329
+ /** How often a terminal asks whether its person has answered. */
19330
+ const pollMillis$1 = 2e3;
19331
+ /** How long one request may take before it's asked again, or the call gives up. */
19332
+ const requestTimeout$1 = Duration.seconds(15);
19333
+ /**
19334
+ * How long a session's request may take for the commands that read and compile before they
19335
+ * answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
19336
+ * lacks the check, one paced read at a time, up to 256 files each.
19337
+ */
19338
+ const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
19339
+ /** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
19340
+ const giveUpAfterMillis$1 = 3e5;
19341
+ /** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
19342
+ const outageMillis = 3e5;
19343
+ /** What a change that failed partway says: ref0 may have made it all the same. */
19344
+ const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
19345
+ /**
19346
+ * An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
19347
+ * 5xx, may have been made before it failed, so it isn't said to try again.
19348
+ */
19349
+ const failedWith$16 = (status, body, change) => Option.match(decodeRefusal(body), {
19350
+ onNone: () => new CallFailed({
19351
+ status,
19352
+ kind: "Unexpected",
19353
+ nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
19354
+ }),
19355
+ onSome: (refusal) => new CallFailed({
19356
+ status,
19357
+ kind: refusal._tag,
19358
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19359
+ })
19360
+ });
19361
+ /**
19362
+ * A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
19363
+ * command's failure: a change that failed at a 5xx may have been made all the same.
19364
+ */
19365
+ const refusedBy = (status, refusal, change) => new CallFailed({
19366
+ status,
19367
+ kind: refusal._tag,
19368
+ nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
19369
+ });
19370
+ /**
19371
+ * An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
19372
+ * the CLI made. A change it answered may have run.
19373
+ */
19374
+ const unreadable = (endpoint, change) => new CallFailed({
19375
+ status: 0,
19376
+ kind: "Unexpected",
19377
+ nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
19378
+ });
19379
+ const unreachable = new CallFailed({
19380
+ status: 0,
19381
+ kind: "Unreachable",
19382
+ nextStep: "Couldn’t reach ref0. Try again."
19383
+ });
19384
+ /**
19385
+ * An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
19386
+ * change may have run, and asking again could make it twice.
19387
+ */
19388
+ const gone = (change, nextStep) => new CallFailed({
19389
+ status: 404,
19390
+ kind: "ApprovalNotFound",
19391
+ nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
19392
+ });
19393
+ /** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
19394
+ const unanswered$1 = new CallFailed({
19395
+ status: 0,
19396
+ kind: "Unreachable",
19397
+ nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
19398
+ });
19399
+ /**
19400
+ * Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
19401
+ * stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
19402
+ * What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
19403
+ */
19404
+ const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
19405
+ const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
19406
+ let approved = false;
19407
+ let heardAt = yield* Clock.currentTimeMillis;
19408
+ while (true) {
19409
+ const answered = yield* collect;
19410
+ const now = yield* Clock.currentTimeMillis;
19411
+ if (Option.isNone(answered)) {
19412
+ if (now - heardAt > outageMillis) return yield* new CallFailed({
19413
+ status: 0,
19414
+ kind: "Unreachable",
19415
+ nextStep: approved ? "It was approved, and ref0 stopped answering before it said it ran. Check whether it did before you run it again." : "ref0 stopped answering before it said how it ended. Check whether it ran before you run it again."
19416
+ });
19417
+ yield* Effect.sleep(pollMillis$1);
19418
+ continue;
19419
+ }
19420
+ const state = answered.value;
19421
+ heardAt = now;
19422
+ if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
19423
+ onNone: () => Effect.fail(unreadable(endpoint, change)),
19424
+ onSome: Result.match({
19425
+ onSuccess: () => Effect.succeed(state.result),
19426
+ onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
19427
+ })
19428
+ });
19429
+ if (state.state === "refused") return yield* new CallFailed({
19430
+ status: 403,
19431
+ kind: "Refused",
19432
+ nextStep: "It was refused."
19433
+ });
19434
+ if (state.state === "approved") {
19435
+ if (!approved) yield* printBlocks([["Approved. Waiting for it to run."]]);
19436
+ approved = true;
19437
+ yield* Effect.sleep(pollMillis$1);
19438
+ continue;
19439
+ }
19440
+ if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
19441
+ status: 408,
19442
+ kind: "Expired",
19443
+ nextStep: "Nobody approved it in time. Run it again."
19444
+ });
19445
+ yield* Effect.sleep(pollMillis$1);
19446
+ }
19447
+ });
19448
+ var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
19449
+ static layer = Layer.effect(Ref0, Effect.gen(function* () {
19450
+ const config = yield* Ref0Config;
19451
+ const session = yield* SessionToken;
19452
+ const http = yield* HttpClient.HttpClient;
19453
+ const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
19454
+ const timeout = timeoutOf(request.command.endpoint);
19455
+ const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
19456
+ const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
19457
+ const change = request.method !== "GET";
19458
+ const lost = change ? unanswered$1 : unreachable;
19459
+ const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
19460
+ response,
19461
+ text
19462
+ }))), Effect.timeoutOrElse({
19463
+ duration: timeout,
19464
+ orElse: () => Effect.fail(lost)
19465
+ }), Effect.mapError(() => lost));
19466
+ const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
19467
+ if (response.status === 202) {
19468
+ const needed = decodeNeeded(body);
19469
+ if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
19470
+ yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
19471
+ return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
19472
+ }
19473
+ return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
19474
+ });
19475
+ const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
19476
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
19477
+ const refused = (status) => (error) => Effect.fail(new CallFailed({
19478
+ status,
19479
+ kind: error._tag,
19480
+ nextStep: error.nextStep
19481
+ }));
19482
+ return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19483
+ duration: requestTimeout$1,
19484
+ orElse: () => Effect.succeedNone
19485
+ }), Effect.catchTags({
19486
+ ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
19487
+ Unauthenticated: refused(401),
19488
+ SessionHasEnded: refused(410)
19489
+ }), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19490
+ });
19491
+ const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
19492
+ const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
19493
+ const { secret, hash } = yield* freshSecret;
19494
+ const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
19495
+ const started = yield* client.terminalApprovals.start({
19496
+ headers: { "idempotency-key": yield* newRequestId },
19497
+ payload: {
19498
+ command,
19499
+ secretHash
19500
+ }
19501
+ }).pipe(Effect.timeoutOrElse({
19502
+ duration: requestTimeout$1,
19503
+ orElse: () => Effect.fail(unreachable)
19504
+ }), Effect.catchTags({
19505
+ CommandRefused: (refused) => Effect.fail(new CallFailed({
19506
+ status: 400,
19507
+ kind: refused._tag,
19508
+ nextStep: refused.nextStep
19509
+ })),
19510
+ SlowDown: (slow) => Effect.fail(new CallFailed({
19511
+ status: 429,
19512
+ kind: slow._tag,
19513
+ nextStep: slow.nextStep
19514
+ }))
19515
+ }), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
19516
+ yield* printBlocks([[`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`]]);
19517
+ const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
19518
+ const collect = (yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http))).terminalApprovals.result({ params: { approval: started.approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
19519
+ duration: requestTimeout$1,
19520
+ orElse: () => Effect.succeedNone
19521
+ }), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
19522
+ return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
19523
+ });
19524
+ const call = Effect.fn("Ref0.call")(function* (command) {
19525
+ const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
19526
+ status: 0,
19527
+ kind: error._tag,
19528
+ nextStep: error.nextStep
19529
+ })));
19530
+ const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
19531
+ status: 400,
19532
+ kind: refused._tag,
19533
+ nextStep: refused.nextStep
19534
+ })));
19535
+ const change = request.method !== "GET";
19536
+ const answer = yield* Option.match(token, {
19537
+ onNone: () => asPerson(request.command, change, command.endpoint),
19538
+ onSome: (bearer) => asSession(bearer, request, command.endpoint)
19539
+ });
19540
+ if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
19541
+ return answer;
19542
+ });
19543
+ return Ref0.of({ call });
19544
+ }));
19545
+ };
19546
+ /**
19547
+ * One endpoint's call, typed from the API: the compiler checks its command, and its answer is
19548
+ * read by the endpoint's own success schemas, so what a command shows of it is checked too.
19549
+ */
19550
+ const callEndpoint = (command) => Effect.gen(function* () {
19551
+ const json = yield* (yield* Ref0).call(command);
19552
+ return {
19553
+ json,
19554
+ value: yield* Option.match(successOf(command.endpoint, json), {
19555
+ onNone: () => Effect.fail(unreadable(command.endpoint, false)),
19556
+ onSome: Effect.succeed
19557
+ })
19558
+ };
19559
+ });
19190
19560
  /** The account's commands. */
19191
19561
  const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
19192
19562
  const answer = yield* callEndpoint({ endpoint: "me.get" });
@@ -20130,7 +20500,7 @@ const waitsFor = (each) => {
20130
20500
  * A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
20131
20501
  * when it's in another of `owner`'s, then its title and stage.
20132
20502
  */
20133
- const threadWords = (thread, owner = "") => {
20503
+ const threadWords$1 = (thread, owner = "") => {
20134
20504
  return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
20135
20505
  };
20136
20506
  /** What holds a stack back below the thread, as a line says it, to follow "and". */
@@ -20148,13 +20518,13 @@ const stackHeld = (stack) => {
20148
20518
  */
20149
20519
  const planLine = (wait, number, owner) => {
20150
20520
  if (wait.kind === "stack") {
20151
- const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
20521
+ const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords$1(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
20152
20522
  return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
20153
20523
  }
20154
20524
  if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
20155
20525
  if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
20156
20526
  const holds = wait.child.project === void 0 || wait.child.stage !== "settled" ? `#${number} is settled once it is` : `#${number} is settled once a settle check reads it so`;
20157
- return `${wait.child.thread} required child ${threadWords(wait.child, owner)}: ${holds}`;
20527
+ return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
20158
20528
  };
20159
20529
  /** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
20160
20530
  const blockedBy$1 = (list) => {
@@ -20180,10 +20550,10 @@ const blockerLines = (list, address) => {
20180
20550
  ].join("\n");
20181
20551
  };
20182
20552
  /** How many blockers stand on the thread now, in words. */
20183
- const standingWords = (changed) => changed.standing === 0 ? "None stands now, so ref0 merges it once it’s ready." : `${changed.standing} ${changed.standing === 1 ? "stands" : "stand"}, so ref0 won’t merge it yet.`;
20553
+ const standingWords$1 = (changed) => changed.standing === 0 ? "None stands now, so ref0 merges it once it’s ready." : `${changed.standing} ${changed.standing === 1 ? "stands" : "stand"}, so ref0 won’t merge it yet.`;
20184
20554
  /** A blocker added, in words: which, its ask, how many stand, and a publish it didn't stop. */
20185
20555
  const addedWords = (changed, address) => [
20186
- `Blocked ${address} by ${changed.blocker}. ${standingWords(changed)}`,
20556
+ `Blocked ${address} by ${changed.blocker}. ${standingWords$1(changed)}`,
20187
20557
  ...changed.decision === void 0 ? [] : [`It waits on a person’s answer to the ask ${changed.decision}.`],
20188
20558
  ...changed.admitted === void 0 ? [] : [`A merge carrying it was already under way in ${changed.admitted.join(", ")}; the blocker doesn’t stop that one.`]
20189
20559
  ].join("\n");
@@ -20330,7 +20700,7 @@ const clearing = (name, description) => Command.make(name, {
20330
20700
  payload
20331
20701
  }));
20332
20702
  const done = name === "remove" ? "Removed" : "Marked met";
20333
- yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords(answer.value)}`);
20703
+ yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords$1(answer.value)}`);
20334
20704
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(description));
20335
20705
  /** `ref0 blocked` and its subcommands. */
20336
20706
  const blockedCommands = [Command.make("blocked").pipe(Command.withDescription("What a thread is blocked by, which stops ref0 merging it: list, add, hold, remove, and mark met"), Command.withShortDescription("List and manage a thread’s blockers"), Command.withSubcommands([
@@ -20359,7 +20729,7 @@ const browserOpener = Effect.gen(function* () {
20359
20729
  });
20360
20730
  /** Starts the browser independently; a missing or failing desktop never stops the paste. */
20361
20731
  const openBrowser = (program, link) => Effect.sync(() => {
20362
- const failed = () => Effect.runFork(Console.error("Couldn’t open the browser. Open the printed link yourself."));
20732
+ const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
20363
20733
  try {
20364
20734
  const child = spawn(program, [link], {
20365
20735
  detached: true,
@@ -21030,7 +21400,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21030
21400
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21031
21401
  * and only as a default export.
21032
21402
  */
21033
- const cliVersion = "0.10.0";
21403
+ const cliVersion = "0.12.0";
21034
21404
  //#endregion
21035
21405
  //#region src/harness/hook-protocol.ts
21036
21406
  /**
@@ -21348,7 +21718,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21348
21718
  const link = yield* freshLinkSecret;
21349
21719
  const pasted = yield* Ref.make(void 0);
21350
21720
  const opener = yield* browserOpener;
21351
- const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(Console.error("Couldn’t listen for the browser. Paste the code instead."), void 0)));
21721
+ const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(printBlocks([["Couldn’t listen for the browser. Paste the code instead."]]), void 0)));
21352
21722
  const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
21353
21723
  const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
21354
21724
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
@@ -21359,7 +21729,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21359
21729
  ...folder,
21360
21730
  shown: shownConfigFolder(home, folder.configFolder),
21361
21731
  grant: yield* freshTokenSecret,
21362
- replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
21732
+ replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said)),
21733
+ before: Option.isSome(held) ? yield* hostGrant.get(folder.configFolder).pipe(Effect.catchTag("KeychainError", (error) => error.unreadable === true ? Effect.succeedNone : said(error))) : Option.none()
21363
21734
  };
21364
21735
  }));
21365
21736
  const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
@@ -21408,9 +21779,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21408
21779
  "Approve it only if you ran this ref0 connect yourself, just now: a yes gives this machine a lasting credential for each agent it connects.",
21409
21780
  "Waiting for the answer. Ctrl-C stops it."
21410
21781
  ]]);
21411
- let typed = "";
21412
- const show = (text) => Effect.ignore(terminal.display(text));
21413
- const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
21782
+ const endWait = yield* Effect.cached(Console.error(""));
21414
21783
  const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
21415
21784
  if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
21416
21785
  const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
@@ -21441,17 +21810,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21441
21810
  yield* Effect.sleep(pollMillis);
21442
21811
  }
21443
21812
  });
21444
- const keepOne = Effect.fnUntraced(function* (folder, connection, keepBy) {
21813
+ const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
21445
21814
  const left = keepBy - (yield* Clock.currentTimeMillis);
21446
- 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.");
21447
- const token = formatToken(connection.credential.person, connection.credential.id, folder.grant.secret);
21448
- const kept = yield* hostGrant.keep(folder.configFolder, {
21449
- harness: folder.harness,
21450
- owner: connection.owner,
21451
- agent: connection.handle,
21452
- agentId: connection.agent.id,
21453
- token: Redacted.make(token)
21454
- }).pipe(Effect.timeoutOrElse({
21815
+ if (left <= 0) return Option.some(late);
21816
+ const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
21455
21817
  duration: Duration.min(keepTimeout, Duration.millis(left)),
21456
21818
  orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
21457
21819
  }), Effect.retry({
@@ -21460,10 +21822,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21460
21822
  }), Effect.result);
21461
21823
  return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
21462
21824
  });
21825
+ const keepOne = (folder, connection, keepBy) => keepTrying(folder.configFolder, {
21826
+ harness: folder.harness,
21827
+ owner: connection.owner,
21828
+ agent: connection.handle,
21829
+ agentId: connection.agent.id,
21830
+ token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
21831
+ }, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
21463
21832
  const finish = Effect.fnUntraced(function* (state) {
21464
21833
  if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
21465
21834
  const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21466
21835
  const kept = [];
21836
+ const keptFrom = [];
21467
21837
  const grants = [];
21468
21838
  const notKept = [];
21469
21839
  for (const folder of asking) {
@@ -21493,9 +21863,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21493
21863
  handle: connection.handle,
21494
21864
  organizations: connection.organizations
21495
21865
  });
21866
+ keptFrom.push(folder);
21496
21867
  grants.push(connection.credential.id);
21497
21868
  }
21498
- yield* oldGrants.retire(kept);
21499
21869
  const [first, ...rest] = grants;
21500
21870
  if (first === void 0) return yield* sayFailed([[
21501
21871
  "It connected, but this machine couldn’t keep what it needs to act:",
@@ -21528,9 +21898,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21528
21898
  times: 2,
21529
21899
  schedule: Schedule.spaced("1 second")
21530
21900
  }), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
21531
- for (const folder of kept) yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
21532
- return yield* sayFailed(`ref0 revoked the grants, since this machine didn’t confirm in time that it kept them. They’re deleted here, and nothing is connected. To connect this machine, run: ${again$1}`);
21901
+ const backBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
21902
+ const notBack = [];
21903
+ for (const folder of keptFrom) {
21904
+ if (Option.isNone(folder.before)) {
21905
+ yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
21906
+ continue;
21907
+ }
21908
+ const failed = yield* keepTrying(folder.configFolder, folder.before.value, backBy, "The keychain took too long with the folders before it.");
21909
+ if (Option.isSome(failed)) notBack.push(`${shownConfigFolder(home, folder.configFolder)}: ${failed.value}`);
21910
+ }
21911
+ return yield* sayFailed([[
21912
+ "ref0 revoked the new grants, since this machine didn’t confirm in time that it kept them.",
21913
+ ...notBack.length === 0 ? ["Each folder has its connection from before back, if it had one. To connect this machine, run:"] : [
21914
+ "These folders hold a grant that works nowhere, since the keychain didn’t take back their records:",
21915
+ ...notBack,
21916
+ "Each other folder has its connection from before back, if it had one. To connect this machine, run:"
21917
+ ],
21918
+ { command: again$1 }
21919
+ ]]);
21533
21920
  }
21921
+ yield* oldGrants.retire(kept);
21534
21922
  return {
21535
21923
  owner: state.owner,
21536
21924
  kept,
@@ -21542,7 +21930,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21542
21930
  };
21543
21931
  });
21544
21932
  const stopped = Effect.gen(function* () {
21545
- yield* Console.error("Stopping: aborting the request.");
21933
+ yield* printBlocks([["Stopping: aborting the request."]]);
21546
21934
  const abortId = yield* newRequestId;
21547
21935
  const state = yield* collector.machineAttachments.abort({
21548
21936
  params,
@@ -21561,46 +21949,41 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21561
21949
  times: 2
21562
21950
  }), Effect.catchTag("Unanswered", () => sayFailed("Couldn’t reach ref0 to abort it. If it’s approved anyway, ref0 revokes its grants 10 minutes after they connect, since this machine never kept them.")));
21563
21951
  if (!isEnded(state)) {
21564
- 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.");
21565
- yield* Console.error("It was already answered, so this machine waits to hear how.");
21952
+ if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", asking.some((folder) => Option.isSome(folder.replaces)) ? "Its new grants expire unconfirmed, and each folder’s connection from before still works." : "Its new grants expire unconfirmed."]]);
21953
+ yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
21566
21954
  }
21567
21955
  const connected = yield* finish(isEnded(state) ? state : yield* answered);
21568
21956
  yield* printBlocks([[{ label: `Connected as ${connected.owner}’s agents` }, ...connected.kept.map((folder) => `${shownConfigFolder(home, folder.configFolder)} ${folder.handle}`)], ["Stopped, so nothing was installed. To install ref0’s plugin into each, run:", ...connected.kept.map((folder) => ({ command: installLine(folder.configFolder) }))]]);
21569
21957
  });
21570
- const typedCode = Effect.gen(function* () {
21958
+ const readAnswer = Effect.gen(function* () {
21959
+ if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21571
21960
  const keys = yield* terminal.readInput;
21961
+ let typed = "";
21572
21962
  while (true) {
21573
21963
  const pressed = yield* Effect.option(Queue.take(keys));
21574
21964
  if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
21965
+ if ((yield* Ref.get(pasted)) !== void 0) continue;
21575
21966
  const { input, key } = pressed.value;
21576
21967
  if (key.name === "backspace") {
21577
- if (typed === "") continue;
21578
21968
  typed = takeBack(typed);
21579
- yield* show("\b \b");
21580
21969
  continue;
21581
21970
  }
21582
21971
  if (key.name !== "return" && key.name !== "enter") {
21583
21972
  const text = Option.getOrElse(input, () => "");
21584
- if (!typesText(text)) continue;
21585
- typed += text;
21586
- yield* show(text);
21973
+ if (typesText(text)) typed += text;
21587
21974
  continue;
21588
21975
  }
21589
21976
  const line = typed.trim();
21590
- if (typed !== "") yield* show("\n");
21591
21977
  typed = "";
21592
21978
  if (line === "") continue;
21593
- if (isAnswerCode(line)) return line;
21594
- yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21979
+ if (!isAnswerCode(line)) {
21980
+ yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
21981
+ continue;
21982
+ }
21983
+ yield* Ref.update(pasted, (first) => first ?? line);
21984
+ if (listener !== void 0) yield* listener.stop;
21595
21985
  }
21596
21986
  }).pipe(Effect.scoped);
21597
- const readAnswer = Effect.gen(function* () {
21598
- yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21599
- const code = yield* typedCode;
21600
- yield* Ref.update(pasted, (first) => first ?? code);
21601
- if (listener !== void 0) yield* listener.stop;
21602
- return yield* Effect.never;
21603
- });
21604
21987
  const receiveAnswer = listener === void 0 ? readAnswer : Effect.raceFirst(readAnswer, listener.answer.pipe(Effect.flatMap((code) => Ref.update(pasted, (first) => first ?? code)), Effect.andThen(Effect.never)));
21605
21988
  return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.ensuring(endWait), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
21606
21989
  }, Effect.scoped);
@@ -22580,6 +22963,111 @@ function parse(toml, options = {}) {
22580
22963
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22581
22964
  */
22582
22965
  //#endregion
22966
+ //#region src/harness/empty-folder.ts
22967
+ /**
22968
+ * The folder one of a harness's own commands runs in: made empty for it in the system's temporary
22969
+ * folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
22970
+ * runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
22971
+ *
22972
+ * Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
22973
+ * from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
22974
+ * `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
22975
+ * ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
22976
+ * 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
22977
+ * a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
22978
+ */
22979
+ const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
22980
+ //#endregion
22981
+ //#region src/harness/codex-app-server.ts
22982
+ /**
22983
+ * Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
22984
+ * from an empty folder of its own, the requests written a line each, the answers read until each
22985
+ * request has one, then stopped. Each answer is matched to its request by ID, and read by that
22986
+ * request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
22987
+ * didn't send, is passed over.
22988
+ */
22989
+ /** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
22990
+ var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
22991
+ /** Codex's app server answered a request with an error. */
22992
+ var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
22993
+ method: Schema.String,
22994
+ error: CodexError
22995
+ }) {};
22996
+ /** Codex's app server answered a request with a result its method's schema doesn't read. */
22997
+ var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
22998
+ method: Schema.String,
22999
+ issue: Schema.String
23000
+ }) {};
23001
+ const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
23002
+ const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
23003
+ /** The IDs of the exchange's two requests: the opening's, and the one asked. */
23004
+ const initializeId = "ref0-initialize";
23005
+ const askedId = "ref0-asked";
23006
+ /** The exchange's opening: who's asking, then that it's ready. */
23007
+ const opening = [{
23008
+ jsonrpc: "2.0",
23009
+ id: initializeId,
23010
+ method: codexMethods.initialize.method,
23011
+ params: { clientInfo: {
23012
+ name: "ref0",
23013
+ title: null,
23014
+ version: cliVersion
23015
+ } }
23016
+ }, {
23017
+ jsonrpc: "2.0",
23018
+ method: codexMethods.initialized.method
23019
+ }];
23020
+ /**
23021
+ * Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
23022
+ * answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
23023
+ */
23024
+ const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
23025
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23026
+ const fs = yield* FileSystem.FileSystem;
23027
+ const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
23028
+ cwd: yield* emptyFolder(fs),
23029
+ env,
23030
+ extendEnv: true,
23031
+ forceKillAfter: "2 seconds",
23032
+ stdin: {
23033
+ stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
23034
+ endOnDone: false
23035
+ }
23036
+ }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
23037
+ return Option.filter(answered, (answers) => answers.size === ids.length);
23038
+ })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
23039
+ /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
23040
+ const resultOf = (method, answer) => {
23041
+ if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
23042
+ if ("error" in answer) return Effect.fail(new CodexRefused({
23043
+ method: method.method,
23044
+ error: answer.error
23045
+ }));
23046
+ const { result } = method;
23047
+ return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
23048
+ method: method.method,
23049
+ issue: error.message
23050
+ })));
23051
+ };
23052
+ /**
23053
+ * One exchange with Codex's app server under the person's Codex home: the opening, then `method`
23054
+ * asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
23055
+ * its request's ID, and its result is read by the method's result schema.
23056
+ */
23057
+ const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
23058
+ const asked = {
23059
+ jsonrpc: "2.0",
23060
+ id: askedId,
23061
+ method: method.method,
23062
+ params
23063
+ };
23064
+ const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
23065
+ const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
23066
+ if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
23067
+ yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
23068
+ return yield* resultOf(method, answers.value.get(askedId));
23069
+ });
23070
+ //#endregion
22583
23071
  //#region src/harness/codex-checkouts.ts
22584
23072
  const codexGitFolder = (configFolder, pluginId) => {
22585
23073
  const at = pluginId.indexOf("@");
@@ -22696,111 +23184,6 @@ const codexSignOff = [
22696
23184
  `perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
22697
23185
  ];
22698
23186
  //#endregion
22699
- //#region src/harness/empty-folder.ts
22700
- /**
22701
- * The folder one of a harness's own commands runs in: made empty for it in the system's temporary
22702
- * folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
22703
- * runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
22704
- *
22705
- * Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
22706
- * from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
22707
- * `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
22708
- * ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
22709
- * 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
22710
- * a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
22711
- */
22712
- const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
22713
- //#endregion
22714
- //#region src/harness/codex-app-server.ts
22715
- /**
22716
- * Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
22717
- * from an empty folder of its own, the requests written a line each, the answers read until each
22718
- * request has one, then stopped. Each answer is matched to its request by ID, and read by that
22719
- * request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
22720
- * didn't send, is passed over.
22721
- */
22722
- /** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
22723
- var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
22724
- /** Codex's app server answered a request with an error. */
22725
- var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
22726
- method: Schema.String,
22727
- error: CodexError
22728
- }) {};
22729
- /** Codex's app server answered a request with a result its method's schema doesn't read. */
22730
- var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
22731
- method: Schema.String,
22732
- issue: Schema.String
22733
- }) {};
22734
- const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
22735
- const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
22736
- /** The IDs of the exchange's two requests: the opening's, and the one asked. */
22737
- const initializeId = "ref0-initialize";
22738
- const askedId = "ref0-asked";
22739
- /** The exchange's opening: who's asking, then that it's ready. */
22740
- const opening = [{
22741
- jsonrpc: "2.0",
22742
- id: initializeId,
22743
- method: codexMethods.initialize.method,
22744
- params: { clientInfo: {
22745
- name: "ref0",
22746
- title: null,
22747
- version: cliVersion
22748
- } }
22749
- }, {
22750
- jsonrpc: "2.0",
22751
- method: codexMethods.initialized.method
22752
- }];
22753
- /**
22754
- * Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
22755
- * answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
22756
- */
22757
- const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
22758
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
22759
- const fs = yield* FileSystem.FileSystem;
22760
- const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
22761
- cwd: yield* emptyFolder(fs),
22762
- env,
22763
- extendEnv: true,
22764
- forceKillAfter: "2 seconds",
22765
- stdin: {
22766
- stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
22767
- endOnDone: false
22768
- }
22769
- }))).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);
22770
- return Option.filter(answered, (answers) => answers.size === ids.length);
22771
- })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
22772
- /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
22773
- const resultOf = (method, answer) => {
22774
- if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
22775
- if ("error" in answer) return Effect.fail(new CodexRefused({
22776
- method: method.method,
22777
- error: answer.error
22778
- }));
22779
- const { result } = method;
22780
- return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
22781
- method: method.method,
22782
- issue: error.message
22783
- })));
22784
- };
22785
- /**
22786
- * One exchange with Codex's app server under the person's Codex home: the opening, then `method`
22787
- * asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
22788
- * its request's ID, and its result is read by the method's result schema.
22789
- */
22790
- const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
22791
- const asked = {
22792
- jsonrpc: "2.0",
22793
- id: askedId,
22794
- method: method.method,
22795
- params
22796
- };
22797
- const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
22798
- const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
22799
- if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
22800
- yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
22801
- return yield* resultOf(method, answers.value.get(askedId));
22802
- });
22803
- //#endregion
22804
23187
  //#region src/harness/commit-hooks.ts
22805
23188
  /**
22806
23189
  * ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
@@ -23110,21 +23493,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
23110
23493
  parameters: shellWord(`include.path=${file}`)
23111
23494
  };
23112
23495
  });
23496
+ /** Only the single ref0 include installed by this CLI belongs to ref0. */
23497
+ const selectedInclude = (set) => {
23498
+ const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
23499
+ const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
23500
+ return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
23501
+ };
23502
+ const decodePolicy = Schema.decodeUnknownEffect(Schema.Struct({ shell_environment_policy: CodexConfigRead.fields.config.fields.shell_environment_policy }));
23503
+ /**
23504
+ * The agent's include a Codex config folder's `config.toml` selects, if it's ref0's: its file, read
23505
+ * as TOML for its `shell_environment_policy` alone.
23506
+ */
23507
+ const selectedCodexInclude = Effect.fnUntraced(function* (configFolder) {
23508
+ const fs = yield* FileSystem.FileSystem;
23509
+ const configFile = `${configFolder}/config.toml`;
23510
+ if (!(yield* fs.exists(configFile))) return Option.none();
23511
+ const text = yield* fs.readFileString(configFile);
23512
+ const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
23513
+ const read = yield* decodePolicy(document);
23514
+ return Option.fromNullishOr(selectedInclude(read.shell_environment_policy?.set ?? {}));
23515
+ });
23516
+ /**
23517
+ * Takes the author out of `selected`, the include a folder's `config.toml` selects, unless it's
23518
+ * this origin's include for the folder's agent, so a folder switched to another agent never names
23519
+ * the one before. Its commits name the person until an install selects the agent's own. Whether it
23520
+ * took one out.
23521
+ */
23522
+ const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, selected, options) {
23523
+ const name = yield* originPluginName((yield* Ref0Config).api);
23524
+ if (selected === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return false;
23525
+ const oldRoot = selected.slice(0, selected.lastIndexOf("/agents/"));
23526
+ const fallback = yield* codexAgentConfig(agent, oldRoot, options.home);
23527
+ yield* writeCodexCheckout({
23528
+ gitFolder: oldRoot,
23529
+ common: "",
23530
+ folder: "",
23531
+ signsOff: false,
23532
+ clearAuthor: selected,
23533
+ author: fallback
23534
+ });
23535
+ return true;
23536
+ });
23537
+ /** What the agent reads at its session's start when its folder's `config.toml` names another. */
23538
+ const otherAgentWords = (configFolder) => `ref0: The config.toml of this Codex config folder, ${configFolder}, names another agent than the one this folder is connected as, so your commits name the person instead of you until they run, in their own terminal: ${installLine(configFolder)}`;
23113
23539
  /**
23114
23540
  * Codex has no environment file: its install selects a file; session start fills its checkout,
23115
- * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`.
23541
+ * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`. An include
23542
+ * `config.toml` selects for another agent, after a switch whose install never finished or another
23543
+ * origin's install, has its author taken out first, and the agent hears so.
23116
23544
  */
23117
23545
  const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
23118
23546
  const folders = yield* ConfigFolders;
23119
23547
  const fs = yield* FileSystem.FileSystem;
23120
23548
  const path = yield* Path.Path;
23121
23549
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23122
- const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex")).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23550
+ const configFolder = yield* folders.folder("codex");
23551
+ const selected = yield* selectedCodexInclude(configFolder).pipe(Effect.mapError(() => new CodexCommitFailed({ detail: `Couldn’t read Codex’s config, ${configFolder}/config.toml.` })), Effect.result);
23552
+ const agent = yield* (yield* HostGrant).get(configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23123
23553
  if (Option.isNone(agent)) return false;
23554
+ const home = yield* Config.String("HOME");
23555
+ if (yield* Option.match(yield* Effect.fromResult(selected), {
23556
+ onNone: () => Effect.succeed(false),
23557
+ onSome: (file) => clearOtherCodexAuthor(agent.value, file, {
23558
+ configFolder,
23559
+ home
23560
+ })
23561
+ })) yield* Console.log(otherAgentWords(configFolder));
23124
23562
  const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
23125
23563
  if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
23126
23564
  const gitFolder = path.join(path.dirname(pluginRoot), "@git");
23127
- const home = yield* Config.String("HOME");
23128
23565
  const author = yield* codexAgentConfig(agent.value, gitFolder, home);
23129
23566
  const common = (yield* spawner.string(ChildProcess.make("git", [
23130
23567
  "-C",
@@ -23480,35 +23917,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
23480
23917
  //#region src/harness/codex-install-commits.ts
23481
23918
  const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
23482
23919
  const sameJson = Schema.toEquivalence(Schema.Json);
23483
- /** Only the single ref0 include installed by this CLI belongs to ref0. */
23484
- const selectedInclude = (set) => {
23485
- const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
23486
- const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
23487
- return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
23920
+ /**
23921
+ * What the person reads when an install that never finished took the agent before out of a
23922
+ * folder's commits: what stopped it, in its own words, a conflict only when there is one, then
23923
+ * what to run.
23924
+ */
23925
+ const unfinishedWords = (stopped, configFolder) => {
23926
+ return `ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. ${Option.match(stopped, {
23927
+ onNone: () => `The install stopped before it finished. To finish it, run: ${installLine(configFolder)}`,
23928
+ onSome: (failed) => `${failed.says} Then run: ${repairOf(failed, configFolder)}`
23929
+ })}`;
23488
23930
  };
23489
- /** A changed grant must never leave the previous agent as author after a failed install. */
23490
- const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, options) {
23491
- const fs = yield* FileSystem.FileSystem;
23492
- const configFile = `${options.configFolder}/config.toml`;
23493
- if (!(yield* fs.exists(configFile))) return;
23494
- const text = yield* fs.readFileString(configFile);
23495
- const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
23496
- const read = yield* decodeProfile({ config: document });
23497
- const file = selectedInclude(read.config.shell_environment_policy?.set ?? {});
23498
- if (file === void 0) return;
23499
- const name = yield* originPluginName((yield* Ref0Config).api);
23500
- if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
23501
- const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
23502
- const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
23503
- yield* writeCodexCheckout({
23504
- gitFolder: oldRoot,
23505
- common: "",
23506
- folder: "",
23507
- signsOff: false,
23508
- clearAuthor: file,
23509
- author: fallback
23510
- });
23511
- yield* Console.error(`ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. Fix the shell_environment_policy conflict in Codex’s config or profile named above: remove conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded. Then run: ${installLine(options.configFolder)}`);
23931
+ /**
23932
+ * After an install into a Codex folder fails or is interrupted, the agent before is taken out of
23933
+ * the include `config.toml` still selects, so the folder's commits never name an agent other than
23934
+ * its grant's, and the person hears what stopped the install: `stopped`, or none when interrupted.
23935
+ */
23936
+ const clearUnfinishedCodexInstall = Effect.fnUntraced(function* (agent, stopped, options) {
23937
+ const selected = yield* selectedCodexInclude(options.configFolder);
23938
+ if (Option.isNone(selected)) return;
23939
+ if (!(yield* clearOtherCodexAuthor(agent, selected.value, options))) return;
23940
+ yield* printBlocks([[unfinishedWords(stopped, options.configFolder)]]);
23512
23941
  });
23513
23942
  /** Install the connected agent's include and shell policy. */
23514
23943
  const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
@@ -23517,6 +23946,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23517
23946
  CODEX_HOME: options.configFolder
23518
23947
  };
23519
23948
  const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
23949
+ const conflicted = new InstallFailed({ says: "Codex’s shell_environment_policy conflicts with ref0’s commit environment, in the config or profile named above. Remove the conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded." });
23520
23950
  return yield* Effect.gen(function* () {
23521
23951
  const fs = yield* FileSystem.FileSystem;
23522
23952
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -23544,8 +23974,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23544
23974
  ].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
23545
23975
  const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
23546
23976
  if (collision !== void 0 || allowlist) {
23547
- 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.`);
23548
- if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
23977
+ yield* printBlocks([[`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`]]);
23978
+ if (own) return yield* conflicted;
23549
23979
  return;
23550
23980
  }
23551
23981
  const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
@@ -23570,8 +24000,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23570
24000
  const losesAuthors = excluded !== void 0 && excluded !== null && ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].some((name) => !excluded.some((pattern) => pathMatchesWithin(pattern.toLowerCase(), name.toLowerCase(), { left: 1e4 })));
23571
24001
  const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
23572
24002
  if (key === void 0 && !losesAuthors && !allowlist) continue;
23573
- 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.`);
23574
- if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
24003
+ yield* printBlocks([[`ref0: Codex’s profile ${file} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`]]);
24004
+ if (own) return yield* conflicted;
23575
24005
  return;
23576
24006
  }
23577
24007
  if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
@@ -23659,9 +24089,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
23659
24089
  }, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
23660
24090
  }).pipe(Effect.onError(() => Effect.gen(function* () {
23661
24091
  if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
23662
- else yield* Console.error("ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again.");
24092
+ else yield* printBlocks([["ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again."]]);
23663
24093
  }).pipe(Effect.orDie)));
23664
- }).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
24094
+ }).pipe(Effect.scoped, Effect.mapError((error) => error instanceof InstallFailed ? error : failed("set Codex’s commit environment")));
23665
24095
  }, Effect.scoped);
23666
24096
  //#endregion
23667
24097
  //#region src/harness/install-claude-code.ts
@@ -23825,7 +24255,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
23825
24255
  trusted,
23826
24256
  profile
23827
24257
  };
23828
- }).pipe(Effect.onError(() => clearOtherCodexAuthor(agent, options).pipe(Effect.orDie)));
24258
+ }).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
23829
24259
  });
23830
24260
  //#endregion
23831
24261
  //#region src/harness/install.ts
@@ -23931,7 +24361,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
23931
24361
  for (const folder of into) answers.set(folder, Result.fail(failed));
23932
24362
  };
23933
24363
  const started = /* @__PURE__ */ new Set();
23934
- const clearUnstarted = (into) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearOtherCodexAuthor(agent, {
24364
+ const clearUnstarted = (into, stopped) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearUnfinishedCodexInstall(agent, stopped, {
23935
24365
  configFolder: folder.configFolder,
23936
24366
  home
23937
24367
  }).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
@@ -23946,8 +24376,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
23946
24376
  if (into.length === 0) continue;
23947
24377
  const command = yield* onPath(harness === "codex" ? "codex" : "claude");
23948
24378
  if (Option.isNone(command)) {
23949
- failAll(into, new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` }));
23950
- yield* clearUnstarted(into);
24379
+ const missing = new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` });
24380
+ failAll(into, missing);
24381
+ yield* clearUnstarted(into, Option.some(missing));
23951
24382
  continue;
23952
24383
  }
23953
24384
  yield* Effect.scoped(Effect.gen(function* () {
@@ -23968,9 +24399,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
23968
24399
  })).pipe(Effect.result);
23969
24400
  answers.set(folder, installed);
23970
24401
  }
23971
- }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
24402
+ }).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into, Option.some(failed)))))));
23972
24403
  }
23973
- }).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
24404
+ }).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
23974
24405
  return folders.map((folder) => ({
23975
24406
  ...folder,
23976
24407
  installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
@@ -24062,7 +24493,12 @@ const mostFolders = 8;
24062
24493
  const isConfigFolderPath = Schema.is(ConfigFolderPath);
24063
24494
  /** The line that asks again. */
24064
24495
  const again = "ref0 connect";
24065
- /** Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. */
24496
+ /**
24497
+ * Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. The
24498
+ * question is said first, and the chooser shows only the folders: once Enter chooses, it leaves
24499
+ * the ticked ones on one line after any question it was given, which the terminal would break
24500
+ * mid-word.
24501
+ */
24066
24502
  const choose = Effect.fnUntraced(function* (found, home) {
24067
24503
  const choices = found.map((folder) => {
24068
24504
  const shown = shownConfigFolder(home, folder.configFolder);
@@ -24079,8 +24515,9 @@ const choose = Effect.fnUntraced(function* (found, home) {
24079
24515
  };
24080
24516
  });
24081
24517
  const ticked = choices.filter((choice) => choice.selected).length;
24518
+ const keys = "Space ticks or unticks one; Enter connects those ticked.";
24519
+ yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
24082
24520
  return yield* Prompt.run(Prompt.MultiSelect({
24083
- message: ticked > mostFolders ? `Connect which config folders? One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. Space ticks or unticks one; Enter connects those ticked.` : "Connect which config folders? Space ticks or unticks one; Enter connects those ticked.",
24084
24521
  choices,
24085
24522
  max: mostFolders
24086
24523
  }));
@@ -24197,6 +24634,7 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
24197
24634
  if (found.length === 0) return yield* sayFailed(`ref0 found no Claude Code or Codex config folder in your home. Start Claude Code or Codex once, then run ${again} again.`);
24198
24635
  const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
24199
24636
  if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
24637
+ yield* Console.error("");
24200
24638
  const connected = yield* requestAttachment(chosen);
24201
24639
  const installed = yield* installInto(connected.kept, { asJson });
24202
24640
  const others = yield* otherOwners(connected);
@@ -24271,7 +24709,7 @@ const signalLines = (answer) => {
24271
24709
  * credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
24272
24710
  * thread in the project sends a signal to one of its connections by name.
24273
24711
  */
24274
- const failedWith$14 = (failed) => sayFailed(failed.nextStep);
24712
+ const failedWith$15 = (failed) => sayFailed(failed.nextStep);
24275
24713
  const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
24276
24714
  const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
24277
24715
  const list$5 = Command.make("list", {
@@ -24283,7 +24721,7 @@ const list$5 = Command.make("list", {
24283
24721
  params: projectParts(address)
24284
24722
  });
24285
24723
  yield* printAnswer(answer.json, asJson, connectionLines(answer.value));
24286
- }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("The project’s connections, their last signal and what they dropped"), Command.withShortDescription("List a project’s connections and their last signals"));
24724
+ }).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("The project’s connections, their last signal and what they dropped"), Command.withShortDescription("List a project’s connections and their last signals"));
24287
24725
  /** A change made on the project's page: its link, and what to do there. */
24288
24726
  const onThePage = (address, change) => Effect.gen(function* () {
24289
24727
  const config = yield* Ref0Config;
@@ -24359,14 +24797,14 @@ const send = Command.make("send", {
24359
24797
  })
24360
24798
  });
24361
24799
  yield* printAnswer(answer.json, asJson, signalLines(answer.value));
24362
- }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
24800
+ }).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
24363
24801
  /** A project's connections, and sending a signal by one. */
24364
24802
  const connectionCommands = [connectionCommand, Command.make("signal").pipe(Command.withDescription("A project’s signals: send one by a connection"), Command.withShortDescription("Send a signal to a project"), Command.withSubcommands([send]))];
24365
24803
  //#endregion
24366
24804
  //#region src/harness/checkout.ts
24367
24805
  const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
24368
24806
  const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
24369
- const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
24807
+ const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
24370
24808
  const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
24371
24809
  var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24372
24810
  static layer = Layer.effect(Checkout, Effect.gen(function* () {
@@ -24405,7 +24843,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
24405
24843
  "config",
24406
24844
  "--get",
24407
24845
  "ref0.thread"
24408
- ]), (written) => decodeThread$1(written.trim()));
24846
+ ]), (written) => decodeThread$2(written.trim()));
24409
24847
  const repoAt = (folder) => Effect.map(git(folder, [
24410
24848
  "config",
24411
24849
  "--get",
@@ -24582,7 +25020,7 @@ const paramValues = (params) => {
24582
25020
  * its params, and when it ends; under it, each workflow's run, by its title and its file, and
24583
25021
  * how it stands; and under each, its newest asks and passkeys.
24584
25022
  */
24585
- const waitingLines = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
25023
+ const waitingLines$1 = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
24586
25024
  const last = index === wait.runs.length - 1;
24587
25025
  const under = last ? " " : "│ ";
24588
25026
  return [`${last ? "└─" : "├─"} ${run.title ?? run.workflow} · ${waitedText[run.state]} · ${run.workflow}.ts`, ...run.leaves.map((leaf, at) => `${under}${at === run.leaves.length - 1 ? "└─" : "├─"} ${leafText(leaf, now)}`)];
@@ -25408,7 +25846,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25408
25846
  })).value;
25409
25847
  if (code.compiled.entry.reaches?.browser === true) return yield* sayFailed(`${code.check} browses, and only a run on ref0 browses: run it there, with ref0 run ${check} --thread ${thread.value}.`);
25410
25848
  if (code.runs.length === 0) {
25411
- yield* Console.error(`${code.check} has no part left for a local run on version ${code.version}: each is met, or failed on ref0. See ref0 gate.`);
25849
+ yield* printBlocks([[`${code.check} has no part left for a local run on version ${code.version}: each is met, or failed on ref0. See ref0 gate.`]]);
25412
25850
  return;
25413
25851
  }
25414
25852
  yield* holdsVersion(root.value, code);
@@ -25417,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25417
25855
  thread: `${thread.value}’s own .ref0/`,
25418
25856
  project: "the project’s checks"
25419
25857
  }[code.code.origin];
25420
- yield* Console.error(`Running ${code.check} from ${from}, as ref0 compiled it for version ${code.version}: its code and its sandboxes’ setup run on this machine, as you, outside any container.`);
25858
+ yield* printBlocks([[`Running ${code.check} from ${from}, as ref0 compiled it for version ${code.version}: its code and its sandboxes’ setup run on this machine, as you, outside any container.`]]);
25421
25859
  const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
25422
25860
  const sandboxes = code.compiled.entry.sandboxes ?? [];
25423
25861
  const held = /* @__PURE__ */ new Set();
@@ -25429,7 +25867,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25429
25867
  const run = yield* newRunId;
25430
25868
  const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
25431
25869
  const sandbox = yield* localOpener(root.value, held, sandboxes, deadline);
25432
- yield* Console.error(`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`);
25870
+ yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
25433
25871
  const ran = yield* runPart(own, {
25434
25872
  params,
25435
25873
  host: treeHost(root.value, code.tree),
@@ -25492,7 +25930,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
25492
25930
  /** Says a waiting start's tree on stderr, as it stands now. */
25493
25931
  const sayTree = Effect.fnUntraced(function* (wait) {
25494
25932
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25495
- yield* Console.error(printable(waitingLines(wait, now)));
25933
+ yield* printBlocks([[waitingLines$1(wait, now)]]);
25496
25934
  });
25497
25935
  /**
25498
25936
  * Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
@@ -25538,7 +25976,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
25538
25976
  * reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
25539
25977
  * at a time.
25540
25978
  */
25541
- const failedWith$13 = (failed) => sayFailed(failed.nextStep);
25979
+ const failedWith$14 = (failed) => sayFailed(failed.nextStep);
25542
25980
  const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
25543
25981
  const decodeTraced = Schema.decodeUnknownOption(TracedRun);
25544
25982
  /** A run named by a flag, or a failure that says how to name one. */
@@ -25639,8 +26077,8 @@ const run = Command.make("run", {
25639
26077
  value
25640
26078
  }, asJson, rerunCommand);
25641
26079
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25642
- yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25643
- }).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"));
26080
+ yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
26081
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Run a check on main, or on a thread’s head: a run it ran already is answered, not run again, unless the check says reuse: false. A start its check’s start rule makes wait shows what it waits on, and waits up to 9 minutes for it, unless --no-wait. With --local, run a check of the thread’s open version on this machine, and report it"), Command.withShortDescription("Run a check on main or a thread’s head"));
25644
26082
  const trace = Command.make("trace", {
25645
26083
  run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
25646
26084
  repo: repoFlag,
@@ -25683,7 +26121,7 @@ const trace = Command.make("trace", {
25683
26121
  ...narrowed,
25684
26122
  `--cursor ${next}`
25685
26123
  ].join(" ")));
25686
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A run’s trace, a check run’s or a workflow run’s: its steps and its log lines, a page at a time"), Command.withShortDescription("Show a run’s trace and log lines"));
26124
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A run’s trace, a check run’s or a workflow run’s: its steps and its log lines, a page at a time"), Command.withShortDescription("Show a run’s trace and log lines"));
25687
26125
  const waiting$1 = Command.make("waiting", {
25688
26126
  start: startArgument,
25689
26127
  repo: repoFlag,
@@ -25717,8 +26155,8 @@ const waiting$1 = Command.make("waiting", {
25717
26155
  value
25718
26156
  }, asJson, rerunCommand);
25719
26157
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25720
- yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25721
- }).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"));
26158
+ yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
26159
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A start of yours its check’s start rule made wait: what it waits on, as a tree, waiting up to 9 minutes for it to go on or end, unless --no-wait"), Command.withShortDescription("Show what a waiting start waits on"));
25722
26160
  /** What a cancel did, in words. */
25723
26161
  const cancelledText = {
25724
26162
  start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
@@ -25744,7 +26182,7 @@ const cancel = Command.make("cancel", {
25744
26182
  })
25745
26183
  });
25746
26184
  yield* printAnswer(answer.json, asJson, cancelledText[answer.value.cancelled]);
25747
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Cancel a start of yours that waits: yours ends it and the workflow runs it waits on; one that joined another’s only leaves it"), Command.withShortDescription("Cancel a waiting start of yours"));
26185
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Cancel a start of yours that waits: yours ends it and the workflow runs it waits on; one that joined another’s only leaves it"), Command.withShortDescription("Cancel a waiting start of yours"));
25748
26186
  /** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
25749
26187
  const runCommands = [run, Command.make("runs", {
25750
26188
  repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
@@ -25786,14 +26224,14 @@ const runCommands = [run, Command.make("runs", {
25786
26224
  ...narrowed,
25787
26225
  `--before ${next}`
25788
26226
  ].join(" ")));
25789
- }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A repo’s check runs, main’s or a thread’s, the newest first, a page at a time"), Command.withShortDescription("List a repo’s check runs"), Command.withSubcommands([
26227
+ }).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A repo’s check runs, main’s or a thread’s, the newest first, a page at a time"), Command.withShortDescription("List a repo’s check runs"), Command.withSubcommands([
25790
26228
  trace,
25791
26229
  waiting$1,
25792
26230
  cancel
25793
26231
  ]))];
25794
26232
  //#endregion
25795
26233
  //#region src/commands/checks.ts
25796
- const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26234
+ const failedWith$13 = (failed) => sayFailed(failed.nextStep);
25797
26235
  const decodeCheck = Schema.decodeUnknownOption(CheckKey);
25798
26236
  const decodeToken = Schema.decodeUnknownOption(TokenId);
25799
26237
  const decodePerson = Schema.decodeUnknownOption(OwnerName);
@@ -25829,26 +26267,28 @@ const restore = Command.make("restore", {
25829
26267
  }
25830
26268
  });
25831
26269
  yield* printAnswer(answer.json, asJson, restoredWords(answer.value, repo));
25832
- }).pipe(Effect.catchTag("CallFailed", failedWith$12))).pipe(Command.withDescription("Restore a signer a miss revoked from answering a check, as an admin: its local runs, or their yes, count again"), Command.withShortDescription("Restore a revoked check signer"));
26270
+ }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Restore a signer a miss revoked from answering a check, as an admin: its local runs, or their yes, count again"), Command.withShortDescription("Restore a revoked check signer"));
25833
26271
  const signers = Command.make("signers").pipe(Command.withDescription("Who signs a check’s local runs or says yes over its runs: restore one a miss revoked"), Command.withShortDescription("Manage a check’s signers"), Command.withSubcommands([restore]));
25834
26272
  /** `ref0 checks`: a repo's checks. */
25835
26273
  const checksCommands = [Command.make("checks").pipe(Command.withDescription("A repo’s checks: the signers of their local runs and yeses"), Command.withShortDescription("Manage a repo’s check signers"), Command.withSubcommands([signers]))];
25836
26274
  //#endregion
25837
26275
  //#region src/gate-lines.ts
25838
26276
  /**
25839
- * A pull request's gate as `ref0 gate` says it: its current version, whether its needs are
25840
- * answered, the project gate's revision and whether it failed, and each need on its own line
25841
- * with where it came from, ref0's floor or the line of `.ref0/gate.ts`, or of the project gate's,
25842
- * that returned it, with that line's code. A need for one of the project's checks says the
25843
- * project gate revision whose checks run for it, the version's own. Then what each gate read of
25844
- * the change, each read's tree, path and the object it found, short. Then its gate decided again
25845
- * in place under `main`'s `.ref0/`, with `main`'s commit: the needs it held, or those that grew,
25846
- * and why. Then the additions standing on the pull request now, each with the ID `ref0 pr needs
25847
- * remove` takes: the version's own say who added its needs when it was made, and one may have gone
25848
- * since. Then the local runs of its parts' identities, each with its signer; each part that takes
25849
- * local or needs a yes, with its wrappers, the run and the yeses that meet it or what it waits for;
25850
- * and last, each signer revoked from answering one of their checks, an attachment or a person by
25851
- * name, with how an admin restores them.
26277
+ * A pull request's gate as `ref0 gate` says it: its current version, whether its gate returned its
26278
+ * needs, and once it's open, how many still wait and whose one yes would meet them all; the project
26279
+ * gate's revision and whether it failed; and each need on its own line with where it came from,
26280
+ * ref0's floor or the line of `.ref0/gate.ts`, or of the project gate's, that returned it, with
26281
+ * that line's code, and under it whether it's met, by whose yes, or on whom or what it waits; in an
26282
+ * organization, what one yes gives its giver. A need for one of the project's checks says the
26283
+ * project gate revision whose checks run for it, the version's own. Then what each gate read of the
26284
+ * change, each read's tree, path and the object it found, short. Then its gate decided again in
26285
+ * place under `main`'s `.ref0/`, with `main`'s commit: the needs it held, or those that grew, and
26286
+ * why. Then the additions standing on the pull request now, each with the ID `ref0 pr needs remove`
26287
+ * takes: the version's own say who added its needs when it was made, and one may have gone since.
26288
+ * Then the local runs of its parts' identities, each with its signer; each part that takes local or
26289
+ * needs a yes, with its wrappers, the run and the yeses that meet it or what it waits for; and
26290
+ * last, each signer revoked from answering one of their checks, an attachment or a person by name,
26291
+ * with how an admin restores them.
25852
26292
  */
25853
26293
  const fromWords = (from) => {
25854
26294
  switch (from.kind) {
@@ -25879,11 +26319,116 @@ const columns = (lines) => {
25879
26319
  * version was decided under, whose checks run for it, never `main`'s.
25880
26320
  */
25881
26321
  const noteOf = (need, view) => need.kind === "check" && need.source === "project" && view.projectRevision !== void 0 ? ` at gate revision ${view.projectRevision}` : "";
25882
- const stateWords = (view) => {
26322
+ const isPerson = Schema.is(PersonId);
26323
+ /** An owner or a person, by name where it's known. */
26324
+ const nameOf = (id, named) => named.names.get(id) ?? id;
26325
+ /** The admins of an owner, in words: at home, its person alone, by name. */
26326
+ const adminsOf = (owner, named) => isPerson(owner) ? nameOf(owner, named) : `an admin of ${nameOf(owner, named)}`;
26327
+ /** Who answers as a decider, in words: a person by their name, a check by its key. */
26328
+ const whoWords = (decider, named) => {
26329
+ switch (decider.kind) {
26330
+ case "person": return nameOf(decider.id, named);
26331
+ case "role": return adminsOf(decider.owner, named);
26332
+ case "permission": return adminsOf(named.owner, named);
26333
+ case "check": return checkKey(decider);
26334
+ case "agent": return decider.id;
26335
+ case "override": return whoWords(decider.by, named);
26336
+ }
26337
+ };
26338
+ /** Alternatives in words, each once: `pip or an admin of acme`. */
26339
+ const anyOf = (deciders, named) => [...new Set(deciders.map((each) => whoWords(each, named)))].join(" or ");
26340
+ /** A yes that met a need, in words: whose, or the check that passed or was overridden. */
26341
+ const needYesWords = (yes, named) => {
26342
+ if (yes.decider.kind === "check") return `${checkKey(yes.decider)} ${yes.overridden === true ? "overridden" : "passed"}`;
26343
+ if (yes.by === void 0) return `${whoWords(yes.decider, named)}’s yes`;
26344
+ const by = nameOf(yes.by, named);
26345
+ return yes.decider.kind === "person" ? `${by}’s yes` : `${by}’s yes, as an admin`;
26346
+ };
26347
+ /** An override left of a check, in words: whose yes overrides which of its noes. */
26348
+ const overrideWords = (check, left, named) => `a yes from ${whoWords(left.by, named)} to override ${left.of === "failed run" ? `${check}’s failed run` : `${check} not running`}`;
26349
+ /**
26350
+ * One decider a need waits on, in words: a check to pass, or its parts when the need wraps it,
26351
+ * or, once ref0 said no to it, every override left.
26352
+ */
26353
+ const waitWords = (wait, need, named) => {
26354
+ if (wait.decider.kind !== "check") return whoWords(wait.decider, named);
26355
+ const check = checkKey(wait.decider);
26356
+ if (wait.overrides === void 0) return need.kind === "check" && (need.local === true || need.passkey !== void 0) ? `${check}’s parts, as below` : `${check} to pass`;
26357
+ if (wait.overrides.length === 0) return `${check}, which ref0 said no to`;
26358
+ return [...new Set(wait.overrides.map((left) => overrideWords(check, left, named)))].join(", and ");
26359
+ };
26360
+ /** The decider keys whose answers meet a need, for telling two needs met by the same yes. */
26361
+ const yesKey = (standing) => {
26362
+ if (standing.state === "met" && "by" in standing && standing.by.every(({ decider }) => decider.kind !== "check")) return standing.by.map(({ decider }) => deciderKey(decider)).join(" ");
26363
+ if (standing.state === "waiting" && "on" in standing && !standing.all) return standing.on.map(({ decider }) => deciderKey(decider)).join(" ");
26364
+ };
26365
+ /** How a need stands, in words. */
26366
+ const standingWords = (standing, need, named) => {
26367
+ switch (standing.state) {
26368
+ case "refused": return `refused: ${nameOf(standing.by, named)} said no, which decides the gate no`;
26369
+ case "met": {
26370
+ if ("as" in standing) return standing.as === "no checks" ? "met: no check runs on this change" : "met: the merge queue’s tip runs it, and main moves only once it passes there";
26371
+ const people = /* @__PURE__ */ new Map();
26372
+ for (const yes of standing.by) {
26373
+ const who = yes.by ?? deciderKey(yes.decider);
26374
+ if (!people.has(who)) people.set(who, yes);
26375
+ }
26376
+ const yeses = [...new Set([...people.values()].map((yes) => needYesWords(yes, named)))];
26377
+ return standing.by.some(({ decider }) => decider.kind === "check") ? `met: ${yeses.join(", ")}` : `met by ${yeses.join(" and ")}`;
26378
+ }
26379
+ case "waiting": {
26380
+ if ("for" in standing) return "waiting for ref0 to read which of main’s checks run on this change";
26381
+ if (standing.all) return `waiting for ${standing.on.map((wait) => waitWords(wait, need, named)).join(", and ")}`;
26382
+ const asked = `a yes from ${anyOf(standing.on.map(({ decider }) => decider), named)}`;
26383
+ return need.kind === "check" || need.kind === "checks" ? `waiting on ${asked}: no run can meet it before main moves` : `waiting on ${asked}`;
26384
+ }
26385
+ }
26386
+ };
26387
+ /**
26388
+ * Who one yes would meet a waiting need with, in words, each alone: anyone it asks a yes of, or
26389
+ * the one answerer of every override it waits on; none for a check to pass.
26390
+ */
26391
+ const meetersOf = (standing, named) => {
26392
+ if (standing.state !== "waiting" || !("on" in standing)) return [];
26393
+ if (!standing.all) return standing.on.map(({ decider }) => whoWords(decider, named));
26394
+ const answerers = new Set(standing.on.flatMap(({ overrides }) => overrides === void 0 || overrides.length === 0 ? [""] : overrides.map((left) => whoWords(left.by, named))));
26395
+ return answerers.size === 1 && !answerers.has("") ? [...answerers] : [];
26396
+ };
26397
+ /** An open gate's standings, or none until every need has one. */
26398
+ const standingsOf = (needs) => {
26399
+ const standings = needs.flatMap(({ standing }) => standing === void 0 ? [] : [standing]);
26400
+ return standings.length === 0 || standings.length < needs.length ? void 0 : standings;
26401
+ };
26402
+ /**
26403
+ * Where an open gate's needs stand, after its state: each met; refused, by whom; or how many
26404
+ * still wait, and whose one yes would meet them all, since one yes answers every decider its
26405
+ * person may answer as.
26406
+ */
26407
+ const standsWords = (needs, named) => {
26408
+ const standings = standingsOf(needs);
26409
+ if (standings === void 0) return "";
26410
+ const refused = standings.find((each) => each.state === "refused");
26411
+ if (refused !== void 0 && refused.state === "refused") return `; ${nameOf(refused.by, named)} said no, which decides it no`;
26412
+ const waiting = standings.filter((each) => each.state === "waiting");
26413
+ if (waiting.length === 0) return "; each need is met";
26414
+ const [first, ...rest] = waiting.map((each) => meetersOf(each, named));
26415
+ const common = (first ?? []).filter((who) => rest.every((each) => each.includes(who)));
26416
+ const still = waiting.length === 1 ? "1 still waits" : `${waiting.length} still wait`;
26417
+ if (common.length === 0) return `; ${still}`;
26418
+ return `; ${still}, and one yes from ${[...new Set(common)].join(" or ")} meets ${waiting.length === 1 ? "it" : "them all"}`;
26419
+ };
26420
+ /**
26421
+ * In an organization, what one yes gives, under the needs, while one waits on a yes: every
26422
+ * approval and override its giver may give, so an admin's meets every need an admin may meet.
26423
+ */
26424
+ const oneYesLines = (view, named) => {
26425
+ return (standingsOf(view.needs) ?? []).some((each) => each.state === "waiting" && "on" in each && each.on.some(({ decider, overrides }) => decider.kind !== "check" || overrides !== void 0)) && !isPerson(view.owner) ? [` one yes to this gate gives every approval and override above its giver may give, as themselves and as ${adminsOf(view.owner, named)}`] : [];
26426
+ };
26427
+ const stateWords = (view, named) => {
25883
26428
  switch (view.state) {
25884
- case "pending": return "its gate hasn’t answered yet";
25885
- case "answered": return "answered";
25886
- case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}`;
26429
+ case "pending": return "its gate hasn’t returned its needs yet";
26430
+ case "answered": return `its gate returned its needs${standsWords(view.needs, named)}`;
26431
+ case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}${standsWords(view.needs, named)}`;
25887
26432
  }
25888
26433
  };
25889
26434
  /** The project gate's line, under the head: its revision, and why it failed. */
@@ -25891,7 +26436,7 @@ const projectWords$1 = (view) => {
25891
26436
  const project = view.projectGate;
25892
26437
  if (project === void 0) return [];
25893
26438
  const at = ` project gate, revision ${project.revision}`;
25894
- return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state}`];
26439
+ return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state === "pending" ? "hasn’t returned its needs yet" : "returned its needs"}`];
25895
26440
  };
25896
26441
  /**
25897
26442
  * What a gate read of the change, under `head`: each read's tree, its path and the object it
@@ -25906,6 +26451,23 @@ const needLines = (needs, view) => columns(needs.map(({ need, from }) => ({
25906
26451
  need: spellNeed(need),
25907
26452
  from: `${from.map(fromWords).join("; ")}${noteOf(need, view)}`
25908
26453
  })));
26454
+ /**
26455
+ * The version's needs, each with how it stands on the line under it, once its gate is open: a
26456
+ * need met by, or waiting on, the same yes as one above says so.
26457
+ */
26458
+ const standingLines = (view, named) => {
26459
+ const lines = needLines(view.needs, view);
26460
+ const seen = /* @__PURE__ */ new Map();
26461
+ return view.needs.flatMap(({ need, standing }, i) => {
26462
+ const line = lines[i] ?? "";
26463
+ if (standing === void 0) return [line];
26464
+ const key = yesKey(standing);
26465
+ const same = key === void 0 ? void 0 : seen.get(key);
26466
+ if (key !== void 0 && same === void 0) seen.set(key, need);
26467
+ const also = same === void 0 ? "" : `, the same yes as ${spellNeed(same)}`;
26468
+ return [line, ` ${standingWords(standing, need, named)}${also}`];
26469
+ });
26470
+ };
25909
26471
  const grewWords = (grew) => {
25910
26472
  const why = grew.why ?? "ref0 couldn’t say why";
25911
26473
  switch (grew.kind) {
@@ -25917,7 +26479,7 @@ const grewWords = (grew) => {
25917
26479
  };
25918
26480
  const inPlaceLines = (inPlace, view) => {
25919
26481
  const head = `under main’s .ref0/, at ${inPlace.main.slice(0, 7)}:`;
25920
- if (inPlace.state === "pending") return [`${head} its gate there hasn’t answered yet`];
26482
+ if (inPlace.state === "pending") return [`${head} its gate there hasn’t returned its needs yet`];
25921
26483
  const reads = readLines("its gate there read:", inPlace.reads ?? []);
25922
26484
  if (inPlace.state === "held" || inPlace.grew === void 0) return [
25923
26485
  `${head} ${inPlace.state}`,
@@ -25991,7 +26553,11 @@ const wrappedPartLines = (parts) => parts.length === 0 ? [] : ["parts that take
25991
26553
  const revokedSignerLines = (one) => one.kind === "attachment" ? [` ${one.check} attachment ${one.attachment.id} of ${one.attachment.person}, since ${DateTime.formatIso(one.at)}: ref0 failed local run ${one.signed}'s identity twice, the second in run ${one.failed}`, ` an admin restores it: ref0 checks signers restore ${one.check} --attachment ${one.attachment.id}`] : [` ${one.check} ${one.person}, since ${DateTime.formatIso(one.at)}: ref0 failed the identity of run ${one.signed}, which they said yes over, twice, the second in run ${one.failed}`, ` an admin restores them: ref0 checks signers restore ${one.check} --person ${one.person}`];
25992
26554
  const revokedLines = (revoked) => revoked.length === 0 ? [] : ["signers revoked:", ...revoked.flatMap(revokedSignerLines)];
25993
26555
  const gateLines = (view, repo) => {
25994
- const head = `${repo}#${view.pullRequest}, version ${view.version}: ${stateWords(view)}`;
26556
+ const named = {
26557
+ names: new Map((view.names ?? []).map(({ owner, name }) => [owner, name])),
26558
+ owner: view.owner
26559
+ };
26560
+ const head = `${repo}#${view.pullRequest}, version ${view.version}: ${stateWords(view, named)}`;
25995
26561
  const inPlace = view.inPlace === void 0 ? [] : inPlaceLines(view.inPlace, view);
25996
26562
  const added = view.added.map(({ need, from }) => ({
25997
26563
  need: spellNeed(need),
@@ -26003,7 +26569,8 @@ const gateLines = (view, repo) => {
26003
26569
  return [
26004
26570
  head,
26005
26571
  ...projectWords$1(view),
26006
- ...needLines(view.needs, view),
26572
+ ...standingLines(view, named),
26573
+ ...oneYesLines(view, named),
26007
26574
  ...readLines("its gate read:", view.reads),
26008
26575
  ...readLines("its project gate read:", view.projectGate?.reads ?? []),
26009
26576
  ...inPlace,
@@ -26032,7 +26599,7 @@ const projectGateLines = (view, repo) => {
26032
26599
  };
26033
26600
  //#endregion
26034
26601
  //#region src/commands/gate.ts
26035
- const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26602
+ const failedWith$12 = (failed) => sayFailed(failed.nextStep);
26036
26603
  const project$4 = Flag.Boolean("project").pipe(Flag.withDescription("The project gate instead: its active revision, and a pending one with each latch it waits on"), Flag.withDefault(false));
26037
26604
  /** A pull request's gate. */
26038
26605
  const gateCommands = [Command.make("gate", {
@@ -26058,7 +26625,7 @@ const gateCommands = [Command.make("gate", {
26058
26625
  }
26059
26626
  });
26060
26627
  yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
26061
- }).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; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
26628
+ }).pipe(Effect.catchTag("CallFailed", failedWith$12))).pipe(Command.withDescription("A pull request’s needs, each with where it came from, ref0’s floor or a line of .ref0/gate.ts, and whether it’s met or on whom it waits; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
26062
26629
  //#endregion
26063
26630
  //#region src/clone.ts
26064
26631
  /** The remote a clone pushes to: its thread's fork path, named as the thread is. */
@@ -26416,7 +26983,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
26416
26983
  * with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
26417
26984
  * thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
26418
26985
  */
26419
- const failedWith$10 = (failed) => sayFailed(failed.nextStep);
26986
+ const failedWith$11 = (failed) => sayFailed(failed.nextStep);
26420
26987
  /** `ref0 git-credential`, as Git runs it: see `answerGit`. */
26421
26988
  const credential = Command.make("git-credential", { operation: Argument.String("operation").pipe(Argument.withDescription("What Git asks: get, store or erase")) }, ({ operation }) => Effect.gen(function* () {
26422
26989
  const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
@@ -26470,7 +27037,7 @@ const gitCommands = [
26470
27037
  value
26471
27038
  ])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
26472
27039
  yield* printAnswer(fork.json, asJson, `Cloned ${owner}/${name} into ${into}. origin is read-only to agents: git push goes to ${forkRemote(fork.value)}, the fork of whichever session on the thread pushes.`);
26473
- }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
27040
+ }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
26474
27041
  Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
26475
27042
  yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
26476
27043
  const synced = yield* syncBase(".");
@@ -26486,7 +27053,7 @@ const gitCommands = [
26486
27053
  on,
26487
27054
  commit
26488
27055
  }, asJson, `Merged ${at}. Push it when you’re ready: git push.`);
26489
- }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Merge in main, or the thread this one is stacked on, as ref0 holds it now. ref0 mcp says to when what merged touches your work"), Command.withShortDescription("Merge in main or the thread this one is stacked on")),
27056
+ }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Merge in main, or the thread this one is stacked on, as ref0 holds it now. ref0 mcp says to when what merged touches your work"), Command.withShortDescription("Merge in main or the thread this one is stacked on")),
26490
27057
  credential
26491
27058
  ];
26492
27059
  //#endregion
@@ -26899,7 +27466,7 @@ const usageCommands = [personUsage, personMeters];
26899
27466
  * invitations. Each is a person's, asked of them by approval, as every terminal command of
26900
27467
  * theirs is; agents never change members.
26901
27468
  */
26902
- const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27469
+ const failedWith$10 = (failed) => sayFailed(failed.nextStep);
26903
27470
  const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
26904
27471
  const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
26905
27472
  const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
@@ -26929,7 +27496,7 @@ const create$1 = Command.make("create", {
26929
27496
  payload
26930
27497
  });
26931
27498
  yield* printAnswer(answer.json, asJson, answer.value.state === "ready" ? `Made ${answer.value.name}.` : `Made ${answer.value.name}; it’s being set up.`);
26932
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Make an organization, with you as its admin"));
27499
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
26933
27500
  const leave = Command.make("leave", {
26934
27501
  organization: organization$2,
26935
27502
  json
@@ -26939,7 +27506,7 @@ const leave = Command.make("leave", {
26939
27506
  params: { owner: org }
26940
27507
  });
26941
27508
  yield* printAnswer(answer.json, asJson, `Left ${org}.`);
26942
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Leave an organization"));
27509
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
26943
27510
  const orgCommand = Command.make("org").pipe(Command.withDescription("Make an organization, leave one, or read its usage and what ref0 ran and kept for it"), Command.withShortDescription("Manage an organization and read its usage"), Command.withSubcommands([
26944
27511
  create$1,
26945
27512
  leave,
@@ -26956,7 +27523,7 @@ const orgs = Command.make("orgs", {
26956
27523
  });
26957
27524
  const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
26958
27525
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You’re in no organization yet." : [...lines, ...more(answer.value.next)].join("\n"));
26959
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
27526
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
26960
27527
  const members = Command.make("members", {
26961
27528
  organization: organization$2,
26962
27529
  cursor: cursor$3,
@@ -26969,7 +27536,7 @@ const members = Command.make("members", {
26969
27536
  });
26970
27537
  const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
26971
27538
  yield* printAnswer(answer.json, asJson, [...lines, ...more(answer.value.next)].join("\n"));
26972
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("An organization’s members, people and its own agents"), Command.withShortDescription("List an organization’s members"));
27539
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s members, people and its own agents"), Command.withShortDescription("List an organization’s members"));
26973
27540
  const role = Command.make("role", {
26974
27541
  organization: organization$2,
26975
27542
  person,
@@ -26985,7 +27552,7 @@ const role = Command.make("role", {
26985
27552
  payload: { role: chosen }
26986
27553
  });
26987
27554
  yield* printAnswer(answer.json, asJson, `${named} is ${answer.value.role} of ${org} now.`);
26988
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
27555
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
26989
27556
  const remove$1 = Command.make("remove", {
26990
27557
  organization: organization$2,
26991
27558
  person,
@@ -26999,7 +27566,7 @@ const remove$1 = Command.make("remove", {
26999
27566
  }
27000
27567
  });
27001
27568
  yield* printAnswer(answer.json, asJson, `Removed ${named} from ${org}.`);
27002
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
27569
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
27003
27570
  const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
27004
27571
  const listInvitations = Command.make("list", {
27005
27572
  organization: organization$2,
@@ -27013,7 +27580,7 @@ const listInvitations = Command.make("list", {
27013
27580
  });
27014
27581
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
27015
27582
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? `${org} has no open invitations.` : [...lines, ...more(answer.value.next)].join("\n"));
27016
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
27583
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
27017
27584
  const withdraw = Command.make("withdraw", {
27018
27585
  organization: organization$2,
27019
27586
  invitation,
@@ -27027,7 +27594,7 @@ const withdraw = Command.make("withdraw", {
27027
27594
  }
27028
27595
  });
27029
27596
  yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
27030
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Take an open invitation back"));
27597
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
27031
27598
  const invite = Command.make("invite", {
27032
27599
  organization: organization$2,
27033
27600
  person,
@@ -27047,7 +27614,7 @@ const invite = Command.make("invite", {
27047
27614
  payload
27048
27615
  });
27049
27616
  yield* printAnswer(answer.json, asJson, `Invited ${answer.value.invitee.name} to ${org} as ${answer.value.role}: ${answer.value.invitation}.`);
27050
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
27617
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
27051
27618
  const invitations = Command.make("invitations", {
27052
27619
  cursor: cursor$3,
27053
27620
  json
@@ -27058,7 +27625,7 @@ const invitations = Command.make("invitations", {
27058
27625
  });
27059
27626
  const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
27060
27627
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You have no open invitations." : [...lines, ...more(answer.value.next)].join("\n"));
27061
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
27628
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
27062
27629
  const accept = Command.make("accept", {
27063
27630
  invitation,
27064
27631
  json
@@ -27068,7 +27635,7 @@ const accept = Command.make("accept", {
27068
27635
  params: { invitation: id }
27069
27636
  });
27070
27637
  yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
27071
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Accept an invitation"));
27638
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
27072
27639
  const decline = Command.make("decline", {
27073
27640
  invitation,
27074
27641
  json
@@ -27078,7 +27645,7 @@ const decline = Command.make("decline", {
27078
27645
  params: { invitation: id }
27079
27646
  });
27080
27647
  yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
27081
- }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("Decline an invitation"));
27648
+ }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
27082
27649
  /** Organizations, their members and invitations. */
27083
27650
  const organizationCommands = [
27084
27651
  orgCommand,
@@ -27091,7 +27658,7 @@ const organizationCommands = [
27091
27658
  ];
27092
27659
  //#endregion
27093
27660
  //#region src/commands/grants.ts
27094
- const failedWith$8 = (failed) => sayFailed(failed.nextStep);
27661
+ const failedWith$9 = (failed) => sayFailed(failed.nextStep);
27095
27662
  const isOwnerRef = Schema.is(OwnerRef);
27096
27663
  const isProject$1 = Schema.is(WrittenProject);
27097
27664
  const isThread = Schema.is(WrittenThread);
@@ -27213,7 +27780,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
27213
27780
  onSome: (on) => [on]
27214
27781
  })];
27215
27782
  yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
27216
- }).pipe(Effect.catchTag("CallFailed", failedWith$8)));
27783
+ }).pipe(Effect.catchTag("CallFailed", failedWith$9)));
27217
27784
  const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
27218
27785
  const grant = changeCommand("grant", "grants.set").pipe(Command.withDescription("Give a grant in an organization, as an admin: ref0 grant acme/billing pip/claude write"), Command.withShortDescription("Give a grant in an organization"), Command.withSubcommands([revoke]));
27219
27786
  /** An organization's grants. */
@@ -27233,7 +27800,41 @@ const grantCommands = [Command.make("grants", {
27233
27800
  const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
27234
27801
  const next = answer.value.nextAfter;
27235
27802
  yield* printAnswer(answer.json, asJson, lines.length === 0 ? `You see no grants in ${org}.` : [...lines, ...next === void 0 ? [] : [`More: --cursor ${next}`]].join("\n"));
27236
- }).pipe(Effect.catchTag("CallFailed", failedWith$8))).pipe(Command.withDescription("The grants in an organization you may see"), Command.withShortDescription("List an organization’s grants")), grant];
27803
+ }).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The grants in an organization you may see"), Command.withShortDescription("List an organization’s grants")), grant];
27804
+ //#endregion
27805
+ //#region src/commands/identity-token.ts
27806
+ /**
27807
+ * `ref0 identity-token --audience <origin>`: an identity token for the agent's session it runs
27808
+ * in, for one outside service, which verifies it against ref0's published keys. One request, by
27809
+ * the session's own token, which never starts or renews a session. The token alone goes to
27810
+ * stdout, so it pipes into a verifier; anything else goes to stderr, with no token, and fails.
27811
+ */
27812
+ const decodeAudience = Schema.decodeUnknownOption(IdentityAudience);
27813
+ const outside = "ref0 identity-token runs only in an agent’s ref0 session, and none runs here. In Claude Code or Codex, one starts at each prompt once ref0 connect has connected its config folder: ask the agent there.";
27814
+ const audienceOnce = "Name the audience once, as --audience https://verifier.example: the outside service’s HTTPS origin.";
27815
+ const notAnAudience = "Send the outside service’s HTTPS origin as the audience, such as https://verifier.example, with no path.";
27816
+ const mayHaveIssued = "ref0’s answer was lost or failed partway, so it may have made a token that never reached you. Ask again for a fresh one in a moment.";
27817
+ /**
27818
+ * What went wrong, said once, on stderr. An answer lost or failed partway may still have made a
27819
+ * token, as ref0's log records, but nothing to check or undo: a fresh request is the way on.
27820
+ */
27821
+ const failedWith$8 = (failed) => sayFailed(failed.status >= 500 || failed.status === 0 && (failed.kind === "Unreachable" || failed.kind === "Unexpected") ? mayHaveIssued : failed.nextStep);
27822
+ /** Identity tokens, for outside services. */
27823
+ const identityTokenCommands = [Command.make("identity-token", {
27824
+ audience: Flag.String("audience").pipe(Flag.withDescription("The outside service that verifies the token, as its HTTPS origin, such as https://verifier.example"), Flag.atLeast(0)),
27825
+ json
27826
+ }, ({ audience: named, json: asJson }) => Effect.gen(function* () {
27827
+ if (named.length !== 1) return yield* sayFailed(audienceOnce);
27828
+ const audience = decodeAudience(named[0]);
27829
+ if (Option.isNone(audience)) return yield* sayFailed(notAnAudience);
27830
+ yield* ownSession(outside);
27831
+ const issued = yield* callEndpoint({
27832
+ endpoint: "me.identityToken",
27833
+ payload: { audience: audience.value }
27834
+ });
27835
+ if (asJson) return yield* printAnswer(issued.json, true, "");
27836
+ yield* Console.log(issued.value.token);
27837
+ }).pipe(Effect.catchTag("CallFailed", failedWith$8))).pipe(Command.withDescription("Print a short-lived identity token, a signed JWT naming your agent, for one outside service"), Command.withShortDescription("Print an identity token for one outside service"))];
27237
27838
  //#endregion
27238
27839
  //#region src/commands/agents.ts
27239
27840
  /**
@@ -27508,18 +28109,40 @@ const repoCommands = [Command.make("repos", {
27508
28109
  //#endregion
27509
28110
  //#region src/decision-lines.ts
27510
28111
  /** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
27511
- /** What a decision is about, in words. */
27512
- const subjectWords = (subject) => {
27513
- switch (subject.kind) {
27514
- case "thread": return subject.thread;
27515
- case "version": return `version ${subject.version} of pull request ${subject.pullRequest} in ${subject.repo}, from ${subject.thread}`;
27516
- }
28112
+ /**
28113
+ * A thread as a line read in `scope` names it: `#3` when it's in that scope, else in full, as
28114
+ * `pip/default#3`.
28115
+ */
28116
+ const threadWords = (thread, scope) => thread.startsWith(`${scope}#`) ? thread.slice(scope.length) : thread;
28117
+ /**
28118
+ * What waits, in words: a gate's pull request and version, and the thread it's from, or the thread
28119
+ * an ask is on.
28120
+ */
28121
+ const subjectWords = ({ subject, names }, scope) => subject.kind === "version" && names.pullRequest !== void 0 ? `${names.pullRequest}, version ${subject.version}, from ${threadWords(names.thread, scope)}` : names.thread;
28122
+ /**
28123
+ * A decision in a line, read in `scope`: what waits, its answers or how it ended, then its ID, for
28124
+ * `ref0 decision` and `ref0 answer`.
28125
+ */
28126
+ const decisionLines = (decision, scope) => {
28127
+ const on = subjectWords(decision, scope);
28128
+ const answers = decision.options.map((option) => option.key).join(" or ");
28129
+ return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
27517
28130
  };
27518
- /** A decision in a line. */
27519
- const decisionLines = (view) => {
27520
- const on = subjectWords(view.subject);
27521
- const answers = view.options.map((option) => option.key).join(" or ");
27522
- return view.state === "open" ? `${view.decision} on ${on}: ${answers}` : `${view.decision} on ${on}: ${view.result ?? "no answer"}, ${view.closedWhy ?? "closed"}`;
28131
+ /**
28132
+ * What waits in `scope`, as `ref0 waiting` prints it: what waits on the caller, then, for an agent,
28133
+ * what waits on its person, which only they answer, on their own page.
28134
+ */
28135
+ const waitingLines = (decisions, scope) => {
28136
+ const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision, scope)] : []);
28137
+ const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision, scope)}`] : []);
28138
+ if (theirs.length === 0) return yours.length === 0 ? `Nothing waits on you in ${scope}.` : yours.join("\n");
28139
+ const them = theirs.length === 1 ? "it" : "them";
28140
+ return [
28141
+ ...yours,
28142
+ `Waits on your person, who alone can answer ${them}, under Waiting on you on their page:`,
28143
+ ...theirs,
28144
+ `ref0 refuses an agent’s answer to ${them}.`
28145
+ ].join("\n");
27523
28146
  };
27524
28147
  //#endregion
27525
28148
  //#region src/commands/plans.ts
@@ -27534,7 +28157,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
27534
28157
  const encodePlan = Schema.encodeEffect(PlanInput);
27535
28158
  const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
27536
28159
  const thread$4 = Argument.String("thread").pipe(Argument.withDescription("The thread, as owner/project#number, or an org thread, as acme#7"), Argument.withSchema(WrittenThread));
27537
- const project$2 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads"), Argument.withSchema(Schema.Union([WrittenProject, OwnerName])));
28160
+ /** A project, `acme/billing`, or an organization, `acme`, for its org threads. */
28161
+ const Scope$1 = Schema.Union([WrittenProject, OwnerName]);
28162
+ const decodeScope = Schema.decodeUnknownOption(Scope$1);
28163
+ const project$2 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads"), Argument.withSchema(Scope$1));
27538
28164
  const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
27539
28165
  const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
27540
28166
  /** Where a version's approval stands: who accepted it, or its check. */
@@ -27695,27 +28321,37 @@ const comments = Command.make("comments", {
27695
28321
  ].filter((line) => line !== "").join("\n");
27696
28322
  yield* printAnswer(answer.json, asJson, words);
27697
28323
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("A plan version’s comments, oldest first"), Command.withShortDescription("List a plan version’s comments"));
28324
+ const plan = Command.make("plan").pipe(Command.withDescription("A thread’s plan: write, propose and read its versions, and comment"), Command.withShortDescription("Write, propose and read a thread’s plan"), Command.withSubcommands([
28325
+ list$4,
28326
+ write,
28327
+ onVersion("show"),
28328
+ onVersion("propose"),
28329
+ comment,
28330
+ comments
28331
+ ]));
28332
+ /**
28333
+ * The project of the thread the session runs on, or its organization for an org thread's. Outside
28334
+ * a session, it says how to name one, and fails before anything's sent.
28335
+ */
28336
+ const sessionScope = Effect.gen(function* () {
28337
+ const thread = yield* (yield* SessionThread).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
28338
+ const scope = Option.flatMap(thread, (written) => decodeScope(projectOf$2(written)));
28339
+ if (Option.isNone(scope)) return yield* sayFailed("Name the project: ref0 waiting pip/shop. Or, for an organization’s org threads: ref0 waiting acme.");
28340
+ return scope.value;
28341
+ });
27698
28342
  /** Plans' and decisions' commands. */
27699
28343
  const planCommands = [
27700
- 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([
27701
- list$4,
27702
- write,
27703
- onVersion("show"),
27704
- onVersion("propose"),
27705
- comment,
27706
- comments
27707
- ])),
28344
+ plan,
27708
28345
  Command.make("waiting", {
27709
- project: project$2,
28346
+ project: Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads; in an agent’s session, its own if not named"), Argument.withSchema(Scope$1), Argument.optional),
27710
28347
  json
27711
- }, ({ project: address, json: asJson }) => Effect.gen(function* () {
28348
+ }, ({ project: named, json: asJson }) => Effect.gen(function* () {
28349
+ const address = Option.isSome(named) ? named.value : yield* sessionScope;
27712
28350
  const answer = yield* callEndpoint(routed(address, {
27713
28351
  endpoint: "decisions.waiting",
27714
28352
  params: scopeParams(address)
27715
28353
  }));
27716
- const { decisions } = answer.value;
27717
- const words = decisions.length === 0 ? "Nothing waits on you here." : decisions.map((each) => decisionLines(each)).join("\n");
27718
- yield* printAnswer(answer.json, asJson, words);
28354
+ yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
27719
28355
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("What in a project, or in an organization’s org threads, waits on you"), Command.withShortDescription("Show what waits on you")),
27720
28356
  Command.make("decision", {
27721
28357
  project: project$2,
@@ -27729,7 +28365,7 @@ const planCommands = [
27729
28365
  decision: id
27730
28366
  }
27731
28367
  }));
27732
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28368
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
27733
28369
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("One decision, as it stands"), Command.withShortDescription("Show a decision and where it stands")),
27734
28370
  Command.make("answer", {
27735
28371
  project: project$2,
@@ -27752,7 +28388,7 @@ const planCommands = [
27752
28388
  })
27753
28389
  })
27754
28390
  }));
27755
- yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
28391
+ yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
27756
28392
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
27757
28393
  ];
27758
28394
  //#endregion
@@ -27792,9 +28428,14 @@ const endedLines = (view) => {
27792
28428
  const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
27793
28429
  return [` ${view.state.padEnd(8)} ${at}${why}`];
27794
28430
  };
27795
- const openedByWords = (view) => {
27796
- 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}`;
27797
- return `${minute(view.openedAt)} by ${by}`;
28431
+ /**
28432
+ * When it opened, and by what: a workflow's run, or an agent's session, whose IDs go on a line of
28433
+ * their own, since ref0 answers no name for either.
28434
+ */
28435
+ const openedLines = (view) => {
28436
+ const at = minute(view.openedAt);
28437
+ if ("workflow" in view.openedBy) return [` opened ${at} by workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`];
28438
+ return [` opened ${at} by an agent’s session:`, ` ${view.openedBy.session} of ${view.openedBy.agent.id}`];
27798
28439
  };
27799
28440
  /** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
27800
28441
  const pullRequestLines = (view, repo) => {
@@ -27805,7 +28446,7 @@ const pullRequestLines = (view, repo) => {
27805
28446
  `${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
27806
28447
  ` head ${headWords(head)}`,
27807
28448
  ` current ${versionWords(current)}, on ${base}`,
27808
- ` opened ${openedByWords(view)}`,
28449
+ ...openedLines(view),
27809
28450
  ...endedLines(view)
27810
28451
  ].join("\n");
27811
28452
  };
@@ -28791,7 +29432,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
28791
29432
  const { thread, blockers } = first.value;
28792
29433
  if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
28793
29434
  const them = blockers.length === 1 ? "it" : "them";
28794
- yield* Console.error(printable(`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`));
29435
+ yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
28795
29436
  let renewedAt = Number.NEGATIVE_INFINITY;
28796
29437
  for (;;) {
28797
29438
  const now = yield* Clock.currentTimeMillis;
@@ -29684,7 +30325,7 @@ const enable = Command.make("enable", {
29684
30325
  const why = found.why === void 0 ? "" : ` (${found.why})`;
29685
30326
  return yield* sayFailed(found.why === "activation failed" ? `ref0 couldn't read main's .ref0/ for a day, and reads it again each hour. Once ref0 workflows list ${address} shows a version of ${workflow}, turn it on.` : `main has no version of ${workflow} ref0 can run${why}. Fix its code in .ref0/ and merge it, so ref0 compiles a new version, then turn that one on.`);
29686
30327
  }
29687
- yield* Console.error(printable(enablingLines(workflow, found.version, found.manifest, found)));
30328
+ yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
29688
30329
  const answer = yield* callEndpoint({
29689
30330
  endpoint: "workflows.enable",
29690
30331
  params: repoParts(address),
@@ -29717,7 +30358,7 @@ const start = Command.make("start", {
29717
30358
  source: source.value
29718
30359
  }
29719
30360
  });
29720
- yield* Console.error(printable(attachingLines(written, compiled.value)));
30361
+ yield* printBlocks([[attachingLines(written, compiled.value)]]);
29721
30362
  const answer = yield* callEndpoint({
29722
30363
  endpoint: "threadWorkflows.start",
29723
30364
  params,
@@ -32677,11 +33318,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
32677
33318
  * started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
32678
33319
  * someone else's words, and nothing here reads it.
32679
33320
  */
32680
- const decodeThread = Schema.decodeUnknownOption(WrittenThread);
33321
+ const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
32681
33322
  const decodeEvent = Schema.decodeUnknownOption(EventId);
32682
33323
  const optional = (name) => Config.String(name).pipe(Config.option);
32683
33324
  const watchedHere = Effect.gen(function* () {
32684
- const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
33325
+ const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
32685
33326
  const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
32686
33327
  return Option.zipWith(thread, event, (on, by) => ({
32687
33328
  thread: on,
@@ -34059,6 +34700,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
34059
34700
  });
34060
34701
  return SessionToken.of({ current });
34061
34702
  }));
34703
+ const decodeThread = Schema.decodeUnknownOption(WrittenThread);
34704
+ /** `SessionThread` from the keychain, as the start of the harness session this runs in kept it. */
34705
+ const harnessSessionThread = Layer.effect(SessionThread, Effect.gen(function* () {
34706
+ const keychain = yield* Keychain;
34707
+ const harness = yield* HarnessProcess;
34708
+ const current = Effect.gen(function* () {
34709
+ const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
34710
+ if (Option.isNone(binding)) return Option.none();
34711
+ const kept = yield* keychain.get(threadAccount(binding.value));
34712
+ return Option.flatMap(kept, (thread) => decodeThread(Redacted.value(thread)));
34713
+ });
34714
+ return SessionThread.of({ current });
34715
+ }));
34062
34716
  //#endregion
34063
34717
  //#region src/harness/server-check.ts
34064
34718
  /**
@@ -34168,6 +34822,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
34168
34822
  CLAUDE_CONFIG_DIR: configFolder,
34169
34823
  ...checked
34170
34824
  };
34825
+ const withheld = Object.keys(yield* fromHere(keychainVariables)).some((variable) => env[variable] === void 0);
34171
34826
  return yield* Effect.scoped(Effect.gen(function* () {
34172
34827
  const handle = yield* spawner.spawn(ChildProcess.make(server.value.command, server.value.args, {
34173
34828
  cwd: yield* emptyFolder(fs),
@@ -34182,6 +34837,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
34182
34837
  const said = yield* handle.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkScoped);
34183
34838
  const lines = yield* handle.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.takeUntil((line) => Option.isSome(decodeAnswer(line))), Stream.runCollect);
34184
34839
  if (lines.some((line) => Option.isSome(decodeAnswer(line)))) return { kind: "answered" };
34840
+ if (withheld) return { kind: "withoutKeychain" };
34185
34841
  return {
34186
34842
  kind: "stopped",
34187
34843
  said: firstLine(yield* Fiber.join(said)) || firstLine(lines.join("\n"))
@@ -34252,6 +34908,7 @@ const serverCheckOf = (started, configFolder) => {
34252
34908
  switch (started.kind) {
34253
34909
  case "answered": return checkOf("server", "yes", "starts");
34254
34910
  case "stopped": return checkOf("server", "no", started.said === "" ? "can’t start, and said nothing" : `can’t start: ${started.said}`, installLine(configFolder));
34911
+ case "withoutKeychain": return checkOf("server", "no", "can’t reach the keychain, since an older ref0 installed it", installLine(configFolder));
34255
34912
  case "unnamed": return checkOf("server", "no", "the plugin names no ref0 server", installLine(configFolder));
34256
34913
  case "unanswered": return checkOf("server", "unknown", `didn’t answer in ${Duration.toSeconds(serverLimit)} s`, "ref0 status");
34257
34914
  }
@@ -34417,7 +35074,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
34417
35074
  ...organizationCommands,
34418
35075
  ...grantCommands,
34419
35076
  ...agentCommands,
34420
- ...attachmentCommands
35077
+ ...attachmentCommands,
35078
+ ...identityTokenCommands
34421
35079
  ]));
34422
35080
  const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
34423
35081
  const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
@@ -34432,7 +35090,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
34432
35090
  Ref0Config.layer,
34433
35091
  sessionToken,
34434
35092
  FetchHttpClient.layer
34435
- ])), sessionToken, FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
35093
+ ])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
34436
35094
  //#endregion
34437
35095
  //#region src/main.ts
34438
35096
  ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);