@hasna/loops 0.3.25 → 0.3.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -416,6 +416,42 @@ function validateTarget(value, label) {
416
416
  throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
417
417
  }
418
418
  }
419
+ if (value.worktree !== undefined) {
420
+ assertObject(value.worktree, `${label}.worktree`);
421
+ assertString(value.worktree.mode, `${label}.worktree.mode`);
422
+ const modes = ["auto", "required", "off", "main"];
423
+ if (!modes.includes(value.worktree.mode))
424
+ throw new Error(`${label}.worktree.mode must be one of ${modes.join(", ")}`);
425
+ if (typeof value.worktree.enabled !== "boolean")
426
+ throw new Error(`${label}.worktree.enabled must be a boolean`);
427
+ assertString(value.worktree.originalCwd, `${label}.worktree.originalCwd`);
428
+ assertString(value.worktree.cwd, `${label}.worktree.cwd`);
429
+ if (value.worktree.repoRoot !== undefined)
430
+ assertString(value.worktree.repoRoot, `${label}.worktree.repoRoot`);
431
+ if (value.worktree.root !== undefined)
432
+ assertString(value.worktree.root, `${label}.worktree.root`);
433
+ if (value.worktree.path !== undefined)
434
+ assertString(value.worktree.path, `${label}.worktree.path`);
435
+ if (value.worktree.branch !== undefined)
436
+ assertString(value.worktree.branch, `${label}.worktree.branch`);
437
+ if (value.worktree.reason !== undefined)
438
+ assertString(value.worktree.reason, `${label}.worktree.reason`);
439
+ }
440
+ if (value.routing !== undefined) {
441
+ assertObject(value.routing, `${label}.routing`);
442
+ if (value.routing.projectPath !== undefined)
443
+ assertString(value.routing.projectPath, `${label}.routing.projectPath`);
444
+ if (value.routing.projectGroup !== undefined)
445
+ assertString(value.routing.projectGroup, `${label}.routing.projectGroup`);
446
+ if (value.routing.taskId !== undefined)
447
+ assertString(value.routing.taskId, `${label}.routing.taskId`);
448
+ if (value.routing.eventId !== undefined)
449
+ assertString(value.routing.eventId, `${label}.routing.eventId`);
450
+ if (value.routing.eventType !== undefined)
451
+ assertString(value.routing.eventType, `${label}.routing.eventType`);
452
+ if (value.routing.eventSource !== undefined)
453
+ assertString(value.routing.eventSource, `${label}.routing.eventSource`);
454
+ }
419
455
  return value;
420
456
  }
421
457
  throw new Error(`${label}.type must be command or agent`);
@@ -2187,6 +2223,19 @@ class Store {
2187
2223
  const row = this.db.query("SELECT * FROM daemon_lease LIMIT 1").get();
2188
2224
  return row ? rowToLease(row) : undefined;
2189
2225
  }
2226
+ writeTransaction(fn) {
2227
+ this.db.exec("BEGIN IMMEDIATE;");
2228
+ try {
2229
+ const result = fn();
2230
+ this.db.exec("COMMIT;");
2231
+ return result;
2232
+ } catch (error) {
2233
+ try {
2234
+ this.db.exec("ROLLBACK;");
2235
+ } catch {}
2236
+ throw error;
2237
+ }
2238
+ }
2190
2239
  close() {
2191
2240
  this.db.close();
2192
2241
  }
@@ -4222,6 +4271,10 @@ function loops(opts = {}) {
4222
4271
  return new LoopsClient(opts);
4223
4272
  }
4224
4273
  // src/lib/templates.ts
4274
+ import { execFileSync } from "child_process";
4275
+ import { existsSync as existsSync2 } from "fs";
4276
+ import { homedir as homedir3 } from "os";
4277
+ import { basename, isAbsolute, join as join3, relative, resolve } from "path";
4225
4278
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
4226
4279
  var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4227
4280
  var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
