@hasna/loops 0.3.26 → 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/cli/index.js +588 -79
- package/dist/daemon/index.js +50 -1
- package/dist/index.js +337 -24
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +49 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/sdk/index.js +49 -0
- package/dist/types.d.ts +22 -0
- package/docs/USAGE.md +54 -3
- package/package.json +1 -1
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:
|
|
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",
|
|
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",
|
|
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
|
|
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((
|
|
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 (!
|
|
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(
|
|
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;
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -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 {};
|