feat(FN-2854): ship node routing persistence and dashboard workflow updates
- Add effective node routing fields to task types/store, persist them in SQLite, and expand regression coverage for node override guard behavior - Add dashboard and API support for manual memory dream processing/trigger actions plus expanded memory regression tests - Improve task creation and task detail model/node UX, including quick chat default model selection and workflow/settings UI polish - Apply mobile/dashboard UX fixes (form input zoom prevention, expand toggle styling, input layout tweaks) and update extension/docs/changelogs for the release
This commit is contained in:
@@ -131,7 +131,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -154,7 +154,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("priority");
|
||||
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -976,7 +976,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -1000,7 +1000,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -1104,7 +1104,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1473,7 +1473,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -779,7 +779,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
|
||||
const db1 = createDatabase(legacyDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(49);
|
||||
expect(db1.getSchemaVersion()).toBe(50);
|
||||
db1.close();
|
||||
|
||||
// Step 2: Manually downgrade to version 32 and drop insight tables
|
||||
@@ -814,7 +814,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
expect(tableNamesBefore).not.toContain("project_insight_runs");
|
||||
// Now run init — this triggers the v32→v33 migration
|
||||
db3.init();
|
||||
expect(db3.getSchemaVersion()).toBe(49);
|
||||
expect(db3.getSchemaVersion()).toBe(50);
|
||||
|
||||
// Step 4: Verify insight tables exist after migration
|
||||
const tablesAfter = db3.prepare(
|
||||
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
try {
|
||||
const db1 = createDatabase(testDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(49);
|
||||
expect(db1.getSchemaVersion()).toBe(50);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(49);
|
||||
expect(db2.getSchemaVersion()).toBe(50);
|
||||
db2.close();
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
|
||||
@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
|
||||
|
||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||
it("schema version is 40 after migration", () => {
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { TaskStore } from "../store.js";
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return mkdtempSync(join(tmpdir(), "kb-effective-node-fields-"));
|
||||
}
|
||||
|
||||
describe("effective node routing fields persistence", () => {
|
||||
let rootDir: string;
|
||||
let globalDir: string;
|
||||
let store: TaskStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootDir = makeTmpDir();
|
||||
globalDir = makeTmpDir();
|
||||
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
|
||||
await store.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
store.stopWatching();
|
||||
store.close();
|
||||
await rm(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
await rm(globalDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
});
|
||||
|
||||
it("persists effective node fields through create/update/read and clear cycle", async () => {
|
||||
const created = await store.createTask({ description: "task for effective node fields" });
|
||||
|
||||
await store.updateTask(created.id, {
|
||||
effectiveNodeId: "node-abc",
|
||||
effectiveNodeSource: "project-default",
|
||||
});
|
||||
|
||||
const withRouting = await store.getTask(created.id);
|
||||
expect(withRouting.effectiveNodeId).toBe("node-abc");
|
||||
expect(withRouting.effectiveNodeSource).toBe("project-default");
|
||||
|
||||
await store.updateTask(created.id, {
|
||||
effectiveNodeId: null,
|
||||
effectiveNodeSource: null,
|
||||
});
|
||||
|
||||
const cleared = await store.getTask(created.id);
|
||||
expect(cleared.effectiveNodeId).toBeUndefined();
|
||||
expect(cleared.effectiveNodeSource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists defaultNodeId in project settings through save/load", async () => {
|
||||
await store.updateSettings({ defaultNodeId: "node-default-1" });
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.defaultNodeId).toBe("node-default-1");
|
||||
});
|
||||
|
||||
it("defaults defaultNodeId to undefined in fresh project settings", async () => {
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.defaultNodeId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(49);
|
||||
expect(db.getSchemaVersion()).toBe(50);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -86,7 +86,7 @@ export function probeFts5(db: DatabaseSync): boolean {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 49;
|
||||
const SCHEMA_VERSION = 50;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -1852,6 +1852,15 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
// Resolved effective node fields for task routing (FN-2854).
|
||||
// effectiveNodeId is the scheduler-resolved target; effectiveNodeSource explains how it was chosen.
|
||||
if (version < 50) {
|
||||
this.applyMigration(50, () => {
|
||||
this.addColumnIfMissing("tasks", "effectiveNodeId", "TEXT");
|
||||
this.addColumnIfMissing("tasks", "effectiveNodeSource", "TEXT");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,6 +78,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
pushAfterMerge: false,
|
||||
pushRemote: "origin",
|
||||
unavailableNodePolicy: "block",
|
||||
defaultNodeId: undefined,
|
||||
worktreeInitCommand: undefined,
|
||||
testCommand: undefined,
|
||||
buildCommand: undefined,
|
||||
|
||||
@@ -91,6 +91,8 @@ interface TaskRow {
|
||||
assignedAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
nodeId: string | null;
|
||||
effectiveNodeId: string | null;
|
||||
effectiveNodeSource: string | null;
|
||||
checkedOutBy: string | null;
|
||||
checkedOutAt: string | null;
|
||||
}
|
||||
@@ -615,6 +617,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
assignedAgentId: row.assignedAgentId || undefined,
|
||||
assigneeUserId: row.assigneeUserId || undefined,
|
||||
nodeId: row.nodeId || undefined,
|
||||
effectiveNodeId: row.effectiveNodeId || undefined,
|
||||
effectiveNodeSource: (row.effectiveNodeSource as Task["effectiveNodeSource"]) || undefined,
|
||||
checkedOutBy: row.checkedOutBy || undefined,
|
||||
checkedOutAt: row.checkedOutAt || undefined,
|
||||
};
|
||||
@@ -837,7 +841,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
"dependencies", "steps", "comments", "workflowStepResults", "steeringComments",
|
||||
"attachments", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
|
||||
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"checkedOutBy", "checkedOutAt",
|
||||
// `log` is fetched in slim mode so the server can aggregate
|
||||
// `timedExecutionMs` from `[timing] … in <N>ms` entries before
|
||||
@@ -885,7 +889,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
"dependencies", "steps", "attachments", "steeringComments",
|
||||
"comments", "workflowStepResults", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
|
||||
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"checkedOutBy", "checkedOutAt",
|
||||
];
|
||||
|
||||
@@ -926,9 +930,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
comments, workflowStepResults, prInfo, issueInfo,
|
||||
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
|
||||
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, checkedOutBy, checkedOutAt
|
||||
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, checkedOutBy, checkedOutAt
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
@@ -996,6 +1000,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
assignedAgentId = excluded.assignedAgentId,
|
||||
assigneeUserId = excluded.assigneeUserId,
|
||||
nodeId = excluded.nodeId,
|
||||
effectiveNodeId = excluded.effectiveNodeId,
|
||||
effectiveNodeSource = excluded.effectiveNodeSource,
|
||||
checkedOutBy = excluded.checkedOutBy,
|
||||
checkedOutAt = excluded.checkedOutAt
|
||||
`).run(
|
||||
@@ -1065,6 +1071,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.assignedAgentId ?? null,
|
||||
task.assigneeUserId ?? null,
|
||||
task.nodeId ?? null,
|
||||
task.effectiveNodeId ?? null,
|
||||
task.effectiveNodeSource ?? null,
|
||||
task.checkedOutBy ?? null,
|
||||
task.checkedOutAt ?? null,
|
||||
);
|
||||
@@ -2619,7 +2627,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
@@ -2704,6 +2712,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
} else if (updates.nodeId !== undefined) {
|
||||
task.nodeId = updates.nodeId;
|
||||
}
|
||||
if (updates.effectiveNodeId === null) {
|
||||
task.effectiveNodeId = undefined;
|
||||
} else if (updates.effectiveNodeId !== undefined) {
|
||||
task.effectiveNodeId = updates.effectiveNodeId;
|
||||
}
|
||||
if (updates.effectiveNodeSource === null) {
|
||||
task.effectiveNodeSource = undefined;
|
||||
} else if (updates.effectiveNodeSource !== undefined) {
|
||||
task.effectiveNodeSource = updates.effectiveNodeSource as Task["effectiveNodeSource"];
|
||||
}
|
||||
if (updates.checkedOutBy === null) {
|
||||
task.checkedOutBy = undefined;
|
||||
task.checkedOutAt = undefined;
|
||||
|
||||
@@ -845,6 +845,10 @@ export interface Task {
|
||||
assignedAgentId?: string;
|
||||
/** Per-task node override. When set, this task routes to the specified node instead of the project's default node. Undefined means use the project default. Use empty string to explicitly clear. */
|
||||
nodeId?: string;
|
||||
/** The node this task is actually routed to (resolved from nodeId override or project default). Set by the scheduler at dispatch time. */
|
||||
effectiveNodeId?: string;
|
||||
/** How the effectiveNodeId was determined. Set by the scheduler at dispatch time. */
|
||||
effectiveNodeSource?: "task-override" | "project-default" | "local";
|
||||
/** Explicitly assigned user ID for task-user linking. Used during review handoff to indicate
|
||||
* which user should review the task. The sentinel value "requesting-user" indicates the
|
||||
* user who created or steered the task. */
|
||||
@@ -1332,6 +1336,8 @@ export interface ProjectSettings {
|
||||
* - "block": prevent execution until the selected node is healthy/available (default)
|
||||
* - "fallback-local": run on the local node when the selected node is unavailable */
|
||||
unavailableNodePolicy?: UnavailableNodePolicy;
|
||||
/** ID of the pinned default execution node. Tasks without a per-task override run on this node. */
|
||||
defaultNodeId?: string;
|
||||
/** Shell command to run inside each new worktree immediately after creation.
|
||||
* Useful for project-specific setup (e.g. `pnpm install --frozen-lockfile`, `cp .env.local .env`). */
|
||||
worktreeInitCommand?: string;
|
||||
|
||||
Reference in New Issue
Block a user