specrails-core 6.4.0 → 6.6.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 (131) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +2 -2
  2. package/dist/agent-runtime/capabilities.js +22 -5
  3. package/dist/agent-runtime/cli-executor.js +8 -3
  4. package/dist/agent-runtime/cli.d.ts +4 -4
  5. package/dist/agent-runtime/cli.js +50 -3
  6. package/dist/agent-runtime/compact/environment.d.ts +50 -4
  7. package/dist/agent-runtime/compact/environment.js +183 -6
  8. package/dist/agent-runtime/compact/step.d.ts +1 -1
  9. package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
  10. package/dist/agent-runtime/efficiency-types.d.ts +1 -1
  11. package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
  12. package/dist/agent-runtime/engine/contracts.d.ts +2 -1
  13. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  14. package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
  15. package/dist/agent-runtime/engine/fork.d.ts +1 -1
  16. package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
  17. package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
  18. package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
  19. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
  20. package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
  21. package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
  22. package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
  23. package/dist/agent-runtime/engine/pieces/verify.js +46 -13
  24. package/dist/agent-runtime/engine/run-status.d.ts +165 -165
  25. package/dist/agent-runtime/engine/runs.d.ts +168 -168
  26. package/dist/agent-runtime/engine/state.d.ts +16 -16
  27. package/dist/agent-runtime/evaluation.d.ts +3 -3
  28. package/dist/agent-runtime/executor-types.d.ts +10 -10
  29. package/dist/agent-runtime/graph/convergence.d.ts +2 -2
  30. package/dist/agent-runtime/graph/convergence.js +11 -0
  31. package/dist/agent-runtime/graph/nodes.js +35 -4
  32. package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
  33. package/dist/agent-runtime/graph/state.d.ts +6 -2
  34. package/dist/agent-runtime/guardrails.d.ts +1 -1
  35. package/dist/agent-runtime/guardrails.js +4 -1
  36. package/dist/agent-runtime/index.d.ts +3 -2
  37. package/dist/agent-runtime/index.js +1 -0
  38. package/dist/agent-runtime/openai-executor.d.ts +9 -9
  39. package/dist/agent-runtime/openspec.d.ts +4 -4
  40. package/dist/agent-runtime/prompts.d.ts +6 -0
  41. package/dist/agent-runtime/prompts.js +32 -8
  42. package/dist/agent-runtime/recovery.d.ts +12 -12
  43. package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
  44. package/dist/agent-runtime/session/application/active-session.js +701 -0
  45. package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
  46. package/dist/agent-runtime/session/application/delegation.js +171 -0
  47. package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
  48. package/dist/agent-runtime/session/application/output-buffer.js +65 -0
  49. package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
  50. package/dist/agent-runtime/session/application/session-service.js +256 -0
  51. package/dist/agent-runtime/session/application/timers.d.ts +15 -0
  52. package/dist/agent-runtime/session/application/timers.js +35 -0
  53. package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
  54. package/dist/agent-runtime/session/domain/descriptor.js +32 -0
  55. package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
  56. package/dist/agent-runtime/session/domain/errors.js +16 -0
  57. package/dist/agent-runtime/session/domain/events.d.ts +147 -0
  58. package/dist/agent-runtime/session/domain/events.js +24 -0
  59. package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
  60. package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
  61. package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
  62. package/dist/agent-runtime/session/domain/interruption.js +39 -0
  63. package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
  64. package/dist/agent-runtime/session/domain/policy.js +104 -0
  65. package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
  66. package/dist/agent-runtime/session/domain/snapshot.js +234 -0
  67. package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
  68. package/dist/agent-runtime/session/domain/state-machines.js +41 -0
  69. package/dist/agent-runtime/session/domain/types.d.ts +130 -0
  70. package/dist/agent-runtime/session/domain/types.js +15 -0
  71. package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
  72. package/dist/agent-runtime/session/domain/usage.js +86 -0
  73. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
  74. package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
  75. package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
  76. package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
  77. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
  78. package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
  79. package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
  80. package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
  81. package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
  82. package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
  83. package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
  84. package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
  85. package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
  86. package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
  87. package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
  88. package/dist/agent-runtime/session/drivers/common/process.js +116 -0
  89. package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
  90. package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
  91. package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
  92. package/dist/agent-runtime/session/drivers/registry.js +38 -0
  93. package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
  94. package/dist/agent-runtime/session/host/protocol.js +88 -0
  95. package/dist/agent-runtime/session/host/server.d.ts +57 -0
  96. package/dist/agent-runtime/session/host/server.js +229 -0
  97. package/dist/agent-runtime/session/index.d.ts +45 -0
  98. package/dist/agent-runtime/session/index.js +74 -0
  99. package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
  100. package/dist/agent-runtime/session/journal/schema.js +44 -0
  101. package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
  102. package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
  103. package/dist/agent-runtime/session/ports.d.ts +192 -0
  104. package/dist/agent-runtime/session/ports.js +1 -0
  105. package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
  106. package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
  107. package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
  108. package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
  109. package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
  110. package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
  111. package/dist/agent-runtime/session/testing/index.d.ts +9 -0
  112. package/dist/agent-runtime/session/testing/index.js +9 -0
  113. package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
  114. package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
  115. package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
  116. package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
  117. package/dist/agent-runtime/verification-proposals.d.ts +9 -0
  118. package/dist/agent-runtime/verification-proposals.js +69 -0
  119. package/dist/agent-runtime/verification-repair.d.ts +31 -2
  120. package/dist/agent-runtime/verification-repair.js +58 -3
  121. package/dist/installer/commands/init.d.ts +1 -1
  122. package/dist/installer/phases/install-config.js +10 -3
  123. package/dist/installer/util/paths.d.ts +25 -1
  124. package/dist/installer/util/registry.js +3 -3
  125. package/dist/pipeline/pipeline-state.d.ts +42 -11
  126. package/dist/pipeline/pipeline-state.js +47 -8
  127. package/dist/shared/specrails-home.d.ts +21 -0
  128. package/dist/shared/specrails-home.js +36 -0
  129. package/integration-contract.json +42 -1
  130. package/package.json +23 -11
  131. package/schemas/agent-runtime.schema.json +1 -1
