@gajae-code/coding-agent 0.11.8 → 0.11.10
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 +35 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/runtime-mcp/redaction.d.ts +2 -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/index.d.ts +8 -1
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +3 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +9 -10
- package/src/config/model-registry.ts +4 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +843 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +60 -5
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +685 -120
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +12 -0
- package/src/session/agent-session.ts +140 -10
- package/src/session/internal/managed-session-scope.ts +339 -228
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +154 -44
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +3 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
} from "./native-publish-outcome";
|
|
27
27
|
|
|
28
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
29
|
-
export const MANAGED_ARTIFACT_MAX_FILES =
|
|
29
|
+
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
30
30
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
31
31
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
32
|
+
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
32
33
|
const LOCK_LEASE_MS = 60_000;
|
|
33
34
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
34
35
|
const LOCK_WAIT_MS = 5_000;
|
|
@@ -120,6 +121,7 @@ export type ManagedStorageFailure =
|
|
|
120
121
|
| "destination_conflict"
|
|
121
122
|
| "source_changed"
|
|
122
123
|
| "unsafe_artifacts"
|
|
124
|
+
| "artifact_capacity_exceeded"
|
|
123
125
|
| "durability_failed"
|
|
124
126
|
| "atomic_unavailable"
|
|
125
127
|
| "durability_not_provable"
|
|
@@ -1638,8 +1640,20 @@ export async function acquireManagedLock(
|
|
|
1638
1640
|
}
|
|
1639
1641
|
}
|
|
1640
1642
|
|
|
1643
|
+
export interface ManagedArtifactTreeLimits {
|
|
1644
|
+
maxFiles?: number;
|
|
1645
|
+
maxTotalBytes?: number;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1641
1648
|
/** Bounds a no-follow artifact tree before it can be copied or deleted. */
|
|
1642
|
-
export function validateManagedArtifactTree(root: string): void {
|
|
1649
|
+
export function validateManagedArtifactTree(root: string, limits: ManagedArtifactTreeLimits = {}): void {
|
|
1650
|
+
const clampLimit = (limit: number | undefined, maximum: number): number => {
|
|
1651
|
+
if (limit === undefined) return maximum;
|
|
1652
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit <= 0) throw new Error("unsafe_artifacts");
|
|
1653
|
+
return Math.min(limit, maximum);
|
|
1654
|
+
};
|
|
1655
|
+
const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
|
|
1656
|
+
const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
|
|
1643
1657
|
let files = 0;
|
|
1644
1658
|
let bytes = 0;
|
|
1645
1659
|
const visit = (directory: string, depth: number): void => {
|
|
@@ -1656,8 +1670,7 @@ export function validateManagedArtifactTree(root: string): void {
|
|
|
1656
1670
|
if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
|
|
1657
1671
|
files++;
|
|
1658
1672
|
bytes += stat.size;
|
|
1659
|
-
if (files >
|
|
1660
|
-
throw new Error("unsafe_artifacts");
|
|
1673
|
+
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
1661
1674
|
}
|
|
1662
1675
|
};
|
|
1663
1676
|
const rootStat = fs.lstatSync(root);
|
|
@@ -547,6 +547,13 @@ export class SessionMigrationPolicyError extends Error {
|
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
+
export class SessionArtifactCapacityError extends Error {
|
|
551
|
+
constructor(message: string) {
|
|
552
|
+
super(message);
|
|
553
|
+
this.name = "SessionArtifactCapacityError";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
550
557
|
export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
|
|
551
558
|
export interface StrictSessionOpenSuccess {
|
|
552
559
|
kind: "opened";
|
|
@@ -555,7 +562,8 @@ export interface StrictSessionOpenSuccess {
|
|
|
555
562
|
|
|
556
563
|
export interface StrictSessionOpenFailure {
|
|
557
564
|
kind: "error";
|
|
558
|
-
reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required";
|
|
565
|
+
reason: ResumeTailError["reason"] | "identity-mismatch" | "migration-required" | "artifact_capacity_exceeded";
|
|
566
|
+
message?: string;
|
|
559
567
|
}
|
|
560
568
|
|
|
561
569
|
/** Exact transcript bytes and authority captured from one descriptor-bound read. */
|
|
@@ -2110,41 +2118,90 @@ export async function recoverOrphanedBackups(sessionDir: string, storage: Sessio
|
|
|
2110
2118
|
}
|
|
2111
2119
|
|
|
2112
2120
|
/**
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2121
|
+
* Returns session metadata sorted by mtime (newest first).
|
|
2122
|
+
*
|
|
2123
|
+
* Directory entries are cheap to stat, but opening every transcript before applying a
|
|
2124
|
+
* small caller limit makes welcome-screen startup scale with the entire session history.
|
|
2125
|
+
* Rank paths first, then read bounded batches until the requested number of valid
|
|
2126
|
+
* sessions has been found. Invalid or future-version files do not prevent older valid
|
|
2127
|
+
* sessions from filling the result.
|
|
2116
2128
|
*/
|
|
2117
|
-
async function getSortedSessions(
|
|
2129
|
+
async function getSortedSessions(
|
|
2130
|
+
sessionDir: string,
|
|
2131
|
+
storage: SessionStorage,
|
|
2132
|
+
limit?: number,
|
|
2133
|
+
): Promise<RecentSessionInfo[]> {
|
|
2118
2134
|
await recoverOrphanedBackups(sessionDir, storage);
|
|
2119
2135
|
try {
|
|
2120
|
-
|
|
2136
|
+
let candidates: Array<{ path: string; mtime: number }> | undefined;
|
|
2137
|
+
if (storage.listFilesByMtime) {
|
|
2138
|
+
try {
|
|
2139
|
+
candidates = (await storage.listFilesByMtime(sessionDir, "*.jsonl")).map(candidate => ({
|
|
2140
|
+
path: candidate.path,
|
|
2141
|
+
mtime: candidate.mtimeMs,
|
|
2142
|
+
}));
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
logger.warn("Native session mtime listing failed; using JavaScript fallback", {
|
|
2145
|
+
sessionDir,
|
|
2146
|
+
error: toError(error).message,
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
logger.debug("Native session mtime listing unavailable; using JavaScript fallback", { sessionDir });
|
|
2151
|
+
}
|
|
2152
|
+
candidates ??= storage.listFilesSync(sessionDir, "*.jsonl").flatMap(path => {
|
|
2153
|
+
try {
|
|
2154
|
+
return [{ path, mtime: storage.statSync(path).mtimeMs }];
|
|
2155
|
+
} catch {
|
|
2156
|
+
return [];
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
candidates.sort((left, right) => {
|
|
2160
|
+
if (left.mtime !== right.mtime) return right.mtime - left.mtime;
|
|
2161
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2121
2164
|
const sessions: RecentSessionInfo[] = [];
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2165
|
+
const batchSize = Math.max(32, limit ?? 0);
|
|
2166
|
+
|
|
2167
|
+
for (
|
|
2168
|
+
let offset = 0;
|
|
2169
|
+
offset < candidates.length && (limit === undefined || sessions.length < limit);
|
|
2170
|
+
offset += batchSize
|
|
2171
|
+
) {
|
|
2172
|
+
const batch = candidates.slice(offset, offset + batchSize);
|
|
2173
|
+
const parsed = await Promise.all(
|
|
2174
|
+
batch.map(async candidate => {
|
|
2175
|
+
try {
|
|
2176
|
+
const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
|
|
2177
|
+
const content = await readSessionListPrefix(candidate.path, storage, buffer);
|
|
2178
|
+
const entries = parseJsonlLenient<Record<string, unknown>>(content);
|
|
2179
|
+
if (entries.length === 0) return undefined;
|
|
2180
|
+
const header = entries[0] as Record<string, unknown>;
|
|
2181
|
+
if (
|
|
2182
|
+
header.type !== "session" ||
|
|
2183
|
+
typeof header.id !== "string" ||
|
|
2184
|
+
!isSupportedSessionVersion(header.version)
|
|
2185
|
+
)
|
|
2186
|
+
return undefined;
|
|
2187
|
+
if (typeof header.version === "number" && header.version >= 4) {
|
|
2188
|
+
for (const patch of await readSessionListTrailingPatches(candidate.path, storage, buffer)) {
|
|
2189
|
+
applySessionListHeaderPatch(header as unknown as SessionListHeader, patch);
|
|
2190
|
+
}
|
|
2139
2191
|
}
|
|
2192
|
+
const firstPrompt = header.title ? undefined : extractFirstUserPrompt(entries);
|
|
2193
|
+
return new RecentSessionInfo(candidate.path, candidate.mtime, header, firstPrompt);
|
|
2194
|
+
} catch {
|
|
2195
|
+
return undefined;
|
|
2140
2196
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2197
|
+
}),
|
|
2198
|
+
);
|
|
2199
|
+
for (const session of parsed) {
|
|
2200
|
+
if (session) sessions.push(session);
|
|
2201
|
+
if (limit !== undefined && sessions.length >= limit) break;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return sessions;
|
|
2148
2205
|
} catch {
|
|
2149
2206
|
return [];
|
|
2150
2207
|
}
|
|
@@ -2155,7 +2212,7 @@ export async function findMostRecentSession(
|
|
|
2155
2212
|
sessionDir: string,
|
|
2156
2213
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2157
2214
|
): Promise<string | null> {
|
|
2158
|
-
const sessions = await getSortedSessions(sessionDir, storage);
|
|
2215
|
+
const sessions = await getSortedSessions(sessionDir, storage, 1);
|
|
2159
2216
|
return sessions[0]?.path || null;
|
|
2160
2217
|
}
|
|
2161
2218
|
|
|
@@ -3427,8 +3484,7 @@ export async function getRecentSessions(
|
|
|
3427
3484
|
limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
|
|
3428
3485
|
storage: SessionStorage = new FileSessionStorage(),
|
|
3429
3486
|
): Promise<RecentSessionInfo[]> {
|
|
3430
|
-
|
|
3431
|
-
return sessions.slice(0, limit);
|
|
3487
|
+
return getSortedSessions(sessionDir, storage, limit);
|
|
3432
3488
|
}
|
|
3433
3489
|
|
|
3434
3490
|
/**
|
|
@@ -7299,6 +7355,7 @@ export class SessionManager {
|
|
|
7299
7355
|
if (opened.kind === "error") {
|
|
7300
7356
|
managedDestinationStore.assertBound();
|
|
7301
7357
|
if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7358
|
+
if (opened.code === "artifact_capacity_exceeded") throw new SessionArtifactCapacityError(opened.message);
|
|
7302
7359
|
throw new Error(`Could not open managed session: ${opened.message}`);
|
|
7303
7360
|
}
|
|
7304
7361
|
assertManagedDestinationBound();
|
|
@@ -7809,6 +7866,8 @@ export class SessionManager {
|
|
|
7809
7866
|
} catch (error) {
|
|
7810
7867
|
if (error instanceof SessionMigrationPolicyError)
|
|
7811
7868
|
return { kind: "error", reason: "legacy_migration_disabled" };
|
|
7869
|
+
if (error instanceof SessionArtifactCapacityError)
|
|
7870
|
+
return { kind: "error", reason: "artifact_capacity_exceeded", message: error.message };
|
|
7812
7871
|
if (
|
|
7813
7872
|
error instanceof Error &&
|
|
7814
7873
|
(error.message.includes("source_changed") || error.message.includes("changed before migration"))
|
|
@@ -7867,6 +7926,10 @@ export class SessionManager {
|
|
|
7867
7926
|
);
|
|
7868
7927
|
if (opened.kind === "error") {
|
|
7869
7928
|
if (opened.reason === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7929
|
+
if (opened.reason === "artifact_capacity_exceeded")
|
|
7930
|
+
throw new SessionArtifactCapacityError(
|
|
7931
|
+
opened.message ?? "Session artifacts exceed the migration capacity.",
|
|
7932
|
+
);
|
|
7870
7933
|
return undefined;
|
|
7871
7934
|
}
|
|
7872
7935
|
return opened.manager;
|
|
@@ -167,6 +167,8 @@ export interface SessionStorage {
|
|
|
167
167
|
readSnapshotSync?(path: string): SessionStorageSnapshot;
|
|
168
168
|
statSync(path: string): SessionStorageStat;
|
|
169
169
|
listFilesSync(dir: string, pattern: string): string[];
|
|
170
|
+
/** List matching files with mtimes without issuing one JavaScript stat call per path. */
|
|
171
|
+
listFilesByMtime?(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>>;
|
|
170
172
|
/**
|
|
171
173
|
* Strict directory scan that never suppresses scan/root errors. Used by strict
|
|
172
174
|
* authorization inventory; the forgiving {@link listFilesSync} stays display-only.
|
|
@@ -705,6 +707,21 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
705
707
|
return [];
|
|
706
708
|
}
|
|
707
709
|
}
|
|
710
|
+
async listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
|
|
711
|
+
const result = await native.glob({
|
|
712
|
+
path: dir,
|
|
713
|
+
pattern,
|
|
714
|
+
fileType: native.FileType.File,
|
|
715
|
+
recursive: false,
|
|
716
|
+
hidden: false,
|
|
717
|
+
gitignore: false,
|
|
718
|
+
sortByMtime: true,
|
|
719
|
+
});
|
|
720
|
+
return result.matches.map(match => ({
|
|
721
|
+
path: path.join(dir, match.path),
|
|
722
|
+
mtimeMs: match.mtime ?? 0,
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
708
725
|
|
|
709
726
|
listFilesStrictSync(dir: string, pattern: string): string[] {
|
|
710
727
|
// Strict: never suppress scan/root errors. Authorization inventory depends on
|
|
@@ -1470,6 +1487,12 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
1470
1487
|
}
|
|
1471
1488
|
return files;
|
|
1472
1489
|
}
|
|
1490
|
+
listFilesByMtime(dir: string, pattern: string): Promise<Array<{ path: string; mtimeMs: number }>> {
|
|
1491
|
+
const files = this.listFilesSync(dir, pattern)
|
|
1492
|
+
.map(path => ({ path, mtimeMs: this.statSync(path).mtimeMs }))
|
|
1493
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1494
|
+
return Promise.resolve(files);
|
|
1495
|
+
}
|
|
1473
1496
|
listFilesStrictSync(dir: string, pattern: string): string[] {
|
|
1474
1497
|
// In-memory scan never suppresses; identical to the display scan.
|
|
1475
1498
|
return this.listFilesSync(dir, pattern);
|
|
@@ -71,6 +71,8 @@ export interface SkillActiveEntry {
|
|
|
71
71
|
handoff_at?: string;
|
|
72
72
|
active_subskills?: ActiveSubskillEntry[];
|
|
73
73
|
source_state_revision?: number;
|
|
74
|
+
/** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
|
|
75
|
+
committed_mode_state_revision?: number;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
export interface SkillActiveState {
|
|
@@ -114,6 +116,8 @@ export interface SyncSkillActiveStateOptions {
|
|
|
114
116
|
handoff_at?: string;
|
|
115
117
|
active_subskills?: ActiveSubskillEntry[];
|
|
116
118
|
sourceRevision?: number;
|
|
119
|
+
/** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
|
|
120
|
+
committedModeRevision?: number;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
const HUD_TEXT_LIMIT = 80;
|
|
@@ -785,6 +789,9 @@ async function persistActiveEntry(
|
|
|
785
789
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
786
790
|
cwd,
|
|
787
791
|
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
792
|
+
...(typeof entry.committed_mode_state_revision === "number"
|
|
793
|
+
? { orderingRevision: entry.committed_mode_state_revision }
|
|
794
|
+
: {}),
|
|
788
795
|
});
|
|
789
796
|
}
|
|
790
797
|
}
|
|
@@ -863,6 +870,9 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
863
870
|
? { active_subskills: preservedActiveSubskills }
|
|
864
871
|
: {}),
|
|
865
872
|
...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
|
|
873
|
+
...(typeof options.committedModeRevision === "number"
|
|
874
|
+
? { committed_mode_state_revision: options.committedModeRevision }
|
|
875
|
+
: {}),
|
|
866
876
|
};
|
|
867
877
|
const sessionScope = { sessionId: options.sessionId };
|
|
868
878
|
await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
|
|
@@ -35,17 +35,28 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
38
|
+
/**
|
|
39
|
+
* Only `/dev/null` is exempt. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>` are descriptor
|
|
40
|
+
* aliases: `exec 1<>src/product.ts; printf x >/dev/stdout` reaches a real repository file
|
|
41
|
+
* through a rebound descriptor, so they must stay blocked.
|
|
42
|
+
*/
|
|
43
|
+
const DEVICE_SINK_PATHS = new Set(["/dev/null"]);
|
|
44
|
+
/** Bash write forms the plain `>`/`>>` scanner misses: clobber, dup-to-path, and read-write open. */
|
|
45
|
+
const BASH_EXTENDED_WRITE_RE = /(?:>\||<>|>&(?![\s;&|]*(?:\d+|-)(?:[\s;&|]|$)))\s*([^\s;&|]+)/g;
|
|
46
|
+
/** Descriptor rebinding is not statically resolvable; any `exec` redirection forces a fail-closed verdict. */
|
|
47
|
+
const BASH_EXEC_REDIRECT_RE = /(?:^|[;&|\n])\s*(?:sudo\s+)?exec\b[^;&|\n]*[<>]/i;
|
|
48
|
+
|
|
38
49
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
39
50
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
40
51
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
41
52
|
const BASH_MUTATION_COMMAND_RE =
|
|
42
|
-
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
53
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
43
54
|
const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
|
|
44
55
|
const BASH_OPAQUE_INTERPRETER_WRITE_RE =
|
|
45
56
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
46
57
|
const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
|
|
47
58
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
48
|
-
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
|
|
59
|
+
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?dd\b([^;&|\n]*)/gi;
|
|
49
60
|
|
|
50
61
|
type ToolWithEditMode = AgentTool & {
|
|
51
62
|
mode?: unknown;
|
|
@@ -371,6 +382,10 @@ function cleanShellWord(value: string): string {
|
|
|
371
382
|
return value.replace(/^['"]|['"]$/g, "");
|
|
372
383
|
}
|
|
373
384
|
|
|
385
|
+
function isDeviceSinkPath(value: string): boolean {
|
|
386
|
+
return DEVICE_SINK_PATHS.has(cleanShellWord(value));
|
|
387
|
+
}
|
|
388
|
+
|
|
374
389
|
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
375
390
|
const record = getRecord(args);
|
|
376
391
|
const command = safeString(record?.command);
|
|
@@ -382,11 +397,25 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
382
397
|
if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
|
|
383
398
|
targets.unknown = true;
|
|
384
399
|
}
|
|
400
|
+
// `exec 1<>file` rebinds a descriptor, so a later `>/dev/null` may not reach the device at all.
|
|
401
|
+
if (BASH_EXEC_REDIRECT_RE.test(command)) {
|
|
402
|
+
targets.unknown = true;
|
|
403
|
+
}
|
|
404
|
+
for (const match of command.matchAll(BASH_EXTENDED_WRITE_RE)) {
|
|
405
|
+
const cleaned = cleanShellWord(match[1] ?? "");
|
|
406
|
+
if (!cleaned) targets.unknown = true;
|
|
407
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
408
|
+
}
|
|
385
409
|
for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
|
|
386
410
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
411
|
+
// Every `of=` counts: GNU dd honors the last one, so `of=/dev/null of=real.ts` writes real.ts.
|
|
412
|
+
const outputs = parts.filter(part => part.startsWith("of="));
|
|
413
|
+
if (outputs.length === 0) targets.unknown = true;
|
|
414
|
+
for (const output of outputs) {
|
|
415
|
+
const outputPath = cleanShellWord(output.slice(3));
|
|
416
|
+
if (!outputPath) targets.unknown = true;
|
|
417
|
+
else if (!isDeviceSinkPath(outputPath)) addPath(targets, outputPath);
|
|
418
|
+
}
|
|
390
419
|
}
|
|
391
420
|
for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
|
|
392
421
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
@@ -399,12 +428,17 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
399
428
|
for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
|
|
400
429
|
const redirected = match[2]?.trim();
|
|
401
430
|
if (redirected) {
|
|
402
|
-
|
|
431
|
+
const cleaned = cleanShellWord(redirected);
|
|
432
|
+
// A capture that dequotes to nothing (e.g. `>" src.ts"`) is a truncated parse, not a safe no-op.
|
|
433
|
+
if (!cleaned) targets.unknown = true;
|
|
434
|
+
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
403
435
|
continue;
|
|
404
436
|
}
|
|
405
437
|
const parts = shellWords(match[1] ?? "");
|
|
406
438
|
const commandName = match[0]
|
|
407
|
-
?.match(
|
|
439
|
+
?.match(
|
|
440
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i,
|
|
441
|
+
)?.[1]
|
|
408
442
|
?.toLowerCase();
|
|
409
443
|
const targetParts =
|
|
410
444
|
commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
|