@amistio/cli 0.1.1 → 0.1.2
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/README.md +2 -2
- package/dist/index.js +1138 -96
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
4
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
6
|
+
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
7
|
+
import os5 from "node:os";
|
|
8
|
+
import path9 from "node:path";
|
|
7
9
|
import { Command } from "commander";
|
|
8
10
|
|
|
9
11
|
// ../shared/src/schemas.ts
|
|
@@ -24,6 +26,9 @@ var itemTypeSchema = z.enum([
|
|
|
24
26
|
"workItem",
|
|
25
27
|
"runnerHeartbeat",
|
|
26
28
|
"runnerExecutionLog",
|
|
29
|
+
"runnerCredential",
|
|
30
|
+
"runnerCommand",
|
|
31
|
+
"planReviewMessage",
|
|
27
32
|
"toolSession",
|
|
28
33
|
"activityEvent",
|
|
29
34
|
"pairingSession"
|
|
@@ -60,7 +65,7 @@ var workStatusSchema = z.enum([
|
|
|
60
65
|
]);
|
|
61
66
|
var sourceSchema = z.enum(["web", "repo", "generated", "runner"]);
|
|
62
67
|
var executionModeSchema = z.enum(["localRunner", "cloudSandbox"]);
|
|
63
|
-
var workKindSchema = z.enum(["brainGeneration", "implementation"]);
|
|
68
|
+
var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision"]);
|
|
64
69
|
var generatedDraftStatusSchema = z.enum(["queued", "generating", "reviewing", "approved", "rejected", "changesRequested", "failed"]);
|
|
65
70
|
var generatedBrainArtifactSchema = z.object({
|
|
66
71
|
documentType: documentTypeSchema,
|
|
@@ -76,6 +81,27 @@ var sessionPolicySchema = z.union([z.enum(["auto", "new", "none"]), z.string().r
|
|
|
76
81
|
var sessionDecisionSchema = z.enum(["created", "continued", "forcedNew", "forcedContinue", "notSupported", "skipped"]);
|
|
77
82
|
var toolSessionStatusSchema = z.enum(["open", "active", "closed", "archived", "blocked", "unavailable"]);
|
|
78
83
|
var sessionResumabilityScopeSchema = z.enum(["none", "localMachine", "repository", "account", "providerCloud"]);
|
|
84
|
+
var runnerToolNames = ["opencode", "claude", "codex", "copilot", "gemini", "aider", "cursor-agent"];
|
|
85
|
+
var runnerToolNameSchema = z.enum(runnerToolNames);
|
|
86
|
+
var runnerToolSelectionSchema = z.union([runnerToolNameSchema, z.literal("auto")]);
|
|
87
|
+
var runnerPreferenceScopeSchema = z.enum(["account", "project"]);
|
|
88
|
+
var runnerPreferenceSourceSchema = z.enum(["cli", "project", "account", "default"]);
|
|
89
|
+
var runnerPreferenceStatusSchema = z.enum(["resolved", "unavailable", "modelUnsupported", "custom", "none"]);
|
|
90
|
+
var runnerToolModelPreferenceSchema = z.object({
|
|
91
|
+
tool: runnerToolSelectionSchema.optional(),
|
|
92
|
+
model: z.string().trim().min(1).max(160).optional()
|
|
93
|
+
});
|
|
94
|
+
var runnerToolCapabilitySchema = z.object({
|
|
95
|
+
name: runnerToolNameSchema,
|
|
96
|
+
description: z.string().min(1),
|
|
97
|
+
available: z.boolean(),
|
|
98
|
+
sdkAvailable: z.boolean(),
|
|
99
|
+
commandAvailable: z.boolean(),
|
|
100
|
+
execution: z.enum(["sdk", "command", "unavailable"]),
|
|
101
|
+
supportsSessionReuse: z.boolean(),
|
|
102
|
+
resumabilityScope: sessionResumabilityScopeSchema,
|
|
103
|
+
supportsModelSelection: z.boolean()
|
|
104
|
+
});
|
|
79
105
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
80
106
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
81
107
|
var baseItemSchema = z.object({
|
|
@@ -182,6 +208,10 @@ var workItemSchema = baseItemSchema.extend({
|
|
|
182
208
|
approvedBy: z.string().min(1).optional(),
|
|
183
209
|
sourceWish: z.string().min(1).optional(),
|
|
184
210
|
generatedDraftId: z.string().min(1).optional(),
|
|
211
|
+
reviewThreadId: z.string().min(1).optional(),
|
|
212
|
+
reviewDocumentId: z.string().min(1).optional(),
|
|
213
|
+
reviewDocumentRevision: z.number().int().nonnegative().optional(),
|
|
214
|
+
reviewMessageId: z.string().min(1).optional(),
|
|
185
215
|
claimedByRunnerId: z.string().optional(),
|
|
186
216
|
leaseExpiresAt: isoDateTimeSchema.optional(),
|
|
187
217
|
attempt: z.number().int().nonnegative().default(0),
|
|
@@ -201,8 +231,25 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
201
231
|
repositoryLinkId: z.string().min(1),
|
|
202
232
|
status: z.enum(["online", "offline", "running", "blocked"]),
|
|
203
233
|
version: z.string().optional(),
|
|
234
|
+
mode: z.enum(["foreground", "background"]).optional(),
|
|
235
|
+
hostname: z.string().min(1).optional(),
|
|
236
|
+
runnerName: z.string().min(1).optional(),
|
|
237
|
+
capabilities: z.array(runnerToolCapabilitySchema).optional(),
|
|
238
|
+
requestedTool: runnerToolSelectionSchema.optional(),
|
|
239
|
+
effectiveTool: z.union([runnerToolNameSchema, z.literal("custom")]).optional(),
|
|
240
|
+
effectiveModel: z.string().min(1).optional(),
|
|
241
|
+
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
242
|
+
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
243
|
+
preferenceMessage: z.string().optional(),
|
|
204
244
|
lastSeenAt: isoDateTimeSchema
|
|
205
245
|
});
|
|
246
|
+
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
247
|
+
type: z.literal("accountSettings"),
|
|
248
|
+
projectId: z.string().min(1),
|
|
249
|
+
settingsType: z.literal("runnerPreferences"),
|
|
250
|
+
scope: runnerPreferenceScopeSchema,
|
|
251
|
+
preferences: runnerToolModelPreferenceSchema
|
|
252
|
+
});
|
|
206
253
|
var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
207
254
|
type: z.literal("runnerExecutionLog"),
|
|
208
255
|
projectId: z.string().min(1),
|
|
@@ -232,6 +279,57 @@ var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
|
232
279
|
message: z.string().optional(),
|
|
233
280
|
error: z.string().optional()
|
|
234
281
|
});
|
|
282
|
+
var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
283
|
+
type: z.literal("runnerCredential"),
|
|
284
|
+
projectId: z.string().min(1),
|
|
285
|
+
runnerCredentialId: z.string().min(1),
|
|
286
|
+
repositoryLinkId: z.string().min(1),
|
|
287
|
+
tokenHash: z.string().min(32),
|
|
288
|
+
issuedAt: isoDateTimeSchema,
|
|
289
|
+
lastUsedAt: isoDateTimeSchema.optional(),
|
|
290
|
+
status: z.enum(["active", "revoked"]).default("active")
|
|
291
|
+
});
|
|
292
|
+
var runnerCommandKindSchema = z.enum(["update", "restart", "remove"]);
|
|
293
|
+
var runnerCommandStatusSchema = z.enum(["pending", "acknowledged", "running", "completed", "failed", "expired", "cancelled"]);
|
|
294
|
+
var runnerCommandItemSchema = baseItemSchema.extend({
|
|
295
|
+
type: z.literal("runnerCommand"),
|
|
296
|
+
projectId: z.string().min(1),
|
|
297
|
+
commandId: z.string().min(1),
|
|
298
|
+
commandKind: runnerCommandKindSchema,
|
|
299
|
+
status: runnerCommandStatusSchema,
|
|
300
|
+
runnerId: z.string().min(1),
|
|
301
|
+
repositoryLinkId: z.string().min(1),
|
|
302
|
+
requestedByUserId: z.string().min(1),
|
|
303
|
+
idempotencyKey: z.string().min(1),
|
|
304
|
+
lastStatusIdempotencyKey: z.string().min(1).optional(),
|
|
305
|
+
expiresAt: isoDateTimeSchema,
|
|
306
|
+
acknowledgedAt: isoDateTimeSchema.optional(),
|
|
307
|
+
startedAt: isoDateTimeSchema.optional(),
|
|
308
|
+
completedAt: isoDateTimeSchema.optional(),
|
|
309
|
+
cancelledAt: isoDateTimeSchema.optional(),
|
|
310
|
+
message: z.string().optional(),
|
|
311
|
+
error: z.string().optional()
|
|
312
|
+
});
|
|
313
|
+
var planReviewMessageRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
314
|
+
var planReviewMessageIntentSchema = z.enum(["ask", "revisionRequest", "revisionResult"]);
|
|
315
|
+
var planReviewMessageStatusSchema = z.enum(["posted", "queued", "running", "completed", "failed"]);
|
|
316
|
+
var planReviewMessageItemSchema = baseItemSchema.extend({
|
|
317
|
+
type: z.literal("planReviewMessage"),
|
|
318
|
+
projectId: z.string().min(1),
|
|
319
|
+
messageId: z.string().min(1),
|
|
320
|
+
threadId: z.string().min(1),
|
|
321
|
+
generatedDraftId: z.string().min(1),
|
|
322
|
+
documentId: z.string().min(1),
|
|
323
|
+
documentRevision: z.number().int().nonnegative(),
|
|
324
|
+
role: planReviewMessageRoleSchema,
|
|
325
|
+
intent: planReviewMessageIntentSchema,
|
|
326
|
+
status: planReviewMessageStatusSchema,
|
|
327
|
+
content: z.string().min(1),
|
|
328
|
+
workItemId: z.string().min(1).optional(),
|
|
329
|
+
responseToMessageId: z.string().min(1).optional(),
|
|
330
|
+
createdByUserId: z.string().min(1).optional(),
|
|
331
|
+
runnerId: z.string().min(1).optional()
|
|
332
|
+
});
|
|
235
333
|
var toolSessionItemSchema = baseItemSchema.extend({
|
|
236
334
|
type: z.literal("toolSession"),
|
|
237
335
|
projectId: z.string().min(1),
|
|
@@ -275,7 +373,11 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
|
275
373
|
syncConflictItemSchema,
|
|
276
374
|
workItemSchema,
|
|
277
375
|
runnerHeartbeatItemSchema,
|
|
376
|
+
runnerSettingsItemSchema,
|
|
278
377
|
runnerExecutionLogItemSchema,
|
|
378
|
+
runnerCredentialItemSchema,
|
|
379
|
+
runnerCommandItemSchema,
|
|
380
|
+
planReviewMessageItemSchema,
|
|
279
381
|
toolSessionItemSchema
|
|
280
382
|
]);
|
|
281
383
|
|
|
@@ -461,6 +563,211 @@ function decideSyncState(state) {
|
|
|
461
563
|
return "clean";
|
|
462
564
|
}
|
|
463
565
|
|
|
566
|
+
// ../shared/src/next-action.ts
|
|
567
|
+
var nextActionRunnerHeartbeatFreshMs = 15 * 60 * 1e3;
|
|
568
|
+
function computeProjectNextAction(input) {
|
|
569
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
570
|
+
if (!input.projectId) {
|
|
571
|
+
return {
|
|
572
|
+
kind: "noProject",
|
|
573
|
+
actor: "user",
|
|
574
|
+
tone: "neutral",
|
|
575
|
+
title: "Create a project",
|
|
576
|
+
message: "Create a project before linking a repository or starting runner work."
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const activeRepositoryLinks = input.repositoryLinks.filter((link) => link.status !== "revoked");
|
|
580
|
+
const repositoryLink = activeRepositoryLinks[0];
|
|
581
|
+
if (!repositoryLink) {
|
|
582
|
+
return {
|
|
583
|
+
kind: "linkRepository",
|
|
584
|
+
actor: "user",
|
|
585
|
+
tone: "warning",
|
|
586
|
+
title: "Link a repository",
|
|
587
|
+
message: "Add the project repository so Amistio can pair a local runner."
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const pairedRepositoryLinks = activeRepositoryLinks.filter((link) => Boolean(link.repoFingerprint));
|
|
591
|
+
if (!pairedRepositoryLinks.length) {
|
|
592
|
+
return {
|
|
593
|
+
kind: "pairRepository",
|
|
594
|
+
actor: "user",
|
|
595
|
+
tone: "warning",
|
|
596
|
+
title: "Pair the repository",
|
|
597
|
+
message: `Run the pairing command from ${repositoryLink.repoName} before queueing runner work.`,
|
|
598
|
+
repositoryLinkId: repositoryLink.repositoryLinkId,
|
|
599
|
+
updatedAt: repositoryLink.updatedAt
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const runningWork = latestWorkItem(input.workItems.filter((item) => item.status === "running"));
|
|
603
|
+
if (runningWork) {
|
|
604
|
+
if (runningWork.workKind === "brainGeneration") {
|
|
605
|
+
return workAction(runningWork, "brainGenerationRunning", "runner", "warning", "Generating project brain", "The local runner is generating draft brain artifacts for review.");
|
|
606
|
+
}
|
|
607
|
+
if (runningWork.workKind === "planRevision") {
|
|
608
|
+
return workAction(runningWork, "planRevisionRunning", "runner", "warning", "Revising the plan", "The local runner is revising the generated plan from the conversation.");
|
|
609
|
+
}
|
|
610
|
+
return workAction(runningWork, "runnerRunningWork", "runner", "warning", "Runner is working", "The local runner has claimed approved implementation work.");
|
|
611
|
+
}
|
|
612
|
+
const generatedReview = generatedReviewState(input.documents);
|
|
613
|
+
if (generatedReview.unapproved.length > 0) {
|
|
614
|
+
const partial = generatedReview.approved.length > 0;
|
|
615
|
+
const title = partial ? "Finish generated review" : "Review generated artifacts";
|
|
616
|
+
const message = partial ? `${generatedReview.unapproved.length} generated artifact${generatedReview.unapproved.length === 1 ? "" : "s"} still need approval before implementation work can queue.` : `${generatedReview.unapproved.length} generated artifact${generatedReview.unapproved.length === 1 ? "" : "s"} need approval in the web app before the runner can implement them.`;
|
|
617
|
+
return {
|
|
618
|
+
kind: partial ? "generatedArtifactsPartiallyApproved" : "generatedArtifactsReview",
|
|
619
|
+
actor: "user",
|
|
620
|
+
tone: "warning",
|
|
621
|
+
title,
|
|
622
|
+
message,
|
|
623
|
+
count: generatedReview.unapproved.length,
|
|
624
|
+
...generatedReview.unapproved[0]?.documentId ? { documentId: generatedReview.unapproved[0].documentId } : {},
|
|
625
|
+
...latestTimestamp(generatedReview.unapproved.map((document) => document.updatedAt)) ? { updatedAt: latestTimestamp(generatedReview.unapproved.map((document) => document.updatedAt)) } : {}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const failedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "failed"));
|
|
629
|
+
if (failedBrainGeneration) {
|
|
630
|
+
return workAction(failedBrainGeneration, "brainGenerationFailed", "user", "danger", "Brain generation failed", failedBrainGeneration.lastStatusMessage ?? "Retry generation after checking the runner output.");
|
|
631
|
+
}
|
|
632
|
+
const failedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "failed"));
|
|
633
|
+
if (failedPlanRevision) {
|
|
634
|
+
return workAction(failedPlanRevision, "planRevisionFailed", "user", "danger", "Plan revision failed", failedPlanRevision.lastStatusMessage ?? "Review the conversation and request another revision if needed.");
|
|
635
|
+
}
|
|
636
|
+
const blockedWork = latestWorkItem(input.workItems.filter((item) => item.status === "blocked" || item.status === "changesRequested"));
|
|
637
|
+
if (blockedWork) {
|
|
638
|
+
return workAction(blockedWork, "workBlocked", "user", "danger", "Work is blocked", blockedWork.lastStatusMessage ?? "Review the blocked work item before the runner can continue.");
|
|
639
|
+
}
|
|
640
|
+
const failedWork = latestWorkItem(input.workItems.filter((item) => item.status === "failed"));
|
|
641
|
+
if (failedWork) {
|
|
642
|
+
return workAction(failedWork, "workFailed", "user", "danger", "Work failed", failedWork.lastStatusMessage ?? "Review the failure and retry or update the plan.");
|
|
643
|
+
}
|
|
644
|
+
const queuedBrainGeneration = latestWorkItem(input.workItems.filter((item) => item.workKind === "brainGeneration" && item.status === "approved"));
|
|
645
|
+
const queuedPlanRevision = latestWorkItem(input.workItems.filter((item) => item.workKind === "planRevision" && item.status === "approved"));
|
|
646
|
+
const queuedImplementation = latestWorkItem(input.workItems.filter((item) => item.workKind !== "brainGeneration" && item.workKind !== "planRevision" && item.status === "approved"));
|
|
647
|
+
const queuedWork = queuedBrainGeneration ?? queuedPlanRevision ?? queuedImplementation;
|
|
648
|
+
const readiness = getSharedRunnerReadiness(pairedRepositoryLinks, input.runnerHeartbeats, nowMs);
|
|
649
|
+
if (queuedWork && !readiness.ready) {
|
|
650
|
+
const title = queuedWork.workKind === "brainGeneration" ? "Brain generation is queued" : queuedWork.workKind === "planRevision" ? "Plan revision is queued" : "Implementation is queued";
|
|
651
|
+
return runnerWaitAction(readiness, title);
|
|
652
|
+
}
|
|
653
|
+
if (queuedBrainGeneration) {
|
|
654
|
+
return workAction(queuedBrainGeneration, "brainGenerationQueued", "runner", "warning", "Brain generation queued", "The local runner can claim this queued brain-generation work.");
|
|
655
|
+
}
|
|
656
|
+
if (queuedPlanRevision) {
|
|
657
|
+
return workAction(queuedPlanRevision, "planRevisionQueued", "runner", "warning", "Plan revision queued", "The local runner can claim the requested plan revision.");
|
|
658
|
+
}
|
|
659
|
+
if (queuedImplementation) {
|
|
660
|
+
return workAction(queuedImplementation, "implementationQueued", "runner", "warning", "Implementation queued", "The local runner can claim approved implementation work.");
|
|
661
|
+
}
|
|
662
|
+
if (!readiness.ready) {
|
|
663
|
+
return runnerWaitAction(readiness, "Runner setup needed");
|
|
664
|
+
}
|
|
665
|
+
const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed"));
|
|
666
|
+
if (completedWork) {
|
|
667
|
+
return workAction(completedWork, "workCompleted", "none", "success", "Work completed", completedWork.lastStatusMessage ?? "The latest runner work is complete.");
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
kind: "idle",
|
|
671
|
+
actor: "user",
|
|
672
|
+
tone: "success",
|
|
673
|
+
title: "Ready for the next task",
|
|
674
|
+
message: "The repository and runner are ready for a new project-brain request.",
|
|
675
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
676
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
677
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function formatProjectNextAction(action) {
|
|
681
|
+
return `${action.title}: ${action.message}`;
|
|
682
|
+
}
|
|
683
|
+
function runnerWaitAction(readiness, fallbackTitle) {
|
|
684
|
+
const repositoryName = readiness.repositoryLink?.repoName ?? "the paired repository";
|
|
685
|
+
if (readiness.reason === "runnerOffline") {
|
|
686
|
+
return {
|
|
687
|
+
kind: "runnerOffline",
|
|
688
|
+
actor: "user",
|
|
689
|
+
tone: "danger",
|
|
690
|
+
title: "Restart the runner",
|
|
691
|
+
message: `The runner for ${repositoryName} is offline. Start amistio run --watch from the paired checkout.`,
|
|
692
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
693
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
694
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (readiness.reason === "runnerStale") {
|
|
698
|
+
return {
|
|
699
|
+
kind: "runnerStale",
|
|
700
|
+
actor: "user",
|
|
701
|
+
tone: "warning",
|
|
702
|
+
title: "Refresh the runner",
|
|
703
|
+
message: `The runner for ${repositoryName} has not checked in recently. Restart amistio run --watch from the paired checkout.`,
|
|
704
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {},
|
|
705
|
+
...readiness.heartbeat?.runnerId ? { runnerId: readiness.heartbeat.runnerId } : {},
|
|
706
|
+
...readiness.heartbeat?.lastSeenAt ? { updatedAt: readiness.heartbeat.lastSeenAt } : {}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
kind: "runnerMissing",
|
|
711
|
+
actor: "user",
|
|
712
|
+
tone: "warning",
|
|
713
|
+
title: fallbackTitle,
|
|
714
|
+
message: `Start amistio run --watch from ${repositoryName} so the local runner can claim work.`,
|
|
715
|
+
...readiness.repositoryLink?.repositoryLinkId ? { repositoryLinkId: readiness.repositoryLink.repositoryLinkId } : {}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function getSharedRunnerReadiness(repositoryLinks, runnerHeartbeats, nowMs) {
|
|
719
|
+
const repositoryLinkIds = new Set(repositoryLinks.map((link) => link.repositoryLinkId));
|
|
720
|
+
const latestHeartbeat = runnerHeartbeats.filter((heartbeat) => repositoryLinkIds.has(heartbeat.repositoryLinkId)).sort((first, second) => heartbeatTime(second) - heartbeatTime(first))[0];
|
|
721
|
+
const repositoryLink = latestHeartbeat ? repositoryLinks.find((link) => link.repositoryLinkId === latestHeartbeat.repositoryLinkId) ?? repositoryLinks[0] : repositoryLinks[0];
|
|
722
|
+
if (!latestHeartbeat) {
|
|
723
|
+
return { ready: false, reason: "runnerMissing", ...repositoryLink ? { repositoryLink } : {} };
|
|
724
|
+
}
|
|
725
|
+
if (latestHeartbeat.status === "offline") {
|
|
726
|
+
return { ready: false, reason: "runnerOffline", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
727
|
+
}
|
|
728
|
+
if (!isFreshHeartbeat(latestHeartbeat, nowMs)) {
|
|
729
|
+
return { ready: false, reason: "runnerStale", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
730
|
+
}
|
|
731
|
+
return { ready: true, reason: "ready", ...repositoryLink ? { repositoryLink } : {}, heartbeat: latestHeartbeat };
|
|
732
|
+
}
|
|
733
|
+
function generatedReviewState(documents) {
|
|
734
|
+
const generated = documents.filter(isGeneratedDocument);
|
|
735
|
+
return {
|
|
736
|
+
approved: generated.filter((document) => document.status === "approved"),
|
|
737
|
+
unapproved: generated.filter((document) => document.status !== "approved" && document.status !== "rejected" && document.syncState !== "archived")
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function isGeneratedDocument(document) {
|
|
741
|
+
const generatedDraftId = document.frontmatter.generatedDraftId;
|
|
742
|
+
return typeof generatedDraftId === "string" && generatedDraftId.length > 0;
|
|
743
|
+
}
|
|
744
|
+
function workAction(workItem, kind, actor, tone, title, message) {
|
|
745
|
+
return {
|
|
746
|
+
kind,
|
|
747
|
+
actor,
|
|
748
|
+
tone,
|
|
749
|
+
title,
|
|
750
|
+
message,
|
|
751
|
+
workItemId: workItem.workItemId,
|
|
752
|
+
...workItem.claimedByRunnerId ? { runnerId: workItem.claimedByRunnerId } : {},
|
|
753
|
+
updatedAt: workItem.lastStatusAt
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function latestWorkItem(workItems) {
|
|
757
|
+
return [...workItems].sort((first, second) => Date.parse(second.updatedAt) - Date.parse(first.updatedAt))[0];
|
|
758
|
+
}
|
|
759
|
+
function latestTimestamp(values) {
|
|
760
|
+
return values.sort((first, second) => Date.parse(second) - Date.parse(first))[0];
|
|
761
|
+
}
|
|
762
|
+
function heartbeatTime(heartbeat) {
|
|
763
|
+
const timestamp = Date.parse(heartbeat.lastSeenAt);
|
|
764
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
765
|
+
}
|
|
766
|
+
function isFreshHeartbeat(heartbeat, nowMs) {
|
|
767
|
+
const lastSeenMs = heartbeatTime(heartbeat);
|
|
768
|
+
return lastSeenMs > 0 && nowMs - lastSeenMs <= nextActionRunnerHeartbeatFreshMs;
|
|
769
|
+
}
|
|
770
|
+
|
|
464
771
|
// src/bootstrap.ts
|
|
465
772
|
import { execFile } from "node:child_process";
|
|
466
773
|
import { mkdir, readdir, stat } from "node:fs/promises";
|
|
@@ -544,6 +851,16 @@ var LocalCredentialStore = class {
|
|
|
544
851
|
const data = await this.read();
|
|
545
852
|
return data.credentials[key];
|
|
546
853
|
}
|
|
854
|
+
async delete(key) {
|
|
855
|
+
const data = await this.read();
|
|
856
|
+
if (!(key in data.credentials)) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
delete data.credentials[key];
|
|
860
|
+
await mkdir2(path2.dirname(this.filePath), { recursive: true });
|
|
861
|
+
await writeFile(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
|
|
862
|
+
await chmod(this.filePath, 384);
|
|
863
|
+
}
|
|
547
864
|
async read() {
|
|
548
865
|
try {
|
|
549
866
|
return JSON.parse(await readFile(this.filePath, "utf8"));
|
|
@@ -699,6 +1016,14 @@ var ApiClient = class {
|
|
|
699
1016
|
{ method: "GET" }
|
|
700
1017
|
);
|
|
701
1018
|
}
|
|
1019
|
+
async listPlanReviewMessages(projectId, documentId) {
|
|
1020
|
+
const suffix = documentId ? `?documentId=${encodeURIComponent(documentId)}` : "";
|
|
1021
|
+
return this.request(
|
|
1022
|
+
`/projects/${projectId}/plan-review-messages${suffix}`,
|
|
1023
|
+
z3.object({ messages: z3.array(planReviewMessageItemSchema) }),
|
|
1024
|
+
{ method: "GET" }
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
702
1027
|
async pushBrainDocuments(projectId, documents) {
|
|
703
1028
|
return this.request(
|
|
704
1029
|
`/projects/${projectId}/brain-documents`,
|
|
@@ -709,13 +1034,58 @@ var ApiClient = class {
|
|
|
709
1034
|
}
|
|
710
1035
|
);
|
|
711
1036
|
}
|
|
712
|
-
async
|
|
1037
|
+
async listRunners(projectId) {
|
|
1038
|
+
return this.request(
|
|
1039
|
+
`/projects/${projectId}/runners`,
|
|
1040
|
+
z3.object({ runners: z3.array(runnerHeartbeatItemSchema) }),
|
|
1041
|
+
{ method: "GET" }
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
async listRunnerCommands(projectId, runnerId, repositoryLinkId) {
|
|
1045
|
+
return this.request(
|
|
1046
|
+
`/projects/${projectId}/runner-commands?runnerId=${encodeURIComponent(runnerId)}&repositoryLinkId=${encodeURIComponent(repositoryLinkId)}`,
|
|
1047
|
+
z3.object({ commands: z3.array(runnerCommandItemSchema) }),
|
|
1048
|
+
{ method: "GET" }
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
async updateRunnerCommand(projectId, commandId, input) {
|
|
1052
|
+
return this.request(
|
|
1053
|
+
`/projects/${projectId}/runner-commands/${commandId}`,
|
|
1054
|
+
z3.object({ command: runnerCommandItemSchema }),
|
|
1055
|
+
{
|
|
1056
|
+
method: "PATCH",
|
|
1057
|
+
body: JSON.stringify(input)
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
async getRunnerPreferences(projectId) {
|
|
1062
|
+
const response = await this.request(
|
|
1063
|
+
`/projects/${projectId}/runner-preferences`,
|
|
1064
|
+
z3.object({
|
|
1065
|
+
account: runnerSettingsItemSchema.optional(),
|
|
1066
|
+
project: runnerSettingsItemSchema.optional(),
|
|
1067
|
+
effective: z3.object({
|
|
1068
|
+
tool: runnerToolSelectionSchema,
|
|
1069
|
+
model: z3.string().optional(),
|
|
1070
|
+
source: runnerPreferenceSourceSchema
|
|
1071
|
+
})
|
|
1072
|
+
}),
|
|
1073
|
+
{ method: "GET" }
|
|
1074
|
+
);
|
|
1075
|
+
return {
|
|
1076
|
+
...response.account ? { account: response.account } : {},
|
|
1077
|
+
...response.project ? { project: response.project } : {},
|
|
1078
|
+
effective: response.effective
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
async sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, status, metadata) {
|
|
1082
|
+
const heartbeatMetadata = typeof metadata === "string" ? { version: metadata } : metadata ?? {};
|
|
713
1083
|
return this.request(
|
|
714
1084
|
`/projects/${projectId}/runners`,
|
|
715
1085
|
z3.object({ runner: runnerHeartbeatItemSchema }),
|
|
716
1086
|
{
|
|
717
1087
|
method: "POST",
|
|
718
|
-
body: JSON.stringify({ runnerId, repositoryLinkId, status, ...
|
|
1088
|
+
body: JSON.stringify({ runnerId, repositoryLinkId, status, ...heartbeatMetadata, lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
719
1089
|
}
|
|
720
1090
|
);
|
|
721
1091
|
}
|
|
@@ -808,8 +1178,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
808
1178
|
});
|
|
809
1179
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
810
1180
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
811
|
-
const
|
|
812
|
-
return new URL(`${base}${
|
|
1181
|
+
const path10 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1182
|
+
return new URL(`${base}${path10}`);
|
|
813
1183
|
}
|
|
814
1184
|
|
|
815
1185
|
// src/orchestrator.ts
|
|
@@ -878,7 +1248,7 @@ import { spawn } from "node:child_process";
|
|
|
878
1248
|
import { mkdtemp, rm, writeFile as writeFile4 } from "node:fs/promises";
|
|
879
1249
|
import os2 from "node:os";
|
|
880
1250
|
import path5 from "node:path";
|
|
881
|
-
var localToolNames =
|
|
1251
|
+
var localToolNames = runnerToolNames;
|
|
882
1252
|
var localToolAdapters = [
|
|
883
1253
|
{
|
|
884
1254
|
name: "opencode",
|
|
@@ -931,6 +1301,7 @@ var localToolAdapters = [
|
|
|
931
1301
|
description: "GitHub Copilot SDK adapter using @github/copilot-sdk.",
|
|
932
1302
|
sdkPackageName: "@github/copilot-sdk",
|
|
933
1303
|
sdkDisplayCommand: "@github/copilot-sdk CopilotClient.createSession().sendAndWait()",
|
|
1304
|
+
supportsModelSelection: true,
|
|
934
1305
|
supportsSessionReuse: false,
|
|
935
1306
|
resumabilityScope: "none",
|
|
936
1307
|
runWithSdk: runCopilotSdk
|
|
@@ -988,7 +1359,8 @@ async function detectLocalTools() {
|
|
|
988
1359
|
commandAvailable,
|
|
989
1360
|
execution: sdkAvailable ? "sdk" : commandAvailable ? "command" : "unavailable",
|
|
990
1361
|
supportsSessionReuse: Boolean(adapter.supportsSessionReuse),
|
|
991
|
-
resumabilityScope: adapter.resumabilityScope ?? "none"
|
|
1362
|
+
resumabilityScope: adapter.resumabilityScope ?? "none",
|
|
1363
|
+
supportsModelSelection: Boolean(adapter.supportsModelSelection)
|
|
992
1364
|
};
|
|
993
1365
|
})
|
|
994
1366
|
);
|
|
@@ -1002,13 +1374,14 @@ async function runLocalTool(options) {
|
|
|
1002
1374
|
rootDir: options.rootDir,
|
|
1003
1375
|
prompt: options.prompt,
|
|
1004
1376
|
promptFilePath,
|
|
1005
|
-
tool: options.tool ?? "auto"
|
|
1377
|
+
tool: options.tool ?? "auto",
|
|
1378
|
+
...options.model ? { model: options.model } : {}
|
|
1006
1379
|
};
|
|
1007
1380
|
if (options.toolCommand) {
|
|
1008
1381
|
runnerOptions.toolCommand = options.toolCommand;
|
|
1009
1382
|
}
|
|
1010
|
-
const
|
|
1011
|
-
const result = await executeToolRunner(
|
|
1383
|
+
const runner2 = await createToolRunner(runnerOptions);
|
|
1384
|
+
const result = await executeToolRunner(runner2, {
|
|
1012
1385
|
rootDir: options.rootDir,
|
|
1013
1386
|
prompt: options.prompt,
|
|
1014
1387
|
promptFilePath,
|
|
@@ -1016,10 +1389,11 @@ async function runLocalTool(options) {
|
|
|
1016
1389
|
...options.session ? { session: options.session } : {}
|
|
1017
1390
|
});
|
|
1018
1391
|
return {
|
|
1019
|
-
toolName:
|
|
1020
|
-
displayCommand:
|
|
1021
|
-
supportsSessionReuse:
|
|
1022
|
-
resumabilityScope:
|
|
1392
|
+
toolName: runner2.toolName,
|
|
1393
|
+
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1394
|
+
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1395
|
+
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1396
|
+
...options.model ? { model: options.model } : {},
|
|
1023
1397
|
...result
|
|
1024
1398
|
};
|
|
1025
1399
|
} finally {
|
|
@@ -1032,17 +1406,19 @@ async function createToolRunPreview(options) {
|
|
|
1032
1406
|
rootDir: options.rootDir,
|
|
1033
1407
|
prompt: options.prompt,
|
|
1034
1408
|
promptFilePath,
|
|
1035
|
-
tool: options.tool ?? "auto"
|
|
1409
|
+
tool: options.tool ?? "auto",
|
|
1410
|
+
...options.model ? { model: options.model } : {}
|
|
1036
1411
|
};
|
|
1037
1412
|
if (options.toolCommand) {
|
|
1038
1413
|
runnerOptions.toolCommand = options.toolCommand;
|
|
1039
1414
|
}
|
|
1040
|
-
const
|
|
1415
|
+
const runner2 = await createToolRunner(runnerOptions);
|
|
1041
1416
|
return {
|
|
1042
|
-
toolName:
|
|
1043
|
-
displayCommand:
|
|
1044
|
-
supportsSessionReuse:
|
|
1045
|
-
resumabilityScope:
|
|
1417
|
+
toolName: runner2.toolName,
|
|
1418
|
+
displayCommand: runner2.kind === "sdk" ? runner2.displayCommand : runner2.invocation.displayCommand,
|
|
1419
|
+
supportsSessionReuse: runner2.kind === "sdk" ? Boolean(runner2.adapter.supportsSessionReuse) : false,
|
|
1420
|
+
resumabilityScope: runner2.kind === "sdk" ? runner2.adapter.resumabilityScope ?? "none" : "none",
|
|
1421
|
+
...options.model ? { model: options.model } : {}
|
|
1046
1422
|
};
|
|
1047
1423
|
}
|
|
1048
1424
|
function createCustomToolInvocation(commandTemplate, input) {
|
|
@@ -1067,7 +1443,10 @@ async function createToolRunner(options) {
|
|
|
1067
1443
|
if (tool === "none") {
|
|
1068
1444
|
throw new Error("No local tool selected. Use --tool auto, a supported tool name, or --tool-command.");
|
|
1069
1445
|
}
|
|
1070
|
-
const adapter = tool === "auto" ? await selectFirstAvailableAdapter() : await selectRequestedAdapter(tool);
|
|
1446
|
+
const adapter = tool === "auto" ? await selectFirstAvailableAdapter(Boolean(options.model)) : await selectRequestedAdapter(tool);
|
|
1447
|
+
if (options.model && !adapter.supportsModelSelection) {
|
|
1448
|
+
throw new Error(`Model selection is not supported by ${adapter.name}. Remove --model or choose a model-aware adapter.`);
|
|
1449
|
+
}
|
|
1071
1450
|
if (adapter.runWithSdk && await isSdkAvailable(adapter)) {
|
|
1072
1451
|
return {
|
|
1073
1452
|
toolName: adapter.name,
|
|
@@ -1085,17 +1464,17 @@ async function createToolRunner(options) {
|
|
|
1085
1464
|
}
|
|
1086
1465
|
throw new Error(`The ${adapter.name} SDK or executable was not found. Install the SDK/runtime or pass --tool-command.`);
|
|
1087
1466
|
}
|
|
1088
|
-
async function executeToolRunner(
|
|
1089
|
-
if (
|
|
1090
|
-
return executeToolInvocation(
|
|
1467
|
+
async function executeToolRunner(runner2, input) {
|
|
1468
|
+
if (runner2.kind === "command") {
|
|
1469
|
+
return executeToolInvocation(runner2.invocation, input.rootDir, input.streamOutput);
|
|
1091
1470
|
}
|
|
1092
1471
|
try {
|
|
1093
|
-
return await
|
|
1472
|
+
return await runner2.adapter.runWithSdk(input);
|
|
1094
1473
|
} catch (error) {
|
|
1095
|
-
if (
|
|
1096
|
-
const fallback =
|
|
1474
|
+
if (runner2.adapter.buildInvocation && runner2.adapter.executable && await commandExists(runner2.adapter.executable)) {
|
|
1475
|
+
const fallback = runner2.adapter.buildInvocation(input);
|
|
1097
1476
|
const result = await executeToolInvocation(fallback, input.rootDir, input.streamOutput);
|
|
1098
|
-
const sdkFailure = `SDK execution for ${
|
|
1477
|
+
const sdkFailure = `SDK execution for ${runner2.adapter.name} failed, fell back to ${fallback.displayCommand}: ${errorMessage(error)}`;
|
|
1099
1478
|
return {
|
|
1100
1479
|
...result,
|
|
1101
1480
|
stderr: result.stderr ? `${sdkFailure}
|
|
@@ -1111,8 +1490,11 @@ function normalizeToolRequest(value) {
|
|
|
1111
1490
|
}
|
|
1112
1491
|
throw new Error(`Unsupported local tool: ${value}. Supported tools: auto, none, ${localToolNames.join(", ")}.`);
|
|
1113
1492
|
}
|
|
1114
|
-
async function selectFirstAvailableAdapter() {
|
|
1493
|
+
async function selectFirstAvailableAdapter(requiresModelSelection = false) {
|
|
1115
1494
|
for (const adapter of localToolAdapters) {
|
|
1495
|
+
if (requiresModelSelection && !adapter.supportsModelSelection) {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1116
1498
|
const sdkAvailable = await isSdkAvailable(adapter);
|
|
1117
1499
|
const commandAvailable = adapter.executable ? await commandExists(adapter.executable) : false;
|
|
1118
1500
|
if (sdkAvailable || commandAvailable) {
|
|
@@ -1120,7 +1502,7 @@ async function selectFirstAvailableAdapter() {
|
|
|
1120
1502
|
}
|
|
1121
1503
|
}
|
|
1122
1504
|
throw new Error(
|
|
1123
|
-
`No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
|
|
1505
|
+
requiresModelSelection ? "No installed local AI tool supports model selection. Remove --model or choose a model-aware adapter." : `No supported local AI tool was found. Install one of ${localToolNames.join(", ")} or pass --tool-command "your-tool --prompt-file {promptFile}".`
|
|
1124
1506
|
);
|
|
1125
1507
|
}
|
|
1126
1508
|
async function selectRequestedAdapter(tool) {
|
|
@@ -1292,7 +1674,7 @@ async function runCopilotSdk(input) {
|
|
|
1292
1674
|
await client.start();
|
|
1293
1675
|
const session = await client.createSession({
|
|
1294
1676
|
clientName: "amistio-cli",
|
|
1295
|
-
model: process.env.AMISTIO_COPILOT_MODEL ?? "gpt-5",
|
|
1677
|
+
model: input.model ?? process.env.AMISTIO_COPILOT_MODEL ?? "gpt-5",
|
|
1296
1678
|
workingDirectory: input.rootDir,
|
|
1297
1679
|
enableConfigDiscovery: true,
|
|
1298
1680
|
streaming: input.streamOutput,
|
|
@@ -1334,6 +1716,187 @@ function shellQuote(value) {
|
|
|
1334
1716
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1335
1717
|
}
|
|
1336
1718
|
|
|
1719
|
+
// src/runner-daemon.ts
|
|
1720
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1721
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1722
|
+
import { openSync } from "node:fs";
|
|
1723
|
+
import { mkdir as mkdir5, readdir as readdir2, readFile as readFile3, writeFile as writeFile5 } from "node:fs/promises";
|
|
1724
|
+
import os3 from "node:os";
|
|
1725
|
+
import path6 from "node:path";
|
|
1726
|
+
function currentRunnerMode() {
|
|
1727
|
+
return process.env.AMISTIO_RUNNER_MODE === "background" ? "background" : "foreground";
|
|
1728
|
+
}
|
|
1729
|
+
function defaultRunnerMetadataDir() {
|
|
1730
|
+
return path6.join(os3.homedir(), ".config", "amistio", "runners");
|
|
1731
|
+
}
|
|
1732
|
+
async function startRunnerDaemon(input) {
|
|
1733
|
+
const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
|
|
1734
|
+
const existing = await readRunnerDaemonMetadata(input, metadataDir);
|
|
1735
|
+
if (existing?.status === "running" && isProcessRunning(existing.pid)) {
|
|
1736
|
+
throw new Error(`Background runner ${existing.runnerId} is already running with PID ${existing.pid}.`);
|
|
1737
|
+
}
|
|
1738
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1739
|
+
const logPath = path6.join(metadataDir, `${runnerDaemonKey(input)}.log`);
|
|
1740
|
+
const logFd = openSync(logPath, "a");
|
|
1741
|
+
const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...input.args], {
|
|
1742
|
+
cwd: input.rootDir,
|
|
1743
|
+
detached: true,
|
|
1744
|
+
env: {
|
|
1745
|
+
...process.env,
|
|
1746
|
+
AMISTIO_RUNNER_MODE: "background"
|
|
1747
|
+
},
|
|
1748
|
+
stdio: ["ignore", logFd, logFd]
|
|
1749
|
+
});
|
|
1750
|
+
if (!child.pid) {
|
|
1751
|
+
throw new Error("Failed to start background runner process.");
|
|
1752
|
+
}
|
|
1753
|
+
child.unref();
|
|
1754
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1755
|
+
const metadata = {
|
|
1756
|
+
schemaVersion: 1,
|
|
1757
|
+
accountId: input.accountId,
|
|
1758
|
+
projectId: input.projectId,
|
|
1759
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
1760
|
+
runnerId: input.runnerId,
|
|
1761
|
+
rootDir: path6.resolve(input.rootDir),
|
|
1762
|
+
apiUrl: input.apiUrl,
|
|
1763
|
+
pid: child.pid,
|
|
1764
|
+
status: "running",
|
|
1765
|
+
startedAt: now,
|
|
1766
|
+
updatedAt: now,
|
|
1767
|
+
hostname: os3.hostname(),
|
|
1768
|
+
logPath
|
|
1769
|
+
};
|
|
1770
|
+
await writeRunnerDaemonMetadata(metadata, metadataDir);
|
|
1771
|
+
return metadata;
|
|
1772
|
+
}
|
|
1773
|
+
async function restartRunnerDaemonProcess(metadata, args, input = {}) {
|
|
1774
|
+
const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
|
|
1775
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1776
|
+
const logPath = metadata.logPath ?? path6.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
|
|
1777
|
+
const logFd = openSync(logPath, "a");
|
|
1778
|
+
const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...args], {
|
|
1779
|
+
cwd: metadata.rootDir,
|
|
1780
|
+
detached: true,
|
|
1781
|
+
env: {
|
|
1782
|
+
...process.env,
|
|
1783
|
+
AMISTIO_RUNNER_MODE: "background"
|
|
1784
|
+
},
|
|
1785
|
+
stdio: ["ignore", logFd, logFd]
|
|
1786
|
+
});
|
|
1787
|
+
if (!child.pid) {
|
|
1788
|
+
throw new Error("Failed to start replacement background runner process.");
|
|
1789
|
+
}
|
|
1790
|
+
child.unref();
|
|
1791
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1792
|
+
const replacement = {
|
|
1793
|
+
...metadata,
|
|
1794
|
+
pid: child.pid,
|
|
1795
|
+
status: "running",
|
|
1796
|
+
startedAt: now,
|
|
1797
|
+
updatedAt: now,
|
|
1798
|
+
hostname: os3.hostname(),
|
|
1799
|
+
logPath
|
|
1800
|
+
};
|
|
1801
|
+
await writeRunnerDaemonMetadata(replacement, metadataDir);
|
|
1802
|
+
return replacement;
|
|
1803
|
+
}
|
|
1804
|
+
async function listRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
1805
|
+
let entries;
|
|
1806
|
+
try {
|
|
1807
|
+
entries = await readdir2(metadataDir);
|
|
1808
|
+
} catch {
|
|
1809
|
+
return [];
|
|
1810
|
+
}
|
|
1811
|
+
const records = await Promise.all(
|
|
1812
|
+
entries.filter((entry) => entry.endsWith(".json")).map(async (entry) => readRunnerDaemonMetadataFile(path6.join(metadataDir, entry)))
|
|
1813
|
+
);
|
|
1814
|
+
return records.filter((record) => Boolean(record)).filter((record) => record.accountId === input.accountId && record.projectId === input.projectId && record.repositoryLinkId === input.repositoryLinkId).filter((record) => !input.runnerId || record.runnerId === input.runnerId).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1815
|
+
}
|
|
1816
|
+
async function readRunnerDaemonMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
1817
|
+
return readRunnerDaemonMetadataFile(runnerDaemonMetadataPath(input, metadataDir));
|
|
1818
|
+
}
|
|
1819
|
+
async function writeRunnerDaemonMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
1820
|
+
await mkdir5(metadataDir, { recursive: true });
|
|
1821
|
+
await writeFile5(runnerDaemonMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
1822
|
+
}
|
|
1823
|
+
async function markRunnerDaemonStopped(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
1824
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1825
|
+
const stopped = {
|
|
1826
|
+
...metadata,
|
|
1827
|
+
status: "stopped",
|
|
1828
|
+
stoppedAt: now,
|
|
1829
|
+
updatedAt: now
|
|
1830
|
+
};
|
|
1831
|
+
await writeRunnerDaemonMetadata(stopped, metadataDir);
|
|
1832
|
+
return stopped;
|
|
1833
|
+
}
|
|
1834
|
+
async function stopRunnerDaemonProcess(metadata) {
|
|
1835
|
+
if (!isProcessRunning(metadata.pid)) {
|
|
1836
|
+
return "not-running";
|
|
1837
|
+
}
|
|
1838
|
+
process.kill(metadata.pid, "SIGTERM");
|
|
1839
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
1840
|
+
if (!isProcessRunning(metadata.pid)) {
|
|
1841
|
+
return "stopped";
|
|
1842
|
+
}
|
|
1843
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1844
|
+
}
|
|
1845
|
+
return isProcessRunning(metadata.pid) ? "not-running" : "stopped";
|
|
1846
|
+
}
|
|
1847
|
+
function isProcessRunning(pid) {
|
|
1848
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
try {
|
|
1852
|
+
process.kill(pid, 0);
|
|
1853
|
+
return true;
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
|
|
1856
|
+
return code === "EPERM";
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function runnerDaemonRuntimeStatus(metadata) {
|
|
1860
|
+
if (metadata.status === "stopped") {
|
|
1861
|
+
return "stopped";
|
|
1862
|
+
}
|
|
1863
|
+
return isProcessRunning(metadata.pid) ? "running" : "stale";
|
|
1864
|
+
}
|
|
1865
|
+
function runnerDaemonUptime(metadata, now = Date.now()) {
|
|
1866
|
+
const startedAt = Date.parse(metadata.startedAt);
|
|
1867
|
+
if (!Number.isFinite(startedAt)) {
|
|
1868
|
+
return "unknown";
|
|
1869
|
+
}
|
|
1870
|
+
const totalSeconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
1871
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1872
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
1873
|
+
const seconds = totalSeconds % 60;
|
|
1874
|
+
if (hours > 0) {
|
|
1875
|
+
return `${hours}h ${minutes}m`;
|
|
1876
|
+
}
|
|
1877
|
+
if (minutes > 0) {
|
|
1878
|
+
return `${minutes}m ${seconds}s`;
|
|
1879
|
+
}
|
|
1880
|
+
return `${seconds}s`;
|
|
1881
|
+
}
|
|
1882
|
+
function runnerDaemonMetadataPath(input, metadataDir) {
|
|
1883
|
+
return path6.join(metadataDir, `${runnerDaemonKey(input)}.json`);
|
|
1884
|
+
}
|
|
1885
|
+
function runnerDaemonKey(input) {
|
|
1886
|
+
return createHash2("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
1887
|
+
}
|
|
1888
|
+
async function readRunnerDaemonMetadataFile(filePath) {
|
|
1889
|
+
try {
|
|
1890
|
+
const parsed = JSON.parse(await readFile3(filePath, "utf8"));
|
|
1891
|
+
if (parsed.schemaVersion !== 1 || !parsed.runnerId || !parsed.projectId || !parsed.repositoryLinkId) {
|
|
1892
|
+
return void 0;
|
|
1893
|
+
}
|
|
1894
|
+
return parsed;
|
|
1895
|
+
} catch {
|
|
1896
|
+
return void 0;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1337
1900
|
// src/session-policy.ts
|
|
1338
1901
|
var maxIdleMs = 24 * 60 * 60 * 1e3;
|
|
1339
1902
|
var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -1455,8 +2018,8 @@ function tokens(value) {
|
|
|
1455
2018
|
}
|
|
1456
2019
|
|
|
1457
2020
|
// src/sync.ts
|
|
1458
|
-
import { mkdir as
|
|
1459
|
-
import
|
|
2021
|
+
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2022
|
+
import path7 from "node:path";
|
|
1460
2023
|
var syncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
1461
2024
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
1462
2025
|
const localDocuments = await readLocalSyncedDocuments(rootDir);
|
|
@@ -1533,7 +2096,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
1533
2096
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
1534
2097
|
const documents = [];
|
|
1535
2098
|
for (const fullPath of markdownFiles) {
|
|
1536
|
-
const raw = await
|
|
2099
|
+
const raw = await readFile4(fullPath, "utf8");
|
|
1537
2100
|
const parsed = parseSyncedMarkdown(raw);
|
|
1538
2101
|
if (!parsed) {
|
|
1539
2102
|
continue;
|
|
@@ -1574,8 +2137,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
1574
2137
|
result.skipped.push(document.repoPath);
|
|
1575
2138
|
continue;
|
|
1576
2139
|
}
|
|
1577
|
-
await
|
|
1578
|
-
await
|
|
2140
|
+
await mkdir6(path7.dirname(fullPath), { recursive: true });
|
|
2141
|
+
await writeFile6(fullPath, createSyncedDocumentMarkdown(document), "utf8");
|
|
1579
2142
|
result.written.push(document.repoPath);
|
|
1580
2143
|
}
|
|
1581
2144
|
return result;
|
|
@@ -1636,7 +2199,7 @@ function parseSyncedMarkdown(content) {
|
|
|
1636
2199
|
}
|
|
1637
2200
|
async function readExistingSyncedDocument(fullPath) {
|
|
1638
2201
|
try {
|
|
1639
|
-
const raw = await
|
|
2202
|
+
const raw = await readFile4(fullPath, "utf8");
|
|
1640
2203
|
const parsed = parseSyncedMarkdown(raw);
|
|
1641
2204
|
if (!parsed) {
|
|
1642
2205
|
return { exists: true };
|
|
@@ -1661,7 +2224,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
1661
2224
|
async function findMarkdownFiles(rootDir) {
|
|
1662
2225
|
const files = [];
|
|
1663
2226
|
for (const syncRoot of syncRoots) {
|
|
1664
|
-
const fullRoot =
|
|
2227
|
+
const fullRoot = path7.join(rootDir, syncRoot);
|
|
1665
2228
|
if (!await exists2(fullRoot)) {
|
|
1666
2229
|
continue;
|
|
1667
2230
|
}
|
|
@@ -1670,8 +2233,8 @@ async function findMarkdownFiles(rootDir) {
|
|
|
1670
2233
|
return files;
|
|
1671
2234
|
}
|
|
1672
2235
|
async function walkMarkdownFiles(directory, files) {
|
|
1673
|
-
for (const entry of await
|
|
1674
|
-
const fullPath =
|
|
2236
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
2237
|
+
const fullPath = path7.join(directory, entry.name);
|
|
1675
2238
|
if (entry.isDirectory()) {
|
|
1676
2239
|
await walkMarkdownFiles(fullPath, files);
|
|
1677
2240
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -1680,30 +2243,30 @@ async function walkMarkdownFiles(directory, files) {
|
|
|
1680
2243
|
}
|
|
1681
2244
|
}
|
|
1682
2245
|
function safeRepoPath(rootDir, repoPath) {
|
|
1683
|
-
if (
|
|
2246
|
+
if (path7.isAbsolute(repoPath)) {
|
|
1684
2247
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
1685
2248
|
}
|
|
1686
|
-
const normalized =
|
|
1687
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
2249
|
+
const normalized = path7.normalize(repoPath);
|
|
2250
|
+
if (normalized === ".." || normalized.startsWith(`..${path7.sep}`)) {
|
|
1688
2251
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
1689
2252
|
}
|
|
1690
|
-
const root =
|
|
1691
|
-
const fullPath =
|
|
1692
|
-
if (!fullPath.startsWith(`${root}${
|
|
2253
|
+
const root = path7.resolve(rootDir);
|
|
2254
|
+
const fullPath = path7.resolve(root, normalized);
|
|
2255
|
+
if (!fullPath.startsWith(`${root}${path7.sep}`)) {
|
|
1693
2256
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
1694
2257
|
}
|
|
1695
2258
|
return fullPath;
|
|
1696
2259
|
}
|
|
1697
2260
|
function isControlPlanePath(repoPath) {
|
|
1698
|
-
const normalized =
|
|
1699
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
2261
|
+
const normalized = path7.normalize(repoPath);
|
|
2262
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path7.sep}`));
|
|
1700
2263
|
}
|
|
1701
2264
|
function toRepoPath(rootDir, fullPath) {
|
|
1702
|
-
return
|
|
2265
|
+
return path7.relative(rootDir, fullPath).split(path7.sep).join("/");
|
|
1703
2266
|
}
|
|
1704
2267
|
function inferTitle(content, repoPath) {
|
|
1705
2268
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
1706
|
-
return heading ||
|
|
2269
|
+
return heading || path7.basename(repoPath, path7.extname(repoPath));
|
|
1707
2270
|
}
|
|
1708
2271
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
1709
2272
|
return {
|
|
@@ -1724,9 +2287,9 @@ async function exists2(filePath) {
|
|
|
1724
2287
|
}
|
|
1725
2288
|
|
|
1726
2289
|
// src/tool-session-store.ts
|
|
1727
|
-
import { mkdir as
|
|
1728
|
-
import
|
|
1729
|
-
import
|
|
2290
|
+
import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
|
|
2291
|
+
import os4 from "node:os";
|
|
2292
|
+
import path8 from "node:path";
|
|
1730
2293
|
var LocalToolSessionStore = class {
|
|
1731
2294
|
constructor(filePath = defaultSessionStorePath()) {
|
|
1732
2295
|
this.filePath = filePath;
|
|
@@ -1740,12 +2303,12 @@ var LocalToolSessionStore = class {
|
|
|
1740
2303
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
1741
2304
|
const data = await this.read();
|
|
1742
2305
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1743
|
-
await
|
|
1744
|
-
await
|
|
2306
|
+
await mkdir7(path8.dirname(this.filePath), { recursive: true });
|
|
2307
|
+
await writeFile7(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
1745
2308
|
}
|
|
1746
2309
|
async read() {
|
|
1747
2310
|
try {
|
|
1748
|
-
return JSON.parse(await
|
|
2311
|
+
return JSON.parse(await readFile5(this.filePath, "utf8"));
|
|
1749
2312
|
} catch {
|
|
1750
2313
|
return {};
|
|
1751
2314
|
}
|
|
@@ -1753,21 +2316,24 @@ var LocalToolSessionStore = class {
|
|
|
1753
2316
|
};
|
|
1754
2317
|
function defaultSessionStorePath() {
|
|
1755
2318
|
if (process.platform === "darwin") {
|
|
1756
|
-
return
|
|
2319
|
+
return path8.join(os4.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
1757
2320
|
}
|
|
1758
2321
|
if (process.platform === "win32") {
|
|
1759
|
-
return
|
|
2322
|
+
return path8.join(process.env.APPDATA ?? os4.homedir(), "Amistio", "tool-sessions.json");
|
|
1760
2323
|
}
|
|
1761
|
-
return
|
|
2324
|
+
return path8.join(process.env.XDG_STATE_HOME ?? path8.join(os4.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
1762
2325
|
}
|
|
1763
2326
|
|
|
1764
2327
|
// src/work-runner.ts
|
|
1765
2328
|
var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
|
|
1766
2329
|
var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
|
|
1767
|
-
function createWorkExecutionPrompt(workItem) {
|
|
2330
|
+
function createWorkExecutionPrompt(workItem, context) {
|
|
1768
2331
|
if (workItem.workKind === "brainGeneration") {
|
|
1769
2332
|
return createBrainGenerationPrompt(workItem);
|
|
1770
2333
|
}
|
|
2334
|
+
if (workItem.workKind === "planRevision") {
|
|
2335
|
+
return createPlanRevisionPrompt(workItem, context?.planRevision);
|
|
2336
|
+
}
|
|
1771
2337
|
return [
|
|
1772
2338
|
"# Amistio Work Execution",
|
|
1773
2339
|
"",
|
|
@@ -1792,6 +2358,42 @@ function createWorkExecutionPrompt(workItem) {
|
|
|
1792
2358
|
"- Run relevant verification commands when feasible and summarize results."
|
|
1793
2359
|
].join("\n");
|
|
1794
2360
|
}
|
|
2361
|
+
function createPlanRevisionPrompt(workItem, context) {
|
|
2362
|
+
const messages = context?.messages ?? [];
|
|
2363
|
+
return [
|
|
2364
|
+
"# Amistio Plan Revision",
|
|
2365
|
+
"",
|
|
2366
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
2367
|
+
"Revise the selected generated plan using the user's conversation. Do not implement product/source code changes in this pass.",
|
|
2368
|
+
"",
|
|
2369
|
+
"## Work Item",
|
|
2370
|
+
"",
|
|
2371
|
+
`Title: ${workItem.title}`,
|
|
2372
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
2373
|
+
`Project ID: ${workItem.projectId}`,
|
|
2374
|
+
`Document ID: ${workItem.reviewDocumentId ?? "unknown"}`,
|
|
2375
|
+
`Document revision: ${workItem.reviewDocumentRevision ?? "unknown"}`,
|
|
2376
|
+
"",
|
|
2377
|
+
"## Current Plan",
|
|
2378
|
+
"",
|
|
2379
|
+
context?.planDocument.content ?? "The current plan document could not be loaded. Explain the blocker in the result summary.",
|
|
2380
|
+
"",
|
|
2381
|
+
"## Conversation",
|
|
2382
|
+
"",
|
|
2383
|
+
messages.length ? messages.map((message) => `- ${message.role} / ${message.intent} / ${message.status} / rev ${message.documentRevision}: ${message.content}`).join("\n") : "No conversation messages were loaded.",
|
|
2384
|
+
"",
|
|
2385
|
+
"## Output Contract",
|
|
2386
|
+
"",
|
|
2387
|
+
"Print exactly one JSON object between the markers below with an artifacts array containing one revised plan artifact.",
|
|
2388
|
+
'The artifact must use documentType "plan", keep the plan under plans/, and include the complete revised plan content.',
|
|
2389
|
+
"",
|
|
2390
|
+
generationResultStart,
|
|
2391
|
+
'{"artifacts":[{"documentType":"plan","title":"Revised Plan","repoPath":"plans/PLAN-revised.md","content":"# Revised Plan\\n\\n## Goal\\n..."}]}',
|
|
2392
|
+
generationResultEnd,
|
|
2393
|
+
"",
|
|
2394
|
+
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
2395
|
+
].join("\n");
|
|
2396
|
+
}
|
|
1795
2397
|
function parseBrainGenerationArtifacts(output) {
|
|
1796
2398
|
const start = output.indexOf(generationResultStart);
|
|
1797
2399
|
const end = output.indexOf(generationResultEnd, start + generationResultStart.length);
|
|
@@ -1861,12 +2463,33 @@ function stripJsonFence(value) {
|
|
|
1861
2463
|
return trimmed.replace(/^```(?:json)?\s*/i, "").replace(/```$/i, "").trim();
|
|
1862
2464
|
}
|
|
1863
2465
|
|
|
2466
|
+
// src/runner-status.ts
|
|
2467
|
+
var watchStateReminderMs = 60 * 1e3;
|
|
2468
|
+
function formatWatchStartupContext(input) {
|
|
2469
|
+
return [
|
|
2470
|
+
`Runner ${input.runnerId} is watching project ${input.projectId}.`,
|
|
2471
|
+
`Repository link: ${input.repositoryLinkId}`,
|
|
2472
|
+
`API: ${input.apiUrl}`,
|
|
2473
|
+
`Polling interval: ${input.intervalSeconds}s. Press Ctrl+C to stop.`
|
|
2474
|
+
];
|
|
2475
|
+
}
|
|
2476
|
+
function formatWatchIdleLine(action, intervalSeconds) {
|
|
2477
|
+
return `${formatProjectNextAction(action)} Checking again in ${intervalSeconds}s.`;
|
|
2478
|
+
}
|
|
2479
|
+
function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
|
|
2480
|
+
const key = watchStateKey(action);
|
|
2481
|
+
return !previous || previous.key !== key || nowMs - previous.printedAtMs >= reminderMs;
|
|
2482
|
+
}
|
|
2483
|
+
function watchStateKey(action) {
|
|
2484
|
+
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
2485
|
+
}
|
|
2486
|
+
|
|
1864
2487
|
// src/index.ts
|
|
1865
2488
|
var program = new Command();
|
|
1866
2489
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
1867
2490
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
1868
|
-
program.name("amistio").description("Amistio project brain CLI").version("0.1.
|
|
1869
|
-
var CLI_VERSION = "0.1.
|
|
2491
|
+
program.name("amistio").description("Amistio project brain CLI").version("0.1.2");
|
|
2492
|
+
var CLI_VERSION = "0.1.2";
|
|
1870
2493
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
1871
2494
|
const created = await initControlPlane(options.root);
|
|
1872
2495
|
console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
|
|
@@ -2019,7 +2642,19 @@ work.command("list").description("List queued work without claiming it").option(
|
|
|
2019
2642
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
2020
2643
|
return;
|
|
2021
2644
|
}
|
|
2022
|
-
const { workItems } = await
|
|
2645
|
+
const [{ workItems }, { documents }, { runners }] = await Promise.all([
|
|
2646
|
+
context.client.listWorkItems(context.metadata.amistioProjectId),
|
|
2647
|
+
context.client.listBrainDocuments(context.metadata.amistioProjectId),
|
|
2648
|
+
context.client.listRunners(context.metadata.amistioProjectId).catch(() => ({ runners: [] }))
|
|
2649
|
+
]);
|
|
2650
|
+
const nextAction = computeProjectNextAction({
|
|
2651
|
+
projectId: context.metadata.amistioProjectId,
|
|
2652
|
+
repositoryLinks: [createCliRepositoryLink(context.metadata, options.root)],
|
|
2653
|
+
documents,
|
|
2654
|
+
workItems,
|
|
2655
|
+
runnerHeartbeats: runners
|
|
2656
|
+
});
|
|
2657
|
+
console.log(`Next action: ${formatProjectNextAction(nextAction)}`);
|
|
2023
2658
|
if (!workItems.length) {
|
|
2024
2659
|
console.log("No work items are queued for this project.");
|
|
2025
2660
|
return;
|
|
@@ -2042,7 +2677,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
2042
2677
|
}
|
|
2043
2678
|
const prompt = createWorkExecutionPrompt(workItem);
|
|
2044
2679
|
if (options.out) {
|
|
2045
|
-
await
|
|
2680
|
+
await writeFile8(options.out, prompt, "utf8");
|
|
2046
2681
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
2047
2682
|
} else {
|
|
2048
2683
|
console.log(prompt);
|
|
@@ -2056,7 +2691,7 @@ program.command("tools").description("List local AI coding tools that the Amisti
|
|
|
2056
2691
|
}
|
|
2057
2692
|
console.log("custom - pass --tool-command to use any other local runner command.");
|
|
2058
2693
|
});
|
|
2059
|
-
program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
|
|
2694
|
+
program.command("orchestrate").description("Update the Amistio control plane through a user-installed local AI tool").argument("[goal...]", "Goal or next-step instruction for the orchestration pass").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "auto").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--prompt-out <path>", "Write the generated orchestration prompt to a file before running").option("--dry-run", "Print the generated orchestration prompt without running a tool").option("--no-stream", "Capture local tool output instead of streaming it").action(async (goalParts, options) => {
|
|
2060
2695
|
const goal = goalParts?.join(" ").trim() || "Review the current repository state and update the Amistio control plane with the next useful orchestration steps.";
|
|
2061
2696
|
const prompt = await createOrchestrationPrompt({ rootDir: options.root, goal });
|
|
2062
2697
|
if (options.promptOut) {
|
|
@@ -2068,13 +2703,14 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2068
2703
|
return;
|
|
2069
2704
|
}
|
|
2070
2705
|
const sessionPolicy = normalizeSessionPolicy(options.session);
|
|
2071
|
-
const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, ...options.toolCommand ? { toolCommand: options.toolCommand } : {} });
|
|
2706
|
+
const preview = await createToolRunPreview({ rootDir: options.root, prompt, tool: options.tool, ...options.toolCommand ? { toolCommand: options.toolCommand } : {}, ...options.model ? { model: options.model } : {} });
|
|
2072
2707
|
console.log(`Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
2073
2708
|
const result = await runLocalTool({
|
|
2074
2709
|
rootDir: options.root,
|
|
2075
2710
|
prompt,
|
|
2076
2711
|
tool: options.tool,
|
|
2077
2712
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
2713
|
+
...options.model ? { model: options.model } : {},
|
|
2078
2714
|
streamOutput: options.stream,
|
|
2079
2715
|
...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
|
|
2080
2716
|
});
|
|
@@ -2088,7 +2724,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
2088
2724
|
process.exitCode = result.exitCode;
|
|
2089
2725
|
}
|
|
2090
2726
|
});
|
|
2091
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent", "
|
|
2727
|
+
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
|
|
2092
2728
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2093
2729
|
if (!context) {
|
|
2094
2730
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -2099,10 +2735,34 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2099
2735
|
process.exitCode = 1;
|
|
2100
2736
|
return;
|
|
2101
2737
|
}
|
|
2738
|
+
if (options.background) {
|
|
2739
|
+
if (options.dryRun) {
|
|
2740
|
+
console.log("Background runners cannot be started in dry-run mode.");
|
|
2741
|
+
process.exitCode = 1;
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
const metadata = await startRunnerDaemon({
|
|
2745
|
+
accountId: context.metadata.amistioAccountId,
|
|
2746
|
+
projectId: context.metadata.amistioProjectId,
|
|
2747
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
2748
|
+
runnerId: options.runnerId,
|
|
2749
|
+
rootDir: path9.resolve(options.root),
|
|
2750
|
+
apiUrl: options.apiUrl,
|
|
2751
|
+
args: buildBackgroundRunnerArgs(options)
|
|
2752
|
+
});
|
|
2753
|
+
console.log(`Started background runner ${metadata.runnerId} with PID ${metadata.pid}.`);
|
|
2754
|
+
if (metadata.logPath) {
|
|
2755
|
+
console.log(`Log: ${metadata.logPath}`);
|
|
2756
|
+
}
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2102
2759
|
if (options.watch) {
|
|
2103
|
-
|
|
2760
|
+
for (const line of formatWatchStartupContext({ runnerId: options.runnerId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, apiUrl: options.apiUrl, intervalSeconds: options.intervalSeconds })) {
|
|
2761
|
+
console.log(line);
|
|
2762
|
+
}
|
|
2104
2763
|
}
|
|
2105
2764
|
let iterations = 0;
|
|
2765
|
+
let lastWatchStateLog;
|
|
2106
2766
|
while (true) {
|
|
2107
2767
|
iterations += 1;
|
|
2108
2768
|
const result = await runNextWorkItem({
|
|
@@ -2112,10 +2772,21 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2112
2772
|
runnerId: options.runnerId,
|
|
2113
2773
|
root: options.root,
|
|
2114
2774
|
sessionPolicy: normalizeSessionPolicy(options.session),
|
|
2115
|
-
tool: options.tool,
|
|
2775
|
+
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
2776
|
+
...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
|
|
2116
2777
|
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
2117
2778
|
dryRun: Boolean(options.dryRun),
|
|
2118
|
-
stream: options.stream
|
|
2779
|
+
stream: options.stream,
|
|
2780
|
+
commandContext: {
|
|
2781
|
+
accountId: context.metadata.amistioAccountId,
|
|
2782
|
+
apiUrl: options.apiUrl,
|
|
2783
|
+
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
2784
|
+
projectId: context.metadata.amistioProjectId,
|
|
2785
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
2786
|
+
root: options.root,
|
|
2787
|
+
runnerId: options.runnerId
|
|
2788
|
+
},
|
|
2789
|
+
suppressIdleOutput: Boolean(options.watch)
|
|
2119
2790
|
});
|
|
2120
2791
|
if (!options.watch || options.dryRun) {
|
|
2121
2792
|
if (result.exitCode !== 0) {
|
|
@@ -2123,16 +2794,94 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
2123
2794
|
}
|
|
2124
2795
|
return;
|
|
2125
2796
|
}
|
|
2797
|
+
if (result.status === "idle" && result.nextAction) {
|
|
2798
|
+
const nowMs = Date.now();
|
|
2799
|
+
if (shouldPrintWatchState(result.nextAction, lastWatchStateLog, nowMs)) {
|
|
2800
|
+
console.log(formatWatchIdleLine(result.nextAction, options.intervalSeconds));
|
|
2801
|
+
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (result.stopRunner) {
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2126
2807
|
if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
|
|
2127
2808
|
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
2128
2809
|
return;
|
|
2129
2810
|
}
|
|
2130
|
-
if (result.status === "idle") {
|
|
2131
|
-
console.log(`No approved work item is available. Checking again in ${options.intervalSeconds}s.`);
|
|
2132
|
-
}
|
|
2133
2811
|
await delay(options.intervalSeconds * 1e3);
|
|
2134
2812
|
}
|
|
2135
2813
|
});
|
|
2814
|
+
var runner = program.command("runner").description("Manage local Amistio runner processes");
|
|
2815
|
+
runner.command("status").description("Show background runner status for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Limit status to one runner ID").action(async (options) => {
|
|
2816
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2817
|
+
if (!context) {
|
|
2818
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
const records = await listRunnerDaemonMetadata({
|
|
2822
|
+
accountId: context.metadata.amistioAccountId,
|
|
2823
|
+
projectId: context.metadata.amistioProjectId,
|
|
2824
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
2825
|
+
...options.runnerId ? { runnerId: options.runnerId } : {}
|
|
2826
|
+
});
|
|
2827
|
+
const runners = await context.client.listRunners(context.metadata.amistioProjectId).then((result) => result.runners).catch(() => []);
|
|
2828
|
+
if (!records.length) {
|
|
2829
|
+
console.log("No background runner metadata found for this paired repository.");
|
|
2830
|
+
if (runners.length) {
|
|
2831
|
+
console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
|
|
2832
|
+
}
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
for (const record of records) {
|
|
2836
|
+
const runtimeStatus = runnerDaemonRuntimeStatus(record);
|
|
2837
|
+
const heartbeat = runners.find((item) => item.runnerId === record.runnerId);
|
|
2838
|
+
console.log(`Runner ${record.runnerId}: ${runtimeStatus}`);
|
|
2839
|
+
console.log(` PID: ${record.pid}`);
|
|
2840
|
+
console.log(` Uptime: ${runtimeStatus === "running" ? runnerDaemonUptime(record) : "not running"}`);
|
|
2841
|
+
console.log(` Project: ${record.projectId}`);
|
|
2842
|
+
console.log(` Repository link: ${record.repositoryLinkId}`);
|
|
2843
|
+
console.log(` Root: ${record.rootDir}`);
|
|
2844
|
+
console.log(` API: ${record.apiUrl}`);
|
|
2845
|
+
console.log(` Host: ${record.hostname}`);
|
|
2846
|
+
if (record.logPath) {
|
|
2847
|
+
console.log(` Log: ${record.logPath}`);
|
|
2848
|
+
}
|
|
2849
|
+
if (heartbeat) {
|
|
2850
|
+
console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
runner.command("stop").description("Stop a background runner for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Runner ID to stop when multiple background runners exist").action(async (options) => {
|
|
2855
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
2856
|
+
if (!context) {
|
|
2857
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const records = await listRunnerDaemonMetadata({
|
|
2861
|
+
accountId: context.metadata.amistioAccountId,
|
|
2862
|
+
projectId: context.metadata.amistioProjectId,
|
|
2863
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
2864
|
+
...options.runnerId ? { runnerId: options.runnerId } : {}
|
|
2865
|
+
});
|
|
2866
|
+
if (!records.length) {
|
|
2867
|
+
console.log("No background runner metadata found for this paired repository.");
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (records.length > 1 && !options.runnerId) {
|
|
2871
|
+
console.log(`Multiple background runners found: ${records.map((record2) => record2.runnerId).join(", ")}. Pass --runner-id to stop one.`);
|
|
2872
|
+
process.exitCode = 1;
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const record = records[0];
|
|
2876
|
+
const stopResult = await stopRunnerDaemonProcess(record);
|
|
2877
|
+
await markRunnerDaemonStopped(record);
|
|
2878
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
|
|
2879
|
+
version: CLI_VERSION,
|
|
2880
|
+
mode: "background",
|
|
2881
|
+
hostname: record.hostname
|
|
2882
|
+
}).catch(() => void 0);
|
|
2883
|
+
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
2884
|
+
});
|
|
2136
2885
|
async function runNextWorkItem({
|
|
2137
2886
|
apiClient,
|
|
2138
2887
|
dryRun,
|
|
@@ -2142,23 +2891,48 @@ async function runNextWorkItem({
|
|
|
2142
2891
|
runnerId,
|
|
2143
2892
|
sessionPolicy,
|
|
2144
2893
|
stream,
|
|
2145
|
-
|
|
2146
|
-
|
|
2894
|
+
explicitModel,
|
|
2895
|
+
explicitTool,
|
|
2896
|
+
toolCommand,
|
|
2897
|
+
commandContext,
|
|
2898
|
+
suppressIdleOutput
|
|
2147
2899
|
}) {
|
|
2148
|
-
|
|
2900
|
+
const toolConfig = await resolveRunnerToolConfig({
|
|
2901
|
+
apiClient,
|
|
2902
|
+
projectId,
|
|
2903
|
+
...explicitModel ? { explicitModel } : {},
|
|
2904
|
+
...explicitTool ? { explicitTool } : {},
|
|
2905
|
+
...toolCommand ? { toolCommand } : {}
|
|
2906
|
+
});
|
|
2907
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, toolConfig.ready ? "online" : "blocked", runnerHeartbeatMetadata(toolConfig));
|
|
2908
|
+
const commandResult = await runPendingRunnerCommand(apiClient, commandContext, runnerHeartbeatMetadata(toolConfig));
|
|
2909
|
+
if (commandResult.handled) {
|
|
2910
|
+
if (commandResult.message) {
|
|
2911
|
+
console.log(commandResult.message);
|
|
2912
|
+
}
|
|
2913
|
+
return { status: commandResult.succeeded ? "completed" : "failed", exitCode: commandResult.succeeded ? 0 : 1, ...commandResult.stopRunner ? { stopRunner: true } : {} };
|
|
2914
|
+
}
|
|
2915
|
+
if (!toolConfig.ready) {
|
|
2916
|
+
console.log(toolConfig.message);
|
|
2917
|
+
return { status: "blocked", exitCode: 1 };
|
|
2918
|
+
}
|
|
2149
2919
|
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId);
|
|
2150
2920
|
if (!result.workItem) {
|
|
2151
|
-
|
|
2152
|
-
|
|
2921
|
+
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
2922
|
+
const message = formatProjectNextAction(nextAction);
|
|
2923
|
+
if (!suppressIdleOutput) {
|
|
2924
|
+
console.log(message);
|
|
2925
|
+
}
|
|
2926
|
+
return { status: "idle", exitCode: 0, nextAction, message };
|
|
2153
2927
|
}
|
|
2154
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running",
|
|
2155
|
-
const prompt =
|
|
2156
|
-
if (dryRun || tool === "none") {
|
|
2928
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", runnerHeartbeatMetadata(toolConfig));
|
|
2929
|
+
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
2930
|
+
if (dryRun || toolConfig.tool === "none") {
|
|
2157
2931
|
console.log(prompt);
|
|
2158
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
2932
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2159
2933
|
return { status: "preview", exitCode: 0 };
|
|
2160
2934
|
}
|
|
2161
|
-
const preview = await createToolRunPreview({ rootDir: root, prompt, tool, ...toolCommand ? { toolCommand } : {} });
|
|
2935
|
+
const preview = await createToolRunPreview({ rootDir: root, prompt, tool: toolConfig.tool, ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
2162
2936
|
const sessionContext = await prepareToolSession({
|
|
2163
2937
|
apiClient,
|
|
2164
2938
|
projectId,
|
|
@@ -2166,6 +2940,7 @@ async function runNextWorkItem({
|
|
|
2166
2940
|
runnerId,
|
|
2167
2941
|
sessionPolicy: result.workItem.sessionPolicy ?? sessionPolicy,
|
|
2168
2942
|
toolName: preview.toolName,
|
|
2943
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
2169
2944
|
supportsSessionReuse: preview.supportsSessionReuse,
|
|
2170
2945
|
resumabilityScope: preview.resumabilityScope,
|
|
2171
2946
|
workItem: result.workItem
|
|
@@ -2177,8 +2952,9 @@ async function runNextWorkItem({
|
|
|
2177
2952
|
const toolResult = await runLocalTool({
|
|
2178
2953
|
rootDir: root,
|
|
2179
2954
|
prompt,
|
|
2180
|
-
tool,
|
|
2955
|
+
tool: toolConfig.tool,
|
|
2181
2956
|
...toolCommand ? { toolCommand } : {},
|
|
2957
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
2182
2958
|
streamOutput: stream,
|
|
2183
2959
|
...sessionContext.toolSession ? {
|
|
2184
2960
|
session: {
|
|
@@ -2189,7 +2965,7 @@ async function runNextWorkItem({
|
|
|
2189
2965
|
}
|
|
2190
2966
|
} : {}
|
|
2191
2967
|
}).catch(async (error) => {
|
|
2192
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked",
|
|
2968
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", runnerHeartbeatMetadata(toolConfig));
|
|
2193
2969
|
await markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage2(error));
|
|
2194
2970
|
throw error;
|
|
2195
2971
|
});
|
|
@@ -2202,7 +2978,7 @@ async function runNextWorkItem({
|
|
|
2202
2978
|
if (!stream && toolResult.stderr.trim()) {
|
|
2203
2979
|
console.error(toolResult.stderr.trim());
|
|
2204
2980
|
}
|
|
2205
|
-
if (result.workItem.workKind === "brainGeneration") {
|
|
2981
|
+
if (result.workItem.workKind === "brainGeneration" || result.workItem.workKind === "planRevision") {
|
|
2206
2982
|
return finalizeBrainGenerationWork({
|
|
2207
2983
|
apiClient,
|
|
2208
2984
|
durationMs: Date.now() - startedAt,
|
|
@@ -2210,6 +2986,7 @@ async function runNextWorkItem({
|
|
|
2210
2986
|
repositoryLinkId,
|
|
2211
2987
|
runnerId,
|
|
2212
2988
|
sessionContext,
|
|
2989
|
+
toolConfig,
|
|
2213
2990
|
toolName: preview.toolName,
|
|
2214
2991
|
toolResult,
|
|
2215
2992
|
workItem: result.workItem
|
|
@@ -2239,6 +3016,7 @@ async function runNextWorkItem({
|
|
|
2239
3016
|
runnerId,
|
|
2240
3017
|
{
|
|
2241
3018
|
tool: preview.toolName,
|
|
3019
|
+
...toolResult.model ? { model: toolResult.model } : {},
|
|
2242
3020
|
durationMs,
|
|
2243
3021
|
message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
2244
3022
|
sessionPolicy: sessionContext.policy,
|
|
@@ -2252,11 +3030,100 @@ async function runNextWorkItem({
|
|
|
2252
3030
|
...failureExcerpt ? { error: failureExcerpt } : {}
|
|
2253
3031
|
}
|
|
2254
3032
|
);
|
|
2255
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
3033
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2256
3034
|
const durationSeconds = Math.round(durationMs / 1e3);
|
|
2257
3035
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
2258
3036
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
2259
3037
|
}
|
|
3038
|
+
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
3039
|
+
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
3040
|
+
const command = commands.filter((item) => item.status === "pending" || item.status === "acknowledged" || item.status === "running").sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
|
|
3041
|
+
if (!command) {
|
|
3042
|
+
return { handled: false, succeeded: true };
|
|
3043
|
+
}
|
|
3044
|
+
await updateRunnerCommandStatus(apiClient, context, command, "acknowledged", "Command acknowledged by local runner.");
|
|
3045
|
+
await updateRunnerCommandStatus(apiClient, context, command, "running", `Running ${runnerCommandLabel(command.commandKind)} command.`);
|
|
3046
|
+
const result = await executeRunnerCommand(command, context);
|
|
3047
|
+
await updateRunnerCommandStatus(apiClient, context, command, result.succeeded ? "completed" : "failed", result.message, result.error);
|
|
3048
|
+
if (command.commandKind === "remove" && result.succeeded) {
|
|
3049
|
+
await new LocalCredentialStore().delete(credentialKey(context.accountId, context.projectId, context.repositoryLinkId));
|
|
3050
|
+
}
|
|
3051
|
+
if (result.stopRunner && command.commandKind === "remove") {
|
|
3052
|
+
await apiClient.sendRunnerHeartbeat(context.projectId, context.runnerId, context.repositoryLinkId, "offline", heartbeatMetadata).catch(() => void 0);
|
|
3053
|
+
}
|
|
3054
|
+
return { handled: true, succeeded: result.succeeded, message: result.message, ...result.stopRunner ? { stopRunner: true } : {} };
|
|
3055
|
+
}
|
|
3056
|
+
async function updateRunnerCommandStatus(apiClient, context, command, status, message, error) {
|
|
3057
|
+
const result = await apiClient.updateRunnerCommand(context.projectId, command.commandId, {
|
|
3058
|
+
runnerId: context.runnerId,
|
|
3059
|
+
repositoryLinkId: context.repositoryLinkId,
|
|
3060
|
+
status,
|
|
3061
|
+
idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID()}`,
|
|
3062
|
+
message,
|
|
3063
|
+
...error ? { error } : {}
|
|
3064
|
+
});
|
|
3065
|
+
return result.command;
|
|
3066
|
+
}
|
|
3067
|
+
async function executeRunnerCommand(command, context) {
|
|
3068
|
+
if (command.commandKind === "remove") {
|
|
3069
|
+
return { succeeded: true, stopRunner: true, message: "Runner credential revoked. This local runner will stop after removing its stored credential." };
|
|
3070
|
+
}
|
|
3071
|
+
if (command.commandKind === "restart") {
|
|
3072
|
+
return restartCurrentRunner(context);
|
|
3073
|
+
}
|
|
3074
|
+
return runOfficialCliUpdate();
|
|
3075
|
+
}
|
|
3076
|
+
async function restartCurrentRunner(context) {
|
|
3077
|
+
if (currentRunnerMode() !== "background") {
|
|
3078
|
+
return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
|
|
3079
|
+
}
|
|
3080
|
+
const [metadata] = await listRunnerDaemonMetadata({ accountId: context.accountId, projectId: context.projectId, repositoryLinkId: context.repositoryLinkId, runnerId: context.runnerId });
|
|
3081
|
+
if (!metadata) {
|
|
3082
|
+
return { succeeded: false, message: "Background runner metadata was not found, so restart needs manual action." };
|
|
3083
|
+
}
|
|
3084
|
+
try {
|
|
3085
|
+
const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
|
|
3086
|
+
return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
return { succeeded: false, message: "Background restart failed.", error: errorMessage2(error) };
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
async function runOfficialCliUpdate() {
|
|
3092
|
+
const result = await runOfficialUpdateProcess("npm", ["install", "-g", "@amistio/cli"], 12e4);
|
|
3093
|
+
if (result.exitCode === 0) {
|
|
3094
|
+
return { succeeded: true, message: "Official Amistio CLI update command completed." };
|
|
3095
|
+
}
|
|
3096
|
+
return { succeeded: false, message: "Official Amistio CLI update command failed.", error: result.output || `npm exited with code ${result.exitCode}.` };
|
|
3097
|
+
}
|
|
3098
|
+
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
3099
|
+
return new Promise((resolve) => {
|
|
3100
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3101
|
+
let output = "";
|
|
3102
|
+
const timer = setTimeout(() => {
|
|
3103
|
+
output += "Timed out while running official CLI update.\n";
|
|
3104
|
+
child.kill("SIGTERM");
|
|
3105
|
+
}, timeoutMs);
|
|
3106
|
+
child.stdout?.on("data", (chunk) => {
|
|
3107
|
+
output += chunk.toString("utf8");
|
|
3108
|
+
});
|
|
3109
|
+
child.stderr?.on("data", (chunk) => {
|
|
3110
|
+
output += chunk.toString("utf8");
|
|
3111
|
+
});
|
|
3112
|
+
child.on("error", (error) => {
|
|
3113
|
+
clearTimeout(timer);
|
|
3114
|
+
resolve({ exitCode: 1, output: error.message });
|
|
3115
|
+
});
|
|
3116
|
+
child.on("close", (code) => {
|
|
3117
|
+
clearTimeout(timer);
|
|
3118
|
+
resolve({ exitCode: code ?? 1, output: truncateLogExcerpt(output) });
|
|
3119
|
+
});
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
function runnerCommandLabel(commandKind) {
|
|
3123
|
+
if (commandKind === "update") return "update";
|
|
3124
|
+
if (commandKind === "restart") return "restart";
|
|
3125
|
+
return "remove";
|
|
3126
|
+
}
|
|
2260
3127
|
async function finalizeBrainGenerationWork({
|
|
2261
3128
|
apiClient,
|
|
2262
3129
|
durationMs,
|
|
@@ -2264,6 +3131,7 @@ async function finalizeBrainGenerationWork({
|
|
|
2264
3131
|
repositoryLinkId,
|
|
2265
3132
|
runnerId,
|
|
2266
3133
|
sessionContext,
|
|
3134
|
+
toolConfig,
|
|
2267
3135
|
toolName,
|
|
2268
3136
|
toolResult,
|
|
2269
3137
|
workItem
|
|
@@ -2302,6 +3170,7 @@ ${toolResult.stderr}`);
|
|
|
2302
3170
|
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
2303
3171
|
};
|
|
2304
3172
|
if (artifacts) {
|
|
3173
|
+
const completionMessage = workItem.workKind === "planRevision" ? `${toolName} returned a revised plan for review.` : `${toolName} generated ${artifacts.length} brain artifact${artifacts.length === 1 ? "" : "s"}.`;
|
|
2305
3174
|
const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
2306
3175
|
status: "completed",
|
|
2307
3176
|
runnerId,
|
|
@@ -2310,10 +3179,10 @@ ${toolResult.stderr}`);
|
|
|
2310
3179
|
tool: toolName,
|
|
2311
3180
|
durationMs,
|
|
2312
3181
|
...sessionTelemetry,
|
|
2313
|
-
message:
|
|
3182
|
+
message: completionMessage
|
|
2314
3183
|
});
|
|
2315
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
2316
|
-
console.log(`Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
3184
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3185
|
+
console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
2317
3186
|
return { status: "completed", exitCode: 0 };
|
|
2318
3187
|
}
|
|
2319
3188
|
await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
@@ -2326,10 +3195,23 @@ ${toolResult.stderr}`);
|
|
|
2326
3195
|
message: `${toolName} did not produce valid brain artifacts.`,
|
|
2327
3196
|
...generationError ? { error: generationError } : {}
|
|
2328
3197
|
});
|
|
2329
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online",
|
|
3198
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
2330
3199
|
console.error(generationError ?? "Local runner generation failed.");
|
|
2331
3200
|
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
2332
3201
|
}
|
|
3202
|
+
async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
|
|
3203
|
+
if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
|
|
3204
|
+
return createWorkExecutionPrompt(workItem);
|
|
3205
|
+
}
|
|
3206
|
+
const [{ documents }, { messages }] = await Promise.all([
|
|
3207
|
+
apiClient.listBrainDocuments(projectId),
|
|
3208
|
+
apiClient.listPlanReviewMessages(projectId, workItem.reviewDocumentId)
|
|
3209
|
+
]);
|
|
3210
|
+
const planDocument = documents.find((document) => document.documentId === workItem.reviewDocumentId || document.id === workItem.reviewDocumentId);
|
|
3211
|
+
return createWorkExecutionPrompt(workItem, {
|
|
3212
|
+
...planDocument ? { planRevision: { planDocument, messages } } : {}
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
2333
3215
|
async function loadPairedApiContext(root, apiUrl) {
|
|
2334
3216
|
const metadata = await readProjectLink(root);
|
|
2335
3217
|
if (!metadata) {
|
|
@@ -2348,6 +3230,37 @@ async function loadPairedApiContext(root, apiUrl) {
|
|
|
2348
3230
|
})
|
|
2349
3231
|
};
|
|
2350
3232
|
}
|
|
3233
|
+
async function loadProjectNextAction(apiClient, projectId, repositoryLinkId, root) {
|
|
3234
|
+
const [{ workItems }, { documents }, { runners }] = await Promise.all([
|
|
3235
|
+
apiClient.listWorkItems(projectId),
|
|
3236
|
+
apiClient.listBrainDocuments(projectId),
|
|
3237
|
+
apiClient.listRunners(projectId).catch(() => ({ runners: [] }))
|
|
3238
|
+
]);
|
|
3239
|
+
return computeProjectNextAction({
|
|
3240
|
+
projectId,
|
|
3241
|
+
repositoryLinks: [createCliRepositoryLink({ amistioAccountId: "local_runner", amistioProjectId: projectId, repositoryLinkId, defaultBranch: "main", lastSyncedRevision: 0 }, root)],
|
|
3242
|
+
documents,
|
|
3243
|
+
workItems,
|
|
3244
|
+
runnerHeartbeats: runners
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
function createCliRepositoryLink(metadata, root) {
|
|
3248
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3249
|
+
return {
|
|
3250
|
+
id: metadata.repositoryLinkId,
|
|
3251
|
+
type: "repositoryLink",
|
|
3252
|
+
schemaVersion: 1,
|
|
3253
|
+
accountId: metadata.amistioAccountId,
|
|
3254
|
+
projectId: metadata.amistioProjectId,
|
|
3255
|
+
repositoryLinkId: metadata.repositoryLinkId,
|
|
3256
|
+
repoName: inferRepoName(root),
|
|
3257
|
+
repoFingerprint: createRepoFingerprint(metadata.amistioAccountId, metadata.amistioProjectId, metadata.repositoryLinkId),
|
|
3258
|
+
defaultBranch: metadata.defaultBranch,
|
|
3259
|
+
status: "active",
|
|
3260
|
+
createdAt: now,
|
|
3261
|
+
updatedAt: now
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
2351
3264
|
function selectPromptWorkItem(workItems, workItemId) {
|
|
2352
3265
|
if (workItemId) {
|
|
2353
3266
|
return workItems.find((item) => item.workItemId === workItemId || item.id === workItemId);
|
|
@@ -2366,6 +3279,7 @@ async function prepareToolSession({
|
|
|
2366
3279
|
supportsSessionReuse,
|
|
2367
3280
|
resumabilityScope,
|
|
2368
3281
|
toolName,
|
|
3282
|
+
model,
|
|
2369
3283
|
workItem
|
|
2370
3284
|
}) {
|
|
2371
3285
|
const { toolSessions } = await apiClient.listToolSessions(projectId);
|
|
@@ -2396,6 +3310,7 @@ async function prepareToolSession({
|
|
|
2396
3310
|
repositoryLinkId,
|
|
2397
3311
|
tool: toolName,
|
|
2398
3312
|
provider: toolName,
|
|
3313
|
+
...model ? { model } : {},
|
|
2399
3314
|
resumabilityScope: supportsSessionReuse ? resumabilityScope : "none",
|
|
2400
3315
|
title: workItem.title,
|
|
2401
3316
|
summary: selection.reason,
|
|
@@ -2479,10 +3394,10 @@ function parsePositiveInteger(value) {
|
|
|
2479
3394
|
return parsed;
|
|
2480
3395
|
}
|
|
2481
3396
|
function inferRepoName(root) {
|
|
2482
|
-
return
|
|
3397
|
+
return path9.basename(path9.resolve(root)) || "repository";
|
|
2483
3398
|
}
|
|
2484
3399
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
2485
|
-
return
|
|
3400
|
+
return createHash3("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
2486
3401
|
}
|
|
2487
3402
|
function defaultApiUrl() {
|
|
2488
3403
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -2497,6 +3412,133 @@ function formatApiUrlFlag(apiUrl) {
|
|
|
2497
3412
|
function formatShellArg(value) {
|
|
2498
3413
|
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
2499
3414
|
}
|
|
3415
|
+
async function resolveRunnerToolConfig({ apiClient, explicitModel, explicitTool, projectId, toolCommand }) {
|
|
3416
|
+
const capabilities = toRunnerToolCapabilities(await detectLocalTools());
|
|
3417
|
+
if (toolCommand) {
|
|
3418
|
+
return {
|
|
3419
|
+
ready: true,
|
|
3420
|
+
tool: explicitTool ?? "auto",
|
|
3421
|
+
capabilities,
|
|
3422
|
+
source: "cli",
|
|
3423
|
+
status: "custom",
|
|
3424
|
+
effectiveTool: "custom",
|
|
3425
|
+
...explicitTool && explicitTool !== "none" && explicitTool !== "auto" && isLocalToolName(explicitTool) ? { requestedTool: explicitTool } : explicitTool === "auto" ? { requestedTool: "auto" } : {},
|
|
3426
|
+
...explicitModel ? { model: explicitModel } : {},
|
|
3427
|
+
message: "Using local custom tool command."
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
if (explicitTool === "none") {
|
|
3431
|
+
if (explicitModel) {
|
|
3432
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "modelUnsupported", message: "--model cannot be used with --tool none.", tool: "none", model: explicitModel });
|
|
3433
|
+
}
|
|
3434
|
+
return { ready: true, tool: "none", capabilities, source: "cli", status: "none", message: "No local tool selected." };
|
|
3435
|
+
}
|
|
3436
|
+
if (explicitTool && explicitTool !== "auto" && !isLocalToolName(explicitTool)) {
|
|
3437
|
+
return unavailableToolConfig({ capabilities, source: "cli", status: "unavailable", message: `Unsupported local tool: ${explicitTool}.`, tool: explicitTool, ...explicitModel ? { model: explicitModel } : {} });
|
|
3438
|
+
}
|
|
3439
|
+
const remotePreference = explicitTool || explicitModel ? void 0 : await apiClient.getRunnerPreferences(projectId).then((response) => response.effective).catch(() => void 0);
|
|
3440
|
+
const requestedTool = explicitTool ?? remotePreference?.tool ?? "auto";
|
|
3441
|
+
const model = explicitModel ?? remotePreference?.model;
|
|
3442
|
+
const source = explicitTool || explicitModel ? "cli" : remotePreference?.source ?? "default";
|
|
3443
|
+
return resolveRequestedTool({ capabilities, requestedTool, source, ...model ? { model } : {} });
|
|
3444
|
+
}
|
|
3445
|
+
function resolveRequestedTool({ capabilities, model, requestedTool, source }) {
|
|
3446
|
+
if (requestedTool === "auto") {
|
|
3447
|
+
const candidate = capabilities.find((capability2) => capability2.available && (!model || capability2.supportsModelSelection));
|
|
3448
|
+
if (!candidate) {
|
|
3449
|
+
return unavailableToolConfig({
|
|
3450
|
+
capabilities,
|
|
3451
|
+
source,
|
|
3452
|
+
status: model ? "modelUnsupported" : "unavailable",
|
|
3453
|
+
requestedTool,
|
|
3454
|
+
tool: "auto",
|
|
3455
|
+
...model ? { model } : {},
|
|
3456
|
+
message: model ? "No installed local tool can honor the selected model." : "No supported local AI tool is installed."
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
return { ready: true, tool: "auto", capabilities, source, status: "resolved", requestedTool, effectiveTool: candidate.name, ...model ? { model } : {} };
|
|
3460
|
+
}
|
|
3461
|
+
const capability = capabilities.find((candidate) => candidate.name === requestedTool);
|
|
3462
|
+
if (!capability?.available) {
|
|
3463
|
+
return unavailableToolConfig({ capabilities, source, status: "unavailable", requestedTool, tool: requestedTool, ...model ? { model } : {}, message: `${requestedTool} is selected but is not available on this runner.` });
|
|
3464
|
+
}
|
|
3465
|
+
if (model && !capability.supportsModelSelection) {
|
|
3466
|
+
return unavailableToolConfig({ capabilities, source, status: "modelUnsupported", requestedTool, effectiveTool: requestedTool, tool: requestedTool, model, message: `${requestedTool} is available but does not support Amistio model selection yet.` });
|
|
3467
|
+
}
|
|
3468
|
+
return { ready: true, tool: requestedTool, capabilities, source, status: "resolved", requestedTool, effectiveTool: requestedTool, ...model ? { model } : {} };
|
|
3469
|
+
}
|
|
3470
|
+
function unavailableToolConfig(input) {
|
|
3471
|
+
return {
|
|
3472
|
+
ready: false,
|
|
3473
|
+
tool: input.tool,
|
|
3474
|
+
capabilities: input.capabilities,
|
|
3475
|
+
source: input.source,
|
|
3476
|
+
status: input.status,
|
|
3477
|
+
message: input.message,
|
|
3478
|
+
...input.requestedTool ? { requestedTool: input.requestedTool } : {},
|
|
3479
|
+
...input.effectiveTool ? { effectiveTool: input.effectiveTool } : {},
|
|
3480
|
+
...input.model ? { model: input.model } : {}
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
function toRunnerToolCapabilities(tools) {
|
|
3484
|
+
return tools.map((tool) => ({
|
|
3485
|
+
name: tool.name,
|
|
3486
|
+
description: tool.description,
|
|
3487
|
+
available: tool.available,
|
|
3488
|
+
sdkAvailable: tool.sdkAvailable,
|
|
3489
|
+
commandAvailable: tool.commandAvailable,
|
|
3490
|
+
execution: tool.execution,
|
|
3491
|
+
supportsSessionReuse: tool.supportsSessionReuse,
|
|
3492
|
+
resumabilityScope: tool.resumabilityScope,
|
|
3493
|
+
supportsModelSelection: tool.supportsModelSelection
|
|
3494
|
+
}));
|
|
3495
|
+
}
|
|
3496
|
+
function buildBackgroundRunnerArgs(options) {
|
|
3497
|
+
const args = [
|
|
3498
|
+
"run",
|
|
3499
|
+
"--watch",
|
|
3500
|
+
"--api-url",
|
|
3501
|
+
options.apiUrl,
|
|
3502
|
+
"--runner-id",
|
|
3503
|
+
options.runnerId,
|
|
3504
|
+
"--root",
|
|
3505
|
+
path9.resolve(options.root),
|
|
3506
|
+
"--session",
|
|
3507
|
+
options.session,
|
|
3508
|
+
"--interval-seconds",
|
|
3509
|
+
String(options.intervalSeconds)
|
|
3510
|
+
];
|
|
3511
|
+
if (options.tool) {
|
|
3512
|
+
args.push("--tool", options.tool);
|
|
3513
|
+
}
|
|
3514
|
+
if (options.toolCommand) {
|
|
3515
|
+
args.push("--tool-command", options.toolCommand);
|
|
3516
|
+
}
|
|
3517
|
+
if (options.model) {
|
|
3518
|
+
args.push("--model", options.model);
|
|
3519
|
+
}
|
|
3520
|
+
if (options.maxIterations !== void 0) {
|
|
3521
|
+
args.push("--max-iterations", String(options.maxIterations));
|
|
3522
|
+
}
|
|
3523
|
+
if (!options.stream) {
|
|
3524
|
+
args.push("--no-stream");
|
|
3525
|
+
}
|
|
3526
|
+
return args;
|
|
3527
|
+
}
|
|
3528
|
+
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
3529
|
+
return {
|
|
3530
|
+
version: CLI_VERSION,
|
|
3531
|
+
mode,
|
|
3532
|
+
hostname: os5.hostname(),
|
|
3533
|
+
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
3534
|
+
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
3535
|
+
...toolConfig?.effectiveTool ? { effectiveTool: toolConfig.effectiveTool } : {},
|
|
3536
|
+
...toolConfig?.model ? { effectiveModel: toolConfig.model } : {},
|
|
3537
|
+
...toolConfig?.source ? { preferenceSource: toolConfig.source } : {},
|
|
3538
|
+
...toolConfig?.status ? { preferenceStatus: toolConfig.status } : {},
|
|
3539
|
+
...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
2500
3542
|
async function delay(milliseconds) {
|
|
2501
3543
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2502
3544
|
}
|