@gajae-code/coding-agent 0.11.2 → 0.11.4
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 +34 -0
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +85 -10
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +39 -12
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +24 -8
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/launch-disposition.ts +42 -0
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +3 -2
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +55 -10
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +51 -212
- package/src/main.ts +99 -22
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/session-observer-overlay.ts +10 -23
- package/src/modes/components/tool-transcript-format.ts +72 -0
- package/src/modes/components/transcript-viewer-overlay.ts +42 -2
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +228 -15
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/index.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +50 -10
- package/src/sdk/host/host.ts +13 -1
- package/src/sdk/session.ts +4 -1
- package/src/session/internal/managed-session-scope.ts +15 -1
- package/src/tools/ask.ts +216 -41
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/output-meta.ts +6 -2
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/read.ts +189 -64
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/subagent.ts +43 -2
- package/src/utils/file-mentions.ts +4 -4
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
package/src/tools/ask.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
19
|
+
import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
|
|
19
20
|
import {
|
|
20
21
|
type Component,
|
|
21
22
|
Container,
|
|
@@ -109,36 +110,40 @@ const DeepInterviewIntentReview = z
|
|
|
109
110
|
.strict();
|
|
110
111
|
|
|
111
112
|
/** Optional structured deep-interview round metadata; when present the round is recorded automatically. */
|
|
112
|
-
const
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.strict()
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
113
|
+
const DeepInterviewMetadata = z.object({
|
|
114
|
+
round_id: z.string().max(128).describe("stable optional round identity").optional(),
|
|
115
|
+
round: z.number().int().nonnegative().describe("round number"),
|
|
116
|
+
component: z.string().min(1).max(128).describe("targeted topology component"),
|
|
117
|
+
dimension: z.string().min(1).max(128).describe("targeted clarity dimension"),
|
|
118
|
+
ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** Provider-visible branches keep ordinary, contract, and review metadata mutually exclusive. */
|
|
122
|
+
const DeepInterviewMeta = z.union([
|
|
123
|
+
DeepInterviewMetadata.strict(),
|
|
124
|
+
DeepInterviewMetadata.extend({
|
|
125
|
+
round: z.literal(0).describe("round number"),
|
|
126
|
+
component: z.literal("review-topology").describe("targeted topology component"),
|
|
127
|
+
dimension: z.literal("topology").describe("targeted clarity dimension"),
|
|
128
|
+
intent_contract: DeepInterviewIntentContract.describe("Round-0 locked intent contract"),
|
|
129
|
+
}).strict(),
|
|
130
|
+
DeepInterviewMetadata.extend({
|
|
131
|
+
round: z.number().int().positive().describe("round number"),
|
|
132
|
+
intent_review: DeepInterviewIntentReview.describe("Locked-intent reduction review"),
|
|
133
|
+
}).strict(),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
type DeepInterviewMeta = z.infer<typeof DeepInterviewMeta>;
|
|
137
|
+
|
|
138
|
+
function intentContract(
|
|
139
|
+
metadata: DeepInterviewMeta | undefined,
|
|
140
|
+
): z.infer<typeof DeepInterviewIntentContract> | undefined {
|
|
141
|
+
return metadata && "intent_contract" in metadata ? metadata.intent_contract : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function intentReview(metadata: DeepInterviewMeta | undefined): z.infer<typeof DeepInterviewIntentReview> | undefined {
|
|
145
|
+
return metadata && "intent_review" in metadata ? metadata.intent_review : undefined;
|
|
146
|
+
}
|
|
142
147
|
|
|
143
148
|
const WorkflowGateMeta = z.object({
|
|
144
149
|
stage: z.enum(["deep-interview", "ralplan", "ultragoal"]).describe("workflow gate stage"),
|
|
@@ -157,23 +162,25 @@ const QuestionItem = z
|
|
|
157
162
|
})
|
|
158
163
|
.superRefine((value, context) => {
|
|
159
164
|
const labels = new Set(value.options.map(option => option.label));
|
|
160
|
-
|
|
165
|
+
const contract = intentContract(value.deepInterview);
|
|
166
|
+
const review = intentReview(value.deepInterview);
|
|
167
|
+
if ((contract || review) && value.multi === true)
|
|
161
168
|
context.addIssue({ code: "custom", message: "intent gates must be single-select", path: ["multi"] });
|
|
162
|
-
const confirmationOptions =
|
|
169
|
+
const confirmationOptions = contract?.confirmation_options ?? [];
|
|
163
170
|
if (new Set(confirmationOptions).size !== confirmationOptions.length)
|
|
164
171
|
context.addIssue({
|
|
165
172
|
code: "custom",
|
|
166
173
|
message: "intent confirmation options must be unique",
|
|
167
174
|
path: ["deepInterview", "intent_contract"],
|
|
168
175
|
});
|
|
169
|
-
const approvalOptions =
|
|
176
|
+
const approvalOptions = review?.approval_options ?? [];
|
|
170
177
|
if (new Set(approvalOptions).size !== approvalOptions.length)
|
|
171
178
|
context.addIssue({
|
|
172
179
|
code: "custom",
|
|
173
180
|
message: "intent approval options must be unique",
|
|
174
181
|
path: ["deepInterview", "intent_review"],
|
|
175
182
|
});
|
|
176
|
-
for (const label of
|
|
183
|
+
for (const label of confirmationOptions) {
|
|
177
184
|
if (!labels.has(label))
|
|
178
185
|
context.addIssue({
|
|
179
186
|
code: "custom",
|
|
@@ -181,7 +188,7 @@ const QuestionItem = z
|
|
|
181
188
|
path: ["deepInterview", "intent_contract"],
|
|
182
189
|
});
|
|
183
190
|
}
|
|
184
|
-
for (const label of
|
|
191
|
+
for (const label of approvalOptions) {
|
|
185
192
|
if (!labels.has(label))
|
|
186
193
|
context.addIssue({
|
|
187
194
|
code: "custom",
|
|
@@ -197,6 +204,173 @@ export const askSchema = z.object({
|
|
|
197
204
|
|
|
198
205
|
export type AskToolInput = z.infer<typeof askSchema>;
|
|
199
206
|
|
|
207
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
208
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
209
|
+
const prototype = Object.getPrototypeOf(value);
|
|
210
|
+
return prototype === Object.prototype || prototype === null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isOnlyPlainData(value: unknown): boolean {
|
|
214
|
+
if (Array.isArray(value)) return value.every(isOnlyPlainData);
|
|
215
|
+
if (typeof value !== "object" || value === null) return true;
|
|
216
|
+
return isPlainRecord(value) && Object.values(value).every(isOnlyPlainData);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function hasExactOwnKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
220
|
+
const keys = Reflect.ownKeys(value);
|
|
221
|
+
return keys.length === allowed.length && keys.every(key => typeof key === "string" && allowed.includes(key));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasOnlyAllowedOwnKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
225
|
+
return Reflect.ownKeys(value).every(key => typeof key === "string" && allowed.includes(key));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hasUniqueDisplayedLabels(labels: readonly string[], optionLabels: ReadonlySet<string>): boolean {
|
|
229
|
+
return new Set(labels).size === labels.length && labels.every(label => optionLabels.has(label));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Parse only to recognize a retired recovery shape; parsed values are never eligible for recovery. */
|
|
233
|
+
function parseEncodedContainer(value: unknown): unknown {
|
|
234
|
+
if (typeof value !== "string") return value;
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(value);
|
|
237
|
+
} catch {
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Whether malformed input is close enough to the retired pair shape to require a terminal rejection. */
|
|
243
|
+
function isRoundZeroRecoveryCandidate(value: unknown): boolean {
|
|
244
|
+
const root = parseEncodedContainer(value);
|
|
245
|
+
if (typeof root !== "object" || root === null || !Object.hasOwn(root, "questions")) return false;
|
|
246
|
+
const questionsValue = parseEncodedContainer((root as Record<string, unknown>).questions);
|
|
247
|
+
if (!Array.isArray(questionsValue)) return questionsValue === null;
|
|
248
|
+
return questionsValue.some(rawQuestion => {
|
|
249
|
+
const question = parseEncodedContainer(rawQuestion);
|
|
250
|
+
if (typeof question !== "object" || question === null || !Object.hasOwn(question, "deepInterview")) return false;
|
|
251
|
+
const deepInterview = parseEncodedContainer((question as Record<string, unknown>).deepInterview);
|
|
252
|
+
if (typeof deepInterview !== "object" || deepInterview === null) return deepInterview === null;
|
|
253
|
+
const metadata = deepInterview as Record<string, unknown>;
|
|
254
|
+
return Object.hasOwn(metadata, "intent_contract") || Object.hasOwn(metadata, "intent_review");
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Remove only strict-provider null placeholders for fields optional in the canonical Ask contract. */
|
|
259
|
+
function normalizeRoundZeroOptionalNulls(arguments_: Record<string, unknown>): Record<string, unknown> {
|
|
260
|
+
if (!isPlainRecord(arguments_) || !Array.isArray(arguments_.questions) || arguments_.questions.length !== 1)
|
|
261
|
+
return arguments_;
|
|
262
|
+
const question = arguments_.questions[0];
|
|
263
|
+
if (!isPlainRecord(question) || !isPlainRecord(question.deepInterview)) return arguments_;
|
|
264
|
+
const normalizedQuestion = { ...question };
|
|
265
|
+
let changed = false;
|
|
266
|
+
for (const key of ["multi", "recommended", "workflowGate"] as const) {
|
|
267
|
+
if (Object.hasOwn(normalizedQuestion, key) && normalizedQuestion[key] === null) {
|
|
268
|
+
delete normalizedQuestion[key];
|
|
269
|
+
changed = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const normalizedDeepInterview = { ...question.deepInterview };
|
|
273
|
+
if (Object.hasOwn(normalizedDeepInterview, "round_id") && normalizedDeepInterview.round_id === null) {
|
|
274
|
+
delete normalizedDeepInterview.round_id;
|
|
275
|
+
normalizedQuestion.deepInterview = normalizedDeepInterview;
|
|
276
|
+
changed = true;
|
|
277
|
+
}
|
|
278
|
+
return changed ? { ...arguments_, questions: [normalizedQuestion] } : arguments_;
|
|
279
|
+
}
|
|
280
|
+
function recoverRoundZeroIntentContract(arguments_: Record<string, unknown>): RawArgumentValidationResult {
|
|
281
|
+
if (!isRoundZeroRecoveryCandidate(arguments_)) return { outcome: "passthrough" };
|
|
282
|
+
const normalizedArguments = normalizeRoundZeroOptionalNulls(arguments_);
|
|
283
|
+
if (!isOnlyPlainData(normalizedArguments) || !isPlainRecord(normalizedArguments)) return { outcome: "reject" };
|
|
284
|
+
if (
|
|
285
|
+
!hasExactOwnKeys(normalizedArguments, ["questions"]) ||
|
|
286
|
+
!Array.isArray(normalizedArguments.questions) ||
|
|
287
|
+
normalizedArguments.questions.length !== 1
|
|
288
|
+
)
|
|
289
|
+
return { outcome: "reject" };
|
|
290
|
+
|
|
291
|
+
const question = normalizedArguments.questions[0];
|
|
292
|
+
if (!isPlainRecord(question)) return { outcome: "reject" };
|
|
293
|
+
const questionKeys = ["id", "question", "options", "multi", "recommended", "deepInterview", "workflowGate"];
|
|
294
|
+
if (!hasOnlyAllowedOwnKeys(question, questionKeys)) return { outcome: "reject" };
|
|
295
|
+
if (
|
|
296
|
+
typeof question.id !== "string" ||
|
|
297
|
+
typeof question.question !== "string" ||
|
|
298
|
+
!Array.isArray(question.options) ||
|
|
299
|
+
!Object.hasOwn(question, "deepInterview") ||
|
|
300
|
+
!isPlainRecord(question.deepInterview) ||
|
|
301
|
+
(Object.hasOwn(question, "multi") && question.multi !== false) ||
|
|
302
|
+
(Object.hasOwn(question, "recommended") && typeof question.recommended !== "number")
|
|
303
|
+
)
|
|
304
|
+
return { outcome: "reject" };
|
|
305
|
+
const deepInterview = question.deepInterview;
|
|
306
|
+
const hasIntentContract = Object.hasOwn(deepInterview, "intent_contract");
|
|
307
|
+
const hasIntentReview = Object.hasOwn(deepInterview, "intent_review");
|
|
308
|
+
if (hasIntentContract !== hasIntentReview && askSchema.safeParse(normalizedArguments).success)
|
|
309
|
+
return { outcome: "passthrough" };
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
Object.hasOwn(question, "workflowGate") &&
|
|
313
|
+
(!isPlainRecord(question.workflowGate) ||
|
|
314
|
+
!hasExactOwnKeys(question.workflowGate, ["stage", "kind"]) ||
|
|
315
|
+
question.workflowGate.stage !== "deep-interview" ||
|
|
316
|
+
question.workflowGate.kind !== "question")
|
|
317
|
+
)
|
|
318
|
+
return { outcome: "reject" };
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
!question.options.every(
|
|
322
|
+
option => isPlainRecord(option) && hasExactOwnKeys(option, ["label"]) && typeof option.label === "string",
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
return { outcome: "reject" };
|
|
326
|
+
const optionLabels = question.options.map(option => (option as { label: string }).label);
|
|
327
|
+
if (new Set(optionLabels).size !== optionLabels.length) return { outcome: "reject" };
|
|
328
|
+
|
|
329
|
+
const deepInterviewKeys = [
|
|
330
|
+
"round_id",
|
|
331
|
+
"round",
|
|
332
|
+
"component",
|
|
333
|
+
"dimension",
|
|
334
|
+
"ambiguity",
|
|
335
|
+
"intent_contract",
|
|
336
|
+
"intent_review",
|
|
337
|
+
];
|
|
338
|
+
if (
|
|
339
|
+
!hasOnlyAllowedOwnKeys(deepInterview, deepInterviewKeys) ||
|
|
340
|
+
!Object.hasOwn(deepInterview, "intent_contract") ||
|
|
341
|
+
!Object.hasOwn(deepInterview, "intent_review") ||
|
|
342
|
+
deepInterview.round !== 0 ||
|
|
343
|
+
typeof deepInterview.component !== "string" ||
|
|
344
|
+
deepInterview.component !== "review-topology" ||
|
|
345
|
+
typeof deepInterview.dimension !== "string" ||
|
|
346
|
+
deepInterview.dimension !== "topology" ||
|
|
347
|
+
typeof deepInterview.ambiguity !== "number" ||
|
|
348
|
+
(Object.hasOwn(deepInterview, "round_id") && typeof deepInterview.round_id !== "string")
|
|
349
|
+
)
|
|
350
|
+
return { outcome: "reject" };
|
|
351
|
+
|
|
352
|
+
const contract = DeepInterviewIntentContract.safeParse(deepInterview.intent_contract);
|
|
353
|
+
const review = DeepInterviewIntentReview.safeParse(deepInterview.intent_review);
|
|
354
|
+
if (!contract.success || !review.success) return { outcome: "reject" };
|
|
355
|
+
const displayedLabels = new Set(optionLabels);
|
|
356
|
+
if (
|
|
357
|
+
!hasUniqueDisplayedLabels(contract.data.confirmation_options, displayedLabels) ||
|
|
358
|
+
!hasUniqueDisplayedLabels(review.data.approval_options, displayedLabels)
|
|
359
|
+
)
|
|
360
|
+
return { outcome: "reject" };
|
|
361
|
+
|
|
362
|
+
const { intent_review: _intentReview, ...recoveredDeepInterview } = deepInterview;
|
|
363
|
+
const recovered = {
|
|
364
|
+
questions: [
|
|
365
|
+
{
|
|
366
|
+
...question,
|
|
367
|
+
deepInterview: { ...recoveredDeepInterview, intent_contract: contract.data },
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
return askSchema.safeParse(recovered).success ? { outcome: "accept", arguments: recovered } : { outcome: "reject" };
|
|
372
|
+
}
|
|
373
|
+
|
|
200
374
|
/** Result for a single question */
|
|
201
375
|
export interface QuestionResult {
|
|
202
376
|
id: string;
|
|
@@ -803,6 +977,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
803
977
|
readonly summary = "Ask the user a clarifying question";
|
|
804
978
|
readonly description: string;
|
|
805
979
|
readonly parameters = askSchema;
|
|
980
|
+
readonly rawArgumentValidation = recoverRoundZeroIntentContract;
|
|
806
981
|
readonly strict = true;
|
|
807
982
|
readonly loadMode = "discoverable";
|
|
808
983
|
|
|
@@ -850,14 +1025,14 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
850
1025
|
ambiguity: meta.ambiguity,
|
|
851
1026
|
selectedOptions,
|
|
852
1027
|
customInput,
|
|
853
|
-
intent_contract: meta
|
|
854
|
-
intent_review: meta
|
|
1028
|
+
intent_contract: intentContract(meta),
|
|
1029
|
+
intent_review: intentReview(meta),
|
|
855
1030
|
},
|
|
856
1031
|
{ sessionId },
|
|
857
1032
|
).then(async () => {
|
|
858
1033
|
await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
|
|
859
1034
|
}),
|
|
860
|
-
meta
|
|
1035
|
+
intentContract(meta) !== undefined || intentReview(meta) !== undefined,
|
|
861
1036
|
);
|
|
862
1037
|
}
|
|
863
1038
|
|
|
@@ -1190,7 +1365,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
1190
1365
|
navigation: options?.navigation,
|
|
1191
1366
|
scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
|
|
1192
1367
|
otherOptionLabel,
|
|
1193
|
-
autoSelectOnTimeout: !q.deepInterview
|
|
1368
|
+
autoSelectOnTimeout: !intentContract(q.deepInterview) && !intentReview(q.deepInterview),
|
|
1194
1369
|
clarificationOptionLabel,
|
|
1195
1370
|
onRemoteState: state => {
|
|
1196
1371
|
activeRemoteRequest = {
|
|
@@ -1277,7 +1452,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
1277
1452
|
}
|
|
1278
1453
|
if (
|
|
1279
1454
|
clarificationQuestion === undefined &&
|
|
1280
|
-
!(timedOut && (q.deepInterview
|
|
1455
|
+
!(timedOut && (intentContract(q.deepInterview) || intentReview(q.deepInterview)))
|
|
1281
1456
|
) {
|
|
1282
1457
|
await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
|
|
1283
1458
|
}
|
|
@@ -1358,7 +1533,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
1358
1533
|
|
|
1359
1534
|
if (
|
|
1360
1535
|
clarificationQuestion === undefined &&
|
|
1361
|
-
!(timedOut && (q.deepInterview
|
|
1536
|
+
!(timedOut && (intentContract(q.deepInterview) || intentReview(q.deepInterview)))
|
|
1362
1537
|
) {
|
|
1363
1538
|
await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
|
|
1364
1539
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyStateArgv,
|
|
3
|
+
STATE_ACTION_NAMES,
|
|
4
|
+
type StateAction,
|
|
5
|
+
type StateArgvClassification,
|
|
6
|
+
} from "../gjc-runtime/state-argv";
|
|
7
|
+
import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
|
|
8
|
+
|
|
1
9
|
export interface BashAllowedPrefixesCheck {
|
|
2
10
|
allowed: boolean;
|
|
3
11
|
reason?: string;
|
|
@@ -11,9 +19,8 @@ export interface BashRestrictionOptions {
|
|
|
11
19
|
|
|
12
20
|
const SHELL_CONTROL_CHARS = new Set([";", "|", "&", "<", ">", "(", ")"]);
|
|
13
21
|
const UNSAFE_UNQUOTED_EXPANSION_CHARS = new Set(["$", "*", "?", "[", "]", "{", "}", "~"]);
|
|
14
|
-
const STATE_FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id", "--to"]);
|
|
15
|
-
const STATE_ACTIONS = new Set(["read", "write", "clear", "contract", "handoff"]);
|
|
16
22
|
const ALLOWED_STATE_ACTIONS = new Set(["read", "write", "contract"]);
|
|
23
|
+
const CANONICAL_STATE_TARGETS = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
|
|
17
24
|
const READ_ONLY_COMMANDS = new Set(["grep", "rg", "tree", "ls", "pwd", "wc", "du", "file", "stat"]);
|
|
18
25
|
|
|
19
26
|
function parseShellWords(command: string): { words: string[]; reason?: string } {
|
|
@@ -21,6 +28,7 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
21
28
|
let current = "";
|
|
22
29
|
let quote: "single" | "double" | null = null;
|
|
23
30
|
|
|
31
|
+
let wordStarted = false;
|
|
24
32
|
for (let index = 0; index < command.length; index += 1) {
|
|
25
33
|
const char = command[index]!;
|
|
26
34
|
const next = command[index + 1];
|
|
@@ -54,10 +62,12 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
54
62
|
|
|
55
63
|
if (char === "'") {
|
|
56
64
|
quote = "single";
|
|
65
|
+
wordStarted = true;
|
|
57
66
|
continue;
|
|
58
67
|
}
|
|
59
68
|
if (char === '"') {
|
|
60
69
|
quote = "double";
|
|
70
|
+
wordStarted = true;
|
|
61
71
|
continue;
|
|
62
72
|
}
|
|
63
73
|
if (char === "`" || (char === "$" && next === "(")) {
|
|
@@ -73,9 +83,10 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
73
83
|
return { words, reason: `shell expansion character '${char}' is not allowed in restricted bash commands` };
|
|
74
84
|
}
|
|
75
85
|
if (/\s/u.test(char)) {
|
|
76
|
-
if (
|
|
86
|
+
if (wordStarted) {
|
|
77
87
|
words.push(current);
|
|
78
88
|
current = "";
|
|
89
|
+
wordStarted = false;
|
|
79
90
|
}
|
|
80
91
|
continue;
|
|
81
92
|
}
|
|
@@ -83,12 +94,13 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
83
94
|
return { words, reason: "backslash escapes are not allowed in restricted bash commands" };
|
|
84
95
|
}
|
|
85
96
|
current += char;
|
|
97
|
+
wordStarted = true;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
if (quote !== null) {
|
|
89
101
|
return { words, reason: "unterminated quote in restricted bash command" };
|
|
90
102
|
}
|
|
91
|
-
if (
|
|
103
|
+
if (wordStarted) words.push(current);
|
|
92
104
|
return { words };
|
|
93
105
|
}
|
|
94
106
|
|
|
@@ -101,28 +113,62 @@ function wordsStartWith(words: readonly string[], prefix: readonly string[]): bo
|
|
|
101
113
|
return prefix.every((word, index) => words[index] === word);
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
function malformedStateShape(): StateActionClassification {
|
|
117
|
+
return { reason: "restricted role-agent bash only allows documented `gjc state` action shapes" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ParsedStateAction {
|
|
121
|
+
action: string;
|
|
122
|
+
target: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface StateActionClassification {
|
|
126
|
+
parsed?: ParsedStateAction;
|
|
127
|
+
reason?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasDocumentedStatePositionals(classification: StateArgvClassification): boolean {
|
|
131
|
+
const [first, second, third] = classification.positionals;
|
|
132
|
+
if (third) return false;
|
|
133
|
+
if (!second) return true;
|
|
134
|
+
return STATE_ACTION_NAMES.has(first as StateAction) || STATE_ACTION_NAMES.has(second as StateAction);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function classifyStateAction(words: readonly string[]): StateActionClassification {
|
|
138
|
+
const classification = classifyStateArgv(words.slice(2));
|
|
139
|
+
if (
|
|
140
|
+
classification.unknownFlags.length > 0 ||
|
|
141
|
+
classification.flags.some(flag => flag.malformed) ||
|
|
142
|
+
!hasDocumentedStatePositionals(classification)
|
|
143
|
+
) {
|
|
144
|
+
return malformedStateShape();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const modeFlags = classification.flags.filter(flag => flag.name === "--mode");
|
|
148
|
+
const inputFlags = classification.flags.filter(flag => flag.name === "--input");
|
|
149
|
+
if (modeFlags.length > 1 || inputFlags.length > 1) {
|
|
150
|
+
return { reason: "restricted role-agent bash rejects repeated or conflicting `gjc state` target selectors" };
|
|
151
|
+
}
|
|
152
|
+
if (classification.inputs.some(input => input.kind === "file")) {
|
|
153
|
+
return { reason: "restricted role-agent bash does not allow file-backed `gjc state --input` values" };
|
|
118
154
|
}
|
|
155
|
+
if (classification.inputs.some(input => input.kind === "invalid")) return malformedStateShape();
|
|
119
156
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
157
|
+
const runtimeTarget = classification.runtimeSelectorCandidates.find(candidate => candidate.value)?.value;
|
|
158
|
+
const suppliedTargets = classification.selectorCandidates
|
|
159
|
+
.map(candidate => candidate.value)
|
|
160
|
+
.filter((target): target is string => target !== undefined);
|
|
161
|
+
const distinctTargets = new Set(suppliedTargets);
|
|
162
|
+
if (distinctTargets.size > 1 || (runtimeTarget && suppliedTargets.some(target => target !== runtimeTarget))) {
|
|
163
|
+
return {
|
|
164
|
+
reason:
|
|
165
|
+
"restricted role-agent bash rejects conflicting `gjc state` selectors that disagree with runtime precedence",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!runtimeTarget || !CANONICAL_STATE_TARGETS.has(runtimeTarget)) {
|
|
169
|
+
return { reason: "restricted role-agent bash requires a canonical workflow skill for `gjc state` commands" };
|
|
170
|
+
}
|
|
171
|
+
return { parsed: { action: classification.effectiveAction, target: runtimeTarget } };
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
function optionWords(words: readonly string[]): string[] {
|
|
@@ -189,15 +235,19 @@ function validateMatchedGjcCommand(words: readonly string[]): BashAllowedPrefixe
|
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
if (words[1] === "state") {
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
238
|
+
const classification = classifyStateAction(words);
|
|
239
|
+
if (!classification.parsed) {
|
|
194
240
|
return {
|
|
195
241
|
allowed: false,
|
|
196
|
-
reason:
|
|
242
|
+
reason:
|
|
243
|
+
classification.reason ?? "restricted role-agent bash only allows documented `gjc state` action shapes",
|
|
197
244
|
};
|
|
198
245
|
}
|
|
199
|
-
if (!ALLOWED_STATE_ACTIONS.has(action)) {
|
|
200
|
-
return {
|
|
246
|
+
if (!ALLOWED_STATE_ACTIONS.has(classification.parsed.action)) {
|
|
247
|
+
return {
|
|
248
|
+
allowed: false,
|
|
249
|
+
reason: `restricted role-agent bash does not allow \`gjc state ${classification.parsed.action}\``,
|
|
250
|
+
};
|
|
201
251
|
}
|
|
202
252
|
return { allowed: true };
|
|
203
253
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getPuppeteerDir } from "@gajae-code/utils/dirs";
|
|
5
|
+
import * as logger from "@gajae-code/utils/logger";
|
|
6
|
+
import { $which } from "@gajae-code/utils/which";
|
|
5
7
|
import * as browsers from "@puppeteer/browsers";
|
|
6
8
|
import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
|
|
7
9
|
import { PUPPETEER_REVISIONS } from "puppeteer-core/internal/revisions.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readability } from "@mozilla/readability";
|
|
2
2
|
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
3
|
-
import { htmlToBasicMarkdown } from "../../web/scrapers/
|
|
3
|
+
import { htmlToBasicMarkdown } from "../../web/scrapers/html-to-markdown";
|
|
4
4
|
|
|
5
5
|
export type ReadableFormat = "text" | "markdown";
|
|
6
6
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
|
|
3
|
+
import { formatDimensionNote, type ResizedImage } from "../../utils/image-resize";
|
|
4
|
+
|
|
5
|
+
function shortenPath(filePath: string): string {
|
|
6
|
+
const home = os.homedir();
|
|
7
|
+
return home && filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatScreenshot(opts: {
|
|
11
|
+
saveFullRes: boolean;
|
|
12
|
+
savedMimeType: string;
|
|
13
|
+
savedByteLength: number;
|
|
14
|
+
dest: string;
|
|
15
|
+
resized: ResizedImage;
|
|
16
|
+
}): string[] {
|
|
17
|
+
const lines = ["Screenshot captured"];
|
|
18
|
+
if (opts.saveFullRes) {
|
|
19
|
+
lines.push(
|
|
20
|
+
`Saved: ${opts.savedMimeType} (${(opts.savedByteLength / 1024).toFixed(2)} KB) to ${shortenPath(opts.dest)}`,
|
|
21
|
+
);
|
|
22
|
+
lines.push(
|
|
23
|
+
`Model: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB, ${opts.resized.width}x${opts.resized.height})`,
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
lines.push(`Format: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB)`);
|
|
27
|
+
lines.push(`Dimensions: ${opts.resized.width}x${opts.resized.height}`);
|
|
28
|
+
}
|
|
29
|
+
const dimensionNote = formatDimensionNote(opts.resized);
|
|
30
|
+
if (dimensionNote) {
|
|
31
|
+
lines.push(dimensionNote);
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
@@ -63,6 +63,7 @@ export type WorkerInitPayload =
|
|
|
63
63
|
export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
|
|
64
64
|
|
|
65
65
|
export type WorkerInbound =
|
|
66
|
+
| { type: "bootstrap"; version: 1; mode: "native-free" }
|
|
66
67
|
| { type: "init"; payload: WorkerInitPayload }
|
|
67
68
|
| { type: "run"; id: string; name: string; code: string; timeoutMs: number; session: SessionSnapshot }
|
|
68
69
|
| { type: "abort"; id: string }
|
|
@@ -91,6 +92,8 @@ export interface RunErrorPayload {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export type WorkerOutbound =
|
|
95
|
+
| { type: "bootstrap-ready"; version: 1; mode: "native-free" }
|
|
96
|
+
| { type: "bootstrap-failed"; error: string }
|
|
94
97
|
| { type: "ready"; info: ReadyInfo }
|
|
95
98
|
| { type: "init-failed"; error: RunErrorPayload }
|
|
96
99
|
| { type: "result"; id: string; ok: true; payload: RunResultOk }
|