@hasna/loops 0.3.24 → 0.3.26

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/README.md CHANGED
@@ -318,6 +318,16 @@ mutating path: it upserts deduped Todos tasks for failed expectations and marks
318
318
  them with `no_tmux_dispatch=true` metadata. Use `--dry-run --json` before
319
319
  turning it into a production loop.
320
320
 
321
+ Add `--evidence-dir <dir>` to `health route-tasks` or `hygiene route-tasks`
322
+ when the deterministic loop should write a durable JSON heartbeat/report in
323
+ addition to loop stdout. Add `--auto-route` only for task lists that are
324
+ intentionally connected to task-created worker/verifier automation; it appends
325
+ `auto:route`, sets route metadata, and lets OpenTodos/OpenEvents trigger the
326
+ existing headless workflow path when the finding has a cwd or
327
+ `--route-project-path` is provided. Findings with no routeable working
328
+ directory stay as tasks and record an `auto_route_skipped_reason`. Without
329
+ `--auto-route`, the command only creates or updates deduped tasks.
330
+
321
331
  `hygiene names` reports canonical `machine-*` or `repo-<name>-*` loop names and
322
332
  renames only with `--apply`. Apply mode writes a SQLite backup under
323
333
  `<LOOPS_DATA_DIR>/backups` before changing loop names. `hygiene duplicates`
package/dist/cli/index.js CHANGED
@@ -2195,7 +2195,7 @@ class Store {
2195
2195
  }
2196
2196
 
2197
2197
  // src/cli/index.ts
2198
- import { createHash as createHash2 } from "crypto";
2198
+ import { createHash as createHash2, randomUUID } from "crypto";
2199
2199
  import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2200
2200
  import { spawnSync as spawnSync5 } from "child_process";
2201
2201
  import { join as join3 } from "path";
@@ -4770,6 +4770,9 @@ function bounded(value, limit = EVIDENCE_CHARS) {
4770
4770
  return `${value.slice(0, limit)}
4771
4771
  [truncated ${value.length - limit} chars]`;
4772
4772
  }
