@dotdrelle/wiki-manager 0.15.28 → 0.15.32

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 (43) hide show
  1. package/.env.example +5 -2
  2. package/README.md +5 -4
  3. package/agents.docker-compose.override.example.yml +1 -1
  4. package/agents.docker-compose.yml +10 -0
  5. package/bin/wiki-manager.js +1 -1
  6. package/docker-compose.override.example.yml +3 -2
  7. package/mcp.endpoints.example.json +1 -1
  8. package/package.json +2 -2
  9. package/src/agent/graph.js +77 -13
  10. package/src/agent/graph.test.js +165 -8
  11. package/src/cli/runtimeStartup.test.js +14 -0
  12. package/src/cli/wiki-manager.js +118 -19
  13. package/src/cli/wiki-manager.test.js +192 -0
  14. package/src/commands/slash.js +168 -39
  15. package/src/commands/slash.test.js +100 -1
  16. package/src/core/agentsCompose.js +79 -0
  17. package/src/core/agentsCompose.test.js +99 -0
  18. package/src/core/buildInfo.json +2 -2
  19. package/src/core/composeOverrides.test.js +17 -3
  20. package/src/core/env.js +27 -10
  21. package/src/core/mcp.js +113 -38
  22. package/src/core/mcp.test.js +181 -20
  23. package/src/core/startupCheck.js +64 -27
  24. package/src/core/startupCheck.test.js +49 -1
  25. package/src/core/wikiSetup.js +57 -22
  26. package/src/core/wikiSetup.test.js +87 -0
  27. package/src/core/wikiWorkspace.test.js +16 -0
  28. package/src/orchestrator/agentRegistry.js +35 -7
  29. package/src/orchestrator/agentRegistry.test.js +73 -0
  30. package/src/orchestrator/dispatcher.js +6 -1
  31. package/src/orchestrator/dispatcher.test.js +24 -1
  32. package/src/orchestrator/objectiveResolver.js +24 -0
  33. package/src/orchestrator/objectiveResolver.test.js +27 -0
  34. package/src/runtime/auth.test.js +1 -65
  35. package/src/runtime/donna-contract.test.js +2 -1
  36. package/src/runtime/lifecycle.js +0 -33
  37. package/src/runtime/runner.test.js +10 -1
  38. package/src/runtime/supervisor.test.js +11 -1
  39. package/src/shell/LeftPane.tsx +49 -24
  40. package/src/shell/repl.js +114 -21
  41. package/src/shell/repl.test.js +88 -17
  42. package/src/shell/tui.tsx +10 -19
  43. package/wiki-workspace +98 -12
