@krapht/ork 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/__build_info__-CtSVVuZE.mjs +691 -0
  2. package/dist/bin.mjs +826 -526
  3. package/dist/clean-Dcr807be.mjs +88 -0
  4. package/dist/{context-BFGC_C02.mjs → context-C_bksb0C.mjs} +45 -129
  5. package/dist/{find-BcvhT6UN.mjs → find-DptmbhGd.mjs} +1 -1
  6. package/dist/{format-DqpObyMl.mjs → format-J-a21Yvn.mjs} +3 -163
  7. package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-D1Qc5Mit.mjs} +1 -1
  8. package/dist/{limits-BKYAmqCR.mjs → limits-BDZCmGEu.mjs} +1 -1
  9. package/dist/list-ZvY2OcLq.mjs +110 -0
  10. package/dist/{mcp-BWsdQyri.mjs → mcp-CHVKKg3B.mjs} +6 -6
  11. package/dist/{output-Q2EdeVUt.mjs → output-CXSJO00X.mjs} +68 -24
  12. package/dist/{pause-DKACDfV1.mjs → pause-BPSmg4yz.mjs} +4 -4
  13. package/dist/{relay-CbK-Wme9.mjs → relay-Do-aNbSD.mjs} +2 -2
  14. package/dist/remove-AwaMMaHf.mjs +194 -0
  15. package/dist/{send-C6yr04Fe.mjs → send-DhGBkowy.mjs} +3 -3
  16. package/dist/{src-CZOVF2yU.mjs → src-BHnxpJyH.mjs} +1 -1
  17. package/dist/{src-Cz1yOUTg.mjs → src-CGqMati3.mjs} +1411 -747
  18. package/dist/{src-D5ruZFgD.mjs → src-CpDKIulI.mjs} +1200 -55
  19. package/dist/{src-DtN1k-B3.mjs → src-UWkk1sl9.mjs} +1 -1
  20. package/dist/ui/assets/app-BgVDC_5l.js +67 -0
  21. package/dist/ui/assets/{app-C722xNZ3.css → app-DVf68fj5.css} +1 -1
  22. package/dist/ui/assets/{index-BoG6O-Lp.js → index-MQNNgsyT.js} +2 -2
  23. package/dist/ui/index.html +1 -1
  24. package/dist/ui-DhutUMgQ.mjs +472 -0
  25. package/dist/{usage-BIjf9Xdt.mjs → usage-CT7Z-yjr.mjs} +5 -4
  26. package/dist/{watch-B8o4Szd_.mjs → watch-0Qr2UfCQ.mjs} +6 -6
  27. package/dist/worktrees-DHl80k96.mjs +212 -0
  28. package/package.json +4 -4
  29. package/dist/__build_info__-CXXMLvjt.mjs +0 -104
  30. package/dist/ui/assets/app-BlDdP396.js +0 -61
  31. package/dist/ui-CZS1XKla.mjs +0 -160
