specrails-core 6.5.0 → 6.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +2 -2
  2. package/dist/agent-runtime/cli-executor.js +1 -1
  3. package/dist/agent-runtime/cli.d.ts +4 -4
  4. package/dist/agent-runtime/compact/environment.d.ts +20 -2
  5. package/dist/agent-runtime/compact/environment.js +63 -4
  6. package/dist/agent-runtime/compact/step.d.ts +1 -1
  7. package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
  8. package/dist/agent-runtime/efficiency-types.d.ts +1 -1
  9. package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
  10. package/dist/agent-runtime/engine/contracts.d.ts +2 -1
  11. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  12. package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
  13. package/dist/agent-runtime/engine/fork.d.ts +1 -1
  14. package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
  15. package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
  16. package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
  17. package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
  18. package/dist/agent-runtime/engine/pieces/role-turn.js +10 -1
  19. package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
  20. package/dist/agent-runtime/engine/pieces/verify.js +43 -8
  21. package/dist/agent-runtime/engine/run-status.d.ts +165 -165
  22. package/dist/agent-runtime/engine/runs.d.ts +168 -168
  23. package/dist/agent-runtime/engine/state.d.ts +16 -16
  24. package/dist/agent-runtime/evaluation.d.ts +3 -3
  25. package/dist/agent-runtime/executor-types.d.ts +10 -10
  26. package/dist/agent-runtime/graph/convergence.d.ts +2 -2
  27. package/dist/agent-runtime/graph/convergence.js +11 -0
  28. package/dist/agent-runtime/graph/nodes.js +35 -4
  29. package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
  30. package/dist/agent-runtime/graph/state.d.ts +6 -2
  31. package/dist/agent-runtime/guardrails.d.ts +1 -1
  32. package/dist/agent-runtime/guardrails.js +4 -1
  33. package/dist/agent-runtime/index.d.ts +3 -2
  34. package/dist/agent-runtime/index.js +1 -0
  35. package/dist/agent-runtime/openai-executor.d.ts +9 -9
  36. package/dist/agent-runtime/openspec.d.ts +4 -4
  37. package/dist/agent-runtime/prompts.d.ts +6 -0
  38. package/dist/agent-runtime/prompts.js +32 -8
  39. package/dist/agent-runtime/recovery.d.ts +12 -12
  40. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +2 -2
  41. package/dist/agent-runtime/verification-repair.d.ts +31 -2
  42. package/dist/agent-runtime/verification-repair.js +58 -3
  43. package/dist/installer/commands/init.d.ts +1 -1
  44. package/dist/installer/phases/install-config.js +10 -3
  45. package/dist/installer/util/paths.d.ts +25 -1
  46. package/dist/pipeline/pipeline-state.d.ts +42 -11
  47. package/dist/pipeline/pipeline-state.js +47 -8
  48. package/package.json +13 -11
  49. package/schemas/agent-runtime.schema.json +1 -1
@@ -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
@@ -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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-core",
3
- "version": "6.5.0",
3
+ "version": "6.6.1",
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": {
@@ -89,23 +89,25 @@
89
89
  },
90
90
  "dependencies": {
91
91
  "@fission-ai/openspec": "1.4.1",
92
- "@langchain/core": "1.2.10",
93
- "@langchain/langgraph": "1.4.14",
92
+ "@langchain/core": "1.2.14",
93
+ "@langchain/langgraph": "1.4.18",
94
94
  "@langchain/langgraph-checkpoint": "1.1.5",
95
- "@modelcontextprotocol/sdk": "1.29.0",
95
+ "@modelcontextprotocol/sdk": "1.31.0",
96
96
  "ajv": "^8.20.0",
97
97
  "cross-spawn": "7.0.6",
98
- "js-yaml": "^4.1.0",
98
+ "js-yaml": "^5.4.3",
99
99
  "picocolors": "^1.1.1",
100
- "zod": "4.6.2"
100
+ "semver": "^7.8.5",
101
+ "zod": "4.6.5"
101
102
  },
102
103
  "devDependencies": {
103
104
  "@types/cross-spawn": "6.0.6",
104
- "@types/js-yaml": "^4.0.9",
105
- "@types/node": "^22.10.2",
106
- "@vitest/coverage-v8": "^4.1.5",
107
- "typescript": "^5.7.2",
108
- "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"
109
111
  },
110
112
  "license": "MIT",
111
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" },