gemstack-ai 1.0.1 → 1.1.2
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.
- package/.agents/rules/01-gemstack-core.md +15 -0
- package/.agents/rules/02-gemstack-constitution.md +12 -1
- package/.agents/skills/gemstack-handoff/SKILL.md +2 -1
- package/.agents/skills/gemstack-plan/SKILL.md +2 -1
- package/.agents/skills/gemstack-review/SKILL.md +7 -5
- package/.agents/skills/gemstack-ship/SKILL.md +5 -0
- package/.agents/skills/gemstack-spec/SKILL.md +3 -2
- package/.agents/skills/gemstack-tasks/SKILL.md +4 -3
- package/.gemstack/state.json +20 -2
- package/CHANGELOG.md +52 -0
- package/MANUAL.md +4 -4
- package/README.md +36 -8
- package/RELEASE_NOTES.md +105 -0
- package/assets/logo.jpg +0 -0
- package/docs/architecture-consistency.md +144 -0
- package/gemstack-ai-1.1.2.tgz +0 -0
- package/handoff.md +27 -40
- package/package.json +3 -2
- package/scripts/ci/smoke-cli.js +1 -0
- package/specs/006-architecture-consistency-engine/.gemstack.json +9 -0
- package/specs/006-architecture-consistency-engine/plan.md +319 -0
- package/specs/006-architecture-consistency-engine/spec.md +179 -0
- package/specs/006-architecture-consistency-engine/tasks.md +532 -0
- package/specs/templates/plan.md +11 -0
- package/specs/templates/spec.md +17 -0
- package/specs/templates/tasks.md +1 -0
- package/src/cli.js +9 -4
- package/src/commands/verify.js +311 -0
- package/src/lib/contracts.js +388 -0
- package/src/lib/findings.js +227 -0
- package/src/lib/hasher.js +103 -0
- package/src/lib/state.js +143 -0
- package/src/mcp-server.js +1 -1
- package/template/.agents/rules/01-gemstack-core.md +15 -0
- package/template/.agents/rules/02-gemstack-constitution.md +12 -1
- package/template/.agents/skills/gemstack-handoff/SKILL.md +2 -1
- package/template/.agents/skills/gemstack-plan/SKILL.md +2 -1
- package/template/.agents/skills/gemstack-review/SKILL.md +7 -5
- package/template/.agents/skills/gemstack-ship/SKILL.md +5 -0
- package/template/.agents/skills/gemstack-spec/SKILL.md +3 -2
- package/template/.agents/skills/gemstack-tasks/SKILL.md +4 -3
- package/template/docs/architecture-consistency.md +144 -0
- package/template/specs/templates/plan.md +11 -0
- package/template/specs/templates/spec.md +17 -0
- package/template/specs/templates/tasks.md +1 -0
- package/gemstack-ai-1.0.1.tgz +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const { normalizePath } = require('./hasher');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Computes canonical 64-character lowercase hexadecimal SHA-256 fingerprint for a finding.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} params
|
|
8
|
+
* @param {string} params.code - Finding error code
|
|
9
|
+
* @param {string|null} params.contractId - Optional contract id
|
|
10
|
+
* @param {string} params.phase - Phase where violation was found
|
|
11
|
+
* @param {string} params.location - File path of the violation
|
|
12
|
+
* @returns {string} 64-char lowercase hex SHA-256
|
|
13
|
+
*/
|
|
14
|
+
function computeFindingFingerprint({ code, contractId, phase, location }) {
|
|
15
|
+
const normLocation = normalizePath(location);
|
|
16
|
+
const payload = JSON.stringify({
|
|
17
|
+
code: code || 'UNKNOWN_ERROR',
|
|
18
|
+
contractId: contractId ?? null,
|
|
19
|
+
phase: phase || 'unknown',
|
|
20
|
+
location: normLocation || null
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formats a cosmetic display string for a fingerprint (12-char prefix).
|
|
28
|
+
* Never used for internal storage, indexing or anti-loop logic.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} fingerprint
|
|
31
|
+
* @returns {string} 12-char display token
|
|
32
|
+
*/
|
|
33
|
+
function formatDisplayFingerprint(fingerprint) {
|
|
34
|
+
if (typeof fingerprint !== 'string') return '';
|
|
35
|
+
return fingerprint.slice(0, 12);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a structured Finding object.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} params
|
|
42
|
+
* @returns {object} Finding
|
|
43
|
+
*/
|
|
44
|
+
function createFinding({ code, contractId, phase, location, delta = null, details = null }) {
|
|
45
|
+
const fp = computeFindingFingerprint({ code, contractId, phase, location });
|
|
46
|
+
return {
|
|
47
|
+
fingerprint: fp,
|
|
48
|
+
display_id: formatDisplayFingerprint(fp),
|
|
49
|
+
code,
|
|
50
|
+
contractId: contractId ?? null,
|
|
51
|
+
phase,
|
|
52
|
+
location: normalizePath(location),
|
|
53
|
+
delta,
|
|
54
|
+
details,
|
|
55
|
+
status: 'OPEN',
|
|
56
|
+
is_blocking: true,
|
|
57
|
+
detected_at: new Date().toISOString(),
|
|
58
|
+
resolved_at: null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Computes the full frozen contextHash for an accepted exception.
|
|
64
|
+
* Context identity = SHA-256(upstreamAcceptedPhaseHash + currentComparedPhaseHash + normalizedContractRepresentation).
|
|
65
|
+
*
|
|
66
|
+
* @param {object} params
|
|
67
|
+
* @param {string} params.upstreamAcceptedPhaseHash
|
|
68
|
+
* @param {string} params.currentComparedPhaseHash
|
|
69
|
+
* @param {string} params.normalizedContractRepresentation - Deterministically serialized contract JSON
|
|
70
|
+
* @returns {string} 64-character lowercase hex SHA-256
|
|
71
|
+
*/
|
|
72
|
+
function computeContextHash({ upstreamAcceptedPhaseHash, currentComparedPhaseHash, normalizedContractRepresentation }) {
|
|
73
|
+
const payload = JSON.stringify({
|
|
74
|
+
upstreamAcceptedPhaseHash: upstreamAcceptedPhaseHash || '',
|
|
75
|
+
currentComparedPhaseHash: currentComparedPhaseHash || '',
|
|
76
|
+
normalizedContractRepresentation: normalizedContractRepresentation || ''
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reconciles findings between successive checks (Anti-Loop mechanism):
|
|
84
|
+
* - If an existing finding is no longer in current violations -> status becomes 'RESOLVED'.
|
|
85
|
+
* - If a finding was 'RESOLVED' but the violation recurs -> status reopens to 'OPEN'.
|
|
86
|
+
* - New violations are added as 'OPEN'.
|
|
87
|
+
*
|
|
88
|
+
* @param {Array<object>} existingFindings
|
|
89
|
+
* @param {Array<object>} currentViolations
|
|
90
|
+
* @returns {Array<object>} Reconciled findings
|
|
91
|
+
*/
|
|
92
|
+
function reconcileFindings(existingFindings = [], currentViolations = []) {
|
|
93
|
+
const reconciled = [];
|
|
94
|
+
const currentViolationFps = new Map();
|
|
95
|
+
|
|
96
|
+
for (const v of currentViolations) {
|
|
97
|
+
const fp = computeFindingFingerprint({
|
|
98
|
+
code: v.code,
|
|
99
|
+
contractId: v.contractId,
|
|
100
|
+
phase: v.phase,
|
|
101
|
+
location: v.location
|
|
102
|
+
});
|
|
103
|
+
currentViolationFps.set(fp, v);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Process existing findings
|
|
107
|
+
for (const f of existingFindings) {
|
|
108
|
+
if (currentViolationFps.has(f.fingerprint)) {
|
|
109
|
+
const v = currentViolationFps.get(f.fingerprint);
|
|
110
|
+
currentViolationFps.delete(f.fingerprint); // Handled
|
|
111
|
+
|
|
112
|
+
// If it was RESOLVED, reopen it (anti-loop protection)
|
|
113
|
+
if (f.status === 'RESOLVED') {
|
|
114
|
+
reconciled.push({
|
|
115
|
+
...f,
|
|
116
|
+
status: 'OPEN',
|
|
117
|
+
is_blocking: true,
|
|
118
|
+
delta: v.delta,
|
|
119
|
+
detected_at: new Date().toISOString(),
|
|
120
|
+
resolved_at: null
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
reconciled.push({
|
|
124
|
+
...f,
|
|
125
|
+
delta: v.delta,
|
|
126
|
+
is_blocking: f.status !== 'ACCEPTED_EXCEPTION'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Violation not present in current analysis -> mark RESOLVED if it was OPEN
|
|
131
|
+
if (f.status === 'OPEN') {
|
|
132
|
+
reconciled.push({
|
|
133
|
+
...f,
|
|
134
|
+
status: 'RESOLVED',
|
|
135
|
+
is_blocking: false,
|
|
136
|
+
resolved_at: new Date().toISOString()
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
reconciled.push(f);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Any remaining current violations are new
|
|
145
|
+
for (const [fp, v] of currentViolationFps.entries()) {
|
|
146
|
+
reconciled.push(createFinding(v));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return reconciled;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Evaluates accepted exceptions against current findings.
|
|
154
|
+
* If an exception matches the finding fingerprint AND has an identical full contextHash,
|
|
155
|
+
* it suppresses the blocker (is_blocking: false, status: 'ACCEPTED_EXCEPTION').
|
|
156
|
+
* If contextHash differs, the exception does NOT suppress and finding remains OPEN / blocking.
|
|
157
|
+
*
|
|
158
|
+
* @param {Array<object>} findings
|
|
159
|
+
* @param {Array<object>} acceptedExceptions
|
|
160
|
+
* @param {object} currentContext - { upstreamAcceptedPhaseHash, currentComparedPhaseHash, normalizedContractRepresentation }
|
|
161
|
+
* @returns {Array<object>} Evaluated findings
|
|
162
|
+
*/
|
|
163
|
+
function evaluateAcceptedExceptions(findings = [], acceptedExceptions = [], currentContext = {}) {
|
|
164
|
+
const currentContextHash = computeContextHash(currentContext);
|
|
165
|
+
|
|
166
|
+
const exceptionMap = new Map();
|
|
167
|
+
for (const ex of acceptedExceptions) {
|
|
168
|
+
if (ex && ex.fingerprint) {
|
|
169
|
+
exceptionMap.set(ex.fingerprint, ex);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return findings.map(f => {
|
|
174
|
+
if (exceptionMap.has(f.fingerprint)) {
|
|
175
|
+
const ex = exceptionMap.get(f.fingerprint);
|
|
176
|
+
if (ex.contextHash === currentContextHash) {
|
|
177
|
+
return {
|
|
178
|
+
...f,
|
|
179
|
+
status: 'ACCEPTED_EXCEPTION',
|
|
180
|
+
is_blocking: false,
|
|
181
|
+
exceptionReason: ex.reason || 'Approved exception'
|
|
182
|
+
};
|
|
183
|
+
} else {
|
|
184
|
+
// Context mutated -> exception invalidated
|
|
185
|
+
return {
|
|
186
|
+
...f,
|
|
187
|
+
status: 'OPEN',
|
|
188
|
+
is_blocking: true,
|
|
189
|
+
exceptionInvalidated: true
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return f;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Marks findings as SUPERSEDED if associated contracts were formally amended or removed.
|
|
199
|
+
*
|
|
200
|
+
* @param {Array<object>} findings
|
|
201
|
+
* @param {Array<string>} activeContractIds
|
|
202
|
+
* @returns {Array<object>}
|
|
203
|
+
*/
|
|
204
|
+
function markSupersededFindings(findings = [], activeContractIds = []) {
|
|
205
|
+
const activeSet = new Set(activeContractIds);
|
|
206
|
+
return findings.map(f => {
|
|
207
|
+
if (f.contractId && !activeSet.has(f.contractId)) {
|
|
208
|
+
return {
|
|
209
|
+
...f,
|
|
210
|
+
status: 'SUPERSEDED',
|
|
211
|
+
is_blocking: false,
|
|
212
|
+
superseded_at: new Date().toISOString()
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return f;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
computeFindingFingerprint,
|
|
221
|
+
formatDisplayFingerprint,
|
|
222
|
+
createFinding,
|
|
223
|
+
computeContextHash,
|
|
224
|
+
reconcileFindings,
|
|
225
|
+
evaluateAcceptedExceptions,
|
|
226
|
+
markSupersededFindings
|
|
227
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes content for canonical hashing:
|
|
7
|
+
* - Checks and rejects UTF-8 BOM by throwing CONTRACT_PARSE_ERROR
|
|
8
|
+
* - Normalizes CRLF and lone CR to standard LF
|
|
9
|
+
* Does NOT trim or alter whitespace/markdown/json formatting.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} content
|
|
12
|
+
* @returns {string} normalized string
|
|
13
|
+
*/
|
|
14
|
+
function normalizeContent(content) {
|
|
15
|
+
if (typeof content !== 'string') {
|
|
16
|
+
throw new TypeError('Content must be a string');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Reject UTF-8 BOM
|
|
20
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
21
|
+
const err = new Error('UTF-8 BOM is forbidden in phase artifacts.');
|
|
22
|
+
err.code = 'CONTRACT_PARSE_ERROR';
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Normalize newlines: CRLF -> LF, then lone CR -> LF
|
|
27
|
+
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Computes canonical 64-character lowercase hexadecimal SHA-256 hash of normalized content.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} content
|
|
34
|
+
* @returns {string} 64-char lowercase hex SHA-256
|
|
35
|
+
*/
|
|
36
|
+
function hashContent(content) {
|
|
37
|
+
const normalized = normalizeContent(content);
|
|
38
|
+
return crypto.createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads a file as UTF-8 and computes its canonical hash.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} filePath
|
|
45
|
+
* @returns {string} 64-char lowercase hex SHA-256
|
|
46
|
+
*/
|
|
47
|
+
function hashFile(filePath) {
|
|
48
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
49
|
+
return hashContent(raw);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const WINDOWS_DRIVE_RE = /^[A-Za-z]:[\\/]/;
|
|
53
|
+
const WINDOWS_UNC_RE = /^\\\\[^\\/]+[\\/][^\\/]+/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Checks if a path string has Windows structure (drive letter, UNC, or backslashes).
|
|
57
|
+
*
|
|
58
|
+
* @param {string} p
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isWindowsPath(p) {
|
|
62
|
+
if (typeof p !== 'string') return false;
|
|
63
|
+
return WINDOWS_DRIVE_RE.test(p) || WINDOWS_UNC_RE.test(p) || p.includes('\\');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalizes file paths to repository-relative POSIX format (using forward slashes '/').
|
|
68
|
+
* Uses explicit path.win32 or path.posix based on input path flavor rather than host OS.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} filePath
|
|
71
|
+
* @param {string} [rootPath] - optional repository root
|
|
72
|
+
* @returns {string} normalized POSIX relative path
|
|
73
|
+
*/
|
|
74
|
+
function normalizePath(filePath, rootPath) {
|
|
75
|
+
if (!filePath) return '';
|
|
76
|
+
if (!rootPath) {
|
|
77
|
+
return filePath.replace(/\\/g, '/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fileIsWindows = isWindowsPath(filePath);
|
|
81
|
+
const rootIsWindows = isWindowsPath(rootPath);
|
|
82
|
+
|
|
83
|
+
const isPosixAbs = (p) => typeof p === 'string' && p.startsWith('/') && !WINDOWS_DRIVE_RE.test(p);
|
|
84
|
+
if ((fileIsWindows && isPosixAbs(rootPath)) || (isPosixAbs(filePath) && rootIsWindows)) {
|
|
85
|
+
throw new Error(`Incompatible mixed path flavors: filePath="${filePath}", rootPath="${rootPath}"`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const impl = (fileIsWindows || rootIsWindows) ? path.win32 : path.posix;
|
|
89
|
+
const rel = impl.relative(rootPath, filePath);
|
|
90
|
+
|
|
91
|
+
if (rel.startsWith('..\\') || rel.startsWith('../') || rel === '..') {
|
|
92
|
+
throw new Error(`Path "${filePath}" is outside root directory "${rootPath}"`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return rel.replace(/\\/g, '/');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
normalizeContent,
|
|
100
|
+
hashContent,
|
|
101
|
+
hashFile,
|
|
102
|
+
normalizePath
|
|
103
|
+
};
|
package/src/lib/state.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reads .gemstack/state.json with legacy fallback defaults.
|
|
6
|
+
* Preserves all unknown existing fields.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} rootPath - Workspace root path
|
|
9
|
+
* @returns {object} State object
|
|
10
|
+
*/
|
|
11
|
+
function readState(rootPath) {
|
|
12
|
+
const statePath = path.join(rootPath, '.gemstack', 'state.json');
|
|
13
|
+
if (!fs.existsSync(statePath)) {
|
|
14
|
+
return {
|
|
15
|
+
version: '0.1',
|
|
16
|
+
current_phase: null,
|
|
17
|
+
status: null,
|
|
18
|
+
stop_reason: null,
|
|
19
|
+
active_spec: null,
|
|
20
|
+
completed_phases: [],
|
|
21
|
+
phase_hashes: null,
|
|
22
|
+
consistency: null,
|
|
23
|
+
guard_mode: { careful: false, freeze: false, allowed_paths: [] }
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(statePath, 'utf8');
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
const { findings, accepted_exceptions, ...cleanParsed } = parsed;
|
|
31
|
+
return {
|
|
32
|
+
version: cleanParsed.version || '0.1',
|
|
33
|
+
current_phase: cleanParsed.current_phase ?? null,
|
|
34
|
+
status: cleanParsed.status ?? null,
|
|
35
|
+
stop_reason: cleanParsed.stop_reason ?? null,
|
|
36
|
+
active_spec: cleanParsed.active_spec ?? null,
|
|
37
|
+
completed_phases: Array.isArray(cleanParsed.completed_phases) ? cleanParsed.completed_phases : [],
|
|
38
|
+
phase_hashes: cleanParsed.phase_hashes ?? null,
|
|
39
|
+
consistency: cleanParsed.consistency ?? null,
|
|
40
|
+
guard_mode: cleanParsed.guard_mode || { careful: false, freeze: false, allowed_paths: [] },
|
|
41
|
+
...cleanParsed // preserve any extra operational fields (excluding findings / accepted_exceptions)
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`Failed to parse .gemstack/state.json: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Writes an object atomically to disk by writing to a temporary file in the same directory
|
|
50
|
+
* and renaming it over the destination. Bounded retry for Windows locks.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} filePath - Absolute path to destination file
|
|
53
|
+
* @param {object} data - Object to serialize
|
|
54
|
+
*/
|
|
55
|
+
function writeJsonAtomic(filePath, data) {
|
|
56
|
+
const dir = path.dirname(filePath);
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
62
|
+
const serialized = JSON.stringify(data, null, 2) + '\n';
|
|
63
|
+
fs.writeFileSync(tmpPath, serialized, 'utf8');
|
|
64
|
+
|
|
65
|
+
// Bounded retry for Windows transient file locks
|
|
66
|
+
let attempts = 0;
|
|
67
|
+
const maxAttempts = 5;
|
|
68
|
+
while (attempts < maxAttempts) {
|
|
69
|
+
try {
|
|
70
|
+
fs.renameSync(tmpPath, filePath);
|
|
71
|
+
return;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
attempts++;
|
|
74
|
+
if (attempts >= maxAttempts) {
|
|
75
|
+
// cleanup tmp
|
|
76
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
// Busy wait short sleep
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
while (Date.now() - start < 20) {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Writes .gemstack/state.json atomically.
|
|
88
|
+
* Strips historical findings and accepted_exceptions to enforce persistence boundary.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} rootPath - Workspace root path
|
|
91
|
+
* @param {object} stateObj - State data
|
|
92
|
+
*/
|
|
93
|
+
function writeStateAtomic(rootPath, stateObj) {
|
|
94
|
+
const statePath = path.join(rootPath, '.gemstack', 'state.json');
|
|
95
|
+
const { findings, accepted_exceptions, ...operationalState } = stateObj;
|
|
96
|
+
writeJsonAtomic(statePath, operationalState);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads feature sidecar .gemstack.json with safe defaults.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} featureDir - Path to specs/<feature>
|
|
103
|
+
* @returns {object} Sidecar data
|
|
104
|
+
*/
|
|
105
|
+
function readSidecar(featureDir) {
|
|
106
|
+
const sidecarPath = path.join(featureDir, '.gemstack.json');
|
|
107
|
+
if (!fs.existsSync(sidecarPath)) {
|
|
108
|
+
return {
|
|
109
|
+
phase_hashes: {},
|
|
110
|
+
historical_findings: [],
|
|
111
|
+
accepted_exceptions: []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const raw = fs.readFileSync(sidecarPath, 'utf8');
|
|
116
|
+
return JSON.parse(raw);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
phase_hashes: {},
|
|
120
|
+
historical_findings: [],
|
|
121
|
+
accepted_exceptions: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Writes feature sidecar .gemstack.json atomically.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} featureDir - Path to specs/<feature>
|
|
130
|
+
* @param {object} sidecarObj - Data to write
|
|
131
|
+
*/
|
|
132
|
+
function writeSidecarAtomic(featureDir, sidecarObj) {
|
|
133
|
+
const sidecarPath = path.join(featureDir, '.gemstack.json');
|
|
134
|
+
writeJsonAtomic(sidecarPath, sidecarObj);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
readState,
|
|
139
|
+
writeStateAtomic,
|
|
140
|
+
readSidecar,
|
|
141
|
+
writeSidecarAtomic,
|
|
142
|
+
writeJsonAtomic
|
|
143
|
+
};
|
package/src/mcp-server.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const readline = require('readline');
|
|
10
|
-
const manifest = require('
|
|
10
|
+
const manifest = require('./lib/manifest');
|
|
11
11
|
|
|
12
12
|
// MCP Protocol structures
|
|
13
13
|
function sendResponse(id, result, error = null) {
|
|
@@ -13,6 +13,7 @@ Eres Antigravity operando bajo el framework **Gemstack**, una metodología local
|
|
|
13
13
|
2. **Aprobación para Deploy/Push:** Nunca hagas `git push`, merge o deploy sin aprobación explícita.
|
|
14
14
|
3. **Handoff Inmutable:** NUNCA borres la sección "Intentos fallidos" de `handoff.md`. Si crece demasiado, mueve de forma segura el contenido antiguo a `handoff_archive.md`.
|
|
15
15
|
4. **Dependencias y Auth:** Cambios a la arquitectura de autenticación, migraciones de base de datos o instalación de dependencias globales requieren generación de plan técnico y aprobación humana.
|
|
16
|
+
5. **Consistencia de Arquitectura y Congelamiento de Fases (Upgrade A):** Las decisiones arquitectónicas congeladas en `gemstack-contracts` no pueden ser contradichas silenciosamente por fases posteriores. Toda enmienda requiere aprobación humana explícita.
|
|
16
17
|
|
|
17
18
|
## Pseudo-Comandos (Ruteo Obligatorio)
|
|
18
19
|
Si el usuario empieza su mensaje con uno de estos comandos, **NO improvises. DEBES cargar o seguir el skill correspondiente**:
|
|
@@ -49,3 +50,17 @@ Si el usuario empieza su mensaje con uno de estos comandos, **NO improvises. DEB
|
|
|
49
50
|
- `/guard` -> Invoca `gemstack-guard`
|
|
50
51
|
- `/unfreeze` -> Invoca `gemstack-guard`
|
|
51
52
|
|
|
53
|
+
## Ruteo Semántico por Intención (Intent-Based Routing)
|
|
54
|
+
Si el usuario interactúa en lenguaje natural sin usar un `/comando` explícito, DEBES detectar la intención subyacente y activar el protocolo correspondiente:
|
|
55
|
+
1. **Nueva funcionalidad o módulo mayor sin spec activa:**
|
|
56
|
+
- Si pide crear o agregar una feature sustancial (ej. "pon una parte para editar paquetes", "vamos a agregar cobro bimoneda"), **NO saltes directo al código**. Activa `gemstack-spec` para definir requisitos y criterios de éxito antes de implementar.
|
|
57
|
+
2. **Solicitud de pruebas o validación:**
|
|
58
|
+
- Si el usuario dice "haz pruebas", "valida lo hecho", "comprueba que funcione" o "verifica los cambios", activa `gemstack-qa`.
|
|
59
|
+
3. **Reporte de error o bug:**
|
|
60
|
+
- Si el usuario reporta que algo falló o no funciona como se esperaba, activa `gemstack-investigate` (principio: *No fixes before investigation*).
|
|
61
|
+
4. **Cierre o pausa de sesión:**
|
|
62
|
+
- Si el usuario indica "terminamos por hoy", "voy a pausar", "dejo esto listo" o "prepara el resumen", activa `gemstack-handoff`.
|
|
63
|
+
5. **Auditoría de seguridad:**
|
|
64
|
+
- Si pide revisar seguridad, permisos, tokens o vulnerabilidades, activa `gemstack-cso`.
|
|
65
|
+
6. **Entrega o preparación de release:**
|
|
66
|
+
- Si pide preparar el merge, PR o entrega formal de la feature terminada, activa `gemstack-ship`.
|
|
@@ -14,11 +14,16 @@ Toda nueva funcionalidad DEBE nacer y estar estructurada preferentemente como un
|
|
|
14
14
|
## Article II: CLI / Interface Mandate
|
|
15
15
|
Cada módulo o librería clave debe tener una forma de probarse e interactuar textualmente (CLI, scripts independientes, peticiones directas de texto o JSON). Evita componentes opacos que solo puedan probarse levantando interfaces gráficas complejas.
|
|
16
16
|
|
|
17
|
-
## Article III: Test-First Imperative (NON-NEGOTIABLE)
|
|
17
|
+
## Article III: Test-First Imperative & Zero Silent Failures (NON-NEGOTIABLE)
|
|
18
18
|
NUNCA escribas el código de implementación antes que los tests (TDD).
|
|
19
19
|
1. Escribe los tests (unitarios, de integración o contratos) basándote en la especificación.
|
|
20
20
|
2. Si es posible, demuestra que fallan.
|
|
21
21
|
3. Solo entonces, escribe la implementación real.
|
|
22
|
+
4. **Zero Silent Failures (Multiplataforma Windows / Linux / macOS):**
|
|
23
|
+
- PROHIBIDO el uso de operadores de supresión de shell que enmascaren fallos en scripts de `test` de `package.json` (ej. `2>nul`, `2>/dev/null || true`).
|
|
24
|
+
- El script de test DEBE terminar con exit code distinto de cero si ocurre cualquier falla.
|
|
25
|
+
- En monorepos TypeScript, usa runners multiplataforma estandarizados (ej. `tsx --test src/**/*.test.ts tests/**/*.test.ts`, `node --test`, `vitest` o `jest`).
|
|
26
|
+
- El test runner debe validar que la suite ejecutó efectivamente pruebas (`tests > 0`). Un reporte de 0 tests ejecutados finalizando con exit code 0 es considerado un falso positivo inaceptable.
|
|
22
27
|
|
|
23
28
|
## Article IV: Zero Assumptions (No Hallucinations)
|
|
24
29
|
Si un requerimiento del humano es vago, la IA NO debe adivinar.
|
|
@@ -42,3 +47,9 @@ Usa las funciones del framework y librerías estándar nativamente en lugar de c
|
|
|
42
47
|
|
|
43
48
|
## Article IX: Integration-First Testing
|
|
44
49
|
Prioriza el testing realista. Si puedes probar el contrato real o la base de datos local real (con un entorno temporal) por encima de mocks complejos, hazlo. El código generado debe funcionar en la práctica.
|
|
50
|
+
|
|
51
|
+
## Article X: Architecture Consistency & Immutability Gate (Upgrade A)
|
|
52
|
+
Las decisiones arquitectónicas congeladas no pueden ser contradichas silenciosamente por fases posteriores.
|
|
53
|
+
- Antes de avanzar de fase, el hash del artefacto aguas arriba debe ser válido, los contratos compatibles y la cantidad de bloqueadores deterministas abiertos debe ser exactamente 0.
|
|
54
|
+
- Los artefactos de fases aprobadas quedan sellados criptográficamente (SHA-256) y no pueden mutar sin enmienda explícita aprobada por humanos.
|
|
55
|
+
- Proyectos preexistentes sin bloques de contratos operan en modo LEGACY transparente sin fallas.
|
|
@@ -17,5 +17,6 @@ Esta habilidad se ejecuta cuando el usuario pide terminar la sesión, invoca `/h
|
|
|
17
17
|
5. **ARCHIVADO SEGURO:** Si "Intentos fallidos" en `handoff.md` tiene demasiados puntos (más de 10-15), CORTA los elementos más antiguos y pégalos en `handoff_archive.md` bajo su lista, conservando solo los 3-5 intentos recientes en `handoff.md`.
|
|
18
18
|
6. Define los "5. Próximos pasos" exactos para la próxima sesión.
|
|
19
19
|
7. Guarda los cambios en `handoff.md` (y `handoff_archive.md` si fue necesario).
|
|
20
|
-
8.
|
|
20
|
+
8. **Sincronización de Estado:** Verifica `.gemstack/state.json` asegurando que `current_phase` refleje con precisión si hay una spec activa o en espera, y actualiza `last_update` con la marca temporal actual.
|
|
21
|
+
9. Despídete del usuario indicando que el handoff está listo.
|
|
21
22
|
|
|
@@ -16,5 +16,6 @@ Invocado mediante `/plan`.
|
|
|
16
16
|
4. Genera `specs/[nombre-feature]/plan.md` usando `specs/templates/plan.md`.
|
|
17
17
|
5. Detalla el stack y llena la tabla "Complexity Tracking" SÓLO si rompiste alguna regla de la constitución y necesitas justificarlo.
|
|
18
18
|
6. Opcionalmente, genera los entregables satélites: `data-model.md`, `contracts/` (para APIs/Interfaces), y `quickstart.md`.
|
|
19
|
-
7.
|
|
19
|
+
7. **Herencia y Contratos Aditivos (Upgrade A)**: PLAN hereda automáticamente los contratos declarados en `spec.md`. No contradigas ni alteres los contratos congelados heredados; si requieres contratos técnicos adicionales, decláralos de forma compatible y aditiva en el bloque ````gemstack-contracts ```` de `plan.md`.
|
|
20
|
+
8. Pide aprobación al usuario antes de permitir la ejecución de `/tasks`.
|
|
20
21
|
|
|
@@ -11,8 +11,10 @@ Invocado mediante `/review`.
|
|
|
11
11
|
|
|
12
12
|
## Proceso:
|
|
13
13
|
1. Analiza el código modificado (git diff, o archivos editados).
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
4. Verifica que
|
|
17
|
-
5.
|
|
18
|
-
6.
|
|
14
|
+
2. **Validación Determinista Primero (Upgrade A)**: Ejecuta `node src/cli.js verify` o comprueba contratos congelados, hashes de fase y bloqueadores antes de proceder a la revisión semántica.
|
|
15
|
+
3. Proporciona al revisor la lista de contratos efectivos, hashes de fase y hallazgos deterministas para no forzarlo a redescubrir desviaciones mecánicas.
|
|
16
|
+
4. Verifica que las convenciones arquitectónicas del proyecto se respeten.
|
|
17
|
+
5. VERIFICACIÓN DE SEGURIDAD: Revisa obligatoriamente `.agents/rules/03-gemstack-security.md` para garantizar que el código propuesto no introduzca brechas de seguridad (IDOR, XSS, tokens expuestos).
|
|
18
|
+
6. Verifica que los tests cubran adecuadamente los cambios.
|
|
19
|
+
7. Emite sugerencias o aplica correcciones automáticas si son triviales.
|
|
20
|
+
8. Si el código está listo, sugiere `/qa` o `/ship`.
|
|
@@ -16,4 +16,9 @@ Invocado mediante `/ship`.
|
|
|
16
16
|
4. Genera un PR summary si se pide.
|
|
17
17
|
5. NO hagas push, merge o deploy sin aprobación explícita.
|
|
18
18
|
6. Sugiere ejecutar `/handoff` para documentar la entrega en la memoria del proyecto.
|
|
19
|
+
7. **Sincronización de Estado:** Actualiza `.gemstack/state.json`:
|
|
20
|
+
- Establece `"active_spec": null`
|
|
21
|
+
- Registra `"last_completed_feature": "specs/[nombre-feature]/"`
|
|
22
|
+
- Establece `"current_phase": "shipped"`
|
|
23
|
+
- Actualiza `"last_update"` con el timestamp ISO actual.
|
|
19
24
|
|
|
@@ -19,6 +19,7 @@ Eres un Product Manager técnico. Tu objetivo es convertir ideas vagas en requis
|
|
|
19
19
|
4. El documento DEBE incluir: Historias de usuario priorizadas (P1, P2) que sean independientemente testeables, Criterios de Éxito medibles, y Casos Extremos.
|
|
20
20
|
5. **CERO SUPOSICIONES**: Si el usuario omitió detalles, NO adivines. Usa el marcador `[NEEDS CLARIFICATION: tu duda]` en el documento.
|
|
21
21
|
6. No describas implementación técnica (nada de stacks, bases de datos o APIs). Concéntrate estrictamente en el "Qué" y "Por qué".
|
|
22
|
-
7.
|
|
23
|
-
8.
|
|
22
|
+
7. **Contratos Arquitectónicos Congelados (Upgrade A)**: Si la funcionalidad implica decisiones estructurales críticas (estados enum, tuplas de identidad, reglas de procedencia, invariantes booleanas, límites de roadmap o límites boundary), decláralos explícitamente en el bloque canónico ````gemstack-contracts ````. Congela solo decisiones materiales, no texto arbitrario.
|
|
23
|
+
8. Actualiza el archivo `.gemstack/state.json` para reflejar la rama activa y fase: `{"active_spec": "specs/[nombre-feature]/", "current_phase": "spec", "last_update": "<timestamp>"}`.
|
|
24
|
+
9. Una vez finalizado, indica al usuario que puede revisar la especificación y, tras resolver las dudas, ejecutar `/plan`.
|
|
24
25
|
|
|
@@ -12,7 +12,8 @@ Invocado mediante `/tasks`.
|
|
|
12
12
|
## Proceso:
|
|
13
13
|
1. Lee `specs/[nombre-feature]/plan.md` y, si existen, `data-model.md` y la carpeta `contracts/`.
|
|
14
14
|
2. Convierte los contratos, entidades y el plan en una lista estricta de ejecución en `specs/[nombre-feature]/tasks.md` usando la plantilla `specs/templates/tasks.md`.
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
15
|
+
3. **Herencia de Contratos (Upgrade A)**: TASKS hereda automáticamente los contratos consolidados de SPEC y PLAN. No se requiere declarar un bloque de contratos propio salvo que se agreguen contratos operacionales específicos de tareas.
|
|
16
|
+
4. Aplica Test-First: Las tareas de escribir pruebas (y validarlas) deben ir ANTES que la implementación de código.
|
|
17
|
+
5. Usa el marcador `[P]` para tareas independientes que se puedan paralelizar.
|
|
18
|
+
6. Ofrece al usuario comenzar automáticamente con la primera tarea o delegar a subagentes paralelos si hay múltiples `[P]`.
|
|
18
19
|
|