@bubblebrain-ai/bubble 0.0.46 → 0.0.47

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 (92) hide show
  1. package/dist/agent/child-runner.js +11 -0
  2. package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
  3. package/dist/agent/internal-reminder-sanitizer.js +13 -0
  4. package/dist/agent/subagent/router.d.ts +65 -0
  5. package/dist/agent/subagent/router.js +198 -0
  6. package/dist/agent/subagent/runtime.d.ts +203 -0
  7. package/dist/agent/subagent/runtime.js +789 -0
  8. package/dist/agent/subagent-control.d.ts +5 -0
  9. package/dist/agent/subagent-store.d.ts +18 -2
  10. package/dist/agent/subagent-store.js +32 -1
  11. package/dist/agent/tool-intent.d.ts +10 -0
  12. package/dist/agent/tool-intent.js +18 -0
  13. package/dist/agent/tool-visibility.d.ts +10 -0
  14. package/dist/agent/tool-visibility.js +6 -0
  15. package/dist/agent/workflow/runs.d.ts +72 -0
  16. package/dist/agent/workflow/runs.js +157 -0
  17. package/dist/agent.d.ts +78 -96
  18. package/dist/agent.js +266 -1072
  19. package/dist/context/compact-llm.js +14 -5
  20. package/dist/context/compact.d.ts +9 -0
  21. package/dist/context/compact.js +36 -10
  22. package/dist/context/compaction-files.d.ts +44 -0
  23. package/dist/context/compaction-files.js +148 -0
  24. package/dist/context/llm-compactor.js +24 -5
  25. package/dist/feishu/agent-host/run-driver.js +3 -0
  26. package/dist/feishu/serve.js +13 -9
  27. package/dist/goal/tools.js +12 -3
  28. package/dist/main.js +24 -19
  29. package/dist/model-catalog.js +36 -2
  30. package/dist/model-config.d.ts +2 -0
  31. package/dist/model-data.generated.d.ts +16 -0
  32. package/dist/model-data.generated.js +275 -0
  33. package/dist/model-pricing.d.ts +11 -7
  34. package/dist/model-pricing.js +0 -0
  35. package/dist/oauth/grok.d.ts +1 -1
  36. package/dist/oauth/grok.js +5 -2
  37. package/dist/oauth/storage.d.ts +9 -2
  38. package/dist/oauth/storage.js +16 -9
  39. package/dist/orchestrator/default-hooks.d.ts +20 -0
  40. package/dist/orchestrator/default-hooks.js +47 -295
  41. package/dist/orchestrator/hooks.d.ts +6 -39
  42. package/dist/permission/mode.js +1 -1
  43. package/dist/prompt/reminders.d.ts +9 -35
  44. package/dist/prompt/reminders.js +30 -136
  45. package/dist/prompt/task-reminders.d.ts +0 -31
  46. package/dist/prompt/task-reminders.js +0 -87
  47. package/dist/provider-ai-sdk.d.ts +2 -0
  48. package/dist/provider-ai-sdk.js +1 -0
  49. package/dist/provider-anthropic.d.ts +2 -0
  50. package/dist/provider-anthropic.js +3 -1
  51. package/dist/provider-ark-responses.d.ts +2 -0
  52. package/dist/provider-ark-responses.js +1 -0
  53. package/dist/provider-registry.d.ts +13 -0
  54. package/dist/provider-registry.js +83 -0
  55. package/dist/provider-transform.js +37 -1
  56. package/dist/provider.d.ts +7 -0
  57. package/dist/provider.js +13 -1
  58. package/dist/sdk/index.d.ts +152 -0
  59. package/dist/sdk/index.js +400 -0
  60. package/dist/session.js +9 -1
  61. package/dist/slash-commands/commands.js +33 -19
  62. package/dist/tools/exit-plan-mode.js +8 -0
  63. package/dist/tui/detect-theme.d.ts +1 -0
  64. package/dist/tui/detect-theme.js +11 -5
  65. package/dist/tui-ink/app.d.ts +2 -6
  66. package/dist/tui-ink/app.js +11 -18
  67. package/dist/tui-ink/display-history.d.ts +0 -2
  68. package/dist/tui-ink/display-history.js +0 -52
  69. package/dist/tui-ink/footer.js +2 -2
  70. package/dist/tui-ink/message-list.js +62 -23
  71. package/dist/tui-ink/run.js +10 -5
  72. package/dist/tui-ink/sync-output.d.ts +17 -0
  73. package/dist/tui-ink/sync-output.js +35 -0
  74. package/dist/tui-ink/theme.d.ts +0 -1
  75. package/dist/tui-ink/theme.js +0 -2
  76. package/dist/tui-ink/trace-groups.js +1 -1
  77. package/dist/types.d.ts +21 -1
  78. package/package.json +16 -2
  79. package/dist/agent/discovery-barrier.d.ts +0 -21
  80. package/dist/agent/discovery-barrier.js +0 -173
  81. package/dist/agent/evidence-tracker.d.ts +0 -15
  82. package/dist/agent/evidence-tracker.js +0 -93
  83. package/dist/agent/execution-governor.d.ts +0 -36
  84. package/dist/agent/execution-governor.js +0 -218
  85. package/dist/agent/task-classifier.d.ts +0 -3
  86. package/dist/agent/task-classifier.js +0 -114
  87. package/dist/agent/task-size.d.ts +0 -9
  88. package/dist/agent/task-size.js +0 -33
  89. package/dist/agent/tool-arbiter.d.ts +0 -7
  90. package/dist/agent/tool-arbiter.js +0 -33
  91. package/dist/orchestrator/workflow.d.ts +0 -10
  92. package/dist/orchestrator/workflow.js +0 -22
