@pellux/goodvibes-tui 1.13.0 → 1.14.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 (157) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +363 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +79 -0
  141. package/src/runtime/services.ts +38 -37
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/render-scheduler.ts +0 -90
@@ -0,0 +1,185 @@
1
+ /**
2
+ * recall-files.ts — `/recall files sync|review|apply`, the git-backed markdown
3
+ * projection surface for standing (project/team-scope) memory records.
4
+ *
5
+ * Round-trip:
6
+ * sync — project store records to `<dir>/<id>.md`, git-commit the directory
7
+ * when it sits inside a repo. Read-only against the store.
8
+ * review — diff the on-disk projection against the current store and print
9
+ * the resulting proposals (a file edit -> 'update', a deleted file
10
+ * for an in-scope record -> 'delete'). PURE — no store writes.
11
+ * apply — re-diff fresh, then mutate the store ONLY for the proposal ids
12
+ * the caller explicitly names (or --all), through the memory spine
13
+ * client's own update/delete. Never a silent write.
14
+ *
15
+ * `apply` re-diffs on every call rather than caching `review`'s output, so it
16
+ * always acts on the current on-disk/store state and never on a stale
17
+ * snapshot from an earlier `review` call.
18
+ *
19
+ * Routes through `getMemorySpine` (not the host-only `knowledgeApi.memory`)
20
+ * per the SDK's memory-wire-full-detach decision — same as every other
21
+ * mutating `/recall` subcommand, so this fully detaches from the local store
22
+ * file when a daemon has been adopted.
23
+ *
24
+ * The memory spine's wire `MemoryUpdatePatch` (SDK platform/runtime/memory-spine)
25
+ * now carries validFrom/validUntil alongside scope/summary/detail/tags — a
26
+ * number sets the bound, `null` clears it, and an omitted field leaves it
27
+ * unchanged — so a proposal whose `desired` changes ONLY the temporal window
28
+ * applies for real over the wire, the same as any other field change.
29
+ */
30
+ import { existsSync } from 'node:fs';
31
+ import type { CommandContext } from '../command-registry.ts';
32
+ import {
33
+ diffProjectionToProposals,
34
+ projectMemoryToFiles,
35
+ readProjectedMemoryFiles,
36
+ type MemoryProjectionProposal,
37
+ } from '@pellux/goodvibes-sdk/platform/state';
38
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
39
+ import { requireShellPaths } from './runtime-services.ts';
40
+ import { getMemorySpine } from './recall-query.ts';
41
+ import { readMemoryProjectionDir } from './recall-files-config.ts';
42
+ import { resolveSyncGitSeam } from './recall-files-git.ts';
43
+
44
+ const PROJECTION_LIST_LIMIT = 5000;
45
+
46
+ /** Resolve the projection directory: an explicit --dir argument wins, else the configured setting. */
47
+ function resolveProjectionDir(args: string[], context: CommandContext): string {
48
+ const shellPaths = requireShellPaths(context);
49
+ const dirIdx = args.indexOf('--dir');
50
+ const explicit = dirIdx !== -1 ? args[dirIdx + 1] : undefined;
51
+ const configured = readMemoryProjectionDir(context.platform.configManager);
52
+ return shellPaths.resolveWorkspacePath(explicit || configured);
53
+ }
54
+
55
+ export async function handleRecallFilesSync(args: string[], context: CommandContext): Promise<void> {
56
+ const memory = getMemorySpine(context);
57
+ if (!memory) return;
58
+
59
+ const dir = resolveProjectionDir(args, context);
60
+ const records = await memory.list({ limit: PROJECTION_LIST_LIMIT });
61
+ // Resolve the git seam from the project working directory, not `dir` itself
62
+ // — `dir` may not exist yet on a first sync, and `git -C <dir>` requires the
63
+ // directory to already exist. The working directory is always real.
64
+ const gitSeam = resolveSyncGitSeam(requireShellPaths(context).workingDirectory);
65
+
66
+ try {
67
+ const report = projectMemoryToFiles(records, dir, gitSeam ? { git: gitSeam.seam } : {});
68
+ context.print(`[recall] Projected ${report.written.length} record(s) to ${report.dir}`);
69
+ if (gitSeam) {
70
+ context.print(report.committed ? ` committed in ${gitSeam.repoRoot}` : ' not committed (git seam did not run)');
71
+ } else {
72
+ context.print(` not committed: ${dir} is not inside a git repository`);
73
+ }
74
+ } catch (error) {
75
+ context.print(`[recall] Projection sync failed: ${summarizeError(error)}`);
76
+ }
77
+ }
78
+
79
+ /** Compute the current proposals for `dir` against the live store. Returns null (already printed) when the directory doesn't exist yet. */
80
+ async function computeProposals(
81
+ dir: string,
82
+ context: CommandContext,
83
+ ): Promise<MemoryProjectionProposal[] | null> {
84
+ const memory = getMemorySpine(context);
85
+ if (!memory) return null;
86
+ if (!existsSync(dir)) {
87
+ context.print(`[recall] No projection directory at ${dir}. Run /recall files sync first.`);
88
+ return null;
89
+ }
90
+ const records = await memory.list({ limit: PROJECTION_LIST_LIMIT });
91
+ const files = readProjectedMemoryFiles(dir);
92
+ return diffProjectionToProposals(records, files);
93
+ }
94
+
95
+ function printProposal(context: CommandContext, proposal: MemoryProjectionProposal): void {
96
+ context.print(` ${proposal.id} [${proposal.kind}] ${proposal.reason}`);
97
+ if (proposal.kind === 'update' && proposal.changedFields && proposal.changedFields.length > 0) {
98
+ context.print(` changed: ${proposal.changedFields.join(', ')}`);
99
+ }
100
+ }
101
+
102
+ export async function handleRecallFilesReview(args: string[], context: CommandContext): Promise<void> {
103
+ const dir = resolveProjectionDir(args, context);
104
+ const proposals = await computeProposals(dir, context);
105
+ if (proposals === null) return;
106
+
107
+ if (proposals.length === 0) {
108
+ context.print(`[recall] No changes — ${dir} matches the store.`);
109
+ return;
110
+ }
111
+ context.print(`[recall] ${proposals.length} proposal(s) from ${dir}:`);
112
+ for (const proposal of proposals) printProposal(context, proposal);
113
+ context.print('[recall] Nothing has been applied. Run /recall files apply <id> [<id> ...] | --all to confirm.');
114
+ }
115
+
116
+ export async function handleRecallFilesApply(args: string[], context: CommandContext): Promise<void> {
117
+ const memory = getMemorySpine(context);
118
+ if (!memory) return;
119
+
120
+ const applyAll = args.includes('--all') || args.includes('all');
121
+ const ids = new Set(args.filter((a) => a !== '--all' && a !== 'all' && a !== '--dir' && !a.startsWith('.') && !a.startsWith('/')));
122
+ // --dir's value (the token right after it) is not a proposal id — drop it.
123
+ const dirIdx = args.indexOf('--dir');
124
+ if (dirIdx !== -1 && args[dirIdx + 1]) ids.delete(args[dirIdx + 1]!);
125
+
126
+ if (!applyAll && ids.size === 0) {
127
+ context.print('[recall] Usage: /recall files apply <id> [<id> ...] | --all [--dir <path>]');
128
+ context.print('[recall] Run /recall files review first to see the current proposal ids.');
129
+ return;
130
+ }
131
+
132
+ const dir = resolveProjectionDir(args, context);
133
+ const proposals = await computeProposals(dir, context);
134
+ if (proposals === null) return;
135
+
136
+ const targeted = applyAll ? proposals : proposals.filter((p) => ids.has(p.id));
137
+ if (targeted.length === 0) {
138
+ context.print('[recall] No matching proposals. Run /recall files review to see the current list.');
139
+ return;
140
+ }
141
+ const targetedIds = new Set(targeted.map((p) => p.id));
142
+
143
+ const applied: MemoryProjectionProposal[] = [];
144
+ const skipped: MemoryProjectionProposal[] = [];
145
+ const failed: { proposal: MemoryProjectionProposal; reason: string }[] = [];
146
+
147
+ for (const proposal of proposals) {
148
+ if (!targetedIds.has(proposal.id)) {
149
+ skipped.push(proposal);
150
+ continue;
151
+ }
152
+ try {
153
+ if (proposal.kind === 'delete') {
154
+ const ok = await memory.delete(proposal.id);
155
+ if (ok) applied.push(proposal);
156
+ else failed.push({ proposal, reason: 'store delete returned false (record already gone?)' });
157
+ continue;
158
+ }
159
+ const desired = proposal.desired ?? {};
160
+ const patch: {
161
+ scope?: typeof desired.scope;
162
+ summary?: string;
163
+ detail?: string;
164
+ tags?: string[];
165
+ validFrom?: number | null;
166
+ validUntil?: number | null;
167
+ } = {};
168
+ if (desired.scope !== undefined) patch.scope = desired.scope;
169
+ if (desired.summary !== undefined) patch.summary = desired.summary;
170
+ if (desired.detail !== undefined) patch.detail = desired.detail;
171
+ if (desired.tags !== undefined) patch.tags = [...desired.tags];
172
+ if (desired.validFrom !== undefined) patch.validFrom = desired.validFrom;
173
+ if (desired.validUntil !== undefined) patch.validUntil = desired.validUntil;
174
+ const result = await memory.update(proposal.id, patch);
175
+ if (result) applied.push(proposal);
176
+ else failed.push({ proposal, reason: 'store update returned null (record not found?)' });
177
+ } catch (error) {
178
+ failed.push({ proposal, reason: summarizeError(error) });
179
+ }
180
+ }
181
+
182
+ context.print(`[recall] Applied ${applied.length}, skipped ${skipped.length}, failed ${failed.length}.`);
183
+ for (const proposal of applied) context.print(` applied ${proposal.id} [${proposal.kind}]`);
184
+ for (const { proposal, reason } of failed) context.print(` failed ${proposal.id} [${proposal.kind}] — ${reason}`);
185
+ }
@@ -2,7 +2,7 @@ import type { CommandContext } from '../command-registry.ts';
2
2
  import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