@@ -106,7 +106,7 @@ export declare function buildOpenSpecInvocation(repoRoot: string, provider: Prov
106
106
  bin: string;
107
107
  args: string[];
108
108
  };
109
- export declare const KIMI_REQUIRED_OPENSPEC_SKILLS: readonly ["openspec-propose", "openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-apply-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard"];
109
+ export declare const KIMI_REQUIRED_OPENSPEC_SKILLS: readonly ['openspec-propose', 'openspec-explore', 'openspec-new-change', 'openspec-continue-change', 'openspec-apply-change', 'openspec-ff-change', 'openspec-sync-specs', 'openspec-archive-change', 'openspec-bulk-archive-change', 'openspec-verify-change', 'openspec-onboard'];
110
110
  /**
111
111
  * Move only OpenSpec-owned Kimi workflow directories from either upstream
112
112
  * location into the artifact workspace. Every destination is copied to a
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import yaml from 'js-yaml';
2
+ import { dump, loadAll } from 'js-yaml';
3
3
  import { FilesystemError, InstallerError } from '../util/errors.js';
4
4
  import { pathExists, readTextFile, writeFileLf } from '../util/fs.js';
5
5
  import { warn } from '../util/logger.js';
@@ -72,7 +72,14 @@ export function loadInstallConfig(configPath) {
72
72
  const raw = readTextFile(configPath);
73
73
  let parsed;
74
74
  try {
75
- parsed = yaml.load(raw);
75
+ // js-yaml 5 `load()` throws on a stream with no document (empty or
76
+ // comment-only file). Keep reporting that as a non-mapping config, and
77
+ // keep rejecting multi-document streams as `load()` did.
78
+ const documents = loadAll(raw);
79
+ if (documents.length > 1) {
80
+ throw new Error('expected a single document in the stream, but found more');
81
+ }
82
+ parsed = documents[0];
76
83
  }
77
84
  catch (err) {
78
85
  throw new InvalidConfigError([`YAML parse error: ${err.message}`]);
@@ -258,7 +265,7 @@ function isExactModelIdentifier(value) {
258
265
  */
