@ref0/cli 0.9.0 → 0.11.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 (2) hide show
  1. package/dist/main.mjs +1436 -677
  2. package/package.json +2 -2
package/dist/main.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
- import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
3
+ import { Cause, Clock, Config, ConfigProvider, Console, Context, Cron, Crypto, Data, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, Match, Option, Path, Predicate, Queue, Random, Redacted, Ref, Result, Runtime, Schedule, Schema, SchemaIssue, SchemaTransformation, Scope, Semaphore, Stdio, Stream, Struct, Terminal, Tuple } from "effect";
4
4
  import { Argument, CliOutput, Command, Flag, Prompt } from "effect/cli";
5
5
  import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
6
6
  import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity } from "effect/http-api";
@@ -11,7 +11,7 @@ import { access, constants } from "node:fs/promises";
11
11
  import { hostname } from "node:os";
12
12
  import { connect, createServer as createServer$1 } from "node:net";
13
13
  import { join } from "node:path";
14
- import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai";
14
+ import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai";
15
15
  //#region ../contracts/src/typeid.ts
16
16
  /**
17
17
  * TypeIDs: a prefix, an underscore, then a UUID in 26 characters of Crockford's base32,
@@ -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) ?? [];
@@ -6767,383 +6770,6 @@ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
6767
6770
  attachGrant: Schema.optionalKey(EventId)
6768
6771
  })]);
6769
6772
  //#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
6773
  //#region ../contracts/src/plan-events.ts
7148
6774
  /**
7149
6775
  * A plan's events, in its thread's project's log, written by the Project DO. A version's
@@ -7282,6 +6908,21 @@ const DecisionView = Schema.Struct({
7282
6908
  closedWhy: Schema.optionalKey(DecisionClosedWhy)
7283
6909
  });
7284
6910
  /**
6911
+ * Where a decision is, as people write it: its thread, `pip/default#3`, or an org thread's,
6912
+ * `acme#7`; and a gate's pull request, `pip/notes#1`. Its Project DO writes them as it answers,
6913
+ * from its own threads and repos and the owner's name the Worker gives it, so each reads as it's
6914
+ * called now.
6915
+ */
6916
+ const DecisionNames = Schema.Struct({
6917
+ thread: WrittenThread,
6918
+ pullRequest: Schema.optionalKey(WrittenPullRequest)
6919
+ });
6920
+ /** A decision as `/v1` answers it: as it stands, and where it is, in words. */
6921
+ const NamedDecision = Schema.Struct({
6922
+ ...DecisionView.fields,
6923
+ names: DecisionNames
6924
+ });
6925
+ /**
7285
6926
  * A version as it stands: its content, its state, and, once proposed, where its approval is: its
7286
6927
  * repos still being checked, or who accepted it.
7287
6928
  */
@@ -7435,7 +7076,7 @@ const GateAnswerRoute = Schema.TaggedStruct("GateAnswerRoute", {
7435
7076
  reason: Schema.optionalKey(TextId)
7436
7077
  });
7437
7078
  Schema.Union([
7438
- DecisionView,
7079
+ NamedDecision,
7439
7080
  GateAnswerRoute,
7440
7081
  DecisionNotFound,
7441
7082
  AnswerRefused,
@@ -7467,14 +7108,29 @@ Schema.Union([
7467
7108
  ]) }),
7468
7109
  DeliveryOutsideWindow
7469
7110
  ]);
7470
- Schema.Struct({
7111
+ /** Someone decisions wait on: a person, or an agent, and the roles the Worker found they hold. */
7112
+ const Waiter = Schema.Struct({
7471
7113
  decider: DeciderRef,
7472
- roles: Schema.optionalKey(Schema.Array(Schema.Struct({
7114
+ roles: Schema.Array(Schema.Struct({
7473
7115
  role: Schema.Literal("admin"),
7474
7116
  owner: OwnerId
7475
- })))
7117
+ }))
7476
7118
  });
7477
- const WaitingPage = Schema.Struct({ decisions: Schema.Array(DecisionView) });
7119
+ Schema.Struct({
7120
+ ownerName: OwnerName,
7121
+ you: Waiter,
7122
+ person: Schema.optionalKey(Waiter),
7123
+ decisions: Schema.optionalKey(Schema.Array(DecisionId).check(Schema.isMaxLength(100)))
7124
+ });
7125
+ /**
7126
+ * An open decision that waits on the caller: on them, or, for an agent, on its person, who
7127
+ * answers it. A gate never takes an agent's answer, so an agent's gate always waits on its person.
7128
+ */
7129
+ const WaitingDecision = Schema.Struct({
7130
+ ...NamedDecision.fields,
7131
+ waitsOn: Schema.Literals(["you", "your person"])
7132
+ });
7133
+ const WaitingPage = Schema.Struct({ decisions: Schema.Array(WaitingDecision) });
7478
7134
  Schema.Struct({
7479
7135
  ownerName: OwnerName,
7480
7136
  number: ThreadNumber
@@ -7492,7 +7148,7 @@ Schema.Struct({
7492
7148
  decision: DecisionId,
7493
7149
  wait: Schema.optionalKey(DecisionWait)
7494
7150
  });
7495
- Schema.Union([DecisionView, DecisionNotFound]);
7151
+ Schema.Union([NamedDecision, DecisionNotFound]);
7496
7152
  /**
7497
7153
  * `POST …/plans`: a version, as a draft unless it's proposed at once, and why it was written.
7498
7154
  * A first version needs no reason, and a later one may leave it out.
@@ -7541,6 +7197,385 @@ const ChildStageView = Schema.Struct({
7541
7197
  });
7542
7198
  Schema.Union([ChildStageView, ThreadNotFound]);
7543
7199
  //#endregion
7200
+ //#region ../contracts/src/approvals.ts
7201
+ /**
7202
+ * Approvals, as their commands travel: what a terminal, a page or an agent asks its person to
7203
+ * say yes to. A command is one endpoint, by its name in the surface table, and its request, as
7204
+ * that endpoint encodes it. ref0 decodes each part by the endpoint's own `Schema` before it
7205
+ * keeps or shows anything, so a command is exactly what the endpoint would take.
7206
+ */
7207
+ /**
7208
+ * An endpoint by its name in the surface table, `group.endpoint`. Checked against the table
7209
+ * where the command is made canonical, since the table is typed from the API this is part of.
7210
+ */
7211
+ const CommandEndpoint = Schema.String.check(Schema.isPattern(/^[a-zA-Z]+\.[a-zA-Z]+$/));
7212
+ /** One command, as asked: its endpoint, and its request's path, query and payload, encoded. */
7213
+ const ApprovalCommand = Schema.Struct({
7214
+ endpoint: CommandEndpoint,
7215
+ params: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
7216
+ query: Schema.optionalKey(Schema.JsonObject),
7217
+ payload: Schema.optionalKey(Schema.Json)
7218
+ });
7219
+ /** A command ref0 won't ask anyone about: not an endpoint's request, or not one an approval runs. */
7220
+ var CommandRefused = class extends Schema.TaggedError()("CommandRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
7221
+ /** A terminal asking: its command, and the hash of the secret it collects the result with. */
7222
+ const StartApproval = Schema.Struct({
7223
+ command: ApprovalCommand,
7224
+ secretHash: SecretHash
7225
+ });
7226
+ /** A terminal's approval, started: what to show its person, and until when. */
7227
+ const ApprovalStarted = Schema.Struct({
7228
+ approval: ApprovalId,
7229
+ code: ApprovalCode,
7230
+ expiresAt: Timestamp
7231
+ });
7232
+ /**
7233
+ * An answer that waits for the person: the call only the person may make, or that agents ask
7234
+ * for, became an approval, which the person answers on their own page. A `202`, never a failure
7235
+ * of the call: whoever asked collects the result with `GET /v1/approvals/{approval}`.
7236
+ */
7237
+ var ApprovalNeeded = class extends Schema.TaggedError()("ApprovalNeeded", {
7238
+ approval: ApprovalId,
7239
+ says: Schema.String,
7240
+ expiresAt: Timestamp
7241
+ }, { httpApiStatus: 202 }) {};
7242
+ const terminalAsker = Schema.Struct({ kind: Schema.Literal("terminal") });
7243
+ const pageAsker = Schema.Struct({ kind: Schema.Literal("page") });
7244
+ const agentAsker = Schema.Struct({
7245
+ kind: Schema.Literal("agent"),
7246
+ agent: AgentId,
7247
+ handle: AgentHandle,
7248
+ session: SessionId,
7249
+ /** The attachment its credential was made under, for a person's agent in an organization. */
7250
+ attachment: Schema.optionalKey(CredentialRef)
7251
+ });
7252
+ /**
7253
+ * Who asked, as a receipt keeps it: a terminal, one of ref0's pages, an agent, or a machine
7254
+ * asking to connect, by its ID and its own word for itself, and from where.
7255
+ */
7256
+ const ApprovalAsker = Schema.Union([
7257
+ terminalAsker,
7258
+ pageAsker,
7259
+ Schema.Struct({
7260
+ kind: Schema.Literal("machine"),
7261
+ machine: MachineId,
7262
+ machineName: MachineName,
7263
+ askedFrom: Schema.optionalKey(Schema.String)
7264
+ }),
7265
+ agentAsker
7266
+ ]);
7267
+ /**
7268
+ * Who asked, as the person reads a request before they answer: a terminal, a page or an agent.
7269
+ * A machine's request is read as an `AttachmentRequestView`, never as a command.
7270
+ */
7271
+ const ApprovalViewAsker = Schema.Union([
7272
+ terminalAsker,
7273
+ pageAsker,
7274
+ agentAsker
7275
+ ]);
7276
+ /** A request, as the person reads it before they answer. */
7277
+ const ApprovalView = Schema.Struct({
7278
+ approval: ApprovalId,
7279
+ /** The command as its CLI line. */
7280
+ says: Schema.String,
7281
+ /** Everything the command carries, each named, in order: nothing it would run is left out. */
7282
+ fields: Schema.Array(Schema.Struct({
7283
+ name: Schema.String,
7284
+ value: Schema.String,
7285
+ part: Schema.Literals([
7286
+ "path",
7287
+ "query",
7288
+ "payload"
7289
+ ])
7290
+ })),
7291
+ command: ApprovalCommand,
7292
+ asker: ApprovalViewAsker,
7293
+ askedFrom: Schema.optionalKey(Schema.String),
7294
+ askedAt: Timestamp,
7295
+ expiresAt: Timestamp,
7296
+ /**
7297
+ * An agent's ask: the kind of action its answer may set agents' permission for, and whether a
7298
+ * yes may let agents take it without asking. A no with the passkey may always stop them asking.
7299
+ */
7300
+ agentPermission: Schema.optionalKey(Schema.Struct({
7301
+ action: ScopeAction,
7302
+ mayAllow: Schema.Boolean
7303
+ })),
7304
+ /**
7305
+ * What the command acts on, drawn as ref0 resolves it for the person reading, where its
7306
+ * endpoint names a preview. Without one the page can draw, it offers only no.
7307
+ */
7308
+ preview: Schema.optionalKey(ApprovalPreview)
7309
+ });
7310
+ /** A statement's hash: the SHA-256 of its words and quoted values as JSON, as lowercase hex. */
7311
+ const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
7312
+ /**
7313
+ * Who asks for a yes to a statement: a workflow's run, `passkey.run`'s, by the workflow and the
7314
+ * run; or a gate's check, `passkey.check`'s, by the check, the pull request's version it gates and
7315
+ * the check run its statement is over.
7316
+ */
7317
+ const StatementAsker = Schema.Union([Schema.Struct({
7318
+ kind: Schema.Literal("workflow"),
7319
+ workflow: WorkflowName,
7320
+ run: WorkflowRunId
7321
+ }), Schema.Struct({
7322
+ kind: Schema.Literal("gate"),
7323
+ check: CheckKey,
7324
+ pullRequest: PullRequestNumber,
7325
+ version: VersionNumber,
7326
+ run: CheckRunId
7327
+ })]);
7328
+ /**
7329
+ * A passkey request, as the person reads it before they answer: the statement, its words as its
7330
+ * asker wrote them and each value it quotes, and its hash; who asks; where, a workflow's
7331
+ * `owner/repo` or a thread's `owner/project#number`, or a gate's pull request, `owner/repo#number`;
7332
+ * whom it asks, the run's accountable person, or for a gate the person behind its run, a person by
7333
+ * name, or a permission's holders; and until when.
7334
+ */
7335
+ const StatementView = Schema.Struct({
7336
+ approval: ApprovalId,
7337
+ statement: WorkflowText,
7338
+ hash: StatementHash,
7339
+ asker: StatementAsker,
7340
+ home: Schema.String,
7341
+ from: Schema.Union([Schema.Literals(["accountable", "person"]), Schema.Struct({ permission: RepoPermissionName })]),
7342
+ askedAt: Timestamp,
7343
+ expiresAt: Timestamp
7344
+ });
7345
+ /**
7346
+ * The person's own requests, waiting for them; the passkey requests workflows' runs and gates asked
7347
+ * them; each machine's request this browser session is the answerer of, so a reload finds it
7348
+ * again without its code; and the gates' decisions that wait on their yes or no, in every project
7349
+ * they see, oldest first.
7350
+ */
7351
+ const ApprovalsWaiting = Schema.Struct({
7352
+ approvals: Schema.Array(ApprovalView),
7353
+ statements: Schema.Array(StatementView),
7354
+ requests: Schema.Array(AttachmentRequestView),
7355
+ decisions: Schema.Array(NamedDecision)
7356
+ });
7357
+ /** Finding a terminal's request by the code it showed. */
7358
+ const FindApproval = Schema.Union([Schema.Struct({ code: TypedApprovalCode }), Schema.Struct({ link: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/)) })]);
7359
+ /**
7360
+ * Asking for the passkey's options to answer a request: a terminal's request needs its code
7361
+ * again, so its ID alone, which travels where its code doesn't, never answers it. A machine's
7362
+ * request needs its answerer, and the agent it chooses to connect, before a yes has options.
7363
+ */
7364
+ const ApprovalOptionsRequest = Schema.Struct({
7365
+ code: Schema.optionalKey(TypedApprovalCode),
7366
+ folders: Schema.optionalKey(ConnectFolderChoices)
7367
+ });
7368
+ /**
7369
+ * An answer: yes, with the passkey, or no. For an agent's ask, each answer can set the owner's
7370
+ * permission its own way, so a pairing that can't happen can't be sent: yes, and let agents do
7371
+ * it without asking; no, and stop agents asking, with the passkey too.
7372
+ */
7373
+ const AnswerApproval = Schema.Union([
7374
+ Schema.Struct({
7375
+ answer: Schema.Literal("yes"),
7376
+ permission: Schema.optionalKey(Schema.Literal("allow")),
7377
+ assertion: PasskeyAsserted,
7378
+ code: Schema.optionalKey(TypedApprovalCode),
7379
+ /** The preview the page drew, where the command's endpoint names one. */
7380
+ preview: Schema.optionalKey(PreviewEcho),
7381
+ /** The statement the page showed, by its hash, for a workflow run's or a gate's passkey request. */
7382
+ statement: Schema.optionalKey(Schema.Struct({ hash: StatementHash }))
7383
+ }),
7384
+ Schema.Struct({
7385
+ answer: Schema.Literal("no"),
7386
+ permission: Schema.Literal("deny"),
7387
+ assertion: PasskeyAsserted,
7388
+ code: Schema.optionalKey(TypedApprovalCode)
7389
+ }),
7390
+ Schema.Struct({
7391
+ answer: Schema.Literal("no"),
7392
+ code: Schema.optionalKey(TypedApprovalCode)
7393
+ })
7394
+ ]);
7395
+ /**
7396
+ * What running an approved command came to: its status, and its endpoint's answer or refusal,
7397
+ * as JSON. ref0 reads it by the command's endpoint before anyone sees it, and whoever asked reads
7398
+ * it the same way, with `ranOf`: this schema is part of the API the endpoints' types come from,
7399
+ * so it can't be typed by them itself.
7400
+ */
7401
+ const Ran = Schema.Struct({
7402
+ status: Schema.Int,
7403
+ result: Schema.Json
7404
+ });
7405
+ /**
7406
+ * A ran command whose answer its endpoint's schemas no longer read, such as one kept from
7407
+ * before they changed: it ran, and what it came to can't be said.
7408
+ */
7409
+ var ResultUnreadable = class extends Schema.TaggedError()("ResultUnreadable", { nextStep: Schema.String }, { httpApiStatus: 500 }) {};
7410
+ /**
7411
+ * Where a request stands, for whoever asked: still waiting; approved, and running as soon as
7412
+ * ref0 can; refused; expired; or what it ran to, with the endpoint it ran, whose schemas read it.
7413
+ */
7414
+ const ApprovalState = Schema.Union([
7415
+ Schema.Struct({
7416
+ state: Schema.Literal("waiting"),
7417
+ expiresAt: Timestamp
7418
+ }),
7419
+ Schema.Struct({
7420
+ state: Schema.Literal("approved"),
7421
+ /** A machine's answering browser may forget its saved answer once the terminal kept it. */
7422
+ collected: Schema.optionalKey(Schema.Literal(true))
7423
+ }),
7424
+ Schema.Struct({ state: Schema.Literal("refused") }),
7425
+ Schema.Struct({ state: Schema.Literal("expired") }),
7426
+ Schema.Struct({
7427
+ state: Schema.Literal("ran"),
7428
+ endpoint: CommandEndpoint,
7429
+ ...Ran.fields
7430
+ })
7431
+ ]);
7432
+ /** Only the answering browser receives a machine's answer code; state reads never carry it. */
7433
+ const ApprovalAnswer = Schema.Union([
7434
+ Schema.Struct({
7435
+ ...ApprovalState.members[1].fields,
7436
+ answerCode: ApprovalCode
7437
+ }),
7438
+ Schema.Struct({
7439
+ ...ApprovalState.members[4].fields,
7440
+ answerCode: ApprovalCode
7441
+ }),
7442
+ ApprovalState
7443
+ ]);
7444
+ /** A request that isn't there, or isn't the caller's to see, answers the same. */
7445
+ var ApprovalNotFound = class extends Schema.TaggedError()("ApprovalNotFound", { nextStep: Schema.String }, { httpApiStatus: 404 }) {};
7446
+ /** Too many asked, or too many codes typed, from here in the last hour. */
7447
+ var SlowDown = class extends Schema.TaggedError()("SlowDown", { nextStep: Schema.String }, { httpApiStatus: 429 }) {};
7448
+ /** A command's hash: the SHA-256 of its canonical JSON, as lowercase hex. */
7449
+ const CommandHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
7450
+ Schema.Struct({
7451
+ approval: ApprovalId,
7452
+ endpoint: CommandEndpoint,
7453
+ /** The command's canonical JSON. */
7454
+ command: Schema.String,
7455
+ commandHash: CommandHash,
7456
+ asker: ApprovalAsker,
7457
+ /** The browser session the person answered in, for their log. */
7458
+ credential: CredentialRef,
7459
+ generation: Generation,
7460
+ /** When the request expires, in epoch milliseconds. */
7461
+ deadline: Schema.Int,
7462
+ challenge: Schema.String,
7463
+ /** A machine answer retries only from its answering session, with this key. */
7464
+ answerKey: Schema.optionalKey(RequestId),
7465
+ /** An agent's ask: the credential it asked with, checked again before the answer counts. */
7466
+ asking: Schema.optionalKey(TokenId),
7467
+ /** The kind of action the person, answering, let agents take without asking from now on. */
7468
+ allow: Schema.optionalKey(ScopeAction)
7469
+ });
7470
+ Schema.Struct({
7471
+ approval: ApprovalId,
7472
+ action: ScopeAction,
7473
+ credential: CredentialRef,
7474
+ generation: Generation
7475
+ });
7476
+ Schema.Union([Schema.Struct({ denied: Schema.Literal(true) }), Schema.Struct({
7477
+ denied: Schema.Literal(false),
7478
+ reason: Schema.Literals(["generation moved", "not set up"])
7479
+ })]);
7480
+ Schema.Union([Schema.Struct({
7481
+ admitted: Schema.Literal(true),
7482
+ admittedAt: Schema.Int,
7483
+ answerCode: Schema.optionalKey(ApprovalCode)
7484
+ }), Schema.Struct({
7485
+ admitted: Schema.Literal(false),
7486
+ reason: Schema.Literals([
7487
+ "expired",
7488
+ "generation moved",
7489
+ "challenge used",
7490
+ "not set up",
7491
+ "asker gone"
7492
+ ])
7493
+ })]);
7494
+ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
7495
+ found: Schema.Literal(true),
7496
+ endpoint: CommandEndpoint,
7497
+ command: Schema.String,
7498
+ commandHash: CommandHash,
7499
+ generation: Generation,
7500
+ generationNow: Generation,
7501
+ /** The `approval.answered` its admission recorded in the person's log. */
7502
+ answeredEvent: EventId,
7503
+ /** Who asked, as that `approval.answered` says. */
7504
+ asker: ApprovalAsker,
7505
+ /** What running it came to, once it ran, as `KeptRan` keeps it. */
7506
+ result: Schema.optionalKey(Schema.String)
7507
+ })]);
7508
+ /**
7509
+ * A `Ran` as it's kept, on its receipt and on its approval: its JSON, as text. The Owner DO
7510
+ * keeps it as it comes and runs no endpoint's schemas, so it's read only by its endpoint, with
7511
+ * `keptRan`, which refuses one they don't take.
7512
+ */
7513
+ const KeptRan = Schema.fromJsonString(Ran);
7514
+ Schema.Struct({
7515
+ approval: ApprovalId,
7516
+ result: Schema.String
7517
+ });
7518
+ Schema.Struct({ result: Schema.String });
7519
+ Schema.Struct({ approval: ApprovalId });
7520
+ Schema.Struct({ approval: ApprovalId });
7521
+ Schema.Union([Schema.Struct({ expired: Schema.Literal(true) }), Schema.Struct({ expired: Schema.Literal(false) })]);
7522
+ Schema.Struct({
7523
+ person: PersonId,
7524
+ approval: ApprovalId
7525
+ });
7526
+ //#endregion
7527
+ //#region ../contracts/src/browser-sessions.ts
7528
+ /**
7529
+ * A browser session's cookie: Better Auth's, named for ref0, `Secure` and host-only on the
7530
+ * app's one origin. It holds the session's token, signed; the directory and the person's Owner DO hold
7531
+ * only the token's SHA-256.
7532
+ */
7533
+ const browserSessionCookie = "__Secure-ref0.session_token";
7534
+ Schema.Struct({
7535
+ token: TokenId,
7536
+ secretHash: SecretHash,
7537
+ expiresAt: Schema.DateTimeUtcFromMillis
7538
+ });
7539
+ Schema.Struct({ registered: Schema.Boolean });
7540
+ /**
7541
+ * Where a signed-in person stands: naming, when the name their GitHub login gave was taken or
7542
+ * kept back, with that login as a suggestion; setting up, once their owner is made and before
7543
+ * it's ready with this session registered, which naming again finishes; or ready.
7544
+ */
7545
+ const Welcome = Schema.Union([Schema.Struct({
7546
+ state: Schema.Literals(["ready", "setting up"]),
7547
+ person: PersonId,
7548
+ owner: OwnerId,
7549
+ name: OwnerName
7550
+ }), Schema.Struct({
7551
+ state: Schema.Literal("naming"),
7552
+ person: PersonId,
7553
+ suggestion: Schema.optionalKey(Schema.String)
7554
+ })]);
7555
+ /**
7556
+ * A browser session renewed: its expiry, in its person's Owner DO and in the directory, and on its cookie.
7557
+ * Renewed at most once a day, so one asked sooner answers its expiry unchanged.
7558
+ */
7559
+ const SessionRenewed = Schema.Struct({ expiresAt: Schema.DateTimeUtcFromMillis });
7560
+ /**
7561
+ * What starting a GitHub sign-in at `POST /auth/sign-in/social` answers: GitHub's address to go
7562
+ * to, with the sign-in's state in a cookie set alongside. Better Auth's answer, which ref0 serves.
7563
+ */
7564
+ 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"));
7565
+ Schema.Struct({ url: NavigationUrl });
7566
+ /**
7567
+ * Where signing in sends the browser back to, as `POST /auth/sign-in/social` takes it: a path on
7568
+ * ref0's own origin, such as `/welcome`, or a whole URL. Never a socket's.
7569
+ */
7570
+ const SignInDestination = Schema.String.pipe(Schema.brand("SignInDestination"));
7571
+ Schema.Struct({
7572
+ provider: Schema.Literal("github"),
7573
+ callbackURL: SignInDestination,
7574
+ errorCallbackURL: SignInDestination
7575
+ });
7576
+ /** The name a naming person chose for their owner. */
7577
+ const NameOwner = Schema.Struct({ name: OwnerName });
7578
+ //#endregion
7544
7579
  //#region ../contracts/src/ask-calls.ts
