@gajae-code/coding-agent 0.18.10 → 0.18.12
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 +295 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
package/src/tools/subagent.ts
CHANGED
|
@@ -540,8 +540,11 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
540
540
|
if (!onUpdate || streamingDisabled) return;
|
|
541
541
|
try {
|
|
542
542
|
const result = this.#progressResult(manager, records, true);
|
|
543
|
-
const signature = subagentAwaitRenderedStateSignature(result.details?.subagents ?? []);
|
|
544
543
|
const t = nowMs();
|
|
544
|
+
const snapshots = result.details?.subagents ?? [];
|
|
545
|
+
const signature = this.session.settings.get("task.compactSubagentPanel")
|
|
546
|
+
? subagentCompactRenderedStateSignature(snapshots, Date.now())
|
|
547
|
+
: subagentAwaitRenderedStateSignature(snapshots);
|
|
545
548
|
const livenessDue = force || t - lastEmitMs >= livenessIntervalMs;
|
|
546
549
|
if (!livenessDue && signature === lastEmittedSignature) return;
|
|
547
550
|
lastEmittedSignature = signature;
|
|
@@ -1136,6 +1139,110 @@ export function subagentAwaitRenderedStateSignature(
|
|
|
1136
1139
|
});
|
|
1137
1140
|
}
|
|
1138
1141
|
|
|
1142
|
+
/** A running subagent with no activity for longer than this counts as idle in the compact panel. */
|
|
1143
|
+
export const SUBAGENT_COMPACT_IDLE_MS = 30_000;
|
|
1144
|
+
|
|
1145
|
+
/** Group counts behind the compact await panel's single summary line. */
|
|
1146
|
+
export interface SubagentGroupSummary {
|
|
1147
|
+
running: number;
|
|
1148
|
+
idle: number;
|
|
1149
|
+
retrying: number;
|
|
1150
|
+
queued: number;
|
|
1151
|
+
paused: number;
|
|
1152
|
+
completed: number;
|
|
1153
|
+
failed: number;
|
|
1154
|
+
cancelled: number;
|
|
1155
|
+
/** Running subagents in provider retry, per provider; drives the degradation warning. */
|
|
1156
|
+
retryingByProvider: Record<string, number>;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export function summarizeSubagentGroup(subagents: readonly SubagentSnapshot[], nowMs: number): SubagentGroupSummary {
|
|
1160
|
+
const summary: SubagentGroupSummary = {
|
|
1161
|
+
running: 0,
|
|
1162
|
+
idle: 0,
|
|
1163
|
+
retrying: 0,
|
|
1164
|
+
queued: 0,
|
|
1165
|
+
paused: 0,
|
|
1166
|
+
completed: 0,
|
|
1167
|
+
failed: 0,
|
|
1168
|
+
cancelled: 0,
|
|
1169
|
+
retryingByProvider: {},
|
|
1170
|
+
};
|
|
1171
|
+
for (const snapshot of subagents) {
|
|
1172
|
+
switch (snapshot.status) {
|
|
1173
|
+
case "running": {
|
|
1174
|
+
summary.running += 1;
|
|
1175
|
+
const live = snapshot.liveProgressAvailable !== false ? snapshot.progress : undefined;
|
|
1176
|
+
if (live?.status === "running" && live.retryState) {
|
|
1177
|
+
summary.retrying += 1;
|
|
1178
|
+
const provider = live.retryState.provider ?? "provider";
|
|
1179
|
+
summary.retryingByProvider[provider] = (summary.retryingByProvider[provider] ?? 0) + 1;
|
|
1180
|
+
}
|
|
1181
|
+
// Before its first activity a subagent has no timestamp; its elapsed job time
|
|
1182
|
+
// is then the age, so one stalled from the start still turns idle.
|
|
1183
|
+
const quietMs = live?.lastActivityMs !== undefined ? nowMs - live.lastActivityMs : snapshot.durationMs;
|
|
1184
|
+
if (quietMs > SUBAGENT_COMPACT_IDLE_MS) summary.idle += 1;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
case "queued":
|
|
1188
|
+
summary.queued += 1;
|
|
1189
|
+
break;
|
|
1190
|
+
case "paused":
|
|
1191
|
+
summary.paused += 1;
|
|
1192
|
+
break;
|
|
1193
|
+
case "completed":
|
|
1194
|
+
case "already_completed":
|
|
1195
|
+
summary.completed += 1;
|
|
1196
|
+
break;
|
|
1197
|
+
case "failed":
|
|
1198
|
+
case "not_found":
|
|
1199
|
+
summary.failed += 1;
|
|
1200
|
+
break;
|
|
1201
|
+
case "cancelled":
|
|
1202
|
+
summary.cancelled += 1;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return summary;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* The compact degradation row shows only the most-degraded provider, its count, and
|
|
1211
|
+
* how many other providers degrade (providers with a single retrying subagent are
|
|
1212
|
+
* not a degradation). Signing exactly that keeps invisible per-provider churn from
|
|
1213
|
+
* re-emitting.
|
|
1214
|
+
*/
|
|
1215
|
+
function compactDegradationSignature(
|
|
1216
|
+
retryingByProvider: Record<string, number>,
|
|
1217
|
+
): { provider: string; count: number; moreProviders: number } | null {
|
|
1218
|
+
const groups = Object.entries(retryingByProvider)
|
|
1219
|
+
.filter(([, count]) => count > 1)
|
|
1220
|
+
.sort(([a, countA], [b, countB]) => countB - countA || a.localeCompare(b));
|
|
1221
|
+
const [worst] = groups;
|
|
1222
|
+
return worst ? { provider: worst[0], count: worst[1], moreProviders: groups.length - 1 } : null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Rendered-state signature for the compact await panel. It covers only what the
|
|
1227
|
+
* compact view shows (group counts and the single degradation row), so per-subagent activity that does not move
|
|
1228
|
+
* a count no longer re-emits: the update rate is bounded by count transitions and
|
|
1229
|
+
* the liveness interval instead of growing with the number of live subagents.
|
|
1230
|
+
*/
|
|
1231
|
+
export function subagentCompactRenderedStateSignature(
|
|
1232
|
+
subagents: readonly SubagentSnapshot[],
|
|
1233
|
+
nowMs: number,
|
|
1234
|
+
receipt?: Pick<SubagentToolDetails, "awaitOutcome" | "interrupted">,
|
|
1235
|
+
): string {
|
|
1236
|
+
return JSON.stringify({
|
|
1237
|
+
awaitOutcome: receipt?.awaitOutcome ?? null,
|
|
1238
|
+
interrupted: receipt?.interrupted === true,
|
|
1239
|
+
summary: (({ retryingByProvider, ...counts }) => ({
|
|
1240
|
+
...counts,
|
|
1241
|
+
degradation: compactDegradationSignature(retryingByProvider),
|
|
1242
|
+
}))(summarizeSubagentGroup(subagents, nowMs)),
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1139
1246
|
function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
|
|
1140
1247
|
return {
|
|
1141
1248
|
id: snapshot.id,
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -268,6 +268,14 @@ function removeTasks(phases: TodoPhase[], entry: TodoOpEntryValue, errors: strin
|
|
|
268
268
|
return phases;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/** Type of a value that was expected to be a string, with its article. */
|
|
272
|
+
function describeArgument(value: unknown): string {
|
|
273
|
+
if (value === null) return "null";
|
|
274
|
+
if (Array.isArray(value)) return "an array";
|
|
275
|
+
if (typeof value === "object") return "an object";
|
|
276
|
+
return `a ${typeof value}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
271
279
|
function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
|
|
272
280
|
switch (entry.op) {
|
|
273
281
|
case "init":
|
|
@@ -302,6 +310,16 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
|
|
|
302
310
|
case "note": {
|
|
303
311
|
const hit = resolveTaskOrError(phases, entry.task, errors);
|
|
304
312
|
if (!hit) return phases;
|
|
313
|
+
// The schema types `text`, but bridges and replays reach `execute` without
|
|
314
|
+
// raw validation, and `.replace` on a non-string body used to crash the
|
|
315
|
+
// call. Reject it with a correction instead of storing it.
|
|
316
|
+
if (entry.text !== undefined && typeof entry.text !== "string") {
|
|
317
|
+
errors.push(
|
|
318
|
+
`Note text must be a string; got ${describeArgument(entry.text)}. ` +
|
|
319
|
+
'Pass the note body as "text", for example { op: "note", task: "<task content>", text: "why" }.',
|
|
320
|
+
);
|
|
321
|
+
return phases;
|
|
322
|
+
}
|
|
305
323
|
const text = (entry.text ?? "").replace(/\s+$/u, "");
|
|
306
324
|
if (!text) {
|
|
307
325
|
errors.push("Missing text for note operation");
|
|
@@ -337,6 +355,26 @@ export function applyOpsToPhases(
|
|
|
337
355
|
// Markdown round-trip
|
|
338
356
|
// =============================================================================
|
|
339
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Notes are declared as `string[]`, but the array is only typed, never validated:
|
|
360
|
+
* a session persisted elsewhere or a bridged caller that reached `execute`
|
|
361
|
+
* without raw validation can hand over any element, and every render path splits
|
|
362
|
+
* each note on newlines. One non-string element used to crash the call with a
|
|
363
|
+
* minified `.split is not a function` TypeError instead of a tool result. String
|
|
364
|
+
* notes render exactly as before.
|
|
365
|
+
*/
|
|
366
|
+
function noteDisplayText(note: unknown): string {
|
|
367
|
+
if (typeof note === "string") return note;
|
|
368
|
+
if (note !== null && typeof note === "object") {
|
|
369
|
+
try {
|
|
370
|
+
return JSON.stringify(note) ?? "[unrenderable note]";
|
|
371
|
+
} catch {
|
|
372
|
+
return "[unrenderable note]";
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return String(note);
|
|
376
|
+
}
|
|
377
|
+
|
|
340
378
|
const STATUS_TO_MARKER: Record<TodoStatus, string> = {
|
|
341
379
|
pending: " ",
|
|
342
380
|
in_progress: "/",
|
|
@@ -356,7 +394,7 @@ export function phasesToMarkdown(phases: TodoPhase[]): string {
|
|
|
356
394
|
if (task.notes && task.notes.length > 0) {
|
|
357
395
|
for (let j = 0; j < task.notes.length; j++) {
|
|
358
396
|
if (j > 0) out.push(" >");
|
|
359
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
397
|
+
for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
|
|
360
398
|
out.push(noteLine === "" ? " >" : ` > ${noteLine}`);
|
|
361
399
|
}
|
|
362
400
|
}
|
|
@@ -504,7 +542,7 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
|
|
504
542
|
if (task.status === "in_progress" && task.notes && task.notes.length > 0) {
|
|
505
543
|
for (let j = 0; j < task.notes.length; j++) {
|
|
506
544
|
if (j > 0) lines.push(" ---");
|
|
507
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
545
|
+
for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
|
|
508
546
|
lines.push(` ${noteLine}`);
|
|
509
547
|
}
|
|
510
548
|
}
|
|
@@ -685,7 +723,7 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
685
723
|
lines.push(` ${title}`);
|
|
686
724
|
for (let j = 0; j < task.notes.length; j++) {
|
|
687
725
|
if (j > 0) lines.push(` ${bar}`);
|
|
688
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
726
|
+
for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
|
|
689
727
|
lines.push(` ${bar} ${uiTheme.fg("dim", noteLine)}`);
|
|
690
728
|
}
|
|
691
729
|
}
|
|
@@ -27,7 +27,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
27
27
|
"read": {
|
|
28
28
|
"name": "read",
|
|
29
29
|
"label": "Read",
|
|
30
|
-
"description": "Read files, directories, archives, SQLite databases, images, documents, internal resources, and web URLs through a single `path` string.\n\n<instruction>\n- One tool for filesystem, archives, SQLite, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), internal URIs, and web URLs (reader-mode by default).\n- You SHOULD parallelize independent reads when exploring related files.\n- You SHOULD reach for `read` — not a browser/puppeteer tool — for fetching web content.\n</instruction>\n\n## Parameters\n\n- `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).\n- `truncation` — optional `head` | `last` | `both`; selects which end of an over-budget result to retain. Configured default: last (factory default: `last`); structural summaries and routes such as URLs, directories and converted documents default to `head`. A line-range selector still bounds the selection — this only picks which end of that selection survives the byte/line cap. SQLite row queries page via their own `limit`/`offset` and ignore it.\n## Selectors\nAppend `:<sel>` to `path`. The bare path falls back to the default mode.\n\n- _(none)_ — parseable code → structural summary (signatures kept, bodies elided); a plain text file → a bounded receipt of about 50 lines or 10 KiB, whichever is smaller; the configured truncation direction is last (factory default: last). Line+hash anchors keep their real file line numbers and a footer names the omitted range. Archive members use the larger 3000-line / 50 KiB budget. Converted documents, notebooks, URLs and directory listings still start from the beginning.\n- `:50` / `:50-` — read from line 50 onward.\n- `:50-200` — lines 50–200 inclusive.\n- `:50+150` — 150 lines starting at line 50.\n- `:20+1` — exactly one line.\n- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).\n- `:raw` — verbatim text; no anchors, no summary, no line prefixes.\n- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.\n- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.\n\n# Files\n\n- Reading a directory path returns a depth-limited dirent listing.\n- Parseable code without a selector returns a **structural summary**: declarations kept, large bodies collapsed to `..` (merged brace pair) or `…` (standalone). Summarized output ends with a footer of the form:\n\n `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`\n\n If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.\n- Structural summaries honor explicit `truncation`: `head` retains the beginning, `last` the end, and `both` both ends within the summary byte budget. Complete summary units and their original source anchors stay intact and in source order; budget omissions are marked where they occur. This does not switch to raw source. Summaries that fit are unchanged by direction.\n- Directional windows identify the retained first/last lines and the omitted range; use the `re-read <path>:1-<total>` or `:raw` hint in the footer to recover the full content.\n\n# Documents & Notebooks\n\nExtracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipynb`) are shown as editable `# %% [type] cell:N` text; edits round-trip back to the underlying JSON preserving notebook metadata. Add `:raw` to a notebook to bypass the converter and read the JSON directly.\n\n# Images\n\nReading an image path returns the image itself for visual inspection by a vision-capable model.\n\n# Archives\n\nSupports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read a member, and append a normal selector to the inner path: `archive.zip:dir/file.ts:50-60`.\n\n# SQLite\n\nFor `.sqlite`, `.sqlite3`, `.db`, `.db3`:\n- `file.db` — list tables with row counts\n- `file.db:table` — schema + sample rows\n- `file.db:table:key` — single row by primary key\n- `file.db:table?limit=50&offset=100` — paginated rows\n- `file.db:table?where=status='active'&order=created:desc` — filtered rows\n- `file.db?q=SELECT …` — read-only SELECT query\n\n# URLs\n\n- Default reader-mode: HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs → clean text/markdown.\n- `:raw` returns untouched HTML; line selectors (`:50`, `:50-100`, `:50+150`) paginate the cached fetched output.\n- Bare `host:port` URLs collide with the selector grammar — add a trailing slash before the selector: `https://example.com/:80`.\n\n# Internal URIs\n\n`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.\n\nBundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments
|
|
30
|
+
"description": "Read files, directories, archives, SQLite databases, images, documents, internal resources, and web URLs through a single `path` string.\n\n<instruction>\n- One tool for filesystem, archives, SQLite, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), internal URIs, and web URLs (reader-mode by default).\n- You SHOULD parallelize independent reads when exploring related files.\n- You SHOULD reach for `read` — not a browser/puppeteer tool — for fetching web content.\n</instruction>\n\n## Parameters\n\n- `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).\n- `truncation` — optional `head` | `last` | `both`; selects which end of an over-budget result to retain. Configured default: last (factory default: `last`); structural summaries and routes such as URLs, directories and converted documents default to `head`. A line-range selector still bounds the selection — this only picks which end of that selection survives the byte/line cap. SQLite row queries page via their own `limit`/`offset` and ignore it.\n## Selectors\nAppend `:<sel>` to `path`. The bare path falls back to the default mode.\n\n- _(none)_ — parseable code → structural summary (signatures kept, bodies elided); a plain text file → a bounded receipt of about 50 lines or 10 KiB, whichever is smaller; the configured truncation direction is last (factory default: last). Line+hash anchors keep their real file line numbers and a footer names the omitted range. Archive members use the larger 3000-line / 50 KiB budget. Converted documents, notebooks, URLs and directory listings still start from the beginning.\n- `:50` / `:50-` — read from line 50 onward.\n- `:50-200` — lines 50–200 inclusive.\n- `:50+150` — 150 lines starting at line 50.\n- `:20+1` — exactly one line.\n- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).\n- `:raw` — verbatim text; no anchors, no summary, no line prefixes.\n- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.\n- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.\n\n# Files\n\n- Reading a directory path returns a depth-limited dirent listing.\n- Parseable code without a selector returns a **structural summary**: declarations kept, large bodies collapsed to `..` (merged brace pair) or `…` (standalone). Summarized output ends with a footer of the form:\n\n `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`\n\n If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.\n- Structural summaries honor explicit `truncation`: `head` retains the beginning, `last` the end, and `both` both ends within the summary byte budget. Complete summary units and their original source anchors stay intact and in source order; budget omissions are marked where they occur. This does not switch to raw source. Summaries that fit are unchanged by direction.\n- Directional windows identify the retained first/last lines and the omitted range; use the `re-read <path>:1-<total>` or `:raw` hint in the footer to recover the full content.\n\n# Documents & Notebooks\n\nExtracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipynb`) are shown as editable `# %% [type] cell:N` text; edits round-trip back to the underlying JSON preserving notebook metadata. Add `:raw` to a notebook to bypass the converter and read the JSON directly.\n\n# Images\n\nReading an image path returns the image itself for visual inspection by a vision-capable model.\n\n# Archives\n\nSupports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read a member, and append a normal selector to the inner path: `archive.zip:dir/file.ts:50-60`.\n\n# SQLite\n\nFor `.sqlite`, `.sqlite3`, `.db`, `.db3`:\n- `file.db` — list tables with row counts\n- `file.db:table` — schema + sample rows\n- `file.db:table:key` — single row by primary key\n- `file.db:table?limit=50&offset=100` — paginated rows\n- `file.db:table?where=status='active'&order=created:desc` — filtered rows\n- `file.db?q=SELECT …` — read-only SELECT query\n\n# URLs\n\n- Default reader-mode: HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs → clean text/markdown.\n- `:raw` returns untouched HTML; line selectors (`:50`, `:50-100`, `:50+150`) paginate the cached fetched output.\n- Bare `host:port` URLs collide with the selector grammar — add a trailing slash before the selector: `https://example.com/:80`.\n\n# Internal URIs\n\n`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.\n\nBundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/…` identifiers.\n\n<critical>\n- Always include `path`; never call `read` with `{}`.\n- For line ranges, append the selector to `path`.\n- Re-issue the selector named by a summary footer before relying on elided content.\n</critical>",
|
|
31
31
|
"parameters": {
|
|
32
32
|
"type": "object",
|
|
33
33
|
"properties": {
|
|
@@ -407,7 +407,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
407
407
|
"debug": {
|
|
408
408
|
"name": "debug",
|
|
409
409
|
"label": "Debug",
|
|
410
|
-
"description": "Provides debugger access through the Debug Adapter Protocol (DAP).\nUse for launching or attaching debuggers, setting breakpoints, stepping through execution, inspecting threads/stack/variables, evaluating expressions, capturing output, and interrupting hung programs.\n\n<instruction>\n- Prefer over bash for program state, breakpoints, stepping, thread inspection, or interrupting a running process.\n- `action: \"launch\"` starts a session; `program` is required, `adapter` optional (auto-selected from target path and workspace).\n For Python, set `adapter: \"debugpy\"` and `program` to the target `.py` file; put interpreter/script flags in `args`.\n- `action: \"attach\"` connects to an existing process: `pid` for local attach, `port` for remote attach (where the adapter supports it), `adapter` to force a specific debugger.\n- **Breakpoints**: `set_breakpoint`/`remove_breakpoint` with source (`file`+`line`) or function (`function`); optional `condition` for conditional breakpoints.\n- **Flow control**: `continue` (resumes; briefly waits to observe whether the program stops or keeps running), `step_over`/`step_in`/`step_out` (single-step), `pause` (interrupt a running program so you can inspect state).\n- **Inspect**: `threads` (list), `stack_trace` (frames for current stopped thread), `scopes` (needs `frame_id` or a current stopped frame), `variables` (needs `variable_ref` or `scope_id`), `evaluate` (needs `expression`; `context: \"repl\"` for raw debugger commands when the adapter supports them), `output` (captured stdout/stderr/console), `sessions` (tracked debug sessions), `terminate`.\n- Timeouts apply per-request, not to the full session lifetime.\n</instruction>\n\n<caution>\n- Only one active debug session is supported at a time.\n- Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.\n- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -m debugpy.adapter`, `dlv dap`.\n- `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.\n</caution>\n\n<examples>\n# Launch and inspect hang\n1. `debug(action: \"launch\", program: \"./my_app\")`\n2. `debug(action: \"set_breakpoint\", file: \"src/main.c\", line: 42)`\n3. `debug(action: \"continue\")`\n4. If the program appears hung: `debug(action: \"pause\")`\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`\n# Launch a Python script with debugpy\n`debug(action: \"launch\", adapter: \"debugpy\", program: \"scripts/job.py\", args: [\"--flag\"])`\n# Raw debugger command through repl\n`debug(action: \"evaluate\", expression: \"info registers\", context: \"repl\")`\n</examples>",
|
|
410
|
+
"description": "Provides debugger access through the Debug Adapter Protocol (DAP).\nUse for launching or attaching debuggers, setting breakpoints, stepping through execution, inspecting threads/stack/variables, evaluating expressions, capturing output, and interrupting hung programs.\n\n<instruction>\n- Prefer over bash for program state, breakpoints, stepping, thread inspection, or interrupting a running process.\n- `action: \"launch\"` starts a session; `program` is required, `adapter` optional (auto-selected from target path and workspace).\n For Python, set `adapter: \"debugpy\"` and `program` to the target `.py` file; put interpreter/script flags in `args`.\n- `action: \"attach\"` connects to an existing process: `pid` for local attach, `port` for remote attach (where the adapter supports it), `adapter` to force a specific debugger.\n- **Breakpoints**: `set_breakpoint`/`remove_breakpoint` with source (`file`+`line`) or function (`function`); optional `condition` for conditional breakpoints.\n- **Flow control**: `continue` (resumes; briefly waits to observe whether the program stops or keeps running), `step_over`/`step_in`/`step_out` (single-step), `pause` (interrupt a running program so you can inspect state).\n- **Inspect**: `threads` (list), `stack_trace` (frames for current stopped thread), `scopes` (needs `frame_id` or a current stopped frame), `variables` (needs `variable_ref` or `scope_id`), `evaluate` (needs `expression`; `context: \"repl\"` for raw debugger commands when the adapter supports them), `output` (captured stdout/stderr/console), `sessions` (tracked debug sessions), `terminate`.\n- Timeouts apply per-request, not to the full session lifetime.\n</instruction>\n\n<caution>\n- Only one active debug session is supported at a time.\n- Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.\n- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -I -m debugpy.adapter`, `dlv dap`.\n- `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.\n</caution>\n\n<examples>\n# Launch and inspect hang\n1. `debug(action: \"launch\", program: \"./my_app\")`\n2. `debug(action: \"set_breakpoint\", file: \"src/main.c\", line: 42)`\n3. `debug(action: \"continue\")`\n4. If the program appears hung: `debug(action: \"pause\")`\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`\n# Launch a Python script with debugpy\n`debug(action: \"launch\", adapter: \"debugpy\", program: \"scripts/job.py\", args: [\"--flag\"])`\n# Raw debugger command through repl\n`debug(action: \"evaluate\", expression: \"info registers\", context: \"repl\")`\n</examples>",
|
|
411
411
|
"parameters": {
|
|
412
412
|
"type": "object",
|
|
413
413
|
"properties": {
|
|
@@ -2096,7 +2096,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2096
2096
|
"task": {
|
|
2097
2097
|
"name": "task",
|
|
2098
2098
|
"label": "Task",
|
|
2099
|
-
"description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\nUse `irc` for live coordination when it is available.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n\n- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the
|
|
2099
|
+
"description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\nUse `irc` for live coordination when it is available.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n\n- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the session's committed cwd and cannot itself authorize relocation. After an authorized session move, new work uses the committed target; already-admitted work retains its source execution inputs. Verified storage relocation of that same parent does not change child execution cwd; a logical session switch does not substitute successor storage. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`) or use the existing user-authorized relocation surface; `move_session` still only narrows to a descendant directory.\n</parameters>\n\n<rules>\n- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.\n- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.\n- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.\n- Each task: ≤3–5 explicit files. No globs, no \"update all\", no package-wide scope. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.\n- Pass large payloads via `local://<path>` URIs, not inline.\n- Put shared constraints in `context` once; do not duplicate across assignments.\n- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.\n</rules>\n\n<parallelization>\nTest: can task B run correctly without seeing A's output? If no, sequence A → B, unless `irc` is available and A can answer a small clarification.\nStill sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.\nParallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.\n</parallelization>\n\n<context-fmt>\n# Goal ← one sentence: what the batch accomplishes\n# Constraints ← MUST/NEVER rules and session decisions\n# Contract ← exact types/signatures if tasks share an interface\n</context-fmt>\n\n<assignment-fmt>\n# Target ← exact files and symbols; explicit non-goals\n# Change ← step-by-step add/remove/rename; APIs and patterns\n# Acceptance ← observable result; no project-wide commands\n</assignment-fmt>\n\n<agents>\nBundled role names: executor, architect, planner, critic. A configured agent may override a bundled role name and takes precedence.\nOther configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.\n</agents>",
|
|
2100
2100
|
"parameters": {
|
|
2101
2101
|
"type": "object",
|
|
2102
2102
|
"properties": {
|
|
@@ -2955,6 +2955,34 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2955
2955
|
"concurrency": "exclusive",
|
|
2956
2956
|
"intent": "omit"
|
|
2957
2957
|
},
|
|
2958
|
+
"report_issue": {
|
|
2959
|
+
"name": "report_issue",
|
|
2960
|
+
"label": "Report Issue",
|
|
2961
|
+
"description": "Records a tool that behaved inconsistently with its own description, for local quality tracking.\n\n<instruction>\n- Call this when a tool's output contradicts what its description or parameters promise: ignored or misapplied parameters, wrong or truncated results, misleading errors, silent no-ops.\n- `tool` is the misbehaving tool's name; `report` states what you called, what you expected, and what actually happened, in one or two sentences.\n- Do not report your own mistakes, user-caused failures, or expected errors (missing files, failing tests, network outages).\n- Do not paste secrets, tokens, credentials, or raw tool output into report; summarize it instead.\n- False positives are acceptable. Report once per distinct problem, then continue the task; this never fixes or retries anything.\n</instruction>\n\n<output>\nConfirms whether the report was recorded. Reports stay on this machine.\n</output>",
|
|
2962
|
+
"parameters": {
|
|
2963
|
+
"type": "object",
|
|
2964
|
+
"properties": {
|
|
2965
|
+
"tool": {
|
|
2966
|
+
"type": "string",
|
|
2967
|
+
"description": "name of the tool that misbehaved"
|
|
2968
|
+
},
|
|
2969
|
+
"report": {
|
|
2970
|
+
"type": "string",
|
|
2971
|
+
"description": "what was called, what was expected, what happened"
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
"required": [
|
|
2975
|
+
"tool",
|
|
2976
|
+
"report"
|
|
2977
|
+
],
|
|
2978
|
+
"additionalProperties": false
|
|
2979
|
+
},
|
|
2980
|
+
"strict": true,
|
|
2981
|
+
"deferrable": false,
|
|
2982
|
+
"loadMode": "essential",
|
|
2983
|
+
"summary": "Record a tool that behaved inconsistently with its description",
|
|
2984
|
+
"intent": "omit"
|
|
2985
|
+
},
|
|
2958
2986
|
"yield": {
|
|
2959
2987
|
"name": "yield",
|
|
2960
2988
|
"label": "Submit Result",
|
package/src/tools/write.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
4
4
|
import type { Component } from "@gajae-code/tui";
|
|
5
5
|
import { Text } from "@gajae-code/tui";
|
|
6
|
-
import { isEnoent, isRecord, prompt, untilAborted } from "@gajae-code/utils";
|
|
6
|
+
import { isEnoent, isRecord, pathIsWithin, prompt, untilAborted } from "@gajae-code/utils";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
import { stripHashlinePrefixes } from "../edit";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -177,6 +177,31 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
|
|
|
177
177
|
return { table, key };
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* `new Database` follows symlinks. A lexical path inside the workspace can
|
|
182
|
+
* still name a file outside it, so open only the real path that stays inside
|
|
183
|
+
* the same session workspace an ordinary write resolves against.
|
|
184
|
+
*/
|
|
185
|
+
async function realSqliteDatabaseInWorkspace(
|
|
186
|
+
workspaceRoot: string,
|
|
187
|
+
absolutePath: string,
|
|
188
|
+
signal?: AbortSignal,
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
let realPath: string;
|
|
192
|
+
try {
|
|
193
|
+
realPath = await fs.realpath(absolutePath);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (isEnoent(error)) throw error;
|
|
196
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
197
|
+
}
|
|
198
|
+
signal?.throwIfAborted();
|
|
199
|
+
if (!pathIsWithin(workspaceRoot, realPath)) {
|
|
200
|
+
throw new ToolError(`SQLite write target resolves outside the workspace: ${absolutePath}`);
|
|
201
|
+
}
|
|
202
|
+
return realPath;
|
|
203
|
+
}
|
|
204
|
+
|
|
180
205
|
/**
|
|
181
206
|
* Write tool implementation.
|
|
182
207
|
*
|
|
@@ -372,6 +397,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
372
397
|
displayPath: string,
|
|
373
398
|
content: string,
|
|
374
399
|
resolvedSqlitePath: ResolvedSqliteWritePath,
|
|
400
|
+
signal?: AbortSignal,
|
|
375
401
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
376
402
|
let db: Database | null = null;
|
|
377
403
|
try {
|
|
@@ -379,7 +405,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
379
405
|
throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
380
406
|
}
|
|
381
407
|
|
|
382
|
-
|
|
408
|
+
const realDatabasePath = await realSqliteDatabaseInWorkspace(
|
|
409
|
+
this.session.cwd,
|
|
410
|
+
resolvedSqlitePath.absolutePath,
|
|
411
|
+
signal,
|
|
412
|
+
);
|
|
413
|
+
signal?.throwIfAborted();
|
|
414
|
+
db = new Database(realDatabasePath, { create: false, strict: true });
|
|
383
415
|
db.run("PRAGMA busy_timeout = 3000");
|
|
384
416
|
|
|
385
417
|
const trimmedContent = content.trim();
|
|
@@ -718,7 +750,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
718
750
|
if (resolvedSqlitePath) {
|
|
719
751
|
enforcePlanModeWrite(this.session, resolvedSqlitePath.sqlitePath, { op: "update" });
|
|
720
752
|
|
|
721
|
-
const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath);
|
|
753
|
+
const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath, signal);
|
|
722
754
|
if (stripped) {
|
|
723
755
|
const firstText = sqliteResult.content.find(
|
|
724
756
|
(block): block is { type: "text"; text: string } =>
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
6
6
|
import type { Api, Model } from "@gajae-code/ai/core";
|
|
7
7
|
import { completeSimple } from "@gajae-code/ai/core";
|
|
8
|
-
import { logger, prompt } from "@gajae-code/utils";
|
|
8
|
+
import { logger, prompt, redactCrashSecrets } from "@gajae-code/utils";
|
|
9
|
+
import { redactChangelogEntry } from "../commit/changelog";
|
|
9
10
|
import type { ModelRegistry } from "../config/model-registry";
|
|
10
11
|
import { resolveModelRoleValue } from "../config/model-resolver";
|
|
11
12
|
import type { Settings } from "../config/settings";
|
|
@@ -35,6 +36,11 @@ function filterDiffNoise(diff: string): string {
|
|
|
35
36
|
return filtered.join("\n");
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** Keep changelog patterns, then scrub known credential shapes on the whole string. */
|
|
40
|
+
function redactCommitMessageText(text: string): string {
|
|
41
|
+
return redactCrashSecrets(redactChangelogEntry(text));
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
function getModelCandidates(
|
|
39
45
|
registry: ModelRegistry,
|
|
40
46
|
settings: Settings,
|
|
@@ -87,7 +93,7 @@ export async function generateCommitMessage(
|
|
|
87
93
|
logger.debug("commit-msg-generator: diff is empty after noise filtering");
|
|
88
94
|
return null;
|
|
89
95
|
}
|
|
90
|
-
const userMessage = `<diff>\n${truncatedDiff}\n</diff>`;
|
|
96
|
+
const userMessage = `<diff>\n${redactCommitMessageText(truncatedDiff)}\n</diff>`;
|
|
91
97
|
|
|
92
98
|
for (const candidate of candidates) {
|
|
93
99
|
const apiKey = await registry.getApiKey(candidate.model, sessionId);
|
|
@@ -120,6 +126,7 @@ export async function generateCommitMessage(
|
|
|
120
126
|
|
|
121
127
|
// Clean up: remove wrapping quotes, backticks, trailing period
|
|
122
128
|
msg = msg.replace(/^[`"']|[`"']$/g, "").replace(/\.$/, "");
|
|
129
|
+
msg = redactCommitMessageText(msg);
|
|
123
130
|
|
|
124
131
|
logger.debug("commit-msg-generator: generated", { model: candidate.model.id, msg });
|
|
125
132
|
return msg;
|
|
@@ -80,6 +80,7 @@ type MentionCandidate = {
|
|
|
80
80
|
path: string;
|
|
81
81
|
pathLower: string;
|
|
82
82
|
normalizedPath: string;
|
|
83
|
+
segments: string[];
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
function normalizeMentionQuery(query: string): string {
|
|
@@ -117,11 +118,37 @@ async function listMentionCandidates(cwd: string): Promise<MentionCandidate[]> {
|
|
|
117
118
|
if (normalizedPath.length === 0) {
|
|
118
119
|
continue;
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
+
const segments = pathLower.split(PATH_SEPARATOR_REGEX).filter(segment => segment.length > 0);
|
|
122
|
+
candidates.push({ path: entry, pathLower, normalizedPath, segments });
|
|
121
123
|
}
|
|
122
124
|
return candidates;
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
/**
|
|
128
|
+
* True when `query` can be spelled as prefixes of path segments taken in order
|
|
129
|
+
* (segments may be skipped), e.g. `httpserap` → `http_server_api_tests`.
|
|
130
|
+
* Plain subsequence matching would also accept letters scattered mid-word across
|
|
131
|
+
* any long path, so a stray `@handle` or `@word` would resolve to an unrelated file.
|
|
132
|
+
*/
|
|
133
|
+
function matchesSegmentPrefixes(query: string, segments: string[]): boolean {
|
|
134
|
+
const failed = new Set<number>();
|
|
135
|
+
const visit = (segmentIndex: number, queryIndex: number): boolean => {
|
|
136
|
+
if (queryIndex === query.length) return true;
|
|
137
|
+
if (segmentIndex === segments.length) return false;
|
|
138
|
+
const key = segmentIndex * (query.length + 1) + queryIndex;
|
|
139
|
+
if (failed.has(key)) return false;
|
|
140
|
+
const segment = segments[segmentIndex]!;
|
|
141
|
+
for (let taken = 1; taken <= segment.length && queryIndex + taken <= query.length; taken++) {
|
|
142
|
+
if (segment[taken - 1] !== query[queryIndex + taken - 1]) break;
|
|
143
|
+
if (visit(segmentIndex + 1, queryIndex + taken)) return true;
|
|
144
|
+
}
|
|
145
|
+
if (visit(segmentIndex + 1, queryIndex)) return true;
|
|
146
|
+
failed.add(key);
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
return visit(0, 0);
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
async function resolveMentionPath(
|
|
126
153
|
filePath: string,
|
|
127
154
|
cwd: string,
|
|
@@ -148,6 +175,7 @@ async function resolveMentionPath(
|
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
const scored = candidates
|
|
178
|
+
.filter(candidate => matchesSegmentPrefixes(normalizedQuery, candidate.segments))
|
|
151
179
|
.map(candidate => ({ candidate, match: fuzzyMatch(normalizedQuery, candidate.normalizedPath) }))
|
|
152
180
|
.filter(entry => entry.match.matches)
|
|
153
181
|
.sort((a, b) => {
|
|
@@ -5,7 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
7
7
|
import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "@gajae-code/ai/core";
|
|
8
|
-
import { logger, prompt } from "@gajae-code/utils";
|
|
8
|
+
import { logger, prompt, redactCrashSecrets } from "@gajae-code/utils";
|
|
9
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
10
10
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
11
11
|
import type { Settings } from "../config/settings";
|
|
@@ -159,7 +159,12 @@ ${truncatedMessage}
|
|
|
159
159
|
userMessage,
|
|
160
160
|
maxTokens,
|
|
161
161
|
};
|
|
162
|
-
logger.debug("title-generator: request",
|
|
162
|
+
logger.debug("title-generator: request", {
|
|
163
|
+
model: request.model,
|
|
164
|
+
systemPrompt: request.systemPrompt,
|
|
165
|
+
userMessage: redactCrashSecrets(request.userMessage),
|
|
166
|
+
maxTokens: request.maxTokens,
|
|
167
|
+
});
|
|
163
168
|
|
|
164
169
|
try {
|
|
165
170
|
const response = await completeSimple(
|
|
@@ -169,7 +169,9 @@ specialHandlers.push(
|
|
|
169
169
|
handleGitHub,
|
|
170
170
|
handleGitLab,
|
|
171
171
|
// Video/Media
|
|
172
|
-
(
|
|
172
|
+
// Registered directly (not through an argument-dropping adapter) so the
|
|
173
|
+
// handler receives the caller's storage and settings.
|
|
174
|
+
handleYouTube,
|
|
173
175
|
handleVimeo,
|
|
174
176
|
handleSpotify,
|
|
175
177
|
handleDiscogs,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Shared types and utilities for web-fetch handlers
|
|
3
3
|
*/
|
|
4
4
|
import { ptree } from "@gajae-code/utils";
|
|
5
|
+
import type { Settings } from "../../config/settings";
|
|
5
6
|
import type { AgentStorage } from "../../session/agent-storage";
|
|
6
7
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
7
8
|
import type { AddressResolver } from "../insane/url-guard";
|
|
@@ -20,11 +21,18 @@ export interface RenderResult {
|
|
|
20
21
|
notes: string[];
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `settings` is the caller's session settings. Handlers must read through it
|
|
26
|
+
* rather than the process-global singleton: the SDK/ACP host path runs tools
|
|
27
|
+
* without ever calling `Settings.init()`, where a global read throws
|
|
28
|
+
* "Settings not initialized" instead of scraping the page.
|
|
29
|
+
*/
|
|
23
30
|
export type SpecialHandler = (
|
|
24
31
|
url: string,
|
|
25
32
|
timeout: number,
|
|
26
33
|
signal?: AbortSignal,
|
|
27
34
|
storage?: AgentStorage | null,
|
|
35
|
+
settings?: Settings,
|
|
28
36
|
) => Promise<RenderResult | null>;
|
|
29
37
|
|
|
30
38
|
export const MAX_OUTPUT_CHARS = 500_000;
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { ptree, Snowflake } from "@gajae-code/utils";
|
|
5
|
-
import {
|
|
5
|
+
import type { Settings } from "../../config/settings";
|
|
6
6
|
import type { AgentStorage } from "../../session/agent-storage";
|
|
7
7
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
8
8
|
import { ensureTool } from "../../utils/tools-manager";
|
|
@@ -115,6 +115,7 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
115
115
|
timeout: number,
|
|
116
116
|
userSignal?: AbortSignal,
|
|
117
117
|
storage?: AgentStorage | null,
|
|
118
|
+
settings?: Settings,
|
|
118
119
|
): Promise<RenderResult | null> => {
|
|
119
120
|
throwIfAborted(userSignal);
|
|
120
121
|
const yt = parseYouTubeUrl(url);
|
|
@@ -126,7 +127,7 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
126
127
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
127
128
|
|
|
128
129
|
// Prefer Parallel extract when credentials are available
|
|
129
|
-
if (settings
|
|
130
|
+
if (settings?.get("providers.parallelFetch") === true && findParallelApiKey(storage)) {
|
|
130
131
|
try {
|
|
131
132
|
const parallelResult = await extractWithParallel(
|
|
132
133
|
[videoUrl],
|