@gajae-code/coding-agent 0.7.10 → 0.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +4 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +8 -5
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/utils/cmux-workspace.d.ts +30 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/dist/types/web/search/provider.d.ts +9 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/package.json +7 -7
- package/src/cli/update-cli.ts +24 -0
- package/src/cli/web-search-cli.ts +2 -5
- package/src/cli.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/config/keybindings.ts +14 -5
- package/src/config/model-profiles.ts +16 -0
- package/src/coordinator-mcp/server.ts +9 -3
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +164 -42
- package/src/gjc-runtime/ralplan-runtime.ts +22 -6
- package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +8 -5
- package/src/gjc-runtime/state-writer.ts +5 -5
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/main.ts +2 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/model-selector.ts +6 -2
- package/src/modes/components/settings-selector.ts +0 -2
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/input-controller.ts +41 -9
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/prompt-action-autocomplete.ts +23 -1
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/telegram-daemon.ts +4 -4
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/tools/bash.md +1 -1
- package/src/sdk.ts +3 -6
- package/src/session/agent-session.ts +60 -17
- package/src/slash-commands/builtin-registry.ts +30 -10
- package/src/slash-commands/types.ts +2 -0
- package/src/tools/bash.ts +24 -8
- package/src/utils/cmux-workspace.ts +134 -7
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +118 -45
- package/src/web/search/provider.ts +65 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
|
@@ -22,6 +22,7 @@ interface AppKeybindings {
|
|
|
22
22
|
"app.suspend": true;
|
|
23
23
|
"app.thinking.cycle": true;
|
|
24
24
|
"app.thinking.toggle": true;
|
|
25
|
+
"app.commandPalette.open": true;
|
|
25
26
|
"app.model.cycleForward": true;
|
|
26
27
|
"app.model.cycleBackward": true;
|
|
27
28
|
"app.model.select": true;
|
|
@@ -59,6 +60,10 @@ declare module "@gajae-code/tui" {
|
|
|
59
60
|
interface Keybindings extends AppKeybindings {}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
export function defaultMessageQueueKeysForPlatform(platform: NodeJS.Platform = process.platform): KeyId {
|
|
64
|
+
return platform === "win32" ? "alt+q" : "alt+enter";
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
/**
|
|
63
68
|
* All keybindings definitions: TUI + app-specific.
|
|
64
69
|
*/
|
|
@@ -88,12 +93,16 @@ export const KEYBINDINGS = {
|
|
|
88
93
|
defaultKeys: "ctrl+t",
|
|
89
94
|
description: "Toggle thinking mode",
|
|
90
95
|
},
|
|
91
|
-
"app.
|
|
96
|
+
"app.commandPalette.open": {
|
|
92
97
|
defaultKeys: "ctrl+p",
|
|
98
|
+
description: "Open command palette",
|
|
99
|
+
},
|
|
100
|
+
"app.model.cycleForward": {
|
|
101
|
+
defaultKeys: "alt+n",
|
|
93
102
|
description: "Cycle to next model",
|
|
94
103
|
},
|
|
95
104
|
"app.model.cycleBackward": {
|
|
96
|
-
defaultKeys: "shift+
|
|
105
|
+
defaultKeys: "alt+shift+n",
|
|
97
106
|
description: "Cycle to previous model",
|
|
98
107
|
},
|
|
99
108
|
"app.model.select": {
|
|
@@ -121,7 +130,7 @@ export const KEYBINDINGS = {
|
|
|
121
130
|
description: "Send follow-up message (no default; Ctrl+Enter submits)",
|
|
122
131
|
},
|
|
123
132
|
"app.message.queue": {
|
|
124
|
-
defaultKeys:
|
|
133
|
+
defaultKeys: defaultMessageQueueKeysForPlatform(),
|
|
125
134
|
description: "Queue message for next turn",
|
|
126
135
|
},
|
|
127
136
|
"app.message.dequeue": {
|
|
@@ -141,8 +150,8 @@ export const KEYBINDINGS = {
|
|
|
141
150
|
description: "Copy prompt",
|
|
142
151
|
},
|
|
143
152
|
"app.session.new": {
|
|
144
|
-
defaultKeys:
|
|
145
|
-
description: "
|
|
153
|
+
defaultKeys: "ctrl+n",
|
|
154
|
+
description: "Start a new session",
|
|
146
155
|
},
|
|
147
156
|
"app.session.tree": {
|
|
148
157
|
defaultKeys: [],
|
|
@@ -104,6 +104,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
104
104
|
critic: "anthropic/claude-opus-4-8:high",
|
|
105
105
|
architect: "anthropic/claude-opus-4-8:xhigh",
|
|
106
106
|
}),
|
|
107
|
+
profile("claude-fable", ["anthropic"], {
|
|
108
|
+
default: "anthropic/claude-fable-5:xhigh",
|
|
109
|
+
executor: "anthropic/claude-sonnet-5",
|
|
110
|
+
planner: "anthropic/claude-fable-5:low",
|
|
111
|
+
critic: "anthropic/claude-fable-5:high",
|
|
112
|
+
architect: "anthropic/claude-fable-5:xhigh",
|
|
113
|
+
}),
|
|
107
114
|
profile("glm-eco", ["zai"], {
|
|
108
115
|
default: "zai/glm-5.2:low",
|
|
109
116
|
executor: "zai/glm-5.2:minimal",
|
|
@@ -261,6 +268,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
261
268
|
critic: "opencode-go/mimo-v2.5-pro",
|
|
262
269
|
architect: "openai-codex/gpt-5.5:xhigh",
|
|
263
270
|
}),
|
|
271
|
+
profile("fable-opus-codex", ["anthropic", "openai-codex"], {
|
|
272
|
+
default: "anthropic/claude-fable-5:high",
|
|
273
|
+
executor: "openai-codex/gpt-5.5:high",
|
|
274
|
+
planner: "anthropic/claude-opus-4-8:medium",
|
|
275
|
+
critic: "anthropic/claude-opus-4-8:high",
|
|
276
|
+
architect: "openai-codex/gpt-5.5:xhigh",
|
|
277
|
+
}),
|
|
264
278
|
];
|
|
265
279
|
|
|
266
280
|
export interface ModelProfilePresentation {
|
|
@@ -278,6 +292,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
278
292
|
"codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
|
|
279
293
|
opencodego: { displayName: "OpenCodeGo", providerGroup: "OPENCODEGO" },
|
|
280
294
|
"claude-opus": { displayName: "Claude Opus", providerGroup: "CLAUDE" },
|
|
295
|
+
"claude-fable": { displayName: "Claude Fable", providerGroup: "CLAUDE" },
|
|
281
296
|
"glm-eco": { displayName: "GLM Eco", providerGroup: "GLM" },
|
|
282
297
|
"glm-medium": { displayName: "GLM Medium", providerGroup: "GLM" },
|
|
283
298
|
"glm-pro": { displayName: "GLM Pro", providerGroup: "GLM" },
|
|
@@ -299,6 +314,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
299
314
|
"minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
|
|
300
315
|
"opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
|
|
301
316
|
"codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
|
|
317
|
+
"fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
|
|
302
318
|
};
|
|
303
319
|
|
|
304
320
|
const PROFILE_GROUP_ORDER = [
|
|
@@ -1273,9 +1273,15 @@ async function sendTmuxPromptKeys(
|
|
|
1273
1273
|
prompt: string,
|
|
1274
1274
|
runner: CommandRunner = runCommand,
|
|
1275
1275
|
): Promise<boolean> {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1276
|
+
const bufferName = `gjc-coordinator-prompt-${randomUUID()}`;
|
|
1277
|
+
const buffered = await runner(["tmux", "set-buffer", "-b", bufferName, "--", prompt]);
|
|
1278
|
+
if (buffered.exitCode !== 0) return false;
|
|
1279
|
+
const pasted = await runner(["tmux", "paste-buffer", "-d", "-b", bufferName, "-t", target]);
|
|
1280
|
+
if (pasted.exitCode !== 0) {
|
|
1281
|
+
await runner(["tmux", "delete-buffer", "-b", bufferName]);
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
const submitted = await runner(["tmux", "send-keys", "-t", target, "Enter"]);
|
|
1279
1285
|
return submitted.exitCode === 0;
|
|
1280
1286
|
}
|
|
1281
1287
|
|
|
@@ -45,13 +45,15 @@ Planning artifacts and stage handoffs MUST be persisted through the ralplan CLI
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
gjc ralplan --write --stage <type> --stage_n <N> --artifact "markdown file path or markdown string"
|
|
48
|
+
# restricted role agents use:
|
|
49
|
+
gjc ralplan --write --stage <type> --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
Use stage values that match the producer or artifact kind, such as `planner`, `architect`, `critic`, `revision`, `post-interview`, `adr`, or `final`. Increment `--stage_n` for each consensus-loop pass. The `--artifact` value may be either a markdown file path prepared outside `.gjc/` for ingestion or the markdown content string itself. The native `--write` handler persists markdown under `.gjc/_session-{sessionid}/plans/ralplan/<run-id>/stage-<NN>-<stage>.md`, maintains an `index.jsonl` audit log, and for `final` stages additionally writes a `pending-approval.md` copy. Direct `write`, `edit`, or `ast_edit` calls against `.gjc/_session-{sessionid}/specs`, `.gjc/_session-{sessionid}/plans`, `.gjc/_session-{sessionid}/state`, or any other `.gjc/` path are forbidden unless an explicit force override is active.
|
|
52
|
+
Use stage values that match the producer or artifact kind, such as `planner`, `architect`, `critic`, `revision`, `post-interview`, `adr`, or `final`. Increment `--stage_n` for each consensus-loop pass. The `--artifact` value may be either a markdown file path prepared outside `.gjc/` for ingestion or the markdown content string itself. The native `--write` handler also accepts `--artifact-env GJC_RALPLAN_ARTIFACT` to read markdown from that per-command env override. It persists markdown under `.gjc/_session-{sessionid}/plans/ralplan/<run-id>/stage-<NN>-<stage>.md`, maintains an `index.jsonl` audit log, and for `final` stages additionally writes a `pending-approval.md` copy. Direct `write`, `edit`, or `ast_edit` calls against `.gjc/_session-{sessionid}/specs`, `.gjc/_session-{sessionid}/plans`, `.gjc/_session-{sessionid}/state`, or any other `.gjc/` path are forbidden unless an explicit force override is active.
|
|
51
53
|
|
|
52
|
-
While ralplan is active it is a pre-approval planning phase: product-code mutation tools (`write`/`edit`/`ast_edit`) and product-mutating `bash` (e.g. `tee src/...`, redirects into the project tree) are blocked, exactly like deep-interview.
|
|
54
|
+
While ralplan is active it is a pre-approval planning phase: product-code mutation tools (`write`/`edit`/`ast_edit`) and product-mutating `bash` (e.g. `tee src/...`, redirects into the project tree) are blocked, exactly like deep-interview. Leaders may pass `--artifact` markdown inline or, when an artifact is too large to pass inline, stage it as a file in a system temp directory (`os.tmpdir()`/`$TMPDIR`, `/tmp`, `/var/tmp`) outside the project tree and pass that path — never write scratch files into the repo or `.gjc/`. Product code is mutated only after the plan is approved and execution begins.
|
|
53
55
|
|
|
54
|
-
Restricted read-only role agents (`planner`, `architect`, and `critic`) must pass markdown content
|
|
56
|
+
Restricted read-only role agents (`planner`, `architect`, and `critic`) must pass markdown content through the `GJC_RALPLAN_ARTIFACT` env override with `--artifact-env GJC_RALPLAN_ARTIFACT`; their restricted bash environment intentionally disables artifact file-path ingestion so a verdict command cannot persist arbitrary file contents.
|
|
55
57
|
|
|
56
58
|
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.
|
|
57
59
|
|
|
@@ -60,7 +62,7 @@ RECEIPT-ONLY guideline: role agents (`planner`, `architect`, and `critic`) persi
|
|
|
60
62
|
This skill runs GJC planning in consensus mode for the provided arguments.
|
|
61
63
|
|
|
62
64
|
The consensus workflow:
|
|
63
|
-
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
|
|
65
|
+
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-env GJC_RALPLAN_ARTIFACT --planner-id <id> --planner-resumable <true|false>` (see **Persisted Planner** below):
|
|
64
66
|
- 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.
|
|
65
67
|
- Principles (3-5)
|
|
66
68
|
- Decision Drivers (top 3)
|
|
@@ -69,26 +71,26 @@ The consensus workflow:
|
|
|
69
71
|
- Deliberate mode only: pre-mortem (3 scenarios) + expanded test plan (unit/integration/e2e/observability)
|
|
70
72
|
2. **User feedback** *(--interactive only)*: If `--interactive` is set, use the `ask` tool to present the draft plan **plus the Principles / Drivers / Options summary** before review (Proceed to review / Request changes / Skip review). Otherwise, automatically proceed to review.
|
|
71
73
|
3. **Architect** reviews for architectural soundness and must provide the strongest steelman antithesis, at least one real tradeoff tension, and (when possible) synthesis — **await completion before step 4**. In deliberate mode, Architect should explicitly flag principle violations.
|
|
72
|
-
- The Architect agent/subagent must persist its review with `gjc ralplan --write --stage architect --stage_n <N> --artifact
|
|
74
|
+
- The Architect agent/subagent must persist its review with `gjc ralplan --write --stage architect --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus compact verdict/status (`CLEAR`/`WATCH`/`BLOCK`, `APPROVE`/`COMMENT`/`REQUEST CHANGES`) instead of pasting the full review body.
|
|
73
75
|
4. **Critic** evaluates against quality criteria — run only after step 3 completes. Critic must enforce principle-option consistency, fair alternatives, risk mitigation clarity, testable acceptance criteria, and concrete verification steps. In deliberate mode, Critic must reject missing/weak pre-mortem or expanded test plan.
|
|
74
|
-
- The Critic agent/subagent must persist its evaluation with `gjc ralplan --write --stage critic --stage_n <N> --artifact
|
|
75
|
-
5. **Re-review loop** (max 5 iterations): Any non-`
|
|
76
|
+
- The Critic agent/subagent must persist its evaluation with `gjc ralplan --write --stage critic --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus compact verdict/status (`OKAY`/`ITERATE`/`REJECT`) instead of pasting the full evaluation body.
|
|
77
|
+
5. **Re-review loop** (max 5 iterations): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) MUST run the same full closed loop:
|
|
76
78
|
a. Collect Architect + Critic feedback
|
|
77
79
|
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
|
|
78
80
|
c. Return to Architect review
|
|
79
|
-
- Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact
|
|
81
|
+
- Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json` before re-review, then pass the receipt/path forward instead of duplicating the full revision markdown in the parent conversation.
|
|
80
82
|
d. Return to Critic evaluation
|
|
81
|
-
e. Repeat this loop until Critic returns `
|
|
82
|
-
f. If 5 iterations are reached without `
|
|
83
|
-
6. **Post-ralplan interview** (intent reconciliation gate): After Critic returns `
|
|
83
|
+
e. Repeat this loop until Critic returns `OKAY` or 5 iterations are reached
|
|
84
|
+
f. If 5 iterations are reached without `OKAY`, present the best version to the user
|
|
85
|
+
6. **Post-ralplan interview** (intent reconciliation gate): After Critic returns `OKAY` and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
|
|
84
86
|
a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
|
|
85
87
|
b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
|
|
86
88
|
c. **Reconcile with the user via the `ask` tool (always, regardless of `--interactive`)**: Never stop idle with plain-text prose after the consensus loop. Every reconciliation question MUST go through the `ask` tool with contextual options plus free-text.
|
|
87
89
|
- If open items exist, confirm the open assumptions and conflicts **one at a time** with the `ask` tool, weakest/highest-impact first, polishing intent. If any confirmation reveals that the plan diverges from user intent, route the consolidated correction back into the re-review loop (step 5b Planner revision) and re-run Architect + Critic before returning here. Cap at the same 5-iteration ceiling.
|
|
88
90
|
- If the plan is crystal clear (no open assumptions or prior-context conflicts), skip straight to the step 8 final-options `ask` instead of inventing filler questions.
|
|
89
91
|
- For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
|
|
90
|
-
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact
|
|
91
|
-
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact
|
|
92
|
+
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
|
|
93
|
+
7. On reconciliation completion, mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
92
94
|
8. **Always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
|
|
93
95
|
- **Refine further** — re-run the consensus loop / request changes, then return here
|
|
94
96
|
- **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
|
|
@@ -128,7 +130,7 @@ The Planner is a **same-session persisted subagent**: launched detached once, aw
|
|
|
128
130
|
**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:
|
|
129
131
|
|
|
130
132
|
```
|
|
131
|
-
gjc ralplan --write --stage revision --stage_n <N> --artifact
|
|
133
|
+
gjc ralplan --write --stage revision --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT \
|
|
132
134
|
--planner-id <id> --planner-resumable <true|false> \
|
|
133
135
|
--fallback-reason <context_unavailable|not_found|no_runner|resume_failed|process_restart|missing_record> \
|
|
134
136
|
--fallback-attempted-id <id> --fallback-stage-n <N> \
|
|
@@ -44,6 +44,13 @@ export type DefaultGjcDefinition = DefaultGjcSkillDefinition | DefaultGjcSkillFr
|
|
|
44
44
|
export interface InstallDefaultGjcDefinitionsOptions {
|
|
45
45
|
check?: boolean;
|
|
46
46
|
force?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Only rewrite default definition files that already exist on disk but whose
|
|
49
|
+
* content differs from the embedded defaults. Files that are absent are left
|
|
50
|
+
* absent (status "missing"). Used by `gjc update` to refresh opted-in copies
|
|
51
|
+
* without materializing new on-disk copies for users who never installed them.
|
|
52
|
+
*/
|
|
53
|
+
refreshOnly?: boolean;
|
|
47
54
|
targetRoot?: string;
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -160,6 +167,15 @@ export async function installDefaultGjcDefinitions(
|
|
|
160
167
|
|
|
161
168
|
if (options.check) {
|
|
162
169
|
status = existing === undefined ? "missing" : existing === definition.content ? "matching" : "different";
|
|
170
|
+
} else if (options.refreshOnly) {
|
|
171
|
+
if (existing === undefined) {
|
|
172
|
+
status = "missing";
|
|
173
|
+
} else if (existing === definition.content) {
|
|
174
|
+
status = "matching";
|
|
175
|
+
} else {
|
|
176
|
+
await Bun.write(destination, definition.content);
|
|
177
|
+
status = "written";
|
|
178
|
+
}
|
|
163
179
|
} else if (existing !== undefined && !options.force) {
|
|
164
180
|
status = "skipped";
|
|
165
181
|
} else {
|
|
@@ -37,6 +37,7 @@ export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
|
37
37
|
export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
38
38
|
export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
39
39
|
export const GJC_PSMUX_PROFILE_FORCE_ENV = "GJC_PSMUX_PROFILE_FORCE";
|
|
40
|
+
const WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS = 100;
|
|
40
41
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
41
42
|
|
|
42
43
|
type LaunchPolicy = "direct" | "tmux";
|
|
@@ -59,6 +60,7 @@ export interface TmuxLaunchContext {
|
|
|
59
60
|
tty?: TtyState;
|
|
60
61
|
spawnSync?: TmuxSpawnSync;
|
|
61
62
|
tmuxAvailable?: boolean;
|
|
63
|
+
tmuxStatusLines?: number;
|
|
62
64
|
worktreeBranch?: string | null;
|
|
63
65
|
currentBranch?: string | null;
|
|
64
66
|
existingBranchSessionName?: string | null;
|
|
@@ -107,6 +109,8 @@ export interface TmuxLaunchPlan {
|
|
|
107
109
|
project?: string | null;
|
|
108
110
|
sessionId?: string | null;
|
|
109
111
|
sessionStateFile?: string | null;
|
|
112
|
+
isPsmux: boolean;
|
|
113
|
+
platform: NodeJS.Platform;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
@@ -117,7 +121,9 @@ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolea
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
|
|
120
|
-
|
|
124
|
+
// `--resume` belongs to the inner GJC session resolver. Let it reach main.ts so
|
|
125
|
+
// value-less resume can show the session picker and valued resume can honor the target.
|
|
126
|
+
return Boolean(parsed.continue || explicitTmuxSessionName(env));
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function findExistingSessionForLaunch(context: {
|
|
@@ -544,42 +550,88 @@ function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
|
544
550
|
const stderr = result.stderr?.toLowerCase() ?? "";
|
|
545
551
|
return stderr.includes("eio") || stderr.includes("input/output error");
|
|
546
552
|
}
|
|
553
|
+
function isWindowsPsmuxAttachConnectionRefused(plan: TmuxLaunchPlan, result: TmuxSpawnResult): boolean {
|
|
554
|
+
if (plan.platform !== "win32" || !plan.isPsmux) return false;
|
|
555
|
+
return result.stderr?.toLowerCase().includes("os error 10061") === true;
|
|
556
|
+
}
|
|
557
|
+
function waitForWindowsPsmuxAttachRetry(): void {
|
|
558
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS);
|
|
559
|
+
}
|
|
547
560
|
function normalizeTmuxTerminalDimension(value: number | undefined): number | undefined {
|
|
548
561
|
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return undefined;
|
|
549
562
|
return value;
|
|
550
563
|
}
|
|
551
564
|
|
|
552
|
-
function
|
|
565
|
+
function normalizeTmuxStatusLineCount(value: number | undefined): number {
|
|
566
|
+
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return 0;
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function parseTmuxStatusLineCount(value: string): number {
|
|
571
|
+
const normalized = value.trim().toLowerCase();
|
|
572
|
+
if (normalized.length === 0 || normalized === "off" || normalized === "0") return 0;
|
|
573
|
+
if (normalized === "on") return 1;
|
|
574
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
575
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function readTmuxStatusLineCount(tmuxCommand: string, cwd: string, env: NodeJS.ProcessEnv): number {
|
|
579
|
+
const result = Bun.spawnSync([tmuxCommand, "show-options", "-gqv", "status"], {
|
|
580
|
+
cwd,
|
|
581
|
+
env,
|
|
582
|
+
stdin: "pipe",
|
|
583
|
+
stdout: "pipe",
|
|
584
|
+
stderr: "pipe",
|
|
585
|
+
});
|
|
586
|
+
if (result.exitCode !== 0) return 0;
|
|
587
|
+
return parseTmuxStatusLineCount(new TextDecoder().decode(result.stdout));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function resolveCallerTmuxTerminalSize(tty: TtyState, tmuxStatusLines = 0): TmuxTerminalSize | undefined {
|
|
553
591
|
if (!tty.stdout) return undefined;
|
|
554
592
|
const columns = normalizeTmuxTerminalDimension(tty.columns);
|
|
555
593
|
const rows = normalizeTmuxTerminalDimension(tty.rows);
|
|
556
594
|
if (columns === undefined || rows === undefined) return undefined;
|
|
557
|
-
|
|
595
|
+
const adjustedRows = Math.max(1, rows - normalizeTmuxStatusLineCount(tmuxStatusLines));
|
|
596
|
+
return { columns, rows: adjustedRows };
|
|
558
597
|
}
|
|
559
598
|
|
|
560
599
|
function buildTmuxNewSessionSizeArgs(size: TmuxTerminalSize | undefined): string[] {
|
|
561
600
|
return size ? ["-x", String(size.columns), "-y", String(size.rows)] : [];
|
|
562
601
|
}
|
|
563
602
|
|
|
564
|
-
|
|
603
|
+
// Ensure the freshly created window fits the terminal that ultimately attaches.
|
|
604
|
+
// `new-session` already starts the window (and the inner TUI) at the caller's
|
|
605
|
+
// captured `-x/-y` size; this step governs what happens on `attach-session`.
|
|
606
|
+
//
|
|
607
|
+
// On native tmux we must NOT reassert with `resize-window`: that command flips
|
|
608
|
+
// the window's `window-size` option to `manual`, pinning it to the capture-time
|
|
609
|
+
// dimensions and stopping `attach-session` from resizing the window to the real
|
|
610
|
+
// client. When the attaching terminal is larger than the capture — e.g. a GUI
|
|
611
|
+
// terminal that reports a smaller size before it finishes sizing — the pinned
|
|
612
|
+
// window stays small and tmux paints the uncovered client area with `·` fill
|
|
613
|
+
// (the "window smaller than client" symptom). Keeping `window-size` on `latest`
|
|
614
|
+
// lets tmux size the window to the attaching client (status line included).
|
|
615
|
+
//
|
|
616
|
+
// psmux (Windows) does not share tmux's `window-size` semantics, so preserve the
|
|
617
|
+
// historical explicit `resize-window` reassert there rather than sending an
|
|
618
|
+
// option its server may reject and echo into the user's pane.
|
|
619
|
+
function ensureCreatedTmuxWindowTracksCallerTerminal(
|
|
565
620
|
plan: TmuxLaunchPlan,
|
|
566
621
|
spawnSync: TmuxSpawnSync,
|
|
567
622
|
options: TmuxSpawnOptions,
|
|
568
623
|
): void {
|
|
569
624
|
if (!plan.initialSize) return;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"-t",
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
],
|
|
581
|
-
options,
|
|
582
|
-
);
|
|
625
|
+
const target = buildGjcTmuxExactOptionTarget(plan.sessionName, { env: options.env });
|
|
626
|
+
if (plan.isPsmux) {
|
|
627
|
+
spawnSync(
|
|
628
|
+
plan.tmuxCommand,
|
|
629
|
+
["resize-window", "-t", target, "-x", String(plan.initialSize.columns), "-y", String(plan.initialSize.rows)],
|
|
630
|
+
options,
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
spawnSync(plan.tmuxCommand, ["set-window-option", "-t", target, "window-size", "latest"], options);
|
|
583
635
|
}
|
|
584
636
|
|
|
585
637
|
export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
|
|
@@ -641,9 +693,14 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
641
693
|
},
|
|
642
694
|
context.rawArgs,
|
|
643
695
|
);
|
|
644
|
-
const
|
|
696
|
+
const tmuxStatusLines =
|
|
697
|
+
context.tmuxStatusLines ??
|
|
698
|
+
(context.tmuxAvailable === undefined ? readTmuxStatusLineCount(tmuxCommand, cwd, env) : 0);
|
|
699
|
+
const initialSize = resolveCallerTmuxTerminalSize(tty, tmuxStatusLines);
|
|
645
700
|
return {
|
|
646
701
|
tmuxCommand,
|
|
702
|
+
isPsmux: resolvedBinary.isPsmux,
|
|
703
|
+
platform,
|
|
647
704
|
sessionName,
|
|
648
705
|
cwd,
|
|
649
706
|
innerCommand,
|
|
@@ -742,6 +799,30 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
742
799
|
const rootTerminalTitle = shouldSetGjcTmuxRootTerminalTitle(context.parsed, env)
|
|
743
800
|
? buildGjcTmuxRootTerminalTitle(plan.project ?? plan.cwd, plan.branch)
|
|
744
801
|
: undefined;
|
|
802
|
+
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
803
|
+
tmuxCommand: plan.tmuxCommand,
|
|
804
|
+
target: plan.sessionName,
|
|
805
|
+
cwd: plan.cwd,
|
|
806
|
+
env,
|
|
807
|
+
spawnSync,
|
|
808
|
+
branch: plan.branch,
|
|
809
|
+
project: plan.project,
|
|
810
|
+
sessionId: plan.sessionId ?? null,
|
|
811
|
+
sessionStateFile: plan.sessionStateFile ?? null,
|
|
812
|
+
version: VERSION,
|
|
813
|
+
});
|
|
814
|
+
const probeHasSession = (): TmuxSpawnResult =>
|
|
815
|
+
spawnSync(
|
|
816
|
+
plan.tmuxCommand,
|
|
817
|
+
["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
818
|
+
probeOptions,
|
|
819
|
+
);
|
|
820
|
+
const attachCreatedSession = (): TmuxSpawnResult =>
|
|
821
|
+
spawnSync(
|
|
822
|
+
plan.tmuxCommand,
|
|
823
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
824
|
+
attachOptions,
|
|
825
|
+
);
|
|
745
826
|
|
|
746
827
|
if (plan.attachSessionName) {
|
|
747
828
|
applyGjcTmuxRootTerminalTitleProfile({
|
|
@@ -767,24 +848,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
767
848
|
// server first, and if the race fired, retry new-session before
|
|
768
849
|
// we hand the session to rename-window / applyGjcTmuxProfile so the
|
|
769
850
|
// profile-tagging path always sees a registered session.
|
|
770
|
-
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
771
|
-
tmuxCommand: plan.tmuxCommand,
|
|
772
|
-
target: plan.sessionName,
|
|
773
|
-
cwd: plan.cwd,
|
|
774
|
-
env,
|
|
775
|
-
spawnSync,
|
|
776
|
-
branch: plan.branch,
|
|
777
|
-
project: plan.project,
|
|
778
|
-
sessionId: plan.sessionId ?? null,
|
|
779
|
-
sessionStateFile: plan.sessionStateFile ?? null,
|
|
780
|
-
version: VERSION,
|
|
781
|
-
});
|
|
782
|
-
const probeHasSession = (): TmuxSpawnResult =>
|
|
783
|
-
spawnSync(
|
|
784
|
-
plan.tmuxCommand,
|
|
785
|
-
["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
786
|
-
probeOptions,
|
|
787
|
-
);
|
|
788
851
|
const probeResult = probeHasSession();
|
|
789
852
|
if (probeResult.exitCode !== 0) {
|
|
790
853
|
const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
|
|
@@ -851,7 +914,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
851
914
|
}
|
|
852
915
|
// Recovery succeeded via retry — fall through to attach-session below.
|
|
853
916
|
}
|
|
854
|
-
|
|
917
|
+
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
855
918
|
applyGjcTmuxRootTerminalTitleProfile({
|
|
856
919
|
tmuxCommand: plan.tmuxCommand,
|
|
857
920
|
target: plan.sessionName,
|
|
@@ -875,16 +938,75 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
875
938
|
return false;
|
|
876
939
|
}
|
|
877
940
|
// attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
|
|
878
|
-
const attached =
|
|
879
|
-
plan.tmuxCommand,
|
|
880
|
-
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
881
|
-
attachOptions,
|
|
882
|
-
);
|
|
941
|
+
const attached = attachCreatedSession();
|
|
883
942
|
if (attached.exitCode === 0) return true;
|
|
884
943
|
if (isTmuxAttachDisconnectError(attached)) {
|
|
885
944
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
886
945
|
return true;
|
|
887
946
|
}
|
|
947
|
+
if (isWindowsPsmuxAttachConnectionRefused(plan, attached)) {
|
|
948
|
+
waitForWindowsPsmuxAttachRetry();
|
|
949
|
+
const probeAfterAttach = probeHasSession();
|
|
950
|
+
if (probeAfterAttach.exitCode === 0) {
|
|
951
|
+
const retryAttached = attachCreatedSession();
|
|
952
|
+
if (retryAttached.exitCode === 0) return true;
|
|
953
|
+
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
954
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
955
|
+
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
956
|
+
);
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
|
|
961
|
+
const retryProbe = probeHasSession();
|
|
962
|
+
if (retry.exitCode === 0 && retryProbe.exitCode === 0) {
|
|
963
|
+
renameTmuxWindow(
|
|
964
|
+
plan.tmuxCommand,
|
|
965
|
+
windowTitle,
|
|
966
|
+
spawnSync,
|
|
967
|
+
controlOptions,
|
|
968
|
+
buildGjcTmuxExactSessionTarget(plan.sessionName, { env }),
|
|
969
|
+
);
|
|
970
|
+
const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
|
|
971
|
+
const retryOwnershipFailure = retryProfile.failures.find(item =>
|
|
972
|
+
item.command.args.includes("@gjc-profile"),
|
|
973
|
+
);
|
|
974
|
+
if (!retryOwnershipFailure) {
|
|
975
|
+
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
976
|
+
applyGjcTmuxRootTerminalTitleProfile({
|
|
977
|
+
tmuxCommand: plan.tmuxCommand,
|
|
978
|
+
target: plan.sessionName,
|
|
979
|
+
title: rootTerminalTitle,
|
|
980
|
+
spawnSync,
|
|
981
|
+
options,
|
|
982
|
+
});
|
|
983
|
+
const retryAttached = attachCreatedSession();
|
|
984
|
+
if (retryAttached.exitCode === 0) return true;
|
|
985
|
+
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
986
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
987
|
+
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
988
|
+
);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Closing an SSH/Windows Terminal tab can make `tmux attach-session`
|
|
996
|
+
// exit with code 1 and no captured stderr while the tmux server correctly
|
|
997
|
+
// keeps the just-created session alive. Preserve that live session so the
|
|
998
|
+
// user can reattach instead of treating the parent client teardown as a
|
|
999
|
+
// launch failure.
|
|
1000
|
+
const attachFailureStderr = attached.stderr?.trim() ?? "";
|
|
1001
|
+
if (attachFailureStderr.length === 0) {
|
|
1002
|
+
const probeAfterAttachFailure = probeHasSession();
|
|
1003
|
+
if (probeAfterAttachFailure.exitCode === 0) {
|
|
1004
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1005
|
+
formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
|
|
1006
|
+
);
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
888
1010
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
889
1011
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
890
1012
|
return true;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type RalplanIndexRow,
|
|
12
12
|
summarizeRalplanIndex,
|
|
13
13
|
} from "./ledger-event-renderer";
|
|
14
|
-
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
14
|
+
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
15
15
|
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
16
16
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
17
17
|
import { migrateWorkflowState } from "./state-migrations";
|
|
@@ -35,8 +35,9 @@ import {
|
|
|
35
35
|
* that lives in the bundled `/skill:ralplan` skill — but it accepts every documented flag so
|
|
36
36
|
* scripted users see a useful response and the active run is visible to the TUI.
|
|
37
37
|
*
|
|
38
|
-
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
39
|
-
* <path-or-string>
|
|
38
|
+
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
39
|
+
* (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
|
|
40
|
+
* [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
40
41
|
* / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
41
42
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
42
43
|
* the HUD chip to reflect the latest persisted stage.
|
|
@@ -81,6 +82,7 @@ const VALUE_FLAGS = new Set([
|
|
|
81
82
|
"--stage",
|
|
82
83
|
"--stage_n",
|
|
83
84
|
"--artifact",
|
|
85
|
+
"--artifact-env",
|
|
84
86
|
"--run-id",
|
|
85
87
|
"--session-id",
|
|
86
88
|
"--architect",
|
|
@@ -409,8 +411,16 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
409
411
|
const stageN = parseStageN(flagValue(args, "--stage_n"));
|
|
410
412
|
|
|
411
413
|
const rawArtifact = flagValue(args, "--artifact");
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
const artifactEnvName = flagValue(args, "--artifact-env");
|
|
415
|
+
const artifactSources = [rawArtifact, artifactEnvName].filter(value => value !== undefined);
|
|
416
|
+
if (artifactSources.length === 0 || artifactSources.some(value => value === "")) {
|
|
417
|
+
throw new RalplanCommandError(2, "--artifact or --artifact-env is required for ralplan --write");
|
|
418
|
+
}
|
|
419
|
+
if (artifactSources.length > 1) {
|
|
420
|
+
throw new RalplanCommandError(2, "--artifact and --artifact-env are mutually exclusive");
|
|
421
|
+
}
|
|
422
|
+
if (artifactEnvName !== undefined && artifactEnvName !== GJC_RALPLAN_ARTIFACT_ENV) {
|
|
423
|
+
throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
const session = resolveGjcSessionForWrite(cwd, {
|
|
@@ -430,7 +440,13 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
430
440
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
431
441
|
assertSafePathComponent(runId, "run-id");
|
|
432
442
|
|
|
433
|
-
const artifact =
|
|
443
|
+
const artifact =
|
|
444
|
+
artifactEnvName !== undefined
|
|
445
|
+
? (process.env[GJC_RALPLAN_ARTIFACT_ENV] ?? "")
|
|
446
|
+
: await resolveArtifactContent(rawArtifact!, cwd);
|
|
447
|
+
if (artifact === "") {
|
|
448
|
+
throw new RalplanCommandError(2, "artifact content is empty");
|
|
449
|
+
}
|
|
434
450
|
return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
|
|
435
451
|
}
|
|
436
452
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const GJC_RESTRICTED_ROLE_AGENT_BASH_ENV = "GJC_RESTRICTED_ROLE_AGENT_BASH";
|
|
2
|
+
export const GJC_RALPLAN_ARTIFACT_ENV = "GJC_RALPLAN_ARTIFACT";
|
|
2
3
|
|
|
3
4
|
export function isRestrictedRoleAgentBash(): boolean {
|
|
4
5
|
return process.env[GJC_RESTRICTED_ROLE_AGENT_BASH_ENV] === "1";
|
|
@@ -847,11 +847,14 @@ async function writeJsonAtomic(
|
|
|
847
847
|
forced: options?.force ?? false,
|
|
848
848
|
},
|
|
849
849
|
});
|
|
850
|
-
// `writeResult.revision`
|
|
851
|
-
// write actually owns.
|
|
852
|
-
// could
|
|
853
|
-
//
|
|
854
|
-
|
|
850
|
+
// `writeResult.stamped` and `.revision` are computed inside the writer lock, so they are
|
|
851
|
+
// the envelope/revision this write actually owns. Never post-lock re-read here: a concurrent
|
|
852
|
+
// writer could advance the file before that read and make this payload publish another
|
|
853
|
+
// writer's newer revision into the derived active-state cache.
|
|
854
|
+
if (!writeResult.written || !isPlainObject(writeResult.stamped)) {
|
|
855
|
+
throw new Error(`state writer did not return a stamped workflow envelope for ${filePath}`);
|
|
856
|
+
}
|
|
857
|
+
return { warning, stamped: writeResult.stamped, revision: writeResult.revision };
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
|