@gajae-code/coding-agent 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. package/src/tools/subagent.ts +255 -64
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Session-scoped storage for the harness control plane.
3
+ *
4
+ * Layout (under the harness state root, default `<cwd>/.gjc/state/harness`):
5
+ * sessions/<encoded-id>/state.json lifecycle + handle (atomic)
6
+ * sessions/<encoded-id>/lease.json owner lease (M3)
7
+ * sessions/<encoded-id>/events.jsonl owner-only severity envelopes
8
+ * sessions/<encoded-id>/receipts.jsonl append-only receipt index
9
+ * sessions/<encoded-id>/receipts/<family>/<receiptId>.json immutable receipts
10
+ * sessions/<encoded-id>/artifacts/... diff/validation artifacts
11
+ * sessions/<encoded-id>/gjc-session/ underlying gajae-code --session-dir
12
+ *
13
+ * Receipt files are immutable: re-writing an existing receipt id fails closed.
14
+ * JSON writes are atomic (temp + rename).
15
+ */
16
+ import { createHash, randomBytes } from "node:crypto";
17
+ import * as fsSync from "node:fs";
18
+ import * as fs from "node:fs/promises";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+ import type { EventEnvelope, ReceiptFamily, SessionState } from "./types";
22
+
23
+ const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
24
+ export const MAX_UNIX_SOCKET_PATH_BYTES = 100;
25
+
26
+ interface SocketPathMetadata {
27
+ root: string;
28
+ sessionId: string;
29
+ }
30
+
31
+ function socketBase(env: NodeJS.ProcessEnv, allowOverride: boolean): { base: string; fromOverride: boolean } {
32
+ const override = env.GJC_HARNESS_SOCKET_DIR?.trim();
33
+ if (allowOverride && override) return { base: path.resolve(override), fromOverride: true };
34
+ return { base: path.join(os.tmpdir(), `gjch${process.getuid?.() ?? "u"}`), fromOverride: false };
35
+ }
36
+
37
+ function socketPathForBase(root: string, sessionId: string, base: string): string {
38
+ const digest = createHash("sha256").update(`${root}\0${sessionId}`).digest("hex");
39
+ fsSync.mkdirSync(base, { recursive: true });
40
+ for (const len of [16, 24, 32, 48, 64]) {
41
+ const stem = `c-${digest.slice(0, len)}`;
42
+ const metadataPath = path.join(base, `${stem}.json`);
43
+ const metadata: SocketPathMetadata = { root, sessionId };
44
+ try {
45
+ const existing = JSON.parse(fsSync.readFileSync(metadataPath, "utf8")) as SocketPathMetadata;
46
+ if (existing.root === root && existing.sessionId === sessionId) return path.join(base, `${stem}.sock`);
47
+ } catch (error) {
48
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
49
+ fsSync.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
50
+ return path.join(base, `${stem}.sock`);
51
+ }
52
+ }
53
+ throw new StorageError(`socket_path_collision:${sessionId}`, "socket_path_collision");
54
+ }
55
+
56
+ export function controlSocketPath(root: string, sessionId: string, env: NodeJS.ProcessEnv = process.env): string {
57
+ assertSafeSessionId(sessionId);
58
+ let { base, fromOverride } = socketBase(env, true);
59
+ let finalPath = socketPathForBase(root, sessionId, base);
60
+ if (Buffer.byteLength(finalPath) > MAX_UNIX_SOCKET_PATH_BYTES && fromOverride) {
61
+ base = socketBase(env, false).base;
62
+ finalPath = socketPathForBase(root, sessionId, base);
63
+ }
64
+ const finalBytes = Buffer.byteLength(finalPath);
65
+ if (finalBytes > MAX_UNIX_SOCKET_PATH_BYTES) {
66
+ throw new StorageError(`socket_path_too_long:${finalBytes}`, "socket_path_too_long");
67
+ }
68
+ return finalPath;
69
+ }
70
+
71
+ export class StorageError extends Error {
72
+ constructor(
73
+ message: string,
74
+ readonly code: string,
75
+ ) {
76
+ super(message);
77
+ this.name = "StorageError";
78
+ }
79
+ }
80
+
81
+ /** Resolve the harness state root from explicit value, env, or cwd default. */
82
+ export function resolveHarnessRoot(opts?: { root?: string; cwd?: string; env?: NodeJS.ProcessEnv }): string {
83
+ const env = opts?.env ?? process.env;
84
+ if (opts?.root) return path.resolve(opts.root);
85
+ const fromEnv = env.GJC_HARNESS_STATE_ROOT;
86
+ if (fromEnv?.trim()) return path.resolve(fromEnv.trim());
87
+ return path.join(opts?.cwd ?? process.cwd(), ".gjc", "state", "harness");
88
+ }
89
+
90
+ export function assertSafeSessionId(id: string): void {
91
+ if (!SESSION_ID_RE.test(id)) {
92
+ throw new StorageError(`unsafe_session_id:${id}`, "unsafe_session_id");
93
+ }
94
+ }
95
+
96
+ export function generateSessionId(prefix = "h"): string {
97
+ const ts = new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15);
98
+ const rand = randomBytes(4).toString("hex");
99
+ return `${prefix}-${ts}-${rand}`;
100
+ }
101
+
102
+ export interface SessionPaths {
103
+ dir: string;
104
+ state: string;
105
+ lease: string;
106
+ events: string;
107
+ receiptsIndex: string;
108
+ receiptsDir: string;
109
+ artifactsDir: string;
110
+ controlSock: string;
111
+ controlFifo: string;
112
+ gjcSessionDir: string;
113
+ }
114
+
115
+ export function sessionPaths(root: string, sessionId: string): SessionPaths {
116
+ assertSafeSessionId(sessionId);
117
+ const dir = path.join(root, "sessions", sessionId);
118
+ return {
119
+ dir,
120
+ state: path.join(dir, "state.json"),
121
+ lease: path.join(dir, "lease.json"),
122
+ events: path.join(dir, "events.jsonl"),
123
+ receiptsIndex: path.join(dir, "receipts.jsonl"),
124
+ receiptsDir: path.join(dir, "receipts"),
125
+ artifactsDir: path.join(dir, "artifacts"),
126
+ controlSock: path.join(dir, "control.sock"),
127
+ controlFifo: path.join(dir, "control.fifo"),
128
+ gjcSessionDir: path.join(dir, "gjc-session"),
129
+ };
130
+ }
131
+
132
+ async function writeJsonAtomic(file: string, value: unknown): Promise<void> {
133
+ await fs.mkdir(path.dirname(file), { recursive: true });
134
+ const tmp = `${file}.tmp-${randomBytes(4).toString("hex")}`;
135
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
136
+ await fs.rename(tmp, file);
137
+ }
138
+
139
+ async function readJson<T>(file: string): Promise<T | null> {
140
+ try {
141
+ const raw = await fs.readFile(file, "utf8");
142
+ return JSON.parse(raw) as T;
143
+ } catch (error) {
144
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ export async function readSessionState(root: string, sessionId: string): Promise<SessionState | null> {
150
+ return readJson<SessionState>(sessionPaths(root, sessionId).state);
151
+ }
152
+
153
+ export async function writeSessionState(root: string, state: SessionState): Promise<void> {
154
+ const paths = sessionPaths(root, state.sessionId);
155
+ await fs.mkdir(paths.dir, { recursive: true });
156
+ await writeJsonAtomic(paths.state, state);
157
+ }
158
+
159
+ export async function sessionExists(root: string, sessionId: string): Promise<boolean> {
160
+ return (await readSessionState(root, sessionId)) !== null;
161
+ }
162
+
163
+ /** Append a single severity envelope to events.jsonl. Single-writer discipline is the owner's job (M3). */
164
+ export async function appendEvent(root: string, sessionId: string, envelope: EventEnvelope): Promise<void> {
165
+ const paths = sessionPaths(root, sessionId);
166
+ await fs.mkdir(paths.dir, { recursive: true });
167
+ await fs.appendFile(paths.events, `${JSON.stringify(envelope)}\n`, "utf8");
168
+ }
169
+
170
+ /** Read events from cursor (exclusive). Tail-only: never mutates the log. */
171
+ export async function readEvents(root: string, sessionId: string, fromCursor = 0): Promise<EventEnvelope[]> {
172
+ const paths = sessionPaths(root, sessionId);
173
+ let raw: string;
174
+ try {
175
+ raw = await fs.readFile(paths.events, "utf8");
176
+ } catch (error) {
177
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
178
+ throw error;
179
+ }
180
+ const out: EventEnvelope[] = [];
181
+ for (const line of raw.split("\n")) {
182
+ const trimmed = line.trim();
183
+ if (!trimmed) continue;
184
+ const env = JSON.parse(trimmed) as EventEnvelope;
185
+ if (env.cursor > fromCursor) out.push(env);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ export interface ReceiptIndexEntry {
191
+ receiptId: string;
192
+ family: ReceiptFamily;
193
+ valid: boolean;
194
+ createdAt: string;
195
+ path: string;
196
+ }
197
+
198
+ /**
199
+ * Persist a receipt immutably. Fails closed if the receipt id already exists,
200
+ * then appends an index entry to receipts.jsonl.
201
+ */
202
+ export async function writeReceiptImmutable(
203
+ root: string,
204
+ sessionId: string,
205
+ family: ReceiptFamily,
206
+ receiptId: string,
207
+ value: { receiptId: string; family: ReceiptFamily; valid: boolean; createdAt: string },
208
+ ): Promise<ReceiptIndexEntry> {
209
+ assertSafeSessionId(sessionId);
210
+ if (!SESSION_ID_RE.test(receiptId)) {
211
+ throw new StorageError(`unsafe_receipt_id:${receiptId}`, "unsafe_receipt_id");
212
+ }
213
+ const paths = sessionPaths(root, sessionId);
214
+ const familyDir = path.join(paths.receiptsDir, family);
215
+ const file = path.join(familyDir, `${receiptId}.json`);
216
+ await fs.mkdir(familyDir, { recursive: true });
217
+ try {
218
+ await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
219
+ } catch (error) {
220
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
221
+ throw new StorageError(`receipt_immutable_conflict:${family}/${receiptId}`, "receipt_immutable_conflict");
222
+ }
223
+ throw error;
224
+ }
225
+ const entry: ReceiptIndexEntry = {
226
+ receiptId,
227
+ family,
228
+ valid: value.valid,
229
+ createdAt: value.createdAt,
230
+ path: file,
231
+ };
232
+ await fs.appendFile(paths.receiptsIndex, `${JSON.stringify(entry)}\n`, "utf8");
233
+ return entry;
234
+ }
235
+
236
+ export async function readReceiptIndex(
237
+ root: string,
238
+ sessionId: string,
239
+ family?: ReceiptFamily,
240
+ ): Promise<ReceiptIndexEntry[]> {
241
+ const paths = sessionPaths(root, sessionId);
242
+ let raw: string;
243
+ try {
244
+ raw = await fs.readFile(paths.receiptsIndex, "utf8");
245
+ } catch (error) {
246
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
247
+ throw error;
248
+ }
249
+ const out: ReceiptIndexEntry[] = [];
250
+ for (const line of raw.split("\n")) {
251
+ const trimmed = line.trim();
252
+ if (!trimmed) continue;
253
+ const entry = JSON.parse(trimmed) as ReceiptIndexEntry;
254
+ if (!family || entry.family === family) out.push(entry);
255
+ }
256
+ return out;
257
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Core types for the gajae-code-native coding-harness operations control plane (v1).
3
+ *
4
+ * See the approved consensus plan at
5
+ * `.gjc/plans/ralplan/2026-06-02-0853-3e33/stage-02-revision.md` and the spec at
6
+ * `.gjc/specs/deep-interview-harness-control-plane.md`.
7
+ *
8
+ * v1 implements the gajae-code adapter only. omx/codex/remote/auth are deferred seams.
9
+ */
10
+
11
+ /** Harnesses the control plane can operate. v1 implements `gajae-code` only. */
12
+ export type Harness = "gajae-code" | "codex" | "omx";
13
+
14
+ /** Lifecycle states of an operated session. */
15
+ export type HarnessLifecycle =
16
+ | "new"
17
+ | "started"
18
+ | "submitted"
19
+ | "observing"
20
+ | "recovering"
21
+ | "validating"
22
+ | "finalizing"
23
+ | "completed"
24
+ | "blocked"
25
+ | "retired";
26
+
27
+ /** Event severities emitted by the owner. */
28
+ export type Severity = "info" | "warn" | "critical";
29
+
30
+ /** Bounded git delta classification surfaced by `observe`. */
31
+ export type GitDelta = "clean" | "dirty" | "zero-delta" | "unknown";
32
+
33
+ /** Risk classification surfaced by `observe`. */
34
+ export type RiskKind = "normal" | "prompt-not-accepted" | "deleted-worktree" | "vanished-dirty";
35
+
36
+ /** Deterministic recovery classifications. */
37
+ export type RecoveryClassification =
38
+ | "continue"
39
+ | "send-enter"
40
+ | "reinject-prompt"
41
+ | "restart-clean"
42
+ | "restart-preserve-delta"
43
+ | "fallback-codex-exec"
44
+ | "human-check";
45
+
46
+ /** Receipt families persisted under the session storage dir. */
47
+ export type ReceiptFamily = "vanish" | "prompt-acceptance" | "validation" | "completion";
48
+
49
+ /** The CLI verbs / primitives exposed by `gjc harness <verb>`. */
50
+ export type HarnessVerb =
51
+ | "start"
52
+ | "submit"
53
+ | "observe"
54
+ | "classify"
55
+ | "recover"
56
+ | "validate"
57
+ | "finalize"
58
+ | "retire"
59
+ | "events"
60
+ | "monitor"
61
+ | "operate";
62
+
63
+ /** Submission transports. */
64
+ export type SubmitMode = "paste-buffer" | "stdin" | "file";
65
+
66
+ /** A single entry in the forcing-function `nextAllowedActions` list. */
67
+ export interface NextAllowedAction {
68
+ verb: HarnessVerb;
69
+ available: boolean;
70
+ /** Present when `available` is false; explains why the verb is currently disallowed. */
71
+ reason?: string;
72
+ }
73
+
74
+ /** Compact, model-facing view of session state included in every response. */
75
+ export interface SessionStateView {
76
+ sessionId: string;
77
+ lifecycle: HarnessLifecycle;
78
+ harness: Harness;
79
+ ownerLive: boolean;
80
+ blockers: string[];
81
+ }
82
+
83
+ /**
84
+ * The universal contract: EVERY primitive response carries `{state, evidence, nextAllowedActions}`.
85
+ * `ok` is a transport-level convenience; semantic blocking is expressed via state + nextAllowedActions.
86
+ */
87
+ export interface PrimitiveResponse<E = Record<string, unknown>> {
88
+ ok: boolean;
89
+ state: SessionStateView;
90
+ evidence: E;
91
+ nextAllowedActions: NextAllowedAction[];
92
+ }
93
+
94
+ /** Re-grabbable session handle returned by `start` / `operate`. */
95
+ export interface SessionHandle {
96
+ sessionId: string;
97
+ harness: Harness;
98
+ repo: string | null;
99
+ workspace: string;
100
+ branch: string | null;
101
+ base: string | null;
102
+ issueOrPr: string | null;
103
+ processHandle: { kind: "runtime-owner"; ownerId: string | null; pid: number | null };
104
+ rpcHandle: { kind: "rpc-subprocess"; pid: number | null; sessionDir: string };
105
+ ownerHandle: { leasePath: string; endpoint: string | null; heartbeatAt: string | null };
106
+ routerHandle: { kind: "default-in-owner"; policy: string; eventsPath: string };
107
+ viewportHandle: { kind: "event-monitor"; tmuxSessionName: string | null; viewOnly: true };
108
+ startedAt: string;
109
+ updatedAt: string;
110
+ }
111
+
112
+ /** Persisted per-session record (state.json). */
113
+ export interface SessionState {
114
+ schemaVersion: number;
115
+ sessionId: string;
116
+ lifecycle: HarnessLifecycle;
117
+ harness: Harness;
118
+ handle: SessionHandle;
119
+ /** Per-classification retry counters consumed by the recovery policy. */
120
+ retries: Record<string, number>;
121
+ blockers: string[];
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ }
125
+
126
+ /** Bounded observed-signal vocabulary surfaced by `observe` (the owner only ever emits these). */
127
+ export type ObservedSignal =
128
+ | "SessionStart"
129
+ | "prompt-accepted"
130
+ | "tool-call"
131
+ | "test-running"
132
+ | "commit-created"
133
+ | "completed"
134
+ | "error"
135
+ | "streaming"
136
+ | "idle";
137
+
138
+ export const OBSERVED_SIGNALS: readonly ObservedSignal[] = [
139
+ "SessionStart",
140
+ "prompt-accepted",
141
+ "tool-call",
142
+ "test-running",
143
+ "commit-created",
144
+ "completed",
145
+ "error",
146
+ "streaming",
147
+ "idle",
148
+ ];
149
+
150
+ /** Bounded observation surfaced by `observe` — never a raw pane/transcript dump. */
151
+ export interface Observation {
152
+ lifecycle: HarnessLifecycle;
153
+ ownerLive: boolean;
154
+ cwd: string;
155
+ branch: string | null;
156
+ gitDelta: GitDelta;
157
+ lastActivityAt: string | null;
158
+ observedSignals: string[];
159
+ risk: RiskKind;
160
+ /** RPC subprocess liveness, distinct from owner-process/lease liveness. Optional for back-compat. */
161
+ rpcLive?: boolean;
162
+ /** ISO timestamp of the most recent RPC frame the owner observed, if any. */
163
+ rpcLastFrameAt?: string | null;
164
+ }
165
+
166
+ /** Input to the deterministic recovery classifier. */
167
+ export interface ClassifyInput {
168
+ observation: Observation;
169
+ /** Remaining retry budget per classification family. */
170
+ retryBudget: RetryBudget;
171
+ /** Whether an accepted prompt was in flight when the owner/RPC was last seen. */
172
+ acceptedPromptActive?: boolean;
173
+ }
174
+
175
+ /** Default and supplied retry budgets. */
176
+ export interface RetryBudget {
177
+ reinjectPrompt: number;
178
+ zeroDeltaVanish: number;
179
+ dirtyVanishPreserve: number;
180
+ validationRepair: number;
181
+ }
182
+
183
+ /** Result of the deterministic recovery classifier. */
184
+ export interface RecoveryDecision {
185
+ classification: RecoveryClassification;
186
+ reason: string;
187
+ severity: Severity;
188
+ /** Whether executing the recommended action requires a live owner. */
189
+ ownerRequired: boolean;
190
+ /** Receipt family that MUST be valid before the action may proceed (e.g. `vanish`). */
191
+ requiredReceiptFamily: ReceiptFamily | null;
192
+ }
193
+
194
+ /** Severity-tagged event envelope written exclusively by the owner. */
195
+ export interface EventEnvelope<E = Record<string, unknown>> {
196
+ eventId: string;
197
+ cursor: number;
198
+ createdAt: string;
199
+ severity: Severity;
200
+ kind: string;
201
+ state: SessionStateView;
202
+ evidence: E;
203
+ nextAllowedActions: NextAllowedAction[];
204
+ writer: { ownerId: string; leaseEpoch: number };
205
+ }
206
+
207
+ export const SESSION_SCHEMA_VERSION = 1 as const;
208
+
209
+ export const DEFAULT_RETRY_BUDGET: RetryBudget = {
210
+ reinjectPrompt: 2,
211
+ zeroDeltaVanish: 1,
212
+ dirtyVanishPreserve: 1,
213
+ validationRepair: 2,
214
+ };
@@ -1,3 +1,5 @@
1
+ import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
2
+
1
3
  export interface SkillKeywordDefinition {
2
4
  keyword: string;
3
5
  skill: GjcWorkflowSkill;
@@ -5,9 +7,9 @@ export interface SkillKeywordDefinition {
5
7
  guidance: string;
6
8
  }
7
9
 
8
- export const GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "team"] as const;
10
+ export const GJC_WORKFLOW_SKILLS = CANONICAL_GJC_WORKFLOW_SKILLS;
9
11
 
10
- export type GjcWorkflowSkill = (typeof GJC_WORKFLOW_SKILLS)[number];
12
+ export type GjcWorkflowSkill = CanonicalGjcWorkflowSkill;
11
13
 
12
14
  export const GJC_SKILL_KEYWORD_DEFINITIONS: readonly SkillKeywordDefinition[] = [
13
15
  {
@@ -1,9 +1,13 @@
1
- import * as fs from "node:fs/promises";
2
1
  import * as path from "node:path";
3
2
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
3
+ import { writeJsonAtomic } from "../gjc-runtime/state-writer";
4
4
  import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
5
5
  import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
6
- import type { SkillActiveEntry as CanonicalSkillActiveEntry, WorkflowHudSummary } from "../skill-state/active-state";
6
+ import {
7
+ readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
8
+ type SkillActiveEntry,
9
+ type SkillActiveState,
10
+ } from "../skill-state/active-state";
7
11
  import {
8
12
  compareSkillKeywordMatches,
9
13
  GJC_SKILL_KEYWORD_DEFINITIONS,
@@ -74,35 +78,7 @@ export interface SkillKeywordMatch {
74
78
  priority: number;
75
79
  }
76
80
 
77
- export interface SkillActiveEntry extends Omit<CanonicalSkillActiveEntry, "skill"> {
78
- skill: GjcWorkflowSkill;
79
- phase?: string;
80
- active?: boolean;
81
- activated_at?: string;
82
- updated_at?: string;
83
- session_id?: string;
84
- thread_id?: string;
85
- turn_id?: string;
86
- hud?: WorkflowHudSummary;
87
- stale?: boolean;
88
- }
89
-
90
- export interface SkillActiveState {
91
- version: number;
92
- active: boolean;
93
- skill: GjcWorkflowSkill;
94
- keyword: string;
95
- phase: string;
96
- activated_at: string;
97
- updated_at: string;
98
- source: "gjc-skill-state-hook";
99
- session_id?: string;
100
- thread_id?: string;
101
- turn_id?: string;
102
- initialized_mode?: GjcWorkflowSkill;
103
- initialized_state_path?: string;
104
- active_skills: SkillActiveEntry[];
105
- }
81
+ export type { SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
106
82
 
107
83
  export interface ModeState {
108
84
  active?: boolean;
@@ -252,9 +228,11 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
252
228
  }
253
229
  }
254
230
 
255
- async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
256
- await fs.mkdir(path.dirname(filePath), { recursive: true });
257
- await Bun.write(filePath, `${JSON.stringify(value, null, 2)}\n`);
231
+ async function writeJsonFile(filePath: string, value: unknown, cwd: string): Promise<void> {
232
+ await writeJsonAtomic(filePath, value, {
233
+ cwd,
234
+ audit: { category: "state", verb: "write", owner: "gjc-hook" },
235
+ });
258
236
  }
259
237
 
260
238
  function entryMatchesContext(
@@ -272,7 +250,11 @@ function entryMatchesContext(
272
250
 
273
251
  function listActiveSkills(state: SkillActiveState | null): SkillActiveEntry[] {
274
252
  if (!state?.active) return [];
275
- return state.active_skills.filter(entry => entry.active !== false);
253
+ return (state.active_skills ?? []).filter(entry => entry.active !== false);
254
+ }
255
+
256
+ function isWorkflowActiveEntry(entry: SkillActiveEntry): entry is SkillActiveEntry & { skill: GjcWorkflowSkill } {
257
+ return isGjcWorkflowSkill(entry.skill);
276
258
  }
277
259
 
278
260
  export async function readVisibleSkillActiveState(
@@ -280,6 +262,7 @@ export async function readVisibleSkillActiveState(
280
262
  sessionId?: string,
281
263
  stateDir?: string,
282
264
  ): Promise<SkillActiveState | null> {
265
+ if (!stateDir) return await readCanonicalVisibleSkillActiveState(cwd, sessionId);
283
266
  const resolvedStateDir = resolveGjcStateDir(cwd, stateDir);
284
267
  if (sessionId) {
285
268
  const sessionState = await readJsonFile<SkillActiveState>(skillStatePath(resolvedStateDir, sessionId));
@@ -337,10 +320,10 @@ export async function recordSkillActivation(input: RecordSkillActivationInput):
337
320
  modeState.threshold_source = "default";
338
321
  }
339
322
 
340
- await writeJsonFile(initializedStatePath, modeState);
341
- await writeJsonFile(skillStatePath(resolvedStateDir, input.sessionId), state);
323
+ await writeJsonFile(initializedStatePath, modeState, input.cwd);
324
+ await writeJsonFile(skillStatePath(resolvedStateDir, input.sessionId), state, input.cwd);
342
325
  if (!input.sessionId) return state;
343
- await writeJsonFile(skillStatePath(resolvedStateDir), state);
326
+ await writeJsonFile(skillStatePath(resolvedStateDir), state, input.cwd);
344
327
  return state;
345
328
  }
346
329
 
@@ -432,9 +415,9 @@ export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitS
432
415
  export async function buildSkillStopOutput(input: StopHookInput): Promise<Record<string, unknown> | null> {
433
416
  const resolvedStateDir = resolveGjcStateDir(input.cwd, input.stateDir);
434
417
  const skillState = await readVisibleSkillActiveState(input.cwd, input.sessionId, input.stateDir);
435
- const activeEntries = listActiveSkills(skillState).filter(entry =>
436
- skillState ? entryMatchesContext(entry, skillState, input.sessionId, input.threadId) : false,
437
- );
418
+ const activeEntries = listActiveSkills(skillState)
419
+ .filter(isWorkflowActiveEntry)
420
+ .filter(entry => (skillState ? entryMatchesContext(entry, skillState, input.sessionId, input.threadId) : false));
438
421
  if (!skillState || activeEntries.length === 0) return null;
439
422
 
440
423
  for (const entry of activeEntries) {