@gajae-code/coding-agent 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/session.d.ts +24 -0
  5. package/dist/types/commands/skills.d.ts +26 -0
  6. package/dist/types/config/model-registry.d.ts +33 -4
  7. package/dist/types/config/models-config-schema.d.ts +52 -5
  8. package/dist/types/config/settings-schema.d.ts +1 -24
  9. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  10. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  11. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  12. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  17. package/dist/types/goals/runtime.d.ts +3 -9
  18. package/dist/types/goals/state.d.ts +3 -6
  19. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  20. package/dist/types/modes/components/model-selector.d.ts +21 -1
  21. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  22. package/dist/types/modes/components/status-line.d.ts +0 -3
  23. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -12
  25. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  26. package/dist/types/modes/theme/theme.d.ts +1 -2
  27. package/dist/types/modes/types.d.ts +1 -7
  28. package/dist/types/session/agent-session.d.ts +2 -0
  29. package/dist/types/session/contribution-prep.d.ts +47 -0
  30. package/dist/types/skill-state/active-state.d.ts +4 -0
  31. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  32. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  33. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  34. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  35. package/package.json +7 -7
  36. package/src/cli/args.ts +17 -2
  37. package/src/cli/skills-cli.ts +88 -0
  38. package/src/cli.ts +7 -1
  39. package/src/commands/contribution-prep.ts +41 -0
  40. package/src/commands/deep-interview.ts +6 -22
  41. package/src/commands/launch.ts +10 -1
  42. package/src/commands/ralplan.ts +10 -22
  43. package/src/commands/session.ts +150 -0
  44. package/src/commands/skills.ts +48 -0
  45. package/src/commands/state.ts +14 -4
  46. package/src/commands/team.ts +23 -3
  47. package/src/commit/agentic/index.ts +1 -0
  48. package/src/commit/pipeline.ts +1 -0
  49. package/src/config/model-registry.ts +269 -10
  50. package/src/config/models-config-schema.ts +124 -88
  51. package/src/config/settings-schema.ts +1 -25
  52. package/src/config.ts +1 -1
  53. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  54. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  55. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  57. package/src/eval/py/prelude.py +1 -1
  58. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  59. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  60. package/src/gjc-runtime/launch-tmux.ts +83 -43
  61. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  62. package/src/gjc-runtime/state-runtime.ts +562 -0
  63. package/src/gjc-runtime/team-runtime.ts +708 -52
  64. package/src/gjc-runtime/tmux-common.ts +119 -0
  65. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  66. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  67. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  68. package/src/goals/runtime.ts +38 -144
  69. package/src/goals/state.ts +36 -7
  70. package/src/goals/tools/goal-tool.ts +15 -172
  71. package/src/hooks/skill-state.ts +31 -12
  72. package/src/internal-urls/docs-index.generated.ts +4 -3
  73. package/src/main.ts +10 -1
  74. package/src/modes/components/model-selector.ts +109 -28
  75. package/src/modes/components/skill-hud/render.ts +4 -0
  76. package/src/modes/components/status-line/segments.ts +5 -16
  77. package/src/modes/components/status-line/types.ts +0 -3
  78. package/src/modes/components/status-line.ts +0 -6
  79. package/src/modes/controllers/command-controller.ts +25 -1
  80. package/src/modes/controllers/input-controller.ts +0 -15
  81. package/src/modes/controllers/selector-controller.ts +42 -2
  82. package/src/modes/interactive-mode.ts +18 -219
  83. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  84. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  85. package/src/modes/theme/theme.ts +0 -6
  86. package/src/modes/types.ts +1 -7
  87. package/src/prompts/goals/goal-continuation.md +1 -4
  88. package/src/prompts/goals/goal-mode-active.md +3 -5
  89. package/src/prompts/system/system-prompt.md +5 -7
  90. package/src/prompts/tools/goal.md +4 -4
  91. package/src/sdk.ts +2 -1
  92. package/src/session/agent-session.ts +18 -0
  93. package/src/session/contribution-prep.ts +320 -0
  94. package/src/setup/provider-onboarding.ts +2 -0
  95. package/src/skill-state/active-state.ts +38 -0
  96. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  97. package/src/skill-state/workflow-hud.ts +23 -5
  98. package/src/skill-state/workflow-state-contract.ts +121 -0
  99. package/src/slash-commands/acp-builtins.ts +11 -2
  100. package/src/slash-commands/builtin-registry.ts +40 -13
  101. package/src/task/commands.ts +1 -5
  102. package/src/tools/gh.ts +212 -2
  103. package/src/tools/index.ts +2 -5
  104. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  105. package/dist/types/commands/question.d.ts +0 -7
  106. package/dist/types/modes/loop-limit.d.ts +0 -22
  107. package/src/commands/gjc-runtime-bridge.ts +0 -227
  108. package/src/commands/question.ts +0 -12
  109. package/src/modes/loop-limit.ts +0 -140
  110. package/src/prompts/commands/orchestrate.md +0 -49
  111. package/src/prompts/goals/goal-budget-limit.md +0 -16
  112. package/src/prompts/tools/create-goal.md +0 -3
  113. package/src/prompts/tools/get-goal.md +0 -3
  114. package/src/prompts/tools/update-goal.md +0 -3
