@pellux/goodvibes-agent 1.1.7 → 1.3.0

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 (274) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/README.md +46 -30
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-swtppvy9.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-ttfcdtap.node} +0 -0
  5. package/dist/package/main.js +138903 -91477
  6. package/docs/README.md +14 -7
  7. package/docs/channels-remote-and-api.md +8 -5
  8. package/docs/connected-host.md +14 -12
  9. package/docs/getting-started.md +58 -30
  10. package/docs/knowledge-artifacts-and-multimodal.md +7 -5
  11. package/docs/project-planning.md +2 -2
  12. package/docs/providers-and-routing.md +11 -3
  13. package/docs/tools-and-commands.md +116 -45
  14. package/docs/voice-and-live-tts.md +3 -1
  15. package/package.json +3 -2
  16. package/release/release-notes.md +142 -5
  17. package/release/release-readiness.json +124 -94
  18. package/src/agent/assistant-cockpit.ts +247 -0
  19. package/src/agent/autonomy-schedule-format.ts +96 -0
  20. package/src/agent/autonomy-schedule.ts +514 -0
  21. package/src/agent/channel-delivery-receipts.ts +292 -0
  22. package/src/agent/competitive-feature-inventory.ts +294 -0
  23. package/src/agent/document-registry-types.ts +106 -0
  24. package/src/agent/document-registry.ts +734 -0
  25. package/src/agent/harness-control.ts +2 -2
  26. package/src/agent/memory-prompt.ts +2 -2
  27. package/src/agent/operator-actions.ts +50 -6
  28. package/src/agent/persona-registry.ts +8 -0
  29. package/src/agent/project-context-files.ts +273 -0
  30. package/src/agent/prompt-context-receipts.ts +502 -0
  31. package/src/agent/reminder-schedule-format.ts +16 -2
  32. package/src/agent/research-run-registry.ts +582 -0
  33. package/src/agent/research-source-registry.ts +441 -0
  34. package/src/agent/routine-registry.ts +19 -3
  35. package/src/agent/routine-schedule-format.ts +27 -2
  36. package/src/agent/runtime-profile-starters.ts +7 -0
  37. package/src/agent/runtime-profile.ts +106 -4
  38. package/src/agent/schedule-edit-format.ts +129 -0
  39. package/src/agent/schedule-edit.ts +496 -0
  40. package/src/agent/schedule-next-routes.ts +42 -0
  41. package/src/agent/setup-wizard-artifact-receipts.ts +366 -0
  42. package/src/agent/setup-wizard-checkpoint.ts +140 -0
  43. package/src/agent/setup-wizard.ts +683 -0
  44. package/src/agent/skill-registry-types.ts +102 -0
  45. package/src/agent/skill-registry.ts +81 -107
  46. package/src/agent/vibe-confirmation-routes.ts +62 -0
  47. package/src/agent/vibe-file.ts +285 -0
  48. package/src/cli/agent-knowledge-command.ts +45 -2
  49. package/src/cli/help.ts +3 -0
  50. package/src/cli/profiles-command.ts +25 -11
  51. package/src/config/agent-settings-policy.ts +1 -18
  52. package/src/input/agent-workspace-activation.ts +53 -0
  53. package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
  54. package/src/input/agent-workspace-artifact-metadata.ts +17 -0
  55. package/src/input/agent-workspace-basic-command-editors.ts +5 -1
  56. package/src/input/agent-workspace-categories.ts +153 -155
  57. package/src/input/agent-workspace-channel-triage.ts +481 -0
  58. package/src/input/agent-workspace-channels.ts +263 -0
  59. package/src/input/agent-workspace-command-editor.ts +152 -0
  60. package/src/input/agent-workspace-context-snapshot.ts +349 -0
  61. package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
  62. package/src/input/agent-workspace-document-editor.ts +502 -0
  63. package/src/input/agent-workspace-document-ops-editor.ts +523 -0
  64. package/src/input/agent-workspace-host-category.ts +1 -1
  65. package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
  66. package/src/input/agent-workspace-model-compare-editor.ts +376 -0
  67. package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
  68. package/src/input/agent-workspace-model-compare-types.ts +82 -0
  69. package/src/input/agent-workspace-model-compare-utils.ts +35 -0
  70. package/src/input/agent-workspace-onboarding-categories.ts +141 -0
  71. package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
  72. package/src/input/agent-workspace-operations-command-editors.ts +22 -0
  73. package/src/input/agent-workspace-research-report-editor.ts +212 -0
  74. package/src/input/agent-workspace-research-run-editor.ts +144 -0
  75. package/src/input/agent-workspace-research-source-editor.ts +167 -0
  76. package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
  77. package/src/input/agent-workspace-review-packet-utils.ts +265 -0
  78. package/src/input/agent-workspace-settings.ts +313 -30
  79. package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
  80. package/src/input/agent-workspace-setup-snapshot.ts +217 -0
  81. package/src/input/agent-workspace-setup.ts +61 -1
  82. package/src/input/agent-workspace-snapshot.ts +184 -149
  83. package/src/input/agent-workspace-types.ts +287 -2
  84. package/src/input/agent-workspace.ts +23 -1
  85. package/src/input/command-registry.ts +9 -2
  86. package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
  87. package/src/input/commands/channels-runtime.ts +102 -114
  88. package/src/input/commands/delegation-runtime.ts +87 -24
  89. package/src/input/commands/knowledge-browser-flags.ts +12 -0
  90. package/src/input/commands/knowledge.ts +28 -14
  91. package/src/input/commands/operator-actions-runtime.ts +6 -3
  92. package/src/input/commands/schedule-runtime.ts +71 -14
  93. package/src/input/commands/session-content.ts +2 -2
  94. package/src/input/commands/shell-core.ts +2 -0
  95. package/src/input/commands/vibe-runtime.ts +140 -0
  96. package/src/input/commands.ts +2 -0
  97. package/src/input/connected-host-routes.ts +123 -0
  98. package/src/input/settings-modal-types.ts +14 -0
  99. package/src/input/setup-wizard-live-receipts.ts +76 -0
  100. package/src/main.ts +15 -15
  101. package/src/panels/qr-panel.ts +2 -2
  102. package/src/renderer/agent-workspace-context-lines.ts +701 -0
  103. package/src/renderer/agent-workspace.ts +68 -421
  104. package/src/renderer/settings-modal-helpers.ts +12 -0
  105. package/src/renderer/settings-modal.ts +12 -0
  106. package/src/runtime/bootstrap-command-context.ts +11 -1
  107. package/src/runtime/bootstrap-command-parts.ts +14 -3
  108. package/src/runtime/bootstrap-core.ts +39 -1
  109. package/src/runtime/bootstrap-shell.ts +6 -0
  110. package/src/runtime/bootstrap.ts +113 -30
  111. package/src/runtime/connected-host-auth.ts +3 -2
  112. package/src/runtime/execution-ledger.ts +231 -0
  113. package/src/runtime/services.ts +88 -10
  114. package/src/runtime/tool-permission-safety.ts +199 -5
  115. package/src/shell/session-continuity-hints.ts +39 -0
  116. package/src/tools/agent-artifacts-tool.ts +606 -0
  117. package/src/tools/agent-audit-tool.ts +155 -0
  118. package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
  119. package/src/tools/agent-autonomy-tool.ts +140 -0
  120. package/src/tools/agent-channel-send-tool.ts +24 -2
  121. package/src/tools/agent-channels-tool.ts +140 -0
  122. package/src/tools/agent-computer-tool.ts +168 -0
  123. package/src/tools/agent-context-policy.ts +137 -7
  124. package/src/tools/agent-context-tool.ts +143 -0
  125. package/src/tools/agent-delegation-tool.ts +128 -0
  126. package/src/tools/agent-device-tool.ts +240 -0
  127. package/src/tools/agent-documents-tool.ts +684 -0
  128. package/src/tools/agent-execution-tool.ts +230 -0
  129. package/src/tools/agent-harness-agent-orchestration-policy.ts +75 -0
  130. package/src/tools/agent-harness-agent-orchestration.ts +791 -0
  131. package/src/tools/agent-harness-autonomy-intake.ts +785 -0
  132. package/src/tools/agent-harness-autonomy-live-records.ts +742 -0
  133. package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
  134. package/src/tools/agent-harness-autonomy-queue.ts +571 -0
  135. package/src/tools/agent-harness-autonomy-watcher-read-models.ts +509 -0
  136. package/src/tools/agent-harness-background-processes-types.ts +35 -0
  137. package/src/tools/agent-harness-background-processes.ts +798 -0
  138. package/src/tools/agent-harness-browser-cockpit-route.ts +373 -0
  139. package/src/tools/agent-harness-browser-control.ts +346 -0
  140. package/src/tools/agent-harness-browser-pwa-read-models.ts +622 -0
  141. package/src/tools/agent-harness-channel-metadata.ts +168 -10
  142. package/src/tools/agent-harness-cli-command-policy.ts +110 -0
  143. package/src/tools/agent-harness-command-catalog.ts +4 -2
  144. package/src/tools/agent-harness-command-runner.ts +42 -0
  145. package/src/tools/agent-harness-connected-host-status.ts +7 -4
  146. package/src/tools/agent-harness-delegation-posture.ts +234 -19
  147. package/src/tools/agent-harness-device-live-read-models.ts +366 -0
  148. package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
  149. package/src/tools/agent-harness-document-ops-types.ts +72 -0
  150. package/src/tools/agent-harness-document-ops.ts +671 -0
  151. package/src/tools/agent-harness-execution-history.ts +489 -0
  152. package/src/tools/agent-harness-execution-posture.ts +385 -0
  153. package/src/tools/agent-harness-file-recovery.ts +135 -0
  154. package/src/tools/agent-harness-interactive-runtime-records.ts +421 -0
  155. package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
  156. package/src/tools/agent-harness-learning-curator-common.ts +102 -0
  157. package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
  158. package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
  159. package/src/tools/agent-harness-learning-curator-types.ts +151 -0
  160. package/src/tools/agent-harness-learning-curator.ts +417 -0
  161. package/src/tools/agent-harness-local-model-benchmarks.ts +269 -0
  162. package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
  163. package/src/tools/agent-harness-local-model-endpoints.ts +788 -0
  164. package/src/tools/agent-harness-local-model-smoke.ts +277 -0
  165. package/src/tools/agent-harness-local-model-url.ts +78 -0
  166. package/src/tools/agent-harness-media-posture.ts +223 -2
  167. package/src/tools/agent-harness-memory-external-providers.ts +796 -0
  168. package/src/tools/agent-harness-memory-posture.ts +672 -0
  169. package/src/tools/agent-harness-memory-provider-certification.ts +219 -0
  170. package/src/tools/agent-harness-memory-refinement.ts +340 -0
  171. package/src/tools/agent-harness-metadata.ts +100 -168
  172. package/src/tools/agent-harness-mode-catalog.ts +86 -33
  173. package/src/tools/agent-harness-model-catalog.ts +162 -0
  174. package/src/tools/agent-harness-model-provider-health.ts +283 -0
  175. package/src/tools/agent-harness-model-readiness.ts +432 -0
  176. package/src/tools/agent-harness-model-routing-types.ts +327 -0
  177. package/src/tools/agent-harness-model-routing-utils.ts +30 -0
  178. package/src/tools/agent-harness-model-routing.ts +165 -193
  179. package/src/tools/agent-harness-operator-methods.ts +115 -133
  180. package/src/tools/agent-harness-pairing-posture.ts +452 -18
  181. package/src/tools/agent-harness-personal-ops-certification.ts +116 -0
  182. package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
  183. package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
  184. package/src/tools/agent-harness-personal-ops-lanes.ts +270 -0
  185. package/src/tools/agent-harness-personal-ops-operations.ts +225 -0
  186. package/src/tools/agent-harness-personal-ops-provider-records.ts +358 -0
  187. package/src/tools/agent-harness-personal-ops-provider-task-records.ts +321 -0
  188. package/src/tools/agent-harness-personal-ops-records.ts +684 -0
  189. package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
  190. package/src/tools/agent-harness-personal-ops-types.ts +240 -0
  191. package/src/tools/agent-harness-personal-ops.ts +613 -0
  192. package/src/tools/agent-harness-project-context.ts +144 -0
  193. package/src/tools/agent-harness-prompt-context.ts +589 -0
  194. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  195. package/src/tools/agent-harness-release-evidence.ts +9 -7
  196. package/src/tools/agent-harness-release-readiness.ts +9 -7
  197. package/src/tools/agent-harness-remote-read-models.ts +541 -0
  198. package/src/tools/agent-harness-research-briefing.ts +446 -0
  199. package/src/tools/agent-harness-research-live-read-models.ts +500 -0
  200. package/src/tools/agent-harness-research-queue.ts +250 -0
  201. package/src/tools/agent-harness-research-runs.ts +307 -0
  202. package/src/tools/agent-harness-research-workflow.ts +395 -0
  203. package/src/tools/agent-harness-security-posture.ts +2 -2
  204. package/src/tools/agent-harness-service-posture.ts +9 -9
  205. package/src/tools/agent-harness-session-metadata.ts +3 -3
  206. package/src/tools/agent-harness-setup-connected-host.ts +501 -0
  207. package/src/tools/agent-harness-setup-describe.ts +557 -0
  208. package/src/tools/agent-harness-setup-handoffs.ts +502 -0
  209. package/src/tools/agent-harness-setup-model-helpers.ts +102 -0
  210. package/src/tools/agent-harness-setup-plan.ts +253 -0
  211. package/src/tools/agent-harness-setup-posture-types.ts +218 -0
  212. package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
  213. package/src/tools/agent-harness-setup-posture.ts +449 -221
  214. package/src/tools/agent-harness-setup-smoke.ts +533 -0
  215. package/src/tools/agent-harness-sudo-posture.ts +114 -0
  216. package/src/tools/agent-harness-tool-schema.ts +142 -7
  217. package/src/tools/agent-harness-tool-types.ts +83 -0
  218. package/src/tools/agent-harness-tool-utils.ts +43 -0
  219. package/src/tools/agent-harness-tool.ts +319 -370
  220. package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
  221. package/src/tools/agent-harness-ui-surface-types.ts +46 -0
  222. package/src/tools/agent-harness-vibe-health.ts +105 -0
  223. package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
  224. package/src/tools/agent-harness-workspace-actions.ts +114 -8
  225. package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
  226. package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
  227. package/src/tools/agent-host-tool.ts +159 -0
  228. package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
  229. package/src/tools/agent-learning-consolidation-core.ts +327 -0
  230. package/src/tools/agent-learning-consolidation-tool.ts +758 -0
  231. package/src/tools/agent-memory-tool.ts +253 -0
  232. package/src/tools/agent-model-compare-export.ts +315 -0
  233. package/src/tools/agent-model-compare-handoff.ts +592 -0
  234. package/src/tools/agent-model-compare-judgment.ts +633 -0
  235. package/src/tools/agent-model-compare-run.ts +735 -0
  236. package/src/tools/agent-model-compare-tool.ts +698 -0
  237. package/src/tools/agent-model-compare-types.ts +191 -0
  238. package/src/tools/agent-model-compare-utils.ts +93 -0
  239. package/src/tools/agent-models-tool.ts +208 -0
  240. package/src/tools/agent-operator-action-tool.ts +1 -1
  241. package/src/tools/agent-operator-method-tool.ts +643 -0
  242. package/src/tools/agent-personal-ops-tool.ts +197 -0
  243. package/src/tools/agent-policy-explanation.ts +415 -0
  244. package/src/tools/agent-research-report-tool.ts +608 -0
  245. package/src/tools/agent-research-runner.ts +367 -0
  246. package/src/tools/agent-research-runs-tool.ts +434 -0
  247. package/src/tools/agent-research-sources-tool.ts +443 -0
  248. package/src/tools/agent-research-tool.ts +515 -0
  249. package/src/tools/agent-review-packet-presets-core.ts +757 -0
  250. package/src/tools/agent-review-packet-presets-tool.ts +466 -0
  251. package/src/tools/agent-review-packet-share-tool.ts +305 -0
  252. package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
  253. package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
  254. package/src/tools/agent-route-planner-candidates-work.ts +251 -0
  255. package/src/tools/agent-route-planner-helpers.ts +667 -0
  256. package/src/tools/agent-route-planner.ts +185 -0
  257. package/src/tools/agent-route-tool.ts +105 -0
  258. package/src/tools/agent-schedule-edit-tool.ts +210 -0
  259. package/src/tools/agent-schedule-tool.ts +282 -0
  260. package/src/tools/agent-security-tool.ts +145 -0
  261. package/src/tools/agent-sessions-tool.ts +122 -0
  262. package/src/tools/agent-settings-import-tool.ts +104 -0
  263. package/src/tools/agent-settings-tool.ts +176 -0
  264. package/src/tools/agent-setup-tool.ts +226 -0
  265. package/src/tools/agent-support-tool.ts +122 -0
  266. package/src/tools/agent-terminal-process-tools.ts +167 -0
  267. package/src/tools/agent-tool-policy-guard-types.ts +77 -0
  268. package/src/tools/agent-tool-policy-guard.ts +106 -85
  269. package/src/tools/agent-vibe-tool.ts +297 -0
  270. package/src/tools/agent-work-plan-tool.ts +265 -6
  271. package/src/tools/agent-workspace-tool.ts +305 -0
  272. package/src/tools/artifact-archive.ts +169 -0
  273. package/src/tools/tool-definition-compaction.ts +36 -0
  274. package/src/version.ts +1 -1
