@gajae-code/coding-agent 0.4.1 → 0.4.3
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 +22 -0
- package/dist/types/async/job-manager.d.ts +25 -0
- package/dist/types/commands/ultragoal.d.ts +1 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +26 -4
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/finalize.d.ts +8 -0
- package/dist/types/harness-control-plane/receipts.d.ts +16 -1
- package/dist/types/harness-control-plane/types.d.ts +16 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/reminders/star-reminder.d.ts +115 -0
- package/dist/types/session/agent-session.d.ts +30 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/examples/extensions/README.md +20 -41
- package/package.json +7 -7
- package/src/async/job-manager.ts +120 -1
- package/src/cli/grep-cli.ts +1 -1
- package/src/commands/harness.ts +42 -3
- package/src/commands/ultragoal.ts +8 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +27 -4
- package/src/defaults/gjc/skills/team/SKILL.md +10 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -2
- package/src/gjc-runtime/team-runtime.ts +78 -3
- package/src/gjc-runtime/ultragoal-guard.ts +18 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
- package/src/harness-control-plane/finalize.ts +84 -0
- package/src/harness-control-plane/owner.ts +16 -3
- package/src/harness-control-plane/receipts.ts +39 -1
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +33 -12
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/reminders/star-reminder.ts +422 -0
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +139 -17
- package/src/session/session-manager.ts +1 -1
- package/src/task/agents.ts +1 -1
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { fuzzyMatch } from "@gajae-code/tui";
|
|
16
16
|
import { logger } from "@gajae-code/utils";
|
|
17
17
|
import chalk from "chalk";
|
|
18
|
-
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
19
18
|
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
|
20
19
|
import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
21
20
|
import type { Settings } from "./settings";
|
|
@@ -246,6 +245,15 @@ function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceCo
|
|
|
246
245
|
return (aProviderUsage ?? Number.POSITIVE_INFINITY) - (bProviderUsage ?? Number.POSITIVE_INFINITY);
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
// Prefer vision-capable variants over configured provider/registration order
|
|
249
|
+
// so an ambiguous id never resolves to a text-only namesake when a
|
|
250
|
+
// vision-capable variant of the same id is available.
|
|
251
|
+
const aVision = a.input.includes("image") ? 0 : 1;
|
|
252
|
+
const bVision = b.input.includes("image") ? 0 : 1;
|
|
253
|
+
if (aVision !== bVision) {
|
|
254
|
+
return aVision - bVision;
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
const aDeprioritized = context.deprioritizedProviders.has(a.provider);
|
|
250
258
|
const bDeprioritized = context.deprioritizedProviders.has(b.provider);
|
|
251
259
|
if (aDeprioritized !== bDeprioritized) {
|
|
@@ -520,7 +528,7 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
|
|
|
520
528
|
}
|
|
521
529
|
|
|
522
530
|
function isSessionInheritedAgentPattern(value: string): boolean {
|
|
523
|
-
return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}
|
|
531
|
+
return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}`;
|
|
524
532
|
}
|
|
525
533
|
|
|
526
534
|
function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
|
|
@@ -535,8 +543,7 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
|
|
|
535
543
|
if (!role) return [normalized];
|
|
536
544
|
|
|
537
545
|
const configured = settings?.getModelRole(role)?.trim();
|
|
538
|
-
const
|
|
539
|
-
const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
|
|
546
|
+
const resolved = configured ? normalizeModelPatternList(configured) : undefined;
|
|
540
547
|
if (!resolved || resolved.length === 0) {
|
|
541
548
|
return undefined;
|
|
542
549
|
}
|
|
@@ -545,7 +552,7 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
|
|
|
545
552
|
}
|
|
546
553
|
|
|
547
554
|
/**
|
|
548
|
-
* Expand a role alias like "pi/
|
|
555
|
+
* Expand a role alias like "pi/default" to the configured model string.
|
|
549
556
|
*/
|
|
550
557
|
export function expandRoleAlias(value: string, settings?: Settings): string {
|
|
551
558
|
const normalized = value.trim();
|
|
@@ -582,9 +589,8 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
|
|
|
582
589
|
const configuredAgentPatterns = resolveConfiguredModelPatterns(agentModel, settings);
|
|
583
590
|
const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
|
|
584
591
|
const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
|
|
585
|
-
if (configuredAgentPatterns.length > 0) {
|
|
586
|
-
|
|
587
|
-
if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
|
|
592
|
+
if (configuredAgentPatterns.length > 0 && !agentInheritsSessionModel) {
|
|
593
|
+
return configuredAgentPatterns;
|
|
588
594
|
}
|
|
589
595
|
|
|
590
596
|
const fallback =
|
|
@@ -1325,79 +1331,3 @@ export async function restoreModelFromSession(
|
|
|
1325
1331
|
// No models available
|
|
1326
1332
|
return { model: undefined, fallbackMessage: undefined };
|
|
1327
1333
|
}
|
|
1328
|
-
|
|
1329
|
-
/**
|
|
1330
|
-
* Find a smol/fast model using the priority chain.
|
|
1331
|
-
* Tries exact matches first, then fuzzy matches.
|
|
1332
|
-
*
|
|
1333
|
-
* @param modelRegistry The model registry to search
|
|
1334
|
-
* @param savedModel Optional saved model string from settings (provider/modelId)
|
|
1335
|
-
* @returns The best available smol model, or undefined if none found
|
|
1336
|
-
*/
|
|
1337
|
-
export async function findSmolModel(
|
|
1338
|
-
modelRegistry: ModelLookupRegistry,
|
|
1339
|
-
savedModel?: string,
|
|
1340
|
-
): Promise<Model<Api> | undefined> {
|
|
1341
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1342
|
-
if (availableModels.length === 0) return undefined;
|
|
1343
|
-
|
|
1344
|
-
// 1. Try saved model from settings
|
|
1345
|
-
if (savedModel) {
|
|
1346
|
-
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1347
|
-
if (match) return match;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// 2. Try priority chain
|
|
1351
|
-
for (const pattern of MODEL_PRIO.smol) {
|
|
1352
|
-
// Try exact match with provider prefix
|
|
1353
|
-
const providerMatch = availableModels.find(m => `${m.provider}/${m.id}`.toLowerCase() === pattern);
|
|
1354
|
-
if (providerMatch) return providerMatch;
|
|
1355
|
-
|
|
1356
|
-
// Try exact match first
|
|
1357
|
-
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1358
|
-
if (exactMatch) return exactMatch;
|
|
1359
|
-
|
|
1360
|
-
// Try fuzzy match (substring)
|
|
1361
|
-
const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern));
|
|
1362
|
-
if (fuzzyMatch) return fuzzyMatch;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// 3. Fallback to first available (same as default)
|
|
1366
|
-
return availableModels[0];
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
/**
|
|
1370
|
-
* Find a slow/comprehensive model using the priority chain.
|
|
1371
|
-
* Prioritizes reasoning and OpenAI code backend models for thorough analysis.
|
|
1372
|
-
*
|
|
1373
|
-
* @param modelRegistry The model registry to search
|
|
1374
|
-
* @param savedModel Optional saved model string from settings (provider/modelId)
|
|
1375
|
-
* @returns The best available slow model, or undefined if none found
|
|
1376
|
-
*/
|
|
1377
|
-
export async function findSlowModel(
|
|
1378
|
-
modelRegistry: ModelLookupRegistry,
|
|
1379
|
-
savedModel?: string,
|
|
1380
|
-
): Promise<Model<Api> | undefined> {
|
|
1381
|
-
const availableModels = modelRegistry.getAvailable();
|
|
1382
|
-
if (availableModels.length === 0) return undefined;
|
|
1383
|
-
|
|
1384
|
-
// 1. Try saved model from settings
|
|
1385
|
-
if (savedModel) {
|
|
1386
|
-
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1387
|
-
if (match) return match;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// 2. Try priority chain
|
|
1391
|
-
for (const pattern of MODEL_PRIO.slow) {
|
|
1392
|
-
// Try exact match first
|
|
1393
|
-
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1394
|
-
if (exactMatch) return exactMatch;
|
|
1395
|
-
|
|
1396
|
-
// Try fuzzy match (substring)
|
|
1397
|
-
const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
|
|
1398
|
-
if (fuzzyMatch) return fuzzyMatch;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// 3. Fallback to first available (same as default)
|
|
1402
|
-
return availableModels[0];
|
|
1403
|
-
}
|
|
@@ -135,6 +135,7 @@ const ModelDefinitionSchema = z
|
|
|
135
135
|
reasoning: z.boolean().optional(),
|
|
136
136
|
thinking: ModelThinkingSchema.optional(),
|
|
137
137
|
input: z.array(z.enum(["text", "image"])).optional(),
|
|
138
|
+
output: z.array(z.enum(["text", "image"])).optional(),
|
|
138
139
|
cost: z
|
|
139
140
|
.object({
|
|
140
141
|
input: z.number(),
|
|
@@ -161,6 +162,7 @@ export const ModelOverrideSchema = z
|
|
|
161
162
|
reasoning: z.boolean().optional(),
|
|
162
163
|
thinking: ModelThinkingSchema.optional(),
|
|
163
164
|
input: z.array(z.enum(["text", "image"])).optional(),
|
|
165
|
+
output: z.array(z.enum(["text", "image"])).optional(),
|
|
164
166
|
cost: z
|
|
165
167
|
.object({
|
|
166
168
|
input: z.number().optional(),
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from "./skill-settings-defaults";
|
|
10
10
|
|
|
11
11
|
/** Unified settings schema - single source of truth for all settings.
|
|
12
|
-
* Unified settings schema - single source of truth for all settings.
|
|
13
12
|
*
|
|
14
13
|
* Each setting is defined once here with:
|
|
15
14
|
* - Type and default value
|
|
@@ -939,6 +938,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
939
938
|
},
|
|
940
939
|
},
|
|
941
940
|
|
|
941
|
+
busyPromptMode: {
|
|
942
|
+
type: "enum",
|
|
943
|
+
values: ["steer", "queue"] as const,
|
|
944
|
+
default: "steer",
|
|
945
|
+
ui: {
|
|
946
|
+
tab: "interaction",
|
|
947
|
+
label: "Busy Prompt Mode",
|
|
948
|
+
description:
|
|
949
|
+
"What a submitted prompt does while the agent is busy: steer (interrupt the active turn) or queue (run after the active turn completes)",
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
|
|
942
953
|
// Input and startup
|
|
943
954
|
doubleEscapeAction: {
|
|
944
955
|
type: "enum",
|
|
@@ -1010,6 +1021,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1010
1021
|
},
|
|
1011
1022
|
},
|
|
1012
1023
|
|
|
1024
|
+
"starReminder.enabled": {
|
|
1025
|
+
type: "boolean",
|
|
1026
|
+
default: true,
|
|
1027
|
+
ui: {
|
|
1028
|
+
tab: "interaction",
|
|
1029
|
+
label: "GitHub Star Reminder",
|
|
1030
|
+
description: "Show the interactive GitHub star reminder when gh is authenticated",
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
|
|
1013
1034
|
collapseChangelog: {
|
|
1014
1035
|
type: "boolean",
|
|
1015
1036
|
default: false,
|
|
@@ -1089,11 +1110,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
1089
1110
|
// Context promotion
|
|
1090
1111
|
"contextPromotion.enabled": {
|
|
1091
1112
|
type: "boolean",
|
|
1092
|
-
default:
|
|
1113
|
+
default: false,
|
|
1093
1114
|
ui: {
|
|
1094
1115
|
tab: "context",
|
|
1095
1116
|
label: "Auto-Promote Context",
|
|
1096
|
-
description:
|
|
1117
|
+
description:
|
|
1118
|
+
"Promote to a larger-context model on context overflow instead of compacting (off by default; opt in to enable)",
|
|
1097
1119
|
},
|
|
1098
1120
|
},
|
|
1099
1121
|
|
|
@@ -2617,7 +2639,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2617
2639
|
},
|
|
2618
2640
|
"providers.image": {
|
|
2619
2641
|
type: "enum",
|
|
2620
|
-
values: ["auto", "openai", "gemini", "openrouter"] as const,
|
|
2642
|
+
values: ["auto", "openai", "gemini", "openrouter", "antigravity"] as const,
|
|
2621
2643
|
default: "auto",
|
|
2622
2644
|
ui: {
|
|
2623
2645
|
tab: "providers",
|
|
@@ -2632,6 +2654,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2632
2654
|
{ value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
|
|
2633
2655
|
{ value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
|
|
2634
2656
|
{ value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
|
|
2657
|
+
{ value: "antigravity", label: "Antigravity", description: "Requires login with google-antigravity" },
|
|
2635
2658
|
],
|
|
2636
2659
|
},
|
|
2637
2660
|
},
|
|
@@ -124,6 +124,15 @@ When `$team` is used as a follow-up mode from ralplan, carry forward the approve
|
|
|
124
124
|
- explain why each lane exists (delivery, verification, specialist support)
|
|
125
125
|
- include an explicit launch hint (`gjc team "<task>"` / `$team "<task>"`) for the coordinated worker run; mention `$ultragoal` as the default durable follow-up/ledger path; mention a later separate Single-owner execution follow-up only when explicitly requested or genuinely needed as a fallback
|
|
126
126
|
- if the ideal role is unavailable, choose the closest role from the roster and say so
|
|
127
|
+
- For multi-worker follow-up execution, do not pass an inline "Split lanes: A..., B..." sentence as the whole team task. `gjc team` rejects ambiguous inline lane splits because they previously caused every worker to receive the same broad task. Use explicit markdown lane sections instead:
|
|
128
|
+
```md
|
|
129
|
+
### Lane A — Delivery
|
|
130
|
+
Implement delivery-only changes and evidence.
|
|
131
|
+
|
|
132
|
+
### Lane B — Verification
|
|
133
|
+
Add focused tests and smoke evidence.
|
|
134
|
+
```
|
|
135
|
+
Explicit `### Lane <id> — <title>` sections are converted into distinct worker-owned initial tasks.
|
|
127
136
|
|
|
128
137
|
## Current Runtime Behavior (As Implemented)
|
|
129
138
|
|
|
@@ -134,7 +143,7 @@ When `$team` is used as a follow-up mode from ralplan, carry forward the approve
|
|
|
134
143
|
3. Initialize team state:
|
|
135
144
|
- `.gjc/state/team/<team>/config.json`
|
|
136
145
|
- `.gjc/state/team/<team>/manifest.v2.json`
|
|
137
|
-
- `.gjc/state/team/<team>/tasks/task
|
|
146
|
+
- `.gjc/state/team/<team>/tasks/task-*.json` (one per explicit lane section, otherwise one worker-owned compatibility task per worker)
|
|
138
147
|
- `.gjc/state/team/<team>/mailbox/worker-1.json`
|
|
139
148
|
- `.gjc/state/team/<team>/workers/<worker>/status.json`
|
|
140
149
|
- `.gjc/state/team/<team>/workers/<worker>/lifecycle.json`
|
|
@@ -96,11 +96,12 @@ Loop until `gjc ultragoal status` reports all goals complete:
|
|
|
96
96
|
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.
|
|
97
97
|
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:
|
|
98
98
|
`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>`
|
|
99
|
+
A successful complete checkpoint is story completion, not automatic run completion. Read the checkpoint output: when it prints `Next ultragoal goal: <id>`, continue that active story under the same aggregate GJC goal; when it prints `All ultragoal goals are complete`, the durable run is terminal. `gjc ultragoal complete-goals` remains the supported manual next-story command if continuation output was missed.
|
|
99
100
|
9. If blocked or failed, checkpoint failure:
|
|
100
101
|
`gjc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"`
|
|
101
|
-
|
|
102
|
+
10. For legacy per-story completed-goal blockers, preserve the non-terminal blocker with:
|
|
102
103
|
`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>`
|
|
103
|
-
|
|
104
|
+
11. Resume failed goals with `gjc ultragoal complete-goals --retry-failed`.
|
|
104
105
|
|
|
105
106
|
## Dynamic steering
|
|
106
107
|
|
|
@@ -25,6 +25,12 @@ export interface PendingGoalModeRequest {
|
|
|
25
25
|
objective: string;
|
|
26
26
|
createdAt: string;
|
|
27
27
|
goalsPath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Session id that produced this request (from GJC_SESSION_ID). When present,
|
|
30
|
+
* only the originating session may consume it, so concurrent sessions sharing
|
|
31
|
+
* the same `.gjc` project state never auto-run each other's ultragoal.
|
|
32
|
+
*/
|
|
33
|
+
sessionId?: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export type CurrentSessionGoalModeWriteResult =
|
|
@@ -77,9 +83,11 @@ export async function writePendingGoalModeRequest(input: {
|
|
|
77
83
|
cwd: string;
|
|
78
84
|
objective: string;
|
|
79
85
|
goalsPath?: string;
|
|
86
|
+
sessionId?: string | null;
|
|
80
87
|
}): Promise<PendingGoalModeRequest> {
|
|
81
88
|
const objective = input.objective.trim();
|
|
82
89
|
if (!objective) throw new Error("goal objective is required");
|
|
90
|
+
const sessionId = input.sessionId?.trim();
|
|
83
91
|
const request: PendingGoalModeRequest = {
|
|
84
92
|
version: REQUEST_VERSION,
|
|
85
93
|
kind: "goal_mode_request",
|
|
@@ -87,6 +95,7 @@ export async function writePendingGoalModeRequest(input: {
|
|
|
87
95
|
objective,
|
|
88
96
|
createdAt: new Date().toISOString(),
|
|
89
97
|
goalsPath: input.goalsPath,
|
|
98
|
+
...(sessionId ? { sessionId } : {}),
|
|
90
99
|
};
|
|
91
100
|
const filePath = requestPath(input.cwd);
|
|
92
101
|
await writeJsonAtomic(filePath, request, {
|
|
@@ -162,7 +171,10 @@ export async function writeCurrentSessionGoalModeState(input: {
|
|
|
162
171
|
return { status: "updated", goal: state.goal, sessionFile };
|
|
163
172
|
}
|
|
164
173
|
|
|
165
|
-
export async function consumePendingGoalModeRequest(
|
|
174
|
+
export async function consumePendingGoalModeRequest(
|
|
175
|
+
cwd: string,
|
|
176
|
+
currentSessionId?: string | null,
|
|
177
|
+
): Promise<PendingGoalModeRequest | null> {
|
|
166
178
|
const filePath = requestPath(cwd);
|
|
167
179
|
let raw: unknown;
|
|
168
180
|
try {
|
|
@@ -181,6 +193,14 @@ export async function consumePendingGoalModeRequest(cwd: string): Promise<Pendin
|
|
|
181
193
|
) {
|
|
182
194
|
return null;
|
|
183
195
|
}
|
|
196
|
+
// Session isolation: a request stamped with an owning session id may only be
|
|
197
|
+
// consumed by that same session. Leave another session's request untouched
|
|
198
|
+
// (do not delete it) so its rightful owner can still pick it up. Legacy/unscoped
|
|
199
|
+
// requests (no sessionId) remain consumable by any session in this cwd.
|
|
200
|
+
const ownerSessionId = typeof candidate.sessionId === "string" ? candidate.sessionId.trim() : "";
|
|
201
|
+
if (ownerSessionId && ownerSessionId !== (currentSessionId?.trim() ?? "")) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
184
204
|
await removeFileAudited(filePath, {
|
|
185
205
|
cwd,
|
|
186
206
|
audit: { category: "prune", verb: "remove", owner: "gjc-runtime" },
|
|
@@ -48,6 +48,7 @@ export interface TmuxLaunchContext {
|
|
|
48
48
|
currentBranch?: string | null;
|
|
49
49
|
existingBranchSessionName?: string | null;
|
|
50
50
|
project?: string | null;
|
|
51
|
+
diagnosticWriter?: (message: string) => void;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export interface TmuxSpawnResult {
|
|
@@ -120,6 +121,16 @@ function isInteractiveRootLaunch(parsed: Args, tty: TtyState): boolean {
|
|
|
120
121
|
);
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
function isBunVirtualPath(value: string | undefined): boolean {
|
|
125
|
+
return value?.startsWith("/$bunfs/") === true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
|
|
129
|
+
const detail = stderr?.trim();
|
|
130
|
+
const suffix = detail ? ` ${detail.slice(0, 240)}` : "";
|
|
131
|
+
return `gjc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
function shellQuote(value: string): string {
|
|
124
135
|
if (value.length === 0) return "''";
|
|
125
136
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -148,6 +159,9 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
148
159
|
function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
149
160
|
const entrypoint = context.argv[1];
|
|
150
161
|
if (!entrypoint) return ["gjc"];
|
|
162
|
+
if (isBunVirtualPath(entrypoint)) {
|
|
163
|
+
return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
|
|
164
|
+
}
|
|
151
165
|
const resolvedEntrypoint = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(context.cwd, entrypoint);
|
|
152
166
|
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
|
|
153
167
|
return [context.execPath, resolvedEntrypoint];
|
|
@@ -264,10 +278,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
264
278
|
});
|
|
265
279
|
if (profile.failures.length > 0) {
|
|
266
280
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
267
|
-
|
|
281
|
+
const failure =
|
|
282
|
+
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
283
|
+
(context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
|
|
284
|
+
formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
|
|
285
|
+
);
|
|
286
|
+
return true;
|
|
268
287
|
}
|
|
269
288
|
}
|
|
270
289
|
if (created.exitCode !== 0) return false;
|
|
271
290
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
272
|
-
|
|
291
|
+
if (attached.exitCode === 0) return true;
|
|
292
|
+
(context.diagnosticWriter ?? process.stderr.write.bind(process.stderr))(
|
|
293
|
+
formatTmuxLaunchDiagnostic("attach failed", attached.stderr),
|
|
294
|
+
);
|
|
295
|
+
return true;
|
|
273
296
|
}
|
|
@@ -1673,9 +1673,10 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
1673
1673
|
`You are ${worker.id} in gjc team ${config.team_name}.`,
|
|
1674
1674
|
`Team state root: ${config.state_root}.`,
|
|
1675
1675
|
workspace,
|
|
1676
|
-
`
|
|
1676
|
+
`Team brief (context only): ${config.task}`,
|
|
1677
|
+
"Before implementation, claim your worker-owned task and treat the claimed task record as the source of truth. Do not implement directly from the broad team brief.",
|
|
1677
1678
|
`Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
1678
|
-
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
1679
|
+
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
1679
1680
|
].join("\n");
|
|
1680
1681
|
const env = [
|
|
1681
1682
|
`GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
|
|
@@ -1689,7 +1690,80 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
1689
1690
|
];
|
|
1690
1691
|
return `${env.join(" ")} ${config.worker_command} ${shellQuote(prompt)}`;
|
|
1691
1692
|
}
|
|
1693
|
+
interface GjcTeamInitialLane {
|
|
1694
|
+
label: string;
|
|
1695
|
+
title: string;
|
|
1696
|
+
body: string;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function normalizeLaneId(label: string): string {
|
|
1700
|
+
return `lane-${sanitizeName(label).toLowerCase() || stableHash(label).slice(0, 8)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function parseExplicitTeamLanes(task: string): GjcTeamInitialLane[] {
|
|
1704
|
+
const lines = task.split(/\r?\n/);
|
|
1705
|
+
const lanes: GjcTeamInitialLane[] = [];
|
|
1706
|
+
let current: { label: string; title: string; body: string[] } | null = null;
|
|
1707
|
+
const laneHeading = /^#{2,6}\s+Lane\s+([A-Za-z0-9]+)\s*(?:[—–-]\s*(.+))?\s*$/;
|
|
1708
|
+
const boundaryHeading = /^#{1,6}\s+(?:Integration Owner|Verification Plan|ADR|Approval State)\b/i;
|
|
1709
|
+
|
|
1710
|
+
for (const line of lines) {
|
|
1711
|
+
const match = line.match(laneHeading);
|
|
1712
|
+
if (match) {
|
|
1713
|
+
if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1714
|
+
current = {
|
|
1715
|
+
label: match[1] ?? `${lanes.length + 1}`,
|
|
1716
|
+
title: (match[2] ?? `Lane ${match[1] ?? lanes.length + 1}`).trim(),
|
|
1717
|
+
body: [],
|
|
1718
|
+
};
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (current && boundaryHeading.test(line)) {
|
|
1722
|
+
lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1723
|
+
current = null;
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (current) current.body.push(line);
|
|
1727
|
+
}
|
|
1728
|
+
if (current) lanes.push({ ...current, body: current.body.join("\n").trim() });
|
|
1729
|
+
return lanes.filter(lane => lane.body.length > 0 || lane.title.length > 0);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function hasAmbiguousLaneSplitIntent(task: string): boolean {
|
|
1733
|
+
return (
|
|
1734
|
+
/\bsplit\s+lanes?\s*:/i.test(task) || /\blanes?\s*:\s*[A-Z]\b/i.test(task) || /\bLane\s+[A-Z]\s*[—–-]/.test(task)
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1692
1738
|
function buildInitialTasks(task: string, workers: GjcTeamWorker[]): GjcTeamTask[] {
|
|
1739
|
+
const lanes = parseExplicitTeamLanes(task);
|
|
1740
|
+
if (lanes.length > 0)
|
|
1741
|
+
return lanes.map((lane, index) => {
|
|
1742
|
+
const worker = workers[index % workers.length];
|
|
1743
|
+
if (!worker) throw new Error("team_lane_requires_worker");
|
|
1744
|
+
const laneTitle = `Lane ${lane.label} — ${lane.title}`;
|
|
1745
|
+
const objective = [`${laneTitle}`, lane.body].filter(part => part.trim().length > 0).join("\n\n");
|
|
1746
|
+
return {
|
|
1747
|
+
id: `task-${index + 1}`,
|
|
1748
|
+
subject: laneTitle,
|
|
1749
|
+
description: objective,
|
|
1750
|
+
title: laneTitle,
|
|
1751
|
+
objective,
|
|
1752
|
+
status: "pending",
|
|
1753
|
+
owner: worker.id,
|
|
1754
|
+
lane: normalizeLaneId(lane.label),
|
|
1755
|
+
required_role: worker.role,
|
|
1756
|
+
version: 1,
|
|
1757
|
+
created_at: now(),
|
|
1758
|
+
updated_at: now(),
|
|
1759
|
+
};
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
if (workers.length > 1 && hasAmbiguousLaneSplitIntent(task))
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
"ambiguous_team_lane_split: multi-worker team launch mentions lanes but does not provide explicit markdown lane sections such as `### Lane A — Title`",
|
|
1765
|
+
);
|
|
1766
|
+
|
|
1693
1767
|
return workers.map(worker => ({
|
|
1694
1768
|
id: `task-${worker.index}`,
|
|
1695
1769
|
subject: `Execute team brief (${worker.id})`,
|
|
@@ -2456,6 +2530,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2456
2530
|
? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
|
|
2457
2531
|
: readCurrentTmuxLeaderContext(tmuxCommand, env);
|
|
2458
2532
|
const initialWorkers = buildWorkers(options.workerCount, options.agentType, stateRoot);
|
|
2533
|
+
const initialTasks = buildInitialTasks(options.task, initialWorkers);
|
|
2459
2534
|
const workers: GjcTeamWorker[] = [];
|
|
2460
2535
|
try {
|
|
2461
2536
|
for (const worker of initialWorkers)
|
|
@@ -2509,7 +2584,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2509
2584
|
updated_at: createdAt,
|
|
2510
2585
|
});
|
|
2511
2586
|
await writePhase(dir, "starting");
|
|
2512
|
-
for (const task of
|
|
2587
|
+
for (const task of initialTasks) await writeTask(dir, task);
|
|
2513
2588
|
await appendEvent(dir, {
|
|
2514
2589
|
type: "team_started",
|
|
2515
2590
|
message: options.dryRun
|
|
@@ -3,6 +3,7 @@ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
|
3
3
|
import {
|
|
4
4
|
computeUltragoalPlanGeneration,
|
|
5
5
|
getUltragoalPaths,
|
|
6
|
+
getUltragoalRunCompletionState,
|
|
6
7
|
hashStructuredValue,
|
|
7
8
|
readUltragoalLedger,
|
|
8
9
|
readUltragoalPlan,
|
|
@@ -246,7 +247,8 @@ export async function readUltragoalVerificationState(input: {
|
|
|
246
247
|
message: "Ultragoal has recorded review blockers; complete blocker work and rerun verification.",
|
|
247
248
|
};
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
+
const runState = getUltragoalRunCompletionState(plan);
|
|
251
|
+
if (runState.incompleteGoals.some(goal => goal.status === "blocked" || goal.status === "failed")) {
|
|
250
252
|
return {
|
|
251
253
|
state: "active_dirty_quality_gate",
|
|
252
254
|
message: "Ultragoal has blocked or failed goals; record blockers or rerun verification.",
|
|
@@ -259,7 +261,21 @@ export async function readUltragoalVerificationState(input: {
|
|
|
259
261
|
message: "Ultragoal aggregate completion requires a fresh final aggregate receipt.",
|
|
260
262
|
};
|
|
261
263
|
}
|
|
262
|
-
|
|
264
|
+
const receiptDiagnostic = validateCompletionReceipt({
|
|
265
|
+
plan,
|
|
266
|
+
ledger,
|
|
267
|
+
goal: receiptTarget.goal,
|
|
268
|
+
receiptKind: receiptTarget.receiptKind,
|
|
269
|
+
});
|
|
270
|
+
if (receiptDiagnostic.state !== "active_verified_complete") return receiptDiagnostic;
|
|
271
|
+
if (runState.incompleteGoals.length > 0) {
|
|
272
|
+
return {
|
|
273
|
+
state: "active_missing_final_receipt",
|
|
274
|
+
message: `Ultragoal still has incomplete required goals: ${runState.incompleteGoals.map(goal => goal.id).join(", ")}. Run \`gjc ultragoal complete-goals\` to continue.`,
|
|
275
|
+
goalId: receiptTarget.goal.id,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return receiptDiagnostic;
|
|
263
279
|
}
|
|
264
280
|
|
|
265
281
|
export async function assertCanCompleteCurrentGoal(input: {
|