@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 +10 -0
- package/dist/cli/index.js +160 -24
- package/dist/daemon/index.js +1 -1
- package/dist/index.js +129 -6
- package/docs/USAGE.md +8 -0
- package/package.json +1 -1
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:
|
|
4794
|
-
stdout:
|
|
4795
|
-
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:
|
|
4828
|
-
stdout:
|
|
4829
|
-
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.
|
|
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),
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
6952
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6841
6953
|
else {
|
|
6842
|
-
console.log(`health_route_tasks inspected=${
|
|
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),
|
|
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 {
|
|
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
|
-
|
|
7231
|
+
routeTask.tags.join(","),
|
|
7232
|
+
...routeTaskWorkingDirArgs(routeTask),
|
|
7101
7233
|
"--metadata-json",
|
|
7102
|
-
JSON.stringify(
|
|
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(
|
|
7254
|
+
console.log(JSON.stringify(output, null, 2));
|
|
7121
7255
|
else {
|
|
7122
|
-
console.log(`hygiene_route_tasks checks=${checks.join(",")} findings=${
|
|
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
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -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.
|
|
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:
|
|
4889
|
-
stdout:
|
|
4890
|
-
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:
|
|
4923
|
-
stdout:
|
|
4924
|
-
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