@hasna/loops 0.3.13 → 0.3.14
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 +41 -12
- package/dist/daemon/index.js +2 -2
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2074,6 +2074,7 @@ class Store {
|
|
|
2074
2074
|
}
|
|
2075
2075
|
|
|
2076
2076
|
// src/cli/index.ts
|
|
2077
|
+
import { createHash } from "crypto";
|
|
2077
2078
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2078
2079
|
import { Command } from "commander";
|
|
2079
2080
|
|
|
@@ -4494,7 +4495,7 @@ function runDoctor(store) {
|
|
|
4494
4495
|
// package.json
|
|
4495
4496
|
var package_default = {
|
|
4496
4497
|
name: "@hasna/loops",
|
|
4497
|
-
version: "0.3.
|
|
4498
|
+
version: "0.3.14",
|
|
4498
4499
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4499
4500
|
type: "module",
|
|
4500
4501
|
main: "dist/index.js",
|
|
@@ -4558,7 +4559,7 @@ var package_default = {
|
|
|
4558
4559
|
bun: ">=1.0.0"
|
|
4559
4560
|
},
|
|
4560
4561
|
dependencies: {
|
|
4561
|
-
"@hasna/events": "^0.1.
|
|
4562
|
+
"@hasna/events": "^0.1.9",
|
|
4562
4563
|
"@hasna/machines": "0.0.49",
|
|
4563
4564
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4564
4565
|
ai: "6.0.204",
|
|
@@ -4956,12 +4957,21 @@ function eventData(event) {
|
|
|
4956
4957
|
return data;
|
|
4957
4958
|
return {};
|
|
4958
4959
|
}
|
|
4960
|
+
function eventMetadata(event) {
|
|
4961
|
+
const metadata = event.metadata;
|
|
4962
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata))
|
|
4963
|
+
return metadata;
|
|
4964
|
+
return {};
|
|
4965
|
+
}
|
|
4959
4966
|
function stringField(value) {
|
|
4960
4967
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
4961
4968
|
}
|
|
4962
4969
|
function slugSegment(value, fallback = "event") {
|
|
4963
4970
|
return value.toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || fallback;
|
|
4964
4971
|
}
|
|
4972
|
+
function stableSuffix(value) {
|
|
4973
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
4974
|
+
}
|
|
4965
4975
|
function taskEventField(data, keys) {
|
|
4966
4976
|
for (const key of keys) {
|
|
4967
4977
|
const direct = stringField(data[key]);
|
|
@@ -5142,12 +5152,23 @@ var eventsHandle = events.command("handle").description("handle a Hasna event en
|
|
|
5142
5152
|
eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--account <profile>", "OpenAccounts profile name").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
5143
5153
|
const event = await readEventEnvelopeFromStdin();
|
|
5144
5154
|
const data = eventData(event);
|
|
5155
|
+
const metadata = eventMetadata(event);
|
|
5145
5156
|
const taskId = taskEventField(data, ["id", "task_id", "taskId"]);
|
|
5146
5157
|
if (!taskId)
|
|
5147
5158
|
throw new Error("todos task event is missing task id in data.id, data.task_id, data.task.id, or data.payload.id");
|
|
5148
5159
|
const taskTitle = taskEventField(data, ["title", "task_title", "taskTitle"]);
|
|
5149
5160
|
const taskDescription = taskEventField(data, ["description", "body"]);
|
|
5150
|
-
const
|
|
5161
|
+
const dataProjectPath = taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd"]);
|
|
5162
|
+
const metadataProjectPath = taskEventField(metadata, [
|
|
5163
|
+
"working_dir",
|
|
5164
|
+
"workingDir",
|
|
5165
|
+
"project_path",
|
|
5166
|
+
"projectPath",
|
|
5167
|
+
"project_canonical_path",
|
|
5168
|
+
"cwd"
|
|
5169
|
+
]);
|
|
5170
|
+
const projectPath = opts.projectPath ?? dataProjectPath ?? metadataProjectPath ?? process.cwd();
|
|
5171
|
+
const idempotencyKey = `todos-task:${taskId}:${event.type}`;
|
|
5151
5172
|
const provider = opts.provider;
|
|
5152
5173
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
|
|
5153
5174
|
throw new Error("unsupported provider");
|
|
@@ -5170,13 +5191,14 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5170
5191
|
eventId: event.id,
|
|
5171
5192
|
eventType: event.type
|
|
5172
5193
|
});
|
|
5173
|
-
const
|
|
5174
|
-
workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${
|
|
5175
|
-
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}`;
|
|
5176
|
-
const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${
|
|
5194
|
+
const idempotencySuffix = stableSuffix(idempotencyKey);
|
|
5195
|
+
workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:workflow`;
|
|
5196
|
+
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}`;
|
|
5197
|
+
const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:run`;
|
|
5198
|
+
const legacyLoopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${event.id.slice(0, 8)}:run`;
|
|
5177
5199
|
const loopInput = {
|
|
5178
5200
|
name: loopName,
|
|
5179
|
-
description: `Run ${workflowBody.name} once for task ${taskId}`,
|
|
5201
|
+
description: `Run ${workflowBody.name} once for task ${taskId}; idempotency=${idempotencyKey}; event=${event.id}`,
|
|
5180
5202
|
schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
|
|
5181
5203
|
target: { type: "workflow", workflowId: "<created-workflow-id>" },
|
|
5182
5204
|
overlap: "skip",
|
|
@@ -5185,15 +5207,22 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5185
5207
|
leaseMs: 90 * 60000
|
|
5186
5208
|
};
|
|
5187
5209
|
if (opts.dryRun) {
|
|
5188
|
-
print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5210
|
+
print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5189
5211
|
return;
|
|
5190
5212
|
}
|
|
5191
5213
|
const store = new Store;
|
|
5192
5214
|
try {
|
|
5193
|
-
const existingLoop = store.findLoopByName(loopName);
|
|
5215
|
+
const existingLoop = store.findLoopByName(loopName) ?? store.findLoopByName(legacyLoopName);
|
|
5194
5216
|
if (existingLoop) {
|
|
5195
5217
|
const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
|
|
5196
|
-
print({
|
|
5218
|
+
print({
|
|
5219
|
+
deduped: true,
|
|
5220
|
+
idempotencyKey,
|
|
5221
|
+
dedupedBy: existingLoop.name === loopName ? "idempotency" : "legacy-event-name",
|
|
5222
|
+
event,
|
|
5223
|
+
workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined,
|
|
5224
|
+
loop: publicLoop(existingLoop)
|
|
5225
|
+
}, `deduped existing loop ${existingLoop.id} (${existingLoop.name}) for event=${event.id} idempotency=${idempotencyKey}`);
|
|
5197
5226
|
return;
|
|
5198
5227
|
}
|
|
5199
5228
|
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
@@ -5202,7 +5231,7 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5202
5231
|
...loopInput,
|
|
5203
5232
|
target: { type: "workflow", workflowId: workflow.id }
|
|
5204
5233
|
});
|
|
5205
|
-
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
5234
|
+
print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
|
|
5206
5235
|
} finally {
|
|
5207
5236
|
store.close();
|
|
5208
5237
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -4276,7 +4276,7 @@ function enableStartup(result) {
|
|
|
4276
4276
|
// package.json
|
|
4277
4277
|
var package_default = {
|
|
4278
4278
|
name: "@hasna/loops",
|
|
4279
|
-
version: "0.3.
|
|
4279
|
+
version: "0.3.14",
|
|
4280
4280
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4281
4281
|
type: "module",
|
|
4282
4282
|
main: "dist/index.js",
|
|
@@ -4340,7 +4340,7 @@ var package_default = {
|
|
|
4340
4340
|
bun: ">=1.0.0"
|
|
4341
4341
|
},
|
|
4342
4342
|
dependencies: {
|
|
4343
|
-
"@hasna/events": "^0.1.
|
|
4343
|
+
"@hasna/events": "^0.1.9",
|
|
4344
4344
|
"@hasna/machines": "0.0.49",
|
|
4345
4345
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4346
4346
|
ai: "6.0.204",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"bun": ">=1.0.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@hasna/events": "^0.1.
|
|
67
|
+
"@hasna/events": "^0.1.9",
|
|
68
68
|
"@hasna/machines": "0.0.49",
|
|
69
69
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
70
70
|
"ai": "6.0.204",
|