@gajae-code/coding-agent 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/commands/contribution-prep.d.ts +18 -0
- package/dist/types/commands/deep-interview.d.ts +41 -0
- package/dist/types/commands/session.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +2 -2
- package/dist/types/config/models-config-schema.d.ts +17 -9
- package/dist/types/config/settings-schema.d.ts +37 -24
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
- package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
- package/dist/types/goals/runtime.d.ts +3 -9
- package/dist/types/goals/state.d.ts +3 -6
- package/dist/types/goals/tools/goal-tool.d.ts +1 -69
- package/dist/types/hooks/skill-state.d.ts +5 -0
- package/dist/types/memories/index.d.ts +1 -1
- package/dist/types/memory-backend/local-backend.d.ts +3 -3
- package/dist/types/modes/components/hook-selector.d.ts +7 -0
- package/dist/types/modes/components/settings-selector.d.ts +0 -2
- package/dist/types/modes/components/status-line/types.d.ts +0 -3
- package/dist/types/modes/components/status-line.d.ts +0 -3
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -12
- package/dist/types/modes/theme/defaults/index.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +1 -2
- package/dist/types/modes/types.d.ts +1 -7
- package/dist/types/modes/utils/context-usage.d.ts +6 -2
- package/dist/types/sdk.d.ts +6 -2
- package/dist/types/session/agent-session.d.ts +47 -1
- package/dist/types/session/contribution-prep.d.ts +47 -0
- package/dist/types/session/session-manager.d.ts +3 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
- package/dist/types/setup/provider-onboarding.d.ts +29 -5
- package/dist/types/skill-state/active-state.d.ts +30 -1
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
- package/dist/types/skill-state/initial-phase.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +9 -4
- package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +11 -0
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/skill.d.ts +47 -0
- package/dist/types/utils/changelog.d.ts +18 -2
- package/package.json +7 -7
- package/src/cli/args.ts +3 -2
- package/src/cli/setup-cli.ts +26 -12
- package/src/cli.ts +7 -1
- package/src/commands/contribution-prep.ts +41 -0
- package/src/commands/deep-interview.ts +30 -23
- package/src/commands/launch.ts +10 -1
- package/src/commands/ralplan.ts +10 -22
- package/src/commands/session.ts +150 -0
- package/src/commands/setup.ts +2 -0
- package/src/commands/state.ts +15 -4
- package/src/commands/team.ts +23 -3
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +120 -102
- package/src/config/settings-schema.ts +42 -25
- package/src/config.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
- package/src/defaults/gjc/skills/team/SKILL.md +39 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
- package/src/discovery/helpers.ts +24 -1
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
- package/src/gjc-runtime/goal-mode-request.ts +2 -19
- package/src/gjc-runtime/launch-tmux.ts +83 -43
- package/src/gjc-runtime/ralplan-runtime.ts +460 -0
- package/src/gjc-runtime/state-runtime.ts +731 -0
- package/src/gjc-runtime/team-runtime.ts +708 -52
- package/src/gjc-runtime/tmux-common.ts +119 -0
- package/src/gjc-runtime/tmux-sessions.ts +165 -0
- package/src/gjc-runtime/ultragoal-guard.ts +6 -3
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
- package/src/goals/runtime.ts +38 -144
- package/src/goals/state.ts +36 -7
- package/src/goals/tools/goal-tool.ts +15 -172
- package/src/hooks/skill-state.ts +39 -18
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/internal-urls/memory-protocol.ts +3 -2
- package/src/main.ts +2 -3
- package/src/memories/index.ts +2 -1
- package/src/memory-backend/local-backend.ts +14 -6
- package/src/modes/components/hook-selector.ts +156 -1
- package/src/modes/components/settings-selector.ts +5 -12
- package/src/modes/components/skill-hud/render.ts +4 -0
- package/src/modes/components/status-line/segments.ts +5 -16
- package/src/modes/components/status-line/types.ts +0 -3
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/controllers/command-controller.ts +27 -4
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/interactive-mode.ts +18 -219
- package/src/modes/theme/defaults/dark-poimandres.json +0 -1
- package/src/modes/theme/defaults/light-poimandres.json +0 -1
- package/src/modes/theme/theme.ts +0 -6
- package/src/modes/types.ts +1 -7
- package/src/modes/utils/context-usage.ts +66 -17
- package/src/prompts/agents/architect.md +3 -0
- package/src/prompts/agents/executor.md +2 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/goals/goal-continuation.md +1 -4
- package/src/prompts/goals/goal-mode-active.md +3 -5
- package/src/prompts/system/subagent-system-prompt.md +6 -0
- package/src/prompts/system/system-prompt.md +5 -7
- package/src/prompts/tools/goal.md +4 -4
- package/src/prompts/tools/skill.md +28 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/sdk.ts +51 -11
- package/src/session/agent-session.ts +222 -21
- package/src/session/contribution-prep.ts +320 -0
- package/src/session/session-manager.ts +9 -1
- package/src/setup/model-onboarding-guidance.ts +6 -3
- package/src/setup/provider-onboarding.ts +177 -16
- package/src/skill-state/active-state.ts +188 -25
- package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
- package/src/skill-state/initial-phase.ts +17 -0
- package/src/skill-state/workflow-hud.ts +23 -5
- package/src/skill-state/workflow-state-contract.ts +121 -0
- package/src/slash-commands/builtin-registry.ts +75 -25
- package/src/slash-commands/helpers/context-report.ts +123 -13
- package/src/task/agents.ts +1 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +91 -4
- package/src/task/types.ts +6 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/gh.ts +212 -2
- package/src/tools/index.ts +25 -6
- package/src/tools/skill.ts +153 -0
- package/src/utils/changelog.ts +67 -44
- package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
- package/dist/types/commands/question.d.ts +0 -7
- package/dist/types/modes/loop-limit.d.ts +0 -22
- package/src/commands/gjc-runtime-bridge.ts +0 -227
- package/src/commands/question.ts +0 -12
- package/src/modes/loop-limit.ts +0 -140
- package/src/prompts/commands/orchestrate.md +0 -49
- package/src/prompts/goals/goal-budget-limit.md +0 -16
- package/src/prompts/tools/create-goal.md +0 -3
- package/src/prompts/tools/get-goal.md +0 -3
- package/src/prompts/tools/update-goal.md +0 -3
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
|
+
import {
|
|
6
|
+
applyHandoffToActiveState,
|
|
7
|
+
CANONICAL_GJC_WORKFLOW_SKILLS,
|
|
8
|
+
type CanonicalGjcWorkflowSkill,
|
|
9
|
+
listActiveSkills,
|
|
10
|
+
readVisibleSkillActiveState,
|
|
11
|
+
syncSkillActiveState,
|
|
12
|
+
} from "../skill-state/active-state";
|
|
13
|
+
import { initialPhaseForSkill } from "../skill-state/initial-phase";
|
|
14
|
+
import {
|
|
15
|
+
buildDeepInterviewHudSummary,
|
|
16
|
+
buildRalplanHudSummary,
|
|
17
|
+
buildTeamHudSummary,
|
|
18
|
+
buildUltragoalHudSummary,
|
|
19
|
+
} from "../skill-state/workflow-hud";
|
|
20
|
+
import {
|
|
21
|
+
buildWorkflowStateReceipt,
|
|
22
|
+
canonicalWorkflowSkill,
|
|
23
|
+
describeWorkflowStateContract,
|
|
24
|
+
type WorkflowStateReceipt,
|
|
25
|
+
} from "../skill-state/workflow-state-contract";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Native implementation of the `gjc state read|write|clear` command surface.
|
|
29
|
+
*
|
|
30
|
+
* Simple file-receipt operations against `.gjc/state/[sessions/<id>/]<mode>-state.json` and
|
|
31
|
+
* `.gjc/state/[sessions/<id>/]skill-active-state.json`. This is the sanctioned CLI mediator for
|
|
32
|
+
* the mutation-guarded `.gjc/state` ACL — agents call it instead of editing those files directly.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface StateCommandResult {
|
|
36
|
+
status: number;
|
|
37
|
+
stdout?: string;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
42
|
+
const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
43
|
+
const KNOWN_MODES: readonly string[] = CANONICAL_GJC_WORKFLOW_SKILLS;
|
|
44
|
+
|
|
45
|
+
class StateCommandError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
public readonly exitStatus: number,
|
|
48
|
+
message: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "StateCommandError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flagValue(args: readonly string[], flag: string): string | undefined {
|
|
56
|
+
const index = args.indexOf(flag);
|
|
57
|
+
if (index < 0) return undefined;
|
|
58
|
+
return args[index + 1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
62
|
+
return args.includes(flag);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id", "--to"]);
|
|
66
|
+
const ACTION_NAMES = new Set(["read", "write", "clear", "contract", "handoff"]);
|
|
67
|
+
|
|
68
|
+
interface ParsedInvocation {
|
|
69
|
+
action: "read" | "write" | "clear" | "contract" | "handoff";
|
|
70
|
+
positionalSkill?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parsePositionalArgs(args: readonly string[]): ParsedInvocation {
|
|
74
|
+
let skipNext = false;
|
|
75
|
+
const positional: string[] = [];
|
|
76
|
+
for (const arg of args) {
|
|
77
|
+
if (skipNext) {
|
|
78
|
+
skipNext = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (FLAGS_WITH_VALUES.has(arg)) {
|
|
82
|
+
skipNext = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!arg.startsWith("-")) positional.push(arg);
|
|
86
|
+
}
|
|
87
|
+
// Documented argv shapes:
|
|
88
|
+
// gjc state read|write|clear|contract ...
|
|
89
|
+
// gjc state <skill> read|write|contract ...
|
|
90
|
+
const first = positional[0];
|
|
91
|
+
const second = positional[1];
|
|
92
|
+
if (first && ACTION_NAMES.has(first)) {
|
|
93
|
+
return { action: first as ParsedInvocation["action"] };
|
|
94
|
+
}
|
|
95
|
+
if (first && second && ACTION_NAMES.has(second)) {
|
|
96
|
+
return { action: second as ParsedInvocation["action"], positionalSkill: first };
|
|
97
|
+
}
|
|
98
|
+
// `gjc state <skill>` alone defaults to read for that skill.
|
|
99
|
+
if (first && !second) {
|
|
100
|
+
return { action: "read", positionalSkill: first };
|
|
101
|
+
}
|
|
102
|
+
return { action: "read" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertSafePathComponent(value: string, label: string): void {
|
|
106
|
+
if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
|
|
107
|
+
throw new StateCommandError(2, `invalid path component for --${label}: ${value}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertKnownMode(mode: string): asserts mode is CanonicalGjcWorkflowSkill {
|
|
112
|
+
if (!KNOWN_MODES.includes(mode)) {
|
|
113
|
+
throw new StateCommandError(2, `unknown --mode: ${mode}. Expected one of: ${KNOWN_MODES.join(", ")}.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readInputJson(value: string | undefined, cwd: string): Promise<Record<string, unknown> | undefined> {
|
|
118
|
+
if (value === undefined) return undefined;
|
|
119
|
+
const trimmed = value.trim();
|
|
120
|
+
if (!trimmed) return undefined;
|
|
121
|
+
let raw: string;
|
|
122
|
+
if (trimmed.startsWith("@")) {
|
|
123
|
+
const filePath = path.resolve(cwd, trimmed.slice(1));
|
|
124
|
+
try {
|
|
125
|
+
raw = await fs.readFile(filePath, "utf-8");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new StateCommandError(2, `failed to read --input file ${filePath}: ${(error as Error).message}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
raw = trimmed;
|
|
131
|
+
}
|
|
132
|
+
let parsed: unknown;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(raw);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new StateCommandError(2, `--input is not valid JSON: ${(error as Error).message}`);
|
|
137
|
+
}
|
|
138
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
139
|
+
throw new StateCommandError(2, "--input must be a JSON object");
|
|
140
|
+
}
|
|
141
|
+
return parsed as Record<string, unknown>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface ResolvedSelectors {
|
|
145
|
+
mode: CanonicalGjcWorkflowSkill | undefined;
|
|
146
|
+
sessionId: string | undefined;
|
|
147
|
+
threadId: string | undefined;
|
|
148
|
+
turnId: string | undefined;
|
|
149
|
+
payload: Record<string, unknown> | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function resolveSelectors(
|
|
153
|
+
args: readonly string[],
|
|
154
|
+
cwd: string,
|
|
155
|
+
positionalSkill: string | undefined,
|
|
156
|
+
): Promise<ResolvedSelectors> {
|
|
157
|
+
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
158
|
+
|
|
159
|
+
const candidates: Array<string | undefined> = [
|
|
160
|
+
flagValue(args, "--mode")?.trim() || undefined,
|
|
161
|
+
positionalSkill?.trim() || undefined,
|
|
162
|
+
typeof payload?.mode === "string" ? (payload.mode as string).trim() || undefined : undefined,
|
|
163
|
+
typeof payload?.skill === "string" ? (payload.skill as string).trim() || undefined : undefined,
|
|
164
|
+
];
|
|
165
|
+
let mode: string | undefined;
|
|
166
|
+
for (const candidate of candidates) {
|
|
167
|
+
if (candidate) {
|
|
168
|
+
mode = candidate;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (mode) assertKnownMode(mode);
|
|
173
|
+
|
|
174
|
+
// Session-id resolution order: explicit --session-id flag, then payload
|
|
175
|
+
// session_id, then GJC_SESSION_ID env var (set by AgentSession.sdk for
|
|
176
|
+
// agent-initiated CLI invocations). The env-var default keeps shell
|
|
177
|
+
// snippets in skill docs short while still routing writes/reads to the
|
|
178
|
+
// caller's session-scoped state files.
|
|
179
|
+
let sessionId = flagValue(args, "--session-id")?.trim() || undefined;
|
|
180
|
+
if (!sessionId && payload && typeof payload.session_id === "string") {
|
|
181
|
+
sessionId = payload.session_id.trim() || undefined;
|
|
182
|
+
}
|
|
183
|
+
if (!sessionId) {
|
|
184
|
+
const envSessionId = process.env.GJC_SESSION_ID?.trim();
|
|
185
|
+
if (envSessionId) sessionId = envSessionId;
|
|
186
|
+
}
|
|
187
|
+
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
188
|
+
|
|
189
|
+
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
190
|
+
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
191
|
+
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
192
|
+
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
193
|
+
|
|
194
|
+
return { mode: mode as CanonicalGjcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function inferModeFromActiveState(
|
|
198
|
+
cwd: string,
|
|
199
|
+
sessionId: string | undefined,
|
|
200
|
+
): Promise<CanonicalGjcWorkflowSkill | undefined> {
|
|
201
|
+
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
202
|
+
const entries = listActiveSkills(state);
|
|
203
|
+
const candidate = entries[0]?.skill ?? state?.skill;
|
|
204
|
+
if (!candidate) return undefined;
|
|
205
|
+
const canonical = canonicalWorkflowSkill(candidate);
|
|
206
|
+
return canonical ?? undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function encodeSessionSegment(value: string): string {
|
|
210
|
+
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stateDirFor(cwd: string, sessionId: string | undefined): string {
|
|
214
|
+
const base = path.join(cwd, ".gjc", "state");
|
|
215
|
+
if (!sessionId) return base;
|
|
216
|
+
return path.join(base, "sessions", encodeSessionSegment(sessionId));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function modeStateFile(cwd: string, mode: string, sessionId: string | undefined): string {
|
|
220
|
+
return path.join(stateDirFor(cwd, sessionId), `${mode}-state.json`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function activeStateFile(cwd: string, sessionId: string | undefined): string {
|
|
224
|
+
return path.join(stateDirFor(cwd, sessionId), SKILL_ACTIVE_STATE_FILE);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
|
228
|
+
try {
|
|
229
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
230
|
+
const parsed = JSON.parse(raw);
|
|
231
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
232
|
+
return parsed as Record<string, unknown>;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const err = error as NodeJS.ErrnoException;
|
|
237
|
+
if (err.code === "ENOENT") return null;
|
|
238
|
+
throw new StateCommandError(1, `failed to read ${filePath}: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
|
|
243
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
244
|
+
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
245
|
+
await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
246
|
+
await fs.rename(tmp, filePath);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
250
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Shallow-merge `source` into `target`, with the convention that a `source` key whose value is
|
|
255
|
+
* `null` deletes that key from `target`. Nested objects are replaced wholesale (not deep-merged)
|
|
256
|
+
* so callers retain explicit control over substructure semantics; pre-existing skills that want
|
|
257
|
+
* to merge nested fields can supply the full sub-object themselves.
|
|
258
|
+
*/
|
|
259
|
+
function mergeWithNullDelete(
|
|
260
|
+
target: Record<string, unknown>,
|
|
261
|
+
source: Record<string, unknown>,
|
|
262
|
+
): Record<string, unknown> {
|
|
263
|
+
const result: Record<string, unknown> = { ...target };
|
|
264
|
+
for (const [key, value] of Object.entries(source)) {
|
|
265
|
+
if (value === null) {
|
|
266
|
+
delete result[key];
|
|
267
|
+
} else {
|
|
268
|
+
result[key] = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function nowIso(): string {
|
|
275
|
+
return new Date().toISOString();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildHudForMode(
|
|
279
|
+
mode: CanonicalGjcWorkflowSkill,
|
|
280
|
+
payload: Record<string, unknown>,
|
|
281
|
+
): WorkflowHudSummary | undefined {
|
|
282
|
+
const updatedAt = new Date().toISOString();
|
|
283
|
+
const phase = typeof payload.current_phase === "string" ? payload.current_phase : undefined;
|
|
284
|
+
const stateField = isPlainObject(payload.state) ? (payload.state as Record<string, unknown>) : {};
|
|
285
|
+
switch (mode) {
|
|
286
|
+
case "deep-interview": {
|
|
287
|
+
const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
|
|
288
|
+
const v = (stateField as Record<string, unknown>)[key] ?? (payload as Record<string, unknown>)[key];
|
|
289
|
+
return guard(v) ? v : undefined;
|
|
290
|
+
};
|
|
291
|
+
const isNumber = (v: unknown): v is number => typeof v === "number";
|
|
292
|
+
const isString = (v: unknown): v is string => typeof v === "string";
|
|
293
|
+
const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
|
|
294
|
+
const ambiguity = pick("current_ambiguity", isNumber);
|
|
295
|
+
const threshold = pick("threshold", isNumber);
|
|
296
|
+
const rounds = pick("rounds", isArray);
|
|
297
|
+
const targetComponent = pick("last_targeted_component_id", isString);
|
|
298
|
+
const weakestDimension = pick("weakest_dimension", isString);
|
|
299
|
+
return buildDeepInterviewHudSummary({
|
|
300
|
+
phase,
|
|
301
|
+
ambiguity,
|
|
302
|
+
threshold,
|
|
303
|
+
roundCount: rounds?.length,
|
|
304
|
+
targetComponent,
|
|
305
|
+
weakestDimension,
|
|
306
|
+
updatedAt,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
case "ralplan": {
|
|
310
|
+
const stage =
|
|
311
|
+
typeof payload.current_phase === "string"
|
|
312
|
+
? (payload.current_phase as string)
|
|
313
|
+
: typeof payload.mode === "string"
|
|
314
|
+
? (payload.mode as string)
|
|
315
|
+
: undefined;
|
|
316
|
+
const verdict = typeof payload.verdict === "string" ? (payload.verdict as string) : undefined;
|
|
317
|
+
const iteration = typeof payload.iteration === "number" ? (payload.iteration as number) : undefined;
|
|
318
|
+
const pendingApproval = payload.pending_approval === true || stage === "final";
|
|
319
|
+
return buildRalplanHudSummary({
|
|
320
|
+
stage,
|
|
321
|
+
verdict,
|
|
322
|
+
iteration,
|
|
323
|
+
pendingApproval,
|
|
324
|
+
updatedAt,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
case "ultragoal": {
|
|
328
|
+
const goals = Array.isArray(payload.goals)
|
|
329
|
+
? (payload.goals as Array<{ id?: string; title?: string; status?: string }>).filter(
|
|
330
|
+
g => g && typeof g.id === "string" && typeof g.title === "string" && typeof g.status === "string",
|
|
331
|
+
)
|
|
332
|
+
: [];
|
|
333
|
+
const counts: Record<string, number> = {};
|
|
334
|
+
for (const goal of goals) {
|
|
335
|
+
const status = goal.status as string;
|
|
336
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
337
|
+
}
|
|
338
|
+
const currentGoalRaw = goals.find(g => g.status === "active") ?? goals.find(g => g.status === "pending");
|
|
339
|
+
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
340
|
+
return buildUltragoalHudSummary({
|
|
341
|
+
status,
|
|
342
|
+
currentGoal: currentGoalRaw
|
|
343
|
+
? {
|
|
344
|
+
id: currentGoalRaw.id as string,
|
|
345
|
+
title: currentGoalRaw.title as string,
|
|
346
|
+
status: currentGoalRaw.status as string,
|
|
347
|
+
}
|
|
348
|
+
: undefined,
|
|
349
|
+
counts,
|
|
350
|
+
goals: goals.map(g => ({ id: g.id as string, title: g.title as string, status: g.status as string })),
|
|
351
|
+
updatedAt,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
case "team": {
|
|
355
|
+
const teamPhase = typeof payload.phase === "string" ? (payload.phase as string) : (phase ?? "running");
|
|
356
|
+
const taskCounts =
|
|
357
|
+
typeof payload.task_counts === "object" && payload.task_counts && !Array.isArray(payload.task_counts)
|
|
358
|
+
? (payload.task_counts as Record<string, number>)
|
|
359
|
+
: {};
|
|
360
|
+
const taskTotal = typeof payload.task_total === "number" ? (payload.task_total as number) : 0;
|
|
361
|
+
const workers = Array.isArray(payload.workers)
|
|
362
|
+
? (payload.workers as Array<{ id?: string; status?: string }>)
|
|
363
|
+
.filter(w => w && typeof w.id === "string")
|
|
364
|
+
.map(w => ({
|
|
365
|
+
id: w.id as string,
|
|
366
|
+
status: typeof w.status === "string" ? (w.status as string) : undefined,
|
|
367
|
+
}))
|
|
368
|
+
: [];
|
|
369
|
+
return buildTeamHudSummary({
|
|
370
|
+
phase: teamPhase,
|
|
371
|
+
task_total: taskTotal,
|
|
372
|
+
task_counts: taskCounts,
|
|
373
|
+
workers,
|
|
374
|
+
updated_at: updatedAt,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
default:
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function syncWorkflowSkillState(options: {
|
|
383
|
+
cwd: string;
|
|
384
|
+
mode: CanonicalGjcWorkflowSkill;
|
|
385
|
+
sessionId: string | undefined;
|
|
386
|
+
threadId?: string;
|
|
387
|
+
turnId?: string;
|
|
388
|
+
active: boolean;
|
|
389
|
+
phase: string | undefined;
|
|
390
|
+
payload: Record<string, unknown>;
|
|
391
|
+
receipt?: WorkflowStateReceipt;
|
|
392
|
+
}): Promise<void> {
|
|
393
|
+
try {
|
|
394
|
+
await syncSkillActiveState({
|
|
395
|
+
cwd: options.cwd,
|
|
396
|
+
skill: options.mode,
|
|
397
|
+
active: options.active,
|
|
398
|
+
phase: options.phase,
|
|
399
|
+
sessionId: options.sessionId,
|
|
400
|
+
threadId: options.threadId,
|
|
401
|
+
turnId: options.turnId,
|
|
402
|
+
source: "gjc-state-cli",
|
|
403
|
+
hud: buildHudForMode(options.mode, options.payload),
|
|
404
|
+
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
405
|
+
});
|
|
406
|
+
} catch {
|
|
407
|
+
// HUD sync is best-effort and must not change command semantics.
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function handleRead(
|
|
411
|
+
args: readonly string[],
|
|
412
|
+
cwd: string,
|
|
413
|
+
positionalSkill: string | undefined,
|
|
414
|
+
): Promise<StateCommandResult> {
|
|
415
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
416
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
|
|
417
|
+
if (mode) {
|
|
418
|
+
const filePath = modeStateFile(cwd, mode, selectors.sessionId);
|
|
419
|
+
const existing = await readJsonFile(filePath);
|
|
420
|
+
return {
|
|
421
|
+
status: 0,
|
|
422
|
+
stdout: `${JSON.stringify({ skill: mode, state: existing, storage_path: filePath }, null, 2)}\n`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const filePath = activeStateFile(cwd, selectors.sessionId);
|
|
426
|
+
const existing = await readJsonFile(filePath);
|
|
427
|
+
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function handleWrite(
|
|
431
|
+
args: readonly string[],
|
|
432
|
+
cwd: string,
|
|
433
|
+
positionalSkill: string | undefined,
|
|
434
|
+
): Promise<StateCommandResult> {
|
|
435
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
436
|
+
const { sessionId, threadId, turnId, payload } = selectors;
|
|
437
|
+
if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
|
|
438
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
439
|
+
if (!mode)
|
|
440
|
+
throw new StateCommandError(
|
|
441
|
+
2,
|
|
442
|
+
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
446
|
+
const existing = await readJsonFile(filePath);
|
|
447
|
+
const nowIsoStr = nowIso();
|
|
448
|
+
const receipt = buildWorkflowStateReceipt({
|
|
449
|
+
cwd,
|
|
450
|
+
skill: mode,
|
|
451
|
+
owner: "gjc-state-cli",
|
|
452
|
+
command: `gjc state ${mode} write`,
|
|
453
|
+
sessionId,
|
|
454
|
+
nowIso: nowIsoStr,
|
|
455
|
+
});
|
|
456
|
+
const existingPayload = existing ?? {};
|
|
457
|
+
const innerState = (payload.state as Record<string, unknown> | undefined) ?? {};
|
|
458
|
+
const incomingPhase =
|
|
459
|
+
typeof payload.current_phase === "string" && payload.current_phase.trim()
|
|
460
|
+
? payload.current_phase.trim()
|
|
461
|
+
: typeof payload.phase === "string" && payload.phase.trim()
|
|
462
|
+
? payload.phase.trim()
|
|
463
|
+
: typeof innerState.current_phase === "string" && (innerState.current_phase as string).trim()
|
|
464
|
+
? (innerState.current_phase as string).trim()
|
|
465
|
+
: undefined;
|
|
466
|
+
let merged: Record<string, unknown>;
|
|
467
|
+
if (hasFlag(args, "--replace")) {
|
|
468
|
+
merged = { ...payload };
|
|
469
|
+
} else {
|
|
470
|
+
merged = mergeWithNullDelete(existingPayload, payload);
|
|
471
|
+
// Flatten payload.state.* into the top-level envelope so downstream consumers
|
|
472
|
+
// see a single canonical structure with the receipt at top level.
|
|
473
|
+
if (payload.state && typeof payload.state === "object" && !Array.isArray(payload.state)) {
|
|
474
|
+
merged = mergeWithNullDelete(merged, payload.state as Record<string, unknown>);
|
|
475
|
+
delete merged.state;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
merged.skill = mode;
|
|
479
|
+
if (incomingPhase) {
|
|
480
|
+
merged.current_phase = incomingPhase;
|
|
481
|
+
} else if (typeof merged.current_phase !== "string") {
|
|
482
|
+
merged.current_phase =
|
|
483
|
+
typeof existingPayload.current_phase === "string" ? existingPayload.current_phase : "active";
|
|
484
|
+
}
|
|
485
|
+
if (typeof merged.version !== "number") merged.version = 1;
|
|
486
|
+
if (typeof merged.active !== "boolean") merged.active = true;
|
|
487
|
+
merged.updated_at = nowIsoStr;
|
|
488
|
+
merged.receipt = receipt;
|
|
489
|
+
if (sessionId && typeof merged.session_id !== "string") merged.session_id = sessionId;
|
|
490
|
+
await writeJsonAtomic(filePath, merged);
|
|
491
|
+
|
|
492
|
+
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
493
|
+
const active = merged.active !== false;
|
|
494
|
+
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
status: 0,
|
|
498
|
+
stdout: `${JSON.stringify({ skill: mode, state: merged, receipt }, null, 2)}\n`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function handleClear(
|
|
503
|
+
args: readonly string[],
|
|
504
|
+
cwd: string,
|
|
505
|
+
positionalSkill: string | undefined,
|
|
506
|
+
): Promise<StateCommandResult> {
|
|
507
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
508
|
+
const { sessionId, threadId, turnId } = selectors;
|
|
509
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
510
|
+
if (!mode)
|
|
511
|
+
throw new StateCommandError(
|
|
512
|
+
2,
|
|
513
|
+
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
517
|
+
const existing = (await readJsonFile(filePath)) ?? {};
|
|
518
|
+
const cleared: Record<string, unknown> = {
|
|
519
|
+
...existing,
|
|
520
|
+
active: false,
|
|
521
|
+
current_phase: "complete",
|
|
522
|
+
updated_at: nowIso(),
|
|
523
|
+
};
|
|
524
|
+
await writeJsonAtomic(filePath, cleared);
|
|
525
|
+
|
|
526
|
+
await syncWorkflowSkillState({
|
|
527
|
+
cwd,
|
|
528
|
+
mode,
|
|
529
|
+
sessionId,
|
|
530
|
+
threadId,
|
|
531
|
+
turnId,
|
|
532
|
+
active: false,
|
|
533
|
+
phase: "complete",
|
|
534
|
+
payload: cleared,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
return { status: 0, stdout: `${JSON.stringify(cleared, null, 2)}\n` };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* `handoff` exists in two distinct roles:
|
|
542
|
+
* - As a verb: this CLI action, which atomically transitions caller→callee.
|
|
543
|
+
* Writes the callee mode-state first, the caller mode-state second, then
|
|
544
|
+
* syncs both `skill-active-state.json` files. Every intermediate crashed
|
|
545
|
+
* state remains HUD-coherent: the active-state file either reflects the
|
|
546
|
+
* old skill entirely or the new skill entirely, never both as active.
|
|
547
|
+
* - As a phase: `current_phase: "handoff"` is set by this verb when demoting
|
|
548
|
+
* the caller. Agents writing `current_phase: "handoff"` manually via
|
|
549
|
+
* `gjc state <skill> write` are declaring "I am ready to be handed off";
|
|
550
|
+
* the next agent-initiated `skill` tool call will then satisfy the phase
|
|
551
|
+
* guard and may chain.
|
|
552
|
+
*
|
|
553
|
+
* `handoff` is in the terminal-phase set used by `isTerminalModeState` and by
|
|
554
|
+
* the skill tool's chain guard. A manual `current_phase: "handoff"` write does
|
|
555
|
+
* NOT mark `active: false` — only this verb does that — so a skill that wrote
|
|
556
|
+
* the phase remains in `skill-active-state.json` until a chain call (or
|
|
557
|
+
* explicit `clear`) demotes it.
|
|
558
|
+
*/
|
|
559
|
+
async function handleHandoff(
|
|
560
|
+
args: readonly string[],
|
|
561
|
+
cwd: string,
|
|
562
|
+
positionalSkill: string | undefined,
|
|
563
|
+
): Promise<StateCommandResult> {
|
|
564
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
565
|
+
const { sessionId, threadId, turnId } = selectors;
|
|
566
|
+
const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
567
|
+
if (!caller) {
|
|
568
|
+
throw new StateCommandError(
|
|
569
|
+
2,
|
|
570
|
+
"gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const calleeRaw = flagValue(args, "--to")?.trim();
|
|
574
|
+
if (!calleeRaw) {
|
|
575
|
+
throw new StateCommandError(2, "gjc state handoff requires --to <callee>");
|
|
576
|
+
}
|
|
577
|
+
assertKnownMode(calleeRaw);
|
|
578
|
+
const callee = calleeRaw as CanonicalGjcWorkflowSkill;
|
|
579
|
+
if (callee === caller) {
|
|
580
|
+
throw new StateCommandError(2, `gjc state handoff: --to must differ from caller (both are "${caller}")`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const callerPath = modeStateFile(cwd, caller, sessionId);
|
|
584
|
+
const calleePath = modeStateFile(cwd, callee, sessionId);
|
|
585
|
+
const existingCaller = await readJsonFile(callerPath);
|
|
586
|
+
if (!existingCaller) {
|
|
587
|
+
throw new StateCommandError(
|
|
588
|
+
2,
|
|
589
|
+
`gjc state ${caller} handoff: caller is not active (no mode-state file at ${callerPath})`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
const existingCallee = (await readJsonFile(calleePath)) ?? {};
|
|
593
|
+
|
|
594
|
+
const handoffAt = nowIso();
|
|
595
|
+
const callerReceipt = buildWorkflowStateReceipt({
|
|
596
|
+
cwd,
|
|
597
|
+
skill: caller,
|
|
598
|
+
owner: "gjc-state-cli",
|
|
599
|
+
command: `gjc state ${caller} handoff --to ${callee}`,
|
|
600
|
+
sessionId,
|
|
601
|
+
nowIso: handoffAt,
|
|
602
|
+
});
|
|
603
|
+
const calleeReceipt = buildWorkflowStateReceipt({
|
|
604
|
+
cwd,
|
|
605
|
+
skill: callee,
|
|
606
|
+
owner: "gjc-state-cli",
|
|
607
|
+
command: `gjc state ${caller} handoff --to ${callee}`,
|
|
608
|
+
sessionId,
|
|
609
|
+
nowIso: handoffAt,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const calleeInitial = initialPhaseForSkill(callee);
|
|
613
|
+
const mergedCalleeState: Record<string, unknown> = {
|
|
614
|
+
...existingCallee,
|
|
615
|
+
skill: callee,
|
|
616
|
+
version: typeof existingCallee.version === "number" ? existingCallee.version : 1,
|
|
617
|
+
active: true,
|
|
618
|
+
current_phase: calleeInitial,
|
|
619
|
+
handoff_from: caller,
|
|
620
|
+
handoff_at: handoffAt,
|
|
621
|
+
updated_at: handoffAt,
|
|
622
|
+
receipt: calleeReceipt,
|
|
623
|
+
};
|
|
624
|
+
if (sessionId && typeof mergedCalleeState.session_id !== "string") {
|
|
625
|
+
mergedCalleeState.session_id = sessionId;
|
|
626
|
+
}
|
|
627
|
+
const mergedCallerState: Record<string, unknown> = {
|
|
628
|
+
...existingCaller,
|
|
629
|
+
skill: caller,
|
|
630
|
+
active: false,
|
|
631
|
+
current_phase: "handoff",
|
|
632
|
+
handoff_to: callee,
|
|
633
|
+
handoff_at: handoffAt,
|
|
634
|
+
updated_at: handoffAt,
|
|
635
|
+
receipt: callerReceipt,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Atomic write order (architecture blocker AR-3): mode-state files first,
|
|
639
|
+
// then a single atomic active-state mutation per file (session before root)
|
|
640
|
+
// via applyHandoffToActiveState. The single-write transaction prevents the
|
|
641
|
+
// HUD from observing a window where neither caller nor callee is active,
|
|
642
|
+
// and write order keeps the session-scoped source of truth ahead of the
|
|
643
|
+
// root aggregate. strict:true on the active-state read tolerates ENOENT
|
|
644
|
+
// only; corrupt JSON / IO failures propagate as non-zero CLI status.
|
|
645
|
+
await writeJsonAtomic(calleePath, mergedCalleeState);
|
|
646
|
+
await writeJsonAtomic(callerPath, mergedCallerState);
|
|
647
|
+
await applyHandoffToActiveState({
|
|
648
|
+
cwd,
|
|
649
|
+
nowIso: handoffAt,
|
|
650
|
+
strict: true,
|
|
651
|
+
caller: {
|
|
652
|
+
cwd,
|
|
653
|
+
skill: caller,
|
|
654
|
+
active: false,
|
|
655
|
+
phase: "handoff",
|
|
656
|
+
sessionId,
|
|
657
|
+
threadId,
|
|
658
|
+
turnId,
|
|
659
|
+
source: "gjc-state-cli",
|
|
660
|
+
hud: buildHudForMode(caller, mergedCallerState),
|
|
661
|
+
handoff_to: callee,
|
|
662
|
+
handoff_at: handoffAt,
|
|
663
|
+
receipt: callerReceipt,
|
|
664
|
+
},
|
|
665
|
+
callee: {
|
|
666
|
+
cwd,
|
|
667
|
+
skill: callee,
|
|
668
|
+
active: true,
|
|
669
|
+
phase: calleeInitial,
|
|
670
|
+
sessionId,
|
|
671
|
+
threadId,
|
|
672
|
+
turnId,
|
|
673
|
+
source: "gjc-state-cli",
|
|
674
|
+
hud: buildHudForMode(callee, mergedCalleeState),
|
|
675
|
+
handoff_from: caller,
|
|
676
|
+
handoff_at: handoffAt,
|
|
677
|
+
receipt: calleeReceipt,
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
status: 0,
|
|
683
|
+
stdout: `${JSON.stringify(
|
|
684
|
+
{
|
|
685
|
+
from: caller,
|
|
686
|
+
to: callee,
|
|
687
|
+
handoff_at: handoffAt,
|
|
688
|
+
caller_state: mergedCallerState,
|
|
689
|
+
callee_state: mergedCalleeState,
|
|
690
|
+
},
|
|
691
|
+
null,
|
|
692
|
+
2,
|
|
693
|
+
)}\n`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function handleContract(
|
|
698
|
+
args: readonly string[],
|
|
699
|
+
cwd: string,
|
|
700
|
+
positionalSkill: string | undefined,
|
|
701
|
+
): Promise<StateCommandResult> {
|
|
702
|
+
const { mode } = await resolveSelectors(args, cwd, positionalSkill);
|
|
703
|
+
if (!mode) {
|
|
704
|
+
throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
|
|
705
|
+
}
|
|
706
|
+
const payload = { skill: mode, contract: describeWorkflowStateContract(mode) };
|
|
707
|
+
return { status: 0, stdout: `${JSON.stringify(payload, null, 2)}\n` };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function runNativeStateCommand(args: string[], cwd = process.cwd()): Promise<StateCommandResult> {
|
|
711
|
+
try {
|
|
712
|
+
const parsed = parsePositionalArgs(args);
|
|
713
|
+
switch (parsed.action) {
|
|
714
|
+
case "read":
|
|
715
|
+
return await handleRead(args, cwd, parsed.positionalSkill);
|
|
716
|
+
case "write":
|
|
717
|
+
return await handleWrite(args, cwd, parsed.positionalSkill);
|
|
718
|
+
case "clear":
|
|
719
|
+
return await handleClear(args, cwd, parsed.positionalSkill);
|
|
720
|
+
case "contract":
|
|
721
|
+
return await handleContract(args, cwd, parsed.positionalSkill);
|
|
722
|
+
case "handoff":
|
|
723
|
+
return await handleHandoff(args, cwd, parsed.positionalSkill);
|
|
724
|
+
default:
|
|
725
|
+
return { status: 2, stderr: `Unknown gjc state command: ${parsed.action}\n` };
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
729
|
+
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
730
|
+
}
|
|
731
|
+
}
|