@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.
Files changed (59) hide show
  1. package/dist/commands/allow.d.ts.map +1 -1
  2. package/dist/commands/allow.js +5 -19
  3. package/dist/commands/allow.js.map +1 -1
  4. package/dist/commands/apply.d.ts +1 -0
  5. package/dist/commands/apply.d.ts.map +1 -1
  6. package/dist/commands/apply.js +105 -46
  7. package/dist/commands/apply.js.map +1 -1
  8. package/dist/commands/ask.d.ts.map +1 -1
  9. package/dist/commands/ask.js +1849 -1783
  10. package/dist/commands/ask.js.map +1 -1
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +83 -24
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/plan.d.ts +4 -0
  16. package/dist/commands/plan.d.ts.map +1 -1
  17. package/dist/commands/plan.js +344 -48
  18. package/dist/commands/plan.js.map +1 -1
  19. package/dist/commands/policy.d.ts.map +1 -1
  20. package/dist/commands/policy.js +629 -0
  21. package/dist/commands/policy.js.map +1 -1
  22. package/dist/commands/prompt.d.ts +7 -1
  23. package/dist/commands/prompt.d.ts.map +1 -1
  24. package/dist/commands/prompt.js +106 -25
  25. package/dist/commands/prompt.js.map +1 -1
  26. package/dist/commands/ship.d.ts +32 -0
  27. package/dist/commands/ship.d.ts.map +1 -1
  28. package/dist/commands/ship.js +1404 -75
  29. package/dist/commands/ship.js.map +1 -1
  30. package/dist/commands/verify.d.ts +6 -0
  31. package/dist/commands/verify.d.ts.map +1 -1
  32. package/dist/commands/verify.js +527 -102
  33. package/dist/commands/verify.js.map +1 -1
  34. package/dist/index.js +89 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/utils/custom-policy-rules.d.ts +21 -0
  37. package/dist/utils/custom-policy-rules.d.ts.map +1 -0
  38. package/dist/utils/custom-policy-rules.js +71 -0
  39. package/dist/utils/custom-policy-rules.js.map +1 -0
  40. package/dist/utils/plan-cache.d.ts.map +1 -1
  41. package/dist/utils/plan-cache.js +4 -0
  42. package/dist/utils/plan-cache.js.map +1 -1
  43. package/dist/utils/policy-audit.d.ts +29 -0
  44. package/dist/utils/policy-audit.d.ts.map +1 -0
  45. package/dist/utils/policy-audit.js +208 -0
  46. package/dist/utils/policy-audit.js.map +1 -0
  47. package/dist/utils/policy-exceptions.d.ts +96 -0
  48. package/dist/utils/policy-exceptions.d.ts.map +1 -0
  49. package/dist/utils/policy-exceptions.js +389 -0
  50. package/dist/utils/policy-exceptions.js.map +1 -0
  51. package/dist/utils/policy-governance.d.ts +24 -0
  52. package/dist/utils/policy-governance.d.ts.map +1 -0
  53. package/dist/utils/policy-governance.js +124 -0
  54. package/dist/utils/policy-governance.js.map +1 -0
  55. package/dist/utils/policy-packs.d.ts +72 -1
  56. package/dist/utils/policy-packs.d.ts.map +1 -1
  57. package/dist/utils/policy-packs.js +285 -0
  58. package/dist/utils/policy-packs.js.map +1 -1
  59. package/package.json +1 -1
@@ -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
- resolvePromise({
72
- code: code ?? 1,
73
- stdout,
74
- stderr,
75
- durationMs: Date.now() - startedAt,
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
- resolvePromise({
105
- code: code ?? 1,
106
- stdout,
107
- stderr,
108
- durationMs: Date.now() - startedAt,
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
- console.error(chalk.red('❌ Working tree is not clean.'));
242
- console.error(chalk.dim(' `neurcode ship` requires a clean tree so auto-remediation can safely revert scope drift.'));
243
- console.error(chalk.dim(' Commit/stash your changes or re-run with --allow-dirty if intentional.'));
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 planId = extractPlanId(planOutput);
616
- if (planRun.code !== 0 || !planId) {
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
- const writtenFiles = collectApplyWrittenFiles(`${applyRun.stdout}\n${applyRun.stderr}`);
638
- return { planId, planRun, applyRun, writtenFiles };
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 startedAt = Date.now();
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
- process.exit(1);
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
- const baselineDirtyPaths = ensureCleanTreeOrExit(cwd, options.allowDirty === true);
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
- console.log(chalk.dim('1/4 Planning and applying initial implementation...'));
666
- let initial = await runPlanAndApply(cwd, scopedGoal, options.projectId, {
667
- enforceDocumentationScope: documentationOnlyGoal,
668
- });
669
- if (documentationOnlyGoal &&
670
- initial.applyRun &&
671
- initial.applyRun.code === 9 &&
672
- initial.applyRun.stderr.startsWith('DOC_SCOPE_VIOLATION')) {
673
- console.log(chalk.yellow('⚠️ Plan attempted non-documentation files. Retrying with strict README-only scope...'));
674
- const strictGoal = buildDocumentationOnlyIntent(normalizedGoal, true);
675
- initial = await runPlanAndApply(cwd, strictGoal, options.projectId, {
676
- enforceDocumentationScope: true,
677
- });
678
- }
679
- if (!initial.planId || initial.planRun.code !== 0 || !initial.applyRun || initial.applyRun.code !== 0) {
680
- console.error(chalk.red('\n❌ Ship failed during initial plan/apply.'));
681
- const detail = initial.applyRun?.stderr || initial.planRun.stderr || initial.planRun.stdout;
682
- if (detail) {
683
- console.error(chalk.dim(` Details: ${shellTailLines(stripAnsi(detail), 8)}`));
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
- process.exit(1);
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
- process.exit(1);
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
- verifyPayload = parsedVerify;
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
- (parsedVerify.verdict === 'PASS' || isInfoOnlyGovernanceResult(parsedVerify));
1525
+ (verifiedPayload.verdict === 'PASS' || (!requirePass && isInfoOnlyGovernanceResult(verifiedPayload)));
723
1526
  if (verifyPassed) {
724
- if (parsedVerify.verdict === 'PASS') {
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
- const scopeDriftFiles = parsedVerify.violations
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, parsedVerify.violations);
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, parsedVerify, remediationAttemptsUsed);
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
- process.exit(1);
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
- (verifyPayload.verdict === 'PASS' || isInfoOnlyGovernanceResult(verifyPayload));
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, ['verify', '--plan-id', currentPlanId, '--json', ...(recordVerify ? ['--record'] : [])], baselineDirtyPaths.length > 0
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(verifyPayload, blast, testsPassed, remediationAttemptsUsed);
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: initial.planId,
1844
+ initialPlanId,
879
1845
  finalPlanId: currentPlanId,
880
1846
  repairPlanIds,
881
1847
  },
882
- verification: verifyPayload,
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: initial.planRun.durationMs,
904
- initialApply: initial.applyRun ? initial.applyRun.durationMs : 0,
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
- const artifactPaths = writeMergeConfidenceArtifacts(cwd, card);
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: ${verifyPayload.verdict} | Tests: ${testsPassed ? 'PASS' : 'FAIL'}`));
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
- console.log(JSON.stringify({
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
- }, null, 2));
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