@@ -0,0 +1,88 @@
1
+ import { E as removeWorktree, O as gitOk } from "./src-CGqMati3.mjs";
2
+ // ../core/src/worktree/clean.ts
3
+ /**
4
+ * @file What `ork worktrees clean` and `ork worktrees remove` do with what
5
+ * list.ts reads: the plan (what goes, what's kept and why), by one rule for
6
+ * every caller, and carrying it out. The CLI and the Worktrees panel both
7
+ * plan here, so neither removes what the other would keep.
8
+ */
9
+ /** What names an item for `remove`: its branch, or its worktree's path. */
10
+ const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
11
+ /** The reasons no `force` overrides: removing would break something. */
12
+ const MAIN_CHECKOUT = "checked out in the main checkout";
13
+ const RUN_GOING = "its run is still going";
14
+ const kept = (item, skip) => ({
15
+ item,
16
+ removeWorktree: false,
17
+ deleteBranch: false,
18
+ skip
19
+ });
20
+ /** What clean does with one item. */
21
+ const cleanStep = (item, options) => {
22
+ if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
23
+ if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
24
+ if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
25
+ if (item.dirty && !options.force) return kept(item, "uncommitted changes");
26
+ if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
27
+ const removable = item.path !== null && !item.missing;
28
+ if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
29
+ item,
30
+ removeWorktree: removable,
31
+ deleteBranch: false,
32
+ skip: "branch not merged"
33
+ };
34
+ return {
35
+ item,
36
+ removeWorktree: removable,
37
+ deleteBranch: options.branches && item.branch !== void 0
38
+ };
39
+ };
40
+ /** Whether a step drops a worktree whose folder is gone, by its own path. */
41
+ const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
42
+ /** Whether a step does anything. */
43
+ const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
44
+ /**
45
+ * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
46
+ * unmerged branch, or a detached worktree holding commits no branch has, is
47
+ * kept unless `force`; one checked out in the main checkout or in a worktree
48
+ * Ork didn't create, or whose run is still going, is kept even then.
49
+ */
50
+ const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
51
+ /**
52
+ * Carries out one step; what it did goes into `done` as it goes. It touches
53
+ * only its own item: a missing worktree is dropped by its own path (`git
54
+ * worktree remove`), never by `git worktree prune`, which would drop every
55
+ * missing worktree in the repository, the user's own included.
56
+ */
57
+ const applyStep = async (root, step, force, done) => {
58
+ const { item } = step;
59
+ if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
60
+ await gitOk(root, [
61
+ "worktree",
62
+ "remove",
63
+ item.path
64
+ ]);
65
+ done.push(`pruned ${item.path}`);
66
+ }
67
+ if (step.removeWorktree && item.path !== null) {
68
+ await removeWorktree(root, item.path, force);
69
+ done.push(`removed ${item.path}`);
70
+ }
71
+ if (step.deleteBranch && item.branch !== void 0) {
72
+ await gitOk(root, [
73
+ "branch",
74
+ force ? "-D" : "-d",
75
+ item.branch
76
+ ]);
77
+ done.push(`deleted ${item.branch}`);
78
+ }
79
+ };
80
+ /**
81
+ * Carries out a clean plan, step by step. What it did goes into `done` as it
82
+ * goes, so a caller still has it when git fails halfway.
83
+ */
84
+ const applyClean = async (root, steps, force, done = []) => {
85
+ for (const step of steps) await applyStep(root, step, force, done);
86
+ return done;
87
+ };
88
+ export { planClean as a, keyOf as i, applyStep as n, stepActs as o, cleanStep as r, stepPrunes as s, applyClean as t };
@@ -1,55 +1,9 @@
1
- import { E as planNodeSession, Qt as placeFor, Zt as placeFiles, an as practiceInstructions, ct as checkWorkflowText, en as runDirOf, g as workflowTextHash, h as readProjectTriggers, lt as walkChecks, nn as workflowDirOf, on as resolvePractices, rn as inside, ut as walkSummary } from "./src-Cz1yOUTg.mjs";
2
- import { a as write, c as openStore, i as style, l as storePath, r as plain } from "./output-Q2EdeVUt.mjs";
1
+ import { _n as inside, _t as walkChecks, b as readProjectTriggers, dn as placeFiles, fn as placeFor, gn as workflowDirOf, gt as checkWorkflowText, mn as runDirOf, ot as explainSession, vt as walkSummary, x as workflowTextHash, yn as LESSON_CAP_TOKENS } from "./src-CGqMati3.mjs";
2
+ import { b as builtinWorkflowFile, x as isBuiltinArg, y as builtinProblem } from "./__build_info__-CtSVVuZE.mjs";
3
+ import { a as write, c as openStore, i as style, l as storePath, o as writeErr, r as plain } from "./output-CXSJO00X.mjs";
3
4
  import { defineCommand } from "citty";
4
5
  import path from "node:path";
