@gajae-code/coding-agent 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/dist/types/cli/skills-cli.d.ts +9 -0
- package/dist/types/commands/contribution-prep.d.ts +18 -0
- package/dist/types/commands/session.d.ts +24 -0
- package/dist/types/commands/skills.d.ts +26 -0
- package/dist/types/config/model-registry.d.ts +33 -4
- package/dist/types/config/models-config-schema.d.ts +52 -5
- package/dist/types/config/settings-schema.d.ts +1 -24
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -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/modes/components/model-selector.d.ts +21 -1
- 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/session/agent-session.d.ts +2 -0
- package/dist/types/session/contribution-prep.d.ts +47 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
- 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/slash-commands/builtin-registry.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +17 -2
- package/src/cli/skills-cli.ts +88 -0
- package/src/cli.ts +7 -1
- package/src/commands/contribution-prep.ts +41 -0
- package/src/commands/deep-interview.ts +6 -22
- package/src/commands/launch.ts +10 -1
- package/src/commands/ralplan.ts +10 -22
- package/src/commands/session.ts +150 -0
- package/src/commands/skills.ts +48 -0
- package/src/commands/state.ts +14 -4
- package/src/commands/team.ts +23 -3
- package/src/commit/agentic/index.ts +1 -0
- package/src/commit/pipeline.ts +1 -0
- package/src/config/model-registry.ts +269 -10
- package/src/config/models-config-schema.ts +124 -88
- package/src/config/settings-schema.ts +1 -25
- package/src/config.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
- package/src/defaults/gjc/skills/team/SKILL.md +29 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
- package/src/eval/py/prelude.py +1 -1
- package/src/gjc-runtime/deep-interview-runtime.ts +279 -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 +562 -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 +31 -12
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +10 -1
- package/src/modes/components/model-selector.ts +109 -28
- 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 +25 -1
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/selector-controller.ts +42 -2
- 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/prompts/goals/goal-continuation.md +1 -4
- package/src/prompts/goals/goal-mode-active.md +3 -5
- package/src/prompts/system/system-prompt.md +5 -7
- package/src/prompts/tools/goal.md +4 -4
- package/src/sdk.ts +2 -1
- package/src/session/agent-session.ts +18 -0
- package/src/session/contribution-prep.ts +320 -0
- package/src/setup/provider-onboarding.ts +2 -0
- package/src/skill-state/active-state.ts +38 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
- package/src/skill-state/workflow-hud.ts +23 -5
- package/src/skill-state/workflow-state-contract.ts +121 -0
- package/src/slash-commands/acp-builtins.ts +11 -2
- package/src/slash-commands/builtin-registry.ts +40 -13
- package/src/task/commands.ts +1 -5
- package/src/tools/gh.ts +212 -2
- package/src/tools/index.ts +2 -5
- 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,562 @@
|
|
|
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
|
+
CANONICAL_GJC_WORKFLOW_SKILLS,
|
|
7
|
+
type CanonicalGjcWorkflowSkill,
|
|
8
|
+
listActiveSkills,
|
|
9
|
+
readVisibleSkillActiveState,
|
|
10
|
+
syncSkillActiveState,
|
|
11
|
+
} from "../skill-state/active-state";
|
|
12
|
+
import {
|
|
13
|
+
buildDeepInterviewHudSummary,
|
|
14
|
+
buildRalplanHudSummary,
|
|
15
|
+
buildTeamHudSummary,
|
|
16
|
+
buildUltragoalHudSummary,
|
|
17
|
+
} from "../skill-state/workflow-hud";
|
|
18
|
+
import {
|
|
19
|
+
buildWorkflowStateReceipt,
|
|
20
|
+
canonicalWorkflowSkill,
|
|
21
|
+
describeWorkflowStateContract,
|
|
22
|
+
type WorkflowStateReceipt,
|
|
23
|
+
} from "../skill-state/workflow-state-contract";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Native implementation of the `gjc state read|write|clear` command surface.
|
|
27
|
+
*
|
|
28
|
+
* Simple file-receipt operations against `.gjc/state/[sessions/<id>/]<mode>-state.json` and
|
|
29
|
+
* `.gjc/state/[sessions/<id>/]skill-active-state.json`. This is the sanctioned CLI mediator for
|
|
30
|
+
* the mutation-guarded `.gjc/state` ACL — agents call it instead of editing those files directly.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export interface StateCommandResult {
|
|
34
|
+
status: number;
|
|
35
|
+
stdout?: string;
|
|
36
|
+
stderr?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
40
|
+
const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
41
|
+
const KNOWN_MODES: readonly string[] = CANONICAL_GJC_WORKFLOW_SKILLS;
|
|
42
|
+
|
|
43
|
+
class StateCommandError extends Error {
|
|
44
|
+
constructor(
|
|
45
|
+
public readonly exitStatus: number,
|
|
46
|
+
message: string,
|
|
47
|
+
) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "StateCommandError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function flagValue(args: readonly string[], flag: string): string | undefined {
|
|
54
|
+
const index = args.indexOf(flag);
|
|
55
|
+
if (index < 0) return undefined;
|
|
56
|
+
return args[index + 1];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasFlag(args: readonly string[], flag: string): boolean {
|
|
60
|
+
return args.includes(flag);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id"]);
|
|
64
|
+
const ACTION_NAMES = new Set(["read", "write", "clear", "contract"]);
|
|
65
|
+
|
|
66
|
+
interface ParsedInvocation {
|
|
67
|
+
action: "read" | "write" | "clear" | "contract";
|
|
68
|
+
positionalSkill?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parsePositionalArgs(args: readonly string[]): ParsedInvocation {
|
|
72
|
+
let skipNext = false;
|
|
73
|
+
const positional: string[] = [];
|
|
74
|
+
for (const arg of args) {
|
|
75
|
+
if (skipNext) {
|
|
76
|
+
skipNext = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (FLAGS_WITH_VALUES.has(arg)) {
|
|
80
|
+
skipNext = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!arg.startsWith("-")) positional.push(arg);
|
|
84
|
+
}
|
|
85
|
+
// Documented argv shapes:
|
|
86
|
+
// gjc state read|write|clear|contract ...
|
|
87
|
+
// gjc state <skill> read|write|contract ...
|
|
88
|
+
const first = positional[0];
|
|
89
|
+
const second = positional[1];
|
|
90
|
+
if (first && ACTION_NAMES.has(first)) {
|
|
91
|
+
return { action: first as ParsedInvocation["action"] };
|
|
92
|
+
}
|
|
93
|
+
if (first && second && ACTION_NAMES.has(second)) {
|
|
94
|
+
return { action: second as ParsedInvocation["action"], positionalSkill: first };
|
|
95
|
+
}
|
|
96
|
+
// `gjc state <skill>` alone defaults to read for that skill.
|
|
97
|
+
if (first && !second) {
|
|
98
|
+
return { action: "read", positionalSkill: first };
|
|
99
|
+
}
|
|
100
|
+
return { action: "read" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertSafePathComponent(value: string, label: string): void {
|
|
104
|
+
if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
|
|
105
|
+
throw new StateCommandError(2, `invalid path component for --${label}: ${value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assertKnownMode(mode: string): asserts mode is CanonicalGjcWorkflowSkill {
|
|
110
|
+
if (!KNOWN_MODES.includes(mode)) {
|
|
111
|
+
throw new StateCommandError(2, `unknown --mode: ${mode}. Expected one of: ${KNOWN_MODES.join(", ")}.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readInputJson(value: string | undefined, cwd: string): Promise<Record<string, unknown> | undefined> {
|
|
116
|
+
if (value === undefined) return undefined;
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
if (!trimmed) return undefined;
|
|
119
|
+
let raw: string;
|
|
120
|
+
if (trimmed.startsWith("@")) {
|
|
121
|
+
const filePath = path.resolve(cwd, trimmed.slice(1));
|
|
122
|
+
try {
|
|
123
|
+
raw = await fs.readFile(filePath, "utf-8");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new StateCommandError(2, `failed to read --input file ${filePath}: ${(error as Error).message}`);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
raw = trimmed;
|
|
129
|
+
}
|
|
130
|
+
let parsed: unknown;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(raw);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new StateCommandError(2, `--input is not valid JSON: ${(error as Error).message}`);
|
|
135
|
+
}
|
|
136
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
137
|
+
throw new StateCommandError(2, "--input must be a JSON object");
|
|
138
|
+
}
|
|
139
|
+
return parsed as Record<string, unknown>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ResolvedSelectors {
|
|
143
|
+
mode: CanonicalGjcWorkflowSkill | undefined;
|
|
144
|
+
sessionId: string | undefined;
|
|
145
|
+
threadId: string | undefined;
|
|
146
|
+
turnId: string | undefined;
|
|
147
|
+
payload: Record<string, unknown> | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function resolveSelectors(
|
|
151
|
+
args: readonly string[],
|
|
152
|
+
cwd: string,
|
|
153
|
+
positionalSkill: string | undefined,
|
|
154
|
+
): Promise<ResolvedSelectors> {
|
|
155
|
+
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
156
|
+
|
|
157
|
+
const candidates: Array<string | undefined> = [
|
|
158
|
+
flagValue(args, "--mode")?.trim() || undefined,
|
|
159
|
+
positionalSkill?.trim() || undefined,
|
|
160
|
+
typeof payload?.mode === "string" ? (payload.mode as string).trim() || undefined : undefined,
|
|
161
|
+
typeof payload?.skill === "string" ? (payload.skill as string).trim() || undefined : undefined,
|
|
162
|
+
];
|
|
163
|
+
let mode: string | undefined;
|
|
164
|
+
for (const candidate of candidates) {
|
|
165
|
+
if (candidate) {
|
|
166
|
+
mode = candidate;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (mode) assertKnownMode(mode);
|
|
171
|
+
|
|
172
|
+
let sessionId = flagValue(args, "--session-id")?.trim() || undefined;
|
|
173
|
+
if (!sessionId && payload && typeof payload.session_id === "string") {
|
|
174
|
+
sessionId = payload.session_id.trim() || undefined;
|
|
175
|
+
}
|
|
176
|
+
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
177
|
+
|
|
178
|
+
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
179
|
+
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
180
|
+
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
181
|
+
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
182
|
+
|
|
183
|
+
return { mode: mode as CanonicalGjcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function inferModeFromActiveState(
|
|
187
|
+
cwd: string,
|
|
188
|
+
sessionId: string | undefined,
|
|
189
|
+
): Promise<CanonicalGjcWorkflowSkill | undefined> {
|
|
190
|
+
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
191
|
+
const entries = listActiveSkills(state);
|
|
192
|
+
const candidate = entries[0]?.skill ?? state?.skill;
|
|
193
|
+
if (!candidate) return undefined;
|
|
194
|
+
const canonical = canonicalWorkflowSkill(candidate);
|
|
195
|
+
return canonical ?? undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function encodeSessionSegment(value: string): string {
|
|
199
|
+
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stateDirFor(cwd: string, sessionId: string | undefined): string {
|
|
203
|
+
const base = path.join(cwd, ".gjc", "state");
|
|
204
|
+
if (!sessionId) return base;
|
|
205
|
+
return path.join(base, "sessions", encodeSessionSegment(sessionId));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function modeStateFile(cwd: string, mode: string, sessionId: string | undefined): string {
|
|
209
|
+
return path.join(stateDirFor(cwd, sessionId), `${mode}-state.json`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function activeStateFile(cwd: string, sessionId: string | undefined): string {
|
|
213
|
+
return path.join(stateDirFor(cwd, sessionId), SKILL_ACTIVE_STATE_FILE);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
|
217
|
+
try {
|
|
218
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
219
|
+
const parsed = JSON.parse(raw);
|
|
220
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
221
|
+
return parsed as Record<string, unknown>;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const err = error as NodeJS.ErrnoException;
|
|
226
|
+
if (err.code === "ENOENT") return null;
|
|
227
|
+
throw new StateCommandError(1, `failed to read ${filePath}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
|
|
232
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
233
|
+
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
234
|
+
await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
235
|
+
await fs.rename(tmp, filePath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
239
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Shallow-merge `source` into `target`, with the convention that a `source` key whose value is
|
|
244
|
+
* `null` deletes that key from `target`. Nested objects are replaced wholesale (not deep-merged)
|
|
245
|
+
* so callers retain explicit control over substructure semantics; pre-existing skills that want
|
|
246
|
+
* to merge nested fields can supply the full sub-object themselves.
|
|
247
|
+
*/
|
|
248
|
+
function mergeWithNullDelete(
|
|
249
|
+
target: Record<string, unknown>,
|
|
250
|
+
source: Record<string, unknown>,
|
|
251
|
+
): Record<string, unknown> {
|
|
252
|
+
const result: Record<string, unknown> = { ...target };
|
|
253
|
+
for (const [key, value] of Object.entries(source)) {
|
|
254
|
+
if (value === null) {
|
|
255
|
+
delete result[key];
|
|
256
|
+
} else {
|
|
257
|
+
result[key] = value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function nowIso(): string {
|
|
264
|
+
return new Date().toISOString();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildHudForMode(
|
|
268
|
+
mode: CanonicalGjcWorkflowSkill,
|
|
269
|
+
payload: Record<string, unknown>,
|
|
270
|
+
): WorkflowHudSummary | undefined {
|
|
271
|
+
const updatedAt = new Date().toISOString();
|
|
272
|
+
const phase = typeof payload.current_phase === "string" ? payload.current_phase : undefined;
|
|
273
|
+
const stateField = isPlainObject(payload.state) ? (payload.state as Record<string, unknown>) : {};
|
|
274
|
+
switch (mode) {
|
|
275
|
+
case "deep-interview": {
|
|
276
|
+
const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
|
|
277
|
+
const v = (stateField as Record<string, unknown>)[key] ?? (payload as Record<string, unknown>)[key];
|
|
278
|
+
return guard(v) ? v : undefined;
|
|
279
|
+
};
|
|
280
|
+
const isNumber = (v: unknown): v is number => typeof v === "number";
|
|
281
|
+
const isString = (v: unknown): v is string => typeof v === "string";
|
|
282
|
+
const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
|
|
283
|
+
const ambiguity = pick("current_ambiguity", isNumber);
|
|
284
|
+
const threshold = pick("threshold", isNumber);
|
|
285
|
+
const rounds = pick("rounds", isArray);
|
|
286
|
+
const targetComponent = pick("last_targeted_component_id", isString);
|
|
287
|
+
const weakestDimension = pick("weakest_dimension", isString);
|
|
288
|
+
return buildDeepInterviewHudSummary({
|
|
289
|
+
phase,
|
|
290
|
+
ambiguity,
|
|
291
|
+
threshold,
|
|
292
|
+
roundCount: rounds?.length,
|
|
293
|
+
targetComponent,
|
|
294
|
+
weakestDimension,
|
|
295
|
+
updatedAt,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
case "ralplan": {
|
|
299
|
+
const stage =
|
|
300
|
+
typeof payload.current_phase === "string"
|
|
301
|
+
? (payload.current_phase as string)
|
|
302
|
+
: typeof payload.mode === "string"
|
|
303
|
+
? (payload.mode as string)
|
|
304
|
+
: undefined;
|
|
305
|
+
const verdict = typeof payload.verdict === "string" ? (payload.verdict as string) : undefined;
|
|
306
|
+
const iteration = typeof payload.iteration === "number" ? (payload.iteration as number) : undefined;
|
|
307
|
+
const pendingApproval = payload.pending_approval === true || stage === "final";
|
|
308
|
+
return buildRalplanHudSummary({
|
|
309
|
+
stage,
|
|
310
|
+
verdict,
|
|
311
|
+
iteration,
|
|
312
|
+
pendingApproval,
|
|
313
|
+
updatedAt,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
case "ultragoal": {
|
|
317
|
+
const goals = Array.isArray(payload.goals)
|
|
318
|
+
? (payload.goals as Array<{ id?: string; title?: string; status?: string }>).filter(
|
|
319
|
+
g => g && typeof g.id === "string" && typeof g.title === "string" && typeof g.status === "string",
|
|
320
|
+
)
|
|
321
|
+
: [];
|
|
322
|
+
const counts: Record<string, number> = {};
|
|
323
|
+
for (const goal of goals) {
|
|
324
|
+
const status = goal.status as string;
|
|
325
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
326
|
+
}
|
|
327
|
+
const currentGoalRaw = goals.find(g => g.status === "active") ?? goals.find(g => g.status === "pending");
|
|
328
|
+
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
329
|
+
return buildUltragoalHudSummary({
|
|
330
|
+
status,
|
|
331
|
+
currentGoal: currentGoalRaw
|
|
332
|
+
? {
|
|
333
|
+
id: currentGoalRaw.id as string,
|
|
334
|
+
title: currentGoalRaw.title as string,
|
|
335
|
+
status: currentGoalRaw.status as string,
|
|
336
|
+
}
|
|
337
|
+
: undefined,
|
|
338
|
+
counts,
|
|
339
|
+
goals: goals.map(g => ({ id: g.id as string, title: g.title as string, status: g.status as string })),
|
|
340
|
+
updatedAt,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
case "team": {
|
|
344
|
+
const teamPhase = typeof payload.phase === "string" ? (payload.phase as string) : (phase ?? "running");
|
|
345
|
+
const taskCounts =
|
|
346
|
+
typeof payload.task_counts === "object" && payload.task_counts && !Array.isArray(payload.task_counts)
|
|
347
|
+
? (payload.task_counts as Record<string, number>)
|
|
348
|
+
: {};
|
|
349
|
+
const taskTotal = typeof payload.task_total === "number" ? (payload.task_total as number) : 0;
|
|
350
|
+
const workers = Array.isArray(payload.workers)
|
|
351
|
+
? (payload.workers as Array<{ id?: string; status?: string }>)
|
|
352
|
+
.filter(w => w && typeof w.id === "string")
|
|
353
|
+
.map(w => ({
|
|
354
|
+
id: w.id as string,
|
|
355
|
+
status: typeof w.status === "string" ? (w.status as string) : undefined,
|
|
356
|
+
}))
|
|
357
|
+
: [];
|
|
358
|
+
return buildTeamHudSummary({
|
|
359
|
+
phase: teamPhase,
|
|
360
|
+
task_total: taskTotal,
|
|
361
|
+
task_counts: taskCounts,
|
|
362
|
+
workers,
|
|
363
|
+
updated_at: updatedAt,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function syncWorkflowSkillState(options: {
|
|
372
|
+
cwd: string;
|
|
373
|
+
mode: CanonicalGjcWorkflowSkill;
|
|
374
|
+
sessionId: string | undefined;
|
|
375
|
+
threadId?: string;
|
|
376
|
+
turnId?: string;
|
|
377
|
+
active: boolean;
|
|
378
|
+
phase: string | undefined;
|
|
379
|
+
payload: Record<string, unknown>;
|
|
380
|
+
receipt?: WorkflowStateReceipt;
|
|
381
|
+
}): Promise<void> {
|
|
382
|
+
try {
|
|
383
|
+
await syncSkillActiveState({
|
|
384
|
+
cwd: options.cwd,
|
|
385
|
+
skill: options.mode,
|
|
386
|
+
active: options.active,
|
|
387
|
+
phase: options.phase,
|
|
388
|
+
sessionId: options.sessionId,
|
|
389
|
+
threadId: options.threadId,
|
|
390
|
+
turnId: options.turnId,
|
|
391
|
+
source: "gjc-state-cli",
|
|
392
|
+
hud: buildHudForMode(options.mode, options.payload),
|
|
393
|
+
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
394
|
+
});
|
|
395
|
+
} catch {
|
|
396
|
+
// HUD sync is best-effort and must not change command semantics.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function handleRead(
|
|
401
|
+
args: readonly string[],
|
|
402
|
+
cwd: string,
|
|
403
|
+
positionalSkill: string | undefined,
|
|
404
|
+
): Promise<StateCommandResult> {
|
|
405
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
406
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
|
|
407
|
+
if (mode) {
|
|
408
|
+
const filePath = modeStateFile(cwd, mode, selectors.sessionId);
|
|
409
|
+
const existing = await readJsonFile(filePath);
|
|
410
|
+
return {
|
|
411
|
+
status: 0,
|
|
412
|
+
stdout: `${JSON.stringify({ skill: mode, state: existing, storage_path: filePath }, null, 2)}\n`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const filePath = activeStateFile(cwd, selectors.sessionId);
|
|
416
|
+
const existing = await readJsonFile(filePath);
|
|
417
|
+
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function handleWrite(
|
|
421
|
+
args: readonly string[],
|
|
422
|
+
cwd: string,
|
|
423
|
+
positionalSkill: string | undefined,
|
|
424
|
+
): Promise<StateCommandResult> {
|
|
425
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
426
|
+
const { sessionId, threadId, turnId, payload } = selectors;
|
|
427
|
+
if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
|
|
428
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
429
|
+
if (!mode)
|
|
430
|
+
throw new StateCommandError(
|
|
431
|
+
2,
|
|
432
|
+
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
436
|
+
const existing = await readJsonFile(filePath);
|
|
437
|
+
const nowIsoStr = nowIso();
|
|
438
|
+
const receipt = buildWorkflowStateReceipt({
|
|
439
|
+
cwd,
|
|
440
|
+
skill: mode,
|
|
441
|
+
owner: "gjc-state-cli",
|
|
442
|
+
command: `gjc state ${mode} write`,
|
|
443
|
+
sessionId,
|
|
444
|
+
nowIso: nowIsoStr,
|
|
445
|
+
});
|
|
446
|
+
const existingPayload = existing ?? {};
|
|
447
|
+
const innerState = (payload.state as Record<string, unknown> | undefined) ?? {};
|
|
448
|
+
const incomingPhase =
|
|
449
|
+
typeof payload.current_phase === "string" && payload.current_phase.trim()
|
|
450
|
+
? payload.current_phase.trim()
|
|
451
|
+
: typeof payload.phase === "string" && payload.phase.trim()
|
|
452
|
+
? payload.phase.trim()
|
|
453
|
+
: typeof innerState.current_phase === "string" && (innerState.current_phase as string).trim()
|
|
454
|
+
? (innerState.current_phase as string).trim()
|
|
455
|
+
: undefined;
|
|
456
|
+
let merged: Record<string, unknown>;
|
|
457
|
+
if (hasFlag(args, "--replace")) {
|
|
458
|
+
merged = { ...payload };
|
|
459
|
+
} else {
|
|
460
|
+
merged = mergeWithNullDelete(existingPayload, payload);
|
|
461
|
+
// Flatten payload.state.* into the top-level envelope so downstream consumers
|
|
462
|
+
// see a single canonical structure with the receipt at top level.
|
|
463
|
+
if (payload.state && typeof payload.state === "object" && !Array.isArray(payload.state)) {
|
|
464
|
+
merged = mergeWithNullDelete(merged, payload.state as Record<string, unknown>);
|
|
465
|
+
delete merged.state;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
merged.skill = mode;
|
|
469
|
+
if (incomingPhase) {
|
|
470
|
+
merged.current_phase = incomingPhase;
|
|
471
|
+
} else if (typeof merged.current_phase !== "string") {
|
|
472
|
+
merged.current_phase =
|
|
473
|
+
typeof existingPayload.current_phase === "string" ? existingPayload.current_phase : "active";
|
|
474
|
+
}
|
|
475
|
+
if (typeof merged.version !== "number") merged.version = 1;
|
|
476
|
+
if (typeof merged.active !== "boolean") merged.active = true;
|
|
477
|
+
merged.updated_at = nowIsoStr;
|
|
478
|
+
merged.receipt = receipt;
|
|
479
|
+
if (sessionId && typeof merged.session_id !== "string") merged.session_id = sessionId;
|
|
480
|
+
await writeJsonAtomic(filePath, merged);
|
|
481
|
+
|
|
482
|
+
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
483
|
+
const active = merged.active !== false;
|
|
484
|
+
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
status: 0,
|
|
488
|
+
stdout: `${JSON.stringify({ skill: mode, state: merged, receipt }, null, 2)}\n`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function handleClear(
|
|
493
|
+
args: readonly string[],
|
|
494
|
+
cwd: string,
|
|
495
|
+
positionalSkill: string | undefined,
|
|
496
|
+
): Promise<StateCommandResult> {
|
|
497
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
498
|
+
const { sessionId, threadId, turnId } = selectors;
|
|
499
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
500
|
+
if (!mode)
|
|
501
|
+
throw new StateCommandError(
|
|
502
|
+
2,
|
|
503
|
+
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
507
|
+
const existing = (await readJsonFile(filePath)) ?? {};
|
|
508
|
+
const cleared: Record<string, unknown> = {
|
|
509
|
+
...existing,
|
|
510
|
+
active: false,
|
|
511
|
+
current_phase: "complete",
|
|
512
|
+
updated_at: nowIso(),
|
|
513
|
+
};
|
|
514
|
+
await writeJsonAtomic(filePath, cleared);
|
|
515
|
+
|
|
516
|
+
await syncWorkflowSkillState({
|
|
517
|
+
cwd,
|
|
518
|
+
mode,
|
|
519
|
+
sessionId,
|
|
520
|
+
threadId,
|
|
521
|
+
turnId,
|
|
522
|
+
active: false,
|
|
523
|
+
phase: "complete",
|
|
524
|
+
payload: cleared,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return { status: 0, stdout: `${JSON.stringify(cleared, null, 2)}\n` };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function handleContract(
|
|
531
|
+
args: readonly string[],
|
|
532
|
+
cwd: string,
|
|
533
|
+
positionalSkill: string | undefined,
|
|
534
|
+
): Promise<StateCommandResult> {
|
|
535
|
+
const { mode } = await resolveSelectors(args, cwd, positionalSkill);
|
|
536
|
+
if (!mode) {
|
|
537
|
+
throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
|
|
538
|
+
}
|
|
539
|
+
const payload = { skill: mode, contract: describeWorkflowStateContract(mode) };
|
|
540
|
+
return { status: 0, stdout: `${JSON.stringify(payload, null, 2)}\n` };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function runNativeStateCommand(args: string[], cwd = process.cwd()): Promise<StateCommandResult> {
|
|
544
|
+
try {
|
|
545
|
+
const parsed = parsePositionalArgs(args);
|
|
546
|
+
switch (parsed.action) {
|
|
547
|
+
case "read":
|
|
548
|
+
return await handleRead(args, cwd, parsed.positionalSkill);
|
|
549
|
+
case "write":
|
|
550
|
+
return await handleWrite(args, cwd, parsed.positionalSkill);
|
|
551
|
+
case "clear":
|
|
552
|
+
return await handleClear(args, cwd, parsed.positionalSkill);
|
|
553
|
+
case "contract":
|
|
554
|
+
return await handleContract(args, cwd, parsed.positionalSkill);
|
|
555
|
+
default:
|
|
556
|
+
return { status: 2, stderr: `Unknown gjc state command: ${parsed.action}\n` };
|
|
557
|
+
}
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
560
|
+
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
561
|
+
}
|
|
562
|
+
}
|