@amistio/cli 0.1.4 → 0.1.6
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/dist/index.js +1325 -168
- 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
|
|
4
|
+
import { createHash as createHash5, randomUUID } from "node:crypto";
|
|
5
5
|
import { writeFile as writeFile8 } from "node:fs/promises";
|
|
6
6
|
import os5 from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import path12 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,37 @@ 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 workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
|
|
107
145
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
108
146
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
109
147
|
var projectStatusSchema = z.enum(["active", "archived"]);
|
|
148
|
+
var workspaceScopeKindSchema = z.enum(["personal", "organization"]);
|
|
149
|
+
var personalWorkspaceScopeSchema = z.object({
|
|
150
|
+
kind: z.literal("personal"),
|
|
151
|
+
accountId: z.string().min(1),
|
|
152
|
+
userId: z.string().min(1),
|
|
153
|
+
label: z.string().trim().min(1).default("Personal workspace")
|
|
154
|
+
});
|
|
155
|
+
var organizationWorkspaceScopeSchema = z.object({
|
|
156
|
+
kind: z.literal("organization"),
|
|
157
|
+
accountId: z.string().min(1),
|
|
158
|
+
organizationId: z.string().min(1),
|
|
159
|
+
organizationSlug: z.string().trim().min(1).optional(),
|
|
160
|
+
organizationRole: z.string().trim().min(1).optional(),
|
|
161
|
+
label: z.string().trim().min(1).default("Organization workspace")
|
|
162
|
+
});
|
|
163
|
+
var activeWorkspaceScopeSchema = z.discriminatedUnion("kind", [personalWorkspaceScopeSchema, organizationWorkspaceScopeSchema]);
|
|
164
|
+
var organizationWorkspaceOnboardingStatusSchema = z.enum(["available", "upgradeRequired", "authRequired", "unconfigured"]);
|
|
165
|
+
var organizationWorkspaceOnboardingSchema = z.object({
|
|
166
|
+
enabled: z.boolean(),
|
|
167
|
+
canCreate: z.boolean(),
|
|
168
|
+
status: organizationWorkspaceOnboardingStatusSchema,
|
|
169
|
+
reason: z.string().trim().min(1).optional()
|
|
170
|
+
});
|
|
110
171
|
var baseItemSchema = z.object({
|
|
111
172
|
id: z.string().min(1),
|
|
112
173
|
type: itemTypeSchema,
|
|
@@ -212,15 +273,36 @@ var workItemSchema = baseItemSchema.extend({
|
|
|
212
273
|
executionMode: executionModeSchema.optional(),
|
|
213
274
|
title: z.string().min(1),
|
|
214
275
|
requestedBy: z.string().min(1),
|
|
276
|
+
requestedByUserId: z.string().min(1).optional(),
|
|
215
277
|
approvedBy: z.string().min(1).optional(),
|
|
278
|
+
approvedByUserId: z.string().min(1).optional(),
|
|
216
279
|
sourceWish: z.string().min(1).optional(),
|
|
217
280
|
generatedDraftId: z.string().min(1).optional(),
|
|
281
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
218
282
|
reviewThreadId: z.string().min(1).optional(),
|
|
219
283
|
reviewDocumentId: z.string().min(1).optional(),
|
|
220
284
|
reviewDocumentRevision: z.number().int().nonnegative().optional(),
|
|
221
285
|
reviewMessageId: z.string().min(1).optional(),
|
|
286
|
+
assistantMessageId: z.string().min(1).optional(),
|
|
287
|
+
assistantQuestionMode: assistantQuestionModeSchema.optional(),
|
|
288
|
+
impactReportId: z.string().min(1).optional(),
|
|
289
|
+
impactDocumentId: z.string().min(1).optional(),
|
|
290
|
+
impactDocumentRevision: z.number().int().nonnegative().optional(),
|
|
222
291
|
claimedByRunnerId: z.string().optional(),
|
|
292
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
293
|
+
machineId: z.string().min(1).optional(),
|
|
294
|
+
claimLeaseId: z.string().min(1).optional(),
|
|
295
|
+
claimAttempt: z.number().int().nonnegative().optional(),
|
|
223
296
|
leaseExpiresAt: isoDateTimeSchema.optional(),
|
|
297
|
+
controllingAdrId: z.string().min(1).optional(),
|
|
298
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
299
|
+
executionBranch: z.string().min(1).optional(),
|
|
300
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
301
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
302
|
+
repositoryLockId: z.string().min(1).optional(),
|
|
303
|
+
baseRevision: z.string().min(1).optional(),
|
|
304
|
+
baseContentHash: z.string().min(1).optional(),
|
|
305
|
+
blockerReason: z.string().min(1).optional(),
|
|
224
306
|
attempt: z.number().int().nonnegative().default(0),
|
|
225
307
|
idempotencyKey: z.string().min(1),
|
|
226
308
|
sessionPolicy: sessionPolicySchema.optional(),
|
|
@@ -237,10 +319,19 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
237
319
|
runnerId: z.string().min(1),
|
|
238
320
|
repositoryLinkId: z.string().min(1),
|
|
239
321
|
status: z.enum(["online", "offline", "running", "blocked", "removed"]),
|
|
322
|
+
workspaceId: z.string().min(1).optional(),
|
|
323
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
324
|
+
machineId: z.string().min(1).optional(),
|
|
240
325
|
version: z.string().optional(),
|
|
241
326
|
mode: z.enum(["foreground", "background"]).optional(),
|
|
242
327
|
hostname: z.string().min(1).optional(),
|
|
243
328
|
runnerName: z.string().min(1).optional(),
|
|
329
|
+
supportsBranchIsolation: z.boolean().optional(),
|
|
330
|
+
supportsGitWorktreeIsolation: z.boolean().optional(),
|
|
331
|
+
currentWorkItemId: z.string().min(1).optional(),
|
|
332
|
+
currentImplementationScopeId: z.string().min(1).optional(),
|
|
333
|
+
currentWorktreeKey: z.string().min(1).optional(),
|
|
334
|
+
currentBranch: z.string().min(1).optional(),
|
|
244
335
|
capabilities: z.array(runnerToolCapabilitySchema).optional(),
|
|
245
336
|
requestedTool: runnerToolSelectionSchema.optional(),
|
|
246
337
|
requestedInvocationChannel: runnerInvocationChannelSchema.optional(),
|
|
@@ -269,6 +360,16 @@ var runnerExecutionLogItemSchema = baseItemSchema.extend({
|
|
|
269
360
|
workTitle: z.string().min(1).optional(),
|
|
270
361
|
initiatedBy: z.string().min(1).optional(),
|
|
271
362
|
approvedBy: z.string().min(1).optional(),
|
|
363
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
364
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
365
|
+
machineId: z.string().min(1).optional(),
|
|
366
|
+
controllingAdrId: z.string().min(1).optional(),
|
|
367
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
368
|
+
executionBranch: z.string().min(1).optional(),
|
|
369
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
370
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
371
|
+
repositoryLockId: z.string().min(1).optional(),
|
|
372
|
+
claimLeaseId: z.string().min(1).optional(),
|
|
272
373
|
status: z.enum(["claimed", "running", "completed", "failed", "blocked", "idle"]),
|
|
273
374
|
executionMode: executionModeSchema.optional(),
|
|
274
375
|
tool: z.string().min(1).optional(),
|
|
@@ -293,6 +394,8 @@ var runnerCredentialItemSchema = baseItemSchema.extend({
|
|
|
293
394
|
projectId: z.string().min(1),
|
|
294
395
|
runnerCredentialId: z.string().min(1),
|
|
295
396
|
repositoryLinkId: z.string().min(1),
|
|
397
|
+
pairedByUserId: z.string().min(1).optional(),
|
|
398
|
+
machineId: z.string().min(1).optional(),
|
|
296
399
|
tokenHash: z.string().min(32),
|
|
297
400
|
issuedAt: isoDateTimeSchema,
|
|
298
401
|
lastUsedAt: isoDateTimeSchema.optional(),
|
|
@@ -339,6 +442,231 @@ var planReviewMessageItemSchema = baseItemSchema.extend({
|
|
|
339
442
|
createdByUserId: z.string().min(1).optional(),
|
|
340
443
|
runnerId: z.string().min(1).optional()
|
|
341
444
|
});
|
|
445
|
+
var assistantMessageRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
446
|
+
var assistantMessageStatusSchema = z.enum(["posted", "queued", "running", "completed", "failed"]);
|
|
447
|
+
var assistantSourceBoundarySchema = z.enum(["projectBrain", "localSource", "runnerState", "mixed"]);
|
|
448
|
+
var assistantCitationSchema = z.object({
|
|
449
|
+
source: assistantSourceBoundarySchema,
|
|
450
|
+
documentId: z.string().min(1).optional(),
|
|
451
|
+
title: z.string().min(1).optional(),
|
|
452
|
+
repoPath: z.string().min(1).optional(),
|
|
453
|
+
documentRevision: z.number().int().nonnegative().optional(),
|
|
454
|
+
excerpt: z.string().max(600).optional()
|
|
455
|
+
});
|
|
456
|
+
var assistantMessageItemSchema = baseItemSchema.extend({
|
|
457
|
+
type: z.literal("assistantMessage"),
|
|
458
|
+
projectId: z.string().min(1),
|
|
459
|
+
messageId: z.string().min(1),
|
|
460
|
+
threadId: z.string().min(1),
|
|
461
|
+
role: assistantMessageRoleSchema,
|
|
462
|
+
status: assistantMessageStatusSchema,
|
|
463
|
+
content: z.string().min(1),
|
|
464
|
+
questionMode: assistantQuestionModeSchema.optional(),
|
|
465
|
+
sourceBoundary: assistantSourceBoundarySchema.optional(),
|
|
466
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
467
|
+
relatedDocumentIds: z.array(z.string().min(1)).default([]),
|
|
468
|
+
workItemId: z.string().min(1).optional(),
|
|
469
|
+
responseToMessageId: z.string().min(1).optional(),
|
|
470
|
+
createdByUserId: z.string().min(1).optional(),
|
|
471
|
+
runnerId: z.string().min(1).optional(),
|
|
472
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
473
|
+
repoFingerprint: z.string().min(1).optional(),
|
|
474
|
+
answeredAt: isoDateTimeSchema.optional(),
|
|
475
|
+
error: z.string().optional()
|
|
476
|
+
});
|
|
477
|
+
var assistantAnswerResultSchema = z.object({
|
|
478
|
+
answer: z.string().trim().min(1),
|
|
479
|
+
sourceBoundary: assistantSourceBoundarySchema.default("localSource"),
|
|
480
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
481
|
+
summary: z.string().optional()
|
|
482
|
+
});
|
|
483
|
+
var knowledgeSearchScopeSchema = z.enum(["project", "organization"]);
|
|
484
|
+
var knowledgeSourceTypeSchema = z.enum(["brainDocument", "generatedArtifact", "runnerSummary", "knowledgeEntity", "knowledgeRelation"]);
|
|
485
|
+
var knowledgeChunkStatusSchema = z.enum(["approved", "synced", "stale"]);
|
|
486
|
+
var knowledgeEntityTypeSchema = z.enum(["project", "system", "component", "domain", "tool", "decision", "feature", "risk", "team", "workflow", "unknown"]);
|
|
487
|
+
var knowledgeRelationTypeSchema = z.enum(["uses", "depends_on", "decides", "supersedes", "touches", "blocks", "implements", "mentions"]);
|
|
488
|
+
var knowledgeIndexFreshnessSchema = z.object({
|
|
489
|
+
status: z.enum(["disabled", "fresh", "stale", "indexing", "degraded"]),
|
|
490
|
+
indexedAt: isoDateTimeSchema.optional(),
|
|
491
|
+
staleReason: z.string().trim().min(1).optional(),
|
|
492
|
+
failedChunkCount: z.number().int().nonnegative().default(0),
|
|
493
|
+
indexedChunkCount: z.number().int().nonnegative().default(0)
|
|
494
|
+
});
|
|
495
|
+
var knowledgeSearchChunkSchema = z.object({
|
|
496
|
+
chunkId: z.string().min(1),
|
|
497
|
+
accountId: z.string().min(1),
|
|
498
|
+
projectId: z.string().min(1),
|
|
499
|
+
sourceType: knowledgeSourceTypeSchema.default("brainDocument"),
|
|
500
|
+
documentId: z.string().min(1),
|
|
501
|
+
documentType: documentTypeSchema,
|
|
502
|
+
title: z.string().min(1),
|
|
503
|
+
repoPath: z.string().min(1),
|
|
504
|
+
revision: z.number().int().nonnegative(),
|
|
505
|
+
sectionHeading: z.string().trim().min(1).optional(),
|
|
506
|
+
chunkText: z.string().trim().min(1).max(12e3),
|
|
507
|
+
chunkHash: z.string().min(1),
|
|
508
|
+
contentHash: z.string().min(1),
|
|
509
|
+
sourceBoundary: assistantSourceBoundarySchema.default("projectBrain"),
|
|
510
|
+
status: knowledgeChunkStatusSchema,
|
|
511
|
+
embeddingModel: z.string().trim().min(1).optional(),
|
|
512
|
+
embedding: z.array(z.number()).max(8192).optional(),
|
|
513
|
+
indexedAt: isoDateTimeSchema.optional(),
|
|
514
|
+
createdAt: isoDateTimeSchema,
|
|
515
|
+
updatedAt: isoDateTimeSchema
|
|
516
|
+
});
|
|
517
|
+
var knowledgeEntitySchema = z.object({
|
|
518
|
+
entityId: z.string().min(1),
|
|
519
|
+
accountId: z.string().min(1),
|
|
520
|
+
projectIds: z.array(z.string().min(1)).min(1),
|
|
521
|
+
name: z.string().trim().min(1).max(200),
|
|
522
|
+
entityType: knowledgeEntityTypeSchema,
|
|
523
|
+
aliases: z.array(z.string().trim().min(1).max(200)).default([]),
|
|
524
|
+
description: z.string().trim().min(1).max(1200).optional(),
|
|
525
|
+
sourceDocumentIds: z.array(z.string().min(1)).default([]),
|
|
526
|
+
confidence: z.number().min(0).max(1).default(0.6),
|
|
527
|
+
createdAt: isoDateTimeSchema,
|
|
528
|
+
updatedAt: isoDateTimeSchema
|
|
529
|
+
});
|
|
530
|
+
var knowledgeRelationEndpointSchema = z.object({
|
|
531
|
+
entityId: z.string().min(1),
|
|
532
|
+
name: z.string().trim().min(1).max(200),
|
|
533
|
+
entityType: knowledgeEntityTypeSchema.default("unknown")
|
|
534
|
+
});
|
|
535
|
+
var knowledgeRelationSchema = z.object({
|
|
536
|
+
relationId: z.string().min(1),
|
|
537
|
+
accountId: z.string().min(1),
|
|
538
|
+
projectIds: z.array(z.string().min(1)).min(1),
|
|
539
|
+
relationType: knowledgeRelationTypeSchema,
|
|
540
|
+
from: knowledgeRelationEndpointSchema,
|
|
541
|
+
to: knowledgeRelationEndpointSchema,
|
|
542
|
+
summary: z.string().trim().min(1).max(1200),
|
|
543
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
544
|
+
confidence: z.number().min(0).max(1).default(0.6),
|
|
545
|
+
createdAt: isoDateTimeSchema,
|
|
546
|
+
updatedAt: isoDateTimeSchema
|
|
547
|
+
});
|
|
548
|
+
var knowledgeSearchRequestSchema = z.object({
|
|
549
|
+
query: z.string().trim().min(1).max(2e3),
|
|
550
|
+
scope: knowledgeSearchScopeSchema.default("project"),
|
|
551
|
+
projectIds: z.array(z.string().min(1)).max(100).optional(),
|
|
552
|
+
topK: z.number().int().min(1).max(20).default(6),
|
|
553
|
+
includeRelations: z.boolean().default(true),
|
|
554
|
+
useSemanticRanking: z.boolean().default(false)
|
|
555
|
+
});
|
|
556
|
+
var knowledgeSearchResultSchema = z.object({
|
|
557
|
+
chunkId: z.string().min(1),
|
|
558
|
+
accountId: z.string().min(1),
|
|
559
|
+
projectId: z.string().min(1),
|
|
560
|
+
documentId: z.string().min(1),
|
|
561
|
+
documentType: documentTypeSchema,
|
|
562
|
+
title: z.string().min(1),
|
|
563
|
+
repoPath: z.string().min(1),
|
|
564
|
+
revision: z.number().int().nonnegative(),
|
|
565
|
+
sectionHeading: z.string().trim().min(1).optional(),
|
|
566
|
+
excerpt: z.string().trim().min(1).max(1200),
|
|
567
|
+
sourceBoundary: assistantSourceBoundarySchema.default("projectBrain"),
|
|
568
|
+
score: z.number().optional(),
|
|
569
|
+
citation: assistantCitationSchema
|
|
570
|
+
});
|
|
571
|
+
var knowledgeSearchResponseSchema = z.object({
|
|
572
|
+
query: z.string().trim().min(1),
|
|
573
|
+
scope: knowledgeSearchScopeSchema,
|
|
574
|
+
results: z.array(knowledgeSearchResultSchema).default([]),
|
|
575
|
+
relations: z.array(knowledgeRelationSchema).default([]),
|
|
576
|
+
freshness: knowledgeIndexFreshnessSchema,
|
|
577
|
+
degradedReason: z.string().trim().min(1).optional()
|
|
578
|
+
});
|
|
579
|
+
var knowledgeDiagramFormatSchema = z.enum(["mermaid"]);
|
|
580
|
+
var knowledgeDiagramRequestSchema = knowledgeSearchRequestSchema.extend({
|
|
581
|
+
format: knowledgeDiagramFormatSchema.default("mermaid"),
|
|
582
|
+
saveAsDocument: z.boolean().default(false),
|
|
583
|
+
title: z.string().trim().min(1).max(160).optional()
|
|
584
|
+
});
|
|
585
|
+
var knowledgeDiagramResultSchema = z.object({
|
|
586
|
+
diagramId: z.string().min(1),
|
|
587
|
+
title: z.string().trim().min(1).max(160),
|
|
588
|
+
format: knowledgeDiagramFormatSchema,
|
|
589
|
+
content: z.string().trim().min(1),
|
|
590
|
+
citations: z.array(assistantCitationSchema).default([]),
|
|
591
|
+
sourceResultIds: z.array(z.string().min(1)).default([]),
|
|
592
|
+
documentId: z.string().min(1).optional(),
|
|
593
|
+
freshness: knowledgeIndexFreshnessSchema,
|
|
594
|
+
warnings: z.array(z.string().trim().min(1).max(600)).default([])
|
|
595
|
+
});
|
|
596
|
+
var impactAffectedAreaSchema = z.object({
|
|
597
|
+
name: z.string().trim().min(1),
|
|
598
|
+
description: z.string().trim().min(1).optional()
|
|
599
|
+
});
|
|
600
|
+
var impactPathSchema = z.object({
|
|
601
|
+
repoPath: z.string().trim().min(1),
|
|
602
|
+
reason: z.string().trim().min(1).optional()
|
|
603
|
+
});
|
|
604
|
+
var impactReportItemSchema = baseItemSchema.extend({
|
|
605
|
+
type: z.literal("impactReport"),
|
|
606
|
+
projectId: z.string().min(1),
|
|
607
|
+
impactReportId: z.string().min(1),
|
|
608
|
+
status: impactReportStatusSchema,
|
|
609
|
+
riskLevel: impactRiskLevelSchema.optional(),
|
|
610
|
+
summary: z.string().trim().min(1).optional(),
|
|
611
|
+
affectedAreas: z.array(impactAffectedAreaSchema).default([]),
|
|
612
|
+
likelyPaths: z.array(impactPathSchema).default([]),
|
|
613
|
+
dependencies: z.array(z.string().trim().min(1)).default([]),
|
|
614
|
+
dataSchemaImpact: z.string().trim().min(1).optional(),
|
|
615
|
+
securityPrivacyImpact: z.string().trim().min(1).optional(),
|
|
616
|
+
verificationPlan: z.array(z.string().trim().min(1)).default([]),
|
|
617
|
+
rollbackPlan: z.string().trim().min(1).optional(),
|
|
618
|
+
generatedDraftId: z.string().min(1).optional(),
|
|
619
|
+
sourceWorkItemId: z.string().min(1).optional(),
|
|
620
|
+
documentId: z.string().min(1).optional(),
|
|
621
|
+
documentRevision: z.number().int().nonnegative().optional(),
|
|
622
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
623
|
+
repoFingerprint: z.string().min(1).optional(),
|
|
624
|
+
analyzedRepoRevision: z.number().int().nonnegative().optional(),
|
|
625
|
+
workItemId: z.string().min(1).optional(),
|
|
626
|
+
runnerId: z.string().min(1).optional(),
|
|
627
|
+
createdByUserId: z.string().min(1).optional(),
|
|
628
|
+
completedAt: isoDateTimeSchema.optional(),
|
|
629
|
+
staleReason: z.string().optional(),
|
|
630
|
+
error: z.string().optional()
|
|
631
|
+
});
|
|
632
|
+
var impactPreviewResultSchema = z.object({
|
|
633
|
+
riskLevel: impactRiskLevelSchema,
|
|
634
|
+
summary: z.string().trim().min(1),
|
|
635
|
+
affectedAreas: z.array(impactAffectedAreaSchema).min(1),
|
|
636
|
+
likelyPaths: z.array(impactPathSchema).default([]),
|
|
637
|
+
dependencies: z.array(z.string().trim().min(1)).default([]),
|
|
638
|
+
dataSchemaImpact: z.string().trim().min(1),
|
|
639
|
+
securityPrivacyImpact: z.string().trim().min(1),
|
|
640
|
+
verificationPlan: z.array(z.string().trim().min(1)).min(1),
|
|
641
|
+
rollbackPlan: z.string().trim().min(1),
|
|
642
|
+
analyzedRepoRevision: z.number().int().nonnegative().optional(),
|
|
643
|
+
repoFingerprint: z.string().min(1).optional()
|
|
644
|
+
});
|
|
645
|
+
var activityEventItemSchema = baseItemSchema.extend({
|
|
646
|
+
type: z.literal("activityEvent"),
|
|
647
|
+
projectId: z.string().min(1),
|
|
648
|
+
activityEventId: z.string().min(1),
|
|
649
|
+
eventType: activityEventTypeSchema,
|
|
650
|
+
actorType: activityEventActorSchema,
|
|
651
|
+
actorId: z.string().min(1).optional(),
|
|
652
|
+
status: activityEventStatusSchema,
|
|
653
|
+
summary: z.string().trim().min(1).max(600),
|
|
654
|
+
metadata: activityMetadataSchema,
|
|
655
|
+
idempotencyKey: z.string().min(1),
|
|
656
|
+
occurredAt: isoDateTimeSchema,
|
|
657
|
+
relatedWorkItemId: z.string().min(1).optional(),
|
|
658
|
+
relatedDocumentId: z.string().min(1).optional(),
|
|
659
|
+
generatedDraftId: z.string().min(1).optional(),
|
|
660
|
+
runnerId: z.string().min(1).optional(),
|
|
661
|
+
repositoryLinkId: z.string().min(1).optional(),
|
|
662
|
+
runnerLogId: z.string().min(1).optional(),
|
|
663
|
+
commandId: z.string().min(1).optional()
|
|
664
|
+
});
|
|
665
|
+
var activityHandoffExportSchema = z.object({
|
|
666
|
+
markdown: z.string().min(1),
|
|
667
|
+
generatedAt: isoDateTimeSchema,
|
|
668
|
+
eventCount: z.number().int().nonnegative()
|
|
669
|
+
});
|
|
342
670
|
var toolSessionItemSchema = baseItemSchema.extend({
|
|
343
671
|
type: z.literal("toolSession"),
|
|
344
672
|
projectId: z.string().min(1),
|
|
@@ -355,6 +683,10 @@ var toolSessionItemSchema = baseItemSchema.extend({
|
|
|
355
683
|
status: toolSessionStatusSchema,
|
|
356
684
|
createdByUserId: z.string().min(1).optional(),
|
|
357
685
|
runnerId: z.string().min(1).optional(),
|
|
686
|
+
machineId: z.string().min(1).optional(),
|
|
687
|
+
implementationScopeId: z.string().min(1).optional(),
|
|
688
|
+
executionWorktreeKey: z.string().min(1).optional(),
|
|
689
|
+
isolationMode: workIsolationModeSchema.optional(),
|
|
358
690
|
lastWorkItemId: z.string().min(1).optional(),
|
|
359
691
|
lastActivityAt: isoDateTimeSchema,
|
|
360
692
|
messageCount: z.number().int().nonnegative().optional(),
|
|
@@ -387,6 +719,9 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
|
|
|
387
719
|
runnerCredentialItemSchema,
|
|
388
720
|
runnerCommandItemSchema,
|
|
389
721
|
planReviewMessageItemSchema,
|
|
722
|
+
assistantMessageItemSchema,
|
|
723
|
+
impactReportItemSchema,
|
|
724
|
+
activityEventItemSchema,
|
|
390
725
|
toolSessionItemSchema
|
|
391
726
|
]);
|
|
392
727
|
|
|
@@ -412,6 +747,19 @@ function isOfficialAmistioApiUrl(value) {
|
|
|
412
747
|
}
|
|
413
748
|
}
|
|
414
749
|
|
|
750
|
+
// ../shared/src/brain-documents.ts
|
|
751
|
+
var controlPlaneRoots = /* @__PURE__ */ new Set(["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"]);
|
|
752
|
+
function isControlPlaneTemplateRepoPath(repoPath) {
|
|
753
|
+
const normalized = normalizeRepoPath(repoPath);
|
|
754
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
755
|
+
const root = segments[0] === "docs" ? segments[1] : segments[0];
|
|
756
|
+
const basename = segments[segments.length - 1]?.toLowerCase();
|
|
757
|
+
return Boolean(root && controlPlaneRoots.has(root) && (basename === "_template.md" || basename === "_template.mdx"));
|
|
758
|
+
}
|
|
759
|
+
function normalizeRepoPath(repoPath) {
|
|
760
|
+
return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
761
|
+
}
|
|
762
|
+
|
|
415
763
|
// ../shared/src/repo-metadata.ts
|
|
416
764
|
import { z as z2 } from "zod";
|
|
417
765
|
var repoLinkMetadataSchema = z2.object({
|
|
@@ -504,7 +852,7 @@ function buildParsedRepositoryCloneUrl({ cloneUrl, host, protocol, rawPath }) {
|
|
|
504
852
|
if (!normalizedHost) {
|
|
505
853
|
throw new Error("Repository URL must include a host.");
|
|
506
854
|
}
|
|
507
|
-
const pathWithoutSlash =
|
|
855
|
+
const pathWithoutSlash = normalizeRepoPath2(rawPath);
|
|
508
856
|
const segments = pathWithoutSlash.split("/").filter(Boolean);
|
|
509
857
|
if (segments.length < 2) {
|
|
510
858
|
throw new Error("Repository URL must include an owner and repository name.");
|
|
@@ -531,7 +879,7 @@ function buildParsedRepositoryCloneUrl({ cloneUrl, host, protocol, rawPath }) {
|
|
|
531
879
|
repoFullName
|
|
532
880
|
};
|
|
533
881
|
}
|
|
534
|
-
function
|
|
882
|
+
function normalizeRepoPath2(rawPath) {
|
|
535
883
|
const withoutLeadingSlash = rawPath.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
536
884
|
if (!withoutLeadingSlash || withoutLeadingSlash.includes("..")) {
|
|
537
885
|
throw new Error("Repository URL path is not supported.");
|
|
@@ -886,14 +1234,14 @@ function credentialKey(accountId, projectId, repositoryLinkId) {
|
|
|
886
1234
|
import { mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
887
1235
|
import path3 from "node:path";
|
|
888
1236
|
var controlPlaneFolders = [
|
|
889
|
-
"architecture",
|
|
890
|
-
"context",
|
|
891
|
-
"decisions",
|
|
892
|
-
"features",
|
|
893
|
-
"memory",
|
|
894
|
-
"plans",
|
|
895
|
-
path3.join("prompts", "shared"),
|
|
896
|
-
"workflows"
|
|
1237
|
+
path3.join("docs", "architecture"),
|
|
1238
|
+
path3.join("docs", "context"),
|
|
1239
|
+
path3.join("docs", "decisions"),
|
|
1240
|
+
path3.join("docs", "features"),
|
|
1241
|
+
path3.join("docs", "memory"),
|
|
1242
|
+
path3.join("docs", "plans"),
|
|
1243
|
+
path3.join("docs", "prompts", "shared"),
|
|
1244
|
+
path3.join("docs", "workflows")
|
|
897
1245
|
];
|
|
898
1246
|
async function initControlPlane(rootDir) {
|
|
899
1247
|
const created = [];
|
|
@@ -906,10 +1254,10 @@ async function initControlPlane(rootDir) {
|
|
|
906
1254
|
}
|
|
907
1255
|
await writeIfMissing(
|
|
908
1256
|
path3.join(rootDir, "AGENTS.md"),
|
|
909
|
-
"# Agents\n\
|
|
1257
|
+
"# 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
1258
|
);
|
|
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");
|
|
1259
|
+
await writeIfMissing(path3.join(rootDir, "docs", "context", "product.md"), "# Product Context\n\nDescribe the product direction here.\n");
|
|
1260
|
+
await writeIfMissing(path3.join(rootDir, "docs", "architecture", "overview.md"), "# Architecture Overview\n\nDescribe the system shape here.\n");
|
|
913
1261
|
return created;
|
|
914
1262
|
}
|
|
915
1263
|
async function inspectControlPlane(rootDir) {
|
|
@@ -925,20 +1273,26 @@ async function inspectControlPlane(rootDir) {
|
|
|
925
1273
|
return { present, missing };
|
|
926
1274
|
}
|
|
927
1275
|
async function writeProjectLink(rootDir, metadata) {
|
|
928
|
-
const contextDir = path3.join(rootDir, "context");
|
|
1276
|
+
const contextDir = path3.join(rootDir, "docs", "context");
|
|
929
1277
|
await mkdir3(contextDir, { recursive: true });
|
|
930
1278
|
const filePath = path3.join(contextDir, "amistio-project.md");
|
|
931
1279
|
await writeFile2(filePath, createProjectLinkMarkdown(metadata), "utf8");
|
|
932
1280
|
return filePath;
|
|
933
1281
|
}
|
|
934
1282
|
async function readProjectLink(rootDir) {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
1283
|
+
const candidatePaths = [
|
|
1284
|
+
path3.join(rootDir, "docs", "context", "amistio-project.md"),
|
|
1285
|
+
path3.join(rootDir, "context", "amistio-project.md")
|
|
1286
|
+
];
|
|
1287
|
+
for (const filePath of candidatePaths) {
|
|
1288
|
+
if (!await exists(filePath)) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const content = await readFile2(filePath, "utf8");
|
|
1292
|
+
const frontmatter = parseFrontmatter(content);
|
|
1293
|
+
return repoLinkMetadataSchema.parse(frontmatter);
|
|
938
1294
|
}
|
|
939
|
-
|
|
940
|
-
const frontmatter = parseFrontmatter(content);
|
|
941
|
-
return repoLinkMetadataSchema.parse(frontmatter);
|
|
1295
|
+
return void 0;
|
|
942
1296
|
}
|
|
943
1297
|
function parseFrontmatter(content) {
|
|
944
1298
|
if (!content.startsWith("---\n")) {
|
|
@@ -1017,13 +1371,13 @@ var ApiClient = class {
|
|
|
1017
1371
|
}
|
|
1018
1372
|
);
|
|
1019
1373
|
}
|
|
1020
|
-
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300) {
|
|
1374
|
+
async claimWork(projectId, runnerId, repositoryLinkId, leaseSeconds = 300, metadata = {}) {
|
|
1021
1375
|
return this.request(
|
|
1022
1376
|
`/projects/${projectId}/work-items/claim`,
|
|
1023
1377
|
z3.object({ workItem: workItemSchema.optional() }).transform(({ workItem }) => ({ workItem })),
|
|
1024
1378
|
{
|
|
1025
1379
|
method: "POST",
|
|
1026
|
-
body: JSON.stringify({ runnerId, repositoryLinkId, leaseSeconds })
|
|
1380
|
+
body: JSON.stringify({ runnerId, repositoryLinkId, leaseSeconds, ...metadata })
|
|
1027
1381
|
}
|
|
1028
1382
|
);
|
|
1029
1383
|
}
|
|
@@ -1049,6 +1403,21 @@ var ApiClient = class {
|
|
|
1049
1403
|
{ method: "GET" }
|
|
1050
1404
|
);
|
|
1051
1405
|
}
|
|
1406
|
+
async listAssistantMessages(projectId) {
|
|
1407
|
+
return this.request(
|
|
1408
|
+
`/projects/${projectId}/assistant-messages`,
|
|
1409
|
+
z3.object({ messages: z3.array(assistantMessageItemSchema) }),
|
|
1410
|
+
{ method: "GET" }
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
async listImpactReports(projectId, generatedDraftId) {
|
|
1414
|
+
const query = generatedDraftId ? `?generatedDraftId=${encodeURIComponent(generatedDraftId)}` : "";
|
|
1415
|
+
return this.request(
|
|
1416
|
+
`/projects/${projectId}/impact-reports${query}`,
|
|
1417
|
+
z3.object({ reports: z3.array(impactReportItemSchema) }),
|
|
1418
|
+
{ method: "GET" }
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1052
1421
|
async pushBrainDocuments(projectId, documents) {
|
|
1053
1422
|
return this.request(
|
|
1054
1423
|
`/projects/${projectId}/brain-documents`,
|
|
@@ -1115,6 +1484,16 @@ var ApiClient = class {
|
|
|
1115
1484
|
}
|
|
1116
1485
|
);
|
|
1117
1486
|
}
|
|
1487
|
+
async recordRunnerLog(projectId, input) {
|
|
1488
|
+
return this.request(
|
|
1489
|
+
`/projects/${projectId}/runner-logs`,
|
|
1490
|
+
z3.object({ runnerLog: runnerExecutionLogItemSchema }),
|
|
1491
|
+
{
|
|
1492
|
+
method: "POST",
|
|
1493
|
+
body: JSON.stringify(input)
|
|
1494
|
+
}
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1118
1497
|
async listToolSessions(projectId) {
|
|
1119
1498
|
return this.request(
|
|
1120
1499
|
`/projects/${projectId}/tool-sessions`,
|
|
@@ -1142,6 +1521,16 @@ var ApiClient = class {
|
|
|
1142
1521
|
}
|
|
1143
1522
|
);
|
|
1144
1523
|
}
|
|
1524
|
+
async recordActivityEvent(projectId, event) {
|
|
1525
|
+
return this.request(
|
|
1526
|
+
`/projects/${projectId}/activity-events`,
|
|
1527
|
+
z3.object({ event: activityEventItemSchema }),
|
|
1528
|
+
{
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
body: JSON.stringify(event)
|
|
1531
|
+
}
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1145
1534
|
async updateWorkStatus(projectId, workItemId, status, idempotencyKey, runnerId, telemetry = {}) {
|
|
1146
1535
|
return this.request(
|
|
1147
1536
|
`/projects/${projectId}/work-items/${workItemId}/status`,
|
|
@@ -1162,6 +1551,26 @@ var ApiClient = class {
|
|
|
1162
1551
|
}
|
|
1163
1552
|
);
|
|
1164
1553
|
}
|
|
1554
|
+
async submitAssistantResult(projectId, workItemId, result) {
|
|
1555
|
+
return this.request(
|
|
1556
|
+
`/projects/${projectId}/work-items/${workItemId}/assistant-result`,
|
|
1557
|
+
z3.object({ message: assistantMessageItemSchema, answer: assistantMessageItemSchema.optional(), workItem: workItemSchema }),
|
|
1558
|
+
{
|
|
1559
|
+
method: "POST",
|
|
1560
|
+
body: JSON.stringify(result)
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
async submitImpactPreviewResult(projectId, workItemId, result) {
|
|
1565
|
+
return this.request(
|
|
1566
|
+
`/projects/${projectId}/work-items/${workItemId}/impact-result`,
|
|
1567
|
+
z3.object({ report: impactReportItemSchema, workItem: workItemSchema, implementationWorkItem: workItemSchema.optional() }),
|
|
1568
|
+
{
|
|
1569
|
+
method: "POST",
|
|
1570
|
+
body: JSON.stringify(result)
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1165
1574
|
async request(urlPath, schema, init) {
|
|
1166
1575
|
const response = await fetch(resolveApiUrl(this.options.apiUrl, urlPath), {
|
|
1167
1576
|
...init,
|
|
@@ -1200,12 +1609,16 @@ var toolSessionMutationSchema = z3.object({
|
|
|
1200
1609
|
sessionGroupKey: z3.string().min(1).optional(),
|
|
1201
1610
|
reusePolicy: sessionPolicySchema.optional(),
|
|
1202
1611
|
sessionDecision: sessionDecisionSchema.optional(),
|
|
1203
|
-
closedReason: z3.string().optional()
|
|
1612
|
+
closedReason: z3.string().optional(),
|
|
1613
|
+
machineId: z3.string().min(1).optional(),
|
|
1614
|
+
implementationScopeId: z3.string().min(1).optional(),
|
|
1615
|
+
executionWorktreeKey: z3.string().min(1).optional(),
|
|
1616
|
+
isolationMode: workIsolationModeSchema.optional()
|
|
1204
1617
|
});
|
|
1205
1618
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
1206
1619
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
1207
|
-
const
|
|
1208
|
-
return new URL(`${base}${
|
|
1620
|
+
const path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1621
|
+
return new URL(`${base}${path13}`);
|
|
1209
1622
|
}
|
|
1210
1623
|
|
|
1211
1624
|
// src/orchestrator.ts
|
|
@@ -1225,12 +1638,12 @@ async function createOrchestrationPrompt(options) {
|
|
|
1225
1638
|
"",
|
|
1226
1639
|
"## Orchestrator Principle",
|
|
1227
1640
|
"",
|
|
1228
|
-
"The
|
|
1641
|
+
"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
1642
|
"",
|
|
1230
1643
|
"## Repository Rules",
|
|
1231
1644
|
"",
|
|
1232
1645
|
"- Read AGENTS.md first when it exists.",
|
|
1233
|
-
"- Read existing architecture, context, decisions, features, memory, plans, prompts, and workflows before proposing changes.",
|
|
1646
|
+
"- Read existing docs/architecture, docs/context, docs/decisions, docs/features, docs/memory, docs/plans, docs/prompts, and docs/workflows files before proposing changes.",
|
|
1234
1647
|
"- Preserve existing intent. Add the next useful orchestration layer instead of replacing old content wholesale.",
|
|
1235
1648
|
"- Prefer small additive edits, dated notes, explicit decisions, focused plans, and executable prompts.",
|
|
1236
1649
|
"- Do not modify product source code unless the user's goal explicitly asks for implementation.",
|
|
@@ -1241,14 +1654,14 @@ async function createOrchestrationPrompt(options) {
|
|
|
1241
1654
|
"## Control-Plane Areas",
|
|
1242
1655
|
"",
|
|
1243
1656
|
"- 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.",
|
|
1657
|
+
"- docs/architecture/**/*.md for system shape and boundaries.",
|
|
1658
|
+
"- docs/context/**/*.md for product, stack, and implementation context.",
|
|
1659
|
+
"- docs/decisions/**/*.md for ADRs and durable choices.",
|
|
1660
|
+
"- docs/features/**/*.md for behavior specs and acceptance criteria.",
|
|
1661
|
+
"- docs/memory/**/*.md for lessons, patterns, and known pitfalls.",
|
|
1662
|
+
"- docs/plans/**/*.md for near-term execution plans.",
|
|
1663
|
+
"- docs/prompts/**/*.md for model-agnostic implementation prompts.",
|
|
1664
|
+
"- docs/workflows/**/*.md for repeatable procedures.",
|
|
1252
1665
|
"",
|
|
1253
1666
|
"## Current Control-Plane Folder Status",
|
|
1254
1667
|
"",
|
|
@@ -2020,9 +2433,22 @@ function sessionIneligibleReason(session, input, now) {
|
|
|
2020
2433
|
if (session.repositoryLinkId && session.repositoryLinkId !== input.repositoryLinkId) {
|
|
2021
2434
|
return "repository link mismatch";
|
|
2022
2435
|
}
|
|
2436
|
+
if (session.machineId && session.machineId !== input.machineId) {
|
|
2437
|
+
return "session is scoped to another machine";
|
|
2438
|
+
}
|
|
2023
2439
|
if (session.resumabilityScope === "localMachine" && session.runnerId && session.runnerId !== input.runnerId) {
|
|
2024
2440
|
return "session is scoped to another runner machine";
|
|
2025
2441
|
}
|
|
2442
|
+
const requestedByUserId = input.workItem.requestedByUserId ?? input.workItem.requestedBy;
|
|
2443
|
+
if (session.createdByUserId && session.createdByUserId !== requestedByUserId) {
|
|
2444
|
+
return "session belongs to another requester";
|
|
2445
|
+
}
|
|
2446
|
+
if (input.workItem.implementationScopeId && session.implementationScopeId !== input.workItem.implementationScopeId) {
|
|
2447
|
+
return "implementation scope mismatch";
|
|
2448
|
+
}
|
|
2449
|
+
if (input.workItem.executionWorktreeKey && session.executionWorktreeKey !== input.workItem.executionWorktreeKey) {
|
|
2450
|
+
return "worktree scope mismatch";
|
|
2451
|
+
}
|
|
2026
2452
|
if (session.status === "closed" || session.status === "archived" || session.status === "blocked" || session.status === "unavailable") {
|
|
2027
2453
|
return `session is ${session.status}`;
|
|
2028
2454
|
}
|
|
@@ -2074,11 +2500,13 @@ function tokens(value) {
|
|
|
2074
2500
|
// src/sync.ts
|
|
2075
2501
|
import { mkdir as mkdir6, readdir as readdir3, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2076
2502
|
import path7 from "node:path";
|
|
2077
|
-
var
|
|
2503
|
+
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2504
|
+
var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
|
|
2078
2505
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
2079
2506
|
const localDocuments = await readLocalSyncedDocuments(rootDir);
|
|
2080
|
-
const
|
|
2081
|
-
const
|
|
2507
|
+
const normalizedWebDocuments = webDocuments.map((document) => ({ ...document, repoPath: canonicalControlPlaneRepoPath(document.repoPath) }));
|
|
2508
|
+
const webByDocumentId = new Map(normalizedWebDocuments.map((document) => [document.documentId, document]));
|
|
2509
|
+
const pullableWebDocuments = normalizedWebDocuments.filter((document) => !isControlPlaneTemplateRepoPath(document.repoPath) && (document.status === "approved" || document.syncState === "approved" || document.syncState === "synced"));
|
|
2082
2510
|
const localByDocumentId = new Map(localDocuments.map((document) => [document.frontmatter.amistioDocumentId, document]));
|
|
2083
2511
|
const items = [];
|
|
2084
2512
|
for (const localDocument of localDocuments) {
|
|
@@ -2155,9 +2583,13 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2155
2583
|
if (!parsed) {
|
|
2156
2584
|
continue;
|
|
2157
2585
|
}
|
|
2586
|
+
const repoPath = toRepoPath(rootDir, fullPath);
|
|
2587
|
+
if (isControlPlaneTemplateRepoPath(repoPath)) {
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2158
2590
|
documents.push({
|
|
2159
2591
|
fullPath,
|
|
2160
|
-
repoPath
|
|
2592
|
+
repoPath,
|
|
2161
2593
|
frontmatter: parsed.frontmatter,
|
|
2162
2594
|
content: parsed.content,
|
|
2163
2595
|
contentHash: sha256ContentHash(parsed.content)
|
|
@@ -2168,10 +2600,15 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2168
2600
|
async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
2169
2601
|
const result = { written: [], skipped: [], conflicts: [] };
|
|
2170
2602
|
for (const inputDocument of documents) {
|
|
2171
|
-
const
|
|
2603
|
+
const parsedDocument = brainDocumentItemSchema.parse(inputDocument);
|
|
2604
|
+
const document = { ...parsedDocument, repoPath: canonicalControlPlaneRepoPath(parsedDocument.repoPath) };
|
|
2172
2605
|
const fullPath = safeRepoPath(rootDir, document.repoPath);
|
|
2606
|
+
if (isControlPlaneTemplateRepoPath(document.repoPath)) {
|
|
2607
|
+
result.skipped.push(document.repoPath);
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2173
2610
|
if (!isControlPlanePath(document.repoPath)) {
|
|
2174
|
-
result.conflicts.push(`${document.repoPath}: refusing to write outside
|
|
2611
|
+
result.conflicts.push(`${document.repoPath}: refusing to write outside docs control-plane folders`);
|
|
2175
2612
|
continue;
|
|
2176
2613
|
}
|
|
2177
2614
|
const existing = await readExistingSyncedDocument(fullPath);
|
|
@@ -2315,6 +2752,14 @@ function isControlPlanePath(repoPath) {
|
|
|
2315
2752
|
const normalized = path7.normalize(repoPath);
|
|
2316
2753
|
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path7.sep}`));
|
|
2317
2754
|
}
|
|
2755
|
+
function canonicalControlPlaneRepoPath(repoPath) {
|
|
2756
|
+
const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
2757
|
+
const [firstSegment] = normalized.split("/");
|
|
2758
|
+
if (firstSegment && legacySyncRoots.includes(firstSegment)) {
|
|
2759
|
+
return `docs/${normalized}`;
|
|
2760
|
+
}
|
|
2761
|
+
return normalized;
|
|
2762
|
+
}
|
|
2318
2763
|
function toRepoPath(rootDir, fullPath) {
|
|
2319
2764
|
return path7.relative(rootDir, fullPath).split(path7.sep).join("/");
|
|
2320
2765
|
}
|
|
@@ -2381,6 +2826,10 @@ function defaultSessionStorePath() {
|
|
|
2381
2826
|
// src/work-runner.ts
|
|
2382
2827
|
var generationResultStart = "AMISTIO_BRAIN_GENERATION_RESULT_START";
|
|
2383
2828
|
var generationResultEnd = "AMISTIO_BRAIN_GENERATION_RESULT_END";
|
|
2829
|
+
var assistantAnswerStart = "AMISTIO_ASSISTANT_ANSWER_START";
|
|
2830
|
+
var assistantAnswerEnd = "AMISTIO_ASSISTANT_ANSWER_END";
|
|
2831
|
+
var impactPreviewStart = "AMISTIO_IMPACT_PREVIEW_START";
|
|
2832
|
+
var impactPreviewEnd = "AMISTIO_IMPACT_PREVIEW_END";
|
|
2384
2833
|
function createWorkExecutionPrompt(workItem, context) {
|
|
2385
2834
|
if (workItem.workKind === "brainGeneration") {
|
|
2386
2835
|
return createBrainGenerationPrompt(workItem);
|
|
@@ -2388,6 +2837,12 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2388
2837
|
if (workItem.workKind === "planRevision") {
|
|
2389
2838
|
return createPlanRevisionPrompt(workItem, context?.planRevision);
|
|
2390
2839
|
}
|
|
2840
|
+
if (workItem.workKind === "assistantQuestion") {
|
|
2841
|
+
return createAssistantQuestionPrompt(workItem, context?.assistantQuestion);
|
|
2842
|
+
}
|
|
2843
|
+
if (workItem.workKind === "impactPreview") {
|
|
2844
|
+
return createImpactPreviewPrompt(workItem, context?.impactPreview);
|
|
2845
|
+
}
|
|
2391
2846
|
return [
|
|
2392
2847
|
"# Amistio Work Execution",
|
|
2393
2848
|
"",
|
|
@@ -2399,11 +2854,15 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2399
2854
|
`Title: ${workItem.title}`,
|
|
2400
2855
|
`Work item ID: ${workItem.workItemId}`,
|
|
2401
2856
|
`Project ID: ${workItem.projectId}`,
|
|
2857
|
+
`Implementation scope: ${workItem.implementationScopeId ?? workItem.controllingAdrId ?? "work-item"}`,
|
|
2858
|
+
`Execution branch: ${workItem.executionBranch ?? "managed by Amistio CLI"}`,
|
|
2859
|
+
`Execution worktree key: ${workItem.executionWorktreeKey ?? "managed by Amistio CLI"}`,
|
|
2402
2860
|
"",
|
|
2403
2861
|
"## Rules",
|
|
2404
2862
|
"",
|
|
2405
2863
|
"- Read AGENTS.md first when it exists.",
|
|
2406
|
-
"- Read the relevant
|
|
2864
|
+
"- Read AGENTS.md and the relevant docs/ control-plane files before implementation so the work stays aligned with existing direction.",
|
|
2865
|
+
"- Treat the current working directory as the Amistio-managed implementation worktree; do not switch back to the paired primary checkout for mutating work.",
|
|
2407
2866
|
"- Keep changes focused on this work item.",
|
|
2408
2867
|
"- Preserve old decisions, plans, memory, and prompts unless the work item explicitly supersedes them.",
|
|
2409
2868
|
"- Do not commit changes.",
|
|
@@ -2412,6 +2871,116 @@ function createWorkExecutionPrompt(workItem, context) {
|
|
|
2412
2871
|
"- Run relevant verification commands when feasible and summarize results."
|
|
2413
2872
|
].join("\n");
|
|
2414
2873
|
}
|
|
2874
|
+
function createImpactPreviewPrompt(workItem, context) {
|
|
2875
|
+
const implementationPrompt = context?.implementationPrompt;
|
|
2876
|
+
const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
|
|
2877
|
+
`### ${document.title}`,
|
|
2878
|
+
`documentId: ${document.documentId}`,
|
|
2879
|
+
`documentType: ${document.documentType}`,
|
|
2880
|
+
`repoPath: ${document.repoPath}`,
|
|
2881
|
+
`revision: ${document.revision}`,
|
|
2882
|
+
document.content.slice(0, 3e3)
|
|
2883
|
+
].join("\n")).join("\n\n");
|
|
2884
|
+
return [
|
|
2885
|
+
"# Amistio Implementation Impact Preview",
|
|
2886
|
+
"",
|
|
2887
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
2888
|
+
"Analyze the likely impact of the approved implementation prompt. This is a preview-only task.",
|
|
2889
|
+
"Do not modify files, do not create branches, do not run implementation commands, and do not commit changes.",
|
|
2890
|
+
"",
|
|
2891
|
+
"## Work Item",
|
|
2892
|
+
"",
|
|
2893
|
+
`Title: ${workItem.title}`,
|
|
2894
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
2895
|
+
`Project ID: ${workItem.projectId}`,
|
|
2896
|
+
`Generated draft ID: ${workItem.generatedDraftId ?? "unknown"}`,
|
|
2897
|
+
`Impact report ID: ${workItem.impactReportId ?? "unknown"}`,
|
|
2898
|
+
`Analyzed repo revision: ${context?.analyzedRepoRevision ?? "unknown"}`,
|
|
2899
|
+
"",
|
|
2900
|
+
"## Approved Implementation Prompt",
|
|
2901
|
+
"",
|
|
2902
|
+
implementationPrompt ? implementationPrompt.content : workItem.sourceWish ?? workItem.title,
|
|
2903
|
+
"",
|
|
2904
|
+
"## Approved Project Brain Context",
|
|
2905
|
+
"",
|
|
2906
|
+
approvedContext || "No approved project-brain records were loaded. Inspect the local repository and explain the gap in the report.",
|
|
2907
|
+
"",
|
|
2908
|
+
"## Report Requirements",
|
|
2909
|
+
"",
|
|
2910
|
+
"- Identify affected product areas and likely modules/files using safe path summaries.",
|
|
2911
|
+
"- Assign riskLevel as low, medium, high, or critical with operational reasoning in summary.",
|
|
2912
|
+
"- Include data/schema/migration impact, security/privacy/access-control considerations, verification plan, and rollback plan.",
|
|
2913
|
+
"- Keep repository source and secrets local. Do not include raw source dumps, credentials, local secret paths, or provider session references.",
|
|
2914
|
+
"- If you inspect files, cite paths only through likelyPaths and concise reasons.",
|
|
2915
|
+
"",
|
|
2916
|
+
"## Output Contract",
|
|
2917
|
+
"",
|
|
2918
|
+
"Print exactly one JSON object between the markers below. The CLI will submit only this structured report back to Amistio.",
|
|
2919
|
+
"",
|
|
2920
|
+
impactPreviewStart,
|
|
2921
|
+
'{"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."}',
|
|
2922
|
+
impactPreviewEnd,
|
|
2923
|
+
"",
|
|
2924
|
+
"Do not put Markdown fences around the markers. Do not implement the work."
|
|
2925
|
+
].join("\n");
|
|
2926
|
+
}
|
|
2927
|
+
function createAssistantQuestionPrompt(workItem, context) {
|
|
2928
|
+
const question = context?.question.content ?? workItem.sourceWish ?? workItem.title;
|
|
2929
|
+
const priorMessages = (context?.messages ?? []).filter((message) => message.messageId !== context?.question.messageId).slice(-12).map((message) => `- ${message.role} / ${message.status}: ${message.content}`).join("\n");
|
|
2930
|
+
const brainContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 12).map((document) => [
|
|
2931
|
+
`### ${document.title}`,
|
|
2932
|
+
`documentId: ${document.documentId}`,
|
|
2933
|
+
`repoPath: ${document.repoPath}`,
|
|
2934
|
+
`revision: ${document.revision}`,
|
|
2935
|
+
document.content.slice(0, 2400)
|
|
2936
|
+
].join("\n")).join("\n\n");
|
|
2937
|
+
return [
|
|
2938
|
+
"# Amistio Project Knowledge Assistant",
|
|
2939
|
+
"",
|
|
2940
|
+
"You are running locally through the Amistio CLI inside the user's repository.",
|
|
2941
|
+
"Answer the user's project question using approved project-brain context and local repository inspection when useful.",
|
|
2942
|
+
"This is an answer-only task. Do not modify files, do not create branches, and do not commit changes.",
|
|
2943
|
+
"",
|
|
2944
|
+
"## User Question",
|
|
2945
|
+
"",
|
|
2946
|
+
question,
|
|
2947
|
+
"",
|
|
2948
|
+
"## Work Item",
|
|
2949
|
+
"",
|
|
2950
|
+
`Title: ${workItem.title}`,
|
|
2951
|
+
`Work item ID: ${workItem.workItemId}`,
|
|
2952
|
+
`Project ID: ${workItem.projectId}`,
|
|
2953
|
+
`Assistant message ID: ${workItem.assistantMessageId ?? context?.question.messageId ?? "unknown"}`,
|
|
2954
|
+
"",
|
|
2955
|
+
"## Approved Project Brain Context",
|
|
2956
|
+
"",
|
|
2957
|
+
brainContext || "No approved project-brain records were loaded. Say what you inspected locally and avoid pretending citations exist.",
|
|
2958
|
+
"",
|
|
2959
|
+
"## Recent Assistant Conversation",
|
|
2960
|
+
"",
|
|
2961
|
+
priorMessages || "No prior assistant messages were loaded.",
|
|
2962
|
+
"",
|
|
2963
|
+
"## Source Boundary Rules",
|
|
2964
|
+
"",
|
|
2965
|
+
"- Keep repository source, secrets, tokens, local credential paths, and provider session references local.",
|
|
2966
|
+
"- Summarize findings instead of dumping source code. Tiny identifiers, filenames, and short excerpts are acceptable when necessary.",
|
|
2967
|
+
'- Use sourceBoundary "projectBrain" when the answer only uses project-brain records.',
|
|
2968
|
+
'- Use sourceBoundary "localSource" when you inspected local repository files.',
|
|
2969
|
+
'- Use sourceBoundary "mixed" when both project-brain records and local files shaped the answer.',
|
|
2970
|
+
"- Include citations for project-brain records with documentId/title/repoPath where relevant.",
|
|
2971
|
+
"- Include local file citations with repoPath only; do not include raw source dumps.",
|
|
2972
|
+
"",
|
|
2973
|
+
"## Output Contract",
|
|
2974
|
+
"",
|
|
2975
|
+
"Print exactly one JSON object between the markers below. The CLI will submit only this structured answer back to Amistio.",
|
|
2976
|
+
"",
|
|
2977
|
+
assistantAnswerStart,
|
|
2978
|
+
'{"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"}]}',
|
|
2979
|
+
assistantAnswerEnd,
|
|
2980
|
+
"",
|
|
2981
|
+
"Do not put Markdown fences around the markers. Do not include implementation diffs."
|
|
2982
|
+
].join("\n");
|
|
2983
|
+
}
|
|
2415
2984
|
function createPlanRevisionPrompt(workItem, context) {
|
|
2416
2985
|
const messages = context?.messages ?? [];
|
|
2417
2986
|
return [
|
|
@@ -2439,10 +3008,10 @@ function createPlanRevisionPrompt(workItem, context) {
|
|
|
2439
3008
|
"## Output Contract",
|
|
2440
3009
|
"",
|
|
2441
3010
|
"Print exactly one JSON object between the markers below with an artifacts array containing one revised plan artifact.",
|
|
2442
|
-
'The artifact must use documentType "plan", keep the plan under plans/, and include the complete revised plan content.',
|
|
3011
|
+
'The artifact must use documentType "plan", keep the plan under docs/plans/, and include the complete revised plan content.',
|
|
2443
3012
|
"",
|
|
2444
3013
|
generationResultStart,
|
|
2445
|
-
'{"artifacts":[{"documentType":"plan","title":"Revised Plan","repoPath":"plans/PLAN-revised.md","content":"# Revised Plan\\n\\n## Goal\\n..."}]}',
|
|
3014
|
+
'{"artifacts":[{"documentType":"plan","title":"Revised Plan","repoPath":"docs/plans/PLAN-revised.md","content":"# Revised Plan\\n\\n## Goal\\n..."}]}',
|
|
2446
3015
|
generationResultEnd,
|
|
2447
3016
|
"",
|
|
2448
3017
|
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
@@ -2458,6 +3027,26 @@ function parseBrainGenerationArtifacts(output) {
|
|
|
2458
3027
|
const parsed = JSON.parse(stripJsonFence(payload));
|
|
2459
3028
|
return brainGenerationResultSchema.parse(parsed).artifacts;
|
|
2460
3029
|
}
|
|
3030
|
+
function parseAssistantAnswerResult(output) {
|
|
3031
|
+
const start = output.indexOf(assistantAnswerStart);
|
|
3032
|
+
const end = output.indexOf(assistantAnswerEnd, start + assistantAnswerStart.length);
|
|
3033
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
3034
|
+
throw new Error("Local AI answer did not return an Amistio assistant answer block.");
|
|
3035
|
+
}
|
|
3036
|
+
const payload = output.slice(start + assistantAnswerStart.length, end).trim();
|
|
3037
|
+
const parsed = JSON.parse(stripJsonFence(payload));
|
|
3038
|
+
return assistantAnswerResultSchema.parse(parsed);
|
|
3039
|
+
}
|
|
3040
|
+
function parseImpactPreviewResult(output) {
|
|
3041
|
+
const start = output.indexOf(impactPreviewStart);
|
|
3042
|
+
const end = output.indexOf(impactPreviewEnd, start + impactPreviewStart.length);
|
|
3043
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
3044
|
+
throw new Error("Local AI preview did not return an Amistio impact preview block.");
|
|
3045
|
+
}
|
|
3046
|
+
const payload = output.slice(start + impactPreviewStart.length, end).trim();
|
|
3047
|
+
const parsed = JSON.parse(stripJsonFence(payload));
|
|
3048
|
+
return impactPreviewResultSchema.parse(parsed);
|
|
3049
|
+
}
|
|
2461
3050
|
function createBrainGenerationPrompt(workItem) {
|
|
2462
3051
|
const wish = workItem.sourceWish ?? workItem.title;
|
|
2463
3052
|
return [
|
|
@@ -2480,21 +3069,21 @@ function createBrainGenerationPrompt(workItem) {
|
|
|
2480
3069
|
"## Read First",
|
|
2481
3070
|
"",
|
|
2482
3071
|
"- Read AGENTS.md first when it exists.",
|
|
2483
|
-
"- Read relevant files under architecture/, context/, decisions/, features/, memory/, plans/, prompts/, and workflows/ before drafting.",
|
|
3072
|
+
"- Read relevant files under docs/architecture/, docs/context/, docs/decisions/, docs/features/, docs/memory/, docs/plans/, docs/prompts/, and docs/workflows/ before drafting.",
|
|
2484
3073
|
"- 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
3074
|
"",
|
|
2486
3075
|
"## Generate Artifacts",
|
|
2487
3076
|
"",
|
|
2488
3077
|
"Return Markdown artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
|
|
2489
3078
|
"",
|
|
2490
|
-
"- architecture -> architecture/",
|
|
2491
|
-
"- context -> context/",
|
|
2492
|
-
"- decision -> decisions/",
|
|
2493
|
-
"- feature -> features/",
|
|
2494
|
-
"- memory -> memory/",
|
|
2495
|
-
"- plan -> plans/",
|
|
2496
|
-
"- prompt -> prompts/",
|
|
2497
|
-
"- workflow -> workflows/",
|
|
3079
|
+
"- architecture -> docs/architecture/",
|
|
3080
|
+
"- context -> docs/context/",
|
|
3081
|
+
"- decision -> docs/decisions/",
|
|
3082
|
+
"- feature -> docs/features/",
|
|
3083
|
+
"- memory -> docs/memory/",
|
|
3084
|
+
"- plan -> docs/plans/",
|
|
3085
|
+
"- prompt -> docs/prompts/",
|
|
3086
|
+
"- workflow -> docs/workflows/",
|
|
2498
3087
|
"",
|
|
2499
3088
|
"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
3089
|
"",
|
|
@@ -2503,7 +3092,7 @@ function createBrainGenerationPrompt(workItem) {
|
|
|
2503
3092
|
"Print exactly one JSON object between the markers below. The CLI will submit only this structured result back to Amistio.",
|
|
2504
3093
|
"",
|
|
2505
3094
|
generationResultStart,
|
|
2506
|
-
'{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
|
|
3095
|
+
'{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"docs/plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
|
|
2507
3096
|
generationResultEnd,
|
|
2508
3097
|
"",
|
|
2509
3098
|
"Do not put Markdown fences around the markers. Do not claim implementation is complete."
|
|
@@ -2518,6 +3107,7 @@ function stripJsonFence(value) {
|
|
|
2518
3107
|
}
|
|
2519
3108
|
|
|
2520
3109
|
// src/runner-status.ts
|
|
3110
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2521
3111
|
var watchStateReminderMs = 60 * 1e3;
|
|
2522
3112
|
function formatWatchStartupContext(input) {
|
|
2523
3113
|
return [
|
|
@@ -2537,28 +3127,32 @@ function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateR
|
|
|
2537
3127
|
function watchStateKey(action) {
|
|
2538
3128
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
2539
3129
|
}
|
|
3130
|
+
function stableRunnerId(input) {
|
|
3131
|
+
const digest = createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
|
|
3132
|
+
return `runner_${digest}`;
|
|
3133
|
+
}
|
|
2540
3134
|
|
|
2541
3135
|
// src/importer.ts
|
|
2542
3136
|
import { execFile as execFile2 } from "node:child_process";
|
|
2543
|
-
import { createHash as
|
|
3137
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2544
3138
|
import { readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
2545
3139
|
import path9 from "node:path";
|
|
2546
3140
|
import { promisify as promisify2 } from "node:util";
|
|
2547
3141
|
var execFileAsync2 = promisify2(execFile2);
|
|
2548
3142
|
var defaultMaxFileKb = 256;
|
|
2549
|
-
var
|
|
3143
|
+
var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2550
3144
|
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"]);
|
|
3145
|
+
var excludedFileNames = /* @__PURE__ */ new Set(["docs/context/amistio-project.md", "context/amistio-project.md"]);
|
|
2552
3146
|
var generatedPathSegments = /* @__PURE__ */ new Set(["generated", "__generated__", "vendor", "vendors"]);
|
|
2553
3147
|
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"
|
|
3148
|
+
architecture: "docs/architecture",
|
|
3149
|
+
context: "docs/context",
|
|
3150
|
+
decision: "docs/decisions",
|
|
3151
|
+
feature: "docs/features",
|
|
3152
|
+
memory: "docs/memory",
|
|
3153
|
+
plan: "docs/plans",
|
|
3154
|
+
prompt: "docs/prompts/shared",
|
|
3155
|
+
workflow: "docs/workflows"
|
|
2562
3156
|
};
|
|
2563
3157
|
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
2564
3158
|
const requestedRoot = path9.resolve(rootDir);
|
|
@@ -2645,7 +3239,7 @@ function buildImportedBrainDocuments(options) {
|
|
|
2645
3239
|
documentId,
|
|
2646
3240
|
documentType: candidate.documentType,
|
|
2647
3241
|
title: candidate.title,
|
|
2648
|
-
status: "
|
|
3242
|
+
status: "approved",
|
|
2649
3243
|
repoPath: candidate.repoPath,
|
|
2650
3244
|
content: candidate.content,
|
|
2651
3245
|
contentHash: candidate.contentHash,
|
|
@@ -2658,10 +3252,10 @@ function buildImportedBrainDocuments(options) {
|
|
|
2658
3252
|
},
|
|
2659
3253
|
revision,
|
|
2660
3254
|
source: "repo",
|
|
2661
|
-
syncState: "
|
|
3255
|
+
syncState: "approved",
|
|
2662
3256
|
createdAt: existing?.createdAt ?? importedAt,
|
|
2663
3257
|
updatedAt: importedAt,
|
|
2664
|
-
|
|
3258
|
+
approvedRevision: revision
|
|
2665
3259
|
};
|
|
2666
3260
|
});
|
|
2667
3261
|
}
|
|
@@ -2688,7 +3282,7 @@ function parseOptionalOriginCloneUrl(originUrl) {
|
|
|
2688
3282
|
async function listRepositoryPaths(rootDir) {
|
|
2689
3283
|
const gitFiles = await runGit2(["-C", rootDir, "ls-files", "--cached", "--others", "--exclude-standard"]).catch(() => void 0);
|
|
2690
3284
|
if (gitFiles !== void 0) {
|
|
2691
|
-
return gitFiles.split("\n").map((line) =>
|
|
3285
|
+
return gitFiles.split("\n").map((line) => normalizeRepoPath3(line)).filter((line) => line.length > 0);
|
|
2692
3286
|
}
|
|
2693
3287
|
const files = [];
|
|
2694
3288
|
await walkRepository(rootDir, rootDir, files);
|
|
@@ -2698,7 +3292,7 @@ async function walkRepository(rootDir, directory, files) {
|
|
|
2698
3292
|
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
2699
3293
|
for (const entry of entries) {
|
|
2700
3294
|
const fullPath = path9.join(directory, entry.name);
|
|
2701
|
-
const repoPath =
|
|
3295
|
+
const repoPath = normalizeRepoPath3(path9.relative(rootDir, fullPath));
|
|
2702
3296
|
if (entry.isDirectory()) {
|
|
2703
3297
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
2704
3298
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -2718,7 +3312,7 @@ function matchesIncludeExclude(repoPath, include, exclude) {
|
|
|
2718
3312
|
return true;
|
|
2719
3313
|
}
|
|
2720
3314
|
function wildcardMatch(pattern, repoPath) {
|
|
2721
|
-
const normalizedPattern =
|
|
3315
|
+
const normalizedPattern = normalizeRepoPath3(pattern);
|
|
2722
3316
|
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
3317
|
return new RegExp(`^${escaped}$`).test(repoPath);
|
|
2724
3318
|
}
|
|
@@ -2726,6 +3320,7 @@ function isMarkdownDocument(repoPath) {
|
|
|
2726
3320
|
return /\.(md|mdx)$/i.test(repoPath);
|
|
2727
3321
|
}
|
|
2728
3322
|
function isExcludedRepoPath(repoPath) {
|
|
3323
|
+
if (isControlPlaneTemplateRepoPath(repoPath)) return true;
|
|
2729
3324
|
if (excludedFileNames.has(repoPath)) return true;
|
|
2730
3325
|
const segments = repoPath.split("/");
|
|
2731
3326
|
if (segments.some((segment) => excludedDirectoryNames.has(segment) || generatedPathSegments.has(segment))) return true;
|
|
@@ -2748,9 +3343,12 @@ function classifyLegacyDocument(repoPath, content) {
|
|
|
2748
3343
|
return "context";
|
|
2749
3344
|
}
|
|
2750
3345
|
function canonicalImportPath(sourcePath, documentType) {
|
|
2751
|
-
if (
|
|
3346
|
+
if (isCanonicalControlPlanePath(sourcePath)) {
|
|
2752
3347
|
return sourcePath;
|
|
2753
3348
|
}
|
|
3349
|
+
if (isLegacyControlPlanePath(sourcePath)) {
|
|
3350
|
+
return `docs/${sourcePath}`;
|
|
3351
|
+
}
|
|
2754
3352
|
const baseSlug = slugFromPath(sourcePath);
|
|
2755
3353
|
return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
|
|
2756
3354
|
}
|
|
@@ -2766,9 +3364,13 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
2766
3364
|
usedPaths.add(uniquePath);
|
|
2767
3365
|
return uniquePath;
|
|
2768
3366
|
}
|
|
2769
|
-
function
|
|
3367
|
+
function isCanonicalControlPlanePath(repoPath) {
|
|
3368
|
+
const [firstSegment, secondSegment] = normalizeRepoPath3(repoPath).split("/");
|
|
3369
|
+
return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
|
|
3370
|
+
}
|
|
3371
|
+
function isLegacyControlPlanePath(repoPath) {
|
|
2770
3372
|
const [firstSegment] = repoPath.split("/");
|
|
2771
|
-
return Boolean(firstSegment &&
|
|
3373
|
+
return Boolean(firstSegment && controlPlaneRoots2.includes(firstSegment));
|
|
2772
3374
|
}
|
|
2773
3375
|
function inferTitle2(content, repoPath) {
|
|
2774
3376
|
const body = stripFrontmatter(content);
|
|
@@ -2796,9 +3398,9 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
|
|
|
2796
3398
|
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
2797
3399
|
}
|
|
2798
3400
|
function hashText(value, length) {
|
|
2799
|
-
return
|
|
3401
|
+
return createHash4("sha256").update(value).digest("hex").slice(0, length);
|
|
2800
3402
|
}
|
|
2801
|
-
function
|
|
3403
|
+
function normalizeRepoPath3(value) {
|
|
2802
3404
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
2803
3405
|
}
|
|
2804
3406
|
async function runGit2(args) {
|
|
@@ -2842,6 +3444,9 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
2842
3444
|
if (!options.stream) {
|
|
2843
3445
|
args.push("--no-stream");
|
|
2844
3446
|
}
|
|
3447
|
+
if (options.verbose) {
|
|
3448
|
+
args.push("--verbose");
|
|
3449
|
+
}
|
|
2845
3450
|
return args;
|
|
2846
3451
|
}
|
|
2847
3452
|
async function runOfficialCliUpdate() {
|
|
@@ -2880,12 +3485,110 @@ function truncateProcessOutput(value) {
|
|
|
2880
3485
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
2881
3486
|
}
|
|
2882
3487
|
|
|
3488
|
+
// src/git-worktree.ts
|
|
3489
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3490
|
+
import { mkdir as mkdir8, stat as stat5 } from "node:fs/promises";
|
|
3491
|
+
import path11 from "node:path";
|
|
3492
|
+
import { promisify as promisify3 } from "node:util";
|
|
3493
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3494
|
+
function needsGitWorktreeIsolation(workItem) {
|
|
3495
|
+
return (workItem.workKind ?? "implementation") === "implementation";
|
|
3496
|
+
}
|
|
3497
|
+
function resolveWorktreeIdentity(workItem) {
|
|
3498
|
+
const implementationScopeId = workItem.controllingAdrId ?? workItem.implementationScopeId ?? workItem.impactDocumentId ?? workItem.reviewDocumentId ?? workItem.generatedDraftId ?? workItem.workItemId;
|
|
3499
|
+
const slug = workIsolationSlug(implementationScopeId, workItem.title);
|
|
3500
|
+
return {
|
|
3501
|
+
implementationScopeId,
|
|
3502
|
+
branch: workItem.executionBranch ?? `amistio/work/${slug}`,
|
|
3503
|
+
worktreeKey: workItem.executionWorktreeKey ?? `amistio/worktrees/${slug}`,
|
|
3504
|
+
...workItem.repositoryLockId ? { repositoryLockId: workItem.repositoryLockId } : {}
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
3508
|
+
const identity = resolveWorktreeIdentity(workItem);
|
|
3509
|
+
const repoRoot = await gitOutput(rootDir, ["rev-parse", "--show-toplevel"]).catch((error) => {
|
|
3510
|
+
throw new Error(`Git worktree isolation requires a paired Git checkout: ${errorMessage2(error)}`);
|
|
3511
|
+
});
|
|
3512
|
+
const currentHead = await gitOutput(repoRoot, ["rev-parse", "HEAD"]);
|
|
3513
|
+
await assertBaseRevision(repoRoot, workItem.baseRevision, currentHead);
|
|
3514
|
+
const baseRevision = currentHead;
|
|
3515
|
+
const worktreePath = localWorktreePath(repoRoot, identity.worktreeKey);
|
|
3516
|
+
if (await pathExists(worktreePath)) {
|
|
3517
|
+
await assertExistingWorktree(worktreePath, identity.branch);
|
|
3518
|
+
return { ...identity, baseRevision, worktreePath };
|
|
3519
|
+
}
|
|
3520
|
+
await mkdir8(path11.dirname(worktreePath), { recursive: true });
|
|
3521
|
+
const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
|
|
3522
|
+
const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
|
|
3523
|
+
await gitOutput(repoRoot, worktreeArgs).catch((error) => {
|
|
3524
|
+
throw new Error(`Could not create Git worktree ${identity.worktreeKey} on ${identity.branch}: ${errorMessage2(error)}`);
|
|
3525
|
+
});
|
|
3526
|
+
return { ...identity, baseRevision, worktreePath };
|
|
3527
|
+
}
|
|
3528
|
+
function localWorktreePath(repoRoot, worktreeKey) {
|
|
3529
|
+
const repoName = path11.basename(repoRoot);
|
|
3530
|
+
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
3531
|
+
return path11.join(path11.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
|
|
3532
|
+
}
|
|
3533
|
+
async function assertExistingWorktree(worktreePath, branch) {
|
|
3534
|
+
await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
3535
|
+
const currentBranch = await gitOutput(worktreePath, ["branch", "--show-current"]);
|
|
3536
|
+
if (currentBranch && currentBranch !== branch) {
|
|
3537
|
+
throw new Error(`Existing worktree is on ${currentBranch}; expected ${branch}.`);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
|
|
3541
|
+
if (!baseRevision || baseRevision === currentHead) {
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
const revisionExists = await gitCommandSucceeds(repoRoot, ["cat-file", "-e", `${baseRevision}^{commit}`]);
|
|
3545
|
+
if (!revisionExists) {
|
|
3546
|
+
throw new Error(`Work item base revision ${baseRevision} is not available in this checkout; sync the repository before running implementation work.`);
|
|
3547
|
+
}
|
|
3548
|
+
const isAncestor = await gitCommandSucceeds(repoRoot, ["merge-base", "--is-ancestor", baseRevision, currentHead]);
|
|
3549
|
+
if (!isAncestor) {
|
|
3550
|
+
throw new Error(`Work item base revision ${baseRevision} is not an ancestor of ${currentHead}; refresh the work item before implementation.`);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
async function gitOutput(cwd, args) {
|
|
3554
|
+
const { stdout } = await execFileAsync3("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
3555
|
+
return stdout.trim();
|
|
3556
|
+
}
|
|
3557
|
+
async function gitCommandSucceeds(cwd, args) {
|
|
3558
|
+
return execFileAsync3("git", args, { cwd }).then(() => true, () => false);
|
|
3559
|
+
}
|
|
3560
|
+
async function pathExists(value) {
|
|
3561
|
+
return stat5(value).then(() => true, () => false);
|
|
3562
|
+
}
|
|
3563
|
+
function workIsolationSlug(scopeId, title) {
|
|
3564
|
+
const scopeSlug = slugify(scopeId);
|
|
3565
|
+
const titleSlug = slugify(title).slice(0, 32);
|
|
3566
|
+
const combined = titleSlug && !scopeSlug.includes(titleSlug) ? `${scopeSlug}-${titleSlug}` : scopeSlug;
|
|
3567
|
+
return combined.slice(0, 96).replace(/-+$/g, "") || "work";
|
|
3568
|
+
}
|
|
3569
|
+
function slugify(value) {
|
|
3570
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "work";
|
|
3571
|
+
}
|
|
3572
|
+
function errorMessage2(error) {
|
|
3573
|
+
return error instanceof Error ? error.message : String(error);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// src/version.ts
|
|
3577
|
+
import { readFileSync } from "node:fs";
|
|
3578
|
+
function readCliPackageVersion() {
|
|
3579
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
3580
|
+
if (typeof packageJson.version !== "string" || !packageJson.version.trim()) {
|
|
3581
|
+
throw new Error("@amistio/cli package version is missing.");
|
|
3582
|
+
}
|
|
3583
|
+
return packageJson.version.trim();
|
|
3584
|
+
}
|
|
3585
|
+
var CLI_VERSION = readCliPackageVersion();
|
|
3586
|
+
|
|
2883
3587
|
// src/index.ts
|
|
2884
3588
|
var program = new Command();
|
|
2885
3589
|
var defaultRoot = process.env.INIT_CWD ?? process.cwd();
|
|
2886
3590
|
var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
|
|
2887
|
-
program.name("amistio").description("Amistio project brain CLI").version(
|
|
2888
|
-
var CLI_VERSION = "0.1.4";
|
|
3591
|
+
program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
|
|
2889
3592
|
program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
|
|
2890
3593
|
const created = await initControlPlane(options.root);
|
|
2891
3594
|
console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
|
|
@@ -2926,7 +3629,7 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
|
|
|
2926
3629
|
console.log(`Wrote non-secret project metadata to ${filePath}.`);
|
|
2927
3630
|
console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
|
|
2928
3631
|
});
|
|
2929
|
-
program.command("import").description("Pair an existing checkout and import legacy Markdown docs
|
|
3632
|
+
program.command("import").description("Pair an existing checkout and import legacy Markdown docs as accepted brain records").argument("[code]", "Short-lived pairing code from the Amistio app").option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--root <path>", "Repository root", defaultRoot).option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--default-branch <branch>", "Default branch fallback", "main").option("--include <glob>", "Only import files matching a repo-relative glob", collectRepeatedOption, []).option("--exclude <glob>", "Exclude files matching a repo-relative glob", collectRepeatedOption, []).option("--max-file-kb <kb>", "Maximum Markdown file size to import", parsePositiveInteger, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
|
|
2930
3633
|
const pairingCode = (options.pairingCode ?? code)?.trim();
|
|
2931
3634
|
if (!pairingCode) {
|
|
2932
3635
|
throw new Error("Provide a pairing code as `amistio import <code>` or with `--pairing-code <code>`.");
|
|
@@ -2991,7 +3694,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
2991
3694
|
}
|
|
2992
3695
|
console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}; repository link ${pairing.repositoryLinkAction}.`);
|
|
2993
3696
|
console.log(`Wrote non-secret project metadata to ${metadataFilePath}.`);
|
|
2994
|
-
console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"}
|
|
3697
|
+
console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"} as accepted brain record${documents.length === 1 ? "" : "s"}.`);
|
|
2995
3698
|
console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
|
|
2996
3699
|
});
|
|
2997
3700
|
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) => {
|
|
@@ -3139,7 +3842,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
3139
3842
|
console.log(workItemId ? `No work item found for ${workItemId}.` : "No approved work item is ready for prompt export.");
|
|
3140
3843
|
return;
|
|
3141
3844
|
}
|
|
3142
|
-
const prompt =
|
|
3845
|
+
const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
|
|
3143
3846
|
if (options.out) {
|
|
3144
3847
|
await writeFile8(options.out, prompt, "utf8");
|
|
3145
3848
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
@@ -3189,7 +3892,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
|
|
|
3189
3892
|
process.exitCode = result.exitCode;
|
|
3190
3893
|
}
|
|
3191
3894
|
});
|
|
3192
|
-
program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID"
|
|
3895
|
+
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) => {
|
|
3193
3896
|
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
3194
3897
|
if (!context) {
|
|
3195
3898
|
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
@@ -3200,6 +3903,13 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3200
3903
|
process.exitCode = 1;
|
|
3201
3904
|
return;
|
|
3202
3905
|
}
|
|
3906
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
3907
|
+
accountId: context.metadata.amistioAccountId,
|
|
3908
|
+
projectId: context.metadata.amistioProjectId,
|
|
3909
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3910
|
+
machineId: runnerMachineId()
|
|
3911
|
+
});
|
|
3912
|
+
const resolvedOptions = { ...options, runnerId };
|
|
3203
3913
|
if (options.background) {
|
|
3204
3914
|
if (options.dryRun) {
|
|
3205
3915
|
console.log("Background runners cannot be started in dry-run mode.");
|
|
@@ -3210,10 +3920,10 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3210
3920
|
accountId: context.metadata.amistioAccountId,
|
|
3211
3921
|
projectId: context.metadata.amistioProjectId,
|
|
3212
3922
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3213
|
-
runnerId
|
|
3214
|
-
rootDir:
|
|
3923
|
+
runnerId,
|
|
3924
|
+
rootDir: path12.resolve(options.root),
|
|
3215
3925
|
apiUrl: options.apiUrl,
|
|
3216
|
-
args: buildBackgroundRunnerArgs(
|
|
3926
|
+
args: buildBackgroundRunnerArgs(resolvedOptions)
|
|
3217
3927
|
});
|
|
3218
3928
|
console.log(`Started background runner ${metadata.runnerId} with PID ${metadata.pid}.`);
|
|
3219
3929
|
if (metadata.logPath) {
|
|
@@ -3222,59 +3932,60 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3222
3932
|
return;
|
|
3223
3933
|
}
|
|
3224
3934
|
if (options.watch) {
|
|
3225
|
-
for (const line of formatWatchStartupContext({ runnerId
|
|
3935
|
+
for (const line of formatWatchStartupContext({ runnerId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, apiUrl: options.apiUrl, intervalSeconds: options.intervalSeconds })) {
|
|
3226
3936
|
console.log(line);
|
|
3227
3937
|
}
|
|
3228
3938
|
}
|
|
3939
|
+
let offlineSent = false;
|
|
3940
|
+
const sendOfflineHeartbeat = async (message) => {
|
|
3941
|
+
if (offlineSent || options.dryRun) return;
|
|
3942
|
+
offlineSent = true;
|
|
3943
|
+
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "offline", { ...runnerHeartbeatMetadata(), ...message ? { preferenceMessage: message } : {} }).catch(() => void 0);
|
|
3944
|
+
};
|
|
3945
|
+
const handleShutdownSignal = (signal) => {
|
|
3946
|
+
void sendOfflineHeartbeat(`Runner stopped by ${signal}.`).finally(() => {
|
|
3947
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
3948
|
+
});
|
|
3949
|
+
};
|
|
3950
|
+
process.once("SIGINT", handleShutdownSignal);
|
|
3951
|
+
process.once("SIGTERM", handleShutdownSignal);
|
|
3229
3952
|
let iterations = 0;
|
|
3230
3953
|
let lastWatchStateLog;
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
stream: options.stream,
|
|
3246
|
-
commandContext: {
|
|
3247
|
-
accountId: context.metadata.amistioAccountId,
|
|
3248
|
-
apiUrl: options.apiUrl,
|
|
3249
|
-
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
3250
|
-
projectId: context.metadata.amistioProjectId,
|
|
3251
|
-
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3252
|
-
root: options.root,
|
|
3253
|
-
runnerId: options.runnerId
|
|
3254
|
-
},
|
|
3255
|
-
suppressIdleOutput: Boolean(options.watch)
|
|
3256
|
-
});
|
|
3257
|
-
if (!options.watch || options.dryRun) {
|
|
3258
|
-
if (result.exitCode !== 0) {
|
|
3259
|
-
process.exitCode = result.exitCode;
|
|
3954
|
+
try {
|
|
3955
|
+
while (true) {
|
|
3956
|
+
iterations += 1;
|
|
3957
|
+
const result = await runWatchIteration({
|
|
3958
|
+
command,
|
|
3959
|
+
context,
|
|
3960
|
+
options: resolvedOptions,
|
|
3961
|
+
runnerId
|
|
3962
|
+
});
|
|
3963
|
+
if (!options.watch || options.dryRun) {
|
|
3964
|
+
if (result.exitCode !== 0) {
|
|
3965
|
+
process.exitCode = result.exitCode;
|
|
3966
|
+
}
|
|
3967
|
+
return;
|
|
3260
3968
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
3969
|
+
if (result.status === "idle" && result.nextAction) {
|
|
3970
|
+
const nowMs = Date.now();
|
|
3971
|
+
if (shouldPrintWatchState(result.nextAction, lastWatchStateLog, nowMs)) {
|
|
3972
|
+
console.log(formatWatchIdleLine(result.nextAction, options.intervalSeconds));
|
|
3973
|
+
lastWatchStateLog = { key: watchStateKey(result.nextAction), printedAtMs: nowMs };
|
|
3974
|
+
}
|
|
3268
3975
|
}
|
|
3976
|
+
if (result.stopRunner) {
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
if (options.maxIterations !== void 0 && iterations >= options.maxIterations) {
|
|
3980
|
+
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
await delay(options.intervalSeconds * 1e3);
|
|
3269
3984
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
console.log(`Runner stopped after ${iterations} polling attempt${iterations === 1 ? "" : "s"}.`);
|
|
3275
|
-
return;
|
|
3276
|
-
}
|
|
3277
|
-
await delay(options.intervalSeconds * 1e3);
|
|
3985
|
+
} finally {
|
|
3986
|
+
process.off("SIGINT", handleShutdownSignal);
|
|
3987
|
+
process.off("SIGTERM", handleShutdownSignal);
|
|
3988
|
+
await sendOfflineHeartbeat("Runner stopped.");
|
|
3278
3989
|
}
|
|
3279
3990
|
});
|
|
3280
3991
|
var runner = program.command("runner").description("Manage local Amistio runner processes");
|
|
@@ -3348,6 +4059,59 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
3348
4059
|
}).catch(() => void 0);
|
|
3349
4060
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
3350
4061
|
});
|
|
4062
|
+
async function runWatchIteration({ command, context, options, runnerId }) {
|
|
4063
|
+
try {
|
|
4064
|
+
return await runNextWorkItem({
|
|
4065
|
+
apiClient: context.client,
|
|
4066
|
+
projectId: context.metadata.amistioProjectId,
|
|
4067
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4068
|
+
runnerId,
|
|
4069
|
+
root: options.root,
|
|
4070
|
+
sessionPolicy: normalizeSessionPolicy(options.session),
|
|
4071
|
+
...command.getOptionValueSource("tool") === "cli" && options.tool ? { explicitTool: options.tool } : {},
|
|
4072
|
+
...command.getOptionValueSource("invocationChannel") === "cli" && options.invocationChannel ? { explicitInvocationChannel: options.invocationChannel } : {},
|
|
4073
|
+
...command.getOptionValueSource("model") === "cli" && options.model ? { explicitModel: options.model } : {},
|
|
4074
|
+
...options.toolCommand ? { toolCommand: options.toolCommand } : {},
|
|
4075
|
+
dryRun: Boolean(options.dryRun),
|
|
4076
|
+
stream: options.stream,
|
|
4077
|
+
commandContext: {
|
|
4078
|
+
accountId: context.metadata.amistioAccountId,
|
|
4079
|
+
apiUrl: options.apiUrl,
|
|
4080
|
+
backgroundArgs: buildBackgroundRunnerArgs(options),
|
|
4081
|
+
projectId: context.metadata.amistioProjectId,
|
|
4082
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4083
|
+
root: options.root,
|
|
4084
|
+
runnerId
|
|
4085
|
+
},
|
|
4086
|
+
suppressIdleOutput: Boolean(options.watch),
|
|
4087
|
+
verbose: Boolean(options.verbose)
|
|
4088
|
+
});
|
|
4089
|
+
} catch (error) {
|
|
4090
|
+
if (!options.watch) {
|
|
4091
|
+
throw error;
|
|
4092
|
+
}
|
|
4093
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4094
|
+
const message = "Runner hit an error while watching and will keep listening.";
|
|
4095
|
+
if (options.verbose) {
|
|
4096
|
+
console.error(`${message}
|
|
4097
|
+
${detail}`);
|
|
4098
|
+
} else {
|
|
4099
|
+
console.error(`${message} Run with --verbose for details.`);
|
|
4100
|
+
}
|
|
4101
|
+
await Promise.allSettled([
|
|
4102
|
+
context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runnerId, context.metadata.repositoryLinkId, "blocked", { ...runnerHeartbeatMetadata(), preferenceMessage: message }),
|
|
4103
|
+
context.client.recordRunnerLog(context.metadata.amistioProjectId, {
|
|
4104
|
+
runnerId,
|
|
4105
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4106
|
+
status: "failed",
|
|
4107
|
+
message,
|
|
4108
|
+
error: detail,
|
|
4109
|
+
machineId: runnerMachineId()
|
|
4110
|
+
})
|
|
4111
|
+
]);
|
|
4112
|
+
return { status: "failed", exitCode: 1, message };
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
3351
4115
|
async function runNextWorkItem({
|
|
3352
4116
|
apiClient,
|
|
3353
4117
|
dryRun,
|
|
@@ -3362,7 +4126,8 @@ async function runNextWorkItem({
|
|
|
3362
4126
|
explicitTool,
|
|
3363
4127
|
toolCommand,
|
|
3364
4128
|
commandContext,
|
|
3365
|
-
suppressIdleOutput
|
|
4129
|
+
suppressIdleOutput,
|
|
4130
|
+
verbose
|
|
3366
4131
|
}) {
|
|
3367
4132
|
const toolConfig = await resolveRunnerToolConfig({
|
|
3368
4133
|
apiClient,
|
|
@@ -3384,7 +4149,7 @@ async function runNextWorkItem({
|
|
|
3384
4149
|
console.log(toolConfig.message);
|
|
3385
4150
|
return { status: "blocked", exitCode: 1 };
|
|
3386
4151
|
}
|
|
3387
|
-
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId);
|
|
4152
|
+
const result = await apiClient.claimWork(projectId, runnerId, repositoryLinkId, 300, runnerIsolationCapabilityMetadata());
|
|
3388
4153
|
if (!result.workItem) {
|
|
3389
4154
|
const nextAction = await loadProjectNextAction(apiClient, projectId, repositoryLinkId, root);
|
|
3390
4155
|
const message = formatProjectNextAction(nextAction);
|
|
@@ -3393,51 +4158,100 @@ async function runNextWorkItem({
|
|
|
3393
4158
|
}
|
|
3394
4159
|
return { status: "idle", exitCode: 0, nextAction, message };
|
|
3395
4160
|
}
|
|
3396
|
-
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", runnerHeartbeatMetadata(toolConfig));
|
|
3397
4161
|
const prompt = await createRunnerWorkPrompt(apiClient, projectId, result.workItem);
|
|
3398
4162
|
if (dryRun || toolConfig.tool === "none") {
|
|
3399
4163
|
console.log(prompt);
|
|
3400
4164
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3401
4165
|
return { status: "preview", exitCode: 0 };
|
|
3402
4166
|
}
|
|
3403
|
-
const
|
|
4167
|
+
const worktreeIsolation = await prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem: result.workItem });
|
|
4168
|
+
if (worktreeIsolation.status === "blocked") {
|
|
4169
|
+
return { status: "blocked", exitCode: 1, message: worktreeIsolation.message };
|
|
4170
|
+
}
|
|
4171
|
+
const executionRoot = worktreeIsolation.isolation?.worktreePath ?? root;
|
|
4172
|
+
const isolationTelemetry = workItemIsolationTelemetry(result.workItem, worktreeIsolation.isolation);
|
|
4173
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "running", {
|
|
4174
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
4175
|
+
currentWorkItemId: result.workItem.workItemId,
|
|
4176
|
+
...isolationTelemetry.implementationScopeId ? { currentImplementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
4177
|
+
...isolationTelemetry.executionWorktreeKey ? { currentWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
4178
|
+
...isolationTelemetry.executionBranch ? { currentBranch: isolationTelemetry.executionBranch } : {}
|
|
4179
|
+
});
|
|
4180
|
+
const preview = await createToolRunPreview({ rootDir: executionRoot, prompt, tool: toolConfig.tool, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto", ...toolCommand ? { toolCommand } : {}, ...toolConfig.model ? { model: toolConfig.model } : {} });
|
|
3404
4181
|
const sessionContext = await prepareToolSession({
|
|
3405
4182
|
apiClient,
|
|
3406
4183
|
projectId,
|
|
3407
4184
|
repositoryLinkId,
|
|
3408
4185
|
runnerId,
|
|
4186
|
+
machineId: runnerMachineId(),
|
|
3409
4187
|
sessionPolicy: result.workItem.sessionPolicy ?? sessionPolicy,
|
|
3410
4188
|
toolName: preview.toolName,
|
|
3411
4189
|
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
3412
4190
|
supportsSessionReuse: preview.supportsSessionReuse,
|
|
3413
4191
|
resumabilityScope: preview.resumabilityScope,
|
|
3414
|
-
workItem: result.workItem
|
|
4192
|
+
workItem: result.workItem,
|
|
4193
|
+
isolationTelemetry
|
|
3415
4194
|
});
|
|
3416
4195
|
console.log(`Claimed ${result.workItem.workItemId}. Running ${preview.toolName}: ${preview.displayCommand}`);
|
|
4196
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4197
|
+
status: "running",
|
|
4198
|
+
summary: `Local runner started ${preview.toolName} execution.`,
|
|
4199
|
+
idempotencyKey: `runner_milestone_started_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
4200
|
+
metadata: { tool: preview.toolName, invocationChannel: toolConfig.requestedInvocationChannel ?? "auto" }
|
|
4201
|
+
});
|
|
3417
4202
|
const startedAt = Date.now();
|
|
3418
4203
|
const providerSessionStore = new LocalToolSessionStore();
|
|
3419
4204
|
const providerSessionId = sessionContext.toolSession ? await providerSessionStore.getProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName) : void 0;
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
4205
|
+
let toolResult;
|
|
4206
|
+
try {
|
|
4207
|
+
toolResult = await runLocalTool({
|
|
4208
|
+
rootDir: executionRoot,
|
|
4209
|
+
prompt,
|
|
4210
|
+
tool: toolConfig.tool,
|
|
4211
|
+
invocationChannel: toolConfig.requestedInvocationChannel ?? "auto",
|
|
4212
|
+
...toolCommand ? { toolCommand } : {},
|
|
4213
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
4214
|
+
streamOutput: stream,
|
|
4215
|
+
...sessionContext.toolSession ? {
|
|
4216
|
+
session: {
|
|
4217
|
+
toolSessionId: sessionContext.toolSession.toolSessionId,
|
|
4218
|
+
policy: sessionContext.policy,
|
|
4219
|
+
decision: sessionContext.decision,
|
|
4220
|
+
...providerSessionId ? { providerSessionId } : {}
|
|
4221
|
+
}
|
|
4222
|
+
} : {}
|
|
4223
|
+
});
|
|
4224
|
+
} catch (error) {
|
|
4225
|
+
const detail = truncateLogExcerpt(errorDetail(error));
|
|
4226
|
+
const durationMs2 = Date.now() - startedAt;
|
|
4227
|
+
const message = `${preview.toolName} failed before returning a result.`;
|
|
4228
|
+
await Promise.allSettled([
|
|
4229
|
+
apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig)),
|
|
4230
|
+
markToolSessionBlocked(apiClient, projectId, sessionContext.toolSession, errorMessage3(error)),
|
|
4231
|
+
apiClient.updateWorkStatus(projectId, result.workItem.workItemId, "failed", `run_failed_${result.workItem.workItemId}_${result.workItem.attempt}_${runnerId}`, runnerId, {
|
|
4232
|
+
...isolationTelemetry,
|
|
4233
|
+
tool: preview.toolName,
|
|
4234
|
+
...toolConfig.model ? { model: toolConfig.model } : {},
|
|
4235
|
+
durationMs: durationMs2,
|
|
4236
|
+
message,
|
|
4237
|
+
error: detail,
|
|
4238
|
+
...sessionContext.toolSession ? { toolSessionId: sessionContext.toolSession.toolSessionId } : {},
|
|
4239
|
+
sessionPolicy: sessionContext.policy,
|
|
4240
|
+
sessionDecision: sessionContext.decision,
|
|
4241
|
+
sessionDecisionReason: sessionContext.reason
|
|
4242
|
+
}),
|
|
4243
|
+
recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4244
|
+
status: "failed",
|
|
4245
|
+
summary: message,
|
|
4246
|
+
idempotencyKey: `runner_milestone_tool_throw_${result.workItem.workItemId}_${result.workItem.attempt}`,
|
|
4247
|
+
metadata: { tool: preview.toolName, error: detail }
|
|
4248
|
+
})
|
|
4249
|
+
]);
|
|
4250
|
+
if (verbose || !stream) {
|
|
4251
|
+
console.error(detail);
|
|
4252
|
+
}
|
|
4253
|
+
return { status: "failed", exitCode: 1, message };
|
|
4254
|
+
}
|
|
3441
4255
|
if (sessionContext.toolSession && toolResult.providerSessionId) {
|
|
3442
4256
|
await providerSessionStore.setProviderSessionId(sessionContext.toolSession.toolSessionId, preview.toolName, toolResult.providerSessionId);
|
|
3443
4257
|
}
|
|
@@ -3461,6 +4275,35 @@ async function runNextWorkItem({
|
|
|
3461
4275
|
workItem: result.workItem
|
|
3462
4276
|
});
|
|
3463
4277
|
}
|
|
4278
|
+
if (result.workItem.workKind === "assistantQuestion") {
|
|
4279
|
+
return finalizeAssistantQuestionWork({
|
|
4280
|
+
apiClient,
|
|
4281
|
+
durationMs: Date.now() - startedAt,
|
|
4282
|
+
projectId,
|
|
4283
|
+
repositoryLinkId,
|
|
4284
|
+
runnerId,
|
|
4285
|
+
sessionContext,
|
|
4286
|
+
toolConfig,
|
|
4287
|
+
toolName: preview.toolName,
|
|
4288
|
+
toolResult,
|
|
4289
|
+
workItem: result.workItem
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
if (result.workItem.workKind === "impactPreview") {
|
|
4293
|
+
return finalizeImpactPreviewWork({
|
|
4294
|
+
apiClient,
|
|
4295
|
+
durationMs: Date.now() - startedAt,
|
|
4296
|
+
projectId,
|
|
4297
|
+
repositoryLinkId,
|
|
4298
|
+
root,
|
|
4299
|
+
runnerId,
|
|
4300
|
+
sessionContext,
|
|
4301
|
+
toolConfig,
|
|
4302
|
+
toolName: preview.toolName,
|
|
4303
|
+
toolResult,
|
|
4304
|
+
workItem: result.workItem
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
3464
4307
|
const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
|
|
3465
4308
|
const durationMs = Date.now() - startedAt;
|
|
3466
4309
|
const failureExcerpt = toolResult.exitCode === 0 ? void 0 : truncateLogExcerpt(toolResult.stderr || toolResult.stdout);
|
|
@@ -3488,6 +4331,7 @@ async function runNextWorkItem({
|
|
|
3488
4331
|
...toolResult.model ? { model: toolResult.model } : {},
|
|
3489
4332
|
durationMs,
|
|
3490
4333
|
message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
4334
|
+
...isolationTelemetry,
|
|
3491
4335
|
sessionPolicy: sessionContext.policy,
|
|
3492
4336
|
sessionDecision: sessionContext.decision,
|
|
3493
4337
|
sessionDecisionReason: sessionContext.reason,
|
|
@@ -3499,11 +4343,85 @@ async function runNextWorkItem({
|
|
|
3499
4343
|
...failureExcerpt ? { error: failureExcerpt } : {}
|
|
3500
4344
|
}
|
|
3501
4345
|
);
|
|
4346
|
+
await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
|
|
4347
|
+
status: finalStatus,
|
|
4348
|
+
summary: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
|
|
4349
|
+
idempotencyKey: `runner_milestone_finished_${result.workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
4350
|
+
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 ?? "" }
|
|
4351
|
+
});
|
|
3502
4352
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3503
4353
|
const durationSeconds = Math.round(durationMs / 1e3);
|
|
3504
4354
|
console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
|
|
3505
4355
|
return { status: finalStatus, exitCode: toolResult.exitCode };
|
|
3506
4356
|
}
|
|
4357
|
+
async function prepareWorktreeForClaimedItem({ apiClient, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
|
|
4358
|
+
if (!needsGitWorktreeIsolation(workItem)) {
|
|
4359
|
+
return { status: "ready" };
|
|
4360
|
+
}
|
|
4361
|
+
const identity = resolveWorktreeIdentity(workItem);
|
|
4362
|
+
try {
|
|
4363
|
+
const isolation = await prepareGitWorktreeIsolation(root, workItem);
|
|
4364
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4365
|
+
status: "running",
|
|
4366
|
+
summary: `Prepared Git worktree ${isolation.worktreeKey} on ${isolation.branch}.`,
|
|
4367
|
+
idempotencyKey: `runner_milestone_worktree_${workItem.workItemId}_${workItem.attempt}`,
|
|
4368
|
+
metadata: { executionWorktreeKey: isolation.worktreeKey, executionBranch: isolation.branch, implementationScopeId: isolation.implementationScopeId }
|
|
4369
|
+
});
|
|
4370
|
+
return { status: "ready", isolation };
|
|
4371
|
+
} catch (error) {
|
|
4372
|
+
const message = errorMessage3(error);
|
|
4373
|
+
const telemetry = workItemIsolationTelemetry(workItem, { ...identity, baseRevision: workItem.baseRevision ?? "unknown", worktreePath: "" });
|
|
4374
|
+
const statusResult = await apiClient.updateWorkStatus(projectId, workItem.workItemId, "blocked", `worktree_${workItem.workItemId}_${randomUUID()}`, runnerId, {
|
|
4375
|
+
...telemetry,
|
|
4376
|
+
message,
|
|
4377
|
+
blockerReason: message,
|
|
4378
|
+
error: message
|
|
4379
|
+
});
|
|
4380
|
+
await recordRunnerMilestone(apiClient, projectId, statusResult.workItem, runnerId, repositoryLinkId, {
|
|
4381
|
+
status: "blocked",
|
|
4382
|
+
summary: message,
|
|
4383
|
+
idempotencyKey: `runner_milestone_worktree_blocked_${workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
|
|
4384
|
+
metadata: { executionWorktreeKey: telemetry.executionWorktreeKey ?? "", executionBranch: telemetry.executionBranch ?? "", implementationScopeId: telemetry.implementationScopeId ?? "" }
|
|
4385
|
+
});
|
|
4386
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "blocked", {
|
|
4387
|
+
...runnerHeartbeatMetadata(toolConfig),
|
|
4388
|
+
currentWorkItemId: workItem.workItemId,
|
|
4389
|
+
...telemetry.implementationScopeId ? { currentImplementationScopeId: telemetry.implementationScopeId } : {},
|
|
4390
|
+
...telemetry.executionWorktreeKey ? { currentWorktreeKey: telemetry.executionWorktreeKey } : {},
|
|
4391
|
+
...telemetry.executionBranch ? { currentBranch: telemetry.executionBranch } : {}
|
|
4392
|
+
});
|
|
4393
|
+
console.error(message);
|
|
4394
|
+
return { status: "blocked", message };
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
function workItemIsolationTelemetry(workItem, isolation) {
|
|
4398
|
+
const implementationScopeId = isolation?.implementationScopeId ?? workItem.implementationScopeId;
|
|
4399
|
+
const executionBranch = isolation?.branch ?? workItem.executionBranch;
|
|
4400
|
+
const executionWorktreeKey = isolation?.worktreeKey ?? workItem.executionWorktreeKey;
|
|
4401
|
+
const repositoryLockId = isolation?.repositoryLockId ?? workItem.repositoryLockId;
|
|
4402
|
+
return {
|
|
4403
|
+
...needsGitWorktreeIsolation(workItem) ? { isolationMode: "gitWorktree" } : workItem.isolationMode ? { isolationMode: workItem.isolationMode } : {},
|
|
4404
|
+
...workItem.claimLeaseId ? { claimLeaseId: workItem.claimLeaseId } : {},
|
|
4405
|
+
...workItem.controllingAdrId ? { controllingAdrId: workItem.controllingAdrId } : {},
|
|
4406
|
+
...implementationScopeId ? { implementationScopeId } : {},
|
|
4407
|
+
...executionBranch ? { executionBranch } : {},
|
|
4408
|
+
...executionWorktreeKey ? { executionWorktreeKey } : {},
|
|
4409
|
+
...repositoryLockId ? { repositoryLockId } : {},
|
|
4410
|
+
...isolation?.baseRevision && isolation.baseRevision !== "unknown" ? { baseRevision: isolation.baseRevision } : {},
|
|
4411
|
+
machineId: runnerMachineId()
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
4414
|
+
async function recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, input) {
|
|
4415
|
+
await apiClient.recordActivityEvent(projectId, {
|
|
4416
|
+
eventType: "runnerMilestone",
|
|
4417
|
+
runnerId,
|
|
4418
|
+
repositoryLinkId,
|
|
4419
|
+
relatedWorkItemId: workItem.workItemId,
|
|
4420
|
+
...workItem.reviewDocumentId ? { relatedDocumentId: workItem.reviewDocumentId } : workItem.impactDocumentId ? { relatedDocumentId: workItem.impactDocumentId } : {},
|
|
4421
|
+
...workItem.generatedDraftId ? { generatedDraftId: workItem.generatedDraftId } : {},
|
|
4422
|
+
...input
|
|
4423
|
+
}).catch(() => void 0);
|
|
4424
|
+
}
|
|
3507
4425
|
async function runPendingRunnerCommand(apiClient, context, heartbeatMetadata) {
|
|
3508
4426
|
const { commands } = await apiClient.listRunnerCommands(context.projectId, context.runnerId, context.repositoryLinkId).catch(() => ({ commands: [] }));
|
|
3509
4427
|
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];
|
|
@@ -3554,7 +4472,7 @@ async function restartCurrentRunner(context) {
|
|
|
3554
4472
|
const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
|
|
3555
4473
|
return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
|
|
3556
4474
|
} catch (error) {
|
|
3557
|
-
return { succeeded: false, message: "Background restart failed.", error:
|
|
4475
|
+
return { succeeded: false, message: "Background restart failed.", error: errorMessage3(error) };
|
|
3558
4476
|
}
|
|
3559
4477
|
}
|
|
3560
4478
|
function runnerCommandLabel(commandKind) {
|
|
@@ -3581,7 +4499,7 @@ async function finalizeBrainGenerationWork({
|
|
|
3581
4499
|
artifacts = parseBrainGenerationArtifacts(`${toolResult.stdout}
|
|
3582
4500
|
${toolResult.stderr}`);
|
|
3583
4501
|
} catch (error) {
|
|
3584
|
-
generationError =
|
|
4502
|
+
generationError = errorMessage3(error);
|
|
3585
4503
|
}
|
|
3586
4504
|
} else {
|
|
3587
4505
|
generationError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
@@ -3619,11 +4537,17 @@ ${toolResult.stderr}`);
|
|
|
3619
4537
|
...sessionTelemetry,
|
|
3620
4538
|
message: completionMessage
|
|
3621
4539
|
});
|
|
4540
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4541
|
+
status: "completed",
|
|
4542
|
+
summary: completionMessage,
|
|
4543
|
+
idempotencyKey: `runner_milestone_generation_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
4544
|
+
metadata: { tool: toolName, durationMs, artifactCount: result.documents.length, verificationSummary: "Generated artifacts were accepted by the Amistio API for review." }
|
|
4545
|
+
});
|
|
3622
4546
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3623
4547
|
console.log(workItem.workKind === "planRevision" ? "Revised plan returned for review." : `Generated ${result.documents.length} brain artifact${result.documents.length === 1 ? "" : "s"} for review.`);
|
|
3624
4548
|
return { status: "completed", exitCode: 0 };
|
|
3625
4549
|
}
|
|
3626
|
-
await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
4550
|
+
const failedResult = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
|
|
3627
4551
|
status: "failed",
|
|
3628
4552
|
runnerId,
|
|
3629
4553
|
idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
|
|
@@ -3633,11 +4557,216 @@ ${toolResult.stderr}`);
|
|
|
3633
4557
|
message: `${toolName} did not produce valid brain artifacts.`,
|
|
3634
4558
|
...generationError ? { error: generationError } : {}
|
|
3635
4559
|
});
|
|
4560
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4561
|
+
status: "failed",
|
|
4562
|
+
summary: generationError ?? `${toolName} did not produce valid brain artifacts.`,
|
|
4563
|
+
idempotencyKey: `runner_milestone_generation_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
4564
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Generation output could not be parsed into approved artifact JSON." }
|
|
4565
|
+
});
|
|
3636
4566
|
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
3637
4567
|
console.error(generationError ?? "Local runner generation failed.");
|
|
3638
4568
|
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
3639
4569
|
}
|
|
4570
|
+
async function finalizeAssistantQuestionWork({
|
|
4571
|
+
apiClient,
|
|
4572
|
+
durationMs,
|
|
4573
|
+
projectId,
|
|
4574
|
+
repositoryLinkId,
|
|
4575
|
+
runnerId,
|
|
4576
|
+
sessionContext,
|
|
4577
|
+
toolConfig,
|
|
4578
|
+
toolName,
|
|
4579
|
+
toolResult,
|
|
4580
|
+
workItem
|
|
4581
|
+
}) {
|
|
4582
|
+
let answerResult;
|
|
4583
|
+
let answerError;
|
|
4584
|
+
if (toolResult.exitCode === 0) {
|
|
4585
|
+
try {
|
|
4586
|
+
answerResult = parseAssistantAnswerResult(`${toolResult.stdout}
|
|
4587
|
+
${toolResult.stderr}`);
|
|
4588
|
+
} catch (error) {
|
|
4589
|
+
answerError = errorMessage3(error);
|
|
4590
|
+
}
|
|
4591
|
+
} else {
|
|
4592
|
+
answerError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
4593
|
+
}
|
|
4594
|
+
const finalStatus = answerResult ? "completed" : "failed";
|
|
4595
|
+
const updatedToolSession = await finalizeToolSession({
|
|
4596
|
+
apiClient,
|
|
4597
|
+
projectId,
|
|
4598
|
+
status: finalStatus,
|
|
4599
|
+
runnerId,
|
|
4600
|
+
workItemId: workItem.workItemId,
|
|
4601
|
+
stdout: toolResult.stdout,
|
|
4602
|
+
...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
|
|
4603
|
+
...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
|
|
4604
|
+
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
4605
|
+
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
4606
|
+
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
4607
|
+
});
|
|
4608
|
+
const sessionTelemetry = {
|
|
4609
|
+
sessionPolicy: sessionContext.policy,
|
|
4610
|
+
sessionDecision: sessionContext.decision,
|
|
4611
|
+
sessionDecisionReason: sessionContext.reason,
|
|
4612
|
+
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
4613
|
+
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
4614
|
+
};
|
|
4615
|
+
if (answerResult) {
|
|
4616
|
+
const result = await apiClient.submitAssistantResult(projectId, workItem.workItemId, {
|
|
4617
|
+
status: "completed",
|
|
4618
|
+
runnerId,
|
|
4619
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID()}`,
|
|
4620
|
+
answer: answerResult.answer,
|
|
4621
|
+
sourceBoundary: answerResult.sourceBoundary,
|
|
4622
|
+
citations: answerResult.citations,
|
|
4623
|
+
tool: toolName,
|
|
4624
|
+
durationMs,
|
|
4625
|
+
...sessionTelemetry,
|
|
4626
|
+
message: `${toolName} returned a project knowledge answer.`
|
|
4627
|
+
});
|
|
4628
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4629
|
+
status: "completed",
|
|
4630
|
+
summary: `${toolName} returned a project knowledge answer.`,
|
|
4631
|
+
idempotencyKey: `runner_milestone_assistant_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
4632
|
+
metadata: { tool: toolName, durationMs, sourceBoundary: answerResult.sourceBoundary, citationCount: answerResult.citations.length }
|
|
4633
|
+
});
|
|
4634
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4635
|
+
console.log("Project knowledge answer returned.");
|
|
4636
|
+
return { status: "completed", exitCode: 0 };
|
|
4637
|
+
}
|
|
4638
|
+
const failedResult = await apiClient.submitAssistantResult(projectId, workItem.workItemId, {
|
|
4639
|
+
status: "failed",
|
|
4640
|
+
runnerId,
|
|
4641
|
+
idempotencyKey: `assistant_${workItem.workItemId}_${randomUUID()}`,
|
|
4642
|
+
tool: toolName,
|
|
4643
|
+
durationMs,
|
|
4644
|
+
...sessionTelemetry,
|
|
4645
|
+
message: `${toolName} did not produce a valid project knowledge answer.`,
|
|
4646
|
+
...answerError ? { error: answerError } : {}
|
|
4647
|
+
});
|
|
4648
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4649
|
+
status: "failed",
|
|
4650
|
+
summary: answerError ?? `${toolName} did not produce a valid project knowledge answer.`,
|
|
4651
|
+
idempotencyKey: `runner_milestone_assistant_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
4652
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Assistant output did not include a valid structured answer." }
|
|
4653
|
+
});
|
|
4654
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4655
|
+
console.error(answerError ?? "Local runner assistant answer failed.");
|
|
4656
|
+
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
4657
|
+
}
|
|
4658
|
+
async function finalizeImpactPreviewWork({
|
|
4659
|
+
apiClient,
|
|
4660
|
+
durationMs,
|
|
4661
|
+
projectId,
|
|
4662
|
+
repositoryLinkId,
|
|
4663
|
+
root,
|
|
4664
|
+
runnerId,
|
|
4665
|
+
sessionContext,
|
|
4666
|
+
toolConfig,
|
|
4667
|
+
toolName,
|
|
4668
|
+
toolResult,
|
|
4669
|
+
workItem
|
|
4670
|
+
}) {
|
|
4671
|
+
let report = void 0;
|
|
4672
|
+
let previewError;
|
|
4673
|
+
if (toolResult.exitCode === 0) {
|
|
4674
|
+
try {
|
|
4675
|
+
report = parseImpactPreviewResult(`${toolResult.stdout}
|
|
4676
|
+
${toolResult.stderr}`);
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
previewError = errorMessage3(error);
|
|
4679
|
+
}
|
|
4680
|
+
} else {
|
|
4681
|
+
previewError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
|
|
4682
|
+
}
|
|
4683
|
+
const finalStatus = report ? "completed" : "failed";
|
|
4684
|
+
const updatedToolSession = await finalizeToolSession({
|
|
4685
|
+
apiClient,
|
|
4686
|
+
projectId,
|
|
4687
|
+
status: finalStatus,
|
|
4688
|
+
runnerId,
|
|
4689
|
+
workItemId: workItem.workItemId,
|
|
4690
|
+
stdout: toolResult.stdout,
|
|
4691
|
+
...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
|
|
4692
|
+
...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
|
|
4693
|
+
...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
|
|
4694
|
+
...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
|
|
4695
|
+
...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
|
|
4696
|
+
});
|
|
4697
|
+
const sessionTelemetry = {
|
|
4698
|
+
sessionPolicy: sessionContext.policy,
|
|
4699
|
+
sessionDecision: sessionContext.decision,
|
|
4700
|
+
sessionDecisionReason: sessionContext.reason,
|
|
4701
|
+
...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
|
|
4702
|
+
...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
|
|
4703
|
+
};
|
|
4704
|
+
if (report) {
|
|
4705
|
+
const metadata = await readProjectLink(root).catch(() => void 0);
|
|
4706
|
+
const result = await apiClient.submitImpactPreviewResult(projectId, workItem.workItemId, {
|
|
4707
|
+
status: "completed",
|
|
4708
|
+
runnerId,
|
|
4709
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID()}`,
|
|
4710
|
+
report: {
|
|
4711
|
+
...report,
|
|
4712
|
+
analyzedRepoRevision: report.analyzedRepoRevision ?? metadata?.lastSyncedRevision
|
|
4713
|
+
},
|
|
4714
|
+
tool: toolName,
|
|
4715
|
+
durationMs,
|
|
4716
|
+
...sessionTelemetry,
|
|
4717
|
+
message: `${toolName} returned an implementation impact preview.`
|
|
4718
|
+
});
|
|
4719
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4720
|
+
status: "completed",
|
|
4721
|
+
summary: `${toolName} returned an implementation impact preview.`,
|
|
4722
|
+
idempotencyKey: `runner_milestone_impact_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
|
|
4723
|
+
metadata: { tool: toolName, durationMs, riskLevel: report.riskLevel, likelyPathCount: report.likelyPaths.length, verificationSummary: report.verificationPlan }
|
|
4724
|
+
});
|
|
4725
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4726
|
+
console.log(result.implementationWorkItem ? "Impact preview returned; implementation work is now queued." : "Impact preview returned.");
|
|
4727
|
+
return { status: "completed", exitCode: 0 };
|
|
4728
|
+
}
|
|
4729
|
+
const failedResult = await apiClient.submitImpactPreviewResult(projectId, workItem.workItemId, {
|
|
4730
|
+
status: "failed",
|
|
4731
|
+
runnerId,
|
|
4732
|
+
idempotencyKey: `impact_${workItem.workItemId}_${randomUUID()}`,
|
|
4733
|
+
tool: toolName,
|
|
4734
|
+
durationMs,
|
|
4735
|
+
...sessionTelemetry,
|
|
4736
|
+
message: `${toolName} did not produce a valid impact preview.`,
|
|
4737
|
+
...previewError ? { error: previewError } : {}
|
|
4738
|
+
});
|
|
4739
|
+
await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
|
|
4740
|
+
status: "failed",
|
|
4741
|
+
summary: previewError ?? `${toolName} did not produce a valid impact preview.`,
|
|
4742
|
+
idempotencyKey: `runner_milestone_impact_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
|
|
4743
|
+
metadata: { tool: toolName, durationMs, verificationSummary: "Impact preview output did not include valid structured JSON." }
|
|
4744
|
+
});
|
|
4745
|
+
await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
|
|
4746
|
+
console.error(previewError ?? "Local runner impact preview failed.");
|
|
4747
|
+
return { status: "failed", exitCode: toolResult.exitCode || 1 };
|
|
4748
|
+
}
|
|
3640
4749
|
async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
|
|
4750
|
+
if (workItem.workKind === "assistantQuestion") {
|
|
4751
|
+
const [{ documents: documents2 }, { messages: messages2 }] = await Promise.all([
|
|
4752
|
+
apiClient.listBrainDocuments(projectId),
|
|
4753
|
+
apiClient.listAssistantMessages(projectId)
|
|
4754
|
+
]);
|
|
4755
|
+
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];
|
|
4756
|
+
return createWorkExecutionPrompt(workItem, {
|
|
4757
|
+
...question ? { assistantQuestion: { question, messages: messages2, documents: documents2 } } : {}
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
if (workItem.workKind === "impactPreview") {
|
|
4761
|
+
const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
|
|
4762
|
+
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"));
|
|
4763
|
+
return createWorkExecutionPrompt(workItem, {
|
|
4764
|
+
impactPreview: {
|
|
4765
|
+
documents: documents2,
|
|
4766
|
+
...implementationPrompt ? { implementationPrompt } : {}
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
3641
4770
|
if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
|
|
3642
4771
|
return createWorkExecutionPrompt(workItem);
|
|
3643
4772
|
}
|
|
@@ -3710,6 +4839,8 @@ function isPromptReadyStatus(status) {
|
|
|
3710
4839
|
}
|
|
3711
4840
|
async function prepareToolSession({
|
|
3712
4841
|
apiClient,
|
|
4842
|
+
isolationTelemetry,
|
|
4843
|
+
machineId,
|
|
3713
4844
|
projectId,
|
|
3714
4845
|
repositoryLinkId,
|
|
3715
4846
|
runnerId,
|
|
@@ -3728,6 +4859,7 @@ async function prepareToolSession({
|
|
|
3728
4859
|
toolName,
|
|
3729
4860
|
runnerId,
|
|
3730
4861
|
repositoryLinkId,
|
|
4862
|
+
machineId,
|
|
3731
4863
|
supportsSessionReuse
|
|
3732
4864
|
});
|
|
3733
4865
|
if (selection.decision === "skipped") {
|
|
@@ -3737,8 +4869,12 @@ async function prepareToolSession({
|
|
|
3737
4869
|
const { toolSession: toolSession2 } = await apiClient.updateToolSession(projectId, selection.toolSession.toolSessionId, {
|
|
3738
4870
|
status: "active",
|
|
3739
4871
|
runnerId,
|
|
4872
|
+
machineId,
|
|
3740
4873
|
lastWorkItemId: workItem.workItemId,
|
|
3741
|
-
reusePolicy: sessionPolicy
|
|
4874
|
+
reusePolicy: sessionPolicy,
|
|
4875
|
+
...isolationTelemetry.implementationScopeId ? { implementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
4876
|
+
...isolationTelemetry.executionWorktreeKey ? { executionWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
4877
|
+
...isolationTelemetry.isolationMode ? { isolationMode: isolationTelemetry.isolationMode } : {}
|
|
3742
4878
|
});
|
|
3743
4879
|
return { ...selection, toolSession: toolSession2 };
|
|
3744
4880
|
}
|
|
@@ -3754,9 +4890,14 @@ async function prepareToolSession({
|
|
|
3754
4890
|
summary: selection.reason,
|
|
3755
4891
|
status: "active",
|
|
3756
4892
|
runnerId,
|
|
4893
|
+
machineId,
|
|
3757
4894
|
lastWorkItemId: workItem.workItemId,
|
|
3758
4895
|
messageCount: 0,
|
|
3759
4896
|
reusePolicy: sessionPolicy,
|
|
4897
|
+
...workItem.requestedByUserId ?? workItem.requestedBy ? { createdByUserId: workItem.requestedByUserId ?? workItem.requestedBy } : {},
|
|
4898
|
+
...isolationTelemetry.implementationScopeId ? { implementationScopeId: isolationTelemetry.implementationScopeId } : {},
|
|
4899
|
+
...isolationTelemetry.executionWorktreeKey ? { executionWorktreeKey: isolationTelemetry.executionWorktreeKey } : {},
|
|
4900
|
+
...isolationTelemetry.isolationMode ? { isolationMode: isolationTelemetry.isolationMode } : {},
|
|
3760
4901
|
...workItem.sessionGroupKey ? { sessionGroupKey: workItem.sessionGroupKey } : {}
|
|
3761
4902
|
});
|
|
3762
4903
|
return { ...selection, toolSession };
|
|
@@ -3817,9 +4958,12 @@ function summarizeToolOutput(value) {
|
|
|
3817
4958
|
}
|
|
3818
4959
|
return trimmed.length > 300 ? `${trimmed.slice(0, 300)}...` : trimmed;
|
|
3819
4960
|
}
|
|
3820
|
-
function
|
|
4961
|
+
function errorMessage3(error) {
|
|
3821
4962
|
return error instanceof Error ? error.message : String(error);
|
|
3822
4963
|
}
|
|
4964
|
+
function errorDetail(error) {
|
|
4965
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
4966
|
+
}
|
|
3823
4967
|
function truncateLogExcerpt(value) {
|
|
3824
4968
|
const trimmed = value.trim();
|
|
3825
4969
|
return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
|
|
@@ -3844,10 +4988,10 @@ function parseInvocationChannel(value) {
|
|
|
3844
4988
|
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
3845
4989
|
}
|
|
3846
4990
|
function inferRepoName(root) {
|
|
3847
|
-
return
|
|
4991
|
+
return path12.basename(path12.resolve(root)) || "repository";
|
|
3848
4992
|
}
|
|
3849
4993
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
3850
|
-
return
|
|
4994
|
+
return createHash5("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
3851
4995
|
}
|
|
3852
4996
|
function defaultApiUrl() {
|
|
3853
4997
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -3961,14 +5105,24 @@ function toRunnerToolCapabilities(tools) {
|
|
|
3961
5105
|
execution: tool.execution,
|
|
3962
5106
|
supportsSessionReuse: tool.supportsSessionReuse,
|
|
3963
5107
|
resumabilityScope: tool.resumabilityScope,
|
|
3964
|
-
supportsModelSelection: tool.supportsModelSelection
|
|
5108
|
+
supportsModelSelection: tool.supportsModelSelection,
|
|
5109
|
+
supportsBranchIsolation: true,
|
|
5110
|
+
supportsGitWorktreeIsolation: true
|
|
3965
5111
|
}));
|
|
3966
5112
|
}
|
|
5113
|
+
function runnerIsolationCapabilityMetadata() {
|
|
5114
|
+
return {
|
|
5115
|
+
machineId: runnerMachineId(),
|
|
5116
|
+
supportsBranchIsolation: true,
|
|
5117
|
+
supportsGitWorktreeIsolation: true
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
3967
5120
|
function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
3968
5121
|
return {
|
|
3969
5122
|
version: CLI_VERSION,
|
|
3970
5123
|
mode,
|
|
3971
5124
|
hostname: os5.hostname(),
|
|
5125
|
+
...runnerIsolationCapabilityMetadata(),
|
|
3972
5126
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
3973
5127
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
3974
5128
|
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
@@ -3980,6 +5134,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
3980
5134
|
...toolConfig?.message ? { preferenceMessage: toolConfig.message } : {}
|
|
3981
5135
|
};
|
|
3982
5136
|
}
|
|
5137
|
+
function runnerMachineId() {
|
|
5138
|
+
return createHash5("sha256").update(`${os5.hostname()}:${os5.platform()}:${os5.arch()}`).digest("hex").slice(0, 20);
|
|
5139
|
+
}
|
|
3983
5140
|
async function delay(milliseconds) {
|
|
3984
5141
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
3985
5142
|
}
|