aios-core 4.2.5 → 4.2.7

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 (32) hide show
  1. package/.aios-core/core/code-intel/code-intel-client.js +280 -0
  2. package/.aios-core/core/code-intel/code-intel-enricher.js +159 -0
  3. package/.aios-core/core/code-intel/index.js +137 -0
  4. package/.aios-core/core/code-intel/providers/code-graph-provider.js +201 -0
  5. package/.aios-core/core/code-intel/providers/provider-interface.js +108 -0
  6. package/.aios-core/core/orchestration/context-manager.js +333 -5
  7. package/.aios-core/core/orchestration/dashboard-integration.js +17 -1
  8. package/.aios-core/core/orchestration/execution-profile-resolver.js +107 -0
  9. package/.aios-core/core/orchestration/index.js +3 -0
  10. package/.aios-core/core/orchestration/skill-dispatcher.js +2 -0
  11. package/.aios-core/core/orchestration/subagent-prompt-builder.js +2 -0
  12. package/.aios-core/core/orchestration/workflow-orchestrator.js +113 -5
  13. package/.aios-core/data/entity-registry.yaml +44 -22
  14. package/.aios-core/development/agents/ux-design-expert.md +1 -1
  15. package/.aios-core/development/checklists/brownfield-compatibility-checklist.md +114 -0
  16. package/.aios-core/development/scripts/workflow-state-manager.js +128 -1
  17. package/.aios-core/development/tasks/next.md +36 -5
  18. package/.aios-core/hooks/ids-post-commit.js +29 -1
  19. package/.aios-core/hooks/ids-pre-push.js +29 -1
  20. package/.aios-core/infrastructure/contracts/compatibility/aios-4.0.4.yaml +44 -0
  21. package/.aios-core/infrastructure/scripts/validate-parity.js +238 -2
  22. package/.aios-core/install-manifest.yaml +63 -27
  23. package/.aios-core/product/templates/brownfield-risk-report-tmpl.yaml +277 -0
  24. package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +114 -5
  25. package/LICENSE +13 -1
  26. package/README.md +39 -9
  27. package/package.json +8 -6
  28. package/packages/installer/src/wizard/ide-config-generator.js +0 -117
  29. package/packages/installer/src/wizard/index.js +2 -118
  30. package/packages/installer/src/wizard/pro-setup.js +58 -12
  31. package/pro/license/license-api.js +9 -9
  32. package/scripts/semantic-lint.js +190 -0
@@ -17,13 +17,15 @@
17
17
  * - Each session picks up where the last left off via state file
18
18
  *
19
19
  * @module workflow-state-manager
20
- * @version 1.0.0
20
+ * @version 2.0.0
21
21
  */
22
22
 
23
23
  const fs = require('fs').promises;
24
24
  const path = require('path');
25
25
  const yaml = require('js-yaml');
26
26
 
