@aopslabs/aops 0.3.23 → 0.3.25

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 (68) hide show
  1. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/SKILL.md +0 -1
  2. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/agentspace/SKILL.md +0 -6
  3. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/collaborative-work/SKILL.md +1 -2
  4. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/working-disciplines/SKILL.md +2 -5
  5. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agentspace.md +1 -16
  6. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-cli.md +4 -33
  7. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/working-disciplines.md +32 -138
  8. package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/manifest.json +17 -22
  9. package/agent-assets-release/agent-assets/inventory.json +5 -5
  10. package/agent-assets-release/agent-assets/projection.json +1 -1
  11. package/agent-assets-release/agent-assets-release.json +48 -53
  12. package/agent-assets-release/agent-assets-release.sigstore.json +27 -31
  13. package/assets/agent-assets/core/SKILL.md +0 -1
  14. package/assets/agent-assets/core/references/agentspace/SKILL.md +0 -6
  15. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +1 -2
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +2 -5
  17. package/assets/agent-assets/core/user-guides/agentspace.md +1 -16
  18. package/assets/agent-assets/core/user-guides/aops-cli.md +4 -33
  19. package/assets/agent-assets/core/user-guides/working-disciplines.md +32 -138
  20. package/dist/commands/checkpoint.js +4 -23
  21. package/dist/commands/community-server.js +212 -6
  22. package/dist/commands/db.js +145 -0
  23. package/dist/commands/experience.js +0 -2
  24. package/dist/commands/global-update.js +447 -22
  25. package/dist/commands/memory.js +8 -2
  26. package/dist/commands/official-catalog.js +1 -1
  27. package/dist/commands/plan.js +2 -2
  28. package/dist/commands/pm/index.js +39 -3
  29. package/dist/commands/pm/projectman.js +281 -8
  30. package/dist/commands/start-disciplines.js +8 -21
  31. package/dist/commands/start.js +20 -504
  32. package/dist/lib/agent-assets/native-fs.js +2 -2
  33. package/dist/lib/community-cockpit-lifecycle.js +95 -1
  34. package/dist/lib/community-migration-snapshot.js +43 -2
  35. package/dist/lib/community-native-backup.js +386 -0
  36. package/dist/lib/community-native-lifecycle.js +147 -5
  37. package/dist/lib/community-native-migration.js +20 -0
  38. package/dist/main.js +2 -2
  39. package/dist/seeds/agents-md.js +1 -1
  40. package/dist/utils/playbook-workspace.js +5 -33
  41. package/dist/utils/session-guidance.js +0 -7
  42. package/native/bin/darwin-arm64/aops-agent-assets-fs +0 -0
  43. package/native/bin/darwin-x64/aops-agent-assets-fs +0 -0
  44. package/native/bin/linux-arm64/aops-agent-assets-fs +0 -0
  45. package/native/bin/linux-x64/aops-agent-assets-fs +0 -0
  46. package/native/bin/win32-x64/aops-agent-assets-fs.exe +0 -0
  47. package/native/manifest.json +6 -6
  48. package/native/qualifications/win32-x64.json +2 -2
  49. package/native/tui/darwin-arm64/aops-tui +0 -0
  50. package/native/tui/linux-x64/aops-tui +0 -0
  51. package/native/tui/win32-x64/aops-tui.exe +0 -0
  52. package/package.json +21 -9
  53. package/agent-assets-release/agent-assets/core/647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09/files/references/mission/SKILL.md +0 -54
  54. package/assets/agent-assets/core/references/mission/SKILL.md +0 -54
  55. package/dist/commands/mission.js +0 -562
  56. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/aops-cli-core/SKILL.md +0 -0
  57. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/chatv3/SKILL.md +0 -0
  58. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/discuss/SKILL.md +0 -0
  59. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/docman/SKILL.md +0 -0
  60. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/projectman/SKILL.md +0 -0
  61. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/sys/SKILL.md +0 -0
  62. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/references/view/SKILL.md +0 -0
  63. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/agent-assets.md +0 -0
  64. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/aops-system.md +0 -0
  65. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/chatv3.md +0 -0
  66. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/docman.md +0 -0
  67. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/projectman.md +0 -0
  68. /package/agent-assets-release/agent-assets/core/{647c6e84fbc48f9a36945d81660f4d2a5241f58a53a18054365bfc16868b0f09 → bc42dd7792dbe66040e99a7ef3d80b3bc788c72b369b5a065cf936aef29f53fb}/files/user-guides/sys.md +0 -0
