@neurcode-ai/cli 0.19.0 โ†’ 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/commands/brain.d.ts.map +1 -1
  2. package/dist/commands/brain.js +87 -7
  3. package/dist/commands/brain.js.map +1 -1
  4. package/dist/commands/ops.d.ts +5 -0
  5. package/dist/commands/ops.d.ts.map +1 -1
  6. package/dist/commands/ops.js +32 -2
  7. package/dist/commands/ops.js.map +1 -1
  8. package/dist/commands/policy.d.ts.map +1 -1
  9. package/dist/commands/policy.js +50 -0
  10. package/dist/commands/policy.js.map +1 -1
  11. package/dist/commands/quickstart.d.ts.map +1 -1
  12. package/dist/commands/quickstart.js +11 -6
  13. package/dist/commands/quickstart.js.map +1 -1
  14. package/dist/commands/runtime-doctor.d.ts.map +1 -1
  15. package/dist/commands/runtime-doctor.js +36 -1
  16. package/dist/commands/runtime-doctor.js.map +1 -1
  17. package/dist/commands/session-hook.d.ts.map +1 -1
  18. package/dist/commands/session-hook.js +125 -8
  19. package/dist/commands/session-hook.js.map +1 -1
  20. package/dist/commands/session.d.ts +18 -0
  21. package/dist/commands/session.d.ts.map +1 -1
  22. package/dist/commands/session.js +617 -181
  23. package/dist/commands/session.js.map +1 -1
  24. package/dist/commands/verify-output.d.ts +2 -0
  25. package/dist/commands/verify-output.d.ts.map +1 -1
  26. package/dist/commands/verify-output.js +4 -0
  27. package/dist/commands/verify-output.js.map +1 -1
  28. package/dist/commands/verify.d.ts.map +1 -1
  29. package/dist/commands/verify.js +108 -24
  30. package/dist/commands/verify.js.map +1 -1
  31. package/dist/governance/structural-on-diff.d.ts +11 -0
  32. package/dist/governance/structural-on-diff.d.ts.map +1 -1
  33. package/dist/governance/structural-on-diff.js +38 -5
  34. package/dist/governance/structural-on-diff.js.map +1 -1
  35. package/dist/index.js +12 -6
  36. package/dist/index.js.map +1 -1
  37. package/dist/runtime-build.json +5 -5
  38. package/dist/utils/brain-context.d.ts.map +1 -1
  39. package/dist/utils/brain-context.js +11 -2
  40. package/dist/utils/brain-context.js.map +1 -1
  41. package/dist/utils/local-repo-brain.d.ts.map +1 -1
  42. package/dist/utils/local-repo-brain.js +4 -0
  43. package/dist/utils/local-repo-brain.js.map +1 -1
  44. package/dist/utils/profile-drift-recovery.d.ts +40 -0
  45. package/dist/utils/profile-drift-recovery.d.ts.map +1 -0
  46. package/dist/utils/profile-drift-recovery.js +235 -0
  47. package/dist/utils/profile-drift-recovery.js.map +1 -0
  48. package/dist/utils/proposed-change-analysis.d.ts.map +1 -1
  49. package/dist/utils/proposed-change-analysis.js +7 -5
  50. package/dist/utils/proposed-change-analysis.js.map +1 -1
  51. package/dist/utils/repo-intelligence-v2.d.ts.map +1 -1
  52. package/dist/utils/repo-intelligence-v2.js +41 -0
  53. package/dist/utils/repo-intelligence-v2.js.map +1 -1
  54. package/dist/utils/runtime-companion.d.ts.map +1 -1
  55. package/dist/utils/runtime-companion.js +9 -1
  56. package/dist/utils/runtime-companion.js.map +1 -1
  57. package/dist/utils/structural-understanding.d.ts +2 -2
  58. package/dist/utils/structural-understanding.d.ts.map +1 -1
  59. package/dist/utils/structural-understanding.js +1 -1
  60. package/dist/utils/structural-understanding.js.map +1 -1
  61. package/dist/utils/team-memory-path-hygiene.d.ts +4 -0
  62. package/dist/utils/team-memory-path-hygiene.d.ts.map +1 -0
  63. package/dist/utils/team-memory-path-hygiene.js +50 -0
  64. package/dist/utils/team-memory-path-hygiene.js.map +1 -0
  65. package/dist/utils/v0-governance.d.ts +19 -1
  66. package/dist/utils/v0-governance.d.ts.map +1 -1
  67. package/dist/utils/v0-governance.js +65 -4
  68. package/dist/utils/v0-governance.js.map +1 -1
  69. package/package.json +4 -3
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.resolveUnderstandingDiffFiles = resolveUnderstandingDiffFiles;
46
47
  exports.buildLocalGovernanceStatus = buildLocalGovernanceStatus;
47
48
  exports.localGovernanceStatusCommand = localGovernanceStatusCommand;
48
49
  exports.resetStaleGovernanceSessionCommand = resetStaleGovernanceSessionCommand;
@@ -61,6 +62,7 @@ exports.verifyAIChangeRecordForCli = verifyAIChangeRecordForCli;
61
62
  exports.structuralUnderstandingCommand = structuralUnderstandingCommand;
62
63
  exports.listSessionsCommand = listSessionsCommand;
63
64
  exports.endSessionCommand = endSessionCommand;
65
+ exports.endSessionCommandWithDependencies = endSessionCommandWithDependencies;
64
66
  exports.sessionStatusCommand = sessionStatusCommand;
65
67
  exports.listLocalSessionsCommand = listLocalSessionsCommand;
66
68
  exports.currentLocalSessionCommand = currentLocalSessionCommand;
@@ -84,6 +86,8 @@ const runtime_outbox_1 = require("../utils/runtime-outbox");
84
86
  const repo_brain_impact_1 = require("../utils/repo-brain-impact");
85
87
  const structural_understanding_1 = require("../utils/structural-understanding");
86
88
  const agent_guard_supervisor_1 = require("../utils/agent-guard-supervisor");
89
+ const hook_heartbeat_1 = require("../utils/hook-heartbeat");
90
+ const profile_drift_recovery_1 = require("../utils/profile-drift-recovery");
87
91
  const node_child_process_1 = require("node:child_process");
88
92
  const node_fs_1 = require("node:fs");
89
93
  const node_path_1 = require("node:path");
@@ -173,7 +177,79 @@ function approvalContextFrom(event) {
173
177
  : event?.filePath,
174
178
  };
175
179
  }
