@gajae-code/coding-agent 0.7.6 → 0.7.7
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/CHANGELOG.md +6 -0
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +0 -5
- package/dist/types/config/settings-schema.d.ts +78 -9
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/web-search-cli.ts +5 -0
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +3 -6
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +80 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/ultragoal-guard.ts +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +3 -4
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +44 -0
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -1113,17 +1113,6 @@
|
|
|
1113
1113
|
return html;
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
-
function renderInspectImage(name, args, result, ctx) {
|
|
1117
|
-
const p = str(args.path == null ? args.url : args.path) || '';
|
|
1118
|
-
let html = toolHead('inspect_image', escapeHtml(shortenPath(p)));
|
|
1119
|
-
if (result) {
|
|
1120
|
-
html += ctx.renderResultImages();
|
|
1121
|
-
const output = ctx.getResultText();
|
|
1122
|
-
if (output) html += formatExpandableOutput(output, 8);
|
|
1123
|
-
}
|
|
1124
|
-
return html;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1116
|
function renderGenerateImage(name, args, result, ctx) {
|
|
1128
1117
|
const subject = str(args.subject) || '';
|
|
1129
1118
|
const badges = args.aspect_ratio ? [String(args.aspect_ratio)] : null;
|
|
@@ -1544,7 +1533,6 @@
|
|
|
1544
1533
|
debug: renderDebug,
|
|
1545
1534
|
puppeteer: renderBrowser,
|
|
1546
1535
|
browser: renderBrowser,
|
|
1547
|
-
inspect_image: renderInspectImage,
|
|
1548
1536
|
generate_image: renderGenerateImage,
|
|
1549
1537
|
ask: renderAsk,
|
|
1550
1538
|
resolve: renderResolve,
|
|
@@ -256,6 +256,11 @@ export function renderUltragoalStatusMarkdown(summary: {
|
|
|
256
256
|
currentGoal?: { id: string; status: string; title?: string; objective?: string };
|
|
257
257
|
counts: Record<string, number>;
|
|
258
258
|
goals: unknown[];
|
|
259
|
+
nudgeBudget?: number;
|
|
260
|
+
nudgeCount?: number;
|
|
261
|
+
nudgeRemaining?: number;
|
|
262
|
+
nudgeGoalId?: string;
|
|
263
|
+
nudgeTargetKind?: string;
|
|
259
264
|
}): string {
|
|
260
265
|
if (!summary.exists)
|
|
261
266
|
return `# ultragoal status\n\n- status: missing\n- No ultragoal plan found at ${summary.paths.goalsPath}. Run \`gjc ultragoal create-goals --brief "..."\` first.\n`;
|
|
@@ -270,6 +275,14 @@ export function renderUltragoalStatusMarkdown(summary: {
|
|
|
270
275
|
];
|
|
271
276
|
if (summary.gjcObjective) lines.push(`- objective: ${summary.gjcObjective}`);
|
|
272
277
|
if (summary.currentGoal) lines.push(`- current: ${summary.currentGoal.id} (${summary.currentGoal.status})`);
|
|
278
|
+
if (summary.nudgeBudget !== undefined && summary.nudgeGoalId) {
|
|
279
|
+
const used = summary.nudgeCount ?? 0;
|
|
280
|
+
const remaining = summary.nudgeRemaining ?? Math.max(0, summary.nudgeBudget - used);
|
|
281
|
+
const target = summary.nudgeTargetKind ? ` target=${summary.nudgeTargetKind}` : "";
|
|
282
|
+
lines.push(
|
|
283
|
+
`- nudge: ${summary.nudgeGoalId} ${used}/${summary.nudgeBudget} used (${remaining} remaining)${target}`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
273
286
|
lines.push(`- goals_path: ${summary.paths.goalsPath}`);
|
|
274
287
|
if (summary.paths.ledgerPath) lines.push(`- ledger_path: ${summary.paths.ledgerPath}`);
|
|
275
288
|
return `${lines.join("\n")}\n`;
|
|
@@ -8,9 +8,13 @@ import {
|
|
|
8
8
|
hashStructuredValue,
|
|
9
9
|
readUltragoalLedger,
|
|
10
10
|
readUltragoalPlan,
|
|
11
|
+
recordUltragoalNudgeIfBudgetRemaining,
|
|
12
|
+
resolveUltragoalNudgeBudget,
|
|
13
|
+
selectUltragoalNudgeTarget,
|
|
11
14
|
type UltragoalCompletionVerification,
|
|
12
15
|
type UltragoalGoal,
|
|
13
16
|
type UltragoalLedgerEvent,
|
|
17
|
+
type UltragoalNudgeSurface,
|
|
14
18
|
type UltragoalPaths,
|
|
15
19
|
type UltragoalPlan,
|
|
16
20
|
type UltragoalReceiptKind,
|
|
@@ -494,6 +498,96 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
|
|
|
494
498
|
});
|
|
495
499
|
}
|
|
496
500
|
|
|
501
|
+
const NUDGE_SURFACE_LABEL: Record<UltragoalNudgeSurface, string> = {
|
|
502
|
+
pause: "pausing the goal",
|
|
503
|
+
drop: "dropping the run",
|
|
504
|
+
ask: "asking the user",
|
|
505
|
+
premature_complete: "finishing the story early",
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const NUDGE_SURFACE_REASON: Record<UltragoalNudgeSurface, string> = {
|
|
509
|
+
pause: "an active story still has resolvable work",
|
|
510
|
+
drop: "the aggregate run still has unfinished stories",
|
|
511
|
+
ask: "the decision can be resolved as durable story work",
|
|
512
|
+
premature_complete: "story verification is not yet satisfied",
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Escalating per-attempt refusal text. Deliberately avoids every
|
|
517
|
+
* `isUltragoalBypassPrompt` trigger: no `update_goal(`, no "skip/weaken verification",
|
|
518
|
+
* no "mark ... complete", no "--status complete", and the word "complete" never
|
|
519
|
+
* appears (so the `goal` ... `complete` proximity rule cannot match).
|
|
520
|
+
*/
|
|
521
|
+
export function formatUltragoalNudgeMessage(input: {
|
|
522
|
+
surface: UltragoalNudgeSurface;
|
|
523
|
+
attempt: number;
|
|
524
|
+
budget: number;
|
|
525
|
+
goalId: string;
|
|
526
|
+
}): string {
|
|
527
|
+
const label = NUDGE_SURFACE_LABEL[input.surface];
|
|
528
|
+
const reason = NUDGE_SURFACE_REASON[input.surface];
|
|
529
|
+
return [
|
|
530
|
+
`Ultragoal try-harder nudge (${input.attempt}/${input.budget}) for ${input.goalId}: ${label} was refused before the normal gate.`,
|
|
531
|
+
`Resolving this is part of the goal, not a reason to stop. Try a different approach first: inspect the failure, run a focused test or replay, find local credentials/config if access is the blocker, split the obstacle with \`gjc ultragoal steer --kind add_subgoal\`, delegate an executor, or record concrete review blockers.`,
|
|
532
|
+
`Reason: ${reason}.`,
|
|
533
|
+
].join("\n");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Consume one nudge for a guarded give-up attempt. MUST only be called from assert/
|
|
538
|
+
* consume paths, never from read-style `is...Blocked` diagnostics. Returns a nudge
|
|
539
|
+
* message while the per-story budget remains; otherwise reports not-nudged so the
|
|
540
|
+
* caller falls through to today's gate.
|
|
541
|
+
*/
|
|
542
|
+
async function consumeUltragoalNudge(input: {
|
|
543
|
+
cwd: string;
|
|
544
|
+
surface: UltragoalNudgeSurface;
|
|
545
|
+
currentGoal?: CurrentGoalLike | null;
|
|
546
|
+
sessionId?: string | null;
|
|
547
|
+
}): Promise<{ nudged: true; message: string } | { nudged: false }> {
|
|
548
|
+
const sessionId = input.sessionId?.trim() || (await ultragoalReadPaths(input.cwd)).sessionId;
|
|
549
|
+
if (!sessionId) return { nudged: false };
|
|
550
|
+
const plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
551
|
+
if (!plan) return { nudged: false };
|
|
552
|
+
const target = selectUltragoalNudgeTarget(plan, { currentGoalObjective: input.currentGoal?.objective });
|
|
553
|
+
if (!target) return { nudged: false };
|
|
554
|
+
const { budget } = await resolveUltragoalNudgeBudget(input.cwd);
|
|
555
|
+
const outcome = await recordUltragoalNudgeIfBudgetRemaining({
|
|
556
|
+
cwd: input.cwd,
|
|
557
|
+
sessionId,
|
|
558
|
+
target,
|
|
559
|
+
surface: input.surface,
|
|
560
|
+
budget,
|
|
561
|
+
reason: NUDGE_SURFACE_REASON[input.surface],
|
|
562
|
+
...(input.currentGoal?.objective ? { currentGoalObjective: input.currentGoal.objective } : {}),
|
|
563
|
+
});
|
|
564
|
+
if (outcome.nudged) {
|
|
565
|
+
return {
|
|
566
|
+
nudged: true,
|
|
567
|
+
message: formatUltragoalNudgeMessage({
|
|
568
|
+
surface: input.surface,
|
|
569
|
+
attempt: outcome.attempt,
|
|
570
|
+
budget: outcome.budget,
|
|
571
|
+
goalId: outcome.goalId,
|
|
572
|
+
}),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return { nudged: false };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Assert-path entry for the `ask` surface (the ask guard lives in another module).
|
|
580
|
+
* Resolves the active (leader) Ultragoal session so subagent/headless asks consume
|
|
581
|
+
* the leader run's budget rather than a fresh child ledger.
|
|
582
|
+
*/
|
|
583
|
+
export async function consumeUltragoalAskNudge(
|
|
584
|
+
cwd: string,
|
|
585
|
+
sessionId?: string | null,
|
|
586
|
+
): Promise<{ nudged: true; message: string } | { nudged: false }> {
|
|
587
|
+
if (!cwd) return { nudged: false };
|
|
588
|
+
return consumeUltragoalNudge({ cwd, surface: "ask", sessionId });
|
|
589
|
+
}
|
|
590
|
+
|
|
497
591
|
export async function assertCanCompleteCurrentGoal(input: {
|
|
498
592
|
cwd: string;
|
|
499
593
|
currentGoal?: CurrentGoalLike | null;
|
|
@@ -502,6 +596,13 @@ export async function assertCanCompleteCurrentGoal(input: {
|
|
|
502
596
|
if (!input.cwd) return;
|
|
503
597
|
const diagnostic = await readUltragoalVerificationState(input);
|
|
504
598
|
if (["inactive", "unrelated_goal", "active_verified_complete"].includes(diagnostic.state)) return;
|
|
599
|
+
const nudge = await consumeUltragoalNudge({
|
|
600
|
+
cwd: input.cwd,
|
|
601
|
+
surface: "premature_complete",
|
|
602
|
+
currentGoal: input.currentGoal,
|
|
603
|
+
sessionId: input.sessionId,
|
|
604
|
+
});
|
|
605
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
505
606
|
throw new Error(
|
|
506
607
|
`${diagnostic.message} Run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>\` first, or record review blockers and rerun verification.`,
|
|
507
608
|
);
|
|
@@ -558,6 +659,10 @@ export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPau
|
|
|
558
659
|
}
|
|
559
660
|
|
|
560
661
|
export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
662
|
+
if (cwd) {
|
|
663
|
+
const nudge = await consumeUltragoalNudge({ cwd, surface: "pause" });
|
|
664
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
665
|
+
}
|
|
561
666
|
const diagnostic = await isUltragoalPauseBlocked(cwd);
|
|
562
667
|
if (!diagnostic.blocked) return;
|
|
563
668
|
throw new Error(
|
|
@@ -568,3 +673,65 @@ export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
|
568
673
|
].join("\n"),
|
|
569
674
|
);
|
|
570
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Guard `goal({"op":"drop"})` during an active Ultragoal run. A *real give-up* (an
|
|
678
|
+
* aggregate run still mid-flight with incomplete required stories) is nudged while
|
|
679
|
+
* budget remains; once exhausted it falls through to today's drop behavior. Legitimate
|
|
680
|
+
* aggregate-reset drops — no durable run, unrelated goal, an already dropped/stale
|
|
681
|
+
* aggregate, or an all-stories-complete run — are never nudged. If durable state
|
|
682
|
+
* exists but cannot be read to classify the drop, fail closed.
|
|
683
|
+
*/
|
|
684
|
+
export async function assertUltragoalDropAllowed(input: {
|
|
685
|
+
cwd: string;
|
|
686
|
+
currentGoal?: CurrentGoalLike | null;
|
|
687
|
+
sessionId?: string | null;
|
|
688
|
+
}): Promise<void> {
|
|
689
|
+
if (!input.cwd) return;
|
|
690
|
+
let paths: UltragoalPaths;
|
|
691
|
+
let sessionId: string | null;
|
|
692
|
+
try {
|
|
693
|
+
({ paths, sessionId } = await ultragoalReadPaths(input.cwd));
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Unable to classify Ultragoal drop (durable state unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (sessionId === null) return;
|
|
700
|
+
try {
|
|
701
|
+
await fs.stat(paths.dir);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (isEnoent(error)) return;
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Unable to classify Ultragoal drop (durable state present but unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
let plan: UltragoalPlan | null;
|
|
709
|
+
try {
|
|
710
|
+
plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Unable to classify Ultragoal drop (goals.json unreadable): ${error instanceof Error ? error.message : String(error)}`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (!plan) {
|
|
717
|
+
throw new Error("Unable to classify Ultragoal drop: durable state exists but goals.json is missing or empty.");
|
|
718
|
+
}
|
|
719
|
+
// Out of scope: per-story mode drops keep today's behavior.
|
|
720
|
+
if (plan.gjcGoalMode !== "aggregate") return;
|
|
721
|
+
// A real give-up requires an active aggregate goal to actually be abandoned. With no
|
|
722
|
+
// current goal-mode goal (or a non-active one), `drop` is a no-op/reset before a fresh
|
|
723
|
+
// `create`, never a give-up — so it is left un-nudged.
|
|
724
|
+
if (!input.currentGoal) return;
|
|
725
|
+
if (input.currentGoal.status !== "active") return;
|
|
726
|
+
// Unrelated active goal: not this aggregate run.
|
|
727
|
+
if (!objectiveMatches(input.currentGoal.objective, plan)) return;
|
|
728
|
+
// All required stories complete: a legitimate reset, not a give-up.
|
|
729
|
+
if (getUltragoalRunCompletionState(plan).allComplete) return;
|
|
730
|
+
const nudge = await consumeUltragoalNudge({
|
|
731
|
+
cwd: input.cwd,
|
|
732
|
+
surface: "drop",
|
|
733
|
+
currentGoal: input.currentGoal,
|
|
734
|
+
sessionId,
|
|
735
|
+
});
|
|
736
|
+
if (nudge.nudged) throw new Error(nudge.message);
|
|
737
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { inflateSync } from "node:zlib";
|
|
4
5
|
import { normalizeGoal } from "../goals/state";
|
|
@@ -11,7 +12,13 @@ import { gjcRoot, sessionUltragoalDir } from "./session-layout";
|
|
|
11
12
|
import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
12
13
|
import { renderUltragoalStatusMarkdown } from "./state-renderer";
|
|
13
14
|
import { reconcileWorkflowSkillState } from "./state-runtime";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
appendJsonl,
|
|
17
|
+
persistedStateRevision,
|
|
18
|
+
withWorkflowStateLock,
|
|
19
|
+
writeArtifact,
|
|
20
|
+
writeGuardedJsonAtomic,
|
|
21
|
+
} from "./state-writer";
|
|
15
22
|
|
|
16
23
|
export type UltragoalGjcGoalMode = "aggregate" | "per-story";
|
|
17
24
|
export type UltragoalGoalStatus =
|
|
@@ -80,6 +87,44 @@ export interface UltragoalLedgerEvent extends JsonObject {
|
|
|
80
87
|
timestamp?: string;
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
export type UltragoalNudgeSurface = "pause" | "drop" | "ask" | "premature_complete";
|
|
91
|
+
export type UltragoalNudgeTargetKind = "story" | "final_aggregate_receipt";
|
|
92
|
+
|
|
93
|
+
export interface UltragoalNudgeLedgerEvent extends UltragoalLedgerEvent {
|
|
94
|
+
event: "nudge";
|
|
95
|
+
goalId: string;
|
|
96
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
97
|
+
surface: UltragoalNudgeSurface;
|
|
98
|
+
attempt: number;
|
|
99
|
+
budget: number;
|
|
100
|
+
reason: string;
|
|
101
|
+
currentGoalObjective?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UltragoalNudgeTarget {
|
|
105
|
+
goalId: string;
|
|
106
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type UltragoalNudgeOutcome =
|
|
110
|
+
| {
|
|
111
|
+
nudged: true;
|
|
112
|
+
attempt: number;
|
|
113
|
+
budget: number;
|
|
114
|
+
goalId: string;
|
|
115
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
116
|
+
event: UltragoalNudgeLedgerEvent;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
nudged: false;
|
|
120
|
+
exhausted: true;
|
|
121
|
+
count: number;
|
|
122
|
+
budget: number;
|
|
123
|
+
goalId: string;
|
|
124
|
+
targetKind: UltragoalNudgeTargetKind;
|
|
125
|
+
}
|
|
126
|
+
| { nudged: false; inactive: true; reason: string };
|
|
127
|
+
|
|
83
128
|
export interface UltragoalPaths {
|
|
84
129
|
dir: string;
|
|
85
130
|
briefPath: string;
|
|
@@ -95,6 +140,11 @@ export interface UltragoalStatusSummary {
|
|
|
95
140
|
currentGoal?: UltragoalGoal;
|
|
96
141
|
counts: Record<UltragoalGoalStatus, number>;
|
|
97
142
|
goals: UltragoalGoal[];
|
|
143
|
+
nudgeBudget?: number;
|
|
144
|
+
nudgeCount?: number;
|
|
145
|
+
nudgeRemaining?: number;
|
|
146
|
+
nudgeGoalId?: string;
|
|
147
|
+
nudgeTargetKind?: UltragoalNudgeTargetKind;
|
|
98
148
|
}
|
|
99
149
|
|
|
100
150
|
export interface UltragoalCommandResult {
|
|
@@ -220,6 +270,156 @@ export async function readUltragoalLedger(cwd: string, sessionId?: string | null
|
|
|
220
270
|
}
|
|
221
271
|
}
|
|
222
272
|
|
|
273
|
+
export const DEFAULT_ULTRAGOAL_NUDGE_BUDGET = 10;
|
|
274
|
+
|
|
275
|
+
/** Pure: count ledger `nudge` rows for an exact goalId. */
|
|
276
|
+
export function countUltragoalNudges(ledger: readonly UltragoalLedgerEvent[], goalId: string): number {
|
|
277
|
+
return ledger.filter(event => event.event === "nudge" && event.goalId === goalId).length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseNudgeBudgetValue(value: unknown): number | null {
|
|
281
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 ? value : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function readSettingsNudgeBudget(settingsPath: string): Promise<number | null> {
|
|
285
|
+
try {
|
|
286
|
+
const raw = await Bun.file(settingsPath).text();
|
|
287
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
288
|
+
// Support both the flat dotted key and a nested gjc.ultragoal.nudgeBudget shape.
|
|
289
|
+
const flat = parseNudgeBudgetValue(parsed["gjc.ultragoal.nudgeBudget"]);
|
|
290
|
+
if (flat !== null) return flat;
|
|
291
|
+
const gjc = parsed.gjc;
|
|
292
|
+
if (gjc && typeof gjc === "object") {
|
|
293
|
+
const ultragoal = (gjc as Record<string, unknown>).ultragoal;
|
|
294
|
+
if (ultragoal && typeof ultragoal === "object") {
|
|
295
|
+
return parseNudgeBudgetValue((ultragoal as Record<string, unknown>).nudgeBudget);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve the per-story nudge budget. Project `./.gjc/settings.json` overrides the
|
|
306
|
+
* user settings (`$GJC_CONFIG_DIR/settings.json` or `~/.gjc/settings.json`), else the
|
|
307
|
+
* default. Mirrors the `gjc.deepInterview.ambiguityThreshold` user+project precedence.
|
|
308
|
+
*/
|
|
309
|
+
export async function resolveUltragoalNudgeBudget(cwd: string): Promise<{ budget: number; source: string }> {
|
|
310
|
+
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
311
|
+
const project = await readSettingsNudgeBudget(projectPath);
|
|
312
|
+
if (project !== null) return { budget: project, source: projectPath };
|
|
313
|
+
const userDir = process.env.GJC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".gjc");
|
|
314
|
+
const userPath = path.join(userDir, "settings.json");
|
|
315
|
+
const user = await readSettingsNudgeBudget(userPath);
|
|
316
|
+
if (user !== null) return { budget: user, source: userPath };
|
|
317
|
+
return { budget: DEFAULT_ULTRAGOAL_NUDGE_BUDGET, source: "default" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Pure canonical selector shared by guards and status so `nudgeGoalId` can never
|
|
322
|
+
* diverge between what a guard consumes and what status displays. Prefers the active
|
|
323
|
+
* current-goal objective, then active > pending > failed (matching `chooseNextGoal`),
|
|
324
|
+
* then the aggregate final-receipt target when all stories are complete but the
|
|
325
|
+
* aggregate run still needs a final receipt. Returns null for verified-complete or
|
|
326
|
+
* absent/unrelated plans.
|
|
327
|
+
*/
|
|
328
|
+
export function selectUltragoalNudgeTarget(
|
|
329
|
+
plan: UltragoalPlan,
|
|
330
|
+
options: { currentGoalObjective?: string; retryFailed?: boolean } = {},
|
|
331
|
+
): UltragoalNudgeTarget | null {
|
|
332
|
+
const objective = options.currentGoalObjective?.trim();
|
|
333
|
+
if (objective) {
|
|
334
|
+
const matched = plan.goals.find(
|
|
335
|
+
goal => goal.objective.trim() === objective && SCHEDULABLE_STATUSES.has(goal.status),
|
|
336
|
+
);
|
|
337
|
+
if (matched) return { goalId: matched.id, targetKind: "story" };
|
|
338
|
+
}
|
|
339
|
+
const next = chooseNextGoal(plan, options.retryFailed === true);
|
|
340
|
+
if (next) return { goalId: next.id, targetKind: "story" };
|
|
341
|
+
const completion = getUltragoalRunCompletionState(plan, { retryFailed: options.retryFailed });
|
|
342
|
+
if (completion.needsFinalAggregateReceipt) {
|
|
343
|
+
const required = requiredUltragoalGoals(plan);
|
|
344
|
+
const finalGoal = required.at(-1);
|
|
345
|
+
if (finalGoal) return { goalId: finalGoal.id, targetKind: "final_aggregate_receipt" };
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Atomic consuming writer. Locks the ledger path, rereads + counts nudge rows for the
|
|
352
|
+
* target story, and appends exactly one `nudge` row inside the same critical section
|
|
353
|
+
* only while budget remains. Reuses the lockless `appendLedger` inside the lock (it
|
|
354
|
+
* does not acquire a conflicting lock), so concurrent guarded attempts cannot both
|
|
355
|
+
* observe `count = budget - 1` and overshoot the budget.
|
|
356
|
+
*/
|
|
357
|
+
export async function recordUltragoalNudgeIfBudgetRemaining(input: {
|
|
358
|
+
cwd: string;
|
|
359
|
+
sessionId?: string | null;
|
|
360
|
+
target: UltragoalNudgeTarget;
|
|
361
|
+
surface: UltragoalNudgeSurface;
|
|
362
|
+
budget: number;
|
|
363
|
+
reason: string;
|
|
364
|
+
currentGoalObjective?: string;
|
|
365
|
+
}): Promise<UltragoalNudgeOutcome> {
|
|
366
|
+
const { cwd, sessionId, target, surface, budget, reason } = input;
|
|
367
|
+
if (!Number.isFinite(budget) || budget <= 0) {
|
|
368
|
+
return {
|
|
369
|
+
nudged: false,
|
|
370
|
+
exhausted: true,
|
|
371
|
+
count: 0,
|
|
372
|
+
budget: Math.max(0, budget | 0),
|
|
373
|
+
goalId: target.goalId,
|
|
374
|
+
targetKind: target.targetKind,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const resolvedSessionId =
|
|
378
|
+
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
379
|
+
const paths = getUltragoalPaths(cwd, resolvedSessionId);
|
|
380
|
+
return withWorkflowStateLock(
|
|
381
|
+
paths.ledgerPath,
|
|
382
|
+
async () => {
|
|
383
|
+
const ledger = await readUltragoalLedger(cwd, resolvedSessionId);
|
|
384
|
+
const count = countUltragoalNudges(ledger, target.goalId);
|
|
385
|
+
if (count >= budget) {
|
|
386
|
+
return {
|
|
387
|
+
nudged: false,
|
|
388
|
+
exhausted: true,
|
|
389
|
+
count,
|
|
390
|
+
budget,
|
|
391
|
+
goalId: target.goalId,
|
|
392
|
+
targetKind: target.targetKind,
|
|
393
|
+
} as const;
|
|
394
|
+
}
|
|
395
|
+
const attempt = count + 1;
|
|
396
|
+
const entry = (await appendLedger(
|
|
397
|
+
cwd,
|
|
398
|
+
{
|
|
399
|
+
event: "nudge",
|
|
400
|
+
goalId: target.goalId,
|
|
401
|
+
targetKind: target.targetKind,
|
|
402
|
+
surface,
|
|
403
|
+
attempt,
|
|
404
|
+
budget,
|
|
405
|
+
reason,
|
|
406
|
+
...(input.currentGoalObjective ? { currentGoalObjective: input.currentGoalObjective } : {}),
|
|
407
|
+
},
|
|
408
|
+
resolvedSessionId,
|
|
409
|
+
)) as UltragoalNudgeLedgerEvent;
|
|
410
|
+
return {
|
|
411
|
+
nudged: true,
|
|
412
|
+
attempt,
|
|
413
|
+
budget,
|
|
414
|
+
goalId: target.goalId,
|
|
415
|
+
targetKind: target.targetKind,
|
|
416
|
+
event: entry,
|
|
417
|
+
} as const;
|
|
418
|
+
},
|
|
419
|
+
{ cwd },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
223
423
|
async function writePlan(cwd: string, plan: UltragoalPlan, sessionId?: string | null): Promise<void> {
|
|
224
424
|
const resolvedSessionId =
|
|
225
425
|
sessionId?.trim() || resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
|
|
@@ -525,6 +725,20 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
525
725
|
else if (counts.active > 0) status = "active";
|
|
526
726
|
else if (counts.failed > 0) status = "failed";
|
|
527
727
|
else if (counts.blocked > 0 || counts.review_blocked > 0) status = "blocked";
|
|
728
|
+
const nudgeTarget = selectUltragoalNudgeTarget(plan, { currentGoalObjective: currentGoal?.objective });
|
|
729
|
+
let nudgeFields: Partial<UltragoalStatusSummary> = {};
|
|
730
|
+
if (nudgeTarget) {
|
|
731
|
+
const { budget } = await resolveUltragoalNudgeBudget(cwd);
|
|
732
|
+
const ledger = await readUltragoalLedger(cwd, resolvedSessionId);
|
|
733
|
+
const nudgeCount = countUltragoalNudges(ledger, nudgeTarget.goalId);
|
|
734
|
+
nudgeFields = {
|
|
735
|
+
nudgeBudget: budget,
|
|
736
|
+
nudgeCount,
|
|
737
|
+
nudgeRemaining: Math.max(0, budget - nudgeCount),
|
|
738
|
+
nudgeGoalId: nudgeTarget.goalId,
|
|
739
|
+
nudgeTargetKind: nudgeTarget.targetKind,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
528
742
|
return {
|
|
529
743
|
exists: true,
|
|
530
744
|
status,
|
|
@@ -533,6 +747,7 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
533
747
|
currentGoal,
|
|
534
748
|
counts,
|
|
535
749
|
goals: plan.goals,
|
|
750
|
+
...nudgeFields,
|
|
536
751
|
};
|
|
537
752
|
}
|
|
538
753
|
export function buildUltragoalHudSummary(
|
|
@@ -3847,6 +4062,11 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
|
|
|
3847
4062
|
goals_path: summary.paths.goalsPath,
|
|
3848
4063
|
};
|
|
3849
4064
|
if (summary.gjcObjective) payload.gjc_objective = summary.gjcObjective;
|
|
4065
|
+
if (summary.nudgeBudget !== undefined) payload.nudge_budget = summary.nudgeBudget;
|
|
4066
|
+
if (summary.nudgeCount !== undefined) payload.nudge_count = summary.nudgeCount;
|
|
4067
|
+
if (summary.nudgeRemaining !== undefined) payload.nudge_remaining = summary.nudgeRemaining;
|
|
4068
|
+
if (summary.nudgeGoalId !== undefined) payload.nudge_goal_id = summary.nudgeGoalId;
|
|
4069
|
+
if (summary.nudgeTargetKind !== undefined) payload.nudge_target_kind = summary.nudgeTargetKind;
|
|
3850
4070
|
const ledgerText = await Bun.file(summary.paths.ledgerPath)
|
|
3851
4071
|
.text()
|
|
3852
4072
|
.catch(() => "");
|
|
@@ -4,7 +4,11 @@ import { Text } from "@gajae-code/tui";
|
|
|
4
4
|
import { formatNumber, prompt } from "@gajae-code/utils";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
6
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
assertCanCompleteCurrentGoal,
|
|
9
|
+
assertUltragoalDropAllowed,
|
|
10
|
+
assertUltragoalPauseAllowed,
|
|
11
|
+
} from "../../gjc-runtime/ultragoal-guard";
|
|
8
12
|
import type { Theme, ThemeColor } from "../../modes/theme/theme";
|
|
9
13
|
import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
|
|
10
14
|
import { formatDuration } from "../../slash-commands/helpers/format";
|
|
@@ -103,6 +107,15 @@ async function executeGoalOperation(session: ToolSession, params: GoalToolInput)
|
|
|
103
107
|
return buildGoalToolResponse(paused?.goal ?? null);
|
|
104
108
|
}
|
|
105
109
|
if (params.op === "drop") {
|
|
110
|
+
try {
|
|
111
|
+
await assertUltragoalDropAllowed({
|
|
112
|
+
cwd: session.cwd,
|
|
113
|
+
currentGoal: session.getGoalModeState?.()?.goal ?? null,
|
|
114
|
+
sessionId: session.getSessionId?.(),
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
118
|
+
}
|
|
106
119
|
const dropped = await runtime.dropGoal();
|
|
107
120
|
return buildGoalToolResponse(dropped ?? null);
|
|
108
121
|
}
|