@@ -29,11 +29,13 @@ const GLOBAL_UPDATE_EXECUTOR_MAX_BYTES = 512 * 1024 * 1024;
29
29
  class ChildCommandError extends Error {
30
30
  status;
31
31
  diagnostic;
32
- constructor(command, status, diagnostic) {
32
+ stdout;
33
+ constructor(command, status, diagnostic, stdout = '') {
33
34
  super(`${path.basename(command)} failed with status ${status ?? 'unknown'}`);
34
35
  this.name = 'ChildCommandError';
35
36
  this.status = status;
36
37
  this.diagnostic = diagnostic;
38
+ this.stdout = stdout;
37
39
  }
38
40
  }
39
41
  function cleanEnvironment() {
@@ -69,7 +71,7 @@ function defaultRunner(command, args, cwd = process.cwd()) {
69
71
  }
70
72
  catch {
71
73
  }
72
- throw new ChildCommandError(command, result.status, diagnostic);
74
+ throw new ChildCommandError(command, result.status, diagnostic, result.stdout ?? '');
73
75
  }
74
76
  return result.stdout;
75
77
  }
@@ -280,10 +282,33 @@ export function inspectGlobalUpdate(run = defaultRunner) {
280
282
  function runInstalledJson(run, cliPath, args) {
281
283
  return parseJson(run(process.execPath, [cliPath, ...args]).trim(), args.join('_'));
282
284
  }
285
+ function childCommandStdout(error) {
286
+ const stdout = error?.stdout;
287
+ return typeof stdout === 'string' && stdout.trim() !== '' ? stdout : undefined;
288
+ }
289
+ function readServiceStatus(run, cliPath, service) {
290
+ try {
291
+ return runInstalledJson(run, cliPath, [service, 'status', '--json']);
292
+ }
293
+ catch (error) {
294
+ const stdout = childCommandStdout(error);
295
+ if (stdout === undefined)
296
+ throw error;
297
+ try {
298
+ const parsed = JSON.parse(stdout.trim());
299
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
300
+ throw error;
301
+ return parsed;
302
+ }
303
+ catch {
304
+ throw error;
305
+ }
306
+ }
307
+ }
283
308
  function serviceSnapshot(run, cliPath) {
284
309
  return {
285
- server: runInstalledJson(run, cliPath, ['server', 'status', '--json']),
286
- cockpit: runInstalledJson(run, cliPath, ['cockpit', 'status', '--json']),
310
+ server: readServiceStatus(run, cliPath, 'server'),
311
+ cockpit: readServiceStatus(run, cliPath, 'cockpit'),
287
312
  };
288
313
  }
289
314
  function assertHealthyServices(services) {
@@ -565,6 +590,7 @@ export function dispatchGlobalUpdate(options, run = defaultRunner) {
565
590
  rollbackPerformed: false,
566
591
  error: safeUpdateError(error),
567
592
  events: [],
593
+ eventTimes: [],
568
594
  });
569
595
  }
570
596
  throw new Error(`global update candidate executor failed; diagnostic record: ${resultPath}`, { cause: error });
