@runuai/host 0.9.160 → 0.9.162
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/lib/agent-cli.ts +28 -15
- package/lib/agent.ts +9 -0
- package/lib/agents/uai.ts +3 -4
- package/lib/managed-runtime.ts +5 -0
- package/lib/orchestrator.ts +5 -8
- package/lib/standard-image-pin.ts +5 -2
- package/lib/task-credential-cleanup.ts +12 -2
- package/lib/task-environment/machine-task-up.ts +6 -2
- package/lib/task-environment/machine.ts +2 -1
- package/lib/task-environment/types.ts +4 -0
- package/package.json +1 -1
- package/scripts/agent/task-down.sh +13 -0
- package/scripts/agent/task-up.sh +30 -0
package/lib/agent-cli.ts
CHANGED
|
@@ -183,11 +183,15 @@ export async function writeAgentCliViaEnvironment(
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Per-agent env for the `docker exec` that spawns one agent (ADR-048): its OWN
|
|
186
|
-
* task token (carrying only ITS permissions) + the API url.
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
186
|
+
* task token (carrying only ITS permissions) + the API url. Every agent gets
|
|
187
|
+
* one, permissions or not: it is the agent's identity — the Uai Managed
|
|
188
|
+
* Engine's gateway authenticates and bills by it (ADR-128), and the
|
|
189
|
+
* Secretary's dispatch is role-gated on it. Until 2026-10-20 an agent with no
|
|
190
|
+
* CLI permissions got none, so such a persona could not run on the managed
|
|
191
|
+
* engine at all. An empty-permission token cannot borrow anyone else's powers:
|
|
192
|
+
* every permissioned route refuses it, and the self-scoped ones
|
|
193
|
+
* (`requireAnyPersonaPermission`) do too. Empty only when the task has no
|
|
194
|
+
* owner, API url or CLI secret.
|
|
191
195
|
*/
|
|
192
196
|
export function agentCliEnv(
|
|
193
197
|
taskId: string,
|
|
@@ -195,18 +199,10 @@ export function agentCliEnv(
|
|
|
195
199
|
ownerUserId: string | null,
|
|
196
200
|
apiUrl: string | null,
|
|
197
201
|
cliSecret: string | null,
|
|
198
|
-
allowPermissionless = false,
|
|
199
202
|
): Record<string, string> {
|
|
200
203
|
const runtimeAuthority = { ...AGENT_RUNTIME_AUTHORITY_ENV };
|
|
201
204
|
const permissions = agent.permissions ?? [];
|
|
202
|
-
if (
|
|
203
|
-
!ownerUserId ||
|
|
204
|
-
!apiUrl ||
|
|
205
|
-
!cliSecret ||
|
|
206
|
-
(permissions.length === 0 && !allowPermissionless)
|
|
207
|
-
) {
|
|
208
|
-
return runtimeAuthority;
|
|
209
|
-
}
|
|
205
|
+
if (!ownerUserId || !apiUrl || !cliSecret) return runtimeAuthority;
|
|
210
206
|
return {
|
|
211
207
|
...runtimeAuthority,
|
|
212
208
|
UAI_TASK_TOKEN: signTaskToken(
|
|
@@ -498,7 +494,22 @@ async function main() {
|
|
|
498
494
|
const port = Number(pos[1] || flags.port);
|
|
499
495
|
if (!name || !Number.isInteger(port)) { console.error("uai: preview add <name> <containerPort>"); process.exit(1); }
|
|
500
496
|
const r = await api("POST", "/api/agent/previews", { name, containerPort: port });
|
|
501
|
-
out("preview '" + r.name + "' on port " + r.containerPort + " —
|
|
497
|
+
out("preview '" + r.name + "' on port " + r.containerPort + " — turn it on with: uai preview enable " + r.name);
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "preview enable":
|
|
501
|
+
case "preview disable":
|
|
502
|
+
case "preview public":
|
|
503
|
+
case "preview private": {
|
|
504
|
+
const name = pos[0] || flags.name;
|
|
505
|
+
if (!name) { console.error("uai: preview " + action + " <name>"); process.exit(1); }
|
|
506
|
+
const body = action === "enable" || action === "disable"
|
|
507
|
+
? { enabled: action === "enable" }
|
|
508
|
+
: { public: action === "public" };
|
|
509
|
+
const r = await api("PATCH", "/api/agent/previews/" + encodeURIComponent(name), body);
|
|
510
|
+
const state = (r.enabled ? "on" : "off") + ", " + (r.public ? "public (anyone with the link)" : "private (sign-in)");
|
|
511
|
+
out("preview '" + r.name + "' is " + state + (r.url ? " — " + r.url : ""));
|
|
512
|
+
if (!r.enabled && r.public) console.error("uai: it's public but off — it serves nothing until: uai preview enable " + r.name);
|
|
502
513
|
break;
|
|
503
514
|
}
|
|
504
515
|
case "project create": {
|
|
@@ -551,6 +562,8 @@ async function main() {
|
|
|
551
562
|
" uai todo remove <#id>",
|
|
552
563
|
" uai todo claim|done|reopen|unclaim <#id>",
|
|
553
564
|
" uai preview add <name> <containerPort>",
|
|
565
|
+
" uai preview enable|disable <name> (turn it on; prints its URL)",
|
|
566
|
+
" uai preview public|private <name> (public: a link anyone can open)",
|
|
554
567
|
" uai project create --name <n> --repo <git-url> [--prompt <p>]",
|
|
555
568
|
" uai task search <query> (your own past tasks)",
|
|
556
569
|
" uai task history (list your past tasks)",
|
package/lib/agent.ts
CHANGED
|
@@ -157,6 +157,13 @@ const TaskUpData = z.object({
|
|
|
157
157
|
export type TaskUpResult = z.infer<typeof TaskUpData>;
|
|
158
158
|
|
|
159
159
|
// task-down
|
|
160
|
+
/** task-down.sh destroys the task's runtime AND its directory (worktree +
|
|
161
|
+
* task-private Git data), so it refuses without this explicit terminal
|
|
162
|
+
* intent. Only the cloud taskDown reaches these two calls: the handler in
|
|
163
|
+
* src/index.ts, and a credential cleanup under that same command
|
|
164
|
+
* (`reason: "teardown"`, task-credential-cleanup.ts). */
|
|
165
|
+
const TASK_DOWN_TERMINAL_ENV = Object.freeze({ UAI_TASK_DOWN_TERMINAL: "1" });
|
|
166
|
+
|
|
160
167
|
const TaskDownData = z
|
|
161
168
|
.object({
|
|
162
169
|
status: z.string().optional(),
|
|
@@ -598,6 +605,7 @@ export const agent = {
|
|
|
598
605
|
[input.taskId],
|
|
599
606
|
TaskDownData,
|
|
600
607
|
commandDbPath,
|
|
608
|
+
TASK_DOWN_TERMINAL_ENV,
|
|
601
609
|
);
|
|
602
610
|
} finally {
|
|
603
611
|
removeCommandDb(commandDbPath);
|
|
@@ -617,6 +625,7 @@ export const agent = {
|
|
|
617
625
|
[taskId],
|
|
618
626
|
TaskDownData,
|
|
619
627
|
commandDbPath,
|
|
628
|
+
TASK_DOWN_TERMINAL_ENV,
|
|
620
629
|
);
|
|
621
630
|
} finally {
|
|
622
631
|
removeCommandDb(commandDbPath);
|
package/lib/agents/uai.ts
CHANGED
|
@@ -93,11 +93,10 @@ const create: AgentSessionFactory["create"] = async (args): Promise<AgentSession
|
|
|
93
93
|
}
|
|
94
94
|
const taskToken = args.agentEnv?.UAI_TASK_TOKEN;
|
|
95
95
|
if (!taskToken) {
|
|
96
|
-
// agentCliEnv mints
|
|
97
|
-
//
|
|
98
|
-
// needs it. The default crew grants permissions, so this is rare.
|
|
96
|
+
// agentCliEnv mints one for every managed-engine agent, permissions or
|
|
97
|
+
// not; none here means the task has no owner, API url or CLI secret.
|
|
99
98
|
throw new Error(
|
|
100
|
-
"uai engine: this agent has no task token (no CLI
|
|
99
|
+
"uai engine: this agent has no task token (the task has no CLI identity yet) — restart the task",
|
|
101
100
|
);
|
|
102
101
|
}
|
|
103
102
|
return delegate.create({
|
package/lib/managed-runtime.ts
CHANGED
|
@@ -1004,6 +1004,11 @@ async function managedInstallState(paths: ManagedRuntimePaths): Promise<ManagedI
|
|
|
1004
1004
|
] as const) {
|
|
1005
1005
|
try {
|
|
1006
1006
|
await lstat(state);
|
|
1007
|
+
// ensureManagedDirectories (a capability-floor claim) creates the bare
|
|
1008
|
+
// runtime directory on an npm install too. Empty, it holds no
|
|
1009
|
+
// generation: still no managed install. Every npm-installed task
|
|
1010
|
+
// machine sat pending on this (2026-10-20).
|
|
1011
|
+
if (state === paths.runtime && (await readdir(state)).length === 0) continue;
|
|
1007
1012
|
return { kind: "damaged", detail: `the managed runtime has a ${what} but no current runtime; repair it first` };
|
|
1008
1013
|
} catch (error) {
|
|
1009
1014
|
if (!isMissing(error)) throw error;
|
package/lib/orchestrator.ts
CHANGED
|
@@ -2595,7 +2595,6 @@ export class Orchestrator {
|
|
|
2595
2595
|
task.ownerUserId,
|
|
2596
2596
|
apiUrl,
|
|
2597
2597
|
cliSecret,
|
|
2598
|
-
this.isPromptLevelSecretary(channel, agent.id),
|
|
2599
2598
|
),
|
|
2600
2599
|
task,
|
|
2601
2600
|
),
|
|
@@ -4371,7 +4370,6 @@ export class Orchestrator {
|
|
|
4371
4370
|
task.ownerUserId,
|
|
4372
4371
|
apiUrl,
|
|
4373
4372
|
cliSecret,
|
|
4374
|
-
this.isPromptLevelSecretary(channel, agent.id),
|
|
4375
4373
|
),
|
|
4376
4374
|
task,
|
|
4377
4375
|
),
|
|
@@ -5488,7 +5486,6 @@ export class Orchestrator {
|
|
|
5488
5486
|
task.ownerUserId,
|
|
5489
5487
|
apiUrl,
|
|
5490
5488
|
cliSecret,
|
|
5491
|
-
this.isPromptLevelSecretary(channel, agent.id),
|
|
5492
5489
|
);
|
|
5493
5490
|
const boundAccountId = channel.accountByAgent.get(agentId);
|
|
5494
5491
|
const boundAccount = boundAccountId
|
|
@@ -5698,7 +5695,6 @@ export class Orchestrator {
|
|
|
5698
5695
|
task.ownerUserId,
|
|
5699
5696
|
apiUrl,
|
|
5700
5697
|
cliSecret,
|
|
5701
|
-
this.isPromptLevelSecretary(channel, agent.id),
|
|
5702
5698
|
);
|
|
5703
5699
|
channel.accountByAgent.set(agentId, next.id);
|
|
5704
5700
|
noteEngineAccountUsed(next.id);
|
|
@@ -5908,7 +5904,6 @@ export class Orchestrator {
|
|
|
5908
5904
|
task.ownerUserId,
|
|
5909
5905
|
apiUrl,
|
|
5910
5906
|
cliSecret,
|
|
5911
|
-
this.isPromptLevelSecretary(channel, agent.id),
|
|
5912
5907
|
);
|
|
5913
5908
|
channel.accountByAgent.set(agentId, account.id);
|
|
5914
5909
|
noteEngineAccountUsed(account.id);
|
|
@@ -7280,7 +7275,7 @@ export function buildSystemPreamble(
|
|
|
7280
7275
|
? ", `todo list|add|edit|remove|claim|done`"
|
|
7281
7276
|
: "") +
|
|
7282
7277
|
(agent.permissions?.includes("previews.write")
|
|
7283
|
-
? ", `preview add <name
|
|
7278
|
+
? ", `preview add|enable|public <name>`"
|
|
7284
7279
|
: "") +
|
|
7285
7280
|
(agent.permissions?.includes("projects.write")
|
|
7286
7281
|
? ", `project create --name --repo`"
|
|
@@ -7355,8 +7350,10 @@ export function buildSystemPreamble(
|
|
|
7355
7350
|
...(agent.permissions?.includes("previews.write")
|
|
7356
7351
|
? [
|
|
7357
7352
|
"**Previews:** started a dev server? `preview add <name>",
|
|
7358
|
-
"<containerPort>`
|
|
7359
|
-
"
|
|
7353
|
+
"<containerPort>` registers it, then `preview enable <name>` turns",
|
|
7354
|
+
"it on and prints its URL: give that URL to the humans. It needs",
|
|
7355
|
+
"a sign-in; `preview public <name>` makes the task's previews",
|
|
7356
|
+
"a link anyone can open — only when a human asks for that.",
|
|
7360
7357
|
]
|
|
7361
7358
|
: []),
|
|
7362
7359
|
"",
|
|
@@ -22,8 +22,11 @@ import { join } from "node:path";
|
|
|
22
22
|
/** Image label carrying the build-context content hash (rebuild trigger). */
|
|
23
23
|
export const CONTEXT_HASH_LABEL = "com.runuai.context-hash";
|
|
24
24
|
|
|
25
|
-
/** The public registry repository CI publishes the prebuilt image to.
|
|
26
|
-
|
|
25
|
+
/** The public registry repository CI publishes the prebuilt image to. The
|
|
26
|
+
* GitHub org was renamed runuai → moondeckio on 2026-10-20; GHCR, unlike
|
|
27
|
+
* github.com, does not redirect the old owner (pushes fail "owner not
|
|
28
|
+
* found", pulls 404). */
|
|
29
|
+
export const STANDARD_IMAGE_RELEASE_REF = "ghcr.io/moondeckio/uai-standard";
|
|
27
30
|
|
|
28
31
|
/** Pin file basename; packaged at `agent/images/<basename>` in the payload,
|
|
29
32
|
* one directory above the build context so it never perturbs the hash. */
|
|
@@ -64,7 +64,8 @@ export interface TaskCredentialCleanupDeps {
|
|
|
64
64
|
db?: Db;
|
|
65
65
|
hooks: TaskCredentialCleanupHooks;
|
|
66
66
|
removePath?: (row: HostCredentialCleanupPath) => Promise<void>;
|
|
67
|
-
/** Full cleanup
|
|
67
|
+
/** Full cleanup under the cloud's taskDown (`reason: "teardown"`) only:
|
|
68
|
+
* destroy one exact runtime and prove it absent. */
|
|
68
69
|
destroyRuntime?: (request: {
|
|
69
70
|
taskId: string;
|
|
70
71
|
runEpoch: number;
|
|
@@ -464,9 +465,18 @@ export class TaskCredentialCleanupCoordinator {
|
|
|
464
465
|
}
|
|
465
466
|
|
|
466
467
|
// A whole-task cleanup may prove absence by destroying the exact writable
|
|
467
|
-
// runtime instead
|
|
468
|
+
// runtime instead, but ONLY inside the cloud's taskDown of this run (the
|
|
469
|
+
// durable reason `teardown` has no other source). Every other cleanup —
|
|
470
|
+
// boot recovery, a fence escalation, stop, park, assignment — defers: the
|
|
471
|
+
// gate stays closed, the pending witness and inventory rows stay, and the
|
|
472
|
+
// registry's retry tries the exact paths again later. A failed exec is
|
|
473
|
+
// not proof of absence; Docker briefly gone (an OrbStack update,
|
|
474
|
+
// 2026-10-20) once turned this fallback into task-down.sh on every live
|
|
475
|
+
// task, deleting their worktrees. Selective removal must never take this
|
|
476
|
+
// broader action either.
|
|
468
477
|
if (
|
|
469
478
|
!consumersFailed &&
|
|
479
|
+
request.reason === "teardown" &&
|
|
470
480
|
request.credentialIds === undefined &&
|
|
471
481
|
failed.length > 0
|
|
472
482
|
) {
|
|
@@ -1015,7 +1015,8 @@ export function createMachineHostTaskEnvironmentProvider(
|
|
|
1015
1015
|
// timeouts bound the wait). A teardown left for later could take a
|
|
1016
1016
|
// replacement, which reuses the task's machine name.
|
|
1017
1017
|
const late = await provisioning.catch(() => null);
|
|
1018
|
-
|
|
1018
|
+
// A reused machine holds the task's work: stopped, never destroyed.
|
|
1019
|
+
if (late) await (late.reused ? late.handle.stop() : late.handle.teardown()).catch(() => {});
|
|
1019
1020
|
}
|
|
1020
1021
|
throw error;
|
|
1021
1022
|
}
|
|
@@ -1043,7 +1044,10 @@ export function createMachineHostTaskEnvironmentProvider(
|
|
|
1043
1044
|
const abandon = (): Promise<void> => {
|
|
1044
1045
|
over = true;
|
|
1045
1046
|
abandoning ??= (async () => {
|
|
1046
|
-
|
|
1047
|
+
// Only a machine this start launched is destroyed. One it reused (a
|
|
1048
|
+
// Resume of a stopped machine) holds the task's work on its disk, so
|
|
1049
|
+
// a failed start stops it again and the next Resume tries afresh.
|
|
1050
|
+
await (provisioned.reused ? handle.stop() : handle.teardown()).catch(() => {});
|
|
1047
1051
|
await joinBounded(begun, deps.abandonJoinMs ?? ABANDON_JOIN_MS);
|
|
1048
1052
|
})();
|
|
1049
1053
|
return abandoning;
|
|
@@ -862,6 +862,7 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
862
862
|
`machine ${machineId} state is unknown: ${existing.detail ?? "no detail"}`,
|
|
863
863
|
);
|
|
864
864
|
}
|
|
865
|
+
const reused = existing.state !== "absent";
|
|
865
866
|
if (existing.state === "absent") {
|
|
866
867
|
const launched = await deps.machines.launch({
|
|
867
868
|
taskId,
|
|
@@ -935,7 +936,7 @@ export function createMachineTaskEnvironmentProvider(
|
|
|
935
936
|
);
|
|
936
937
|
}
|
|
937
938
|
}
|
|
938
|
-
return { handle, result: handle.descriptor };
|
|
939
|
+
return { handle, result: handle.descriptor, reused };
|
|
939
940
|
},
|
|
940
941
|
|
|
941
942
|
async reconstruct(
|
|
@@ -349,6 +349,10 @@ export interface TaskEnvironmentProvisioned<TResult, TTeardownResult = void> {
|
|
|
349
349
|
handle: TaskEnvironmentHandle<TTeardownResult>;
|
|
350
350
|
/** Provider launch result retained for rolling wire compatibility. */
|
|
351
351
|
result: TResult;
|
|
352
|
+
/** The provider reused an environment that already existed — a Resume's
|
|
353
|
+
* stopped machine, whose disk holds the task's work — rather than creating
|
|
354
|
+
* it. A failed start stops such an environment; it never destroys it. */
|
|
355
|
+
reused?: boolean;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
export interface TaskEnvironmentProvider<
|
package/package.json
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
# 2. rm -rf the whole task dir — workspace + task-private Git repositories.
|
|
6
6
|
# Host caches under projects/<id>/host-cache.git persist.
|
|
7
7
|
# 3. Persist final state: status=killed (unless already error/shipped).
|
|
8
|
+
#
|
|
9
|
+
# Destruction needs explicit terminal intent: the runner sets
|
|
10
|
+
# UAI_TASK_DOWN_TERMINAL=1 only on the cloud taskDown path (a task close or
|
|
11
|
+
# orphan GC). Without it the script refuses before touching Docker or disk —
|
|
12
|
+
# a stray caller (recovery, a credential-cleanup fallback, an operator
|
|
13
|
+
# running the script by hand) must never delete a live task's worktree
|
|
14
|
+
# (2026-10-20: a cleanup fallback did, while Docker was briefly gone).
|
|
8
15
|
|
|
9
16
|
set -euo pipefail
|
|
10
17
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -20,6 +27,12 @@ if [ "${#task_id}" -gt 128 ] ||
|
|
|
20
27
|
[[ ! "$task_id" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
|
|
21
28
|
emit_err "BAD_ARGS" "invalid task id" "task identity validation"
|
|
22
29
|
fi
|
|
30
|
+
# Before UAI_TASK_ID is set: a refusal must not mark the live task errored.
|
|
31
|
+
if [ "${UAI_TASK_DOWN_TERMINAL:-}" != "1" ]; then
|
|
32
|
+
emit_err "BAD_ARGS" \
|
|
33
|
+
"task-down requires terminal intent (UAI_TASK_DOWN_TERMINAL=1, set only by the cloud taskDown); refusing to destroy task $task_id" \
|
|
34
|
+
"terminal intent"
|
|
35
|
+
fi
|
|
23
36
|
UAI_TASK_ID="$task_id"
|
|
24
37
|
setup_err_trap
|
|
25
38
|
validate_task_runtime_selection
|
package/scripts/agent/task-up.sh
CHANGED
|
@@ -1453,9 +1453,39 @@ while IFS= read -r project_obj; do
|
|
|
1453
1453
|
git_host_control -C "$worktree_repo_dir" worktree repair \
|
|
1454
1454
|
"$worktree_target" 2>/dev/null || true
|
|
1455
1455
|
fi
|
|
1456
|
+
elif [ -n "$task_branch" ] && [ "$task_branch" != "null" ] \
|
|
1457
|
+
&& [ "$task_branch" != "$project_default" ] \
|
|
1458
|
+
&& ! git_host_control -C "$worktree_repo_dir" rev-parse --verify --quiet \
|
|
1459
|
+
"refs/heads/$task_branch" >/dev/null 2>&1 \
|
|
1460
|
+
&& git_host_control -C "$worktree_repo_dir" rev-parse --verify --quiet \
|
|
1461
|
+
"refs/remotes/origin/$task_branch^{commit}" >/dev/null 2>&1; then
|
|
1462
|
+
# The worktree is gone but the task's branch is on the remote (a lost
|
|
1463
|
+
# task directory, 2026-10-20: Resume rebuilt from the base, putting the
|
|
1464
|
+
# agents on the wrong code with the pushed commits out of sight). The
|
|
1465
|
+
# fetch above is fresh — a missing worktree makes a failed fetch fatal —
|
|
1466
|
+
# so continue from the pushed branch, never the base.
|
|
1467
|
+
step "WORKTREE_FAILED" "git worktree add from origin/$task_branch ($project_id)"
|
|
1468
|
+
log "worktree for $project_slug missing — restoring it from origin/$task_branch"
|
|
1469
|
+
# The base this task branched from, as nearly as the remote can tell:
|
|
1470
|
+
# where the task branch leaves the default branch.
|
|
1471
|
+
env_schema_oid=$(git_host_control -C "$mirror_dir" merge-base \
|
|
1472
|
+
"refs/remotes/origin/$task_branch" "refs/remotes/origin/$project_default" \
|
|
1473
|
+
2>/dev/null || true)
|
|
1474
|
+
if [ -n "$env_schema_oid" ]; then
|
|
1475
|
+
record_env_schema_base "$project_id" "oid $env_schema_oid" \
|
|
1476
|
+
|| log "warning: could not record the .env.schema base for $project_id; its keys without a binding are withheld"
|
|
1477
|
+
fi
|
|
1478
|
+
# --no-track: the sanitizer above already wrote this branch's upstream.
|
|
1479
|
+
if ! git_host_control -C "$worktree_repo_dir" worktree add --no-track \
|
|
1480
|
+
"$worktree_target" -b "$task_branch" "refs/remotes/origin/$task_branch"; then
|
|
1481
|
+
emit_err "WORKTREE_FAILED" \
|
|
1482
|
+
"could not restore the task worktree from origin/$task_branch for $repo_url" \
|
|
1483
|
+
"git worktree add from origin/$task_branch ($project_id)"
|
|
1484
|
+
fi
|
|
1456
1485
|
elif git_host_control -C "$worktree_repo_dir" rev-parse --verify --quiet \
|
|
1457
1486
|
"refs/remotes/origin/$project_default" >/dev/null 2>&1; then
|
|
1458
1487
|
# Normal case: branch the task worktree off origin/<defaultBranch>.
|
|
1488
|
+
# Only when the remote has no branch of this task's yet.
|
|
1459
1489
|
step "WORKTREE_FAILED" "git worktree add ($project_id)"
|
|
1460
1490
|
# The same commit the task repository just copied, read where the task
|
|
1461
1491
|
# never writes: the host cache, under this project's lock.
|