@@ -0,0 +1,265 @@
1
+ import type { ArtifactDescriptor } from '@pellux/goodvibes-sdk/platform/artifacts';
2
+ import type { AgentWorkspaceRecentReviewerHandoffArtifact, AgentWorkspaceReviewPacketPresetLineage } from './agent-workspace-types.ts';
3
+ import { readArtifactMetadataNumber, readArtifactMetadataString, readArtifactMetadataStringList } from './agent-workspace-artifact-metadata.ts';
4
+
5
+ export function isoToEpoch(value: string): number {
6
+ const timestamp = Date.parse(value);
7
+ return Number.isFinite(timestamp) ? timestamp : 0;
8
+ }
9
+
10
+ export function compactTimelineText(value: string, maxLength = 96): string {
11
+ const normalized = value.replace(/\s+/g, ' ').trim();
12
+ if (normalized.length <= maxLength) return normalized;
13
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
14
+ }
15
+
16
+ export function uniqueStrings(values: readonly (string | null | undefined)[], limit: number): readonly string[] {
17
+ const seen = new Set<string>();
18
+ const result: string[] = [];
19
+ for (const value of values) {
20
+ const trimmed = value?.trim();
21
+ if (!trimmed || seen.has(trimmed)) continue;
22
+ seen.add(trimmed);
23
+ result.push(trimmed);
24
+ if (result.length >= limit) break;
25
+ }
26
+ return result;
27
+ }
28
+
29
+ export function isReviewerHandoffArtifact(artifact: ArtifactDescriptor): boolean {
30
+ const purpose = readArtifactMetadataString(artifact.metadata, 'purpose');
31
+ if (purpose === 'agent-model-compare-handoff') return true;
32
+ if (purpose.length > 0) return false;
33
+ return (artifact.filename ?? '').startsWith('blind-model-comparison-handoff-');
34
+ }
35
+
36
+ export function isModelCompareArtifact(artifact: ArtifactDescriptor): boolean {
37
+ const purpose = readArtifactMetadataString(artifact.metadata, 'purpose');
38
+ if (purpose === 'agent-model-compare') return true;
39
+ if (purpose.length > 0) return false;
40
+ return (artifact.filename ?? '').startsWith('blind-model-comparison-cmp_');
41
+ }
42
+
43
+ export function isModelCompareJudgmentArtifact(artifact: ArtifactDescriptor): boolean {
44
+ const purpose = readArtifactMetadataString(artifact.metadata, 'purpose');
45
+ if (purpose === 'agent-model-compare-judgment') return true;
46
+ if (purpose.length > 0) return false;
47
+ return (artifact.filename ?? '').startsWith('blind-model-comparison-judgment-');
48
+ }
49
+
50
+ export function isDocumentExportArtifact(artifact: ArtifactDescriptor): boolean {
51
+ return readArtifactMetadataString(artifact.metadata, 'purpose') === 'agent-document-export';
52
+ }
53
+
54
+ export function isModelCompareExportArtifact(artifact: ArtifactDescriptor): boolean {
55
+ return readArtifactMetadataString(artifact.metadata, 'purpose') === 'agent-model-compare-export';
56
+ }
57
+
58
+ export function isReviewerHandoffArchiveArtifact(artifact: ArtifactDescriptor): boolean {
59
+ return readArtifactMetadataString(artifact.metadata, 'purpose') === 'agent-model-compare-handoff-archive';
60
+ }
61
+
62
+ export function isModelCompareRouteDecisionArtifact(artifact: ArtifactDescriptor): boolean {
63
+ return readArtifactMetadataString(artifact.metadata, 'purpose') === 'agent-model-compare-route-decision';
64
+ }
65
+
66
+ export function isReviewPacketPresetArtifact(artifact: ArtifactDescriptor): boolean {
67
+ return readArtifactMetadataString(artifact.metadata, 'purpose') === 'agent-review-packet-preset';
68
+ }
69
+
70
+ export function artifactById(artifacts: readonly ArtifactDescriptor[], artifactId: string): ArtifactDescriptor | null {
71
+ return artifacts.find((artifact) => artifact.id === artifactId) ?? null;
72
+ }
73
+
74
+ export function firstArtifactMetadataString(
75
+ artifacts: readonly ArtifactDescriptor[],
76
+ artifactIds: readonly string[],
77
+ keys: readonly string[],
78
+ ): string {
79
+ for (const artifactId of artifactIds) {
80
+ const artifact = artifactById(artifacts, artifactId);
81
+ if (!artifact) continue;
82
+ for (const key of keys) {
83
+ const value = readArtifactMetadataString(artifact.metadata, key);
84
+ if (value) return value;
85
+ }
86
+ }
87
+ return '';
88
+ }
89
+
90
+ export function latestMatchingArtifact(
91
+ artifacts: readonly ArtifactDescriptor[],
92
+ currentArtifactId: string,
93
+ predicate: (artifact: ArtifactDescriptor) => boolean,
94
+ ): ArtifactDescriptor | null {
95
+ const current = artifactById(artifacts, currentArtifactId);
96
+ const currentCreatedAt = current?.createdAt ?? -1;
97
+ return artifacts
98
+ .filter((artifact) => artifact.id !== currentArtifactId && artifact.createdAt > currentCreatedAt && predicate(artifact))
99
+ .sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id))[0] ?? null;
100
+ }
101
+
102
+ export function reviewPacketPresetFreshness(
103
+ preset: ArtifactDescriptor,
104
+ artifacts: readonly ArtifactDescriptor[],
105
+ ): { readonly missingCount: number; readonly newerCount: number; readonly summary: string; readonly status: 'info' | 'attention' } {
106
+ const metadata = preset.metadata;
107
+ const primaryIds = [
108
+ readArtifactMetadataString(metadata, 'documentExportArtifactId'),
109
+ readArtifactMetadataString(metadata, 'comparisonArtifactId'),
110
+ readArtifactMetadataString(metadata, 'judgmentArtifactId'),
111
+ readArtifactMetadataString(metadata, 'revealedJudgmentArtifactId'),
112
+ readArtifactMetadataString(metadata, 'routeDecisionArtifactId'),
113
+ readArtifactMetadataString(metadata, 'handoffArtifactId'),
114
+ readArtifactMetadataString(metadata, 'handoffArchiveArtifactId'),
115
+ ].filter(Boolean);
116
+ const relatedIds = readArtifactMetadataStringList(metadata, 'relatedArtifactIds');
117
+ const referencedIds = uniqueStrings([...primaryIds, ...relatedIds], 40);
118
+ const missingCount = referencedIds.filter((artifactId) => !artifactById(artifacts, artifactId)).length;
119
+ const documentId = readArtifactMetadataString(metadata, 'documentId')
120
+ || firstArtifactMetadataString(artifacts, primaryIds, ['documentId', 'sourceDocumentId']);
121
+ const comparisonId = firstArtifactMetadataString(artifacts, primaryIds, ['comparisonId']);
122
+ const sourceArtifactId = firstArtifactMetadataString(artifacts, primaryIds, ['sourceArtifactId'])
123
+ || readArtifactMetadataString(metadata, 'documentExportArtifactId');
124
+ const handoffSourceArtifactId = readArtifactMetadataString(metadata, 'revealedJudgmentArtifactId')
125
+ || readArtifactMetadataString(metadata, 'judgmentArtifactId')
126
+ || readArtifactMetadataString(metadata, 'comparisonArtifactId')
127
+ || readArtifactMetadataString(metadata, 'documentExportArtifactId');
128
+ const judgmentArtifactId = readArtifactMetadataString(metadata, 'revealedJudgmentArtifactId')
129
+ || readArtifactMetadataString(metadata, 'judgmentArtifactId')
130
+ || firstArtifactMetadataString(artifacts, [readArtifactMetadataString(metadata, 'routeDecisionArtifactId')], ['judgmentArtifactId']);
131
+ const handoffArtifactId = readArtifactMetadataString(metadata, 'handoffArtifactId')
132
+ || firstArtifactMetadataString(artifacts, [readArtifactMetadataString(metadata, 'handoffArchiveArtifactId')], ['handoffArtifactId']);
133
+ const documentExportArtifactId = readArtifactMetadataString(metadata, 'documentExportArtifactId');
134
+ const comparisonArtifactId = readArtifactMetadataString(metadata, 'comparisonArtifactId');
135
+ const judgmentPresetArtifactId = readArtifactMetadataString(metadata, 'judgmentArtifactId');
136
+ const revealedJudgmentArtifactId = readArtifactMetadataString(metadata, 'revealedJudgmentArtifactId');
137
+ const routeDecisionArtifactId = readArtifactMetadataString(metadata, 'routeDecisionArtifactId');
138
+ const handoffPresetArtifactId = readArtifactMetadataString(metadata, 'handoffArtifactId');
139
+ const handoffArchiveArtifactId = readArtifactMetadataString(metadata, 'handoffArchiveArtifactId');
140
+ const freshnessChecks: Array<ArtifactDescriptor | null> = [
141
+ documentId && documentExportArtifactId
142
+ ? latestMatchingArtifact(artifacts, documentExportArtifactId, (artifact) => (
143
+ isDocumentExportArtifact(artifact) && readArtifactMetadataString(artifact.metadata, 'documentId') === documentId
144
+ ))
145
+ : null,
146
+ comparisonArtifactId && (documentId || sourceArtifactId)
147
+ ? latestMatchingArtifact(artifacts, comparisonArtifactId, (artifact) => (
148
+ isModelCompareArtifact(artifact)
149
+ && (
150
+ Boolean(documentId && readArtifactMetadataString(artifact.metadata, 'documentId') === documentId)
151
+ || Boolean(sourceArtifactId && readArtifactMetadataString(artifact.metadata, 'sourceArtifactId') === sourceArtifactId)
152
+ )
153
+ ))
154
+ : null,
155
+ judgmentPresetArtifactId && (comparisonId || sourceArtifactId || documentId)
156
+ ? latestMatchingArtifact(artifacts, judgmentPresetArtifactId, (artifact) => (
157
+ isModelCompareJudgmentArtifact(artifact)
158
+ && (
159
+ Boolean(comparisonId && readArtifactMetadataString(artifact.metadata, 'comparisonId') === comparisonId)
160
+ || Boolean(documentId && readArtifactMetadataString(artifact.metadata, 'documentId') === documentId)
161
+ || Boolean(sourceArtifactId && readArtifactMetadataString(artifact.metadata, 'sourceArtifactId') === sourceArtifactId)
162
+ )
163
+ ))
164
+ : null,
165
+ revealedJudgmentArtifactId && (comparisonId || sourceArtifactId || documentId)
166
+ ? latestMatchingArtifact(artifacts, revealedJudgmentArtifactId, (artifact) => (
167
+ isModelCompareJudgmentArtifact(artifact)
168
+ && artifact.metadata.revealIncludedInJudgment === true
169
+ && readArtifactMetadataString(artifact.metadata, 'winnerModel').length > 0
170
+ && (
171
+ Boolean(comparisonId && readArtifactMetadataString(artifact.metadata, 'comparisonId') === comparisonId)
172
+ || Boolean(documentId && readArtifactMetadataString(artifact.metadata, 'documentId') === documentId)
173
+ || Boolean(sourceArtifactId && readArtifactMetadataString(artifact.metadata, 'sourceArtifactId') === sourceArtifactId)
174
+ )
175
+ ))
176
+ : null,
177
+ routeDecisionArtifactId && (comparisonId || judgmentArtifactId)
178
+ ? latestMatchingArtifact(artifacts, routeDecisionArtifactId, (artifact) => (
179
+ isModelCompareRouteDecisionArtifact(artifact)
180
+ && (
181
+ Boolean(judgmentArtifactId && readArtifactMetadataString(artifact.metadata, 'judgmentArtifactId') === judgmentArtifactId)
182
+ || Boolean(comparisonId && readArtifactMetadataString(artifact.metadata, 'comparisonId') === comparisonId)
183
+ )
184
+ ))
185
+ : null,
186
+ handoffPresetArtifactId && (comparisonId || handoffSourceArtifactId)
187
+ ? latestMatchingArtifact(artifacts, handoffPresetArtifactId, (artifact) => (
188
+ isReviewerHandoffArtifact(artifact)
189
+ && (
190
+ Boolean(handoffSourceArtifactId && readArtifactMetadataString(artifact.metadata, 'sourceArtifactId') === handoffSourceArtifactId)
191
+ || Boolean(comparisonId && readArtifactMetadataString(artifact.metadata, 'comparisonId') === comparisonId)
192
+ )
193
+ ))
194
+ : null,
195
+ handoffArchiveArtifactId && (comparisonId || handoffArtifactId)
196
+ ? latestMatchingArtifact(artifacts, handoffArchiveArtifactId, (artifact) => (
197
+ isReviewerHandoffArchiveArtifact(artifact)
198
+ && (
199
+ Boolean(handoffArtifactId && readArtifactMetadataString(artifact.metadata, 'handoffArtifactId') === handoffArtifactId)
200
+ || Boolean(comparisonId && readArtifactMetadataString(artifact.metadata, 'comparisonId') === comparisonId)
201
+ )
202
+ ))
203
+ : null,
204
+ ];
205
+ const newerCount = new Set(freshnessChecks.filter((artifact): artifact is ArtifactDescriptor => artifact !== null).map((artifact) => artifact.id)).size;
206
+ const needsReview = missingCount > 0 || newerCount > 0;
207
+ return {
208
+ missingCount,
209
+ newerCount,
210
+ summary: needsReview ? `freshness needs review: ${missingCount} missing, ${newerCount} newer` : 'freshness current',
211
+ status: needsReview ? 'attention' : 'info',
212
+ };
213
+ }
214
+
215
+ export function buildReviewPacketPresetLineage(preset: ArtifactDescriptor | null): AgentWorkspaceReviewPacketPresetLineage | null {
216
+ if (!preset) return null;
217
+ const metadata = preset.metadata;
218
+ const refreshedFromArtifactId = readArtifactMetadataString(metadata, 'refreshOfArtifactId') || null;
219
+ const refreshedFromPresetId = readArtifactMetadataString(metadata, 'refreshOfPresetId') || null;
220
+ const freshnessMissingCount = readArtifactMetadataNumber(metadata, 'freshnessMissingCount');
221
+ const freshnessSupersededCount = readArtifactMetadataNumber(metadata, 'freshnessSupersededCount');
222
+ const freshnessUnresolvedCount = readArtifactMetadataNumber(metadata, 'freshnessUnresolvedCount');
223
+ const repairedCount = (freshnessMissingCount ?? 0) + (freshnessSupersededCount ?? 0);
224
+ const name = readArtifactMetadataString(metadata, 'name') || null;
225
+ const presetId = readArtifactMetadataString(metadata, 'presetId') || null;
226
+ const displayName = name ?? preset.filename ?? preset.id;
227
+ const summary = refreshedFromArtifactId
228
+ ? [
229
+ `${displayName} refreshed from ${refreshedFromArtifactId}`,
230
+ refreshedFromPresetId ? `source preset ${refreshedFromPresetId}` : '',
231
+ `repaired ${repairedCount}`,
232
+ `unresolved ${freshnessUnresolvedCount ?? 0}`,
233
+ ].filter(Boolean).join('; ')
234
+ : [
235
+ `${displayName} has no refresh lineage`,
236
+ presetId ? `preset ${presetId}` : '',
237
+ ].filter(Boolean).join('; ');
238
+ return {
239
+ artifactId: preset.id,
240
+ presetId,
241
+ name,
242
+ refreshed: Boolean(refreshedFromArtifactId),
243
+ refreshedFromArtifactId,
244
+ refreshedFromPresetId,
245
+ freshnessMissingCount,
246
+ freshnessSupersededCount,
247
+ freshnessUnresolvedCount,
248
+ summary,
249
+ inspectRoute: `agent_review_packet_presets show artifactId:"${preset.id}"`,
250
+ };
251
+ }
252
+
253
+ export function summarizeReviewerHandoffArtifact(artifact: ArtifactDescriptor): AgentWorkspaceRecentReviewerHandoffArtifact {
254
+ const metadata = artifact.metadata;
255
+ return {
256
+ id: artifact.id,
257
+ filename: artifact.filename ?? '(unnamed handoff artifact)',
258
+ createdAt: artifact.createdAt,
259
+ handoffId: readArtifactMetadataString(metadata, 'handoffId') || 'unknown-handoff',
260
+ comparisonId: readArtifactMetadataString(metadata, 'comparisonId') || 'unknown-comparison',
261
+ sourceArtifactId: readArtifactMetadataString(metadata, 'sourceArtifactId') || '(missing source)',
262
+ sourceKind: readArtifactMetadataString(metadata, 'sourceKind') || 'unknown',
263
+ relatedArtifactCount: readArtifactMetadataStringList(metadata, 'relatedArtifactIds').length,
264
+ };
265
+ }
@@ -56,6 +56,95 @@ export interface TuiSettingsImportOutcome extends SettingMutationOutcome {
56
56
  readonly runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
57
57
  }
