@pellux/goodvibes-agent 1.8.0 → 1.8.2

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 (88) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
  5. package/dist/package/main.js +135434 -118764
  6. package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
  7. package/docs/tools-and-commands.md +9 -0
  8. package/package.json +3 -2
  9. package/src/agent/capability-registry.ts +300 -0
  10. package/src/agent/email/style-reply-lane.ts +13 -4
  11. package/src/agent/memory-consolidation-receipts.ts +79 -0
  12. package/src/agent/memory-prompt.ts +3 -1
  13. package/src/agent/operator-gateway-call.ts +134 -0
  14. package/src/agent/principal-attribution.ts +97 -0
  15. package/src/agent/prompt-context-receipts.ts +5 -1
  16. package/src/agent/unified-inbox.ts +17 -1
  17. package/src/cli/bundle-command.ts +2 -1
  18. package/src/cli/channel-profiles-command.ts +168 -0
  19. package/src/cli/ci-command.ts +214 -0
  20. package/src/cli/completion.ts +5 -0
  21. package/src/cli/connected-host-metrics.ts +165 -0
  22. package/src/cli/entrypoint.ts +25 -1
  23. package/src/cli/fleet-command.ts +178 -0
  24. package/src/cli/help.ts +115 -0
  25. package/src/cli/import-command.ts +208 -0
  26. package/src/cli/index.ts +1 -0
  27. package/src/cli/management-commands.ts +9 -8
  28. package/src/cli/management.ts +42 -0
  29. package/src/cli/memory-command.ts +4 -3
  30. package/src/cli/openclaw-import.ts +382 -0
  31. package/src/cli/operator-command-args.ts +88 -0
  32. package/src/cli/parser.ts +12 -0
  33. package/src/cli/personas-command.ts +3 -2
  34. package/src/cli/principals-command.ts +230 -0
  35. package/src/cli/profiles-command.ts +2 -1
  36. package/src/cli/relay-command.ts +100 -0
  37. package/src/cli/routines-command.ts +4 -3
  38. package/src/cli/skill-bundle-command.ts +3 -2
  39. package/src/cli/skills-command.ts +3 -2
  40. package/src/cli/status.ts +114 -2
  41. package/src/cli/temporal-label.ts +89 -0
  42. package/src/cli/types.ts +7 -0
  43. package/src/cli/workspaces-command.ts +135 -0
  44. package/src/config/agent-settings-policy.ts +8 -0
  45. package/src/config/checkpoint-settings.ts +113 -0
  46. package/src/config/workspace-registration.ts +299 -0
  47. package/src/core/rewind-turn-anchors.ts +82 -0
  48. package/src/input/agent-workspace-categories.ts +1 -0
  49. package/src/input/agent-workspace-channel-triage.ts +27 -8
  50. package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
  51. package/src/input/agent-workspace-onboarding-categories.ts +17 -1
  52. package/src/input/agent-workspace-snapshot.ts +25 -3
  53. package/src/input/agent-workspace-types.ts +5 -5
  54. package/src/input/commands/connected-host-admin-runtime.ts +75 -0
  55. package/src/input/commands.ts +2 -0
  56. package/src/input/settings-modal-types.ts +16 -5
  57. package/src/main.ts +17 -12
  58. package/src/permissions/approval-posture.ts +40 -5
  59. package/src/permissions/prompt.ts +35 -1
  60. package/src/renderer/settings-modal-helpers.ts +11 -0
  61. package/src/renderer/settings-modal.ts +21 -0
  62. package/src/renderer/terminal-escapes.ts +65 -12
  63. package/src/runtime/agent-runtime-events.ts +102 -3
  64. package/src/runtime/bootstrap-core.ts +111 -10
  65. package/src/runtime/bootstrap-external-services.ts +156 -0
  66. package/src/runtime/bootstrap.ts +73 -69
  67. package/src/runtime/context-accounting-source.ts +114 -0
  68. package/src/runtime/conversation-rewind-port.ts +133 -0
  69. package/src/runtime/index.ts +8 -28
  70. package/src/runtime/memory-consolidation-scheduler.ts +67 -0
  71. package/src/runtime/memory-consolidation-wiring.ts +56 -0
  72. package/src/runtime/memory-usage-wiring.ts +75 -0
  73. package/src/runtime/services.ts +257 -6
  74. package/src/runtime/tool-permission-safety.ts +18 -1
  75. package/src/shell/blocking-input.ts +38 -8
  76. package/src/shell/startup-wiring.ts +26 -1
  77. package/src/tools/agent-context-policy.ts +1 -1
  78. package/src/tools/agent-harness-cli-command-policy.ts +29 -3
  79. package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
  80. package/src/tools/agent-harness-memory-posture.ts +57 -2
  81. package/src/tools/agent-harness-metadata.ts +12 -0
  82. package/src/tools/agent-harness-mode-catalog.ts +1 -1
  83. package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
  84. package/src/tools/agent-harness-prompt-context.ts +3 -1
  85. package/src/tools/agent-harness-tool.ts +3 -2
  86. package/src/tools/agent-policy-explanation.ts +40 -0
  87. package/src/tools/tool-definition-compaction.ts +2 -0
  88. package/src/version.ts +1 -1
@@ -1,8 +1,11 @@
1
1
  import { join } from 'node:path';
2
+ import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
2
3
  import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
