@krapht/ork 1.0.0-alpha.17 → 1.0.0-alpha.19
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/{__build_info__-C1Nm0hlJ.mjs → __build_info__-dPf4cEGL.mjs} +3 -134
- package/dist/{attach-EP374oMP.mjs → attach-BQl8nQz2.mjs} +5 -5
- package/dist/{attach-RQx__RzH.mjs → attach-DhZQ87Nz.mjs} +49 -224
- package/dist/bin.mjs +204 -154
- package/dist/{brief-Bs3cMQU0.mjs → brief-BQbeoPMc.mjs} +19 -9
- package/dist/{builtin-files-yYaNTQRU.mjs → builtin-files-CWL3JEo-.mjs} +1 -1
- package/dist/{command-tree-BkSYWmYJ.mjs → command-tree-B6uvMJq1.mjs} +1 -1
- package/dist/{completion-DIXWlnAl.mjs → completion-DKPuD_Rw.mjs} +47 -14
- package/dist/{context-Dq-VM_sq.mjs → context-Bw6R_EYq.mjs} +3 -3
- package/dist/detached-BQlbmqR0.mjs +38 -0
- package/dist/{find-k489laBa.mjs → find-B30-rd-u.mjs} +1 -1
- package/dist/flags-ByYBx-Xb.mjs +170 -0
- package/dist/{follow-ups-DD8NTPe6.mjs → follow-ups-Cxca1Jgg.mjs} +24 -18
- package/dist/{format-DQc9A_ei.mjs → format-C-nEZv-P.mjs} +27 -11
- package/dist/{help-groups-hB6kY34d.mjs → help-groups-BpRgBSTh.mjs} +13 -7
- package/dist/{in-a-run-D_9GlAlS.mjs → in-a-run-CZFzoT3R.mjs} +2 -2
- package/dist/{install-PVlV2QPq.mjs → install-CsqK1avz.mjs} +2 -183
- package/dist/{limits-CHqBO7wb.mjs → limits-CXh052II.mjs} +1 -1
- package/dist/{mcp-D38Ajb0b.mjs → mcp-B4wME9C4.mjs} +6 -6
- package/dist/{once-DuUzpzL2.mjs → once-BKn3pOtg.mjs} +3 -3
- package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
- package/dist/{pause-Bx3ePGrn.mjs → pause-B1grR1cl.mjs} +3 -3
- package/dist/{print-n2qetUHc.mjs → print-Bd8MWVJk.mjs} +1 -1
- package/dist/{project-records-Cune1l6I.mjs → project-records-BOEVs69i.mjs} +1 -1
- package/dist/{parse-BzWpMN-a.mjs → refusals-DQQX3AIb.mjs} +10 -1
- package/dist/{relay-D7RN4m68.mjs → relay-CeNiq3GT.mjs} +1 -1
- package/dist/{run-OOWQIKMJ.mjs → run-C0C54UiU.mjs} +116 -137
- package/dist/{run-BfxdsQJk.mjs → run-D6yU2rLf.mjs} +1 -1
- package/dist/{send-CKth_vgn.mjs → send-DqgF58hb.mjs} +2 -2
- package/dist/{src-DLsFsmOD.mjs → src-Ciy7i3se.mjs} +882 -71
- package/dist/{src-BHod38UQ.mjs → src-D2qpd-G7.mjs} +394 -247
- package/dist/{src-DmmrX2_C.mjs → src-DP02RP9O.mjs} +227 -180
- package/dist/{src-DOjTY2ET.mjs → src-n-u4TGev.mjs} +186 -43
- package/dist/{store-DACH768X.mjs → store-2KXzMCEN.mjs} +1 -1
- package/dist/ui/assets/app-C5vO1M-l.css +1 -0
- package/dist/ui/assets/{app-CN3acQ-1.js → app-ICEKpy_C.js} +21 -20
- package/dist/ui/assets/{index-B2uGxA4W.js → index-BGaix5wh.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CJ-BX12n.mjs → ui-CAvygxTb.mjs} +53 -24
- package/dist/{usage-DxZ6JGd4.mjs → usage-BFBco5OS.mjs} +7 -7
- package/dist/{validate-BS0nCccW.mjs → validate-UrmC5AG1.mjs} +5 -5
- package/dist/{watch-Dp7IB4FR.mjs → watch-DTxkgB8R.mjs} +5 -5
- package/dist/{workflows-Djo1edlK.mjs → workflows-8GV-SeRm.mjs} +28 -4
- package/dist/{worktrees-Drof_Nyx.mjs → worktrees-CUdUvZlO.mjs} +9 -6
- package/package.json +1 -1
- package/dist/flags-B4ZZWspN.mjs +0 -125
- package/dist/post-2CaJz_mV.mjs +0 -46
- package/dist/ui/assets/app-9Q-62Ywz.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { LineCounter, YAMLMap, YAMLSeq, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { appendFileSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
6
|
import { constants as constants$1, homedir, setPriority, tmpdir } from "node:os";
|
|
7
7
|
import { createConnection, createServer } from "node:net";
|
|
@@ -1268,6 +1268,13 @@ const nodeTypeMessage = (issue, value) => {
|
|
|
1268
1268
|
const types = NODE_TYPES.map((type) => markValue(type)).join(", ");
|
|
1269
1269
|
return value === void 0 ? `node needs a \`type\`: one of ${types}` : `unknown node type ${markValue(typeof value === "string" ? value : JSON.stringify(value) ?? "")}: expected one of ${types}`;
|
|
1270
1270
|
};
|
|
1271
|
+
/**
|
|
1272
|
+
* The text for a file without `schemaVersion`, else `null`: Zod's own words
|
|
1273
|
+
* ("Invalid input: expected 1") don't say the key is missing.
|
|
1274
|
+
*/
|
|
1275
|
+
const versionMessage = (issue, path, value) => issue.code === "invalid_value" && path.length === 1 && path[0] === "schemaVersion" && value === void 0 ? `schemaVersion is missing: write schemaVersion: ${String(issue.values[0])}` : null;
|
|
1276
|
+
/** The text Ork has for an issue instead of Zod's, else `null`. */
|
|
1277
|
+
const knownMessage = (issue, path, value) => forEachMessage(issue, path, value) ?? listRuleMessage(issue, value) ?? nodeTypeMessage(issue, value) ?? versionMessage(issue, path, value);
|
|
1271
1278
|
// ../schema/src/newer-version.ts
|
|
1272
1279
|
/**
|
|
1273
1280
|
* A file with a higher `schemaVersion` than this Ork reads was made by a newer
|
|
@@ -1319,7 +1326,7 @@ const describeIssue = (issue, raw, path) => {
|
|
|
1319
1326
|
};
|
|
1320
1327
|
}
|
|
1321
1328
|
const value = valueAt(raw, path);
|
|
1322
|
-
const said =
|
|
1329
|
+
const said = knownMessage(issue, path, value);
|
|
1323
1330
|
if (said !== null) return {
|
|
1324
1331
|
message: said,
|
|
1325
1332
|
path
|
|
@@ -3921,26 +3928,295 @@ const writePlugin = (stack, dir, description) => {
|
|
|
3921
3928
|
}
|
|
3922
3929
|
return dir;
|
|
3923
3930
|
};
|
|
3931
|
+
// ../core/src/store/secret-patterns.ts
|
|
3932
|
+
const SECRET_PATTERNS = [
|
|
3933
|
+
{
|
|
3934
|
+
name: "aws-access-key-id",
|
|
3935
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/u
|
|
3936
|
+
},
|
|
3937
|
+
{
|
|
3938
|
+
name: "github-token",
|
|
3939
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/u
|
|
3940
|
+
},
|
|
3941
|
+
{
|
|
3942
|
+
name: "github-fine-grained-token",
|
|
3943
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{50,}\b/u
|
|
3944
|
+
},
|
|
3945
|
+
{
|
|
3946
|
+
name: "anthropic-api-key",
|
|
3947
|
+
pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/u
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
name: "openai-api-key",
|
|
3951
|
+
pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_-]{40,}/u
|
|
3952
|
+
},
|
|
3953
|
+
{
|
|
3954
|
+
name: "npm-token",
|
|
3955
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/u
|
|
3956
|
+
},
|
|
3957
|
+
{
|
|
3958
|
+
name: "slack-token",
|
|
3959
|
+
pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}/u
|
|
3960
|
+
},
|
|
3961
|
+
{
|
|
3962
|
+
name: "google-api-key",
|
|
3963
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/u
|
|
3964
|
+
},
|
|
3965
|
+
{
|
|
3966
|
+
name: "stripe-live-key",
|
|
3967
|
+
pattern: /\b[rs]k_live_[0-9A-Za-z]{20,}\b/u
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
name: "private-key-block",
|
|
3971
|
+
pattern: /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY(?: BLOCK)?-----/u
|
|
3972
|
+
}
|
|
3973
|
+
];
|
|
3924
3974
|
// ../core/src/store/mask.ts
|
|
3925
3975
|
/**
|
|
3926
3976
|
* @file Masks secrets before anything is stored: tokens and keys in common
|
|
3927
3977
|
* formats, anywhere in an event's strings. The store and feedback records use
|
|
3928
3978
|
* the same patterns; the terminal is not masked (it is the user's own).
|
|
3929
3979
|
*/
|
|
3930
|
-
|
|
3980
|
+
/** Looser than the shared list, for masking only; they run first. */
|
|
3981
|
+
const MASK_ONLY = [
|
|
3931
3982
|
/sk-ant-[\w-]{20,}/gu,
|
|
3932
3983
|
/\bgh[opsur]_[A-Za-z0-9]{20,}/gu,
|
|
3933
3984
|
/\bgithub_pat_\w{20,}/gu,
|
|
3934
|
-
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/gu,
|
|
3935
3985
|
/\bxox[abprs]-[\w-]{10,}/gu,
|
|
3936
3986
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/gu,
|
|
3937
3987
|
/(?<=\bbearer\s+)[\w.~+/-]{20,}=*/giu,
|
|
3938
3988
|
/(?<=aws_secret_access_key\s*[=:]\s*)\S+/giu
|
|
3939
3989
|
];
|
|
3990
|
+
/**
|
|
3991
|
+
* The value after a secret-sounding key, as `key=v`, `key: v` or JSON's `"key":
|
|
3992
|
+
* "v"`; 8 characters or more, so short words and fixtures stay. A `{{…}}`
|
|
3993
|
+
* template is not a value, so a workflow's `env:` stays readable.
|
|
3994
|
+
*/
|
|
3995
|
+
const GENERIC = /(?<=(?:password|passwd|secret|token|api[_-]key)["']?\s*[=:]\s*["']?)(?!\{\{)[^\s"'`&,;]{8,}/giu;
|
|
3996
|
+
const PATTERNS = [
|
|
3997
|
+
...MASK_ONLY,
|
|
3998
|
+
...SECRET_PATTERNS.map(({ pattern }) => new RegExp(pattern.source, `${pattern.flags}g`)),
|
|
3999
|
+
GENERIC
|
|
4000
|
+
];
|
|
3940
4001
|
const MASK = "[masked]";
|
|
3941
4002
|
const maskSecrets = (text) => PATTERNS.reduce((out, pattern) => out.replaceAll(pattern, MASK), text);
|
|
3942
4003
|
/** A `JSON.stringify` replacer that masks every string. */
|
|
3943
4004
|
const maskReplacer = (_key, value) => typeof value === "string" ? maskSecrets(value) : value;
|
|
4005
|
+
// ../core/src/control/queue.ts
|
|
4006
|
+
const PERMISSION_OPTIONS = [{
|
|
4007
|
+
id: "allow",
|
|
4008
|
+
label: "Allow"
|
|
4009
|
+
}, {
|
|
4010
|
+
id: "deny",
|
|
4011
|
+
label: "Deny"
|
|
4012
|
+
}];
|
|
4013
|
+
const DENIED = "The user did not allow this tool.";
|
|
4014
|
+
/** The deny message a session gets, with the person's words when given. */
|
|
4015
|
+
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
4016
|
+
/** The input fields that say what a tool will do, the first one present. */
|
|
4017
|
+
const TOOL_INPUT_KEYS = [
|
|
4018
|
+
"command",
|
|
4019
|
+
"file_path",
|
|
4020
|
+
"path",
|
|
4021
|
+
"url",
|
|
4022
|
+
"pattern"
|
|
4023
|
+
];
|
|
4024
|
+
/** A tool's input for the decision's context: as written, cut at `max`. */
|
|
4025
|
+
const inputContext = (input, max = 4e3) => {
|
|
4026
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
4027
|
+
const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
|
|
4028
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
4029
|
+
};
|
|
4030
|
+
const createDecisionQueue = (options) => {
|
|
4031
|
+
const { store, runId, emit } = options;
|
|
4032
|
+
const earlier = store.decisions({ runId });
|
|
4033
|
+
let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
|
|
4034
|
+
for (const { id, node, status } of earlier) {
|
|
4035
|
+
if (status !== "open") continue;
|
|
4036
|
+
store.withdrawDecision(id);
|
|
4037
|
+
emit({
|
|
4038
|
+
type: "decision.withdrawn",
|
|
4039
|
+
node,
|
|
4040
|
+
decision: id
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
/** Opens a decision and settles it with the first answer. */
|
|
4044
|
+
const decide = (open, local, withdrawn) => {
|
|
4045
|
+
count += 1;
|
|
4046
|
+
const id = `${runId}-${String(count)}`;
|
|
4047
|
+
const record = store.openDecision({
|
|
4048
|
+
...open,
|
|
4049
|
+
id,
|
|
4050
|
+
runId
|
|
4051
|
+
});
|
|
4052
|
+
const event = {
|
|
4053
|
+
type: "decision.opened",
|
|
4054
|
+
node: record.node,
|
|
4055
|
+
decision: id,
|
|
4056
|
+
kind: record.kind,
|
|
4057
|
+
question: record.question,
|
|
4058
|
+
options: record.options
|
|
4059
|
+
};
|
|
4060
|
+
if (record.recommended !== void 0) event.recommended = record.recommended;
|
|
4061
|
+
if (record.owner !== void 0) event.owner = record.owner;
|
|
4062
|
+
emit(event);
|
|
4063
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
4064
|
+
const settled = new AbortController();
|
|
4065
|
+
/** `note` is the stored one: masked, and absent when blank. */
|
|
4066
|
+
const finish = (answer, by, note) => {
|
|
4067
|
+
if (settled.signal.aborted) return;
|
|
4068
|
+
settled.abort();
|
|
4069
|
+
const answered = {
|
|
4070
|
+
type: "decision.answered",
|
|
4071
|
+
node: open.node,
|
|
4072
|
+
decision: id,
|
|
4073
|
+
answer,
|
|
4074
|
+
by
|
|
4075
|
+
};
|
|
4076
|
+
if (note !== void 0) answered.note = note;
|
|
4077
|
+
emit(answered);
|
|
4078
|
+
resolve(note === void 0 ? {
|
|
4079
|
+
answer,
|
|
4080
|
+
by
|
|
4081
|
+
} : {
|
|
4082
|
+
answer,
|
|
4083
|
+
by,
|
|
4084
|
+
note
|
|
4085
|
+
});
|
|
4086
|
+
};
|
|
4087
|
+
const withdraw = () => {
|
|
4088
|
+
if (settled.signal.aborted) return;
|
|
4089
|
+
settled.abort();
|
|
4090
|
+
store.withdrawDecision(id);
|
|
4091
|
+
emit({
|
|
4092
|
+
type: "decision.withdrawn",
|
|
4093
|
+
node: open.node,
|
|
4094
|
+
decision: id
|
|
4095
|
+
});
|
|
4096
|
+
reject(/* @__PURE__ */ new Error("the question was withdrawn"));
|
|
4097
|
+
};
|
|
4098
|
+
if (withdrawn?.aborted === true) withdraw();
|
|
4099
|
+
withdrawn?.addEventListener("abort", withdraw, { once: true });
|
|
4100
|
+
const fromStore = async () => {
|
|
4101
|
+
try {
|
|
4102
|
+
const closed = await options.waitFor(id, settled.signal);
|
|
4103
|
+
if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
|
|
4104
|
+
} catch {}
|
|
4105
|
+
};
|
|
4106
|
+
const fromTerminal = async (ask) => {
|
|
4107
|
+
try {
|
|
4108
|
+
const { answer, note } = await ask(settled.signal);
|
|
4109
|
+
if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
|
|
4110
|
+
} catch {}
|
|
4111
|
+
};
|
|
4112
|
+
fromStore();
|
|
4113
|
+
if (local !== void 0) fromTerminal(local);
|
|
4114
|
+
return promise;
|
|
4115
|
+
};
|
|
4116
|
+
return {
|
|
4117
|
+
ask: (question) => {
|
|
4118
|
+
const open = {
|
|
4119
|
+
node: question.nodeId,
|
|
4120
|
+
kind: "question",
|
|
4121
|
+
question: question.question,
|
|
4122
|
+
options: question.options
|
|
4123
|
+
};
|
|
4124
|
+
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
4125
|
+
if (question.context !== void 0) open.context = question.context;
|
|
4126
|
+
if (question.document !== void 0) open.document = question.document;
|
|
4127
|
+
if (question.owner !== void 0) open.owner = question.owner;
|
|
4128
|
+
const { local } = options;
|
|
4129
|
+
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
4130
|
+
let note;
|
|
4131
|
+
return local.ask({
|
|
4132
|
+
...question,
|
|
4133
|
+
question: maskSecrets(question.question),
|
|
4134
|
+
...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
|
|
4135
|
+
...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
|
|
4136
|
+
signal,
|
|
4137
|
+
onNote: (given) => {
|
|
4138
|
+
note = given;
|
|
4139
|
+
}
|
|
4140
|
+
}).then((answer) => note === void 0 ? { answer } : {
|
|
4141
|
+
answer,
|
|
4142
|
+
note
|
|
4143
|
+
});
|
|
4144
|
+
}, question.signal).then(({ answer, note }) => {
|
|
4145
|
+
if (note !== void 0) question.onNote?.(note);
|
|
4146
|
+
return answer;
|
|
4147
|
+
});
|
|
4148
|
+
},
|
|
4149
|
+
settled: (question) => {
|
|
4150
|
+
count += 1;
|
|
4151
|
+
const id = `${runId}-${String(count)}`;
|
|
4152
|
+
const { owner } = question;
|
|
4153
|
+
const record = store.openDecision({
|
|
4154
|
+
id,
|
|
4155
|
+
runId,
|
|
4156
|
+
node: question.nodeId,
|
|
4157
|
+
kind: "question",
|
|
4158
|
+
question: question.question,
|
|
4159
|
+
options: question.options,
|
|
4160
|
+
owner,
|
|
4161
|
+
...question.recommended === void 0 ? {} : { recommended: question.recommended },
|
|
4162
|
+
...question.context === void 0 ? {} : { context: question.context }
|
|
4163
|
+
});
|
|
4164
|
+
emit({
|
|
4165
|
+
type: "decision.opened",
|
|
4166
|
+
node: record.node,
|
|
4167
|
+
decision: id,
|
|
4168
|
+
kind: "question",
|
|
4169
|
+
question: record.question,
|
|
4170
|
+
options: record.options,
|
|
4171
|
+
owner,
|
|
4172
|
+
...record.recommended === void 0 ? {} : { recommended: record.recommended }
|
|
4173
|
+
});
|
|
4174
|
+
store.answerDecision(id, question.answer, "owner", question.note);
|
|
4175
|
+
const note = store.decision(id)?.note;
|
|
4176
|
+
emit({
|
|
4177
|
+
type: "decision.answered",
|
|
4178
|
+
node: record.node,
|
|
4179
|
+
decision: id,
|
|
4180
|
+
answer: question.answer,
|
|
4181
|
+
by: "owner",
|
|
4182
|
+
owner,
|
|
4183
|
+
...note === void 0 ? {} : { note }
|
|
4184
|
+
});
|
|
4185
|
+
},
|
|
4186
|
+
permit: async (request) => {
|
|
4187
|
+
const { local } = options;
|
|
4188
|
+
let localMessage = DENIED;
|
|
4189
|
+
const { answer, note } = await decide({
|
|
4190
|
+
node: request.nodeId,
|
|
4191
|
+
kind: "permission",
|
|
4192
|
+
question: `Allow ${request.toolName}?`,
|
|
4193
|
+
options: PERMISSION_OPTIONS,
|
|
4194
|
+
context: inputContext(request.input)
|
|
4195
|
+
}, local === void 0 ? void 0 : (signal) => {
|
|
4196
|
+
let said;
|
|
4197
|
+
return local.permit({
|
|
4198
|
+
...request,
|
|
4199
|
+
signal,
|
|
4200
|
+
onNote: (given) => {
|
|
4201
|
+
said = given;
|
|
4202
|
+
}
|
|
4203
|
+
}).then((result) => {
|
|
4204
|
+
if (!result.allow) localMessage = result.message;
|
|
4205
|
+
const choice = result.allow ? "allow" : "deny";
|
|
4206
|
+
return said === void 0 ? { answer: choice } : {
|
|
4207
|
+
answer: choice,
|
|
4208
|
+
note: said
|
|
4209
|
+
};
|
|
4210
|
+
});
|
|
4211
|
+
}, request.signal);
|
|
4212
|
+
if (note !== void 0) request.onNote?.(note);
|
|
4213
|
+
return answer === "allow" ? { allow: true } : {
|
|
4214
|
+
allow: false,
|
|
4215
|
+
message: note === void 0 ? localMessage : deniedMessage(note)
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
};
|
|
3944
4220
|
// ../core/src/control/socket.ts
|
|
3945
4221
|
/**
|
|
3946
4222
|
* @file The per-run local socket (`~/.ork/sockets/<run>.sock`): the fast path
|
|
@@ -3990,7 +4266,10 @@ const messageOf$3 = (line) => {
|
|
|
3990
4266
|
};
|
|
3991
4267
|
/** Listens on the run's socket; resolves once it is listening. */
|
|
3992
4268
|
const serveRun = async (options) => {
|
|
3993
|
-
mkdirSync(path.dirname(options.path), {
|
|
4269
|
+
mkdirSync(path.dirname(options.path), {
|
|
4270
|
+
recursive: true,
|
|
4271
|
+
mode: 448
|
|
4272
|
+
});
|
|
3994
4273
|
rmSync(options.path, { force: true });
|
|
3995
4274
|
const followers = /* @__PURE__ */ new Set();
|
|
3996
4275
|
const server = createServer((socket) => {
|
|
@@ -4079,6 +4358,75 @@ const followSocket = (file, after, onEvent, signal) => {
|
|
|
4079
4358
|
});
|
|
4080
4359
|
return promise;
|
|
4081
4360
|
};
|
|
4361
|
+
// ../core/src/control/control.ts
|
|
4362
|
+
const startControl = async (options) => {
|
|
4363
|
+
const { store, runId } = options;
|
|
4364
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
4365
|
+
let stopped = false;
|
|
4366
|
+
const check = () => {
|
|
4367
|
+
for (const [id, resolve] of waiting) {
|
|
4368
|
+
const decision = store.decision(id);
|
|
4369
|
+
if (decision !== void 0 && decision.status !== "open") {
|
|
4370
|
+
waiting.delete(id);
|
|
4371
|
+
resolve(decision);
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
for (const command of store.takeCommands(runId)) if (command.kind === "message") options.onMessage(command.node, command.body);
|
|
4375
|
+
else if (command.kind === "relay") options.onRelay?.(command.node, command.from, command.body);
|
|
4376
|
+
else if (command.kind === "script") options.onScript?.(command.node, command.from, command.body);
|
|
4377
|
+
else if (command.kind === "notice") options.onNotice?.(command.node, command.body);
|
|
4378
|
+
else if (command.kind === "stop" && command.node === void 0) {
|
|
4379
|
+
if (stopped) continue;
|
|
4380
|
+
stopped = true;
|
|
4381
|
+
options.emit({
|
|
4382
|
+
type: "run.command",
|
|
4383
|
+
command: "stop"
|
|
4384
|
+
});
|
|
4385
|
+
options.onStop();
|
|
4386
|
+
} else if (!stopped) {
|
|
4387
|
+
if (options.onControl?.(command.kind, command.node) ?? false) options.emit({
|
|
4388
|
+
type: "run.command",
|
|
4389
|
+
command: command.kind,
|
|
4390
|
+
...command.node === void 0 ? {} : { node: command.node }
|
|
4391
|
+
});
|
|
4392
|
+
}
|
|
4393
|
+
};
|
|
4394
|
+
const timer = setInterval(check, options.intervalMs ?? 1e3);
|
|
4395
|
+
let socket;
|
|
4396
|
+
try {
|
|
4397
|
+
socket = await serveRun({
|
|
4398
|
+
path: socketPath(runId, options.home),
|
|
4399
|
+
onWake: check,
|
|
4400
|
+
backlog: (after) => store.events(runId, after)
|
|
4401
|
+
});
|
|
4402
|
+
} catch {}
|
|
4403
|
+
return {
|
|
4404
|
+
human: createDecisionQueue({
|
|
4405
|
+
store,
|
|
4406
|
+
runId,
|
|
4407
|
+
emit: options.emit,
|
|
4408
|
+
waitFor: (id, signal) => {
|
|
4409
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
4410
|
+
waiting.set(id, resolve);
|
|
4411
|
+
signal.addEventListener("abort", () => {
|
|
4412
|
+
waiting.delete(id);
|
|
4413
|
+
reject(/* @__PURE__ */ new Error("settled"));
|
|
4414
|
+
}, { once: true });
|
|
4415
|
+
check();
|
|
4416
|
+
return promise;
|
|
4417
|
+
},
|
|
4418
|
+
...options.local === void 0 ? {} : { local: options.local }
|
|
4419
|
+
}),
|
|
4420
|
+
publish: (stored) => {
|
|
4421
|
+
socket?.publish(stored);
|
|
4422
|
+
},
|
|
4423
|
+
check,
|
|
4424
|
+
close: async () => {
|
|
4425
|
+
clearInterval(timer);
|
|
4426
|
+
await socket?.close();
|
|
4427
|
+
}
|
|
4428
|
+
};
|
|
4429
|
+
};
|
|
4082
4430
|
// ../core/src/expr/parse.ts
|
|
4083
4431
|
/**
|
|
4084
4432
|
* @file A small, safe expression language for `when:` conditions and `{{ }}`
|
|
@@ -4632,6 +4980,60 @@ const renderTemplate = (parts, scope, { readable = false } = {}) => {
|
|
|
4632
4980
|
}
|
|
4633
4981
|
return text;
|
|
4634
4982
|
};
|
|
4983
|
+
// ../core/src/feedback/actions.ts
|
|
4984
|
+
const ALLOWED = { ok: true };
|
|
4985
|
+
const no = (why, error) => ({
|
|
4986
|
+
ok: false,
|
|
4987
|
+
why,
|
|
4988
|
+
error
|
|
4989
|
+
});
|
|
4990
|
+
/** A dismissed item allows nothing until it's restored. */
|
|
4991
|
+
const dismissed = (short) => no("dismissed; restore it first", `${short} is dismissed; restore it first.`);
|
|
4992
|
+
const already = (short, status) => no(`${status} already`, `${short} is ${status} already.`);
|
|
4993
|
+
/**
|
|
4994
|
+
* `fixRun` is a running `ork:feedback-apply` run for the item ({@link
|
|
4995
|
+
* openFixRun}): a second fix isn't started beside it.
|
|
4996
|
+
*/
|
|
4997
|
+
const feedbackActions = (item, fixRun) => {
|
|
4998
|
+
const short = item.id.slice(0, 12);
|
|
4999
|
+
const isOrk = item.target === "ork";
|
|
5000
|
+
if (item.status === "dismissed") {
|
|
5001
|
+
const check = dismissed(short);
|
|
5002
|
+
return {
|
|
5003
|
+
accept: check,
|
|
5004
|
+
lesson: check,
|
|
5005
|
+
apply: check,
|
|
5006
|
+
publish: check
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
const settled = item.status === "resolved" || item.status === "published";
|
|
5010
|
+
const accept = item.status === "accepted" || settled ? already(short, item.status) : ALLOWED;
|
|
5011
|
+
let lesson = ALLOWED;
|
|
5012
|
+
if (isOrk) lesson = no("about Ork itself", `${short} is about Ork itself, so it isn't a lesson for a workflow; share it with Ork's maintainers instead.`);
|
|
5013
|
+
else if (item.workflows.length === 0) lesson = no("no workflow recorded, so no lessons file", `No workflow is recorded for ${short}, so it has no lessons file.`);
|
|
5014
|
+
let apply = ALLOWED;
|
|
5015
|
+
if (isOrk) apply = no("about Ork itself", `${short} is about Ork itself, so its fix isn't in this project; share it with Ork's maintainers instead.`);
|
|
5016
|
+
else if (settled) apply = already(short, item.status);
|
|
5017
|
+
else if (fixRun !== void 0) apply = no(`run ${fixRun} is proposing a fix`, `Run ${fixRun} is proposing a fix for ${short} already: answer its question in \`ork decide\` or the UI, or stop it first.`);
|
|
5018
|
+
let publish = ALLOWED;
|
|
5019
|
+
if (!isOrk) publish = no("only feedback about Ork is shared", `${short} is about this ${item.target}, not Ork itself: only feedback about Ork is shared, and the rest stays on your machine.`);
|
|
5020
|
+
else if (item.status === "published") publish = already(short, item.status);
|
|
5021
|
+
return {
|
|
5022
|
+
accept,
|
|
5023
|
+
lesson,
|
|
5024
|
+
apply,
|
|
5025
|
+
publish
|
|
5026
|
+
};
|
|
5027
|
+
};
|
|
5028
|
+
/** The running `ork:feedback-apply` run proposing a fix for `item`, if any. */
|
|
5029
|
+
const openFixRun = (store, item) => {
|
|
5030
|
+
const short = item.id.slice(0, 12);
|
|
5031
|
+
return store.runs().filter((run) => run.workflow === "ork-feedback-apply" && run.status === "running").find((run) => {
|
|
5032
|
+
const [first] = store.events(run.id);
|
|
5033
|
+
const event = first?.event;
|
|
5034
|
+
return event?.type === "run.started" && event.inputs?.["item"] === short;
|
|
5035
|
+
})?.id;
|
|
5036
|
+
};
|
|
4635
5037
|
// ../core/src/feedback/fingerprint.ts
|
|
4636
5038
|
/**
|
|
4637
5039
|
* @file Feedback fingerprints: records about the same thing (same target, kind,
|
|
@@ -4654,16 +5056,17 @@ const defaultTarget = (kind) => {
|
|
|
4654
5056
|
};
|
|
4655
5057
|
/** The draft's target, or its kind's default. */
|
|
4656
5058
|
const targetOf = (draft) => draft.target ?? defaultTarget(draft.kind);
|
|
4657
|
-
const MAX_TITLE = 120;
|
|
5059
|
+
const MAX_TITLE$1 = 120;
|
|
4658
5060
|
const MAX_BODY = 2e3;
|
|
4659
5061
|
const MAX_EVIDENCE = 10;
|
|
4660
|
-
const createFeedback = ({ graph, onEvent }) => {
|
|
5062
|
+
const createFeedback = ({ graph, onEvent, scrub = (text) => text }) => {
|
|
4661
5063
|
const seen = /* @__PURE__ */ new Set();
|
|
5064
|
+
const clean = (text) => scrub(maskSecrets(text));
|
|
4662
5065
|
return { port: (id, template) => {
|
|
4663
5066
|
let left = 3;
|
|
4664
5067
|
const nodeType = graph.nodes.get(template ?? id)?.node.type;
|
|
4665
5068
|
return { report: (draft) => {
|
|
4666
|
-
const title =
|
|
5069
|
+
const title = clean(draft.title.trim()).slice(0, MAX_TITLE$1);
|
|
4667
5070
|
if (title === "") return {
|
|
4668
5071
|
ok: false,
|
|
4669
5072
|
error: "the title is empty"
|
|
@@ -4693,8 +5096,8 @@ const createFeedback = ({ graph, onEvent }) => {
|
|
|
4693
5096
|
target,
|
|
4694
5097
|
severity: draft.severity ?? "medium",
|
|
4695
5098
|
title,
|
|
4696
|
-
body:
|
|
4697
|
-
evidence: (draft.evidence ?? []).slice(0, MAX_EVIDENCE).map((e) =>
|
|
5099
|
+
body: clean(draft.body?.trim() ?? "").slice(0, MAX_BODY),
|
|
5100
|
+
evidence: (draft.evidence ?? []).slice(0, MAX_EVIDENCE).map((e) => clean(e)),
|
|
4698
5101
|
fingerprint: print,
|
|
4699
5102
|
origin: nodeType === void 0 ? {} : { nodeType }
|
|
4700
5103
|
}
|
|
@@ -4958,6 +5361,136 @@ const FEEDBACK_SEVERITIES = [
|
|
|
4958
5361
|
"medium",
|
|
4959
5362
|
"high"
|
|
4960
5363
|
];
|
|
5364
|
+
// ../core/src/feedback/human.ts
|
|
5365
|
+
const RATINGS = ["good", "bad"];
|
|
5366
|
+
const MAX_TITLE = 120;
|
|
5367
|
+
/**
|
|
5368
|
+
* The run event a note becomes; its title is the note's first line. `scrub` (an
|
|
5369
|
+
* env-value scrubber) runs after `maskSecrets`.
|
|
5370
|
+
*/
|
|
5371
|
+
const humanNoteEvent = (note, scrub = (text) => text) => {
|
|
5372
|
+
const text = scrub(maskSecrets(note.text.trim()));
|
|
5373
|
+
const [first = ""] = text.split("\n");
|
|
5374
|
+
const title = first.slice(0, MAX_TITLE);
|
|
5375
|
+
const kind = note.kind ?? "friction";
|
|
5376
|
+
const target = targetOf({
|
|
5377
|
+
kind,
|
|
5378
|
+
title,
|
|
5379
|
+
...note.target === void 0 ? {} : { target: note.target }
|
|
5380
|
+
});
|
|
5381
|
+
const feedback = {
|
|
5382
|
+
source: "human",
|
|
5383
|
+
kind,
|
|
5384
|
+
target,
|
|
5385
|
+
severity: note.severity ?? (note.rating === "bad" ? "high" : "medium"),
|
|
5386
|
+
title,
|
|
5387
|
+
body: text,
|
|
5388
|
+
evidence: note.rating === void 0 ? [] : [`rating: ${note.rating}`],
|
|
5389
|
+
fingerprint: fingerprint({
|
|
5390
|
+
target,
|
|
5391
|
+
kind,
|
|
5392
|
+
title
|
|
5393
|
+
}),
|
|
5394
|
+
origin: { workflow: note.workflow }
|
|
5395
|
+
};
|
|
5396
|
+
return {
|
|
5397
|
+
type: "feedback.recorded",
|
|
5398
|
+
...note.node === void 0 ? {} : { node: note.node },
|
|
5399
|
+
feedback
|
|
5400
|
+
};
|
|
5401
|
+
};
|
|
5402
|
+
/** `value` when it's one of `allowed`, or why not. */
|
|
5403
|
+
const checked = (name, value, allowed) => {
|
|
5404
|
+
if (value === void 0) return { ok: true };
|
|
5405
|
+
const found = allowed.find((a) => a === value);
|
|
5406
|
+
return found === void 0 ? {
|
|
5407
|
+
ok: false,
|
|
5408
|
+
error: `${name} is one of ${allowed.join(", ")}`
|
|
5409
|
+
} : {
|
|
5410
|
+
ok: true,
|
|
5411
|
+
value: found
|
|
5412
|
+
};
|
|
5413
|
+
};
|
|
5414
|
+
/**
|
|
5415
|
+
* Whether a note can be recorded, before anything else is done: its words,
|
|
5416
|
+
* kind, severity and rating. `flag` names the options as the caller does (`--`
|
|
5417
|
+
* for the CLI).
|
|
5418
|
+
*/
|
|
5419
|
+
const checkNote = (ask, flag = "") => {
|
|
5420
|
+
if (ask.text.trim() === "") return {
|
|
5421
|
+
ok: false,
|
|
5422
|
+
error: "The note is empty."
|
|
5423
|
+
};
|
|
5424
|
+
const kind = checked(`${flag}kind`, ask.kind, FEEDBACK_KINDS);
|
|
5425
|
+
const severity = checked(`${flag}severity`, ask.severity, FEEDBACK_SEVERITIES);
|
|
5426
|
+
const rating = checked(`${flag}rating`, ask.rating, RATINGS);
|
|
5427
|
+
if (!kind.ok) return kind;
|
|
5428
|
+
if (!severity.ok) return severity;
|
|
5429
|
+
if (!rating.ok) return rating;
|
|
5430
|
+
return {
|
|
5431
|
+
ok: true,
|
|
5432
|
+
...kind.value === void 0 ? {} : { kind: kind.value },
|
|
5433
|
+
...severity.value === void 0 ? {} : { severity: severity.value },
|
|
5434
|
+
...rating.value === void 0 ? {} : { rating: rating.value }
|
|
5435
|
+
};
|
|
5436
|
+
};
|
|
5437
|
+
/**
|
|
5438
|
+
* Records a person's note on `run`, or one node that ran in it, as that run's
|
|
5439
|
+
* feedback (a run that ended takes it too); returns the item it joins (the one
|
|
5440
|
+
* it was merged into, when triage merged it), or why it can't. `ork feedback
|
|
5441
|
+
* note`, `ork decide --note` and the UI write it.
|
|
5442
|
+
*/
|
|
5443
|
+
const recordNote = (store, run, ask, flag = "", scrub) => {
|
|
5444
|
+
const note = checkNote(ask, flag);
|
|
5445
|
+
if (!note.ok) return note;
|
|
5446
|
+
const ran = store.events(run.id).some(({ event }) => "id" in event && event.id === ask.node);
|
|
5447
|
+
if (ask.node !== void 0 && !ran) return {
|
|
5448
|
+
ok: false,
|
|
5449
|
+
error: `No node ${ask.node} ran in run ${run.id}.`
|
|
5450
|
+
};
|
|
5451
|
+
const event = humanNoteEvent({
|
|
5452
|
+
text: ask.text,
|
|
5453
|
+
workflow: run.workflow,
|
|
5454
|
+
...ask.node === void 0 ? {} : { node: ask.node },
|
|
5455
|
+
...note.kind === void 0 ? {} : { kind: note.kind },
|
|
5456
|
+
...note.severity === void 0 ? {} : { severity: note.severity },
|
|
5457
|
+
...note.rating === void 0 ? {} : { rating: note.rating }
|
|
5458
|
+
}, scrub);
|
|
5459
|
+
store.append(run.id, event);
|
|
5460
|
+
const recorded = event.type === "feedback.recorded" ? event.feedback.fingerprint : "";
|
|
5461
|
+
return {
|
|
5462
|
+
ok: true,
|
|
5463
|
+
item: store.findFeedback(recorded)[0]?.id ?? recorded
|
|
5464
|
+
};
|
|
5465
|
+
};
|
|
5466
|
+
// ../core/src/feedback/preview.ts
|
|
5467
|
+
/**
|
|
5468
|
+
* @file The preview of an `ork` item as a post to Ork's Discussions (ADR-059),
|
|
5469
|
+
* for `ork feedback publish` and the Feedback panel. The projects its records
|
|
5470
|
+
* came from and every registered one are rewritten to `<project>`, and when
|
|
5471
|
+
* names are hidden their folders' and the workflows' names go too. What the
|
|
5472
|
+
* person confirms is what is sent.
|
|
5473
|
+
*/
|
|
5474
|
+
/** The item as a post, or why it isn't shared. */
|
|
5475
|
+
const previewPost = (store, item, ask) => {
|
|
5476
|
+
const allowed = feedbackActions(item).publish;
|
|
5477
|
+
if (!allowed.ok) return {
|
|
5478
|
+
ok: false,
|
|
5479
|
+
error: allowed.error
|
|
5480
|
+
};
|
|
5481
|
+
const records = store.feedbackRecords(item.id);
|
|
5482
|
+
const runRoots = records.flatMap((r) => store.lookup(r.runId).find((run) => run.id === r.runId)?.project ?? []);
|
|
5483
|
+
const roots = [.../* @__PURE__ */ new Set([...runRoots, ...ask.registered.map((p) => p.root)])];
|
|
5484
|
+
const names = ask.hideNames ? [...item.workflows, ...roots.map((root) => path.basename(root))] : [];
|
|
5485
|
+
return {
|
|
5486
|
+
ok: true,
|
|
5487
|
+
post: publishPost(item, records, {
|
|
5488
|
+
roots,
|
|
5489
|
+
home: ask.home,
|
|
5490
|
+
names
|
|
5491
|
+
})
|
|
5492
|
+
};
|
|
5493
|
+
};
|
|
4961
5494
|
// ../core/src/runtime/ends.ts
|
|
4962
5495
|
/**
|
|
4963
5496
|
* @file How a node ended, as one of a closed set of kinds (specs/runtime/
|
|
@@ -5313,6 +5846,137 @@ const createSignals = (options = {}) => {
|
|
|
5313
5846
|
return signal === null ? [event] : [event, ...record(...signal)];
|
|
5314
5847
|
};
|
|
5315
5848
|
};
|
|
5849
|
+
// ../core/src/follow-ups/change.ts
|
|
5850
|
+
/** The project's follow-up whose id is, or starts with, `prefix`. */
|
|
5851
|
+
const findFollowUp = (rows, project, prefix) => {
|
|
5852
|
+
const exact = rows.followUp(project, prefix);
|
|
5853
|
+
if (exact !== void 0) return {
|
|
5854
|
+
ok: true,
|
|
5855
|
+
followUp: exact
|
|
5856
|
+
};
|
|
5857
|
+
const matches = prefix === "" ? [] : rows.followUps(project).filter((f) => f.id.startsWith(prefix));
|
|
5858
|
+
const [only, other] = matches;
|
|
5859
|
+
if (only === void 0) return {
|
|
5860
|
+
ok: false,
|
|
5861
|
+
error: `No follow-up \`${prefix}\` in this project.`
|
|
5862
|
+
};
|
|
5863
|
+
if (other !== void 0) return {
|
|
5864
|
+
ok: false,
|
|
5865
|
+
error: `\`${prefix}\` matches ${matches.map((f) => f.id).join(", ")}; give more of the id.`
|
|
5866
|
+
};
|
|
5867
|
+
return {
|
|
5868
|
+
ok: true,
|
|
5869
|
+
followUp: only
|
|
5870
|
+
};
|
|
5871
|
+
};
|
|
5872
|
+
/** Which statuses each action starts from, and where it goes. */
|
|
5873
|
+
const ACTIONS = {
|
|
5874
|
+
done: {
|
|
5875
|
+
from: ["waiting", "due"],
|
|
5876
|
+
to: () => "done",
|
|
5877
|
+
note: "done"
|
|
5878
|
+
},
|
|
5879
|
+
drop: {
|
|
5880
|
+
from: ["waiting", "due"],
|
|
5881
|
+
to: () => "dropped",
|
|
5882
|
+
note: "dropped"
|
|
5883
|
+
},
|
|
5884
|
+
reopen: {
|
|
5885
|
+
from: ["done", "dropped"],
|
|
5886
|
+
to: (f) => f.due === "now" ? "due" : "waiting",
|
|
5887
|
+
note: "reopened"
|
|
5888
|
+
},
|
|
5889
|
+
due: {
|
|
5890
|
+
from: ["waiting"],
|
|
5891
|
+
to: () => "due",
|
|
5892
|
+
note: "marked due"
|
|
5893
|
+
}
|
|
5894
|
+
};
|
|
5895
|
+
/** Why `action` can't be done to `f` now, or `undefined`. */
|
|
5896
|
+
const refusal$1 = (f, action) => {
|
|
5897
|
+
const rule = ACTIONS[action];
|
|
5898
|
+
return rule.from.includes(f.status) ? void 0 : `${f.id} is ${f.status}, so it can't be ${rule.note}.`;
|
|
5899
|
+
};
|
|
5900
|
+
const check = (f, action) => {
|
|
5901
|
+
const error = refusal$1(f, action);
|
|
5902
|
+
return error === void 0 ? { ok: true } : {
|
|
5903
|
+
ok: false,
|
|
5904
|
+
kind: "status",
|
|
5905
|
+
why: `it is ${f.status}`,
|
|
5906
|
+
error
|
|
5907
|
+
};
|
|
5908
|
+
};
|
|
5909
|
+
/** Whether `to` names one GitHub issue or pull request by its URL. */
|
|
5910
|
+
const isGithubIssueUrl = (to) => /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+$/u.test(to);
|
|
5911
|
+
/**
|
|
5912
|
+
* Why `f`'s draft can't be posted to GitHub whatever its status, or
|
|
5913
|
+
* `undefined`.
|
|
5914
|
+
*/
|
|
5915
|
+
const githubDraftRefusal = (f) => {
|
|
5916
|
+
if (f.kind !== "message" || f.draft?.channel !== "github") return {
|
|
5917
|
+
why: "not drafted for the github channel",
|
|
5918
|
+
error: "only a message drafted for the github channel is posted; send the others yourself"
|
|
5919
|
+
};
|
|
5920
|
+
return isGithubIssueUrl(f.draft.to) ? void 0 : {
|
|
5921
|
+
why: "its `to` isn't a GitHub issue or pull request URL",
|
|
5922
|
+
error: `${f.id}'s \`to\` (${f.draft.to}) isn't a GitHub issue or pull request URL, so it is not posted; copy the text and send it yourself.`
|
|
5923
|
+
};
|
|
5924
|
+
};
|
|
5925
|
+
const postCheck = (f) => {
|
|
5926
|
+
const open = f.status === "waiting" || f.status === "due";
|
|
5927
|
+
const refused = githubDraftRefusal(f);
|
|
5928
|
+
if (refused !== void 0) return {
|
|
5929
|
+
ok: false,
|
|
5930
|
+
kind: "draft",
|
|
5931
|
+
...refused
|
|
5932
|
+
};
|
|
5933
|
+
return open ? { ok: true } : {
|
|
5934
|
+
ok: false,
|
|
5935
|
+
kind: "status",
|
|
5936
|
+
why: `it is ${f.status}`,
|
|
5937
|
+
error: `${f.id} is ${f.status}, so it is not posted.`
|
|
5938
|
+
};
|
|
5939
|
+
};
|
|
5940
|
+
const followUpActions = (f) => ({
|
|
5941
|
+
done: check(f, "done"),
|
|
5942
|
+
drop: check(f, "drop"),
|
|
5943
|
+
reopen: check(f, "reopen"),
|
|
5944
|
+
due: check(f, "due"),
|
|
5945
|
+
post: postCheck(f)
|
|
5946
|
+
});
|
|
5947
|
+
/** Applies `action` to `followUp`, and keeps it. */
|
|
5948
|
+
const changeFollowUp = (rows, followUp, action, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
5949
|
+
const rule = ACTIONS[action];
|
|
5950
|
+
const refused = refusal$1(followUp, action);
|
|
5951
|
+
if (refused !== void 0) return {
|
|
5952
|
+
ok: false,
|
|
5953
|
+
error: refused
|
|
5954
|
+
};
|
|
5955
|
+
const { note: _old, ...rest } = followUp;
|
|
5956
|
+
const changed = {
|
|
5957
|
+
...rest,
|
|
5958
|
+
status: rule.to(followUp),
|
|
5959
|
+
note: rule.note,
|
|
5960
|
+
updatedAt: now()
|
|
5961
|
+
};
|
|
5962
|
+
rows.putFollowUp(changed);
|
|
5963
|
+
return {
|
|
5964
|
+
ok: true,
|
|
5965
|
+
followUp: changed
|
|
5966
|
+
};
|
|
5967
|
+
};
|
|
5968
|
+
/**
|
|
5969
|
+
* Why a due follow-up is due, read from the note `due.ts` left: its ref merged,
|
|
5970
|
+
* its date came, its ref closed unmerged, or a person marked it. `undefined`
|
|
5971
|
+
* for one that is not due, or was due from the start.
|
|
5972
|
+
*/
|
|
5973
|
+
const dueCause = (f) => {
|
|
5974
|
+
if (f.status !== "due" || f.note === void 0) return void 0;
|
|
5975
|
+
if (f.note.includes("closed without merging")) return "closed-unmerged";
|
|
5976
|
+
if (f.note.endsWith(" merged")) return "merged";
|
|
5977
|
+
if (f.note.startsWith("due on ")) return "overdue";
|
|
5978
|
+
return f.note === "marked due" ? "marked" : void 0;
|
|
5979
|
+
};
|
|
5316
5980
|
// ../core/src/project/heavy.ts
|
|
5317
5981
|
/**
|
|
5318
5982
|
* @file A project's rules for the machine its runs share: the heavy commands
|
|
@@ -5351,6 +6015,16 @@ const matchesPattern = (pattern, command) => {
|
|
|
5351
6015
|
};
|
|
5352
6016
|
/** Whether `command` is one of the project's heavy commands. */
|
|
5353
6017
|
const isHeavy = (heavy, command) => heavy.some((pattern) => matchesPattern(pattern, command));
|
|
6018
|
+
/**
|
|
6019
|
+
* The commands of a shell script, as far as a heavy check can tell without a
|
|
6020
|
+
* shell: comments dropped, split at newlines, `;`, `&&`, `||`, `|` and `&` (not
|
|
6021
|
+
* the `&` of a redirect such as `2>&1`), each trimmed, without a leading
|
|
6022
|
+
* `VAR=value` or `then`/`do`/`else`. shortcut: it splits inside quotes too and
|
|
6023
|
+
* cannot see `eval`, variables, heredocs or called scripts (specs/gaps).
|
|
6024
|
+
*/
|
|
6025
|
+
const scriptCommands = (script) => script.split("\n").map((line) => line.replace(/(?:^|\s)#.*$/u, "")).flatMap((line) => line.split(/&&|\|\||;|\||(?<![<>])&(?!>)/u)).map((part) => part.trim().replace(/^(?:(?:then|do|else)(?:\s+|$))?(?:\w+=\S*\s+)*/u, "")).filter((part) => part !== "");
|
|
6026
|
+
/** Whether any command of a shell script is one of the project's heavy ones. */
|
|
6027
|
+
const isHeavyScript = (heavy, script) => heavy.length > 0 && scriptCommands(script).some((command) => isHeavy(heavy, command));
|
|
5354
6028
|
const machineRulesOf = (project) => ({
|
|
5355
6029
|
heavy: project?.heavy ?? [],
|
|
5356
6030
|
priority: project?.priority ?? "low"
|
|
@@ -6308,18 +6982,30 @@ const sameFiles = (a, b) => a.length === b.length && a.toSorted().join("\n") ===
|
|
|
6308
6982
|
* title.
|
|
6309
6983
|
*/
|
|
6310
6984
|
const sameFollowUp = (kept, input) => (kept.status === "waiting" || kept.status === "due") && kept.for === input.for && sameFiles(kept.where, input.where) && nearTitle(kept.title, input.title);
|
|
6311
|
-
/**
|
|
6312
|
-
const
|
|
6313
|
-
|
|
6985
|
+
/** Who a follow-up is for, its name scrubbed and its kind kept. */
|
|
6986
|
+
const scrubFor = (to, scrub) => {
|
|
6987
|
+
if (to.startsWith("project:")) return `project:${scrub(to.slice(8))}`;
|
|
6988
|
+
if (to.startsWith("person:")) return `person:${scrub(to.slice(7))}`;
|
|
6989
|
+
return to;
|
|
6990
|
+
};
|
|
6991
|
+
/**
|
|
6992
|
+
* `input` with what shouldn't be kept or leave the machine taken out: each text
|
|
6993
|
+
* passed through `scrub` (the store's redactor, or a run's env-value
|
|
6994
|
+
* scrubber).
|
|
6995
|
+
*/
|
|
6996
|
+
const scrubFollowUp = (input, scrub) => {
|
|
6314
6997
|
return {
|
|
6315
6998
|
...input,
|
|
6999
|
+
for: scrubFor(input.for, scrub),
|
|
6316
7000
|
title: scrub(input.title),
|
|
7001
|
+
...input.ref === void 0 ? {} : { ref: scrub(input.ref) },
|
|
6317
7002
|
...input.body === void 0 ? {} : { body: scrub(input.body) },
|
|
6318
7003
|
...input.where === void 0 ? {} : { where: input.where.map(scrub) },
|
|
6319
7004
|
...input.draft === void 0 ? {} : { draft: {
|
|
6320
7005
|
...input.draft,
|
|
6321
7006
|
to: scrub(input.draft.to),
|
|
6322
|
-
text: scrub(input.draft.text)
|
|
7007
|
+
text: scrub(input.draft.text),
|
|
7008
|
+
...input.draft.channel === void 0 ? {} : { channel: scrub(input.draft.channel) }
|
|
6323
7009
|
} },
|
|
6324
7010
|
...input.steps === void 0 ? {} : { steps: input.steps.map((step) => ({
|
|
6325
7011
|
text: scrub(step.text),
|
|
@@ -6354,10 +7040,10 @@ const followUpStore = (options) => {
|
|
|
6354
7040
|
return {
|
|
6355
7041
|
place: adapter.place,
|
|
6356
7042
|
add: async (raw) => {
|
|
6357
|
-
const input = scrubFollowUp(raw, {
|
|
7043
|
+
const input = scrubFollowUp(raw, (text) => redact(text, {
|
|
6358
7044
|
roots: [project, worktree],
|
|
6359
7045
|
home
|
|
6360
|
-
});
|
|
7046
|
+
}));
|
|
6361
7047
|
const where = input.where ?? [];
|
|
6362
7048
|
const at = now();
|
|
6363
7049
|
const kept = list().find((f) => sameFollowUp(f, {
|
|
@@ -6554,7 +7240,8 @@ const deliverFollowUp = async (input, options) => {
|
|
|
6554
7240
|
* too, with a note saying so, never dropped. A dated one is due on its date;
|
|
6555
7241
|
* `after-deploy` only when someone says so (`markDue`). A run that publishes
|
|
6556
7242
|
* records its ref on the follow-ups it left (`recordPublished`), and the
|
|
6557
|
-
* `agent` follow-ups it took as its brief are done
|
|
7243
|
+
* `agent` follow-ups it took as its brief are done (taking them is
|
|
7244
|
+
* `take.ts`).
|
|
6558
7245
|
*/
|
|
6559
7246
|
const DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
6560
7247
|
const why = (result) => result.stderr.trim() || result.stdout.trim() || "no output";
|
|
@@ -6700,25 +7387,6 @@ const recordPublished = (rows, published, now = () => (/* @__PURE__ */ new Date(
|
|
|
6700
7387
|
};
|
|
6701
7388
|
};
|
|
6702
7389
|
/**
|
|
6703
|
-
* `ork run start --follow-ups <ids>`: the open `agent` follow-ups among `ids`
|
|
6704
|
-
* become `run`'s brief; why each other id wasn't taken.
|
|
6705
|
-
*/
|
|
6706
|
-
const takeFollowUps = (rows, taking, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
6707
|
-
const taken = [];
|
|
6708
|
-
const refused = [];
|
|
6709
|
-
for (const id of taking.ids) {
|
|
6710
|
-
const f = rows.followUp(taking.project, id);
|
|
6711
|
-
if (f === void 0) refused.push(`${id}: there is no such follow-up`);
|
|
6712
|
-
else if (f.for !== "agent") refused.push(`${id}: it is for ${f.for}`);
|
|
6713
|
-
else if (f.status === "done" || f.status === "dropped") refused.push(`${id}: it is ${f.status}`);
|
|
6714
|
-
else taken.push(changed(rows, f, { takenBy: taking.run }, now()));
|
|
6715
|
-
}
|
|
6716
|
-
return {
|
|
6717
|
-
taken,
|
|
6718
|
-
refused
|
|
6719
|
-
};
|
|
6720
|
-
};
|
|
6721
|
-
/**
|
|
6722
7390
|
* Checks the projects' follow-ups now and on a schedule, while `ork ui` or `ork
|
|
6723
7391
|
* watch` runs; `onDue` hears of those that came due (a notification). Returns
|
|
6724
7392
|
* the stop. A check still going when the next is due is not doubled.
|
|
@@ -6754,18 +7422,61 @@ const startFollowUpChecks = (options) => {
|
|
|
6754
7422
|
clearInterval(timer);
|
|
6755
7423
|
};
|
|
6756
7424
|
};
|
|
6757
|
-
|
|
7425
|
+
// ../core/src/follow-ups/post.ts
|
|
7426
|
+
/** Posts `f`'s draft as a GitHub comment once `confirmed`, and marks it done. */
|
|
7427
|
+
const postFollowUpMessage = async (f, options) => {
|
|
7428
|
+
const draft = f.draft;
|
|
7429
|
+
const refused = githubDraftRefusal(f);
|
|
7430
|
+
if (refused !== void 0 || draft === void 0) return {
|
|
7431
|
+
ok: false,
|
|
7432
|
+
error: refused?.error ?? "it has no draft"
|
|
7433
|
+
};
|
|
7434
|
+
if (!options.confirmed) return {
|
|
7435
|
+
ok: false,
|
|
7436
|
+
error: "posting to GitHub needs the maintainer's confirmation"
|
|
7437
|
+
};
|
|
7438
|
+
const body = redact(draft.text, {
|
|
7439
|
+
roots: [f.project],
|
|
7440
|
+
home: options.home
|
|
7441
|
+
});
|
|
7442
|
+
const on = /\/pull\/\d+/u.test(draft.to) ? "pr" : "issue";
|
|
7443
|
+
const result = await options.gh([
|
|
7444
|
+
on,
|
|
7445
|
+
"comment",
|
|
7446
|
+
draft.to,
|
|
7447
|
+
"--body",
|
|
7448
|
+
body
|
|
7449
|
+
], { cwd: f.project });
|
|
7450
|
+
if (!result.ok) return {
|
|
7451
|
+
ok: false,
|
|
7452
|
+
error: `gh failed: ${result.stderr.trim() || result.stdout.trim() || "no output"}`
|
|
7453
|
+
};
|
|
7454
|
+
const url = result.stdout.trim().split("\n").findLast((line) => line.startsWith("https://"));
|
|
7455
|
+
const done = {
|
|
7456
|
+
...f,
|
|
7457
|
+
status: "done",
|
|
7458
|
+
note: `posted${url === void 0 ? "" : ` as ${url}`}`,
|
|
7459
|
+
updatedAt: options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
7460
|
+
};
|
|
7461
|
+
options.rows.putFollowUp(done);
|
|
7462
|
+
return {
|
|
7463
|
+
ok: true,
|
|
7464
|
+
followUp: done,
|
|
7465
|
+
...url === void 0 ? {} : { url }
|
|
7466
|
+
};
|
|
7467
|
+
};
|
|
7468
|
+
const createFollowUps = ({ runId, sink, scrub }) => ({
|
|
6758
7469
|
/** One session's port. */
|
|
6759
7470
|
port: (node, cwd) => {
|
|
6760
7471
|
let left = 30;
|
|
6761
|
-
return { add: async (
|
|
7472
|
+
return { add: async (raw) => {
|
|
6762
7473
|
if (left === 0) return {
|
|
6763
7474
|
ok: false,
|
|
6764
7475
|
error: `you already left ${String(30)} follow-ups in this session; put the rest in your report`
|
|
6765
7476
|
};
|
|
6766
7477
|
left -= 1;
|
|
6767
7478
|
const result = await sink({
|
|
6768
|
-
...
|
|
7479
|
+
...scrub === void 0 ? raw : scrubFollowUp(raw, (text) => scrub(maskSecrets(text))),
|
|
6769
7480
|
from: {
|
|
6770
7481
|
run: runId,
|
|
6771
7482
|
node
|
|
@@ -9364,8 +10075,9 @@ const machinePort = (options, runId, id, clock) => {
|
|
|
9364
10075
|
const { rules, ledger } = machine;
|
|
9365
10076
|
return {
|
|
9366
10077
|
priority: rules.priority,
|
|
9367
|
-
slot: async (command, signal) => {
|
|
9368
|
-
|
|
10078
|
+
slot: async (command, signal, run) => {
|
|
10079
|
+
const heavy = isHeavy(rules.heavy, command) || run?.script !== void 0 && isHeavyScript(rules.heavy, run.script);
|
|
10080
|
+
if (ledger === void 0 || !heavy) return none;
|
|
9369
10081
|
let waited = false;
|
|
9370
10082
|
const release = await clock.waitFor(id, () => ledger.take({
|
|
9371
10083
|
run: runId,
|
|
@@ -9415,11 +10127,11 @@ const slotClock = (base, machine) => {
|
|
|
9415
10127
|
},
|
|
9416
10128
|
machine: machine === void 0 ? void 0 : {
|
|
9417
10129
|
...machine,
|
|
9418
|
-
slot: async (command, signal) => {
|
|
10130
|
+
slot: async (command, signal, run) => {
|
|
9419
10131
|
waiting += 1;
|
|
9420
10132
|
tell();
|
|
9421
10133
|
try {
|
|
9422
|
-
return await machine.slot(command, signal);
|
|
10134
|
+
return await machine.slot(command, signal, run);
|
|
9423
10135
|
} finally {
|
|
9424
10136
|
waiting -= 1;
|
|
9425
10137
|
tell();
|
|
@@ -10414,7 +11126,8 @@ const jobOf = (id, task, template) => {
|
|
|
10414
11126
|
brief: task.brief
|
|
10415
11127
|
};
|
|
10416
11128
|
if (template !== void 0) job.template = template;
|
|
10417
|
-
const
|
|
11129
|
+
const folded = task.description?.replaceAll(/[\s\p{Cc}]+/gu, " ").trim();
|
|
11130
|
+
const description = folded === void 0 ? void 0 : Array.from(maskSecrets(folded)).slice(0, 80).join("");
|
|
10418
11131
|
if (description !== void 0 && description !== "") job.description = description;
|
|
10419
11132
|
return job;
|
|
10420
11133
|
};
|
|
@@ -10436,8 +11149,17 @@ const createOwnership = (hooks) => {
|
|
|
10436
11149
|
const leaderNode = owner?.node;
|
|
10437
11150
|
const maxWorkers = leaderNode?.type === "agent.leader" ? leaderNode.maxWorkers ?? 0 : 0;
|
|
10438
11151
|
const spawned = [];
|
|
10439
|
-
|
|
11152
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
11153
|
+
/** Runs the accepted jobs, then frees the children it reserved. */
|
|
10440
11154
|
const runJobs = async (jobs) => {
|
|
11155
|
+
try {
|
|
11156
|
+
return await runAccepted(jobs);
|
|
11157
|
+
} finally {
|
|
11158
|
+
for (const job of jobs) if ("brief" in job) reserved.delete(job.id);
|
|
11159
|
+
}
|
|
11160
|
+
};
|
|
11161
|
+
/** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
|
|
11162
|
+
const runAccepted = async (jobs) => {
|
|
10441
11163
|
const accepted = jobs.filter((j) => "brief" in j);
|
|
10442
11164
|
const notes = [];
|
|
10443
11165
|
if (accepted.length > 0) {
|
|
@@ -10465,14 +11187,13 @@ const createOwnership = (hooks) => {
|
|
|
10465
11187
|
}
|
|
10466
11188
|
};
|
|
10467
11189
|
const delegate = (tasks) => {
|
|
10468
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10469
11190
|
const jobs = tasks.map((task) => {
|
|
10470
11191
|
if (!children.some((c) => c.id === task.child)) {
|
|
10471
11192
|
const names = children.map((c) => c.id).join(", ");
|
|
10472
11193
|
return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
|
|
10473
11194
|
}
|
|
10474
|
-
if (
|
|
10475
|
-
|
|
11195
|
+
if (reserved.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
|
|
11196
|
+
reserved.add(task.child);
|
|
10476
11197
|
return jobOf(task.child, task);
|
|
10477
11198
|
});
|
|
10478
11199
|
return runJobs(jobs);
|
|
@@ -10989,14 +11710,34 @@ const summaryOf = (summaries, id) => {
|
|
|
10989
11710
|
summaries.set(id, created);
|
|
10990
11711
|
return created;
|
|
10991
11712
|
};
|
|
10992
|
-
/**
|
|
10993
|
-
|
|
11713
|
+
/**
|
|
11714
|
+
* A run's summary; a resumed run's leaders keep their forks' spend (ADR-063).
|
|
11715
|
+
* `record` counts a result, `replayed` when a resume handed it back; `counted`
|
|
11716
|
+
* gives the nodes and, on a resumed run, how many were `kept`: handed back
|
|
11717
|
+
* whole, with no result of their own this time.
|
|
11718
|
+
*/
|
|
11719
|
+
const createSummaries = (replay) => {
|
|
10994
11720
|
const summaries = /* @__PURE__ */ new Map();
|
|
11721
|
+
/** Nodes with a result of this run's own: they ran, not kept. */
|
|
11722
|
+
const anew = /* @__PURE__ */ new Set();
|
|
10995
11723
|
const summary = (id) => summaryOf(summaries, id);
|
|
10996
|
-
for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
11724
|
+
for (const [id, costUsd] of replay?.earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
11725
|
+
const record = (id, result, replayed) => {
|
|
11726
|
+
if (!replayed) anew.add(id);
|
|
11727
|
+
recordRun(summary(id), result);
|
|
11728
|
+
};
|
|
11729
|
+
const counted = () => {
|
|
11730
|
+
const nodes = [...summaries.values()];
|
|
11731
|
+
if (replay === void 0) return { nodes };
|
|
11732
|
+
return {
|
|
11733
|
+
nodes,
|
|
11734
|
+
kept: nodes.filter((n) => n.runs > 0 && !anew.has(n.id)).length
|
|
11735
|
+
};
|
|
11736
|
+
};
|
|
10997
11737
|
return {
|
|
10998
|
-
|
|
10999
|
-
|
|
11738
|
+
summary,
|
|
11739
|
+
record,
|
|
11740
|
+
counted
|
|
11000
11741
|
};
|
|
11001
11742
|
};
|
|
11002
11743
|
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
@@ -11047,6 +11788,7 @@ const runResult = (run) => ({
|
|
|
11047
11788
|
worktrees: run.worktrees,
|
|
11048
11789
|
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
11049
11790
|
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
11791
|
+
...run.kept === void 0 ? {} : { kept: run.kept },
|
|
11050
11792
|
failuresHandled: run.failuresHandled,
|
|
11051
11793
|
...unreadOf(run.nodes, run.unread)
|
|
11052
11794
|
});
|
|
@@ -12142,14 +12884,16 @@ const createOwners = (graph) => {
|
|
|
12142
12884
|
* the process (`feedback/agent.ts`) and follow-ups about the product
|
|
12143
12885
|
* (`follow-ups/session.ts`), the latter only when the run keeps them.
|
|
12144
12886
|
*/
|
|
12145
|
-
const createSessionReports = ({ graph, runId, sink, onEvent }) => {
|
|
12887
|
+
const createSessionReports = ({ graph, runId, sink, onEvent, scrub }) => {
|
|
12146
12888
|
const feedback = createFeedback({
|
|
12147
12889
|
graph,
|
|
12148
|
-
onEvent
|
|
12890
|
+
onEvent,
|
|
12891
|
+
scrub
|
|
12149
12892
|
});
|
|
12150
12893
|
const followUps = sink === void 0 ? void 0 : createFollowUps({
|
|
12151
12894
|
runId,
|
|
12152
|
-
sink
|
|
12895
|
+
sink,
|
|
12896
|
+
scrub
|
|
12153
12897
|
});
|
|
12154
12898
|
return {
|
|
12155
12899
|
/** One session's ports; `template` is a spawned worker's node. */
|
|
@@ -12243,7 +12987,8 @@ const createRunPorts = ({ options, runId, slots, scope, stopping }) => {
|
|
|
12243
12987
|
graph,
|
|
12244
12988
|
runId,
|
|
12245
12989
|
sink: options.followUps,
|
|
12246
|
-
onEvent
|
|
12990
|
+
onEvent,
|
|
12991
|
+
scrub
|
|
12247
12992
|
});
|
|
12248
12993
|
const send = bus.rule;
|
|
12249
12994
|
const rules = createRules({
|
|
@@ -12381,7 +13126,7 @@ const startRun = (options) => {
|
|
|
12381
13126
|
const sessionIds = /* @__PURE__ */ new Map();
|
|
12382
13127
|
const spend = spendOf(limits.budgetUsd, options, limits.budgetTokens);
|
|
12383
13128
|
const running = /* @__PURE__ */ new Set();
|
|
12384
|
-
const {
|
|
13129
|
+
const { summary, record, counted } = createSummaries(options.replay);
|
|
12385
13130
|
const queue = [];
|
|
12386
13131
|
const reasons = [];
|
|
12387
13132
|
const stops = createStops(abort.signal);
|
|
@@ -12472,7 +13217,7 @@ const startRun = (options) => {
|
|
|
12472
13217
|
runId,
|
|
12473
13218
|
stopped,
|
|
12474
13219
|
...spend.sums(),
|
|
12475
|
-
|
|
13220
|
+
...counted(),
|
|
12476
13221
|
reasons,
|
|
12477
13222
|
worktrees,
|
|
12478
13223
|
outputs: renderOutputs(graph.workflow.outputs, scope()),
|
|
@@ -12560,7 +13305,7 @@ const startRun = (options) => {
|
|
|
12560
13305
|
clock.leave(id);
|
|
12561
13306
|
outputs[id] = result.output;
|
|
12562
13307
|
if (result.sessionId !== void 0) sessionIds.set(id, result.sessionId);
|
|
12563
|
-
|
|
13308
|
+
record(id, result, recorded !== null);
|
|
12564
13309
|
if (recorded === null) {
|
|
12565
13310
|
options.replay?.ran(result);
|
|
12566
13311
|
onEvent(finishedEvent(id, round, result));
|
|
@@ -13614,6 +14359,13 @@ const STANDARD = /* @__PURE__ */ new Set([
|
|
|
13614
14359
|
"artifacts",
|
|
13615
14360
|
"branch"
|
|
13616
14361
|
]);
|
|
14362
|
+
/**
|
|
14363
|
+
* A name git takes as a branch (`git check-ref-format --branch`'s rules), and
|
|
14364
|
+
* nothing a command would read as an option or a refspec: no leading `-`, no
|
|
14365
|
+
* `:` or `+`, no space or control character. A reported branch reaches tool
|
|
14366
|
+
* arguments, so one that isn't a plain branch name is dropped.
|
|
14367
|
+
*/
|
|
14368
|
+
const isBranchName = (name) => name !== "" && name !== "@" && !name.startsWith("-") && !name.startsWith("/") && !name.endsWith("/") && !name.endsWith(".") && !name.endsWith(".lock") && !name.includes("..") && !name.includes("//") && !name.includes("@{") && !name.includes("/.") && !name.startsWith(".") && !/[\u0000-\u0020\u007F~^:?*[\\+]/u.test(name);
|
|
13617
14369
|
/** Reads a driver's structured output as a report, or `null` if it is not one. */
|
|
13618
14370
|
const parseReport = (value) => {
|
|
13619
14371
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -13630,7 +14382,7 @@ const parseReport = (value) => {
|
|
|
13630
14382
|
artifacts: paths,
|
|
13631
14383
|
fields: Object.fromEntries(Object.entries(record).filter(([key]) => !STANDARD.has(key)))
|
|
13632
14384
|
};
|
|
13633
|
-
if (typeof branch === "string" && branch
|
|
14385
|
+
if (typeof branch === "string" && isBranchName(branch)) report.branch = branch;
|
|
13634
14386
|
return report;
|
|
13635
14387
|
};
|
|
13636
14388
|
// ../core/src/run/brief.ts
|
|
@@ -16141,6 +16893,19 @@ const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.pa
|
|
|
16141
16893
|
"--tags",
|
|
16142
16894
|
"--remotes"
|
|
16143
16895
|
])).stdout;
|
|
16896
|
+
/**
|
|
16897
|
+
* Whether a detached worktree's HEAD is already in the current branch's
|
|
16898
|
+
* history.
|
|
16899
|
+
*/
|
|
16900
|
+
const headIsMerged = async (root, path) => {
|
|
16901
|
+
const head = await git(path, ["rev-parse", "HEAD"]);
|
|
16902
|
+
return head.code === 0 && (await git(root, [
|
|
16903
|
+
"merge-base",
|
|
16904
|
+
"--is-ancestor",
|
|
16905
|
+
head.stdout.trim(),
|
|
16906
|
+
"HEAD"
|
|
16907
|
+
])).code === 0;
|
|
16908
|
+
};
|
|
16144
16909
|
/** One created worktree or branch, with its state read from git. */
|
|
16145
16910
|
const stateOf = async (root, entries, item, recorded) => {
|
|
16146
16911
|
const { branch } = item;
|
|
@@ -16157,6 +16922,7 @@ const stateOf = async (root, entries, item, recorded) => {
|
|
|
16157
16922
|
])).stdout;
|
|
16158
16923
|
const ahead = Number(counted);
|
|
16159
16924
|
const changes = lines(status).length;
|
|
16925
|
+
const merged = on === void 0 ? entry !== void 0 && !missing && await headIsMerged(root, entry.path) : ahead === 0;
|
|
16160
16926
|
return {
|
|
16161
16927
|
...branch === void 0 ? {} : { branch },
|
|
16162
16928
|
...checkedOut === void 0 ? {} : { checkedOut },
|
|
@@ -16168,7 +16934,7 @@ const stateOf = async (root, entries, item, recorded) => {
|
|
|
16168
16934
|
dirty: changes > 0,
|
|
16169
16935
|
changes,
|
|
16170
16936
|
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
16171
|
-
merged
|
|
16937
|
+
merged,
|
|
16172
16938
|
inMainCheckout: entry?.path === root || holder?.path === root,
|
|
16173
16939
|
...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
|
|
16174
16940
|
};
|
|
@@ -16325,6 +17091,31 @@ const applyPrune = (store, plan) => {
|
|
|
16325
17091
|
}
|
|
16326
17092
|
return outcome;
|
|
16327
17093
|
};
|
|
17094
|
+
// ../core/src/store/recorder.ts
|
|
17095
|
+
/**
|
|
17096
|
+
* @file Records a run as it happens: each event appended to the store and
|
|
17097
|
+
* copied to the run's `events.jsonl`, both with secrets masked, before
|
|
17098
|
+
* anything else sees it; and adds to a finished run (`ork chat`).
|
|
17099
|
+
*/
|
|
17100
|
+
const EVENTS_FILE = "events.jsonl";
|
|
17101
|
+
/**
|
|
17102
|
+
* Appends to a run already in the store, e.g. `ork chat` resuming a finished
|
|
17103
|
+
* node's session; `runDir` gets the JSONL copy.
|
|
17104
|
+
*/
|
|
17105
|
+
const appendRecorder = (store, runId, runDir) => {
|
|
17106
|
+
mkdirSync(runDir, { recursive: true });
|
|
17107
|
+
const file = path.join(runDir, EVENTS_FILE);
|
|
17108
|
+
return (event) => {
|
|
17109
|
+
const stored = store.append(runId, event);
|
|
17110
|
+
appendFileSync(file, `${JSON.stringify(stored, maskReplacer)}\n`);
|
|
17111
|
+
return stored;
|
|
17112
|
+
};
|
|
17113
|
+
};
|
|
17114
|
+
/** Begins `run` in the store; `runDir` gets the JSONL copy. */
|
|
17115
|
+
const createRecorder = (store, run, runDir) => {
|
|
17116
|
+
store.begin(run);
|
|
17117
|
+
return appendRecorder(store, run.id, runDir);
|
|
17118
|
+
};
|
|
16328
17119
|
// ../core/src/store/decisions.ts
|
|
16329
17120
|
const choicesOf = (json) => {
|
|
16330
17121
|
const parsed = JSON.parse(json);
|
|
@@ -17210,10 +18001,25 @@ const openReadOnly = (file) => {
|
|
|
17210
18001
|
throw error;
|
|
17211
18002
|
}
|
|
17212
18003
|
};
|
|
18004
|
+
/**
|
|
18005
|
+
* Keeps Ork's folder for the person alone: `~/.ork` holds every run's prompts
|
|
18006
|
+
* and output, its sockets and its locks, so it is made 0700, and one an older
|
|
18007
|
+
* Ork made with the umask's mode is narrowed. A store elsewhere (a test's) only
|
|
18008
|
+
* gets its folder made.
|
|
18009
|
+
*/
|
|
18010
|
+
const keepPrivate = (dir) => {
|
|
18011
|
+
mkdirSync(dir, {
|
|
18012
|
+
recursive: true,
|
|
18013
|
+
mode: 448
|
|
18014
|
+
});
|
|
18015
|
+
if (path.basename(dir) !== ".ork") return;
|
|
18016
|
+
if ((statSync(dir).mode & 63) !== 0) chmodSync(dir, 448);
|
|
18017
|
+
};
|
|
17213
18018
|
/** A database opened for writing: created, in WAL mode, migrated. */
|
|
17214
18019
|
const openWritable = (file) => {
|
|
17215
|
-
if (file !== ":memory:")
|
|
18020
|
+
if (file !== ":memory:") keepPrivate(path.dirname(file));
|
|
17216
18021
|
const db = new DatabaseSync(file);
|
|
18022
|
+
if (file !== ":memory:") chmodSync(file, 384);
|
|
17217
18023
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
17218
18024
|
db.exec("PRAGMA journal_mode = WAL");
|
|
17219
18025
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
@@ -18360,11 +19166,17 @@ const syncFrom = async (worktree, from) => {
|
|
|
18360
19166
|
*/
|
|
18361
19167
|
/** An item's name: its branch, or its run and node when created detached. */
|
|
18362
19168
|
const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
|
|
18363
|
-
/**
|
|
19169
|
+
/**
|
|
19170
|
+
* Merged into the current branch (a detached worktree too, when its HEAD is
|
|
19171
|
+
* already there), not, or detached on no branch with work the current branch
|
|
19172
|
+
* lacks.
|
|
19173
|
+
*/
|
|
18364
19174
|
const mergeStateOf = (item) => {
|
|
18365
|
-
if (item.
|
|
18366
|
-
return item.
|
|
19175
|
+
if (item.merged) return "merged";
|
|
19176
|
+
return item.branch === void 0 && item.checkedOut === void 0 ? "detached" : "unmerged";
|
|
18367
19177
|
};
|
|
19178
|
+
/** On no branch at all: created detached and not on a branch since. */
|
|
19179
|
+
const isDetached = (item) => item.branch === void 0 && item.checkedOut === void 0;
|
|
18368
19180
|
/** `3 commits`, `1 commit`. */
|
|
18369
19181
|
const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
18370
19182
|
/**
|
|
@@ -18385,12 +19197,11 @@ const stateParts = (item, into) => {
|
|
|
18385
19197
|
else if (item.missing) part("missing (prunable)");
|
|
18386
19198
|
if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
|
|
18387
19199
|
if (item.dirty) part("uncommitted changes", true);
|
|
18388
|
-
|
|
18389
|
-
if (state === "unmerged") {
|
|
19200
|
+
if (mergeStateOf(item) === "unmerged") {
|
|
18390
19201
|
const where = into === void 0 ? "" : ` into ${into}`;
|
|
18391
19202
|
part(`${counted(item.ahead, "commit")} not merged${where}`);
|
|
18392
19203
|
}
|
|
18393
|
-
if (
|
|
19204
|
+
if (isDetached(item)) {
|
|
18394
19205
|
if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
|
|
18395
19206
|
else if (item.path !== null && !item.missing) part("nothing committed");
|
|
18396
19207
|
}
|
|
@@ -18412,7 +19223,7 @@ const planWhat = (step) => {
|
|
|
18412
19223
|
const planLine = (step) => {
|
|
18413
19224
|
const { item } = step;
|
|
18414
19225
|
return {
|
|
18415
|
-
text: `${item
|
|
19226
|
+
text: `${isDetached(item) ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
|
|
18416
19227
|
...step.skip === void 0 ? {} : { reason: step.skip }
|
|
18417
19228
|
};
|
|
18418
19229
|
};
|
|
@@ -19019,4 +19830,4 @@ const createWorkspace = (options) => {
|
|
|
19019
19830
|
close
|
|
19020
19831
|
};
|
|
19021
19832
|
};
|
|
19022
|
-
export {
|
|
19833
|
+
export { driverQueues as $, createSignals as $n, guardedBase as $r, stepActs as $t, triggerStanding as A, driverChecks as Ai, requestPause as An, applySteps as Ar, runScope as At, appendRecorder as B, stillTheSameProcess as Bn, hasInRepoFiles as Br, programFound as Bt, detectInstaller as C, NodeIdSchema as Ci, liveSessionOf as Cn, startControl as Cr, checkWorkflowText as Ct, lateTriggers as D, TOOL_PREFIX as Di, linkNodes as Dn, maskReplacer as Dr, MESSAGE_LIMIT as Dt, parseVersion as E, INTEGRATIONS as Ei, endLinksOf as En, MASK as Er, startRun as Et, watchingPid as F, DRIVER_IDS as Fi, pausedOf as Fn, createAgent as Fr, stoppedEnd as Ft, listOrkWorktrees as G, gitRulesOf as Gn, projectsHome as Gr, followUpListTable as Gt, applyPrune as H, nestItems as Hn, newProjectId as Hr, LIST_TABLES as Ht, activeTree as I, firstLine as Ii, runStatus as In, deleteAgent as Ir, declaredProblem as It, parseInputPairs as J, changeFollowUp as Jn, workflowDirOf as Jr, worktreeListTable as Jt, createReplay as K, loadProject as Kn, runDirOf as Kr, runListTable as Kt, isNodeEvent as L, EXPERIMENTS as Li, slotWaitsOf as Ln, AGENT_SOURCES as Lr, slotClock as Lt, readProjectTriggers as M, AGENT_TOOL_LAYOUTS as Mi, tellLiveRun as Mn, linkTools as Mr, lockWaitLine as Mt, workflowTextHash as N, ORK_LAYOUT as Ni, liveStatus as Nn, toolLinks as Nr, endedWith as Nt, triggersReport as O, integrationFor as Oi, unlinkNodes as On, maskSecrets as Or, userMessageText as Ot, takeWatchLock as P, AGENT_DRIVERS as Pi, lockWaitsOf as Pn, toolsInUse as Pr, failedWith as Pt, sessionRefusals as Q, isGithubIssueUrl as Qn, estimateTokens as Qr, planClean as Qt, openStore as R, experimentsOn as Ri, templateOf$1 as Rn, catalogAgent as Rr, slotWaitLine as Rt, runningScript as S, DriverSchema as Si, followUpProblem as Sn, readCondition as Sr, reportSchemaFor as St, isNewer as T, timeoutMs as Ti, addressOf as Tn, TOOL_INPUT_KEYS as Tr, walkSummary as Tt, planPrune as U, countedTokens as Un, placeFiles as Ur, decisionListTable as Ut, createRecorder as V, isItem as Vn, inRepoPlace as Vr, startAt as Vt, createdWorktrees as W, PROJECT_FILE as Wn, placeFor as Wr, feedbackListTable as Wt, budgetWarnings as X, findFollowUp as Xn, projectRootOf as Xr, cleanStep as Xt, resolveInputs as Y, dueCause as Yn, inside as Yr, applyClean as Yt, driverRefusals as Z, followUpActions as Zn, LESSON_CAP_TOKENS as Zr, keyOf as Zt, usageReport as _, expandEdge as _i, checkFollowUpsDue as _n, feedbackActions as _r, planReason as _t, planRemove as a, newWorkflowText as ai, envReadsOf as an, previewPost as ar, answerDecision as at, releaseNotes as b, isClaudeNode as bi, startFollowUpChecks as bn, parseTemplate as br, planNodeSession as bt, repoRoot as c, beforeOf as ci, sessionDriver as cn, FEEDBACK_KINDS as cr, adoptedCalls as ct, mergeStateOf as d, parseWorkflow as di, MCP_FILE as dn, DISCUSSIONS as dr, DETACHED_ENV as dt, plainPathInside as ei, stepPrunes as en, endOf as er, TOO_LONG as et, planLine as f, serializeWorkflow as fi, mcpNotText as fn, planPost as fr, detachRun as ft, USAGE_DIMENSIONS as g, CheckReportSchema as gi, postFollowUpMessage as gn, normalizeTitle as gr, limitsReport as gt, resolveBase as h, WorkflowSchema as hi, createFollowUps as hn, createFeedback as hr, limitsNow as ht, lostWork as i, applyEdits as ii, clearGitLocalEnv as in, endFor as ir, followRunTree as it, declaredTriggers as j, readyDriver as ji, requestStop as jn, planMoveInto as jr, calledWorkflows as jt, createWatcher as k, PREFERRED_DRIVER as ki, requestContinue as kn, planAgentMigration as kr, heldTimeout as kt, isDetached as l, programsRunBy as li, nodeSays as ln, FEEDBACK_SEVERITIES as lr, detachResume as lt, stateParts as m, EdgeInputSchema as mi, FOLLOW_UP_KINDS as mn, sendPost as mr, NOT_STARTED as mt, resumedOptions as n, skillsIn as ni, git as nn, END_DETAIL_LIMIT as nr, sendFromScript as nt, projectHead as o, readDocument as oi, modelDriver as on, checkNote as or, pendingDecisions as ot, removeRefusal as p, markValue as pi, serversIn as pn, runGh as pr, followEvents as pt, replayLoopAnswers as q, machineRulesOf as qn, shownPath as qr, usageListTable as qt, applyRemove as r, WorkflowEditSchema as ri, gitOk as rn, END_KINDS as rr, createRelay as rt, readWorktrees as s, renameNodeTypes as si, namingNoDriver as sn, recordNote as sr, createCalls as st, createWorkspace as t, writePlainFile as ti, inspectProject as tn, outcomeEnd as tr, scriptRefusal as tt, labelOf as u, renderJsonSchema as ui, declaredOutputOf as un, FEEDBACK_TARGETS as ur, resumable as ut, capsHere as v, settingsOf as vi, markDue as vn, openFixRun as vr, explainWorkflowText as vt, compareVersions as w, retiredDriverHint as wi, DEFAULT_FOLLOW_UPS_RULE as wn, socketRoom as wr, walkChecks as wt, runningRunIds as x, isToolNode as xi, deliverFollowUp as xn, renderTemplate as xr, parseReport as xt, planUpgrade as y, NodeSchema as yi, recordPublished as yn, evaluate as yr, explainSession as yt, storePath as z, knownExperiments as zi, processAlive as zn, loadAgents as zr, lowerChild as zt };
|