@@ -7,6 +7,7 @@ import {
7
7
  applyRuntimeStateToShellSession,
8
8
  chatAllowedTools,
9
9
  createSession,
10
+ isProductHelpQuestion,
10
11
  runHeadlessChatTurn,
11
12
  sanitizeOpenWikiPage,
12
13
  sanitizeOpenWikiPages,
@@ -42,23 +43,14 @@ test('ShellUI renders the user header full-width before constraining only its bo
42
43
  assert.match(source.slice(userBodyBranch), /justifyContent="flex-start"/);
43
44
  assert.doesNotMatch(source.slice(userBodyBranch, source.indexOf(') : line.status ?', userBodyBranch)), /#12263A/);
44
45
  assert.doesNotMatch(source, /messageHeaderSegments\(message\.role, contentColumns\)/);
45
- // The third argument reserves room for [ redo ]; the width stays `columns`.
46
- assert.match(source, /messageHeaderSegments\(message\.role, columns(, canRedo)?\)/);
46
+ assert.match(source, /messageHeaderSegments\(message\.role, columns\)/);
47
47
  });
48
48
 
49
- test('redo is offered on user questions only, next to copy', async () => {
49
+ test('ShellUI exposes copy without a reply or redo action', async () => {
50
50
  const source = await readFile(new URL('./LeftPane.tsx', import.meta.url), 'utf8');
51
- assert.match(source, /const canRedo = message\.role === 'user' && raw\.trim\(\)\.length > 0;/);
52
- assert.match(source, /props\.onRedo\?\.\(line\.redoIndex!, line\.redoContent!\)/);
53
-
54
- const session = await readFile(new URL('./useSession.ts', import.meta.url), 'utf8');
55
- // The runtime must be truncated first: its conversation is derived from the
56
- // event log and would be merged straight back in otherwise.
57
- const truncateIndex = session.indexOf('postRuntimeConversationTruncate(runtimeIndex');
58
- const spliceIndex = session.indexOf('full.splice(index)');
59
- const submitIndex = session.indexOf('return submitInput(content)');
60
- assert.ok(truncateIndex > 0 && spliceIndex > truncateIndex && submitIndex > spliceIndex);
61
- assert.match(session, /const windowOffset = Math\.max\(0, full\.length - messages\(\)\.length\);/);
51
+ assert.match(source, /const COPY_BTN = ' \[ copy \]';/);
52
+ assert.doesNotMatch(source, /\[\s*(?:reply|redo)\s*\]/i);
53
+ assert.doesNotMatch(source, /onRedo|redoContent|redoIndex|REDO_BTN/);
62
54
  });
63
55
 
64
56
  test('redo translates the local thread index into a runtime conversation index', async () => {
@@ -388,6 +380,29 @@ test('applyRuntimeStateToShellSession preserves terminal diagnostics when runtim
388
380
  assert.deepEqual(session.agentProjection.planPatches, [{ id: 'old-patch' }]);
389
381
  });
390
382
 
383
+ test('runtime sync does not resurrect a terminal plan dismissed by a newer user turn', () => {
384
+ const session = createSession();
385
+ session._dismissedTerminalRunId = 'old-run';
386
+ applyRuntimeStateToShellSession(session, {
387
+ runId: 'old-run',
388
+ status: 'error',
389
+ conversation: [{ role: 'assistant', content: 'Old failure' }],
390
+ plan: [{ id: 'old-task', status: 'failed' }],
391
+ activities: [{ key: 'old-job', status: 'failed', terminal: true }],
392
+ logs: ['old error'],
393
+ summary: 'Old run failed',
394
+ workflow: { nodes: [{ id: 'task:old' }], relations: [] },
395
+ planPatches: [{ id: 'old-patch' }],
396
+ });
397
+
398
+ assert.equal(session.headlessPlan, null);
399
+ assert.deepEqual(session.activities, {});
400
+ assert.equal(session.workflow, null);
401
+ assert.deepEqual(session.agentProjection.logs, []);
402
+ assert.equal(session.agentProjection.summary, null);
403
+ assert.deepEqual(session.agentProjection.conversation, [{ role: 'assistant', content: 'Old failure' }]);
404
+ });
405
+
391
406
  test('legacy runtime sync does not synthesize per-job or canned completion messages', () => {
392
407
  const session = createSession();
393
408
  applyRuntimeStateToShellSession(session, {
@@ -567,6 +582,53 @@ test('/agent <question> reports an unavailable runtime without leaving chat mode
567
582
  assert.match(conversationMessages(session).at(-1).content, /runtime stopped/);
568
583
  });
569
584
 
585
+ test('every slash-command result is presented by Donna when an agent is available', async () => {
586
+ const session = createSession();
587
+ let synthesisInput = '';
588
+ const agent = {
589
+ async invoke({ input, session: activeSession }) {
590
+ synthesisInput = input;
591
+ assert.equal(activeSession._responseSynthesisOnly, true);
592
+ return { response: 'Vous utilisez wiki-manager test.' };
593
+ },
594
+ };
595
+
596
+ const result = await runLine('/version', {
597
+ agent,
598
+ packageJson: { version: 'test' },
599
+ session,
600
+ });
601
+
602
+ assert.equal(result.exit, false);
603
+ assert.ok(synthesisInput.includes('commande shell /version'));
604
+ assert.deepEqual(conversationMessages(session).map((message) => message.role), ['user', 'donna']);
605
+ assert.equal(conversationMessages(session)[0].content, '/version');
606
+ assert.equal(conversationMessages(session)[1].content, 'Vous utilisez wiki-manager test.');
607
+ assert.equal(session._responseSynthesisOnly, undefined);
608
+ });
609
+
610
+ test('/status remains an immediate deterministic display without Donna', async () => {
611
+ const session = createSession();
612
+ let invoked = false;
613
+ const agent = {
614
+ async invoke() {
615
+ invoked = true;
616
+ return { response: 'must not run' };
617
+ },
618
+ };
619
+
620
+ const result = await runLine('/status', {
621
+ agent,
622
+ packageJson: { version: 'test' },
623
+ session,
624
+ });
625
+
626
+ assert.equal(result.exit, false);
627
+ assert.equal(invoked, false);
628
+ assert.equal(conversationMessages(session).at(-1)?.role, 'command');
629
+ assert.match(conversationMessages(session).at(-1)?.content ?? '', /Workspace · -/);
630
+ });
631
+
570
632
  test('runLine does not update workspace profile before Donna handles the request', async () => {
571
633
  const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-shell-'));
572
634
  mkdirSync(join(workspacePath, '.wiki'), { recursive: true });
@@ -745,9 +807,9 @@ test('chatAllowedTools exposes exactly the declared MCP tools to /chat', () => {
745
807
  };
746
808
  const names = chatAllowedTools(session).map((item) => item.function.name).sort();
747
809
  // cme_setup: not declared. documents_status: server absent from chatAccess.
748
- // cme_export_run: declared, so it is offered — the allow-list is the
749
- // operator's explicit decision, not a suggestion filtered by a heuristic.
750
- assert.deepEqual(names, ['cme__cme_export_run', 'cme__cme_sources_list', 'cme__cme_status']);
810
+ // cme_export_run is an orchestration bypass and stays hidden even when an
811
+ // older allow-list still names it.
812
+ assert.deepEqual(names, ['cme__cme_sources_list', 'cme__cme_status']);
751
813
  });
752
814
 
753
815
  test('chatAllowedTools offers every declared tool, reads and writes alike', () => {
@@ -940,6 +1002,15 @@ test('runHeadlessChatTurn (HTTP /chat) uses the read-tool path and returns text'
940
1002
  assert.doesNotMatch(reply, /STREAM_FALLBACK/);
941
1003
  });
942
1004
 
1005
+ test('product-help questions are detected without treating ordinary domain questions as product help', () => {
1006
+ assert.equal(isProductHelpQuestion('À quoi correspond Parallelism & throughput ?'), true);
1007
+ assert.equal(isProductHelpQuestion('Comment fonctionne Donna ?'), true);
1008
+ assert.equal(isProductHelpQuestion('Explique le panneau /status'), true);
1009
+ assert.equal(isProductHelpQuestion('Comment fonctionnent les approbations ?'), true);
1010
+ assert.equal(isProductHelpQuestion('Que dit le wiki sur la météo ?'), false);
1011
+ assert.equal(isProductHelpQuestion('Résume ce document'), false);
1012
+ });
1013
+
943
1014
  test('sanitizeOpenWikiPage accepts wiki and untracked markdown context paths', () => {
944
1015
  assert.equal(sanitizeOpenWikiPage('wiki/concepts/foo.md'), 'wiki/concepts/foo.md');
945
1016
  assert.equal(sanitizeOpenWikiPage(' wiki/a.md '), 'wiki/a.md');
package/src/shell/tui.tsx CHANGED
@@ -150,30 +150,22 @@ function App(props: {
150
150
  const [preflightBusy, setPreflightBusy] = createSignal(false);
151
151
  let ctrlCTimer: ReturnType<typeof setTimeout> | null = null;
152
152
  let exiting = false;
153
- // Single exit path: the owned-runtime shutdown MUST happen here, on the
154
- // user's actual exit gesture. render() resolves at MOUNT, so code placed
155
- // after `await runOpenTuiShell(...)` runs while the shell is still on
156
- // screen — 0.12.9 shipped that and killed the runtime mid-session.
153
+ // The runtime is shared infrastructure: `llm-wiki serve` can still be using
154
+ // it after this shell closes. Exiting the UI only destroys the renderer;
155
+ // explicit lifecycle commands own runtime shutdown.
157
156
  const exitShell = () => {
158
157
  if (exiting) return;
159
158
  exiting = true;
160
159
  setExitStatus('Fermeture enclenchée…');
161
- // Register the complete shutdown before destroying the renderer: onDestroy
162
- // resolves runOpenTuiShell, which must still await this cleanup task.
163
160
  const task = Promise.resolve().then(async () => {
164
161
  renderer.destroy();
165
- console.log('[wiki-manager] fermeture enclenchée — nettoyage en cours…');
166
- const messages: string[] = [];
167
- try {
168
- if (props.runtime?.url) {
169
- const { shutdownOwnedRuntime } = await import('../runtime/lifecycle.js');
170
- await shutdownOwnedRuntime(props.runtime, { log: (message: string) => { messages.push(message); } });
171
- }
172
- } catch {
173
- // Best effort: never block the exit on runtime cleanup.
174
- }
175
- for (const message of messages) console.log(`[wiki-manager] ${message}`);
176
- console.log('[wiki-manager] fermeture terminée — shell clos proprement.');
162
+ console.log('[wiki-manager] shell closed; shared runtime left running.');
163
+ // MCP clients, image-refresh probes, and reconnect timers may still own
164
+ // event-loop handles. They belong to this shell process, not to the
165
+ // detached shared runtime. Once the renderer is gone there is no useful
166
+ // foreground work left, so terminate explicitly instead of leaving the
167
+ // terminal pending indefinitely.
168
+ process.exit(0);
177
169
  });
178
170
  if (props.exitRef) props.exitRef.current = task;
179
171
  };
@@ -398,7 +390,6 @@ function App(props: {
398
390
  scrollConversation={state.scrollConversation}
399
391
  spinnerFrame={SPINNER_FRAMES[spinnerIndex()] ?? SPINNER_FRAMES[0]}
400
392
  onInputHeightChange={setChatInputHeight}
401
- onRedo={(index, content) => { void state.redoMessage(index, content); }}
402
393
  onCopy={(content) => showCopyHint(
403
394
  copyToClipboard(content, renderer)
404
395
  ? 'Copied.'
package/wiki-workspace CHANGED
@@ -17,6 +17,7 @@ WORKSPACES_DIR="${WIKI_WORKSPACES_DIR:-$DEFAULT_MANAGER_DIR/workspaces}"
17
17
  MANAGER_ENV_FILE="${WIKI_MANAGER_ENV_FILE:-$DEFAULT_MANAGER_DIR/.env}"
18
18
  MANAGER_ENDPOINTS_FILE="${WIKI_MANAGER_ENDPOINTS_FILE:-$DEFAULT_MANAGER_DIR/mcp.endpoints.json}"
19
19
  MANAGER_STATE_DIR="$(cd "$(dirname "$MANAGER_ENV_FILE")" && pwd)"
20
+ MANAGER_COMPOSE_DIR="$MANAGER_STATE_DIR/.wiki/compose"
20
21
  MANAGER_RUNTIME_DIR="$MANAGER_STATE_DIR/.wiki/runtime"
21
22
  LEGACY_MANAGER_RUNTIME_DIR="$MANAGER_STATE_DIR/.wiki-manager"
22
23
  if [[ ! -e "$MANAGER_RUNTIME_DIR" && -d "$LEGACY_MANAGER_RUNTIME_DIR" ]]; then
@@ -83,6 +84,7 @@ Usage:
83
84
  wiki-workspace [--cacert <path>] <command> ...
84
85
  wiki-workspace config <workspace> [path]
85
86
  wiki-workspace up <workspace>
87
+ wiki-workspace refresh
86
88
  wiki-workspace wiki <workspace> <command> [args...]
87
89
  wiki-workspace list
88
90
 
@@ -90,6 +92,7 @@ Commands:
90
92
  config <workspace> [path] Create workspace folder, auto-select ports, run wiki init
91
93
  up <workspace> [--open] Start workspace stack: wiki + production
92
94
  list List configured workspaces
95
+ refresh Stop runtime/stacks and remove their Compose images
93
96
 
94
97
  agents up [--pull] Start all external agents (cme, documents)
95
98
  agents down Stop all external agents
@@ -120,9 +123,9 @@ Configuration:
120
123
  Runtime node: WIKI_MANAGER_NODE_BIN=/path/to/node
121
124
  Local CA: --cacert /absolute/path/to/ca.pem (Docker must be able to read this host path)
122
125
 
123
- Compose overrides (next to the manager .env, created once, never regenerated):
124
- docker-compose.override.yml Workspace stack (serve, mcp-http, production-mcp)
125
- agents.docker-compose.override.yml Agents stack (cme, documents, connectors)
126
+ Compose overrides (under .wiki/compose, created once, never regenerated):
127
+ .wiki/compose/docker-compose.override.yml Workspace stack
128
+ .wiki/compose/agents.docker-compose.override.yml Agents stack
126
129
  Use them for proxy passthrough, extra mounts and optional agents.
127
130
  Never edit .wiki/runtime/*.compose.yml — that is generated state, rewritten
128
131
  on every compose command.
@@ -259,6 +262,31 @@ agents_compose() {
259
262
  local compose_env_args=()
260
263
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
261
264
 
265
+ # Local-iteration build only: the packaged connectors service carries a
266
+ # `build:` block pointing at the sibling agent-connectors repo, whose OAuth
267
+ # credentials live in a gitignored .env.build.local. Compose resolves the
268
+ # value-less build args from *this* process environment, not from
269
+ # --env-file, so the variables must be exported here or the image ships with
270
+ # empty WIKILLM_GOOGLE_OAUTH_* and every Google connector stays unusable.
271
+ local connectors_build_env="$ROOT_DIR/../agent-external/agent-connectors/.env.build.local"
272
+ if [[ -f "$connectors_build_env" ]]; then
273
+ local build_key build_value
274
+ while IFS='=' read -r build_key build_value; do
275
+ case "$build_key" in
276
+ WIKILLM_GOOGLE_OAUTH_CLIENT_ID|WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET) ;;
277
+ *) continue ;;
278
+ esac
279
+ [[ -n "${!build_key:-}" ]] || export "$build_key=$build_value"
280
+ done < <(grep -v '^[[:space:]]*#' "$connectors_build_env" || true)
281
+ fi
282
+
283
+ require_connectors_build_credentials() {
284
+ local build_key
285
+ for build_key in WIKILLM_GOOGLE_OAUTH_CLIENT_ID WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET; do
286
+ [[ -n "${!build_key:-}" ]] || die "cannot build connectors: $build_key is missing from $connectors_build_env"
287
+ done
288
+ }
289
+
262
290
  _agents_dc() {
263
291
  local cacert_args=()
264
292
  local profile_args=()
@@ -266,10 +294,10 @@ agents_compose() {
266
294
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
267
295
  read_lines_into_array cacert_args cacert_compose_args "$agents_compose_file" "agents.cacert.compose.yml"
268
296
  # User-owned override: optional external agents, proxy passthrough and
269
- # any local fix live in a file the user maintains NEXT TO the .env —
297
+ # any local fix live in a file the user maintains under .wiki/compose —
270
298
  # scaffolded once by ensure_compose_override, never overwritten afterwards.
271
299
  local override_args=()
272
- local user_override="$MANAGER_STATE_DIR/agents.docker-compose.override.yml"
300
+ local user_override="$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml"
273
301
  [[ -f "$user_override" ]] && override_args+=(-f "$user_override")
274
302
  WORKSPACES_ROOT="$workspaces_root" AGENTS_DATA_DIR="$agents_data_dir" \
275
303
  COMPOSE_PROFILES= \
@@ -287,6 +315,9 @@ agents_compose() {
287
315
  [[ $# -eq 0 ]] || die "agents up accepts only --pull"
288
316
  ensure_agent_tokens
289
317
  ensure_endpoints_file
318
+ if connectors_enabled && [[ -f "$ROOT_DIR/../agent-external/agent-connectors/Dockerfile" ]]; then
319
+ require_connectors_build_credentials
320
+ fi
290
321
  # The .env may have just been created above — recompute the env-file
291
322
  # flag so freshly generated tokens reach the containers on first run.
292
323
  compose_env_args=()
@@ -307,12 +338,16 @@ agents_compose() {
307
338
  "${CME_MCP_PORT:-3336}" "${DOCUMENTS_MCP_PORT:-3337}"
308
339
  connectors_enabled && printf ' connectors=:%s' "${CONNECTORS_MCP_PORT:-3338}"
309
340
  printf '\n'
310
- [[ ! -f "$MANAGER_STATE_DIR/agents.docker-compose.override.yml" ]] || printf 'User override: %s\n' "$MANAGER_STATE_DIR/agents.docker-compose.override.yml"
341
+ [[ ! -f "$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml" ]] || printf 'User override: %s\n' "$MANAGER_COMPOSE_DIR/agents.docker-compose.override.yml"
311
342
  ;;
312
343
  down)
313
344
  [[ $# -eq 0 ]] || die "agents down does not take arguments"
314
345
  _agents_dc down
315
346
  ;;
347
+ purge)
348
+ [[ $# -eq 0 ]] || die "agents purge does not take arguments"
349
+ _agents_dc down --rmi all --remove-orphans
350
+ ;;
316
351
  logs)
317
352
  local log_args=()
318
353
  read_lines_into_array log_args logs_args "$@"
@@ -534,6 +569,13 @@ if (enabled === 'true') {
534
569
  Authorization: 'Bearer ${CONNECTORS_MCP_AUTH_TOKEN}',
535
570
  },
536
571
  };
572
+ const connectorEndpoint = config.mcpServers.connectors;
573
+ connectorEndpoint.requireApproval = [
574
+ ...new Set([
575
+ ...(Array.isArray(connectorEndpoint.requireApproval) ? connectorEndpoint.requireApproval : []),
576
+ 'connectors_gmail_modify',
577
+ ]),
578
+ ];
537
579
  config.chatAccess.servers.connectors ??= {};
538
580
  const connectorAccess = config.chatAccess.servers.connectors;
539
581
  // One shape for every server: a single `allow` list. The legacy
@@ -545,6 +587,9 @@ if (enabled === 'true') {
545
587
  ...(Array.isArray(connectorAccess.allowActions) ? connectorAccess.allowActions : []),
546
588
  'connectors_google_status',
547
589
  'connectors_google_oauth_start',
590
+ 'connectors_gmail_summary',
591
+ 'connectors_gmail_search',
592
+ 'connectors_gmail_labels',
548
593
  ]),
549
594
  ];
550
595
  delete connectorAccess.allowActions;
@@ -676,6 +721,19 @@ runtime_command() {
676
721
  ;;
677
722
  down|stop)
678
723
  [[ $# -eq 0 ]] || die "runtime down does not take arguments"
724
+ token="$(ensure_manager_runtime_token)"
725
+ if runtime_health_check; then
726
+ RUNTIME_URL="$(runtime_health_url)/shutdown" RUNTIME_TOKEN="$token" "$node_bin" --input-type=module -e \
727
+ "const h=process.env.RUNTIME_TOKEN?{Authorization:'Bearer '+process.env.RUNTIME_TOKEN}:{}; const r=await fetch(process.env.RUNTIME_URL,{method:'POST',headers:h}); process.exit(r.ok?0:1);"
728
+ local wait_count=0
729
+ while runtime_health_check && (( wait_count < 40 )); do
730
+ sleep 0.1
731
+ (( wait_count++ )) || true
732
+ done
733
+ rm -f "$pid_file"
734
+ printf 'agent-runtime stopped\n'
735
+ return
736
+ fi
679
737
  if ! runtime_pid_running; then
680
738
  printf 'agent-runtime is not running from %s\n' "$pid_file"
681
739
  rm -f "$pid_file"
@@ -720,6 +778,23 @@ runtime_command() {
720
778
  esac
721
779
  }
722
780
 
781
+ refresh_project() {
782
+ printf 'Stopping shared runtime...\n'
783
+ runtime_command down
784
+
785
+ local entry workspace
786
+ for entry in "$WORKSPACES_DIR"/*; do
787
+ [[ -f "$entry/.env" ]] || continue
788
+ workspace="$(basename "$entry")"
789
+ printf 'Removing workspace stack and images: %s\n' "$workspace"
790
+ compose_for_workspace "$workspace" down --rmi all --remove-orphans
791
+ done
792
+
793
+ printf 'Removing external-agent stack and images...\n'
794
+ agents_compose purge
795
+ printf 'Refresh complete. Persistent workspace and agent data were preserved.\n'
796
+ }
797
+
723
798
  ensure_runtime_up() {
724
799
  if [[ "${WIKI_MANAGER_RUNTIME_AUTOSTART:-1}" == "0" ]]; then
725
800
  return
@@ -745,10 +820,16 @@ ensure_compose_override() {
745
820
  # the generated override and lose the change on the next compose command.
746
821
  local example_name="$1"
747
822
  local target_name="$2"
748
- local target="$MANAGER_STATE_DIR/$target_name"
823
+ local target="$MANAGER_COMPOSE_DIR/$target_name"
824
+ local legacy="$MANAGER_STATE_DIR/$target_name"
749
825
  [[ -f "$target" ]] && return 0
826
+ mkdir -p "$MANAGER_COMPOSE_DIR"
827
+ if [[ -f "$legacy" ]]; then
828
+ mv "$legacy" "$target"
829
+ printf 'Migrated user-owned compose override: %s -> %s\n' "$legacy" "$target" >&2
830
+ return 0
831
+ fi
750
832
  [[ -f "$ROOT_DIR/$example_name" ]] || return 0
751
- mkdir -p "$MANAGER_STATE_DIR"
752
833
  cp "$ROOT_DIR/$example_name" "$target"
753
834
  printf 'Created %s (user-owned compose override, never regenerated)\n' "$target" >&2
754
835
  }
@@ -889,11 +970,11 @@ compose_for_workspace() {
889
970
  WIKI_MCP_PORT="$mcp_port" \
890
971
  PRODUCTION_MCP_PORT="$prod_port" \
891
972
  read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
892
- # Same contract as the agents stack: a single global user-owned override next
893
- # to the .env, merged after the packaged file and before the generated CA
973
+ # Same contract as the agents stack: a single global user-owned override in
974
+ # .wiki/compose, merged after the packaged file and before the generated CA
894
975
  # override so the operator can still be overruled on CA paths only.
895
976
  local override_args=()
896
- local user_override="$MANAGER_STATE_DIR/docker-compose.override.yml"
977
+ local user_override="$MANAGER_COMPOSE_DIR/docker-compose.override.yml"
897
978
  [[ -f "$user_override" ]] && override_args+=(-f "$user_override")
898
979
 
899
980
  WIKI_WORKSPACE_PATH="$ws_path" \
@@ -1149,7 +1230,7 @@ config_workspace() {
1149
1230
  WIKI_MCP_PORT="$mcp_port" \
1150
1231
  read_lines_into_array cacert_args cacert_compose_args "$ROOT_DIR/docker-compose.yml" "cacert.compose.yml"
1151
1232
  local override_args=()
1152
- local user_override="$MANAGER_STATE_DIR/docker-compose.override.yml"
1233
+ local user_override="$MANAGER_COMPOSE_DIR/docker-compose.override.yml"
1153
1234
  [[ -f "$user_override" ]] && override_args+=(-f "$user_override")
1154
1235
 
1155
1236
  WIKI_WORKSPACE_PATH="$target_path" \
@@ -1197,6 +1278,11 @@ main() {
1197
1278
  exit 0
1198
1279
  fi
1199
1280
 
1281
+ if [[ $# -eq 1 && "$1" == "refresh" ]]; then
1282
+ refresh_project
1283
+ exit 0
1284
+ fi
1285
+
1200
1286
  # config <workspace> [path]
1201
1287
  if [[ $# -ge 2 && "$1" == "config" ]]; then
1202
1288
  local cfg_workspace="$2"