3
4
  import { shell as runtimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
4
5
  import type { shell as RuntimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
5
6
  import { SecretsManager } from '../config/secrets.ts';
7
+ import { readCheckpointGuardSettings, readCheckpointRegistrationSetting } from '../config/checkpoint-settings.ts';
8
+ import { createWorkspaceRegistrationLiveChecker, migrateLegacyWorkspaceRegistryIfNeeded } from '../config/workspace-registration.ts';
6
9
  import { FocusTracker } from '../core/focus-tracker.ts';
7
10
  import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
8
11
  import { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
@@ -10,6 +13,13 @@ import { AutomationDeliveryManager, AutomationManager, AutomationRouteStore } fr
10
13
  import { ChannelPluginRegistry, ChannelPolicyManager, RouteBindingManager, SurfaceRegistry } from '@pellux/goodvibes-sdk/platform/channels';
11
14
  import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
12
15
  import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
16
+ import { attachWsOnlyGatewayVerbHandlers, createArchivableFleetRegistry } from '@pellux/goodvibes-terminal-shell';
17
+ import { createSessionConversationRewindPort } from './conversation-rewind-port.ts';
18
+ // Not re-exported by @pellux/goodvibes-terminal-shell (only the gateway-verb
19
+ // composition and the registry factory are) — reached directly per the SDK
20
+ // adoption convention of going straight to the platform package for whatever
21
+ // terminal-shell does not already wrap, rather than hand-rolling the bridge.
22
+ import { attachFleetEmitBridge } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
13
23
  import type { SharedSessionRoutingIntent } from '@pellux/goodvibes-sdk/platform/control-plane';
14
24
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
15
25
  import { AGENT_SPINE_PARTICIPANT, SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
@@ -45,9 +55,17 @@ import { AgentOrchestrator } from '@pellux/goodvibes-sdk/platform/agents';
45
55
  import { ArchetypeLoader } from '@pellux/goodvibes-sdk/platform/agents';
46
56
  import { CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
47
57
  import { CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
48
- import { createProcessRegistry, withFleetArchive } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
49
58
  import { createOrchestrationEngine } from '@pellux/goodvibes-sdk/platform/orchestration';
50
59
  import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
60
+
61
+ /**
62
+ * Structural mirror of the SDK's CheckpointSessionResolver (checkpoint/manager):
63
+ * maps a triggering turn/agent lifecycle event to its owning session id. Declared
64
+ * locally because the type is re-exported only from the deep checkpoint subpath,
65
+ * not the `platform/workspace` barrel this module already imports; the shape is
66
+ * exact, so a resolver typed against it is assignable to the constructor option.
67
+ */
68
+ type CheckpointSessionResolver = (ctx: { readonly turnId?: string | undefined; readonly agentId?: string | undefined }) => string | undefined;
51
69
  import { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
52
70
  import { ModeManager } from '@pellux/goodvibes-sdk/platform/state';
53
71
  import { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
@@ -95,6 +113,7 @@ import { FileStateCache } from '@pellux/goodvibes-sdk/platform/state';
95
113
  import { ProjectIndex } from '@pellux/goodvibes-sdk/platform/state';
96
114
  import { IdempotencyStore } from '@/runtime/index.ts';
97
115
  import { OverflowHandler } from '@pellux/goodvibes-sdk/platform/tools';
116
+ import { ContextAccountingHolder } from '@pellux/goodvibes-sdk/platform/tools';
98
117
  import { ToolLLM } from '@pellux/goodvibes-sdk/platform/config';
99
118
  import { ComponentHealthMonitor } from '@/runtime/index.ts';
100
119
  import { SandboxSessionRegistry } from '@/runtime/index.ts';
@@ -397,6 +416,16 @@ export interface RuntimeServicesOptions {
397
416
  readonly localUserAuthManager?: UserAuthManager;
398
417
  readonly featureFlags?: FeatureFlagManager;
399
418
  readonly getConversationTitle?: () => string | undefined;
419
+ /**
420
+ * Maps a triggering turn/agent lifecycle event to the owning session id so the
421
+ * WorkspaceCheckpointManager can stamp automatic (and defaulted explicit)
422
+ * checkpoints with it. Without this, same-launch checkpoints are written with
423
+ * an undefined session id and the session-scoped restore/rewind lookup filters
424
+ * them out — so a checkpoint made this launch could not be activated until a
425
+ * restart. Consulted live at each event, so a resolver reading a mutable
426
+ * session-id ref reflects the current session without re-wiring.
427
+ */
428
+ readonly resolveSessionId?: CheckpointSessionResolver;
400
429
  readonly workingDir: string;
401
430
  readonly homeDirectory: string;
402
431
  }
@@ -549,6 +578,15 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
549
578
  secretsManager,
550
579
  subscriptionManager,
551
580
  });
581
+ // Settable holder for the context_accounting tool's session source (SDK
582
+ // 1.6.1). Constructed here (unbound) so it exists for the whole runtime
583
+ // services lifetime and every consumer of `services.contextAccountingHolder`
584
+ // sees the SAME instance the tool roster is registered against — bootstrap.ts
585
+ // binds an Orchestrator-backed ContextAccountingSource onto it once the
586
+ // interactive Orchestrator exists (see bindOrchestratorContextAccounting in
587
+ // context-accounting-source.ts). Left unbound here: the tool honestly
588
+ // reports "no live session context bound" until that bind call runs.
589
+ const contextAccountingHolder = new ContextAccountingHolder();
552
590
  const providerCapabilityRegistry = new ProviderCapabilityRegistry();
553
591
  const cacheHitTracker = new CacheHitTracker();
554
592
  const favoritesStore = new FavoritesStore({ dir: shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT) });
@@ -913,9 +951,14 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
913
951
  // never fires for the Agent.
914
952
  // • processRegistry — fleet observability over the managers the Agent
915
953
  // already owns (agents, wrfc, processes, watchers).
916
- // • workspaceCheckpointManager — constructed without a runtimeBus so it
917
- // never auto-subscribes to turn events / snapshots
918
- // the workspace; inert unless explicitly invoked.
954
+ // • workspaceCheckpointManager — a runtimeBus (hence automatic turn/agent-
955
+ // lifecycle snapshots) is only passed when
956
+ // `workingDirectory` is a registered workspace (or
957
+ // the unregistered-workspaces override is
958
+ // "guarded"); otherwise it stays inert unless
959
+ // explicitly invoked. See the construction site
960
+ // below for the full registered-workspaces-only
961
+ // rule (owner ruling, 2026-07-10).
919
962
  const orchestrationEngine = createOrchestrationEngine({
920
963
  agentManager,
921
964
  configManager,
@@ -934,7 +977,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
934
977
  });
935
978
  // Archive-aware: finished agent/swarm subtrees can be moved out of the
936
979
  // live fleet view into a session-scoped archive (SDK fleet/archive.ts).
937
- const processRegistry = withFleetArchive(createProcessRegistry({
980
+ // createArchivableFleetRegistry is the shared terminal-shell wrapper over the
981
+ // SDK's withFleetArchive(createProcessRegistry(...)) — one named seam both
982
+ // daemon front-ends build the registry through so it cannot drift.
983
+ const processRegistry = createArchivableFleetRegistry({
938
984
  agentManager,
939
985
  wrfcController,
940
986
  orchestrationEngine,
@@ -947,17 +993,222 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
947
993
  messageBus: agentMessageBus,
948
994
  automationManager,
949
995
  runtimeBus: options.runtimeBus,
950
- }));
996
+ });
997
+ // Root/retention guard options come from the user's `checkpoints.*` settings
998
+ // (see config/checkpoint-settings.ts); any key the user did not set is omitted
999
+ // so the SDK manager applies its own default. These guards are defense in
1000
+ // depth UNDER the registered-workspaces-only rule below, not replaced by it:
1001
+ // even a registered root can still be refused as too broad or too large.
1002
+ //
1003
+ // Registered-workspaces-only (owner ruling, 2026-07-10; live-gating extended
1004
+ // 2026-07-11 so registering mid-launch takes effect without a restart — the
1005
+ // same stale-until-restart defect class fixed for permission-mode metadata).
1006
+ // The SDK's WorkspaceCheckpointManager only takes automatic turn/agent-
1007
+ // lifecycle snapshots when constructed WITH a runtimeBus (see the SDK's own
1008
+ // platform/runtime/services.ts, which always passes one). This composition
1009
+ // root ALWAYS passes runtimeBus now (so the automatic-snapshot subscription
1010
+ // is live from process start regardless of boot-time registration state),
1011
+ // and instead gates each individual automatic snapshot attempt through a
1012
+ // LIVE re-check via `checkpointsCurrentlyAllowed` below — patched onto this
1013
+ // one manager instance's own `create` method, since the SDK has no built-in
1014
+ // predicate hook for this. `checkpointsCurrentlyAllowed` is cheap to call on
1015
+ // every event: `createWorkspaceRegistrationLiveChecker` probes the git
1016
+ // worktree link ONCE (a `git` subprocess spawn) and every call after that
1017
+ // only re-reads the shared registration JSON file. "Allowed" means
1018
+ // `workingDirectory` resolves as COVERED by the shared registration store
1019
+ // (SDK 1.6.1 platform/workspace/registration, via
1020
+ // config/workspace-registration.ts — the successor to this fork's local
1021
+ // per-user registry, migrated in once below), or the owner has explicitly
1022
+ // opted an unregistered workspace back in via
1023
+ // `checkpoints.unregisteredWorkspaces: "guarded"` (config/checkpoint-settings.ts;
1024
+ // default "off"). Coverage flows down a registered root's subtree AND
1025
+ // through the git worktree→main-repo link, so an orchestration-spawned
1026
+ // worktree of a registered repo checkpoints automatically without being
1027
+ // registered itself. A workspace SWAP at runtime is still not tracked here
1028
+ // (WorkspaceSwapManager's rerootStores does not re-root this manager; a swap
1029
+ // needs a restart to pick up a new root's registration) — only the
1030
+ // registration STATE of this fixed workingDirectory is now live.
1031
+ const registrationMigration = migrateLegacyWorkspaceRegistryIfNeeded(shellPaths);
1032
+ if (registrationMigration) {
1033
+ logger.info('Migrated the local workspace registry into the shared registration store', { ...registrationMigration });
1034
+ }
1035
+ const checkpointsRegistrationStatus = createWorkspaceRegistrationLiveChecker(shellPaths, workingDirectory);
1036
+ const checkpointsCurrentlyAllowed = (): boolean =>
1037
+ checkpointsRegistrationStatus() === 'covered' || readCheckpointRegistrationSetting(configManager) === 'guarded';
951
1038
  const workspaceCheckpointManager = new WorkspaceCheckpointManager({
952
1039
  workspaceRoot: workingDirectory,
1040
+ ...readCheckpointGuardSettings(configManager),
1041
+ runtimeBus: options.runtimeBus,
1042
+ // Stamp automatic snapshots with the live session id so a checkpoint created
1043
+ // during this launch is found by the session-scoped restore/rewind lookup —
1044
+ // the fix that makes same-launch activation work without a restart. The
1045
+ // resolver is consulted at each lifecycle event, so it tracks the current
1046
+ // session even though the id is finalized after this manager is built.
1047
+ ...(options.resolveSessionId ? { resolveSessionId: options.resolveSessionId } : {}),
953
1048
  });
1049
+ // Gate AUTOMATIC snapshots (kind 'turn' | 'agent-run', fired internally by
1050
+ // the manager's own bus subscription via `this.create(...)`) on the live
1051
+ // registration check, by overriding this instance's own `create` method —
1052
+ // the manager has no predicate-hook option, and this is the one seam that
1053
+ // every automatic-snapshot call and every explicit caller both go through.
1054
+ // 'manual' (explicit, gateway-invoked) creates are NOT re-gated here: they
1055
+ // already go through checkpointsGatewayManager.create's own live check
1056
+ // below, which throws an actionable message BEFORE ever reaching this
1057
+ // method — this override would just be a silent, redundant pass for that
1058
+ // path. For an automatic snapshot there is no caller to throw to (the
1059
+ // manager's own subscription callbacks only `.catch()`-log a rejection), so
1060
+ // an automatic attempt against an unregistered/un-guarded workspace resolves
1061
+ // to `null` quietly — the same "cheap no-op" contract `create()` already
1062
+ // documents for an unchanged tree, not a new error path.
1063
+ const originalCreateCheckpoint = workspaceCheckpointManager.create.bind(workspaceCheckpointManager);
1064
+ workspaceCheckpointManager.create = ((opts) => {
1065
+ if (opts.kind !== 'manual' && !checkpointsCurrentlyAllowed()) return Promise.resolve(null);
1066
+ return originalCreateCheckpoint(opts);
1067
+ }) as typeof workspaceCheckpointManager.create;
1068
+ // Eagerly initialize so the automatic snapshot subscription is wired up
1069
+ // immediately (mirrors the SDK's own default runtime services) rather than
1070
+ // only on first explicit checkpoints.* call — otherwise the very first
1071
+ // TURN_COMPLETED could arrive before anything has touched the manager. Now
1072
+ // unconditional (previously skipped for an unregistered-at-boot workspace):
1073
+ // the subscription itself is inert-by-gate rather than inert-by-absence, so
1074
+ // a workspace registered mid-launch starts producing automatic snapshots on
1075
+ // the very next eligible event, with no restart needed to (re)wire the bus.
1076
+ void workspaceCheckpointManager.init().catch((err) => {
1077
+ logger.warn('WorkspaceCheckpointManager.init failed', { error: err instanceof Error ? err.message : String(err) });
1078
+ });
1079
+ // Explicit user-invoked checkpoint creation (the ws-only `checkpoints.create`
1080
+ // gateway verb) is gated the SAME way: refuse with an actionable hint rather
1081
+ // than silently registering the workspace on the caller's behalf. This is
1082
+ // the chosen policy for the pending owner ruling (the alternative —
1083
+ // register-and-proceed — was NOT chosen, to keep "create a checkpoint" from
1084
+ // having a side effect the caller did not ask for). list/diff/restore/
1085
+ // sessionChanges are read/restore operations over checkpoints that may
1086
+ // already exist (e.g. from a since-unregistered workspace) and are left
1087
+ // unrestricted here.
1088
+ const checkpointsGatewayManager: Pick<WorkspaceCheckpointManager, 'list' | 'create' | 'diff' | 'restore' | 'sessionChanges' | 'workspaceRoot'> = {
1089
+ workspaceRoot: workspaceCheckpointManager.workspaceRoot,
1090
+ list: workspaceCheckpointManager.list.bind(workspaceCheckpointManager),
1091
+ diff: workspaceCheckpointManager.diff.bind(workspaceCheckpointManager),
1092
+ restore: workspaceCheckpointManager.restore.bind(workspaceCheckpointManager),
1093
+ sessionChanges: workspaceCheckpointManager.sessionChanges.bind(workspaceCheckpointManager),
1094
+ create: (opts) => {
1095
+ // Re-reads the registry and setting live via the same cheap checker the
1096
+ // automatic-snapshot gate above uses, so registering the workspace via
1097
+ // `goodvibes-agent workspaces register` takes effect for the NEXT
1098
+ // explicit create call (same as it always has) — and, as of the live
1099
+ // gate above, for automatic snapshots too, without a restart either way.
1100
+ if (!checkpointsCurrentlyAllowed()) {
1101
+ throw new Error(
1102
+ `Checkpoints are off for this workspace: ${workingDirectory} is not registered. `
1103
+ + 'Register it first, then retry: goodvibes-agent workspaces register --yes '
1104
+ + '(or set checkpoints.unregisteredWorkspaces to "guarded" to opt this workspace out of the registration gate).',
1105
+ );
1106
+ }
1107
+ // Default the session stamp from the live resolver when the caller omits it
1108
+ // (the resolveSessionId hook only auto-stamps AUTOMATIC snapshots, not
1109
+ // explicit create calls). Without this, an explicit checkpoint made this
1110
+ // launch would be written unstamped and excluded by the session-scoped
1111
+ // restore lookup — the same same-launch activation gap the resolver closes
1112
+ // for automatic snapshots. An explicitly supplied sessionId always wins.
1113
+ const sessionId = opts.sessionId ?? options.resolveSessionId?.({});
1114
+ return workspaceCheckpointManager.create(sessionId ? { ...opts, sessionId } : opts);
1115
+ },
1116
+ };
1117
+
1118
+ // Attach handlers for every ws-only gateway verb group (fleet.* including
1119
+ // the archive verbs, checkpoints.*, sessions.search, push.*, principals.*,
1120
+ // channels.profiles.*, ci.*, and — when the deps below are all present —
1121
+ // checkin.*). Without this call the catalog carries descriptors but no
1122
+ // handlers, and every one of those verbs answers 501 "Gateway method is not
1123
+ // invokable" — the same gap the companion app found on the TUI-vendored
1124
+ // daemon. Mirrors the SDK runtime's composition root (goodvibes-sdk
1125
+ // platform/runtime/services.ts). attachWsOnlyGatewayVerbHandlers is the
1126
+ // shared terminal-shell wrapper over the SDK's registerGatewayVerbGroups —
1127
+ // the single named call site both front-ends bind these handlers through,
1128
+ // gated by the package's conformance check.
1129
+ //
1130
+ // configManager and runtimeStore are required (SDK 1.6.1): they back
1131
+ // sessions.permissionMode.get/set and sessions.contextUsage.get. The four
1132
+ // check-in deps (channelDeliveryRouter, providerRegistry, automationManager,
1133
+ // sessionLister) are each optional individually, but the check-in verb
1134
+ // group (checkin.config.get/set, checkin.run, checkin.receipts.list) is
1135
+ // registered ONLY when all four are present — every one of them is already
1136
+ // constructed above in this composition root, so the Agent wires all four:
1137
+ // the proactive check-in loop runs for real here, not as a facade. Off by
1138
+ // default via checkin.enabled (see config/schema-domain-runtime.ts).
1139
+ attachWsOnlyGatewayVerbHandlers(gatewayMethods, {
1140
+ processRegistry,
1141
+ workspaceCheckpointManager: checkpointsGatewayManager,
1142
+ sessionBroker,
1143
+ secretsManager,
1144
+ approvalBroker,
1145
+ shellPaths,
1146
+ configManager,
1147
+ runtimeStore: options.runtimeStore,
1148
+ channelDeliveryRouter,
1149
+ providerRegistry,
1150
+ automationManager,
1151
+ sessionLister: sessionBroker,
1152
+ // The best-of-N surface (fleet.attempts.list/pick/judge): the
1153
+ // orchestration engine already implements FleetAttemptsController
1154
+ // (listHeldMergeGroups/pickAttemptWinner/proposeAttemptWinner) — wiring
1155
+ // it here is what turns those verbs from cataloged-but-unhandled into
1156
+ // real handlers (see src/test/daemon/gateway-ws-only-invokable.test.ts).
1157
+ attemptsController: orchestrationEngine,
1158
+ // worktrees.setup.run (SDK 1.6.1): the rerun affordance for worktree
1159
+ // cold-start setup, registered over a WorktreeRegistry rooted at this
1160
+ // daemon's own working directory (matching worktrees.snapshot's reader)
1161
+ // only when workingDirectory is present. It already is here — this was a
1162
+ // real cataloged-but-unhandled gap (found by the gateway parity pin
1163
+ // sweep, see src/test/daemon/gateway-parity-verb-families.test.ts) with
1164
+ // no reason to leave unfixed: the daemon's own working directory was
1165
+ // already in scope in this composition root (see
1166
+ // checkpointsGatewayManager above).
1167
+ workingDirectory,
1168
+ // Conversation half of the unified rewind (rewind.plan/apply with scope
1169
+ // 'conversation' or 'both'): this Agent's own ConversationManager (see
1170
+ // src/core/conversation.ts) IS a genuine in-process mutable conversation
1171
+ // store — the exact shape RewindConversationPort assumes ("a daemon-hosted
1172
+ // mutable conversation store") — so it is wired here rather than left
1173
+ // null, ported from goodvibes-tui's identical seam
1174
+ // (conversation-rewind-port.ts, registerSessionConversation at bootstrap;
1175
+ // see bootstrap-core.ts). createSessionConversationRewindPort() reads the
1176
+ // live per-session registry lazily (no conversation reference needed at
1177
+ // construction time), so it can be called here even though bootstrap
1178
+ // wires the actual session registration separately. See
1179
+ // src/test/daemon/gateway-rewind-conversation-scope.test.ts for the live
1180
+ // proof this resolves a real conversation, not a stub.
1181
+ conversationRewindPort: createSessionConversationRewindPort(),
1182
+ });
1183
+ // Turn the fleet registry's coalesced snapshot tick into poll-free
1184
+ // spawn/progress/attention/completion events on the runtime event bus's
1185
+ // 'fleet' domain (SDK 1.6.1, runtime/fleet/emit-bridge.ts) — the SDK's own
1186
+ // doc note on this bridge: "The control-plane gateway already fans every
1187
+ // runtime-bus domain out to subscribed SSE/WebSocket clients, so no
1188
+ // gateway/channel change is needed once the fleet domain exists." Without
1189
+ // this call the fleet.* gateway verbs above still answer pull queries
1190
+ // (fleet.snapshot/fleet.list), but nothing pushes live deltas — a webui
1191
+ // FleetView watching this agent's orchestrator-spawned sub-agents would see
1192
+ // them only on manual refresh, never their attention state flipping to
1193
+ // needs-input in real time. No unsubscribe is kept: the bridge is meant to
1194
+ // live for the registry's lifetime (same non-disposed pattern as the
1195
+ // fleet/push verb registrations just above; see the SDK's own doc comment
1196
+ // on attachFleetEmitBridge).
1197
+ attachFleetEmitBridge({ registry: processRegistry, bus: options.runtimeBus });
1198
+
1199
+ // The relay step-up ceremony service the SDK's RuntimeServices requires (and
1200
+ // the daemon facade dereferences at start). Constructed exactly as the SDK
1201
+ // composition root does, from the public @pellux/goodvibes-sdk/daemon export.
1202
+ const stepUpService = new StepUpService({ secrets: secretsManager });
954
1203
 
955
1204
  return {
1205
+ stepUpService,
956
1206
  workingDirectory,
957
1207
  homeDirectory,
958
1208
  shellPaths,
959
1209
  configManager,
960
1210
  featureFlags,
1211
+ contextAccountingHolder,
961
1212
  orchestrationEngine,
962
1213
  codeIndexStore,
963
1214
  codeIndexReindexScheduler,
@@ -1,5 +1,6 @@
1
1
  import type { PermissionCategory, PermissionCheckResult } from '@pellux/goodvibes-sdk/platform/permissions';
2
2
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
+ import { HARNESS_MODE_DESCRIPTORS } from '../tools/agent-harness-mode-catalog.ts';
3
4
 
4
5
  type PermissionManagerLike = {
5
6
  check(toolName: string, args: Record<string, unknown>): Promise<boolean>;
@@ -18,6 +19,7 @@ const READ_TOOL_NAMES = new Set([
18
19
  'state',
19
20
  'registry',
20
21
  'route',
22
+ 'repo_map',
21
23
  'goodvibes_context',
22
24
  'autonomy',
23
25
  'channels',
@@ -36,7 +38,6 @@ const READ_TOOL_NAMES = new Set([
36
38
  'settings',
37
39
  'vibe',
38
40
  'workspace',
39
- 'agent_harness',
40
41
  'agent_knowledge',
41
42
  'agent_operator_briefing',
42
43
  ]);
@@ -76,6 +77,18 @@ const READ_ONLY_AUTONOMY_ACTIONS = new Set(['', 'intake', 'request', 'route', 'p
76
77
  const READ_ONLY_DELEGATION_ACTIONS = new Set(['', 'status', 'summary', 'overview', 'policy', 'decision', 'decisions', 'delegation_posture', 'routes', 'list', 'catalog', 'posture', 'route', 'item', 'card', 'show', 'inspect', 'delegation_route']);
77
78
  const READ_ONLY_EXECUTION_ACTIONS = new Set(['', 'status', 'summary', 'overview', 'routes', 'posture', 'execution_posture', 'route', 'show_route', 'inspect_route', 'execution_route', 'history', 'activity', 'records', 'execution_history', 'record', 'item', 'show', 'inspect', 'execution_history_item', 'processes', 'background', 'backgrounds', 'background_processes', 'capabilities', 'process_capabilities', 'process', 'background_process', 'recovery', 'file_recovery', 'undo_redo']);
78
79
 
80
+ // The agent_harness tool takes a `mode`. Every mode whose catalog kind is not
81
+ // 'effect' only inspects, lists, or aliases read-only state, so it classifies as
82
+ // 'read'. Every 'effect' mode changes settings, memory, skills, personas,
83
+ // routines, keybindings, drafts, or runs a promotion/consolidation/command pass,
84
+ // so it classifies as a write. A mode string that is absent from the catalog
85
+ // (unknown or misspelled) is deliberately treated as a write, never a read.
86
+ const READ_ONLY_HARNESS_MODES: ReadonlySet<string> = new Set(
87
+ HARNESS_MODE_DESCRIPTORS
88
+ .filter((descriptor) => descriptor.kind !== 'effect')
89
+ .map((descriptor) => descriptor.id),
90
+ );
91
+
79
92
  type MarkedPermissionManager = PermissionManagerLike & { [SAFETY_MARKER]?: true };
80
93
 
81
94
  export function installPermissionManagerSafetyGuard(manager: PermissionManagerLike): void {
@@ -275,6 +288,10 @@ export function fallbackPermissionCategoryForArgs(toolName: string, args: Record
275
288
  : '';
276
289
  return READ_ONLY_EXECUTION_ACTIONS.has(action) ? 'read' : 'write';
277
290
  }
291
+ if (toolName === 'agent_harness') {
292
+ const mode = typeof args.mode === 'string' ? args.mode.trim() : '';
293
+ return READ_ONLY_HARNESS_MODES.has(mode) ? 'read' : 'write';
294
+ }
278
295
  if (toolName === 'agent_artifacts') {
279
296
  const mode = typeof args.mode === 'string' ? args.mode.trim() : '';
280
297
  return mode === 'list' || mode === 'show' ? 'read' : 'write';
@@ -3,15 +3,28 @@ import type { PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissio
3
3
  import type { SessionSnapshot } from '@/runtime/index.ts';
4
4
  import type { SystemMessageRouter } from '../core/system-message-router.ts';
5
5
  import { readConversationMessageSnapshots } from '../core/conversation-message-snapshot.ts';
6
+ import { answerWorkspaceRegistrationPrompt, type StoreShellPaths } from '../config/workspace-registration.ts';
6
7
 
7
8
  export type PendingPermissionState = PermissionRequest & {
8
9
  resolve: (approved: boolean, remember?: boolean) => void;
9
10
  };
10
11
 
12
+ /**
13
+ * First-start registration prompt (owner-approved design): the root that was
14
+ * offered, and the shellPaths needed to answer it (register or decline)
15
+ * against the shared registration store without threading a separate
16
+ * callback through every caller — see answerWorkspaceRegistrationPrompt.
17
+ */
18
+ export type PendingWorkspaceRegistrationState = {
19
+ readonly root: string;
20
+ readonly shellPaths: StoreShellPaths;
21
+ };
22
+
11
23
  export type BlockingInputHandlerOptions = {
12
24
  data: string;
13
25
  pendingPermission: PendingPermissionState | null;
14
26
  recoveryPending: boolean;
27
+ pendingWorkspaceRegistration: PendingWorkspaceRegistrationState | null;
15
28
  abortTurn: () => void;
16
29
  conversation: ConversationManager;
17
30
  systemMessageRouter: SystemMessageRouter;
@@ -24,6 +37,7 @@ export type BlockingInputHandlerResult = {
24
37
  handled: boolean;
25
38
  pendingPermission: PendingPermissionState | null;
26
39
  recoveryPending: boolean;
40
+ pendingWorkspaceRegistration: PendingWorkspaceRegistrationState | null;
27
41
  };
28
42
 
29
43
  export function handleBlockingShellInput(
@@ -33,6 +47,7 @@ export function handleBlockingShellInput(
33
47
  data,
34
48
  pendingPermission,
35
49
  recoveryPending,
50
+ pendingWorkspaceRegistration,
36
51
  abortTurn,
37
52
  conversation,
38
53
  systemMessageRouter,
@@ -48,24 +63,24 @@ export function handleBlockingShellInput(
48
63
  if (key === 'y') {
49
64
  req.resolve(true, false);
50
65
  render();
51
- return { handled: true, pendingPermission: null, recoveryPending };
66
+ return { handled: true, pendingPermission: null, recoveryPending, pendingWorkspaceRegistration };
52
67
  }
53
68
 
54
69
  if (key === 'a') {
55
70
  req.resolve(true, true);
56
71
  render();
57
- return { handled: true, pendingPermission: null, recoveryPending };
72
+ return { handled: true, pendingPermission: null, recoveryPending, pendingWorkspaceRegistration };
58
73
  }
59
74
 
60
75
  if (key === 'n' || data === '\x1b' || data === '\x03') {
61
76
  req.resolve(false, false);
62
77
  abortTurn();
63
78
  render();
64
- return { handled: true, pendingPermission: null, recoveryPending };
79
+ return { handled: true, pendingPermission: null, recoveryPending, pendingWorkspaceRegistration };
65
80
  }
66
81
 
67
82
  render();
68
- return { handled: true, pendingPermission, recoveryPending };
83
+ return { handled: true, pendingPermission, recoveryPending, pendingWorkspaceRegistration };
69
84
  }
70
85
 
71
86
  if (recoveryPending) {
@@ -79,21 +94,36 @@ export function handleBlockingShellInput(
79
94
  }
80
95
  deleteRecoveryFile();
81
96
  render();
82
- return { handled: true, pendingPermission: null, recoveryPending: false };
97
+ return { handled: true, pendingPermission: null, recoveryPending: false, pendingWorkspaceRegistration };
83
98
  }
84
99
 
85
100
  if (data === '\x1b' || data === '\x03') {
86
101
  systemMessageRouter.high('[Recovery] Discarded recovery data.');
87
102
  deleteRecoveryFile();
88
103
  render();
89
- return { handled: true, pendingPermission: null, recoveryPending: false };
104
+ return { handled: true, pendingPermission: null, recoveryPending: false, pendingWorkspaceRegistration };
90
105
  }
91
106
 
92
107
  systemMessageRouter.high('[Recovery] Ignored saved session; starting a new prompt.');
93
108
  deleteRecoveryFile();
94
109
  render();
95
- return { handled: false, pendingPermission: null, recoveryPending: false };
110
+ return { handled: false, pendingPermission: null, recoveryPending: false, pendingWorkspaceRegistration };
111
+ }
112
+
113
+ // First-start registration prompt: 'y' registers, EVERY other key (Escape,
114
+ // Enter-through, Ctrl+C, any stray key) declines — default no, matching the
115
+ // owner-approved design. Subtree-scoped: never asks again at this root
116
+ // either way, since both add() and decline() are recorded against it.
117
+ if (pendingWorkspaceRegistration) {
118
+ const { root, shellPaths } = pendingWorkspaceRegistration;
119
+ const accepted = data.toLowerCase().trim() === 'y';
120
+ answerWorkspaceRegistrationPrompt(shellPaths, root, accepted);
121
+ systemMessageRouter.high(accepted
122
+ ? `[Workspace] Registered ${root} — automatic checkpoints are now allowed here.`
123
+ : `[Workspace] Not registered — automatic checkpoints stay off here (won't ask again for this location).`);
124
+ render();
125
+ return { handled: true, pendingPermission: null, recoveryPending, pendingWorkspaceRegistration: null };
96
126
  }
97
127
 
98
- return { handled: false, pendingPermission, recoveryPending };
128
+ return { handled: false, pendingPermission, recoveryPending, pendingWorkspaceRegistration };
99
129
  }
@@ -3,6 +3,7 @@ import type { HookDispatcher, HookEventPath, HookPhase, HookCategory } from '@pe
3
3
  import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
4
4
  import {
5
5
  checkRecoveryFile,
6
+ createShellPathService,
6
7
  formatReturnContextForDisplay,
7
8
  persistConversation,
8
9
  writeRecoveryFile,
@@ -14,6 +15,8 @@ import {
14
15
  } from '../runtime/onboarding/index.ts';
15
16
  import { deriveOnboardingState } from '../runtime/onboarding/onboarding-state.ts';
16
17
  import { buildSetupIncompleteHint } from '../core/setup-incomplete-hint.ts';
18
+ import { isBroadWorkspaceRoot, normalizeWorkspaceRoot, resolveWorkspaceRegistrationSync } from '../config/workspace-registration.ts';
19
+ import type { PendingWorkspaceRegistrationState } from './blocking-input.ts';
17
20
 
18
21
  export interface SessionPersistenceAndRecoveryDeps {
19
22
  readonly buildCurrentSessionSnapshot: () => SessionSnapshot;
@@ -41,6 +44,8 @@ export interface SessionPersistenceAndRecoveryResult {
41
44
  recoveryInterval: ReturnType<typeof setInterval>;
42
45
  /** True if an unsaved recovery session was found and the user prompt was shown. */
43
46
  recoveryPending: boolean;
47
+ /** Set when the first-start registration prompt was shown this launch (see below). */
48
+ pendingWorkspaceRegistration: PendingWorkspaceRegistrationState | null;
44
49
  }
45
50
 
46
51
  /**
@@ -69,6 +74,7 @@ export function wireSessionPersistenceAndRecovery(
69
74
  sessionManager,
70
75
  onStreamSpeedUpdate,
71
76
  } = deps;
77
+ const shellPaths = createShellPathService({ workingDirectory: workingDir, homeDirectory });
72
78
 
73
79
  // --- Streaming speed + tool preview wiring ---
74
80
  let streamStartTime = 0;
@@ -125,7 +131,26 @@ export function wireSessionPersistenceAndRecovery(
125
131
  );
126
132
  }, 60_000);
127
133
 
128
- return { recoveryInterval, recoveryPending };
134
+ // First-start registration prompt (owner-approved design): only when this
135
+ // directory resolves to unknown (not covered, not declined) AND it isn't a
136
+ // broad root the store would refuse anyway (see isBroadWorkspaceRoot's own
137
+ // doc comment on why an "offer" check is safe to keep separate from the
138
+ // store's authoritative write-time guard). Skipped when a recovery prompt
139
+ // already claimed this launch's ambient attention, and while onboarding is
140
+ // still incomplete — the full-screen onboarding wizard would otherwise
141
+ // compete with this prompt for the very next keypress the owner types.
142
+ let pendingWorkspaceRegistration: PendingWorkspaceRegistrationState | null = null;
143
+ const onboardingDone = Boolean(readOnboardingCompletionMarker(shellPaths, 'user').payload);
144
+ if (!recoveryPending && onboardingDone) {
145
+ const resolution = resolveWorkspaceRegistrationSync(shellPaths, workingDir);
146
+ if (resolution.status === 'unknown' && !isBroadWorkspaceRoot(shellPaths, workingDir)) {
147
+ systemMessageRouter.high(`[Workspace] "${workingDir}" is not a registered workspace, so automatic (turn-end) checkpoints are off here. Register it? Press "y" to register, any other key to decline (won't ask again for this location).`);
148
+ render();
149
+ pendingWorkspaceRegistration = { root: normalizeWorkspaceRoot(workingDir), shellPaths };
150
+ }
151
+ }
152
+
153
+ return { recoveryInterval, recoveryPending, pendingWorkspaceRegistration };
129
154
  }
130
155
 
131
156
  /**
@@ -147,7 +147,7 @@ function buildAgentCapabilitiesContract(registry?: ToolRegistry): Record<string,
147
147
  {
148
148
  area: 'Project and computer work',
149
149
  can: 'Read, search, analyze, edit, and write workspace files; run bounded shell commands; inspect diffs and project structure; recover recent file edit/write snapshots.',
150
- tools: ['read', 'find', 'analyze', 'inspect', 'edit', 'write', 'exec', optionalTool('execution'), optionalTool('delegation')].filter(has),
150
+ tools: ['read', 'find', 'analyze', 'inspect', 'edit', 'write', 'exec', optionalTool('repo_map'), optionalTool('execution'), optionalTool('delegation')].filter(has),
151
151
  inspect: 'execution action:"status"; delegation action:"status"',
152
152
  },
153
153
  {
@@ -68,12 +68,30 @@ export function describeCliCommandPolicy(commandName: string): CommandExecutionP
68
68
  boundary: 'Diagnostics and posture commands are readable from Agent-owned settings, provider, model, and connected-host capability surfaces without taking connected-host lifecycle ownership.',
69
69
  };
70
70
  }
71
- if (root === 'profiles' || root === 'personas' || root === 'skills' || root === 'memory' || root === 'routines' || root === 'sessions' || root === 'bundle') {
71
+ if (root === 'profiles' || root === 'personas' || root === 'skills' || root === 'memory' || root === 'routines' || root === 'sessions' || root === 'bundle' || root === 'import' || root === 'workspaces' || root === 'fleet') {
72
72
  return {
73
73
  effect: 'local-state',
74
74
  confirmation,
75
- preferredModelTool: root === 'profiles' ? agentHarnessModes('workspace_actions', 'workspace_action', 'run_workspace_action') : 'agent_local_registry',
76
- boundary: 'Local library/profile/session/bundle CLI commands operate on Agent-local data. Mutations require explicit user intent and should use first-class Agent-local tools where available.',
75
+ preferredModelTool: root === 'profiles' || root === 'workspaces' || root === 'fleet' ? agentHarnessModes('workspace_actions', 'workspace_action', 'run_workspace_action') : 'agent_local_registry',
76
+ boundary: root === 'workspaces'
77
+ ? 'The registered-workspace list that gates automatic checkpoints (owner ruling, 2026-07-10) is Agent-local, user-scoped state. There is no dedicated model tool yet; registration/unregistration is a mutating action and requires explicit user intent (the CLI itself requires --yes).'
78
+ : root === 'fleet'
79
+ ? 'Best-of-N held-merge attempt groups (SDK 1.6.1) are this Agent\'s own orchestration engine state, not a connected-host call — fleet.attempts.* is ws-only with no HTTP binding, so agent_operator_method cannot reach it either. There is no dedicated model tool yet; picking a winner is a mutating, worktree-cleaning action and requires explicit user intent (the CLI itself requires --yes).'
80
+ : 'Local library/profile/session/bundle/import CLI commands operate on Agent-local data. Mutations require explicit user intent and should use first-class Agent-local tools where available.',
81
+ };
82
+ }
83
+ if (root === 'ci' || root === 'principals' || root === 'channel-profiles') {
84
+ return {
85
+ effect: 'connected-host-state',
86
+ confirmation,
87
+ // agent_operator_method, NOT agent_operator_action: the action tool's
88
+ // allowlist covers only approvals.* and automation.* and cannot invoke
89
+ // ci.*/principals.*/channels.profiles.* — pointing the model there would
90
+ // dead-end in "unknown action". The generic operator-method tool routes
91
+ // these directly (read-only methods run without confirmation; writes
92
+ // require confirm + explicitUserRequest).
93
+ preferredModelTool: 'agent_operator_method (methodId "ci.status", "ci.watches.*", "principals.*", "channels.profiles.*")',
94
+ boundary: 'CI status/watches, principal identity mappings, and per-channel profile defaults live on the connected host. Reads run through agent_operator_method without confirmation; writes require confirm + explicitUserRequest there, and mutating CLI subcommands require --yes.',
77
95
  };
78
96
  }
79
97
  if (root === 'knowledge' || root === 'ask' || root === 'search') {
@@ -92,6 +110,14 @@ export function describeCliCommandPolicy(commandName: string): CommandExecutionP
92
110
  boundary: 'Delegation is explicit user-directed work only; no hidden background review or separate Agent job should be created implicitly.',
93
111
  };
94
112
  }
113
+ if (root === 'relay') {
114
+ return {
115
+ effect: 'read-only',
116
+ confirmation,
117
+ preferredModelTool: `${settingsActions('list', 'get')} (category "relay") or ${agentHarnessModes('tools')}`,
118
+ boundary: 'relay status reports the connected host\'s imported relay.* configuration and the relay-connect feature flag only — it is not a live check (Agent hosts no daemon and the SDK has no remote relay-status route). relay pair always honestly refuses: minting a pairing payload needs the relay identity private key, which only the daemon actually holding the relay identity has.',
119
+ };
120
+ }
95
121
  if (root === 'subscription' || root === 'secrets' || root === 'pair') {
96
122
  return {
97
123
  effect: root === 'pair' ? 'external-network' : 'mixed',