@gajae-code/coding-agent 0.3.0 → 0.3.1
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 +18 -0
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +4 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +9 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +29 -0
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +29 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +257 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -172,6 +172,7 @@ import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime"
|
|
|
172
172
|
import { GoalRuntime } from "../goals/runtime";
|
|
173
173
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
174
174
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
175
|
+
import { ensureWorkflowSkillActivationState } from "../hooks/skill-state";
|
|
175
176
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
176
177
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
177
178
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
@@ -297,7 +298,7 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
|
297
298
|
|
|
298
299
|
/** Listener function for agent session events */
|
|
299
300
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
300
|
-
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
301
|
+
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime" | "metadata">;
|
|
301
302
|
|
|
302
303
|
export interface AsyncJobSnapshot {
|
|
303
304
|
running: AsyncJobSnapshotItem[];
|
|
@@ -872,6 +873,7 @@ export class AgentSession {
|
|
|
872
873
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
873
874
|
// Todo completion reminder state
|
|
874
875
|
#todoReminderCount = 0;
|
|
876
|
+
#lastGoalReminderAssistantTimestamp: number | undefined = undefined;
|
|
875
877
|
#todoPhases: TodoPhase[] = [];
|
|
876
878
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
877
879
|
|
|
@@ -972,6 +974,12 @@ export class AgentSession {
|
|
|
972
974
|
* without producing an aborted message_end). */
|
|
973
975
|
#planCompactAbortPending = false;
|
|
974
976
|
|
|
977
|
+
/** One-shot flag armed by `abort({ silent: true })` (e.g. Esc consuming a
|
|
978
|
+
* queued steer). Consumed in #handleAgentEvent to stamp `SILENT_ABORT_MARKER`
|
|
979
|
+
* on the resulting aborted assistant `message_end` so the interrupt does not
|
|
980
|
+
* surface a red "Operation aborted" line; cleared by a later non-silent abort
|
|
981
|
+
* or by `abort`'s safety net when no aborted message_end is produced. */
|
|
982
|
+
#silentAbortPending = false;
|
|
975
983
|
/** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
|
|
976
984
|
* combined with `Date.now()` so tags stay unique even across rapid
|
|
977
985
|
* same-tick enqueues. */
|
|
@@ -1050,6 +1058,7 @@ export class AgentSession {
|
|
|
1050
1058
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
1051
1059
|
if (this.#promptInFlightCount === 0) {
|
|
1052
1060
|
this.#releasePowerAssertion();
|
|
1061
|
+
this.#flushPendingBackgroundExchanges();
|
|
1053
1062
|
this.#flushPendingAgentEnd();
|
|
1054
1063
|
}
|
|
1055
1064
|
}
|
|
@@ -1057,6 +1066,7 @@ export class AgentSession {
|
|
|
1057
1066
|
#resetInFlight(): void {
|
|
1058
1067
|
this.#promptInFlightCount = 0;
|
|
1059
1068
|
this.#releasePowerAssertion();
|
|
1069
|
+
this.#flushPendingBackgroundExchanges();
|
|
1060
1070
|
this.#flushPendingAgentEnd();
|
|
1061
1071
|
}
|
|
1062
1072
|
|
|
@@ -1485,6 +1495,10 @@ export class AgentSession {
|
|
|
1485
1495
|
return tag;
|
|
1486
1496
|
}
|
|
1487
1497
|
|
|
1498
|
+
getAgentId(): string | undefined {
|
|
1499
|
+
return this.#agentId;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1488
1502
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
1489
1503
|
const manager = AsyncJobManager.instance();
|
|
1490
1504
|
if (!manager) return null;
|
|
@@ -1495,6 +1509,7 @@ export class AgentSession {
|
|
|
1495
1509
|
status: job.status,
|
|
1496
1510
|
label: job.label,
|
|
1497
1511
|
startTime: job.startTime,
|
|
1512
|
+
metadata: job.metadata,
|
|
1498
1513
|
}));
|
|
1499
1514
|
const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
|
|
1500
1515
|
id: job.id,
|
|
@@ -1502,6 +1517,7 @@ export class AgentSession {
|
|
|
1502
1517
|
status: job.status,
|
|
1503
1518
|
label: job.label,
|
|
1504
1519
|
startTime: job.startTime,
|
|
1520
|
+
metadata: job.metadata,
|
|
1505
1521
|
}));
|
|
1506
1522
|
const delivery = manager.getDeliveryState(ownerFilter);
|
|
1507
1523
|
return { running, recent, delivery };
|
|
@@ -1644,10 +1660,11 @@ export class AgentSession {
|
|
|
1644
1660
|
event.type === "message_end" &&
|
|
1645
1661
|
event.message.role === "assistant" &&
|
|
1646
1662
|
event.message.stopReason === "aborted" &&
|
|
1647
|
-
this.#planCompactAbortPending
|
|
1663
|
+
(this.#planCompactAbortPending || this.#silentAbortPending)
|
|
1648
1664
|
) {
|
|
1649
1665
|
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
1650
1666
|
this.#planCompactAbortPending = false;
|
|
1667
|
+
this.#silentAbortPending = false;
|
|
1651
1668
|
}
|
|
1652
1669
|
|
|
1653
1670
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
@@ -2015,6 +2032,9 @@ export class AgentSession {
|
|
|
2015
2032
|
|
|
2016
2033
|
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
2017
2034
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2035
|
+
if (msg.stopReason !== "error" && msg.stopReason !== "aborted" && (await this.#checkGoalCompletion(msg))) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2018
2038
|
return;
|
|
2019
2039
|
}
|
|
2020
2040
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
@@ -2050,6 +2070,9 @@ export class AgentSession {
|
|
|
2050
2070
|
if (this.#enforceRewindBeforeYield()) {
|
|
2051
2071
|
return;
|
|
2052
2072
|
}
|
|
2073
|
+
if (await this.#checkGoalCompletion(msg)) {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2053
2076
|
await this.#checkTodoCompletion();
|
|
2054
2077
|
}
|
|
2055
2078
|
}
|
|
@@ -2138,13 +2161,23 @@ export class AgentSession {
|
|
|
2138
2161
|
delayMs?: number;
|
|
2139
2162
|
generation?: number;
|
|
2140
2163
|
shouldContinue?: () => boolean;
|
|
2141
|
-
onSkip?: () => void;
|
|
2142
|
-
onError?: () => void;
|
|
2164
|
+
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
|
|
2165
|
+
onError?: (error: unknown) => void;
|
|
2143
2166
|
}): void {
|
|
2167
|
+
const scheduledGeneration = options?.generation;
|
|
2168
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
2144
2169
|
this.#schedulePostPromptTask(
|
|
2145
2170
|
async () => {
|
|
2171
|
+
if (signal.aborted) {
|
|
2172
|
+
options?.onSkip?.("aborted_signal");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
2176
|
+
options?.onSkip?.("generation_changed");
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2146
2179
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
2147
|
-
options.onSkip?.();
|
|
2180
|
+
options.onSkip?.("queue_drained");
|
|
2148
2181
|
return;
|
|
2149
2182
|
}
|
|
2150
2183
|
try {
|
|
@@ -2154,17 +2187,45 @@ export class AgentSession {
|
|
|
2154
2187
|
logger.warn("agent.continue failed after scheduling", {
|
|
2155
2188
|
error: error instanceof Error ? error.message : String(error),
|
|
2156
2189
|
});
|
|
2157
|
-
options?.onError?.();
|
|
2190
|
+
options?.onError?.(error);
|
|
2158
2191
|
}
|
|
2159
2192
|
},
|
|
2160
|
-
{
|
|
2161
|
-
delayMs: options?.delayMs,
|
|
2162
|
-
generation: options?.generation,
|
|
2163
|
-
onSkip: options?.onSkip,
|
|
2164
|
-
},
|
|
2193
|
+
{ delayMs: options?.delayMs },
|
|
2165
2194
|
);
|
|
2166
2195
|
}
|
|
2167
2196
|
|
|
2197
|
+
#logCompactionContinuationSkipped(
|
|
2198
|
+
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
2199
|
+
reason: string,
|
|
2200
|
+
): void {
|
|
2201
|
+
logger.warn("Auto-compaction continuation skipped", { source, reason });
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
#logCompactionContinuationError(
|
|
2205
|
+
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
2206
|
+
error: unknown,
|
|
2207
|
+
): void {
|
|
2208
|
+
logger.warn("Auto-compaction continuation failed", {
|
|
2209
|
+
source,
|
|
2210
|
+
reason: error instanceof Error && error.name === "AgentBusyError" ? "queue_drained" : "not_resumable_tail",
|
|
2211
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
#isResumableAgentTail(): boolean {
|
|
2216
|
+
const lastMsg = this.agent.state.messages.at(-1);
|
|
2217
|
+
return lastMsg !== undefined && lastMsg.role !== "assistant";
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
#stripOverflowFailedTurnForRetry(): void {
|
|
2221
|
+
const messages = this.agent.state.messages;
|
|
2222
|
+
const lastMsg = messages.at(-1);
|
|
2223
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2224
|
+
if (lastMsg?.role === "assistant" && isContextOverflow(lastMsg as AssistantMessage, contextWindow)) {
|
|
2225
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2168
2229
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2169
2230
|
const continuePrompt = async () => {
|
|
2170
2231
|
await this.#promptWithMessage(
|
|
@@ -2175,16 +2236,28 @@ export class AgentSession {
|
|
|
2175
2236
|
timestamp: Date.now(),
|
|
2176
2237
|
},
|
|
2177
2238
|
autoContinuePrompt,
|
|
2178
|
-
{ skipPostPromptRecoveryWait: true },
|
|
2239
|
+
{ skipPostPromptRecoveryWait: true, skipCompactionCheck: true },
|
|
2179
2240
|
);
|
|
2180
2241
|
};
|
|
2181
|
-
|
|
2182
|
-
|
|
2242
|
+
const scheduledGeneration = generation;
|
|
2243
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
2244
|
+
this.#trackPostPromptTask(
|
|
2245
|
+
(async () => {
|
|
2183
2246
|
await Promise.resolve();
|
|
2184
|
-
if (signal.aborted)
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2247
|
+
if (signal.aborted) {
|
|
2248
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
2252
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
try {
|
|
2256
|
+
await continuePrompt();
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
2259
|
+
}
|
|
2260
|
+
})(),
|
|
2188
2261
|
);
|
|
2189
2262
|
}
|
|
2190
2263
|
|
|
@@ -4327,12 +4400,17 @@ export class AgentSession {
|
|
|
4327
4400
|
// Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
|
|
4328
4401
|
// own their `.gjc/state/skill-active-state.json` row through the
|
|
4329
4402
|
// `gjc state handoff` and `gjc state clear` runtime verbs. The prompt
|
|
4330
|
-
// observer
|
|
4331
|
-
//
|
|
4332
|
-
//
|
|
4333
|
-
//
|
|
4334
|
-
//
|
|
4335
|
-
//
|
|
4403
|
+
// observer must not overwrite an existing row (that clobbered handoff
|
|
4404
|
+
// lineage `handoff_from`/`handoff_at` and desynced the HUD). But a fresh
|
|
4405
|
+
// `/skill:<name>` invocation has no row yet, so seed `.gjc/state`
|
|
4406
|
+
// idempotently here: `ensureWorkflowSkillActivationState` writes the
|
|
4407
|
+
// initial mode-state + active row only when the skill is not already
|
|
4408
|
+
// active, so the mutation guard and Stop hook engage immediately instead
|
|
4409
|
+
// of relying on the skill prompt to run its own state-init steps.
|
|
4410
|
+
if (active) {
|
|
4411
|
+
await ensureWorkflowSkillActivationState({ cwd: this.sessionManager.getCwd(), skill, sessionId });
|
|
4412
|
+
}
|
|
4413
|
+
// In-memory tracking keeps `getActiveSkillState` accurate for the chain guard.
|
|
4336
4414
|
this.#activeSkillState = active ? { skill, sessionId } : undefined;
|
|
4337
4415
|
}
|
|
4338
4416
|
|
|
@@ -4978,6 +5056,13 @@ export class AgentSession {
|
|
|
4978
5056
|
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
4979
5057
|
}
|
|
4980
5058
|
|
|
5059
|
+
/** Whether the agent has queued steering messages that a `user_interrupt`
|
|
5060
|
+
* abort would resume into (steer-on-interrupt). Drives the Esc-on-steer UX:
|
|
5061
|
+
* the first Esc consumes the steer and auto-continues, a second Esc aborts. */
|
|
5062
|
+
get hasQueuedSteering(): boolean {
|
|
5063
|
+
return this.agent.hasQueuedSteering();
|
|
5064
|
+
}
|
|
5065
|
+
|
|
4981
5066
|
/** Get pending messages (read-only). Returns the public text-only view;
|
|
4982
5067
|
* internal `{text, tag?}` records are mapped to `.text` so callers
|
|
4983
5068
|
* (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
|
|
@@ -5074,7 +5159,17 @@ export class AgentSession {
|
|
|
5074
5159
|
| "handoff"
|
|
5075
5160
|
| "tool_abort"
|
|
5076
5161
|
| "internal";
|
|
5162
|
+
/** Suppress the "Operation aborted" line on the resulting aborted message
|
|
5163
|
+
* by stamping `SILENT_ABORT_MARKER`. Used when Esc consumes a queued steer
|
|
5164
|
+
* and resumes via steer-on-interrupt, so the interrupt reads as a quiet
|
|
5165
|
+
* hand-off rather than a failure. */
|
|
5166
|
+
silent?: boolean;
|
|
5077
5167
|
}): Promise<void> {
|
|
5168
|
+
if (options?.silent) {
|
|
5169
|
+
this.#silentAbortPending = true;
|
|
5170
|
+
} else {
|
|
5171
|
+
this.#silentAbortPending = false;
|
|
5172
|
+
}
|
|
5078
5173
|
this.abortRetry();
|
|
5079
5174
|
this.#promptGeneration++;
|
|
5080
5175
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -5114,6 +5209,10 @@ export class AgentSession {
|
|
|
5114
5209
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
5115
5210
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
5116
5211
|
this.#resetInFlight();
|
|
5212
|
+
// Safety net: clear the silent-abort flag if it was never consumed (the
|
|
5213
|
+
// abort produced no aborted assistant message_end to stamp). Prevents the
|
|
5214
|
+
// marker from leaking onto a later, unrelated abort.
|
|
5215
|
+
this.#silentAbortPending = false;
|
|
5117
5216
|
// Safety net: if the agent loop aborted without producing an assistant
|
|
5118
5217
|
// message (e.g. failed before the first stream), the in-flight yield was
|
|
5119
5218
|
// never resolved or rejected by the normal message_end path. Reject it now
|
|
@@ -5188,6 +5287,9 @@ export class AgentSession {
|
|
|
5188
5287
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5189
5288
|
|
|
5190
5289
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
5290
|
+
if (this.model) {
|
|
5291
|
+
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
5292
|
+
}
|
|
5191
5293
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
5192
5294
|
if (nextDiscoverySessionToolNames) {
|
|
5193
5295
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
@@ -5979,6 +6081,11 @@ export class AgentSession {
|
|
|
5979
6081
|
this.#pendingNextTurnMessages = [];
|
|
5980
6082
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5981
6083
|
this.#todoReminderCount = 0;
|
|
6084
|
+
if (model) {
|
|
6085
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
|
|
6086
|
+
}
|
|
6087
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6088
|
+
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
5982
6089
|
|
|
5983
6090
|
// Inject the handoff document as a custom message
|
|
5984
6091
|
const handoffContent = createHandoffContext(handoffText);
|
|
@@ -6266,6 +6373,39 @@ export class AgentSession {
|
|
|
6266
6373
|
toolChoice: todoWriteToolChoice,
|
|
6267
6374
|
};
|
|
6268
6375
|
}
|
|
6376
|
+
|
|
6377
|
+
async #checkGoalCompletion(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
6378
|
+
const state = this.getGoalModeState();
|
|
6379
|
+
if (!state?.enabled || state.goal.status !== "active") {
|
|
6380
|
+
this.#lastGoalReminderAssistantTimestamp = undefined;
|
|
6381
|
+
return false;
|
|
6382
|
+
}
|
|
6383
|
+
if (this.#lastGoalReminderAssistantTimestamp === assistantMessage.timestamp) {
|
|
6384
|
+
return false;
|
|
6385
|
+
}
|
|
6386
|
+
this.#lastGoalReminderAssistantTimestamp = assistantMessage.timestamp;
|
|
6387
|
+
|
|
6388
|
+
const continuationPrompt = this.#goalRuntime.buildContinuationPrompt();
|
|
6389
|
+
if (!continuationPrompt) return false;
|
|
6390
|
+
const reminder = [
|
|
6391
|
+
"<system-reminder>",
|
|
6392
|
+
"You stopped while a goal is still active and uncleared.",
|
|
6393
|
+
"Continue working on the active goal until it is verified complete, paused, or dropped.",
|
|
6394
|
+
"",
|
|
6395
|
+
continuationPrompt,
|
|
6396
|
+
"</system-reminder>",
|
|
6397
|
+
].join("\n");
|
|
6398
|
+
|
|
6399
|
+
logger.debug("Goal completion: sending active-goal reminder", { goalId: state.goal.id });
|
|
6400
|
+
this.agent.appendMessage({
|
|
6401
|
+
role: "developer",
|
|
6402
|
+
content: [{ type: "text", text: reminder }],
|
|
6403
|
+
attribution: "agent",
|
|
6404
|
+
timestamp: Date.now(),
|
|
6405
|
+
});
|
|
6406
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
6407
|
+
return true;
|
|
6408
|
+
}
|
|
6269
6409
|
/**
|
|
6270
6410
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
6271
6411
|
*/
|
|
@@ -6909,12 +7049,34 @@ export class AgentSession {
|
|
|
6909
7049
|
willRetry: false,
|
|
6910
7050
|
skipped: true,
|
|
6911
7051
|
});
|
|
6912
|
-
if (
|
|
7052
|
+
if (willRetry) {
|
|
7053
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
7054
|
+
if (this.#isResumableAgentTail()) {
|
|
7055
|
+
this.#scheduleAgentContinue({
|
|
7056
|
+
delayMs: 100,
|
|
7057
|
+
generation,
|
|
7058
|
+
onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
|
|
7059
|
+
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7060
|
+
});
|
|
7061
|
+
} else {
|
|
7062
|
+
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7063
|
+
logger.warn("Auto-compaction continuation skipped", {
|
|
7064
|
+
source: "overflow_retry",
|
|
7065
|
+
reason: "not_resumable_tail",
|
|
7066
|
+
role: tail?.role,
|
|
7067
|
+
stopReason: tail?.stopReason,
|
|
7068
|
+
});
|
|
7069
|
+
}
|
|
7070
|
+
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
6913
7071
|
this.#scheduleAgentContinue({
|
|
6914
7072
|
delayMs: 100,
|
|
6915
7073
|
generation,
|
|
6916
7074
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
7075
|
+
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
7076
|
+
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
6917
7077
|
});
|
|
7078
|
+
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7079
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
6918
7080
|
}
|
|
6919
7081
|
return;
|
|
6920
7082
|
}
|
|
@@ -7116,26 +7278,36 @@ export class AgentSession {
|
|
|
7116
7278
|
};
|
|
7117
7279
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
7118
7280
|
|
|
7119
|
-
if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7120
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
7121
|
-
}
|
|
7122
|
-
|
|
7123
7281
|
if (willRetry) {
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7282
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
7283
|
+
if (!this.#isResumableAgentTail()) {
|
|
7284
|
+
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7285
|
+
logger.warn("Auto-compaction continuation skipped", {
|
|
7286
|
+
source: "overflow_retry",
|
|
7287
|
+
reason: "not_resumable_tail",
|
|
7288
|
+
role: tail?.role,
|
|
7289
|
+
stopReason: tail?.stopReason,
|
|
7290
|
+
});
|
|
7291
|
+
} else {
|
|
7292
|
+
this.#scheduleAgentContinue({
|
|
7293
|
+
delayMs: 100,
|
|
7294
|
+
generation,
|
|
7295
|
+
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
7296
|
+
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7297
|
+
});
|
|
7128
7298
|
}
|
|
7129
|
-
|
|
7130
|
-
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
7131
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
7299
|
+
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7132
7300
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
7133
7301
|
// Kick the loop so queued messages are actually delivered.
|
|
7134
7302
|
this.#scheduleAgentContinue({
|
|
7135
7303
|
delayMs: 100,
|
|
7136
7304
|
generation,
|
|
7137
7305
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
7306
|
+
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
7307
|
+
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7138
7308
|
});
|
|
7309
|
+
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7310
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
7139
7311
|
}
|
|
7140
7312
|
} catch (error) {
|
|
7141
7313
|
if (autoCompactionSignal.aborted) {
|
|
@@ -8377,13 +8549,17 @@ export class AgentSession {
|
|
|
8377
8549
|
return;
|
|
8378
8550
|
}
|
|
8379
8551
|
if (this.isStreaming) {
|
|
8380
|
-
|
|
8552
|
+
// Re-poll while streaming, but do not let this housekeeping timer
|
|
8553
|
+
// keep the event loop alive on its own (CPU-7).
|
|
8554
|
+
const pollTimer = setTimeout(attempt, 50);
|
|
8555
|
+
pollTimer.unref?.();
|
|
8381
8556
|
return;
|
|
8382
8557
|
}
|
|
8383
8558
|
this.#scheduledBackgroundExchangeFlush = false;
|
|
8384
8559
|
this.#flushPendingBackgroundExchanges();
|
|
8385
8560
|
};
|
|
8386
|
-
setTimeout(attempt, 0);
|
|
8561
|
+
const kickoff = setTimeout(attempt, 0);
|
|
8562
|
+
kickoff.unref?.();
|
|
8387
8563
|
}
|
|
8388
8564
|
|
|
8389
8565
|
#flushPendingBackgroundExchanges(): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
3
3
|
import { expandApplyPatchToEntries } from "../edit/modes/apply-patch";
|
|
4
|
+
import { ModeStateSchema } from "../gjc-runtime/state-schema";
|
|
4
5
|
import { LocalProtocolHandler, resolveLocalUrlToPath } from "../internal-urls/local-protocol";
|
|
5
6
|
import { resolveToCwd } from "../tools/path-utils";
|
|
6
7
|
import { ToolError } from "../tools/tool-errors";
|
|
@@ -76,19 +77,37 @@ function modeStatePath(cwd: string, skill: string, sessionId?: string): string {
|
|
|
76
77
|
return path.join(stateDir, fileName);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
function warnInvalidModeState(filePath: string, error: string): void {
|
|
81
|
+
console.warn(`gjc skill-state: invalid mode-state at ${filePath}: ${error}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readValidatedModeState(filePath: string): Promise<ModeState | null> {
|
|
85
|
+
let raw: string;
|
|
80
86
|
try {
|
|
81
|
-
|
|
87
|
+
raw = await Bun.file(filePath).text();
|
|
82
88
|
} catch {
|
|
83
89
|
return null;
|
|
84
90
|
}
|
|
91
|
+
let state: ModeState;
|
|
92
|
+
try {
|
|
93
|
+
state = JSON.parse(raw) as ModeState;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
warnInvalidModeState(filePath, `invalid JSON: ${(error as Error).message}`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const parsed = ModeStateSchema.safeParse(state);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
warnInvalidModeState(filePath, parsed.error.message);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return state;
|
|
85
104
|
}
|
|
86
105
|
async function readVisibleModeState(cwd: string, skill: string, sessionId?: string): Promise<ModeState | null> {
|
|
87
106
|
if (sessionId) {
|
|
88
|
-
const sessionState = await
|
|
107
|
+
const sessionState = await readValidatedModeState(modeStatePath(cwd, skill, sessionId));
|
|
89
108
|
if (sessionState) return sessionState;
|
|
90
109
|
}
|
|
91
|
-
return await
|
|
110
|
+
return await readValidatedModeState(modeStatePath(cwd, skill));
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
function isTerminalModeState(state: ModeState | null): boolean {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill, SKILL_ACTIVE_STATE_FILE } from "./active-state";
|
|
3
|
+
import { WORKFLOW_STATE_RECEIPT_FRESH_MS, WORKFLOW_STATE_RECEIPT_VERSION } from "./workflow-state-version";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export {
|
|
6
|
+
WORKFLOW_STATE_RECEIPT_FRESH_MS,
|
|
7
|
+
WORKFLOW_STATE_RECEIPT_VERSION,
|
|
8
|
+
WORKFLOW_STATE_VERSION,
|
|
9
|
+
} from "./workflow-state-version";
|
|
8
10
|
|
|
11
|
+
export type { CanonicalGjcWorkflowSkill };
|
|
9
12
|
export type WorkflowStateMutationOwner = "gjc-state-cli" | "gjc-runtime" | "gjc-hook";
|
|
10
13
|
export type WorkflowStateReceiptStatus = "fresh" | "stale";
|
|
11
14
|
|
|
@@ -586,6 +586,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
586
586
|
runtime.ctx.editor.setText("");
|
|
587
587
|
},
|
|
588
588
|
},
|
|
589
|
+
{
|
|
590
|
+
name: "monitors",
|
|
591
|
+
description: "Open the monitor/cron jobs overlay",
|
|
592
|
+
handleTui: (_command, runtime) => {
|
|
593
|
+
runtime.ctx.showJobsOverlay();
|
|
594
|
+
runtime.ctx.editor.setText("");
|
|
595
|
+
},
|
|
596
|
+
},
|
|
589
597
|
{
|
|
590
598
|
name: "tree",
|
|
591
599
|
description: "Navigate session tree (switch branches)",
|
package/src/task/executor.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
7
9
|
import path from "node:path";
|
|
8
10
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@gajae-code/agent-core";
|
|
9
11
|
import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
|
|
@@ -1189,6 +1191,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1189
1191
|
hasUI: false,
|
|
1190
1192
|
spawns: spawnsEnv,
|
|
1191
1193
|
taskDepth: childDepth,
|
|
1194
|
+
currentAgentType: agent.name,
|
|
1192
1195
|
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1193
1196
|
parentTaskPrefix: id,
|
|
1194
1197
|
agentId: id,
|
|
@@ -1536,18 +1539,39 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1536
1539
|
|
|
1537
1540
|
// Write output artifact (input and jsonl already written in real-time)
|
|
1538
1541
|
// Compute output metadata for agent:// URL integration
|
|
1539
|
-
let outputMeta: { lineCount: number; charCount: number } | undefined;
|
|
1542
|
+
let outputMeta: { lineCount: number; charCount: number; byteSize?: number; sha256?: string } | undefined;
|
|
1540
1543
|
let outputPath: string | undefined;
|
|
1541
1544
|
if (options.artifactsDir) {
|
|
1542
|
-
|
|
1545
|
+
const candidateOutputPath = path.join(options.artifactsDir, `${id}.md`);
|
|
1543
1546
|
try {
|
|
1544
|
-
await Bun.write(
|
|
1547
|
+
await Bun.write(candidateOutputPath, rawOutput);
|
|
1548
|
+
const byteSize = Buffer.byteLength(rawOutput, "utf8");
|
|
1549
|
+
const lineCount = rawOutput.split("\n").length;
|
|
1550
|
+
const sha256 = createHash("sha256").update(rawOutput).digest("hex");
|
|
1551
|
+
const createdAt = new Date().toISOString();
|
|
1552
|
+
await Bun.write(
|
|
1553
|
+
`${candidateOutputPath}.meta.json`,
|
|
1554
|
+
JSON.stringify({ id, kind: "agent-output", sizeBytes: byteSize, lineCount, sha256, createdAt }, null, 2),
|
|
1555
|
+
);
|
|
1556
|
+
outputPath = candidateOutputPath;
|
|
1545
1557
|
outputMeta = {
|
|
1546
|
-
lineCount
|
|
1558
|
+
lineCount,
|
|
1547
1559
|
charCount: rawOutput.length,
|
|
1560
|
+
byteSize,
|
|
1561
|
+
sha256,
|
|
1548
1562
|
};
|
|
1549
1563
|
} catch {
|
|
1550
|
-
//
|
|
1564
|
+
// Output or metadata write failed: never advertise an unverifiable
|
|
1565
|
+
// artifact. Best-effort remove any orphaned `.md`/sidecar so a later
|
|
1566
|
+
// agent:// read cannot serve unverified content. Non-fatal.
|
|
1567
|
+
outputPath = undefined;
|
|
1568
|
+
outputMeta = undefined;
|
|
1569
|
+
try {
|
|
1570
|
+
await fs.rm(candidateOutputPath, { force: true });
|
|
1571
|
+
await fs.rm(`${candidateOutputPath}.meta.json`, { force: true });
|
|
1572
|
+
} catch {
|
|
1573
|
+
// best-effort cleanup; ignore
|
|
1574
|
+
}
|
|
1551
1575
|
}
|
|
1552
1576
|
}
|
|
1553
1577
|
|
package/src/task/id.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/;
|
|
2
|
+
export const TASK_ID_DESCRIPTION = "filesystem-safe identifier matching ^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$";
|
|
3
|
+
|
|
4
|
+
const ALLOCATED_TASK_ID_PATTERN = new RegExp(
|
|
5
|
+
`^\\d+-${TASK_ID_PATTERN.source.slice(1, -1)}(?:\\.\\d+-${TASK_ID_PATTERN.source.slice(1, -1)})*$`,
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export function isValidTaskId(id: string): boolean {
|
|
9
|
+
return TASK_ID_PATTERN.test(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getTaskIdValidationError(id: unknown): string | undefined {
|
|
13
|
+
if (typeof id !== "string") return "Task id must be a string.";
|
|
14
|
+
if (isValidTaskId(id)) return undefined;
|
|
15
|
+
return `Task id ${JSON.stringify(id)} is invalid. Use ${TASK_ID_DESCRIPTION}.`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateTaskId(id: string): string {
|
|
19
|
+
const error = getTaskIdValidationError(id);
|
|
20
|
+
if (error) throw new Error(error);
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isValidAllocatedTaskId(id: string): boolean {
|
|
25
|
+
return ALLOCATED_TASK_ID_PATTERN.test(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateAllocatedTaskId(id: string): string {
|
|
29
|
+
if (!isValidAllocatedTaskId(id)) {
|
|
30
|
+
throw new Error(`Allocated task id ${JSON.stringify(id)} is invalid for filesystem artifact paths.`);
|
|
31
|
+
}
|
|
32
|
+
return id;
|
|
33
|
+
}
|