@hasna/loops 0.3.28 → 0.3.29

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 CHANGED
@@ -5253,7 +5253,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5253
5253
  // package.json
5254
5254
  var package_default = {
5255
5255
  name: "@hasna/loops",
5256
- version: "0.3.28",
5256
+ version: "0.3.29",
5257
5257
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5258
5258
  type: "module",
5259
5259
  main: "dist/index.js",
@@ -6852,6 +6852,10 @@ function taskListId(task) {
6852
6852
  function taskProjectId(task) {
6853
6853
  return taskField(task, ["project_id", "projectId"]);
6854
6854
  }
6855
+ function taskProjectPath(task) {
6856
+ const metadata = objectField(task.metadata) ?? {};
6857
+ return taskField(task, ["working_dir", "workingDir", "project_path", "projectPath", "cwd"]) ?? taskEventField(metadata, ["working_dir", "workingDir", "project_path", "projectPath", "project_canonical_path", "cwd"]);
6858
+ }
6855
6859
  function taskDrainEvent(task) {
6856
6860
  const taskId = taskField(task, ["id", "task_id", "taskId"]);
6857
6861
  if (!taskId)
@@ -6917,6 +6921,15 @@ function taskMatchesDrainFilters(task, filters) {
6917
6921
  return false;
6918
6922
  if (filters.taskListId && taskListId(task) !== filters.taskListId)
6919
6923
  return false;
6924
+ if (filters.projectPathPrefix) {
6925
+ const path = taskProjectPath(task);
6926
+ if (!path)
6927
+ return false;
6928
+ const normalizedPath = normalizeRoutePath(path) ?? resolve2(path);
6929
+ const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve2(filters.projectPathPrefix);
6930
+ if (normalizedPath !== normalizedPrefix && !normalizedPath.startsWith(`${normalizedPrefix}/`))
6931
+ return false;
6932
+ }
6920
6933
  if (filters.tags.length) {
6921
6934
  const taskTags = new Set(tagsFromValue(task.tags));
6922
6935
  for (const tag of filters.tags) {
@@ -7082,19 +7095,20 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
7082
7095
  print(result.value, result.human);
7083
7096
  });
7084
7097
  var eventsDrain = events.command("drain").description("drain durable source queues into bounded OpenLoops workflows");
7085
- eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops").option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything").action((opts) => {
7098
+ eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops").option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything").action((opts) => {
7086
7099
  const maxDispatch = positiveInteger(opts.maxDispatch ?? "1", "--max-dispatch") ?? 1;
7087
7100
  const todosProject = opts.todosProject ?? defaultLoopsProject();
7088
7101
  const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
7089
7102
  const taskListFilter = resolveTaskListFilter(todosProject, opts.taskList);
7090
7103
  const candidateLimit = positiveInteger(opts.limit ?? "50", "--limit") ?? 50;
7091
- const hasPostFilters = Boolean(opts.todosProjectId || taskListFilter || requiredTags.length);
7104
+ const hasPostFilters = Boolean(opts.todosProjectId || taskListFilter || opts.projectPathPrefix || requiredTags.length);
7092
7105
  const defaultScanLimit = hasPostFilters ? Math.max(candidateLimit, 500) : candidateLimit;
7093
7106
  const scanLimit = positiveInteger(opts.scanLimit ?? String(defaultScanLimit), "--scan-limit") ?? defaultScanLimit;
7094
7107
  const ready = loadReadyTodosTasks(opts, scanLimit);
7095
7108
  const filteredCandidates = ready.filter((task) => taskMatchesDrainFilters(task, {
7096
7109
  projectId: opts.todosProjectId,
7097
7110
  taskListId: taskListFilter,
7111
+ projectPathPrefix: opts.projectPathPrefix,
7098
7112
  tags: requiredTags
7099
7113
  }));
7100
7114
  const candidates = filteredCandidates.slice(0, candidateLimit);
@@ -7117,6 +7131,7 @@ eventsDrain.command("todos-task").description("drain ready todos tasks into boun
7117
7131
  todosProjectId: opts.todosProjectId,
7118
7132
  taskList: opts.taskList,
7119
7133
  taskListId: taskListFilter,
7134
+ projectPathPrefix: opts.projectPathPrefix,
7120
7135
  tags: requiredTags,
7121
7136
  limit: candidateLimit,
7122
7137
  scanLimit,
@@ -4574,7 +4574,7 @@ function enableStartup(result) {
4574
4574
  // package.json
4575
4575
  var package_default = {
4576
4576
  name: "@hasna/loops",
4577
- version: "0.3.28",
4577
+ version: "0.3.29",
4578
4578
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4579
4579
  type: "module",
4580
4580
  main: "dist/index.js",
package/docs/USAGE.md CHANGED
@@ -320,6 +320,7 @@ loops events drain todos-task \
320
320
  --todos-project /home/hasna/.hasna/loops \
321
321
  --task-list repoops-pr-queue \
322
322
  --tags auto:route \
323
+ --project-path-prefix /home/hasna/workspace/hasna/opensource \
323
324
  --provider codewith \
324
325
  --auth-profile-pool account004,account005,account006 \
325
326
  --project-group oss \
@@ -334,14 +335,16 @@ loops events drain todos-task \
334
335
 
335
336
  `--max-dispatch` caps new workflow-loop creation per drain run. `--limit` caps
336
337
  filtered ready-task candidates, while `--scan-limit` controls how many raw
337
- `todos ready` rows are fetched before filters. When `--task-list`, `--tags`, or
338
- `--todos-project-id` are set, the default scan limit is raised to 500 so a busy
339
- shared queue is less likely to starve project-specific drains. The route
340
- throttle flags are still checked for every candidate, so a drain can safely run
341
- every few minutes as a deterministic command loop: it fills only available
342
- capacity, writes compact JSON evidence when requested, and leaves excess ready
343
- tasks in todos for a later drain pass. Use `--dry-run` to preview candidates and
344
- rendered workflows without mutating OpenLoops state.
338
+ `todos ready` rows are fetched before filters. Use `--task-list`, `--tags`,
339
+ `--todos-project-id`, and `--project-path-prefix` to keep each drain aligned
340
+ with the route/name-prefix it services. When any of those filters are set, the
341
+ default scan limit is raised to 500 so a busy shared queue is less likely to
342
+ starve project-specific drains. The route throttle flags are still checked for
343
+ every candidate, so a drain can safely run every few minutes as a deterministic
344
+ command loop: it fills only available capacity, writes compact JSON evidence
345
+ when requested, and leaves excess ready tasks in todos for a later drain pass.
346
+ Use `--dry-run` to preview candidates and rendered workflows without mutating
347
+ OpenLoops state.
345
348
 
346
349
  For other Hasna apps that expose `@hasna/events` webhooks, use the generic
347
350
  handler:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",