@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.
- package/dist/agent/child-runner.js +11 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +13 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +198 -0
- package/dist/agent/subagent/runtime.d.ts +203 -0
- package/dist/agent/subagent/runtime.js +789 -0
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-store.d.ts +18 -2
- package/dist/agent/subagent-store.js +32 -1
- package/dist/agent/tool-intent.d.ts +10 -0
- package/dist/agent/tool-intent.js +18 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/workflow/runs.d.ts +72 -0
- package/dist/agent/workflow/runs.js +157 -0
- package/dist/agent.d.ts +78 -96
- package/dist/agent.js +266 -1072
- package/dist/context/compact-llm.js +14 -5
- package/dist/context/compact.d.ts +9 -0
- package/dist/context/compact.js +36 -10
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.js +24 -5
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +13 -9
- package/dist/goal/tools.js +12 -3
- package/dist/main.js +24 -19
- package/dist/model-catalog.js +36 -2
- package/dist/model-config.d.ts +2 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +11 -7
- package/dist/model-pricing.js +0 -0
- package/dist/oauth/grok.d.ts +1 -1
- package/dist/oauth/grok.js +5 -2
- package/dist/oauth/storage.d.ts +9 -2
- package/dist/oauth/storage.js +16 -9
- package/dist/orchestrator/default-hooks.d.ts +20 -0
- package/dist/orchestrator/default-hooks.js +47 -295
- package/dist/orchestrator/hooks.d.ts +6 -39
- package/dist/permission/mode.js +1 -1
- package/dist/prompt/reminders.d.ts +9 -35
- package/dist/prompt/reminders.js +30 -136
- package/dist/prompt/task-reminders.d.ts +0 -31
- package/dist/prompt/task-reminders.js +0 -87
- package/dist/provider-ai-sdk.d.ts +2 -0
- package/dist/provider-ai-sdk.js +1 -0
- package/dist/provider-anthropic.d.ts +2 -0
- package/dist/provider-anthropic.js +3 -1
- package/dist/provider-ark-responses.d.ts +2 -0
- package/dist/provider-ark-responses.js +1 -0
- package/dist/provider-registry.d.ts +13 -0
- package/dist/provider-registry.js +83 -0
- package/dist/provider-transform.js +37 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +13 -1
- package/dist/sdk/index.d.ts +152 -0
- package/dist/sdk/index.js +400 -0
- package/dist/session.js +9 -1
- package/dist/slash-commands/commands.js +33 -19
- package/dist/tools/exit-plan-mode.js +8 -0
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +11 -5
- package/dist/tui-ink/app.d.ts +2 -6
- package/dist/tui-ink/app.js +11 -18
- package/dist/tui-ink/display-history.d.ts +0 -2
- package/dist/tui-ink/display-history.js +0 -52
- package/dist/tui-ink/footer.js +2 -2
- package/dist/tui-ink/message-list.js +62 -23
- package/dist/tui-ink/run.js +10 -5
- package/dist/tui-ink/sync-output.d.ts +17 -0
- package/dist/tui-ink/sync-output.js +35 -0
- package/dist/tui-ink/theme.d.ts +0 -1
- package/dist/tui-ink/theme.js +0 -2
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/types.d.ts +21 -1
- package/package.json +16 -2
- package/dist/agent/discovery-barrier.d.ts +0 -21
- package/dist/agent/discovery-barrier.js +0 -173
- package/dist/agent/evidence-tracker.d.ts +0 -15
- package/dist/agent/evidence-tracker.js +0 -93
- package/dist/agent/execution-governor.d.ts +0 -36
- package/dist/agent/execution-governor.js +0 -218
- package/dist/agent/task-classifier.d.ts +0 -3
- package/dist/agent/task-classifier.js +0 -114
- package/dist/agent/task-size.d.ts +0 -9
- package/dist/agent/task-size.js +0 -33
- package/dist/agent/tool-arbiter.d.ts +0 -7
- package/dist/agent/tool-arbiter.js +0 -33
- package/dist/orchestrator/workflow.d.ts +0 -10
- package/dist/orchestrator/workflow.js +0 -22
package/dist/oauth/storage.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OAuth credential storage in
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
44
|
+
if (!existsSync(this.authPath))
|
|
38
45
|
return;
|
|
39
46
|
try {
|
|
40
|
-
const raw = readFileSync(
|
|
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(
|
|
56
|
+
const dir = dirname(this.authPath);
|
|
50
57
|
if (!existsSync(dir))
|
|
51
58
|
mkdirSync(dir, { recursive: true });
|
|
52
|
-
writeFileSync(
|
|
59
|
+
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2) + "\n", { mode: 0o600 });
|
|
53
60
|
}
|
|
54
61
|
getPath() {
|
|
55
|
-
return
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
20
|
-
// misses bash-written files and subagent worktree merges;
|
|
21
|
-
// baseline at run start makes the run's real footprint
|
|
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
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
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
|
|
244
|
-
//
|
|
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
|
|
248
|
-
//
|
|
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
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
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.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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:
|
|
75
|
-
arbiterNote?: string;
|
|
76
|
-
}>;
|
|
43
|
+
toolCalls: ParsedToolCall[];
|
|
77
44
|
toolResults: ToolResult[];
|
|
78
45
|
requestTextOnlyTurn: (reason: string) => void;
|
|
79
46
|
}
|
package/dist/permission/mode.js
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|