@pellux/goodvibes-agent 1.1.7 → 1.2.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 (255) hide show
  1. package/CHANGELOG.md +170 -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 +132577 -91483
  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 +56 -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 +114 -45
  14. package/docs/voice-and-live-tts.md +3 -1
  15. package/package.json +3 -2
  16. package/release/release-notes.md +130 -5
  17. package/release/release-readiness.json +120 -90
  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 +296 -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-checkpoint.ts +140 -0
  42. package/src/agent/setup-wizard.ts +447 -0
  43. package/src/agent/skill-registry-types.ts +102 -0
  44. package/src/agent/skill-registry.ts +81 -107
  45. package/src/agent/vibe-confirmation-routes.ts +62 -0
  46. package/src/agent/vibe-file.ts +285 -0
  47. package/src/cli/agent-knowledge-command.ts +45 -2
  48. package/src/cli/help.ts +3 -0
  49. package/src/cli/profiles-command.ts +25 -11
  50. package/src/config/agent-settings-policy.ts +1 -18
  51. package/src/input/agent-workspace-activation.ts +53 -0
  52. package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
  53. package/src/input/agent-workspace-artifact-metadata.ts +17 -0
  54. package/src/input/agent-workspace-basic-command-editors.ts +5 -1
  55. package/src/input/agent-workspace-categories.ts +153 -155
  56. package/src/input/agent-workspace-channel-triage.ts +481 -0
  57. package/src/input/agent-workspace-channels.ts +263 -0
  58. package/src/input/agent-workspace-command-editor.ts +152 -0
  59. package/src/input/agent-workspace-context-snapshot.ts +349 -0
  60. package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
  61. package/src/input/agent-workspace-document-editor.ts +502 -0
  62. package/src/input/agent-workspace-document-ops-editor.ts +523 -0
  63. package/src/input/agent-workspace-host-category.ts +1 -1
  64. package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
  65. package/src/input/agent-workspace-model-compare-editor.ts +376 -0
  66. package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
  67. package/src/input/agent-workspace-model-compare-types.ts +82 -0
  68. package/src/input/agent-workspace-model-compare-utils.ts +35 -0
  69. package/src/input/agent-workspace-onboarding-categories.ts +141 -0
  70. package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
  71. package/src/input/agent-workspace-operations-command-editors.ts +22 -0
  72. package/src/input/agent-workspace-research-report-editor.ts +212 -0
  73. package/src/input/agent-workspace-research-run-editor.ts +144 -0
  74. package/src/input/agent-workspace-research-source-editor.ts +167 -0
  75. package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
  76. package/src/input/agent-workspace-review-packet-utils.ts +265 -0
  77. package/src/input/agent-workspace-settings.ts +262 -30
  78. package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
  79. package/src/input/agent-workspace-setup-snapshot.ts +198 -0
  80. package/src/input/agent-workspace-setup.ts +34 -1
  81. package/src/input/agent-workspace-snapshot.ts +162 -147
  82. package/src/input/agent-workspace-types.ts +287 -2
  83. package/src/input/agent-workspace.ts +18 -0
  84. package/src/input/command-registry.ts +9 -2
  85. package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
  86. package/src/input/commands/channels-runtime.ts +102 -114
  87. package/src/input/commands/delegation-runtime.ts +87 -24
  88. package/src/input/commands/knowledge-browser-flags.ts +12 -0
  89. package/src/input/commands/knowledge.ts +28 -14
  90. package/src/input/commands/operator-actions-runtime.ts +6 -3
  91. package/src/input/commands/schedule-runtime.ts +71 -14
  92. package/src/input/commands/session-content.ts +2 -2
  93. package/src/input/commands/shell-core.ts +2 -0
  94. package/src/input/commands/vibe-runtime.ts +140 -0
  95. package/src/input/commands.ts +2 -0
  96. package/src/input/connected-host-routes.ts +123 -0
  97. package/src/input/settings-modal-types.ts +14 -0
  98. package/src/main.ts +15 -15
  99. package/src/panels/qr-panel.ts +2 -2
  100. package/src/renderer/agent-workspace-context-lines.ts +686 -0
  101. package/src/renderer/agent-workspace.ts +30 -419
  102. package/src/renderer/settings-modal-helpers.ts +12 -0
  103. package/src/renderer/settings-modal.ts +12 -0
  104. package/src/runtime/bootstrap-command-context.ts +11 -1
  105. package/src/runtime/bootstrap-command-parts.ts +14 -3
  106. package/src/runtime/bootstrap-core.ts +39 -1
  107. package/src/runtime/bootstrap-shell.ts +6 -0
  108. package/src/runtime/bootstrap.ts +113 -30
  109. package/src/runtime/connected-host-auth.ts +3 -2
  110. package/src/runtime/execution-ledger.ts +231 -0
  111. package/src/runtime/services.ts +88 -10
  112. package/src/runtime/tool-permission-safety.ts +199 -5
  113. package/src/shell/session-continuity-hints.ts +39 -0
  114. package/src/tools/agent-artifacts-tool.ts +606 -0
  115. package/src/tools/agent-audit-tool.ts +155 -0
  116. package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
  117. package/src/tools/agent-autonomy-tool.ts +140 -0
  118. package/src/tools/agent-channel-send-tool.ts +24 -2
  119. package/src/tools/agent-channels-tool.ts +140 -0
  120. package/src/tools/agent-computer-tool.ts +168 -0
  121. package/src/tools/agent-context-policy.ts +137 -7
  122. package/src/tools/agent-context-tool.ts +143 -0
  123. package/src/tools/agent-delegation-tool.ts +128 -0
  124. package/src/tools/agent-device-tool.ts +240 -0
  125. package/src/tools/agent-documents-tool.ts +684 -0
  126. package/src/tools/agent-execution-tool.ts +230 -0
  127. package/src/tools/agent-harness-agent-orchestration.ts +703 -0
  128. package/src/tools/agent-harness-autonomy-intake.ts +785 -0
  129. package/src/tools/agent-harness-autonomy-live-records.ts +588 -0
  130. package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
  131. package/src/tools/agent-harness-autonomy-queue.ts +560 -0
  132. package/src/tools/agent-harness-background-processes-types.ts +35 -0
  133. package/src/tools/agent-harness-background-processes.ts +794 -0
  134. package/src/tools/agent-harness-browser-cockpit-route.ts +219 -0
  135. package/src/tools/agent-harness-browser-control.ts +338 -0
  136. package/src/tools/agent-harness-channel-metadata.ts +168 -10
  137. package/src/tools/agent-harness-cli-command-policy.ts +110 -0
  138. package/src/tools/agent-harness-command-catalog.ts +4 -2
  139. package/src/tools/agent-harness-command-runner.ts +42 -0
  140. package/src/tools/agent-harness-connected-host-status.ts +7 -4
  141. package/src/tools/agent-harness-delegation-posture.ts +234 -19
  142. package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
  143. package/src/tools/agent-harness-document-ops-types.ts +72 -0
  144. package/src/tools/agent-harness-document-ops.ts +671 -0
  145. package/src/tools/agent-harness-execution-history.ts +489 -0
  146. package/src/tools/agent-harness-execution-posture.ts +382 -0
  147. package/src/tools/agent-harness-file-recovery.ts +135 -0
  148. package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
  149. package/src/tools/agent-harness-learning-curator-common.ts +102 -0
  150. package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
  151. package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
  152. package/src/tools/agent-harness-learning-curator-types.ts +151 -0
  153. package/src/tools/agent-harness-learning-curator.ts +417 -0
  154. package/src/tools/agent-harness-local-model-benchmarks.ts +199 -0
  155. package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
  156. package/src/tools/agent-harness-local-model-endpoints.ts +673 -0
  157. package/src/tools/agent-harness-media-posture.ts +208 -2
  158. package/src/tools/agent-harness-memory-posture.ts +556 -0
  159. package/src/tools/agent-harness-metadata.ts +100 -168
  160. package/src/tools/agent-harness-mode-catalog.ts +84 -33
  161. package/src/tools/agent-harness-model-catalog.ts +162 -0
  162. package/src/tools/agent-harness-model-provider-health.ts +186 -0
  163. package/src/tools/agent-harness-model-readiness.ts +406 -0
  164. package/src/tools/agent-harness-model-routing-types.ts +266 -0
  165. package/src/tools/agent-harness-model-routing-utils.ts +30 -0
  166. package/src/tools/agent-harness-model-routing.ts +137 -190
  167. package/src/tools/agent-harness-operator-methods.ts +115 -133
  168. package/src/tools/agent-harness-pairing-posture.ts +431 -18
  169. package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
  170. package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
  171. package/src/tools/agent-harness-personal-ops-lanes.ts +204 -0
  172. package/src/tools/agent-harness-personal-ops-records.ts +732 -0
  173. package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
  174. package/src/tools/agent-harness-personal-ops-types.ts +222 -0
  175. package/src/tools/agent-harness-personal-ops.ts +606 -0
  176. package/src/tools/agent-harness-project-context.ts +144 -0
  177. package/src/tools/agent-harness-prompt-context.ts +589 -0
  178. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  179. package/src/tools/agent-harness-release-evidence.ts +9 -7
  180. package/src/tools/agent-harness-release-readiness.ts +9 -7
  181. package/src/tools/agent-harness-research-briefing.ts +427 -0
  182. package/src/tools/agent-harness-research-queue.ts +250 -0
  183. package/src/tools/agent-harness-research-runs.ts +295 -0
  184. package/src/tools/agent-harness-research-workflow.ts +322 -0
  185. package/src/tools/agent-harness-security-posture.ts +2 -2
  186. package/src/tools/agent-harness-service-posture.ts +9 -9
  187. package/src/tools/agent-harness-session-metadata.ts +3 -3
  188. package/src/tools/agent-harness-setup-connected-host.ts +501 -0
  189. package/src/tools/agent-harness-setup-describe.ts +557 -0
  190. package/src/tools/agent-harness-setup-handoffs.ts +502 -0
  191. package/src/tools/agent-harness-setup-model-helpers.ts +101 -0
  192. package/src/tools/agent-harness-setup-plan.ts +253 -0
  193. package/src/tools/agent-harness-setup-posture-types.ts +218 -0
  194. package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
  195. package/src/tools/agent-harness-setup-posture.ts +449 -221
  196. package/src/tools/agent-harness-setup-smoke.ts +508 -0
  197. package/src/tools/agent-harness-sudo-posture.ts +114 -0
  198. package/src/tools/agent-harness-tool-schema.ts +120 -7
  199. package/src/tools/agent-harness-tool-types.ts +78 -0
  200. package/src/tools/agent-harness-tool-utils.ts +43 -0
  201. package/src/tools/agent-harness-tool.ts +311 -370
  202. package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
  203. package/src/tools/agent-harness-ui-surface-types.ts +46 -0
  204. package/src/tools/agent-harness-vibe-health.ts +105 -0
  205. package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
  206. package/src/tools/agent-harness-workspace-actions.ts +114 -8
  207. package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
  208. package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
  209. package/src/tools/agent-host-tool.ts +159 -0
  210. package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
  211. package/src/tools/agent-learning-consolidation-core.ts +327 -0
  212. package/src/tools/agent-learning-consolidation-tool.ts +758 -0
  213. package/src/tools/agent-memory-tool.ts +214 -0
  214. package/src/tools/agent-model-compare-export.ts +315 -0
  215. package/src/tools/agent-model-compare-handoff.ts +592 -0
  216. package/src/tools/agent-model-compare-judgment.ts +633 -0
  217. package/src/tools/agent-model-compare-run.ts +722 -0
  218. package/src/tools/agent-model-compare-tool.ts +698 -0
  219. package/src/tools/agent-model-compare-types.ts +191 -0
  220. package/src/tools/agent-model-compare-utils.ts +93 -0
  221. package/src/tools/agent-models-tool.ts +208 -0
  222. package/src/tools/agent-operator-action-tool.ts +1 -1
  223. package/src/tools/agent-operator-method-tool.ts +643 -0
  224. package/src/tools/agent-personal-ops-tool.ts +197 -0
  225. package/src/tools/agent-policy-explanation.ts +415 -0
  226. package/src/tools/agent-research-report-tool.ts +608 -0
  227. package/src/tools/agent-research-runs-tool.ts +434 -0
  228. package/src/tools/agent-research-sources-tool.ts +443 -0
  229. package/src/tools/agent-research-tool.ts +687 -0
  230. package/src/tools/agent-review-packet-presets-core.ts +757 -0
  231. package/src/tools/agent-review-packet-presets-tool.ts +466 -0
  232. package/src/tools/agent-review-packet-share-tool.ts +305 -0
  233. package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
  234. package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
  235. package/src/tools/agent-route-planner-candidates-work.ts +251 -0
  236. package/src/tools/agent-route-planner-helpers.ts +667 -0
  237. package/src/tools/agent-route-planner.ts +185 -0
  238. package/src/tools/agent-route-tool.ts +105 -0
  239. package/src/tools/agent-schedule-edit-tool.ts +210 -0
  240. package/src/tools/agent-schedule-tool.ts +282 -0
  241. package/src/tools/agent-security-tool.ts +145 -0
  242. package/src/tools/agent-sessions-tool.ts +122 -0
  243. package/src/tools/agent-settings-import-tool.ts +104 -0
  244. package/src/tools/agent-settings-tool.ts +176 -0
  245. package/src/tools/agent-setup-tool.ts +226 -0
  246. package/src/tools/agent-support-tool.ts +122 -0
  247. package/src/tools/agent-terminal-process-tools.ts +167 -0
  248. package/src/tools/agent-tool-policy-guard-types.ts +77 -0
  249. package/src/tools/agent-tool-policy-guard.ts +106 -85
  250. package/src/tools/agent-vibe-tool.ts +297 -0
  251. package/src/tools/agent-work-plan-tool.ts +265 -6
  252. package/src/tools/agent-workspace-tool.ts +305 -0
  253. package/src/tools/artifact-archive.ts +169 -0
  254. package/src/tools/tool-definition-compaction.ts +36 -0
  255. package/src/version.ts +1 -1