5
6
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
- // ../core/src/update/version.ts
7
- const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
8
- /** `1.2.3`, `v1.2.3-beta.1` and the like; `null` for anything else. */
9
- const parseVersion = (text) => {
10
- const match = VERSION.exec(text.trim());
11
- if (match === null) return null;
12
- const [, major = "0", minor = "0", patch = "0", pre] = match;
13
- return {
14
- core: [
15
- Number(major),
16
- Number(minor),
17
- Number(patch)
18
- ],
19
- pre: pre === void 0 ? [] : pre.split(".")
20
- };
21
- };
22
- const NUMERIC = /^\d+$/u;
23
- /** One pre-release identifier against another: numbers sort below words. */
24
- const compareIdentifier = (a, b) => {
25
- const an = NUMERIC.test(a);
26
- const bn = NUMERIC.test(b);
27
- if (an && bn) return Number(a) - Number(b);
28
- if (an !== bn) return an ? -1 : 1;
29
- return a < b ? -1 : Number(a > b);
30
- };
31
- const comparePre = (a, b) => {
32
- if (a.length === 0 || b.length === 0) return b.length - a.length;
33
- for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
34
- const order = compareIdentifier(a[i] ?? "", b[i] ?? "");
35
- if (order !== 0) return order;
36
- }
37
- return a.length - b.length;
38
- };
39
- /** Semver precedence: negative when `a` is older, 0 when equal. */
40
- const compareVersions = (a, b) => {
41
- for (let i = 0; i < 3; i += 1) {
42
- const order = (a.core[i] ?? 0) - (b.core[i] ?? 0);
43
- if (order !== 0) return order;
44
- }
45
- return comparePre(a.pre, b.pre);
46
- };
47
- /** Whether `candidate` is newer than `current`; false when either is invalid. */
48
- const isNewer = (candidate, current) => {
49
- const a = parseVersion(candidate);
50
- const b = parseVersion(current);
51
- return a !== null && b !== null && compareVersions(a, b) > 0;
52
- };
53
7
  // src/project/place.ts
54
8
  /**
55
9
  * @file Which project a folder belongs to, from the projects the store has
@@ -74,8 +28,9 @@ const fromCwd = (target, cwd) => inside(cwd, target) ? path.relative(cwd, target
74
28
  // src/workflows.ts
75
29
  /**
76
30
  * @file Finding workflow files: the project's workflows folder by default
77
- * (`.ork/workflows/`, or an attached project's), directories expanded, and a
78
- * workflow given by name.
31
+ * (`.ork/workflows/`, or an attached project's), directories expanded, a
32
+ * workflow given by name, and one of Ork's own by its `ork:` name, written
33
+ * into the project's run folder.
79
34
  */
80
35
  const WORKFLOW_DIR = path.join(".ork", "workflows");
81
36
  const WORKFLOW_SUFFIX = ".ork.yaml";
@@ -93,16 +48,32 @@ const findWorkflowFiles = (paths, cwd, workflows = path.join(cwd, WORKFLOW_DIR))
93
48
  });
94
49
  };
95
50
  /**
96
- * A workflow given by path, or by name for one in the project's workflows
97
- * folder: `hello` is `.ork/workflows/hello.ork.yaml` when no file `hello`
98
- * exists.
51
+ * A workflow given by path, by name for one in the project's workflows folder
52
+ * (`hello` is `.ork/workflows/hello.ork.yaml` when no file `hello` exists), or
53
+ * by its `ork:` name for one of Ork's own. An unknown `ork:` name comes back as
54
+ * it is: commands refuse it first (`unknownBuiltin`).
99
55
  */
100
56
  const resolveWorkflowArg = (arg, cwd, workflows = path.join(cwd, WORKFLOW_DIR)) => {
57
+ if (isBuiltinArg(arg)) {
58
+ const file = builtinWorkflowFile(projectAt(cwd), arg);
59
+ return file === void 0 ? arg : fromCwd(file, cwd);
60
+ }
101
61
  if (existsSync(path.resolve(cwd, arg)) || arg.endsWith(".ork.yaml")) return arg;
102
62
  const named = path.join(workflows, `${arg}${WORKFLOW_SUFFIX}`);
103
63
  return existsSync(named) ? fromCwd(named, cwd) : arg;
104
64
  };
105
65
  /**
66
+ * Refuses an `ork:` name that isn't one of Ork's workflows, saying which are,
67
+ * or whose file's place in the project is a link; true when it did, so the
68
+ * command stops.
69
+ */
70
+ const refusedBuiltin = (args, cwd) => {
71
+ const problems = args.filter((arg) => isBuiltinArg(arg)).flatMap((arg) => builtinProblem(projectAt(cwd), arg) ?? []);
72
+ for (const problem of problems) writeErr(problem);
73
+ if (problems.length > 0) process.exitCode = 1;
74
+ return problems.length > 0;
75
+ };
76
+ /**
106
77
  * A command's workflow argument, by path or by name in the project `cwd` is in,
107
78
  * with the project the file belongs to and a run's folder there.
108
79
  */
