@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 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: stableFingerprint([
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 used = new Set;
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 = [scriptsDir, "/.hasna/loops/scripts/"].filter((needle) => text.includes(needle));
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.17",
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, 65535).toString(16)}-worker-verifier`,
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) => {
@@ -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.17",
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, 65535).toString(16)}-worker-verifier`,
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: stableFingerprint([
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 used = new Set;
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 = [scriptsDir, "/.hasna/loops/scripts/"].filter((needle) => text.includes(needle));
5163
+ const matches = needles.filter((needle) => text.includes(needle));
5138
5164
  if (!matches.length)
5139
5165
  return;
5140
5166
  return {
@@ -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;
@@ -16,6 +16,7 @@ export interface NameHygieneReport {
16
16
  checked: number;
17
17
  changed: number;
18
18
  changes: NameHygieneChange[];
19
+ conflicts: NameHygieneChange[];
19
20
  }
20
21
  export interface DuplicateOverlapGroup {
21
22
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",