@neurcode-ai/cli 0.9.11 → 0.9.13

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.
@@ -0,0 +1,707 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shipCommand = shipCommand;
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const project_root_1 = require("../utils/project-root");
8
+ const state_1 = require("../utils/state");
9
+ let chalk;
10
+ try {
11
+ chalk = require('chalk');
12
+ }
13
+ catch {
14
+ chalk = {
15
+ green: (str) => str,
16
+ yellow: (str) => str,
17
+ red: (str) => str,
18
+ bold: (str) => str,
19
+ dim: (str) => str,
20
+ cyan: (str) => str,
21
+ white: (str) => str,
22
+ };
23
+ }
24
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
25
+ const PLAN_ID_PATTERN = /Plan ID:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
26
+ const WRITE_PATH_PATTERN = /✅\s+Written:\s+(.+)$/gm;
27
+ function stripAnsi(value) {
28
+ return value.replace(ANSI_PATTERN, '');
29
+ }
30
+ function clamp(value, min, max) {
31
+ return Math.max(min, Math.min(max, value));
32
+ }
33
+ function shellTailLines(text, limit) {
34
+ return text
35
+ .split('\n')
36
+ .filter(Boolean)
37
+ .slice(-limit)
38
+ .join('\n');
39
+ }
40
+ function getCliEntryPath() {
41
+ return (0, path_1.resolve)(__dirname, '..', 'index.js');
42
+ }
43
+ function runCliCommand(cwd, args) {
44
+ return new Promise((resolvePromise) => {
45
+ const startedAt = Date.now();
46
+ const child = (0, child_process_1.spawn)(process.execPath, [getCliEntryPath(), ...args], {
47
+ cwd,
48
+ env: {
49
+ ...process.env,
50
+ CI: 'true',
51
+ },
52
+ stdio: ['ignore', 'pipe', 'pipe'],
53
+ });
54
+ let stdout = '';
55
+ let stderr = '';
56
+ child.stdout.on('data', (chunk) => {
57
+ const text = chunk.toString();
58
+ stdout += text;
59
+ process.stdout.write(text);
60
+ });
61
+ child.stderr.on('data', (chunk) => {
62
+ const text = chunk.toString();
63
+ stderr += text;
64
+ process.stderr.write(text);
65
+ });
66
+ child.on('close', (code) => {
67
+ resolvePromise({
68
+ code: code ?? 1,
69
+ stdout,
70
+ stderr,
71
+ durationMs: Date.now() - startedAt,
72
+ });
73
+ });
74
+ });
75
+ }
76
+ function runShellCommand(cwd, command) {
77
+ return new Promise((resolvePromise) => {
78
+ const startedAt = Date.now();
79
+ const child = (0, child_process_1.spawn)(command, {
80
+ cwd,
81
+ env: {
82
+ ...process.env,
83
+ },
84
+ shell: true,
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ });
87
+ let stdout = '';
88
+ let stderr = '';
89
+ child.stdout.on('data', (chunk) => {
90
+ const text = chunk.toString();
91
+ stdout += text;
92
+ process.stdout.write(text);
93
+ });
94
+ child.stderr.on('data', (chunk) => {
95
+ const text = chunk.toString();
96
+ stderr += text;
97
+ process.stderr.write(text);
98
+ });
99
+ child.on('close', (code) => {
100
+ resolvePromise({
101
+ code: code ?? 1,
102
+ stdout,
103
+ stderr,
104
+ durationMs: Date.now() - startedAt,
105
+ });
106
+ });
107
+ });
108
+ }
109
+ function extractPlanId(output) {
110
+ const clean = stripAnsi(output);
111
+ let latest = null;
112
+ let match;
113
+ while ((match = PLAN_ID_PATTERN.exec(clean)) !== null) {
114
+ latest = match[1];
115
+ }
116
+ return latest;
117
+ }
118
+ function extractLastJsonObject(output) {
119
+ const clean = stripAnsi(output).trim();
120
+ const end = clean.lastIndexOf('}');
121
+ if (end < 0)
122
+ return null;
123
+ let start = clean.lastIndexOf('{', end);
124
+ while (start >= 0) {
125
+ const candidate = clean.slice(start, end + 1).trim();
126
+ try {
127
+ return JSON.parse(candidate);
128
+ }
129
+ catch {
130
+ start = clean.lastIndexOf('{', start - 1);
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ function parseVerifyPayload(output) {
136
+ const parsed = extractLastJsonObject(output);
137
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
138
+ return null;
139
+ const record = parsed;
140
+ if (typeof record.verdict !== 'string' || typeof record.grade !== 'string') {
141
+ return null;
142
+ }
143
+ const rawViolations = Array.isArray(record.violations) ? record.violations : [];
144
+ const violations = rawViolations
145
+ .filter((item) => !!item && typeof item === 'object')
146
+ .map((item) => ({
147
+ file: typeof item.file === 'string' ? item.file : 'unknown',
148
+ rule: typeof item.rule === 'string' ? item.rule : 'unknown',
149
+ severity: typeof item.severity === 'string' ? item.severity : 'warn',
150
+ message: typeof item.message === 'string' ? item.message : undefined,
151
+ startLine: typeof item.startLine === 'number' ? item.startLine : undefined,
152
+ }));
153
+ return {
154
+ grade: record.grade,
155
+ score: typeof record.score === 'number' ? record.score : 0,
156
+ verdict: record.verdict,
157
+ violations,
158
+ message: typeof record.message === 'string' ? record.message : undefined,
159
+ adherenceScore: typeof record.adherenceScore === 'number' ? record.adherenceScore : undefined,
160
+ scopeGuardPassed: typeof record.scopeGuardPassed === 'boolean' ? record.scopeGuardPassed : undefined,
161
+ bloatCount: typeof record.bloatCount === 'number' ? record.bloatCount : undefined,
162
+ bloatFiles: Array.isArray(record.bloatFiles)
163
+ ? record.bloatFiles.filter((item) => typeof item === 'string')
164
+ : undefined,
165
+ plannedFilesModified: typeof record.plannedFilesModified === 'number' ? record.plannedFilesModified : undefined,
166
+ totalPlannedFiles: typeof record.totalPlannedFiles === 'number' ? record.totalPlannedFiles : undefined,
167
+ policyDecision: typeof record.policyDecision === 'string' ? record.policyDecision : undefined,
168
+ };
169
+ }
170
+ function collectApplyWrittenFiles(output) {
171
+ const clean = stripAnsi(output);
172
+ const files = [];
173
+ let match;
174
+ while ((match = WRITE_PATH_PATTERN.exec(clean)) !== null) {
175
+ files.push(match[1].trim());
176
+ }
177
+ return Array.from(new Set(files));
178
+ }
179
+ function runGit(cwd, args) {
180
+ const result = (0, child_process_1.spawnSync)('git', args, {
181
+ cwd,
182
+ encoding: 'utf-8',
183
+ stdio: ['ignore', 'pipe', 'pipe'],
184
+ });
185
+ return {
186
+ code: result.status ?? 1,
187
+ stdout: result.stdout || '',
188
+ stderr: result.stderr || '',
189
+ };
190
+ }
191
+ function ensureCleanTreeOrExit(cwd, allowDirty) {
192
+ const status = runGit(cwd, ['status', '--porcelain']);
193
+ const hasDirtyFiles = status.stdout.trim().length > 0;
194
+ if (hasDirtyFiles && !allowDirty) {
195
+ console.error(chalk.red('❌ Working tree is not clean.'));
196
+ console.error(chalk.dim(' `neurcode ship` requires a clean tree so auto-remediation can safely revert scope drift.'));
197
+ console.error(chalk.dim(' Commit/stash your changes or re-run with --allow-dirty if intentional.'));
198
+ process.exit(1);
199
+ }
200
+ }
201
+ function restoreScopeDriftFiles(cwd, files) {
202
+ const uniqueFiles = Array.from(new Set(files.filter(Boolean)));
203
+ if (uniqueFiles.length === 0)
204
+ return [];
205
+ const result = runGit(cwd, ['restore', '--worktree', '--source=HEAD', '--', ...uniqueFiles]);
206
+ if (result.code !== 0) {
207
+ return [];
208
+ }
209
+ return uniqueFiles;
210
+ }
211
+ function applySimplePolicyFixes(cwd, violations) {
212
+ const touched = [];
213
+ const byFile = new Map();
214
+ for (const violation of violations) {
215
+ if (!violation.file || violation.file === 'unknown')
216
+ continue;
217
+ if (!byFile.has(violation.file)) {
218
+ byFile.set(violation.file, []);
219
+ }
220
+ byFile.get(violation.file).push(violation);
221
+ }
222
+ for (const [file, fileViolations] of byFile.entries()) {
223
+ const fullPath = (0, path_1.resolve)(cwd, file);
224
+ if (!(0, fs_1.existsSync)(fullPath))
225
+ continue;
226
+ let content;
227
+ try {
228
+ content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ let nextContent = content;
234
+ const flattened = fileViolations
235
+ .map((entry) => `${entry.rule} ${entry.message || ''}`.toLowerCase())
236
+ .join(' ');
237
+ if (flattened.includes('console.log')) {
238
+ nextContent = nextContent
239
+ .split('\n')
240
+ .filter((line) => !/console\.log\s*\(/.test(line))
241
+ .join('\n');
242
+ }
243
+ if (flattened.includes('debugger')) {
244
+ nextContent = nextContent
245
+ .split('\n')
246
+ .filter((line) => !/\bdebugger\b/.test(line))
247
+ .join('\n');
248
+ }
249
+ if (flattened.includes('eval(') || flattened.includes('no eval')) {
250
+ nextContent = nextContent
251
+ .split('\n')
252
+ .filter((line) => !/\beval\s*\(/.test(line))
253
+ .join('\n');
254
+ }
255
+ if (nextContent !== content) {
256
+ if (content.endsWith('\n') && !nextContent.endsWith('\n')) {
257
+ nextContent += '\n';
258
+ }
259
+ (0, fs_1.writeFileSync)(fullPath, nextContent, 'utf-8');
260
+ touched.push(file);
261
+ }
262
+ }
263
+ return Array.from(new Set(touched));
264
+ }
265
+ function collectBlastRadius(cwd) {
266
+ let output = '';
267
+ try {
268
+ output = (0, child_process_1.execSync)('git diff --numstat', {
269
+ cwd,
270
+ encoding: 'utf-8',
271
+ maxBuffer: 1024 * 1024 * 1024,
272
+ });
273
+ }
274
+ catch {
275
+ return {
276
+ changedFiles: 0,
277
+ linesAdded: 0,
278
+ linesRemoved: 0,
279
+ netLines: 0,
280
+ topFiles: [],
281
+ };
282
+ }
283
+ const rows = [];
284
+ let linesAdded = 0;
285
+ let linesRemoved = 0;
286
+ for (const line of output.split('\n')) {
287
+ if (!line.trim())
288
+ continue;
289
+ const [addedRaw, removedRaw, ...pathParts] = line.split('\t');
290
+ if (!addedRaw || !removedRaw || pathParts.length === 0)
291
+ continue;
292
+ const path = pathParts.join('\t');
293
+ const added = addedRaw === '-' ? 0 : parseInt(addedRaw, 10);
294
+ const removed = removedRaw === '-' ? 0 : parseInt(removedRaw, 10);
295
+ if (!Number.isFinite(added) || !Number.isFinite(removed))
296
+ continue;
297
+ rows.push({ path, added, removed });
298
+ linesAdded += added;
299
+ linesRemoved += removed;
300
+ }
301
+ rows.sort((a, b) => b.added + b.removed - (a.added + a.removed));
302
+ return {
303
+ changedFiles: rows.length,
304
+ linesAdded,
305
+ linesRemoved,
306
+ netLines: linesAdded - linesRemoved,
307
+ topFiles: rows.slice(0, 10),
308
+ };
309
+ }
310
+ function inferTestCommand(cwd, explicit) {
311
+ if (explicit && explicit.trim()) {
312
+ return explicit.trim();
313
+ }
314
+ if ((0, fs_1.existsSync)((0, path_1.join)(cwd, 'pnpm-lock.yaml')))
315
+ return 'pnpm test --if-present';
316
+ if ((0, fs_1.existsSync)((0, path_1.join)(cwd, 'yarn.lock')))
317
+ return 'yarn test';
318
+ if ((0, fs_1.existsSync)((0, path_1.join)(cwd, 'package-lock.json')))
319
+ return 'npm test --if-present';
320
+ if ((0, fs_1.existsSync)((0, path_1.join)(cwd, 'package.json')))
321
+ return 'npm test --if-present';
322
+ return null;
323
+ }
324
+ function buildVerifyRepairIntent(goal, currentPlanId, verify, attempt) {
325
+ const failureLines = verify.violations
326
+ .slice(0, 12)
327
+ .map((v) => `- ${v.file} [${v.severity}] ${v.rule}${v.message ? `: ${v.message}` : ''}`)
328
+ .join('\n');
329
+ return [
330
+ `Original goal: ${goal}`,
331
+ `Auto-repair attempt: ${attempt}`,
332
+ `Current failing plan ID: ${currentPlanId}`,
333
+ '',
334
+ 'Repair only what is required to pass governance verification.',
335
+ 'Constraints:',
336
+ '- Keep edits minimal and localized.',
337
+ '- Do not add new dependencies.',
338
+ '- Remove unplanned file changes.',
339
+ '- Preserve intended behavior.',
340
+ '',
341
+ 'Current verification failures:',
342
+ failureLines || '- Scope or policy checks failed.',
343
+ ].join('\n');
344
+ }
345
+ function buildTestRepairIntent(goal, currentPlanId, testOutput, attempt) {
346
+ return [
347
+ `Original goal: ${goal}`,
348
+ `Auto-repair attempt: ${attempt}`,
349
+ `Current plan ID: ${currentPlanId}`,
350
+ '',
351
+ 'Fix test failures with the smallest safe patch.',
352
+ 'Constraints:',
353
+ '- Do not expand scope.',
354
+ '- Keep behavior changes limited to what tests require.',
355
+ '- Do not add risky dependencies.',
356
+ '',
357
+ 'Test failure context (tail):',
358
+ shellTailLines(testOutput, 40) || '(no test output captured)',
359
+ ].join('\n');
360
+ }
361
+ function classifyRiskLabel(score) {
362
+ if (score >= 70)
363
+ return 'HIGH';
364
+ if (score >= 40)
365
+ return 'MEDIUM';
366
+ return 'LOW';
367
+ }
368
+ function computeRiskScore(verify, blast, testsPassed, remediationAttempts) {
369
+ const adherence = Number.isFinite(verify.adherenceScore) ? verify.adherenceScore : verify.score;
370
+ let risk = 0;
371
+ if (verify.verdict === 'FAIL')
372
+ risk += 52;
373
+ else if (verify.verdict === 'WARN')
374
+ risk += 30;
375
+ else
376
+ risk += 12;
377
+ risk += Math.round((100 - clamp(adherence, 0, 100)) * 0.3);
378
+ risk += Math.min(28, verify.violations.length * 4);
379
+ risk += Math.min(20, Math.max(0, blast.changedFiles - 3) * 2);
380
+ risk += Math.min(18, Math.floor((blast.linesAdded + blast.linesRemoved) / 100) * 3);
381
+ risk += remediationAttempts * 5;
382
+ if (!testsPassed) {
383
+ risk += 20;
384
+ }
385
+ return clamp(Math.round(risk), 0, 100);
386
+ }
387
+ function writeMergeConfidenceArtifacts(cwd, card) {
388
+ const outDir = (0, path_1.join)(cwd, '.neurcode', 'ship');
389
+ (0, fs_1.mkdirSync)(outDir, { recursive: true });
390
+ const ts = card.generatedAt.replace(/[:.]/g, '-');
391
+ const jsonPath = (0, path_1.join)(outDir, `merge-confidence-${ts}.json`);
392
+ const markdownPath = (0, path_1.join)(outDir, `merge-confidence-${ts}.md`);
393
+ const markdown = [
394
+ '# Merge Confidence Card',
395
+ '',
396
+ `- Status: **${card.status}**`,
397
+ `- Goal: ${card.goal}`,
398
+ `- Branch: ${card.repository.branch}`,
399
+ `- Head SHA: ${card.repository.headSha}`,
400
+ `- Initial Plan: ${card.plans.initialPlanId}`,
401
+ `- Final Plan: ${card.plans.finalPlanId}`,
402
+ '',
403
+ '## Governance',
404
+ '',
405
+ `- Verdict: **${card.verification.verdict}**`,
406
+ `- Grade: **${card.verification.grade}**`,
407
+ `- Score: **${card.verification.adherenceScore ?? card.verification.score}**`,
408
+ `- Violations: **${card.verification.violations.length}**`,
409
+ '',
410
+ '## Blast Radius',
411
+ '',
412
+ `- Files Changed: **${card.blastRadius.changedFiles}**`,
413
+ `- Lines Added: **${card.blastRadius.linesAdded}**`,
414
+ `- Lines Removed: **${card.blastRadius.linesRemoved}**`,
415
+ `- Net Lines: **${card.blastRadius.netLines}**`,
416
+ '',
417
+ '## Risk',
418
+ '',
419
+ `- Risk Score: **${card.risk.score}/100 (${card.risk.label})**`,
420
+ `- Merge Confidence: **${card.risk.mergeConfidence}/100**`,
421
+ '',
422
+ '## Auto-Remediation',
423
+ '',
424
+ `- Attempts Used: **${card.remediation.attemptsUsed}/${card.remediation.maxAttempts}**`,
425
+ `- Actions: ${card.remediation.actions.length > 0 ? card.remediation.actions.join('; ') : 'None'}`,
426
+ '',
427
+ '## Tests',
428
+ '',
429
+ `- Skipped: **${card.tests.skipped ? 'yes' : 'no'}**`,
430
+ `- Passed: **${card.tests.passed ? 'yes' : 'no'}**`,
431
+ card.tests.command ? `- Command: \`${card.tests.command}\`` : '- Command: none',
432
+ '',
433
+ '## Top Changed Files',
434
+ '',
435
+ ...(card.blastRadius.topFiles.length > 0
436
+ ? card.blastRadius.topFiles.map((file) => `- ${file.path} (+${file.added}/-${file.removed})`)
437
+ : ['- none']),
438
+ '',
439
+ '## Evidence (Violations)',
440
+ '',
441
+ ...(card.verification.violations.length > 0
442
+ ? card.verification.violations.slice(0, 20).map((v) => `- ${v.file}${typeof v.startLine === 'number' ? `:${v.startLine}` : ''} [${v.severity}] ${v.rule}${v.message ? ` - ${v.message}` : ''}`)
443
+ : ['- none']),
444
+ '',
445
+ ].join('\n');
446
+ (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(card, null, 2) + '\n', 'utf-8');
447
+ (0, fs_1.writeFileSync)(markdownPath, markdown, 'utf-8');
448
+ return { jsonPath, markdownPath };
449
+ }
450
+ async function runPlanAndApply(cwd, intent, projectId) {
451
+ const planArgs = ['plan', intent, '--force-plan'];
452
+ if (projectId) {
453
+ planArgs.push('--project-id', projectId);
454
+ }
455
+ const planRun = await runCliCommand(cwd, planArgs);
456
+ const planOutput = `${planRun.stdout}\n${planRun.stderr}`;
457
+ const planId = extractPlanId(planOutput);
458
+ if (planRun.code !== 0 || !planId) {
459
+ return { planId: null, planRun, applyRun: null, writtenFiles: [] };
460
+ }
461
+ const applyArgs = ['apply', planId, '--force'];
462
+ const applyRun = await runCliCommand(cwd, applyArgs);
463
+ const writtenFiles = collectApplyWrittenFiles(`${applyRun.stdout}\n${applyRun.stderr}`);
464
+ return { planId, planRun, applyRun, writtenFiles };
465
+ }
466
+ async function shipCommand(goal, options) {
467
+ const startedAt = Date.now();
468
+ const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
469
+ const maxFixAttempts = clamp(options.maxFixAttempts ?? 2, 0, 5);
470
+ const remediationActions = [];
471
+ const repairPlanIds = [];
472
+ const recordVerify = options.record !== false;
473
+ if (!goal || !goal.trim()) {
474
+ console.error(chalk.red('❌ Error: goal cannot be empty.'));
475
+ console.log(chalk.dim('Usage: neurcode ship "<goal>"'));
476
+ process.exit(1);
477
+ }
478
+ ensureCleanTreeOrExit(cwd, options.allowDirty === true);
479
+ console.log(chalk.bold.cyan('\n🚀 Neurcode Ship\n'));
480
+ console.log(chalk.dim(`Goal: ${goal.trim()}`));
481
+ console.log(chalk.dim(`Workspace: ${cwd}\n`));
482
+ console.log(chalk.dim('1/4 Planning and applying initial implementation...'));
483
+ const initial = await runPlanAndApply(cwd, goal.trim(), options.projectId);
484
+ if (!initial.planId || initial.planRun.code !== 0 || !initial.applyRun || initial.applyRun.code !== 0) {
485
+ console.error(chalk.red('\n❌ Ship failed during initial plan/apply.'));
486
+ process.exit(1);
487
+ }
488
+ let currentPlanId = initial.planId;
489
+ try {
490
+ (0, state_1.setActivePlanId)(currentPlanId);
491
+ (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
492
+ }
493
+ catch {
494
+ // Non-critical state write.
495
+ }
496
+ let verifyTotalMs = 0;
497
+ let testsTotalMs = 0;
498
+ let remediationAttemptsUsed = 0;
499
+ let verifyPayload = null;
500
+ let verifyExitCode = 1;
501
+ let testsPassed = options.skipTests === true;
502
+ let testsExitCode = options.skipTests ? 0 : 1;
503
+ let testsAttempts = 0;
504
+ let testCommand = inferTestCommand(cwd, options.testCommand);
505
+ while (true) {
506
+ console.log(chalk.dim('\n2/4 Running governance verification...'));
507
+ const verifyArgs = ['verify', '--plan-id', currentPlanId, '--json'];
508
+ if (recordVerify) {
509
+ verifyArgs.push('--record');
510
+ }
511
+ const verifyRun = await runCliCommand(cwd, verifyArgs);
512
+ verifyTotalMs += verifyRun.durationMs;
513
+ verifyExitCode = verifyRun.code;
514
+ const parsedVerify = parseVerifyPayload(`${verifyRun.stdout}\n${verifyRun.stderr}`);
515
+ if (!parsedVerify) {
516
+ console.error(chalk.red('\n❌ Could not parse verify JSON output.'));
517
+ process.exit(1);
518
+ }
519
+ verifyPayload = parsedVerify;
520
+ const verifyPassed = verifyRun.code === 0 && parsedVerify.verdict === 'PASS';
521
+ if (verifyPassed) {
522
+ console.log(chalk.green('✅ Governance verification passed.'));
523
+ break;
524
+ }
525
+ if (remediationAttemptsUsed >= maxFixAttempts) {
526
+ console.log(chalk.red(`❌ Verification still failing after ${remediationAttemptsUsed} remediation attempt(s).`));
527
+ break;
528
+ }
529
+ remediationAttemptsUsed += 1;
530
+ console.log(chalk.yellow(`⚠️ Auto-remediation attempt ${remediationAttemptsUsed}/${maxFixAttempts}`));
531
+ const scopeDriftFiles = parsedVerify.violations
532
+ .filter((v) => v.rule === 'scope_guard' && v.file)
533
+ .map((v) => v.file);
534
+ const restored = restoreScopeDriftFiles(cwd, scopeDriftFiles);
535
+ if (restored.length > 0) {
536
+ remediationActions.push(`restored_scope_files=${restored.join(',')}`);
537
+ console.log(chalk.dim(` Restored ${restored.length} out-of-scope file(s) from HEAD.`));
538
+ }
539
+ const policyFixes = applySimplePolicyFixes(cwd, parsedVerify.violations);
540
+ if (policyFixes.length > 0) {
541
+ remediationActions.push(`policy_cleanup_files=${policyFixes.join(',')}`);
542
+ console.log(chalk.dim(` Applied simple policy cleanup to ${policyFixes.length} file(s).`));
543
+ }
544
+ if (restored.length > 0 || policyFixes.length > 0) {
545
+ continue;
546
+ }
547
+ console.log(chalk.dim(' Falling back to constrained repair plan...'));
548
+ const repairIntent = buildVerifyRepairIntent(goal.trim(), currentPlanId, parsedVerify, remediationAttemptsUsed);
549
+ const repair = await runPlanAndApply(cwd, repairIntent, options.projectId);
550
+ if (!repair.planId || repair.planRun.code !== 0 || !repair.applyRun || repair.applyRun.code !== 0) {
551
+ remediationActions.push('repair_plan_failed');
552
+ console.log(chalk.red(' Repair plan/apply failed.'));
553
+ break;
554
+ }
555
+ currentPlanId = repair.planId;
556
+ repairPlanIds.push(repair.planId);
557
+ remediationActions.push(`repair_plan_applied=${repair.planId}`);
558
+ try {
559
+ (0, state_1.setActivePlanId)(currentPlanId);
560
+ (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
561
+ }
562
+ catch {
563
+ // Non-critical state write.
564
+ }
565
+ }
566
+ if (!verifyPayload) {
567
+ console.error(chalk.red('❌ Verification did not produce a valid payload.'));
568
+ process.exit(1);
569
+ }
570
+ const verifyPassedFinal = verifyExitCode === 0 && verifyPayload.verdict === 'PASS';
571
+ if (verifyPassedFinal && !options.skipTests) {
572
+ testsAttempts += 1;
573
+ if (!testCommand) {
574
+ console.log(chalk.yellow('\n⚠️ No test command detected. Skipping tests.'));
575
+ testsPassed = true;
576
+ testsExitCode = 0;
577
+ }
578
+ else {
579
+ console.log(chalk.dim(`\n3/4 Running tests: ${testCommand}`));
580
+ const testRun = await runShellCommand(cwd, testCommand);
581
+ testsTotalMs += testRun.durationMs;
582
+ testsExitCode = testRun.code;
583
+ testsPassed = testRun.code === 0;
584
+ if (!testsPassed && remediationAttemptsUsed < maxFixAttempts) {
585
+ remediationAttemptsUsed += 1;
586
+ console.log(chalk.yellow(`⚠️ Test failure auto-remediation attempt ${remediationAttemptsUsed}/${maxFixAttempts}`));
587
+ const repairIntent = buildTestRepairIntent(goal.trim(), currentPlanId, `${testRun.stdout}\n${testRun.stderr}`, remediationAttemptsUsed);
588
+ const repair = await runPlanAndApply(cwd, repairIntent, options.projectId);
589
+ if (repair.planId && repair.planRun.code === 0 && repair.applyRun && repair.applyRun.code === 0) {
590
+ currentPlanId = repair.planId;
591
+ repairPlanIds.push(repair.planId);
592
+ remediationActions.push(`test_repair_plan_applied=${repair.planId}`);
593
+ try {
594
+ (0, state_1.setActivePlanId)(currentPlanId);
595
+ (0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
596
+ }
597
+ catch {
598
+ // Non-critical state write.
599
+ }
600
+ const verifyAfterTestRepair = await runCliCommand(cwd, ['verify', '--plan-id', currentPlanId, '--json', ...(recordVerify ? ['--record'] : [])]);
601
+ verifyTotalMs += verifyAfterTestRepair.durationMs;
602
+ const parsedAfterRepair = parseVerifyPayload(`${verifyAfterTestRepair.stdout}\n${verifyAfterTestRepair.stderr}`);
603
+ if (parsedAfterRepair) {
604
+ verifyPayload = parsedAfterRepair;
605
+ verifyExitCode = verifyAfterTestRepair.code;
606
+ }
607
+ testsAttempts += 1;
608
+ const finalTestRun = await runShellCommand(cwd, testCommand);
609
+ testsTotalMs += finalTestRun.durationMs;
610
+ testsExitCode = finalTestRun.code;
611
+ testsPassed = finalTestRun.code === 0;
612
+ }
613
+ else {
614
+ remediationActions.push('test_repair_plan_failed');
615
+ }
616
+ }
617
+ }
618
+ }
619
+ const blast = collectBlastRadius(cwd);
620
+ const riskScore = computeRiskScore(verifyPayload, blast, testsPassed, remediationAttemptsUsed);
621
+ const riskLabel = classifyRiskLabel(riskScore);
622
+ const status = verifyExitCode === 0 && verifyPayload.verdict === 'PASS' && testsPassed ? 'READY_TO_MERGE' : 'BLOCKED';
623
+ const branch = (() => {
624
+ const branchResult = runGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
625
+ return branchResult.code === 0 ? branchResult.stdout.trim() : 'unknown';
626
+ })();
627
+ const headSha = (() => {
628
+ const headResult = runGit(cwd, ['rev-parse', 'HEAD']);
629
+ return headResult.code === 0 ? headResult.stdout.trim() : 'unknown';
630
+ })();
631
+ const card = {
632
+ status,
633
+ generatedAt: new Date().toISOString(),
634
+ goal: goal.trim(),
635
+ repository: {
636
+ root: cwd,
637
+ branch,
638
+ headSha,
639
+ },
640
+ plans: {
641
+ initialPlanId: initial.planId,
642
+ finalPlanId: currentPlanId,
643
+ repairPlanIds,
644
+ },
645
+ verification: verifyPayload,
646
+ tests: {
647
+ skipped: options.skipTests === true,
648
+ command: testCommand || undefined,
649
+ passed: testsPassed,
650
+ attempts: testsAttempts,
651
+ lastExitCode: testsExitCode,
652
+ },
653
+ remediation: {
654
+ maxAttempts: maxFixAttempts,
655
+ attemptsUsed: remediationAttemptsUsed,
656
+ actions: remediationActions,
657
+ },
658
+ blastRadius: blast,
659
+ risk: {
660
+ score: riskScore,
661
+ label: riskLabel,
662
+ mergeConfidence: 100 - riskScore,
663
+ },
664
+ timingsMs: {
665
+ initialPlan: initial.planRun.durationMs,
666
+ initialApply: initial.applyRun ? initial.applyRun.durationMs : 0,
667
+ verifyTotal: verifyTotalMs,
668
+ testsTotal: testsTotalMs,
669
+ total: Date.now() - startedAt,
670
+ },
671
+ };
672
+ const artifactPaths = writeMergeConfidenceArtifacts(cwd, card);
673
+ console.log(chalk.dim('\n4/4 Merge Confidence Card generated.'));
674
+ console.log(chalk.dim(` JSON: ${artifactPaths.jsonPath}`));
675
+ console.log(chalk.dim(` Markdown: ${artifactPaths.markdownPath}`));
676
+ console.log('');
677
+ if (status === 'READY_TO_MERGE') {
678
+ console.log(chalk.bold.green('✅ Ready to merge'));
679
+ console.log(chalk.green(` Confidence: ${card.risk.mergeConfidence}/100 | Risk: ${card.risk.label}`));
680
+ }
681
+ else {
682
+ console.log(chalk.bold.red('❌ Blocked'));
683
+ console.log(chalk.red(` Verdict: ${verifyPayload.verdict} | Tests: ${testsPassed ? 'PASS' : 'FAIL'}`));
684
+ console.log(chalk.red(` Confidence: ${card.risk.mergeConfidence}/100 | Risk: ${card.risk.label}`));
685
+ }
686
+ if (options.json) {
687
+ console.log(JSON.stringify({
688
+ status: card.status,
689
+ finalPlanId: card.plans.finalPlanId,
690
+ mergeConfidence: card.risk.mergeConfidence,
691
+ riskScore: card.risk.score,
692
+ verification: {
693
+ verdict: card.verification.verdict,
694
+ grade: card.verification.grade,
695
+ score: card.verification.adherenceScore ?? card.verification.score,
696
+ violations: card.verification.violations.length,
697
+ },
698
+ tests: {
699
+ skipped: card.tests.skipped,
700
+ passed: card.tests.passed,
701
+ },
702
+ artifacts: artifactPaths,
703
+ }, null, 2));
704
+ }
705
+ process.exit(status === 'READY_TO_MERGE' ? 0 : 2);
706
+ }
707
+ //# sourceMappingURL=ship.js.map