contextos-agents 2.0.0 → 2.1.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 (45) hide show
  1. package/.agents/AGENTS.md +53 -33
  2. package/.agents/adapters/aider/export.js +41 -14
  3. package/.agents/adapters/claude/export.js +1 -1
  4. package/.agents/adapters/copilot/export.js +1 -1
  5. package/.agents/adapters/cursor/export.js +1 -1
  6. package/.agents/adapters/drift-detector.js +80 -7
  7. package/.agents/adapters/gemini/export.js +1 -1
  8. package/.agents/adapters/pure-compiler.js +10 -0
  9. package/.agents/adapters/shared.js +13 -4
  10. package/.agents/adapters/zed/export.js +1 -1
  11. package/.agents/compiled/registry.v2.json +29 -25
  12. package/.agents/compiled/registry.v2.sha256 +1 -1
  13. package/.agents/core/skills/context-os/SKILL.md +34 -37
  14. package/.agents/core/skills/engineering-workflow/SKILL.md +24 -24
  15. package/.agents/core/skills/gemini-precision/EXAMPLES.md +72 -0
  16. package/.agents/core/skills/gemini-precision/SKILL.md +2 -1
  17. package/.agents/core/skills/gemini-precision/TROUBLESHOOTING.md +25 -0
  18. package/.agents/core/skills/gemini-precision/skill.yaml +2 -0
  19. package/.agents/core/skills/gstack-roles/SKILL.md +7 -6
  20. package/.agents/core/skills/security/SKILL.md +44 -16
  21. package/.agents/core/skills/security/skill.yaml +0 -1
  22. package/.agents/ctx.js +16 -10
  23. package/.agents/generated/claude/skills/context-os/SKILL.md +34 -37
  24. package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +24 -24
  25. package/.agents/generated/claude/skills/gemini-precision/SKILL.md +102 -1
  26. package/.agents/generated/claude/skills/gstack-roles/SKILL.md +7 -6
  27. package/.agents/generated/claude/skills/security/SKILL.md +44 -16
  28. package/.agents/generated/gemini/skills/context-os/SKILL.md +34 -37
  29. package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +24 -24
  30. package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +105 -1
  31. package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +7 -6
  32. package/.agents/generated/gemini/skills/security/SKILL.md +44 -125
  33. package/.agents/plugins.js +5 -4
  34. package/.agents/resolver/canonical-resolver.js +7 -7
  35. package/.agents/validate.js +69 -1
  36. package/README.md +80 -23
  37. package/bin/commands/hook.js +129 -0
  38. package/bin/commands/scan.js +70 -0
  39. package/bin/commands.js +39 -1
  40. package/bin/index.js +144 -33
  41. package/bin/lib/gate.js +171 -0
  42. package/bin/lib/git-snapshot.js +187 -0
  43. package/bin/lib/scan.js +380 -0
  44. package/package.json +4 -2
  45. package/.agents/core/skills/security/security.md +0 -106
