gsd-pi 2.37.1-dev.d3ace49 → 2.38.0-dev.63ad7e5
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/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
- package/dist/resources/extensions/gsd/auto-loop.js +68 -97
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -71
- package/dist/resources/extensions/gsd/auto-prompts.js +7 -31
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +2 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/index.js +2 -1
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +0 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -11
- package/dist/resources/extensions/gsd/preferences.js +5 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
- package/src/resources/extensions/gsd/auto-loop.ts +88 -133
- package/src/resources/extensions/gsd/auto-post-unit.ts +52 -42
- package/src/resources/extensions/gsd/auto-prompts.ts +7 -33
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +0 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -11
- package/src/resources/extensions/gsd/preferences.ts +5 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getMainBranch,
|
|
12
12
|
getSliceBranchName,
|
|
13
13
|
parseSliceBranch,
|
|
14
|
+
resolveProjectRoot,
|
|
14
15
|
setActiveMilestoneId,
|
|
15
16
|
SLICE_BRANCH_RE,
|
|
16
17
|
} from "../worktree.ts";
|
|
@@ -165,6 +166,52 @@ async function main(): Promise<void> {
|
|
|
165
166
|
rmSync(repo, { recursive: true, force: true });
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
// ── detectWorktreeName: symlink-resolved paths ───────────────────────────
|
|
170
|
+
console.log("\n=== detectWorktreeName (symlink-resolved paths) ===");
|
|
171
|
+
assertEq(
|
|
172
|
+
detectWorktreeName("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
173
|
+
"M001",
|
|
174
|
+
"detects milestone in symlink-resolved path",
|
|
175
|
+
);
|
|
176
|
+
assertEq(
|
|
177
|
+
detectWorktreeName("/Users/fran/.gsd/projects/abc123/worktrees/M002/subdir"),
|
|
178
|
+
"M002",
|
|
179
|
+
"detects milestone with trailing subdir in symlink-resolved path",
|
|
180
|
+
);
|
|
181
|
+
assertEq(
|
|
182
|
+
detectWorktreeName("/Users/fran/.gsd/projects/abc123"),
|
|
183
|
+
null,
|
|
184
|
+
"returns null for project root without worktrees segment",
|
|
185
|
+
);
|
|
186
|
+
assertEq(
|
|
187
|
+
detectWorktreeName("/foo/.gsd/worktrees/M001"),
|
|
188
|
+
"M001",
|
|
189
|
+
"still detects direct layout path",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ── resolveProjectRoot: symlink-resolved paths ──────────────────────────
|
|
193
|
+
console.log("\n=== resolveProjectRoot (symlink-resolved paths) ===");
|
|
194
|
+
assertEq(
|
|
195
|
+
resolveProjectRoot("/Users/fran/.gsd/projects/89e1c9ad49bf/worktrees/M001"),
|
|
196
|
+
"/Users/fran",
|
|
197
|
+
"resolves to user home for symlink-resolved path",
|
|
198
|
+
);
|
|
199
|
+
assertEq(
|
|
200
|
+
resolveProjectRoot("/foo/.gsd/worktrees/M001"),
|
|
201
|
+
"/foo",
|
|
202
|
+
"still resolves direct layout path",
|
|
203
|
+
);
|
|
204
|
+
assertEq(
|
|
205
|
+
resolveProjectRoot("/some/repo"),
|
|
206
|
+
"/some/repo",
|
|
207
|
+
"returns unchanged for non-worktree path",
|
|
208
|
+
);
|
|
209
|
+
assertEq(
|
|
210
|
+
resolveProjectRoot("/data/.gsd/projects/deadbeef/worktrees/M003/nested"),
|
|
211
|
+
"/data",
|
|
212
|
+
"resolves correctly with nested subdirs after worktree name",
|
|
213
|
+
);
|
|
214
|
+
|
|
168
215
|
rmSync(base, { recursive: true, force: true });
|
|
169
216
|
report();
|
|
170
217
|
}
|
|
@@ -423,7 +423,6 @@ export interface Requirement {
|
|
|
423
423
|
|
|
424
424
|
// ─── Parallel Orchestration Types ────────────────────────────────────────
|
|
425
425
|
|
|
426
|
-
export type CompressionStrategy = "truncate" | "compress";
|
|
427
426
|
export type ContextSelectionMode = "full" | "smart";
|
|
428
427
|
|
|
429
428
|
export type MergeStrategy = "per-slice" | "per-milestone";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { deriveState } from './state.js';
|
|
5
5
|
import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
|
|
6
|
-
import { findMilestoneIds } from './
|
|
6
|
+
import { findMilestoneIds } from './milestone-ids.js';
|
|
7
7
|
import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
|
|
8
8
|
import {
|
|
9
9
|
getLedger,
|
|
@@ -67,40 +67,60 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string,
|
|
|
67
67
|
|
|
68
68
|
// ─── Pure Utility Functions (unchanged) ────────────────────────────────────
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Find the worktrees segment in a path, supporting both direct
|
|
72
|
+
* (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
|
|
73
|
+
* layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
|
|
74
|
+
* paths contain the intermediate `projects/<hash>/` segment that the old
|
|
75
|
+
* single-marker check missed.
|
|
76
|
+
*/
|
|
77
|
+
function findWorktreeSegment(normalizedPath: string): { gsdIdx: number; afterWorktrees: number } | null {
|
|
78
|
+
// Direct layout: /.gsd/worktrees/<name>
|
|
79
|
+
const directMarker = "/.gsd/worktrees/";
|
|
80
|
+
const idx = normalizedPath.indexOf(directMarker);
|
|
81
|
+
if (idx !== -1) {
|
|
82
|
+
return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
|
|
83
|
+
}
|
|
84
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
|
|
85
|
+
const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
|
|
86
|
+
const match = normalizedPath.match(symlinkRe);
|
|
87
|
+
if (match && match.index !== undefined) {
|
|
88
|
+
return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
/**
|
|
71
94
|
* Detect the active worktree name from the current working directory.
|
|
72
95
|
* Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
|
|
73
96
|
*/
|
|
74
97
|
export function detectWorktreeName(basePath: string): string | null {
|
|
75
98
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const afterMarker = normalizedPath.slice(idx + marker.length);
|
|
99
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
100
|
+
if (!seg) return null;
|
|
101
|
+
const afterMarker = normalizedPath.slice(seg.afterWorktrees);
|
|
80
102
|
const name = afterMarker.split("/")[0];
|
|
81
103
|
return name || null;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
/**
|
|
85
107
|
* Resolve the project root from a path that may be inside a worktree.
|
|
86
|
-
* If the path contains
|
|
87
|
-
*
|
|
108
|
+
* If the path contains a worktrees segment, returns the portion before
|
|
109
|
+
* `/.gsd/`. Otherwise returns the input unchanged.
|
|
88
110
|
*
|
|
89
111
|
* Use this in commands that call `process.cwd()` to ensure they always
|
|
90
112
|
* operate against the real project root, not a worktree subdirectory.
|
|
91
113
|
*/
|
|
92
114
|
export function resolveProjectRoot(basePath: string): string {
|
|
93
115
|
const normalizedPath = basePath.replaceAll("\\", "/");
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Return the original path up to the .gsd/ marker (un-normalized)
|
|
98
|
-
// Account for potential OS-specific separators
|
|
116
|
+
const seg = findWorktreeSegment(normalizedPath);
|
|
117
|
+
if (!seg) return basePath;
|
|
118
|
+
// Return the original path up to the /.gsd/ boundary
|
|
99
119
|
const sep = basePath.includes("\\") ? "\\" : "/";
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
return basePath.slice(0,
|
|
120
|
+
const gsdMarker = `${sep}.gsd${sep}`;
|
|
121
|
+
const gsdIdx = basePath.indexOf(gsdMarker);
|
|
122
|
+
if (gsdIdx !== -1) return basePath.slice(0, gsdIdx);
|
|
123
|
+
return basePath.slice(0, seg.gsdIdx);
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
@@ -7,6 +7,8 @@ import { join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { readPromptRecord } from "./store.js";
|
|
9
9
|
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
|
+
|
|
10
12
|
export interface LatestPromptSummary {
|
|
11
13
|
id: string;
|
|
12
14
|
status: string;
|
|
@@ -14,7 +16,7 @@ export interface LatestPromptSummary {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function getLatestPromptSummary(): LatestPromptSummary | null {
|
|
17
|
-
const runtimeDir = join(
|
|
19
|
+
const runtimeDir = join(gsdHome, "runtime", "remote-questions");
|
|
18
20
|
if (!existsSync(runtimeDir)) return null;
|
|
19
21
|
const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
|
|
20
22
|
if (files.length === 0) return null;
|
|
@@ -7,8 +7,10 @@ import { join } from "node:path";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
|
|
9
9
|
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
|
+
|
|
10
12
|
function runtimeDir(): string {
|
|
11
|
-
return join(
|
|
13
|
+
return join(gsdHome, "runtime", "remote-questions");
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function recordPath(id: string): string {
|
|
@@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
|
|
|
17
17
|
// Compute authFilePath locally instead of importing from app-paths.ts,
|
|
18
18
|
// because extensions are copied to ~/.gsd/agent/extensions/ at runtime
|
|
19
19
|
// where the relative import '../../../app-paths.ts' doesn't resolve.
|
|
20
|
-
const
|
|
20
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
|
|
21
|
+
const authFilePath = join(gsdHome, 'agent', 'auth.json')
|
|
21
22
|
|
|
22
23
|
export type SearchProvider = 'tavily' | 'brave' | 'ollama'
|
|
23
24
|
export type SearchProviderPreference = SearchProvider | 'auto'
|
|
@@ -452,7 +452,7 @@ async function runSingleAgent(
|
|
|
452
452
|
|
|
453
453
|
async function runSingleAgentInCmuxSplit(
|
|
454
454
|
cmuxClient: CmuxClient,
|
|
455
|
-
|
|
455
|
+
directionOrSurfaceId: "right" | "down" | string,
|
|
456
456
|
defaultCwd: string,
|
|
457
457
|
agents: AgentConfig[],
|
|
458
458
|
agentName: string,
|
|
@@ -503,7 +503,12 @@ async function runSingleAgentInCmuxSplit(
|
|
|
503
503
|
const stdoutPath = path.join(tmpOutputDir, "stdout.jsonl");
|
|
504
504
|
const stderrPath = path.join(tmpOutputDir, "stderr.log");
|
|
505
505
|
const exitPath = path.join(tmpOutputDir, "exit.code");
|
|
506
|
-
|
|
506
|
+
// Accept either a pre-created surface ID or a direction to create a new split
|
|
507
|
+
const isDirection = directionOrSurfaceId === "right" || directionOrSurfaceId === "down"
|
|
508
|
+
|| directionOrSurfaceId === "left" || directionOrSurfaceId === "up";
|
|
509
|
+
const cmuxSurfaceId = isDirection
|
|
510
|
+
? await cmuxClient.createSplit(directionOrSurfaceId as "right" | "down" | "left" | "up")
|
|
511
|
+
: directionOrSurfaceId;
|
|
507
512
|
if (!cmuxSurfaceId) {
|
|
508
513
|
return runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails);
|
|
509
514
|
}
|
|
@@ -806,12 +811,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
806
811
|
const MAX_RETRIES = 1; // Retry failed tasks once
|
|
807
812
|
const batchId = crypto.randomUUID();
|
|
808
813
|
const batchSize = params.tasks.length;
|
|
814
|
+
// Pre-create a grid layout for cmux splits so agents get a clean tiled arrangement
|
|
815
|
+
const gridSurfaces = cmuxSplitsEnabled
|
|
816
|
+
? await cmuxClient.createGridLayout(Math.min(batchSize, MAX_CONCURRENCY))
|
|
817
|
+
: [];
|
|
809
818
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
810
819
|
const workerId = registerWorker(t.agent, t.task, index, batchSize, batchId);
|
|
811
820
|
const runTask = () => cmuxSplitsEnabled
|
|
812
821
|
? runSingleAgentInCmuxSplit(
|
|
813
822
|
cmuxClient,
|
|
814
|
-
index % 2 === 0 ? "right" : "down",
|
|
823
|
+
gridSurfaces[index] ?? (index % 2 === 0 ? "right" : "down"),
|
|
815
824
|
ctx.cwd,
|
|
816
825
|
agents,
|
|
817
826
|
t.agent,
|
|
@@ -57,8 +57,10 @@ function encodeCwd(cwd: string): string {
|
|
|
57
57
|
return cwd.replace(/\//g, "--");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
|
|
61
|
+
|
|
60
62
|
function getIsolationBaseDir(cwd: string, taskId: string): string {
|
|
61
|
-
return path.join(
|
|
63
|
+
return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Track active isolation dirs for cleanup on exit
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
9
9
|
import { join, basename } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
13
|
import type { Rule } from "./ttsr-manager.js";
|
|
12
14
|
import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
|
|
13
15
|
|
|
@@ -59,7 +61,7 @@ function scanDir(dir: string): Rule[] {
|
|
|
59
61
|
* Project rules override global rules with the same name.
|
|
60
62
|
*/
|
|
61
63
|
export function loadRules(cwd: string): Rule[] {
|
|
62
|
-
const globalDir = join(
|
|
64
|
+
const globalDir = join(gsdHome, "agent", "rules");
|
|
63
65
|
const projectDir = join(cwd, ".gsd", "rules");
|
|
64
66
|
|
|
65
67
|
const globalRules = scanDir(globalDir);
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt Compressor — deterministic text compression for context reduction.
|
|
3
|
-
*
|
|
4
|
-
* Applies a series of lossless and near-lossless transformations to reduce
|
|
5
|
-
* token count while preserving semantic meaning. No LLM calls, no external
|
|
6
|
-
* dependencies. Sub-millisecond for typical prompt sizes.
|
|
7
|
-
*
|
|
8
|
-
* Compression techniques (applied in order):
|
|
9
|
-
* 1. Redundant whitespace normalization
|
|
10
|
-
* 2. Markdown formatting reduction (collapse verbose tables, lists)
|
|
11
|
-
* 3. Common phrase abbreviation
|
|
12
|
-
* 4. Repeated pattern deduplication
|
|
13
|
-
* 5. Low-information content removal (empty sections, boilerplate)
|
|
14
|
-
*/
|
|
15
|
-
// ─── Phrase Abbreviation Map ────────────────────────────────────────────────
|
|
16
|
-
/**
|
|
17
|
-
* Build a regex that matches a verbose phrase even when split across lines.
|
|
18
|
-
* Whitespace between words is matched with \s+ to handle line wrapping.
|
|
19
|
-
*/
|
|
20
|
-
function phraseRegex(phrase) {
|
|
21
|
-
const words = phrase.split(/\s+/);
|
|
22
|
-
const pattern = `\\b${words.join("\\s+")}\\b`;
|
|
23
|
-
return new RegExp(pattern, "gi");
|
|
24
|
-
}
|
|
25
|
-
const VERBOSE_PHRASES = [
|
|
26
|
-
[phraseRegex("In order to"), "To"],
|
|
27
|
-
[phraseRegex("It is important to note that"), "Note:"],
|
|
28
|
-
[phraseRegex("As mentioned previously"), "(see above)"],
|
|
29
|
-
[phraseRegex("The following"), "These"],
|
|
30
|
-
[phraseRegex("In addition to"), "Also,"],
|
|
31
|
-
[phraseRegex("Due to the fact that"), "Because"],
|
|
32
|
-
[phraseRegex("At this point in time"), "Now"],
|
|
33
|
-
[phraseRegex("For the purpose of"), "For"],
|
|
34
|
-
[phraseRegex("In the event that"), "If"],
|
|
35
|
-
[phraseRegex("With regard to"), "Re:"],
|
|
36
|
-
[phraseRegex("Prior to"), "Before"],
|
|
37
|
-
[phraseRegex("Subsequent to"), "After"],
|
|
38
|
-
[phraseRegex("In accordance with"), "Per"],
|
|
39
|
-
[phraseRegex("A number of"), "Several"],
|
|
40
|
-
[phraseRegex("In the case of"), "For"],
|
|
41
|
-
[phraseRegex("On the basis of"), "Based on"],
|
|
42
|
-
];
|
|
43
|
-
function extractCodeBlocks(content) {
|
|
44
|
-
const blocks = new Map();
|
|
45
|
-
let counter = 0;
|
|
46
|
-
const text = content.replace(/```[\s\S]*?```/g, (match) => {
|
|
47
|
-
const placeholder = `\x00CODEBLOCK_${counter++}\x00`;
|
|
48
|
-
blocks.set(placeholder, match);
|
|
49
|
-
return placeholder;
|
|
50
|
-
});
|
|
51
|
-
return { text, blocks };
|
|
52
|
-
}
|
|
53
|
-
function restoreCodeBlocks(text, blocks) {
|
|
54
|
-
let result = text;
|
|
55
|
-
for (const [placeholder, block] of blocks) {
|
|
56
|
-
result = result.replace(placeholder, block);
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
// ─── Light Transformations ──────────────────────────────────────────────────
|
|
61
|
-
function normalizeWhitespace(content) {
|
|
62
|
-
// Collapse 3+ consecutive blank lines to 2
|
|
63
|
-
let result = content.replace(/(\n\s*){3,}\n/g, "\n\n");
|
|
64
|
-
// Trim trailing whitespace on every line
|
|
65
|
-
result = result.replace(/[ \t]+$/gm, "");
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
function removeMarkdownComments(content) {
|
|
69
|
-
return content.replace(/<!--[\s\S]*?-->/g, "");
|
|
70
|
-
}
|
|
71
|
-
function removeHorizontalRules(content) {
|
|
72
|
-
// Remove horizontal rules (---, ***, ___) that stand alone on a line
|
|
73
|
-
return content.replace(/^\s*[-*_]{3,}\s*$/gm, "");
|
|
74
|
-
}
|
|
75
|
-
function collapseEmptyListItems(content) {
|
|
76
|
-
// Collapse repeated empty list items (- \n- \n- \n) into one
|
|
77
|
-
return content.replace(/(^[ \t]*[-*+]\s*$\n){2,}/gm, "$1");
|
|
78
|
-
}
|
|
79
|
-
function applyLightTransformations(content) {
|
|
80
|
-
let count = 0;
|
|
81
|
-
let result = content;
|
|
82
|
-
const after1 = normalizeWhitespace(result);
|
|
83
|
-
if (after1 !== result)
|
|
84
|
-
count++;
|
|
85
|
-
result = after1;
|
|
86
|
-
const after2 = removeMarkdownComments(result);
|
|
87
|
-
if (after2 !== result)
|
|
88
|
-
count++;
|
|
89
|
-
result = after2;
|
|
90
|
-
const after3 = removeHorizontalRules(result);
|
|
91
|
-
if (after3 !== result)
|
|
92
|
-
count++;
|
|
93
|
-
result = after3;
|
|
94
|
-
const after4 = collapseEmptyListItems(result);
|
|
95
|
-
if (after4 !== result)
|
|
96
|
-
count++;
|
|
97
|
-
result = after4;
|
|
98
|
-
return { content: result, count };
|
|
99
|
-
}
|
|
100
|
-
// ─── Moderate Transformations ───────────────────────────────────────────────
|
|
101
|
-
function abbreviateVerbosePhrases(content) {
|
|
102
|
-
let count = 0;
|
|
103
|
-
let result = content;
|
|
104
|
-
for (const [pattern, replacement] of VERBOSE_PHRASES) {
|
|
105
|
-
const after = result.replace(pattern, replacement);
|
|
106
|
-
if (after !== result)
|
|
107
|
-
count++;
|
|
108
|
-
result = after;
|
|
109
|
-
}
|
|
110
|
-
return { content: result, count };
|
|
111
|
-
}
|
|
112
|
-
function removeBoilerplateLines(content) {
|
|
113
|
-
const lines = content.split("\n");
|
|
114
|
-
const filtered = lines.filter((line) => {
|
|
115
|
-
const trimmed = line.trim();
|
|
116
|
-
// Remove lines that are just N/A, (none), (empty), (not applicable)
|
|
117
|
-
if (/^(?:N\/A|\(none\)|\(empty\)|\(not applicable\))$/i.test(trimmed)) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
return true;
|
|
121
|
-
});
|
|
122
|
-
return filtered.join("\n");
|
|
123
|
-
}
|
|
124
|
-
function deduplicateConsecutiveLines(content) {
|
|
125
|
-
const lines = content.split("\n");
|
|
126
|
-
const result = [];
|
|
127
|
-
for (let i = 0; i < lines.length; i++) {
|
|
128
|
-
if (i === 0 || lines[i] !== lines[i - 1] || lines[i].trim() === "") {
|
|
129
|
-
result.push(lines[i]);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return result.join("\n");
|
|
133
|
-
}
|
|
134
|
-
function collapseTableFormatting(content) {
|
|
135
|
-
// Remove excessive padding in markdown table cells
|
|
136
|
-
// Matches table rows like | cell | cell | and collapses to | cell | cell |
|
|
137
|
-
return content.replace(/\|[ \t]{2,}([^|\n]*?)[ \t]{2,}\|/g, (_, cellContent) => {
|
|
138
|
-
return `| ${cellContent.trim()} |`;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
function applyModerateTransformations(content) {
|
|
142
|
-
let count = 0;
|
|
143
|
-
let result = content;
|
|
144
|
-
const phraseResult = abbreviateVerbosePhrases(result);
|
|
145
|
-
count += phraseResult.count;
|
|
146
|
-
result = phraseResult.content;
|
|
147
|
-
const after1 = removeBoilerplateLines(result);
|
|
148
|
-
if (after1 !== result)
|
|
149
|
-
count++;
|
|
150
|
-
result = after1;
|
|
151
|
-
const after2 = deduplicateConsecutiveLines(result);
|
|
152
|
-
if (after2 !== result)
|
|
153
|
-
count++;
|
|
154
|
-
result = after2;
|
|
155
|
-
const after3 = collapseTableFormatting(result);
|
|
156
|
-
if (after3 !== result)
|
|
157
|
-
count++;
|
|
158
|
-
result = after3;
|
|
159
|
-
return { content: result, count };
|
|
160
|
-
}
|
|
161
|
-
// ─── Aggressive Transformations ─────────────────────────────────────────────
|
|
162
|
-
function removeMarkdownEmphasis(content) {
|
|
163
|
-
// Bold: **text** or __text__
|
|
164
|
-
let result = content.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
165
|
-
result = result.replace(/__(.+?)__/g, "$1");
|
|
166
|
-
// Italic: *text* or _text_ (single, not inside words)
|
|
167
|
-
result = result.replace(/(?<!\w)\*([^*\n]+?)\*(?!\w)/g, "$1");
|
|
168
|
-
result = result.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, "$1");
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
function removeMarkdownLinks(content) {
|
|
172
|
-
// [text](url) → text
|
|
173
|
-
return content.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
174
|
-
}
|
|
175
|
-
function truncateLongLines(content) {
|
|
176
|
-
const lines = content.split("\n");
|
|
177
|
-
const result = lines.map((line) => {
|
|
178
|
-
if (line.length <= 300)
|
|
179
|
-
return line;
|
|
180
|
-
// Find a sentence boundary (. ! ?) near the 300 char mark
|
|
181
|
-
const truncateZone = line.slice(0, 300);
|
|
182
|
-
const lastSentenceEnd = Math.max(truncateZone.lastIndexOf(". "), truncateZone.lastIndexOf("! "), truncateZone.lastIndexOf("? "));
|
|
183
|
-
if (lastSentenceEnd > 150) {
|
|
184
|
-
return line.slice(0, lastSentenceEnd + 1);
|
|
185
|
-
}
|
|
186
|
-
// Fallback: cut at last space before 300
|
|
187
|
-
const lastSpace = truncateZone.lastIndexOf(" ");
|
|
188
|
-
if (lastSpace > 150) {
|
|
189
|
-
return line.slice(0, lastSpace);
|
|
190
|
-
}
|
|
191
|
-
return truncateZone;
|
|
192
|
-
});
|
|
193
|
-
return result.join("\n");
|
|
194
|
-
}
|
|
195
|
-
function removeBulletMarkers(content) {
|
|
196
|
-
// Remove bullet markers: - , * , + , numbered (1. 2. etc)
|
|
197
|
-
return content.replace(/^[ \t]*(?:[-*+]|\d+\.)\s+/gm, "");
|
|
198
|
-
}
|
|
199
|
-
function removeBlockquoteMarkers(content) {
|
|
200
|
-
return content.replace(/^[ \t]*>+\s?/gm, "");
|
|
201
|
-
}
|
|
202
|
-
function deduplicateStructuralPatterns(content) {
|
|
203
|
-
// Deduplicate consecutive lines that match the same "Key: value" pattern
|
|
204
|
-
const lines = content.split("\n");
|
|
205
|
-
const result = [];
|
|
206
|
-
const seen = new Set();
|
|
207
|
-
let lastWasStructural = false;
|
|
208
|
-
for (const line of lines) {
|
|
209
|
-
const trimmed = line.trim();
|
|
210
|
-
// Detect structural patterns: "Key: value"
|
|
211
|
-
const structMatch = trimmed.match(/^(\w[\w\s]*?):\s+(.+)$/);
|
|
212
|
-
if (structMatch) {
|
|
213
|
-
if (seen.has(trimmed)) {
|
|
214
|
-
lastWasStructural = true;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
seen.add(trimmed);
|
|
218
|
-
lastWasStructural = true;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
// Reset seen set when structural block ends
|
|
222
|
-
if (!lastWasStructural || trimmed === "") {
|
|
223
|
-
seen.clear();
|
|
224
|
-
}
|
|
225
|
-
lastWasStructural = false;
|
|
226
|
-
}
|
|
227
|
-
result.push(line);
|
|
228
|
-
}
|
|
229
|
-
return result.join("\n");
|
|
230
|
-
}
|
|
231
|
-
function applyAggressiveTransformations(content, preserveHeadings) {
|
|
232
|
-
let count = 0;
|
|
233
|
-
let result = content;
|
|
234
|
-
const after1 = removeMarkdownEmphasis(result);
|
|
235
|
-
if (after1 !== result)
|
|
236
|
-
count++;
|
|
237
|
-
result = after1;
|
|
238
|
-
const after2 = removeMarkdownLinks(result);
|
|
239
|
-
if (after2 !== result)
|
|
240
|
-
count++;
|
|
241
|
-
result = after2;
|
|
242
|
-
const after3 = truncateLongLines(result);
|
|
243
|
-
if (after3 !== result)
|
|
244
|
-
count++;
|
|
245
|
-
result = after3;
|
|
246
|
-
const after4 = removeBulletMarkers(result);
|
|
247
|
-
if (after4 !== result)
|
|
248
|
-
count++;
|
|
249
|
-
result = after4;
|
|
250
|
-
const after5 = removeBlockquoteMarkers(result);
|
|
251
|
-
if (after5 !== result)
|
|
252
|
-
count++;
|
|
253
|
-
result = after5;
|
|
254
|
-
const after6 = deduplicateStructuralPatterns(result);
|
|
255
|
-
if (after6 !== result)
|
|
256
|
-
count++;
|
|
257
|
-
result = after6;
|
|
258
|
-
return { content: result, count };
|
|
259
|
-
}
|
|
260
|
-
function extractHeadings(content) {
|
|
261
|
-
const headings = new Map();
|
|
262
|
-
let counter = 0;
|
|
263
|
-
const text = content.replace(/^(#{1,6}\s.+)$/gm, (match) => {
|
|
264
|
-
const placeholder = `\x00HEADING_${counter++}\x00`;
|
|
265
|
-
headings.set(placeholder, match);
|
|
266
|
-
return placeholder;
|
|
267
|
-
});
|
|
268
|
-
return { text, headings };
|
|
269
|
-
}
|
|
270
|
-
function restoreHeadings(text, headings) {
|
|
271
|
-
let result = text;
|
|
272
|
-
for (const [placeholder, heading] of headings) {
|
|
273
|
-
result = result.replace(placeholder, heading);
|
|
274
|
-
}
|
|
275
|
-
return result;
|
|
276
|
-
}
|
|
277
|
-
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
278
|
-
/**
|
|
279
|
-
* Compress prompt content using deterministic text transformations.
|
|
280
|
-
*/
|
|
281
|
-
export function compressPrompt(content, options) {
|
|
282
|
-
const level = options?.level ?? "moderate";
|
|
283
|
-
const preserveHeadings = options?.preserveHeadings ?? true;
|
|
284
|
-
const preserveCodeBlocks = options?.preserveCodeBlocks ?? true;
|
|
285
|
-
if (content === "") {
|
|
286
|
-
return {
|
|
287
|
-
content: "",
|
|
288
|
-
originalChars: 0,
|
|
289
|
-
compressedChars: 0,
|
|
290
|
-
savingsPercent: 0,
|
|
291
|
-
level,
|
|
292
|
-
transformationsApplied: 0,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
const originalChars = content.length;
|
|
296
|
-
let working = content;
|
|
297
|
-
let totalTransformations = 0;
|
|
298
|
-
// Extract code blocks if preserving
|
|
299
|
-
let codeBlocks = null;
|
|
300
|
-
if (preserveCodeBlocks) {
|
|
301
|
-
const extracted = extractCodeBlocks(working);
|
|
302
|
-
working = extracted.text;
|
|
303
|
-
codeBlocks = extracted.blocks;
|
|
304
|
-
}
|
|
305
|
-
// Extract headings if preserving
|
|
306
|
-
let headings = null;
|
|
307
|
-
if (preserveHeadings) {
|
|
308
|
-
const extracted = extractHeadings(working);
|
|
309
|
-
working = extracted.text;
|
|
310
|
-
headings = extracted.headings;
|
|
311
|
-
}
|
|
312
|
-
// Apply light transformations (always)
|
|
313
|
-
const lightResult = applyLightTransformations(working);
|
|
314
|
-
working = lightResult.content;
|
|
315
|
-
totalTransformations += lightResult.count;
|
|
316
|
-
// Check target
|
|
317
|
-
if (options?.targetChars && getRestoredLength(working, codeBlocks, headings) <= options.targetChars) {
|
|
318
|
-
return buildResult(working, originalChars, level, totalTransformations, codeBlocks, headings);
|
|
319
|
-
}
|
|
320
|
-
// Apply moderate transformations
|
|
321
|
-
if (level === "moderate" || level === "aggressive") {
|
|
322
|
-
const modResult = applyModerateTransformations(working);
|
|
323
|
-
working = modResult.content;
|
|
324
|
-
totalTransformations += modResult.count;
|
|
325
|
-
if (options?.targetChars && getRestoredLength(working, codeBlocks, headings) <= options.targetChars) {
|
|
326
|
-
return buildResult(working, originalChars, level, totalTransformations, codeBlocks, headings);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Apply aggressive transformations
|
|
330
|
-
if (level === "aggressive") {
|
|
331
|
-
const aggResult = applyAggressiveTransformations(working, preserveHeadings);
|
|
332
|
-
working = aggResult.content;
|
|
333
|
-
totalTransformations += aggResult.count;
|
|
334
|
-
}
|
|
335
|
-
return buildResult(working, originalChars, level, totalTransformations, codeBlocks, headings);
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Compress with a target size — applies progressively more aggressive
|
|
339
|
-
* compression until the target is reached or all transformations exhausted.
|
|
340
|
-
*/
|
|
341
|
-
export function compressToTarget(content, targetChars) {
|
|
342
|
-
if (content.length <= targetChars) {
|
|
343
|
-
return {
|
|
344
|
-
content,
|
|
345
|
-
originalChars: content.length,
|
|
346
|
-
compressedChars: content.length,
|
|
347
|
-
savingsPercent: 0,
|
|
348
|
-
level: "light",
|
|
349
|
-
transformationsApplied: 0,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
const levels = ["light", "moderate", "aggressive"];
|
|
353
|
-
for (const level of levels) {
|
|
354
|
-
const result = compressPrompt(content, { level, targetChars });
|
|
355
|
-
if (result.compressedChars <= targetChars) {
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
// If aggressive and still over target, return best effort
|
|
359
|
-
if (level === "aggressive") {
|
|
360
|
-
return result;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Unreachable, but satisfy TypeScript
|
|
364
|
-
return compressPrompt(content, { level: "aggressive" });
|
|
365
|
-
}
|
|
366
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
367
|
-
function getRestoredLength(text, codeBlocks, headings) {
|
|
368
|
-
let result = text;
|
|
369
|
-
if (headings)
|
|
370
|
-
result = restoreHeadings(result, headings);
|
|
371
|
-
if (codeBlocks)
|
|
372
|
-
result = restoreCodeBlocks(result, codeBlocks);
|
|
373
|
-
return result.length;
|
|
374
|
-
}
|
|
375
|
-
function buildResult(working, originalChars, level, transformationsApplied, codeBlocks, headings) {
|
|
376
|
-
let content = working;
|
|
377
|
-
if (headings)
|
|
378
|
-
content = restoreHeadings(content, headings);
|
|
379
|
-
if (codeBlocks)
|
|
380
|
-
content = restoreCodeBlocks(content, codeBlocks);
|
|
381
|
-
const compressedChars = content.length;
|
|
382
|
-
const savingsPercent = originalChars > 0
|
|
383
|
-
? Math.round(((originalChars - compressedChars) / originalChars) * 10000) / 100
|
|
384
|
-
: 0;
|
|
385
|
-
return {
|
|
386
|
-
content,
|
|
387
|
-
originalChars,
|
|
388
|
-
compressedChars,
|
|
389
|
-
savingsPercent,
|
|
390
|
-
level,
|
|
391
|
-
transformationsApplied,
|
|
392
|
-
};
|
|
393
|
-
}
|