@@ -251,6 +222,7 @@ const validate = defineCommand({
251
222
  },
252
223
  run: async ({ args }) => {
253
224
  const cwd = process.cwd();
225
+ if (refusedBuiltin(args._, cwd)) return;
254
226
  const files = findWorkflowFiles(args._, cwd, placeFiles(projectAt(cwd)).workflows);
255
227
  if (files.length === 0) {
256
228
  write("No workflow files found. Run `ork init`, or pass a path.");
@@ -279,88 +251,28 @@ const validate = defineCommand({
279
251
  // src/commands/context.ts
280
252
  /**
281
253
  * @file `ork context explain <wf> <node>`: what one node's session would load,
282
- * layer by layer. Its project instructions, every skill with the layer it
283
- * came from and whether it is live, shadowed or switched off, its context
284
- * files, its practices, and estimated tokens. Plans the session without
285
- * writing anything.
254
+ * layer by layer, as core's `explainSession` plans it without writing
255
+ * anything (one of Ork's own `ork:` workflows is written into the project's
256
+ * run folder first, as `ork run` does): its project instructions, every skill
257
+ * with the layer it came from and whether it is live, shadowed or switched
258
+ * off, its context files, its practices, and estimated tokens.
286
259
  */
287
- /** A rough count, for comparing sizes: about four characters a token. */
288
- const tokens = (text) => Math.ceil(text.length / 4);
289
- /** Which project instruction file Claude Code reads from the checkout. */
290
- const instructionsOf = (stack, checkout) => {
291
- if (stack.instructions === "ignore") return "left out (project.instructions: ignore)";
292
- const has = (file) => existsSync(path.join(checkout, file));
293
- if ([
294
- "CLAUDE.md",
295
- ".claude/CLAUDE.md",
296
- "CLAUDE.local.md"
297
- ].some((file) => has(file))) return "CLAUDE.md, loaded by Claude Code";
298
- if (has("AGENTS.md")) return "AGENTS.md, loaded by Claude Code";
299
- return "none in the project";
300
- };
301
- const skillLines = (stack, root) => stack.skills.map((skill) => {
302
- const source = skill.text === void 0 ? path.relative(root, skill.dir) || "." : "built-in";
303
- const line = {
304
- name: skill.layer !== "project" ? `ork:${skill.name}` : skill.name,
305
- layer: skill.layer,
306
- source: source.startsWith("..") ? skill.dir : source,
307
- state: skill.shadowedBy === void 0 && skill.ignored === void 0 ? "live" : "off",
308
- tokens: tokens(skill.text ?? readFileSync(path.join(skill.dir, "SKILL.md"), "utf8"))
309
- };
310
- if (skill.shadowedBy !== void 0) line.why = `shadowed by the ${skill.shadowedBy} layer`;
311
- else if (skill.ignored !== void 0) line.why = skill.ignored === "ignore" ? "project.skills: ignore" : "not in project.skills.allow";
312
- return line;
313
- });
260
+ /** Loads a workflow file and plans one node's session in it, in place. */
314
261
  const explainNode = (file, nodeId, cwd, home) => {
315
262
  const { report, workflow, graph, place, workflowDir } = loadWorkflow(file, cwd, home);
316
263
  if (workflow === null) return {
317
264
  ok: false,
318
265
  lines: formatReport(report)
319
266
  };
320
- const { root } = place;
321
- const plan = planNodeSession(workflow, nodeId, {
322
- cwd: root,
323
- inputs: {},
324
- inPlace: true,
325
- workflowDir,
326
- projectRoot: root,
267
+ const result = explainSession(workflow, graph, nodeId, {
268
+ root: place.root,
327
269
  orkDir: place.dir,
270
+ workflowDir,
328
271
  ...home === void 0 ? {} : { home }
329
272
  });
330
- if (!plan.ok) return {
273
+ return result.ok ? result : {
331
274
  ok: false,
332
- lines: [plan.error]
333
- };
334
- const { context } = plan;
335
- const practices = practiceInstructions(resolvePractices(workflow, nodeId), root);
336
- const names = ["caveman", "ponytail"].filter((n) => !practices.skipped.includes(n));
337
- const ref = graph?.agents.get(nodeId);
338
- const role = ref === void 0 ? void 0 : workflow.agents?.[ref.name]?.instructions;
339
- return {
340
- ok: true,
341
- explanation: {
342
- node: nodeId,
343
- type: workflow.nodes[nodeId]?.type ?? "",
344
- workflow: workflow.name,
345
- ...ref === void 0 ? {} : { agent: {
346
- name: ref.name,
347
- where: ref.where,
348
- tokens: tokens(role ?? "")
349
- } },
350
- instructions: instructionsOf(context, root),
351
- skills: skillLines(context, root),
352
- files: context.files.map((f) => ({
353
- layer: f.layer,
354
- name: f.name,
355
- tokens: tokens(f.text)
356
- })),
357
- practices: practices.text.map((text, index) => ({
358
- name: names[index] ?? "practice",
359
- tokens: tokens(text)
360
- })),
361
- skippedPractices: practices.skipped,
362
- appendedTokens: tokens(plan.spec.systemAppend)
363
- }
275
+ lines: [result.error]
364
276
  };
365
277
  };
366
278
  const approx = (count) => style("dim", `≈${count.toLocaleString("en")} tokens`);
@@ -368,7 +280,7 @@ const formatExplanation = (e) => {
368
280
  const width = Math.max(8, ...e.skills.map((s) => s.name.length));
369
281
  const sourceWidth = Math.max(0, ...e.skills.map((s) => s.source.length));
370
282
  const lines = [
371
- `${style("bold", e.node)} ${style("dim", `(${e.type}) in ${e.workflow}`)}`,
283
+ `${style("bold", e.node)} ${style("dim", `(${e.type}) in ${e.workflow}`)}${e.template === void 0 ? "" : style("dim", `, planned as ${e.template}, the node it was spawned from`)}`,
372
284
  "",
373
285
  ...e.agent === void 0 ? [] : [`${style("bold", "Agent")} ${e.agent.name} ${style("dim", `(${e.agent.where})`)} ${approx(e.agent.tokens)}`],
374
286
  `${style("bold", "Instructions")} ${e.instructions}`,
@@ -383,7 +295,10 @@ const formatExplanation = (e) => {
383
295
  }
384
296
  lines.push("", style("bold", "Context files"));
385
297
  if (e.files.length === 0) lines.push(style("dim", " none"));
386
- for (const f of e.files) lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}`);
298
+ for (const f of e.files) {
299
+ const cut = f.cutFrom === void 0 ? "" : ` ${style("yellow", `cut at the ${LESSON_CAP_TOKENS.toLocaleString("en")}-token cap; the whole file is ${approx(f.cutFrom)}`)}`;
300
+ lines.push(` ${f.layer.padEnd(8)} ${f.name} ${approx(f.tokens)}${cut}`);
301
+ }
387
302
  lines.push("", style("bold", "Practices"));
388
303
  for (const p of e.practices) lines.push(` ${p.name.padEnd(8)} ${approx(p.tokens)}`);
389
304
  for (const name of e.skippedPractices) lines.push(` ${name.padEnd(8)} ${style("dim", "the project's own skill wins")}`);
@@ -399,7 +314,7 @@ const explain = defineCommand({
399
314
  args: {
400
315
  workflow: {
401
316
  type: "positional",
402
- description: "A workflow file, or the name of one in the project's workflows (.ork/workflows/)",
317
+ description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
403
318
  required: true
404
319
  },
405
320
  node: {
@@ -415,6 +330,7 @@ const explain = defineCommand({
415
330
  },
416
331
  run: ({ args }) => {
417
332
  const cwd = process.cwd();
333
+ if (refusedBuiltin([args.workflow], cwd)) return;
418
334
  const result = explainNode(workflowArg(args.workflow, cwd).file, args.node, cwd);
419
335
  if (!result.ok) {
420
336
  for (const line of result.lines) process.stderr.write(`${line}\n`);
@@ -432,4 +348,4 @@ const context = defineCommand({
432
348
  },
433
349
  subCommands: { explain }
434
350
  });
435
- export { validate as a, warningLines as c, workflowArg as d, projectAt as f, loadWorkflow as i, WORKFLOW_DIR as l, explainNode as n, validateFile as o, isNewer as p, formatReport as r, walkLoaded as s, context as t, WORKFLOW_SUFFIX as u };
351
+ export { validate as a, warningLines as c, refusedBuiltin as d, workflowArg as f, loadWorkflow as i, WORKFLOW_DIR as l, registeredProjects as m, explainNode as n, validateFile as o, projectAt as p, formatReport as r, walkLoaded as s, context as t, WORKFLOW_SUFFIX as u };
@@ -1,4 +1,4 @@
1
- import { Qt as placeFor } from "./src-Cz1yOUTg.mjs";
1
+ import { fn as placeFor } from "./src-CGqMati3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  // ../core/src/remote/find.ts
4
4
  /**
@@ -1,166 +1,6 @@
1
- import { C as gitOk, S as git, rt as liveStatus, w as lines, x as removeWorktree } from "./src-Cz1yOUTg.mjs";
2
- import { i as style, n as cost } from "./output-Q2EdeVUt.mjs";
1
+ import { lt as liveStatus } from "./src-CGqMati3.mjs";
2
+ import { i as style, n as cost } from "./output-CXSJO00X.mjs";
3
3
  import { createInterface } from "node:readline/promises";
4
- import { existsSync } from "node:fs";
5
- // ../core/src/worktree/list.ts
6
- /**
7
- * @file What `ork worktrees` shows and cleans: every worktree and branch Ork's
8
- * runs recorded creating, whatever the project's git rules named them, and
9
- * any `ork/*` branch besides, with their state read from git itself.
10
- */
11
- /**
12
- * The worktrees the project's runs recorded creating, oldest run first; one
13
- * entry per branch or folder, from the latest run that created it.
14
- */
15
- const createdWorktrees = (store, project) => {
16
- const byName = /* @__PURE__ */ new Map();
17
- for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
18
- if (event.type !== "worktree" || event.action !== "created") continue;
19
- byName.set(event.branch ?? event.path, {
20
- runId: run.id,
21
- nodeId: event.id,
22
- path: event.path,
23
- ...event.branch === void 0 ? {} : { branch: event.branch }
24
- });
25
- }
26
- return [...byName.values()];
27
- };
28
- const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
29
- const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
30
- return {
31
- path: value("worktree") ?? "",
32
- branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
33
- prunable: fields.some((f) => f.startsWith("prunable"))
34
- };
35
- }).filter((entry) => entry.path !== "");
36
- /** The `ork/<run>/<node>` branches of runs that recorded nothing. */
37
- const prefixed = (branch) => {
38
- const [, runId = "", ...rest] = branch.split("/");
39
- return {
40
- runId,
41
- nodeId: rest.join("/"),
42
- path: "",
43
- branch
44
- };
45
- };
46
- /** How many commits a detached worktree holds that no branch, tag or remote has. */
47
- const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
48
- "rev-list",
49
- "--count",
50
- "HEAD",
51
- "--not",
52
- "--branches",
53
- "--tags",
54
- "--remotes"
55
- ])).stdout;
56
- /** One created worktree or branch, with its state read from git. */
57
- const stateOf = async (root, entries, item) => {
58
- const { branch } = item;
59
- const entry = branch === void 0 ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
60
- const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
61
- const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
62
- const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
63
- const on = branch ?? checkedOut;
64
- const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
65
- "rev-list",
66
- "--count",
67
- `HEAD..${on}`
68
- ])).stdout;
69
- const ahead = Number(counted);
70
- return {
71
- ...branch === void 0 ? {} : { branch },
72
- ...checkedOut === void 0 ? {} : { checkedOut },
73
- runId: item.runId,
74
- nodeId: item.nodeId,
75
- path: entry?.path ?? null,
76
- missing,
77
- dirty: lines(status).length > 0,
78
- ahead: Number.isNaN(ahead) ? 0 : ahead,
79
- merged: on !== void 0 && ahead === 0,
80
- inMainCheckout: entry?.path === root
81
- };
82
- };
83
- /**
84
- * Every worktree and branch Ork's runs created in the repository at `root`,
85
- * with its state: those the runs recorded (`created`, whatever the project's
86
- * git rules named them), and any `ork/` branch besides. A recorded one that git
87
- * no longer has is left out.
88
- */
89
- const listOrkWorktrees = async (root, created = []) => {
90
- const entries = parsePorcelain(await gitOk(root, [
91
- "worktree",
92
- "list",
93
- "--porcelain"
94
- ]));
95
- const branches = lines(await gitOk(root, [
96
- "for-each-ref",
97
- "--format=%(refname:short)",
98
- "refs/heads/"
99
- ]));
100
- const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
101
- const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => prefixed(b))];
102
- return Promise.all(items.map((item) => stateOf(root, entries, item)));
103
- };
104
- const cleanStep = (item, options) => {
105
- if (item.inMainCheckout) return {
106
- item,
107
- removeWorktree: false,
108
- deleteBranch: false,
109
- skip: "checked out in the main checkout"
110
- };
111
- if (item.dirty && !options.force) return {
112
- item,
113
- removeWorktree: false,
114
- deleteBranch: false,
115
- skip: "uncommitted changes"
116
- };
117
- if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return {
118
- item,
119
- removeWorktree: false,
120
- deleteBranch: false,
121
- skip: "commits on no branch"
122
- };
123
- const removable = item.path !== null && !item.missing;
124
- if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
125
- item,
126
- removeWorktree: removable,
127
- deleteBranch: false,
128
- skip: "branch not merged"
129
- };
130
- return {
131
- item,
132
- removeWorktree: removable,
133
- deleteBranch: options.branches && item.branch !== void 0
134
- };
135
- };
136
- /**
137
- * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
138
- * unmerged branch, or a detached worktree holding commits no branch has, is
139
- * kept unless `force`.
140
- */
141
- const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
142
- /** Carries out a clean plan, then prunes stale worktree entries. */
143
- const applyClean = async (root, steps, force) => {
144
- const done = [];
145
- await gitOk(root, ["worktree", "prune"]);
146
- for (const step of steps) {
147
- const { item } = step;
148
- if (step.removeWorktree && item.path !== null) {
149
- await removeWorktree(root, item.path, force);
150
- done.push(`removed ${item.path}`);
151
- }
152
- if (step.deleteBranch && item.branch !== void 0) {
153
- await gitOk(root, [
154
- "branch",
155
- force ? "-D" : "-d",
156
- item.branch
157
- ]);
158
- done.push(`deleted ${item.branch}`);
159
- }
160
- }
161
- await gitOk(root, ["worktree", "prune"]);
162
- return done;
163
- };
164
4
  // src/human.ts
