@chrysb/alphaclaw 0.4.6-beta.7 → 0.4.6-beta.8

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.
@@ -1,12 +1,16 @@
1
1
  export const getDoctorPriorityTone = (priority = "") => {
2
- const normalized = String(priority || "").trim().toUpperCase();
2
+ const normalized = String(priority || "")
3
+ .trim()
4
+ .toUpperCase();
3
5
  if (normalized === "P0") return "danger";
4
6
  if (normalized === "P1") return "warning";
5
7
  return "neutral";
6
8
  };
7
9
 
8
10
  export const getDoctorStatusTone = (status = "") => {
9
- const normalized = String(status || "").trim().toLowerCase();
11
+ const normalized = String(status || "")
12
+ .trim()
13
+ .toLowerCase();
10
14
  if (normalized === "fixed") return "success";
11
15
  if (normalized === "dismissed") return "neutral";
12
16
  return "warning";
@@ -35,7 +39,9 @@ export const formatDoctorCategory = (category = "") => {
35
39
  export const buildDoctorPriorityCounts = (cards = []) =>
36
40
  cards.reduce(
37
41
  (totals, card) => {
38
- const priority = String(card?.priority || "").trim().toUpperCase();
42
+ const priority = String(card?.priority || "")
43
+ .trim()
44
+ .toUpperCase();
39
45
  if (priority === "P0" || priority === "P1" || priority === "P2") {
40
46
  totals[priority] += 1;
41
47
  }
@@ -47,7 +53,9 @@ export const buildDoctorPriorityCounts = (cards = []) =>
47
53
  export const groupDoctorCardsByStatus = (cards = []) =>
48
54
  cards.reduce(
49
55
  (groups, card) => {
50
- const status = String(card?.status || "open").trim().toLowerCase();
56
+ const status = String(card?.status || "open")
57
+ .trim()
58
+ .toLowerCase();
51
59
  if (status === "fixed") {
52
60
  groups.fixed.push(card);
53
61
  return groups;
@@ -74,13 +82,71 @@ export const shouldShowDoctorWarning = (
74
82
 
75
83
  export const getDoctorWarningMessage = (doctorStatus = null) => {
76
84
  if (!doctorStatus) return "";
77
- const changedFilesCount = Number(doctorStatus.changeSummary?.changedFilesCount || 0);
85
+ const changedFilesCount = Number(
86
+ doctorStatus.changeSummary?.changedFilesCount || 0,
87
+ );
78
88
  if (changedFilesCount > 0) {
79
89
  return `Drift Doctor has not been run in the last week and ${changedFilesCount} file${changedFilesCount === 1 ? "" : "s"} changed since the last review.`;
80
90
  }
81
91
  return "Doctor has not been run in the last week.";
82
92
  };
83
93
 
94
+ export const formatDoctorCharCount = (value = 0) =>
95
+ `${Number(value || 0).toLocaleString()} chars`;
96
+
97
+ const isManagedBootstrapContextPath = (filePath = "") =>
98
+ String(filePath || "").startsWith("hooks/bootstrap/");
99
+
100
+ export const getDoctorBootstrapTruncationItems = (doctorStatus = null) => {
101
+ const bootstrapContext = doctorStatus?.bootstrapContext;
102
+ const truncatedFiles = (bootstrapContext?.activeTruncatedFiles || []).filter(
103
+ (file) => !isManagedBootstrapContextPath(file?.path),
104
+ );
105
+ const nearLimitFiles = (bootstrapContext?.activeNearLimitFiles || []).filter(
106
+ (file) => !isManagedBootstrapContextPath(file?.path),
107
+ );
108
+ return [
109
+ ...truncatedFiles.map((file) => ({
110
+ path: file.path,
111
+ size: formatDoctorCharCount(file.rawChars),
112
+ statusText: `-${Number(
113
+ Math.max(
114
+ 0,
115
+ Number(file.rawChars || 0) - Number(file.injectedChars || 0),
116
+ ),
117
+ ).toLocaleString()} cut`,
118
+ statusTone: "danger",
119
+ })),
120
+ ...nearLimitFiles.map((file) => ({
121
+ path: file.path,
122
+ size: formatDoctorCharCount(file.rawChars),
123
+ statusText: "Near limit",
124
+ statusTone: "warning",
125
+ })),
126
+ ];
127
+ };
128
+
129
+ export const hasDoctorBootstrapWarnings = (doctorStatus = null) =>
130
+ getDoctorBootstrapTruncationItems(doctorStatus).length > 0;
131
+
132
+ export const getDoctorBootstrapWarningTitle = (doctorStatus = null) => {
133
+ const items = getDoctorBootstrapTruncationItems(doctorStatus);
134
+ if (!items.length) return "";
135
+ const hasTruncatedItems = items.some((item) => item.statusTone === "danger");
136
+ const hasNearLimitItems = items.some((item) => item.statusTone === "warning");
137
+ if (hasTruncatedItems && hasNearLimitItems) {
138
+ return "Some of your main files are being truncated or nearing the limit:";
139
+ }
140
+ if (hasNearLimitItems) {
141
+ return items.length === 1
142
+ ? "One of your main files is nearing the limit:"
143
+ : "Some of your main files are nearing the limit:";
144
+ }
145
+ return items.length === 1
146
+ ? "One of your main files is being truncated:"
147
+ : "Some of your main files are being truncated:";
148
+ };
149
+
84
150
  export const getDoctorChangeLabel = (changeSummary = null) => {
85
151
  const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
86
152
  if (changedFilesCount === 0) return "No changes since last run";
@@ -20,8 +20,11 @@ import { DoctorFixCardModal } from "./fix-card-modal.js";
20
20
  import {
21
21
  buildDoctorRunMarkers,
22
22
  buildDoctorStatusFilterOptions,
23
+ getDoctorBootstrapTruncationItems,
24
+ getDoctorBootstrapWarningTitle,
23
25
  getDoctorChangeLabel,
24
26
  getDoctorRunPillDetail,
27
+ hasDoctorBootstrapWarnings,
25
28
  shouldShowDoctorWarning,
26
29
  } from "./helpers.js";
27
30
 
@@ -182,6 +185,18 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
182
185
  () => shouldShowDoctorWarning(doctorStatus, 0),
183
186
  [doctorStatus],
184
187
  );
188
+ const showBootstrapTruncationBanner = useMemo(
189
+ () => hasDoctorBootstrapWarnings(doctorStatus),
190
+ [doctorStatus],
191
+ );
192
+ const bootstrapTruncationMessage = useMemo(
193
+ () => getDoctorBootstrapWarningTitle(doctorStatus),
194
+ [doctorStatus],
195
+ );
196
+ const bootstrapTruncationItems = useMemo(
197
+ () => getDoctorBootstrapTruncationItems(doctorStatus),
198
+ [doctorStatus],
199
+ );
185
200
  const hasCompletedDoctorRun = !!doctorStatus?.lastRunAt;
186
201
  const hasRuns = runs.length > 0;
187
202
  const hasLoadedRuns = runsPoll.data !== null || runsPoll.error !== null;
@@ -312,29 +327,76 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
312
327
  : null}
313
328
  ${!showInitialLoadingState && hasRuns
314
329
  ? html`
315
- <${DoctorSummaryCards} cards=${openCards} />
316
- <div class="space-y-2">
317
- ${hasCompletedDoctorRun
318
- ? html`
319
- <div
320
- class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
321
- >
322
- <span class="text-xs text-gray-500">
323
- Last run ·${" "}
324
- <span class="text-gray-300">
325
- ${formatLocaleDateTime(doctorStatus?.lastRunAt, {
326
- fallback: "Never",
327
- })}
330
+ <div class="space-y-3">
331
+ <${DoctorSummaryCards} cards=${openCards} />
332
+ <div class="space-y-3">
333
+ ${hasCompletedDoctorRun
334
+ ? html`
335
+ <div
336
+ class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
337
+ >
338
+ <span class="text-xs text-gray-500">
339
+ Last run ·${" "}
340
+ <span class="text-gray-300">
341
+ ${formatLocaleDateTime(doctorStatus?.lastRunAt, {
342
+ fallback: "Never",
343
+ })}
344
+ </span>
328
345
  </span>
329
- </span>
330
- <span class="text-xs text-gray-500">
331
- ${changeLabel}
332
- </span>
333
- </div>
334
- `
335
- : null}
336
- ${
337
- showDoctorStaleBanner
346
+ <span class="text-xs text-gray-500">
347
+ ${changeLabel}
348
+ </span>
349
+ </div>
350
+ ${showBootstrapTruncationBanner
351
+ ? html`
352
+ <div
353
+ class="bg-surface border border-border rounded-xl p-4 space-y-3"
354
+ >
355
+ <div class="text-xs text-gray-400">
356
+ ⚠️ ${bootstrapTruncationMessage}
357
+ </div>
358
+ <div class="space-y-2">
359
+ ${bootstrapTruncationItems.map(
360
+ (item) => html`
361
+ <div
362
+ class="flex items-center justify-between gap-3 text-xs"
363
+ >
364
+ <button
365
+ type="button"
366
+ class="font-mono text-gray-200 ac-tip-link hover:underline text-left cursor-pointer"
367
+ onClick=${() => onOpenFile(String(item.path || ""))}
368
+ >
369
+ ${item.path}
370
+ </button>
371
+ <span
372
+ class="flex items-center gap-3 whitespace-nowrap"
373
+ >
374
+ <span class="text-gray-500">
375
+ ${item.size}
376
+ </span>
377
+ <span
378
+ class=${item.statusTone === "warning"
379
+ ? "text-yellow-300"
380
+ : "text-red-300"}
381
+ >
382
+ ${item.statusText}
383
+ </span>
384
+ </span>
385
+ </div>
386
+ `,
387
+ )}
388
+ </div>
389
+ <div class="border-t border-border"></div>
390
+ <p class="text-xs text-gray-500 leading-5">
391
+ Truncated files become partially hidden from
392
+ your agent and could cause drift.
393
+ </p>
394
+ </div>
395
+ `
396
+ : null}
397
+ `
398
+ : null}
399
+ ${showDoctorStaleBanner
338
400
  ? html`
339
401
  <div
340
402
  class="text-xs text-yellow-300 bg-yellow-500/10 border border-yellow-500/35 rounded-lg px-3 py-2"
@@ -344,8 +406,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
344
406
  changed.
345
407
  </div>
346
408
  `
347
- : null
348
- }
409
+ : null}
410
+ </div>
349
411
  </div>
350
412
  `
351
413
  : null}
@@ -461,9 +523,7 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
461
523
  ${selectedRunIsInProgress
462
524
  ? html`
463
525
  <div class="ac-surface-inset rounded-xl p-4">
464
- <div
465
- class="text-xs leading-5 text-gray-400"
466
- >
526
+ <div class="text-xs leading-5 text-gray-400">
467
527
  <span
468
528
  >Run in progress. Findings will appear when analysis
469
529
  completes.</span
@@ -479,7 +539,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
479
539
  onAskAgentFix=${setFixCard}
480
540
  onUpdateStatus=${handleUpdateStatus}
481
541
  onOpenFile=${onOpenFile}
482
- changedPaths=${doctorStatus?.changeSummary?.changedPaths || []}
542
+ changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
543
+ []}
483
544
  showRunMeta=${selectedRunFilter === "all"}
484
545
  hideEmptyState=${selectedRunIsInProgress}
485
546
  />
@@ -0,0 +1,191 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const kDoctorBootstrapMaxChars = 20000;
5
+ const kDoctorBootstrapTotalMaxChars = 150000;
6
+ const kDoctorBootstrapNearLimitRatio = 0.9;
7
+ const kDoctorContextTruncationGuidance =
8
+ "OpenClaw trims oversized injected files by keeping the first 70%, keeping the last 20%, and cutting the middle 10% without a warning.";
9
+
10
+ const kDoctorRootContextFiles = [
11
+ { path: "AGENTS.md", injectMode: "always" },
12
+ { path: "SOUL.md", injectMode: "always" },
13
+ { path: "TOOLS.md", injectMode: "always" },
14
+ { path: "IDENTITY.md", injectMode: "always" },
15
+ { path: "USER.md", injectMode: "always" },
16
+ { path: "HEARTBEAT.md", injectMode: "always" },
17
+ { path: "BOOTSTRAP.md", injectMode: "first_run_only" },
18
+ ];
19
+
20
+ const kDoctorBootstrapExtraFiles = [
21
+ { path: "hooks/bootstrap/AGENTS.md", injectMode: "always" },
22
+ { path: "hooks/bootstrap/TOOLS.md", injectMode: "always" },
23
+ ];
24
+
25
+ const kDoctorBootstrapContextFiles = [...kDoctorRootContextFiles, ...kDoctorBootstrapExtraFiles];
26
+
27
+ const readWorkspaceFileChars = (workspaceRoot, relativePath) => {
28
+ const fullPath = path.join(workspaceRoot, relativePath);
29
+ try {
30
+ const content = fs.readFileSync(fullPath, "utf8");
31
+ return {
32
+ exists: true,
33
+ chars: content.length,
34
+ };
35
+ } catch {
36
+ return {
37
+ exists: false,
38
+ chars: 0,
39
+ };
40
+ }
41
+ };
42
+
43
+ const analyzeBootstrapContext = ({
44
+ workspaceRoot = "",
45
+ bootstrapMaxChars = kDoctorBootstrapMaxChars,
46
+ bootstrapTotalMaxChars = kDoctorBootstrapTotalMaxChars,
47
+ } = {}) => {
48
+ const files = kDoctorBootstrapContextFiles.map((spec) => {
49
+ const fileState = readWorkspaceFileChars(workspaceRoot, spec.path);
50
+ const rawChars = fileState.chars;
51
+ const fileLimitChars = Math.min(rawChars, bootstrapMaxChars);
52
+ const nearFileLimit = rawChars > 0 && rawChars >= Math.floor(bootstrapMaxChars * kDoctorBootstrapNearLimitRatio);
53
+ return {
54
+ ...spec,
55
+ exists: fileState.exists,
56
+ rawChars,
57
+ fileLimitChars,
58
+ injectedChars: 0,
59
+ truncatedByFileLimit: rawChars > bootstrapMaxChars,
60
+ truncatedByTotalLimit: false,
61
+ truncated: rawChars > bootstrapMaxChars,
62
+ nearFileLimit: nearFileLimit && rawChars <= bootstrapMaxChars,
63
+ active: spec.injectMode === "always",
64
+ reason: rawChars > bootstrapMaxChars ? "file_limit" : "",
65
+ };
66
+ });
67
+
68
+ let injectedTotalChars = 0;
69
+ for (const file of files) {
70
+ if (!file.active || !file.exists) continue;
71
+ const remainingChars = Math.max(0, bootstrapTotalMaxChars - injectedTotalChars);
72
+ file.injectedChars = Math.min(file.fileLimitChars, remainingChars);
73
+ file.truncatedByTotalLimit = file.fileLimitChars > file.injectedChars;
74
+ file.truncated = file.truncatedByFileLimit || file.truncatedByTotalLimit;
75
+ if (file.truncatedByFileLimit && file.truncatedByTotalLimit) {
76
+ file.reason = "file_and_total_limit";
77
+ } else if (file.truncatedByFileLimit) {
78
+ file.reason = "file_limit";
79
+ } else if (file.truncatedByTotalLimit) {
80
+ file.reason = "total_limit";
81
+ }
82
+ injectedTotalChars += file.injectedChars;
83
+ }
84
+
85
+ const activeFiles = files.filter((file) => file.active && file.exists);
86
+ const activeTruncatedFiles = activeFiles.filter((file) => file.truncated);
87
+ const activeNearLimitFiles = activeFiles.filter((file) => file.nearFileLimit && !file.truncated);
88
+ const inactiveTruncatedFiles = files.filter((file) => !file.active && file.exists && file.truncated);
89
+ const hasTotalLimitTruncation = activeTruncatedFiles.some(
90
+ (file) => file.reason === "total_limit" || file.reason === "file_and_total_limit",
91
+ );
92
+
93
+ return {
94
+ bootstrapMaxChars,
95
+ bootstrapTotalMaxChars,
96
+ truncationGuidance: kDoctorContextTruncationGuidance,
97
+ files,
98
+ activeFiles,
99
+ activeRawChars: activeFiles.reduce((sum, file) => sum + file.rawChars, 0),
100
+ activeInjectedChars: activeFiles.reduce((sum, file) => sum + file.injectedChars, 0),
101
+ hasActiveTruncation: activeTruncatedFiles.length > 0,
102
+ hasActiveNearLimitFiles: activeNearLimitFiles.length > 0,
103
+ hasActiveWarnings: activeTruncatedFiles.length > 0 || activeNearLimitFiles.length > 0,
104
+ hasAnyTruncation: activeTruncatedFiles.length > 0 || inactiveTruncatedFiles.length > 0,
105
+ activeTruncatedFiles,
106
+ activeNearLimitFiles,
107
+ inactiveTruncatedFiles,
108
+ hasTotalLimitTruncation,
109
+ totalLimitReached: injectedTotalChars >= bootstrapTotalMaxChars,
110
+ };
111
+ };
112
+
113
+ const formatChars = (value = 0) => `${Number(value || 0).toLocaleString()} chars`;
114
+
115
+ const buildBootstrapTruncationCards = (bootstrapContext = null) => {
116
+ if (!bootstrapContext?.hasActiveTruncation) return [];
117
+
118
+ const cards = bootstrapContext.activeTruncatedFiles
119
+ .filter((file) => file.reason === "file_limit")
120
+ .map((file) => ({
121
+ priority: "P0",
122
+ category: "project context",
123
+ title: `${file.path} is being truncated in Project Context`,
124
+ summary:
125
+ `${file.path} is ${formatChars(file.rawChars)}, above the per-file Project Context limit ` +
126
+ `of ${formatChars(bootstrapContext.bootstrapMaxChars)}. The agent is not seeing the full file.`,
127
+ recommendation:
128
+ `Move the most important rules to the top of ${file.path}, shorten or split low-priority content, ` +
129
+ `and increase OpenClaw's bootstrap limits if this file legitimately needs more room. ` +
130
+ kDoctorContextTruncationGuidance,
131
+ evidence: [
132
+ { type: "path", path: file.path },
133
+ {
134
+ type: "text",
135
+ text:
136
+ `Raw size: ${formatChars(file.rawChars)}. ` +
137
+ `Per-file limit: ${formatChars(bootstrapContext.bootstrapMaxChars)}.`,
138
+ },
139
+ ],
140
+ targetPaths: [{ path: file.path }],
141
+ fixPrompt:
142
+ `Reorganize ${file.path} so the most important instructions appear at the top and reduce unnecessary length. ` +
143
+ `Do not change unrelated behavior.`,
144
+ status: "open",
145
+ }));
146
+
147
+ const totalLimitedFiles = bootstrapContext.activeTruncatedFiles.filter(
148
+ (file) => file.reason === "total_limit" || file.reason === "file_and_total_limit",
149
+ );
150
+ if (totalLimitedFiles.length > 0) {
151
+ cards.unshift({
152
+ priority: "P0",
153
+ category: "project context",
154
+ title: "Project Context total bootstrap limit is truncating injected files",
155
+ summary:
156
+ `Injected workspace guidance needs ${formatChars(bootstrapContext.activeRawChars)} raw across active ` +
157
+ `Project Context files, exceeding the total bootstrap budget of ` +
158
+ `${formatChars(bootstrapContext.bootstrapTotalMaxChars)}.`,
159
+ recommendation:
160
+ `Reduce total Project Context size across injected guidance files, keep critical instructions near the top, ` +
161
+ `and raise OpenClaw's total bootstrap budget if the workspace legitimately needs more injected guidance. ` +
162
+ kDoctorContextTruncationGuidance,
163
+ evidence: totalLimitedFiles.map((file) => ({
164
+ type: "text",
165
+ text:
166
+ `${file.path}: raw ${formatChars(file.rawChars)}, injected ${formatChars(file.injectedChars)} ` +
167
+ `before the total limit stopped more content from being included.`,
168
+ })),
169
+ targetPaths: totalLimitedFiles.map((file) => ({ path: file.path })),
170
+ fixPrompt:
171
+ `Reduce the combined size of the affected Project Context files and keep the most important instructions near the top. ` +
172
+ `Only edit the files listed in the finding.`,
173
+ status: "open",
174
+ });
175
+ }
176
+
177
+ return cards;
178
+ };
179
+
180
+ module.exports = {
181
+ analyzeBootstrapContext,
182
+ buildBootstrapTruncationCards,
183
+ formatChars,
184
+ kDoctorBootstrapContextFiles,
185
+ kDoctorBootstrapExtraFiles,
186
+ kDoctorBootstrapMaxChars,
187
+ kDoctorBootstrapNearLimitRatio,
188
+ kDoctorBootstrapTotalMaxChars,
189
+ kDoctorContextTruncationGuidance,
190
+ kDoctorRootContextFiles,
191
+ };
@@ -1,6 +1,17 @@
1
+ const {
2
+ kDoctorBootstrapExtraFiles,
3
+ kDoctorBootstrapMaxChars,
4
+ kDoctorBootstrapTotalMaxChars,
5
+ kDoctorContextTruncationGuidance,
6
+ kDoctorRootContextFiles,
7
+ } = require("./bootstrap-context");
8
+
1
9
  const renderList = (items = []) =>
2
10
  items.length ? items.map((item) => `- ${item}`).join("\n") : "- (none)";
3
11
 
12
+ const renderContextFileList = (files = []) =>
13
+ files.map((file) => `\`${file.path}\``).join(", ");
14
+
4
15
  const renderResolvedCards = (cards = []) => {
5
16
  if (!cards.length) return "";
6
17
  const lines = cards.map(
@@ -37,10 +48,15 @@ Important:
37
48
  - Return ONLY valid JSON. No markdown fences. No extra prose.
38
49
 
39
50
  OpenClaw context injection:
40
- - OpenClaw automatically injects a fixed set of named workspace files into the agent's context window ("Project Context") on every turn. The exact set is: \`AGENTS.md\`, \`SOUL.md\`, \`TOOLS.md\`, \`IDENTITY.md\`, \`USER.md\`, \`HEARTBEAT.md\`, and \`BOOTSTRAP.md\` (first-run only).
41
- - Only these specific files are auto-injected. Other \`.md\` files at the workspace root (e.g. INTERESTS.md, MEMORY.md, README.md) are NOT injected and must be explicitly read by the agent.
42
- - Do not flag auto-injected files as orphaned or unreferenced — they are loaded by the runtime, not by explicit file references in AGENTS.md.
43
- - Additionally, AlphaClaw injects bootstrap files from \`hooks/bootstrap/\` (e.g. AGENTS.md, TOOLS.md) as extra context on every turn.
51
+ - OpenClaw automatically injects these workspace files into the agent's Project Context: ${renderContextFileList(
52
+ kDoctorRootContextFiles,
53
+ )}.
54
+ - \`BOOTSTRAP.md\` is first-run only; the others above are injected on normal turns when present.
55
+ - Additionally, AlphaClaw injects these extra bootstrap files on normal turns when present: ${renderContextFileList(
56
+ kDoctorBootstrapExtraFiles,
57
+ )}.
58
+ - Large injected files are truncated per-file at ${kDoctorBootstrapMaxChars} chars by default, and total bootstrap injection across files is capped at ${kDoctorBootstrapTotalMaxChars} chars by default.
59
+ - ${kDoctorContextTruncationGuidance}
44
60
 
45
61
  OpenClaw default context:
46
62
  - \`AGENTS.md\` is the workspace home file in the default OpenClaw template. It may intentionally include first-run instructions, session-startup guidance, memory conventions, safety rules, tool pointers, and optional behavioral guidance.
@@ -1,5 +1,9 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const {
4
+ analyzeBootstrapContext,
5
+ buildBootstrapTruncationCards,
6
+ } = require("./bootstrap-context");
3
7
  const { buildDoctorPrompt } = require("./prompt");
4
8
  const { normalizeDoctorResult } = require("./normalize");
5
9
  const { calculateWorkspaceDelta, computeWorkspaceSnapshot } = require("./workspace-fingerprint");
@@ -137,6 +141,7 @@ const createDoctorService = ({
137
141
  };
138
142
 
139
143
  const buildStatus = () => {
144
+ const bootstrapContext = analyzeBootstrapContext({ workspaceRoot });
140
145
  const recentRuns = listDoctorRuns({ limit: 10 });
141
146
  const latestRun = recentRuns[0] || null;
142
147
  const latestCompletedRun =
@@ -179,6 +184,7 @@ const createDoctorService = ({
179
184
  lastRunAgeMs,
180
185
  needsInitialRun: !latestCompletedRun,
181
186
  stale,
187
+ bootstrapContext,
182
188
  changeSummary: {
183
189
  ...delta,
184
190
  hasBaseline: hasManifestBaseline,
@@ -245,10 +251,14 @@ const createDoctorService = ({
245
251
  console.error(`[doctor] run ${runId} stderr end`);
246
252
  throw error;
247
253
  }
248
- captureEvidenceSnippets(normalizedResult.cards, workspaceRoot);
254
+ const bootstrapTruncationCards = buildBootstrapTruncationCards(
255
+ analyzeBootstrapContext({ workspaceRoot }),
256
+ );
257
+ const cards = [...bootstrapTruncationCards, ...normalizedResult.cards];
258
+ captureEvidenceSnippets(cards, workspaceRoot);
249
259
  insertDoctorCards({
250
260
  runId,
251
- cards: normalizedResult.cards,
261
+ cards,
252
262
  });
253
263
  completeDoctorRun({
254
264
  id: runId,
@@ -342,7 +352,11 @@ const createDoctorService = ({
342
352
  throw new Error("Doctor import requires raw output");
343
353
  }
344
354
  const normalizedResult = normalizeDoctorResult(normalizedRawOutput);
345
- captureEvidenceSnippets(normalizedResult.cards, workspaceRoot);
355
+ const bootstrapTruncationCards = buildBootstrapTruncationCards(
356
+ analyzeBootstrapContext({ workspaceRoot }),
357
+ );
358
+ const cards = [...bootstrapTruncationCards, ...normalizedResult.cards];
359
+ captureEvidenceSnippets(cards, workspaceRoot);
346
360
  const workspaceSnapshot = getCurrentWorkspaceSnapshot();
347
361
  const runId = createDoctorRun({
348
362
  status: kDoctorRunStatus.completed,
@@ -354,7 +368,7 @@ const createDoctorService = ({
354
368
  });
355
369
  insertDoctorCards({
356
370
  runId,
357
- cards: normalizedResult.cards,
371
+ cards,
358
372
  });
359
373
  completeDoctorRun({
360
374
  id: runId,
package/lib/server.js CHANGED
@@ -131,7 +131,7 @@ const app = express();
131
131
  app.set("trust proxy", kTrustProxyHops);
132
132
  app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
133
133
  app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
134
- app.use(express.json());
134
+ app.use(express.json({ limit: "5mb" }));
135
135
 
136
136
  const proxy = httpProxy.createProxyServer({
137
137
  target: GATEWAY_URL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.7",
3
+ "version": "0.4.6-beta.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },