@gajae-code/coding-agent 0.3.2 → 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 (122) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/discovery/claude-plugins.ts +25 -5
  58. package/src/edit/diff.ts +64 -1
  59. package/src/edit/modes/replace.ts +60 -2
  60. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  61. package/src/extensibility/gjc-plugins/index.ts +9 -0
  62. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  63. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  64. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  65. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  66. package/src/extensibility/gjc-plugins/state.ts +29 -0
  67. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  68. package/src/extensibility/gjc-plugins/types.ts +97 -0
  69. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  70. package/src/extensibility/skills.ts +39 -7
  71. package/src/gjc-runtime/state-runtime.ts +93 -2
  72. package/src/gjc-runtime/state-writer.ts +17 -1
  73. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  74. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  75. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  76. package/src/harness-control-plane/storage.ts +144 -2
  77. package/src/hashline/hash.ts +23 -0
  78. package/src/hooks/skill-state.ts +2 -0
  79. package/src/internal-urls/docs-index.generated.ts +5 -5
  80. package/src/lsp/client.ts +7 -0
  81. package/src/modes/acp/acp-agent.ts +25 -2
  82. package/src/modes/bridge/bridge-mode.ts +124 -2
  83. package/src/modes/controllers/input-controller.ts +14 -2
  84. package/src/modes/prompt-action-autocomplete.ts +49 -10
  85. package/src/modes/rpc/rpc-client.ts +57 -3
  86. package/src/modes/rpc/rpc-mode.ts +67 -0
  87. package/src/modes/rpc/rpc-types.ts +224 -2
  88. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  89. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  90. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  91. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  92. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  93. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  94. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  95. package/src/modes/shared/agent-wire/responses.ts +2 -2
  96. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  97. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  98. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  99. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  101. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  102. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  103. package/src/modes/theme/theme.ts +6 -0
  104. package/src/runtime-mcp/client.ts +7 -4
  105. package/src/runtime-mcp/manager.ts +45 -13
  106. package/src/runtime-mcp/transports/http.ts +40 -14
  107. package/src/runtime-mcp/transports/stdio.ts +11 -10
  108. package/src/sdk.ts +47 -0
  109. package/src/session/agent-session.ts +211 -2
  110. package/src/session/blob-store.ts +84 -0
  111. package/src/session/messages.ts +3 -0
  112. package/src/session/session-manager.ts +390 -33
  113. package/src/session/session-storage.ts +26 -0
  114. package/src/setup/provider-onboarding.ts +2 -2
  115. package/src/skill-state/active-state.ts +89 -1
  116. package/src/task/discovery.ts +7 -1
  117. package/src/task/executor.ts +16 -2
  118. package/src/thinking.ts +8 -2
  119. package/src/tools/ask.ts +39 -9
  120. package/src/tools/index.ts +3 -0
  121. package/src/tools/skill.ts +15 -3
  122. 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