@@ -652,24 +678,161 @@ function rollbackServerApplication(run, cliPath, updateId, serverPath, expectedR
652
678
  const defaultExecutionDependencies = {
653
679
  fetch: globalThis.fetch.bind(globalThis),
654
680
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
681
+ now: () => new Date(),
655
682
  };
683
+ class GlobalUpdateEventLog {
684
+ now;
685
+ names = [];
686
+ times = [];
687
+ constructor(now) {
688
+ this.now = now;
689
+ }
690
+ push(event) {
691
+ this.names.push(event);
692
+ this.times.push(this.now().toISOString());
693
+ }
694
+ includes(event) {
695
+ return this.names.includes(event);
696
+ }
697
+ get events() {
698
+ return [...this.names];
699
+ }
700
+ get eventTimes() {
701
+ return [...this.times];
702
+ }
703
+ }
656
704
  class StartupHealthPending extends Error {
657
- constructor(cause) {
705
+ detail;
706
+ constructor(cause, detail) {
658
707
  super('global_update_health_startup_pending', { cause });
708
+ this.detail = detail;
709
+ }
710
+ }
711
+ function serviceHealthFacts(services) {
712
+ const liveness = services.server.liveness;
713
+ return {
714
+ serverState: services.server.runtimeState,
715
+ serverHealth: liveness?.health,
716
+ serverReason: services.server.error,
717
+ cockpitState: services.cockpit.runtimeState,
718
+ cockpitHealth: services.cockpit.health,
719
+ cockpitReason: services.cockpit.reason,
720
+ };
721
+ }
722
+ const HEALTH_SUBJECTS = new Set([
723
+ 'install-readback', 'server-status', 'cockpit-status', 'service-health',
724
+ 'server', 'cockpit', 'cockpit-proxy',
725
+ ]);
726
+ const HEALTH_PHASES = new Set(['startup', 'steady']);
727
+ const SAFE_STATE = /^[a-z][a-z0-9-]{0,40}$/u;
728
+ const SAFE_REASON_CODE = /\b(?:aops|community|global_update)_[a-z0-9_]+\b/iu;
729
+ function sanitizeHealthDetail(detail) {
730
+ const safe = {
731
+ subject: typeof detail.subject === 'string' && HEALTH_SUBJECTS.has(detail.subject)
732
+ ? detail.subject
733
+ : 'unrecognized',
734
+ };
735
+ if (typeof detail.phase === 'string' && HEALTH_PHASES.has(detail.phase))
736
+ safe.phase = detail.phase;
737
+ if (typeof detail.reachable === 'boolean')
738
+ safe.reachable = detail.reachable;
739
+ for (const key of ['sample', 'attempts', 'childStatus', 'status']) {
740
+ const value = detail[key];
741
+ if (typeof value === 'number' && Number.isSafeInteger(value))
742
+ safe[key] = value;
743
+ }
744
+ for (const key of ['serverState', 'serverHealth', 'cockpitState', 'cockpitHealth']) {
745
+ const value = detail[key];
746
+ if (value === undefined || value === null)
747
+ continue;
748
+ safe[key] = typeof value === 'string' && SAFE_STATE.test(value) ? value : 'unrecognized';
659
749
  }
750
+ for (const key of ['serverReason', 'cockpitReason']) {
751
+ const value = detail[key];
752
+ if (value === undefined || value === null)
753
+ continue;
754
+ safe[key] = typeof value === 'string'
755
+ ? SAFE_REASON_CODE.exec(value)?.[0]?.toLowerCase() ?? 'unrecognized'
756
+ : 'unrecognized';
757
+ }
758
+ return safe;
759
+ }
760
+ function childCommandStatus(error) {
761
+ const status = error?.status;
762
+ return typeof status === 'number' ? status : undefined;
763
+ }
764
+ class ForwardSchemaRetainsTarget extends Error {
765
+ applicationRollbackError;
766
+ constructor(applicationRollbackError) {
767
+ super('global_update_forward_schema_retains_target');
768
+ this.applicationRollbackError = applicationRollbackError;
769
+ }
770
+ }
771
+ export class StableHealthFailure extends Error {
772
+ healthFailure;
773
+ constructor(detail) {
774
+ const safe = sanitizeHealthDetail(detail);
775
+ super(`global_update_stable_health_failed:${String(safe.subject)}`);
776
+ this.healthFailure = Object.freeze(safe);
777
+ }
778
+ }
779
+ export function resolveRecoveryClosure(state) {
780
+ if (!state.applicationSideEffectPossible)
781
+ return 'prior';
782
+ return state.applicationRolledBack ? 'prior' : 'target';
783
+ }
784
+ const APPLICATION_PRE_EFFECT_CODES = new Set([
785
+ 'community_native_application_update_content_unchanged',
786
+ ]);
787
+ export function applicationSideEffectPossible(attempted, updateError) {
788
+ if (!attempted)
789
+ return false;
790
+ const diagnostic = nestedCommunityDiagnostic(updateError);
791
+ if (!diagnostic)
792
+ return true;
793
+ const cause = diagnostic.cause;
794
+ const codes = [diagnostic.code, cause?.code];
795
+ return !codes.some((code) => typeof code === 'string' && APPLICATION_PRE_EFFECT_CODES.has(code));
660
796
  }
661
797
  async function assertStableHealth(run, plan, execution) {
662
798
  const evidence = [];
799
+ const fail = (error, startup, detail) => {
800
+ if (startup)
801
+ throw new StartupHealthPending(error, detail);
802
+ throw new StableHealthFailure({ ...detail, phase: 'steady' });
803
+ };
663
804
  const collectSample = async (sample, startup) => {
664
- const installed = inspectGlobalInstall(run);
805
+ let installed;
806
+ try {
807
+ installed = inspectGlobalInstall(run);
808
+ }
809
+ catch (error) {
810
+ fail(error, startup, { subject: 'install-readback', sample, childStatus: childCommandStatus(error) });
811
+ throw error;
812
+ }
665
813
  let services;
666
814
  try {
667
- services = serviceSnapshot(run, installed.cliPath);
815
+ services = {
816
+ server: readServiceStatus(run, installed.cliPath, 'server'),
817
+ cockpit: { runtimeState: 'not-read' },
818
+ };
819
+ }
820
+ catch (error) {
821
+ fail(error, startup, { subject: 'server-status', sample, childStatus: childCommandStatus(error) });
822
+ throw error;
823
+ }
824
+ try {
825
+ services = { ...services, cockpit: readServiceStatus(run, installed.cliPath, 'cockpit') };
826
+ }
827
+ catch (error) {
828
+ fail(error, startup, { subject: 'cockpit-status', sample, childStatus: childCommandStatus(error) });
829
+ throw error;
830
+ }
831
+ try {
668
832
  assertHealthyServices(services);
669
833
  }
670
834
  catch (error) {
671
- if (startup)
672
- throw new StartupHealthPending(error);
835
+ fail(error, startup, { subject: 'service-health', sample, ...serviceHealthFacts(services) });
673
836
  throw error;
674
837
  }
675
838
  const serverOrigin = validateLoopbackOrigin(services.server.origin, 'AOPS Server origin');
@@ -685,14 +848,12 @@ async function assertStableHealth(run, plan, execution) {
685
848
  response = await execution.fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(15_000) });
686
849
  }
687
850
  catch (error) {
688
- if (startup)
689
- throw new StartupHealthPending(error);
851
+ fail(error, startup, { subject: name, sample, reachable: false });
690
852
  throw error;
691
853
  }
692
- if (response.status !== 200 && startup) {
693
- throw new StartupHealthPending(new Error(`${name} health returned ${response.status}`));
854
+ if (response.status !== 200) {
855
+ fail(new Error(`${name} health returned ${response.status}`), startup, { subject: name, sample, reachable: true, status: response.status });
694
856
  }
695
- assert.equal(response.status, 200, `${name} health returned ${response.status}`);
696
857
  const body = await response.arrayBuffer();
697
858
  endpoints.push({ name, url, status: response.status, bytes: body.byteLength });
698
859
  }
@@ -708,7 +869,11 @@ async function assertStableHealth(run, plan, execution) {
708
869
  catch (error) {
709
870
  if (!(error instanceof StartupHealthPending))
710
871
  throw error;
711
- startupError = error.cause ?? error;
872
+ startupError = new StableHealthFailure({
873
+ ...error.detail,
874
+ phase: 'startup',
875
+ attempts: attempt,
876
+ });
712
877
  if (attempt < plan.safety.healthStartupAttempts) {
713
878
  await execution.sleep(plan.safety.healthStartupIntervalMs);
714
879
  }
@@ -755,6 +920,9 @@ function nestedCommunityDiagnostic(error) {
755
920
  return undefined;
756
921
  }
757
922
  function safeUpdateError(error) {
923
+ if (error instanceof StableHealthFailure) {
924
+ return { code: 'global_update_stable_health_failed', healthFailure: error.healthFailure };
925
+ }
758
926
  const diagnostic = nestedCommunityDiagnostic(error);
759
927
  if (diagnostic) {
760
928
  const details = diagnostic.details;
@@ -785,6 +953,160 @@ function writeTerminalUpdateResult(plan, result) {
785
953
  const resultPath = path.join(plan.operationRoot, RESULT_FILE);
786
954
  writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
787
955
  }
956
+ const RECOVERY_CONTRACT = 'aops.global-update-recovery/v1';
957
+ const RECOVERY_RESULT_PATTERN = /^update-recovery-(0|[1-9]\d*)\.json$/u;
958
+ const RECOVERABLE_TERMINAL_STATUSES = new Set(['recovery-required']);
959
+ function recoveryAttemptFiles(plan) {
960
+ const operationRoot = plainDirectory(plan.operationRoot, 'global update operation root');
961
+ const attempts = [];
962
+ for (const entry of readdirSync(operationRoot)) {
963
+ const match = RECOVERY_RESULT_PATTERN.exec(entry);
964
+ if (match)
965
+ attempts.push({ attempt: Number(match[1]), path: path.join(operationRoot, entry) });
966
+ }
967
+ return attempts.sort((left, right) => left.attempt - right.attempt);
968
+ }
969
+ function nextRecoveryResultPath(plan) {
970
+ const attempts = recoveryAttemptFiles(plan);
971
+ const next = attempts.length === 0 ? 1 : attempts[attempts.length - 1].attempt + 1;
972
+ return path.join(plan.operationRoot, `update-recovery-${next}.json`);
973
+ }
974
+ function sameClosure(installed, expected) {
975
+ try {
976
+ assert.deepEqual(installed, expected);
977
+ return true;
978
+ }
979
+ catch {
980
+ return false;
981
+ }
982
+ }
983
+ function writeRecoveryResult(resultPath, result) {
984
+ writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
985
+ }
986
+ function preflightGlobalUpdateRecovery(options, run) {
987
+ const plan = readGlobalUpdatePlan(options.plan);
988
+ assert.equal(options.confirm, plan.planId, 'global update recovery confirmation does not match the exact plan');
989
+ const terminalPath = path.join(plan.operationRoot, RESULT_FILE);
990
+ assert.equal(existsSync(terminalPath), true, 'global_update_recovery_requires_terminal_result');
991
+ const terminal = parseJson(readFileSync(terminalPath, 'utf8'), 'global_update_result');
992
+ assert.equal(terminal.planId, plan.planId, 'terminal result belongs to a different plan');
993
+ const terminalStatus = String(terminal.status);
994
+ assert.equal(RECOVERABLE_TERMINAL_STATUSES.has(terminalStatus), true, `global_update_recovery_status_not_recoverable:${terminalStatus}`);
995
+ const installed = inspectGlobalInstall(run);
996
+ assert.equal(installed.globalPrefix, plan.globalPrefix, 'global npm prefix changed after update planning');
997
+ const installedClosure = [installed.packages, installed.topology];
998
+ const atTarget = sameClosure(installedClosure, [plan.target.packages, plan.target.topology]);
999
+ const atPrior = sameClosure(installedClosure, [plan.prior.packages, plan.prior.topology]);
1000
+ assert.equal(atTarget || atPrior, true, 'global_update_recovery_closure_drifted');
1001
+ const succeeded = recoveryAttemptFiles(plan)
1002
+ .map((file) => ({
1003
+ ...file,
1004
+ record: parseJson(readFileSync(file.path, 'utf8'), 'global_update_recovery_result'),
1005
+ }))
1006
+ .find((entry) => entry.record.status === 'recovered');
1007
+ if (succeeded) {
1008
+ assert.equal(atTarget, true, 'global_update_recovery_recorded_but_closure_moved');
1009
+ }
1010
+ return { plan, terminalStatus, installed, atTarget, atPrior, succeeded };
1011
+ }
1012
+ export function dispatchGlobalUpdateRecovery(options, run = defaultRunner) {
1013
+ const preflight = preflightGlobalUpdateRecovery(options, run);
1014
+ const { plan } = preflight;
1015
+ if (preflight.succeeded)
1016
+ return preflight.succeeded.record;
1017
+ const resultPath = nextRecoveryResultPath(plan);
1018
+ let executorStarted = false;
1019
+ try {
1020
+ const { receipt, receiptPath } = stageCandidateGlobalUpdateExecutor(plan, run);
1021
+ executorStarted = true;
1022
+ return parseJson(run(process.execPath, [
1023
+ receipt.entrypoint,
1024
+ 'update',
1025
+ 'recover',
1026
+ '--plan',
1027
+ options.plan,
1028
+ '--confirm',
1029
+ options.confirm,
1030
+ '--executor-receipt',
1031
+ receiptPath,
1032
+ '--json',
1033
+ ]).trim(), 'global_update_recovery_result');
1034
+ }
1035
+ catch (error) {
1036
+ if (!existsSync(resultPath)) {
1037
+ writeRecoveryResult(resultPath, {
1038
+ schemaVersion: 1,
1039
+ contract: RECOVERY_CONTRACT,
1040
+ status: executorStarted ? 'executor-result-missing' : 'executor-staging-failed',
1041
+ planId: plan.planId,
1042
+ failedStage: executorStarted ? 'candidate-executor' : 'stage-candidate-executor',
1043
+ globalState: executorStarted ? 'unknown-requires-inspection' : 'unchanged',
1044
+ error: safeUpdateError(error),
1045
+ events: [],
1046
+ eventTimes: [],
1047
+ });
1048
+ }
1049
+ throw new Error(`global update recovery executor failed; diagnostic record: ${resultPath}`, { cause: error });
1050
+ }
1051
+ }
1052
+ export async function recoverGlobalUpdate(options, run = defaultRunner, execution = defaultExecutionDependencies) {
1053
+ const preflight = preflightGlobalUpdateRecovery(options, run);
1054
+ const { plan, installed: current, atTarget, terminalStatus } = preflight;
1055
+ const installedCliRoot = plainDirectory(path.dirname(path.dirname(current.cliPath)), 'installed global CLI root');
1056
+ const invokedEntrypoint = plainFile(process.argv[1], 'invoked AOPS CLI entrypoint');
1057
+ const relativeToInstalled = path.relative(installedCliRoot, invokedEntrypoint);
1058
+ assert.equal(relativeToInstalled.startsWith('..') || path.isAbsolute(relativeToInstalled), true, 'global_update_recovery_must_not_run_from_the_replaced_cli');
1059
+ if (preflight.succeeded)
1060
+ return preflight.succeeded.record;
1061
+ const resultPath = nextRecoveryResultPath(plan);
1062
+ const events = new GlobalUpdateEventLog(execution.now ?? (() => new Date()));
1063
+ try {
1064
+ stopService(run, current.cliPath, 'cockpit', true);
1065
+ stopService(run, current.cliPath, 'server', true);
1066
+ if (atTarget) {
1067
+ events.push('retained:target-closure');
1068
+ }
1069
+ else {
1070
+ installGlobalUpdateClosure(run, plan, plan.candidates, plan.target.topology);
1071
+ events.push('installed:target-closure');
1072
+ }
1073
+ const restored = inspectGlobalInstall(run);
1074
+ assert.deepEqual(restored.packages, plan.target.packages, 'recovered closure is not the exact target');
1075
+ assert.deepEqual(restored.topology, plan.target.topology, 'recovered topology is not canonical');
1076
+ startService(run, restored.cliPath, 'server');
1077
+ events.push('started:server');
1078
+ startService(run, restored.cliPath, 'cockpit');
1079
+ events.push('started:cockpit');
1080
+ const healthEvidence = await assertStableHealth(run, plan, execution);
1081
+ const result = {
1082
+ schemaVersion: 1,
1083
+ contract: RECOVERY_CONTRACT,
1084
+ status: 'recovered',
1085
+ planId: plan.planId,
1086
+ installed: plan.target.packages,
1087
+ recoveredFrom: terminalStatus,
1088
+ events: events.events,
1089
+ eventTimes: events.eventTimes,
1090
+ healthEvidence,
1091
+ };
1092
+ writeRecoveryResult(resultPath, result);
1093
+ return result;
1094
+ }
1095
+ catch (recoveryError) {
1096
+ writeRecoveryResult(resultPath, {
1097
+ schemaVersion: 1,
1098
+ contract: RECOVERY_CONTRACT,
1099
+ status: 'recovery-failed',
1100
+ planId: plan.planId,
1101
+ recoveredFrom: terminalStatus,
1102
+ globalState: 'unknown-requires-inspection',
1103
+ error: safeUpdateError(recoveryError),
1104
+ events: events.events,
1105
+ eventTimes: events.eventTimes,
1106
+ });
1107
+ throw recoveryError;
1108
+ }
1109
+ }
788
1110
  export async function applyGlobalUpdate(options, run = defaultRunner, execution = defaultExecutionDependencies) {
789
1111
  const plan = readGlobalUpdatePlan(options.plan);
790
1112
  assert.equal(options.confirm, plan.planId, 'global update confirmation does not match the exact plan');
@@ -794,8 +1116,9 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
794
1116
  assert.deepEqual(current.topology, plan.prior.topology, 'global package topology changed after update planning');
795
1117
  assert.equal(current.globalPrefix, plan.globalPrefix, 'global npm prefix changed after update planning');
796
1118
  assertHealthyServices(serviceSnapshot(run, current.cliPath));
797
- const events = [];
1119
+ const events = new GlobalUpdateEventLog(execution.now ?? (() => new Date()));
798
1120
  let applicationUpdateId = null;
1121
+ let applicationUpdateAttempted = false;
799
1122
  let failedStage = 'stop-cockpit';
800
1123
  try {
801
1124
  stopService(run, current.cliPath, 'cockpit');
@@ -817,6 +1140,7 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
817
1140
  }
818
1141
  else {
819
1142
  failedStage = 'update-server-application';
1143
+ applicationUpdateAttempted = true;
820
1144
  applicationUpdateId = updateServerApplication(run, updated.cliPath, {
821
1145
  prior: plan.prior.packages.server,
822
1146
  target: plan.target.packages.server,
@@ -835,7 +1159,8 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
835
1159
  planId: plan.planId,
836
1160
  installed: updated.packages,
837
1161
  rollbackPerformed: false,
838
- events,
1162
+ events: events.events,
1163
+ eventTimes: events.eventTimes,
839
1164
  healthEvidence,
840
1165
  next: 'Exit and reopen the AOPS TUI to run the newly installed binary.',
841
1166
  };
@@ -844,6 +1169,7 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
844
1169
  }
845
1170
  catch (updateError) {
846
1171
  const updateStage = failedStage;
1172
+ const sideEffectPossible = applicationSideEffectPossible(applicationUpdateAttempted, updateError);
847
1173
  events.push('rollback:started');
848
1174
  let rollbackError;
849
1175
  try {
@@ -854,19 +1180,89 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
854
1180
  const restored = inspectGlobalInstall(run);
855
1181
  assert.deepEqual(restored.packages, plan.prior.packages, 'rollback package closure is not exact');
856
1182
  assert.deepEqual(restored.topology, plan.prior.topology, 'rollback package topology is not exact');
1183
+ let applicationRollbackError;
857
1184
  if (applicationUpdateId) {
858
- rollbackServerApplication(run, restored.cliPath, applicationUpdateId, restored.serverPath, plan.prior.packages.server);
859
- events.push('rolled-back:server-application');
1185
+ try {
1186
+ rollbackServerApplication(run, restored.cliPath, applicationUpdateId, restored.serverPath, plan.prior.packages.server);
1187
+ events.push('rolled-back:server-application');
1188
+ }
1189
+ catch (error) {
1190
+ applicationRollbackError = error;
1191
+ events.push('failed:server-application-rollback');
1192
+ }
1193
+ }
1194
+ else if (sideEffectPossible) {
1195
+ applicationRollbackError = new Error('global_update_application_outcome_unrecorded');
1196
+ events.push('failed:server-application-outcome-unrecorded');
860
1197
  }
861
1198
  else {
862
1199
  startService(run, restored.cliPath, 'server');
863
1200
  events.push('started:server-restore');
864
1201
  }
1202
+ if (resolveRecoveryClosure({
1203
+ applicationSideEffectPossible: sideEffectPossible,
1204
+ applicationRolledBack: applicationUpdateId !== null && applicationRollbackError === undefined,
1205
+ }) === 'target') {
1206
+ throw new ForwardSchemaRetainsTarget(applicationRollbackError);
1207
+ }
865
1208
  startService(run, restored.cliPath, 'cockpit');
866
1209
  await assertStableHealth(run, plan, execution);
867
1210
  events.push('rollback:healthy');
868
1211
  }
869
1212
  catch (rollbackError) {
1213
+ if (rollbackError instanceof ForwardSchemaRetainsTarget) {
1214
+ try {
1215
+ installGlobalUpdateClosure(run, plan, plan.candidates, plan.target.topology);
1216
+ events.push('reinstalled:target-closure');
1217
+ const retained = inspectGlobalInstall(run);
1218
+ assert.deepEqual(retained.packages, plan.target.packages, 'retained target closure is not exact');
1219
+ assert.deepEqual(retained.topology, plan.target.topology, 'retained target topology is not canonical');
1220
+ startService(run, retained.cliPath, 'server');
1221
+ events.push('started:server-target-retained');
1222
+ startService(run, retained.cliPath, 'cockpit');
1223
+ events.push('started:cockpit-target-retained');
1224
+ await assertStableHealth(run, plan, execution);
1225
+ events.push('target-retained:healthy');
1226
+ }
1227
+ catch (retainError) {
1228
+ writeTerminalUpdateResult(plan, {
1229
+ schemaVersion: 1,
1230
+ contract: RESULT_CONTRACT,
1231
+ status: 'recovery-required',
1232
+ planId: plan.planId,
1233
+ failedStage: updateStage,
1234
+ rollbackPerformed: false,
1235
+ rollback: {
1236
+ status: 'failed',
1237
+ reason: 'application_rollback_failed_forward_schema',
1238
+ error: safeUpdateError(rollbackError.applicationRollbackError),
1239
+ },
1240
+ targetRetention: { status: 'failed', error: safeUpdateError(retainError) },
1241
+ error: safeUpdateError(updateError),
1242
+ events: events.events,
1243
+ eventTimes: events.eventTimes,
1244
+ });
1245
+ throw new AggregateError([updateError, rollbackError.applicationRollbackError, retainError], `global update failed, application rollback failed, and the schema-compatible target closure could not be restarted; services may remain stopped; recovery artifacts remain at ${plan.operationRoot}`);
1246
+ }
1247
+ writeTerminalUpdateResult(plan, {
1248
+ schemaVersion: 1,
1249
+ contract: RESULT_CONTRACT,
1250
+ status: 'failed-target-retained',
1251
+ planId: plan.planId,
1252
+ failedStage: updateStage,
1253
+ installed: plan.target.packages,
1254
+ rollbackPerformed: false,
1255
+ rollback: {
1256
+ status: 'not-applicable',
1257
+ reason: 'application_rollback_failed_forward_schema',
1258
+ applicationRollbackError: safeUpdateError(rollbackError.applicationRollbackError),
1259
+ },
1260
+ error: safeUpdateError(updateError),
1261
+ events: events.events,
1262
+ eventTimes: events.eventTimes,
1263
+ });
1264
+ throw new AggregateError([updateError, rollbackError.applicationRollbackError], 'global update failed and the application could not be rolled back; the schema-compatible target closure was reinstated and healthy services were restarted');
1265
+ }
870
1266
  try {
871
1267
  const restoredForRecovery = inspectGlobalInstall(run);
872
1268
  assert.deepEqual(restoredForRecovery.packages, plan.prior.packages, 'service recovery package closure is not exact');
@@ -891,7 +1287,8 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
891
1287
  rollback: { status: 'failed', error: safeUpdateError(rollbackError) },
892
1288
  serviceRecovery: { status: 'failed', error: safeUpdateError(serviceRecoveryError) },
893
1289
  error: safeUpdateError(updateError),
894
- events,
1290
+ events: events.events,
1291
+ eventTimes: events.eventTimes,
895
1292
  });
896
1293
  throw new AggregateError([updateError, rollbackError, serviceRecoveryError], `global update, automatic rollback, and service recovery failed; services may remain stopped; recovery artifacts remain at ${plan.operationRoot}`);
897
1294
  }
@@ -909,7 +1306,8 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
909
1306
  applicationRollbackError: safeUpdateError(rollbackError),
910
1307
  },
911
1308
  error: safeUpdateError(updateError),
912
- events,
1309
+ events: events.events,
1310
+ eventTimes: events.eventTimes,
913
1311
  });
914
1312
  throw new AggregateError([updateError, rollbackError], `global update and application rollback failed; prior exact packages and healthy services were restored; recovery artifacts remain at ${plan.operationRoot}`);
915
1313
  }
@@ -923,7 +1321,8 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
923
1321
  rollbackPerformed: true,
924
1322
  rollback: { status: 'healthy' },
925
1323
  error: safeUpdateError(updateError),
926
- events,
1324
+ events: events.events,
1325
+ eventTimes: events.eventTimes,
927
1326
  });
928
1327
  throw new Error('global update failed; the prior exact packages were restored and are healthy', { cause: updateError });
929
1328
  }
@@ -931,6 +1330,22 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
931
1330
  function output(value, json) {
932
1331
  process.stdout.write(`${JSON.stringify(value, null, json ? 0 : 2)}\n`);
933
1332
  }
1333
+ const defaultRecoveryCommandDependencies = {
1334
+ dispatch: dispatchGlobalUpdateRecovery,
1335
+ recover: recoverGlobalUpdate,
1336
+ readPlan: readGlobalUpdatePlan,
1337
+ validateExecutor: validateCandidateExecutorInvocation,
1338
+ emit: output,
1339
+ };
1340
+ export async function runGlobalUpdateRecoveryCommand(options, dependencies = defaultRecoveryCommandDependencies) {
1341
+ if (typeof options.executorReceipt === 'string') {
1342
+ const plan = dependencies.readPlan(options.plan);
1343
+ dependencies.validateExecutor(plan, options.executorReceipt);
1344
+ dependencies.emit(await dependencies.recover(options), options.json === true);
1345
+ return;
1346
+ }
1347
+ dependencies.emit(dependencies.dispatch(options), options.json === true);
1348
+ }
934
1349
  export function makeGlobalUpdateCommand() {
935
1350
  const command = new Command('update').description('Check, prepare, and apply a guarded global AOPS npm update');
936
1351
  command
@@ -960,6 +1375,16 @@ export function makeGlobalUpdateCommand() {
960
1375
  }
961
1376
  output(dispatchGlobalUpdate(options), options.json === true);
962
1377
  });
