total-recall-brain 3.46.2 → 3.47.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 (36) hide show
  1. package/frontend/dist/assets/index-CpsWeV4X.css +1 -0
  2. package/frontend/dist/assets/{index-D1goIPjC.js → index-poecbHSl.js} +100 -100
  3. package/frontend/dist/brand/total-recall-lockup-transparent.png +0 -0
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +1 -1
  6. package/frontend/src/App.tsx +15 -81
  7. package/frontend/src/components/DashboardOverview.tsx +164 -0
  8. package/frontend/src/components/brand/BrandMark.tsx +8 -6
  9. package/frontend/src/components/plugins/PluginWorkspace.tsx +112 -57
  10. package/frontend/src/components/workspace/PluginHome.tsx +3 -3
  11. package/frontend/src/index.css +43 -11
  12. package/package.json +1 -1
  13. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
  14. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +1 -1
  15. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +8 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +5 -33
  17. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-fleet-ops.md +16 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-publish.md +16 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +119 -10
  20. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  21. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +52 -4
  23. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +7 -7
  24. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +23 -22
  25. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +20 -33
  26. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +42 -49
  27. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/markdown-links.mjs +112 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +3 -2
  29. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +206 -60
  30. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +2 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +18 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +51 -2
  33. package/src/cli/context.mjs +6 -2
  34. package/src/core/context-compactor.mjs +2 -0
  35. package/src/server/routes/vault.mjs +1 -0
  36. package/frontend/dist/assets/index-BRYE1jmv.css +0 -1
