specrails-core 5.2.2 → 5.3.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 (102) hide show
  1. package/README.md +3 -0
  2. package/bin/specrails-core.mjs +5 -0
  3. package/dist/agent-runtime/cli-executor.d.ts +34 -0
  4. package/dist/agent-runtime/cli-executor.js +335 -0
  5. package/dist/agent-runtime/cli-executor.js.map +1 -0
  6. package/dist/agent-runtime/cli-process.d.ts +32 -0
  7. package/dist/agent-runtime/cli-process.js +159 -0
  8. package/dist/agent-runtime/cli-process.js.map +1 -0
  9. package/dist/agent-runtime/cli.d.ts +38 -0
  10. package/dist/agent-runtime/cli.js +168 -0
  11. package/dist/agent-runtime/cli.js.map +1 -0
  12. package/dist/agent-runtime/config.d.ts +7 -0
  13. package/dist/agent-runtime/config.js +162 -0
  14. package/dist/agent-runtime/config.js.map +1 -0
  15. package/dist/agent-runtime/core-host.d.ts +33 -0
  16. package/dist/agent-runtime/core-host.js +97 -0
  17. package/dist/agent-runtime/core-host.js.map +1 -0
  18. package/dist/agent-runtime/durable-store.d.ts +22 -0
  19. package/dist/agent-runtime/durable-store.js +205 -0
  20. package/dist/agent-runtime/durable-store.js.map +1 -0
  21. package/dist/agent-runtime/executor-types.d.ts +92 -0
  22. package/dist/agent-runtime/executor-types.js +29 -0
  23. package/dist/agent-runtime/executor-types.js.map +1 -0
  24. package/dist/agent-runtime/executors.d.ts +22 -0
  25. package/dist/agent-runtime/executors.js +44 -0
  26. package/dist/agent-runtime/executors.js.map +1 -0
  27. package/dist/agent-runtime/gemini-policy.d.ts +8 -0
  28. package/dist/agent-runtime/gemini-policy.js +36 -0
  29. package/dist/agent-runtime/gemini-policy.js.map +1 -0
  30. package/dist/agent-runtime/graph/artifacts.d.ts +40 -0
  31. package/dist/agent-runtime/graph/artifacts.js +179 -0
  32. package/dist/agent-runtime/graph/artifacts.js.map +1 -0
  33. package/dist/agent-runtime/graph/nodes.d.ts +36 -0
  34. package/dist/agent-runtime/graph/nodes.js +274 -0
  35. package/dist/agent-runtime/graph/nodes.js.map +1 -0
  36. package/dist/agent-runtime/graph/review-policy.d.ts +18 -0
  37. package/dist/agent-runtime/graph/review-policy.js +32 -0
  38. package/dist/agent-runtime/graph/review-policy.js.map +1 -0
  39. package/dist/agent-runtime/graph/roles.d.ts +44 -0
  40. package/dist/agent-runtime/graph/roles.js +126 -0
  41. package/dist/agent-runtime/graph/roles.js.map +1 -0
  42. package/dist/agent-runtime/graph/state.d.ts +75 -0
  43. package/dist/agent-runtime/graph/state.js +23 -0
  44. package/dist/agent-runtime/graph/state.js.map +1 -0
  45. package/dist/agent-runtime/graph-checkpointer.d.ts +61 -0
  46. package/dist/agent-runtime/graph-checkpointer.js +137 -0
  47. package/dist/agent-runtime/graph-checkpointer.js.map +1 -0
  48. package/dist/agent-runtime/index.d.ts +14 -0
  49. package/dist/agent-runtime/index.js +15 -0
  50. package/dist/agent-runtime/index.js.map +1 -0
  51. package/dist/agent-runtime/kimi-acp.d.ts +7 -0
  52. package/dist/agent-runtime/kimi-acp.js +179 -0
  53. package/dist/agent-runtime/kimi-acp.js.map +1 -0
  54. package/dist/agent-runtime/openai-executor.d.ts +18 -0
  55. package/dist/agent-runtime/openai-executor.js +174 -0
  56. package/dist/agent-runtime/openai-executor.js.map +1 -0
  57. package/dist/agent-runtime/prompts.d.ts +40 -0
  58. package/dist/agent-runtime/prompts.js +291 -0
  59. package/dist/agent-runtime/prompts.js.map +1 -0
  60. package/dist/agent-runtime/workflow-types.d.ts +210 -0
  61. package/dist/agent-runtime/workflow-types.js +2 -0
  62. package/dist/agent-runtime/workflow-types.js.map +1 -0
  63. package/dist/agent-runtime/workflow.d.ts +17 -0
  64. package/dist/agent-runtime/workflow.js +609 -0
  65. package/dist/agent-runtime/workflow.js.map +1 -0
  66. package/dist/agent-runtime/workspace-tools.d.ts +25 -0
  67. package/dist/agent-runtime/workspace-tools.js +120 -0
  68. package/dist/agent-runtime/workspace-tools.js.map +1 -0
  69. package/dist/installer/cli.d.ts +35 -0
  70. package/dist/installer/cli.js +3 -0
  71. package/dist/installer/cli.js.map +1 -1
  72. package/dist/installer/commands/doctor.d.ts +27 -0
  73. package/dist/installer/commands/framework.d.ts +85 -0
  74. package/dist/installer/commands/init.d.ts +147 -0
  75. package/dist/installer/commands/update.d.ts +56 -0
  76. package/dist/installer/commands/v5-migration.d.ts +32 -0
  77. package/dist/installer/phases/framework-lifecycle.d.ts +53 -0
  78. package/dist/installer/phases/install-config.d.ts +64 -0
  79. package/dist/installer/phases/manifest.d.ts +45 -0
  80. package/dist/installer/phases/prereqs.d.ts +51 -0
  81. package/dist/installer/phases/provider-detect.d.ts +89 -0
  82. package/dist/installer/phases/scaffold.d.ts +211 -0
  83. package/dist/installer/runtime/kimi.d.ts +84 -0
  84. package/dist/installer/runtime/pipeline-state.d.ts +210 -0
  85. package/dist/installer/runtime/pipeline-state.js +107 -38
  86. package/dist/installer/runtime/pipeline-state.js.map +1 -1
  87. package/dist/installer/util/errors.d.ts +46 -0
  88. package/dist/installer/util/exec.d.ts +41 -0
  89. package/dist/installer/util/fs.d.ts +153 -0
  90. package/dist/installer/util/git.d.ts +44 -0
  91. package/dist/installer/util/install-transaction.d.ts +29 -0
  92. package/dist/installer/util/logger.d.ts +31 -0
  93. package/dist/installer/util/paths.d.ts +34 -0
  94. package/dist/installer/util/prompts.d.ts +23 -0
  95. package/dist/installer/util/registry.d.ts +174 -0
  96. package/dist/installer/util/template.d.ts +23 -0
  97. package/docs/README.md +1 -0
  98. package/docs/agent-runtime.md +268 -0
  99. package/integration-contract.json +79 -7
  100. package/package.json +16 -1
  101. package/schemas/agent-runtime.schema.json +55 -0
  102. package/templates/runtime/provider-pipeline.md +16 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Paths the installer MUST NOT create, modify, or delete. These hold