@@ -4235,6 +4288,8 @@ var TEMPLATE_SUMMARIES = [
4235
4288
  { name: "taskId", required: true, description: "Todos task id to execute." },
4236
4289
  { name: "taskTitle", description: "Human-readable task title." },
4237
4290
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4291
+ { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4292
+ { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4238
4293
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4239
4294
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4240
4295
  { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
@@ -4244,7 +4299,10 @@ var TEMPLATE_SUMMARIES = [
4244
4299
  { name: "model", description: "Provider model." },
4245
4300
  { name: "variant", description: "Provider reasoning/model effort variant." },
4246
4301
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4247
- { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4302
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." },
4303
+ { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
4304
+ { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
4305
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
4248
4306
  ]
4249
4307
  },
4250
4308
  {
@@ -4258,6 +4316,8 @@ var TEMPLATE_SUMMARIES = [
4258
4316
  { name: "eventSource", required: true, description: "Hasna event source." },
4259
4317
  { name: "eventJson", required: true, description: "Full event envelope JSON." },
4260
4318
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4319
+ { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4320
+ { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4261
4321
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4262
4322
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4263
4323
  { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
@@ -4267,7 +4327,10 @@ var TEMPLATE_SUMMARIES = [
4267
4327
  { name: "model", description: "Provider model." },
4268
4328
  { name: "variant", description: "Provider reasoning/model effort variant." },
4269
4329
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4270
- { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4330
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." },
4331
+ { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
4332
+ { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
4333
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
4271
4334
  ]
4272
4335
  },
4273
4336
  {
@@ -4279,6 +4342,8 @@ var TEMPLATE_SUMMARIES = [
4279
4342
  { name: "objective", required: true, description: "Narrow goal-mode objective for the worker." },
4280
4343
  { name: "prompt", description: "Optional extra worker prompt details." },
4281
4344
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4345
+ { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4346
+ { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4282
4347
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4283
4348
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4284
4349
  { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
@@ -4289,6 +4354,9 @@ var TEMPLATE_SUMMARIES = [
4289
4354
  { name: "variant", description: "Provider reasoning/model effort variant." },
4290
4355
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4291
4356
  { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." },
4357
+ { name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
4358
+ { name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
4359
+ { name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
4292
4360
  { name: "timeoutMs", default: "2700000", description: "Step timeout in milliseconds." }
4293
4361
  ]
4294
4362
  }
@@ -4330,6 +4398,183 @@ function accountForRole(input, role, seed) {
4330
4398
  return input.verifierAccount;
4331
4399
  return rolePoolValue(input.accountPool, seed, role) ?? input.account;
4332
4400
  }
4401
+ function slugSegment(value, fallback = "item") {
4402
+ const slug = (value ?? "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
4403
+ return slug || fallback;
4404
+ }
4405
+ function stableHex(seed) {
4406
+ return stableIndex(seed, 4294967295).toString(16).padStart(8, "0");
4407
+ }
4408
+ function shellQuote2(value) {
4409
+ return `'${value.replace(/'/g, `'\\''`)}'`;
4410
+ }
4411
+ function normalizeWorktreeMode(mode) {
4412
+ const value = mode ?? "auto";
4413
+ if (!["auto", "required", "off", "main"].includes(value)) {
4414
+ throw new Error(`worktreeMode must be one of auto, required, off, or main`);
4415
+ }
4416
+ return value;
4417
+ }
4418
+ function defaultWorktreeRoot(root) {
4419
+ if (root?.trim()) {
4420
+ const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
4421
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
4422
+ }
4423
+ return join3(homedir3(), ".hasna", "loops", "worktrees");
4424
+ }
4425
+ function gitRootFor(path) {
4426
+ if (!existsSync2(path))
4427
+ return;
4428
+ try {
4429
+ return execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], {
4430
+ encoding: "utf8",
4431
+ stdio: ["ignore", "pipe", "ignore"]
4432
+ }).trim();
4433
+ } catch {
4434
+ return;
4435
+ }
4436
+ }
4437
+ function prepareWorktreeCommand(plan) {
4438
+ const repo = shellQuote2(plan.repoRoot);
4439
+ const path = shellQuote2(plan.path);
4440
+ const branch = shellQuote2(plan.branch);
4441
+ return [
4442
+ "set -euo pipefail",
4443
+ `repo=${repo}`,
4444
+ `path=${path}`,
4445
+ `branch=${branch}`,
4446
+ 'resolve_path() { cd "$1" && pwd -P; }',
4447
+ "git_common_dir() {",
4448
+ ' local base="$1"',
4449
+ " local common",
4450
+ ' common="$(git -C "$base" rev-parse --git-common-dir)"',
4451
+ ' case "$common" in',
4452
+ ' /*) printf "%s\\n" "$common" ;;',
4453
+ ' *) (cd "$base" && cd "$common" && pwd -P) ;;',
4454
+ " esac",
4455
+ "}",
4456
+ 'mkdir -p "$(dirname "$path")"',
4457
+ 'if [ -e "$path" ]; then',
4458
+ ' if [ -L "$path" ]; then',
4459
+ ' printf "refusing symlinked worktree path %s\\n" "$path" >&2',
4460
+ " exit 1",
4461
+ " fi",
4462
+ ' if git -C "$path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then',
4463
+ ' expected_common="$(git_common_dir "$repo")"',
4464
+ ' actual_common="$(git_common_dir "$path")"',
4465
+ ' if [ "$actual_common" != "$expected_common" ]; then',
4466
+ ' printf "existing worktree %s belongs to different git common dir\\n" "$path" >&2',
4467
+ ' printf "expected %s got %s\\n" "$expected_common" "$actual_common" >&2',
4468
+ " exit 1",
4469
+ " fi",
4470
+ ' actual_top="$(git -C "$path" rev-parse --show-toplevel)"',
4471
+ ' actual_top="$(resolve_path "$actual_top")"',
4472
+ ' expected_top="$(resolve_path "$path")"',
4473
+ ' if [ "$actual_top" != "$expected_top" ]; then',
4474
+ ' printf "existing worktree top-level mismatch for %s: %s\\n" "$path" "$actual_top" >&2',
4475
+ " exit 1",
4476
+ " fi",
4477
+ ' actual_branch="$(git -C "$path" branch --show-current)"',
4478
+ ' if [ "$actual_branch" != "$branch" ]; then',
4479
+ ' printf "existing worktree %s is on branch %s, expected %s\\n" "$path" "$actual_branch" "$branch" >&2',
4480
+ " exit 1",
4481
+ " fi",
4482
+ ' printf "existing worktree %s branch %s\\n" "$path" "$branch"',
4483
+ " exit 0",
4484
+ " fi",
4485
+ ' printf "refusing to overwrite non-git path: %s\\n" "$path" >&2',
4486
+ " exit 1",
4487
+ "fi",
4488
+ 'git -C "$repo" rev-parse --is-inside-work-tree >/dev/null',
4489
+ 'if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then',
4490
+ ' git -C "$repo" worktree add "$path" "$branch"',
4491
+ "else",
4492
+ ' git -C "$repo" worktree add -b "$branch" "$path" HEAD',
4493
+ "fi",
4494
+ 'printf "prepared worktree %s branch %s\\n" "$path" "$branch"'
4495
+ ].join(`
4496
+ `);
4497
+ }
4498
+ function worktreePlan(input, seed) {
4499
+ const mode = normalizeWorktreeMode(input.worktreeMode);
4500
+ const originalCwd = input.projectPath;
4501
+ if (mode === "off" || mode === "main") {
4502
+ return {
4503
+ mode,
4504
+ enabled: false,
4505
+ originalCwd,
4506
+ cwd: originalCwd,
4507
+ reason: mode === "main" ? "explicit main/default checkout mode" : "worktree mode disabled"
4508
+ };
4509
+ }
4510
+ const repoRoot = gitRootFor(originalCwd);
4511
+ if (!repoRoot) {
4512
+ if (mode === "required") {
4513
+ throw new Error(`worktreeMode=required but projectPath is not an existing git repository: ${originalCwd}`);
4514
+ }
4515
+ return {
4516
+ mode,
4517
+ enabled: false,
4518
+ originalCwd,
4519
+ cwd: originalCwd,
4520
+ reason: "projectPath is not an existing git repository"
4521
+ };
4522
+ }
4523
+ const root = defaultWorktreeRoot(input.worktreeRoot);
4524
+ const repoSlug = slugSegment(basename(repoRoot), "repo");
4525
+ const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
4526
+ const worktreePath = join3(root, repoSlug, seedSlug);
4527
+ const relativeCwd = relative(repoRoot, originalCwd);
4528
+ const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ? join3(worktreePath, relativeCwd) : worktreePath;
4529
+ const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
4530
+ const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
4531
+ const prepareStep = {
4532
+ id: "prepare-worktree",
4533
+ name: "Prepare Worktree",
4534
+ description: "Create or reuse the isolated OpenLoops git worktree for this workflow run.",
4535
+ target: {
4536
+ type: "command",
4537
+ command: "bash",
4538
+ args: ["-lc", prepareWorktreeCommand({ repoRoot, path: worktreePath, branch })],
4539
+ cwd: repoRoot,
4540
+ timeoutMs: 5 * 60000
4541
+ },
4542
+ timeoutMs: 5 * 60000
4543
+ };
4544
+ return {
4545
+ mode,
4546
+ enabled: true,
4547
+ originalCwd,
4548
+ cwd,
4549
+ repoRoot,
4550
+ root,
4551
+ path: worktreePath,
4552
+ branch,
4553
+ prepareStep
4554
+ };
4555
+ }
4556
+ function worktreePrompt(plan) {
4557
+ if (plan.enabled) {
4558
+ return [
4559
+ "OpenLoops worktree policy:",
4560
+ "- Use the isolated git worktree as the only writeable repository checkout for this task/event.",
4561
+ `- Worktree cwd: ${plan.cwd}`,
4562
+ `- Worktree root: ${plan.path}`,
4563
+ `- Branch: ${plan.branch}`,
4564
+ `- Original checkout: ${plan.originalCwd}`,
4565
+ "- Do not mutate the original checkout/main branch except for read-only inspection.",
4566
+ "- Preserve unrelated changes in both the original checkout and this worktree."
4567
+ ].join(`
4568
+ `);
4569
+ }
4570
+ return [
4571
+ "OpenLoops worktree policy:",
4572
+ `- Worktree mode ${plan.mode} did not select an isolated worktree: ${plan.reason ?? "not enabled"}.`,
4573
+ `- Cwd: ${plan.cwd}`,
4574
+ "- Do not create ad hoc worktrees unless the task itself explicitly requires one."
4575
+ ].join(`
4576
+ `);
4577
+ }
4333
4578
  function assertNativeAuthProfileSupport(input, provider) {
4334
4579
  if (provider === "codewith")
4335
4580
  return;
@@ -4338,7 +4583,7 @@ function assertNativeAuthProfileSupport(input, provider) {
4338
4583
  return;
4339
4584
  throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
4340
4585
  }
4341
- function agentTarget(input, prompt, role, seed) {
4586
+ function agentTarget(input, prompt, role, seed, plan) {
4342
4587
  const provider = input.provider ?? "codewith";
4343
4588
  assertNativeAuthProfileSupport(input, provider);
4344
4589
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
@@ -4346,7 +4591,7 @@ function agentTarget(input, prompt, role, seed) {
4346
4591
  type: "agent",
4347
4592
  provider,
4348
4593
  prompt,
4349
- cwd: input.projectPath,
4594
+ cwd: plan.cwd,
4350
4595
  model: input.model,
4351
4596
  variant: input.variant,
4352
4597
  agent: input.agent,
@@ -4354,10 +4599,33 @@ function agentTarget(input, prompt, role, seed) {
4354
4599
  configIsolation: "safe",
4355
4600
  permissionMode: input.permissionMode ?? "bypass",
4356
4601
  sandbox,
4602
+ worktree: {
4603
+ mode: plan.mode,
4604
+ enabled: plan.enabled,
4605
+ originalCwd: plan.originalCwd,
4606
+ cwd: plan.cwd,
4607
+ repoRoot: plan.repoRoot,
4608
+ root: plan.root,
4609
+ path: plan.path,
4610
+ branch: plan.branch,
4611
+ reason: plan.reason
4612
+ },
4613
+ routing: {
4614
+ projectPath: input.routeProjectPath ?? input.projectPath,
4615
+ ...input.projectGroup ? { projectGroup: input.projectGroup } : {}
4616
+ },
4357
4617
  account: accountForRole(input, role, seed),
4358
4618
  timeoutMs: 45 * 60000
4359
4619
  };
4360
4620
  }
4621
+ function workflowStepsWithWorktree(plan, steps) {
4622
+ if (!plan.prepareStep)
4623
+ return steps;
4624
+ return [
4625
+ plan.prepareStep,
4626
+ ...steps.map((step) => step.id === "worker" ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
4627
+ ];
4628
+ }
4361
4629
  function listLoopTemplates() {
4362
4630
  return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
4363
4631
  }
@@ -4369,18 +4637,30 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4369
4637
  throw new Error("taskId is required");
4370
4638
  if (!input.projectPath?.trim())
4371
4639
  throw new Error("projectPath is required");
4640
+ const plan = worktreePlan(input, input.taskId);
4372
4641
  const taskContext = {
4373
4642
  taskId: input.taskId,
4374
4643
  taskTitle: input.taskTitle,
4375
4644
  taskDescription: input.taskDescription,
4376
4645
  eventId: input.eventId,
4377
4646
  eventType: input.eventType,
4378
- projectPath: input.projectPath
4647
+ projectPath: input.projectPath,
4648
+ routeProjectPath: input.routeProjectPath,
4649
+ projectGroup: input.projectGroup,
4650
+ worktree: {
4651
+ mode: plan.mode,
4652
+ enabled: plan.enabled,
4653
+ cwd: plan.cwd,
4654
+ path: plan.path,
4655
+ branch: plan.branch,
4656
+ reason: plan.reason
4657
+ }
4379
4658
  };
4380
4659
  const workerPrompt = [
4381
4660
  `/goal Complete todos task ${input.taskId} in ${input.projectPath}.`,
4382
4661
  "",
4383
4662
  "You are the worker agent for a task-triggered OpenLoops workflow.",
4663
+ worktreePrompt(plan),
4384
4664
  "Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
4385
4665
  "Claim/start the task if appropriate, inspect the repository/project state, implement only the task scope, run focused validation, preserve unrelated user changes, and update the task with comments, evidence, changed files, commits, and blockers.",
4386
4666
  "Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
@@ -4393,6 +4673,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4393
4673
  `/goal Verify todos task ${input.taskId} after the worker step.`,
4394
4674
  "",
4395
4675
  "You are the verifier agent for a task-triggered OpenLoops workflow.",
4676
+ worktreePrompt(plan),
4396
4677
  "Use fresh context. Inspect the task, repository state, commits, tests, and worker evidence. Act as an adversarial reviewer focused on correctness, regressions, missing tests, security, and incomplete requirements.",
4397
4678
  "If the work is valid, record verification evidence in todos and mark/leave the task in the correct completed state according to the todos CLI. If it is not valid, add precise follow-up tasks or comments and leave the original task open or blocked with clear evidence.",
4398
4679
  "Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
@@ -4405,12 +4686,12 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4405
4686
  name: `todos-task-${input.taskId.slice(0, 8)}-worker-verifier`,
4406
4687
  description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
4407
4688
  version: 1,
4408
- steps: [
4689
+ steps: workflowStepsWithWorktree(plan, [
4409
4690
  {
4410
4691
  id: "worker",
4411
4692
  name: "Worker",
4412
4693
  description: "Implement the todos task and record evidence.",
4413
- target: agentTarget(input, workerPrompt, "worker", input.taskId),
4694
+ target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
4414
4695
  timeoutMs: 45 * 60000
4415
4696
  },
4416
4697
  {
@@ -4418,10 +4699,10 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4418
4699
  name: "Verifier",
4419
4700
  description: "Adversarially verify worker output and update todos.",
4420
4701
  dependsOn: ["worker"],
4421
- target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
4702
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
4422
4703
  timeoutMs: 30 * 60000
4423
4704
  }
4424
- ]
4705
+ ])
4425
4706
  };
4426
4707
  }
4427
4708
  function renderEventWorkerVerifierWorkflow(input) {
@@ -4435,18 +4716,31 @@ function renderEventWorkerVerifierWorkflow(input) {
4435
4716
  throw new Error("eventJson is required");
4436
4717
  if (!input.projectPath?.trim())
4437
4718
  throw new Error("projectPath is required");
4719
+ const seed = `${input.eventSource}:${input.eventType}:${input.eventId}`;
4720
+ const plan = worktreePlan(input, seed);
4438
4721
  const eventContext = {
4439
4722
  eventId: input.eventId,
4440
4723
  eventType: input.eventType,
4441
4724
  eventSource: input.eventSource,
4442
4725
  eventSubject: input.eventSubject,
4443
4726
  eventMessage: input.eventMessage,
4444
- projectPath: input.projectPath
4727
+ projectPath: input.projectPath,
4728
+ routeProjectPath: input.routeProjectPath,
4729
+ projectGroup: input.projectGroup,
4730
+ worktree: {
4731
+ mode: plan.mode,
4732
+ enabled: plan.enabled,
4733
+ cwd: plan.cwd,
4734
+ path: plan.path,
4735
+ branch: plan.branch,
4736
+ reason: plan.reason
4737
+ }
4445
4738
  };
4446
4739
  const workerPrompt = [
4447
4740
  `/goal Handle Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}) in ${input.projectPath}.`,
4448
4741
  "",
4449
4742
  "You are the worker agent for an event-triggered OpenLoops workflow.",
4743
+ worktreePrompt(plan),
4450
4744
  "Investigate first before changing files. Read the full event envelope and decide the narrow action required by that event. Preserve unrelated user changes and update the relevant local CLI/task/knowledge system with evidence, changed files, commits, and blockers.",
4451
4745
  "If the event is informational or does not require action, record that finding and stop without making changes.",
4452
4746
  "",
@@ -4458,6 +4752,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4458
4752
  `/goal Verify handling of Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}).`,
4459
4753
  "",
4460
4754
  "You are the verifier agent for an event-triggered OpenLoops workflow.",
4755
+ worktreePrompt(plan),
4461
4756
  "Use fresh context. Inspect the event, repository/project state, worker evidence, tests, and any created tasks or notes. Act as an adversarial reviewer focused on correctness, regressions, security, missing evidence, and incomplete requirements.",
4462
4757
  "If the work is valid, record verification evidence in the relevant local system. If it is not valid, add precise follow-up tasks/comments and leave the event handling state open or blocked with clear evidence.",
4463
4758
  "",
@@ -4469,12 +4764,12 @@ function renderEventWorkerVerifierWorkflow(input) {
4469
4764
  name: `event-${input.eventSource}-${input.eventType}-${input.eventId.slice(0, 8)}-worker-verifier`.replace(/[^a-zA-Z0-9._:-]+/g, "-"),
4470
4765
  description: `Event-triggered worker/verifier workflow for ${input.eventSource}/${input.eventType}`,
4471
4766
  version: 1,
4472
- steps: [
4767
+ steps: workflowStepsWithWorktree(plan, [
4473
4768
  {
4474
4769
  id: "worker",
4475
4770
  name: "Worker",
4476
4771
  description: "Handle the Hasna event and record evidence.",
4477
- target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4772
+ target: agentTarget(input, workerPrompt, "worker", seed, plan),
4478
4773
  timeoutMs: 45 * 60000
4479
4774
  },
4480
4775
  {
@@ -4482,10 +4777,10 @@ function renderEventWorkerVerifierWorkflow(input) {
4482
4777
  name: "Verifier",
4483
4778
  description: "Adversarially verify event handling.",
4484
4779
  dependsOn: ["worker"],
4485
- target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4780
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
4486
4781
  timeoutMs: 30 * 60000
4487
4782
  }
4488
- ]
4783
+ ])
4489
4784
  };
4490
4785
  }
4491
4786
  function renderBoundedAgentWorkerVerifierWorkflow(input) {
@@ -4494,11 +4789,13 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
4494
4789
  if (!input.projectPath?.trim())
4495
4790
  throw new Error("projectPath is required");
4496
4791
  const seed = `${input.projectPath}:${input.objective}`;
4792
+ const plan = worktreePlan(input, seed);
4497
4793
  const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
4498
4794
  const workerPrompt = [
4499
4795
  `/goal ${input.objective}`,
4500
4796
  "",
4501
4797
  "You are the worker step for a bounded OpenLoops agent workflow.",
4798
+ worktreePrompt(plan),
4502
4799
  "Investigate first. Keep scope narrow, use local project/task systems as the source of truth when relevant, preserve unrelated changes, run focused validation, and record concise evidence.",
4503
4800
  "Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
4504
4801
  input.prompt ? "" : undefined,
@@ -4509,6 +4806,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
4509
4806
  `/goal Adversarially verify: ${input.objective}`,
4510
4807
  "",
4511
4808
  "You are the verifier step for a bounded OpenLoops agent workflow.",
4809
+ worktreePrompt(plan),
4512
4810
  "Use fresh context. Review the worker result for correctness, regressions, missing tests, safety, runaway-agent risk, output bounds, and incomplete evidence.",
4513
4811
  "If valid, record verification evidence. If invalid, create precise follow-up tasks or comments and leave the original work open. Do not make broad unrelated changes."
4514
4812
  ].join(`
@@ -4517,12 +4815,12 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
4517
4815
  name: input.name ?? `bounded-agent-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
4518
4816
  description: `Bounded worker/verifier workflow for ${input.objective.slice(0, 180)}`,
4519
4817
  version: 1,
4520
- steps: [
4818
+ steps: workflowStepsWithWorktree(plan, [
4521
4819
  {
4522
4820
  id: "worker",
4523
4821
  name: "Worker",
4524
4822
  description: "Execute the bounded objective and record evidence.",
4525
- target: agentTarget(input, workerPrompt, "worker", seed),
4823
+ target: agentTarget(input, workerPrompt, "worker", seed, plan),
4526
4824
  timeoutMs
4527
4825
  },
4528
4826
  {
@@ -4530,10 +4828,10 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
4530
4828
  name: "Verifier",
4531
4829
  description: "Adversarially verify the bounded objective result.",
4532
4830
  dependsOn: ["worker"],
4533
- target: agentTarget(input, verifierPrompt, "verifier", seed),
4831
+ target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
4534
4832
  timeoutMs: Math.min(timeoutMs, 30 * 60000)
4535
4833
  }
4536
- ]
4834
+ ])
4537
4835
  };
4538
4836
  }
4539
4837
  function renderLoopTemplate(id, values) {
@@ -4543,6 +4841,8 @@ function renderLoopTemplate(id, values) {
4543
4841
  taskTitle: values.taskTitle,
4544
4842
  taskDescription: values.taskDescription,
4545
4843
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4844
+ routeProjectPath: values.routeProjectPath,
4845
+ projectGroup: values.projectGroup,
4546
4846
  provider: values.provider,
4547
4847
  authProfile: values.authProfile,
4548
4848
  authProfilePool: listVar(values.authProfilePool),
@@ -4555,6 +4855,9 @@ function renderLoopTemplate(id, values) {
4555
4855
  agent: values.agent,
4556
4856
  permissionMode: values.permissionMode,
4557
4857
  sandbox: values.sandbox,
4858
+ worktreeMode: values.worktreeMode,
4859
+ worktreeRoot: values.worktreeRoot,
4860
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
4558
4861
  eventId: values.eventId,
4559
4862
  eventType: values.eventType
4560
4863
  });
@@ -4568,6 +4871,8 @@ function renderLoopTemplate(id, values) {
4568
4871
  eventMessage: values.eventMessage,
4569
4872
  eventJson: values.eventJson ?? "",
4570
4873
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4874
+ routeProjectPath: values.routeProjectPath,
4875
+ projectGroup: values.projectGroup,
4571
4876
  provider: values.provider,
4572
4877
  authProfile: values.authProfile,
4573
4878
  authProfilePool: listVar(values.authProfilePool),
@@ -4579,7 +4884,10 @@ function renderLoopTemplate(id, values) {
4579
4884
  variant: values.variant,
4580
4885
  agent: values.agent,
4581
4886
  permissionMode: values.permissionMode,
4582
- sandbox: values.sandbox
4887
+ sandbox: values.sandbox,
4888
+ worktreeMode: values.worktreeMode,
4889
+ worktreeRoot: values.worktreeRoot,
4890
+ worktreeBranchPrefix: values.worktreeBranchPrefix
4583
4891
  });
4584
4892
  }
4585
4893
  if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
@@ -4588,6 +4896,8 @@ function renderLoopTemplate(id, values) {
4588
4896
  objective: values.objective ?? "",
4589
4897
  prompt: values.prompt,
4590
4898
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4899
+ routeProjectPath: values.routeProjectPath,
4900
+ projectGroup: values.projectGroup,
4591
4901
  provider: values.provider,
4592
4902
  authProfile: values.authProfile,
4593
4903
  authProfilePool: listVar(values.authProfilePool),
@@ -4600,6 +4910,9 @@ function renderLoopTemplate(id, values) {
4600
4910
  agent: values.agent,
4601
4911
  permissionMode: values.permissionMode,
4602
4912
  sandbox: values.sandbox,
4913
+ worktreeMode: values.worktreeMode,
4914
+ worktreeRoot: values.worktreeRoot,
4915
+ worktreeBranchPrefix: values.worktreeBranchPrefix,
4603
4916
  timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
4604
4917
  });
4605
4918
  }
@@ -4617,13 +4930,13 @@ import { spawnSync as spawnSync3 } from "child_process";
4617
4930
  import { accessSync as accessSync2, constants as constants2 } from "fs";
4618
4931
 
4619
4932
  // src/daemon/control.ts
4620
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
4933
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
4621
4934
  import { hostname } from "os";
4622
4935
  import { dirname as dirname2 } from "path";
4623
4936
 
4624
4937
  // src/daemon/loop.ts
4625
4938
  function realSleep(ms) {
4626
- return new Promise((resolve) => setTimeout(resolve, ms));
4939
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4627
4940
  }
4628
4941
  async function runLoop(opts) {
4629
4942
  const sleep = opts.sleep ?? realSleep;
@@ -4645,7 +4958,7 @@ async function runLoop(opts) {
4645
4958
 
4646
4959
  // src/daemon/control.ts
4647
4960
  function readPid(path = pidFilePath()) {
4648
- if (!existsSync2(path))
4961
+ if (!existsSync3(path))
4649
4962
  return;
4650
4963
  try {
4651
4964
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -5180,7 +5493,7 @@ function buildHealthReport(store, opts = {}) {
5180
5493
  };
5181
5494
  }
5182
5495
  // src/lib/hygiene.ts
5183
- import { basename } from "path";
5496
+ import { basename as basename2 } from "path";
5184
5497
  var PROVIDER_TOKENS = new Set([
5185
5498
  "codewith",
5186
5499
  "claude",
@@ -5201,7 +5514,7 @@ function repoSlugFromCwd(cwd) {
5201
5514
  return "";
5202
5515
  if (cwd.includes("/.hasna/loops/"))
5203
5516
  return "";
5204
- return slugify(basename(cwd));
5517
+ return slugify(basename2(cwd));
5205
5518
  }
5206
5519
  function scopeForLoop(loop) {
5207
5520
  const cwd = loop.target.type === "command" || loop.target.type === "agent" ? loop.target.cwd : undefined;
@@ -179,6 +179,7 @@ export declare class Store {
179
179
  heartbeatDaemonLease(id: string, ttlMs: number, now?: Date): DaemonLease | undefined;
180
180
  releaseDaemonLease(id: string): void;
181
181
  getDaemonLease(): DaemonLease | undefined;
182
+ writeTransaction<T>(fn: () => T): T;
182
183
  close(): void;
183
184
  }
184
185
  export {};