total-recall-brain 3.44.0 → 3.45.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 (212) hide show
  1. package/bin/total-recall.mjs +6 -4
  2. package/docs/ARCHITECTURE.md +10 -7
  3. package/docs/reference/cli-reference.md +13 -8
  4. package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
  5. package/frontend/dist/index.html +1 -1
  6. package/frontend/src/App.tsx +2 -2
  7. package/frontend/src/pages/PluginsPage.tsx +1 -1
  8. package/package.json +1 -1
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  11. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
  12. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
  15. package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
  16. package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
  17. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
  20. package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
  21. package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
  25. package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
  26. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
  27. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
  34. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
  42. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
  43. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
  44. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
  45. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
  46. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
  49. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
  50. package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
  51. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
  68. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
  69. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
  74. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
  75. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
  76. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
  77. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
  78. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
  80. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
  81. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
  82. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
  84. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
  85. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
  86. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
  87. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
  88. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
  90. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
  91. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
  92. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
  93. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
  94. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
  96. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
  97. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
  98. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
  99. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
  101. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
  102. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
  105. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
  106. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
  107. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
  108. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
  109. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
  110. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
  111. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
  112. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
  113. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
  114. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
  115. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
  116. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
  117. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
  118. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
  119. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
  120. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
  122. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
  123. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  124. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  125. package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
  126. package/src/cli/agent.mjs +1 -1
  127. package/src/cli/command.mjs +41 -2
  128. package/src/cli/dream.mjs +93 -69
  129. package/src/cli/harness.mjs +4 -4
  130. package/src/cli/help.mjs +1 -1
  131. package/src/cli/init.mjs +0 -25
  132. package/src/cli/lessons.mjs +51 -0
  133. package/src/cli/memory-plugin.mjs +1 -1
  134. package/src/cli/plugin/copies.mjs +64 -14
  135. package/src/cli/plugin/create.mjs +6 -7
  136. package/src/cli/plugin/index.mjs +11 -7
  137. package/src/cli/plugin/install.mjs +10 -15
  138. package/src/cli/plugin/remove.mjs +1 -1
  139. package/src/cli/plugin/search.mjs +2 -2
  140. package/src/cli/preflight.mjs +19 -6
  141. package/src/cli/proposals.mjs +1 -1
  142. package/src/cli/recall.mjs +66 -23
  143. package/src/cli/remember.mjs +3 -14
  144. package/src/cli/server.mjs +18 -0
  145. package/src/cli/startup.mjs +1 -0
  146. package/src/cli/task.mjs +9 -2
  147. package/src/cli/update.mjs +6 -2
  148. package/src/core/access-ledger.mjs +78 -0
  149. package/src/core/agent-manager.mjs +2 -2
  150. package/src/core/brain-locate.mjs +15 -0
  151. package/src/core/brain-updates.mjs +31 -11
  152. package/src/core/claude-hooks.mjs +3 -1
  153. package/src/core/cleanup-task.mjs +15 -0
  154. package/src/core/cli-commands.mjs +2 -0
  155. package/src/core/cli-maintenance.mjs +4 -1
  156. package/src/core/cli-semantic-index.mjs +23 -3
  157. package/src/core/context-compiler.mjs +20 -0
  158. package/src/core/daemon-loop.mjs +39 -62
  159. package/src/core/default-plugins.mjs +1 -1
  160. package/src/core/dream.mjs +33 -105
  161. package/src/core/ensure-dependencies.mjs +148 -0
  162. package/src/core/fast-recall.mjs +2 -2
  163. package/src/core/field-evaluation.mjs +23 -0
  164. package/src/core/gate-mode.mjs +1 -1
  165. package/src/core/harness-hooks.mjs +14 -2
  166. package/src/core/lesson-hooks.mjs +74 -0
  167. package/src/core/lesson-memory.mjs +24 -0
  168. package/src/core/lesson-sessions.mjs +52 -0
  169. package/src/core/lessons.mjs +96 -0
  170. package/src/core/meta-harness.mjs +2 -2
  171. package/src/core/optimizer.mjs +66 -165
  172. package/src/core/plugin-contracts/card.mjs +3 -0
  173. package/src/core/plugin-loader.mjs +12 -3
  174. package/src/core/plugin-runtime.mjs +40 -1
  175. package/src/core/plugin-store.mjs +35 -23
  176. package/src/core/post-mortem.mjs +52 -36
  177. package/src/core/preflight-gate.mjs +23 -2
  178. package/src/core/proposal-applier.mjs +31 -2
  179. package/src/core/repo-sync.mjs +4 -6
  180. package/src/core/required-plugins.mjs +26 -4
  181. package/src/core/rule-length.mjs +23 -0
  182. package/src/core/runtime.mjs +1 -1
  183. package/src/core/scheduler.mjs +35 -43
  184. package/src/core/schema.mjs +1 -2
  185. package/src/core/search.mjs +4 -3
  186. package/src/core/server-service.mjs +148 -0
  187. package/src/core/session-watcher.mjs +19 -1
  188. package/src/core/skill-gap.mjs +16 -0
  189. package/src/core/ssss-kernel-bridge.mjs +32 -4
  190. package/src/core/startup-health.mjs +6 -1
  191. package/src/core/surface.mjs +65 -54
  192. package/src/core/task-executors.mjs +54 -106
  193. package/src/core/validated-write.mjs +1 -0
  194. package/src/core/vector-field.mjs +134 -192
  195. package/src/server/invoke-core-child.mjs +24 -0
  196. package/src/server/invoke-core-operations.mjs +102 -0
  197. package/src/server/route-manifest.json +0 -56
  198. package/src/server/routes/brains.mjs +0 -18
  199. package/src/server/routes/context.mjs +0 -41
  200. package/src/server/routes/invoke.mjs +26 -1
  201. package/src/server/routes/ssss.mjs +0 -169
  202. package/src/server/routes/system.mjs +0 -15
  203. package/src/server/routes/tasks.mjs +0 -26
  204. package/src/server/routes/vault.mjs +2 -23
  205. package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
  206. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
  207. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
  208. package/src/core/append-log.mjs +0 -131
  209. package/src/core/plugin-tasks.mjs +0 -173
  210. package/templates/onboarding-interview.md +0 -82
  211. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
  212. /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
