@tudeorangbiasa/sdd-multiagent-opencode 0.2.2 → 0.2.3
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/.opencode/commands/sdd-construct.md +693 -0
- package/.opencode/commands/sdd-quick.md +136 -105
- package/.sdd/templates/model-profile-template.json +4 -2
- package/.sdd/templates/reasoning-profile-template.json +4 -2
- package/GUIDE.md +141 -4
- package/README.md +100 -33
- package/bin/sdd-quick.js +607 -0
- package/package.json +4 -3
package/bin/sdd-quick.js
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sdd-quick CLI Wrapper
|
|
5
|
+
* Deterministic pre-flight and post-flight checks for quick cosmetic/config changes.
|
|
6
|
+
* Handles: classification, lock management, impact scan, verification, ledger append.
|
|
7
|
+
* LLM is only invoked for the actual edit (Phase 3), never for classification or verification.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
|
|
19
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const LOCK_FILE = path.join(projectRoot, "specs", ".sdd-lock");
|
|
22
|
+
const LEDGER_FILE = path.join(projectRoot, "specs", "QUICKFIX_LOG.md");
|
|
23
|
+
const LEDGER_ARCHIVE = path.join(projectRoot, "specs", "QUICKFIX_LOG_ARCHIVE.md");
|
|
24
|
+
const MAX_LEDGER_ENTRIES = 50;
|
|
25
|
+
const ARCHIVE_THRESHOLD = 25;
|
|
26
|
+
const MAX_FILE_LINES_FOR_FULL_RETURN = 500;
|
|
27
|
+
const MAX_CROSS_FILE_REFS = 5;
|
|
28
|
+
const MAX_STRING_DELTA = 20;
|
|
29
|
+
const MAX_STRING_PERCENT_INCREASE = 50;
|
|
30
|
+
|
|
31
|
+
const CONTRACT_PATH_PATTERNS = [
|
|
32
|
+
/\/api\//,
|
|
33
|
+
/\/dto\//,
|
|
34
|
+
/\/schema\//,
|
|
35
|
+
/\/types\/public-api\.ts$/,
|
|
36
|
+
/\/graphql\/types\.ts$/,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const LOGIC_OPERATORS = /\b(>=|<=|==|===|!=|!==|&&|\|\|)\b|[><]{2,}|[&|]{2,}|(?<![-=])[><](?![=-])/;
|
|
40
|
+
const LOGIC_KEYWORDS = /\b(if|else|switch|case|for|while|return|throw|try|catch|finally)\b/;
|
|
41
|
+
|
|
42
|
+
// ─── Utility ─────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function logError(msg) {
|
|
45
|
+
console.error(`❌ ${msg}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function logWarn(msg) {
|
|
49
|
+
console.warn(`⚠️ ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logInfo(msg) {
|
|
53
|
+
console.log(`ℹ️ ${msg}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function logSuccess(msg) {
|
|
57
|
+
console.log(`✅ ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exitWith(code, msg) {
|
|
61
|
+
if (msg) logError(msg);
|
|
62
|
+
process.exit(code);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function slugify(text) {
|
|
66
|
+
return text
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
69
|
+
.replace(/\s+/g, "-")
|
|
70
|
+
.replace(/-+/g, "-")
|
|
71
|
+
.slice(0, 40);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function now() {
|
|
75
|
+
return new Date().toISOString().replace("T", " ").slice(0, 16);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Phase 1a: Keyword Scan (CLI, 0 tokens) ──────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function keywordScan(request) {
|
|
81
|
+
const lower = request.toLowerCase();
|
|
82
|
+
|
|
83
|
+
if (LOGIC_OPERATORS.test(request)) {
|
|
84
|
+
return {
|
|
85
|
+
type: "LOGIC",
|
|
86
|
+
reason: "Request contains operators (>, <, >=, <=, ==, ===, !=, !==, &&, ||, !)",
|
|
87
|
+
blocked: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (LOGIC_KEYWORDS.test(lower)) {
|
|
92
|
+
return {
|
|
93
|
+
type: "LOGIC",
|
|
94
|
+
reason: "Request contains control flow keywords (if, else, switch, case, for, while, return, throw, try, catch)",
|
|
95
|
+
blocked: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { type: "COSMETIC_OR_CONFIG", blocked: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Phase 1b: Context-Aware Classification (LLM, ~50 tokens) ───────────────
|
|
103
|
+
|
|
104
|
+
function buildClassificationPrompt(request, filePath, fileContent) {
|
|
105
|
+
return `Classify this change request. Return ONLY JSON: {"safe": true|false, "type": "COSMETIC"|"CONFIG"|"LOGIC"|"CONTRACT", "reason": "..."}
|
|
106
|
+
|
|
107
|
+
Request: "${request}"
|
|
108
|
+
File: ${filePath}
|
|
109
|
+
File content:
|
|
110
|
+
\`\`\`
|
|
111
|
+
${fileContent.slice(0, 3000)}
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
Rules:
|
|
115
|
+
- If change touches exported function/class/type name → CONTRACT (blocked)
|
|
116
|
+
- If change modifies logic, condition, or calculation → LOGIC (blocked)
|
|
117
|
+
- If change is in a comment or string literal only → COSMETIC (allowed)
|
|
118
|
+
- If change is a constant/env/config value → CONFIG (allowed)`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Phase 1c: Contract Path Check (CLI, 0 tokens) ──────────────────────────
|
|
122
|
+
|
|
123
|
+
function contractPathCheck(filePath) {
|
|
124
|
+
for (const pattern of CONTRACT_PATH_PATTERNS) {
|
|
125
|
+
if (pattern.test(filePath)) {
|
|
126
|
+
return {
|
|
127
|
+
type: "CONTRACT",
|
|
128
|
+
reason: `File matches contract boundary pattern: ${pattern.source}`,
|
|
129
|
+
blocked: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { type: "OK", blocked: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Phase 2: Pre-Flight Checks (CLI, 0 tokens) ─────────────────────────────
|
|
137
|
+
|
|
138
|
+
function checkLockFile() {
|
|
139
|
+
if (!fs.existsSync(LOCK_FILE)) {
|
|
140
|
+
return { locked: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_FILE, "utf8"));
|
|
145
|
+
const pid = lock.pid;
|
|
146
|
+
|
|
147
|
+
// Check if process is still running
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 0);
|
|
150
|
+
return {
|
|
151
|
+
locked: true,
|
|
152
|
+
pid,
|
|
153
|
+
command: lock.command,
|
|
154
|
+
changeId: lock["change-id"],
|
|
155
|
+
started: lock.started,
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
// Process is dead, remove stale lock
|
|
159
|
+
fs.unlinkSync(LOCK_FILE);
|
|
160
|
+
return { locked: false, stale: true };
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Corrupt lock file, remove it
|
|
164
|
+
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
165
|
+
return { locked: false, corrupt: true };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function acquireLock(command, changeId) {
|
|
170
|
+
const lock = {
|
|
171
|
+
pid: process.pid,
|
|
172
|
+
command,
|
|
173
|
+
"change-id": changeId,
|
|
174
|
+
started: now(),
|
|
175
|
+
};
|
|
176
|
+
const specsDir = path.dirname(LOCK_FILE);
|
|
177
|
+
if (!fs.existsSync(specsDir)) {
|
|
178
|
+
fs.mkdirSync(specsDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify(lock, null, 2));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function releaseLock() {
|
|
184
|
+
try {
|
|
185
|
+
if (fs.existsSync(LOCK_FILE)) {
|
|
186
|
+
fs.unlinkSync(LOCK_FILE);
|
|
187
|
+
}
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function crossFileScan(pattern, targetFile) {
|
|
192
|
+
try {
|
|
193
|
+
const result = execSync(
|
|
194
|
+
`grep -r "${pattern}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | grep -v node_modules | grep -v ".sdd/" | wc -l`,
|
|
195
|
+
{ cwd: projectRoot, encoding: "utf8" }
|
|
196
|
+
);
|
|
197
|
+
const count = parseInt(result.trim(), 10);
|
|
198
|
+
return { count, exceeds: count > MAX_CROSS_FILE_REFS };
|
|
199
|
+
} catch {
|
|
200
|
+
return { count: 0, exceeds: false };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stringLengthCheck(oldStr, newStr) {
|
|
205
|
+
const oldLen = oldStr.length;
|
|
206
|
+
const newLen = newStr.length;
|
|
207
|
+
const delta = Math.abs(newLen - oldLen);
|
|
208
|
+
const percentIncrease = oldLen > 0 ? ((newLen - oldLen) / oldLen) * 100 : 0;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
delta,
|
|
212
|
+
percentIncrease,
|
|
213
|
+
exceeds: delta > MAX_STRING_DELTA || percentIncrease > MAX_STRING_PERCENT_INCREASE,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Phase 3: LLM Edit (invoked by agent, not CLI) ──────────────────────────
|
|
218
|
+
|
|
219
|
+
function buildEditPrompt(request, filePath, fileContent) {
|
|
220
|
+
const isSmall = fileContent.split("\n").length <= MAX_FILE_LINES_FOR_FULL_RETURN;
|
|
221
|
+
|
|
222
|
+
if (isSmall) {
|
|
223
|
+
return `Here is the full content of ${filePath}.
|
|
224
|
+
|
|
225
|
+
\`\`\`
|
|
226
|
+
${fileContent}
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
Task: ${request}
|
|
230
|
+
|
|
231
|
+
Return the ENTIRE file content with the change applied. No explanation. No diff format. No markdown code blocks. Just the raw file content.`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// For large files, use search-replace blocks
|
|
235
|
+
return `File: ${filePath}
|
|
236
|
+
|
|
237
|
+
Task: ${request}
|
|
238
|
+
|
|
239
|
+
Use this format to show changes:
|
|
240
|
+
<<<SEARCH
|
|
241
|
+
exact lines from original file
|
|
242
|
+
>>>
|
|
243
|
+
<<<REPLACE
|
|
244
|
+
new lines to replace with
|
|
245
|
+
>>>
|
|
246
|
+
|
|
247
|
+
Only show the sections that change. Do NOT return the entire file.`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Phase 4: Post-Flight Verification (CLI, 0 tokens) ──────────────────────
|
|
251
|
+
|
|
252
|
+
function verifyChange(backupPath, targetPath, request) {
|
|
253
|
+
const backup = fs.readFileSync(backupPath, "utf8");
|
|
254
|
+
const current = fs.readFileSync(targetPath, "utf8");
|
|
255
|
+
|
|
256
|
+
if (backup === current) {
|
|
257
|
+
return { changed: false, reason: "No changes were made to the file." };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Compute diff
|
|
261
|
+
const backupLines = backup.split("\n");
|
|
262
|
+
const currentLines = current.split("\n");
|
|
263
|
+
const addedLines = currentLines.length - backupLines.length;
|
|
264
|
+
|
|
265
|
+
// Check if only the requested change was made
|
|
266
|
+
// Simple heuristic: if too many lines added beyond the request, flag it
|
|
267
|
+
if (addedLines > 3) {
|
|
268
|
+
return {
|
|
269
|
+
changed: true,
|
|
270
|
+
extraLines: addedLines,
|
|
271
|
+
reason: `LLM added ${addedLines} extra lines beyond the requested change.`,
|
|
272
|
+
revert: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { changed: true, extraLines: addedLines };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function runLint() {
|
|
280
|
+
try {
|
|
281
|
+
// Try common lint commands
|
|
282
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
283
|
+
if (!fs.existsSync(pkgPath)) return { available: false };
|
|
284
|
+
|
|
285
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
286
|
+
const scripts = pkg.scripts || {};
|
|
287
|
+
|
|
288
|
+
if (scripts["lint"]) {
|
|
289
|
+
execSync(`npm run lint`, { cwd: projectRoot, stdio: "pipe" });
|
|
290
|
+
return { available: true, passed: true, command: "npm run lint" };
|
|
291
|
+
}
|
|
292
|
+
return { available: false };
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return { available: true, passed: false, command: "npm run lint", error: err.message };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function runTypecheck() {
|
|
299
|
+
try {
|
|
300
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
301
|
+
if (!fs.existsSync(pkgPath)) return { available: false };
|
|
302
|
+
|
|
303
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
304
|
+
const scripts = pkg.scripts || {};
|
|
305
|
+
|
|
306
|
+
if (scripts["typecheck"]) {
|
|
307
|
+
execSync(`npm run typecheck`, { cwd: projectRoot, stdio: "pipe" });
|
|
308
|
+
return { available: true, passed: true, command: "npm run typecheck" };
|
|
309
|
+
}
|
|
310
|
+
if (scripts["tsc"]) {
|
|
311
|
+
execSync(`npm run tsc`, { cwd: projectRoot, stdio: "pipe" });
|
|
312
|
+
return { available: true, passed: true, command: "npm run tsc" };
|
|
313
|
+
}
|
|
314
|
+
// Try npx tsc --noEmit
|
|
315
|
+
try {
|
|
316
|
+
execSync(`npx tsc --noEmit`, { cwd: projectRoot, stdio: "pipe" });
|
|
317
|
+
return { available: true, passed: true, command: "npx tsc --noEmit" };
|
|
318
|
+
} catch {
|
|
319
|
+
return { available: false };
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return { available: true, passed: false, command: "typecheck", error: err.message };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── Phase 5: Atomic Ledger Append (CLI, 0 tokens) ──────────────────────────
|
|
327
|
+
|
|
328
|
+
function appendToLedger(entry) {
|
|
329
|
+
const specsDir = path.dirname(LEDGER_FILE);
|
|
330
|
+
if (!fs.existsSync(specsDir)) {
|
|
331
|
+
fs.mkdirSync(specsDir, { recursive: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let content = "";
|
|
335
|
+
if (fs.existsSync(LEDGER_FILE)) {
|
|
336
|
+
content = fs.readFileSync(LEDGER_FILE, "utf8");
|
|
337
|
+
} else {
|
|
338
|
+
content = "# Quick Change Log\n\n";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
content += entry + "\n";
|
|
342
|
+
|
|
343
|
+
// Atomic write via temp file
|
|
344
|
+
const tmpFile = LEDGER_FILE + ".tmp";
|
|
345
|
+
fs.writeFileSync(tmpFile, content);
|
|
346
|
+
fs.renameSync(tmpFile, LEDGER_FILE);
|
|
347
|
+
|
|
348
|
+
// Ledger maintenance: archive old entries if too large
|
|
349
|
+
const entries = content.split(/^## /m).filter(Boolean);
|
|
350
|
+
if (entries.length > MAX_LEDGER_ENTRIES) {
|
|
351
|
+
const keep = entries.slice(-ARCHIVE_THRESHOLD);
|
|
352
|
+
const archive = entries.slice(0, -ARCHIVE_THRESHOLD);
|
|
353
|
+
|
|
354
|
+
let archiveContent = "";
|
|
355
|
+
if (fs.existsSync(LEDGER_ARCHIVE)) {
|
|
356
|
+
archiveContent = fs.readFileSync(LEDGER_ARCHIVE, "utf8");
|
|
357
|
+
} else {
|
|
358
|
+
archiveContent = "# Quick Change Log Archive\n\n";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
archiveContent += archive.join("## ");
|
|
362
|
+
|
|
363
|
+
const archiveTmp = LEDGER_ARCHIVE + ".tmp";
|
|
364
|
+
fs.writeFileSync(archiveTmp, archiveContent);
|
|
365
|
+
fs.renameSync(archiveTmp, LEDGER_ARCHIVE);
|
|
366
|
+
|
|
367
|
+
const newContent = "# Quick Change Log\n\n" + keep.join("## ");
|
|
368
|
+
const newTmp = LEDGER_FILE + ".tmp";
|
|
369
|
+
fs.writeFileSync(newTmp, newContent);
|
|
370
|
+
fs.renameSync(newTmp, LEDGER_FILE);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── CLI Entry Point ─────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
function main() {
|
|
377
|
+
const args = process.argv.slice(2);
|
|
378
|
+
|
|
379
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
380
|
+
console.log(`sdd-quick CLI Wrapper
|
|
381
|
+
|
|
382
|
+
Usage:
|
|
383
|
+
sdd-quick classify "<request>" [file] - Classify a change request
|
|
384
|
+
sdd-quick preflight "<request>" [file] - Run pre-flight checks
|
|
385
|
+
sdd-quick postflight <backup> <target> - Run post-flight verification
|
|
386
|
+
sdd-quick lint - Run lint check
|
|
387
|
+
sdd-quick typecheck - Run typecheck
|
|
388
|
+
sdd-quick ledger append "<entry>" - Append to ledger
|
|
389
|
+
sdd-quick lock acquire <command> [id] - Acquire lock
|
|
390
|
+
sdd-quick lock release - Release lock
|
|
391
|
+
sdd-quick lock check - Check lock status
|
|
392
|
+
sdd-quick grep "<pattern>" - Cross-file reference count
|
|
393
|
+
sdd-quick string-len "<old>" "<new>" - String length delta check
|
|
394
|
+
sdd-quick edit-prompt "<request>" <file> - Generate LLM edit prompt
|
|
395
|
+
|
|
396
|
+
All commands exit 0 on success, non-zero on failure.`);
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const command = args[0];
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
switch (command) {
|
|
404
|
+
case "classify": {
|
|
405
|
+
const request = args[1] || "";
|
|
406
|
+
const filePath = args[2] || "";
|
|
407
|
+
|
|
408
|
+
if (!request) exitWith(1, "Request is required for classification.");
|
|
409
|
+
|
|
410
|
+
// Phase 1a: Keyword scan
|
|
411
|
+
const keywordResult = keywordScan(request);
|
|
412
|
+
if (keywordResult.blocked) {
|
|
413
|
+
console.log(JSON.stringify({
|
|
414
|
+
blocked: true,
|
|
415
|
+
type: keywordResult.type,
|
|
416
|
+
reason: keywordResult.reason,
|
|
417
|
+
needsLLM: false,
|
|
418
|
+
}));
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Phase 1c: Contract path check
|
|
423
|
+
if (filePath) {
|
|
424
|
+
const contractResult = contractPathCheck(filePath);
|
|
425
|
+
if (contractResult.blocked) {
|
|
426
|
+
console.log(JSON.stringify({
|
|
427
|
+
blocked: true,
|
|
428
|
+
type: contractResult.type,
|
|
429
|
+
reason: contractResult.reason,
|
|
430
|
+
needsLLM: false,
|
|
431
|
+
}));
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// If file provided and keyword scan found something ambiguous, suggest LLM classification
|
|
437
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
438
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
439
|
+
const prompt = buildClassificationPrompt(request, filePath, content);
|
|
440
|
+
console.log(JSON.stringify({
|
|
441
|
+
blocked: false,
|
|
442
|
+
type: "NEEDS_LLM_CLASSIFICATION",
|
|
443
|
+
prompt,
|
|
444
|
+
}));
|
|
445
|
+
} else {
|
|
446
|
+
console.log(JSON.stringify({
|
|
447
|
+
blocked: false,
|
|
448
|
+
type: "COSMETIC_OR_CONFIG",
|
|
449
|
+
reason: "No blocking keywords or contract paths detected.",
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "preflight": {
|
|
456
|
+
const request = args[1] || "";
|
|
457
|
+
const filePath = args[2] || "";
|
|
458
|
+
|
|
459
|
+
// Lock check
|
|
460
|
+
const lock = checkLockFile();
|
|
461
|
+
if (lock.locked) {
|
|
462
|
+
console.log(JSON.stringify({
|
|
463
|
+
preflight: "FAIL",
|
|
464
|
+
reason: `Another SDD process is active (PID ${lock.pid}, ${lock.command}).`,
|
|
465
|
+
lock,
|
|
466
|
+
}));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Cross-file scan
|
|
471
|
+
if (request) {
|
|
472
|
+
const refs = crossFileScan(request, filePath);
|
|
473
|
+
if (refs.exceeds) {
|
|
474
|
+
console.log(JSON.stringify({
|
|
475
|
+
preflight: "WARN",
|
|
476
|
+
crossFileRefs: refs.count,
|
|
477
|
+
exceeds: true,
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// String length check (if old/new strings provided)
|
|
483
|
+
if (args[3] && args[4]) {
|
|
484
|
+
const strCheck = stringLengthCheck(args[3], args[4]);
|
|
485
|
+
if (strCheck.exceeds) {
|
|
486
|
+
console.log(JSON.stringify({
|
|
487
|
+
preflight: "WARN",
|
|
488
|
+
stringDelta: strCheck.delta,
|
|
489
|
+
percentIncrease: strCheck.percentIncrease,
|
|
490
|
+
exceeds: true,
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.log(JSON.stringify({ preflight: "PASS", lockStale: lock.stale }));
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case "postflight": {
|
|
500
|
+
const backupPath = args[1];
|
|
501
|
+
const targetPath = args[2];
|
|
502
|
+
|
|
503
|
+
if (!backupPath || !targetPath) {
|
|
504
|
+
exitWith(1, "Usage: sdd-quick postflight <backup> <target>");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const result = verifyChange(backupPath, targetPath);
|
|
508
|
+
console.log(JSON.stringify(result));
|
|
509
|
+
|
|
510
|
+
if (result.revert) {
|
|
511
|
+
fs.copyFileSync(backupPath, targetPath);
|
|
512
|
+
logWarn(`Auto-reverted: ${result.reason}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
process.exit(0);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
case "lint": {
|
|
520
|
+
const result = runLint();
|
|
521
|
+
console.log(JSON.stringify(result));
|
|
522
|
+
process.exit(result.available && !result.passed ? 1 : 0);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
case "typecheck": {
|
|
526
|
+
const result = runTypecheck();
|
|
527
|
+
console.log(JSON.stringify(result));
|
|
528
|
+
process.exit(result.available && !result.passed ? 1 : 0);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case "ledger": {
|
|
532
|
+
if (args[1] !== "append") exitWith(1, "Usage: sdd-quick ledger append \"<entry>\"");
|
|
533
|
+
const entry = args[2] || "";
|
|
534
|
+
if (!entry) exitWith(1, "Entry is required.");
|
|
535
|
+
appendToLedger(entry);
|
|
536
|
+
logSuccess(`Ledger updated: ${LEDGER_FILE}`);
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case "lock": {
|
|
541
|
+
const sub = args[1];
|
|
542
|
+
switch (sub) {
|
|
543
|
+
case "acquire": {
|
|
544
|
+
const cmd = args[2] || "sdd-quick";
|
|
545
|
+
const id = args[3] || `quick-${Date.now()}`;
|
|
546
|
+
acquireLock(cmd, id);
|
|
547
|
+
logSuccess(`Lock acquired: PID ${process.pid}`);
|
|
548
|
+
process.exit(0);
|
|
549
|
+
}
|
|
550
|
+
case "release": {
|
|
551
|
+
releaseLock();
|
|
552
|
+
logSuccess("Lock released");
|
|
553
|
+
process.exit(0);
|
|
554
|
+
}
|
|
555
|
+
case "check": {
|
|
556
|
+
const lock = checkLockFile();
|
|
557
|
+
console.log(JSON.stringify(lock));
|
|
558
|
+
process.exit(0);
|
|
559
|
+
}
|
|
560
|
+
default:
|
|
561
|
+
exitWith(1, "Usage: sdd-quick lock [acquire|release|check]");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case "grep": {
|
|
566
|
+
const pattern = args[1] || "";
|
|
567
|
+
if (!pattern) exitWith(1, "Pattern is required.");
|
|
568
|
+
const result = crossFileScan(pattern);
|
|
569
|
+
console.log(JSON.stringify(result));
|
|
570
|
+
process.exit(0);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case "string-len": {
|
|
574
|
+
const oldStr = args[1] || "";
|
|
575
|
+
const newStr = args[2] || "";
|
|
576
|
+
const result = stringLengthCheck(oldStr, newStr);
|
|
577
|
+
console.log(JSON.stringify(result));
|
|
578
|
+
process.exit(0);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case "edit-prompt": {
|
|
582
|
+
const request = args[1] || "";
|
|
583
|
+
const filePath = args[2] || "";
|
|
584
|
+
|
|
585
|
+
if (!request || !filePath) {
|
|
586
|
+
exitWith(1, "Usage: sdd-quick edit-prompt \"<request>\" <file>");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!fs.existsSync(filePath)) {
|
|
590
|
+
exitWith(1, `File not found: ${filePath}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
594
|
+
const prompt = buildEditPrompt(request, filePath, content);
|
|
595
|
+
console.log(prompt);
|
|
596
|
+
process.exit(0);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
default:
|
|
600
|
+
exitWith(1, `Unknown command: ${command}. Run sdd-quick --help for usage.`);
|
|
601
|
+
}
|
|
602
|
+
} catch (err) {
|
|
603
|
+
exitWith(1, `Error: ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tudeorangbiasa/sdd-multiagent-opencode",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Spec-Driven Development workflow kit for OpenCode with
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Spec-Driven Development workflow kit for OpenCode with 5 core commands, multi-agent support, CLI wrappers, and configurable model routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": ".opencode/plugins/sdd-register.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"sdd-opencode": "bin/sdd-opencode.js"
|
|
8
|
+
"sdd-opencode": "bin/sdd-opencode.js",
|
|
9
|
+
"sdd-quick": "bin/sdd-quick.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"bin",
|