@gajae-code/coding-agent 0.2.0 → 0.2.2

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 (114) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/session.d.ts +24 -0
  5. package/dist/types/commands/skills.d.ts +26 -0
  6. package/dist/types/config/model-registry.d.ts +33 -4
  7. package/dist/types/config/models-config-schema.d.ts +52 -5
  8. package/dist/types/config/settings-schema.d.ts +1 -24
  9. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  10. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  11. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  12. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  17. package/dist/types/goals/runtime.d.ts +3 -9
  18. package/dist/types/goals/state.d.ts +3 -6
  19. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  20. package/dist/types/modes/components/model-selector.d.ts +21 -1
  21. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  22. package/dist/types/modes/components/status-line.d.ts +0 -3
  23. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -12
  25. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  26. package/dist/types/modes/theme/theme.d.ts +1 -2
  27. package/dist/types/modes/types.d.ts +1 -7
  28. package/dist/types/session/agent-session.d.ts +2 -0
  29. package/dist/types/session/contribution-prep.d.ts +47 -0
  30. package/dist/types/skill-state/active-state.d.ts +4 -0
  31. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  32. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  33. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  34. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  35. package/package.json +7 -7
  36. package/src/cli/args.ts +17 -2
  37. package/src/cli/skills-cli.ts +88 -0
  38. package/src/cli.ts +7 -1
  39. package/src/commands/contribution-prep.ts +41 -0
  40. package/src/commands/deep-interview.ts +6 -22
  41. package/src/commands/launch.ts +10 -1
  42. package/src/commands/ralplan.ts +10 -22
  43. package/src/commands/session.ts +150 -0
  44. package/src/commands/skills.ts +48 -0
  45. package/src/commands/state.ts +14 -4
  46. package/src/commands/team.ts +23 -3
  47. package/src/commit/agentic/index.ts +1 -0
  48. package/src/commit/pipeline.ts +1 -0
  49. package/src/config/model-registry.ts +269 -10
  50. package/src/config/models-config-schema.ts +124 -88
  51. package/src/config/settings-schema.ts +1 -25
  52. package/src/config.ts +1 -1
  53. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  54. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  55. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  57. package/src/eval/py/prelude.py +1 -1
  58. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  59. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  60. package/src/gjc-runtime/launch-tmux.ts +83 -43
  61. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  62. package/src/gjc-runtime/state-runtime.ts +562 -0
  63. package/src/gjc-runtime/team-runtime.ts +708 -52
  64. package/src/gjc-runtime/tmux-common.ts +119 -0
  65. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  66. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  67. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  68. package/src/goals/runtime.ts +38 -144
  69. package/src/goals/state.ts +36 -7
  70. package/src/goals/tools/goal-tool.ts +15 -172
  71. package/src/hooks/skill-state.ts +31 -12
  72. package/src/internal-urls/docs-index.generated.ts +4 -3
  73. package/src/main.ts +10 -1
  74. package/src/modes/components/model-selector.ts +109 -28
  75. package/src/modes/components/skill-hud/render.ts +4 -0
  76. package/src/modes/components/status-line/segments.ts +5 -16
  77. package/src/modes/components/status-line/types.ts +0 -3
  78. package/src/modes/components/status-line.ts +0 -6
  79. package/src/modes/controllers/command-controller.ts +25 -1
  80. package/src/modes/controllers/input-controller.ts +0 -15
  81. package/src/modes/controllers/selector-controller.ts +42 -2
  82. package/src/modes/interactive-mode.ts +18 -219
  83. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  84. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  85. package/src/modes/theme/theme.ts +0 -6
  86. package/src/modes/types.ts +1 -7
  87. package/src/prompts/goals/goal-continuation.md +1 -4
  88. package/src/prompts/goals/goal-mode-active.md +3 -5
  89. package/src/prompts/system/system-prompt.md +5 -7
  90. package/src/prompts/tools/goal.md +4 -4
  91. package/src/sdk.ts +2 -1
  92. package/src/session/agent-session.ts +18 -0
  93. package/src/session/contribution-prep.ts +320 -0
  94. package/src/setup/provider-onboarding.ts +2 -0
  95. package/src/skill-state/active-state.ts +38 -0
  96. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  97. package/src/skill-state/workflow-hud.ts +23 -5
  98. package/src/skill-state/workflow-state-contract.ts +121 -0
  99. package/src/slash-commands/acp-builtins.ts +11 -2
  100. package/src/slash-commands/builtin-registry.ts +40 -13
  101. package/src/task/commands.ts +1 -5
  102. package/src/tools/gh.ts +212 -2
  103. package/src/tools/index.ts +2 -5
  104. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  105. package/dist/types/commands/question.d.ts +0 -7
  106. package/dist/types/modes/loop-limit.d.ts +0 -22
  107. package/src/commands/gjc-runtime-bridge.ts +0 -227
  108. package/src/commands/question.ts +0 -12
  109. package/src/modes/loop-limit.ts +0 -140
  110. package/src/prompts/commands/orchestrate.md +0 -49
  111. package/src/prompts/goals/goal-budget-limit.md +0 -16
  112. package/src/prompts/tools/create-goal.md +0 -3
  113. package/src/prompts/tools/get-goal.md +0 -3
  114. package/src/prompts/tools/update-goal.md +0 -3
