@gajae-code/coding-agent 0.2.5 → 0.3.1

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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -0,0 +1,749 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import type { SkillActiveEntry, SkillActiveState } from "../skill-state/active-state";
5
+ import {
6
+ type AuditEntry,
7
+ buildWorkflowStateReceipt,
8
+ type CanonicalGjcWorkflowSkill,
9
+ type WorkflowStateMutationOwner,
10
+ type WorkflowStateReceipt,
11
+ } from "../skill-state/workflow-state-contract";
12
+ import { RequiredOnWriteEnvelopeSchema } from "./state-schema";
13
+
14
+ /**
15
+ * Sole sanctioned project `.gjc/**` writer module (gate G1).
16
+ *
17
+ * All native `.gjc/**` filesystem mutations must route through these primitives.
18
+ * The primitives validate project `.gjc/**` ownership, create parent directories,
19
+ * and emit workflow receipts or audit entries where applicable by the caller's
20
+ * supplied mutation context. No lockfiles are used; isolation is by atomic rename,
21
+ * append, O_EXCL creates, conditional deletes, per-entry active-state files,
22
+ * and derived active-state snapshots.
23
+ * Transaction journals are per mutation id under `.gjc/state/transactions/`;
24
+ * they are recovery evidence only, never global locks or waiters, so stale
25
+ * journals do not block unrelated state reads or writes.
26
+ */
27
+
28
+ export type WriterCategory =
29
+ | "state"
30
+ | "artifact"
31
+ | "ledger"
32
+ | "log"
33
+ | "report"
34
+ | "agents"
35
+ | "prune"
36
+ | "force"
37
+ | "transaction";
38
+
39
+ export interface StateWriterReceiptContext {
40
+ cwd?: string;
41
+ skill: CanonicalGjcWorkflowSkill;
42
+ owner: WorkflowStateMutationOwner;
43
+ command: string;
44
+ sessionId?: string;
45
+ mutationId?: string;
46
+ nowIso?: string;
47
+ }
48
+
49
+ export interface StateWriterAuditContext {
50
+ cwd?: string;
51
+ category: WriterCategory;
52
+ verb: string;
53
+ owner: WorkflowStateMutationOwner;
54
+ skill?: CanonicalGjcWorkflowSkill | string;
55
+ mutationId?: string;
56
+ fromPhase?: string;
57
+ toPhase?: string;
58
+ forced?: boolean;
59
+ }
60
+
61
+ export interface WorkflowEnvelopeIntegrityMismatch {
62
+ path: string;
63
+ expected: string;
64
+ actual: string;
65
+ }
66
+
67
+ export interface WorkflowTransactionJournal {
68
+ version: 1;
69
+ mutation_id: string;
70
+ status: "pending" | "committed";
71
+ created_at: string;
72
+ updated_at: string;
73
+ caller?: CanonicalGjcWorkflowSkill;
74
+ callee?: CanonicalGjcWorkflowSkill;
75
+ paths: string[];
76
+ steps: string[];
77
+ }
78
+
79
+ export interface StateWriterOptions {
80
+ cwd?: string;
81
+ receipt?: StateWriterReceiptContext;
82
+ audit?: StateWriterAuditContext;
83
+ }
84
+
85
+ export interface DeleteIfOwnedOptions extends StateWriterOptions {
86
+ predicate?: (current: unknown) => boolean | Promise<boolean>;
87
+ }
88
+
89
+ export interface DeleteResult {
90
+ path: string;
91
+ deleted: boolean;
92
+ }
93
+
94
+ export interface ActiveSessionScope {
95
+ sessionId?: string;
96
+ }
97
+
98
+ export interface ActiveEntryWriteResult {
99
+ entryPath: string;
100
+ snapshotPath: string;
101
+ }
102
+
103
+ export interface HardPruneSelectorContext {
104
+ path: string;
105
+ value: unknown;
106
+ }
107
+
108
+ export interface GenericHardPruneTarget {
109
+ path: string;
110
+ category: WriterCategory | string;
111
+ }
112
+
113
+ export interface GenericHardPruneSelectorContext {
114
+ path: string;
115
+ category: WriterCategory | string;
116
+ stat: Awaited<ReturnType<typeof fs.stat>>;
117
+ readJson: () => Promise<unknown>;
118
+ }
119
+
120
+ export type GenericHardPruneSelector = (context: GenericHardPruneSelectorContext) => boolean | Promise<boolean>;
121
+
122
+ export interface ForceOverwriteOptions extends StateWriterOptions {
123
+ raw?: boolean;
124
+ }
125
+
126
+ export type HardPruneSelector = (context: HardPruneSelectorContext) => boolean | Promise<boolean>;
127
+
128
+ export class AlreadyExistsError extends Error {
129
+ constructor(public readonly path: string) {
130
+ super(`file already exists: ${path}`);
131
+ this.name = "AlreadyExistsError";
132
+ }
133
+ }
134
+
135
+ export type StrictMutationReadResult =
136
+ | { kind: "absent" }
137
+ | { kind: "corrupt"; error: string }
138
+ | { kind: "valid"; value: Record<string, unknown> };
139
+
140
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
141
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
142
+ }
143
+
144
+ export async function readExistingStateForMutation(filePath: string): Promise<StrictMutationReadResult> {
145
+ try {
146
+ const raw = await fs.readFile(filePath, "utf-8");
147
+ const parsed = JSON.parse(raw);
148
+ if (isPlainObject(parsed)) return { kind: "valid", value: parsed };
149
+ return { kind: "corrupt", error: "state file must contain a JSON object" };
150
+ } catch (error) {
151
+ const err = error as NodeJS.ErrnoException;
152
+ if (err.code === "ENOENT") return { kind: "absent" };
153
+ return { kind: "corrupt", error: err.message };
154
+ }
155
+ }
156
+ function isErrno(error: unknown, code: string): boolean {
157
+ return typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === code;
158
+ }
159
+
160
+ function cwdForOptions(options?: StateWriterOptions): string {
161
+ return path.resolve(options?.cwd ?? process.cwd());
162
+ }
163
+
164
+ function resolveGjcTarget(targetPath: string, cwd = process.cwd()): string {
165
+ if (!targetPath.trim()) throw new Error("targetPath is required");
166
+ const projectRoot = path.resolve(cwd);
167
+ const gjcRoot = path.join(projectRoot, ".gjc");
168
+ const resolved = path.resolve(projectRoot, targetPath);
169
+ const relative = path.relative(gjcRoot, resolved);
170
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
171
+ throw new Error(`target path must be within project .gjc/**: ${targetPath}`);
172
+ }
173
+ return resolved;
174
+ }
175
+
176
+ function tempPathFor(filePath: string): string {
177
+ return `${filePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
178
+ }
179
+
180
+ function jsonText(value: unknown): string {
181
+ return `${JSON.stringify(value, null, 2)}\n`;
182
+ }
183
+
184
+ function canonicalizeJson(value: unknown): unknown {
185
+ if (Array.isArray(value)) return value.map(canonicalizeJson);
186
+ if (!value || typeof value !== "object") return value;
187
+ const out: Record<string, unknown> = {};
188
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
189
+ const v = (value as Record<string, unknown>)[key];
190
+ if (v !== undefined) out[key] = canonicalizeJson(v);
191
+ }
192
+ return out;
193
+ }
194
+
195
+ function withoutReceiptChecksum(value: unknown): unknown {
196
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
197
+ const clone: Record<string, unknown> = { ...(value as Record<string, unknown>) };
198
+ if (clone.receipt && typeof clone.receipt === "object" && !Array.isArray(clone.receipt)) {
199
+ const receipt = { ...(clone.receipt as Record<string, unknown>) };
200
+ delete receipt.content_sha256;
201
+ clone.receipt = receipt;
202
+ }
203
+ return clone;
204
+ }
205
+
206
+ export function workflowEnvelopeContentSha256(value: unknown): string {
207
+ return createHash("sha256")
208
+ .update(JSON.stringify(canonicalizeJson(withoutReceiptChecksum(value))))
209
+ .digest("hex");
210
+ }
211
+
212
+ export function stampWorkflowEnvelopeChecksum<T>(value: T, filePath: string, computedAt = new Date().toISOString()): T {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
214
+ const envelope = { ...(value as Record<string, unknown>) };
215
+ const receipt =
216
+ envelope.receipt && typeof envelope.receipt === "object" && !Array.isArray(envelope.receipt)
217
+ ? { ...(envelope.receipt as Record<string, unknown>) }
218
+ : {};
219
+ envelope.receipt = {
220
+ ...receipt,
221
+ content_sha256: {
222
+ algorithm: "sha256",
223
+ value: workflowEnvelopeContentSha256(envelope),
224
+ covered_path: filePath,
225
+ computed_at: computedAt,
226
+ },
227
+ };
228
+ return envelope as T;
229
+ }
230
+
231
+ export async function detectWorkflowEnvelopeIntegrityMismatch(
232
+ filePath: string,
233
+ ): Promise<WorkflowEnvelopeIntegrityMismatch | undefined> {
234
+ const current = await readJsonIfPresent(filePath);
235
+ if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
236
+ const receipt = (current as Record<string, unknown>).receipt;
237
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return undefined;
238
+ const checksum = (receipt as Record<string, unknown>).content_sha256;
239
+ if (!checksum || typeof checksum !== "object" || Array.isArray(checksum)) return undefined;
240
+ const expected = (checksum as Record<string, unknown>).value;
241
+ if (typeof expected !== "string" || !expected) return undefined;
242
+ const actual = workflowEnvelopeContentSha256(current);
243
+ return actual === expected ? undefined : { path: filePath, expected, actual };
244
+ }
245
+
246
+ function safeString(value: unknown): string {
247
+ return typeof value === "string" ? value : "";
248
+ }
249
+
250
+ function encodePathSegment(value: string): string {
251
+ return encodeURIComponent(value).replaceAll(".", "%2E");
252
+ }
253
+
254
+ function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope): string {
255
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
256
+ const normalizedSessionId = safeString(sessionId).trim();
257
+ const stateDir = path.join(cwd, ".gjc", "state");
258
+ return normalizedSessionId
259
+ ? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "active")
260
+ : path.join(stateDir, "active");
261
+ }
262
+
263
+ function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
264
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
265
+ const normalizedSessionId = safeString(sessionId).trim();
266
+ const stateDir = path.join(cwd, ".gjc", "state");
267
+ return normalizedSessionId
268
+ ? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "skill-active-state.json")
269
+ : path.join(stateDir, "skill-active-state.json");
270
+ }
271
+
272
+ function activeEntryPath(cwd: string, sessionScope: string | ActiveSessionScope | undefined, skill: string): string {
273
+ const normalizedSkill = safeString(skill).trim();
274
+ if (!normalizedSkill) throw new Error("skill is required");
275
+ return path.join(activeStateDir(cwd, sessionScope), `${encodePathSegment(normalizedSkill)}.json`);
276
+ }
277
+
278
+ function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
279
+ const visible = entries.filter(entry => entry.active !== false);
280
+ const primary = visible[0];
281
+ return {
282
+ version: 1,
283
+ active: visible.length > 0,
284
+ skill: primary?.skill ?? "",
285
+ phase: primary?.phase ?? "",
286
+ updated_at: primary?.updated_at ?? "",
287
+ session_id: primary?.session_id,
288
+ thread_id: primary?.thread_id,
289
+ turn_id: primary?.turn_id,
290
+ active_skills: entries,
291
+ };
292
+ }
293
+
294
+ async function atomicRemove(filePath: string): Promise<boolean> {
295
+ const tmpPath = tempPathFor(filePath);
296
+ try {
297
+ await fs.rename(filePath, tmpPath);
298
+ } catch (error) {
299
+ if (isErrno(error, "ENOENT")) return false;
300
+ throw error;
301
+ }
302
+ await fs.rm(tmpPath, { force: true });
303
+ return true;
304
+ }
305
+
306
+ async function readJsonIfPresent(filePath: string): Promise<unknown | undefined> {
307
+ try {
308
+ return JSON.parse(await fs.readFile(filePath, "utf-8"));
309
+ } catch (error) {
310
+ if (isErrno(error, "ENOENT")) return undefined;
311
+ throw error;
312
+ }
313
+ }
314
+
315
+ function withWorkflowReceipt(value: unknown, receipt: WorkflowStateReceipt | undefined): unknown {
316
+ if (!receipt || !value || typeof value !== "object" || Array.isArray(value)) return value;
317
+ return { ...(value as Record<string, unknown>), receipt };
318
+ }
319
+
320
+ function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateReceipt | undefined {
321
+ if (!options?.receipt) return undefined;
322
+ return buildWorkflowStateReceipt({
323
+ cwd: path.resolve(options.receipt.cwd ?? options.cwd ?? process.cwd()),
324
+ skill: options.receipt.skill,
325
+ owner: options.receipt.owner,
326
+ command: options.receipt.command,
327
+ sessionId: options.receipt.sessionId,
328
+ nowIso: options.receipt.nowIso,
329
+ mutationId: options.receipt.mutationId,
330
+ });
331
+ }
332
+
333
+ async function maybeAudit(mutatedPath: string, options?: StateWriterOptions): Promise<void> {
334
+ if (!options?.audit) return;
335
+ const audit = options.audit;
336
+ const cwd = path.resolve(audit.cwd ?? options.cwd ?? process.cwd());
337
+ await appendAuditEntry(cwd, {
338
+ ts: new Date().toISOString(),
339
+ skill: audit.skill,
340
+ category: audit.category,
341
+ verb: audit.verb,
342
+ owner: audit.owner,
343
+ mutation_id: audit.mutationId ?? randomUUID(),
344
+ from_phase: audit.fromPhase,
345
+ to_phase: audit.toPhase,
346
+ forced: audit.forced ?? false,
347
+ paths: [mutatedPath],
348
+ });
349
+ }
350
+
351
+ async function atomicWrite(filePath: string, content: string): Promise<string> {
352
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
353
+ const tmpPath = tempPathFor(filePath);
354
+ try {
355
+ await fs.writeFile(tmpPath, content, "utf-8");
356
+ await fs.rename(tmpPath, filePath);
357
+ } catch (error) {
358
+ await fs.rm(tmpPath, { force: true }).catch(() => undefined);
359
+ throw error;
360
+ }
361
+ return filePath;
362
+ }
363
+
364
+ export async function writeJsonAtomic(
365
+ targetPath: string,
366
+ value: unknown,
367
+ options?: StateWriterOptions,
368
+ ): Promise<string> {
369
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
370
+ await atomicWrite(filePath, jsonText(withWorkflowReceipt(value, buildReceipt(options))));
371
+ await maybeAudit(filePath, options);
372
+ return filePath;
373
+ }
374
+
375
+ export async function writeWorkflowEnvelopeAtomic(
376
+ targetPath: string,
377
+ value: unknown,
378
+ options?: StateWriterOptions,
379
+ ): Promise<string> {
380
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
381
+ const withReceipt = withWorkflowReceipt(value, buildReceipt(options));
382
+ const stamped = stampWorkflowEnvelopeChecksum(withReceipt, filePath);
383
+ const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
384
+ if (!parsed.success) {
385
+ throw new Error(
386
+ `Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
387
+ .map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
388
+ .join("; ")}`,
389
+ );
390
+ }
391
+ await atomicWrite(filePath, jsonText(stamped));
392
+ await maybeAudit(filePath, options);
393
+ return filePath;
394
+ }
395
+
396
+ export async function writeTextAtomic(targetPath: string, text: string, options?: StateWriterOptions): Promise<string> {
397
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
398
+ await atomicWrite(filePath, text);
399
+ await maybeAudit(filePath, options);
400
+ return filePath;
401
+ }
402
+
403
+ export async function updateJsonAtomic<T = unknown>(
404
+ targetPath: string,
405
+ mutator: (current: T | undefined) => T | Promise<T>,
406
+ options?: StateWriterOptions,
407
+ ): Promise<string> {
408
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
409
+ const current = (await readJsonIfPresent(filePath)) as T | undefined;
410
+ const next = await mutator(current);
411
+ await atomicWrite(filePath, jsonText(withWorkflowReceipt(next, buildReceipt(options))));
412
+ await maybeAudit(filePath, options);
413
+ return filePath;
414
+ }
415
+
416
+ export async function appendJsonl(targetPath: string, entry: unknown, options?: StateWriterOptions): Promise<string> {
417
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
418
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
419
+ await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
420
+ await maybeAudit(filePath, options);
421
+ return filePath;
422
+ }
423
+
424
+ export async function appendText(targetPath: string, text: string, options?: StateWriterOptions): Promise<string> {
425
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
426
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
427
+ await fs.appendFile(filePath, text, "utf-8");
428
+ await maybeAudit(filePath, options);
429
+ return filePath;
430
+ }
431
+
432
+ export async function createJsonNoClobber(
433
+ targetPath: string,
434
+ value: unknown,
435
+ options?: StateWriterOptions,
436
+ ): Promise<string> {
437
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
438
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
439
+ let handle: fs.FileHandle | undefined;
440
+ try {
441
+ handle = await fs.open(filePath, "wx");
442
+ await handle.writeFile(jsonText(withWorkflowReceipt(value, buildReceipt(options))), "utf-8");
443
+ } catch (error) {
444
+ if (isErrno(error, "EEXIST")) throw new AlreadyExistsError(filePath);
445
+ throw error;
446
+ } finally {
447
+ await handle?.close();
448
+ }
449
+ await maybeAudit(filePath, options);
450
+ return filePath;
451
+ }
452
+
453
+ export async function deleteIfOwned(
454
+ targetPath: string,
455
+ predicateOrOptions?: ((current: unknown) => boolean | Promise<boolean>) | DeleteIfOwnedOptions,
456
+ ): Promise<DeleteResult> {
457
+ const options = typeof predicateOrOptions === "function" ? undefined : predicateOrOptions;
458
+ const predicate = typeof predicateOrOptions === "function" ? predicateOrOptions : predicateOrOptions?.predicate;
459
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
460
+ const current = await readJsonIfPresent(filePath);
461
+ if (current === undefined) return { path: filePath, deleted: false };
462
+ if (predicate && !(await predicate(current))) return { path: filePath, deleted: false };
463
+ const deleted = await atomicRemove(filePath);
464
+ if (deleted) await maybeAudit(filePath, options);
465
+ return { path: filePath, deleted };
466
+ }
467
+
468
+ export async function removeFileAudited(targetPath: string, options?: StateWriterOptions): Promise<DeleteResult> {
469
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
470
+ const deleted = await atomicRemove(filePath);
471
+ if (deleted) await maybeAudit(filePath, options);
472
+ return { path: filePath, deleted };
473
+ }
474
+
475
+ /**
476
+ * Active entry files under `.gjc/state/active/<skill>.json` and
477
+ * `.gjc/state/sessions/<id>/active/<skill>.json` are authoritative. The
478
+ * adjacent `skill-active-state.json` file is only a derived cache rebuilt from
479
+ * those entries, so concurrent snapshot rebuilds can race without losing any
480
+ * writer's per-skill state.
481
+ */
482
+ export async function writeActiveEntry(
483
+ cwd: string,
484
+ sessionScope: string | ActiveSessionScope | undefined,
485
+ skill: string,
486
+ entry: SkillActiveEntry,
487
+ options?: StateWriterOptions,
488
+ ): Promise<string> {
489
+ const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
490
+ await atomicWrite(filePath, jsonText({ ...entry, skill }));
491
+ await maybeAudit(filePath, options);
492
+ return filePath;
493
+ }
494
+
495
+ export async function removeActiveEntry(
496
+ cwd: string,
497
+ sessionScope: string | ActiveSessionScope | undefined,
498
+ skill: string,
499
+ options?: StateWriterOptions,
500
+ ): Promise<DeleteResult> {
501
+ const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
502
+ const deleted = await atomicRemove(filePath);
503
+ if (deleted) await maybeAudit(filePath, options);
504
+ return { path: filePath, deleted };
505
+ }
506
+
507
+ export async function readActiveEntries(
508
+ cwd: string,
509
+ sessionScope?: string | ActiveSessionScope,
510
+ ): Promise<SkillActiveEntry[]> {
511
+ const dir = activeStateDir(path.resolve(cwd), sessionScope);
512
+ let names: string[];
513
+ try {
514
+ names = await fs.readdir(dir);
515
+ } catch (error) {
516
+ if (isErrno(error, "ENOENT")) return [];
517
+ throw error;
518
+ }
519
+ const entries: SkillActiveEntry[] = [];
520
+ for (const name of names.sort()) {
521
+ if (!name.endsWith(".json")) continue;
522
+ const raw = await readJsonIfPresent(path.join(dir, name));
523
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
524
+ const skill = safeString((raw as SkillActiveEntry).skill).trim();
525
+ if (!skill) continue;
526
+ entries.push(raw as SkillActiveEntry);
527
+ }
528
+ return entries;
529
+ }
530
+
531
+ export async function rebuildActiveSnapshot(
532
+ cwd: string,
533
+ sessionScope?: string | ActiveSessionScope,
534
+ options?: StateWriterOptions,
535
+ ): Promise<string> {
536
+ const resolvedCwd = path.resolve(cwd);
537
+ const snapshotPath = activeSnapshotPath(resolvedCwd, sessionScope);
538
+ const entries = await readActiveEntries(resolvedCwd, sessionScope);
539
+ await atomicWrite(snapshotPath, jsonText(buildActiveSnapshot(entries)));
540
+ await maybeAudit(snapshotPath, options);
541
+ return snapshotPath;
542
+ }
543
+
544
+ export async function mergeActiveState(
545
+ cwd: string,
546
+ sessionScope: string | ActiveSessionScope | undefined,
547
+ skill: string,
548
+ entry: SkillActiveEntry,
549
+ options?: StateWriterOptions,
550
+ ): Promise<ActiveEntryWriteResult> {
551
+ const entryPath = await writeActiveEntry(cwd, sessionScope, skill, entry, options);
552
+ const snapshotPath = await rebuildActiveSnapshot(cwd, sessionScope, options);
553
+ return { entryPath, snapshotPath };
554
+ }
555
+
556
+ export async function writeArtifact(
557
+ targetPath: string,
558
+ content: string,
559
+ options?: StateWriterOptions,
560
+ ): Promise<string> {
561
+ return writeTextAtomic(targetPath, content, {
562
+ ...options,
563
+ audit: options?.audit ?? { category: "artifact", verb: "write", owner: "gjc-runtime" },
564
+ });
565
+ }
566
+
567
+ export async function writeReport(targetPath: string, content: string, options?: StateWriterOptions): Promise<string> {
568
+ return writeTextAtomic(targetPath, content, {
569
+ ...options,
570
+ audit: options?.audit ?? { category: "report", verb: "write", owner: "gjc-runtime" },
571
+ });
572
+ }
573
+
574
+ export async function writeLogJsonl(targetPath: string, entry: unknown, options?: StateWriterOptions): Promise<string> {
575
+ return appendJsonl(targetPath, entry, {
576
+ ...options,
577
+ audit: options?.audit ?? { category: "log", verb: "append", owner: "gjc-runtime" },
578
+ });
579
+ }
580
+
581
+ export async function softDelete(
582
+ targetPath: string,
583
+ meta: Record<string, unknown>,
584
+ options?: StateWriterOptions,
585
+ ): Promise<string> {
586
+ return updateJsonAtomic<Record<string, unknown>>(
587
+ targetPath,
588
+ current => ({
589
+ ...(current && typeof current === "object" && !Array.isArray(current) ? current : {}),
590
+ archived: true,
591
+ active: false,
592
+ tombstone: { ...meta, archived_at: new Date().toISOString() },
593
+ }),
594
+ {
595
+ ...options,
596
+ audit: options?.audit ?? { category: "prune", verb: "soft-delete", owner: "gjc-runtime" },
597
+ },
598
+ );
599
+ }
600
+
601
+ export async function hardPruneJson(
602
+ targetPaths: readonly string[],
603
+ selector: HardPruneSelector,
604
+ options?: StateWriterOptions,
605
+ ): Promise<string[]> {
606
+ const targets: GenericHardPruneTarget[] = targetPaths.map(targetPath => ({ path: targetPath, category: "prune" }));
607
+ return hardPrune(
608
+ targets,
609
+ async context => {
610
+ const value = await context.readJson();
611
+ return selector({ path: context.path, value });
612
+ },
613
+ options,
614
+ );
615
+ }
616
+
617
+ export async function hardPrune(
618
+ targets: readonly GenericHardPruneTarget[],
619
+ selector: GenericHardPruneSelector,
620
+ options?: StateWriterOptions,
621
+ ): Promise<string[]> {
622
+ const cwd = cwdForOptions(options);
623
+ const removed: string[] = [];
624
+ for (const target of targets) {
625
+ const filePath = resolveGjcTarget(target.path, cwd);
626
+ let stat: Awaited<ReturnType<typeof fs.stat>>;
627
+ try {
628
+ stat = await fs.stat(filePath);
629
+ } catch (error) {
630
+ if (isErrno(error, "ENOENT")) continue;
631
+ throw error;
632
+ }
633
+ const shouldRemove = await selector({
634
+ path: filePath,
635
+ category: target.category,
636
+ stat,
637
+ readJson: async () => JSON.parse(await fs.readFile(filePath, "utf-8")),
638
+ });
639
+ if (!shouldRemove) continue;
640
+ const deleted = await atomicRemove(filePath);
641
+ if (deleted) removed.push(filePath);
642
+ }
643
+ if (options?.audit && removed.length > 0) {
644
+ const audit = options.audit;
645
+ await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), {
646
+ ts: new Date().toISOString(),
647
+ skill: audit.skill,
648
+ category: audit.category,
649
+ verb: audit.verb,
650
+ owner: audit.owner,
651
+ mutation_id: audit.mutationId ?? randomUUID(),
652
+ from_phase: audit.fromPhase,
653
+ to_phase: audit.toPhase,
654
+ forced: audit.forced ?? false,
655
+ paths: removed,
656
+ });
657
+ }
658
+ return removed;
659
+ }
660
+
661
+ export async function forceOverwrite(
662
+ targetPath: string,
663
+ rawValue: unknown,
664
+ options?: ForceOverwriteOptions,
665
+ ): Promise<string> {
666
+ const auditOptions = {
667
+ ...options,
668
+ audit: options?.audit ?? { category: "force", verb: "force-overwrite", owner: "gjc-state-cli", forced: true },
669
+ };
670
+ if (options?.raw === true) {
671
+ const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
672
+ await atomicWrite(filePath, jsonText(rawValue));
673
+ await maybeAudit(filePath, auditOptions);
674
+ return filePath;
675
+ }
676
+ return writeJsonAtomic(
677
+ targetPath,
678
+ {
679
+ forced: true,
680
+ forced_at: new Date().toISOString(),
681
+ value: rawValue,
682
+ },
683
+ auditOptions,
684
+ );
685
+ }
686
+
687
+ export async function appendAuditEntry(cwd: string, entry: AuditEntry): Promise<string> {
688
+ const filePath = resolveGjcTarget(path.join(".gjc", "state", "audit.jsonl"), cwd);
689
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
690
+ await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
691
+ return filePath;
692
+ }
693
+
694
+ function transactionJournalPath(cwd: string, mutationId: string): string {
695
+ return path.join(path.resolve(cwd), ".gjc", "state", "transactions", `${encodePathSegment(mutationId)}.json`);
696
+ }
697
+
698
+ export async function readWorkflowTransactionJournal(
699
+ cwd: string,
700
+ mutationId: string,
701
+ ): Promise<WorkflowTransactionJournal | undefined> {
702
+ return (await readJsonIfPresent(transactionJournalPath(cwd, mutationId))) as WorkflowTransactionJournal | undefined;
703
+ }
704
+
705
+ export async function beginWorkflowTransactionJournal(input: {
706
+ cwd: string;
707
+ mutationId: string;
708
+ caller?: CanonicalGjcWorkflowSkill;
709
+ callee?: CanonicalGjcWorkflowSkill;
710
+ paths: string[];
711
+ }): Promise<string> {
712
+ const now = new Date().toISOString();
713
+ const journal: WorkflowTransactionJournal = {
714
+ version: 1,
715
+ mutation_id: input.mutationId,
716
+ status: "pending",
717
+ created_at: now,
718
+ updated_at: now,
719
+ caller: input.caller,
720
+ callee: input.callee,
721
+ paths: input.paths,
722
+ steps: [],
723
+ };
724
+ try {
725
+ return await createJsonNoClobber(transactionJournalPath(input.cwd, input.mutationId), journal, {
726
+ cwd: input.cwd,
727
+ });
728
+ } catch (error) {
729
+ if (error instanceof AlreadyExistsError) return error.path;
730
+ throw error;
731
+ }
732
+ }
733
+
734
+ export async function updateWorkflowTransactionJournal(
735
+ cwd: string,
736
+ mutationId: string,
737
+ patch: Partial<WorkflowTransactionJournal>,
738
+ ): Promise<string> {
739
+ const filePath = transactionJournalPath(cwd, mutationId);
740
+ const current = ((await readJsonIfPresent(filePath)) ?? {}) as WorkflowTransactionJournal;
741
+ const next = { ...current, ...patch, updated_at: new Date().toISOString() } as WorkflowTransactionJournal;
742
+ await atomicWrite(filePath, jsonText(next));
743
+ return filePath;
744
+ }
745
+
746
+ export async function completeWorkflowTransactionJournal(cwd: string, mutationId: string): Promise<void> {
747
+ await updateWorkflowTransactionJournal(cwd, mutationId, { status: "committed" });
748
+ await atomicRemove(transactionJournalPath(cwd, mutationId)).catch(() => false);
749
+ }