specrails-core 6.6.4 → 6.6.5

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.
@@ -1,29 +1,14 @@
1
- export interface ArchiveRequirementFinding {
2
- path: string;
3
- line: number;
4
- requirement: string;
5
- }
6
1
  export type ArchivePreflight = {
7
2
  valid: true;
8
3
  } | {
9
4
  valid: false;
10
5
  reason: string;
11
- findings: ArchiveRequirementFinding[];
12
6
  };
13
- /**
14
- * Requirements OpenSpec rejects when it rebuilds a main spec on archive: its spec parser reads only
15
- * the first non-empty body line before any heading and needs SHALL or MUST there. Delta validation
16
- * skips `**Key**:` metadata lines, so a delta (or a pre-existing main-spec requirement) can pass
17
- * `openspec validate` and still abort the archive.
18
- */
19
- export declare function requirementsWithoutKeyword(markdown: string): Array<{
20
- line: number;
21
- requirement: string;
22
- }>;
23
7
  /**
24
8
  * Dry-runs `openspec archive` on a disposable copy of `openspec/`, so a change that validates but
25
- * cannot be archived is caught while its author can still edit the plan. Nothing in the
26
- * artifact root is written.
9
+ * cannot be archived is caught while its author can still edit the plan. Delta validation does not
10
+ * check deltas against the main specs; archive rejects a MODIFIED or RENAMED requirement the main
11
+ * spec lacks and an ADDED one it already has. Nothing in the artifact root is written.
27
12
  */
28
13
  export declare function previewOpenSpecArchive(artifactRoot: string, change: string, signal?: AbortSignal): Promise<ArchivePreflight>;
29
14
  /** Plain-text guidance for the planning role's history. */