@@ -1,8 +1,13 @@
1
1
  import { prompt, Snowflake } from "@gajae-code/utils";
2
- import goalBudgetLimitPrompt from "../prompts/goals/goal-budget-limit.md" with { type: "text" };
3
2
  import goalContinuationPrompt from "../prompts/goals/goal-continuation.md" with { type: "text" };
4
3
  import goalModeActivePrompt from "../prompts/goals/goal-mode-active.md" with { type: "text" };
5
- import type { Goal, GoalBudgetSteering, GoalModeState, GoalRuntimeEvent, GoalTokenUsage } from "./state";
4
+ import {
5
+ type Goal,
6
+ type GoalModeState,
7
+ type GoalRuntimeEvent,
8
+ type GoalTokenUsage,
9
+ normalizeGoalModeState,
10
+ } from "./state";
6
11
 
7
12
  export interface GoalRuntimeHost {
8
13
  getState(): GoalModeState | undefined;
@@ -32,10 +37,9 @@ export interface GoalWallClockSnapshot {
32
37
  export interface GoalRuntimeSnapshot {
33
38
  turnSnapshot?: GoalTurnSnapshot;
34
39
  wallClock: GoalWallClockSnapshot;
35
- budgetReportedFor?: string;
36
40
  }
37
41
 
38
- export type GoalPromptKind = "active" | "continuation" | "budget-limit";
42
+ export type GoalPromptKind = "active" | "continuation";
39
43
 
40
44
  function cloneGoal(goal: Goal): Goal {
41
45
  return { ...goal };
@@ -45,19 +49,6 @@ function cloneState(state: GoalModeState): GoalModeState {
45
49
  return { ...state, goal: cloneGoal(state.goal) };
46
50
  }
47
51
 
48
- function budgetValue(goal: Goal): string {
49
- return goal.tokenBudget === undefined ? "none" : String(goal.tokenBudget);
50
- }
51
-
52
- function remainingValue(goal: Goal): string {
53
- return goal.tokenBudget === undefined ? "unbounded" : String(Math.max(0, goal.tokenBudget - goal.tokensUsed));
54
- }
55
-
56
- export function remainingTokens(goal: Goal | null | undefined): number | null {
57
- if (!goal || goal.tokenBudget === undefined) return null;
58
- return Math.max(0, goal.tokenBudget - goal.tokensUsed);
59
- }
60
-
61
52
  export function escapeXmlText(input: string): string {
62
53
  let firstEscapable = -1;
63
54
  for (let index = 0; index < input.length; index++) {
@@ -97,9 +88,8 @@ export function goalTokenDelta(current: GoalTokenUsage, baseline: GoalTokenUsage
97
88
  // Diverges from OpenAI code backend-rs: OpenAI code backend omits cache creation because its target providers
98
89
  // do not bill cache writes distinctly through the token-usage stream. Pi receives
99
90
  // cacheWrite separately on Anthropic/Bedrock; rotating a 1h ephemeral cache or
100
- // re-anchoring a changed system prompt can write 100K+ tokens, which the goal
101
- // budget must account for. cacheRead is excluded because it is reused prefix,
102
- // not new work consumed by the goal.
91
+ // re-anchoring a changed system prompt can write 100K+ tokens, which usage accounting must track.
92
+ // cacheRead is excluded because it is reused prefix, not new work consumed by the goal.
103
93
  return (
104
94
  Math.max(0, current.input - baseline.input) +
105
95
  Math.max(0, current.cacheWrite - baseline.cacheWrite) +
@@ -108,48 +98,22 @@ export function goalTokenDelta(current: GoalTokenUsage, baseline: GoalTokenUsage
108
98
  }
109
99
 
110
100
  export function renderGoalPrompt(kind: GoalPromptKind, goal: Goal): string {
111
- const template =
112
- kind === "active"
113
- ? goalModeActivePrompt
114
- : kind === "continuation"
115
- ? goalContinuationPrompt
116
- : goalBudgetLimitPrompt;
101
+ const template = kind === "active" ? goalModeActivePrompt : goalContinuationPrompt;
117
102
  return prompt.render(template, {
118
103
  objective: escapeXmlText(goal.objective),
119
104
  tokensUsed: String(goal.tokensUsed),
120
- tokenBudget: budgetValue(goal),
121
- remainingTokens: remainingValue(goal),
122
105
  timeUsedSeconds: String(goal.timeUsedSeconds),
123
106
  });
124
107
  }
125
108
 
126
- export function completionBudgetReport(goal: Goal): string | null {
127
- const parts: string[] = [];
128
- if (goal.tokenBudget !== undefined) {
129
- parts.push(`tokens used: ${goal.tokensUsed} of ${goal.tokenBudget}`);
130
- }
131
- if (goal.timeUsedSeconds > 0) {
132
- parts.push(`time used: ${goal.timeUsedSeconds} seconds`);
133
- }
134
- if (parts.length === 0) return null;
135
- return `Goal achieved. Report final budget usage to the user: ${parts.join("; ")}.`;
136
- }
137
-
138
- function validateTokenBudget(tokenBudget: number | undefined): void {
139
- if (tokenBudget !== undefined && (!Number.isInteger(tokenBudget) || tokenBudget <= 0)) {
140
- throw new Error("goal token_budget must be a positive integer when provided");
141
- }
142
- }
143
-
144
109
  function isAccountingStatus(goal: Goal): boolean {
145
- return goal.status === "active" || goal.status === "budget-limited";
110
+ return goal.status === "active";
146
111
  }
147
112
 
148
113
  export class GoalRuntime {
149
114
  readonly #host: GoalRuntimeHost;
150
115
  #turnSnapshot: GoalTurnSnapshot | undefined;
151
116
  #wallClock: GoalWallClockSnapshot;
152
- #budgetReportedFor: string | undefined;
153
117
  #accountingTail: Promise<void> = Promise.resolve();
154
118
 
155
119
  constructor(host: GoalRuntimeHost) {
@@ -163,7 +127,6 @@ export class GoalRuntime {
163
127
  ? { ...this.#turnSnapshot, baselineUsage: { ...this.#turnSnapshot.baselineUsage } }
164
128
  : undefined,
165
129
  wallClock: { ...this.#wallClock },
166
- budgetReportedFor: this.#budgetReportedFor,
167
130
  };
168
131
  }
169
132
 
@@ -172,7 +135,7 @@ export class GoalRuntime {
172
135
  }
173
136
 
174
137
  #hasAccountingState(): boolean {
175
- const state = this.#host.getState();
138
+ const state = normalizeGoalModeState(this.#host.getState());
176
139
  return Boolean(state?.enabled && isAccountingStatus(state.goal));
177
140
  }
178
141
 
@@ -192,7 +155,7 @@ export class GoalRuntime {
192
155
  }
193
156
 
194
157
  #getStateClone(): GoalModeState | undefined {
195
- const state = this.#host.getState();
158
+ const state = normalizeGoalModeState(this.#host.getState());
196
159
  return state ? cloneState(state) : undefined;
197
160
  }
198
161
 
@@ -228,7 +191,7 @@ export class GoalRuntime {
228
191
 
229
192
  onTurnStart(turnId: string, baselineUsage: GoalTokenUsage): void {
230
193
  this.#turnSnapshot = { turnId, baselineUsage: { ...baselineUsage } };
231
- const state = this.#host.getState();
194
+ const state = this.#getStateClone();
232
195
  if (state?.enabled && isAccountingStatus(state.goal)) {
233
196
  this.#turnSnapshot.activeGoalId = state.goal.id;
234
197
  if (this.#wallClock.activeGoalId !== state.goal.id) {
@@ -240,12 +203,12 @@ export class GoalRuntime {
240
203
  async onToolCompleted(toolName: string): Promise<void> {
241
204
  if (toolName === "goal") return;
242
205
  if (!this.#hasAccountingState()) return;
243
- await this.flushUsage("allowed");
206
+ await this.flushUsage();
244
207
  }
245
208
 
246
209
  async onGoalToolCompleted(): Promise<void> {
247
210
  if (!this.#hasAccountingState()) return;
248
- await this.flushUsage("suppressed");
211
+ await this.flushUsage();
249
212
  }
250
213
 
251
214
  async onAgentEnd(options?: { turnCompleted?: boolean; currentUsage?: GoalTokenUsage }): Promise<void> {
@@ -253,19 +216,19 @@ export class GoalRuntime {
253
216
  this.#turnSnapshot = undefined;
254
217
  return;
255
218
  }
256
- await this.flushUsage("suppressed", options?.currentUsage);
219
+ await this.flushUsage(options?.currentUsage);
257
220
  this.#turnSnapshot = undefined;
258
221
  }
259
222
 
260
223
  async onTaskAborted(_options?: { reason?: "interrupted" | "internal" }): Promise<void> {
261
- const state = this.#host.getState();
224
+ const state = this.#getStateClone();
262
225
  const needsAccounting = state?.enabled && isAccountingStatus(state.goal);
263
226
  if (!needsAccounting) {
264
227
  this.#turnSnapshot = undefined;
265
228
  return;
266
229
  }
267
230
  await this.#withAccounting(async () => {
268
- await this.#flushUsageLocked("suppressed");
231
+ await this.#flushUsageLocked();
269
232
  this.#turnSnapshot = undefined;
270
233
  const cloned = this.#getStateClone();
271
234
  if (!cloned?.enabled || !isAccountingStatus(cloned.goal)) return;
@@ -290,38 +253,7 @@ export class GoalRuntime {
290
253
  return state;
291
254
  }
292
255
 
293
- async onBudgetMutated(newBudget: number | undefined): Promise<GoalModeState | undefined> {
294
- validateTokenBudget(newBudget);
295
- return await this.#withAccounting(async () => {
296
- this.#budgetReportedFor = undefined;
297
- await this.#flushUsageLocked("suppressed");
298
- const state = this.#getStateClone();
299
- if (!state?.goal) return undefined;
300
- state.goal.tokenBudget = newBudget;
301
- state.goal.updatedAt = this.#now();
302
- let shouldSteer = false;
303
- if (newBudget !== undefined && state.goal.tokensUsed >= newBudget) {
304
- if (state.goal.status === "active") {
305
- state.goal.status = "budget-limited";
306
- shouldSteer = true;
307
- }
308
- } else if (state.goal.status === "budget-limited") {
309
- state.goal.status = "active";
310
- state.enabled = true;
311
- this.#markActiveAccounting(state.goal);
312
- }
313
- await this.#commitState(state, { persist: state.enabled ? "goal" : "goal_paused" });
314
- if (shouldSteer) {
315
- await this.#sendBudgetLimitSteer(state.goal);
316
- }
317
- return state;
318
- });
319
- }
320
-
321
- async #flushUsageLocked(
322
- steering: GoalBudgetSteering,
323
- currentUsage: GoalTokenUsage = this.#host.getCurrentUsage(),
324
- ): Promise<void> {
256
+ async #flushUsageLocked(currentUsage: GoalTokenUsage = this.#host.getCurrentUsage()): Promise<void> {
325
257
  const state = this.#getStateClone();
326
258
  if (!state?.enabled || !isAccountingStatus(state.goal)) return;
327
259
  if (this.#turnSnapshot?.activeGoalId !== state.goal.id && this.#wallClock.activeGoalId !== state.goal.id) return;
@@ -339,13 +271,6 @@ export class GoalRuntime {
339
271
  state.goal.tokensUsed += tokenDelta;
340
272
  state.goal.timeUsedSeconds += wallSeconds;
341
273
  state.goal.updatedAt = this.#now();
342
- const flippedToBudgetLimited =
343
- state.goal.tokenBudget !== undefined &&
344
- state.goal.tokensUsed >= state.goal.tokenBudget &&
345
- state.goal.status === "active";
346
- if (flippedToBudgetLimited) {
347
- state.goal.status = "budget-limited";
348
- }
349
274
 
350
275
  if (this.#turnSnapshot?.activeGoalId === state.goal.id) {
351
276
  this.#turnSnapshot.baselineUsage = { ...currentUsage };
@@ -355,29 +280,18 @@ export class GoalRuntime {
355
280
  }
356
281
 
357
282
  await this.#commitState(state, { persist: "goal" });
358
-
359
- if (state.goal.status !== "budget-limited") {
360
- this.#budgetReportedFor = undefined;
361
- }
362
- if (steering === "allowed" && flippedToBudgetLimited && this.#budgetReportedFor !== state.goal.id) {
363
- await this.#sendBudgetLimitSteer(state.goal);
364
- }
365
283
  }
366
284
 
367
- async flushUsage(
368
- steering: GoalBudgetSteering,
369
- currentUsage: GoalTokenUsage = this.#host.getCurrentUsage(),
370
- ): Promise<void> {
371
- await this.#withAccounting(() => this.#flushUsageLocked(steering, currentUsage));
285
+ async flushUsage(currentUsage: GoalTokenUsage = this.#host.getCurrentUsage()): Promise<void> {
286
+ await this.#withAccounting(() => this.#flushUsageLocked(currentUsage));
372
287
  }
373
288
 
374
- #createGoalState(objective: string, tokenBudget: number | undefined): GoalModeState {
289
+ #createGoalState(objective: string): GoalModeState {
375
290
  const now = this.#now();
376
291
  const goal: Goal = {
377
292
  id: String(Snowflake.next()),
378
293
  objective,
379
294
  status: "active",
380
- tokenBudget,
381
295
  tokensUsed: 0,
382
296
  timeUsedSeconds: 0,
383
297
  createdAt: now,
@@ -386,33 +300,29 @@ export class GoalRuntime {
386
300
  return { enabled: true, mode: "active", goal };
387
301
  }
388
302
 
389
- async createGoal(input: { objective: string; tokenBudget?: number }): Promise<GoalModeState> {
303
+ async createGoal(input: { objective: string }): Promise<GoalModeState> {
390
304
  const objective = validateGoalObjective(input.objective, "create");
391
- validateTokenBudget(input.tokenBudget);
392
305
  return await this.#withAccounting(async () => {
393
- const existing = this.#host.getState();
306
+ const existing = this.#getStateClone();
394
307
  if (existing?.goal && existing.goal.status !== "dropped" && existing.goal.status !== "complete") {
395
308
  throw new Error("cannot create a new goal because this session already has a goal");
396
309
  }
397
- const state = this.#createGoalState(objective, input.tokenBudget);
398
- this.#budgetReportedFor = undefined;
310
+ const state = this.#createGoalState(objective);
399
311
  this.#markActiveAccounting(state.goal);
400
312
  await this.#commitState(state, { persist: "goal" });
401
313
  return state;
402
314
  });
403
315
  }
404
316
 
405
- async replaceGoal(input: { objective: string; tokenBudget?: number }): Promise<GoalModeState> {
317
+ async replaceGoal(input: { objective: string }): Promise<GoalModeState> {
406
318
  const objective = validateGoalObjective(input.objective, "replace");
407
- validateTokenBudget(input.tokenBudget);
408
319
  return await this.#withAccounting(async () => {
409
- const existing = this.#host.getState();
320
+ const existing = this.#getStateClone();
410
321
  if (!existing?.enabled || !isAccountingStatus(existing.goal)) {
411
322
  throw new Error("cannot replace goal because no goal is active");
412
323
  }
413
- await this.#flushUsageLocked("suppressed");
414
- const state = this.#createGoalState(objective, input.tokenBudget);
415
- this.#budgetReportedFor = undefined;
324
+ await this.#flushUsageLocked();
325
+ const state = this.#createGoalState(objective);
416
326
  this.#markActiveAccounting(state.goal);
417
327
  await this.#commitState(state, { persist: "goal" });
418
328
  return state;
@@ -429,7 +339,6 @@ export class GoalRuntime {
429
339
  state.reason = undefined;
430
340
  state.goal.status = "active";
431
341
  state.goal.updatedAt = this.#now();
432
- this.#budgetReportedFor = undefined;
433
342
  this.#markActiveAccounting(state.goal);
434
343
  await this.#commitState(state, { persist: "goal" });
435
344
  return state;
@@ -438,18 +347,17 @@ export class GoalRuntime {
438
347
 
439
348
  async pauseGoal(): Promise<GoalModeState | undefined> {
440
349
  return await this.#withAccounting(async () => {
441
- await this.#flushUsageLocked("suppressed");
350
+ await this.#flushUsageLocked();
442
351
  const state = this.#getStateClone();
443
352
  if (!state?.goal) return undefined;
444
353
  state.enabled = false;
445
354
  state.mode = "active";
446
355
  state.reason = undefined;
447
- if (state.goal.status === "active" || state.goal.status === "budget-limited") {
356
+ if (state.goal.status === "active") {
448
357
  state.goal.status = "paused";
449
358
  }
450
359
  state.goal.updatedAt = this.#now();
451
360
  this.#clearActiveAccounting();
452
- this.#budgetReportedFor = undefined;
453
361
  await this.#commitState(state, { persist: "goal_paused" });
454
362
  return state;
455
363
  });
@@ -457,12 +365,11 @@ export class GoalRuntime {
457
365
 
458
366
  async dropGoal(): Promise<Goal | undefined> {
459
367
  return await this.#withAccounting(async () => {
460
- await this.#flushUsageLocked("suppressed");
368
+ await this.#flushUsageLocked();
461
369
  const state = this.#getStateClone();
462
370
  if (!state?.goal) return undefined;
463
371
  const dropped = { ...state.goal, status: "dropped" as const, updatedAt: this.#now() };
464
372
  this.#clearActiveAccounting();
465
- this.#budgetReportedFor = undefined;
466
373
  await this.#host.emit({
467
374
  type: "goal_updated",
468
375
  goal: dropped,
@@ -475,7 +382,7 @@ export class GoalRuntime {
475
382
 
476
383
  async completeGoalFromTool(): Promise<Goal> {
477
384
  return await this.#withAccounting(async () => {
478
- await this.#flushUsageLocked("suppressed");
385
+ await this.#flushUsageLocked();
479
386
  const state = this.#getStateClone();
480
387
  if (!state?.goal) {
481
388
  throw new Error("cannot complete goal because no goal is active");
@@ -492,33 +399,20 @@ export class GoalRuntime {
492
399
  state.mode = "exiting";
493
400
  state.reason = "completed";
494
401
  this.#clearActiveAccounting();
495
- this.#budgetReportedFor = undefined;
496
402
  await this.#commitState(state, { persist: "goal" });
497
403
  return state.goal;
498
404
  });
499
405
  }
500
406
 
501
407
  buildActivePrompt(): string | undefined {
502
- const state = this.#host.getState();
503
- return state?.enabled && state.goal && state.goal.status === "active"
504
- ? renderGoalPrompt("active", state.goal)
505
- : undefined;
408
+ const state = this.#getStateClone();
409
+ return state?.enabled && state.goal.status === "active" ? renderGoalPrompt("active", state.goal) : undefined;
506
410
  }
507
411
 
508
412
  buildContinuationPrompt(): string | undefined {
509
- const state = this.#host.getState();
413
+ const state = this.#getStateClone();
510
414
  return state?.enabled && state.goal.status === "active"
511
415
  ? renderGoalPrompt("continuation", state.goal)
512
416
  : undefined;
513
417
  }
514
-
515
- async #sendBudgetLimitSteer(goal: Goal): Promise<void> {
516
- if (this.#budgetReportedFor === goal.id) return;
517
- this.#budgetReportedFor = goal.id;
518
- await this.#host.sendHiddenMessage({
519
- customType: "goal-budget-limit",
520
- content: renderGoalPrompt("budget-limit", goal),
521
- deliverAs: "steer",
522
- });
523
- }
524
418
  }
@@ -1,12 +1,11 @@
1
1
  import type { UsageStatistics } from "../session/session-manager";
2
2
 
3
- export type GoalStatus = "active" | "paused" | "budget-limited" | "complete" | "dropped";
3
+ export type GoalStatus = "active" | "paused" | "complete" | "dropped";
4
4
 
5
5
  export interface Goal {
6
6
  id: string;
7
7
  objective: string;
8
8
  status: GoalStatus;
9
- tokenBudget?: number;
10
9
  tokensUsed: number;
11
10
  timeUsedSeconds: number;
12
11
  createdAt: number;
@@ -19,12 +18,9 @@ export interface GoalModeState {
19
18
  reason?: "completed";
20
19
  goal: Goal;
21
20
  }
22
-
23
21
  export interface GoalToolDetails {
24
22
  op: "create" | "get" | "complete" | "resume" | "drop";
25
23
  goal?: Goal | null;
26
- remainingTokens?: number | null;
27
- completionBudgetReport?: string | null;
28
24
  }
29
25
 
30
26
  export type GoalRuntimeEvent =
@@ -33,5 +29,38 @@ export type GoalRuntimeEvent =
33
29
 
34
30
  export type GoalTokenUsage = Pick<UsageStatistics, "input" | "output" | "cacheRead" | "cacheWrite">;
35
31
 
36
- export type GoalBudgetSteering = "allowed" | "suppressed";
37
- export type GoalTerminalMetricEmission = "emit" | "suppress";
32
+ export function normalizeGoal(candidate: unknown): Goal | null {
33
+ if (typeof candidate !== "object" || candidate === null) return null;
34
+ const value = candidate as Record<string, unknown>;
35
+ if (
36
+ typeof value.id !== "string" ||
37
+ typeof value.objective !== "string" ||
38
+ typeof value.status !== "string" ||
39
+ typeof value.tokensUsed !== "number" ||
40
+ typeof value.timeUsedSeconds !== "number" ||
41
+ typeof value.createdAt !== "number" ||
42
+ typeof value.updatedAt !== "number"
43
+ ) {
44
+ return null;
45
+ }
46
+ const status = value.status === "budget-limited" ? "active" : value.status;
47
+ if (status !== "active" && status !== "paused" && status !== "complete" && status !== "dropped") {
48
+ return null;
49
+ }
50
+ return {
51
+ id: value.id,
52
+ objective: value.objective,
53
+ status,
54
+ tokensUsed: value.tokensUsed,
55
+ timeUsedSeconds: value.timeUsedSeconds,
56
+ createdAt: value.createdAt,
57
+ updatedAt: value.updatedAt,
58
+ };
59
+ }
60
+
61
+ export function normalizeGoalModeState(candidate: GoalModeState | undefined): GoalModeState | undefined {
62
+ if (!candidate) return undefined;
63
+ const goal = normalizeGoal(candidate.goal);
64
+ if (!goal) return undefined;
65
+ return { ...candidate, goal };
66
+ }