@gajae-code/coding-agent 0.3.0 → 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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  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/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -0,0 +1,223 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ const CRASH_DIAGNOSTICS_ENV = "GJC_CRASH_DIAGNOSTICS";
6
+ const CRASH_DIAGNOSTICS_DIR_ENV = "GJC_CRASH_DIAGNOSTICS_DIR";
7
+ const STDERR_PREVIEW_BYTES = 4096;
8
+ const DIRECTORY_MODE = 0o700;
9
+ const REPORT_FILE_MODE = 0o600;
10
+
11
+ export type CrashProcessKind = "bash" | "python" | "lsp" | "dap" | "mcp" | "browser" | "worker" | "native" | "unknown";
12
+
13
+ export type CrashClass =
14
+ | "clean_exit"
15
+ | "non_zero_exit"
16
+ | "signal_exit"
17
+ | "timeout"
18
+ | "cancelled"
19
+ | "spawn_error"
20
+ | "protocol_exit"
21
+ | "native_panic"
22
+ | "unknown";
23
+
24
+ export interface CrashClassificationInput {
25
+ kind: CrashProcessKind;
26
+ command?: string[];
27
+ exitCode?: number | null;
28
+ signal?: string | null;
29
+ cancelled?: boolean;
30
+ timedOut?: boolean;
31
+ spawnError?: unknown;
32
+ stderr?: string;
33
+ protocol?: string;
34
+ }
35
+
36
+ export interface CrashClassification {
37
+ kind: CrashProcessKind;
38
+ class: CrashClass;
39
+ crashed: boolean;
40
+ exitCode: number | null;
41
+ signal: string | null;
42
+ command?: string[];
43
+ protocol?: string;
44
+ reason: string;
45
+ }
46
+
47
+ export interface CrashReport extends CrashClassification {
48
+ schemaVersion: 1;
49
+ createdAt: string;
50
+ pid: number;
51
+ cwd: string;
52
+ stderrPreview?: string;
53
+ spawnError?: string;
54
+ }
55
+
56
+ export interface CrashReportWriteResult {
57
+ report: CrashReport;
58
+ path: string | null;
59
+ enabled: boolean;
60
+ }
61
+
62
+ export function crashDiagnosticsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
63
+ const value = env[CRASH_DIAGNOSTICS_ENV];
64
+ return value === "1" || value === "true" || value === "yes";
65
+ }
66
+
67
+ export function getCrashDiagnosticsDirectory(env: NodeJS.ProcessEnv = process.env): string {
68
+ return env[CRASH_DIAGNOSTICS_DIR_ENV] ?? path.join(os.tmpdir(), "gjc-crash-diagnostics");
69
+ }
70
+
71
+ export function classifyProcessCrash(input: CrashClassificationInput): CrashClassification {
72
+ const exitCode = input.exitCode ?? null;
73
+ const signal = input.signal ?? null;
74
+ const command = input.command;
75
+ const protocol = input.protocol;
76
+
77
+ if (input.timedOut) {
78
+ return {
79
+ kind: input.kind,
80
+ class: "timeout",
81
+ crashed: true,
82
+ exitCode,
83
+ signal,
84
+ command,
85
+ protocol,
86
+ reason: "process timed out",
87
+ };
88
+ }
89
+ if (input.cancelled) {
90
+ return {
91
+ kind: input.kind,
92
+ class: "cancelled",
93
+ crashed: false,
94
+ exitCode,
95
+ signal,
96
+ command,
97
+ protocol,
98
+ reason: "process was cancelled",
99
+ };
100
+ }
101
+ if (input.spawnError !== undefined) {
102
+ return {
103
+ kind: input.kind,
104
+ class: "spawn_error",
105
+ crashed: true,
106
+ exitCode,
107
+ signal,
108
+ command,
109
+ protocol,
110
+ reason: stringifyError(input.spawnError),
111
+ };
112
+ }
113
+ if (signal) {
114
+ return {
115
+ kind: input.kind,
116
+ class: "signal_exit",
117
+ crashed: true,
118
+ exitCode,
119
+ signal,
120
+ command,
121
+ protocol,
122
+ reason: `process exited after signal ${signal}`,
123
+ };
124
+ }
125
+ if (exitCode === 0) {
126
+ return {
127
+ kind: input.kind,
128
+ class: "clean_exit",
129
+ crashed: false,
130
+ exitCode,
131
+ signal,
132
+ command,
133
+ protocol,
134
+ reason: "process exited cleanly",
135
+ };
136
+ }
137
+ if (exitCode !== null) {
138
+ return {
139
+ kind: input.kind,
140
+ class: "non_zero_exit",
141
+ crashed: true,
142
+ exitCode,
143
+ signal,
144
+ command,
145
+ protocol,
146
+ reason: `process exited with code ${exitCode}`,
147
+ };
148
+ }
149
+ return {
150
+ kind: input.kind,
151
+ class: "protocol_exit",
152
+ crashed: true,
153
+ exitCode,
154
+ signal,
155
+ command,
156
+ protocol,
157
+ reason: "process exited before protocol completion",
158
+ };
159
+ }
160
+
161
+ export async function writeCrashReport(
162
+ input: CrashClassificationInput,
163
+ options: { cwd?: string; env?: NodeJS.ProcessEnv; now?: Date } = {},
164
+ ): Promise<CrashReportWriteResult> {
165
+ const classification = classifyProcessCrash(input);
166
+ const report: CrashReport = {
167
+ schemaVersion: 1,
168
+ createdAt: (options.now ?? new Date()).toISOString(),
169
+ pid: process.pid,
170
+ cwd: options.cwd ?? process.cwd(),
171
+ ...classification,
172
+ stderrPreview: input.stderr ? trimStartBytes(input.stderr, STDERR_PREVIEW_BYTES) : undefined,
173
+ spawnError: input.spawnError === undefined ? undefined : stringifyError(input.spawnError),
174
+ };
175
+ const enabled = crashDiagnosticsEnabled(options.env);
176
+
177
+ if (!classification.crashed || !enabled) {
178
+ return { report, path: null, enabled };
179
+ }
180
+
181
+ try {
182
+ const dir = getCrashDiagnosticsDirectory(options.env);
183
+ await ensurePrivateDiagnosticsDirectory(dir);
184
+ const filename = `${report.createdAt.replace(/[:.]/g, "-")}-${report.kind}-${report.class}-${process.pid}.json`;
185
+ const reportPath = path.join(dir, filename);
186
+ await writePrivateCrashReport(reportPath, `${JSON.stringify(report, null, 2)}\n`);
187
+ return { report, path: reportPath, enabled };
188
+ } catch {
189
+ return { report, path: null, enabled };
190
+ }
191
+ }
192
+
193
+ export function formatCrashDiagnosticNotice(result: CrashReportWriteResult): string | null {
194
+ if (!result.report.crashed || !result.enabled) return null;
195
+ const location = result.path ? ` report=${result.path}` : "";
196
+ return `[crash:${result.report.kind}:${result.report.class}] ${result.report.reason}${location}`;
197
+ }
198
+
199
+ async function ensurePrivateDiagnosticsDirectory(dir: string): Promise<void> {
200
+ await fs.mkdir(dir, { recursive: true, mode: DIRECTORY_MODE });
201
+ await fs.chmod(dir, DIRECTORY_MODE);
202
+ }
203
+
204
+ async function writePrivateCrashReport(reportPath: string, contents: string): Promise<void> {
205
+ const file = await fs.open(reportPath, "wx", REPORT_FILE_MODE);
206
+ try {
207
+ await file.writeFile(contents);
208
+ } finally {
209
+ await file.close();
210
+ }
211
+ await fs.chmod(reportPath, REPORT_FILE_MODE);
212
+ }
213
+
214
+ function stringifyError(error: unknown): string {
215
+ if (error instanceof Error) return error.message;
216
+ return String(error);
217
+ }
218
+
219
+ function trimStartBytes(value: string, maxBytes: number): string {
220
+ const bytes = Buffer.from(value);
221
+ if (bytes.byteLength <= maxBytes) return value;
222
+ return Buffer.from(bytes.subarray(bytes.byteLength - maxBytes)).toString("utf8");
223
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Runtime resource owner gauges (Stage 2 observability).
3
+ *
4
+ * Reports counts of long-lived runtime owners as plain data so memory/CPU leak
5
+ * work can see whether owner maps grow without bound across a session. This is
6
+ * framework-agnostic on purpose: it does not depend on the TUI metrics surface,
7
+ * so any consumer (debug report, a metrics bridge, a test) can sample it.
8
+ */
9
+ import { getShellSessionCount } from "../exec/bash-executor";
10
+
11
+ /**
12
+ * Current runtime owner counts, keyed by `<owner>.<resource>`. Extend as more
13
+ * owners (Python kernels, LSP clients, browser tabs, async jobs, streaming
14
+ * queues) expose count getters.
15
+ */
16
+ export function getRuntimeResourceCounts(): Record<string, number> {
17
+ return {
18
+ "bash.shellSessions": getShellSessionCount(),
19
+ };
20
+ }
@@ -330,6 +330,12 @@ export function renderDeepInterviewAskQuestion(question: string, uiTheme: Theme)
330
330
  return renderModel(model, uiTheme);
331
331
  }