58
58
 
59
+ type TuiImportStatus = 'would_import' | 'unchanged' | 'skipped';
60
+
61
+ interface TuiSettingsImportSource {
62
+ readonly id: string;
63
+ readonly label: string;
64
+ readonly path: string;
65
+ readonly sourcePackage: string;
66
+ readonly surfaceRoot: string;
67
+ readonly scope: 'user' | 'project';
68
+ readonly ownership: 'source-owned';
69
+ readonly mutatesSource: false;
70
+ readonly status: 'missing' | 'loaded' | 'invalid' | 'error';
71
+ readonly importableSettings: number;
72
+ readonly error?: string;
73
+ }
74
+
75
+ interface GoodVibesSettingsImportSourceCatalogEntry {
76
+ readonly packageId: string;
77
+ readonly surfaceRoot: string;
78
+ readonly scopes: readonly string[];
79
+ readonly status: 'implemented';
80
+ readonly ownership: 'source-owned';
81
+ readonly mutatesSource: false;
82
+ }
83
+
84
+ interface TuiSettingImportPlanEntry {
85
+ readonly key: string;
86
+ readonly source: string;
87
+ readonly value: unknown;
88
+ readonly status: TuiImportStatus;
89
+ readonly current: unknown;
90
+ readonly incoming: unknown;
91
+ readonly reason?: string;
92
+ }
93
+
94
+ interface TuiSubscriptionImportPlanEntry {
95
+ readonly provider?: string;
96
+ readonly kind: 'active' | 'pending';
97
+ readonly status: TuiImportStatus;
98
+ readonly reason?: string;
99
+ }
100
+
101
+ interface TuiSettingsImportPlan {
102
+ readonly sources: readonly TuiSettingsImportSource[];
103
+ readonly settings: readonly TuiSettingImportPlanEntry[];
104
+ readonly subscriptions: readonly TuiSubscriptionImportPlanEntry[];
105
+ readonly parseErrors: readonly string[];
106
+ }
107
+
108
+ export interface TuiSettingsImportPreview {
109
+ readonly summary: {
110
+ readonly settingsToImport: number;
111
+ readonly settingsUnchanged: number;
112
+ readonly settingsSkipped: number;
113
+ readonly activeSubscriptionsToImport: number;
114
+ readonly pendingSubscriptionsToImport: number;
115
+ readonly subscriptionsUnchanged: number;
116
+ readonly subscriptionsSkipped: number;
117
+ readonly parseErrors: number;
118
+ };
119
+ readonly sourceCatalog: readonly GoodVibesSettingsImportSourceCatalogEntry[];
120
+ readonly sources: readonly TuiSettingsImportSource[];
121
+ readonly settings: readonly {
122
+ readonly key: string;
123
+ readonly source: string;
124
+ readonly status: TuiImportStatus;
125
+ readonly current: unknown;
126
+ readonly incoming: unknown;
127
+ readonly reason?: string;
128
+ }[];
129
+ readonly subscriptions: readonly TuiSubscriptionImportPlanEntry[];
130
+ readonly policy: {
131
+ readonly effect: 'read-only' | 'state';
132
+ readonly confirmation: 'required-for-apply';
133
+ readonly boundary: string;
134
+ };
135
+ }
136
+
137
+ const GOODVIBES_SETTINGS_IMPORT_SOURCE_CATALOG: readonly GoodVibesSettingsImportSourceCatalogEntry[] = [
138
+ {
139
+ packageId: 'goodvibes-tui',
140
+ surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
141
+ scopes: ['user-settings', 'project-settings', 'provider-subscriptions'],
142
+ status: 'implemented',
143
+ ownership: 'source-owned',
144
+ mutatesSource: false,
145
+ },
146
+ ];
147
+
59
148
  function isRecord(value: unknown): value is Record<string, unknown> {
60
149
  return typeof value === 'object' && value !== null && !Array.isArray(value);
61
150
  }