176
- function resolveUnderstandingDiff(repoRoot, options) {
180
+ const UNTRACKED_UNDERSTANDING_EXCLUDED_DIRS = new Set([
181
+ '.git',
182
+ '.neurcode',
183
+ '.neurcode-admission',
184
+ '.neurcode-ai-record',
185
+ '.cache',
186
+ '.next',
187
+ 'build',
188
+ 'cache',
189
+ 'coverage',
190
+ 'dist',
191
+ 'evidence',
192
+ 'generated',
193
+ 'node_modules',
194
+ 'out',
195
+ 'vendor',
196
+ ]);
197
+ const UNTRACKED_UNDERSTANDING_SOURCE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
198
+ const UNTRACKED_UNDERSTANDING_GENERATED_FILE = /(?:\.d\.ts|\.map|\.min\.js|\.bundle\.js|\.generated\.[cm]?[jt]sx?)$/i;
199
+ function normalizeUnderstandingPath(value) {
200
+ return value.replace(/\\/g, '/').replace(/^\.\//, '').trim();
201
+ }
202
+ function eligibleUntrackedUnderstandingPath(repoRoot, value) {
203
+ const normalized = normalizeUnderstandingPath(value);
204
+ if (!normalized || normalized.startsWith('/') || normalized === '..' || normalized.startsWith('../'))
205
+ return null;
206
+ const segments = normalized.split('/');
207
+ if (segments.includes('..') || segments.some((segment) => UNTRACKED_UNDERSTANDING_EXCLUDED_DIRS.has(segment.toLowerCase()))) {
208
+ return null;
209
+ }
210
+ if (UNTRACKED_UNDERSTANDING_GENERATED_FILE.test(normalized))
211
+ return null;
212
+ const absolutePath = (0, node_path_1.join)(repoRoot, normalized);
213
+ try {
214
+ const stat = (0, node_fs_1.lstatSync)(absolutePath);
215
+ if (!stat.isFile() || stat.isSymbolicLink())
216
+ return null;
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ return normalized;
222
+ }
223
+ function untrackedDiffFile(repoRoot, path) {
224
+ let lines = [];
225
+ if (UNTRACKED_UNDERSTANDING_SOURCE.test(path)) {
226
+ const text = (0, node_fs_1.readFileSync)((0, node_path_1.join)(repoRoot, path), 'utf8').replace(/\r\n/g, '\n');
227
+ lines = text ? text.split('\n') : [];
228
+ if (lines.at(-1) === '')
229
+ lines.pop();
230
+ }
231
+ return {
232
+ path,
233
+ changeType: 'add',
234
+ addedLines: lines.length,
235
+ removedLines: 0,
236
+ hunks: lines.length > 0
237
+ ? [{
238
+ oldStart: 0,
239
+ oldLines: 0,
240
+ newStart: 1,
241
+ newLines: lines.length,
242
+ lines: lines.map((content, index) => ({
243
+ type: 'added',
244
+ content,
245
+ lineNumber: index + 1,
246
+ })),
247
+ }]
248
+ : [],
249
+ provenance: 'git-untracked',
250
+ };
251
+ }
252
+ function resolveUnderstandingDiffFiles(repoRoot, options = {}) {
177
253
  const args = ['diff'];
178
254
  if (options.staged) {
179
255
  args.push('--cached');
@@ -184,11 +260,27 @@ function resolveUnderstandingDiff(repoRoot, options) {
184
260
  else {
185
261
  args.push('HEAD');
186
262
  }
187
- return (0, node_child_process_1.execFileSync)('git', args, {
263
+ const diffText = (0, node_child_process_1.execFileSync)('git', args, {
264
+ cwd: repoRoot,
265
+ encoding: 'utf8',
266
+ maxBuffer: 64 * 1024 * 1024,
267
+ });
268
+ const selected = (0, diff_parser_1.parseDiff)(diffText);
269
+ if (options.staged)
270
+ return selected;
271
+ const untrackedOutput = (0, node_child_process_1.execFileSync)('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
188
272
  cwd: repoRoot,
189
273
  encoding: 'utf8',
190
274
  maxBuffer: 64 * 1024 * 1024,
191
275
  });
276
+ const byPath = new Map(selected.map((file) => [normalizeUnderstandingPath(file.path), file]));
277
+ for (const rawPath of untrackedOutput.split('\0')) {
278
+ const path = eligibleUntrackedUnderstandingPath(repoRoot, rawPath);
279
+ if (!path || byPath.has(path))
280
+ continue;
281
+ byPath.set(path, untrackedDiffFile(repoRoot, path));
282
+ }
283
+ return [...byPath.values()].sort((a, b) => normalizeUnderstandingPath(a.path).localeCompare(normalizeUnderstandingPath(b.path)));
192
284
  }
193
285
  function loadLocalGovernanceSession(repoRoot, sessionId) {
194
286
  return sessionId ? (0, governance_runtime_1.loadSession)(repoRoot, sessionId) : (0, governance_runtime_1.loadActiveSession)(repoRoot);
@@ -251,6 +343,108 @@ function pendingApprovalBlock(session, now = new Date()) {
251
343
  }
252
344
  return null;
253
345
  }
346
+ function activePointerInspection(repoRoot) {
347
+ const path = (0, node_path_1.join)(repoRoot, '.neurcode', 'active-session.json');
348
+ if (!(0, node_fs_1.existsSync)(path))
349
+ return { state: 'missing', sessionId: null, session: null };
350
+ try {
351
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
352
+ if (parsed.sessionId === null)
353
+ return { state: 'cleared', sessionId: null, session: null };
354
+ if (typeof parsed.sessionId !== 'string' || !parsed.sessionId.trim()) {
355
+ return { state: 'malformed', sessionId: null, session: null };
356
+ }
357
+ const session = (0, governance_runtime_1.loadSession)(repoRoot, parsed.sessionId);
358
+ if (!session || session.status !== 'active') {
359
+ return { state: 'stale', sessionId: parsed.sessionId, session };
360
+ }
361
+ return { state: 'valid', sessionId: parsed.sessionId, session };
362
+ }
363
+ catch {
364
+ return { state: 'malformed', sessionId: null, session: null };
365
+ }
366
+ }
367
+ function validGovernanceSessionRecord(value) {
368
+ if (!value || typeof value !== 'object')
369
+ return false;
370
+ const record = value;
371
+ return (typeof record.sessionId === 'string' &&
372
+ typeof record.profileHash === 'string' &&
373
+ (record.status === 'active' || record.status === 'finished') &&
374
+ Array.isArray(record.events) &&
375
+ Boolean(record.contract && typeof record.contract === 'object'));
376
+ }
377
+ function scanSessionRecords(repoRoot) {
378
+ const directory = (0, governance_runtime_1.sessionsDir)(repoRoot);
379
+ if (!(0, node_fs_1.existsSync)(directory))
380
+ return { active: [], malformed: [] };
381
+ const active = [];
382
+ const malformed = [];
383
+ for (const entry of (0, node_fs_1.readdirSync)(directory, { withFileTypes: true })
384
+ .filter((item) => item.isFile() && item.name.endsWith('.json') && !item.name.endsWith('.change-record.json'))
385
+ .sort((a, b) => a.name.localeCompare(b.name))) {
386
+ const relativePath = `.neurcode/sessions/${entry.name}`;
387
+ try {
388
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(directory, entry.name), 'utf8'));
389
+ if (!validGovernanceSessionRecord(parsed)) {
390
+ malformed.push({ file: relativePath, reasonCode: 'invalid_session_record' });
391
+ continue;
392
+ }
393
+ if (entry.name !== `${parsed.sessionId}.json`) {
394
+ malformed.push({ file: relativePath, reasonCode: 'session_id_filename_mismatch' });
395
+ continue;
396
+ }
397
+ if (parsed.status === 'active')
398
+ active.push(parsed);
399
+ }
400
+ catch {
401
+ malformed.push({ file: relativePath, reasonCode: 'malformed_json' });
402
+ }
403
+ }
404
+ return {
405
+ active: active.sort((a, b) => a.sessionId.localeCompare(b.sessionId)),
406
+ malformed,
407
+ };
408
+ }
409
+ function recoveryReasonCode(pointer, sessionId) {
410
+ if (pointer.state === 'valid') {
411
+ return pointer.sessionId === sessionId
412
+ ? 'stale_referenced_active_session'
413
+ : 'orphan_unreferenced_active_record';
414
+ }
415
+ if (pointer.state === 'missing')
416
+ return 'orphan_missing_active_pointer';
417
+ if (pointer.state === 'cleared')
418
+ return 'orphan_cleared_active_pointer';
419
+ if (pointer.state === 'malformed')
420
+ return 'orphan_malformed_active_pointer';
421
+ return 'orphan_stale_active_pointer';
422
+ }
423
+ function activeSessionLiveness(repoRoot, sessionId, now) {
424
+ const reasons = [];
425
+ const supervisor = (0, agent_guard_supervisor_1.inspectAgentGuardSupervisor)(repoRoot, sessionId);
426
+ if (supervisor.state?.sessionId === sessionId &&
427
+ supervisor.alive &&
428
+ ['running', 'starting', 'stopping'].includes(supervisor.effectiveStatus)) {
429
+ reasons.push('live_supervisor_process');
430
+ }
431
+ const heartbeat = (0, hook_heartbeat_1.readHookHeartbeat)(repoRoot);
432
+ const heartbeatAt = Date.parse(heartbeat?.lastEvent.ts ?? '');
433
+ if (heartbeat?.lastEvent.sessionId === sessionId &&
434
+ Number.isFinite(heartbeatAt) &&
435
+ now.getTime() - heartbeatAt >= 0 &&
436
+ now.getTime() - heartbeatAt <= 5 * 60_000) {
437
+ reasons.push('fresh_hook_heartbeat');
438
+ }
439
+ return reasons;
440
+ }
441
+ function clearInvalidActivePointer(repoRoot, pointer) {
442
+ if (pointer.state === 'valid')
443
+ return;
444
+ const directory = (0, node_path_1.join)(repoRoot, '.neurcode');
445
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
446
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, 'active-session.json'), JSON.stringify({ sessionId: null }, null, 2) + '\n', 'utf8');
447
+ }
254
448
  function buildLocalGovernanceStatus(options = {}) {
255
449
  const repoRoot = (0, v0_governance_1.resolveRepoRoot)(options.dir || process.cwd());
256
450
  const session = loadLocalGovernanceSession(repoRoot, options.sessionId);
@@ -267,6 +461,19 @@ function buildLocalGovernanceStatus(options = {}) {
267
461
  };
268
462
  }
269
463
  const recentEvents = session.events.slice(-10);
464
+ const staleness = (0, v0_governance_1.getProfileStaleness)(repoRoot);
465
+ const profileAction = (0, v0_governance_1.profileFreshnessActionForSession)(staleness, session.profileHash);
466
+ const pendingProfileDecisions = (0, profile_drift_recovery_1.pendingProfileDriftDecisions)(session);
467
+ const profileFreshness = (0, v0_governance_1.buildProfileFreshnessSignal)(staleness, profileAction, {
468
+ sessionProfileHash: session.profileHash,
469
+ ...(profileAction === 'session_restart_required'
470
+ ? {
471
+ recoveryReason: 'active_session_profile_changed',
472
+ recoveryCommand: profile_drift_recovery_1.PROFILE_DRIFT_RECOVERY_COMMAND,
473
+ unresolvedHumanDecisions: pendingProfileDecisions.length > 0,
474
+ }
475
+ : {}),
476
+ });
270
477
  const latestBlock = [...session.events].reverse().find((event) => event.type === 'check_block');
271
478
  const latestApprovalContext = approvalContextFrom(latestBlock);
272
479
  const suggestedApprovalPath = latestApprovalContext?.suggestedApprovalPath ||
@@ -280,6 +487,7 @@ function buildLocalGovernanceStatus(options = {}) {
280
487
  status: session.status,
281
488
  goal: session.contract.goal,
282
489
  profileHash: session.profileHash,
490
+ profileFreshness,
283
491
  scopeMode: session.contract.scopeMode,
284
492
  planCoherenceMode: session.contract.planCoherenceMode ?? 'warn',
285
493
  agentPlan: session.contract.agentPlan ?? null,
@@ -341,6 +549,14 @@ function localGovernanceStatusCommand(options = {}) {
341
549
  console.log(`Session: ${chalk.white(activeStatus.sessionId)} ${activeStatus.active ? chalk.green('active') : chalk.dim(activeStatus.status)}`);
342
550
  console.log(`Goal: ${chalk.white(truncate(activeStatus.goal))}`);
343
551
  console.log(`Scope: ${chalk.white(activeStatus.scopeMode)}`);
552
+ console.log(`Profile: ${chalk.white(activeStatus.profileFreshness.status)} cache ยท ` +
553
+ `${chalk.white(activeStatus.profileFreshness.sessionCompatibility)} session`);
554
+ if (activeStatus.profileFreshness.sessionCompatibility === 'incompatible') {
555
+ console.log(chalk.yellow(`Hashes: session ${activeStatus.profileHash.slice(0, 12)} ยท ` +
556
+ `current ${activeStatus.profileFreshness.currentProfileHash.slice(0, 12)}`));
557
+ console.log(chalk.yellow(`Recover: ${profile_drift_recovery_1.PROFILE_DRIFT_RECOVERY_COMMAND} ` +
558
+ `(--force abandons unresolved operator state${activeStatus.profileFreshness.unresolvedHumanDecisions ? '; unresolved decisions are present' : ''})`));
559
+ }
344
560
  console.log(`Plan: ${chalk.white(activeStatus.planCoherenceMode)}${activeStatus.agentPlanRevision ? chalk.dim(` ยท rev ${activeStatus.agentPlanRevision}`) : ''}`);
345
561
  console.log(`Agent: ${chalk.white(activeStatus.agentInvocation.status.replace(/_/g, ' '))}` +
346
562
  chalk.dim(` ยท score ${activeStatus.agentInvocation.score}`) +
@@ -413,7 +629,8 @@ async function resetStaleGovernanceSessionCommand(options = {}) {
413
629
  const maxAgeMinutes = Number.isFinite(options.maxAgeMinutes)
414
630
  ? Math.max(0, Number(options.maxAgeMinutes))
415
631
  : 120;
416
- const active = (0, governance_runtime_1.loadActiveSession)(repoRoot);
632
+ const pointer = activePointerInspection(repoRoot);
633
+ const records = scanSessionRecords(repoRoot);
417
634
  const output = (payload, statusCode = 0) => {
418
635
  if (options.json) {
419
636
  console.log(JSON.stringify(payload, null, 2));
@@ -433,92 +650,175 @@ async function resetStaleGovernanceSessionCommand(options = {}) {
433
650
  }
434
651
  process.exitCode = statusCode;
435
652
  };
436
- if (!active || active.status !== 'active') {
653
+ if (records.active.length === 0) {
437
654
  output({
438
655
  ok: true,
439
656
  reset: false,
440
657
  repoRoot,
441
658
  reason: 'no_active_session',
659
+ pointerState: pointer.state,
660
+ malformedRecords: records.malformed,
442
661
  message: 'No active in-flow governance session found.',
443
662
  });
444
663
  return;
445
664
  }
446
- const ageMinutes = sessionAgeMinutes(active, now);
447
- const pending = pendingApprovalBlock(active, now);
448
- const stale = ageMinutes >= maxAgeMinutes;
449
- if (!stale && options.force !== true) {
665
+ const recovered = [];
666
+ const skipped = [];
667
+ for (const active of records.active) {
668
+ const ageMinutes = sessionAgeMinutes(active, now);
669
+ const pending = pendingApprovalBlock(active, now);
670
+ const stale = ageMinutes >= maxAgeMinutes;
671
+ const liveness = activeSessionLiveness(repoRoot, active.sessionId, now);
672
+ const reasonCode = recoveryReasonCode(pointer, active.sessionId);
673
+ if (liveness.length > 0) {
674
+ skipped.push({
675
+ sessionId: active.sessionId,
676
+ reason: 'session_live',
677
+ reasonCode,
678
+ liveness,
679
+ ageMinutes: Number(ageMinutes.toFixed(2)),
680
+ });
681
+ continue;
682
+ }
683
+ if (!stale && options.force !== true) {
684
+ skipped.push({
685
+ sessionId: active.sessionId,
686
+ reason: 'session_not_stale',
687
+ reasonCode,
688
+ ageMinutes: Number(ageMinutes.toFixed(2)),
689
+ pendingApproval: pending,
690
+ });
691
+ continue;
692
+ }
693
+ if (pending && options.force !== true) {
694
+ skipped.push({
695
+ sessionId: active.sessionId,
696
+ reason: 'pending_approval',
697
+ reasonCode,
698
+ ageMinutes: Number(ageMinutes.toFixed(2)),
699
+ filePath: pending.filePath,
700
+ owners: pending.owners,
701
+ suggestedApprovalPath: pending.suggestedApprovalPath,
702
+ });
703
+ continue;
704
+ }
705
+ const pointerReferenced = pointer.state === 'valid' && pointer.sessionId === active.sessionId;
706
+ (0, governance_runtime_1.appendEvent)(repoRoot, active.sessionId, {
707
+ type: 'user_decision',
708
+ ts: now.toISOString(),
709
+ decision: pointerReferenced ? 'reset_stale_session' : 'recover_orphaned_session',
710
+ detail: {
711
+ source: 'local_cli',
712
+ recovery: true,
713
+ reasonCode,
714
+ pointerState: pointer.state,
715
+ force: options.force === true,
716
+ maxAgeMinutes,
717
+ ageMinutes: Number(ageMinutes.toFixed(2)),
718
+ pendingApproval: pending,
719
+ livenessChecks: ['bounded_event_age', 'guard_supervisor_process', 'hook_heartbeat'],
720
+ },
721
+ });
722
+ const livePointerPath = (0, node_path_1.join)(repoRoot, '.neurcode', 'active-session.json');
723
+ const preservedLivePointer = pointer.state === 'valid' && pointer.sessionId !== active.sessionId && (0, node_fs_1.existsSync)(livePointerPath)
724
+ ? (0, node_fs_1.readFileSync)(livePointerPath, 'utf8')
725
+ : null;
726
+ let finished = null;
727
+ try {
728
+ finished = (0, governance_runtime_1.finishSession)(repoRoot, active.sessionId, { reason: reasonCode });
729
+ }
730
+ finally {
731
+ if (preservedLivePointer !== null) {
732
+ (0, node_fs_1.writeFileSync)(livePointerPath, preservedLivePointer, 'utf8');
733
+ }
734
+ }
735
+ if (!finished) {
736
+ skipped.push({ sessionId: active.sessionId, reason: 'finish_failed', reasonCode });
737
+ continue;
738
+ }
739
+ const supervisor = (0, agent_guard_supervisor_1.inspectAgentGuardSupervisor)(repoRoot, finished.sessionId);
740
+ if (supervisor.state?.sessionId === finished.sessionId) {
741
+ (0, agent_guard_supervisor_1.stopSupervisorOnSessionCompletion)(repoRoot);
742
+ }
743
+ await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, finished);
744
+ const replay = (0, governance_runtime_1.replaySession)(finished);
745
+ recovered.push({
746
+ sessionId: finished.sessionId,
747
+ previousGoal: finished.contract.goal,
748
+ status: finished.status,
749
+ reasonCode,
750
+ replayHash: finished.replayHash,
751
+ replayVerified: replay.matchesOriginal,
752
+ ageMinutes: Number(ageMinutes.toFixed(2)),
753
+ forced: options.force === true,
754
+ recordPath: `.neurcode/sessions/${finished.sessionId}.json`,
755
+ evidencePath: (0, node_path_1.relative)(repoRoot, (0, governance_runtime_1.aiChangeRecordPath)(repoRoot, finished.sessionId)).replace(/\\/g, '/'),
756
+ });
757
+ }
758
+ if (recovered.length > 0) {
759
+ clearInvalidActivePointer(repoRoot, pointer);
760
+ const first = recovered[0];
450
761
  output({
451
762
  ok: true,
452
- reset: false,
763
+ reset: true,
453
764
  repoRoot,
454
- sessionId: active.sessionId,
455
- reason: 'session_not_stale',
456
- ageMinutes: Number(ageMinutes.toFixed(2)),
765
+ sessionId: recovered.length === 1 ? first.sessionId : undefined,
766
+ status: recovered.length === 1 ? first.status : 'finished',
767
+ replayHash: recovered.length === 1 ? first.replayHash : undefined,
768
+ ageMinutes: recovered.length === 1 ? first.ageMinutes : undefined,
457
769
  maxAgeMinutes,
458
- pendingApproval: pending,
459
- message: `Active session ${active.sessionId} is not stale yet (${ageMinutes.toFixed(1)}m < ${maxAgeMinutes}m).`,
770
+ forced: options.force === true,
771
+ pointerState: pointer.state,
772
+ recoveredCount: recovered.length,
773
+ recovered,
774
+ skipped,
775
+ malformedRecords: records.malformed,
776
+ message: `Recovered ${recovered.length} stale or orphaned active session record${recovered.length === 1 ? '' : 's'}.`,
460
777
  });
461
778
  return;
462
779
  }
463
- if (pending && options.force !== true) {
780
+ const primary = skipped.find((item) => item.sessionId === pointer.sessionId) ?? skipped[0];
781
+ if (primary?.reason === 'pending_approval') {
464
782
  output({
465
783
  ok: false,
466
784
  reset: false,
467
785
  repoRoot,
468
- sessionId: active.sessionId,
469
- reason: 'pending_approval',
470
- ageMinutes: Number(ageMinutes.toFixed(2)),
786
+ ...primary,
471
787
  maxAgeMinutes,
472
- filePath: pending.filePath,
473
- owners: pending.owners,
474
- suggestedApprovalPath: pending.suggestedApprovalPath,
788
+ pointerState: pointer.state,
789
+ malformedRecords: records.malformed,
475
790
  message: 'Active session is waiting on an unresolved approval; refusing to reset without --force.',
476
791
  next: [
477
- `Approve exactly ${pending.suggestedApprovalPath} from the dashboard or MCP.`,
792
+ `Approve exactly ${primary.suggestedApprovalPath} from the dashboard or MCP.`,
478
793
  'Or run `neurcode session reset-stale --force` if this is abandoned rehearsal state.',
479
794
  ],
480
795
  }, 2);
481
796
  return;
482
797
  }
483
- (0, governance_runtime_1.appendEvent)(repoRoot, active.sessionId, {
484
- type: 'user_decision',
485
- ts: now.toISOString(),
486
- decision: 'reset_stale_session',
487
- detail: {
488
- source: 'local_cli',
489
- force: options.force === true,
490
- maxAgeMinutes,
491
- ageMinutes: Number(ageMinutes.toFixed(2)),
492
- pendingApproval: pending,
493
- },
494
- });
495
- const finished = (0, governance_runtime_1.finishSession)(repoRoot, active.sessionId);
496
- if (!finished) {
798
+ if (primary?.reason === 'finish_failed') {
497
799
  output({
498
800
  ok: false,
499
801
  reset: false,
500
802
  repoRoot,
501
- sessionId: active.sessionId,
502
- reason: 'finish_failed',
503
- message: `Could not finish active session ${active.sessionId}.`,
803
+ ...primary,
804
+ pointerState: pointer.state,
805
+ malformedRecords: records.malformed,
806
+ message: `Could not finish active session ${primary.sessionId}.`,
504
807
  }, 1);
505
808
  return;
506
809
  }
507
- (0, agent_guard_supervisor_1.stopSupervisorOnSessionCompletion)(repoRoot);
508
- await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, finished);
509
810
  output({
510
811
  ok: true,
511
- reset: true,
812
+ reset: false,
512
813
  repoRoot,
513
- sessionId: finished.sessionId,
514
- previousGoal: finished.contract.goal,
515
- status: finished.status,
516
- replayHash: finished.replayHash,
517
- ageMinutes: Number(ageMinutes.toFixed(2)),
814
+ ...primary,
518
815
  maxAgeMinutes,
519
- forced: options.force === true,
520
- recordPath: `.neurcode/sessions/${finished.sessionId}.json`,
521
- message: `Finished active session ${finished.sessionId} and cleared the active pointer.`,
816
+ pointerState: pointer.state,
817
+ skipped,
818
+ malformedRecords: records.malformed,
819
+ message: primary?.reason === 'session_live'
820
+ ? `Session ${primary.sessionId} has current process or heartbeat liveness evidence and was preserved.`
821
+ : `Active session ${primary?.sessionId} is not stale yet (${Number(primary?.ageMinutes ?? 0).toFixed(1)}m < ${maxAgeMinutes}m).`,
522
822
  });
523
823
  }
524
824
  async function replanGovernanceSessionCommand(options = {}) {
@@ -1547,9 +1847,9 @@ function structuralUnderstandingCommand(options = {}) {
1547
1847
  process.exitCode = 1;
1548
1848
  return;
1549
1849
  }
1550
- let diffText = '';
1850
+ let diffFiles = [];
1551
1851
  try {
1552
- diffText = resolveUnderstandingDiff(repoRoot, options);
1852
+ diffFiles = resolveUnderstandingDiffFiles(repoRoot, options);
1553
1853
  }
1554
1854
  catch (error) {
1555
1855
  const message = error instanceof Error ? error.message : String(error);
@@ -1562,7 +1862,6 @@ function structuralUnderstandingCommand(options = {}) {
1562
1862
  process.exitCode = 1;
1563
1863
  return;
1564
1864
  }
1565
- const diffFiles = diffText.trim() ? (0, diff_parser_1.parseDiff)(diffText) : [];
1566
1865
  const profile = (0, v0_governance_1.getProfileStaleness)(repoRoot).currentProfile;
1567
1866
  const artifact = (0, structural_understanding_1.buildStructuralUnderstanding)(repoRoot, diffFiles, {
1568
1867
  session,
@@ -1679,157 +1978,294 @@ async function listSessionsCommand(options) {
1679
1978
  * End a session
1680
1979
  */
1681
1980
  async function endSessionCommand(options) {
1981
+ return endSessionCommandWithDependencies(options);
1982
+ }
1983
+ function endSessionOutput(options, payload, exitCode = 0) {
1984
+ if (options.json) {
1985
+ console.log(JSON.stringify({ ...payload, exitCode }, null, 2));
1986
+ }
1987
+ else if (payload.ok === true) {
1988
+ console.log(chalk.green(String(payload.message || 'Session ended.')));
1989
+ if (payload.sessionId)
1990
+ console.log(chalk.dim(`Session: ${payload.sessionId}`));
1991
+ if (payload.replayHash)
1992
+ console.log(chalk.dim(`replayHash: ${payload.replayHash}`));
1993
+ }
1994
+ else {
1995
+ console.error(chalk.red(String(payload.message || payload.error || 'Session end failed.')));
1996
+ const candidates = Array.isArray(payload.candidates) ? payload.candidates : [];
1997
+ for (const candidate of candidates) {
1998
+ const item = candidate;
1999
+ console.error(chalk.dim(` ${item.sessionId || 'unknown'}: ${item.command || ''}`));
2000
+ }
2001
+ }
2002
+ process.exitCode = exitCode;
2003
+ }
2004
+ async function finishLocalGovernanceSession(repoRoot, session) {
2005
+ if (session.status === 'finished') {
2006
+ return {
2007
+ ok: true,
2008
+ ended: false,
2009
+ mode: 'local',
2010
+ status: 'already_finished',
2011
+ sessionId: session.sessionId,
2012
+ replayHash: session.replayHash || null,
2013
+ message: `Local governance session ${session.sessionId} is already finished.`,
2014
+ };
2015
+ }
2016
+ (0, governance_runtime_1.appendEvent)(repoRoot, session.sessionId, {
2017
+ type: 'user_decision',
2018
+ ts: new Date().toISOString(),
2019
+ decision: 'local_session_end_requested',
2020
+ message: 'Operator ended the local governance session.',
2021
+ detail: {
2022
+ source: 'local_cli',
2023
+ command: 'session end',
2024
+ },
2025
+ });
2026
+ const finished = (0, governance_runtime_1.finishSession)(repoRoot, session.sessionId, {
2027
+ reason: 'local_session_end_requested',
2028
+ });
2029
+ if (!finished)
2030
+ throw new Error(`Local governance session ${session.sessionId} could not be finished.`);
2031
+ (0, agent_guard_supervisor_1.stopSupervisorOnSessionCompletion)(repoRoot);
2032
+ let liveStatusPublished = true;
1682
2033
  try {
1683
- const config = (0, config_1.loadConfig)();
1684
- if (!config.apiKey) {
1685
- config.apiKey = (0, config_1.requireApiKey)();
2034
+ await (0, runtime_live_1.publishRuntimeLiveStatus)(repoRoot, finished);
2035
+ }
2036
+ catch {
2037
+ liveStatusPublished = false;
2038
+ }
2039
+ const replay = (0, governance_runtime_1.replaySession)(finished);
2040
+ return {
2041
+ ok: true,
2042
+ ended: true,
2043
+ mode: 'local',
2044
+ status: finished.status,
2045
+ sessionId: finished.sessionId,
2046
+ replayHash: finished.replayHash,
2047
+ replayVerified: replay.matchesOriginal,
2048
+ liveStatusPublished,
2049
+ recordPath: `.neurcode/sessions/${finished.sessionId}.json`,
2050
+ evidencePath: (0, node_path_1.relative)(repoRoot, (0, governance_runtime_1.aiChangeRecordPath)(repoRoot, finished.sessionId)).replace(/\\/g, '/'),
2051
+ message: `Local governance session ${finished.sessionId} ended with replay-valid evidence.`,
2052
+ };
2053
+ }
2054
+ async function endSessionCommandWithDependencies(options, dependencies = {}) {
2055
+ const repoRoot = (0, v0_governance_1.resolveRepoRoot)(options.dir || process.cwd());
2056
+ const isInteractive = dependencies.isInteractive ??
2057
+ (() => Boolean(process.stdin.isTTY && process.stdout.isTTY));
2058
+ const prompt = dependencies.prompt ?? promptUser;
2059
+ try {
2060
+ if (options.sessionId) {
2061
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, options.sessionId);
2062
+ if (local) {
2063
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
2064
+ return;
2065
+ }
2066
+ if (options.local) {
2067
+ endSessionOutput(options, {
2068
+ ok: false,
2069
+ ended: false,
2070
+ mode: 'local',
2071
+ reason: 'local_session_not_found',
2072
+ sessionId: options.sessionId,
2073
+ message: `Local governance session ${options.sessionId} was not found.`,
2074
+ }, 2);
2075
+ return;
2076
+ }
1686
2077
  }
1687
- const client = new api_client_1.ApiClient(config);
1688
- let sessionId = options.sessionId;
1689
- // If no session ID provided, try to get from state
1690
- if (!sessionId) {
1691
- const stateSessionId = (0, state_1.getSessionId)();
1692
- sessionId = stateSessionId || undefined;
1693
- if (!sessionId) {
1694
- // List active sessions and let user choose
1695
- (0, messages_1.printInfo)('No Active Session', 'Looking for active sessions...');
1696
- const sessions = await client.getSessions(config.projectId, 10);
1697
- const activeSessions = sessions.filter(s => s.status === 'active');
1698
- if (activeSessions.length === 0) {
1699
- (0, messages_1.printInfo)('No Active Sessions', 'There are no active sessions to end.');
2078
+ else {
2079
+ const records = scanSessionRecords(repoRoot);
2080
+ if (records.active.length === 1) {
2081
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[0]));
2082
+ return;
2083
+ }
2084
+ if (records.active.length > 1) {
2085
+ if (!isInteractive()) {
2086
+ endSessionOutput(options, {
2087
+ ok: false,
2088
+ ended: false,
2089
+ mode: 'local',
2090
+ reason: 'multiple_local_sessions_noninteractive',
2091
+ candidates: records.active.map((session) => ({
2092
+ sessionId: session.sessionId,
2093
+ command: `neurcode session end --session-id ${session.sessionId}`,
2094
+ })),
2095
+ malformedRecords: records.malformed,
2096
+ message: 'Multiple active local governance sessions were found; noninteractive selection is disabled.',
2097
+ }, 2);
1700
2098
  return;
1701
2099
  }
1702
- if (activeSessions.length === 1) {
1703
- sessionId = activeSessions[0].sessionId;
1704
- const title = activeSessions[0].title || activeSessions[0].intentDescription || 'Untitled';
1705
- (0, messages_1.printInfo)('Found Active Session', `Ending: ${title}`);
1706
- }
1707
- else {
1708
- // Multiple active sessions - let user choose
1709
- (0, messages_1.printSection)('Multiple Active Sessions');
1710
- activeSessions.forEach((session, index) => {
1711
- const title = session.title || session.intentDescription || 'Untitled';
1712
- console.log(chalk.cyan(` ${index + 1}.`), chalk.white(title));
1713
- console.log(chalk.dim(` ${session.sessionId.substring(0, 20)}...`));
1714
- });
1715
- console.log('');
1716
- const answer = await promptUser(chalk.bold('Select session to end (1-' + activeSessions.length + '): '));
1717
- const choice = parseInt(answer, 10);
1718
- if (choice >= 1 && choice <= activeSessions.length) {
1719
- sessionId = activeSessions[choice - 1].sessionId;
1720
- }
1721
- else {
1722
- (0, messages_1.printError)('Invalid Selection', undefined, ['Please run the command again and select a valid number']);
1723
- process.exit(1);
1724
- }
2100
+ console.log(chalk.bold('Multiple active local governance sessions'));
2101
+ records.active.forEach((session, index) => {
2102
+ console.log(` ${index + 1}. ${session.sessionId} ยท ${truncate(session.contract.goal, 72)}`);
2103
+ });
2104
+ const answer = await prompt(`Select local session to end (1-${records.active.length}): `);
2105
+ const selected = Number.parseInt(answer, 10);
2106
+ if (!Number.isInteger(selected) || selected < 1 || selected > records.active.length) {
2107
+ endSessionOutput(options, {
2108
+ ok: false,
2109
+ ended: false,
2110
+ mode: 'local',
2111
+ reason: 'invalid_local_selection',
2112
+ message: 'No local session was ended.',
2113
+ }, 2);
2114
+ return;
1725
2115
  }
2116
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, records.active[selected - 1]));
2117
+ return;
2118
+ }
2119
+ if (options.local) {
2120
+ endSessionOutput(options, {
2121
+ ok: true,
2122
+ ended: false,
2123
+ mode: 'local',
2124
+ reason: 'no_active_local_session',
2125
+ malformedRecords: records.malformed,
2126
+ message: 'No active local governance session found.',
2127
+ });
2128
+ return;
1726
2129
  }
1727
2130
  }
1728
- if (!sessionId) {
1729
- (0, messages_1.printError)('No Session Specified', undefined, [
1730
- 'No session ID provided and no active session found',
1731
- 'Usage: neurcode session end [session-id]',
1732
- 'Or set a session: neurcode init'
1733
- ]);
1734
- process.exit(1);
2131
+ let config = null;
2132
+ let client = dependencies.cloudClient;
2133
+ if (!client) {
2134
+ config = (0, config_1.loadConfig)();
2135
+ if (!config.apiKey)
2136
+ config.apiKey = (0, config_1.requireApiKey)();
2137
+ client = new api_client_1.ApiClient(config);
1735
2138
  }
1736
- // Get session details first
1737
- try {
1738
- const sessionData = await client.getSession(sessionId);
1739
- const session = sessionData.session;
1740
- if (session.status === 'completed') {
1741
- (0, messages_1.printWarning)('Session Already Completed', `Session "${session.title || session.intentDescription || sessionId}" is already ended.`);
2139
+ let sessionId = options.sessionId;
2140
+ if (!sessionId) {
2141
+ const stateSessionId = (0, state_1.getSessionId)() || undefined;
2142
+ if (stateSessionId && (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId)) {
2143
+ const local = (0, governance_runtime_1.loadSession)(repoRoot, stateSessionId);
2144
+ endSessionOutput(options, await finishLocalGovernanceSession(repoRoot, local));
1742
2145
  return;
1743
2146
  }
1744
- if (session.status === 'cancelled') {
1745
- (0, messages_1.printWarning)('Session Already Cancelled', `Session "${session.title || session.intentDescription || sessionId}" was already cancelled.`);
2147
+ sessionId = stateSessionId;
2148
+ }
2149
+ if (!sessionId) {
2150
+ const sessions = await client.getSessions(options.projectId || config?.projectId, 20);
2151
+ const active = sessions.filter((session) => session.status === 'active');
2152
+ if (active.length === 0) {
2153
+ endSessionOutput(options, {
2154
+ ok: true,
2155
+ ended: false,
2156
+ mode: 'cloud',
2157
+ reason: 'no_active_cloud_session',
2158
+ message: 'No active local or cloud session found.',
2159
+ });
1746
2160
  return;
1747
2161
  }
1748
- // Show session summary
1749
- const title = session.title || session.intentDescription || 'Untitled Session';
1750
- const filesCount = sessionData.files?.length || 0;
1751
- (0, messages_1.printSection)('Session Summary');
1752
- console.log(chalk.white(` Title: ${title}`));
1753
- console.log(chalk.white(` Files Changed: ${filesCount}`));
1754
- console.log(chalk.dim(` Session ID: ${sessionId}`));
1755
- console.log('');
1756
- // Confirm before ending
1757
- const confirm = await promptUser(chalk.bold('End this session? (y/n): '));
1758
- if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
1759
- (0, messages_1.printInfo)('Cancelled', 'Session was not ended.');
2162
+ if (active.length > 1 && !isInteractive()) {
2163
+ endSessionOutput(options, {
2164
+ ok: false,
2165
+ ended: false,
2166
+ mode: 'cloud',
2167
+ reason: 'multiple_cloud_sessions_noninteractive',
2168
+ candidates: active.map((session) => ({
2169
+ sessionId: session.sessionId,
2170
+ command: `neurcode session end --session-id ${session.sessionId}`,
2171
+ })),
2172
+ message: 'Multiple active cloud sessions were found; noninteractive selection is disabled.',
2173
+ }, 2);
1760
2174
  return;
1761
2175
  }
1762
- await client.endSession(sessionId);
1763
- // Clear session ID from local state if it matches the ended session
1764
- try {
1765
- const currentSessionId = (0, state_1.getSessionId)();
1766
- if (currentSessionId === sessionId) {
1767
- const { clearSessionId } = await Promise.resolve().then(() => __importStar(require('../utils/state')));
1768
- clearSessionId();
1769
- }
1770
- }
1771
- catch {
1772
- // Non-critical - continue if state clearing fails
2176
+ if (active.length === 1) {
2177
+ sessionId = active[0].sessionId;
1773
2178
  }
1774
- const firstName = await (0, messages_1.getUserFirstName)();
1775
- await (0, messages_1.printSuccessBanner)('Session Completed', `Great work, ${firstName}! Your session has been marked as complete.`);
1776
- (0, messages_1.printSuccess)('Session Ended Successfully', `"${title}" is now marked as completed.\n View in dashboard: dashboard.neurcode.com`);
1777
- // Display Session ROI Summary
1778
- try {
1779
- // Fetch ROI summary from API
1780
- const apiUrl = config.apiUrl || process.env.NEURCODE_API_URL || 'https://api.neurcode.com';
1781
- const roiUrl = `${apiUrl}/api/v1/roi/summary?timeRange=7d`;
1782
- const roiResponse = await fetch(roiUrl, {
1783
- headers: {
1784
- 'Authorization': `Bearer ${config.apiKey}`,
1785
- 'Content-Type': 'application/json',
1786
- },
1787
- }).catch(() => null);
1788
- if (roiResponse && roiResponse.ok) {
1789
- const roiData = await roiResponse.json().catch(() => null);
1790
- if (roiData && roiData.totalCapitalSaved) {
1791
- const capitalSaved = typeof roiData.totalCapitalSaved === 'string'
1792
- ? parseFloat(roiData.totalCapitalSaved)
1793
- : roiData.totalCapitalSaved;
1794
- const formattedAmount = capitalSaved.toFixed(2);
1795
- const dashboardUrl = 'https://neurcode.com/dashboard';
1796
- console.log('');
1797
- console.log(chalk.cyan('๐Ÿ“Š'), chalk.bold.white('Current Session ROI:'), chalk.green.bold(`+$${formattedAmount}`));
1798
- console.log(chalk.dim(` View full report: ${dashboardUrl}`));
1799
- console.log('');
1800
- }
2179
+ else {
2180
+ active.forEach((session, index) => {
2181
+ const title = session.title || session.intentDescription || 'Untitled';
2182
+ console.log(` ${index + 1}. ${title} ยท ${session.sessionId}`);
2183
+ });
2184
+ const answer = await prompt(`Select cloud session to end (1-${active.length}): `);
2185
+ const selected = Number.parseInt(answer, 10);
2186
+ if (!Number.isInteger(selected) || selected < 1 || selected > active.length) {
2187
+ endSessionOutput(options, {
2188
+ ok: false,
2189
+ ended: false,
2190
+ mode: 'cloud',
2191
+ reason: 'invalid_cloud_selection',
2192
+ message: 'No cloud session was ended.',
2193
+ }, 2);
2194
+ return;
1801
2195
  }
1802
- }
1803
- catch {
1804
- // Silently fail - ROI summary is a nice-to-have
2196
+ sessionId = active[selected - 1].sessionId;
1805
2197
  }
1806
2198
  }
1807
- catch (error) {
1808
- if (error.message?.includes('not found') || error.message?.includes('404')) {
1809
- (0, messages_1.printError)('Session Not Found', error, [
1810
- `Session "${sessionId}" could not be found`,
1811
- 'List your sessions: neurcode session list',
1812
- 'Verify the session ID is correct'
1813
- ]);
1814
- }
1815
- else {
1816
- throw error;
1817
- }
2199
+ if (!sessionId) {
2200
+ endSessionOutput(options, {
2201
+ ok: false,
2202
+ ended: false,
2203
+ mode: 'cloud',
2204
+ reason: 'cloud_session_not_resolved',
2205
+ message: 'No cloud session could be resolved.',
2206
+ }, 2);
2207
+ return;
1818
2208
  }
1819
- }
1820
- catch (error) {
1821
- if (error instanceof Error) {
1822
- if (error.message.includes('401') || error.message.includes('403')) {
1823
- await (0, messages_1.printAuthError)(error);
2209
+ const sessionData = await client.getSession(sessionId);
2210
+ const session = sessionData.session;
2211
+ if (session.status === 'completed' || session.status === 'cancelled') {
2212
+ endSessionOutput(options, {
2213
+ ok: true,
2214
+ ended: false,
2215
+ mode: 'cloud',
2216
+ status: session.status,
2217
+ sessionId,
2218
+ message: `Cloud session ${sessionId} is already ${session.status}.`,
2219
+ });
2220
+ return;
2221
+ }
2222
+ if (isInteractive()) {
2223
+ const confirm = await prompt(`End cloud session ${sessionId}? (y/n): `);
2224
+ if (!['y', 'yes'].includes(confirm.toLowerCase())) {
2225
+ endSessionOutput(options, {
2226
+ ok: true,
2227
+ ended: false,
2228
+ mode: 'cloud',
2229
+ reason: 'operator_cancelled',
2230
+ sessionId,
2231
+ message: 'Cloud session was not ended.',
2232
+ });
2233
+ return;
1824
2234
  }
1825
- else {
1826
- (0, messages_1.printError)('Failed to End Session', error);
2235
+ }
2236
+ await client.endSession(sessionId);
2237
+ try {
2238
+ if ((0, state_1.getSessionId)() === sessionId) {
2239
+ const { clearSessionId } = await Promise.resolve().then(() => __importStar(require('../utils/state')));
2240
+ clearSessionId();
1827
2241
  }
1828
2242
  }
1829
- else {
1830
- (0, messages_1.printError)('Failed to End Session', String(error));
2243
+ catch {
2244
+ // Legacy local cloud pointer cleanup is best-effort.
1831
2245
  }
1832
- process.exit(1);
2246
+ endSessionOutput(options, {
2247
+ ok: true,
2248
+ ended: true,
2249
+ mode: 'cloud',
2250
+ status: 'completed',
2251
+ sessionId,
2252
+ message: `Cloud session ${sessionId} ended successfully.`,
2253
+ });
2254
+ }
2255
+ catch (error) {
2256
+ const message = error instanceof Error ? error.message : String(error);
2257
+ const notFound = /not found|404/i.test(message);
2258
+ endSessionOutput(options, {
2259
+ ok: false,
2260
+ ended: false,
2261
+ mode: 'unknown',
2262
+ reason: notFound ? 'session_not_found' : 'session_end_failed',
2263
+ sessionId: options.sessionId || null,
2264
+ error: message,
2265
+ message: notFound
2266
+ ? `Session ${options.sessionId || ''} was not found locally or in the cloud.`.trim()
2267
+ : `Failed to end session: ${message}`,
2268
+ }, notFound ? 2 : 1);
1833
2269
  }
1834
2270
  }
1835
2271
  /**