Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/explore-project-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---

Explore now reads the project's context and rules from `openspec/config.yaml` (or `config.yml`) at the start of a session, so it reasons with the same tech stack and conventions the artifact-creating workflows already receive.
6 changes: 6 additions & 0 deletions skills/openspec-explore/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ This tells you:
- Their names, schemas, and status
- What the user might be working on

Then read the project's own context from the resolved root - `<root.path>/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists:
- `context`: project background - tech stack, conventions, constraints
- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact

Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.

### When no change exists

Think freely. When insights crystallize, you might offer:
Expand Down
12 changes: 12 additions & 0 deletions src/core/templates/workflows/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ This tells you:
- Their names, schemas, and status
- What the user might be working on

Then read the project's own context from the resolved root - \`<root.path>/openspec/config.yaml\` (or \`config.yml\`). Use the \`root.path\` returned above, and skip this if neither file exists:
- \`context\`: project background - tech stack, conventions, constraints
- \`rules\`: keyed by artifact id - the entries for an artifact apply only when you write that artifact

Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.

### When no change exists

Think freely. When insights crystallize, you might offer:
Expand Down Expand Up @@ -392,6 +398,12 @@ This tells you:
- Their names, schemas, and status
- What the user might be working on

Then read the project's own context from the resolved root - \`<root.path>/openspec/config.yaml\` (or \`config.yml\`). Use the \`root.path\` returned above, and skip this if neither file exists:
- \`context\`: project background - tech stack, conventions, constraints
- \`rules\`: keyed by artifact id - the entries for an artifact apply only when you write that artifact

Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create.

If the user mentioned a specific change name, read its artifacts for context.

### When no change exists
Expand Down
68 changes: 68 additions & 0 deletions test/core/templates/explore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest';

import {
getExploreSkillTemplate,
getOpsxExploreCommandTemplate,
} from '../../../src/core/templates/skill-templates.js';

const skill = getExploreSkillTemplate();
const command = getOpsxExploreCommandTemplate();

// Both delivery surfaces must carry the same contract; every behavioral
// assertion below runs against each body.
const bodies: Array<[string, string]> = [
['skill', skill.instructions],
['command', command.content],
];

describe('explore templates', () => {
// Regression for #696: explore never loaded the project's declared
// context, so it reasoned without the tech stack, conventions, and
// rules every artifact-creating workflow already receives.
it('loads project context from the OpenSpec config at startup (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec/config.yaml');
expect(body, label).toContain('`context`: project background');
expect(body, label).toContain('`rules`: keyed by artifact id');
}
});

it('resolves the config through the reported root rather than assuming a repo-local path (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec list --json');
expect(body, label).toContain('<root.path>/openspec/config.yaml');
expect(body, label).toContain('root.path');
}
});
Comment on lines +22 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that config loading follows openspec list --json.

These assertions only verify independent substrings; they would pass if the config instructions appeared before root resolution or were unrelated to the openspec list --json step. Assert the relative ordering using indexOf(...) so the startup contract is protected.

+      const listIndex = body.indexOf('openspec list --json');
+      const configIndex = body.indexOf('<root.path>/openspec/config.yaml');
+      expect(configIndex).toBeGreaterThan(listIndex);

Run with pnpm exec vitest run test/core/templates/explore.test.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('loads project context from the OpenSpec config at startup (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec/config.yaml');
expect(body, label).toContain('`context`: project background');
expect(body, label).toContain('`rules`: keyed by artifact id');
}
});
it('resolves the config through the reported root rather than assuming a repo-local path (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec list --json');
expect(body, label).toContain('<root.path>/openspec/config.yaml');
expect(body, label).toContain('root.path');
}
});
it('loads project context from the OpenSpec config at startup (`#696`)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec/config.yaml');
expect(body, label).toContain('`context`: project background');
expect(body, label).toContain('`rules`: keyed by artifact id');
}
});
it('resolves the config through the reported root rather than assuming a repo-local path (`#696`)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('openspec list --json');
expect(body, label).toContain('<root.path>/openspec/config.yaml');
expect(body, label).toContain('root.path');
const listIndex = body.indexOf('openspec list --json');
const configIndex = body.indexOf('<root.path>/openspec/config.yaml');
expect(configIndex).toBeGreaterThan(listIndex);
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/templates/explore.test.ts` around lines 22 - 36, Strengthen the
assertions in the tests covering startup config loading so they verify ordering,
not just independent substrings: use indexOf(...) to assert that “openspec list
--json” and root.path resolution occur before the openspec/config.yaml
instructions. Update the relevant test cases in the explore template suite while
preserving the existing checks and run the specified Vitest test.


// resolveConfigFilePath() probes config.yaml then config.yml, and
// `openspec init` leaves a .yml project on .yml forever - naming only
// .yaml would silently skip context for those projects.
it('accepts config.yml as well as config.yaml (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('config.yml');
expect(body, label).toContain('skip this if neither file exists');
}
});

// `rules` is Record<artifactId, string[]>; explore holds no artifact at
// startup, so the guidance must not invite blanket application.
it('scopes rules to the artifact they are keyed to (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain(
'the entries for an artifact apply only when you write that artifact'
);
}
});

// House style across instructions.ts and the sibling workflow templates
// forbids leaking context/rules into the artifact, not just the chat.
it('treats project context as constraints that must not leak into output (#696)', () => {
for (const [label, body] of bodies) {
expect(body, label).toContain('constraints for you to follow');
expect(body, label).toContain(
'do NOT copy them into the conversation or into any artifact you create'
);
}
});
});
6 changes: 3 additions & 3 deletions test/core/templates/skill-templates-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ import {
import { STORE_SELECTION_GUIDANCE } from '../../../src/core/templates/workflows/store-selection.js';

const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getExploreSkillTemplate: '7d2f54e74fffcb36aaaa4498a4a8b033142bb25945fb9b2de532354acbe76b9c',
getExploreSkillTemplate: 'a7eb6fabdc05a5b90a4773ba93320a60edffea88e9b27985668a2959dcec2e3d',
getNewChangeSkillTemplate: '39663a6d2037e6697020393a66f6327506e3e3bc573b7a3556dcb7f9457dc51d',
getContinueChangeSkillTemplate: 'acc07a489a30192b4bf2bbdc587a889478fbf6fffbbc9353c7775c4ca1ec5011',
getApplyChangeSkillTemplate: '0f5a15fc7fb9ad6059a5643d0e01365d27642637a4aaebf182f9eabb45348197',
getFfChangeSkillTemplate: '20ebb682ba89809a100cd4985c074908df5bada2bd649ca1b0f4059a63a1c728',
getSyncSpecsSkillTemplate: 'dc07ea0312687f3edc602329c889dbbab737c6d79327eb7a723553d346b43433',
getOnboardSkillTemplate: 'bc2216b72724b01c3a733e63b8bf4aff457f561c0e9ff7288bdacc39780a37a7',
getOpsxExploreCommandTemplate: '37e53590aae7ac6621d4393aa80a5b8af21881323887fa924ed329199fda27e0',
getOpsxExploreCommandTemplate: 'eef1f8b4fd90ade6d70be46f0f8c3e6722f221fed175a6f9cf626287ef504a94',
getOpsxNewCommandTemplate: '57c600cce318d16b9b4308a18d0d983ea3c0673034e606a7cceec07b4c705e87',
getOpsxContinueCommandTemplate: 'f63964fab7720ede097aa48808baff196c391b962930ca960459205c724800e5',
getOpsxApplyCommandTemplate: 'daeb507206707169de73c828e199648dde5732cbc17791ef2a027adffd028574',
Expand All @@ -65,7 +65,7 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
};

const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-explore': 'ba099821631ce75ee70af370917bbddbc88d0882ad0e50e91ed687d2185102ef',
'openspec-explore': 'c8de6033b2c78009647647c65a504e4ada1a3bdcee31aed38a4bf7d629513f6e',
'openspec-new-change': 'd5b8909bea70a33b7a312b38ce204a91f40b6bb2bff12c4c06b3e11641b6a689',
'openspec-continue-change': 'bdb8bbb6a768a741b05256effbc284d65ac6a45360b59c24b94198792d3d0ebf',
'openspec-apply-change': '09c0e1cdf5ccc82416d0969d6bd715cc70616bdbc3531358a5c36057f78be55a',
Expand Down
Loading