@hasna/loops 0.4.3 → 0.4.4
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/CHANGELOG.md +76 -0
- package/README.md +6 -1
- package/dist/api/index.js +2 -2
- package/dist/cli/index.js +928 -418
- package/dist/daemon/index.js +188 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +759 -70
- package/dist/lib/migration.d.ts +109 -0
- package/dist/lib/mode.js +2 -2
- package/dist/lib/route/todos-cli.d.ts +0 -1
- package/dist/lib/route/types.d.ts +0 -10
- package/dist/lib/storage/index.js +186 -2
- package/dist/lib/storage/sqlite.js +186 -2
- package/dist/lib/store.d.ts +41 -0
- package/dist/lib/store.js +186 -2
- package/dist/lib/template-kit.d.ts +8 -10
- package/dist/lib/templates.d.ts +0 -1
- package/dist/mcp/index.js +188 -4
- package/dist/runner/index.js +2 -2
- package/dist/sdk/index.d.ts +9 -0
- package/dist/sdk/index.js +983 -3
- package/docs/DEPLOYMENT_MODES.md +73 -5
- package/docs/USAGE.md +52 -1
- package/package.json +2 -2
package/dist/lib/store.js
CHANGED
|
@@ -1523,7 +1523,7 @@ class Store {
|
|
|
1523
1523
|
}
|
|
1524
1524
|
const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
1525
1525
|
for (const migration of this.migrations()) {
|
|
1526
|
-
if (!migration.baseline &&
|
|
1526
|
+
if (!migration.baseline && applied.has(migration.id))
|
|
1527
1527
|
continue;
|
|
1528
1528
|
migration.apply();
|
|
1529
1529
|
if (!applied.has(migration.id)) {
|
|
@@ -1576,7 +1576,6 @@ class Store {
|
|
|
1576
1576
|
},
|
|
1577
1577
|
{
|
|
1578
1578
|
id: "0007_run_claim_tokens",
|
|
1579
|
-
reapply: true,
|
|
1580
1579
|
apply: () => {
|
|
1581
1580
|
this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
|
|
1582
1581
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
|
|
@@ -1641,6 +1640,7 @@ class Store {
|
|
|
1641
1640
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1642
1641
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1643
1642
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1643
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1644
1644
|
|
|
1645
1645
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1646
1646
|
id TEXT PRIMARY KEY,
|
|
@@ -3841,6 +3841,190 @@ class Store {
|
|
|
3841
3841
|
const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loop_runs").get();
|
|
3842
3842
|
return row?.count ?? 0;
|
|
3843
3843
|
}
|
|
3844
|
+
exportMigrationRows(opts = {}) {
|
|
3845
|
+
const includeRuns = opts.includeRuns ?? true;
|
|
3846
|
+
const workflows = this.db.query("SELECT * FROM workflow_specs ORDER BY created_at ASC, id ASC").all().map(rowToWorkflow);
|
|
3847
|
+
const loops = this.db.query("SELECT * FROM loops ORDER BY created_at ASC, id ASC").all().map(rowToLoop);
|
|
3848
|
+
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
3849
|
+
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
3850
|
+
}
|
|
3851
|
+
countTable(table) {
|
|
3852
|
+
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
3853
|
+
return row?.count ?? 0;
|
|
3854
|
+
}
|
|
3855
|
+
migrationChecks() {
|
|
3856
|
+
const now = nowIso();
|
|
3857
|
+
return {
|
|
3858
|
+
unsupportedCounts: {
|
|
3859
|
+
workflowInvocations: this.countTable("workflow_invocations"),
|
|
3860
|
+
workflowWorkItems: this.countTable("workflow_work_items"),
|
|
3861
|
+
workflowRuns: this.countTable("workflow_runs"),
|
|
3862
|
+
workflowStepRuns: this.countTable("workflow_step_runs"),
|
|
3863
|
+
workflowEvents: this.countTable("workflow_events"),
|
|
3864
|
+
goals: this.countTable("goals"),
|
|
3865
|
+
goalPlanNodes: this.countTable("goal_plan_nodes"),
|
|
3866
|
+
goalRuns: this.countTable("goal_runs")
|
|
3867
|
+
},
|
|
3868
|
+
volatileCounts: {
|
|
3869
|
+
daemonLeases: this.countTable("daemon_lease"),
|
|
3870
|
+
activeDaemonLeases: this.db.query("SELECT COUNT(*) AS count FROM daemon_lease WHERE expires_at > ?").get(now)?.count ?? 0,
|
|
3871
|
+
runningLoopRuns: this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3872
|
+
runningWorkflowRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3873
|
+
runningWorkflowStepRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3874
|
+
leasedWorkflowWorkItems: this.db.query("SELECT COUNT(*) AS count FROM workflow_work_items WHERE lease_expires_at IS NOT NULL OR status IN ('admitted', 'running')").get()?.count ?? 0
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
upsertMigrationWorkflow(workflow, opts = {}) {
|
|
3879
|
+
const existing = this.getWorkflow(workflow.id);
|
|
3880
|
+
if (existing && !opts.replace)
|
|
3881
|
+
return existing;
|
|
3882
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
3883
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)
|
|
3884
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3885
|
+
name=$name,
|
|
3886
|
+
description=$description,
|
|
3887
|
+
version=$version,
|
|
3888
|
+
status=$status,
|
|
3889
|
+
goal_json=$goal,
|
|
3890
|
+
steps_json=$steps,
|
|
3891
|
+
created_at=$created,
|
|
3892
|
+
updated_at=$updated`).run({
|
|
3893
|
+
$id: workflow.id,
|
|
3894
|
+
$name: workflow.name,
|
|
3895
|
+
$description: workflow.description ?? null,
|
|
3896
|
+
$version: workflow.version,
|
|
3897
|
+
$status: workflow.status,
|
|
3898
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
3899
|
+
$steps: JSON.stringify(workflow.steps),
|
|
3900
|
+
$created: workflow.createdAt,
|
|
3901
|
+
$updated: workflow.updatedAt
|
|
3902
|
+
});
|
|
3903
|
+
const imported = this.getWorkflow(workflow.id);
|
|
3904
|
+
if (!imported)
|
|
3905
|
+
throw new Error(`workflow not found after migration import: ${workflow.id}`);
|
|
3906
|
+
return imported;
|
|
3907
|
+
}
|
|
3908
|
+
upsertMigrationLoop(loop, opts = {}) {
|
|
3909
|
+
const existing = this.getLoop(loop.id);
|
|
3910
|
+
if (existing && !opts.replace)
|
|
3911
|
+
return existing;
|
|
3912
|
+
this.assertNoNestedWorkflowGoal(loop.target, loop.goal);
|
|
3913
|
+
this.db.query(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
|
|
3914
|
+
goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
|
|
3915
|
+
retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
3916
|
+
VALUES ($id, $name, $description, $status, $archivedAt, $archivedFromStatus, $schedule, $target,
|
|
3917
|
+
$goal, $machine, $nextRun, $retrySlot, $catchUp, $catchUpLimit, $overlap, $maxAttempts,
|
|
3918
|
+
$retryDelay, $leaseMs, $expiresAt, $created, $updated)
|
|
3919
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3920
|
+
name=$name,
|
|
3921
|
+
description=$description,
|
|
3922
|
+
status=$status,
|
|
3923
|
+
archived_at=$archivedAt,
|
|
3924
|
+
archived_from_status=$archivedFromStatus,
|
|
3925
|
+
schedule_json=$schedule,
|
|
3926
|
+
target_json=$target,
|
|
3927
|
+
goal_json=$goal,
|
|
3928
|
+
machine_json=$machine,
|
|
3929
|
+
next_run_at=$nextRun,
|
|
3930
|
+
retry_scheduled_for=$retrySlot,
|
|
3931
|
+
catch_up=$catchUp,
|
|
3932
|
+
catch_up_limit=$catchUpLimit,
|
|
3933
|
+
overlap=$overlap,
|
|
3934
|
+
max_attempts=$maxAttempts,
|
|
3935
|
+
retry_delay_ms=$retryDelay,
|
|
3936
|
+
lease_ms=$leaseMs,
|
|
3937
|
+
expires_at=$expiresAt,
|
|
3938
|
+
created_at=$created,
|
|
3939
|
+
updated_at=$updated`).run({
|
|
3940
|
+
$id: loop.id,
|
|
3941
|
+
$name: loop.name,
|
|
3942
|
+
$description: loop.description ?? null,
|
|
3943
|
+
$status: loop.status,
|
|
3944
|
+
$archivedAt: loop.archivedAt ?? null,
|
|
3945
|
+
$archivedFromStatus: loop.archivedFromStatus ?? null,
|
|
3946
|
+
$schedule: JSON.stringify(loop.schedule),
|
|
3947
|
+
$target: JSON.stringify(loop.target),
|
|
3948
|
+
$goal: loop.goal ? JSON.stringify(loop.goal) : null,
|
|
3949
|
+
$machine: loop.machine ? JSON.stringify(loop.machine) : null,
|
|
3950
|
+
$nextRun: loop.nextRunAt ?? null,
|
|
3951
|
+
$retrySlot: loop.retryScheduledFor ?? null,
|
|
3952
|
+
$catchUp: loop.catchUp,
|
|
3953
|
+
$catchUpLimit: loop.catchUpLimit,
|
|
3954
|
+
$overlap: loop.overlap,
|
|
3955
|
+
$maxAttempts: loop.maxAttempts,
|
|
3956
|
+
$retryDelay: loop.retryDelayMs,
|
|
3957
|
+
$leaseMs: loop.leaseMs,
|
|
3958
|
+
$expiresAt: loop.expiresAt ?? null,
|
|
3959
|
+
$created: loop.createdAt,
|
|
3960
|
+
$updated: loop.updatedAt
|
|
3961
|
+
});
|
|
3962
|
+
const imported = this.getLoop(loop.id);
|
|
3963
|
+
if (!imported)
|
|
3964
|
+
throw new Error(`loop not found after migration import: ${loop.id}`);
|
|
3965
|
+
return imported;
|
|
3966
|
+
}
|
|
3967
|
+
upsertMigrationRun(run, opts = {}) {
|
|
3968
|
+
if (run.status === "running")
|
|
3969
|
+
throw new ValidationError(`cannot import running run ${run.id}`);
|
|
3970
|
+
const existing = this.getRun(run.id);
|
|
3971
|
+
if (existing && !opts.replace)
|
|
3972
|
+
return existing;
|
|
3973
|
+
this.db.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
3974
|
+
claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
|
|
3975
|
+
stdout, stderr, error, goal_run_id, created_at, updated_at)
|
|
3976
|
+
VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, $startedAt, $finishedAt,
|
|
3977
|
+
$claimedBy, NULL, $leaseExpiresAt, $pid, $pgid, $processStartedAt, $exitCode, $durationMs,
|
|
3978
|
+
$stdout, $stderr, $error, $goalRunId, $created, $updated)
|
|
3979
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3980
|
+
loop_id=$loopId,
|
|
3981
|
+
loop_name=$loopName,
|
|
3982
|
+
scheduled_for=$scheduledFor,
|
|
3983
|
+
attempt=$attempt,
|
|
3984
|
+
status=$status,
|
|
3985
|
+
started_at=$startedAt,
|
|
3986
|
+
finished_at=$finishedAt,
|
|
3987
|
+
claimed_by=$claimedBy,
|
|
3988
|
+
claim_token=NULL,
|
|
3989
|
+
lease_expires_at=$leaseExpiresAt,
|
|
3990
|
+
pid=$pid,
|
|
3991
|
+
pgid=$pgid,
|
|
3992
|
+
process_started_at=$processStartedAt,
|
|
3993
|
+
exit_code=$exitCode,
|
|
3994
|
+
duration_ms=$durationMs,
|
|
3995
|
+
stdout=$stdout,
|
|
3996
|
+
stderr=$stderr,
|
|
3997
|
+
error=$error,
|
|
3998
|
+
goal_run_id=$goalRunId,
|
|
3999
|
+
created_at=$created,
|
|
4000
|
+
updated_at=$updated`).run({
|
|
4001
|
+
$id: run.id,
|
|
4002
|
+
$loopId: run.loopId,
|
|
4003
|
+
$loopName: run.loopName,
|
|
4004
|
+
$scheduledFor: run.scheduledFor,
|
|
4005
|
+
$attempt: run.attempt,
|
|
4006
|
+
$status: run.status,
|
|
4007
|
+
$startedAt: run.startedAt ?? null,
|
|
4008
|
+
$finishedAt: run.finishedAt ?? null,
|
|
4009
|
+
$claimedBy: run.claimedBy ?? null,
|
|
4010
|
+
$leaseExpiresAt: run.leaseExpiresAt ?? null,
|
|
4011
|
+
$pid: run.pid ?? null,
|
|
4012
|
+
$pgid: run.pgid ?? null,
|
|
4013
|
+
$processStartedAt: run.processStartedAt ?? null,
|
|
4014
|
+
$exitCode: run.exitCode ?? null,
|
|
4015
|
+
$durationMs: run.durationMs ?? null,
|
|
4016
|
+
$stdout: scrubbedOrNull(run.stdout),
|
|
4017
|
+
$stderr: scrubbedOrNull(run.stderr),
|
|
4018
|
+
$error: scrubbedOrNull(run.error),
|
|
4019
|
+
$goalRunId: run.goalRunId ?? null,
|
|
4020
|
+
$created: run.createdAt,
|
|
4021
|
+
$updated: run.updatedAt
|
|
4022
|
+
});
|
|
4023
|
+
const imported = this.getRun(run.id);
|
|
4024
|
+
if (!imported)
|
|
4025
|
+
throw new Error(`run not found after migration import: ${run.id}`);
|
|
4026
|
+
return imported;
|
|
4027
|
+
}
|
|
3844
4028
|
pruneHistory(opts) {
|
|
3845
4029
|
const { maxAgeDays, keepPerLoop } = opts;
|
|
3846
4030
|
if (maxAgeDays === undefined && keepPerLoop === undefined) {
|
|
@@ -45,26 +45,24 @@ export declare function verifierIdleTimeoutMs(input: {
|
|
|
45
45
|
export declare function verifierRuntimeGuidance(input: {
|
|
46
46
|
verifierIdleTimeoutMs?: number;
|
|
47
47
|
}): string;
|
|
48
|
-
export declare function todosInspectLine(todosProjectPath: string, taskId: string
|
|
49
|
-
export declare function todosStartLine(todosProjectPath: string, taskId: string
|
|
50
|
-
export declare function todosEvidenceLine(todosProjectPath: string, taskId: string, placeholder: string
|
|
51
|
-
export declare function todosVerificationLine(todosProjectPath: string, taskId: string
|
|
52
|
-
export declare function todosDoneLine(todosProjectPath: string, taskId: string
|
|
48
|
+
export declare function todosInspectLine(todosProjectPath: string, taskId: string): string;
|
|
49
|
+
export declare function todosStartLine(todosProjectPath: string, taskId: string): string;
|
|
50
|
+
export declare function todosEvidenceLine(todosProjectPath: string, taskId: string, placeholder: string): string;
|
|
51
|
+
export declare function todosVerificationLine(todosProjectPath: string, taskId: string): string;
|
|
52
|
+
export declare function todosDoneLine(todosProjectPath: string, taskId: string): string;
|
|
53
53
|
/** Exact-todos-commands stanza: project pin, cwd-inference warning, inspect, then role-specific command lines. */
|
|
54
|
-
export declare function todosExactCommandsFragment(todosProjectPath: string, taskId: string, commandLines: string[]
|
|
54
|
+
export declare function todosExactCommandsFragment(todosProjectPath: string, taskId: string, commandLines: string[]): string[];
|
|
55
55
|
/** Worktree policy PROSE for agent guidance; enforcement is executor-native via target.worktree. */
|
|
56
56
|
export declare function worktreePrompt(plan: AgentWorktreeSpec): string;
|
|
57
57
|
export declare function worktreeContextFragment(plan: AgentWorktreeSpec): Record<string, unknown>;
|
|
58
58
|
export declare function shellQuote(value: string): string;
|
|
59
|
-
export declare function
|
|
60
|
-
export declare function sourceTaskGateCommand(todosProjectPath: string, taskId: string, todosDbPath?: string): string;
|
|
59
|
+
export declare function sourceTaskGateCommand(todosProjectPath: string, taskId: string): string;
|
|
61
60
|
export type LifecycleGateStage = "triage" | "planner";
|
|
62
|
-
export declare function lifecycleGateCommand(todosProjectPath: string, taskId: string, stage: LifecycleGateStage, goMarker: string, blockedMarker: string
|
|
61
|
+
export declare function lifecycleGateCommand(todosProjectPath: string, taskId: string, stage: LifecycleGateStage, goMarker: string, blockedMarker: string): string;
|
|
63
62
|
export interface PrHandoffCommandOptions {
|
|
64
63
|
artifactPath: string;
|
|
65
64
|
taskId: string;
|
|
66
65
|
todosProjectPath: string;
|
|
67
|
-
todosDbPath?: string;
|
|
68
66
|
worktreeCwd: string;
|
|
69
67
|
worktreeRoot: string;
|
|
70
68
|
expectedBranch: string;
|
package/dist/lib/templates.d.ts
CHANGED
package/dist/mcp/index.js
CHANGED
|
@@ -1525,7 +1525,7 @@ class Store {
|
|
|
1525
1525
|
}
|
|
1526
1526
|
const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
1527
1527
|
for (const migration of this.migrations()) {
|
|
1528
|
-
if (!migration.baseline &&
|
|
1528
|
+
if (!migration.baseline && applied.has(migration.id))
|
|
1529
1529
|
continue;
|
|
1530
1530
|
migration.apply();
|
|
1531
1531
|
if (!applied.has(migration.id)) {
|
|
@@ -1578,7 +1578,6 @@ class Store {
|
|
|
1578
1578
|
},
|
|
1579
1579
|
{
|
|
1580
1580
|
id: "0007_run_claim_tokens",
|
|
1581
|
-
reapply: true,
|
|
1582
1581
|
apply: () => {
|
|
1583
1582
|
this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
|
|
1584
1583
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
|
|
@@ -1643,6 +1642,7 @@ class Store {
|
|
|
1643
1642
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1644
1643
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1645
1644
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1645
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1646
1646
|
|
|
1647
1647
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1648
1648
|
id TEXT PRIMARY KEY,
|
|
@@ -3843,6 +3843,190 @@ class Store {
|
|
|
3843
3843
|
const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loop_runs").get();
|
|
3844
3844
|
return row?.count ?? 0;
|
|
3845
3845
|
}
|
|
3846
|
+
exportMigrationRows(opts = {}) {
|
|
3847
|
+
const includeRuns = opts.includeRuns ?? true;
|
|
3848
|
+
const workflows = this.db.query("SELECT * FROM workflow_specs ORDER BY created_at ASC, id ASC").all().map(rowToWorkflow);
|
|
3849
|
+
const loops = this.db.query("SELECT * FROM loops ORDER BY created_at ASC, id ASC").all().map(rowToLoop);
|
|
3850
|
+
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
3851
|
+
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
3852
|
+
}
|
|
3853
|
+
countTable(table) {
|
|
3854
|
+
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
3855
|
+
return row?.count ?? 0;
|
|
3856
|
+
}
|
|
3857
|
+
migrationChecks() {
|
|
3858
|
+
const now = nowIso();
|
|
3859
|
+
return {
|
|
3860
|
+
unsupportedCounts: {
|
|
3861
|
+
workflowInvocations: this.countTable("workflow_invocations"),
|
|
3862
|
+
workflowWorkItems: this.countTable("workflow_work_items"),
|
|
3863
|
+
workflowRuns: this.countTable("workflow_runs"),
|
|
3864
|
+
workflowStepRuns: this.countTable("workflow_step_runs"),
|
|
3865
|
+
workflowEvents: this.countTable("workflow_events"),
|
|
3866
|
+
goals: this.countTable("goals"),
|
|
3867
|
+
goalPlanNodes: this.countTable("goal_plan_nodes"),
|
|
3868
|
+
goalRuns: this.countTable("goal_runs")
|
|
3869
|
+
},
|
|
3870
|
+
volatileCounts: {
|
|
3871
|
+
daemonLeases: this.countTable("daemon_lease"),
|
|
3872
|
+
activeDaemonLeases: this.db.query("SELECT COUNT(*) AS count FROM daemon_lease WHERE expires_at > ?").get(now)?.count ?? 0,
|
|
3873
|
+
runningLoopRuns: this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3874
|
+
runningWorkflowRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3875
|
+
runningWorkflowStepRuns: this.db.query("SELECT COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running'").get()?.count ?? 0,
|
|
3876
|
+
leasedWorkflowWorkItems: this.db.query("SELECT COUNT(*) AS count FROM workflow_work_items WHERE lease_expires_at IS NOT NULL OR status IN ('admitted', 'running')").get()?.count ?? 0
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
upsertMigrationWorkflow(workflow, opts = {}) {
|
|
3881
|
+
const existing = this.getWorkflow(workflow.id);
|
|
3882
|
+
if (existing && !opts.replace)
|
|
3883
|
+
return existing;
|
|
3884
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
3885
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)
|
|
3886
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3887
|
+
name=$name,
|
|
3888
|
+
description=$description,
|
|
3889
|
+
version=$version,
|
|
3890
|
+
status=$status,
|
|
3891
|
+
goal_json=$goal,
|
|
3892
|
+
steps_json=$steps,
|
|
3893
|
+
created_at=$created,
|
|
3894
|
+
updated_at=$updated`).run({
|
|
3895
|
+
$id: workflow.id,
|
|
3896
|
+
$name: workflow.name,
|
|
3897
|
+
$description: workflow.description ?? null,
|
|
3898
|
+
$version: workflow.version,
|
|
3899
|
+
$status: workflow.status,
|
|
3900
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
3901
|
+
$steps: JSON.stringify(workflow.steps),
|
|
3902
|
+
$created: workflow.createdAt,
|
|
3903
|
+
$updated: workflow.updatedAt
|
|
3904
|
+
});
|
|
3905
|
+
const imported = this.getWorkflow(workflow.id);
|
|
3906
|
+
if (!imported)
|
|
3907
|
+
throw new Error(`workflow not found after migration import: ${workflow.id}`);
|
|
3908
|
+
return imported;
|
|
3909
|
+
}
|
|
3910
|
+
upsertMigrationLoop(loop, opts = {}) {
|
|
3911
|
+
const existing = this.getLoop(loop.id);
|
|
3912
|
+
if (existing && !opts.replace)
|
|
3913
|
+
return existing;
|
|
3914
|
+
this.assertNoNestedWorkflowGoal(loop.target, loop.goal);
|
|
3915
|
+
this.db.query(`INSERT INTO loops (id, name, description, status, archived_at, archived_from_status, schedule_json, target_json,
|
|
3916
|
+
goal_json, machine_json, next_run_at, retry_scheduled_for, catch_up, catch_up_limit, overlap, max_attempts,
|
|
3917
|
+
retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
|
|
3918
|
+
VALUES ($id, $name, $description, $status, $archivedAt, $archivedFromStatus, $schedule, $target,
|
|
3919
|
+
$goal, $machine, $nextRun, $retrySlot, $catchUp, $catchUpLimit, $overlap, $maxAttempts,
|
|
3920
|
+
$retryDelay, $leaseMs, $expiresAt, $created, $updated)
|
|
3921
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3922
|
+
name=$name,
|
|
3923
|
+
description=$description,
|
|
3924
|
+
status=$status,
|
|
3925
|
+
archived_at=$archivedAt,
|
|
3926
|
+
archived_from_status=$archivedFromStatus,
|
|
3927
|
+
schedule_json=$schedule,
|
|
3928
|
+
target_json=$target,
|
|
3929
|
+
goal_json=$goal,
|
|
3930
|
+
machine_json=$machine,
|
|
3931
|
+
next_run_at=$nextRun,
|
|
3932
|
+
retry_scheduled_for=$retrySlot,
|
|
3933
|
+
catch_up=$catchUp,
|
|
3934
|
+
catch_up_limit=$catchUpLimit,
|
|
3935
|
+
overlap=$overlap,
|
|
3936
|
+
max_attempts=$maxAttempts,
|
|
3937
|
+
retry_delay_ms=$retryDelay,
|
|
3938
|
+
lease_ms=$leaseMs,
|
|
3939
|
+
expires_at=$expiresAt,
|
|
3940
|
+
created_at=$created,
|
|
3941
|
+
updated_at=$updated`).run({
|
|
3942
|
+
$id: loop.id,
|
|
3943
|
+
$name: loop.name,
|
|
3944
|
+
$description: loop.description ?? null,
|
|
3945
|
+
$status: loop.status,
|
|
3946
|
+
$archivedAt: loop.archivedAt ?? null,
|
|
3947
|
+
$archivedFromStatus: loop.archivedFromStatus ?? null,
|
|
3948
|
+
$schedule: JSON.stringify(loop.schedule),
|
|
3949
|
+
$target: JSON.stringify(loop.target),
|
|
3950
|
+
$goal: loop.goal ? JSON.stringify(loop.goal) : null,
|
|
3951
|
+
$machine: loop.machine ? JSON.stringify(loop.machine) : null,
|
|
3952
|
+
$nextRun: loop.nextRunAt ?? null,
|
|
3953
|
+
$retrySlot: loop.retryScheduledFor ?? null,
|
|
3954
|
+
$catchUp: loop.catchUp,
|
|
3955
|
+
$catchUpLimit: loop.catchUpLimit,
|
|
3956
|
+
$overlap: loop.overlap,
|
|
3957
|
+
$maxAttempts: loop.maxAttempts,
|
|
3958
|
+
$retryDelay: loop.retryDelayMs,
|
|
3959
|
+
$leaseMs: loop.leaseMs,
|
|
3960
|
+
$expiresAt: loop.expiresAt ?? null,
|
|
3961
|
+
$created: loop.createdAt,
|
|
3962
|
+
$updated: loop.updatedAt
|
|
3963
|
+
});
|
|
3964
|
+
const imported = this.getLoop(loop.id);
|
|
3965
|
+
if (!imported)
|
|
3966
|
+
throw new Error(`loop not found after migration import: ${loop.id}`);
|
|
3967
|
+
return imported;
|
|
3968
|
+
}
|
|
3969
|
+
upsertMigrationRun(run, opts = {}) {
|
|
3970
|
+
if (run.status === "running")
|
|
3971
|
+
throw new ValidationError(`cannot import running run ${run.id}`);
|
|
3972
|
+
const existing = this.getRun(run.id);
|
|
3973
|
+
if (existing && !opts.replace)
|
|
3974
|
+
return existing;
|
|
3975
|
+
this.db.query(`INSERT INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
3976
|
+
claimed_by, claim_token, lease_expires_at, pid, pgid, process_started_at, exit_code, duration_ms,
|
|
3977
|
+
stdout, stderr, error, goal_run_id, created_at, updated_at)
|
|
3978
|
+
VALUES ($id, $loopId, $loopName, $scheduledFor, $attempt, $status, $startedAt, $finishedAt,
|
|
3979
|
+
$claimedBy, NULL, $leaseExpiresAt, $pid, $pgid, $processStartedAt, $exitCode, $durationMs,
|
|
3980
|
+
$stdout, $stderr, $error, $goalRunId, $created, $updated)
|
|
3981
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3982
|
+
loop_id=$loopId,
|
|
3983
|
+
loop_name=$loopName,
|
|
3984
|
+
scheduled_for=$scheduledFor,
|
|
3985
|
+
attempt=$attempt,
|
|
3986
|
+
status=$status,
|
|
3987
|
+
started_at=$startedAt,
|
|
3988
|
+
finished_at=$finishedAt,
|
|
3989
|
+
claimed_by=$claimedBy,
|
|
3990
|
+
claim_token=NULL,
|
|
3991
|
+
lease_expires_at=$leaseExpiresAt,
|
|
3992
|
+
pid=$pid,
|
|
3993
|
+
pgid=$pgid,
|
|
3994
|
+
process_started_at=$processStartedAt,
|
|
3995
|
+
exit_code=$exitCode,
|
|
3996
|
+
duration_ms=$durationMs,
|
|
3997
|
+
stdout=$stdout,
|
|
3998
|
+
stderr=$stderr,
|
|
3999
|
+
error=$error,
|
|
4000
|
+
goal_run_id=$goalRunId,
|
|
4001
|
+
created_at=$created,
|
|
4002
|
+
updated_at=$updated`).run({
|
|
4003
|
+
$id: run.id,
|
|
4004
|
+
$loopId: run.loopId,
|
|
4005
|
+
$loopName: run.loopName,
|
|
4006
|
+
$scheduledFor: run.scheduledFor,
|
|
4007
|
+
$attempt: run.attempt,
|
|
4008
|
+
$status: run.status,
|
|
4009
|
+
$startedAt: run.startedAt ?? null,
|
|
4010
|
+
$finishedAt: run.finishedAt ?? null,
|
|
4011
|
+
$claimedBy: run.claimedBy ?? null,
|
|
4012
|
+
$leaseExpiresAt: run.leaseExpiresAt ?? null,
|
|
4013
|
+
$pid: run.pid ?? null,
|
|
4014
|
+
$pgid: run.pgid ?? null,
|
|
4015
|
+
$processStartedAt: run.processStartedAt ?? null,
|
|
4016
|
+
$exitCode: run.exitCode ?? null,
|
|
4017
|
+
$durationMs: run.durationMs ?? null,
|
|
4018
|
+
$stdout: scrubbedOrNull(run.stdout),
|
|
4019
|
+
$stderr: scrubbedOrNull(run.stderr),
|
|
4020
|
+
$error: scrubbedOrNull(run.error),
|
|
4021
|
+
$goalRunId: run.goalRunId ?? null,
|
|
4022
|
+
$created: run.createdAt,
|
|
4023
|
+
$updated: run.updatedAt
|
|
4024
|
+
});
|
|
4025
|
+
const imported = this.getRun(run.id);
|
|
4026
|
+
if (!imported)
|
|
4027
|
+
throw new Error(`run not found after migration import: ${run.id}`);
|
|
4028
|
+
return imported;
|
|
4029
|
+
}
|
|
3846
4030
|
pruneHistory(opts) {
|
|
3847
4031
|
const { maxAgeDays, keepPerLoop } = opts;
|
|
3848
4032
|
if (maxAgeDays === undefined && keepPerLoop === undefined) {
|
|
@@ -7346,7 +7530,7 @@ async function tick(deps) {
|
|
|
7346
7530
|
// package.json
|
|
7347
7531
|
var package_default = {
|
|
7348
7532
|
name: "@hasna/loops",
|
|
7349
|
-
version: "0.4.
|
|
7533
|
+
version: "0.4.4",
|
|
7350
7534
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7351
7535
|
type: "module",
|
|
7352
7536
|
main: "dist/index.js",
|
|
@@ -7420,7 +7604,7 @@ var package_default = {
|
|
|
7420
7604
|
prepare: "test -d dist || bun run build",
|
|
7421
7605
|
"dev:cli": "bun run src/cli/index.ts",
|
|
7422
7606
|
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7423
|
-
prepublishOnly: "bun run build && bun run test:boundary"
|
|
7607
|
+
prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
|
|
7424
7608
|
},
|
|
7425
7609
|
keywords: [
|
|
7426
7610
|
"loops",
|
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.4",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -77,7 +77,7 @@ var package_default = {
|
|
|
77
77
|
prepare: "test -d dist || bun run build",
|
|
78
78
|
"dev:cli": "bun run src/cli/index.ts",
|
|
79
79
|
"dev:daemon": "bun run src/daemon/index.ts",
|
|
80
|
-
prepublishOnly: "bun run build && bun run test:boundary"
|
|
80
|
+
prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
|
|
81
81
|
},
|
|
82
82
|
keywords: [
|
|
83
83
|
"loops",
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CreateLoopInput, Goal, GoalRun, Loop, LoopRun, LoopStatus, OpenAutomationsRuntimeBinding, RunStatus } from "../types.js";
|
|
2
2
|
import { type DoctorReport } from "../lib/doctor.js";
|
|
3
3
|
import { type LoopsHealthReport } from "../lib/health.js";
|
|
4
|
+
import { type ApplyLoopsMigrationResult, type ExportLoopsMigrationOptions, type ImportLoopsMigrationOptions, type LoopsMigrationBundle, type LoopsMigrationPlan, type SelfHostedPlanOptions } from "../lib/migration.js";
|
|
4
5
|
import { tick } from "../lib/scheduler.js";
|
|
5
6
|
import { Store } from "../lib/store.js";
|
|
6
7
|
export { runGoal } from "../lib/goal/runner.js";
|
|
8
|
+
export { LOOPS_MIGRATION_SCHEMA, applyImportMigrationBundle, buildImportMigrationPlan, buildSelfHostedMigrationPlan, exportLoopsMigrationBundle, migrationHash, registerSelfHostedRunner, validateLoopsMigrationBundle, } from "../lib/migration.js";
|
|
9
|
+
export type { ApplyLoopsMigrationResult, ExportLoopsMigrationOptions, ImportLoopsMigrationOptions, LoopsMigrationAction, LoopsMigrationBundle, LoopsMigrationPlan, LoopsMigrationPlanRow, LoopsMigrationPlanSummary, LoopsMigrationResource, RunnerRegistrationOptions, RunnerRegistrationResult, SelfHostedPlanOptions, } from "../lib/migration.js";
|
|
7
10
|
export interface LoopsClientOptions {
|
|
8
11
|
store?: Store;
|
|
9
12
|
/**
|
|
@@ -53,6 +56,12 @@ export declare class LoopsClient {
|
|
|
53
56
|
};
|
|
54
57
|
tick(): Promise<Awaited<ReturnType<typeof tick>>>;
|
|
55
58
|
runNow(idOrName: string): Promise<LoopRun>;
|
|
59
|
+
exportBundle(opts?: ExportLoopsMigrationOptions): LoopsMigrationBundle;
|
|
60
|
+
planImport(bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): LoopsMigrationPlan;
|
|
61
|
+
importBundle(bundle: LoopsMigrationBundle, opts?: ImportLoopsMigrationOptions): ApplyLoopsMigrationResult;
|
|
62
|
+
planSelfHostedMigration(opts?: Omit<SelfHostedPlanOptions, "operation"> & {
|
|
63
|
+
operation?: SelfHostedPlanOptions["operation"];
|
|
64
|
+
}): Promise<LoopsMigrationPlan>;
|
|
56
65
|
close(): void;
|
|
57
66
|
}
|
|
58
67
|
export declare function loops(opts?: LoopsClientOptions): LoopsClient;
|