3
3
  import type { MemorySpineClient } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
4
4
  import type { MemorySearchFilter } from '@pellux/goodvibes-sdk/platform/state';
5
- import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope } from './recall-shared.ts';
5
+ import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope, temporalStatusLabel } from './recall-shared.ts';
6
6
 
7
7
  /**
8
8
  * The local, host-only `MemoryApi` — kept for `/recall explain` and `/recall
@@ -93,7 +93,7 @@ export async function handleRecallSearch(args: string[], context: CommandContext
93
93
  const ts = new Date(record.createdAt).toISOString().slice(0, 16).replace('T', ' ');
94
94
  const tagStr = record.tags.length ? ` [${record.tags.join(', ')}]` : '';
95
95
  const scoreStr = semanticEntry ? ` sim ${Math.round(semanticEntry.similarity * 100)}%` : '';
96
- context.print(` ${record.id} [${record.cls}]${tagStr} ${ts}${scoreStr}`);
96
+ context.print(` ${record.id} [${record.cls}]${tagStr} ${ts}${scoreStr}${temporalStatusLabel(record)}`);
97
97
  context.print(` ${record.summary}`);
98
98
  if (record.provenance.length) {
99
99
  context.print(` via: ${record.provenance.map((entry) => `${entry.kind}:${entry.ref}`).join(', ')}`);
@@ -159,6 +159,11 @@ export async function handleRecallGet(args: string[], context: CommandContext):
159
159
  if (record.detail) context.print(` Detail: ${record.detail}`);
160
160
  if (record.tags.length) context.print(` Tags: ${record.tags.join(', ')}`);
161
161
  context.print(` Created: ${ts}`);
162
+ if (record.validFrom !== undefined || record.validUntil !== undefined) {
163
+ const from = record.validFrom !== undefined ? new Date(record.validFrom).toISOString().slice(0, 19).replace('T', ' ') : '(none)';
164
+ const until = record.validUntil !== undefined ? new Date(record.validUntil).toISOString().slice(0, 19).replace('T', ' ') : '(none)';
165
+ context.print(` Temporal: ${from} -> ${until}${temporalStatusLabel(record)}`);
166
+ }
162
167
 
163
168
  if (record.provenance.length) {
164
169
  context.print(' Provenance:');
@@ -249,7 +254,7 @@ export async function handleRecallList(args: string[], context: CommandContext):
249
254
  context.print(`\n[recall] ${clsName.toUpperCase()} (${group.length}):`);
250
255
  for (const record of group) {
251
256
  const tagStr = record.tags.length ? ` [${record.tags.join(', ')}]` : '';
252
- context.print(` ${record.id} [${record.scope}]${tagStr} ${record.summary}`);
257
+ context.print(` ${record.id} [${record.scope}]${tagStr} ${record.summary}${temporalStatusLabel(record)}`);
253
258
  }
254
259
  }
255
260
  }
@@ -1,5 +1,5 @@
1
1
  import type { CommandContext } from '../command-registry.ts';
2
- import { VALID_REVIEW_STATES, VALID_SCOPES, isValidReviewState, isValidScope } from './recall-shared.ts';
2
+ import { VALID_REVIEW_STATES, VALID_SCOPES, isValidReviewState, isValidScope, temporalStatusLabel } from './recall-shared.ts';
3
3
  import { getMemoryApi, getMemorySpine } from './recall-query.ts';
4
4
  import { buildMainSessionTurnInjectionsText, buildTurnInjectionsText } from '../../renderer/turn-injection.ts';
5
5
 
@@ -41,7 +41,7 @@ export async function handleRecallQueue(args: string[], context: CommandContext)
41
41
  context.print(`[recall] Review queue (${queue.length}):`);
42
42
  for (const record of queue) {
43
43
  const reason = record.staleReason ? ` — ${record.staleReason}` : '';
44
- context.print(` ${record.id} [${record.scope}/${record.cls}] ${record.reviewState} ${record.confidence}% ${record.summary}${reason}`);
44
+ context.print(` ${record.id} [${record.scope}/${record.cls}] ${record.reviewState} ${record.confidence}% ${record.summary}${reason}${temporalStatusLabel(record)}`);
45
45
  }
46
46
  }
47
47
 
@@ -1,5 +1,6 @@
1
1
  import type { ShellPathService } from '@/runtime/index.ts';
2
2
  import type { MemoryClass, MemoryReviewState, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
3
+ import { memoryRecordTemporalStatus } from '@pellux/goodvibes-sdk/platform/state';
3
4
 
4
5
  export const VALID_CLASSES: MemoryClass[] = ['decision', 'constraint', 'incident', 'pattern', 'fact', 'risk', 'runbook', 'architecture', 'ownership'];
5
6
  export const VALID_SCOPES: MemoryScope[] = ['session', 'project', 'team'];
@@ -20,3 +21,15 @@ export function isValidReviewState(s: string): s is MemoryReviewState {
20
21
  export function resolveBundlePath(pathArg: string, shellPaths: ShellPathService): string {
21
22
  return shellPaths.resolveWorkspacePath(pathArg);
22
23
  }
24
+
25
+ /**
26
+ * Compact inline label for a record's temporal validity window — appended
27
+ * wherever `/recall` lists records, so an out-of-window record is visibly
28
+ * `[pending]`/`[expired]` rather than silently indistinguishable from an
29
+ * always-valid one. Empty string for 'active' (the common case; no window,
30
+ * or currently inside it) so normal listings stay unnoisy.
31
+ */
32
+ export function temporalStatusLabel(record: { validFrom?: number | undefined; validUntil?: number | undefined }, now?: number): string {
33
+ const status = memoryRecordTemporalStatus(record, now);
34
+ return status === 'active' ? '' : ` [${status}]`;
35
+ }
@@ -0,0 +1,86 @@
1
+ import type { CommandContext } from '../command-registry.ts';
2
+ import type { CommandRegistry } from '../command-registry.ts';
3
+ import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
4
+
5
+ /**
6
+ * Register the /relay command — reachability status and QR-encodable pairing
7
+ * for the SDK's outbound zero-knowledge relay (relay.* settings, gated by the
8
+ * `relay-connect` feature flag). The live registration state lives only in
9
+ * the running daemon's memory (`DaemonServer.getRelayReachability()`), reached
10
+ * here through `ctx.platform.externalServices` — see relay-reachability-bridge.ts.
11
+ */
12
+
13
+ function relayConfigured(ctx: CommandContext): { enabled: boolean; url: string; rendezvousId: string; label: string; flagOn: boolean } {
14
+ const config = ctx.platform.configManager;
15
+ return {
16
+ enabled: config.get('relay.enabled') === true,
17
+ url: String(config.get('relay.url') ?? ''),
18
+ rendezvousId: String(config.get('relay.rendezvousId') ?? ''),
19
+ label: String(config.get('relay.label') ?? ''),
20
+ flagOn: ctx.platform.featureFlagManager?.isEnabled('relay-connect') ?? false,
21
+ };
22
+ }
23
+
24
+ async function renderStatus(ctx: CommandContext): Promise<void> {
25
+ const cfg = relayConfigured(ctx);
26
+ if (!cfg.enabled || !cfg.flagOn) {
27
+ ctx.print([
28
+ 'Relay: disabled',
29
+ ` relay.enabled: ${cfg.enabled ? 'yes' : 'no'}`,
30
+ ` relay-connect flag: ${cfg.flagOn ? 'on' : 'off'}`,
31
+ ' Both must be on for outbound relay reachability. See /config relay.',
32
+ ].join('\n'));
33
+ return;
34
+ }
35
+ const live = ctx.platform.externalServices?.relayStatus() ?? 'disabled';
36
+ const state = live === 'registered' ? 'registered' : live === 'disabled' ? 'disabled' : 'offline';
37
+ ctx.print([
38
+ `Relay: ${state}`,
39
+ ` rendezvous id: ${cfg.rendezvousId || '(not yet minted — start the daemon to generate one)'}`,
40
+ ` url: ${cfg.url || '(not set)'}`,
41
+ ` label: ${cfg.label || '(not set)'}`,
42
+ ` live connection state: ${live}`,
43
+ ' The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
44
+ ].join('\n'));
45
+ }
46
+
47
+ async function renderPair(ctx: CommandContext): Promise<void> {
48
+ const cfg = relayConfigured(ctx);
49
+ if (!cfg.enabled || !cfg.flagOn) {
50
+ ctx.print('Relay is disabled — enable relay.enabled and the relay-connect feature flag first (see /config relay, /flags).');
51
+ return;
52
+ }
53
+ const externalServices = ctx.platform.externalServices;
54
+ if (!externalServices) {
55
+ ctx.print('Relay pairing is unavailable: background service controller is not running in this build.');
56
+ return;
57
+ }
58
+ const minted = await externalServices.mintRelayPairing();
59
+ if (!minted) {
60
+ ctx.print('Could not mint a relay pairing payload — the relay connection may not be registered yet. Check /relay status.');
61
+ return;
62
+ }
63
+ const qrMatrix = generateQrMatrix(minted.encoded);
64
+ ctx.print([
65
+ 'Relay pairing payload (scan with a companion app, or copy the string below):',
66
+ '',
67
+ minted.encoded,
68
+ '',
69
+ renderQrToString(qrMatrix),
70
+ ].join('\n'));
71
+ }
72
+
73
+ export function registerRelayRuntimeCommands(registry: CommandRegistry): void {
74
+ registry.register({
75
+ name: 'relay',
76
+ description: 'Outbound relay reachability status, or mint a QR-encodable pairing payload',
77
+ usage: '[status|pair]',
78
+ argsHint: '[status|pair]',
79
+ async handler(args, ctx) {
80
+ const sub = args[0]?.toLowerCase();
81
+ if (sub === 'pair') return renderPair(ctx);
82
+ if (sub === undefined || sub === 'status') return renderStatus(ctx);
83
+ ctx.print(`Unknown /relay subcommand: ${sub}. Use: status | pair`);
84
+ },
85
+ });
86
+ }
@@ -0,0 +1,160 @@
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import type { DiffReviewPanel } from '../../panels/diff-review-panel.ts';
3
+ import type { ReviewHunk } from '../../panels/diff-review-model.ts';
4
+ import { hunkPatchText, buildHunkRevertReceiptBlock } from '../../panels/diff-review-model.ts';
5
+ import { requirePanelManager } from './runtime-services.ts';
6
+
7
+ /** checkpoints.revertHunkPreview result — the read-only clean-or-conflict check plus a confirm token. */
8
+ interface RevertHunkPreview {
9
+ readonly applies: boolean;
10
+ readonly conflict: string | null;
11
+ readonly addedLinesRemoved: number;
12
+ readonly removedLinesRestored: number;
13
+ readonly token: string | null;
14
+ }
15
+
16
+ /** checkpoints.revertHunk result — one applied hunk revert, or a confirmation refusal. */
17
+ interface RevertHunkResult {
18
+ readonly receipt: {
19
+ readonly path: string;
20
+ readonly hunkHeader: string;
21
+ readonly addedLinesRemoved: number;
22
+ readonly removedLinesRestored: number;
23
+ readonly safetyCheckpointId: string | null;
24
+ } | null;
25
+ readonly refused: boolean;
26
+ readonly refusal: { readonly reason: string } | null;
27
+ }
28
+
29
+ const INVOKE_CONTEXT = { context: { clientKind: 'tui' as const } };
30
+
31
+ /** True for the honest 409 the revert verb throws when the hunk drifted since it was captured. */
32
+ function isConflict(err: unknown): boolean {
33
+ return typeof err === 'object' && err !== null && (err as { status?: unknown }).status === 409;
34
+ }
35
+
36
+ function errorText(err: unknown): string {
37
+ return err instanceof Error ? err.message : String(err);
38
+ }
39
+
40
+ /**
41
+ * Reject one hunk: checkpoints.revertHunkPreview (read-only clean check + token)
42
+ * → DiffPanel confirm of exactly what reverts → checkpoints.revertHunk with that
43
+ * token. A stale hunk is an honest "changed since captured" with a refresh, never
44
+ * a partial write; a success renders the HUNK_REVERTED receipt into the transcript
45
+ * and refreshes the review panel so the reverted hunk disappears from the diff.
46
+ */
47
+ async function revertHunkFlow(ctx: CommandContext, panel: DiffReviewPanel, hunk: ReviewHunk): Promise<void> {
48
+ const gateway = ctx.workspace.gatewayMethods;
49
+ if (!gateway || !ctx.workspace.workspaceCheckpointManager) {
50
+ panel.note('Hunk revert unavailable: the checkpoint/gateway surface is not wired in this session.');
51
+ return;
52
+ }
53
+ const path = hunk.filePath;
54
+ const hunkText = hunkPatchText(hunk);
55
+ const sessionId = ctx.session.runtime.sessionId;
56
+
57
+ let preview: RevertHunkPreview;
58
+ try {
59
+ preview = (await gateway.invoke('checkpoints.revertHunkPreview', { ...INVOKE_CONTEXT, body: { path, hunk: hunkText } })) as RevertHunkPreview;
60
+ } catch (err) {
61
+ panel.note(`Could not check this hunk: ${errorText(err)}`);
62
+ return;
63
+ }
64
+
65
+ if (!preview.applies || !preview.token) {
66
+ panel.note(`Cannot revert — ${preview.conflict ?? 'this hunk no longer applies'}. The file changed since this diff was captured; press r again after /review reloads. Nothing was changed.`);
67
+ await panel.refresh();
68
+ return;
69
+ }
70
+
71
+ const { DiffPanel } = await import('../../panels/diff-panel.ts');
72
+ const pm = requirePanelManager(ctx);
73
+ let diff = pm.getAllOpen().find((p) => p.id === 'diff');
74
+ if (!diff) {
75
+ try { diff = pm.open('diff'); } catch { panel.note('Could not open the diff panel to confirm the revert.'); return; }
76
+ }
77
+ pm.activateById('diff');
78
+ if (!pm.isVisible()) pm.show();
79
+ ctx.focusPanels?.();
80
+ const diffPanel = diff as InstanceType<typeof DiffPanel>;
81
+ diffPanel.showDiff(path, hunkText);
82
+
83
+ const summary = `restore ${preview.removedLinesRestored} deleted / drop ${preview.addedLinesRemoved} added line(s)`;
84
+ diffPanel.confirmOverlay.arm({
85
+ id: `${path}:${hunk.header}`,
86
+ label: `Revert hunk in ${path} — ${summary}`,
87
+ verb: 'Revert',
88
+ onConfirm: async () => {
89
+ let result: RevertHunkResult;
90
+ try {
91
+ result = (await gateway.invoke('checkpoints.revertHunk', { ...INVOKE_CONTEXT, body: { path, hunk: hunkText, confirmToken: preview.token, sessionId } })) as RevertHunkResult;
92
+ } catch (err) {
93
+ pm.close('diff');
94
+ ctx.focusPanels?.();
95
+ if (isConflict(err)) {
96
+ panel.note(`Not reverted — ${errorText(err)}. The file changed since captured; nothing was written. Reloading /review…`);
97
+ await panel.refresh();
98
+ } else {
99
+ panel.note(`Revert failed: ${errorText(err)}`);
100
+ }
101
+ return;
102
+ }
103
+ pm.close('diff');
104
+ ctx.focusPanels?.();
105
+ const receipt = result.receipt;
106
+ if (!receipt) {
107
+ panel.note(`Revert not applied: ${result.refusal?.reason ?? 'confirmation was refused.'}`);
108
+ return;
109
+ }
110
+ ctx.session.conversationManager.addTypedSystemMessage(buildHunkRevertReceiptBlock(receipt), 'operational');
111
+ await panel.refresh();
112
+ },
113
+ onCancel: () => {
114
+ pm.close('diff');
115
+ ctx.focusPanels?.();
116
+ panel.note('Revert cancelled — nothing changed.');
117
+ },
118
+ });
119
+ ctx.renderRequest();
120
+ }
121
+
122
+ /**
123
+ * `/review` — open the comment-on-hunk review loop. Loads this session's file
124
+ * changes (the files the SDK SessionChangeTracker recorded) as a git working-
125
+ * tree diff, hunk-boundaried, and wires the panel's steering submit path to the
126
+ * session so an attached comment is sent to the model as a steering message with
127
+ * structured context (file path, line range, patch excerpt). A hunk can also be
128
+ * rejected: `r` reverse-applies exactly that one hunk via checkpoints.revertHunk
129
+ * (preview + confirm), a working-tree change recorded as a [Revert] receipt.
130
+ */
131
+ export function registerReviewRuntimeCommands(registry: CommandRegistry): void {
132
+ registry.register({
133
+ name: 'review',
134
+ aliases: [],
135
+ description: 'Review this session\'s diff hunk-by-hunk, steer comments, or revert a hunk',
136
+ async handler(_args, ctx) {
137
+ const pm = requirePanelManager(ctx);
138
+ let panel = pm.getAllOpen().find((p) => p.id === 'review');
139
+ if (!panel) {
140
+ try {
141
+ panel = pm.open('review');
142
+ } catch {
143
+ ctx.print('Could not open the review panel.');
144
+ return;
145
+ }
146
+ }
147
+ pm.activateById('review');
148
+ if (!pm.isVisible()) pm.show();
149
+
150
+ const reviewPanel = panel as DiffReviewPanel;
151
+ if (ctx.submitInput) {
152
+ reviewPanel.setSubmit((text) => ctx.submitInput!(text));
153
+ }
154
+ reviewPanel.setRevertHandler((hunk) => { void revertHunkFlow(ctx, reviewPanel, hunk); });
155
+ await reviewPanel.loadSessionReview();
156
+ ctx.focusPanels?.();
157
+ ctx.renderRequest();
158
+ },
159
+ });
160
+ }