@obversa/runtime 0.1.0

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/api.d.ts +72 -0
  4. package/dist/api.js +9775 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/artifacts/conformance.d.ts +1 -0
  7. package/dist/artifacts/file-store.d.ts +8 -0
  8. package/dist/artifacts/store.d.ts +1 -0
  9. package/dist/callback/approval.d.ts +8 -0
  10. package/dist/callback/client.d.ts +38 -0
  11. package/dist/callback/gate.d.ts +1 -0
  12. package/dist/callback/stored-client.d.ts +5 -0
  13. package/dist/chunk-3L6YNPN6.js +3 -0
  14. package/dist/chunk-3L6YNPN6.js.map +1 -0
  15. package/dist/chunk-5GLEABOU.js +3 -0
  16. package/dist/chunk-5GLEABOU.js.map +1 -0
  17. package/dist/chunk-DEW5R23M.js +338 -0
  18. package/dist/chunk-DEW5R23M.js.map +1 -0
  19. package/dist/chunk-DV5P4QLI.js +3056 -0
  20. package/dist/chunk-DV5P4QLI.js.map +1 -0
  21. package/dist/chunk-NIBHM5I5.js +34 -0
  22. package/dist/chunk-NIBHM5I5.js.map +1 -0
  23. package/dist/chunk-RZLMX3IA.js +368 -0
  24. package/dist/chunk-RZLMX3IA.js.map +1 -0
  25. package/dist/core/agent-md.d.ts +36 -0
  26. package/dist/core/agent.d.ts +86 -0
  27. package/dist/core/approval-job.d.ts +43 -0
  28. package/dist/core/assert-graph.d.ts +34 -0
  29. package/dist/core/budget.d.ts +49 -0
  30. package/dist/core/concurrency.d.ts +2 -0
  31. package/dist/core/condition.d.ts +209 -0
  32. package/dist/core/context.d.ts +36 -0
  33. package/dist/core/cost.d.ts +59 -0
  34. package/dist/core/dag.d.ts +20 -0
  35. package/dist/core/decision.d.ts +29 -0
  36. package/dist/core/describe.d.ts +56 -0
  37. package/dist/core/engine-meta.d.ts +5 -0
  38. package/dist/core/env-overlay.d.ts +35 -0
  39. package/dist/core/errors.d.ts +46 -0
  40. package/dist/core/feedback.d.ts +68 -0
  41. package/dist/core/git.d.ts +152 -0
  42. package/dist/core/guards.d.ts +70 -0
  43. package/dist/core/isolated.d.ts +40 -0
  44. package/dist/core/job.d.ts +134 -0
  45. package/dist/core/limits.d.ts +22 -0
  46. package/dist/core/loop.d.ts +24 -0
  47. package/dist/core/merge.d.ts +30 -0
  48. package/dist/core/pipeline.d.ts +35 -0
  49. package/dist/core/process.d.ts +11 -0
  50. package/dist/core/progress.d.ts +82 -0
  51. package/dist/core/redact.d.ts +1 -0
  52. package/dist/core/stats.d.ts +63 -0
  53. package/dist/core/team.d.ts +34 -0
  54. package/dist/core/text.d.ts +8 -0
  55. package/dist/core/tournament.d.ts +25 -0
  56. package/dist/core/types.d.ts +650 -0
  57. package/dist/engines/command-runner.d.ts +1 -0
  58. package/dist/engines/conformance.d.ts +1 -0
  59. package/dist/engines/engine.d.ts +4 -0
  60. package/dist/engines/failure.d.ts +1 -0
  61. package/dist/engines/fallback.d.ts +35 -0
  62. package/dist/engines/message-map.d.ts +1 -0
  63. package/dist/engines/mock.d.ts +1 -0
  64. package/dist/engines/preflight.d.ts +36 -0
  65. package/dist/env/command.d.ts +50 -0
  66. package/dist/env/command.js +65 -0
  67. package/dist/env/command.js.map +1 -0
  68. package/dist/env/environment.d.ts +4 -0
  69. package/dist/env/mock.d.ts +24 -0
  70. package/dist/events/conformance.d.ts +1 -0
  71. package/dist/events/envelope.d.ts +1 -0
  72. package/dist/events/jsonl-store.d.ts +8 -0
  73. package/dist/events/store.d.ts +1 -0
  74. package/dist/graph/commands.d.ts +5 -0
  75. package/dist/graph/conformance.d.ts +32 -0
  76. package/dist/graph/kernel.d.ts +5 -0
  77. package/dist/graph/plan.d.ts +1 -0
  78. package/dist/graph/type.d.ts +7 -0
  79. package/dist/graph/value.d.ts +1 -0
  80. package/dist/graph-types/dag.d.ts +137 -0
  81. package/dist/graph-types/loop.d.ts +194 -0
  82. package/dist/graph-types/team.d.ts +68 -0
  83. package/dist/memory.d.ts +82 -0
  84. package/dist/memory.js +397 -0
  85. package/dist/memory.js.map +1 -0
  86. package/dist/proof/acceptance.d.ts +5 -0
  87. package/dist/proof/artifact.d.ts +6 -0
  88. package/dist/proof/cache.d.ts +4 -0
  89. package/dist/runtime/attempt.d.ts +19 -0
  90. package/dist/runtime/budget.d.ts +4 -0
  91. package/dist/runtime/engine-availability.d.ts +18 -0
  92. package/dist/runtime/graph-executor.d.ts +7 -0
  93. package/dist/runtime/monitor.d.ts +80 -0
  94. package/dist/runtime/node-lifecycle.d.ts +84 -0
  95. package/dist/runtime/paths.d.ts +2 -0
  96. package/dist/runtime/persist.d.ts +31 -0
  97. package/dist/runtime/preflight-record.d.ts +149 -0
  98. package/dist/runtime/process-tree.d.ts +1 -0
  99. package/dist/runtime/result-contract.d.ts +4 -0
  100. package/dist/runtime/result-parts.d.ts +1 -0
  101. package/dist/runtime/run-definition.d.ts +24 -0
  102. package/dist/runtime/run-event.d.ts +9 -0
  103. package/dist/runtime/runner.d.ts +139 -0
  104. package/dist/runtime/supervisor.d.ts +111 -0
  105. package/dist/runtime/team-rooms.d.ts +13 -0
  106. package/dist/runtime/workspace-policy.d.ts +25 -0
  107. package/dist/storage/error.d.ts +1 -0
  108. package/dist/storage/id.d.ts +1 -0
  109. package/dist/storage/local.d.ts +12 -0
  110. package/dist/storage/local.js +1492 -0
  111. package/dist/storage/local.js.map +1 -0
  112. package/dist/testing.d.ts +15 -0
  113. package/dist/testing.js +274 -0
  114. package/dist/testing.js.map +1 -0
  115. package/dist/workflow-agent-response.d.ts +3 -0
  116. package/dist/workflow-support.d.ts +36 -0
  117. package/dist/workflow-support.js +5 -0
  118. package/dist/workflow-support.js.map +1 -0
  119. package/dist/workflow.d.ts +73 -0
  120. package/dist/workspace/conformance.d.ts +1 -0
  121. package/dist/workspace/git-provider.d.ts +26 -0
  122. package/dist/workspace/provider.d.ts +2 -0
  123. package/package.json +91 -0
