@hasna/loops 0.3.17 → 0.3.19
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 +4 -3
- package/dist/cli/index.js +65 -20
- 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/docs/USAGE.md +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -318,9 +318,10 @@ them with `no_tmux_dispatch=true` metadata. Use `--dry-run --json` before
|
|
|
318
318
|
turning it into a production loop.
|
|
319
319
|
|
|
320
320
|
`hygiene names` reports canonical `machine-*` or `repo-<name>-*` loop names and
|
|
321
|
-
renames only with `--apply`.
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
renames only with `--apply`. Apply mode writes a SQLite backup under
|
|
322
|
+
`<LOOPS_DATA_DIR>/backups` before changing loop names. `hygiene duplicates`
|
|
323
|
+
groups loops with the same normalized name, cwd, and schedule. `hygiene scripts`
|
|
324
|
+
inventories loops whose command still references `~/.hasna/loops/scripts`.
|
|
324
325
|
|
|
325
326
|
Archive loops when retiring old automation but preserving history:
|
|
326
327
|
|
package/dist/cli/index.js
CHANGED
|
@@ -2196,8 +2196,10 @@ class Store {
|
|
|
2196
2196
|
|
|
2197
2197
|
// src/cli/index.ts
|
|
2198
2198
|
import { createHash as createHash2 } from "crypto";
|
|
2199
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
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
|
+
import { join as join3 } from "path";
|
|
2202
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
2201
2203
|
import { Command } from "commander";
|
|
2202
2204
|
|
|
2203
2205
|
// src/lib/format.ts
|
|
@@ -4640,6 +4642,7 @@ function runDoctor(store) {
|
|
|
4640
4642
|
// src/lib/health.ts
|
|
4641
4643
|
import { createHash } from "crypto";
|
|
4642
4644
|
var EVIDENCE_CHARS = 2000;
|
|
4645
|
+
var FINGERPRINT_EVIDENCE_CHARS = 120;
|
|
4643
4646
|
var CLASSIFICATIONS = [
|
|
4644
4647
|
"rate_limit",
|
|
4645
4648
|
"auth",
|
|
@@ -4668,6 +4671,15 @@ function stableFingerprint(parts) {
|
|
|
4668
4671
|
return createHash("sha256").update(parts.join(`
|
|
4669
4672
|
`)).digest("hex").slice(0, 16);
|
|
4670
4673
|
}
|
|
4674
|
+
function stableFailureFingerprint(run, classification) {
|
|
4675
|
+
return stableFingerprint([
|
|
4676
|
+
run.loopId,
|
|
4677
|
+
classification,
|
|
4678
|
+
String(run.status),
|
|
4679
|
+
String(run.exitCode ?? ""),
|
|
4680
|
+
(run.error ?? run.stderr ?? run.stdout ?? "").replace(/\d{4}-\d{2}-\d{2}T\S+/g, "<timestamp>").slice(0, FINGERPRINT_EVIDENCE_CHARS)
|
|
4681
|
+
]);
|
|
4682
|
+
}
|
|
4671
4683
|
function healthRun(run) {
|
|
4672
4684
|
return {
|
|
4673
4685
|
...run,
|
|
@@ -4701,14 +4713,7 @@ function classifyRunFailure(run) {
|
|
|
4701
4713
|
classification = "sigsegv";
|
|
4702
4714
|
return {
|
|
4703
4715
|
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
|
-
]),
|
|
4716
|
+
fingerprint: stableFailureFingerprint(run, classification),
|
|
4712
4717
|
evidence: {
|
|
4713
4718
|
error: bounded(run.error),
|
|
4714
4719
|
stdout: bounded(run.stdout),
|
|
@@ -4824,7 +4829,7 @@ function expectationForLoop(store, loop) {
|
|
|
4824
4829
|
};
|
|
4825
4830
|
}
|
|
4826
4831
|
function buildHealthReport(store, opts = {}) {
|
|
4827
|
-
const loops = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
|
|
4832
|
+
const loops = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 }).filter((loop) => opts.includeInactive || loop.status === "active" || loop.status === "paused");
|
|
4828
4833
|
const expectations = loops.map((loop) => expectationForLoop(store, loop));
|
|
4829
4834
|
const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
|
|
4830
4835
|
for (const expectation of expectations) {
|
|
@@ -4928,8 +4933,9 @@ function canonicalName(loop) {
|
|
|
4928
4933
|
newName: name
|
|
4929
4934
|
};
|
|
4930
4935
|
}
|
|
4931
|
-
function ensureUnique(changes) {
|
|
4932
|
-
const
|
|
4936
|
+
function ensureUnique(changes, existingNames = []) {
|
|
4937
|
+
const oldNames = new Set(changes.map((change) => change.oldName));
|
|
4938
|
+
const used = new Set([...existingNames].filter((name) => !oldNames.has(name)));
|
|
4933
4939
|
for (const change of changes) {
|
|
4934
4940
|
let candidate = change.newName;
|
|
4935
4941
|
if (!used.has(candidate)) {
|
|
@@ -4959,6 +4965,7 @@ function managedLoops(store, opts) {
|
|
|
4959
4965
|
return loops.filter((loop) => loop.status === "active" || loop.status === "paused");
|
|
4960
4966
|
}
|
|
4961
4967
|
function buildNameHygieneReport(store, opts = {}) {
|
|
4968
|
+
const allLoops = store.listLoops({ includeArchived: true, limit: 1e4 });
|
|
4962
4969
|
const changes = managedLoops(store, opts).map((loop) => {
|
|
4963
4970
|
const canonical = canonicalName(loop);
|
|
4964
4971
|
return {
|
|
@@ -4967,8 +4974,9 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
4967
4974
|
changed: loop.name !== canonical.newName
|
|
4968
4975
|
};
|
|
4969
4976
|
});
|
|
4970
|
-
ensureUnique(changes);
|
|
4977
|
+
ensureUnique(changes, allLoops.map((loop) => loop.name));
|
|
4971
4978
|
const changed = changes.filter((change) => change.changed);
|
|
4979
|
+
const conflicts = changes.filter((change) => allLoops.some((loop) => loop.name === change.newName && loop.id !== change.id));
|
|
4972
4980
|
if (opts.apply) {
|
|
4973
4981
|
for (const change of changed)
|
|
4974
4982
|
store.renameLoop(change.id, change.newName);
|
|
@@ -4979,7 +4987,8 @@ function buildNameHygieneReport(store, opts = {}) {
|
|
|
4979
4987
|
applied: Boolean(opts.apply),
|
|
4980
4988
|
checked: changes.length,
|
|
4981
4989
|
changed: changed.length,
|
|
4982
|
-
changes
|
|
4990
|
+
changes,
|
|
4991
|
+
conflicts
|
|
4983
4992
|
};
|
|
4984
4993
|
}
|
|
4985
4994
|
function baseName(name) {
|
|
@@ -5033,14 +5042,33 @@ function commandText(loop) {
|
|
|
5033
5042
|
return "";
|
|
5034
5043
|
return [loop.target.command, ...loop.target.args ?? []].join(" ");
|
|
5035
5044
|
}
|
|
5045
|
+
function scriptNeedles(scriptsDir) {
|
|
5046
|
+
const home = process.env.HOME ?? "/home/hasna";
|
|
5047
|
+
const normalized = scriptsDir.replace(/\/+$/g, "");
|
|
5048
|
+
const values = [
|
|
5049
|
+
normalized,
|
|
5050
|
+
`${normalized}/`,
|
|
5051
|
+
"~/.hasna/loops/scripts",
|
|
5052
|
+
"~/.hasna/loops/scripts/",
|
|
5053
|
+
"$HOME/.hasna/loops/scripts",
|
|
5054
|
+
"$HOME/.hasna/loops/scripts/",
|
|
5055
|
+
"${HOME}/.hasna/loops/scripts",
|
|
5056
|
+
"${HOME}/.hasna/loops/scripts/",
|
|
5057
|
+
`${home}/.hasna/loops/scripts`,
|
|
5058
|
+
`${home}/.hasna/loops/scripts/`,
|
|
5059
|
+
"/.hasna/loops/scripts/"
|
|
5060
|
+
];
|
|
5061
|
+
return [...new Set(values)];
|
|
5062
|
+
}
|
|
5036
5063
|
function buildScriptInventoryReport(store, opts = {}) {
|
|
5037
5064
|
const scriptsDir = opts.scriptsDir ?? `${process.env.HOME ?? "/home/hasna"}/.hasna/loops/scripts`;
|
|
5065
|
+
const needles = scriptNeedles(scriptsDir);
|
|
5038
5066
|
const loops = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
|
|
5039
5067
|
const scriptBacked = loops.map((loop) => {
|
|
5040
5068
|
const text = commandText(loop);
|
|
5041
5069
|
if (!text)
|
|
5042
5070
|
return;
|
|
5043
|
-
const matches =
|
|
5071
|
+
const matches = needles.filter((needle) => text.includes(needle));
|
|
5044
5072
|
if (!matches.length)
|
|
5045
5073
|
return;
|
|
5046
5074
|
return {
|
|
@@ -5063,7 +5091,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5063
5091
|
// package.json
|
|
5064
5092
|
var package_default = {
|
|
5065
5093
|
name: "@hasna/loops",
|
|
5066
|
-
version: "0.3.
|
|
5094
|
+
version: "0.3.19",
|
|
5067
5095
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5068
5096
|
type: "module",
|
|
5069
5097
|
main: "dist/index.js",
|
|
@@ -5433,7 +5461,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
5433
5461
|
].join(`
|
|
5434
5462
|
`);
|
|
5435
5463
|
return {
|
|
5436
|
-
name: input.name ?? `bounded-agent-${stableIndex(seed,
|
|
5464
|
+
name: input.name ?? `bounded-agent-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
|
|
5437
5465
|
description: `Bounded worker/verifier workflow for ${input.objective.slice(0, 180)}`,
|
|
5438
5466
|
version: 1,
|
|
5439
5467
|
steps: [
|
|
@@ -5716,6 +5744,19 @@ function ensureTodosTaskList(project, slug, name, description) {
|
|
|
5716
5744
|
throw new Error(`todos task list not found after ensure: ${slug}`);
|
|
5717
5745
|
return found.id;
|
|
5718
5746
|
}
|
|
5747
|
+
function backupLoopsDatabase(reason) {
|
|
5748
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z");
|
|
5749
|
+
const backupDir = join3(dataDir(), "backups");
|
|
5750
|
+
mkdirSync5(backupDir, { recursive: true, mode: 448 });
|
|
5751
|
+
const backupPath = join3(backupDir, `loops.db.bak-${reason}-${stamp}`);
|
|
5752
|
+
const db = new Database2(dbPath(), { readonly: true });
|
|
5753
|
+
try {
|
|
5754
|
+
writeFileSync3(backupPath, db.serialize(), { mode: 384 });
|
|
5755
|
+
} finally {
|
|
5756
|
+
db.close();
|
|
5757
|
+
}
|
|
5758
|
+
return backupPath;
|
|
5759
|
+
}
|
|
5719
5760
|
function eventData(event) {
|
|
5720
5761
|
const data = event.data;
|
|
5721
5762
|
if (data && typeof data === "object" && !Array.isArray(data))
|
|
@@ -6468,10 +6509,10 @@ var health = program.command("health").description("summarize loop health and la
|
|
|
6468
6509
|
store.close();
|
|
6469
6510
|
}
|
|
6470
6511
|
});
|
|
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) => {
|
|
6512
|
+
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
6513
|
const store = new Store;
|
|
6473
6514
|
try {
|
|
6474
|
-
const report = buildHealthReport(store, { limit: Number(opts.limit) });
|
|
6515
|
+
const report = buildHealthReport(store, { limit: Number(opts.limit), includeInactive: Boolean(opts.includeInactive) });
|
|
6475
6516
|
const failures = report.expectations.filter((entry) => !entry.ok && entry.recommendedTask).slice(0, Number(opts.maxActions));
|
|
6476
6517
|
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
6518
|
const actions = failures.map((expectation) => {
|
|
@@ -6534,16 +6575,20 @@ var hygiene = program.command("hygiene").description("deterministic OpenLoops hy
|
|
|
6534
6575
|
hygiene.command("names").description("check or apply canonical machine-/repo-prefixed loop names").option("--apply", "rename loops in-place").option("--include-stopped", "include stopped loops").option("--include-inactive", "include stopped, expired, and archived loops").option("--limit <n>", "maximum loops to inspect", "1000").option("--json", "print JSON").action((opts) => {
|
|
6535
6576
|
const store = new Store;
|
|
6536
6577
|
try {
|
|
6578
|
+
const backupPath = opts.apply ? backupLoopsDatabase("name-hygiene") : undefined;
|
|
6537
6579
|
const report = buildNameHygieneReport(store, {
|
|
6538
6580
|
apply: Boolean(opts.apply),
|
|
6539
6581
|
includeStopped: Boolean(opts.includeStopped),
|
|
6540
6582
|
includeInactive: Boolean(opts.includeInactive),
|
|
6541
6583
|
limit: Number(opts.limit)
|
|
6542
6584
|
});
|
|
6585
|
+
const output = backupPath ? { ...report, backupPath } : report;
|
|
6543
6586
|
if (isJson() || opts.json)
|
|
6544
|
-
console.log(JSON.stringify(
|
|
6587
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6545
6588
|
else {
|
|
6546
6589
|
console.log(`hygiene_names checked=${report.checked} changed=${report.changed} applied=${report.applied}`);
|
|
6590
|
+
if (backupPath)
|
|
6591
|
+
console.log(`backup=${backupPath}`);
|
|
6547
6592
|
for (const change of report.changes.filter((entry) => entry.changed)) {
|
|
6548
6593
|
console.log(`${report.applied ? "renamed" : "would-rename"} ${change.id} ${change.oldName} -> ${change.newName}`);
|
|
6549
6594
|
}
|
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.19",
|
|
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/docs/USAGE.md
CHANGED
|
@@ -325,10 +325,11 @@ loops hygiene scripts --json
|
|
|
325
325
|
```
|
|
326
326
|
|
|
327
327
|
`hygiene names` reports canonical `machine-*` or `repo-<name>-*` loop names and
|
|
328
|
-
only renames when `--apply` is present.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
328
|
+
only renames when `--apply` is present. Apply mode writes a SQLite backup under
|
|
329
|
+
`<LOOPS_DATA_DIR>/backups` before changing loop names. `hygiene duplicates`
|
|
330
|
+
groups loops with the same normalized name, cwd, and schedule. `hygiene scripts`
|
|
331
|
+
inventories loops whose command still references `~/.hasna/loops/scripts`; use
|
|
332
|
+
it as a migration gate before deleting local scripts.
|
|
332
333
|
|
|
333
334
|
Archive loops when retiring old automation but preserving history:
|
|
334
335
|
|
package/package.json
CHANGED