gentle-pi 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CUSTOM_ROW_LABEL,
|
|
3
|
+
MAX_HEADER_LENGTH,
|
|
4
|
+
MAX_LABEL_LENGTH,
|
|
5
|
+
MAX_OPTIONS,
|
|
6
|
+
MAX_QUESTIONS,
|
|
7
|
+
MIN_OPTIONS,
|
|
8
|
+
type QuestionParams,
|
|
9
|
+
} from "./schema.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A single questionnaire validation violation, keyed by `code` so callers can
|
|
13
|
+
* branch without parsing the human-readable message.
|
|
14
|
+
*/
|
|
15
|
+
export type QuestionnaireError =
|
|
16
|
+
| { code: "no_questions"; message: string }
|
|
17
|
+
| { code: "empty_options"; message: string; questionIndex: number }
|
|
18
|
+
| { code: "option_count"; message: string; questionIndex: number; count: number }
|
|
19
|
+
| { code: "too_many_questions"; message: string; count: number }
|
|
20
|
+
| { code: "duplicate_question"; message: string; questionIndex: number }
|
|
21
|
+
| { code: "duplicate_option_label"; message: string; questionIndex: number; optionIndex: number; label: string }
|
|
22
|
+
| { code: "label_too_long"; message: string; questionIndex: number; optionIndex: number; length: number }
|
|
23
|
+
| { code: "header_too_long"; message: string; questionIndex: number; length: number }
|
|
24
|
+
| { code: "reserved_label"; message: string; questionIndex: number; optionIndex: number; label: string };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate questionnaire parameters and return the first violation in a fixed
|
|
28
|
+
* precedence order, or `undefined` when the parameters are valid.
|
|
29
|
+
*
|
|
30
|
+
* Precedence: no questions, empty options, option count, question count,
|
|
31
|
+
* duplicate question text, duplicate option label, label length, header
|
|
32
|
+
* length, then reserved custom-row labels.
|
|
33
|
+
*/
|
|
34
|
+
export function validateQuestionnaire(params: QuestionParams): QuestionnaireError | undefined {
|
|
35
|
+
const questions = Array.isArray(params?.questions) ? params.questions : [];
|
|
36
|
+
|
|
37
|
+
if (questions.length === 0) {
|
|
38
|
+
return { code: "no_questions", message: "At least one question is required." };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
42
|
+
if (!Array.isArray(question?.options) || question.options.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
code: "empty_options",
|
|
45
|
+
message: `Question ${questionIndex + 1} must declare at least one option.`,
|
|
46
|
+
questionIndex,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
52
|
+
const count = question.options.length;
|
|
53
|
+
if (count < MIN_OPTIONS || count > MAX_OPTIONS) {
|
|
54
|
+
return {
|
|
55
|
+
code: "option_count",
|
|
56
|
+
message: `Question ${questionIndex + 1} must have between ${MIN_OPTIONS} and ${MAX_OPTIONS} options, received ${count}.`,
|
|
57
|
+
questionIndex,
|
|
58
|
+
count,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (questions.length > MAX_QUESTIONS) {
|
|
64
|
+
return {
|
|
65
|
+
code: "too_many_questions",
|
|
66
|
+
message: `A questionnaire accepts at most ${MAX_QUESTIONS} questions, received ${questions.length}.`,
|
|
67
|
+
count: questions.length,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const seenQuestions = new Set<string>();
|
|
72
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
73
|
+
if (seenQuestions.has(question.question)) {
|
|
74
|
+
return {
|
|
75
|
+
code: "duplicate_question",
|
|
76
|
+
message: `Question ${questionIndex + 1} duplicates an earlier question text.`,
|
|
77
|
+
questionIndex,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
seenQuestions.add(question.question);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Checked after duplicate questions so a repeated question points at the
|
|
84
|
+
// duplicate itself rather than one of its option labels.
|
|
85
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
86
|
+
const seenLabels = new Set<string>();
|
|
87
|
+
for (const [optionIndex, option] of question.options.entries()) {
|
|
88
|
+
if (seenLabels.has(option.label)) {
|
|
89
|
+
return {
|
|
90
|
+
code: "duplicate_option_label",
|
|
91
|
+
message: `Question ${questionIndex + 1} option ${optionIndex + 1} duplicates the label "${option.label}".`,
|
|
92
|
+
questionIndex,
|
|
93
|
+
optionIndex,
|
|
94
|
+
label: option.label,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
seenLabels.add(option.label);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
102
|
+
for (const [optionIndex, option] of question.options.entries()) {
|
|
103
|
+
if (option.label.length > MAX_LABEL_LENGTH) {
|
|
104
|
+
return {
|
|
105
|
+
code: "label_too_long",
|
|
106
|
+
message: `Question ${questionIndex + 1} option ${optionIndex + 1} label exceeds ${MAX_LABEL_LENGTH} characters.`,
|
|
107
|
+
questionIndex,
|
|
108
|
+
optionIndex,
|
|
109
|
+
length: option.label.length,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
116
|
+
if (question.header.length > MAX_HEADER_LENGTH) {
|
|
117
|
+
return {
|
|
118
|
+
code: "header_too_long",
|
|
119
|
+
message: `Question ${questionIndex + 1} header exceeds ${MAX_HEADER_LENGTH} characters.`,
|
|
120
|
+
questionIndex,
|
|
121
|
+
length: question.header.length,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [questionIndex, question] of questions.entries()) {
|
|
127
|
+
for (const [optionIndex, option] of question.options.entries()) {
|
|
128
|
+
if (option.label === "Other" || option.label === CUSTOM_ROW_LABEL) {
|
|
129
|
+
return {
|
|
130
|
+
code: "reserved_label",
|
|
131
|
+
message: `Question ${questionIndex + 1} option ${optionIndex + 1} uses the reserved label "${option.label}".`,
|
|
132
|
+
questionIndex,
|
|
133
|
+
optionIndex,
|
|
134
|
+
label: option.label,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
@@ -27,6 +27,21 @@ const WINDOWS_SYSTEM = "S-1-5-18";
|
|
|
27
27
|
const WINDOWS_ADMINISTRATORS = "S-1-5-32-544";
|
|
28
28
|
const WINDOWS_SYSTEM_DIRECTORY = "\\\\?\\GLOBALROOT\\SystemRoot\\System32";
|
|
29
29
|
|
|
30
|
+
// Cold Windows runners occasionally exceed the bounded 5-second probe timeout
|
|
31
|
+
// on first process start (whoami/PowerShell/icacls). One bounded retry of an
|
|
32
|
+
// idempotent system probe recovers that transient; authority validation is
|
|
33
|
+
// never retried or weakened because it happens after the probe returns.
|
|
34
|
+
const TRANSIENT_WINDOWS_PROBE_CODES = new Set(["ETIMEDOUT", "EAGAIN", "EBUSY"]);
|
|
35
|
+
|
|
36
|
+
export function withWindowsSystemProbeRetry<T>(probe: () => T): T {
|
|
37
|
+
try {
|
|
38
|
+
return probe();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!TRANSIENT_WINDOWS_PROBE_CODES.has((error as NodeJS.ErrnoException | undefined)?.code ?? "")) throw error;
|
|
41
|
+
return probe();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
function windowsSystemExecutable(name: "whoami.exe" | "icacls.exe" | "WindowsPowerShell\\v1.0\\powershell.exe"): string {
|
|
31
46
|
try {
|
|
32
47
|
return realpathSync.native(join(WINDOWS_SYSTEM_DIRECTORY, name));
|
|
@@ -36,7 +51,7 @@ function windowsSystemExecutable(name: "whoami.exe" | "icacls.exe" | "WindowsPow
|
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
function windowsUserSid(): string {
|
|
39
|
-
const output = execFileSync(windowsSystemExecutable("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
54
|
+
const output = withWindowsSystemProbeRetry(() => execFileSync(windowsSystemExecutable("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true }));
|
|
40
55
|
const matches = output.match(/S-\d+(?:-\d+)+/gi) ?? [];
|
|
41
56
|
if (matches.length !== 1) throw new Error("Windows user SID is unavailable");
|
|
42
57
|
return matches[0]!.toUpperCase();
|
|
@@ -45,7 +60,7 @@ function windowsUserSid(): string {
|
|
|
45
60
|
function windowsLocalAdministratorSid(): string {
|
|
46
61
|
const script = "$ErrorActionPreference='Stop';$descriptor=New-Object System.Security.AccessControl.RawSecurityDescriptor 'D:(A;;FA;;;LA)';$descriptor.DiscretionaryAcl[0].SecurityIdentifier.Value";
|
|
47
62
|
const systemRoot = dirname(dirname(windowsSystemExecutable("whoami.exe")));
|
|
48
|
-
const output = execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot } });
|
|
63
|
+
const output = withWindowsSystemProbeRetry(() => execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot } }));
|
|
49
64
|
const matches = output.match(/S-\d+(?:-\d+)+/gi) ?? [];
|
|
50
65
|
if (matches.length !== 1 || !isWindowsSid(matches[0]!)) throw new Error("Windows local Administrator SID is unavailable");
|
|
51
66
|
return matches[0]!.toUpperCase();
|
|
@@ -55,7 +70,7 @@ function windowsDacl(path: string): string {
|
|
|
55
70
|
const archive = `.gentle-ai-acl-${randomUUID()}.txt`;
|
|
56
71
|
const archivePath = join(dirname(path), archive);
|
|
57
72
|
try {
|
|
58
|
-
execFileSync(windowsSystemExecutable("icacls.exe"), [path, "/save", archive, "/c"], { cwd: dirname(path), encoding: "utf8", timeout: 5000, maxBuffer: 16384, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
73
|
+
withWindowsSystemProbeRetry(() => execFileSync(windowsSystemExecutable("icacls.exe"), [path, "/save", archive, "/c"], { cwd: dirname(path), encoding: "utf8", timeout: 5000, maxBuffer: 16384, stdio: ["ignore", "pipe", "pipe"], windowsHide: true }));
|
|
59
74
|
const sddl = readFileSync(archivePath, "utf16le").match(/D:[^\r\n]+/)?.[0];
|
|
60
75
|
if (sddl === undefined) throw new Error("Windows DACL is unavailable");
|
|
61
76
|
return sddl;
|
|
@@ -151,7 +166,7 @@ function windowsOwnerSid(path: string, kind: WindowsObjectKind): string {
|
|
|
151
166
|
const systemRoot = dirname(dirname(windowsSystemExecutable("whoami.exe")));
|
|
152
167
|
let output: string;
|
|
153
168
|
try {
|
|
154
|
-
output = execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot, GENTLE_PI_CANDIDATE_OWNER_PATH: path } });
|
|
169
|
+
output = withWindowsSystemProbeRetry(() => execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 4096, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot, GENTLE_PI_CANDIDATE_OWNER_PATH: path } }));
|
|
155
170
|
} catch {
|
|
156
171
|
throw new WindowsOwnerValidationError();
|
|
157
172
|
}
|
|
@@ -175,7 +190,7 @@ function enforcePrivateWindowsDacl(path: string, identity: WindowsAclIdentity =
|
|
|
175
190
|
const sddl = `D:P(A;OICI;FA;;;${user})(A;OICI;FA;;;${WINDOWS_SYSTEM})(A;OICI;FA;;;${WINDOWS_ADMINISTRATORS})`;
|
|
176
191
|
const script = "$ErrorActionPreference='Stop';$acl=New-Object System.Security.AccessControl.DirectorySecurity;$acl.SetSecurityDescriptorSddlForm($env:GENTLE_PI_CANDIDATE_ACL_SDDL,[System.Security.AccessControl.AccessControlSections]::Access);[System.IO.Directory]::SetAccessControl($env:GENTLE_PI_CANDIDATE_ACL_PATH,$acl)";
|
|
177
192
|
const systemRoot = dirname(dirname(windowsSystemExecutable("whoami.exe")));
|
|
178
|
-
execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 16384, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot, GENTLE_PI_CANDIDATE_ACL_PATH: path, GENTLE_PI_CANDIDATE_ACL_SDDL: sddl } });
|
|
193
|
+
withWindowsSystemProbeRetry(() => execFileSync(windowsSystemExecutable("WindowsPowerShell\\v1.0\\powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { encoding: "utf8", timeout: 5000, maxBuffer: 16384, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: { ...process.env, SystemRoot: systemRoot, GENTLE_PI_CANDIDATE_ACL_PATH: path, GENTLE_PI_CANDIDATE_ACL_SDDL: sddl } }));
|
|
179
194
|
assertPrivateWindowsDacl(path, "directory", true, identity);
|
|
180
195
|
}
|
|
181
196
|
|
|
@@ -634,6 +634,13 @@ function makeWritableForCleanup(path: string): void {
|
|
|
634
634
|
chmodSync(path, 0o644);
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
function candidateOwnerPreparationError(error: unknown): CandidateViewError {
|
|
638
|
+
// Surface the bounded errno of the underlying failure directly in the
|
|
639
|
+
// message so CI logs are self-diagnosing without TAP printing the cause.
|
|
640
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
641
|
+
return new CandidateViewError(`candidate view owner preparation failed${typeof code === "string" && code ? ` (${code})` : ""}`, "candidate-owner-preparation-failed", undefined, { cause: error });
|
|
642
|
+
}
|
|
643
|
+
|
|
637
644
|
function candidateViewParent(commonDir: string, platform: NodeJS.Platform): string {
|
|
638
645
|
const control = join(commonDir, "gentle-ai");
|
|
639
646
|
mkdirSync(control, { recursive: true, mode: 0o700 });
|
|
@@ -646,7 +653,7 @@ function candidateViewParent(commonDir: string, platform: NodeJS.Platform): stri
|
|
|
646
653
|
try {
|
|
647
654
|
return prepareCandidateOwnerParent(commonDir, platform);
|
|
648
655
|
} catch (error) {
|
|
649
|
-
throw
|
|
656
|
+
throw candidateOwnerPreparationError(error);
|
|
650
657
|
}
|
|
651
658
|
}
|
|
652
659
|
|
|
@@ -916,7 +923,7 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
|
|
|
916
923
|
try {
|
|
917
924
|
owner = createCandidateOwner(canonicalCommonDir, root, platform);
|
|
918
925
|
} catch (error) {
|
|
919
|
-
throw
|
|
926
|
+
throw candidateOwnerPreparationError(error);
|
|
920
927
|
}
|
|
921
928
|
// The worktree is created under the same try/catch cleanup boundary as
|
|
922
929
|
// the read-tree materialization that follows. addUnbornWorktree's
|