@@ -11,7 +11,7 @@ Use when the user asks for `ultragoal`, `create-goals`, `complete-goals`, durabl
11
11
 
12
12
  ## Purpose
13
13
 
14
- `ultragoal` turns a brief into repo-native artifacts and then drives a GJC goal safely through the named goal tools: `get_goal`, `create_goal`, and `update_goal`. New plans default to a stable pointer-style aggregate GJC goal for the whole durable plan in `.gjc/ultragoal/goals.json`, including later accepted/appended stories under the original brief constraints, while GJC tracks G001/G002 story progress in the ledger. Ultragoal does not call `/goal clear`; before multiple sequential ultragoal runs in one session/thread, manually run `/goal clear` in the UI so the previous completed aggregate goal does not block or confuse the next `create_goal`.
14
+ `ultragoal` turns a brief into repo-native artifacts and then drives a GJC goal safely through the unified `goal` tool. New plans default to a stable pointer-style aggregate GJC goal for the whole durable plan in `.gjc/ultragoal/goals.json`, including later accepted/appended stories under the original brief constraints, while GJC tracks G001/G002 story progress in the ledger. Ultragoal does not call `/goal clear`; before multiple sequential ultragoal runs in one session/thread, manually run `/goal clear` in the UI so the previous completed aggregate goal does not block or confuse the next `goal({"op":"create"})`.
15
15
 
16
16
  - `.gjc/ultragoal/brief.md`
17
17
  - `.gjc/ultragoal/goals.json`
@@ -30,21 +30,20 @@ gjc ultragoal create-goals --brief "<brief>"
30
30
  gjc ultragoal create-goals --brief-file <path>
31
31
  gjc ultragoal complete-goals
32
32
  gjc ultragoal complete-goals --retry-failed
33
- gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <get-goal-json-or-path> --quality-gate-json <quality-gate-json-or-path>
33
+ gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <goal-get-json-or-path> --quality-gate-json <quality-gate-json-or-path>
34
34
  gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"
35
- gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve final review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --gjc-goal-json <active-get-goal-json-or-path>
35
+ gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve final review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --gjc-goal-json <active-goal-get-json-or-path>
36
36
  ```
37
37
 
38
38
  Use these exact goal-tool calls for the inline goal state:
39
39
 
40
40
  ```json
41
- get_goal({})
42
- create_goal({"objective":"<printed aggregate or per-story objective>"})
43
- update_goal({"status":"complete"})
41
+ goal({"op":"get"})
42
+ goal({"op":"create","objective":"<printed aggregate or per-story objective>"})
43
+ goal({"op":"complete"})
44
44
  ```
45
45
 
46
- `get_goal`, `create_goal`, and `update_goal` share the same session goal state as `/goal`; prefer these named tools inside Ultragoal because they produce JSON snapshots for ledger reconciliation.
47
-
46
+ The unified `goal` tool shares the same session goal state as `/goal`; use `goal({"op":"get"})` snapshots inside Ultragoal for ledger reconciliation.
48
47
 
49
48
  ## Create goals
50
49
 
@@ -61,17 +60,17 @@ Loop until `gjc ultragoal status` reports all goals complete:
61
60
 
62
61
  1. Run `gjc ultragoal complete-goals`.
63
62
  2. Read the printed handoff.
64
- 3. Call `get_goal({})`.
65
- 4. If no active GJC goal exists, call `create_goal({"objective":"<printed payload objective>"})` with the printed payload. In aggregate mode, if the same aggregate objective is already active, continue the current GJC story without creating a new GJC goal.
63
+ 3. Call `goal({"op":"get"})`.
64
+ 4. If no active GJC goal exists, call `goal({"op":"create","objective":"<printed payload objective>"})` with the printed payload. In aggregate mode, if the same aggregate objective is already active, continue the current GJC story without creating a new GJC goal.
66
65
  5. Complete the current GJC story only.
67
66
  6. Run a completion audit against the story objective and real artifacts/tests.
68
- 7. Before any `--status complete` checkpoint, run the mandatory final cleanup/review gate below. In aggregate mode, do **not** call `update_goal` for intermediate stories; checkpoint each story with a fresh `get_goal({})` snapshot whose aggregate objective is still `active`. On the final story, use the same fresh active snapshot to create the final aggregate receipt first; only after that receipt exists may `update_goal({"status":"complete"})` run.
67
+ 7. Before any `--status complete` checkpoint, run the mandatory final cleanup/review gate below. In aggregate mode, do **not** call `goal({"op":"complete"})` for intermediate stories; checkpoint each story with a fresh `goal({"op":"get"})` snapshot whose aggregate objective is still `active`. On the final story, use the same fresh active snapshot to create the final aggregate receipt first; only after that receipt exists may `goal({"op":"complete"})` run.
69
68
  8. Checkpoint the durable ledger with that fresh active snapshot. Complete checkpoints require `--quality-gate-json`; the runtime hook rejects closure without a clean architect review:
70
- `gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <get_goal-json-or-path> --quality-gate-json <quality-gate-json-or-path>`
69
+ `gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --gjc-goal-json <goal-get-json-or-path> --quality-gate-json <quality-gate-json-or-path>`
71
70
  9. If blocked or failed, checkpoint failure:
72
71
  `gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"`