4773
+ function redactedEvidence(value) {
4774
+ return redact(bounded(value));
4775
+ }
4773
4776
  function searchableText(run) {
4774
4777
  return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
4775
4778
  `).toLowerCase();
@@ -4790,9 +4793,9 @@ function stableFailureFingerprint(run, classification) {
4790
4793
  function healthRun(run) {
4791
4794
  return {
4792
4795
  ...run,
4793
- error: bounded(run.error),
4794
- stdout: bounded(run.stdout),
4795
- stderr: bounded(run.stderr)
4796
+ error: redactedEvidence(run.error),
4797
+ stdout: redactedEvidence(run.stdout),
4798
+ stderr: redactedEvidence(run.stderr)
4796
4799
  };
4797
4800
  }
4798
4801
  function classifyRunFailure(run) {
@@ -4824,9 +4827,9 @@ function classifyRunFailure(run) {
4824
4827
  classification,
4825
4828
  fingerprint: stableFailureFingerprint(run, classification),
4826
4829
  evidence: {
4827
- error: bounded(run.error),
4828
- stdout: bounded(run.stdout),
4829
- stderr: bounded(run.stderr),
4830
+ error: redactedEvidence(run.error),
4831
+ stdout: redactedEvidence(run.stdout),
4832
+ stderr: redactedEvidence(run.stderr),
4830
4833
  exitCode: run.exitCode
4831
4834
  }
4832
4835
  };
@@ -5201,7 +5204,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5201
5204
  // package.json
5202
5205
  var package_default = {
5203
5206
  name: "@hasna/loops",
5204
- version: "0.3.24",
5207
+ version: "0.3.26",
5205
5208
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5206
5209
  type: "module",
5207
5210
  main: "dist/index.js",
@@ -5398,8 +5401,17 @@ function accountForRole(input, role, seed) {
5398
5401
  return input.verifierAccount;
5399
5402
  return rolePoolValue(input.accountPool, seed, role) ?? input.account;
5400
5403
  }
5404
+ function assertNativeAuthProfileSupport(input, provider) {
5405
+ if (provider === "codewith")
5406
+ return;
5407
+ const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.workerAuthProfile || input.verifierAuthProfile);
5408
+ if (!hasNativeAuthProfiles)
5409
+ return;
5410
+ throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
5411
+ }
5401
5412
  function agentTarget(input, prompt, role, seed) {
5402
5413
  const provider = input.provider ?? "codewith";
5414
+ assertNativeAuthProfileSupport(input, provider);
5403
5415
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
5404
5416
  return {
5405
5417
  type: "agent",
@@ -5946,6 +5958,15 @@ function writeRouteCursor(key, lastFingerprint) {
5946
5958
  cursors[key] = { lastFingerprint, updatedAt: new Date().toISOString() };
5947
5959
  writeFileSync3(routeCursorsPath(), JSON.stringify(cursors, null, 2), { mode: 384 });
5948
5960
  }
5961
+ function writeRouteEvidence(kind, value, evidenceDir) {
5962
+ if (!evidenceDir)
5963
+ return;
5964
+ mkdirSync5(evidenceDir, { recursive: true, mode: 448 });
5965
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\./g, "");
5966
+ const evidencePath = join3(evidenceDir, `${kind}-${stamp}-${randomUUID().slice(0, 8)}.json`);
5967
+ writeFileSync3(evidencePath, JSON.stringify(value, null, 2), { mode: 384, flag: "wx" });
5968
+ return evidencePath;
5969
+ }
5949
5970
  function selectRouteItems(items, maxActions, cursorKey, fingerprintOf) {
5950
5971
  const total = items.length;
5951
5972
  const boundedMax = Math.max(0, Math.floor(Number.isFinite(maxActions) ? maxActions : 0));
@@ -5972,6 +5993,77 @@ function selectRouteItems(items, maxActions, cursorKey, fingerprintOf) {
5972
5993
  }
5973
5994
  };
5974
5995
  }
5996
+ function taskAutoRoute(tags, base, opts) {
5997
+ if (!opts.autoRoute) {
5998
+ return {
5999
+ tags,
6000
+ metadata: {
6001
+ ...base,
6002
+ route_enabled: false,
6003
+ project_path: null,
6004
+ working_dir: null,
6005
+ auto_route_requested: false,
6006
+ auto_route_enabled: false,
6007
+ automation: {
6008
+ allowed: false,
6009
+ source: opts.source,
6010
+ kind: "task-created-worker-verifier"
6011
+ }
6012
+ },
6013
+ autoRoute: { requested: false, enabled: false }
6014
+ };
6015
+ }
6016
+ const projectPath = typeof base.cwd === "string" && base.cwd.trim() || typeof opts.routeProjectPath === "string" && opts.routeProjectPath.trim() || undefined;
6017
+ if (!projectPath) {
6018
+ return {
6019
+ tags,
6020
+ metadata: {
6021
+ ...base,
6022
+ route_enabled: false,
6023
+ project_path: null,
6024
+ working_dir: null,
6025
+ auto_route_requested: true,
6026
+ auto_route_enabled: false,
6027
+ auto_route_skipped_reason: "missing cwd or --route-project-path",
6028
+ automation: {
6029
+ allowed: false,
6030
+ source: opts.source,
6031
+ kind: "task-created-worker-verifier"
6032
+ }
6033
+ },
6034
+ autoRoute: {
6035
+ requested: true,
6036
+ enabled: false,
6037
+ skippedReason: "missing cwd or --route-project-path"
6038
+ }
6039
+ };
6040
+ }
6041
+ return {
6042
+ tags: [...new Set([...tags, "auto:route"])],
6043
+ metadata: {
6044
+ ...base,
6045
+ route_enabled: true,
6046
+ project_path: projectPath,
6047
+ working_dir: projectPath,
6048
+ auto_route_requested: true,
6049
+ auto_route_enabled: true,
6050
+ automation: {
6051
+ allowed: true,
6052
+ source: opts.source,
6053
+ kind: "task-created-worker-verifier"
6054
+ }
6055
+ },
6056
+ autoRoute: { requested: true, enabled: true }
6057
+ };
6058
+ }
6059
+ function routeTaskWorkingDirArgs(routeTask) {
6060
+ const workingDir = routeTask.autoRoute.enabled && typeof routeTask.metadata.working_dir === "string" ? routeTask.metadata.working_dir : undefined;
6061
+ return workingDir ? ["--working-dir", workingDir] : [];
6062
+ }
6063
+ function routeCursorKey(kind, parts, opts) {
6064
+ const routeMode = opts.autoRoute ? ["auto-route", opts.routeProjectPath ?? "cwd"] : [];
6065
+ return `${kind}:${stableHash([...parts, ...routeMode])}`;
6066
+ }
5975
6067
  function eventData(event) {
5976
6068
  const data = event.data;
5977
6069
  if (data && typeof data === "object" && !Array.isArray(data))
@@ -6778,26 +6870,43 @@ var health = program.command("health").description("summarize loop health and la
6778
6870
  store.close();
6779
6871
  }
6780
6872
  });
6781
- health.command("route-tasks").description("upsert deduped todos tasks for failed loop health expectations").option("--project <path>", "todos project path", defaultLoopsProject()).option("--task-list <slug>", "todos task-list slug", "loop-error-self-heal").option("--limit <n>", "maximum loops to inspect", "200").option("--max-actions <n>", "maximum todos tasks to upsert", "5").option("--include-inactive", "also route stopped or expired loops").option("--dry-run", "print intended task upserts without mutating todos").option("--json", "print JSON").action((opts) => {
6873
+ health.command("route-tasks").description("upsert deduped todos tasks for failed loop health expectations").option("--project <path>", "todos project path", defaultLoopsProject()).option("--task-list <slug>", "todos task-list slug", "loop-error-self-heal").option("--limit <n>", "maximum loops to inspect", "200").option("--max-actions <n>", "maximum todos tasks to upsert", "5").option("--include-inactive", "also route stopped or expired loops").option("--auto-route", "opt routed tasks into task-created headless worker/verifier automation").option("--route-project-path <path>", "fallback project path for --auto-route when the failed loop has no cwd").option("--evidence-dir <path>", "write the route result JSON to this directory").option("--dry-run", "print intended task upserts without mutating todos").option("--json", "print JSON").action((opts) => {
6782
6874
  const store = new Store;
6783
6875
  try {
6784
6876
  const report = buildHealthReport(store, { limit: Number(opts.limit), includeInactive: Boolean(opts.includeInactive) });
6785
6877
  const failures = report.expectations.filter((entry) => !entry.ok && entry.recommendedTask);
6786
- const selection = selectRouteItems(failures, Number(opts.maxActions), `health:${stableHash([opts.project, opts.taskList, opts.limit, Boolean(opts.includeInactive)])}`, (expectation) => expectation.recommendedTask.dedupeKey);
6878
+ const selection = selectRouteItems(failures, Number(opts.maxActions), routeCursorKey("health", [opts.project, opts.taskList, opts.limit, Boolean(opts.includeInactive)], {
6879
+ autoRoute: Boolean(opts.autoRoute),
6880
+ routeProjectPath: opts.routeProjectPath
6881
+ }), (expectation) => expectation.recommendedTask.dedupeKey);
6787
6882
  const listId = opts.dryRun ? undefined : ensureTodosTaskList(opts.project, opts.taskList, "Loop Error Self Heal", "Deduped OpenLoops health expectation failures routed by loops health route-tasks.");
6788
6883
  const actions = selection.selected.map((expectation) => {
6789
6884
  const task = expectation.recommendedTask;
6790
- const metadata = {
6885
+ const routeTask = taskAutoRoute(task.tags, {
6791
6886
  source: "openloops.health.route-tasks",
6792
6887
  loop_id: expectation.loop.id,
6793
6888
  loop_name: expectation.loop.name,
6794
6889
  run_id: expectation.latestRun?.id,
6795
6890
  classification: expectation.failure?.classification,
6796
6891
  fingerprint: task.dedupeKey,
6892
+ cwd: expectation.route.cwd,
6893
+ provider: expectation.route.provider,
6797
6894
  no_tmux_dispatch: true
6798
- };
6895
+ }, {
6896
+ autoRoute: Boolean(opts.autoRoute),
6897
+ routeProjectPath: opts.routeProjectPath,
6898
+ source: "openloops.health.route-tasks"
6899
+ });
6799
6900
  if (opts.dryRun) {
6800
- return { action: "would-upsert", title: task.title, fingerprint: task.dedupeKey, priority: task.priority, metadata };
6901
+ return {
6902
+ action: "would-upsert",
6903
+ title: task.title,
6904
+ fingerprint: task.dedupeKey,
6905
+ priority: task.priority,
6906
+ tags: routeTask.tags,
6907
+ metadata: routeTask.metadata,
6908
+ autoRoute: routeTask.autoRoute
6909
+ };
6801
6910
  }
6802
6911
  const result = runLocalCommand("todos", [
6803
6912
  "--project",
@@ -6818,9 +6927,10 @@ health.command("route-tasks").description("upsert deduped todos tasks for failed
6818
6927
  "--list",
6819
6928
  listId,
6820
6929
  "--tags",
6821
- task.tags.join(","),
6930
+ routeTask.tags.join(","),
6931
+ ...routeTaskWorkingDirArgs(routeTask),
6822
6932
  "--metadata-json",
6823
- JSON.stringify(metadata)
6933
+ JSON.stringify(routeTask.metadata)
6824
6934
  ]);
6825
6935
  if (!result.ok) {
6826
6936
  return { action: "upsert-failed", fingerprint: task.dedupeKey, error: result.stderr || result.error || result.stdout };
@@ -6834,12 +6944,16 @@ health.command("route-tasks").description("upsert deduped todos tasks for failed
6834
6944
  routing: selection.cursor,
6835
6945
  actions
6836
6946
  };
6947
+ const evidencePath = writeRouteEvidence("health-route-tasks", routed, opts.evidenceDir);
6948
+ const output = evidencePath ? { ...routed, evidencePath } : routed;
6837
6949
  if (!opts.dryRun && routed.ok)
6838
6950
  writeRouteCursor(selection.cursor.key, selection.cursor.lastFingerprint);
6839
6951
  if (isJson() || opts.json)
6840
- console.log(JSON.stringify(routed, null, 2));
6952
+ console.log(JSON.stringify(output, null, 2));
6841
6953
  else {
6842
- console.log(`health_route_tasks inspected=${routed.inspected} failures=${routed.failures} actions=${actions.length}`);
6954
+ console.log(`health_route_tasks inspected=${output.inspected} failures=${output.failures} actions=${actions.length}`);
6955
+ if (evidencePath)
6956
+ console.log(`evidence=${evidencePath}`);
6843
6957
  for (const action of actions)
6844
6958
  console.log(`${action.action} ${action.fingerprint}`);
6845
6959
  }
@@ -7062,7 +7176,7 @@ hygiene.command("scripts").description("inventory loops still backed by local ~/
7062
7176
  store.close();
7063
7177
  }
7064
7178
  });
7065
- hygiene.command("route-tasks").description("upsert deduped todos tasks for hygiene findings").option("--checks <list>", "comma-separated hygiene checks: names,duplicates,scripts,all", "all").option("--project <path>", "todos project path", defaultLoopsProject()).option("--task-list <slug>", "todos task-list slug", "openloops-hygiene").option("--limit <n>", "maximum loops to inspect", "1000").option("--max-actions <n>", "maximum todos tasks to upsert", "10").option("--scripts-dir <path>", "script directory to detect for script inventory").option("--include-inactive", "also route stopped, expired, or archived loops").option("--dry-run", "print intended task upserts without mutating todos").option("--json", "print JSON").action((opts) => {
7179
+ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygiene findings").option("--checks <list>", "comma-separated hygiene checks: names,duplicates,scripts,all", "all").option("--project <path>", "todos project path", defaultLoopsProject()).option("--task-list <slug>", "todos task-list slug", "openloops-hygiene").option("--limit <n>", "maximum loops to inspect", "1000").option("--max-actions <n>", "maximum todos tasks to upsert", "10").option("--scripts-dir <path>", "script directory to detect for script inventory").option("--include-inactive", "also route stopped, expired, or archived loops").option("--auto-route", "opt routed tasks into task-created headless worker/verifier automation").option("--route-project-path <path>", "fallback project path for --auto-route when the hygiene finding has no cwd").option("--evidence-dir <path>", "write the route result JSON to this directory").option("--dry-run", "print intended task upserts without mutating todos").option("--json", "print JSON").action((opts) => {
7066
7180
  const store = new Store;
7067
7181
  try {
7068
7182
  const checks = parseHygieneChecks(opts.checks);
@@ -7072,11 +7186,28 @@ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygie
7072
7186
  limit: Number(opts.limit),
7073
7187
  scriptsDir: opts.scriptsDir
7074
7188
  });
7075
- const selection = selectRouteItems(route.tasks, Number(opts.maxActions), `hygiene:${stableHash([opts.project, opts.taskList, checks, opts.limit, Boolean(opts.includeInactive), opts.scriptsDir ?? ""])}`, (task) => task.fingerprint);
7189
+ const selection = selectRouteItems(route.tasks, Number(opts.maxActions), routeCursorKey("hygiene", [opts.project, opts.taskList, checks, opts.limit, Boolean(opts.includeInactive), opts.scriptsDir ?? ""], {
7190
+ autoRoute: Boolean(opts.autoRoute),
7191
+ routeProjectPath: opts.routeProjectPath
7192
+ }), (task) => task.fingerprint);
7076
7193
  const listId = opts.dryRun ? undefined : ensureTodosTaskList(opts.project, opts.taskList, "OpenLoops Hygiene", "Deduped OpenLoops hygiene findings routed by loops hygiene route-tasks.");
7077
7194
  const actions = selection.selected.map((task) => {
7195
+ const routeTask = taskAutoRoute(task.tags, task.metadata, {
7196
+ autoRoute: Boolean(opts.autoRoute),
7197
+ routeProjectPath: opts.routeProjectPath,
7198
+ source: "openloops.hygiene.route-tasks"
7199
+ });
7078
7200
  if (opts.dryRun) {
7079
- return { action: "would-upsert", check: task.check, title: task.title, fingerprint: task.fingerprint, priority: task.priority, metadata: task.metadata };
7201
+ return {
7202
+ action: "would-upsert",
7203
+ check: task.check,
7204
+ title: task.title,
7205
+ fingerprint: task.fingerprint,
7206
+ priority: task.priority,
7207
+ tags: routeTask.tags,
7208
+ metadata: routeTask.metadata,
7209
+ autoRoute: routeTask.autoRoute
7210
+ };
7080
7211
  }
7081
7212
  const result = runLocalCommand("todos", [
7082
7213
  "--project",
@@ -7097,9 +7228,10 @@ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygie
7097
7228
  "--list",
7098
7229
  listId,
7099
7230
  "--tags",
7100
- task.tags.join(","),
7231
+ routeTask.tags.join(","),
7232
+ ...routeTaskWorkingDirArgs(routeTask),
7101
7233
  "--metadata-json",
7102
- JSON.stringify(task.metadata)
7234
+ JSON.stringify(routeTask.metadata)
7103
7235
  ]);
7104
7236
  if (!result.ok) {
7105
7237
  return { action: "upsert-failed", check: task.check, fingerprint: task.fingerprint, error: result.stderr || result.error || result.stdout };
@@ -7114,12 +7246,16 @@ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygie
7114
7246
  routing: selection.cursor,
7115
7247
  actions
7116
7248
  };
7249
+ const evidencePath = writeRouteEvidence("hygiene-route-tasks", routed, opts.evidenceDir);
7250
+ const output = evidencePath ? { ...routed, evidencePath } : routed;
7117
7251
  if (!opts.dryRun && routed.ok)
7118
7252
  writeRouteCursor(selection.cursor.key, selection.cursor.lastFingerprint);
7119
7253
  if (isJson() || opts.json)
7120
- console.log(JSON.stringify(routed, null, 2));
7254
+ console.log(JSON.stringify(output, null, 2));
7121
7255
  else {
7122
- console.log(`hygiene_route_tasks checks=${checks.join(",")} findings=${routed.findings} actions=${actions.length}`);
7256
+ console.log(`hygiene_route_tasks checks=${checks.join(",")} findings=${output.findings} actions=${actions.length}`);
7257
+ if (evidencePath)
7258
+ console.log(`evidence=${evidencePath}`);
7123
7259
  for (const action of actions)
7124
7260
  console.log(`${action.action} ${action.fingerprint}`);
7125
7261
  }
@@ -4525,7 +4525,7 @@ function enableStartup(result) {
4525
4525
  // package.json
4526
4526
  var package_default = {
4527
4527
  name: "@hasna/loops",
4528
- version: "0.3.24",
4528
+ version: "0.3.26",
4529
4529
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4530
4530
  type: "module",
4531
4531
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -4330,8 +4330,17 @@ function accountForRole(input, role, seed) {
4330
4330
  return input.verifierAccount;
4331
4331
  return rolePoolValue(input.accountPool, seed, role) ?? input.account;
4332
4332
  }
4333
+ function assertNativeAuthProfileSupport(input, provider) {
4334
+ if (provider === "codewith")
4335
+ return;
4336
+ const hasNativeAuthProfiles = Boolean(input.authProfile || input.authProfilePool?.length || input.workerAuthProfile || input.verifierAuthProfile);
4337
+ if (!hasNativeAuthProfiles)
4338
+ return;
4339
+ throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
4340
+ }
4333
4341
  function agentTarget(input, prompt, role, seed) {
4334
4342
  const provider = input.provider ?? "codewith";
4343
+ assertNativeAuthProfileSupport(input, provider);
4335
4344
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4336
4345
  return {
4337
4346
  type: "agent",
@@ -4842,6 +4851,117 @@ function runDoctor(store) {
4842
4851
  }
4843
4852
  // src/lib/health.ts
4844
4853
  import { createHash } from "crypto";
4854
+
4855
+ // src/lib/format.ts
4856
+ var TEXT_OUTPUT_LIMIT = 32 * 1024;
4857
+ var SENSITIVE_PAYLOAD_KEYS = new Set(["env", "error", "prompt", "reason", "stderr", "stdout"]);
4858
+ function redact(value, visible = 0) {
4859
+ if (!value)
4860
+ return value;
4861
+ if (value.length <= visible)
4862
+ return value;
4863
+ if (visible <= 0)
4864
+ return `[redacted ${value.length} chars]`;
4865
+ return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
4866
+ }
4867
+ function truncateTextOutput(value) {
4868
+ if (value.length <= TEXT_OUTPUT_LIMIT)
4869
+ return value;
4870
+ return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
4871
+ [truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
4872
+ }
4873
+ function redactSensitivePayload(value, key) {
4874
+ if (key && SENSITIVE_PAYLOAD_KEYS.has(key)) {
4875
+ if (typeof value === "string")
4876
+ return redact(value);
4877
+ if (value === undefined || value === null)
4878
+ return value;
4879
+ return "[redacted]";
4880
+ }
4881
+ if (Array.isArray(value))
4882
+ return value.map((item) => redactSensitivePayload(item));
4883
+ if (value && typeof value === "object") {
4884
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSensitivePayload(entryValue, entryKey)]));
4885
+ }
4886
+ return value;
4887
+ }
4888
+ function textOutputBlocks(value, opts = {}) {
4889
+ const indent = opts.indent ?? "";
4890
+ const nested = `${indent} `;
4891
+ const blocks = [];
4892
+ for (const [label, output] of [
4893
+ ["stdout", value.stdout],
4894
+ ["stderr", value.stderr]
4895
+ ]) {
4896
+ if (!output)
4897
+ continue;
4898
+ blocks.push(`${indent}${label}:`);
4899
+ for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
4900
+ blocks.push(`${nested}${line}`);
4901
+ }
4902
+ }
4903
+ return blocks;
4904
+ }
4905
+ function publicLoop(loop) {
4906
+ const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
4907
+ return {
4908
+ ...loop,
4909
+ target
4910
+ };
4911
+ }
4912
+ function publicRun(run, showOutput = false) {
4913
+ return {
4914
+ ...run,
4915
+ stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
4916
+ stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
4917
+ };
4918
+ }
4919
+ function publicExecutorResult(result, showOutput = false) {
4920
+ return {
4921
+ ...result,
4922
+ stdout: showOutput ? result.stdout : result.stdout ? `[redacted ${result.stdout.length} chars]` : undefined,
4923
+ stderr: showOutput ? result.stderr : result.stderr ? `[redacted ${result.stderr.length} chars]` : undefined,
4924
+ error: redact(result.error)
4925
+ };
4926
+ }
4927
+ function publicWorkflow(workflow) {
4928
+ return {
4929
+ ...workflow,
4930
+ steps: workflow.steps.map((step) => ({
4931
+ ...step,
4932
+ target: step.target.type === "agent" ? { ...step.target, prompt: redact(step.target.prompt) } : step.target.type === "command" && step.target.env ? { ...step.target, env: "[redacted]" } : step.target
4933
+ }))
4934
+ };
4935
+ }
4936
+ function publicWorkflowRun(run) {
4937
+ return { ...run, error: redact(run.error) };
4938
+ }
4939
+ function publicWorkflowStepRun(run, showOutput = false) {
4940
+ return {
4941
+ ...run,
4942
+ stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
4943
+ stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
4944
+ error: redact(run.error)
4945
+ };
4946
+ }
4947
+ function publicWorkflowEvent(event) {
4948
+ return { ...event, payload: redactSensitivePayload(event.payload) };
4949
+ }
4950
+ function publicGoal(goal) {
4951
+ return {
4952
+ ...goal,
4953
+ objective: redact(goal.objective, 120)
4954
+ };
4955
+ }
4956
+ function publicGoalRun(run) {
4957
+ return {
4958
+ ...run,
4959
+ evidence: redactSensitivePayload(run.evidence),
4960
+ rawResponse: redactSensitivePayload(run.rawResponse)
4961
+ };
4962
+ }
4963
+
4964
+ // src/lib/health.ts
4845
4965
  var EVIDENCE_CHARS = 2000;
4846
4966
  var FINGERPRINT_EVIDENCE_CHARS = 120;
4847
4967
  var CLASSIFICATIONS = [
@@ -4865,6 +4985,9 @@ function bounded(value, limit = EVIDENCE_CHARS) {
4865
4985
  return `${value.slice(0, limit)}
4866
4986
  [truncated ${value.length - limit} chars]`;
4867
4987
  }
4988
+ function redactedEvidence(value) {
4989
+ return redact(bounded(value));
4990
+ }
4868
4991
  function searchableText(run) {
4869
4992
  return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
4870
4993
  `).toLowerCase();
