@ref0/cli 0.7.0 → 0.9.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 +2395 -426
- package/package.json +3 -2
package/dist/main.mjs
CHANGED
|
@@ -6510,8 +6510,17 @@ var PasskeyRefused = class extends Schema.TaggedError()("PasskeyRefused", { next
|
|
|
6510
6510
|
//#endregion
|
|
6511
6511
|
//#region ../contracts/src/attachments.ts
|
|
6512
6512
|
/** A machine asks, a signed-in person claims and approves, and the answer returns to its terminal. */
|
|
6513
|
+
/**
|
|
6514
|
+
* What could reorder or hide the text around it, such as U+202E: a control, format, surrogate,
|
|
6515
|
+
* private-use or unassigned character, or a line or paragraph separator.
|
|
6516
|
+
*/
|
|
6517
|
+
const hiddenCharacters = String.raw`\p{C}\p{Zl}\p{Zp}`;
|
|
6518
|
+
/** Text with none of them, which reads as it's written wherever it's shown. */
|
|
6519
|
+
const shownAsWritten = Schema.isPattern(new RegExp(`^[^${hiddenCharacters}]+$`, "u"));
|
|
6520
|
+
/** Text with each character that could reorder or hide what's around it taken out. */
|
|
6521
|
+
const withoutHiddenCharacters = (text) => text.replaceAll(new RegExp(`[${hiddenCharacters}]`, "gu"), "");
|
|
6513
6522
|
/** A machine's own word for itself: shown to the person as what it says, never as fact. */
|
|
6514
|
-
const MachineName = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(64));
|
|
6523
|
+
const MachineName = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(64), shownAsWritten);
|
|
6515
6524
|
/** The harness a machine says it connects for, never proof of what it runs. */
|
|
6516
6525
|
const ConnectHarness = Schema.Literals(["claude-code", "codex"]);
|
|
6517
6526
|
/** The machine's loopback listener; no caller may choose its host. */
|
|
@@ -6523,7 +6532,7 @@ const AttachmentCallback = Schema.Struct({
|
|
|
6523
6532
|
state: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/))
|
|
6524
6533
|
});
|
|
6525
6534
|
/** A config folder's path, as the machine shows it, never proof of its location. */
|
|
6526
|
-
const ConfigFolderPath = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200),
|
|
6535
|
+
const ConfigFolderPath = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(200), shownAsWritten);
|
|
6527
6536
|
const uniqueFolderPaths = Schema.makeFilter((folders) => new Set(folders.map((folder) => folder.path)).size === folders.length, { expected: "unique config folder paths" });
|
|
6528
6537
|
const RequestFolder = Schema.Struct({
|
|
6529
6538
|
path: ConfigFolderPath,
|
|
@@ -11766,10 +11775,12 @@ Schema.Union([
|
|
|
11766
11775
|
NotPermitted
|
|
11767
11776
|
]);
|
|
11768
11777
|
/**
|
|
11769
|
-
* A
|
|
11770
|
-
*
|
|
11771
|
-
*
|
|
11772
|
-
* `…/threads/{n}/sessions/{m}/{repo}.git`, reads that session's fork, and
|
|
11778
|
+
* A fork's path on ref0's host. Its thread's, `/{owner}/{project}/threads/{n}/{repo}.git` on a
|
|
11779
|
+
* project's thread and `/{owner}/threads/{n}/{repo}.git` on an org thread, reaches the fork of the
|
|
11780
|
+
* session whose token asks, and takes its pushes. A session's,
|
|
11781
|
+
* `…/threads/{n}/sessions/{m}/{repo}.git`, reads that session's current fork, and a fork's,
|
|
11782
|
+
* `…/threads/{n}/forks/{fork}/{repo}.git`, reads that fork when it holds thread `n`'s work, a
|
|
11783
|
+
* session's on it or a run's whose pull request is on it. Neither takes a push.
|
|
11773
11784
|
*/
|
|
11774
11785
|
const ForkPath = Schema.String.check(Schema.isPattern(/^\/[^\s]+\.git$/), Schema.isMaxLength(512)).pipe(Schema.brand("ForkPath"));
|
|
11775
11786
|
/** A session's fork's URL on the app's one origin: the one a notice names, and a clone pushes to. */
|
|
@@ -11793,17 +11804,21 @@ const ForkView = Schema.Struct({
|
|
|
11793
11804
|
/** On the app's one origin, whichever host was asked. */
|
|
11794
11805
|
url: ForkUrl
|
|
11795
11806
|
});
|
|
11796
|
-
|
|
11807
|
+
/**
|
|
11808
|
+
* For a reader that sees only threads it holds a grant on, those threads: a fork of a thread
|
|
11809
|
+
* stacked, at any depth, on any other is refused, since it holds that thread's work. Unset for one
|
|
11810
|
+
* that reads the project as a whole.
|
|
11811
|
+
*/
|
|
11812
|
+
const readerSees = { only: Schema.optionalKey(Schema.Array(ThreadId)) };
|
|
11813
|
+
Schema.Union([Schema.Struct({
|
|
11797
11814
|
session: SessionId,
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
});
|
|
11806
|
-
/** A session's fork in Artifacts, as Git reaches it: where it is, and which Git repository. */
|
|
11815
|
+
...readerSees
|
|
11816
|
+
}), Schema.Struct({
|
|
11817
|
+
fork: ForkId,
|
|
11818
|
+
thread: ThreadId,
|
|
11819
|
+
...readerSees
|
|
11820
|
+
})]);
|
|
11821
|
+
/** A fork in Artifacts, as Git reaches it: where it is, and which Git repository. */
|
|
11807
11822
|
const ForkPlace = Schema.Struct({
|
|
11808
11823
|
...CurrentBaseline.fields,
|
|
11809
11824
|
gitRepository: GitRepositoryId
|
|
@@ -11834,19 +11849,16 @@ const SessionBase = Schema.Struct({
|
|
|
11834
11849
|
commit: CommitOid,
|
|
11835
11850
|
/**
|
|
11836
11851
|
* Where to fetch it from, on ref0's origin: the repo's own path, as `ref0 clone` cloned it, for
|
|
11837
|
-
* `main`; for a stack, the path of the fork that holds the pin.
|
|
11852
|
+
* `main`; for a stack, the path of the fork that holds the pin, by its ID under the thread below.
|
|
11838
11853
|
*/
|
|
11839
11854
|
path: Schema.String.check(Schema.isPattern(/^\/[^\s]+\.git$/), Schema.isMaxLength(512)),
|
|
11840
11855
|
/** `path` on the app's one origin. A client checks it's a fork of its own repo there. */
|
|
11841
11856
|
url: Schema.String,
|
|
11842
11857
|
ref: Schema.Literal("refs/heads/main")
|
|
11843
11858
|
});
|
|
11844
|
-
/**
|
|
11845
|
-
* No base to fetch: `main` has no commit yet, or the pin of the thread below is in a workflow
|
|
11846
|
-
* run's fork, which has no session's path to fetch from.
|
|
11847
|
-
*/
|
|
11859
|
+
/** No base to fetch: `main` has no commit yet. */
|
|
11848
11860
|
var NoBase = class extends Schema.TaggedError()("NoBase", {
|
|
11849
|
-
why: Schema.
|
|
11861
|
+
why: Schema.Literal("main empty"),
|
|
11850
11862
|
nextStep: Schema.String
|
|
11851
11863
|
}, { httpApiStatus: 404 }) {};
|
|
11852
11864
|
Schema.Struct({
|
|
@@ -11864,15 +11876,14 @@ const BaseOnMain = Schema.Struct({
|
|
|
11864
11876
|
commit: CommitOid
|
|
11865
11877
|
});
|
|
11866
11878
|
/**
|
|
11867
|
-
* The base on the thread below, `lower`: its pin's commit, the fork that holds it,
|
|
11868
|
-
*
|
|
11879
|
+
* The base on the thread below, `lower`: its pin's commit, and the fork that holds it, a session's
|
|
11880
|
+
* on that thread or a run's whose pull request is on it.
|
|
11869
11881
|
*/
|
|
11870
11882
|
const BaseOnStack = Schema.Struct({
|
|
11871
11883
|
on: Schema.Literal("stack"),
|
|
11872
11884
|
lower: ThreadId,
|
|
11873
11885
|
commit: CommitOid,
|
|
11874
|
-
fork: ForkId
|
|
11875
|
-
session: Schema.NullOr(SessionId)
|
|
11886
|
+
fork: ForkId
|
|
11876
11887
|
});
|
|
11877
11888
|
Schema.Union([
|
|
11878
11889
|
BaseOnMain,
|
|
@@ -14978,6 +14989,11 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
|
|
|
14978
14989
|
security: { bearer: HttpApiSecurity.bearer },
|
|
14979
14990
|
error: [Unauthenticated]
|
|
14980
14991
|
}) {};
|
|
14992
|
+
/**
|
|
14993
|
+
* A machine's request whose name its contract refuses, answered as `CommandRefused`, which says
|
|
14994
|
+
* what to change. Any other part that fails its contract is answered with Effect's empty 400.
|
|
14995
|
+
*/
|
|
14996
|
+
var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
|
|
14981
14997
|
const idempotency = { "idempotency-key": RequestId };
|
|
14982
14998
|
const inProject$1 = {
|
|
14983
14999
|
owner: OwnerRef,
|
|
@@ -15101,7 +15117,7 @@ var MachineAttachmentsGroup = class extends HttpApiGroup.make("machineAttachment
|
|
|
15101
15117
|
SlowDown,
|
|
15102
15118
|
AuthorityUnavailable
|
|
15103
15119
|
]
|
|
15104
|
-
}), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
|
|
15120
|
+
}).middleware(MachineNameRefusal), HttpApiEndpoint.post("collect", "/v1/attachments/requests/:approval/result", {
|
|
15105
15121
|
params: { approval: ApprovalId },
|
|
15106
15122
|
payload: Schema.Struct({ answerCode: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(64))) }),
|
|
15107
15123
|
success: RequestState,
|
|
@@ -18321,11 +18337,29 @@ const CodexProfile = Schema.Struct({
|
|
|
18321
18337
|
])))),
|
|
18322
18338
|
network: Schema.optionalKey(Schema.NullOr(Schema.Struct({ enabled: Schema.optionalKey(Schema.NullOr(Schema.Boolean)) })))
|
|
18323
18339
|
});
|
|
18324
|
-
/**
|
|
18325
|
-
const
|
|
18326
|
-
|
|
18327
|
-
|
|
18328
|
-
|
|
18340
|
+
/** Codex applies excludes before `set`, then applies either form of allowlist after it. */
|
|
18341
|
+
const CodexShellEnvironment = Schema.Struct({
|
|
18342
|
+
set: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.String))),
|
|
18343
|
+
exclude: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
|
|
18344
|
+
include_only: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),
|
|
18345
|
+
filters: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.Literals(["include", "exclude"]))))
|
|
18346
|
+
});
|
|
18347
|
+
/** The permission and environment policies, with user-layer versions and raw policy for rollback. */
|
|
18348
|
+
const CodexConfigRead = Schema.Struct({
|
|
18349
|
+
config: Schema.Struct({
|
|
18350
|
+
default_permissions: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
18351
|
+
permissions: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, CodexProfile))),
|
|
18352
|
+
shell_environment_policy: Schema.optionalKey(Schema.NullOr(CodexShellEnvironment))
|
|
18353
|
+
}),
|
|
18354
|
+
layers: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.Struct({
|
|
18355
|
+
name: Schema.Struct({
|
|
18356
|
+
type: Schema.String,
|
|
18357
|
+
file: Schema.optionalKey(Schema.String)
|
|
18358
|
+
}),
|
|
18359
|
+
version: Schema.String,
|
|
18360
|
+
config: Schema.optionalKey(Schema.Struct({ shell_environment_policy: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)) }))
|
|
18361
|
+
}))))
|
|
18362
|
+
});
|
|
18329
18363
|
/**
|
|
18330
18364
|
* `config/batchWrite`'s result, as much of it as ref0 holds Codex to: that it wrote, and whether
|
|
18331
18365
|
* a layer above the person's own config overrides what it wrote.
|
|
@@ -18581,12 +18615,15 @@ const CommandHookInput = Schema.Struct({
|
|
|
18581
18615
|
});
|
|
18582
18616
|
/**
|
|
18583
18617
|
* `SessionStart`'s input, as far as ref0 reads it: the session, where it started, and why
|
|
18584
|
-
* (`startup`, `resume`, `clear` or `compact`, which both harnesses send)
|
|
18618
|
+
* (`startup`, `resume`, `clear` or `compact`, which both harnesses send); and, for the trailer
|
|
18619
|
+
* on the agent's commits, its transcript and the model it started with, which Claude Code sends.
|
|
18585
18620
|
*/
|
|
18586
18621
|
const SessionStartHookInput = Schema.Struct({
|
|
18587
18622
|
session_id: HarnessSessionId,
|
|
18588
18623
|
cwd: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 4096))),
|
|
18589
|
-
source: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 32)))
|
|
18624
|
+
source: Schema.optionalKey(Schema.String.check(Schema.isBetweenLength(1, 32))),
|
|
18625
|
+
transcript_path: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
18626
|
+
model: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
18590
18627
|
});
|
|
18591
18628
|
/**
|
|
18592
18629
|
* A frame on Claude Code's own inbox socket, as `ref0 mcp` writes one, a line each: the token
|
|
@@ -19063,7 +19100,7 @@ const minute = (at) => `${DateTime.formatIso(at).slice(0, 16).replace("T", " ")}
|
|
|
19063
19100
|
const styled = (text, code, terminal) => terminal ? `\u001b[${code}m${printable(text)}\u001b[0m` : printable(text);
|
|
19064
19101
|
const renderLine = (line, terminal) => {
|
|
19065
19102
|
if (Predicate.isString(line)) return printable(line);
|
|
19066
|
-
if ("label" in line) return styled(line.label, 1, terminal) + (line.text ? ` ${printable(line.text)}` : "");
|
|
19103
|
+
if ("label" in line) return (line.before ? `${printable(line.before)} ` : "") + styled(line.label, 1, terminal) + (line.text ? ` ${printable(line.text)}` : "");
|
|
19067
19104
|
if ("command" in line) return printable(line.command).split("\n").map((part) => ` ${part}`).join("\n");
|
|
19068
19105
|
if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
|
|
19069
19106
|
return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
|
|
@@ -19074,6 +19111,14 @@ const renderBlocks = (blocks, stream) => {
|
|
|
19074
19111
|
};
|
|
19075
19112
|
/** Words go to stderr by default; a command's answer goes to stdout. */
|
|
19076
19113
|
const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream)).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
|
|
19114
|
+
/**
|
|
19115
|
+
* A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
|
|
19116
|
+
* yes. The cursor stays after it, for their answer.
|
|
19117
|
+
*/
|
|
19118
|
+
const questionLine = (question) => `${renderBlocks([[{
|
|
19119
|
+
label: question,
|
|
19120
|
+
text: "(y/N)"
|
|
19121
|
+
}]], "stdout")} `;
|
|
19077
19122
|
/** Keep explicit line breaks and wrap between words within the available columns. */
|
|
19078
19123
|
const wrapWords = (text, width) => text.split("\n").flatMap((line) => {
|
|
19079
19124
|
const wrapped = [];
|
|
@@ -19142,6 +19187,25 @@ const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) =>
|
|
|
19142
19187
|
yield* printAnswer(answer.json, asJson, me.agent === void 0 ? `You, as ${me.actor.id}.` : `Agent ${me.agent.id}, in session ${me.session?.session ?? "none"}.`);
|
|
19143
19188
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Who’s acting, and what they hold"), Command.withShortDescription("Show who’s acting and what they hold"))];
|
|
19144
19189
|
//#endregion
|
|
19190
|
+
//#region ../model/src/agent-commits.ts
|
|
19191
|
+
/**
|
|
19192
|
+
* How an agent's commits name it as their author: `pip/claude <agent_…@ref0.work>`. The person
|
|
19193
|
+
* stays the committer, with their own signature. Git keeps every commit for good, and anyone
|
|
19194
|
+
* who reads the repo reads its author, so these name the agent and nothing about its sessions.
|
|
19195
|
+
*/
|
|
19196
|
+
/** The domain of every agent's commit email, which tells an agent's commits from a person's. */
|
|
19197
|
+
const agentCommitDomain = "ref0.work";
|
|
19198
|
+
/**
|
|
19199
|
+
* An agent's commit email: built from its agent ID alone, so it never changes, and never passes
|
|
19200
|
+
* to another agent. Nobody reads mail there.
|
|
19201
|
+
*/
|
|
19202
|
+
const agentCommitEmail = (agent) => `${agent}@${agentCommitDomain}`;
|
|
19203
|
+
/**
|
|
19204
|
+
* An agent's name as its commits' author: its owner, then its handle, as commands write it. A
|
|
19205
|
+
* handle renamed later stays as it was in the commits made before.
|
|
19206
|
+
*/
|
|
19207
|
+
const agentCommitName = (owner, handle) => `${owner}/${handle}`;
|
|
19208
|
+
//#endregion
|
|
19145
19209
|
//#region ../model/src/cut-text.ts
|
|
19146
19210
|
/**
|
|
19147
19211
|
* `text` within `max` as a `Schema`'s length counts it, in UTF-16 code units: whole code points
|
|
@@ -19422,13 +19486,74 @@ function freeTextProblem(text, maxCharacters) {
|
|
|
19422
19486
|
//#endregion
|
|
19423
19487
|
//#region ../model/src/git-paths.ts
|
|
19424
19488
|
/**
|
|
19425
|
-
* A
|
|
19426
|
-
* thread
|
|
19427
|
-
*
|
|
19428
|
-
*
|
|
19429
|
-
*
|
|
19489
|
+
* A fork's path: an owner, a project for a project's thread and none for an org thread's, a
|
|
19490
|
+
* thread, then a session or a fork by its ID when it names one, a repo, and what Git asks for.
|
|
19491
|
+
* The two thread forms never read alike: after `threads` comes a number on an org thread's, and
|
|
19492
|
+
* the word `threads` on a project's, so a project named `threads` keeps its forks. Nor do the three
|
|
19493
|
+
* fork forms: a repo's name holds no `/`.
|
|
19494
|
+
*/
|
|
19495
|
+
const forkPath = /^\/([^/]+)(?:\/([^/]+))?\/threads\/([1-9]\d{0,9})(?:\/sessions\/([1-9]\d{0,9})|\/forks\/(fork_[a-z0-9]+))?\/([^/]+)\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
19496
|
+
//#endregion
|
|
19497
|
+
//#region ../model/src/path-glob.ts
|
|
19498
|
+
/** Charges `cells` to the budget: whether it had them. */
|
|
19499
|
+
const spend = (budget, cells) => {
|
|
19500
|
+
budget.left -= cells;
|
|
19501
|
+
return budget.left >= 0;
|
|
19502
|
+
};
|
|
19503
|
+
/**
|
|
19504
|
+
* Whether one segment matches one segment pattern, `*` and `?` within it; none once the budget
|
|
19505
|
+
* runs out.
|
|
19506
|
+
*/
|
|
19507
|
+
function segmentMatches(pattern, segment, budget) {
|
|
19508
|
+
const characters = Array.from(segment);
|
|
19509
|
+
const width = characters.length + 1;
|
|
19510
|
+
let reach = new Uint8Array(width);
|
|
19511
|
+
let next = new Uint8Array(width);
|
|
19512
|
+
reach[0] = 1;
|
|
19513
|
+
for (const symbol of pattern) {
|
|
19514
|
+
if (!spend(budget, width)) return void 0;
|
|
19515
|
+
next.fill(0);
|
|
19516
|
+
if (symbol === "*") {
|
|
19517
|
+
let any = 0;
|
|
19518
|
+
for (let j = 0; j < width; j += 1) {
|
|
19519
|
+
any |= reach[j] ?? 0;
|
|
19520
|
+
next[j] = any;
|
|
19521
|
+
}
|
|
19522
|
+
} else for (let j = 1; j < width; j += 1) next[j] = (reach[j - 1] ?? 0) & (symbol === "?" || characters[j - 1] === symbol ? 1 : 0);
|
|
19523
|
+
[reach, next] = [next, reach];
|
|
19524
|
+
}
|
|
19525
|
+
return reach[width - 1] === 1;
|
|
19526
|
+
}
|
|
19527
|
+
/**
|
|
19528
|
+
* Whether `path`, a repo path like `src/a.ts`, matches `pattern`, within `budget`: none once it
|
|
19529
|
+
* runs out.
|
|
19430
19530
|
*/
|
|
19431
|
-
|
|
19531
|
+
function pathMatchesWithin(pattern, path, budget) {
|
|
19532
|
+
const parts = pattern.split("/");
|
|
19533
|
+
const segments = path.split("/");
|
|
19534
|
+
const width = segments.length + 1;
|
|
19535
|
+
let reach = new Uint8Array(width);
|
|
19536
|
+
let next = new Uint8Array(width);
|
|
19537
|
+
reach[0] = 1;
|
|
19538
|
+
for (const part of parts) {
|
|
19539
|
+
if (!spend(budget, width)) return void 0;
|
|
19540
|
+
next.fill(0);
|
|
19541
|
+
if (part === "**") {
|
|
19542
|
+
let any = 0;
|
|
19543
|
+
for (let j = 0; j < width; j += 1) {
|
|
19544
|
+
any |= reach[j] ?? 0;
|
|
19545
|
+
next[j] = any;
|
|
19546
|
+
}
|
|
19547
|
+
} else for (let j = 1; j < width; j += 1) {
|
|
19548
|
+
if (reach[j - 1] !== 1) continue;
|
|
19549
|
+
const matched = segmentMatches(part, segments[j - 1] ?? "", budget);
|
|
19550
|
+
if (matched === void 0) return void 0;
|
|
19551
|
+
next[j] = matched ? 1 : 0;
|
|
19552
|
+
}
|
|
19553
|
+
[reach, next] = [next, reach];
|
|
19554
|
+
}
|
|
19555
|
+
return reach[width - 1] === 1;
|
|
19556
|
+
}
|
|
19432
19557
|
//#endregion
|
|
19433
19558
|
//#region ../model/src/thread-workflows.ts
|
|
19434
19559
|
const minuteMillis = 6e4;
|
|
@@ -20340,6 +20465,19 @@ const watchVariables = {
|
|
|
20340
20465
|
event: "REF0_EVENT",
|
|
20341
20466
|
title: "REF0_TITLE"
|
|
20342
20467
|
};
|
|
20468
|
+
/**
|
|
20469
|
+
* What ref0 puts in the environment of an agent's commands for its commits, beside Git's own
|
|
20470
|
+
* author variables: for the trailer, the harness, the session's transcript, whose last model it
|
|
20471
|
+
* names, and the model the session started with, for a transcript that names none; and what a
|
|
20472
|
+
* forwarder in ref0's hooks folder sets for the repo's own hook it runs, so a forwarder that hook
|
|
20473
|
+
* reaches forwards nothing.
|
|
20474
|
+
*/
|
|
20475
|
+
const gitHookVariables = {
|
|
20476
|
+
harness: "REF0_HARNESS",
|
|
20477
|
+
transcript: "REF0_TRANSCRIPT",
|
|
20478
|
+
model: "REF0_MODEL",
|
|
20479
|
+
forwarded: "REF0_GIT_HOOK"
|
|
20480
|
+
};
|
|
20343
20481
|
//#endregion
|
|
20344
20482
|
//#region src/harness/plugin-files.ts
|
|
20345
20483
|
/**
|
|
@@ -20814,11 +20952,16 @@ const connectLineFor = Effect.fnUntraced(function* (harness, configFolder) {
|
|
|
20814
20952
|
});
|
|
20815
20953
|
//#endregion
|
|
20816
20954
|
//#region src/host-grant.ts
|
|
20817
|
-
/**
|
|
20955
|
+
/**
|
|
20956
|
+
* All lasting authority for one config folder, stored as one keychain value: the agent the folder
|
|
20957
|
+
* acts as, by its owner, handle and agent ID, and its token. The ID is kept in the same write as
|
|
20958
|
+
* the token, so a folder with a grant always knows which agent its commits name.
|
|
20959
|
+
*/
|
|
20818
20960
|
const KeptHostGrant = Schema.Struct({
|
|
20819
20961
|
harness: ConnectHarness,
|
|
20820
20962
|
owner: OwnerName,
|
|
20821
20963
|
agent: AgentHandle,
|
|
20964
|
+
agentId: AgentId,
|
|
20822
20965
|
token: Schema.RedactedFromValue(Schema.String)
|
|
20823
20966
|
});
|
|
20824
20967
|
const decodeGrant = Schema.decodeUnknownOption(Schema.fromJsonString(KeptHostGrant));
|
|
@@ -20865,7 +21008,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
20865
21008
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
20866
21009
|
* and only as a default export.
|
|
20867
21010
|
*/
|
|
20868
|
-
const cliVersion = "0.
|
|
21011
|
+
const cliVersion = "0.9.0";
|
|
20869
21012
|
//#endregion
|
|
20870
21013
|
//#region src/harness/hook-protocol.ts
|
|
20871
21014
|
/**
|
|
@@ -20909,8 +21052,9 @@ const updateLine = (plugin, reinstall) => Option.map(protocolMismatch(plugin, re
|
|
|
20909
21052
|
//#region src/machine.ts
|
|
20910
21053
|
/**
|
|
20911
21054
|
* This machine, as ref0 knows it: an ID made once and kept in ref0's config, `machine` under
|
|
20912
|
-
* `$XDG_CONFIG_HOME/ref0` or `~/.config/ref0`, and its
|
|
20913
|
-
* the person as what it says. Neither is a secret, and neither is proof of
|
|
21055
|
+
* `$XDG_CONFIG_HOME/ref0` or `~/.config/ref0`, and its host name, from which `ref0 connect` makes
|
|
21056
|
+
* the name ref0 shows the person as what it says. Neither is a secret, and neither is proof of
|
|
21057
|
+
* anything.
|
|
20914
21058
|
*/
|
|
20915
21059
|
var MachineError = class extends Schema.TaggedError()("MachineError", { nextStep: Schema.String }) {};
|
|
20916
21060
|
const decodeMachineId = Schema.decodeUnknownOption(MachineId);
|
|
@@ -20938,10 +21082,10 @@ var Machine = class Machine extends Context.Service()("ref0/cli/Machine") {
|
|
|
20938
21082
|
onSome: Effect.succeed
|
|
20939
21083
|
}));
|
|
20940
21084
|
});
|
|
20941
|
-
const
|
|
21085
|
+
const hostName = yield* Effect.sync(() => hostname());
|
|
20942
21086
|
return Machine.of({
|
|
20943
21087
|
id,
|
|
20944
|
-
|
|
21088
|
+
hostName
|
|
20945
21089
|
});
|
|
20946
21090
|
}));
|
|
20947
21091
|
};
|
|
@@ -21020,14 +21164,98 @@ var OldHostGrants = class OldHostGrants extends Context.Service()("ref0/cli/OldH
|
|
|
21020
21164
|
}));
|
|
21021
21165
|
};
|
|
21022
21166
|
//#endregion
|
|
21167
|
+
//#region src/yes-or-no.ts
|
|
21168
|
+
/**
|
|
21169
|
+
* How long what a terminal held while nobody read it takes to arrive once a question reads: one
|
|
21170
|
+
* read, well within this.
|
|
21171
|
+
*/
|
|
21172
|
+
const settle = Duration.millis(50);
|
|
21173
|
+
const again$2 = "Type y or yes, or n or no. Enter alone is no.";
|
|
21174
|
+
/** A line's answer, or none when it's neither yes nor no. */
|
|
21175
|
+
const answerOf$1 = (line) => {
|
|
21176
|
+
const word = line.trim().toLowerCase();
|
|
21177
|
+
if (word === "y" || word === "yes") return Option.some(true);
|
|
21178
|
+
if (word === "" || word === "n" || word === "no") return Option.some(false);
|
|
21179
|
+
return Option.none();
|
|
21180
|
+
};
|
|
21181
|
+
/** What Backspace takes back: one character as the person sees it, an emoji's parts included. */
|
|
21182
|
+
const characters = new Intl.Segmenter();
|
|
21183
|
+
/** `typed` with Backspace pressed once. */
|
|
21184
|
+
const takeBack = (typed) => typed.slice(0, [...characters.segment(typed)].at(-1)?.index);
|
|
21185
|
+
/** Whether a key types text: none of its characters moves the cursor or controls the terminal. */
|
|
21186
|
+
const typesText = (text) => text !== "" && !/\p{Cc}/u.test(text);
|
|
21187
|
+
/** Ctrl-C or Ctrl-D, which a terminal in raw mode hands ref0 as keys, never as a signal. */
|
|
21188
|
+
const quits = (key) => key.ctrl && (key.name === "c" || key.name === "d");
|
|
21189
|
+
/** The explanation goes to stderr, the question to stdout and its answer comes from stdin. */
|
|
21190
|
+
const allTerminals = () => process.stdin.isTTY === true && process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
21191
|
+
/**
|
|
21192
|
+
* Asks yes-or-no questions in this terminal, one after another: each shows `explained` on stderr,
|
|
21193
|
+
* then the question, and waits for a line. What was typed before a question showed doesn't answer
|
|
21194
|
+
* it, unless another question was asked when it arrived: a line typed after the one that answered
|
|
21195
|
+
* a question answers the next. The answer is no, unasked, unless stdin, stdout and stderr are each
|
|
21196
|
+
* a terminal, and on Ctrl-C or Ctrl-D.
|
|
21197
|
+
*/
|
|
21198
|
+
const yesOrNoQuestions = Effect.gen(function* () {
|
|
21199
|
+
const terminal = yield* Terminal.Terminal;
|
|
21200
|
+
const ahead = yield* Ref.make([]);
|
|
21201
|
+
const ask = (explained, question) => Effect.gen(function* () {
|
|
21202
|
+
if (!allTerminals()) return false;
|
|
21203
|
+
const keys = yield* terminal.readInput;
|
|
21204
|
+
yield* Effect.sleep(settle);
|
|
21205
|
+
yield* Queue.clear(keys);
|
|
21206
|
+
const pending = [...yield* Ref.getAndSet(ahead, [])];
|
|
21207
|
+
const next = Effect.suspend(() => {
|
|
21208
|
+
const key = pending.shift();
|
|
21209
|
+
return key === void 0 ? Queue.take(keys) : Effect.succeed(key);
|
|
21210
|
+
});
|
|
21211
|
+
yield* printBlocks([explained]);
|
|
21212
|
+
yield* terminal.display(questionLine(question));
|
|
21213
|
+
let typed = "";
|
|
21214
|
+
let previous = "";
|
|
21215
|
+
while (true) {
|
|
21216
|
+
const pressed = yield* Effect.option(next);
|
|
21217
|
+
if (Option.isNone(pressed) || quits(pressed.value.key)) {
|
|
21218
|
+
yield* terminal.display("\n");
|
|
21219
|
+
return false;
|
|
21220
|
+
}
|
|
21221
|
+
const { input, key } = pressed.value;
|
|
21222
|
+
const ended = key.name === "enter" && previous === "return";
|
|
21223
|
+
previous = key.name;
|
|
21224
|
+
if (ended) continue;
|
|
21225
|
+
if (key.name === "backspace") {
|
|
21226
|
+
if (typed === "") continue;
|
|
21227
|
+
typed = takeBack(typed);
|
|
21228
|
+
yield* terminal.display("\b \b");
|
|
21229
|
+
continue;
|
|
21230
|
+
}
|
|
21231
|
+
if (key.name !== "return" && key.name !== "enter") {
|
|
21232
|
+
const text = Option.getOrElse(input, () => "");
|
|
21233
|
+
if (!typesText(text)) continue;
|
|
21234
|
+
typed += text;
|
|
21235
|
+
yield* terminal.display(text);
|
|
21236
|
+
continue;
|
|
21237
|
+
}
|
|
21238
|
+
yield* terminal.display("\n");
|
|
21239
|
+
const answer = answerOf$1(typed);
|
|
21240
|
+
if (Option.isSome(answer)) {
|
|
21241
|
+
const rest = [...pending, ...yield* Queue.clear(keys)];
|
|
21242
|
+
const ended = key.name === "return" && rest[0]?.key.name === "enter";
|
|
21243
|
+
yield* Ref.set(ahead, ended ? rest.slice(1) : rest);
|
|
21244
|
+
return answer.value;
|
|
21245
|
+
}
|
|
21246
|
+
typed = "";
|
|
21247
|
+
yield* terminal.display(`${again$2}\n${questionLine(question)}`);
|
|
21248
|
+
}
|
|
21249
|
+
}).pipe(Effect.scoped, Effect.orElseSucceed(() => false));
|
|
21250
|
+
return ask;
|
|
21251
|
+
});
|
|
21252
|
+
//#endregion
|
|
21023
21253
|
//#region src/attachment-request.ts
|
|
21024
21254
|
/**
|
|
21025
21255
|
* The machine prints its link and Find code, then collects with its own secret and the answer
|
|
21026
21256
|
* from the approving browser. It keeps and confirms each folder's grant before installing.
|
|
21027
21257
|
*/
|
|
21028
21258
|
const isAnswerCode = Schema.is(TypedApprovalCode);
|
|
21029
|
-
/** A key's escape code, such as an arrow's, as a terminal sends it. */
|
|
21030
|
-
const escapeCode = /\u001b\[[\d;]*[A-Za-z~]/gu;
|
|
21031
21259
|
const decodeTokenId$1 = Schema.decodeUnknownOption(TokenId);
|
|
21032
21260
|
const decodeSecretHash$1 = Schema.decodeUnknownEffect(SecretHash);
|
|
21033
21261
|
/** How often the machine asks whether its person has answered. */
|
|
@@ -21052,6 +21280,19 @@ const keepBudget = Duration.minutes(5);
|
|
|
21052
21280
|
/** A request that couldn't finish: no answer in time, no connection, or ref0 briefly unable. */
|
|
21053
21281
|
var Unanswered = class extends Schema.TaggedError()("Unanswered", {}) {};
|
|
21054
21282
|
const unanswered = () => Effect.fail(new Unanswered());
|
|
21283
|
+
/**
|
|
21284
|
+
* The name the machine sends for itself: its host name without any character that could reorder
|
|
21285
|
+
* or hide what's around it, cut to 64 characters without splitting one in two, or `this machine`
|
|
21286
|
+
* when that leaves none.
|
|
21287
|
+
*/
|
|
21288
|
+
const machineNameOf = (hostName) => {
|
|
21289
|
+
let name = "";
|
|
21290
|
+
for (const character of withoutHiddenCharacters(hostName).trim()) {
|
|
21291
|
+
if (name.length + character.length > 64) break;
|
|
21292
|
+
name += character;
|
|
21293
|
+
}
|
|
21294
|
+
return name || "this machine";
|
|
21295
|
+
};
|
|
21055
21296
|
/** A refusal ref0 gave, said as it gave it. */
|
|
21056
21297
|
const said = (refusal) => sayFailed(refusal.nextStep);
|
|
21057
21298
|
const isEnded = (state) => state.state !== "waiting";
|
|
@@ -21079,6 +21320,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21079
21320
|
const hostGrant = yield* HostGrant;
|
|
21080
21321
|
const oldGrants = yield* OldHostGrants;
|
|
21081
21322
|
const http = yield* HttpClient.HttpClient;
|
|
21323
|
+
const terminal = yield* Terminal.Terminal;
|
|
21082
21324
|
const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
|
|
21083
21325
|
const collecting = yield* freshSecret;
|
|
21084
21326
|
const link = yield* freshLinkSecret;
|
|
@@ -21111,7 +21353,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21111
21353
|
}));
|
|
21112
21354
|
const payload = {
|
|
21113
21355
|
machine: machineId,
|
|
21114
|
-
machineName: machine.
|
|
21356
|
+
machineName: machineNameOf(machine.hostName),
|
|
21115
21357
|
folders,
|
|
21116
21358
|
collectHash: yield* Effect.orDie(decodeSecretHash$1(collecting.hash)),
|
|
21117
21359
|
linkHash: yield* Effect.orDie(decodeSecretHash$1(link.hash))
|
|
@@ -21134,12 +21376,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21134
21376
|
while: Predicate.isTagged("Unanswered"),
|
|
21135
21377
|
times: 2
|
|
21136
21378
|
}), Effect.catchTag("Unanswered", unreachable));
|
|
21137
|
-
yield*
|
|
21379
|
+
yield* printBlocks([[
|
|
21138
21380
|
`Open ${config.api}/connect#${link.secret}`,
|
|
21139
|
-
|
|
21381
|
+
{
|
|
21382
|
+
before: "Or type",
|
|
21383
|
+
label: started.code,
|
|
21384
|
+
text: `in Find under Waiting on you, by ${minute(started.expiresAt)}.`
|
|
21385
|
+
},
|
|
21140
21386
|
"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.",
|
|
21141
21387
|
"Waiting for the answer. Ctrl-C stops it."
|
|
21142
|
-
]
|
|
21388
|
+
]]);
|
|
21389
|
+
let typed = "";
|
|
21390
|
+
const show = (text) => Effect.ignore(terminal.display(text));
|
|
21391
|
+
const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
|
|
21392
|
+
const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
|
|
21143
21393
|
if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
|
|
21144
21394
|
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
|
|
21145
21395
|
const collector = yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http));
|
|
@@ -21151,8 +21401,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21151
21401
|
duration: requestTimeout,
|
|
21152
21402
|
orElse: () => Effect.succeedNone
|
|
21153
21403
|
}), Effect.catchTags({
|
|
21154
|
-
AttachmentNotFound:
|
|
21155
|
-
Unauthenticated:
|
|
21404
|
+
AttachmentNotFound: saidAfterWait,
|
|
21405
|
+
Unauthenticated: saidAfterWait,
|
|
21156
21406
|
AuthorityUnavailable: () => Effect.succeedNone,
|
|
21157
21407
|
HttpClientError: () => Effect.succeedNone,
|
|
21158
21408
|
SchemaError: () => Effect.succeedNone
|
|
@@ -21162,7 +21412,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21162
21412
|
while (true) {
|
|
21163
21413
|
const state = yield* collect;
|
|
21164
21414
|
if (Option.isSome(state) && isEnded(state.value)) return state.value;
|
|
21165
|
-
if ((yield* Clock.currentTimeMillis) > giveUpAt)
|
|
21415
|
+
if ((yield* Clock.currentTimeMillis) > giveUpAt) {
|
|
21416
|
+
yield* endWait;
|
|
21417
|
+
return yield* sayFailed(`ref0 hasn’t said how it ended, so this machine kept nothing. If it was approved, ref0 revokes its grants 10 minutes after it connected. To connect this machine, run: ${again$1}`);
|
|
21418
|
+
}
|
|
21166
21419
|
yield* Effect.sleep(pollMillis);
|
|
21167
21420
|
}
|
|
21168
21421
|
});
|
|
@@ -21174,6 +21427,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21174
21427
|
harness: folder.harness,
|
|
21175
21428
|
owner: connection.owner,
|
|
21176
21429
|
agent: connection.handle,
|
|
21430
|
+
agentId: connection.agent.id,
|
|
21177
21431
|
token: Redacted.make(token)
|
|
21178
21432
|
}).pipe(Effect.timeoutOrElse({
|
|
21179
21433
|
duration: Duration.min(keepTimeout, Duration.millis(left)),
|
|
@@ -21291,23 +21545,42 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21291
21545
|
const connected = yield* finish(isEnded(state) ? state : yield* answered);
|
|
21292
21546
|
yield* printBlocks([[{ label: `Connected as ${connected.owner}’s agents` }, ...connected.kept.map((folder) => `${shownConfigFolder(home, folder.configFolder)} ${folder.handle}`)], ["Stopped, so nothing was installed. To install ref0’s plugin into each, run:", ...connected.kept.map((folder) => ({ command: installLine(folder.configFolder) }))]]);
|
|
21293
21547
|
});
|
|
21294
|
-
const
|
|
21295
|
-
|
|
21296
|
-
yield* Console.error(`Paste the code from your own page on ${config.api}.`);
|
|
21548
|
+
const typedCode = Effect.gen(function* () {
|
|
21549
|
+
const keys = yield* terminal.readInput;
|
|
21297
21550
|
while (true) {
|
|
21298
|
-
const
|
|
21299
|
-
if (
|
|
21300
|
-
|
|
21301
|
-
|
|
21551
|
+
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21552
|
+
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21553
|
+
const { input, key } = pressed.value;
|
|
21554
|
+
if (key.name === "backspace") {
|
|
21555
|
+
if (typed === "") continue;
|
|
21556
|
+
typed = takeBack(typed);
|
|
21557
|
+
yield* show("\b \b");
|
|
21302
21558
|
continue;
|
|
21303
21559
|
}
|
|
21304
|
-
|
|
21305
|
-
|
|
21306
|
-
|
|
21560
|
+
if (key.name !== "return" && key.name !== "enter") {
|
|
21561
|
+
const text = Option.getOrElse(input, () => "");
|
|
21562
|
+
if (!typesText(text)) continue;
|
|
21563
|
+
typed += text;
|
|
21564
|
+
yield* show(text);
|
|
21565
|
+
continue;
|
|
21566
|
+
}
|
|
21567
|
+
const line = typed.trim();
|
|
21568
|
+
if (typed !== "") yield* show("\n");
|
|
21569
|
+
typed = "";
|
|
21570
|
+
if (line === "") continue;
|
|
21571
|
+
if (isAnswerCode(line)) return line;
|
|
21572
|
+
yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
|
|
21307
21573
|
}
|
|
21308
|
-
}).pipe(Effect.
|
|
21574
|
+
}).pipe(Effect.scoped);
|
|
21575
|
+
const readAnswer = Effect.gen(function* () {
|
|
21576
|
+
yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21577
|
+
const code = yield* typedCode;
|
|
21578
|
+
yield* Ref.update(pasted, (first) => first ?? code);
|
|
21579
|
+
if (listener !== void 0) yield* listener.stop;
|
|
21580
|
+
return yield* Effect.never;
|
|
21581
|
+
});
|
|
21309
21582
|
const receiveAnswer = listener === void 0 ? readAnswer : Effect.raceFirst(readAnswer, listener.answer.pipe(Effect.flatMap((code) => Ref.update(pasted, (first) => first ?? code)), Effect.andThen(Effect.never)));
|
|
21310
|
-
return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
|
|
21583
|
+
return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.ensuring(endWait), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
|
|
21311
21584
|
}, Effect.scoped);
|
|
21312
21585
|
//#endregion
|
|
21313
21586
|
//#region src/connect-lock.ts
|
|
@@ -21442,140 +21715,1523 @@ var ConnectLock = class ConnectLock extends Context.Service()("ref0/cli/ConnectL
|
|
|
21442
21715
|
}));
|
|
21443
21716
|
};
|
|
21444
21717
|
//#endregion
|
|
21445
|
-
//#region
|
|
21446
|
-
|
|
21447
|
-
|
|
21448
|
-
|
|
21449
|
-
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
21460
|
-
|
|
21461
|
-
|
|
21462
|
-
|
|
21463
|
-
|
|
21464
|
-
|
|
21718
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/error.js
|
|
21719
|
+
/*!
|
|
21720
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21721
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21722
|
+
*
|
|
21723
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21724
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21725
|
+
*
|
|
21726
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21727
|
+
* list of conditions and the following disclaimer.
|
|
21728
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21729
|
+
* this list of conditions and the following disclaimer in the
|
|
21730
|
+
* documentation and/or other materials provided with the distribution.
|
|
21731
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21732
|
+
* may be used to endorse or promote products derived from this software without
|
|
21733
|
+
* specific prior written permission.
|
|
21734
|
+
*
|
|
21735
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21736
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21737
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21738
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21739
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21740
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21741
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21742
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21743
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21744
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21745
|
+
*/
|
|
21746
|
+
function getLineColFromPtr(string, ptr) {
|
|
21747
|
+
let lines = string.slice(0, ptr).split(/\r?\n/);
|
|
21748
|
+
return [lines.length, lines.pop().length + 1];
|
|
21749
|
+
}
|
|
21750
|
+
function makeCodeBlock(string, line, column) {
|
|
21751
|
+
let lines = string.split(/\r?\n/);
|
|
21752
|
+
let codeblock = "";
|
|
21753
|
+
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
21754
|
+
for (let i = line - 1; i <= line + 1; i++) {
|
|
21755
|
+
let l = lines[i - 1];
|
|
21756
|
+
if (!l) continue;
|
|
21757
|
+
codeblock += i.toString().padEnd(numberLen, " ");
|
|
21758
|
+
codeblock += ": ";
|
|
21759
|
+
codeblock += l;
|
|
21760
|
+
codeblock += "\n";
|
|
21761
|
+
if (i === line) {
|
|
21762
|
+
codeblock += " ".repeat(numberLen + column + 2);
|
|
21763
|
+
codeblock += "^\n";
|
|
21764
|
+
}
|
|
21765
|
+
}
|
|
21766
|
+
return codeblock;
|
|
21767
|
+
}
|
|
21768
|
+
var TomlError = class TomlError extends Error {
|
|
21769
|
+
line;
|
|
21770
|
+
column;
|
|
21771
|
+
codeblock;
|
|
21772
|
+
constructor(message, options) {
|
|
21773
|
+
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
21774
|
+
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
21775
|
+
super(`Invalid TOML document: ${message}\n\n${codeblock}`, options);
|
|
21776
|
+
this.line = line;
|
|
21777
|
+
this.column = column;
|
|
21778
|
+
this.codeblock = codeblock;
|
|
21779
|
+
}
|
|
21780
|
+
/** @internal */
|
|
21781
|
+
static x(message, ctx, ptr) {
|
|
21782
|
+
throw new TomlError(message, {
|
|
21783
|
+
toml: ctx.s,
|
|
21784
|
+
ptr: ptr ?? ctx.p
|
|
21785
|
+
});
|
|
21786
|
+
}
|
|
21787
|
+
};
|
|
21465
21788
|
//#endregion
|
|
21466
|
-
//#region
|
|
21467
|
-
|
|
21468
|
-
*
|
|
21469
|
-
*
|
|
21470
|
-
*
|
|
21471
|
-
*
|
|
21472
|
-
*
|
|
21473
|
-
*
|
|
21474
|
-
*
|
|
21475
|
-
*
|
|
21476
|
-
*
|
|
21477
|
-
|
|
21478
|
-
|
|
21479
|
-
|
|
21480
|
-
|
|
21481
|
-
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
|
|
21490
|
-
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
|
|
21494
|
-
|
|
21495
|
-
|
|
21496
|
-
|
|
21497
|
-
|
|
21498
|
-
|
|
21499
|
-
|
|
21500
|
-
|
|
21501
|
-
|
|
21502
|
-
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21789
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/primitive.js
|
|
21790
|
+
/*!
|
|
21791
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21792
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21793
|
+
*
|
|
21794
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21795
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21796
|
+
*
|
|
21797
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21798
|
+
* list of conditions and the following disclaimer.
|
|
21799
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21800
|
+
* this list of conditions and the following disclaimer in the
|
|
21801
|
+
* documentation and/or other materials provided with the distribution.
|
|
21802
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21803
|
+
* may be used to endorse or promote products derived from this software without
|
|
21804
|
+
* specific prior written permission.
|
|
21805
|
+
*
|
|
21806
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21807
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21808
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21809
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21810
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21811
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21812
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21813
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21814
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21815
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21816
|
+
*/
|
|
21817
|
+
/** @internal */
|
|
21818
|
+
function parseString(ctx) {
|
|
21819
|
+
let startPtr = ctx.p;
|
|
21820
|
+
let c = ctx.s.charCodeAt(ctx.p++);
|
|
21821
|
+
let first = c;
|
|
21822
|
+
let isLiteral = c === 39;
|
|
21823
|
+
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
21824
|
+
if (isMultiline) {
|
|
21825
|
+
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10) ctx.p++;
|
|
21826
|
+
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) ctx.p += 2;
|
|
21827
|
+
}
|
|
21828
|
+
let parsed = "";
|
|
21829
|
+
let sliceStart = ctx.p;
|
|
21830
|
+
let state = 0;
|
|
21831
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
21832
|
+
c = ctx.s.charCodeAt(ctx.p);
|
|
21833
|
+
if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) state = state && 3;
|
|
21834
|
+
else if (c < 32 && c !== 9 || c === 127) TomlError.x("control characters are not allowed in strings", ctx);
|
|
21835
|
+
else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
|
|
21836
|
+
if (isMultiline) {
|
|
21837
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
21838
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first) ctx.p++;
|
|
21839
|
+
}
|
|
21840
|
+
if (!state) {
|
|
21841
|
+
let s = ctx.s.slice(sliceStart, ctx.p);
|
|
21842
|
+
parsed = parsed ? parsed + s : s;
|
|
21843
|
+
}
|
|
21844
|
+
ctx.p += isMultiline ? 3 : 1;
|
|
21845
|
+
return parsed;
|
|
21846
|
+
} else if (!state) {
|
|
21847
|
+
if (!isLiteral && c === 92) {
|
|
21848
|
+
parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
|
|
21849
|
+
state = 1;
|
|
21850
|
+
}
|
|
21851
|
+
} else if (state === 1) {
|
|
21852
|
+
if (c === 120 || c === 117 || c === 85) {
|
|
21853
|
+
let errPtr = ctx.p++ - 1;
|
|
21854
|
+
let value = 0;
|
|
21855
|
+
let len = c === 120 ? 2 : c === 117 ? 4 : 8;
|
|
21856
|
+
for (let j = 0; j < len; j++, ctx.p++) {
|
|
21857
|
+
let hex = ctx.s.charCodeAt(ctx.p);
|
|
21858
|
+
let digit = hex >= 48 && hex <= 57 ? hex - 48 : hex >= 65 && hex <= 70 ? hex - 65 + 10 : hex >= 97 && hex <= 102 ? hex - 97 + 10 : -1;
|
|
21859
|
+
if (digit < 0) TomlError.x("invalid non-hex character in unicode escape", ctx);
|
|
21860
|
+
value = value << 4 | digit;
|
|
21861
|
+
}
|
|
21862
|
+
if (value < 0 || value > 1114111 || value >= 55296 && value <= 57343) TomlError.x("invalid unicode escape", ctx, errPtr);
|
|
21863
|
+
parsed += String.fromCodePoint(value);
|
|
21864
|
+
sliceStart = ctx.p--;
|
|
21865
|
+
state = 0;
|
|
21866
|
+
} else if (isMultiline && (c === 32 || c === 9)) state = 2;
|
|
21867
|
+
else {
|
|
21868
|
+
if (c === 98) parsed += "\b";
|
|
21869
|
+
else if (c === 116) parsed += " ";
|
|
21870
|
+
else if (c === 110) parsed += "\n";
|
|
21871
|
+
else if (c === 102) parsed += "\f";
|
|
21872
|
+
else if (c === 114) parsed += "\r";
|
|
21873
|
+
else if (c === 101) parsed += "\x1B";
|
|
21874
|
+
else if (c === 34) parsed += "\"";
|
|
21875
|
+
else if (c === 92) parsed += "\\";
|
|
21876
|
+
else TomlError.x("unrecognised escape sequence", ctx);
|
|
21877
|
+
sliceStart = ctx.p + 1;
|
|
21878
|
+
state = 0;
|
|
21879
|
+
}
|
|
21880
|
+
} else if (c !== 32 && c !== 9) {
|
|
21881
|
+
if (state === 2) TomlError.x("invalid escape: only line-ending whitespace may be escaped", ctx, sliceStart);
|
|
21882
|
+
state = !isLiteral && c === 92 ? 1 : 0;
|
|
21883
|
+
sliceStart = ctx.p;
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
TomlError.x("unfinished string", ctx, startPtr);
|
|
21887
|
+
}
|
|
21888
|
+
//#endregion
|
|
21889
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/date.js
|
|
21890
|
+
/*!
|
|
21891
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
21892
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21893
|
+
*
|
|
21894
|
+
* Redistribution and use in source and binary forms, with or without
|
|
21895
|
+
* modification, are permitted provided that the following conditions are met:
|
|
21896
|
+
*
|
|
21897
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
21898
|
+
* list of conditions and the following disclaimer.
|
|
21899
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
21900
|
+
* this list of conditions and the following disclaimer in the
|
|
21901
|
+
* documentation and/or other materials provided with the distribution.
|
|
21902
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
21903
|
+
* may be used to endorse or promote products derived from this software without
|
|
21904
|
+
* specific prior written permission.
|
|
21905
|
+
*
|
|
21906
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21907
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
21908
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21909
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21910
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21911
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21912
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
21913
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
21914
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
21915
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21916
|
+
*/
|
|
21917
|
+
let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|z|[-+]\d{2}:\d{2})?$/i;
|
|
21918
|
+
var TomlDate = class TomlDate extends Date {
|
|
21919
|
+
#hasDate = false;
|
|
21920
|
+
#hasTime = false;
|
|
21921
|
+
#offset = null;
|
|
21922
|
+
constructor(date, fasttype, unsafeDelim) {
|
|
21923
|
+
let hasDate = true;
|
|
21924
|
+
let hasTime = true;
|
|
21925
|
+
let offset = "Z";
|
|
21926
|
+
let c;
|
|
21927
|
+
if (typeof date === "string") {
|
|
21928
|
+
if (fasttype) prep: {
|
|
21929
|
+
if (fasttype < 3) {
|
|
21930
|
+
if (+date.slice(11, 13) > 23) {
|
|
21931
|
+
date = "";
|
|
21932
|
+
break prep;
|
|
21933
|
+
}
|
|
21934
|
+
if (fasttype === 2) {
|
|
21935
|
+
offset = null;
|
|
21936
|
+
date += "Z";
|
|
21937
|
+
} else if ((c = date.charCodeAt(date.length - 1)) !== 90 && c !== 122) offset = date.slice(date.length - 6);
|
|
21938
|
+
if (unsafeDelim) date = date.slice(0, 10) + "T" + date.slice(11);
|
|
21939
|
+
} else if (fasttype === 4) date = +date.slice(0, 2) > 23 ? "" : `0000-01-01T${date}Z`;
|
|
21940
|
+
hasDate = fasttype !== 4;
|
|
21941
|
+
hasTime = fasttype !== 3;
|
|
21942
|
+
}
|
|
21943
|
+
else {
|
|
21944
|
+
let match = date.match(DATE_TIME_RE);
|
|
21945
|
+
if (match) {
|
|
21946
|
+
if (!match[1]) {
|
|
21947
|
+
hasDate = false;
|
|
21948
|
+
date = `0000-01-01T${date}`;
|
|
21949
|
+
}
|
|
21950
|
+
hasTime = !!match[2];
|
|
21951
|
+
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
21952
|
+
if (match[2] && +match[2] > 23) date = "";
|
|
21953
|
+
else {
|
|
21954
|
+
offset = match[3] || null;
|
|
21955
|
+
if (!offset && hasTime) date += "Z";
|
|
21956
|
+
}
|
|
21957
|
+
} else date = "";
|
|
21958
|
+
}
|
|
21959
|
+
}
|
|
21960
|
+
super(date);
|
|
21961
|
+
if (!isNaN(this.getTime())) {
|
|
21962
|
+
this.#hasDate = hasDate;
|
|
21963
|
+
this.#hasTime = hasTime;
|
|
21964
|
+
this.#offset = offset;
|
|
21965
|
+
}
|
|
21966
|
+
}
|
|
21967
|
+
isDateTime() {
|
|
21968
|
+
return this.#hasDate && this.#hasTime;
|
|
21969
|
+
}
|
|
21970
|
+
isLocal() {
|
|
21971
|
+
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
21972
|
+
}
|
|
21973
|
+
isDate() {
|
|
21974
|
+
return this.#hasDate && !this.#hasTime;
|
|
21975
|
+
}
|
|
21976
|
+
isTime() {
|
|
21977
|
+
return this.#hasTime && !this.#hasDate;
|
|
21978
|
+
}
|
|
21979
|
+
isValid() {
|
|
21980
|
+
return this.#hasDate || this.#hasTime;
|
|
21981
|
+
}
|
|
21982
|
+
toISOString() {
|
|
21983
|
+
let iso = super.toISOString();
|
|
21984
|
+
if (this.isDate()) return iso.slice(0, 10);
|
|
21985
|
+
if (this.isTime()) return iso.slice(11, 23);
|
|
21986
|
+
if (this.#offset === null) return iso.slice(0, -1);
|
|
21987
|
+
if (this.#offset === "Z" || this.#offset === "z") return iso;
|
|
21988
|
+
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
21989
|
+
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
21990
|
+
return (/* @__PURE__ */ new Date(this.getTime() - offset * 6e4)).toISOString().slice(0, -1) + this.#offset;
|
|
21991
|
+
}
|
|
21992
|
+
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
21993
|
+
let date = new TomlDate(jsDate);
|
|
21994
|
+
date.#offset = offset;
|
|
21995
|
+
return date;
|
|
21996
|
+
}
|
|
21997
|
+
static wrapAsLocalDateTime(jsDate) {
|
|
21998
|
+
let date = new TomlDate(jsDate);
|
|
21999
|
+
date.#offset = null;
|
|
22000
|
+
return date;
|
|
22001
|
+
}
|
|
22002
|
+
static wrapAsLocalDate(jsDate) {
|
|
22003
|
+
let date = new TomlDate(jsDate);
|
|
22004
|
+
date.#hasTime = false;
|
|
22005
|
+
date.#offset = null;
|
|
22006
|
+
return date;
|
|
22007
|
+
}
|
|
22008
|
+
static wrapAsLocalTime(jsDate) {
|
|
22009
|
+
let date = new TomlDate(jsDate);
|
|
22010
|
+
date.#hasDate = false;
|
|
22011
|
+
date.#offset = null;
|
|
22012
|
+
return date;
|
|
22013
|
+
}
|
|
21520
22014
|
};
|
|
21521
|
-
Schema.encodeSync(Schema.fromJsonString(CodexPluginManifest));
|
|
21522
|
-
/** How Codex names a plugin's MCP server in an `mcp_tool` hook. */
|
|
21523
|
-
const codexServer = "ref0";
|
|
21524
|
-
/** Codex's hooks file, by its path inside the plugin. */
|
|
21525
|
-
const codexHooksPath = "hooks/codex-hooks.json";
|
|
21526
|
-
({ ...mcpToolHooks(codexServer, codexHookFields) });
|
|
21527
|
-
/**
|
|
21528
|
-
* The folder of Codex's skills, by its path inside the plugin. Not `skills`, which Claude Code
|
|
21529
|
-
* reads too, where a `status` skill would be a second status command. Codex 0.163.0 skips
|
|
21530
|
-
* Claude Code's `commands/status.md`, since its `!` line is a template it can't run.
|
|
21531
|
-
*/
|
|
21532
|
-
const codexSkillsPath = "codex-skills";
|
|
21533
|
-
/** The status skill, by its path inside the plugin. */
|
|
21534
|
-
const codexStatusSkillPath = `${codexSkillsPath}/status/SKILL.md`;
|
|
21535
|
-
/**
|
|
21536
|
-
* How a person runs the status skill of a plugin installed as `name`: Codex 0.163.0 names a
|
|
21537
|
-
* plugin's skill by the plugin, then the skill.
|
|
21538
|
-
*/
|
|
21539
|
-
const codexStatusShortcut = (name) => `$${name}:status`;
|
|
21540
|
-
/** The skill's placeholder for the plugin's folder, which Codex's agent fills in. */
|
|
21541
|
-
const pluginFolder = "<plugin>";
|
|
21542
|
-
/** What the status skill runs: `ref0 status`, through the plugin's lookup script. */
|
|
21543
|
-
const codexStatusLine = throughLookup([harnessCommandNames.status]).replaceAll(pluginRoot, pluginFolder);
|
|
21544
|
-
[
|
|
21545
|
-
"---",
|
|
21546
|
-
"name: status",
|
|
21547
|
-
`description: Shows whether ref0 follows this Codex session, and what to do next if it doesn’t. Use when the person runs ${codexStatusShortcut("ref0")} or asks whether ref0 is working.`,
|
|
21548
|
-
"---",
|
|
21549
|
-
"",
|
|
21550
|
-
"Run ref0’s check and show the person what it says.",
|
|
21551
|
-
"",
|
|
21552
|
-
`1. This file is \`${pluginFolder}/${codexSkillsPath}/status/SKILL.md\`. Run this command, with \`${pluginFolder}\` replaced by that folder’s absolute path:`,
|
|
21553
|
-
"",
|
|
21554
|
-
" ```sh",
|
|
21555
|
-
` ${codexStatusLine}`,
|
|
21556
|
-
" ```",
|
|
21557
|
-
"",
|
|
21558
|
-
" Run it outside Codex’s sandbox, with `sandbox_permissions: \"require_escalated\"`, since in the sandbox it can’t ask Codex, this machine’s keychain or ref0. If that isn’t allowed, run it in the sandbox.",
|
|
21559
|
-
"2. Show every line it prints as it is, and add nothing. It exits 1 when a line says no: that’s its answer, not an error to fix.",
|
|
21560
|
-
""
|
|
21561
|
-
].join("\n");
|
|
21562
22015
|
//#endregion
|
|
21563
|
-
//#region
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
|
|
21569
|
-
*
|
|
21570
|
-
*
|
|
21571
|
-
|
|
21572
|
-
|
|
21573
|
-
|
|
21574
|
-
|
|
21575
|
-
|
|
21576
|
-
|
|
21577
|
-
|
|
21578
|
-
|
|
22016
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/extract.js
|
|
22017
|
+
/*!
|
|
22018
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22019
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22020
|
+
*
|
|
22021
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22022
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22023
|
+
*
|
|
22024
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22025
|
+
* list of conditions and the following disclaimer.
|
|
22026
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22027
|
+
* this list of conditions and the following disclaimer in the
|
|
22028
|
+
* documentation and/or other materials provided with the distribution.
|
|
22029
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22030
|
+
* may be used to endorse or promote products derived from this software without
|
|
22031
|
+
* specific prior written permission.
|
|
22032
|
+
*
|
|
22033
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22034
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22035
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22036
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22037
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22038
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22039
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22040
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22041
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22042
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22043
|
+
*/
|
|
22044
|
+
function isDigit(char, base = 10) {
|
|
22045
|
+
return base === 16 ? char > 47 && char < 58 || char > 64 && char < 71 || char > 96 && char < 103 : char > 47 && char < 48 + base;
|
|
22046
|
+
}
|
|
22047
|
+
function isEndOfValue(char, delim) {
|
|
22048
|
+
return char === 32 || char === 9 || char === 10 || char === 13 || delim && (char === delim || char === 44) || char === 35;
|
|
22049
|
+
}
|
|
22050
|
+
/** @internal */
|
|
22051
|
+
function extractValue(ctx, end) {
|
|
22052
|
+
let errPtr = ctx.p;
|
|
22053
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
22054
|
+
if (c === 91 || c === 123) {
|
|
22055
|
+
ctx.d-- || TomlError.x("document contains excessively nested structures. aborting.", ctx);
|
|
22056
|
+
let value = c === 91 ? parseArray(ctx) : parseInlineTable(ctx);
|
|
22057
|
+
ctx.d++;
|
|
22058
|
+
return value;
|
|
22059
|
+
}
|
|
22060
|
+
if (c === 34 || c === 39) return parseString(ctx);
|
|
22061
|
+
if (c === 116) {
|
|
22062
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 114 || ctx.s.charCodeAt(++ctx.p) !== 117 || ctx.s.charCodeAt(++ctx.p) !== 101) TomlError.x("invalid value", ctx, errPtr);
|
|
22063
|
+
return ctx.p++, true;
|
|
22064
|
+
}
|
|
22065
|
+
if (c === 102) {
|
|
22066
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 108 || ctx.s.charCodeAt(++ctx.p) !== 115 || ctx.s.charCodeAt(++ctx.p) !== 101) TomlError.x("invalid value", ctx, errPtr);
|
|
22067
|
+
return ctx.p++, false;
|
|
22068
|
+
}
|
|
22069
|
+
if (c === 43 || c === 45) return parseNumber(ctx, ctx.p, ctx.s.charCodeAt(++ctx.p), 44 - c, end);
|
|
22070
|
+
if (ctx.s.charCodeAt(ctx.p + 4) === 45 && ctx.s.charCodeAt(ctx.p + 7) === 45) return parseDate(ctx, c, end);
|
|
22071
|
+
if (ctx.s.charCodeAt(ctx.p + 2) === 58) return parseTime(ctx, c, end);
|
|
22072
|
+
return parseNumber(ctx, ctx.p, c, 0, end);
|
|
22073
|
+
}
|
|
22074
|
+
function parseNumber(ctx, startPtr, startChr, sign, endChr) {
|
|
22075
|
+
let c = startChr;
|
|
22076
|
+
let state = 0;
|
|
22077
|
+
let hasUnderscores = false;
|
|
22078
|
+
if (c === 105) {
|
|
22079
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 110 || ctx.s.charCodeAt(++ctx.p) !== 102) TomlError.x("invalid value", ctx, startPtr);
|
|
22080
|
+
return ctx.p++, (sign || 1) / 0;
|
|
22081
|
+
}
|
|
22082
|
+
if (c === 110) {
|
|
22083
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 110) TomlError.x("invalid value", ctx, startPtr);
|
|
22084
|
+
return ctx.p++, NaN;
|
|
22085
|
+
}
|
|
22086
|
+
if (c === 48) {
|
|
22087
|
+
if (++ctx.p >= ctx.s.length || isEndOfValue(c = ctx.s.charCodeAt(ctx.p), endChr)) return ctx.bi === true ? 0n : 0;
|
|
22088
|
+
if (!sign) {
|
|
22089
|
+
if (c === 120) return parseIntegerBaseN(ctx, startPtr, 16, endChr);
|
|
22090
|
+
else if (c === 98) return parseIntegerBaseN(ctx, startPtr, 2, endChr);
|
|
22091
|
+
else if (c === 111) return parseIntegerBaseN(ctx, startPtr, 8, endChr);
|
|
22092
|
+
}
|
|
22093
|
+
if (c === 46) state = 2;
|
|
22094
|
+
else if (c === 101 || c === 69) state = 4;
|
|
22095
|
+
else TomlError.x("illegal leading zero", ctx, startPtr);
|
|
22096
|
+
} else if (!isDigit(c)) TomlError.x("invalid value", ctx, startPtr);
|
|
22097
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) {
|
|
22098
|
+
if (!state) state = 1;
|
|
22099
|
+
if (c === 95) {
|
|
22100
|
+
if (!(state & 1)) TomlError.x("illegal underscore", ctx);
|
|
22101
|
+
state += 11;
|
|
22102
|
+
hasUnderscores = true;
|
|
22103
|
+
} else if (state === 1 && c === 46) state = 2;
|
|
22104
|
+
else if ((state === 1 || state === 3) && (c === 101 || c === 69)) state = 4;
|
|
22105
|
+
else if (state === 4 && (c === 43 || c === 45)) state = 6;
|
|
22106
|
+
else if (!isDigit(c)) TomlError.x(`illegal character in numeric literal`, ctx);
|
|
22107
|
+
else if (state > 9) state -= 11;
|
|
22108
|
+
else if (!(state & 1)) state++;
|
|
22109
|
+
}
|
|
22110
|
+
if (!state) {
|
|
22111
|
+
let val = (startChr - 48) * (sign || 1);
|
|
22112
|
+
return ctx.bi === true ? BigInt(val) : val;
|
|
22113
|
+
}
|
|
22114
|
+
if (!(state & 1)) TomlError.x("unfinished numeric value", ctx, startPtr);
|
|
22115
|
+
let str = ctx.s.slice(startPtr, ctx.p);
|
|
22116
|
+
if (hasUnderscores) str = str.replaceAll("_", "");
|
|
22117
|
+
return state > 1 ? parseFloat(str) : parseInteger(ctx, str, 10, startPtr);
|
|
22118
|
+
}
|
|
22119
|
+
function parseIntegerBaseN(ctx, startPtr, base, endChr) {
|
|
22120
|
+
let c, underscore = 1;
|
|
22121
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) if (c === 95) {
|
|
22122
|
+
if (underscore & 1) TomlError.x("illegal underscore", ctx);
|
|
22123
|
+
underscore = 3;
|
|
22124
|
+
} else if (!isDigit(c, base)) TomlError.x(`illegal character in numeric literal`, ctx);
|
|
22125
|
+
else if (underscore & 1) underscore--;
|
|
22126
|
+
if (underscore & 1) TomlError.x("unfinished numeric value", ctx);
|
|
22127
|
+
let str = ctx.s.slice(startPtr + 2, ctx.p);
|
|
22128
|
+
if (underscore) str = str.replaceAll("_", "");
|
|
22129
|
+
return parseInteger(ctx, str, base, startPtr);
|
|
22130
|
+
}
|
|
22131
|
+
function parseInteger(ctx, str, base, startPtr) {
|
|
22132
|
+
if (ctx.bi !== true) int: {
|
|
22133
|
+
let val = parseInt(str, base);
|
|
22134
|
+
if (!Number.isSafeInteger(val)) {
|
|
22135
|
+
if (ctx.bi) break int;
|
|
22136
|
+
TomlError.x("integer value cannot be represented losslessly", ctx, startPtr);
|
|
22137
|
+
}
|
|
22138
|
+
return val;
|
|
22139
|
+
}
|
|
22140
|
+
return base === 10 ? BigInt(str) : BigInt((base === 2 ? "0b" : base === 8 ? "0o" : "0x") + str);
|
|
22141
|
+
}
|
|
22142
|
+
function parseDate(ctx, c, endChr) {
|
|
22143
|
+
let startPtr = ctx.p++, unsafeSeparator;
|
|
22144
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++))) return parseNumber(ctx, ctx.p = startPtr, c, 0, endChr);
|
|
22145
|
+
ctx.p += 5;
|
|
22146
|
+
if (!isDigit(ctx.s.charCodeAt(ctx.p++))) TomlError.x("invalid date-time: date part is malformed", ctx, startPtr);
|
|
22147
|
+
if (ctx.p >= ctx.s.length || ((c = ctx.s.charCodeAt(ctx.p)) !== 32 || (unsafeSeparator = true, !isDigit(ctx.s.charCodeAt(ctx.p + 1)))) && c !== 84 && c !== 116) return readDate(ctx, ctx.s.slice(startPtr, ctx.p), 3, false, startPtr);
|
|
22148
|
+
if (ctx.s.charCodeAt(ctx.p += 3) !== 58) TomlError.x("invalid date-time: time part is malformed", ctx, startPtr);
|
|
22149
|
+
if (ctx.s.charCodeAt(ctx.p += 3) === 58) ctx.p += 3;
|
|
22150
|
+
if (ctx.s.charCodeAt(ctx.p) === 46) while (isDigit(ctx.s.charCodeAt(++ctx.p)));
|
|
22151
|
+
if (c = ctx.s.charCodeAt(ctx.p)) {
|
|
22152
|
+
if (c === 90 || c === 122) return readDate(ctx, ctx.s.slice(startPtr, ++ctx.p), 1, unsafeSeparator, startPtr, "[+00:00]");
|
|
22153
|
+
if (c === 43 || c === 45) return readDate(ctx, ctx.s.slice(startPtr, ctx.p += 6), 1, unsafeSeparator, startPtr, !ctx.ld && "[" + ctx.s.slice(ctx.p - 6, ctx.p) + "]");
|
|
22154
|
+
}
|
|
22155
|
+
return readDate(ctx, ctx.s.slice(startPtr, ctx.p), 2, unsafeSeparator, startPtr);
|
|
22156
|
+
}
|
|
22157
|
+
function parseTime(ctx, c, endChr) {
|
|
22158
|
+
let start = ctx.p;
|
|
22159
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(++ctx.p))) return parseNumber(ctx, --ctx.p, c, 0, endChr);
|
|
22160
|
+
if (ctx.s.charCodeAt(ctx.p += 4) === 58) ctx.p += 3;
|
|
22161
|
+
if (ctx.s.charCodeAt(ctx.p) === 46) while (isDigit(ctx.s.charCodeAt(++ctx.p)));
|
|
22162
|
+
return readDate(ctx, ctx.s.slice(start, ctx.p), 4, false, start);
|
|
22163
|
+
}
|
|
22164
|
+
function readDate(ctx, str, type, unsafeDelim, errPtr, temporalSuffix) {
|
|
22165
|
+
if (ctx.ld) {
|
|
22166
|
+
let date = new TomlDate(str, type, unsafeDelim);
|
|
22167
|
+
if (!date.isValid()) TomlError.x("invalid date", ctx, errPtr);
|
|
22168
|
+
return date;
|
|
22169
|
+
}
|
|
22170
|
+
try {
|
|
22171
|
+
if (temporalSuffix) str += temporalSuffix;
|
|
22172
|
+
switch (type) {
|
|
22173
|
+
case 1: return Temporal.ZonedDateTime.from(str);
|
|
22174
|
+
case 2: return Temporal.PlainDateTime.from(str);
|
|
22175
|
+
case 3: return Temporal.PlainDate.from(str);
|
|
22176
|
+
case 4: return Temporal.PlainTime.from(str);
|
|
22177
|
+
}
|
|
22178
|
+
} catch (e) {
|
|
22179
|
+
TomlError.x(e instanceof Error ? e.message : "" + e, ctx, errPtr);
|
|
22180
|
+
}
|
|
22181
|
+
}
|
|
22182
|
+
//#endregion
|
|
22183
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/util.js
|
|
22184
|
+
/*!
|
|
22185
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22186
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22187
|
+
*
|
|
22188
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22189
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22190
|
+
*
|
|
22191
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22192
|
+
* list of conditions and the following disclaimer.
|
|
22193
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22194
|
+
* this list of conditions and the following disclaimer in the
|
|
22195
|
+
* documentation and/or other materials provided with the distribution.
|
|
22196
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22197
|
+
* may be used to endorse or promote products derived from this software without
|
|
22198
|
+
* specific prior written permission.
|
|
22199
|
+
*
|
|
22200
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22201
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22202
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22203
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22204
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22205
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22206
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22207
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22208
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22209
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22210
|
+
*/
|
|
22211
|
+
/** @internal */
|
|
22212
|
+
function skipComment(ctx) {
|
|
22213
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
22214
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
22215
|
+
if (c === 10) break;
|
|
22216
|
+
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
22217
|
+
ctx.p++;
|
|
22218
|
+
break;
|
|
22219
|
+
}
|
|
22220
|
+
if (c < 32 && c !== 9 || c === 127) TomlError.x("control characters are not allowed in comments", ctx);
|
|
22221
|
+
}
|
|
22222
|
+
}
|
|
22223
|
+
/** @internal */
|
|
22224
|
+
function skipVoid(ctx, banNewLines, banComments) {
|
|
22225
|
+
let c;
|
|
22226
|
+
while (ctx.p < ctx.s.length) {
|
|
22227
|
+
while (ctx.p < ctx.s.length && ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10))) ctx.p++;
|
|
22228
|
+
if (banComments || c !== 35) break;
|
|
22229
|
+
skipComment(ctx);
|
|
22230
|
+
}
|
|
22231
|
+
}
|
|
22232
|
+
//#endregion
|
|
22233
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/struct.js
|
|
22234
|
+
/*!
|
|
22235
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22236
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22237
|
+
*
|
|
22238
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22239
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22240
|
+
*
|
|
22241
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22242
|
+
* list of conditions and the following disclaimer.
|
|
22243
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22244
|
+
* this list of conditions and the following disclaimer in the
|
|
22245
|
+
* documentation and/or other materials provided with the distribution.
|
|
22246
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22247
|
+
* may be used to endorse or promote products derived from this software without
|
|
22248
|
+
* specific prior written permission.
|
|
22249
|
+
*
|
|
22250
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22251
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22252
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22253
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22254
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22255
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22256
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22257
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22258
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22259
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22260
|
+
*/
|
|
22261
|
+
/** @internal */
|
|
22262
|
+
function parseKey(ctx, end = 61) {
|
|
22263
|
+
let startPtr;
|
|
22264
|
+
let state = 0;
|
|
22265
|
+
let parsed = [];
|
|
22266
|
+
let sliceStart;
|
|
22267
|
+
let c = ctx.s.charCodeAt(startPtr = ctx.p);
|
|
22268
|
+
do
|
|
22269
|
+
if (c === end) {
|
|
22270
|
+
if (!state) TomlError.x("unexpected end of key", ctx);
|
|
22271
|
+
if (state === 1) parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22272
|
+
return ctx.p++, parsed;
|
|
22273
|
+
} else if (c === 46) {
|
|
22274
|
+
if (!state) TomlError.x("illegal empty bare key", ctx);
|
|
22275
|
+
if (state === 1) parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22276
|
+
state = 0;
|
|
22277
|
+
} else if (!state && (c === 34 || c === 39)) {
|
|
22278
|
+
if (c === ctx.s.charCodeAt(ctx.p + 1) && c === ctx.s.charCodeAt(ctx.p + 2)) TomlError.x("illegal quoted key: multiline strings are not allowed", ctx);
|
|
22279
|
+
parsed.push(parseString(ctx));
|
|
22280
|
+
state = 2;
|
|
22281
|
+
ctx.p--;
|
|
22282
|
+
} else if (c === 32 || c === 9) {
|
|
22283
|
+
if (state === 1) {
|
|
22284
|
+
parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
22285
|
+
state = 2;
|
|
22286
|
+
}
|
|
22287
|
+
} else if (state === 2 || c < 48 && c !== 45 || c > 57 && c < 65 || c > 90 && c < 97 && c !== 95 || c > 122) TomlError.x("illegal character in key", ctx);
|
|
22288
|
+
else if (!state) {
|
|
22289
|
+
state = 1;
|
|
22290
|
+
sliceStart = ctx.p;
|
|
22291
|
+
}
|
|
22292
|
+
while (c = ctx.s.charCodeAt(++ctx.p));
|
|
22293
|
+
TomlError.x("incomplete key-value: cannot find end of key", ctx, startPtr);
|
|
22294
|
+
}
|
|
22295
|
+
/** @internal */
|
|
22296
|
+
function parseInlineTable(ctx) {
|
|
22297
|
+
let startPtr = ctx.p++;
|
|
22298
|
+
let res = Object.create(null);
|
|
22299
|
+
let seen = /* @__PURE__ */ new Set();
|
|
22300
|
+
let c;
|
|
22301
|
+
while (ctx.p < ctx.s.length) {
|
|
22302
|
+
skipVoid(ctx);
|
|
22303
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 125) {
|
|
22304
|
+
ctx.p++;
|
|
22305
|
+
return res;
|
|
22306
|
+
}
|
|
22307
|
+
let k;
|
|
22308
|
+
let t = res;
|
|
22309
|
+
let hasOwn = false;
|
|
22310
|
+
let errPtr = ctx.p;
|
|
22311
|
+
let key = parseKey(ctx);
|
|
22312
|
+
for (let i = 0; i < key.length; i++) {
|
|
22313
|
+
if (i) t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22314
|
+
k = key[i];
|
|
22315
|
+
if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
22316
|
+
let unsafe = k === "__proto__";
|
|
22317
|
+
if (ctx.uk && (unsafe || k === "constructor")) {
|
|
22318
|
+
t = ctx.uk !== 1 && TomlError.x("document contains an unsafe property", ctx, errPtr);
|
|
22319
|
+
break;
|
|
22320
|
+
}
|
|
22321
|
+
if (!hasOwn && unsafe) Object.defineProperty(t, k, {
|
|
22322
|
+
enumerable: true,
|
|
22323
|
+
configurable: true,
|
|
22324
|
+
writable: true
|
|
22325
|
+
});
|
|
22326
|
+
}
|
|
22327
|
+
if (hasOwn) TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
22328
|
+
skipVoid(ctx, true, true);
|
|
22329
|
+
let value = extractValue(ctx, 125);
|
|
22330
|
+
if (t && typeof (t[k] = value) === "object") seen.add(value);
|
|
22331
|
+
skipVoid(ctx);
|
|
22332
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 125) return res;
|
|
22333
|
+
if (c !== 44) TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
22334
|
+
}
|
|
22335
|
+
TomlError.x("unfinished table", ctx, startPtr);
|
|
22336
|
+
}
|
|
22337
|
+
/** @internal */
|
|
22338
|
+
function parseArray(ctx) {
|
|
22339
|
+
let startPtr = ctx.p++;
|
|
22340
|
+
let res = [];
|
|
22341
|
+
let c;
|
|
22342
|
+
while (ctx.p < ctx.s.length) {
|
|
22343
|
+
skipVoid(ctx);
|
|
22344
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 93) {
|
|
22345
|
+
ctx.p++;
|
|
22346
|
+
return res;
|
|
22347
|
+
}
|
|
22348
|
+
res.push(extractValue(ctx, 93));
|
|
22349
|
+
skipVoid(ctx);
|
|
22350
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 93) return res;
|
|
22351
|
+
if (c !== 44) TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
22352
|
+
}
|
|
22353
|
+
TomlError.x("unfinished array", ctx, startPtr);
|
|
22354
|
+
}
|
|
22355
|
+
//#endregion
|
|
22356
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/parse.js
|
|
22357
|
+
/*!
|
|
22358
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22359
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22360
|
+
*
|
|
22361
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22362
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22363
|
+
*
|
|
22364
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22365
|
+
* list of conditions and the following disclaimer.
|
|
22366
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22367
|
+
* this list of conditions and the following disclaimer in the
|
|
22368
|
+
* documentation and/or other materials provided with the distribution.
|
|
22369
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22370
|
+
* may be used to endorse or promote products derived from this software without
|
|
22371
|
+
* specific prior written permission.
|
|
22372
|
+
*
|
|
22373
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22374
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22375
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22376
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22377
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22378
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22379
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22380
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22381
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22382
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22383
|
+
*/
|
|
22384
|
+
function peekTable(ctx, key, table, meta, type) {
|
|
22385
|
+
let t = table;
|
|
22386
|
+
let m = meta;
|
|
22387
|
+
let k;
|
|
22388
|
+
let hasOwn = false;
|
|
22389
|
+
let state;
|
|
22390
|
+
for (let i = 0; i < key.length; i++) {
|
|
22391
|
+
if (i) {
|
|
22392
|
+
t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22393
|
+
m = (state = m[k]).c;
|
|
22394
|
+
if (type === 0 && (state.t === 1 || state.t === 2)) return null;
|
|
22395
|
+
if (state.t === 2) {
|
|
22396
|
+
let l = t.length - 1;
|
|
22397
|
+
t = t[l];
|
|
22398
|
+
m = m[l].c;
|
|
22399
|
+
}
|
|
22400
|
+
}
|
|
22401
|
+
k = key[i];
|
|
22402
|
+
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) return null;
|
|
22403
|
+
if (!hasOwn) {
|
|
22404
|
+
let unsafe = k === "__proto__";
|
|
22405
|
+
if (ctx.uk && (unsafe || k === "constructor")) return false;
|
|
22406
|
+
if (unsafe) {
|
|
22407
|
+
Object.defineProperty(t, k, {
|
|
22408
|
+
enumerable: true,
|
|
22409
|
+
configurable: true,
|
|
22410
|
+
writable: true
|
|
22411
|
+
});
|
|
22412
|
+
Object.defineProperty(m, k, {
|
|
22413
|
+
enumerable: true,
|
|
22414
|
+
configurable: true,
|
|
22415
|
+
writable: true
|
|
22416
|
+
});
|
|
22417
|
+
}
|
|
22418
|
+
m[k] = {
|
|
22419
|
+
t: i < key.length - 1 && type === 2 ? 3 : type,
|
|
22420
|
+
d: false,
|
|
22421
|
+
i: 0,
|
|
22422
|
+
c: Object.create(null)
|
|
22423
|
+
};
|
|
22424
|
+
}
|
|
22425
|
+
}
|
|
22426
|
+
state = m[k];
|
|
22427
|
+
if (state.t !== type && !(type === 1 && state.t === 3)) return null;
|
|
22428
|
+
if (type === 2) {
|
|
22429
|
+
if (!state.d) {
|
|
22430
|
+
state.d = true;
|
|
22431
|
+
t[k] = [];
|
|
22432
|
+
}
|
|
22433
|
+
t[k].push(t = Object.create(null));
|
|
22434
|
+
state.c[state.i++] = state = {
|
|
22435
|
+
t: 1,
|
|
22436
|
+
d: false,
|
|
22437
|
+
i: 0,
|
|
22438
|
+
c: Object.create(null)
|
|
22439
|
+
};
|
|
22440
|
+
}
|
|
22441
|
+
if (state.d) return null;
|
|
22442
|
+
state.d = true;
|
|
22443
|
+
if (type === 1) t = hasOwn ? t[k] : t[k] = Object.create(null);
|
|
22444
|
+
else if (type === 0 && hasOwn) return null;
|
|
22445
|
+
return [
|
|
22446
|
+
k,
|
|
22447
|
+
t,
|
|
22448
|
+
state.c
|
|
22449
|
+
];
|
|
22450
|
+
}
|
|
22451
|
+
function validateTablePeek(ctx, peek, ptr) {
|
|
22452
|
+
if (peek === null || ctx.uk === 2) TomlError.x(peek === null ? "trying to redefine an already defined table or value" : "document contains an unsafe property", ctx, ptr);
|
|
22453
|
+
}
|
|
22454
|
+
function parse(toml, options = {}) {
|
|
22455
|
+
let ctx = {
|
|
22456
|
+
s: toml,
|
|
22457
|
+
p: 0,
|
|
22458
|
+
d: options.maxDepth ?? 1e3,
|
|
22459
|
+
bi: options.integersAsBigInt ?? false,
|
|
22460
|
+
ld: options.useLegacyDate ?? true,
|
|
22461
|
+
uk: options.unsafeKeyBehaviour === "throw" ? 2 : options.unsafeKeyBehaviour === "drop" ? 1 : 0
|
|
22462
|
+
};
|
|
22463
|
+
let res = Object.create(null);
|
|
22464
|
+
let meta = Object.create(null);
|
|
22465
|
+
let tmp;
|
|
22466
|
+
let skipping = false;
|
|
22467
|
+
let tbl = res;
|
|
22468
|
+
let m = meta;
|
|
22469
|
+
if (toml.charCodeAt(0) === 65279) ctx.p++;
|
|
22470
|
+
skipVoid(ctx);
|
|
22471
|
+
while (ctx.p < toml.length) {
|
|
22472
|
+
if (toml.charCodeAt(ctx.p) === 91) {
|
|
22473
|
+
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
22474
|
+
tmp = ctx.p += +isTableArray;
|
|
22475
|
+
skipping = false;
|
|
22476
|
+
let k = parseKey(ctx, 93);
|
|
22477
|
+
if (isTableArray) {
|
|
22478
|
+
if (toml.charCodeAt(ctx.p) !== 93) TomlError.x("expected end of table array declaration", ctx);
|
|
22479
|
+
ctx.p++;
|
|
22480
|
+
}
|
|
22481
|
+
let p = peekTable(ctx, k, res, meta, isTableArray ? 2 : 1);
|
|
22482
|
+
if (!p) {
|
|
22483
|
+
validateTablePeek(ctx, p, tmp);
|
|
22484
|
+
skipping = true;
|
|
22485
|
+
} else {
|
|
22486
|
+
m = p[2];
|
|
22487
|
+
tbl = p[1];
|
|
22488
|
+
}
|
|
22489
|
+
} else {
|
|
22490
|
+
tmp = ctx.p;
|
|
22491
|
+
let p = peekTable(ctx, parseKey(ctx), tbl, m, 0);
|
|
22492
|
+
if (!p && !skipping) validateTablePeek(ctx, p, tmp);
|
|
22493
|
+
skipVoid(ctx, true, true);
|
|
22494
|
+
let v = extractValue(ctx, void 0);
|
|
22495
|
+
if (p && !skipping) p[1][p[0]] = v;
|
|
22496
|
+
}
|
|
22497
|
+
skipVoid(ctx, true);
|
|
22498
|
+
if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && (tmp !== 13 || toml.charCodeAt(ctx.p + 1) !== 10)) TomlError.x("each key-value declaration must be followed by an end-of-line", ctx);
|
|
22499
|
+
skipVoid(ctx);
|
|
22500
|
+
}
|
|
22501
|
+
return res;
|
|
22502
|
+
}
|
|
22503
|
+
/*!
|
|
22504
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22505
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22506
|
+
*
|
|
22507
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22508
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22509
|
+
*
|
|
22510
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22511
|
+
* list of conditions and the following disclaimer.
|
|
22512
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22513
|
+
* this list of conditions and the following disclaimer in the
|
|
22514
|
+
* documentation and/or other materials provided with the distribution.
|
|
22515
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22516
|
+
* may be used to endorse or promote products derived from this software without
|
|
22517
|
+
* specific prior written permission.
|
|
22518
|
+
*
|
|
22519
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22520
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22521
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22522
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22523
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22524
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22525
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22526
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22527
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22528
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22529
|
+
*/
|
|
22530
|
+
"".isWellFormed;
|
|
22531
|
+
//#endregion
|
|
22532
|
+
//#region ../../node_modules/.pnpm/smol-toml@1.9.1/node_modules/smol-toml/dist/index.js
|
|
22533
|
+
/*!
|
|
22534
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
22535
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
22536
|
+
*
|
|
22537
|
+
* Redistribution and use in source and binary forms, with or without
|
|
22538
|
+
* modification, are permitted provided that the following conditions are met:
|
|
22539
|
+
*
|
|
22540
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
22541
|
+
* list of conditions and the following disclaimer.
|
|
22542
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22543
|
+
* this list of conditions and the following disclaimer in the
|
|
22544
|
+
* documentation and/or other materials provided with the distribution.
|
|
22545
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
22546
|
+
* may be used to endorse or promote products derived from this software without
|
|
22547
|
+
* specific prior written permission.
|
|
22548
|
+
*
|
|
22549
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22550
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22551
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22552
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22553
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22554
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22555
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22556
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
22557
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
22558
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22559
|
+
*/
|
|
22560
|
+
//#endregion
|
|
22561
|
+
//#region src/harness/codex-checkouts.ts
|
|
22562
|
+
const codexGitFolder = (configFolder, pluginId) => {
|
|
22563
|
+
const at = pluginId.indexOf("@");
|
|
22564
|
+
return `${configFolder}/plugins/cache/${pluginId.slice(at + 1)}/${pluginId.slice(0, at)}/@git`;
|
|
22565
|
+
};
|
|
22566
|
+
const codexCheckoutsFolder = (gitFolder) => `${gitFolder}/checkouts`;
|
|
22567
|
+
/** Parse Git's quoted parameters as data, never shell code, and keep every other `-c`. */
|
|
22568
|
+
const withoutCodexInclude = String.raw`
|
|
22569
|
+
use Text::ParseWords qw(shellwords);
|
|
22570
|
+
my @parameters = shellwords($ENV{GIT_CONFIG_PARAMETERS} // '');
|
|
22571
|
+
my $root = '';
|
|
22572
|
+
@parameters = grep { if (m{^include\.path=(.*/plugins/cache/[^/]+/[^/]+/\@git)/agents/[^/]+\.gitconfig$}) { $root = $1; 0 } else { 1 } } @parameters;
|
|
22573
|
+
if (($ARGV[0] // '') eq 'root') { print $root; exit; }
|
|
22574
|
+
for (@parameters) { s/'/'\\''/g; $_ = "'$_'"; }
|
|
22575
|
+
print join(' ', @parameters);
|
|
22576
|
+
`;
|
|
22577
|
+
/**
|
|
22578
|
+
* Both session start and the sign-off refusal use this writer. Perl's flock works on Linux
|
|
22579
|
+
* and macOS and is released even after a crash. One lock covers reading the shared includes,
|
|
22580
|
+
* writing a checkout's overrides, replacing the includes, and restoring the agent's file.
|
|
22581
|
+
* Git writes the include keys so unusual checkout names can't become config syntax. Its
|
|
22582
|
+
* gitdir conditions escape wildcard characters and cover the main checkout and worktrees.
|
|
22583
|
+
*/
|
|
22584
|
+
const codexCheckoutWriter = String.raw`
|
|
22585
|
+
use strict;
|
|
22586
|
+
use warnings;
|
|
22587
|
+
use Fcntl qw(LOCK_EX);
|
|
22588
|
+
use File::Path qw(make_path);
|
|
22589
|
+
use File::Temp qw(tempfile);
|
|
22590
|
+
use Digest::SHA qw(sha256_hex);
|
|
22591
|
+
umask 0077;
|
|
22592
|
+
my ($directory, $common, $hooks, $signs_off, $author_file, $author_text, $clear_author, $activate_author) = @ARGV;
|
|
22593
|
+
make_path($directory);
|
|
22594
|
+
open my $lock, '>>', "$directory/lock" or die "open checkout lock: $!\n";
|
|
22595
|
+
$SIG{ALRM} = sub { die "checkout lock timed out\n" };
|
|
22596
|
+
alarm 5;
|
|
22597
|
+
flock($lock, LOCK_EX) or die "lock checkouts: $!\n";
|
|
22598
|
+
alarm 0;
|
|
22599
|
+
sub quoted {
|
|
22600
|
+
my $value = shift;
|
|
22601
|
+
$value =~ s/\\/\\\\/g;
|
|
22602
|
+
$value =~ s/"/\\"/g;
|
|
22603
|
+
$value =~ s/\n/\\n/g;
|
|
22604
|
+
$value =~ s/\t/\\t/g;
|
|
22605
|
+
$value =~ s/\x08/\\b/g;
|
|
22606
|
+
return qq{"$value"};
|
|
22607
|
+
}
|
|
22608
|
+
sub replace_file {
|
|
22609
|
+
my ($file, $text) = @_;
|
|
22610
|
+
my ($out, $next) = tempfile('.ref0-XXXXXX', DIR => $directory, UNLINK => 1);
|
|
22611
|
+
print {$out} $text or die "write config: $!\n";
|
|
22612
|
+
close $out or die "close config: $!\n";
|
|
22613
|
+
rename $next, $file or die "replace config: $!\n";
|
|
22614
|
+
}
|
|
22615
|
+
my $disabled = "# ref0: author disabled until commit setup succeeds\n";
|
|
22616
|
+
if (defined $clear_author && length $clear_author) {
|
|
22617
|
+
my $text = $author_text // '';
|
|
22618
|
+
if (-e $clear_author) {
|
|
22619
|
+
open my $in, '<', $clear_author or die "read author: $!\n";
|
|
22620
|
+
local $/;
|
|
22621
|
+
$text = <$in>;
|
|
22622
|
+
close $in or die "close author: $!\n";
|
|
22623
|
+
}
|
|
22624
|
+
$text =~ s/^\[author\]\n.*?(?=^\[|\z)//msg;
|
|
22625
|
+
$text = $disabled . $text unless index($text, $disabled) == 0;
|
|
22626
|
+
replace_file($clear_author, $text);
|
|
22627
|
+
exit;
|
|
22628
|
+
}
|
|
22629
|
+
if (length $common) {
|
|
22630
|
+
my $checkout = "$directory/" . sha256_hex($common) . '.gitconfig';
|
|
22631
|
+
my $text = "[core]\n\thooksPath = " . quoted($hooks) . "\n";
|
|
22632
|
+
$text .= "[author]\n\tname = \"\"\n\temail = \"\"\n" if $signs_off;
|
|
22633
|
+
my $includes = "$directory/checkouts.gitconfig";
|
|
22634
|
+
my $before = '';
|
|
22635
|
+
if (-e $includes) {
|
|
22636
|
+
open my $in, '<', $includes or die "read checkouts: $!\n";
|
|
22637
|
+
local $/;
|
|
22638
|
+
$before = <$in>;
|
|
22639
|
+
close $in or die "close checkouts: $!\n";
|
|
22640
|
+
}
|
|
22641
|
+
my ($out, $next) = tempfile('.ref0-XXXXXX', DIR => $directory, UNLINK => 1);
|
|
22642
|
+
print {$out} $before or die "write checkouts: $!\n";
|
|
22643
|
+
close $out or die "close checkouts: $!\n";
|
|
22644
|
+
(my $pattern = $common) =~ s/([\\*?\[])/\\$1/g;
|
|
22645
|
+
for my $gitdir ($pattern, "$pattern/worktrees/") {
|
|
22646
|
+
system('git', 'config', '--file', $next, '--replace-all', "includeIf.gitdir:$gitdir.path", $checkout) == 0
|
|
22647
|
+
or die "write checkout include\n";
|
|
22648
|
+
}
|
|
22649
|
+
replace_file($checkout, $text);
|
|
22650
|
+
rename $next, $includes or die "replace checkouts: $!\n";
|
|
22651
|
+
}
|
|
22652
|
+
if (defined $author_file && length $author_file) {
|
|
22653
|
+
# An in-flight old session cannot restore an author cleared after a failed switch.
|
|
22654
|
+
my $may_write = 1;
|
|
22655
|
+
if (!$activate_author && -e $author_file) {
|
|
22656
|
+
open my $in, '<', $author_file or die "read author: $!\n";
|
|
22657
|
+
my $first = <$in> // '';
|
|
22658
|
+
close $in or die "close author: $!\n";
|
|
22659
|
+
$may_write = 0 if $first eq $disabled;
|
|
22660
|
+
}
|
|
22661
|
+
replace_file($author_file, $author_text) if $may_write;
|
|
22662
|
+
}
|
|
22663
|
+
`;
|
|
22664
|
+
/** The same writer inside a hook, where the next plain commit must use the person as author. */
|
|
22665
|
+
const codexSignOff = [
|
|
22666
|
+
"ref0_common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1",
|
|
22667
|
+
"ref0_common=$(cd -P \"$ref0_common\" && pwd) || exit 1",
|
|
22668
|
+
"[ -n \"${ref0_codex-}\" ] || exit 1",
|
|
22669
|
+
"ref0_codex_hooks=$ref0_hooks/most",
|
|
22670
|
+
"for ref0_frequent in post-index-change reference-transaction; do",
|
|
22671
|
+
" ref0_own=$(git rev-parse --git-path \"hooks/$ref0_frequent\") || exit 1",
|
|
22672
|
+
" [ ! -x \"$ref0_own\" ] || ref0_codex_hooks=$ref0_hooks/all",
|
|
22673
|
+
"done",
|
|
22674
|
+
`perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
|
|
22675
|
+
];
|
|
22676
|
+
//#endregion
|
|
22677
|
+
//#region src/harness/empty-folder.ts
|
|
22678
|
+
/**
|
|
22679
|
+
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22680
|
+
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22681
|
+
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22682
|
+
*
|
|
22683
|
+
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22684
|
+
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22685
|
+
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22686
|
+
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22687
|
+
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22688
|
+
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22689
|
+
*/
|
|
22690
|
+
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22691
|
+
//#endregion
|
|
22692
|
+
//#region src/harness/codex-app-server.ts
|
|
22693
|
+
/**
|
|
22694
|
+
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22695
|
+
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22696
|
+
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
22697
|
+
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
22698
|
+
* didn't send, is passed over.
|
|
22699
|
+
*/
|
|
22700
|
+
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
22701
|
+
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
22702
|
+
/** Codex's app server answered a request with an error. */
|
|
22703
|
+
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
22704
|
+
method: Schema.String,
|
|
22705
|
+
error: CodexError
|
|
22706
|
+
}) {};
|
|
22707
|
+
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
22708
|
+
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
22709
|
+
method: Schema.String,
|
|
22710
|
+
issue: Schema.String
|
|
22711
|
+
}) {};
|
|
22712
|
+
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
22713
|
+
const decodeAnswer$1 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
22714
|
+
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
22715
|
+
const initializeId = "ref0-initialize";
|
|
22716
|
+
const askedId = "ref0-asked";
|
|
22717
|
+
/** The exchange's opening: who's asking, then that it's ready. */
|
|
22718
|
+
const opening = [{
|
|
22719
|
+
jsonrpc: "2.0",
|
|
22720
|
+
id: initializeId,
|
|
22721
|
+
method: codexMethods.initialize.method,
|
|
22722
|
+
params: { clientInfo: {
|
|
22723
|
+
name: "ref0",
|
|
22724
|
+
title: null,
|
|
22725
|
+
version: cliVersion
|
|
22726
|
+
} }
|
|
22727
|
+
}, {
|
|
22728
|
+
jsonrpc: "2.0",
|
|
22729
|
+
method: codexMethods.initialized.method
|
|
22730
|
+
}];
|
|
22731
|
+
/**
|
|
22732
|
+
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
22733
|
+
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
22734
|
+
*/
|
|
22735
|
+
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
22736
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
22737
|
+
const fs = yield* FileSystem.FileSystem;
|
|
22738
|
+
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
22739
|
+
cwd: yield* emptyFolder(fs),
|
|
22740
|
+
env,
|
|
22741
|
+
extendEnv: true,
|
|
22742
|
+
forceKillAfter: "2 seconds",
|
|
22743
|
+
stdin: {
|
|
22744
|
+
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
22745
|
+
endOnDone: false
|
|
22746
|
+
}
|
|
22747
|
+
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$1(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
22748
|
+
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
22749
|
+
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
22750
|
+
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
22751
|
+
const resultOf = (method, answer) => {
|
|
22752
|
+
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
22753
|
+
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
22754
|
+
method: method.method,
|
|
22755
|
+
error: answer.error
|
|
22756
|
+
}));
|
|
22757
|
+
const { result } = method;
|
|
22758
|
+
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
22759
|
+
method: method.method,
|
|
22760
|
+
issue: error.message
|
|
22761
|
+
})));
|
|
22762
|
+
};
|
|
22763
|
+
/**
|
|
22764
|
+
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
22765
|
+
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
22766
|
+
* its request's ID, and its result is read by the method's result schema.
|
|
22767
|
+
*/
|
|
22768
|
+
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
22769
|
+
const asked = {
|
|
22770
|
+
jsonrpc: "2.0",
|
|
22771
|
+
id: askedId,
|
|
22772
|
+
method: method.method,
|
|
22773
|
+
params
|
|
22774
|
+
};
|
|
22775
|
+
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
22776
|
+
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
22777
|
+
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
22778
|
+
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
22779
|
+
return yield* resultOf(method, answers.value.get(askedId));
|
|
22780
|
+
});
|
|
22781
|
+
//#endregion
|
|
22782
|
+
//#region src/harness/commit-hooks.ts
|
|
22783
|
+
/**
|
|
22784
|
+
* ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
|
|
22785
|
+
* ref0's hooks folder run once the repo's own hook succeeds, and only in an agent's commands,
|
|
22786
|
+
* where `REF0_HARNESS` is set. A commit anywhere else is unchanged.
|
|
22787
|
+
*
|
|
22788
|
+
* The trailer, `Assisted-by: <harness>:<model>`, goes on once with `git interpret-trailers`:
|
|
22789
|
+
* `prepare-commit-msg` adds it to a message the command or Git wrote, and `commit-msg` to one
|
|
22790
|
+
* written in an editor, or again if an editor removed it, since Git runs `prepare-commit-msg`
|
|
22791
|
+
* before the editor and `commit-msg` after. Neither adds it to a message Git refuses as empty, or
|
|
22792
|
+
* to `commit.template` left as it was, which Git would otherwise commit. Two commits made in an
|
|
22793
|
+
* editor are beyond them: one with `--no-verify`, which skips `commit-msg`, gets no trailer, and
|
|
22794
|
+
* a template from `-t`, which `commit-msg` can't see, gets it even when left as it was. The model
|
|
22795
|
+
* is the last one the session's transcript names, read from its end in `sh`, with no Node
|
|
22796
|
+
* process, or else the one the session started with. Both are as the harness reports them, never
|
|
22797
|
+
* proven.
|
|
22798
|
+
*
|
|
22799
|
+
* The refusal: a commit whose author is an agent's commit email and whose message has a
|
|
22800
|
+
* `Signed-off-by`. A sign-off must match its commit's author, and an agent never signs off.
|
|
22801
|
+
* `prepare-commit-msg` catches `-s` and `-m`, which `--no-verify` doesn't skip, and `commit-msg`
|
|
22802
|
+
* a sign-off written in an editor, unless `--no-verify` skips it.
|
|
22803
|
+
*
|
|
22804
|
+
* Only the refusal stops a commit: when Git can't say what ref0's part needs, such as a Git older
|
|
22805
|
+
* than 2.20 without `--no-divider`, the commit goes on without the trailer.
|
|
22806
|
+
*
|
|
22807
|
+
* Neither part runs while Git replays commits, in a rebase or a cherry-pick: Git makes those
|
|
22808
|
+
* commits itself with their own authors, and these hooks see the command's author instead (Git
|
|
22809
|
+
* 2.53, 23 Oct 2026). Nor does the trailer go on an amend that keeps another's commit: `git
|
|
22810
|
+
* commit` gives its hooks the author it commits with, and an amend keeps HEAD's, to the second.
|
|
22811
|
+
* So a new commit by HEAD's author in HEAD's second gets none either.
|
|
22812
|
+
*/
|
|
22813
|
+
/** The hooks ref0 adds its part to, after the repo's own. */
|
|
22814
|
+
const commitHooks = ["prepare-commit-msg", "commit-msg"];
|
|
22815
|
+
/** Whether a hook is one ref0 adds its part to. */
|
|
22816
|
+
const isCommitHook = (name) => commitHooks.some((hook) => hook === name);
|
|
22817
|
+
/** The trailer's key, as the Linux kernel writes it. */
|
|
22818
|
+
const assistedBy = "Assisted-by";
|
|
22819
|
+
/** How much of the transcript's end the hook reads for its last model: well past its last turn. */
|
|
22820
|
+
const transcriptTail = 1048576;
|
|
22821
|
+
/**
|
|
22822
|
+
* Git's trailer tool, reading and writing `:` alone whatever the repo's `trailer.separators`
|
|
22823
|
+
* says, since Git writes a sign-off with `:`.
|
|
22824
|
+
*/
|
|
22825
|
+
const trailers = "git -c trailer.separators=: interpret-trailers --no-divider";
|
|
22826
|
+
/**
|
|
22827
|
+
* Whether its input says anything, as Git decides whether a message is empty: with a line that's
|
|
22828
|
+
* neither blank nor a sign-off.
|
|
22829
|
+
*/
|
|
22830
|
+
const saysSomething = "grep -qv -e '^[[:space:]]*$' -e '^Signed-off-by: '";
|
|
22831
|
+
/** How the agent commits again as the person, which the refusal prints. */
|
|
22832
|
+
const asThePerson = "env -u GIT_AUTHOR_NAME -u GIT_AUTHOR_EMAIL git commit -s …";
|
|
22833
|
+
/** What the refusal prints, for a commit by `author` that the person `person` should author. */
|
|
22834
|
+
const signOffRefusal = (author, person) => `ref0: this commit has a Signed-off-by, and its author is ${author}, an agent. A sign-off must match its commit's author, and an agent never signs off, so commit again with ${person} as the author, adding --reset-author to an amend:\n ${asThePerson}\n`;
|
|
22835
|
+
/** ref0's part of `hook`, run once the repo's own hook has succeeded. */
|
|
22836
|
+
const commitPart = (hook) => {
|
|
22837
|
+
const { harness, transcript, model } = gitHookVariables;
|
|
22838
|
+
return [
|
|
22839
|
+
"ref0_nl='",
|
|
22840
|
+
"'",
|
|
22841
|
+
`[ -n "\${${harness}-}" ] || exit 0`,
|
|
22842
|
+
`case $${harness} in *[!a-z0-9-]*) exit 0 ;; esac`,
|
|
22843
|
+
"ref0_git=$(git rev-parse --git-dir) || exit 0",
|
|
22844
|
+
"[ -d \"$ref0_git/rebase-merge\" ] || [ -d \"$ref0_git/rebase-apply\" ] || [ -f \"$ref0_git/CHERRY_PICK_HEAD\" ] && exit 0",
|
|
22845
|
+
`ref0_trailers=$(${trailers} --parse "$1") || exit 0`,
|
|
22846
|
+
"case ${GIT_AUTHOR_EMAIL-} in",
|
|
22847
|
+
`*@${agentCommitDomain})`,
|
|
22848
|
+
" if printf '%s\\n' \"$ref0_trailers\" | grep -qi '^signed-off-by:'; then",
|
|
22849
|
+
" ref0_person=$(git var GIT_COMMITTER_IDENT)",
|
|
22850
|
+
` if [ "$${harness}" = codex ]; then`,
|
|
22851
|
+
...codexSignOff.map((line) => ` ${line}`),
|
|
22852
|
+
" printf '%s\\n' 'ref0: this checkout now uses the person as author. Run the same git commit -s again; add --reset-author to an amend.' >&2",
|
|
22853
|
+
" else",
|
|
22854
|
+
` printf ${shellWord(signOffRefusal("%s", "%s"))} "\${GIT_AUTHOR_NAME-} <$GIT_AUTHOR_EMAIL>" "\${ref0_person% * *}" >&2`,
|
|
22855
|
+
" fi",
|
|
22856
|
+
" exit 1",
|
|
22857
|
+
" fi",
|
|
22858
|
+
" ;;",
|
|
22859
|
+
"*)",
|
|
22860
|
+
" if [ -n \"${GIT_AUTHOR_DATE-}\" ]; then",
|
|
22861
|
+
` ref0_head=$(git log -1 --no-show-signature --date=raw --format='%(trailers:key=${assistedBy},valueonly)%n%an <%ae> %ad' HEAD 2>/dev/null)`,
|
|
22862
|
+
" [ \"${ref0_head##*\"$ref0_nl\"}\" = \"${GIT_AUTHOR_NAME-} <${GIT_AUTHOR_EMAIL-}> ${GIT_AUTHOR_DATE#@}\" ] && [ -z \"${ref0_head%\"$ref0_nl\"*}\" ] && exit 0",
|
|
22863
|
+
" fi",
|
|
22864
|
+
" ;;",
|
|
22865
|
+
"esac",
|
|
22866
|
+
...hook === "prepare-commit-msg" ? ["case ${2-} in ''|template) exit 0 ;; esac"] : [],
|
|
22867
|
+
`case "$ref0_nl$ref0_trailers$ref0_nl" in *"\${ref0_nl}${assistedBy}: $${harness}:"*|*"\${ref0_nl}${assistedBy}: $${harness}$ref0_nl"*) exit 0 ;; esac`,
|
|
22868
|
+
...hook === "prepare-commit-msg" ? [`${saysSomething} "$1" || exit 0`] : [
|
|
22869
|
+
"ref0_message=$(git stripspace --strip-comments < \"$1\")",
|
|
22870
|
+
`printf '%s\\n' "$ref0_message" | ${saysSomething} || exit 0`,
|
|
22871
|
+
`ref0_template=$(git config --path commit.template) && ref0_template=$(git stripspace --strip-comments < "$ref0_template" 2>/dev/null) && case $ref0_message in "$ref0_template"*) printf '%s\\n' "\${ref0_message#"$ref0_template"}" | ${saysSomething} || exit 0 ;; esac`
|
|
22872
|
+
],
|
|
22873
|
+
"ref0_model=",
|
|
22874
|
+
`if [ "$${harness}" = codex ]; then`,
|
|
22875
|
+
" case ${CODEX_THREAD_ID-} in \"\"|*[!a-zA-Z0-9-]*) ;; *)",
|
|
22876
|
+
" ref0_rollout=$(find \"${CODEX_HOME:-\"$HOME/.codex\"}/sessions\" -type f -name \"rollout-*-${CODEX_THREAD_ID}.jsonl\" 2>/dev/null | head -n 1)",
|
|
22877
|
+
` [ -f "$ref0_rollout" ] && ref0_model=$(tail -c ${transcriptTail} "$ref0_rollout" | LC_ALL=C grep '"type"[[:space:]]*:[[:space:]]*"turn_context"' | grep -o '"model"[[:space:]]*:[[:space:]]*"[A-Za-z0-9][A-Za-z0-9._:@/-]*"' | tail -n 1 | sed 's/^[^:]*:[[:space:]]*"//; s/"$//')`,
|
|
22878
|
+
" ;; esac",
|
|
22879
|
+
"else",
|
|
22880
|
+
`[ -f "\${${transcript}-}" ] && ref0_model=$(tail -c ${transcriptTail} "$${transcript}" | LC_ALL=C grep -o '"message":{"model":"[A-Za-z0-9][A-Za-z0-9._:@/-]*"')`,
|
|
22881
|
+
"ref0_model=${ref0_model##*\\\"model\\\":\\\"}",
|
|
22882
|
+
"ref0_model=${ref0_model%\\\"}",
|
|
22883
|
+
`[ -n "$ref0_model" ] || ref0_model=\${${model}-}`,
|
|
22884
|
+
"fi",
|
|
22885
|
+
"case $ref0_model in *[!A-Za-z0-9._:@/-]*) ref0_model= ;; esac",
|
|
22886
|
+
`${trailers} --in-place --if-exists addIfDifferent --trailer "${assistedBy}: $${harness}\${ref0_model:+:$ref0_model}" "$1"`,
|
|
22887
|
+
"exit 0"
|
|
22888
|
+
];
|
|
22889
|
+
};
|
|
22890
|
+
//#endregion
|
|
22891
|
+
//#region src/harness/git-hooks.ts
|
|
22892
|
+
/**
|
|
22893
|
+
* ref0's Git hooks: folders of forwarders in `~/.ref0/origins/<origin>/git-hooks`, which ref0 names as
|
|
22894
|
+
* `core.hooksPath` in its agents' commands alone, never in a repo, its config or its hooks.
|
|
22895
|
+
* `core.hooksPath` replaces a repo's whole hooks folder, so each forwarder runs the repo's own
|
|
22896
|
+
* hook of its name, with the same arguments, stdin and exit status, wherever the repo keeps it:
|
|
22897
|
+
* husky's `.husky/_`, `.git/hooks/` or another `core.hooksPath`. It finds and runs that hook
|
|
22898
|
+
* with ref0's entry left out of its environment, so a hook that looks up another, as
|
|
22899
|
+
* `pre-applypatch.sample` runs `pre-commit`, finds the repo's own. A forwarder runs nothing
|
|
22900
|
+
* when the repo's hooks path is one of ref0's folders, or when a forwarder started it, so none
|
|
22901
|
+
* calls itself: a first version that left ref0's entry in started about 1,000 processes. Only
|
|
22902
|
+
* `prepare-commit-msg` and `commit-msg` do more: once the repo's own hook succeeds, they add
|
|
22903
|
+
* ref0's part, in `commit-hooks.ts`.
|
|
22904
|
+
*
|
|
22905
|
+
* Git runs a hook that exists even where the repo has none, so the forwarders cost each hook one
|
|
22906
|
+
* `sh` and one `git rev-parse`, about 1.5 ms. Two hooks run on every index write and ref update,
|
|
22907
|
+
* hundreds of times in a long rebase, so only `all` forwards them, for a checkout that has its
|
|
22908
|
+
* own; `most` forwards every other (measured 23 Oct 2026, in the piece's pull request).
|
|
22909
|
+
*
|
|
22910
|
+
* The folders are the CLI's: a session's start writes them again when a forwarder differs, as
|
|
22911
|
+
* after an update. Each is read-only, so a tool that installs hooks wherever `core.hooksPath`
|
|
22912
|
+
* points, run in an agent's command, fails there rather than replacing a forwarder for every
|
|
22913
|
+
* repo.
|
|
22914
|
+
*/
|
|
22915
|
+
/** The hooks Git runs on every index write and ref update. */
|
|
22916
|
+
const frequentHooks = ["post-index-change", "reference-transaction"];
|
|
22917
|
+
/**
|
|
22918
|
+
* Every other hook Git looks for in its hooks path. Not `push-to-checkout`, which replaces Git's
|
|
22919
|
+
* own update of a checkout pushed to merely by existing, nor `fsmonitor-watchman`, which Git runs
|
|
22920
|
+
* from `core.fsmonitor`, never from the hooks path.
|
|
22921
|
+
*/
|
|
22922
|
+
const forwardedHooks = [
|
|
22923
|
+
"applypatch-msg",
|
|
22924
|
+
"pre-applypatch",
|
|
22925
|
+
"post-applypatch",
|
|
22926
|
+
"pre-commit",
|
|
22927
|
+
"pre-merge-commit",
|
|
22928
|
+
"prepare-commit-msg",
|
|
22929
|
+
"commit-msg",
|
|
22930
|
+
"post-commit",
|
|
22931
|
+
"pre-rebase",
|
|
22932
|
+
"post-checkout",
|
|
22933
|
+
"post-merge",
|
|
22934
|
+
"pre-push",
|
|
22935
|
+
"pre-receive",
|
|
22936
|
+
"update",
|
|
22937
|
+
"proc-receive",
|
|
22938
|
+
"post-receive",
|
|
22939
|
+
"post-update",
|
|
22940
|
+
"pre-auto-gc",
|
|
22941
|
+
"post-rewrite",
|
|
22942
|
+
"sendemail-validate",
|
|
22943
|
+
"p4-changelist",
|
|
22944
|
+
"p4-prepare-changelist",
|
|
22945
|
+
"p4-post-changelist",
|
|
22946
|
+
"p4-pre-submit"
|
|
22947
|
+
];
|
|
22948
|
+
/**
|
|
22949
|
+
* What finds and runs the repo's own hook `name`, with ref0's entry left out: an entry naming one
|
|
22950
|
+
* of ref0's folders, in `$ref0_hooks`, is ref0's, and the rest keep their order, renumbered.
|
|
22951
|
+
* `done` ends it with nothing to run, `failed` when Git can't say where the hook is, and `run`
|
|
22952
|
+
* runs the hook it found.
|
|
22953
|
+
*/
|
|
22954
|
+
const forwarding = (name, end) => [
|
|
22955
|
+
`[ -n "\${${gitHookVariables.forwarded}-}" ] && ${end.done}`,
|
|
22956
|
+
"case ${GIT_CONFIG_PARAMETERS-} in *'/@git/agents/'*)",
|
|
22957
|
+
` ref0_codex=$(perl -e ${shellWord(withoutCodexInclude)} root) || ${end.failed}`,
|
|
22958
|
+
` GIT_CONFIG_PARAMETERS=$(perl -e ${shellWord(withoutCodexInclude)}) || ${end.failed}`,
|
|
22959
|
+
" if [ -z \"$GIT_CONFIG_PARAMETERS\" ]; then unset GIT_CONFIG_PARAMETERS; else export GIT_CONFIG_PARAMETERS; fi",
|
|
22960
|
+
";; esac",
|
|
22961
|
+
"ref0_n=${GIT_CONFIG_COUNT:-0}",
|
|
22962
|
+
"ref0_i=0",
|
|
22963
|
+
"ref0_j=0",
|
|
22964
|
+
"while [ \"$ref0_i\" -lt \"$ref0_n\" ]; do",
|
|
22965
|
+
" eval \"ref0_key=\\${GIT_CONFIG_KEY_$ref0_i-} ref0_value=\\${GIT_CONFIG_VALUE_$ref0_i-}\"",
|
|
22966
|
+
" unset \"GIT_CONFIG_KEY_$ref0_i\" \"GIT_CONFIG_VALUE_$ref0_i\"",
|
|
22967
|
+
" if [ \"$ref0_key\" != core.hooksPath ] || [ \"${ref0_value%/*}\" != \"$ref0_hooks\" ]; then",
|
|
22968
|
+
" export \"GIT_CONFIG_KEY_$ref0_j=$ref0_key\" \"GIT_CONFIG_VALUE_$ref0_j=$ref0_value\"",
|
|
22969
|
+
" ref0_j=$((ref0_j + 1))",
|
|
22970
|
+
" fi",
|
|
22971
|
+
" ref0_i=$((ref0_i + 1))",
|
|
22972
|
+
"done",
|
|
22973
|
+
"if [ \"$ref0_j\" -eq 0 ]; then unset GIT_CONFIG_COUNT; else export GIT_CONFIG_COUNT=\"$ref0_j\"; fi",
|
|
22974
|
+
`ref0_hook=$(git rev-parse --git-path hooks/${name}) || ${end.failed}`,
|
|
22975
|
+
`[ -f "$ref0_hook" ] && [ -x "$ref0_hook" ] || ${end.done}`,
|
|
22976
|
+
`ref0_dir=$(cd -P "\${ref0_hook%/*}" && pwd) || ${end.failed}`,
|
|
22977
|
+
`[ "\${ref0_dir%/*}" = "$ref0_hooks" ] && ${end.done}`,
|
|
22978
|
+
...end.run
|
|
22979
|
+
];
|
|
22980
|
+
/**
|
|
22981
|
+
* The forwarder for one hook, in ref0's folders in `hooks`. Each `exec`s the repo's own hook,
|
|
22982
|
+
* but `prepare-commit-msg` and `commit-msg`, which run it and add ref0's part once it succeeds,
|
|
22983
|
+
* even when a forwarder started them and they forward nothing.
|
|
22984
|
+
*/
|
|
22985
|
+
const forwarder = (name, hooks) => [
|
|
22986
|
+
"#!/bin/sh",
|
|
22987
|
+
`# ref0 ${cliVersion}'s forwarder for Git's ${name} hook. ref0 sets core.hooksPath to this`,
|
|
22988
|
+
"# folder only in its agents' commands. This runs the repo's own hook, found and run with",
|
|
22989
|
+
"# ref0's entry left out, with the same arguments, stdin and exit status.",
|
|
22990
|
+
`ref0_hooks=${shellWord(hooks)}`,
|
|
22991
|
+
...isCommitHook(name) ? [
|
|
22992
|
+
"# Then, in an agent's commands, it adds the Assisted-by trailer and refuses a sign-off on an",
|
|
22993
|
+
"# agent's commit.",
|
|
22994
|
+
"ref0_forward() {",
|
|
22995
|
+
...forwarding(name, {
|
|
22996
|
+
done: "return 0",
|
|
22997
|
+
failed: "return",
|
|
22998
|
+
run: [`${gitHookVariables.forwarded}=${name} "$ref0_hook" "$@"`]
|
|
22999
|
+
}).map((line) => ` ${line}`),
|
|
23000
|
+
"}",
|
|
23001
|
+
"ref0_forward \"$@\" || exit",
|
|
23002
|
+
...commitPart(name)
|
|
23003
|
+
] : forwarding(name, {
|
|
23004
|
+
done: "exit 0",
|
|
23005
|
+
failed: "exit",
|
|
23006
|
+
run: [`export ${gitHookVariables.forwarded}=${name}`, "exec \"$ref0_hook\" \"$@\""]
|
|
23007
|
+
}),
|
|
23008
|
+
""
|
|
23009
|
+
].join("\n");
|
|
23010
|
+
/** The hooks each folder forwards. */
|
|
23011
|
+
const hooksIn = (folder) => folder === "all" ? [...forwardedHooks, ...frequentHooks] : forwardedHooks;
|
|
23012
|
+
/** The folder holding ref0's hooks folders, made if it isn't yet: its path, through any links. */
|
|
23013
|
+
const gitHooksHome = Effect.fnUntraced(function* (home, api) {
|
|
23014
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23015
|
+
const given = (yield* Path.Path).join(home, ".ref0", "origins", originKey(api), "git-hooks");
|
|
23016
|
+
yield* fs.makeDirectory(given, {
|
|
23017
|
+
recursive: true,
|
|
23018
|
+
mode: 448
|
|
23019
|
+
});
|
|
23020
|
+
return yield* fs.realPath(given);
|
|
23021
|
+
});
|
|
23022
|
+
/**
|
|
23023
|
+
* One of ref0's hooks folders, in `hooks`, written first unless every forwarder in it is already
|
|
23024
|
+
* as this CLI writes it: its path, as the agent's `core.hooksPath` names it.
|
|
23025
|
+
*/
|
|
23026
|
+
const hooksFolder = Effect.fnUntraced(function* (hooks, folder) {
|
|
23027
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23028
|
+
const path = yield* Path.Path;
|
|
23029
|
+
const crypto = yield* Crypto.Crypto;
|
|
23030
|
+
const written = path.join(hooks, folder);
|
|
23031
|
+
const names = hooksIn(folder);
|
|
23032
|
+
const current = Effect.forEach(names, (name) => fs.readFileString(path.join(written, name)).pipe(Effect.map((text) => text === forwarder(name, hooks)), Effect.orElseSucceed(() => false))).pipe(Effect.map((each) => each.every(Boolean)));
|
|
23033
|
+
if (yield* current) return written;
|
|
23034
|
+
yield* Effect.gen(function* () {
|
|
23035
|
+
yield* fs.makeDirectory(written, { recursive: true });
|
|
23036
|
+
yield* fs.chmod(written, 493);
|
|
23037
|
+
for (const name of names) {
|
|
23038
|
+
const next = path.join(written, `${name}.${yield* crypto.randomUUIDv4}.next`);
|
|
23039
|
+
yield* fs.writeFileString(next, forwarder(name, hooks), { mode: 365 });
|
|
23040
|
+
yield* fs.rename(next, path.join(written, name));
|
|
23041
|
+
}
|
|
23042
|
+
yield* fs.chmod(written, 365);
|
|
23043
|
+
}).pipe(Effect.catch((error) => Effect.flatMap(current, (done) => done ? Effect.void : Effect.fail(error))));
|
|
23044
|
+
return written;
|
|
23045
|
+
});
|
|
23046
|
+
//#endregion
|
|
23047
|
+
//#region src/harness/codex-commits.ts
|
|
23048
|
+
/**
|
|
23049
|
+
* `@git` isn't a valid Codex plugin version. Version selection and upgrade skip it, while
|
|
23050
|
+
* `plugin remove` deletes it with the plugin's cache root (Codex 0.160–0.164). A same-version
|
|
23051
|
+
* reinstall replaces the root; session start restores these files before the first command.
|
|
23052
|
+
* Only `checkouts` is writable in ref0's permission profile, never the agents' author files.
|
|
23053
|
+
*/
|
|
23054
|
+
/** A Git config value, including paths with quotes, backslashes or line breaks. */
|
|
23055
|
+
const gitValue = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll(" ", "\\t").replaceAll("\b", "\\b")}"`;
|
|
23056
|
+
var CodexCommitFailed = class extends Schema.TaggedError()("CodexCommitFailed", { detail: Schema.String }) {};
|
|
23057
|
+
/** Writes a checkout's overrides and, at session start, the agent's include under the same lock. */
|
|
23058
|
+
const writeCodexCheckout = Effect.fnUntraced(function* (input) {
|
|
23059
|
+
if ((yield* (yield* ChildProcessSpawner.ChildProcessSpawner).exitCode(ChildProcess.make("perl", [
|
|
23060
|
+
"-e",
|
|
23061
|
+
codexCheckoutWriter,
|
|
23062
|
+
codexCheckoutsFolder(input.gitFolder),
|
|
23063
|
+
input.common,
|
|
23064
|
+
input.folder,
|
|
23065
|
+
input.signsOff ? "1" : "0",
|
|
23066
|
+
input.author?.file ?? "",
|
|
23067
|
+
input.author?.text ?? "",
|
|
23068
|
+
input.clearAuthor ?? "",
|
|
23069
|
+
input.activateAuthor ? "1" : ""
|
|
23070
|
+
]))) !== ChildProcessSpawner.ExitCode(0)) return yield* new CodexCommitFailed({ detail: "Could not write Codex’s Git config files." });
|
|
23071
|
+
});
|
|
23072
|
+
/** The include installed into `set`, and restored by each session's start. */
|
|
23073
|
+
const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
|
|
23074
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23075
|
+
const directory = `${root}/agents`;
|
|
23076
|
+
yield* fs.makeDirectory(directory, {
|
|
23077
|
+
recursive: true,
|
|
23078
|
+
mode: 448
|
|
23079
|
+
});
|
|
23080
|
+
const folder = yield* hooksFolder(yield* gitHooksHome(home, (yield* Ref0Config).api), "most");
|
|
23081
|
+
yield* hooksFolder(yield* gitHooksHome(home, (yield* Ref0Config).api), "all");
|
|
23082
|
+
const author = `[author]\n\tname = ${gitValue(agentCommitName(agent.owner, agent.agent))}\n\temail = ${gitValue(agentCommitEmail(agent.agentId))}\n`;
|
|
23083
|
+
const file = `${directory}/${agent.agentId}.gitconfig`;
|
|
23084
|
+
return {
|
|
23085
|
+
file,
|
|
23086
|
+
text: `${author}[core]\n\thooksPath = ${gitValue(folder)}\n[include]\n\tpath = ${gitValue(`${codexCheckoutsFolder(root)}/checkouts.gitconfig`)}\n`,
|
|
23087
|
+
folder,
|
|
23088
|
+
parameters: shellWord(`include.path=${file}`)
|
|
23089
|
+
};
|
|
23090
|
+
});
|
|
23091
|
+
/** Codex has no environment file: its install selects a file; session start fills its checkout. */
|
|
23092
|
+
const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
|
|
23093
|
+
const folders = yield* ConfigFolders;
|
|
23094
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23095
|
+
const path = yield* Path.Path;
|
|
23096
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23097
|
+
const agent = yield* (yield* HostGrant).get(yield* folders.folder("codex"));
|
|
23098
|
+
if (Option.isNone(agent)) return false;
|
|
23099
|
+
const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
|
|
23100
|
+
if (pluginRoot === "") return false;
|
|
23101
|
+
const gitFolder = path.join(path.dirname(pluginRoot), "@git");
|
|
23102
|
+
const home = yield* Config.String("HOME");
|
|
23103
|
+
const author = yield* codexAgentConfig(agent.value, gitFolder, home);
|
|
23104
|
+
const common = (yield* spawner.string(ChildProcess.make("git", [
|
|
23105
|
+
"-C",
|
|
23106
|
+
cwd,
|
|
23107
|
+
"rev-parse",
|
|
23108
|
+
"--path-format=absolute",
|
|
23109
|
+
"--git-common-dir"
|
|
23110
|
+
])).pipe(Effect.orElseSucceed(() => ""))).trim();
|
|
23111
|
+
const folder = yield* hooksFolder(yield* gitHooksHome(yield* Config.String("HOME"), (yield* Ref0Config).api), checkout.folder);
|
|
23112
|
+
yield* writeCodexCheckout({
|
|
23113
|
+
gitFolder,
|
|
23114
|
+
common: common === "" ? "" : yield* fs.realPath(path.resolve(common)),
|
|
23115
|
+
folder,
|
|
23116
|
+
signsOff: checkout.signsOff,
|
|
23117
|
+
author
|
|
23118
|
+
});
|
|
23119
|
+
return true;
|
|
23120
|
+
});
|
|
23121
|
+
//#endregion
|
|
23122
|
+
//#region src/harness/hook-fields.ts
|
|
23123
|
+
/**
|
|
23124
|
+
* The same for Codex 0.160.0, which skips a hook outright if its template names a field the event
|
|
23125
|
+
* lacks, so each list holds only fields every hook of its event carried in the prototypes, and
|
|
23126
|
+
* only events they saw, each a `HookEvent`. Codex puts the model in every hook but `SessionEnd`.
|
|
23127
|
+
* Its `UserPromptSubmit` carries the prompt (`spikes/harness/results/codex/catalog.json`), which
|
|
23128
|
+
* no list names, as for Claude Code.
|
|
23129
|
+
* Every Codex hook carries `transcript_path`, the session's rollout, `null` for a session kept
|
|
23130
|
+
* nowhere (Codex's hook input schemas, 0.160.0 to 0.163.0): its turns' tokens are read there.
|
|
23131
|
+
* A subagent's are read from its own rollout, which its `SubagentStop` names, with the
|
|
23132
|
+
* subagent's own turn as its `turn_id` (Codex 0.163.0's subagents, 20 Oct 2026).
|
|
23133
|
+
*/
|
|
23134
|
+
const codexHookFields = {
|
|
23135
|
+
UserPromptSubmit: [
|
|
23136
|
+
"session_id",
|
|
23137
|
+
"turn_id",
|
|
23138
|
+
"cwd",
|
|
23139
|
+
"model",
|
|
23140
|
+
"transcript_path"
|
|
23141
|
+
],
|
|
23142
|
+
PreToolUse: [
|
|
23143
|
+
"session_id",
|
|
23144
|
+
"turn_id",
|
|
23145
|
+
"cwd",
|
|
23146
|
+
"model",
|
|
23147
|
+
"tool_name",
|
|
23148
|
+
"tool_use_id",
|
|
23149
|
+
"tool_input"
|
|
23150
|
+
],
|
|
23151
|
+
PostToolUse: [
|
|
23152
|
+
"session_id",
|
|
23153
|
+
"turn_id",
|
|
23154
|
+
"cwd",
|
|
23155
|
+
"model",
|
|
23156
|
+
"tool_name",
|
|
23157
|
+
"tool_use_id",
|
|
23158
|
+
"tool_input",
|
|
23159
|
+
"tool_response"
|
|
23160
|
+
],
|
|
23161
|
+
Stop: [
|
|
23162
|
+
"session_id",
|
|
23163
|
+
"turn_id",
|
|
23164
|
+
"model",
|
|
23165
|
+
"stop_hook_active",
|
|
23166
|
+
"transcript_path"
|
|
23167
|
+
],
|
|
23168
|
+
SubagentStop: [
|
|
23169
|
+
"session_id",
|
|
23170
|
+
"turn_id",
|
|
23171
|
+
"agent_id",
|
|
23172
|
+
"agent_type",
|
|
23173
|
+
"stop_hook_active",
|
|
23174
|
+
"agent_transcript_path"
|
|
23175
|
+
]
|
|
23176
|
+
};
|
|
23177
|
+
Schema.encodeSync(Schema.fromJsonString(CodexPluginManifest));
|
|
23178
|
+
/** How Codex names a plugin's MCP server in an `mcp_tool` hook. */
|
|
23179
|
+
const codexServer = "ref0";
|
|
23180
|
+
/** Codex's hooks file, by its path inside the plugin. */
|
|
23181
|
+
const codexHooksPath = "hooks/codex-hooks.json";
|
|
23182
|
+
({ ...mcpToolHooks(codexServer, codexHookFields) });
|
|
23183
|
+
/**
|
|
23184
|
+
* The folder of Codex's skills, by its path inside the plugin. Not `skills`, which Claude Code
|
|
23185
|
+
* reads too, where a `status` skill would be a second status command. Codex 0.163.0 skips
|
|
23186
|
+
* Claude Code's `commands/status.md`, since its `!` line is a template it can't run.
|
|
23187
|
+
*/
|
|
23188
|
+
const codexSkillsPath = "codex-skills";
|
|
23189
|
+
/** The status skill, by its path inside the plugin. */
|
|
23190
|
+
const codexStatusSkillPath = `${codexSkillsPath}/status/SKILL.md`;
|
|
23191
|
+
/**
|
|
23192
|
+
* How a person runs the status skill of a plugin installed as `name`: Codex 0.163.0 names a
|
|
23193
|
+
* plugin's skill by the plugin, then the skill.
|
|
23194
|
+
*/
|
|
23195
|
+
const codexStatusShortcut = (name) => `$${name}:status`;
|
|
23196
|
+
/** The skill's placeholder for the plugin's folder, which Codex's agent fills in. */
|
|
23197
|
+
const pluginFolder = "<plugin>";
|
|
23198
|
+
/** What the status skill runs: `ref0 status`, through the plugin's lookup script. */
|
|
23199
|
+
const codexStatusLine = throughLookup([harnessCommandNames.status]).replaceAll(pluginRoot, pluginFolder);
|
|
23200
|
+
[
|
|
23201
|
+
"---",
|
|
23202
|
+
"name: status",
|
|
23203
|
+
`description: Shows whether ref0 follows this Codex session, and what to do next if it doesn’t. Use when the person runs ${codexStatusShortcut("ref0")} or asks whether ref0 is working.`,
|
|
23204
|
+
"---",
|
|
23205
|
+
"",
|
|
23206
|
+
"Run ref0’s check and show the person what it says.",
|
|
23207
|
+
"",
|
|
23208
|
+
`1. This file is \`${pluginFolder}/${codexSkillsPath}/status/SKILL.md\`. Run this command, with \`${pluginFolder}\` replaced by that folder’s absolute path:`,
|
|
23209
|
+
"",
|
|
23210
|
+
" ```sh",
|
|
23211
|
+
` ${codexStatusLine}`,
|
|
23212
|
+
" ```",
|
|
23213
|
+
"",
|
|
23214
|
+
" Run it outside Codex’s sandbox, with `sandbox_permissions: \"require_escalated\"`, since in the sandbox it can’t ask Codex, this machine’s keychain or ref0. If that isn’t allowed, run it in the sandbox.",
|
|
23215
|
+
"2. Show every line it prints as it is, and add nothing. It exits 1 when a line says no: that’s its answer, not an error to fix.",
|
|
23216
|
+
""
|
|
23217
|
+
].join("\n");
|
|
23218
|
+
//#endregion
|
|
23219
|
+
//#region src/harness/origin-plugin.ts
|
|
23220
|
+
const manifestJson = Schema.fromJsonString(PluginManifest);
|
|
23221
|
+
const codexJson = Schema.fromJsonString(CodexPluginManifest);
|
|
23222
|
+
const marketJson = Schema.fromJsonString(PluginMarketplace);
|
|
23223
|
+
const hooksJson = Schema.fromJsonString(HooksFile);
|
|
23224
|
+
/**
|
|
23225
|
+
* Keep the checked release under this origin's own marketplace and plugin identity, with its
|
|
23226
|
+
* hooks, server and Codex status skill named for it.
|
|
23227
|
+
*/
|
|
23228
|
+
const keepOriginPlugin = Effect.fnUntraced(function* (source, home) {
|
|
23229
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23230
|
+
const path = yield* Path.Path;
|
|
23231
|
+
const { api } = yield* Ref0Config;
|
|
23232
|
+
const name = yield* originPluginName(api);
|
|
23233
|
+
const directory = path.join(home, ".ref0", "origins", originKey(api), "plugins");
|
|
23234
|
+
yield* fs.makeDirectory(directory, {
|
|
21579
23235
|
recursive: true,
|
|
21580
23236
|
mode: 448
|
|
21581
23237
|
});
|
|
@@ -21675,6 +23331,7 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
21675
23331
|
const failed = (reason, detail = "") => into === void 0 ? unavailable : new InstallFailed({ says: `${name} ${reason}.${detail === "" ? "" : ` ${detail}`}` });
|
|
21676
23332
|
return Effect.scoped(Effect.gen(function* () {
|
|
21677
23333
|
const handle = yield* spawner.spawn(ChildProcess.make(options.command, args, {
|
|
23334
|
+
cwd: yield* emptyFolder(fs),
|
|
21678
23335
|
env: into === void 0 ? fetchEnv : envFor(into),
|
|
21679
23336
|
extendEnv: true
|
|
21680
23337
|
}));
|
|
@@ -21795,90 +23452,212 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
21795
23452
|
};
|
|
21796
23453
|
});
|
|
21797
23454
|
//#endregion
|
|
21798
|
-
//#region src/harness/codex-
|
|
21799
|
-
|
|
21800
|
-
|
|
21801
|
-
|
|
21802
|
-
|
|
21803
|
-
|
|
21804
|
-
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
/**
|
|
21808
|
-
|
|
21809
|
-
|
|
21810
|
-
|
|
21811
|
-
|
|
21812
|
-
|
|
21813
|
-
|
|
21814
|
-
|
|
21815
|
-
|
|
21816
|
-
|
|
21817
|
-
const
|
|
21818
|
-
|
|
21819
|
-
|
|
21820
|
-
const
|
|
21821
|
-
|
|
21822
|
-
|
|
21823
|
-
|
|
21824
|
-
|
|
21825
|
-
|
|
21826
|
-
|
|
21827
|
-
|
|
21828
|
-
|
|
21829
|
-
|
|
21830
|
-
|
|
21831
|
-
|
|
21832
|
-
|
|
21833
|
-
|
|
21834
|
-
|
|
21835
|
-
|
|
21836
|
-
/**
|
|
21837
|
-
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
21838
|
-
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
21839
|
-
*/
|
|
21840
|
-
const exchange = (codex, lines, ids, configFolder) => Effect.scoped(Effect.gen(function* () {
|
|
21841
|
-
const answered = yield* (yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(ChildProcess.make(codex, ["app-server"], {
|
|
21842
|
-
env: configFolder === void 0 ? void 0 : { CODEX_HOME: configFolder },
|
|
21843
|
-
extendEnv: true,
|
|
21844
|
-
forceKillAfter: "2 seconds",
|
|
21845
|
-
stdin: {
|
|
21846
|
-
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
21847
|
-
endOnDone: false
|
|
21848
|
-
}
|
|
21849
|
-
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$1(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
21850
|
-
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
21851
|
-
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
21852
|
-
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
21853
|
-
const resultOf = (method, answer) => {
|
|
21854
|
-
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
21855
|
-
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
21856
|
-
method: method.method,
|
|
21857
|
-
error: answer.error
|
|
21858
|
-
}));
|
|
21859
|
-
const { result } = method;
|
|
21860
|
-
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
21861
|
-
method: method.method,
|
|
21862
|
-
issue: error.message
|
|
21863
|
-
})));
|
|
21864
|
-
};
|
|
21865
|
-
/**
|
|
21866
|
-
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
21867
|
-
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
21868
|
-
* its request's ID, and its result is read by the method's result schema.
|
|
21869
|
-
*/
|
|
21870
|
-
const askCodex = (codex, method, params, configFolder) => Effect.gen(function* () {
|
|
21871
|
-
const asked = {
|
|
21872
|
-
jsonrpc: "2.0",
|
|
21873
|
-
id: askedId,
|
|
21874
|
-
method: method.method,
|
|
21875
|
-
params
|
|
23455
|
+
//#region src/harness/codex-install-commits.ts
|
|
23456
|
+
const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
|
|
23457
|
+
const sameJson = Schema.toEquivalence(Schema.Json);
|
|
23458
|
+
/** Only the single ref0 include installed by this CLI belongs to ref0. */
|
|
23459
|
+
const selectedInclude = (set) => {
|
|
23460
|
+
const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
|
|
23461
|
+
const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
|
|
23462
|
+
return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
|
|
23463
|
+
};
|
|
23464
|
+
/** A changed grant must never leave the previous agent as author after a failed install. */
|
|
23465
|
+
const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, options) {
|
|
23466
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23467
|
+
const configFile = `${options.configFolder}/config.toml`;
|
|
23468
|
+
if (!(yield* fs.exists(configFile))) return;
|
|
23469
|
+
const text = yield* fs.readFileString(configFile);
|
|
23470
|
+
const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
|
|
23471
|
+
const read = yield* decodeProfile({ config: document });
|
|
23472
|
+
const file = selectedInclude(read.config.shell_environment_policy?.set ?? {});
|
|
23473
|
+
if (file === void 0) return;
|
|
23474
|
+
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23475
|
+
if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
|
|
23476
|
+
const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
|
|
23477
|
+
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
|
|
23478
|
+
yield* writeCodexCheckout({
|
|
23479
|
+
gitFolder: oldRoot,
|
|
23480
|
+
common: "",
|
|
23481
|
+
folder: "",
|
|
23482
|
+
signsOff: false,
|
|
23483
|
+
clearAuthor: file,
|
|
23484
|
+
author: fallback
|
|
23485
|
+
});
|
|
23486
|
+
yield* Console.error(`ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. Fix the shell_environment_policy conflict in Codex’s config or profile named above: remove conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded. Then run: ${installLine(options.configFolder)}`);
|
|
23487
|
+
});
|
|
23488
|
+
/** Install the connected agent's include and shell policy. */
|
|
23489
|
+
const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
23490
|
+
const env = {
|
|
23491
|
+
HOME: options.home,
|
|
23492
|
+
CODEX_HOME: options.configFolder
|
|
21876
23493
|
};
|
|
21877
|
-
const
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
23494
|
+
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23495
|
+
return yield* Effect.gen(function* () {
|
|
23496
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23497
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23498
|
+
yield* fs.makeDirectory(`${options.home}/.ref0`, {
|
|
23499
|
+
recursive: true,
|
|
23500
|
+
mode: 448
|
|
23501
|
+
});
|
|
23502
|
+
if ((yield* (yield* spawner.spawn(ChildProcess.make("perl", [
|
|
23503
|
+
"-e",
|
|
23504
|
+
"use Fcntl qw(LOCK_EX); open my $lock, \">>\", $ARGV[0] or die $!; alarm 5; flock($lock, LOCK_EX) or die $!; alarm 0; $|=1; print \"locked\\n\"; <STDIN>;",
|
|
23505
|
+
`${options.home}/.ref0/codex-install.lock`
|
|
23506
|
+
], { stdin: "pipe" }))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)) !== "locked") return yield* failed("lock Codex’s commit setup");
|
|
23507
|
+
const read = yield* askCodex(options.codex, codexMethods.configRead, {
|
|
23508
|
+
includeLayers: true,
|
|
23509
|
+
cwd: null
|
|
23510
|
+
}, env);
|
|
23511
|
+
const policy = read.config.shell_environment_policy;
|
|
23512
|
+
const set = policy?.set ?? {};
|
|
23513
|
+
const own = selectedInclude(set) !== void 0;
|
|
23514
|
+
const collision = [
|
|
23515
|
+
"GIT_CONFIG_PARAMETERS",
|
|
23516
|
+
"REF0_HARNESS",
|
|
23517
|
+
"GIT_AUTHOR_NAME",
|
|
23518
|
+
"GIT_AUTHOR_EMAIL"
|
|
23519
|
+
].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
|
|
23520
|
+
const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
|
|
23521
|
+
if (collision !== void 0 || allowlist) {
|
|
23522
|
+
yield* Console.error(`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`);
|
|
23523
|
+
if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
|
|
23524
|
+
return;
|
|
23525
|
+
}
|
|
23526
|
+
const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
|
|
23527
|
+
for (const file of profiles) {
|
|
23528
|
+
const text = yield* fs.readFileString(`${options.configFolder}/${file}`);
|
|
23529
|
+
const document = yield* Effect.try({
|
|
23530
|
+
try: () => parse(text, { integersAsBigInt: "asNeeded" }),
|
|
23531
|
+
catch: () => failed(`read Codex’s profile ${printable(file)}`)
|
|
23532
|
+
});
|
|
23533
|
+
const profile = yield* decodeProfile({ config: document });
|
|
23534
|
+
const overrides = profile.config.shell_environment_policy?.set ?? {};
|
|
23535
|
+
const key = [
|
|
23536
|
+
"GIT_CONFIG_PARAMETERS",
|
|
23537
|
+
"REF0_HARNESS",
|
|
23538
|
+
"GIT_AUTHOR_NAME",
|
|
23539
|
+
"GIT_AUTHOR_EMAIL"
|
|
23540
|
+
].find((key) => Object.hasOwn(overrides, key));
|
|
23541
|
+
const profilePolicy = profile.config.shell_environment_policy;
|
|
23542
|
+
const filters = profilePolicy?.filters;
|
|
23543
|
+
const replaced = filters != null && policy?.filters == null;
|
|
23544
|
+
const excluded = filters != null ? [...replaced ? [] : ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"], ...Object.keys(filters)] : profilePolicy?.exclude;
|
|
23545
|
+
const losesAuthors = excluded !== void 0 && excluded !== null && ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].some((name) => !excluded.some((pattern) => pathMatchesWithin(pattern.toLowerCase(), name.toLowerCase(), { left: 1e4 })));
|
|
23546
|
+
const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
|
|
23547
|
+
if (key === void 0 && !losesAuthors && !allowlist) continue;
|
|
23548
|
+
yield* Console.error(`ref0: Codex’s profile ${printable(file)} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`);
|
|
23549
|
+
if (own) return yield* failed("update the existing commit environment; fix the conflicting Codex config or profile, then reinstall");
|
|
23550
|
+
return;
|
|
23551
|
+
}
|
|
23552
|
+
if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
|
|
23553
|
+
const crypto = yield* Crypto.Crypto;
|
|
23554
|
+
const author = yield* codexAgentConfig(agent, options.gitFolder, options.home);
|
|
23555
|
+
const configFile = `${options.configFolder}/config.toml`;
|
|
23556
|
+
const before = (file) => Effect.flatMap(fs.exists(file), (exists) => exists ? Effect.map(fs.readFileString(file), Option.some) : Effect.succeed(Option.none()));
|
|
23557
|
+
const previousAuthor = yield* before(author.file);
|
|
23558
|
+
const restore = (file, before) => Effect.gen(function* () {
|
|
23559
|
+
if (Option.isNone(before)) return yield* fs.remove(file, { force: true });
|
|
23560
|
+
const next = `${file}.${yield* crypto.randomUUIDv4}.next`;
|
|
23561
|
+
yield* fs.writeFileString(next, before.value, { mode: 384 });
|
|
23562
|
+
yield* fs.rename(next, file);
|
|
23563
|
+
});
|
|
23564
|
+
const user = read.layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23565
|
+
const previousPolicy = user?.config === void 0 ? policy ?? null : user.config.shell_environment_policy ?? null;
|
|
23566
|
+
const edits = [{
|
|
23567
|
+
keyPath: "shell_environment_policy.set",
|
|
23568
|
+
value: {
|
|
23569
|
+
...set,
|
|
23570
|
+
GIT_CONFIG_PARAMETERS: author.parameters,
|
|
23571
|
+
REF0_HARNESS: "codex"
|
|
23572
|
+
},
|
|
23573
|
+
mergeStrategy: "replace"
|
|
23574
|
+
}, ...policy?.filters == null ? [{
|
|
23575
|
+
keyPath: "shell_environment_policy.exclude",
|
|
23576
|
+
value: [.../* @__PURE__ */ new Set([
|
|
23577
|
+
...policy?.exclude ?? [],
|
|
23578
|
+
"GIT_AUTHOR_NAME",
|
|
23579
|
+
"GIT_AUTHOR_EMAIL"
|
|
23580
|
+
])],
|
|
23581
|
+
mergeStrategy: "replace"
|
|
23582
|
+
}] : [{
|
|
23583
|
+
keyPath: "shell_environment_policy.filters",
|
|
23584
|
+
value: {
|
|
23585
|
+
...policy.filters,
|
|
23586
|
+
...Object.fromEntries(["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].flatMap((key) => Object.keys(policy.filters ?? {}).some((pattern) => pattern.toUpperCase() === key) ? [] : [[key, "exclude"]]))
|
|
23587
|
+
},
|
|
23588
|
+
mergeStrategy: "replace"
|
|
23589
|
+
}]];
|
|
23590
|
+
const installedPolicy = {
|
|
23591
|
+
...previousPolicy,
|
|
23592
|
+
...Object.fromEntries(edits.map((edit) => [edit.keyPath.slice(25), edit.value]))
|
|
23593
|
+
};
|
|
23594
|
+
const readCurrent = askCodex(options.codex, codexMethods.configRead, {
|
|
23595
|
+
includeLayers: true,
|
|
23596
|
+
cwd: null
|
|
23597
|
+
}, env);
|
|
23598
|
+
const recover = Effect.gen(function* () {
|
|
23599
|
+
const layer = (yield* readCurrent).layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23600
|
+
if (layer?.config === void 0) return false;
|
|
23601
|
+
const currentPolicy = layer.config.shell_environment_policy ?? null;
|
|
23602
|
+
if (sameJson(currentPolicy, previousPolicy)) return true;
|
|
23603
|
+
if (!sameJson(currentPolicy, installedPolicy)) return false;
|
|
23604
|
+
const restored = yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23605
|
+
edits: [{
|
|
23606
|
+
keyPath: "shell_environment_policy",
|
|
23607
|
+
value: previousPolicy,
|
|
23608
|
+
mergeStrategy: "replace"
|
|
23609
|
+
}],
|
|
23610
|
+
filePath: configFile,
|
|
23611
|
+
expectedVersion: layer.version,
|
|
23612
|
+
reloadUserConfig: false
|
|
23613
|
+
}, env).pipe(Effect.option);
|
|
23614
|
+
if (Option.isSome(restored)) return true;
|
|
23615
|
+
const checkedLayer = (yield* readCurrent).layers?.find((layer) => layer.name.type === "user" && layer.name.file === configFile);
|
|
23616
|
+
return checkedLayer?.config !== void 0 && sameJson(checkedLayer.config.shell_environment_policy ?? null, previousPolicy);
|
|
23617
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
23618
|
+
let attemptedConfig = false;
|
|
23619
|
+
yield* Effect.gen(function* () {
|
|
23620
|
+
yield* writeCodexCheckout({
|
|
23621
|
+
gitFolder: options.gitFolder,
|
|
23622
|
+
common: "",
|
|
23623
|
+
folder: author.folder,
|
|
23624
|
+
signsOff: false,
|
|
23625
|
+
author,
|
|
23626
|
+
activateAuthor: true
|
|
23627
|
+
});
|
|
23628
|
+
attemptedConfig = true;
|
|
23629
|
+
if ((yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23630
|
+
edits,
|
|
23631
|
+
filePath: configFile,
|
|
23632
|
+
expectedVersion: user?.version ?? null,
|
|
23633
|
+
reloadUserConfig: false
|
|
23634
|
+
}, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
|
|
23635
|
+
}).pipe(Effect.onError(() => Effect.gen(function* () {
|
|
23636
|
+
if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
|
|
23637
|
+
else yield* Console.error("ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again.");
|
|
23638
|
+
}).pipe(Effect.orDie)));
|
|
23639
|
+
}).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
|
|
23640
|
+
}, Effect.scoped);
|
|
23641
|
+
//#endregion
|
|
23642
|
+
//#region src/harness/install-claude-code.ts
|
|
23643
|
+
/** Install into one config folder, from the release Claude Code fetched and ref0 checked. */
|
|
23644
|
+
const installClaudeCode = Effect.fnUntraced(function* (release, configFolder) {
|
|
23645
|
+
const run = yield* release.into(configFolder);
|
|
23646
|
+
yield* run([
|
|
23647
|
+
"plugin",
|
|
23648
|
+
"install",
|
|
23649
|
+
release.id,
|
|
23650
|
+
"--scope",
|
|
23651
|
+
"user"
|
|
23652
|
+
]);
|
|
23653
|
+
yield* run([
|
|
23654
|
+
"plugin",
|
|
23655
|
+
"update",
|
|
23656
|
+
release.id,
|
|
23657
|
+
"--scope",
|
|
23658
|
+
"user"
|
|
23659
|
+
]);
|
|
23660
|
+
return release.folder;
|
|
21882
23661
|
});
|
|
21883
23662
|
//#endregion
|
|
21884
23663
|
//#region src/harness/install-codex.ts
|
|
@@ -21923,95 +23702,120 @@ const trustRef0Hooks = (reinstall, id) => `In a terminal, run: ${reinstall}, and
|
|
|
21923
23702
|
* `CODEX_HOME`: neither half alone lets a commit and HTTPS through. A linked worktree's own git
|
|
21924
23703
|
* directory isn't covered; there Codex needs full access, which whatever launches it sets.
|
|
21925
23704
|
*/
|
|
21926
|
-
const ref0Profile = {
|
|
23705
|
+
const ref0Profile = (checkouts) => ({
|
|
21927
23706
|
extends: ":workspace",
|
|
21928
|
-
filesystem: {
|
|
23707
|
+
filesystem: {
|
|
23708
|
+
":workspace_roots": { ".git": "write" },
|
|
23709
|
+
[checkouts]: "write"
|
|
23710
|
+
},
|
|
21929
23711
|
network: { enabled: true }
|
|
21930
|
-
};
|
|
23712
|
+
});
|
|
21931
23713
|
const isPathModes = Schema.is(Schema.Record(Schema.String, Schema.String));
|
|
21932
23714
|
/** A default that already lets a command write `.git` and reach the network, so needs nothing. */
|
|
21933
23715
|
const enough = (read) => read.config.default_permissions === ":danger-full-access";
|
|
21934
23716
|
/** Whether a setup's config has ref0's profile, as its default. */
|
|
21935
|
-
const profileSet = (read) => {
|
|
23717
|
+
const profileSet = (read, checkouts) => {
|
|
21936
23718
|
const profile = read.config.permissions?.["ref0"];
|
|
21937
23719
|
const roots = profile?.filesystem?.[":workspace_roots"];
|
|
21938
|
-
return read.config.default_permissions === "ref0" && profile?.extends === ref0Profile.extends && isPathModes(roots) && roots[".git"] === "write" && profile.network?.enabled === true;
|
|
23720
|
+
return read.config.default_permissions === "ref0" && profile?.extends === ref0Profile(checkouts).extends && isPathModes(roots) && roots[".git"] === "write" && profile.filesystem?.[checkouts] === "write" && profile.network?.enabled === true;
|
|
21939
23721
|
};
|
|
21940
|
-
/**
|
|
21941
|
-
|
|
21942
|
-
|
|
21943
|
-
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
|
|
21947
|
-
id
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
ownRef0: false
|
|
21976
|
-
}),
|
|
21977
|
-
onSome: (read) => ({
|
|
21978
|
-
default: read.config.default_permissions ?? void 0,
|
|
21979
|
-
ownRef0: read.config.permissions?.["ref0"] !== void 0
|
|
21980
|
-
})
|
|
23722
|
+
/**
|
|
23723
|
+
* Install into one config folder, from the release Codex fetched and ref0 checked, for the agent
|
|
23724
|
+
* the folder's host grant names.
|
|
23725
|
+
*/
|
|
23726
|
+
const installCodex = Effect.fnUntraced(function* (release, agent, options) {
|
|
23727
|
+
return yield* Effect.gen(function* () {
|
|
23728
|
+
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23729
|
+
const { folder, id, name } = release;
|
|
23730
|
+
yield* (yield* release.into(options.configFolder))([
|
|
23731
|
+
"plugin",
|
|
23732
|
+
"add",
|
|
23733
|
+
id
|
|
23734
|
+
]);
|
|
23735
|
+
const gitFolder = codexGitFolder(options.configFolder, id);
|
|
23736
|
+
const checkouts = codexCheckoutsFolder(gitFolder);
|
|
23737
|
+
const env = {
|
|
23738
|
+
HOME: options.home,
|
|
23739
|
+
CODEX_HOME: options.configFolder
|
|
23740
|
+
};
|
|
23741
|
+
const hooks = (yield* askCodex(options.codex, codexMethods.hooksList, { cwds: [] }, env).pipe(Effect.mapError(() => failed("read the plugin’s hooks from Codex")))).data.flatMap((entry) => entry.hooks).filter((hook) => pluginHook(hook, id));
|
|
23742
|
+
if (hooks.length === 0) return yield* failed("read the plugin’s hooks from Codex");
|
|
23743
|
+
if (!hooks.every((hook) => ownHook(hook, name))) return yield* new InstallFailed({ says: `Codex’s ref0 plugin holds hooks ref0 didn’t write, so none was trusted. Remove it with: codex plugin remove ${id}.` });
|
|
23744
|
+
const trust = Effect.fnUntraced(function* (waiting) {
|
|
23745
|
+
if (!(yield* options.approve(waiting))) return false;
|
|
23746
|
+
yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23747
|
+
edits: [{
|
|
23748
|
+
keyPath: "hooks.state",
|
|
23749
|
+
value: Object.fromEntries(waiting.map((hook) => [hook.key, { trusted_hash: hook.currentHash }])),
|
|
23750
|
+
mergeStrategy: "upsert"
|
|
23751
|
+
}],
|
|
23752
|
+
filePath: null,
|
|
23753
|
+
expectedVersion: null,
|
|
23754
|
+
reloadUserConfig: false
|
|
23755
|
+
}, env).pipe(Effect.mapError(() => failed("record the hooks as trusted in Codex")));
|
|
23756
|
+
return true;
|
|
21981
23757
|
});
|
|
21982
|
-
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
21993
|
-
|
|
21994
|
-
|
|
21995
|
-
|
|
21996
|
-
|
|
21997
|
-
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
23758
|
+
const offerProfile = Effect.fnUntraced(function* () {
|
|
23759
|
+
const config = yield* askCodex(options.codex, codexMethods.configRead, {
|
|
23760
|
+
includeLayers: false,
|
|
23761
|
+
cwd: null
|
|
23762
|
+
}, env).pipe(Effect.option);
|
|
23763
|
+
if (Option.isSome(config) && (profileSet(config.value, checkouts) || enough(config.value))) return true;
|
|
23764
|
+
const replacing = Option.match(config, {
|
|
23765
|
+
onNone: () => ({
|
|
23766
|
+
default: void 0,
|
|
23767
|
+
ownRef0: false
|
|
23768
|
+
}),
|
|
23769
|
+
onSome: (read) => ({
|
|
23770
|
+
default: read.config.default_permissions ?? void 0,
|
|
23771
|
+
ownRef0: read.config.permissions?.["ref0"] !== void 0
|
|
23772
|
+
})
|
|
23773
|
+
});
|
|
23774
|
+
if (!(yield* options.approveProfile(replacing, checkouts))) return false;
|
|
23775
|
+
yield* askCodex(options.codex, codexMethods.configBatchWrite, {
|
|
23776
|
+
edits: [{
|
|
23777
|
+
keyPath: "permissions.ref0",
|
|
23778
|
+
value: ref0Profile(checkouts),
|
|
23779
|
+
mergeStrategy: "replace"
|
|
23780
|
+
}, {
|
|
23781
|
+
keyPath: "default_permissions",
|
|
23782
|
+
value: "ref0",
|
|
23783
|
+
mergeStrategy: "replace"
|
|
23784
|
+
}],
|
|
23785
|
+
filePath: null,
|
|
23786
|
+
expectedVersion: null,
|
|
23787
|
+
reloadUserConfig: false
|
|
23788
|
+
}, env).pipe(Effect.mapError(() => failed("set ref0’s profile in Codex")));
|
|
23789
|
+
return true;
|
|
23790
|
+
});
|
|
23791
|
+
const waiting = hooks.filter(waitingForTrust);
|
|
23792
|
+
const trusted = waiting.length === 0 || (yield* trust(waiting));
|
|
23793
|
+
const profile = yield* offerProfile();
|
|
23794
|
+
yield* installCodexCommits(agent, {
|
|
23795
|
+
...options,
|
|
23796
|
+
gitFolder
|
|
23797
|
+
});
|
|
23798
|
+
return {
|
|
23799
|
+
folder,
|
|
23800
|
+
trusted,
|
|
23801
|
+
profile
|
|
23802
|
+
};
|
|
23803
|
+
}).pipe(Effect.onError(() => clearOtherCodexAuthor(agent, options).pipe(Effect.orDie)));
|
|
22005
23804
|
});
|
|
22006
23805
|
//#endregion
|
|
22007
23806
|
//#region src/harness/install.ts
|
|
22008
|
-
/**
|
|
23807
|
+
/**
|
|
23808
|
+
* A command on this machine's `PATH`, by absolute path: a folder `PATH` names relative to this
|
|
23809
|
+
* one is read from here, since a harness's commands run from an empty folder of their own. Each
|
|
23810
|
+
* folder is kept as written, `..` and all, so the system follows a symlink before the `..` after it.
|
|
23811
|
+
*/
|
|
22009
23812
|
const onPath = Effect.fnUntraced(function* (name) {
|
|
22010
23813
|
const fs = yield* FileSystem.FileSystem;
|
|
23814
|
+
const paths = yield* Path.Path;
|
|
22011
23815
|
const path = yield* Config.String("PATH").pipe(Config.withDefault(""));
|
|
22012
23816
|
for (const folder of path.split(":")) {
|
|
22013
23817
|
if (folder === "") continue;
|
|
22014
|
-
const candidate = `${folder}/${name}`;
|
|
23818
|
+
const candidate = `${paths.isAbsolute(folder) ? folder : `${process.cwd()}/${folder}`}/${name}`;
|
|
22015
23819
|
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) return Option.some(candidate);
|
|
22016
23820
|
}
|
|
22017
23821
|
return Option.none();
|
|
@@ -22020,41 +23824,27 @@ const onPath = Effect.fnUntraced(function* (name) {
|
|
|
22020
23824
|
const runs$2 = (hook) => hook.handlerType === "command" ? printable(hook.command ?? "") : `the ${printable(hook.tool ?? "")} tool on MCP server ${printable(hook.server ?? "")}`;
|
|
22021
23825
|
/**
|
|
22022
23826
|
* Shows the person ref0's hooks for Codex, on stderr so a JSON answer stays alone on stdout,
|
|
22023
|
-
* and asks whether Codex may run them.
|
|
22024
|
-
*
|
|
22025
|
-
|
|
22026
|
-
|
|
22027
|
-
const askToTrust = (hooks, shown, asked) => Effect.gen(function* () {
|
|
22028
|
-
const terminal = process.stdin.isTTY === true && process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
22029
|
-
if (!asked || !terminal) return false;
|
|
22030
|
-
yield* Console.error([`Codex runs a plugin's hooks only once you trust them. ref0's, for ${printable(shown)}, are:`, ...hooks.map((hook) => `- ${hook.eventName}: ${runs$2(hook)}`)].join("\n"));
|
|
22031
|
-
return yield* Prompt.run(Prompt.Confirm({
|
|
22032
|
-
message: "Trust them, so Codex runs them?",
|
|
22033
|
-
initial: false
|
|
22034
|
-
}));
|
|
22035
|
-
}).pipe(Effect.orElseSucceed(() => false));
|
|
23827
|
+
* and asks whether Codex may run them. On a no, or with no terminal to ask in, the hooks stay
|
|
23828
|
+
* off until the person runs it again or trusts them in Codex itself.
|
|
23829
|
+
*/
|
|
23830
|
+
const askToTrust = (hooks, shown, yesOrNo) => yesOrNo([`Codex runs a plugin's hooks only once you trust them. ref0's, for ${printable(shown)}, are:`, ...hooks.map((hook) => `- ${hook.eventName}: ${runs$2(hook)}`)], "Trust them, so Codex runs them?");
|
|
22036
23831
|
/**
|
|
22037
23832
|
* Shows the person ref0's sandbox profile for Codex, on stderr, and asks whether it may be
|
|
22038
|
-
* Codex's default
|
|
22039
|
-
*
|
|
22040
|
-
*/
|
|
22041
|
-
const askForProfile = (replacing, shown,
|
|
22042
|
-
|
|
22043
|
-
|
|
22044
|
-
|
|
22045
|
-
|
|
22046
|
-
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
|
|
22050
|
-
|
|
22051
|
-
|
|
22052
|
-
|
|
22053
|
-
return yield* Prompt.run(Prompt.Confirm({
|
|
22054
|
-
message: `Make "ref0" the default profile for ${printable(shown)}?`,
|
|
22055
|
-
initial: false
|
|
22056
|
-
}));
|
|
22057
|
-
}).pipe(Effect.orElseSucceed(() => false));
|
|
23833
|
+
* Codex's default. A change to their Codex's security, so it's shown in full and asked the first
|
|
23834
|
+
* time (decided 9 Oct, 5).
|
|
23835
|
+
*/
|
|
23836
|
+
const askForProfile = (replacing, checkouts, shown, yesOrNo) => yesOrNo([
|
|
23837
|
+
"Codex's sandbox keeps commands from writing .git or reaching the network, so a Codex agent can't commit or push to ref0.",
|
|
23838
|
+
"ref0's profile, \"ref0\", is Codex's workspace profile with these changes:",
|
|
23839
|
+
"- each workspace root's .git is writable, so an agent can commit",
|
|
23840
|
+
`- ${printable(checkouts)} is writable, so a signed-off checkout can keep you as author; the agent’s author files stay outside it`,
|
|
23841
|
+
"- the network is on, so an agent can push; any command can then reach the internet, services and sockets on this machine, and your login Keychain",
|
|
23842
|
+
"In a linked worktree (git worktree add), Codex still needs full access there.",
|
|
23843
|
+
...replacing.default === void 0 ? [] : [`It replaces your default now, ${printable(replacing.default)}.`],
|
|
23844
|
+
...replacing.ownRef0 ? ["It replaces the profile you have named \"ref0\", and every rule in it."] : []
|
|
23845
|
+
], `Make "ref0" the default profile for ${printable(shown)}?`);
|
|
23846
|
+
/** Shows the person what naming the agent as author changes in Codex, and asks whether it may. */
|
|
23847
|
+
const askForCommits = (shown, yesOrNo) => yesOrNo(["ref0 will set GIT_CONFIG_PARAMETERS and REF0_HARNESS in this Codex setup’s shell_environment_policy, and exclude inherited GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL. Its agents will author their commits, with you as committer. Signed-off checkouts keep you as author. Your own terminal is unchanged."], `Name the agent as author of its Codex commits, for ${printable(shown)}?`);
|
|
22058
23848
|
/** What a Codex folder without ref0's profile, or full access, can't do. */
|
|
22059
23849
|
const sandboxWords = "Codex’s sandbox is as it was, so its agents commit and push to ref0 only where it has full access; to change that, run it again in a terminal.";
|
|
22060
23850
|
/**
|
|
@@ -22105,44 +23895,52 @@ const writeRef0Launcher = Effect.fnUntraced(function* (home, git) {
|
|
|
22105
23895
|
const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
22106
23896
|
const home = yield* Config.String("HOME");
|
|
22107
23897
|
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
22108
|
-
const
|
|
23898
|
+
const yesOrNo = options.asJson ? () => Effect.succeed(false) : yield* yesOrNoQuestions;
|
|
22109
23899
|
const answers = /* @__PURE__ */ new Map();
|
|
22110
23900
|
const failAll = (into, failed) => {
|
|
22111
23901
|
for (const folder of into) answers.set(folder, Result.fail(failed));
|
|
22112
23902
|
};
|
|
23903
|
+
const started = /* @__PURE__ */ new Set();
|
|
23904
|
+
const clearUnstarted = (into) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearOtherCodexAuthor(agent, {
|
|
23905
|
+
configFolder: folder.configFolder,
|
|
23906
|
+
home
|
|
23907
|
+
}).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
|
|
22113
23908
|
const launcher = yield* Effect.cached(Effect.gen(function* () {
|
|
22114
23909
|
const git = yield* onPath("git");
|
|
22115
23910
|
if (Option.isNone(git)) return yield* new InstallFailed({ says: "Git isn’t on this machine’s PATH. Install it." });
|
|
22116
23911
|
yield* writeRef0Launcher(home, git.value).pipe(Effect.mapError(() => new InstallFailed({ says: "Couldn’t write ~/.ref0. Check that it’s writable." })));
|
|
22117
23912
|
}));
|
|
22118
|
-
|
|
22119
|
-
const
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
|
|
22126
|
-
|
|
22127
|
-
yield* launcher;
|
|
22128
|
-
const release = yield* marketplaceFor(harness, {
|
|
22129
|
-
command: command.value,
|
|
22130
|
-
home,
|
|
22131
|
-
configFolder: into[0]?.configFolder ?? home
|
|
22132
|
-
});
|
|
22133
|
-
for (const folder of into) {
|
|
22134
|
-
const installed = yield* (harness === "claude-code" ? installedInClaudeCode(release, folder.configFolder) : installedInCodex(release, {
|
|
22135
|
-
codex: command.value,
|
|
22136
|
-
home,
|
|
22137
|
-
configFolder: folder.configFolder,
|
|
22138
|
-
name,
|
|
22139
|
-
asked: !options.asJson,
|
|
22140
|
-
terminal
|
|
22141
|
-
})).pipe(Effect.result);
|
|
22142
|
-
answers.set(folder, installed);
|
|
23913
|
+
yield* Effect.gen(function* () {
|
|
23914
|
+
for (const harness of ConnectHarness.literals) {
|
|
23915
|
+
const into = folders.filter((folder) => folder.harness === harness);
|
|
23916
|
+
if (into.length === 0) continue;
|
|
23917
|
+
const command = yield* onPath(harness === "codex" ? "codex" : "claude");
|
|
23918
|
+
if (Option.isNone(command)) {
|
|
23919
|
+
failAll(into, new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` }));
|
|
23920
|
+
yield* clearUnstarted(into);
|
|
23921
|
+
continue;
|
|
22143
23922
|
}
|
|
22144
|
-
|
|
22145
|
-
|
|
23923
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
23924
|
+
yield* launcher;
|
|
23925
|
+
const release = yield* marketplaceFor(harness, {
|
|
23926
|
+
command: command.value,
|
|
23927
|
+
home,
|
|
23928
|
+
configFolder: into[0]?.configFolder ?? home
|
|
23929
|
+
});
|
|
23930
|
+
for (const folder of into) {
|
|
23931
|
+
started.add(folder);
|
|
23932
|
+
const installed = yield* (harness === "claude-code" ? installedInClaudeCode(release, folder.configFolder) : installedInCodex(release, {
|
|
23933
|
+
codex: command.value,
|
|
23934
|
+
home,
|
|
23935
|
+
configFolder: folder.configFolder,
|
|
23936
|
+
name,
|
|
23937
|
+
yesOrNo
|
|
23938
|
+
})).pipe(Effect.result);
|
|
23939
|
+
answers.set(folder, installed);
|
|
23940
|
+
}
|
|
23941
|
+
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
|
|
23942
|
+
}
|
|
23943
|
+
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
|
|
22146
23944
|
return folders.map((folder) => ({
|
|
22147
23945
|
...folder,
|
|
22148
23946
|
installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
|
|
@@ -22157,14 +23955,27 @@ const installedInClaudeCode = Effect.fnUntraced(function* (release, configFolder
|
|
|
22157
23955
|
words: `Installed in Claude Code, from ${folder}.`
|
|
22158
23956
|
};
|
|
22159
23957
|
});
|
|
23958
|
+
/** The agent a Codex folder's host grant names, whose ID its commits' author file is named by. */
|
|
23959
|
+
const codexAgentOf = (configFolder) => HostGrant.use((grants) => grants.get(configFolder)).pipe(Effect.catchTag("KeychainError", (failed) => Effect.fail(failed.unreadable === true ? new InstallFailed({
|
|
23960
|
+
says: "Its host grant can’t be read.",
|
|
23961
|
+
run: "ref0 connect"
|
|
23962
|
+
}) : new InstallFailed({ says: failed.nextStep }))), Effect.flatMap(Option.match({
|
|
23963
|
+
onNone: () => Effect.fail(new InstallFailed({
|
|
23964
|
+
says: "It isn’t connected.",
|
|
23965
|
+
run: "ref0 connect"
|
|
23966
|
+
})),
|
|
23967
|
+
onSome: (grant) => Effect.succeed(grant)
|
|
23968
|
+
})));
|
|
22160
23969
|
const installedInCodex = Effect.fnUntraced(function* (release, options) {
|
|
22161
23970
|
const shown = shownConfigFolder(options.home, options.configFolder);
|
|
22162
|
-
const
|
|
23971
|
+
const agent = yield* codexAgentOf(options.configFolder);
|
|
23972
|
+
const done = yield* installCodex(release, agent, {
|
|
22163
23973
|
codex: options.codex,
|
|
22164
23974
|
home: options.home,
|
|
22165
23975
|
configFolder: options.configFolder,
|
|
22166
|
-
approve: (hooks) => askToTrust(hooks, shown, options.
|
|
22167
|
-
approveProfile: (replacing) => askForProfile(replacing, shown, options.
|
|
23976
|
+
approve: (hooks) => askToTrust(hooks, shown, options.yesOrNo),
|
|
23977
|
+
approveProfile: (replacing, checkouts) => askForProfile(replacing, checkouts, shown, options.yesOrNo),
|
|
23978
|
+
approveCommits: () => askForCommits(shown, options.yesOrNo)
|
|
22168
23979
|
});
|
|
22169
23980
|
const sandbox = done.profile ? "" : ` ${sandboxWords}`;
|
|
22170
23981
|
const trust = trustRef0Hooks(installLine(options.configFolder), `${options.name}@${options.name}`);
|
|
@@ -30592,6 +32403,7 @@ var Waker = class Waker extends Context.Service()("ref0/cli/Waker") {
|
|
|
30592
32403
|
const socket = yield* Config.String("CLAUDE_CODE_MESSAGING_SOCKET").pipe(Config.option);
|
|
30593
32404
|
const token = yield* Config.Redacted("CLAUDE_CODE_MESSAGING_TOKEN").pipe(Config.option);
|
|
30594
32405
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
32406
|
+
const fs = yield* FileSystem.FileSystem;
|
|
30595
32407
|
const post = Option.zipWith(socket, token, (path, secret) => Effect.callback((resume) => {
|
|
30596
32408
|
const connection = connect(path);
|
|
30597
32409
|
connection.on("error", () => resume(Effect.succeed(false)));
|
|
@@ -30618,6 +32430,7 @@ var Waker = class Waker extends Context.Service()("ref0/cli/Waker") {
|
|
|
30618
32430
|
"--message",
|
|
30619
32431
|
wakeLine
|
|
30620
32432
|
], {
|
|
32433
|
+
cwd: yield* emptyFolder(fs),
|
|
30621
32434
|
env: { CODEX_HOME: setup.home },
|
|
30622
32435
|
extendEnv: true
|
|
30623
32436
|
}));
|
|
@@ -31731,7 +33544,7 @@ const codexSetupHere = Effect.fnUntraced(function* (process) {
|
|
|
31731
33544
|
const mcp = Command.make(harnessCommandNames.mcp, {}, () => Effect.gen(function* () {
|
|
31732
33545
|
const agents = yield* ConfigFolders;
|
|
31733
33546
|
const found = yield* (yield* HarnessProcess).innermost;
|
|
31734
|
-
const agent = Option.isSome(found) ? yield* (yield* HostGrant).get(yield* agents.folder(found.value.harness)) : Option.none();
|
|
33547
|
+
const agent = Option.isSome(found) ? yield* (yield* HostGrant).get(yield* agents.folder(found.value.harness)).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone)) : Option.none();
|
|
31735
33548
|
const codex = Option.isSome(found) && found.value.harness === "codex" ? yield* codexSetupHere(found.value.process) : Option.none();
|
|
31736
33549
|
const reinstall = Option.isSome(found) && Option.isSome(agent) ? installLine(yield* agents.folder(found.value.harness)) : "ref0 connect";
|
|
31737
33550
|
return yield* Layer.launch(mcpServer(cliVersion, {
|
|
@@ -31827,6 +33640,148 @@ const endHarnessSession = Effect.gen(function* () {
|
|
|
31827
33640
|
}).pipe(Effect.ignore);
|
|
31828
33641
|
const sessionEnd = Command.make(harnessCommandNames.sessionEnd, { protocol: protocolFlag }, (flags) => flags.protocol === "1" ? endHarnessSession : Effect.void).pipe(Command.withDescription("Ends the ref0 session of a harness session that's closing"), Command.withShortDescription("End the harness’s ref0 session"));
|
|
31829
33642
|
//#endregion
|
|
33643
|
+
//#region src/harness/commit-environment.ts
|
|
33644
|
+
/**
|
|
33645
|
+
* The agent as the author of its commits, set at a Claude Code session's start in the
|
|
33646
|
+
* environment of the agent's own commands, and nowhere else: the file `CLAUDE_ENV_FILE` names,
|
|
33647
|
+
* which Claude Code runs ahead of each of them. It sets Git's author to the agent, by its owner
|
|
33648
|
+
* and handle and its commit email; one `GIT_CONFIG_*` entry naming ref0's hooks folder as
|
|
33649
|
+
* `core.hooksPath`, after any the command already has; and, for the trailer, the harness, the
|
|
33650
|
+
* session's transcript and the model it started with, both from Claude Code's `SessionStart`.
|
|
33651
|
+
* Its other hooks carry no model, so the trailer reads the transcript's last. Git takes the
|
|
33652
|
+
* committer from the person's own config, with their signature, and a commit the person makes in
|
|
33653
|
+
* their own terminal has none of it.
|
|
33654
|
+
*
|
|
33655
|
+
* The agent is the one the session's config folder acts as, from that folder's host grant, which
|
|
33656
|
+
* `ref0 connect` keeps with the agent's ID. It sets no author when the grant can't be read, or
|
|
33657
|
+
* when the session's checkout signs off its commits: any of the last 20 on its default branch
|
|
33658
|
+
* has a `Signed-off-by`, or it has `.github/dco.yml`. A DCO check wants each sign-off to match
|
|
33659
|
+
* its commit's author, and an agent never signs off. The hooks path is still set.
|
|
33660
|
+
*/
|
|
33661
|
+
/** How many commits on the default branch a session's start reads for a sign-off. */
|
|
33662
|
+
const signedOffWithin = 20;
|
|
33663
|
+
/** A model as the trailer names it, which the hook also checks: one word, without a newline. */
|
|
33664
|
+
const modelName = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
33665
|
+
/**
|
|
33666
|
+
* The model a session started with, as the trailer names it: none for a value it can't name.
|
|
33667
|
+
* Claude Code may follow the model with its context window, as `[1m]`, which isn't the model.
|
|
33668
|
+
*/
|
|
33669
|
+
const startingModel = (model) => Option.filter(Option.fromNullishOr(model?.replace(/\[[^\]]*\]$/, "")), (name) => modelName.test(name));
|
|
33670
|
+
/** A variable the file exports, or unsets when the session has no value for it. */
|
|
33671
|
+
const exported = (name, value) => Option.match(value, {
|
|
33672
|
+
onNone: () => `unset ${name}`,
|
|
33673
|
+
onSome: (word) => `export ${name}=${shellWord(word)}`
|
|
33674
|
+
});
|
|
33675
|
+
/**
|
|
33676
|
+
* What a session's start adds to the file Claude Code runs ahead of the agent's commands. The
|
|
33677
|
+
* hooks path goes after the entries a command already has, once however often the file runs.
|
|
33678
|
+
* Without an author, it drops one an outer agent's session set.
|
|
33679
|
+
*/
|
|
33680
|
+
const environmentScript = (set) => [
|
|
33681
|
+
"",
|
|
33682
|
+
"# ref0: the agent as the author of its commits, and ref0's Git hooks, in its commands",
|
|
33683
|
+
...Option.match(set.author, {
|
|
33684
|
+
onNone: () => [`case \${GIT_AUTHOR_EMAIL-} in *@${agentCommitDomain}) unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL ;; esac`],
|
|
33685
|
+
onSome: (author) => [`export GIT_AUTHOR_NAME=${shellWord(author.name)}`, `export GIT_AUTHOR_EMAIL=${shellWord(author.email)}`]
|
|
33686
|
+
}),
|
|
33687
|
+
`export ${gitHookVariables.harness}=${set.harness}`,
|
|
33688
|
+
exported(gitHookVariables.transcript, set.transcript),
|
|
33689
|
+
exported(gitHookVariables.model, set.model),
|
|
33690
|
+
"if [ -z \"${ref0_hooks_set-}\" ]; then",
|
|
33691
|
+
" ref0_n=${GIT_CONFIG_COUNT:-0}",
|
|
33692
|
+
` export "GIT_CONFIG_KEY_$ref0_n=core.hooksPath" "GIT_CONFIG_VALUE_$ref0_n="${shellWord(set.folder)} "GIT_CONFIG_COUNT=$((ref0_n + 1))"`,
|
|
33693
|
+
" ref0_hooks_set=1",
|
|
33694
|
+
" unset ref0_n",
|
|
33695
|
+
"fi",
|
|
33696
|
+
""
|
|
33697
|
+
].join("\n");
|
|
33698
|
+
/** How the agent runs a tool that installs hooks where `core.hooksPath` points. */
|
|
33699
|
+
const hookInstallerPrefix = "env -u GIT_CONFIG_COUNT";
|
|
33700
|
+
/**
|
|
33701
|
+
* What the agent reads at its session's start once its commands name ref0's hooks folder: an
|
|
33702
|
+
* installer run there fails on that read-only folder, and without ref0's entry it installs into
|
|
33703
|
+
* the repo's own.
|
|
33704
|
+
*/
|
|
33705
|
+
const hookInstallerWords = `ref0: Git in your commands runs hooks through ref0's read-only folder, so run a hook installer, such as git lfs install, pre-commit install, lefthook install or simple-git-hooks, as ${hookInstallerPrefix} <command>, to install into the repo's own hooks folder.`;
|
|
33706
|
+
/** Codex uses an include in `GIT_CONFIG_PARAMETERS`, leaving a launcher's count alone. */
|
|
33707
|
+
const codexHookInstallerWords = hookInstallerWords.replace(hookInstallerPrefix, "env -u GIT_CONFIG_PARAMETERS");
|
|
33708
|
+
/**
|
|
33709
|
+
* What the session's checkout says: which of ref0's hooks folders it needs, `all` when it has
|
|
33710
|
+
* its own frequent hook, and whether it signs off its commits. Outside a checkout, neither.
|
|
33711
|
+
*/
|
|
33712
|
+
const checkoutAt = Effect.fnUntraced(function* (cwd) {
|
|
33713
|
+
const fs = yield* FileSystem.FileSystem;
|
|
33714
|
+
const path = yield* Path.Path;
|
|
33715
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
33716
|
+
const git = (folder, args) => spawner.string(ChildProcess.make("git", [
|
|
33717
|
+
"-C",
|
|
33718
|
+
folder,
|
|
33719
|
+
...args
|
|
33720
|
+
])).pipe(Effect.catchCause(() => Effect.succeed("")));
|
|
33721
|
+
const executable = (file) => fs.stat(file).pipe(Effect.map((info) => info.type === "File" && (info.mode & 73) !== 0), Effect.orElseSucceed(() => false));
|
|
33722
|
+
const top = (yield* git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
33723
|
+
const outside = {
|
|
33724
|
+
folder: "most",
|
|
33725
|
+
signsOff: false
|
|
33726
|
+
};
|
|
33727
|
+
if (top === "") return outside;
|
|
33728
|
+
const hooks = (yield* git(top, ["rev-parse", ...frequentHooks.flatMap((name) => ["--git-path", `hooks/${name}`])])).split("\n").filter(Boolean);
|
|
33729
|
+
const folder = (yield* Effect.forEach(hooks, (hook) => executable(path.resolve(top, hook)))).some(Boolean) ? "all" : "most";
|
|
33730
|
+
if (yield* fs.exists(path.join(top, ".github", "dco.yml")).pipe(Effect.orElseSucceed(() => false))) return {
|
|
33731
|
+
folder,
|
|
33732
|
+
signsOff: true
|
|
33733
|
+
};
|
|
33734
|
+
const defaultBranch = (yield* git(top, [
|
|
33735
|
+
"symbolic-ref",
|
|
33736
|
+
"-q",
|
|
33737
|
+
"refs/remotes/origin/HEAD"
|
|
33738
|
+
])).trim();
|
|
33739
|
+
return {
|
|
33740
|
+
folder,
|
|
33741
|
+
signsOff: (yield* git(top, [
|
|
33742
|
+
"log",
|
|
33743
|
+
"--no-show-signature",
|
|
33744
|
+
"-n",
|
|
33745
|
+
`${signedOffWithin}`,
|
|
33746
|
+
"--format=%(trailers:key=Signed-off-by,valueonly)",
|
|
33747
|
+
defaultBranch === "" ? "HEAD" : defaultBranch,
|
|
33748
|
+
"--"
|
|
33749
|
+
])).trim() !== ""
|
|
33750
|
+
};
|
|
33751
|
+
});
|
|
33752
|
+
/**
|
|
33753
|
+
* Adds the agent's commit environment to the file Claude Code names, for the session its
|
|
33754
|
+
* `SessionStart` opens: only in Claude Code, and only for a setup with an agent. ref0's hooks
|
|
33755
|
+
* folder is written first, so the file never names a folder that isn't there. Whether it set
|
|
33756
|
+
* ref0's hooks path.
|
|
33757
|
+
*/
|
|
33758
|
+
const setCommitEnvironment = Effect.fnUntraced(function* (session) {
|
|
33759
|
+
const fs = yield* FileSystem.FileSystem;
|
|
33760
|
+
const folders = yield* ConfigFolders;
|
|
33761
|
+
const file = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
|
|
33762
|
+
if (file === "") {
|
|
33763
|
+
if ((yield* Config.String("CODEX_HOME").pipe(Config.withDefault(""))) === "") return false;
|
|
33764
|
+
return yield* setCodexCommitEnvironment(session.cwd ?? ".", yield* checkoutAt(session.cwd ?? "."));
|
|
33765
|
+
}
|
|
33766
|
+
const connected = yield* (yield* HostGrant).get(yield* folders.folder("claude-code")).pipe(Effect.map(Option.map(Option.some)), Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedSome(Option.none())));
|
|
33767
|
+
if (Option.isNone(connected)) return false;
|
|
33768
|
+
const checkout = yield* checkoutAt(session.cwd ?? ".");
|
|
33769
|
+
const folder = yield* hooksFolder(yield* gitHooksHome(yield* Config.String("HOME"), (yield* Ref0Config).api), checkout.folder);
|
|
33770
|
+
const author = checkout.signsOff ? Option.none() : Option.map(connected.value, (grant) => ({
|
|
33771
|
+
name: agentCommitName(grant.owner, grant.agent),
|
|
33772
|
+
email: agentCommitEmail(grant.agentId)
|
|
33773
|
+
}));
|
|
33774
|
+
const script = environmentScript({
|
|
33775
|
+
author,
|
|
33776
|
+
folder,
|
|
33777
|
+
harness: "claude-code",
|
|
33778
|
+
transcript: Option.filter(Option.fromNullishOr(session.transcript_path), (path) => path !== ""),
|
|
33779
|
+
model: startingModel(session.model)
|
|
33780
|
+
});
|
|
33781
|
+
yield* fs.writeFileString(file, script, { flag: "a" });
|
|
33782
|
+
return true;
|
|
33783
|
+
});
|
|
33784
|
+
//#endregion
|
|
31830
33785
|
//#region src/harness/session-start.ts
|
|
31831
33786
|
/**
|
|
31832
33787
|
* `ref0 hook session-start`: the harness's `SessionStart` hook, a command, before `ref0 mcp`
|
|
@@ -31876,11 +33831,22 @@ const startOnHook = (protocol) => Effect.gen(function* () {
|
|
|
31876
33831
|
const input = yield* stdio.stdin.pipe(Stream.decodeText(), Stream.mkString);
|
|
31877
33832
|
const hook = decodeHook(input);
|
|
31878
33833
|
if (Option.isNone(hook)) return;
|
|
33834
|
+
if (yield* setCommitEnvironment(hook.value).pipe(Effect.orElseSucceed(() => false))) {
|
|
33835
|
+
const claude = yield* Config.String("CLAUDE_ENV_FILE").pipe(Config.withDefault(""));
|
|
33836
|
+
yield* Console.log(claude === "" ? codexHookInstallerWords : hookInstallerWords);
|
|
33837
|
+
}
|
|
31879
33838
|
const binding = yield* bindingHere(hook.value.session_id);
|
|
31880
33839
|
if (Option.isNone(binding)) return;
|
|
31881
33840
|
const harnessName = binding.value.harness === "codex" ? "codex" : "claude-code";
|
|
31882
33841
|
const folder = yield* agents.folder(harnessName);
|
|
31883
|
-
const
|
|
33842
|
+
const kept = yield* (yield* HostGrant).get(folder).pipe(Effect.map((agent) => ({
|
|
33843
|
+
agent,
|
|
33844
|
+
readable: true
|
|
33845
|
+
})), Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeed({
|
|
33846
|
+
agent: Option.none(),
|
|
33847
|
+
readable: false
|
|
33848
|
+
})));
|
|
33849
|
+
const agent = kept.agent;
|
|
31884
33850
|
const repair = Option.isSome(agent) ? installLine(folder) : yield* connectLineFor(harnessName, folder);
|
|
31885
33851
|
const update = updateLine({
|
|
31886
33852
|
protocol,
|
|
@@ -31889,7 +33855,8 @@ const startOnHook = (protocol) => Effect.gen(function* () {
|
|
|
31889
33855
|
if (Option.isSome(update)) return yield* Console.log(update.value);
|
|
31890
33856
|
if (Option.isNone(agent)) {
|
|
31891
33857
|
const name = harnessName === "codex" ? "Codex" : "Claude Code";
|
|
31892
|
-
|
|
33858
|
+
const why = kept.readable ? `no agent is connected for this ${name} config folder, ${folder}` : `the host grant kept for this ${name} config folder, ${folder}, can't be read`;
|
|
33859
|
+
return yield* Console.log(`ref0: ${why}, so no session starts. In your own terminal, run: ${repair}`);
|
|
31893
33860
|
}
|
|
31894
33861
|
const account = sessionAccount(binding.value);
|
|
31895
33862
|
const held = yield* keychain.get(account);
|
|
@@ -31984,24 +33951,26 @@ const claudeCodeChecks = Effect.fnUntraced(function* (configFolder) {
|
|
|
31984
33951
|
const claude = yield* onPath("claude");
|
|
31985
33952
|
if (Option.isNone(claude)) return [checkOf("plugin", "unknown", "Claude Code isn’t on PATH", reinstall)];
|
|
31986
33953
|
const home = yield* Config.String("HOME").pipe(Config.withDefault("."));
|
|
31987
|
-
const
|
|
33954
|
+
const fs = yield* FileSystem.FileSystem;
|
|
33955
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
33956
|
+
const listed = yield* Effect.scoped(Effect.flatMap(emptyFolder(fs), (cwd) => spawner.string(ChildProcess.make(claude.value, [
|
|
31988
33957
|
"plugin",
|
|
31989
33958
|
"list",
|
|
31990
33959
|
"--json"
|
|
31991
33960
|
], {
|
|
33961
|
+
cwd,
|
|
31992
33962
|
env: {
|
|
31993
33963
|
HOME: home,
|
|
31994
33964
|
CLAUDE_CONFIG_DIR: configFolder
|
|
31995
33965
|
},
|
|
31996
33966
|
extendEnv: true
|
|
31997
|
-
})).pipe(Effect.flatMap(decodePlugins), Effect.timeout("15 seconds"), Effect.option);
|
|
33967
|
+
})))).pipe(Effect.flatMap(decodePlugins), Effect.timeout("15 seconds"), Effect.option);
|
|
31998
33968
|
if (Option.isNone(listed)) return [checkOf("plugin", "unknown", "Claude Code didn’t list its plugins", reinstall)];
|
|
31999
33969
|
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
32000
33970
|
const id = `${name}@${name}`;
|
|
32001
33971
|
const plugin = listed.value.find((each) => each.id === id);
|
|
32002
33972
|
if (plugin === void 0) return [checkOf("plugin", "no", "not installed in this folder", reinstall)];
|
|
32003
33973
|
if (!plugin.enabled) return [checkOf("plugin", "no", "turned off in this folder", `CLAUDE_CONFIG_DIR=${shellWord(configFolder)} claude plugin enable ${id}`)];
|
|
32004
|
-
const fs = yield* FileSystem.FileSystem;
|
|
32005
33974
|
const folder = plugin.readFromFolder ?? plugin.installPath;
|
|
32006
33975
|
const hooks = folder === void 0 ? Option.none() : yield* fs.readFileString(`${folder}/hooks/hooks.json`).pipe(Effect.flatMap(decodeHooks), Effect.option);
|
|
32007
33976
|
return [speaksCheck(Option.match(hooks, {
|
|
@@ -32021,8 +33990,7 @@ const codexChecks = Effect.fnUntraced(function* (configFolder) {
|
|
|
32021
33990
|
const reinstall = installLine(configFolder);
|
|
32022
33991
|
const codex = yield* onPath("codex");
|
|
32023
33992
|
if (Option.isNone(codex)) return [checkOf("plugin", "unknown", "Codex isn’t on PATH", reinstall)];
|
|
32024
|
-
const
|
|
32025
|
-
const listed = yield* askCodex(codex.value, codexMethods.hooksList, { cwds: [home] }, configFolder).pipe(Effect.option);
|
|
33993
|
+
const listed = yield* askCodex(codex.value, codexMethods.hooksList, { cwds: [] }, { CODEX_HOME: configFolder }).pipe(Effect.option);
|
|
32026
33994
|
if (Option.isNone(listed)) return [checkOf("plugin", "unknown", "Codex didn’t list its hooks", reinstall)];
|
|
32027
33995
|
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
32028
33996
|
const hooks = listed.value.data.flatMap((entry) => entry.hooks).filter((hook) => pluginHook(hook, `${name}@${name}`));
|
|
@@ -32060,12 +34028,13 @@ const statusCommands = [Command.make(harnessCommandNames.status, { json }, ({ js
|
|
|
32060
34028
|
const reports = [];
|
|
32061
34029
|
for (const configFolder of paths) {
|
|
32062
34030
|
const kept = yield* Effect.result(grants.get(configFolder));
|
|
34031
|
+
const unreadable = Result.isFailure(kept) && kept.failure.unreadable === true;
|
|
32063
34032
|
const grant = Result.isSuccess(kept) ? kept.success : Option.none();
|
|
32064
34033
|
const harness = Option.isSome(grant) ? grant.value.harness : candidates.get(configFolder);
|
|
32065
34034
|
if (Result.isFailure(kept) || Option.isNone(grant)) {
|
|
32066
34035
|
const report = {
|
|
32067
34036
|
configFolder,
|
|
32068
|
-
checks: [
|
|
34037
|
+
checks: [Result.isFailure(kept) && !unreadable ? checkOf("connected", "unknown", "The keychain didn’t say whether this config folder holds a host grant.", kept.failure.nextStep) : checkOf("connected", "no", unreadable ? "its host grant can’t be read" : "not connected", harness === void 0 ? "ref0 connect" : yield* connectLineFor(harness, configFolder))]
|
|
32069
34038
|
};
|
|
32070
34039
|
reports.push(harness === void 0 ? report : {
|
|
32071
34040
|
...report,
|