332
332
 
333
+ export function isDeepInterviewAskQuestion(question: string): boolean {
334
+ if (parseTopologyQuestion(question) ?? parseRoundQuestion(question)) return true;
335
+ const normalized = normalizeText(question);
336
+ return /(?:^|\n)\s*Round\s+\d+\s*\|.*?\bAmbiguity\b/i.test(normalized);
337
+ }
338
+
333
339
  export function formatDeepInterviewSelectorPrompt(question: string): string | null {
334
340
  const model = parseTopologyQuestion(question) ?? parseRoundQuestion(question);
335
341
  if (!model) return null;
@@ -125,7 +125,7 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
125
125
  ```json
126
126
  {
127
127
  "active": true,
128
- "current_phase": "deep-interview",
128
+ "current_phase": "interviewing",
129
129
  "state": {
130
130
  "interview_id": "<uuid>",
131
131
  "type": "greenfield|brownfield",
@@ -49,10 +49,12 @@ Restricted read-only role agents (`planner`, `architect`, and `critic`) must pas
49
49
 
50
50
  After a role agent persists a stage artifact, its model-facing response to the caller SHOULD be receipt-only: return the `gjc ralplan --write --json` receipt (`run_id`, `path`, `stage`, `stage_n`, `sha256`, `created_at`) plus the minimal verdict/status fields the caller needs for routing, and do **not** paste the full persisted markdown back into the parent conversation. Downstream reviewers should receive the artifact path/receipt and read the persisted file themselves when they actually need the body. This preserves the audit trail while preventing Planner/Architect/Critic verdict bodies from being duplicated into the main-agent context.
51
51
 
52
+ RECEIPT-ONLY guideline: role agents (`planner`, `architect`, and `critic`) persist durable outputs via `gjc ralplan --write` and return ONLY the receipt fields (`run_id`, `path`, `sha256`) plus verdict/status routing fields; include `stage` and `stage_n` when available, and never return the full persisted body.
53
+
52
54
  This skill runs GJC planning in consensus mode for the provided arguments.
53
55
 
54
56
  The consensus workflow:
55
- 1. **Planner** creates initial plan and a compact **RALPLAN-DR summary** before review, then persists the stage with `gjc ralplan --write --stage planner --stage_n 1 --artifact "..."`:
57
+ 1. **Planner** creates the initial plan and a compact **RALPLAN-DR summary** before review. Launch the Planner ONCE per run as a detached, resumable subagent (await it before the Architect) and record its returned subagent id as the run's persisted Planner id; persist the stage with `gjc ralplan --write --stage planner --stage_n 1 --artifact "..." --planner-id <id> --planner-resumable <true|false>` (see **Persisted Planner** below):
56
58
  - After persistence, return only the receipt/path plus compact planning status; do not paste the full plan markdown back to the caller unless explicitly requested.
57
59
  - Principles (3-5)
58
60
  - Decision Drivers (top 3)
@@ -66,7 +68,7 @@ The consensus workflow:
66
68
  - The Critic agent/subagent must persist its evaluation with `gjc ralplan --write --stage critic --stage_n <N> --artifact "..." --json`, then return the receipt/path plus compact verdict/status (`OKAY`/`ITERATE`/`REJECT`) instead of pasting the full evaluation body.
67
69
  5. **Re-review loop** (max 5 iterations): Any non-`APPROVE` Critic verdict (`ITERATE` or `REJECT`) MUST run the same full closed loop:
68
70
  a. Collect Architect + Critic feedback
69
- b. Revise the plan with Planner
71
+ b. Revise the plan by resuming the SAME persisted Planner subagent with consolidated Architect + Critic feedback (see **Persisted Planner** below); fall back to a fresh Planner spawn only per the fallback routing table
70
72
  c. Return to Architect review
71
73
  - Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact "..." --json` before re-review, then pass the receipt/path forward instead of duplicating the full revision markdown in the parent conversation.
72
74
  d. Return to Critic evaluation
@@ -88,6 +90,33 @@ The consensus workflow:
88
90
 
89
91
  Follow the Plan skill's full documentation for consensus mode details.
90
92
 
93
+ ### Persisted Planner (consensus loop)
94
+
95
+ The Planner is a **same-session persisted subagent**: launched detached once, awaited before the Architect, then **resumed** with consolidated Architect + Critic feedback on every re-review pass instead of being re-spawned. The Architect and Critic stay **fresh, independent spawns each pass** so their verdicts remain reproducible from their pass artifacts alone. Do NOT modify the subagent control surface; this orchestration uses the existing `subagent` resume/steer controls only.
96
+
97
+ **Persistence boundary:** this is same-parent, active-session continuity only. Resumability depends on the in-memory subagent record (and a persistent parent session — an in-memory parent yields `resumable:false`), not just a session file. The `.gjc` run-state record is an audit/routing hint, NOT a durable cross-process subagent registry. After a process restart, a missing record, or any unavailable/failed resume, use the fresh Planner fallback.
98
+
99
+ **Resume routing table** (per re-review pass, when resuming the persisted Planner id):
100
+
101
+ | Resume outcome | Action |
102
+ |---|---|
103
+ | `running` | `steer`/inject the consolidated feedback to the same id, then await — do NOT fresh-spawn |
104
+ | `queued` | retain/update the queued message or await the same id — do NOT fresh-spawn just because it is queued |
105
+ | `context_unavailable`, `not_found`, `no_runner`, `resume_failed` | fresh Planner spawn for that pass; record the fallback metadata |
106
+ | terminal (`completed`/`failed`/`cancelled`) + revision message | resume the same id when context is available; otherwise use the fresh fallback above |
107
+
108
+ **Recording persisted-Planner metadata** (audit/routing only — never claim `subagent list` proves resumability, since the snapshot does not expose `resumable`). Ride these optional flags on the normal `--write` for the planner/revision stage of the pass:
109
+
110
+ ```
111
+ gjc ralplan --write --stage revision --stage_n <N> --artifact "..." \
112
+ --planner-id <id> --planner-resumable <true|false> \
113
+ --fallback-reason <context_unavailable|not_found|no_runner|resume_failed|process_restart|missing_record> \
114
+ --fallback-attempted-id <id> --fallback-stage-n <N> \
115
+ --fallback-receipt-path <fresh-planner-stage-artifact-path> --json
116
+ ```
117
+
118
+ Set `--planner-resumable true` only when the parent session is provably persistent; set/record `false` after an observed `context_unavailable`; otherwise omit it (unknown). Fallback flags are recorded only when a fresh-spawn fallback actually occurs: a fallback record requires `--fallback-reason` **together with** `--fallback-attempted-id` and `--fallback-stage-n` (the failed id and the pass it failed on), while `--fallback-receipt-path` (the fresh Planner's stage artifact) is optional.
119
+
91
120
  ## Pre-Execution Gate
92
121
 
93
122
  ### Why the Gate Exists
@@ -50,12 +50,38 @@ Use `goal({"op":"get"})` snapshots inside Ultragoal for ledger reconciliation. T
50
50
 
51
51
  ## Create goals
52
52
 
53
- 1. Run one of:
53
+ 1. Decide on the brief. To produce **multiple** stories, separate them with a reserved `@goal:` delimiter line; the title follows on the same line and the objective is everything beneath it until the next delimiter:
54
+
55
+ ```text
56
+ Shared brief constraints / context go here (optional preamble).
57
+
58
+ @goal: Parse the intake CSVs
59
+ Ingest reviewer CSVs from the watch dir, validate headers, and reject
60
+ malformed rows with a per-row reason. Objectives can span multiple lines
61
+ and contain `code`, "quotes", or commands — no escaping needed.
62
+
63
+ @goal: Normalize records
64
+ Map raw rows onto the canonical schema and dedupe by record id.
65
+
66
+ @goal: Export the audit report
67
+ Emit an audit-ready report covering every accepted and rejected row.
68
+ ```
69
+
70
+ Delimiter contract:
71
+ - A `@goal` line is a story boundary **only** when it starts at column 0 (no leading whitespace) and the character right after `@goal` is `:`, whitespace (space or tab), or end-of-line. So `@goal: Title`, `@goal Title`, and a bare `@goal` line all open a story.
72
+ - `@goalish`, `@goals:`, `@goal-foo`, `@goal.foo`, `@goal/foo`, and any indented or mid-line `@goal` are ordinary objective text, not delimiters. To keep a literal `@goal` line inside an objective, indent it.
73
+ - A title-only block (no body) uses the title as its objective. An empty title borrows the first body line as the title. A block with **neither** title nor body is rejected — `create-goals` errors instead of writing a placeholder goal.
74
+ - **Preamble** (any text before the first `@goal` delimiter) is global context/constraints only; it is retained in the brief but is **not** turned into a goal. Every executable story needs its own `@goal` block.
75
+ - With **no** `@goal` delimiter anywhere, the whole brief becomes a single goal `G001` (unchanged legacy behavior).
76
+
77
+ Stories become `G001`, `G002`, … in order.
78
+
79
+ 2. Run one of:
54
80
  - `gjc ultragoal create-goals --brief "<brief>"`
55
81
  - `gjc ultragoal create-goals --brief-file <path>`
56
82
  - `cat <brief> | gjc ultragoal create-goals --from-stdin`
57
83
  - `gjc ultragoal create-goals --gjc-goal-mode per-story --brief "<brief>"` only when one GJC goal context per story is explicitly preferred
58
- 2. Inspect `.gjc/ultragoal/goals.json` and refine if needed.
84
+ 3. Inspect `.gjc/ultragoal/goals.json` and refine if needed.
59
85
 
60
86
  ## Complete goals
61
87
 
@@ -1,5 +1,6 @@
1
1
  import { getProjectDir, logger } from "@gajae-code/utils";
2
2
  import { Settings } from "../../config/settings";
3
+ import { formatCrashDiagnosticNotice, writeCrashReport } from "../../debug/crash-diagnostics";
3
4
  import { OutputSink } from "../../session/streaming-output";
4
5
  import type { ToolSession } from "../../tools";
5
6
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
@@ -62,6 +63,8 @@ export interface PythonExecutorOptions {
62
63
 
63
64
  export interface PythonKernelExecutor {
64
65
  execute: (code: string, options?: KernelExecuteOptions) => Promise<KernelExecuteResult>;
66
+ getExitCode?: () => number | null;
67
+ peekStderr?: () => string;
65
68
  }
66
69
 
67
70
  export interface PythonResult {
@@ -446,12 +449,29 @@ async function executeWithKernel(
446
449
  const annotation = result.timedOut
447
450
  ? formatKernelTimeoutAnnotation(executionTimeoutMs, result.kernelKilled ?? false)
448
451
  : undefined;
452
+ let crashNotice: string | null = null;
453
+ if (result.kernelKilled) {
454
+ crashNotice = formatCrashDiagnosticNotice(
455
+ await writeCrashReport(
456
+ {
457
+ kind: "python",
458
+ exitCode: kernel.getExitCode?.(),
459
+ cancelled: false,
460
+ timedOut: result.timedOut,
461
+ stderr: kernel.peekStderr?.(),
462
+ protocol: "eval.py.kernel",
463
+ },
464
+ { cwd: options?.cwd },
465
+ ),
466
+ );
467
+ }
468
+ const notice = [annotation, crashNotice].filter(text => text).join("; ") || undefined;
449
469
  return {
450
470
  exitCode: undefined,
451
471
  cancelled: true,
452
472
  displayOutputs,
453
473
  stdinRequested: result.stdinRequested,
454
- ...(await sink.dump(annotation)),
474
+ ...(await sink.dump(notice)),
455
475
  };
456
476
  }
457
477
 
@@ -183,6 +183,8 @@ export class PythonKernel {
183
183
  #disposed = false;
184
184
  #shutdownConfirmed = false;
185
185
  #exitedPromise: Promise<number> | null = null;
186
+ #exitCode: number | null = null;
187
+ #stderrTail = "";
186
188
  #pending = new Map<string, PendingExecution>();
187
189
  #readBuffer = "";
188
190
 
@@ -230,6 +232,7 @@ export class PythonKernel {
230
232
  kernel.#stdin = proc.stdin;
231
233
  kernel.#exitedPromise = proc.exited;
232
234
  void kernel.#exitedPromise.then(code => {
235
+ kernel.#exitCode = code;
233
236
  kernel.#alive = false;
234
237
  kernel.#abortPendingExecutions(`Python kernel exited with code ${code}`, { kernelKilled: true });
235
238
  });
@@ -255,6 +258,14 @@ export class PythonKernel {
255
258
  return this.#alive && !this.#disposed;
256
259
  }
257
260
 
261
+ getExitCode(): number | null {
262
+ return this.#exitCode;
263
+ }
264
+
265
+ peekStderr(): string {
266
+ return this.#stderrTail;
267
+ }
268
+
258
269
  async execute(code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
259
270
  if (!this.isAlive()) {
260
271
  throw new Error("Python kernel is not running");
@@ -493,6 +504,10 @@ export class PythonKernel {
493
504
  const { done, value } = await reader.read();
494
505
  if (done) break;
495
506
  const text = decoder.decode(value);
507
+ this.#stderrTail += text;
508
+ if (this.#stderrTail.length > 4096) {
509
+ this.#stderrTail = this.#stderrTail.slice(-4096);
510
+ }
496
511
  if (text.trim()) {
497
512
  logger.warn("Python runner stderr", { text });
498
513
  }
@@ -5,7 +5,9 @@
5
5
  */
6
6
  import * as fs from "node:fs/promises";
7
7
  import { executeShell, type MinimizerOptions, Shell } from "@gajae-code/natives";
8
+ import { postmortem } from "@gajae-code/utils";
8
9
  import { Settings, type ShellMinimizerSettings } from "../config/settings";
10
+ import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
9
11
  import { OutputSink } from "../session/streaming-output";
10
12
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
11
13
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
@@ -58,6 +60,30 @@ export interface BashResult {
58
60
  const shellSessions = new Map<string, Shell>();
59
61
  const brokenShellSessions = new Set<string>();
60
62
 
63
+ /** Number of persistent shell sessions currently retained (owner gauge). */
64
+ export function getShellSessionCount(): number {
65
+ return shellSessions.size;
66
+ }
67
+
68
+ /**
69
+ * Dispose all persistent shell sessions: abort in-flight work and drop the
70
+ * strong references so the native shells can be finalized. Healthy persistent
71
+ * sessions are otherwise retained for the whole process lifetime (MEM-7). This
72
+ * is registered as a postmortem cleanup so shutdown/signals release native
73
+ * shell resources, and is also callable directly (e.g. on owner teardown).
74
+ */
75
+ export async function disposeAllShellSessions(): Promise<void> {
76
+ // Snapshot and drop strong references up front so concurrent callers cannot
77
+ // reuse a session that is being torn down, then await every native abort so
78
+ // shutdown/signal cleanup does not return before resources are released.
79
+ const sessions = [...shellSessions.values()];
80
+ shellSessions.clear();
81
+ brokenShellSessions.clear();
82
+ await Promise.allSettled(sessions.map(session => session.abort()));
83
+ }
84
+
85
+ postmortem.register("bash-executor:shell-sessions", () => disposeAllShellSessions());
86
+
61
87
  async function resolveShellCwd(cwd: string | undefined): Promise<string | undefined> {
62
88
  if (!cwd) return undefined;
63
89
 
@@ -280,6 +306,21 @@ export async function executeBash(command: string, options?: BashExecutorOptions
280
306
  }
281
307
  }
282
308
 
309
+ const crashReport = await writeCrashReport(
310
+ {
311
+ kind: "bash",
312
+ command: [shell, "-lc", finalCommand],
313
+ exitCode: winner.result.exitCode,
314
+ stderr: undefined,
315
+ },
316
+ { cwd: commandCwd },
317
+ );
318
+ const crashNotice = formatCrashDiagnosticNotice(crashReport);
319
+ if (crashNotice) {
320
+ const separator = "\n";
321
+ sink.push(`${separator}${crashNotice}\n`);
322
+ }
323
+
283
324
  // Normal completion
284
325
  return {
285
326
  exitCode: winner.result.exitCode,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * CLI write/mutation receipt shaping (Workstream B, v4).
3
+ *
4
+ * `CliWriteReceipt` is the compact **stdout presentation** returned by a GJC
5
+ * mutation command. It carries only routing/audit fields a caller needs; it
6
+ * NEVER echoes the persisted body (full `state` envelope, ultragoal `plan`,
7
+ * team task body, ralplan `task`, etc.) — echoing those back is a redundant
8
+ * token leak because the caller already has the content it just wrote.
9
+ *
10
+ * This is intentionally a *separate* concept from the persisted
11
+ * `WorkflowStateReceipt` (the on-disk envelope `receipt` integrity field).
12
+ * Do NOT use `CliWriteReceipt` as a persistence schema or validate persisted
13
+ * envelopes against it.
14
+ */
15
+ export interface CliWriteReceipt {
16
+ ok: boolean;
17
+ [field: string]: unknown;
18
+ }
19
+
20
+ /**
21
+ * Serialize a write/mutation receipt to compact stdout JSON.
22
+ * `undefined` fields are dropped so optional routing fields stay absent
23
+ * rather than serialized as `null`.
24
+ */
25
+ export function renderCliWriteReceipt(receipt: Record<string, unknown>): string {
26
+ const out: Record<string, unknown> = {};
27
+ for (const key of Object.keys(receipt)) {
28
+ if (receipt[key] !== undefined) out[key] = receipt[key];
29
+ }
30
+ return `${JSON.stringify(out)}\n`;
31
+ }