73
72
  11. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
74
- `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks create_goal in this thread>" --gjc-goal-json <get_goal-json-or-path>`
73
+ `gjc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy GJC goal blocks goal create in this thread>" --gjc-goal-json <goal-get-json-or-path>`
75
74
  12. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
76
75
 
77
76
  ## Dynamic steering
@@ -104,7 +103,6 @@ Steering invariants:
104
103
 
105
104
  UserPromptSubmit uses the same steering API only for structured directives such as `GJC_ULTRAGOAL_STEER: { ... }`, `gjc.ultragoal.steer: { ... }`, or `gjc ultragoal steer: { ... }`. Normal prose does not mutate state, and repeated prompt-submit directives dedupe by prompt signature or idempotency key.
106
105
 
107
-
108
106
  ## Role-agent delegation guidance
109
107
 
110
108
  Ultragoal execution should use GJC's bundled role-agent roster when a durable story is large enough to benefit from delegation:
@@ -126,10 +124,10 @@ For large subgoals with independent slices, the Ultragoal leader must spawn para
126
124
 
127
125
  Use ultragoal and team together for a durable Ultragoal story that benefits from one visible tmux worker session. Ultragoal remains leader-owned: `.gjc/ultragoal/goals.json` stores the story plan and `.gjc/ultragoal/ledger.jsonl` stores checkpoints. Team is the single-worker tmux execution engine and returns task/evidence status to the leader.
128
126
 
129
- The leader checkpoints Ultragoal from Team evidence with a fresh `get_goal` snapshot:
127
+ The leader checkpoints Ultragoal from Team evidence with a fresh `goal({"op":"get"})` snapshot:
130
128
 
131
129
  ```sh
132
- gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<team evidence mentioning .gjc/ultragoal and <id>>" --gjc-goal-json <fresh-get_goal-json-or-path> --quality-gate-json <quality-gate-json-or-path>
130
+ gjc ultragoal checkpoint --goal-id <id> --status complete --evidence "<team evidence mentioning .gjc/ultragoal and <id>>" --gjc-goal-json <fresh-goal-get-json-or-path> --quality-gate-json <quality-gate-json-or-path>
133
131
  ```
134
132
 
135
133
  Workers do not own ultragoal goal state, do not create worker ultragoal ledgers, and do not checkpoint Ultragoal. Workers must not run `gjc ultragoal checkpoint`; checkpoint authority stays with the leader after worker tasks are terminal. Team launch remains explicit; Ultragoal does not auto-launch Team and performs no hidden goal mutation.
@@ -142,7 +140,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
142
140
  2. Run a cleanup/refactor review pass on changed files only; if there are no relevant edits, the cleaner still runs and records a passed/no-op report.
143
141
  3. Rerun verification after the cleaner pass.
144
142
  4. Run a final code review pass and fold it into the strict quality gate. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, every evidence field is non-empty, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, or non-empty blockers are non-clean.
145
- 5. If review is non-clean, do **not** call `update_goal`. Record durable blocker work instead:
143
+ 5. If review is non-clean, do **not** call `goal({"op":"complete"})`. Record durable blocker work instead:
146
144
 
147
145
  1. Run targeted implementation verification for the story.
148
146
  2. Delegate an `architect` review covering all three lanes:
@@ -150,12 +148,12 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
150
148
  - product-side: user-visible behavior, acceptance criteria, edge cases, regressions.
151
149
  - code-side: maintainability, tests, integration points, and unsafe shortcuts.
152
150
  3. Delegate an `executor` QA/red-team lane to build and run the e2e/read-teaming QA suite appropriate for the story. This lane must try to break the change, not just confirm the happy path.