@@ -84,6 +173,36 @@ function valuesMatch(left: unknown, right: unknown): boolean {
84
173
  return JSON.stringify(left) === JSON.stringify(right);
85
174
  }
86
175
 
176
+ function isSecretLikeSettingKey(key: string): boolean {
177
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, '$1.$2').toLowerCase();
178
+ if (normalized.endsWith('secret.policy')) return false;
179
+ return /(?:secret|token|password|api[-_.]?key|api\.key|signing)/i.test(normalized);
180
+ }
181
+
182
+ function compactSettingValue(key: string, value: unknown): string {
183
+ if (value === undefined || value === null) return '(unset)';
184
+ if (isSecretLikeSettingKey(key)) return value === '' ? '(empty)' : '(secret)';
185
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
186
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '(invalid)';
187
+ if (typeof value === 'string') {
188
+ if (value.length === 0) return '(empty)';
189
+ if (value.startsWith('goodvibes://secrets/')) return '(secret)';
190
+ return value.length > 36 ? `${value.slice(0, 33)}...` : value;
191
+ }
192
+ if (Array.isArray(value)) return `[${value.length} item${value.length === 1 ? '' : 's'}]`;
193
+ if (isRecord(value)) return '{...}';
194
+ return String(value);
195
+ }
196
+
197
+ function redactImportValue(key: string, value: unknown): unknown {
198
+ if (typeof value !== 'string') return value;
199
+ if (!value) return value;
200
+ if (isSecretLikeSettingKey(key)) {
201
+ return value.startsWith('goodvibes://secrets/') ? '<secret-ref>' : '<redacted>';
202
+ }
203
+ return value;
204
+ }
205
+
87
206
  function readRecordMap(record: Record<string, unknown>, key: string): readonly unknown[] {
88
207
  const value = record[key];
89
208
  return isRecord(value) ? Object.values(value) : [];
@@ -109,7 +228,45 @@ function isPendingSubscriptionLogin(value: unknown): value is PendingSubscriptio
109
228
  && typeof value.createdAt === 'number';
110
229
  }
