@gajae-code/coding-agent 0.12.8 → 0.12.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 +36 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +23 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +17 -0
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +12 -6
- package/src/commands/sdk.ts +54 -0
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/controllers/selector-controller.ts +61 -43
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/notification-service.ts +140 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
- package/src/sdk/session.ts +24 -7
- package/src/session/agent-session.ts +176 -34
- package/src/session/internal/managed-session-storage.ts +280 -248
- package/src/session/internal/native-publish-outcome.ts +12 -2
- package/src/session/session-manager.ts +140 -6
- package/src/session/session-storage.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +35 -11
- package/src/system-prompt.ts +28 -15
- package/src/tools/bash.ts +3 -1
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/computer.ts +322 -45
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -16,6 +16,8 @@ export type NativePublishOperation = "direct_rename" | "retained_file" | "retain
|
|
|
16
16
|
|
|
17
17
|
export type NativePublishPrimitive =
|
|
18
18
|
| "renameat2_noreplace"
|
|
19
|
+
| "linkat_noreplace"
|
|
20
|
+
| "mkdirat_renameat_noreplace"
|
|
19
21
|
| "renameatx_np_excl"
|
|
20
22
|
| "windows_rename_noreplace"
|
|
21
23
|
| "unsupported"
|
|
@@ -24,6 +26,7 @@ export type NativePublishPhase =
|
|
|
24
26
|
| "preflight"
|
|
25
27
|
| "file_sync"
|
|
26
28
|
| "rename"
|
|
29
|
+
| "source_unlink"
|
|
27
30
|
| "source_parent_sync"
|
|
28
31
|
| "destination_parent_sync"
|
|
29
32
|
| "terminal_identity"
|
|
@@ -83,6 +86,8 @@ const reasons = new Set<NativePublishReason>([
|
|
|
83
86
|
]);
|
|
84
87
|
const primitives = new Set<NativePublishPrimitive>([
|
|
85
88
|
"renameat2_noreplace",
|
|
89
|
+
"linkat_noreplace",
|
|
90
|
+
"mkdirat_renameat_noreplace",
|
|
86
91
|
"renameatx_np_excl",
|
|
87
92
|
"windows_rename_noreplace",
|
|
88
93
|
"unsupported",
|
|
@@ -92,6 +97,7 @@ const phases = new Set<NativePublishPhase>([
|
|
|
92
97
|
"preflight",
|
|
93
98
|
"file_sync",
|
|
94
99
|
"rename",
|
|
100
|
+
"source_unlink",
|
|
95
101
|
"source_parent_sync",
|
|
96
102
|
"destination_parent_sync",
|
|
97
103
|
"terminal_identity",
|
|
@@ -204,7 +210,7 @@ function legalOutcome(outcome: NativePublishOutcome): boolean {
|
|
|
204
210
|
((outcome.reason === "durability_not_provable" &&
|
|
205
211
|
["file_sync", "source_parent_sync", "destination_parent_sync"].includes(outcome.phase)) ||
|
|
206
212
|
(outcome.reason === "identity_violation" && outcome.phase === "terminal_identity") ||
|
|
207
|
-
(outcome.reason === "io_failure" &&
|
|
213
|
+
(outcome.reason === "io_failure" && ["source_unlink", "terminal_identity"].includes(outcome.phase)))
|
|
208
214
|
);
|
|
209
215
|
}
|
|
210
216
|
|
|
@@ -247,8 +253,12 @@ export function classifyNativePublishOutcome(
|
|
|
247
253
|
const outcome = value as unknown as NativePublishOutcome;
|
|
248
254
|
if (!legalOutcome(outcome)) return malformed;
|
|
249
255
|
if (operation === "direct_rename") return outcome;
|
|
256
|
+
const retainedPrimitiveValid =
|
|
257
|
+
operation === "retained_file"
|
|
258
|
+
? outcome.primitive === "renameat2_noreplace" || outcome.primitive === "linkat_noreplace"
|
|
259
|
+
: outcome.primitive === "renameat2_noreplace" || outcome.primitive === "mkdirat_renameat_noreplace";
|
|
250
260
|
if (
|
|
251
|
-
|
|
261
|
+
!retainedPrimitiveValid ||
|
|
252
262
|
(outcome.ok && (!outcome.identity || outcome.durabilityState !== "proven" || outcome.phase !== "complete"))
|
|
253
263
|
)
|
|
254
264
|
return malformed;
|
|
@@ -899,6 +899,7 @@ export interface ResumeSessionIdentity {
|
|
|
899
899
|
size: number;
|
|
900
900
|
mtimeMs: number;
|
|
901
901
|
mtimeNs: bigint;
|
|
902
|
+
ctimeNs?: bigint;
|
|
902
903
|
sha256: string;
|
|
903
904
|
}
|
|
904
905
|
|
|
@@ -918,6 +919,21 @@ export interface ResumeTailError {
|
|
|
918
919
|
}
|
|
919
920
|
|
|
920
921
|
export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
|
|
922
|
+
export type SessionAppendPersistenceFailurePhase = "current_append" | "prior_failure";
|
|
923
|
+
|
|
924
|
+
export class SessionAppendPersistenceError extends Error {
|
|
925
|
+
readonly phase: SessionAppendPersistenceFailurePhase;
|
|
926
|
+
readonly entryId: string;
|
|
927
|
+
readonly persistenceError: Error;
|
|
928
|
+
|
|
929
|
+
constructor(phase: SessionAppendPersistenceFailurePhase, entryId: string, persistenceError: Error) {
|
|
930
|
+
super(persistenceError.message, { cause: persistenceError });
|
|
931
|
+
this.name = "SessionAppendPersistenceError";
|
|
932
|
+
this.phase = phase;
|
|
933
|
+
this.entryId = entryId;
|
|
934
|
+
this.persistenceError = persistenceError;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
921
937
|
|
|
922
938
|
export class SessionManagedStorageError extends Error {
|
|
923
939
|
readonly code = "managed_storage_unsupported";
|
|
@@ -2161,6 +2177,7 @@ function sameResumeIdentity(left: ResumeSessionIdentity, right: ResumeSessionIde
|
|
|
2161
2177
|
left.size === right.size &&
|
|
2162
2178
|
left.mtimeMs === right.mtimeMs &&
|
|
2163
2179
|
left.mtimeNs === right.mtimeNs &&
|
|
2180
|
+
(left.ctimeNs === undefined || right.ctimeNs === undefined || left.ctimeNs === right.ctimeNs) &&
|
|
2164
2181
|
left.sha256 === right.sha256
|
|
2165
2182
|
);
|
|
2166
2183
|
}
|
|
@@ -2174,7 +2191,8 @@ function sameResumeStat(left: SessionStorageStat, right: SessionStorageStat): bo
|
|
|
2174
2191
|
left.nlink === right.nlink &&
|
|
2175
2192
|
left.size === right.size &&
|
|
2176
2193
|
left.mtimeMs === right.mtimeMs &&
|
|
2177
|
-
left.mtimeNs === right.mtimeNs
|
|
2194
|
+
left.mtimeNs === right.mtimeNs &&
|
|
2195
|
+
left.ctimeNs === right.ctimeNs
|
|
2178
2196
|
);
|
|
2179
2197
|
}
|
|
2180
2198
|
|
|
@@ -2372,6 +2390,7 @@ function inspectResumeSessionFile(
|
|
|
2372
2390
|
size: snapshot.size,
|
|
2373
2391
|
mtimeMs: snapshot.mtimeMs,
|
|
2374
2392
|
mtimeNs: snapshot.mtimeNs,
|
|
2393
|
+
ctimeNs: snapshot.ctimeNs,
|
|
2375
2394
|
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
2376
2395
|
};
|
|
2377
2396
|
return { identity, content: bytes, entries, context, migrationApplied };
|
|
@@ -2380,6 +2399,46 @@ function inspectResumeSessionFile(
|
|
|
2380
2399
|
}
|
|
2381
2400
|
}
|
|
2382
2401
|
|
|
2402
|
+
function fsyncResumeSessionIdentity(expected: ResumeSessionIdentity): void {
|
|
2403
|
+
const fd = fs.openSync(expected.canonicalPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2404
|
+
try {
|
|
2405
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
2406
|
+
if (
|
|
2407
|
+
before.dev !== expected.dev ||
|
|
2408
|
+
before.ino !== expected.ino ||
|
|
2409
|
+
(expected.nlink !== undefined && before.nlink !== expected.nlink) ||
|
|
2410
|
+
Number(before.size) !== expected.size ||
|
|
2411
|
+
before.mtimeNs !== expected.mtimeNs ||
|
|
2412
|
+
(expected.ctimeNs !== undefined && before.ctimeNs !== expected.ctimeNs)
|
|
2413
|
+
) {
|
|
2414
|
+
throw new Error("session_persistence_recovery_identity_mismatch");
|
|
2415
|
+
}
|
|
2416
|
+
fs.fsyncSync(fd);
|
|
2417
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
2418
|
+
const named = fs.lstatSync(expected.canonicalPath, { bigint: true });
|
|
2419
|
+
if (
|
|
2420
|
+
after.dev !== before.dev ||
|
|
2421
|
+
after.ino !== before.ino ||
|
|
2422
|
+
after.nlink !== before.nlink ||
|
|
2423
|
+
after.size !== before.size ||
|
|
2424
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
2425
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
2426
|
+
named.dev !== after.dev ||
|
|
2427
|
+
named.ino !== after.ino ||
|
|
2428
|
+
named.nlink !== after.nlink
|
|
2429
|
+
) {
|
|
2430
|
+
throw new Error("session_persistence_recovery_identity_mismatch");
|
|
2431
|
+
}
|
|
2432
|
+
} finally {
|
|
2433
|
+
fs.closeSync(fd);
|
|
2434
|
+
}
|
|
2435
|
+
const parentFd = fs.openSync(path.dirname(expected.canonicalPath), fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
|
|
2436
|
+
try {
|
|
2437
|
+
fs.fsyncSync(parentFd);
|
|
2438
|
+
} finally {
|
|
2439
|
+
fs.closeSync(parentFd);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2383
2442
|
/** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
|
|
2384
2443
|
function revalidateResumeSessionIdentity(
|
|
2385
2444
|
filePath: string,
|
|
@@ -2410,6 +2469,7 @@ function revalidateResumeSessionIdentity(
|
|
|
2410
2469
|
size: snapshot.stat.size,
|
|
2411
2470
|
mtimeMs: snapshot.stat.mtimeMs,
|
|
2412
2471
|
mtimeNs: snapshot.stat.mtimeNs,
|
|
2472
|
+
ctimeNs: snapshot.stat.ctimeNs,
|
|
2413
2473
|
sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
2414
2474
|
};
|
|
2415
2475
|
return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
|
|
@@ -4751,7 +4811,9 @@ export class SessionManager {
|
|
|
4751
4811
|
#recoveryPromotionTranscriptPath: string | undefined;
|
|
4752
4812
|
#memoryGuardParticipantIngressToken: symbol | undefined;
|
|
4753
4813
|
#fileEntries: FileEntry[] = [];
|
|
4754
|
-
#pendingStrictAdoption:
|
|
4814
|
+
#pendingStrictAdoption:
|
|
4815
|
+
| { canonicalPath: string; identity: ResumeSessionIdentity; inspection?: ResumeInspectionSnapshot }
|
|
4816
|
+
| undefined;
|
|
4755
4817
|
#byId: Map<string, SessionEntry> = new Map();
|
|
4756
4818
|
#labelsById: Map<string, string> = new Map();
|
|
4757
4819
|
#leafId: string | null = null;
|
|
@@ -5501,11 +5563,14 @@ export class SessionManager {
|
|
|
5501
5563
|
if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
5502
5564
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5503
5565
|
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
5504
|
-
throw new Error("Prepared
|
|
5566
|
+
throw new Error("Prepared session changed before strict adoption.");
|
|
5505
5567
|
}
|
|
5506
5568
|
let entries: FileEntry[];
|
|
5507
5569
|
let candidateMigrationApplied = false;
|
|
5508
|
-
if (
|
|
5570
|
+
if (strictAdoption?.inspection && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
5571
|
+
entries = strictAdoption.inspection.entries;
|
|
5572
|
+
candidateMigrationApplied = strictAdoption.inspection.migrationApplied;
|
|
5573
|
+
} else if (this.storage.existsSync(resolvedSessionFile)) {
|
|
5509
5574
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5510
5575
|
if ("kind" in inspected) throw new Error(`Could not switch session: ${inspected.reason}`);
|
|
5511
5576
|
entries = inspected.entries;
|
|
@@ -5516,7 +5581,7 @@ export class SessionManager {
|
|
|
5516
5581
|
if (strictAdoption) {
|
|
5517
5582
|
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5518
5583
|
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
5519
|
-
throw new Error("Prepared
|
|
5584
|
+
throw new Error("Prepared session changed during strict adoption.");
|
|
5520
5585
|
}
|
|
5521
5586
|
if (entries.length > 0) {
|
|
5522
5587
|
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
@@ -5541,6 +5606,13 @@ export class SessionManager {
|
|
|
5541
5606
|
prepared.dispose();
|
|
5542
5607
|
throw error;
|
|
5543
5608
|
}
|
|
5609
|
+
if (strictAdoption) {
|
|
5610
|
+
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
5611
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity)) {
|
|
5612
|
+
prepared.dispose();
|
|
5613
|
+
throw new Error("Prepared session changed before strict adoption commit.");
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5544
5616
|
const previous = {
|
|
5545
5617
|
sessionId: this.#sessionId,
|
|
5546
5618
|
sessionName: this.#sessionName,
|
|
@@ -8265,13 +8337,31 @@ export class SessionManager {
|
|
|
8265
8337
|
this.#assertRecoveryHydrationWritable();
|
|
8266
8338
|
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
8267
8339
|
const residentEntry = this.#prepareEntryForCurrentResidentStore(normalizedEntry) as SessionEntry;
|
|
8340
|
+
const previousLeafId = this.#leafId;
|
|
8341
|
+
const priorPersistenceError = this.#persistError;
|
|
8268
8342
|
this.#fileEntries.push(residentEntry);
|
|
8269
8343
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
8270
8344
|
this.#leafId = residentEntry.id;
|
|
8271
8345
|
this.#bumpEntryRevision();
|
|
8272
8346
|
this.#leafRevision++;
|
|
8273
8347
|
if (entry.type === "label") this.#labelRevision++;
|
|
8274
|
-
|
|
8348
|
+
try {
|
|
8349
|
+
this._persist(residentEntry);
|
|
8350
|
+
} catch (error) {
|
|
8351
|
+
const removed = this.#fileEntries.pop();
|
|
8352
|
+
if (removed !== residentEntry)
|
|
8353
|
+
throw new Error("Session append rollback lost resident ordering.", { cause: error });
|
|
8354
|
+
this.#byId.delete(residentEntry.id);
|
|
8355
|
+
this.#leafId = previousLeafId;
|
|
8356
|
+
this.#bumpEntryRevision();
|
|
8357
|
+
this.#leafRevision++;
|
|
8358
|
+
if (entry.type === "label") this.#labelRevision++;
|
|
8359
|
+
throw new SessionAppendPersistenceError(
|
|
8360
|
+
priorPersistenceError ? "prior_failure" : "current_append",
|
|
8361
|
+
residentEntry.id,
|
|
8362
|
+
this.#persistError ?? toError(error),
|
|
8363
|
+
);
|
|
8364
|
+
}
|
|
8275
8365
|
// Same validated, overflow-guarded aggregation as the resume path (#buildIndex): a
|
|
8276
8366
|
// malformed task-result or assistant usage shape reaching the append path must not
|
|
8277
8367
|
// crash or poison getUsageStatistics() either.
|
|
@@ -8513,6 +8603,50 @@ export class SessionManager {
|
|
|
8513
8603
|
this.#replayMetadataRevision++;
|
|
8514
8604
|
}
|
|
8515
8605
|
|
|
8606
|
+
/**
|
|
8607
|
+
* Rehydrate the canonical transcript after a synchronous persistence failure.
|
|
8608
|
+
*
|
|
8609
|
+
* The failed append may have committed before reporting an uncertain outcome,
|
|
8610
|
+
* so callers must not clear the sticky error or retry against the resident
|
|
8611
|
+
* branch. Reloading the exact session file is the only supported recovery
|
|
8612
|
+
* boundary for both managed and explicit persistent destinations.
|
|
8613
|
+
*/
|
|
8614
|
+
async recoverPersistenceFailure(): Promise<void> {
|
|
8615
|
+
const persistenceError = this.#persistError;
|
|
8616
|
+
if (!persistenceError) return;
|
|
8617
|
+
if (!this.#sessionFile) throw persistenceError;
|
|
8618
|
+
const sessionFile = this.#sessionFile;
|
|
8619
|
+
try {
|
|
8620
|
+
await this.#closePersistWriter();
|
|
8621
|
+
} catch {
|
|
8622
|
+
// A writer reports its prior write failure after a confirmed close. A
|
|
8623
|
+
// second close clears that terminal writer; retryable close failures are
|
|
8624
|
+
// genuinely re-dispatched and still fail if the descriptor cannot settle.
|
|
8625
|
+
}
|
|
8626
|
+
await this.#closePersistWriter();
|
|
8627
|
+
const inspected = inspectResumeSessionFile(sessionFile, this.storage);
|
|
8628
|
+
if ("kind" in inspected) throw persistenceError;
|
|
8629
|
+
try {
|
|
8630
|
+
fsyncResumeSessionIdentity(inspected.identity);
|
|
8631
|
+
} catch {
|
|
8632
|
+
throw persistenceError;
|
|
8633
|
+
}
|
|
8634
|
+
const durable = inspectResumeSessionFile(sessionFile, this.storage);
|
|
8635
|
+
if ("kind" in durable || !sameResumeIdentity(inspected.identity, durable.identity)) {
|
|
8636
|
+
throw persistenceError;
|
|
8637
|
+
}
|
|
8638
|
+
const adoption = {
|
|
8639
|
+
canonicalPath: durable.identity.canonicalPath,
|
|
8640
|
+
identity: durable.identity,
|
|
8641
|
+
inspection: durable,
|
|
8642
|
+
};
|
|
8643
|
+
this.#pendingStrictAdoption = adoption;
|
|
8644
|
+
try {
|
|
8645
|
+
await this.setSessionFile(durable.identity.canonicalPath);
|
|
8646
|
+
} finally {
|
|
8647
|
+
if (this.#pendingStrictAdoption === adoption) this.#pendingStrictAdoption = undefined;
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8516
8650
|
/**
|
|
8517
8651
|
* Rewrite the session file after in-place entry updates.
|
|
8518
8652
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
@@ -28,6 +28,7 @@ export interface SessionStorageStat {
|
|
|
28
28
|
size: number;
|
|
29
29
|
mtimeMs: number;
|
|
30
30
|
mtimeNs: bigint;
|
|
31
|
+
ctimeNs: bigint;
|
|
31
32
|
mtime: Date;
|
|
32
33
|
isFile: boolean;
|
|
33
34
|
}
|
|
@@ -47,6 +48,7 @@ function statFromNode(stats: fs.BigIntStats): SessionStorageStat {
|
|
|
47
48
|
size: Number(stats.size),
|
|
48
49
|
mtimeMs: Number(stats.mtimeMs),
|
|
49
50
|
mtimeNs: stats.mtimeNs,
|
|
51
|
+
ctimeNs: stats.ctimeNs,
|
|
50
52
|
mtime: stats.mtime,
|
|
51
53
|
isFile: stats.isFile(),
|
|
52
54
|
};
|
|
@@ -1624,6 +1626,7 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1624
1626
|
size: entry.content.byteLength,
|
|
1625
1627
|
mtimeMs: entry.mtimeMs,
|
|
1626
1628
|
mtimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
1629
|
+
ctimeNs: BigInt(entry.mtimeMs) * 1_000_000n,
|
|
1627
1630
|
mtime: new Date(entry.mtimeMs),
|
|
1628
1631
|
isFile: true,
|
|
1629
1632
|
};
|
|
@@ -417,6 +417,9 @@ function modelSelectionUsage(runtime: SlashCommandRuntime, currentModelLine?: st
|
|
|
417
417
|
.join("\n\n");
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/** Opt into the paste-a-code OAuth login for browsers that cannot reach this machine. */
|
|
421
|
+
const MANUAL_LOGIN_FLAG = "--manual";
|
|
422
|
+
|
|
420
423
|
const EFFORT_COMMAND_INPUT_HINT = "[inherit|off|minimal|low|medium|high|xhigh|max]";
|
|
421
424
|
const EFFORT_COMMAND_ACCEPTED_VALUES = ["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
422
425
|
|
|
@@ -1364,20 +1367,45 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1364
1367
|
{
|
|
1365
1368
|
name: "login",
|
|
1366
1369
|
description: "Login with OAuth provider",
|
|
1367
|
-
inlineHint: "[provider|redirect URL]",
|
|
1370
|
+
inlineHint: "[provider|redirect URL] [--manual]",
|
|
1368
1371
|
allowArgs: true,
|
|
1369
1372
|
handleTui: (command, runtime) => {
|
|
1370
1373
|
const manualInput = runtime.ctx.oauthManualInput;
|
|
1371
1374
|
const args = command.args.trim();
|
|
1375
|
+
const pendingLoginMessage = (): string => {
|
|
1376
|
+
const pendingProvider = manualInput.pendingProviderId;
|
|
1377
|
+
return pendingProvider
|
|
1378
|
+
? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
|
|
1379
|
+
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1380
|
+
};
|
|
1372
1381
|
if (args.length > 0) {
|
|
1382
|
+
const tokens = args.split(/\s+/);
|
|
1383
|
+
// `--manual` is resolved before the paste fallback below, otherwise
|
|
1384
|
+
// `/login anthropic --manual` would be submitted as an authorization code.
|
|
1385
|
+
if (tokens.includes(MANUAL_LOGIN_FLAG)) {
|
|
1386
|
+
const rest = tokens.filter(token => token !== MANUAL_LOGIN_FLAG);
|
|
1387
|
+
const requestedProvider = rest.length === 1 ? rest[0] : undefined;
|
|
1388
|
+
const manualProvider = requestedProvider
|
|
1389
|
+
? getOAuthProviders().find(provider => provider.id === requestedProvider)
|
|
1390
|
+
: undefined;
|
|
1391
|
+
if (!manualProvider) {
|
|
1392
|
+
runtime.ctx.showWarning(`Usage: /login <provider> ${MANUAL_LOGIN_FLAG}`);
|
|
1393
|
+
runtime.ctx.editor.setText("");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (manualInput.hasPending()) {
|
|
1397
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1398
|
+
runtime.ctx.editor.setText("");
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
void runtime.ctx.showOAuthSelector("login", manualProvider.id, { manualCode: true });
|
|
1402
|
+
runtime.ctx.editor.setText("");
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1373
1405
|
const matchedProvider = getOAuthProviders().find(provider => provider.id === args);
|
|
1374
1406
|
if (matchedProvider) {
|
|
1375
1407
|
if (manualInput.hasPending()) {
|
|
1376
|
-
|
|
1377
|
-
const message = pendingProvider
|
|
1378
|
-
? `OAuth login already in progress for ${pendingProvider}. Paste the redirect URL with /login <url>.`
|
|
1379
|
-
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1380
|
-
runtime.ctx.showWarning(message);
|
|
1408
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1381
1409
|
runtime.ctx.editor.setText("");
|
|
1382
1410
|
return;
|
|
1383
1411
|
}
|
|
@@ -1396,11 +1424,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1396
1424
|
}
|
|
1397
1425
|
|
|
1398
1426
|
if (manualInput.hasPending()) {
|
|
1399
|
-
|
|
1400
|
-
const message = provider
|
|
1401
|
-
? `OAuth login already in progress for ${provider}. Paste the redirect URL with /login <url>.`
|
|
1402
|
-
: "OAuth login already in progress. Paste the redirect URL with /login <url>.";
|
|
1403
|
-
runtime.ctx.showWarning(message);
|
|
1427
|
+
runtime.ctx.showWarning(pendingLoginMessage());
|
|
1404
1428
|
runtime.ctx.editor.setText("");
|
|
1405
1429
|
return;
|
|
1406
1430
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -245,10 +245,14 @@ function dedupeExactContextFiles(
|
|
|
245
245
|
// Keep the closest matching context entry when content is byte-for-byte identical.
|
|
246
246
|
lastIndexByContent.set(file.content, index);
|
|
247
247
|
}
|
|
248
|
-
|
|
249
248
|
return contextFiles.filter((file, index) => lastIndexByContent.get(file.content) === index);
|
|
250
249
|
}
|
|
251
250
|
|
|
251
|
+
export interface ProjectContextFilesResult {
|
|
252
|
+
contextFiles: Array<{ path: string; content: string; depth?: number }>;
|
|
253
|
+
warnings: string[];
|
|
254
|
+
}
|
|
255
|
+
|
|
252
256
|
/**
|
|
253
257
|
* Load all context files using the capability API.
|
|
254
258
|
* Returns {path, content, depth} entries for all discovered context files.
|
|
@@ -257,11 +261,10 @@ function dedupeExactContextFiles(
|
|
|
257
261
|
* prominent. User-home files from foreign providers (`~/.claude/CLAUDE.md`,
|
|
258
262
|
* `~/.codex/AGENTS.md`, …) stay excluded — only gjc's own user config applies.
|
|
259
263
|
*/
|
|
260
|
-
export async function
|
|
264
|
+
export async function loadProjectContextFilesResult(
|
|
261
265
|
options: LoadContextFilesOptions = {},
|
|
262
|
-
): Promise<
|
|
266
|
+
): Promise<ProjectContextFilesResult> {
|
|
263
267
|
const resolvedCwd = options.cwd ?? getProjectDir();
|
|
264
|
-
|
|
265
268
|
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
266
269
|
const items = result.items as ContextFile[];
|
|
267
270
|
|
|
@@ -288,7 +291,17 @@ export async function loadProjectContextFiles(
|
|
|
288
291
|
return depthB - depthA;
|
|
289
292
|
});
|
|
290
293
|
|
|
291
|
-
return
|
|
294
|
+
return {
|
|
295
|
+
contextFiles: dedupeExactContextFiles([...userFiles, ...projectFiles]),
|
|
296
|
+
warnings: result.warnings,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Load project context files without exposing discovery diagnostics. */
|
|
301
|
+
export async function loadProjectContextFiles(
|
|
302
|
+
options: LoadContextFilesOptions = {},
|
|
303
|
+
): Promise<Array<{ path: string; content: string; depth?: number }>> {
|
|
304
|
+
return (await loadProjectContextFilesResult(options)).contextFiles;
|
|
292
305
|
}
|
|
293
306
|
|
|
294
307
|
/**
|
|
@@ -386,10 +399,11 @@ export interface BuildSystemPromptOptions {
|
|
|
386
399
|
subagent?: boolean;
|
|
387
400
|
}
|
|
388
401
|
|
|
389
|
-
/** Result of building provider-facing system prompt messages. */
|
|
390
402
|
export interface BuildSystemPromptResult {
|
|
391
403
|
/** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
|
|
392
404
|
systemPrompt: string[];
|
|
405
|
+
/** Context-file discovery warnings visible to SDK and session callers. */
|
|
406
|
+
warnings: string[];
|
|
393
407
|
}
|
|
394
408
|
export interface BuildVolatileProjectContextOptions {
|
|
395
409
|
cwd?: string;
|
|
@@ -421,7 +435,7 @@ export function buildVolatileProjectContext(options: BuildVolatileProjectContext
|
|
|
421
435
|
/** Build the system prompt with tools, guidelines, and context */
|
|
422
436
|
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
|
|
423
437
|
if ($env.NULL_PROMPT === "true") {
|
|
424
|
-
return { systemPrompt: [] };
|
|
438
|
+
return { systemPrompt: [], warnings: [] };
|
|
425
439
|
}
|
|
426
440
|
|
|
427
441
|
const {
|
|
@@ -448,7 +462,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
448
462
|
resolvedCustomPrompt: undefined as string | undefined,
|
|
449
463
|
resolvedAppendPrompt: undefined as string | undefined,
|
|
450
464
|
systemPromptCustomization: null as string | null,
|
|
451
|
-
contextFiles:
|
|
465
|
+
contextFiles: { contextFiles: [] as Array<{ path: string; content: string; depth?: number }>, warnings: [] },
|
|
452
466
|
workspaceTree: {
|
|
453
467
|
rootPath: resolvedCwd,
|
|
454
468
|
rendered: "",
|
|
@@ -490,8 +504,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
490
504
|
cwd: resolvedCwd,
|
|
491
505
|
});
|
|
492
506
|
const contextFilesPromise = providedContextFiles
|
|
493
|
-
? Promise.resolve(providedContextFiles)
|
|
494
|
-
: logger.time("loadProjectContextFiles",
|
|
507
|
+
? Promise.resolve({ contextFiles: providedContextFiles, warnings: [] })
|
|
508
|
+
: logger.time("loadProjectContextFiles", loadProjectContextFilesResult, { cwd: resolvedCwd });
|
|
495
509
|
const workspaceTreePromise =
|
|
496
510
|
providedWorkspaceTree !== undefined
|
|
497
511
|
? Promise.resolve(providedWorkspaceTree)
|
|
@@ -499,7 +513,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
499
513
|
buildWorkspaceTree(resolvedCwd, { timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS }),
|
|
500
514
|
);
|
|
501
515
|
|
|
502
|
-
const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization,
|
|
516
|
+
const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFileResult, workspaceTree] =
|
|
503
517
|
await Promise.all([
|
|
504
518
|
withDeadline(
|
|
505
519
|
"customPrompt",
|
|
@@ -516,11 +530,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
516
530
|
systemPromptCustomizationPromise,
|
|
517
531
|
prepDefaults.systemPromptCustomization,
|
|
518
532
|
),
|
|
519
|
-
withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles)
|
|
520
|
-
dedupeExactContextFiles,
|
|
521
|
-
),
|
|
533
|
+
withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles),
|
|
522
534
|
withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
|
|
523
535
|
]);
|
|
536
|
+
const contextFiles = dedupeExactContextFiles(contextFileResult.contextFiles);
|
|
524
537
|
const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT);
|
|
525
538
|
|
|
526
539
|
if (timedOut.length > 0) {
|
|
@@ -628,5 +641,5 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
628
641
|
systemPrompt.push(pluginAppendices.trim());
|
|
629
642
|
}
|
|
630
643
|
|
|
631
|
-
return { systemPrompt };
|
|
644
|
+
return { systemPrompt, warnings: contextFileResult.warnings };
|
|
632
645
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -982,8 +982,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
982
982
|
}
|
|
983
983
|
}
|
|
984
984
|
|
|
985
|
+
const activeModel = this.session.model;
|
|
985
986
|
const composerPolicy = checkComposerBashPolicy({
|
|
986
|
-
modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ??
|
|
987
|
+
modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ?? activeModel?.id,
|
|
988
|
+
provider: activeModel?.provider,
|
|
987
989
|
commands: rawCommand === command ? [command] : [rawCommand, command],
|
|
988
990
|
});
|
|
989
991
|
if (!composerPolicy.allowed) {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ComposerBashPolicyToolSurface,
|
|
3
|
+
formatComposerBashPolicyError,
|
|
4
|
+
isComposerHarnessModel,
|
|
5
|
+
} from "@gajae-code/ai/providers/composer-discipline";
|
|
2
6
|
|
|
3
|
-
export const COMPOSER_BASH_POLICY_ERROR =
|
|
4
|
-
|
|
7
|
+
export const COMPOSER_BASH_POLICY_ERROR = formatComposerBashPolicyError("generic");
|
|
8
|
+
export const CURSOR_COMPOSER_BASH_POLICY_ERROR = formatComposerBashPolicyError("cursor");
|
|
5
9
|
|
|
6
10
|
type ComposerBashPolicyResult =
|
|
7
11
|
| { allowed: true }
|
|
@@ -9,6 +13,7 @@ type ComposerBashPolicyResult =
|
|
|
9
13
|
allowed: false;
|
|
10
14
|
reason: string;
|
|
11
15
|
message: string;
|
|
16
|
+
surface: ComposerBashPolicyToolSurface;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
const BLOCK_PATTERNS: Array<{ id: string; pattern: RegExp }> = [
|
|
@@ -77,19 +82,30 @@ export function isComposerBashPolicyModel(modelId: string | undefined): boolean
|
|
|
77
82
|
return Boolean(modelId && isComposerHarnessModel(modelId));
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
export function resolveComposerBashPolicyToolSurface(input: {
|
|
86
|
+
modelId?: string;
|
|
87
|
+
provider?: string;
|
|
88
|
+
}): ComposerBashPolicyToolSurface {
|
|
89
|
+
if (input.provider?.toLowerCase() === "cursor") return "cursor";
|
|
90
|
+
return /(?:^|[/:._-])cursor(?:[/:._-]|$)/i.test(input.modelId ?? "") ? "cursor" : "generic";
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
export function checkComposerBashPolicy(input: {
|
|
81
94
|
modelId?: string;
|
|
95
|
+
provider?: string;
|
|
82
96
|
commands: readonly string[];
|
|
83
97
|
}): ComposerBashPolicyResult {
|
|
84
98
|
if (!isComposerBashPolicyModel(input.modelId)) return { allowed: true };
|
|
99
|
+
const surface = resolveComposerBashPolicyToolSurface(input);
|
|
100
|
+
const message = formatComposerBashPolicyError(surface);
|
|
85
101
|
for (const command of input.commands) {
|
|
86
102
|
for (const block of BLOCK_PATTERNS) {
|
|
87
103
|
if (block.pattern.test(command)) {
|
|
88
|
-
return { allowed: false, reason: block.id, message
|
|
104
|
+
return { allowed: false, reason: block.id, message, surface };
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
if (!isAllowedComposerTerminalCommand(command)) {
|
|
92
|
-
return { allowed: false, reason: "not-allowlisted", message
|
|
108
|
+
return { allowed: false, reason: "not-allowlisted", message, surface };
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
return { allowed: true };
|