@hasna/loops 0.4.28 → 0.4.29

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/README.md +226 -49
  3. package/dist/api/index.d.ts +20 -19
  4. package/dist/api/index.js +6846 -1087
  5. package/dist/cli/index.js +2471 -885
  6. package/dist/cli/safe-error-context.d.ts +1 -0
  7. package/dist/daemon/daemon.d.ts +1 -0
  8. package/dist/daemon/index.js +1786 -493
  9. package/dist/generated/storage-kit/index.d.ts +1 -1
  10. package/dist/generated/storage-kit/mode.d.ts +6 -12
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.js +4778 -1016
  13. package/dist/lib/advancement.d.ts +52 -0
  14. package/dist/lib/agent-adapter.d.ts +20 -2
  15. package/dist/lib/auth/route-policy.d.ts +14 -0
  16. package/dist/lib/auth/tenant-auth.d.ts +38 -0
  17. package/dist/lib/cloud/mode.d.ts +2 -8
  18. package/dist/lib/cloud/storage.d.ts +1 -2
  19. package/dist/lib/cloud/transport.d.ts +4 -18
  20. package/dist/lib/errors.d.ts +43 -1
  21. package/dist/lib/format.d.ts +2 -2
  22. package/dist/lib/goal/runner.d.ts +36 -3
  23. package/dist/lib/health.d.ts +1 -1
  24. package/dist/lib/labels.d.ts +4 -0
  25. package/dist/lib/loop-status.d.ts +4 -0
  26. package/dist/lib/migration.d.ts +4 -17
  27. package/dist/lib/mode.d.ts +2 -5
  28. package/dist/lib/mode.js +27 -36
  29. package/dist/lib/route/index.d.ts +2 -2
  30. package/dist/lib/route/throttle.d.ts +7 -0
  31. package/dist/lib/route/types.d.ts +3 -0
  32. package/dist/lib/run-completion.d.ts +18 -0
  33. package/dist/lib/scheduler.d.ts +5 -11
  34. package/dist/lib/storage/contract.d.ts +15 -1
  35. package/dist/lib/storage/index.d.ts +1 -1
  36. package/dist/lib/storage/index.js +3864 -457
  37. package/dist/lib/storage/pg-executor.d.ts +10 -5
  38. package/dist/lib/storage/postgres-loop-storage.d.ts +50 -24
  39. package/dist/lib/storage/postgres-schema.d.ts +8 -0
  40. package/dist/lib/storage/postgres-schema.js +1323 -1
  41. package/dist/lib/storage/postgres.d.ts +3 -1
  42. package/dist/lib/storage/postgres.js +1353 -27
  43. package/dist/lib/storage/provider-credentials.d.ts +84 -0
  44. package/dist/lib/storage/shared-database-transfer.d.ts +136 -0
  45. package/dist/lib/storage/sqlite.d.ts +15 -2
  46. package/dist/lib/storage/sqlite.js +1299 -217
  47. package/dist/lib/storage/tenant-backfill-s3.d.ts +53 -0
  48. package/dist/lib/storage/tenant-backfill.d.ts +40 -0
  49. package/dist/lib/store/index.d.ts +11 -8
  50. package/dist/lib/store.d.ts +52 -7
  51. package/dist/lib/store.js +1266 -217
  52. package/dist/lib/templates.d.ts +11 -0
  53. package/dist/lib/workflow-events.d.ts +6 -0
  54. package/dist/lib/workflow-provenance.d.ts +8 -0
  55. package/dist/lib/workflow-runner.d.ts +52 -4
  56. package/dist/mcp/index.js +1961 -651
  57. package/dist/runner/index.d.ts +20 -2
  58. package/dist/runner/index.js +2113 -177
  59. package/dist/sdk/http.d.ts +211 -3
  60. package/dist/sdk/http.js +301 -0
  61. package/dist/sdk/index.d.ts +7 -2
  62. package/dist/sdk/index.js +1959 -711
  63. package/dist/serve/index.d.ts +14 -0
  64. package/dist/serve/index.js +12323 -1743
  65. package/dist/types.d.ts +64 -3
  66. package/docs/AUTOMATION_RUNTIME_DESIGN.md +32 -32
  67. package/docs/CUTOVER-RUNBOOK.md +158 -31
  68. package/docs/DEPLOYMENT_MODES.md +78 -56
  69. package/docs/RUNTIME_BOUNDARY.md +26 -26
  70. package/docs/SHARED-DATABASE-TRANSFER.md +95 -0
  71. package/docs/SHARED_KIT_EXTRACTION_INVENTORY.md +631 -0
  72. package/docs/TRANSCRIPT_LOOP_PATTERNS.md +3 -3
  73. package/docs/UNIFIED_PRODUCT_CONTRACT.md +365 -0
  74. package/docs/USAGE.md +143 -52
  75. package/docs/workflows/transcript-feedback-to-loops.json +2 -2
  76. package/package.json +7 -3
  77. package/dist/lib/storage/pg-runner-claim.d.ts +0 -40