7545
7580
  /**
7546
7581
  * Asks: a question that waits on an answer, from an agent's session or a person, on a thread,
@@ -11659,6 +11694,151 @@ Schema.Struct({
11659
11694
  name: RepoName
11660
11695
  }))
11661
11696
  });
11697
+ /** Where an agent's session asks for an identity token. */
11698
+ const identityTokenPath = "/v1/me/identity-token";
11699
+ /** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
11700
+ const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
11701
+ /** `https://`, a host name, then maybe `:` and a port with no leading zero. */
11702
+ const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
11703
+ /**
11704
+ * Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
11705
+ * the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
11706
+ * the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
11707
+ */
11708
+ const parsesAsWritten = (audience) => {
11709
+ const parts = audiencePattern.exec(audience);
11710
+ if (parts === null || !URL.canParse(audience)) return false;
11711
+ const url = new URL(audience);
11712
+ const port = parts[2] ?? "";
11713
+ return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
11714
+ };
11715
+ /**
11716
+ * Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
11717
+ * with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
11718
+ * case and any port too, so `https://verifier.example:443` is another audience than
11719
+ * `https://verifier.example`. ref0 never fetches or resolves it.
11720
+ */
11721
+ 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"));
11722
+ /** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
11723
+ const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
11724
+ /** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
11725
+ 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"));
11726
+ /** What asking for an identity token answers: the token, and when it expires, its `exp`. */
11727
+ const IssuedIdentityToken = Schema.Struct({
11728
+ token: IdentityToken,
11729
+ expiresAt: Schema.DateTimeUtcFromMillis
11730
+ });
11731
+ /**
11732
+ * An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
11733
+ * token, naming its issuance in ref0's log without naming any row or secret.
11734
+ */
11735
+ 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"));
11736
+ /**
11737
+ * A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
11738
+ * person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
11739
+ * audience, different at every other, and carrying no time.
11740
+ */
11741
+ const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
11742
+ /** A session, as one audience knows it, made as a person's is. */
11743
+ const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
11744
+ /** The accountable person, last in a token's chain: only their pairwise ID. */
11745
+ const PersonActor = Schema.Struct({ sub: PairwisePersonId });
11746
+ /** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
11747
+ const sessionActing = (act) => Schema.Struct({
11748
+ sub: PairwiseSessionId,
11749
+ act
11750
+ });
11751
+ const oneSession = sessionActing(PersonActor);
11752
+ const twoSessions = sessionActing(oneSession);
11753
+ const threeSessions = sessionActing(twoSessions);
11754
+ /**
11755
+ * A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
11756
+ * then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
11757
+ * allows the agent in `sub`, never an actor.
11758
+ */
11759
+ const IdentityActor = Schema.Union([
11760
+ oneSession,
11761
+ twoSessions,
11762
+ threeSessions,
11763
+ sessionActing(threeSessions)
11764
+ ]);
11765
+ /** A time in a token, in whole UTC epoch seconds. */
11766
+ const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
11767
+ Schema.Struct({
11768
+ iss: ApiOrigin,
11769
+ sub: AgentId,
11770
+ aud: IdentityAudience,
11771
+ iat: EpochSeconds,
11772
+ exp: EpochSeconds,
11773
+ jti: IdentityTokenId,
11774
+ act: IdentityActor
11775
+ }).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
11776
+ /**
11777
+ * A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
11778
+ * never names two keys.
11779
+ */
11780
+ const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
11781
+ Schema.Struct({
11782
+ alg: Schema.Literal("ES256"),
11783
+ kid: SigningKeyId,
11784
+ typ: Schema.Literal("JWT")
11785
+ });
11786
+ /** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
11787
+ const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
11788
+ /** One public key in the JWKS: exactly these fields, and never a private one. */
11789
+ const PublicSigningKey = Schema.Struct({
11790
+ kty: Schema.Literal("EC"),
11791
+ crv: Schema.Literal("P-256"),
11792
+ x: Coordinate,
11793
+ y: Coordinate,
11794
+ use: Schema.Literal("sig"),
11795
+ alg: Schema.Literal("ES256"),
11796
+ kid: SigningKeyId
11797
+ });
11798
+ Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
11799
+ Schema.Struct({
11800
+ issuer: ApiOrigin,
11801
+ jwks_uri: Schema.String,
11802
+ subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
11803
+ id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
11804
+ claims_supported: Schema.Tuple([
11805
+ Schema.Literal("iss"),
11806
+ Schema.Literal("sub"),
11807
+ Schema.Literal("aud"),
11808
+ Schema.Literal("exp"),
11809
+ Schema.Literal("iat"),
11810
+ Schema.Literal("jti"),
11811
+ Schema.Literal("act")
11812
+ ])
11813
+ });
11814
+ /**
11815
+ * The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
11816
+ * `IdentityAudience` says. The refused text is never echoed.
11817
+ */
11818
+ var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
11819
+ /**
11820
+ * Less than a whole second is left before the session, a parent or the credential expires, or
11821
+ * the token would expire before it's recorded: the next prompt renews or replaces the session.
11822
+ */
11823
+ var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
11824
+ /**
11825
+ * ref0 signed an identity token for one of a project's sessions: in that project's log, on the
11826
+ * session's thread, with ref0's own IDs for the session, its agent and its accountable person,
11827
+ * and the token's `jti`, never the token. It proves ref0 made the token, not that any service
11828
+ * received or used it. A failed append discards the token, so every token has its event.
11829
+ */
11830
+ const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
11831
+ session: SessionId,
11832
+ agent: AgentRef,
11833
+ /** The person whose attachment authorized the session's chain, last in the token's `act`. */
11834
+ accountable: PersonId,
11835
+ jti: IdentityTokenId,
11836
+ audience: IdentityAudience,
11837
+ issuer: ApiOrigin,
11838
+ keyId: SigningKeyId,
11839
+ issuedAt: Schema.DateTimeUtcFromMillis,
11840
+ expiresAt: Schema.DateTimeUtcFromMillis
11841
+ }));
11662
11842
  Schema.Literals([
11663
11843
  "info/refs",
11664
11844
  "git-upload-pack",
@@ -11911,6 +12091,71 @@ const GateAddition = Schema.Struct({
11911
12091
  from: NeedFrom
11912
12092
  });
11913
12093
  /**
12094
+ * A yes that met a need: its decider's, given by the person `by` when one gave it, such as an
12095
+ * admin answering as the owner's admins; or, `overridden`, a check ref0 said no to, met by the
12096
+ * yes of each of its overrides.
12097
+ */
12098
+ const NeedYes = Schema.Struct({
12099
+ decider: DeciderRef,
12100
+ by: Schema.optionalKey(PersonId),
12101
+ overridden: Schema.optionalKey(Schema.Literal(true))
12102
+ });
12103
+ /**
12104
+ * An override left of a check ref0 said no to: of a part's failed run, or of the check not running,
12105
+ * and who answers it.
12106
+ */
12107
+ const OverrideLeft = Schema.Struct({
12108
+ of: Schema.Literals(["failed run", "not run"]),
12109
+ by: OverrideAnswerer
12110
+ });
12111
+ /**
12112
+ * A decider a need waits on. For a check ref0 said no to, `overrides` lists each of its overrides
12113
+ * left, every one of whose yeses meets it in a pass's place, none when it has none: one yes to a
12114
+ * gate answers every decider its person may answer as, each override among them.
12115
+ */
12116
+ const NeedWait = Schema.Struct({
12117
+ decider: DeciderRef,
12118
+ overrides: Schema.optionalKey(Schema.Array(OverrideLeft).check(Schema.isMaxLength(maxGateDeciders)))
12119
+ });
12120
+ /**
12121
+ * How one of a version's needs stands under its gate's answers now, as the gate is decided from
12122
+ * them: `met` by each yes in `by`; `met` with nothing to wait for, `as` `no checks`, for `checks`
12123
+ * when none of the repo's checks runs on the change, or `on the tip`, for a check only the merge
12124
+ * queue's tip runs, which judges it before `main` moves; `waiting` on each decider in `on`, when
12125
+ * `all`, or else on a yes from any one of them; `waiting` for ref0 to read which of `main`'s
12126
+ * checks run on the change; or `refused` by a person's no to one of its deciders, or to an
12127
+ * override of one, which decides the whole gate no.
12128
+ */
12129
+ const NeedStanding = Schema.Union([
12130
+ Schema.Struct({
12131
+ state: Schema.Literal("met"),
12132
+ by: Schema.Array(NeedYes).check(Schema.isMaxLength(maxGateDeciders))
12133
+ }),
12134
+ Schema.Struct({
12135
+ state: Schema.Literal("met"),
12136
+ as: Schema.Literals(["no checks", "on the tip"])
12137
+ }),
12138
+ Schema.Struct({
12139
+ state: Schema.Literal("waiting"),
12140
+ on: Schema.Array(NeedWait).check(Schema.isMaxLength(maxGateDeciders)),
12141
+ all: Schema.Boolean
12142
+ }),
12143
+ Schema.Struct({
12144
+ state: Schema.Literal("waiting"),
12145
+ for: Schema.Literal("checks read")
12146
+ }),
12147
+ Schema.Struct({
12148
+ state: Schema.Literal("refused"),
12149
+ decider: DeciderRef,
12150
+ by: PersonId
12151
+ })
12152
+ ]);
12153
+ /** A version's need, with every place it came from, and how it stands once its gate is open. */
12154
+ const GateNeedView = Schema.Struct({
12155
+ ...GateNeed.fields,
12156
+ standing: Schema.optionalKey(NeedStanding)
12157
+ });
12158
+ /**
11914
12159
  * The version's gate decided again in place under `main`'s `.ref0/` now (Q7c): on which `main`
11915
12160
  * and under its tree, `pending` until `main`'s gate answers, then `held` or `grew`; the needs
11916
12161
  * it answered, joined to ref0's floor, absent while pending; what `main`'s gate read of the
@@ -12012,8 +12257,21 @@ const GateView = Schema.Struct({
12012
12257
  state: GateState,
12013
12258
  /** Why the gate failed; null otherwise. */
12014
12259
  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),
12260
+ /**
12261
+ * None while pending; once answered, the floor's too, sorted as they're spelled, each with how
12262
+ * it stands once the version's gate is open.
12263
+ */
12264
+ needs: Schema.Array(GateNeedView),
12265
+ /** The repo's owner, whose admins answer its admins' needs: at home, its person alone. */
12266
+ owner: OwnerId,
12267
+ /**
12268
+ * The names of the owner and of the people its needs' standings name, where the directory has
12269
+ * them.
12270
+ */
12271
+ names: Schema.optionalKey(Schema.Array(Schema.Struct({
12272
+ owner: OwnerId,
12273
+ name: OwnerName
12274
+ })).check(Schema.isMaxLength(maxGateDeciders + 1))),
12017
12275
  /** What the gate read of the change's trees, each read once, in order; none while pending. */
12018
12276
  reads: GateReads,
12019
12277
  /** The project gate revision the version was decided under; none for 0. */
@@ -14133,6 +14391,7 @@ const KnownEvent = Schema.Union([
14133
14391
  ThreadListingChanged,
14134
14392
  SessionStarted,
14135
14393
  SessionEnded,
14394
+ IdentityTokenIssued,
14136
14395
  MemberRoleChanged,
14137
14396
  MemberRemoved,
14138
14397
  InvitationSent,
@@ -14994,6 +15253,11 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
14994
15253
  * what to change. Any other part that fails its contract is answered with Effect's empty 400.
14995
15254
  */
14996
15255
  var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
15256
+ /**
15257
+ * An identity token's request whose audience its contract refuses, missing or not an HTTPS
15258
+ * origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
15259
+ */
15260
+ var IdentityAudienceRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/IdentityAudienceRefusal", { error: IdentityAudienceRefused }) {};
14997
15261
  const idempotency = { "idempotency-key": RequestId };
14998
15262
  const inProject$1 = {
14999
15263
  owner: OwnerRef,
@@ -15216,7 +15480,18 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
15216
15480
  UsageRangeRefused,
15217
15481
  AuthorityUnavailable
15218
15482
  ]
15219
- })).middleware(Authorization) {};
15483
+ }), HttpApiEndpoint.post("identityToken", identityTokenPath, {
15484
+ payload: IdentityTokenRequest,
15485
+ success: IssuedIdentityToken,
15486
+ error: [
15487
+ IdentityAudienceRefused,
15488
+ IdentityTokenNotReady,
15489
+ NotPermitted,
15490
+ SessionHasEnded,
15491
+ SlowDown,
15492
+ AuthorityUnavailable
15493
+ ]
15494
+ }).middleware(IdentityAudienceRefusal)).middleware(Authorization) {};
15220
15495
  /**
15221
15496
  * A thread's endpoints, as one of a project's threads and an org thread take them alike, by
15222
15497
  * where they are: `at` is a project's path parameters, or the owner's alone for its own scope.
@@ -16152,15 +16427,18 @@ const decisionEndpoints = (at) => {
16152
16427
  HttpApiEndpoint.get("get", "/:decision", {
16153
16428
  params: onDecision,
16154
16429
  query: { wait: Schema.optionalKey(DecisionWait) },
16155
- success: DecisionView,
16430
+ success: NamedDecision,
16156
16431
  error: [...projectErrors, DecisionNotFound]
16157
16432
  }),
16158
16433
  HttpApiEndpoint.post("answer", "/:decision/answers", {
16159
16434
  params: onDecision,
16160
16435
  headers: idempotency,
16161
16436
  payload: AnswerRequest,
16162
- success: DecisionView,
16437
+ success: NamedDecision,
16163
16438
  error: [
16439
+ ApprovalNeeded,
16440
+ CommandRefused,
16441
+ SlowDown,
16164
16442
  ...projectErrors,
16165
16443
  DecisionNotFound,
16166
16444
  AnswerRefused,
@@ -17007,6 +17285,15 @@ const ownSurfaces = {
17007
17285
  cli: "meters",
17008
17286
  mcp: null
17009
17287
  },
17288
+ "me.identityToken": {
17289
+ action: "session:change",
17290
+ actors: "agent",
17291
+ target: "session",
17292
+ stepUp: "never",
17293
+ asks: "never",
17294
+ cli: "identity-token",
17295
+ mcp: null
17296
+ },
17010
17297
  "threads.list": {
17011
17298
  action: "project:read",
17012
17299
  actors: "both",
@@ -18464,7 +18751,7 @@ const HooksFile = Schema.Struct({ hooks: Schema.Record(Schema.String, Schema.Arr
18464
18751
  const McpServersFile = Schema.Struct({ mcpServers: Schema.Record(Schema.String, Schema.Struct({
18465
18752
  command: Schema.String,
18466
18753
  args: Schema.Array(Schema.String),
18467
- /** The variables Codex passes the server beside `HOME`, `PATH` and `SHELL`. */
18754
+ /** The variables Codex passes the server beside the few it always passes, such as `HOME`. */
18468
18755
  env_vars: Schema.optionalKey(Schema.Array(Schema.String))
