@amistio/cli 0.1.5 → 0.1.7
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 +24 -0
- package/dist/index.js +1852 -238
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { writeFile as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
import { createHash as createHash6, randomUUID } from "node:crypto";
|
|
5
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
6
|
+
import os7 from "node:os";
|
|
7
|
+
import path13 from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// ../shared/src/schemas.ts
|
|
@@ -28,6 +28,8 @@ var itemTypeSchema = z.enum([
|
|
|
28
28
|
"runnerCredential",
|
|
29
29
|
"runnerCommand",
|
|
30
30
|
"planReviewMessage",
|
|
31
|
+
"assistantMessage",
|
|
32
|
+
"impactReport",
|
|
31
33
|
"toolSession",
|
|
32
34
|
"activityEvent",
|
|
33
35
|
"pairingSession"
|
|
@@ -64,8 +66,41 @@ var workStatusSchema = z.enum([
|
|
|
64
66
|
]);
|
|
65
67
|
var sourceSchema = z.enum(["web", "repo", "generated", "runner"]);
|
|
66
68
|
var executionModeSchema = z.enum(["localRunner", "cloudSandbox"]);
|
|
67
|
-
var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision"]);
|
|
69
|
+
var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview"]);
|
|
68
70
|
var generatedDraftStatusSchema = z.enum(["queued", "generating", "reviewing", "approved", "rejected", "changesRequested", "failed"]);
|
|
71
|
+
var assistantQuestionModeSchema = z.enum(["brainOnly", "sourceAware"]);
|
|
72
|
+
var impactRiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
73
|
+
var impactReportStatusSchema = z.enum(["queued", "running", "completed", "failed", "stale"]);
|
|
74
|
+
var activityEventTypeSchema = z.enum([
|
|
75
|
+
"commandSubmitted",
|
|
76
|
+
"generationQueued",
|
|
77
|
+
"generationCompleted",
|
|
78
|
+
"generationFailed",
|
|
79
|
+
"documentGenerated",
|
|
80
|
+
"documentReview",
|
|
81
|
+
"documentSync",
|
|
82
|
+
"workQueued",
|
|
83
|
+
"workClaimed",
|
|
84
|
+
"workStatusChanged",
|
|
85
|
+
"runnerMilestone",
|
|
86
|
+
"assistantAnswered",
|
|
87
|
+
"assistantFailed",
|
|
88
|
+
"impactPreviewQueued",
|
|
89
|
+
"impactPreviewCompleted",
|
|
90
|
+
"impactPreviewFailed",
|
|
91
|
+
"impactPreviewStale",
|
|
92
|
+
"handoffExported"
|
|
93
|
+
]);
|
|
94
|
+
var activityEventActorSchema = z.enum(["webUser", "runner", "cli", "system"]);
|
|
95
|
+
var activityEventStatusSchema = z.enum(["info", "queued", "running", "completed", "failed", "blocked", "warning"]);
|
|
96
|
+
var activityMetadataValueSchema = z.union([
|
|
97
|
+
z.string().max(600),
|
|
98
|
+
z.number(),
|
|
99
|
+
z.boolean(),
|
|
100
|
+
z.null(),
|
|
101
|
+
z.array(z.string().max(200)).max(20)
|
|
102
|
+
]);
|
|
103
|
+
var activityMetadataSchema = z.record(z.string().min(1).max(80), activityMetadataValueSchema).default({});
|
|
69
104
|
var generatedBrainArtifactSchema = z.object({
|
|
70
105
|
documentType: documentTypeSchema,
|
|
71
106
|
title: z.string().trim().min(1),
|
|
@@ -102,11 +137,52 @@ var runnerToolCapabilitySchema = z.object({
|
|
|
102
137
|
execution: z.enum(["sdk", "command", "unavailable"]),
|
|
103
138
|
supportsSessionReuse: z.boolean(),
|
|
104
139
|
resumabilityScope: sessionResumabilityScopeSchema,
|
|
105
|
-
supportsModelSelection: z.boolean()
|
|
140
|
+
supportsModelSelection: z.boolean(),
|
|
141
|
+
supportsBranchIsolation: z.boolean().optional(),
|
|
142
|
+
supportsGitWorktreeIsolation: z.boolean().optional()
|
|
106
143
|
});
|
|
144
|
+
var runnerResourceUsageSchema = z.object({
|
|
145
|
+
sampledAt: isoDateTimeSchema,
|
|
146
|
+
processUptimeSeconds: z.number().nonnegative().optional(),
|
|
147
|
+
processMemoryRssBytes: z.number().int().nonnegative().optional(),
|
|
148
|
+
processMemoryHeapUsedBytes: z.number().int().nonnegative().optional(),
|
|
149
|
+
processMemoryHeapTotalBytes: z.number().int().nonnegative().optional(),
|
|
150
|
+
processCpuUserMicros: z.number().int().nonnegative().optional(),
|
|
151
|
+
processCpuSystemMicros: z.number().int().nonnegative().optional(),
|
|
152
|
+
processCpuPercent: z.number().nonnegative().optional(),
|
|
153
|
+
systemMemoryTotalBytes: z.number().int().nonnegative().optional(),
|
|
154
|
+
systemMemoryFreeBytes: z.number().int().nonnegative().optional(),
|
|
155
|
+
systemLoadAverage1m: z.number().nonnegative().optional(),
|
|
156
|
+
systemLoadAverage5m: z.number().nonnegative().optional(),
|
|
157
|
+
systemLoadAverage15m: z.number().nonnegative().optional()
|
|
158
|
+
});
|
|
159
|
+
var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
|
|
107
160
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
108
161
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
109
162
|
var projectStatusSchema = z.enum(["active", "archived"]);
|
|
163
|
+
var workspaceScopeKindSchema = z.enum(["personal", "organization"]);
|
|
164
|
+
var personalWorkspaceScopeSchema = z.object({
|
|
165
|
+
kind: z.literal("personal"),
|
|
166
|
+
accountId: z.string().min(1),
|
|
167
|
+
userId: z.string().min(1),
|
|
168
|
+
label: z.string().trim().min(1).default("Personal workspace")
|
|
169
|
+
});
|
|
170
|
+
var organizationWorkspaceScopeSchema = z.object({
|
|
171
|
+
kind: z.literal("organization"),
|
|
172
|
+
accountId: z.string().min(1),
|
|
173
|
+
organizationId: z.string().min(1),
|
|
174
|
+
organizationSlug: z.string().trim().min(1).optional(),
|
|
175
|
+
organizationRole: z.string().trim().min(1).optional(),
|
|
176
|
+
label: z.string().trim().min(1).default("Organization workspace")
|
|
177
|
+
});
|
|
178
|
+
var activeWorkspaceScopeSchema = z.discriminatedUnion("kind", [personalWorkspaceScopeSchema, organizationWorkspaceScopeSchema]);
|
|
179
|
+
var organizationWorkspaceOnboardingStatusSchema = z.enum(["available", "upgradeRequired", "authRequired", "unconfigured"]);
|
|
180
|
+
var organizationWorkspaceOnboardingSchema = z.object({
|
|
181
|
+
enabled: z.boolean(),
|
|
182
|
+
canCreate: z.boolean(),
|
|
183
|
+
status: organizationWorkspaceOnboardingStatusSchema,
|
|
184
|
+
reason: z.string().trim().min(1).optional()
|
|
185
|
+
});
|
|
110
186
|
var baseItemSchema = z.object({
|
|
111
187
|
id: z.string().min(1),
|
|
112
188
|
type: itemTypeSchema,
|
|
@@ -212,15 +288,36 @@ var workItemSchema = baseItemSchema.extend({
|
|
|
212
288
|
executionMode: executionModeSchema.optional(),
|
|
213
289
|
title: z.string().min(1),
|
|
214
290
|
requestedBy: z.string().min(1),
|
|
291
|
+
requestedByUserId: z.string().min(1).optional(),
|
|
215
292
|
approvedBy: z.string().min(1).optional(),
|
|
293
|
+
approvedByUserId: z.string().min(1).optional(),
|
|
216
294
|
sourceWish: z.string().min(1).optional(),
|
|
217
295
|
generatedDraftId: z.string().min(1).optional(),
|
|
296
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
218
297
|
reviewThreadId: z.string().min(1).optional(),
|
|
219
298
|
reviewDocumentId: z.string().min(1).optional(),
|
|
220
299
|
reviewDocumentRevision: z.number().int().nonnegative().optional(),
|
|
221
300
|
reviewMessageId: z.string().min(1).optional(),
|
|
301
|
+
assistantMessageId: z.string().min(1).optional(),
|
|
302
|
+
assistantQuestionMode: assistantQuestionModeSchema.optional(),
|
|
303
|
+
impactReportId: z.string().min(1).optional(),
|
|
304
|
+
impactDocumentId: z.string().min(1).optional(),
|
|
305
|
+
impactDocumentRevision: z.number().int().nonnegative().optional(),
|
|
222
306
|
claimedByRunnerId: z.string().optional(),
|
|
307
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
308
|
+
machineId: z.string().min(1).optional(),
|
|
309
|
+
claimLeaseId: z.string().min(1).optional(),
|
|
310
|
+
claimAttempt: z.number().int().nonnegative().optional(),
|
|
223
311
|
leaseExpiresAt: isoDateTimeSchema.optional(),
|
|
312
|
+
controllingAdrId: z.string().min(1).optional(),
|
|
313
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
314
|
+
executionBranch: z.string().min(1).optional(),
|
|
315
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
316
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
317
|
+
repositoryLockId: z.string().min(1).optional(),
|
|
318
|
+
baseRevision: z.string().min(1).optional(),
|
|
319
|
+
baseContentHash: z.string().min(1).optional(),
|
|
320
|
+
blockerReason: z.string().min(1).optional(),
|
|
224
321
|
attempt: z.number().int().nonnegative().default(0),
|
|
225
322
|
idempotencyKey: z.string().min(1),
|
|
226
323
|
sessionPolicy: sessionPolicySchema.optional(),
|
|
@@ -237,10 +334,19 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
237
334
|
runnerId: z.string().min(1),
|
|
238
335
|
repositoryLinkId: z.string().min(1),
|
|
239
336
|
status: z.enum(["online", "offline", "running", "blocked", "removed"]),
|
|
337
|
+
workspaceId: z.string().min(1).optional(),
|
|
338
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
339
|
+
machineId: z.string().min(1).optional(),
|
|
240
340
|
version: z.string().optional(),
|
|
241
341
|
mode: z.enum(["foreground", "background"]).optional(),
|
|
242
342
|
hostname: z.string().min(1).optional(),
|
|
243
343
|
runnerName: z.string().min(1).optional(),
|
|
344
|
+
supportsBranchIsolation: z.boolean().optional(),
|
|
345
|
+
supportsGitWorktreeIsolation: z.boolean().optional(),
|
|
346
|
+
currentWorkItemId: z.string().min(1).optional(),
|
|
347
|
+
currentImplementationScopeId: z.string().min(1).optional(),
|
|
348
|
+
currentWorktreeKey: z.string().min(1).optional(),
|
|
349
|
+
currentBranch: z.string().min(1).optional(),
|
|
244
350
|
capabilities: z.array(runnerToolCapabilitySchema).optional(),
|
|
245
351
|
requestedTool: runnerToolSelectionSchema.optional(),
|
|
246
352
|
requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
|
|
@@ -250,6 +356,7 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
250
356
|
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
251
357
|
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
252
358
|
preferenceMessage: z.string().optional(),
|
|
359
|
+
resourceUsage: runnerResourceUsageSchema.optional(),
|
|
253
360
|
lastSeenAt: isoDateTimeSchema
|
|
254
361
|
});
|
|
255
362
|
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
@@ -269,6 +376,16 @@ var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
|
269
376
|
workTitle: z.string().min(1).optional(),
|
|
270
377
|
initiatedBy: z.string().min(1).optional(),
|
|
271
378
|
approvedBy: z.string().min(1).optional(),
|
|
379
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
380
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
381
|
+
machineId: z.string().min(1).optional(),
|
|
382
|
+
controllingAdrId: z.string().min(1).optional(),
|
|
383
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
384
|
+
executionBranch: z.string().min(1).optional(),
|
|
385
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
386
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
387
|
+
repositoryLockId: z.string().min(1).optional(),
|
|
388
|
+
claimLeaseId: z.string().min(1).optional(),
|
|
272
389
|
status: z.enum(["claimed", "running", "completed", "failed", "blocked", "idle"]),
|
|
273
390
|
executionMode: executionModeSchema.optional(),
|
|
274
391
|
tool: z.string().min(1).optional(),
|
|
@@ -293,6 +410,8 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
|
293
410
|
projectId: z.string().min(1),
|
|
294
411
|
runnerCredentialId: z.string().min(1),
|
|
295
412
|
repositoryLinkId: z.string().min(1),
|
|
413
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
414
|
+
machineId: z.string().min(1).optional(),
|
|
296
415
|
tokenHash: z.string().min(32),
|
|
297
416
|
issuedAt: isoDateTimeSchema,
|
|
298
417
|
lastUsedAt: isoDateTimeSchema.optional(),
|
|
@@ -339,6 +458,231 @@ var planReviewMessageItemSchema = baseItemSchema.extend({
|
|
|
339
458
|
createdByUserId: z.string().min(1).optional(),
|
|
340
459
|
runnerId: z.string().min(1).optional()
|
|
341
460
|
});
|
|
461
|
+
var assistantMessageRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
462
|
+
var assistantMessageStatusSchema = z.enum(["posted", "queued", "running", "completed", "failed"]);
|
|
463
|
+
var assistantSourceBoundarySchema = z.enum(["projectBrain", "localSource", "runnerState", "mixed"]);
|
|
464
|
+
var assistantCitationSchema = z.object({
|
|
465
|
+
source: assistantSourceBoundarySchema,
|
|
466
|
+
documentId: z.string().min(1).optional(),
|
|
467
|
+
title: z.string().min(1).optional(),
|
|
468
|
+
repoPath: z.string().min(1).optional(),
|
|
469
|
+
documentRevision: z.number().int().nonnegative().optional(),
|
|
470
|
+
excerpt: z.string().max(600).optional()
|
|
471
|
+
});
|
|
472
|
+
var assistantMessageItemSchema = baseItemSchema.extend({
|
|
473
|
+
type: z.literal("assistantMessage"),
|
|
474
|
+
projectId: z.string().min(1),
|
|
475
|
+
messageId: z.string().min(1),
|
|
476
|
+
threadId: z.string().min(1),
|
|
477
|
+
role: assistantMessageRoleSchema,
|
|
478
|
+
status: assistantMessageStatusSchema,
|
|
479
|
+
content: z.string().min(1),
|
|
480
|
+
questionMode: assistantQuestionModeSchema.optional(),
|
|
481
|
+
sourceBoundary: assistantSourceBoundarySchema.optional(),
|
|
482
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
483
|
+
relatedDocumentIds: z.array(z.string().min(1)).default([]),
|
|
484
|
+
workItemId: z.string().min(1).optional(),
|
|
485
|
+
responseToMessageId: z.string().min(1).optional(),
|
|
486
|
+
createdByUserId: z.string().min(1).optional(),
|
|
487
|
+
runnerId: z.string().min(1).optional(),
|
|
488
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
489
|
+
repoFingerprint: z.string().min(1).optional(),
|
|
490
|
+
answeredAt: isoDateTimeSchema.optional(),
|
|
491
|
+
error: z.string().optional()
|
|
492
|
+
});
|
|
493
|
+
var assistantAnswerResultSchema = z.object({
|
|
494
|
+
answer: z.string().trim().min(1),
|
|
495
|
+
sourceBoundary: assistantSourceBoundarySchema.default("localSource"),
|
|
496
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
497
|
+
summary: z.string().optional()
|
|
498
|
+
});
|
|
499
|
+
var knowledgeSearchScopeSchema = z.enum(["project", "organization"]);
|
|
500
|
+
var knowledgeSourceTypeSchema = z.enum(["brainDocument", "generatedArtifact", "runnerSummary", "knowledgeEntity", "knowledgeRelation"]);
|
|
501
|
+
var knowledgeChunkStatusSchema = z.enum(["approved", "synced", "stale"]);
|
|
502
|
+
var knowledgeEntityTypeSchema = z.enum(["project", "system", "component", "domain", "tool", "decision", "feature", "risk", "team", "workflow", "unknown"]);
|
|
503
|
+
var knowledgeRelationTypeSchema = z.enum(["uses", "depends_on", "decides", "supersedes", "touches", "blocks", "implements", "mentions"]);
|
|
504
|
+
var knowledgeIndexFreshnessSchema = z.object({
|
|
505
|
+
status: z.enum(["disabled", "fresh", "stale", "indexing", "degraded"]),
|
|
506
|
+
indexedAt: isoDateTimeSchema.optional(),
|
|
507
|
+
staleReason: z.string().trim().min(1).optional(),
|
|
508
|
+
failedChunkCount: z.number().int().nonnegative().default(0),
|
|
509
|
+
indexedChunkCount: z.number().int().nonnegative().default(0)
|
|
510
|
+
});
|
|
511
|
+
var knowledgeSearchChunkSchema = z.object({
|
|
512
|
+
chunkId: z.string().min(1),
|
|
513
|
+
accountId: z.string().min(1),
|
|
514
|
+
projectId: z.string().min(1),
|
|
515
|
+
sourceType: knowledgeSourceTypeSchema.default("brainDocument"),
|
|
516
|
+
documentId: z.string().min(1),
|
|
517
|
+
documentType: documentTypeSchema,
|
|
518
|
+
title: z.string().min(1),
|
|
519
|
+
repoPath: z.string().min(1),
|
|
520
|
+
revision: z.number().int().nonnegative(),
|
|
521
|
+
sectionHeading: z.string().trim().min(1).optional(),
|
|
522
|
+
chunkText: z.string().trim().min(1).max(12e3),
|
|
523
|
+
chunkHash: z.string().min(1),
|
|
524
|
+
contentHash: z.string().min(1),
|
|
525
|
+
sourceBoundary: assistantSourceBoundarySchema.default("projectBrain"),
|
|
526
|
+
status: knowledgeChunkStatusSchema,
|
|
527
|
+
embeddingModel: z.string().trim().min(1).optional(),
|
|
528
|
+
embedding: z.array(z.number()).max(8192).optional(),
|
|
529
|
+
indexedAt: isoDateTimeSchema.optional(),
|
|
530
|
+
createdAt: isoDateTimeSchema,
|
|
531
|
+
updatedAt: isoDateTimeSchema
|
|
532
|
+
});
|
|
533
|
+
var knowledgeEntitySchema = z.object({
|
|
534
|
+
entityId: z.string().min(1),
|
|
535
|
+
accountId: z.string().min(1),
|
|
536
|
+
projectIds: z.array(z.string().min(1)).min(1),
|
|
537
|
+
name: z.string().trim().min(1).max(200),
|
|
538
|
+
entityType: knowledgeEntityTypeSchema,
|
|
539
|
+
aliases: z.array(z.string().trim().min(1).max(200)).default([]),
|
|
540
|
+
description: z.string().trim().min(1).max(1200).optional(),
|
|
541
|
+
sourceDocumentIds: z.array(z.string().min(1)).default([]),
|
|
542
|
+
confidence: z.number().min(0).max(1).default(0.6),
|
|
543
|
+
createdAt: isoDateTimeSchema,
|
|
544
|
+
updatedAt: isoDateTimeSchema
|
|
545
|
+
});
|
|
546
|
+
var knowledgeRelationEndpointSchema = z.object({
|
|
547
|
+
entityId: z.string().min(1),
|
|
548
|
+
name: z.string().trim().min(1).max(200),
|
|
549
|
+
entityType: knowledgeEntityTypeSchema.default("unknown")
|
|
550
|
+
});
|
|
551
|
+
var knowledgeRelationSchema = z.object({
|
|
552
|
+
relationId: z.string().min(1),
|
|
553
|
+
accountId: z.string().min(1),
|
|
554
|
+
projectIds: z.array(z.string().min(1)).min(1),
|
|
555
|
+
relationType: knowledgeRelationTypeSchema,
|
|
556
|
+
from: knowledgeRelationEndpointSchema,
|
|
557
|
+
to: knowledgeRelationEndpointSchema,
|
|
558
|
+
summary: z.string().trim().min(1).max(1200),
|
|
559
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
560
|
+
confidence: z.number().min(0).max(1).default(0.6),
|
|
561
|
+
createdAt: isoDateTimeSchema,
|
|
562
|
+
updatedAt: isoDateTimeSchema
|
|
563
|
+
});
|
|
564
|
+
var knowledgeSearchRequestSchema = z.object({
|
|
565
|
+
query: z.string().trim().min(1).max(2e3),
|
|
566
|
+
scope: knowledgeSearchScopeSchema.default("project"),
|
|
567
|
+
projectIds: z.array(z.string().min(1)).max(100).optional(),
|
|
568
|
+
topK: z.number().int().min(1).max(20).default(6),
|
|
569
|
+
includeRelations: z.boolean().default(true),
|
|
570
|
+
useSemanticRanking: z.boolean().default(false)
|
|
571
|
+
});
|
|
572
|
+
var knowledgeSearchResultSchema = z.object({
|
|
573
|
+
chunkId: z.string().min(1),
|
|
574
|
+
accountId: z.string().min(1),
|
|
575
|
+
projectId: z.string().min(1),
|
|
576
|
+
documentId: z.string().min(1),
|
|
577
|
+
documentType: documentTypeSchema,
|
|
578
|
+
title: z.string().min(1),
|
|
579
|
+
repoPath: z.string().min(1),
|
|
580
|
+
revision: z.number().int().nonnegative(),
|
|
581
|
+
sectionHeading: z.string().trim().min(1).optional(),
|
|
582
|
+
excerpt: z.string().trim().min(1).max(1200),
|
|
583
|
+
sourceBoundary: assistantSourceBoundarySchema.default("projectBrain"),
|
|
584
|
+
score: z.number().optional(),
|
|
585
|
+
citation: assistantCitationSchema
|
|
586
|
+
});
|
|
587
|
+
var knowledgeSearchResponseSchema = z.object({
|
|
588
|
+
query: z.string().trim().min(1),
|
|
589
|
+
scope: knowledgeSearchScopeSchema,
|
|
590
|
+
results: z.array(knowledgeSearchResultSchema).default([]),
|
|
591
|
+
relations: z.array(knowledgeRelationSchema).default([]),
|
|
592
|
+
freshness: knowledgeIndexFreshnessSchema,
|
|
593
|
+
degradedReason: z.string().trim().min(1).optional()
|
|
594
|
+
});
|
|
595
|
+
var knowledgeDiagramFormatSchema = z.enum(["mermaid"]);
|
|
596
|
+
var knowledgeDiagramRequestSchema = knowledgeSearchRequestSchema.extend({
|
|
597
|
+
format: knowledgeDiagramFormatSchema.default("mermaid"),
|
|
598
|
+
saveAsDocument: z.boolean().default(false),
|
|
599
|
+
title: z.string().trim().min(1).max(160).optional()
|
|
600
|
+
});
|
|
601
|
+
var knowledgeDiagramResultSchema = z.object({
|
|
602
|
+
diagramId: z.string().min(1),
|
|
603
|
+
title: z.string().trim().min(1).max(160),
|
|
604
|
+
format: knowledgeDiagramFormatSchema,
|
|
605
|
+
content: z.string().trim().min(1),
|
|
606
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
607
|
+
sourceResultIds: z.array(z.string().min(1)).default([]),
|
|
608
|
+
documentId: z.string().min(1).optional(),
|
|
609
|
+
freshness: knowledgeIndexFreshnessSchema,
|
|
610
|
+
warnings: z.array(z.string().trim().min(1).max(600)).default([])
|
|
611
|
+
});
|
|
612
|
+
var impactAffectedAreaSchema = z.object({
|
|
613
|
+
name: z.string().trim().min(1),
|
|
614
|
+
description: z.string().trim().min(1).optional()
|
|
615
|
+
});
|
|
616
|
+
var impactPathSchema = z.object({
|
|
617
|
+
repoPath: z.string().trim().min(1),
|
|
618
|
+
reason: z.string().trim().min(1).optional()
|
|
619
|
+
});
|
|
620
|
+
var impactReportItemSchema = baseItemSchema.extend({
|
|
621
|
+
type: z.literal("impactReport"),
|
|
622
|
+
projectId: z.string().min(1),
|
|
623
|
+
impactReportId: z.string().min(1),
|
|
624
|
+
status: impactReportStatusSchema,
|
|
625
|
+
riskLevel: impactRiskLevelSchema.optional(),
|
|
626
|
+
summary: z.string().trim().min(1).optional(),
|
|
627
|
+
affectedAreas: z.array(impactAffectedAreaSchema).default([]),
|
|
628
|
+
likelyPaths: z.array(impactPathSchema).default([]),
|
|
629
|
+
dependencies: z.array(z.string().trim().min(1)).default([]),
|
|
630
|
+
dataSchemaImpact: z.string().trim().min(1).optional(),
|
|
631
|
+
securityPrivacyImpact: z.string().trim().min(1).optional(),
|
|
632
|
+
verificationPlan: z.array(z.string().trim().min(1)).default([]),
|
|
633
|
+
rollbackPlan: z.string().trim().min(1).optional(),
|
|
634
|
+
generatedDraftId: z.string().min(1).optional(),
|
|
635
|
+
sourceWorkItemId: z.string().min(1).optional(),
|
|
636
|
+
documentId: z.string().min(1).optional(),
|
|
637
|
+
documentRevision: z.number().int().nonnegative().optional(),
|
|
638
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
639
|
+
repoFingerprint: z.string().min(1).optional(),
|
|
640
|
+
analyzedRepoRevision: z.number().int().nonnegative().optional(),
|
|
641
|
+
workItemId: z.string().min(1).optional(),
|
|
642
|
+
runnerId: z.string().min(1).optional(),
|
|
643
|
+
createdByUserId: z.string().min(1).optional(),
|
|
644
|
+
completedAt: isoDateTimeSchema.optional(),
|
|
645
|
+
staleReason: z.string().optional(),
|
|
646
|
+
error: z.string().optional()
|
|
647
|
+
});
|
|
648
|
+
var impactPreviewResultSchema = z.object({
|
|
649
|
+
riskLevel: impactRiskLevelSchema,
|
|
650
|
+
summary: z.string().trim().min(1),
|
|
651
|
+
affectedAreas: z.array(impactAffectedAreaSchema).min(1),
|
|
652
|
+
likelyPaths: z.array(impactPathSchema).default([]),
|
|
653
|
+
dependencies: z.array(z.string().trim().min(1)).default([]),
|
|
654
|
+
dataSchemaImpact: z.string().trim().min(1),
|
|
655
|
+
securityPrivacyImpact: z.string().trim().min(1),
|
|
656
|
+
verificationPlan: z.array(z.string().trim().min(1)).min(1),
|
|
657
|
+
rollbackPlan: z.string().trim().min(1),
|
|
658
|
+
analyzedRepoRevision: z.number().int().nonnegative().optional(),
|
|
659
|
+
repoFingerprint: z.string().min(1).optional()
|
|
660
|
+
});
|
|
661
|
+
var activityEventItemSchema = baseItemSchema.extend({
|
|
662
|
+
type: z.literal("activityEvent"),
|
|
663
|
+
projectId: z.string().min(1),
|
|
664
|
+
activityEventId: z.string().min(1),
|
|
665
|
+
eventType: activityEventTypeSchema,
|
|
666
|
+
actorType: activityEventActorSchema,
|
|
667
|
+
actorId: z.string().min(1).optional(),
|
|
668
|
+
status: activityEventStatusSchema,
|
|
669
|
+
summary: z.string().trim().min(1).max(600),
|
|
670
|
+
metadata: activityMetadataSchema,
|
|
671
|
+
idempotencyKey: z.string().min(1),
|
|
672
|
+
occurredAt: isoDateTimeSchema,
|
|
673
|
+
relatedWorkItemId: z.string().min(1).optional(),
|
|
674
|
+
relatedDocumentId: z.string().min(1).optional(),
|
|
675
|
+
generatedDraftId: z.string().min(1).optional(),
|
|
676
|
+
runnerId: z.string().min(1).optional(),
|
|
677
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
678
|
+
runnerLogId: z.string().min(1).optional(),
|
|
679
|
+
commandId: z.string().min(1).optional()
|
|
680
|
+
});
|
|
681
|
+
var activityHandoffExportSchema = z.object({
|
|
682
|
+
markdown: z.string().min(1),
|
|
683
|
+
generatedAt: isoDateTimeSchema,
|
|
684
|
+
eventCount: z.number().int().nonnegative()
|
|
685
|
+
});
|
|
342
686
|
var toolSessionItemSchema = baseItemSchema.extend({
|
|
343
687
|
type: z.literal("toolSession"),
|
|
344
688
|
projectId: z.string().min(1),
|
|
@@ -355,6 +699,10 @@ var toolSessionItemSchema = baseItemSchema.extend({
|
|
|
355
699
|
status: toolSessionStatusSchema,
|
|
356
700
|
createdByUserId: z.string().min(1).optional(),
|
|
357
701
|
runnerId: z.string().min(1).optional(),
|
|
702
|
+
machineId: z.string().min(1).optional(),
|
|
703
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
704
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
705
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
358
706
|
lastWorkItemId: z.string().min(1).optional(),
|
|
359
707
|
lastActivityAt: isoDateTimeSchema,
|
|
360
708
|
messageCount: z.number().int().nonnegative().optional(),
|
|
@@ -387,6 +735,9 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
|
387
735
|
runnerCredentialItemSchema,
|
|
388
736
|
runnerCommandItemSchema,
|
|
389
737
|
planReviewMessageItemSchema,
|
|
738
|
+
assistantMessageItemSchema,
|
|
739
|
+
impactReportItemSchema,
|
|
740
|
+
activityEventItemSchema,
|
|
390
741
|
toolSessionItemSchema
|
|
391
742
|
]);
|
|
392
743
|
|
|
@@ -412,6 +763,19 @@ function isOfficialAmistioApiUrl(value) {
|
|
|
412
763
|
}
|
|
413
764
|
}
|
|
414
765
|
|
|
766
|
+
// ../shared/src/brain-documents.ts
|
|
767
|
+
var controlPlaneRoots = /* @__PURE__ */ new Set(["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"]);
|
|
768
|
+
function isControlPlaneTemplateRepoPath(repoPath) {
|
|
769
|
+
const normalized = normalizeRepoPath(repoPath);
|
|
770
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
771
|
+
const root = segments[0] === "docs" ? segments[1] : segments[0];
|
|
772
|
+
const basename = segments[segments.length - 1]?.toLowerCase();
|
|
773
|
+
return Boolean(root && controlPlaneRoots.has(root) && (basename === "_template.md" || basename === "_template.mdx"));
|
|
774
|
+
}
|
|
775
|
+
function normalizeRepoPath(repoPath) {
|
|
776
|
+
return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
777
|
+
}
|
|
778
|
+
|
|
415
779
|
// ../shared/src/repo-metadata.ts
|
|
416
780
|
import { z as z2 } from "zod";
|
|
417
781
|
var repoLinkMetadataSchema = z2.object({
|
|
@@ -504,7 +868,7 @@ function buildParsedRepositoryCloneUrl({ cloneUrl, host, protocol, rawPath }) {
|
|
|
504
868
|
if (!normalizedHost) {
|
|
505
869
|
throw new Error("Repository URL must include a host.");
|
|
506
870
|
}
|
|
507
|
-
const pathWithoutSlash =
|
|
871
|
+
const pathWithoutSlash = normalizeRepoPath2(rawPath);
|
|
508
872
|
const segments = pathWithoutSlash.split("/").filter(Boolean);
|
|
509
873
|
if (segments.length < 2) {
|
|
510
874
|
throw new Error("Repository URL must include an owner and repository name.");
|
|
@@ -531,7 +895,7 @@ function buildParsedRepositoryCloneUrl({ cloneUrl, host, protocol, rawPath }) {
|
|
|
531
895
|
repoFullName
|
|
532
896
|
};
|
|
533
897
|
}
|
|
534
|
-
function
|
|
898
|
+
function normalizeRepoPath2(rawPath) {
|
|
535
899
|
const withoutLeadingSlash = rawPath.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
536
900
|
if (!withoutLeadingSlash || withoutLeadingSlash.includes("..")) {
|
|
537
901
|
throw new Error("Repository URL path is not supported.");
|
|
@@ -883,17 +1247,20 @@ function credentialKey(accountId, projectId, repositoryLinkId) {
|
|
|
883
1247
|
}
|
|
884
1248
|
|
|
885
1249
|
// src/control-plane.ts
|
|
1250
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
886
1251
|
import { mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
887
1252
|
import path3 from "node:path";
|
|
1253
|
+
import { promisify as promisify2 } from "node:util";
|
|
1254
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
888
1255
|
var controlPlaneFolders = [
|
|
889
|
-
"architecture",
|
|
890
|
-
"context",
|
|
891
|
-
"decisions",
|
|
892
|
-
"features",
|
|
893
|
-
"memory",
|
|
894
|
-
"plans",
|
|
895
|
-
path3.join("prompts", "shared"),
|
|
896
|
-
"workflows"
|
|
1256
|
+
path3.join("docs", "architecture"),
|
|
1257
|
+
path3.join("docs", "context"),
|
|
1258
|
+
path3.join("docs", "decisions"),
|
|
1259
|
+
path3.join("docs", "features"),
|
|
1260
|
+
path3.join("docs", "memory"),
|
|
1261
|
+
path3.join("docs", "plans"),
|
|
1262
|
+
path3.join("docs", "prompts", "shared"),
|
|
1263
|
+
path3.join("docs", "workflows")
|
|
897
1264
|
];
|
|
898
1265
|
async function initControlPlane(rootDir) {
|
|
899
1266
|
const created = [];
|
|
@@ -906,10 +1273,10 @@ async function initControlPlane(rootDir) {
|
|
|
906
1273
|
}
|
|
907
1274
|
await writeIfMissing(
|
|
908
1275
|
path3.join(rootDir, "AGENTS.md"),
|
|
909
|
-
"# Agents\n\
|
|
1276
|
+
"# Agents\n\nThe docs/ control-plane folders are the project brain. Keep docs/architecture, docs/context, docs/decisions, docs/features, docs/memory, docs/plans, docs/prompts, and docs/workflows in sync with product intent.\n"
|
|
910
1277
|
);
|
|
911
|
-
await writeIfMissing(path3.join(rootDir, "context", "product.md"), "# Product Context\n\nDescribe the product direction here.\n");
|
|
912
|
-
await writeIfMissing(path3.join(rootDir, "architecture", "overview.md"), "# Architecture Overview\n\nDescribe the system shape here.\n");
|
|
1278
|
+
await writeIfMissing(path3.join(rootDir, "docs", "context", "product.md"), "# Product Context\n\nDescribe the product direction here.\n");
|
|
1279
|
+
await writeIfMissing(path3.join(rootDir, "docs", "architecture", "overview.md"), "# Architecture Overview\n\nDescribe the system shape here.\n");
|
|
913
1280
|
return created;
|
|
914
1281
|
}
|
|
915
1282
|
async function inspectControlPlane(rootDir) {
|
|
@@ -925,20 +1292,37 @@ async function inspectControlPlane(rootDir) {
|
|
|
925
1292
|
return { present, missing };
|
|
926
1293
|
}
|
|
927
1294
|
async function writeProjectLink(rootDir, metadata) {
|
|
928
|
-
const contextDir = path3.join(rootDir, "context");
|
|
1295
|
+
const contextDir = path3.join(rootDir, "docs", "context");
|
|
929
1296
|
await mkdir3(contextDir, { recursive: true });
|
|
930
1297
|
const filePath = path3.join(contextDir, "amistio-project.md");
|
|
931
1298
|
await writeFile2(filePath, createProjectLinkMarkdown(metadata), "utf8");
|
|
932
1299
|
return filePath;
|
|
933
1300
|
}
|
|
1301
|
+
async function resolvePairingRoot(rootDir, options = {}) {
|
|
1302
|
+
const requestedRoot = path3.resolve(rootDir);
|
|
1303
|
+
const gitRoot = await readGitTopLevel(requestedRoot).catch(() => void 0);
|
|
1304
|
+
if (gitRoot) {
|
|
1305
|
+
return gitRoot;
|
|
1306
|
+
}
|
|
1307
|
+
if (options.explicitRoot) {
|
|
1308
|
+
return requestedRoot;
|
|
1309
|
+
}
|
|
1310
|
+
throw new Error("Run `amistio pair` from inside the repository checkout, or pass --root <repo-root> explicitly.");
|
|
1311
|
+
}
|
|
934
1312
|
async function readProjectLink(rootDir) {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
1313
|
+
const candidatePaths = [
|
|
1314
|
+
path3.join(rootDir, "docs", "context", "amistio-project.md"),
|
|
1315
|
+
path3.join(rootDir, "context", "amistio-project.md")
|
|
1316
|
+
];
|
|
1317
|
+
for (const filePath of candidatePaths) {
|
|
1318
|
+
if (!await exists(filePath)) {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const content = await readFile2(filePath, "utf8");
|
|
1322
|
+
const frontmatter = parseFrontmatter(content);
|
|
1323
|
+
return repoLinkMetadataSchema.parse(frontmatter);
|
|
938
1324
|
}
|
|
939
|
-
|
|
940
|
-
const frontmatter = parseFrontmatter(content);
|
|
941
|
-
return repoLinkMetadataSchema.parse(frontmatter);
|
|
1325
|
+
return void 0;
|
|
942
1326
|
}
|
|
943
1327
|
function parseFrontmatter(content) {
|
|
944
1328
|
if (!content.startsWith("---\n")) {
|
|
@@ -973,6 +1357,14 @@ async function exists(filePath) {
|
|
|
973
1357
|
return false;
|
|
974
1358
|
}
|
|
975
1359
|
}
|
|
1360
|
+
async function readGitTopLevel(rootDir) {
|
|
1361
|
+
const { stdout } = await execFileAsync2("git", ["-C", rootDir, "rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
|
|
1362
|
+
const gitRoot = stdout.trim();
|
|
1363
|
+
if (!gitRoot) {
|
|
1364
|
+
throw new Error("Git top-level path was empty.");
|
|
1365
|
+
}
|
|
1366
|
+
return path3.resolve(gitRoot);
|
|
1367
|
+
}
|
|
976
1368
|
|
|
977
1369
|
// src/api-client.ts
|
|
978
1370
|
import { z as z3 } from "zod";
|
|
@@ -1017,13 +1409,13 @@ var ApiClient = class {
|
|
|
1017
1409
|
}
|
|
1018
1410
|
);
|
|
1019
1411
|
}
|
|
1020
|
-
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300) {
|
|
1412
|
+
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300, metadata = {}) {
|
|
1021
1413
|
return this.request(
|
|
1022
1414
|
`/projects/${projectId}/work-items/claim`,
|
|
1023
1415
|
z3.object({ workItem: workItemSchema.optional() }).transform(({ workItem }) => ({ workItem })),
|
|
1024
1416
|
{
|
|
1025
1417
|
method: "POST",
|
|
1026
|
-
body: JSON.stringify({ runnerId, repositoryLinkId, leaseSeconds })
|
|
1418
|
+
body: JSON.stringify({ runnerId, repositoryLinkId, leaseSeconds, ...metadata })
|
|
1027
1419
|
}
|
|
1028
1420
|
);
|
|
1029
1421
|
}
|
|
@@ -1049,6 +1441,21 @@ var ApiClient = class {
|
|
|
1049
1441
|
{ method: "GET" }
|
|
1050
1442
|
);
|
|
1051
1443
|
}
|
|
1444
|
+
async listAssistantMessages(projectId) {
|
|
1445
|
+
return this.request(
|
|
1446
|
+
`/projects/${projectId}/assistant-messages`,
|
|
1447
|
+
z3.object({ messages: z3.array(assistantMessageItemSchema) }),
|
|
1448
|
+
{ method: "GET" }
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
async listImpactReports(projectId, generatedDraftId) {
|
|
1452
|
+
const query = generatedDraftId ? `?generatedDraftId=${encodeURIComponent(generatedDraftId)}` : "";
|
|
1453
|
+
return this.request(
|
|
1454
|
+
`/projects/${projectId}/impact-reports${query}`,
|
|
1455
|
+
z3.object({ reports: z3.array(impactReportItemSchema) }),
|
|
1456
|
+
{ method: "GET" }
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1052
1459
|
async pushBrainDocuments(projectId, documents) {
|
|
1053
1460
|
return this.request(
|
|
1054
1461
|
`/projects/${projectId}/brain-documents`,
|
|
@@ -1115,6 +1522,16 @@ var ApiClient = class {
|
|
|
1115
1522
|
}
|
|
1116
1523
|
);
|
|
1117
1524
|
}
|
|
1525
|
+
async recordRunnerLog(projectId, input) {
|
|
1526
|
+
return this.request(
|
|
1527
|
+
`/projects/${projectId}/runner-logs`,
|
|
1528
|
+
z3.object({ runnerLog: runnerExecutionLogItemSchema }),
|
|
1529
|
+
{
|
|
1530
|
+
method: "POST",
|
|
1531
|
+
body: JSON.stringify(input)
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1118
1535
|
async listToolSessions(projectId) {
|
|
1119
1536
|
return this.request(
|
|
1120
1537
|
`/projects/${projectId}/tool-sessions`,
|
|
@@ -1142,6 +1559,16 @@ var ApiClient = class {
|
|
|
1142
1559
|
}
|
|
1143
1560
|
);
|
|
1144
1561
|
}
|
|
1562
|
+
async recordActivityEvent(projectId, event) {
|
|
1563
|
+
return this.request(
|
|
1564
|
+
`/projects/${projectId}/activity-events`,
|
|
1565
|
+
z3.object({ event: activityEventItemSchema }),
|
|
1566
|
+
{
|
|
1567
|
+
method: "POST",
|
|
1568
|
+
body: JSON.stringify(event)
|
|
1569
|
+
}
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1145
1572
|
async updateWorkStatus(projectId, workItemId, status, idempotencyKey, runnerId, telemetry = {}) {
|
|
1146
1573
|
return this.request(
|
|
1147
1574
|
`/projects/${projectId}/work-items/${workItemId}/status`,
|
|
@@ -1162,6 +1589,26 @@ var ApiClient = class {
|
|
|
1162
1589
|
}
|
|
1163
1590
|
);
|
|
1164
1591
|
}
|
|
1592
|
+
async submitAssistantResult(projectId, workItemId, result) {
|
|
1593
|
+
return this.request(
|
|
1594
|
+
`/projects/${projectId}/work-items/${workItemId}/assistant-result`,
|
|
1595
|
+
z3.object({ message: assistantMessageItemSchema, answer: assistantMessageItemSchema.optional(), workItem: workItemSchema }),
|
|
1596
|
+
{
|
|
1597
|
+
method: "POST",
|
|
1598
|
+
body: JSON.stringify(result)
|
|
1599
|
+
}
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
async submitImpactPreviewResult(projectId, workItemId, result) {
|
|
1603
|
+
return this.request(
|
|
1604
|
+
`/projects/${projectId}/work-items/${workItemId}/impact-result`,
|
|
1605
|
+
z3.object({ report: impactReportItemSchema, workItem: workItemSchema, implementationWorkItem: workItemSchema.optional() }),
|
|
1606
|
+
{
|
|
1607
|
+
method: "POST",
|
|
1608
|
+
body: JSON.stringify(result)
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1165
1612
|
async request(urlPath, schema, init) {
|
|
1166
1613
|
const response = await fetch(resolveApiUrl(this.options.apiUrl, urlPath), {
|
|
1167
1614
|
...init,
|
|
@@ -1200,12 +1647,16 @@ var toolSessionMutationSchema = z3.object({
|
|
|
1200
1647
|
sessionGroupKey: z3.string().min(1).optional(),
|
|
1201
1648
|
reusePolicy: sessionPolicySchema.optional(),
|
|
1202
1649
|
sessionDecision: sessionDecisionSchema.optional(),
|
|
1203
|
-
closedReason: z3.string().optional()
|
|
1650
|
+
closedReason: z3.string().optional(),
|
|
1651
|
+
machineId: z3.string().min(1).optional(),
|
|
1652
|
+
implementationScopeId: z3.string().min(1).optional(),
|
|
1653
|
+
executionWorktreeKey: z3.string().min(1).optional(),
|
|
1654
|
+
isolationMode: workIsolationModeSchema.optional()
|
|
1204
1655
|
});
|
|
1205
1656
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
1206
1657
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
1207
|
-
const
|
|
1208
|
-
return new URL(`${base}${
|
|
1658
|
+
const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1659
|
+
return new URL(`${base}${path14}`);
|
|
1209
1660
|
}
|
|
1210
1661
|
|
|
1211
1662
|
// src/orchestrator.ts
|
|
@@ -1225,12 +1676,12 @@ async function createOrchestrationPrompt(options) {
|
|
|
1225
1676
|
"",
|
|
1226
1677
|
"## Orchestrator Principle",
|
|
1227
1678
|
"",
|
|
1228
|
-
"The
|
|
1679
|
+
"The docs/ control-plane files are the project brain. They exist so agentic coding sessions can continue from established context, decisions, memory, plans, workflows, and prompts instead of drifting into a fresh interpretation each time.",
|
|
1229
1680
|
"",
|
|
1230
1681
|
"## Repository Rules",
|
|
1231
1682
|
"",
|
|
1232
1683
|
"- Read AGENTS.md first when it exists.",
|
|
1233
|
-
"- Read existing architecture, context, decisions, features, memory, plans, prompts, and workflows before proposing changes.",
|
|
1684
|
+
"- Read existing docs/architecture, docs/context, docs/decisions, docs/features, docs/memory, docs/plans, docs/prompts, and docs/workflows files before proposing changes.",
|
|
1234
1685
|
"- Preserve existing intent. Add the next useful orchestration layer instead of replacing old content wholesale.",
|
|
1235
1686
|
"- Prefer small additive edits, dated notes, explicit decisions, focused plans, and executable prompts.",
|
|
1236
1687
|
"- Do not modify product source code unless the user's goal explicitly asks for implementation.",
|
|
@@ -1241,14 +1692,14 @@ async function createOrchestrationPrompt(options) {
|
|
|
1241
1692
|
"## Control-Plane Areas",
|
|
1242
1693
|
"",
|
|
1243
1694
|
"- AGENTS.md for agent operating rules.",
|
|
1244
|
-
"- architecture/**/*.md for system shape and boundaries.",
|
|
1245
|
-
"- context/**/*.md for product, stack, and implementation context.",
|
|
1246
|
-
"- decisions/**/*.md for ADRs and durable choices.",
|
|
1247
|
-
"- features/**/*.md for behavior specs and acceptance criteria.",
|
|
1248
|
-
"- memory/**/*.md for lessons, patterns, and known pitfalls.",
|
|
1249
|
-
"- plans/**/*.md for near-term execution plans.",
|
|
1250
|
-
"- prompts/**/*.md for model-agnostic implementation prompts.",
|
|
1251
|
-
"- workflows/**/*.md for repeatable procedures.",
|
|
1695
|
+
"- docs/architecture/**/*.md for system shape and boundaries.",
|
|
1696
|
+
"- docs/context/**/*.md for product, stack, and implementation context.",
|
|
1697
|
+
"- docs/decisions/**/*.md for ADRs and durable choices.",
|
|
1698
|
+
"- docs/features/**/*.md for behavior specs and acceptance criteria.",
|
|
1699
|
+
"- docs/memory/**/*.md for lessons, patterns, and known pitfalls.",
|
|
1700
|
+
"- docs/plans/**/*.md for near-term execution plans.",
|
|
1701
|
+
"- docs/prompts/**/*.md for model-agnostic implementation prompts.",
|
|
1702
|
+
"- docs/workflows/**/*.md for repeatable procedures.",
|
|
1252
1703
|
"",
|
|
1253
1704
|
"## Current Control-Plane Folder Status",
|
|
1254
1705
|
"",
|
|
@@ -1951,6 +2402,221 @@ async function readRunnerDaemonMetadataFile(filePath) {
|
|
|
1951
2402
|
}
|
|
1952
2403
|
}
|
|
1953
2404
|
|
|
2405
|
+
// src/runner-service.ts
|
|
2406
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2407
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2408
|
+
import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2409
|
+
import os4 from "node:os";
|
|
2410
|
+
import path7 from "node:path";
|
|
2411
|
+
function detectRunnerServicePlatform(platform = process.platform) {
|
|
2412
|
+
if (platform === "darwin") return "launchd";
|
|
2413
|
+
if (platform === "linux") return "systemd";
|
|
2414
|
+
return "unsupported";
|
|
2415
|
+
}
|
|
2416
|
+
function createRunnerServiceDescriptor(input) {
|
|
2417
|
+
const platform = input.platform ?? detectRunnerServicePlatform();
|
|
2418
|
+
if (platform === "unsupported") {
|
|
2419
|
+
throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
2420
|
+
}
|
|
2421
|
+
const homeDir = input.homeDir ?? os4.homedir();
|
|
2422
|
+
const serviceName = runnerServiceName(input);
|
|
2423
|
+
const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
|
|
2424
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2425
|
+
const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
|
|
2426
|
+
const logPath = path7.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
|
|
2427
|
+
const metadata = {
|
|
2428
|
+
schemaVersion: 1,
|
|
2429
|
+
accountId: input.accountId,
|
|
2430
|
+
projectId: input.projectId,
|
|
2431
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
2432
|
+
runnerId: input.runnerId,
|
|
2433
|
+
rootDir: path7.resolve(input.rootDir),
|
|
2434
|
+
apiUrl: input.apiUrl,
|
|
2435
|
+
serviceName,
|
|
2436
|
+
serviceFilePath,
|
|
2437
|
+
platform,
|
|
2438
|
+
status: "installed",
|
|
2439
|
+
createdAt: now,
|
|
2440
|
+
updatedAt: now,
|
|
2441
|
+
args: input.args
|
|
2442
|
+
};
|
|
2443
|
+
return {
|
|
2444
|
+
metadata,
|
|
2445
|
+
content: platform === "launchd" ? createLaunchdPlist({ command, label: serviceName, logPath, rootDir: metadata.rootDir }) : createSystemdUnit({ command, description: `Amistio runner ${input.runnerId}`, logPath, rootDir: metadata.rootDir })
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
async function installRunnerService(input, options = {}) {
|
|
2449
|
+
const descriptor = createRunnerServiceDescriptor(input);
|
|
2450
|
+
await mkdir6(path7.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
|
|
2451
|
+
await mkdir6(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
|
|
2452
|
+
await writeFile6(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
|
|
2453
|
+
await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
|
|
2454
|
+
if (options.activate !== false) {
|
|
2455
|
+
const activation = await activateRunnerService(descriptor.metadata);
|
|
2456
|
+
if (!activation.succeeded) {
|
|
2457
|
+
throw new Error(`Startup service file was written to ${descriptor.metadata.serviceFilePath}, but activation failed: ${activation.message}`);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return descriptor.metadata;
|
|
2461
|
+
}
|
|
2462
|
+
async function removeRunnerService(input) {
|
|
2463
|
+
const metadata = await readRunnerServiceMetadata(input, input.metadataDir);
|
|
2464
|
+
if (!metadata) {
|
|
2465
|
+
return void 0;
|
|
2466
|
+
}
|
|
2467
|
+
await deactivateRunnerService(metadata).catch(() => void 0);
|
|
2468
|
+
await rm2(metadata.serviceFilePath, { force: true });
|
|
2469
|
+
await rm2(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
|
|
2470
|
+
return { ...metadata, status: "removed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2471
|
+
}
|
|
2472
|
+
async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
2473
|
+
try {
|
|
2474
|
+
const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
|
|
2475
|
+
if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
|
|
2476
|
+
return void 0;
|
|
2477
|
+
}
|
|
2478
|
+
return parsed;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
2484
|
+
await mkdir6(metadataDir, { recursive: true });
|
|
2485
|
+
await writeFile6(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
2486
|
+
}
|
|
2487
|
+
async function runnerServiceRuntimeStatus(metadata) {
|
|
2488
|
+
if (metadata.platform === "launchd") {
|
|
2489
|
+
const target = launchdTarget(metadata);
|
|
2490
|
+
const result2 = await runProcess("launchctl", ["print", target], 5e3);
|
|
2491
|
+
return result2.exitCode === 0 ? "loaded" : "not loaded";
|
|
2492
|
+
}
|
|
2493
|
+
const result = await runProcess("systemctl", ["--user", "is-active", metadata.serviceName], 5e3);
|
|
2494
|
+
return result.exitCode === 0 ? result.output.trim() || "active" : "not active";
|
|
2495
|
+
}
|
|
2496
|
+
async function activateRunnerService(metadata) {
|
|
2497
|
+
if (metadata.platform === "launchd") {
|
|
2498
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 5e3).catch(() => void 0);
|
|
2499
|
+
const result = await runProcess("launchctl", ["bootstrap", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2500
|
+
return result.exitCode === 0 ? { succeeded: true, message: "launchd service loaded." } : { succeeded: false, message: result.output || `launchctl exited with ${result.exitCode}.` };
|
|
2501
|
+
}
|
|
2502
|
+
const reload = await runProcess("systemctl", ["--user", "daemon-reload"], 1e4);
|
|
2503
|
+
if (reload.exitCode !== 0) {
|
|
2504
|
+
return { succeeded: false, message: reload.output || `systemctl daemon-reload exited with ${reload.exitCode}.` };
|
|
2505
|
+
}
|
|
2506
|
+
const enable = await runProcess("systemctl", ["--user", "enable", "--now", metadata.serviceName], 2e4);
|
|
2507
|
+
return enable.exitCode === 0 ? { succeeded: true, message: "systemd user service enabled." } : { succeeded: false, message: enable.output || `systemctl enable exited with ${enable.exitCode}.` };
|
|
2508
|
+
}
|
|
2509
|
+
async function deactivateRunnerService(metadata) {
|
|
2510
|
+
if (metadata.platform === "launchd") {
|
|
2511
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
await runProcess("systemctl", ["--user", "disable", "--now", metadata.serviceName], 2e4);
|
|
2515
|
+
await runProcess("systemctl", ["--user", "daemon-reload"], 1e4).catch(() => void 0);
|
|
2516
|
+
}
|
|
2517
|
+
function createLaunchdPlist(input) {
|
|
2518
|
+
const commandItems = input.command.map((item) => ` <string>${xmlEscape(item)}</string>`).join("\n");
|
|
2519
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2520
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2521
|
+
<plist version="1.0">
|
|
2522
|
+
<dict>
|
|
2523
|
+
<key>Label</key>
|
|
2524
|
+
<string>${xmlEscape(input.label)}</string>
|
|
2525
|
+
<key>ProgramArguments</key>
|
|
2526
|
+
<array>
|
|
2527
|
+
${commandItems}
|
|
2528
|
+
</array>
|
|
2529
|
+
<key>WorkingDirectory</key>
|
|
2530
|
+
<string>${xmlEscape(input.rootDir)}</string>
|
|
2531
|
+
<key>EnvironmentVariables</key>
|
|
2532
|
+
<dict>
|
|
2533
|
+
<key>AMISTIO_RUNNER_MODE</key>
|
|
2534
|
+
<string>background</string>
|
|
2535
|
+
</dict>
|
|
2536
|
+
<key>RunAtLoad</key>
|
|
2537
|
+
<true/>
|
|
2538
|
+
<key>KeepAlive</key>
|
|
2539
|
+
<true/>
|
|
2540
|
+
<key>StandardOutPath</key>
|
|
2541
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2542
|
+
<key>StandardErrorPath</key>
|
|
2543
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2544
|
+
</dict>
|
|
2545
|
+
</plist>
|
|
2546
|
+
`;
|
|
2547
|
+
}
|
|
2548
|
+
function createSystemdUnit(input) {
|
|
2549
|
+
return `[Unit]
|
|
2550
|
+
Description=${input.description}
|
|
2551
|
+
|
|
2552
|
+
[Service]
|
|
2553
|
+
Type=simple
|
|
2554
|
+
WorkingDirectory=${systemdEscape(input.rootDir)}
|
|
2555
|
+
Environment=AMISTIO_RUNNER_MODE=background
|
|
2556
|
+
ExecStart=${input.command.map(systemdEscape).join(" ")}
|
|
2557
|
+
Restart=always
|
|
2558
|
+
RestartSec=5
|
|
2559
|
+
StandardOutput=append:${systemdEscape(input.logPath)}
|
|
2560
|
+
StandardError=append:${systemdEscape(input.logPath)}
|
|
2561
|
+
|
|
2562
|
+
[Install]
|
|
2563
|
+
WantedBy=default.target
|
|
2564
|
+
`;
|
|
2565
|
+
}
|
|
2566
|
+
function runnerServiceFilePath(platform, serviceName, homeDir) {
|
|
2567
|
+
if (platform === "launchd") {
|
|
2568
|
+
return path7.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
|
|
2569
|
+
}
|
|
2570
|
+
return path7.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
|
|
2571
|
+
}
|
|
2572
|
+
function runnerServiceMetadataPath(input, metadataDir) {
|
|
2573
|
+
return path7.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
|
|
2574
|
+
}
|
|
2575
|
+
function runnerServiceName(input) {
|
|
2576
|
+
return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
|
|
2577
|
+
}
|
|
2578
|
+
function runnerServiceKey(input) {
|
|
2579
|
+
return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
2580
|
+
}
|
|
2581
|
+
function launchdDomain() {
|
|
2582
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
2583
|
+
return uid === void 0 ? "gui/0" : `gui/${uid}`;
|
|
2584
|
+
}
|
|
2585
|
+
function launchdTarget(metadata) {
|
|
2586
|
+
return `${launchdDomain()}/${metadata.serviceName}`;
|
|
2587
|
+
}
|
|
2588
|
+
function xmlEscape(value) {
|
|
2589
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
2590
|
+
}
|
|
2591
|
+
function systemdEscape(value) {
|
|
2592
|
+
return value.includes(" ") || value.includes(" ") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"')}"` : value;
|
|
2593
|
+
}
|
|
2594
|
+
function runProcess(command, args, timeoutMs) {
|
|
2595
|
+
return new Promise((resolve) => {
|
|
2596
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2597
|
+
let output = "";
|
|
2598
|
+
const timeout = setTimeout(() => {
|
|
2599
|
+
output += `Timed out while running ${command}.
|
|
2600
|
+
`;
|
|
2601
|
+
child.kill("SIGTERM");
|
|
2602
|
+
}, timeoutMs);
|
|
2603
|
+
child.stdout?.on("data", (chunk) => {
|
|
2604
|
+
output += chunk.toString("utf8");
|
|
2605
|
+
});
|
|
2606
|
+
child.stderr?.on("data", (chunk) => {
|
|
2607
|
+
output += chunk.toString("utf8");
|
|
2608
|
+
});
|
|
2609
|
+
child.on("error", (error) => {
|
|
2610
|
+
clearTimeout(timeout);
|
|
2611
|
+
resolve({ exitCode: 1, output: error.message });
|
|
2612
|
+
});
|
|
2613
|
+
child.on("close", (code) => {
|
|
2614
|
+
clearTimeout(timeout);
|
|
2615
|
+
resolve({ exitCode: code ?? 1, output: output.trim() });
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
|
|
1954
2620
|
// src/session-policy.ts
|
|
1955
2621
|
var maxIdleMs = 24 * 60 * 60 * 1e3;
|
|
1956
2622
|
var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -2020,9 +2686,22 @@ function sessionIneligibleReason(session, input, now) {
|
|
|
2020
2686
|
if (session.repositoryLinkId && session.repositoryLinkId !== input.repositoryLinkId) {
|
|
2021
2687
|
return "repository link mismatch";
|
|
2022
2688
|
}
|
|
2689
|
+
if (session.machineId && session.machineId !== input.machineId) {
|
|
2690
|
+
return "session is scoped to another machine";
|
|
2691
|
+
}
|
|
2023
2692
|
if (session.resumabilityScope === "localMachine" && session.runnerId && session.runnerId !== input.runnerId) {
|
|
2024
2693
|
return "session is scoped to another runner machine";
|
|
2025
2694
|
}
|
|
2695
|
+
const requestedByUserId = input.workItem.requestedByUserId ?? input.workItem.requestedBy;
|
|
2696
|
+
if (session.createdByUserId && session.createdByUserId !== requestedByUserId) {
|
|
2697
|
+
return "session belongs to another requester";
|
|
2698
|
+
}
|
|
2699
|
+
if (input.workItem.implementationScopeId && session.implementationScopeId !== input.workItem.implementationScopeId) {
|
|
2700
|
+
return "implementation scope mismatch";
|
|
2701
|
+
}
|
|
2702
|
+
if (input.workItem.executionWorktreeKey && session.executionWorktreeKey !== input.workItem.executionWorktreeKey) {
|
|
2703
|
+
return "worktree scope mismatch";
|
|
2704
|
+
}
|
|
2026
2705
|
if (session.status === "closed" || session.status === "archived" || session.status === "blocked" || session.status === "unavailable") {
|
|
2027
2706
|
return `session is ${session.status}`;
|
|
2028
2707
|
}
|
|
@@ -2072,13 +2751,15 @@ function tokens(value) {
|
|
|
2072
2751
|
}
|
|
2073
2752
|
|
|
2074
2753
|
// src/sync.ts
|
|
2075
|
-
import { mkdir as
|
|
2076
|
-
import
|
|
2077
|
-
var
|
|
2754
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
2755
|
+
import path8 from "node:path";
|
|
2756
|
+
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2757
|
+
var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
|
|
2078
2758
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
2079
2759
|
const localDocuments = await readLocalSyncedDocuments(rootDir);
|
|
2080
|
-
const
|
|
2081
|
-
const
|
|
2760
|
+
const normalizedWebDocuments = webDocuments.map((document) => ({ ...document, repoPath: canonicalControlPlaneRepoPath(document.repoPath) }));
|
|
2761
|
+
const webByDocumentId = new Map(normalizedWebDocuments.map((document) => [document.documentId, document]));
|
|
2762
|
+
const pullableWebDocuments = normalizedWebDocuments.filter((document) => !isControlPlaneTemplateRepoPath(document.repoPath) && (document.status === "approved" || document.syncState === "approved" || document.syncState === "synced"));
|
|
2082
2763
|
const localByDocumentId = new Map(localDocuments.map((document) => [document.frontmatter.amistioDocumentId, document]));
|
|
2083
2764
|
const items = [];
|
|
2084
2765
|
for (const localDocument of localDocuments) {
|
|
@@ -2150,14 +2831,18 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2150
2831
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
2151
2832
|
const documents = [];
|
|
2152
2833
|
for (const fullPath of markdownFiles) {
|
|
2153
|
-
const raw = await
|
|
2834
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2154
2835
|
const parsed = parseSyncedMarkdown(raw);
|
|
2155
2836
|
if (!parsed) {
|
|
2156
2837
|
continue;
|
|
2157
2838
|
}
|
|
2839
|
+
const repoPath = toRepoPath(rootDir, fullPath);
|
|
2840
|
+
if (isControlPlaneTemplateRepoPath(repoPath)) {
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2158
2843
|
documents.push({
|
|
2159
2844
|
fullPath,
|
|
2160
|
-
repoPath
|
|
2845
|
+
repoPath,
|
|
2161
2846
|
frontmatter: parsed.frontmatter,
|
|
2162
2847
|
content: parsed.content,
|
|
2163
2848
|
contentHash: sha256ContentHash(parsed.content)
|
|
@@ -2168,10 +2853,15 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2168
2853
|
async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
2169
2854
|
const result = { written: [], skipped: [], conflicts: [] };
|
|
2170
2855
|
for (const inputDocument of documents) {
|
|
2171
|
-
const
|
|
2856
|
+
const parsedDocument = brainDocumentItemSchema.parse(inputDocument);
|
|
2857
|
+
const document = { ...parsedDocument, repoPath: canonicalControlPlaneRepoPath(parsedDocument.repoPath) };
|
|
2172
2858
|
const fullPath = safeRepoPath(rootDir, document.repoPath);
|
|
2859
|
+
if (isControlPlaneTemplateRepoPath(document.repoPath)) {
|
|
2860
|
+
result.skipped.push(document.repoPath);
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2173
2863
|
if (!isControlPlanePath(document.repoPath)) {
|
|
2174
|
-
result.conflicts.push(`${document.repoPath}: refusing to write outside
|
|
2864
|
+
result.conflicts.push(`${document.repoPath}: refusing to write outside docs control-plane folders`);
|
|
2175
2865
|
continue;
|
|
2176
2866
|
}
|
|
2177
2867
|
const existing = await readExistingSyncedDocument(fullPath);
|
|
@@ -2191,8 +2881,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
2191
2881
|
result.skipped.push(document.repoPath);
|
|
2192
2882
|
continue;
|
|
2193
2883
|
}
|
|
2194
|
-
await
|
|
2195
|
-
await
|
|
2884
|
+
await mkdir7(path8.dirname(fullPath), { recursive: true });
|
|
2885
|
+
await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
|
|
2196
2886
|
result.written.push(document.repoPath);
|
|
2197
2887
|
}
|
|
2198
2888
|
return result;
|
|
@@ -2253,7 +2943,7 @@ function parseSyncedMarkdown(content) {
|
|
|
2253
2943
|
}
|
|
2254
2944
|
async function readExistingSyncedDocument(fullPath) {
|
|
2255
2945
|
try {
|
|
2256
|
-
const raw = await
|
|
2946
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2257
2947
|
const parsed = parseSyncedMarkdown(raw);
|
|
2258
2948
|
if (!parsed) {
|
|
2259
2949
|
return { exists: true };
|
|
@@ -2278,7 +2968,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
2278
2968
|
async function findMarkdownFiles(rootDir) {
|
|
2279
2969
|
const files = [];
|
|
2280
2970
|
for (const syncRoot of syncRoots) {
|
|
2281
|
-
const fullRoot =
|
|
2971
|
+
const fullRoot = path8.join(rootDir, syncRoot);
|
|
2282
2972
|
if (!await exists2(fullRoot)) {
|
|
2283
2973
|
continue;
|
|
2284
2974
|
}
|
|
@@ -2288,7 +2978,7 @@ async function findMarkdownFiles(rootDir) {
|
|
|
2288
2978
|
}
|
|
2289
2979
|
async function walkMarkdownFiles(directory, files) {
|
|
2290
2980
|
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
2291
|
-
const fullPath =
|
|
2981
|
+
const fullPath = path8.join(directory, entry.name);
|
|
2292
2982
|
if (entry.isDirectory()) {
|
|
2293
2983
|
await walkMarkdownFiles(fullPath, files);
|
|
2294
2984
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2297,30 +2987,38 @@ async function walkMarkdownFiles(directory, files) {
|
|
|
2297
2987
|
}
|
|
2298
2988
|
}
|
|
2299
2989
|
function safeRepoPath(rootDir, repoPath) {
|
|
2300
|
-
if (
|
|
2990
|
+
if (path8.isAbsolute(repoPath)) {
|
|
2301
2991
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
2302
2992
|
}
|
|
2303
|
-
const normalized =
|
|
2304
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
2993
|
+
const normalized = path8.normalize(repoPath);
|
|
2994
|
+
if (normalized === ".." || normalized.startsWith(`..${path8.sep}`)) {
|
|
2305
2995
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2306
2996
|
}
|
|
2307
|
-
const root =
|
|
2308
|
-
const fullPath =
|
|
2309
|
-
if (!fullPath.startsWith(`${root}${
|
|
2997
|
+
const root = path8.resolve(rootDir);
|
|
2998
|
+
const fullPath = path8.resolve(root, normalized);
|
|
2999
|
+
if (!fullPath.startsWith(`${root}${path8.sep}`)) {
|
|
2310
3000
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2311
3001
|
}
|
|
2312
3002
|
return fullPath;
|
|
2313
3003
|
}
|
|
2314
3004
|
function isControlPlanePath(repoPath) {
|
|
2315
|
-
const normalized =
|
|
2316
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
3005
|
+
const normalized = path8.normalize(repoPath);
|
|
3006
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
|
|
3007
|
+
}
|
|
3008
|
+
function canonicalControlPlaneRepoPath(repoPath) {
|
|
3009
|
+
const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
3010
|
+
const [firstSegment] = normalized.split("/");
|
|
3011
|
+
if (firstSegment && legacySyncRoots.includes(firstSegment)) {
|
|
3012
|
+
return `docs/${normalized}`;
|
|
3013
|
+
}
|
|
3014
|
+
return normalized;
|
|
2317
3015
|
}
|
|
2318
3016
|
function toRepoPath(rootDir, fullPath) {
|
|
2319
|
-
return
|
|
3017
|
+
return path8.relative(rootDir, fullPath).split(path8.sep).join("/");
|
|
2320
3018
|
}
|
|
2321
3019
|
function inferTitle(content, repoPath) {
|
|
2322
3020
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
2323
|
-
return heading ||
|
|
3021
|
+
return heading || path8.basename(repoPath, path8.extname(repoPath));
|
|
2324
3022
|
}
|
|
2325
3023
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
2326
3024
|
return {
|
|
@@ -2341,9 +3039,9 @@ async function exists2(filePath) {
|
|
|
2341
3039
|
}
|
|
2342
3040
|
|
|
2343
3041
|
// src/tool-session-store.ts
|
|
2344
|
-
import { mkdir as
|
|
2345
|
-
import
|
|
2346
|
-
import
|
|
3042
|
+
import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
|
|
3043
|
+
import os5 from "node:os";
|
|
3044
|
+
import path9 from "node:path";
|
|
2347
3045
|
var LocalToolSessionStore = class {
|
|
2348
3046
|
constructor(filePath = defaultSessionStorePath()) {
|
|
2349
3047
|
this.filePath = filePath;
|
|
@@ -2357,12 +3055,12 @@ var LocalToolSessionStore = class {
|
|
|
2357
3055
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
2358
3056
|
const data = await this.read();
|
|
2359
3057
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2360
|
-
await
|
|
2361
|
-
await
|
|
3058
|
+
await mkdir8(path9.dirname(this.filePath), { recursive: true });
|
|
3059
|
+
await writeFile8(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
2362
3060
|
}
|
|
2363
3061
|
async read() {
|
|
2364
3062
|
try {
|
|
2365
|
-
return JSON.parse(await
|
|
3063
|
+
return JSON.parse(await readFile6(this.filePath, "utf8"));
|
|
2366
3064
|
} catch {
|
|
2367
3065
|
return {};
|
|
2368
3066
|
}
|
|
@@ -2370,17 +3068,21 @@ var LocalToolSessionStore = class {
|
|
|
2370
3068
|
};
|
|
2371
3069
|
function defaultSessionStorePath() {
|
|
2372
3070
|
if (process.platform === "darwin") {
|
|
2373
|
-
return
|
|
3071
|
+
return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
2374
3072
|
}
|
|
2375
3073
|
if (process.platform === "win32") {
|
|
2376
|
-
return
|
|
3074
|
+
return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
|
|
2377
3075
|
}
|
|
2378
|
-
return
|
|
3076
|
+
return path9.join(process.env.XDG_STATE_HOME ?? path9.join(os5.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
2379
3077
|
}
|
|
2380
3078
|
|
|
2381
3079
|
// src/work-runner.ts
|
|
2382
3080
|
var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
|
|
2383
3081
|
var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
|
|
3082
|
+
var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
|
|
3083
|
+
var assistantAnswerEnd = "AMISTIO_ASSISTANT_ANSWER_END";
|
|
3084
|
+
var impactPreviewStart = "AMISTIO_IMPACT_PREVIEW_START";
|
|
3085
|
+
var impactPreviewEnd = "AMISTIO_IMPACT_PREVIEW_END";
|
|
2384
3086
|
function createWorkExecutionPrompt(workItem, context) {
|
|
2385
3087
|
if (workItem.workKind === "brainGeneration") {
|
|
2386
3088
|
return createBrainGenerationPrompt(workItem);
|
|
@@ -2388,6 +3090,12 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2388
3090
|
if (workItem.workKind === "planRevision") {
|
|
2389
3091
|
return createPlanRevisionPrompt(workItem, context?.planRevision);
|
|
2390
3092
|
}
|
|
3093
|
+
if (workItem.workKind === "assistantQuestion") {
|
|
3094
|
+
return createAssistantQuestionPrompt(workItem, context?.assistantQuestion);
|
|
3095
|
+
}
|
|
3096
|
+
if (workItem.workKind === "impactPreview") {
|
|
3097
|
+
return createImpactPreviewPrompt(workItem, context?.impactPreview);
|
|
3098
|
+
}
|
|
2391
3099
|
return [
|
|
2392
3100
|
"# Amistio Work Execution",
|
|
2393
3101
|
"",
|
|
@@ -2399,11 +3107,15 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2399
3107
|
`Title: ${workItem.title}`,
|
|
2400
3108
|
`Work item ID: ${workItem.workItemId}`,
|
|
2401
3109
|
`Project ID: ${workItem.projectId}`,
|
|
3110
|
+
`Implementation scope: ${workItem.implementationScopeId ?? workItem.controllingAdrId ?? "work-item"}`,
|
|
3111
|
+
`Execution branch: ${workItem.executionBranch ?? "managed by Amistio CLI"}`,
|
|
3112
|
+
`Execution worktree key: ${workItem.executionWorktreeKey ?? "managed by Amistio CLI"}`,
|
|
2402
3113
|
"",
|
|
2403
3114
|
"## Rules",
|
|
2404
3115
|
"",
|
|
2405
3116
|
"- Read AGENTS.md first when it exists.",
|
|
2406
|
-
"- Read the relevant
|
|
3117
|
+
"- Read AGENTS.md and the relevant docs/ control-plane files before implementation so the work stays aligned with existing direction.",
|
|
3118
|
+
"- Treat the current working directory as the Amistio-managed implementation worktree; do not switch back to the paired primary checkout for mutating work.",
|
|
2407
3119
|
"- Keep changes focused on this work item.",
|
|
2408
3120
|
"- Preserve old decisions, plans, memory, and prompts unless the work item explicitly supersedes them.",
|
|
2409
3121
|
"- Do not commit changes.",
|
|
@@ -2412,51 +3124,181 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2412
3124
|
"- Run relevant verification commands when feasible and summarize results."
|
|
2413
3125
|
].join("\n");
|
|
2414
3126
|
}
|
|
2415
|
-
function
|
|
2416
|
-
const
|
|
3127
|
+
function createImpactPreviewPrompt(workItem, context) {
|
|
3128
|
+
const implementationPrompt = context?.implementationPrompt;
|
|
3129
|
+
const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
|
|
3130
|
+
`### ${document.title}`,
|
|
3131
|
+
`documentId: ${document.documentId}`,
|
|
3132
|
+
`documentType: ${document.documentType}`,
|
|
3133
|
+
`repoPath: ${document.repoPath}`,
|
|
3134
|
+
`revision: ${document.revision}`,
|
|
3135
|
+
document.content.slice(0, 3e3)
|
|
3136
|
+
].join("\n")).join("\n\n");
|
|
2417
3137
|
return [
|
|
2418
|
-
"# Amistio
|
|
3138
|
+
"# Amistio Implementation Impact Preview",
|
|
2419
3139
|
"",
|
|
2420
3140
|
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
2421
|
-
"
|
|
3141
|
+
"Analyze the likely impact of the approved implementation prompt. This is a preview-only task.",
|
|
3142
|
+
"Do not modify files, do not create branches, do not run implementation commands, and do not commit changes.",
|
|
2422
3143
|
"",
|
|
2423
3144
|
"## Work Item",
|
|
2424
3145
|
"",
|
|
2425
3146
|
`Title: ${workItem.title}`,
|
|
2426
3147
|
`Work item ID: ${workItem.workItemId}`,
|
|
2427
3148
|
`Project ID: ${workItem.projectId}`,
|
|
2428
|
-
`
|
|
2429
|
-
`
|
|
3149
|
+
`Generated draft ID: ${workItem.generatedDraftId ?? "unknown"}`,
|
|
3150
|
+
`Impact report ID: ${workItem.impactReportId ?? "unknown"}`,
|
|
3151
|
+
`Analyzed repo revision: ${context?.analyzedRepoRevision ?? "unknown"}`,
|
|
2430
3152
|
"",
|
|
2431
|
-
"##
|
|
3153
|
+
"## Approved Implementation Prompt",
|
|
2432
3154
|
"",
|
|
2433
|
-
|
|
3155
|
+
implementationPrompt ? implementationPrompt.content : workItem.sourceWish ?? workItem.title,
|
|
2434
3156
|
"",
|
|
2435
|
-
"##
|
|
3157
|
+
"## Approved Project Brain Context",
|
|
2436
3158
|
"",
|
|
2437
|
-
|
|
3159
|
+
approvedContext || "No approved project-brain records were loaded. Inspect the local repository and explain the gap in the report.",
|
|
3160
|
+
"",
|
|
3161
|
+
"## Report Requirements",
|
|
3162
|
+
"",
|
|
3163
|
+
"- Identify affected product areas and likely modules/files using safe path summaries.",
|
|
3164
|
+
"- Assign riskLevel as low, medium, high, or critical with operational reasoning in summary.",
|
|
3165
|
+
"- Include data/schema/migration impact, security/privacy/access-control considerations, verification plan, and rollback plan.",
|
|
3166
|
+
"- Keep repository source and secrets local. Do not include raw source dumps, credentials, local secret paths, or provider session references.",
|
|
3167
|
+
"- If you inspect files, cite paths only through likelyPaths and concise reasons.",
|
|
2438
3168
|
"",
|
|
2439
3169
|
"## Output Contract",
|
|
2440
3170
|
"",
|
|
2441
|
-
"Print exactly one JSON object between the markers below
|
|
2442
|
-
'The artifact must use documentType "plan", keep the plan under plans/, and include the complete revised plan content.',
|
|
3171
|
+
"Print exactly one JSON object between the markers below. The CLI will submit only this structured report back to Amistio.",
|
|
2443
3172
|
"",
|
|
2444
|
-
|
|
2445
|
-
'{"
|
|
2446
|
-
|
|
3173
|
+
impactPreviewStart,
|
|
3174
|
+
'{"riskLevel":"medium","summary":"Touches the workspace command flow and runner claim gate.","affectedAreas":[{"name":"Workspace UI","description":"Shows impact before execution"}],"likelyPaths":[{"repoPath":"src/apps/web/components/workspace-client.tsx","reason":"Workspace action wiring"}],"dependencies":["local runner"],"dataSchemaImpact":"Adds an impact report project item.","securityPrivacyImpact":"Source remains local; SaaS stores summaries and paths only.","verificationPlan":["Run shared, web, and CLI tests","Run root verify"],"rollbackPlan":"Disable the impact gate and remove queued preview work if needed."}',
|
|
3175
|
+
impactPreviewEnd,
|
|
2447
3176
|
"",
|
|
2448
|
-
"Do not put Markdown fences around the markers. Do not
|
|
3177
|
+
"Do not put Markdown fences around the markers. Do not implement the work."
|
|
2449
3178
|
].join("\n");
|
|
2450
3179
|
}
|
|
2451
|
-
function
|
|
2452
|
-
const
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
3180
|
+
function createAssistantQuestionPrompt(workItem, context) {
|
|
3181
|
+
const question = context?.question.content ?? workItem.sourceWish ?? workItem.title;
|
|
3182
|
+
const priorMessages = (context?.messages ?? []).filter((message) => message.messageId !== context?.question.messageId).slice(-12).map((message) => `- ${message.role} / ${message.status}: ${message.content}`).join("\n");
|
|
3183
|
+
const brainContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 12).map((document) => [
|
|
3184
|
+
`### ${document.title}`,
|
|
3185
|
+
`documentId: ${document.documentId}`,
|
|
3186
|
+
`repoPath: ${document.repoPath}`,
|
|
3187
|
+
`revision: ${document.revision}`,
|
|
3188
|
+
document.content.slice(0, 2400)
|
|
3189
|
+
].join("\n")).join("\n\n");
|
|
3190
|
+
return [
|
|
3191
|
+
"# Amistio Project Knowledge Assistant",
|
|
3192
|
+
"",
|
|
3193
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
3194
|
+
"Answer the user's project question using approved project-brain context and local repository inspection when useful.",
|
|
3195
|
+
"This is an answer-only task. Do not modify files, do not create branches, and do not commit changes.",
|
|
3196
|
+
"",
|
|
3197
|
+
"## User Question",
|
|
3198
|
+
"",
|
|
3199
|
+
question,
|
|
3200
|
+
"",
|
|
3201
|
+
"## Work Item",
|
|
3202
|
+
"",
|
|
3203
|
+
`Title: ${workItem.title}`,
|
|
3204
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
3205
|
+
`Project ID: ${workItem.projectId}`,
|
|
3206
|
+
`Assistant message ID: ${workItem.assistantMessageId ?? context?.question.messageId ?? "unknown"}`,
|
|
3207
|
+
"",
|
|
3208
|
+
"## Approved Project Brain Context",
|
|
3209
|
+
"",
|
|
3210
|
+
brainContext || "No approved project-brain records were loaded. Say what you inspected locally and avoid pretending citations exist.",
|
|
3211
|
+
"",
|
|
3212
|
+
"## Recent Assistant Conversation",
|
|
3213
|
+
"",
|
|
3214
|
+
priorMessages || "No prior assistant messages were loaded.",
|
|
3215
|
+
"",
|
|
3216
|
+
"## Source Boundary Rules",
|
|
3217
|
+
"",
|
|
3218
|
+
"- Keep repository source, secrets, tokens, local credential paths, and provider session references local.",
|
|
3219
|
+
"- Summarize findings instead of dumping source code. Tiny identifiers, filenames, and short excerpts are acceptable when necessary.",
|
|
3220
|
+
'- Use sourceBoundary "projectBrain" when the answer only uses project-brain records.',
|
|
3221
|
+
'- Use sourceBoundary "localSource" when you inspected local repository files.',
|
|
3222
|
+
'- Use sourceBoundary "mixed" when both project-brain records and local files shaped the answer.',
|
|
3223
|
+
"- Include citations for project-brain records with documentId/title/repoPath where relevant.",
|
|
3224
|
+
"- Include local file citations with repoPath only; do not include raw source dumps.",
|
|
3225
|
+
"",
|
|
3226
|
+
"## Output Contract",
|
|
3227
|
+
"",
|
|
3228
|
+
"Print exactly one JSON object between the markers below. The CLI will submit only this structured answer back to Amistio.",
|
|
3229
|
+
"",
|
|
3230
|
+
assistantAnswerStart,
|
|
3231
|
+
'{"answer":"Concise answer with concrete next steps.","sourceBoundary":"mixed","citations":[{"source":"projectBrain","documentId":"doc_123","title":"ADR-001","repoPath":"docs/decisions/ADR-001.md","excerpt":"Short supporting excerpt"},{"source":"localSource","repoPath":"src/apps/web/example.ts","excerpt":"Short local finding"}]}',
|
|
3232
|
+
assistantAnswerEnd,
|
|
3233
|
+
"",
|
|
3234
|
+
"Do not put Markdown fences around the markers. Do not include implementation diffs."
|
|
3235
|
+
].join("\n");
|
|
3236
|
+
}
|
|
3237
|
+
function createPlanRevisionPrompt(workItem, context) {
|
|
3238
|
+
const messages = context?.messages ?? [];
|
|
3239
|
+
return [
|
|
3240
|
+
"# Amistio Plan Revision",
|
|
3241
|
+
"",
|
|
3242
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
3243
|
+
"Revise the selected generated plan using the user's conversation. Do not implement product/source code changes in this pass.",
|
|
3244
|
+
"",
|
|
3245
|
+
"## Work Item",
|
|
3246
|
+
"",
|
|
3247
|
+
`Title: ${workItem.title}`,
|
|
3248
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
3249
|
+
`Project ID: ${workItem.projectId}`,
|
|
3250
|
+
`Document ID: ${workItem.reviewDocumentId ?? "unknown"}`,
|
|
3251
|
+
`Document revision: ${workItem.reviewDocumentRevision ?? "unknown"}`,
|
|
3252
|
+
"",
|
|
3253
|
+
"## Current Plan",
|
|
3254
|
+
"",
|
|
3255
|
+
context?.planDocument.content ?? "The current plan document could not be loaded. Explain the blocker in the result summary.",
|
|
3256
|
+
"",
|
|
3257
|
+
"## Conversation",
|
|
3258
|
+
"",
|
|
3259
|
+
messages.length ? messages.map((message) => `- ${message.role} / ${message.intent} / ${message.status} / rev ${message.documentRevision}: ${message.content}`).join("\n") : "No conversation messages were loaded.",
|
|
3260
|
+
"",
|
|
3261
|
+
"## Output Contract",
|
|
3262
|
+
"",
|
|
3263
|
+
"Print exactly one JSON object between the markers below with an artifacts array containing one revised plan artifact.",
|
|
3264
|
+
'The artifact must use documentType "plan", keep the plan under docs/plans/, and include the complete revised plan content.',
|
|
3265
|
+
"",
|
|
3266
|
+
generationResultStart,
|
|
3267
|
+
'{"artifacts":[{"documentType":"plan","title":"Revised Plan","repoPath":"docs/plans/PLAN-revised.md","content":"# Revised Plan\\n\\n## Goal\\n..."}]}',
|
|
3268
|
+
generationResultEnd,
|
|
3269
|
+
"",
|
|
3270
|
+
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
3271
|
+
].join("\n");
|
|
3272
|
+
}
|
|
3273
|
+
function parseBrainGenerationArtifacts(output) {
|
|
3274
|
+
const start = output.indexOf(generationResultStart);
|
|
3275
|
+
const end = output.indexOf(generationResultEnd, start + generationResultStart.length);
|
|
3276
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
3277
|
+
throw new Error("Local AI generation did not return an Amistio brain generation result block.");
|
|
3278
|
+
}
|
|
3279
|
+
const payload = output.slice(start + generationResultStart.length, end).trim();
|
|
3280
|
+
const parsed = JSON.parse(stripJsonFence(payload));
|
|
3281
|
+
return brainGenerationResultSchema.parse(parsed).artifacts;
|
|
3282
|
+
}
|
|
3283
|
+
function parseAssistantAnswerResult(output) {
|
|
3284
|
+
const start = output.indexOf(assistantAnswerStart);
|
|
3285
|
+
const end = output.indexOf(assistantAnswerEnd, start + assistantAnswerStart.length);
|
|
3286
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
3287
|
+
throw new Error("Local AI answer did not return an Amistio assistant answer block.");
|
|
3288
|
+
}
|
|
3289
|
+
const payload = output.slice(start + assistantAnswerStart.length, end).trim();
|
|
3290
|
+
const parsed = JSON.parse(stripJsonFence(payload));
|
|
3291
|
+
return assistantAnswerResultSchema.parse(parsed);
|
|
3292
|
+
}
|
|
3293
|
+
function parseImpactPreviewResult(output) {
|
|
3294
|
+
const start = output.indexOf(impactPreviewStart);
|
|
3295
|
+
const end = output.indexOf(impactPreviewEnd, start + impactPreviewStart.length);
|
|
3296
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
3297
|
+
throw new Error("Local AI preview did not return an Amistio impact preview block.");
|
|
3298
|
+
}
|
|
3299
|
+
const payload = output.slice(start + impactPreviewStart.length, end).trim();
|
|
3300
|
+
const parsed = JSON.parse(stripJsonFence(payload));
|
|
3301
|
+
return impactPreviewResultSchema.parse(parsed);
|
|
2460
3302
|
}
|
|
2461
3303
|
function createBrainGenerationPrompt(workItem) {
|
|
2462
3304
|
const wish = workItem.sourceWish ?? workItem.title;
|
|
@@ -2480,21 +3322,21 @@ function createBrainGenerationPrompt(workItem) {
|
|
|
2480
3322
|
"## Read First",
|
|
2481
3323
|
"",
|
|
2482
3324
|
"- Read AGENTS.md first when it exists.",
|
|
2483
|
-
"- Read relevant files under architecture/, context/, decisions/, features/, memory/, plans/, prompts/, and workflows/ before drafting.",
|
|
3325
|
+
"- Read relevant files under docs/architecture/, docs/context/, docs/decisions/, docs/features/, docs/memory/, docs/plans/, docs/prompts/, and docs/workflows/ before drafting.",
|
|
2484
3326
|
"- Keep source code and secrets local. Do not include source-code payloads, access tokens, API keys, local credential paths, or provider session references in the result.",
|
|
2485
3327
|
"",
|
|
2486
3328
|
"## Generate Artifacts",
|
|
2487
3329
|
"",
|
|
2488
3330
|
"Return Markdown artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
|
|
2489
3331
|
"",
|
|
2490
|
-
"- architecture -> architecture/",
|
|
2491
|
-
"- context -> context/",
|
|
2492
|
-
"- decision -> decisions/",
|
|
2493
|
-
"- feature -> features/",
|
|
2494
|
-
"- memory -> memory/",
|
|
2495
|
-
"- plan -> plans/",
|
|
2496
|
-
"- prompt -> prompts/",
|
|
2497
|
-
"- workflow -> workflows/",
|
|
3332
|
+
"- architecture -> docs/architecture/",
|
|
3333
|
+
"- context -> docs/context/",
|
|
3334
|
+
"- decision -> docs/decisions/",
|
|
3335
|
+
"- feature -> docs/features/",
|
|
3336
|
+
"- memory -> docs/memory/",
|
|
3337
|
+
"- plan -> docs/plans/",
|
|
3338
|
+
"- prompt -> docs/prompts/",
|
|
3339
|
+
"- workflow -> docs/workflows/",
|
|
2498
3340
|
"",
|
|
2499
3341
|
"Each artifact needs documentType, title, repoPath, and content. Include at least a plan and an implementation prompt when implementation work is implied. Use model-agnostic prompt wording.",
|
|
2500
3342
|
"",
|
|
@@ -2503,7 +3345,7 @@ function createBrainGenerationPrompt(workItem) {
|
|
|
2503
3345
|
"Print exactly one JSON object between the markers below. The CLI will submit only this structured result back to Amistio.",
|
|
2504
3346
|
"",
|
|
2505
3347
|
generationResultStart,
|
|
2506
|
-
'{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
|
|
3348
|
+
'{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"docs/plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
|
|
2507
3349
|
generationResultEnd,
|
|
2508
3350
|
"",
|
|
2509
3351
|
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
@@ -2518,6 +3360,7 @@ function stripJsonFence(value) {
|
|
|
2518
3360
|
}
|
|
2519
3361
|
|
|
2520
3362
|
// src/runner-status.ts
|
|
3363
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2521
3364
|
var watchStateReminderMs = 60 * 1e3;
|
|
2522
3365
|
function formatWatchStartupContext(input) {
|
|
2523
3366
|
return [
|
|
@@ -2537,36 +3380,159 @@ function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateR
|
|
|
2537
3380
|
function watchStateKey(action) {
|
|
2538
3381
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
2539
3382
|
}
|
|
3383
|
+
function stableRunnerId(input) {
|
|
3384
|
+
const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
|
|
3385
|
+
return `runner_${digest}`;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// src/runner-resources.ts
|
|
3389
|
+
import os6 from "node:os";
|
|
3390
|
+
var defaultRuntime = {
|
|
3391
|
+
nowMs: () => Date.now(),
|
|
3392
|
+
memoryUsage: () => process.memoryUsage(),
|
|
3393
|
+
uptime: () => process.uptime(),
|
|
3394
|
+
cpuUsage: () => process.cpuUsage(),
|
|
3395
|
+
totalmem: () => os6.totalmem(),
|
|
3396
|
+
freemem: () => os6.freemem(),
|
|
3397
|
+
loadavg: () => os6.loadavg()
|
|
3398
|
+
};
|
|
3399
|
+
var previousRunnerResourceSample;
|
|
3400
|
+
function sampleCurrentRunnerResourceUsage() {
|
|
3401
|
+
const sample = collectRunnerResourceUsage(previousRunnerResourceSample);
|
|
3402
|
+
previousRunnerResourceSample = sample.state;
|
|
3403
|
+
return sample.resourceUsage;
|
|
3404
|
+
}
|
|
3405
|
+
function collectRunnerResourceUsage(previous, runtime = defaultRuntime) {
|
|
3406
|
+
const sampledAtMs = runtime.nowMs();
|
|
3407
|
+
const memory = runtime.memoryUsage();
|
|
3408
|
+
const cpu = runtime.cpuUsage();
|
|
3409
|
+
const load = runtime.loadavg();
|
|
3410
|
+
const totalMemory = runtime.totalmem();
|
|
3411
|
+
const freeMemory = runtime.freemem();
|
|
3412
|
+
const resourceUsage = {
|
|
3413
|
+
sampledAt: new Date(sampledAtMs).toISOString(),
|
|
3414
|
+
processUptimeSeconds: roundNumber(runtime.uptime(), 3),
|
|
3415
|
+
processMemoryRssBytes: Math.round(memory.rss),
|
|
3416
|
+
processMemoryHeapUsedBytes: Math.round(memory.heapUsed),
|
|
3417
|
+
processMemoryHeapTotalBytes: Math.round(memory.heapTotal),
|
|
3418
|
+
processCpuUserMicros: Math.round(cpu.user),
|
|
3419
|
+
processCpuSystemMicros: Math.round(cpu.system),
|
|
3420
|
+
systemMemoryTotalBytes: Math.round(totalMemory),
|
|
3421
|
+
systemMemoryFreeBytes: Math.round(freeMemory)
|
|
3422
|
+
};
|
|
3423
|
+
const cpuPercent = processCpuPercent(previous, { sampledAtMs, processCpuUserMicros: cpu.user, processCpuSystemMicros: cpu.system });
|
|
3424
|
+
if (cpuPercent !== void 0) {
|
|
3425
|
+
resourceUsage.processCpuPercent = cpuPercent;
|
|
3426
|
+
}
|
|
3427
|
+
if (load.length >= 3) {
|
|
3428
|
+
resourceUsage.systemLoadAverage1m = roundNumber(load[0], 2);
|
|
3429
|
+
resourceUsage.systemLoadAverage5m = roundNumber(load[1], 2);
|
|
3430
|
+
resourceUsage.systemLoadAverage15m = roundNumber(load[2], 2);
|
|
3431
|
+
}
|
|
3432
|
+
return {
|
|
3433
|
+
resourceUsage,
|
|
3434
|
+
state: {
|
|
3435
|
+
sampledAtMs,
|
|
3436
|
+
processCpuUserMicros: cpu.user,
|
|
3437
|
+
processCpuSystemMicros: cpu.system
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
function formatRunnerResourceUsage(resourceUsage) {
|
|
3442
|
+
if (!resourceUsage) {
|
|
3443
|
+
return "unavailable until this runner reports a sample.";
|
|
3444
|
+
}
|
|
3445
|
+
const parts = [
|
|
3446
|
+
resourceUsage.processMemoryRssBytes !== void 0 ? `RSS ${formatBytes(resourceUsage.processMemoryRssBytes)}` : void 0,
|
|
3447
|
+
heapSummary(resourceUsage),
|
|
3448
|
+
resourceUsage.processCpuPercent !== void 0 ? `CPU ${formatPercent(resourceUsage.processCpuPercent)}` : "CPU warming up",
|
|
3449
|
+
systemMemorySummary(resourceUsage),
|
|
3450
|
+
loadSummary(resourceUsage),
|
|
3451
|
+
`sampled ${resourceUsage.sampledAt}`
|
|
3452
|
+
].filter(Boolean);
|
|
3453
|
+
return parts.length ? parts.join("; ") : "unavailable until this runner reports a complete sample.";
|
|
3454
|
+
}
|
|
3455
|
+
function formatBytes(bytes) {
|
|
3456
|
+
if (bytes === void 0 || !Number.isFinite(bytes)) {
|
|
3457
|
+
return "unknown";
|
|
3458
|
+
}
|
|
3459
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3460
|
+
let value = Math.max(0, bytes);
|
|
3461
|
+
let unitIndex = 0;
|
|
3462
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
3463
|
+
value /= 1024;
|
|
3464
|
+
unitIndex += 1;
|
|
3465
|
+
}
|
|
3466
|
+
const digits = Number.isInteger(value) || value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
3467
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
3468
|
+
}
|
|
3469
|
+
function formatPercent(value) {
|
|
3470
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
3471
|
+
return "unknown";
|
|
3472
|
+
}
|
|
3473
|
+
return `${roundNumber(value, 1).toFixed(1)}%`;
|
|
3474
|
+
}
|
|
3475
|
+
function processCpuPercent(previous, current) {
|
|
3476
|
+
if (!previous) return void 0;
|
|
3477
|
+
const elapsedMicros = (current.sampledAtMs - previous.sampledAtMs) * 1e3;
|
|
3478
|
+
const cpuDeltaMicros = current.processCpuUserMicros + current.processCpuSystemMicros - previous.processCpuUserMicros - previous.processCpuSystemMicros;
|
|
3479
|
+
if (elapsedMicros <= 0 || cpuDeltaMicros < 0) return void 0;
|
|
3480
|
+
return roundNumber(cpuDeltaMicros / elapsedMicros * 100, 1);
|
|
3481
|
+
}
|
|
3482
|
+
function heapSummary(resourceUsage) {
|
|
3483
|
+
if (resourceUsage.processMemoryHeapUsedBytes === void 0 && resourceUsage.processMemoryHeapTotalBytes === void 0) {
|
|
3484
|
+
return void 0;
|
|
3485
|
+
}
|
|
3486
|
+
return `heap ${formatBytes(resourceUsage.processMemoryHeapUsedBytes)} / ${formatBytes(resourceUsage.processMemoryHeapTotalBytes)}`;
|
|
3487
|
+
}
|
|
3488
|
+
function systemMemorySummary(resourceUsage) {
|
|
3489
|
+
if (resourceUsage.systemMemoryTotalBytes === void 0 || resourceUsage.systemMemoryFreeBytes === void 0 || resourceUsage.systemMemoryTotalBytes <= 0) {
|
|
3490
|
+
return void 0;
|
|
3491
|
+
}
|
|
3492
|
+
const usedBytes = Math.max(0, resourceUsage.systemMemoryTotalBytes - resourceUsage.systemMemoryFreeBytes);
|
|
3493
|
+
const usedPercent = usedBytes / resourceUsage.systemMemoryTotalBytes * 100;
|
|
3494
|
+
return `system memory ${formatBytes(usedBytes)} / ${formatBytes(resourceUsage.systemMemoryTotalBytes)} used (${formatPercent(usedPercent)})`;
|
|
3495
|
+
}
|
|
3496
|
+
function loadSummary(resourceUsage) {
|
|
3497
|
+
if (resourceUsage.systemLoadAverage1m === void 0 || resourceUsage.systemLoadAverage5m === void 0 || resourceUsage.systemLoadAverage15m === void 0) {
|
|
3498
|
+
return void 0;
|
|
3499
|
+
}
|
|
3500
|
+
return `load ${resourceUsage.systemLoadAverage1m.toFixed(2)} / ${resourceUsage.systemLoadAverage5m.toFixed(2)} / ${resourceUsage.systemLoadAverage15m.toFixed(2)}`;
|
|
3501
|
+
}
|
|
3502
|
+
function roundNumber(value, digits) {
|
|
3503
|
+
const factor = 10 ** digits;
|
|
3504
|
+
return Math.round(value * factor) / factor;
|
|
3505
|
+
}
|
|
2540
3506
|
|
|
2541
3507
|
// src/importer.ts
|
|
2542
|
-
import { execFile as
|
|
2543
|
-
import { createHash as
|
|
2544
|
-
import { readdir as readdir4, readFile as
|
|
2545
|
-
import
|
|
2546
|
-
import { promisify as
|
|
2547
|
-
var
|
|
3508
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3509
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3510
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
3511
|
+
import path10 from "node:path";
|
|
3512
|
+
import { promisify as promisify3 } from "node:util";
|
|
3513
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2548
3514
|
var defaultMaxFileKb = 256;
|
|
2549
|
-
var
|
|
3515
|
+
var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2550
3516
|
var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
|
|
2551
|
-
var excludedFileNames = /* @__PURE__ */ new Set(["context/amistio-project.md"]);
|
|
3517
|
+
var excludedFileNames = /* @__PURE__ */ new Set(["docs/context/amistio-project.md", "context/amistio-project.md"]);
|
|
2552
3518
|
var generatedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
|
|
2553
3519
|
var documentFolderByType = {
|
|
2554
|
-
architecture: "architecture",
|
|
2555
|
-
context: "context",
|
|
2556
|
-
decision: "decisions",
|
|
2557
|
-
feature: "features",
|
|
2558
|
-
memory: "memory",
|
|
2559
|
-
plan: "plans",
|
|
2560
|
-
prompt: "prompts/shared",
|
|
2561
|
-
workflow: "workflows"
|
|
3520
|
+
architecture: "docs/architecture",
|
|
3521
|
+
context: "docs/context",
|
|
3522
|
+
decision: "docs/decisions",
|
|
3523
|
+
feature: "docs/features",
|
|
3524
|
+
memory: "docs/memory",
|
|
3525
|
+
plan: "docs/plans",
|
|
3526
|
+
prompt: "docs/prompts/shared",
|
|
3527
|
+
workflow: "docs/workflows"
|
|
2562
3528
|
};
|
|
2563
3529
|
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
2564
|
-
const requestedRoot =
|
|
3530
|
+
const requestedRoot = path10.resolve(rootDir);
|
|
2565
3531
|
const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
|
|
2566
3532
|
const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
|
|
2567
3533
|
const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
|
|
2568
3534
|
const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
|
|
2569
|
-
const repoName = (parsedCloneUrl?.repoName ??
|
|
3535
|
+
const repoName = (parsedCloneUrl?.repoName ?? path10.basename(root)) || "repository";
|
|
2570
3536
|
const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
|
|
2571
3537
|
return {
|
|
2572
3538
|
rootDir: root,
|
|
@@ -2578,7 +3544,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
|
2578
3544
|
};
|
|
2579
3545
|
}
|
|
2580
3546
|
async function scanLegacyDocuments(options) {
|
|
2581
|
-
const rootDir =
|
|
3547
|
+
const rootDir = path10.resolve(options.rootDir);
|
|
2582
3548
|
const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
|
|
2583
3549
|
const skipped = [];
|
|
2584
3550
|
const candidates = [];
|
|
@@ -2597,7 +3563,7 @@ async function scanLegacyDocuments(options) {
|
|
|
2597
3563
|
skipped.push({ repoPath, reason: "excluded" });
|
|
2598
3564
|
continue;
|
|
2599
3565
|
}
|
|
2600
|
-
const fullPath =
|
|
3566
|
+
const fullPath = path10.join(rootDir, ...repoPath.split("/"));
|
|
2601
3567
|
const fileStat = await stat4(fullPath).catch(() => void 0);
|
|
2602
3568
|
if (!fileStat?.isFile()) {
|
|
2603
3569
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
@@ -2607,7 +3573,7 @@ async function scanLegacyDocuments(options) {
|
|
|
2607
3573
|
skipped.push({ repoPath, reason: "tooLarge" });
|
|
2608
3574
|
continue;
|
|
2609
3575
|
}
|
|
2610
|
-
const content = await
|
|
3576
|
+
const content = await readFile7(fullPath, "utf8").catch(() => void 0);
|
|
2611
3577
|
if (content === void 0) {
|
|
2612
3578
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
2613
3579
|
continue;
|
|
@@ -2688,7 +3654,7 @@ function parseOptionalOriginCloneUrl(originUrl) {
|
|
|
2688
3654
|
async function listRepositoryPaths(rootDir) {
|
|
2689
3655
|
const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
|
|
2690
3656
|
if (gitFiles !== void 0) {
|
|
2691
|
-
return gitFiles.split("\n").map((line) =>
|
|
3657
|
+
return gitFiles.split("\n").map((line) => normalizeRepoPath3(line)).filter((line) => line.length > 0);
|
|
2692
3658
|
}
|
|
2693
3659
|
const files = [];
|
|
2694
3660
|
await walkRepository(rootDir, rootDir, files);
|
|
@@ -2697,8 +3663,8 @@ async function listRepositoryPaths(rootDir) {
|
|
|
2697
3663
|
async function walkRepository(rootDir, directory, files) {
|
|
2698
3664
|
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
2699
3665
|
for (const entry of entries) {
|
|
2700
|
-
const fullPath =
|
|
2701
|
-
const repoPath =
|
|
3666
|
+
const fullPath = path10.join(directory, entry.name);
|
|
3667
|
+
const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
|
|
2702
3668
|
if (entry.isDirectory()) {
|
|
2703
3669
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
2704
3670
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -2718,7 +3684,7 @@ function matchesIncludeExclude(repoPath, include, exclude) {
|
|
|
2718
3684
|
return true;
|
|
2719
3685
|
}
|
|
2720
3686
|
function wildcardMatch(pattern, repoPath) {
|
|
2721
|
-
const normalizedPattern =
|
|
3687
|
+
const normalizedPattern = normalizeRepoPath3(pattern);
|
|
2722
3688
|
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "::DOUBLE_STAR_SLASH::").replace(/\*\*/g, "::DOUBLE_STAR::").replace(/\?/g, "[^/]").replace(/\*/g, "[^/]*").replace(/::DOUBLE_STAR_SLASH::/g, "(?:.*/)?").replace(/::DOUBLE_STAR::/g, ".*");
|
|
2723
3689
|
return new RegExp(`^${escaped}$`).test(repoPath);
|
|
2724
3690
|
}
|
|
@@ -2726,6 +3692,7 @@ function isMarkdownDocument(repoPath) {
|
|
|
2726
3692
|
return /\.(md|mdx)$/i.test(repoPath);
|
|
2727
3693
|
}
|
|
2728
3694
|
function isExcludedRepoPath(repoPath) {
|
|
3695
|
+
if (isControlPlaneTemplateRepoPath(repoPath)) return true;
|
|
2729
3696
|
if (excludedFileNames.has(repoPath)) return true;
|
|
2730
3697
|
const segments = repoPath.split("/");
|
|
2731
3698
|
if (segments.some((segment) => excludedDirectoryNames.has(segment) || generatedPathSegments.has(segment))) return true;
|
|
@@ -2748,9 +3715,12 @@ function classifyLegacyDocument(repoPath, content) {
|
|
|
2748
3715
|
return "context";
|
|
2749
3716
|
}
|
|
2750
3717
|
function canonicalImportPath(sourcePath, documentType) {
|
|
2751
|
-
if (
|
|
3718
|
+
if (isCanonicalControlPlanePath(sourcePath)) {
|
|
2752
3719
|
return sourcePath;
|
|
2753
3720
|
}
|
|
3721
|
+
if (isLegacyControlPlanePath(sourcePath)) {
|
|
3722
|
+
return `docs/${sourcePath}`;
|
|
3723
|
+
}
|
|
2754
3724
|
const baseSlug = slugFromPath(sourcePath);
|
|
2755
3725
|
return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
|
|
2756
3726
|
}
|
|
@@ -2759,22 +3729,26 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
2759
3729
|
usedPaths.add(basePath);
|
|
2760
3730
|
return basePath;
|
|
2761
3731
|
}
|
|
2762
|
-
const extension =
|
|
2763
|
-
const directory =
|
|
2764
|
-
const basename =
|
|
3732
|
+
const extension = path10.posix.extname(basePath) || ".md";
|
|
3733
|
+
const directory = path10.posix.dirname(basePath);
|
|
3734
|
+
const basename = path10.posix.basename(basePath, extension);
|
|
2765
3735
|
const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
|
|
2766
3736
|
usedPaths.add(uniquePath);
|
|
2767
3737
|
return uniquePath;
|
|
2768
3738
|
}
|
|
2769
|
-
function
|
|
3739
|
+
function isCanonicalControlPlanePath(repoPath) {
|
|
3740
|
+
const [firstSegment, secondSegment] = normalizeRepoPath3(repoPath).split("/");
|
|
3741
|
+
return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
|
|
3742
|
+
}
|
|
3743
|
+
function isLegacyControlPlanePath(repoPath) {
|
|
2770
3744
|
const [firstSegment] = repoPath.split("/");
|
|
2771
|
-
return Boolean(firstSegment &&
|
|
3745
|
+
return Boolean(firstSegment && controlPlaneRoots2.includes(firstSegment));
|
|
2772
3746
|
}
|
|
2773
3747
|
function inferTitle2(content, repoPath) {
|
|
2774
3748
|
const body = stripFrontmatter(content);
|
|
2775
3749
|
const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
|
|
2776
3750
|
if (heading) return heading;
|
|
2777
|
-
const basename =
|
|
3751
|
+
const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
|
|
2778
3752
|
return titleCase(basename || "Imported Document");
|
|
2779
3753
|
}
|
|
2780
3754
|
function stripFrontmatter(content) {
|
|
@@ -2796,19 +3770,19 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
|
|
|
2796
3770
|
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
2797
3771
|
}
|
|
2798
3772
|
function hashText(value, length) {
|
|
2799
|
-
return
|
|
3773
|
+
return createHash5("sha256").update(value).digest("hex").slice(0, length);
|
|
2800
3774
|
}
|
|
2801
|
-
function
|
|
3775
|
+
function normalizeRepoPath3(value) {
|
|
2802
3776
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
2803
3777
|
}
|
|
2804
3778
|
async function runGit2(args) {
|
|
2805
|
-
const { stdout } = await
|
|
3779
|
+
const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
2806
3780
|
return stdout.trim();
|
|
2807
3781
|
}
|
|
2808
3782
|
|
|
2809
3783
|
// src/runner-actions.ts
|
|
2810
|
-
import { spawn as
|
|
2811
|
-
import
|
|
3784
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3785
|
+
import path11 from "node:path";
|
|
2812
3786
|
function buildBackgroundRunnerArgs(options) {
|
|
2813
3787
|
const args = [
|
|
2814
3788
|
"run",
|
|
@@ -2818,7 +3792,7 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
2818
3792
|
"--runner-id",
|
|
2819
3793
|
options.runnerId,
|
|
2820
3794
|
"--root",
|
|
2821
|
-
|
|
3795
|
+
path11.resolve(options.root),
|
|
2822
3796
|
"--session",
|
|
2823
3797
|
options.session,
|
|
2824
3798
|
"--interval-seconds",
|
|
@@ -2842,6 +3816,9 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
2842
3816
|
if (!options.stream) {
|
|
2843
3817
|
args.push("--no-stream");
|
|
2844
3818
|
}
|
|
3819
|
+
if (options.verbose) {
|
|
3820
|
+
args.push("--verbose");
|
|
3821
|
+
}
|
|
2845
3822
|
return args;
|
|
2846
3823
|
}
|
|
2847
3824
|
async function runOfficialCliUpdate() {
|
|
@@ -2853,7 +3830,7 @@ async function runOfficialCliUpdate() {
|
|
|
2853
3830
|
}
|
|
2854
3831
|
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
2855
3832
|
return new Promise((resolve) => {
|
|
2856
|
-
const child =
|
|
3833
|
+
const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2857
3834
|
let output = "";
|
|
2858
3835
|
const updateTimeout = setTimeout(() => {
|
|
2859
3836
|
output += "Timed out while running official CLI update.\n";
|
|
@@ -2880,6 +3857,94 @@ function truncateProcessOutput(value) {
|
|
|
2880
3857
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
2881
3858
|
}
|
|
2882
3859
|
|
|
3860
|
+
// src/git-worktree.ts
|
|
3861
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3862
|
+
import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
|
|
3863
|
+
import path12 from "node:path";
|
|
3864
|
+
import { promisify as promisify4 } from "node:util";
|
|
3865
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
3866
|
+
function needsGitWorktreeIsolation(workItem) {
|
|
3867
|
+
return (workItem.workKind ?? "implementation") === "implementation";
|
|
3868
|
+
}
|
|
3869
|
+
function resolveWorktreeIdentity(workItem) {
|
|
3870
|
+
const implementationScopeId = workItem.controllingAdrId ?? workItem.implementationScopeId ?? workItem.impactDocumentId ?? workItem.reviewDocumentId ?? workItem.generatedDraftId ?? workItem.workItemId;
|
|
3871
|
+
const slug = workIsolationSlug(implementationScopeId, workItem.title);
|
|
3872
|
+
return {
|
|
3873
|
+
implementationScopeId,
|
|
3874
|
+
branch: workItem.executionBranch ?? `amistio/work/${slug}`,
|
|
3875
|
+
worktreeKey: workItem.executionWorktreeKey ?? `amistio/worktrees/${slug}`,
|
|
3876
|
+
...workItem.repositoryLockId ? { repositoryLockId: workItem.repositoryLockId } : {}
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
3880
|
+
const identity = resolveWorktreeIdentity(workItem);
|
|
3881
|
+
const repoRoot = await gitOutput(rootDir, ["rev-parse", "--show-toplevel"]).catch((error) => {
|
|
3882
|
+
throw new Error(`Git worktree isolation requires a paired Git checkout: ${errorMessage2(error)}`);
|
|
3883
|
+
});
|
|
3884
|
+
const currentHead = await gitOutput(repoRoot, ["rev-parse", "HEAD"]);
|
|
3885
|
+
await assertBaseRevision(repoRoot, workItem.baseRevision, currentHead);
|
|
3886
|
+
const baseRevision = currentHead;
|
|
3887
|
+
const worktreePath = localWorktreePath(repoRoot, identity.worktreeKey);
|
|
3888
|
+
if (await pathExists(worktreePath)) {
|
|
3889
|
+
await assertExistingWorktree(worktreePath, identity.branch);
|
|
3890
|
+
return { ...identity, baseRevision, worktreePath };
|
|
3891
|
+
}
|
|
3892
|
+
await mkdir9(path12.dirname(worktreePath), { recursive: true });
|
|
3893
|
+
const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
|
|
3894
|
+
const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
|
|
3895
|
+
await gitOutput(repoRoot, worktreeArgs).catch((error) => {
|
|
3896
|
+
throw new Error(`Could not create Git worktree ${identity.worktreeKey} on ${identity.branch}: ${errorMessage2(error)}`);
|
|
3897
|
+
});
|
|
3898
|
+
return { ...identity, baseRevision, worktreePath };
|
|
3899
|
+
}
|
|
3900
|
+
function localWorktreePath(repoRoot, worktreeKey) {
|
|
3901
|
+
const repoName = path12.basename(repoRoot);
|
|
3902
|
+
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
3903
|
+
return path12.join(path12.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
|
|
3904
|
+
}
|
|
3905
|
+
async function assertExistingWorktree(worktreePath, branch) {
|
|
3906
|
+
await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
3907
|
+
const currentBranch = await gitOutput(worktreePath, ["branch", "--show-current"]);
|
|
3908
|
+
if (currentBranch && currentBranch !== branch) {
|
|
3909
|
+
throw new Error(`Existing worktree is on ${currentBranch}; expected ${branch}.`);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
|
|
3913
|
+
if (!baseRevision || baseRevision === currentHead) {
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
const revisionExists = await gitCommandSucceeds(repoRoot, ["cat-file", "-e", `${baseRevision}^{commit}`]);
|
|
3917
|
+
if (!revisionExists) {
|
|
3918
|
+
throw new Error(`Work item base revision ${baseRevision} is not available in this checkout; sync the repository before running implementation work.`);
|
|
3919
|
+
}
|
|
3920
|
+
const isAncestor = await gitCommandSucceeds(repoRoot, ["merge-base", "--is-ancestor", baseRevision, currentHead]);
|
|
3921
|
+
if (!isAncestor) {
|
|
3922
|
+
throw new Error(`Work item base revision ${baseRevision} is not an ancestor of ${currentHead}; refresh the work item before implementation.`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
async function gitOutput(cwd, args) {
|
|
3926
|
+
const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
3927
|
+
return stdout.trim();
|
|
3928
|
+
}
|
|
3929
|
+
async function gitCommandSucceeds(cwd, args) {
|
|
3930
|
+
return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
|
|
3931
|
+
}
|
|
3932
|
+
async function pathExists(value) {
|
|
3933
|
+
return stat5(value).then(() => true, () => false);
|
|
3934
|
+
}
|
|
3935
|
+
function workIsolationSlug(scopeId, title) {
|
|
3936
|
+
const scopeSlug = slugify(scopeId);
|
|
3937
|
+
const titleSlug = slugify(title).slice(0, 32);
|
|
3938
|
+
const combined = titleSlug && !scopeSlug.includes(titleSlug) ? `${scopeSlug}-${titleSlug}` : scopeSlug;
|
|
3939
|
+
return combined.slice(0, 96).replace(/-+$/g, "") || "work";
|
|
3940
|
+
}
|
|
3941
|
+
function slugify(value) {
|
|
3942
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "work";
|
|
3943
|
+
}
|
|
3944
|
+
function errorMessage2(error) {
|
|
3945
|
+
return error instanceof Error ? error.message : String(error);
|
|
3946
|
+
}
|
|
3947
|
+
|
|
2883
3948
|
// src/version.ts
|
|
2884
3949
|
import { readFileSync } from "node:fs";
|
|
2885
3950
|
function readCliPackageVersion() {
|
|
@@ -3005,6 +4070,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
3005
4070
|
console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
|
|
3006
4071
|
});
|
|
3007
4072
|
program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
|
|
4073
|
+
const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
|
|
3008
4074
|
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
|
|
3009
4075
|
let credential = options.token;
|
|
3010
4076
|
if (options.pairingCode) {
|
|
@@ -3016,7 +4082,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
3016
4082
|
projectId: options.project,
|
|
3017
4083
|
pairingCode: options.pairingCode,
|
|
3018
4084
|
repositoryLinkId,
|
|
3019
|
-
repoName: inferRepoName(
|
|
4085
|
+
repoName: inferRepoName(pairingRoot),
|
|
3020
4086
|
repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
|
|
3021
4087
|
defaultBranch: options.defaultBranch
|
|
3022
4088
|
});
|
|
@@ -3024,7 +4090,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
3024
4090
|
credential = credential ?? pairing.token;
|
|
3025
4091
|
console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}.`);
|
|
3026
4092
|
}
|
|
3027
|
-
const filePath = await writeProjectLink(
|
|
4093
|
+
const filePath = await writeProjectLink(pairingRoot, {
|
|
3028
4094
|
amistioAccountId: options.account,
|
|
3029
4095
|
amistioProjectId: options.project,
|
|
3030
4096
|
repositoryLinkId,
|
|
@@ -3149,9 +4215,9 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
3149
4215
|
console.log(workItemId ? `No work item found for ${workItemId}.` : "No approved work item is ready for prompt export.");
|
|
3150
4216
|
return;
|
|
3151
4217
|
}
|
|
3152
|
-
const prompt =
|
|
4218
|
+
const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
|
|
3153
4219
|
if (options.out) {
|
|
3154
|
-
await
|
|
4220
|
+
await writeFile9(options.out, prompt, "utf8");
|
|
3155
4221
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
3156
4222
|
} else {
|
|
3157
4223
|
console.log(prompt);
|
|
@@ -3199,7 +4265,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
3199
4265
|
process.exitCode = result.exitCode;
|
|
3200
4266
|
}
|
|
3201
4267
|
});
|
|
3202
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID"
|
|
4268
|
+
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID").option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).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").option("--verbose", "Print detailed runner errors while watching").action(async (options, command) => {
|
|
3203
4269
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
3204
4270
|
if (!context) {
|
|
3205
4271
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -3210,6 +4276,13 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3210
4276
|
process.exitCode = 1;
|
|
3211
4277
|
return;
|
|
3212
4278
|
}
|
|
4279
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4280
|
+
accountId: context.metadata.amistioAccountId,
|
|
4281
|
+
projectId: context.metadata.amistioProjectId,
|
|
4282
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4283
|
+
machineId: runnerMachineId()
|
|
4284
|
+
});
|
|
4285
|
+
const resolvedOptions = { ...options, runnerId };
|
|
3213
4286
|
if (options.background) {
|
|
3214
4287
|
if (options.dryRun) {
|
|
3215
4288
|
console.log("Background runners cannot be started in dry-run mode.");
|
|
@@ -3220,10 +4293,10 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3220
4293
|
accountId: context.metadata.amistioAccountId,
|
|
3221
4294
|
projectId: context.metadata.amistioProjectId,
|
|
3222
4295
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3223
|
-
runnerId
|
|
3224
|
-
rootDir:
|
|
4296
|
+
runnerId,
|
|
4297
|
+
rootDir: path13.resolve(options.root),
|
|
3225
4298
|
apiUrl: options.apiUrl,
|
|
3226
|
-
args: buildBackgroundRunnerArgs(
|
|
4299
|
+
args: buildBackgroundRunnerArgs(resolvedOptions)
|
|
3227
4300
|
});
|
|
3228
4301
|
console.log(`Started background runner ${metadata.runnerId} with PID ${metadata.pid}.`);
|
|
3229
4302
|
if (metadata.logPath) {
|
|
@@ -3232,59 +4305,60 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3232
4305
|
return;
|
|
3233
4306
|
}
|
|
3234
4307
|
if (options.watch) {
|
|
3235
|
-
for (const line of formatWatchStartupContext({ runnerId
|
|
4308
|
+
for (const line of formatWatchStartupContext({ runnerId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, apiUrl: options.apiUrl, intervalSeconds: options.intervalSeconds })) {
|
|
3236
4309
|
console.log(line);
|
|
3237
4310
|
}
|
|
3238
4311
|
}
|
|
4312
|
+
let offlineSent = false;
|
|
4313
|
+
const sendOfflineHeartbeat = async (message) => {
|
|
4314
|
+
if (offlineSent || options.dryRun) return;
|
|
4315
|
+
offlineSent = true;
|
|
4316
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "offline", { ...runnerHeartbeatMetadata(), ...message ? { preferenceMessage: message } : {} }).catch(() => void 0);
|
|
4317
|
+
};
|
|
4318
|
+
const handleShutdownSignal = (signal) => {
|
|
4319
|
+
void sendOfflineHeartbeat(`Runner stopped by ${signal}.`).finally(() => {
|
|
4320
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
4321
|
+
});
|
|
4322
|
+
};
|
|
4323
|
+
process.once("SIGINT", handleShutdownSignal);
|
|
4324
|
+
process.once("SIGTERM", handleShutdownSignal);
|
|
3239
4325
|
let iterations = 0;
|
|
3240
4326
|
let lastWatchStateLog;
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
stream: options.stream,
|
|
3256
|
-
commandContext: {
|
|
3257
|
-
accountId: context.metadata.amistioAccountId,
|
|
3258
|
-
apiUrl: options.apiUrl,
|
|
3259
|
-
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
3260
|
-
projectId: context.metadata.amistioProjectId,
|
|
3261
|
-
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3262
|
-
root: options.root,
|
|
3263
|
-
runnerId: options.runnerId
|
|
3264
|
-
},
|
|
3265
|
-
suppressIdleOutput: Boolean(options.watch)
|
|
3266
|
-
});
|
|
3267
|
-
if (!options.watch || options.dryRun) {
|
|
3268
|
-
if (result.exitCode !== 0) {
|
|
3269
|
-
process.exitCode = result.exitCode;
|
|
4327
|
+
try {
|
|
4328
|
+
while (true) {
|
|
4329
|
+
iterations += 1;
|
|
4330
|
+
const result = await runWatchIteration({
|
|
4331
|
+
command,
|
|
4332
|
+
context,
|
|
4333
|
+
options: resolvedOptions,
|
|
4334
|
+
runnerId
|
|
4335
|
+
});
|
|
4336
|
+
if (!options.watch || options.dryRun) {
|
|
4337
|
+
if (result.exitCode !== 0) {
|
|
4338
|
+
process.exitCode = result.exitCode;
|
|
4339
|
+
}
|
|
4340
|
+
return;
|
|
3270
4341
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
4342
|
+
if (result.status === "idle" && result.nextAction) {
|
|
4343
|
+
const nowMs = Date.now();
|
|
4344
|
+
if (shouldPrintWatchState(result.nextAction, lastWatchStateLog, nowMs)) {
|
|
4345
|
+
console.log(formatWatchIdleLine(result.nextAction, options.intervalSeconds));
|
|
4346
|
+
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
4347
|
+
}
|
|
3278
4348
|
}
|
|
4349
|
+
if (result.stopRunner) {
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
|
|
4353
|
+
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
await delay(options.intervalSeconds * 1e3);
|
|
3279
4357
|
}
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
3285
|
-
return;
|
|
3286
|
-
}
|
|
3287
|
-
await delay(options.intervalSeconds * 1e3);
|
|
4358
|
+
} finally {
|
|
4359
|
+
process.off("SIGINT", handleShutdownSignal);
|
|
4360
|
+
process.off("SIGTERM", handleShutdownSignal);
|
|
4361
|
+
await sendOfflineHeartbeat("Runner stopped.");
|
|
3288
4362
|
}
|
|
3289
4363
|
});
|
|
3290
4364
|
var runner = program.command("runner").description("Manage local Amistio runner processes");
|
|
@@ -3305,6 +4379,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
3305
4379
|
console.log("No background runner metadata found for this paired repository.");
|
|
3306
4380
|
if (runners.length) {
|
|
3307
4381
|
console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
|
|
4382
|
+
console.log(`Resource usage: ${formatRunnerResourceUsage(runners[0].resourceUsage)}`);
|
|
3308
4383
|
}
|
|
3309
4384
|
return;
|
|
3310
4385
|
}
|
|
@@ -3325,6 +4400,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
3325
4400
|
if (heartbeat) {
|
|
3326
4401
|
console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
|
|
3327
4402
|
}
|
|
4403
|
+
console.log(` Resource usage: ${formatRunnerResourceUsage(heartbeat?.resourceUsage)}`);
|
|
3328
4404
|
}
|
|
3329
4405
|
});
|
|
3330
4406
|
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) => {
|
|
@@ -3349,15 +4425,159 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
3349
4425
|
return;
|
|
3350
4426
|
}
|
|
3351
4427
|
const record = records[0];
|
|
4428
|
+
const existingRunner = await context.client.listRunners(context.metadata.amistioProjectId).then((result) => result.runners.find((runner2) => runner2.runnerId === record.runnerId && runner2.repositoryLinkId === context.metadata.repositoryLinkId)).catch(() => void 0);
|
|
3352
4429
|
const stopResult = await stopRunnerDaemonProcess(record);
|
|
3353
4430
|
await markRunnerDaemonStopped(record);
|
|
3354
4431
|
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
|
|
3355
4432
|
version: CLI_VERSION,
|
|
3356
4433
|
mode: "background",
|
|
3357
|
-
hostname: record.hostname
|
|
4434
|
+
hostname: record.hostname,
|
|
4435
|
+
...existingRunner?.resourceUsage ? { resourceUsage: existingRunner.resourceUsage } : {}
|
|
3358
4436
|
}).catch(() => void 0);
|
|
3359
4437
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
3360
4438
|
});
|
|
4439
|
+
var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
|
|
4440
|
+
runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
|
|
4441
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4442
|
+
if (!context) {
|
|
4443
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
if (!context.token) {
|
|
4447
|
+
console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
|
|
4448
|
+
process.exitCode = 1;
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
const platform = detectRunnerServicePlatform();
|
|
4452
|
+
if (platform === "unsupported") {
|
|
4453
|
+
console.log("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
4454
|
+
process.exitCode = 1;
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4458
|
+
accountId: context.metadata.amistioAccountId,
|
|
4459
|
+
projectId: context.metadata.amistioProjectId,
|
|
4460
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4461
|
+
machineId: runnerMachineId()
|
|
4462
|
+
});
|
|
4463
|
+
const args = buildBackgroundRunnerArgs({ ...options, runnerId, apiUrl: options.apiUrl, root: options.root });
|
|
4464
|
+
const serviceInput = {
|
|
4465
|
+
accountId: context.metadata.amistioAccountId,
|
|
4466
|
+
projectId: context.metadata.amistioProjectId,
|
|
4467
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4468
|
+
runnerId,
|
|
4469
|
+
rootDir: path13.resolve(options.root),
|
|
4470
|
+
apiUrl: options.apiUrl,
|
|
4471
|
+
args,
|
|
4472
|
+
platform
|
|
4473
|
+
};
|
|
4474
|
+
if (options.dryRun) {
|
|
4475
|
+
const descriptor = createRunnerServiceDescriptor(serviceInput);
|
|
4476
|
+
console.log(`Startup service file: ${descriptor.metadata.serviceFilePath}`);
|
|
4477
|
+
console.log(descriptor.content.trim());
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
try {
|
|
4481
|
+
const metadata = await installRunnerService(serviceInput);
|
|
4482
|
+
console.log(`Installed startup service ${metadata.serviceName}.`);
|
|
4483
|
+
console.log(`Service file: ${metadata.serviceFilePath}`);
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
console.error(errorMessage3(error));
|
|
4486
|
+
process.exitCode = 1;
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
runnerService.command("status").description("Show the startup service status for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4490
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4491
|
+
if (!context) {
|
|
4492
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4496
|
+
accountId: context.metadata.amistioAccountId,
|
|
4497
|
+
projectId: context.metadata.amistioProjectId,
|
|
4498
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4499
|
+
machineId: runnerMachineId()
|
|
4500
|
+
});
|
|
4501
|
+
const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4502
|
+
if (!metadata) {
|
|
4503
|
+
console.log("No startup service metadata found for this paired repository runner.");
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
const runtimeStatus = await runnerServiceRuntimeStatus(metadata);
|
|
4507
|
+
console.log(`Startup service ${metadata.serviceName}: ${runtimeStatus}`);
|
|
4508
|
+
console.log(` Platform: ${metadata.platform}`);
|
|
4509
|
+
console.log(` File: ${metadata.serviceFilePath}`);
|
|
4510
|
+
console.log(` Root: ${metadata.rootDir}`);
|
|
4511
|
+
console.log(` API: ${metadata.apiUrl}`);
|
|
4512
|
+
});
|
|
4513
|
+
runnerService.command("remove").description("Remove the startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4514
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4515
|
+
if (!context) {
|
|
4516
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4520
|
+
accountId: context.metadata.amistioAccountId,
|
|
4521
|
+
projectId: context.metadata.amistioProjectId,
|
|
4522
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4523
|
+
machineId: runnerMachineId()
|
|
4524
|
+
});
|
|
4525
|
+
const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4526
|
+
console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
|
|
4527
|
+
});
|
|
4528
|
+
async function runWatchIteration({ command, context, options, runnerId }) {
|
|
4529
|
+
try {
|
|
4530
|
+
return await runNextWorkItem({
|
|
4531
|
+
apiClient: context.client,
|
|
4532
|
+
projectId: context.metadata.amistioProjectId,
|
|
4533
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4534
|
+
runnerId,
|
|
4535
|
+
root: options.root,
|
|
4536
|
+
sessionPolicy: normalizeSessionPolicy(options.session),
|
|
4537
|
+
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
4538
|
+
...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
|
|
4539
|
+
...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
|
|
4540
|
+
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
4541
|
+
dryRun: Boolean(options.dryRun),
|
|
4542
|
+
stream: options.stream,
|
|
4543
|
+
commandContext: {
|
|
4544
|
+
accountId: context.metadata.amistioAccountId,
|
|
4545
|
+
apiUrl: options.apiUrl,
|
|
4546
|
+
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
4547
|
+
projectId: context.metadata.amistioProjectId,
|
|
4548
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4549
|
+
root: options.root,
|
|
4550
|
+
runnerId
|
|
4551
|
+
},
|
|
4552
|
+
suppressIdleOutput: Boolean(options.watch),
|
|
4553
|
+
verbose: Boolean(options.verbose)
|
|
4554
|
+
});
|
|
4555
|
+
} catch (error) {
|
|
4556
|
+
if (!options.watch) {
|
|
4557
|
+
throw error;
|
|
4558
|
+
}
|
|
4559
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4560
|
+
const message = "Runner hit an error while watching and will keep listening.";
|
|
4561
|
+
if (options.verbose) {
|
|
4562
|
+
console.error(`${message}
|
|
4563
|
+
${detail}`);
|
|
4564
|
+
} else {
|
|
4565
|
+
console.error(`${message} Run with --verbose for details.`);
|
|
4566
|
+
}
|
|
4567
|
+
await Promise.allSettled([
|
|
4568
|
+
context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
|
|
4569
|
+
context.client.recordRunnerLog(context.metadata.amistioProjectId, {
|
|
4570
|
+
runnerId,
|
|
4571
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4572
|
+
status: "failed",
|
|
4573
|
+
message,
|
|
4574
|
+
error: detail,
|
|
4575
|
+
machineId: runnerMachineId()
|
|
4576
|
+
})
|
|
4577
|
+
]);
|
|
4578
|
+
return { status: "failed", exitCode: 1, message };
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
3361
4581
|
async function runNextWorkItem({
|
|
3362
4582
|
apiClient,
|
|
3363
4583
|
dryRun,
|
|
@@ -3372,7 +4592,8 @@ async function runNextWorkItem({
|
|
|
3372
4592
|
explicitTool,
|
|
3373
4593
|
toolCommand,
|
|
3374
4594
|
commandContext,
|
|
3375
|
-
suppressIdleOutput
|
|
4595
|
+
suppressIdleOutput,
|
|
4596
|
+
verbose
|
|
3376
4597
|
}) {
|
|
3377
4598
|
const toolConfig = await resolveRunnerToolConfig({
|
|
3378
4599
|
apiClient,
|
|
@@ -3394,7 +4615,7 @@ async function runNextWorkItem({
|
|
|
3394
4615
|
console.log(toolConfig.message);
|
|
3395
4616
|
return { status: "blocked", exitCode: 1 };
|
|
3396
4617
|
}
|
|
3397
|
-
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId);
|
|
4618
|
+
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, 300, runnerIsolationCapabilityMetadata());
|
|
3398
4619
|
if (!result.workItem) {
|
|
3399
4620
|
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
3400
4621
|
const message = formatProjectNextAction(nextAction);
|
|
@@ -3403,51 +4624,100 @@ async function runNextWorkItem({
|
|
|
3403
4624
|
}
|
|
3404
4625
|
return { status: "idle", exitCode: 0, nextAction, message };
|
|
3405
4626
|
}
|
|
3406
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", runnerHeartbeatMetadata(toolConfig));
|
|
3407
4627
|
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
3408
4628
|
if (dryRun || toolConfig.tool === "none") {
|
|
3409
4629
|
console.log(prompt);
|
|
3410
4630
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3411
4631
|
return { status: "preview", exitCode: 0 };
|
|
3412
4632
|
}
|
|
3413
|
-
const
|
|
4633
|
+
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
4634
|
+
if (worktreeIsolation.status === "blocked") {
|
|
4635
|
+
return { status: "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
4636
|
+
}
|
|
4637
|
+
const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
|
|
4638
|
+
const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
|
|
4639
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
|
|
4640
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
4641
|
+
currentWorkItemId: result.workItem.workItemId,
|
|
4642
|
+
...isolationTelemetry.implementationScopeId ? { currentImplementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
4643
|
+
...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
4644
|
+
...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
|
|
4645
|
+
});
|
|
4646
|
+
const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
3414
4647
|
const sessionContext = await prepareToolSession({
|
|
3415
4648
|
apiClient,
|
|
3416
4649
|
projectId,
|
|
3417
4650
|
repositoryLinkId,
|
|
3418
4651
|
runnerId,
|
|
4652
|
+
machineId: runnerMachineId(),
|
|
3419
4653
|
sessionPolicy: result.workItem.sessionPolicy ?? sessionPolicy,
|
|
3420
4654
|
toolName: preview.toolName,
|
|
3421
4655
|
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
3422
4656
|
supportsSessionReuse: preview.supportsSessionReuse,
|
|
3423
4657
|
resumabilityScope: preview.resumabilityScope,
|
|
3424
|
-
workItem: result.workItem
|
|
4658
|
+
workItem: result.workItem,
|
|
4659
|
+
isolationTelemetry
|
|
3425
4660
|
});
|
|
3426
4661
|
console.log(`Claimed ${result.workItem.workItemId}. Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
4662
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4663
|
+
status: "running",
|
|
4664
|
+
summary: `Local runner started ${preview.toolName} execution.`,
|
|
4665
|
+
idempotencyKey: `runner_milestone_started_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
4666
|
+
metadata: { tool: preview.toolName, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto" }
|
|
4667
|
+
});
|
|
3427
4668
|
const startedAt = Date.now();
|
|
3428
4669
|
const providerSessionStore = new LocalToolSessionStore();
|
|
3429
4670
|
const providerSessionId = sessionContext.toolSession ? await providerSessionStore.getProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName) : void 0;
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
4671
|
+
let toolResult;
|
|
4672
|
+
try {
|
|
4673
|
+
toolResult = await runLocalTool({
|
|
4674
|
+
rootDir: executionRoot,
|
|
4675
|
+
prompt,
|
|
4676
|
+
tool: toolConfig.tool,
|
|
4677
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
4678
|
+
...toolCommand ? { toolCommand } : {},
|
|
4679
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
4680
|
+
streamOutput: stream,
|
|
4681
|
+
...sessionContext.toolSession ? {
|
|
4682
|
+
session: {
|
|
4683
|
+
toolSessionId: sessionContext.toolSession.toolSessionId,
|
|
4684
|
+
policy: sessionContext.policy,
|
|
4685
|
+
decision: sessionContext.decision,
|
|
4686
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
4687
|
+
}
|
|
4688
|
+
} : {}
|
|
4689
|
+
});
|
|
4690
|
+
} catch (error) {
|
|
4691
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4692
|
+
const durationMs2 = Date.now() - startedAt;
|
|
4693
|
+
const message = `${preview.toolName} failed before returning a result.`;
|
|
4694
|
+
await Promise.allSettled([
|
|
4695
|
+
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
4696
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
4697
|
+
apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
|
|
4698
|
+
...isolationTelemetry,
|
|
4699
|
+
tool: preview.toolName,
|
|
4700
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
4701
|
+
durationMs: durationMs2,
|
|
4702
|
+
message,
|
|
4703
|
+
error: detail,
|
|
4704
|
+
...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
|
|
4705
|
+
sessionPolicy: sessionContext.policy,
|
|
4706
|
+
sessionDecision: sessionContext.decision,
|
|
4707
|
+
sessionDecisionReason: sessionContext.reason
|
|
4708
|
+
}),
|
|
4709
|
+
recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4710
|
+
status: "failed",
|
|
4711
|
+
summary: message,
|
|
4712
|
+
idempotencyKey: `runner_milestone_tool_throw_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
4713
|
+
metadata: { tool: preview.toolName, error: detail }
|
|
4714
|
+
})
|
|
4715
|
+
]);
|
|
4716
|
+
if (verbose || !stream) {
|
|
4717
|
+
console.error(detail);
|
|
4718
|
+
}
|
|
4719
|
+
return { status: "failed", exitCode: 1, message };
|
|
4720
|
+
}
|
|
3451
4721
|
if (sessionContext.toolSession && toolResult.providerSessionId) {
|
|
3452
4722
|
await providerSessionStore.setProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName, toolResult.providerSessionId);
|
|
3453
4723
|
}
|
|
@@ -3471,6 +4741,35 @@ async function runNextWorkItem({
|
|
|
3471
4741
|
workItem: result.workItem
|
|
3472
4742
|
});
|
|
3473
4743
|
}
|
|
4744
|
+
if (result.workItem.workKind === "assistantQuestion") {
|
|
4745
|
+
return finalizeAssistantQuestionWork({
|
|
4746
|
+
apiClient,
|
|
4747
|
+
durationMs: Date.now() - startedAt,
|
|
4748
|
+
projectId,
|
|
4749
|
+
repositoryLinkId,
|
|
4750
|
+
runnerId,
|
|
4751
|
+
sessionContext,
|
|
4752
|
+
toolConfig,
|
|
4753
|
+
toolName: preview.toolName,
|
|
4754
|
+
toolResult,
|
|
4755
|
+
workItem: result.workItem
|
|
4756
|
+
});
|
|
4757
|
+
}
|
|
4758
|
+
if (result.workItem.workKind === "impactPreview") {
|
|
4759
|
+
return finalizeImpactPreviewWork({
|
|
4760
|
+
apiClient,
|
|
4761
|
+
durationMs: Date.now() - startedAt,
|
|
4762
|
+
projectId,
|
|
4763
|
+
repositoryLinkId,
|
|
4764
|
+
root,
|
|
4765
|
+
runnerId,
|
|
4766
|
+
sessionContext,
|
|
4767
|
+
toolConfig,
|
|
4768
|
+
toolName: preview.toolName,
|
|
4769
|
+
toolResult,
|
|
4770
|
+
workItem: result.workItem
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
3474
4773
|
const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
|
|
3475
4774
|
const durationMs = Date.now() - startedAt;
|
|
3476
4775
|
const failureExcerpt = toolResult.exitCode === 0 ? void 0 : truncateLogExcerpt(toolResult.stderr || toolResult.stdout);
|
|
@@ -3498,6 +4797,7 @@ async function runNextWorkItem({
|
|
|
3498
4797
|
...toolResult.model ? { model: toolResult.model } : {},
|
|
3499
4798
|
durationMs,
|
|
3500
4799
|
message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
4800
|
+
...isolationTelemetry,
|
|
3501
4801
|
sessionPolicy: sessionContext.policy,
|
|
3502
4802
|
sessionDecision: sessionContext.decision,
|
|
3503
4803
|
sessionDecisionReason: sessionContext.reason,
|
|
@@ -3509,11 +4809,85 @@ async function runNextWorkItem({
|
|
|
3509
4809
|
...failureExcerpt ? { error: failureExcerpt } : {}
|
|
3510
4810
|
}
|
|
3511
4811
|
);
|
|
4812
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4813
|
+
status: finalStatus,
|
|
4814
|
+
summary: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
4815
|
+
idempotencyKey: `runner_milestone_finished_${result.workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
4816
|
+
metadata: { tool: preview.toolName, durationMs, exitCode: toolResult.exitCode, verificationSummary: finalStatus === "completed" ? "Local execution reported completion." : "Local execution reported failure.", executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "", executionBranch: isolationTelemetry.executionBranch ?? "" }
|
|
4817
|
+
});
|
|
3512
4818
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3513
4819
|
const durationSeconds = Math.round(durationMs / 1e3);
|
|
3514
4820
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
3515
4821
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
3516
4822
|
}
|
|
4823
|
+
async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
4824
|
+
if (!needsGitWorktreeIsolation(workItem)) {
|
|
4825
|
+
return { status: "ready" };
|
|
4826
|
+
}
|
|
4827
|
+
const identity = resolveWorktreeIdentity(workItem);
|
|
4828
|
+
try {
|
|
4829
|
+
const isolation = await prepareGitWorktreeIsolation(root, workItem);
|
|
4830
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4831
|
+
status: "running",
|
|
4832
|
+
summary: `Prepared Git worktree ${isolation.worktreeKey} on ${isolation.branch}.`,
|
|
4833
|
+
idempotencyKey: `runner_milestone_worktree_${workItem.workItemId}_${workItem.attempt}`,
|
|
4834
|
+
metadata: { executionWorktreeKey: isolation.worktreeKey, executionBranch: isolation.branch, implementationScopeId: isolation.implementationScopeId }
|
|
4835
|
+
});
|
|
4836
|
+
return { status: "ready", isolation };
|
|
4837
|
+
} catch (error) {
|
|
4838
|
+
const message = errorMessage3(error);
|
|
4839
|
+
const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
|
|
4840
|
+
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, "blocked", `worktree_${workItem.workItemId}_${randomUUID()}`, runnerId, {
|
|
4841
|
+
...telemetry,
|
|
4842
|
+
message,
|
|
4843
|
+
blockerReason: message,
|
|
4844
|
+
error: message
|
|
4845
|
+
});
|
|
4846
|
+
await recordRunnerMilestone(apiClient, projectId, statusResult.workItem, runnerId, repositoryLinkId, {
|
|
4847
|
+
status: "blocked",
|
|
4848
|
+
summary: message,
|
|
4849
|
+
idempotencyKey: `runner_milestone_worktree_blocked_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
4850
|
+
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "" }
|
|
4851
|
+
});
|
|
4852
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", {
|
|
4853
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
4854
|
+
currentWorkItemId: workItem.workItemId,
|
|
4855
|
+
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
4856
|
+
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
4857
|
+
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
4858
|
+
});
|
|
4859
|
+
console.error(message);
|
|
4860
|
+
return { status: "blocked", message };
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
function workItemIsolationTelemetry(workItem, isolation) {
|
|
4864
|
+
const implementationScopeId = isolation?.implementationScopeId ?? workItem.implementationScopeId;
|
|
4865
|
+
const executionBranch = isolation?.branch ?? workItem.executionBranch;
|
|
4866
|
+
const executionWorktreeKey = isolation?.worktreeKey ?? workItem.executionWorktreeKey;
|
|
4867
|
+
const repositoryLockId = isolation?.repositoryLockId ?? workItem.repositoryLockId;
|
|
4868
|
+
return {
|
|
4869
|
+
...needsGitWorktreeIsolation(workItem) ? { isolationMode: "gitWorktree" } : workItem.isolationMode ? { isolationMode: workItem.isolationMode } : {},
|
|
4870
|
+
...workItem.claimLeaseId ? { claimLeaseId: workItem.claimLeaseId } : {},
|
|
4871
|
+
...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
|
|
4872
|
+
...implementationScopeId ? { implementationScopeId } : {},
|
|
4873
|
+
...executionBranch ? { executionBranch } : {},
|
|
4874
|
+
...executionWorktreeKey ? { executionWorktreeKey } : {},
|
|
4875
|
+
...repositoryLockId ? { repositoryLockId } : {},
|
|
4876
|
+
...isolation?.baseRevision && isolation.baseRevision !== "unknown" ? { baseRevision: isolation.baseRevision } : {},
|
|
4877
|
+
machineId: runnerMachineId()
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
async function recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, input) {
|
|
4881
|
+
await apiClient.recordActivityEvent(projectId, {
|
|
4882
|
+
eventType: "runnerMilestone",
|
|
4883
|
+
runnerId,
|
|
4884
|
+
repositoryLinkId,
|
|
4885
|
+
relatedWorkItemId: workItem.workItemId,
|
|
4886
|
+
...workItem.reviewDocumentId ? { relatedDocumentId: workItem.reviewDocumentId } : workItem.impactDocumentId ? { relatedDocumentId: workItem.impactDocumentId } : {},
|
|
4887
|
+
...workItem.generatedDraftId ? { generatedDraftId: workItem.generatedDraftId } : {},
|
|
4888
|
+
...input
|
|
4889
|
+
}).catch(() => void 0);
|
|
4890
|
+
}
|
|
3517
4891
|
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
3518
4892
|
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
3519
4893
|
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];
|
|
@@ -3564,7 +4938,7 @@ async function restartCurrentRunner(context) {
|
|
|
3564
4938
|
const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
|
|
3565
4939
|
return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
|
|
3566
4940
|
} catch (error) {
|
|
3567
|
-
return { succeeded: false, message: "Background restart failed.", error:
|
|
4941
|
+
return { succeeded: false, message: "Background restart failed.", error: errorMessage3(error) };
|
|
3568
4942
|
}
|
|
3569
4943
|
}
|
|
3570
4944
|
function runnerCommandLabel(commandKind) {
|
|
@@ -3591,7 +4965,7 @@ async function finalizeBrainGenerationWork({
|
|
|
3591
4965
|
artifacts = parseBrainGenerationArtifacts(`${toolResult.stdout}
|
|
3592
4966
|
${toolResult.stderr}`);
|
|
3593
4967
|
} catch (error) {
|
|
3594
|
-
generationError =
|
|
4968
|
+
generationError = errorMessage3(error);
|
|
3595
4969
|
}
|
|
3596
4970
|
} else {
|
|
3597
4971
|
generationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -3629,11 +5003,17 @@ ${toolResult.stderr}`);
|
|
|
3629
5003
|
...sessionTelemetry,
|
|
3630
5004
|
message: completionMessage
|
|
3631
5005
|
});
|
|
5006
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5007
|
+
status: "completed",
|
|
5008
|
+
summary: completionMessage,
|
|
5009
|
+
idempotencyKey: `runner_milestone_generation_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
5010
|
+
metadata: { tool: toolName, durationMs, artifactCount: result.documents.length, verificationSummary: "Generated artifacts were accepted by the Amistio API for review." }
|
|
5011
|
+
});
|
|
3632
5012
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3633
5013
|
console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
3634
5014
|
return { status: "completed", exitCode: 0 };
|
|
3635
5015
|
}
|
|
3636
|
-
await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
5016
|
+
const failedResult = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
3637
5017
|
status: "failed",
|
|
3638
5018
|
runnerId,
|
|
3639
5019
|
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
|
|
@@ -3643,11 +5023,216 @@ ${toolResult.stderr}`);
|
|
|
3643
5023
|
message: `${toolName} did not produce valid brain artifacts.`,
|
|
3644
5024
|
...generationError ? { error: generationError } : {}
|
|
3645
5025
|
});
|
|
5026
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5027
|
+
status: "failed",
|
|
5028
|
+
summary: generationError ?? `${toolName} did not produce valid brain artifacts.`,
|
|
5029
|
+
idempotencyKey: `runner_milestone_generation_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
5030
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Generation output could not be parsed into approved artifact JSON." }
|
|
5031
|
+
});
|
|
3646
5032
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3647
5033
|
console.error(generationError ?? "Local runner generation failed.");
|
|
3648
5034
|
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
3649
5035
|
}
|
|
5036
|
+
async function finalizeAssistantQuestionWork({
|
|
5037
|
+
apiClient,
|
|
5038
|
+
durationMs,
|
|
5039
|
+
projectId,
|
|
5040
|
+
repositoryLinkId,
|
|
5041
|
+
runnerId,
|
|
5042
|
+
sessionContext,
|
|
5043
|
+
toolConfig,
|
|
5044
|
+
toolName,
|
|
5045
|
+
toolResult,
|
|
5046
|
+
workItem
|
|
5047
|
+
}) {
|
|
5048
|
+
let answerResult;
|
|
5049
|
+
let answerError;
|
|
5050
|
+
if (toolResult.exitCode === 0) {
|
|
5051
|
+
try {
|
|
5052
|
+
answerResult = parseAssistantAnswerResult(`${toolResult.stdout}
|
|
5053
|
+
${toolResult.stderr}`);
|
|
5054
|
+
} catch (error) {
|
|
5055
|
+
answerError = errorMessage3(error);
|
|
5056
|
+
}
|
|
5057
|
+
} else {
|
|
5058
|
+
answerError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
5059
|
+
}
|
|
5060
|
+
const finalStatus = answerResult ? "completed" : "failed";
|
|
5061
|
+
const updatedToolSession = await finalizeToolSession({
|
|
5062
|
+
apiClient,
|
|
5063
|
+
projectId,
|
|
5064
|
+
status: finalStatus,
|
|
5065
|
+
runnerId,
|
|
5066
|
+
workItemId: workItem.workItemId,
|
|
5067
|
+
stdout: toolResult.stdout,
|
|
5068
|
+
...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
|
|
5069
|
+
...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
|
|
5070
|
+
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
5071
|
+
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
5072
|
+
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
5073
|
+
});
|
|
5074
|
+
const sessionTelemetry = {
|
|
5075
|
+
sessionPolicy: sessionContext.policy,
|
|
5076
|
+
sessionDecision: sessionContext.decision,
|
|
5077
|
+
sessionDecisionReason: sessionContext.reason,
|
|
5078
|
+
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
5079
|
+
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
5080
|
+
};
|
|
5081
|
+
if (answerResult) {
|
|
5082
|
+
const result = await apiClient.submitAssistantResult(projectId, workItem.workItemId, {
|
|
5083
|
+
status: "completed",
|
|
5084
|
+
runnerId,
|
|
5085
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID()}`,
|
|
5086
|
+
answer: answerResult.answer,
|
|
5087
|
+
sourceBoundary: answerResult.sourceBoundary,
|
|
5088
|
+
citations: answerResult.citations,
|
|
5089
|
+
tool: toolName,
|
|
5090
|
+
durationMs,
|
|
5091
|
+
...sessionTelemetry,
|
|
5092
|
+
message: `${toolName} returned a project knowledge answer.`
|
|
5093
|
+
});
|
|
5094
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5095
|
+
status: "completed",
|
|
5096
|
+
summary: `${toolName} returned a project knowledge answer.`,
|
|
5097
|
+
idempotencyKey: `runner_milestone_assistant_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
5098
|
+
metadata: { tool: toolName, durationMs, sourceBoundary: answerResult.sourceBoundary, citationCount: answerResult.citations.length }
|
|
5099
|
+
});
|
|
5100
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
5101
|
+
console.log("Project knowledge answer returned.");
|
|
5102
|
+
return { status: "completed", exitCode: 0 };
|
|
5103
|
+
}
|
|
5104
|
+
const failedResult = await apiClient.submitAssistantResult(projectId, workItem.workItemId, {
|
|
5105
|
+
status: "failed",
|
|
5106
|
+
runnerId,
|
|
5107
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID()}`,
|
|
5108
|
+
tool: toolName,
|
|
5109
|
+
durationMs,
|
|
5110
|
+
...sessionTelemetry,
|
|
5111
|
+
message: `${toolName} did not produce a valid project knowledge answer.`,
|
|
5112
|
+
...answerError ? { error: answerError } : {}
|
|
5113
|
+
});
|
|
5114
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5115
|
+
status: "failed",
|
|
5116
|
+
summary: answerError ?? `${toolName} did not produce a valid project knowledge answer.`,
|
|
5117
|
+
idempotencyKey: `runner_milestone_assistant_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
5118
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Assistant output did not include a valid structured answer." }
|
|
5119
|
+
});
|
|
5120
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
5121
|
+
console.error(answerError ?? "Local runner assistant answer failed.");
|
|
5122
|
+
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
5123
|
+
}
|
|
5124
|
+
async function finalizeImpactPreviewWork({
|
|
5125
|
+
apiClient,
|
|
5126
|
+
durationMs,
|
|
5127
|
+
projectId,
|
|
5128
|
+
repositoryLinkId,
|
|
5129
|
+
root,
|
|
5130
|
+
runnerId,
|
|
5131
|
+
sessionContext,
|
|
5132
|
+
toolConfig,
|
|
5133
|
+
toolName,
|
|
5134
|
+
toolResult,
|
|
5135
|
+
workItem
|
|
5136
|
+
}) {
|
|
5137
|
+
let report = void 0;
|
|
5138
|
+
let previewError;
|
|
5139
|
+
if (toolResult.exitCode === 0) {
|
|
5140
|
+
try {
|
|
5141
|
+
report = parseImpactPreviewResult(`${toolResult.stdout}
|
|
5142
|
+
${toolResult.stderr}`);
|
|
5143
|
+
} catch (error) {
|
|
5144
|
+
previewError = errorMessage3(error);
|
|
5145
|
+
}
|
|
5146
|
+
} else {
|
|
5147
|
+
previewError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
5148
|
+
}
|
|
5149
|
+
const finalStatus = report ? "completed" : "failed";
|
|
5150
|
+
const updatedToolSession = await finalizeToolSession({
|
|
5151
|
+
apiClient,
|
|
5152
|
+
projectId,
|
|
5153
|
+
status: finalStatus,
|
|
5154
|
+
runnerId,
|
|
5155
|
+
workItemId: workItem.workItemId,
|
|
5156
|
+
stdout: toolResult.stdout,
|
|
5157
|
+
...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
|
|
5158
|
+
...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
|
|
5159
|
+
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
5160
|
+
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
5161
|
+
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
5162
|
+
});
|
|
5163
|
+
const sessionTelemetry = {
|
|
5164
|
+
sessionPolicy: sessionContext.policy,
|
|
5165
|
+
sessionDecision: sessionContext.decision,
|
|
5166
|
+
sessionDecisionReason: sessionContext.reason,
|
|
5167
|
+
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
5168
|
+
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
5169
|
+
};
|
|
5170
|
+
if (report) {
|
|
5171
|
+
const metadata = await readProjectLink(root).catch(() => void 0);
|
|
5172
|
+
const result = await apiClient.submitImpactPreviewResult(projectId, workItem.workItemId, {
|
|
5173
|
+
status: "completed",
|
|
5174
|
+
runnerId,
|
|
5175
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID()}`,
|
|
5176
|
+
report: {
|
|
5177
|
+
...report,
|
|
5178
|
+
analyzedRepoRevision: report.analyzedRepoRevision ?? metadata?.lastSyncedRevision
|
|
5179
|
+
},
|
|
5180
|
+
tool: toolName,
|
|
5181
|
+
durationMs,
|
|
5182
|
+
...sessionTelemetry,
|
|
5183
|
+
message: `${toolName} returned an implementation impact preview.`
|
|
5184
|
+
});
|
|
5185
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5186
|
+
status: "completed",
|
|
5187
|
+
summary: `${toolName} returned an implementation impact preview.`,
|
|
5188
|
+
idempotencyKey: `runner_milestone_impact_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
5189
|
+
metadata: { tool: toolName, durationMs, riskLevel: report.riskLevel, likelyPathCount: report.likelyPaths.length, verificationSummary: report.verificationPlan }
|
|
5190
|
+
});
|
|
5191
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
5192
|
+
console.log(result.implementationWorkItem ? "Impact preview returned; implementation work is now queued." : "Impact preview returned.");
|
|
5193
|
+
return { status: "completed", exitCode: 0 };
|
|
5194
|
+
}
|
|
5195
|
+
const failedResult = await apiClient.submitImpactPreviewResult(projectId, workItem.workItemId, {
|
|
5196
|
+
status: "failed",
|
|
5197
|
+
runnerId,
|
|
5198
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID()}`,
|
|
5199
|
+
tool: toolName,
|
|
5200
|
+
durationMs,
|
|
5201
|
+
...sessionTelemetry,
|
|
5202
|
+
message: `${toolName} did not produce a valid impact preview.`,
|
|
5203
|
+
...previewError ? { error: previewError } : {}
|
|
5204
|
+
});
|
|
5205
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
5206
|
+
status: "failed",
|
|
5207
|
+
summary: previewError ?? `${toolName} did not produce a valid impact preview.`,
|
|
5208
|
+
idempotencyKey: `runner_milestone_impact_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
5209
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Impact preview output did not include valid structured JSON." }
|
|
5210
|
+
});
|
|
5211
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
5212
|
+
console.error(previewError ?? "Local runner impact preview failed.");
|
|
5213
|
+
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
5214
|
+
}
|
|
3650
5215
|
async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
|
|
5216
|
+
if (workItem.workKind === "assistantQuestion") {
|
|
5217
|
+
const [{ documents: documents2 }, { messages: messages2 }] = await Promise.all([
|
|
5218
|
+
apiClient.listBrainDocuments(projectId),
|
|
5219
|
+
apiClient.listAssistantMessages(projectId)
|
|
5220
|
+
]);
|
|
5221
|
+
const question = messages2.find((message) => message.messageId === workItem.assistantMessageId || message.id === workItem.assistantMessageId) ?? messages2.filter((message) => message.workItemId === workItem.workItemId).sort((first, second) => Date.parse(first.createdAt) - Date.parse(second.createdAt))[0];
|
|
5222
|
+
return createWorkExecutionPrompt(workItem, {
|
|
5223
|
+
...question ? { assistantQuestion: { question, messages: messages2, documents: documents2 } } : {}
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
5226
|
+
if (workItem.workKind === "impactPreview") {
|
|
5227
|
+
const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
|
|
5228
|
+
const implementationPrompt = workItem.impactDocumentId ? documents2.find((document) => document.documentId === workItem.impactDocumentId || document.id === workItem.impactDocumentId) : documents2.find((document) => document.frontmatter.generatedDraftId === workItem.generatedDraftId && (document.frontmatter.createsWorkItem === true || document.documentType === "prompt"));
|
|
5229
|
+
return createWorkExecutionPrompt(workItem, {
|
|
5230
|
+
impactPreview: {
|
|
5231
|
+
documents: documents2,
|
|
5232
|
+
...implementationPrompt ? { implementationPrompt } : {}
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
3651
5236
|
if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
|
|
3652
5237
|
return createWorkExecutionPrompt(workItem);
|
|
3653
5238
|
}
|
|
@@ -3720,6 +5305,8 @@ function isPromptReadyStatus(status) {
|
|
|
3720
5305
|
}
|
|
3721
5306
|
async function prepareToolSession({
|
|
3722
5307
|
apiClient,
|
|
5308
|
+
isolationTelemetry,
|
|
5309
|
+
machineId,
|
|
3723
5310
|
projectId,
|
|
3724
5311
|
repositoryLinkId,
|
|
3725
5312
|
runnerId,
|
|
@@ -3738,6 +5325,7 @@ async function prepareToolSession({
|
|
|
3738
5325
|
toolName,
|
|
3739
5326
|
runnerId,
|
|
3740
5327
|
repositoryLinkId,
|
|
5328
|
+
machineId,
|
|
3741
5329
|
supportsSessionReuse
|
|
3742
5330
|
});
|
|
3743
5331
|
if (selection.decision === "skipped") {
|
|
@@ -3747,8 +5335,12 @@ async function prepareToolSession({
|
|
|
3747
5335
|
const { toolSession: toolSession2 } = await apiClient.updateToolSession(projectId, selection.toolSession.toolSessionId, {
|
|
3748
5336
|
status: "active",
|
|
3749
5337
|
runnerId,
|
|
5338
|
+
machineId,
|
|
3750
5339
|
lastWorkItemId: workItem.workItemId,
|
|
3751
|
-
reusePolicy: sessionPolicy
|
|
5340
|
+
reusePolicy: sessionPolicy,
|
|
5341
|
+
...isolationTelemetry.implementationScopeId ? { implementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
5342
|
+
...isolationTelemetry.executionWorktreeKey ? { executionWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
5343
|
+
...isolationTelemetry.isolationMode ? { isolationMode: isolationTelemetry.isolationMode } : {}
|
|
3752
5344
|
});
|
|
3753
5345
|
return { ...selection, toolSession: toolSession2 };
|
|
3754
5346
|
}
|
|
@@ -3764,9 +5356,14 @@ async function prepareToolSession({
|
|
|
3764
5356
|
summary: selection.reason,
|
|
3765
5357
|
status: "active",
|
|
3766
5358
|
runnerId,
|
|
5359
|
+
machineId,
|
|
3767
5360
|
lastWorkItemId: workItem.workItemId,
|
|
3768
5361
|
messageCount: 0,
|
|
3769
5362
|
reusePolicy: sessionPolicy,
|
|
5363
|
+
...workItem.requestedByUserId ?? workItem.requestedBy ? { createdByUserId: workItem.requestedByUserId ?? workItem.requestedBy } : {},
|
|
5364
|
+
...isolationTelemetry.implementationScopeId ? { implementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
5365
|
+
...isolationTelemetry.executionWorktreeKey ? { executionWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
5366
|
+
...isolationTelemetry.isolationMode ? { isolationMode: isolationTelemetry.isolationMode } : {},
|
|
3770
5367
|
...workItem.sessionGroupKey ? { sessionGroupKey: workItem.sessionGroupKey } : {}
|
|
3771
5368
|
});
|
|
3772
5369
|
return { ...selection, toolSession };
|
|
@@ -3827,9 +5424,12 @@ function summarizeToolOutput(value) {
|
|
|
3827
5424
|
}
|
|
3828
5425
|
return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
|
|
3829
5426
|
}
|
|
3830
|
-
function
|
|
5427
|
+
function errorMessage3(error) {
|
|
3831
5428
|
return error instanceof Error ? error.message : String(error);
|
|
3832
5429
|
}
|
|
5430
|
+
function errorDetail(error) {
|
|
5431
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
5432
|
+
}
|
|
3833
5433
|
function truncateLogExcerpt(value) {
|
|
3834
5434
|
const trimmed = value.trim();
|
|
3835
5435
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
@@ -3854,10 +5454,10 @@ function parseInvocationChannel(value) {
|
|
|
3854
5454
|
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
3855
5455
|
}
|
|
3856
5456
|
function inferRepoName(root) {
|
|
3857
|
-
return
|
|
5457
|
+
return path13.basename(path13.resolve(root)) || "repository";
|
|
3858
5458
|
}
|
|
3859
5459
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
3860
|
-
return
|
|
5460
|
+
return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
3861
5461
|
}
|
|
3862
5462
|
function defaultApiUrl() {
|
|
3863
5463
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -3971,14 +5571,25 @@ function toRunnerToolCapabilities(tools) {
|
|
|
3971
5571
|
execution: tool.execution,
|
|
3972
5572
|
supportsSessionReuse: tool.supportsSessionReuse,
|
|
3973
5573
|
resumabilityScope: tool.resumabilityScope,
|
|
3974
|
-
supportsModelSelection: tool.supportsModelSelection
|
|
5574
|
+
supportsModelSelection: tool.supportsModelSelection,
|
|
5575
|
+
supportsBranchIsolation: true,
|
|
5576
|
+
supportsGitWorktreeIsolation: true
|
|
3975
5577
|
}));
|
|
3976
5578
|
}
|
|
5579
|
+
function runnerIsolationCapabilityMetadata() {
|
|
5580
|
+
return {
|
|
5581
|
+
machineId: runnerMachineId(),
|
|
5582
|
+
supportsBranchIsolation: true,
|
|
5583
|
+
supportsGitWorktreeIsolation: true
|
|
5584
|
+
};
|
|
5585
|
+
}
|
|
3977
5586
|
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
3978
5587
|
return {
|
|
3979
5588
|
version: CLI_VERSION,
|
|
3980
5589
|
mode,
|
|
3981
|
-
hostname:
|
|
5590
|
+
hostname: os7.hostname(),
|
|
5591
|
+
...runnerIsolationCapabilityMetadata(),
|
|
5592
|
+
resourceUsage: sampleCurrentRunnerResourceUsage(),
|
|
3982
5593
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
3983
5594
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
3984
5595
|
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
@@ -3990,6 +5601,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
3990
5601
|
...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
|
|
3991
5602
|
};
|
|
3992
5603
|
}
|
|
5604
|
+
function runnerMachineId() {
|
|
5605
|
+
return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
|
|
5606
|
+
}
|
|
3993
5607
|
async function delay(milliseconds) {
|
|
3994
5608
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
3995
5609
|
}
|