153
- 4. If any lane finds an issue, do **not** checkpoint `complete` and do **not** call `update_goal`. Record durable blocker work instead:
151
+ 4. If any lane finds an issue, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record durable blocker work instead:
154
152
  ```sh
155
- gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve verification blockers" --objective "<blocker-resolution objective>" --evidence "<architect/executor findings>" --gjc-goal-json <active-get-goal-json-or-path>
153
+ gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve verification blockers" --objective "<blocker-resolution objective>" --evidence "<architect/executor findings>" --gjc-goal-json <active-goal-get-json-or-path>
156
154
  ```
157
155
  5. Complete or steer through the blocker story, then rerun the full blocking verification loop. Repeat until all verifier lanes are clean.
158
- 6. Only after the loop is clean, checkpoint the story as complete with a structured quality gate and a fresh active `get_goal` snapshot. The checkpoint creates a receipt; `goals.json.status` alone is not proof. In aggregate mode, the final aggregate receipt must exist before `update_goal({"status":"complete"})` is allowed.
156
+ 6. Only after the loop is clean, checkpoint the story as complete with a structured quality gate and a fresh active `goal({"op":"get"})` snapshot. The checkpoint creates a receipt; `goals.json.status` alone is not proof. In aggregate mode, the final aggregate receipt must exist before `goal({"op":"complete"})` is allowed.
159
157
 
160
158
  The native `checkpoint --status complete` command rejects missing or shallow gates. `--quality-gate-json` must include:
161
159
 
@@ -197,10 +195,10 @@ Receipts are freshness-scoped:
197
195
  ## Constraints
198
196
 
199
197
  - The shell command cannot directly invoke interactive `/goal`; it emits a model-facing handoff for the active GJC agent.
200
- - Ultragoal intentionally does not invoke `/goal clear` or hidden `thread/goal/clear`; use only the named goal-tool surface: `get_goal`, `create_goal`, and `update_goal`.
198
+ - Ultragoal intentionally does not invoke `/goal clear` or hidden `thread/goal/clear`; use only the unified goal-tool surface: `goal({"op":"get"})`, `goal({"op":"create"})`, and `goal({"op":"complete"})`.
201
199
  - After a completed aggregate ultragoal run, clear the goal manually with `/goal clear` before starting another ultragoal run in the same session/thread.