18469
18756
  })) });
18470
18757
  const PluginManifest = Schema.Struct({
@@ -18669,6 +18956,7 @@ const StatusCheck = Schema.Struct({
18669
18956
  "accepted",
18670
18957
  "plugin",
18671
18958
  "trusted",
18959
+ "server",
18672
18960
  "session"
18673
18961
  ]),
18674
18962
  answer: StatusAnswer,
@@ -18823,6 +19111,12 @@ const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandom
18823
19111
  //#endregion
18824
19112
  //#region src/session.ts
18825
19113
  /**
19114
+ * An agent's command in a harness session that has no ref0 session, though an agent is connected
19115
+ * for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
19116
+ * person instead.
19117
+ */
19118
+ var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
19119
+ /**
18826
19120
  * The token of the session this command runs in, if it runs in one: an agent's, which its
18827
19121
  * harness bound when the session started. Where it's found is the harness's to say. Outside a
18828
19122
  * session, there's none, and every command is the person's, asked by approval.
@@ -20173,10 +20467,10 @@ const blockerLines = (list, address) => {
20173
20467
  ].join("\n");
20174
20468
  };
20175
20469
  /** How many blockers stand on the thread now, in words. */
20176
- 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.`;
20470
+ 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.`;
20177
20471
  /** A blocker added, in words: which, its ask, how many stand, and a publish it didn't stop. */
20178
20472
  const addedWords = (changed, address) => [
20179
- `Blocked ${address} by ${changed.blocker}. ${standingWords(changed)}`,
20473
+ `Blocked ${address} by ${changed.blocker}. ${standingWords$1(changed)}`,
20180
20474
  ...changed.decision === void 0 ? [] : [`It waits on a person’s answer to the ask ${changed.decision}.`],
20181
20475
  ...changed.admitted === void 0 ? [] : [`A merge carrying it was already under way in ${changed.admitted.join(", ")}; the blocker doesn’t stop that one.`]
20182
20476
  ].join("\n");
@@ -20323,7 +20617,7 @@ const clearing = (name, description) => Command.make(name, {
20323
20617
  payload
20324
20618
  }));
20325
20619
  const done = name === "remove" ? "Removed" : "Marked met";
20326
- yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords(answer.value)}`);
20620
+ yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords$1(answer.value)}`);
20327
20621
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(description));
20328
20622
  /** `ref0 blocked` and its subcommands. */
20329
20623
  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([
@@ -20454,6 +20748,18 @@ const harnessFlagNames = { protocol: "protocol" };
20454
20748
  * release, which no hook's declaration carries.
20455
20749
  */
20456
20750
  const pluginVersionVariable = "REF0_PLUGIN_VERSION";
20751
+ /**
20752
+ * What the launcher puts in `ref0 mcp`'s environment in a checkout of another origin: that
20753
+ * origin. The server runs all the same, answering every hook with nothing, since a harness
20754
+ * takes a server that exits before `initialize` as broken.
20755
+ */
20756
+ const otherOriginVariable = "REF0_OTHER_ORIGIN";
20757
+ /**
20758
+ * What `ref0 status` puts in the environment of a folder's ref0 server, which it starts to check
20759
+ * it starts: the harness it plays. No harness sets it, so a harness's server finds its harness up
20760
+ * its parent chain.
20761
+ */
20762
+ const serverCheckVariable = "REF0_SERVER_CHECK";
20457
20763
  /** A flag as a command line writes it. */
20458
20764
  const flagWord = (name) => `--${name}`;
20459
20765
  /**
@@ -20561,10 +20867,19 @@ const sessionStartHook = ref0Hook(harnessCommandNames.sessionStart);
20561
20867
  /** Ends the session, as a command too. */
20562
20868
  const sessionEndHook = ref0Hook(harnessCommandNames.sessionEnd);
20563
20869
  /**
20870
+ * What Linux's keychain needs to be reached: `secret-tool` finds the session bus by its address,
20871
+ * or else in the runtime folder, and with neither it can't read a grant. Codex 0.164.0 passes a
20872
+ * server neither unless asked. macOS's keychain needs neither, and Codex passes an unset one as
20873
+ * unset.
20874
+ */
20875
+ const keychainVariables = ["DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR"];
20876
+ /**
20564
20877
  * The plugin's server, `ref0 mcp`, started by M0's inline launcher, the lookup in one line: a
20565
20878
  * script path in a server's arguments stays literal in Codex. It has no `cwd`, so the server
20566
- * keeps the project's folder in both harnesses. Codex starts its servers with only `HOME`, `PATH`
20567
- * and `SHELL`, so it's asked for `CODEX_HOME`, which an unset one leaves unset.
20879
+ * keeps the project's folder in both harnesses. Codex starts its servers with only a few of its
20880
+ * variables, such as `HOME`, `PATH` and `SHELL` (`server-check.ts` lists them), so it's asked for
20881
+ * `CODEX_HOME` and the keychain's, each passed through as Codex has it and never written down: a
20882
+ * bus address changes with each login. Claude Code passes its whole environment.
20568
20883
  */
20569
20884
  const mcpFile = (version, api, server = "ref0") => ({
20570
20885
  path: "ref0/.mcp.json",
@@ -20579,133 +20894,14 @@ const mcpFile = (version, api, server = "ref0") => ({
20579
20894
  env_vars: [
20580
20895
  "CODEX_HOME",
20581
20896
  "CLAUDE_CONFIG_DIR",
20582
- "REF0_API"
20897
+ "REF0_API",
20898
+ ...keychainVariables
20583
20899
  ]
20584
20900
  } } })
20585
20901
  });
20586
20902
  /** Each event a harness's hooks send `ref0 mcp`, with the fields it reads, as an `mcp_tool` hook. */
20587
20903
  const mcpToolHooks = (server, hookFields) => Object.fromEntries(Object.entries(hookFields).map(([event, fields]) => [event, [mcpToolHook(server, event, fields)]]));
20588
20904
  //#endregion
20589
- //#region src/keychain.ts
20590
- /**
20591
- * The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
20592
- * makes, and a session's token. Never a file. On macOS through `security`, on Linux through
20593
- * `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
20594
- * an argument, since any process on the machine can read another's arguments.
20595
- */
20596
- var KeychainError = class extends Schema.TaggedError()("KeychainError", {
20597
- nextStep: Schema.String,
20598
- /** The keychain answered, with an item ref0 can't read, rather than not answering. */
20599
- unreadable: Schema.optionalKey(Schema.Literal(true))
20600
- }) {};
20601
- /**
20602
- * The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
20603
- * grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
20604
- */
20605
- const keychainIn = Effect.fnUntraced(function* (service) {
20606
- const crypto = yield* Crypto.Crypto;
20607
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
20608
- const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
20609
- const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
20610
- const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
20611
- const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
20612
- const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
20613
- const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
20614
- const code = yield* handle.exitCode;
20615
- return {
20616
- output: output.trim(),
20617
- complaint: complaint.trim(),
20618
- code,
20619
- succeeded: code === ChildProcessSpawner.ExitCode(0)
20620
- };
20621
- })).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
20622
- const get = Effect.fn("Keychain.get")(function* (account) {
20623
- const found = macOS ? yield* run("security", [
20624
- "find-generic-password",
20625
- "-s",
20626
- service,
20627
- "-a",
20628
- account,
20629
- "-w"
20630
- ], "") : yield* run("secret-tool", [
20631
- "lookup",
20632
- "service",
20633
- service,
20634
- "account",
20635
- account
20636
- ], "");
20637
- if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
20638
- if (macOS) {
20639
- if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
20640
- return yield* unavailable;
20641
- }
20642
- if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
20643
- const listed = yield* run("secret-tool", [
20644
- "search",
20645
- "--all",
20646
- "service",
20647
- service,
20648
- "account",
20649
- account
20650
- ], "");
20651
- if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
20652
- return Option.none();
20653
- });
20654
- const set = Effect.fn("Keychain.set")(function* (account, secret) {
20655
- const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
20656
- const value = Redacted.value(secret);
20657
- if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
20658
- const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
20659
- if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
20660
- if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
20661
- "store",
20662
- "--label",
20663
- `ref0 ${account}`,
20664
- "service",
20665
- service,
20666
- "account",
20667
- account
20668
- ], Redacted.value(secret))).succeeded) return yield* unavailable;
20669
- });
20670
- const remove = Effect.fn("Keychain.remove")(function* (account) {
20671
- if (!(yield* macOS ? run("security", [
20672
- "delete-generic-password",
20673
- "-s",
20674
- service,
20675
- "-a",
20676
- account
20677
- ], "") : run("secret-tool", [
20678
- "clear",
20679
- "service",
20680
- service,
20681
- "account",
20682
- account
20683
- ], "")).succeeded) return yield* unavailable;
20684
- });
20685
- const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
20686
- return {
20687
- probe: Effect.gen(function* () {
20688
- const account = `connect-probe/${yield* random}`;
20689
- const value = Redacted.make(yield* random);
20690
- yield* Effect.gen(function* () {
20691
- yield* set(account, value);
20692
- const kept = yield* get(account);
20693
- if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
20694
- yield* remove(account);
20695
- }).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
20696
- }).pipe(Effect.timeoutOrElse({
20697
- duration: Duration.minutes(1),
20698
- orElse: () => Effect.fail(unavailable)
20699
- })),
20700
- get,
20701
- set,
20702
- remove
20703
- };
20704
- });
20705
- var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
20706
- static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
20707
- };
20708
- //#endregion
20709
20905
  //#region src/harness/binding.ts
20710
20906
  /**
20711
20907
  * Where a session's token is kept: the machine's keychain, under the API origin’s keychain service, by an
@@ -20743,9 +20939,8 @@ function bindingFromEnvironment(environment, found, hookSession) {
20743
20939
  //#endregion
20744
20940
  //#region src/harness/session-token.ts
20745
20941
  /**
20746
- * The session token of the harness session a command runs in, from the keychain, under the
20747
- * account its binding names. Outside a harness, or in one whose session ref0 never started,
20748
- * there's none, and a command is its person's, asked by approval.
20942
+ * The harness session a command runs in: the harness process up its parent chain, and the
20943
+ * binding its session's token is kept under, which `command-token.ts` reads.
20749
20944
  */
20750
20945
  const optional$1 = (name) => Config.String(name).pipe(Config.option, Config.map(Option.getOrUndefined));
20751
20946
  /** What the harness put in this process's environment, as far as a binding needs. */
@@ -20753,7 +20948,6 @@ const harnessEnvironment = Config.all({
20753
20948
  CLAUDE_CODE_SESSION_ID: optional$1("CLAUDE_CODE_SESSION_ID"),
20754
20949
  CLAUDE_PID: optional$1("CLAUDE_PID"),
20755
20950
  CODEX_SESSION_ID: optional$1("CODEX_SESSION_ID"),
20756
- CODEX_HOME: optional$1("CODEX_HOME"),
20757
20951
  CODEX_SANDBOX: optional$1("CODEX_SANDBOX")
20758
20952
  });
20759
20953
  /**
@@ -20828,17 +21022,6 @@ const bindingForServer = Effect.fnUntraced(function* (harnessSession) {
20828
21022
  process: found.value.process
20829
21023
  });
20830
21024
  });
20831
- /** `SessionToken` from the keychain, by the binding of the harness session this runs in. */
20832
- const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
20833
- const keychain = yield* Keychain;
20834
- const harness = yield* HarnessProcess;
20835
- const current = Effect.gen(function* () {
20836
- const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
20837
- if (Option.isNone(binding)) return Option.none();
20838
- return yield* keychain.get(sessionAccount(binding.value));
20839
- });
20840
- return SessionToken.of({ current });
20841
- }));
20842
21025
  //#endregion
20843
21026
  //#region src/harness/config-folders.ts
20844
21027
  const Folders = Schema.Array(Schema.String);
@@ -20951,6 +21134,126 @@ const connectLineFor = Effect.fnUntraced(function* (harness, configFolder) {
20951
21134
  return `${configFolders[harness].variable}=${shellWord(configFolder)} ref0 connect`;
20952
21135
  });
20953
21136
  //#endregion