111
230
 
112
- function importTuiSubscriptions(context: CommandContext, parseErrors: string[]): {
231
+ function planTuiSubscriptions(context: CommandContext, parseErrors: string[]): readonly TuiSubscriptionImportPlanEntry[] {
232
+ const shellPaths = context.workspace.shellPaths;
233
+ const manager = context.platform.subscriptionManager;
234
+ const result: TuiSubscriptionImportPlanEntry[] = [];
235
+ if (!shellPaths || !manager) return result;
236
+
237
+ const sourcePath = shellPaths.resolveUserPath(GOODVIBES_TUI_SURFACE_ROOT, 'subscriptions.json');
238
+ try {
239
+ const store = readJsonRecord(sourcePath);
240
+ if (!store) return result;
241
+ for (const entry of readRecordMap(store, 'subscriptions')) {
242
+ if (!isProviderSubscription(entry)) {
243
+ result.push({ kind: 'active', status: 'skipped', reason: 'active subscription with invalid shape' });
244
+ continue;
245
+ }
246
+ if (valuesMatch(manager.get(entry.provider), entry)) {
247
+ result.push({ provider: entry.provider, kind: 'active', status: 'unchanged' });
248
+ continue;
249
+ }
250
+ result.push({ provider: entry.provider, kind: 'active', status: 'would_import' });
251
+ }
252
+ for (const entry of readRecordMap(store, 'pending')) {
253
+ if (!isPendingSubscriptionLogin(entry)) {
254
+ result.push({ kind: 'pending', status: 'skipped', reason: 'pending subscription with invalid shape' });
255
+ continue;
256
+ }
257
+ if (valuesMatch(manager.getPending(entry.provider), entry)) {
258
+ result.push({ provider: entry.provider, kind: 'pending', status: 'unchanged' });
259
+ continue;
260
+ }
261
+ result.push({ provider: entry.provider, kind: 'pending', status: 'would_import' });
262
+ }
263
+ } catch (error) {
264
+ parseErrors.push(`subscriptions: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ return result;
267
+ }
268
+
269
+ function applyTuiSubscriptions(context: CommandContext, parseErrors: string[]): {
113
270
  readonly importedActive: string[];
114
271
  readonly importedPending: string[];
115
272
  readonly unchangedActive: string[];
@@ -233,6 +390,36 @@ export function buildAgentWorkspaceSettingActionEffect(
233
390
  };
234
391
  }
235
392
 
393
+ export function buildAgentWorkspaceSettingActionPreview(
394
+ context: CommandContext | null,
395
+ action: AgentWorkspaceAction,
396
+ ): string | null {
397
+ const settingKey = action.settingKey?.trim();
398
+ const configManager = context?.platform?.configManager;
399
+ if (!settingKey || !configManager) return null;
400
+
401
+ const setting = agentWorkspaceSettingSchema(context, settingKey);
402
+ const currentValue = configManager.get(settingKey as ConfigKey);
403
+ let proposedValue: unknown;
404
+
405
+ if (action.settingValueHint !== undefined) {
406
+ proposedValue = action.settingValueHint;
407
+ } else if (setting?.type === 'boolean') {
408
+ proposedValue = !Boolean(currentValue);
409
+ } else if (setting?.type === 'enum' && setting.enumValues && setting.enumValues.length > 0) {
410
+ const currentIndex = Math.max(0, setting.enumValues.indexOf(String(currentValue)));
411
+ proposedValue = setting.enumValues[(currentIndex + 1) % setting.enumValues.length]!;
412
+ } else if (setting?.type === 'number' || setting?.type === 'string') {
413
+ proposedValue = 'edit value';
414
+ } else if (typeof currentValue === 'boolean') {
415
+ proposedValue = !currentValue;
416
+ } else {
417
+ proposedValue = 'choose value';
418
+ }
419
+
420
+ return `${settingKey}: ${compactSettingValue(settingKey, currentValue)} -> ${compactSettingValue(settingKey, proposedValue)}`;
421
+ }
422
+
236
423
  export async function applyAgentWorkspaceSettingValue(
237
424
  context: CommandContext | null,
238
425
  setting: ConfigSetting,
@@ -275,51 +462,147 @@ export async function applyAgentWorkspaceSettingValue(
275
462
  }
276
463
  }
277
464
 
278
- export async function importAgentWorkspaceTuiSettings(context: CommandContext | null): Promise<TuiSettingsImportOutcome> {
465
+ function buildTuiSettingsImportPlan(context: CommandContext | null): TuiSettingsImportPlan | null {
279
466
  const shellPaths = context?.workspace?.shellPaths;
280
467
  const configManager = context?.platform?.configManager;
281
- if (!context || !shellPaths || !configManager) {
282
- return {
283
- status: 'GoodVibes TUI settings import is unavailable in this runtime.',
284
- runtimeSnapshot: null,
285
- result: {
286
- kind: 'error',
287
- title: 'Import unavailable',
288
- detail: 'The workspace cannot locate shell paths or the Agent config manager.',
289
- safety: 'safe',
290
- },
291
- };
292
- }
468
+ if (!context || !shellPaths || !configManager) return null;
293
469
 
294
- const sources = [
295
- { label: 'user', path: shellPaths.resolveUserPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json') },
296
- { label: 'project', path: shellPaths.resolveProjectPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json') },
470
+ const sourceInputs: readonly Omit<TuiSettingsImportSource, 'status' | 'importableSettings' | 'error'>[] = [
471
+ {
472
+ id: 'goodvibes-tui:user-settings',
473
+ label: 'goodvibes-tui user settings',
474
+ path: shellPaths.resolveUserPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json'),
475
+ sourcePackage: 'goodvibes-tui',
476
+ surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
477
+ scope: 'user',
478
+ ownership: 'source-owned',
479
+ mutatesSource: false,
480
+ },
481
+ {
482
+ id: 'goodvibes-tui:project-settings',
483
+ label: 'goodvibes-tui project settings',
484
+ path: shellPaths.resolveProjectPath(GOODVIBES_TUI_SURFACE_ROOT, 'settings.json'),
485
+ sourcePackage: 'goodvibes-tui',
486
+ surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
487
+ scope: 'project',
488
+ ownership: 'source-owned',
489
+ mutatesSource: false,
490
+ },
297
491
  ];
298
492
  const values = new Map<string, { readonly value: unknown; readonly source: string }>();
299
493
  const parseErrors: string[] = [];
300
- for (const source of sources) {
494
+ const sources: TuiSettingsImportSource[] = [];
495
+ for (const source of sourceInputs) {
496
+ if (!existsSync(source.path)) {
497
+ sources.push({ ...source, status: 'missing', importableSettings: 0 });
498
+ continue;
499
+ }
301
500
  try {
302
501
  const record = readJsonRecord(source.path);
303
- if (!record) continue;
502
+ if (!record) {
503
+ const message = `${source.label}: settings store is not an object`;
504
+ parseErrors.push(message);
505
+ sources.push({ ...source, status: 'invalid', importableSettings: 0, error: message });
506
+ continue;
507
+ }
508
+ let importableSettings = 0;
304
509
  for (const setting of configManager.getSchema()) {
305
510
  if (!canImportTuiSetting(setting.key)) continue;
306
511
  const value = readNestedSettingValue(record, setting.key);
307
- if (value !== undefined) values.set(setting.key, { value, source: source.label });
512
+ if (value === undefined) continue;
513
+ importableSettings += 1;
514
+ values.set(setting.key, { value, source: source.id });
308
515
  }
516
+ sources.push({ ...source, status: 'loaded', importableSettings });
309
517
  } catch (error) {
310
- parseErrors.push(`${source.label}: ${error instanceof Error ? error.message : String(error)}`);
518
+ const message = `${source.label}: ${error instanceof Error ? error.message : String(error)}`;
519
+ parseErrors.push(message);
520
+ sources.push({ ...source, status: 'error', importableSettings: 0, error: message });
311
521
  }
312
522
  }
313
- const subscriptions = importTuiSubscriptions(context, parseErrors);
523
+
524
+ const settings: TuiSettingImportPlanEntry[] = [];
525
+ for (const [key, entry] of values) {
526
+ const setting = agentWorkspaceSettingSchema(context, key);
527
+ if (!setting) continue;
528
+ const current = configManager.get(setting.key as ConfigKey);
529
+ const status: TuiImportStatus = valuesMatch(current, entry.value) ? 'unchanged' : 'would_import';
530
+ settings.push({
531
+ key: setting.key,
532
+ source: entry.source,
533
+ value: entry.value,
534
+ status,
535
+ current: redactImportValue(setting.key, current),
536
+ incoming: redactImportValue(setting.key, entry.value),
537
+ });
538
+ }
539
+ const subscriptions = planTuiSubscriptions(context, parseErrors);
540
+ return { sources, settings, subscriptions, parseErrors };
541
+ }
542
+
543
+ export function previewAgentWorkspaceTuiSettingsImport(context: CommandContext | null): TuiSettingsImportPreview | null {
544
+ const plan = buildTuiSettingsImportPlan(context);
545
+ if (!plan) return null;
546
+ return {
547
+ summary: {
548
+ settingsToImport: plan.settings.filter((entry) => entry.status === 'would_import').length,
549
+ settingsUnchanged: plan.settings.filter((entry) => entry.status === 'unchanged').length,
550
+ settingsSkipped: plan.settings.filter((entry) => entry.status === 'skipped').length,
551
+ activeSubscriptionsToImport: plan.subscriptions.filter((entry) => entry.kind === 'active' && entry.status === 'would_import').length,
552
+ pendingSubscriptionsToImport: plan.subscriptions.filter((entry) => entry.kind === 'pending' && entry.status === 'would_import').length,
553
+ subscriptionsUnchanged: plan.subscriptions.filter((entry) => entry.status === 'unchanged').length,
554
+ subscriptionsSkipped: plan.subscriptions.filter((entry) => entry.status === 'skipped').length,
555
+ parseErrors: plan.parseErrors.length,
556
+ },
557
+ sourceCatalog: GOODVIBES_SETTINGS_IMPORT_SOURCE_CATALOG,
558
+ sources: plan.sources,
559
+ settings: plan.settings.map((entry) => ({
560
+ key: entry.key,
561
+ source: entry.source,
562
+ status: entry.status,
563
+ current: entry.current,
564
+ incoming: entry.incoming,
565
+ ...(entry.reason ? { reason: entry.reason } : {}),
566
+ })),
567
+ subscriptions: plan.subscriptions,
568
+ policy: {
569
+ effect: 'read-only',
570
+ confirmation: 'required-for-apply',
571
+ boundary: 'Preview does not mutate state. Apply imports only Agent-owned settings and provider subscriptions from published GoodVibes platform sources.',
572
+ },
573
+ };
574
+ }
575
+
576
+ export async function importAgentWorkspaceTuiSettings(context: CommandContext | null): Promise<TuiSettingsImportOutcome> {
577
+ const shellPaths = context?.workspace?.shellPaths;
578
+ const configManager = context?.platform?.configManager;
579
+ if (!context || !shellPaths || !configManager) {
580
+ return {
581
+ status: 'GoodVibes settings import is unavailable in this runtime.',
582
+ runtimeSnapshot: null,
583
+ result: {
584
+ kind: 'error',
585
+ title: 'Import unavailable',
586
+ detail: 'The workspace cannot locate shell paths or the Agent config manager.',
587
+ safety: 'safe',
588
+ },
589
+ };
590
+ }
591
+
592
+ const plan = buildTuiSettingsImportPlan(context);
593
+ const values = plan?.settings ?? [];
594
+ const subscriptionParseErrors: string[] = [];
595
+ const subscriptions = applyTuiSubscriptions(context, subscriptionParseErrors);
596
+ const parseErrors: string[] = [...new Set([...(plan?.parseErrors ?? []), ...subscriptionParseErrors])];
314
597
  const subscriptionImports = subscriptions.importedActive.length + subscriptions.importedPending.length;
315
598
  const subscriptionUnchanged = subscriptions.unchangedActive.length + subscriptions.unchangedPending.length;
316
599
 
317
- if (values.size === 0 && subscriptionImports === 0 && subscriptionUnchanged === 0 && subscriptions.skipped.length === 0) {
600
+ if (values.length === 0 && subscriptionImports === 0 && subscriptionUnchanged === 0 && subscriptions.skipped.length === 0) {
318
601
  const detail = parseErrors.length > 0
319
602
  ? `No importable settings or subscriptions found. ${parseErrors.join('; ')}`
320
- : 'No GoodVibes TUI settings or subscription store with importable Agent-owned state was found.';
603
+ : 'No GoodVibes settings or subscription store with importable Agent-owned state was found.';
321
604
  return {
322
- status: 'No GoodVibes TUI settings imported.',
605
+ status: 'No GoodVibes settings imported.',
323
606
  runtimeSnapshot: null,
324
607
  result: {
325
608
  kind: parseErrors.length > 0 ? 'error' : 'guidance',
@@ -333,10 +616,10 @@ export async function importAgentWorkspaceTuiSettings(context: CommandContext |
333
616
  const imported: string[] = [];
334
617
  const unchanged: string[] = [];
335
618
  const skipped: string[] = [];
336
- for (const [key, entry] of values) {
337
- const setting = agentWorkspaceSettingSchema(context, key);
619
+ for (const entry of values) {
620
+ const setting = agentWorkspaceSettingSchema(context, entry.key);
338
621
  if (!setting) continue;
339
- if (valuesMatch(configManager.get(setting.key as ConfigKey), entry.value)) {
622
+ if (entry.status === 'unchanged' || valuesMatch(configManager.get(setting.key as ConfigKey), entry.value)) {
340
623
  unchanged.push(setting.key);
341
624
  continue;
342
625
  }
@@ -352,11 +635,11 @@ export async function importAgentWorkspaceTuiSettings(context: CommandContext |
352
635
  const unchangedCount = unchanged.length + subscriptionUnchanged;
353
636
 
354
637
  return {
355
- status: changedCount > 0 ? `Imported ${changedCount} GoodVibes TUI item(s).` : 'No GoodVibes TUI settings changed.',
638
+ status: changedCount > 0 ? `Imported ${changedCount} GoodVibes settings item(s).` : 'No GoodVibes settings changed.',
356
639
  runtimeSnapshot: context ? buildAgentWorkspaceRuntimeSnapshot(context) : null,
357
640
  result: {
358
641
  kind: skipped.length > 0 && changedCount === 0 ? 'error' : changedCount > 0 ? 'refreshed' : 'guidance',
359
- title: changedCount > 0 ? 'GoodVibes TUI settings imported' : 'No settings changed',
642
+ title: changedCount > 0 ? 'GoodVibes settings imported' : 'No settings changed',
360
643
  detail: [
361
644
  imported.length > 0 ? `Imported: ${imported.slice(0, 10).join(', ')}${imported.length > 10 ? `, +${imported.length - 10} more` : ''}.` : '',
362
645
  subscriptions.importedActive.length > 0 ? `Imported active subscription(s): ${subscriptions.importedActive.join(', ')}.` : '',