@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
3
4
|
|
|
4
5
|
export const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
5
|
-
export const SKILL_ACTIVE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
6
6
|
|
|
7
7
|
export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
|
|
8
8
|
|
|
@@ -25,6 +25,8 @@ export interface WorkflowHudSummary {
|
|
|
25
25
|
updated_at?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export type { WorkflowStateReceipt } from "./workflow-state-contract";
|
|
29
|
+
|
|
28
30
|
export interface SkillActiveEntry {
|
|
29
31
|
skill: string;
|
|
30
32
|
phase?: string;
|
|
@@ -36,6 +38,10 @@ export interface SkillActiveEntry {
|
|
|
36
38
|
turn_id?: string;
|
|
37
39
|
hud?: WorkflowHudSummary;
|
|
38
40
|
stale?: boolean;
|
|
41
|
+
receipt?: WorkflowStateReceipt;
|
|
42
|
+
handoff_from?: string;
|
|
43
|
+
handoff_to?: string;
|
|
44
|
+
handoff_at?: string;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export interface SkillActiveState {
|
|
@@ -70,6 +76,10 @@ export interface SyncSkillActiveStateOptions {
|
|
|
70
76
|
nowIso?: string;
|
|
71
77
|
source?: string;
|
|
72
78
|
hud?: WorkflowHudSummary;
|
|
79
|
+
receipt?: WorkflowStateReceipt;
|
|
80
|
+
handoff_from?: string;
|
|
81
|
+
handoff_to?: string;
|
|
82
|
+
handoff_at?: string;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
const HUD_TEXT_LIMIT = 80;
|
|
@@ -142,6 +152,36 @@ export function normalizeWorkflowHudSummary(raw: unknown): WorkflowHudSummary |
|
|
|
142
152
|
};
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
function normalizeWorkflowStateReceipt(raw: unknown): WorkflowStateReceipt | undefined {
|
|
156
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
157
|
+
const record = raw as Record<string, unknown>;
|
|
158
|
+
if (record.version !== 1) return undefined;
|
|
159
|
+
const skill = safeString(record.skill).trim();
|
|
160
|
+
if (!isCanonicalGjcWorkflowSkill(skill)) return undefined;
|
|
161
|
+
const owner = safeString(record.owner).trim();
|
|
162
|
+
if (owner !== "gjc-state-cli" && owner !== "gjc-runtime" && owner !== "gjc-hook") return undefined;
|
|
163
|
+
const command = sanitizeHudString(record.command, 120);
|
|
164
|
+
const statePath = sanitizeHudString(record.state_path, 240);
|
|
165
|
+
const storagePath = sanitizeHudString(record.storage_path, 240);
|
|
166
|
+
const mutatedAt = sanitizeHudString(record.mutated_at, 40);
|
|
167
|
+
const freshUntil = sanitizeHudString(record.fresh_until, 40);
|
|
168
|
+
const status = safeString(record.status).trim();
|
|
169
|
+
const mutationId = sanitizeHudString(record.mutation_id, 120);
|
|
170
|
+
if (!command || !statePath || !storagePath || !mutatedAt || !freshUntil || !mutationId) return undefined;
|
|
171
|
+
return {
|
|
172
|
+
version: 1,
|
|
173
|
+
skill,
|
|
174
|
+
owner,
|
|
175
|
+
command,
|
|
176
|
+
state_path: statePath,
|
|
177
|
+
storage_path: storagePath,
|
|
178
|
+
mutated_at: mutatedAt,
|
|
179
|
+
fresh_until: freshUntil,
|
|
180
|
+
status: status === "stale" ? "stale" : "fresh",
|
|
181
|
+
mutation_id: mutationId,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
function encodePathSegment(value: string): string {
|
|
146
186
|
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
147
187
|
}
|
|
@@ -150,31 +190,13 @@ function entryKey(entry: Pick<SkillActiveEntry, "skill" | "session_id">): string
|
|
|
150
190
|
return `${entry.skill}::${safeString(entry.session_id).trim()}`;
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
function timestampMs(value: string | undefined): number | null {
|
|
154
|
-
if (!value) return null;
|
|
155
|
-
const ms = Date.parse(value);
|
|
156
|
-
return Number.isFinite(ms) ? ms : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function entryTimestampMs(entry: SkillActiveEntry): number | null {
|
|
160
|
-
return timestampMs(entry.hud?.updated_at) ?? timestampMs(entry.updated_at) ?? timestampMs(entry.activated_at);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function isFreshEntry(entry: SkillActiveEntry, nowMs = Date.now()): boolean {
|
|
164
|
-
const ms = entryTimestampMs(entry);
|
|
165
|
-
return ms === null || nowMs - ms <= SKILL_ACTIVE_STALE_MS;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function withDerivedStale(entry: SkillActiveEntry, nowMs = Date.now()): SkillActiveEntry {
|
|
169
|
-
return { ...entry, stale: !isFreshEntry(entry, nowMs) };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
193
|
function normalizeEntry(raw: unknown): SkillActiveEntry | null {
|
|
173
194
|
if (!raw || typeof raw !== "object") return null;
|
|
174
195
|
const record = raw as Record<string, unknown>;
|
|
175
196
|
const skill = safeString(record.skill).trim();
|
|
176
197
|
if (!skill) return null;
|
|
177
198
|
const hud = normalizeWorkflowHudSummary(record.hud);
|
|
199
|
+
const receipt = normalizeWorkflowStateReceipt(record.receipt);
|
|
178
200
|
return {
|
|
179
201
|
...record,
|
|
180
202
|
skill,
|
|
@@ -185,7 +207,11 @@ function normalizeEntry(raw: unknown): SkillActiveEntry | null {
|
|
|
185
207
|
session_id: safeString(record.session_id).trim() || undefined,
|
|
186
208
|
thread_id: safeString(record.thread_id).trim() || undefined,
|
|
187
209
|
turn_id: safeString(record.turn_id).trim() || undefined,
|
|
210
|
+
handoff_from: safeString(record.handoff_from).trim() || undefined,
|
|
211
|
+
handoff_to: safeString(record.handoff_to).trim() || undefined,
|
|
212
|
+
handoff_at: safeString(record.handoff_at).trim() || undefined,
|
|
188
213
|
...(hud ? { hud } : {}),
|
|
214
|
+
...(receipt ? { receipt } : {}),
|
|
189
215
|
stale: undefined,
|
|
190
216
|
};
|
|
191
217
|
}
|
|
@@ -268,6 +294,48 @@ async function readStateFile(filePath: string): Promise<SkillActiveState | null>
|
|
|
268
294
|
}
|
|
269
295
|
}
|
|
270
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Raw read for handoff mutations. Returns the *unnormalized* parsed object so
|
|
299
|
+
* inactive entries remain visible to `rawActiveEntries` — `normalizeSkillActiveState`
|
|
300
|
+
* delegates to `listActiveSkills`, which filters out `active:false` rows for HUD
|
|
301
|
+
* purposes. Handoff history (e.g. previously demoted callers carrying
|
|
302
|
+
* `handoff_to`/`handoff_at` lineage) must survive across successive handoffs,
|
|
303
|
+
* so the on-disk `active_skills` array is preserved verbatim and the next
|
|
304
|
+
* write recomputes the per-skill row from there.
|
|
305
|
+
*
|
|
306
|
+
* Strict semantics: tolerates ENOENT only. Corrupt JSON / non-ENOENT I/O
|
|
307
|
+
* errors propagate so callers can surface a non-zero CLI status.
|
|
308
|
+
*/
|
|
309
|
+
async function readRawActiveStateForHandoff(filePath: string, strict: boolean): Promise<SkillActiveState | null> {
|
|
310
|
+
let raw: string;
|
|
311
|
+
try {
|
|
312
|
+
raw = await Bun.file(filePath).text();
|
|
313
|
+
} catch (err) {
|
|
314
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
315
|
+
if (code === "ENOENT") return null;
|
|
316
|
+
if (!strict) return null;
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(raw);
|
|
321
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
322
|
+
return parsed as SkillActiveState;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (!strict) return null;
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function rawActiveEntries(state: SkillActiveState | null): SkillActiveEntry[] {
|
|
330
|
+
if (!state || !Array.isArray(state.active_skills)) return [];
|
|
331
|
+
const out: SkillActiveEntry[] = [];
|
|
332
|
+
for (const candidate of state.active_skills) {
|
|
333
|
+
const normalized = normalizeEntry(candidate);
|
|
334
|
+
if (normalized) out.push(normalized);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
271
339
|
function filterRootEntriesForSession(entries: SkillActiveEntry[], sessionId?: string): SkillActiveEntry[] {
|
|
272
340
|
const normalizedSessionId = safeString(sessionId).trim();
|
|
273
341
|
if (!normalizedSessionId) return entries;
|
|
@@ -282,12 +350,9 @@ function mergeVisibleEntries(
|
|
|
282
350
|
rootState: SkillActiveState | null,
|
|
283
351
|
sessionId?: string,
|
|
284
352
|
): SkillActiveEntry[] {
|
|
285
|
-
const
|
|
286
|
-
const rootEntries = filterRootEntriesForSession(listActiveSkills(rootState), sessionId).map(entry =>
|
|
287
|
-
withDerivedStale(entry, nowMs),
|
|
288
|
-
);
|
|
353
|
+
const rootEntries = filterRootEntriesForSession(listActiveSkills(rootState), sessionId);
|
|
289
354
|
const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
|
|
290
|
-
for (const entry of listActiveSkills(sessionState)
|
|
355
|
+
for (const entry of listActiveSkills(sessionState)) {
|
|
291
356
|
merged.set(entryKey(entry), entry);
|
|
292
357
|
}
|
|
293
358
|
return [...merged.values()];
|
|
@@ -337,7 +402,11 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
337
402
|
session_id: options.sessionId,
|
|
338
403
|
thread_id: options.threadId,
|
|
339
404
|
turn_id: options.turnId,
|
|
405
|
+
...(options.handoff_from ? { handoff_from: options.handoff_from } : {}),
|
|
406
|
+
...(options.handoff_to ? { handoff_to: options.handoff_to } : {}),
|
|
407
|
+
...(options.handoff_at ? { handoff_at: options.handoff_at } : {}),
|
|
340
408
|
...(hud ? { hud } : {}),
|
|
409
|
+
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
341
410
|
};
|
|
342
411
|
const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, options.sessionId);
|
|
343
412
|
const rootState = (await readStateFile(rootPath)) ?? { version: 1, active_skills: [] };
|
|
@@ -370,3 +439,97 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
370
439
|
};
|
|
371
440
|
await writeStateFile(sessionPath, nextSession);
|
|
372
441
|
}
|
|
442
|
+
|
|
443
|
+
export interface ApplyHandoffOptions {
|
|
444
|
+
cwd: string;
|
|
445
|
+
caller: SyncSkillActiveStateOptions;
|
|
446
|
+
callee: SyncSkillActiveStateOptions;
|
|
447
|
+
/** Shared timestamp; falls back to new Date().toISOString(). */
|
|
448
|
+
nowIso?: string;
|
|
449
|
+
/** When true, read errors other than ENOENT propagate. */
|
|
450
|
+
strict?: boolean;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Atomically apply a workflow-skill handoff to both the session-scoped and
|
|
455
|
+
* root `skill-active-state.json` files in a single write per file.
|
|
456
|
+
*
|
|
457
|
+
* Write order: **session first, root last**. The session file is the
|
|
458
|
+
* source of truth for HUD; the root aggregate must never lead the session
|
|
459
|
+
* during a handoff window. Each file is rewritten once with caller demoted
|
|
460
|
+
* to `active:false` (preserving `handoff_to`/`handoff_at` lineage) and
|
|
461
|
+
* callee promoted to `active:true` (with `handoff_from`/`handoff_at`).
|
|
462
|
+
*/
|
|
463
|
+
export async function applyHandoffToActiveState(options: ApplyHandoffOptions): Promise<void> {
|
|
464
|
+
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
465
|
+
const callerEntry = buildSyncEntry(options.caller, nowIso);
|
|
466
|
+
const calleeEntry = buildSyncEntry(options.callee, nowIso);
|
|
467
|
+
const sessionId = options.callee.sessionId ?? options.caller.sessionId;
|
|
468
|
+
const { rootPath, sessionPath } = getSkillActiveStatePaths(options.cwd, sessionId);
|
|
469
|
+
const readState = (filePath: string) => readRawActiveStateForHandoff(filePath, options.strict === true);
|
|
470
|
+
|
|
471
|
+
const applyEntries = (entries: SkillActiveEntry[]): SkillActiveEntry[] => {
|
|
472
|
+
const callerKey = entryKey(callerEntry);
|
|
473
|
+
const calleeKey = entryKey(calleeEntry);
|
|
474
|
+
const priorCaller = entries.find(e => entryKey(e) === callerKey);
|
|
475
|
+
const kept = entries.filter(e => entryKey(e) !== callerKey && entryKey(e) !== calleeKey);
|
|
476
|
+
// Merge prior lineage into the demoted caller so multi-step handoff
|
|
477
|
+
// chains preserve `handoff_from` from the previous transition while
|
|
478
|
+
// the new `handoff_to`/`handoff_at` describe this one.
|
|
479
|
+
const mergedCaller: SkillActiveEntry = priorCaller
|
|
480
|
+
? {
|
|
481
|
+
...callerEntry,
|
|
482
|
+
...(priorCaller.handoff_from && !callerEntry.handoff_from
|
|
483
|
+
? { handoff_from: priorCaller.handoff_from }
|
|
484
|
+
: {}),
|
|
485
|
+
}
|
|
486
|
+
: callerEntry;
|
|
487
|
+
return [...kept, mergedCaller, calleeEntry];
|
|
488
|
+
};
|
|
489
|
+
const buildNextState = (
|
|
490
|
+
prior: SkillActiveState | null,
|
|
491
|
+
entries: SkillActiveEntry[],
|
|
492
|
+
scope: "session" | "root",
|
|
493
|
+
): SkillActiveState => {
|
|
494
|
+
const visible = entries.filter(e => e.active !== false);
|
|
495
|
+
return {
|
|
496
|
+
...(prior ?? {}),
|
|
497
|
+
version: 1,
|
|
498
|
+
active: visible.length > 0,
|
|
499
|
+
skill: visible[0]?.skill ?? "",
|
|
500
|
+
phase: visible[0]?.phase ?? "",
|
|
501
|
+
...(scope === "session" ? { session_id: sessionId } : {}),
|
|
502
|
+
updated_at: nowIso,
|
|
503
|
+
source: options.callee.source ?? options.caller.source,
|
|
504
|
+
active_skills: entries,
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
if (sessionPath) {
|
|
509
|
+
const prior = await readState(sessionPath);
|
|
510
|
+
const next = buildNextState(prior, applyEntries(rawActiveEntries(prior)), "session");
|
|
511
|
+
await writeStateFile(sessionPath, next);
|
|
512
|
+
}
|
|
513
|
+
const priorRoot = await readState(rootPath);
|
|
514
|
+
const nextRoot = buildNextState(priorRoot, applyEntries(rawActiveEntries(priorRoot)), "root");
|
|
515
|
+
await writeStateFile(rootPath, nextRoot);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildSyncEntry(options: SyncSkillActiveStateOptions, nowIso: string): SkillActiveEntry {
|
|
519
|
+
const hud = normalizeWorkflowHudSummary(options.hud);
|
|
520
|
+
return {
|
|
521
|
+
skill: options.skill,
|
|
522
|
+
phase: options.phase,
|
|
523
|
+
active: options.active,
|
|
524
|
+
activated_at: nowIso,
|
|
525
|
+
updated_at: nowIso,
|
|
526
|
+
session_id: options.sessionId,
|
|
527
|
+
thread_id: options.threadId,
|
|
528
|
+
turn_id: options.turnId,
|
|
529
|
+
...(options.handoff_from ? { handoff_from: options.handoff_from } : {}),
|
|
530
|
+
...(options.handoff_to ? { handoff_to: options.handoff_to } : {}),
|
|
531
|
+
...(options.handoff_at ? { handoff_at: options.handoff_at } : {}),
|
|
532
|
+
...(hud ? { hud } : {}),
|
|
533
|
+
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
@@ -5,14 +5,20 @@ import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/lo
|
|
|
5
5
|
import { resolveToCwd } from "../tools/path-utils";
|
|
6
6
|
import { ToolError } from "../tools/tool-errors";
|
|
7
7
|
import { listActiveSkills, readVisibleSkillActiveState, type SkillActiveEntry } from "./active-state";
|
|
8
|
+
import {
|
|
9
|
+
type CanonicalGjcWorkflowSkill,
|
|
10
|
+
sanctionedWorkflowStateCommand,
|
|
11
|
+
workflowModeStateFileName,
|
|
12
|
+
} from "./workflow-state-contract";
|
|
8
13
|
|
|
9
14
|
export const DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE =
|
|
10
|
-
"Deep-interview
|
|
15
|
+
"Deep-interview phase boundary: continue gathering context/questions/risks and emit a handoff/spec before code edits. Mutation tools and patch execution are blocked while deep-interview is active; finalize specs through `gjc deep-interview --write --stage final` or hand off to an execution phase.";
|
|
16
|
+
export const WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE =
|
|
17
|
+
"Workflow state JSON is runtime-owned. Use `gjc state <skill> read|write --input '<json>'` for deep-interview, ralplan, ultragoal, and team. Planning artifacts under `.gjc/specs/` and `.gjc/plans/` remain allowed.";
|
|
11
18
|
|
|
12
19
|
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
13
20
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
14
21
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
15
|
-
const GLOB_META_RE = /[*?[\]{}]/;
|
|
16
22
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
17
23
|
|
|
18
24
|
type ToolWithEditMode = AgentTool & {
|
|
@@ -26,6 +32,8 @@ export interface DeepInterviewMutationGuardInput {
|
|
|
26
32
|
threadId?: string;
|
|
27
33
|
tool: ToolWithEditMode;
|
|
28
34
|
args: unknown;
|
|
35
|
+
forceOverride?: boolean;
|
|
36
|
+
enforceWorkflowState?: boolean;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
interface ExtractedTargets {
|
|
@@ -38,6 +46,7 @@ export interface DeepInterviewMutationDecision {
|
|
|
38
46
|
message?: string;
|
|
39
47
|
targets: string[];
|
|
40
48
|
reason?: string;
|
|
49
|
+
command?: string;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface ModeState {
|
|
@@ -79,7 +88,7 @@ async function readVisibleModeState(cwd: string, skill: string, sessionId?: stri
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
function isTerminalModeState(state: ModeState | null): boolean {
|
|
82
|
-
if (
|
|
91
|
+
if (state?.active !== true) return true;
|
|
83
92
|
const phase = String(state.current_phase ?? "")
|
|
84
93
|
.trim()
|
|
85
94
|
.toLowerCase();
|
|
@@ -246,34 +255,57 @@ function resolveRawPath(cwd: string, rawPath: string): { absolutePath?: string;
|
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
function
|
|
258
|
+
function relativeGjcSegments(cwd: string, rawPath: string): string[] | null {
|
|
250
259
|
const { absolutePath, unknown } = resolveRawPath(cwd, rawPath);
|
|
251
|
-
if (unknown || !absolutePath) return
|
|
260
|
+
if (unknown || !absolutePath) return null;
|
|
252
261
|
const relative = path.relative(path.resolve(cwd), path.resolve(absolutePath));
|
|
253
|
-
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return
|
|
254
|
-
|
|
255
|
-
return segments[0] === ".gjc" && (segments[1] === "specs" || segments[1] === "state");
|
|
262
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
|
263
|
+
return normalizePosix(relative).split("/").filter(Boolean);
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
function blockedWorkflowStateSkill(cwd: string, rawPath: string): CanonicalGjcWorkflowSkill | null {
|
|
267
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
268
|
+
if (segments?.[0] !== ".gjc") return null;
|
|
269
|
+
if (segments[1] === "specs" || segments[1] === "plans") return null;
|
|
270
|
+
if (segments[1] !== "state") return null;
|
|
271
|
+
const fileName = segments.at(-1) ?? "";
|
|
272
|
+
for (const skillName of ["deep-interview", "ralplan", "ultragoal", "team"] as const) {
|
|
273
|
+
if (fileName === workflowModeStateFileName(skillName)) return skillName;
|
|
274
|
+
}
|
|
275
|
+
if (fileName === "skill-active-state.json") return "deep-interview";
|
|
276
|
+
return null;
|
|
266
277
|
}
|
|
267
278
|
|
|
279
|
+
function firstBlockedWorkflowStateSkill(cwd: string, targets: ExtractedTargets): CanonicalGjcWorkflowSkill | null {
|
|
280
|
+
for (const rawPath of targets.paths) {
|
|
281
|
+
const skill = blockedWorkflowStateSkill(cwd, rawPath);
|
|
282
|
+
if (skill) return skill;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isAllowlistedPath(cwd: string, rawPath: string): boolean {
|
|
288
|
+
const segments = relativeGjcSegments(cwd, rawPath);
|
|
289
|
+
if (segments?.[0] !== ".gjc") return false;
|
|
290
|
+
return segments[1] === "specs" || segments[1] === "plans";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function allTargetsAllowlisted(cwd: string, targets: ExtractedTargets): boolean {
|
|
294
|
+
return (
|
|
295
|
+
!targets.unknown && targets.paths.length > 0 && targets.paths.every(rawPath => isAllowlistedPath(cwd, rawPath))
|
|
296
|
+
);
|
|
297
|
+
}
|
|
268
298
|
export async function assertDeepInterviewMutationRawPathsAllowed(input: {
|
|
269
299
|
cwd: string;
|
|
270
300
|
sessionId?: string;
|
|
271
301
|
threadId?: string;
|
|
272
302
|
rawPaths: string[];
|
|
303
|
+
forceOverride?: boolean;
|
|
273
304
|
}): Promise<void> {
|
|
305
|
+
if (input.forceOverride) return;
|
|
274
306
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) return;
|
|
275
307
|
const targets: ExtractedTargets = { paths: input.rawPaths, unknown: input.rawPaths.length === 0 };
|
|
276
|
-
if (
|
|
308
|
+
if (targets.unknown || targets.paths.length > 0) {
|
|
277
309
|
throw new ToolError(DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE);
|
|
278
310
|
}
|
|
279
311
|
}
|
|
@@ -282,18 +314,37 @@ export async function getDeepInterviewMutationDecision(
|
|
|
282
314
|
input: DeepInterviewMutationGuardInput,
|
|
283
315
|
): Promise<DeepInterviewMutationDecision> {
|
|
284
316
|
if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
|
|
317
|
+
const targets = extractTargets(input.tool, input.args);
|
|
318
|
+
if (input.enforceWorkflowState !== false) {
|
|
319
|
+
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
320
|
+
if (stateSkill) {
|
|
321
|
+
const command = sanctionedWorkflowStateCommand(stateSkill);
|
|
322
|
+
return {
|
|
323
|
+
blocked: true,
|
|
324
|
+
message: `${WORKFLOW_STATE_MUTATION_BLOCK_MESSAGE}\nUse: ${command}`,
|
|
325
|
+
targets: targets.paths,
|
|
326
|
+
reason: "workflow-state-target",
|
|
327
|
+
command,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
285
331
|
if (!(await isActiveDeepInterview(input.cwd, input.sessionId, input.threadId))) {
|
|
286
332
|
return { blocked: false, targets: [] };
|
|
287
333
|
}
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
return {
|
|
334
|
+
if (input.forceOverride) return { blocked: false, targets: [] };
|
|
335
|
+
if (targets.unknown) {
|
|
336
|
+
return {
|
|
337
|
+
blocked: true,
|
|
338
|
+
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
339
|
+
targets: targets.paths,
|
|
340
|
+
reason: "unknown-target",
|
|
341
|
+
};
|
|
291
342
|
}
|
|
292
343
|
return {
|
|
293
344
|
blocked: true,
|
|
294
345
|
message: DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE,
|
|
295
346
|
targets: targets.paths,
|
|
296
|
-
reason:
|
|
347
|
+
reason: allTargetsAllowlisted(input.cwd, targets) ? "handoff-artifact-tool-target" : "phase-boundary",
|
|
297
348
|
};
|
|
298
349
|
}
|
|
299
350
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CanonicalGjcWorkflowSkill } from "./active-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical initial phase for each GJC workflow skill. Used by both
|
|
5
|
+
* `recordSkillActivation` (UserPromptSubmit hook seeding initial mode-state)
|
|
6
|
+
* and the `gjc state <caller> handoff --to <callee>` runtime when promoting
|
|
7
|
+
* the callee.
|
|
8
|
+
*
|
|
9
|
+
* Keeping this mapping in a neutral skill-state module avoids cycles between
|
|
10
|
+
* `gjc-runtime/state-runtime.ts` and `hooks/skill-state.ts` (which pulls in
|
|
11
|
+
* session-manager and ultragoal verification code).
|
|
12
|
+
*/
|
|
13
|
+
export function initialPhaseForSkill(skill: CanonicalGjcWorkflowSkill | string): string {
|
|
14
|
+
if (skill === "deep-interview") return "interviewing";
|
|
15
|
+
if (skill === "ultragoal") return "goal-planning";
|
|
16
|
+
return "planning";
|
|
17
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { WorkflowHudChip, WorkflowHudSummary } from "./active-state";
|
|
2
2
|
|
|
3
|
-
interface
|
|
3
|
+
interface WorkflowGateHudState {
|
|
4
|
+
approvalStatus?: string;
|
|
5
|
+
blockedReason?: string;
|
|
6
|
+
nextAction?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DeepInterviewHudState extends WorkflowGateHudState {
|
|
4
10
|
phase?: string;
|
|
5
11
|
ambiguity?: number;
|
|
6
12
|
threshold?: number;
|
|
@@ -11,7 +17,7 @@ interface DeepInterviewHudState {
|
|
|
11
17
|
updatedAt?: string;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
interface RalplanHudState {
|
|
20
|
+
interface RalplanHudState extends WorkflowGateHudState {
|
|
15
21
|
stage?: string;
|
|
16
22
|
waiting?: string;
|
|
17
23
|
iteration?: number;
|
|
@@ -27,7 +33,7 @@ interface UltragoalLikeGoal {
|
|
|
27
33
|
status: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
interface UltragoalHudState {
|
|
36
|
+
interface UltragoalHudState extends WorkflowGateHudState {
|
|
31
37
|
status: string;
|
|
32
38
|
currentGoal?: UltragoalLikeGoal;
|
|
33
39
|
counts: Record<string, number>;
|
|
@@ -41,7 +47,7 @@ interface TeamHudWorker {
|
|
|
41
47
|
status?: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
interface TeamHudState {
|
|
50
|
+
interface TeamHudState extends WorkflowGateHudState {
|
|
45
51
|
phase: string;
|
|
46
52
|
task_total: number;
|
|
47
53
|
task_counts: Record<string, number>;
|
|
@@ -66,6 +72,14 @@ function chip(
|
|
|
66
72
|
return { label, value, priority, ...(severity ? { severity } : {}) };
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
function gateChips(state: WorkflowGateHudState, gatePriority: number): Array<WorkflowHudChip | null> {
|
|
76
|
+
return [
|
|
77
|
+
chip("gate", state.approvalStatus, gatePriority, state.approvalStatus === "approved" ? "success" : "warning"),
|
|
78
|
+
chip("blocked", state.blockedReason, gatePriority + 10, "blocked"),
|
|
79
|
+
chip("next", state.nextAction, gatePriority + 20),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function compactChips(chips: Array<WorkflowHudChip | null>): WorkflowHudChip[] {
|
|
70
84
|
return chips.filter((item): item is WorkflowHudChip => item !== null);
|
|
71
85
|
}
|
|
@@ -74,6 +88,7 @@ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): Work
|
|
|
74
88
|
return {
|
|
75
89
|
version: 1,
|
|
76
90
|
chips: compactChips([
|
|
91
|
+
...gateChips(state, 5),
|
|
77
92
|
chip("phase", state.phase, 10),
|
|
78
93
|
chip("ambiguity", [percent(state.ambiguity), percent(state.threshold)].filter(Boolean).join("/"), 20),
|
|
79
94
|
chip("round", state.roundCount === undefined ? undefined : String(state.roundCount), 30),
|
|
@@ -100,6 +115,7 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
|
|
|
100
115
|
summary: state.latestSummary,
|
|
101
116
|
chips: compactChips([
|
|
102
117
|
state.pendingApproval ? { label: "pending", value: "approval", priority: 5, severity: "warning" } : null,
|
|
118
|
+
...gateChips(state, 6),
|
|
103
119
|
chip("stage", state.stage, 10),
|
|
104
120
|
chip("waiting", state.waiting, 20),
|
|
105
121
|
chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
|
|
@@ -120,6 +136,7 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
|
|
|
120
136
|
chip("goals", `${complete}/${total}`, 10),
|
|
121
137
|
chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
|
|
122
138
|
chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
|
|
139
|
+
...gateChips(state, 40),
|
|
123
140
|
]),
|
|
124
141
|
details: state.latestLedgerEvent
|
|
125
142
|
? compactChips([
|
|
@@ -153,7 +170,8 @@ export function buildTeamHudSummary(state: TeamHudState): WorkflowHudSummary {
|
|
|
153
170
|
chip("phase", state.phase, 10),
|
|
154
171
|
chip("workers", `${state.workers.length - failedWorkers}/${state.workers.length}`, 20),
|
|
155
172
|
chip("tasks", `${completed}/${state.task_total}`, 30),
|
|
156
|
-
|
|
173
|
+
...gateChips(state, 40),
|
|
174
|
+
chip("latest", latest, 70),
|
|
157
175
|
]),
|
|
158
176
|
...(state.updated_at ? { updated_at: state.updated_at } : {}),
|
|
159
177
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill, SKILL_ACTIVE_STATE_FILE } from "./active-state";
|
|
3
|
+
|
|
4
|
+
export type { CanonicalGjcWorkflowSkill };
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_STATE_RECEIPT_VERSION = 1;
|
|
7
|
+
export const WORKFLOW_STATE_RECEIPT_FRESH_MS = 30 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export type WorkflowStateMutationOwner = "gjc-state-cli" | "gjc-runtime" | "gjc-hook";
|
|
10
|
+
export type WorkflowStateReceiptStatus = "fresh" | "stale";
|
|
11
|
+
|
|
12
|
+
export interface WorkflowStateReceipt {
|
|
13
|
+
version: 1;
|
|
14
|
+
skill: CanonicalGjcWorkflowSkill;
|
|
15
|
+
owner: WorkflowStateMutationOwner;
|
|
16
|
+
command: string;
|
|
17
|
+
state_path: string;
|
|
18
|
+
storage_path: string;
|
|
19
|
+
mutated_at: string;
|
|
20
|
+
fresh_until: string;
|
|
21
|
+
status: WorkflowStateReceiptStatus;
|
|
22
|
+
mutation_id: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeString(value: unknown): string {
|
|
26
|
+
return typeof value === "string" ? value : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function encodePathSegment(value: string): string {
|
|
30
|
+
return encodeURIComponent(value).replaceAll(".", "%2E");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function workflowModeStateFileName(skill: CanonicalGjcWorkflowSkill): string {
|
|
34
|
+
return `${skill}-state.json`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function workflowStateStoragePath(cwd: string, skill: CanonicalGjcWorkflowSkill, sessionId?: string): string {
|
|
38
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
39
|
+
if (normalizedSessionId) {
|
|
40
|
+
return path.join(
|
|
41
|
+
cwd,
|
|
42
|
+
".gjc",
|
|
43
|
+
"state",
|
|
44
|
+
"sessions",
|
|
45
|
+
encodePathSegment(normalizedSessionId),
|
|
46
|
+
workflowModeStateFileName(skill),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return path.join(cwd, ".gjc", "state", workflowModeStateFileName(skill));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function workflowActiveStatePath(cwd: string, sessionId?: string): string {
|
|
53
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
54
|
+
if (normalizedSessionId) {
|
|
55
|
+
return path.join(
|
|
56
|
+
cwd,
|
|
57
|
+
".gjc",
|
|
58
|
+
"state",
|
|
59
|
+
"sessions",
|
|
60
|
+
encodePathSegment(normalizedSessionId),
|
|
61
|
+
SKILL_ACTIVE_STATE_FILE,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return path.join(cwd, ".gjc", "state", SKILL_ACTIVE_STATE_FILE);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildWorkflowStateReceipt(input: {
|
|
68
|
+
cwd: string;
|
|
69
|
+
skill: CanonicalGjcWorkflowSkill;
|
|
70
|
+
owner: WorkflowStateMutationOwner;
|
|
71
|
+
command: string;
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
nowIso?: string;
|
|
74
|
+
mutationId?: string;
|
|
75
|
+
}): WorkflowStateReceipt {
|
|
76
|
+
const mutatedAt = input.nowIso ?? new Date().toISOString();
|
|
77
|
+
const freshUntil = new Date(Date.parse(mutatedAt) + WORKFLOW_STATE_RECEIPT_FRESH_MS).toISOString();
|
|
78
|
+
return {
|
|
79
|
+
version: WORKFLOW_STATE_RECEIPT_VERSION,
|
|
80
|
+
skill: input.skill,
|
|
81
|
+
owner: input.owner,
|
|
82
|
+
command: input.command,
|
|
83
|
+
state_path: workflowActiveStatePath(input.cwd, input.sessionId),
|
|
84
|
+
storage_path: workflowStateStoragePath(input.cwd, input.skill, input.sessionId),
|
|
85
|
+
mutated_at: mutatedAt,
|
|
86
|
+
fresh_until: freshUntil,
|
|
87
|
+
status: "fresh",
|
|
88
|
+
mutation_id: input.mutationId ?? `${input.skill}:${mutatedAt}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function workflowReceiptStatus(
|
|
93
|
+
receipt: WorkflowStateReceipt | undefined,
|
|
94
|
+
nowMs = Date.now(),
|
|
95
|
+
): WorkflowStateReceiptStatus | undefined {
|
|
96
|
+
if (!receipt) return undefined;
|
|
97
|
+
const freshUntilMs = Date.parse(receipt.fresh_until);
|
|
98
|
+
if (!Number.isFinite(freshUntilMs)) return "stale";
|
|
99
|
+
return nowMs <= freshUntilMs ? "fresh" : "stale";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function canonicalWorkflowSkill(value: string): CanonicalGjcWorkflowSkill | null {
|
|
103
|
+
return (CANONICAL_GJC_WORKFLOW_SKILLS as readonly string[]).includes(value)
|
|
104
|
+
? (value as CanonicalGjcWorkflowSkill)
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sanctionedWorkflowStateCommand(skill: CanonicalGjcWorkflowSkill): string {
|
|
109
|
+
return `gjc state ${skill} write --input '<json>'`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function describeWorkflowStateContract(skill: CanonicalGjcWorkflowSkill): string[] {
|
|
113
|
+
return [
|
|
114
|
+
`Sanctioned mutation path: gjc state ${skill} read|write --input '<json>'`,
|
|
115
|
+
`Canonical active HUD state: .gjc/state/${SKILL_ACTIVE_STATE_FILE} and .gjc/state/sessions/<session>/${SKILL_ACTIVE_STATE_FILE}`,
|
|
116
|
+
`Skill mode state: .gjc/state/${workflowModeStateFileName(skill)} or .gjc/state/sessions/<session>/${workflowModeStateFileName(skill)}`,
|
|
117
|
+
"Receipts include version, skill, owner, command, state_path, storage_path, mutated_at, fresh_until, status, and mutation_id.",
|
|
118
|
+
"Receipts are fresh for 30 minutes; older receipts are stale and render as HUD warnings.",
|
|
119
|
+
"Planning artifacts under .gjc/specs/** and .gjc/plans/** remain writable outside the state command.",
|
|
120
|
+
];
|
|
121
|
+
}
|