specrails-core 6.6.4 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/agent-runtime/capabilities.d.ts +5 -2
  2. package/dist/agent-runtime/capabilities.js +8 -2
  3. package/dist/agent-runtime/change-scope.d.ts +2 -0
  4. package/dist/agent-runtime/change-scope.js +5 -0
  5. package/dist/agent-runtime/cli-executor.js +7 -0
  6. package/dist/agent-runtime/cli.d.ts +6 -1
  7. package/dist/agent-runtime/cli.js +15 -4
  8. package/dist/agent-runtime/compact/reviewer.js +3 -1
  9. package/dist/agent-runtime/config.js +16 -5
  10. package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
  11. package/dist/agent-runtime/efficiency-summary.js +7 -1
  12. package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
  13. package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
  14. package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
  15. package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
  16. package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
  17. package/dist/agent-runtime/engine/cli.d.ts +12 -0
  18. package/dist/agent-runtime/engine/cli.js +15 -4
  19. package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
  20. package/dist/agent-runtime/engine/definition-schema.js +1 -0
  21. package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
  22. package/dist/agent-runtime/engine/definition-types.js +1 -1
  23. package/dist/agent-runtime/engine/definition-validator.js +8 -0
  24. package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
  25. package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
  26. package/dist/agent-runtime/engine/invocation-context.js +2 -0
  27. package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
  28. package/dist/agent-runtime/engine/piece-registry.js +1 -1
  29. package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
  30. package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
  31. package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
  32. package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
  33. package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
  34. package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
  35. package/dist/agent-runtime/engine/pieces/index.js +2 -1
  36. package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
  37. package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
  38. package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
  39. package/dist/agent-runtime/engine/preflight.d.ts +2 -1
  40. package/dist/agent-runtime/engine/preflight.js +3 -1
  41. package/dist/agent-runtime/engine/regex.d.ts +12 -2
  42. package/dist/agent-runtime/engine/regex.js +25 -11
  43. package/dist/agent-runtime/engine/run-status.d.ts +34 -0
  44. package/dist/agent-runtime/engine/run-status.js +7 -1
  45. package/dist/agent-runtime/engine/runs.d.ts +56 -0
  46. package/dist/agent-runtime/engine/runs.js +88 -5
  47. package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
  48. package/dist/agent-runtime/engine/steering/inbox.js +21 -1
  49. package/dist/agent-runtime/engine/storage/private-path.js +7 -1
  50. package/dist/agent-runtime/evaluation.js +1 -1
  51. package/dist/agent-runtime/executor-types.d.ts +19 -0
  52. package/dist/agent-runtime/executor-types.js +10 -0
  53. package/dist/agent-runtime/failure-classification.d.ts +51 -0
  54. package/dist/agent-runtime/failure-classification.js +110 -0
  55. package/dist/agent-runtime/graph/archive-preflight.d.ts +3 -18
  56. package/dist/agent-runtime/graph/archive-preflight.js +6 -51
  57. package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
  58. package/dist/agent-runtime/graph/artifacts.js +6 -2
  59. package/dist/agent-runtime/graph/baseline.d.ts +50 -0
  60. package/dist/agent-runtime/graph/baseline.js +145 -0
  61. package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
  62. package/dist/agent-runtime/graph/capability-map.js +65 -0
  63. package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
  64. package/dist/agent-runtime/graph/clarifications.js +158 -0
  65. package/dist/agent-runtime/graph/nodes.js +178 -19
  66. package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
  67. package/dist/agent-runtime/graph/review-brief-record.js +26 -0
  68. package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
  69. package/dist/agent-runtime/graph/review-brief.js +127 -0
  70. package/dist/agent-runtime/graph/roles.js +5 -3
  71. package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
  72. package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
  73. package/dist/agent-runtime/graph/state.d.ts +8 -0
  74. package/dist/agent-runtime/index.d.ts +7 -0
  75. package/dist/agent-runtime/index.js +7 -0
  76. package/dist/agent-runtime/openspec.d.ts +2 -1
  77. package/dist/agent-runtime/openspec.js +11 -5
  78. package/dist/agent-runtime/project-rules.d.ts +139 -0
  79. package/dist/agent-runtime/project-rules.js +683 -0
  80. package/dist/agent-runtime/prompts.d.ts +32 -1
  81. package/dist/agent-runtime/prompts.js +180 -7
  82. package/dist/agent-runtime/role-routing.d.ts +2 -1
  83. package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
  84. package/dist/agent-runtime/session/application/active-session.js +163 -7
  85. package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
  86. package/dist/agent-runtime/session/domain/descriptor.js +5 -0
  87. package/dist/agent-runtime/session/domain/interruption.js +1 -0
  88. package/dist/agent-runtime/session/domain/policy.js +2 -0
  89. package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
  90. package/dist/agent-runtime/session/domain/redaction.js +71 -0
  91. package/dist/agent-runtime/session/domain/types.d.ts +17 -1
  92. package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
  93. package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
  94. package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
  95. package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
  96. package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
  97. package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
  98. package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
  99. package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
  100. package/dist/agent-runtime/session/host/protocol.js +10 -2
  101. package/dist/agent-runtime/session/host/server.js +6 -5
  102. package/dist/agent-runtime/session/index.d.ts +1 -1
  103. package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
  104. package/dist/agent-runtime/session/ports.d.ts +6 -2
  105. package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
  106. package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
  107. package/dist/installer/cli.js +10 -1
  108. package/dist/installer/commands/init.js +15 -4
  109. package/dist/installer/phases/prereqs.d.ts +1 -1
  110. package/dist/installer/phases/prereqs.js +2 -2
  111. package/dist/installer/phases/scaffold.js +59 -11
  112. package/dist/pipeline/pipeline-state.d.ts +15 -0
  113. package/dist/pipeline/pipeline-state.js +36 -3
  114. package/dist/pipeline/spec-trace.d.ts +134 -0
  115. package/dist/pipeline/spec-trace.js +392 -0
  116. package/dist/shared/secret-redaction.d.ts +42 -0
  117. package/dist/shared/secret-redaction.js +184 -0
  118. package/integration-contract.json +177 -7
  119. package/package.json +8 -8
  120. package/pinned-versions.json +1 -1
  121. package/schemas/agent-runtime.schema.json +3 -1
  122. package/schemas/workflow-definition.schema.json +1 -0
  123. package/templates/settings/codex-config.toml +3 -2
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Deterministic secret redaction for text and structured values that Core
3
+ * persists for display (the agent session journal).
4
+ *
5
+ * Every pattern is bounded: quantifiers have explicit upper limits and no
6
+ * pattern nests unbounded repetition, so the work per input character is
7
+ * constant and a 1 MB string redacts in linear time. PEM blocks are found with
8
+ * `indexOf`, not a regex, for the same reason.
9
+ *
10
+ * The rules favour leaking less over keeping every byte: a value assigned to a
11
+ * secret-like name is replaced even when it is not a credential (for example
12
+ * `token = readToken()` in a file read). Words that merely contain a secret
13
+ * word are not names: `author`, `authority`, `authenticate`, `max_tokens` and
14
+ * `tokenizer` stay readable. Bare `session` / `sessionId` are protocol
15
+ * identifiers and are not redacted; `session_token`, `session_secret`,
16
+ * `session_key` and `session_cookie` are.
17
+ *
18
+ * Known limits (documented in docs/agent-sessions/journal.md):
19
+ * - A secret split across two separately redacted strings (for example two
20
+ * streamed output deltas) can leak the part after the split.
21
+ * - Short flags such as `mysql -pSECRET` and secrets with no recognisable name
22
+ * or format are not detected.
23
+ *
24
+ * `src/pipeline/pipeline-state.ts` keeps its own self-contained
25
+ * `redactRuntimeText` because it is copied into projects and may only use Node
26
+ * built-ins; it is not replaced by this module.
27
+ */
28
+ export const REDACTED = '[REDACTED]';
29
+ const MAX_PEM_BODY = 16_384;
30
+ /**
31
+ * Secret words inside a normalized (lower-case, `_`-separated) name.
32
+ * `auth` must end a word so `author`/`authority`/`authenticate` do not match.
33
+ * `token` must not be followed by `s`/`iz` so token counts and tokenizers stay.
34
+ */
35
+ const SECRET_NAME = /(?:passw(?:or)?d|passphrase|secret|token(?!s|iz)|api_?key|access_?key|private_?key|signing_?key|encryption_?key|credentials?|(?:^|_)auth(?:orization)?(?:_|$)|bearer|cookie|session_(?:token|secret|key|cookie))/;
36
+ /** Lower-case and split camelCase / kebab-case / dotted names into `_` words. */
37
+ function normalizeName(name) {
38
+ return name
39
+ .slice(0, 128)
40
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
41
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9]+/g, '_');
44
+ }
45
+ /** True when a key, variable or flag name looks like it holds a secret. */
46
+ export function isSecretName(name) {
47
+ return SECRET_NAME.test(normalizeName(name));
48
+ }
49
+ // ── Inline patterns ─────────────────────────────────────────────────────────
50
+ /** Well-known credential formats, replaced wherever they appear. */
51
+ const KNOWN_FORMATS = [
52
+ // OpenAI / Anthropic style keys: sk-..., sk-proj-..., sk-ant-...; must contain a digit.
53
+ /\bsk-(?=[A-Za-z0-9_-]{0,200}\d)[A-Za-z0-9_-]{20,512}/g,
54
+ // GitHub tokens.
55
+ /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g,
56
+ /\bgithub_pat_[A-Za-z0-9_]{22,255}\b/g,
57
+ // GitLab and npm tokens.
58
+ /\bglpat-[A-Za-z0-9_-]{20,255}\b/g,
59
+ /\bnpm_[A-Za-z0-9]{36,255}\b/g,
60
+ // Slack tokens.
61
+ /\bxox[abeoprs]-[A-Za-z0-9-]{10,255}/g,
62
+ /\bxapp-[A-Za-z0-9-]{10,255}/g,
63
+ // AWS access key ids.
64
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
65
+ // Google API keys.
66
+ /\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g,
67
+ // JSON Web Tokens.
68
+ /\beyJ[A-Za-z0-9_-]{8,4096}\.eyJ[A-Za-z0-9_-]{8,4096}\.[A-Za-z0-9_-]{8,4096}/g,
69
+ ];
70
+ /** `Authorization: Bearer x`, `"authorization": "Basic x"`, `Proxy-Authorization=...`; keeps the scheme. */
71
+ const AUTHORIZATION_HEADER = /\b((?:proxy-)?authorization["']?[ \t]{0,8}[:=][ \t]{0,8}["']?)((?:bearer|basic|token|digest|negotiate)[ \t]{1,8})?([^\s"'`,;]{1,4096})/gi;
72
+ /** A standalone `Bearer <token>` (the token must be long enough not to be prose). */
73
+ const BEARER = /\b(bearer[ \t]{1,8})([A-Za-z0-9._~+/-]{16,4096}=*)/gi;
74
+ /** `scheme://user:password@host` → keeps the user and host. */
75
+ const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]{0,20}:\/\/[^\s:/@]{1,256}:)([^\s/@]{1,256})(@)/gi;
76
+ /**
77
+ * `--token v`, `--api-key=v`, `--password "v"`: the flag name must END with the
78
+ * secret word, so `--password-stdin` or `--token-file` are left alone; a value
79
+ * starting with `-` is the next flag, not a secret.
80
+ */
81
+ const SECRET_FLAG = /(^|[\s"'=])(--?(?:[a-z0-9]{1,24}[-_]){0,4}(?:token|api[-_]?key|apikey|password|passwd|passphrase|secret|private[-_]?key|access[-_]?key|auth|credentials?))(=|[ \t]{1,8})("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|[^\s"'-][^\s"']{0,4095})/gi;
82
+ /**
83
+ * `NAME=value`, `NAME: value`, `"name": "value"`, `export NAME='v'` where the
84
+ * name contains a secret word. The name starts at a non-name character so
85
+ * matching is attempted only at word starts; `isSecretName` then refines it.
86
+ */
87
+ const ASSIGNMENT = /(?<![A-Za-z0-9_.-])(["']?)([A-Za-z_][A-Za-z0-9_.-]{0,127})\1([ \t]{0,4}[:=][ \t]{0,4})("[^"\n]{0,4096}"|'[^'\n]{0,4096}'|\[REDACTED\][^\s"',;&|<>(){}[\]]{1,4096}|(?![ \t]|\/\/)[^\s"',;&|<>(){}[\]]{1,4096})/g;
88
+ /** Values that are not secrets even after a secret-like name (types, flags, empty literals). */
89
+ const KEPT_VALUES = new Set(['', 'true', 'false', 'null', 'undefined', 'none', 'string', 'number', 'boolean', 'unknown', 'any', 'required', 'optional']);
90
+ const PEM_BEGIN = /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/g;
91
+ function replaceValue(quoted) {
92
+ if (quoted.startsWith('"') && quoted.endsWith('"') && quoted.length >= 2)
93
+ return `"${REDACTED}"`;
94
+ if (quoted.startsWith("'") && quoted.endsWith("'") && quoted.length >= 2)
95
+ return `'${REDACTED}'`;
96
+ return REDACTED;
97
+ }
98
+ /** PEM private key blocks: BEGIN..END, or BEGIN plus its base64 body when END is missing (truncated output). */
99
+ function redactPem(text) {
100
+ if (!text.includes('PRIVATE KEY-----'))
101
+ return text;
102
+ let out = '';
103
+ let cursor = 0;
104
+ // The next END marker is searched once and reused, so a text full of BEGIN
105
+ // markers without END stays linear.
106
+ let nextEnd = -2;
107
+ PEM_BEGIN.lastIndex = 0;
108
+ for (let match = PEM_BEGIN.exec(text); match; match = PEM_BEGIN.exec(text)) {
109
+ const begin = match.index;
110
+ if (begin < cursor)
111
+ continue;
112
+ const bodyStart = begin + match[0].length;
113
+ if (nextEnd !== -1 && nextEnd < bodyStart)
114
+ nextEnd = text.indexOf('-----END ', bodyStart);
115
+ const endMarker = nextEnd;
116
+ let stop;
117
+ if (endMarker >= 0 && endMarker - bodyStart <= MAX_PEM_BODY) {
118
+ const close = text.indexOf('-----', endMarker + 9);
119
+ stop = close >= 0 && close - endMarker <= 60 ? close + 5 : endMarker + 9;
120
+ }
121
+ else {
122
+ stop = bodyStart;
123
+ const limit = Math.min(text.length, bodyStart + MAX_PEM_BODY);
124
+ while (stop < limit && /[A-Za-z0-9+/=\s]/.test(text[stop]))
125
+ stop++;
126
+ }
127
+ out += text.slice(cursor, begin) + REDACTED;
128
+ cursor = stop;
129
+ PEM_BEGIN.lastIndex = Math.max(stop, bodyStart);
130
+ }
131
+ return out + text.slice(cursor);
132
+ }
133
+ /**
134
+ * Replace secrets inside free text. Idempotent: redacting a redacted string
135
+ * returns it unchanged. Non-secret text is returned byte for byte.
136
+ */
137
+ export function redactSecrets(text) {
138
+ if (text.length === 0)
139
+ return text;
140
+ let result = redactPem(text);
141
+ for (const pattern of KNOWN_FORMATS)
142
+ result = result.replace(pattern, REDACTED);
143
+ result = result.replace(AUTHORIZATION_HEADER, (_match, prefix, scheme) => `${prefix}${scheme ?? ''}${REDACTED}`);
144
+ result = result.replace(BEARER, (_match, prefix) => `${prefix}${REDACTED}`);
145
+ result = result.replace(URL_CREDENTIALS, (_match, prefix, _password, at) => `${prefix}${REDACTED}${at}`);
146
+ result = result.replace(SECRET_FLAG, (_match, lead, flag, separator, value) => `${lead}${flag}${separator}${replaceValue(value)}`);
147
+ result = result.replace(ASSIGNMENT, (match, quote, name, separator, value, offset, whole) => {
148
+ if (!isSecretName(name))
149
+ return match;
150
+ const bare = value.replace(/^["']|["']$/g, '');
151
+ if (bare === REDACTED || KEPT_VALUES.has(bare.toLowerCase()) || bare.startsWith('$'))
152
+ return match;
153
+ // `Authorization: Bearer [REDACTED]` was already handled: do not take the scheme for the value.
154
+ if (whole.slice(offset + match.length, offset + match.length + 24).trimStart().startsWith(REDACTED))
155
+ return match;
156
+ return `${quote}${name}${quote}${separator}${replaceValue(value)}`;
157
+ });
158
+ return result;
159
+ }
160
+ /**
161
+ * Redact a JSON-like value: object entries whose key is secret-like have their
162
+ * string leaves replaced; every other string goes through `redactSecrets`.
163
+ * Numbers, booleans and null are kept (a numeric `max_tokens` stays readable).
164
+ * Returns a new value; the input is not mutated. Depth is bounded.
165
+ */
166
+ export function redactSecretsDeep(value) {
167
+ return redactValue(value, false, 0);
168
+ }
169
+ const MAX_DEPTH = 32;
170
+ function redactValue(value, secret, depth) {
171
+ if (typeof value === 'string')
172
+ return secret ? (value.length === 0 ? value : REDACTED) : redactSecrets(value);
173
+ if (value === null || typeof value !== 'object')
174
+ return value;
175
+ if (depth >= MAX_DEPTH)
176
+ return secret ? REDACTED : value;
177
+ if (Array.isArray(value))
178
+ return value.map((item) => redactValue(item, secret, depth + 1));
179
+ const out = {};
180
+ for (const [key, item] of Object.entries(value)) {
181
+ out[key] = redactValue(item, secret || isSecretName(key), depth + 1);
182
+ }
183
+ return out;
184
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": "5.2",
2
+ "schemaVersion": "5.9",
3
3
  "providers": {
4
4
  "claude": {
5
5
  "cli": {
@@ -242,6 +242,7 @@
242
242
  "fork",
243
243
  "signal",
244
244
  "cancel",
245
+ "pause",
245
246
  "host"
246
247
  ],
247
248
  "cliPresentationOperations": [
@@ -254,6 +255,126 @@
254
255
  "eventStream": "run/resume emit JSON lines typed workflow-event, agent-event, verification-output, span ({traceId, spanId, name, stepId, attempt, visit, startedAt, endedAt, status, usage?, error?}) and a final runtime-result",
255
256
  "reviewGate": "review.minScore and review.aspects may only tighten Core's own gate (minScore 70, security 75, other aspects 60); the pipeline journal enforces the floor",
256
257
  "acceptance": "The reviewer certifies every frozen acceptance criterion by specId and criterionIndex; Core records the verification commands it ran as required checks and binds the report to the exact candidate before the reviewer verdict",
258
+ "reviewBrief": {
259
+ "version": 1,
260
+ "capability": "reviewBrief",
261
+ "advisory": "The reviewer's optional risk brief never changes the approval gate",
262
+ "runResult": "runtime-result.reviewBrief: {schemaVersion: 1, change, approved, score, recordedAt, brief | null, diagnostic?} | null (engine 1 builtin runs)",
263
+ "recordPath": "<backlogRoot>/.specrails/pipeline/<runId>/review-brief.json",
264
+ "confidenceScore": "openspec/changes/<change>/confidence-score.json brief (approved verdicts; archived with the change)",
265
+ "stepOutput": "reviewer step output brief / briefDiagnostic; engine 2 implementation output review.brief",
266
+ "documentation": "docs/agent-runtime.md#reviewer-risk-brief"
267
+ },
268
+ "architectClarifications": {
269
+ "version": 1,
270
+ "capability": "architectClarifications",
271
+ "output": "architect reply clarifications: [{question, impact: high|medium|low, default: string | null}] (at most 5) before planning",
272
+ "policy": "config architect.clarifications: ask-on-high (default) | always-proceed",
273
+ "pause": "ask-on-high and a high-impact clarification with default null pauses the run with pendingQuestion (the questions, numbered when several); resume --answer feeds the answer to the architect as authoritative",
274
+ "proceed": "otherwise the architect plans on its defaults and records them under Assumptions in design.md",
275
+ "designSections": "design.md must contain a non-empty Non-Goals section (and Assumptions when clarifications were listed); a missing section spends the architect's single repair turn",
276
+ "runResult": "runtime-result.clarifications: {schemaVersion: 1, change, policy, status: awaiting-answers | proceeded, recordedAt, clarifications, blocking, answers, assumptions} | null (engine 1 builtin runs)",
277
+ "recordPath": "<backlogRoot>/.specrails/pipeline/<runId>/clarifications.json",
278
+ "stepOutput": "architect step output clarifications; graph state architecture.clarifications",
279
+ "documentation": "docs/agent-runtime.md#architect-clarifications"
280
+ },
281
+ "specTrace": {
282
+ "version": 1,
283
+ "capability": "specTrace",
284
+ "requirementId": "<capability>/<slug> from '### Requirement: <name>' in openspec/specs/<capability>/spec.md (and the change's ADDED/MODIFIED/RENAMED deltas); slug: NFKD without accents, lowercase, every run of non-alphanumerics one '-', no leading or trailing '-'",
285
+ "testTags": "[spec:<capability>/<slug>] in a test title, or a '// spec: <capability>/<slug>' comment ('# spec:' for Python and shell) on the line before the test; any language",
286
+ "states": "demonstrated: tagged test file reached by a verification command that exited 0; undemonstrated: reason no-test | not-executed | failed; untagged: tags that match no requirement (advisory)",
287
+ "report": "{schemaVersion: 1, generatedAt, changes, evidence: receipt | tests-passed | none, receiptId, requirements, demonstrated[{id, name, tests}], undemonstrated[{id, name, reason, tests}], untagged[{tag, repositoryId, file, line, form}], touched, touchedUndemonstrated, scannedFiles}",
288
+ "reportPath": "<backlogRoot>/.specrails/pipeline/<runId>/spec-trace.json (written after each passing verification, bound to the receipt id)",
289
+ "runResult": "runtime-result.specTrace: the report | null (engine 1 builtin runs)",
290
+ "advisory": "The trace never changes verification or acceptance status; the acceptance report carries a finding 'Spec trace (advisory, receipt <id>): Requirements demonstrated X/Y…', the reviewer treats a touched requirement without a demonstrated tag as an issue",
291
+ "divergences": "reviewer check 9 fills brief.divergences[{capability, requirement, observed}] with spec claims of touched capabilities the code contradicts",
292
+ "cli": "specrails-core pipeline trace [--root <dir>] [--context <context.json>] [--change <name>[,...]] [--tests <glob>[,...]] [--tests-passed] [--check] [--json] [--out <file>]; installed projects also get node .specrails/runtime/trace.mjs; normal mode exits 0, --check exits 1 when a requirement touched by the change is not demonstrated",
293
+ "driftSweepDefinition": "src/agent-runtime/engine/__fixtures__/drift-sweep.json (read-only role-turn of a host-declared read role drift-auditor; structured output {divergences[{capability, requirement, observed}], unchecked[]})",
294
+ "documentation": "docs/agent-runtime.md#requirement-traceability"
295
+ },
296
+ "baselineOnTouch": {
297
+ "version": 1,
298
+ "capability": "baselineOnTouch",
299
+ "config": "baselineOnTouch: boolean (default false) in .specrails/agent-runtime.json",
300
+ "capabilityMap": "openspec/capability-map.json {schemaVersion: 1, capabilities: {<capability>: {files: string[], changes: string[]}}}; the archive step merges the files each archived change touched into every capability of its delta specs (paths prefixed '<repositoryId>:' outside the artifact repository); excluded from the reviewed candidate",
301
+ "detection": "after planning, design.md Blast radius files that exist, are code (not tests, docs or lockfiles) and appear neither in the capability map nor as a path or specific directory in openspec/specs are uncovered",
302
+ "plan": "uncovered files make the architect add a '## 0. Baseline' task group (characterization tests of current behaviour plus a minimal as-built spec under ADDED Requirements, requirements tagged per specTrace) in the same session",
303
+ "hardRule": "the developer's first turn writes only the Baseline group; Core refuses non-test edits and runs the verification plan on the unchanged code; failure stops the run (status blocked, error code baseline_characterization_failed) before the developer touches the module",
304
+ "recordPath": "<backlogRoot>/.specrails/pipeline/<runId>/baseline.json {schemaVersion: 1, change, status: not-needed | planned | passed | failed | violated, recordedAt, detection {blastRadius, covered, uncovered, skipped}, tests, receiptId?, reason?}",
305
+ "runResult": "runtime-result.baseline: the record | null (engine 1 builtin runs)",
306
+ "cost": "baseline turns run with invocation kind 'baseline'; efficiencySummary.invocations.byKind.baseline and efficiencySummary.baseline {invocations, roles, costUsd, inputTokens, outputTokens} keep their spend apart",
307
+ "documentation": "docs/agent-runtime.md#baseline-on-touch"
308
+ },
309
+ "rulesInventory": {
310
+ "version": 1,
311
+ "capability": "rulesInventory",
312
+ "cli": "specrails-core rules inventory [--root <repo>] [--artifact-root <dir>] [--offline] [--reclassify] [--json]; exits 0 and always writes the inventory (classification problems are reported in classification.status); init runs it offline after installing",
313
+ "path": "<artifactRoot>/.specrails/rules.json (the directory that holds .specrails/agent-runtime.json); the runtime reads <backlogRoot>/.specrails/rules.json, then <artifactRoot>/.specrails/rules.json",
314
+ "schema": "{schemaVersion: 1, generatedAt, gates[{id, kind: lint | format | typecheck | test | architecture | hook | ci | spec, command, source}], rules[{id, text, source: '<file>:<line>', status: enforcedBy | automatable | humanOnly | unclassified, enforcedBy?, proposedCheck?}], classification {status: classified | cached | offline | unavailable | failed | empty, provider?, model?, reason?, classifiedAt?, usage?}}",
315
+ "gates": "deterministic discovery in the repository: package scripts (lint, format, typecheck, test, architecture, openspec), lint/format configs (ESLint, Biome, Prettier, Ruff, Black, mypy, golangci-lint, rustfmt, clippy, RuboCop), tsconfig strict, architecture tests and dependency-cruiser, git hooks (.husky, .githooks, pre-commit, lefthook), check-like run steps of .github/workflows, openspec validate",
316
+ "rules": "list items and strongly normative sentences of CLAUDE.md, AGENTS.md, CONTRIBUTING.md (also .github/ and docs/), outside code fences, tables and specrails-managed blocks; at most 200; id r-<12 hex> = hash of file and statement, stable across line moves",
317
+ "classification": "one provider call (agents.architect of .specrails/agent-runtime.json, read-only, no role instructions) classifies only rules without a cached classification; the model never adds, drops or rewrites a statement; offline, without configuration or on failure the rules stay unclassified",
318
+ "reviewer": "humanOnly rules are listed to the reviewer, who copies the ones the change could break into brief.humanChecks",
319
+ "verification": "with no host verification configured, the architect is offered the discovered gate commands as the default verification proposal for the artifact repository",
320
+ "claudeInstructions": "Claude Code installs get the sentinel-managed block (<!-- specrails-managed:start/end -->) in the root CLAUDE.md, like codex AGENTS.md, gemini GEMINI.md and kimi .kimi-code/AGENTS.md",
321
+ "documentation": "docs/agent-runtime.md#project-rules-inventory"
322
+ },
323
+ "failureClassification": {
324
+ "version": 1,
325
+ "capability": "failureClassification",
326
+ "kinds": [
327
+ "tests_failed",
328
+ "provider_limit",
329
+ "environment",
330
+ "blocked",
331
+ "timeout",
332
+ "tool_inactivity",
333
+ "crash",
334
+ "unknown"
335
+ ],
336
+ "record": "{kind, phase?, rounds?, resetAt?, detail?, resumablePhase?, confidence: high | low}",
337
+ "runResult": "runtime-result.failure and runtime-status.failure (both engines): the record when the run status is failed or blocked; absent otherwise. A run Core could not start (fatal argument or configuration error) carries none.",
338
+ "semantics": "classified only from signals the run already produced (status, the failing step or node, error codes and messages); a failure it cannot tie to a documented signal is kind unknown with confidence low and a host must show it as an unknown cause. phase is the failing step id (engine 1) or node path (engine 2); rounds counts visits of correction steps; resetAt is the provider's own wording of the reset time, never reformatted; detail is one bounded sentence from the run's own error; resumablePhase is the step runtime resume continues from when resuming can help (blocked runs and transient kinds), absent for a plain failed verification.",
339
+ "kindMeaning": {
340
+ "tests_failed": "verification kept failing, or the correction loop stopped on the same failing change",
341
+ "provider_limit": "the provider reported a session, usage, rate or quota limit",
342
+ "environment": "the host environment cannot run verification (credentials, toolchain, setup command, non-deterministic generated output, edits outside the scope)",
343
+ "blocked": "a configured limit stopped the run: baseline characterization, attempt or budget limits, a reviewer rejecting the same change",
344
+ "timeout": "the total execution or active-duration limit elapsed",
345
+ "tool_inactivity": "the agent produced no tool call, reply or usage within the idle limit",
346
+ "crash": "the provider process, endpoint, lease or storage failed under the run",
347
+ "unknown": "no documented signal matched"
348
+ },
349
+ "compatibility": "additive: hosts ignore an absent field; an older Core never emits it and the host derives a low-confidence diagnosis from the error text",
350
+ "documentation": "docs/agent-runtime.md#failure-classification"
351
+ },
352
+ "findingLocations": {
353
+ "version": 1,
354
+ "capability": "findingLocations",
355
+ "record": "acceptance.findingLocations[] = {finding, path, line?, endLine?}",
356
+ "location": "state.json acceptance.findingLocations in the run's pipeline directory, next to acceptance.findings (which stays string[])",
357
+ "semantics": "finding is the zero-based index into acceptance.findings; path is repository-relative with forward slashes (never absolute, never containing ..); line and endLine are 1-based and optional (endLine only with line, greater than it). Core drops a malformed anchor instead of failing the review, keeps at most 50 and one per finding, and never invents a location: a finding without an entry is unanchored.",
358
+ "compatibility": "additive: hosts ignore an absent field and render the finding unanchored; an older Core never emits it",
359
+ "documentation": "docs/agent-runtime.md#finding-locations"
360
+ },
361
+ "pauseRuns": {
362
+ "version": 1,
363
+ "capability": "pauseRuns",
364
+ "cli": "specrails-core runtime pause --context <json> | --run-dir <dir> [--request-id <id>] [--reason <token>]; engine 2 runs only; always exits 0 with one line {type: runtime-pause-result, runId, requestId, reason, outcome, status, createdAt?}",
365
+ "reason": "lowercase token matching ^[a-z][a-z0-9-]{0,63}$, default requested (Desktop's daily budget guard sends budget)",
366
+ "outcomes": [
367
+ "requested",
368
+ "paused",
369
+ "already-paused",
370
+ "not-running",
371
+ "terminal"
372
+ ],
373
+ "semantics": "requested: the run executes under a live lease; the idempotent inbox request (same request id and reason returns the original record, a different reason is control_conflict) lets every admitted attempt finish and commit, refuses the next admission, settles status paused and releases the lease (the run process exits 2). paused: an ownerless run still marked running is settled paused directly. already-paused, not-running (failed, interrupted, blocked) and terminal (succeeded, cancelled, completed) change nothing. A run of another Core package is refused with resume_incompatible.",
374
+ "status": "runtime status and runtime-result state.pause {requestId, reason, at} while a requested pause holds the run (no pendingInterrupts); events pause_requested {requestId, reason} and workflow_paused {requestId, reason}",
375
+ "resume": "runtime resume continues a paused run from its last committed step without repeating it and discards pause requests that arrived before the resume",
376
+ "documentation": "docs/agent-runtime.md#pausing-a-run"
377
+ },
257
378
  "configSchemaPath": "schemas/agent-runtime.schema.json",
258
379
  "configSchemaExport": "specrails-core/schemas/agent-runtime.schema.json",
259
380
  "projectConfigPath": ".specrails/agent-runtime.json",
@@ -291,10 +412,10 @@
291
412
  },
292
413
  "recovery": "Explicit resume uses frozen configuration and revalidates Core evidence; interrupted writes require named recovery; grants persist until evidence invalidation.",
293
414
  "delivery": "Core implements through archive; the host owns worktrees, commits, pushes, PRs and backlog delivery. A standalone continuation does not restart Desktop delivery.",
294
- "specificationFormat": "Architect specs contain complete main-spec replacements, including unchanged requirements; archive does not merge partial delta snippets.",
415
+ "specificationFormat": "Architect specs are OpenSpec delta specs authored through the official fast-forward skill (ADDED/MODIFIED/REMOVED/RENAMED requirements); existing requirements are preserved through delta semantics and merged into main specs by OpenSpec archive. Complete replacement specs are not fabricated.",
295
416
  "compatibility": "Additive to integration schema 4 and execution context schema 1. Legacy workflows and profile v1 remain supported. Hosts must check RUNTIME_API_VERSION, not infer runtime support from Core major version.",
296
417
  "documentation": "docs/agent-runtime.md",
297
- "instructionsVersion": "14",
418
+ "instructionsVersion": "17",
298
419
  "workflowVersion": "7",
299
420
  "engines": [
300
421
  1,
@@ -303,7 +424,7 @@
303
424
  "engine": {
304
425
  "version": 2,
305
426
  "definitionSchema": "schemas/workflow-definition.schema.json",
306
- "nodeKindsVersion": 7
427
+ "nodeKindsVersion": 8
307
428
  },
308
429
  "nodeKinds": [
309
430
  "prompt",
@@ -324,7 +445,8 @@
324
445
  "component",
325
446
  "map",
326
447
  "implementation",
327
- "join"
448
+ "join",
449
+ "derive-change"
328
450
  ],
329
451
  "implementationSteps": {
330
452
  "version": 1,
@@ -346,6 +468,47 @@
346
468
  "structuredOutput": "Claude receives --json-schema and Codex --output-schema for architect/reviewer replies; other providers are parsed leniently.",
347
469
  "resumeBudget": "An explicit resume resets the transition ceiling and grants the developer a fresh maxAttempts budget.",
348
470
  "defaultImplementationEngine": true,
471
+ "deriveChange": {
472
+ "version": 1,
473
+ "capability": "deriveChange",
474
+ "nodeKind": "derive-change",
475
+ "role": "deriver",
476
+ "roleAssignment": "Core engine role: access read, artifacts all, openspecSkill openspec-ff-change; takes agents.architect's provider assignment; instructions are Core's and the id is reserved for custom roles",
477
+ "params": {
478
+ "change": "required; the run's frozen change name ({{run.changeId}})",
479
+ "summaryPath": "optional; file relative to <backlogRoot>/.specrails/pipeline/<runId>/, at most 32768 bytes, no symlinks or .. segments; absent or empty derives from the diff alone; optional front matter 'transcriptTurns: <n>'",
480
+ "feedback": "optional string (at most 32000 characters, normally {{run.latestFindings}}); when non-blank the deriver re-derives the existing change in place after a correction round, with the findings bounded to 8000 characters as untrusted fenced data under '## Re-derivation after a correction round'",
481
+ "timeoutMs": "optional",
482
+ "idleTimeoutMs": "optional"
483
+ },
484
+ "outcomes": [
485
+ "pass",
486
+ "fail"
487
+ ],
488
+ "diff": "change-scope changeSet: repositories[].baseSha (or HEAD at first measurement) to the working tree; untracked files present at the first measurement are pre-existing; .specrails/ and the change's own openspec/changes/<change>/ are excluded",
489
+ "emptyChangeSet": "node fails with error code empty_change_set before any provider call",
490
+ "derivationArtifact": "<backlogRoot>/.specrails/pipeline/<runId>/agent-workflow/artifacts/<scope-node digest>/derivation.json; the absolute path is output.derivationPath and the report is output.derivation",
491
+ "derivationFields": [
492
+ "schemaVersion",
493
+ "change",
494
+ "base",
495
+ "head",
496
+ "files",
497
+ "explained",
498
+ "unexplainedFiles",
499
+ "tasks",
500
+ "requirementsAdded",
501
+ "requirementsModified",
502
+ "confidence",
503
+ "transcriptTurnsUsed",
504
+ "repositories",
505
+ "unexplainedOmitted"
506
+ ],
507
+ "verificationProposals": "output.structured.verification (at most 20, architect schema); bind them with verify additionalCommandsFrom: <derive node id>",
508
+ "reviewer": "A reviewer role-turn in the same run receives derivation.json; every unexplained file is an issue",
509
+ "correctionLoop": "role-turn stance 'fixer' (write roles only; the built-in developer takes the FIXER instructions, rolePrompts.fixer and config.fixer in a fresh session). flow-spec routes a rejected review or a failing check to at most correctionLimit (2) rounds of fix then derive (feedback) then check then review; a fixer turn that leaves $outputs.fix.candidateHash equal to $outputs.check.candidateHash, or a spent budget, ends at end node changes-requested with reason 'Flow → Spec: the reviewer requested changes that <n> correction round(s) did not resolve. Latest findings: <findings>'",
510
+ "referenceDefinition": "src/agent-runtime/engine/__fixtures__/flow-spec.json"
511
+ },
349
512
  "openRoles": {
350
513
  "version": 1,
351
514
  "builtinRoles": [
@@ -412,8 +575,15 @@
412
575
  ],
413
576
  "capabilities": [
414
577
  "sessions",
415
- "delegation"
416
- ]
578
+ "delegation",
579
+ "flowSessions"
580
+ ],
581
+ "deliveries": [
582
+ "queue",
583
+ "steer",
584
+ "interrupt"
585
+ ],
586
+ "interruptDelivery": "session.send delivery 'interrupt' cuts the running turn (turn.completed status 'interrupted', partial text kept) and runs the input as the next turn, ahead of inputs queued before it; on an idle session it behaves like 'queue'. Drivers declare capabilities.interrupt 'native' (provider primitive) or 'emulated'; runtime api advertises flowSessions"
417
587
  }
418
588
  },
419
589
  "execution": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-core",
3
- "version": "6.6.4",
3
+ "version": "6.7.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": {
@@ -82,23 +82,23 @@
82
82
  "test:coverage": "npm run build && vitest run --coverage",
83
83
  "dogfood": "npm run build && node bin/specrails-core.mjs init --yes",
84
84
  "prepack": "npm run build",
85
- "test:scripts": "node --test scripts/release-utils.test.mjs scripts/ci-tests.test.mjs scripts/evaluate-definition-corpus.test.mjs",
85
+ "test:scripts": "node --test scripts/release-utils.test.mjs scripts/ci-tests.test.mjs scripts/evaluate-definition-corpus.test.mjs scripts/contract-policy.test.mjs",
86
86
  "test:engine-spikes": "npm run build && node scripts/run-engine-spikes.mjs",
87
87
  "check:package": "npm run build && node scripts/verify-package.mjs",
88
88
  "ci": "npm run typecheck && npm run test:scripts && npm run test:coverage && npm run check:package"
89
89
  },
90
90
  "dependencies": {
91
- "@fission-ai/openspec": "1.4.1",
92
- "@langchain/core": "1.2.14",
93
- "@langchain/langgraph": "1.4.18",
94
- "@langchain/langgraph-checkpoint": "1.1.5",
95
- "@modelcontextprotocol/sdk": "1.31.0",
91
+ "@fission-ai/openspec": "1.14.3",
92
+ "@langchain/core": "1.2.18",
93
+ "@langchain/langgraph": "1.5.1",
94
+ "@langchain/langgraph-checkpoint": "1.2.0",
95
+ "@modelcontextprotocol/sdk": "1.32.1",
96
96
  "ajv": "^8.20.0",
97
97
  "cross-spawn": "7.0.6",
98
98
  "js-yaml": "^5.4.3",
99
99
  "picocolors": "^1.1.1",
100
100
  "semver": "^7.8.5",
101
- "zod": "4.6.5"
101
+ "zod": "4.6.7"
102
102
  },
103
103
  "devDependencies": {
104
104
  "@types/cross-spawn": "6.0.6",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "$comment": "Pinned versions of third-party tools the installer fetches via npx. Edit and commit to bump deliberately.",
3
- "openspec": "1.4.1"
3
+ "openspec": "1.14.3"
4
4
  }
@@ -137,6 +137,7 @@
137
137
  "verification": { "type": "array", "items": { "$ref": "#/definitions/checkCommand" } },
138
138
  "setup": { "type": "array", "description": "Idempotent commands the verify piece runs sequentially in the admitted workspace before each verification plan; a failing setup command blocks the run instead of starting a correction round.", "items": { "$ref": "#/definitions/checkCommand" } },
139
139
  "approvalBeforeArchive": { "type": "boolean" },
140
+ "baselineOnTouch": { "type": "boolean", "description": "When the blast radius reaches code no capability covers (openspec/capability-map.json or a path named in openspec/specs), the architect adds a Baseline task group (characterization tests plus a minimal as-built spec) whose tests must pass on the unchanged code before the developer touches the module. Default false." },
140
141
  "review": { "type": "object", "additionalProperties": false, "description": "Review gate thresholds; unset fields keep Core's defaults (score 70, security 75, other aspects 60).", "properties": {
141
142
  "minScore": { "$ref": "#/definitions/score" },
142
143
  "aspects": { "type": "object", "additionalProperties": false, "properties": {
@@ -145,7 +146,8 @@
145
146
  } }
146
147
  } },
147
148
  "architect": { "type": "object", "additionalProperties": false, "properties": {
148
- "onLowConfidence": { "enum": ["ask", "proceed"], "description": "After one autonomous investigation pass, a still-low design either pauses with its question (ask, default) or proceeds on stated assumptions (proceed)." }
149
+ "onLowConfidence": { "enum": ["ask", "proceed"], "description": "After one autonomous investigation pass, a still-low design either pauses with its question (ask, default) or proceeds on stated assumptions (proceed)." },
150
+ "clarifications": { "enum": ["ask-on-high", "always-proceed"], "description": "Before planning, a high-impact clarification without a safe default either pauses the run with its question (ask-on-high, default) or the architect proceeds on stated assumptions written under Assumptions in design.md (always-proceed)." }
149
151
  } }
150
152
  },
151
153
  "definitions": {
@@ -251,6 +251,7 @@
251
251
  "component",
252
252
  "implementation",
253
253
  "implementation-step",
254
+ "derive-change",
254
255
  "artifact-contract",
255
256
  "end"
256
257
  ]
@@ -10,8 +10,9 @@ model = "{{MODEL_NAME}}"
10
10
  model_reasoning_effort = "medium"
11
11
 
12
12
  # Sandbox mode applied to interactive `codex` sessions launched from this
13
- # project. The desktop app passes `--sandbox workspace-write` per-spawn so this
14
- # setting only affects manual terminal use of codex inside this repo.
13
+ # project. specrails runtime spawns set their own sandbox per invocation
14
+ # (`danger-full-access` for write roles, `read-only` for read-only roles), so
15
+ # this setting only affects manual terminal use of codex inside this repo.
15
16
  # Values: "read-only" | "workspace-write" | "danger-full-access".
16
17
  sandbox_mode = "workspace-write"
17
18