@@ -0,0 +1,794 @@
1
+ import { getOperatorContract } from '@pellux/goodvibes-sdk/contracts';
2
+ import type { BackgroundProcess, ProcessManager } from '@pellux/goodvibes-sdk/platform/tools';
3
+ import type { CommandContext } from '../input/command-registry.ts';
4
+ import { sudoExecutionPosture } from './agent-harness-sudo-posture.ts';
5
+ import { previewHarnessText } from './agent-harness-text.ts';
6
+ import type {
7
+ AgentHarnessBackgroundProcessArgs,
8
+ BackgroundProcessLookupSource,
9
+ BackgroundProcessResolution,
10
+ ProcessCapabilityStatus,
11
+ } from './agent-harness-background-processes-types.ts';
12
+ export type {
13
+ AgentHarnessBackgroundProcessArgs,
14
+ BackgroundProcessResolution,
15
+ } from './agent-harness-background-processes-types.ts';
16
+
17
+ const MAX_LOG_PREVIEW_CHARS = 4_000;
18
+ const MAX_COMPACT_LOG_PREVIEW_CHARS = 600;
19
+ const DEFAULT_BACKGROUND_TIMEOUT_MS = 30 * 60 * 1000;
20
+ const MAX_BACKGROUND_TIMEOUT_MS = 8 * 60 * 60 * 1000;
21
+ const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
22
+ const PROCESS_PARITY_METHODS = ['terminal(background=true)', 'process(list)', 'process(poll)', 'process(wait)', 'process(log)', 'process(kill)', 'process(write)', 'pty', 'sudo'] as const;
23
+ const STDIN_WRITE_METHOD_NAMES = ['write', 'writeInput', 'sendInput', 'writeStdin', 'sendStdin', 'stdinWrite'] as const;
24
+ const PTY_METHOD_NAMES = ['spawnPty', 'openPty', 'createPty', 'pty'] as const;
25
+ const SENSITIVE_TEXT_PATTERNS: readonly [RegExp, string][] = [
26
+ [/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTHORIZATION|BEARER)[A-Z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/gi, '$1=<redacted>'],
27
+ [/(\b(?:token|secret|password|passwd|api[-_]?key|authorization|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s]+)/gi, '$1<redacted>'],
28
+ [/(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>'],
29
+ [/(\s--(?:token|password|secret|api-key|api_key)\s+)("[^"]*"|'[^']*'|[^\s]+)/gi, '$1<redacted>'],
30
+ ];
31
+
32
+ function readString(value: unknown): string {
33
+ return typeof value === 'string' ? value.trim() : '';
34
+ }
35
+
36
+ function readNumber(value: unknown, fallback: number): number {
37
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
38
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
39
+ return Math.trunc(parsed);
40
+ }
41
+
42
+ function readLimit(value: unknown, fallback: number): number {
43
+ return Math.max(1, Math.min(500, readNumber(value, fallback)));
44
+ }
45
+
46
+ function clampTimeout(value: unknown, fallback: number): number {
47
+ return Math.max(1_000, Math.min(MAX_BACKGROUND_TIMEOUT_MS, readNumber(value, fallback)));
48
+ }
49
+
50
+ function fieldMap(value: unknown): Readonly<Record<string, string>> {
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
52
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, typeof entry === 'string' ? entry : String(entry)]));
53
+ }
54
+
55
+ function readField(args: AgentHarnessBackgroundProcessArgs, id: string): string {
56
+ return fieldMap(args.fields)[id] ?? '';
57
+ }
58
+
59
+ function readData(args: AgentHarnessBackgroundProcessArgs): string {
60
+ if (typeof args.data === 'string') return args.data;
61
+ const fromFields = readField(args, 'data');
62
+ return fromFields;
63
+ }
64
+
65
+ function managerFrom(context: CommandContext): ProcessManager | undefined {
66
+ return context.workspace.processManager;
67
+ }
68
+
69
+ interface OperatorContractMethod {
70
+ readonly id: string;
71
+ readonly title?: string;
72
+ readonly description?: string;
73
+ readonly category?: string;
74
+ readonly access?: string;
75
+ readonly scopes?: readonly string[];
76
+ readonly http?: {
77
+ readonly method?: string;
78
+ readonly path?: string;
79
+ };
80
+ }
81
+
82
+ function operatorContractMethods(): readonly OperatorContractMethod[] {
83
+ const methods = getOperatorContract().operator?.methods;
84
+ return Array.isArray(methods)
85
+ ? methods.filter((method): method is OperatorContractMethod => Boolean(method?.id))
86
+ : [];
87
+ }
88
+
89
+ function operatorMethodSearchText(method: OperatorContractMethod): string {
90
+ return [
91
+ method.id,
92
+ method.title,
93
+ method.description,
94
+ method.category,
95
+ method.http?.method,
96
+ method.http?.path,
97
+ method.scopes?.join(' '),
98
+ ].filter(Boolean).join('\n').toLowerCase();
99
+ }
100
+
101
+ function matchingOperatorMethodRoutes(tokens: readonly string[]): readonly Record<string, unknown>[] {
102
+ if (tokens.length === 0) return [];
103
+ return operatorContractMethods()
104
+ .filter((method) => {
105
+ const text = operatorMethodSearchText(method);
106
+ return tokens.some((token) => text.includes(token));
107
+ })
108
+ .map((method) => ({
109
+ methodId: method.id,
110
+ label: method.title ?? method.description ?? method.id,
111
+ category: method.category ?? 'uncategorized',
112
+ effect: method.scopes?.every((scope) => scope.startsWith('read:')) ? 'read-only-network' : method.access === 'admin' ? 'confirmed-admin-connected-host-state' : 'confirmed-connected-host-state',
113
+ route: `${method.http?.method?.toUpperCase() ?? 'GET'} ${method.http?.path ?? '/'}`,
114
+ modelRoute: `agent_operator_method methodId:"${method.id}"`,
115
+ }))
116
+ .sort((left, right) => String(left.methodId).localeCompare(String(right.methodId)))
117
+ .slice(0, 12);
118
+ }
119
+
120
+ function managerMethodNames(manager: ProcessManager | undefined): readonly string[] {
121
+ if (!manager) return [];
122
+ const own = Object.keys(manager);
123
+ const prototype = Object.getPrototypeOf(manager) as Record<string, unknown> | null;
124
+ const protoNames = prototype ? Object.getOwnPropertyNames(prototype) : [];
125
+ return [...new Set([...own, ...protoNames])]
126
+ .filter((name) => name !== 'constructor' && typeof (manager as unknown as Record<string, unknown>)[name] === 'function')
127
+ .sort((left, right) => left.localeCompare(right));
128
+ }
129
+
130
+ function firstManagerFunction(manager: ProcessManager | undefined, names: readonly string[]): ((processId: string, data: string) => unknown) | null {
131
+ if (!manager) return null;
132
+ const record = manager as unknown as Record<string, unknown>;
133
+ for (const name of names) {
134
+ const fn = record[name];
135
+ if (typeof fn === 'function') return fn.bind(manager) as (processId: string, data: string) => unknown;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function processSubstrateReport(context?: CommandContext): Record<string, unknown> {
141
+ const manager = context ? managerFrom(context) : undefined;
142
+ const methodNames = managerMethodNames(manager);
143
+ const localStdinMethod = STDIN_WRITE_METHOD_NAMES.find((name) => methodNames.includes(name)) ?? null;
144
+ const localPtyMethod = PTY_METHOD_NAMES.find((name) => methodNames.includes(name)) ?? null;
145
+ const terminalRoutes = matchingOperatorMethodRoutes(['terminal', 'pty', 'process.write', 'stdin']);
146
+ const sessionInputRoutes = matchingOperatorMethodRoutes(['sessions.inputs']);
147
+ const credentialRoutes = matchingOperatorMethodRoutes(['credential', 'sudo', 'privilege']);
148
+ return {
149
+ localProcessManager: {
150
+ status: manager ? 'available' : 'unavailable',
151
+ methodNames,
152
+ supports: methodNames.filter((name) => ['spawn', 'list', 'getStatus', 'getOutput', 'stop', 'handleCommand', localStdinMethod, localPtyMethod].filter(Boolean).includes(name)),
153
+ stdinWrite: localStdinMethod
154
+ ? { status: 'contract-discovered', method: localStdinMethod, executableByHarness: true }
155
+ : { status: 'blocked-contract-gap', missingAnyOf: STDIN_WRITE_METHOD_NAMES },
156
+ pty: localPtyMethod
157
+ ? { status: 'contract-discovered', method: localPtyMethod, executableByHarness: false }
158
+ : { status: 'blocked-contract-gap', missingAnyOf: PTY_METHOD_NAMES },
159
+ },
160
+ daemonOperatorContract: {
161
+ status: terminalRoutes.length > 0 ? 'terminal-or-pty-methods-discovered' : 'no-published-terminal-or-pty-method',
162
+ terminalOrPtyRoutes: terminalRoutes,
163
+ sessionInputRoutes,
164
+ credentialRoutes,
165
+ policy: 'Session input routes steer GoodVibes sessions; they are not equivalent to writing stdin into a tracked local process.',
166
+ },
167
+ };
168
+ }
169
+
170
+ function redactText(value: string): string {
171
+ return SENSITIVE_TEXT_PATTERNS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), value);
172
+ }
173
+
174
+ function compactText(value: string, max: number): string {
175
+ return tailText(redactText(value), max);
176
+ }
177
+
178
+ function tailText(value: string, max: number): string {
179
+ if (value.length <= max) return value;
180
+ return value.slice(Math.max(0, value.length - max)).trimStart();
181
+ }
182
+
183
+ function outputTail(value: string, max: number): {
184
+ readonly text: string;
185
+ readonly chars: number;
186
+ readonly bytes: number;
187
+ readonly truncated: boolean;
188
+ readonly omittedChars: number;
189
+ } {
190
+ const redacted = redactText(value);
191
+ const text = tailText(redacted, max);
192
+ const omittedChars = Math.max(0, redacted.length - text.length);
193
+ return {
194
+ text,
195
+ chars: redacted.length,
196
+ bytes: Buffer.byteLength(redacted),
197
+ truncated: omittedChars > 0,
198
+ omittedChars,
199
+ };
200
+ }
201
+
202
+ function safeJsonPreview(value: unknown): string {
203
+ try {
204
+ return JSON.stringify(value);
205
+ } catch {
206
+ return String(value);
207
+ }
208
+ }
209
+
210
+ function redactWrittenInputPreview(value: string, data: string): { readonly text: string; readonly redactedInputEcho: boolean } {
211
+ if (!data) return { text: value, redactedInputEcho: false };
212
+ const escaped = JSON.stringify(data).slice(1, -1);
213
+ let text = value;
214
+ let redactedInputEcho = false;
215
+ for (const candidate of [data, escaped]) {
216
+ if (!candidate || !text.includes(candidate)) continue;
217
+ text = text.split(candidate).join('<redacted-input>');
218
+ redactedInputEcho = true;
219
+ }
220
+ return { text, redactedInputEcho };
221
+ }
222
+
223
+ function summarizeWriteResult(value: unknown, data: string): Record<string, unknown> {
224
+ if (typeof value === 'undefined') return { returned: false };
225
+ const redacted = redactText(safeJsonPreview(value));
226
+ const inputRedacted = redactWrittenInputPreview(redacted, data);
227
+ return {
228
+ returned: true,
229
+ type: value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value,
230
+ preview: previewHarnessText(inputRedacted.text, 240),
231
+ inputEchoRedacted: inputRedacted.redactedInputEcho,
232
+ policy: 'Result preview is bounded, secret-redacted, and strips exact input echoes.',
233
+ };
234
+ }
235
+
236
+ function processStatus(entry: BackgroundProcess): 'running' | 'succeeded' | 'failed' | 'cancelled' {
237
+ if (!entry.done) return 'running';
238
+ if (entry.exitCode === 0) return 'succeeded';
239
+ if (entry.exitCode === null) return 'cancelled';
240
+ return 'failed';
241
+ }
242
+
243
+ function processAgeMs(entry: BackgroundProcess, now = Date.now()): number {
244
+ return Math.max(0, (entry.completedAt ?? now) - entry.startTime);
245
+ }
246
+
247
+ function routeFor(processId: string, mode: 'background_process' | 'run_background_process', action?: string): string {
248
+ if (mode === 'background_process') return `execution action:"process" processId:"${processId}"`;
249
+ const processAction = action === 'stop' ? 'kill' : action || 'poll';
250
+ const confirmation = ['wait', 'kill', 'stop', 'write', 'start'].includes(processAction) ? ' confirm:true explicitUserRequest:"..."' : '';
251
+ return `process action:"${processAction}" processId:"${processId}"${confirmation}`;
252
+ }
253
+
254
+ function readProcessSessionId(args: AgentHarnessBackgroundProcessArgs): { readonly source: BackgroundProcessLookupSource; readonly input: string } | null {
255
+ const processId = readString(args.processId) || readField(args, 'processId');
256
+ if (processId) return { source: 'processId', input: processId };
257
+ const processSessionId = readString(args.processSessionId) || readField(args, 'processSessionId');
258
+ if (processSessionId) return { source: 'processSessionId', input: processSessionId };
259
+ const sessionId = readString(args.sessionId) || readField(args, 'sessionId');
260
+ if (sessionId) return { source: 'sessionId', input: sessionId };
261
+ const snakeSessionId = readString(args.session_id) || readField(args, 'session_id');
262
+ if (snakeSessionId) return { source: 'session_id', input: snakeSessionId };
263
+ return null;
264
+ }
265
+
266
+ function describeProcessEntry(
267
+ manager: ProcessManager,
268
+ entry: BackgroundProcess,
269
+ options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
270
+ ): Record<string, unknown> {
271
+ const output = manager.getOutput(entry.id) ?? { stdout: '', stderr: '' };
272
+ const maxOutputChars = options.includeParameters ? MAX_LOG_PREVIEW_CHARS : MAX_COMPACT_LOG_PREVIEW_CHARS;
273
+ const stdout = outputTail(output.stdout, maxOutputChars);
274
+ const stderr = outputTail(output.stderr, maxOutputChars);
275
+ const includeOutput = options.includeParameters === true || Boolean(stdout.text || stderr.text);
276
+ return {
277
+ processId: entry.id,
278
+ processSessionId: entry.id,
279
+ sessionId: entry.id,
280
+ session_id: entry.id,
281
+ pid: entry.pid,
282
+ status: processStatus(entry),
283
+ done: entry.done,
284
+ exitCode: entry.exitCode,
285
+ command: options.includeParameters ? redactText(entry.cmd) : previewHarnessText(redactText(entry.cmd), 120),
286
+ startedAt: new Date(entry.startTime).toISOString(),
287
+ ageMs: processAgeMs(entry),
288
+ routes: {
289
+ inspect: routeFor(entry.id, 'background_process'),
290
+ poll: routeFor(entry.id, 'run_background_process', 'poll'),
291
+ log: routeFor(entry.id, 'run_background_process', 'log'),
292
+ wait: routeFor(entry.id, 'run_background_process', 'wait'),
293
+ stop: routeFor(entry.id, 'run_background_process', 'stop'),
294
+ visibleMonitor: 'workspace action:"open" surfaceId:"process-monitor"',
295
+ liveTail: `workspace action:"open" surfaceId:"live-tail" target:"${entry.id}"`,
296
+ },
297
+ ...(includeOutput ? {
298
+ output: {
299
+ stdoutTail: stdout.text,
300
+ stderrTail: stderr.text,
301
+ stdoutChars: stdout.chars,
302
+ stderrChars: stderr.chars,
303
+ stdoutBytes: stdout.bytes,
304
+ stderrBytes: stderr.bytes,
305
+ stdoutTruncated: stdout.truncated,
306
+ stderrTruncated: stderr.truncated,
307
+ omittedStdoutChars: stdout.omittedChars,
308
+ omittedStderrChars: stderr.omittedChars,
309
+ maxOutputChars,
310
+ fullOutputIncluded: !stdout.truncated && !stderr.truncated,
311
+ policy: 'Output is bounded; counts are computed after secret-looking text is redacted before model display.',
312
+ },
313
+ } : {}),
314
+ ...(options.lookup ? { lookup: options.lookup } : {}),
315
+ };
316
+ }
317
+
318
+ function processSearchText(entry: BackgroundProcess): string {
319
+ return [
320
+ entry.id,
321
+ String(entry.pid),
322
+ entry.cmd,
323
+ processStatus(entry),
324
+ ].join('\n').toLowerCase();
325
+ }
326
+
327
+ function lookupFromArgs(args: AgentHarnessBackgroundProcessArgs): { readonly source: BackgroundProcessLookupSource; readonly input: string } | null {
328
+ const processSession = readProcessSessionId(args);
329
+ if (processSession) return processSession;
330
+ const target = readString(args.target);
331
+ if (target) return { source: 'target', input: target };
332
+ const query = readString(args.query);
333
+ return query ? { source: 'query', input: query } : null;
334
+ }
335
+
336
+ function matchingProcesses(manager: ProcessManager, input: string): readonly BackgroundProcess[] {
337
+ const entries = manager.list()
338
+ .map((entry) => manager.getStatus(entry.id))
339
+ .filter((entry): entry is BackgroundProcess => Boolean(entry));
340
+ const normalized = input.toLowerCase().trim();
341
+ if (!normalized) return entries;
342
+ return entries.filter((entry) => processSearchText(entry).includes(normalized));
343
+ }
344
+
345
+ function candidateProcess(entry: BackgroundProcess): Record<string, unknown> {
346
+ return {
347
+ processId: entry.id,
348
+ processSessionId: entry.id,
349
+ sessionId: entry.id,
350
+ session_id: entry.id,
351
+ pid: entry.pid,
352
+ status: processStatus(entry),
353
+ command: previewHarnessText(redactText(entry.cmd), 96),
354
+ };
355
+ }
356
+
357
+ function processToolParity(context?: CommandContext): readonly Record<string, unknown>[] {
358
+ const substrate = processSubstrateReport(context);
359
+ const localProcessManager = substrate.localProcessManager as Record<string, unknown>;
360
+ const daemonContract = substrate.daemonOperatorContract as Record<string, unknown>;
361
+ const stdinWrite = localProcessManager.stdinWrite as Record<string, unknown>;
362
+ const pty = localProcessManager.pty as Record<string, unknown>;
363
+ const terminalRoutes = Array.isArray(daemonContract.terminalOrPtyRoutes) ? daemonContract.terminalOrPtyRoutes : [];
364
+ const writeStatus: ProcessCapabilityStatus = stdinWrite.status === 'contract-discovered'
365
+ ? 'contract-discovered'
366
+ : terminalRoutes.some((route) => String((route as Record<string, unknown>).methodId).toLowerCase().includes('write'))
367
+ ? 'contract-discovered'
368
+ : 'blocked-contract-gap';
369
+ const ptyStatus: ProcessCapabilityStatus = pty.status === 'contract-discovered' || terminalRoutes.length > 0 ? 'contract-discovered' : 'blocked-contract-gap';
370
+ return [
371
+ {
372
+ capability: 'terminal(background=true)',
373
+ status: 'supported',
374
+ userOutcome: 'Start one visible tracked local command without blocking the conversation.',
375
+ modelRoute: 'terminal command:"..." background:true confirm:true explicitUserRequest:"..."',
376
+ },
377
+ {
378
+ capability: 'process(list)',
379
+ status: 'supported',
380
+ userOutcome: 'See every tracked local background process from the shared ProcessManager.',
381
+ modelRoute: 'execution action:"processes"',
382
+ },
383
+ {
384
+ capability: 'process(poll)',
385
+ status: 'supported',
386
+ userOutcome: 'Poll one tracked process status without waiting.',
387
+ modelRoute: 'process action:"poll" sessionId:"..."',
388
+ },
389
+ {
390
+ capability: 'process(wait)',
391
+ status: 'supported',
392
+ userOutcome: 'Wait on one tracked process with a bounded timeout.',
393
+ modelRoute: 'process action:"wait" processId:"..." confirm:true explicitUserRequest:"..."',
394
+ },
395
+ {
396
+ capability: 'process(log)',
397
+ status: 'supported',
398
+ userOutcome: 'Read redacted stdout/stderr tails with explicit truncation metadata.',
399
+ modelRoute: 'process action:"log" sessionId:"..."',
400
+ },
401
+ {
402
+ capability: 'process(kill)',
403
+ status: 'supported',
404
+ userOutcome: 'Stop and remove one tracked process from the shared ProcessManager.',
405
+ modelRoute: 'process action:"kill" sessionId:"..." confirm:true explicitUserRequest:"..."',
406
+ },
407
+ {
408
+ capability: 'process(write)',
409
+ status: writeStatus,
410
+ userOutcome: writeStatus === 'contract-discovered'
411
+ ? 'Interactive input has a published contract; Agent requires confirmation and a process id before writing.'
412
+ : 'Interactive input is not exposed because the SDK ProcessManager has no safe stdin handle.',
413
+ modelRoute: 'process action:"write" processId:"..." data:"..." confirm:true explicitUserRequest:"..."',
414
+ },
415
+ {
416
+ capability: 'pty',
417
+ status: ptyStatus,
418
+ userOutcome: ptyStatus === 'contract-discovered'
419
+ ? 'A PTY-like contract is discoverable; Agent still requires an explicit session API before generic PTY spawn is enabled.'
420
+ : 'Interactive CLIs need a published PTY/session API before Agent can make them safe and visible.',
421
+ modelRoute: 'terminal command:"..." background:true pty:true confirm:true explicitUserRequest:"..."',
422
+ },
423
+ {
424
+ capability: 'sudo',
425
+ status: 'visible-only',
426
+ userOutcome: 'Privilege prompts must stay foreground or use a future safe credential-prompt contract.',
427
+ modelRoute: 'execution action:"route" id:"local-shell-command"',
428
+ },
429
+ ];
430
+ }
431
+
432
+ function capabilities(context?: CommandContext): Record<string, unknown> {
433
+ const sudoPosture = sudoExecutionPosture(context);
434
+ const substrate = processSubstrateReport(context);
435
+ const localProcessManager = substrate.localProcessManager as Record<string, unknown>;
436
+ const stdinWrite = localProcessManager.stdinWrite as Record<string, unknown>;
437
+ const pty = localProcessManager.pty as Record<string, unknown>;
438
+ return {
439
+ start: 'terminal command:"..." background:true confirm:true explicitUserRequest:"..."',
440
+ inspect: 'execution action:"processes" or action:"process"',
441
+ wait: 'process action:"wait" processId|sessionId:"..." confirm:true explicitUserRequest:"..."',
442
+ stop: 'process action:"kill" processId|sessionId:"..." confirm:true explicitUserRequest:"..."',
443
+ aliases: {
444
+ actions: {
445
+ poll: 'status',
446
+ kill: 'stop',
447
+ log: 'output',
448
+ write: stdinWrite.status === 'contract-discovered' ? 'confirmed stdin write through discovered ProcessManager method' : 'unsupported until ProcessManager exposes stdin',
449
+ },
450
+ ids: ['processId', 'processSessionId', 'sessionId', 'session_id'],
451
+ userOutcome: 'The harness accepts the process-tool words users expect while returning stable processId/sessionId aliases.',
452
+ },
453
+ parity: processToolParity(context),
454
+ substrate: {
455
+ ...substrate,
456
+ auditedTerms: ['terminal', 'process.write', 'stdin', 'pty', 'sudo', 'sessions.inputs'],
457
+ },
458
+ pty: {
459
+ status: pty.status === 'contract-discovered' ? 'contract-discovered-but-not-generic-executable' : 'not-yet-supported-in-agent-harness',
460
+ guidance: pty.status === 'contract-discovered'
461
+ ? 'A PTY-like method is discoverable, but Agent needs a typed session contract before generic PTY spawn can be safe.'
462
+ : 'Use foreground exec for noninteractive commands. Interactive PTY needs a published SDK/daemon session API before Agent can expose it safely.',
463
+ },
464
+ stdinWrite: {
465
+ status: stdinWrite.status === 'contract-discovered' ? 'supported-with-confirmation' : 'not-yet-supported-in-agent-harness',
466
+ modelRoute: 'process action:"write" processId:"..." data:"..." confirm:true explicitUserRequest:"..."',
467
+ guidance: stdinWrite.status === 'contract-discovered'
468
+ ? 'The shared ProcessManager exposes a stdin write method. Agent requires confirm:true, explicitUserRequest, one exact process id, and non-empty data.'
469
+ : 'ProcessManager currently tracks output and stop lifecycle; it does not expose a safe stdin write API.',
470
+ },
471
+ sudo: {
472
+ ...sudoPosture,
473
+ guidance: sudoPosture.credentialSignal.guidance,
474
+ },
475
+ };
476
+ }
477
+
478
+ export function backgroundProcessCatalogStatus(context: CommandContext): Record<string, unknown> {
479
+ const manager = managerFrom(context);
480
+ if (!manager) {
481
+ return {
482
+ modes: ['background_processes', 'background_process', 'run_background_process'],
483
+ status: 'unavailable',
484
+ tracked: 0,
485
+ running: 0,
486
+ readOnly: false,
487
+ reason: 'ProcessManager is not wired into this runtime.',
488
+ };
489
+ }
490
+ const entries = matchingProcesses(manager, '');
491
+ return {
492
+ modes: ['background_processes', 'background_process', 'run_background_process'],
493
+ status: 'available',
494
+ tracked: entries.length,
495
+ running: entries.filter((entry) => !entry.done).length,
496
+ completed: entries.filter((entry) => entry.done).length,
497
+ readOnly: false,
498
+ };
499
+ }
500
+
501
+ export function backgroundProcessSummary(context: CommandContext, args: AgentHarnessBackgroundProcessArgs): Record<string, unknown> {
502
+ const manager = managerFrom(context);
503
+ if (!manager) {
504
+ return {
505
+ status: 'unavailable',
506
+ processes: [],
507
+ returned: 0,
508
+ total: 0,
509
+ capabilities: capabilities(context),
510
+ policy: 'Background process UX uses the shared GoodVibes ProcessManager when the current runtime wires it in.',
511
+ };
512
+ }
513
+ const query = readString(args.query || args.target);
514
+ const entries = matchingProcesses(manager, query);
515
+ const limit = readLimit(args.limit, 100);
516
+ const processes = entries.slice(0, limit).map((entry) => describeProcessEntry(manager, entry, {
517
+ includeParameters: args.includeParameters === true,
518
+ }));
519
+ return {
520
+ status: 'available',
521
+ summary: {
522
+ tracked: entries.length,
523
+ running: entries.filter((entry) => !entry.done).length,
524
+ completed: entries.filter((entry) => entry.done).length,
525
+ visibleMonitor: 'workspace action:"open" surfaceId:"process-monitor"',
526
+ liveTail: 'workspace action:"open" surfaceId:"live-tail"',
527
+ },
528
+ processes,
529
+ returned: processes.length,
530
+ total: entries.length,
531
+ capabilities: capabilities(context),
532
+ policy: 'List/status/log routes are read-only and bounded. Starting, waiting on, or stopping a background process requires confirm:true and explicitUserRequest.',
533
+ };
534
+ }
535
+
536
+ export function describeBackgroundProcess(context: CommandContext, args: AgentHarnessBackgroundProcessArgs): BackgroundProcessResolution {
537
+ const manager = managerFrom(context);
538
+ if (!manager) {
539
+ return {
540
+ status: 'missing_lookup',
541
+ usage: 'Background processes are unavailable because ProcessManager is not wired into this runtime.',
542
+ };
543
+ }
544
+ const lookup = lookupFromArgs(args);
545
+ if (!lookup) {
546
+ return {
547
+ status: 'missing_lookup',
548
+ usage: 'execution action:"process" requires processId, target, or query. Use execution action:"processes" to inspect tracked process ids.',
549
+ };
550
+ }
551
+ const exact = manager.getStatus(lookup.input);
552
+ if (exact) {
553
+ return {
554
+ status: 'found',
555
+ process: describeProcessEntry(manager, exact, {
556
+ includeParameters: true,
557
+ lookup: { ...lookup, resolvedBy: 'id' },
558
+ }),
559
+ };
560
+ }
561
+ const matches = matchingProcesses(manager, lookup.input);
562
+ if (matches.length === 1) {
563
+ return {
564
+ status: 'found',
565
+ process: describeProcessEntry(manager, matches[0]!, {
566
+ includeParameters: true,
567
+ lookup: { ...lookup, resolvedBy: 'search' },
568
+ }),
569
+ };
570
+ }
571
+ if (matches.length > 1) {
572
+ return {
573
+ status: 'ambiguous',
574
+ input: lookup.input,
575
+ candidates: matches.slice(0, 8).map(candidateProcess),
576
+ };
577
+ }
578
+ return {
579
+ status: 'missing_lookup',
580
+ usage: `Unknown background process ${lookup.input}. Use execution action:"processes" to inspect tracked process ids.`,
581
+ };
582
+ }
583
+
584
+ function requireConfirmed(args: AgentHarnessBackgroundProcessArgs, action: string): string | null {
585
+ if (!readString(args.explicitUserRequest)) return `${action} requires explicitUserRequest with the user's exact request or a short faithful summary.`;
586
+ if (args.confirm !== true) return `${action} requires confirm:true after an explicit user request.`;
587
+ return null;
588
+ }
589
+
590
+ function readProcessAction(args: AgentHarnessBackgroundProcessArgs): string {
591
+ const action = (
592
+ readString(args.processAction)
593
+ || readString(args.action)
594
+ || readField(args, 'action')
595
+ || (readString(args.command) || readField(args, 'command') ? 'start' : '')
596
+ ).toLowerCase();
597
+ if (action === 'poll') return 'status';
598
+ if (action === 'kill') return 'stop';
599
+ return action;
600
+ }
601
+
602
+ function readCommand(args: AgentHarnessBackgroundProcessArgs): string {
603
+ return readString(args.command) || readField(args, 'command');
604
+ }
605
+
606
+ function readProcessId(args: AgentHarnessBackgroundProcessArgs): string {
607
+ return readProcessSessionId(args)?.input || readString(args.target);
608
+ }
609
+
610
+ function readCwd(context: CommandContext, args: AgentHarnessBackgroundProcessArgs): string | undefined {
611
+ const raw = readString(args.cwd) || readField(args, 'cwd');
612
+ const shellPaths = context.workspace.shellPaths;
613
+ if (!raw) return shellPaths?.workingDirectory;
614
+ if (!shellPaths) return raw;
615
+ const resolved = shellPaths.resolveWorkspacePath(raw);
616
+ if (!shellPaths.isWithinWorkingDirectory(resolved)) {
617
+ throw new Error('Background process cwd must stay inside the current Agent workspace.');
618
+ }
619
+ return resolved;
620
+ }
621
+
622
+ function looksLikeSudo(command: string): boolean {
623
+ return /(^|[\s;&|])(?:sudo|su|doas|pkexec)\b/.test(command);
624
+ }
625
+
626
+ async function waitForProcess(manager: ProcessManager, processId: string, timeoutMs: number): Promise<BackgroundProcess | undefined> {
627
+ const started = Date.now();
628
+ for (;;) {
629
+ const entry = manager.getStatus(processId);
630
+ if (!entry || entry.done) return entry;
631
+ if (Date.now() - started >= timeoutMs) return entry;
632
+ await new Promise((resolve) => setTimeout(resolve, 250));
633
+ }
634
+ }
635
+
636
+ export async function runBackgroundProcessAction(context: CommandContext, args: AgentHarnessBackgroundProcessArgs): Promise<Record<string, unknown>> {
637
+ const manager = managerFrom(context);
638
+ if (!manager) {
639
+ return {
640
+ status: 'unavailable',
641
+ reason: 'ProcessManager is not wired into this runtime.',
642
+ policy: 'Use foreground exec or connected-host delegation until background process lifecycle is available.',
643
+ };
644
+ }
645
+
646
+ if (args.pty === true || readField(args, 'pty').toLowerCase() === 'true') {
647
+ return {
648
+ ...(capabilities(context).pty as Record<string, unknown>),
649
+ status: 'unsupported',
650
+ capability: 'pty',
651
+ };
652
+ }
653
+
654
+ const action = readProcessAction(args);
655
+ if (action === 'write') {
656
+ const confirmationError = requireConfirmed(args, 'Background process stdin write');
657
+ if (confirmationError) throw new Error(confirmationError);
658
+ const writeInput = firstManagerFunction(manager, STDIN_WRITE_METHOD_NAMES);
659
+ if (!writeInput) {
660
+ return {
661
+ ...(capabilities(context).stdinWrite as Record<string, unknown>),
662
+ status: 'unsupported',
663
+ capability: 'stdinWrite',
664
+ processId: readProcessId(args) || null,
665
+ dataReceived: readData(args).length > 0,
666
+ };
667
+ }
668
+ const processId = readProcessId(args);
669
+ if (!processId) throw new Error('Background process stdin write requires processId.');
670
+ const data = readData(args);
671
+ if (!data) throw new Error('Background process stdin write requires non-empty data.');
672
+ const entry = manager.getStatus(processId);
673
+ if (!entry) {
674
+ return {
675
+ status: 'not_found',
676
+ capability: 'stdinWrite',
677
+ processId,
678
+ policy: 'No tracked process matched that id; no input was written.',
679
+ };
680
+ }
681
+ if (entry.done) {
682
+ return {
683
+ status: 'not_running',
684
+ capability: 'stdinWrite',
685
+ processId,
686
+ policy: 'Input is only written to currently running tracked processes.',
687
+ };
688
+ }
689
+ const result = await writeInput(processId, data);
690
+ return {
691
+ status: 'written',
692
+ capability: 'stdinWrite',
693
+ processId,
694
+ bytes: Buffer.byteLength(data),
695
+ result: summarizeWriteResult(result, data),
696
+ policy: 'Input was written through the shared ProcessManager stdin contract after explicit confirmation. The input data is not echoed in model-visible output.',
697
+ };
698
+ }
699
+ if (action === 'capabilities' || action === 'doctor' || action === 'parity') {
700
+ return {
701
+ status: 'available',
702
+ capabilities: capabilities(context),
703
+ methods: PROCESS_PARITY_METHODS,
704
+ policy: 'This is a read-only process UX contract report. It does not start, stop, or write to any process.',
705
+ };
706
+ }
707
+ if (action === 'start' || action === 'spawn' || action === 'run') {
708
+ const confirmationError = requireConfirmed(args, 'Background process start');
709
+ if (confirmationError) throw new Error(confirmationError);
710
+ const command = readCommand(args);
711
+ if (!command) throw new Error('Background process start requires command.');
712
+ if (looksLikeSudo(command)) {
713
+ return {
714
+ status: 'blocked',
715
+ capability: 'sudo',
716
+ reason: 'Background sudo prompts are not exposed by Agent because they can hang or hide privilege escalation.',
717
+ guidance: capabilities(context).sudo,
718
+ };
719
+ }
720
+ const cwd = readCwd(context, args);
721
+ const timeoutMs = clampTimeout(args.timeoutMs ?? readField(args, 'timeoutMs'), DEFAULT_BACKGROUND_TIMEOUT_MS);
722
+ const result = await manager.spawn(command, cwd, undefined, { timeout_ms: timeoutMs, sigterm_grace_ms: 5_000 });
723
+ return {
724
+ status: 'started',
725
+ processId: result.process_id,
726
+ processSessionId: result.process_id,
727
+ sessionId: result.process_id,
728
+ session_id: result.process_id,
729
+ pid: result.pid,
730
+ command: redactText(command),
731
+ cwd,
732
+ timeoutMs,
733
+ routes: {
734
+ inspect: result.process_id ? routeFor(result.process_id, 'background_process') : null,
735
+ poll: result.process_id ? routeFor(result.process_id, 'run_background_process', 'poll') : null,
736
+ log: result.process_id ? routeFor(result.process_id, 'run_background_process', 'log') : null,
737
+ wait: result.process_id ? routeFor(result.process_id, 'run_background_process', 'wait') : null,
738
+ stop: result.process_id ? routeFor(result.process_id, 'run_background_process', 'stop') : null,
739
+ visibleMonitor: 'workspace action:"open" surfaceId:"process-monitor"',
740
+ liveTail: result.process_id ? `workspace action:"open" surfaceId:"live-tail" target:"${result.process_id}"` : null,
741
+ },
742
+ policy: 'Started as a tracked local background process with bounded timeout and visible monitor/live-tail routes.',
743
+ };
744
+ }
745
+
746
+ if (action === 'stop' || action === 'kill' || action === 'cancel') {
747
+ const confirmationError = requireConfirmed(args, 'Background process stop');
748
+ if (confirmationError) throw new Error(confirmationError);
749
+ const processId = readProcessId(args);
750
+ if (!processId) throw new Error('Background process stop requires processId.');
751
+ const stopped = manager.stop(processId);
752
+ return {
753
+ status: stopped ? 'stopped' : 'not_found',
754
+ processId,
755
+ processSessionId: processId,
756
+ sessionId: processId,
757
+ session_id: processId,
758
+ policy: stopped
759
+ ? 'The process was signaled and removed from the shared ProcessManager.'
760
+ : 'No tracked process matched that id.',
761
+ };
762
+ }
763
+
764
+ if (action === 'wait') {
765
+ const confirmationError = requireConfirmed(args, 'Background process wait');
766
+ if (confirmationError) throw new Error(confirmationError);
767
+ const processId = readProcessId(args);
768
+ if (!processId) throw new Error('Background process wait requires processId.');
769
+ const timeoutMs = clampTimeout(args.timeoutMs ?? readField(args, 'timeoutMs'), DEFAULT_WAIT_TIMEOUT_MS);
770
+ const entry = await waitForProcess(manager, processId, timeoutMs);
771
+ return {
772
+ status: entry?.done ? 'completed' : entry ? 'still_running' : 'not_found',
773
+ processId,
774
+ processSessionId: processId,
775
+ sessionId: processId,
776
+ session_id: processId,
777
+ timeoutMs,
778
+ ...(entry ? { process: describeProcessEntry(manager, entry, { includeParameters: true }) } : {}),
779
+ policy: 'Wait observes the tracked process and does not send input or signals.',
780
+ };
781
+ }
782
+
783
+ if (action === 'list') return backgroundProcessSummary(context, args);
784
+ if (action === 'status' || action === 'log' || action === 'output') {
785
+ const resolved = describeBackgroundProcess(context, args);
786
+ if (resolved.status === 'found') return resolved.process;
787
+ return {
788
+ status: resolved.status,
789
+ ...(resolved.status === 'ambiguous' ? { input: resolved.input, candidates: resolved.candidates } : { usage: resolved.usage }),
790
+ };
791
+ }
792
+
793
+ throw new Error('run_background_process requires processAction start, stop/kill, wait, list, status/poll, log/output, write, or capabilities.');
794
+ }