259
266
  export function writeInstallConfig(configPath, config) {
260
267
  try {
261
- const text = yaml.dump(config, { lineWidth: 120, noRefs: true });
268
+ const text = dump(config, { lineWidth: 120, noRefs: true });
262
269
  writeFileLf(configPath, text);
263
270
  }
264
271
  catch (err) {
@@ -6,7 +6,31 @@
6
6
  *
7
7
  * Audited by vitest spec `reserved-paths.test.ts`.
8
8
  */
9
- export declare const RESERVED_PATHS: readonly [".specrails/profiles/", ".claude/agents/custom-", ".kimi-code/skills/custom-", ".kimi-code/skills/rails/custom-"];
9
+ export declare const RESERVED_PATHS: readonly [
10
+ /**
11
+ * .specrails/profiles/** — project + desktop-app-authored profile JSON.
12
+ * specrails-desktop writes profile files here and expects them preserved
13
+ * across specrails-core updates.
14
+ */
15
+ '.specrails/profiles/',
16
+ /**
17
+ * .claude/agents/custom-*.md — user-authored custom agents, kept
18
+ * distinct from the bundled sr-* agents so updates never overwrite
19
+ * them.
20
+ */
21
+ '.claude/agents/custom-',
22
+ /**
23
+ * `.kimi-code/skills/custom-<id>/...` — user-authored Kimi role skills.
24
+ * Kimi discovers skills only one directory below `skills/`, so custom roles
25
+ * share the flat namespace with managed `sr-*` and workflow skills.
26
+ */
27
+ '.kimi-code/skills/custom-',
28
+ /**
29
+ * Pre-release compatibility: preserve a legacy nested custom role until the
30
+ * installer can atomically migrate it to the flat path. A destination
31
+ * collision is never overwritten or deleted automatically.
32
+ */
33
+ '.kimi-code/skills/rails/custom-'];
10
34
  /**
11
35
  * Returns true when a repo-relative path falls inside a reserved
12
36
  * region. Accepts both POSIX (/) and Windows (\) separators; the
@@ -17,8 +17,8 @@
17
17
  * Everything in this module is synchronous to fit the installer's sync flow.
18
18
  */
19
19
  import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
20
- import os from 'node:os';
21
20
  import path from 'node:path';
21
+ import { userHome } from '../../shared/specrails-home.js';
22
22
  /** Registry schema version. A reader that sees a higher value MUST treat all
23
23
  * entries as absent (legacy fallback), never mis-parse. */
24
24
  export const REGISTRY_SCHEMA_VERSION = 1;
@@ -190,9 +190,9 @@ function isValidProjectEntry(key, value, home) {
190
190
  export function slugify(name) {
191
191
  return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
192
192
  }
193
- /** `$HOME` for the registry, overridable for tests. */
193
+ /** `$HOME` for the registry, overridable for tests (shared with session journals). */
194
194
  function resolveHome(home) {
195
- return home ?? process.env.SPECRAILS_REGISTRY_HOME ?? os.homedir();
195
+ return userHome(home);
196
196
  }
197
197
  /** Absolute path to `registry.json`. */
198
198
  export function registryPath(home) {
@@ -107,6 +107,22 @@ export interface VerificationReceipt {
107
107
  reason?: string;
108
108
  /** Repositories in scope that this receipt ran no command for (explicitly admitted by the request). */
109
109
  unverifiedRepositories?: string[];
110
+ /** Candidate files the verification commands themselves added, modified or removed (absent when the candidate did not move). */
111
+ selfMutation?: VerificationSelfMutation;
112
+ }
113
+ export interface CandidateFileChange {
114
+ repositoryId: string;
115
+ path: string;
116
+ change: 'added' | 'modified' | 'removed';
117
+ }
118
+ /** What verification wrote into its own candidate: the files (bounded, `omittedFiles` counts the rest) and the commands that executed in the run. */
119
+ export interface VerificationSelfMutation {
120
+ files: CandidateFileChange[];
121
+ omittedFiles?: number;
122
+ commands: Array<{
123
+ repositoryId: string;
124
+ label: string;
125
+ }>;
110
126
  }
111
127
  export interface AcceptanceCriterion {
112
128
  specId: string;
@@ -232,7 +248,19 @@ export interface CandidateManifest {
232
248
  }>;
233
249
  }
234
250
  export declare function candidateManifest(state: CandidateScope): CandidateManifest;
251
+ export declare function candidateManifestHash(manifest: CandidateManifest): string;
235
252
  export declare function fingerprintCandidate(state: CandidateScope): string;
253
+ /** Files that differ between two manifests of the same scope, sorted by repository, then path. */
254
+ export declare function diffCandidateManifests(before: CandidateManifest, after: CandidateManifest): CandidateFileChange[];
255
+ /** `1 candidate file: a` / `4 candidate files: a, b, c and 1 more`, shared by receipts and failure summaries. */
256
+ export declare function describeModifiedFiles(mutation: Pick<VerificationSelfMutation, 'files' | 'omittedFiles'>): string;
257
+ /**
258
+ * A receipt whose candidate moved while it ran. During a verify step no role
259
+ * turn runs, so a manifest diff around the run attributes the change to the
260
+ * verification commands themselves (a lint pre-step regenerating a tracked
261
+ * file); without both manifests the generic reason stays.
262
+ */
263
+ export declare function candidateChangedReceipt(receipt: VerificationReceipt, before: CandidateManifest | undefined, after: (() => CandidateManifest) | undefined): VerificationReceipt;
236
264
  export declare function initializePipeline(contextInput: unknown, change: string): PipelineState;
237
265
  /** Host-owned binding, including immutable harness bytes outside the candidate. */
238
266
  export declare function bindVerificationPlan(contextInput: PipelineContext, hash: string, files: Array<{
@@ -309,6 +337,9 @@ export interface VerificationRunOptions {
309
337
  /** Persistence and candidate ownership are bound by the host, never inferred from a journal path. */
310
338
  export interface VerificationEvidencePort {
311
339
  candidateHash: string;
340
+ /** The manifest `candidateHash` was computed from; with `currentManifest`, an invalidated receipt names the files verification modified. */
341
+ candidateManifest?: CandidateManifest;
342
+ currentManifest?(): CandidateManifest;
312
343
  scopeHash: string;
313
344
  planHash?: string;
314
345
  previous?: VerificationReceipt;
@@ -335,21 +366,15 @@ export interface VerificationEvidenceQuery {
335
366
  export declare function persistCheckEvidence(context: PipelineContext, result: CommandReceipt, planHash: string, candidateHash: string): void;
336
367
  /** Historical read: no live repository validation, source execution or lifecycle mutation. */
337
368
  export declare function readVerificationEvidence(context: Pick<PipelineContext, 'backlogRoot' | 'runId'>, query?: VerificationEvidenceQuery): {
338
- nextCursor?: string | undefined;
339
369
  schemaVersion: number;
340
370
  available: boolean;
341
371
  items: Record<string, unknown>[];
342
372
  truncated: boolean;
373
+ nextCursor?: string | undefined;
343
374
  } | {
344
375
  schemaVersion: number;
345
376
  available: boolean;
346
377
  items: {
347
- sources: {
348
- id: string;
349
- displayPath: string;
350
- byteCount: number;
351
- hash: string;
352
- }[];
353
378
  schemaVersion: 1;
354
379
  runId: string;
355
380
  id: string;
@@ -358,9 +383,9 @@ export declare function readVerificationEvidence(context: Pick<PipelineContext,
358
383
  label: string;
359
384
  origin: string[];
360
385
  required: true;
361
- disposition: "executed" | "reused" | "not-run";
386
+ disposition: 'executed' | 'reused' | 'not-run';
362
387
  reusedFrom?: string;
363
- status: "passed" | "failed" | "cancelled" | "interrupted" | "unavailable";
388
+ status: 'passed' | 'failed' | 'cancelled' | 'interrupted' | 'unavailable';
364
389
  reuseReason?: string;
365
390
  recordedAt: string;
366
391
  execution: {
@@ -382,16 +407,22 @@ export declare function readVerificationEvidence(context: Pick<PipelineContext,
382
407
  stderrBytes: number;
383
408
  stdoutHash: string;
384
409
  stderrHash: string;
410
+ sources: {
411
+ id: string;
412
+ displayPath: string;
413
+ byteCount: number;
414
+ hash: string;
415
+ }[];
385
416
  }[];
386
417
  truncated: boolean;
387
418
  } | {
388
- nextCursor?: string | undefined;
419
+ items?: undefined;
389
420
  schemaVersion: number;
390
421
  available: boolean;
391
422
  text: string;
392
423
  byteCount: number;
393
424
  totalBytes: number;
394
425
  truncated: boolean;
395
- items?: undefined;
426
+ nextCursor?: string | undefined;
396
427
  };
397
428
  export {};
@@ -407,7 +407,45 @@ export function candidateManifest(state) {
407
407
  }));
408
408
  return { schemaVersion: 1, scopeHash: state.scopeHash, repositories: entries };
409
409
  }
410
- export function fingerprintCandidate(state) { return digest(canonical(candidateManifest(state).repositories)); }
410
+ export function candidateManifestHash(manifest) { return digest(canonical(manifest.repositories)); }
411
+ export function fingerprintCandidate(state) { return candidateManifestHash(candidateManifest(state)); }
412
+ /** Files that differ between two manifests of the same scope, sorted by repository, then path. */
413
+ export function diffCandidateManifests(before, after) {
414
+ const changes = [];
415
+ for (const repositoryId of [...new Set([...before.repositories, ...after.repositories].map(repo => repo.id))].sort()) {
416
+ const old = new Map(before.repositories.find(repo => repo.id === repositoryId)?.files ?? []);
417
+ const current = new Map(after.repositories.find(repo => repo.id === repositoryId)?.files ?? []);
418
+ for (const file of [...new Set([...old.keys(), ...current.keys()])].sort()) {
419
+ const change = !old.has(file) ? 'added' : !current.has(file) ? 'removed' : old.get(file) !== current.get(file) ? 'modified' : undefined;
420
+ if (change)
421
+ changes.push({ repositoryId, path: file, change });
422
+ }
423
+ }
424
+ return changes;
425
+ }
426
+ const SELF_MUTATION_FILE_LIMIT = 200;
427
+ /** `1 candidate file: a` / `4 candidate files: a, b, c and 1 more`, shared by receipts and failure summaries. */
428
+ export function describeModifiedFiles(mutation) {
429
+ const total = mutation.files.length + (mutation.omittedFiles ?? 0), shown = mutation.files.slice(0, 3).map(file => file.path);
430
+ return `${total} candidate file${total === 1 ? '' : 's'}: ${shown.join(', ')}${total > shown.length ? ` and ${total - shown.length} more` : ''}`;
431
+ }
432
+ /**
433
+ * A receipt whose candidate moved while it ran. During a verify step no role
434
+ * turn runs, so a manifest diff around the run attributes the change to the
435
+ * verification commands themselves (a lint pre-step regenerating a tracked
436
+ * file); without both manifests the generic reason stays.
437
+ */
438
+ export function candidateChangedReceipt(receipt, before, after) {
439
+ if (receipt.selfMutation)
440
+ return { ...receipt, valid: false };
441
+ const files = before && after ? diffCandidateManifests(before, after()) : [];
442
+ if (!files.length)
443
+ return { ...receipt, valid: false, reason: 'Candidate changed during verification' };
444
+ const selfMutation = { files: files.slice(0, SELF_MUTATION_FILE_LIMIT), ...(files.length > SELF_MUTATION_FILE_LIMIT ? { omittedFiles: files.length - SELF_MUTATION_FILE_LIMIT } : {}),
445
+ commands: receipt.commands.filter(command => command.disposition !== 'reused' && command.disposition !== 'not-run').map(command => ({ repositoryId: command.repositoryId, label: command.label ?? command.key ?? [command.command, ...command.args].join(' ').slice(0, 256) })) };
446
+ const failed = receipt.commands.some(command => command.exitCode !== 0);
447
+ return { ...receipt, valid: false, reason: `${failed ? 'A verification command failed. ' : ''}Verification commands modified ${describeModifiedFiles(selfMutation)}`, selfMutation };
448
+ }
411
449
  function activeArtifactPath(state) { return state.archivePath ?? path.join(state.context.artifactRoot, 'openspec', 'changes', state.change); }
412
450
  function artifactFingerprint(state) {
413
451
  const root = activeArtifactPath(state);
@@ -1154,19 +1192,19 @@ export async function verifyPipeline(contextInput, raw, log = () => { }, signal,
1154
1192
  const identityProblems = planReasons(state, typeof request.planHash === 'string' ? request.planHash : undefined);
1155
1193
  if (identityProblems.length)
1156
1194
  fail(identityProblems.join('; '));
1157
- const candidateHash = fingerprintCandidate(state);
1195
+ const manifest = candidateManifest(state), candidateHash = candidateManifestHash(manifest);
1158
1196
  const isCurrent = () => {
1159
1197
  const current = readState(context);
1160
1198
  return fingerprintCandidate(current) === candidateHash && current.revision === state.revision && planReasons(current, request.planHash).length === 0;
1161
1199
  };
1162
1200
  return executeVerification(context, raw, {
1163
- candidateHash, scopeHash: state.scopeHash, planHash: state.verificationPlan?.hash, previous,
1201
+ candidateHash, candidateManifest: manifest, currentManifest: () => candidateManifest(readState(context)), scopeHash: state.scopeHash, planHash: state.verificationPlan?.hash, previous,
1164
1202
  isCurrent: () => locked(context, isCurrent),
1165
1203
  persistCheck: (result, planHash, candidate) => persistCheckEvidence(context, result, planHash, candidate),
1166
1204
  commitReceipt: receipt => locked(context, () => {
1167
1205
  const current = readState(context);
1168
1206
  if (!isCurrent())
1169
- receipt = { ...receipt, valid: false, reason: 'Candidate changed during verification' };
1207
+ receipt = candidateChangedReceipt(receipt, manifest, () => candidateManifest(current));
1170
1208
  atomicJson(safeChild(pipelineStateDirectory(context), 'receipts/' + receipt.id + '.json'), receipt);
1171
1209
  if (receipt.kind === 'full' || !receipt.valid || !current.verification)
1172
1210
  current.verification = receipt;
@@ -1249,12 +1287,13 @@ export async function executeVerification(contextInput, raw, evidence, log = ()
1249
1287
  notRunEvidenceIds.push(id);
1250
1288
  }
1251
1289
  const changed = !evidence.isCurrent();
1252
- const receipt = await evidence.commitReceipt({
1290
+ const draft = {
1253
1291
  id: randomUUID(), ...(evidence.planHash ? { planHash: evidence.planHash } : {}), kind: request.kind, scopeHash: evidence.scopeHash, candidateHash, commands: results, ...(notRunEvidenceIds.length ? { notRunEvidenceIds } : {}),
1254
- completedAt: new Date().toISOString(), valid: !changed && !signal?.aborted && (options.deadline === undefined || Date.now() <= options.deadline) && results.length === commands.length && results.every((result) => result.exitCode === 0),
1255
- ...(changed ? { reason: 'Candidate changed during verification' } : results.some((result) => result.exitCode !== 0) ? { reason: 'A verification command failed' } : {}),
1292
+ completedAt: new Date().toISOString(), valid: !signal?.aborted && (options.deadline === undefined || Date.now() <= options.deadline) && results.length === commands.length && results.every((result) => result.exitCode === 0),
1293
+ ...(results.some((result) => result.exitCode !== 0) ? { reason: 'A verification command failed' } : {}),
1256
1294
  ...(unverifiedRepositories.length ? { unverifiedRepositories } : {}),
1257
- });
1295
+ };
1296
+ const receipt = await evidence.commitReceipt(changed ? candidateChangedReceipt(draft, evidence.candidateManifest, evidence.currentManifest && (() => evidence.currentManifest())) : draft);
1258
1297
  if (!receipt.valid)
1259
1298
  for (const result of results) {
1260
1299
  await options.onEvidence?.('check-invalidated', { executionId: result.evidenceId ?? '', repositoryId: result.repositoryId, checkId: result.key ?? '', label: result.label ?? result.command, reason: receipt.reason ?? 'Verification did not complete successfully' });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The user-level Specrails home. Registry, framework store, workspaces and
3
+ * agent session journals all live under `<home>/.specrails`, never inside a
4
+ * project repository.
5
+ *
6
+ * `SPECRAILS_REGISTRY_HOME` overrides the base directory so tests (and
7
+ * isolated installs) redirect every user-level artifact together.
8
+ */
9
+ export declare function userHome(home?: string): string;
10
+ /** Absolute path to `<home>/.specrails`. */
11
+ export declare function specrailsHome(home?: string): string;
12
+ /** Scope used by session hosts that are not bound to a project. */
13
+ export declare const GLOBAL_SESSION_SCOPE = "global";
14
+ /** True when `scope` is a valid session scope key (a project slug or `global`). */
15
+ export declare function isSessionScopeKey(scope: string): boolean;
16
+ /**
17
+ * Directory that holds every agent session journal of one scope:
18
+ * `<home>/.specrails/sessions/<scope>/`. Sessions are grouped per project so a
19
+ * project's history can be inspected, backed up or removed as one unit.
20
+ */
21
+ export declare function sessionsRoot(scope: string, home?: string): string;
@@ -0,0 +1,36 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /**
4
+ * The user-level Specrails home. Registry, framework store, workspaces and
5
+ * agent session journals all live under `<home>/.specrails`, never inside a
6
+ * project repository.
7
+ *
8
+ * `SPECRAILS_REGISTRY_HOME` overrides the base directory so tests (and
9
+ * isolated installs) redirect every user-level artifact together.
10
+ */
11
+ export function userHome(home) {
12
+ return home ?? process.env.SPECRAILS_REGISTRY_HOME ?? os.homedir();
13
+ }
14
+ /** Absolute path to `<home>/.specrails`. */
15
+ export function specrailsHome(home) {
16
+ return path.join(userHome(home), '.specrails');
17
+ }
18
+ /** Scope used by session hosts that are not bound to a project. */
19
+ export const GLOBAL_SESSION_SCOPE = 'global';
20
+ /** Same alphabet the registry uses for project slugs, bounded for file names. */
21
+ const SCOPE_KEY = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/;
22
+ /** True when `scope` is a valid session scope key (a project slug or `global`). */
23
+ export function isSessionScopeKey(scope) {
24
+ return SCOPE_KEY.test(scope);
25
+ }
26
+ /**
27
+ * Directory that holds every agent session journal of one scope:
28
+ * `<home>/.specrails/sessions/<scope>/`. Sessions are grouped per project so a
29
+ * project's history can be inspected, backed up or removed as one unit.
30
+ */
31
+ export function sessionsRoot(scope, home) {
32
+ if (!isSessionScopeKey(scope)) {
33
+ throw new Error(`Invalid session scope "${scope}": use a project slug (a-z, 0-9, "-") or "${GLOBAL_SESSION_SCOPE}"`);
34
+ }
35
+ return path.join(specrailsHome(home), 'sessions', scope);
36
+ }
@@ -241,7 +241,8 @@
241
241
  "workflows",
242
242
  "fork",
243
243
  "signal",
244
- "cancel"
244
+ "cancel",
245
+ "host"
245
246
  ],
246
247
  "cliPresentationOperations": [
247
248
  "help"
@@ -373,6 +374,46 @@
373
374
  "workflowAgentSteps": {
374
375
  "version": 1,
375
376
  "ownership": "Host-defined roles, schemas and gates; Core provides scoped artifact contracts and verified candidate guards"
377
+ },
378
+ "sessions": {
379
+ "version": 1,
380
+ "protocolVersions": [
381
+ 1
382
+ ],
383
+ "capability": "sessions",
384
+ "cliOperation": "host",
385
+ "cliCommand": "specrails-core runtime host --stdio --scope <projectKey|global>",
386
+ "transport": "JSON-RPC 2.0 over newline-delimited stdio; one host per scope",
387
+ "journalRoot": "~/.specrails/sessions/<scope>/sessions.sqlite",
388
+ "moduleExport": "./agent-runtime/session",
389
+ "documentation": "docs/agent-sessions/protocol.md",
390
+ "eventTypes": [
391
+ "session.opened",
392
+ "session.phase",
393
+ "session.process",
394
+ "session.provider-ref",
395
+ "session.updated",
396
+ "session.closed",
397
+ "input.accepted",
398
+ "input.state",
399
+ "turn.started",
400
+ "turn.output",
401
+ "turn.tool",
402
+ "turn.completed",
403
+ "subagent.started",
404
+ "subagent.phase",
405
+ "subagent.output",
406
+ "subagent.usage",
407
+ "subagent.result",
408
+ "subagents.settled",
409
+ "output.truncated",
410
+ "notice.interruption",
411
+ "provider.diagnostic"
412
+ ],
413
+ "capabilities": [
414
+ "sessions",
415
+ "delegation"
416
+ ]
376
417
  }
377
418
  },
378
419
  "execution": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-core",
3
- "version": "6.4.0",
3
+ "version": "6.6.0",
4
4
  "description": "Agent workflow engine for specrails-desktop: provider-independent installer and programmatic agent runtime for Claude Code and Codex",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,16 @@
14
14
  "import": "./dist/agent-runtime/engine/index.js",
15
15
  "default": "./dist/agent-runtime/engine/index.js"
16
16
  },
17
+ "./agent-runtime/session": {
18
+ "types": "./dist/agent-runtime/session/index.d.ts",
19
+ "import": "./dist/agent-runtime/session/index.js",
20
+ "default": "./dist/agent-runtime/session/index.js"
21
+ },
22
+ "./agent-runtime/session/testing": {
23
+ "types": "./dist/agent-runtime/session/testing/index.d.ts",
24
+ "import": "./dist/agent-runtime/session/testing/index.js",
25
+ "default": "./dist/agent-runtime/session/testing/index.js"
26
+ },
17
27
  "./package.json": "./package.json",
18
28
  "./integration-contract.json": "./integration-contract.json",
19
29
  "./schemas/agent-runtime.schema.json": "./schemas/agent-runtime.schema.json",
@@ -79,23 +89,25 @@
79
89
  },
80
90
  "dependencies": {
81
91
  "@fission-ai/openspec": "1.4.1",
82
- "@langchain/core": "1.2.10",
83
- "@langchain/langgraph": "1.4.14",
92
+ "@langchain/core": "1.2.14",
93
+ "@langchain/langgraph": "1.4.18",
84
94
  "@langchain/langgraph-checkpoint": "1.1.5",
85
- "@modelcontextprotocol/sdk": "1.29.0",
95
+ "@modelcontextprotocol/sdk": "1.31.0",
86
96
  "ajv": "^8.20.0",
87
97
  "cross-spawn": "7.0.6",
88
- "js-yaml": "^4.1.0",
98
+ "js-yaml": "^5.4.3",
89
99
  "picocolors": "^1.1.1",
90
- "zod": "4.6.2"
100
+ "semver": "^7.8.5",
101
+ "zod": "4.6.5"
91
102
  },
92
103
  "devDependencies": {
93
104
  "@types/cross-spawn": "6.0.6",
94
- "@types/js-yaml": "^4.0.9",
95
- "@types/node": "^22.10.2",
96
- "@vitest/coverage-v8": "^4.1.5",
97
- "typescript": "^5.7.2",
98
- "vitest": "^4.1.5"
105
+ "@types/node": "^26.6.5",
106
+ "@types/semver": "^7.8.0",
107
+ "@vitest/coverage-v8": "^5.0.3",
108
+ "js-yaml-4": "npm:js-yaml@^4.3.2",
109
+ "typescript": "^7.0.2",
110
+ "vitest": "^5.0.3"
99
111
  },
100
112
  "license": "MIT",
101
113
  "author": "fjpulidop"
@@ -12,7 +12,7 @@
12
12
  "planning": { "enum": ["full", "proportional"] }, "acceptDeveloperChecks": { "type": "boolean" },
13
13
  "verification": { "type": "object", "additionalProperties": false, "properties": { "maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 4 } } }
14
14
  } },
15
- "guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" } } },
15
+ "guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" }, "verification-output-adoption": { "type": "boolean" } } },
16
16
  "rolePrompts": { "type": "object", "propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,63}$" }, "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" }, "properties": {
17
17
  "architect": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
18
18
  "developer": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },