@gajae-code/coding-agent 0.3.1 → 0.4.0

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 (166) hide show
  1. package/CHANGELOG.md +46 -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 +25 -10
  8. package/dist/types/config/model-resolver.d.ts +1 -1
  9. package/dist/types/config/models-config-schema.d.ts +84 -0
  10. package/dist/types/config/settings-schema.d.ts +15 -0
  11. package/dist/types/edit/diff.d.ts +16 -0
  12. package/dist/types/edit/modes/replace.d.ts +7 -0
  13. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  16. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  17. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  18. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  19. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  20. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  23. package/dist/types/extensibility/skills.d.ts +9 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  25. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  26. package/dist/types/lsp/client.d.ts +1 -0
  27. package/dist/types/main.d.ts +10 -1
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  29. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  30. package/dist/types/modes/components/model-selector.d.ts +6 -1
  31. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  34. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  35. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  36. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  37. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  38. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  39. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  40. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  41. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  42. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  43. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  44. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  45. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  46. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  47. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  49. package/dist/types/modes/theme/theme.d.ts +2 -1
  50. package/dist/types/modes/types.d.ts +1 -0
  51. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  52. package/dist/types/sdk.d.ts +8 -1
  53. package/dist/types/session/agent-session.d.ts +10 -0
  54. package/dist/types/session/blob-store.d.ts +17 -0
  55. package/dist/types/session/messages.d.ts +3 -0
  56. package/dist/types/session/session-storage.d.ts +6 -0
  57. package/dist/types/skill-state/active-state.d.ts +13 -0
  58. package/dist/types/task/executor.d.ts +1 -0
  59. package/dist/types/thinking.d.ts +3 -2
  60. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  61. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  62. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  63. package/dist/types/tools/index.d.ts +7 -4
  64. package/package.json +9 -7
  65. package/src/cli/args.ts +10 -0
  66. package/src/cli.ts +14 -0
  67. package/src/commands/harness.ts +192 -7
  68. package/src/commands/launch.ts +8 -0
  69. package/src/commands/ultragoal.ts +1 -21
  70. package/src/config/model-equivalence.ts +1 -1
  71. package/src/config/model-profile-activation.ts +157 -0
  72. package/src/config/model-profiles.ts +155 -0
  73. package/src/config/model-registry.ts +51 -5
  74. package/src/config/model-resolver.ts +3 -2
  75. package/src/config/models-config-schema.ts +42 -1
  76. package/src/config/settings-schema.ts +14 -1
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
  78. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  79. package/src/defaults/gjc-defaults.ts +7 -0
  80. package/src/discovery/claude-plugins.ts +25 -5
  81. package/src/edit/diff.ts +64 -1
  82. package/src/edit/modes/replace.ts +60 -2
  83. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  84. package/src/extensibility/gjc-plugins/index.ts +9 -0
  85. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  86. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  88. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  89. package/src/extensibility/gjc-plugins/state.ts +29 -0
  90. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  91. package/src/extensibility/gjc-plugins/types.ts +97 -0
  92. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  93. package/src/extensibility/skills.ts +39 -7
  94. package/src/gjc-runtime/state-runtime.ts +93 -2
  95. package/src/gjc-runtime/state-writer.ts +17 -1
  96. package/src/gjc-runtime/ultragoal-runtime.ts +62 -2
  97. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  98. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  99. package/src/harness-control-plane/storage.ts +144 -2
  100. package/src/hashline/hash.ts +23 -0
  101. package/src/hooks/skill-state.ts +2 -0
  102. package/src/internal-urls/docs-index.generated.ts +8 -11
  103. package/src/lsp/client.ts +7 -0
  104. package/src/main.ts +67 -1
  105. package/src/modes/acp/acp-agent.ts +25 -2
  106. package/src/modes/bridge/bridge-mode.ts +124 -2
  107. package/src/modes/components/custom-provider-wizard.ts +318 -0
  108. package/src/modes/components/model-selector.ts +108 -18
  109. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  110. package/src/modes/controllers/input-controller.ts +14 -2
  111. package/src/modes/controllers/selector-controller.ts +57 -1
  112. package/src/modes/prompt-action-autocomplete.ts +49 -10
  113. package/src/modes/rpc/rpc-client.ts +57 -3
  114. package/src/modes/rpc/rpc-mode.ts +67 -0
  115. package/src/modes/rpc/rpc-types.ts +224 -2
  116. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  117. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  118. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  119. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  120. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  121. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  122. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  123. package/src/modes/shared/agent-wire/responses.ts +2 -2
  124. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  125. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  126. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  127. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  128. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  129. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  130. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  131. package/src/modes/theme/theme.ts +6 -0
  132. package/src/modes/types.ts +1 -0
  133. package/src/prompts/memories/consolidation.md +1 -1
  134. package/src/prompts/memories/read-path.md +6 -7
  135. package/src/prompts/memories/unavailable.md +2 -2
  136. package/src/prompts/tools/bash.md +1 -1
  137. package/src/prompts/tools/irc.md +1 -1
  138. package/src/prompts/tools/read.md +2 -2
  139. package/src/prompts/tools/recall.md +1 -0
  140. package/src/prompts/tools/reflect.md +1 -0
  141. package/src/prompts/tools/retain.md +1 -0
  142. package/src/runtime-mcp/client.ts +7 -4
  143. package/src/runtime-mcp/manager.ts +45 -13
  144. package/src/runtime-mcp/transports/http.ts +40 -14
  145. package/src/runtime-mcp/transports/stdio.ts +11 -10
  146. package/src/sdk.ts +48 -1
  147. package/src/session/agent-session.ts +211 -2
  148. package/src/session/blob-store.ts +84 -0
  149. package/src/session/messages.ts +3 -0
  150. package/src/session/session-manager.ts +390 -33
  151. package/src/session/session-storage.ts +26 -0
  152. package/src/setup/provider-onboarding.ts +2 -2
  153. package/src/skill-state/active-state.ts +89 -1
  154. package/src/slash-commands/builtin-registry.ts +1 -1
  155. package/src/task/discovery.ts +7 -1
  156. package/src/task/executor.ts +18 -2
  157. package/src/task/index.ts +2 -0
  158. package/src/thinking.ts +8 -2
  159. package/src/tools/ask.ts +39 -9
  160. package/src/tools/hindsight-recall.ts +0 -2
  161. package/src/tools/hindsight-reflect.ts +0 -2
  162. package/src/tools/hindsight-retain.ts +0 -2
  163. package/src/tools/index.ts +7 -18
  164. package/src/tools/read.ts +3 -3
  165. package/src/tools/skill.ts +15 -3
  166. package/src/utils/edit-mode.ts +1 -1
@@ -0,0 +1,79 @@
1
+ import { GJC_PLUGIN_KIND, GjcPluginLoadError, type GjcPluginManifest, type SubskillFrontmatter } from "./types";
2
+
3
+ const FORBIDDEN_MANIFEST_KEYS = ["skills", "slash-commands", "commands", "hooks", "mcp", "mcpServers", "agents"];
4
+
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ function requireNonEmptyString(value: unknown, field: string, filePath: string): string {
10
+ if (typeof value !== "string" || value.trim().length === 0) {
11
+ throw new GjcPluginLoadError(
12
+ "invalid_frontmatter",
13
+ `Invalid sub-skill frontmatter in ${filePath}: ${field} must be a non-empty string`,
14
+ );
15
+ }
16
+ return value;
17
+ }
18
+
19
+ function requireStringArray(value: unknown, field: string, manifestPath: string): string[] {
20
+ if (!Array.isArray(value) || !value.every(item => typeof item === "string")) {
21
+ throw new GjcPluginLoadError(
22
+ "invalid_manifest",
23
+ `Invalid GJC plugin manifest at ${manifestPath}: ${field} must be a string array`,
24
+ );
25
+ }
26
+ return [...value];
27
+ }
28
+
29
+ export function parseManifest(raw: unknown, manifestPath: string): GjcPluginManifest {
30
+ if (!isRecord(raw)) {
31
+ throw new GjcPluginLoadError(
32
+ "invalid_manifest",
33
+ `Invalid GJC plugin manifest at ${manifestPath}: expected object`,
34
+ );
35
+ }
36
+
37
+ for (const key of FORBIDDEN_MANIFEST_KEYS) {
38
+ if (Object.hasOwn(raw, key)) {
39
+ throw new GjcPluginLoadError("forbidden_surface", `Forbidden GJC plugin surface in ${manifestPath}: ${key}`);
40
+ }
41
+ }
42
+
43
+ if (raw.kind !== GJC_PLUGIN_KIND) {
44
+ throw new GjcPluginLoadError(
45
+ "invalid_kind",
46
+ `Invalid GJC plugin kind in ${manifestPath}: expected ${GJC_PLUGIN_KIND}`,
47
+ );
48
+ }
49
+ if (typeof raw.name !== "string" || raw.name.trim().length === 0) {
50
+ throw new GjcPluginLoadError(
51
+ "invalid_manifest",
52
+ `Invalid GJC plugin manifest at ${manifestPath}: name must be a non-empty string`,
53
+ );
54
+ }
55
+ if (typeof raw.version !== "string" || raw.version.trim().length === 0) {
56
+ throw new GjcPluginLoadError(
57
+ "invalid_manifest",
58
+ `Invalid GJC plugin manifest at ${manifestPath}: version must be a non-empty string`,
59
+ );
60
+ }
61
+
62
+ return {
63
+ name: raw.name,
64
+ version: raw.version,
65
+ kind: GJC_PLUGIN_KIND,
66
+ subskills: requireStringArray(raw.subskills, "subskills", manifestPath),
67
+ tools: requireStringArray(raw.tools, "tools", manifestPath),
68
+ };
69
+ }
70
+
71
+ export function parseSubskillFrontmatter(fm: Record<string, unknown>, filePath: string): SubskillFrontmatter {
72
+ return {
73
+ name: requireNonEmptyString(fm.name, "name", filePath),
74
+ binds_to: requireNonEmptyString(fm.binds_to, "binds_to", filePath),
75
+ phase: requireNonEmptyString(fm.phase, "phase", filePath),
76
+ activation_arg: requireNonEmptyString(fm.activation_arg, "activation_arg", filePath),
77
+ description: requireNonEmptyString(fm.description, "description", filePath),
78
+ };
79
+ }
@@ -0,0 +1,29 @@
1
+ import type { ActiveSubskillEntry } from "../../skill-state/active-state";
2
+ import { readVisibleSkillActiveState } from "../../skill-state/active-state";
3
+ import type { LoadedSubskillActivation } from "./types";
4
+
5
+ export function toActiveSubskillEntry(activation: LoadedSubskillActivation): ActiveSubskillEntry {
6
+ return {
7
+ plugin: activation.plugin,
8
+ subskillName: activation.subskillName,
9
+ parent: activation.parent,
10
+ bindsTo: activation.bindsTo,
11
+ phase: activation.phase,
12
+ activationArg: activation.activationArg,
13
+ filePath: activation.filePath,
14
+ toolPaths: activation.toolPaths,
15
+ };
16
+ }
17
+
18
+ export async function readActiveSubskillsForParent(input: {
19
+ cwd: string;
20
+ sessionId?: string;
21
+ parent: string;
22
+ phase: string;
23
+ }): Promise<ActiveSubskillEntry[]> {
24
+ const state = await readVisibleSkillActiveState(input.cwd, input.sessionId);
25
+ const parent = input.parent.trim();
26
+ const phase = input.phase.trim();
27
+ if (!state || !parent || !phase) return [];
28
+ return (state.active_subskills ?? []).filter(entry => entry.parent === parent && entry.phase === phase);
29
+ }
@@ -0,0 +1,47 @@
1
+ import { logger } from "@gajae-code/utils";
2
+ import { loadCustomTools } from "../custom-tools/loader";
3
+ import type { CustomTool } from "../custom-tools/types";
4
+ import { readActiveSubskillsForParent } from "./state";
5
+
6
+ export async function loadActiveSubskillTools(input: {
7
+ cwd: string;
8
+ sessionId?: string;
9
+ parent: string;
10
+ phase: string;
11
+ reservedToolNames?: string[];
12
+ }): Promise<CustomTool[]> {
13
+ const entries = await readActiveSubskillsForParent(input);
14
+ const toolPaths = [
15
+ ...new Set(entries.flatMap(entry => entry.toolPaths ?? []).filter(path => path.trim().length > 0)),
16
+ ];
17
+ if (toolPaths.length === 0) return [];
18
+
19
+ const reservedToolNames = new Set(input.reservedToolNames ?? []);
20
+ const result = await loadCustomTools(
21
+ toolPaths.map(path => ({ path })),
22
+ input.cwd,
23
+ input.reservedToolNames ?? [],
24
+ );
25
+
26
+ for (const error of result.errors) {
27
+ logger.warn("Skipping GJC plugin sub-skill tool", { path: error.path, error: error.error });
28
+ }
29
+
30
+ const tools: CustomTool[] = [];
31
+ const seenNames = new Set<string>();
32
+ for (const loadedTool of result.tools) {
33
+ const name = loadedTool.tool.name;
34
+ if (reservedToolNames.has(name)) {
35
+ logger.warn("Skipping GJC plugin sub-skill tool name because it conflicts with a reserved tool", { name });
36
+ continue;
37
+ }
38
+ if (seenNames.has(name)) {
39
+ logger.warn("Skipping duplicate GJC plugin sub-skill tool name", { name, path: loadedTool.path });
40
+ continue;
41
+ }
42
+ seenNames.add(name);
43
+ tools.push(loadedTool.tool);
44
+ }
45
+
46
+ return tools;
47
+ }
@@ -0,0 +1,97 @@
1
+ import type { CanonicalGjcWorkflowSkill } from "../../skill-state/active-state";
2
+ import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../../skill-state/active-state";
3
+
4
+ export const GJC_PLUGIN_MANIFEST_FILENAME = "gajae-plugin.json";
5
+ export const GJC_PLUGIN_KIND = "gajae-code-plugin";
6
+
7
+ export const GJC_SUBSKILL_PARENT_SKILLS = CANONICAL_GJC_WORKFLOW_SKILLS;
8
+ export type GjcSubskillParentSkill = CanonicalGjcWorkflowSkill;
9
+
10
+ export const GJC_SUBSKILL_PARENT_AGENTS = ["executor", "architect", "planner", "critic"] as const;
11
+ export type GjcSubskillParentAgent = (typeof GJC_SUBSKILL_PARENT_AGENTS)[number];
12
+
13
+ export type GjcSubskillParent = GjcSubskillParentSkill | GjcSubskillParentAgent;
14
+
15
+ export const GJC_AGENT_SUBSKILL_PHASES: Record<GjcSubskillParentAgent, string[]> = {
16
+ executor: ["prompt"],
17
+ architect: ["prompt"],
18
+ planner: ["prompt"],
19
+ critic: ["prompt"],
20
+ };
21
+
22
+ export interface GjcPluginManifest {
23
+ name: string;
24
+ version: string;
25
+ kind: "gajae-code-plugin";
26
+ subskills: string[];
27
+ tools: string[];
28
+ }
29
+
30
+ export interface SubskillFrontmatter {
31
+ name: string;
32
+ binds_to: string;
33
+ phase: string;
34
+ activation_arg: string;
35
+ description: string;
36
+ }
37
+
38
+ export interface LoadedSubskillBinding {
39
+ plugin: string;
40
+ subskillName: string;
41
+ parent: string;
42
+ bindsTo: string;
43
+ phase: string;
44
+ activationArg: string;
45
+ description: string;
46
+ filePath: string;
47
+ body: string;
48
+ toolPaths: string[];
49
+ }
50
+
51
+ export interface LoadedSubskillActivation {
52
+ activationArg: string;
53
+ plugin: string;
54
+ subskillName: string;
55
+ parent: string;
56
+ bindsTo: string;
57
+ phase: string;
58
+ filePath: string;
59
+ toolPaths: string[];
60
+ }
61
+
62
+ export interface PhaseScopedToolBinding {
63
+ plugin: string;
64
+ parent: string;
65
+ phase: string;
66
+ toolPath: string;
67
+ }
68
+
69
+ export interface LoadedGjcPlugin {
70
+ name: string;
71
+ version: string;
72
+ root: string;
73
+ manifestPath: string;
74
+ bindings: LoadedSubskillBinding[];
75
+ toolBindings: PhaseScopedToolBinding[];
76
+ }
77
+
78
+ export type GjcPluginLoadErrorCode =
79
+ | "forbidden_surface"
80
+ | "invalid_manifest"
81
+ | "invalid_frontmatter"
82
+ | "invalid_parent"
83
+ | "invalid_phase"
84
+ | "duplicate_arg"
85
+ | "duplicate_parent_phase"
86
+ | "missing_file"
87
+ | "invalid_kind";
88
+
89
+ export class GjcPluginLoadError extends Error {
90
+ readonly code: GjcPluginLoadErrorCode;
91
+
92
+ constructor(code: GjcPluginLoadErrorCode, message: string, options?: ErrorOptions) {
93
+ super(message, options);
94
+ this.name = "GjcPluginLoadError";
95
+ this.code = code;
96
+ }
97
+ }
@@ -0,0 +1,76 @@
1
+ import { isKnownWorkflowState } from "../../gjc-runtime/workflow-manifest";
2
+ import type { CanonicalGjcWorkflowSkill } from "../../skill-state/active-state";
3
+ import {
4
+ GJC_AGENT_SUBSKILL_PHASES,
5
+ GJC_SUBSKILL_PARENT_AGENTS,
6
+ GJC_SUBSKILL_PARENT_SKILLS,
7
+ GjcPluginLoadError,
8
+ type GjcSubskillParentAgent,
9
+ type LoadedSubskillBinding,
10
+ type SubskillFrontmatter,
11
+ } from "./types";
12
+
13
+ function isParentSkill(value: string): value is CanonicalGjcWorkflowSkill {
14
+ return (GJC_SUBSKILL_PARENT_SKILLS as readonly string[]).includes(value);
15
+ }
16
+
17
+ function isParentAgent(value: string): value is GjcSubskillParentAgent {
18
+ return (GJC_SUBSKILL_PARENT_AGENTS as readonly string[]).includes(value);
19
+ }
20
+
21
+ export function validateBinding(fm: SubskillFrontmatter): void {
22
+ const parent = fm.binds_to;
23
+ if (isParentSkill(parent)) {
24
+ if (!isKnownWorkflowState(parent, fm.phase)) {
25
+ throw new GjcPluginLoadError("invalid_phase", `Invalid GJC sub-skill phase for ${parent}: ${fm.phase}`);
26
+ }
27
+ return;
28
+ }
29
+
30
+ if (isParentAgent(parent)) {
31
+ if (!GJC_AGENT_SUBSKILL_PHASES[parent].includes(fm.phase)) {
32
+ throw new GjcPluginLoadError("invalid_phase", `Invalid GJC sub-skill phase for ${parent}: ${fm.phase}`);
33
+ }
34
+ return;
35
+ }
36
+
37
+ throw new GjcPluginLoadError("invalid_parent", `Invalid GJC sub-skill parent: ${parent}`);
38
+ }
39
+
40
+ export function buildParentArgMap(
41
+ bindings: readonly LoadedSubskillBinding[],
42
+ ): Map<string, Map<string, LoadedSubskillBinding>> {
43
+ const byParent = new Map<string, Map<string, LoadedSubskillBinding>>();
44
+ for (const binding of bindings) {
45
+ let byArg = byParent.get(binding.parent);
46
+ if (!byArg) {
47
+ byArg = new Map<string, LoadedSubskillBinding>();
48
+ byParent.set(binding.parent, byArg);
49
+ }
50
+ const existing = byArg.get(binding.activationArg);
51
+ if (existing) {
52
+ throw new GjcPluginLoadError(
53
+ "duplicate_arg",
54
+ `Duplicate GJC sub-skill activation_arg for ${binding.parent}: ${binding.activationArg} (${existing.filePath}, ${binding.filePath})`,
55
+ );
56
+ }
57
+ byArg.set(binding.activationArg, binding);
58
+ }
59
+ return byParent;
60
+ }
61
+
62
+ export function buildParentPhaseSet(bindings: readonly LoadedSubskillBinding[]): Set<string> {
63
+ const seen = new Map<string, LoadedSubskillBinding>();
64
+ for (const binding of bindings) {
65
+ const key = `${binding.parent}\u0000${binding.phase}`;
66
+ const existing = seen.get(key);
67
+ if (existing) {
68
+ throw new GjcPluginLoadError(
69
+ "duplicate_parent_phase",
70
+ `Duplicate GJC sub-skill parent/phase binding for ${binding.parent}/${binding.phase} (${existing.filePath}, ${binding.filePath})`,
71
+ );
72
+ }
73
+ seen.set(key, binding);
74
+ }
75
+ return new Set(seen.keys());
76
+ }
@@ -8,6 +8,8 @@ import { type Skill as CapabilitySkill, loadCapability } from "../discovery";
8
8
  import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
