@hasna/loops 0.3.15 → 0.3.17
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 +30 -1
- package/dist/cli/index.js +872 -5
- package/dist/daemon/index.js +60 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +582 -2
- package/dist/lib/health.d.ts +70 -0
- package/dist/lib/hygiene.d.ts +62 -0
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +43 -0
- package/dist/lib/templates.d.ts +23 -0
- package/dist/sdk/index.js +59 -1
- package/dist/types.d.ts +6 -0
- package/docs/USAGE.md +81 -1
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Loop, LoopRun } from "../types.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
4
|
+
export interface RunFailureSignal {
|
|
5
|
+
classification: RunFailureClassification;
|
|
6
|
+
fingerprint: string;
|
|
7
|
+
evidence: {
|
|
8
|
+
error?: string;
|
|
9
|
+
stdout?: string;
|
|
10
|
+
stderr?: string;
|
|
11
|
+
exitCode?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface RecommendedTaskUpsert {
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
priority: "critical" | "high" | "medium" | "low";
|
|
18
|
+
tags: string[];
|
|
19
|
+
dedupeKey: string;
|
|
20
|
+
search: {
|
|
21
|
+
query: string;
|
|
22
|
+
};
|
|
23
|
+
compatibilityFallback: {
|
|
24
|
+
search: string[];
|
|
25
|
+
add: string[];
|
|
26
|
+
comment: string[];
|
|
27
|
+
};
|
|
28
|
+
futureNativeUpsert: {
|
|
29
|
+
command: string;
|
|
30
|
+
fields: Record<string, string | string[]>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface LoopExpectationResult {
|
|
34
|
+
loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt">;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
check: {
|
|
37
|
+
id: "latest-run-succeeded";
|
|
38
|
+
status: "pass" | "fail" | "warn";
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
latestRun?: LoopRun;
|
|
42
|
+
failure?: RunFailureSignal;
|
|
43
|
+
route: {
|
|
44
|
+
source: "openloops";
|
|
45
|
+
kind: "loop_expectation";
|
|
46
|
+
loopId: string;
|
|
47
|
+
loopName: string;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
provider?: string;
|
|
50
|
+
};
|
|
51
|
+
recommendedTask?: RecommendedTaskUpsert;
|
|
52
|
+
}
|
|
53
|
+
export interface LoopsHealthReport {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
summary: {
|
|
57
|
+
loops: number;
|
|
58
|
+
healthy: number;
|
|
59
|
+
unhealthy: number;
|
|
60
|
+
warnings: number;
|
|
61
|
+
};
|
|
62
|
+
classifications: Record<RunFailureClassification, number>;
|
|
63
|
+
expectations: LoopExpectationResult[];
|
|
64
|
+
}
|
|
65
|
+
export declare function classifyRunFailure(run: LoopRun): RunFailureSignal | undefined;
|
|
66
|
+
export declare function expectationForLoop(store: Store, loop: Loop): LoopExpectationResult;
|
|
67
|
+
export declare function buildHealthReport(store: Store, opts?: {
|
|
68
|
+
includeArchived?: boolean;
|
|
69
|
+
limit?: number;
|
|
70
|
+
}): LoopsHealthReport;
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
20
|
+
export interface DuplicateOverlapGroup {
|
|
21
|
+
key: string;
|
|
22
|
+
baseName: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
schedule: string;
|
|
25
|
+
loops: Array<Pick<Loop, "id" | "name" | "status" | "nextRunAt">>;
|
|
26
|
+
}
|
|
27
|
+
export interface DuplicateOverlapReport {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
generatedAt: string;
|
|
30
|
+
checked: number;
|
|
31
|
+
groups: DuplicateOverlapGroup[];
|
|
32
|
+
}
|
|
33
|
+
export interface ScriptBackedLoop {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
status: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
command: string;
|
|
39
|
+
scriptMatches: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ScriptInventoryReport {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
checked: number;
|
|
45
|
+
scriptBacked: number;
|
|
46
|
+
loops: ScriptBackedLoop[];
|
|
47
|
+
}
|
|
48
|
+
export declare function buildNameHygieneReport(store: Store, opts?: {
|
|
49
|
+
apply?: boolean;
|
|
50
|
+
includeStopped?: boolean;
|
|
51
|
+
includeInactive?: boolean;
|
|
52
|
+
limit?: number;
|
|
53
|
+
}): NameHygieneReport;
|
|
54
|
+
export declare function buildDuplicateOverlapReport(store: Store, opts?: {
|
|
55
|
+
includeInactive?: boolean;
|
|
56
|
+
limit?: number;
|
|
57
|
+
}): DuplicateOverlapReport;
|
|
58
|
+
export declare function buildScriptInventoryReport(store: Store, opts?: {
|
|
59
|
+
scriptsDir?: string;
|
|
60
|
+
includeInactive?: boolean;
|
|
61
|
+
limit?: number;
|
|
62
|
+
}): ScriptInventoryReport;
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -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
|
@@ -326,6 +326,17 @@ function optionalPositiveInteger(value, label) {
|
|
|
326
326
|
throw new Error(`${label} must be a positive integer`);
|
|
327
327
|
return value;
|
|
328
328
|
}
|
|
329
|
+
function optionalStringArray(value, label) {
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return;
|
|
332
|
+
if (!Array.isArray(value))
|
|
333
|
+
throw new Error(`${label} must be an array`);
|
|
334
|
+
const values = value.map((entry, index) => {
|
|
335
|
+
assertString(entry, `${label}[${index}]`);
|
|
336
|
+
return entry.trim();
|
|
337
|
+
}).filter(Boolean);
|
|
338
|
+
return values.length ? values : undefined;
|
|
339
|
+
}
|
|
329
340
|
function normalizeGoalSpec(value, label = "goal") {
|
|
330
341
|
if (value === undefined)
|
|
331
342
|
return;
|
|
@@ -397,6 +408,14 @@ function validateTarget(value, label) {
|
|
|
397
408
|
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
409
|
}
|
|
399
410
|
}
|
|
411
|
+
if (value.allowlist !== undefined) {
|
|
412
|
+
assertObject(value.allowlist, `${label}.allowlist`);
|
|
413
|
+
optionalStringArray(value.allowlist.tools, `${label}.allowlist.tools`);
|
|
414
|
+
optionalStringArray(value.allowlist.commands, `${label}.allowlist.commands`);
|
|
415
|
+
if (value.allowlist.enforcement !== undefined && value.allowlist.enforcement !== "metadata_only") {
|
|
416
|
+
throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
400
419
|
return value;
|
|
401
420
|
}
|
|
402
421
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -1031,6 +1050,30 @@ class Store {
|
|
|
1031
1050
|
throw new Error(`loop not found after update: ${id}`);
|
|
1032
1051
|
return after;
|
|
1033
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
|
+
}
|
|
1034
1077
|
archiveLoop(idOrName) {
|
|
1035
1078
|
const loop = this.requireLoop(idOrName);
|
|
1036
1079
|
if (loop.archivedAt)
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -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;
|
package/dist/sdk/index.js
CHANGED
|
@@ -326,6 +326,17 @@ function optionalPositiveInteger(value, label) {
|
|
|
326
326
|
throw new Error(`${label} must be a positive integer`);
|
|
327
327
|
return value;
|
|
328
328
|
}
|
|
329
|
+
function optionalStringArray(value, label) {
|
|
330
|
+
if (value === undefined)
|
|
331
|
+
return;
|
|
332
|
+
if (!Array.isArray(value))
|
|
333
|
+
throw new Error(`${label} must be an array`);
|
|
334
|
+
const values = value.map((entry, index) => {
|
|
335
|
+
assertString(entry, `${label}[${index}]`);
|
|
336
|
+
return entry.trim();
|
|
337
|
+
}).filter(Boolean);
|
|
338
|
+
return values.length ? values : undefined;
|
|
339
|
+
}
|
|
329
340
|
function normalizeGoalSpec(value, label = "goal") {
|
|
330
341
|
if (value === undefined)
|
|
331
342
|
return;
|
|
@@ -397,6 +408,14 @@ function validateTarget(value, label) {
|
|
|
397
408
|
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
409
|
}
|
|
399
410
|
}
|
|
411
|
+
if (value.allowlist !== undefined) {
|
|
412
|
+
assertObject(value.allowlist, `${label}.allowlist`);
|
|
413
|
+
optionalStringArray(value.allowlist.tools, `${label}.allowlist.tools`);
|
|
414
|
+
optionalStringArray(value.allowlist.commands, `${label}.allowlist.commands`);
|
|
415
|
+
if (value.allowlist.enforcement !== undefined && value.allowlist.enforcement !== "metadata_only") {
|
|
416
|
+
throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
400
419
|
return value;
|
|
401
420
|
}
|
|
402
421
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -1031,6 +1050,30 @@ class Store {
|
|
|
1031
1050
|
throw new Error(`loop not found after update: ${id}`);
|
|
1032
1051
|
return after;
|
|
1033
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
|
+
}
|
|
1034
1077
|
archiveLoop(idOrName) {
|
|
1035
1078
|
const loop = this.requireLoop(idOrName);
|
|
1036
1079
|
if (loop.archivedAt)
|
|
@@ -2455,6 +2498,16 @@ function metadataEnv(metadata) {
|
|
|
2455
2498
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
2456
2499
|
return env;
|
|
2457
2500
|
}
|
|
2501
|
+
function allowlistEnv(allowlist) {
|
|
2502
|
+
const env = {};
|
|
2503
|
+
if (allowlist?.tools?.length)
|
|
2504
|
+
env.LOOPS_AGENT_ALLOWED_TOOLS = allowlist.tools.join(",");
|
|
2505
|
+
if (allowlist?.commands?.length)
|
|
2506
|
+
env.LOOPS_AGENT_ALLOWED_COMMANDS = allowlist.commands.join(",");
|
|
2507
|
+
if (allowlist?.tools?.length || allowlist?.commands?.length)
|
|
2508
|
+
env.LOOPS_AGENT_ALLOWLIST_ENFORCEMENT = "metadata_only";
|
|
2509
|
+
return env;
|
|
2510
|
+
}
|
|
2458
2511
|
function providerCommand(provider) {
|
|
2459
2512
|
switch (provider) {
|
|
2460
2513
|
case "claude":
|
|
@@ -2662,7 +2715,8 @@ function commandSpec(target) {
|
|
|
2662
2715
|
account: agentTarget.account,
|
|
2663
2716
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2664
2717
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2665
|
-
stdin: agentTarget.prompt
|
|
2718
|
+
stdin: agentTarget.prompt,
|
|
2719
|
+
allowlist: agentTarget.allowlist
|
|
2666
2720
|
};
|
|
2667
2721
|
}
|
|
2668
2722
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -2674,6 +2728,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
2674
2728
|
Object.assign(env, accountEnv);
|
|
2675
2729
|
}
|
|
2676
2730
|
Object.assign(env, spec.env ?? {});
|
|
2731
|
+
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
2677
2732
|
env.PATH = normalizeExecutionPath(env);
|
|
2678
2733
|
Object.assign(env, metadataEnv(metadata));
|
|
2679
2734
|
return env;
|
|
@@ -2712,6 +2767,9 @@ function remoteBootstrapLines(spec, metadata) {
|
|
|
2712
2767
|
continue;
|
|
2713
2768
|
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2714
2769
|
}
|
|
2770
|
+
for (const [key, value] of Object.entries(allowlistEnv(spec.allowlist))) {
|
|
2771
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2772
|
+
}
|
|
2715
2773
|
return lines;
|
|
2716
2774
|
}
|
|
2717
2775
|
function remoteScript(spec, metadata) {
|
package/dist/types.d.ts
CHANGED
|
@@ -54,6 +54,11 @@ export type AgentProvider = "claude" | "cursor" | "codewith" | "aicopilot" | "op
|
|
|
54
54
|
export type AgentConfigIsolation = "safe" | "none";
|
|
55
55
|
export type AgentPermissionMode = "default" | "plan" | "auto" | "bypass";
|
|
56
56
|
export type AgentSandbox = "read-only" | "workspace-write" | "danger-full-access" | "enabled" | "disabled";
|
|
57
|
+
export interface AgentAllowlistSpec {
|
|
58
|
+
tools?: string[];
|
|
59
|
+
commands?: string[];
|
|
60
|
+
enforcement?: "metadata_only";
|
|
61
|
+
}
|
|
57
62
|
export interface AgentTarget {
|
|
58
63
|
type: "agent";
|
|
59
64
|
provider: AgentProvider;
|
|
@@ -68,6 +73,7 @@ export interface AgentTarget {
|
|
|
68
73
|
configIsolation?: AgentConfigIsolation;
|
|
69
74
|
permissionMode?: AgentPermissionMode;
|
|
70
75
|
sandbox?: AgentSandbox;
|
|
76
|
+
allowlist?: AgentAllowlistSpec;
|
|
71
77
|
account?: AccountRef;
|
|
72
78
|
}
|
|
73
79
|
export interface WorkflowTarget {
|
package/docs/USAGE.md
CHANGED
|
@@ -94,6 +94,23 @@ loops create agent supply-chain-watch \
|
|
|
94
94
|
--prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
Agent loops can also carry advisory per-session allowlist metadata:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
loops create agent repo-check \
|
|
101
|
+
--provider codewith \
|
|
102
|
+
--every 15m \
|
|
103
|
+
--cwd /path/to/repo \
|
|
104
|
+
--prompt "Check the repo and report concrete failures." \
|
|
105
|
+
--allow-tool functions.exec_command \
|
|
106
|
+
--allow-command git,bun
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
These fields are stored on the loop target and exposed to the run environment
|
|
110
|
+
as `LOOPS_AGENT_ALLOWED_TOOLS`, `LOOPS_AGENT_ALLOWED_COMMANDS`, and
|
|
111
|
+
`LOOPS_AGENT_ALLOWLIST_ENFORCEMENT=metadata_only`. They are not enforced by
|
|
112
|
+
OpenLoops yet; provider-native enforcement will be added separately.
|
|
113
|
+
|
|
97
114
|
For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
|
|
98
115
|
|
|
99
116
|
```bash
|
|
@@ -167,7 +184,8 @@ Use `shell: true` only when you intentionally want shell parsing:
|
|
|
167
184
|
Built-in templates turn common orchestration flows into reusable workflow JSON.
|
|
168
185
|
`todos-task-worker-verifier` performs one todos task and then verifies it.
|
|
169
186
|
`event-worker-verifier` handles any Hasna event envelope and then verifies the
|
|
170
|
-
handling.
|
|
187
|
+
handling. `bounded-agent-worker-verifier` is for recurring bounded agent work:
|
|
188
|
+
one worker runs a narrow objective, then a fresh verifier audits the result.
|
|
171
189
|
|
|
172
190
|
```bash
|
|
173
191
|
loops templates list
|
|
@@ -187,6 +205,12 @@ loops templates render event-worker-verifier \
|
|
|
187
205
|
--var eventSource=knowledge \
|
|
188
206
|
--var eventJson='{"id":"<event-id>"}' \
|
|
189
207
|
--var projectPath=/path/to/repo
|
|
208
|
+
loops templates render bounded-agent-worker-verifier \
|
|
209
|
+
--var objective="Check docs drift and queue tasks for gaps" \
|
|
210
|
+
--var projectPath=/path/to/repo \
|
|
211
|
+
--var provider=codewith \
|
|
212
|
+
--var authProfilePool=account004,account005 \
|
|
213
|
+
--var sandbox=danger-full-access
|
|
190
214
|
```
|
|
191
215
|
|
|
192
216
|
For event-driven task automation, `loops events handle todos-task` reads a
|
|
@@ -201,6 +225,14 @@ cat task-created-event.json | loops events handle todos-task \
|
|
|
201
225
|
--sandbox danger-full-access
|
|
202
226
|
```
|
|
203
227
|
|
|
228
|
+
Task routing is explicit opt-in. The handler skips the event without creating a
|
|
229
|
+
workflow unless the event data or metadata has `route_enabled=true`,
|
|
230
|
+
`automation.allowed=true`, or a task tag containing `auto:route`. It also skips
|
|
231
|
+
blocked, completed/done, cancelled/canceled, failed, archived, manual,
|
|
232
|
+
approval-required, or `no-auto` tasks. This guard exists even when the upstream
|
|
233
|
+
`@hasna/events` webhook filter is misconfigured, so task existence alone is not
|
|
234
|
+
permission to execute agent work.
|
|
235
|
+
|
|
204
236
|
For other Hasna apps that expose `@hasna/events` webhooks, use the generic
|
|
205
237
|
handler:
|
|
206
238
|
|
|
@@ -250,6 +282,54 @@ loops run-now <id-or-name>
|
|
|
250
282
|
|
|
251
283
|
Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output. `loops run-now` exits non-zero when the recorded run fails or times out.
|
|
252
284
|
|
|
285
|
+
## Health And Expectations
|
|
286
|
+
|
|
287
|
+
`loops health --json` summarizes the latest run for each loop and classifies
|
|
288
|
+
agent-run failures for default-loop SLOs:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
loops health --json
|
|
292
|
+
loops expectations <loop-id-or-name> --json
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The JSON contains the expectation result, bounded error/stdout/stderr evidence,
|
|
296
|
+
a stable failure fingerprint, route metadata, and recommended task fields.
|
|
297
|
+
OpenLoops does not mutate Todos from `health` or `expectations`. To turn failed
|
|
298
|
+
expectations into deduped tasks, use the explicit routing command:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
loops health route-tasks \
|
|
302
|
+
--project ~/.hasna/loops \
|
|
303
|
+
--task-list loop-error-self-heal \
|
|
304
|
+
--max-actions 5
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Use `--dry-run --json` first when testing a new automation path. Routed tasks
|
|
308
|
+
include the stable failure fingerprint, classification, loop id/name, and
|
|
309
|
+
`no_tmux_dispatch=true` metadata.
|
|
310
|
+
|
|
311
|
+
Failure classifications are: `rate_limit`, `auth`, `model_not_found`,
|
|
312
|
+
`context_length`, `schema_response_format`, `node_init`, `timeout`, `sigsegv`,
|
|
313
|
+
`skipped_previous_active`, and `unknown`.
|
|
314
|
+
|
|
315
|
+
## Hygiene
|
|
316
|
+
|
|
317
|
+
OpenLoops includes deterministic hygiene checks for replacing local maintenance
|
|
318
|
+
scripts with package commands:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
loops hygiene names --json
|
|
322
|
+
loops hygiene names --apply
|
|
323
|
+
loops hygiene duplicates --json
|
|
324
|
+
loops hygiene scripts --json
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`hygiene names` reports canonical `machine-*` or `repo-<name>-*` loop names and
|
|
328
|
+
only renames when `--apply` is present. `hygiene duplicates` groups loops with
|
|
329
|
+
the same normalized name, cwd, and schedule. `hygiene scripts` inventories loops
|
|
330
|
+
whose command still references `~/.hasna/loops/scripts`; use it as a migration
|
|
331
|
+
gate before deleting local scripts.
|
|
332
|
+
|
|
253
333
|
Archive loops when retiring old automation but preserving history:
|
|
254
334
|
|
|
255
335
|
```bash
|
package/package.json
CHANGED