@neurcode-ai/cli 0.9.26 → 0.9.28
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/dist/commands/allow.d.ts.map +1 -1
- package/dist/commands/allow.js +5 -19
- package/dist/commands/allow.js.map +1 -1
- package/dist/commands/apply.d.ts +1 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +105 -46
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +1849 -1783
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +83 -24
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +4 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +344 -48
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +629 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/prompt.d.ts +7 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +106 -25
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/ship.d.ts +32 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +1404 -75
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +527 -102
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +89 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/custom-policy-rules.d.ts +21 -0
- package/dist/utils/custom-policy-rules.d.ts.map +1 -0
- package/dist/utils/custom-policy-rules.js +71 -0
- package/dist/utils/custom-policy-rules.js.map +1 -0
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +4 -0
- package/dist/utils/plan-cache.js.map +1 -1
- package/dist/utils/policy-audit.d.ts +29 -0
- package/dist/utils/policy-audit.d.ts.map +1 -0
- package/dist/utils/policy-audit.js +208 -0
- package/dist/utils/policy-audit.js.map +1 -0
- package/dist/utils/policy-exceptions.d.ts +96 -0
- package/dist/utils/policy-exceptions.d.ts.map +1 -0
- package/dist/utils/policy-exceptions.js +389 -0
- package/dist/utils/policy-exceptions.js.map +1 -0
- package/dist/utils/policy-governance.d.ts +24 -0
- package/dist/utils/policy-governance.d.ts.map +1 -0
- package/dist/utils/policy-governance.js +124 -0
- package/dist/utils/policy-governance.js.map +1 -0
- package/dist/utils/policy-packs.d.ts +72 -1
- package/dist/utils/policy-packs.d.ts.map +1 -1
- package/dist/utils/policy-packs.js +285 -0
- package/dist/utils/policy-packs.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/ship.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.shipCommand = shipCommand;
|
|
4
|
+
exports.shipResumeCommand = shipResumeCommand;
|
|
5
|
+
exports.shipRunsCommand = shipRunsCommand;
|
|
6
|
+
exports.shipAttestationVerifyCommand = shipAttestationVerifyCommand;
|
|
4
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
5
9
|
const fs_1 = require("fs");
|
|
6
10
|
const path_1 = require("path");
|
|
7
11
|
const api_client_1 = require("../api-client");
|
|
@@ -27,12 +31,145 @@ catch {
|
|
|
27
31
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
28
32
|
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;
|
|
29
33
|
const WRITE_PATH_PATTERN = /✅\s+Written:\s+(.+)$/gm;
|
|
34
|
+
function getShipRunDir(cwd) {
|
|
35
|
+
return (0, path_1.join)(cwd, '.neurcode', 'ship', 'runs');
|
|
36
|
+
}
|
|
37
|
+
function getShipRunPath(cwd, runId) {
|
|
38
|
+
return (0, path_1.join)(getShipRunDir(cwd), `${runId}.json`);
|
|
39
|
+
}
|
|
40
|
+
function saveShipCheckpoint(cwd, checkpoint) {
|
|
41
|
+
const dir = getShipRunDir(cwd);
|
|
42
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
43
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
(0, fs_1.writeFileSync)(getShipRunPath(cwd, checkpoint.runId), JSON.stringify(checkpoint, null, 2) + '\n', 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
function loadShipCheckpoint(cwd, runId) {
|
|
48
|
+
const path = getShipRunPath(cwd, runId);
|
|
49
|
+
if (!(0, fs_1.existsSync)(path))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
53
|
+
if (parsed &&
|
|
54
|
+
parsed.version === 1 &&
|
|
55
|
+
typeof parsed.runId === 'string' &&
|
|
56
|
+
typeof parsed.goal === 'string' &&
|
|
57
|
+
typeof parsed.cwd === 'string') {
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Invalid checkpoint payload.
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function listShipRunSummaries(cwd) {
|
|
67
|
+
const dir = getShipRunDir(cwd);
|
|
68
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
69
|
+
return [];
|
|
70
|
+
const summaries = [];
|
|
71
|
+
for (const entry of (0, fs_1.readdirSync)(dir)) {
|
|
72
|
+
if (!entry.endsWith('.json'))
|
|
73
|
+
continue;
|
|
74
|
+
const runId = entry.replace(/\.json$/, '');
|
|
75
|
+
const checkpoint = loadShipCheckpoint(cwd, runId);
|
|
76
|
+
if (!checkpoint)
|
|
77
|
+
continue;
|
|
78
|
+
summaries.push({
|
|
79
|
+
runId: checkpoint.runId,
|
|
80
|
+
status: checkpoint.status,
|
|
81
|
+
stage: checkpoint.stage,
|
|
82
|
+
goal: checkpoint.goal,
|
|
83
|
+
updatedAt: checkpoint.updatedAt,
|
|
84
|
+
currentPlanId: checkpoint.currentPlanId,
|
|
85
|
+
resultStatus: checkpoint.resultStatus,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
89
|
+
return summaries;
|
|
90
|
+
}
|
|
91
|
+
function createShipCheckpoint(input) {
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
runId: input.runId,
|
|
95
|
+
goal: input.goal,
|
|
96
|
+
cwd: input.cwd,
|
|
97
|
+
status: 'running',
|
|
98
|
+
stage: 'bootstrap',
|
|
99
|
+
startedAt: input.startedAt,
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
options: {
|
|
102
|
+
projectId: input.options.projectId || null,
|
|
103
|
+
maxFixAttempts: input.maxFixAttempts,
|
|
104
|
+
allowDirty: input.options.allowDirty === true,
|
|
105
|
+
skipTests: input.options.skipTests === true,
|
|
106
|
+
testCommand: input.options.testCommand || null,
|
|
107
|
+
record: input.options.record !== false,
|
|
108
|
+
requirePass: input.requirePass,
|
|
109
|
+
requirePolicyLock: input.requirePolicyLock,
|
|
110
|
+
skipPolicyLock: input.skipPolicyLock,
|
|
111
|
+
publishCard: input.options.publishCard !== false,
|
|
112
|
+
},
|
|
113
|
+
baselineDirtyPaths: [],
|
|
114
|
+
initialPlanId: null,
|
|
115
|
+
currentPlanId: null,
|
|
116
|
+
repairPlanIds: [],
|
|
117
|
+
remediationAttemptsUsed: 0,
|
|
118
|
+
verifyExitCode: null,
|
|
119
|
+
verifyPayload: null,
|
|
120
|
+
tests: {
|
|
121
|
+
skipped: input.options.skipTests === true,
|
|
122
|
+
passed: input.options.skipTests === true,
|
|
123
|
+
exitCode: input.options.skipTests === true ? 0 : null,
|
|
124
|
+
attempts: 0,
|
|
125
|
+
command: input.options.testCommand || null,
|
|
126
|
+
},
|
|
127
|
+
resultStatus: null,
|
|
128
|
+
artifacts: null,
|
|
129
|
+
shareCard: null,
|
|
130
|
+
audit: null,
|
|
131
|
+
error: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
30
134
|
function stripAnsi(value) {
|
|
31
135
|
return value.replace(ANSI_PATTERN, '');
|
|
32
136
|
}
|
|
33
137
|
function clamp(value, min, max) {
|
|
34
138
|
return Math.max(min, Math.min(max, value));
|
|
35
139
|
}
|
|
140
|
+
function parsePositiveInt(raw) {
|
|
141
|
+
if (!raw)
|
|
142
|
+
return null;
|
|
143
|
+
const parsed = Number(raw);
|
|
144
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
145
|
+
return null;
|
|
146
|
+
return Math.floor(parsed);
|
|
147
|
+
}
|
|
148
|
+
function resolveTimeoutMs(raw, fallbackMs) {
|
|
149
|
+
const parsed = parsePositiveInt(raw);
|
|
150
|
+
const candidate = parsed ?? fallbackMs;
|
|
151
|
+
return clamp(candidate, 30_000, 60 * 60 * 1000);
|
|
152
|
+
}
|
|
153
|
+
function resolveHeartbeatMs(raw, fallbackMs) {
|
|
154
|
+
const parsed = parsePositiveInt(raw);
|
|
155
|
+
const candidate = parsed ?? fallbackMs;
|
|
156
|
+
return clamp(candidate, 5_000, 120_000);
|
|
157
|
+
}
|
|
158
|
+
function getPlanTimeoutMs() {
|
|
159
|
+
return resolveTimeoutMs(process.env.NEURCODE_SHIP_PLAN_TIMEOUT_MS, resolveTimeoutMs(process.env.NEURCODE_SHIP_STEP_TIMEOUT_MS, 8 * 60 * 1000));
|
|
160
|
+
}
|
|
161
|
+
function getApplyTimeoutMs() {
|
|
162
|
+
return resolveTimeoutMs(process.env.NEURCODE_SHIP_APPLY_TIMEOUT_MS, resolveTimeoutMs(process.env.NEURCODE_SHIP_STEP_TIMEOUT_MS, 15 * 60 * 1000));
|
|
163
|
+
}
|
|
164
|
+
function getVerifyTimeoutMs() {
|
|
165
|
+
return resolveTimeoutMs(process.env.NEURCODE_SHIP_VERIFY_TIMEOUT_MS, resolveTimeoutMs(process.env.NEURCODE_SHIP_STEP_TIMEOUT_MS, 6 * 60 * 1000));
|
|
166
|
+
}
|
|
167
|
+
function getTestTimeoutMs() {
|
|
168
|
+
return resolveTimeoutMs(process.env.NEURCODE_SHIP_TEST_TIMEOUT_MS, resolveTimeoutMs(process.env.NEURCODE_SHIP_STEP_TIMEOUT_MS, 20 * 60 * 1000));
|
|
169
|
+
}
|
|
170
|
+
function getHeartbeatIntervalMs() {
|
|
171
|
+
return resolveHeartbeatMs(process.env.NEURCODE_SHIP_HEARTBEAT_MS, 30_000);
|
|
172
|
+
}
|
|
36
173
|
function shellTailLines(text, limit) {
|
|
37
174
|
return text
|
|
38
175
|
.split('\n')
|
|
@@ -40,12 +177,46 @@ function shellTailLines(text, limit) {
|
|
|
40
177
|
.slice(-limit)
|
|
41
178
|
.join('\n');
|
|
42
179
|
}
|
|
180
|
+
function emitShipJson(payload) {
|
|
181
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
182
|
+
}
|
|
183
|
+
function inferStepStatus(run) {
|
|
184
|
+
if (!run)
|
|
185
|
+
return 'SKIPPED';
|
|
186
|
+
const stderr = (run.stderr || '').toLowerCase();
|
|
187
|
+
if (run.code === 124 || stderr.includes('exceeded timeout')) {
|
|
188
|
+
return 'TIMEOUT';
|
|
189
|
+
}
|
|
190
|
+
return run.code === 0 ? 'SUCCESS' : 'FAILED';
|
|
191
|
+
}
|
|
192
|
+
function recordRunStep(steps, input) {
|
|
193
|
+
const nowIso = new Date().toISOString();
|
|
194
|
+
const run = input.run;
|
|
195
|
+
const durationMs = run?.durationMs ?? 0;
|
|
196
|
+
const startedAt = input.startedAt
|
|
197
|
+
? input.startedAt
|
|
198
|
+
: new Date(Date.now() - Math.max(0, durationMs)).toISOString();
|
|
199
|
+
steps.push({
|
|
200
|
+
stage: input.stage,
|
|
201
|
+
attempt: input.attempt,
|
|
202
|
+
status: inferStepStatus(run),
|
|
203
|
+
startedAt,
|
|
204
|
+
endedAt: nowIso,
|
|
205
|
+
durationMs,
|
|
206
|
+
...(run ? { exitCode: run.code } : {}),
|
|
207
|
+
...(input.planId ? { planId: input.planId } : {}),
|
|
208
|
+
...(input.message ? { message: input.message } : {}),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
43
211
|
function getCliEntryPath() {
|
|
44
212
|
return (0, path_1.resolve)(__dirname, '..', 'index.js');
|
|
45
213
|
}
|
|
46
|
-
function runCliCommand(cwd, args, extraEnv) {
|
|
214
|
+
function runCliCommand(cwd, args, extraEnv, execution) {
|
|
47
215
|
return new Promise((resolvePromise) => {
|
|
48
216
|
const startedAt = Date.now();
|
|
217
|
+
const timeoutMs = execution?.timeoutMs ?? getPlanTimeoutMs();
|
|
218
|
+
const heartbeatMs = execution?.heartbeatMs ?? getHeartbeatIntervalMs();
|
|
219
|
+
const commandLabel = execution?.label || `neurcode ${args.join(' ')}`;
|
|
49
220
|
const child = (0, child_process_1.spawn)(process.execPath, [getCliEntryPath(), ...args], {
|
|
50
221
|
cwd,
|
|
51
222
|
env: {
|
|
@@ -57,6 +228,61 @@ function runCliCommand(cwd, args, extraEnv) {
|
|
|
57
228
|
});
|
|
58
229
|
let stdout = '';
|
|
59
230
|
let stderr = '';
|
|
231
|
+
let settled = false;
|
|
232
|
+
let timedOut = false;
|
|
233
|
+
let timeoutHandle = null;
|
|
234
|
+
let heartbeatHandle = null;
|
|
235
|
+
let forceKillHandle = null;
|
|
236
|
+
const finalize = (code) => {
|
|
237
|
+
if (settled)
|
|
238
|
+
return;
|
|
239
|
+
settled = true;
|
|
240
|
+
if (timeoutHandle)
|
|
241
|
+
clearTimeout(timeoutHandle);
|
|
242
|
+
if (heartbeatHandle)
|
|
243
|
+
clearInterval(heartbeatHandle);
|
|
244
|
+
if (forceKillHandle)
|
|
245
|
+
clearTimeout(forceKillHandle);
|
|
246
|
+
resolvePromise({
|
|
247
|
+
code,
|
|
248
|
+
stdout,
|
|
249
|
+
stderr,
|
|
250
|
+
durationMs: Date.now() - startedAt,
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
timeoutHandle = setTimeout(() => {
|
|
254
|
+
timedOut = true;
|
|
255
|
+
const timeoutMessage = `⏱️ ${commandLabel} exceeded timeout (${Math.round(timeoutMs / 1000)}s). Terminating.`;
|
|
256
|
+
stderr += `${timeoutMessage}\n`;
|
|
257
|
+
console.error(chalk.red(timeoutMessage));
|
|
258
|
+
try {
|
|
259
|
+
child.kill('SIGTERM');
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Ignore process termination errors.
|
|
263
|
+
}
|
|
264
|
+
forceKillHandle = setTimeout(() => {
|
|
265
|
+
try {
|
|
266
|
+
child.kill('SIGKILL');
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Ignore process termination errors.
|
|
270
|
+
}
|
|
271
|
+
}, 5_000);
|
|
272
|
+
if (typeof forceKillHandle.unref === 'function') {
|
|
273
|
+
forceKillHandle.unref();
|
|
274
|
+
}
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
if (typeof timeoutHandle.unref === 'function') {
|
|
277
|
+
timeoutHandle.unref();
|
|
278
|
+
}
|
|
279
|
+
heartbeatHandle = setInterval(() => {
|
|
280
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
281
|
+
console.log(chalk.dim(`⏳ ${commandLabel} still running (${elapsedSeconds}s elapsed)...`));
|
|
282
|
+
}, heartbeatMs);
|
|
283
|
+
if (typeof heartbeatHandle.unref === 'function') {
|
|
284
|
+
heartbeatHandle.unref();
|
|
285
|
+
}
|
|
60
286
|
child.stdout.on('data', (chunk) => {
|
|
61
287
|
const text = chunk.toString();
|
|
62
288
|
stdout += text;
|
|
@@ -67,19 +293,25 @@ function runCliCommand(cwd, args, extraEnv) {
|
|
|
67
293
|
stderr += text;
|
|
68
294
|
process.stderr.write(text);
|
|
69
295
|
});
|
|
296
|
+
child.on('error', (error) => {
|
|
297
|
+
stderr += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
298
|
+
finalize(1);
|
|
299
|
+
});
|
|
70
300
|
child.on('close', (code) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
301
|
+
if (timedOut) {
|
|
302
|
+
finalize(124);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
finalize(code ?? 1);
|
|
77
306
|
});
|
|
78
307
|
});
|
|
79
308
|
}
|
|
80
|
-
function runShellCommand(cwd, command) {
|
|
309
|
+
function runShellCommand(cwd, command, execution) {
|
|
81
310
|
return new Promise((resolvePromise) => {
|
|
82
311
|
const startedAt = Date.now();
|
|
312
|
+
const timeoutMs = execution?.timeoutMs ?? getTestTimeoutMs();
|
|
313
|
+
const heartbeatMs = execution?.heartbeatMs ?? getHeartbeatIntervalMs();
|
|
314
|
+
const commandLabel = execution?.label || command;
|
|
83
315
|
const child = (0, child_process_1.spawn)(command, {
|
|
84
316
|
cwd,
|
|
85
317
|
env: {
|
|
@@ -90,6 +322,61 @@ function runShellCommand(cwd, command) {
|
|
|
90
322
|
});
|
|
91
323
|
let stdout = '';
|
|
92
324
|
let stderr = '';
|
|
325
|
+
let settled = false;
|
|
326
|
+
let timedOut = false;
|
|
327
|
+
let timeoutHandle = null;
|
|
328
|
+
let heartbeatHandle = null;
|
|
329
|
+
let forceKillHandle = null;
|
|
330
|
+
const finalize = (code) => {
|
|
331
|
+
if (settled)
|
|
332
|
+
return;
|
|
333
|
+
settled = true;
|
|
334
|
+
if (timeoutHandle)
|
|
335
|
+
clearTimeout(timeoutHandle);
|
|
336
|
+
if (heartbeatHandle)
|
|
337
|
+
clearInterval(heartbeatHandle);
|
|
338
|
+
if (forceKillHandle)
|
|
339
|
+
clearTimeout(forceKillHandle);
|
|
340
|
+
resolvePromise({
|
|
341
|
+
code,
|
|
342
|
+
stdout,
|
|
343
|
+
stderr,
|
|
344
|
+
durationMs: Date.now() - startedAt,
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
timeoutHandle = setTimeout(() => {
|
|
348
|
+
timedOut = true;
|
|
349
|
+
const timeoutMessage = `⏱️ ${commandLabel} exceeded timeout (${Math.round(timeoutMs / 1000)}s). Terminating.`;
|
|
350
|
+
stderr += `${timeoutMessage}\n`;
|
|
351
|
+
console.error(chalk.red(timeoutMessage));
|
|
352
|
+
try {
|
|
353
|
+
child.kill('SIGTERM');
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Ignore process termination errors.
|
|
357
|
+
}
|
|
358
|
+
forceKillHandle = setTimeout(() => {
|
|
359
|
+
try {
|
|
360
|
+
child.kill('SIGKILL');
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Ignore process termination errors.
|
|
364
|
+
}
|
|
365
|
+
}, 5_000);
|
|
366
|
+
if (typeof forceKillHandle.unref === 'function') {
|
|
367
|
+
forceKillHandle.unref();
|
|
368
|
+
}
|
|
369
|
+
}, timeoutMs);
|
|
370
|
+
if (typeof timeoutHandle.unref === 'function') {
|
|
371
|
+
timeoutHandle.unref();
|
|
372
|
+
}
|
|
373
|
+
heartbeatHandle = setInterval(() => {
|
|
374
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
375
|
+
console.log(chalk.dim(`⏳ ${commandLabel} still running (${elapsedSeconds}s elapsed)...`));
|
|
376
|
+
}, heartbeatMs);
|
|
377
|
+
if (typeof heartbeatHandle.unref === 'function') {
|
|
378
|
+
heartbeatHandle.unref();
|
|
379
|
+
}
|
|
93
380
|
child.stdout.on('data', (chunk) => {
|
|
94
381
|
const text = chunk.toString();
|
|
95
382
|
stdout += text;
|
|
@@ -100,13 +387,16 @@ function runShellCommand(cwd, command) {
|
|
|
100
387
|
stderr += text;
|
|
101
388
|
process.stderr.write(text);
|
|
102
389
|
});
|
|
390
|
+
child.on('error', (error) => {
|
|
391
|
+
stderr += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
392
|
+
finalize(1);
|
|
393
|
+
});
|
|
103
394
|
child.on('close', (code) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
395
|
+
if (timedOut) {
|
|
396
|
+
finalize(124);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
finalize(code ?? 1);
|
|
110
400
|
});
|
|
111
401
|
});
|
|
112
402
|
}
|
|
@@ -154,6 +444,104 @@ function parseVerifyPayload(output) {
|
|
|
154
444
|
message: typeof item.message === 'string' ? item.message : undefined,
|
|
155
445
|
startLine: typeof item.startLine === 'number' ? item.startLine : undefined,
|
|
156
446
|
}));
|
|
447
|
+
const policyLock = record.policyLock && typeof record.policyLock === 'object' && !Array.isArray(record.policyLock)
|
|
448
|
+
? (() => {
|
|
449
|
+
const raw = record.policyLock;
|
|
450
|
+
const mismatches = Array.isArray(raw.mismatches)
|
|
451
|
+
? raw.mismatches
|
|
452
|
+
.filter((item) => !!item && typeof item === 'object')
|
|
453
|
+
.map((item) => ({
|
|
454
|
+
code: typeof item.code === 'string' ? item.code : 'UNKNOWN',
|
|
455
|
+
message: typeof item.message === 'string' ? item.message : '',
|
|
456
|
+
expected: typeof item.expected === 'string' ? item.expected : undefined,
|
|
457
|
+
actual: typeof item.actual === 'string' ? item.actual : undefined,
|
|
458
|
+
}))
|
|
459
|
+
: [];
|
|
460
|
+
return {
|
|
461
|
+
enforced: raw.enforced === true,
|
|
462
|
+
matched: raw.matched !== false,
|
|
463
|
+
path: typeof raw.path === 'string' ? raw.path : '',
|
|
464
|
+
mismatches,
|
|
465
|
+
};
|
|
466
|
+
})()
|
|
467
|
+
: undefined;
|
|
468
|
+
const policyExceptions = record.policyExceptions && typeof record.policyExceptions === 'object' && !Array.isArray(record.policyExceptions)
|
|
469
|
+
? (() => {
|
|
470
|
+
const raw = record.policyExceptions;
|
|
471
|
+
const matchedExceptionIds = Array.isArray(raw.matchedExceptionIds)
|
|
472
|
+
? raw.matchedExceptionIds.filter((item) => typeof item === 'string')
|
|
473
|
+
: [];
|
|
474
|
+
const suppressedViolations = Array.isArray(raw.suppressedViolations)
|
|
475
|
+
? raw.suppressedViolations
|
|
476
|
+
.filter((item) => !!item && typeof item === 'object')
|
|
477
|
+
.map((item) => ({
|
|
478
|
+
file: typeof item.file === 'string' ? item.file : 'unknown',
|
|
479
|
+
rule: typeof item.rule === 'string' ? item.rule : 'unknown',
|
|
480
|
+
severity: typeof item.severity === 'string' ? item.severity : 'warn',
|
|
481
|
+
message: typeof item.message === 'string' ? item.message : undefined,
|
|
482
|
+
exceptionId: typeof item.exceptionId === 'string' ? item.exceptionId : 'unknown',
|
|
483
|
+
reason: typeof item.reason === 'string' ? item.reason : '',
|
|
484
|
+
expiresAt: typeof item.expiresAt === 'string' ? item.expiresAt : '',
|
|
485
|
+
startLine: typeof item.startLine === 'number' ? item.startLine : undefined,
|
|
486
|
+
}))
|
|
487
|
+
: [];
|
|
488
|
+
return {
|
|
489
|
+
configured: typeof raw.configured === 'number' ? raw.configured : 0,
|
|
490
|
+
active: typeof raw.active === 'number' ? raw.active : 0,
|
|
491
|
+
usable: typeof raw.usable === 'number' ? raw.usable : undefined,
|
|
492
|
+
matched: typeof raw.matched === 'number' ? raw.matched : matchedExceptionIds.length,
|
|
493
|
+
suppressed: typeof raw.suppressed === 'number' ? raw.suppressed : suppressedViolations.length,
|
|
494
|
+
blocked: typeof raw.blocked === 'number' ? raw.blocked : undefined,
|
|
495
|
+
matchedExceptionIds,
|
|
496
|
+
suppressedViolations,
|
|
497
|
+
blockedViolations: Array.isArray(raw.blockedViolations)
|
|
498
|
+
? raw.blockedViolations
|
|
499
|
+
.filter((item) => !!item && typeof item === 'object')
|
|
500
|
+
.map((item) => ({
|
|
501
|
+
file: typeof item.file === 'string' ? item.file : 'unknown',
|
|
502
|
+
rule: typeof item.rule === 'string' ? item.rule : 'unknown',
|
|
503
|
+
severity: typeof item.severity === 'string' ? item.severity : 'warn',
|
|
504
|
+
message: typeof item.message === 'string' ? item.message : undefined,
|
|
505
|
+
startLine: typeof item.startLine === 'number' ? item.startLine : undefined,
|
|
506
|
+
}))
|
|
507
|
+
: undefined,
|
|
508
|
+
};
|
|
509
|
+
})()
|
|
510
|
+
: undefined;
|
|
511
|
+
const policyGovernance = record.policyGovernance && typeof record.policyGovernance === 'object' && !Array.isArray(record.policyGovernance)
|
|
512
|
+
? (() => {
|
|
513
|
+
const raw = record.policyGovernance;
|
|
514
|
+
const approvalsRaw = raw.exceptionApprovals && typeof raw.exceptionApprovals === 'object' && !Array.isArray(raw.exceptionApprovals)
|
|
515
|
+
? raw.exceptionApprovals
|
|
516
|
+
: null;
|
|
517
|
+
const auditRaw = raw.audit && typeof raw.audit === 'object' && !Array.isArray(raw.audit)
|
|
518
|
+
? raw.audit
|
|
519
|
+
: null;
|
|
520
|
+
return {
|
|
521
|
+
exceptionApprovals: approvalsRaw
|
|
522
|
+
? {
|
|
523
|
+
required: approvalsRaw.required === true,
|
|
524
|
+
minApprovals: typeof approvalsRaw.minApprovals === 'number' ? approvalsRaw.minApprovals : 1,
|
|
525
|
+
disallowSelfApproval: approvalsRaw.disallowSelfApproval !== false,
|
|
526
|
+
allowedApprovers: Array.isArray(approvalsRaw.allowedApprovers)
|
|
527
|
+
? approvalsRaw.allowedApprovers.filter((item) => typeof item === 'string')
|
|
528
|
+
: [],
|
|
529
|
+
}
|
|
530
|
+
: undefined,
|
|
531
|
+
audit: auditRaw
|
|
532
|
+
? {
|
|
533
|
+
requireIntegrity: auditRaw.requireIntegrity === true,
|
|
534
|
+
valid: auditRaw.valid !== false,
|
|
535
|
+
issues: Array.isArray(auditRaw.issues)
|
|
536
|
+
? auditRaw.issues.filter((item) => typeof item === 'string')
|
|
537
|
+
: [],
|
|
538
|
+
lastHash: typeof auditRaw.lastHash === 'string' ? auditRaw.lastHash : null,
|
|
539
|
+
eventCount: typeof auditRaw.eventCount === 'number' ? auditRaw.eventCount : 0,
|
|
540
|
+
}
|
|
541
|
+
: undefined,
|
|
542
|
+
};
|
|
543
|
+
})()
|
|
544
|
+
: undefined;
|
|
157
545
|
return {
|
|
158
546
|
grade: record.grade,
|
|
159
547
|
score: typeof record.score === 'number' ? record.score : 0,
|
|
@@ -170,6 +558,59 @@ function parseVerifyPayload(output) {
|
|
|
170
558
|
plannedFilesModified: typeof record.plannedFilesModified === 'number' ? record.plannedFilesModified : undefined,
|
|
171
559
|
totalPlannedFiles: typeof record.totalPlannedFiles === 'number' ? record.totalPlannedFiles : undefined,
|
|
172
560
|
policyDecision: typeof record.policyDecision === 'string' ? record.policyDecision : undefined,
|
|
561
|
+
policyLock,
|
|
562
|
+
policyExceptions,
|
|
563
|
+
policyGovernance,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function parsePlanPayload(output) {
|
|
567
|
+
const parsed = extractLastJsonObject(output);
|
|
568
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
569
|
+
return null;
|
|
570
|
+
const record = parsed;
|
|
571
|
+
if (typeof record.success !== 'boolean') {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const planIdValue = record.planId;
|
|
575
|
+
const planId = typeof planIdValue === 'string' && planIdValue.trim().length > 0 ? planIdValue : null;
|
|
576
|
+
return {
|
|
577
|
+
success: record.success,
|
|
578
|
+
planId,
|
|
579
|
+
sessionId: typeof record.sessionId === 'string' ? record.sessionId : null,
|
|
580
|
+
projectId: typeof record.projectId === 'string' ? record.projectId : null,
|
|
581
|
+
mode: typeof record.mode === 'string' ? record.mode : undefined,
|
|
582
|
+
cached: typeof record.cached === 'boolean' ? record.cached : undefined,
|
|
583
|
+
timestamp: typeof record.timestamp === 'string' ? record.timestamp : undefined,
|
|
584
|
+
message: typeof record.message === 'string' ? record.message : undefined,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function parseApplyPayload(output) {
|
|
588
|
+
const parsed = extractLastJsonObject(output);
|
|
589
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
590
|
+
return null;
|
|
591
|
+
const record = parsed;
|
|
592
|
+
if (typeof record.success !== 'boolean' || typeof record.planId !== 'string') {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const files = Array.isArray(record.files)
|
|
596
|
+
? record.files
|
|
597
|
+
.filter((item) => !!item && typeof item === 'object')
|
|
598
|
+
.map((item) => ({
|
|
599
|
+
path: typeof item.path === 'string' ? item.path : '',
|
|
600
|
+
content: typeof item.content === 'string' ? item.content : '',
|
|
601
|
+
}))
|
|
602
|
+
.filter((item) => item.path.length > 0)
|
|
603
|
+
: [];
|
|
604
|
+
const writtenFiles = Array.isArray(record.writtenFiles)
|
|
605
|
+
? record.writtenFiles.filter((item) => typeof item === 'string')
|
|
606
|
+
: undefined;
|
|
607
|
+
return {
|
|
608
|
+
success: record.success,
|
|
609
|
+
planId: record.planId,
|
|
610
|
+
filesGenerated: typeof record.filesGenerated === 'number' ? record.filesGenerated : files.length,
|
|
611
|
+
files,
|
|
612
|
+
writtenFiles,
|
|
613
|
+
message: typeof record.message === 'string' ? record.message : undefined,
|
|
173
614
|
};
|
|
174
615
|
}
|
|
175
616
|
function isInfoOnlyGovernanceResult(payload) {
|
|
@@ -238,11 +679,9 @@ function ensureCleanTreeOrExit(cwd, allowDirty) {
|
|
|
238
679
|
});
|
|
239
680
|
const hasDirtyFiles = relevantPaths.length > 0;
|
|
240
681
|
if (hasDirtyFiles && !allowDirty) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.error(chalk.dim(` Dirty paths: ${relevantPaths.slice(0, 5).join(', ')}`));
|
|
245
|
-
process.exit(1);
|
|
682
|
+
const error = new Error(`WORKTREE_DIRTY:${relevantPaths.slice(0, 5).join(', ')}`);
|
|
683
|
+
error.dirtyPaths = relevantPaths;
|
|
684
|
+
throw error;
|
|
246
685
|
}
|
|
247
686
|
return relevantPaths;
|
|
248
687
|
}
|
|
@@ -554,6 +993,11 @@ function writeMergeConfidenceArtifacts(cwd, card) {
|
|
|
554
993
|
`- Grade: **${card.verification.grade}**`,
|
|
555
994
|
`- Score: **${card.verification.adherenceScore ?? card.verification.score}**`,
|
|
556
995
|
`- Violations: **${card.verification.violations.length}**`,
|
|
996
|
+
`- Policy Lock: **${card.verification.policyLock?.enforced ? `enforced (${card.verification.policyLock.matched ? 'matched' : 'mismatch'})` : 'not enforced'}**`,
|
|
997
|
+
`- Policy Exceptions Suppressed: **${card.verification.policyExceptions?.suppressed ?? 0}**`,
|
|
998
|
+
`- Policy Exceptions Blocked: **${card.verification.policyExceptions?.blocked ?? 0}**`,
|
|
999
|
+
`- Approval Governance: **${card.verification.policyGovernance?.exceptionApprovals?.required ? `required (${card.verification.policyGovernance.exceptionApprovals.minApprovals})` : 'not required'}**`,
|
|
1000
|
+
`- Audit Integrity: **${card.verification.policyGovernance?.audit?.requireIntegrity ? (card.verification.policyGovernance.audit.valid ? 'required+valid' : 'required+invalid') : 'not required'}**`,
|
|
557
1001
|
'',
|
|
558
1002
|
'## Blast Radius',
|
|
559
1003
|
'',
|
|
@@ -586,6 +1030,19 @@ function writeMergeConfidenceArtifacts(cwd, card) {
|
|
|
586
1030
|
`- Passed: **${card.tests.passed ? 'yes' : 'no'}**`,
|
|
587
1031
|
card.tests.command ? `- Command: \`${card.tests.command}\`` : '- Command: none',
|
|
588
1032
|
'',
|
|
1033
|
+
'## Execution Audit',
|
|
1034
|
+
'',
|
|
1035
|
+
`- Run ID: \`${card.audit.runId}\``,
|
|
1036
|
+
`- Started: ${card.audit.startedAt}`,
|
|
1037
|
+
`- Finished: ${card.audit.finishedAt}`,
|
|
1038
|
+
`- Duration: **${card.audit.durationMs}ms**`,
|
|
1039
|
+
`- Timeouts (ms): plan=${card.audit.timeoutMs.plan}, apply=${card.audit.timeoutMs.apply}, verify=${card.audit.timeoutMs.verify}, tests=${card.audit.timeoutMs.tests}`,
|
|
1040
|
+
`- Heartbeat: ${card.audit.heartbeatMs}ms`,
|
|
1041
|
+
'- Step Timeline:',
|
|
1042
|
+
...(card.audit.steps.length > 0
|
|
1043
|
+
? card.audit.steps.map((step) => ` - ${step.stage}#${step.attempt} ${step.status} (${step.durationMs}ms)${typeof step.exitCode === 'number' ? ` exit=${step.exitCode}` : ''}${step.planId ? ` plan=${step.planId}` : ''}${step.message ? ` :: ${step.message}` : ''}`)
|
|
1044
|
+
: [' - none']),
|
|
1045
|
+
'',
|
|
589
1046
|
'## Top Changed Files',
|
|
590
1047
|
'',
|
|
591
1048
|
...(card.blastRadius.topFiles.length > 0
|
|
@@ -603,17 +1060,110 @@ function writeMergeConfidenceArtifacts(cwd, card) {
|
|
|
603
1060
|
(0, fs_1.writeFileSync)(markdownPath, markdown, 'utf-8');
|
|
604
1061
|
return { jsonPath, markdownPath };
|
|
605
1062
|
}
|
|
1063
|
+
function sha256Hex(input) {
|
|
1064
|
+
return (0, crypto_1.createHash)('sha256').update(input, 'utf-8').digest('hex');
|
|
1065
|
+
}
|
|
1066
|
+
function writeReleaseAttestation(cwd, card, artifacts) {
|
|
1067
|
+
const outDir = (0, path_1.join)(cwd, '.neurcode', 'ship', 'attestations');
|
|
1068
|
+
(0, fs_1.mkdirSync)(outDir, { recursive: true });
|
|
1069
|
+
const attestationBase = {
|
|
1070
|
+
schemaVersion: 1,
|
|
1071
|
+
attestationId: `att_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`,
|
|
1072
|
+
generatedAt: new Date().toISOString(),
|
|
1073
|
+
runId: card.audit.runId,
|
|
1074
|
+
status: card.status,
|
|
1075
|
+
repository: {
|
|
1076
|
+
root: card.repository.root,
|
|
1077
|
+
branch: card.repository.branch,
|
|
1078
|
+
headSha: card.repository.headSha,
|
|
1079
|
+
},
|
|
1080
|
+
card: {
|
|
1081
|
+
jsonPath: artifacts.jsonPath,
|
|
1082
|
+
markdownPath: artifacts.markdownPath,
|
|
1083
|
+
sha256: sha256Hex((0, fs_1.readFileSync)(artifacts.jsonPath, 'utf-8')),
|
|
1084
|
+
},
|
|
1085
|
+
plans: {
|
|
1086
|
+
initialPlanId: card.plans.initialPlanId,
|
|
1087
|
+
finalPlanId: card.plans.finalPlanId,
|
|
1088
|
+
repairPlanIds: [...card.plans.repairPlanIds],
|
|
1089
|
+
},
|
|
1090
|
+
verification: {
|
|
1091
|
+
verdict: card.verification.verdict,
|
|
1092
|
+
grade: card.verification.grade,
|
|
1093
|
+
score: Number.isFinite(card.verification.adherenceScore)
|
|
1094
|
+
? card.verification.adherenceScore
|
|
1095
|
+
: card.verification.score,
|
|
1096
|
+
violations: card.verification.violations.length,
|
|
1097
|
+
policyLock: {
|
|
1098
|
+
enforced: card.verification.policyLock?.enforced === true,
|
|
1099
|
+
matched: card.verification.policyLock?.matched !== false,
|
|
1100
|
+
},
|
|
1101
|
+
policyExceptions: {
|
|
1102
|
+
matched: card.verification.policyExceptions?.matched ?? 0,
|
|
1103
|
+
suppressed: card.verification.policyExceptions?.suppressed ?? 0,
|
|
1104
|
+
blocked: card.verification.policyExceptions?.blocked ?? 0,
|
|
1105
|
+
matchedExceptionIds: card.verification.policyExceptions?.matchedExceptionIds
|
|
1106
|
+
? [...card.verification.policyExceptions.matchedExceptionIds]
|
|
1107
|
+
: [],
|
|
1108
|
+
},
|
|
1109
|
+
policyGovernance: {
|
|
1110
|
+
approvalRequired: card.verification.policyGovernance?.exceptionApprovals?.required === true,
|
|
1111
|
+
minApprovals: card.verification.policyGovernance?.exceptionApprovals?.minApprovals ?? 1,
|
|
1112
|
+
disallowSelfApproval: card.verification.policyGovernance?.exceptionApprovals?.disallowSelfApproval !== false,
|
|
1113
|
+
allowedApprovers: card.verification.policyGovernance?.exceptionApprovals?.allowedApprovers
|
|
1114
|
+
? [...card.verification.policyGovernance.exceptionApprovals.allowedApprovers]
|
|
1115
|
+
: [],
|
|
1116
|
+
auditIntegrityRequired: card.verification.policyGovernance?.audit?.requireIntegrity === true,
|
|
1117
|
+
auditIntegrityValid: card.verification.policyGovernance?.audit?.valid !== false,
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
tests: {
|
|
1121
|
+
skipped: card.tests.skipped,
|
|
1122
|
+
passed: card.tests.passed,
|
|
1123
|
+
attempts: card.tests.attempts,
|
|
1124
|
+
lastExitCode: card.tests.lastExitCode,
|
|
1125
|
+
},
|
|
1126
|
+
remediation: {
|
|
1127
|
+
attemptsUsed: card.remediation.attemptsUsed,
|
|
1128
|
+
maxAttempts: card.remediation.maxAttempts,
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
const hmacKey = process.env.NEURCODE_ATTEST_HMAC_KEY;
|
|
1132
|
+
const signature = hmacKey
|
|
1133
|
+
? {
|
|
1134
|
+
algorithm: 'hmac-sha256',
|
|
1135
|
+
keyId: process.env.NEURCODE_ATTEST_KEY_ID || null,
|
|
1136
|
+
value: (0, crypto_1.createHmac)('sha256', hmacKey)
|
|
1137
|
+
.update(JSON.stringify(attestationBase), 'utf-8')
|
|
1138
|
+
.digest('hex'),
|
|
1139
|
+
}
|
|
1140
|
+
: null;
|
|
1141
|
+
const attestation = {
|
|
1142
|
+
...attestationBase,
|
|
1143
|
+
signature,
|
|
1144
|
+
};
|
|
1145
|
+
const ts = attestation.generatedAt.replace(/[:.]/g, '-');
|
|
1146
|
+
const attestationPath = (0, path_1.join)(outDir, `release-attestation-${ts}.json`);
|
|
1147
|
+
(0, fs_1.writeFileSync)(attestationPath, JSON.stringify(attestation, null, 2) + '\n', 'utf-8');
|
|
1148
|
+
return attestationPath;
|
|
1149
|
+
}
|
|
606
1150
|
async function runPlanAndApply(cwd, intent, projectId, controls) {
|
|
607
|
-
const planArgs = ['plan', intent, '--force-plan'];
|
|
1151
|
+
const planArgs = ['plan', intent, '--force-plan', '--json'];
|
|
608
1152
|
if (projectId) {
|
|
609
1153
|
planArgs.push('--project-id', projectId);
|
|
610
1154
|
}
|
|
611
1155
|
const planRun = await runCliCommand(cwd, planArgs, {
|
|
612
1156
|
NEURCODE_PLAN_SKIP_SNAPSHOTS: '1',
|
|
1157
|
+
}, {
|
|
1158
|
+
timeoutMs: getPlanTimeoutMs(),
|
|
1159
|
+
label: 'ship:plan',
|
|
613
1160
|
});
|
|
614
1161
|
const planOutput = `${planRun.stdout}\n${planRun.stderr}`;
|
|
615
|
-
const
|
|
616
|
-
|
|
1162
|
+
const parsedPlan = parsePlanPayload(planOutput);
|
|
1163
|
+
const planId = parsedPlan?.success && parsedPlan.planId
|
|
1164
|
+
? parsedPlan.planId
|
|
1165
|
+
: extractPlanId(planOutput);
|
|
1166
|
+
if (planRun.code !== 0 || !planId || parsedPlan?.success === false) {
|
|
617
1167
|
return { planId: null, planRun, applyRun: null, writtenFiles: [] };
|
|
618
1168
|
}
|
|
619
1169
|
if (controls?.enforceDocumentationScope) {
|
|
@@ -632,59 +1182,274 @@ async function runPlanAndApply(cwd, intent, projectId, controls) {
|
|
|
632
1182
|
};
|
|
633
1183
|
}
|
|
634
1184
|
}
|
|
635
|
-
const applyArgs = ['apply', planId, '--force'];
|
|
636
|
-
const applyRun = await runCliCommand(cwd, applyArgs
|
|
637
|
-
|
|
638
|
-
|
|
1185
|
+
const applyArgs = ['apply', planId, '--force', '--json'];
|
|
1186
|
+
const applyRun = await runCliCommand(cwd, applyArgs, undefined, {
|
|
1187
|
+
timeoutMs: getApplyTimeoutMs(),
|
|
1188
|
+
label: 'ship:apply',
|
|
1189
|
+
});
|
|
1190
|
+
const applyOutput = `${applyRun.stdout}\n${applyRun.stderr}`;
|
|
1191
|
+
const parsedApply = parseApplyPayload(applyOutput);
|
|
1192
|
+
const normalizedApplyRun = parsedApply?.success === false && applyRun.code === 0
|
|
1193
|
+
? { ...applyRun, code: 1 }
|
|
1194
|
+
: applyRun;
|
|
1195
|
+
const writtenFiles = parsedApply?.writtenFiles && parsedApply.writtenFiles.length > 0
|
|
1196
|
+
? Array.from(new Set(parsedApply.writtenFiles))
|
|
1197
|
+
: parsedApply?.files && parsedApply.files.length > 0
|
|
1198
|
+
? Array.from(new Set(parsedApply.files.map((item) => item.path)))
|
|
1199
|
+
: collectApplyWrittenFiles(applyOutput);
|
|
1200
|
+
return { planId, planRun, applyRun: normalizedApplyRun, writtenFiles };
|
|
639
1201
|
}
|
|
640
1202
|
async function shipCommand(goal, options) {
|
|
641
|
-
const
|
|
1203
|
+
const resumedStart = options.resumeStartedAtIso ? Date.parse(options.resumeStartedAtIso) : NaN;
|
|
1204
|
+
const startedAt = Number.isFinite(resumedStart) && resumedStart > 0 ? resumedStart : Date.now();
|
|
1205
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
1206
|
+
const runId = options.resumeRunId || `ship_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
|
642
1207
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
643
1208
|
const maxFixAttempts = clamp(options.maxFixAttempts ?? 2, 0, 5);
|
|
1209
|
+
const requirePass = options.requirePass === true || process.env.NEURCODE_SHIP_REQUIRE_PASS === '1';
|
|
1210
|
+
const requirePolicyLock = options.requirePolicyLock === true || process.env.NEURCODE_SHIP_REQUIRE_POLICY_LOCK === '1';
|
|
1211
|
+
const skipPolicyLock = options.skipPolicyLock === true || process.env.NEURCODE_SHIP_SKIP_POLICY_LOCK === '1';
|
|
644
1212
|
const remediationActions = [];
|
|
645
|
-
const repairPlanIds =
|
|
1213
|
+
const repairPlanIds = Array.isArray(options.resumeRepairPlanIds)
|
|
1214
|
+
? [...options.resumeRepairPlanIds]
|
|
1215
|
+
: [];
|
|
646
1216
|
const recordVerify = options.record !== false;
|
|
1217
|
+
const auditSteps = [];
|
|
1218
|
+
const timeoutConfig = {
|
|
1219
|
+
plan: getPlanTimeoutMs(),
|
|
1220
|
+
apply: getApplyTimeoutMs(),
|
|
1221
|
+
verify: getVerifyTimeoutMs(),
|
|
1222
|
+
tests: getTestTimeoutMs(),
|
|
1223
|
+
};
|
|
1224
|
+
const heartbeatMs = getHeartbeatIntervalMs();
|
|
1225
|
+
const buildAuditSnapshot = () => {
|
|
1226
|
+
const finishedAt = new Date().toISOString();
|
|
1227
|
+
return {
|
|
1228
|
+
runId,
|
|
1229
|
+
startedAt: startedAtIso,
|
|
1230
|
+
finishedAt,
|
|
1231
|
+
durationMs: Date.now() - startedAt,
|
|
1232
|
+
timeoutMs: timeoutConfig,
|
|
1233
|
+
heartbeatMs,
|
|
1234
|
+
steps: auditSteps,
|
|
1235
|
+
};
|
|
1236
|
+
};
|
|
1237
|
+
const checkpoint = createShipCheckpoint({
|
|
1238
|
+
runId,
|
|
1239
|
+
goal: goal || '',
|
|
1240
|
+
cwd,
|
|
1241
|
+
startedAt: startedAtIso,
|
|
1242
|
+
maxFixAttempts,
|
|
1243
|
+
options,
|
|
1244
|
+
requirePass,
|
|
1245
|
+
requirePolicyLock,
|
|
1246
|
+
skipPolicyLock,
|
|
1247
|
+
});
|
|
1248
|
+
if (options.resumeFromPlanId) {
|
|
1249
|
+
checkpoint.stage = 'planned';
|
|
1250
|
+
checkpoint.initialPlanId = options.resumeInitialPlanId || options.resumeFromPlanId;
|
|
1251
|
+
checkpoint.currentPlanId = options.resumeFromPlanId;
|
|
1252
|
+
checkpoint.repairPlanIds = [...repairPlanIds];
|
|
1253
|
+
checkpoint.remediationAttemptsUsed = Math.max(0, options.resumeRemediationAttempts ?? 0);
|
|
1254
|
+
}
|
|
1255
|
+
const persistCheckpoint = (mutate) => {
|
|
1256
|
+
try {
|
|
1257
|
+
if (mutate)
|
|
1258
|
+
mutate(checkpoint);
|
|
1259
|
+
checkpoint.updatedAt = new Date().toISOString();
|
|
1260
|
+
saveShipCheckpoint(cwd, checkpoint);
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
// Checkpoint persistence is best-effort and must not break ship runs.
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
persistCheckpoint();
|
|
1267
|
+
const emitShipErrorAndExit = (input) => {
|
|
1268
|
+
const exitCode = input.exitCode ?? 1;
|
|
1269
|
+
const auditSnapshot = buildAuditSnapshot();
|
|
1270
|
+
persistCheckpoint((draft) => {
|
|
1271
|
+
draft.status = 'failed';
|
|
1272
|
+
draft.stage = 'error';
|
|
1273
|
+
draft.currentPlanId = input.finalPlanId || draft.currentPlanId;
|
|
1274
|
+
draft.resultStatus = 'ERROR';
|
|
1275
|
+
draft.audit = auditSnapshot;
|
|
1276
|
+
draft.error = {
|
|
1277
|
+
stage: input.stage,
|
|
1278
|
+
code: input.code,
|
|
1279
|
+
message: input.message,
|
|
1280
|
+
detail: input.detail,
|
|
1281
|
+
exitCode,
|
|
1282
|
+
};
|
|
1283
|
+
});
|
|
1284
|
+
if (options.json) {
|
|
1285
|
+
emitShipJson({
|
|
1286
|
+
success: false,
|
|
1287
|
+
status: 'ERROR',
|
|
1288
|
+
finalPlanId: input.finalPlanId ?? null,
|
|
1289
|
+
mergeConfidence: null,
|
|
1290
|
+
riskScore: null,
|
|
1291
|
+
artifacts: null,
|
|
1292
|
+
shareCard: null,
|
|
1293
|
+
error: {
|
|
1294
|
+
stage: input.stage,
|
|
1295
|
+
code: input.code,
|
|
1296
|
+
message: input.message,
|
|
1297
|
+
detail: input.detail,
|
|
1298
|
+
exitCode,
|
|
1299
|
+
},
|
|
1300
|
+
audit: auditSnapshot,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
process.exit(exitCode);
|
|
1304
|
+
};
|
|
647
1305
|
if (!goal || !goal.trim()) {
|
|
648
1306
|
console.error(chalk.red('❌ Error: goal cannot be empty.'));
|
|
649
1307
|
console.log(chalk.dim('Usage: neurcode ship "<goal>"'));
|
|
650
|
-
|
|
1308
|
+
emitShipErrorAndExit({
|
|
1309
|
+
stage: 'input',
|
|
1310
|
+
code: 'INVALID_GOAL',
|
|
1311
|
+
message: 'goal cannot be empty',
|
|
1312
|
+
exitCode: 1,
|
|
1313
|
+
});
|
|
651
1314
|
}
|
|
652
1315
|
const normalizedGoal = goal.trim();
|
|
1316
|
+
persistCheckpoint((draft) => {
|
|
1317
|
+
draft.goal = normalizedGoal;
|
|
1318
|
+
});
|
|
653
1319
|
const documentationOnlyGoal = isDocumentationOnlyGoal(normalizedGoal);
|
|
654
1320
|
const scopedGoal = documentationOnlyGoal
|
|
655
1321
|
? buildDocumentationOnlyIntent(normalizedGoal, false)
|
|
656
1322
|
: normalizedGoal;
|
|
657
|
-
|
|
1323
|
+
let baselineDirtyPaths = Array.isArray(options.resumeBaselineDirtyPaths)
|
|
1324
|
+
? [...options.resumeBaselineDirtyPaths]
|
|
1325
|
+
: [];
|
|
1326
|
+
try {
|
|
1327
|
+
if (baselineDirtyPaths.length === 0) {
|
|
1328
|
+
baselineDirtyPaths = ensureCleanTreeOrExit(cwd, options.allowDirty === true);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1333
|
+
const dirtyPaths = error.dirtyPaths ||
|
|
1334
|
+
(message.startsWith('WORKTREE_DIRTY:') ? message.replace('WORKTREE_DIRTY:', '').split(',').map((v) => v.trim()).filter(Boolean) : []);
|
|
1335
|
+
console.error(chalk.red('❌ Working tree is not clean.'));
|
|
1336
|
+
console.error(chalk.dim(' `neurcode ship` requires a clean tree so auto-remediation can safely revert scope drift.'));
|
|
1337
|
+
console.error(chalk.dim(' Commit/stash your changes or re-run with --allow-dirty if intentional.'));
|
|
1338
|
+
if (dirtyPaths.length > 0) {
|
|
1339
|
+
console.error(chalk.dim(` Dirty paths: ${dirtyPaths.slice(0, 5).join(', ')}`));
|
|
1340
|
+
}
|
|
1341
|
+
emitShipErrorAndExit({
|
|
1342
|
+
stage: 'bootstrap',
|
|
1343
|
+
code: 'WORKTREE_DIRTY',
|
|
1344
|
+
message: 'Working tree is not clean',
|
|
1345
|
+
detail: dirtyPaths.slice(0, 5).join(', '),
|
|
1346
|
+
exitCode: 1,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
persistCheckpoint((draft) => {
|
|
1350
|
+
draft.stage = 'bootstrap';
|
|
1351
|
+
draft.baselineDirtyPaths = [...baselineDirtyPaths];
|
|
1352
|
+
});
|
|
658
1353
|
const baselineDirtySet = new Set(baselineDirtyPaths.map((p) => p.replace(/\\/g, '/')));
|
|
659
1354
|
console.log(chalk.bold.cyan('\n🚀 Neurcode Ship\n'));
|
|
660
1355
|
console.log(chalk.dim(`Goal: ${normalizedGoal}`));
|
|
661
1356
|
console.log(chalk.dim(`Workspace: ${cwd}\n`));
|
|
1357
|
+
if (requirePass) {
|
|
1358
|
+
console.log(chalk.dim('ℹ️ strict governance: PASS verdict required (INFO will block this run).'));
|
|
1359
|
+
}
|
|
662
1360
|
if (baselineDirtyPaths.length > 0 && options.allowDirty) {
|
|
663
1361
|
console.log(chalk.dim(`ℹ️ allow-dirty: preserving ${baselineDirtyPaths.length} pre-existing dirty path(s) during verification.`));
|
|
664
1362
|
}
|
|
665
|
-
|
|
666
|
-
let
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
console.log(chalk.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1363
|
+
let initialPlanDurationMs = 0;
|
|
1364
|
+
let initialApplyDurationMs = 0;
|
|
1365
|
+
let remediationAttemptsUsed = Math.max(0, options.resumeRemediationAttempts ?? 0);
|
|
1366
|
+
let initialPlanId;
|
|
1367
|
+
let currentPlanId;
|
|
1368
|
+
if (options.resumeFromPlanId) {
|
|
1369
|
+
initialPlanId = options.resumeInitialPlanId || options.resumeFromPlanId;
|
|
1370
|
+
currentPlanId = options.resumeFromPlanId;
|
|
1371
|
+
console.log(chalk.dim('1/4 Resuming from existing ship checkpoint (skipping plan/apply)...'));
|
|
1372
|
+
auditSteps.push({
|
|
1373
|
+
stage: 'resume',
|
|
1374
|
+
attempt: 1,
|
|
1375
|
+
status: 'SUCCESS',
|
|
1376
|
+
startedAt: new Date().toISOString(),
|
|
1377
|
+
endedAt: new Date().toISOString(),
|
|
1378
|
+
durationMs: 0,
|
|
1379
|
+
planId: currentPlanId,
|
|
1380
|
+
message: `resume_from_plan=${currentPlanId}`,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
console.log(chalk.dim('1/4 Planning and applying initial implementation...'));
|
|
1385
|
+
let planningAttempt = 1;
|
|
1386
|
+
let initial = await runPlanAndApply(cwd, scopedGoal, options.projectId, {
|
|
1387
|
+
enforceDocumentationScope: documentationOnlyGoal,
|
|
1388
|
+
});
|
|
1389
|
+
recordRunStep(auditSteps, {
|
|
1390
|
+
stage: 'plan',
|
|
1391
|
+
attempt: planningAttempt,
|
|
1392
|
+
run: initial.planRun,
|
|
1393
|
+
message: 'initial',
|
|
1394
|
+
planId: initial.planId || undefined,
|
|
1395
|
+
});
|
|
1396
|
+
recordRunStep(auditSteps, {
|
|
1397
|
+
stage: 'apply',
|
|
1398
|
+
attempt: planningAttempt,
|
|
1399
|
+
run: initial.applyRun,
|
|
1400
|
+
message: 'initial',
|
|
1401
|
+
planId: initial.planId || undefined,
|
|
1402
|
+
});
|
|
1403
|
+
if (documentationOnlyGoal &&
|
|
1404
|
+
initial.applyRun &&
|
|
1405
|
+
initial.applyRun.code === 9 &&
|
|
1406
|
+
initial.applyRun.stderr.startsWith('DOC_SCOPE_VIOLATION')) {
|
|
1407
|
+
console.log(chalk.yellow('⚠️ Plan attempted non-documentation files. Retrying with strict README-only scope...'));
|
|
1408
|
+
remediationActions.push('documentation_scope_retry');
|
|
1409
|
+
const strictGoal = buildDocumentationOnlyIntent(normalizedGoal, true);
|
|
1410
|
+
planningAttempt += 1;
|
|
1411
|
+
initial = await runPlanAndApply(cwd, strictGoal, options.projectId, {
|
|
1412
|
+
enforceDocumentationScope: true,
|
|
1413
|
+
});
|
|
1414
|
+
recordRunStep(auditSteps, {
|
|
1415
|
+
stage: 'plan',
|
|
1416
|
+
attempt: planningAttempt,
|
|
1417
|
+
run: initial.planRun,
|
|
1418
|
+
message: 'documentation_scope_retry',
|
|
1419
|
+
planId: initial.planId || undefined,
|
|
1420
|
+
});
|
|
1421
|
+
recordRunStep(auditSteps, {
|
|
1422
|
+
stage: 'apply',
|
|
1423
|
+
attempt: planningAttempt,
|
|
1424
|
+
run: initial.applyRun,
|
|
1425
|
+
message: 'documentation_scope_retry',
|
|
1426
|
+
planId: initial.planId || undefined,
|
|
1427
|
+
});
|
|
684
1428
|
}
|
|
685
|
-
|
|
1429
|
+
if (!initial.planId || initial.planRun.code !== 0 || !initial.applyRun || initial.applyRun.code !== 0) {
|
|
1430
|
+
console.error(chalk.red('\n❌ Ship failed during initial plan/apply.'));
|
|
1431
|
+
const detail = initial.applyRun?.stderr || initial.planRun.stderr || initial.planRun.stdout;
|
|
1432
|
+
if (detail) {
|
|
1433
|
+
console.error(chalk.dim(` Details: ${shellTailLines(stripAnsi(detail), 8)}`));
|
|
1434
|
+
}
|
|
1435
|
+
const failedStage = !initial.planId || initial.planRun.code !== 0 ? 'plan' : 'apply';
|
|
1436
|
+
const exitCode = failedStage === 'plan'
|
|
1437
|
+
? initial.planRun.code || 1
|
|
1438
|
+
: initial.applyRun?.code || 1;
|
|
1439
|
+
emitShipErrorAndExit({
|
|
1440
|
+
stage: failedStage,
|
|
1441
|
+
code: failedStage === 'plan' ? 'PLAN_APPLY_INIT_FAILED_PLAN' : 'PLAN_APPLY_INIT_FAILED_APPLY',
|
|
1442
|
+
message: 'Ship failed during initial plan/apply',
|
|
1443
|
+
detail: detail ? shellTailLines(stripAnsi(detail), 12) : undefined,
|
|
1444
|
+
exitCode,
|
|
1445
|
+
finalPlanId: initial.planId,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
initialPlanId = initial.planId;
|
|
1449
|
+
currentPlanId = initialPlanId;
|
|
1450
|
+
initialPlanDurationMs = initial.planRun.durationMs;
|
|
1451
|
+
initialApplyDurationMs = initial.applyRun ? initial.applyRun.durationMs : 0;
|
|
686
1452
|
}
|
|
687
|
-
let currentPlanId = initial.planId;
|
|
688
1453
|
try {
|
|
689
1454
|
(0, state_1.setActivePlanId)(currentPlanId);
|
|
690
1455
|
(0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
|
|
@@ -692,9 +1457,15 @@ async function shipCommand(goal, options) {
|
|
|
692
1457
|
catch {
|
|
693
1458
|
// Non-critical state write.
|
|
694
1459
|
}
|
|
1460
|
+
persistCheckpoint((draft) => {
|
|
1461
|
+
draft.stage = 'planned';
|
|
1462
|
+
draft.initialPlanId = initialPlanId;
|
|
1463
|
+
draft.currentPlanId = currentPlanId;
|
|
1464
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1465
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1466
|
+
});
|
|
695
1467
|
let verifyTotalMs = 0;
|
|
696
1468
|
let testsTotalMs = 0;
|
|
697
|
-
let remediationAttemptsUsed = 0;
|
|
698
1469
|
let verifyPayload = null;
|
|
699
1470
|
let verifyExitCode = 1;
|
|
700
1471
|
let testsPassed = options.skipTests === true;
|
|
@@ -703,25 +1474,57 @@ async function shipCommand(goal, options) {
|
|
|
703
1474
|
let testCommand = inferTestCommand(cwd, options.testCommand);
|
|
704
1475
|
while (true) {
|
|
705
1476
|
console.log(chalk.dim('\n2/4 Running governance verification...'));
|
|
1477
|
+
const verifyAttempt = remediationAttemptsUsed + 1;
|
|
706
1478
|
const verifyArgs = ['verify', '--plan-id', currentPlanId, '--json'];
|
|
707
1479
|
if (recordVerify) {
|
|
708
1480
|
verifyArgs.push('--record');
|
|
709
1481
|
}
|
|
1482
|
+
if (requirePolicyLock) {
|
|
1483
|
+
verifyArgs.push('--require-policy-lock');
|
|
1484
|
+
}
|
|
1485
|
+
if (skipPolicyLock) {
|
|
1486
|
+
verifyArgs.push('--skip-policy-lock');
|
|
1487
|
+
}
|
|
710
1488
|
const verifyRun = await runCliCommand(cwd, verifyArgs, baselineDirtyPaths.length > 0
|
|
711
1489
|
? { NEURCODE_VERIFY_IGNORE_PATHS: JSON.stringify(baselineDirtyPaths) }
|
|
712
|
-
: undefined
|
|
1490
|
+
: undefined, {
|
|
1491
|
+
timeoutMs: getVerifyTimeoutMs(),
|
|
1492
|
+
label: 'ship:verify',
|
|
1493
|
+
});
|
|
713
1494
|
verifyTotalMs += verifyRun.durationMs;
|
|
714
1495
|
verifyExitCode = verifyRun.code;
|
|
1496
|
+
recordRunStep(auditSteps, {
|
|
1497
|
+
stage: 'verify',
|
|
1498
|
+
attempt: verifyAttempt,
|
|
1499
|
+
run: verifyRun,
|
|
1500
|
+
planId: currentPlanId,
|
|
1501
|
+
});
|
|
715
1502
|
const parsedVerify = parseVerifyPayload(`${verifyRun.stdout}\n${verifyRun.stderr}`);
|
|
716
1503
|
if (!parsedVerify) {
|
|
717
1504
|
console.error(chalk.red('\n❌ Could not parse verify JSON output.'));
|
|
718
|
-
|
|
1505
|
+
emitShipErrorAndExit({
|
|
1506
|
+
stage: 'verify',
|
|
1507
|
+
code: 'VERIFY_JSON_PARSE_FAILED',
|
|
1508
|
+
message: 'Could not parse verify JSON output',
|
|
1509
|
+
detail: shellTailLines(stripAnsi(`${verifyRun.stdout}\n${verifyRun.stderr}`), 12),
|
|
1510
|
+
exitCode: verifyRun.code === 0 ? 1 : verifyRun.code,
|
|
1511
|
+
finalPlanId: currentPlanId,
|
|
1512
|
+
});
|
|
719
1513
|
}
|
|
720
|
-
|
|
1514
|
+
const verifiedPayload = parsedVerify;
|
|
1515
|
+
verifyPayload = verifiedPayload;
|
|
1516
|
+
persistCheckpoint((draft) => {
|
|
1517
|
+
draft.stage = 'verify';
|
|
1518
|
+
draft.currentPlanId = currentPlanId;
|
|
1519
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1520
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1521
|
+
draft.verifyExitCode = verifyRun.code;
|
|
1522
|
+
draft.verifyPayload = verifiedPayload;
|
|
1523
|
+
});
|
|
721
1524
|
const verifyPassed = verifyRun.code === 0 &&
|
|
722
|
-
(
|
|
1525
|
+
(verifiedPayload.verdict === 'PASS' || (!requirePass && isInfoOnlyGovernanceResult(verifiedPayload)));
|
|
723
1526
|
if (verifyPassed) {
|
|
724
|
-
if (
|
|
1527
|
+
if (verifiedPayload.verdict === 'PASS') {
|
|
725
1528
|
console.log(chalk.green('✅ Governance verification passed.'));
|
|
726
1529
|
}
|
|
727
1530
|
else {
|
|
@@ -729,13 +1532,21 @@ async function shipCommand(goal, options) {
|
|
|
729
1532
|
}
|
|
730
1533
|
break;
|
|
731
1534
|
}
|
|
1535
|
+
if (verifyRun.code === 0 && requirePass && isInfoOnlyGovernanceResult(verifiedPayload)) {
|
|
1536
|
+
remediationActions.push('strict_pass_required_info_block');
|
|
1537
|
+
console.log(chalk.red('❌ Governance strict mode requires PASS verdict; verify returned INFO.'));
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
732
1540
|
if (remediationAttemptsUsed >= maxFixAttempts) {
|
|
733
1541
|
console.log(chalk.red(`❌ Verification still failing after ${remediationAttemptsUsed} remediation attempt(s).`));
|
|
734
1542
|
break;
|
|
735
1543
|
}
|
|
736
1544
|
remediationAttemptsUsed += 1;
|
|
737
1545
|
console.log(chalk.yellow(`⚠️ Auto-remediation attempt ${remediationAttemptsUsed}/${maxFixAttempts}`));
|
|
738
|
-
|
|
1546
|
+
persistCheckpoint((draft) => {
|
|
1547
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1548
|
+
});
|
|
1549
|
+
const scopeDriftFiles = verifiedPayload.violations
|
|
739
1550
|
.filter((v) => v.rule === 'scope_guard' && v.file)
|
|
740
1551
|
.map((v) => v.file)
|
|
741
1552
|
.filter((path) => !baselineDirtySet.has(path.replace(/\\/g, '/')));
|
|
@@ -744,7 +1555,7 @@ async function shipCommand(goal, options) {
|
|
|
744
1555
|
remediationActions.push(`restored_scope_files=${restored.join(',')}`);
|
|
745
1556
|
console.log(chalk.dim(` Restored ${restored.length} out-of-scope file(s) from HEAD.`));
|
|
746
1557
|
}
|
|
747
|
-
const policyFixes = applySimplePolicyFixes(cwd,
|
|
1558
|
+
const policyFixes = applySimplePolicyFixes(cwd, verifiedPayload.violations);
|
|
748
1559
|
if (policyFixes.length > 0) {
|
|
749
1560
|
remediationActions.push(`policy_cleanup_files=${policyFixes.join(',')}`);
|
|
750
1561
|
console.log(chalk.dim(` Applied simple policy cleanup to ${policyFixes.length} file(s).`));
|
|
@@ -753,8 +1564,22 @@ async function shipCommand(goal, options) {
|
|
|
753
1564
|
continue;
|
|
754
1565
|
}
|
|
755
1566
|
console.log(chalk.dim(' Falling back to constrained repair plan...'));
|
|
756
|
-
const repairIntent = buildVerifyRepairIntent(normalizedGoal, currentPlanId,
|
|
1567
|
+
const repairIntent = buildVerifyRepairIntent(normalizedGoal, currentPlanId, verifiedPayload, remediationAttemptsUsed);
|
|
757
1568
|
const repair = await runPlanAndApply(cwd, repairIntent, options.projectId);
|
|
1569
|
+
recordRunStep(auditSteps, {
|
|
1570
|
+
stage: 'plan',
|
|
1571
|
+
attempt: remediationAttemptsUsed + 1,
|
|
1572
|
+
run: repair.planRun,
|
|
1573
|
+
message: 'verify_repair',
|
|
1574
|
+
planId: repair.planId || undefined,
|
|
1575
|
+
});
|
|
1576
|
+
recordRunStep(auditSteps, {
|
|
1577
|
+
stage: 'apply',
|
|
1578
|
+
attempt: remediationAttemptsUsed + 1,
|
|
1579
|
+
run: repair.applyRun,
|
|
1580
|
+
message: 'verify_repair',
|
|
1581
|
+
planId: repair.planId || undefined,
|
|
1582
|
+
});
|
|
758
1583
|
if (!repair.planId || repair.planRun.code !== 0 || !repair.applyRun || repair.applyRun.code !== 0) {
|
|
759
1584
|
remediationActions.push('repair_plan_failed');
|
|
760
1585
|
console.log(chalk.red(' Repair plan/apply failed.'));
|
|
@@ -763,6 +1588,12 @@ async function shipCommand(goal, options) {
|
|
|
763
1588
|
currentPlanId = repair.planId;
|
|
764
1589
|
repairPlanIds.push(repair.planId);
|
|
765
1590
|
remediationActions.push(`repair_plan_applied=${repair.planId}`);
|
|
1591
|
+
persistCheckpoint((draft) => {
|
|
1592
|
+
draft.stage = 'planned';
|
|
1593
|
+
draft.currentPlanId = currentPlanId;
|
|
1594
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1595
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1596
|
+
});
|
|
766
1597
|
try {
|
|
767
1598
|
(0, state_1.setActivePlanId)(currentPlanId);
|
|
768
1599
|
(0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
|
|
@@ -773,23 +1604,46 @@ async function shipCommand(goal, options) {
|
|
|
773
1604
|
}
|
|
774
1605
|
if (!verifyPayload) {
|
|
775
1606
|
console.error(chalk.red('❌ Verification did not produce a valid payload.'));
|
|
776
|
-
|
|
1607
|
+
emitShipErrorAndExit({
|
|
1608
|
+
stage: 'verify',
|
|
1609
|
+
code: 'VERIFY_PAYLOAD_MISSING',
|
|
1610
|
+
message: 'Verification did not produce a valid payload',
|
|
1611
|
+
exitCode: 1,
|
|
1612
|
+
finalPlanId: currentPlanId,
|
|
1613
|
+
});
|
|
777
1614
|
}
|
|
1615
|
+
const finalVerifyPayload = verifyPayload;
|
|
778
1616
|
const verifyPassedFinal = verifyExitCode === 0 &&
|
|
779
|
-
(
|
|
1617
|
+
(finalVerifyPayload.verdict === 'PASS' || (!requirePass && isInfoOnlyGovernanceResult(finalVerifyPayload)));
|
|
780
1618
|
if (verifyPassedFinal && !options.skipTests) {
|
|
781
1619
|
testsAttempts += 1;
|
|
782
1620
|
if (!testCommand) {
|
|
783
1621
|
console.log(chalk.yellow('\n⚠️ No test command detected. Skipping tests.'));
|
|
784
1622
|
testsPassed = true;
|
|
785
1623
|
testsExitCode = 0;
|
|
1624
|
+
recordRunStep(auditSteps, {
|
|
1625
|
+
stage: 'tests',
|
|
1626
|
+
attempt: testsAttempts,
|
|
1627
|
+
run: null,
|
|
1628
|
+
message: 'no_test_command_detected',
|
|
1629
|
+
planId: currentPlanId,
|
|
1630
|
+
});
|
|
786
1631
|
}
|
|
787
1632
|
else {
|
|
788
1633
|
console.log(chalk.dim(`\n3/4 Running tests: ${testCommand}`));
|
|
789
|
-
const testRun = await runShellCommand(cwd, testCommand
|
|
1634
|
+
const testRun = await runShellCommand(cwd, testCommand, {
|
|
1635
|
+
timeoutMs: getTestTimeoutMs(),
|
|
1636
|
+
label: 'ship:tests',
|
|
1637
|
+
});
|
|
790
1638
|
testsTotalMs += testRun.durationMs;
|
|
791
1639
|
testsExitCode = testRun.code;
|
|
792
1640
|
testsPassed = testRun.code === 0;
|
|
1641
|
+
recordRunStep(auditSteps, {
|
|
1642
|
+
stage: 'tests',
|
|
1643
|
+
attempt: testsAttempts,
|
|
1644
|
+
run: testRun,
|
|
1645
|
+
planId: currentPlanId,
|
|
1646
|
+
});
|
|
793
1647
|
const testOutput = `${testRun.stdout}\n${testRun.stderr}`;
|
|
794
1648
|
if (!testsPassed && isNonRemediableTestFailure(testOutput)) {
|
|
795
1649
|
remediationActions.push('test_infra_failure');
|
|
@@ -797,13 +1651,36 @@ async function shipCommand(goal, options) {
|
|
|
797
1651
|
}
|
|
798
1652
|
else if (!testsPassed && remediationAttemptsUsed < maxFixAttempts) {
|
|
799
1653
|
remediationAttemptsUsed += 1;
|
|
1654
|
+
persistCheckpoint((draft) => {
|
|
1655
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1656
|
+
});
|
|
800
1657
|
console.log(chalk.yellow(`⚠️ Test failure auto-remediation attempt ${remediationAttemptsUsed}/${maxFixAttempts}`));
|
|
801
1658
|
const repairIntent = buildTestRepairIntent(normalizedGoal, currentPlanId, testOutput, remediationAttemptsUsed);
|
|
802
1659
|
const repair = await runPlanAndApply(cwd, repairIntent, options.projectId);
|
|
1660
|
+
recordRunStep(auditSteps, {
|
|
1661
|
+
stage: 'plan',
|
|
1662
|
+
attempt: remediationAttemptsUsed + 1,
|
|
1663
|
+
run: repair.planRun,
|
|
1664
|
+
message: 'test_repair',
|
|
1665
|
+
planId: repair.planId || undefined,
|
|
1666
|
+
});
|
|
1667
|
+
recordRunStep(auditSteps, {
|
|
1668
|
+
stage: 'apply',
|
|
1669
|
+
attempt: remediationAttemptsUsed + 1,
|
|
1670
|
+
run: repair.applyRun,
|
|
1671
|
+
message: 'test_repair',
|
|
1672
|
+
planId: repair.planId || undefined,
|
|
1673
|
+
});
|
|
803
1674
|
if (repair.planId && repair.planRun.code === 0 && repair.applyRun && repair.applyRun.code === 0) {
|
|
804
1675
|
currentPlanId = repair.planId;
|
|
805
1676
|
repairPlanIds.push(repair.planId);
|
|
806
1677
|
remediationActions.push(`test_repair_plan_applied=${repair.planId}`);
|
|
1678
|
+
persistCheckpoint((draft) => {
|
|
1679
|
+
draft.stage = 'planned';
|
|
1680
|
+
draft.currentPlanId = currentPlanId;
|
|
1681
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1682
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1683
|
+
});
|
|
807
1684
|
try {
|
|
808
1685
|
(0, state_1.setActivePlanId)(currentPlanId);
|
|
809
1686
|
(0, state_1.setLastPlanGeneratedAt)(new Date().toISOString());
|
|
@@ -811,20 +1688,56 @@ async function shipCommand(goal, options) {
|
|
|
811
1688
|
catch {
|
|
812
1689
|
// Non-critical state write.
|
|
813
1690
|
}
|
|
814
|
-
const verifyAfterTestRepair = await runCliCommand(cwd, [
|
|
1691
|
+
const verifyAfterTestRepair = await runCliCommand(cwd, [
|
|
1692
|
+
'verify',
|
|
1693
|
+
'--plan-id',
|
|
1694
|
+
currentPlanId,
|
|
1695
|
+
'--json',
|
|
1696
|
+
...(recordVerify ? ['--record'] : []),
|
|
1697
|
+
...(requirePolicyLock ? ['--require-policy-lock'] : []),
|
|
1698
|
+
...(skipPolicyLock ? ['--skip-policy-lock'] : []),
|
|
1699
|
+
], baselineDirtyPaths.length > 0
|
|
815
1700
|
? { NEURCODE_VERIFY_IGNORE_PATHS: JSON.stringify(baselineDirtyPaths) }
|
|
816
|
-
: undefined
|
|
1701
|
+
: undefined, {
|
|
1702
|
+
timeoutMs: getVerifyTimeoutMs(),
|
|
1703
|
+
label: 'ship:verify',
|
|
1704
|
+
});
|
|
817
1705
|
verifyTotalMs += verifyAfterTestRepair.durationMs;
|
|
1706
|
+
recordRunStep(auditSteps, {
|
|
1707
|
+
stage: 'verify',
|
|
1708
|
+
attempt: remediationAttemptsUsed + 1,
|
|
1709
|
+
run: verifyAfterTestRepair,
|
|
1710
|
+
message: 'post_test_repair',
|
|
1711
|
+
planId: currentPlanId,
|
|
1712
|
+
});
|
|
818
1713
|
const parsedAfterRepair = parseVerifyPayload(`${verifyAfterTestRepair.stdout}\n${verifyAfterTestRepair.stderr}`);
|
|
819
1714
|
if (parsedAfterRepair) {
|
|
820
1715
|
verifyPayload = parsedAfterRepair;
|
|
821
1716
|
verifyExitCode = verifyAfterTestRepair.code;
|
|
1717
|
+
persistCheckpoint((draft) => {
|
|
1718
|
+
draft.stage = 'verify';
|
|
1719
|
+
draft.currentPlanId = currentPlanId;
|
|
1720
|
+
draft.verifyExitCode = verifyExitCode;
|
|
1721
|
+
draft.verifyPayload = parsedAfterRepair;
|
|
1722
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1723
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1724
|
+
});
|
|
822
1725
|
}
|
|
823
1726
|
testsAttempts += 1;
|
|
824
|
-
const finalTestRun = await runShellCommand(cwd, testCommand
|
|
1727
|
+
const finalTestRun = await runShellCommand(cwd, testCommand, {
|
|
1728
|
+
timeoutMs: getTestTimeoutMs(),
|
|
1729
|
+
label: 'ship:tests',
|
|
1730
|
+
});
|
|
825
1731
|
testsTotalMs += finalTestRun.durationMs;
|
|
826
1732
|
testsExitCode = finalTestRun.code;
|
|
827
1733
|
testsPassed = finalTestRun.code === 0;
|
|
1734
|
+
recordRunStep(auditSteps, {
|
|
1735
|
+
stage: 'tests',
|
|
1736
|
+
attempt: testsAttempts,
|
|
1737
|
+
run: finalTestRun,
|
|
1738
|
+
message: 'post_test_repair',
|
|
1739
|
+
planId: currentPlanId,
|
|
1740
|
+
});
|
|
828
1741
|
}
|
|
829
1742
|
else {
|
|
830
1743
|
remediationActions.push('test_repair_plan_failed');
|
|
@@ -832,8 +1745,43 @@ async function shipCommand(goal, options) {
|
|
|
832
1745
|
}
|
|
833
1746
|
}
|
|
834
1747
|
}
|
|
1748
|
+
else if (options.skipTests === true) {
|
|
1749
|
+
recordRunStep(auditSteps, {
|
|
1750
|
+
stage: 'tests',
|
|
1751
|
+
attempt: 0,
|
|
1752
|
+
run: null,
|
|
1753
|
+
message: 'skipped_by_flag',
|
|
1754
|
+
planId: currentPlanId,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
recordRunStep(auditSteps, {
|
|
1759
|
+
stage: 'tests',
|
|
1760
|
+
attempt: 0,
|
|
1761
|
+
run: null,
|
|
1762
|
+
message: 'skipped_due_to_verify_failure',
|
|
1763
|
+
planId: currentPlanId,
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
persistCheckpoint((draft) => {
|
|
1767
|
+
draft.stage = 'tests';
|
|
1768
|
+
draft.currentPlanId = currentPlanId;
|
|
1769
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1770
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1771
|
+
draft.tests = {
|
|
1772
|
+
skipped: options.skipTests === true || !verifyPassedFinal,
|
|
1773
|
+
passed: testsPassed,
|
|
1774
|
+
exitCode: testsExitCode,
|
|
1775
|
+
attempts: testsAttempts,
|
|
1776
|
+
command: testCommand || null,
|
|
1777
|
+
};
|
|
1778
|
+
draft.verifyExitCode = verifyExitCode;
|
|
1779
|
+
draft.verifyPayload = verifyPayload;
|
|
1780
|
+
});
|
|
835
1781
|
const blast = collectBlastRadius(cwd);
|
|
836
1782
|
let simulatorSummary;
|
|
1783
|
+
const simulatorStartedMs = Date.now();
|
|
1784
|
+
const simulatorStartedAt = new Date(simulatorStartedMs).toISOString();
|
|
837
1785
|
try {
|
|
838
1786
|
const simulation = await (0, breakage_simulator_1.runBreakageSimulation)(cwd, {
|
|
839
1787
|
mode: 'working',
|
|
@@ -850,11 +1798,29 @@ async function shipCommand(goal, options) {
|
|
|
850
1798
|
confidence: item.confidence,
|
|
851
1799
|
})),
|
|
852
1800
|
};
|
|
1801
|
+
auditSteps.push({
|
|
1802
|
+
stage: 'simulate',
|
|
1803
|
+
attempt: 1,
|
|
1804
|
+
status: 'SUCCESS',
|
|
1805
|
+
startedAt: simulatorStartedAt,
|
|
1806
|
+
endedAt: new Date().toISOString(),
|
|
1807
|
+
durationMs: Date.now() - simulatorStartedMs,
|
|
1808
|
+
message: `impacted=${simulatorSummary.impactedFiles};predicted_regressions=${simulatorSummary.predictedRegressions}`,
|
|
1809
|
+
});
|
|
853
1810
|
}
|
|
854
1811
|
catch {
|
|
855
1812
|
simulatorSummary = undefined;
|
|
1813
|
+
auditSteps.push({
|
|
1814
|
+
stage: 'simulate',
|
|
1815
|
+
attempt: 1,
|
|
1816
|
+
status: 'FAILED',
|
|
1817
|
+
startedAt: simulatorStartedAt,
|
|
1818
|
+
endedAt: new Date().toISOString(),
|
|
1819
|
+
durationMs: Date.now() - simulatorStartedMs,
|
|
1820
|
+
message: 'simulation_failed',
|
|
1821
|
+
});
|
|
856
1822
|
}
|
|
857
|
-
const riskScore = computeRiskScore(
|
|
1823
|
+
const riskScore = computeRiskScore(finalVerifyPayload, blast, testsPassed, remediationAttemptsUsed);
|
|
858
1824
|
const riskLabel = classifyRiskLabel(riskScore);
|
|
859
1825
|
const status = verifyPassedFinal && testsPassed ? 'READY_TO_MERGE' : 'BLOCKED';
|
|
860
1826
|
const branch = (() => {
|
|
@@ -875,11 +1841,11 @@ async function shipCommand(goal, options) {
|
|
|
875
1841
|
headSha,
|
|
876
1842
|
},
|
|
877
1843
|
plans: {
|
|
878
|
-
initialPlanId
|
|
1844
|
+
initialPlanId,
|
|
879
1845
|
finalPlanId: currentPlanId,
|
|
880
1846
|
repairPlanIds,
|
|
881
1847
|
},
|
|
882
|
-
verification:
|
|
1848
|
+
verification: finalVerifyPayload,
|
|
883
1849
|
tests: {
|
|
884
1850
|
skipped: options.skipTests === true,
|
|
885
1851
|
command: testCommand || undefined,
|
|
@@ -900,16 +1866,34 @@ async function shipCommand(goal, options) {
|
|
|
900
1866
|
},
|
|
901
1867
|
simulator: simulatorSummary,
|
|
902
1868
|
timingsMs: {
|
|
903
|
-
initialPlan:
|
|
904
|
-
initialApply:
|
|
1869
|
+
initialPlan: initialPlanDurationMs,
|
|
1870
|
+
initialApply: initialApplyDurationMs,
|
|
905
1871
|
verifyTotal: verifyTotalMs,
|
|
906
1872
|
testsTotal: testsTotalMs,
|
|
907
1873
|
total: Date.now() - startedAt,
|
|
908
1874
|
},
|
|
1875
|
+
audit: buildAuditSnapshot(),
|
|
909
1876
|
};
|
|
910
|
-
|
|
1877
|
+
persistCheckpoint((draft) => {
|
|
1878
|
+
draft.stage = 'finalize';
|
|
1879
|
+
draft.currentPlanId = currentPlanId;
|
|
1880
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1881
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1882
|
+
draft.verifyExitCode = verifyExitCode;
|
|
1883
|
+
draft.verifyPayload = finalVerifyPayload;
|
|
1884
|
+
draft.tests = {
|
|
1885
|
+
skipped: options.skipTests === true || !verifyPassedFinal,
|
|
1886
|
+
passed: testsPassed,
|
|
1887
|
+
exitCode: testsExitCode,
|
|
1888
|
+
attempts: testsAttempts,
|
|
1889
|
+
command: testCommand || null,
|
|
1890
|
+
};
|
|
1891
|
+
draft.resultStatus = status;
|
|
1892
|
+
});
|
|
911
1893
|
let publishedCard = null;
|
|
912
1894
|
if (options.publishCard !== false) {
|
|
1895
|
+
const publishStartedMs = Date.now();
|
|
1896
|
+
const publishStartedAt = new Date(publishStartedMs).toISOString();
|
|
913
1897
|
try {
|
|
914
1898
|
const config = (0, config_1.loadConfig)();
|
|
915
1899
|
if (config.apiKey) {
|
|
@@ -935,19 +1919,85 @@ async function shipCommand(goal, options) {
|
|
|
935
1919
|
card: sanitizeCardForCloud(card),
|
|
936
1920
|
});
|
|
937
1921
|
publishedCard = publishResponse;
|
|
1922
|
+
auditSteps.push({
|
|
1923
|
+
stage: 'publish_card',
|
|
1924
|
+
attempt: 1,
|
|
1925
|
+
status: 'SUCCESS',
|
|
1926
|
+
startedAt: publishStartedAt,
|
|
1927
|
+
endedAt: new Date().toISOString(),
|
|
1928
|
+
durationMs: Date.now() - publishStartedMs,
|
|
1929
|
+
message: `card_id=${publishResponse.id}`,
|
|
1930
|
+
});
|
|
938
1931
|
}
|
|
939
1932
|
else {
|
|
940
1933
|
console.log(chalk.dim('ℹ️ Merge card publish skipped (no API key found in current org scope).'));
|
|
1934
|
+
auditSteps.push({
|
|
1935
|
+
stage: 'publish_card',
|
|
1936
|
+
attempt: 1,
|
|
1937
|
+
status: 'SKIPPED',
|
|
1938
|
+
startedAt: publishStartedAt,
|
|
1939
|
+
endedAt: new Date().toISOString(),
|
|
1940
|
+
durationMs: Date.now() - publishStartedMs,
|
|
1941
|
+
message: 'missing_api_key',
|
|
1942
|
+
});
|
|
941
1943
|
}
|
|
942
1944
|
}
|
|
943
1945
|
catch (error) {
|
|
944
1946
|
const message = error instanceof Error ? error.message : String(error);
|
|
945
1947
|
console.log(chalk.yellow(`⚠️ Merge card publish failed (non-blocking): ${message}`));
|
|
1948
|
+
auditSteps.push({
|
|
1949
|
+
stage: 'publish_card',
|
|
1950
|
+
attempt: 1,
|
|
1951
|
+
status: 'FAILED',
|
|
1952
|
+
startedAt: publishStartedAt,
|
|
1953
|
+
endedAt: new Date().toISOString(),
|
|
1954
|
+
durationMs: Date.now() - publishStartedMs,
|
|
1955
|
+
message,
|
|
1956
|
+
});
|
|
946
1957
|
}
|
|
947
1958
|
}
|
|
1959
|
+
else {
|
|
1960
|
+
auditSteps.push({
|
|
1961
|
+
stage: 'publish_card',
|
|
1962
|
+
attempt: 1,
|
|
1963
|
+
status: 'SKIPPED',
|
|
1964
|
+
startedAt: new Date().toISOString(),
|
|
1965
|
+
endedAt: new Date().toISOString(),
|
|
1966
|
+
durationMs: 0,
|
|
1967
|
+
message: 'publish_disabled',
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
card.audit = buildAuditSnapshot();
|
|
1971
|
+
const artifactPaths = writeMergeConfidenceArtifacts(cwd, card);
|
|
1972
|
+
artifactPaths.attestationPath = writeReleaseAttestation(cwd, card, artifactPaths);
|
|
1973
|
+
persistCheckpoint((draft) => {
|
|
1974
|
+
draft.status = 'completed';
|
|
1975
|
+
draft.stage = 'completed';
|
|
1976
|
+
draft.initialPlanId = initialPlanId;
|
|
1977
|
+
draft.currentPlanId = currentPlanId;
|
|
1978
|
+
draft.repairPlanIds = [...repairPlanIds];
|
|
1979
|
+
draft.remediationAttemptsUsed = remediationAttemptsUsed;
|
|
1980
|
+
draft.verifyExitCode = verifyExitCode;
|
|
1981
|
+
draft.verifyPayload = finalVerifyPayload;
|
|
1982
|
+
draft.tests = {
|
|
1983
|
+
skipped: options.skipTests === true || !verifyPassedFinal,
|
|
1984
|
+
passed: testsPassed,
|
|
1985
|
+
exitCode: testsExitCode,
|
|
1986
|
+
attempts: testsAttempts,
|
|
1987
|
+
command: testCommand || null,
|
|
1988
|
+
};
|
|
1989
|
+
draft.resultStatus = card.status;
|
|
1990
|
+
draft.artifacts = artifactPaths;
|
|
1991
|
+
draft.shareCard = publishedCard;
|
|
1992
|
+
draft.audit = card.audit;
|
|
1993
|
+
draft.error = null;
|
|
1994
|
+
});
|
|
948
1995
|
console.log(chalk.dim('\n4/4 Merge Confidence Card generated.'));
|
|
949
1996
|
console.log(chalk.dim(` JSON: ${artifactPaths.jsonPath}`));
|
|
950
1997
|
console.log(chalk.dim(` Markdown: ${artifactPaths.markdownPath}`));
|
|
1998
|
+
if (artifactPaths.attestationPath) {
|
|
1999
|
+
console.log(chalk.dim(` Attestation: ${artifactPaths.attestationPath}`));
|
|
2000
|
+
}
|
|
951
2001
|
if (publishedCard?.shareUrl) {
|
|
952
2002
|
console.log(chalk.dim(` Share URL: ${publishedCard.shareUrl}`));
|
|
953
2003
|
}
|
|
@@ -958,14 +2008,15 @@ async function shipCommand(goal, options) {
|
|
|
958
2008
|
}
|
|
959
2009
|
else {
|
|
960
2010
|
console.log(chalk.bold.red('❌ Blocked'));
|
|
961
|
-
console.log(chalk.red(` Verdict: ${
|
|
2011
|
+
console.log(chalk.red(` Verdict: ${finalVerifyPayload.verdict} | Tests: ${testsPassed ? 'PASS' : 'FAIL'}`));
|
|
962
2012
|
console.log(chalk.red(` Confidence: ${card.risk.mergeConfidence}/100 | Risk: ${card.risk.label}`));
|
|
963
2013
|
}
|
|
964
2014
|
if (card.simulator) {
|
|
965
2015
|
console.log(chalk.dim(` Simulator: impacted=${card.simulator.impactedFiles}, predicted_regressions=${card.simulator.predictedRegressions}`));
|
|
966
2016
|
}
|
|
967
2017
|
if (options.json) {
|
|
968
|
-
|
|
2018
|
+
emitShipJson({
|
|
2019
|
+
success: card.status === 'READY_TO_MERGE',
|
|
969
2020
|
status: card.status,
|
|
970
2021
|
finalPlanId: card.plans.finalPlanId,
|
|
971
2022
|
mergeConfidence: card.risk.mergeConfidence,
|
|
@@ -983,8 +2034,286 @@ async function shipCommand(goal, options) {
|
|
|
983
2034
|
simulator: card.simulator,
|
|
984
2035
|
artifacts: artifactPaths,
|
|
985
2036
|
shareCard: publishedCard,
|
|
986
|
-
|
|
2037
|
+
audit: card.audit,
|
|
2038
|
+
});
|
|
987
2039
|
}
|
|
988
2040
|
process.exit(status === 'READY_TO_MERGE' ? 0 : 2);
|
|
989
2041
|
}
|
|
2042
|
+
function emitResumeErrorJson(input) {
|
|
2043
|
+
const now = new Date().toISOString();
|
|
2044
|
+
const exitCode = input.exitCode ?? 1;
|
|
2045
|
+
emitShipJson({
|
|
2046
|
+
success: false,
|
|
2047
|
+
status: 'ERROR',
|
|
2048
|
+
finalPlanId: null,
|
|
2049
|
+
mergeConfidence: null,
|
|
2050
|
+
riskScore: null,
|
|
2051
|
+
artifacts: null,
|
|
2052
|
+
shareCard: null,
|
|
2053
|
+
error: {
|
|
2054
|
+
stage: 'resume',
|
|
2055
|
+
code: input.code,
|
|
2056
|
+
message: input.message,
|
|
2057
|
+
detail: input.detail,
|
|
2058
|
+
exitCode,
|
|
2059
|
+
},
|
|
2060
|
+
audit: {
|
|
2061
|
+
runId: input.runId,
|
|
2062
|
+
startedAt: now,
|
|
2063
|
+
finishedAt: now,
|
|
2064
|
+
durationMs: 0,
|
|
2065
|
+
timeoutMs: {
|
|
2066
|
+
plan: getPlanTimeoutMs(),
|
|
2067
|
+
apply: getApplyTimeoutMs(),
|
|
2068
|
+
verify: getVerifyTimeoutMs(),
|
|
2069
|
+
tests: getTestTimeoutMs(),
|
|
2070
|
+
},
|
|
2071
|
+
heartbeatMs: getHeartbeatIntervalMs(),
|
|
2072
|
+
steps: [],
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
async function shipResumeCommand(runId, options) {
|
|
2077
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
2078
|
+
const normalizedRunId = String(runId || '').trim();
|
|
2079
|
+
if (!normalizedRunId) {
|
|
2080
|
+
if (options.json) {
|
|
2081
|
+
emitResumeErrorJson({
|
|
2082
|
+
runId: 'unknown',
|
|
2083
|
+
code: 'RUN_ID_REQUIRED',
|
|
2084
|
+
message: 'runId is required',
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
else {
|
|
2088
|
+
console.error(chalk.red('❌ ship-resume requires a run ID.'));
|
|
2089
|
+
console.log(chalk.dim('Usage: neurcode ship-resume <run-id>'));
|
|
2090
|
+
}
|
|
2091
|
+
process.exit(1);
|
|
2092
|
+
}
|
|
2093
|
+
const checkpoint = loadShipCheckpoint(cwd, normalizedRunId);
|
|
2094
|
+
if (!checkpoint) {
|
|
2095
|
+
if (options.json) {
|
|
2096
|
+
emitResumeErrorJson({
|
|
2097
|
+
runId: normalizedRunId,
|
|
2098
|
+
code: 'CHECKPOINT_NOT_FOUND',
|
|
2099
|
+
message: `No ship checkpoint found for run ${normalizedRunId}`,
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
else {
|
|
2103
|
+
console.error(chalk.red(`❌ No ship checkpoint found for run ${normalizedRunId}.`));
|
|
2104
|
+
console.log(chalk.dim('Run `neurcode ship-runs` to list resumable runs.'));
|
|
2105
|
+
}
|
|
2106
|
+
process.exit(1);
|
|
2107
|
+
}
|
|
2108
|
+
if (checkpoint.status === 'completed') {
|
|
2109
|
+
const completedStatus = checkpoint.resultStatus || 'BLOCKED';
|
|
2110
|
+
if (options.json) {
|
|
2111
|
+
emitShipJson({
|
|
2112
|
+
success: completedStatus === 'READY_TO_MERGE',
|
|
2113
|
+
status: completedStatus,
|
|
2114
|
+
finalPlanId: checkpoint.currentPlanId,
|
|
2115
|
+
mergeConfidence: null,
|
|
2116
|
+
riskScore: null,
|
|
2117
|
+
verification: checkpoint.verifyPayload
|
|
2118
|
+
? {
|
|
2119
|
+
verdict: checkpoint.verifyPayload.verdict,
|
|
2120
|
+
grade: checkpoint.verifyPayload.grade,
|
|
2121
|
+
score: checkpoint.verifyPayload.adherenceScore ?? checkpoint.verifyPayload.score,
|
|
2122
|
+
violations: checkpoint.verifyPayload.violations.length,
|
|
2123
|
+
}
|
|
2124
|
+
: undefined,
|
|
2125
|
+
tests: {
|
|
2126
|
+
skipped: checkpoint.tests.skipped,
|
|
2127
|
+
passed: checkpoint.tests.passed,
|
|
2128
|
+
},
|
|
2129
|
+
artifacts: checkpoint.artifacts,
|
|
2130
|
+
shareCard: checkpoint.shareCard,
|
|
2131
|
+
audit: checkpoint.audit || {
|
|
2132
|
+
runId: checkpoint.runId,
|
|
2133
|
+
startedAt: checkpoint.startedAt,
|
|
2134
|
+
finishedAt: checkpoint.updatedAt,
|
|
2135
|
+
durationMs: Math.max(0, Date.parse(checkpoint.updatedAt) - Date.parse(checkpoint.startedAt)),
|
|
2136
|
+
timeoutMs: {
|
|
2137
|
+
plan: getPlanTimeoutMs(),
|
|
2138
|
+
apply: getApplyTimeoutMs(),
|
|
2139
|
+
verify: getVerifyTimeoutMs(),
|
|
2140
|
+
tests: getTestTimeoutMs(),
|
|
2141
|
+
},
|
|
2142
|
+
heartbeatMs: getHeartbeatIntervalMs(),
|
|
2143
|
+
steps: [],
|
|
2144
|
+
},
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
else {
|
|
2148
|
+
console.log(chalk.yellow(`ℹ️ Run ${normalizedRunId} is already completed (${completedStatus}).`));
|
|
2149
|
+
if (checkpoint.artifacts) {
|
|
2150
|
+
console.log(chalk.dim(` JSON: ${checkpoint.artifacts.jsonPath}`));
|
|
2151
|
+
console.log(chalk.dim(` Markdown: ${checkpoint.artifacts.markdownPath}`));
|
|
2152
|
+
if (checkpoint.artifacts.attestationPath) {
|
|
2153
|
+
console.log(chalk.dim(` Attestation: ${checkpoint.artifacts.attestationPath}`));
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
process.exit(completedStatus === 'READY_TO_MERGE' ? 0 : completedStatus === 'BLOCKED' ? 2 : 1);
|
|
2158
|
+
}
|
|
2159
|
+
if (!checkpoint.currentPlanId) {
|
|
2160
|
+
if (options.json) {
|
|
2161
|
+
emitResumeErrorJson({
|
|
2162
|
+
runId: normalizedRunId,
|
|
2163
|
+
code: 'CHECKPOINT_MISSING_PLAN',
|
|
2164
|
+
message: `Run ${normalizedRunId} has no resumable plan`,
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
console.error(chalk.red(`❌ Run ${normalizedRunId} has no resumable plan checkpoint.`));
|
|
2169
|
+
console.log(chalk.dim('Start a new run with `neurcode ship "<goal>"`.'));
|
|
2170
|
+
}
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
}
|
|
2173
|
+
await shipCommand(checkpoint.goal, {
|
|
2174
|
+
projectId: options.projectId || checkpoint.options.projectId || undefined,
|
|
2175
|
+
maxFixAttempts: Number.isFinite(options.maxFixAttempts)
|
|
2176
|
+
? options.maxFixAttempts
|
|
2177
|
+
: checkpoint.options.maxFixAttempts,
|
|
2178
|
+
allowDirty: true,
|
|
2179
|
+
skipTests: options.skipTests ?? checkpoint.options.skipTests,
|
|
2180
|
+
testCommand: options.testCommand || checkpoint.options.testCommand || undefined,
|
|
2181
|
+
record: options.record ?? checkpoint.options.record,
|
|
2182
|
+
requirePass: options.requirePass ?? checkpoint.options.requirePass,
|
|
2183
|
+
requirePolicyLock: options.requirePolicyLock ?? checkpoint.options.requirePolicyLock,
|
|
2184
|
+
skipPolicyLock: options.skipPolicyLock ?? checkpoint.options.skipPolicyLock,
|
|
2185
|
+
publishCard: options.publishCard ?? checkpoint.options.publishCard,
|
|
2186
|
+
json: options.json === true,
|
|
2187
|
+
resumeRunId: checkpoint.runId,
|
|
2188
|
+
resumeFromPlanId: checkpoint.currentPlanId,
|
|
2189
|
+
resumeInitialPlanId: checkpoint.initialPlanId || checkpoint.currentPlanId,
|
|
2190
|
+
resumeRepairPlanIds: checkpoint.repairPlanIds,
|
|
2191
|
+
resumeRemediationAttempts: checkpoint.remediationAttemptsUsed,
|
|
2192
|
+
resumeBaselineDirtyPaths: checkpoint.baselineDirtyPaths,
|
|
2193
|
+
resumeStartedAtIso: checkpoint.startedAt,
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
function shipRunsCommand(options) {
|
|
2197
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
2198
|
+
const limitRaw = Number.isFinite(options.limit) ? Number(options.limit) : 20;
|
|
2199
|
+
const limit = Math.max(1, Math.min(200, Math.floor(limitRaw)));
|
|
2200
|
+
const runs = listShipRunSummaries(cwd).slice(0, limit);
|
|
2201
|
+
if (options.json) {
|
|
2202
|
+
console.log(JSON.stringify({ runs }, null, 2));
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (runs.length === 0) {
|
|
2206
|
+
console.log(chalk.yellow('\n⚠️ No ship runs found for this repository.\n'));
|
|
2207
|
+
console.log(chalk.dim('Start one with: neurcode ship "<goal>"\n'));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
console.log(chalk.bold('\n🧭 Ship Runs\n'));
|
|
2211
|
+
for (const run of runs) {
|
|
2212
|
+
console.log(chalk.cyan(`• ${run.runId}`));
|
|
2213
|
+
console.log(chalk.dim(` status=${run.status} stage=${run.stage} result=${run.resultStatus || 'n/a'}`));
|
|
2214
|
+
if (run.currentPlanId) {
|
|
2215
|
+
console.log(chalk.dim(` plan=${run.currentPlanId}`));
|
|
2216
|
+
}
|
|
2217
|
+
console.log(chalk.dim(` updated=${run.updatedAt}`));
|
|
2218
|
+
console.log(chalk.dim(` goal=${run.goal}`));
|
|
2219
|
+
console.log('');
|
|
2220
|
+
}
|
|
2221
|
+
console.log(chalk.dim('Resume a run with: neurcode ship-resume <run-id>\n'));
|
|
2222
|
+
}
|
|
2223
|
+
function shipAttestationVerifyCommand(attestationPathInput, options) {
|
|
2224
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
2225
|
+
const attestationPath = (0, path_1.resolve)(cwd, String(attestationPathInput || '').trim());
|
|
2226
|
+
if (!(0, fs_1.existsSync)(attestationPath)) {
|
|
2227
|
+
const message = `Attestation file not found: ${attestationPath}`;
|
|
2228
|
+
if (options.json) {
|
|
2229
|
+
console.log(JSON.stringify({ pass: false, message }, null, 2));
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
2233
|
+
}
|
|
2234
|
+
process.exit(1);
|
|
2235
|
+
}
|
|
2236
|
+
let payload;
|
|
2237
|
+
try {
|
|
2238
|
+
payload = JSON.parse((0, fs_1.readFileSync)(attestationPath, 'utf-8'));
|
|
2239
|
+
}
|
|
2240
|
+
catch (error) {
|
|
2241
|
+
const message = `Failed to parse attestation JSON: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
2242
|
+
if (options.json) {
|
|
2243
|
+
console.log(JSON.stringify({ pass: false, message }, null, 2));
|
|
2244
|
+
}
|
|
2245
|
+
else {
|
|
2246
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
2247
|
+
}
|
|
2248
|
+
process.exit(1);
|
|
2249
|
+
}
|
|
2250
|
+
const cardPathRaw = payload?.card?.jsonPath;
|
|
2251
|
+
const expectedSha = payload?.card?.sha256;
|
|
2252
|
+
const signature = payload?.signature;
|
|
2253
|
+
const cardPath = typeof cardPathRaw === 'string' ? (0, path_1.resolve)(cwd, cardPathRaw) : '';
|
|
2254
|
+
const cardExists = cardPath ? (0, fs_1.existsSync)(cardPath) : false;
|
|
2255
|
+
const actualSha = cardExists && typeof expectedSha === 'string'
|
|
2256
|
+
? sha256Hex((0, fs_1.readFileSync)(cardPath, 'utf-8'))
|
|
2257
|
+
: null;
|
|
2258
|
+
const digestMatched = typeof expectedSha === 'string' && actualSha === expectedSha;
|
|
2259
|
+
const hmacKey = options.hmacKey || process.env.NEURCODE_ATTEST_HMAC_KEY;
|
|
2260
|
+
let signatureVerified = null;
|
|
2261
|
+
let signatureMessage = null;
|
|
2262
|
+
if (signature && typeof signature === 'object' && typeof signature.value === 'string') {
|
|
2263
|
+
if (!hmacKey) {
|
|
2264
|
+
signatureVerified = false;
|
|
2265
|
+
signatureMessage = 'signature present but no HMAC key provided';
|
|
2266
|
+
}
|
|
2267
|
+
else {
|
|
2268
|
+
const basePayload = { ...payload };
|
|
2269
|
+
delete basePayload.signature;
|
|
2270
|
+
const expectedSignature = (0, crypto_1.createHmac)('sha256', hmacKey)
|
|
2271
|
+
.update(JSON.stringify(basePayload), 'utf-8')
|
|
2272
|
+
.digest('hex');
|
|
2273
|
+
signatureVerified = expectedSignature === signature.value;
|
|
2274
|
+
signatureMessage = signatureVerified ? null : 'signature mismatch';
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const pass = digestMatched && (signatureVerified === null || signatureVerified === true);
|
|
2278
|
+
const response = {
|
|
2279
|
+
pass,
|
|
2280
|
+
attestationPath,
|
|
2281
|
+
cardPath,
|
|
2282
|
+
cardExists,
|
|
2283
|
+
digest: {
|
|
2284
|
+
expected: typeof expectedSha === 'string' ? expectedSha : null,
|
|
2285
|
+
actual: actualSha,
|
|
2286
|
+
matched: digestMatched,
|
|
2287
|
+
},
|
|
2288
|
+
signature: {
|
|
2289
|
+
present: Boolean(signature && typeof signature === 'object' && typeof signature.value === 'string'),
|
|
2290
|
+
verified: signatureVerified,
|
|
2291
|
+
keyId: signature && typeof signature === 'object' && typeof signature.keyId === 'string' ? signature.keyId : null,
|
|
2292
|
+
message: signatureMessage,
|
|
2293
|
+
},
|
|
2294
|
+
};
|
|
2295
|
+
if (options.json) {
|
|
2296
|
+
console.log(JSON.stringify(response, null, 2));
|
|
2297
|
+
}
|
|
2298
|
+
else if (pass) {
|
|
2299
|
+
console.log(chalk.green('\n✅ Attestation verified.\n'));
|
|
2300
|
+
console.log(chalk.dim(`Attestation: ${attestationPath}`));
|
|
2301
|
+
console.log(chalk.dim(`Card: ${cardPath}`));
|
|
2302
|
+
console.log(chalk.dim(`Digest: ${response.digest.actual}\n`));
|
|
2303
|
+
}
|
|
2304
|
+
else {
|
|
2305
|
+
console.log(chalk.red('\n❌ Attestation verification failed.\n'));
|
|
2306
|
+
if (!cardExists) {
|
|
2307
|
+
console.log(chalk.red(`- Card file missing: ${cardPath}`));
|
|
2308
|
+
}
|
|
2309
|
+
if (!digestMatched) {
|
|
2310
|
+
console.log(chalk.red(`- Digest mismatch (expected=${response.digest.expected}, actual=${response.digest.actual})`));
|
|
2311
|
+
}
|
|
2312
|
+
if (response.signature.verified === false && response.signature.message) {
|
|
2313
|
+
console.log(chalk.red(`- Signature: ${response.signature.message}`));
|
|
2314
|
+
}
|
|
2315
|
+
console.log('');
|
|
2316
|
+
}
|
|
2317
|
+
process.exit(pass ? 0 : 1);
|
|
2318
|
+
}
|
|
990
2319
|
//# sourceMappingURL=ship.js.map
|