@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.
Files changed (48) hide show
  1. package/dist/{__build_info__-B8TNbFtN.mjs → __build_info__-dPf4cEGL.mjs} +3 -134
  2. package/dist/{attach-CU4Pk42S.mjs → attach-BQl8nQz2.mjs} +5 -5
  3. package/dist/{attach-qvfyaQXC.mjs → attach-DhZQ87Nz.mjs} +49 -224
  4. package/dist/bin.mjs +204 -154
  5. package/dist/{brief-Bh1FnAHv.mjs → brief-BQbeoPMc.mjs} +19 -9
  6. package/dist/{builtin-files-dnTP2GaX.mjs → builtin-files-CWL3JEo-.mjs} +1 -1
  7. package/dist/{command-tree-BkSYWmYJ.mjs → command-tree-B6uvMJq1.mjs} +1 -1
  8. package/dist/{completion-CzXUGSVn.mjs → completion-DKPuD_Rw.mjs} +47 -14
  9. package/dist/{context-b4_gPlv3.mjs → context-Bw6R_EYq.mjs} +3 -3
  10. package/dist/detached-BQlbmqR0.mjs +38 -0
  11. package/dist/{find-P-VV4B47.mjs → find-B30-rd-u.mjs} +1 -1
  12. package/dist/flags-ByYBx-Xb.mjs +170 -0
  13. package/dist/{follow-ups-B_c1DSpA.mjs → follow-ups-Cxca1Jgg.mjs} +24 -18
  14. package/dist/{format-BS1qzpkf.mjs → format-C-nEZv-P.mjs} +27 -11
  15. package/dist/{help-groups-hB6kY34d.mjs → help-groups-BpRgBSTh.mjs} +13 -7
  16. package/dist/{in-a-run-DKoMGTNE.mjs → in-a-run-CZFzoT3R.mjs} +2 -2
  17. package/dist/{install-B743Bwzz.mjs → install-CsqK1avz.mjs} +2 -183
  18. package/dist/{limits-DrsKCqaN.mjs → limits-CXh052II.mjs} +1 -1
  19. package/dist/{mcp-BMwekw38.mjs → mcp-B4wME9C4.mjs} +6 -6
  20. package/dist/{once-6L8gciJV.mjs → once-BKn3pOtg.mjs} +3 -3
  21. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  22. package/dist/{pause-8gnVgICR.mjs → pause-B1grR1cl.mjs} +3 -3
  23. package/dist/{print-DLe4MHn-.mjs → print-Bd8MWVJk.mjs} +1 -1
  24. package/dist/{project-records-CsILxwfI.mjs → project-records-BOEVs69i.mjs} +1 -1
  25. package/dist/{parse-BzWpMN-a.mjs → refusals-DQQX3AIb.mjs} +10 -1
  26. package/dist/{relay-Bvm9JEoN.mjs → relay-CeNiq3GT.mjs} +1 -1
  27. package/dist/{run-D4yuTmmJ.mjs → run-C0C54UiU.mjs} +116 -137
  28. package/dist/{run-y8jI7bAI.mjs → run-D6yU2rLf.mjs} +1 -1
  29. package/dist/{send-BV9eIxQ7.mjs → send-DqgF58hb.mjs} +2 -2
  30. package/dist/{src-Frwpx3WF.mjs → src-Ciy7i3se.mjs} +843 -66
  31. package/dist/{src-ygFMh2eP.mjs → src-D2qpd-G7.mjs} +4 -4
  32. package/dist/{src-CVeA4tAw.mjs → src-DP02RP9O.mjs} +227 -180
  33. package/dist/{src-DWAcd3Sy.mjs → src-n-u4TGev.mjs} +186 -43
  34. package/dist/{store-B0nGqnPq.mjs → store-2KXzMCEN.mjs} +1 -1
  35. package/dist/ui/assets/app-C5vO1M-l.css +1 -0
  36. package/dist/ui/assets/{app-C4yxmYQR.js → app-ICEKpy_C.js} +21 -20
  37. package/dist/ui/assets/{index-CcbJWo5C.js → index-BGaix5wh.js} +2 -2
  38. package/dist/ui/index.html +1 -1
  39. package/dist/{ui-BZ9BhFw8.mjs → ui-CAvygxTb.mjs} +53 -24
  40. package/dist/{usage-BIs4j5Rw.mjs → usage-BFBco5OS.mjs} +7 -7
  41. package/dist/{validate-Xn2ZbJQC.mjs → validate-UrmC5AG1.mjs} +5 -5
  42. package/dist/{watch-9VM3DFTp.mjs → watch-DTxkgB8R.mjs} +5 -5
  43. package/dist/{workflows-CBYtycIy.mjs → workflows-8GV-SeRm.mjs} +28 -4
  44. package/dist/{worktrees-C7vfh7y5.mjs → worktrees-CUdUvZlO.mjs} +9 -6
  45. package/package.json +1 -1
  46. package/dist/flags-CqjJ44uW.mjs +0 -125
  47. package/dist/post-y1qL1Fnj.mjs +0 -46
  48. 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 = forEachMessage(issue, path, value) ?? listRuleMessage(issue, value) ?? nodeTypeMessage(issue, value);
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
- const PATTERNS = [
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 = maskSecrets(draft.title.trim()).slice(0, MAX_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: maskSecrets(draft.body?.trim() ?? "").slice(0, MAX_BODY),
4700
- evidence: (draft.evidence ?? []).slice(0, MAX_EVIDENCE).map((e) => maskSecrets(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
- /** `input` with what shouldn't be kept or leave the machine taken out. */
6321
- const scrubFollowUp = (input, options) => {
6322
- const scrub = (text) => redact(text, options);
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
- const createFollowUps = ({ runId, sink }) => ({
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 (input) => {
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
- ...input,
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
- if (ledger === void 0 || !isHeavy(rules.heavy, command)) return none;
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 description = task.description?.replaceAll(/[\s\p{Cc}]+/gu, " ").trim().slice(0, 80);
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
- /** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
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 (seen.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");
10487
- seen.add(task.child);
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
- /** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
11005
- const createSummaries = (earlier) => {
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
- summaries,
11011
- summary
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 { summaries, summary } = createSummaries(options.replay?.earlier);
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
- nodes: [...summaries.values()],
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
- recordRun(summary(id), result);
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: on !== void 0 && ahead === 0,
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
- /** Merged into the current branch, not, or detached on no branch at all. */
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.branch === void 0 && item.checkedOut === void 0) return "detached";
18400
- return item.merged ? "merged" : "unmerged";
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
- const state = mergeStateOf(item);
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 (state === "detached") {
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.branch === void 0 && item.checkedOut === void 0 ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
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 { sendFromScript as $, FEEDBACK_TARGETS as $n, beforeOf as $r, git as $t, declaredTriggers as A, knownExperiments as Ai, liveStatus as An, loadAgents as Ar, endedWith as At, planPrune as B, countedTokens as Bn, inside as Br, decisionListTable as Bt, compareVersions as C, AGENT_TOOL_LAYOUTS as Ci, endLinksOf as Cn, linkTools as Cr, startRun as Ct, triggersReport as D, firstLine as Di, requestPause as Dn, deleteAgent as Dr, runScope as Dt, lateTriggers as E, DRIVER_IDS as Ei, requestContinue as En, createAgent as Er, heldTimeout as Et, activeTree as F, templateOf$1 as Fn, placeFor as Fr, slotWaitLine as Ft, parseInputPairs as G, createSignals as Gn, plainPathInside as Gr, worktreeListTable as Gt, listOrkWorktrees as H, gitRulesOf as Hn, LESSON_CAP_TOKENS as Hr, followUpListTable as Ht, isNodeEvent as I, processAlive as In, projectsHome as Ir, lowerChild as It, driverRefusals as J, END_DETAIL_LIMIT as Jn, WorkflowEditSchema as Jr, keyOf as Jt, resolveInputs as K, endOf as Kn, writePlainFile as Kr, applyClean as Kt, openStore as L, stillTheSameProcess as Ln, runDirOf as Lr, programFound as Lt, workflowTextHash as M, pausedOf as Mn, inRepoPlace as Mr, stoppedEnd as Mt, takeWatchLock as N, runStatus as Nn, newProjectId as Nr, declaredProblem as Nt, createWatcher as O, EXPERIMENTS as Oi, requestStop as On, AGENT_SOURCES as Or, calledWorkflows as Ot, watchingPid as P, slotWaitsOf as Pn, placeFiles as Pr, slotClock as Pt, scriptRefusal as Q, FEEDBACK_SEVERITIES as Qn, renameNodeTypes as Qr, inspectProject as Qt, storePath as R, isItem as Rn, shownPath as Rr, startAt as Rt, detectInstaller as S, readyDriver as Si, addressOf as Sn, planMoveInto as Sr, walkSummary as St, parseVersion as T, AGENT_DRIVERS as Ti, unlinkNodes as Tn, toolsInUse as Tr, userMessageText as Tt, createReplay as U, loadProject as Un, estimateTokens as Ur, runListTable as Ut, createdWorktrees as V, PROJECT_FILE as Vn, projectRootOf as Vr, feedbackListTable as Vt, replayLoopAnswers as W, machineRulesOf as Wn, guardedBase as Wr, usageListTable as Wt, driverQueues as X, endFor as Xn, newWorkflowText as Xr, stepActs as Xt, sessionRefusals as Y, END_KINDS as Yn, applyEdits as Yr, planClean as Yt, TOO_LONG as Z, FEEDBACK_KINDS as Zn, readDocument as Zr, stepPrunes as Zt, capsHere as _, INTEGRATIONS as _i, takeFollowUps as _n, MASK as _r, planNodeSession as _t, planRemove as a, EdgeInputSchema as ai, sessionDriver as an, redact as ar, adoptedCalls as at, runningRunIds as b, PREFERRED_DRIVER as bi, liveSessionOf as bn, planAgentMigration as br, checkWorkflowText as bt, repoRoot as c, expandEdge as ci, MCP_FILE as cn, normalizeTitle as cr, DETACHED_ENV as ct, planLine as d, isClaudeNode as di, FOLLOW_UP_KINDS as dn, parseTemplate as dr, NOT_STARTED as dt, programsRunBy as ei, gitOk as en, DISCUSSIONS as er, createRelay as et, removeRefusal as f, isToolNode as fi, createFollowUps as fn, renderTemplate as fr, limitsNow as ft, usageReport as g, timeoutMs as gi, startFollowUpChecks as gn, socketRoom as gr, explainSession as gt, USAGE_DIMENSIONS as h, retiredDriverHint as hi, recordPublished as hn, socketPath as hr, explainWorkflowText as ht, lostWork as i, markValue as ii, namingNoDriver as in, publishPost as ir, createCalls as it, readProjectTriggers as j, lockWaitsOf as jn, hasInRepoFiles as jr, failedWith as jt, triggerStanding as k, experimentsOn as ki, tellLiveRun as kn, catalogAgent as kr, lockWaitLine as kt, labelOf as l, settingsOf as li, mcpNotText as ln, targetOf as lr, detachRun as lt, resolveBase as m, NodeIdSchema as mi, markDue as mn, serveRun as mr, planReason as mt, resumedOptions as n, parseWorkflow as ni, envReadsOf as nn, runGh as nr, answerDecision as nt, projectHead as o, WorkflowSchema as oi, nodeSays as on, createFeedback as or, detachResume as ot, stateParts as p, DriverSchema as pi, checkFollowUpsDue as pn, readCondition as pr, limitsReport as pt, budgetWarnings as q, outcomeEnd as qn, skillsIn as qr, cleanStep as qt, applyRemove as r, serializeWorkflow as ri, modelDriver as rn, sendPost as rr, pendingDecisions as rt, readWorktrees as s, CheckReportSchema as si, declaredOutputOf as sn, fingerprint as sr, resumable as st, createWorkspace as t, renderJsonSchema as ti, clearGitLocalEnv as tn, planPost as tr, followRunTree as tt, mergeStateOf as u, NodeSchema as ui, serversIn as un, evaluate as ur, followEvents as ut, planUpgrade as v, TOOL_PREFIX as vi, deliverFollowUp as vn, maskReplacer as vr, parseReport as vt, isNewer as w, ORK_LAYOUT as wi, linkNodes as wn, toolLinks as wr, MESSAGE_LIMIT as wt, runningScript as x, driverChecks as xi, DEFAULT_FOLLOW_UPS_RULE as xn, applySteps as xr, walkChecks as xt, releaseNotes as y, integrationFor as yi, followUpProblem as yn, maskSecrets as yr, reportSchemaFor as yt, applyPrune as z, nestItems as zn, workflowDirOf as zr, LIST_TABLES as zt };
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 };