@ref0/cli 0.10.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.
- package/dist/main.mjs +1100 -669
- package/package.json +2 -2
package/dist/main.mjs
CHANGED
|
@@ -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,589 +6770,227 @@ 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/
|
|
6773
|
+
//#region ../contracts/src/plan-events.ts
|
|
6771
6774
|
/**
|
|
6772
|
-
*
|
|
6773
|
-
*
|
|
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.
|
|
6775
|
+
* A plan's events, in its thread's project's log, written by the Project DO. A version's
|
|
6776
|
+
* state is folded from these and its thread's, never recorded on its own.
|
|
6776
6777
|
*/
|
|
6777
6778
|
/**
|
|
6778
|
-
*
|
|
6779
|
-
*
|
|
6779
|
+
* A new version, as a draft. Its content never changes after this. Its reason, when given, says
|
|
6780
|
+
* why it was written; it's beside the plan, never in it, so it's no part of the content hash.
|
|
6780
6781
|
*/
|
|
6781
|
-
const
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
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
|
-
});
|
|
6782
|
+
const PlanWritten = eventKind("plan.written", Schema.Struct({
|
|
6783
|
+
thread: ThreadId,
|
|
6784
|
+
version: PlanVersion,
|
|
6785
|
+
plan: PlanRecord,
|
|
6786
|
+
contentHash: PlanContentHash,
|
|
6787
|
+
reason: Schema.optionalKey(TextId)
|
|
6788
|
+
}));
|
|
6802
6789
|
/**
|
|
6803
|
-
*
|
|
6804
|
-
*
|
|
6805
|
-
*
|
|
6790
|
+
* The version is put to approval: each repo it names is asked to answer for it, and ref0 says yes
|
|
6791
|
+
* once they have or their time runs out. Any earlier draft or proposed version is
|
|
6792
|
+
* superseded.
|
|
6806
6793
|
*/
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
|
|
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)
|
|
6794
|
+
const PlanProposed = eventKind("plan.proposed", Schema.Struct({
|
|
6795
|
+
thread: ThreadId,
|
|
6796
|
+
version: PlanVersion
|
|
6797
|
+
}));
|
|
6798
|
+
/** Who said yes: ref0's plans part, once every repo answered or its time ran out. */
|
|
6799
|
+
const PlanAcceptedBy = Schema.Struct({
|
|
6800
|
+
kind: Schema.Literal("answered"),
|
|
6801
|
+
part: Ref0Part
|
|
6821
6802
|
});
|
|
6822
|
-
/**
|
|
6823
|
-
|
|
6824
|
-
|
|
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)
|
|
6803
|
+
/** A child thread, by the plan entry that made it. */
|
|
6804
|
+
const PlanChildThread = Schema.Struct({
|
|
6805
|
+
entry: PlanEntryKey,
|
|
6806
|
+
thread: ThreadId
|
|
6879
6807
|
});
|
|
6880
|
-
|
|
6881
|
-
const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
|
|
6808
|
+
const ChildThreads = Schema.Array(PlanChildThread).check(Schema.isMaxLength(50));
|
|
6882
6809
|
/**
|
|
6883
|
-
*
|
|
6884
|
-
*
|
|
6885
|
-
* the check run its statement is over.
|
|
6810
|
+
* The version is accepted, and the one accepted before it superseded. Each child is named only
|
|
6811
|
+
* once its owner has registered it.
|
|
6886
6812
|
*/
|
|
6887
|
-
const
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
version:
|
|
6896
|
-
|
|
6897
|
-
|
|
6813
|
+
const PlanAccepted = eventKind("plan.accepted", Schema.Struct({
|
|
6814
|
+
thread: ThreadId,
|
|
6815
|
+
version: PlanVersion,
|
|
6816
|
+
by: PlanAcceptedBy,
|
|
6817
|
+
/** The version accepted before this one. */
|
|
6818
|
+
supersedes: Schema.optionalKey(PlanVersion),
|
|
6819
|
+
/** Children made now, each starting in planning. */
|
|
6820
|
+
started: ChildThreads,
|
|
6821
|
+
/** Entries an earlier version dropped and this one adds back: the same child, reopened if it was closed. */
|
|
6822
|
+
rejoined: ChildThreads,
|
|
6823
|
+
/** Children no longer part of the plan, so it stops waiting on them. Closing one is its own event. */
|
|
6824
|
+
dropped: ChildThreads
|
|
6825
|
+
}));
|
|
6826
|
+
//#endregion
|
|
6827
|
+
//#region ../contracts/src/plan-calls.ts
|
|
6898
6828
|
/**
|
|
6899
|
-
*
|
|
6900
|
-
*
|
|
6901
|
-
*
|
|
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.
|
|
6829
|
+
* Plans and decisions: what the Worker asks a thread's Project DO, once `Authorize` has said
|
|
6830
|
+
* who's acting. A plan is written with its words; the Project DO stores each as a text and
|
|
6831
|
+
* records the version with their IDs.
|
|
6904
6832
|
*/
|
|
6905
|
-
|
|
6906
|
-
|
|
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
|
-
});
|
|
6833
|
+
/** A plan's free text: one of its outcomes, or an answer's reason. */
|
|
6834
|
+
const PlanText = Schema.String.check(Schema.isBetweenLength(1, 2048));
|
|
6915
6835
|
/**
|
|
6916
|
-
*
|
|
6917
|
-
*
|
|
6918
|
-
* again without its code.
|
|
6836
|
+
* How a plan does its work, in system terms, for people: about a page. A revision replaces it,
|
|
6837
|
+
* with a reason; no one appends to it.
|
|
6919
6838
|
*/
|
|
6920
|
-
const
|
|
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}$/)) })]);
|
|
6839
|
+
const Approach = Schema.String.check(Schema.isBetweenLength(1, 6e3));
|
|
6927
6840
|
/**
|
|
6928
|
-
*
|
|
6929
|
-
*
|
|
6930
|
-
* request needs its answerer, and the agent it chooses to connect, before a yes has options.
|
|
6841
|
+
* A child as written. On a top-level org thread's plan, `project` names one of its owner's
|
|
6842
|
+
* projects to start it in, as that project's thread; left out, it's in its parent's scope.
|
|
6931
6843
|
*/
|
|
6932
|
-
const
|
|
6933
|
-
|
|
6934
|
-
|
|
6844
|
+
const PlanChildInput = Schema.Struct({
|
|
6845
|
+
entry: PlanEntryKey,
|
|
6846
|
+
title: Title,
|
|
6847
|
+
outcome: Outcome,
|
|
6848
|
+
stackedOn: Schema.optionalKey(PlanEntryKey),
|
|
6849
|
+
required: Schema.Boolean,
|
|
6850
|
+
project: Schema.optionalKey(ProjectName)
|
|
6935
6851
|
});
|
|
6936
6852
|
/**
|
|
6937
|
-
*
|
|
6938
|
-
*
|
|
6939
|
-
* it without asking; no, and stop agents asking, with the passkey too.
|
|
6853
|
+
* A plan as written: `PlanRecord`'s shape, with its words in place of text IDs. Its repos are up
|
|
6854
|
+
* to 20 of the project's, by name; a parent whose work is all in its children names none.
|
|
6940
6855
|
*/
|
|
6941
|
-
const
|
|
6942
|
-
Schema.
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
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
|
-
]);
|
|
6856
|
+
const PlanInput = Schema.Struct({
|
|
6857
|
+
approach: Schema.optionalKey(Approach),
|
|
6858
|
+
repos: Schema.Array(RepoName).check(Schema.isMaxLength(20)),
|
|
6859
|
+
outcomes: Schema.Array(PlanText).check(Schema.isBetweenLength(1, 50)),
|
|
6860
|
+
children: Schema.Array(PlanChildInput).check(Schema.isMaxLength(50))
|
|
6861
|
+
}).check(Schema.makeFilter((plan) => planProblem(plan) ?? true));
|
|
6963
6862
|
/**
|
|
6964
|
-
*
|
|
6965
|
-
*
|
|
6966
|
-
*
|
|
6967
|
-
*
|
|
6863
|
+
* A project a plan's child names, as the Worker found it in the plan's owner before it asked the
|
|
6864
|
+
* scope: its ID, and whether the writer can see it; or, with no `project`, that the owner has
|
|
6865
|
+
* none by that name. The Worker refuses nothing for it, since only the scope knows which entries
|
|
6866
|
+
* a version keeps.
|
|
6968
6867
|
*/
|
|
6969
|
-
const
|
|
6970
|
-
|
|
6971
|
-
|
|
6868
|
+
const NamedProject = Schema.Struct({
|
|
6869
|
+
name: ProjectName,
|
|
6870
|
+
project: Schema.optionalKey(ProjectId),
|
|
6871
|
+
seen: Schema.Boolean
|
|
6972
6872
|
});
|
|
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
6873
|
Schema.Struct({
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
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)
|
|
6874
|
+
...commandFields,
|
|
6875
|
+
project: ProjectId,
|
|
6876
|
+
ownerName: OwnerName,
|
|
6877
|
+
number: ThreadNumber,
|
|
6878
|
+
plan: PlanInput,
|
|
6879
|
+
projects: Schema.Array(NamedProject).check(Schema.isMaxLength(50)),
|
|
6880
|
+
reason: Schema.optionalKey(Reason),
|
|
6881
|
+
propose: Schema.Boolean
|
|
7037
6882
|
});
|
|
7038
6883
|
Schema.Struct({
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
6884
|
+
...commandFields,
|
|
6885
|
+
ownerName: OwnerName,
|
|
6886
|
+
project: ProjectId,
|
|
6887
|
+
number: ThreadNumber,
|
|
6888
|
+
version: PlanVersion
|
|
6889
|
+
});
|
|
6890
|
+
/** A decision as it stands: what it asks, who decides, the answers so far, and how it closed. */
|
|
6891
|
+
const DecisionView = Schema.Struct({
|
|
6892
|
+
decision: DecisionId,
|
|
6893
|
+
kind: DecisionKind,
|
|
6894
|
+
subject: DecisionSubject,
|
|
6895
|
+
question: Schema.optionalKey(TextId),
|
|
6896
|
+
options: Schema.Array(AnswerOption),
|
|
6897
|
+
lean: Schema.optionalKey(AnswerKey),
|
|
6898
|
+
deciders: Schema.Array(Decider),
|
|
6899
|
+
deadline: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
6900
|
+
state: Schema.Literals(["open", "closed"]),
|
|
6901
|
+
answers: Schema.Array(Schema.Struct({
|
|
6902
|
+
decider: DeciderRef,
|
|
6903
|
+
answer: AnswerKey,
|
|
6904
|
+
reason: Schema.optionalKey(TextId),
|
|
6905
|
+
at: Schema.DateTimeUtcFromMillis
|
|
6906
|
+
})),
|
|
6907
|
+
result: Schema.optionalKey(AnswerKey),
|
|
6908
|
+
closedWhy: Schema.optionalKey(DecisionClosedWhy)
|
|
7043
6909
|
});
|
|
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
6910
|
/**
|
|
7077
|
-
*
|
|
7078
|
-
*
|
|
7079
|
-
*
|
|
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.
|
|
7080
6915
|
*/
|
|
7081
|
-
const
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
result: Schema.String
|
|
6916
|
+
const DecisionNames = Schema.Struct({
|
|
6917
|
+
thread: WrittenThread,
|
|
6918
|
+
pullRequest: Schema.optionalKey(WrittenPullRequest)
|
|
7085
6919
|
});
|
|
7086
|
-
|
|
7087
|
-
Schema.Struct({
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
Schema.Struct({
|
|
7091
|
-
person: PersonId,
|
|
7092
|
-
approval: ApprovalId
|
|
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
|
|
7093
6924
|
});
|
|
7094
|
-
//#endregion
|
|
7095
|
-
//#region ../contracts/src/browser-sessions.ts
|
|
7096
6925
|
/**
|
|
7097
|
-
* A
|
|
7098
|
-
*
|
|
7099
|
-
* only the token's SHA-256.
|
|
6926
|
+
* A version as it stands: its content, its state, and, once proposed, where its approval is: its
|
|
6927
|
+
* repos still being checked, or who accepted it.
|
|
7100
6928
|
*/
|
|
7101
|
-
const
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
6929
|
+
const PlanVersionView = Schema.Struct({
|
|
6930
|
+
thread: ThreadId,
|
|
6931
|
+
number: ThreadNumber,
|
|
6932
|
+
version: PlanVersion,
|
|
6933
|
+
state: PlanState,
|
|
6934
|
+
contentHash: PlanContentHash,
|
|
6935
|
+
plan: PlanView,
|
|
6936
|
+
/** Why it was written, when its writer said: never part of its content. */
|
|
6937
|
+
reason: Schema.optionalKey(TextId),
|
|
6938
|
+
writtenBy: ActorRef,
|
|
6939
|
+
writtenAt: Schema.DateTimeUtcFromMillis,
|
|
6940
|
+
/** Proposed, and its repos haven't all answered yet: it isn't accepted yet. */
|
|
6941
|
+
checking: Schema.Boolean,
|
|
6942
|
+
/**
|
|
6943
|
+
* What its check found when it ended, every repo answered or their time run out: the repos
|
|
6944
|
+
* that never answered. None for a draft, while it's being checked, or when a newer version or
|
|
6945
|
+
* the thread closing ended the check first.
|
|
6946
|
+
*/
|
|
6947
|
+
check: Schema.optionalKey(PlanCheck),
|
|
6948
|
+
/**
|
|
6949
|
+
* Accepted, and waiting to be recorded: on its new `children` registering, or on a merge under
|
|
6950
|
+
* the thread's current revision `publishing`.
|
|
6951
|
+
*/
|
|
6952
|
+
accepting: Schema.optionalKey(Schema.Literals(["children", "publishing"])),
|
|
6953
|
+
acceptedBy: Schema.optionalKey(PlanAcceptedBy)
|
|
7106
6954
|
});
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
})
|
|
6955
|
+
/** A plan's free text refused before anything was stored, saying which. */
|
|
6956
|
+
var PlanTextRefused = class extends Schema.TaggedError()("PlanTextRefused", {
|
|
6957
|
+
/** Where it is in the plan, such as `outcomes[3]`. */
|
|
6958
|
+
where: Schema.String,
|
|
6959
|
+
problem: Schema.Literals([
|
|
6960
|
+
"empty",
|
|
6961
|
+
"too long",
|
|
6962
|
+
"looks like a secret"
|
|
6963
|
+
]),
|
|
6964
|
+
nextStep: Schema.String
|
|
6965
|
+
}, { httpApiStatus: 422 }) {};
|
|
6966
|
+
/** A plan names a repo this project doesn't hold. */
|
|
6967
|
+
var PlanRepoNotFound = class extends Schema.TaggedError()("PlanRepoNotFound", {
|
|
6968
|
+
repo: RepoName,
|
|
6969
|
+
nextStep: Schema.String
|
|
6970
|
+
}, { httpApiStatus: 422 }) {};
|
|
7123
6971
|
/**
|
|
7124
|
-
* A
|
|
7125
|
-
*
|
|
6972
|
+
* A plan names its scope's own repo, made just now, before the scope holds it. Nothing was kept
|
|
6973
|
+
* under the request's delivery ID, so the same request is written once it does.
|
|
7126
6974
|
*/
|
|
7127
|
-
|
|
6975
|
+
var OwnRepoNotReady = class extends Schema.TaggedError()("OwnRepoNotReady", {
|
|
6976
|
+
repo: RepoName,
|
|
6977
|
+
nextStep: Schema.String
|
|
6978
|
+
}, { httpApiStatus: 503 }) {};
|
|
7128
6979
|
/**
|
|
7129
|
-
*
|
|
7130
|
-
*
|
|
6980
|
+
* A child entry this plan can't have: a project named on a project thread's plan or a nested org
|
|
6981
|
+
* thread's, a project that isn't there or the writer can't see, one where its proposer may not
|
|
6982
|
+
* start threads, an entry moved to another project, or a stack across scopes. A project that
|
|
6983
|
+
* isn't there and one the writer can't see are refused alike, as `ProjectNotFound` is.
|
|
7131
6984
|
*/
|
|
7132
|
-
|
|
7133
|
-
Schema.
|
|
6985
|
+
var PlanChildRefused = class extends Schema.TaggedError()("PlanChildRefused", { nextStep: Schema.String }, { httpApiStatus: 422 }) {};
|
|
6986
|
+
var PlanVersionNotFound = class extends Schema.TaggedError()("PlanVersionNotFound", {
|
|
6987
|
+
number: ThreadNumber,
|
|
6988
|
+
version: PlanVersion,
|
|
6989
|
+
nextStep: Schema.String
|
|
6990
|
+
}, { httpApiStatus: 404 }) {};
|
|
7134
6991
|
/**
|
|
7135
|
-
*
|
|
7136
|
-
*
|
|
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
|
-
//#region ../contracts/src/plan-events.ts
|
|
7148
|
-
/**
|
|
7149
|
-
* A plan's events, in its thread's project's log, written by the Project DO. A version's
|
|
7150
|
-
* state is folded from these and its thread's, never recorded on its own.
|
|
7151
|
-
*/
|
|
7152
|
-
/**
|
|
7153
|
-
* A new version, as a draft. Its content never changes after this. Its reason, when given, says
|
|
7154
|
-
* why it was written; it's beside the plan, never in it, so it's no part of the content hash.
|
|
7155
|
-
*/
|
|
7156
|
-
const PlanWritten = eventKind("plan.written", Schema.Struct({
|
|
7157
|
-
thread: ThreadId,
|
|
7158
|
-
version: PlanVersion,
|
|
7159
|
-
plan: PlanRecord,
|
|
7160
|
-
contentHash: PlanContentHash,
|
|
7161
|
-
reason: Schema.optionalKey(TextId)
|
|
7162
|
-
}));
|
|
7163
|
-
/**
|
|
7164
|
-
* The version is put to approval: each repo it names is asked to answer for it, and ref0 says yes
|
|
7165
|
-
* once they have or their time runs out. Any earlier draft or proposed version is
|
|
7166
|
-
* superseded.
|
|
7167
|
-
*/
|
|
7168
|
-
const PlanProposed = eventKind("plan.proposed", Schema.Struct({
|
|
7169
|
-
thread: ThreadId,
|
|
7170
|
-
version: PlanVersion
|
|
7171
|
-
}));
|
|
7172
|
-
/** Who said yes: ref0's plans part, once every repo answered or its time ran out. */
|
|
7173
|
-
const PlanAcceptedBy = Schema.Struct({
|
|
7174
|
-
kind: Schema.Literal("answered"),
|
|
7175
|
-
part: Ref0Part
|
|
7176
|
-
});
|
|
7177
|
-
/** A child thread, by the plan entry that made it. */
|
|
7178
|
-
const PlanChildThread = Schema.Struct({
|
|
7179
|
-
entry: PlanEntryKey,
|
|
7180
|
-
thread: ThreadId
|
|
7181
|
-
});
|
|
7182
|
-
const ChildThreads = Schema.Array(PlanChildThread).check(Schema.isMaxLength(50));
|
|
7183
|
-
/**
|
|
7184
|
-
* The version is accepted, and the one accepted before it superseded. Each child is named only
|
|
7185
|
-
* once its owner has registered it.
|
|
7186
|
-
*/
|
|
7187
|
-
const PlanAccepted = eventKind("plan.accepted", Schema.Struct({
|
|
7188
|
-
thread: ThreadId,
|
|
7189
|
-
version: PlanVersion,
|
|
7190
|
-
by: PlanAcceptedBy,
|
|
7191
|
-
/** The version accepted before this one. */
|
|
7192
|
-
supersedes: Schema.optionalKey(PlanVersion),
|
|
7193
|
-
/** Children made now, each starting in planning. */
|
|
7194
|
-
started: ChildThreads,
|
|
7195
|
-
/** Entries an earlier version dropped and this one adds back: the same child, reopened if it was closed. */
|
|
7196
|
-
rejoined: ChildThreads,
|
|
7197
|
-
/** Children no longer part of the plan, so it stops waiting on them. Closing one is its own event. */
|
|
7198
|
-
dropped: ChildThreads
|
|
7199
|
-
}));
|
|
7200
|
-
//#endregion
|
|
7201
|
-
//#region ../contracts/src/plan-calls.ts
|
|
7202
|
-
/**
|
|
7203
|
-
* Plans and decisions: what the Worker asks a thread's Project DO, once `Authorize` has said
|
|
7204
|
-
* who's acting. A plan is written with its words; the Project DO stores each as a text and
|
|
7205
|
-
* records the version with their IDs.
|
|
7206
|
-
*/
|
|
7207
|
-
/** A plan's free text: one of its outcomes, or an answer's reason. */
|
|
7208
|
-
const PlanText = Schema.String.check(Schema.isBetweenLength(1, 2048));
|
|
7209
|
-
/**
|
|
7210
|
-
* How a plan does its work, in system terms, for people: about a page. A revision replaces it,
|
|
7211
|
-
* with a reason; no one appends to it.
|
|
7212
|
-
*/
|
|
7213
|
-
const Approach = Schema.String.check(Schema.isBetweenLength(1, 6e3));
|
|
7214
|
-
/**
|
|
7215
|
-
* A child as written. On a top-level org thread's plan, `project` names one of its owner's
|
|
7216
|
-
* projects to start it in, as that project's thread; left out, it's in its parent's scope.
|
|
7217
|
-
*/
|
|
7218
|
-
const PlanChildInput = Schema.Struct({
|
|
7219
|
-
entry: PlanEntryKey,
|
|
7220
|
-
title: Title,
|
|
7221
|
-
outcome: Outcome,
|
|
7222
|
-
stackedOn: Schema.optionalKey(PlanEntryKey),
|
|
7223
|
-
required: Schema.Boolean,
|
|
7224
|
-
project: Schema.optionalKey(ProjectName)
|
|
7225
|
-
});
|
|
7226
|
-
/**
|
|
7227
|
-
* A plan as written: `PlanRecord`'s shape, with its words in place of text IDs. Its repos are up
|
|
7228
|
-
* to 20 of the project's, by name; a parent whose work is all in its children names none.
|
|
7229
|
-
*/
|
|
7230
|
-
const PlanInput = Schema.Struct({
|
|
7231
|
-
approach: Schema.optionalKey(Approach),
|
|
7232
|
-
repos: Schema.Array(RepoName).check(Schema.isMaxLength(20)),
|
|
7233
|
-
outcomes: Schema.Array(PlanText).check(Schema.isBetweenLength(1, 50)),
|
|
7234
|
-
children: Schema.Array(PlanChildInput).check(Schema.isMaxLength(50))
|
|
7235
|
-
}).check(Schema.makeFilter((plan) => planProblem(plan) ?? true));
|
|
7236
|
-
/**
|
|
7237
|
-
* A project a plan's child names, as the Worker found it in the plan's owner before it asked the
|
|
7238
|
-
* scope: its ID, and whether the writer can see it; or, with no `project`, that the owner has
|
|
7239
|
-
* none by that name. The Worker refuses nothing for it, since only the scope knows which entries
|
|
7240
|
-
* a version keeps.
|
|
7241
|
-
*/
|
|
7242
|
-
const NamedProject = Schema.Struct({
|
|
7243
|
-
name: ProjectName,
|
|
7244
|
-
project: Schema.optionalKey(ProjectId),
|
|
7245
|
-
seen: Schema.Boolean
|
|
7246
|
-
});
|
|
7247
|
-
Schema.Struct({
|
|
7248
|
-
...commandFields,
|
|
7249
|
-
project: ProjectId,
|
|
7250
|
-
ownerName: OwnerName,
|
|
7251
|
-
number: ThreadNumber,
|
|
7252
|
-
plan: PlanInput,
|
|
7253
|
-
projects: Schema.Array(NamedProject).check(Schema.isMaxLength(50)),
|
|
7254
|
-
reason: Schema.optionalKey(Reason),
|
|
7255
|
-
propose: Schema.Boolean
|
|
7256
|
-
});
|
|
7257
|
-
Schema.Struct({
|
|
7258
|
-
...commandFields,
|
|
7259
|
-
ownerName: OwnerName,
|
|
7260
|
-
project: ProjectId,
|
|
7261
|
-
number: ThreadNumber,
|
|
7262
|
-
version: PlanVersion
|
|
7263
|
-
});
|
|
7264
|
-
/** A decision as it stands: what it asks, who decides, the answers so far, and how it closed. */
|
|
7265
|
-
const DecisionView = Schema.Struct({
|
|
7266
|
-
decision: DecisionId,
|
|
7267
|
-
kind: DecisionKind,
|
|
7268
|
-
subject: DecisionSubject,
|
|
7269
|
-
question: Schema.optionalKey(TextId),
|
|
7270
|
-
options: Schema.Array(AnswerOption),
|
|
7271
|
-
lean: Schema.optionalKey(AnswerKey),
|
|
7272
|
-
deciders: Schema.Array(Decider),
|
|
7273
|
-
deadline: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
7274
|
-
state: Schema.Literals(["open", "closed"]),
|
|
7275
|
-
answers: Schema.Array(Schema.Struct({
|
|
7276
|
-
decider: DeciderRef,
|
|
7277
|
-
answer: AnswerKey,
|
|
7278
|
-
reason: Schema.optionalKey(TextId),
|
|
7279
|
-
at: Schema.DateTimeUtcFromMillis
|
|
7280
|
-
})),
|
|
7281
|
-
result: Schema.optionalKey(AnswerKey),
|
|
7282
|
-
closedWhy: Schema.optionalKey(DecisionClosedWhy)
|
|
7283
|
-
});
|
|
7284
|
-
/**
|
|
7285
|
-
* A version as it stands: its content, its state, and, once proposed, where its approval is: its
|
|
7286
|
-
* repos still being checked, or who accepted it.
|
|
7287
|
-
*/
|
|
7288
|
-
const PlanVersionView = Schema.Struct({
|
|
7289
|
-
thread: ThreadId,
|
|
7290
|
-
number: ThreadNumber,
|
|
7291
|
-
version: PlanVersion,
|
|
7292
|
-
state: PlanState,
|
|
7293
|
-
contentHash: PlanContentHash,
|
|
7294
|
-
plan: PlanView,
|
|
7295
|
-
/** Why it was written, when its writer said: never part of its content. */
|
|
7296
|
-
reason: Schema.optionalKey(TextId),
|
|
7297
|
-
writtenBy: ActorRef,
|
|
7298
|
-
writtenAt: Schema.DateTimeUtcFromMillis,
|
|
7299
|
-
/** Proposed, and its repos haven't all answered yet: it isn't accepted yet. */
|
|
7300
|
-
checking: Schema.Boolean,
|
|
7301
|
-
/**
|
|
7302
|
-
* What its check found when it ended, every repo answered or their time run out: the repos
|
|
7303
|
-
* that never answered. None for a draft, while it's being checked, or when a newer version or
|
|
7304
|
-
* the thread closing ended the check first.
|
|
7305
|
-
*/
|
|
7306
|
-
check: Schema.optionalKey(PlanCheck),
|
|
7307
|
-
/**
|
|
7308
|
-
* Accepted, and waiting to be recorded: on its new `children` registering, or on a merge under
|
|
7309
|
-
* the thread's current revision `publishing`.
|
|
7310
|
-
*/
|
|
7311
|
-
accepting: Schema.optionalKey(Schema.Literals(["children", "publishing"])),
|
|
7312
|
-
acceptedBy: Schema.optionalKey(PlanAcceptedBy)
|
|
7313
|
-
});
|
|
7314
|
-
/** A plan's free text refused before anything was stored, saying which. */
|
|
7315
|
-
var PlanTextRefused = class extends Schema.TaggedError()("PlanTextRefused", {
|
|
7316
|
-
/** Where it is in the plan, such as `outcomes[3]`. */
|
|
7317
|
-
where: Schema.String,
|
|
7318
|
-
problem: Schema.Literals([
|
|
7319
|
-
"empty",
|
|
7320
|
-
"too long",
|
|
7321
|
-
"looks like a secret"
|
|
7322
|
-
]),
|
|
7323
|
-
nextStep: Schema.String
|
|
7324
|
-
}, { httpApiStatus: 422 }) {};
|
|
7325
|
-
/** A plan names a repo this project doesn't hold. */
|
|
7326
|
-
var PlanRepoNotFound = class extends Schema.TaggedError()("PlanRepoNotFound", {
|
|
7327
|
-
repo: RepoName,
|
|
7328
|
-
nextStep: Schema.String
|
|
7329
|
-
}, { httpApiStatus: 422 }) {};
|
|
7330
|
-
/**
|
|
7331
|
-
* A plan names its scope's own repo, made just now, before the scope holds it. Nothing was kept
|
|
7332
|
-
* under the request's delivery ID, so the same request is written once it does.
|
|
7333
|
-
*/
|
|
7334
|
-
var OwnRepoNotReady = class extends Schema.TaggedError()("OwnRepoNotReady", {
|
|
7335
|
-
repo: RepoName,
|
|
7336
|
-
nextStep: Schema.String
|
|
7337
|
-
}, { httpApiStatus: 503 }) {};
|
|
7338
|
-
/**
|
|
7339
|
-
* A child entry this plan can't have: a project named on a project thread's plan or a nested org
|
|
7340
|
-
* thread's, a project that isn't there or the writer can't see, one where its proposer may not
|
|
7341
|
-
* start threads, an entry moved to another project, or a stack across scopes. A project that
|
|
7342
|
-
* isn't there and one the writer can't see are refused alike, as `ProjectNotFound` is.
|
|
7343
|
-
*/
|
|
7344
|
-
var PlanChildRefused = class extends Schema.TaggedError()("PlanChildRefused", { nextStep: Schema.String }, { httpApiStatus: 422 }) {};
|
|
7345
|
-
var PlanVersionNotFound = class extends Schema.TaggedError()("PlanVersionNotFound", {
|
|
7346
|
-
number: ThreadNumber,
|
|
7347
|
-
version: PlanVersion,
|
|
7348
|
-
nextStep: Schema.String
|
|
7349
|
-
}, { httpApiStatus: 404 }) {};
|
|
7350
|
-
/**
|
|
7351
|
-
* Only a thread's newest version is proposed, and never one already declined, nor one whose
|
|
7352
|
-
* required or stacked children would close a cycle of threads waiting on each other.
|
|
6992
|
+
* Only a thread's newest version is proposed, and never one already declined, nor one whose
|
|
6993
|
+
* required or stacked children would close a cycle of threads waiting on each other.
|
|
7353
6994
|
*/
|
|
7354
6995
|
var PlanNotProposable = class extends Schema.TaggedError()("PlanNotProposable", {
|
|
7355
6996
|
version: PlanVersion,
|
|
@@ -7435,7 +7076,7 @@ const GateAnswerRoute = Schema.TaggedStruct("GateAnswerRoute", {
|
|
|
7435
7076
|
reason: Schema.optionalKey(TextId)
|
|
7436
7077
|
});
|
|
7437
7078
|
Schema.Union([
|
|
7438
|
-
|
|
7079
|
+
NamedDecision,
|
|
7439
7080
|
GateAnswerRoute,
|
|
7440
7081
|
DecisionNotFound,
|
|
7441
7082
|
AnswerRefused,
|
|
@@ -7467,79 +7108,473 @@ Schema.Union([
|
|
|
7467
7108
|
]) }),
|
|
7468
7109
|
DeliveryOutsideWindow
|
|
7469
7110
|
]);
|
|
7111
|
+
/** Someone decisions wait on: a person, or an agent, and the roles the Worker found they hold. */
|
|
7112
|
+
const Waiter = Schema.Struct({
|
|
7113
|
+
decider: DeciderRef,
|
|
7114
|
+
roles: Schema.Array(Schema.Struct({
|
|
7115
|
+
role: Schema.Literal("admin"),
|
|
7116
|
+
owner: OwnerId
|
|
7117
|
+
}))
|
|
7118
|
+
});
|
|
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) });
|
|
7134
|
+
Schema.Struct({
|
|
7135
|
+
ownerName: OwnerName,
|
|
7136
|
+
number: ThreadNumber
|
|
7137
|
+
});
|
|
7138
|
+
const PlanVersionPage = Schema.Struct({ versions: Schema.Array(PlanVersionView) });
|
|
7139
|
+
Schema.Union([PlanVersionPage, ThreadNotFound]);
|
|
7140
|
+
/** One decision of the project's, by its ID. */
|
|
7141
|
+
/** At most how long a read of a decision waits for it to close, in seconds. */
|
|
7142
|
+
const DecisionWait = Schema.Int.check(Schema.isBetween({
|
|
7143
|
+
minimum: 1,
|
|
7144
|
+
maximum: 60
|
|
7145
|
+
}));
|
|
7146
|
+
Schema.Struct({
|
|
7147
|
+
ownerName: OwnerName,
|
|
7148
|
+
decision: DecisionId,
|
|
7149
|
+
wait: Schema.optionalKey(DecisionWait)
|
|
7150
|
+
});
|
|
7151
|
+
Schema.Union([NamedDecision, DecisionNotFound]);
|
|
7152
|
+
/**
|
|
7153
|
+
* `POST …/plans`: a version, as a draft unless it's proposed at once, and why it was written.
|
|
7154
|
+
* A first version needs no reason, and a later one may leave it out.
|
|
7155
|
+
*/
|
|
7156
|
+
const WritePlanRequest = Schema.Struct({
|
|
7157
|
+
plan: PlanInput,
|
|
7158
|
+
reason: Schema.optionalKey(Reason),
|
|
7159
|
+
propose: Schema.optionalKey(Schema.Boolean)
|
|
7160
|
+
});
|
|
7161
|
+
/** `POST …/decisions/{decision}/answers`: one of its answers, and why, in words. */
|
|
7162
|
+
const AnswerRequest = Schema.Struct({
|
|
7163
|
+
answer: AnswerKey,
|
|
7164
|
+
reason: Schema.optionalKey(PlanText)
|
|
7165
|
+
});
|
|
7166
|
+
Schema.Struct({
|
|
7167
|
+
thread: ThreadId,
|
|
7168
|
+
owner: OwnerId,
|
|
7169
|
+
parent: ThreadId,
|
|
7170
|
+
parentScope: ProjectId,
|
|
7171
|
+
parentNumber: ThreadNumber,
|
|
7172
|
+
fromPlan: Schema.Struct({
|
|
7173
|
+
version: PlanVersion,
|
|
7174
|
+
entry: PlanEntryKey
|
|
7175
|
+
}),
|
|
7176
|
+
title: Title,
|
|
7177
|
+
outcome: Outcome,
|
|
7178
|
+
required: Schema.Boolean,
|
|
7179
|
+
actor: ActorRef,
|
|
7180
|
+
cause: EventRef
|
|
7181
|
+
});
|
|
7182
|
+
/** The child as it stands, whether this call started it or an earlier one did. */
|
|
7183
|
+
const RemoteChildStarted = Schema.Struct({
|
|
7184
|
+
number: ThreadNumber,
|
|
7185
|
+
stage: ThreadStage,
|
|
7186
|
+
stageRevision: StageRevision
|
|
7187
|
+
});
|
|
7188
|
+
Schema.Union([RemoteChildStarted, ProjectNotReady]);
|
|
7189
|
+
Schema.Struct({
|
|
7190
|
+
thread: ThreadId,
|
|
7191
|
+
number: ThreadNumber
|
|
7192
|
+
});
|
|
7193
|
+
/** A child's stage and its revision, as one read found them. */
|
|
7194
|
+
const ChildStageView = Schema.Struct({
|
|
7195
|
+
stage: ThreadStage,
|
|
7196
|
+
stageRevision: StageRevision
|
|
7197
|
+
});
|
|
7198
|
+
Schema.Union([ChildStageView, ThreadNotFound]);
|
|
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);
|
|
7470
7514
|
Schema.Struct({
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
role: Schema.Literal("admin"),
|
|
7474
|
-
owner: OwnerId
|
|
7475
|
-
})))
|
|
7515
|
+
approval: ApprovalId,
|
|
7516
|
+
result: Schema.String
|
|
7476
7517
|
});
|
|
7477
|
-
|
|
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) })]);
|
|
7478
7522
|
Schema.Struct({
|
|
7479
|
-
|
|
7480
|
-
|
|
7523
|
+
person: PersonId,
|
|
7524
|
+
approval: ApprovalId
|
|
7481
7525
|
});
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
/**
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
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";
|
|
7490
7534
|
Schema.Struct({
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7535
|
+
token: TokenId,
|
|
7536
|
+
secretHash: SecretHash,
|
|
7537
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
7494
7538
|
});
|
|
7495
|
-
Schema.
|
|
7539
|
+
Schema.Struct({ registered: Schema.Boolean });
|
|
7496
7540
|
/**
|
|
7497
|
-
*
|
|
7498
|
-
*
|
|
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.
|
|
7499
7544
|
*/
|
|
7500
|
-
const
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
propose: Schema.optionalKey(Schema.Boolean)
|
|
7504
|
-
});
|
|
7505
|
-
/** `POST …/decisions/{decision}/answers`: one of its answers, and why, in words. */
|
|
7506
|
-
const AnswerRequest = Schema.Struct({
|
|
7507
|
-
answer: AnswerKey,
|
|
7508
|
-
reason: Schema.optionalKey(PlanText)
|
|
7509
|
-
});
|
|
7510
|
-
Schema.Struct({
|
|
7511
|
-
thread: ThreadId,
|
|
7545
|
+
const Welcome = Schema.Union([Schema.Struct({
|
|
7546
|
+
state: Schema.Literals(["ready", "setting up"]),
|
|
7547
|
+
person: PersonId,
|
|
7512
7548
|
owner: OwnerId,
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
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"));
|
|
7533
7571
|
Schema.Struct({
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
/** A child's stage and its revision, as one read found them. */
|
|
7538
|
-
const ChildStageView = Schema.Struct({
|
|
7539
|
-
stage: ThreadStage,
|
|
7540
|
-
stageRevision: StageRevision
|
|
7572
|
+
provider: Schema.Literal("github"),
|
|
7573
|
+
callbackURL: SignInDestination,
|
|
7574
|
+
errorCallbackURL: SignInDestination
|
|
7541
7575
|
});
|
|
7542
|
-
|
|
7576
|
+
/** The name a naming person chose for their owner. */
|
|
7577
|
+
const NameOwner = Schema.Struct({ name: OwnerName });
|
|
7543
7578
|
//#endregion
|
|
7544
7579
|
//#region ../contracts/src/ask-calls.ts
|
|
7545
7580
|
/**
|
|
@@ -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
|
-
/**
|
|
12016
|
-
|
|
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
|
-
})
|
|
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:
|
|
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:
|
|
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",
|
|
@@ -20180,10 +20467,10 @@ const blockerLines = (list, address) => {
|
|
|
20180
20467
|
].join("\n");
|
|
20181
20468
|
};
|
|
20182
20469
|
/** How many blockers stand on the thread now, in words. */
|
|
20183
|
-
const standingWords = (changed) => changed.standing === 0 ? "None stands now, so ref0 merges it once it’s ready." : `${changed.standing} ${changed.standing === 1 ? "stands" : "stand"}, so ref0 won’t merge it yet.`;
|
|
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.`;
|
|
20184
20471
|
/** A blocker added, in words: which, its ask, how many stand, and a publish it didn't stop. */
|
|
20185
20472
|
const addedWords = (changed, address) => [
|
|
20186
|
-
`Blocked ${address} by ${changed.blocker}. ${standingWords(changed)}`,
|
|
20473
|
+
`Blocked ${address} by ${changed.blocker}. ${standingWords$1(changed)}`,
|
|
20187
20474
|
...changed.decision === void 0 ? [] : [`It waits on a person’s answer to the ask ${changed.decision}.`],
|
|
20188
20475
|
...changed.admitted === void 0 ? [] : [`A merge carrying it was already under way in ${changed.admitted.join(", ")}; the blocker doesn’t stop that one.`]
|
|
20189
20476
|
].join("\n");
|
|
@@ -20330,7 +20617,7 @@ const clearing = (name, description) => Command.make(name, {
|
|
|
20330
20617
|
payload
|
|
20331
20618
|
}));
|
|
20332
20619
|
const done = name === "remove" ? "Removed" : "Marked met";
|
|
20333
|
-
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)}`);
|
|
20334
20621
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(description));
|
|
20335
20622
|
/** `ref0 blocked` and its subcommands. */
|
|
20336
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([
|
|
@@ -21030,7 +21317,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
21030
21317
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21031
21318
|
* and only as a default export.
|
|
21032
21319
|
*/
|
|
21033
|
-
const cliVersion = "0.
|
|
21320
|
+
const cliVersion = "0.11.0";
|
|
21034
21321
|
//#endregion
|
|
21035
21322
|
//#region src/harness/hook-protocol.ts
|
|
21036
21323
|
/**
|
|
@@ -21408,9 +21695,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21408
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.",
|
|
21409
21696
|
"Waiting for the answer. Ctrl-C stops it."
|
|
21410
21697
|
]]);
|
|
21411
|
-
|
|
21412
|
-
const show = (text) => Effect.ignore(terminal.display(text));
|
|
21413
|
-
const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
|
|
21698
|
+
const endWait = yield* Effect.cached(Console.error(""));
|
|
21414
21699
|
const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
|
|
21415
21700
|
if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
|
|
21416
21701
|
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
|
|
@@ -21569,25 +21854,21 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21569
21854
|
});
|
|
21570
21855
|
const typedCode = Effect.gen(function* () {
|
|
21571
21856
|
const keys = yield* terminal.readInput;
|
|
21857
|
+
let typed = "";
|
|
21572
21858
|
while (true) {
|
|
21573
21859
|
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21574
21860
|
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21575
21861
|
const { input, key } = pressed.value;
|
|
21576
21862
|
if (key.name === "backspace") {
|
|
21577
|
-
if (typed === "") continue;
|
|
21578
21863
|
typed = takeBack(typed);
|
|
21579
|
-
yield* show("\b \b");
|
|
21580
21864
|
continue;
|
|
21581
21865
|
}
|
|
21582
21866
|
if (key.name !== "return" && key.name !== "enter") {
|
|
21583
21867
|
const text = Option.getOrElse(input, () => "");
|
|
21584
|
-
if (
|
|
21585
|
-
typed += text;
|
|
21586
|
-
yield* show(text);
|
|
21868
|
+
if (typesText(text)) typed += text;
|
|
21587
21869
|
continue;
|
|
21588
21870
|
}
|
|
21589
21871
|
const line = typed.trim();
|
|
21590
|
-
if (typed !== "") yield* show("\n");
|
|
21591
21872
|
typed = "";
|
|
21592
21873
|
if (line === "") continue;
|
|
21593
21874
|
if (isAnswerCode(line)) return line;
|
|
@@ -21595,7 +21876,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21595
21876
|
}
|
|
21596
21877
|
}).pipe(Effect.scoped);
|
|
21597
21878
|
const readAnswer = Effect.gen(function* () {
|
|
21598
|
-
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}.`]]);
|
|
21599
21880
|
const code = yield* typedCode;
|
|
21600
21881
|
yield* Ref.update(pasted, (first) => first ?? code);
|
|
21601
21882
|
if (listener !== void 0) yield* listener.stop;
|
|
@@ -24062,8 +24343,16 @@ const mostFolders = 8;
|
|
|
24062
24343
|
const isConfigFolderPath = Schema.is(ConfigFolderPath);
|
|
24063
24344
|
/** The line that asks again. */
|
|
24064
24345
|
const again = "ref0 connect";
|
|
24065
|
-
/**
|
|
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
|
+
*/
|
|
24066
24354
|
const choose = Effect.fnUntraced(function* (found, home) {
|
|
24355
|
+
const columns = yield* (yield* Terminal.Terminal).columns;
|
|
24067
24356
|
const choices = found.map((folder) => {
|
|
24068
24357
|
const shown = shownConfigFolder(home, folder.configFolder);
|
|
24069
24358
|
const sendable = isConfigFolderPath(shown);
|
|
@@ -24079,8 +24368,9 @@ const choose = Effect.fnUntraced(function* (found, home) {
|
|
|
24079
24368
|
};
|
|
24080
24369
|
});
|
|
24081
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)]]);
|
|
24082
24373
|
return yield* Prompt.run(Prompt.MultiSelect({
|
|
24083
|
-
message: ticked > mostFolders ? `Connect which config folders? One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. Space ticks or unticks one; Enter connects those ticked.` : "Connect which config folders? Space ticks or unticks one; Enter connects those ticked.",
|
|
24084
24374
|
choices,
|
|
24085
24375
|
max: mostFolders
|
|
24086
24376
|
}));
|
|
@@ -24197,6 +24487,7 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
|
|
|
24197
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.`);
|
|
24198
24488
|
const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
|
|
24199
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("");
|
|
24200
24491
|
const connected = yield* requestAttachment(chosen);
|
|
24201
24492
|
const installed = yield* installInto(connected.kept, { asJson });
|
|
24202
24493
|
const others = yield* otherOwners(connected);
|
|
@@ -24582,7 +24873,7 @@ const paramValues = (params) => {
|
|
|
24582
24873
|
* its params, and when it ends; under it, each workflow's run, by its title and its file, and
|
|
24583
24874
|
* how it stands; and under each, its newest asks and passkeys.
|
|
24584
24875
|
*/
|
|
24585
|
-
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) => {
|
|
24586
24877
|
const last = index === wait.runs.length - 1;
|
|
24587
24878
|
const under = last ? " " : "│ ";
|
|
24588
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)}`)];
|
|
@@ -25492,7 +25783,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
|
|
|
25492
25783
|
/** Says a waiting start's tree on stderr, as it stands now. */
|
|
25493
25784
|
const sayTree = Effect.fnUntraced(function* (wait) {
|
|
25494
25785
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25495
|
-
yield* Console.error(printable(waitingLines(wait, now)));
|
|
25786
|
+
yield* Console.error(printable(waitingLines$1(wait, now)));
|
|
25496
25787
|
});
|
|
25497
25788
|
/**
|
|
25498
25789
|
* Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
|
|
@@ -25639,7 +25930,7 @@ const run = Command.make("run", {
|
|
|
25639
25930
|
value
|
|
25640
25931
|
}, asJson, rerunCommand);
|
|
25641
25932
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25642
|
-
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)}`);
|
|
25643
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"));
|
|
25644
25935
|
const trace = Command.make("trace", {
|
|
25645
25936
|
run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
|
|
@@ -25717,7 +26008,7 @@ const waiting$1 = Command.make("waiting", {
|
|
|
25717
26008
|
value
|
|
25718
26009
|
}, asJson, rerunCommand);
|
|
25719
26010
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25720
|
-
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)}`);
|
|
25721
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"));
|
|
25722
26013
|
/** What a cancel did, in words. */
|
|
25723
26014
|
const cancelledText = {
|
|
@@ -25836,19 +26127,21 @@ const checksCommands = [Command.make("checks").pipe(Command.withDescription("A r
|
|
|
25836
26127
|
//#endregion
|
|
25837
26128
|
//#region src/gate-lines.ts
|
|
25838
26129
|
/**
|
|
25839
|
-
* A pull request's gate as `ref0 gate` says it: its current version, whether its
|
|
25840
|
-
*
|
|
25841
|
-
*
|
|
25842
|
-
*
|
|
25843
|
-
*
|
|
25844
|
-
*
|
|
25845
|
-
*
|
|
25846
|
-
*
|
|
25847
|
-
*
|
|
25848
|
-
*
|
|
25849
|
-
*
|
|
25850
|
-
*
|
|
25851
|
-
*
|
|
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.
|
|
25852
26145
|
*/
|
|
25853
26146
|
const fromWords = (from) => {
|
|
25854
26147
|
switch (from.kind) {
|
|
@@ -25879,11 +26172,116 @@ const columns = (lines) => {
|
|
|
25879
26172
|
* version was decided under, whose checks run for it, never `main`'s.
|
|
25880
26173
|
*/
|
|
25881
26174
|
const noteOf = (need, view) => need.kind === "check" && need.source === "project" && view.projectRevision !== void 0 ? ` at gate revision ${view.projectRevision}` : "";
|
|
25882
|
-
const
|
|
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) => {
|
|
25883
26281
|
switch (view.state) {
|
|
25884
|
-
case "pending": return "its gate hasn’t
|
|
25885
|
-
case "answered": return
|
|
25886
|
-
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)}`;
|
|
25887
26285
|
}
|
|
25888
26286
|
};
|
|
25889
26287
|
/** The project gate's line, under the head: its revision, and why it failed. */
|
|
@@ -25891,7 +26289,7 @@ const projectWords$1 = (view) => {
|
|
|
25891
26289
|
const project = view.projectGate;
|
|
25892
26290
|
if (project === void 0) return [];
|
|
25893
26291
|
const at = ` project gate, revision ${project.revision}`;
|
|
25894
|
-
return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state}`];
|
|
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"}`];
|
|
25895
26293
|
};
|
|
25896
26294
|
/**
|
|
25897
26295
|
* What a gate read of the change, under `head`: each read's tree, its path and the object it
|
|
@@ -25906,6 +26304,23 @@ const needLines = (needs, view) => columns(needs.map(({ need, from }) => ({
|
|
|
25906
26304
|
need: spellNeed(need),
|
|
25907
26305
|
from: `${from.map(fromWords).join("; ")}${noteOf(need, view)}`
|
|
25908
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
|
+
};
|
|
25909
26324
|
const grewWords = (grew) => {
|
|
25910
26325
|
const why = grew.why ?? "ref0 couldn’t say why";
|
|
25911
26326
|
switch (grew.kind) {
|
|
@@ -25917,7 +26332,7 @@ const grewWords = (grew) => {
|
|
|
25917
26332
|
};
|
|
25918
26333
|
const inPlaceLines = (inPlace, view) => {
|
|
25919
26334
|
const head = `under main’s .ref0/, at ${inPlace.main.slice(0, 7)}:`;
|
|
25920
|
-
if (inPlace.state === "pending") return [`${head} its gate there hasn’t
|
|
26335
|
+
if (inPlace.state === "pending") return [`${head} its gate there hasn’t returned its needs yet`];
|
|
25921
26336
|
const reads = readLines("its gate there read:", inPlace.reads ?? []);
|
|
25922
26337
|
if (inPlace.state === "held" || inPlace.grew === void 0) return [
|
|
25923
26338
|
`${head} ${inPlace.state}`,
|
|
@@ -25991,7 +26406,11 @@ const wrappedPartLines = (parts) => parts.length === 0 ? [] : ["parts that take
|
|
|
25991
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}`];
|
|
25992
26407
|
const revokedLines = (revoked) => revoked.length === 0 ? [] : ["signers revoked:", ...revoked.flatMap(revokedSignerLines)];
|
|
25993
26408
|
const gateLines = (view, repo) => {
|
|
25994
|
-
const
|
|
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)}`;
|
|
25995
26414
|
const inPlace = view.inPlace === void 0 ? [] : inPlaceLines(view.inPlace, view);
|
|
25996
26415
|
const added = view.added.map(({ need, from }) => ({
|
|
25997
26416
|
need: spellNeed(need),
|
|
@@ -26003,7 +26422,8 @@ const gateLines = (view, repo) => {
|
|
|
26003
26422
|
return [
|
|
26004
26423
|
head,
|
|
26005
26424
|
...projectWords$1(view),
|
|
26006
|
-
...
|
|
26425
|
+
...standingLines(view, named),
|
|
26426
|
+
...oneYesLines(view, named),
|
|
26007
26427
|
...readLines("its gate read:", view.reads),
|
|
26008
26428
|
...readLines("its project gate read:", view.projectGate?.reads ?? []),
|
|
26009
26429
|
...inPlace,
|
|
@@ -26058,7 +26478,7 @@ const gateCommands = [Command.make("gate", {
|
|
|
26058
26478
|
}
|
|
26059
26479
|
});
|
|
26060
26480
|
yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
|
|
26061
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("A pull request’s needs, each with where it came from
|
|
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"))];
|
|
26062
26482
|
//#endregion
|
|
26063
26483
|
//#region src/clone.ts
|
|
26064
26484
|
/** The remote a clone pushes to: its thread's fork path, named as the thread is. */
|
|
@@ -27508,18 +27928,28 @@ const repoCommands = [Command.make("repos", {
|
|
|
27508
27928
|
//#endregion
|
|
27509
27929
|
//#region src/decision-lines.ts
|
|
27510
27930
|
/** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
|
|
27511
|
-
/** What a decision is about, in words. */
|
|
27512
|
-
const subjectWords = (subject) => {
|
|
27513
|
-
switch (subject.kind) {
|
|
27514
|
-
case "thread": return subject.thread;
|
|
27515
|
-
case "version": return `version ${subject.version} of pull request ${subject.pullRequest} in ${subject.repo}, from ${subject.thread}`;
|
|
27516
|
-
}
|
|
27517
|
-
};
|
|
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;
|
|
27518
27933
|
/** A decision in a line. */
|
|
27519
|
-
const decisionLines = (
|
|
27520
|
-
const on = subjectWords(
|
|
27521
|
-
const answers =
|
|
27522
|
-
return
|
|
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");
|
|
27523
27953
|
};
|
|
27524
27954
|
//#endregion
|
|
27525
27955
|
//#region src/commands/plans.ts
|
|
@@ -27713,9 +28143,7 @@ const planCommands = [
|
|
|
27713
28143
|
endpoint: "decisions.waiting",
|
|
27714
28144
|
params: scopeParams(address)
|
|
27715
28145
|
}));
|
|
27716
|
-
|
|
27717
|
-
const words = decisions.length === 0 ? "Nothing waits on you here." : decisions.map((each) => decisionLines(each)).join("\n");
|
|
27718
|
-
yield* printAnswer(answer.json, asJson, words);
|
|
28146
|
+
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
|
|
27719
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")),
|
|
27720
28148
|
Command.make("decision", {
|
|
27721
28149
|
project: project$2,
|
|
@@ -34168,6 +34596,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
|
|
|
34168
34596
|
CLAUDE_CONFIG_DIR: configFolder,
|
|
34169
34597
|
...checked
|
|
34170
34598
|
};
|
|
34599
|
+
const withheld = Object.keys(yield* fromHere(keychainVariables)).some((variable) => env[variable] === void 0);
|
|
34171
34600
|
return yield* Effect.scoped(Effect.gen(function* () {
|
|
34172
34601
|
const handle = yield* spawner.spawn(ChildProcess.make(server.value.command, server.value.args, {
|
|
34173
34602
|
cwd: yield* emptyFolder(fs),
|
|
@@ -34182,6 +34611,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
|
|
|
34182
34611
|
const said = yield* handle.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkScoped);
|
|
34183
34612
|
const lines = yield* handle.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.takeUntil((line) => Option.isSome(decodeAnswer(line))), Stream.runCollect);
|
|
34184
34613
|
if (lines.some((line) => Option.isSome(decodeAnswer(line)))) return { kind: "answered" };
|
|
34614
|
+
if (withheld) return { kind: "withoutKeychain" };
|
|
34185
34615
|
return {
|
|
34186
34616
|
kind: "stopped",
|
|
34187
34617
|
said: firstLine(yield* Fiber.join(said)) || firstLine(lines.join("\n"))
|
|
@@ -34252,6 +34682,7 @@ const serverCheckOf = (started, configFolder) => {
|
|
|
34252
34682
|
switch (started.kind) {
|
|
34253
34683
|
case "answered": return checkOf("server", "yes", "starts");
|
|
34254
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));
|
|
34255
34686
|
case "unnamed": return checkOf("server", "no", "the plugin names no ref0 server", installLine(configFolder));
|
|
34256
34687
|
case "unanswered": return checkOf("server", "unknown", `didn’t answer in ${Duration.toSeconds(serverLimit)} s`, "ref0 status");
|
|
34257
34688
|
}
|