@@ -4885,9 +5008,9 @@ function stableFailureFingerprint(run, classification) {
4885
5008
  function healthRun(run) {
4886
5009
  return {
4887
5010
  ...run,
4888
- error: bounded(run.error),
4889
- stdout: bounded(run.stdout),
4890
- stderr: bounded(run.stderr)
5011
+ error: redactedEvidence(run.error),
5012
+ stdout: redactedEvidence(run.stdout),
5013
+ stderr: redactedEvidence(run.stderr)
4891
5014
  };
4892
5015
  }
4893
5016
  function classifyRunFailure(run) {
@@ -4919,9 +5042,9 @@ function classifyRunFailure(run) {
4919
5042
  classification,
4920
5043
  fingerprint: stableFailureFingerprint(run, classification),
4921
5044
  evidence: {
4922
- error: bounded(run.error),
4923
- stdout: bounded(run.stdout),
4924
- stderr: bounded(run.stderr),
5045
+ error: redactedEvidence(run.error),
5046
+ stdout: redactedEvidence(run.stdout),
5047
+ stderr: redactedEvidence(run.stderr),
4925
5048
  exitCode: run.exitCode
4926
5049
  }
4927
5050
  };
package/docs/USAGE.md CHANGED
@@ -338,6 +338,14 @@ Use `--dry-run --json` first when testing a new automation path. Routed tasks
338
338
  include the stable failure fingerprint, classification, loop id/name, and
339
339
  `no_tmux_dispatch=true` metadata.
340
340
 
341
+ Use `--evidence-dir <dir>` when a deterministic loop needs a compact JSON
342
+ heartbeat/report on disk. Use `--auto-route` only on task lists that should feed
343
+ the task-created headless worker/verifier workflow; it adds the `auto:route`
344
+ tag and route metadata when the finding has a cwd or `--route-project-path` is
345
+ provided. Findings with no routeable working directory remain plain tasks and
346
+ record an `auto_route_skipped_reason`. Without `--auto-route`, route commands
347
+ only upsert deduped tasks and do not launch agents.
348
+
341
349
  Failure classifications are: `rate_limit`, `auth`, `model_not_found`,
342
350
  `context_length`, `schema_response_format`, `node_init`, `timeout`, `sigsegv`,
343
351
  `skipped_previous_active`, and `unknown`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
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",