@@ -0,0 +1,52 @@
1
+ import type { Loop, LoopRun } from "../types.js";
2
+ export declare const MAX_RETRY_DELAY_MS: number;
3
+ export declare const DEFAULT_CIRCUIT_BREAKER_THRESHOLD = 5;
4
+ export declare const CIRCUIT_BREAKER_REASON_PREFIX = "circuit breaker open";
5
+ export type CircuitBreakerThreshold = number | ((loop: Loop) => number | undefined);
6
+ export type LoopAdvancementPatch = Partial<Pick<Loop, "status" | "nextRunAt" | "retryScheduledFor">>;
7
+ export type LoopAdvancementPlan = {
8
+ kind: "none";
9
+ reason: "running" | "missing" | "inactive" | "archived" | "stale" | "already_applied";
10
+ } | {
11
+ kind: "update";
12
+ reason: "retry" | "deferred_retry" | "recurrence";
13
+ patch: LoopAdvancementPatch;
14
+ } | {
15
+ kind: "circuit_breaker";
16
+ failures: number;
17
+ reason: string;
18
+ markerScheduledFor: string;
19
+ patch: LoopAdvancementPatch;
20
+ };
21
+ export declare function loopAdvancementPatchMatchesCurrent(current: Loop, patch: LoopAdvancementPatch): boolean;
22
+ /**
23
+ * Exponential retry backoff with jitter:
24
+ * delay = retryDelayMs * 2^(attempt-1) * (0.5 + random), capped at 6h.
25
+ * Provider-gated failures back off 4x harder.
26
+ */
27
+ export declare function retryBackoffDelayMs(loop: Loop, run: LoopRun, random?: () => number): number;
28
+ export declare function resolveBreakerThreshold(loop: Loop, override?: CircuitBreakerThreshold): number;
29
+ /**
30
+ * Count consecutive final failures from newest-first run history.
31
+ *
32
+ * Retryable failures are neutral, a success resets the streak, and the newest
33
+ * circuit-breaker marker is a watermark so a manual resume starts a fresh
34
+ * streak.
35
+ */
36
+ export declare function consecutiveFailureCountFromRuns(runs: readonly LoopRun[], maxAttempts?: number): number;
37
+ /**
38
+ * Pure scheduling policy shared by the local scheduler and hosted API.
39
+ * Callers gather current loop state, deferred retry state, and recent history,
40
+ * then apply the returned storage mutations using their native sync/async API.
41
+ */
42
+ export declare function planLoopAdvancement(input: {
43
+ current: Loop | undefined;
44
+ run: LoopRun;
45
+ finishedAt: Date;
46
+ succeeded: boolean;
47
+ deferredRetry?: LoopRun;
48
+ retryIntentRun?: LoopRun;
49
+ recentRuns?: readonly LoopRun[];
50
+ retryRandom?: number;
51
+ circuitBreakerThreshold?: CircuitBreakerThreshold;
52
+ }): LoopAdvancementPlan;
@@ -1,8 +1,10 @@
1
- import type { AgentProvider, AgentSandbox, AgentTarget } from "../types.js";
1
+ import type { AgentAllowlistEnforcement, AgentProvider, AgentSandbox, AgentSessionContract, AgentTarget, WorkflowStep } from "../types.js";
2
2
  export type ProviderPromptChannel = "stdin" | "argv";