202
- - Never call `create_goal` when `get_goal` reports a different active goal.
203
- - Never call `update_goal` unless the aggregate run or legacy per-story goal is actually complete.
204
- - In aggregate mode, intermediate and final story checkpoints require a matching `active` GJC goal snapshot; the final story checkpoint creates the final aggregate receipt before `update_goal({"status":"complete"})` may reconcile the inline goal state.
205
- - Completion checkpoints require read-only goal snapshot reconciliation: pass fresh `get_goal` JSON/path with `--gjc-goal-json`; shell commands and hooks must not mutate goal state.
200
+ - Never call `goal({"op":"create"})` when `goal({"op":"get"})` reports a different active goal.
201
+ - Never call `goal({"op":"complete"})` unless the aggregate run or legacy per-story goal is actually complete.
202
+ - In aggregate mode, intermediate and final story checkpoints require a matching `active` GJC goal snapshot; the final story checkpoint creates the final aggregate receipt before `goal({"op":"complete"})` may reconcile the inline goal state.
203
+ - Completion checkpoints require read-only goal snapshot reconciliation: pass fresh `goal({"op":"get"})` JSON/path with `--gjc-goal-json`; shell commands and hooks must not mutate goal state.
206
204
  - Treat `ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
@@ -431,7 +431,7 @@ if "__gjc_prelude_loaded__" not in globals():
431
431
  return data.get("value")
432
432
 
433
433
  class _ToolProxy:
434
- """`tool.<name>(args)` proxy mirroring the JS runtime bridge."""
434
+ """`tool.<name>(args)` proxy mirroring the JS session tool API."""
435
435
 
436
436
  __slots__ = ("_base", "_token", "_session")
437
437
 
@@ -0,0 +1,279 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { syncSkillActiveState } from "../skill-state/active-state";
5
+ import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
6
+
7
+ /**
8
+ * Native implementation of `gjc deep-interview`.
9
+ *
10
+ * The CLI itself does not run the Socratic interview; that lives inside the `/skill:deep-interview`
11
+ * skill executed by the agent. This handler validates the documented argument-hint surface
12
+ * (`[--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
13
+ * updates the shared HUD rail via `syncSkillActiveState` so the active interview is visible to
14
+ * the TUI.
15
+ */
16
+
17
+ export interface DeepInterviewCommandResult {
18
+ status: number;
19
+ stdout?: string;
20
+ stderr?: string;
21
+ }
22
+
23
+ const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
24
+
25
+ const DEFAULT_AMBIGUITY_THRESHOLD = 0.05;
26
+
27
+ const RESOLUTION_THRESHOLDS = {
28
+ quick: 0.6,
29
+ standard: 0.5,
30
+ deep: 0.35,
31
+ } as const;
32
+
33
+ type DeepInterviewResolution = keyof typeof RESOLUTION_THRESHOLDS;
34
+
35
+ class DeepInterviewCommandError extends Error {
36
+ constructor(
37
+ public readonly exitStatus: number,
38
+ message: string,
39
+ ) {
40
+ super(message);
41
+ this.name = "DeepInterviewCommandError";
42
+ }
43
+ }
44
+
45
+ const VALUE_FLAGS = new Set(["--session-id", "--threshold", "--threshold-source"]);
46
+
47
+ function flagValue(args: readonly string[], flag: string): string | undefined {
48
+ const index = args.indexOf(flag);
49
+ if (index < 0) return undefined;
50
+ return args[index + 1];
51
+ }
52
+
53
+ function hasFlag(args: readonly string[], flag: string): boolean {
54
+ return args.includes(flag);
55
+ }
56
+
57
+ function assertSafePathComponent(value: string, label: string): void {
58
+ if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
59
+ throw new DeepInterviewCommandError(2, `invalid path component for --${label}: ${value}`);
60
+ }
61
+ }
62
+
63
+ function encodeSessionSegment(value: string): string {
64
+ return encodeURIComponent(value).replaceAll(".", "%2E");
65
+ }
66
+
67
+ interface ResolvedDeepInterviewArgs {
68
+ resolution: DeepInterviewResolution;
69
+ threshold: number;
70
+ thresholdSource: string;
71
+ sessionId?: string;
72
+ idea: string;
73
+ json: boolean;
74
+ }
75
+
76
+ async function readSettingsAmbiguityThreshold(
77
+ settingsPath: string,
78
+ ): Promise<{ threshold: number; source: string } | undefined> {
79
+ let raw: string;
80
+ try {
81
+ raw = await fs.readFile(settingsPath, "utf-8");
82
+ } catch (error) {
83
+ const err = error as NodeJS.ErrnoException;
84
+ if (err.code === "ENOENT") return undefined;
85
+ return undefined;
86
+ }
87
+ let parsed: unknown;
88
+ try {
89
+ parsed = JSON.parse(raw);
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ const candidate = (parsed as { gjc?: { deepInterview?: { ambiguityThreshold?: unknown } } })?.gjc?.deepInterview
94
+ ?.ambiguityThreshold;
95
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate <= 0 || candidate > 1) {
96
+ return undefined;
97
+ }
98
+ return { threshold: candidate, source: settingsPath };
99
+ }
100
+
101
+ async function resolveConfiguredAmbiguityThreshold(
102
+ cwd: string,
103
+ ): Promise<{ threshold: number; source: string } | undefined> {
104
+ const projectSettings = path.join(cwd, ".gjc", "settings.json");
105
+ const projectValue = await readSettingsAmbiguityThreshold(projectSettings);
106
+ if (projectValue) return projectValue;
107
+ const configDir = process.env.GJC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".gjc");
108
+ const userSettings = path.join(configDir, "settings.json");
109
+ return await readSettingsAmbiguityThreshold(userSettings);
110
+ }
111
+
112
+ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
113
+ const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
114
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
115
+
116
+ const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
117
+ if (explicitResolutions.length > 1) {
118
+ throw new DeepInterviewCommandError(2, "pass at most one of --quick, --standard, --deep");
119
+ }
120
+ const resolution: DeepInterviewResolution | undefined = explicitResolutions[0];
121
+
122
+ // Precedence: --threshold > settings.json (project then user) > resolution flag default > 0.05.
123
+ let threshold: number = DEFAULT_AMBIGUITY_THRESHOLD;
124
+ let thresholdSource = "default";
125
+ const thresholdOverride = flagValue(args, "--threshold");
126
+ if (thresholdOverride !== undefined) {
127
+ const parsed = Number(thresholdOverride);
128
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
129
+ throw new DeepInterviewCommandError(
130
+ 2,
131
+ `invalid --threshold: ${thresholdOverride}. Expected 0 < threshold <= 1.`,
132
+ );
133
+ }
134
+ threshold = parsed;
135
+ thresholdSource = flagValue(args, "--threshold-source")?.trim() || "flag:--threshold";
136
+ } else {
137
+ const configured = await resolveConfiguredAmbiguityThreshold(cwd);
138
+ if (configured) {
139
+ threshold = configured.threshold;
140
+ thresholdSource = configured.source;
141
+ } else if (resolution) {
142
+ threshold = RESOLUTION_THRESHOLDS[resolution];
143
+ thresholdSource = `flag:--${resolution}`;
144
+ }
145
+ }
146
+
147
+ const ideaParts: string[] = [];
148
+ let skipNext = false;
149
+ for (const arg of args) {
150
+ if (skipNext) {
151
+ skipNext = false;
152
+ continue;
153
+ }
154
+ if (VALUE_FLAGS.has(arg)) {
155
+ skipNext = true;
156
+ continue;
157
+ }
158
+ if (arg === "--quick" || arg === "--standard" || arg === "--deep" || arg === "--json") continue;
159
+ if (arg.startsWith("-")) {
160
+ throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview: ${arg}`);
161
+ }
162
+ ideaParts.push(arg);
163
+ }
164
+ const idea = ideaParts.join(" ").trim();
165
+ const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
166
+ return {
167
+ resolution: effectiveResolution,
168
+ threshold,
169
+ thresholdSource,
170
+ sessionId,
171
+ idea,
172
+ json: hasFlag(args, "--json"),
173
+ };
174
+ }
175
+
176
+ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
177
+ const stateDir = resolved.sessionId
178
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
179
+ : path.join(cwd, ".gjc", "state");
180
+ await fs.mkdir(stateDir, { recursive: true });
181
+ const statePath = path.join(stateDir, "deep-interview-state.json");
182
+ const now = new Date().toISOString();
183
+ const payload: Record<string, unknown> = {
184
+ active: true,
185
+ current_phase: "interviewing",
186
+ skill: "deep-interview",
187
+ resolution: resolved.resolution,
188
+ threshold: resolved.threshold,
189
+ threshold_source: resolved.thresholdSource,
190
+ state: {
191
+ initial_idea: resolved.idea,
192
+ rounds: [],
193
+ current_ambiguity: 1.0,
194
+ threshold: resolved.threshold,
195
+ threshold_source: resolved.thresholdSource,
196
+ },
197
+ updated_at: now,
198
+ };
199
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
200
+ await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
201
+ return statePath;
202
+ }
203
+
204
+ async function syncDeepInterviewHud(options: {
205
+ cwd: string;
206
+ sessionId?: string;
207
+ phase: string;
208
+ ambiguity?: number;
209
+ threshold?: number;
210
+ roundCount?: number;
211
+ specStatus?: string;
212
+ }): Promise<void> {
213
+ try {
214
+ await syncSkillActiveState({
215
+ cwd: options.cwd,
216
+ skill: "deep-interview",
217
+ active: options.phase !== "complete",
218
+ phase: options.phase,
219
+ sessionId: options.sessionId,
220
+ source: "gjc-deep-interview-native",
221
+ hud: buildDeepInterviewHudSummary({
222
+ phase: options.phase,
223
+ ambiguity: options.ambiguity,
224
+ threshold: options.threshold,
225
+ roundCount: options.roundCount,
226
+ specStatus: options.specStatus,
227
+ updatedAt: new Date().toISOString(),
228
+ }),
229
+ });
230
+ } catch {
231
+ // HUD sync is best-effort and must not change command semantics.
232
+ }
233
+ }
234
+
235
+ export async function runNativeDeepInterviewCommand(
236
+ args: string[],
237
+ cwd = process.cwd(),
238
+ ): Promise<DeepInterviewCommandResult> {
239
+ try {
240
+ const resolved = await resolveDeepInterviewArgs(args, cwd);
241
+ if (!resolved.idea) {
242
+ throw new DeepInterviewCommandError(
243
+ 2,
244
+ 'gjc deep-interview requires an idea, e.g. `gjc deep-interview "<idea>"`.',
245
+ );
246
+ }
247
+ const statePath = await seedDeepInterviewState(cwd, resolved);
248
+ await syncDeepInterviewHud({
249
+ cwd,
250
+ sessionId: resolved.sessionId,
251
+ phase: "interviewing",
252
+ ambiguity: 1,
253
+ threshold: resolved.threshold,
254
+ roundCount: 0,
255
+ });
256
+
257
+ const summary = {
258
+ skill: "deep-interview",
259
+ resolution: resolved.resolution,
260
+ threshold: resolved.threshold,
261
+ threshold_source: resolved.thresholdSource,
262
+ idea: resolved.idea,
263
+ state_path: statePath,
264
+ handoff: "Run `/skill:deep-interview` inside the GJC agent to drive the Socratic interview loop.",
265
+ };
266
+ const stdout = resolved.json
267
+ ? `${JSON.stringify(summary, null, 2)}\n`
268
+ : [
269
+ `Seeded deep-interview ${resolved.resolution} run at ${statePath}.`,
270
+ `Threshold: ${(resolved.threshold * 100).toFixed(0)}% (source: ${resolved.thresholdSource}).`,
271
+ "Run `/skill:deep-interview` inside the GJC agent to execute the interview.",
272
+ "",
273
+ ].join("\n");
274
+ return { status: 0, stdout };
275
+ } catch (error) {
276
+ if (error instanceof DeepInterviewCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
277
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
278
+ }
279
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { Snowflake } from "@gajae-code/utils";
4
- import type { Goal, GoalModeState } from "../goals/state";
4
+ import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
5
5
  import {
6
6
  buildSessionContext,
7
7
  loadEntriesFromFile,
@@ -94,24 +94,7 @@ export async function writePendingGoalModeRequest(input: {
94
94
  }
95
95
 
96
96
  function goalFromModeData(modeData: Record<string, unknown> | undefined): Goal | null {
97
- const candidate = modeData?.goal;
98
- if (typeof candidate !== "object" || candidate === null) return null;
99
- const goal = candidate as Partial<Goal>;
100
- if (
101
- typeof goal.id !== "string" ||
102
- typeof goal.objective !== "string" ||
103
- typeof goal.status !== "string" ||
104
- typeof goal.tokensUsed !== "number" ||
105
- typeof goal.timeUsedSeconds !== "number" ||
106
- typeof goal.createdAt !== "number" ||
107
- typeof goal.updatedAt !== "number"
108
- ) {
109
- return null;
110
- }
111
- if (!["active", "paused", "budget-limited", "complete", "dropped"].includes(goal.status)) {
112
- return null;
113
- }
114
- return goal as Goal;
97
+ return normalizeGoal(modeData?.goal);
115
98
  }
116
99
 
117
100
  function isNonTerminalGoal(goal: Goal | null): goal is Goal {
@@ -1,12 +1,30 @@
1
1
  import * as path from "node:path";
2
2
  import type { Args } from "../cli/args";
3
+ import {
4
+ buildGjcTmuxProfileCommands,
5
+ buildGjcTmuxSessionName,
6
+ buildGjcTmuxSessionSlug,
7
+ GJC_DEFAULT_TMUX_SESSION,
8
+ GJC_TMUX_COMMAND_ENV,
9
+ GJC_TMUX_MOUSE_ENV,
10
+ GJC_TMUX_PROFILE_ENV,
11
+ GJC_TMUX_SESSION_PREFIX,
12
+ type GjcTmuxProfileCommand,
13
+ resolveGjcTmuxCommand,
14
+ } from "./tmux-common";
15
+ import { findGjcTmuxSessionByBranch } from "./tmux-sessions";
16
+
17
+ export {
18
+ buildGjcTmuxProfileCommands,
19
+ GJC_DEFAULT_TMUX_SESSION,
20
+ GJC_TMUX_COMMAND_ENV,
21
+ GJC_TMUX_MOUSE_ENV,
22
+ GJC_TMUX_PROFILE_ENV,
23
+ GJC_TMUX_SESSION_PREFIX,
24
+ };
3
25
 
4
- export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
5
26
  export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
6
27
  export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
7
- export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
8
- export const GJC_TMUX_PROFILE_ENV = "GJC_TMUX_PROFILE";
9
- export const GJC_TMUX_MOUSE_ENV = "GJC_MOUSE";
10
28
 
11
29
  type LaunchPolicy = "direct" | "tmux";
12
30
 
@@ -26,6 +44,10 @@ export interface TmuxLaunchContext {
26
44
  tty?: TtyState;
27
45
  spawnSync?: TmuxSpawnSync;
28
46
  tmuxAvailable?: boolean;
47
+ worktreeBranch?: string | null;
48
+ currentBranch?: string | null;
49
+ existingBranchSessionName?: string | null;
50
+ project?: string | null;
29
51
  }
30
52
 
31
53
  export interface TmuxSpawnResult {
@@ -50,12 +72,9 @@ export interface TmuxLaunchPlan {
50
72
  cwd: string;
51
73
  innerCommand: string;
52
74
  newSessionArgs: string[];
53
- attachSessionArgs: string[];
54
- }
55
-
56
- export interface GjcTmuxProfileCommand {
57
- description: string;
58
- args: string[];
75
+ branch?: string | null;
76
+ attachSessionName?: string;
77
+ project?: string | null;
59
78
  }
60
79
 
61
80
  export interface GjcTmuxProfileResult {
@@ -70,6 +89,9 @@ export interface GjcTmuxProfileContext {
70
89
  cwd?: string;
71
90
  env?: NodeJS.ProcessEnv;
72
91
  spawnSync?: TmuxSpawnSync;
92
+ branch?: string | null;
93
+ branchSlug?: string | null;
94
+ project?: string | null;
73
95
  }
74
96
 
75
97
  interface CommandResolutionContext {
@@ -103,35 +125,14 @@ function shellQuote(value: string): string {
103
125
  return `'${value.replace(/'/g, `'\\''`)}'`;