@@ -0,0 +1,152 @@
1
+ /** Local Git helpers used by workspace-aware jobs. */
2
+ interface GitOpts {
3
+ cwd: string;
4
+ signal?: AbortSignal;
5
+ excludePaths?: string[];
6
+ includePaths?: string[];
7
+ }
8
+ /** True when `cwd` is inside a git work tree. Never throws. */
9
+ export declare function isRepo(opts: GitOpts): Promise<boolean>;
10
+ /** The checked-out branch name, or undefined on a detached HEAD / non-repo. */
11
+ export declare function currentBranch(opts: GitOpts): Promise<string | undefined>;
12
+ /** The git worktree root containing `cwd`, or undefined outside a git repo. */
13
+ export declare function gitRoot(opts: GitOpts): Promise<string | undefined>;
14
+ /** The HEAD commit sha, or undefined when the branch has no commits yet. */
15
+ export declare function headSha(opts: GitOpts): Promise<string | undefined>;
16
+ /** Stage every change in the work tree (`git add -A`). */
17
+ export declare function stageAll(opts: GitOpts): Promise<void>;
18
+ /** True when there is something staged to commit. */
19
+ export declare function hasStagedChanges(opts: GitOpts): Promise<boolean>;
20
+ /** True when the work tree (staged or unstaged) has any change. */
21
+ export declare function isDirty(opts: GitOpts): Promise<boolean>;
22
+ /**
23
+ * A content hash of the workspace's observable state: HEAD, every pending
24
+ * tracked change (staged + unstaged, with content), the porcelain status, and
25
+ * the CONTENT of untracked files (hashed by git itself, so a revisit to a
26
+ * byte-identical tree fingerprints identically). Whole-workspace hashes omit
27
+ * ignored files; `includePaths` observes ignored content explicitly selected
28
+ * by the caller. Scoped hashes omit unrelated commits. This is the
29
+ * deterministic evidence channel behind `noProgress`: two iterations with the
30
+ * same fingerprint left the observed workspace in the same state. Returns
31
+ * undefined outside a git work tree; the caller treats that channel as
32
+ * absent, never as "unchanged". Never throws.
33
+ */
34
+ export declare function workspaceFingerprint(opts: GitOpts): Promise<string | undefined>;
35
+ interface GitIndexState {
36
+ readonly mode: string;
37
+ readonly oid: string;
38
+ }
39
+ interface GitDirtyState {
40
+ readonly status: string;
41
+ readonly kind: 'file' | 'symlink' | 'missing';
42
+ readonly mode: number;
43
+ readonly digest: string | null;
44
+ readonly lines: readonly string[];
45
+ }
46
+ export interface GitWorkspaceFileState {
47
+ readonly path: string;
48
+ readonly index: GitIndexState | null;
49
+ readonly dirty: GitDirtyState | null;
50
+ }
51
+ export interface GitWorkspaceSnapshot {
52
+ readonly root: string;
53
+ readonly head: string | null;
54
+ readonly files: readonly GitWorkspaceFileState[];
55
+ }
56
+ export interface GitWorkspaceDelta {
57
+ readonly headChanged: boolean;
58
+ readonly changedPaths: readonly string[];
59
+ readonly filesChanged: number;
60
+ readonly linesChanged: number;
61
+ }
62
+ /**
63
+ * Capture the Git-visible entry state for one attempt. Clean tracked files are
64
+ * represented by their index object; only already-dirty bytes are read and
65
+ * reduced to hashes. No workspace content is retained in the snapshot.
66
+ */
67
+ export declare function captureGitWorkspaceSnapshot(opts: GitOpts): Promise<GitWorkspaceSnapshot>;
68
+ /** Compare two snapshots without treating untouched entry dirt as attempt work. */
69
+ export declare function compareGitWorkspaceSnapshots(before: GitWorkspaceSnapshot, after: GitWorkspaceSnapshot, signal?: AbortSignal): Promise<GitWorkspaceDelta>;
70
+ export interface CommitInput {
71
+ subject: string;
72
+ /** The structured body. Joined to the subject with a blank line. */
73
+ body?: string;
74
+ /** Commit even with an empty index (default false). */
75
+ allowEmpty?: boolean;
76
+ }
77
+ /**
78
+ * Commit the staged index. The message is passed on stdin (`-F -`) so an
79
+ * arbitrarily-shaped body never has to survive shell escaping. The repo's
80
+ * configured author is used. The runtime never changes commit authorship.
81
+ * Returns the new sha, or undefined when there was nothing to commit and
82
+ * `allowEmpty` was not set.
83
+ */
84
+ export declare function commit(input: CommitInput, opts: GitOpts): Promise<string | undefined>;
85
+ /** Recent commit messages from one branch, used only to explain a merge. */
86
+ export declare function branchCommits(opts: {
87
+ cwd: string;
88
+ ref: string;
89
+ max: number;
90
+ signal?: AbortSignal;
91
+ }): Promise<Array<{
92
+ subject: string;
93
+ body: string;
94
+ }>>;
95
+ export interface WorktreeHandle {
96
+ /** The isolated working directory. */
97
+ dir: string;
98
+ /** The branch checked out there. */
99
+ branch: string;
100
+ }
101
+ /**
102
+ * Fork an isolated worktree on a new branch from `base` (default HEAD). Each
103
+ * concurrent writer gets its own working dir and branch, so siblings never
104
+ * collide on files or the index.
105
+ */
106
+ export declare function addWorktree(repoDir: string, opts: {
107
+ branch: string;
108
+ base?: string;
109
+ signal?: AbortSignal;
110
+ }): Promise<WorktreeHandle>;
111
+ /** Remove a worktree (force-discards anything uncommitted left in it). */
112
+ export declare function removeWorktree(repoDir: string, dir: string, opts?: {
113
+ signal?: AbortSignal;
114
+ }): Promise<void>;
115
+ /** Delete a branch ref (used to clean up a merged fork branch). */
116
+ export declare function deleteBranch(repoDir: string, branch: string, opts?: {
117
+ signal?: AbortSignal;
118
+ }): Promise<void>;
119
+ export interface MergeResult {
120
+ ok: boolean;
121
+ conflict: boolean;
122
+ }
123
+ /**
124
+ * Land a fork branch back into the branch checked out at `repoDir` (`--no-ff`).
125
+ * On conflict the merge is aborted so the target stays clean and the caller can
126
+ * fail the node. The runtime does not auto-resolve (a merge-resolver is a separate
127
+ * layer).
128
+ */
129
+ export declare function mergeBranch(repoDir: string, branch: string, opts?: {
130
+ signal?: AbortSignal;
131
+ message?: string;
132
+ }): Promise<MergeResult>;
133
+ /**
134
+ * Begin a `--no-ff --no-commit` merge WITHOUT aborting on conflict, so a resolver
135
+ * can synthesise the result. `clean` means it merged cleanly (staged, ready to
136
+ * commit); otherwise `conflicted` lists the unresolved paths (with markers).
137
+ */
138
+ export declare function mergeNoCommit(repoDir: string, branch: string, opts?: {
139
+ signal?: AbortSignal;
140
+ }): Promise<{
141
+ clean: boolean;
142
+ conflicted: string[];
143
+ }>;
144
+ /** Paths with unresolved merge conflicts. */
145
+ export declare function conflictedFiles(repoDir: string, opts?: {
146
+ signal?: AbortSignal;
147
+ }): Promise<string[]>;
148
+ /** Abort an in-progress merge. */
149
+ export declare function mergeAbort(repoDir: string, opts?: {
150
+ signal?: AbortSignal;
151
+ }): Promise<void>;
152
+ export {};
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Hardening gates that keep a convergence loop honest without spending a
3
+ * model call — deterministic conditions in the supervisor-orchestrator
4
+ * tradition, adapted to the Obversa runtime:
5
+ *
6
+ * - `ratchet`: a measured metric may only hold or improve against a
7
+ * runtime-owned baseline that is written **only in the improving
8
+ * direction**, so the agent can never loosen its own bar.
9
+ * - `writeScope`: every pending workspace change introduced since loop entry
10
+ * must match a declared glob, so pre-existing dirt cannot wedge a scoped job.
11
+ * - `sampled`: run an expensive condition on a deterministic bucket of
12
+ * iterations (a sha256 cut, so re-runs land on the same side), treating the
13
+ * unsampled rest as met — how a costly judge stays affordable on a
14
+ * high-iteration loop.
15
+ */
16
+ import type { Condition, ConditionInput, JobContext } from './types.js';
17
+ export interface RatchetOptions {
18
+ /** The metric key to read from the command's JSON output. */
19
+ metric: string;
20
+ /** Which way is better: `down` (default; the value must not rise — lint
21
+ * errors, bundle bytes, TODO count) or `up` (must not fall — coverage). */
22
+ direction?: 'down' | 'up';
23
+ /** Where baselines live. Default `<OBVERSA_HOME|~/.obversa>/ratchets` — outside
24
+ * the workspace, so the baseline is never edited or committed by the loop
25
+ * it constrains. */
26
+ baselineDir?: string;
27
+ cwd?: string;
28
+ timeoutMs?: number;
29
+ env?: Record<string, string>;
30
+ }
31
+ /**
32
+ * Deterministic gate over a **measured, monotone** signal. Runs `command`,
33
+ * reads `metric` from its JSON output (`{"metrics": {"<name>": n}}`, or a
34
+ * bare object), and is met only when the value holds or improves on the
35
+ * stored baseline. The baseline is runtime-owned and written only in the
36
+ * improving direction — the first run seeds it — so the constrained loop can
37
+ * neither loosen nor forget its own bar. Everything else fails closed: a
38
+ * command failure, missing metric, or unparsable output is "not met".
39
+ */
40
+ export declare function ratchet(command: string, args: string[] | undefined, opts: RatchetOptions): Condition;
41
+ /** Minimal glob → RegExp: `**` crosses directories, `*` stays inside one,
42
+ * `?` is a single char. Enough for scope declarations without a dependency. */
43
+ export declare function globToRegExp(glob: string): RegExp;
44
+ export interface WriteScopeOptions {
45
+ cwd?: string;
46
+ /** Compare pending changes with loop entry, or require the whole tree to fit. */
47
+ mode?: 'delta' | 'absolute';
48
+ }
49
+ /**
50
+ * Met only when every staged, unstaged, or untracked change introduced since
51
+ * the enclosing loop started matches a declared glob. This ignores untouched
52
+ * pre-existing dirt without hiding a body edit to an already-dirty file. Use
53
+ * `mode: 'absolute'` when the complete pending state must fit the scope.
54
+ * Outside a loop, evaluation is absolute. A workspace that is not a git
55
+ * repository fails closed.
56
+ */
57
+ export declare function writeScope(globs: string[], opts?: WriteScopeOptions): Condition;
58
+ export interface SampledOptions {
59
+ /** Stable sampling key; default `<path>:<iteration>`, so a re-run of the
60
+ * same iteration lands on the same side of the cut. */
61
+ key?: string | ((ctx: JobContext) => string);
62
+ }
63
+ /**
64
+ * Run `condition` on a deterministic fraction of evaluations and treat the
65
+ * rest as met. The bucket is a sha256 cut of a stable key (not `Math.random`),
66
+ * so the same iteration always samples the same way — an expensive judge on
67
+ * `rate: 0.25` really runs every ~4th iteration, reproducibly. Deterministic
68
+ * gates cost nothing; sample only what spends.
69
+ */
70
+ export declare function sampled(rate: number, condition: ConditionInput, opts?: SampledOptions): Condition;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `isolated(job)` runs any Job in its own git worktree on a fork branch, and lands
3
+ * its work back into the parent branch on pass. The concurrency boundary as a Job
4
+ * wrapper, not a node type.
5
+ *
6
+ * dag nodes can already fork a worktree (`isolation: 'worktree'`), but that only
7
+ * works for predeclared nodes. A Tend loop dispatches dynamically: it discovers each
8
+ * ticket at runtime and routes it to the right shape of sub-loop, and each dispatch
9
+ * wants its own isolated worktree so parallel tickets never collide on files or the
10
+ * index. `isolated()` makes that composable: wrap the dispatched Job.
11
+ *
12
+ * On pass: any uncommitted remainder is committed in the worktree, then the fork
13
+ * branch merges back (`--no-ff`). Land-back merges are serialised across all
14
+ * `isolated()` jobs in the process, so concurrent dispatch cannot race the parent
15
+ * index/HEAD. A conflict fails, or is synthesised when asked. The worktree
16
+ * is always removed; a cleanly-merged fork branch is deleted. A non-repo workspace
17
+ * degrades to running in place (a warning, no isolation).
18
+ *
19
+ * NOTE: dag's own runNodeJob holds parallel worktree/land-back logic (plus per-team
20
+ * environments). The two should be unified (dag delegating to `isolated()`) once
21
+ * `isolated()` grows environment support; until then the land-back logic lives in
22
+ * both deliberately, to avoid destabilising the dag path.
23
+ */
24
+ import type { Job } from './types.js';
25
+ import type { ReasoningRecorder } from '@obversa/api';
26
+ export interface IsolatedOptions {
27
+ /** Label for the fork branch and the child path. Default 'isolated'. */
28
+ label?: string;
29
+ /** On a land-back conflict: 'fail' (default) or 'synthesize'. */
30
+ onConflict?: 'fail' | 'synthesize';
31
+ /**
32
+ * A record of why this stage's change exists. It watches the stage's own
33
+ * events and, when the stage passes with something to commit, supplies the
34
+ * message for the commit that carries the change. A stage that changed
35
+ * nothing produces no commit, so it is never asked for one.
36
+ */
37
+ record?: ReasoningRecorder;
38
+ }
39
+ /** Wrap a Job so it runs in an isolated worktree and lands back on pass. */
40
+ export declare function isolated(job: Job, opts?: IsolatedOptions): Job;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Job builders. A `Job` is the unit of work; these are the common shapes.
3
+ * The agent launch (`agentJob`) is deliberately provider-agnostic: it only
4
+ * ever calls `Engine.run`, so it knows nothing about Claude, the CLI, an SDK,
5
+ * an HTTP API, or any framework — swap the engine and the same job runs.
6
+ */
7
+ import type { Outcome, Job, JobContext, ProofArtifact } from './types.js';
8
+ /** Shared state key holding every engine answer the run recorded so far.
9
+ * Written by the runtime beside each engine:usage event; read by workflow
10
+ * layers that must compare what answered against what was declared. */
11
+ export declare const RECORDED_ENGINE_USAGE = "obversa:recorded-engine-usage";
12
+ /** One recorded engine answer: the answering model and the job path that
13
+ * produced it. */
14
+ export interface RecordedEngineUsage {
15
+ readonly model: string;
16
+ readonly path: readonly string[];
17
+ /** The review side this answer belongs to, set by the workflow layer
18
+ * that built the job. Untagged entries (advisors, agentCheck) belong
19
+ * to neither side and are outside the recorded-family gate. */
20
+ readonly role?: 'writer' | 'reviewer';
21
+ /** The stage name this answer belongs to. */
22
+ readonly stage?: string;
23
+ }
24
+ import type { AgentRequest, EngineRef } from '../engines/engine.js';
25
+ import { type LoopErrorCode } from './errors.js';
26
+ import { type AgentDef } from './agent.js';
27
+ import { kickback, revisionRequest } from './feedback.js';
28
+ export interface AgentJobConfig {
29
+ /** Tag this job's engine answers with the review side and stage they
30
+ * belong to, so a workflow layer can compare recorded sides without
31
+ * inferring them from paths. Untagged answers are outside such gates. */
32
+ readonly recordAs?: {
33
+ readonly role: 'writer' | 'reviewer';
34
+ readonly stage: string;
35
+ };
36
+ /** Job label (for events). Defaults to the agent's name, then `'agent'`. */
37
+ label?: string;
38
+ /**
39
+ * A reusable agent definition — supplies `system` (persona + skills), `model`, and
40
+ * `tools` (the job's `system` and `model` override it when also set). The
41
+ * persona lives in markdown via `fromFile`; this is the typed wrapper around it.
42
+ */
43
+ agent?: AgentDef;
44
+ /** The prompt, or a function of the context (e.g. include the iteration). */
45
+ prompt: string | ((ctx: JobContext) => string | Promise<string>);
46
+ system?: string | ((ctx: JobContext) => string);
47
+ /** Engine override: a registered name, your own `Engine`, or the default. */
48
+ engine?: EngineRef;
49
+ /** Bare model id — passed straight through to the engine. */
50
+ model?: string;
51
+ maxTokens?: number;
52
+ tools?: string[];
53
+ allowedTools?: string[];
54
+ workspaceMode?: AgentRequest['workspaceMode'];
55
+ /**
56
+ * Mark this turn a leaf: forbid spawning sub-agents (the engine disallows the sub-agent
57
+ * tool), so a branch bottoms out here. Falls back to the agent def's `leaf`.
58
+ */
59
+ leaf?: boolean;
60
+ /**
61
+ * Append the current `ctx.lastReview` / revision feedback to the prompt. This
62
+ * keeps implementation agents from having to remember to manually read the
63
+ * runtime feedback channel in every prompt function.
64
+ */
65
+ consumeFeedback?: boolean;
66
+ /**
67
+ * Append a compact DAG-position block: this node, its direct dependencies, and
68
+ * its direct dependents, without handing the agent the whole orchestration graph.
69
+ */
70
+ graphContext?: boolean;
71
+ /**
72
+ * Bounded, visible escalation: the worker may ask for a consult by replying
73
+ * with a `<consult_advisor>` block. The runtime runs one model-pinned advisor turn,
74
+ * records the question/reply, and then gives the reply back to the worker in a
75
+ * fresh turn. This is the sanctioned alternative to shelling out to another
76
+ * model from inside a leaf.
77
+ */
78
+ advisor?: AdvisorConfig;
79
+ /** Working dir for the turn. Default: the workspace dir (the worktree). */
80
+ cwd?: string;
81
+ /**
82
+ * Env vars pinned for this leaf's engine subprocess — the most specific
83
+ * layer, over any `withEnv` overlay and the running environment's vars.
84
+ * Engines that spawn no subprocess ignore it.
85
+ */
86
+ env?: Record<string, string>;
87
+ /**
88
+ * Soft timeout for each worker or fallback invocation. Advisor consults
89
+ * inherit it unless overridden; each invocation receives its own window.
90
+ */
91
+ timeoutMs?: number;
92
+ /** Extra hard-timeout window after `timeoutMs` for completed final results. */
93
+ timeoutGraceMs?: number;
94
+ /** Fallback route(s) used when the primary engine hits a configured error. */
95
+ fallback?: AgentRoute | AgentRoute[];
96
+ /** Error codes that may spill to `fallback`. Default: RATE_LIMIT and QUOTA. */
97
+ fallbackOn?: LoopErrorCode[];
98
+ /**
99
+ * Map the agent's raw text into an `Outcome`. Default: `pass`, with the text
100
+ * as the summary. Return `fail` to keep an enclosing loop going. `text` is
101
+ * the reply after the capture scrub (injected env values and secret-shaped
102
+ * tokens are redacted) before the outcome enters persisted records.
103
+ */
104
+ outcome?: (text: string, ctx: JobContext) => Outcome | Promise<Outcome>;
105
+ }
106
+ export interface AdvisorConfig {
107
+ engine?: EngineRef;
108
+ model?: string;
109
+ systemPrompt?: string;
110
+ maxCalls?: number;
111
+ maxTokens?: number;
112
+ timeoutMs?: number;
113
+ timeoutGraceMs?: number;
114
+ }
115
+ export interface AgentRoute {
116
+ engine?: EngineRef;
117
+ model?: string;
118
+ timeoutMs?: number;
119
+ timeoutGraceMs?: number;
120
+ }
121
+ export type ProofDescriptor = ProofArtifact;
122
+ export type ProofProducer = (ctx: JobContext) => ProofDescriptor | Promise<ProofDescriptor>;
123
+ /** Run one fresh agent turn through whichever engine is selected. */
124
+ export declare function agentJob(config: AgentJobConfig): Job;
125
+ export { kickback, revisionRequest };
126
+ /** A deterministic step from a plain function — for glue, checks, side effects. */
127
+ /**
128
+ * What a `fnJob` function may return: a full outcome, a one-line summary (the
129
+ * step passed, and this is what it did), or nothing (the step passed; its
130
+ * label is the summary). A throw is a fail carrying the error.
131
+ */
132
+ export type FnJobResult = Outcome | string | void;
133
+ export declare function fnJob(label: string, fn: (ctx: JobContext) => FnJobResult | Promise<FnJobResult>): Job;
134
+ export declare function prove(name: string, producer: ProofProducer): Job;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Provider-limit plumbing shared by the engines and the runner.
3
+ *
4
+ * Engines report a provider-neutral `EngineError`. The runtime maps rate-limit
5
+ * and quota failures to a `LoopError` while preserving `retryAfterMs` and
6
+ * `resetAt`. The runner reads that hint through `waitMsFor` to decide whether
7
+ * to wait, pause, or fail (see `onLimit`).
8
+ *
9
+ * Keeping the reset-time math in one place means every engine and the policy
10
+ * agree on what "a known, bounded wait" means.
11
+ */
12
+ import type { LoopError } from './errors.js';
13
+ export { retryAfterHeaderToMs } from '@obversa/core/command';
14
+ /** True when an error is one the `onLimit` policy governs. */
15
+ export declare function isLimitError(error: LoopError | undefined): error is LoopError;
16
+ /**
17
+ * The wait a limit error implies, in ms, or `undefined` when no reset is known.
18
+ * Prefers an explicit `retryAfterMs`; falls back to `resetAt - now` (floored at
19
+ * 0 so an already-passed reset waits nothing rather than going negative). BUDGET
20
+ * never refreshes within a run, so it never yields a wait.
21
+ */
22
+ export declare function waitMsFor(error: LoopError, now?: number): number | undefined;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The loop primitive. `loop(config)` returns a `Job`, so loop jobs nest by simply
3
+ * passing one as another's `body` or `review`.
4
+ *
5
+ * Lifecycle of one loop:
6
+ * 1. `start` gate (one-or-many conditions) — unmet => `aborted`.
7
+ * 2. repeat, up to `max`:
8
+ * run `body` (fresh context each turn) → `stopOn`? → `until`?
9
+ * if `until` is met and there's a `review`, run it:
10
+ * review `pass` => loop completes `pass`
11
+ * review !pass => re-enter the loop ← "review fails, run main loop again"
12
+ * with no `until`, a `pass` body ends the loop; `max` reached => `exhausted`.
13
+ * 3. `onComplete` post-action runs once, whatever the status.
14
+ *
15
+ * The review-restart cycle is bounded: by `max` (shared with ordinary
16
+ * iterations) and, independently, by `maxReviewRestarts`. The failed review is
17
+ * threaded to the next iteration as `ctx.lastReview` so the body can act on it.
18
+ *
19
+ * Every piece of user code (conditions, the body, hooks, the review) is guarded:
20
+ * a throw is classified and ends the loop with `fail`, but `loop:end` and the
21
+ * `onComplete` post-action still run.
22
+ */
23
+ import type { LoopConfig, Job } from './types.js';
24
+ export declare function loop(config: LoopConfig): Job;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Merge as synthesis. A raw `git merge` either applies cleanly or fails on conflict.
3
+ * `mergeSynthesis` instead has an agent resolve each conflicted file coherently
4
+ * (preserving both intents), and writes a merge commit body that synthesises what the
5
+ * two branches were each trying to do, not "merge branch X".
6
+ *
7
+ * It is text-in/text-out, so it works through any `Engine` (no tool-use needed): the
8
+ * conflicted file content goes in, the resolved content comes back. One call per
9
+ * conflicted file plus one for the synthesis body, and nothing when the merge is
10
+ * already clean. The merge is aborted if resolution throws, so the target is never
11
+ * left half-merged.
12
+ */
13
+ import type { JobContext } from './types.js';
14
+ import type { EngineRef } from '../engines/engine.js';
15
+ export declare const mergeLock: import("p-limit").LimitFunction;
16
+ export interface MergeSynthesisConfig {
17
+ /** The branch to land into the current workspace. */
18
+ branch: string;
19
+ /** Conventional subject for the merge commit. */
20
+ message?: string;
21
+ engine?: EngineRef;
22
+ model?: string;
23
+ }
24
+ export interface MergeSynthesisResult {
25
+ ok: boolean;
26
+ /** Whether a conflict had to be resolved. */
27
+ conflict: boolean;
28
+ sha?: string;
29
+ }
30
+ export declare function mergeSynthesis(ctx: JobContext, config: MergeSynthesisConfig): Promise<MergeSynthesisResult>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `pipeline(name, stages)` is declarative ordered stages as sugar over `dag()`: the
3
+ * Job graph is the pipeline. Each stage becomes a dag node that `needs` the stage
4
+ * before it; an explicit `needs` replaces that default, so fan-out/fan-in is still
5
+ * just edges. All dag semantics apply unchanged: a skipped stage (unmet `when`) counts
6
+ * green so the chain continues, and an optional stage's failure neither fails the
7
+ * pipeline nor blocks the next stage (its consumers must tolerate its artifacts being
8
+ * absent).
9
+ */
10
+ import type { ConditionInput, DagConfig, Job, JobMeta } from './types.js';
11
+ export interface PipelineStage {
12
+ name: string;
13
+ job: Job;
14
+ /** Gate (one or many): when unmet the stage is skipped, not failed. */
15
+ when?: ConditionInput;
16
+ /** A failure here does not fail the pipeline, and does not block later stages. */
17
+ optional?: boolean;
18
+ /**
19
+ * Explicit dependencies, replacing the default `[previous stage]` entirely
20
+ * (`[]` detaches the stage). This is how a linear pipeline grows fan-out
21
+ * (two stages needing the same producer) and fan-in (one stage needing both).
22
+ */
23
+ needs?: string[];
24
+ /** Per-stage isolation override (dag's per-node `isolate`). */
25
+ isolate?: boolean;
26
+ /** Kickback allowlist for this stage (dag's per-node `acceptsKickbackTo`). */
27
+ acceptsKickbackTo?: string[];
28
+ }
29
+ /** Ordered named stages, auto-chained: stage i needs stage i-1. Sugar over `dag`. */
30
+ export declare function pipeline(name: string, stages: PipelineStage[], opts?: Omit<DagConfig, 'name' | 'nodes'>): Job;
31
+ /**
32
+ * Render a `kind:'dag'` job's stages as a GitHub-markdown table (one row per
33
+ * node, in the meta's node order). Accepts the `Job` itself or its `JobMeta`.
34
+ */
35
+ export declare function renderPipelineTable(source: Job | JobMeta): string;
@@ -0,0 +1,11 @@
1
+ import { type RunChildOptions, type RunChildResult } from '@obversa/core';
2
+ export declare const DEFAULT_PROCESS_TIMEOUT_MS: number;
3
+ export declare const DEFAULT_PROCESS_GRACE_MS: number;
4
+ export declare const DEFAULT_PROCESS_OUTPUT_BYTES: number;
5
+ export type RuntimeProcessOptions = Omit<RunChildOptions, 'timeoutMs' | 'killGraceMs' | 'maxOutputBytes' | 'hooks'> & {
6
+ readonly timeoutMs?: number;
7
+ readonly killGraceMs?: number;
8
+ readonly maxOutputBytes?: number;
9
+ };
10
+ export declare function runRuntimeProcess(options: RuntimeProcessOptions): Promise<RunChildResult>;
11
+ export declare function processText(output: Uint8Array): string;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * No-progress (stall) detection: the third hard stop, alongside `max` and
3
+ * `budget`. `max` bounds how many attempts a loop gets and `budget` bounds what
4
+ * they cost; neither can tell slow-but-real convergence from the same failure
5
+ * repeating. This module detects the latter, so a stalled loop exits at
6
+ * iteration N+window instead of running to its cap.
7
+ *
8
+ * The decision rule is NOVELTY, not change. An iteration makes progress when it
9
+ * reaches a state this run has never seen:
10
+ *
11
+ * - the workspace fingerprint (HEAD + pending diff + untracked content) is new
12
+ * — so an agent oscillating A→B→A gets no credit for the return trip;
13
+ * - a caller-supplied `signal` value is new — the escape hatch for loop jobs whose
14
+ * progress lives outside the worktree (a queue length, a passing-test count);
15
+ * - the gate confidence beats its previous best by `minConfidenceDelta` — a
16
+ * high-water mark, so judge jitter around a flat score is not progress but
17
+ * slow, steady improvement accumulates until it clears the bar;
18
+ * - (opt-in, `gate: true`) the failing until-gate's diagnostic OUTPUT is new
19
+ * — the same failure signature repeating is stall evidence, for
20
+ * deterministic gates whose output is stable across identical failures.
21
+ *
22
+ * `window` consecutive iterations with evidence and no novelty = stalled. The
23
+ * default is deliberately conservative (any channel's novelty counts): a false
24
+ * "stalled" on work that was actually converging is worse than one more
25
+ * iteration. An iteration with NO evidence channel at all (no git workspace, no
26
+ * confidence, no signal) is indeterminate — it neither extends nor resets the
27
+ * stall run, and the detector reports itself inert so the loop can warn once.
28
+ * Gate/review reasons are deliberately NOT compared: judge prose varies between
29
+ * identical verdicts, so it is quoted in the report but never used as evidence.
30
+ */
31
+ import type { NoProgressConfig, NoProgressInput, StallReport } from './types.js';
32
+ export type { NoProgressConfig, NoProgressInput, StallReport } from './types.js';
33
+ /** One completed, non-converged iteration as the tracker sees it. */
34
+ export interface ProgressSample {
35
+ iteration: number;
36
+ /** Workspace fingerprint, when the workspace is a git repo. */
37
+ fingerprint?: string;
38
+ /** The confidence that gated this turn (review ?? until ?? body). */
39
+ confidence?: number;
40
+ /** The custom signal value, when a `signal` fn is configured. */
41
+ signal?: string;
42
+ /** The failing gate's diagnostic OUTPUT — never the prose reason. */
43
+ gate?: string;
44
+ /** The gate/review reason — reporting only, never evidence. */
45
+ reason?: string;
46
+ }
47
+ /** Resolve the `noProgress` sugar (`3` ⇒ `{ window: 3 }`) with defaults applied. */
48
+ export declare function resolveNoProgress(input: NoProgressInput | undefined): (Required<Pick<NoProgressConfig, 'window' | 'minConfidenceDelta' | 'gate'>> & NoProgressConfig) | undefined;
49
+ /**
50
+ * The novelty tracker behind `LoopConfig.noProgress`. Feed it one sample per
51
+ * non-converged iteration; it returns a `StallReport` the moment `window`
52
+ * consecutive samples show evidence and no novelty.
53
+ */
54
+ export declare class ProgressTracker {
55
+ readonly window: number;
56
+ readonly minConfidenceDelta: number;
57
+ /** Every state this run has reached, namespaced by channel. */
58
+ private readonly seen;
59
+ /** Confidence high-water mark — the best score at the last progress point. */
60
+ private best;
61
+ /** The current run of consecutive no-progress iterations. */
62
+ private stalledRun;
63
+ private lastEvidence;
64
+ private lastReason;
65
+ private indeterminate;
66
+ private sampled;
67
+ constructor(cfg: {
68
+ window: number;
69
+ minConfidenceDelta: number;
70
+ });
71
+ /**
72
+ * Record one iteration. Returns a `StallReport` when this sample fills the
73
+ * window, else undefined.
74
+ */
75
+ record(sample: ProgressSample): StallReport | undefined;
76
+ /**
77
+ * True when the detector has seen a full window of samples and none carried
78
+ * any evidence channel — detection is configured but cannot fire. The loop
79
+ * uses this to warn once instead of failing silently-inert.
80
+ */
81
+ isInert(): boolean;
82
+ }
@@ -0,0 +1 @@
1
+ export { redactEnvValues, redactSecrets, scrubCapture, } from '@obversa/core/command';