@gajae-code/coding-agent 0.7.11 → 0.8.0
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 +33 -0
- package/README.md +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- package/src/config/mcp-schema.json +5 -1
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +6 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/gjc-runtime/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +18 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +58 -2
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -0
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +151 -30
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +20 -23
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/types.ts +1 -0
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +21 -1
- package/src/session/session-manager.ts +58 -18
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +15 -6
- package/src/tools/index.ts +8 -1
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
|
@@ -1461,9 +1461,25 @@ function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
|
|
|
1461
1461
|
function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
|
|
1462
1462
|
entries: T[],
|
|
1463
1463
|
stores: ResidentBlobStores,
|
|
1464
|
+
missingPolicy: ResidentBlobMissingPolicy = "throw",
|
|
1464
1465
|
): T[] {
|
|
1465
1466
|
const cache = new Map<string, string>();
|
|
1466
|
-
return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
|
|
1467
|
+
return entries.map(entry => materializeResidentEntrySync(entry, stores, cache, missingPolicy));
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function materializeResidentEntryForReadSync<T extends FileEntry | SessionEntry>(
|
|
1471
|
+
entry: T,
|
|
1472
|
+
stores: ResidentBlobStores,
|
|
1473
|
+
cache: Map<string, string>,
|
|
1474
|
+
): T {
|
|
1475
|
+
return materializeResidentEntrySync(entry, stores, cache, "placeholder");
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function materializeResidentEntriesForReadSync<T extends FileEntry | SessionEntry>(
|
|
1479
|
+
entries: T[],
|
|
1480
|
+
stores: ResidentBlobStores,
|
|
1481
|
+
): T[] {
|
|
1482
|
+
return materializeResidentEntriesSync(entries, stores, "placeholder");
|
|
1467
1483
|
}
|
|
1468
1484
|
|
|
1469
1485
|
function materializeResidentEntryForPersistenceSync<T extends FileEntry | SessionEntry>(
|
|
@@ -1520,12 +1536,24 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
|
|
|
1520
1536
|
function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
|
|
1521
1537
|
if (entry.type === "compaction") {
|
|
1522
1538
|
const cache = new Map<string, string>();
|
|
1523
|
-
const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache);
|
|
1524
|
-
const shortSummary = materializeResidentValueSync(
|
|
1539
|
+
const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache, "placeholder");
|
|
1540
|
+
const shortSummary = materializeResidentValueSync(
|
|
1541
|
+
entry.shortSummary,
|
|
1542
|
+
stores,
|
|
1543
|
+
"shortSummary",
|
|
1544
|
+
cache,
|
|
1545
|
+
"placeholder",
|
|
1546
|
+
);
|
|
1525
1547
|
const remote = entry.preserveData?.openaiRemoteCompaction;
|
|
1526
1548
|
const remoteRecord = isRecord(remote) ? remote : undefined;
|
|
1527
1549
|
const replacementHistory = remoteRecord
|
|
1528
|
-
? materializeResidentValueSync(
|
|
1550
|
+
? materializeResidentValueSync(
|
|
1551
|
+
remoteRecord.replacementHistory,
|
|
1552
|
+
stores,
|
|
1553
|
+
"replacementHistory",
|
|
1554
|
+
cache,
|
|
1555
|
+
"placeholder",
|
|
1556
|
+
)
|
|
1529
1557
|
: undefined;
|
|
1530
1558
|
const preserveData =
|
|
1531
1559
|
remoteRecord && replacementHistory !== undefined && replacementHistory !== remoteRecord.replacementHistory
|
|
@@ -1545,7 +1573,13 @@ function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: Reside
|
|
|
1545
1573
|
};
|
|
1546
1574
|
}
|
|
1547
1575
|
if (entry.type === "branch_summary") {
|
|
1548
|
-
const summary = materializeResidentValueSync(
|
|
1576
|
+
const summary = materializeResidentValueSync(
|
|
1577
|
+
entry.summary,
|
|
1578
|
+
stores,
|
|
1579
|
+
"summary",
|
|
1580
|
+
new Map<string, string>(),
|
|
1581
|
+
"placeholder",
|
|
1582
|
+
);
|
|
1549
1583
|
return typeof summary === "string" ? { ...entry, summary } : { ...entry };
|
|
1550
1584
|
}
|
|
1551
1585
|
return cloneSessionEntry(entry);
|
|
@@ -2751,7 +2785,10 @@ export class SessionManager {
|
|
|
2751
2785
|
}
|
|
2752
2786
|
|
|
2753
2787
|
captureState(): SessionManagerStateSnapshot {
|
|
2754
|
-
const materializedFileEntries =
|
|
2788
|
+
const materializedFileEntries = materializeResidentEntriesForReadSync(
|
|
2789
|
+
this.#fileEntries,
|
|
2790
|
+
this.#residentBlobStores(),
|
|
2791
|
+
);
|
|
2755
2792
|
return {
|
|
2756
2793
|
sessionId: this.#sessionId,
|
|
2757
2794
|
sessionName: this.#sessionName,
|
|
@@ -2875,7 +2912,7 @@ export class SessionManager {
|
|
|
2875
2912
|
|
|
2876
2913
|
const oldSessionFile = this.#sessionFile;
|
|
2877
2914
|
const oldSessionId = this.#sessionId;
|
|
2878
|
-
const materializedEntries =
|
|
2915
|
+
const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
|
|
2879
2916
|
|
|
2880
2917
|
// Close the current writer
|
|
2881
2918
|
await this.#closePersistWriter();
|
|
@@ -2947,7 +2984,10 @@ export class SessionManager {
|
|
|
2947
2984
|
hadSessionFile = this.storage.existsSync(oldSessionFile);
|
|
2948
2985
|
let movedSessionFile = false;
|
|
2949
2986
|
let movedArtifactDir = false;
|
|
2950
|
-
const materializedEntries =
|
|
2987
|
+
const materializedEntries = materializeResidentEntriesForReadSync(
|
|
2988
|
+
this.#fileEntries,
|
|
2989
|
+
this.#residentBlobStores(),
|
|
2990
|
+
);
|
|
2951
2991
|
const restoreResidentStateAfterFailure = (): void => {
|
|
2952
2992
|
this.#fileEntries = materializedEntries;
|
|
2953
2993
|
this.#resetResidentTextBlobStore();
|
|
@@ -4006,7 +4046,7 @@ export class SessionManager {
|
|
|
4006
4046
|
getLeafEntry(): SessionEntry | undefined {
|
|
4007
4047
|
if (!this.#leafId) return undefined;
|
|
4008
4048
|
const entry = this.#byId.get(this.#leafId);
|
|
4009
|
-
return entry ?
|
|
4049
|
+
return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
4010
4050
|
}
|
|
4011
4051
|
|
|
4012
4052
|
/**
|
|
@@ -4176,7 +4216,7 @@ export class SessionManager {
|
|
|
4176
4216
|
const entry = this.#byId.get(id);
|
|
4177
4217
|
return entry
|
|
4178
4218
|
? rehydrateColdSpillEntry(
|
|
4179
|
-
|
|
4219
|
+
materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
|
|
4180
4220
|
this.#blobStore,
|
|
4181
4221
|
this.#residentBlobStoresForColdRehydrate(),
|
|
4182
4222
|
)
|
|
@@ -4193,7 +4233,7 @@ export class SessionManager {
|
|
|
4193
4233
|
visited.add(current.id);
|
|
4194
4234
|
path.push(
|
|
4195
4235
|
rehydrateColdSpillEntry(
|
|
4196
|
-
|
|
4236
|
+
materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
|
|
4197
4237
|
this.#blobStore,
|
|
4198
4238
|
this.#residentBlobStoresForColdRehydrate(),
|
|
4199
4239
|
),
|
|
@@ -4233,7 +4273,7 @@ export class SessionManager {
|
|
|
4233
4273
|
.filter((entry): entry is SessionEntry => entry.type !== "session")
|
|
4234
4274
|
.map(entry =>
|
|
4235
4275
|
rehydrateColdSpillEntry(
|
|
4236
|
-
|
|
4276
|
+
materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
|
|
4237
4277
|
this.#blobStore,
|
|
4238
4278
|
this.#residentBlobStoresForColdRehydrate(),
|
|
4239
4279
|
),
|
|
@@ -4244,7 +4284,7 @@ export class SessionManager {
|
|
|
4244
4284
|
this.#publicMaterializerCallCount++;
|
|
4245
4285
|
this.#getEntryMaterializerCallCount++;
|
|
4246
4286
|
const entry = this.#byId.get(id);
|
|
4247
|
-
return entry ?
|
|
4287
|
+
return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
4248
4288
|
}
|
|
4249
4289
|
|
|
4250
4290
|
/**
|
|
@@ -4255,7 +4295,7 @@ export class SessionManager {
|
|
|
4255
4295
|
const children: SessionEntry[] = [];
|
|
4256
4296
|
for (const entry of this.#byId.values()) {
|
|
4257
4297
|
if (entry.parentId === parentId) {
|
|
4258
|
-
children.push(
|
|
4298
|
+
children.push(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache));
|
|
4259
4299
|
}
|
|
4260
4300
|
}
|
|
4261
4301
|
return children;
|
|
@@ -4310,7 +4350,7 @@ export class SessionManager {
|
|
|
4310
4350
|
while (current) {
|
|
4311
4351
|
if (visited.has(current.id)) break;
|
|
4312
4352
|
visited.add(current.id);
|
|
4313
|
-
path.push(
|
|
4353
|
+
path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
|
|
4314
4354
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4315
4355
|
}
|
|
4316
4356
|
path.reverse();
|
|
@@ -4375,7 +4415,7 @@ export class SessionManager {
|
|
|
4375
4415
|
}
|
|
4376
4416
|
if (entry.type !== "message" && entry.type !== "custom_message")
|
|
4377
4417
|
return materializeProviderVisibleEntrySync(entry, this.#residentBlobStores());
|
|
4378
|
-
const materialized =
|
|
4418
|
+
const materialized = materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map());
|
|
4379
4419
|
const rehydrated = rehydrateColdSpillEntry(
|
|
4380
4420
|
materialized,
|
|
4381
4421
|
this.#blobStore,
|
|
@@ -4434,7 +4474,7 @@ export class SessionManager {
|
|
|
4434
4474
|
const resolvedTextBlobCache = new Map<string, string>();
|
|
4435
4475
|
const materializedEntries = this.#fileEntries
|
|
4436
4476
|
.filter((e): e is SessionEntry => e.type !== "session")
|
|
4437
|
-
.map(entry =>
|
|
4477
|
+
.map(entry => materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
|
|
4438
4478
|
this.#materializedEntriesCache = materializedEntries;
|
|
4439
4479
|
this.#materializedEntriesRevision = this.#entryRevision;
|
|
4440
4480
|
return materializedEntries;
|
|
@@ -4591,7 +4631,7 @@ export class SessionManager {
|
|
|
4591
4631
|
|
|
4592
4632
|
// Filter out LabelEntry from path - we'll recreate them from the resolved map
|
|
4593
4633
|
const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
|
|
4594
|
-
const materializedPathWithoutLabels =
|
|
4634
|
+
const materializedPathWithoutLabels = materializeResidentEntriesForReadSync(
|
|
4595
4635
|
pathWithoutLabels,
|
|
4596
4636
|
this.#residentBlobStores(),
|
|
4597
4637
|
);
|
package/src/system-prompt.ts
CHANGED
|
@@ -356,6 +356,10 @@ export interface BuildSystemPromptOptions {
|
|
|
356
356
|
mcpDiscoveryMode?: boolean;
|
|
357
357
|
/** Discoverable MCP server summaries to advertise when discovery mode is active. */
|
|
358
358
|
mcpDiscoveryServerSummaries?: string[];
|
|
359
|
+
/** Whether built-in tool discovery ("all" mode) is active; enables the tool-discovery prompt block. */
|
|
360
|
+
toolDiscoveryActive?: boolean;
|
|
361
|
+
/** Hidden discoverable tools to advertise (name + one-line summary) when tool discovery is active. */
|
|
362
|
+
discoverableTools?: Array<{ name: string; summary: string }>;
|
|
359
363
|
/** Encourage the agent to delegate via tasks unless changes are trivial. */
|
|
360
364
|
eagerTasks?: boolean;
|
|
361
365
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
@@ -394,6 +398,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
394
398
|
intentField,
|
|
395
399
|
mcpDiscoveryMode = false,
|
|
396
400
|
mcpDiscoveryServerSummaries = [],
|
|
401
|
+
toolDiscoveryActive = false,
|
|
402
|
+
discoverableTools = [],
|
|
397
403
|
eagerTasks = false,
|
|
398
404
|
secretsEnabled = false,
|
|
399
405
|
workspaceTree: providedWorkspaceTree,
|
|
@@ -572,6 +578,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
572
578
|
mcpDiscoveryMode,
|
|
573
579
|
hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
|
|
574
580
|
mcpDiscoveryServerSummaries,
|
|
581
|
+
toolDiscoveryActive,
|
|
582
|
+
discoverableTools,
|
|
575
583
|
eagerTasks,
|
|
576
584
|
secretsEnabled,
|
|
577
585
|
};
|
package/src/task/executor.ts
CHANGED
|
@@ -1535,6 +1535,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1535
1535
|
);
|
|
1536
1536
|
await awaitAbortable(session.waitForIdle());
|
|
1537
1537
|
} catch (err) {
|
|
1538
|
+
// An abort (parent cancel, session shutdown, timeout) rejects
|
|
1539
|
+
// awaitAbortable with ToolAbortError — a cancellation, not a prompt
|
|
1540
|
+
// failure. Mirror the outer catch's `!abortSignal.aborted` guard so
|
|
1541
|
+
// expected cancellations aren't logged at error level.
|
|
1542
|
+
if (abortSignal.aborted || err instanceof ToolAbortError) break;
|
|
1538
1543
|
logger.error("Subagent prompt failed", {
|
|
1539
1544
|
error: err instanceof Error ? err.message : String(err),
|
|
1540
1545
|
});
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -223,13 +223,16 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
223
223
|
const modelOut: string[] = [];
|
|
224
224
|
const displayOut: string[] = [];
|
|
225
225
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const endLine = match.startLine +
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
const preparedMatches = fileMatches.map(match => {
|
|
227
|
+
const lines = match.text.split("\n");
|
|
228
|
+
const endLine = match.startLine + lines.length - 1;
|
|
229
|
+
return { match, lines, endLine };
|
|
230
|
+
});
|
|
231
|
+
const lineNumberWidth = preparedMatches.reduce(
|
|
232
|
+
(width, entry) => Math.max(width, String(entry.match.startLine).length, String(entry.endLine).length),
|
|
233
|
+
0,
|
|
234
|
+
);
|
|
235
|
+
for (const { match, lines: matchLines } of preparedMatches) {
|
|
233
236
|
for (let index = 0; index < matchLines.length; index++) {
|
|
234
237
|
const lineNumber = match.startLine + index;
|
|
235
238
|
const isMatch = index === 0;
|
package/src/tools/bash.ts
CHANGED
|
@@ -833,9 +833,22 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
833
833
|
});
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
+
// Route through the client terminal when the client advertises the terminal capability.
|
|
837
|
+
// Skip when pty=true (PTY needs the local terminal UI).
|
|
838
|
+
const clientBridge =
|
|
839
|
+
this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
|
|
840
|
+
const clientTerminalActive = Boolean(clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty);
|
|
841
|
+
|
|
836
842
|
const autoBgManager = AsyncJobManager.instance();
|
|
837
|
-
|
|
838
|
-
|
|
843
|
+
// Run non-PTY bash through the managed job path so Ctrl+B-twice fold-on-demand works
|
|
844
|
+
// even when auto-background is disabled. When a client terminal will handle the
|
|
845
|
+
// command, keep the existing bridge path unless auto-background is enabled.
|
|
846
|
+
if (!pty && autoBgManager && (this.#autoBackgroundEnabled || !clientTerminalActive)) {
|
|
847
|
+
// With auto-background off, wait past the command's own timeout so the job only
|
|
848
|
+
// leaves the foreground on an explicit Ctrl+B fold, never on an auto-background timer.
|
|
849
|
+
const autoBackgroundWaitMs = this.#autoBackgroundEnabled
|
|
850
|
+
? this.#resolveAutoBackgroundWaitMs(timeoutMs)
|
|
851
|
+
: timeoutMs + 1_000;
|
|
839
852
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
|
840
853
|
const job = this.#startManagedBashJob({
|
|
841
854
|
command,
|
|
@@ -891,10 +904,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
891
904
|
});
|
|
892
905
|
}
|
|
893
906
|
|
|
894
|
-
// Route through the client terminal when the client advertises the terminal capability.
|
|
895
|
-
// Skip when pty=true (PTY needs the local terminal UI).
|
|
896
|
-
const clientBridge =
|
|
897
|
-
this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
|
|
898
907
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
899
908
|
const handle = await clientBridge.createTerminal({
|
|
900
909
|
command,
|
package/src/tools/index.ts
CHANGED
|
@@ -325,7 +325,14 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
|
|
|
325
325
|
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
|
326
326
|
|
|
327
327
|
/** Default essential tool names when tools.essentialOverride is empty. */
|
|
328
|
-
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
328
|
+
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
329
|
+
"read",
|
|
330
|
+
"bash",
|
|
331
|
+
"edit",
|
|
332
|
+
"write",
|
|
333
|
+
"search",
|
|
334
|
+
"find",
|
|
335
|
+
] as const;
|
|
329
336
|
|
|
330
337
|
/**
|
|
331
338
|
* Resolve the active essential built-in tool names from settings.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
export const WORKFLOW_INTENT_DIFF_CUSTOM_TYPE = "workflow-intent-diff";
|
|
2
|
+
|
|
3
|
+
export type WorkflowIntentRoute = "direct" | "deep-interview" | "ralplan" | "ultragoal" | "team";
|
|
4
|
+
export type DirectTrackingMode = "custom-entry-only" | "not-direct";
|
|
5
|
+
export type RootCausePhaseStatus = "active" | "inactive";
|
|
6
|
+
|
|
7
|
+
export interface WorkflowIntentDiff {
|
|
8
|
+
readonly version: 1;
|
|
9
|
+
readonly route: WorkflowIntentRoute;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
readonly directTracking: DirectTrackingMode;
|
|
12
|
+
readonly recommendedSkill?: Exclude<WorkflowIntentRoute, "direct">;
|
|
13
|
+
readonly recommendedInvocation?: string;
|
|
14
|
+
readonly triggers: readonly string[];
|
|
15
|
+
readonly rootCausePhase: {
|
|
16
|
+
readonly status: RootCausePhaseStatus;
|
|
17
|
+
readonly triggers: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
readonly promptPreview: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RouteMatch {
|
|
23
|
+
readonly route: WorkflowIntentRoute;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
readonly recommendedInvocation?: string;
|
|
26
|
+
readonly triggers: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PROMPT_PREVIEW_LIMIT = 240;
|
|
30
|
+
|
|
31
|
+
const DURABLE_TRACKING_PATTERNS = [
|
|
32
|
+
/\bultragoal\b/i,
|
|
33
|
+
/\bdurable (?:goal|tracking|ledger|plan)\b/i,
|
|
34
|
+
/\b(?:goal|tracking|plan) ledger\b/i,
|
|
35
|
+
/\bcheckpoint(?:ed|ing)? (?:goal|plan|workflow|release|work)\b/i,
|
|
36
|
+
/\bcheckpoint (?:this|the) (?:goal|plan|workflow|release|work)\b/i,
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const AMBIGUOUS_REQUIREMENT_PATTERNS = [
|
|
40
|
+
/\bambiguous (?:requirement|requirements|request|requests|scope)\b/i,
|
|
41
|
+
/\bdeep[- ]interview\b/i,
|
|
42
|
+
/\binterview me\b/i,
|
|
43
|
+
/\bdon't assume\b/i,
|
|
44
|
+
/\bnot sure\b/i,
|
|
45
|
+
/\bunclear\b/i,
|
|
46
|
+
/\bvague\b/i,
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
const ARCHITECTURE_SEQUENCE_PATTERNS = [
|
|
50
|
+
/\barchitecture\b/i,
|
|
51
|
+
/\barchitectural\b/i,
|
|
52
|
+
/\bsequence\b/i,
|
|
53
|
+
/\bmigrate\b/i,
|
|
54
|
+
/\bmigration\b/i,
|
|
55
|
+
/\bauth(?:entication|orization)? (?:migration|sequence|rollout|release|refactor|rewrite|transition)\b/i,
|
|
56
|
+
/\bsecurity\b/i,
|
|
57
|
+
/\bcompliance\b/i,
|
|
58
|
+
/\bPII\b/i,
|
|
59
|
+
/\bproduction (?:release|rollout|deployment|migration|incident|data|change|cutover)\b/i,
|
|
60
|
+
/\bdeploy(?:ing)? to production\b/i,
|
|
61
|
+
/\bbreaking change\b/i,
|
|
62
|
+
/\bdata loss\b/i,
|
|
63
|
+
/\bdestructive\b/i,
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
const TEAM_PATTERNS = [
|
|
67
|
+
/\buse (?:a )?team\b/i,
|
|
68
|
+
/\bcoordinated (?:persistent )?workers\b/i,
|
|
69
|
+
/\bpersistent workers\b/i,
|
|
70
|
+
/\bworker coordination\b/i,
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
const ROOT_CAUSE_TRIGGERS = [
|
|
74
|
+
{ name: "contradiction", pattern: /\bcontradict(?:ion|s|ory)?\b/i },
|
|
75
|
+
{ name: "regression", pattern: /\bregression\b/i },
|
|
76
|
+
{
|
|
77
|
+
name: "high-risk transition",
|
|
78
|
+
pattern:
|
|
79
|
+
/\b(?:migrate|migration|auth(?:entication|orization)? (?:migration|sequence|rollout|release|refactor|rewrite|transition)|security|breaking change|data loss|destructive|production (?:release|rollout|deployment|migration|incident|data|change|cutover)|deploy(?:ing)? to production|compliance|PII)\b/i,
|
|
80
|
+
},
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
function matchesAny(text: string, patterns: readonly RegExp[]): boolean {
|
|
84
|
+
return patterns.some(pattern => pattern.test(text));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withGlobal(pattern: RegExp): RegExp {
|
|
88
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
89
|
+
return new RegExp(pattern.source, flags);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNegatedAt(text: string, index: number): boolean {
|
|
93
|
+
const prefix = text.slice(Math.max(0, index - 48), index).toLowerCase();
|
|
94
|
+
const boundary = Math.max(
|
|
95
|
+
prefix.lastIndexOf("."),
|
|
96
|
+
prefix.lastIndexOf(","),
|
|
97
|
+
prefix.lastIndexOf(";"),
|
|
98
|
+
prefix.lastIndexOf(":"),
|
|
99
|
+
prefix.lastIndexOf("?"),
|
|
100
|
+
prefix.lastIndexOf("!"),
|
|
101
|
+
prefix.lastIndexOf("\n"),
|
|
102
|
+
prefix.lastIndexOf(" but "),
|
|
103
|
+
prefix.lastIndexOf(" however "),
|
|
104
|
+
prefix.lastIndexOf(" yet "),
|
|
105
|
+
prefix.lastIndexOf(" then "),
|
|
106
|
+
);
|
|
107
|
+
const localPrefix = boundary >= 0 ? prefix.slice(boundary + 1) : prefix;
|
|
108
|
+
return /(?:^|[\s([{])(?:no|not|without|excluding|exclude|absent|absence of|avoid)\b[^.,!?;:\n]{0,48}$/.test(
|
|
109
|
+
localPrefix,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function matchesAnyNonNegated(text: string, patterns: readonly RegExp[]): boolean {
|
|
114
|
+
return patterns.some(pattern => {
|
|
115
|
+
for (const match of text.matchAll(withGlobal(pattern))) {
|
|
116
|
+
if (!isNegatedAt(text, match.index ?? 0)) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectRootCauseTriggers(text: string): readonly string[] {
|
|
123
|
+
return ROOT_CAUSE_TRIGGERS.filter(trigger => matchesAnyNonNegated(text, [trigger.pattern])).map(
|
|
124
|
+
trigger => trigger.name,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildPromptPreview(text: string): string {
|
|
129
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
130
|
+
return compact.length > PROMPT_PREVIEW_LIMIT ? `${compact.slice(0, PROMPT_PREVIEW_LIMIT - 3)}...` : compact;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function classifyRoute(text: string): RouteMatch {
|
|
134
|
+
if (matchesAny(text, AMBIGUOUS_REQUIREMENT_PATTERNS)) {
|
|
135
|
+
return {
|
|
136
|
+
route: "deep-interview",
|
|
137
|
+
reason: "ambiguous requirements need clarification",
|
|
138
|
+
recommendedInvocation: "/skill:deep-interview",
|
|
139
|
+
triggers: ["ambiguous requirements"],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (matchesAny(text, DURABLE_TRACKING_PATTERNS)) {
|
|
144
|
+
return {
|
|
145
|
+
route: "ultragoal",
|
|
146
|
+
reason: "durable tracking requested",
|
|
147
|
+
recommendedInvocation: "/skill:ultragoal",
|
|
148
|
+
triggers: ["durable tracking"],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (matchesAnyNonNegated(text, ARCHITECTURE_SEQUENCE_PATTERNS)) {
|
|
153
|
+
return {
|
|
154
|
+
route: "ralplan",
|
|
155
|
+
reason: "architecture or sequence risk requires deliberate planning",
|
|
156
|
+
recommendedInvocation: "/skill:ralplan --deliberate",
|
|
157
|
+
triggers: ["architecture/sequence risk"],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (matchesAny(text, TEAM_PATTERNS)) {
|
|
162
|
+
return {
|
|
163
|
+
route: "team",
|
|
164
|
+
reason: "coordinated persistent workers requested",
|
|
165
|
+
recommendedInvocation: "/skill:team",
|
|
166
|
+
triggers: ["coordinated workers"],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
route: "direct",
|
|
172
|
+
reason: "clear low-risk prompt stays on direct implementation path",
|
|
173
|
+
triggers: ["low-risk direct"],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function buildWorkflowIntentDiff(text: string): WorkflowIntentDiff | null {
|
|
178
|
+
const promptPreview = buildPromptPreview(text);
|
|
179
|
+
if (!promptPreview) return null;
|
|
180
|
+
|
|
181
|
+
const route = classifyRoute(text);
|
|
182
|
+
const rootCauseTriggers = collectRootCauseTriggers(text);
|
|
183
|
+
const rootCauseActive = rootCauseTriggers.length > 0;
|
|
184
|
+
const direct = route.route === "direct";
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
version: 1,
|
|
188
|
+
route: route.route,
|
|
189
|
+
reason: route.reason,
|
|
190
|
+
directTracking: direct ? "custom-entry-only" : "not-direct",
|
|
191
|
+
...(direct ? {} : { recommendedSkill: route.route, recommendedInvocation: route.recommendedInvocation }),
|
|
192
|
+
triggers: route.triggers,
|
|
193
|
+
rootCausePhase: {
|
|
194
|
+
status: rootCauseActive ? "active" : "inactive",
|
|
195
|
+
triggers: rootCauseTriggers,
|
|
196
|
+
},
|
|
197
|
+
promptPreview,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/src/discovery/vscode.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VS Code Provider
|
|
3
|
-
*
|
|
4
|
-
* Loads config from `.vscode` directory (project-only).
|
|
5
|
-
* Supports MCP server discovery from `mcp.json` with nested `mcp.servers` structure.
|
|
6
|
-
*/
|
|
7
|
-
import { tryParseJson } from "@gajae-code/utils";
|
|
8
|
-
import { registerProvider } from "../capability";
|
|
9
|
-
import { readFile } from "../capability/fs";
|
|
10
|
-
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
11
|
-
import type { LoadContext, LoadResult } from "../capability/types";
|
|
12
|
-
import { createSourceMeta, expandEnvVarsDeep, getProjectPath } from "./helpers";
|
|
13
|
-
|
|
14
|
-
const PROVIDER_ID = "vscode";
|
|
15
|
-
const DISPLAY_NAME = "VS Code";
|
|
16
|
-
const PRIORITY = 20;
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// MCP Servers
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
registerProvider<MCPServer>(mcpCapability.id, {
|
|
23
|
-
id: PROVIDER_ID,
|
|
24
|
-
displayName: DISPLAY_NAME,
|
|
25
|
-
description: "Load MCP servers from .vscode/mcp.json",
|
|
26
|
-
priority: PRIORITY,
|
|
27
|
-
async load(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
28
|
-
const items: MCPServer[] = [];
|
|
29
|
-
const warnings: string[] = [];
|
|
30
|
-
|
|
31
|
-
// Project-only (VS Code doesn't support user-level MCP config)
|
|
32
|
-
const projectPath = getProjectPath(ctx, "vscode", "mcp.json");
|
|
33
|
-
if (projectPath) {
|
|
34
|
-
const result = await loadMCPConfig(ctx, projectPath, "project");
|
|
35
|
-
items.push(...result.items);
|
|
36
|
-
if (result.warnings) warnings.push(...result.warnings);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return { items, warnings };
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Load MCP servers from a mcp.json file.
|
|
45
|
-
* VS Code uses nested structure: { "mcp": { "servers": { ... } } }
|
|
46
|
-
*/
|
|
47
|
-
async function loadMCPConfig(
|
|
48
|
-
_ctx: LoadContext,
|
|
49
|
-
path: string,
|
|
50
|
-
level: "user" | "project",
|
|
51
|
-
): Promise<LoadResult<MCPServer>> {
|
|
52
|
-
const items: MCPServer[] = [];
|
|
53
|
-
const warnings: string[] = [];
|
|
54
|
-
|
|
55
|
-
const content = await readFile(path);
|
|
56
|
-
if (!content) {
|
|
57
|
-
warnings.push(`Failed to read ${path}`);
|
|
58
|
-
return { items, warnings };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const parsed = tryParseJson<{ mcp?: { servers?: Record<string, unknown> } }>(content);
|
|
62
|
-
if (!parsed) {
|
|
63
|
-
warnings.push(`Invalid JSON in ${path}`);
|
|
64
|
-
return { items, warnings };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// VS Code uses nested structure: mcp.servers
|
|
68
|
-
const servers = parsed.mcp?.servers;
|
|
69
|
-
if (!servers || typeof servers !== "object") {
|
|
70
|
-
return { items, warnings };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const [name, config] of Object.entries(servers)) {
|
|
74
|
-
if (!config || typeof config !== "object") {
|
|
75
|
-
warnings.push(`Invalid config for server "${name}" in ${path}`);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const raw = config as Record<string, unknown>;
|
|
80
|
-
|
|
81
|
-
// Expand environment variables
|
|
82
|
-
const expanded = expandEnvVarsDeep(raw);
|
|
83
|
-
|
|
84
|
-
const server: MCPServer = {
|
|
85
|
-
name,
|
|
86
|
-
command: typeof expanded.command === "string" ? expanded.command : undefined,
|
|
87
|
-
args: Array.isArray(expanded.args) ? (expanded.args as string[]) : undefined,
|
|
88
|
-
env: expanded.env && typeof expanded.env === "object" ? (expanded.env as Record<string, string>) : undefined,
|
|
89
|
-
url: typeof expanded.url === "string" ? expanded.url : undefined,
|
|
90
|
-
headers:
|
|
91
|
-
expanded.headers && typeof expanded.headers === "object"
|
|
92
|
-
? (expanded.headers as Record<string, string>)
|
|
93
|
-
: undefined,
|
|
94
|
-
transport: ["stdio", "sse", "http"].includes(expanded.transport as string)
|
|
95
|
-
? (expanded.transport as "stdio" | "sse" | "http")
|
|
96
|
-
: undefined,
|
|
97
|
-
timeout: typeof expanded.timeout === "number" ? expanded.timeout : undefined,
|
|
98
|
-
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
items.push(server);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { items, warnings };
|
|
105
|
-
}
|