@@ -1,14 +1,21 @@
1
1
  /**
2
- * OAuth credential storage in ~/.bubble/auth.json
2
+ * OAuth credential storage in <bubble home>/auth.json.
3
+ *
4
+ * The path is resolved at CONSTRUCTION time through getBubbleHome(), never at
5
+ * module load: a module-level constant froze the real ~/.bubble at import,
6
+ * which no test could isolate (known-defects #7) and which ignored
7
+ * BUBBLE_HOME / dev mode while every other config file honored them
8
+ * (config.json, sessions, memory all derive from getBubbleHome).
3
9
  */
4
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
11
  import { dirname, join } from "node:path";
6
- import { homedir } from "node:os";
7
- const AUTH_PATH = join(homedir(), ".bubble", "auth.json");
12
+ import { getBubbleHome } from "../bubble-home.js";
8
13
  export class AuthStorage {
9
14
  data = {};
10
15
  mutationListeners = [];
11
- constructor() {
16
+ authPath;
17
+ constructor(authPath = join(getBubbleHome(), "auth.json")) {
18
+ this.authPath = authPath;
12
19
  this.load();
13
20
  }
14
21
  /**
@@ -34,10 +41,10 @@ export class AuthStorage {
34
41
  }
35
42
  }
36
43
  load() {
37
- if (!existsSync(AUTH_PATH))
44
+ if (!existsSync(this.authPath))
38
45
  return;
39
46
  try {
40
- const raw = readFileSync(AUTH_PATH, "utf-8");
47
+ const raw = readFileSync(this.authPath, "utf-8");
41
48
  const parsed = JSON.parse(raw);
42
49
  this.data = parsed;
43
50
  }
@@ -46,13 +53,13 @@ export class AuthStorage {
46
53
  }
47
54
  }
48
55
  save() {
49
- const dir = dirname(AUTH_PATH);
56
+ const dir = dirname(this.authPath);
50
57
  if (!existsSync(dir))
51
58
  mkdirSync(dir, { recursive: true });
52
- writeFileSync(AUTH_PATH, JSON.stringify(this.data, null, 2) + "\n", { mode: 0o600 });
59
+ writeFileSync(this.authPath, JSON.stringify(this.data, null, 2) + "\n", { mode: 0o600 });
53
60
  }
54
61
  getPath() {
55
- return AUTH_PATH;
62
+ return this.authPath;
56
63
  }
57
64
  get(providerId) {
58
65
  return this.data[providerId];
@@ -1,2 +1,22 @@
1
1
  import type { TurnHooks } from "./hooks.js";
2
+ /**
3
+ * Default hooks: infrastructure and information only.
4
+ *
5
+ * The cognitive-governance layer that used to live here (task classifier,
6
+ * step-budget governor, discovery barrier, evidence tracker, bash-search
7
+ * arbitration, delegation nudge, completion self-check) was removed — see
8
+ * docs/harness-thinning.md. The TurnHooks mechanism itself stays, pi-style:
9
+ * hosts may install their own policies, the core ships none.
10
+ *
11
+ * What remains, and why it passes the ruler in that document:
12
+ * - resident-history compaction calls (infrastructure)
13
+ * - background-task and subagent-lifecycle reminders (facts the model
14
+ * cannot otherwise see)
15
+ * - orchestrationRequestReminder (deliberately retained policy: amplifies
16
+ * the user's EXPLICIT orchestration request at the decision turn)
17
+ * - userNamedModelReminder (deterministic catalog resolution of a model the
18
+ * user named — information the model would otherwise retype from priors)
19
+ * - modified-existing-tests disclosure (git ground truth: bash writes and
20
+ * subagent worktree merges do not appear in the model's own tool memory)
21
+ */
2
22
  export declare function createDefaultHooks(): TurnHooks[];
@@ -1,51 +1,43 @@
1
- import { classifyTask } from "../agent/task-classifier.js";
2
- import { classifyTaskSize } from "../agent/task-size.js";
3
- import { EvidenceTracker } from "../agent/evidence-tracker.js";
4
- import { ExecutionGovernor } from "../agent/execution-governor.js";
5
- import { DiscoveryBarrier } from "../agent/discovery-barrier.js";
6
- import { arbitrateToolCall } from "../agent/tool-arbiter.js";
7
- import { buildCompletionSelfCheckReminder, buildEditRetryEscalationReminder, buildSmallTaskHint, buildWorkflowPhaseReminder, } from "../prompt/reminders.js";
8
- import { largeImplementationTaskReminder, orchestrationRequestReminder, reminderForTaskType, userNamedModelReminder } from "../prompt/task-reminders.js";
1
+ import { buildModifiedTestsDisclosure } from "../prompt/reminders.js";
2
+ import { orchestrationRequestReminder, userNamedModelReminder } from "../prompt/task-reminders.js";
9
3
  import { captureGitBaseline, detectRunChanges } from "../agent/change-tracker.js";
10
- import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
11
- import { resolve as resolvePath } from "node:path";
12
- import { traceEvent } from "../debug-trace.js";
13
- import { builtinAgentProfiles, discoverAgentProfiles, hasWriteWorktreeProfile } from "../agent/profiles.js";
14
4
  import { buildSubagentLifecycleReminder } from "../agent/subagent-lifecycle-reminder.js";
5
+ /**
6
+ * Default hooks: infrastructure and information only.
7
+ *
8
+ * The cognitive-governance layer that used to live here (task classifier,
9
+ * step-budget governor, discovery barrier, evidence tracker, bash-search
10
+ * arbitration, delegation nudge, completion self-check) was removed — see
11
+ * docs/harness-thinning.md. The TurnHooks mechanism itself stays, pi-style:
12
+ * hosts may install their own policies, the core ships none.
13
+ *
14
+ * What remains, and why it passes the ruler in that document:
15
+ * - resident-history compaction calls (infrastructure)
16
+ * - background-task and subagent-lifecycle reminders (facts the model
17
+ * cannot otherwise see)
18
+ * - orchestrationRequestReminder (deliberately retained policy: amplifies
19
+ * the user's EXPLICIT orchestration request at the decision turn)
20
+ * - userNamedModelReminder (deterministic catalog resolution of a model the
21
+ * user named — information the model would otherwise retype from priors)
22
+ * - modified-existing-tests disclosure (git ground truth: bash writes and
23
+ * subagent worktree merges do not appear in the model's own tool memory)
24
+ */
15
25
  export function createDefaultHooks() {
16
26
  return [
17
27
  {
18
28
  async beforeTurn(ctx) {
19
- // Git ground truth for the completion gate: tool-metadata bookkeeping
20
- // misses bash-written files and subagent worktree merges; a dirty-state
21
- // baseline at run start makes the run's real footprint observable.
29
+ // Git ground truth for the tests-touched disclosure: tool-metadata
30
+ // bookkeeping misses bash-written files and subagent worktree merges;
31
+ // a dirty-state baseline at run start makes the run's real footprint
32
+ // observable.
22
33
  if (ctx.state.gitBaseline === undefined) {
23
34
  ctx.state.gitBaseline = await captureGitBaseline(ctx.cwd);
24
35
  }
25
- const taskType = classifyTask(ctx.input);
26
- ctx.state.taskType = taskType;
27
- ctx.state.governor = new ExecutionGovernor(taskType);
28
- ctx.state.discoveryBarrier = new DiscoveryBarrier({
29
- cwd: ctx.cwd,
30
- input: ctx.input,
31
- enabled: taskType === "repo_orientation",
32
- });
33
- const taskReminder = reminderForTaskType(taskType, {
34
- // Only parent agents carry the delegation tools; the nudge must
35
- // never reach a child that cannot spawn anything.
36
- canDelegate: ctx.agent.hasToolAvailable("spawn_agent"),
37
- });
38
- if (taskReminder) {
39
- ctx.queueReminder(taskReminder);
40
- }
41
36
  // User named the mechanism ("agent team" / workflow / 编排): remind at
42
37
  // the decision turn, where it outweighs the parallel-spawns prior.
43
38
  const orchestrationReminder = orchestrationRequestReminder(ctx.input, ctx.agent.hasToolAvailable("run_workflow"));
44
39
  if (orchestrationReminder) {
45
40
  ctx.queueReminder(orchestrationReminder);
46
- // The large-change checkpoint must not re-offer spawn_agent in a
47
- // turn where this reminder just forbade it (design §3).
48
- ctx.state.orchestrationReminderSent = true;
49
41
  }
50
42
  // User named a configured model ("gpt 5.6 sol"): resolve it against
51
43
  // the routable catalog and hand over the exact id at the decision
@@ -54,18 +46,6 @@ export function createDefaultHooks() {
54
46
  if (namedModelReminder) {
55
47
  ctx.queueReminder(namedModelReminder);
56
48
  }
57
- // Small-task hint: counterweight to the default protocol's exploration
58
- // bias, only fires once per run on focused one-shot requests like
59
- // "写个 HTML 介绍元旦". Don't issue for the same input twice.
60
- if (!ctx.state.smallTaskHintSent && classifyTaskSize(ctx.input) === "small") {
61
- ctx.state.smallTaskHintSent = true;
62
- ctx.queueReminder(buildSmallTaskHint());
63
- }
64
- if (taskType === "security_investigation") {
65
- ctx.state.evidenceTracker = new EvidenceTracker();
66
- ctx.state.workflowPhase = "investigate";
67
- ctx.state.workflowKey = "";
68
- }
69
49
  // Background-task truth at turn start: completions that landed while
70
50
  // the agent was idle (or during another session's turn) reach the
71
51
  // model here; the same state-change gate prevents duplicates with the
@@ -74,31 +54,10 @@ export function createDefaultHooks() {
74
54
  if (backgroundTaskReminder) {
75
55
  ctx.queueReminder(backgroundTaskReminder);
76
56
  }
77
- for (const reminder of ctx.state.governor.consumePendingReminders()) {
78
- ctx.queueReminder(reminder);
79
- }
80
57
  ctx.agent.compactResidentHistory();
81
58
  },
82
59
  beforeModelCall(ctx) {
83
60
  ctx.agent.compactResidentHistory();
84
- if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker && ctx.state.governor) {
85
- const coverage = ctx.state.evidenceTracker.snapshot();
86
- const phase = resolveWorkflowPhase({
87
- coreCoverageComplete: ctx.state.evidenceTracker.isCoreCoverageComplete(),
88
- searchFrozen: false,
89
- });
90
- ctx.state.workflowPhase = phase;
91
- const summary = formatCoverageSummary(coverage);
92
- const key = `${phase}:${ctx.state.evidenceTracker.key()}:0`;
93
- if (ctx.state.workflowKey !== key) {
94
- ctx.state.workflowKey = key;
95
- ctx.queueReminder(buildWorkflowPhaseReminder({
96
- phase,
97
- covered: summary.covered,
98
- pending: summary.pending,
99
- }));
100
- }
101
- }
102
61
  },
103
62
  afterTurn(ctx) {
104
63
  ctx.agent.compactResidentHistory();
@@ -107,98 +66,6 @@ export function createDefaultHooks() {
107
66
  {
108
67
  beforeToolCall(ctx) {
109
68
  ctx.state.toolUsed = true;
110
- const arbitration = arbitrateToolCall(ctx.toolCall);
111
- const toolCall = { ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) };
112
- ctx.replaceToolCall(toolCall);
113
- ctx.state.governor?.beforeToolCall(toolCall);
114
- // Large-change checkpoint (large-task-delegation design §1): the
115
- // first mutation of the turn is the decision point — exploration is
116
- // over, nothing has been edited yet.
117
- if (isMutationTool(toolCall.name)) {
118
- maybeQueueLargeTaskNudge(ctx);
119
- }
120
- const blockedResult = ctx.state.discoveryBarrier?.beforeToolCall(toolCall);
121
- if (blockedResult)
122
- ctx.blockToolCall(blockedResult);
123
- },
124
- afterToolCall(ctx) {
125
- if (ctx.toolCall.arbiterNote) {
126
- ctx.replaceResult({
127
- ...ctx.result,
128
- metadata: {
129
- ...ctx.result.metadata,
130
- arbiterNote: ctx.toolCall.arbiterNote,
131
- },
132
- });
133
- }
134
- ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
135
- ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
136
- ctx.state.discoveryBarrier?.afterToolCall(ctx.toolCall, ctx.result);
137
- // Edit/write retry-escalation: models can spiral on "identical content"
138
- // or "not found" errors. Nudge them to re-ground or switch strategy.
139
- if (isMutationTool(ctx.toolCall.name) && ctx.result.isError) {
140
- if (ctx.toolCall.name === "edit" && ctx.result.status === "no_match" && ctx.result.metadata?.kind === "edit") {
141
- const path = typeof ctx.result.metadata.path === "string" ? ctx.result.metadata.path : "";
142
- const reminded = ctx.state.editNoMatchReminderPaths ?? (ctx.state.editNoMatchReminderPaths = []);
143
- if (path && !reminded.includes(path)) {
144
- reminded.push(path);
145
- const summary = ctx.result.content.split("\n")[0] || "";
146
- ctx.queueReminder(buildEditRetryEscalationReminder(`Edit oldText did not match ${path}. ${summary}`));
147
- }
148
- }
149
- const hash = hashEditCall(ctx.toolCall);
150
- const history = ctx.state.recentEditFailures ?? (ctx.state.recentEditFailures = []);
151
- history.push(hash);
152
- // Keep last 4 entries.
153
- if (history.length > 4)
154
- history.shift();
155
- const len = history.length;
156
- if (len >= 2 && history[len - 1] === history[len - 2] && !ctx.state.editRetryReminderSent) {
157
- ctx.state.editRetryReminderSent = true;
158
- const summary = ctx.result.content.split("\n")[0] || "";
159
- ctx.queueReminder(buildEditRetryEscalationReminder(`Last failure: ${ctx.toolCall.name} on the same target with identical arguments. ${summary}`));
160
- }
161
- }
162
- else if (isMutationTool(ctx.toolCall.name) && !ctx.result.isError) {
163
- // Successful mutation resets the dedup state so a later, unrelated
164
- // failure won't fire the reminder spuriously.
165
- ctx.state.recentEditFailures = [];
166
- ctx.state.editNoMatchReminderPaths = [];
167
- ctx.state.editRetryReminderSent = false;
168
- }
169
- // Redundant-Read detection moved into the read tool itself: it now
170
- // returns a FILE_UNCHANGED_STUB (or auto-advances to the next page)
171
- // when the same args land on an unchanged file. Hook-level reminder
172
- // is removed to avoid duplicate signals and to let the structural
173
- // dedup do the work.
174
- if (isCodeWriteResult(ctx.toolCall, ctx.result)) {
175
- markCodeChanged(ctx.state);
176
- ctx.state.appliedEditCount = (ctx.state.appliedEditCount ?? 0) + 1;
177
- }
178
- // Large-task breadth evidence (design §1): only files actually READ.
179
- // Search results contribute nothing — one glob returns up to 100
180
- // match paths and would saturate any threshold instantly (review
181
- // blocker). Bash-parsed reads (cat/head/sed -n) carry relative
182
- // paths, so resolve against cwd before dedup.
183
- if (!ctx.result.isError && ctx.result.metadata?.kind === "read") {
184
- const path = typeof ctx.result.metadata.path === "string" ? ctx.result.metadata.path : undefined;
185
- if (path) {
186
- (ctx.state.exploredFiles ??= new Set()).add(resolvePath(ctx.cwd, path));
187
- }
188
- }
189
- // Removed: active verification tracking. The previous design nagged the
190
- // model every turn until it ran a recognised verification command, and
191
- // narrowly accepted only test/lint commands — which meant ad-hoc python
192
- // checks did not count, the nag never cleared, and reasoning models
193
- // (DeepSeek v4-pro with hex-blindness) spiraled trying to "prove" the
194
- // edit was correct. CC's approach is the opposite: verify when there
195
- // is something real to verify, say so explicitly when there isn't, and
196
- // trust the model to judge. We follow that.
197
- if (ctx.state.governor) {
198
- for (const reminder of ctx.state.governor.consumePendingReminders()) {
199
- ctx.queueReminder(reminder);
200
- }
201
- }
202
69
  },
203
70
  beforeContinuation(ctx) {
204
71
  if (hasSubagentLifecycleActivity(ctx.toolCalls, ctx.toolResults)) {
@@ -215,131 +82,39 @@ export function createDefaultHooks() {
215
82
  if (taskReminder) {
216
83
  ctx.queueReminder(taskReminder);
217
84
  }
218
- // Routing detector note (model-routing design §6): rides the same
219
- // channel as the lifecycle reminder, once per session. Consuming also
220
- // closes the per-fan-out counting window.
221
- const routingReminder = ctx.agent.consumePendingRoutingReminder?.();
222
- if (routingReminder) {
223
- ctx.queueReminder(routingReminder);
224
- }
225
- if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
226
- ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
227
- return;
228
- }
229
- // Verification reminders intentionally removed. See afterToolCall.
230
85
  },
231
86
  async afterTurn(ctx) {
232
- // Verification force-continuation removed. The model decides whether
233
- // verification is meaningful for the task, per the system prompt.
234
- //
235
- // Completion self-check gate: when a run that changed code is about
236
- // to end (this hook only runs on final, no-tool-call turns), remind
237
- // the model ONCE to re-read the original request before finishing.
238
- // Unlike the removed verification gate it never repeats — the model
239
- // is explicitly told a confirming final answer ends the run — so the
240
- // "prove it" spiral that killed the old design cannot start.
241
- if (ctx.state.completionGateFired)
87
+ // Modified-existing-tests disclosure: pure information, delivered at
88
+ // most once, only when this run touched test files that already
89
+ // existed at run start. Forcing one continuation turn is the only way
90
+ // a final-turn fact can reach the model at all. The old completion
91
+ // self-check ("re-read the request...") was removed with the
92
+ // governance layer; this survives it because the underlying fact
93
+ // comes from git, not from the model's own tool history.
94
+ if (ctx.state.testsTouchedDisclosed)
242
95
  return;
243
- // Tools are frozen: the model could not act on anything the check
244
- // surfaces, so another turn is pure token burn.
96
+ // Tools are frozen: the model could not act on the disclosure, so
97
+ // another turn is pure token burn.
245
98
  if (ctx.state.forceTextOnlyReason)
246
99
  return;
247
- // Subagents hand off to a parent that does its own closing pass, and
248
- // worktree profiles explicitly forbid the very actions a completion
249
- // check would suggest. The parent's gate still covers their merged
250
- // changes via the git baseline below.
100
+ // Subagents hand off to a parent whose own disclosure covers their
101
+ // merged changes via the git baseline.
251
102
  if (ctx.agent.role === "subagent")
252
103
  return;
253
- // Change detection: tool metadata first (cheap), then git ground
254
- // truth — which also catches bash-written files and subagent
255
- // worktree merges. Gate only runs git when the run actually used
256
- // tools, so external repo churn cannot hijack a pure Q&A turn.
257
- let changes = null;
258
- if (ctx.state.toolUsed && ctx.state.gitBaseline) {
259
- changes = await detectRunChanges(ctx.cwd, ctx.state.gitBaseline);
260
- }
261
- const changedSomething = !!ctx.state.codeChanged || (changes !== null && changes.changedFiles.length > 0);
262
- if (!changedSomething)
104
+ // Only attribute git changes to runs that actually used tools, so
105
+ // external repo churn cannot hijack a pure Q&A turn.
106
+ if (!ctx.state.toolUsed || !ctx.state.gitBaseline)
263
107
  return;
264
- ctx.state.completionGateFired = true;
265
- ctx.state.forceContinuationReason = "completion_self_check";
266
- ctx.queueReminder(buildCompletionSelfCheckReminder({
267
- modifiedExistingTests: changes?.modifiedExistingTests,
268
- }));
269
- traceEvent("completion_gate_fired", {
270
- turnCount: ctx.state.turnCount,
271
- changedFiles: changes?.changedFiles.length,
272
- modifiedExistingTests: changes?.modifiedExistingTests.length,
273
- });
108
+ const changes = await detectRunChanges(ctx.cwd, ctx.state.gitBaseline);
109
+ if (!changes || changes.modifiedExistingTests.length === 0)
110
+ return;
111
+ ctx.state.testsTouchedDisclosed = true;
112
+ ctx.state.forceContinuationReason = "tests_touched_disclosure";
113
+ ctx.queueReminder(buildModifiedTestsDisclosure(changes.modifiedExistingTests));
274
114
  },
275
115
  },
276
116
  ];
277
117
  }
278
- function markCodeChanged(state) {
279
- state.codeChanged = true;
280
- }
281
- // Large-task delegation checkpoint (large-task-delegation design §1-3).
282
- const BREADTH_FILES = 8;
283
- const BREADTH_FILES_WITH_PLAN = 5;
284
- const BREADTH_TODOS = 6;
285
- function maybeQueueLargeTaskNudge(ctx) {
286
- // Evaluated exactly once per turn, at the first mutation — fired or not
287
- // (mid-implementation re-detection is out of scope by design §4).
288
- if (ctx.state.largeTaskCheckpointDone)
289
- return;
290
- ctx.state.largeTaskCheckpointDone = true;
291
- const agent = ctx.agent;
292
- const taskType = ctx.state.taskType;
293
- const exploredFiles = ctx.state.exploredFiles?.size ?? 0;
294
- const pendingTodos = agent.getTodos?.()
295
- ?.filter((todo) => todo.status === "pending" || todo.status === "in_progress").length ?? 0;
296
- const threshold = pendingTodos >= BREADTH_TODOS ? BREADTH_FILES_WITH_PLAN : BREADTH_FILES;
297
- // Gate order: cheap checks first; the profile discovery (filesystem) runs
298
- // only when everything else already passed.
299
- const suppressedBy = agent.largeTaskNudgeConsumed ? "session_latch"
300
- : (taskType === "code_review" || taskType === "security_investigation") ? "task_type"
301
- : !agent.hasToolAvailable("spawn_agent") ? "no_spawn_tool"
302
- : (agent.activeSubAgentCount?.() ?? 0) > 0 ? "active_children"
303
- : agent.hasRunningWorkflow?.() ? "running_workflow"
304
- : exploredFiles < threshold ? "below_threshold"
305
- : !writeWorktreeSurfaceAvailable(ctx.cwd) ? "no_write_profile"
306
- : undefined;
307
- traceEvent("delegation_detector", {
308
- fired: suppressedBy === undefined,
309
- exploredFiles,
310
- pendingTodos,
311
- threshold,
312
- taskType: taskType ?? "unknown",
313
- ...(suppressedBy ? { suppressedBy } : {}),
314
- });
315
- if (suppressedBy !== undefined)
316
- return;
317
- agent.largeTaskNudgeConsumed = true;
318
- ctx.queueReminder(largeImplementationTaskReminder({
319
- exploredFiles,
320
- pendingTodos,
321
- appliedEdits: ctx.state.appliedEditCount ?? 0,
322
- orchestrationRequested: ctx.state.orchestrationReminderSent === true,
323
- }));
324
- }
325
- function writeWorktreeSurfaceAvailable(cwd) {
326
- try {
327
- const discovered = discoverAgentProfiles(cwd, "both").profiles;
328
- return hasWriteWorktreeProfile([...builtinAgentProfiles(), ...discovered]);
329
- }
330
- catch {
331
- return hasWriteWorktreeProfile(builtinAgentProfiles());
332
- }
333
- }
334
- function isCodeWriteResult(_toolCall, result) {
335
- if (result.isError || result.status === "blocked" || result.status === "cancelled" || result.status === "command_error") {
336
- return false;
337
- }
338
- return result.metadata?.kind === "write" || result.metadata?.kind === "edit" || result.metadata?.kind === "patch";
339
- }
340
- function isMutationTool(name) {
341
- return name === "edit" || name === "write";
342
- }
343
118
  function hasSubagentLifecycleActivity(toolCalls, toolResults) {
344
119
  return toolCalls.some((toolCall) => isSubagentLifecycleTool(toolCall.name))
345
120
  || toolResults.some((result) => result.metadata?.kind === "subagent");
@@ -350,26 +125,3 @@ function isSubagentLifecycleTool(name) {
350
125
  || name === "send_input"
351
126
  || name === "close_agent";
352
127
  }
353
- function hashEditCall(toolCall) {
354
- // Cheap fingerprint that identifies "same edit/write call". JSON of the
355
- // sorted parsed args is good enough — we only need stable equality between
356
- // identical calls, not cryptographic strength.
357
- try {
358
- return `${toolCall.name}:${stableStringify(toolCall.parsedArgs)}`;
359
- }
360
- catch {
361
- return `${toolCall.name}:${toolCall.arguments}`;
362
- }
363
- }
364
- function stableStringify(value) {
365
- if (Array.isArray(value)) {
366
- return `[${value.map(stableStringify).join(",")}]`;
367
- }
368
- if (value && typeof value === "object") {
369
- const entries = Object.keys(value)
370
- .sort()
371
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
372
- return `{${entries.join(",")}}`;
373
- }
374
- return JSON.stringify(value ?? null);
375
- }
@@ -1,45 +1,20 @@
1
1
  import type { Agent } from "../agent.js";
2
2
  import type { ContentPart, ParsedToolCall, ToolRegistryEntry, ToolResult } from "../types.js";
3
3
  import type { GitChangeBaseline } from "../agent/change-tracker.js";
4
- import type { TaskType } from "../agent/task-classifier.js";
5
- import type { ExecutionGovernor } from "../agent/execution-governor.js";
6
- import type { EvidenceTracker } from "../agent/evidence-tracker.js";
7
- import type { DiscoveryBarrier } from "../agent/discovery-barrier.js";
8
- import type { WorkflowPhase } from "./workflow.js";
9
4
  export interface TurnHookState {
10
- taskType?: TaskType;
11
- governor?: ExecutionGovernor;
12
- discoveryBarrier?: DiscoveryBarrier;
13
- evidenceTracker?: EvidenceTracker;
14
- workflowPhase?: WorkflowPhase;
15
- workflowKey?: string;
16
5
  turnCount?: number;
17
6
  forceTextOnlyReason?: string;
18
7
  forceContinuationReason?: string;
19
- codeChanged?: boolean;
20
- /** Completion self-check gate already fired this run (fires at most once). */
21
- completionGateFired?: boolean;
8
+ /** Modified-existing-tests disclosure already delivered this run (at most once). */
9
+ testsTouchedDisclosed?: boolean;
22
10
  /** Git dirty-state snapshot at run start (null = not a git repo / git failed). */
23
11
  gitBaseline?: GitChangeBaseline | null;
24
12
  /** Any tool executed this run — guards git-change attribution for pure Q&A turns. */
25
13
  toolUsed?: boolean;
26
- smallTaskHintSent?: boolean;
27
- recentEditFailures?: string[];
28
- editNoMatchReminderPaths?: string[];
29
- editRetryReminderSent?: boolean;
30
- recentReadPaths?: string[];
31
- redundantReadReminded?: Set<string>;
32
14
  taskBudget?: {
33
15
  total: number;
34
16
  spent: number;
35
17
  };
36
- exploredFiles?: Set<string>;
37
- /** Checkpoint evaluated at the first mutation — once per turn, fired or not. */
38
- largeTaskCheckpointDone?: boolean;
39
- /** Successful mutations this turn; rendered as K in the nudge wording. */
40
- appliedEditCount?: number;
41
- /** orchestrationRequestReminder fired: the nudge offers only run_workflow. */
42
- orchestrationReminderSent?: boolean;
43
18
  }
44
19
  export interface TurnHookContext {
45
20
  agent: Agent;
@@ -54,26 +29,18 @@ export interface BeforeModelCallHookContext extends TurnHookContext {
54
29
  disableTools: (reason: string) => void;
55
30
  }
56
31
  export interface BeforeToolCallHookContext extends TurnHookContext {
57
- toolCall: ParsedToolCall & {
58
- arbiterNote?: string;
59
- };
32
+ toolCall: ParsedToolCall;
60
33
  blockedResult?: ToolResult;
61
- replaceToolCall: (toolCall: ParsedToolCall & {
62
- arbiterNote?: string;
63
- }) => void;
34
+ replaceToolCall: (toolCall: ParsedToolCall) => void;
64
35
  blockToolCall: (result: ToolResult) => void;
65
36
  }
66
37
  export interface AfterToolCallHookContext extends TurnHookContext {
67
- toolCall: ParsedToolCall & {
68
- arbiterNote?: string;
69
- };
38
+ toolCall: ParsedToolCall;
70
39
  result: ToolResult;
71
40
  replaceResult: (result: ToolResult) => void;
72
41
  }
73
42
  export interface BeforeContinuationHookContext extends TurnHookContext {
74
- toolCalls: Array<ParsedToolCall & {
75
- arbiterNote?: string;
76
- }>;
43
+ toolCalls: ParsedToolCall[];
77
44
  toolResults: ToolResult[];
78
45
  requestTextOnlyTurn: (reason: string) => void;
79
46
  }
@@ -1,7 +1,7 @@
1
1
  export const PERMISSION_MODE_INFO = {
2
2
  default: { title: "Default", shortTitle: "default", symbol: "", color: "muted" },
3
3
  plan: { title: "Plan", shortTitle: "plan", symbol: "⏸", color: "accent" },
4
- bypassPermissions: { title: "Bypass permissions", shortTitle: "bypass", symbol: "⏵⏵", color: "error" },
4
+ bypassPermissions: { title: "Bypass permissions", shortTitle: "bypass permission", symbol: "⏵⏵", color: "error" },
5
5
  };
6
6
  /**
7
7
  * Cycle order for the interactive mode keybind. The visible TUI loop keeps the
@@ -19,41 +19,15 @@ export declare const PLAN_MODE_EXIT_REMINDER: string;
19
19
  * they can be invoked.
20
20
  */
21
21
  export declare function buildDeferredToolsReminder(names: string[]): string;
22
- export declare function buildInvestigationReminder(): string;
23
- export declare function buildLoopWarningReminder(reason: string): string;
24
- export declare function buildSearchFreezeReminder(reason: string): string;
25
- export declare function buildExplorationFreezeReminder(reason: string): string;
26
- export interface CompletionSelfCheckOptions {
27
- /** Pre-existing test files this run modified, with deleted-line counts. */
28
- modifiedExistingTests?: Array<{
29
- path: string;
30
- deletedLines: number;
31
- }>;
32
- }
33
- export declare function buildCompletionSelfCheckReminder(options?: CompletionSelfCheckOptions): string;
34
22
  export declare function buildToolFreezeReminder(reason: string): string;
35
- export declare function buildWorkflowPhaseReminder(input: {
36
- phase: "investigate" | "correlate" | "conclude";
37
- covered: string[];
38
- pending: string[];
39
- }): string;
40
23
  /**
41
- * Fired when a file mutation failure suggests the model may be relying on stale
42
- * local memory instead of the current file bytes. Models — especially
43
- * thinking-heavy ones — can otherwise spiral on `No changes made: identical
44
- * content` or `oldText not found` because their internal reasoning convinces
45
- * them they are typing the change correctly.
24
+ * Modified-existing-tests disclosure (docs/harness-thinning.md): a fact from
25
+ * git ground truth — bash writes and subagent worktree merges do not appear
26
+ * in the model's own tool memory. Disclosure, not accusation: legitimate
27
+ * test updates are normal engineering; the rule is that they must be
28
+ * declared, never silent.
46
29
  */
47
- export declare function buildEditRetryEscalationReminder(reason: string): string;
48
- /**
49
- * Fired the FIRST time the model re-reads a file it already read in this turn.
50
- * Soft — does not freeze the tool. The model may still re-read when context was
51
- * pruned, the requested range changed, or a later mutation needs verification.
52
- */
53
- export declare function buildRedundantReadReminder(path: string): string;
54
- /**
55
- * Injected once at task start when the user's input looks like a small,
56
- * focused task (e.g. "write an HTML page about X"). Counterweight to the
57
- * default protocol which biases toward thorough exploration.
58
- */
59
- export declare function buildSmallTaskHint(): string;
30
+ export declare function buildModifiedTestsDisclosure(modifiedExistingTests: Array<{
31
+ path: string;
32
+ deletedLines: number;
33
+ }>): string;