104
126
  }
105
127
 
106
- function envDisabled(value: string | undefined): boolean {
107
- const normalized = value?.trim().toLowerCase();
108
- return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
109
- }
110
-
111
- export function buildGjcTmuxProfileCommands(
112
- target: string,
113
- env: NodeJS.ProcessEnv = process.env,
114
- ): GjcTmuxProfileCommand[] {
115
- if (envDisabled(env[GJC_TMUX_PROFILE_ENV])) return [];
116
- const commands: GjcTmuxProfileCommand[] = [
117
- { description: "mark GJC tmux ownership", args: ["set-option", "-t", target, "@gjc-profile", "1"] },
118
- { description: "enable tmux clipboard integration", args: ["set-option", "-t", target, "set-clipboard", "on"] },
119
- {
120
- description: "make copy-mode selection readable",
121
- args: ["set-window-option", "-t", target, "mode-style", "fg=colour231,bg=colour60"],
122
- },
123
- ];
124
- if (!envDisabled(env[GJC_TMUX_MOUSE_ENV]))
125
- commands.unshift({
126
- description: "enable tmux mouse scrolling",
127
- args: ["set-option", "-t", target, "mouse", "on"],
128
- });
129
- return commands;
130
- }
131
-
132
128
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
133
129
  const env = context.env ?? process.env;
