@krapht/ork 1.0.0-alpha.18 → 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__-B8TNbFtN.mjs → __build_info__-dPf4cEGL.mjs} +3 -134
- package/dist/{attach-CU4Pk42S.mjs → attach-BQl8nQz2.mjs} +5 -5
- package/dist/{attach-qvfyaQXC.mjs → attach-DhZQ87Nz.mjs} +49 -224
- package/dist/bin.mjs +204 -154
- package/dist/{brief-Bh1FnAHv.mjs → brief-BQbeoPMc.mjs} +19 -9
- package/dist/{builtin-files-dnTP2GaX.mjs → builtin-files-CWL3JEo-.mjs} +1 -1
- package/dist/{command-tree-BkSYWmYJ.mjs → command-tree-B6uvMJq1.mjs} +1 -1
- package/dist/{completion-CzXUGSVn.mjs → completion-DKPuD_Rw.mjs} +47 -14
- package/dist/{context-b4_gPlv3.mjs → context-Bw6R_EYq.mjs} +3 -3
- package/dist/detached-BQlbmqR0.mjs +38 -0
- package/dist/{find-P-VV4B47.mjs → find-B30-rd-u.mjs} +1 -1
- package/dist/flags-ByYBx-Xb.mjs +170 -0
- package/dist/{follow-ups-B_c1DSpA.mjs → follow-ups-Cxca1Jgg.mjs} +24 -18
- package/dist/{format-BS1qzpkf.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-DKoMGTNE.mjs → in-a-run-CZFzoT3R.mjs} +2 -2
- package/dist/{install-B743Bwzz.mjs → install-CsqK1avz.mjs} +2 -183
- package/dist/{limits-DrsKCqaN.mjs → limits-CXh052II.mjs} +1 -1
- package/dist/{mcp-BMwekw38.mjs → mcp-B4wME9C4.mjs} +6 -6
- package/dist/{once-6L8gciJV.mjs → once-BKn3pOtg.mjs} +3 -3
- package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
- package/dist/{pause-8gnVgICR.mjs → pause-B1grR1cl.mjs} +3 -3
- package/dist/{print-DLe4MHn-.mjs → print-Bd8MWVJk.mjs} +1 -1
- package/dist/{project-records-CsILxwfI.mjs → project-records-BOEVs69i.mjs} +1 -1
- package/dist/{parse-BzWpMN-a.mjs → refusals-DQQX3AIb.mjs} +10 -1
- package/dist/{relay-Bvm9JEoN.mjs → relay-CeNiq3GT.mjs} +1 -1
- package/dist/{run-D4yuTmmJ.mjs → run-C0C54UiU.mjs} +116 -137
- package/dist/{run-y8jI7bAI.mjs → run-D6yU2rLf.mjs} +1 -1
- package/dist/{send-BV9eIxQ7.mjs → send-DqgF58hb.mjs} +2 -2
- package/dist/{src-Frwpx3WF.mjs → src-Ciy7i3se.mjs} +843 -66
- package/dist/{src-ygFMh2eP.mjs → src-D2qpd-G7.mjs} +4 -4
- package/dist/{src-CVeA4tAw.mjs → src-DP02RP9O.mjs} +227 -180
- package/dist/{src-DWAcd3Sy.mjs → src-n-u4TGev.mjs} +186 -43
- package/dist/{store-B0nGqnPq.mjs → store-2KXzMCEN.mjs} +1 -1
- package/dist/ui/assets/app-C5vO1M-l.css +1 -0
- package/dist/ui/assets/{app-C4yxmYQR.js → app-ICEKpy_C.js} +21 -20
- package/dist/ui/assets/{index-CcbJWo5C.js → index-BGaix5wh.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-BZ9BhFw8.mjs → ui-CAvygxTb.mjs} +53 -24
- package/dist/{usage-BIs4j5Rw.mjs → usage-BFBco5OS.mjs} +7 -7
- package/dist/{validate-Xn2ZbJQC.mjs → validate-UrmC5AG1.mjs} +5 -5
- package/dist/{watch-9VM3DFTp.mjs → watch-DTxkgB8R.mjs} +5 -5
- package/dist/{workflows-CBYtycIy.mjs → workflows-8GV-SeRm.mjs} +28 -4
- package/dist/{worktrees-C7vfh7y5.mjs → worktrees-CUdUvZlO.mjs} +9 -6
- package/package.json +1 -1
- package/dist/flags-CqjJ44uW.mjs +0 -125
- package/dist/post-y1qL1Fnj.mjs +0 -46
- package/dist/ui/assets/app-9Q-62Ywz.css +0 -1
|
@@ -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
|
|
@@ -4082,6 +4358,75 @@ const followSocket = (file, after, onEvent, signal) => {
|
|
|
4082
4358
|
});
|
|
4083
4359
|
return promise;
|
|
4084
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
|
+
};
|
|
4085
4430
|
// ../core/src/expr/parse.ts
|
|
4086
4431
|
/**
|
|
4087
4432
|
* @file A small, safe expression language for `when:` conditions and `{{ }}`
|
|
@@ -4635,6 +4980,60 @@ const renderTemplate = (parts, scope, { readable = false } = {}) => {
|
|
|
4635
4980
|
}
|
|
4636
4981
|
return text;
|
|
4637
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
|
+
};
|
|
4638
5037
|
// ../core/src/feedback/fingerprint.ts
|
|
4639
5038
|
/**
|
|
4640
5039
|
* @file Feedback fingerprints: records about the same thing (same target, kind,
|
|
@@ -4657,16 +5056,17 @@ const defaultTarget = (kind) => {
|
|
|
4657
5056
|
};
|
|
4658
5057
|
/** The draft's target, or its kind's default. */
|
|
4659
5058
|
const targetOf = (draft) => draft.target ?? defaultTarget(draft.kind);
|
|
4660
|
-
const MAX_TITLE = 120;
|
|
5059
|
+
const MAX_TITLE$1 = 120;
|
|
4661
5060
|
const MAX_BODY = 2e3;
|
|
4662
5061
|
const MAX_EVIDENCE = 10;
|
|
4663
|
-
const createFeedback = ({ graph, onEvent }) => {
|
|
5062
|
+
const createFeedback = ({ graph, onEvent, scrub = (text) => text }) => {
|
|
4664
5063
|
const seen = /* @__PURE__ */ new Set();
|
|
5064
|
+
const clean = (text) => scrub(maskSecrets(text));
|
|
4665
5065
|
return { port: (id, template) => {
|
|
4666
5066
|
let left = 3;
|
|
4667
5067
|
const nodeType = graph.nodes.get(template ?? id)?.node.type;
|
|
4668
5068
|
return { report: (draft) => {
|
|
4669
|
-
const title =
|
|
5069
|
+
const title = clean(draft.title.trim()).slice(0, MAX_TITLE$1);
|
|
4670
5070
|
if (title === "") return {
|
|
4671
5071
|
ok: false,
|
|
4672
5072
|
error: "the title is empty"
|
|
@@ -4696,8 +5096,8 @@ const createFeedback = ({ graph, onEvent }) => {
|
|
|
4696
5096
|
target,
|
|
4697
5097
|
severity: draft.severity ?? "medium",
|
|
4698
5098
|
title,
|
|
4699
|
-
body:
|
|
4700
|
-
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)),
|
|
4701
5101
|
fingerprint: print,
|
|
4702
5102
|
origin: nodeType === void 0 ? {} : { nodeType }
|
|
4703
5103
|
}
|
|
@@ -4961,6 +5361,136 @@ const FEEDBACK_SEVERITIES = [
|
|
|
4961
5361
|
"medium",
|
|
4962
5362
|
"high"
|
|
4963
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
|
+
};
|
|
4964
5494
|
// ../core/src/runtime/ends.ts
|
|
4965
5495
|
/**
|
|
4966
5496
|
* @file How a node ended, as one of a closed set of kinds (specs/runtime/
|
|
@@ -5316,6 +5846,137 @@ const createSignals = (options = {}) => {
|
|
|
5316
5846
|
return signal === null ? [event] : [event, ...record(...signal)];
|
|
5317
5847
|
};
|
|
5318
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
|
+
};
|
|
5319
5980
|
// ../core/src/project/heavy.ts
|
|
5320
5981
|
/**
|
|
5321
5982
|
* @file A project's rules for the machine its runs share: the heavy commands
|
|
@@ -5354,6 +6015,16 @@ const matchesPattern = (pattern, command) => {
|
|
|
5354
6015
|
};
|
|
5355
6016
|
/** Whether `command` is one of the project's heavy commands. */
|
|
5356
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));
|
|
5357
6028
|
const machineRulesOf = (project) => ({
|
|
5358
6029
|
heavy: project?.heavy ?? [],
|
|
5359
6030
|
priority: project?.priority ?? "low"
|
|
@@ -6317,9 +6988,12 @@ const scrubFor = (to, scrub) => {
|
|
|
6317
6988
|
if (to.startsWith("person:")) return `person:${scrub(to.slice(7))}`;
|
|
6318
6989
|
return to;
|
|
6319
6990
|
};
|
|
6320
|
-
/**
|
|
6321
|
-
|
|
6322
|
-
|
|
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) => {
|
|
6323
6997
|
return {
|
|
6324
6998
|
...input,
|
|
6325
6999
|
for: scrubFor(input.for, scrub),
|
|
@@ -6366,10 +7040,10 @@ const followUpStore = (options) => {
|
|
|
6366
7040
|
return {
|
|
6367
7041
|
place: adapter.place,
|
|
6368
7042
|
add: async (raw) => {
|
|
6369
|
-
const input = scrubFollowUp(raw, {
|
|
7043
|
+
const input = scrubFollowUp(raw, (text) => redact(text, {
|
|
6370
7044
|
roots: [project, worktree],
|
|
6371
7045
|
home
|
|
6372
|
-
});
|
|
7046
|
+
}));
|
|
6373
7047
|
const where = input.where ?? [];
|
|
6374
7048
|
const at = now();
|
|
6375
7049
|
const kept = list().find((f) => sameFollowUp(f, {
|
|
@@ -6566,7 +7240,8 @@ const deliverFollowUp = async (input, options) => {
|
|
|
6566
7240
|
* too, with a note saying so, never dropped. A dated one is due on its date;
|
|
6567
7241
|
* `after-deploy` only when someone says so (`markDue`). A run that publishes
|
|
6568
7242
|
* records its ref on the follow-ups it left (`recordPublished`), and the
|
|
6569
|
-
* `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`).
|
|
6570
7245
|
*/
|
|
6571
7246
|
const DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
6572
7247
|
const why = (result) => result.stderr.trim() || result.stdout.trim() || "no output";
|
|
@@ -6712,25 +7387,6 @@ const recordPublished = (rows, published, now = () => (/* @__PURE__ */ new Date(
|
|
|
6712
7387
|
};
|
|
6713
7388
|
};
|
|
6714
7389
|
/**
|
|
6715
|
-
* `ork run start --follow-ups <ids>`: the open `agent` follow-ups among `ids`
|
|
6716
|
-
* become `run`'s brief; why each other id wasn't taken.
|
|
6717
|
-
*/
|
|
6718
|
-
const takeFollowUps = (rows, taking, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
6719
|
-
const taken = [];
|
|
6720
|
-
const refused = [];
|
|
6721
|
-
for (const id of taking.ids) {
|
|
6722
|
-
const f = rows.followUp(taking.project, id);
|
|
6723
|
-
if (f === void 0) refused.push(`${id}: there is no such follow-up`);
|
|
6724
|
-
else if (f.for !== "agent") refused.push(`${id}: it is for ${f.for}`);
|
|
6725
|
-
else if (f.status === "done" || f.status === "dropped") refused.push(`${id}: it is ${f.status}`);
|
|
6726
|
-
else taken.push(changed(rows, f, { takenBy: taking.run }, now()));
|
|
6727
|
-
}
|
|
6728
|
-
return {
|
|
6729
|
-
taken,
|
|
6730
|
-
refused
|
|
6731
|
-
};
|
|
6732
|
-
};
|
|
6733
|
-
/**
|
|
6734
7390
|
* Checks the projects' follow-ups now and on a schedule, while `ork ui` or `ork
|
|
6735
7391
|
* watch` runs; `onDue` hears of those that came due (a notification). Returns
|
|
6736
7392
|
* the stop. A check still going when the next is due is not doubled.
|
|
@@ -6766,18 +7422,61 @@ const startFollowUpChecks = (options) => {
|
|
|
6766
7422
|
clearInterval(timer);
|
|
6767
7423
|
};
|
|
6768
7424
|
};
|
|
6769
|
-
|
|
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 }) => ({
|
|
6770
7469
|
/** One session's port. */
|
|
6771
7470
|
port: (node, cwd) => {
|
|
6772
7471
|
let left = 30;
|
|
6773
|
-
return { add: async (
|
|
7472
|
+
return { add: async (raw) => {
|
|
6774
7473
|
if (left === 0) return {
|
|
6775
7474
|
ok: false,
|
|
6776
7475
|
error: `you already left ${String(30)} follow-ups in this session; put the rest in your report`
|
|
6777
7476
|
};
|
|
6778
7477
|
left -= 1;
|
|
6779
7478
|
const result = await sink({
|
|
6780
|
-
...
|
|
7479
|
+
...scrub === void 0 ? raw : scrubFollowUp(raw, (text) => scrub(maskSecrets(text))),
|
|
6781
7480
|
from: {
|
|
6782
7481
|
run: runId,
|
|
6783
7482
|
node
|
|
@@ -9376,8 +10075,9 @@ const machinePort = (options, runId, id, clock) => {
|
|
|
9376
10075
|
const { rules, ledger } = machine;
|
|
9377
10076
|
return {
|
|
9378
10077
|
priority: rules.priority,
|
|
9379
|
-
slot: async (command, signal) => {
|
|
9380
|
-
|
|
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;
|
|
9381
10081
|
let waited = false;
|
|
9382
10082
|
const release = await clock.waitFor(id, () => ledger.take({
|
|
9383
10083
|
run: runId,
|
|
@@ -9427,11 +10127,11 @@ const slotClock = (base, machine) => {
|
|
|
9427
10127
|
},
|
|
9428
10128
|
machine: machine === void 0 ? void 0 : {
|
|
9429
10129
|
...machine,
|
|
9430
|
-
slot: async (command, signal) => {
|
|
10130
|
+
slot: async (command, signal, run) => {
|
|
9431
10131
|
waiting += 1;
|
|
9432
10132
|
tell();
|
|
9433
10133
|
try {
|
|
9434
|
-
return await machine.slot(command, signal);
|
|
10134
|
+
return await machine.slot(command, signal, run);
|
|
9435
10135
|
} finally {
|
|
9436
10136
|
waiting -= 1;
|
|
9437
10137
|
tell();
|
|
@@ -10426,7 +11126,8 @@ const jobOf = (id, task, template) => {
|
|
|
10426
11126
|
brief: task.brief
|
|
10427
11127
|
};
|
|
10428
11128
|
if (template !== void 0) job.template = template;
|
|
10429
|
-
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("");
|
|
10430
11131
|
if (description !== void 0 && description !== "") job.description = description;
|
|
10431
11132
|
return job;
|
|
10432
11133
|
};
|
|
@@ -10448,8 +11149,17 @@ const createOwnership = (hooks) => {
|
|
|
10448
11149
|
const leaderNode = owner?.node;
|
|
10449
11150
|
const maxWorkers = leaderNode?.type === "agent.leader" ? leaderNode.maxWorkers ?? 0 : 0;
|
|
10450
11151
|
const spawned = [];
|
|
10451
|
-
|
|
11152
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
11153
|
+
/** Runs the accepted jobs, then frees the children it reserved. */
|
|
10452
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) => {
|
|
10453
11163
|
const accepted = jobs.filter((j) => "brief" in j);
|
|
10454
11164
|
const notes = [];
|
|
10455
11165
|
if (accepted.length > 0) {
|
|
@@ -10477,14 +11187,13 @@ const createOwnership = (hooks) => {
|
|
|
10477
11187
|
}
|
|
10478
11188
|
};
|
|
10479
11189
|
const delegate = (tasks) => {
|
|
10480
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10481
11190
|
const jobs = tasks.map((task) => {
|
|
10482
11191
|
if (!children.some((c) => c.id === task.child)) {
|
|
10483
11192
|
const names = children.map((c) => c.id).join(", ");
|
|
10484
11193
|
return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
|
|
10485
11194
|
}
|
|
10486
|
-
if (
|
|
10487
|
-
|
|
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);
|
|
10488
11197
|
return jobOf(task.child, task);
|
|
10489
11198
|
});
|
|
10490
11199
|
return runJobs(jobs);
|
|
@@ -11001,14 +11710,34 @@ const summaryOf = (summaries, id) => {
|
|
|
11001
11710
|
summaries.set(id, created);
|
|
11002
11711
|
return created;
|
|
11003
11712
|
};
|
|
11004
|
-
/**
|
|
11005
|
-
|
|
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) => {
|
|
11006
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();
|
|
11007
11723
|
const summary = (id) => summaryOf(summaries, id);
|
|
11008
|
-
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
|
+
};
|
|
11009
11737
|
return {
|
|
11010
|
-
|
|
11011
|
-
|
|
11738
|
+
summary,
|
|
11739
|
+
record,
|
|
11740
|
+
counted
|
|
11012
11741
|
};
|
|
11013
11742
|
};
|
|
11014
11743
|
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
@@ -11059,6 +11788,7 @@ const runResult = (run) => ({
|
|
|
11059
11788
|
worktrees: run.worktrees,
|
|
11060
11789
|
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
11061
11790
|
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
11791
|
+
...run.kept === void 0 ? {} : { kept: run.kept },
|
|
11062
11792
|
failuresHandled: run.failuresHandled,
|
|
11063
11793
|
...unreadOf(run.nodes, run.unread)
|
|
11064
11794
|
});
|
|
@@ -12154,14 +12884,16 @@ const createOwners = (graph) => {
|
|
|
12154
12884
|
* the process (`feedback/agent.ts`) and follow-ups about the product
|
|
12155
12885
|
* (`follow-ups/session.ts`), the latter only when the run keeps them.
|
|
12156
12886
|
*/
|
|
12157
|
-
const createSessionReports = ({ graph, runId, sink, onEvent }) => {
|
|
12887
|
+
const createSessionReports = ({ graph, runId, sink, onEvent, scrub }) => {
|
|
12158
12888
|
const feedback = createFeedback({
|
|
12159
12889
|
graph,
|
|
12160
|
-
onEvent
|
|
12890
|
+
onEvent,
|
|
12891
|
+
scrub
|
|
12161
12892
|
});
|
|
12162
12893
|
const followUps = sink === void 0 ? void 0 : createFollowUps({
|
|
12163
12894
|
runId,
|
|
12164
|
-
sink
|
|
12895
|
+
sink,
|
|
12896
|
+
scrub
|
|
12165
12897
|
});
|
|
12166
12898
|
return {
|
|
12167
12899
|
/** One session's ports; `template` is a spawned worker's node. */
|
|
@@ -12255,7 +12987,8 @@ const createRunPorts = ({ options, runId, slots, scope, stopping }) => {
|
|
|
12255
12987
|
graph,
|
|
12256
12988
|
runId,
|
|
12257
12989
|
sink: options.followUps,
|
|
12258
|
-
onEvent
|
|
12990
|
+
onEvent,
|
|
12991
|
+
scrub
|
|
12259
12992
|
});
|
|
12260
12993
|
const send = bus.rule;
|
|
12261
12994
|
const rules = createRules({
|
|
@@ -12393,7 +13126,7 @@ const startRun = (options) => {
|
|
|
12393
13126
|
const sessionIds = /* @__PURE__ */ new Map();
|
|
12394
13127
|
const spend = spendOf(limits.budgetUsd, options, limits.budgetTokens);
|
|
12395
13128
|
const running = /* @__PURE__ */ new Set();
|
|
12396
|
-
const {
|
|
13129
|
+
const { summary, record, counted } = createSummaries(options.replay);
|
|
12397
13130
|
const queue = [];
|
|
12398
13131
|
const reasons = [];
|
|
12399
13132
|
const stops = createStops(abort.signal);
|
|
@@ -12484,7 +13217,7 @@ const startRun = (options) => {
|
|
|
12484
13217
|
runId,
|
|
12485
13218
|
stopped,
|
|
12486
13219
|
...spend.sums(),
|
|
12487
|
-
|
|
13220
|
+
...counted(),
|
|
12488
13221
|
reasons,
|
|
12489
13222
|
worktrees,
|
|
12490
13223
|
outputs: renderOutputs(graph.workflow.outputs, scope()),
|
|
@@ -12572,7 +13305,7 @@ const startRun = (options) => {
|
|
|
12572
13305
|
clock.leave(id);
|
|
12573
13306
|
outputs[id] = result.output;
|
|
12574
13307
|
if (result.sessionId !== void 0) sessionIds.set(id, result.sessionId);
|
|
12575
|
-
|
|
13308
|
+
record(id, result, recorded !== null);
|
|
12576
13309
|
if (recorded === null) {
|
|
12577
13310
|
options.replay?.ran(result);
|
|
12578
13311
|
onEvent(finishedEvent(id, round, result));
|
|
@@ -16160,6 +16893,19 @@ const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.pa
|
|
|
16160
16893
|
"--tags",
|
|
16161
16894
|
"--remotes"
|
|
16162
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
|
+
};
|
|
16163
16909
|
/** One created worktree or branch, with its state read from git. */
|
|
16164
16910
|
const stateOf = async (root, entries, item, recorded) => {
|
|
16165
16911
|
const { branch } = item;
|
|
@@ -16176,6 +16922,7 @@ const stateOf = async (root, entries, item, recorded) => {
|
|
|
16176
16922
|
])).stdout;
|
|
16177
16923
|
const ahead = Number(counted);
|
|
16178
16924
|
const changes = lines(status).length;
|
|
16925
|
+
const merged = on === void 0 ? entry !== void 0 && !missing && await headIsMerged(root, entry.path) : ahead === 0;
|
|
16179
16926
|
return {
|
|
16180
16927
|
...branch === void 0 ? {} : { branch },
|
|
16181
16928
|
...checkedOut === void 0 ? {} : { checkedOut },
|
|
@@ -16187,7 +16934,7 @@ const stateOf = async (root, entries, item, recorded) => {
|
|
|
16187
16934
|
dirty: changes > 0,
|
|
16188
16935
|
changes,
|
|
16189
16936
|
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
16190
|
-
merged
|
|
16937
|
+
merged,
|
|
16191
16938
|
inMainCheckout: entry?.path === root || holder?.path === root,
|
|
16192
16939
|
...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
|
|
16193
16940
|
};
|
|
@@ -16344,6 +17091,31 @@ const applyPrune = (store, plan) => {
|
|
|
16344
17091
|
}
|
|
16345
17092
|
return outcome;
|
|
16346
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
|
+
};
|
|
16347
17119
|
// ../core/src/store/decisions.ts
|
|
16348
17120
|
const choicesOf = (json) => {
|
|
16349
17121
|
const parsed = JSON.parse(json);
|
|
@@ -18394,11 +19166,17 @@ const syncFrom = async (worktree, from) => {
|
|
|
18394
19166
|
*/
|
|
18395
19167
|
/** An item's name: its branch, or its run and node when created detached. */
|
|
18396
19168
|
const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
|
|
18397
|
-
/**
|
|
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
|
+
*/
|
|
18398
19174
|
const mergeStateOf = (item) => {
|
|
18399
|
-
if (item.
|
|
18400
|
-
return item.
|
|
19175
|
+
if (item.merged) return "merged";
|
|
19176
|
+
return item.branch === void 0 && item.checkedOut === void 0 ? "detached" : "unmerged";
|
|
18401
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;
|
|
18402
19180
|
/** `3 commits`, `1 commit`. */
|
|
18403
19181
|
const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
18404
19182
|
/**
|
|
@@ -18419,12 +19197,11 @@ const stateParts = (item, into) => {
|
|
|
18419
19197
|
else if (item.missing) part("missing (prunable)");
|
|
18420
19198
|
if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
|
|
18421
19199
|
if (item.dirty) part("uncommitted changes", true);
|
|
18422
|
-
|
|
18423
|
-
if (state === "unmerged") {
|
|
19200
|
+
if (mergeStateOf(item) === "unmerged") {
|
|
18424
19201
|
const where = into === void 0 ? "" : ` into ${into}`;
|
|
18425
19202
|
part(`${counted(item.ahead, "commit")} not merged${where}`);
|
|
18426
19203
|
}
|
|
18427
|
-
if (
|
|
19204
|
+
if (isDetached(item)) {
|
|
18428
19205
|
if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
|
|
18429
19206
|
else if (item.path !== null && !item.missing) part("nothing committed");
|
|
18430
19207
|
}
|
|
@@ -18446,7 +19223,7 @@ const planWhat = (step) => {
|
|
|
18446
19223
|
const planLine = (step) => {
|
|
18447
19224
|
const { item } = step;
|
|
18448
19225
|
return {
|
|
18449
|
-
text: `${item
|
|
19226
|
+
text: `${isDetached(item) ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
|
|
18450
19227
|
...step.skip === void 0 ? {} : { reason: step.skip }
|
|
18451
19228
|
};
|
|
18452
19229
|
};
|
|
@@ -19053,4 +19830,4 @@ const createWorkspace = (options) => {
|
|
|
19053
19830
|
close
|
|
19054
19831
|
};
|
|
19055
19832
|
};
|
|
19056
|
-
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 };
|