@loops-adk/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -447,6 +447,19 @@ The error taxonomy backs this: an engine classifies a throttle into a `RATE_LIMI
447
447
 
448
448
  Every mode ends with a summary: result, per-loop iterations, review tallies, token usage by model, and any errors.
449
449
 
450
+ ## Supervise a running loop
451
+
452
+ Run with `--supervise` and the loop registers itself under `~/.loops/runs/`, writing its live state there as it goes. Another process reads it with no daemon and no socket, because the filesystem is the channel (the same bet the rest of the library makes).
453
+
454
+ ```bash
455
+ loops run build.loop.ts --supervise # in one terminal
456
+ loops list # in another: every supervised run, with state and iteration
457
+ loops status <runId> # its shape plus where it is now: iteration, last gate verdict, tokens
458
+ loops tail <runId> # stream its events live
459
+ ```
460
+
461
+ `list` marks a run dead if its process is gone. The read side is also on the public surface (`listRuns`, `readRunStatus`, `runEventsPath`), so an agent supervising a fleet of loops, killing the ones that drift and kicking work back into the ones that hit a problem, reads the same files. Out-of-process control (pause, abort, and kickback from outside) is the next step.
462
+
450
463
  ## What `loops` is (and isn't)
451
464
 
452
465
  `loops` is a **fresh-context loop primitive**, not a durable workflow engine. The design bet is that **the workspace is the state**: progress _and its reasoning_ live in git (the Ledger), so each iteration can start clean and still know what came before. If the process dies mid-run, you re-run against the same workspace (the worktree holds the files, the scratch files hold the why, the log holds the milestones) and continue. You lose the bookkeeping, not the work.
@@ -464,7 +477,9 @@ It deliberately does **not** do durable mid-run replay (re-running a half-finish
464
477
  - [x] **Ledger**, git-memory core: the scratch files (working memory + handoff), grounding, milestone commits
465
478
  - [x] Worktree isolation (branches-as-teams) with `--no-ff` land-back
466
479
  - [x] Environment axis: provider interface + offline mock
467
- - [ ] Publish to npm (with a built `dist` + `exports`)
480
+ - [x] Publish to npm (`@loops-adk/core`, built `dist` + types, CI release)
481
+ - [x] Supervision: a file-based run registry with `loops list` / `status` / `tail`
482
+ - [ ] Out-of-process control: `pause` / `abort` / `kickback` a running loop from outside
468
483
  - [ ] Optional `wip:` autosave tier (per-iteration recovery, squashed on convergence)
469
484
  - [ ] No-progress / stall detection: the third hard stop, alongside `max` and `budget`
470
485
  - [ ] `cost per accepted change` as a first-class reported metric
package/bin/loops.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Thin launcher. Registers tsx's ESM loader globally so the CLI can transform a
3
3
  // user's `.loop.ts` recipe from any repo (the run-from-anywhere contract), then
4
- // hands off to the CLI. In a published install loops' own code is the built
5
- // `dist/`; running from source (this repo, no build step) falls back to the
6
- // TypeScript entry, which the same tsx loader transforms.
4
+ // hands off to the CLI. From a checkout the TypeScript source is the entry (the
5
+ // no-build-step dev path); a published install ships no `src`, so it falls back
6
+ // to the built `dist`. Source wins so a stale `dist` never shadows live code.
7
7
  import { existsSync } from 'node:fs';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { dirname, join } from 'node:path';
@@ -11,6 +11,6 @@ import { register } from 'tsx/esm/api';
11
11
 
12
12
  register();
13
13
  const here = dirname(fileURLToPath(import.meta.url));
14
- const dist = join(here, '..', 'dist', 'index.js');
15
- const entry = existsSync(dist) ? dist : join(here, '..', 'src', 'index.ts');
14
+ const src = join(here, '..', 'src', 'index.ts');
15
+ const entry = existsSync(src) ? src : join(here, '..', 'dist', 'index.js');
16
16
  await import(pathToFileURL(entry).href);
package/dist/api.d.ts CHANGED
@@ -903,6 +903,12 @@ interface RunOptions {
903
903
  recordTo?: string;
904
904
  /** Snapshot the shared run state here at each loop/dag/job boundary. */
905
905
  checkpoint?: string;
906
+ /**
907
+ * Register this run in the global registry (`~/.loops/runs/<runId>`) and write
908
+ * its live state there, so another process can `loops list` / `status` / `tail`
909
+ * it. Off by default — opt in to make a run observable from outside.
910
+ */
911
+ supervise?: boolean;
906
912
  /** Restore shared run state written by a prior `checkpoint` before starting. */
907
913
  resumeFrom?: string;
908
914
  /**
@@ -930,11 +936,74 @@ interface RunResult {
930
936
  spent: number;
931
937
  remaining: number;
932
938
  };
939
+ /** The registry id, when the run was supervised. */
940
+ runId?: string;
933
941
  }
934
942
  declare function run(job: Job, options?: RunOptions): Promise<RunResult>;
935
943
  /** Process exit code mapped from a terminal outcome. */
936
944
  declare function exitCodeFor(outcome: Outcome): number;
937
945
 
946
+ /**
947
+ * Out-of-process supervision. A supervised run registers itself under a global
948
+ * registry (`~/.loops/runs/<runId>/`) and writes its live state there as it goes:
949
+ *
950
+ * - `status.json`: a snapshot rewritten at each boundary: the run's shape (the
951
+ * static `JobMeta`) plus where it is right now (path, iteration, last gate
952
+ * verdict and confidence, last outcome, token usage, terminal status at end).
953
+ * - `events.jsonl`: the event stream appended live (the same record `recordTo`
954
+ * writes, here automatically and in the registry).
955
+ *
956
+ * A separate process (a human `loops list/status/tail`, or an agent over MCP)
957
+ * reads those files. No daemon, no socket: the filesystem is the channel, which
958
+ * is the same "the workspace is the state" bet the rest of the library makes.
959
+ * Liveness is a pid check, so a crashed run is distinguishable from a live one.
960
+ */
961
+
962
+ /** The registry root. `LOOPS_HOME` overrides `~/.loops` (used to isolate tests). */
963
+ declare function runsHome(): string;
964
+ interface RunLive {
965
+ path: string[];
966
+ iteration: number;
967
+ lastGate?: {
968
+ which: string;
969
+ met: boolean;
970
+ confidence?: number;
971
+ reason: string;
972
+ };
973
+ lastOutcome?: {
974
+ status: string;
975
+ summary?: string;
976
+ };
977
+ usage: {
978
+ inputTokens: number;
979
+ outputTokens: number;
980
+ calls: number;
981
+ };
982
+ }
983
+ interface RunStatus {
984
+ runId: string;
985
+ pid: number;
986
+ cwd: string;
987
+ title: string;
988
+ startedAt: number;
989
+ updatedAt: number;
990
+ endedAt?: number;
991
+ /** Stored disposition: `running` until the run ends, then the terminal status. */
992
+ status: 'running' | Outcome['status'];
993
+ /** Whether the owning process is still alive: computed on read, not stored. */
994
+ alive?: boolean;
995
+ shape?: JobMeta;
996
+ live: RunLive;
997
+ }
998
+ /** Read one run's status, with `alive` computed from its pid. */
999
+ declare function readRunStatus(runId: string): RunStatus | undefined;
1000
+ /** All known runs, newest first. */
1001
+ declare function listRuns(): RunStatus[];
1002
+ /** Path to a run's appended event stream (for tailing). */
1003
+ declare function runEventsPath(runId: string): string;
1004
+ /** A compact one-line rendering of an event, for `loops tail`. */
1005
+ declare function formatEvent(event: LoopEvent): string;
1006
+
938
1007
  /**
939
1008
  * Public API. A loop-definition file imports from here and `export default`s a
940
1009
  * `Job` (usually a `loop(...)` or `dag(...)`). The CLI runs that default export.
@@ -946,4 +1015,4 @@ declare function exitCodeFor(outcome: Outcome): number;
946
1015
  /** Identity helper that pins the type of a default export to `Job`. */
947
1016
  declare function defineJob(job: Job): Job;
948
1017
 
949
- export { type AgentCheckConfig, AgentDef, AgentRequest, AgentResult, BudgetConfig, type CommitInput, type CommitRecord, type CompactOptions, Condition, ConditionInput, type ConsolidateJobConfig, type ConsolidateOptions, DagConfig, EXIT_PAUSED, Engine, type EngineFactory, EngineName, EngineOptions, EngineRef, EngineRegistry, EnvHandle, Environment, Forge, type GroundOptions, type IsolatedOptions, Job, JobContext, JobMeta, type LedgerEntry, LimitPolicy, type LogQuery, LoopConfig, LoopEvent, type MergeJobConfig, type MergeResult, type MergeSynthesisConfig, type MergeSynthesisResult, MockEngine, type MockEnvOptions, MockEnvironment, type MockResponder, Outcome, type PromptNote, type PullRequestJobConfig, type PushJobConfig, type PushOptions, type PushResult, type RetrieveOptions, type RunOptions, type RunResult, Stats, type StatsSnapshot, type TournamentConfig, Usage, Workspace, type WorktreeHandle, addWorktree, agentCheck, all, always, any, appendLedger, appendPrompt, bodyPassed, commandSucceeds, commit, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, dag, defineJob, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, forgeChecks, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isRepo, isolated, jobMeta, ledgerPath, log, loop, mergeAbort, mergeBranch, mergeJob, mergeNoCommit, mergeSynthesis, minConfidence, mockVerdict, never, not, parallel, predicate, promptPath, pullRequestJob, push, pushJob, quorum, readLedger, readPrompt, removeWorktree, renderPlan, resetLedger, resetPrompt, retrieveLedger, run, sequence, stageAll, toCondition, tournament };
1018
+ export { type AgentCheckConfig, AgentDef, AgentRequest, AgentResult, BudgetConfig, type CommitInput, type CommitRecord, type CompactOptions, Condition, ConditionInput, type ConsolidateJobConfig, type ConsolidateOptions, DagConfig, EXIT_PAUSED, Engine, type EngineFactory, EngineName, EngineOptions, EngineRef, EngineRegistry, EnvHandle, Environment, Forge, type GroundOptions, type IsolatedOptions, Job, JobContext, JobMeta, type LedgerEntry, LimitPolicy, type LogQuery, LoopConfig, LoopEvent, type MergeJobConfig, type MergeResult, type MergeSynthesisConfig, type MergeSynthesisResult, MockEngine, type MockEnvOptions, MockEnvironment, type MockResponder, Outcome, type PromptNote, type PullRequestJobConfig, type PushJobConfig, type PushOptions, type PushResult, type RetrieveOptions, type RunLive, type RunOptions, type RunResult, type RunStatus, Stats, type StatsSnapshot, type TournamentConfig, Usage, Workspace, type WorktreeHandle, addWorktree, agentCheck, all, always, any, appendLedger, appendPrompt, bodyPassed, commandSucceeds, commit, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, dag, defineJob, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, forgeChecks, formatEvent, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isRepo, isolated, jobMeta, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeJob, mergeNoCommit, mergeSynthesis, minConfidence, mockVerdict, never, not, parallel, predicate, promptPath, pullRequestJob, push, pushJob, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, retrieveLedger, run, runEventsPath, runsHome, sequence, stageAll, toCondition, tournament };
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { mergeNoCommit, stageAll, commit, mergeAbort, log, setMeta, jobMeta, isRepo, addWorktree, childContext, composeCommitBody, mergeBranch, removeWorktree, deleteBranch, push, consolidate, toCondition, GhForge } from './chunk-3BPU34DE.js';
2
- export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, fnJob, forgeChecks, fromFile, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, jobMeta, kickback, ledgerPath, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, not, predicate, promptPath, push, quorum, readLedger, readPrompt, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, run, stageAll, toCondition } from './chunk-3BPU34DE.js';
1
+ import { mergeNoCommit, stageAll, commit, mergeAbort, log, setMeta, jobMeta, isRepo, addWorktree, childContext, composeCommitBody, mergeBranch, removeWorktree, deleteBranch, push, consolidate, toCondition, GhForge } from './chunk-6BDWTFOS.js';
2
+ export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, fnJob, forgeChecks, formatEvent, fromFile, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, run, runEventsPath, runsHome, stageAll, toCondition } from './chunk-6BDWTFOS.js';
3
3
  import './chunk-JFTXJ7I2.js';
4
4
  export { SUBAGENT_TOOLS, isEngine } from './chunk-XC46B4FD.js';
5
5
  import './chunk-Y2SD7GBL.js';