@@ -16,6 +16,7 @@ import { getTokenStatus, setTokenValue } from './src/token.mjs';
16
16
  import { lintWikiHubs } from './src/lint.mjs';
17
17
  import { generateWiki } from './src/generate.mjs';
18
18
  import { retrieveContext } from './src/context-provider.mjs';
19
+ import { refreshDiagnostics } from './src/refresh-diagnostics.mjs';
19
20
 
20
21
  /**
21
22
  * Regenerate the generated pages of every registered project that carries them.
@@ -25,7 +26,14 @@ import { retrieveContext } from './src/context-provider.mjs';
25
26
  * Defaults to a report; --apply performs the refreshes, because regenerating a
26
27
  * repository's documentation is a write to that repository.
27
28
  */
28
- async function refreshCommand(args) {
29
+ export async function refreshCommand(args, {
30
+ read = readProjects, dueFor = isDue, refresh = refreshProjectAsync,
31
+ getKey = resolveKey, configFor = loadOpenWikiConfig,
32
+ } = {}) {
33
+ if (args.includes('--help') || args.includes('-h')) {
34
+ console.log('Usage: total-recall openwiki refresh [--repo <name>] [--apply] [--force] [--diagnostics [--slot <local-minute>]] [--json]');
35
+ return;
36
+ }
29
37
  const json = args.includes('--json');
30
38
  const apply = args.includes('--apply');
31
39
  const force = args.includes('--force');
@@ -37,7 +45,7 @@ async function refreshCommand(args) {
37
45
 
38
46
  let projects;
39
47
  try {
40
- projects = readProjects();
48
+ projects = read();
41
49
  } catch (err) {
42
50
  if (json) console.log(JSON.stringify({ ok: false, error: err.message }));
43
51
  else console.error(`OpenWiki refresh: ${err.message}`);
@@ -52,20 +60,28 @@ async function refreshCommand(args) {
52
60
  return;
53
61
  }
54
62
 
63
+ if (args.includes('--diagnostics')) {
64
+ const slotIndex = args.indexOf('--slot');
65
+ const result = await refreshDiagnostics({ projects, slot: slotIndex >= 0 ? args[slotIndex + 1] : null });
66
+ console.log(JSON.stringify(result, null, 2));
67
+ if (!result.ok) process.exitCode = 1;
68
+ return;
69
+ }
70
+
55
71
  const reports = [];
56
72
  for (const project of projects) {
57
73
  let state;
58
74
  try {
59
- state = force ? { due: true, ...staleness(project.root) } : isDue(project);
75
+ state = dueFor(project, { force });
60
76
  } catch (err) {
61
- reports.push({ ...project, action: 'skipped', reason: err.message });
77
+ reports.push({ ...project, action: 'failed', reason: err.message });
62
78
  continue;
63
79
  }
64
80
  if (!state.due) {
65
81
  if (state.skipped) {
66
82
  reports.push({ ...project, action: 'skipped', reason: state.reason });
67
83
  } else {
68
- reports.push({ ...project, action: 'current', generatedAt: state.generatedAt, commitsSince: state.commitsSince });
84
+ reports.push({ ...project, action: 'current', reason: state.reason, generationCommit: state.generationCommit, generationTime: state.generationTime, generatedAt: state.generatedAt, commitsSince: state.commitsSince });
69
85
  }
70
86
  continue;
71
87
  }
@@ -73,7 +89,10 @@ async function refreshCommand(args) {
73
89
  ...project,
74
90
  action: 'due',
75
91
  uninitialized: Boolean(state.uninitialized),
92
+ mode: state.mode,
76
93
  generatedAt: state.generatedAt,
94
+ generationCommit: state.generationCommit,
95
+ generationTime: state.generationTime,
77
96
  commitsSince: state.commitsSince,
78
97
  reason: state.reason
79
98
  });
@@ -82,8 +101,8 @@ async function refreshCommand(args) {
82
101
  const due = reports.filter((r) => r.action === 'due');
83
102
  let payloadBudgetNote = null;
84
103
  if (apply && due.length) {
85
- const config = loadOpenWikiConfig();
86
- const { key, from } = resolveKey({ provider: config.provider });
104
+ const config = configFor();
105
+ const { key, from } = getKey({ provider: config.provider });
87
106
  if (!key) {
88
107
  const keyName = config.provider === 'google' ? 'GOOGLE_API_KEY' : 'OPENROUTER_API_KEY';
89
108
  const error = `No model credential for provider '${config.provider}'. Save via: total-recall auth secret set ${keyName} --stdin`;
@@ -109,20 +128,25 @@ async function refreshCommand(args) {
109
128
  while (queueIndex < due.length) {
110
129
  const itemIndex = queueIndex++;
111
130
  const entry = due[itemIndex];
112
- const mode = entry.uninitialized ? 'init' : 'update';
131
+ const mode = entry.mode || (entry.uninitialized ? 'init' : 'update');
113
132
  if (!json) {
114
133
  console.log(`[${itemIndex + 1}/${due.length}] ▶ ${entry.name.padEnd(28)} starting ${mode}...`);
115
134
  }
116
- const result = await refreshProjectAsync(
117
- { name: entry.name, root: entry.root },
118
- { key, model: config.model, provider: config.provider, timeoutMs: config.timeout_ms }
119
- );
135
+ let result;
136
+ try {
137
+ result = await refresh(
138
+ { name: entry.name, root: entry.root },
139
+ { key, model: config.model, provider: config.provider, timeoutMs: config.timeout_ms }
140
+ );
141
+ } catch (error) {
142
+ result = { action: 'failed', reason: error.message.split(key).join('[REDACTED]') };
143
+ }
120
144
  completedCount++;
121
145
  const index = reports.findIndex((r) => r.root === entry.root);
122
146
  reports[index] = { ...reports[index], ...result, credential: from };
123
147
  if (!json) {
124
- const duration = Math.round((Date.now() - (result.startedAt || Date.now())) / 1000);
125
- const icon = result.action === 'failed' ? '✖' : '✔';
148
+ const duration = Math.round((result.durationMs || 0) / 1000);
149
+ const icon = ['failed', 'partial'].includes(result.action) ? '✖' : '✔';
126
150
  console.log(`[${completedCount}/${due.length}] ${icon} ${entry.name.padEnd(28)} ${result.action} (${duration}s)${result.reason ? ` — ${result.reason}` : ''}`);
127
151
  }
128
152
  }
@@ -133,8 +157,9 @@ async function refreshCommand(args) {
133
157
  }
134
158
 
135
159
  const failed = reports.filter((r) => r.action === 'failed');
160
+ const partial = reports.filter((r) => r.action === 'partial');
136
161
  const payload = {
137
- ok: failed.length === 0,
162
+ ok: failed.length === 0 && partial.length === 0,
138
163
  applied: apply,
139
164
  retention: describeRetention(),
140
165
  note: payloadBudgetNote,
@@ -145,6 +170,8 @@ async function refreshCommand(args) {
145
170
  uninitialized: reports.filter((r) => r.uninitialized).length,
146
171
  refreshed: reports.filter((r) => r.action === 'refreshed' || r.action === 'initialized').length,
147
172
  failed: failed.length,
173
+ partial: partial.length,
174
+ skipped: reports.filter((r) => r.action === 'skipped').length,
148
175
  current: reports.filter((r) => r.action === 'current').length,
149
176
  },
150
177
  reports,
@@ -152,15 +179,17 @@ async function refreshCommand(args) {
152
179
  if (json) console.log(JSON.stringify(payload, null, 2));
153
180
  else {
154
181
  console.log(`OpenWiki refresh (${apply ? 'apply' : 'report'}): ${payload.counts.registered} registered, ${payload.counts.due} due (${payload.counts.uninitialized} uninitialized)`);
155
- for (const r of reports) {
182
+ // Put failures last so bounded scheduler tails retain their causes.
183
+ for (const r of [...reports].sort((a, b) => Number(['failed', 'partial'].includes(a.action)) - Number(['failed', 'partial'].includes(b.action)))) {
156
184
  if (r.action === 'current') continue;
157
- const age = r.uninitialized ? 'uninitialized' : (r.generatedAt ? `generated ${r.generatedAt.slice(0, 10)}` : 'never generated');
185
+ const age = r.action === 'skipped' ? 'outside repository generation' : r.generationTime ? `generated ${r.generationTime} at ${r.generationCommit}` : r.uninitialized ? 'uninitialized' : (r.generatedAt ? `last attempt ${r.generatedAt}; generation commit unknown` : 'never generated');
158
186
  console.log(` ${r.action.padEnd(9)} ${r.name.padEnd(28)} ${age}${Number.isFinite(r.commitsSince) && r.commitsSince !== Infinity ? `, ${r.commitsSince} commits since` : ''}${r.reason ? ` — ${r.reason}` : ''}`);
159
187
  }
160
188
  if (payloadBudgetNote) console.log(payloadBudgetNote);
161
189
  if (!apply && due.length) console.log('Re-run with --apply to regenerate. Pages are a projection; vault content is never touched.');
162
190
  }
163
- if (failed.length) process.exitCode = 1;
191
+ if (!payload.ok) process.exitCode = 1;
192
+ return payload;
164
193
  }
165
194
 
166
195
  async function probeCommand(args) {
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-openwiki",
9
- "version": "0.1.10",
9
+ "version": "0.1.11",
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.43.3"
15
+ "total-recall-brain": "3.44.0"
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.43.3",
5533
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.43.3.tgz",
5534
- "integrity": "sha512-yjfPN/BQxEEa+iodyFXQov/6fRt1pZg/7RcmgsbpALwBt6Ki3JJjT3tzArBC7NxHQmXgGYyKnCYId6xe/W2rPg==",
5532
+ "version": "3.44.0",
5533
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.44.0.tgz",
5534
+ "integrity": "sha512-VNPaIO9zsSwoxAsJ5ByIQBP9zPpLq4RiQO7Vrex4rBOHFQJT50P7utcZiLMSZTyZJU4C/m5uB8qTCSBMp6+l6Q==",
5535
5535
  "dev": true,
5536
5536
  "license": "MIT",
5537
5537
  "workspaces": [
@@ -5539,7 +5539,7 @@
5539
5539
  ],
5540
5540
  "dependencies": {
5541
5541
  "@simplewebauthn/server": "^13.3.2",
5542
- "@ssss/cli": "npm:@gregiteen/ssss-cli@0.14.4",
5542
+ "@ssss/cli": "npm:@gregiteen/ssss-cli@0.14.5",
5543
5543
  "bcrypt": "^6.0.0",
5544
5544
  "better-sqlite3": "^12.11.1",
5545
5545
  "cookie-parser": "^1.4.7",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-openwiki",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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.1.10",
5
+ "version": "0.1.11",
6
6
  "description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall.",
7
7
  "license": "MIT",
8
8
  "runtime": {
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { readProjects } from './refresh.mjs';
6
+
7
+ /** Read through Total Recall's event service. Never open vault files here. */
8
+ async function readTaskEvents(options) {
9
+ const service = process.env.TR_PACKAGE_ROOT
10
+ ? pathToFileURL(path.join(process.env.TR_PACKAGE_ROOT, 'src', 'core', 'ssss-operation-service.mjs')).href
11
+ : 'total-recall-brain/src/core/ssss-operation-service.mjs';
12
+ const { listVfsEvents } = await import(service);
13
+ return listVfsEvents(options);
14
+ }
15
+
16
+ export async function refreshDiagnostics({
17
+ projects = readProjects(),
18
+ vaultRoot = path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'memory-vault'),
19
+ listEvents = readTaskEvents,
20
+ slot = null,
21
+ } = {}) {
22
+ let taskEvents = [], eventError = null;
23
+ try {
24
+ taskEvents = (await listEvents({ vaultRoot })).flatMap(event => {
25
+ let payload = event.payload || event.content;
26
+ if (typeof payload === 'string') {
27
+ try { payload = JSON.parse(payload); } catch { return []; }
28
+ }
29
+ return payload?.kind === 'plugin.task_run' && payload.plugin_id === 'openwiki' && payload.command === 'auto-refresh'
30
+ ? [{ at: event.timestamp || event.created_at || null, ...payload }] : [];
31
+ });
32
+ } catch (error) { eventError = error.message; }
33
+ taskEvents.sort((a, b) => String(a.at).localeCompare(String(b.at)));
34
+ taskEvents = slot ? taskEvents.filter(event => event.slot === slot) : taskEvents.slice(-1);
35
+ const reports = projects.map(project => {
36
+ const logPath = path.join(project.root, 'openwiki', '.refresh-attempt.json');
37
+ try { return { ...project, logPath, attempt: JSON.parse(fs.readFileSync(logPath, 'utf8')) }; }
38
+ catch (error) { return { ...project, logPath, attempt: null, logError: error.code === 'ENOENT' ? 'No retained generator output' : error.message }; }
39
+ });
40
+ return {
41
+ ok: !eventError, eventError, taskEvents, reports,
42
+ note: 'Task events retain only an output tail. Full legacy generator output was not saved; it cannot be reconstructed from that tail.',
43
+ };
44
+ }
@@ -30,6 +30,19 @@ const PLUGIN_ROOT = fileURLToPath(new URL('..', import.meta.url));
30
30
  const RETENTION_NOTE = 'pages are a projection, not canonical memory';
31
31
  const DEFAULT_LOCK_MS = 6 * 60 * 60 * 1000;
32
32
 
33
+ const gitRun = (_command, args, options) => spawnSync('total-recall', ['git', ...args], options);
34
+
35
+ function readJson(file) {
36
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
37
+ }
38
+
39
+ /** An unfinished init must resume as init, even after it has written pages. */
40
+ export function generationMode(root) {
41
+ const state = readJson(path.join(root, 'openwiki', '.run.json'));
42
+ if (state?.mode === 'init' && state.phase && !['complete', 'idle'].includes(state.phase)) return 'init';
43
+ return hasWiki(root) ? 'update' : 'init';
44
+ }
45
+
33
46
  export function registryPath(home = os.homedir()) {
34
47
  return path.join(home, '.agent', 'skills', 'total-recall', 'config', 'project-registry.json');
35
48
  }
@@ -69,7 +82,7 @@ export function readProjects({ registry = registryPath() } = {}) {
69
82
  });
70
83
  }
71
84
 
72
- /** Does this project carry generated pages at all? A repo with no openwiki/ is skipped, never created. */
85
+ /** Does this project carry generated Markdown pages? */
73
86
  export function hasWiki(root) {
74
87
  const dir = path.join(root, 'openwiki');
75
88
  const stat = fs.lstatSync(dir, { throwIfNoEntry: false });
@@ -82,15 +95,18 @@ export function hasWiki(root) {
82
95
  * since the recorded generation, which is the same measure the per-repo driver
83
96
  * used. Uncommitted work is not counted, so an open edit does not read as drift.
84
97
  */
85
- export function isGitRepo(root, { git = spawnSync } = {}) {
98
+ export function isGitRepo(root, { git = gitRun } = {}) {
86
99
  if (fs.existsSync(path.join(root, '.git'))) return true;
87
100
  const res = git('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
88
- return !res.error && res.status === 0;
101
+ if (res.error || res.status !== 0 || String(res.stdout).trim() !== 'true') return false;
102
+ const top = git('git', ['-C', root, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
103
+ if (top.error || top.status !== 0) return false;
104
+ return fs.realpathSync(root) === fs.realpathSync(String(top.stdout).trim());
89
105
  }
90
106
 
91
- export function staleness(root, { git = spawnSync } = {}) {
107
+ export function staleness(root, { git = gitRun } = {}) {
92
108
  if (!isGitRepo(root, { git })) {
93
- return { root, head: '', generatedAt: null, generatedStatus: null, files: null, lastCodeChange: null, commitsSince: 0, stale: false };
109
+ return { root, head: '', generationCommit: null, generationTime: null, generatedAt: null, generatedStatus: null, files: null, lastCodeChange: null, commitsSince: 0, stale: false };
94
110
  }
95
111
  const marker = path.join(root, 'openwiki', '.last-update.json');
96
112
  let head = '';
@@ -104,6 +120,11 @@ export function staleness(root, { git = spawnSync } = {}) {
104
120
  generatedAt = typeof parsed.updatedAt === 'string' ? parsed.updatedAt : null;
105
121
  generatedStatus = typeof parsed.status === 'string' ? parsed.status : null;
106
122
  } catch { /* no marker: never generated, or unreadable */ }
123
+ const receipt = readJson(path.join(root, 'openwiki', '.generation.json'));
124
+ const validReceipt = typeof receipt?.commit === 'string' && /^[a-f0-9]{7,64}$/i.test(receipt.commit)
125
+ && typeof receipt.completedAt === 'string' && Number.isFinite(Date.parse(receipt.completedAt));
126
+ const generationCommit = validReceipt ? receipt.commit : null;
127
+ const generationTime = validReceipt ? receipt.completedAt : null;
107
128
 
108
129
  // The page contents, so a refresh can be judged on what it actually rewrote.
109
130
  // openwiki writes its marker when generation begins, so the marker alone
@@ -122,12 +143,15 @@ export function staleness(root, { git = spawnSync } = {}) {
122
143
  const lastCodeChange = !log.error && log.status === 0 ? String(log.stdout || '').trim() || null : null;
123
144
 
124
145
  let commitsSince = null;
125
- if (generatedAt) {
126
- const count = git('git', ['-C', root, 'rev-list', '--count', `--since=${generatedAt}`, 'HEAD', '--', '.', ':!openwiki'], { encoding: 'utf8' });
127
- if (!count.error && count.status === 0) commitsSince = Number(String(count.stdout || '').trim());
128
- }
129
- const stale = generatedAt === null || (Number.isFinite(commitsSince) && commitsSince > 0);
130
- return { root, head, generatedAt, generatedStatus, files, lastCodeChange, commitsSince, stale };
146
+ if (generationCommit || generatedAt) {
147
+ const range = generationCommit ? [`${generationCommit}..HEAD`] : [`--since=${generatedAt}`, 'HEAD'];
148
+ const count = git('git', ['-C', root, 'rev-list', '--count', ...range, '--', '.', ':!openwiki'], { encoding: 'utf8' });
149
+ const value = String(count.stdout || '').trim();
150
+ if (!count.error && count.status === 0 && /^\d+$/.test(value)) commitsSince = Number(value);
151
+ }
152
+ const incomplete = generatedStatus === 'interrupted' || stillGenerating(root);
153
+ const stale = incomplete || generatedAt === null || commitsSince === null || commitsSince > 0;
154
+ return { root, head, generationCommit, generationTime, generatedAt, generatedStatus, files, lastCodeChange, commitsSince, stale, mode: generationMode(root) };
131
155
  }
132
156
 
133
157
  /**
@@ -250,14 +274,81 @@ export function stillGenerating(root) {
250
274
  }
251
275
  }
252
276
 
277
+ function writeReceipt(root, name, value) {
278
+ const dir = path.join(root, 'openwiki');
279
+ fs.mkdirSync(dir, { recursive: true });
280
+ const target = path.join(dir, name);
281
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
282
+ try {
283
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
284
+ fs.renameSync(tmp, target);
285
+ } finally {
286
+ fs.rmSync(tmp, { force: true });
287
+ }
288
+ }
289
+
290
+ function redact(value, key) {
291
+ let text = String(value || '');
292
+ if (key) text = text.split(key).join('[REDACTED]');
293
+ return text.replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]').replace(/\bsk-[\w-]+/g, '[REDACTED]');
294
+ }
295
+
296
+ function finishRefresh(project, { before, result, git, key, mode, startedAt, via, clock }) {
297
+ const after = staleness(project.root, { git });
298
+ const exitCode = result.error ? (result.error.code || 1) : result.signal ? 1 : result.status;
299
+ const completedAt = new Date(clock()).toISOString();
300
+ const stdout = redact(result.stdout, key), stderr = redact(result.stderr, key);
301
+ const error = redact(result.error?.message, key);
302
+ const lines = `${stderr}\n${stdout}`.replace(/\x1b\[[0-9;]*m/g, '').split('\n').map(line => line.trim()).filter(Boolean);
303
+ const diagnostic = lines.find(line => /context_length_exceeded|Provider returned error|exited without|Could not restore|interrupted OpenWiki|fetch failed/.test(line));
304
+ const reason = error || (result.signal ? `terminated by ${result.signal}` : diagnostic || lines.filter(line => !/^(\[ERR\]\s*)?Error Diagnostics$/.test(line)).at(-1) || `exit ${exitCode}`);
305
+ const wrote = pagesWritten(before, after) || (!before.files && hasWiki(project.root) ? 1 : 0);
306
+ const advanced = after.generatedAt !== before.generatedAt;
307
+ let action, failureReason;
308
+ if (after.generatedStatus === 'interrupted' || stillGenerating(project.root)) {
309
+ action = 'partial';
310
+ failureReason = `${wrote} page(s) rewritten, but the run did not finish; it resumes on the next refresh (${reason})`;
311
+ } else if (exitCode !== 0) {
312
+ action = 'failed'; failureReason = reason;
313
+ } else if (!advanced && !wrote) {
314
+ action = 'failed'; failureReason = 'command exited 0 and no page changed';
315
+ } else if (!before.head) {
316
+ action = 'failed'; failureReason = 'Cannot record generation without a source commit';
317
+ } else {
318
+ action = mode === 'init' ? 'initialized' : 'refreshed';
319
+ }
320
+ const logPath = path.join(project.root, 'openwiki', '.refresh-attempt.json');
321
+ const report = { ...project, ...after, action, reason: failureReason, exitCode, signal: result.signal || null, from: before.generatedAt, to: after.generatedAt, durationMs: clock() - startedAt, via, mode, logPath };
322
+ try {
323
+ writeReceipt(project.root, '.refresh-attempt.json', { startedAt: new Date(startedAt).toISOString(), completedAt, sourceCommit: before.head || null, action, mode, exitCode, signal: result.signal || null, error, stdout, stderr });
324
+ if (action === 'initialized' || action === 'refreshed') {
325
+ writeReceipt(project.root, '.generation.json', { commit: before.head, completedAt });
326
+ report.generationCommit = before.head;
327
+ report.generationTime = completedAt;
328
+ report.commitsSince = after.head === before.head ? 0 : null;
329
+ report.stale = after.head !== before.head;
330
+ } else if (before.generationCommit && before.generationTime) {
331
+ // Init can replace openwiki/ before it fails. Retain the last success.
332
+ writeReceipt(project.root, '.generation.json', { commit: before.generationCommit, completedAt: before.generationTime });
333
+ report.generationCommit = before.generationCommit;
334
+ report.generationTime = before.generationTime;
335
+ }
336
+ } catch (error) {
337
+ report.action = 'failed'; report.reason = `Could not save refresh receipt: ${error.message}`;
338
+ }
339
+ return report;
340
+ }
341
+
253
342
  export function refreshProject(project, {
254
- key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, run = spawnSync, now = Date.now(), targetVersion = null, pluginRoot = PLUGIN_ROOT,
343
+ key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, run = spawnSync, now = Date.now(), clock = Date.now, targetVersion = null, pluginRoot = PLUGIN_ROOT,
255
344
  } = {}) {
256
- if (!isGitRepo(project.root, { git: run })) {
345
+ const git = run === spawnSync ? gitRun : run;
346
+ if (!isGitRepo(project.root, { git })) {
257
347
  return { ...project, action: 'skipped', reason: 'not a git repository' };
258
348
  }
259
- const before = staleness(project.root, { git: run });
349
+ const before = staleness(project.root, { git });
260
350
  const uninitialized = !hasWiki(project.root);
351
+ const mode = generationMode(project.root);
261
352
 
262
353
  let command = binary, args = binaryArgs, via = binary ? 'explicit' : null;
263
354
  if (!command) {
@@ -273,19 +364,12 @@ export function refreshProject(project, {
273
364
  const selectedModel = model || config.model;
274
365
  const selectedProvider = provider || config.provider;
275
366
 
276
- const modeFlag = uninitialized ? '--init' : '--update';
367
+ const modeFlag = `--${mode}`;
277
368
  const cmdArgs = [...args, modeFlag, '-p'];
278
369
  if (selectedModel) {
279
370
  cmdArgs.push('--modelId', selectedModel);
280
371
  }
281
372
 
282
- if (uninitialized) {
283
- const runFile = path.join(project.root, 'openwiki', '.run.json');
284
- if (fs.existsSync(runFile)) {
285
- try { fs.unlinkSync(runFile); } catch {}
286
- }
287
- }
288
-
289
373
  const childEnv = { ...process.env };
290
374
  if (selectedProvider === 'google') {
291
375
  childEnv.GOOGLE_API_KEY = key;
@@ -312,47 +396,18 @@ export function refreshProject(project, {
312
396
  timeout: timeoutMs,
313
397
  });
314
398
 
315
- const after = staleness(project.root, { git: run });
316
- const exitCode = result.error ? (result.error.code || 1) : result.status;
317
- const advanced = after.generatedAt !== before.generatedAt || (uninitialized && hasWiki(project.root));
318
- const wrote = pagesWritten(before, after) || (uninitialized && hasWiki(project.root) ? 1 : 0);
319
-
320
- const outcome = after.generatedStatus === 'interrupted' || stillGenerating(project.root)
321
- ? 'partial'
322
- : (advanced ? (uninitialized ? 'initialized' : 'refreshed') : 'unchanged');
323
-
324
- if (exitCode !== 0 && !wrote) {
325
- return { ...project, action: 'failed', reason: result.error?.message || `exit ${exitCode}`, exitCode, ...after };
326
- }
327
- if (!advanced && !wrote) {
328
- return { ...project, action: 'failed', reason: `command exited ${exitCode} and no page changed`, exitCode, ...after };
329
- }
330
- if (outcome === 'partial') {
331
- return {
332
- ...project,
333
- action: 'partial',
334
- reason: `${wrote} page(s) rewritten, but the run did not finish (the generator was cut off); it resumes on the next refresh`,
335
- exitCode,
336
- from: before.generatedAt,
337
- to: after.generatedAt,
338
- via,
339
- ...after,
340
- };
341
- }
342
- if (exitCode !== 0) {
343
- return { ...project, action: 'failed', reason: result.error?.message || `exit ${exitCode}`, exitCode, ...after };
344
- }
345
- return { ...project, action: uninitialized ? 'initialized' : 'refreshed', from: before.generatedAt, to: after.generatedAt, durationMs: Date.now() - startedAt, via, ...after };
399
+ return finishRefresh(project, { before, result, git, key, mode, startedAt, via, clock });
346
400
  }
347
401
 
348
402
  export async function refreshProjectAsync(project, {
349
- key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, now = Date.now(), targetVersion = null, pluginRoot = PLUGIN_ROOT,
403
+ key, model = null, provider = null, binary = null, binaryArgs = [], timeoutMs = 30 * 60 * 1000, now = Date.now(), clock = Date.now, targetVersion = null, pluginRoot = PLUGIN_ROOT, git = gitRun, spawnProcess = spawn,
350
404
  } = {}) {
351
- if (!isGitRepo(project.root, { git: spawnSync })) {
405
+ if (!isGitRepo(project.root, { git })) {
352
406
  return { ...project, action: 'skipped', reason: 'not a git repository' };
353
407
  }
354
- const before = staleness(project.root, { git: spawnSync });
408
+ const before = staleness(project.root, { git });
355
409
  const uninitialized = !hasWiki(project.root);
410
+ const mode = generationMode(project.root);
356
411
  let command = binary, args = binaryArgs, via = binary ? 'explicit' : null;
357
412
  if (!command) {
358
413
  const resolved = resolveGenerator({ run: spawnSync, targetVersion, pluginRoot, requireBundled: uninitialized });
@@ -367,19 +422,12 @@ export async function refreshProjectAsync(project, {
367
422
  const selectedModel = model || config.model;
368
423
  const selectedProvider = provider || config.provider;
369
424
 
370
- const modeFlag = uninitialized ? '--init' : '--update';
425
+ const modeFlag = `--${mode}`;
371
426
  const cmdArgs = [...args, modeFlag, '-p'];
372
427
  if (selectedModel) {
373
428
  cmdArgs.push('--modelId', selectedModel);
374
429
  }
375
430
 
376
- if (uninitialized) {
377
- const runFile = path.join(project.root, 'openwiki', '.run.json');
378
- if (fs.existsSync(runFile)) {
379
- try { fs.unlinkSync(runFile); } catch {}
380
- }
381
- }
382
-
383
431
  const childEnv = { ...process.env };
384
432
  if (selectedProvider === 'google') {
385
433
  childEnv.GOOGLE_API_KEY = key;
@@ -399,74 +447,33 @@ export async function refreshProjectAsync(project, {
399
447
 
400
448
  const startedAt = now;
401
449
  const result = await new Promise((resolve) => {
402
- const proc = spawn(command, cmdArgs, {
450
+ const proc = spawnProcess(command, cmdArgs, {
403
451
  cwd: project.root,
404
452
  env: childEnv,
405
453
  stdio: ['ignore', 'pipe', 'pipe'],
406
454
  timeout: timeoutMs,
407
455
  });
408
- let stdout = '', stderr = '';
456
+ let stdout = '', stderr = '', error = null;
409
457
  proc.stdout?.on('data', (d) => { stdout += d; });
410
458
  proc.stderr?.on('data', (d) => { stderr += d; });
411
- proc.on('error', (err) => resolve({ error: err, status: 1, stdout, stderr }));
412
- proc.on('close', (status) => resolve({ status: status ?? 0, stdout, stderr }));
459
+ proc.on('error', (err) => { error = err; });
460
+ proc.on('close', (status, signal) => resolve({ error, status, signal, stdout, stderr }));
413
461
  });
414
-
415
- const after = staleness(project.root, { git: spawnSync });
416
- const exitCode = result.error ? (result.error.code || 1) : result.status;
417
- const advanced = after.generatedAt !== before.generatedAt || (uninitialized && hasWiki(project.root));
418
- const wrote = pagesWritten(before, after) || (uninitialized && hasWiki(project.root) ? 1 : 0);
419
-
420
- const outcome = after.generatedStatus === 'interrupted' || stillGenerating(project.root)
421
- ? 'partial'
422
- : (advanced ? (uninitialized ? 'initialized' : 'refreshed') : 'unchanged');
423
-
424
- const extractReason = (rawErr, rawOut) => {
425
- if (result.error?.message) return result.error.message;
426
- const combined = `${rawErr || ''}\n${rawOut || ''}`.trim();
427
- if (!combined) return `exit ${exitCode}`;
428
- const lines = combined.split('\n').map((l) => l.trim()).filter(Boolean);
429
- const diag = lines.find((l) => l.includes('Error Diagnostics') || l.includes('exited without') || l.includes('Could not restore') || l.includes('Provider returned error') || l.includes('fetch failed'));
430
- if (diag) return diag.replace(/^\[ERR\]\s*/, '');
431
- const last = lines[lines.length - 1];
432
- return last ? last.replace(/^\[ERR\]\s*/, '') : `exit ${exitCode}`;
433
- };
434
- const failReason = extractReason(result.stderr, result.stdout);
435
-
436
- if (exitCode !== 0 && !wrote) {
437
- return { ...project, action: 'failed', reason: failReason, exitCode, ...after };
438
- }
439
- if (!advanced && !wrote) {
440
- return { ...project, action: 'failed', reason: failReason || `command exited ${exitCode} and no page changed`, exitCode, ...after };
441
- }
442
- if (outcome === 'partial') {
443
- return {
444
- ...project,
445
- action: 'partial',
446
- reason: `${wrote} page(s) rewritten, but the run did not finish (the generator was cut off); it resumes on the next refresh`,
447
- exitCode,
448
- from: before.generatedAt,
449
- to: after.generatedAt,
450
- via,
451
- ...after,
452
- };
453
- }
454
- if (exitCode !== 0) {
455
- return { ...project, action: 'failed', reason: failReason, exitCode, ...after };
456
- }
457
- return { ...project, action: uninitialized ? 'initialized' : 'refreshed', from: before.generatedAt, to: after.generatedAt, durationMs: Date.now() - startedAt, via, ...after };
462
+ return finishRefresh(project, { before, result, git, key, mode, startedAt, via, clock });
458
463
  }
459
464
 
460
465
  /** Is a refresh due for this project under the configured interval? */
461
- export function isDue(project, { lockMs = DEFAULT_LOCK_MS, now = Date.now(), git = spawnSync } = {}) {
466
+ export function isDue(project, { lockMs = DEFAULT_LOCK_MS, now = Date.now(), git = gitRun, force = false } = {}) {
462
467
  if (!isGitRepo(project.root, { git })) {
463
468
  return { due: false, skipped: true, reason: 'not a git repository' };
464
469
  }
465
470
  const state = staleness(project.root, { git });
471
+ if (force) return { due: true, uninitialized: !hasWiki(project.root), ...state };
466
472
  if (!hasWiki(project.root)) {
467
473
  return { due: true, uninitialized: true, reason: 'uninitialized (missing openwiki/ pages)', ...state };
468
474
  }
469
475
  if (!state.stale) return { due: false, ...state };
476
+ if (state.generatedStatus === 'interrupted' || stillGenerating(project.root)) return { due: true, reason: 'resume incomplete generation', ...state };
470
477
  if (state.generatedAt) {
471
478
  const at = Date.parse(state.generatedAt);
472
479
  if (Number.isFinite(at) && now - at < lockMs) return { due: false, reason: 'refreshed recently', ...state };