@gajae-code/coding-agent 0.3.1 → 0.3.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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +30 -0
- package/dist/types/config/model-profiles.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +8 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +47 -0
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
- package/dist/types/main.d.ts +10 -1
- package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/tools/hindsight-recall.d.ts +0 -2
- package/dist/types/tools/hindsight-reflect.d.ts +0 -2
- package/dist/types/tools/hindsight-retain.d.ts +0 -2
- package/dist/types/tools/index.d.ts +4 -4
- package/package.json +7 -7
- package/src/cli/args.ts +10 -0
- package/src/commands/launch.ts +8 -0
- package/src/config/model-profile-activation.ts +157 -0
- package/src/config/model-profiles.ts +155 -0
- package/src/config/model-registry.ts +19 -0
- package/src/config/model-resolver.ts +3 -2
- package/src/config/models-config-schema.ts +36 -0
- package/src/config/settings-schema.ts +10 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +119 -14
- package/src/internal-urls/docs-index.generated.ts +6 -9
- package/src/main.ts +67 -1
- package/src/modes/components/custom-provider-wizard.ts +318 -0
- package/src/modes/components/model-selector.ts +108 -18
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/controllers/selector-controller.ts +57 -1
- package/src/modes/types.ts +1 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read-path.md +6 -7
- package/src/prompts/memories/unavailable.md +2 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/recall.md +1 -0
- package/src/prompts/tools/reflect.md +1 -0
- package/src/prompts/tools/retain.md +1 -0
- package/src/sdk.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +2 -0
- package/src/task/index.ts +2 -0
- package/src/tools/hindsight-recall.ts +0 -2
- package/src/tools/hindsight-reflect.ts +0 -2
- package/src/tools/hindsight-retain.ts +0 -2
- package/src/tools/index.ts +4 -18
- package/src/tools/read.ts +3 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { GjcModelAssignmentTargetId } from "./model-registry";
|
|
2
|
+
import type { ModelsConfig } from "./models-config-schema";
|
|
3
|
+
|
|
4
|
+
export type ModelProfileRole = GjcModelAssignmentTargetId;
|
|
5
|
+
|
|
6
|
+
export interface ModelProfileDefinition {
|
|
7
|
+
name: string;
|
|
8
|
+
requiredProviders: string[];
|
|
9
|
+
modelMapping: Partial<Record<ModelProfileRole, string>>;
|
|
10
|
+
source: "builtin" | "user";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResolvedProfileBinding {
|
|
14
|
+
defaultSelector?: string;
|
|
15
|
+
agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default">, string>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseModelSelectorProvider(selector: string): string | undefined {
|
|
19
|
+
const slashIdx = selector.indexOf("/");
|
|
20
|
+
if (slashIdx <= 0) return undefined;
|
|
21
|
+
return selector.slice(0, slashIdx);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deriveModelProfileMappedProviders(definition: Pick<ModelProfileDefinition, "modelMapping">): string[] {
|
|
25
|
+
const providers = new Set<string>();
|
|
26
|
+
for (const selector of Object.values(definition.modelMapping)) {
|
|
27
|
+
if (!selector) continue;
|
|
28
|
+
const provider = parseModelSelectorProvider(selector);
|
|
29
|
+
if (provider) providers.add(provider);
|
|
30
|
+
}
|
|
31
|
+
return [...providers].sort((a, b) => a.localeCompare(b));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function aggregateModelProfileRequiredProviders(
|
|
35
|
+
requiredProviders: readonly string[],
|
|
36
|
+
definition: Pick<ModelProfileDefinition, "modelMapping">,
|
|
37
|
+
): string[] {
|
|
38
|
+
const providers = new Set(requiredProviders);
|
|
39
|
+
for (const provider of deriveModelProfileMappedProviders(definition)) {
|
|
40
|
+
providers.add(provider);
|
|
41
|
+
}
|
|
42
|
+
return [...providers];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const profile = (
|
|
46
|
+
name: string,
|
|
47
|
+
requiredProviders: string[],
|
|
48
|
+
modelMapping: Record<ModelProfileRole, string>,
|
|
49
|
+
): ModelProfileDefinition => ({
|
|
50
|
+
name,
|
|
51
|
+
requiredProviders: aggregateModelProfileRequiredProviders(requiredProviders, { modelMapping }),
|
|
52
|
+
modelMapping,
|
|
53
|
+
source: "builtin",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
57
|
+
profile("opencode-go-eco", ["opencode-go"], {
|
|
58
|
+
default: "opencode-go/deepseek-v4-flash",
|
|
59
|
+
executor: "opencode-go/qwen3.5-plus",
|
|
60
|
+
architect: "opencode-go/glm-5",
|
|
61
|
+
planner: "opencode-go/minimax-m2.5",
|
|
62
|
+
critic: "opencode-go/kimi-k2.5",
|
|
63
|
+
}),
|
|
64
|
+
profile("opencode-go-standard", ["opencode-go"], {
|
|
65
|
+
default: "opencode-go/kimi-k2.6",
|
|
66
|
+
executor: "opencode-go/qwen3.6-plus",
|
|
67
|
+
architect: "opencode-go/glm-5.1",
|
|
68
|
+
planner: "opencode-go/minimax-m2.7",
|
|
69
|
+
critic: "opencode-go/deepseek-v4-pro",
|
|
70
|
+
}),
|
|
71
|
+
profile("opencode-go-pro", ["opencode-go"], {
|
|
72
|
+
default: "opencode-go/qwen3.7-max",
|
|
73
|
+
executor: "opencode-go/kimi-k2.6",
|
|
74
|
+
architect: "opencode-go/deepseek-v4-pro:high",
|
|
75
|
+
planner: "opencode-go/glm-5.1:high",
|
|
76
|
+
critic: "opencode-go/minimax-m2.7:high",
|
|
77
|
+
}),
|
|
78
|
+
profile("codex-eco", ["openai-codex"], {
|
|
79
|
+
default: "openai-codex/gpt-5.4-mini",
|
|
80
|
+
executor: "openai-codex/gpt-5.4-nano",
|
|
81
|
+
architect: "openai-codex/gpt-5.4-mini",
|
|
82
|
+
planner: "openai-codex/gpt-5.4-mini",
|
|
83
|
+
critic: "openai-codex/gpt-5.4-mini",
|
|
84
|
+
}),
|
|
85
|
+
profile("codex-standard", ["openai-codex"], {
|
|
86
|
+
default: "openai-codex/gpt-5.4:medium",
|
|
87
|
+
executor: "openai-codex/gpt-5.4:low",
|
|
88
|
+
architect: "openai-codex/gpt-5.4:xhigh",
|
|
89
|
+
planner: "openai-codex/gpt-5.4:medium",
|
|
90
|
+
critic: "openai-codex/gpt-5.4:high",
|
|
91
|
+
}),
|
|
92
|
+
profile("codex-pro", ["openai-codex"], {
|
|
93
|
+
default: "openai-codex/gpt-5.5",
|
|
94
|
+
executor: "openai-codex/gpt-5.2-codex",
|
|
95
|
+
architect: "openai-codex/gpt-5.1-codex-max:high",
|
|
96
|
+
planner: "openai-codex/gpt-5.5:high",
|
|
97
|
+
critic: "openai-codex/gpt-5.3-codex-spark:high",
|
|
98
|
+
}),
|
|
99
|
+
profile("opencode-go-codex-eco", ["opencode-go", "openai-codex"], {
|
|
100
|
+
default: "opencode-go/deepseek-v4-flash",
|
|
101
|
+
executor: "opencode-go/qwen3.5-plus",
|
|
102
|
+
architect: "openai-codex/gpt-5.4-mini",
|
|
103
|
+
planner: "openai-codex/gpt-5.4-mini",
|
|
104
|
+
critic: "openai-codex/gpt-5.4-mini",
|
|
105
|
+
}),
|
|
106
|
+
profile("opencode-go-codex-standard", ["opencode-go", "openai-codex"], {
|
|
107
|
+
default: "opencode-go/kimi-k2.6",
|
|
108
|
+
executor: "opencode-go/qwen3.6-plus",
|
|
109
|
+
architect: "openai-codex/gpt-5.4",
|
|
110
|
+
planner: "openai-codex/gpt-5.4",
|
|
111
|
+
critic: "openai-codex/gpt-5.4",
|
|
112
|
+
}),
|
|
113
|
+
profile("opencode-go-codex-pro", ["opencode-go", "openai-codex"], {
|
|
114
|
+
default: "opencode-go/qwen3.7-max",
|
|
115
|
+
executor: "opencode-go/kimi-k2.6",
|
|
116
|
+
architect: "openai-codex/gpt-5.1-codex-max:high",
|
|
117
|
+
planner: "openai-codex/gpt-5.5:high",
|
|
118
|
+
critic: "openai-codex/gpt-5.3-codex-spark:high",
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map<string, ModelProfileDefinition> {
|
|
123
|
+
const profiles = new Map<string, ModelProfileDefinition>();
|
|
124
|
+
for (const definition of BUILTIN_MODEL_PROFILES) {
|
|
125
|
+
profiles.set(definition.name, {
|
|
126
|
+
...definition,
|
|
127
|
+
requiredProviders: [...definition.requiredProviders],
|
|
128
|
+
modelMapping: { ...definition.modelMapping },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (const [name, definition] of Object.entries(userProfiles ?? {})) {
|
|
132
|
+
const modelMapping = { ...definition.model_mapping };
|
|
133
|
+
profiles.set(name, {
|
|
134
|
+
name,
|
|
135
|
+
requiredProviders: aggregateModelProfileRequiredProviders(definition.required_providers, { modelMapping }),
|
|
136
|
+
modelMapping,
|
|
137
|
+
source: "user",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return profiles;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveProfileBindings(definition: ModelProfileDefinition): ResolvedProfileBinding {
|
|
144
|
+
const { default: defaultSelector, executor, architect, planner, critic } = definition.modelMapping;
|
|
145
|
+
const agentModelOverrides: ResolvedProfileBinding["agentModelOverrides"] = {};
|
|
146
|
+
if (executor !== undefined) agentModelOverrides.executor = executor;
|
|
147
|
+
if (architect !== undefined) agentModelOverrides.architect = architect;
|
|
148
|
+
if (planner !== undefined) agentModelOverrides.planner = planner;
|
|
149
|
+
if (critic !== undefined) agentModelOverrides.critic = critic;
|
|
150
|
+
return { defaultSelector, agentModelOverrides };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function formatAvailableProfileNames(profiles: ReadonlyMap<string, ModelProfileDefinition>): string {
|
|
154
|
+
return [...profiles.keys()].sort((a, b) => a.localeCompare(b)).join(", ");
|
|
155
|
+
}
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
formatCanonicalVariantSelector,
|
|
44
44
|
type ModelEquivalenceConfig,
|
|
45
45
|
} from "./model-equivalence";
|
|
46
|
+
import { type ModelProfileDefinition, mergeModelProfiles } from "./model-profiles";
|
|
46
47
|
import {
|
|
47
48
|
type ModelOverride,
|
|
48
49
|
type ModelsConfig,
|
|
@@ -511,6 +512,7 @@ interface CustomModelsResult {
|
|
|
511
512
|
configuredProviders?: Set<string>;
|
|
512
513
|
equivalence?: ModelEquivalenceConfig;
|
|
513
514
|
modelBindings?: NonNullable<ModelsConfig["modelBindings"]>;
|
|
515
|
+
profiles?: ModelsConfig["profiles"];
|
|
514
516
|
error?: ConfigError;
|
|
515
517
|
found: boolean;
|
|
516
518
|
}
|
|
@@ -954,6 +956,7 @@ export class ModelRegistry {
|
|
|
954
956
|
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
955
957
|
#equivalenceConfig: ModelEquivalenceConfig | undefined;
|
|
956
958
|
#configuredModelBindings: NonNullable<ModelsConfig["modelBindings"]> | undefined;
|
|
959
|
+
#modelProfiles: Map<string, ModelProfileDefinition> = mergeModelProfiles();
|
|
957
960
|
#modelBindingsTargetSettings: Settings | undefined;
|
|
958
961
|
#appliedModelBindingRoles = new Set<string>();
|
|
959
962
|
#appliedAgentModelBindingOverrides = new Set<string>();
|
|
@@ -1093,6 +1096,7 @@ export class ModelRegistry {
|
|
|
1093
1096
|
configuredProviders = new Set(),
|
|
1094
1097
|
equivalence,
|
|
1095
1098
|
modelBindings,
|
|
1099
|
+
profiles,
|
|
1096
1100
|
error: configError,
|
|
1097
1101
|
} = this.#loadCustomModels();
|
|
1098
1102
|
this.#configError = configError;
|
|
@@ -1103,6 +1107,7 @@ export class ModelRegistry {
|
|
|
1103
1107
|
this.#modelOverrides = modelOverrides;
|
|
1104
1108
|
this.#equivalenceConfig = equivalence;
|
|
1105
1109
|
this.#configuredModelBindings = modelBindings;
|
|
1110
|
+
this.#modelProfiles = mergeModelProfiles(profiles);
|
|
1106
1111
|
|
|
1107
1112
|
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
1108
1113
|
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
@@ -1343,6 +1348,7 @@ export class ModelRegistry {
|
|
|
1343
1348
|
discoverableProviders: [],
|
|
1344
1349
|
configuredProviders: new Set(),
|
|
1345
1350
|
error,
|
|
1351
|
+
profiles: undefined,
|
|
1346
1352
|
found: true,
|
|
1347
1353
|
};
|
|
1348
1354
|
} else if (status === "not-found") {
|
|
@@ -1353,6 +1359,7 @@ export class ModelRegistry {
|
|
|
1353
1359
|
keylessProviders: new Set(),
|
|
1354
1360
|
discoverableProviders: [],
|
|
1355
1361
|
configuredProviders: new Set(),
|
|
1362
|
+
profiles: undefined,
|
|
1356
1363
|
found: false,
|
|
1357
1364
|
};
|
|
1358
1365
|
}
|
|
@@ -1441,10 +1448,22 @@ export class ModelRegistry {
|
|
|
1441
1448
|
configuredProviders,
|
|
1442
1449
|
equivalence: value.equivalence,
|
|
1443
1450
|
modelBindings: value.modelBindings,
|
|
1451
|
+
profiles: value.profiles,
|
|
1444
1452
|
found: true,
|
|
1445
1453
|
};
|
|
1446
1454
|
}
|
|
1447
1455
|
|
|
1456
|
+
getModelProfiles(): Map<string, ModelProfileDefinition> {
|
|
1457
|
+
return new Map(this.#modelProfiles);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
getModelProfile(name: string): ModelProfileDefinition | undefined {
|
|
1461
|
+
return this.#modelProfiles.get(name);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
getAvailableModelProfileNames(): string[] {
|
|
1465
|
+
return [...this.#modelProfiles.keys()].sort((a, b) => a.localeCompare(b));
|
|
1466
|
+
}
|
|
1448
1467
|
applyConfiguredModelBindings(targetSettings: Settings): void {
|
|
1449
1468
|
this.#modelBindingsTargetSettings = targetSettings;
|
|
1450
1469
|
this.#applyConfiguredModelBindingsToTarget();
|
|
@@ -764,6 +764,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
764
764
|
parentActiveModelPattern: string | undefined,
|
|
765
765
|
modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
|
|
766
766
|
settings?: Settings,
|
|
767
|
+
sessionId?: string,
|
|
767
768
|
): Promise<{
|
|
768
769
|
model?: Model<Api>;
|
|
769
770
|
thinkingLevel?: ThinkingLevel;
|
|
@@ -775,7 +776,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
775
776
|
return { ...primary, authFallbackUsed: false };
|
|
776
777
|
}
|
|
777
778
|
|
|
778
|
-
const primaryKey = await modelRegistry.getApiKey(primary.model);
|
|
779
|
+
const primaryKey = await modelRegistry.getApiKey(primary.model, sessionId);
|
|
779
780
|
if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
|
|
780
781
|
return { ...primary, authFallbackUsed: false };
|
|
781
782
|
}
|
|
@@ -787,7 +788,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
787
788
|
if (modelsAreEqual(fallback.model, primary.model)) {
|
|
788
789
|
return { ...primary, authFallbackUsed: false };
|
|
789
790
|
}
|
|
790
|
-
const fallbackKey = await modelRegistry.getApiKey(fallback.model);
|
|
791
|
+
const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
|
|
791
792
|
if (!isAuthenticated(fallbackKey)) {
|
|
792
793
|
return { ...primary, authFallbackUsed: false };
|
|
793
794
|
}
|
|
@@ -76,6 +76,39 @@ const ModelBindingsSchema = z.object({
|
|
|
76
76
|
modelRoles: z.record(z.string(), z.string().min(1)).optional(),
|
|
77
77
|
agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
|
|
78
78
|
});
|
|
79
|
+
export const ProfileRoleSchema = z.enum(["default", "executor", "architect", "planner", "critic"]);
|
|
80
|
+
|
|
81
|
+
function isValidProfileModelSelector(value: string): boolean {
|
|
82
|
+
if (value.includes(",")) return false;
|
|
83
|
+
const slashIdx = value.indexOf("/");
|
|
84
|
+
if (slashIdx <= 0 || slashIdx === value.length - 1) return false;
|
|
85
|
+
const provider = value.slice(0, slashIdx);
|
|
86
|
+
const modelId = value.slice(slashIdx + 1);
|
|
87
|
+
if (!provider || !modelId) return false;
|
|
88
|
+
const parts = modelId.split(":");
|
|
89
|
+
if (parts.length > 2) return false;
|
|
90
|
+
const [base, suffix] = parts;
|
|
91
|
+
if (!base) return false;
|
|
92
|
+
return suffix === undefined || ["minimal", "low", "medium", "high", "xhigh"].includes(suffix);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const ProfileModelSelectorSchema = z
|
|
96
|
+
.string()
|
|
97
|
+
.min(1)
|
|
98
|
+
.refine(value => isValidProfileModelSelector(value), {
|
|
99
|
+
message: "Expected provider/modelId with optional :effort suffix",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const ProfileModelMappingSchema = z.partialRecord(ProfileRoleSchema, ProfileModelSelectorSchema);
|
|
103
|
+
|
|
104
|
+
export const ProfileDefinitionSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
required_providers: z.array(z.string().min(1)).min(1),
|
|
107
|
+
model_mapping: ProfileModelMappingSchema,
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
|
|
111
|
+
export const ProfilesSchema = z.record(z.string().min(1), ProfileDefinitionSchema);
|
|
79
112
|
|
|
80
113
|
const ModelDefinitionSchema = z
|
|
81
114
|
.object({
|
|
@@ -205,7 +238,10 @@ export const ModelsConfigSchema = z
|
|
|
205
238
|
providers: z.record(z.string(), ProviderConfigSchema).optional(),
|
|
206
239
|
modelBindings: ModelBindingsSchema.optional(),
|
|
207
240
|
equivalence: EquivalenceConfigSchema.optional(),
|
|
241
|
+
profiles: ProfilesSchema.optional(),
|
|
208
242
|
})
|
|
209
243
|
.strict();
|
|
210
244
|
|
|
211
245
|
export type ModelsConfig = z.infer<typeof ModelsConfigSchema>;
|
|
246
|
+
export type ModelProfileConfig = z.infer<typeof ProfileDefinitionSchema>;
|
|
247
|
+
export type ModelProfilesConfig = z.infer<typeof ProfilesSchema>;
|
|
@@ -314,6 +314,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
314
314
|
disabledExtensions: { type: "array", default: DEFAULT_DISABLED_EXTENSIONS },
|
|
315
315
|
|
|
316
316
|
modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
|
|
317
|
+
"modelProfile.default": {
|
|
318
|
+
type: "string",
|
|
319
|
+
default: undefined,
|
|
320
|
+
ui: {
|
|
321
|
+
tab: "model",
|
|
322
|
+
label: "Default Model Profile",
|
|
323
|
+
description: "Model profile applied automatically at startup",
|
|
324
|
+
options: "runtime",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
317
327
|
|
|
318
328
|
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
|
|
319
329
|
|
|
@@ -161,12 +161,22 @@ gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<team evid
|
|
|
161
161
|
|
|
162
162
|
Workers do not own ultragoal goal state, do not create worker ultragoal ledgers, and do not checkpoint Ultragoal. Workers must not run `gjc ultragoal checkpoint`; checkpoint authority stays with the leader after worker tasks are terminal. Team launch remains explicit; Ultragoal does not auto-launch Team and performs no hidden goal mutation.
|
|
163
163
|
|
|
164
|
+
## Internal Ultragoal sub-skill fragments
|
|
165
|
+
|
|
166
|
+
The completion-gate cleanup sweep is driven by `ai-slop-cleaner`, an internal Ultragoal sub-skill bundled as a `kind: "skill-fragment"` prompt with parent skill `ultragoal` (installed at `skill-fragments/ultragoal/ai-slop-cleaner.md`). It is analogous to deep-interview's auto-research fragment: loaded on demand for one specific hook, never a user-facing skill.
|
|
167
|
+
|
|
168
|
+
- It is not slash-command discoverable, has no public skill-listing entry, and is never resolvable through `skill://`.
|
|
169
|
+
- It is a read-only detector+reporter over the active story's changed files only: it never edits code, writes files, mutates `.gjc/`, checkpoints, calls goal tools, or spawns workflows.
|
|
170
|
+
- It classifies every finding as blocking or advisory across the full taxonomy (fallback-like masking vs. grounded, duplication, dead code, needless abstraction, boundary violations, UI/design slop, missing tests).
|
|
171
|
+
- The leader and a leader-spawned `executor` own all fixes; the cleaner reruns until zero blocking findings remain. Advisory findings live in the gate report only.
|
|
172
|
+
- Recursion guard: it must not spawn nested `ralplan`/`team`/`deep-interview`/`ultragoal`; broad or architectural findings are handed back to the leader as review blockers.
|
|
173
|
+
|
|
164
174
|
## Mandatory completion cleanup and review gate
|
|
165
175
|
|
|
166
176
|
An ultragoal story cannot be checkpointed `complete` until the active agent has run the quality gate. The gate is plan-first, contract-driven, and surface-based:
|
|
167
177
|
|
|
168
178
|
1. Run targeted implementation verification for the story.
|
|
169
|
-
2. Run
|
|
179
|
+
2. Run the internal ai-slop-cleaner skill fragment as the final cleanup sweep on the story's changed files only, before verification and red-team so only clean code is reviewed. It is a read-only detector that emits an `AI SLOP CLEANUP REPORT`; if there are no relevant edits it still runs and records a passed/no-op report. Every BLOCKING cleaner finding is a completion blocker: the leader spawns an `executor` to fix blocking findings only, then reruns the cleaner until blocking findings are zero. Advisory findings are included in the gate report only and are not written to the Ultragoal ledger. Carry the report through the existing `qualityGate.iteration.evidence` field; do not add a new top-level quality-gate key.
|
|
170
180
|
3. Rerun verification after the cleaner pass.
|
|
171
181
|
4. Delegate an `architect` review covering all three lanes:
|
|
172
182
|
- architecture-side: system boundaries, layering, data/control flow, operational risks.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Ultragoal AI Slop Cleaner Fragment
|
|
2
|
+
|
|
3
|
+
You are the AI slop cleaner for the Ultragoal completion gate. This is an internal Ultragoal sub-skill, loaded on demand as a `kind: "skill-fragment"` prompt with parent skill `ultragoal`. It is never user-facing: not slash-command discoverable, no public skill listing entry, and never resolvable through `skill://`.
|
|
4
|
+
|
|
5
|
+
You are a **read-only detector and reporter**. You never edit code, write files, run formatters, mutate `.gjc/` state, checkpoint, call goal tools, or spawn workflows. You detect slop in the active Ultragoal story's changed files, classify each finding, and emit a report. The Ultragoal leader spawns an `executor` to fix BLOCKING findings; you do not fix anything yourself.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
- Inspect ONLY the active Ultragoal story's changed-files list. No broad rewrites, no inspection outside that scope, no new dependencies.
|
|
10
|
+
- Allow only narrow supporting reads needed to understand the contracts of changed files; if you need broader context, report that need to the leader instead of expanding scope.
|
|
11
|
+
- If there are no relevant edits, emit a passed/no-op report (`Gate Result: PASS`, `Changed Files Reviewed` listing the files as "no relevant edits").
|
|
12
|
+
- Recursion guard: you are already inside an Ultragoal workflow. Do NOT spawn nested `ralplan`, `team`, `deep-interview`, or `ultragoal` workflows. Broad, ambiguous, cross-layer, or architectural findings are handed to the leader as review blockers, not resolved here.
|
|
13
|
+
|
|
14
|
+
## Taxonomy
|
|
15
|
+
|
|
16
|
+
Classify every finding against the full taxonomy:
|
|
17
|
+
|
|
18
|
+
1. **Fallback-like code** — classify each as **masking fallback slop** or **grounded compatibility/fail-safe fallback**.
|
|
19
|
+
- Masking signals (blocking): swallowed errors, silent defaults, bypassed validation/tests, untested alternate execution paths, primary-contract suppression.
|
|
20
|
+
- Grounded signals (advisory): scoped to an external/version/fail-safe boundary, documented rationale, preserved failure evidence, and regression tests covering both primary and fallback behavior.
|
|
21
|
+
2. **Duplication** — repeated logic, copy-paste branches, redundant helpers.
|
|
22
|
+
3. **Dead code** — unused code, unreachable branches, stale flags, debug leftovers.
|
|
23
|
+
4. **Needless abstraction** — pass-through wrappers, speculative indirection, single-use helper layers.
|
|
24
|
+
5. **Boundary violations** — hidden coupling, leaky responsibilities, wrong-layer imports or side effects.
|
|
25
|
+
6. **UI/design slop** — context-sensitive signals, not absolute bans; preserve intentional brand/design-system/accessibility/product rationale. Signals: small Korean body copy (challenge 11-12px; Korean body text generally needs 14px+ unless a dense accessible system supports smaller), gratuitous shadows/depth, repetitive eyebrow+title+description scaffolding and filler/emoji badges, default blue/purple palettes (e.g. #3B82F6) without rationale, over-perfect uniform 3/4-column grids, and extreme "AI demo" gradients.
|
|
26
|
+
7. **Missing tests** — behavior not locked, weak regression coverage, missing edge/failure-mode cases.
|
|
27
|
+
|
|
28
|
+
## Blocking vs advisory
|
|
29
|
+
|
|
30
|
+
- **Blocking** if it can mask failures, violate accepted contracts, weaken boundaries, leave changed behavior untested, create maintenance traps, or make later verification unsafe.
|
|
31
|
+
- **Advisory** if it is nice-to-have, stylistic/contextual, or outside safe story scope.
|
|
32
|
+
- Advisory findings stay in the gate report only; they are NOT written to the Ultragoal ledger.
|
|
33
|
+
|
|
34
|
+
## Report
|
|
35
|
+
|
|
36
|
+
Emit exactly this text block with these mandated labels:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
AI SLOP CLEANUP REPORT
|
|
40
|
+
======================
|
|
41
|
+
|
|
42
|
+
Scope: [changed files inspected]
|
|
43
|
+
Mode: read-only detector/report; no edits performed
|
|
44
|
+
Blocking Findings: [none, or numbered findings with file, category, evidence, required executor fix]
|
|
45
|
+
Advisory Findings: [none, or numbered findings with file, category, evidence, why advisory]
|
|
46
|
+
Fallback Findings: [none, or finding -> masking fallback slop / grounded compatibility/fail-safe fallback -> blocking/advisory]
|
|
47
|
+
UI/Design Findings: [none/N/A, or signal -> blocking/advisory -> rationale]
|
|
48
|
+
Missing Test Findings: [none, or gap -> blocking/advisory -> required coverage]
|
|
49
|
+
Recursion Guard: [confirmed no nested ralplan/team/deep-interview/ultragoal spawned; broad findings handed to leader]
|
|
50
|
+
Changed Files Reviewed:
|
|
51
|
+
- [path] - [reviewed / no relevant edits]
|
|
52
|
+
|
|
53
|
+
Gate Result: PASS | BLOCKED
|
|
54
|
+
Leader Action:
|
|
55
|
+
- PASS: continue to verification, architect review, and executor red-team QA.
|
|
56
|
+
- BLOCKED: spawn executor to fix BLOCKING findings only, then rerun this sweep until Blocking Findings is none.
|
|
57
|
+
Remaining Risks:
|
|
58
|
+
- [none, or advisory/deferred risks]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Port the oh-my-codex taxonomy and report shape, not its editing workflow. Do not instruct yourself to execute cleanup passes — detect and report only.
|
|
@@ -7,6 +7,7 @@ import autoResearchGreenfieldFragment from "./gjc/skills/deep-interview/auto-res
|
|
|
7
7
|
import deepInterviewSkill from "./gjc/skills/deep-interview/SKILL.md" with { type: "text" };
|
|
8
8
|
import ralplanSkill from "./gjc/skills/ralplan/SKILL.md" with { type: "text" };
|
|
9
9
|
import teamSkill from "./gjc/skills/team/SKILL.md" with { type: "text" };
|
|
10
|
+
import aiSlopCleanerFragment from "./gjc/skills/ultragoal/ai-slop-cleaner.md" with { type: "text" };
|
|
10
11
|
import ultragoalSkill from "./gjc/skills/ultragoal/SKILL.md" with { type: "text" };
|
|
11
12
|
|
|
12
13
|
export const DEFAULT_GJC_DEFINITION_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
|
|
@@ -92,6 +93,12 @@ const DEFAULT_GJC_DEFINITIONS: readonly DefaultGjcDefinition[] = [
|
|
|
92
93
|
relativePath: "skill-fragments/deep-interview/auto-answer-uncertain.md",
|
|
93
94
|
content: autoAnswerUncertainFragment,
|
|
94
95
|
},
|
|
96
|
+
{
|
|
97
|
+
kind: "skill-fragment",
|
|
98
|
+
parentSkillName: "ultragoal",
|
|
99
|
+
relativePath: "skill-fragments/ultragoal/ai-slop-cleaner.md",
|
|
100
|
+
content: aiSlopCleanerFragment,
|
|
101
|
+
},
|
|
95
102
|
];
|
|
96
103
|
|
|
97
104
|
export function getDefaultGjcDefinitions(): readonly DefaultGjcDefinition[] {
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { syncSkillActiveState, type WorkflowHudSummary } from "../skill-state/active-state";
|
|
4
4
|
import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
|
|
5
|
+
import { WORKFLOW_STATE_VERSION, workflowStateStoragePath } from "../skill-state/workflow-state-contract";
|
|
5
6
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
6
7
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
7
8
|
import { renderUltragoalStatusMarkdown } from "./state-renderer";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
appendJsonl,
|
|
11
|
+
readExistingStateForMutation,
|
|
12
|
+
writeArtifact,
|
|
13
|
+
writeJsonAtomic,
|
|
14
|
+
writeWorkflowEnvelopeAtomic,
|
|
15
|
+
} from "./state-writer";
|
|
9
16
|
|
|
10
17
|
export type UltragoalGjcGoalMode = "aggregate" | "per-story";
|
|
11
18
|
export type UltragoalGoalStatus =
|
|
@@ -474,6 +481,92 @@ export function buildUltragoalHudSummary(
|
|
|
474
481
|
updatedAt: new Date().toISOString(),
|
|
475
482
|
});
|
|
476
483
|
}
|
|
484
|
+
function currentSessionId(): string | undefined {
|
|
485
|
+
const sessionId = process.env.GJC_SESSION_ID?.trim();
|
|
486
|
+
return sessionId || undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ultragoalModeStateFromSummary(
|
|
490
|
+
summary: UltragoalStatusSummary,
|
|
491
|
+
latestLedger: UltragoalLedgerEvent | undefined,
|
|
492
|
+
existing: Record<string, unknown> | undefined,
|
|
493
|
+
sessionId: string | undefined,
|
|
494
|
+
): Record<string, unknown> {
|
|
495
|
+
const updatedAt = new Date().toISOString();
|
|
496
|
+
return {
|
|
497
|
+
...(existing ?? {}),
|
|
498
|
+
skill: "ultragoal",
|
|
499
|
+
version: WORKFLOW_STATE_VERSION,
|
|
500
|
+
active: summary.exists && summary.status !== "complete",
|
|
501
|
+
current_phase: summary.status,
|
|
502
|
+
status: summary.status,
|
|
503
|
+
active_goal_id: summary.currentGoal?.id,
|
|
504
|
+
counts: summary.counts,
|
|
505
|
+
brief_path: summary.paths.briefPath,
|
|
506
|
+
ledger_path: summary.paths.ledgerPath,
|
|
507
|
+
goals_path: summary.paths.goalsPath,
|
|
508
|
+
latest_ledger_event: latestLedger?.event,
|
|
509
|
+
latest_ledger_event_id: latestLedger?.eventId,
|
|
510
|
+
updated_at: updatedAt,
|
|
511
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export async function syncUltragoalWorkflowState(cwd: string): Promise<void> {
|
|
516
|
+
const summary = await getUltragoalStatus(cwd);
|
|
517
|
+
const ledger = await readUltragoalLedger(cwd);
|
|
518
|
+
const latestLedger = ledger.at(-1);
|
|
519
|
+
const sessionId = currentSessionId();
|
|
520
|
+
const modeStateActive = summary.exists && summary.status !== "complete";
|
|
521
|
+
const syncModeState = async (targetSessionId: string | undefined): Promise<void> => {
|
|
522
|
+
const statePath = workflowStateStoragePath(cwd, "ultragoal", targetSessionId);
|
|
523
|
+
const existing = await readExistingStateForMutation(statePath);
|
|
524
|
+
if (existing.kind === "corrupt" && modeStateActive)
|
|
525
|
+
throw new Error(`Cannot sync corrupt ultragoal mode-state: ${existing.error}`);
|
|
526
|
+
const existingValue = existing.kind === "valid" ? existing.value : undefined;
|
|
527
|
+
await writeWorkflowEnvelopeAtomic(
|
|
528
|
+
statePath,
|
|
529
|
+
ultragoalModeStateFromSummary(summary, latestLedger, existingValue, targetSessionId),
|
|
530
|
+
{
|
|
531
|
+
cwd,
|
|
532
|
+
receipt: {
|
|
533
|
+
cwd,
|
|
534
|
+
skill: "ultragoal",
|
|
535
|
+
owner: "gjc-runtime",
|
|
536
|
+
command: "gjc ultragoal sync",
|
|
537
|
+
sessionId: targetSessionId,
|
|
538
|
+
},
|
|
539
|
+
audit: {
|
|
540
|
+
category: "state",
|
|
541
|
+
verb: "sync",
|
|
542
|
+
owner: "gjc-runtime",
|
|
543
|
+
skill: "ultragoal",
|
|
544
|
+
fromPhase: typeof existingValue?.current_phase === "string" ? existingValue.current_phase : undefined,
|
|
545
|
+
toPhase: summary.status,
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
};
|
|
550
|
+
await syncModeState(undefined);
|
|
551
|
+
if (sessionId) await syncModeState(sessionId);
|
|
552
|
+
await syncSkillActiveState({
|
|
553
|
+
cwd,
|
|
554
|
+
skill: "ultragoal",
|
|
555
|
+
active: summary.exists && summary.status !== "complete",
|
|
556
|
+
phase: summary.status,
|
|
557
|
+
sessionId: currentSessionId(),
|
|
558
|
+
hud: buildUltragoalHudSummary(summary, latestLedger),
|
|
559
|
+
source: "gjc-ultragoal",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function syncUltragoalWorkflowStateBestEffort(cwd: string): Promise<void> {
|
|
564
|
+
try {
|
|
565
|
+
await syncUltragoalWorkflowState(cwd);
|
|
566
|
+
} catch {
|
|
567
|
+
// HUD and mode-state sync are best-effort and must not change command semantics.
|
|
568
|
+
}
|
|
569
|
+
}
|
|
477
570
|
|
|
478
571
|
function clampTitle(title: string): string {
|
|
479
572
|
return title.length > 80 ? `${title.slice(0, 77)}...` : title;
|
|
@@ -1311,14 +1404,18 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1311
1404
|
try {
|
|
1312
1405
|
const command = commandName(args);
|
|
1313
1406
|
const json = hasFlag(args, "--json");
|
|
1407
|
+
let result: UltragoalCommandResult;
|
|
1314
1408
|
switch (command) {
|
|
1315
1409
|
case "status":
|
|
1316
|
-
|
|
1410
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1411
|
+
result = { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd), json) };
|
|
1412
|
+
break;
|
|
1317
1413
|
case "create":
|
|
1318
1414
|
case "create-goals": {
|
|
1319
1415
|
const mode = flagValue(args, "--gjc-goal-mode") === "per-story" ? "per-story" : "aggregate";
|
|
1320
1416
|
const plan = await createUltragoalPlan({ cwd, brief: await readBrief(cwd, args), gjcGoalMode: mode });
|
|
1321
|
-
|
|
1417
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1418
|
+
result = {
|
|
1322
1419
|
status: 0,
|
|
1323
1420
|
createdPlan: true,
|
|
1324
1421
|
stdout: json
|
|
@@ -1330,16 +1427,17 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1330
1427
|
})
|
|
1331
1428
|
: `Created ultragoal plan with ${plan.goals.length} goal${plan.goals.length === 1 ? "" : "s"} at ${getUltragoalPaths(cwd).goalsPath}.\n`,
|
|
1332
1429
|
};
|
|
1430
|
+
break;
|
|
1333
1431
|
}
|
|
1334
|
-
case "complete-goals":
|
|
1335
|
-
|
|
1432
|
+
case "complete-goals": {
|
|
1433
|
+
const handoff = await startNextUltragoalGoal({ cwd, retryFailed: hasFlag(args, "--retry-failed") });
|
|
1434
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1435
|
+
result = {
|
|
1336
1436
|
status: 0,
|
|
1337
|
-
stdout: renderCompleteHandoff(
|
|
1338
|
-
await startNextUltragoalGoal({ cwd, retryFailed: hasFlag(args, "--retry-failed") }),
|
|
1339
|
-
json,
|
|
1340
|
-
cwd,
|
|
1341
|
-
),
|
|
1437
|
+
stdout: renderCompleteHandoff(handoff, json, cwd),
|
|
1342
1438
|
};
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1343
1441
|
case "checkpoint": {
|
|
1344
1442
|
const goalId = flagValue(args, "--goal-id") ?? "";
|
|
1345
1443
|
const status = parseGoalStatus(flagValue(args, "--status"));
|
|
@@ -1352,8 +1450,9 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1352
1450
|
gjcGoalJson: flagValue(args, "--gjc-goal-json"),
|
|
1353
1451
|
qualityGateJson: flagValue(args, "--quality-gate-json"),
|
|
1354
1452
|
});
|
|
1453
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1355
1454
|
const goal = plan.goals.find(item => item.id === goalId);
|
|
1356
|
-
|
|
1455
|
+
result = {
|
|
1357
1456
|
status: 0,
|
|
1358
1457
|
stdout: json
|
|
1359
1458
|
? renderCliWriteReceipt({
|
|
@@ -1366,6 +1465,7 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1366
1465
|
})
|
|
1367
1466
|
: `ultragoal checkpoint goal-id=${goalId} status=${status}\n`,
|
|
1368
1467
|
};
|
|
1468
|
+
break;
|
|
1369
1469
|
}
|
|
1370
1470
|
case "steer": {
|
|
1371
1471
|
const kind = flagValue(args, "--kind");
|
|
@@ -1377,8 +1477,9 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1377
1477
|
evidence: flagValue(args, "--evidence") ?? "",
|
|
1378
1478
|
rationale: flagValue(args, "--rationale") ?? "",
|
|
1379
1479
|
});
|
|
1480
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1380
1481
|
const goal = plan.goals.at(-1);
|
|
1381
|
-
|
|
1482
|
+
result = {
|
|
1382
1483
|
status: 0,
|
|
1383
1484
|
stdout: json
|
|
1384
1485
|
? renderCliWriteReceipt({
|
|
@@ -1389,6 +1490,7 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1389
1490
|
})
|
|
1390
1491
|
: "Accepted add_subgoal steering.\n",
|
|
1391
1492
|
};
|
|
1493
|
+
break;
|
|
1392
1494
|
}
|
|
1393
1495
|
case "record-review-blockers": {
|
|
1394
1496
|
const plan = await recordUltragoalReviewBlockers({
|
|
@@ -1399,17 +1501,20 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
|
|
|
1399
1501
|
evidence: flagValue(args, "--evidence") ?? "",
|
|
1400
1502
|
gjcGoalJson: flagValue(args, "--gjc-goal-json"),
|
|
1401
1503
|
});
|
|
1504
|
+
await syncUltragoalWorkflowStateBestEffort(cwd);
|
|
1402
1505
|
const goal = plan.goals.at(-1);
|
|
1403
|
-
|
|
1506
|
+
result = {
|
|
1404
1507
|
status: 0,
|
|
1405
1508
|
stdout: json
|
|
1406
1509
|
? renderCliWriteReceipt({ ok: true, goal_id: goal?.id, goals_path: getUltragoalPaths(cwd).goalsPath })
|
|
1407
1510
|
: "Recorded review blockers.\n",
|
|
1408
1511
|
};
|
|
1512
|
+
break;
|
|
1409
1513
|
}
|
|
1410
1514
|
default:
|
|
1411
1515
|
return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
|
|
1412
1516
|
}
|
|
1517
|
+
return result;
|
|
1413
1518
|
} catch (error) {
|
|
1414
1519
|
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
1415
1520
|
}
|