3
3
  export interface ProviderCapabilities {
4
4
  /** Sandbox values the provider CLI accepts; empty when sandboxing is unsupported. */
5
5
  sandbox: readonly AgentSandbox[];
6
+ /** Tool/command restrictions exposed by this adapter. Metadata-only means advisory, not provider-enforced. */
7
+ allowlist: ProviderAllowlistCapabilities;
6
8
  /** Whether the provider runs as a durable background agent instead of a one-shot process. */
7
9
  durable: boolean;
8
10
  /** Whether the provider can be dispatched to a remote machine transport. */
@@ -10,6 +12,10 @@ export interface ProviderCapabilities {
10
12
  /** How the prompt reaches the provider process. */
11
13
  promptChannel: ProviderPromptChannel;
12
14
  }
15
+ export interface ProviderAllowlistCapabilities {
16
+ tools: AgentAllowlistEnforcement;
17
+ commands: AgentAllowlistEnforcement;
18
+ }
13
19
  export interface AgentInvocation {
14
20
  command: string;
15
21
  args: string[];
@@ -18,16 +24,28 @@ export interface AgentInvocation {
18
24
  /** Executables of which at least one must exist besides the command itself. */
19
25
  preflightAnyOf?: string[];
20
26
  }
27
+ export interface PreparedAgentInvocation {
28
+ /** Invocation built from the first validated snapshot. */
29
+ invocation: AgentInvocation;
30
+ /** Rebuild only cwd-dependent fields while reusing that same snapshot. */
31
+ forCwd(cwd: string): AgentInvocation;
32
+ }
21
33
  export interface ProviderAdapter {
22
34
  provider: AgentProvider;
23
35
  capabilities: ProviderCapabilities;
24
36
  validate(target: AgentTarget, label?: string): void;
25
37
  buildInvocation(target: AgentTarget): AgentInvocation;
38
+ prepareInvocation(target: AgentTarget): PreparedAgentInvocation;
26
39
  }
27
- export declare const UNSAFE_CODEWITH_EXEC_EXTRA_ARGS: Set<string>;
40
+ export declare function effectiveAgentSandbox(target: AgentTarget): AgentSandbox | "provider-default";
41
+ export declare function agentSessionContract(target: AgentTarget, cwd?: string | undefined): AgentSessionContract | undefined;
42
+ export declare function workflowStepAgentSessionContract(step: WorkflowStep): AgentSessionContract | undefined;
43
+ export declare function agentSessionContractPrompt(target: AgentTarget, cwd?: string | undefined): string | undefined;
28
44
  export declare const PROVIDER_ADAPTERS: Record<AgentProvider, ProviderAdapter>;
29
45
  export declare const AGENT_PROVIDERS: AgentProvider[];
30
46
  export declare function providerAdapter(provider: AgentProvider): ProviderAdapter;
47
+ /** Validate an untrusted or persisted agent target without relying on TypeScript-only shape guarantees. */
48
+ export declare function validateAgentTarget(target: unknown, label?: string): asserts target is AgentTarget;
31
49
  export interface SpawnCaptureOptions {
32
50
  cwd?: string;
33
51
  env?: NodeJS.ProcessEnv;
@@ -0,0 +1,14 @@
1
+ export type TenantRole = "admin" | "operator" | "member" | "readonly" | "service" | "worker";
2
+ export type TokenKind = "api_key" | "service" | "machine";
3
+ export interface RoutePolicy {
4
+ operationId: string;
5
+ method: string;
6
+ pathTemplate: string;
7
+ path: RegExp;
8
+ scopes: readonly string[];
9
+ roles: readonly TenantRole[];
10
+ tokenKinds: readonly TokenKind[];
11
+ risk: "read" | "write" | "destructive" | "runner";
12
+ }
13
+ export declare const ROUTE_POLICIES: readonly RoutePolicy[];
14
+ export declare function routePolicy(method: string, path: string): RoutePolicy | undefined;
@@ -0,0 +1,38 @@
1
+ import { type ApiKeyClaims } from "@hasna/contracts/auth";
2
+ import type { TypedQueryClient } from "../../generated/storage-kit/query.js";
3
+ import type { RoutePolicy, TenantRole, TokenKind } from "./route-policy.js";
4
+ export interface TenantAuthContext {
5
+ tenantId: string;
6
+ principalId: string;
7
+ requestId: string;
8
+ kid: string;
9
+ agent: string | null;
10
+ scopes: string[];
11
+ roles: TenantRole[];
12
+ tokenKind: TokenKind;
13
+ claims: ApiKeyClaims;
14
+ }
15
+ export type TenantAuthDecision = {
16
+ ok: true;
17
+ status: 200;
18
+ principal: TenantAuthContext;
19
+ } | {
20
+ ok: false;
21
+ status: 401 | 403 | 503;
22
+ reason: string;
23
+ message: string;
24
+ requestId: string;
25
+ };
26
+ export declare class TenantApiAuthenticator {
27
+ private readonly client;
28
+ private readonly signingSecret;
29
+ private readonly now;
30
+ constructor(client: TypedQueryClient, signingSecret: string | Buffer, now?: () => number);
31
+ authenticate(headers: Headers, context: {
32
+ method: string;
33
+ path: string;
34
+ policy: RoutePolicy;
35
+ }): Promise<TenantAuthDecision>;
36
+ private deny;
37
+ private audit;
38
+ }
@@ -1,16 +1,10 @@
1
- export type StorageMode = "local" | "cloud";
2
- /** Deprecated mode aliases that all normalize to `cloud`. */
3
- export declare const DEPRECATED_STORAGE_MODE_ALIASES: readonly ["remote", "hybrid", "self_hosted"];
1
+ export type StorageMode = "local" | "self_hosted" | "cloud";
4
2
  export type Env = Record<string, string | undefined>;
5
3
  export interface StorageModeNormalization {
6
4
  mode: StorageMode;
7
- /** The deprecated alias that was normalized to `cloud`, if any. */
8
- deprecatedAlias: string | null;
9
5
  }
10
6
  /**
11
- * Normalize a raw storage-mode string to the `local | cloud` runtime enum.
12
- * Accepts deprecated aliases (`remote`, `hybrid`, `self_hosted`) and maps them
13
- * to `cloud`. Throws on any other value.
7
+ * Normalize a raw storage-mode string to the canonical deployment vocabulary.
14
8
  */
15
9
  export declare function normalizeStorageMode(value: string): StorageModeNormalization;
16
10
  /** Upper-snake env token for an app name, e.g. `todos` -> `TODOS`. */
@@ -76,7 +76,6 @@ export type ResolveStorageClientResult = {
76
76
  * `name`; when it resolves to `cloud-http` (mode=cloud/self_hosted + API_URL +
77
77
  * API_KEY), returns a ready {@link HasnaStorageClient}. Otherwise returns
78
78
  * `{ transport: 'local', client: null }` so the app uses its local store.
79
- * Throws if cloud was requested but is misconfigured (so callers never silently
80
- * read the wrong dataset).
79
+ * Incomplete remote configuration throws so callers never read the wrong dataset.
81
80
  */
82
81
  export declare function resolveStorageClient(name: string, env?: Env, overrides?: Parameters<typeof createClientTransport>[2]): ResolveStorageClientResult;
@@ -1,7 +1,5 @@
1
1
  import { type Env } from "./mode.js";
2
2
  import type { StorageMode } from "./mode.js";
3
- /** Default cloud host template. `<app>` is the app slug. */
4
- export declare function defaultCloudBaseUrl(name: string): string;
5
3
  export interface ClientTransportEnvKeys {
6
4
  /** Mode keys, in precedence order. */
7
5
  modeKeys: string[];
@@ -18,10 +16,8 @@ export type ClientTransportKind = "local" | "cloud-http";
18
16
  export interface ClientTransportResolution {
19
17
  /** Where the client should read/write from. */
20
18
  transport: ClientTransportKind;
21
- /** Resolved storage mode (`local` | `cloud`). */
19
+ /** Resolved canonical deployment mode. */
22
20
  mode: StorageMode;
23
- /** Deprecated mode alias that was normalized (e.g. `self_hosted`), if any. */
24
- deprecatedAlias: string | null;
25
21
  /** Env key the mode was read from, or `"default"`. */
26
22
  modeSource: string;
27
23
  /** `<origin>/v1` base for the cloud API when transport is cloud-http, else null. */
@@ -32,20 +28,11 @@ export interface ClientTransportResolution {
32
28
  apiKeyPresent: boolean;
33
29
  /** Env key the API key came from, or null. */
34
30
  apiKeySource: string | null;
35
- /**
36
- * True when the operator asked for cloud but the config is incomplete (no API
37
- * key), so we fell back to local. Callers SHOULD treat this as an error rather
38
- * than silently reading local data.
39
- */
40
- misconfigured: boolean;
41
- /** Human-readable warning, or null. Never contains secret values. */
42
- warning: string | null;
43
31
  }
44
32
  /**
45
33
  * Resolve how a client should reach an app's data given the environment.
46
34
  *
47
- * Precedence for the mode: the first present of `HASNA_<NAME>_STORAGE_MODE`,
48
- * `HASNA_<NAME>_MODE`, `<NAME>_STORAGE_MODE`, `<NAME>_MODE`, else `local`.
35
+ * The only mode key is `HASNA_<NAME>_STORAGE_MODE`; unset means `local`.
49
36
  */
50
37
  export declare function resolveClientTransport(name: string, env?: Env): ClientTransportResolution;
51
38
  /** Thrown when a cloud HTTP request returns a non-2xx status. */
@@ -132,9 +119,8 @@ export declare function createHasnaHttpTransport(options: HasnaHttpTransportOpti
132
119
  /**
133
120
  * Convenience: resolve transport from env and, when cloud-http, build the HTTP
134
121
  * client in one call. Returns `{ transport: 'local', resolution }` for local, or
135
- * `{ transport: 'cloud-http', client, resolution }` for cloud. Throws if the
136
- * config is `misconfigured` (cloud requested but unusable) so callers can't drift
137
- * onto local data by accident.
122
+ * `{ transport: 'cloud-http', client, resolution }` for self_hosted/cloud.
123
+ * Incomplete remote configuration throws before a client is built.
138
124
  */
139
125
  export declare function createClientTransport(name: string, env?: Env, overrides?: Partial<Pick<HasnaHttpTransportOptions, "fetchImpl" | "headers" | "timeoutMs" | "retry" | "sleepImpl">>): {
140
126
  transport: "local";
@@ -13,9 +13,51 @@ export declare class LoopNotFoundError extends CodedError {
13
13
  export declare class LoopArchivedError extends CodedError {
14
14
  constructor(idOrName: string);
15
15
  }
16
+ export declare class LoopAdvancementConflictError extends CodedError {
17
+ constructor(loopId: string, runId: string);
18
+ }
19
+ export type RunFinalizationConflictReason = "stale_claim" | "run_not_running";
20
+ export declare class RunFinalizationConflictError extends CodedError {
21
+ readonly reason: RunFinalizationConflictReason;
22
+ constructor(reason: RunFinalizationConflictReason, runId: string);
23
+ }
16
24
  export declare class AmbiguousNameError extends CodedError {
17
25
  constructor(name: string);
18
26
  }
27
+ export type AgentExtraArgsValidationReason = "not_array" | "invalid_array" | "invalid_item" | "option_not_allowed";
28
+ /**
29
+ * Deliberately public, bounded validation metadata. API boundaries may expose
30
+ * this object, but must never expose the accompanying Error.message.
31
+ */
32
+ export interface PublicValidationDetails {
33
+ code: "agent_extra_args_invalid";
34
+ reason: AgentExtraArgsValidationReason;
35
+ path: string;
36
+ index?: number;
37
+ option?: string;
38
+ }
39
+ export declare function publicValidationDetails(value: unknown): Readonly<PublicValidationDetails> | undefined;
19
40
  export declare class ValidationError extends CodedError {
20
- constructor(message: string);
41
+ readonly publicDetails?: Readonly<PublicValidationDetails>;
42
+ constructor(message: string, publicDetails?: PublicValidationDetails);
43
+ }
44
+ /** Safely capture and re-project even forged/subclass validation errors. */
45
+ export declare function validationErrorPublicDetails(error: ValidationError): Readonly<PublicValidationDetails> | undefined;
46
+ export declare class DuplicateWorkflowEventError extends CodedError {
47
+ constructor(workflowRunId: string, eventType: string, stepId?: string);
48
+ }
49
+ export declare class LegacyWorkflowRunProvenanceError extends CodedError {
50
+ constructor(workflowRunId: string);
51
+ }
52
+ export declare class WorkflowRunDefinitionConflictError extends CodedError {
53
+ constructor(workflowRunId: string);
54
+ }
55
+ export declare class WorkflowRunHasLiveStepsError extends CodedError {
56
+ constructor();
57
+ }
58
+ export declare class WorkflowRunStepOwnershipUnverifiableError extends CodedError {
59
+ constructor();
60
+ }
61
+ export declare class WorkflowRunNotRunningError extends CodedError {
62
+ constructor();
21
63
  }
@@ -1,4 +1,4 @@
1
- import type { ExecutorResult, Goal, GoalRun, Loop, LoopRun, RunReceipt, WorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem } from "../types.js";
1
+ import type { ExecutorResult, Goal, GoalRun, Loop, LoopRun, RunReceipt, PublicWorkflowEvent, StoredWorkflowEvent, WorkflowInvocation, WorkflowRun, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem } from "../types.js";
2
2
  export declare function redact(value: string | undefined, visible?: number): string | undefined;
3
3
  export declare function textOutputBlocks(value: Pick<LoopRun | WorkflowStepRun, "stdout" | "stderr">, opts?: {
4
4
  indent?: string;
@@ -14,6 +14,6 @@ export declare function publicWorkflowRun(run: WorkflowRun): Record<string, unkn
14
14
  export declare function publicWorkflowInvocation(invocation: WorkflowInvocation): Record<string, unknown>;
15
15
  export declare function publicWorkflowWorkItem(item: WorkflowWorkItem): Record<string, unknown>;
16
16
  export declare function publicWorkflowStepRun(run: WorkflowStepRun, showOutput?: boolean): Record<string, unknown>;
17
- export declare function publicWorkflowEvent(event: WorkflowEvent): Record<string, unknown>;
17
+ export declare function publicWorkflowEvent(event: StoredWorkflowEvent | PublicWorkflowEvent): Record<string, unknown>;
18
18
  export declare function publicGoal(goal: Goal): Record<string, unknown>;
19
19
  export declare function publicGoalRun(run: GoalRun): Record<string, unknown>;
@@ -1,3 +1,36 @@
1
- import type { Store } from "../store.js";
2
- import type { GoalExecutorResult, GoalSpec, RunGoalOptions } from "./types.js";
3
- export declare function runGoal(store: Store, input: GoalSpec, opts?: RunGoalOptions): Promise<GoalExecutorResult>;
1
+ import type { CreateGoalInput, CreateGoalPlanNodeInput, RecordGoalEventInput } from "../store.js";
2
+ import type { Goal, GoalExecutorResult, GoalPlanNode, GoalRun, GoalSpec, GoalStatus, RunGoalOptions } from "./types.js";
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ export interface GoalRunnerStore {
5
+ findGoalByContext(context: {
6
+ loopRunId?: string;
7
+ workflowRunId?: string;
8
+ workflowStepId?: string;
9
+ sourceType?: string;
10
+ sourceId?: string;
11
+ }): MaybePromise<Goal | undefined>;
12
+ createGoal(input: CreateGoalInput, opts?: {
13
+ daemonLeaseId?: string;
14
+ now?: Date;
15
+ }): MaybePromise<Goal>;
16
+ requireGoal(id: string): MaybePromise<Goal>;
17
+ createGoalPlanNodes(goalId: string, nodes: CreateGoalPlanNodeInput[], opts?: {
18
+ daemonLeaseId?: string;
19
+ now?: Date;
20
+ }): MaybePromise<GoalPlanNode[]>;
21
+ listGoalPlanNodes(goalIdOrPlanId: string): MaybePromise<GoalPlanNode[]>;
22
+ updateGoalStatus(goalId: string, status: GoalStatus, opts?: {
23
+ daemonLeaseId?: string;
24
+ now?: Date;
25
+ }): MaybePromise<Goal>;
26
+ updateGoalPlanNode(goalId: string, key: string, patch: Partial<Pick<GoalPlanNode, "status" | "tokensUsed" | "timeUsedSeconds" | "ready">>, opts?: {
27
+ daemonLeaseId?: string;
28
+ now?: Date;
29
+ }): MaybePromise<GoalPlanNode>;
30
+ recordGoalEvent(input: RecordGoalEventInput, opts?: {
31
+ daemonLeaseId?: string;
32
+ now?: Date;
33
+ }): MaybePromise<GoalRun>;
34
+ }
35
+ export declare function runGoal(store: GoalRunnerStore, input: GoalSpec, opts?: RunGoalOptions): Promise<GoalExecutorResult>;
36
+ export {};
@@ -2,7 +2,7 @@ import type { Loop, LoopRun, LoopStatus } from "../types.js";
2
2
  import type { DaemonStatus } from "../daemon/control.js";
3
3
  import type { DoctorCheck, DoctorReport } from "./doctor.js";
4
4
  import type { Store } from "./store.js";
5
- export type RunFailureClassification = "rate_limit" | "auth" | "provider_unavailable" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "restart_interrupted" | "skipped_previous_active" | "circuit_breaker" | "unknown";
5
+ export type RunFailureClassification = "rate_limit" | "auth" | "provider_capacity" | "provider_unavailable" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "restart_interrupted" | "skipped_previous_active" | "circuit_breaker" | "unknown";
6
6
  export interface RunFailureSignal {
7
7
  classification: RunFailureClassification;
8
8
  fingerprint: string;
@@ -0,0 +1,4 @@
1
+ export declare const LOOP_LABEL_MAX_COUNT = 32;
2
+ export declare function normalizeLoopLabels(value: string | string[] | undefined, label?: string): string[];
3
+ export declare function mergeLoopLabels(current: string[] | undefined, added: string | string[]): string[];
4
+ export declare function removeLoopLabels(current: string[] | undefined, removed: string | string[]): string[];
@@ -0,0 +1,4 @@
1
+ import type { LoopStatus } from "../types.js";
2
+ export declare const LOOP_STATUSES: readonly ["active", "paused", "stopped", "expired"];
3
+ export declare function isLoopStatus(value: unknown): value is LoopStatus;
4
+ export declare function assertLoopStatus(value: unknown): asserts value is LoopStatus;
@@ -81,26 +81,13 @@ export interface ApplyLoopsMigrationResult {
81
81
  export interface SelfHostedPlanOptions {
82
82
  operation: "self-hosted-push" | "self-hosted-pull" | "self-hosted-migrate";
83
83
  apiUrl?: string;
84
- apiToken?: string;
84
+ apiKey?: string;
85
+ timeoutMs?: number;
85
86
  fetchImpl?: typeof fetch;
86
87
  env?: NodeJS.ProcessEnv;
87
88
  includeRuns?: boolean;
88
89
  replace?: boolean;
89
90
  }
90
- export interface RunnerRegistrationOptions {
91
- apiUrl?: string;
92
- apiToken?: string;
93
- runnerId: string;
94
- machineId?: string;
95
- labels?: Record<string, string>;
96
- capabilities?: Record<string, unknown>;
97
- fetchImpl?: typeof fetch;
98
- env?: NodeJS.ProcessEnv;
99
- }
100
- export interface RunnerRegistrationResult {
101
- ok: boolean;
102
- runner: Record<string, unknown>;
103
- }
104
91
  export declare function migrationHash(value: unknown): string;
105
92
  export declare function exportLoopsMigrationBundle(store: Store, opts?: ExportLoopsMigrationOptions): LoopsMigrationBundle;
106
93
  export declare function validateLoopsMigrationBundle(value: unknown): LoopsMigrationBundle;
@@ -109,7 +96,8 @@ export declare function applyImportMigrationBundle(store: Store, bundle: LoopsMi
109
96
  export declare function buildSelfHostedMigrationPlan(store: Store, opts: SelfHostedPlanOptions): Promise<LoopsMigrationPlan>;
110
97
  export interface SelfHostedPushOptions {
111
98
  apiUrl?: string;
112
- apiToken?: string;
99
+ apiKey?: string;
100
+ timeoutMs?: number;
113
101
  includeRuns?: boolean;
114
102
  replace?: boolean;
115
103
  fetchImpl?: typeof fetch;
@@ -210,6 +198,5 @@ export interface SelfHostedPushManifest {
210
198
  * endpoint upserts by id, so re-running never duplicates rows.
211
199
  */
212
200
  export declare function applySelfHostedPush(store: Store, opts: SelfHostedPushOptions): Promise<SelfHostedPushResult>;
213
- export declare function registerSelfHostedRunner(opts: RunnerRegistrationOptions): Promise<RunnerRegistrationResult>;
214
201
  export declare function publicMigrationBundle(bundle: LoopsMigrationBundle): Record<string, unknown>;
215
202
  export declare function selfHostedControlPlaneSummary(env?: NodeJS.ProcessEnv): Record<string, unknown>;
@@ -11,11 +11,8 @@ export interface LoopModeResolution {
11
11
  }
12
12
  export interface LoopControlPlaneConfig {
13
13
  apiUrl?: string;
14
- cloudApiUrl?: string;
15
14
  databaseUrlPresent: boolean;
16
- apiAuthTokenPresent: boolean;
17
- cloudAuthTokenPresent: boolean;
18
- authTokenPresent: boolean;
15
+ apiKeyPresent: boolean;
19
16
  }
20
17
  export interface LoopDeploymentStatus {
21
18
  packageVersion: string;
@@ -32,7 +29,7 @@ export interface LoopDeploymentStatus {
32
29
  configured: boolean;
33
30
  apiUrl?: string;
34
31
  databaseUrlPresent: boolean;
35
- authTokenPresent: boolean;
32
+ apiKeyPresent: boolean;
36
33
  };
37
34
  runner: {
38
35
  required: boolean;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.28",
5
+ version: "0.4.29",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -81,11 +81,14 @@ var package_default = {
81
81
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
82
82
  typecheck: "tsc --noEmit",
83
83
  test: "bun test",
84
+ "check:branding": "bun test scripts/check-branding.test.mjs && bun run scripts/check-branding.mjs",
84
85
  "test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
86
+ "check:contracts": "bun run scripts/check-storage-kit.mjs && bun run scripts/check-contract-conformance.mjs",
87
+ "check:supply-chain": "bun audit && bun audit --production && bun run check:contracts && bun run check:branding && bun pm pack --dry-run --ignore-scripts && bun run test:boundary && bun run scripts/check-packed-boundary.mjs",
85
88
  prepare: "test -d dist || bun run build",
86
89
  "dev:cli": "bun run src/cli/index.ts",
87
90
  "dev:daemon": "bun run src/daemon/index.ts",
88
- prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
91
+ prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary && bun run check:supply-chain"
89
92
  },
90
93
  keywords: [
91
94
  "loops",
@@ -112,7 +115,8 @@ var package_default = {
112
115
  bun: ">=1.0.0"
113
116
  },
114
117
  dependencies: {
115
- "@hasna/contracts": "^0.5.1",
118
+ "@aws-sdk/client-secrets-manager": "^3.1083.0",
119
+ "@hasna/contracts": "0.5.2",
116
120
  "@hasna/events": "^0.1.9",
117
121
  "@modelcontextprotocol/sdk": "^1.29.0",
118
122
  "@openrouter/ai-sdk-provider": "2.9.1",
@@ -144,13 +148,10 @@ function packageVersion() {
144
148
 
145
149
  // src/lib/mode.ts
146
150
  var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
147
- var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
148
- var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
149
- var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
150
- var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
151
- var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
152
- var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
153
- var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
151
+ var MODE_ENV_KEYS = ["HASNA_LOOPS_STORAGE_MODE"];
152
+ var API_URL_ENV_KEYS = ["HASNA_LOOPS_API_URL"];
153
+ var DATABASE_URL_ENV_KEYS = ["HASNA_LOOPS_DATABASE_URL"];
154
+ var API_KEY_ENV_KEYS = ["HASNA_LOOPS_API_KEY"];
154
155
  function envValue(env, keys) {
155
156
  for (const key of keys) {
156
157
  const value = env[key]?.trim();
@@ -160,14 +161,14 @@ function envValue(env, keys) {
160
161
  return;
161
162
  }
162
163
  function normalizeLoopDeploymentMode(value) {
163
- const normalized = value.trim().toLowerCase().replace(/-/g, "_");
164
+ const normalized = value.trim();
164
165
  if (normalized === "local")
165
166
  return "local";
166
- if (normalized === "self_hosted" || normalized === "selfhosted")
167
+ if (normalized === "self_hosted")
167
168
  return "self_hosted";
168
- if (normalized === "cloud" || normalized === "saas")
169
+ if (normalized === "cloud")
169
170
  return "cloud";
170
- throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
171
+ throw new Error(`unsupported Loops deployment mode "${value}"; expected local, self_hosted, or cloud`);
171
172
  }
172
173
  function resolveLoopDeploymentMode(env = process.env) {
173
174
  const explicitMode = envValue(env, MODE_ENV_KEYS);
@@ -177,9 +178,6 @@ function resolveLoopDeploymentMode(env = process.env) {
177
178
  source: explicitMode.key
178
179
  };
179
180
  }
180
- const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
181
- if (cloudApiUrl)
182
- return { deploymentMode: "cloud", source: cloudApiUrl.key };
183
181
  const apiUrl = envValue(env, API_URL_ENV_KEYS);
184
182
  if (apiUrl)
185
183
  return { deploymentMode: "self_hosted", source: apiUrl.key };
@@ -191,11 +189,8 @@ function resolveLoopDeploymentMode(env = process.env) {
191
189
  function loopControlPlaneConfig(env = process.env) {
192
190
  return {
193
191
  apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
194
- cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
195
192
  databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
196
- apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
197
- cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
198
- authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
193
+ apiKeyPresent: Boolean(envValue(env, API_KEY_ENV_KEYS))
199
194
  };
200
195
  }
201
196
  function sourceOfTruthForMode(mode) {
@@ -217,7 +212,7 @@ function remoteSchedulerBackendForMode(mode, config) {
217
212
  if (mode === "local")
218
213
  return "none";
219
214
  if (mode === "cloud")
220
- return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
215
+ return config.apiUrl ? "hosted_control_plane_contract" : "unconfigured";
221
216
  if (config.databaseUrlPresent)
222
217
  return "postgres_contract";
223
218
  if (config.apiUrl)
@@ -265,26 +260,22 @@ function buildDeploymentStatus(opts = {}) {
265
260
  const active = resolveLoopDeploymentMode(env);
266
261
  const deploymentMode = opts.perspective ?? active.deploymentMode;
267
262
  const config = loopControlPlaneConfig(env);
268
- const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
269
- const apiUrl = displayControlPlaneUrl(rawApiUrl);
263
+ const apiUrl = displayControlPlaneUrl(config.apiUrl);
270
264
  const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
271
- const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
272
- const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
265
+ const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.apiKeyPresent : deploymentMode === "self_hosted" ? config.apiKeyPresent : false;
266
+ const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.apiUrl && deploymentAuthTokenPresent);
273
267
  const warnings = [];
274
268
  if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
275
- warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
269
+ warnings.push("self_hosted mode needs HASNA_LOOPS_API_URL or HASNA_LOOPS_DATABASE_URL before it can become authoritative");
276
270
  }
277
271
  if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
278
- warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
272
+ warnings.push("HASNA_LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs HASNA_LOOPS_API_URL to claim remote work");
279
273
  }
280
- if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
281
- warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
274
+ if (deploymentMode === "cloud" && !config.apiUrl) {
275
+ warnings.push("cloud mode needs HASNA_LOOPS_API_URL before it can become ready");
282
276
  }
283
- if (deploymentMode === "cloud" && !config.cloudApiUrl) {
284
- warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
285
- }
286
- if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
287
- warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
277
+ if (deploymentMode === "cloud" && config.apiUrl && !deploymentAuthTokenPresent) {
278
+ warnings.push("cloud mode needs HASNA_LOOPS_API_KEY before it can become ready");
288
279
  }
289
280
  if (opts.perspective && opts.perspective !== active.deploymentMode) {
290
281
  warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
@@ -304,7 +295,7 @@ function buildDeploymentStatus(opts = {}) {
304
295
  configured: controlPlaneConfigured,
305
296
  apiUrl,
306
297
  databaseUrlPresent: config.databaseUrlPresent,
307
- authTokenPresent: deploymentAuthTokenPresent
298
+ apiKeyPresent: deploymentAuthTokenPresent
308
299
  },
309
300
  runner: {
310
301
  required: deploymentMode !== "local",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Route engine: turns Hasna events, todos ready queues, and health/hygiene
3
- * findings into deduped, throttled OpenLoops workflow loops and todos tasks.
3
+ * findings into deduped, throttled Loops workflow loops and todos tasks.
4
4
  * Extracted from the CLI so commands stay thin and the SDK/daemon can reuse it.
5
5
  */
6
6
  export * from "./types.js";
@@ -11,7 +11,7 @@ export { GateError, normalizeLoopTargetForStorage, normalizeWorkflowForStorage,
11
11
  export { accountPoolFromOpts, normalizeAgentProvider, permissionModeFromOpts, providerAuthProfileFromOpts, providerRoutingPublic, resolveProviderRouting, roleAccountFromOpts, sandboxFromOpts, SUPPORTED_AGENT_PROVIDERS, type ProviderRoutingDecision, } from "./provider.js";
12
12
  export { checkProviderAdmission, parseCodewithAdmissionDiagnostics, providerActiveCapFromOpts, providerAdmissionPlanFromOpts, type CodewithAdmissionDiagnostics, type ProviderAdmissionDecision, type ProviderAdmissionPlan, } from "./provider-admission.js";
13
13
  export { prReviewRoutingDecision, type PrReviewRoutingDecision } from "./pr-review.js";
14
- export { hasThrottleLimits, normalizeRoutePath, routeThrottleDecision, routeThrottleLimitsFromOpts, type RouteThrottleDecision, type RouteThrottleLimits, } from "./throttle.js";
14
+ export { hasThrottleLimits, normalizeRoutePath, routeThrottleDecision, routeThrottleLimitsFromInputs, routeThrottleLimitsFromOpts, type RouteThrottleDecision, type RouteThrottleLimits, } from "./throttle.js";
15
15
  export { generatedRouteSandboxPreflight, readEventEnvelopeInput, routeEventByKind, routeGenericEvent, routeTodosTaskEvent, todosTaskRouteTemplateId, } from "./route-event.js";
16
16
  export { drainTodosTaskRoutes, type DrainResult } from "./drain.js";
17
17
  export { buildHygieneRouteTasks, parseHygieneChecks, taskAutoRoute, upsertRouteTasks, type HygieneCheckKind, type HygieneRouteTask, type RouteTaskSpec, type UpsertRouteTasksOptions, } from "./route-tasks.js";
@@ -26,6 +26,13 @@ export declare function routeThrottleLimitsFromOpts(opts: {
26
26
  maxActivePerProjectGroup?: string;
27
27
  maxPerProfile?: string;
28
28
  }): RouteThrottleLimits;
29
+ export declare function routeThrottleLimitsFromInputs(opts: {
30
+ maxActive?: string;
31
+ maxActiveScope?: string;
32
+ maxActivePerProject?: string;
33
+ maxActivePerProjectGroup?: string;
34
+ maxPerProfile?: string;
35
+ }, data: Record<string, unknown>, metadata: Record<string, unknown>): RouteThrottleLimits;
29
36
  export declare function hasThrottleLimits(limits: RouteThrottleLimits): boolean;
30
37
  export declare function normalizeRoutePath(value: string | undefined): string | undefined;
31
38
  export declare function routeProjectGroup(optsGroup: string | undefined, data: Record<string, unknown>, metadata: Record<string, unknown>): string | undefined;
@@ -29,6 +29,9 @@ export interface TodosTaskRouteOptions {
29
29
  verifierIdleTimeout?: string;
30
30
  permissionMode?: string;
31
31
  sandbox?: string;
32
+ allowTool?: string[];
33
+ allowCommand?: string[];
34
+ safetyReason?: string;
32
35
  manualBreakGlass?: boolean;
33
36
  projectPath?: string;
34
37
  projectGroup?: string;