@@ -18,7 +18,7 @@ export function PluginHome({ plugin, plugins, brainId, onCommand }: {
18
18
  const home = (plugin.manifest?.ui as { home?: string } | undefined)?.home || plugin.ui?.home
19
19
  const panel = panels.find(element => element.id === home) || panels[0]
20
20
  const operations = scopedPluginOperations(plugin)
21
- const chunks = published?.chunks.length ? published.chunks : defaults
21
+ const chunks = published?.chunks.length ? published.chunks : (panel ? [] : defaults)
22
22
 
23
23
  useEffect(() => {
24
24
  const controller = new AbortController()
@@ -62,8 +62,8 @@ export function PluginHome({ plugin, plugins, brainId, onCommand }: {
62
62
  )}
63
63
  {!loading && !published?.chunks.length && panel && !chunks.some(chunk => chunk.panel === panel.id) &&
64
64
  <PluginPanel plugin={plugin} elementId={panel.id} designCss={designCss} brainId={brainId} />}
65
- {!published?.chunks.length && !chunks.some(chunk => chunk.operations?.length) && operations.length > 0 && <section aria-label="Plugin operations">
66
- <h2>Start a workflow</h2>
65
+ {!loading && !panel && !chunks.length && operations.length > 0 && <section aria-label="Plugin operations">
66
+ <h2>Quick actions</h2>
67
67
  <ul className="plugin-home-operations">
68
68
  {operations.slice(0, 6).map(command => <li key={command.name}>
69
69
  <button type="button" className="btn btn-ghost" onClick={() => onCommand(command.name)}>{command.name}</button>
@@ -132,17 +132,15 @@ code, pre { font-family: var(--font-mono); }
132
132
  inset: 0 auto 0 0;
133
133
  width: var(--sidebar-width);
134
134
  min-width: var(--sidebar-width);
135
- background: var(--glass);
136
- backdrop-filter: blur(20px) saturate(1.4);
137
- -webkit-backdrop-filter: blur(20px) saturate(1.4);
138
- border-right: 1px solid var(--glass-border);
135
+ background: var(--bg-secondary);
136
+ border-right: 1px solid var(--border);
139
137
  display: flex;
140
138
  flex-direction: column;
141
139
  padding: 14px 0 12px;
142
140
  transform: translateX(-100%);
143
141
  transition: transform var(--transition-normal);
144
142
  z-index: 100;
145
- box-shadow: 4px 0 40px rgba(0, 0, 0, 0.25);
143
+ box-shadow: 4px 0 40px rgba(0, 0, 0, 0.45);
146
144
  }
147
145
 
148
146
  .sidebar-nav {
@@ -211,24 +209,56 @@ code, pre { font-family: var(--font-mono); }
211
209
  flex-direction: column;
212
210
  min-width: 0;
213
211
  min-height: 0;
212
+ max-width: 100%;
213
+ overflow-x: hidden;
214
214
  }
215
215
 
216
216
  .workspace-toolbar {
217
- display: flex;
217
+ display: grid;
218
+ grid-template-columns: 1fr auto 1fr;
218
219
  align-items: center;
219
- justify-content: space-between;
220
- gap: 12px;
221
- padding: 8px clamp(12px, 3vw, 24px);
220
+ gap: 8px 12px;
221
+ width: 100%;
222
+ max-width: 100%;
223
+ box-sizing: border-box;
224
+ padding: 10px clamp(12px, 3vw, 24px);
222
225
  border-bottom: 1px solid var(--border);
223
226
  }
224
227
 
225
228
  .workspace-logo {
229
+ grid-column: 2;
230
+ justify-self: center;
226
231
  border-radius: var(--radius-sm);
227
232
  }
228
233
 
234
+ .workspace-toolbar-actions {
235
+ grid-column: 3;
236
+ justify-self: end;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 8px;
240
+ }
241
+
242
+ @media (max-width: 680px) {
243
+ .workspace-toolbar {
244
+ grid-template-columns: minmax(0, 1fr);
245
+ justify-items: center;
246
+ }
247
+ .workspace-toolbar-spacer {
248
+ display: none;
249
+ }
250
+ .workspace-logo,
251
+ .workspace-toolbar-actions {
252
+ grid-column: 1;
253
+ justify-self: center;
254
+ }
255
+ }
256
+
229
257
  .workspace-body {
230
258
  flex: 1;
259
+ min-width: 0;
231
260
  min-height: 0;
261
+ max-width: 100%;
232
262
  }
233
263
 
234
264
  .workspace-body[hidden] {
@@ -399,14 +429,16 @@ code, pre { font-family: var(--font-mono); }
399
429
  .drawer-shortcut-edit { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto; gap: 8px; }
400
430
  .drawer-code, .drawer-context { font-family: var(--font-mono), monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
401
431
  .drawer-code { width: 100%; resize: vertical; }
402
- .drawer-html { display: block; width: 100%; height: clamp(160px, 35vh, 340px); border: 1px solid var(--border); border-radius: var(--radius-sm); }
432
+ .drawer-html { display: block; width: 100%; height: 110px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
403
433
  .help-content { display: grid; gap: 10px; min-width: 0; }
404
434
  .help-topic-picker { display: grid; gap: 4px; font-size: 13px; color: var(--text-secondary); }
405
435
  .help-topic-description { font-size: 12px; color: var(--text-secondary); }
406
436
  .help-article { min-width: 0; overflow-wrap: anywhere; }
407
437
 
408
438
  @media (max-width: 540px) {
409
- .app-layout { --workspace-right-width: calc(100vw - 48px); --workspace-bottom-height: min(72dvh, 600px); }
439
+ .app-layout { --workspace-right-width: calc(100vw - 48px); --workspace-bottom-height: min(72dvh, 600px); overflow-x: clip; }
440
+ .workspace-toolbar { gap: 6px 8px; padding: 6px 10px; }
441
+ .workspace-toolbar .btn { padding-inline: 10px; font-size: 13px; }
410
442
  .workspace-edge-handle--right { font-size: 11px; padding-inline: 5px; }
411
443
  .workspace-edge-handle--right[aria-expanded="true"] { font-size: 0; }
412
444
  .drawer-shortcut-edit { grid-template-columns: minmax(0, 1fr); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.46.2",
3
+ "version": "3.47.0",
4
4
  "description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-git",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-git",
9
- "version": "2.2.0",
9
+ "version": "2.3.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.14.5"
13
13
  },
14
14
  "devDependencies": {
15
- "total-recall-brain": "3.45.0",
15
+ "total-recall-brain": "3.46.2",
16
16
  "vitest": "^4.1.10"
17
17
  }
18
18
  },
@@ -6533,9 +6533,9 @@
6533
6533
  }
6534
6534
  },
6535
6535
  "node_modules/total-recall-brain": {
6536
- "version": "3.45.0",
6537
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.45.0.tgz",
6538
- "integrity": "sha512-HDYfROazZbsLHQsFPnlBcSpe1HL1QEi7JG8x/eNRUI06RxVQsZWl8LwBo2lTWjWRDrKmCy7BUmSXQDuWF3lrEw==",
6536
+ "version": "3.46.2",
6537
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.46.2.tgz",
6538
+ "integrity": "sha512-xEVtIA/lLbMtDJ4fO6WT37N4tDGbDjL/8imvrbFygOA+C17v6Y5M5023A5Wbm8PvwiUE18j8vzBdX5DJl8H3cg==",
6539
6539
  "dev": true,
6540
6540
  "license": "MIT",
6541
6541
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-git",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Reports the state of the current git repository: branch, dirty files, unpushed commits and stashes.",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "git",
4
4
  "name": "Git",
5
- "version": "2.2.0",
5
+ "version": "2.3.0",
6
6
  "description": "The whole git CLI through Total Recall: sign-in from the auth store, guarded commits and pushes, undo, fleet state, and the intent behind every change.",
7
7
  "author": "Total Recall",
8
8
  "license": "MIT",
@@ -147,5 +147,12 @@
147
147
  "distribution": {
148
148
  "tier": "official",
149
149
  "license": "open"
150
+ },
151
+ "reuse": {
152
+ "commands": [
153
+ "total-recall auth secret",
154
+ "total-recall ssss status"
155
+ ],
156
+ "owned": "Secret-firewalled commits and pushes, worktree management, unmerged branch detection, and fleet repository hygiene."
150
157
  }
151
158
  }
@@ -1,26 +1,9 @@
1
- import { spawn } from 'node:child_process';
2
1
  import { git, tr, ledger, intent, session } from '../lib/context.mjs';
3
- import { defaultBranch } from './unmerged.mjs';
4
2
 
5
- /**
6
- * Ask OpenWiki to refresh this repository without waiting for it. A due refresh
7
- * regenerates pages with a model for minutes; OpenWiki throttles repeats, keeps
8
- * its own 30-minute limit and writes the outcome to openwiki/.refresh-attempt.json.
9
- * Waiting here held every commit for 30 s, then killed only the CLI: the
10
- * generator finished on its own while the commit reported the refresh as failed.
11
- */
12
- export function requestWikiRefresh(ctx, launch = spawn) {
13
- try {
14
- const child = launch(ctx.env.GIT_PLUGIN_TR_BIN || 'total-recall', ['openwiki', 'refresh', '--repo', ctx.cwd, '--apply', '--json'], {
15
- cwd: ctx.cwd, env: ctx.env, detached: true, stdio: 'ignore', shell: false,
16
- });
17
- child.on?.('error', () => {});
18
- child.unref?.();
19
- return child.pid ? { status: 'requested', command: 'openwiki refresh', outcome: 'openwiki/.refresh-attempt.json' } : { status: 'failed', command: 'openwiki refresh', reason: 'could not start' };
20
- } catch {
21
- return { status: 'failed', command: 'openwiki refresh', reason: 'could not start' };
22
- }
23
- }
3
+ // A commit never starts an OpenWiki refresh. Requesting one after every commit on the default
4
+ // branch paid a model to re-check wiki pages after each automatic version-pin commit, in about
5
+ // 40 repositories per Total Recall release. OpenWiki refreshes from its own nightly task on one
6
+ // selected node (total-recall openwiki schedule).
24
7
 
25
8
  export const guards = [{
26
9
  name: 'commit-memory',
@@ -37,18 +20,7 @@ export const guards = [{
37
20
  // Save the commit before any optional provider call.
38
21
  store.append(record, { idempotencyKey: `commit-${sha}` });
39
22
  const memory = record.decisions.length ? tr(ctx, ['remember', 'decision', `${record.decisions.join('\n')}\nCommit: ${sha}`, '--project', '--slug', `git-decision-${sha}`]) : null;
40
- let wiki;
41
- // The wiki documents the default branch. Regenerating it from a feature branch spends model
42
- // calls on pages that only conflict when the branch merges.
43
- const branch = ctx.git(['symbolic-ref', '--quiet', '--short', 'HEAD']);
44
- const base = defaultBranch(ctx);
45
- if (!base || branch.status !== 0 || String(branch.stdout).trim() !== base.name) wiki = { status: 'skipped', reason: 'not on the default branch' };
46
- else {
47
- const available = tr(ctx, ['openwiki', '--help']);
48
- wiki = available.status !== 0 || available.error ? { status: 'skipped', reason: 'OpenWiki is unavailable' } : requestWikiRefresh(ctx);
49
- }
50
- store.append({ type: 'git.commit_memory', sha, timestamp: new Date().toISOString(), memory: memory === null ? 'skipped: no named decision' : memory.status === 0 && !memory.error ? 'recorded' : 'failed', openwiki: wiki });
23
+ store.append({ type: 'git.commit_memory', sha, timestamp: new Date().toISOString(), memory: memory === null ? 'skipped: no named decision' : memory.status === 0 && !memory.error ? 'recorded' : 'failed' });
51
24
  if (memory && (memory.status !== 0 || memory.error)) console.error('Commit saved. Repository decision memory failed.');
52
- if (wiki.status === 'failed') console.error('Commit saved. The OpenWiki refresh could not be started.');
53
25
  },
54
26
  }];
@@ -0,0 +1,16 @@
1
+ ---
2
+ type: ui-chunk:chunk
3
+ title: Fleet routing & agent coordination
4
+ description: Inspect fleet locations, bisect regressions, resolve conflicts, and coordinate multi-agent git handoffs.
5
+ timestamp: "2026-11-14T08:51:00Z"
6
+ plugin: git
7
+ id: git-fleet-ops
8
+ intents: [inspect fleet repos, locate branch or worktree, bisect or resolve conflicts, hand off git state]
9
+ surfaces: [page]
10
+ operations: [fleet, where, why, group, message, bisect-agent, resolve, handoff]
11
+ ---
12
+
13
+ <section>
14
+ <h2>Fleet &amp; coordination ops</h2>
15
+ <p>Track repository placement across the fleet, diagnose branch divergence, resolve conflicts, and hand off clean git state between agents.</p>
16
+ </section>
@@ -0,0 +1,16 @@
1
+ ---
2
+ type: ui-chunk:chunk
3
+ title: Secret-firewalled git publish & auth
4
+ description: Commit, push, configure git credential helper/auth/cli bindings, and safely undo recent changes.
5
+ timestamp: "2026-11-14T08:51:00Z"
6
+ plugin: git
7
+ id: git-publish
8
+ intents: [commit changes, push branch, configure git auth, undo git change]
9
+ surfaces: [page]
10
+ operations: [commit, push, credential-helper, auth, cli, undo]
11
+ ---
12
+
13
+ <section>
14
+ <h2>Publish &amp; credential guard</h2>
15
+ <p>Run secret-firewalled commits and pushes, bind Total Recall git credentials, or undo recent working-tree changes.</p>
16
+ </section>
@@ -5,8 +5,11 @@
5
5
  */
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
+ import { pathToFileURL } from "node:url";
8
9
  import { runBootstrapCommand } from './src/bootstrap.mjs';
9
- import { describeRetention, hasWiki, isDue, readProjects, refreshProject, refreshProjectAsync, resolveKey, staleness } from './src/refresh.mjs';
10
+ import { CREDIT_REFUSAL, NO_MODEL, describeRetention, hasWiki, isDue, readProjects, refreshProject, refreshProjectAsync, resolveKey, staleness } from './src/refresh.mjs';
11
+
12
+ const DEFAULT_REPO_CONCURRENCY = 2;
10
13
  import { watchUpstream, propagateUpdates, readUpstreamState, bundledGeneratorVersion } from './src/upstream-watcher.mjs';
11
14
  import { loadOpenWikiConfig, setOpenWikiConfigValue, resetOpenWikiConfig, resolveConfigPath, ALLOWED_CONFIG_KEYS, ALLOWED_PROVIDERS } from './src/config.mjs';
12
15
  import { discoverAllHubs } from './src/mesh-vaults.mjs';
@@ -26,12 +29,14 @@ import { refreshDiagnostics } from './src/refresh-diagnostics.mjs';
26
29
  * Defaults to a report; --apply performs the refreshes, because regenerating a
27
30
  * repository's documentation is a write to that repository.
28
31
  */
32
+ // OpenRouter answers 402 when the balance cannot cover a request.
33
+
29
34
  export async function refreshCommand(args, {
30
35
  read = readProjects, dueFor = isDue, refresh = refreshProjectAsync,
31
- getKey = resolveKey, configFor = loadOpenWikiConfig,
36
+ getKey = resolveKey, configFor = loadOpenWikiConfig, clock = Date.now,
32
37
  } = {}) {
33
38
  if (args.includes('--help') || args.includes('-h')) {
34
- console.log('Usage: total-recall openwiki refresh [--repo <name>] [--apply] [--force] [--diagnostics [--slot <local-minute>]] [--json]');
39
+ console.log('Usage: total-recall openwiki refresh [--repo <name>] [--apply [--budget-ms <n>]] [--force] [--diagnostics [--slot <local-minute>]] [--json]');
35
40
  return;
36
41
  }
37
42
  const json = args.includes('--json');
@@ -80,6 +85,8 @@ export async function refreshCommand(args, {
80
85
  if (!state.due) {
81
86
  if (state.skipped) {
82
87
  reports.push({ ...project, action: 'skipped', reason: state.reason });
88
+ } else if (state.held) {
89
+ reports.push({ ...project, action: 'held', uninitialized: Boolean(state.uninitialized), reason: state.reason, generationCommit: state.generationCommit, generationTime: state.generationTime, generatedAt: state.generatedAt, commitsSince: state.commitsSince });
83
90
  } else {
84
91
  reports.push({ ...project, action: 'current', reason: state.reason, generationCommit: state.generationCommit, generationTime: state.generationTime, generatedAt: state.generatedAt, commitsSince: state.commitsSince });
85
92
  }
@@ -102,6 +109,12 @@ export async function refreshCommand(args, {
102
109
  let payloadBudgetNote = null;
103
110
  if (apply && due.length) {
104
111
  const config = configFor();
112
+ if (!String(config.model || '').trim()) {
113
+ if (json) console.log(JSON.stringify({ ok: false, error: NO_MODEL, reports }));
114
+ else console.error(`OpenWiki refresh: ${NO_MODEL}`);
115
+ process.exitCode = 1;
116
+ return;
117
+ }
105
118
  const { key, from } = getKey({ provider: config.provider });
106
119
  if (!key) {
107
120
  const keyName = config.provider === 'google' ? 'GOOGLE_API_KEY' : 'OPENROUTER_API_KEY';
@@ -111,21 +124,37 @@ export async function refreshCommand(args, {
111
124
  process.exitCode = 1;
112
125
  return;
113
126
  }
127
+ // Repositories refreshed at once. Each one is a separate paid generator run, so
128
+ // the default stays low; OpenWiki's own page concurrency is left at its default.
114
129
  const concurrencyArg = args.find((a) => a.startsWith('--concurrency='));
115
130
  const concurrencyIndex = args.indexOf('--concurrency');
116
131
  const concurrency = Math.max(1, parseInt(
117
- concurrencyArg ? concurrencyArg.slice('--concurrency='.length) : (concurrencyIndex >= 0 ? args[concurrencyIndex + 1] : '8'),
132
+ concurrencyArg ? concurrencyArg.slice('--concurrency='.length) : (concurrencyIndex >= 0 ? args[concurrencyIndex + 1] : String(DEFAULT_REPO_CONCURRENCY)),
118
133
  10
119
- ) || 8);
134
+ ) || DEFAULT_REPO_CONCURRENCY);
135
+
136
+ // A scheduled run passes the time it has left before the task runner kills
137
+ // it. A generation started without room to finish is killed part-way, marked
138
+ // interrupted and paid for again on the next attempt, so none is started.
139
+ const budgetArg = args.find((a) => a.startsWith('--budget-ms='));
140
+ const budgetIndex = args.indexOf('--budget-ms');
141
+ const budgetMs = Number(budgetArg ? budgetArg.slice('--budget-ms='.length) : (budgetIndex >= 0 ? args[budgetIndex + 1] : NaN));
142
+ const generationMs = Number.isInteger(config.timeout_ms) ? config.timeout_ms : 300000;
143
+ const dispatchStarted = clock();
144
+ let stopReason = null;
120
145
 
121
146
  if (!json) {
122
- console.log(`OpenWiki refresh (apply): dispatching ${due.length} projects with concurrency ${concurrency}`);
147
+ console.log(`OpenWiki refresh (apply): dispatching ${due.length} projects with concurrency ${concurrency}${Number.isFinite(budgetMs) ? ` within ${Math.round(budgetMs / 1000)}s` : ''}`);
123
148
  }
124
149
 
125
150
  let queueIndex = 0;
126
151
  let completedCount = 0;
127
152
  const worker = async () => {
128
153
  while (queueIndex < due.length) {
154
+ if (!stopReason && Number.isFinite(budgetMs) && clock() - dispatchStarted + generationMs > budgetMs) {
155
+ stopReason = `not started: fewer than ${Math.round(generationMs / 1000)}s left in this run's ${Math.round(budgetMs / 1000)}s budget`;
156
+ }
157
+ if (stopReason) break;
129
158
  const itemIndex = queueIndex++;
130
159
  const entry = due[itemIndex];
131
160
  const mode = entry.mode || (entry.uninitialized ? 'init' : 'update');
@@ -142,6 +171,9 @@ export async function refreshCommand(args, {
142
171
  result = { action: 'failed', reason: error.message.split(key).join('[REDACTED]') };
143
172
  }
144
173
  completedCount++;
174
+ if (!stopReason && CREDIT_REFUSAL.test(String(result.reason || ''))) {
175
+ stopReason = `not started: the provider refused ${entry.name} for credit; add credit, then refresh again`;
176
+ }
145
177
  const index = reports.findIndex((r) => r.root === entry.root);
146
178
  reports[index] = { ...reports[index], ...result, credential: from };
147
179
  if (!json) {
@@ -154,6 +186,11 @@ export async function refreshCommand(args, {
154
186
 
155
187
  const workers = Array.from({ length: Math.min(concurrency, due.length) }, () => worker());
156
188
  await Promise.all(workers);
189
+ for (const entry of due.slice(queueIndex)) {
190
+ const index = reports.findIndex((r) => r.root === entry.root);
191
+ reports[index] = { ...reports[index], action: 'deferred', reason: stopReason };
192
+ }
193
+ if (stopReason && queueIndex < due.length) payloadBudgetNote = `${due.length - queueIndex} project(s) ${stopReason}.`;
157
194
  }
158
195
 
159
196
  const failed = reports.filter((r) => r.action === 'failed');
@@ -171,6 +208,8 @@ export async function refreshCommand(args, {
171
208
  refreshed: reports.filter((r) => r.action === 'refreshed' || r.action === 'initialized').length,
172
209
  failed: failed.length,
173
210
  partial: partial.length,
211
+ held: reports.filter((r) => r.action === 'held').length,
212
+ deferred: reports.filter((r) => r.action === 'deferred').length,
174
213
  skipped: reports.filter((r) => r.action === 'skipped').length,
175
214
  current: reports.filter((r) => r.action === 'current').length,
176
215
  },
@@ -438,6 +477,8 @@ async function generateCommand(args) {
438
477
 
439
478
  if (json) {
440
479
  console.log(JSON.stringify(res, null, 2));
480
+ } else if (res.dryRun) {
481
+ console.log(`\n${res.message}\n`);
441
482
  } else {
442
483
  console.log(`\n✔ Generated documentation for ${res.repoName} (${Math.round((res.durationMs || 0)/1000)}s) via ${res.generatorVia}`);
443
484
  console.log(` Output: ${res.outputDir}\n`);
@@ -572,8 +613,8 @@ async function configCommand(args) {
572
613
  else {
573
614
  console.log(`\nOpenWiki Configuration (${configPath}):`);
574
615
  console.log(` Provider : ${config.provider}`);
575
- console.log(` Model : ${config.model}`);
576
- console.log(` Fallback Model : ${config.fallback_model}`);
616
+ console.log(` Model : ${config.model || 'not set (refreshes stop until one is chosen)'}`);
617
+ console.log(` Fallback Model : ${config.fallback_model || 'not set'}`);
577
618
  console.log(` Max Tokens : ${config.max_tokens}`);
578
619
  console.log(` Timeout (ms) : ${config.timeout_ms}\n`);
579
620
  }
@@ -624,9 +665,76 @@ async function configCommand(args) {
624
665
  }
625
666
  }
626
667
 
668
+ /**
669
+ * The scheduled run. The Automations runner kills it at the task's declared
670
+ * timeout_ms, so it dispatches only generations that can finish a minute
671
+ * before that.
672
+ */
673
+ export function autoRefreshBudgetMs(manifestPath = new URL('./plugin.json', import.meta.url)) {
674
+ let timeout = null;
675
+ try {
676
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
677
+ timeout = manifest.tasks?.find((task) => task.command === 'auto-refresh')?.timeout_ms ?? null;
678
+ } catch { /* no readable manifest: use the runner's own limit */ }
679
+ return Math.max(60000, (Number.isInteger(timeout) ? timeout : 600000) - 60000);
680
+ }
681
+
682
+ async function loadScheduleCore() {
683
+ const root = process.env.TR_PACKAGE_ROOT;
684
+ if (!root) throw new Error('Run through total-recall openwiki schedule');
685
+ const load = (module) => import(pathToFileURL(path.join(root, 'src', 'core', module)).href);
686
+ const [loader, store, mesh] = await Promise.all([load('plugin-loader.mjs'), load('plugin-store.mjs'), load('mesh.mjs')]);
687
+ return { getPlugin: loader.getPlugin, readPluginRecord: store.readPluginRecord, patchPluginRecord: store.patchPluginRecord, getMeshSelf: mesh.getMeshSelf };
688
+ }
689
+
690
+ /**
691
+ * Where the nightly refresh runs. Like other selected-node tasks, the placement is
692
+ * task_nodes['auto-refresh'] in this plugin's record; with none, the task stays idle.
693
+ * Pick the node that holds the repository checkouts.
694
+ */
695
+ export async function scheduleCommand(args = [], { core = null, config = loadOpenWikiConfig } = {}) {
696
+ const json = args.includes('--json');
697
+ const usage = 'Usage: total-recall openwiki schedule [--node <mesh hostname>|--node self|--off] [--json]';
698
+ try {
699
+ if (args.includes('--help') || args.includes('-h')) { console.log(usage); return; }
700
+ const nodeIndex = args.indexOf('--node');
701
+ const nodeArg = nodeIndex >= 0 ? args[nodeIndex + 1] : null;
702
+ if (nodeIndex >= 0 && (!nodeArg || nodeArg.startsWith('--'))) throw new Error(usage);
703
+ const off = args.includes('--off');
704
+ if (off && nodeArg) throw new Error(usage);
705
+ const owner = core || await loadScheduleCore();
706
+ const plugin = owner.getPlugin('openwiki');
707
+ if (!plugin?.valid) throw new Error('Install a valid OpenWiki plugin first');
708
+ const record = owner.readPluginRecord(plugin) || {};
709
+ const task = plugin.manifest?.tasks?.find((entry) => entry.command === 'auto-refresh') || null;
710
+ let node = record.task_nodes?.['auto-refresh'] || null;
711
+ if (off || nodeArg) {
712
+ node = off ? null : nodeArg === 'self' ? owner.getMeshSelf()?.hostname || null : nodeArg;
713
+ if (!off && !node) throw new Error('This machine has no mesh hostname; name the node with --node <hostname>');
714
+ const taskNodes = { ...(record.task_nodes || {}) };
715
+ if (node) taskNodes['auto-refresh'] = node;
716
+ else delete taskNodes['auto-refresh'];
717
+ await owner.patchPluginRecord(plugin, { task_nodes: taskNodes });
718
+ }
719
+ const model = config().model || null;
720
+ const payload = { ok: true, task: 'auto-refresh', schedule: task?.schedule || null, placement: task?.placement || null, node, model, ready: Boolean(node && model) };
721
+ if (json) console.log(JSON.stringify(payload, null, 2));
722
+ else {
723
+ console.log(`OpenWiki nightly refresh: ${node ? `runs on ${node}` : 'idle (no node selected)'} at "${payload.schedule}"`);
724
+ console.log(` Model: ${model || `not set — ${NO_MODEL}`}`);
725
+ }
726
+ } catch (err) {
727
+ if (json) console.log(JSON.stringify({ ok: false, error: err.message }));
728
+ else console.error(`OpenWiki schedule: ${err.message}`);
729
+ process.exitCode = 1;
730
+ }
731
+ }
732
+
627
733
  async function autoRefreshCommand(args = []) {
628
- const hasApply = args.includes('--apply');
629
- const forwardArgs = hasApply ? args : ['--apply', ...args];
734
+ const forwardArgs = args.includes('--apply') ? [...args] : ['--apply', ...args];
735
+ if (!forwardArgs.some((a) => a === '--budget-ms' || a.startsWith('--budget-ms='))) {
736
+ forwardArgs.push(`--budget-ms=${autoRefreshBudgetMs()}`);
737
+ }
630
738
  return refreshCommand(forwardArgs);
631
739
  }
632
740
 
@@ -651,6 +759,7 @@ IMPLEMENTED.autorefresh = autoRefreshCommand;
651
759
  IMPLEMENTED.watch = watchCommand;
652
760
  IMPLEMENTED.propagate = propagateCommand;
653
761
  IMPLEMENTED.config = configCommand;
762
+ IMPLEMENTED.schedule = scheduleCommand;
654
763
  IMPLEMENTED.search = searchCommand;
655
764
  IMPLEMENTED.graph = graphCommand;
656
765
  IMPLEMENTED.token = tokenCommand;
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-openwiki",
9
- "version": "0.2.0",
9
+ "version": "0.3.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@gregiteen/ssss-cli": "0.14.5"
13
13
  },
14
14
  "devDependencies": {
15
- "total-recall-brain": "3.45.0"
15
+ "total-recall-brain": "3.46.2"
16
16
  }
17
17
  },
18
18
  "node_modules/@alcalzone/ansi-tokenize": {
@@ -5529,9 +5529,9 @@
5529
5529
  }
5530
5530
  },
5531
5531
  "node_modules/total-recall-brain": {
5532
- "version": "3.45.0",
5533
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.45.0.tgz",
5534
- "integrity": "sha512-HDYfROazZbsLHQsFPnlBcSpe1HL1QEi7JG8x/eNRUI06RxVQsZWl8LwBo2lTWjWRDrKmCy7BUmSXQDuWF3lrEw==",
5532
+ "version": "3.46.2",
5533
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.46.2.tgz",
5534
+ "integrity": "sha512-xEVtIA/lLbMtDJ4fO6WT37N4tDGbDjL/8imvrbFygOA+C17v6Y5M5023A5Wbm8PvwiUE18j8vzBdX5DJl8H3cg==",
5535
5535
  "dev": true,
5536
5536
  "license": "MIT",
5537
5537
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall",
6
6
  "scripts": {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
3
  "id": "openwiki",
4
4
  "name": "OpenWiki",
5
- "version": "0.2.0",
5
+ "version": "0.3.0",
6
6
  "description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall.",
7
7
  "license": "MIT",
8
8
  "runtime": {
@@ -17,10 +17,11 @@
17
17
  "command": "watch"
18
18
  },
19
19
  {
20
- "intent": "Auto-refresh OpenWiki documentation for repositories with new commits",
21
- "schedule": "*/30 * * * *",
20
+ "intent": "Refresh OpenWiki documentation nightly for repositories with real code changes, on the selected node",
21
+ "schedule": "20 1-5 * * *",
22
22
  "command": "auto-refresh",
23
- "timeout_ms": 600000
23
+ "timeout_ms": 600000,
24
+ "placement": "selected-node"
24
25
  }
25
26
  ],
26
27
  "branding": {
@@ -291,6 +292,12 @@
291
292
  "label": "Force regeneration",
292
293
  "type": "boolean"
293
294
  },
295
+ {
296
+ "name": "budgetMs",
297
+ "flag": "--budget-ms",
298
+ "label": "Start no generation that cannot finish within this many milliseconds",
299
+ "type": "number"
300
+ },
294
301
  {
295
302
  "name": "json",
296
303
  "flag": "--json",
@@ -422,6 +429,32 @@
422
429
  "type": "boolean"
423
430
  }
424
431
  ]
432
+ },
433
+ {
434
+ "name": "schedule",
435
+ "description": "Show or choose the node that runs the nightly wiki refresh (task auto-refresh)",
436
+ "effect": "write",
437
+ "output": "json",
438
+ "args": [
439
+ {
440
+ "name": "node",
441
+ "flag": "--node",
442
+ "label": "Mesh hostname, or self for this machine",
443
+ "type": "string"
444
+ },
445
+ {
446
+ "name": "off",
447
+ "flag": "--off",
448
+ "label": "Stop the nightly refresh on every node",
449
+ "type": "boolean"
450
+ },
451
+ {
452
+ "name": "json",
453
+ "flag": "--json",
454
+ "label": "JSON output",
455
+ "type": "boolean"
456
+ }
457
+ ]
425
458
  }
426
459
  ]
427
460
  },
@@ -460,8 +493,23 @@
460
493
  "formatPropagationReceipt"
461
494
  ]
462
495
  },
496
+ "auth": {
497
+ "plugin": "auth",
498
+ "methods": [
499
+ "passkey",
500
+ "google-oauth2"
501
+ ],
502
+ "session_verification": true
503
+ },
463
504
  "status": {
464
505
  "command": "status",
465
506
  "refresh_seconds": 300
507
+ },
508
+ "reuse": {
509
+ "commands": [
510
+ "total-recall auth secret",
511
+ "total-recall ssss status"
512
+ ],
513
+ "owned": "Documentation hub discovery, wiki page auditing, staleness tracking, and OpenWiki knowledge ingestion."
466
514
  }
467
515
  }