27
+ const WORKFLOW_STATE_VERSION = '2.0';
28
+
27
29
  class WorkflowStateManager {
28
30
  /**
29
31
  * @param {Object} [options={}]
@@ -64,6 +66,7 @@ class WorkflowStateManager {
64
66
  const instanceId = this._generateInstanceId(wf.id);
65
67
 
66
68
  const state = {
69
+ state_version: WORKFLOW_STATE_VERSION,
67
70
  workflow_id: wf.id,
68
71
  workflow_name: wf.name || wf.id,
69
72
  instance_id: instanceId,
@@ -137,6 +140,130 @@ class WorkflowStateManager {
137
140
  return state;
138
141
  }
139
142
 
143
+ // ============ Runtime-First Next Action ============
144
+
145
+ /**
146
+ * Build normalized execution state from runtime signals.
147
+ * Deterministic priority:
148
+ * blocked > qa_rejected > ci_red > completed > in_development > ready_for_validation > unknown
149
+ *
150
+ * @param {Object} [signals={}]
151
+ * @param {string} [signals.story_status]
152
+ * @param {string} [signals.qa_status]
153
+ * @param {string} [signals.ci_status]
154
+ * @param {boolean} [signals.has_uncommitted_changes]
155
+ * @returns {{ state: string, reasons: string[], normalized: Object }}
156
+ */
157
+ evaluateExecutionState(signals = {}) {
158
+ const storyStatus = String(signals.story_status || 'unknown').toLowerCase();
159
+ const qaStatus = String(signals.qa_status || 'unknown').toLowerCase();
160
+ const ciStatus = String(signals.ci_status || 'unknown').toLowerCase();
161
+ const hasUncommitted = Boolean(signals.has_uncommitted_changes);
162
+
163
+ const reasons = [];
164
+ let state = 'unknown';
165
+
166
+ if (storyStatus === 'blocked') {
167
+ state = 'blocked';
168
+ reasons.push('story status is blocked');
169
+ } else if (qaStatus === 'rejected' || qaStatus === 'fail' || qaStatus === 'failed') {
170
+ state = 'qa_rejected';
171
+ reasons.push(`qa status is ${qaStatus}`);
172
+ } else if (ciStatus === 'red' || ciStatus === 'failed' || ciStatus === 'error') {
173
+ state = 'ci_red';
174
+ reasons.push(`ci status is ${ciStatus}`);
175
+ } else if (storyStatus === 'done' || storyStatus === 'completed') {
176
+ state = 'completed';
177
+ reasons.push(`story status is ${storyStatus}`);
178
+ } else if (storyStatus === 'in_progress' || storyStatus === 'review') {
179
+ if (hasUncommitted) {
180
+ state = 'in_development';
181
+ reasons.push('story in progress with uncommitted changes');
182
+ } else {
183
+ state = 'ready_for_validation';
184
+ reasons.push('story in progress with clean working tree');
185
+ }
186
+ }
187
+
188
+ return {
189
+ state,
190
+ reasons,
191
+ normalized: {
192
+ story_status: storyStatus,
193
+ qa_status: qaStatus,
194
+ ci_status: ciStatus,
195
+ has_uncommitted_changes: hasUncommitted,
196
+ },
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Deterministic next-action recommendation for runtime-first flows.
202
+ *
203
+ * @param {Object} [signals={}]
204
+ * @param {Object} [options={}]
205
+ * @param {string} [options.story]
206
+ * @returns {{ state: string, command: string, agent: string, rationale: string, confidence: number }}
207
+ */
208
+ getNextActionRecommendation(signals = {}, options = {}) {
209
+ const result = this.evaluateExecutionState(signals);
210
+ const storyArg = options.story ? ` ${options.story}` : '';
211
+
212
+ const map = {
213
+ blocked: {
214
+ command: `*orchestrate-status${storyArg}`,
215
+ agent: '@po',
216
+ rationale: 'Story is blocked. Surface blockers and unblock path first.',
217
+ confidence: 0.95,
218
+ },
219
+ qa_rejected: {
220
+ command: `*apply-qa-fixes${storyArg}`,
221
+ agent: '@dev',
222
+ rationale: 'QA rejected the story. Apply fixes before progressing.',
223
+ confidence: 0.95,
224
+ },
225
+ ci_red: {
226
+ command: '*run-tests',
227
+ agent: '@dev',
228
+ rationale: 'CI is red. Reproduce and fix failing tests/checks first.',
229
+ confidence: 0.92,
230
+ },
231
+ completed: {
232
+ command: `*close-story${storyArg}`,
233
+ agent: '@po',
234
+ rationale: 'Story is complete. Close lifecycle and move to next item.',
235
+ confidence: 0.9,
236
+ },
237
+ in_development: {
238
+ command: '*run-tests',
239
+ agent: '@dev',
240
+ rationale: 'Code is in progress with local changes. Validate before QA handoff.',
241
+ confidence: 0.85,
242
+ },
243
+ ready_for_validation: {
244
+ command: `*review-build${storyArg}`,
245
+ agent: '@qa',
246
+ rationale: 'Development appears stable. Proceed to QA structured review.',
247
+ confidence: 0.82,
248
+ },
249
+ unknown: {
250
+ command: `*next${storyArg}`,
251
+ agent: '@dev',
252
+ rationale: 'Context is incomplete. Request explicit workflow guidance.',
253
+ confidence: 0.4,
254
+ },
255
+ };
256
+
257
+ const recommendation = map[result.state] || map.unknown;
258
+ return {
259
+ state: result.state,
260
+ command: recommendation.command,
261
+ agent: recommendation.agent,
262
+ rationale: recommendation.rationale,
263
+ confidence: recommendation.confidence,
264
+ };
265
+ }
266
+
140
267
  /**
141
268
  * Load state from file
142
269
  * @param {string} instanceId
@@ -4,6 +4,9 @@
4
4
 
5
5
  Suggest next commands based on current workflow context using the Workflow Intelligence System (WIS). Helps users navigate workflows efficiently without memorizing command sequences.
6
6
 
7
+ AIOS 4.0.4 runtime-first mode adds deterministic next-step recommendation from
8
+ execution signals (story/qa/ci/diff) via `workflow-state-manager`.
9
+
7
10
  ## Task Definition (AIOS Task Format V1.0)
8
11
 
9
12
  ```yaml
@@ -88,7 +91,23 @@ const context = await engine.buildContext({
88
91
  });
89
92
  ```
90
93
 
91
- ### Step 3: Get Suggestions
94
+ ### Step 3: Runtime-First Deterministic Recommendation (Preferred)
95
+ ```javascript
96
+ const { WorkflowStateManager } = require('.aios-core/development/scripts/workflow-state-manager');
97
+ const manager = new WorkflowStateManager();
98
+
99
+ const runtimeNext = manager.getNextActionRecommendation(
100
+ {
101
+ story_status: context.projectState?.storyStatus || 'unknown',
102
+ qa_status: context.projectState?.qaStatus || 'unknown',
103
+ ci_status: context.projectState?.ciStatus || 'unknown',
104
+ has_uncommitted_changes: context.projectState?.hasUncommittedChanges || false,
105
+ },
106
+ { story: args.story || context.storyPath || '' },
107
+ );
108
+ ```
109
+
110
+ ### Step 4: Get WIS Suggestions (Fallback / enrichment)
92
111
  ```javascript
93
112
  const result = await engine.suggestNext(context);
94
113
 
@@ -103,15 +122,27 @@ const result = await engine.suggestNext(context);
103
122
  // }
104
123
  ```
105
124
 
106
- ### Step 4: Format Output
125
+ ### Step 5: Format Output
107
126
  ```javascript
108
127
  const formatter = require('.aios-core/workflow-intelligence/engine/output-formatter');
109
128
 
110
- // Limit to top 3 unless --all flag
111
- const displaySuggestions = args.all ? result.suggestions : result.suggestions.slice(0, 3);
129
+ const runtimeSuggestion = {
130
+ command: runtimeNext.command,
131
+ args: '',
132
+ description: runtimeNext.rationale,
133
+ confidence: runtimeNext.confidence,
134
+ priority: 1,
135
+ };
136
+ const mergedSuggestions = [runtimeSuggestion, ...(result.suggestions || [])];
137
+ const displaySuggestions = args.all ? mergedSuggestions : mergedSuggestions.slice(0, 3);
112
138
 
113
139
  // Display formatted output
114
- formatter.displaySuggestions(result.workflow, result.currentState, result.confidence, displaySuggestions);
140
+ formatter.displaySuggestions({
141
+ workflow: result.workflow || 'runtime_first',
142
+ currentState: runtimeNext.state,
143
+ confidence: runtimeNext.confidence,
144
+ suggestions: displaySuggestions,
145
+ });
115
146
  ```
116
147
 
117
148
  ---
@@ -15,6 +15,25 @@ const path = require('path');
15
15
 
16
16
  const REPO_ROOT = path.resolve(__dirname, '../..');
17
17
 
18
+ function isDocsOnlyPath(relativePath) {
19
+ const p = String(relativePath || '').replace(/\\/g, '/');
20
+ if (!p) return false;
21
+
22
+ if (p.startsWith('docs/')) return true;
23
+ if (p === 'README.md') return true;
24
+ if (p === 'CHANGELOG.md') return true;
25
+ if (p === 'CONTRIBUTING.md') return true;
26
+ if (p === 'CODE_OF_CONDUCT.md') return true;
27
+ if (p === 'AGENTS.md') return true;
28
+
29
+ return false;
30
+ }
31
+
32
+ function isDocsOnlyCommit(changes) {
33
+ if (!Array.isArray(changes) || changes.length === 0) return false;
34
+ return changes.every((c) => isDocsOnlyPath(c.relativePath));
35
+ }
36
+
18
37
  function getChangedFilesFromLastCommit() {
19
38
  try {
20
39
  const output = execSync('git diff-tree --no-commit-id --name-status -r HEAD', {
@@ -52,7 +71,11 @@ function getChangedFilesFromLastCommit() {
52
71
  }
53
72
 
54
73
  if (!filePath) continue;
55
- changes.push({ action, filePath: path.resolve(REPO_ROOT, filePath) });
74
+ changes.push({
75
+ action,
76
+ relativePath: filePath,
77
+ filePath: path.resolve(REPO_ROOT, filePath),
78
+ });
56
79
  }
57
80
 
58
81
  return changes;
@@ -67,6 +90,11 @@ async function main() {
67
90
 
68
91
  if (changes.length === 0) return;
69
92
 
93
+ if (process.env.AIOS_IDS_FORCE !== '1' && isDocsOnlyCommit(changes)) {
94
+ console.log('[IDS-Hook] Docs-only commit detected, skipping registry update.');
95
+ return;
96
+ }
97
+
70
98
  try {
71
99
  const { RegistryUpdater } = require(path.resolve(REPO_ROOT, '.aios-core/core/ids/registry-updater.js'));
72
100
  const updater = new RegistryUpdater();
@@ -16,6 +16,25 @@ const path = require('path');
16
16
 
17
17
  const REPO_ROOT = path.resolve(__dirname, '../..');
18
18
 
19
+ function isDocsOnlyPath(relativePath) {
20
+ const p = String(relativePath || '').replace(/\\/g, '/');
21
+ if (!p) return false;
22
+
23
+ if (p.startsWith('docs/')) return true;
24
+ if (p === 'README.md') return true;
25
+ if (p === 'CHANGELOG.md') return true;
26
+ if (p === 'CONTRIBUTING.md') return true;
27
+ if (p === 'CODE_OF_CONDUCT.md') return true;
28
+ if (p === 'AGENTS.md') return true;
29
+
30
+ return false;
31
+ }
32
+
33
+ function isDocsOnlyPush(changes) {
34
+ if (!Array.isArray(changes) || changes.length === 0) return false;
35
+ return changes.every((c) => isDocsOnlyPath(c.relativePath));
36
+ }
37
+
19
38
  function getChangedFilesSinceRemote() {
20
39
  try {
21
40
  const trackingBranch = execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}', {
@@ -58,7 +77,11 @@ function getChangedFilesSinceRemote() {
58
77
  }
59
78
 
60
79
  if (!filePath) continue;
61
- changes.push({ action, filePath: path.resolve(REPO_ROOT, filePath) });
80
+ changes.push({
81
+ action,
82
+ relativePath: filePath,
83
+ filePath: path.resolve(REPO_ROOT, filePath),
84
+ });
62
85
  }
63
86
 
64
87
  return changes;
@@ -76,6 +99,11 @@ async function main() {
76
99
  process.exit(0);
77
100
  }
78
101
 
102
+ if (process.env.AIOS_IDS_FORCE !== '1' && isDocsOnlyPush(changes)) {
103
+ console.log('[IDS-Hook] Docs-only push detected, skipping registry update.');
104
+ process.exit(0);
105
+ }
106
+
79
107
  try {
80
108
  const { RegistryUpdater } = require(path.resolve(REPO_ROOT, '.aios-core/core/ids/registry-updater.js'));
81
109
  const updater = new RegistryUpdater();
@@ -0,0 +1,44 @@
1
+ release: "AIOS 4.0.4"
2
+ updated_at: "2026-02-16"
3
+ source_of_truth:
4
+ docs_matrix: "docs/ide-integration.md"
5
+ validator: ".aios-core/infrastructure/scripts/validate-parity.js"
6
+
7
+ global_required_checks:
8
+ - paths
9
+
10
+ ide_matrix:
11
+ - ide: "claude-code"
12
+ display_name: "Claude Code"
13
+ expected_status: "Works"
14
+ required_checks:
15
+ - claude-sync
16
+ - claude-integration
17
+ - ide: "gemini"
18
+ display_name: "Gemini CLI"
19
+ expected_status: "Works"
20
+ required_checks:
21
+ - gemini-sync
22
+ - gemini-integration
23
+ - ide: "codex"
24
+ display_name: "Codex CLI"
25
+ expected_status: "Limited"
26
+ required_checks:
27
+ - codex-sync
28
+ - codex-integration
29
+ - codex-skills
30
+ - ide: "cursor"
31
+ display_name: "Cursor"
32
+ expected_status: "Limited"
33
+ required_checks:
34
+ - cursor-sync
35
+ - ide: "github-copilot"
36
+ display_name: "GitHub Copilot"
37
+ expected_status: "Limited"
38
+ required_checks:
39
+ - github-copilot-sync
40
+ - ide: "antigravity"
41
+ display_name: "AntiGravity"
42
+ expected_status: "Limited"
43
+ required_checks:
44
+ - antigravity-sync
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const fs = require('fs');
4
5
  const path = require('path');
6
+ const yaml = require('js-yaml');
5
7
  const { spawnSync } = require('child_process');
6
8
  const { validateClaudeIntegration } = require('./validate-claude-integration');
7
9
  const { validateCodexIntegration } = require('./validate-codex-integration');
@@ -10,10 +12,16 @@ const { validateCodexSkills } = require('./codex-skills-sync/validate');
10
12
  const { validatePaths } = require('./validate-paths');
11
13
 
12
14
  function parseArgs(argv = process.argv.slice(2)) {
13
- const args = new Set(argv);
15
+ const args = new Set(
16
+ argv.filter((arg) => !arg.startsWith('--contract=') && !arg.startsWith('--diff=')),
17
+ );
18
+ const contractArg = argv.find((arg) => arg.startsWith('--contract='));
19
+ const diffArg = argv.find((arg) => arg.startsWith('--diff='));
14
20
  return {
15
21
  quiet: args.has('--quiet') || args.has('-q'),
16
22
  json: args.has('--json'),
23
+ contractPath: contractArg ? contractArg.slice('--contract='.length) : null,
24
+ diffPath: diffArg ? diffArg.slice('--diff='.length) : null,
17
25
  };
18
26
  }
19
27
 
@@ -31,6 +39,25 @@ function runSyncValidate(ide, projectRoot) {
31
39
  };
32
40
  }
33
41
 
42
+ function getDefaultContractPath(projectRoot = process.cwd()) {
43
+ return path.join(
44
+ projectRoot,
45
+ '.aios-core',
46
+ 'infrastructure',
47
+ 'contracts',
48
+ 'compatibility',
49
+ 'aios-4.0.4.yaml',
50
+ );
51
+ }
52
+
53
+ function loadCompatibilityContract(contractPath) {
54
+ if (!contractPath || !fs.existsSync(contractPath)) {
55
+ return null;
56
+ }
57
+ const raw = fs.readFileSync(contractPath, 'utf8');
58
+ return yaml.load(raw);
59
+ }
60
+
34
61
  function normalizeResult(input) {
35
62
  if (!input || typeof input !== 'object') {
36
63
  return { ok: false, errors: ['Validator returned invalid result'], warnings: [] };
@@ -43,6 +70,152 @@ function normalizeResult(input) {
43
70
  };
44
71
  }
45
72
 
73
+ function escapeRegex(value) {
74
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
75
+ }
76
+
77
+ function validateCompatibilityContract(contract, resultById, options = {}) {
78
+ const violations = [];
79
+
80
+ if (!contract || typeof contract !== 'object') {
81
+ return ['Compatibility contract is missing or invalid'];
82
+ }
83
+
84
+ const matrix = Array.isArray(contract.ide_matrix) ? contract.ide_matrix : [];
85
+ if (matrix.length === 0) {
86
+ return ['Compatibility contract ide_matrix is empty'];
87
+ }
88
+
89
+ const docsPath = options.docsPath;
90
+ if (!docsPath || !fs.existsSync(docsPath)) {
91
+ violations.push(`Compatibility matrix document not found: ${docsPath || 'undefined'}`);
92
+ return violations;
93
+ }
94
+ const docsContent = fs.readFileSync(docsPath, 'utf8');
95
+
96
+ for (const ide of matrix) {
97
+ const ideName = ide.ide || 'unknown';
98
+ const displayName = ide.display_name || ideName;
99
+ const requiredChecks = Array.isArray(ide.required_checks) ? ide.required_checks : [];
100
+ const expectedStatus = ide.expected_status;
101
+
102
+ if (!expectedStatus) {
103
+ violations.push(`Contract missing expected_status for IDE "${ideName}"`);
104
+ }
105
+
106
+ const rowRegex = new RegExp(
107
+ `\\|\\s*${escapeRegex(displayName)}\\s*\\|\\s*${escapeRegex(expectedStatus || '')}\\s*\\|`,
108
+ 'i',
109
+ );
110
+ if (!rowRegex.test(docsContent)) {
111
+ violations.push(
112
+ `Docs matrix mismatch for "${displayName}": expected status "${expectedStatus}" in ${options.docsPathRelative}`,
113
+ );
114
+ }
115
+
116
+ for (const checkId of requiredChecks) {
117
+ const checkResult = resultById[checkId];
118
+ if (!checkResult) {
119
+ violations.push(`Contract requires unknown check "${checkId}" for IDE "${ideName}"`);
120
+ continue;
121
+ }
122
+ if (!checkResult.ok) {
123
+ violations.push(`Contract violation for "${ideName}": required check "${checkId}" failed`);
124
+ }
125
+ }
126
+ }
127
+
128
+ const globalRequiredChecks = Array.isArray(contract.global_required_checks)
129
+ ? contract.global_required_checks
130
+ : [];
131
+ for (const checkId of globalRequiredChecks) {
132
+ const checkResult = resultById[checkId];
133
+ if (!checkResult) {
134
+ violations.push(`Contract requires unknown global check "${checkId}"`);
135
+ continue;
136
+ }
137
+ if (!checkResult.ok) {
138
+ violations.push(`Contract violation: global required check "${checkId}" failed`);
139
+ }
140
+ }
141
+
142
+ return violations;
143
+ }
144
+
145
+ function sortUnique(values = []) {
146
+ return [...new Set(values)].sort();
147
+ }
148
+
149
+ function diffCompatibilityContracts(currentContract, previousContract) {
150
+ if (!currentContract || !previousContract) {
151
+ return null;
152
+ }
153
+
154
+ const currentRelease = currentContract.release || null;
155
+ const previousRelease = previousContract.release || null;
156
+ const releaseChanged = currentRelease !== previousRelease;
157
+
158
+ const currentGlobalChecks = sortUnique(currentContract.global_required_checks || []);
159
+ const previousGlobalChecks = sortUnique(previousContract.global_required_checks || []);
160
+ const globalChecksAdded = currentGlobalChecks.filter((item) => !previousGlobalChecks.includes(item));
161
+ const globalChecksRemoved = previousGlobalChecks.filter((item) => !currentGlobalChecks.includes(item));
162
+
163
+ const currentByIde = Object.fromEntries((currentContract.ide_matrix || []).map((item) => [item.ide, item]));
164
+ const previousByIde = Object.fromEntries((previousContract.ide_matrix || []).map((item) => [item.ide, item]));
165
+ const ideKeys = sortUnique([...Object.keys(currentByIde), ...Object.keys(previousByIde)]);
166
+
167
+ const ideChanges = [];
168
+ for (const ide of ideKeys) {
169
+ const current = currentByIde[ide];
170
+ const previous = previousByIde[ide];
171
+
172
+ if (!previous && current) {
173
+ ideChanges.push({ ide, type: 'added', current });
174
+ continue;
175
+ }
176
+ if (previous && !current) {
177
+ ideChanges.push({ ide, type: 'removed', previous });
178
+ continue;
179
+ }
180
+
181
+ const currentStatus = current.expected_status || null;
182
+ const previousStatus = previous.expected_status || null;
183
+ const statusChanged = currentStatus !== previousStatus;
184
+ const currentChecks = sortUnique(current.required_checks || []);
185
+ const previousChecks = sortUnique(previous.required_checks || []);
186
+ const checksAdded = currentChecks.filter((item) => !previousChecks.includes(item));
187
+ const checksRemoved = previousChecks.filter((item) => !currentChecks.includes(item));
188
+
189
+ if (statusChanged || checksAdded.length > 0 || checksRemoved.length > 0) {
190
+ ideChanges.push({
191
+ ide,
192
+ type: 'changed',
193
+ status: { previous: previousStatus, current: currentStatus },
194
+ required_checks: {
195
+ added: checksAdded,
196
+ removed: checksRemoved,
197
+ },
198
+ });
199
+ }
200
+ }
201
+
202
+ return {
203
+ from_release: previousRelease,
204
+ to_release: currentRelease,
205
+ release_changed: releaseChanged,
206
+ global_required_checks: {
207
+ added: globalChecksAdded,
208
+ removed: globalChecksRemoved,
209
+ },
210
+ ide_changes: ideChanges,
211
+ has_changes:
212
+ releaseChanged
213
+ || globalChecksAdded.length > 0
214
+ || globalChecksRemoved.length > 0
215
+ || ideChanges.length > 0,
216
+ };
217
+ }
218
+
46
219
  function runParityValidation(options = {}, deps = {}) {
47
220
  const projectRoot = options.projectRoot || process.cwd();
48
221
  const runSync = deps.runSyncValidate || runSyncValidate;
@@ -51,6 +224,15 @@ function runParityValidation(options = {}, deps = {}) {
51
224
  const runGeminiIntegration = deps.validateGeminiIntegration || validateGeminiIntegration;
52
225
  const runCodexSkills = deps.validateCodexSkills || validateCodexSkills;
53
226
  const runPaths = deps.validatePaths || validatePaths;
227
+ const resolvedContractPath = options.contractPath
228
+ ? path.resolve(projectRoot, options.contractPath)
229
+ : getDefaultContractPath(projectRoot);
230
+ const loadContract = deps.loadCompatibilityContract || loadCompatibilityContract;
231
+ const contract = loadContract(resolvedContractPath);
232
+ const resolvedDiffPath = options.diffPath ? path.resolve(projectRoot, options.diffPath) : null;
233
+ const previousContract = resolvedDiffPath ? loadContract(resolvedDiffPath) : null;
234
+ const docsPath = path.join(projectRoot, 'docs', 'ide-integration.md');
235
+ const docsPathRelative = path.relative(projectRoot, docsPath);
54
236
  const checks = [
55
237
  { id: 'claude-sync', exec: () => runSync('claude-code', projectRoot) },
56
238
  { id: 'claude-integration', exec: () => runClaudeIntegration({ projectRoot }) },
@@ -58,6 +240,9 @@ function runParityValidation(options = {}, deps = {}) {
58
240
  { id: 'codex-integration', exec: () => runCodexIntegration({ projectRoot }) },
59
241
  { id: 'gemini-sync', exec: () => runSync('gemini', projectRoot) },
60
242
  { id: 'gemini-integration', exec: () => runGeminiIntegration({ projectRoot }) },
243
+ { id: 'cursor-sync', exec: () => runSync('cursor', projectRoot) },
244
+ { id: 'github-copilot-sync', exec: () => runSync('github-copilot', projectRoot) },
245
+ { id: 'antigravity-sync', exec: () => runSync('antigravity', projectRoot) },
61
246
  { id: 'codex-skills', exec: () => runCodexSkills({ projectRoot, strict: true, quiet: true }) },
62
247
  { id: 'paths', exec: () => runPaths({ projectRoot }) },
63
248
  ];
@@ -66,15 +251,60 @@ function runParityValidation(options = {}, deps = {}) {
66
251
  const normalized = normalizeResult(check.exec());
67
252
  return { id: check.id, ...normalized };
68
253
  });
254
+ const resultById = Object.fromEntries(results.map((r) => [r.id, r]));
255
+ const contractViolations = validateCompatibilityContract(contract, resultById, {
256
+ docsPath,
257
+ docsPathRelative,
258
+ });
259
+ const contractSummary = contract
260
+ ? {
261
+ release: contract.release || null,
262
+ path: path.relative(projectRoot, resolvedContractPath),
263
+ }
264
+ : {
265
+ release: null,
266
+ path: path.relative(projectRoot, resolvedContractPath),
267
+ };
69
268
 
70
269
  return {
71
- ok: results.every((r) => r.ok),
270
+ ok: results.every((r) => r.ok) && contractViolations.length === 0,
72
271
  checks: results,
272
+ contract: contractSummary,
273
+ contractDiff: diffCompatibilityContracts(contract, previousContract),
274
+ contractViolations,
73
275
  };
74
276
  }
75
277
 
76
278
  function formatHumanReport(result) {
77
279
  const lines = [];
280
+ if (result.contract && result.contract.release) {
281
+ lines.push(`Compatibility Contract: ${result.contract.release} (${result.contract.path})`);
282
+ lines.push('');
283
+ }
284
+ if (result.contractDiff) {
285
+ lines.push(
286
+ `Contract Diff: ${result.contractDiff.from_release || 'unknown'} -> ${result.contractDiff.to_release || 'unknown'}`,
287
+ );
288
+ if (!result.contractDiff.has_changes) {
289
+ lines.push('- no changes');
290
+ } else {
291
+ if (result.contractDiff.release_changed) {
292
+ lines.push('- release changed');
293
+ }
294
+ const globalAdded = result.contractDiff.global_required_checks.added || [];
295
+ const globalRemoved = result.contractDiff.global_required_checks.removed || [];
296
+ if (globalAdded.length > 0) {
297
+ lines.push(`- global checks added: ${globalAdded.join(', ')}`);
298
+ }
299
+ if (globalRemoved.length > 0) {
300
+ lines.push(`- global checks removed: ${globalRemoved.join(', ')}`);
301
+ }
302
+ for (const ideChange of result.contractDiff.ide_changes || []) {
303
+ lines.push(`- ${ideChange.ide}: ${ideChange.type}`);
304
+ }
305
+ }
306
+ lines.push('');
307
+ }
78
308
  for (const check of result.checks) {
79
309
  lines.push(`${check.ok ? '✅' : '❌'} ${check.id}`);
80
310
  if (check.errors.length > 0) {
@@ -84,6 +314,11 @@ function formatHumanReport(result) {
84
314
  lines.push(...check.warnings.map((w) => `⚠️ ${w}`));
85
315
  }
86
316
  }
317
+ if (Array.isArray(result.contractViolations) && result.contractViolations.length > 0) {
318
+ lines.push('');
319
+ lines.push('❌ Compatibility Contract Violations');
320
+ lines.push(...result.contractViolations.map((v) => `- ${v}`));
321
+ }
87
322
  lines.push('');
88
323
  lines.push(result.ok ? '✅ Parity validation passed' : '❌ Parity validation failed');
89
324
  return lines.join('\n');
@@ -116,4 +351,5 @@ module.exports = {
116
351
  runParityValidation,
117
352
  normalizeResult,
118
353
  formatHumanReport,
354
+ diffCompatibilityContracts,
119
355
  };