@krapht/ork 1.0.0-alpha.17 → 1.0.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{__build_info__-C1Nm0hlJ.mjs → __build_info__-dPf4cEGL.mjs} +3 -134
  2. package/dist/{attach-EP374oMP.mjs → attach-BQl8nQz2.mjs} +5 -5
  3. package/dist/{attach-RQx__RzH.mjs → attach-DhZQ87Nz.mjs} +49 -224
  4. package/dist/bin.mjs +204 -154
  5. package/dist/{brief-Bs3cMQU0.mjs → brief-BQbeoPMc.mjs} +19 -9
  6. package/dist/{builtin-files-yYaNTQRU.mjs → builtin-files-CWL3JEo-.mjs} +1 -1
  7. package/dist/{command-tree-BkSYWmYJ.mjs → command-tree-B6uvMJq1.mjs} +1 -1
  8. package/dist/{completion-DIXWlnAl.mjs → completion-DKPuD_Rw.mjs} +47 -14
  9. package/dist/{context-Dq-VM_sq.mjs → context-Bw6R_EYq.mjs} +3 -3
  10. package/dist/detached-BQlbmqR0.mjs +38 -0
  11. package/dist/{find-k489laBa.mjs → find-B30-rd-u.mjs} +1 -1
  12. package/dist/flags-ByYBx-Xb.mjs +170 -0
  13. package/dist/{follow-ups-DD8NTPe6.mjs → follow-ups-Cxca1Jgg.mjs} +24 -18
  14. package/dist/{format-DQc9A_ei.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-D_9GlAlS.mjs → in-a-run-CZFzoT3R.mjs} +2 -2
  17. package/dist/{install-PVlV2QPq.mjs → install-CsqK1avz.mjs} +2 -183
  18. package/dist/{limits-CHqBO7wb.mjs → limits-CXh052II.mjs} +1 -1
  19. package/dist/{mcp-D38Ajb0b.mjs → mcp-B4wME9C4.mjs} +6 -6
  20. package/dist/{once-DuUzpzL2.mjs → once-BKn3pOtg.mjs} +3 -3
  21. package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
  22. package/dist/{pause-Bx3ePGrn.mjs → pause-B1grR1cl.mjs} +3 -3
  23. package/dist/{print-n2qetUHc.mjs → print-Bd8MWVJk.mjs} +1 -1
  24. package/dist/{project-records-Cune1l6I.mjs → project-records-BOEVs69i.mjs} +1 -1
  25. package/dist/{parse-BzWpMN-a.mjs → refusals-DQQX3AIb.mjs} +10 -1
  26. package/dist/{relay-D7RN4m68.mjs → relay-CeNiq3GT.mjs} +1 -1
  27. package/dist/{run-OOWQIKMJ.mjs → run-C0C54UiU.mjs} +116 -137
  28. package/dist/{run-BfxdsQJk.mjs → run-D6yU2rLf.mjs} +1 -1
  29. package/dist/{send-CKth_vgn.mjs → send-DqgF58hb.mjs} +2 -2
  30. package/dist/{src-DLsFsmOD.mjs → src-Ciy7i3se.mjs} +882 -71
  31. package/dist/{src-BHod38UQ.mjs → src-D2qpd-G7.mjs} +394 -247
  32. package/dist/{src-DmmrX2_C.mjs → src-DP02RP9O.mjs} +227 -180
  33. package/dist/{src-DOjTY2ET.mjs → src-n-u4TGev.mjs} +186 -43
  34. package/dist/{store-DACH768X.mjs → store-2KXzMCEN.mjs} +1 -1
  35. package/dist/ui/assets/app-C5vO1M-l.css +1 -0
  36. package/dist/ui/assets/{app-CN3acQ-1.js → app-ICEKpy_C.js} +21 -20
  37. package/dist/ui/assets/{index-B2uGxA4W.js → index-BGaix5wh.js} +2 -2
  38. package/dist/ui/index.html +1 -1
  39. package/dist/{ui-CJ-BX12n.mjs → ui-CAvygxTb.mjs} +53 -24
  40. package/dist/{usage-DxZ6JGd4.mjs → usage-BFBco5OS.mjs} +7 -7
  41. package/dist/{validate-BS0nCccW.mjs → validate-UrmC5AG1.mjs} +5 -5
  42. package/dist/{watch-Dp7IB4FR.mjs → watch-DTxkgB8R.mjs} +5 -5
  43. package/dist/{workflows-Djo1edlK.mjs → workflows-8GV-SeRm.mjs} +28 -4
  44. package/dist/{worktrees-Drof_Nyx.mjs → worktrees-CUdUvZlO.mjs} +9 -6
  45. package/package.json +1 -1
  46. package/dist/flags-B4ZZWspN.mjs +0 -125
  47. package/dist/post-2CaJz_mV.mjs +0 -46
  48. package/dist/ui/assets/app-9Q-62Ywz.css +0 -1
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { LineCounter, YAMLMap, YAMLSeq, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
3
3
  import { z } from "zod";
4
- import { appendFileSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
6
6
  import { constants as constants$1, homedir, setPriority, tmpdir } from "node:os";
7
7
  import { createConnection, createServer } from "node:net";
@@ -1268,6 +1268,13 @@ const nodeTypeMessage = (issue, value) => {
1268
1268
  const types = NODE_TYPES.map((type) => markValue(type)).join(", ");
1269
1269
  return value === void 0 ? `node needs a \`type\`: one of ${types}` : `unknown node type ${markValue(typeof value === "string" ? value : JSON.stringify(value) ?? "")}: expected one of ${types}`;
1270
1270
  };
1271
+ /**
1272
+ * The text for a file without `schemaVersion`, else `null`: Zod's own words
1273
+ * ("Invalid input: expected 1") don't say the key is missing.
1274
+ */
1275
+ const versionMessage = (issue, path, value) => issue.code === "invalid_value" && path.length === 1 && path[0] === "schemaVersion" && value === void 0 ? `schemaVersion is missing: write schemaVersion: ${String(issue.values[0])}` : null;
1276
+ /** The text Ork has for an issue instead of Zod's, else `null`. */
1277
+ const knownMessage = (issue, path, value) => forEachMessage(issue, path, value) ?? listRuleMessage(issue, value) ?? nodeTypeMessage(issue, value) ?? versionMessage(issue, path, value);
1271
1278
  // ../schema/src/newer-version.ts
1272
1279
  /**
1273
1280
  * A file with a higher `schemaVersion` than this Ork reads was made by a newer
@@ -1319,7 +1326,7 @@ const describeIssue = (issue, raw, path) => {
1319
1326
  };
1320
1327
  }
1321
1328
  const value = valueAt(raw, path);
1322
- const said = 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
@@ -3990,7 +4266,10 @@ const messageOf$3 = (line) => {
3990
4266
  };
3991
4267
  /** Listens on the run's socket; resolves once it is listening. */
3992
4268
  const serveRun = async (options) => {
3993
- mkdirSync(path.dirname(options.path), { recursive: true });
4269
+ mkdirSync(path.dirname(options.path), {
4270
+ recursive: true,
4271
+ mode: 448
4272
+ });
3994
4273
  rmSync(options.path, { force: true });
3995
4274
  const followers = /* @__PURE__ */ new Set();
3996
4275
  const server = createServer((socket) => {
@@ -4079,6 +4358,75 @@ const followSocket = (file, after, onEvent, signal) => {
4079
4358
  });
4080
4359
  return promise;
4081
4360
  };
4361
+ // ../core/src/control/control.ts
4362
+ const startControl = async (options) => {
4363
+ const { store, runId } = options;
4364
+ const waiting = /* @__PURE__ */ new Map();
4365
+ let stopped = false;
4366
+ const check = () => {
4367
+ for (const [id, resolve] of waiting) {
4368
+ const decision = store.decision(id);
4369
+ if (decision !== void 0 && decision.status !== "open") {
4370
+ waiting.delete(id);
4371
+ resolve(decision);
4372
+ }
4373
+ }
4374
+ for (const command of store.takeCommands(runId)) if (command.kind === "message") options.onMessage(command.node, command.body);
4375
+ else if (command.kind === "relay") options.onRelay?.(command.node, command.from, command.body);
4376
+ else if (command.kind === "script") options.onScript?.(command.node, command.from, command.body);
4377
+ else if (command.kind === "notice") options.onNotice?.(command.node, command.body);
4378
+ else if (command.kind === "stop" && command.node === void 0) {
4379
+ if (stopped) continue;
4380
+ stopped = true;
4381
+ options.emit({
4382
+ type: "run.command",
4383
+ command: "stop"
4384
+ });
4385
+ options.onStop();
4386
+ } else if (!stopped) {
4387
+ if (options.onControl?.(command.kind, command.node) ?? false) options.emit({
4388
+ type: "run.command",
4389
+ command: command.kind,
4390
+ ...command.node === void 0 ? {} : { node: command.node }
4391
+ });
4392
+ }
4393
+ };
4394
+ const timer = setInterval(check, options.intervalMs ?? 1e3);
4395
+ let socket;
4396
+ try {
4397
+ socket = await serveRun({
4398
+ path: socketPath(runId, options.home),
4399
+ onWake: check,
4400
+ backlog: (after) => store.events(runId, after)
4401
+ });
4402
+ } catch {}
4403
+ return {
4404
+ human: createDecisionQueue({
4405
+ store,
4406
+ runId,
4407
+ emit: options.emit,
4408
+ waitFor: (id, signal) => {
4409
+ const { promise, resolve, reject } = Promise.withResolvers();
4410
+ waiting.set(id, resolve);
4411
+ signal.addEventListener("abort", () => {
4412
+ waiting.delete(id);
4413
+ reject(/* @__PURE__ */ new Error("settled"));
4414
+ }, { once: true });
4415
+ check();
4416
+ return promise;
4417
+ },
4418
+ ...options.local === void 0 ? {} : { local: options.local }
4419
+ }),
4420
+ publish: (stored) => {
4421
+ socket?.publish(stored);
4422
+ },
4423
+ check,
4424
+ close: async () => {
4425
+ clearInterval(timer);
4426
+ await socket?.close();
4427
+ }
4428
+ };
4429
+ };
4082
4430
  // ../core/src/expr/parse.ts
4083
4431
  /**
4084
4432
  * @file A small, safe expression language for `when:` conditions and `{{ }}`
@@ -4632,6 +4980,60 @@ const renderTemplate = (parts, scope, { readable = false } = {}) => {
4632
4980
  }
4633
4981
  return text;
4634
4982
  };
4983
+ // ../core/src/feedback/actions.ts
4984
+ const ALLOWED = { ok: true };
4985
+ const no = (why, error) => ({
4986
+ ok: false,
4987
+ why,
4988
+ error
4989
+ });
4990
+ /** A dismissed item allows nothing until it's restored. */
4991
+ const dismissed = (short) => no("dismissed; restore it first", `${short} is dismissed; restore it first.`);
4992
+ const already = (short, status) => no(`${status} already`, `${short} is ${status} already.`);
4993
+ /**
4994
+ * `fixRun` is a running `ork:feedback-apply` run for the item ({@link
4995
+ * openFixRun}): a second fix isn't started beside it.
4996
+ */
4997
+ const feedbackActions = (item, fixRun) => {
4998
+ const short = item.id.slice(0, 12);
4999
+ const isOrk = item.target === "ork";
5000
+ if (item.status === "dismissed") {
5001
+ const check = dismissed(short);
5002
+ return {
5003
+ accept: check,
5004
+ lesson: check,
5005
+ apply: check,
5006
+ publish: check
5007
+ };
5008
+ }
5009
+ const settled = item.status === "resolved" || item.status === "published";
5010
+ const accept = item.status === "accepted" || settled ? already(short, item.status) : ALLOWED;
5011
+ let lesson = ALLOWED;
5012
+ if (isOrk) lesson = no("about Ork itself", `${short} is about Ork itself, so it isn't a lesson for a workflow; share it with Ork's maintainers instead.`);
5013
+ else if (item.workflows.length === 0) lesson = no("no workflow recorded, so no lessons file", `No workflow is recorded for ${short}, so it has no lessons file.`);
5014
+ let apply = ALLOWED;
5015
+ if (isOrk) apply = no("about Ork itself", `${short} is about Ork itself, so its fix isn't in this project; share it with Ork's maintainers instead.`);
5016
+ else if (settled) apply = already(short, item.status);
5017
+ else if (fixRun !== void 0) apply = no(`run ${fixRun} is proposing a fix`, `Run ${fixRun} is proposing a fix for ${short} already: answer its question in \`ork decide\` or the UI, or stop it first.`);
5018
+ let publish = ALLOWED;
5019
+ if (!isOrk) publish = no("only feedback about Ork is shared", `${short} is about this ${item.target}, not Ork itself: only feedback about Ork is shared, and the rest stays on your machine.`);
5020
+ else if (item.status === "published") publish = already(short, item.status);
5021
+ return {
5022
+ accept,
5023
+ lesson,
5024
+ apply,
5025
+ publish
5026
+ };
5027
+ };
5028
+ /** The running `ork:feedback-apply` run proposing a fix for `item`, if any. */
5029
+ const openFixRun = (store, item) => {
5030
+ const short = item.id.slice(0, 12);
5031
+ return store.runs().filter((run) => run.workflow === "ork-feedback-apply" && run.status === "running").find((run) => {
5032
+ const [first] = store.events(run.id);
5033
+ const event = first?.event;
5034
+ return event?.type === "run.started" && event.inputs?.["item"] === short;
5035
+ })?.id;
5036
+ };
4635
5037
  // ../core/src/feedback/fingerprint.ts
4636
5038
  /**
4637
5039
  * @file Feedback fingerprints: records about the same thing (same target, kind,
@@ -4654,16 +5056,17 @@ const defaultTarget = (kind) => {
4654
5056
  };
4655
5057
  /** The draft's target, or its kind's default. */
4656
5058
  const targetOf = (draft) => draft.target ?? defaultTarget(draft.kind);
4657
- const MAX_TITLE = 120;
5059
+ const MAX_TITLE$1 = 120;
4658
5060
  const MAX_BODY = 2e3;
4659
5061
  const MAX_EVIDENCE = 10;
4660
- const createFeedback = ({ graph, onEvent }) => {
5062
+ const createFeedback = ({ graph, onEvent, scrub = (text) => text }) => {
4661
5063
  const seen = /* @__PURE__ */ new Set();
5064
+ const clean = (text) => scrub(maskSecrets(text));
4662
5065
  return { port: (id, template) => {
4663
5066
  let left = 3;
4664
5067
  const nodeType = graph.nodes.get(template ?? id)?.node.type;
4665
5068
  return { report: (draft) => {
4666
- const title = maskSecrets(draft.title.trim()).slice(0, MAX_TITLE);
5069
+ const title = clean(draft.title.trim()).slice(0, MAX_TITLE$1);
4667
5070
  if (title === "") return {
4668
5071
  ok: false,
4669
5072
  error: "the title is empty"
@@ -4693,8 +5096,8 @@ const createFeedback = ({ graph, onEvent }) => {
4693
5096
  target,
4694
5097
  severity: draft.severity ?? "medium",
4695
5098
  title,
4696
- body: maskSecrets(draft.body?.trim() ?? "").slice(0, MAX_BODY),
4697
- 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)),
4698
5101
  fingerprint: print,
4699
5102
  origin: nodeType === void 0 ? {} : { nodeType }
4700
5103
  }
@@ -4958,6 +5361,136 @@ const FEEDBACK_SEVERITIES = [
4958
5361
  "medium",
4959
5362
  "high"
4960
5363
  ];
5364
+ // ../core/src/feedback/human.ts
5365
+ const RATINGS = ["good", "bad"];
5366
+ const MAX_TITLE = 120;
5367
+ /**
5368
+ * The run event a note becomes; its title is the note's first line. `scrub` (an
5369
+ * env-value scrubber) runs after `maskSecrets`.
5370
+ */
5371
+ const humanNoteEvent = (note, scrub = (text) => text) => {
5372
+ const text = scrub(maskSecrets(note.text.trim()));
5373
+ const [first = ""] = text.split("\n");
5374
+ const title = first.slice(0, MAX_TITLE);
5375
+ const kind = note.kind ?? "friction";
5376
+ const target = targetOf({
5377
+ kind,
5378
+ title,
5379
+ ...note.target === void 0 ? {} : { target: note.target }
5380
+ });
5381
+ const feedback = {
5382
+ source: "human",
5383
+ kind,
5384
+ target,
5385
+ severity: note.severity ?? (note.rating === "bad" ? "high" : "medium"),
5386
+ title,
5387
+ body: text,
5388
+ evidence: note.rating === void 0 ? [] : [`rating: ${note.rating}`],
5389
+ fingerprint: fingerprint({
5390
+ target,
5391
+ kind,
5392
+ title
5393
+ }),
5394
+ origin: { workflow: note.workflow }
5395
+ };
5396
+ return {
5397
+ type: "feedback.recorded",
5398
+ ...note.node === void 0 ? {} : { node: note.node },
5399
+ feedback
5400
+ };
5401
+ };
5402
+ /** `value` when it's one of `allowed`, or why not. */
5403
+ const checked = (name, value, allowed) => {
5404
+ if (value === void 0) return { ok: true };
5405
+ const found = allowed.find((a) => a === value);
5406
+ return found === void 0 ? {
5407
+ ok: false,
5408
+ error: `${name} is one of ${allowed.join(", ")}`
5409
+ } : {
5410
+ ok: true,
5411
+ value: found
5412
+ };
5413
+ };
5414
+ /**
5415
+ * Whether a note can be recorded, before anything else is done: its words,
5416
+ * kind, severity and rating. `flag` names the options as the caller does (`--`
5417
+ * for the CLI).
5418
+ */
5419
+ const checkNote = (ask, flag = "") => {
5420
+ if (ask.text.trim() === "") return {
5421
+ ok: false,
5422
+ error: "The note is empty."
5423
+ };
5424
+ const kind = checked(`${flag}kind`, ask.kind, FEEDBACK_KINDS);
5425
+ const severity = checked(`${flag}severity`, ask.severity, FEEDBACK_SEVERITIES);
5426
+ const rating = checked(`${flag}rating`, ask.rating, RATINGS);
5427
+ if (!kind.ok) return kind;
5428
+ if (!severity.ok) return severity;
5429
+ if (!rating.ok) return rating;
5430
+ return {
5431
+ ok: true,
5432
+ ...kind.value === void 0 ? {} : { kind: kind.value },
5433
+ ...severity.value === void 0 ? {} : { severity: severity.value },
5434
+ ...rating.value === void 0 ? {} : { rating: rating.value }
5435
+ };
5436
+ };
5437
+ /**
5438
+ * Records a person's note on `run`, or one node that ran in it, as that run's
5439
+ * feedback (a run that ended takes it too); returns the item it joins (the one
5440
+ * it was merged into, when triage merged it), or why it can't. `ork feedback
5441
+ * note`, `ork decide --note` and the UI write it.
5442
+ */
5443
+ const recordNote = (store, run, ask, flag = "", scrub) => {
5444
+ const note = checkNote(ask, flag);
5445
+ if (!note.ok) return note;
5446
+ const ran = store.events(run.id).some(({ event }) => "id" in event && event.id === ask.node);
5447
+ if (ask.node !== void 0 && !ran) return {
5448
+ ok: false,
5449
+ error: `No node ${ask.node} ran in run ${run.id}.`
5450
+ };
5451
+ const event = humanNoteEvent({
5452
+ text: ask.text,
5453
+ workflow: run.workflow,
5454
+ ...ask.node === void 0 ? {} : { node: ask.node },
5455
+ ...note.kind === void 0 ? {} : { kind: note.kind },
5456
+ ...note.severity === void 0 ? {} : { severity: note.severity },
5457
+ ...note.rating === void 0 ? {} : { rating: note.rating }
5458
+ }, scrub);
5459
+ store.append(run.id, event);
5460
+ const recorded = event.type === "feedback.recorded" ? event.feedback.fingerprint : "";
5461
+ return {
5462
+ ok: true,
5463
+ item: store.findFeedback(recorded)[0]?.id ?? recorded
5464
+ };
5465
+ };
5466
+ // ../core/src/feedback/preview.ts
5467
+ /**
5468
+ * @file The preview of an `ork` item as a post to Ork's Discussions (ADR-059),
5469
+ * for `ork feedback publish` and the Feedback panel. The projects its records
5470
+ * came from and every registered one are rewritten to `<project>`, and when
5471
+ * names are hidden their folders' and the workflows' names go too. What the
5472
+ * person confirms is what is sent.
5473
+ */
5474
+ /** The item as a post, or why it isn't shared. */
5475
+ const previewPost = (store, item, ask) => {
5476
+ const allowed = feedbackActions(item).publish;
5477
+ if (!allowed.ok) return {
5478
+ ok: false,
5479
+ error: allowed.error
5480
+ };
5481
+ const records = store.feedbackRecords(item.id);
5482
+ const runRoots = records.flatMap((r) => store.lookup(r.runId).find((run) => run.id === r.runId)?.project ?? []);
5483
+ const roots = [.../* @__PURE__ */ new Set([...runRoots, ...ask.registered.map((p) => p.root)])];
5484
+ const names = ask.hideNames ? [...item.workflows, ...roots.map((root) => path.basename(root))] : [];
5485
+ return {
5486
+ ok: true,
5487
+ post: publishPost(item, records, {
5488
+ roots,
5489
+ home: ask.home,
5490
+ names
5491
+ })
5492
+ };
5493
+ };
4961
5494
  // ../core/src/runtime/ends.ts
4962
5495
  /**
4963
5496
  * @file How a node ended, as one of a closed set of kinds (specs/runtime/
@@ -5313,6 +5846,137 @@ const createSignals = (options = {}) => {
5313
5846
  return signal === null ? [event] : [event, ...record(...signal)];
5314
5847
  };
5315
5848
  };
5849
+ // ../core/src/follow-ups/change.ts
5850
+ /** The project's follow-up whose id is, or starts with, `prefix`. */
5851
+ const findFollowUp = (rows, project, prefix) => {
5852
+ const exact = rows.followUp(project, prefix);
5853
+ if (exact !== void 0) return {
5854
+ ok: true,
5855
+ followUp: exact
5856
+ };
5857
+ const matches = prefix === "" ? [] : rows.followUps(project).filter((f) => f.id.startsWith(prefix));
5858
+ const [only, other] = matches;
5859
+ if (only === void 0) return {
5860
+ ok: false,
5861
+ error: `No follow-up \`${prefix}\` in this project.`
5862
+ };
5863
+ if (other !== void 0) return {
5864
+ ok: false,
5865
+ error: `\`${prefix}\` matches ${matches.map((f) => f.id).join(", ")}; give more of the id.`
5866
+ };
5867
+ return {
5868
+ ok: true,
5869
+ followUp: only
5870
+ };
5871
+ };
5872
+ /** Which statuses each action starts from, and where it goes. */
5873
+ const ACTIONS = {
5874
+ done: {
5875
+ from: ["waiting", "due"],
5876
+ to: () => "done",
5877
+ note: "done"
5878
+ },
5879
+ drop: {
5880
+ from: ["waiting", "due"],
5881
+ to: () => "dropped",
5882
+ note: "dropped"
5883
+ },
5884
+ reopen: {
5885
+ from: ["done", "dropped"],
5886
+ to: (f) => f.due === "now" ? "due" : "waiting",
5887
+ note: "reopened"
5888
+ },
5889
+ due: {
5890
+ from: ["waiting"],
5891
+ to: () => "due",
5892
+ note: "marked due"
5893
+ }
5894
+ };
5895
+ /** Why `action` can't be done to `f` now, or `undefined`. */
5896
+ const refusal$1 = (f, action) => {
5897
+ const rule = ACTIONS[action];
5898
+ return rule.from.includes(f.status) ? void 0 : `${f.id} is ${f.status}, so it can't be ${rule.note}.`;
5899
+ };
5900
+ const check = (f, action) => {
5901
+ const error = refusal$1(f, action);
5902
+ return error === void 0 ? { ok: true } : {
5903
+ ok: false,
5904
+ kind: "status",
5905
+ why: `it is ${f.status}`,
5906
+ error
5907
+ };
5908
+ };
5909
+ /** Whether `to` names one GitHub issue or pull request by its URL. */
5910
+ const isGithubIssueUrl = (to) => /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+$/u.test(to);
5911
+ /**
5912
+ * Why `f`'s draft can't be posted to GitHub whatever its status, or
5913
+ * `undefined`.
5914
+ */
5915
+ const githubDraftRefusal = (f) => {
5916
+ if (f.kind !== "message" || f.draft?.channel !== "github") return {
5917
+ why: "not drafted for the github channel",
5918
+ error: "only a message drafted for the github channel is posted; send the others yourself"
5919
+ };
5920
+ return isGithubIssueUrl(f.draft.to) ? void 0 : {
5921
+ why: "its `to` isn't a GitHub issue or pull request URL",
5922
+ error: `${f.id}'s \`to\` (${f.draft.to}) isn't a GitHub issue or pull request URL, so it is not posted; copy the text and send it yourself.`
5923
+ };
5924
+ };
5925
+ const postCheck = (f) => {
5926
+ const open = f.status === "waiting" || f.status === "due";
5927
+ const refused = githubDraftRefusal(f);
5928
+ if (refused !== void 0) return {
5929
+ ok: false,
5930
+ kind: "draft",
5931
+ ...refused
5932
+ };
5933
+ return open ? { ok: true } : {
5934
+ ok: false,
5935
+ kind: "status",
5936
+ why: `it is ${f.status}`,
5937
+ error: `${f.id} is ${f.status}, so it is not posted.`
5938
+ };
5939
+ };
5940
+ const followUpActions = (f) => ({
5941
+ done: check(f, "done"),
5942
+ drop: check(f, "drop"),
5943
+ reopen: check(f, "reopen"),
5944
+ due: check(f, "due"),
5945
+ post: postCheck(f)
5946
+ });
5947
+ /** Applies `action` to `followUp`, and keeps it. */
5948
+ const changeFollowUp = (rows, followUp, action, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
5949
+ const rule = ACTIONS[action];
5950
+ const refused = refusal$1(followUp, action);
5951
+ if (refused !== void 0) return {
5952
+ ok: false,
5953
+ error: refused
5954
+ };
5955
+ const { note: _old, ...rest } = followUp;
5956
+ const changed = {
5957
+ ...rest,
5958
+ status: rule.to(followUp),
5959
+ note: rule.note,
5960
+ updatedAt: now()
5961
+ };
5962
+ rows.putFollowUp(changed);
5963
+ return {
5964
+ ok: true,
5965
+ followUp: changed
5966
+ };
5967
+ };
5968
+ /**
5969
+ * Why a due follow-up is due, read from the note `due.ts` left: its ref merged,
5970
+ * its date came, its ref closed unmerged, or a person marked it. `undefined`
5971
+ * for one that is not due, or was due from the start.
5972
+ */
5973
+ const dueCause = (f) => {
5974
+ if (f.status !== "due" || f.note === void 0) return void 0;
5975
+ if (f.note.includes("closed without merging")) return "closed-unmerged";
5976
+ if (f.note.endsWith(" merged")) return "merged";
5977
+ if (f.note.startsWith("due on ")) return "overdue";
5978
+ return f.note === "marked due" ? "marked" : void 0;
5979
+ };
5316
5980
  // ../core/src/project/heavy.ts
5317
5981
  /**
5318
5982
  * @file A project's rules for the machine its runs share: the heavy commands
@@ -5351,6 +6015,16 @@ const matchesPattern = (pattern, command) => {
5351
6015
  };
5352
6016
  /** Whether `command` is one of the project's heavy commands. */
5353
6017
  const isHeavy = (heavy, command) => heavy.some((pattern) => matchesPattern(pattern, command));
6018
+ /**
6019
+ * The commands of a shell script, as far as a heavy check can tell without a
6020
+ * shell: comments dropped, split at newlines, `;`, `&&`, `||`, `|` and `&` (not
6021
+ * the `&` of a redirect such as `2>&1`), each trimmed, without a leading
6022
+ * `VAR=value` or `then`/`do`/`else`. shortcut: it splits inside quotes too and
6023
+ * cannot see `eval`, variables, heredocs or called scripts (specs/gaps).
6024
+ */
6025
+ const scriptCommands = (script) => script.split("\n").map((line) => line.replace(/(?:^|\s)#.*$/u, "")).flatMap((line) => line.split(/&&|\|\||;|\||(?<![<>])&(?!>)/u)).map((part) => part.trim().replace(/^(?:(?:then|do|else)(?:\s+|$))?(?:\w+=\S*\s+)*/u, "")).filter((part) => part !== "");
6026
+ /** Whether any command of a shell script is one of the project's heavy ones. */
6027
+ const isHeavyScript = (heavy, script) => heavy.length > 0 && scriptCommands(script).some((command) => isHeavy(heavy, command));
5354
6028
  const machineRulesOf = (project) => ({
5355
6029
  heavy: project?.heavy ?? [],
5356
6030
  priority: project?.priority ?? "low"
@@ -6308,18 +6982,30 @@ const sameFiles = (a, b) => a.length === b.length && a.toSorted().join("\n") ===
6308
6982
  * title.
6309
6983
  */
6310
6984
  const sameFollowUp = (kept, input) => (kept.status === "waiting" || kept.status === "due") && kept.for === input.for && sameFiles(kept.where, input.where) && nearTitle(kept.title, input.title);
6311
- /** `input` with what shouldn't be kept or leave the machine taken out. */
6312
- const scrubFollowUp = (input, options) => {
6313
- const scrub = (text) => redact(text, options);
6985
+ /** Who a follow-up is for, its name scrubbed and its kind kept. */
6986
+ const scrubFor = (to, scrub) => {
6987
+ if (to.startsWith("project:")) return `project:${scrub(to.slice(8))}`;
6988
+ if (to.startsWith("person:")) return `person:${scrub(to.slice(7))}`;
6989
+ return to;
6990
+ };
6991
+ /**
6992
+ * `input` with what shouldn't be kept or leave the machine taken out: each text
6993
+ * passed through `scrub` (the store's redactor, or a run's env-value
6994
+ * scrubber).
6995
+ */
6996
+ const scrubFollowUp = (input, scrub) => {
6314
6997
  return {
6315
6998
  ...input,
6999
+ for: scrubFor(input.for, scrub),
6316
7000
  title: scrub(input.title),
7001
+ ...input.ref === void 0 ? {} : { ref: scrub(input.ref) },
6317
7002
  ...input.body === void 0 ? {} : { body: scrub(input.body) },
6318
7003
  ...input.where === void 0 ? {} : { where: input.where.map(scrub) },
6319
7004
  ...input.draft === void 0 ? {} : { draft: {
6320
7005
  ...input.draft,
6321
7006
  to: scrub(input.draft.to),
6322
- text: scrub(input.draft.text)
7007
+ text: scrub(input.draft.text),
7008
+ ...input.draft.channel === void 0 ? {} : { channel: scrub(input.draft.channel) }
6323
7009
  } },
6324
7010
  ...input.steps === void 0 ? {} : { steps: input.steps.map((step) => ({
6325
7011
  text: scrub(step.text),
@@ -6354,10 +7040,10 @@ const followUpStore = (options) => {
6354
7040
  return {
6355
7041
  place: adapter.place,
6356
7042
  add: async (raw) => {
6357
- const input = scrubFollowUp(raw, {
7043
+ const input = scrubFollowUp(raw, (text) => redact(text, {
6358
7044
  roots: [project, worktree],
6359
7045
  home
6360
- });
7046
+ }));
6361
7047
  const where = input.where ?? [];
6362
7048
  const at = now();
6363
7049
  const kept = list().find((f) => sameFollowUp(f, {
@@ -6554,7 +7240,8 @@ const deliverFollowUp = async (input, options) => {
6554
7240
  * too, with a note saying so, never dropped. A dated one is due on its date;
6555
7241
  * `after-deploy` only when someone says so (`markDue`). A run that publishes
6556
7242
  * records its ref on the follow-ups it left (`recordPublished`), and the
6557
- * `agent` follow-ups it took as its brief are done.
7243
+ * `agent` follow-ups it took as its brief are done (taking them is
7244
+ * `take.ts`).
6558
7245
  */
6559
7246
  const DATE = /^\d{4}-\d{2}-\d{2}$/u;
6560
7247
  const why = (result) => result.stderr.trim() || result.stdout.trim() || "no output";
@@ -6700,25 +7387,6 @@ const recordPublished = (rows, published, now = () => (/* @__PURE__ */ new Date(
6700
7387
  };
6701
7388
  };
6702
7389
  /**
6703
- * `ork run start --follow-ups <ids>`: the open `agent` follow-ups among `ids`
6704
- * become `run`'s brief; why each other id wasn't taken.
6705
- */
6706
- const takeFollowUps = (rows, taking, now = () => (/* @__PURE__ */ new Date()).toISOString()) => {
6707
- const taken = [];
6708
- const refused = [];
6709
- for (const id of taking.ids) {
6710
- const f = rows.followUp(taking.project, id);
6711
- if (f === void 0) refused.push(`${id}: there is no such follow-up`);
6712
- else if (f.for !== "agent") refused.push(`${id}: it is for ${f.for}`);
6713
- else if (f.status === "done" || f.status === "dropped") refused.push(`${id}: it is ${f.status}`);
6714
- else taken.push(changed(rows, f, { takenBy: taking.run }, now()));
6715
- }
6716
- return {
6717
- taken,
6718
- refused
6719
- };
6720
- };
6721
- /**
6722
7390
  * Checks the projects' follow-ups now and on a schedule, while `ork ui` or `ork
6723
7391
  * watch` runs; `onDue` hears of those that came due (a notification). Returns
6724
7392
  * the stop. A check still going when the next is due is not doubled.
@@ -6754,18 +7422,61 @@ const startFollowUpChecks = (options) => {
6754
7422
  clearInterval(timer);
6755
7423
  };
6756
7424
  };
6757
- 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 }) => ({
6758
7469
  /** One session's port. */
6759
7470
  port: (node, cwd) => {
6760
7471
  let left = 30;
6761
- return { add: async (input) => {
7472
+ return { add: async (raw) => {
6762
7473
  if (left === 0) return {
6763
7474
  ok: false,
6764
7475
  error: `you already left ${String(30)} follow-ups in this session; put the rest in your report`
6765
7476
  };
6766
7477
  left -= 1;
6767
7478
  const result = await sink({
6768
- ...input,
7479
+ ...scrub === void 0 ? raw : scrubFollowUp(raw, (text) => scrub(maskSecrets(text))),
6769
7480
  from: {
6770
7481
  run: runId,
6771
7482
  node
@@ -9364,8 +10075,9 @@ const machinePort = (options, runId, id, clock) => {
9364
10075
  const { rules, ledger } = machine;
9365
10076
  return {
9366
10077
  priority: rules.priority,
9367
- slot: async (command, signal) => {
9368
- 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;
9369
10081
  let waited = false;
9370
10082
  const release = await clock.waitFor(id, () => ledger.take({
9371
10083
  run: runId,
@@ -9415,11 +10127,11 @@ const slotClock = (base, machine) => {
9415
10127
  },
9416
10128
  machine: machine === void 0 ? void 0 : {
9417
10129
  ...machine,
9418
- slot: async (command, signal) => {
10130
+ slot: async (command, signal, run) => {
9419
10131
  waiting += 1;
9420
10132
  tell();
9421
10133
  try {
9422
- return await machine.slot(command, signal);
10134
+ return await machine.slot(command, signal, run);
9423
10135
  } finally {
9424
10136
  waiting -= 1;
9425
10137
  tell();
@@ -10414,7 +11126,8 @@ const jobOf = (id, task, template) => {
10414
11126
  brief: task.brief
10415
11127
  };
10416
11128
  if (template !== void 0) job.template = template;
10417
- const 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("");
10418
11131
  if (description !== void 0 && description !== "") job.description = description;
10419
11132
  return job;
10420
11133
  };
@@ -10436,8 +11149,17 @@ const createOwnership = (hooks) => {
10436
11149
  const leaderNode = owner?.node;
10437
11150
  const maxWorkers = leaderNode?.type === "agent.leader" ? leaderNode.maxWorkers ?? 0 : 0;
10438
11151
  const spawned = [];
10439
- /** 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. */
10440
11154
  const runJobs = async (jobs) => {
11155
+ try {
11156
+ return await runAccepted(jobs);
11157
+ } finally {
11158
+ for (const job of jobs) if ("brief" in job) reserved.delete(job.id);
11159
+ }
11160
+ };
11161
+ /** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
11162
+ const runAccepted = async (jobs) => {
10441
11163
  const accepted = jobs.filter((j) => "brief" in j);
10442
11164
  const notes = [];
10443
11165
  if (accepted.length > 0) {
@@ -10465,14 +11187,13 @@ const createOwnership = (hooks) => {
10465
11187
  }
10466
11188
  };
10467
11189
  const delegate = (tasks) => {
10468
- const seen = /* @__PURE__ */ new Set();
10469
11190
  const jobs = tasks.map((task) => {
10470
11191
  if (!children.some((c) => c.id === task.child)) {
10471
11192
  const names = children.map((c) => c.id).join(", ");
10472
11193
  return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
10473
11194
  }
10474
- if (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");
10475
- 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);
10476
11197
  return jobOf(task.child, task);
10477
11198
  });
10478
11199
  return runJobs(jobs);
@@ -10989,14 +11710,34 @@ const summaryOf = (summaries, id) => {
10989
11710
  summaries.set(id, created);
10990
11711
  return created;
10991
11712
  };
10992
- /** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
10993
- 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) => {
10994
11720
  const summaries = /* @__PURE__ */ new Map();
11721
+ /** Nodes with a result of this run's own: they ran, not kept. */
11722
+ const anew = /* @__PURE__ */ new Set();
10995
11723
  const summary = (id) => summaryOf(summaries, id);
10996
- for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
11724
+ for (const [id, costUsd] of replay?.earlier?.forks ?? []) summary(id).costUsd += costUsd;
11725
+ const record = (id, result, replayed) => {
11726
+ if (!replayed) anew.add(id);
11727
+ recordRun(summary(id), result);
11728
+ };
11729
+ const counted = () => {
11730
+ const nodes = [...summaries.values()];
11731
+ if (replay === void 0) return { nodes };
11732
+ return {
11733
+ nodes,
11734
+ kept: nodes.filter((n) => n.runs > 0 && !anew.has(n.id)).length
11735
+ };
11736
+ };
10997
11737
  return {
10998
- summaries,
10999
- summary
11738
+ summary,
11739
+ record,
11740
+ counted
11000
11741
  };
11001
11742
  };
11002
11743
  /** The tighter of the workflow's budget and what a caller left it, if any. */
@@ -11047,6 +11788,7 @@ const runResult = (run) => ({
11047
11788
  worktrees: run.worktrees,
11048
11789
  ...run.outputs === void 0 ? {} : { outputs: run.outputs },
11049
11790
  skipped: run.nodes.filter((node) => node.runs === 0).length,
11791
+ ...run.kept === void 0 ? {} : { kept: run.kept },
11050
11792
  failuresHandled: run.failuresHandled,
11051
11793
  ...unreadOf(run.nodes, run.unread)
11052
11794
  });
@@ -12142,14 +12884,16 @@ const createOwners = (graph) => {
12142
12884
  * the process (`feedback/agent.ts`) and follow-ups about the product
12143
12885
  * (`follow-ups/session.ts`), the latter only when the run keeps them.
12144
12886
  */
12145
- const createSessionReports = ({ graph, runId, sink, onEvent }) => {
12887
+ const createSessionReports = ({ graph, runId, sink, onEvent, scrub }) => {
12146
12888
  const feedback = createFeedback({
12147
12889
  graph,
12148
- onEvent
12890
+ onEvent,
12891
+ scrub
12149
12892
  });
12150
12893
  const followUps = sink === void 0 ? void 0 : createFollowUps({
12151
12894
  runId,
12152
- sink
12895
+ sink,
12896
+ scrub
12153
12897
  });
12154
12898
  return {
12155
12899
  /** One session's ports; `template` is a spawned worker's node. */
@@ -12243,7 +12987,8 @@ const createRunPorts = ({ options, runId, slots, scope, stopping }) => {
12243
12987
  graph,
12244
12988
  runId,
12245
12989
  sink: options.followUps,
12246
- onEvent
12990
+ onEvent,
12991
+ scrub
12247
12992
  });
12248
12993
  const send = bus.rule;
12249
12994
  const rules = createRules({
@@ -12381,7 +13126,7 @@ const startRun = (options) => {
12381
13126
  const sessionIds = /* @__PURE__ */ new Map();
12382
13127
  const spend = spendOf(limits.budgetUsd, options, limits.budgetTokens);
12383
13128
  const running = /* @__PURE__ */ new Set();
12384
- const { summaries, summary } = createSummaries(options.replay?.earlier);
13129
+ const { summary, record, counted } = createSummaries(options.replay);
12385
13130
  const queue = [];
12386
13131
  const reasons = [];
12387
13132
  const stops = createStops(abort.signal);
@@ -12472,7 +13217,7 @@ const startRun = (options) => {
12472
13217
  runId,
12473
13218
  stopped,
12474
13219
  ...spend.sums(),
12475
- nodes: [...summaries.values()],
13220
+ ...counted(),
12476
13221
  reasons,
12477
13222
  worktrees,
12478
13223
  outputs: renderOutputs(graph.workflow.outputs, scope()),
@@ -12560,7 +13305,7 @@ const startRun = (options) => {
12560
13305
  clock.leave(id);
12561
13306
  outputs[id] = result.output;
12562
13307
  if (result.sessionId !== void 0) sessionIds.set(id, result.sessionId);
12563
- recordRun(summary(id), result);
13308
+ record(id, result, recorded !== null);
12564
13309
  if (recorded === null) {
12565
13310
  options.replay?.ran(result);
12566
13311
  onEvent(finishedEvent(id, round, result));
@@ -13614,6 +14359,13 @@ const STANDARD = /* @__PURE__ */ new Set([
13614
14359
  "artifacts",
13615
14360
  "branch"
13616
14361
  ]);
14362
+ /**
14363
+ * A name git takes as a branch (`git check-ref-format --branch`'s rules), and
14364
+ * nothing a command would read as an option or a refspec: no leading `-`, no
14365
+ * `:` or `+`, no space or control character. A reported branch reaches tool
14366
+ * arguments, so one that isn't a plain branch name is dropped.
14367
+ */
14368
+ const isBranchName = (name) => name !== "" && name !== "@" && !name.startsWith("-") && !name.startsWith("/") && !name.endsWith("/") && !name.endsWith(".") && !name.endsWith(".lock") && !name.includes("..") && !name.includes("//") && !name.includes("@{") && !name.includes("/.") && !name.startsWith(".") && !/[\u0000-\u0020\u007F~^:?*[\\+]/u.test(name);
13617
14369
  /** Reads a driver's structured output as a report, or `null` if it is not one. */
13618
14370
  const parseReport = (value) => {
13619
14371
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
@@ -13630,7 +14382,7 @@ const parseReport = (value) => {
13630
14382
  artifacts: paths,
13631
14383
  fields: Object.fromEntries(Object.entries(record).filter(([key]) => !STANDARD.has(key)))
13632
14384
  };
13633
- if (typeof branch === "string" && branch !== "") report.branch = branch;
14385
+ if (typeof branch === "string" && isBranchName(branch)) report.branch = branch;
13634
14386
  return report;
13635
14387
  };
13636
14388
  // ../core/src/run/brief.ts
@@ -16141,6 +16893,19 @@ const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.pa
16141
16893
  "--tags",
16142
16894
  "--remotes"
16143
16895
  ])).stdout;
16896
+ /**
16897
+ * Whether a detached worktree's HEAD is already in the current branch's
16898
+ * history.
16899
+ */
16900
+ const headIsMerged = async (root, path) => {
16901
+ const head = await git(path, ["rev-parse", "HEAD"]);
16902
+ return head.code === 0 && (await git(root, [
16903
+ "merge-base",
16904
+ "--is-ancestor",
16905
+ head.stdout.trim(),
16906
+ "HEAD"
16907
+ ])).code === 0;
16908
+ };
16144
16909
  /** One created worktree or branch, with its state read from git. */
16145
16910
  const stateOf = async (root, entries, item, recorded) => {
16146
16911
  const { branch } = item;
@@ -16157,6 +16922,7 @@ const stateOf = async (root, entries, item, recorded) => {
16157
16922
  ])).stdout;
16158
16923
  const ahead = Number(counted);
16159
16924
  const changes = lines(status).length;
16925
+ const merged = on === void 0 ? entry !== void 0 && !missing && await headIsMerged(root, entry.path) : ahead === 0;
16160
16926
  return {
16161
16927
  ...branch === void 0 ? {} : { branch },
16162
16928
  ...checkedOut === void 0 ? {} : { checkedOut },
@@ -16168,7 +16934,7 @@ const stateOf = async (root, entries, item, recorded) => {
16168
16934
  dirty: changes > 0,
16169
16935
  changes,
16170
16936
  ahead: Number.isNaN(ahead) ? 0 : ahead,
16171
- merged: on !== void 0 && ahead === 0,
16937
+ merged,
16172
16938
  inMainCheckout: entry?.path === root || holder?.path === root,
16173
16939
  ...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
16174
16940
  };
@@ -16325,6 +17091,31 @@ const applyPrune = (store, plan) => {
16325
17091
  }
16326
17092
  return outcome;
16327
17093
  };
17094
+ // ../core/src/store/recorder.ts
17095
+ /**
17096
+ * @file Records a run as it happens: each event appended to the store and
17097
+ * copied to the run's `events.jsonl`, both with secrets masked, before
17098
+ * anything else sees it; and adds to a finished run (`ork chat`).
17099
+ */
17100
+ const EVENTS_FILE = "events.jsonl";
17101
+ /**
17102
+ * Appends to a run already in the store, e.g. `ork chat` resuming a finished
17103
+ * node's session; `runDir` gets the JSONL copy.
17104
+ */
17105
+ const appendRecorder = (store, runId, runDir) => {
17106
+ mkdirSync(runDir, { recursive: true });
17107
+ const file = path.join(runDir, EVENTS_FILE);
17108
+ return (event) => {
17109
+ const stored = store.append(runId, event);
17110
+ appendFileSync(file, `${JSON.stringify(stored, maskReplacer)}\n`);
17111
+ return stored;
17112
+ };
17113
+ };
17114
+ /** Begins `run` in the store; `runDir` gets the JSONL copy. */
17115
+ const createRecorder = (store, run, runDir) => {
17116
+ store.begin(run);
17117
+ return appendRecorder(store, run.id, runDir);
17118
+ };
16328
17119
  // ../core/src/store/decisions.ts
16329
17120
  const choicesOf = (json) => {
16330
17121
  const parsed = JSON.parse(json);
@@ -17210,10 +18001,25 @@ const openReadOnly = (file) => {
17210
18001
  throw error;
17211
18002
  }
17212
18003
  };
18004
+ /**
18005
+ * Keeps Ork's folder for the person alone: `~/.ork` holds every run's prompts
18006
+ * and output, its sockets and its locks, so it is made 0700, and one an older
18007
+ * Ork made with the umask's mode is narrowed. A store elsewhere (a test's) only
18008
+ * gets its folder made.
18009
+ */
18010
+ const keepPrivate = (dir) => {
18011
+ mkdirSync(dir, {
18012
+ recursive: true,
18013
+ mode: 448
18014
+ });
18015
+ if (path.basename(dir) !== ".ork") return;
18016
+ if ((statSync(dir).mode & 63) !== 0) chmodSync(dir, 448);
18017
+ };
17213
18018
  /** A database opened for writing: created, in WAL mode, migrated. */
17214
18019
  const openWritable = (file) => {
17215
- if (file !== ":memory:") mkdirSync(path.dirname(file), { recursive: true });
18020
+ if (file !== ":memory:") keepPrivate(path.dirname(file));
17216
18021
  const db = new DatabaseSync(file);
18022
+ if (file !== ":memory:") chmodSync(file, 384);
17217
18023
  db.exec("PRAGMA busy_timeout = 5000");
17218
18024
  db.exec("PRAGMA journal_mode = WAL");
17219
18025
  db.exec("PRAGMA synchronous = NORMAL");
@@ -18360,11 +19166,17 @@ const syncFrom = async (worktree, from) => {
18360
19166
  */
18361
19167
  /** An item's name: its branch, or its run and node when created detached. */
18362
19168
  const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
18363
- /** 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
+ */
18364
19174
  const mergeStateOf = (item) => {
18365
- if (item.branch === void 0 && item.checkedOut === void 0) return "detached";
18366
- return item.merged ? "merged" : "unmerged";
19175
+ if (item.merged) return "merged";
19176
+ return item.branch === void 0 && item.checkedOut === void 0 ? "detached" : "unmerged";
18367
19177
  };
19178
+ /** On no branch at all: created detached and not on a branch since. */
19179
+ const isDetached = (item) => item.branch === void 0 && item.checkedOut === void 0;
18368
19180
  /** `3 commits`, `1 commit`. */
18369
19181
  const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
18370
19182
  /**
@@ -18385,12 +19197,11 @@ const stateParts = (item, into) => {
18385
19197
  else if (item.missing) part("missing (prunable)");
18386
19198
  if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
18387
19199
  if (item.dirty) part("uncommitted changes", true);
18388
- const state = mergeStateOf(item);
18389
- if (state === "unmerged") {
19200
+ if (mergeStateOf(item) === "unmerged") {
18390
19201
  const where = into === void 0 ? "" : ` into ${into}`;
18391
19202
  part(`${counted(item.ahead, "commit")} not merged${where}`);
18392
19203
  }
18393
- if (state === "detached") {
19204
+ if (isDetached(item)) {
18394
19205
  if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
18395
19206
  else if (item.path !== null && !item.missing) part("nothing committed");
18396
19207
  }
@@ -18412,7 +19223,7 @@ const planWhat = (step) => {
18412
19223
  const planLine = (step) => {
18413
19224
  const { item } = step;
18414
19225
  return {
18415
- text: `${item.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)}`,
18416
19227
  ...step.skip === void 0 ? {} : { reason: step.skip }
18417
19228
  };
18418
19229
  };
@@ -19019,4 +19830,4 @@ const createWorkspace = (options) => {
19019
19830
  close
19020
19831
  };
19021
19832
  };
19022
- export { 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 };