165
5
  /**
166
6
  * @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
@@ -550,4 +390,4 @@ const formatTree = (roots) => {
550
390
  return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${usage}`;
551
391
  });
552
392
  };
553
- export { formatRunEvent as a, createdWorktrees as c, formatEvent as i, listOrkWorktrees as l, formatRuns as n, createTerminalHuman as o, formatTree as r, applyClean as s, ago as t, planClean as u };
393
+ export { formatRunEvent as a, formatEvent as i, formatRuns as n, createTerminalHuman as o, formatTree as r, ago as t };
@@ -1,4 +1,4 @@
1
- import { p as runMarkerIn } from "./src-DtN1k-B3.mjs";
1
+ import { p as runMarkerIn } from "./src-UWkk1sl9.mjs";
2
2
  // src/commands/in-a-run.ts
3
3
  /**
4
4
  * @file The commands that act as the user refuse a program or an agent in a run
@@ -1,4 +1,4 @@
1
- import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-Cz1yOUTg.mjs";
1
+ import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-CGqMati3.mjs";
2
2
  // src/run/limits.ts
3
3
  /**
4
4
  * @file The limits across runs that apply here (RFC-007): the user's settings
@@ -0,0 +1,110 @@
1
+ import { D as git, O as gitOk, k as lines } from "./src-CGqMati3.mjs";
2
+ import { existsSync } from "node:fs";
3
+ // ../core/src/worktree/list.ts
4
+ /**
5
+ * @file What `ork worktrees` shows: every worktree and branch Ork's runs
6
+ * recorded creating, whatever the project's git rules named them, and any
7
+ * `ork/*` branch besides, with their state read from git itself. What clean
8
+ * and remove do with them is clean.ts.
9
+ */
10
+ /**
11
+ * The worktrees the project's runs recorded creating, oldest run first; one
12
+ * entry per branch or folder, from the latest run that created it.
13
+ */
14
+ const createdWorktrees = (store, project) => {
15
+ const byName = /* @__PURE__ */ new Map();
16
+ for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
17
+ if (event.type !== "worktree" || event.action !== "created") continue;
18
+ byName.set(event.branch ?? event.path, {
19
+ runId: run.id,
20
+ nodeId: event.id,
21
+ path: event.path,
22
+ ...event.branch === void 0 ? {} : { branch: event.branch }
23
+ });
24
+ }
25
+ return [...byName.values()];
26
+ };
27
+ const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
28
+ const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
29
+ return {
30
+ path: value("worktree") ?? "",
31
+ branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
32
+ prunable: fields.some((f) => f.startsWith("prunable"))
33
+ };
34
+ }).filter((entry) => entry.path !== "");
35
+ /**
36
+ * The `ork/<run>/<node>` branches of runs that recorded nothing; with no path
37
+ * recorded, the worktree is whichever has the branch checked out.
38
+ */
39
+ const prefixed = (branch) => {
40
+ const [, runId = "", ...rest] = branch.split("/");
41
+ return {
42
+ runId,
43
+ nodeId: rest.join("/"),
44
+ path: "",
45
+ branch
46
+ };
47
+ };
48
+ /** How many commits a detached worktree holds that no branch, tag or remote has. */
49
+ const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
50
+ "rev-list",
51
+ "--count",
52
+ "HEAD",
53
+ "--not",
54
+ "--branches",
55
+ "--tags",
56
+ "--remotes"
57
+ ])).stdout;
58
+ /** One created worktree or branch, with its state read from git. */
59
+ const stateOf = async (root, entries, item) => {
60
+ const { branch } = item;
61
+ const entry = branch === void 0 || item.path !== "" ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
62
+ const holder = branch === void 0 ? void 0 : entries.find((e) => e.branch === branch && e.path !== entry?.path);
63
+ const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
64
+ const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
65
+ const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
66
+ const on = branch ?? checkedOut;
67
+ const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
68
+ "rev-list",
69
+ "--count",
70
+ `HEAD..${on}`
71
+ ])).stdout;
72
+ const ahead = Number(counted);
73
+ const changes = lines(status).length;
74
+ return {
75
+ ...branch === void 0 ? {} : { branch },
76
+ ...checkedOut === void 0 ? {} : { checkedOut },
77
+ runId: item.runId,
78
+ nodeId: item.nodeId,
79
+ path: entry?.path ?? null,
80
+ missing,
81
+ dirty: changes > 0,
82
+ changes,
83
+ ahead: Number.isNaN(ahead) ? 0 : ahead,
84
+ merged: on !== void 0 && ahead === 0,
85
+ inMainCheckout: entry?.path === root || holder?.path === root,
86
+ ...holder === void 0 || holder.path === root ? {} : { heldBy: holder.path }
87
+ };
88
+ };
89
+ /**
90
+ * Every worktree and branch Ork's runs created in the repository at `root`,
91
+ * with its state: those the runs recorded (`created`, whatever the project's
92
+ * git rules named them), and any `ork/` branch besides. A recorded one that git
93
+ * no longer has is left out.
94
+ */
95
+ const listOrkWorktrees = async (root, created = []) => {
96
+ const entries = parsePorcelain(await gitOk(root, [
97
+ "worktree",
98
+ "list",
99
+ "--porcelain"
100
+ ]));
101
+ const branches = lines(await gitOk(root, [
102
+ "for-each-ref",
103
+ "--format=%(refname:short)",
104
+ "refs/heads/"
105
+ ]));
106
+ const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
107
+ const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => prefixed(b))];
108
+ return Promise.all(items.map((item) => stateOf(root, entries, item)));
109
+ };
110
+ export { listOrkWorktrees as n, createdWorktrees as t };