@hasna/loops 0.3.17 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +44 -18
- package/dist/daemon/index.js +1 -1
- package/dist/index.js +41 -15
- package/dist/lib/health.d.ts +1 -0
- package/dist/lib/hygiene.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -4640,6 +4640,7 @@ function runDoctor(store) {
|
|
|
4640
4640
|
// src/lib/health.ts
|
|
4641
4641
|
import { createHash } from "crypto";
|
|
4642
4642
|
var EVIDENCE_CHARS = 2000;
|
|
4643
|
+
var FINGERPRINT_EVIDENCE_CHARS = 120;
|
|
4643
4644
|
var CLASSIFICATIONS = [
|
|
4644
4645
|
"rate_limit",
|
|
4645
4646
|
"auth",
|
|
@@ -4668,6 +4669,15 @@ function stableFingerprint(parts) {
|
|
|
4668
4669
|
return createHash("sha256").update(parts.join(`
|
|
4669
4670
|
`)).digest("hex").slice(0, 16);
|
|
4670
4671
|
}
|
|
4672
|
+
function stableFailureFingerprint(run, classification) {
|
|
4673
|
+
return stableFingerprint([
|
|
4674
|
+
run.loopId,
|
|
4675
|
+
classification,
|
|
4676
|
+
String(run.status),
|
|
4677
|
+
String(run.exitCode ?? ""),
|
|
4678
|
+
(run.error ?? run.stderr ?? run.stdout ?? "").replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
4679
|
+
]);
|
|
4680
|
+
}
|
|
4671
4681
|
function healthRun(run) {
|
|
4672
4682
|
return {
|
|
4673
4683
|
...run,
|
|
@@ -4701,14 +4711,7 @@ function classifyRunFailure(run) {
|
|
|
4701
4711
|
classification = "sigsegv";
|
|
4702
4712
|
return {
|
|
4703
4713
|
classification,
|
|
4704
|
-
fingerprint:
|
|
4705
|
-
run.loopId,
|
|
4706
|
-
run.loopName,
|
|
4707
|
-
run.status,
|
|
4708
|
-
classification,
|
|
4709
|
-
String(run.exitCode ?? ""),
|
|
4710
|
-
(run.error ?? run.stderr ?? run.stdout ?? "").slice(0, 500)
|
|
4711
|
-
]),
|
|
4714
|
+
fingerprint: stableFailureFingerprint(run, classification),
|
|
4712
4715
|
evidence: {
|
|
4713
4716
|
error: bounded(run.error),
|
|
4714
4717
|
stdout: bounded(run.stdout),
|
|
@@ -4824,7 +4827,7 @@ function expectationForLoop(store, loop) {
|
|
|
4824
4827
|
};
|
|
4825
4828
|
}
|
|
4826
4829
|
function buildHealthReport(store, opts = {}) {
|
|
4827
|
-
const loops = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
|
|
4830
|
+
const loops = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 }).filter((loop) => opts.includeInactive || loop.status === "active" || loop.status === "paused");
|
|
4828
4831
|
const expectations = loops.map((loop) => expectationForLoop(store, loop));
|
|
4829
4832
|
const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
|
|
4830
4833
|
for (const expectation of expectations) {
|
|
@@ -4928,8 +4931,9 @@ function canonicalName(loop) {
|
|
|
4928
4931
|
newName: name
|
|
4929
4932
|
};
|
|
4930
4933
|
}
|
|
4931
|
-
function ensureUnique(changes) {
|
|
4932
|
-
const
|
|
4934
|
+
function ensureUnique(changes, existingNames = []) {
|
|
4935
|
+
const oldNames = new Set(changes.map((change) => change.oldName));
|
|
4936
|
+
const used = new Set([...existingNames].filter((name) => !oldNames.has(name)));
|
|
4933
4937
|
for (const change of changes) {
|
|
4934
4938
|
let candidate = change.newName;
|
|
4935
4939
|
if (!used.has(candidate)) {
|
|
@@ -4959,6 +4963,7 @@ function managedLoops(store, opts) {
|
|
|
4959
4963
|
return loops.filter((loop) => loop.status === "active" || loop.status === "paused");
|
|
4960
4964
|
}
|
|
4961
4965
|
function buildNameHygieneReport(store, opts = {}) {
|
|
4966
|
+
const allLoops = store.listLoops({ includeArchived: true, limit: 1e4 });
|
|
4962
4967
|
const changes = managedLoops(store, opts).map((loop) => {
|
|
4963
4968
|
const canonical = canonicalName(loop);
|
|
4964
4969
|
return {
|
|
@@ -4967,8 +4972,9 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
4967
4972
|
changed: loop.name !== canonical.newName
|
|
4968
4973
|
};
|
|
4969
4974
|
});
|
|
4970
|
-
ensureUnique(changes);
|
|
4975
|
+
ensureUnique(changes, allLoops.map((loop) => loop.name));
|
|
4971
4976
|
const changed = changes.filter((change) => change.changed);
|
|
4977
|
+
const conflicts = changes.filter((change) => allLoops.some((loop) => loop.name === change.newName && loop.id !== change.id));
|
|
4972
4978
|
if (opts.apply) {
|
|
4973
4979
|
for (const change of changed)
|
|
4974
4980
|
store.renameLoop(change.id, change.newName);
|
|
@@ -4979,7 +4985,8 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
4979
4985
|
applied: Boolean(opts.apply),
|
|
4980
4986
|
checked: changes.length,
|
|
4981
4987
|
changed: changed.length,
|
|
4982
|
-
changes
|
|
4988
|
+
changes,
|
|
4989
|
+
conflicts
|
|
4983
4990
|
};
|
|
4984
4991
|
}
|
|
4985
4992
|
function baseName(name) {
|
|
@@ -5033,14 +5040,33 @@ function commandText(loop) {
|
|
|
5033
5040
|
return "";
|
|
5034
5041
|
return [loop.target.command, ...loop.target.args ?? []].join(" ");
|
|
5035
5042
|
}
|
|
5043
|
+
function scriptNeedles(scriptsDir) {
|
|
5044
|
+
const home = process.env.HOME ?? "/home/hasna";
|
|
5045
|
+
const normalized = scriptsDir.replace(/\/+$/g, "");
|
|
5046
|
+
const values = [
|
|
5047
|
+
normalized,
|
|
5048
|
+
`${normalized}/`,
|
|
5049
|
+
"~/.hasna/loops/scripts",
|
|
5050
|
+
"~/.hasna/loops/scripts/",
|
|
5051
|
+
"$HOME/.hasna/loops/scripts",
|
|
5052
|
+
"$HOME/.hasna/loops/scripts/",
|
|
5053
|
+
"${HOME}/.hasna/loops/scripts",
|
|
5054
|
+
"${HOME}/.hasna/loops/scripts/",
|
|
5055
|
+
`${home}/.hasna/loops/scripts`,
|
|
5056
|
+
`${home}/.hasna/loops/scripts/`,
|
|
5057
|
+
"/.hasna/loops/scripts/"
|
|
5058
|
+
];
|
|
5059
|
+
return [...new Set(values)];
|
|
5060
|
+
}
|
|
5036
5061
|
function buildScriptInventoryReport(store, opts = {}) {
|
|
5037
5062
|
const scriptsDir = opts.scriptsDir ?? `${process.env.HOME ?? "/home/hasna"}/.hasna/loops/scripts`;
|
|
5063
|
+
const needles = scriptNeedles(scriptsDir);
|
|
5038
5064
|
const loops = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
|
|
5039
5065
|
const scriptBacked = loops.map((loop) => {
|
|
5040
5066
|
const text = commandText(loop);
|
|
5041
5067
|
if (!text)
|
|
5042
5068
|
return;
|
|
5043
|
-
const matches =
|
|
5069
|
+
const matches = needles.filter((needle) => text.includes(needle));
|
|
5044
5070
|
if (!matches.length)
|
|
5045
5071
|
return;
|
|
5046
5072
|
return {
|
|
@@ -5063,7 +5089,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5063
5089
|
// package.json
|
|
5064
5090
|
var package_default = {
|
|
5065
5091
|
name: "@hasna/loops",
|
|
5066
|
-
version: "0.3.
|
|
5092
|
+
version: "0.3.18",
|
|
5067
5093
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5068
5094
|
type: "module",
|
|
5069
5095
|
main: "dist/index.js",
|
|
@@ -5433,7 +5459,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
5433
5459
|
].join(`
|
|
5434
5460
|
`);
|
|
5435
5461
|
return {
|
|
5436
|
-
name: input.name ?? `bounded-agent-${stableIndex(seed,
|
|
5462
|
+
name: input.name ?? `bounded-agent-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
|
|
5437
5463
|
description: `Bounded worker/verifier workflow for ${input.objective.slice(0, 180)}`,
|
|
5438
5464
|
version: 1,
|
|
5439
5465
|
steps: [
|
|
@@ -6468,10 +6494,10 @@ var health = program.command("health").description("summarize loop health and la
|
|
|
6468
6494
|
store.close();
|
|
6469
6495
|
}
|
|
6470
6496
|
});
|
|
6471
|
-
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("--dry-run", "print intended task upserts without mutating todos").option("--json", "print JSON").action((opts) => {
|
|
6497
|
+
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) => {
|
|
6472
6498
|
const store = new Store;
|
|
6473
6499
|
try {
|
|
6474
|
-
const report = buildHealthReport(store, { limit: Number(opts.limit) });
|
|
6500
|
+
const report = buildHealthReport(store, { limit: Number(opts.limit), includeInactive: Boolean(opts.includeInactive) });
|
|
6475
6501
|
const failures = report.expectations.filter((entry) => !entry.ok && entry.recommendedTask).slice(0, Number(opts.maxActions));
|
|
6476
6502
|
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.");
|
|
6477
6503
|
const actions = failures.map((expectation) => {
|
package/dist/daemon/index.js
CHANGED
|
@@ -4419,7 +4419,7 @@ function enableStartup(result) {
|
|
|
4419
4419
|
// package.json
|
|
4420
4420
|
var package_default = {
|
|
4421
4421
|
name: "@hasna/loops",
|
|
4422
|
-
version: "0.3.
|
|
4422
|
+
version: "0.3.18",
|
|
4423
4423
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4424
4424
|
type: "module",
|
|
4425
4425
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -4397,7 +4397,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
4397
4397
|
].join(`
|
|
4398
4398
|
`);
|
|
4399
4399
|
return {
|
|
4400
|
-
name: input.name ?? `bounded-agent-${stableIndex(seed,
|
|
4400
|
+
name: input.name ?? `bounded-agent-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
|
|
4401
4401
|
description: `Bounded worker/verifier workflow for ${input.objective.slice(0, 180)}`,
|
|
4402
4402
|
version: 1,
|
|
4403
4403
|
steps: [
|
|
@@ -4735,6 +4735,7 @@ function runDoctor(store) {
|
|
|
4735
4735
|
// src/lib/health.ts
|
|
4736
4736
|
import { createHash } from "crypto";
|
|
4737
4737
|
var EVIDENCE_CHARS = 2000;
|
|
4738
|
+
var FINGERPRINT_EVIDENCE_CHARS = 120;
|
|
4738
4739
|
var CLASSIFICATIONS = [
|
|
4739
4740
|
"rate_limit",
|
|
4740
4741
|
"auth",
|
|
@@ -4763,6 +4764,15 @@ function stableFingerprint(parts) {
|
|
|
4763
4764
|
return createHash("sha256").update(parts.join(`
|
|
4764
4765
|
`)).digest("hex").slice(0, 16);
|
|
4765
4766
|
}
|
|
4767
|
+
function stableFailureFingerprint(run, classification) {
|
|
4768
|
+
return stableFingerprint([
|
|
4769
|
+
run.loopId,
|
|
4770
|
+
classification,
|
|
4771
|
+
String(run.status),
|
|
4772
|
+
String(run.exitCode ?? ""),
|
|
4773
|
+
(run.error ?? run.stderr ?? run.stdout ?? "").replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
4774
|
+
]);
|
|
4775
|
+
}
|
|
4766
4776
|
function healthRun(run) {
|
|
4767
4777
|
return {
|
|
4768
4778
|
...run,
|
|
@@ -4796,14 +4806,7 @@ function classifyRunFailure(run) {
|
|
|
4796
4806
|
classification = "sigsegv";
|
|
4797
4807
|
return {
|
|
4798
4808
|
classification,
|
|
4799
|
-
fingerprint:
|
|
4800
|
-
run.loopId,
|
|
4801
|
-
run.loopName,
|
|
4802
|
-
run.status,
|
|
4803
|
-
classification,
|
|
4804
|
-
String(run.exitCode ?? ""),
|
|
4805
|
-
(run.error ?? run.stderr ?? run.stdout ?? "").slice(0, 500)
|
|
4806
|
-
]),
|
|
4809
|
+
fingerprint: stableFailureFingerprint(run, classification),
|
|
4807
4810
|
evidence: {
|
|
4808
4811
|
error: bounded(run.error),
|
|
4809
4812
|
stdout: bounded(run.stdout),
|
|
@@ -4919,7 +4922,7 @@ function expectationForLoop(store, loop) {
|
|
|
4919
4922
|
};
|
|
4920
4923
|
}
|
|
4921
4924
|
function buildHealthReport(store, opts = {}) {
|
|
4922
|
-
const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
|
|
4925
|
+
const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 }).filter((loop) => opts.includeInactive || loop.status === "active" || loop.status === "paused");
|
|
4923
4926
|
const expectations = loops2.map((loop) => expectationForLoop(store, loop));
|
|
4924
4927
|
const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
|
|
4925
4928
|
for (const expectation of expectations) {
|
|
@@ -5022,8 +5025,9 @@ function canonicalName(loop) {
|
|
|
5022
5025
|
newName: name
|
|
5023
5026
|
};
|
|
5024
5027
|
}
|
|
5025
|
-
function ensureUnique(changes) {
|
|
5026
|
-
const
|
|
5028
|
+
function ensureUnique(changes, existingNames = []) {
|
|
5029
|
+
const oldNames = new Set(changes.map((change) => change.oldName));
|
|
5030
|
+
const used = new Set([...existingNames].filter((name) => !oldNames.has(name)));
|
|
5027
5031
|
for (const change of changes) {
|
|
5028
5032
|
let candidate = change.newName;
|
|
5029
5033
|
if (!used.has(candidate)) {
|
|
@@ -5053,6 +5057,7 @@ function managedLoops(store, opts) {
|
|
|
5053
5057
|
return loops2.filter((loop) => loop.status === "active" || loop.status === "paused");
|
|
5054
5058
|
}
|
|
5055
5059
|
function buildNameHygieneReport(store, opts = {}) {
|
|
5060
|
+
const allLoops = store.listLoops({ includeArchived: true, limit: 1e4 });
|
|
5056
5061
|
const changes = managedLoops(store, opts).map((loop) => {
|
|
5057
5062
|
const canonical = canonicalName(loop);
|
|
5058
5063
|
return {
|
|
@@ -5061,8 +5066,9 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
5061
5066
|
changed: loop.name !== canonical.newName
|
|
5062
5067
|
};
|
|
5063
5068
|
});
|
|
5064
|
-
ensureUnique(changes);
|
|
5069
|
+
ensureUnique(changes, allLoops.map((loop) => loop.name));
|
|
5065
5070
|
const changed = changes.filter((change) => change.changed);
|
|
5071
|
+
const conflicts = changes.filter((change) => allLoops.some((loop) => loop.name === change.newName && loop.id !== change.id));
|
|
5066
5072
|
if (opts.apply) {
|
|
5067
5073
|
for (const change of changed)
|
|
5068
5074
|
store.renameLoop(change.id, change.newName);
|
|
@@ -5073,7 +5079,8 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
5073
5079
|
applied: Boolean(opts.apply),
|
|
5074
5080
|
checked: changes.length,
|
|
5075
5081
|
changed: changed.length,
|
|
5076
|
-
changes
|
|
5082
|
+
changes,
|
|
5083
|
+
conflicts
|
|
5077
5084
|
};
|
|
5078
5085
|
}
|
|
5079
5086
|
function baseName(name) {
|
|
@@ -5127,14 +5134,33 @@ function commandText(loop) {
|
|
|
5127
5134
|
return "";
|
|
5128
5135
|
return [loop.target.command, ...loop.target.args ?? []].join(" ");
|
|
5129
5136
|
}
|
|
5137
|
+
function scriptNeedles(scriptsDir) {
|
|
5138
|
+
const home = process.env.HOME ?? "/home/hasna";
|
|
5139
|
+
const normalized = scriptsDir.replace(/\/+$/g, "");
|
|
5140
|
+
const values = [
|
|
5141
|
+
normalized,
|
|
5142
|
+
`${normalized}/`,
|
|
5143
|
+
"~/.hasna/loops/scripts",
|
|
5144
|
+
"~/.hasna/loops/scripts/",
|
|
5145
|
+
"$HOME/.hasna/loops/scripts",
|
|
5146
|
+
"$HOME/.hasna/loops/scripts/",
|
|
5147
|
+
"${HOME}/.hasna/loops/scripts",
|
|
5148
|
+
"${HOME}/.hasna/loops/scripts/",
|
|
5149
|
+
`${home}/.hasna/loops/scripts`,
|
|
5150
|
+
`${home}/.hasna/loops/scripts/`,
|
|
5151
|
+
"/.hasna/loops/scripts/"
|
|
5152
|
+
];
|
|
5153
|
+
return [...new Set(values)];
|
|
5154
|
+
}
|
|
5130
5155
|
function buildScriptInventoryReport(store, opts = {}) {
|
|
5131
5156
|
const scriptsDir = opts.scriptsDir ?? `${process.env.HOME ?? "/home/hasna"}/.hasna/loops/scripts`;
|
|
5157
|
+
const needles = scriptNeedles(scriptsDir);
|
|
5132
5158
|
const loops2 = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
|
|
5133
5159
|
const scriptBacked = loops2.map((loop) => {
|
|
5134
5160
|
const text = commandText(loop);
|
|
5135
5161
|
if (!text)
|
|
5136
5162
|
return;
|
|
5137
|
-
const matches =
|
|
5163
|
+
const matches = needles.filter((needle) => text.includes(needle));
|
|
5138
5164
|
if (!matches.length)
|
|
5139
5165
|
return;
|
|
5140
5166
|
return {
|
package/dist/lib/health.d.ts
CHANGED
|
@@ -66,5 +66,6 @@ export declare function classifyRunFailure(run: LoopRun): RunFailureSignal | und
|
|
|
66
66
|
export declare function expectationForLoop(store: Store, loop: Loop): LoopExpectationResult;
|
|
67
67
|
export declare function buildHealthReport(store: Store, opts?: {
|
|
68
68
|
includeArchived?: boolean;
|
|
69
|
+
includeInactive?: boolean;
|
|
69
70
|
limit?: number;
|
|
70
71
|
}): LoopsHealthReport;
|
package/dist/lib/hygiene.d.ts
CHANGED
package/package.json
CHANGED