134
- const commands = buildGjcTmuxProfileCommands(context.target, env);
130
+ const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
131
+ const commands = buildGjcTmuxProfileCommands(context.target, env, {
132
+ branch: context.branch ?? null,
133
+ branchSlug,
134
+ project: context.project ?? null,
135
+ });
135
136
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
136
137
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
137
138
  const cwd = context.cwd ?? process.cwd();
@@ -160,6 +161,25 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
160
161
  return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1 ${quoted}`;
161
162
  }
162
163
 
164
+ function readCurrentBranch(cwd: string): string | null {
165
+ try {
166
+ const result = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "--short", "HEAD"], {
167
+ cwd,
168
+ stdout: "pipe",
169
+ stderr: "ignore",
170
+ });
171
+ if (result.exitCode !== 0) return null;
172
+ const branch = result.stdout.toString().trim();
173
+ return branch || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
180
+ spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
181
+ }
182
+
163
183
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
164
184
  const env = context.env ?? process.env;
165
185
  const policy = parseLaunchPolicy(env);
@@ -171,10 +191,18 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
171
191
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
172
192
 
173
193
  const cwd = context.cwd ?? process.cwd();
174
- const sessionName = env.GJC_TMUX_SESSION?.trim() || GJC_DEFAULT_TMUX_SESSION;
175
- const tmuxCommand = env[GJC_TMUX_COMMAND_ENV]?.trim() || "tmux";
194
+ const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
195
+ const project = context.project ?? cwd;
196
+ const sessionName = buildGjcTmuxSessionName(env, { branch });
197
+ const tmuxCommand = resolveGjcTmuxCommand(env);
176
198
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
177
199
  if (!tmuxAvailable) return undefined;
200
+ const existingBranchSessionName =
201
+ "existingBranchSessionName" in context
202
+ ? (context.existingBranchSessionName ?? undefined)
203
+ : context.worktreeBranch
204
+ ? findGjcTmuxSessionByBranch(context.worktreeBranch, env, project)?.name
205
+ : undefined;
178
206
  const innerCommand = buildInnerCommand(
179
207
  {
180
208
  cwd,
@@ -189,7 +217,9 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
189
217
  cwd,
190
218
  innerCommand,
191
219
  newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
192
- attachSessionArgs: ["attach-session", "-t", sessionName],
220
+ branch,
221
+ project,
222
+ attachSessionName: existingBranchSessionName,
193
223
  };
194
224
  }
195
225
 
@@ -217,17 +247,27 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
217
247
  stdout: "inherit",
218
248
  stderr: "inherit",
219
249
  };
250
+ if (plan.attachSessionName) {
251
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
252
+ return attached.exitCode === 0;
253
+ }
220
254
  const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
221
255
  if (created.exitCode === 0) {
222
- applyGjcTmuxProfile({
256
+ const profile = applyGjcTmuxProfile({
223
257
  tmuxCommand: plan.tmuxCommand,
224
258
  target: plan.sessionName,
225
259
  cwd: plan.cwd,
226
260
  env,
227
261
  spawnSync,
262
+ branch: plan.branch,
263
+ project: plan.project,
228
264
  });
265
+ if (profile.failures.length > 0) {
266
+ cleanupCreatedTmuxSession(plan, spawnSync, options);
267
+ return false;
268
+ }
229
269
  }
230
- const attached = spawnSync(plan.tmuxCommand, plan.attachSessionArgs, options);
231
- if (created.exitCode === 0) return attached.exitCode === 0;
270
+ if (created.exitCode !== 0) return false;
271
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
232
272
  return attached.exitCode === 0;
233
273
  }