21137
+ //#region src/keychain.ts
21138
+ /**
21139
+ * The machine's keychain, where the CLI keeps what lets it act: the host grant `ref0 connect`
21140
+ * makes, and a session's token. Never a file. On macOS through `security`, on Linux through
21141
+ * `secret-tool`, under an origin-specific `ref0/<origin>` service, by account. A secret goes in through stdin, never as
21142
+ * an argument, since any process on the machine can read another's arguments.
21143
+ */
21144
+ var KeychainError = class extends Schema.TaggedError()("KeychainError", {
21145
+ nextStep: Schema.String,
21146
+ /** The keychain answered, with an item ref0 can't read, rather than not answering. */
21147
+ unreadable: Schema.optionalKey(Schema.Literal(true))
21148
+ }) {};
21149
+ /**
21150
+ * The keychain under one service. Each API origin has its own, `ref0/<origin>`; 0.5.0 kept its
21151
+ * grants under `ref0`, which only `ref0 connect` reads, to replace and delete them.
21152
+ */
21153
+ const keychainIn = Effect.fnUntraced(function* (service) {
21154
+ const crypto = yield* Crypto.Crypto;
21155
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
21156
+ const macOS = (yield* spawner.string(ChildProcess.make("uname", ["-s"])).pipe(Effect.orElseSucceed(() => ""))).trim() === "Darwin";
21157
+ const unavailable = new KeychainError({ nextStep: macOS ? "The keychain didn’t answer. Unlock it, and try again." : "The keychain didn’t answer. Unlock the keychain or start a secret service, and try again." });
21158
+ const missing = new KeychainError({ nextStep: "The keychain didn’t answer: ref0 needs `secret-tool`, from libsecret. Install `secret-tool` with `sudo apt install libsecret-tools` on Debian or Ubuntu, `sudo dnf install libsecret` on Fedora or `sudo pacman -S libsecret` on Arch." });
21159
+ const run = (command, args, stdin) => Effect.scoped(Effect.gen(function* () {
21160
+ const handle = yield* spawner.spawn(ChildProcess.make(command, args, { stdin: Stream.make(new TextEncoder().encode(stdin)) })).pipe(Effect.mapError((error) => !macOS && Predicate.isTagged(error.reason, "NotFound") ? missing : unavailable));
21161
+ const [output, complaint] = yield* Effect.all([handle.stdout.pipe(Stream.decodeText(), Stream.mkString), handle.stderr.pipe(Stream.decodeText(), Stream.mkString)], { concurrency: "unbounded" });
21162
+ const code = yield* handle.exitCode;
21163
+ return {
21164
+ output: output.trim(),
21165
+ complaint: complaint.trim(),
21166
+ code,
21167
+ succeeded: code === ChildProcessSpawner.ExitCode(0)
21168
+ };
21169
+ })).pipe(Effect.catchTag("PlatformError", () => Effect.fail(unavailable)));
21170
+ const get = Effect.fn("Keychain.get")(function* (account) {
21171
+ const found = macOS ? yield* run("security", [
21172
+ "find-generic-password",
21173
+ "-s",
21174
+ service,
21175
+ "-a",
21176
+ account,
21177
+ "-w"
21178
+ ], "") : yield* run("secret-tool", [
21179
+ "lookup",
21180
+ "service",
21181
+ service,
21182
+ "account",
21183
+ account
21184
+ ], "");
21185
+ if (found.succeeded) return found.output.length > 0 ? Option.some(Redacted.make(found.output)) : Option.none();
21186
+ if (macOS) {
21187
+ if (found.code === ChildProcessSpawner.ExitCode(44)) return Option.none();
21188
+ return yield* unavailable;
21189
+ }
21190
+ if (found.code !== ChildProcessSpawner.ExitCode(1) || found.complaint.length > 0) return yield* unavailable;
21191
+ const listed = yield* run("secret-tool", [
21192
+ "search",
21193
+ "--all",
21194
+ "service",
21195
+ service,
21196
+ "account",
21197
+ account
21198
+ ], "");
21199
+ if (listed.output.length > 0 || listed.complaint.length > 0) return yield* unavailable;
21200
+ return Option.none();
21201
+ });
21202
+ const set = Effect.fn("Keychain.set")(function* (account, secret) {
21203
+ const quote = (text) => `"${text.replace(/[\\"]/g, "\\$&")}"`;
21204
+ const value = Redacted.value(secret);
21205
+ if (macOS && /[\r\n\0]/.test(account + value)) return yield* unavailable;
21206
+ const input = macOS ? `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(value)}\n` : value;
21207
+ if (macOS && new TextEncoder().encode(input).length >= 4096) return yield* unavailable;
21208
+ if (!(macOS ? yield* run("security", ["-i"], input) : yield* run("secret-tool", [
21209
+ "store",
21210
+ "--label",
21211
+ `ref0 ${account}`,
21212
+ "service",
21213
+ service,
21214
+ "account",
21215
+ account
21216
+ ], Redacted.value(secret))).succeeded) return yield* unavailable;
21217
+ });
21218
+ const remove = Effect.fn("Keychain.remove")(function* (account) {
21219
+ if (!(yield* macOS ? run("security", [
21220
+ "delete-generic-password",
21221
+ "-s",
21222
+ service,
21223
+ "-a",
21224
+ account
21225
+ ], "") : run("secret-tool", [
21226
+ "clear",
21227
+ "service",
21228
+ service,
21229
+ "account",
21230
+ account
21231
+ ], "")).succeeded) return yield* unavailable;
21232
+ });
21233
+ const random = crypto.randomUUIDv4.pipe(Effect.mapError(() => unavailable));
21234
+ return {
21235
+ probe: Effect.gen(function* () {
21236
+ const account = `connect-probe/${yield* random}`;
21237
+ const value = Redacted.make(yield* random);
21238
+ yield* Effect.gen(function* () {
21239
+ yield* set(account, value);
21240
+ const kept = yield* get(account);
21241
+ if (Option.isNone(kept) || Redacted.value(kept.value) !== Redacted.value(value)) return yield* unavailable;
21242
+ yield* remove(account);
21243
+ }).pipe(Effect.catch((error) => remove(account).pipe(Effect.ignore, Effect.andThen(Effect.fail(error)))));
21244
+ }).pipe(Effect.timeoutOrElse({
21245
+ duration: Duration.minutes(1),
21246
+ orElse: () => Effect.fail(unavailable)
21247
+ })),
21248
+ get,
21249
+ set,
21250
+ remove
21251
+ };
21252
+ });
21253
+ var Keychain = class Keychain extends Context.Service()("ref0/cli/Keychain") {
21254
+ static layer = Layer.effect(Keychain, Effect.flatMap(Ref0Config, (config) => Effect.map(keychainIn(`ref0/${originKey(config.api)}`), Keychain.of)));
21255
+ };
21256
+ //#endregion
20954
21257
  //#region src/host-grant.ts
20955
21258
  /**
20956
21259
  * All lasting authority for one config folder, stored as one keychain value: the agent the folder
@@ -20966,6 +21269,11 @@ const KeptHostGrant = Schema.Struct({
20966
21269
  });
20967
21270
  const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
20968
21271
  const encodeGrant = Schema.encodeSync(Schema.fromJsonString(KeptHostGrant));
21272
+ /**
21273
+ * The token alone, from any record that's JSON with a string `token`, so a record this ref0 can't
21274
+ * read as a grant, such as one an earlier ref0 kept, still names the grant it held.
21275
+ */
21276
+ const decodeHeldToken = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({ token: KeptHostGrant.fields.token })));
20969
21277
  /** Grants in the origin’s keychain service, account host-grant/<resolved config folder>. */
20970
21278
  var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant") {
20971
21279
  static layer = Layer.effect(HostGrant, Effect.gen(function* () {
@@ -20981,6 +21289,7 @@ var HostGrant = class HostGrant extends Context.Service()("ref0/cli/HostGrant")
20981
21289
  unreadable: true
20982
21290
  }));
20983
21291
  })),
21292
+ heldToken: (folder) => account(folder).pipe(Effect.flatMap(keychain.get), Effect.map(Option.flatMap((kept) => Option.map(decodeHeldToken(Redacted.value(kept)), (held) => held.token)))),
20984
21293
  keep: (folder, grant) => Effect.gen(function* () {
20985
21294
  yield* keychain.set(yield* account(folder), Redacted.make(encodeGrant(grant)));
20986
21295
  yield* folders.keep(folder).pipe(Effect.mapError(() => new KeychainError({ nextStep: "Couldn’t record this connected config folder in ~/.ref0. Check that it’s writable, then run ref0 connect." })));
@@ -21008,7 +21317,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
21008
21317
  * an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
21009
21318
  * and only as a default export.
21010
21319
  */
21011
- const cliVersion = "0.9.0";
21320
+ const cliVersion = "0.11.0";
21012
21321
  //#endregion
21013
21322
  //#region src/harness/hook-protocol.ts
21014
21323
  /**
@@ -21332,12 +21641,12 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21332
21641
  const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
21333
21642
  const requestId = yield* newRequestId;
21334
21643
  const asking = yield* Effect.forEach(asked, Effect.fnUntraced(function* (folder) {
21335
- const previous = yield* hostGrant.get(folder.configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone), Effect.catchTag("KeychainError", said));
21644
+ const held = yield* hostGrant.heldToken(folder.configFolder).pipe(Effect.catchTag("KeychainError", said));
21336
21645
  return {
21337
21646
  ...folder,
21338
21647
  shown: shownConfigFolder(home, folder.configFolder),
21339
21648
  grant: yield* freshTokenSecret,
21340
- replaces: Option.isSome(previous) ? grantOf(previous.value.token) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
21649
+ replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
21341
21650
  };
21342
21651
  }));
21343
21652
  const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
@@ -21386,9 +21695,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21386
21695
  "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.",
21387
21696
  "Waiting for the answer. Ctrl-C stops it."
21388
21697
  ]]);
21389
- let typed = "";
21390
- const show = (text) => Effect.ignore(terminal.display(text));
21391
- const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
21698
+ const endWait = yield* Effect.cached(Console.error(""));
21392
21699
  const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
21393
21700
  if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
21394
21701
  const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
@@ -21547,25 +21854,21 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21547
21854
  });
21548
21855
  const typedCode = Effect.gen(function* () {
21549
21856
  const keys = yield* terminal.readInput;
21857
+ let typed = "";
21550
21858
  while (true) {
21551
21859
  const pressed = yield* Effect.option(Queue.take(keys));
21552
21860
  if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
21553
21861
  const { input, key } = pressed.value;
21554
21862
  if (key.name === "backspace") {
21555
- if (typed === "") continue;
21556
21863
  typed = takeBack(typed);
21557
- yield* show("\b \b");
21558
21864
  continue;
21559
21865
  }
21560
21866
  if (key.name !== "return" && key.name !== "enter") {
21561
21867
  const text = Option.getOrElse(input, () => "");
21562
- if (!typesText(text)) continue;
21563
- typed += text;
21564
- yield* show(text);
21868
+ if (typesText(text)) typed += text;
21565
21869
  continue;
21566
21870
  }
21567
21871
  const line = typed.trim();
21568
- if (typed !== "") yield* show("\n");
21569
21872
  typed = "";
21570
21873
  if (line === "") continue;
21571
21874
  if (isAnswerCode(line)) return line;
@@ -21573,7 +21876,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
21573
21876
  }
21574
21877
  }).pipe(Effect.scoped);
21575
21878
  const readAnswer = Effect.gen(function* () {
21576
- yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21879
+ if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
21577
21880
  const code = yield* typedCode;
21578
21881
  yield* Ref.update(pasted, (first) => first ?? code);
21579
21882
  if (listener !== void 0) yield* listener.stop;
@@ -22710,7 +23013,7 @@ var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatc
22710
23013
  issue: Schema.String
22711
23014
  }) {};
22712
23015
  const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
22713
- const decodeAnswer$1 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
23016
+ const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
22714
23017
  /** The IDs of the exchange's two requests: the opening's, and the one asked. */
22715
23018
  const initializeId = "ref0-initialize";
22716
23019
  const askedId = "ref0-asked";
@@ -22744,7 +23047,7 @@ const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function*
22744
23047
  stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
22745
23048
  endOnDone: false
22746
23049
  }
22747
- }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$1(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);
23050
+ }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
22748
23051
  return Option.filter(answered, (answers) => answers.size === ids.length);
22749
23052
  })).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
22750
23053
  /** `answer`'s result, read by `method`'s own schema, or why it can't be. */
@@ -23088,16 +23391,19 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
23088
23391
  parameters: shellWord(`include.path=${file}`)
23089
23392
  };
23090
23393
  });
23091
- /** Codex has no environment file: its install selects a file; session start fills its checkout. */
23394
+ /**
23395
+ * Codex has no environment file: its install selects a file; session start fills its checkout,
23396
+ * for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`.
23397
+ */
23092
23398
  const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
23093
23399
  const folders = yield* ConfigFolders;
23094
23400
  const fs = yield* FileSystem.FileSystem;
23095
23401
  const path = yield* Path.Path;
23096
23402
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23097
- const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex"));
23403
+ const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex")).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
23098
23404
  if (Option.isNone(agent)) return false;
23099
23405
  const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
23100
- if (pluginRoot === "") return false;
23406
+ if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
23101
23407
  const gitFolder = path.join(path.dirname(pluginRoot), "@git");
23102
23408
  const home = yield* Config.String("HOME");
23103
23409
  const author = yield* codexAgentConfig(agent.value, gitFolder, home);