9
9
  import type { SkillPromptDetails } from "../session/messages";
10
10
  import { expandTilde } from "../tools/path-utils";
11
+ import type { LoadedSubskillActivation } from "./gjc-plugins";
12
+ import { buildSubskillInjection } from "./gjc-plugins/injection";
11
13
  export interface Skill {
12
14
  name: string;
13
15
  description: string;
@@ -280,6 +282,14 @@ export interface BuiltSkillPromptMessage {
280
282
  details: SkillPromptDetails;
281
283
  }
282
284
 
285
+ export interface BuildSkillPromptMessageContext {
286
+ subskillActivation?: LoadedSubskillActivation;
287
+ subskillActivationSet?: LoadedSubskillActivation[];
288
+ currentPhase?: string;
289
+ cwd?: string;
290
+ sessionId?: string;
291
+ }
292
+
283
293
  export function getSkillSlashCommandName(skill: Pick<Skill, "name">): string {
284
294
  return `skill:${skill.name}`;
285
295
  }
@@ -370,6 +380,7 @@ export function resolveSkillSlashCommands(
370
380
  export async function buildSkillPromptMessage(
371
381
  skill: Pick<Skill, "name" | "filePath" | "content">,
372
382
  args: string,
383
+ context?: BuildSkillPromptMessageContext,
373
384
  ): Promise<BuiltSkillPromptMessage> {
374
385
  const content = typeof skill.content === "string" ? skill.content : await Bun.file(skill.filePath).text();
375
386
  const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
@@ -378,14 +389,35 @@ export async function buildSkillPromptMessage(
378
389
  if (trimmedArgs) {
379
390
  metaLines.push(`User: ${trimmedArgs}`);
380
391
  }
381
- const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
392
+ let message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
393
+ const details: SkillPromptDetails = {
394
+ name: skill.name,
395
+ path: skill.filePath,
396
+ args: trimmedArgs || undefined,
397
+ lineCount: body ? body.split("\n").length : 0,
398
+ };
399
+ if (context?.subskillActivationSet) {
400
+ details.subskillActivationSet = context.subskillActivationSet;
401
+ }
402
+ if (context) {
403
+ const injection = context.cwd
404
+ ? await buildSubskillInjection({
405
+ cwd: context.cwd,
406
+ sessionId: context.sessionId,
407
+ skillName: skill.name,
408
+ activation: context.subskillActivation,
409
+ currentPhase: context.currentPhase,
410
+ })
411
+ : null;
412
+ if (injection) {
413
+ message += injection.block;
414
+ details.subskillActivation = injection.details ?? context.subskillActivation;
415
+ } else if (context.subskillActivation) {
416
+ details.subskillActivation = context.subskillActivation;
417
+ }
418
+ }
382
419
  return {
383
420
  message,
384
- details: {
385
- name: skill.name,
386
- path: skill.filePath,
387
- args: trimmedArgs || undefined,
388
- lineCount: body ? body.split("\n").length : 0,
389
- },
421
+ details,
390
422
  };
391
423
  }
@@ -22,6 +22,7 @@ import {
22
22
  canonicalWorkflowSkill,
23
23
  describeWorkflowStateContract,
24
24
  WORKFLOW_STATE_VERSION,
25
+ type WorkflowStateMutationOwner,
25
26
  type WorkflowStateReceipt,
26
27
  } from "../skill-state/workflow-state-contract";
27
28
  import { renderCliWriteReceipt } from "./cli-write-receipt";
@@ -686,13 +687,14 @@ async function writeJsonAtomic(
686
687
  cwd: string,
687
688
  filePath: string,
688
689
  value: unknown,
689
- verb: "write" | "clear" | "handoff" = "write",
690
+ verb: "write" | "clear" | "handoff" | "reconcile" = "write",
690
691
  options?: {
691
692
  skill?: CanonicalGjcWorkflowSkill;
692
693
  mutationId?: string;
693
694
  force?: boolean;
694
695
  fromPhase?: string;
695
696
  toPhase?: string;
697
+ owner?: WorkflowStateMutationOwner;
696
698
  },
697
699
  ): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
698
700
  const warning = options?.skill
@@ -709,7 +711,7 @@ async function writeJsonAtomic(
709
711
  audit: {
710
712
  category: "state",
711
713
  verb,
712
- owner: "gjc-state-cli",
714
+ owner: options?.owner ?? "gjc-state-cli",
713
715
  skill: options?.skill,
714
716
  mutationId: options?.mutationId,
715
717
  fromPhase: options?.fromPhase,
@@ -957,6 +959,95 @@ async function syncWorkflowSkillState(options: {
957
959
  // HUD sync is best-effort and must not change command semantics.
958
960
  }
959
961
  }
962
+
963
+ /**
964
+ * Reconcile a workflow skill's mode-state + active-state/HUD from a caller-derived
965
+ * payload. Unlike `gjc state write`, this is a derived repair: callers reconcile from
966
+ * an authoritative source (e.g. the ultragoal plan/ledger), where intermediate
967
+ * aggregate phases like ultragoal `active -> pending` are legitimate, so it bypasses
968
+ * ONLY verb transition-edge validation while preserving schema validation,
969
+ * unknown-phase rejection, version/checksum stamping, and audit/out-of-band tamper
970
+ * detection. Receipts carry `owner: "gjc-runtime"` and `verb: "reconcile"` so the
971
+ * provenance is distinguishable from a user-initiated write.
972
+ */
973
+ export async function reconcileWorkflowSkillState(options: {
974
+ cwd: string;
975
+ mode: CanonicalGjcWorkflowSkill;
976
+ sessionId: string | undefined;
977
+ threadId?: string;
978
+ turnId?: string;
979
+ active: boolean;
980
+ phase: string;
981
+ payload: Record<string, unknown>;
982
+ }): Promise<{ stateFile: string }> {
983
+ const { cwd, mode, sessionId, threadId, turnId, active, payload } = options;
984
+ const filePath = modeStateFile(cwd, mode, sessionId);
985
+ const existingRead = await readExistingStateForMutation(filePath);
986
+ const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
987
+ const nowIsoStr = nowIso();
988
+ const mutationId = `${mode}:reconcile:${nowIsoStr}`;
989
+
990
+ const trimmedPhase = options.phase.trim();
991
+ const manifestStates = new Set(getSkillManifest(mode).states.map(state => state.id));
992
+ if (!manifestStates.has(trimmedPhase)) {
993
+ throw new StateCommandError(2, `unknown ${mode} phase "${trimmedPhase}" for reconciliation`);
994
+ }
995
+
996
+ const fromPhase =
997
+ typeof existingPayload.current_phase === "string" ? existingPayload.current_phase.trim() : undefined;
998
+ const receipt = buildWorkflowStateReceipt({
999
+ cwd,
1000
+ skill: mode,
1001
+ owner: "gjc-runtime",
1002
+ command: `gjc ${mode} (reconcile)`,
1003
+ sessionId,
1004
+ nowIso: nowIsoStr,
1005
+ mutationId,
1006
+ });
1007
+ receipt.verb = "reconcile";
1008
+ receipt.forced = true;
1009
+ receipt.from_phase = fromPhase;
1010
+ receipt.to_phase = trimmedPhase;
1011
+
1012
+ const merged = mergeWithNullDelete(existingPayload, payload);
1013
+ merged.skill = mode;
1014
+ merged.current_phase = trimmedPhase;
1015
+ merged.active = active;
1016
+ merged.version = WORKFLOW_STATE_VERSION;
1017
+ merged.updated_at = nowIsoStr;
1018
+ merged.receipt = receipt;
1019
+ if (sessionId && typeof merged.session_id !== "string") merged.session_id = sessionId;
1020
+
1021
+ const validation = validateWorkflowStateEnvelope(mode, merged);
1022
+ if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1023
+
1024
+ await writeJsonAtomic(cwd, filePath, merged, "reconcile", {
1025
+ skill: mode,
1026
+ mutationId,
1027
+ force: true,
1028
+ fromPhase,
1029
+ toPhase: trimmedPhase,
1030
+ owner: "gjc-runtime",
1031
+ });
1032
+
1033
+ // Reconciliation drives the active-state/HUD update directly (not via the
1034
+ // best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
1035
+ // is surfaced to the caller and recorded as a reconcile failure, rather than
1036
+ // silently leaving a stale chip behind a freshly reconciled mode-state.
1037
+ await syncSkillActiveState({
1038
+ cwd,
1039
+ skill: mode,
1040
+ active,
1041
+ phase: trimmedPhase,
1042
+ sessionId,
1043
+ threadId,
1044
+ turnId,
1045
+ source: "gjc-runtime-reconcile",
1046
+ hud: buildHudForMode(mode, merged),
1047
+ receipt,
1048
+ });
1049
+ return { stateFile: filePath };
1050
+ }
960
1051
  export async function readWorkflowStateJson(
961
1052
  cwd: string,
962
1053
  skill: CanonicalGjcWorkflowSkill,
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import type { SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
4
+ import type { ActiveSubskillEntry, SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
5
5
  import {
6
6
  type AuditEntry,
7
7
  buildWorkflowStateReceipt,
@@ -275,6 +275,21 @@ function activeEntryPath(cwd: string, sessionScope: string | ActiveSessionScope
275
275
  return path.join(activeStateDir(cwd, sessionScope), `${encodePathSegment(normalizedSkill)}.json`);
276
276
  }
277
277
 
278
+ function activeSubskillKey(entry: ActiveSubskillEntry): string {
279
+ return `${entry.parent}::${entry.phase}::${entry.activationArg}`;
280
+ }
281
+
282
+ function flattenActiveSubskills(entries: SkillActiveEntry[]): ActiveSubskillEntry[] {
283
+ const deduped = new Map<string, ActiveSubskillEntry>();
284
+ for (const entry of entries) {
285
+ if (entry.active === false || !Array.isArray(entry.active_subskills)) continue;
286
+ for (const subskill of entry.active_subskills) {
287
+ deduped.set(activeSubskillKey(subskill), subskill);
288
+ }
289
+ }
290
+ return [...deduped.values()];
291
+ }
292
+
278
293
  function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
279
294
  const visible = entries.filter(entry => entry.active !== false);
280
295
  const primary = visible[0];
@@ -288,6 +303,7 @@ function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
288
303
  thread_id: primary?.thread_id,
289
304
  turn_id: primary?.turn_id,
290
305
  active_skills: entries,
306
+ active_subskills: flattenActiveSubskills(visible),
291
307
  };
292
308
  }
293
309
 
@@ -5,6 +5,7 @@ import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "..
5
5
  import { renderCliWriteReceipt } from "./cli-write-receipt";
6
6
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
7
7
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
8
+ import { reconcileWorkflowSkillState } from "./state-runtime";
8
9
  import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
9
10
 
10
11
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
@@ -474,7 +475,6 @@ export function buildUltragoalHudSummary(
474
475
  updatedAt: new Date().toISOString(),
475
476
  });
476
477
  }
477
-
478
478
  function clampTitle(title: string): string {
479
479
  return title.length > 80 ? `${title.slice(0, 77)}...` : title;
480
480
  }
@@ -1307,7 +1307,7 @@ function renderCompleteHandoff(
1307
1307
  ].join("\n");
1308
1308
  }
1309
1309
 
1310
- export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
1310
+ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
1311
1311
  try {
1312
1312
  const command = commandName(args);
1313
1313
  const json = hasFlag(args, "--json");
@@ -1414,3 +1414,63 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1414
1414
  return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
1415
1415
  }
1416
1416
  }
1417
+
1418
+ const RECONCILE_COMMANDS = new Set([
1419
+ "status",
1420
+ "create",
1421
+ "create-goals",
1422
+ "complete-goals",
1423
+ "checkpoint",
1424
+ "steer",
1425
+ "record-review-blockers",
1426
+ ]);
1427
+
1428
+ /**
1429
+ * Derive a workflow-state payload from the ultragoal plan/ledger and reconcile the
1430
+ * ultragoal mode-state + active-state/HUD so `gjc state ultragoal read`, the
1431
+ * skill-tool chain guard, and the HUD chip mirror the plan/ledger. Session scope
1432
+ * follows `gjc state` (`GJC_SESSION_ID`). This is a derived repair: it never changes
1433
+ * the triggering command's status/stdout, but a failure is surfaced (stderr + a
1434
+ * `reconcile_failed` ledger audit event) rather than silently swallowed. `status` is
1435
+ * therefore a read PLUS a derived repair; it never mutates goals.json/ledger.jsonl
1436
+ * beyond that reconcile-failure audit event.
1437
+ */
1438
+ async function reconcileUltragoalState(cwd: string): Promise<void> {
1439
+ const sessionId = process.env.GJC_SESSION_ID?.trim() || undefined;
1440
+ try {
1441
+ const summary = await getUltragoalStatus(cwd);
1442
+ const status = summary.status;
1443
+ const active = summary.exists && status !== "complete";
1444
+ const payload: Record<string, unknown> = {
1445
+ skill: "ultragoal",
1446
+ status,
1447
+ current_phase: status,
1448
+ active,
1449
+ goals: summary.goals.map(goal => ({ id: goal.id, title: goal.title, status: goal.status })),
1450
+ counts: summary.counts,
1451
+ active_goal_id: summary.currentGoal?.id ?? null,
1452
+ ledger_path: summary.paths.ledgerPath,
1453
+ brief_path: summary.paths.briefPath,
1454
+ goals_path: summary.paths.goalsPath,
1455
+ };
1456
+ if (summary.gjcObjective) payload.gjc_objective = summary.gjcObjective;
1457
+ await reconcileWorkflowSkillState({ cwd, mode: "ultragoal", sessionId, active, phase: status, payload });
1458
+ } catch (error) {
1459
+ const message = error instanceof Error ? error.message : String(error);
1460
+ process.stderr.write(`ultragoal state reconciliation failed: ${message}\n`);
1461
+ try {
1462
+ await appendLedger(cwd, { type: "reconcile_failed", error: message });
1463
+ } catch {
1464
+ // Best-effort audit; never let a secondary failure change command semantics.
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
1470
+ const command = commandName(args);
1471
+ const result = await dispatchUltragoalCommand(args, cwd);
1472
+ if (result.status === 0 && RECONCILE_COMMANDS.has(command)) {
1473
+ await reconcileUltragoalState(cwd);
1474
+ }
1475
+ return result;
1476
+ }
@@ -1171,6 +1171,10 @@
1171
1171
  ],
1172
1172
  "skill": "ultragoal",
1173
1173
  "states": [
1174
+ {
1175
+ "id": "missing",
1176
+ "terminal": true
1177
+ },
1174
1178
  {
1175
1179
  "id": "goal-planning",
1176
1180
  "initial": true
@@ -1198,6 +1202,7 @@
1198
1202
  }
1199
1203
  ],
1200
1204
  "terminalStates": [
1205
+ "missing",
1201
1206
  "failed",
1202
1207
  "complete",
1203
1208
  "handoff"
@@ -212,8 +212,8 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
212
212
  }),
213
213
  ultragoal: manifest({
214
214
  skill: "ultragoal",
215
- states: ["goal-planning", "pending", "active", "blocked", "failed", "complete", "handoff"],
216
- terminalStates: ["failed", "complete", "handoff"],
215
+ states: ["missing", "goal-planning", "pending", "active", "blocked", "failed", "complete", "handoff"],
216
+ terminalStates: ["missing", "failed", "complete", "handoff"],
217
217
  transitions: [
218
218
  { from: "goal-planning", to: "pending", verb: "create-goals" },
219
219
  { from: "pending", to: "active", verb: "complete-goals" },