@@ -0,0 +1,171 @@
1
+ /**
2
+ * bin/lib/gate.js
3
+ * ContextOS - Quality Gate Verification Engine
4
+ *
5
+ * Implements deterministic verification paths without executing untrusted
6
+ * user project scripts. Operates in-process using trusted package modules.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { detectDrift } = require('../../.agents/adapters/drift-detector.js');
14
+ const pureCompiler = require('../../.agents/adapters/pure-compiler.js');
15
+
16
+ /**
17
+ * Runs the quality gate against a target project root.
18
+ *
19
+ * @param {string} [projectRoot=process.cwd()] - Target project directory
20
+ * @param {object} [options]
21
+ * @param {string|string[]} [options.target='all'] - Adapters to verify
22
+ * @param {string} [options.profile=null] - Profile name or override
23
+ * @param {boolean} [options.json=false] - Format as machine-readable JSON
24
+ * @returns {{ ok: boolean, code: number, status: string, drift: object, message: string }}
25
+ */
26
+ function runGate(projectRoot = process.cwd(), options = {}) {
27
+ const absRoot = path.resolve(projectRoot);
28
+ const target = options.target || 'all';
29
+ const profile = options.profile || null;
30
+
31
+ // 1. Run Drift Detection via trusted in-package drift-detector
32
+ const drift = detectDrift(absRoot, target, { profile });
33
+
34
+ let status = 'pass';
35
+ let code = 0;
36
+ let message = 'All adapter outputs are synchronized with source skills.';
37
+
38
+ if (drift.hasError) {
39
+ status = 'error';
40
+ code = drift.code || 2;
41
+ message = drift.error || 'Configuration or execution error occurred during verification.';
42
+ } else if (drift.hasDrift) {
43
+ status = 'drift';
44
+ code = drift.code || 1;
45
+ message = `Adapter drift detected across ${drift.totalFindings} finding(s).`;
46
+ }
47
+
48
+ const result = {
49
+ schemaVersion: '2.0.0',
50
+ status,
51
+ code,
52
+ ok: code === 0,
53
+ projectRoot: absRoot,
54
+ target,
55
+ profile,
56
+ message,
57
+ drift,
58
+ };
59
+
60
+ return result;
61
+ }
62
+
63
+ /**
64
+ * Emits GitHub Actions workflow annotations for drift or errors.
65
+ *
66
+ * @param {object} result - Gate result
67
+ */
68
+ function emitGitHubAnnotations(result) {
69
+ if (!result || !result.drift) return;
70
+
71
+ const escapeActionData = (str) =>
72
+ String(str)
73
+ .replace(/%/g, '%25')
74
+ .replace(/\r/g, '%0D')
75
+ .replace(/\n/g, '%0A');
76
+
77
+ const normalizePath = (p) => (p ? String(p).replace(/\\/g, '/') : '');
78
+
79
+ const { findings, collisions } = result.drift;
80
+
81
+ if (findings) {
82
+ for (const item of findings.MISSING_OUTPUT || []) {
83
+ console.error(`::error file=${normalizePath(item.path)}::[MISSING_OUTPUT] ${escapeActionData(item.reason || 'Missing projected output file')}`);
84
+ }
85
+ for (const item of findings.MODIFIED_MANAGED_OUTPUT || []) {
86
+ console.error(`::error file=${normalizePath(item.path)}::[MODIFIED_OUTPUT] ${escapeActionData(item.reason || 'Managed output modified on disk')}`);
87
+ }
88
+ for (const item of findings.ORPHAN_MANAGED_OUTPUT || []) {
89
+ console.error(`::warning file=${normalizePath(item.path)}::[ORPHAN_OUTPUT] ${escapeActionData(item.reason || 'Orphan file recorded in lockfile')}`);
90
+ }
91
+ for (const item of findings.CORRUPT_LOCKFILE || []) {
92
+ console.error(`::error::[CORRUPT_LOCKFILE] ${escapeActionData(item.reason || 'Lockfile is corrupt')}`);
93
+ }
94
+ for (const item of findings.CONFIG_ERROR || []) {
95
+ console.error(`::error::[CONFIG_ERROR] ${escapeActionData(item.reason || 'Configuration error')}`);
96
+ }
97
+ for (const item of findings.EMPTY_PROJECTION || []) {
98
+ console.error(`::error::[EMPTY_PROJECTION] ${escapeActionData(item.reason || 'Projected artifact set is empty')}`);
99
+ }
100
+ }
101
+
102
+ for (const c of collisions || []) {
103
+ console.error(`::error file=${normalizePath(c.path)}::[PATH_COLLISION] Collision between adapter ${c.firstAdapter} and ${c.secondAdapter}`);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Formats a markdown summary for GitHub Step Summary.
109
+ *
110
+ * @param {object} result - Gate result
111
+ * @returns {string} Markdown summary
112
+ */
113
+ function formatGitHubSummary(result) {
114
+ const icon = result.ok ? '✅' : (result.status === 'drift' ? '⚠️' : '❌');
115
+ const lines = [
116
+ `# ${icon} ContextOS Quality Gate Report`,
117
+ '',
118
+ `**Status**: \`${result.status.toUpperCase()}\` (exit code: \`${result.code}\`) `,
119
+ `**Project Root**: \`${result.projectRoot}\` `,
120
+ `**Target Adapters**: \`${Array.isArray(result.target) ? result.target.join(', ') : result.target}\` `,
121
+ `**Active Profile**: \`${result.profile || 'default'}\` `,
122
+ `**Projected Artifacts**: \`${result.drift?.projectedCount || 0}\` `,
123
+ '',
124
+ `### Overview`,
125
+ result.message,
126
+ '',
127
+ ];
128
+
129
+ if (result.drift && result.drift.totalFindings > 0) {
130
+ lines.push(`### Findings (${result.drift.totalFindings})`);
131
+ lines.push('| State | Path | Detail |');
132
+ lines.push('| --- | --- | --- |');
133
+
134
+ for (const [state, items] of Object.entries(result.drift.findings)) {
135
+ for (const item of items) {
136
+ const itemPath = item.path ? `\`${item.path}\`` : '*(project)*';
137
+ const detail = (item.reason || item.diff || '').replace(/\|/g, '\\|');
138
+ lines.push(`| **${state}** | ${itemPath} | ${detail} |`);
139
+ }
140
+ }
141
+ lines.push('');
142
+ lines.push('> **Remediation**: Run `npx contextos-agents export all` and commit synchronized adapter artifacts.');
143
+ lines.push('');
144
+ }
145
+
146
+ return lines.join('\n');
147
+ }
148
+
149
+ /**
150
+ * Writes the markdown summary to GITHUB_STEP_SUMMARY if available.
151
+ *
152
+ * @param {object} result - Gate result
153
+ */
154
+ function writeGitHubSummary(result) {
155
+ const summaryFile = process.env.GITHUB_STEP_SUMMARY;
156
+ if (!summaryFile) return;
157
+
158
+ try {
159
+ const summaryMd = formatGitHubSummary(result);
160
+ fs.appendFileSync(summaryFile, summaryMd + '\n', 'utf8');
161
+ } catch {
162
+ // Best-effort write to step summary
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ runGate,
168
+ emitGitHubAnnotations,
169
+ formatGitHubSummary,
170
+ writeGitHubSummary,
171
+ };
@@ -0,0 +1,187 @@
1
+ /**
2
+ * bin/lib/git-snapshot.js
3
+ * ContextOS Git Index Snapshot & Staged Blob Reader
4
+ *
5
+ * Inspects staged changes directly from the Git index without modifying
6
+ * the working directory. Safely handles NUL-delimited filenames, Unicode,
7
+ * binary files, and path spaces.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const path = require('path');
13
+ const { execFileSync } = require('child_process');
14
+
15
+ /**
16
+ * Finds the top-level repository root via Git.
17
+ *
18
+ * @param {string} [cwd=process.cwd()] - Working directory
19
+ * @returns {string|null} Absolute repository root or null if not a Git repository
20
+ */
21
+ function findGitRoot(cwd = process.cwd()) {
22
+ try {
23
+ const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
24
+ cwd,
25
+ encoding: 'utf8',
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ });
28
+ return path.resolve(root.trim());
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Discovers the git hooks directory, respecting core.hooksPath if set.
36
+ *
37
+ * @param {string} [cwd=process.cwd()] - Working directory
38
+ * @returns {string|null} Absolute path to hooks directory or null
39
+ */
40
+ function findHooksDir(cwd = process.cwd()) {
41
+ try {
42
+ const gitDir = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
43
+ cwd,
44
+ encoding: 'utf8',
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ }).trim();
47
+ return path.resolve(cwd, gitDir);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Retrieves the list of staged files from the Git index.
55
+ * Uses NUL-delimited output (-z) to support spaces, Unicode, and escapes.
56
+ *
57
+ * @param {string} [cwd=process.cwd()] - Working directory
58
+ * @returns {Array<{ status: string, path: string, oldPath?: string }>} Staged entries
59
+ */
60
+ function getStagedFiles(cwd = process.cwd()) {
61
+ try {
62
+ const output = execFileSync(
63
+ 'git',
64
+ ['diff', '--cached', '-z', '--name-status'],
65
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
66
+ );
67
+
68
+ if (!output) return [];
69
+
70
+ const parts = output.split('\0');
71
+ const entries = [];
72
+
73
+ let i = 0;
74
+ while (i < parts.length) {
75
+ const statusRaw = parts[i];
76
+ if (!statusRaw) {
77
+ i++;
78
+ continue;
79
+ }
80
+
81
+ const status = statusRaw[0];
82
+ if (status === 'R' || status === 'C') {
83
+ // Rename or copy includes old path followed by new path
84
+ const oldPath = parts[i + 1];
85
+ const newPath = parts[i + 2];
86
+ if (newPath) {
87
+ entries.push({ status, path: newPath, oldPath });
88
+ }
89
+ i += 3;
90
+ } else {
91
+ const filePath = parts[i + 1];
92
+ if (filePath) {
93
+ entries.push({ status, path: filePath });
94
+ }
95
+ i += 2;
96
+ }
97
+ }
98
+
99
+ return entries;
100
+ } catch (err) {
101
+ throw new Error(`Failed to query staged files from Git: ${err.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Reads the content of a file directly from the Git staged index.
107
+ *
108
+ * @param {string} relativePath - Path relative to repo root
109
+ * @param {string} [cwd=process.cwd()] - Working directory
110
+ * @returns {Buffer|null} Staged file buffer or null if deleted/missing
111
+ */
112
+ function getStagedBlob(relativePath, cwd = process.cwd()) {
113
+ try {
114
+ const posixPath = relativePath.replace(/\\/g, '/');
115
+ const buf = execFileSync('git', ['show', `:${posixPath}`], {
116
+ cwd,
117
+ stdio: ['pipe', 'pipe', 'pipe'],
118
+ maxBuffer: 10 * 1024 * 1024,
119
+ });
120
+ return buf;
121
+ } catch (err) {
122
+ // If file was deleted or unreadable in index, return null
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Extracts added lines from git diff --cached -U0 for each staged file.
129
+ * Returns only lines that were added (+), excluding diff headers and deleted lines (-).
130
+ *
131
+ * @param {string} [cwd=process.cwd()] - Working directory
132
+ * @returns {Map<string, Array<{ line: number, content: string }>>} Added lines by file
133
+ */
134
+ function getStagedAddedLines(cwd = process.cwd()) {
135
+ const addedLinesByFile = new Map();
136
+
137
+ try {
138
+ const diffOutput = execFileSync('git', ['diff', '--cached', '-U0'], {
139
+ cwd,
140
+ encoding: 'utf8',
141
+ stdio: ['pipe', 'pipe', 'pipe'],
142
+ maxBuffer: 10 * 1024 * 1024,
143
+ });
144
+
145
+ if (!diffOutput) return addedLinesByFile;
146
+
147
+ const lines = diffOutput.split(/\r?\n/);
148
+ let currentFile = null;
149
+ let currentLineNum = 0;
150
+
151
+ for (const line of lines) {
152
+ if (line.startsWith('+++ b/')) {
153
+ currentFile = line.slice(6).trim();
154
+ if (!addedLinesByFile.has(currentFile)) {
155
+ addedLinesByFile.set(currentFile, []);
156
+ }
157
+ } else if (line.startsWith('@@ ')) {
158
+ // Parse @@ -a,b +c,d @@ to get current target line number
159
+ const match = line.match(/\+([0-9]+)(?:,([0-9]+))?/);
160
+ if (match) {
161
+ currentLineNum = parseInt(match[1], 10);
162
+ }
163
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
164
+ if (currentFile) {
165
+ const content = line.slice(1);
166
+ addedLinesByFile.get(currentFile).push({
167
+ line: currentLineNum,
168
+ content,
169
+ });
170
+ }
171
+ currentLineNum++;
172
+ }
173
+ }
174
+
175
+ return addedLinesByFile;
176
+ } catch {
177
+ return addedLinesByFile;
178
+ }
179
+ }
180
+
181
+ module.exports = {
182
+ findGitRoot,
183
+ findHooksDir,
184
+ getStagedFiles,
185
+ getStagedBlob,
186
+ getStagedAddedLines,
187
+ };