@@ -23867,6 +24173,7 @@ const writeRef0Launcher = Effect.fnUntraced(function* (home, git) {
23867
24173
  const gitPath = yield* fs.realPath(git);
23868
24174
  yield* fs.writeFileString(next, [
23869
24175
  "#!/bin/sh",
24176
+ `unset ${otherOriginVariable}`,
23870
24177
  "if [ \"$REF0_SELECT_ORIGIN\" = 1 ]; then",
23871
24178
  ` checkout=$(${shellWord(gitPath)} config --get ref0.api 2>/dev/null)`,
23872
24179
  " result=$?",
@@ -23876,7 +24183,11 @@ const writeRef0Launcher = Effect.fnUntraced(function* (home, git) {
23876
24183
  ` *) printf '%s\\n' ${shellWord("ref0: could not read the checkout origin with Git. Restore Git, then run ref0 install again.")} >&2; exit 1 ;;`,
23877
24184
  " esac",
23878
24185
  " export REF0_SELECTED_API",
23879
- ` ${shellWord(node)} -e ${shellWord("try { process.exit(new URL(process.env.REF0_SELECTED_API).origin === process.argv[1] ? 0 : 1) } catch { process.exit(1) }")} ${shellWord(config.api)} || exit 0`,
24186
+ ` if ! ${shellWord(node)} -e ${shellWord("try { process.exit(new URL(process.env.REF0_SELECTED_API).origin === process.argv[1] ? 0 : 1) } catch { process.exit(1) }")} ${shellWord(config.api)}; then`,
24187
+ ` [ "$1" = ${harnessCommandNames.mcp} ] || exit 0`,
24188
+ ` ${otherOriginVariable}=$REF0_SELECTED_API`,
24189
+ ` export ${otherOriginVariable}`,
24190
+ " fi",
23880
24191
  "fi",
23881
24192
  "unset REF0_SELECT_ORIGIN REF0_SELECTED_API",
23882
24193
  `export REF0_API=${shellWord(config.api)}`,
@@ -24032,8 +24343,16 @@ const mostFolders = 8;
24032
24343
  const isConfigFolderPath = Schema.is(ConfigFolderPath);
24033
24344
  /** The line that asks again. */
24034
24345
  const again = "ref0 connect";
24035
- /** Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. */
24346
+ /** The widest a line of ref0's own words runs, as its help does, in a terminal at least as wide. */
24347
+ const mostColumns = 100;
24348
+ /**
24349
+ * Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. The
24350
+ * question is said first, wrapped between words to the terminal, and the chooser shows only the
24351
+ * folders: once Enter chooses, it leaves the ticked ones on one line after any question it was
24352
+ * given, which the terminal would break mid-word.
24353
+ */
24036
24354
  const choose = Effect.fnUntraced(function* (found, home) {
24355
+ const columns = yield* (yield* Terminal.Terminal).columns;
24037
24356
  const choices = found.map((folder) => {
24038
24357
  const shown = shownConfigFolder(home, folder.configFolder);
24039
24358
  const sendable = isConfigFolderPath(shown);
@@ -24049,8 +24368,9 @@ const choose = Effect.fnUntraced(function* (found, home) {
24049
24368
  };
24050
24369
  });
24051
24370
  const ticked = choices.filter((choice) => choice.selected).length;
24371
+ const keys = "Space ticks or unticks one; Enter connects those ticked.";
24372
+ yield* printBlocks([[{ label: "Connect which config folders?" }, ...wrapWords(ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys, columns > 0 ? Math.min(columns, mostColumns) : mostColumns)]]);
24052
24373
  return yield* Prompt.run(Prompt.MultiSelect({
24053
- 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.",
24054
24374
  choices,
24055
24375
  max: mostFolders
24056
24376
  }));
@@ -24158,7 +24478,7 @@ const answerOf = (connected, installed, others) => ({
24158
24478
  const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) => Effect.gen(function* () {
24159
24479
  const session = yield* SessionToken;
24160
24480
  const tty = yield* Tty;
24161
- if (Option.isSome(yield* session.current)) return yield* sayFailed("ref0 connect runs only in a person’s own terminal, never in an agent’s session: it shows a code, and a yes to that code connects this machine to an agent. If this machine needs connecting, tell your person to run ref0 connect in their own terminal. Never ask them to approve a code for you.");
24481
+ if (yield* session.current.pipe(Effect.map(Option.isSome), Effect.catchTag("NoSession", () => Effect.succeed(true)))) return yield* sayFailed("ref0 connect runs only in a person’s own terminal, never in an agent’s session: it shows a code, and a yes to that code connects this machine to an agent. If this machine needs connecting, tell your person to run ref0 connect in their own terminal. Never ask them to approve a code for you.");
24162
24482
  if (!tty.attached) return yield* sayFailed("ref0 connect runs only in a terminal you type into, and this isn’t one. Run it yourself, in your own terminal: not through a script, a pipe or an agent.");
24163
24483
  yield* (yield* ConnectLock).hold;
24164
24484
  yield* (yield* Keychain).probe;
@@ -24167,6 +24487,7 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
24167
24487
  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.`);
24168
24488
  const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
24169
24489
  if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
24490
+ yield* Console.error("");
24170
24491
  const connected = yield* requestAttachment(chosen);
24171
24492
  const installed = yield* installInto(connected.kept, { asJson });
24172
24493
  const others = yield* otherOwners(connected);
@@ -24552,7 +24873,7 @@ const paramValues = (params) => {
24552
24873
  * its params, and when it ends; under it, each workflow's run, by its title and its file, and
24553
24874
  * how it stands; and under each, its newest asks and passkeys.
24554
24875
  */
24555
- const waitingLines = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
24876
+ const waitingLines$1 = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
24556
24877
  const last = index === wait.runs.length - 1;
24557
24878
  const under = last ? " " : "│ ";
24558
24879
  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)}`)];
@@ -25026,7 +25347,7 @@ const isPlainJson = (value, depth = 0) => {
25026
25347
  const proto = Object.getPrototypeOf(value);
25027
25348
  return (proto === Object.prototype || proto === null) && Object.values(value).every(inner);
25028
25349
  };
25029
- const decodeAnswer = Schema.decodeUnknownOption(CheckRunAnswer);
25350
+ const decodeAnswer$1 = Schema.decodeUnknownOption(CheckRunAnswer);
25030
25351
  const encodeJson$1 = Schema.encodeSync(Schema.fromJsonString(Schema.Json));
25031
25352
  /**
25032
25353
  * A check's bundle, written for `import()` under a name of its code's and this process's, in the
@@ -25167,7 +25488,7 @@ const runPart = Effect.fnUntraced(function* (own, part) {
25167
25488
  error: "timeout",
25168
25489
  why: `It ran past its timeout, ${part.timeoutSeconds} seconds`
25169
25490
  }),
25170
- onSome: (answer) => isPlainJson(answer, 0) ? Option.getOrElse(decodeAnswer(answer), () => ({
25491
+ onSome: (answer) => isPlainJson(answer, 0) ? Option.getOrElse(decodeAnswer$1(answer), () => ({
25171
25492
  error: "invalid output",
25172
25493
  why: "Its answer isn't one a check gives"
25173
25494
  })) : {
@@ -25364,7 +25685,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
25364
25685
  if (Option.isNone(thread)) return yield* sayFailed("Name the thread with --thread owner/project#number.");
25365
25686
  const root = yield* gitIn(folder, ["rev-parse", "--show-toplevel"]);
25366
25687
  if (Option.isNone(root) || root.value === "") return yield* sayFailed("Git couldn’t find this checkout’s root.");
25367
- const session = yield* (yield* SessionToken).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
25688
+ const session = yield* (yield* SessionToken).current.pipe(Effect.catchTag(["KeychainError", "NoSession"], (failed) => sayFailed(failed.nextStep)));
25368
25689
  if (Option.isSome(session) && Option.isSome(named.agent)) return yield* sayFailed("A session’s local run is signed by the session: leave out --agent.");
25369
25690
  if (Option.isNone(session) && Option.isNone(named.agent)) return yield* sayFailed("Outside a session, name the agent this machine is attached as, with --agent owner/handle: a local run is signed by the attachment it ran on.");
25370
25691
  const attachment = Option.isSome(named.agent) ? { attachment: yield* namedAttachment(named.agent.value, Option.none(), true) } : {};
@@ -25462,7 +25783,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
25462
25783
  /** Says a waiting start's tree on stderr, as it stands now. */
25463
25784
  const sayTree = Effect.fnUntraced(function* (wait) {
25464
25785
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25465
- yield* Console.error(printable(waitingLines(wait, now)));
25786
+ yield* Console.error(printable(waitingLines$1(wait, now)));
25466
25787
  });
25467
25788
  /**
25468
25789
  * Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
@@ -25609,7 +25930,7 @@ const run = Command.make("run", {
25609
25930
  value
25610
25931
  }, asJson, rerunCommand);
25611
25932
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25612
- yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25933
+ yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25613
25934
  }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Run a check on main, or on a thread’s head: a run it ran already is answered, not run again, unless the check says reuse: false. A start its check’s start rule makes wait shows what it waits on, and waits up to 9 minutes for it, unless --no-wait. With --local, run a check of the thread’s open version on this machine, and report it"), Command.withShortDescription("Run a check on main or a thread’s head"));
25614
25935
  const trace = Command.make("trace", {
25615
25936
  run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
@@ -25687,7 +26008,7 @@ const waiting$1 = Command.make("waiting", {
25687
26008
  value
25688
26009
  }, asJson, rerunCommand);
25689
26010
  const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
25690
- yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
26011
+ yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
25691
26012
  }).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("A start of yours its check’s start rule made wait: what it waits on, as a tree, waiting up to 9 minutes for it to go on or end, unless --no-wait"), Command.withShortDescription("Show what a waiting start waits on"));
25692
26013
  /** What a cancel did, in words. */
25693
26014
  const cancelledText = {
@@ -25806,19 +26127,21 @@ const checksCommands = [Command.make("checks").pipe(Command.withDescription("A r
25806
26127
  //#endregion
25807
26128
  //#region src/gate-lines.ts
25808
26129
  /**
25809
- * A pull request's gate as `ref0 gate` says it: its current version, whether its needs are
25810
- * answered, the project gate's revision and whether it failed, and each need on its own line
25811
- * with where it came from, ref0's floor or the line of `.ref0/gate.ts`, or of the project gate's,
25812
- * that returned it, with that line's code. A need for one of the project's checks says the
25813
- * project gate revision whose checks run for it, the version's own. Then what each gate read of
25814
- * the change, each read's tree, path and the object it found, short. Then its gate decided again
25815
- * in place under `main`'s `.ref0/`, with `main`'s commit: the needs it held, or those that grew,
25816
- * and why. Then the additions standing on the pull request now, each with the ID `ref0 pr needs
25817
- * remove` takes: the version's own say who added its needs when it was made, and one may have gone
25818
- * since. Then the local runs of its parts' identities, each with its signer; each part that takes
25819
- * local or needs a yes, with its wrappers, the run and the yeses that meet it or what it waits for;
25820
- * and last, each signer revoked from answering one of their checks, an attachment or a person by
25821
- * name, with how an admin restores them.
26130
+ * A pull request's gate as `ref0 gate` says it: its current version, whether its gate returned its
26131
+ * needs, and once it's open, how many still wait and whose one yes would meet them all; the project
26132
+ * gate's revision and whether it failed; and each need on its own line with where it came from,
26133
+ * ref0's floor or the line of `.ref0/gate.ts`, or of the project gate's, that returned it, with
26134
+ * that line's code, and under it whether it's met, by whose yes, or on whom or what it waits; in an
26135
+ * organization, what one yes gives its giver. A need for one of the project's checks says the
26136
+ * project gate revision whose checks run for it, the version's own. Then what each gate read of the
26137
+ * change, each read's tree, path and the object it found, short. Then its gate decided again in
26138
+ * place under `main`'s `.ref0/`, with `main`'s commit: the needs it held, or those that grew, and
26139
+ * why. Then the additions standing on the pull request now, each with the ID `ref0 pr needs remove`
26140
+ * takes: the version's own say who added its needs when it was made, and one may have gone since.
26141
+ * Then the local runs of its parts' identities, each with its signer; each part that takes local or
26142
+ * needs a yes, with its wrappers, the run and the yeses that meet it or what it waits for; and
26143
+ * last, each signer revoked from answering one of their checks, an attachment or a person by name,
26144
+ * with how an admin restores them.
25822
26145
  */
25823
26146
  const fromWords = (from) => {
25824
26147
  switch (from.kind) {
@@ -25849,11 +26172,116 @@ const columns = (lines) => {
25849
26172
  * version was decided under, whose checks run for it, never `main`'s.
25850
26173
  */
25851
26174
  const noteOf = (need, view) => need.kind === "check" && need.source === "project" && view.projectRevision !== void 0 ? ` at gate revision ${view.projectRevision}` : "";
25852
- const stateWords = (view) => {
26175
+ const isPerson = Schema.is(PersonId);
26176
+ /** An owner or a person, by name where it's known. */
26177
+ const nameOf = (id, named) => named.names.get(id) ?? id;
26178
+ /** The admins of an owner, in words: at home, its person alone, by name. */
26179
+ const adminsOf = (owner, named) => isPerson(owner) ? nameOf(owner, named) : `an admin of ${nameOf(owner, named)}`;
26180
+ /** Who answers as a decider, in words: a person by their name, a check by its key. */
26181
+ const whoWords = (decider, named) => {
26182
+ switch (decider.kind) {
26183
+ case "person": return nameOf(decider.id, named);
26184
+ case "role": return adminsOf(decider.owner, named);
26185
+ case "permission": return adminsOf(named.owner, named);
26186
+ case "check": return checkKey(decider);
26187
+ case "agent": return decider.id;
26188
+ case "override": return whoWords(decider.by, named);
26189
+ }
26190
+ };
26191
+ /** Alternatives in words, each once: `pip or an admin of acme`. */
26192
+ const anyOf = (deciders, named) => [...new Set(deciders.map((each) => whoWords(each, named)))].join(" or ");
26193
+ /** A yes that met a need, in words: whose, or the check that passed or was overridden. */
26194
+ const needYesWords = (yes, named) => {
26195
+ if (yes.decider.kind === "check") return `${checkKey(yes.decider)} ${yes.overridden === true ? "overridden" : "passed"}`;
26196
+ if (yes.by === void 0) return `${whoWords(yes.decider, named)}’s yes`;
26197
+ const by = nameOf(yes.by, named);
26198
+ return yes.decider.kind === "person" ? `${by}’s yes` : `${by}’s yes, as an admin`;
26199
+ };
26200
+ /** An override left of a check, in words: whose yes overrides which of its noes. */
26201
+ 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`}`;
26202
+ /**
26203
+ * One decider a need waits on, in words: a check to pass, or its parts when the need wraps it,
26204
+ * or, once ref0 said no to it, every override left.
26205
+ */
26206
+ const waitWords = (wait, need, named) => {
26207
+ if (wait.decider.kind !== "check") return whoWords(wait.decider, named);
26208
+ const check = checkKey(wait.decider);
26209
+ if (wait.overrides === void 0) return need.kind === "check" && (need.local === true || need.passkey !== void 0) ? `${check}’s parts, as below` : `${check} to pass`;
26210
+ if (wait.overrides.length === 0) return `${check}, which ref0 said no to`;
26211
+ return [...new Set(wait.overrides.map((left) => overrideWords(check, left, named)))].join(", and ");
26212
+ };
26213
+ /** The decider keys whose answers meet a need, for telling two needs met by the same yes. */
26214
+ const yesKey = (standing) => {
26215
+ if (standing.state === "met" && "by" in standing && standing.by.every(({ decider }) => decider.kind !== "check")) return standing.by.map(({ decider }) => deciderKey(decider)).join(" ");
26216
+ if (standing.state === "waiting" && "on" in standing && !standing.all) return standing.on.map(({ decider }) => deciderKey(decider)).join(" ");
26217
+ };
26218
+ /** How a need stands, in words. */
26219
+ const standingWords = (standing, need, named) => {
26220
+ switch (standing.state) {
26221
+ case "refused": return `refused: ${nameOf(standing.by, named)} said no, which decides the gate no`;
26222
+ case "met": {
26223
+ 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";
26224
+ const people = /* @__PURE__ */ new Map();
26225
+ for (const yes of standing.by) {
26226
+ const who = yes.by ?? deciderKey(yes.decider);
26227
+ if (!people.has(who)) people.set(who, yes);
26228
+ }
26229
+ const yeses = [...new Set([...people.values()].map((yes) => needYesWords(yes, named)))];
26230
+ return standing.by.some(({ decider }) => decider.kind === "check") ? `met: ${yeses.join(", ")}` : `met by ${yeses.join(" and ")}`;
26231
+ }
26232
+ case "waiting": {
26233
+ if ("for" in standing) return "waiting for ref0 to read which of main’s checks run on this change";
26234
+ if (standing.all) return `waiting for ${standing.on.map((wait) => waitWords(wait, need, named)).join(", and ")}`;
26235
+ const asked = `a yes from ${anyOf(standing.on.map(({ decider }) => decider), named)}`;
26236
+ return need.kind === "check" || need.kind === "checks" ? `waiting on ${asked}: no run can meet it before main moves` : `waiting on ${asked}`;
26237
+ }
26238
+ }
26239
+ };
26240
+ /**
26241
+ * Who one yes would meet a waiting need with, in words, each alone: anyone it asks a yes of, or
26242
+ * the one answerer of every override it waits on; none for a check to pass.
26243
+ */
26244
+ const meetersOf = (standing, named) => {
26245
+ if (standing.state !== "waiting" || !("on" in standing)) return [];
26246
+ if (!standing.all) return standing.on.map(({ decider }) => whoWords(decider, named));
26247
+ const answerers = new Set(standing.on.flatMap(({ overrides }) => overrides === void 0 || overrides.length === 0 ? [""] : overrides.map((left) => whoWords(left.by, named))));
26248
+ return answerers.size === 1 && !answerers.has("") ? [...answerers] : [];
26249
+ };
26250
+ /** An open gate's standings, or none until every need has one. */
26251
+ const standingsOf = (needs) => {
26252
+ const standings = needs.flatMap(({ standing }) => standing === void 0 ? [] : [standing]);
26253
+ return standings.length === 0 || standings.length < needs.length ? void 0 : standings;
26254
+ };
26255
+ /**
26256
+ * Where an open gate's needs stand, after its state: each met; refused, by whom; or how many
26257
+ * still wait, and whose one yes would meet them all, since one yes answers every decider its
26258
+ * person may answer as.
26259
+ */
26260
+ const standsWords = (needs, named) => {
26261
+ const standings = standingsOf(needs);
26262
+ if (standings === void 0) return "";
26263
+ const refused = standings.find((each) => each.state === "refused");
26264
+ if (refused !== void 0 && refused.state === "refused") return `; ${nameOf(refused.by, named)} said no, which decides it no`;
26265
+ const waiting = standings.filter((each) => each.state === "waiting");
26266
+ if (waiting.length === 0) return "; each need is met";
26267
+ const [first, ...rest] = waiting.map((each) => meetersOf(each, named));
26268
+ const common = (first ?? []).filter((who) => rest.every((each) => each.includes(who)));
26269
+ const still = waiting.length === 1 ? "1 still waits" : `${waiting.length} still wait`;
26270
+ if (common.length === 0) return `; ${still}`;
26271
+ return `; ${still}, and one yes from ${[...new Set(common)].join(" or ")} meets ${waiting.length === 1 ? "it" : "them all"}`;
26272
+ };
26273
+ /**
26274
+ * In an organization, what one yes gives, under the needs, while one waits on a yes: every
26275
+ * approval and override its giver may give, so an admin's meets every need an admin may meet.
26276
+ */
26277
+ const oneYesLines = (view, named) => {
26278
+ 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)}`] : [];
26279
+ };
26280
+ const stateWords = (view, named) => {
25853
26281
  switch (view.state) {
25854
- case "pending": return "its gate hasn’t answered yet";
25855
- case "answered": return "answered";
25856
- case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}`;
26282
+ case "pending": return "its gate hasn’t returned its needs yet";
26283
+ case "answered": return `its gate returned its needs${standsWords(view.needs, named)}`;
26284
+ case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}${standsWords(view.needs, named)}`;
25857
26285
  }
25858
26286
  };
25859
26287
  /** The project gate's line, under the head: its revision, and why it failed. */
@@ -25861,7 +26289,7 @@ const projectWords$1 = (view) => {
25861
26289
  const project = view.projectGate;
25862
26290
  if (project === void 0) return [];
25863
26291
  const at = ` project gate, revision ${project.revision}`;
25864
- return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state}`];
26292
+ 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"}`];
25865
26293
  };
25866
26294
  /**
25867
26295
  * What a gate read of the change, under `head`: each read's tree, its path and the object it
@@ -25876,6 +26304,23 @@ const needLines = (needs, view) => columns(needs.map(({ need, from }) => ({
25876
26304
  need: spellNeed(need),
25877
26305
  from: `${from.map(fromWords).join("; ")}${noteOf(need, view)}`
25878
26306
  })));
26307
+ /**
26308
+ * The version's needs, each with how it stands on the line under it, once its gate is open: a
26309
+ * need met by, or waiting on, the same yes as one above says so.
26310
+ */
26311
+ const standingLines = (view, named) => {
26312
+ const lines = needLines(view.needs, view);
26313
+ const seen = /* @__PURE__ */ new Map();
26314
+ return view.needs.flatMap(({ need, standing }, i) => {
26315
+ const line = lines[i] ?? "";
26316
+ if (standing === void 0) return [line];
26317
+ const key = yesKey(standing);
26318
+ const same = key === void 0 ? void 0 : seen.get(key);
26319
+ if (key !== void 0 && same === void 0) seen.set(key, need);
26320
+ const also = same === void 0 ? "" : `, the same yes as ${spellNeed(same)}`;
26321
+ return [line, ` ${standingWords(standing, need, named)}${also}`];
26322
+ });
26323
+ };
25879
26324
  const grewWords = (grew) => {
25880
26325
  const why = grew.why ?? "ref0 couldn’t say why";
25881
26326
  switch (grew.kind) {
@@ -25887,7 +26332,7 @@ const grewWords = (grew) => {
25887
26332
  };
25888
26333
  const inPlaceLines = (inPlace, view) => {
25889
26334
  const head = `under main’s .ref0/, at ${inPlace.main.slice(0, 7)}:`;
25890
- if (inPlace.state === "pending") return [`${head} its gate there hasn’t answered yet`];
26335
+ if (inPlace.state === "pending") return [`${head} its gate there hasn’t returned its needs yet`];
25891
26336
  const reads = readLines("its gate there read:", inPlace.reads ?? []);
25892
26337
  if (inPlace.state === "held" || inPlace.grew === void 0) return [
25893
26338
  `${head} ${inPlace.state}`,
@@ -25961,7 +26406,11 @@ const wrappedPartLines = (parts) => parts.length === 0 ? [] : ["parts that take
25961
26406
  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}`];
25962
26407
  const revokedLines = (revoked) => revoked.length === 0 ? [] : ["signers revoked:", ...revoked.flatMap(revokedSignerLines)];
25963
26408
  const gateLines = (view, repo) => {
25964
- const head = `${repo}#${view.pullRequest}, version ${view.version}: ${stateWords(view)}`;
26409
+ const named = {
26410
+ names: new Map((view.names ?? []).map(({ owner, name }) => [owner, name])),
26411
+ owner: view.owner
26412
+ };
26413
+ const head = `${repo}#${view.pullRequest}, version ${view.version}: ${stateWords(view, named)}`;
25965
26414
  const inPlace = view.inPlace === void 0 ? [] : inPlaceLines(view.inPlace, view);
25966
26415
  const added = view.added.map(({ need, from }) => ({
25967
26416
  need: spellNeed(need),
@@ -25973,7 +26422,8 @@ const gateLines = (view, repo) => {
25973
26422
  return [
25974
26423
  head,
25975
26424
  ...projectWords$1(view),
25976
- ...needLines(view.needs, view),
26425
+ ...standingLines(view, named),
26426
+ ...oneYesLines(view, named),
25977
26427
  ...readLines("its gate read:", view.reads),
25978
26428
  ...readLines("its project gate read:", view.projectGate?.reads ?? []),
25979
26429
  ...inPlace,
@@ -26028,7 +26478,7 @@ const gateCommands = [Command.make("gate", {
26028
26478
  }
26029
26479
  });
26030
26480
  yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
26031
- }).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"))];
26481
+ }).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("A pull request’s needs, each with where it came from, ref0’s floor or a line of .ref0/gate.ts, and whether it’s met or on whom it waits; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
26032
26482
  //#endregion
26033
26483
  //#region src/clone.ts
26034
26484
  /** The remote a clone pushes to: its thread's fork path, named as the thread is. */
@@ -26143,6 +26593,44 @@ const answerGit = Effect.fnUntraced(function* (operation, input) {
26143
26593
  return Option.map(token, credentialLines);
26144
26594
  });
26145
26595
  //#endregion
26596
+ //#region src/own-session.ts
26597
+ /**
26598
+ * A command only an agent's session runs, on a repo of the session's project. Its endpoints take
26599
+ * the repo's name alone and find it in that project, so the command takes the name alone too: an
26600
+ * owner it couldn't check could name another project's repo, and the session's of that name
26601
+ * would be changed in its place.
26602
+ */
26603
+ const decodeRepoName$1 = Schema.decodeUnknownOption(RepoName);
26604
+ /**
26605
+ * The token of the agent's session a command runs in. Outside one it says `outside`, what the
26606
+ * command is for; in a harness session whose ref0 session never started, why and what to do.
26607
+ * Either way it fails before anything's sent.
26608
+ */
26609
+ const ownSession = Effect.fnUntraced(function* (outside) {
26610
+ const token = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", (none) => sayFailed(none.nextStep)));
26611
+ if (Option.isNone(token)) return yield* sayFailed(outside);
26612
+ return token.value;
26613
+ });
26614
+ /** The repo of a session's command, by its name alone. */
26615
+ const sessionRepoFlag = Flag.String("repo").pipe(Flag.withDescription("A repo of your session’s project, by its name alone, as shop; if not named, the one this checkout is of"), Flag.optional);
26616
+ /**
26617
+ * The session a command runs as, and the name of the repo it acts on: the one named, or this
26618
+ * checkout's. Outside a session it says `outside`, what the command is for, and fails before
26619
+ * anything's sent.
26620
+ */
26621
+ const sessionRepo = Effect.fnUntraced(function* (named, outside) {
26622
+ yield* ownSession(outside);
26623
+ const checkout = yield* Checkout;
26624
+ const repo = Option.isSome(named) ? decodeRepoName$1(named.value) : yield* checkout.repoAt(".");
26625
+ if (Option.isNone(repo)) return yield* sayFailed(Option.isSome(named) ? "Name a repo of your session’s project by its name alone, as --repo shop." : "Name the repo with --repo and its name, as --repo shop, or run this in its checkout.");
26626
+ const session = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
26627
+ if (session === void 0) return yield* sayFailed("This token isn’t a session’s.");
26628
+ return {
26629
+ session,
26630
+ repo: repo.value
26631
+ };
26632
+ });
26633
+ //#endregion
26146
26634
  //#region src/sync-history.ts
26147
26635
  const decodePlace = Schema.decodeUnknownEffect(Schema.Struct({
26148
26636
  owner: OwnerRef,
@@ -26198,7 +26686,7 @@ const trimShallow = Effect.fnUntraced(function* (gitDir) {
26198
26686
  */
26199
26687
  const loadSyncHistory = Effect.fnUntraced(function* (gitDir, repo) {
26200
26688
  const config = yield* Ref0Config;
26201
- const token = yield* (yield* SessionToken).current;
26689
+ const token = yield* (yield* SessionToken).current.pipe(Effect.catchTag("NoSession", () => Effect.succeedNone));
26202
26690
  if (Option.isNone(token)) return yield* new HistoryNotLoaded({ why: "this terminal has no session to load it as" });
26203
26691
  const fs = yield* FileSystem.FileSystem;
26204
26692
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -26371,9 +26859,8 @@ const gitCommands = [
26371
26859
  json
26372
26860
  }, ({ repo: written, directory, json: asJson }) => Effect.gen(function* () {
26373
26861
  const { owner, repo: name } = repoParts(written);
26374
- const session = yield* SessionToken;
26375
26862
  const config = yield* Ref0Config;
26376
- if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 clone runs in an agent’s session, which pushes to a fork of its own. A person’s clone, by approval, isn’t built yet.");
26863
+ yield* ownSession("ref0 clone runs only in an agent’s ref0 session, which pushes to a fork of its own, and none runs here. In Claude Code or Codex, one starts at each prompt once ref0 connect has connected its config folder.");
26377
26864
  const ours = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
26378
26865
  if (ours === void 0) return yield* sayFailed("This token isn’t a session’s.");
26379
26866
  const fork = yield* callEndpoint({
@@ -26405,8 +26892,7 @@ const gitCommands = [
26405
26892
  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.`);
26406
26893
  }).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
26407
26894
  Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
26408
- const session = yield* SessionToken;
26409
- if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
26895
+ yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
26410
26896
  const synced = yield* syncBase(".");
26411
26897
  if (synced.kind === "refused") return yield* sayFailed(synced.why);
26412
26898
  const { on, commit } = synced.base;
@@ -27442,18 +27928,28 @@ const repoCommands = [Command.make("repos", {
27442
27928
  //#endregion
27443
27929
  //#region src/decision-lines.ts
27444
27930
  /** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
27445
- /** What a decision is about, in words. */
27446
- const subjectWords = (subject) => {
27447
- switch (subject.kind) {
27448
- case "thread": return subject.thread;
27449
- case "version": return `version ${subject.version} of pull request ${subject.pullRequest} in ${subject.repo}, from ${subject.thread}`;
27450
- }
27451
- };
27931
+ /** What a decision is about, in words: its thread, and a gate's version of its pull request. */
27932
+ const subjectWords = ({ subject, names }) => subject.kind === "version" && names.pullRequest !== void 0 ? `version ${subject.version} of ${names.pullRequest}, from ${names.thread}` : names.thread;
27452
27933
  /** A decision in a line. */
27453
- const decisionLines = (view) => {
27454
- const on = subjectWords(view.subject);
27455
- const answers = view.options.map((option) => option.key).join(" or ");
27456
- return view.state === "open" ? `${view.decision} on ${on}: ${answers}` : `${view.decision} on ${on}: ${view.result ?? "no answer"}, ${view.closedWhy ?? "closed"}`;
27934
+ const decisionLines = (decision) => {
27935
+ const on = subjectWords(decision);
27936
+ const answers = decision.options.map((option) => option.key).join(" or ");
27937
+ return decision.state === "open" ? `${decision.decision} on ${on}: ${answers}` : `${decision.decision} on ${on}: ${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`;
27938
+ };
27939
+ const nothing = "Nothing waits on you here.";
27940
+ /**
27941
+ * What waits, as `ref0 waiting` prints it: what waits on the caller, then, for an agent, what
27942
+ * waits on its person, which they answer on their own page.
27943
+ */
27944
+ const waitingLines = (decisions) => {
27945
+ const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision)] : []);
27946
+ const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision)}`] : []);
27947
+ if (theirs.length === 0) return yours.length === 0 ? nothing : yours.join("\n");
27948
+ return [
27949
+ ...yours,
27950
+ "Waits on your person, under Waiting on you on their page:",
27951
+ ...theirs
27952
+ ].join("\n");
27457
27953
  };
27458
27954
  //#endregion
27459
27955
  //#region src/commands/plans.ts
@@ -27647,9 +28143,7 @@ const planCommands = [
27647
28143
  endpoint: "decisions.waiting",
27648
28144
  params: scopeParams(address)
27649
28145
  }));
27650
- const { decisions } = answer.value;
27651
- const words = decisions.length === 0 ? "Nothing waits on you here." : decisions.map((each) => decisionLines(each)).join("\n");
27652
- yield* printAnswer(answer.json, asJson, words);
28146
+ yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
27653
28147
  }).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")),
27654
28148
  Command.make("decision", {
27655
28149
  project: project$2,
@@ -27690,35 +28184,6 @@ const planCommands = [
27690
28184
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
27691
28185
  ];
27692
28186
  //#endregion
27693
- //#region src/own-session.ts
27694
- /**
27695
- * A command only an agent's session runs, on a repo of the session's project. Its endpoints take
27696
- * the repo's name alone and find it in that project, so the command takes the name alone too: an
27697
- * owner it couldn't check could name another project's repo, and the session's of that name
27698
- * would be changed in its place.
27699
- */
27700
- const decodeRepoName$1 = Schema.decodeUnknownOption(RepoName);
27701
- /** The repo of a session's command, by its name alone. */
27702
- const sessionRepoFlag = Flag.String("repo").pipe(Flag.withDescription("A repo of your session’s project, by its name alone, as shop; if not named, the one this checkout is of"), Flag.optional);
27703
- /**
27704
- * The session a command runs as, and the name of the repo it acts on: the one named, or this
27705
- * checkout's. Outside a session it says `outside`, what the command is for, and fails before
27706
- * anything's sent.
27707
- */
27708
- const sessionRepo = Effect.fnUntraced(function* (named, outside) {
27709
- const token = yield* SessionToken;
27710
- if (Option.isNone(yield* token.current)) return yield* sayFailed(outside);
27711
- const checkout = yield* Checkout;
27712
- const repo = Option.isSome(named) ? decodeRepoName$1(named.value) : yield* checkout.repoAt(".");
27713
- if (Option.isNone(repo)) return yield* sayFailed(Option.isSome(named) ? "Name a repo of your session’s project by its name alone, as --repo shop." : "Name the repo with --repo and its name, as --repo shop, or run this in its checkout.");
27714
- const session = (yield* callEndpoint({ endpoint: "me.get" })).value.session?.session;
27715
- if (session === void 0) return yield* sayFailed("This token isn’t a session’s.");
27716
- return {
27717
- session,
27718
- repo: repo.value
27719
- };
27720
- });
27721
- //#endregion
27722
28187
  //#region src/pull-request-lines.ts
27723
28188
  /**
27724
28189
  * Pull requests as `ref0 pr` says them: each one's number, thread, state and current version,
@@ -28179,8 +28644,7 @@ const taskCommands = [Command.make("tasks", {
28179
28644
  file: Flag.FileText("file").pipe(Flag.withDescription("The whole list, as JSON: each task’s text, up to 200 characters, and its state, \"to do\", \"in progress\" or \"done\"; at most 50")),
28180
28645
  json
28181
28646
  }, ({ file, json: asJson }) => Effect.gen(function* () {
28182
- const session = yield* SessionToken;
28183
- if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 tasks runs in an agent’s session, which sets its own list.");
28647
+ yield* ownSession("ref0 tasks runs in an agent’s session, which sets its own list.");
28184
28648
  const parsed = yield* Effect.result(decodeJsonText(file));
28185
28649
  if (Result.isFailure(parsed)) return yield* sayFailed("That list isn’t JSON.");
28186
28650
  const checked = yield* Effect.result(checkTasks(parsed.success));
@@ -28783,9 +29247,8 @@ const waitCommands = [Command.make("wait", {
28783
29247
  blockers: Flag.Boolean("blockers").pipe(Flag.withDescription("Wait until nothing blocks your session’s thread from merging"), Flag.withDefault(false)),
28784
29248
  json
28785
29249
  }, ({ blockers, json: asJson }) => Effect.gen(function* () {
28786
- const session = yield* SessionToken;
28787
29250
  if (!blockers) return yield* sayFailed("Say what to wait for: ref0 wait --blockers waits until nothing blocks your thread.");
28788
- if (Option.isNone(yield* session.current)) return yield* sayFailed("ref0 wait --blockers runs in an agent’s session: it waits on its own thread.");
29251
+ yield* ownSession("ref0 wait --blockers runs in an agent’s session: it waits on its own thread.");
28789
29252
  yield* waitOnBlockers(asJson);
28790
29253
  }).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(`Wait until nothing blocks your session’s thread from merging, for 9 minutes at most`), Command.withShortDescription("Wait for your session’s thread to be ready to merge"))];
28791
29254
  //#endregion
@@ -32545,6 +33008,8 @@ function readHook(args) {
32545
33008
  */
32546
33009
  /** The keychain account a binding's thread is kept under, beside its session token's. */
32547
33010
  const threadAccount = (binding) => `${sessionAccount(binding)}/thread`;
33011
+ /** The keychain account a start not yet landed is kept under, beside its session token's. */
33012
+ const pendingAccount = (binding) => `${sessionAccount(binding)}/pending`;
32548
33013
  const decodeRequest = Schema.decodeUnknownEffect(StartSessionRequest);
32549
33014
  const decodeRequestId = Schema.decodeUnknownOption(RequestId);
32550
33015
  /**
@@ -32584,14 +33049,14 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
32584
33049
  const folder = yield* (yield* ConfigFolders).folder(harness);
32585
33050
  const grant = yield* hostGrant.get(folder);
32586
33051
  if (Option.isNone(grant) || grant.value.harness !== harness) return Option.none();
32587
- const pendingAccount = `${sessionAccount(input.binding)}/pending`;
33052
+ const pending = pendingAccount(input.binding);
32588
33053
  const held = Option.flatMap(yield* keychain.get(sessionAccount(input.binding)), (kept) => parseToken(Redacted.value(kept)));
32589
- const pending = Option.filter(Option.flatMap(yield* keychain.get(pendingAccount), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
32590
- const { requestId, secret } = Option.isSome(pending) ? pending.value : {
33054
+ const resent = Option.filter(Option.flatMap(yield* keychain.get(pending), (kept) => decodePending(Redacted.value(kept))), (one) => !Option.exists(held, (parts) => parts.secret === one.secret));
33055
+ const { requestId, secret } = Option.isSome(resent) ? resent.value : {
32591
33056
  requestId: yield* newRequestId,
32592
33057
  secret: encodeSecret(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))
32593
33058
  };
32594
- if (Option.isNone(pending)) yield* keychain.set(pendingAccount, Redacted.make(`${requestId} ${secret}`));
33059
+ if (Option.isNone(resent)) yield* keychain.set(pending, Redacted.make(`${requestId} ${secret}`));
32595
33060
  const hash = yield* hashOf(secret);
32596
33061
  const where = input.on.kind === "thread" ? threadParts(input.on.written) : {
32597
33062
  owner: grant.value.owner,
@@ -32621,7 +33086,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
32621
33086
  const started = yield* (yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(grant.value.token)))).sessions.start({
32622
33087
  headers: { "idempotency-key": requestId },
32623
33088
  payload
32624
- }).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(pendingAccount)), Match.orElse(() => Effect.void))));
33089
+ }).pipe(Effect.tapError((error) => Match.value(error).pipe(Match.tag("ProjectNotFound", "ProjectNotReady", "ThreadNotFound", "ThreadIsClosed", "TextRefused", "NotPermitted", "DeliveryOutsideWindow", () => keychain.remove(pending)), Match.orElse(() => Effect.void))));
32625
33090
  const thread = input.on.kind === "thread" ? input.on.written : writtenThread(grant.value.owner, {
32626
33091
  kind: "project",
32627
33092
  name: newThreadProject
@@ -32629,7 +33094,7 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
32629
33094
  yield* keychain.remove(gitTokenAccount(input.binding)).pipe(Effect.ignore);
32630
33095
  yield* keychain.set(sessionAccount(input.binding), Redacted.make(formatToken(started.credential.person, started.credential.id, secret)));
32631
33096
  yield* keychain.set(threadAccount(input.binding), Redacted.make(thread)).pipe(Effect.ignore);
32632
- yield* keychain.remove(pendingAccount).pipe(Effect.ignore);
33097
+ yield* keychain.remove(pending).pipe(Effect.ignore);
32633
33098
  return Option.some(started);
32634
33099
  });
32635
33100
  //#endregion
@@ -32691,6 +33156,11 @@ const ownerOf = (bound) => ({
32691
33156
  const decodeWrittenThread = Schema.decodeUnknownOption(WrittenThread);
32692
33157
  /** How long a harness session with no token is left before the keychain is asked again. */
32693
33158
  const unboundMillis = 3e4;
33159
+ /**
33160
+ * How long a prompt's start may run on past its hook's deadline before it's given up, and a later
33161
+ * prompt sends it again, as itself: well past the slowest start seen on staging, 4.5 seconds.
33162
+ */
33163
+ const startBound = "20 seconds";
32694
33164
  /** How often `ref0 mcp` renews its session's 30-minute lease while its harness process lives. */
32695
33165
  const renewEvery = "10 minutes";
32696
33166
  /** How soon a renewal that failed is tried again, well inside the lease. */
@@ -32706,6 +33176,7 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
32706
33176
  const hints = yield* LiveHints;
32707
33177
  const context = yield* Effect.context();
32708
33178
  const owned = yield* Ref.make(Option.none());
33179
+ const onTheWay = yield* Ref.make(Option.none());
32709
33180
  const listen = Effect.fnUntraced(function* (bound) {
32710
33181
  const client = yield* clientOf(bound.token);
32711
33182
  let pause = 1;
@@ -32801,17 +33272,31 @@ const sessionBinding = Effect.fnUntraced(function* (edges) {
32801
33272
  if (Option.isNone(started)) return Option.none();
32802
33273
  return yield* keychain.get(sessionAccount(binding)).pipe(Effect.orElseSucceed(() => Option.none()));
32803
33274
  });
33275
+ const startedFor = Effect.fnUntraced(function* (binding, hook, starting) {
33276
+ const sent = yield* Ref.get(onTheWay);
33277
+ if (Option.isSome(sent) && sent.value.harnessSession === binding.harnessSession) return yield* Deferred.await(sent.value.done);
33278
+ if (!starting) return Option.none();
33279
+ const done = yield* Deferred.make();
33280
+ const mine = Option.some({
33281
+ harnessSession: binding.harnessSession,
33282
+ done
33283
+ });
33284
+ yield* Ref.set(onTheWay, mine);
33285
+ yield* Effect.forkIn(startAtPrompt(binding, hook).pipe(Effect.timeoutOption(startBound), Effect.map(Option.flatten), Effect.orElseSucceed(() => Option.none()), Effect.ensuring(Ref.update(onTheWay, (now) => now === mine ? Option.none() : now)), Effect.onExit((exit) => Deferred.done(done, exit))), scope);
33286
+ return yield* Deferred.await(done);
33287
+ });
32804
33288
  const bind = Effect.fnUntraced(function* (hook) {
32805
33289
  const now = yield* Clock.currentTimeMillis;
32806
33290
  const current = yield* Ref.get(state);
32807
33291
  if (current.kind === "bound" && current.bound.binding.harnessSession === hook.harnessSession) return Option.some(current.bound);
32808
33292
  const starting = hook.event === "UserPromptSubmit" && Option.isSome(agent);
33293
+ const sent = Option.exists(yield* Ref.get(onTheWay), (one) => one.harnessSession === hook.harnessSession);
32809
33294
  const over = current.kind === "ended" && current.harnessSession === hook.harnessSession;
32810
33295
  const waiting = current.kind === "unbound" && now < current.until && !starting;
32811
- if (over && !starting || waiting) return Option.none();
33296
+ if ((over && !starting || waiting) && !sent) return Option.none();
32812
33297
  const binding = yield* bindingForServer(hook.harnessSession);
32813
33298
  const held = Option.isNone(binding) ? Option.none() : yield* heldFor(binding.value, over);
32814
- const token = Option.isSome(held) || Option.isNone(binding) || !starting ? held : yield* startAtPrompt(binding.value, hook);
33299
+ const token = Option.isSome(held) || Option.isNone(binding) ? held : yield* startedFor(binding.value, hook, starting);
32815
33300
  if (Option.isNone(binding) || Option.isNone(token)) {
32816
33301
  if (!over) yield* Ref.set(state, {
32817
33302
  kind: "unbound",
@@ -33101,7 +33586,7 @@ const toRead = (hook, ownHook) => {
33101
33586
  const sessionTurns = Effect.fnUntraced(function* (edges) {
33102
33587
  const transcripts = yield* Transcripts;
33103
33588
  const turn = yield* Ref.make(Option.none());
33104
- const trackTurn = Effect.fnUntraced(function* (client, bound, hook, deadline) {
33589
+ const trackTurn = Effect.fnUntraced(function* (client, bound, hook, deadline, late = false) {
33105
33590
  const checkout = yield* Checkout;
33106
33591
  const session = bound.session;
33107
33592
  const owner = ownerOf(bound);
@@ -33115,6 +33600,10 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33115
33600
  const prompted = isNew ? hook.transcript : void 0;
33116
33601
  if (prompted !== void 0) yield* transcripts.begin(owner, harness, prompted, harnessTurn);
33117
33602
  const ending = prompted === void 0 ? void 0 : yield* transcripts.read(owner, harness, [prompted], { until: harnessTurn });
33603
+ const fresh = late && harness === "claude-code" ? {
33604
+ ...nothingSeen,
33605
+ tokens: readFailed(nothingSeen.tokens)
33606
+ } : nothingSeen;
33118
33607
  const tracked = isNew ? Option.some({
33119
33608
  kind: "starting",
33120
33609
  harnessTurn,
@@ -33125,7 +33614,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33125
33614
  tokens: addRead(now.seen.tokens, ending, true)
33126
33615
  }
33127
33616
  }) : now.previous),
33128
- seen: nothingSeen
33617
+ seen: fresh
33129
33618
  }) : before;
33130
33619
  if (Option.isNone(tracked)) return;
33131
33620
  const ownHook = hook.harnessTurn === tracked.value.harnessTurn;
@@ -33201,6 +33690,7 @@ const sessionTurns = Effect.fnUntraced(function* (edges) {
33201
33690
  return {
33202
33691
  now: Ref.get(turn),
33203
33692
  track: trackTurn,
33693
+ trackLate: (client, bound, prompt, deadline) => trackTurn(client, bound, prompt, deadline, true),
33204
33694
  fresh: Ref.set(turn, Option.none())
33205
33695
  };
33206
33696
  });
@@ -33337,6 +33827,7 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
33337
33827
  const scope = yield* Scope.Scope;
33338
33828
  const tasks = yield* sessionTasks;
33339
33829
  const turns = yield* sessionTurns({ tasksLanded: tasks.landed });
33830
+ const prompted = yield* Ref.make(Option.none());
33340
33831
  const edits = yield* sessionEdits({ turn: turns.now });
33341
33832
  const delivery = yield* sessionDelivery;
33342
33833
  const wake = yield* sessionWake({
@@ -33394,9 +33885,13 @@ var HarnessSession = class HarnessSession extends Context.Service()("ref0/cli/Ha
33394
33885
  lastLook
33395
33886
  });
33396
33887
  const deliver = Effect.fnUntraced(function* (hook, deadline) {
33888
+ if (hook.event === "UserPromptSubmit") yield* Ref.set(prompted, Option.some(hook));
33397
33889
  const bound = yield* binding.bind(hook);
33398
33890
  if (Option.isNone(bound)) return Option.none();
33399
33891
  const client = yield* clientOf(bound.value.token);
33892
+ const prompt = Option.filter(yield* Ref.get(prompted), (one) => one.harnessSession === hook.harnessSession && one.harnessTurn === hook.harnessTurn);
33893
+ if (Option.isSome(prompt)) yield* Ref.set(prompted, Option.none());
33894
+ if (Option.isSome(prompt) && prompt.value !== hook) yield* turns.trackLate(client, bound.value, prompt.value, deadline);
33400
33895
  const unattributed = yield* edits.noteEdits(bound.value, hook);
33401
33896
  const within = yield* binding.within;
33402
33897
  if (yield* tasks.note(bound.value, hook)) yield* Effect.forkIn(tasks.send(client, bound.value).pipe(Effect.ignore), within);
@@ -33512,6 +34007,18 @@ const mcpServer = (version, started) => McpServer.toolkit(HarnessTools).pipe(Lay
33512
34007
  protocols
33513
34008
  })));
33514
34009
  /**
34010
+ * `ref0 mcp` in a checkout of another origin, which the launcher names: the plugin's server all
34011
+ * the same, answering every hook with nothing, since that origin's own plugin serves the
34012
+ * checkout. A harness takes a server that closes before `initialize` as broken, and Claude Code
34013
+ * then skips it for 15 minutes, in every folder. It reads nothing of this machine's, so it
34014
+ * answers at once.
34015
+ */
34016
+ const otherOriginServer = (version) => McpServer.toolkit(HarnessTools).pipe(Layer.provide(HarnessTools.toLayer({ hook: () => Effect.succeed({}) })), Layer.provide(McpServer.layerStdio({
34017
+ name: "ref0",
34018
+ version,
34019
+ protocols
34020
+ })));
34021
+ /**
33515
34022
  * The `codex` that started this server, by absolute path: its executable, which Linux names, or
33516
34023
  * its command's first word when that's a path. Codex isn't sure to be on `PATH` (M0).
33517
34024
  */
@@ -33542,11 +34049,18 @@ const codexSetupHere = Effect.fnUntraced(function* (process) {
33542
34049
  });
33543
34050
  });
33544
34051
  const mcp = Command.make(harnessCommandNames.mcp, {}, () => Effect.gen(function* () {
34052
+ const checked = yield* Config.schema(ConnectHarness, serverCheckVariable).pipe(Config.option);
34053
+ const other = yield* Config.option(Config.String(otherOriginVariable));
34054
+ if (Option.isSome(other) && Option.isNone(checked)) {
34055
+ yield* printBlocks([[`ref0: this folder is for ${other.value}, so ref0’s plugin for ${(yield* Ref0Config).api} does nothing in it.`]]);
34056
+ return yield* Layer.launch(otherOriginServer(cliVersion));
34057
+ }
33545
34058
  const agents = yield* ConfigFolders;
33546
- const found = yield* (yield* HarnessProcess).innermost;
33547
- const agent = Option.isSome(found) ? yield* (yield* HostGrant).get(yield* agents.folder(found.value.harness)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone)) : Option.none();
34059
+ const found = Option.isSome(checked) ? Option.none() : yield* (yield* HarnessProcess).innermost;
34060
+ const harness = Option.orElse(checked, () => Option.map(found, (each) => each.harness));
34061
+ const agent = Option.isSome(harness) ? yield* (yield* HostGrant).get(yield* agents.folder(harness.value)).pipe(Effect.catchTag("KeychainError", (failed) => failed.unreadable === true ? Effect.succeedNone : sayFailed(failed.nextStep))) : Option.none();
33548
34062
  const codex = Option.isSome(found) && found.value.harness === "codex" ? yield* codexSetupHere(found.value.process) : Option.none();
33549
- const reinstall = Option.isSome(found) && Option.isSome(agent) ? installLine(yield* agents.folder(found.value.harness)) : "ref0 connect";
34063
+ const reinstall = Option.isSome(harness) && Option.isSome(agent) ? installLine(yield* agents.folder(harness.value)) : "ref0 connect";
33550
34064
  return yield* Layer.launch(mcpServer(cliVersion, {
33551
34065
  plugin: {
33552
34066
  version: yield* pluginVersion,
@@ -33750,21 +34264,44 @@ const checkoutAt = Effect.fnUntraced(function* (cwd) {
33750
34264
  };
33751
34265
  });
33752
34266
  /**
33753
- * Adds the agent's commit environment to the file Claude Code names, for the session its
33754
- * `SessionStart` opens: only in Claude Code, and only for a setup with an agent. ref0's hooks
33755
- * folder is written first, so the file never names a folder that isn't there. Whether it set
33756
- * ref0's hooks path.
34267
+ * The harness whose `SessionStart` runs this: Claude Code, which alone gives its hook the file it
34268
+ * runs ahead of the agent's commands, and never its agent's commands; else Codex, when the
34269
+ * innermost harness process above it is Codex's, as the session's binding reads it. Codex's hook
34270
+ * input says nothing Claude Code's doesn't, and Codex sets no `CODEX_HOME` for the default
34271
+ * `~/.codex`. Neither otherwise.
34272
+ */
34273
+ const startingHarness = Effect.gen(function* () {
34274
+ const file = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
34275
+ if (file !== "") return Option.some({
34276
+ harness: "claude-code",
34277
+ file
34278
+ });
34279
+ const found = yield* (yield* HarnessProcess).innermost;
34280
+ return Option.isSome(found) && found.value.harness === "codex" ? Option.some({ harness: "codex" }) : Option.none();
34281
+ });
34282
+ /**
34283
+ * What the agent reads when its commits couldn't be set up at its session's start, in one line,
34284
+ * whatever failed: the session still starts. Its commits may then name the person, skip ref0's
34285
+ * hooks, or meet a sign-off's refusal once.
34286
+ */
34287
+ const commitSetupFailedWords = (error) => {
34288
+ return `ref0: ref0 couldn't set up Git for your commands, so your commits may not name you as their author or run ref0's hooks: ${("nextStep" in error ? error.nextStep : "detail" in error ? error.detail : error.message).replaceAll(/\s+/g, " ").trim()}`;
34289
+ };
34290
+ /**
34291
+ * Adds the agent's commit environment for the session a `SessionStart` opens, in Claude Code to
34292
+ * the file it names and in Codex to its checkout's section, only for a setup with an agent. ref0's
34293
+ * hooks folder is written first, so nothing names a folder that isn't there. The harness whose
34294
+ * hooks path it set, if it set one.
33757
34295
  */
33758
34296
  const setCommitEnvironment = Effect.fnUntraced(function* (session) {
33759
34297
  const fs = yield* FileSystem.FileSystem;
33760
34298
  const folders = yield* ConfigFolders;
33761
- const file = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
33762
- if (file === "") {
33763
- if ((yield* Config.String("CODEX_HOME").pipe(Config.withDefault(""))) === "") return false;
33764
- return yield* setCodexCommitEnvironment(session.cwd ?? ".", yield* checkoutAt(session.cwd ?? "."));
33765
- }
34299
+ const starting = yield* startingHarness;
34300
+ if (Option.isNone(starting)) return Option.none();
34301
+ const hook = starting.value;
34302
+ if (hook.harness === "codex") return (yield* setCodexCommitEnvironment(session.cwd ?? ".", yield* checkoutAt(session.cwd ?? "."))) ? Option.some("codex") : Option.none();
33766
34303
  const connected = yield* (yield* HostGrant).get(yield* folders.folder("claude-code")).pipe(Effect.map(Option.map(Option.some)), Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedSome(Option.none())));
33767
- if (Option.isNone(connected)) return false;
34304
+ if (Option.isNone(connected)) return Option.none();
33768
34305
  const checkout = yield* checkoutAt(session.cwd ?? ".");
33769
34306
  const folder = yield* hooksFolder(yield* gitHooksHome(yield* Config.String("HOME"), (yield* Ref0Config).api), checkout.folder);
33770
34307
  const author = checkout.signsOff ? Option.none() : Option.map(connected.value, (grant) => ({
@@ -33778,8 +34315,8 @@ const setCommitEnvironment = Effect.fnUntraced(function* (session) {
33778
34315
  transcript: Option.filter(Option.fromNullishOr(session.transcript_path), (path) => path !== ""),
33779
34316
  model: startingModel(session.model)
33780
34317
  });
33781
- yield* fs.writeFileString(file, script, { flag: "a" });
33782
- return true;
34318
+ yield* fs.writeFileString(hook.file, script, { flag: "a" });
34319
+ return Option.some("claude-code");
33783
34320
  });
33784
34321
  //#endregion
33785
34322
  //#region src/harness/session-start.ts
@@ -33831,10 +34368,8 @@ const startOnHook = (protocol) => Effect.gen(function* () {
33831
34368
  const input = yield* stdio.stdin.pipe(Stream.decodeText(), Stream.mkString);
33832
34369
  const hook = decodeHook(input);
33833
34370
  if (Option.isNone(hook)) return;
33834
- if (yield* setCommitEnvironment(hook.value).pipe(Effect.orElseSucceed(() => false))) {
33835
- const claude = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
33836
- yield* Console.log(claude === "" ? codexHookInstallerWords : hookInstallerWords);
33837
- }
34371
+ const hooksSet = yield* setCommitEnvironment(hook.value).pipe(Effect.catch((error) => Console.log(commitSetupFailedWords(error)).pipe(Effect.as(Option.none()))));
34372
+ if (Option.isSome(hooksSet)) yield* Console.log(hooksSet.value === "codex" ? codexHookInstallerWords : hookInstallerWords);
33838
34373
  const binding = yield* bindingHere(hook.value.session_id);
33839
34374
  if (Option.isNone(binding)) return;
33840
34375
  const harnessName = binding.value.harness === "codex" ? "codex" : "claude-code";
@@ -33888,6 +34423,205 @@ const sessionStart = Command.make(harnessCommandNames.sessionStart, { protocol:
33888
34423
  /** The commands a harness runs: `ref0 mcp`, and its command hooks. */
33889
34424
  const harnessCommands = [mcp, Command.make(harnessCommandNames.hook).pipe(Command.withDescription("The harness's hooks that run as commands"), Command.withShortDescription("Run the harness’s session hooks"), Command.withSubcommands([sessionStart, sessionEnd]))];
33890
34425
  //#endregion
34426
+ //#region src/harness/command-token.ts
34427
+ /**
34428
+ * The session an agent's command acts as: its harness session's, by the token kept under its
34429
+ * binding. Outside a harness there's none, and a command is its person's, asked by approval; so
34430
+ * too in a harness whose config folder has no agent connected. Where one is connected, a harness
34431
+ * session with no token has lost its start, and the command says why, as far as ref0 can tell,
34432
+ * and what to do, rather than run as its person.
34433
+ */
34434
+ /**
34435
+ * Whether ref0's server, `ref0 mcp`, runs right under a harness process, as the harness starts
34436
+ * it: none when the process table can't be read.
34437
+ */
34438
+ var HarnessServers = class HarnessServers extends Context.Service()("ref0/cli/HarnessServers") {
34439
+ static layer = Layer.effect(HarnessServers, Effect.gen(function* () {
34440
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
34441
+ const runsUnder = (process) => spawner.string(ChildProcess.make("ps", [
34442
+ "-A",
34443
+ "-o",
34444
+ "ppid=,args="
34445
+ ])).pipe(Effect.map((table) => table.trim() === "" ? Option.none() : Option.some(table.split("\n").some((line) => {
34446
+ const [parent = "", ...words] = line.trim().split(/\s+/);
34447
+ return Number(parent) === process && words.at(-1) === harnessCommandNames.mcp && words.some((word) => word.includes("ref0"));
34448
+ }))), Effect.orElseSucceed(() => Option.none()));
34449
+ return HarnessServers.of({ runsUnder });
34450
+ }));
34451
+ };
34452
+ /**
34453
+ * What an agent's command in a harness session with no ref0 session says to do: run again, while
34454
+ * the prompt's start is on its way; reconnect or restart, when the server isn't running; else
34455
+ * send the prompt again, which starts one.
34456
+ */
34457
+ const noSessionStep = (harness, server, seen) => {
34458
+ const name = configFolders[harness].name;
34459
+ const restart = harness === "codex" ? "restart Codex" : `run /mcp, choose ${server} and Reconnect, or restart Claude Code`;
34460
+ if (Option.contains(seen.serverRuns, false)) return `This ${name} session has no ref0 session: ref0’s server isn’t running in it, so none started at the last prompt. Tell your person to ${restart}, then send the prompt again.`;
34461
+ if (seen.pending) return `This ${name} session has no ref0 session yet: its start at the last prompt hasn’t landed. Run this again in a few seconds. If it says this again, tell your person to send the prompt again.`;
34462
+ return `This ${name} session has no ref0 session: none started at the last prompt. Tell your person to send the prompt again. If it says this again, tell them to ${restart}.`;
34463
+ };
34464
+ /** `SessionToken` from the keychain, by the binding of the harness session this runs in. */
34465
+ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
34466
+ const keychain = yield* Keychain;
34467
+ const harness = yield* HarnessProcess;
34468
+ const servers = yield* HarnessServers;
34469
+ const hostGrant = yield* HostGrant;
34470
+ const folders = yield* ConfigFolders;
34471
+ const config = yield* Ref0Config;
34472
+ const plugin = yield* originPluginName(config.api);
34473
+ const current = Effect.gen(function* () {
34474
+ const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
34475
+ if (Option.isNone(binding)) return Option.none();
34476
+ const kept = yield* keychain.get(sessionAccount(binding.value));
34477
+ if (Option.isSome(kept)) return kept;
34478
+ const name = binding.value.harness === "codex" ? "codex" : "claude-code";
34479
+ const grant = yield* hostGrant.get(yield* folders.folder(name)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
34480
+ if (Option.isNone(grant) || grant.value.harness !== name) return Option.none();
34481
+ const pending = yield* keychain.get(pendingAccount(binding.value));
34482
+ const serverRuns = yield* servers.runsUnder(binding.value.process);
34483
+ return yield* new NoSession({ nextStep: noSessionStep(name, `plugin:${plugin}:${plugin}`, {
34484
+ serverRuns,
34485
+ pending: Option.isSome(pending)
34486
+ }) });
34487
+ });
34488
+ return SessionToken.of({ current });
34489
+ }));
34490
+ //#endregion
34491
+ //#region src/harness/server-check.ts
34492
+ /**
34493
+ * Whether a config folder's ref0 server starts, asked as its harness asks: the command its
34494
+ * installed plugin's `.mcp.json` names, started from an empty folder of ref0's own with the
34495
+ * environment that harness would give it from this terminal's, and sent `initialize`. Told by
34496
+ * `serverCheckVariable` which harness it's started as, the server reads that folder's grant from
34497
+ * the keychain as it does at a session's start, and starts no session. Its whole process group is
34498
+ * stopped once it answers, stops, or the limit passes, so nothing it started outlasts the check.
34499
+ */
34500
+ /**
34501
+ * The variables Codex 0.164.0 passes a stdio server of its own, beside those its entry's
34502
+ * `env_vars` names that are set. Nothing else reaches the server (measured on Linux, 24 Oct 2026).
34503
+ */
34504
+ const codexPasses = [
34505
+ "HOME",
34506
+ "LOGNAME",
34507
+ "PATH",
34508
+ "SHELL",
34509
+ "USER",
34510
+ "LANG",
34511
+ "LC_ALL",
34512
+ "TERM",
34513
+ "TMPDIR",
34514
+ "TZ",
34515
+ "__CF_USER_TEXT_ENCODING",
34516
+ "SSL_CERT_FILE",
34517
+ "NODE_EXTRA_CA_CERTS"
34518
+ ];
34519
+ /**
34520
+ * How long a server has to answer `initialize`: ten times its slowest start measured on Linux,
34521
+ * 0.5 s (24 Oct 2026), so status stays quick. A harness may wait longer (Codex 0.164.0 waited 14 s
34522
+ * for a slow one), so a server that doesn't answer in it is unknown, not one that can't start.
34523
+ */
34524
+ const serverLimit = Duration.seconds(5);
34525
+ const checkId = "ref0-status";
34526
+ /**
34527
+ * `initialize` and its answer, as JSON-RPC carries them, in MCP's own schemas: those `ref0 mcp`
34528
+ * serves through Effect's MCP server.
34529
+ */
34530
+ const InitializeRequest = Schema.Struct({
34531
+ jsonrpc: Schema.Literal("2.0"),
34532
+ id: Schema.Literal(checkId),
34533
+ method: Schema.Literal(McpSchema.Initialize._tag),
34534
+ params: McpSchema.Initialize.payloadSchema
34535
+ });
34536
+ const InitializeAnswer = Schema.Struct({
34537
+ id: Schema.Literal(checkId),
34538
+ result: McpSchema.InitializeResult
34539
+ });
34540
+ /** `initialize`, at a version every ref0 server speaks, as a harness's first line. */
34541
+ const initialize = `${Schema.encodeSync(Schema.fromJsonString(InitializeRequest))({
34542
+ jsonrpc: "2.0",
34543
+ id: checkId,
34544
+ method: McpSchema.Initialize._tag,
34545
+ params: {
34546
+ protocolVersion: "2025-06-18",
34547
+ capabilities: new McpSchema.ClientCapabilities({}),
34548
+ clientInfo: {
34549
+ name: "ref0 status",
34550
+ version: cliVersion
34551
+ }
34552
+ }
34553
+ })}\n`;
34554
+ const decodeServers = Schema.decodeUnknownEffect(Schema.fromJsonString(McpServersFile));
34555
+ const decodeAnswer = Schema.decodeUnknownOption(Schema.fromJsonString(InitializeAnswer));
34556
+ /**
34557
+ * `names` as this terminal sets them, which a harness started here would pass on. A relative
34558
+ * `PATH` folder is read from here, as `onPath` reads it, since the server starts elsewhere.
34559
+ */
34560
+ const fromHere = Effect.fnUntraced(function* (names) {
34561
+ const paths = yield* Path.Path;
34562
+ const set = [];
34563
+ for (const name of new Set(names)) {
34564
+ const value = yield* Config.String(name).pipe(Config.option);
34565
+ if (Option.isNone(value)) continue;
34566
+ set.push([name, name === "PATH" ? value.value.split(":").map((folder) => folder === "" || paths.isAbsolute(folder) ? folder : `${process.cwd()}/${folder}`).join(":") : value.value]);
34567
+ }
34568
+ return Object.fromEntries(set);
34569
+ });
34570
+ /** The first line a stopped server said, made printable. */
34571
+ const firstLine = (said) => printable(said.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "");
34572
+ /**
34573
+ * Starts the server named `name` in the plugin installed at `plugin`, as `harness` would for
34574
+ * `configFolder`, and answers how its start went.
34575
+ */
34576
+ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin, name) {
34577
+ const fs = yield* FileSystem.FileSystem;
34578
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
34579
+ const server = yield* fs.readFileString(`${plugin}/.mcp.json`).pipe(Effect.flatMap(decodeServers), Effect.map((file) => Option.fromNullishOr(file.mcpServers[name])), Effect.orElseSucceed(Option.none));
34580
+ if (Option.isNone(server)) return { kind: "unnamed" };
34581
+ const asked = server.value.env_vars ?? [];
34582
+ const checked = {
34583
+ REF0_API: (yield* Ref0Config).api,
34584
+ [serverCheckVariable]: harness
34585
+ };
34586
+ const env = harness === "codex" ? {
34587
+ ...yield* fromHere([...codexPasses, ...asked]),
34588
+ CODEX_HOME: configFolder,
34589
+ ...checked
34590
+ } : {
34591
+ ...yield* fromHere([
34592
+ ...codexPasses,
34593
+ ...keychainVariables,
34594
+ ...asked
34595
+ ]),
34596
+ CLAUDE_CONFIG_DIR: configFolder,
34597
+ ...checked
34598
+ };
34599
+ const withheld = Object.keys(yield* fromHere(keychainVariables)).some((variable) => env[variable] === void 0);
34600
+ return yield* Effect.scoped(Effect.gen(function* () {
34601
+ const handle = yield* spawner.spawn(ChildProcess.make(server.value.command, server.value.args, {
34602
+ cwd: yield* emptyFolder(fs),
34603
+ env,
34604
+ extendEnv: harness === "claude-code",
34605
+ forceKillAfter: "1 second",
34606
+ stdin: {
34607
+ stream: Stream.make(initialize).pipe(Stream.encodeText),
34608
+ endOnDone: false
34609
+ }
34610
+ }));
34611
+ const said = yield* handle.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkScoped);
34612
+ const lines = yield* handle.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.takeUntil((line) => Option.isSome(decodeAnswer(line))), Stream.runCollect);
34613
+ if (lines.some((line) => Option.isSome(decodeAnswer(line)))) return { kind: "answered" };
34614
+ if (withheld) return { kind: "withoutKeychain" };
34615
+ return {
34616
+ kind: "stopped",
34617
+ said: firstLine(yield* Fiber.join(said)) || firstLine(lines.join("\n"))
34618
+ };
34619
+ })).pipe(Effect.catchTag("PlatformError", (error) => Effect.succeed({
34620
+ kind: "stopped",
34621
+ said: firstLine(error.message)
34622
+ })), Effect.timeoutOption(serverLimit), Effect.map(Option.getOrElse(() => ({ kind: "unanswered" }))));
34623
+ });
34624
+ //#endregion
33891
34625
  //#region src/harness/status.ts
33892
34626
  /** Each connected or initialized folder has its own report; secrets are used only to ask ref0. */
33893
34627
  const encodeReports = Schema.encodeEffect(Schema.Array(StatusReport));
@@ -33943,6 +34677,16 @@ const speaksCheck = (protocols, version, reinstall) => {
33943
34677
  if (Option.isSome(mismatch)) return checkOf("plugin", "no", mismatch.value.says, mismatch.value.run);
33944
34678
  return checkOf("plugin", "yes", "installed and speaks this ref0");
33945
34679
  };
34680
+ /** A server's start, as its line says it, with this folder's reinstall for one that can't start. */
34681
+ const serverCheckOf = (started, configFolder) => {
34682
+ switch (started.kind) {
34683
+ case "answered": return checkOf("server", "yes", "starts");
34684
+ case "stopped": return checkOf("server", "no", started.said === "" ? "can’t start, and said nothing" : `can’t start: ${started.said}`, installLine(configFolder));
34685
+ case "withoutKeychain": return checkOf("server", "no", "can’t reach the keychain, since an older ref0 installed it", installLine(configFolder));
34686
+ case "unnamed": return checkOf("server", "no", "the plugin names no ref0 server", installLine(configFolder));
34687
+ case "unanswered": return checkOf("server", "unknown", `didn’t answer in ${Duration.toSeconds(serverLimit)} s`, "ref0 status");
34688
+ }
34689
+ };
33946
34690
  const decodePlugins = Schema.decodeUnknownEffect(Schema.fromJsonString(ClaudePluginsListed));
33947
34691
  const decodeHooks = Schema.decodeUnknownEffect(Schema.fromJsonString(HooksFile));
33948
34692
  /** Ask Claude Code about this folder, even when another folder is current in the terminal. */
@@ -33976,7 +34720,7 @@ const claudeCodeChecks = Effect.fnUntraced(function* (configFolder) {
33976
34720
  return [speaksCheck(Option.match(hooks, {
33977
34721
  onNone: () => [],
33978
34722
  onSome: protocolsOf
33979
- }), Option.fromUndefinedOr(plugin.version), reinstall)];
34723
+ }), Option.fromUndefinedOr(plugin.version), reinstall), folder === void 0 ? checkOf("server", "unknown", "Claude Code didn’t say where ref0’s plugin is", reinstall) : serverCheckOf(yield* startServer("claude-code", configFolder, folder, name), configFolder)];
33980
34724
  });
33981
34725
  /** Trust only ref0's hooks; removal of an unexpected plugin targets this folder too. */
33982
34726
  const trustCheck = (hooks, configFolder, name) => {
@@ -33996,7 +34740,13 @@ const codexChecks = Effect.fnUntraced(function* (configFolder) {
33996
34740
  const hooks = listed.value.data.flatMap((entry) => entry.hooks).filter((hook) => pluginHook(hook, `${name}@${name}`));
33997
34741
  if (hooks.length === 0) return [checkOf("plugin", "no", "not installed or turned off in this folder", reinstall)];
33998
34742
  const protocols = hooks.map((hook) => protocolFlagged(hook.command ?? "")).filter(Predicate.isString);
33999
- return [speaksCheck(protocols, Option.none(), reinstall), trustCheck(hooks, configFolder, name)];
34743
+ const path = yield* Path.Path;
34744
+ const source = hooks.find((hook) => Predicate.isString(hook.sourcePath))?.sourcePath;
34745
+ return [
34746
+ speaksCheck(protocols, Option.none(), reinstall),
34747
+ trustCheck(hooks, configFolder, name),
34748
+ Predicate.isString(source) ? serverCheckOf(yield* startServer("codex", configFolder, path.dirname(path.dirname(source)), name), configFolder) : checkOf("server", "unknown", "Codex didn’t say where ref0’s plugin is", reinstall)
34749
+ ];
34000
34750
  });
34001
34751
  /** A session belongs only to the current folder, even when several folders share its agent. */
34002
34752
  const sessionCheck = Effect.fnUntraced(function* (harness, configFolder) {
@@ -34025,8 +34775,7 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
34025
34775
  }
34026
34776
  for (const { configFolder, harness } of found) candidates.set(configFolder, harness);
34027
34777
  const paths = [...candidates.keys()].sort((a, b) => a === current ? -1 : b === current ? 1 : 0);
34028
- const reports = [];
34029
- for (const configFolder of paths) {
34778
+ const reportOf = Effect.fnUntraced(function* (configFolder) {
34030
34779
  const kept = yield* Effect.result(grants.get(configFolder));
34031
34780
  const unreadable = Result.isFailure(kept) && kept.failure.unreadable === true;
34032
34781
  const grant = Result.isSuccess(kept) ? kept.success : Option.none();
@@ -34036,15 +34785,19 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
34036
34785
  configFolder,
34037
34786
  checks: [Result.isFailure(kept) && !unreadable ? checkOf("connected", "unknown", "The keychain didn’t say whether this config folder holds a host grant.", kept.failure.nextStep) : checkOf("connected", "no", unreadable ? "its host grant can’t be read" : "not connected", harness === void 0 ? "ref0 connect" : yield* connectLineFor(harness, configFolder))]
34038
34787
  };
34039
- reports.push(harness === void 0 ? report : {
34788
+ return harness === void 0 ? report : {
34040
34789
  ...report,
34041
34790
  harness
34042
- });
34043
- continue;
34791
+ };
34044
34792
  }
34045
- const checks = [...yield* grantChecks(grant.value, configFolder), ...yield* grant.value.harness === "codex" ? codexChecks(configFolder) : claudeCodeChecks(configFolder)];
34046
- if (configFolder === current) checks.push(yield* sessionCheck(grant.value.harness, configFolder));
34047
- reports.push({
34793
+ const [granted, installed] = yield* Effect.all([grantChecks(grant.value, configFolder), grant.value.harness === "codex" ? codexChecks(configFolder) : claudeCodeChecks(configFolder)], { concurrency: "unbounded" });
34794
+ const checks = [...granted, ...installed];
34795
+ if (configFolder === current) {
34796
+ const session = yield* sessionCheck(grant.value.harness, configFolder);
34797
+ const server = checks.find((check) => check.check === "server");
34798
+ checks.push(server !== void 0 && server.answer !== "yes" && session.answer === "no" ? checkOf("session", "no", `none, since ref0’s server ${server.answer === "no" ? "can’t start" : "didn’t answer"}`, server.next) : session);
34799
+ }
34800
+ return {
34048
34801
  harness: grant.value.harness,
34049
34802
  configFolder,
34050
34803
  agent: {
@@ -34052,8 +34805,9 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
34052
34805
  agent: grant.value.agent
34053
34806
  },
34054
34807
  checks
34055
- });
34056
- }
34808
+ };
34809
+ });
34810
+ const reports = yield* Effect.forEach(paths, reportOf, { concurrency: "unbounded" });
34057
34811
  const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
34058
34812
  yield* printAnswer(yield* Effect.orDie(encodeReports(reports)), asJson, reports.length === 0 ? [["No initialized harness folders found.", { command: "ref0 connect" }]] : reports.map((report) => wordsOf(report, home)));
34059
34813
  if (reports.some((report) => report.checks.some((check) => check.answer === "no"))) return yield* new Said();
@@ -34098,7 +34852,12 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
34098
34852
  ]));
34099
34853
  const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
34100
34854
  const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
34101
- const sessionToken = harnessSessionToken.pipe(Layer.provide([keychain, harnessProcess]));
34855
+ const sessionToken = harnessSessionToken.pipe(Layer.provide([
34856
+ keychain,
34857
+ harnessProcess,
34858
+ HarnessServers.layer.pipe(Layer.provide(NodeServices.layer)),
34859
+ HostGrant.layer.pipe(Layer.provide(keychain))
34860
+ ]));
34102
34861
  /** Every service `ref0`'s commands use, on this machine. */
34103
34862
  const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
34104
34863
  Ref0Config.layer,