@hasna/loops 0.3.16 → 0.3.18

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.
@@ -1052,6 +1052,30 @@ class Store {
1052
1052
  throw new Error(`loop not found after update: ${id}`);
1053
1053
  return after;
1054
1054
  }
1055
+ renameLoop(id, name, opts = {}) {
1056
+ const current = this.getLoop(id);
1057
+ if (!current)
1058
+ throw new Error(`loop not found: ${id}`);
1059
+ const trimmed = name.trim();
1060
+ if (!trimmed)
1061
+ throw new Error("loop name must not be empty");
1062
+ const updated = (opts.now ?? new Date).toISOString();
1063
+ this.db.query(`UPDATE loops SET name=$name, updated_at=$updated
1064
+ WHERE id=$id
1065
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1066
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1067
+ ))`).run({
1068
+ $id: id,
1069
+ $name: trimmed,
1070
+ $updated: updated,
1071
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1072
+ $now: updated
1073
+ });
1074
+ const after = this.getLoop(id);
1075
+ if (!after)
1076
+ throw new Error(`loop not found after rename: ${id}`);
1077
+ return after;
1078
+ }
1055
1079
  archiveLoop(idOrName) {
1056
1080
  const loop = this.requireLoop(idOrName);
1057
1081
  if (loop.archivedAt)
@@ -4395,7 +4419,7 @@ function enableStartup(result) {
4395
4419
  // package.json
4396
4420
  var package_default = {
4397
4421
  name: "@hasna/loops",
4398
- version: "0.3.16",
4422
+ version: "0.3.18",
4399
4423
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4400
4424
  type: "module",
4401
4425
  main: "dist/index.js",
package/dist/index.d.ts CHANGED
@@ -8,9 +8,10 @@ export { listOpenMachines, refreshLoopMachine, resolveLoopMachine } from "./lib/
8
8
  export { tick } from "./lib/scheduler.js";
9
9
  export { executeWorkflow, executeLoopTarget, preflightWorkflow } from "./lib/workflow-runner.js";
10
10
  export { workflowExecutionOrder, workflowBodyFromJson } from "./lib/workflow-spec.js";
11
- export { EVENT_WORKER_VERIFIER_TEMPLATE_ID, TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID, getLoopTemplate, listLoopTemplates, renderEventWorkerVerifierWorkflow, renderLoopTemplate, renderTodosTaskWorkerVerifierWorkflow, } from "./lib/templates.js";
11
+ export { BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID, EVENT_WORKER_VERIFIER_TEMPLATE_ID, TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID, getLoopTemplate, listLoopTemplates, renderBoundedAgentWorkerVerifierWorkflow, renderEventWorkerVerifierWorkflow, renderLoopTemplate, renderTodosTaskWorkerVerifierWorkflow, } from "./lib/templates.js";
12
12
  export { runDoctor } from "./lib/doctor.js";
13
13
  export { buildHealthReport, classifyRunFailure, expectationForLoop } from "./lib/health.js";
14
+ export { buildDuplicateOverlapReport, buildNameHygieneReport, buildScriptInventoryReport } from "./lib/hygiene.js";
14
15
  export { runGoal } from "./lib/goal/runner.js";
15
16
  export { resolveGoalModel } from "./lib/goal/model-factory.js";
16
17
  export { isTerminal as isGoalTerminal, readyNodeKeys, rollupSummary } from "./lib/goal/status.js";
package/dist/index.js CHANGED
@@ -1050,6 +1050,30 @@ class Store {
1050
1050
  throw new Error(`loop not found after update: ${id}`);
1051
1051
  return after;
1052
1052
  }
1053
+ renameLoop(id, name, opts = {}) {
1054
+ const current = this.getLoop(id);
1055
+ if (!current)
1056
+ throw new Error(`loop not found: ${id}`);
1057
+ const trimmed = name.trim();
1058
+ if (!trimmed)
1059
+ throw new Error("loop name must not be empty");
1060
+ const updated = (opts.now ?? new Date).toISOString();
1061
+ this.db.query(`UPDATE loops SET name=$name, updated_at=$updated
1062
+ WHERE id=$id
1063
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1064
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1065
+ ))`).run({
1066
+ $id: id,
1067
+ $name: trimmed,
1068
+ $updated: updated,
1069
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1070
+ $now: updated
1071
+ });
1072
+ const after = this.getLoop(id);
1073
+ if (!after)
1074
+ throw new Error(`loop not found after rename: ${id}`);
1075
+ return after;
1076
+ }
1053
1077
  archiveLoop(idOrName) {
1054
1078
  const loop = this.requireLoop(idOrName);
1055
1079
  if (loop.archivedAt)
@@ -4094,6 +4118,7 @@ function loops(opts = {}) {
4094
4118
  // src/lib/templates.ts
4095
4119
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
4096
4120
  var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4121
+ var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
4097
4122
  var TEMPLATE_SUMMARIES = [
4098
4123
  {
4099
4124
  id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
@@ -4138,6 +4163,28 @@ var TEMPLATE_SUMMARIES = [
4138
4163
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4139
4164
  { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4140
4165
  ]
4166
+ },
4167
+ {
4168
+ id: BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID,
4169
+ name: "Bounded Agent Worker + Verifier",
4170
+ description: "Create a bounded recurring-agent workflow: one agent performs a narrow objective, then a fresh verifier audits the result with separate account/profile selection.",
4171
+ kind: "workflow",
4172
+ variables: [
4173
+ { name: "objective", required: true, description: "Narrow goal-mode objective for the worker." },
4174
+ { name: "prompt", description: "Optional extra worker prompt details." },
4175
+ { name: "projectPath", required: true, description: "Repository or project working directory." },
4176
+ { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4177
+ { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4178
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4179
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4180
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4181
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4182
+ { name: "model", description: "Provider model." },
4183
+ { name: "variant", description: "Provider reasoning/model effort variant." },
4184
+ { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4185
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." },
4186
+ { name: "timeoutMs", default: "2700000", description: "Step timeout in milliseconds." }
4187
+ ]
4141
4188
  }
4142
4189
  ];
4143
4190
  function compactJson(value) {
@@ -4324,6 +4371,54 @@ function renderEventWorkerVerifierWorkflow(input) {
4324
4371
  ]
4325
4372
  };
4326
4373
  }
4374
+ function renderBoundedAgentWorkerVerifierWorkflow(input) {
4375
+ if (!input.objective?.trim())
4376
+ throw new Error("objective is required");
4377
+ if (!input.projectPath?.trim())
4378
+ throw new Error("projectPath is required");
4379
+ const seed = `${input.projectPath}:${input.objective}`;
4380
+ const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) ? input.timeoutMs : 45 * 60000;
4381
+ const workerPrompt = [
4382
+ `/goal ${input.objective}`,
4383
+ "",
4384
+ "You are the worker step for a bounded OpenLoops agent workflow.",
4385
+ "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.",
4386
+ "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.",
4387
+ input.prompt ? "" : undefined,
4388
+ input.prompt
4389
+ ].filter(Boolean).join(`
4390
+ `);
4391
+ const verifierPrompt = [
4392
+ `/goal Adversarially verify: ${input.objective}`,
4393
+ "",
4394
+ "You are the verifier step for a bounded OpenLoops agent workflow.",
4395
+ "Use fresh context. Review the worker result for correctness, regressions, missing tests, safety, runaway-agent risk, output bounds, and incomplete evidence.",
4396
+ "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."
4397
+ ].join(`
4398
+ `);
4399
+ return {
4400
+ name: input.name ?? `bounded-agent-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
4401
+ description: `Bounded worker/verifier workflow for ${input.objective.slice(0, 180)}`,
4402
+ version: 1,
4403
+ steps: [
4404
+ {
4405
+ id: "worker",
4406
+ name: "Worker",
4407
+ description: "Execute the bounded objective and record evidence.",
4408
+ target: agentTarget(input, workerPrompt, "worker", seed),
4409
+ timeoutMs
4410
+ },
4411
+ {
4412
+ id: "verifier",
4413
+ name: "Verifier",
4414
+ description: "Adversarially verify the bounded objective result.",
4415
+ dependsOn: ["worker"],
4416
+ target: agentTarget(input, verifierPrompt, "verifier", seed),
4417
+ timeoutMs: Math.min(timeoutMs, 30 * 60000)
4418
+ }
4419
+ ]
4420
+ };
4421
+ }
4327
4422
  function renderLoopTemplate(id, values) {
4328
4423
  if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
4329
4424
  return renderTodosTaskWorkerVerifierWorkflow({
@@ -4370,6 +4465,27 @@ function renderLoopTemplate(id, values) {
4370
4465
  sandbox: values.sandbox
4371
4466
  });
4372
4467
  }
4468
+ if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
4469
+ return renderBoundedAgentWorkerVerifierWorkflow({
4470
+ name: values.name,
4471
+ objective: values.objective ?? "",
4472
+ prompt: values.prompt,
4473
+ projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4474
+ provider: values.provider,
4475
+ authProfile: values.authProfile,
4476
+ authProfilePool: listVar(values.authProfilePool),
4477
+ workerAuthProfile: values.workerAuthProfile,
4478
+ verifierAuthProfile: values.verifierAuthProfile,
4479
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4480
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4481
+ model: values.model,
4482
+ variant: values.variant,
4483
+ agent: values.agent,
4484
+ permissionMode: values.permissionMode,
4485
+ sandbox: values.sandbox,
4486
+ timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
4487
+ });
4488
+ }
4373
4489
  throw new Error(`unknown template: ${id}`);
4374
4490
  }
4375
4491
  function listVar(value) {
@@ -4619,6 +4735,7 @@ function runDoctor(store) {
4619
4735
  // src/lib/health.ts
4620
4736
  import { createHash } from "crypto";
4621
4737
  var EVIDENCE_CHARS = 2000;
4738
+ var FINGERPRINT_EVIDENCE_CHARS = 120;
4622
4739
  var CLASSIFICATIONS = [
4623
4740
  "rate_limit",
4624
4741
  "auth",
@@ -4647,6 +4764,15 @@ function stableFingerprint(parts) {
4647
4764
  return createHash("sha256").update(parts.join(`
4648
4765
  `)).digest("hex").slice(0, 16);
4649
4766
  }
4767
+ function stableFailureFingerprint(run, classification) {
4768
+ return stableFingerprint([
4769
+ run.loopId,
4770
+ classification,
4771
+ String(run.status),
4772
+ String(run.exitCode ?? ""),
4773
+ (run.error ?? run.stderr ?? run.stdout ?? "").replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
4774
+ ]);
4775
+ }
4650
4776
  function healthRun(run) {
4651
4777
  return {
4652
4778
  ...run,
@@ -4680,14 +4806,7 @@ function classifyRunFailure(run) {
4680
4806
  classification = "sigsegv";
4681
4807
  return {
4682
4808
  classification,
4683
- fingerprint: stableFingerprint([
4684
- run.loopId,
4685
- run.loopName,
4686
- run.status,
4687
- classification,
4688
- String(run.exitCode ?? ""),
4689
- (run.error ?? run.stderr ?? run.stdout ?? "").slice(0, 500)
4690
- ]),
4809
+ fingerprint: stableFailureFingerprint(run, classification),
4691
4810
  evidence: {
4692
4811
  error: bounded(run.error),
4693
4812
  stdout: bounded(run.stdout),
@@ -4803,7 +4922,7 @@ function expectationForLoop(store, loop) {
4803
4922
  };
4804
4923
  }
4805
4924
  function buildHealthReport(store, opts = {}) {
4806
- const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
4925
+ const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 }).filter((loop) => opts.includeInactive || loop.status === "active" || loop.status === "paused");
4807
4926
  const expectations = loops2.map((loop) => expectationForLoop(store, loop));
4808
4927
  const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
4809
4928
  for (const expectation of expectations) {
@@ -4825,6 +4944,242 @@ function buildHealthReport(store, opts = {}) {
4825
4944
  expectations
4826
4945
  };
4827
4946
  }
4947
+ // src/lib/hygiene.ts
4948
+ import { basename } from "path";
4949
+ var PROVIDER_TOKENS = new Set([
4950
+ "codewith",
4951
+ "claude",
4952
+ "command",
4953
+ "tmux",
4954
+ "codex",
4955
+ "cursor",
4956
+ "opencode",
4957
+ "aicopilot",
4958
+ "agent"
4959
+ ]);
4960
+ var REPO_GENERIC_TOKENS = new Set(["repo", "repoops"]);
4961
+ function slugify(value) {
4962
+ return value.normalize("NFKD").replace(/[^\w\s.-]/g, "-").replace(/[_\s.:/]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
4963
+ }
4964
+ function repoSlugFromCwd(cwd) {
4965
+ if (!cwd || cwd === process.env.HOME || cwd === "/home/hasna")
4966
+ return "";
4967
+ if (cwd.includes("/.hasna/loops/"))
4968
+ return "";
4969
+ return slugify(basename(cwd));
4970
+ }
4971
+ function scopeForLoop(loop) {
4972
+ const cwd = loop.target.type === "command" || loop.target.type === "agent" ? loop.target.cwd : undefined;
4973
+ const repoSlug = repoSlugFromCwd(cwd);
4974
+ if (repoSlug)
4975
+ return { scope: "repo", prefix: `repo-${repoSlug}`, scopeSlug: repoSlug };
4976
+ return { scope: "machine", prefix: "machine", scopeSlug: "machine" };
4977
+ }
4978
+ function taskSlug(loop, scope) {
4979
+ const oldName = loop.name;
4980
+ let nameForParsing = oldName;
4981
+ if (!oldName.includes(":")) {
4982
+ const slug = slugify(oldName);
4983
+ if (scope.scope === "machine" && slug.startsWith("machine-"))
4984
+ nameForParsing = slug.slice("machine-".length);
4985
+ else if (scope.scope === "repo" && slug.startsWith(`repo-${scope.scopeSlug}-`)) {
4986
+ nameForParsing = slug.slice(`repo-${scope.scopeSlug}-`.length);
4987
+ } else
4988
+ nameForParsing = slug;
4989
+ }
4990
+ const parts = [];
4991
+ for (const rawPart of oldName.includes(":") ? oldName.split(":") : [nameForParsing]) {
4992
+ const part = slugify(rawPart);
4993
+ if (!part)
4994
+ continue;
4995
+ if (PROVIDER_TOKENS.has(part) || /^account\d+$/.test(part))
4996
+ continue;
4997
+ if (scope.scope === "repo" && REPO_GENERIC_TOKENS.has(part))
4998
+ continue;
4999
+ let normalized = part;
5000
+ if (scope.scope === "repo" && normalized === scope.scopeSlug)
5001
+ continue;
5002
+ if (scope.scope === "repo" && normalized.startsWith(`${scope.scopeSlug}-`)) {
5003
+ normalized = normalized.slice(scope.scopeSlug.length + 1);
5004
+ }
5005
+ if (normalized)
5006
+ parts.push(normalized);
5007
+ }
5008
+ const deduped = [];
5009
+ for (const token of parts.join("-").split("-").filter(Boolean)) {
5010
+ if (deduped[deduped.length - 1] !== token)
5011
+ deduped.push(token);
5012
+ }
5013
+ return deduped.join("-") || "loop";
5014
+ }
5015
+ function canonicalName(loop) {
5016
+ const scope = scopeForLoop(loop);
5017
+ let name = `${scope.prefix}-${taskSlug(loop, scope)}`.replace(/-+/g, "-").replace(/^-|-$/g, "");
5018
+ if (name.length > 120)
5019
+ name = `${name.slice(0, 111).replace(/-+$/g, "")}-${loop.id.slice(0, 8)}`;
5020
+ return {
5021
+ id: loop.id,
5022
+ status: loop.status,
5023
+ scope: scope.scope,
5024
+ scopeSlug: scope.scopeSlug,
5025
+ newName: name
5026
+ };
5027
+ }
5028
+ function ensureUnique(changes, existingNames = []) {
5029
+ const oldNames = new Set(changes.map((change) => change.oldName));
5030
+ const used = new Set([...existingNames].filter((name) => !oldNames.has(name)));
5031
+ for (const change of changes) {
5032
+ let candidate = change.newName;
5033
+ if (!used.has(candidate)) {
5034
+ used.add(candidate);
5035
+ change.newName = candidate;
5036
+ change.changed = change.oldName !== candidate;
5037
+ continue;
5038
+ }
5039
+ const base = candidate.slice(0, 111).replace(/-+$/g, "");
5040
+ candidate = `${base}-${change.id.slice(0, 8)}`;
5041
+ let suffix = 2;
5042
+ while (used.has(candidate)) {
5043
+ const extra = `-${change.id.slice(0, 6)}-${suffix++}`;
5044
+ candidate = `${base.slice(0, 120 - extra.length)}${extra}`;
5045
+ }
5046
+ used.add(candidate);
5047
+ change.newName = candidate;
5048
+ change.changed = change.oldName !== candidate;
5049
+ }
5050
+ }
5051
+ function managedLoops(store, opts) {
5052
+ const loops2 = store.listLoops({ includeArchived: Boolean(opts.includeInactive), limit: opts.limit ?? 1000 });
5053
+ if (opts.includeInactive)
5054
+ return loops2;
5055
+ if (opts.includeStopped)
5056
+ return loops2.filter((loop) => loop.status !== "expired");
5057
+ return loops2.filter((loop) => loop.status === "active" || loop.status === "paused");
5058
+ }
5059
+ function buildNameHygieneReport(store, opts = {}) {
5060
+ const allLoops = store.listLoops({ includeArchived: true, limit: 1e4 });
5061
+ const changes = managedLoops(store, opts).map((loop) => {
5062
+ const canonical = canonicalName(loop);
5063
+ return {
5064
+ ...canonical,
5065
+ oldName: loop.name,
5066
+ changed: loop.name !== canonical.newName
5067
+ };
5068
+ });
5069
+ ensureUnique(changes, allLoops.map((loop) => loop.name));
5070
+ const changed = changes.filter((change) => change.changed);
5071
+ const conflicts = changes.filter((change) => allLoops.some((loop) => loop.name === change.newName && loop.id !== change.id));
5072
+ if (opts.apply) {
5073
+ for (const change of changed)
5074
+ store.renameLoop(change.id, change.newName);
5075
+ }
5076
+ return {
5077
+ ok: changed.length === 0,
5078
+ generatedAt: new Date().toISOString(),
5079
+ applied: Boolean(opts.apply),
5080
+ checked: changes.length,
5081
+ changed: changed.length,
5082
+ changes,
5083
+ conflicts
5084
+ };
5085
+ }
5086
+ function baseName(name) {
5087
+ return name.replace(/-(bounded|compact|native)?-?low(?:-\d+m)?$/g, "").replace(/-\d+[mhd]$/g, "").replace(/-(bounded|compact)$/g, "");
5088
+ }
5089
+ function scheduleKey(schedule) {
5090
+ if (schedule.type === "cron")
5091
+ return `cron:${schedule.expression}`;
5092
+ if (schedule.type === "interval")
5093
+ return `interval:${schedule.everyMs}`;
5094
+ if (schedule.type === "once")
5095
+ return `once:${schedule.at}`;
5096
+ return `dynamic:${schedule.minIntervalMs ?? ""}`;
5097
+ }
5098
+ function targetCwd(loop) {
5099
+ return loop.target.type === "command" || loop.target.type === "agent" ? loop.target.cwd ?? "" : "";
5100
+ }
5101
+ function buildDuplicateOverlapReport(store, opts = {}) {
5102
+ const loops2 = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
5103
+ const groups = new Map;
5104
+ for (const loop of loops2) {
5105
+ const base = baseName(loop.name);
5106
+ const cwd = targetCwd(loop) || undefined;
5107
+ const schedule = scheduleKey(loop.schedule);
5108
+ const key = `${base}|${cwd ?? ""}|${schedule}`;
5109
+ const existing = groups.get(key) ?? { baseName: base, cwd, schedule, loops: [] };
5110
+ existing.loops.push(loop);
5111
+ groups.set(key, existing);
5112
+ }
5113
+ const duplicateGroups = [...groups.entries()].filter(([, group]) => group.loops.length > 1).map(([key, group]) => ({
5114
+ key,
5115
+ baseName: group.baseName,
5116
+ cwd: group.cwd,
5117
+ schedule: group.schedule,
5118
+ loops: group.loops.map((loop) => ({
5119
+ id: loop.id,
5120
+ name: loop.name,
5121
+ status: loop.status,
5122
+ nextRunAt: loop.nextRunAt
5123
+ }))
5124
+ }));
5125
+ return {
5126
+ ok: duplicateGroups.length === 0,
5127
+ generatedAt: new Date().toISOString(),
5128
+ checked: loops2.length,
5129
+ groups: duplicateGroups
5130
+ };
5131
+ }
5132
+ function commandText(loop) {
5133
+ if (loop.target.type !== "command")
5134
+ return "";
5135
+ return [loop.target.command, ...loop.target.args ?? []].join(" ");
5136
+ }
5137
+ function scriptNeedles(scriptsDir) {
5138
+ const home = process.env.HOME ?? "/home/hasna";
5139
+ const normalized = scriptsDir.replace(/\/+$/g, "");
5140
+ const values = [
5141
+ normalized,
5142
+ `${normalized}/`,
5143
+ "~/.hasna/loops/scripts",
5144
+ "~/.hasna/loops/scripts/",
5145
+ "$HOME/.hasna/loops/scripts",
5146
+ "$HOME/.hasna/loops/scripts/",
5147
+ "${HOME}/.hasna/loops/scripts",
5148
+ "${HOME}/.hasna/loops/scripts/",
5149
+ `${home}/.hasna/loops/scripts`,
5150
+ `${home}/.hasna/loops/scripts/`,
5151
+ "/.hasna/loops/scripts/"
5152
+ ];
5153
+ return [...new Set(values)];
5154
+ }
5155
+ function buildScriptInventoryReport(store, opts = {}) {
5156
+ const scriptsDir = opts.scriptsDir ?? `${process.env.HOME ?? "/home/hasna"}/.hasna/loops/scripts`;
5157
+ const needles = scriptNeedles(scriptsDir);
5158
+ const loops2 = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
5159
+ const scriptBacked = loops2.map((loop) => {
5160
+ const text = commandText(loop);
5161
+ if (!text)
5162
+ return;
5163
+ const matches = needles.filter((needle) => text.includes(needle));
5164
+ if (!matches.length)
5165
+ return;
5166
+ return {
5167
+ id: loop.id,
5168
+ name: loop.name,
5169
+ status: loop.status,
5170
+ cwd: targetCwd(loop) || undefined,
5171
+ command: text.length > 500 ? `${text.slice(0, 500)}...` : text,
5172
+ scriptMatches: [...new Set(matches)]
5173
+ };
5174
+ }).filter((value) => Boolean(value));
5175
+ return {
5176
+ ok: scriptBacked.length === 0,
5177
+ generatedAt: new Date().toISOString(),
5178
+ checked: loops2.length,
5179
+ scriptBacked: scriptBacked.length,
5180
+ loops: scriptBacked
5181
+ };
5182
+ }
4828
5183
  export {
4829
5184
  workflowExecutionOrder,
4830
5185
  workflowBodyFromJson,
@@ -4837,6 +5192,7 @@ export {
4837
5192
  renderTodosTaskWorkerVerifierWorkflow,
4838
5193
  renderLoopTemplate,
4839
5194
  renderEventWorkerVerifierWorkflow,
5195
+ renderBoundedAgentWorkerVerifierWorkflow,
4840
5196
  refreshLoopMachine,
4841
5197
  readyNodeKeys,
4842
5198
  preflightWorkflow,
@@ -4857,9 +5213,13 @@ export {
4857
5213
  executeLoop,
4858
5214
  computeNextAfter,
4859
5215
  classifyRunFailure,
5216
+ buildScriptInventoryReport,
5217
+ buildNameHygieneReport,
4860
5218
  buildHealthReport,
5219
+ buildDuplicateOverlapReport,
4861
5220
  TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
4862
5221
  Store,
4863
5222
  LoopsClient,
4864
- EVENT_WORKER_VERIFIER_TEMPLATE_ID
5223
+ EVENT_WORKER_VERIFIER_TEMPLATE_ID,
5224
+ BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID
4865
5225
  };
@@ -66,5 +66,6 @@ export declare function classifyRunFailure(run: LoopRun): RunFailureSignal | und
66
66
  export declare function expectationForLoop(store: Store, loop: Loop): LoopExpectationResult;
67
67
  export declare function buildHealthReport(store: Store, opts?: {
68
68
  includeArchived?: boolean;
69
+ includeInactive?: boolean;
69
70
  limit?: number;
70
71
  }): LoopsHealthReport;
@@ -0,0 +1,63 @@
1
+ import type { Loop } from "../types.js";
2
+ import type { Store } from "./store.js";
3
+ export interface NameHygieneChange {
4
+ id: string;
5
+ status: string;
6
+ scope: "machine" | "repo";
7
+ scopeSlug: string;
8
+ oldName: string;
9
+ newName: string;
10
+ changed: boolean;
11
+ }
12
+ export interface NameHygieneReport {
13
+ ok: boolean;
14
+ generatedAt: string;
15
+ applied: boolean;
16
+ checked: number;
17
+ changed: number;
18
+ changes: NameHygieneChange[];
19
+ conflicts: NameHygieneChange[];
20
+ }
21
+ export interface DuplicateOverlapGroup {
22
+ key: string;
23
+ baseName: string;
24
+ cwd?: string;
25
+ schedule: string;
26
+ loops: Array<Pick<Loop, "id" | "name" | "status" | "nextRunAt">>;
27
+ }
28
+ export interface DuplicateOverlapReport {
29
+ ok: boolean;
30
+ generatedAt: string;
31
+ checked: number;
32
+ groups: DuplicateOverlapGroup[];
33
+ }
34
+ export interface ScriptBackedLoop {
35
+ id: string;
36
+ name: string;
37
+ status: string;
38
+ cwd?: string;
39
+ command: string;
40
+ scriptMatches: string[];
41
+ }
42
+ export interface ScriptInventoryReport {
43
+ ok: boolean;
44
+ generatedAt: string;
45
+ checked: number;
46
+ scriptBacked: number;
47
+ loops: ScriptBackedLoop[];
48
+ }
49
+ export declare function buildNameHygieneReport(store: Store, opts?: {
50
+ apply?: boolean;
51
+ includeStopped?: boolean;
52
+ includeInactive?: boolean;
53
+ limit?: number;
54
+ }): NameHygieneReport;
55
+ export declare function buildDuplicateOverlapReport(store: Store, opts?: {
56
+ includeInactive?: boolean;
57
+ limit?: number;
58
+ }): DuplicateOverlapReport;
59
+ export declare function buildScriptInventoryReport(store: Store, opts?: {
60
+ scriptsDir?: string;
61
+ includeInactive?: boolean;
62
+ limit?: number;
63
+ }): ScriptInventoryReport;
@@ -79,6 +79,7 @@ export declare class Store {
79
79
  }): Loop[];
80
80
  dueLoops(now: Date): Loop[];
81
81
  updateLoop(id: string, patch: Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor" | "expiresAt">>, opts?: DaemonLeaseFence): Loop;
82
+ renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
82
83
  archiveLoop(idOrName: string): Loop;
83
84
  unarchiveLoop(idOrName: string): Loop;
84
85
  deleteLoop(idOrName: string): boolean;
package/dist/lib/store.js CHANGED
@@ -1050,6 +1050,30 @@ class Store {
1050
1050
  throw new Error(`loop not found after update: ${id}`);
1051
1051
  return after;
1052
1052
  }
1053
+ renameLoop(id, name, opts = {}) {
1054
+ const current = this.getLoop(id);
1055
+ if (!current)
1056
+ throw new Error(`loop not found: ${id}`);
1057
+ const trimmed = name.trim();
1058
+ if (!trimmed)
1059
+ throw new Error("loop name must not be empty");
1060
+ const updated = (opts.now ?? new Date).toISOString();
1061
+ this.db.query(`UPDATE loops SET name=$name, updated_at=$updated
1062
+ WHERE id=$id
1063
+ AND ($daemonLeaseId IS NULL OR EXISTS (
1064
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
1065
+ ))`).run({
1066
+ $id: id,
1067
+ $name: trimmed,
1068
+ $updated: updated,
1069
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
1070
+ $now: updated
1071
+ });
1072
+ const after = this.getLoop(id);
1073
+ if (!after)
1074
+ throw new Error(`loop not found after rename: ${id}`);
1075
+ return after;
1076
+ }
1053
1077
  archiveLoop(idOrName) {
1054
1078
  const loop = this.requireLoop(idOrName);
1055
1079
  if (loop.archivedAt)
@@ -1,6 +1,7 @@
1
1
  import type { AccountRef, AgentPermissionMode, AgentProvider, AgentSandbox, CreateWorkflowInput, LoopTemplateSummary } from "../types.js";
2
2
  export declare const TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
3
3
  export declare const EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4
+ export declare const BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
4
5
  export interface TodosTaskWorkflowTemplateInput {
5
6
  taskId: string;
6
7
  taskTitle?: string;
@@ -46,8 +47,30 @@ export interface EventWorkflowTemplateInput {
46
47
  permissionMode?: AgentPermissionMode;
47
48
  sandbox?: AgentSandbox;
48
49
  }
50
+ export interface BoundedAgentWorkflowTemplateInput {
51
+ name?: string;
52
+ objective: string;
53
+ prompt?: string;
54
+ projectPath: string;
55
+ provider?: AgentProvider;
56
+ authProfile?: string;
57
+ authProfilePool?: string[];
58
+ workerAuthProfile?: string;
59
+ verifierAuthProfile?: string;
60
+ account?: AccountRef;
61
+ accountPool?: AccountRef[];
62
+ workerAccount?: AccountRef;
63
+ verifierAccount?: AccountRef;
64
+ model?: string;
65
+ variant?: string;
66
+ agent?: string;
67
+ permissionMode?: AgentPermissionMode;
68
+ sandbox?: AgentSandbox;
69
+ timeoutMs?: number;
70
+ }
49
71
  export declare function listLoopTemplates(): LoopTemplateSummary[];
50
72
  export declare function getLoopTemplate(id: string): LoopTemplateSummary | undefined;
51
73
  export declare function renderTodosTaskWorkerVerifierWorkflow(input: TodosTaskWorkflowTemplateInput): CreateWorkflowInput;
52
74
  export declare function renderEventWorkerVerifierWorkflow(input: EventWorkflowTemplateInput): CreateWorkflowInput;
75
+ export declare function renderBoundedAgentWorkerVerifierWorkflow(input: BoundedAgentWorkflowTemplateInput): CreateWorkflowInput;
53
76
  export declare function renderLoopTemplate(id: string, values: Record<string, string | undefined>): CreateWorkflowInput;