@sayknow-cli/coding-agent 0.2.6 → 0.3.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/CHANGELOG.md +32 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +71 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/defaults/skc-defaults.d.ts +13 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/plugins/index.d.ts +1 -0
- package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/plugin-cli.ts +63 -6
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +61 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/defaults/skc-defaults.ts +27 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/manager.ts +50 -0
- package/src/extensibility/plugins/marketplace/manager.ts +42 -0
- package/src/extensibility/plugins/security-scanner.ts +477 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +10 -8
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +10 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +27 -2
- package/src/skc-runtime/workflow-manifest.ts +11 -1
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -898,32 +898,29 @@ function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
|
|
|
898
898
|
return "other";
|
|
899
899
|
}
|
|
900
900
|
|
|
901
|
-
function
|
|
901
|
+
function isComputerControlSurfaceCategory(category: UltragoalChangeCategory): boolean {
|
|
902
902
|
return (
|
|
903
|
-
|
|
904
|
-
(row.oldPath ? categorizeComputerChangePath(row.oldPath) !== "other" : false)
|
|
903
|
+
category === "code" || category === "generated-binding" || category === "tool" || category === "settings-registry"
|
|
905
904
|
);
|
|
906
905
|
}
|
|
907
906
|
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
|
|
913
|
-
return category === "docs-static" && oldCategory === "docs-static";
|
|
914
|
-
});
|
|
907
|
+
function isComputerControlSurfaceChangePath(row: UltragoalChangeSetPath): boolean {
|
|
908
|
+
const category = row.category ?? categorizeComputerChangePath(row.path);
|
|
909
|
+
const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
|
|
910
|
+
return isComputerControlSurfaceCategory(category) || isComputerControlSurfaceCategory(oldCategory);
|
|
915
911
|
}
|
|
916
912
|
|
|
917
913
|
function trustedChangeSetRequiresComputerSuite(changeSet: UltragoalChangeSet | undefined): boolean {
|
|
918
914
|
if (!changeSet?.trusted) return false;
|
|
919
|
-
|
|
920
|
-
return changeSet.paths.some(isComputerChangePath);
|
|
915
|
+
return changeSet.paths.some(isComputerControlSurfaceChangePath);
|
|
921
916
|
}
|
|
922
917
|
|
|
923
918
|
function requiresComputerRedTeamSuite(executorQa: JsonObject, changeSet: UltragoalChangeSet | undefined): boolean {
|
|
924
919
|
if (trustedChangeSetRequiresComputerSuite(changeSet)) return true;
|
|
925
920
|
const declaredPaths = Array.isArray(executorQa.changedPaths) ? executorQa.changedPaths : [];
|
|
926
|
-
return declaredPaths.some(
|
|
921
|
+
return declaredPaths.some(
|
|
922
|
+
value => typeof value === "string" && isComputerControlSurfaceCategory(categorizeComputerChangePath(value)),
|
|
923
|
+
);
|
|
927
924
|
}
|
|
928
925
|
|
|
929
926
|
function normalizeAdversarialCaseId(value: string): string {
|
|
@@ -2853,6 +2850,33 @@ export async function recordUltragoalReviewBlockers(input: {
|
|
|
2853
2850
|
return plan;
|
|
2854
2851
|
}
|
|
2855
2852
|
|
|
2853
|
+
export type UltragoalBlockerClassification = "human_blocked" | "resolvable";
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* Record an audited blocker triage classification in the durable ledger. A
|
|
2857
|
+
* `human_blocked` classification is the only thing that authorizes
|
|
2858
|
+
* `goal({"op":"pause"})` while an Ultragoal run is active; `resolvable` is an
|
|
2859
|
+
* audit note and never unblocks pause.
|
|
2860
|
+
*/
|
|
2861
|
+
export async function recordUltragoalBlockerClassification(input: {
|
|
2862
|
+
cwd: string;
|
|
2863
|
+
classification: UltragoalBlockerClassification;
|
|
2864
|
+
evidence: string;
|
|
2865
|
+
goalId?: string;
|
|
2866
|
+
}): Promise<UltragoalLedgerEvent> {
|
|
2867
|
+
const evidence = input.evidence.trim();
|
|
2868
|
+
if (!evidence) throw new Error("classify-blocker --evidence is required");
|
|
2869
|
+
if (input.classification !== "human_blocked" && input.classification !== "resolvable") {
|
|
2870
|
+
throw new Error('classify-blocker --classification must be "human_blocked" or "resolvable"');
|
|
2871
|
+
}
|
|
2872
|
+
return appendLedger(input.cwd, {
|
|
2873
|
+
event: "blocker_classified",
|
|
2874
|
+
classification: input.classification,
|
|
2875
|
+
...(input.goalId?.trim() ? { goalId: input.goalId.trim() } : {}),
|
|
2876
|
+
evidence,
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2856
2880
|
type UltragoalReviewMode = "review-only" | "review-start";
|
|
2857
2881
|
type UltragoalReviewContractStrength = "strong" | "thin-derived";
|
|
2858
2882
|
|
|
@@ -2920,11 +2944,33 @@ async function spawnText(
|
|
|
2920
2944
|
}
|
|
2921
2945
|
}
|
|
2922
2946
|
|
|
2923
|
-
async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2947
|
+
export async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
|
|
2948
|
+
if (branch) {
|
|
2949
|
+
const exists = await spawnText(["git", "rev-parse", "--verify", branch], { cwd, timeoutMs: 3000 });
|
|
2950
|
+
if (exists.ok) return branch;
|
|
2951
|
+
} else {
|
|
2952
|
+
// Prefer the NEAREST integration base (the branch this work actually forks
|
|
2953
|
+
// from) rather than always `main`. A branch opened against `dev` must be
|
|
2954
|
+
// scoped to `dev`; using a stale `main` sweeps in unrelated trunk history
|
|
2955
|
+
// and mis-attributes other people's changes to this story (e.g. falsely
|
|
2956
|
+
// tripping change-scoped gates). Among existing candidates, pick the one
|
|
2957
|
+
// whose merge-base with HEAD is closest to HEAD (fewest commits ahead).
|
|
2958
|
+
const candidates = ["origin/dev", "dev", "origin/main", "origin/master", "main", "master"];
|
|
2959
|
+
let best: { ref: string; ahead: number } | undefined;
|
|
2960
|
+
for (const candidate of candidates) {
|
|
2961
|
+
const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
|
|
2962
|
+
if (!exists.ok) continue;
|
|
2963
|
+
const mergeBase = await spawnText(["git", "merge-base", "HEAD", candidate], { cwd, timeoutMs: 3000 });
|
|
2964
|
+
if (!mergeBase.ok || !mergeBase.stdout.trim()) continue;
|
|
2965
|
+
const count = await spawnText(["git", "rev-list", "--count", `${mergeBase.stdout.trim()}..HEAD`], {
|
|
2966
|
+
cwd,
|
|
2967
|
+
timeoutMs: 3000,
|
|
2968
|
+
});
|
|
2969
|
+
const ahead = Number.parseInt(count.stdout.trim(), 10);
|
|
2970
|
+
if (!Number.isFinite(ahead)) continue;
|
|
2971
|
+
if (!best || ahead < best.ahead) best = { ref: candidate, ahead };
|
|
2972
|
+
}
|
|
2973
|
+
if (best) return best.ref;
|
|
2928
2974
|
}
|
|
2929
2975
|
const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
|
|
2930
2976
|
if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
|
|
@@ -3245,6 +3291,7 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
3245
3291
|
"--rationale",
|
|
3246
3292
|
"--replacements-json",
|
|
3247
3293
|
"--order-json",
|
|
3294
|
+
"--classification",
|
|
3248
3295
|
]);
|
|
3249
3296
|
|
|
3250
3297
|
function isHelpArg(arg: string): boolean {
|
|
@@ -3319,6 +3366,25 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3319
3366
|
"",
|
|
3320
3367
|
].join("\n");
|
|
3321
3368
|
}
|
|
3369
|
+
if (subject === "classify-blocker") {
|
|
3370
|
+
return [
|
|
3371
|
+
"Run native SKC Ultragoal workflow commands",
|
|
3372
|
+
"",
|
|
3373
|
+
"USAGE",
|
|
3374
|
+
" $ skc ultragoal classify-blocker --classification <human_blocked|resolvable> --evidence <text> [FLAGS]",
|
|
3375
|
+
"",
|
|
3376
|
+
"FLAGS",
|
|
3377
|
+
" --classification=<value> Required. human_blocked authorizes pause only as the latest ledger event; resolvable never authorizes pause",
|
|
3378
|
+
" --evidence=<value> Required. Specific blocker evidence; must name the human-only dependency for human_blocked",
|
|
3379
|
+
" --goal-id=<value> Optional durable .skc/ultragoal goal id, e.g. G001",
|
|
3380
|
+
" --json Output a machine-readable receipt",
|
|
3381
|
+
"",
|
|
3382
|
+
"EXAMPLES",
|
|
3383
|
+
' $ skc ultragoal classify-blocker --classification resolvable --evidence "failing test can be fixed autonomously"',
|
|
3384
|
+
' $ skc ultragoal classify-blocker --classification human_blocked --evidence "user must provide production API credentials" --goal-id G001',
|
|
3385
|
+
"",
|
|
3386
|
+
].join("\n");
|
|
3387
|
+
}
|
|
3322
3388
|
return [
|
|
3323
3389
|
"Run native SKC Ultragoal workflow commands",
|
|
3324
3390
|
"",
|
|
@@ -3333,8 +3399,9 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3333
3399
|
" review",
|
|
3334
3400
|
" steer",
|
|
3335
3401
|
" record-review-blockers",
|
|
3402
|
+
" classify-blocker",
|
|
3336
3403
|
"",
|
|
3337
|
-
"Run `skc ultragoal checkpoint --help` or `skc ultragoal
|
|
3404
|
+
"Run `skc ultragoal checkpoint --help`, `skc ultragoal review --help`, or `skc ultragoal classify-blocker --help` for command-specific requirements.",
|
|
3338
3405
|
"",
|
|
3339
3406
|
].join("\n");
|
|
3340
3407
|
}
|
|
@@ -3653,6 +3720,24 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
3653
3720
|
: "Recorded review blockers.\n",
|
|
3654
3721
|
};
|
|
3655
3722
|
}
|
|
3723
|
+
case "classify-blocker": {
|
|
3724
|
+
const event = await recordUltragoalBlockerClassification({
|
|
3725
|
+
cwd,
|
|
3726
|
+
classification: (flagValue(args, "--classification") ?? "") as UltragoalBlockerClassification,
|
|
3727
|
+
evidence: flagValue(args, "--evidence") ?? "",
|
|
3728
|
+
goalId: flagValue(args, "--goal-id"),
|
|
3729
|
+
});
|
|
3730
|
+
return {
|
|
3731
|
+
status: 0,
|
|
3732
|
+
stdout: json
|
|
3733
|
+
? renderCliWriteReceipt({
|
|
3734
|
+
ok: true,
|
|
3735
|
+
event: "blocker_classified",
|
|
3736
|
+
classification: event.classification,
|
|
3737
|
+
})
|
|
3738
|
+
: `Recorded blocker classification: ${String(event.classification)}.\n`,
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3656
3741
|
default:
|
|
3657
3742
|
return { status: 1, stderr: `Unknown skc ultragoal command: ${command}\n` };
|
|
3658
3743
|
}
|
|
@@ -3670,6 +3755,7 @@ const RECONCILE_COMMANDS = new Set([
|
|
|
3670
3755
|
"steer",
|
|
3671
3756
|
"record-review-blockers",
|
|
3672
3757
|
"review",
|
|
3758
|
+
"classify-blocker",
|
|
3673
3759
|
]);
|
|
3674
3760
|
|
|
3675
3761
|
/**
|
|
@@ -1443,7 +1443,8 @@
|
|
|
1443
1443
|
"appliesToVerbs": [
|
|
1444
1444
|
"checkpoint",
|
|
1445
1445
|
"record-review-blockers",
|
|
1446
|
-
"steer"
|
|
1446
|
+
"steer",
|
|
1447
|
+
"classify-blocker"
|
|
1447
1448
|
],
|
|
1448
1449
|
"name": "evidence",
|
|
1449
1450
|
"required": true,
|
|
@@ -1471,6 +1472,25 @@
|
|
|
1471
1472
|
"name": "goal-id",
|
|
1472
1473
|
"type": "string"
|
|
1473
1474
|
},
|
|
1475
|
+
{
|
|
1476
|
+
"appliesToVerbs": [
|
|
1477
|
+
"classify-blocker"
|
|
1478
|
+
],
|
|
1479
|
+
"name": "goal-id",
|
|
1480
|
+
"type": "string"
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
"appliesToVerbs": [
|
|
1484
|
+
"classify-blocker"
|
|
1485
|
+
],
|
|
1486
|
+
"enumValues": [
|
|
1487
|
+
"human_blocked",
|
|
1488
|
+
"resolvable"
|
|
1489
|
+
],
|
|
1490
|
+
"name": "classification",
|
|
1491
|
+
"required": true,
|
|
1492
|
+
"type": "enum"
|
|
1493
|
+
},
|
|
1474
1494
|
{
|
|
1475
1495
|
"appliesToVerbs": [
|
|
1476
1496
|
"steer"
|
|
@@ -1570,7 +1590,8 @@
|
|
|
1570
1590
|
"review",
|
|
1571
1591
|
"checkpoint",
|
|
1572
1592
|
"record-review-blockers",
|
|
1573
|
-
"steer"
|
|
1593
|
+
"steer",
|
|
1594
|
+
"classify-blocker"
|
|
1574
1595
|
],
|
|
1575
1596
|
"name": "json",
|
|
1576
1597
|
"type": "boolean"
|
|
@@ -1651,6 +1672,10 @@
|
|
|
1651
1672
|
"name": "steer",
|
|
1652
1673
|
"surface": "command-positional"
|
|
1653
1674
|
},
|
|
1675
|
+
{
|
|
1676
|
+
"name": "classify-blocker",
|
|
1677
|
+
"surface": "command-positional"
|
|
1678
|
+
},
|
|
1654
1679
|
{
|
|
1655
1680
|
"name": "graph",
|
|
1656
1681
|
"planned": true,
|
|
@@ -237,6 +237,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
|
|
|
237
237
|
"review",
|
|
238
238
|
"record-review-blockers",
|
|
239
239
|
"steer",
|
|
240
|
+
"classify-blocker",
|
|
240
241
|
]),
|
|
241
242
|
...plannedVerbs(PLANNED_ADMIN_VERBS),
|
|
242
243
|
],
|
|
@@ -263,11 +264,19 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
|
|
|
263
264
|
name: "evidence",
|
|
264
265
|
type: "string",
|
|
265
266
|
required: true,
|
|
266
|
-
appliesToVerbs: ["checkpoint", "record-review-blockers", "steer"],
|
|
267
|
+
appliesToVerbs: ["checkpoint", "record-review-blockers", "steer", "classify-blocker"],
|
|
267
268
|
},
|
|
268
269
|
{ name: "skc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
|
|
269
270
|
{ name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
|
|
270
271
|
{ name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
|
|
272
|
+
{ name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },
|
|
273
|
+
{
|
|
274
|
+
name: "classification",
|
|
275
|
+
type: "enum",
|
|
276
|
+
enumValues: ["human_blocked", "resolvable"],
|
|
277
|
+
required: true,
|
|
278
|
+
appliesToVerbs: ["classify-blocker"],
|
|
279
|
+
},
|
|
271
280
|
{
|
|
272
281
|
name: "kind",
|
|
273
282
|
type: "enum",
|
|
@@ -307,6 +316,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalSkcWorkflowSkill, SkillManifest>
|
|
|
307
316
|
"checkpoint",
|
|
308
317
|
"record-review-blockers",
|
|
309
318
|
"steer",
|
|
319
|
+
"classify-blocker",
|
|
310
320
|
],
|
|
311
321
|
},
|
|
312
322
|
{ name: "directive-json", type: "string", appliesToVerbs: ["steer"], planned: true },
|
|
@@ -798,7 +798,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
798
798
|
return;
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
-
runtime.ctx.
|
|
801
|
+
runtime.ctx.showOAuthSelector("login", undefined, {
|
|
802
|
+
allowExternalCredentialDiscovery: true,
|
|
803
|
+
trigger: "bare-login",
|
|
804
|
+
});
|
|
802
805
|
runtime.ctx.editor.setText("");
|
|
803
806
|
},
|
|
804
807
|
},
|
package/src/task/executor.ts
CHANGED
|
@@ -1298,11 +1298,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1298
1298
|
requestPause: () => {
|
|
1299
1299
|
pauseRequested = true;
|
|
1300
1300
|
},
|
|
1301
|
-
injectMessage: async (content, deliverAs) => {
|
|
1301
|
+
injectMessage: async (content, deliverAs, opts) => {
|
|
1302
1302
|
if (deliverAs === "nextTurn") {
|
|
1303
1303
|
await session.prompt(content, { attribution: "agent" });
|
|
1304
1304
|
return;
|
|
1305
1305
|
}
|
|
1306
|
+
if (deliverAs === "steer") {
|
|
1307
|
+
const from = opts?.fromAgentId ?? manager.getSubagentRecord(liveSubagentId)?.ownerId ?? "?";
|
|
1308
|
+
session.emitSubagentSteerObservation({ from, to: liveSubagentId, body: content });
|
|
1309
|
+
}
|
|
1306
1310
|
await session.sendUserMessage(content, { deliverAs });
|
|
1307
1311
|
},
|
|
1308
1312
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry mapping a session id to its active {@link AskAnswerSource}.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the `ask` tool (which reads the source via `AgentSession`) from the
|
|
5
|
+
* notifications extension (which registers one), without threading a new method
|
|
6
|
+
* through the extension/runner/controller wiring. A session has at most one
|
|
7
|
+
* source; registering returns a disposer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AskAnswerSource } from "./index";
|
|
11
|
+
|
|
12
|
+
const sources = new Map<string, AskAnswerSource>();
|
|
13
|
+
|
|
14
|
+
/** Register `source` for `sessionId`. Returns a disposer that clears it. */
|
|
15
|
+
export function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void {
|
|
16
|
+
sources.set(sessionId, source);
|
|
17
|
+
return () => {
|
|
18
|
+
if (sources.get(sessionId) === source) sources.delete(sessionId);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The answer source for `sessionId`, if one is registered. */
|
|
23
|
+
export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined {
|
|
24
|
+
return sources.get(sessionId);
|
|
25
|
+
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -430,8 +430,18 @@ async function askSingleQuestion(
|
|
|
430
430
|
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
431
431
|
}
|
|
432
432
|
} else {
|
|
433
|
-
|
|
434
|
-
|
|
433
|
+
const stripped = stripRecommendedSuffix(choice);
|
|
434
|
+
if (optionLabels.includes(stripped)) {
|
|
435
|
+
selectedOptions = [stripped];
|
|
436
|
+
customInput = undefined;
|
|
437
|
+
} else {
|
|
438
|
+
// A remote answer (e.g. a typed Telegram reply) that is not one of the
|
|
439
|
+
// listed options is the "provide my own" custom input — recorded the same
|
|
440
|
+
// as picking Other and typing it. The local selector can only ever return
|
|
441
|
+
// a listed entry, so this branch is reached only for free-text answers.
|
|
442
|
+
customInput = choice;
|
|
443
|
+
selectedOptions = [];
|
|
444
|
+
}
|
|
435
445
|
}
|
|
436
446
|
if (navigation?.allowForward) {
|
|
437
447
|
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
|
@@ -551,11 +561,71 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
551
561
|
const ui: UIContext = {
|
|
552
562
|
select: (prompt, options, dialogOptions) => {
|
|
553
563
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
554
|
-
|
|
564
|
+
const source = this.session.getAskAnswerSource?.();
|
|
565
|
+
if (!source) return extensionUi.select(prompt, options, dialogOptions);
|
|
566
|
+
// Race the local UI against a remote answer (e.g. a Telegram reply via the
|
|
567
|
+
// notifications SDK) so asks can be answered without RPC mode. When the
|
|
568
|
+
// local UI wins, abort the remote source so it stops waiting and marks the
|
|
569
|
+
// action resolved-locally. First valid answer wins.
|
|
570
|
+
// Race the local UI against a remote answer (e.g. a Telegram reply via the
|
|
571
|
+
// notifications SDK) so asks can be answered without RPC mode. First valid
|
|
572
|
+
// answer wins; the loser is aborted so neither side is left hanging:
|
|
573
|
+
// - local wins -> abort the remote source (marks the action resolved-locally)
|
|
574
|
+
// - remote wins -> abort the local selector so the TUI dialog actually closes
|
|
575
|
+
const remoteController = new AbortController();
|
|
576
|
+
const localController = new AbortController();
|
|
577
|
+
// Propagate an external cancel (the tool's signal) to the local selector too.
|
|
578
|
+
const toolSignal = dialogOptions?.signal;
|
|
579
|
+
if (toolSignal) {
|
|
580
|
+
if (toolSignal.aborted) localController.abort();
|
|
581
|
+
else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
|
|
582
|
+
}
|
|
583
|
+
const remote = source.awaitAnswer(prompt, options, remoteController.signal).then(answer => {
|
|
584
|
+
// undefined is not a valid remote answer (registration failed, or the local
|
|
585
|
+
// UI already won and aborted us): never settle the race, let the local
|
|
586
|
+
// selector decide instead of cancelling the ask.
|
|
587
|
+
if (answer === undefined) return new Promise<string | undefined>(() => {});
|
|
588
|
+
localController.abort();
|
|
589
|
+
return answer;
|
|
590
|
+
});
|
|
591
|
+
const local = extensionUi
|
|
592
|
+
.select(prompt, options, { ...dialogOptions, signal: localController.signal })
|
|
593
|
+
.then(answer => {
|
|
594
|
+
remoteController.abort();
|
|
595
|
+
return answer;
|
|
596
|
+
});
|
|
597
|
+
// The losing selector may reject when aborted after the race already settled;
|
|
598
|
+
// swallow that so it is not an unhandled rejection (the race result is unaffected).
|
|
599
|
+
void local.catch(() => undefined);
|
|
600
|
+
return Promise.race([local, remote]);
|
|
555
601
|
},
|
|
556
602
|
editor: (title, prefill, dialogOptions, editorOptions) => {
|
|
557
603
|
if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
|
|
558
|
-
|
|
604
|
+
const source = this.session.getAskAnswerSource?.();
|
|
605
|
+
if (!source) return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
|
|
606
|
+
// Race the local editor against a remote free-text answer so "Other / type
|
|
607
|
+
// your own" custom input can be provided remotely (e.g. a typed Telegram
|
|
608
|
+
// reply) instead of blocking on the local-only editor. Mirrors `select`.
|
|
609
|
+
const remoteController = new AbortController();
|
|
610
|
+
const localController = new AbortController();
|
|
611
|
+
const toolSignal = dialogOptions?.signal;
|
|
612
|
+
if (toolSignal) {
|
|
613
|
+
if (toolSignal.aborted) localController.abort();
|
|
614
|
+
else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
|
|
615
|
+
}
|
|
616
|
+
const remote = source.awaitAnswer(title, [], remoteController.signal).then(answer => {
|
|
617
|
+
if (answer === undefined) return new Promise<string | undefined>(() => {});
|
|
618
|
+
localController.abort();
|
|
619
|
+
return answer;
|
|
620
|
+
});
|
|
621
|
+
const local = extensionUi
|
|
622
|
+
.editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
|
|
623
|
+
.then(answer => {
|
|
624
|
+
remoteController.abort();
|
|
625
|
+
return answer;
|
|
626
|
+
});
|
|
627
|
+
void local.catch(() => undefined);
|
|
628
|
+
return Promise.race([local, remote]);
|
|
559
629
|
},
|
|
560
630
|
};
|
|
561
631
|
|
|
@@ -605,8 +675,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
605
675
|
}
|
|
606
676
|
try {
|
|
607
677
|
const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
|
|
678
|
+
const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
|
|
608
679
|
const displayQuestion = deepInterviewPrompt ?? q.question;
|
|
609
|
-
const shouldNumberOptions = isDeepInterviewAskQuestion(q.question);
|
|
680
|
+
const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
|
|
610
681
|
const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
|
|
611
682
|
const initialSelection =
|
|
612
683
|
shouldNumberOptions && options?.previous
|
|
@@ -630,7 +701,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
630
701
|
signal,
|
|
631
702
|
initialSelection,
|
|
632
703
|
navigation: options?.navigation,
|
|
633
|
-
scrollTitleRows:
|
|
704
|
+
scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
|
|
634
705
|
otherOptionLabel: shouldNumberOptions
|
|
635
706
|
? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
|
|
636
707
|
: undefined,
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -472,20 +472,17 @@ async function findImageApiKey(
|
|
|
472
472
|
const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
|
|
473
473
|
if (openAI) return openAI;
|
|
474
474
|
// Fall through to auto-detect if preferred provider key not found.
|
|
475
|
-
} else if (preferredImageProvider === "antigravity"
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
475
|
+
} else if (preferredImageProvider === "antigravity") {
|
|
476
|
+
if (!modelRegistry) return null;
|
|
477
|
+
return await findAntigravityCredentials(modelRegistry, sessionId);
|
|
479
478
|
} else if (preferredImageProvider === "gemini") {
|
|
480
479
|
const geminiKey = getEnvApiKey("google");
|
|
481
480
|
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
482
481
|
const googleKey = $env.GOOGLE_API_KEY;
|
|
483
|
-
|
|
484
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
482
|
+
return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
|
|
485
483
|
} else if (preferredImageProvider === "openrouter") {
|
|
486
484
|
const openRouterKey = getEnvApiKey("openrouter");
|
|
487
|
-
|
|
488
|
-
// Fall through to auto-detect if preferred provider key not found.
|
|
485
|
+
return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
|
|
489
486
|
}
|
|
490
487
|
|
|
491
488
|
// Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
|
package/src/tools/index.ts
CHANGED
|
@@ -118,6 +118,18 @@ export type {
|
|
|
118
118
|
DiscoverableToolSource,
|
|
119
119
|
} from "../tool-discovery/tool-index";
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Source of remote answers for interactive asks (e.g. a Telegram reply routed
|
|
123
|
+
* through the notifications SDK). Lets a pending ask resolve without RPC mode.
|
|
124
|
+
*/
|
|
125
|
+
export interface AskAnswerSource {
|
|
126
|
+
/**
|
|
127
|
+
* Race a remote answer against the local UI for one question. Resolves with the
|
|
128
|
+
* chosen option label or free-text answer, or `undefined` to defer to local UI.
|
|
129
|
+
*/
|
|
130
|
+
awaitAnswer(question: string, options: string[], signal?: AbortSignal): Promise<string | undefined>;
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
/** Session context for tool factories */
|
|
122
134
|
export interface ToolSession {
|
|
123
135
|
/** Current working directory */
|
|
@@ -214,6 +226,13 @@ export interface ToolSession {
|
|
|
214
226
|
getGoalModeState?: () => GoalModeState | undefined;
|
|
215
227
|
/** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
|
|
216
228
|
getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Optional remote answer source for interactive asks. When present, the ask
|
|
231
|
+
* tool races the local UI selection against a remote answer (e.g. a Telegram
|
|
232
|
+
* reply via the notifications SDK) so asks can be answered without RPC mode.
|
|
233
|
+
* No-op when undefined: the ask path behaves exactly as before.
|
|
234
|
+
*/
|
|
235
|
+
getAskAnswerSource?: () => AskAnswerSource | undefined;
|
|
217
236
|
/** Optional per-session restriction for goal tool operations. */
|
|
218
237
|
goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
|
|
219
238
|
/** Goal runtime for the active agent session. */
|
|
@@ -78,21 +78,26 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
|
|
81
|
-
|
|
81
|
+
const configuredVisionPattern = this.session.settings.getModelRole("vision")?.trim();
|
|
82
|
+
const configuredVisionModel = configuredVisionPattern ? resolvePattern("pi/vision") : undefined;
|
|
83
|
+
if (configuredVisionPattern && !configuredVisionModel) {
|
|
84
|
+
throw new ToolError(
|
|
85
|
+
`Configured modelRoles.vision (${configuredVisionPattern}) did not resolve to an available model. Configure modelRoles.vision with a vision-capable model.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const model = configuredVisionModel ?? resolvePattern("pi/default") ?? resolvePattern(activeModelPattern);
|
|
82
89
|
if (!model) {
|
|
83
|
-
throw new ToolError(
|
|
90
|
+
throw new ToolError(
|
|
91
|
+
"Unable to resolve a model for inspect_image. Configure modelRoles.vision with a vision-capable model or select a vision-capable active/default model.",
|
|
92
|
+
);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
// inspect_image requires image input
|
|
87
|
-
//
|
|
95
|
+
// inspect_image requires image input. A text-only selected model must be
|
|
96
|
+
// paired with an explicit vision role so the model/cost boundary is visible.
|
|
88
97
|
if (!model.input.includes("image")) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`Resolved model ${model.provider}/${model.id} does not support image input, and no vision-capable model is available. Configure a vision-capable model.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
model = visionModel;
|
|
98
|
+
throw new ToolError(
|
|
99
|
+
`Resolved model ${model.provider}/${model.id} does not support image input. Configure modelRoles.vision with a vision-capable model.`,
|
|
100
|
+
);
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
const apiKey = await modelRegistry.getApiKey(model);
|
|
@@ -160,6 +160,13 @@ function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolea
|
|
|
160
160
|
lines.push(` ${theme.fg("dim", "Assignment:")}`);
|
|
161
161
|
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
162
162
|
}
|
|
163
|
+
if (snapshot.steerMessage) {
|
|
164
|
+
lines.push(` ${theme.fg("accent", `Steer (${snapshot.steerState ?? "queued"})`)}`);
|
|
165
|
+
const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
|
|
166
|
+
for (const pl of getPreviewLines(snapshot.steerMessage, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
|
|
167
|
+
lines.push(` ${theme.fg("toolOutput", replaceTabs(pl))}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
163
170
|
|
|
164
171
|
// Defense in depth: the producer only attaches `progress` when a live producer
|
|
165
172
|
// exists (subagent.ts #liveProgressFields), but the renderer also honors an
|