3
+ * user / team state that survives re-runs (profile JSON authored by
4
+ * specrails-desktop, custom agents authored by the user). Breaking this
5
+ * contract silently destroys user work.
6
+ *
7
+ * Audited by vitest spec `reserved-paths.test.ts`.
8
+ */
9
+ export declare const RESERVED_PATHS: readonly [".specrails/profiles/", ".claude/agents/custom-", ".kimi-code/skills/custom-", ".kimi-code/skills/rails/custom-"];
10
+ /**
11
+ * Returns true when a repo-relative path falls inside a reserved
12
+ * region. Accepts both POSIX (/) and Windows (\) separators; the
13
+ * check normalises internally so callers can pass a value straight
14
+ * from `path.relative()` without worrying about host platform.
15
+ */
16
+ export declare function isReservedPath(relPath: string): boolean;
17
+ /**
18
+ * Joins segments and forces POSIX separators. Use when a generated
19
+ * string is destined for a cross-platform artefact (JSON manifest,
20
+ * YAML config) rather than the local filesystem.
21
+ */
22
+ export declare function toPosix(...segments: string[]): string;
23
+ /**
24
+ * Joins segments using the host platform's separator. Use when the
25
+ * result goes directly to `fs.*` / `child_process.*` calls.
26
+ */
27
+ export declare function toNative(...segments: string[]): string;
28
+ /**
29
+ * Repository-relative representation of an absolute path. Returns a
30
+ * POSIX-style path so callers can store it in manifests without OS
31
+ * contamination. If `absPath` is outside `repoRoot`, returns the
32
+ * original absolute path unchanged (POSIX-normalised).
33
+ */
34
+ export declare function repoRelative(repoRoot: string, absPath: string): string;
@@ -0,0 +1,23 @@
1
+ export interface TextOptions {
2
+ message: string;
3
+ /** Value used when stdin is non-TTY. Absence here in non-TTY mode throws. */
4
+ default?: string;
5
+ /** Returns error message when invalid, or `true` / undefined when valid. */
6
+ validate?: (value: string) => string | true | undefined;
7
+ }
8
+ export declare function text(opts: TextOptions): Promise<string>;
9
+ export interface ConfirmOptions {
10
+ message: string;
11
+ default?: boolean;
12
+ }
13
+ export declare function confirmYesNo(opts: ConfirmOptions): Promise<boolean>;
14
+ export interface SelectOptions<T> {
15
+ message: string;
16
+ choices: Array<{
17
+ name: string;
18
+ value: T;
19
+ description?: string;
20
+ }>;
21
+ default?: T;
22
+ }
23
+ export declare function chooseOne<T>(opts: SelectOptions<T>): Promise<T>;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Shared artifact registry — the contract that lets specrails-core place,
3
+ * and specrails-desktop read, a repo's relocated artifacts OUTSIDE the repo,
4
+ * under `$HOME/.specrails/projects/<slug>/workspace`.
5
+ *
6
+ * Single source of truth: `$HOME/.specrails/registry.json`, an inspectable,
7
+ * schema-versioned JSON file keyed by the **canonical realpath of the repo**.
8
+ * specrails-desktop is the primary writer (a projection of its `desktop.sqlite`);
9
+ * specrails-core reads it and, when run standalone, allocates its own entry.
10
+ *
11
+ * Design contract: `docs/internals/global-artifacts-alignment-contract.md`
12
+ * (in the specrails-desktop repo). The slug algorithm, canonical-key rule,
13
+ * atomic-write rule and lock protocol here MUST stay byte-identical to the
14
+ * desktop-side implementation — the correctness of the cross-tool contract
15
+ * depends on both tools resolving the same repo to the same paths.
16
+ *
17
+ * Everything in this module is synchronous to fit the installer's sync flow.
18
+ */
19
+ /** Registry schema version. A reader that sees a higher value MUST treat all
20
+ * entries as absent (legacy fallback), never mis-parse. */
21
+ export declare const REGISTRY_SCHEMA_VERSION = 1;
22
+ /** Who owns/allocated an entry. Encodes the single-writer-at-a-time rule. */
23
+ export type RegistrySource = 'desktop' | 'core-standalone';
24
+ /** One repo's relocated-artifact locations. All paths are absolute and in the
25
+ * host platform's native separator; consumers treat them as opaque. */
26
+ export interface ProjectEntry {
27
+ /** The canonical realpath of the repo (mirror of the map key). */
28
+ repoPath: string;
29
+ /** Shared slug — MUST equal desktop.sqlite `projects.slug` for the same repo. */
30
+ slug: string;
31
+ /** `$HOME/.specrails/projects/<slug>/workspace` — root of all relocated artifacts. */
32
+ workspaceDir: string;
33
+ /** The dir core treats as its `.specrails`/install root instead of repoRoot (= workspaceDir). */
34
+ artifactRoot: string;
35
+ /** Always the repo (= repoPath); carries the `openspec/**` + worktree carve-outs. */
36
+ codeRoot: string;
37
+ /** Runtime-state base (agent-memory, pipeline-state, …). Injected as SPECRAILS_STATE_DIR. */
38
+ stateDir: string;
39
+ /** Absolute path to the relocated local-tickets.json. Injected as SPECRAILS_TICKETS_PATH. */
40
+ ticketsPath: string;
41
+ /** Absolute path to backlog-config.json (Jira read-only switch). Injected as SPECRAILS_BACKLOG_CONFIG_PATH. */
42
+ backlogConfigPath: string;
43
+ /** Relocated `.specrails/profiles/`. Injected as SPECRAILS_PROFILES_DIR. */
44
+ profilesDir: string;
45
+ /** Relocated `.specrails/plugins/` (desktop-only; present for inspectability). */
46
+ pluginsStateDir: string;
47
+ /** Relocated `.specrails/file-summaries/` (desktop-only). */
48
+ fileSummariesDir: string;
49
+ /** Installed providers; mirror of desktop.sqlite `projects.providers`. */
50
+ providers: string[];
51
+ /** providers[0]; mirror of desktop.sqlite `projects.provider`. */
52
+ primaryProvider: string;
53
+ /** The `specrails-version` pin (name/format frozen; only location moved). */
54
+ coreVersion?: string;
55
+ createdAt?: string;
56
+ lastInstallAt?: string;
57
+ /** Single-owner-at-a-time marker; governs reconciliation. */
58
+ source: RegistrySource;
59
+ /** desktop.sqlite projects.id when source='desktop', for robust repo-move re-link. */
60
+ desktopProjectId?: string;
61
+ }
62
+ /** On-disk shape of `registry.json`. */
63
+ export interface RegistryFile {
64
+ schemaVersion: number;
65
+ generator?: string;
66
+ updatedAt?: string;
67
+ /** Map: canonical-repo-key -> ProjectEntry. */
68
+ projects: Record<string, ProjectEntry>;
69
+ }
70
+ /** The flattened result both tools consume. */
71
+ export interface Resolution {
72
+ /** Normalized map key. */
73
+ key: string;
74
+ repoPath: string;
75
+ slug: string;
76
+ workspaceDir: string;
77
+ artifactRoot: string;
78
+ codeRoot: string;
79
+ stateDir: string;
80
+ ticketsPath: string;
81
+ backlogConfigPath: string;
82
+ profilesDir: string;
83
+ pluginsStateDir: string;
84
+ fileSummariesDir: string;
85
+ providers: string[];
86
+ primaryProvider: string;
87
+ source: RegistrySource | 'legacy';
88
+ /** True when there is no registry entry and we fell back to the in-repo layout. */
89
+ isLegacy: boolean;
90
+ }
91
+ export interface ResolveOptions {
92
+ /** When true, allocate + persist an entry if none exists. Readers pass false. */
93
+ allocate?: boolean;
94
+ /** Who is allocating (only consulted when allocate=true). Default 'core-standalone'. */
95
+ allocator?: RegistrySource;
96
+ /** Override `$HOME` (tests). Default `os.homedir()`. */
97
+ home?: string;
98
+ /** Providers to record on allocation. Default ['claude']. */
99
+ providers?: string[];
100
+ coreVersion?: string;
101
+ desktopProjectId?: string;
102
+ /** Override timestamp (tests). */
103
+ now?: string;
104
+ }
105
+ /**
106
+ * Slug derivation — byte-identical to specrails-desktop's `slugify`
107
+ * (`server/desktop-router.ts`). Do not "improve" it; parity is the contract.
108
+ */
109
+ export declare function slugify(name: string): string;
110
+ /** `$HOME` for the registry, overridable for tests. */
111
+ export declare function resolveHome(home?: string): string;
112
+ /** Absolute path to `registry.json`. */
113
+ export declare function registryPath(home?: string): string;
114
+ /**
115
+ * Absolute path to the versioned framework store
116
+ * (`<home>/.specrails/framework`). The bundled-framework split materializes the
117
+ * provider-invariant subtree to `<frameworkDir>/<version>/<providerDir>/` once
118
+ * and every workspace symlinks `<frameworkDir>/current/<providerDir>/...`. Shares
119
+ * the SAME home as the registry so framework, registry, and workspaces co-locate
120
+ * (and tests pinning `SPECRAILS_REGISTRY_HOME` redirect all three together).
121
+ */
122
+ export declare function frameworkRoot(home?: string): string;
123
+ /** Absolute path to the advisory lock file. */
124
+ export declare function lockPath(home?: string): string;
125
+ /** `fs.realpathSync` that falls back to the resolved-but-unreal path on error
126
+ * (the path may not exist yet, or be on a volume that rejects realpath). */
127
+ export declare function realpathSafe(abs: string): string;
128
+ /** Case-fold the key on case-insensitive platforms (macOS, Windows) so two
129
+ * spellings of the same path map to one entry. The stored `repoPath` keeps
130
+ * its canonical case; only the index key is folded. */
131
+ export declare function normalizeKey(canon: string): string;
132
+ /** Canonical repo path: resolve to absolute, then realpath (collapses symlinks). */
133
+ export declare function canonicalizeRepoPath(repoPathInput: string): string;
134
+ /** The per-project sub-path layout under a workspace dir. Single source of the
135
+ * layout so writer and (allocation) reader never disagree. */
136
+ export declare function workspaceLayout(home: string, slug: string, canon: string, primaryProvider?: string): Omit<ProjectEntry, 'providers' | 'primaryProvider' | 'coreVersion' | 'createdAt' | 'lastInstallAt' | 'source' | 'desktopProjectId'>;
137
+ /** The in-repo layout, used when there is no registry entry (legacy fallback). */
138
+ export declare function legacyResolution(canon: string): Resolution;
139
+ /** Flatten a stored entry into a Resolution. */
140
+ export declare function entryToResolution(key: string, entry: ProjectEntry): Resolution;
141
+ /**
142
+ * Total, fail-open read. A missing file, a parse error, or a `schemaVersion`
143
+ * greater than we understand all yield an empty registry, so a caller treats
144
+ * it as "no entry" and (if allocating) writes a fresh, understood entry rather
145
+ * than crashing. Biases toward availability over strict consistency — correct
146
+ * for a local, inspectable file.
147
+ */
148
+ export declare function readRegistryOrEmpty(home?: string): RegistryFile;
149
+ /** Write `data` to `filePath` atomically: temp file in the same dir, fsync,
150
+ * rename. A reader (even without the lock) only ever sees a complete old or
151
+ * new file — the lock serialises writers, the rename protects readers. */
152
+ export declare function atomicWrite(filePath: string, data: string): void;
153
+ /**
154
+ * Run `fn` while holding an advisory lock over the registry. Mutual exclusion
155
+ * is an exclusive-create lock file (`open(..., 'wx')`) with bounded spin-retry
156
+ * and stale-lock breaking (a lock whose mtime exceeds the TTL is reclaimed —
157
+ * covers a crashed writer). Read-only callers never take the lock.
158
+ */
159
+ export declare function withFileLock<T>(home: string | undefined, fn: () => T): T;
160
+ /** Deterministic slug allocation: basename-derived, `-N` dedup suffix. Both
161
+ * tools implement this identically so they only ever diverge on the suffix,
162
+ * which read-before-allocate resolves. */
163
+ export declare function allocateSlug(canon: string, existing: ReadonlySet<string>): string;
164
+ /**
165
+ * The shared resolver both tools run. Given a repo path, returns where that
166
+ * repo's artifacts live. With `allocate:true`, creates + persists an entry when
167
+ * none exists (under the lock, with a re-read double-check); readers pass
168
+ * `allocate:false` and fall back to the in-repo legacy layout when absent.
169
+ */
170
+ export declare function resolveArtifacts(repoPathInput: string, opts?: ResolveOptions): Resolution;
171
+ /** Commit version metadata only after successful materialization and assembly.
172
+ * Desktop-owned entries remain the host's responsibility; never claim ownership.
173
+ */
174
+ export declare function recordSuccessfulInstall(repoPath: string, opts: Pick<ResolveOptions, 'home' | 'providers' | 'coreVersion' | 'now'>): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Small in-house template renderer. Replaces the bash heredocs the
3
+ * retired install.sh / update.sh used to emit manifests and config
4
+ * files.
5
+ *
6
+ * Syntax (intentionally minimal):
7
+ * ${VAR_NAME} — value interpolation
8
+ * {{#if FLAG}}block{{/if}} — conditional block, rendered when
9
+ * FLAG is truthy in the context
10
+ * {{#ifnot FLAG}}block{{/ifnot}} — conditional block when FLAG is falsy
11
+ *
12
+ * Nested blocks are NOT supported. If a template outgrows this
13
+ * capability we switch to mustache — but every heredoc we've audited
14
+ * fits inside these three forms.
15
+ */
16
+ export type TemplateContext = Record<string, string | boolean | number | null | undefined>;
17
+ /**
18
+ * Renders `template` by evaluating its directives against `context`.
19
+ * Missing interpolation variables render as the empty string rather
20
+ * than throwing — the bash scripts behaved the same way (`${X:-}` in
21
+ * a heredoc). Unknown flags in an `{{#if}}` are treated as falsy.
22
+ */
23
+ export declare function render(template: string, context: TemplateContext): string;
package/docs/README.md CHANGED
@@ -18,6 +18,7 @@ Welcome to the SpecRails docs. This guide will take you from zero to a fully aut
18
18
  | [Plugin Architecture](plugin-architecture.md) | Plugin vs scaffold, what lives where, how updates work |
19
19
  | [Agents](agents.md) | Every agent explained — role, when it runs, why it exists |
20
20
  | [Workflows & Commands](workflows.md) | How to use `/specrails:implement`, `/specrails:get-backlog-specs`, and more |
21
+ | [Programmatic Agent Runtime](agent-runtime.md) | LangGraph phases, Claude/Codex/Gemini/Kimi and local providers, CLI/API configuration, recovery and host delivery |
21
22
  | [Customization](customization.md) | Adapt agents, rules, personas, and conventions to your project |
22
23
  | [Updating](updating.md) | Keep SpecRails up to date without losing your customizations |
23
24
 
@@ -0,0 +1,268 @@
1
+ # Programmatic agent runtime
2
+
3
+ Core can execute implementation as a local TypeScript workflow:
4
+
5
+ ```text
6
+ architect → developer → verify → reviewer → archive
7
+ │ ↑ │ │
8
+ │ └──────────┴─────────┘ bounded corrections
9
+ └─ investigate once, then ask (or proceed on stated assumptions)
10
+ ```
11
+
12
+ The workflow is a [LangGraph](https://docs.langchain.com/oss/javascript/langgraph/overview) state graph. LangGraph owns the traversal, the typed state and its reducers, the checkpoint history, dynamic interrupts (approvals and questions) and time travel; Core owns the role instructions, the structured artifacts, the verification subprocesses, the acceptance evidence, the receipts, budgets, leases and interrupted-write recovery. Provider adapters execute one role; they do not invoke a platform's implementation prompt or skill.
13
+
14
+ The developer role has the same autonomy the legacy Implement step had: it edits files and runs commands inside its CLI's own sandbox (Claude `--tools default --dangerously-skip-permissions` with nested agents and skills disallowed, Codex `workspace-write`, Gemini `--yolo`, Kimi print mode), so it can run the project's tests before handing off. Architect and reviewer roles stay read-only. Claude roles load only project settings (`--setting-sources project,local`), so `CLAUDE.md` and `.claude/rules` apply while the user's global memory and plugins do not.
15
+
16
+ This page describes the current source implementation, **runtime API 1**, workflow version 3, checkpoint envelope format 2. An older published Core package can have the same major version and lack this export. Build the paired checkout when developing this feature; do not assume `@latest` contains unreleased changes.
17
+
18
+ ## Requirements and ownership
19
+
20
+ - Node.js **20.19.0+** and Git on macOS or Windows. Individual provider CLIs may require a newer Node release.
21
+ - Installed and authenticated Claude, Codex, Gemini or Kimi CLIs for whichever roles use them; alternatively, a reachable OpenAI-compatible endpoint with tool support.
22
+ - A frozen execution context and a new change name. Verification commands are optional: configured commands run as given, the architect proposes the project's own checks for repositories that have none, and a repository with no automated check is admitted and recorded as unverified in the receipt.
23
+ - `ownership.git: "host"`. The runtime implements and archives; its caller owns worktrees, commits, pushes, pull requests and backlog delivery. It rejects Core-owned Git delivery rather than reporting success while shipping remains pending.
24
+
25
+ Specrails, LangGraph and the runtime infrastructure are open source and require no paid orchestration service, tracing backend, Python daemon or database server. Model inference, hardware and existing provider CLIs retain their own costs and license terms. No model download or provider subscription is included.
26
+
27
+ ## Configure providers and roles
28
+
29
+ Save this as `.specrails/agent-runtime.json`, adapting the verification command to your repository:
30
+
31
+ ```json
32
+ {
33
+ "schemaVersion": 1,
34
+ "enabled": true,
35
+ "providers": [
36
+ { "id": "claude", "kind": "cli", "cli": "claude" },
37
+ { "id": "codex", "kind": "cli", "cli": "codex" },
38
+ { "id": "gemini", "kind": "cli", "cli": "gemini" },
39
+ { "id": "kimi", "kind": "cli", "cli": "kimi" },
40
+ {
41
+ "id": "local",
42
+ "kind": "openai-compatible",
43
+ "baseUrl": "http://127.0.0.1:11434/v1"
44
+ }
45
+ ],
46
+ "agents": {
47
+ "architect": { "provider": "claude", "maxTurns": 24 },
48
+ "developer": { "provider": "codex", "maxTurns": 24 },
49
+ "reviewer": { "provider": "gemini", "maxTurns": 24 }
50
+ },
51
+ "limits": { "maxAttempts": 3, "timeoutMs": 900000 },
52
+ "verification": [
53
+ { "repositoryId": "app", "command": "node", "args": ["--test"] }
54
+ ],
55
+ "review": { "minScore": 80, "aspects": { "security": 85 } },
56
+ "architect": { "onLowConfidence": "ask" },
57
+ "approvalBeforeArchive": false
58
+ }
59
+ ```
60
+
61
+ `verification` may be an empty array. At run time Core builds the effective plan from the configured commands plus the architect's proposals for uncovered repositories; the plan is frozen in the graph state and reused by every verification and resume. When the plan covers no repository at all, the run still passes through task completion and review, and the receipt lists those repositories under `unverifiedRepositories`.
62
+
63
+ `review` tightens the review gate. Core's own gate is the floor: overall score at least 70, `security` at least 75, every other aspect at least 60. A lower value is rejected by validation, because the pipeline journal enforces the floor regardless of configuration. The reviewer is told the effective thresholds.
64
+
65
+ `architect.onLowConfidence` decides what happens when the architect still reports low confidence after its investigation pass (see [Questions and approvals](#questions-and-approvals)): `ask` (default) pauses the run with the architect's question; `proceed` continues on the assumptions the architect stated, recording `design-confidence.json` as `medium` with `assumed: true` and the original `reportedConfidence`.
66
+
67
+ Set any role's `provider` to `kimi` to use Kimi. A CLI role's optional `model` is passed to that provider; omitting it uses the CLI's default. Provider IDs are aliases, so you can configure several endpoints or replace an executor without changing workflow code.
68
+
69
+ To use the local endpoint, set the desired role to `{"provider":"local","model":"your-installed-model-id","maxTurns":24}`. The endpoint must support `POST <baseUrl>/chat/completions`, OpenAI-style function tool calls and a final assistant result. Its model must actually be able to use these tools; a chat-only endpoint is insufficient for the developer role. The example port is a placeholder for your own running server.
70
+
71
+ For an authenticated endpoint, add `"apiKeyEnv":"MY_MODEL_API_KEY"` to its provider configuration and set that variable in the process launching Core. Omit `apiKeyEnv` when no key is required. URLs cannot contain credentials, query parameters or fragments. Verification commands inherit process credentials; do not put secrets into their persisted `env` overrides.
72
+
73
+ The configuration schema is [agent-runtime.schema.json](../schemas/agent-runtime.schema.json). `validateRuntimeConfig()` also checks relationships such as role-to-provider references and the review floors. Runtime configuration is separate from the existing [profile v1 schema](../schemas/profile.v1.json); it does not translate legacy profile routing into programmatic phases.
74
+
75
+ ## Run from the CLI
76
+
77
+ Use an installed Core package that exposes runtime API 1. For a source checkout, run `npm ci` and `npm run build` in Core, then replace `specrails-core` below with `node /path/to/specrails-core/bin/specrails-core.mjs`.
78
+
79
+ Desktop constructs the execution context automatically. For standalone use, create a context JSON with absolute paths:
80
+
81
+ ```json
82
+ {
83
+ "schemaVersion": 1,
84
+ "runId": "feature-navigation-01",
85
+ "backlogRoot": "/absolute/path/to/app",
86
+ "artifactRoot": "/absolute/path/to/app",
87
+ "artifactRepositoryId": "app",
88
+ "repositories": [
89
+ { "id": "app", "name": "App", "path": "/absolute/path/to/app" }
90
+ ],
91
+ "ownership": { "git": "host", "backlog": "host", "worktrees": "host" },
92
+ "specs": [
93
+ {
94
+ "id": "navigation",
95
+ "title": "Keyboard navigation",
96
+ "description": "Implement the agreed keyboard navigation behavior.",
97
+ "repositoryIds": ["app"],
98
+ "acceptanceCriteria": ["Every setting can be reached using the keyboard."]
99
+ }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ On Windows use absolute paths such as `C:/work/app` or JSON-escaped `C:\\work\\app`. Repository paths must identify existing Git repositories. Multi-repository contexts list every selected repository; `artifactRoot` must be the path of `artifactRepositoryId`. Choose a fresh, portable run ID and a new kebab-case change name. Every acceptance criterion (or the description when a spec has none) becomes a frozen requirement the reviewer must certify.
105
+
106
+ ```sh
107
+ specrails-core runtime api
108
+ specrails-core runtime validate --config .specrails/agent-runtime.json
109
+ specrails-core runtime run --context .specrails/context.json --config .specrails/agent-runtime.json --change keyboard-navigation
110
+ specrails-core runtime status --context .specrails/context.json
111
+ specrails-core runtime resume --context .specrails/context.json --answer "Keep the existing shortcut map"
112
+ specrails-core runtime resume --context .specrails/context.json --approve archive
113
+ ```
114
+
115
+ The commands work in macOS shells and PowerShell; quote paths and answers containing spaces. Verification uses `command` plus an `args` array, not a shell command string. Prefer portable Node/npm commands over Bash scripts when the project supports Windows.
116
+
117
+ Run and resume emit JSON lines: `workflow-event` (the durable ledger events), `agent-event` (role narration and tool activity), `verification-output`, `span` (one per finished role attempt, with `traceId`, `spanId`, timing, status and usage, ready for an OpenTelemetry bridge) and a final `runtime-result`. The direct runtime entry point is `dist/agent-runtime/cli.js`; it also emits JSON errors. The main package CLI can report command-validation errors on stderr. Exit codes are `0` for success, `2` for a pause (approval or question pending), and `1` for failure, blocking or cancellation. A successful Core result means implementation, verification, review, acceptance evidence and archive completed; host delivery remains separate.
118
+
119
+ `runtime api` returns `{type:"runtime-api",apiVersion:1,coreVersion:"..."}` without invoking providers. Hosts can send a JSON configuration through stdin to `runtime validate --stdin` (maximum 2 MiB), avoiding temporary files and platform-specific shell quoting. It is mutually exclusive with `--config`. Use `runtime status --context <file> --compact` for process/UI integration: it retains the run and trace identities, phase status and visits, `pendingApproval`, `pendingQuestion`, usage, the completion verdict and the acceptance summary while omitting accumulated outputs, history and frozen context. Omit `--compact` for full inspection.
120
+
121
+ ## Questions and approvals
122
+
123
+ The graph pauses through LangGraph interrupts; the host resumes it with the matching answer.
124
+
125
+ - **Low design confidence.** When the architect reports `low`, Core first asks the same architect session to investigate the code once more and decide from evidence. If confidence is still low, the run pauses with the architect's single blocking question (`pendingQuestion`), the draft proposal is left on disk for inspection, and `resume --answer <text>` re-runs the architect with the answer as authoritative input. With `architect.onLowConfidence: "proceed"` the run continues on the stated assumptions instead of pausing.
126
+ - **Archive approval.** With `approvalBeforeArchive: true` the run pauses before archive (`pendingApproval`); `resume --approve archive` grants it. The default is no approval, so a fully autonomous run implements, verifies, reviews and archives without a human in the loop.
127
+
128
+ A resumed node collects its answer before doing any work, so the pass that asked the question is never repeated. Approvals and answers are persisted; a granted approval survives interruption, but invalidated candidate evidence clears it.
129
+
130
+ ## Recovery and durable state
131
+
132
+ State lives below `<backlogRoot>/.specrails/pipeline/<runId>/`:
133
+
134
+ | File | Purpose |
135
+ | --- | --- |
136
+ | `state.json` and `receipts/` | Authoritative Core gates, verification and acceptance evidence |
137
+ | `agent-runtime-request.json` | Frozen CLI change name and runtime configuration |
138
+ | `agent-workflow/<runId>/checkpoint.json` | One atomic envelope: the host ledger (attempts, receipts, usage, ordered events, pending interrupts) and the complete LangGraph checkpoint history |
139
+ | `agent-workflow/<runId>/.lease/` | Exclusive runtime process ownership |
140
+
141
+ Resume uses the saved configuration and change. It rejects a different frozen input, Core/instruction identity or workflow definition. Valid completed phases are retained; stale evidence invalidates the affected phase and everything declared after it, and the graph travels back in time to the checkpoint taken right before that phase last ran, so its predecessors' state is exactly what it saw then. The ledger is authoritative for which node runs next: if LangGraph's own position disagrees after a crash, traversal follows the ledger. Once archived, changed evidence requires a new run.
142
+
143
+ Correction loops stay cheap: when verification or review sends work back, the developer's previous provider session is resumed with a short correction prompt (Claude `--resume`, Codex `exec resume`, Gemini `--resume`, Kimi `--session`), so the code it wrote and the reasons behind it are already in context. If the session is gone the developer starts a fresh full turn with the same feedback. Unchecked tasks in `tasks.md` are returned to the developer as feedback, not treated as a workflow failure. A run blocked at `limits.maxAttempts` can be resumed explicitly: the resume grants a fresh attempt budget and transition ceiling; visits and history keep the complete record.
144
+
145
+ Architect and reviewer replies are validated against a JSON Schema (Claude `--json-schema`, Codex `--output-schema`; other providers are parsed leniently, accepting fenced or prefixed objects). The developer finishes with a structured summary (files, tests, verification run, incomplete tasks) validated the same way; a provider that returns prose instead is recorded as such rather than repaired. An unusable architect or reviewer reply gets one repair turn inside the same session before the phase fails.
146
+
147
+ ```sh
148
+ # Retry a reported failure with the same frozen configuration.
149
+ specrails-core runtime resume --context .specrails/context.json
150
+
151
+ # After inspecting partial edits, explicitly allow an interrupted write to run again.
152
+ specrails-core runtime resume --context .specrails/context.json --recover developer
153
+
154
+ # Recheck evidence explicitly; this also invalidates downstream review/archive.
155
+ specrails-core runtime resume --context .specrails/context.json --invalidate verify
156
+ ```
157
+
158
+ Inspect the saved phase and worktree before using `--recover`; it authorizes repeating an effect whose completion was not durably recorded. Dead local process leases can be reclaimed. Live, remote or unverifiable leases are never silently stolen. Do not delete a lease while its process may still be running.
159
+
160
+ Cancellation propagates to owned provider and verification processes. Programmatic callbacks must cooperate with `AbortSignal` and finish subprocess cleanup before returning; the engine retains its lease until they settle. A write step that settles after cancellation is recorded as interrupted and requires explicit recovery.
161
+
162
+ ## Acceptance evidence
163
+
164
+ Core 5.2 requires acceptance evidence before a change can be archived. The runtime produces it without a separate role:
165
+
166
+ - The reviewer receives every frozen acceptance criterion with stable coordinates (`specId`, `criterionIndex`) and certifies each one as `met`, `exception`, `blocked` or `pending` with concrete evidence. A reviewer may only accept a non-material exception itself; material scope changes stay `blocked`.
167
+ - Core records the verification commands it actually ran as required checks (passed or failed by exit code, with the receipt id as evidence), repositories admitted without a check as unavailable checks, and the reviewer's own inspections as supplementary, never required, checks.
168
+ - The report is validated against the frozen scope inside the reviewer turn (a malformed report gets the repair turn), then bound to the exact candidate before the reviewer verdict is recorded. Unresolved requirements or a failed required check block review and archive; the CLI status exposes the reasons.
169
+
170
+ ## Budgets and provider capability differences
171
+
172
+ | Setting | Behavior |
173
+ | --- | --- |
174
+ | `agents.<role>.maxTurns` | Bounded role/tool interaction, default 24; provider transports enforce their available turn/tool events |
175
+ | `limits.maxAttempts` | Maximum development visits per invocation, including correction cycles, default 3; an explicit resume starts a fresh budget |
176
+ | `limits.timeoutMs` | Workflow duration limit and provider timeout; default provider timeout is 15 minutes when omitted |
177
+ | `limits.maxTokens` | Rejects missing required usage or an observed overrun; CLI accounting may arrive only after a call |
178
+ | `limits.maxCostUsd` | Accepted by the built-in Claude executor through its native dollar limit; rejected by built-in Codex, Gemini, Kimi and OpenAI-compatible executors |
179
+
180
+ Do not configure `maxCostUsd` for a mixed-provider run unless every selected custom executor can enforce the requested cap. An observed token limit cannot guarantee that an opaque CLI stops before spending those tokens. Provider spend is accounted the moment each call returns, so a pause or failure after a call never loses it. Unknown usage stays `null`; known spend is tracked as a lower bound, never fabricated as zero. Local endpoints can report zero cost, but absent billing data remains unknown.
181
+
182
+ Kimi read-only roles use an enforced custom agent when the CLI exposes `--agent-file`; the Kimi 0.27 fallback uses ACP plan mode with scoped reads and denied writes/terminal operations. Unsupported ACP modes or multi-repository capabilities fail explicitly. Kimi lacks authoritative token accounting, so its built-in executor rejects token and dollar caps. Configure another provider for a role when its installed Kimi version cannot expose the required repository scope.
183
+
184
+ Gemini architect/reviewer roles require a CLI that advertises `--admin-policy` (verified with Gemini 0.49). Core supplies a temporary admin policy allowing only file reads, listing, glob and grep; all other tools are denied, including shell, writes and mode changes. This policy applies even if user settings disable plan mode or previously approve write tools. Gemini ignores per-run admin policies when system policy files exist, so Core rejects those managed environments before invoking a read-only role; select another provider for these roles or arrange a compatible environment with the system administrator. Older Gemini versions without this policy capability also fail explicitly. Gemini developer roles retain their existing `auto_edit` execution.
185
+
186
+ ## Embed and extend
187
+
188
+ Import the public ESM entry point; TypeScript declarations are included:
189
+
190
+ ```ts
191
+ import { readFileSync } from 'node:fs'
192
+ import { runCoreWorkflow, validateRuntimeConfig } from 'specrails-core/agent-runtime'
193
+
194
+ const context = JSON.parse(readFileSync('.specrails/context.json', 'utf8'))
195
+ const config = validateRuntimeConfig(JSON.parse(readFileSync('.specrails/agent-runtime.json', 'utf8')))
196
+ const controller = new AbortController()
197
+ const state = await runCoreWorkflow({
198
+ context, config, change: 'keyboard-navigation', signal: controller.signal,
199
+ onEvent: event => process.stdout.write(JSON.stringify(event) + '\n'),
200
+ onSpan: span => process.stdout.write(JSON.stringify(span) + '\n'),
201
+ })
202
+ console.log(state.status, state.nextStep, state.pendingQuestion?.question)
203
+ ```
204
+
205
+ Programmatic hosts retain their input/config and supply the same values with `resume: true`, plus `approve`, `answer`, `recoverInterrupted` or `invalidate` as needed. The CLI additionally creates the frozen request file for you.
206
+
207
+ Register an executor directly to add a provider in code. This example registers a private local endpoint with no CLI installation:
208
+
209
+ ```ts
210
+ import {
211
+ ExecutorRegistry, OpenAICompatibleExecutor, runCoreWorkflow,
212
+ type RuntimeConfig,
213
+ } from 'specrails-core/agent-runtime'
214
+
215
+ const registry = new ExecutorRegistry().register('on-prem',
216
+ new OpenAICompatibleExecutor({
217
+ id: 'on-prem', kind: 'openai-compatible', baseUrl: 'http://127.0.0.1:8080/v1',
218
+ }),
219
+ )
220
+ const config: RuntimeConfig = {
221
+ schemaVersion: 1, enabled: true, providers: [],
222
+ agents: {
223
+ architect: { provider: 'on-prem', model: 'your-installed-model-id' },
224
+ developer: { provider: 'on-prem', model: 'your-installed-model-id' },
225
+ reviewer: { provider: 'on-prem', model: 'your-installed-model-id' },
226
+ },
227
+ verification: [{ repositoryId: 'app', command: 'node', args: ['--test'] }],
228
+ }
229
+ // context is the frozen JSON object from the preceding example.
230
+ await runCoreWorkflow({ context, config, registry, change: 'local-navigation' })
231
+ ```
232
+
233
+ A custom executor implements `AgentExecutor.execute(request): Promise<AgentResult>`. The request contains role, central instructions, allowed roots, model, limits and signal. Return final text and honest `{inputTokens, outputTokens, costUsd}` usage; use `null` for unavailable values. The executor must enforce role permissions and supported limits, own its tool/process cleanup, and throw on incomplete results. `createExecutorRegistry(config, {executors: {alias: executor}})` can replace or supplement configured providers.
234
+
235
+ An optional, side-effect-free `validateLimits({maxTokens, maxCostUsd})` method rejects unsupported limits before any role runs. Built-in executors provide this preflight; custom registrations own their capabilities. Core also validates the complete verification command plan against the frozen repository scope before creating a run or invoking providers.
236
+
237
+ For a different host workflow, use `runWorkflow()` with a LangGraph state schema and typed nodes:
238
+
239
+ ```ts
240
+ import { Annotation } from '@langchain/langgraph'
241
+ import { runWorkflow } from 'specrails-core/agent-runtime'
242
+
243
+ const State = Annotation.Root({ notes: Annotation<string[]>({ reducer: (a, b) => [...a, ...b], default: () => [] }) })
244
+ await runWorkflow<typeof State.State>({
245
+ directory, runId, input: { change }, workflow: {
246
+ id: 'my-host', version: '1', schema: State, entry: 'plan',
247
+ nodes: {
248
+ plan: { ends: ['build'], run: async () => ({ status: 'succeeded', update: { notes: ['planned'] } }) },
249
+ build: { effect: 'write', ends: ['plan'], run: async (state, context) => {
250
+ context.reportUsage({ costUsd: 0.1, inputTokens: 10, outputTokens: 5 })
251
+ if (state.notes.length < 2) return { status: 'succeeded', next: 'plan' }
252
+ context.interrupt({ kind: 'approval', reason: 'Ship it?' })
253
+ return { status: 'succeeded', next: null }
254
+ } },
255
+ },
256
+ },
257
+ })
258
+ ```
259
+
260
+ Each node declares its `effect`, optional retry bounds, its possible successors (`ends`) and `run(state, context)`. A result supplies `status`, an optional graph `update` (merged through the schema's reducers), an optional ledger `output` and an optional `next` for conditional routing; `null` completes the workflow. Node names must not collide with state channel names. Nodes pause with `context.interrupt()` and report provider spend with `context.reportUsage()` as it happens. Write retries require `retrySafe: true`; interrupted writes still require explicit recovery. `readWorkflowState(directory, runId)` is read-only. Observer exceptions cannot replay a committed step. The Core nodes themselves are exported as `coreNodes(deps)` for hosts that want to reuse a phase inside another graph.
261
+
262
+ ## Tools, specifications and compatibility
263
+
264
+ The OpenAI-compatible tool executor exposes scoped file listing/reading and developer-only writing. It rejects traversal, symlink escapes and protected runtime metadata. It has no model-accessible shell tool; only the host's configured verification commands run as subprocesses. This is a tool policy, not an operating-system sandbox for arbitrary custom executors or external CLIs.
265
+
266
+ Architect and reviewer responses are structured JSON. The architect's `specs[].content` is the **complete intended main specification**, including unchanged requirements. Archive replaces `openspec/specs/<name>/spec.md` with that reviewed document. It does not interpret a partial OpenSpec delta as a merge instruction. The developer may update task checkboxes, but changing approved design, task descriptions or specification content invalidates the gates.
267
+
268
+ Legacy provider workflows and profile v1 remain available. Missing/disabled runtime configuration keeps Desktop's legacy path; an admitted programmatic run remains bound to its frozen runtime request even if the project's settings are later disabled. See the [integration contract](../integration-contract.json) for runtime API and artifact paths, and Desktop's programmatic runtime guide for release pairing and continuation/delivery behavior.