@@ -1,36 +1,8 @@
1
- import { cpSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
1
+ import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { artifactPath, OpenSpecCommandError, resolveOpenSpecCli, runOpenSpec } from '../openspec.js';
5
5
  const ANSI = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
6
- /**
7
- * Requirements OpenSpec rejects when it rebuilds a main spec on archive: its spec parser reads only
8
- * the first non-empty body line before any heading and needs SHALL or MUST there. Delta validation
9
- * skips `**Key**:` metadata lines, so a delta (or a pre-existing main-spec requirement) can pass
10
- * `openspec validate` and still abort the archive.
11
- */
12
- export function requirementsWithoutKeyword(markdown) {
13
- const lines = markdown.split('\n'), findings = [];
14
- for (let index = 0; index < lines.length; index++) {
15
- const header = lines[index].match(/^###\s+Requirement:\s*(.*)$/);
16
- if (!header)
17
- continue;
18
- let body;
19
- for (let next = index + 1; next < lines.length; next++) {
20
- const text = lines[next].trim();
21
- if (text.startsWith('#'))
22
- break;
23
- if (text) {
24
- body = next;
25
- break;
26
- }
27
- }
28
- const text = body === undefined ? header[1] : lines[body];
29
- if (!text.includes('SHALL') && !text.includes('MUST'))
30
- findings.push({ line: (body ?? index) + 1, requirement: header[1].trim() });
31
- }
32
- return findings;
33
- }
34
6
  function assertNoSymlinks(root) {
35
7
  for (const entry of readdirSync(root, { withFileTypes: true })) {
36
8
  if (entry.isSymbolicLink())
@@ -39,26 +11,11 @@ function assertNoSymlinks(root) {
39
11
  assertNoSymlinks(path.join(root, entry.name));
40
12
  }
41
13
  }
42
- function capabilityFindings(artifactRoot, change) {
43
- const deltas = path.join(artifactRoot, 'openspec/changes', change, 'specs');
44
- if (!existsSync(deltas))
45
- return [];
46
- const findings = [];
47
- for (const capability of readdirSync(deltas).sort()) {
48
- for (const relative of [`openspec/changes/${change}/specs/${capability}/spec.md`, `openspec/specs/${capability}/spec.md`]) {
49
- const file = path.join(artifactRoot, relative);
50
- if (!existsSync(file) || !lstatSync(file).isFile())
51
- continue;
52
- for (const finding of requirementsWithoutKeyword(readFileSync(file, 'utf8')))
53
- findings.push({ path: relative, ...finding });
54
- }
55
- }
56
- return findings;
57
- }
58
14
  /**
59
15
  * Dry-runs `openspec archive` on a disposable copy of `openspec/`, so a change that validates but
60
- * cannot be archived is caught while its author can still edit the plan. Nothing in the
61
- * artifact root is written.
16
+ * cannot be archived is caught while its author can still edit the plan. Delta validation does not
17
+ * check deltas against the main specs; archive rejects a MODIFIED or RENAMED requirement the main
18
+ * spec lacks and an ADDED one it already has. Nothing in the artifact root is written.
62
19
  */
63
20
  export async function previewOpenSpecArchive(artifactRoot, change, signal) {
64
21
  const source = artifactPath(artifactRoot, 'openspec');
@@ -80,7 +37,7 @@ export async function previewOpenSpecArchive(artifactRoot, change, signal) {
80
37
  if (archived && !existsSync(path.join(staging, 'openspec/changes', change)))
81
38
  return { valid: true };
82
39
  const reason = output.replace(ANSI, '').split('\n').map(line => line.trimEnd()).filter(Boolean).join('\n').slice(-2000);
83
- return { valid: false, reason, findings: capabilityFindings(artifactRoot, change) };
40
+ return { valid: false, reason };
84
41
  }
85
42
  finally {
86
43
  rmSync(staging, { recursive: true, force: true });
@@ -88,11 +45,9 @@ export async function previewOpenSpecArchive(artifactRoot, change, signal) {
88
45
  }
89
46
  /** Plain-text guidance for the planning role's history. */
90
47
  export function describeArchivePreflight(change, preflight) {
91
- const located = preflight.findings.map(finding => `- ${finding.path}:${finding.line} "${finding.requirement}"`).join('\n');
92
48
  return [
93
49
  `OpenSpec validates change ${change}, but archiving it would fail. OpenSpec's archive output:`,
94
50
  preflight.reason,
95
- ...(located ? ['Requirements whose first body line has no SHALL or MUST (OpenSpec reads only that line when it rebuilds a main spec):', located,
96
- 'Put the SHALL/MUST statement on the line right after "### Requirement: …". For a requirement that only exists in openspec/specs, restate it under "## MODIFIED Requirements" in this change\'s delta with the corrected first line.'] : []),
51
+ 'Reconcile each delta with openspec/specs: MODIFIED and RENAMED headers must name an existing requirement exactly, and ADDED requirements must not exist yet.',
97
52
  ].join('\n');
98
53
  }
@@ -195,7 +195,7 @@ async function archiveWithOpenSpec(context, change, active, signal, adapter) {
195
195
  // created specification. Preserve that provenance during fork replay;
196
196
  // authored text and all byte-for-byte publication guards stay intact.
197
197
  if (sourceChange && before === null && after !== null)
198
- after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n## Requirements\n`, `\n## Purpose\nTBD - created by archiving change ${sourceChange}. Update Purpose after archive.\n## Requirements\n`);
198
+ after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n`, `\n## Purpose\nTBD - created by archiving change ${sourceChange}. Update Purpose after archive.\n`);
199
199
  return { path: 'openspec/specs/' + name, before, after };
200
200
  }).filter(item => item.before !== item.after) };
201
201
  write(receipt, JSON.stringify(plan));
@@ -1,6 +1,6 @@
1
1
  import { type AgentRole, type CliProvider, type RoleDescriptor, type AgentAccess, type ArtifactAccess, type OpenSpecSkill } from './executor-types.js';
2
2
  import { z } from 'zod';
3
- export declare const OPENSPEC_VERSION = "1.4.1";
3
+ export declare const OPENSPEC_VERSION = "1.14.3";
4
4
  export declare const ROLE_SKILLS: {
5
5
  readonly architect: 'openspec-ff-change';
6
6
  readonly developer: 'openspec-apply-change';
@@ -7,8 +7,11 @@ import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { BUILTIN_ROLE_POLICY, isBuiltinRole } from './executor-types.js';
9
9
  import { z } from 'zod';
10
- export const OPENSPEC_VERSION = '1.4.1';
10
+ export const OPENSPEC_VERSION = '1.14.3';
11
11
  export const ROLE_SKILLS = { architect: 'openspec-ff-change', developer: 'openspec-apply-change', reviewer: 'openspec-verify-change' };
12
+ /** Where `openspec init --tools` writes each provider's skills (Codex and Kimi moved in 1.14). */
13
+ const OPENSPEC_SKILL_DIRECTORIES = { claude: '.claude', codex: '.agents', gemini: '.gemini', kimi: '.kimi-code' };
14
+ const DEFAULT_PROJECT_CONFIG = 'schema: spec-driven\n';
12
15
  /** Saved built-in contexts retain their original shape; new roles carry explicit policy. */
13
16
  export function openSpecPolicy(context) {
14
17
  const legacy = isBuiltinRole(context.role) ? BUILTIN_ROLE_POLICY[context.role] : undefined;
@@ -144,7 +147,7 @@ export function prepareOpenSpec(root, change, directory) {
144
147
  });
145
148
  for (const provider of ['claude', 'codex', 'gemini', 'kimi']) {
146
149
  for (const name of Object.values(ROLE_SKILLS)) {
147
- const content = readFileSync(path.join(target, '.' + provider, 'skills', name, 'SKILL.md'), 'utf8');
150
+ const content = readFileSync(path.join(target, OPENSPEC_SKILL_DIRECTORIES[provider], 'skills', name, 'SKILL.md'), 'utf8');
148
151
  const destination = path.join(skillRoot, provider, name);
149
152
  mkdirSync(destination, { recursive: true });
150
153
  writeFileSync(path.join(destination, 'SKILL.md'), content, { mode: 0o600 });
@@ -163,8 +166,11 @@ export function prepareOpenSpec(root, change, directory) {
163
166
  }
164
167
  if (existsSync(path.join(root, 'openspec/schemas/spec-driven')))
165
168
  throw new Error('Custom spec-driven schema overrides are not supported by this runtime');
166
- const config = artifactPath(root, 'openspec/config.yaml');
167
- identity.projectConfig = existsSync(config) ? hash(readFileSync(config, 'utf8')) : 'absent';
169
+ // Same precedence as OpenSpec. `new change` scaffolds DEFAULT_PROJECT_CONFIG when no config exists;
170
+ // it is equivalent to none, so the architect's first write does not change the run's identity.
171
+ const config = ['openspec/config.yaml', 'openspec/config.yml'].map(file => artifactPath(root, file)).find(file => existsSync(file));
172
+ const projectConfig = config ? readFileSync(config, 'utf8') : undefined;
173
+ identity.projectConfig = projectConfig === undefined || projectConfig === DEFAULT_PROJECT_CONFIG ? 'absent' : hash(projectConfig);
168
174
  return { cli, skillRoot, identity };
169
175
  }
170
176
  export function roleOpenSpecContext(prepared, root, change, directory, roleOrDescriptor, provider) {
@@ -349,7 +355,7 @@ export const OPENSPEC_TOOL_DEFINITION = { type: 'function', function: { name: 'o
349
355
  export function openSpecPrompt(context) {
350
356
  if (!isBuiltinRole(context.role))
351
357
  return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. Load action=load_skill using openspec_workflow and follow the pinned official skill. Use scoped tools for status, instructions, validation and permitted artifact writes. Workspace access: ${openSpecPolicy(context).access}; artifacts: ${openSpecPolicy(context).artifacts}. Read required artifact instructions before writing. Never archive or coordinate another role.\n`;
352
- return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. This headless transport loads the official, version-pinned skill through the openspec_workflow tool (MCP server specrails_openspec, tool workflow on CLI providers). This is explicit skill-document adaptation, not a native slash command.\nFirst call action=load_skill and follow the returned complete official skill. Bind its openspec CLI commands to this tool: new change -> action=new; status -> status; instructions <artifact> -> instructions with artifact; validate -> validate. Author each artifact yourself using write_artifact with its change-relative path and content; read the official instructions first. Do not reproduce templates from memory or return the artifacts in final JSON. The tool fixes the change identity and runs OpenSpec ${OPENSPEC_VERSION}.\nRead source/dependencies with your normal read tools. For this frozen scope, change selection is already answered. Bind AskUserQuestion to a low-confidence final result with question (architect), or a reported blocking issue (other roles); LangGraph asks the requester. Bind TodoWrite to concise progress narration and, for developer, persist the current handoff through action=write_progress with progress {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Save after each completed task or changed blocker, at most 8 KB. This host journal is separate from frozen OpenSpec artifacts; never write runtime files directly. Developer/reviewer receive savedProgress from load_skill and can refresh with read_progress. Reconcile this advisory history with current files; old test outcomes never replace required host verification. Do not invoke another role, apply/archive from architect, or archive from developer/reviewer. Return the Specrails JSON report after the official workflow. For developer and reviewer, load_skill already executes status and instructions apply and returns their real outputs in planning; read planning.apply.contextFiles and perform the rest of the official skill. You may refresh instructions apply when needed. Verify is a skill, not an instructions artifact.\n`;
358
+ return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. This headless transport loads the official, version-pinned skill through the openspec_workflow tool (MCP server specrails_openspec, tool workflow on CLI providers). This is explicit skill-document adaptation, not a native slash command.\nFirst call action=load_skill and follow the returned complete official skill. Bind its openspec CLI commands to this tool: new change -> action=new; status -> status; instructions <artifact> -> instructions with artifact; validate -> validate. Author each artifact yourself using write_artifact with its change-relative path and content; read the official instructions first. Do not reproduce templates from memory or return the artifacts in final JSON. The tool fixes the change identity and runs OpenSpec ${OPENSPEC_VERSION}.\nRead source/dependencies with your normal read tools. For this frozen scope, change selection is already answered, and so are the skill's store selection and project check: no store is selected (never pass --store or run store commands), and the host has admitted this repository as the OpenSpec project, so action=new may create its repo-local openspec/ root; do not run openspec list to check for one. Bind AskUserQuestion to a low-confidence final result with question (architect), or a reported blocking issue (other roles); LangGraph asks the requester. Bind TodoWrite to concise progress narration and, for developer, persist the current handoff through action=write_progress with progress {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Save after each completed task or changed blocker, at most 8 KB. This host journal is separate from frozen OpenSpec artifacts; never write runtime files directly. Developer/reviewer receive savedProgress from load_skill and can refresh with read_progress. Reconcile this advisory history with current files; old test outcomes never replace required host verification. Do not invoke another role, apply/archive from architect, or archive from developer/reviewer. Return the Specrails JSON report after the official workflow. For developer and reviewer, load_skill already executes status and instructions apply and returns their real outputs in planning; read planning.apply.contextFiles and perform the rest of the official skill. You may refresh instructions apply when needed. Verify is a skill, not an instructions artifact.\n`;
353
359
  }
354
360
  export function writeOpenSpecBridge(context, directory) {
355
361
  const file = path.join(directory, 'openspec-context.json');
@@ -340,7 +340,7 @@ function readPinnedVersion(scriptDir, key, fallback) {
340
340
  * 3. neither set → `runCommand('npx', ['--yes', '-p', '@fission-ai/openspec@<pinned>', '--', 'openspec', 'init', …])`
341
341
  * (default online path — users never need a global install).
342
342
  */
343
- export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, pinnedVersion = readPinnedVersion(resolveScriptDir(), 'openspec', '1.4.1')) {
343
+ export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, pinnedVersion = readPinnedVersion(resolveScriptDir(), 'openspec', '1.14.3')) {
344
344
  const bin = env.SPECRAILS_OPENSPEC_BIN;
345
345
  const node = env.SPECRAILS_OPENSPEC_NODE;
346
346
  const initArgs = ['init', '--tools', provider, '--profile', 'custom', repoRoot];
@@ -1,6 +1,6 @@
1
1
  import { type Provider, type ProviderAvailability } from './provider-detect.js';
2
2
  /**
3
- * OpenSpec 1.4.1, which the installer invokes during the default init/update
3
+ * OpenSpec 1.14.3, which the installer invokes during the default init/update
4
4
  * flow, requires Node >=20.19.0. Keep this floor independent from Kimi's npm
5
5
  * package requirement: SpecRails launches an externally installed Kimi CLI and
6
6
  * does not require the Kimi npm distribution.
@@ -6,7 +6,7 @@ import { info, ok, warn } from '../util/logger.js';
6
6
  import { gitInstalled, initRepo, isGitRepo, repoRoot } from '../util/git.js';
7
7
  import { assertClaudeAuthenticated, assertKimiAuthenticated, claudeVersion, detectAvailability, isSupportedKimiVersion, kimiVersion, MIN_KIMI_VERSION, resolveProvider, } from './provider-detect.js';
8
8
  /**
9
- * OpenSpec 1.4.1, which the installer invokes during the default init/update
9
+ * OpenSpec 1.14.3, which the installer invokes during the default init/update
10
10
  * flow, requires Node >=20.19.0. Keep this floor independent from Kimi's npm
11
11
  * package requirement: SpecRails launches an externally installed Kimi CLI and
12
12
  * does not require the Kimi npm distribution.
@@ -34,7 +34,7 @@ export async function checkPrerequisites(options) {
34
34
  const nodeVersion = process.versions.node;
35
35
  if (!isSupportedNodeVersion(nodeVersion)) {
36
36
  throw new PrerequisiteError(`Node.js ${nodeVersion} is unsupported. SpecRails requires Node.js ` +
37
- `${MIN_NODE_VERSION} or newer because OpenSpec 1.4.1 uses that runtime floor. ` +
37
+ `${MIN_NODE_VERSION} or newer because OpenSpec 1.14.3 uses that runtime floor. ` +
38
38
  'Install a supported Node.js release from https://nodejs.org/');
39
39
  }
40
40
  ok(`Node.js: ${nodeVersion}`);
@@ -125,9 +125,8 @@ export function scaffoldInstallation(input) {
125
125
  // --- Directory skeleton ---
126
126
  mk(path.join(input.artifactRoot, input.providerDir));
127
127
  if (input.provider === 'codex') {
128
- // Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
129
- // The pre-§18 code wrote to `.agents/skills/` which codex doesn't read;
130
- // that was a placeholder name from the gated state.
128
+ // Specrails' Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
129
+ // OpenSpec's own Codex skills live under `.agents/skills/`.
131
130
  mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
132
131
  }
133
132
  else if (input.provider === 'gemini') {
@@ -1039,9 +1038,7 @@ function pruneLegacyArtifacts(input) {
1039
1038
  path.join(input.artifactRoot, '.specrails-version'),
1040
1039
  ];
1041
1040
  if (input.provider === 'codex') {
1042
- // Pre-§18 layout used `.agents/skills/` — prune any leftovers from a
1043
- // legacy install before settling on the canonical `.codex/skills/`.
1044
- legacyPaths.push(path.join(input.artifactRoot, '.agents'));
1041
+ // `.agents/skills/` is not pruned: OpenSpec 1.14+ writes its Codex skills there.
1045
1042
  legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills', 'setup'));
1046
1043
  }
1047
1044
  else if (input.provider === 'gemini') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-core",
3
- "version": "6.6.4",
3
+ "version": "6.6.5",
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": {
@@ -88,7 +88,7 @@
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",
91
+ "@fission-ai/openspec": "1.14.3",
92
92
  "@langchain/core": "1.2.14",
93
93
  "@langchain/langgraph": "1.4.18",
94
94
  "@langchain/langgraph-checkpoint": "1.1.5",
@@ -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
  }