1378
+ command
1379
+ .command('recover')
1380
+ .description('Finish one stranded exact plan at its target closure after a failed apply')
1381
+ .requiredOption('--plan <path>', 'Exact update-plan.json path returned by prepare')
1382
+ .requiredOption('--confirm <plan-id>', 'Exact sha256 plan id returned by prepare')
1383
+ .addOption(new Option('--executor-receipt <path>').hideHelp())
1384
+ .option('--json', 'Output JSON')
1385
+ .action(async (options) => {
1386
+ await runGlobalUpdateRecoveryCommand(options);
1387
+ });
963
1388
  command
964
1389
  .command('bridge')
965
1390
  .description('Apply an old-CLI plan from an externally launched exact public candidate')
@@ -486,13 +486,19 @@ function matchesClassificationFilters(record, options) {
486
486
  }
487
487
  return true;
488
488
  }
489
- function filterHostedMemoryResult(result, options) {
489
+ export function filterHostedMemoryResult(result, options) {
490
490
  if (buildClassificationTags(options).length === 0)
491
491
  return result;
492
492
  if (Array.isArray(result)) {
493
493
  return result.filter((entry) => matchesClassificationFilters(entry, options));
494
494
  }
495
- if (!isRecord(result) || !Array.isArray(result.data))
495
+ if (!isRecord(result))
496
+ return result;
497
+ if (Array.isArray(result.items)) {
498
+ const items = result.items.filter((entry) => matchesClassificationFilters(entry, options));
499
+ return { ...result, items, returned: items.length };
500
+ }
501
+ if (!Array.isArray(result.data))
496
502
  return result;
497
503
  return {
498
504
  ...result,
@@ -115,7 +115,7 @@ export function makeOfficialCatalogSetupCommand(dependencies = {}) {
115
115
  The reserved scope is fixed to ${OFFICIAL_CATALOG_SCOPE_V1.slug}. It is routing
116
116
  identity only: official trust comes from the verified Community release and the
117
117
  package/release digests persisted in SkillVersion meta. Import is inert and does
118
- not select a discipline, mutate a mission policy, or touch user/project scopes.
118
+ not select a discipline, mutate a policy, or touch user/project scopes.
119
119
 
120
120
  Agentspace owns the atomic composite operations and durable receipts. Inspect
121
121
  their exact server contracts with:
@@ -37,8 +37,8 @@ export function makePlanCommand() {
37
37
  ' For one-item checklist edits, use `aops-cli pm utask ...` until top-level plan microtask sugar is added.\n');
38
38
  applyPlanContextOptions(applyMemoryCadenceOptions(applyWriteOptions(cmd
39
39
  .command('create')
40
- .description('Create a sprint-backed implementation plan for a kanban task')
41
- .requiredOption('--task <id>', 'Kanban task id')
40
+ .description('Create a sprint-backed implementation plan, optionally bound to a kanban task')
41
+ .option('--task <id>', 'Optional kanban task id to bind the plan to')
42
42
  .requiredOption('--name <name>', 'Plan name')
43
43
  .requiredOption('--goal <text>', 'Plan goal')
44
44
  .option('--reference <value>', 'Repeatable reference item', collectRepeatedOption, [])