@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +6 -0
  5. package/dist/types/config/model-profile-activation.d.ts +30 -0
  6. package/dist/types/config/model-profiles.d.ts +19 -0
  7. package/dist/types/config/model-registry.d.ts +8 -0
  8. package/dist/types/config/model-resolver.d.ts +1 -1
  9. package/dist/types/config/models-config-schema.d.ts +47 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  12. package/dist/types/main.d.ts +10 -1
  13. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  14. package/dist/types/modes/components/model-selector.d.ts +6 -1
  15. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/types.d.ts +1 -0
  18. package/dist/types/sdk.d.ts +1 -1
  19. package/dist/types/task/executor.d.ts +1 -0
  20. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  21. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  22. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  23. package/dist/types/tools/index.d.ts +4 -4
  24. package/package.json +7 -7
  25. package/src/cli/args.ts +10 -0
  26. package/src/commands/launch.ts +8 -0
  27. package/src/config/model-profile-activation.ts +157 -0
  28. package/src/config/model-profiles.ts +155 -0
  29. package/src/config/model-registry.ts +19 -0
  30. package/src/config/model-resolver.ts +3 -2
  31. package/src/config/models-config-schema.ts +36 -0
  32. package/src/config/settings-schema.ts +10 -0
  33. package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
  34. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  35. package/src/defaults/gjc-defaults.ts +7 -0
  36. package/src/gjc-runtime/ultragoal-runtime.ts +119 -14
  37. package/src/internal-urls/docs-index.generated.ts +6 -9
  38. package/src/main.ts +67 -1
  39. package/src/modes/components/custom-provider-wizard.ts +318 -0
  40. package/src/modes/components/model-selector.ts +108 -18
  41. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  42. package/src/modes/controllers/selector-controller.ts +57 -1
  43. package/src/modes/types.ts +1 -0
  44. package/src/prompts/memories/consolidation.md +1 -1
  45. package/src/prompts/memories/read-path.md +6 -7
  46. package/src/prompts/memories/unavailable.md +2 -2
  47. package/src/prompts/tools/bash.md +1 -1
  48. package/src/prompts/tools/irc.md +1 -1
  49. package/src/prompts/tools/read.md +2 -2
  50. package/src/prompts/tools/recall.md +1 -0
  51. package/src/prompts/tools/reflect.md +1 -0
  52. package/src/prompts/tools/retain.md +1 -0
  53. package/src/sdk.ts +1 -1
  54. package/src/slash-commands/builtin-registry.ts +1 -1
  55. package/src/task/executor.ts +2 -0
  56. package/src/task/index.ts +2 -0
  57. package/src/tools/hindsight-recall.ts +0 -2
  58. package/src/tools/hindsight-reflect.ts +0 -2
  59. package/src/tools/hindsight-retain.ts +0 -2
  60. package/src/tools/index.ts +4 -18
  61. 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 a cleanup/refactor review pass on changed files only; if there are no relevant edits, the cleaner still runs and records a passed/no-op report.
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 { WorkflowHudSummary } from "../skill-state/active-state";
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 { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
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
- return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd), json) };
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
- return {
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
- return {
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
- return {
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
- return {
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
- return {
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
  }