@amistio/cli 0.1.12 → 0.1.13

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  import { createHash as createHash7, randomUUID } from "node:crypto";
5
5
  import { writeFile as writeFile9 } from "node:fs/promises";
6
6
  import os7 from "node:os";
7
- import path13 from "node:path";
7
+ import path14 from "node:path";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // ../shared/src/schemas.ts
@@ -24,6 +24,12 @@ var itemTypeSchema = z.enum([
24
24
  "securityScan",
25
25
  "securityFinding",
26
26
  "securityPostureSnapshot",
27
+ "appEvaluationScan",
28
+ "appEvaluationFinding",
29
+ "projectContextMap",
30
+ "projectContextRefresh",
31
+ "projectSystemDiagram",
32
+ "contextMiss",
27
33
  "syncCursor",
28
34
  "syncConflict",
29
35
  "workItem",
@@ -48,6 +54,8 @@ var documentTypeSchema = z.enum([
48
54
  "prompt",
49
55
  "workflow"
50
56
  ]);
57
+ var documentContentFormatSchema = z.enum(["markdown", "html"]);
58
+ var artifactFormatPreferenceSchema = z.enum(["markdown", "html", "both", "auto"]);
51
59
  var syncStateSchema = z.enum([
52
60
  "draft",
53
61
  "approved",
@@ -70,7 +78,23 @@ var workStatusSchema = z.enum([
70
78
  ]);
71
79
  var sourceSchema = z.enum(["web", "repo", "generated", "runner"]);
72
80
  var executionModeSchema = z.enum(["localRunner", "cloudSandbox"]);
73
- var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan"]);
81
+ var workKindSchema = z.enum(["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh"]);
82
+ var implementationHandoffStatusSchema = z.enum(["notStarted", "noChanges", "prReady", "blocked", "failed"]);
83
+ var implementationHandoffCleanupStatusSchema = z.enum(["notApplicable", "pending", "completed", "failed"]);
84
+ var implementationHandoffSchema = z.object({
85
+ provider: z.string().trim().min(1).max(80).optional(),
86
+ status: implementationHandoffStatusSchema,
87
+ baseBranch: z.string().trim().min(1).max(200).optional(),
88
+ headBranch: z.string().trim().min(1).max(200).optional(),
89
+ remoteName: z.string().trim().min(1).max(80).optional(),
90
+ commitSha: z.string().trim().min(7).max(64).optional(),
91
+ prNumber: z.number().int().positive().optional(),
92
+ prUrl: z.string().trim().url().max(500).optional(),
93
+ cleanupStatus: implementationHandoffCleanupStatusSchema.optional(),
94
+ cleanupMessage: z.string().trim().min(1).max(600).optional(),
95
+ message: z.string().trim().min(1).max(600).optional(),
96
+ error: z.string().trim().min(1).max(1200).optional()
97
+ }).strict();
74
98
  var generatedDraftStatusSchema = z.enum(["queued", "generating", "reviewing", "approved", "rejected", "changesRequested", "failed"]);
75
99
  var assistantQuestionModeSchema = z.enum(["brainOnly", "sourceAware"]);
76
100
  var impactRiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
@@ -80,12 +104,19 @@ var issueSeveritySchema = z.enum(["low", "medium", "high", "critical"]);
80
104
  var issueStatusSchema = z.enum(["queued", "diagnosing", "analysisReady", "approved", "changesRequested", "rejected", "implementationQueued", "implemented", "failed"]);
81
105
  var issueApprovalStateSchema = z.enum(["proposed", "approved", "changesRequested", "rejected", "implemented", "blocked"]);
82
106
  var securityScanStatusSchema = z.enum(["queued", "running", "completed", "failed", "stale", "skipped"]);
83
- var securityScanSourceSchema = z.enum(["nightly", "manual"]);
107
+ var securityScanSourceSchema = z.enum(["nightly", "manual", "runner"]);
84
108
  var securityFindingCategorySchema = z.enum(["owasp", "dependency", "supplyChain", "secretHygiene", "authentication", "authorization", "tenantIsolation", "headers", "redirects", "csrf", "cors", "ciCd", "logging", "runnerBoundary", "other"]);
85
109
  var securityFindingSeveritySchema = z.enum(["info", "low", "medium", "high", "critical"]);
86
110
  var securityFindingConfidenceSchema = z.enum(["low", "medium", "high"]);
87
111
  var securityFindingStatusSchema = z.enum(["open", "planReady", "approved", "changesRequested", "dismissed", "acceptedRisk", "remediationQueued", "resolved", "failed"]);
88
112
  var securityApprovalStateSchema = z.enum(["proposed", "approved", "changesRequested", "dismissed", "acceptedRisk", "implemented", "blocked"]);
113
+ var appEvaluationScanStatusSchema = z.enum(["queued", "running", "completed", "failed", "stale", "skipped"]);
114
+ var appEvaluationFindingCategorySchema = z.enum(["verification", "productDrift", "planCleanup", "promptRot", "missingMemory", "releaseReadiness", "ux", "accessibility", "performance", "reliability", "securityPosture", "other"]);
115
+ var appEvaluationFindingSeveritySchema = z.enum(["info", "low", "medium", "high", "critical"]);
116
+ var appEvaluationFindingConfidenceSchema = z.enum(["low", "medium", "high"]);
117
+ var appEvaluationFindingStatusSchema = z.enum(["open", "planReady", "approved", "changesRequested", "dismissed", "acceptedRisk", "implementationQueued", "resolved", "failed"]);
118
+ var appEvaluationApprovalStateSchema = z.enum(["proposed", "approved", "changesRequested", "dismissed", "acceptedRisk", "implemented", "blocked"]);
119
+ var appEvaluationPlanLifecycleActionSchema = z.enum(["createPlan", "updatePlan", "markCompleted", "markImplemented", "markSuperseded", "markBlocked", "archive", "keepActive", "none"]);
89
120
  var activityEventTypeSchema = z.enum([
90
121
  "commandSubmitted",
91
122
  "generationQueued",
@@ -117,6 +148,22 @@ var activityEventTypeSchema = z.enum([
117
148
  "securityFindingReviewed",
118
149
  "securityRemediationPlanCreated",
119
150
  "securityRemediationQueued",
151
+ "appEvaluationQueued",
152
+ "appEvaluationStarted",
153
+ "appEvaluationCompleted",
154
+ "appEvaluationFailed",
155
+ "appEvaluationFindingOpened",
156
+ "appEvaluationFindingReviewed",
157
+ "appEvaluationPlanCreated",
158
+ "appEvaluationImplementationQueued",
159
+ "projectContextRefreshQueued",
160
+ "projectContextRefreshStarted",
161
+ "projectContextRefreshCompleted",
162
+ "projectContextRefreshFailed",
163
+ "projectContextMissRecorded",
164
+ "projectSystemDiagramGenerated",
165
+ "projectSystemDiagramReviewed",
166
+ "projectSystemDiagramMissRecorded",
120
167
  "handoffExported"
121
168
  ]);
122
169
  var activityEventActorSchema = z.enum(["webUser", "runner", "cli", "system"]);
@@ -131,6 +178,7 @@ var activityMetadataValueSchema = z.union([
131
178
  var activityMetadataSchema = z.record(z.string().min(1).max(80), activityMetadataValueSchema).default({});
132
179
  var generatedBrainArtifactSchema = z.object({
133
180
  documentType: documentTypeSchema,
181
+ contentFormat: documentContentFormatSchema.default("markdown"),
134
182
  title: z.string().trim().min(1),
135
183
  repoPath: z.string().trim().min(1),
136
184
  content: z.string().min(1)
@@ -243,6 +291,7 @@ var organizationWorkspaceScopeSchema = z.object({
243
291
  kind: z.literal("organization"),
244
292
  accountId: z.string().min(1),
245
293
  organizationId: z.string().min(1),
294
+ userId: z.string().min(1).optional(),
246
295
  organizationSlug: z.string().trim().min(1).optional(),
247
296
  organizationRole: z.string().trim().min(1).optional(),
248
297
  label: z.string().trim().min(1).default("Organization workspace")
@@ -282,6 +331,7 @@ var projectItemSchema = baseItemSchema.extend({
282
331
  name: z.string().min(1),
283
332
  slug: z.string().min(1),
284
333
  description: z.string().optional(),
334
+ artifactFormatPreference: artifactFormatPreferenceSchema.optional(),
285
335
  status: projectStatusSchema.default("active"),
286
336
  archivedAt: isoDateTimeSchema.optional(),
287
337
  archivedByUserId: z.string().min(1).optional(),
@@ -302,6 +352,7 @@ var repositoryLinkItemSchema = baseItemSchema.extend({
302
352
  linkSource: repositoryLinkSourceSchema.optional(),
303
353
  cloneStatus: repositoryCloneStatusSchema.optional(),
304
354
  autoSyncEnabled: z.boolean().optional(),
355
+ appEvaluationEnabled: z.boolean().optional(),
305
356
  lastValidatedAt: isoDateTimeSchema.optional(),
306
357
  status: z.enum(["active", "revoked"]).default("active")
307
358
  });
@@ -310,6 +361,7 @@ var brainDocumentItemSchema = baseItemSchema.extend({
310
361
  projectId: z.string().min(1),
311
362
  documentId: z.string().min(1),
312
363
  documentType: documentTypeSchema,
364
+ contentFormat: documentContentFormatSchema.default("markdown"),
313
365
  title: z.string().min(1),
314
366
  status: z.string().min(1),
315
367
  repoPath: z.string().min(1),
@@ -369,6 +421,10 @@ var workItemSchema = baseItemSchema.extend({
369
421
  issueId: z.string().min(1).optional(),
370
422
  securityScanId: z.string().min(1).optional(),
371
423
  securityFindingId: z.string().min(1).optional(),
424
+ appEvaluationScanId: z.string().min(1).optional(),
425
+ appEvaluationFindingId: z.string().min(1).optional(),
426
+ projectContextRefreshId: z.string().min(1).optional(),
427
+ contextMissId: z.string().min(1).optional(),
372
428
  repositoryLinkId: z.string().min(1).optional(),
373
429
  reviewThreadId: z.string().min(1).optional(),
374
430
  reviewDocumentId: z.string().min(1).optional(),
@@ -376,6 +432,7 @@ var workItemSchema = baseItemSchema.extend({
376
432
  reviewMessageId: z.string().min(1).optional(),
377
433
  assistantMessageId: z.string().min(1).optional(),
378
434
  assistantQuestionMode: assistantQuestionModeSchema.optional(),
435
+ artifactFormatPreference: artifactFormatPreferenceSchema.optional(),
379
436
  impactReportId: z.string().min(1).optional(),
380
437
  impactDocumentId: z.string().min(1).optional(),
381
438
  impactDocumentRevision: z.number().int().nonnegative().optional(),
@@ -401,6 +458,7 @@ var workItemSchema = baseItemSchema.extend({
401
458
  toolSessionId: z.string().min(1).optional(),
402
459
  sessionDecision: sessionDecisionSchema.optional(),
403
460
  sessionDecisionReason: z.string().min(1).optional(),
461
+ implementationHandoff: implementationHandoffSchema.optional(),
404
462
  lastStatusMessage: z.string().optional(),
405
463
  lastStatusAt: isoDateTimeSchema
406
464
  });
@@ -495,6 +553,7 @@ var runnerExecutionLogItemSchema = baseItemSchema.extend({
495
553
  toolSessionId: z.string().min(1).optional(),
496
554
  sessionDecision: sessionDecisionSchema.optional(),
497
555
  sessionDecisionReason: z.string().min(1).optional(),
556
+ implementationHandoff: implementationHandoffSchema.optional(),
498
557
  message: z.string().optional(),
499
558
  error: z.string().optional()
500
559
  });
@@ -591,7 +650,7 @@ var assistantAnswerResultSchema = z.object({
591
650
  summary: z.string().optional()
592
651
  });
593
652
  var knowledgeSearchScopeSchema = z.enum(["project", "organization"]);
594
- var knowledgeSourceTypeSchema = z.enum(["brainDocument", "generatedArtifact", "runnerSummary", "knowledgeEntity", "knowledgeRelation"]);
653
+ var knowledgeSourceTypeSchema = z.enum(["brainDocument", "generatedArtifact", "runnerSummary", "knowledgeEntity", "knowledgeRelation", "projectContextMap", "projectContextSlice", "projectSystemDiagram", "projectSystemDiagramNode", "projectSystemDiagramEdge"]);
595
654
  var knowledgeChunkStatusSchema = z.enum(["approved", "synced", "stale"]);
596
655
  var knowledgeEntityTypeSchema = z.enum(["project", "system", "component", "domain", "tool", "decision", "feature", "risk", "team", "workflow", "unknown"]);
597
656
  var knowledgeRelationTypeSchema = z.enum(["uses", "depends_on", "decides", "supersedes", "touches", "blocks", "implements", "mentions"]);
@@ -703,6 +762,221 @@ var knowledgeDiagramResultSchema = z.object({
703
762
  freshness: knowledgeIndexFreshnessSchema,
704
763
  warnings: z.array(z.string().trim().min(1).max(600)).default([])
705
764
  });
765
+ var projectContextSliceKindSchema = z.enum(["overview", "architecture", "domain", "data", "api", "frontend", "backend", "cli", "workflow", "operations", "security", "testing", "unknown"]);
766
+ var projectContextFreshnessSchema = z.enum(["fresh", "stale", "partial", "missing"]);
767
+ var projectContextMapStatusSchema = z.enum(["draft", "proposed", "approved", "stale", "archived"]);
768
+ var projectContextRefreshStatusSchema = z.enum(["queued", "running", "completed", "failed", "skipped"]);
769
+ var contextMissStatusSchema = z.enum(["open", "resolved", "dismissed"]);
770
+ var projectContextRepoPathSchema = z.string().trim().min(1).max(300).refine((value) => {
771
+ if (value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value)) {
772
+ return false;
773
+ }
774
+ return !value.split(/[\\/]+/).includes("..");
775
+ }, "Path must be repository-relative without traversal");
776
+ var projectContextCoverageSchema = z.object({
777
+ status: projectContextFreshnessSchema.default("missing"),
778
+ sliceCount: z.number().int().nonnegative().default(0),
779
+ staleSliceCount: z.number().int().nonnegative().default(0),
780
+ missingAreas: z.array(z.string().trim().min(1).max(160)).default([]),
781
+ warnings: z.array(z.string().trim().min(1).max(300)).default([])
782
+ });
783
+ var defaultProjectContextCoverage = { status: "missing", sliceCount: 0, staleSliceCount: 0, missingAreas: [], warnings: [] };
784
+ var projectContextSliceSchema = z.object({
785
+ sliceId: z.string().trim().min(1).max(160),
786
+ kind: projectContextSliceKindSchema.default("unknown"),
787
+ title: z.string().trim().min(1).max(200),
788
+ summary: z.string().trim().min(1).max(4e3),
789
+ repoPaths: z.array(projectContextRepoPathSchema).max(80).default([]),
790
+ ownerHints: z.array(z.string().trim().min(1).max(160)).max(20).default([]),
791
+ tags: z.array(z.string().trim().min(1).max(80)).max(30).default([]),
792
+ citations: z.array(assistantCitationSchema).default([]),
793
+ dependsOn: z.array(z.string().trim().min(1).max(160)).max(50).default([]),
794
+ freshness: projectContextFreshnessSchema.default("fresh"),
795
+ confidence: z.number().min(0).max(1).default(0.6),
796
+ contentHash: z.string().trim().min(1).max(160).optional(),
797
+ lastVerifiedAt: isoDateTimeSchema.optional()
798
+ });
799
+ var projectContextEntitySchema = z.object({
800
+ entityId: z.string().trim().min(1).max(200),
801
+ name: z.string().trim().min(1).max(200),
802
+ entityType: knowledgeEntityTypeSchema.default("unknown"),
803
+ summary: z.string().trim().min(1).max(1200).optional(),
804
+ aliases: z.array(z.string().trim().min(1).max(200)).default([]),
805
+ sliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
806
+ citations: z.array(assistantCitationSchema).default([]),
807
+ confidence: z.number().min(0).max(1).default(0.6)
808
+ });
809
+ var projectContextRelationSchema = z.object({
810
+ relationId: z.string().trim().min(1).max(200),
811
+ relationType: knowledgeRelationTypeSchema.default("mentions"),
812
+ fromId: z.string().trim().min(1).max(200),
813
+ toId: z.string().trim().min(1).max(200),
814
+ summary: z.string().trim().min(1).max(1200),
815
+ citations: z.array(assistantCitationSchema).default([]),
816
+ confidence: z.number().min(0).max(1).default(0.6)
817
+ });
818
+ var projectSystemDiagramStatusSchema = z.enum(["draft", "proposed", "approved", "stale", "archived"]);
819
+ var projectSystemDiagramViewKindSchema = z.enum(["wholeSystem", "projectBrainLifecycle", "contextRefresh", "runtimeTopology", "featureFlow"]);
820
+ var projectSystemDiagramNodeKindSchema = z.enum(["actor", "app", "api", "cli", "runner", "dataStore", "searchIndex", "authBoundary", "repository", "externalService", "workflow", "component", "unknown"]);
821
+ var projectSystemDiagramGroupKindSchema = z.enum(["boundary", "lane", "group"]);
822
+ var projectSystemDiagramDirectionSchema = z.enum(["TD", "LR"]);
823
+ var projectSystemDiagramNodeSchema = z.object({
824
+ nodeId: z.string().trim().min(1).max(200),
825
+ label: z.string().trim().min(1).max(120),
826
+ kind: projectSystemDiagramNodeKindSchema.default("unknown"),
827
+ summary: z.string().trim().min(1).max(1200).optional(),
828
+ groupId: z.string().trim().min(1).max(160).optional(),
829
+ lane: z.string().trim().min(1).max(120).optional(),
830
+ sliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
831
+ entityIds: z.array(z.string().trim().min(1).max(200)).default([]),
832
+ citations: z.array(assistantCitationSchema).min(1),
833
+ confidence: z.number().min(0).max(1).default(0.6),
834
+ freshness: projectContextFreshnessSchema.default("fresh")
835
+ });
836
+ var projectSystemDiagramEdgeSchema = z.object({
837
+ edgeId: z.string().trim().min(1).max(220),
838
+ fromNodeId: z.string().trim().min(1).max(200),
839
+ toNodeId: z.string().trim().min(1).max(200),
840
+ relationType: knowledgeRelationTypeSchema.default("mentions"),
841
+ label: z.string().trim().min(1).max(120).optional(),
842
+ summary: z.string().trim().min(1).max(1200),
843
+ citations: z.array(assistantCitationSchema).min(1),
844
+ confidence: z.number().min(0).max(1).default(0.6),
845
+ freshness: projectContextFreshnessSchema.default("fresh")
846
+ });
847
+ var projectSystemDiagramGroupSchema = z.object({
848
+ groupId: z.string().trim().min(1).max(160),
849
+ label: z.string().trim().min(1).max(120),
850
+ kind: projectSystemDiagramGroupKindSchema.default("group"),
851
+ summary: z.string().trim().min(1).max(1200).optional(),
852
+ citations: z.array(assistantCitationSchema).default([])
853
+ });
854
+ var projectSystemDiagramViewSchema = z.object({
855
+ viewId: z.string().trim().min(1).max(160),
856
+ kind: projectSystemDiagramViewKindSchema,
857
+ title: z.string().trim().min(1).max(160),
858
+ summary: z.string().trim().min(1).max(1200),
859
+ nodeIds: z.array(z.string().trim().min(1).max(200)).min(1),
860
+ edgeIds: z.array(z.string().trim().min(1).max(220)).default([]),
861
+ groupIds: z.array(z.string().trim().min(1).max(160)).default([]),
862
+ mermaid: z.string().trim().min(1).max(12e3),
863
+ warnings: z.array(z.string().trim().min(1).max(600)).default([]),
864
+ freshness: projectContextFreshnessSchema.default("fresh")
865
+ });
866
+ var projectSystemDiagramGraphManifestSchema = z.object({
867
+ manifestId: z.string().trim().min(1).max(200),
868
+ layoutVersion: z.number().int().positive().default(1),
869
+ direction: projectSystemDiagramDirectionSchema.default("LR"),
870
+ nodes: z.array(projectSystemDiagramNodeSchema).min(1),
871
+ edges: z.array(projectSystemDiagramEdgeSchema).default([]),
872
+ groups: z.array(projectSystemDiagramGroupSchema).default([]),
873
+ views: z.array(projectSystemDiagramViewSchema).min(1),
874
+ citations: z.array(assistantCitationSchema).default([]),
875
+ unknowns: z.array(z.string().trim().min(1).max(300)).default([]),
876
+ warnings: z.array(z.string().trim().min(1).max(600)).default([])
877
+ });
878
+ var projectSystemDiagramItemSchema = baseItemSchema.extend({
879
+ type: z.literal("projectSystemDiagram"),
880
+ projectId: z.string().min(1),
881
+ projectSystemDiagramId: z.string().min(1),
882
+ version: z.number().int().positive(),
883
+ status: projectSystemDiagramStatusSchema,
884
+ source: sourceSchema.default("generated"),
885
+ repositoryLinkId: z.string().min(1).optional(),
886
+ projectContextMapId: z.string().min(1),
887
+ projectContextMapVersion: z.number().int().positive(),
888
+ sourceBrainRevision: z.number().int().nonnegative().optional(),
889
+ summary: z.string().trim().min(1).max(4e3),
890
+ graph: projectSystemDiagramGraphManifestSchema,
891
+ coverage: projectContextCoverageSchema.default(defaultProjectContextCoverage),
892
+ mermaid: z.string().trim().min(1).max(12e3),
893
+ approvedByUserId: z.string().min(1).optional(),
894
+ approvedAt: isoDateTimeSchema.optional(),
895
+ generatedAt: isoDateTimeSchema,
896
+ supersedesDiagramId: z.string().min(1).optional(),
897
+ error: z.string().max(1e3).optional()
898
+ });
899
+ var projectContextMapItemSchema = baseItemSchema.extend({
900
+ type: z.literal("projectContextMap"),
901
+ projectId: z.string().min(1),
902
+ projectContextMapId: z.string().min(1),
903
+ version: z.number().int().positive(),
904
+ status: projectContextMapStatusSchema,
905
+ source: sourceSchema.default("runner"),
906
+ repositoryLinkId: z.string().min(1).optional(),
907
+ refreshId: z.string().min(1).optional(),
908
+ summary: z.string().trim().min(1).max(4e3),
909
+ slices: z.array(projectContextSliceSchema).default([]),
910
+ entities: z.array(projectContextEntitySchema).default([]),
911
+ relations: z.array(projectContextRelationSchema).default([]),
912
+ coverage: projectContextCoverageSchema.default(defaultProjectContextCoverage),
913
+ approvedByUserId: z.string().min(1).optional(),
914
+ approvedAt: isoDateTimeSchema.optional(),
915
+ generatedAt: isoDateTimeSchema,
916
+ supersedesMapId: z.string().min(1).optional(),
917
+ error: z.string().max(1e3).optional()
918
+ });
919
+ var projectContextRefreshResultSchema = z.object({
920
+ summary: z.string().trim().min(1).max(4e3),
921
+ slices: z.array(projectContextSliceSchema).default([]),
922
+ entities: z.array(projectContextEntitySchema).default([]),
923
+ relations: z.array(projectContextRelationSchema).default([]),
924
+ coverage: projectContextCoverageSchema.default(defaultProjectContextCoverage),
925
+ changedSliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
926
+ staleSliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
927
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
928
+ warnings: z.array(z.string().trim().min(1).max(600)).default([])
929
+ });
930
+ var projectContextRefreshItemSchema = baseItemSchema.extend({
931
+ type: z.literal("projectContextRefresh"),
932
+ projectId: z.string().min(1),
933
+ projectContextRefreshId: z.string().min(1),
934
+ status: projectContextRefreshStatusSchema,
935
+ source: z.enum(["manual", "runner", "scheduled"]).default("manual"),
936
+ repositoryLinkId: z.string().min(1).optional(),
937
+ workItemId: z.string().min(1).optional(),
938
+ runnerId: z.string().min(1).optional(),
939
+ previousMapId: z.string().min(1).optional(),
940
+ proposedMapId: z.string().min(1).optional(),
941
+ summary: z.string().trim().min(1).max(4e3).optional(),
942
+ changedSliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
943
+ staleSliceIds: z.array(z.string().trim().min(1).max(160)).default([]),
944
+ startedAt: isoDateTimeSchema.optional(),
945
+ completedAt: isoDateTimeSchema.optional(),
946
+ failedAt: isoDateTimeSchema.optional(),
947
+ error: z.string().max(1e3).optional()
948
+ });
949
+ var projectContextPackSchema = z.object({
950
+ packId: z.string().min(1),
951
+ accountId: z.string().min(1),
952
+ projectId: z.string().min(1),
953
+ workItemId: z.string().min(1).optional(),
954
+ query: z.string().trim().min(1).max(2e3),
955
+ mapId: z.string().min(1).optional(),
956
+ mapVersion: z.number().int().positive().optional(),
957
+ slices: z.array(projectContextSliceSchema).default([]),
958
+ entities: z.array(projectContextEntitySchema).default([]),
959
+ relations: z.array(projectContextRelationSchema).default([]),
960
+ citations: z.array(assistantCitationSchema).default([]),
961
+ freshnessWarnings: z.array(z.string().trim().min(1).max(600)).default([]),
962
+ tokenEstimate: z.number().int().nonnegative().default(0),
963
+ generatedAt: isoDateTimeSchema
964
+ });
965
+ var contextMissItemSchema = baseItemSchema.extend({
966
+ type: z.literal("contextMiss"),
967
+ projectId: z.string().min(1),
968
+ contextMissId: z.string().min(1),
969
+ status: contextMissStatusSchema.default("open"),
970
+ source: z.enum(["runner", "web", "system"]).default("runner"),
971
+ workItemId: z.string().min(1).optional(),
972
+ projectContextMapId: z.string().min(1).optional(),
973
+ query: z.string().trim().min(1).max(2e3),
974
+ missingSurface: z.string().trim().min(1).max(200),
975
+ reason: z.string().trim().min(1).max(1200),
976
+ suggestedPaths: z.array(projectContextRepoPathSchema).default([]),
977
+ resolvedAt: isoDateTimeSchema.optional(),
978
+ dismissedAt: isoDateTimeSchema.optional()
979
+ });
706
980
  var impactAffectedAreaSchema = z.object({
707
981
  name: z.string().trim().min(1),
708
982
  description: z.string().trim().min(1).optional()
@@ -843,6 +1117,7 @@ var securityScanItemSchema = baseItemSchema.extend({
843
1117
  status: securityScanStatusSchema,
844
1118
  baselineVersion: z.string().trim().min(1).max(80).default("amistio-security-baseline-v1"),
845
1119
  workItemId: z.string().min(1).optional(),
1120
+ repositoryLinkId: z.string().min(1).optional(),
846
1121
  runnerId: z.string().min(1).optional(),
847
1122
  findingIds: z.array(z.string().min(1)).default([]),
848
1123
  summary: z.string().trim().min(1).max(2e3).optional(),
@@ -894,6 +1169,75 @@ var securityPostureSnapshotItemSchema = baseItemSchema.extend({
894
1169
  nextScheduledAt: isoDateTimeSchema.optional(),
895
1170
  summary: z.string().trim().min(1).max(2e3).optional()
896
1171
  });
1172
+ var appEvaluationFindingResultSchema = z.object({
1173
+ title: z.string().trim().min(1).max(200),
1174
+ category: appEvaluationFindingCategorySchema,
1175
+ severity: appEvaluationFindingSeveritySchema,
1176
+ confidence: appEvaluationFindingConfidenceSchema.default("medium"),
1177
+ summary: z.string().trim().min(1).max(2e3),
1178
+ affectedSurfaces: z.array(z.string().trim().min(1).max(300)).default([]),
1179
+ evidence: z.array(z.string().trim().min(1).max(600)).default([]),
1180
+ suggestedAction: z.string().trim().min(1).max(4e3),
1181
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
1182
+ safePaths: z.array(z.string().trim().min(1).max(300)).default([]),
1183
+ proposedLifecycleAction: appEvaluationPlanLifecycleActionSchema.default("none"),
1184
+ relatedArtifactIds: z.array(z.string().trim().min(1).max(160)).default([]),
1185
+ proposedPlanTitle: z.string().trim().min(1).max(200).optional(),
1186
+ proposedPlanRepoPath: z.string().trim().min(1).max(300).optional(),
1187
+ proposedPlanContent: z.string().trim().min(1).max(3e4).optional(),
1188
+ status: appEvaluationFindingStatusSchema.default("open")
1189
+ });
1190
+ var appEvaluationScanResultSchema = z.object({
1191
+ summary: z.string().trim().min(1).max(2e3),
1192
+ baselineVersion: z.string().trim().min(1).max(80),
1193
+ findings: z.array(appEvaluationFindingResultSchema).default([]),
1194
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1)
1195
+ });
1196
+ var appEvaluationScanItemSchema = baseItemSchema.extend({
1197
+ type: z.literal("appEvaluationScan"),
1198
+ projectId: z.string().min(1),
1199
+ appEvaluationScanId: z.string().min(1),
1200
+ hourBucket: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:00:00\.000Z$/),
1201
+ source: z.enum(["runner", "manual"]),
1202
+ status: appEvaluationScanStatusSchema,
1203
+ baselineVersion: z.string().trim().min(1).max(80).default("amistio-app-evaluation-v1"),
1204
+ workItemId: z.string().min(1).optional(),
1205
+ repositoryLinkId: z.string().min(1).optional(),
1206
+ runnerId: z.string().min(1).optional(),
1207
+ findingIds: z.array(z.string().min(1)).default([]),
1208
+ summary: z.string().trim().min(1).max(2e3).optional(),
1209
+ startedAt: isoDateTimeSchema.optional(),
1210
+ completedAt: isoDateTimeSchema.optional(),
1211
+ failedAt: isoDateTimeSchema.optional(),
1212
+ error: z.string().max(1e3).optional()
1213
+ });
1214
+ var appEvaluationFindingItemSchema = baseItemSchema.extend({
1215
+ type: z.literal("appEvaluationFinding"),
1216
+ projectId: z.string().min(1),
1217
+ appEvaluationFindingId: z.string().min(1),
1218
+ appEvaluationScanId: z.string().min(1),
1219
+ title: z.string().trim().min(1).max(200),
1220
+ category: appEvaluationFindingCategorySchema,
1221
+ severity: appEvaluationFindingSeveritySchema,
1222
+ confidence: appEvaluationFindingConfidenceSchema.default("medium"),
1223
+ status: appEvaluationFindingStatusSchema,
1224
+ approvalState: appEvaluationApprovalStateSchema.default("proposed"),
1225
+ summary: z.string().trim().min(1).max(2e3),
1226
+ affectedSurfaces: z.array(z.string().trim().min(1).max(300)).default([]),
1227
+ evidence: z.array(z.string().trim().min(1).max(600)).default([]),
1228
+ safePaths: z.array(z.string().trim().min(1).max(300)).default([]),
1229
+ suggestedAction: z.string().trim().min(1).max(4e3),
1230
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
1231
+ proposedLifecycleAction: appEvaluationPlanLifecycleActionSchema.default("none"),
1232
+ relatedArtifactIds: z.array(z.string().trim().min(1).max(160)).default([]),
1233
+ proposedPlanDocumentId: z.string().min(1).optional(),
1234
+ implementationWorkItemId: z.string().min(1).optional(),
1235
+ reviewNotes: z.string().trim().min(1).optional(),
1236
+ reviewedByUserId: z.string().min(1).optional(),
1237
+ reviewedAt: isoDateTimeSchema.optional(),
1238
+ resolvedAt: isoDateTimeSchema.optional(),
1239
+ lastReviewAction: z.enum(["approve", "requestChanges", "dismiss", "acceptRisk", "reopen"]).optional()
1240
+ });
897
1241
  var activityEventItemSchema = baseItemSchema.extend({
898
1242
  type: z.literal("activityEvent"),
899
1243
  projectId: z.string().min(1),
@@ -911,6 +1255,11 @@ var activityEventItemSchema = baseItemSchema.extend({
911
1255
  relatedIssueId: z.string().min(1).optional(),
912
1256
  relatedSecurityScanId: z.string().min(1).optional(),
913
1257
  relatedSecurityFindingId: z.string().min(1).optional(),
1258
+ relatedAppEvaluationScanId: z.string().min(1).optional(),
1259
+ relatedAppEvaluationFindingId: z.string().min(1).optional(),
1260
+ relatedProjectContextRefreshId: z.string().min(1).optional(),
1261
+ relatedProjectSystemDiagramId: z.string().min(1).optional(),
1262
+ relatedContextMissId: z.string().min(1).optional(),
914
1263
  generatedDraftId: z.string().min(1).optional(),
915
1264
  runnerId: z.string().min(1).optional(),
916
1265
  repositoryLinkId: z.string().min(1).optional(),
@@ -971,6 +1320,12 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
971
1320
  securityScanItemSchema,
972
1321
  securityFindingItemSchema,
973
1322
  securityPostureSnapshotItemSchema,
1323
+ appEvaluationScanItemSchema,
1324
+ appEvaluationFindingItemSchema,
1325
+ projectContextMapItemSchema,
1326
+ projectContextRefreshItemSchema,
1327
+ projectSystemDiagramItemSchema,
1328
+ contextMissItemSchema,
974
1329
  syncCursorItemSchema,
975
1330
  syncConflictItemSchema,
976
1331
  workItemSchema,
@@ -1010,16 +1365,43 @@ function isOfficialAmistioApiUrl(value) {
1010
1365
 
1011
1366
  // ../shared/src/brain-documents.ts
1012
1367
  var controlPlaneRoots = /* @__PURE__ */ new Set(["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"]);
1368
+ var documentTypeFolders = {
1369
+ architecture: "architecture",
1370
+ context: "context",
1371
+ decision: "decisions",
1372
+ feature: "features",
1373
+ memory: "memory",
1374
+ plan: "plans",
1375
+ prompt: "prompts",
1376
+ workflow: "workflows"
1377
+ };
1378
+ var documentTypeByFolder = Object.fromEntries(Object.entries(documentTypeFolders).map(([documentType, folder]) => [folder, documentType]));
1013
1379
  function isControlPlaneTemplateRepoPath(repoPath) {
1014
1380
  const normalized = normalizeRepoPath(repoPath);
1015
1381
  const segments = normalized.split("/").filter(Boolean);
1016
- const root = segments[0] === "docs" ? segments[1] : segments[0];
1382
+ const root = controlPlaneRootFromSegments(segments);
1017
1383
  const basename = segments[segments.length - 1]?.toLowerCase();
1018
1384
  return Boolean(root && controlPlaneRoots.has(root) && (basename === "_template.md" || basename === "_template.mdx"));
1019
1385
  }
1386
+ function documentTypeForBrainRepoPath(repoPath) {
1387
+ const normalized = normalizeRepoPath(repoPath);
1388
+ const segments = normalized.split("/").filter(Boolean);
1389
+ const root = controlPlaneRootFromSegments(segments);
1390
+ return root ? documentTypeByFolder[root] : void 0;
1391
+ }
1392
+ function inferContentFormatFromRepoPath(repoPath) {
1393
+ if (/\.html?$/i.test(repoPath)) return "html";
1394
+ if (/\.mdx?$/i.test(repoPath)) return "markdown";
1395
+ return void 0;
1396
+ }
1020
1397
  function normalizeRepoPath(repoPath) {
1021
1398
  return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
1022
1399
  }
1400
+ function controlPlaneRootFromSegments(segments) {
1401
+ if (segments[0] === "docs" && segments[1] === "html") return segments[2];
1402
+ if (segments[0] === "docs") return segments[1];
1403
+ return segments[0];
1404
+ }
1023
1405
 
1024
1406
  // ../shared/src/repo-metadata.ts
1025
1407
  import { z as z2 } from "zod";
@@ -1033,6 +1415,7 @@ var repoLinkMetadataSchema = z2.object({
1033
1415
  var syncedDocumentFrontmatterSchema = z2.object({
1034
1416
  amistioDocumentId: z2.string().min(1),
1035
1417
  amistioDocumentType: z2.string().min(1),
1418
+ amistioContentFormat: z2.enum(["markdown", "html"]).default("markdown"),
1036
1419
  amistioRevision: z2.coerce.number().int().nonnegative(),
1037
1420
  amistioContentHash: z2.string().min(1),
1038
1421
  status: z2.string().optional()
@@ -1183,6 +1566,7 @@ function decideSyncState(state) {
1183
1566
 
1184
1567
  // ../shared/src/next-action.ts
1185
1568
  var nextActionRunnerHeartbeatFreshMs = 15 * 60 * 1e3;
1569
+ var nextActionCompletedWorkFreshMs = 60 * 60 * 1e3;
1186
1570
  function computeProjectNextAction(input) {
1187
1571
  const nowMs = input.nowMs ?? Date.now();
1188
1572
  if (!input.projectId) {
@@ -1280,7 +1664,7 @@ function computeProjectNextAction(input) {
1280
1664
  if (!readiness.ready) {
1281
1665
  return runnerWaitAction(readiness, "Runner setup needed");
1282
1666
  }
1283
- const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed"));
1667
+ const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed" && isFreshCompletedWork(item, nowMs)));
1284
1668
  if (completedWork) {
1285
1669
  return workAction(completedWork, "workCompleted", "none", "success", "Work completed", completedWork.lastStatusMessage ?? "The latest runner work is complete.");
1286
1670
  }
@@ -1374,6 +1758,10 @@ function workAction(workItem, kind, actor, tone, title, message) {
1374
1758
  function latestWorkItem(workItems) {
1375
1759
  return [...workItems].sort((first, second) => Date.parse(second.updatedAt) - Date.parse(first.updatedAt))[0];
1376
1760
  }
1761
+ function isFreshCompletedWork(workItem, nowMs) {
1762
+ const timestamp = Date.parse(workItem.lastStatusAt ?? workItem.updatedAt);
1763
+ return timestamp > 0 && nowMs - timestamp <= nextActionCompletedWorkFreshMs;
1764
+ }
1377
1765
  function latestTimestamp(values) {
1378
1766
  return values.sort((first, second) => Date.parse(second) - Date.parse(first))[0];
1379
1767
  }
@@ -1715,6 +2103,27 @@ var ApiClient = class {
1715
2103
  { method: "GET" }
1716
2104
  );
1717
2105
  }
2106
+ async listProjectContext(projectId) {
2107
+ return this.request(
2108
+ `/projects/${projectId}/project-context`,
2109
+ z3.object({
2110
+ maps: z3.array(projectContextMapItemSchema),
2111
+ refreshes: z3.array(projectContextRefreshItemSchema),
2112
+ misses: z3.array(contextMissItemSchema),
2113
+ activeMap: projectContextMapItemSchema.optional()
2114
+ }),
2115
+ { method: "GET" }
2116
+ );
2117
+ }
2118
+ async buildProjectContextPack(projectId, query, workItemId) {
2119
+ const params = new URLSearchParams({ packQuery: query });
2120
+ if (workItemId) params.set("workItemId", workItemId);
2121
+ return this.request(
2122
+ `/projects/${projectId}/project-context?${params.toString()}`,
2123
+ projectContextPackSchema,
2124
+ { method: "GET" }
2125
+ );
2126
+ }
1718
2127
  async pushBrainDocuments(projectId, documents) {
1719
2128
  return this.request(
1720
2129
  `/projects/${projectId}/brain-documents`,
@@ -1892,6 +2301,26 @@ var ApiClient = class {
1892
2301
  }
1893
2302
  );
1894
2303
  }
2304
+ async submitAppEvaluationScanResult(projectId, workItemId, result) {
2305
+ return this.request(
2306
+ `/projects/${projectId}/work-items/${workItemId}/app-evaluation-result`,
2307
+ z3.object({ scan: appEvaluationScanItemSchema, findings: z3.array(appEvaluationFindingItemSchema), workItem: workItemSchema }),
2308
+ {
2309
+ method: "POST",
2310
+ body: JSON.stringify(result)
2311
+ }
2312
+ );
2313
+ }
2314
+ async submitProjectContextRefreshResult(projectId, workItemId, result) {
2315
+ return this.request(
2316
+ `/projects/${projectId}/work-items/${workItemId}/project-context-result`,
2317
+ z3.object({ refresh: projectContextRefreshItemSchema, map: projectContextMapItemSchema.optional(), workItem: workItemSchema }),
2318
+ {
2319
+ method: "POST",
2320
+ body: JSON.stringify(result)
2321
+ }
2322
+ );
2323
+ }
1895
2324
  async request(urlPath, schema, init) {
1896
2325
  const response = await fetch(resolveApiUrl(this.options.apiUrl, urlPath), {
1897
2326
  ...init,
@@ -1938,8 +2367,8 @@ var toolSessionMutationSchema = z3.object({
1938
2367
  });
1939
2368
  function resolveApiUrl(apiUrl, urlPath) {
1940
2369
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
1941
- const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1942
- return new URL(`${base}${path14}`);
2370
+ const path15 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
2371
+ return new URL(`${base}${path15}`);
1943
2372
  }
1944
2373
 
1945
2374
  // src/orchestrator.ts
@@ -2831,6 +3260,15 @@ function currentRunnerMode() {
2831
3260
  function defaultRunnerMetadataDir() {
2832
3261
  return path6.join(os3.homedir(), ".config", "amistio", "runners");
2833
3262
  }
3263
+ function updatedCliRunnerLaunchOptions() {
3264
+ return { executablePath: "amistio", directExecutable: true };
3265
+ }
3266
+ function resolveRunnerDaemonLaunch(input) {
3267
+ if (input.directExecutable) {
3268
+ return { executablePath: input.executablePath ?? "amistio", args: input.args };
3269
+ }
3270
+ return { executablePath: input.executablePath ?? process.execPath, args: [input.scriptPath ?? process.argv[1], ...input.args] };
3271
+ }
2834
3272
  async function startRunnerDaemon(input) {
2835
3273
  const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
2836
3274
  const existing = await readRunnerDaemonMetadata(input, metadataDir);
@@ -2840,7 +3278,12 @@ async function startRunnerDaemon(input) {
2840
3278
  await mkdir5(metadataDir, { recursive: true });
2841
3279
  const logPath = path6.join(metadataDir, `${runnerDaemonKey(input)}.log`);
2842
3280
  const logFd = openSync(logPath, "a");
2843
- const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...input.args], {
3281
+ const launch = resolveRunnerDaemonLaunch({
3282
+ args: input.args,
3283
+ ...input.executablePath ? { executablePath: input.executablePath } : {},
3284
+ ...input.scriptPath ? { scriptPath: input.scriptPath } : {}
3285
+ });
3286
+ const child = spawn2(launch.executablePath, launch.args, {
2844
3287
  cwd: input.rootDir,
2845
3288
  detached: true,
2846
3289
  env: {
@@ -2877,7 +3320,13 @@ async function restartRunnerDaemonProcess(metadata, args, input = {}) {
2877
3320
  await mkdir5(metadataDir, { recursive: true });
2878
3321
  const logPath = metadata.logPath ?? path6.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
2879
3322
  const logFd = openSync(logPath, "a");
2880
- const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...args], {
3323
+ const launch = resolveRunnerDaemonLaunch({
3324
+ args,
3325
+ ...input.executablePath ? { executablePath: input.executablePath } : {},
3326
+ ...input.scriptPath ? { scriptPath: input.scriptPath } : {},
3327
+ ...input.directExecutable ? { directExecutable: true } : {}
3328
+ });
3329
+ const child = spawn2(launch.executablePath, launch.args, {
2881
3330
  cwd: metadata.rootDir,
2882
3331
  detached: true,
2883
3332
  env: {
@@ -3356,6 +3805,7 @@ import { promisify as promisify3 } from "node:util";
3356
3805
  var execFileAsync3 = promisify3(execFile3);
3357
3806
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
3358
3807
  var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
3808
+ var htmlSyncRoot = "docs/html";
3359
3809
  var documentTypeByRoot = {
3360
3810
  architecture: "architecture",
3361
3811
  context: "context",
@@ -3443,15 +3893,15 @@ async function collectSyncStatus(rootDir, webDocuments = []) {
3443
3893
  return { status, items, counts };
3444
3894
  }
3445
3895
  async function readLocalSyncedDocuments(rootDir) {
3446
- const markdownFiles = await findMarkdownFiles(rootDir);
3896
+ const documentFiles = await findBrainDocumentFiles(rootDir);
3447
3897
  const documents = [];
3448
- for (const fullPath of markdownFiles) {
3898
+ for (const fullPath of documentFiles) {
3449
3899
  const raw = await readFile6(fullPath, "utf8");
3450
- const parsed = parseSyncedMarkdown(raw);
3900
+ const repoPath = toRepoPath(rootDir, fullPath);
3901
+ const parsed = parseSyncedDocument(raw, repoPath);
3451
3902
  if (!parsed) {
3452
3903
  continue;
3453
3904
  }
3454
- const repoPath = toRepoPath(rootDir, fullPath);
3455
3905
  if (isControlPlaneTemplateRepoPath(repoPath)) {
3456
3906
  continue;
3457
3907
  }
@@ -3497,7 +3947,7 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
3497
3947
  continue;
3498
3948
  }
3499
3949
  await mkdir7(path8.dirname(fullPath), { recursive: true });
3500
- await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
3950
+ await writeFile7(fullPath, createSyncedDocumentContent(document), "utf8");
3501
3951
  result.written.push(document.repoPath);
3502
3952
  }
3503
3953
  return result;
@@ -3515,6 +3965,7 @@ async function collectDirtyDocumentsForPush(rootDir, metadata) {
3515
3965
  projectId: metadata.amistioProjectId,
3516
3966
  documentId: document.frontmatter.amistioDocumentId,
3517
3967
  documentType,
3968
+ contentFormat: document.frontmatter.amistioContentFormat,
3518
3969
  title: inferTitle(document.content, document.repoPath),
3519
3970
  status: "reviewing",
3520
3971
  repoPath: document.repoPath,
@@ -3552,11 +4003,15 @@ function autoSyncSkipCounts(skipped) {
3552
4003
  unreadable: skipped.filter((item) => item.reason === "unreadable").length
3553
4004
  };
3554
4005
  }
3555
- function createSyncedDocumentMarkdown(document) {
4006
+ function createSyncedDocumentContent(document) {
4007
+ return (document.contentFormat ?? inferContentFormatFromRepoPath(document.repoPath) ?? "markdown") === "html" ? createSyncedDocumentHtml(document) : createSyncedDocumentMarkdownContent(document);
4008
+ }
4009
+ function createSyncedDocumentMarkdownContent(document) {
3556
4010
  return [
3557
4011
  "---",
3558
4012
  `amistioDocumentId: ${document.documentId}`,
3559
4013
  `amistioDocumentType: ${document.documentType}`,
4014
+ `amistioContentFormat: ${document.contentFormat ?? "markdown"}`,
3560
4015
  `amistioRevision: ${document.revision}`,
3561
4016
  `amistioContentHash: ${document.contentHash}`,
3562
4017
  `status: ${document.status}`,
@@ -3565,6 +4020,24 @@ function createSyncedDocumentMarkdown(document) {
3565
4020
  ""
3566
4021
  ].join("\n");
3567
4022
  }
4023
+ function createSyncedDocumentHtml(document) {
4024
+ return [
4025
+ "<!--",
4026
+ `amistioDocumentId: ${document.documentId}`,
4027
+ `amistioDocumentType: ${document.documentType}`,
4028
+ "amistioContentFormat: html",
4029
+ `amistioRevision: ${document.revision}`,
4030
+ `amistioContentHash: ${document.contentHash}`,
4031
+ `status: ${document.status}`,
4032
+ "-->",
4033
+ document.content,
4034
+ ""
4035
+ ].join("\n");
4036
+ }
4037
+ function parseSyncedDocument(content, repoPath) {
4038
+ const format = inferContentFormatFromRepoPath(repoPath);
4039
+ return format === "html" ? parseSyncedHtml(content) : parseSyncedMarkdown(content);
4040
+ }
3568
4041
  function parseSyncedMarkdown(content) {
3569
4042
  const rawFrontmatter = parseFrontmatter(content);
3570
4043
  const frontmatter = syncedDocumentFrontmatterSchema.safeParse(rawFrontmatter);
@@ -3579,10 +4052,32 @@ function parseSyncedMarkdown(content) {
3579
4052
  const bodyStart = closingLineEnd === -1 ? content.length : closingLineEnd + 1;
3580
4053
  return { frontmatter: frontmatter.data, content: content.slice(bodyStart).replace(/\n$/, "") };
3581
4054
  }
4055
+ function parseSyncedHtml(content) {
4056
+ const trimmedStart = content.trimStart();
4057
+ if (!trimmedStart.startsWith("<!--")) {
4058
+ return void 0;
4059
+ }
4060
+ const startOffset = content.indexOf("<!--");
4061
+ const closingMarker = content.indexOf("-->", startOffset + 4);
4062
+ if (closingMarker === -1) {
4063
+ return void 0;
4064
+ }
4065
+ const metadataLines = content.slice(startOffset + 4, closingMarker).split(/\r?\n/);
4066
+ const metadata = Object.fromEntries(metadataLines.map((line) => {
4067
+ const separator = line.indexOf(":");
4068
+ return separator === -1 ? void 0 : [line.slice(0, separator).trim(), line.slice(separator + 1).trim()];
4069
+ }).filter((entry) => Boolean(entry?.[0])));
4070
+ const frontmatter = syncedDocumentFrontmatterSchema.safeParse({ ...metadata, amistioContentFormat: "html" });
4071
+ if (!frontmatter.success) {
4072
+ return void 0;
4073
+ }
4074
+ const bodyStart = content.indexOf("\n", closingMarker + 3);
4075
+ return { frontmatter: frontmatter.data, content: content.slice(bodyStart === -1 ? closingMarker + 3 : bodyStart + 1).replace(/\n$/, "") };
4076
+ }
3582
4077
  async function readExistingSyncedDocument(fullPath) {
3583
4078
  try {
3584
4079
  const raw = await readFile6(fullPath, "utf8");
3585
- const parsed = parseSyncedMarkdown(raw);
4080
+ const parsed = parseSyncedDocument(raw, fullPath);
3586
4081
  if (!parsed) {
3587
4082
  return { exists: true };
3588
4083
  }
@@ -3603,23 +4098,23 @@ async function readExistingSyncedDocument(fullPath) {
3603
4098
  throw error;
3604
4099
  }
3605
4100
  }
3606
- async function findMarkdownFiles(rootDir) {
4101
+ async function findBrainDocumentFiles(rootDir) {
3607
4102
  const files = [];
3608
- for (const syncRoot of syncRoots) {
4103
+ for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
3609
4104
  const fullRoot = path8.join(rootDir, syncRoot);
3610
4105
  if (!await exists2(fullRoot)) {
3611
4106
  continue;
3612
4107
  }
3613
- await walkMarkdownFiles(fullRoot, files);
4108
+ await walkBrainDocumentFiles(fullRoot, files);
3614
4109
  }
3615
4110
  return files;
3616
4111
  }
3617
- async function walkMarkdownFiles(directory, files) {
4112
+ async function walkBrainDocumentFiles(directory, files) {
3618
4113
  for (const entry of await readdir3(directory, { withFileTypes: true })) {
3619
4114
  const fullPath = path8.join(directory, entry.name);
3620
4115
  if (entry.isDirectory()) {
3621
- await walkMarkdownFiles(fullPath, files);
3622
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
4116
+ await walkBrainDocumentFiles(fullPath, files);
4117
+ } else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
3623
4118
  files.push(fullPath);
3624
4119
  }
3625
4120
  }
@@ -3641,7 +4136,7 @@ function safeRepoPath(rootDir, repoPath) {
3641
4136
  }
3642
4137
  function isControlPlanePath(repoPath) {
3643
4138
  const normalized = path8.normalize(repoPath);
3644
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
4139
+ return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path8.sep}`);
3645
4140
  }
3646
4141
  function canonicalControlPlaneRepoPath(repoPath) {
3647
4142
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -3656,7 +4151,9 @@ function toRepoPath(rootDir, fullPath) {
3656
4151
  }
3657
4152
  function inferTitle(content, repoPath) {
3658
4153
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
3659
- return heading || path8.basename(repoPath, path8.extname(repoPath));
4154
+ if (heading) return heading;
4155
+ const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
4156
+ return htmlHeading || path8.basename(repoPath, path8.extname(repoPath));
3660
4157
  }
3661
4158
  async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
3662
4159
  const root = path8.resolve(rootDir);
@@ -3689,7 +4186,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
3689
4186
  skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3690
4187
  continue;
3691
4188
  }
3692
- if (parseSyncedMarkdown(content)) {
4189
+ if (parseSyncedDocument(content, normalizedRepoPath)) {
3693
4190
  skipped.push({ repoPath: normalizedRepoPath, reason: "alreadyManaged" });
3694
4191
  continue;
3695
4192
  }
@@ -3719,6 +4216,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
3719
4216
  projectId: metadata.amistioProjectId,
3720
4217
  documentId,
3721
4218
  documentType,
4219
+ contentFormat: inferContentFormatFromRepoPath(canonicalRepoPath) ?? "markdown",
3722
4220
  title: inferTitle(content, canonicalRepoPath),
3723
4221
  status: "reviewing",
3724
4222
  repoPath: canonicalRepoPath,
@@ -3747,7 +4245,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
3747
4245
  return uniqueSortedRepoPaths(gitFiles.split("\n").map(normalizeRepoPath3).filter((repoPath) => repoPath && isRecognizedBrainRepoPath(repoPath)));
3748
4246
  }
3749
4247
  const files = [];
3750
- for (const syncRoot of [...syncRoots, ...legacySyncRoots]) {
4248
+ for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
3751
4249
  const fullRoot = path8.join(rootDir, syncRoot);
3752
4250
  if (await exists2(fullRoot)) {
3753
4251
  await walkAutoSyncFiles(rootDir, fullRoot, files);
@@ -3771,7 +4269,7 @@ async function walkAutoSyncFiles(rootDir, directory, files) {
3771
4269
  function autoSyncPathSkipReason(repoPath) {
3772
4270
  if (autoSyncMetadataPaths.has(repoPath)) return "metadata";
3773
4271
  if (isControlPlaneTemplateRepoPath(repoPath)) return "template";
3774
- if (!/\.(md|mdx)$/i.test(repoPath)) return "unsupported";
4272
+ if (!/\.(md|mdx|html?)$/i.test(repoPath)) return "unsupported";
3775
4273
  const basename = repoPath.split("/").at(-1)?.toLowerCase() ?? "";
3776
4274
  if (basename.startsWith(".") || basename.endsWith(".lock") || basename.includes(".env") || basename.includes("secret") || basename.includes("credential")) return "excluded";
3777
4275
  if (repoPath.split("/").some((segment) => autoSyncExcludedDirectoryNames.has(segment) || autoSyncGeneratedPathSegments.has(segment))) return "excluded";
@@ -3779,10 +4277,13 @@ function autoSyncPathSkipReason(repoPath) {
3779
4277
  }
3780
4278
  function isRecognizedBrainRepoPath(repoPath) {
3781
4279
  const normalized = normalizeRepoPath3(repoPath);
3782
- const [firstSegment, secondSegment] = normalized.split("/");
3783
- return Boolean(firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
4280
+ const [firstSegment, secondSegment, thirdSegment] = normalized.split("/");
4281
+ return Boolean(firstSegment === "docs" && secondSegment === "html" && thirdSegment && legacySyncRoots.includes(thirdSegment) || firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
3784
4282
  }
3785
4283
  function documentTypeForRepoPath(repoPath) {
4284
+ return documentTypeForBrainRepoPath(repoPath) ?? legacyDocumentTypeForRepoPath(repoPath);
4285
+ }
4286
+ function legacyDocumentTypeForRepoPath(repoPath) {
3786
4287
  const normalized = normalizeRepoPath3(repoPath);
3787
4288
  const segments = normalized.split("/");
3788
4289
  const root = segments[0] === "docs" ? segments[1] : segments[0];
@@ -3801,6 +4302,7 @@ function parseFrontmatterFromSyncedDocument(frontmatter) {
3801
4302
  return {
3802
4303
  amistioDocumentId: frontmatter.amistioDocumentId,
3803
4304
  amistioDocumentType: frontmatter.amistioDocumentType,
4305
+ amistioContentFormat: frontmatter.amistioContentFormat,
3804
4306
  amistioRevision: frontmatter.amistioRevision,
3805
4307
  amistioContentHash: frontmatter.amistioContentHash,
3806
4308
  ...frontmatter.status ? { status: frontmatter.status } : {}
@@ -3864,6 +4366,10 @@ var issueDiagnosisStart = "AMISTIO_ISSUE_DIAGNOSIS_START";
3864
4366
  var issueDiagnosisEnd = "AMISTIO_ISSUE_DIAGNOSIS_END";
3865
4367
  var securityPostureStart = "AMISTIO_SECURITY_POSTURE_START";
3866
4368
  var securityPostureEnd = "AMISTIO_SECURITY_POSTURE_END";
4369
+ var appEvaluationStart = "AMISTIO_APP_EVALUATION_START";
4370
+ var appEvaluationEnd = "AMISTIO_APP_EVALUATION_END";
4371
+ var projectContextRefreshStart = "AMISTIO_PROJECT_CONTEXT_REFRESH_START";
4372
+ var projectContextRefreshEnd = "AMISTIO_PROJECT_CONTEXT_REFRESH_END";
3867
4373
  function createWorkExecutionPrompt(workItem, context) {
3868
4374
  if (workItem.workKind === "brainGeneration") {
3869
4375
  return createBrainGenerationPrompt(workItem);
@@ -3883,6 +4389,12 @@ function createWorkExecutionPrompt(workItem, context) {
3883
4389
  if (workItem.workKind === "securityPostureScan") {
3884
4390
  return createSecurityPostureScanPrompt(workItem, context?.securityPostureScan);
3885
4391
  }
4392
+ if (workItem.workKind === "appEvaluationScan") {
4393
+ return createAppEvaluationScanPrompt(workItem, context?.appEvaluationScan);
4394
+ }
4395
+ if (workItem.workKind === "projectContextRefresh") {
4396
+ return createProjectContextRefreshPrompt(workItem, context?.projectContextRefresh);
4397
+ }
3886
4398
  return [
3887
4399
  "# Amistio Work Execution",
3888
4400
  "",
@@ -3911,6 +4423,77 @@ function createWorkExecutionPrompt(workItem, context) {
3911
4423
  "- Run relevant verification commands when feasible and summarize results."
3912
4424
  ].join("\n");
3913
4425
  }
4426
+ function createProjectContextRefreshPrompt(workItem, context) {
4427
+ const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 24).map((document) => [
4428
+ `### ${document.title}`,
4429
+ `documentId: ${document.documentId}`,
4430
+ `documentType: ${document.documentType}`,
4431
+ `repoPath: ${document.repoPath}`,
4432
+ `revision: ${document.revision}`,
4433
+ document.content.slice(0, 3e3)
4434
+ ].join("\n")).join("\n\n");
4435
+ const activeMap = context?.activeMap;
4436
+ const previousMap = activeMap ? [
4437
+ `Map ID: ${activeMap.projectContextMapId}`,
4438
+ `Version: ${activeMap.version}`,
4439
+ `Status: ${activeMap.status}`,
4440
+ `Coverage: ${activeMap.coverage.status}`,
4441
+ `Summary: ${activeMap.summary}`,
4442
+ "Slices:",
4443
+ ...activeMap.slices.slice(0, 20).map((slice) => `- ${slice.sliceId} / ${slice.kind} / ${slice.freshness}: ${slice.title} - ${slice.summary.slice(0, 500)}`)
4444
+ ].join("\n") : "No approved project context map was loaded. Build the initial map.";
4445
+ return [
4446
+ "# Amistio Living Project Context Refresh",
4447
+ "",
4448
+ "You are running locally through the Amistio CLI inside the user's repository.",
4449
+ "Run a read-only project context refresh that captures the whole app as bounded, reviewable knowledge.",
4450
+ "Do not modify files, create branches, commit, install packages, run implementation prompts, call external services, or make unaudited network calls.",
4451
+ "Use only safe local inspection and focused diagnostic commands that do not expose secrets.",
4452
+ "",
4453
+ "## Work Item",
4454
+ "",
4455
+ `Title: ${workItem.title}`,
4456
+ `Work item ID: ${workItem.workItemId}`,
4457
+ `Project ID: ${workItem.projectId}`,
4458
+ `Project context refresh ID: ${workItem.projectContextRefreshId ?? "unknown"}`,
4459
+ "",
4460
+ "## Refresh Request",
4461
+ "",
4462
+ workItem.sourceWish ?? "Refresh the living project context map.",
4463
+ "",
4464
+ "## Approved Project Brain Context",
4465
+ "",
4466
+ approvedContext || "No approved project-brain records were loaded. Inspect local repository files as needed and explain the gap in the coverage warnings.",
4467
+ "",
4468
+ "## Previous Context Map",
4469
+ "",
4470
+ previousMap,
4471
+ "",
4472
+ "## Mapping Requirements",
4473
+ "",
4474
+ "- Create slices for architecture, domain, data, API, frontend, backend, CLI, workflows, operations, security, and testing when those surfaces exist.",
4475
+ "- Capture entities and relations that explain how the app is put together and where future work should look first.",
4476
+ "- Prefer summaries, repository-relative paths, short citations, tags, and freshness status over raw source excerpts.",
4477
+ "- Mark stale or missing areas explicitly instead of guessing.",
4478
+ "- Keep repoPaths repository-relative; never include /absolute paths or ../ traversal.",
4479
+ "",
4480
+ "## Data Safety",
4481
+ "",
4482
+ "- Persist summaries, entities, relations, citations, and repository-relative paths only.",
4483
+ "- Do not include raw source dumps, secrets, env vars, process lists, absolute local paths, credential values, tokens, provider sessions, or destructive shell output.",
4484
+ "- Citation excerpts must be short evidence snippets, not code payloads.",
4485
+ "",
4486
+ "## Output Contract",
4487
+ "",
4488
+ "Print exactly one JSON object between the markers below. The CLI will submit only this structured refresh result back to Amistio.",
4489
+ "",
4490
+ projectContextRefreshStart,
4491
+ '{"summary":"The app uses docs/ as the orchestration control plane and a local runner for source-aware work.","slices":[{"sliceId":"slice_control_plane","kind":"architecture","title":"Docs control plane","summary":"ADRs, plans, prompts, context, and memory under docs/ guide implementation work.","repoPaths":["AGENTS.md","docs/architecture/overview.md"],"tags":["orchestrator","docs"],"citations":[{"source":"localSource","repoPath":"AGENTS.md","excerpt":"docs/ control-plane folders are the source of truth."}],"freshness":"fresh","confidence":0.9}],"entities":[{"entityId":"entity_local_runner","name":"Local runner","entityType":"component","summary":"Executes approved work locally and returns redacted structured results.","sliceIds":["slice_control_plane"],"confidence":0.8}],"relations":[{"relationId":"rel_runner_uses_docs","relationType":"uses","fromId":"entity_local_runner","toId":"slice_control_plane","summary":"The runner reads approved docs context before executing work.","confidence":0.8}],"coverage":{"status":"partial","sliceCount":1,"staleSliceCount":0,"missingAreas":["runtime data flow"],"warnings":[]},"changedSliceIds":["slice_control_plane"],"staleSliceIds":[],"verificationPlan":["Review the proposed context map in Amistio","Run focused checks for any stale areas"]}',
4492
+ projectContextRefreshEnd,
4493
+ "",
4494
+ "Do not put Markdown fences around the markers. Do not implement any changes."
4495
+ ].join("\n");
4496
+ }
3914
4497
  function createSecurityPostureScanPrompt(workItem, context) {
3915
4498
  const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 20).map((document) => [
3916
4499
  `### ${document.title}`,
@@ -3968,6 +4551,63 @@ function createSecurityPostureScanPrompt(workItem, context) {
3968
4551
  "Do not put Markdown fences around the markers. Do not implement remediation."
3969
4552
  ].join("\n");
3970
4553
  }
4554
+ function createAppEvaluationScanPrompt(workItem, context) {
4555
+ const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 24).map((document) => [
4556
+ `### ${document.title}`,
4557
+ `documentId: ${document.documentId}`,
4558
+ `documentType: ${document.documentType}`,
4559
+ `repoPath: ${document.repoPath}`,
4560
+ `revision: ${document.revision}`,
4561
+ document.content.slice(0, 3e3)
4562
+ ].join("\n")).join("\n\n");
4563
+ return [
4564
+ "# Amistio App Evaluation Scan",
4565
+ "",
4566
+ "You are running locally through the Amistio CLI inside the user's repository.",
4567
+ "Run a read-only app evaluation scan and produce approval-gated improvement and cleanup findings.",
4568
+ "Do not modify files, create branches, commit, install packages, run implementation prompts, delete or archive plans, call external services, or make unaudited network calls.",
4569
+ "Use only safe local inspection and focused diagnostic commands that do not expose secrets.",
4570
+ "",
4571
+ "## Work Item",
4572
+ "",
4573
+ `Title: ${workItem.title}`,
4574
+ `Work item ID: ${workItem.workItemId}`,
4575
+ `Project ID: ${workItem.projectId}`,
4576
+ `App evaluation scan ID: ${workItem.appEvaluationScanId ?? "unknown"}`,
4577
+ "",
4578
+ "## Scan Request",
4579
+ "",
4580
+ workItem.sourceWish ?? "Run the Amistio app evaluation baseline.",
4581
+ "",
4582
+ "## Approved Project Brain Context",
4583
+ "",
4584
+ approvedContext || "No approved project-brain records were loaded. Inspect local repository files as needed and explain the gap in the summary.",
4585
+ "",
4586
+ "## Baseline Requirements",
4587
+ "",
4588
+ "- Check app verification health: lint, typecheck, test, build, CI references, flaky or missing gates.",
4589
+ "- Check product and docs drift between approved context, plans, prompts, and current implementation.",
4590
+ "- Check old plans and prompts for cleanup candidates, but only propose lifecycle actions such as markCompleted, markSuperseded, archive, keepActive, or none.",
4591
+ "- Check missing memory or workflow updates when repeated lessons or operational rules are visible.",
4592
+ "- Check release readiness, UX, accessibility, performance, reliability, and security-posture follow-through at a summary level.",
4593
+ "",
4594
+ "## Data Safety",
4595
+ "",
4596
+ "- Persist summaries, recommended actions, repository-relative paths, and redacted evidence only.",
4597
+ "- Do not include raw source dumps, secrets, env vars, process lists, absolute local paths, credential values, tokens, provider sessions, or destructive shell output.",
4598
+ "- Use safePaths for repo-relative paths only; never include /absolute paths or ../ traversal.",
4599
+ "",
4600
+ "## Output Contract",
4601
+ "",
4602
+ "Print exactly one JSON object between the markers below. The CLI will submit only this structured scan result back to Amistio.",
4603
+ "",
4604
+ appEvaluationStart,
4605
+ '{"summary":"The app is generally healthy, but one release-readiness plan needs cleanup and verification evidence should be refreshed.","baselineVersion":"amistio-app-evaluation-v1","findings":[{"title":"Stale release plan should be marked superseded","category":"planCleanup","severity":"low","confidence":"high","summary":"A release plan appears to describe a workflow that has since been replaced by a newer accepted plan.","affectedSurfaces":["docs/plans"],"evidence":["A newer plan references the same scope and status, while the older plan remains proposed."],"suggestedAction":"Prepare a cleanup update that marks the stale plan superseded and links to the newer plan.","verificationPlan":["Review both plan files","Confirm the newer plan is accepted before changing lifecycle state"],"safePaths":["docs/plans/PLAN-example.md"],"proposedLifecycleAction":"markSuperseded","relatedArtifactIds":[]}],"verificationPlan":["Review App Evaluation findings for approval","Run the canonical verify gate before implementing approved actions"]}',
4606
+ appEvaluationEnd,
4607
+ "",
4608
+ "Do not put Markdown fences around the markers. Do not implement improvements or cleanup."
4609
+ ].join("\n");
4610
+ }
3971
4611
  function createIssueDiagnosisPrompt(workItem, context) {
3972
4612
  const issue = context?.issue;
3973
4613
  const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
@@ -4018,8 +4658,66 @@ function createIssueDiagnosisPrompt(workItem, context) {
4018
4658
  "Do not put Markdown fences around the markers. Do not implement the fix."
4019
4659
  ].join("\n");
4020
4660
  }
4661
+ function formatProjectContextMap(map, query, contextPack) {
4662
+ if ((!map || map.status !== "approved") && !contextPack) {
4663
+ return "No approved living context map was loaded. Use approved brain records and inspect local source only where needed.";
4664
+ }
4665
+ if (!map || map.status !== "approved") {
4666
+ return [
4667
+ `Context pack ID: ${contextPack?.packId ?? "unknown"}`,
4668
+ contextPack?.freshnessWarnings.length ? `Freshness warnings: ${contextPack.freshnessWarnings.join(" | ")}` : "Freshness warnings: none recorded",
4669
+ "No approved living context map was loaded. Use approved brain records and inspect local source only where needed."
4670
+ ].join("\n");
4671
+ }
4672
+ const tokens2 = tokenizeForContext(query);
4673
+ const candidateSlices = contextPack?.slices.length ? contextPack.slices : map.slices;
4674
+ const rankedSlices = [...candidateSlices].map((slice) => ({ slice, score: scoreContextSlice(tokens2, slice.title, slice.summary, [...slice.repoPaths, ...slice.tags, ...slice.ownerHints].join(" ")) })).sort((first, second) => second.score - first.score || first.slice.title.localeCompare(second.slice.title));
4675
+ const selectedSlices = rankedSlices.filter((entry) => entry.score > 0).map((entry) => entry.slice).slice(0, 8);
4676
+ const slices = (selectedSlices.length ? selectedSlices : candidateSlices.slice(0, 8)).map((slice) => [
4677
+ `### ${slice.title}`,
4678
+ `sliceId: ${slice.sliceId}`,
4679
+ `kind: ${slice.kind}`,
4680
+ `freshness: ${slice.freshness}`,
4681
+ `confidence: ${Math.round(slice.confidence * 100)}%`,
4682
+ slice.repoPaths.length ? `repoPaths: ${slice.repoPaths.slice(0, 12).join(", ")}` : "repoPaths: none recorded",
4683
+ slice.summary
4684
+ ].join("\n")).join("\n\n");
4685
+ const entities = (contextPack?.entities.length ? contextPack.entities : map.entities).slice(0, 12).map((entity) => `- ${entity.name} (${entity.entityType}): ${entity.summary ?? entity.entityId}`).join("\n");
4686
+ const relations = (contextPack?.relations.length ? contextPack.relations : map.relations).slice(0, 12).map((relation) => `- ${relation.fromId} ${relation.relationType} ${relation.toId}: ${relation.summary}`).join("\n");
4687
+ return [
4688
+ contextPack ? `Context pack ID: ${contextPack.packId}` : "Context pack ID: not recorded",
4689
+ `Map ID: ${map.projectContextMapId}`,
4690
+ `Version: ${map.version}`,
4691
+ contextPack?.tokenEstimate !== void 0 ? `Pack token estimate: ${contextPack.tokenEstimate}` : "Pack token estimate: not recorded",
4692
+ `Coverage: ${map.coverage.status}; slices ${map.coverage.sliceCount}; stale ${map.coverage.staleSliceCount}`,
4693
+ contextPack?.freshnessWarnings.length ? `Freshness warnings: ${contextPack.freshnessWarnings.join(" | ")}` : "Freshness warnings: none recorded",
4694
+ map.coverage.missingAreas.length ? `Missing areas: ${map.coverage.missingAreas.join(", ")}` : "Missing areas: none recorded",
4695
+ map.coverage.warnings.length ? `Warnings: ${map.coverage.warnings.join(" | ")}` : "Warnings: none recorded",
4696
+ "",
4697
+ map.summary,
4698
+ "",
4699
+ "### Relevant Context Slices",
4700
+ slices || "No slices recorded.",
4701
+ "",
4702
+ "### Entities",
4703
+ entities || "No entities recorded.",
4704
+ "",
4705
+ "### Relations",
4706
+ relations || "No relations recorded."
4707
+ ].join("\n");
4708
+ }
4709
+ function tokenizeForContext(value) {
4710
+ return [...new Set(value.toLowerCase().match(/[a-z0-9][a-z0-9_-]{2,}/g) ?? [])].slice(0, 24);
4711
+ }
4712
+ function scoreContextSlice(tokens2, title, summary, metadata) {
4713
+ const haystackTitle = title.toLowerCase();
4714
+ const haystackSummary = summary.toLowerCase();
4715
+ const haystackMetadata = metadata.toLowerCase();
4716
+ return tokens2.reduce((score, token) => score + (haystackTitle.includes(token) ? 5 : 0) + (haystackMetadata.includes(token) ? 3 : 0) + (haystackSummary.includes(token) ? 1 : 0), 0);
4717
+ }
4021
4718
  function createImpactPreviewPrompt(workItem, context) {
4022
4719
  const implementationPrompt = context?.implementationPrompt;
4720
+ const livingContext = formatProjectContextMap(context?.activeMap, workItem.sourceWish ?? workItem.title, context?.contextPack);
4023
4721
  const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
4024
4722
  `### ${document.title}`,
4025
4723
  `documentId: ${document.documentId}`,
@@ -4048,6 +4746,10 @@ function createImpactPreviewPrompt(workItem, context) {
4048
4746
  "",
4049
4747
  implementationPrompt ? implementationPrompt.content : workItem.sourceWish ?? workItem.title,
4050
4748
  "",
4749
+ "## Approved Living Project Context Map",
4750
+ "",
4751
+ livingContext,
4752
+ "",
4051
4753
  "## Approved Project Brain Context",
4052
4754
  "",
4053
4755
  approvedContext || "No approved project-brain records were loaded. Inspect the local repository and explain the gap in the report.",
@@ -4073,6 +4775,7 @@ function createImpactPreviewPrompt(workItem, context) {
4073
4775
  }
4074
4776
  function createAssistantQuestionPrompt(workItem, context) {
4075
4777
  const question = context?.question.content ?? workItem.sourceWish ?? workItem.title;
4778
+ const livingContext = formatProjectContextMap(context?.activeMap, question, context?.contextPack);
4076
4779
  const priorMessages = (context?.messages ?? []).filter((message) => message.messageId !== context?.question.messageId).slice(-12).map((message) => `- ${message.role} / ${message.status}: ${message.content}`).join("\n");
4077
4780
  const brainContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 12).map((document) => [
4078
4781
  `### ${document.title}`,
@@ -4099,6 +4802,10 @@ function createAssistantQuestionPrompt(workItem, context) {
4099
4802
  `Project ID: ${workItem.projectId}`,
4100
4803
  `Assistant message ID: ${workItem.assistantMessageId ?? context?.question.messageId ?? "unknown"}`,
4101
4804
  "",
4805
+ "## Approved Living Project Context Map",
4806
+ "",
4807
+ livingContext,
4808
+ "",
4102
4809
  "## Approved Project Brain Context",
4103
4810
  "",
4104
4811
  brainContext || "No approved project-brain records were loaded. Say what you inspected locally and avoid pretending citations exist.",
@@ -4112,6 +4819,7 @@ function createAssistantQuestionPrompt(workItem, context) {
4112
4819
  "- Keep repository source, secrets, tokens, local credential paths, and provider session references local.",
4113
4820
  "- Summarize findings instead of dumping source code. Tiny identifiers, filenames, and short excerpts are acceptable when necessary.",
4114
4821
  '- Use sourceBoundary "projectBrain" when the answer only uses project-brain records.',
4822
+ "- Treat the approved living context map as project-brain context; use it to avoid broad repository rereads when it is fresh and relevant.",
4115
4823
  '- Use sourceBoundary "localSource" when you inspected local repository files.',
4116
4824
  '- Use sourceBoundary "mixed" when both project-brain records and local files shaped the answer.',
4117
4825
  "- Include citations for project-brain records with documentId/title/repoPath where relevant.",
@@ -4214,8 +4922,29 @@ function parseSecurityPostureScanResult(output) {
4214
4922
  const parsed = JSON.parse(stripJsonFence(payload));
4215
4923
  return securityPostureScanResultSchema.parse(parsed);
4216
4924
  }
4925
+ function parseAppEvaluationScanResult(output) {
4926
+ const start = output.indexOf(appEvaluationStart);
4927
+ const end = output.indexOf(appEvaluationEnd, start + appEvaluationStart.length);
4928
+ if (start === -1 || end === -1 || end <= start) {
4929
+ throw new Error("Local AI scan did not return an Amistio app evaluation block.");
4930
+ }
4931
+ const payload = output.slice(start + appEvaluationStart.length, end).trim();
4932
+ const parsed = JSON.parse(stripJsonFence(payload));
4933
+ return appEvaluationScanResultSchema.parse(parsed);
4934
+ }
4935
+ function parseProjectContextRefreshResult(output) {
4936
+ const start = output.indexOf(projectContextRefreshStart);
4937
+ const end = output.indexOf(projectContextRefreshEnd, start + projectContextRefreshStart.length);
4938
+ if (start === -1 || end === -1 || end <= start) {
4939
+ throw new Error("Local AI refresh did not return an Amistio project context block.");
4940
+ }
4941
+ const payload = output.slice(start + projectContextRefreshStart.length, end).trim();
4942
+ const parsed = JSON.parse(stripJsonFence(payload));
4943
+ return projectContextRefreshResultSchema.parse(parsed);
4944
+ }
4217
4945
  function createBrainGenerationPrompt(workItem) {
4218
4946
  const wish = workItem.sourceWish ?? workItem.title;
4947
+ const artifactFormatPreference = workItem.artifactFormatPreference ?? "markdown";
4219
4948
  return [
4220
4949
  "# Amistio Brain Generation",
4221
4950
  "",
@@ -4232,6 +4961,7 @@ function createBrainGenerationPrompt(workItem) {
4232
4961
  `Work item ID: ${workItem.workItemId}`,
4233
4962
  `Project ID: ${workItem.projectId}`,
4234
4963
  `Generated draft ID: ${workItem.generatedDraftId ?? "unknown"}`,
4964
+ `Artifact format preference: ${artifactFormatPreference}`,
4235
4965
  "",
4236
4966
  "## Read First",
4237
4967
  "",
@@ -4241,7 +4971,7 @@ function createBrainGenerationPrompt(workItem) {
4241
4971
  "",
4242
4972
  "## Generate Artifacts",
4243
4973
  "",
4244
- "Return Markdown artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
4974
+ "Return artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
4245
4975
  "",
4246
4976
  "- architecture -> docs/architecture/",
4247
4977
  "- context -> docs/context/",
@@ -4251,20 +4981,35 @@ function createBrainGenerationPrompt(workItem) {
4251
4981
  "- plan -> docs/plans/",
4252
4982
  "- prompt -> docs/prompts/",
4253
4983
  "- workflow -> docs/workflows/",
4984
+ "- HTML companions -> docs/html/<same-folder>/ using .html, for example docs/html/plans/PLAN-example.html",
4985
+ "",
4986
+ artifactFormatPreferenceInstructions(artifactFormatPreference),
4254
4987
  "",
4255
- "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.",
4988
+ "Each artifact needs documentType, contentFormat, title, repoPath, and content. contentFormat may be omitted only for Markdown. Include at least a plan and an implementation prompt when implementation work is implied. Use model-agnostic prompt wording.",
4256
4989
  "",
4257
4990
  "## Output Contract",
4258
4991
  "",
4259
4992
  "Print exactly one JSON object between the markers below. The CLI will submit only this structured result back to Amistio.",
4260
4993
  "",
4261
4994
  generationResultStart,
4262
- '{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"docs/plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
4995
+ artifactFormatPreference === "html" ? '{"artifacts":[{"documentType":"plan","contentFormat":"html","title":"Plan: Example","repoPath":"docs/html/plans/PLAN-example.html","content":"<!doctype html><html><body><h1>Plan: Example</h1></body></html>"}]}' : '{"artifacts":[{"documentType":"plan","contentFormat":"markdown","title":"Plan: Example","repoPath":"docs/plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
4263
4996
  generationResultEnd,
4264
4997
  "",
4265
4998
  "Do not put Markdown fences around the markers. Do not claim implementation is complete."
4266
4999
  ].join("\n");
4267
5000
  }
5001
+ function artifactFormatPreferenceInstructions(preference) {
5002
+ if (preference === "html") {
5003
+ return "The user chose HTML. Generate .html artifacts under docs/html/<folder>/ and set contentFormat to html.";
5004
+ }
5005
+ if (preference === "both") {
5006
+ return "The user chose both formats. Generate Markdown artifacts under docs/<folder>/ and matching HTML companions under docs/html/<folder>/ with contentFormat values markdown and html.";
5007
+ }
5008
+ if (preference === "auto") {
5009
+ return "Choose Markdown or HTML based on the artifact content. Use Markdown for text-first docs and HTML for visual or layout-sensitive artifacts.";
5010
+ }
5011
+ return "The user chose Markdown. Generate Markdown artifacts under docs/<folder>/ and set contentFormat to markdown or omit it.";
5012
+ }
4268
5013
  function stripJsonFence(value) {
4269
5014
  const trimmed = value.trim();
4270
5015
  if (!trimmed.startsWith("```")) {
@@ -4289,7 +5034,13 @@ function formatWatchIdleLine(action, intervalSeconds) {
4289
5034
  }
4290
5035
  function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
4291
5036
  const key = watchStateKey(action);
4292
- return !previous || previous.key !== key || nowMs - previous.printedAtMs >= reminderMs;
5037
+ if (!previous || previous.key !== key) {
5038
+ return true;
5039
+ }
5040
+ if (action.kind === "workCompleted") {
5041
+ return false;
5042
+ }
5043
+ return nowMs - previous.printedAtMs >= reminderMs;
4293
5044
  }
4294
5045
  function watchStateKey(action) {
4295
5046
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
@@ -4469,7 +5220,8 @@ async function scanLegacyDocuments(options) {
4469
5220
  skipped.push({ repoPath, reason: "excluded" });
4470
5221
  continue;
4471
5222
  }
4472
- if (!isMarkdownDocument(repoPath)) {
5223
+ const contentFormat = inferContentFormatFromRepoPath(repoPath);
5224
+ if (!contentFormat) {
4473
5225
  skipped.push({ repoPath, reason: "notMarkdown" });
4474
5226
  continue;
4475
5227
  }
@@ -4492,16 +5244,17 @@ async function scanLegacyDocuments(options) {
4492
5244
  skipped.push({ repoPath, reason: "unreadable" });
4493
5245
  continue;
4494
5246
  }
4495
- if (isAmistioManagedMarkdown(content)) {
5247
+ if (isAmistioManagedDocument(content)) {
4496
5248
  skipped.push({ repoPath, reason: "alreadyManaged" });
4497
5249
  continue;
4498
5250
  }
4499
5251
  const documentType = classifyLegacyDocument(repoPath, content);
4500
- const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType), repoPath, usedDestinationPaths);
5252
+ const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType, contentFormat), repoPath, usedDestinationPaths);
4501
5253
  candidates.push({
4502
5254
  sourcePath: repoPath,
4503
5255
  repoPath: destinationPath,
4504
5256
  documentType,
5257
+ contentFormat,
4505
5258
  title: inferTitle2(content, repoPath),
4506
5259
  content,
4507
5260
  contentHash: sha256ContentHash(content)
@@ -4524,6 +5277,7 @@ function buildImportedBrainDocuments(options) {
4524
5277
  projectId: options.projectId,
4525
5278
  documentId,
4526
5279
  documentType: candidate.documentType,
5280
+ contentFormat: candidate.contentFormat,
4527
5281
  title: candidate.title,
4528
5282
  status: "approved",
4529
5283
  repoPath: candidate.repoPath,
@@ -4602,9 +5356,6 @@ function wildcardMatch(pattern, repoPath) {
4602
5356
  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, ".*");
4603
5357
  return new RegExp(`^${escaped}$`).test(repoPath);
4604
5358
  }
4605
- function isMarkdownDocument(repoPath) {
4606
- return /\.(md|mdx)$/i.test(repoPath);
4607
- }
4608
5359
  function isExcludedRepoPath(repoPath) {
4609
5360
  if (isControlPlaneTemplateRepoPath(repoPath)) return true;
4610
5361
  if (excludedFileNames.has(repoPath)) return true;
@@ -4613,9 +5364,12 @@ function isExcludedRepoPath(repoPath) {
4613
5364
  const basename = segments[segments.length - 1]?.toLowerCase() ?? "";
4614
5365
  return basename.startsWith(".env") || basename.includes("secret") || basename.includes("token") || basename.includes("credential") || basename.endsWith(".lock");
4615
5366
  }
4616
- function isAmistioManagedMarkdown(content) {
5367
+ function isAmistioManagedDocument(content) {
4617
5368
  const frontmatter = parseFrontmatter(content);
4618
- return typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0;
5369
+ if (typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0) {
5370
+ return true;
5371
+ }
5372
+ return /^\s*<!--[\s\S]*?amistioDocumentId\s*:/i.test(content);
4619
5373
  }
4620
5374
  function classifyLegacyDocument(repoPath, content) {
4621
5375
  const haystack = `${repoPath} ${inferTitle2(content, repoPath)}`.toLowerCase();
@@ -4628,7 +5382,7 @@ function classifyLegacyDocument(repoPath, content) {
4628
5382
  if (/\b(workflow|workflows|runbook|playbook|process|procedure|ops)\b/.test(haystack)) return "workflow";
4629
5383
  return "context";
4630
5384
  }
4631
- function canonicalImportPath(sourcePath, documentType) {
5385
+ function canonicalImportPath(sourcePath, documentType, contentFormat) {
4632
5386
  if (isCanonicalControlPlanePath(sourcePath)) {
4633
5387
  return sourcePath;
4634
5388
  }
@@ -4636,7 +5390,8 @@ function canonicalImportPath(sourcePath, documentType) {
4636
5390
  return `docs/${sourcePath}`;
4637
5391
  }
4638
5392
  const baseSlug = slugFromPath(sourcePath);
4639
- return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
5393
+ const extension = contentFormat === "html" ? "html" : "md";
5394
+ return `${documentFolderByType[documentType]}/imported/${baseSlug}.${extension}`;
4640
5395
  }
4641
5396
  function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
4642
5397
  if (!usedPaths.has(basePath)) {
@@ -4651,8 +5406,8 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
4651
5406
  return uniquePath;
4652
5407
  }
4653
5408
  function isCanonicalControlPlanePath(repoPath) {
4654
- const [firstSegment, secondSegment] = normalizeRepoPath4(repoPath).split("/");
4655
- return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
5409
+ const [firstSegment, secondSegment, thirdSegment] = normalizeRepoPath4(repoPath).split("/");
5410
+ return firstSegment === "docs" && Boolean(secondSegment === "html" && thirdSegment && controlPlaneRoots2.includes(thirdSegment) || secondSegment && controlPlaneRoots2.includes(secondSegment));
4656
5411
  }
4657
5412
  function isLegacyControlPlanePath(repoPath) {
4658
5413
  const [firstSegment] = repoPath.split("/");
@@ -4662,6 +5417,8 @@ function inferTitle2(content, repoPath) {
4662
5417
  const body = stripFrontmatter(content);
4663
5418
  const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
4664
5419
  if (heading) return heading;
5420
+ const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
5421
+ if (htmlHeading) return htmlHeading;
4665
5422
  const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
4666
5423
  return titleCase(basename || "Imported Document");
4667
5424
  }
@@ -4673,7 +5430,7 @@ function stripFrontmatter(content) {
4673
5430
  return closingLineEnd === -1 ? "" : content.slice(closingLineEnd + 1);
4674
5431
  }
4675
5432
  function slugFromPath(repoPath) {
4676
- const withoutExtension = repoPath.replace(/\.(md|mdx)$/i, "");
5433
+ const withoutExtension = repoPath.replace(/\.(md|mdx|html?)$/i, "");
4677
5434
  const slug = withoutExtension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 90);
4678
5435
  return slug || "imported-document";
4679
5436
  }
@@ -4772,6 +5529,7 @@ async function runOfficialCliUpdateWithRuntimeRefresh(options) {
4772
5529
  if (!options.restartBackgroundRunner) {
4773
5530
  return {
4774
5531
  succeeded: false,
5532
+ stopRunner: true,
4775
5533
  message: `${updateResult.message} Background runner restart was not available after update. Restart the runner manually to load the updated CLI version.`,
4776
5534
  error: "Background runner restart hook was not provided."
4777
5535
  };
@@ -4786,6 +5544,7 @@ async function runOfficialCliUpdateWithRuntimeRefresh(options) {
4786
5544
  }
4787
5545
  return {
4788
5546
  succeeded: false,
5547
+ stopRunner: true,
4789
5548
  message: `${updateResult.message} Background runner restart failed after update. Restart the runner manually to load the updated CLI version.`,
4790
5549
  error: restartResult.error ?? restartResult.message
4791
5550
  };
@@ -4907,6 +5666,186 @@ function errorMessage2(error) {
4907
5666
  return error instanceof Error ? error.message : String(error);
4908
5667
  }
4909
5668
 
5669
+ // src/implementation-handoff.ts
5670
+ import { execFile as execFile6 } from "node:child_process";
5671
+ import path13 from "node:path";
5672
+ import { promisify as promisify6 } from "node:util";
5673
+ var execFileAsync6 = promisify6(execFile6);
5674
+ async function completeImplementationHandoff(input) {
5675
+ const run = input.commandRunner ?? defaultCommandRunner;
5676
+ const headBranch = input.worktreeIsolation?.branch ?? input.workItem.executionBranch;
5677
+ const baseBranch = input.baseBranch ?? input.repositoryLink?.defaultBranch ?? "main";
5678
+ if (!headBranch) {
5679
+ return blockedHandoff({ baseBranch, message: "Implementation handoff needs an execution branch before it can create a pull request." });
5680
+ }
5681
+ const common = {
5682
+ provider: input.repositoryLink?.provider ?? "github",
5683
+ baseBranch,
5684
+ headBranch
5685
+ };
5686
+ try {
5687
+ const unmergedFiles = await gitOutput2(run, input.worktreePath, ["diff", "--name-only", "--diff-filter=U"]);
5688
+ if (unmergedFiles.trim()) {
5689
+ return blockedHandoff({ ...common, message: "Implementation handoff is blocked because the worktree has unresolved merge conflicts." });
5690
+ }
5691
+ const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
5692
+ if (!status) {
5693
+ return {
5694
+ ...common,
5695
+ status: "noChanges",
5696
+ cleanupStatus: "notApplicable",
5697
+ message: "Local execution completed with no repository changes to hand off."
5698
+ };
5699
+ }
5700
+ const remoteName = await resolveRemoteName(run, input.worktreePath);
5701
+ const remoteUrl = await gitOutput2(run, input.worktreePath, ["remote", "get-url", remoteName]);
5702
+ const provider = input.repositoryLink?.provider ?? inferProvider(remoteUrl);
5703
+ if (provider !== "github") {
5704
+ return blockedHandoff({
5705
+ ...common,
5706
+ provider,
5707
+ remoteName,
5708
+ message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository."
5709
+ });
5710
+ }
5711
+ await gitOutput2(run, input.worktreePath, ["add", "-A"]);
5712
+ await gitOutput2(run, input.worktreePath, ["commit", "-m", commitSubject(input.workItem), "-m", commitBody(input)]);
5713
+ const commitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
5714
+ await gitOutput2(run, input.worktreePath, ["push", "--set-upstream", remoteName, headBranch]);
5715
+ const pullRequest = await ensureGithubPullRequest(run, input.worktreePath, { baseBranch, headBranch, workItem: input.workItem, ...input.verificationSummary ? { verificationSummary: input.verificationSummary } : {} });
5716
+ const cleanup = await cleanupWorktree(run, input);
5717
+ return {
5718
+ provider: "github",
5719
+ status: "prReady",
5720
+ baseBranch,
5721
+ headBranch,
5722
+ remoteName,
5723
+ commitSha,
5724
+ prNumber: pullRequest.number,
5725
+ prUrl: pullRequest.url,
5726
+ cleanupStatus: cleanup.status,
5727
+ ...cleanup.message ? { cleanupMessage: cleanup.message } : {},
5728
+ message: cleanup.status === "completed" ? "GitHub pull request is ready for review and the local worktree was cleaned up." : "GitHub pull request is ready for review; local worktree cleanup needs attention."
5729
+ };
5730
+ } catch (error) {
5731
+ return blockedHandoff({ ...common, message: "Implementation handoff is blocked and the local worktree was preserved for recovery.", error: safeErrorMessage(error) });
5732
+ }
5733
+ }
5734
+ async function ensureGithubPullRequest(run, cwd, input) {
5735
+ const existing = await run("gh", ["pr", "list", "--head", input.headBranch, "--base", input.baseBranch, "--state", "open", "--json", "number,url", "--limit", "1"], { cwd });
5736
+ const parsed = parsePullRequestList(existing.stdout);
5737
+ if (parsed) {
5738
+ return parsed;
5739
+ }
5740
+ const created = await run("gh", ["pr", "create", "--base", input.baseBranch, "--head", input.headBranch, "--title", pullRequestTitle(input.workItem), "--body", pullRequestBody(input)], { cwd });
5741
+ const url = extractPullRequestUrl(created.stdout);
5742
+ if (!url) {
5743
+ throw new Error("GitHub CLI did not return a pull request URL.");
5744
+ }
5745
+ const number = pullRequestNumber(url);
5746
+ if (!number) {
5747
+ throw new Error("GitHub CLI returned a pull request URL without a parseable number.");
5748
+ }
5749
+ return { number, url };
5750
+ }
5751
+ async function cleanupWorktree(run, input) {
5752
+ const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
5753
+ if (status) {
5754
+ return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
5755
+ }
5756
+ try {
5757
+ await gitOutput2(run, input.primaryRepoRoot || path13.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
5758
+ return { status: "completed" };
5759
+ } catch (error) {
5760
+ return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
5761
+ }
5762
+ }
5763
+ async function resolveRemoteName(run, cwd) {
5764
+ const remotes = (await gitOutput2(run, cwd, ["remote"])).split(/\r?\n/g).map((remote) => remote.trim()).filter(Boolean);
5765
+ if (!remotes.length) {
5766
+ throw new Error("No Git remote is configured for PR handoff.");
5767
+ }
5768
+ return remotes.includes("origin") ? "origin" : remotes[0];
5769
+ }
5770
+ async function gitOutput2(run, cwd, args) {
5771
+ const result = await run("git", args, { cwd });
5772
+ return result.stdout.trim();
5773
+ }
5774
+ function blockedHandoff(input) {
5775
+ return {
5776
+ status: "blocked",
5777
+ cleanupStatus: "pending",
5778
+ ...input
5779
+ };
5780
+ }
5781
+ function inferProvider(remoteUrl) {
5782
+ try {
5783
+ return parseRepositoryCloneUrl(remoteUrl).provider ?? "other";
5784
+ } catch {
5785
+ return "other";
5786
+ }
5787
+ }
5788
+ function parsePullRequestList(stdout) {
5789
+ const parsed = JSON.parse(stdout || "[]");
5790
+ const first = parsed[0];
5791
+ if (typeof first?.number === "number" && typeof first.url === "string" && first.url) {
5792
+ return { number: first.number, url: first.url };
5793
+ }
5794
+ return void 0;
5795
+ }
5796
+ function extractPullRequestUrl(stdout) {
5797
+ return stdout.split(/\s+/g).find((value) => /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/i.test(value));
5798
+ }
5799
+ function pullRequestNumber(url) {
5800
+ const match = /\/pull\/(\d+)(?:$|[?#])/.exec(url);
5801
+ return match ? Number.parseInt(match[1], 10) : void 0;
5802
+ }
5803
+ function commitSubject(workItem) {
5804
+ return truncate(`Amistio: ${workItem.title}`, 72);
5805
+ }
5806
+ function commitBody(input) {
5807
+ return [
5808
+ `Work item: ${input.workItem.workItemId}`,
5809
+ `Implementation scope: ${input.workItem.controllingAdrId ?? input.workItem.implementationScopeId ?? "unspecified"}`,
5810
+ "",
5811
+ "Generated by Amistio local runner.",
5812
+ "Do not include secrets or source patches in Amistio SaaS metadata."
5813
+ ].join("\n");
5814
+ }
5815
+ function pullRequestTitle(workItem) {
5816
+ return truncate(workItem.title, 120);
5817
+ }
5818
+ function pullRequestBody(input) {
5819
+ return [
5820
+ `Amistio work item: ${input.workItem.workItemId}`,
5821
+ `Implementation scope: ${input.workItem.controllingAdrId ?? input.workItem.implementationScopeId ?? "unspecified"}`,
5822
+ `Base branch: ${input.baseBranch}`,
5823
+ `Head branch: ${input.headBranch}`,
5824
+ "",
5825
+ input.verificationSummary ?? "Verification summary was not reported by the local runner."
5826
+ ].join("\n");
5827
+ }
5828
+ function truncate(value, maxLength) {
5829
+ return value.length <= maxLength ? value : value.slice(0, maxLength - 1).trimEnd();
5830
+ }
5831
+ async function defaultCommandRunner(command, args, options) {
5832
+ const { stdout, stderr } = await execFileAsync6(command, args, { cwd: options.cwd, maxBuffer: 1024 * 1024 });
5833
+ return { stdout, stderr };
5834
+ }
5835
+ function safeErrorMessage(error) {
5836
+ const scrub = (value) => redactLocalPaths(value.replace(/^Command failed:[^\n]*(\n)?/i, "").trim() || value);
5837
+ if (typeof error === "object" && error && "stderr" in error && typeof error.stderr === "string" && error.stderr.trim()) {
5838
+ return truncate(scrub(error.stderr.trim()), 600);
5839
+ }
5840
+ if (error instanceof Error) {
5841
+ return truncate(scrub(error.message), 600);
5842
+ }
5843
+ return truncate(scrub(String(error)), 600);
5844
+ }
5845
+ function redactLocalPaths(value) {
5846
+ return value.replace(/(^|[\s'"`])(\/{1,2}(?:Users|home|private|tmp|var|Volumes)\/[^\s'"`]+)/g, "$1<local-path>").replace(/(^|[\s'"`])([A-Za-z]:\\[^\s'"`]+)/g, "$1<local-path>");
5847
+ }
5848
+
4910
5849
  // src/version.ts
4911
5850
  import { readFileSync } from "node:fs";
4912
5851
  function readCliPackageVersion() {
@@ -4926,7 +5865,7 @@ var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
4926
5865
  var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
4927
5866
  var RUNNER_WORK_LEASE_SECONDS = 300;
4928
5867
  var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
4929
- var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan"];
5868
+ var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh"];
4930
5869
  program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
4931
5870
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
4932
5871
  const created = await initControlPlane(options.root);
@@ -5304,7 +6243,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
5304
6243
  projectId: context.metadata.amistioProjectId,
5305
6244
  repositoryLinkId: context.metadata.repositoryLinkId,
5306
6245
  runnerId,
5307
- rootDir: path13.resolve(options.root),
6246
+ rootDir: path14.resolve(options.root),
5308
6247
  apiUrl: options.apiUrl,
5309
6248
  args: buildBackgroundRunnerArgs(resolvedOptions)
5310
6249
  });
@@ -5476,7 +6415,7 @@ runnerService.command("install").description("Install a user-level startup servi
5476
6415
  projectId: context.metadata.amistioProjectId,
5477
6416
  repositoryLinkId: context.metadata.repositoryLinkId,
5478
6417
  runnerId,
5479
- rootDir: path13.resolve(options.root),
6418
+ rootDir: path14.resolve(options.root),
5480
6419
  apiUrl: options.apiUrl,
5481
6420
  args,
5482
6421
  platform
@@ -5925,13 +6864,72 @@ async function runNextWorkItem({
5925
6864
  return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
5926
6865
  }
5927
6866
  }
5928
- const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
6867
+ if (result.workItem.workKind === "appEvaluationScan") {
6868
+ try {
6869
+ return await finalizeAppEvaluationScanWork({
6870
+ apiClient,
6871
+ durationMs: Date.now() - startedAt,
6872
+ projectId,
6873
+ repositoryLinkId,
6874
+ runnerId,
6875
+ sessionContext,
6876
+ toolConfig,
6877
+ toolName: preview.toolName,
6878
+ toolResult,
6879
+ workItem: result.workItem
6880
+ });
6881
+ } catch (error) {
6882
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
6883
+ }
6884
+ }
6885
+ if (result.workItem.workKind === "projectContextRefresh") {
6886
+ try {
6887
+ return await finalizeProjectContextRefreshWork({
6888
+ apiClient,
6889
+ durationMs: Date.now() - startedAt,
6890
+ projectId,
6891
+ repositoryLinkId,
6892
+ runnerId,
6893
+ sessionContext,
6894
+ toolConfig,
6895
+ toolName: preview.toolName,
6896
+ toolResult,
6897
+ workItem: result.workItem
6898
+ });
6899
+ } catch (error) {
6900
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
6901
+ }
6902
+ }
6903
+ let finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
5929
6904
  const durationMs = Date.now() - startedAt;
5930
6905
  const failureExcerpt = toolResult.exitCode === 0 ? void 0 : truncateLogExcerpt(toolResult.stderr || toolResult.stdout);
6906
+ let implementationHandoff;
6907
+ let finalMessage = `${preview.toolName} exited with code ${toolResult.exitCode}.`;
6908
+ let finalError = failureExcerpt;
6909
+ if (toolResult.exitCode === 0 && (result.workItem.workKind ?? "implementation") === "implementation") {
6910
+ await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
6911
+ status: "running",
6912
+ summary: "Preparing GitHub PR handoff for implementation work.",
6913
+ idempotencyKey: `runner_milestone_handoff_started_${result.workItem.workItemId}_${result.workItem.attempt}`,
6914
+ metadata: { executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "", executionBranch: isolationTelemetry.executionBranch ?? "" }
6915
+ });
6916
+ const repositoryLink = await loadWorkItemRepositoryLink(apiClient, projectId, result.workItem.repositoryLinkId ?? repositoryLinkId);
6917
+ implementationHandoff = await completeImplementationHandoff({
6918
+ primaryRepoRoot: root,
6919
+ ...repositoryLink ? { repositoryLink } : {},
6920
+ verificationSummary: "Local execution reported completion.",
6921
+ workItem: result.workItem,
6922
+ ...worktreeIsolation.isolation ? { worktreeIsolation: worktreeIsolation.isolation } : {},
6923
+ worktreePath: executionRoot
6924
+ });
6925
+ finalStatus = implementationHandoff.status === "prReady" || implementationHandoff.status === "noChanges" ? "completed" : "blocked";
6926
+ finalMessage = implementationHandoff.message ?? "Implementation handoff finished.";
6927
+ finalError = implementationHandoff.error;
6928
+ }
5931
6929
  const updatedToolSession = await finalizeToolSession({
5932
6930
  apiClient,
5933
6931
  projectId,
5934
- status: finalStatus,
6932
+ status: toolResult.exitCode === 0 ? "completed" : "failed",
5935
6933
  runnerId,
5936
6934
  workItemId: result.workItem.workItemId,
5937
6935
  stdout: toolResult.stdout,
@@ -5951,8 +6949,9 @@ async function runNextWorkItem({
5951
6949
  tool: preview.toolName,
5952
6950
  ...toolResult.model ? { model: toolResult.model } : {},
5953
6951
  durationMs,
5954
- message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
6952
+ message: finalMessage,
5955
6953
  ...isolationTelemetry,
6954
+ ...finalStatus === "blocked" ? { blockerReason: finalMessage } : {},
5956
6955
  sessionPolicy: sessionContext.policy,
5957
6956
  sessionDecision: sessionContext.decision,
5958
6957
  sessionDecisionReason: sessionContext.reason,
@@ -5961,19 +6960,34 @@ async function runNextWorkItem({
5961
6960
  ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
5962
6961
  ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
5963
6962
  ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {},
5964
- ...failureExcerpt ? { error: failureExcerpt } : {}
6963
+ ...implementationHandoff ? { implementationHandoff } : {},
6964
+ ...finalError ? { error: finalError } : {}
5965
6965
  }
5966
6966
  );
5967
6967
  await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
5968
6968
  status: finalStatus,
5969
- summary: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
6969
+ summary: finalMessage,
5970
6970
  idempotencyKey: `runner_milestone_finished_${result.workItem.workItemId}_${statusResult.workItem.idempotencyKey}`,
5971
- 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 ?? "" }
6971
+ metadata: {
6972
+ tool: preview.toolName,
6973
+ durationMs,
6974
+ exitCode: toolResult.exitCode,
6975
+ verificationSummary: finalStatus === "completed" ? "Local execution reported completion." : "Local execution reported failure.",
6976
+ executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "",
6977
+ executionBranch: isolationTelemetry.executionBranch ?? "",
6978
+ ...implementationHandoff?.status ? { handoffStatus: implementationHandoff.status } : {},
6979
+ ...implementationHandoff?.prUrl ? { prUrl: implementationHandoff.prUrl } : {},
6980
+ ...implementationHandoff?.cleanupStatus ? { cleanupStatus: implementationHandoff.cleanupStatus } : {}
6981
+ }
5972
6982
  });
5973
6983
  await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
5974
6984
  const durationSeconds = Math.round(durationMs / 1e3);
5975
6985
  console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
5976
- return { status: finalStatus, exitCode: toolResult.exitCode };
6986
+ return { status: finalStatus, exitCode: finalStatus === "completed" ? toolResult.exitCode : 1 };
6987
+ }
6988
+ async function loadWorkItemRepositoryLink(apiClient, projectId, repositoryLinkId) {
6989
+ const { repositoryLinks } = await apiClient.listRepositoryLinks(projectId);
6990
+ return repositoryLinks.find((link) => link.repositoryLinkId === repositoryLinkId && link.status !== "revoked");
5977
6991
  }
5978
6992
  async function prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
5979
6993
  if (!needsGitWorktreeIsolation(workItem)) {
@@ -6177,10 +7191,10 @@ async function executeRunnerCommand(command, context) {
6177
7191
  }
6178
7192
  return runOfficialCliUpdateWithRuntimeRefresh({
6179
7193
  mode: currentRunnerMode(),
6180
- restartBackgroundRunner: () => restartCurrentRunner(context)
7194
+ restartBackgroundRunner: () => restartCurrentRunner(context, { useUpdatedCliExecutable: true })
6181
7195
  });
6182
7196
  }
6183
- async function restartCurrentRunner(context) {
7197
+ async function restartCurrentRunner(context, options = {}) {
6184
7198
  if (currentRunnerMode() !== "background") {
6185
7199
  return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
6186
7200
  }
@@ -6189,7 +7203,7 @@ async function restartCurrentRunner(context) {
6189
7203
  return { succeeded: false, message: "Background runner metadata was not found, so restart needs manual action." };
6190
7204
  }
6191
7205
  try {
6192
- const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
7206
+ const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs, options.useUpdatedCliExecutable ? updatedCliRunnerLaunchOptions() : {});
6193
7207
  return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
6194
7208
  } catch (error) {
6195
7209
  return { succeeded: false, message: "Background restart failed.", error: errorMessage3(error) };
@@ -6638,23 +7652,205 @@ ${toolResult.stderr}`);
6638
7652
  console.error(scanError ?? "Local runner security posture scan failed.");
6639
7653
  return { status: "failed", exitCode: toolResult.exitCode || 1 };
6640
7654
  }
7655
+ async function finalizeAppEvaluationScanWork({
7656
+ apiClient,
7657
+ durationMs,
7658
+ projectId,
7659
+ repositoryLinkId,
7660
+ runnerId,
7661
+ sessionContext,
7662
+ toolConfig,
7663
+ toolName,
7664
+ toolResult,
7665
+ workItem
7666
+ }) {
7667
+ let scanResult = void 0;
7668
+ let scanError;
7669
+ if (toolResult.exitCode === 0) {
7670
+ try {
7671
+ scanResult = parseAppEvaluationScanResult(`${toolResult.stdout}
7672
+ ${toolResult.stderr}`);
7673
+ } catch (error) {
7674
+ scanError = errorMessage3(error);
7675
+ }
7676
+ } else {
7677
+ scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
7678
+ }
7679
+ const finalStatus = scanResult ? "completed" : "failed";
7680
+ const updatedToolSession = await finalizeToolSession({
7681
+ apiClient,
7682
+ projectId,
7683
+ status: finalStatus,
7684
+ runnerId,
7685
+ workItemId: workItem.workItemId,
7686
+ stdout: toolResult.stdout,
7687
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
7688
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
7689
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
7690
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
7691
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
7692
+ });
7693
+ const sessionTelemetry = {
7694
+ sessionPolicy: sessionContext.policy,
7695
+ sessionDecision: sessionContext.decision,
7696
+ sessionDecisionReason: sessionContext.reason,
7697
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
7698
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
7699
+ };
7700
+ if (scanResult) {
7701
+ const result = await apiClient.submitAppEvaluationScanResult(projectId, workItem.workItemId, {
7702
+ status: "completed",
7703
+ runnerId,
7704
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID()}`,
7705
+ result: scanResult,
7706
+ tool: toolName,
7707
+ durationMs,
7708
+ ...sessionTelemetry,
7709
+ message: `${toolName} returned an app evaluation scan.`
7710
+ });
7711
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7712
+ status: "completed",
7713
+ summary: `${toolName} returned an app evaluation scan.`,
7714
+ idempotencyKey: `runner_milestone_app_evaluation_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7715
+ metadata: { tool: toolName, durationMs, findingCount: scanResult.findings.length, verificationSummary: scanResult.verificationPlan.join(" | ") }
7716
+ });
7717
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7718
+ console.log("App evaluation scan returned for review.");
7719
+ return { status: "completed", exitCode: 0 };
7720
+ }
7721
+ const failedResult = await apiClient.submitAppEvaluationScanResult(projectId, workItem.workItemId, {
7722
+ status: "failed",
7723
+ runnerId,
7724
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID()}`,
7725
+ tool: toolName,
7726
+ durationMs,
7727
+ ...sessionTelemetry,
7728
+ message: `${toolName} did not produce a valid app evaluation scan.`,
7729
+ ...scanError ? { error: scanError } : {}
7730
+ });
7731
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7732
+ status: "failed",
7733
+ summary: scanError ?? `${toolName} did not produce a valid app evaluation scan.`,
7734
+ idempotencyKey: `runner_milestone_app_evaluation_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
7735
+ metadata: { tool: toolName, durationMs, verificationSummary: "App evaluation output did not include valid structured JSON." }
7736
+ });
7737
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7738
+ console.error(scanError ?? "Local runner app evaluation scan failed.");
7739
+ return { status: "failed", exitCode: toolResult.exitCode || 1 };
7740
+ }
7741
+ async function finalizeProjectContextRefreshWork({
7742
+ apiClient,
7743
+ durationMs,
7744
+ projectId,
7745
+ repositoryLinkId,
7746
+ runnerId,
7747
+ sessionContext,
7748
+ toolConfig,
7749
+ toolName,
7750
+ toolResult,
7751
+ workItem
7752
+ }) {
7753
+ let refreshResult = void 0;
7754
+ let refreshError;
7755
+ if (toolResult.exitCode === 0) {
7756
+ try {
7757
+ refreshResult = parseProjectContextRefreshResult(`${toolResult.stdout}
7758
+ ${toolResult.stderr}`);
7759
+ } catch (error) {
7760
+ refreshError = errorMessage3(error);
7761
+ }
7762
+ } else {
7763
+ refreshError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
7764
+ }
7765
+ const finalStatus = refreshResult ? "completed" : "failed";
7766
+ const updatedToolSession = await finalizeToolSession({
7767
+ apiClient,
7768
+ projectId,
7769
+ status: finalStatus,
7770
+ runnerId,
7771
+ workItemId: workItem.workItemId,
7772
+ stdout: toolResult.stdout,
7773
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
7774
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
7775
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
7776
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
7777
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
7778
+ });
7779
+ const sessionTelemetry = {
7780
+ sessionPolicy: sessionContext.policy,
7781
+ sessionDecision: sessionContext.decision,
7782
+ sessionDecisionReason: sessionContext.reason,
7783
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
7784
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
7785
+ };
7786
+ if (refreshResult) {
7787
+ const result = await apiClient.submitProjectContextRefreshResult(projectId, workItem.workItemId, {
7788
+ status: "completed",
7789
+ runnerId,
7790
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID()}`,
7791
+ result: refreshResult,
7792
+ tool: toolName,
7793
+ durationMs,
7794
+ ...sessionTelemetry,
7795
+ message: `${toolName} returned a project context refresh.`
7796
+ });
7797
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7798
+ status: "completed",
7799
+ summary: `${toolName} returned a project context refresh.`,
7800
+ idempotencyKey: `runner_milestone_project_context_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7801
+ metadata: { tool: toolName, durationMs, sliceCount: refreshResult.slices.length, entityCount: refreshResult.entities.length, mapId: result.map?.projectContextMapId ?? "", verificationSummary: refreshResult.verificationPlan.join(" | ") }
7802
+ });
7803
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7804
+ console.log("Project context refresh returned for review.");
7805
+ return { status: "completed", exitCode: 0 };
7806
+ }
7807
+ const failedResult = await apiClient.submitProjectContextRefreshResult(projectId, workItem.workItemId, {
7808
+ status: "failed",
7809
+ runnerId,
7810
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID()}`,
7811
+ tool: toolName,
7812
+ durationMs,
7813
+ ...sessionTelemetry,
7814
+ message: `${toolName} did not produce a valid project context refresh.`,
7815
+ ...refreshError ? { error: refreshError } : {}
7816
+ });
7817
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7818
+ status: "failed",
7819
+ summary: refreshError ?? `${toolName} did not produce a valid project context refresh.`,
7820
+ idempotencyKey: `runner_milestone_project_context_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
7821
+ metadata: { tool: toolName, durationMs, verificationSummary: "Project context output did not include valid structured JSON." }
7822
+ });
7823
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7824
+ console.error(refreshError ?? "Local runner project context refresh failed.");
7825
+ return { status: "failed", exitCode: toolResult.exitCode || 1 };
7826
+ }
6641
7827
  async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
6642
7828
  if (workItem.workKind === "assistantQuestion") {
6643
- const [{ documents: documents2 }, { messages: messages2 }] = await Promise.all([
7829
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7830
+ const [{ documents: documents2 }, { messages: messages2 }, context] = await Promise.all([
6644
7831
  apiClient.listBrainDocuments(projectId),
6645
- apiClient.listAssistantMessages(projectId)
7832
+ apiClient.listAssistantMessages(projectId),
7833
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
6646
7834
  ]);
6647
7835
  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];
7836
+ const contextPack = await apiClient.buildProjectContextPack(projectId, question?.content ?? workItem.sourceWish ?? workItem.title, workItem.workItemId).catch(() => void 0);
6648
7837
  return createWorkExecutionPrompt(workItem, {
6649
- ...question ? { assistantQuestion: { question, messages: messages2, documents: documents2 } } : {}
7838
+ ...question ? { assistantQuestion: { question, messages: messages2, documents: documents2, ...context.activeMap ? { activeMap: context.activeMap } : {}, ...contextPack ? { contextPack } : {} } } : {}
6650
7839
  });
6651
7840
  }
6652
7841
  if (workItem.workKind === "impactPreview") {
6653
- const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
7842
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7843
+ const [{ documents: documents2 }, context] = await Promise.all([
7844
+ apiClient.listBrainDocuments(projectId),
7845
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
7846
+ ]);
6654
7847
  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"));
7848
+ const contextPack = await apiClient.buildProjectContextPack(projectId, implementationPrompt?.content ?? workItem.sourceWish ?? workItem.title, workItem.workItemId).catch(() => void 0);
6655
7849
  return createWorkExecutionPrompt(workItem, {
6656
7850
  impactPreview: {
6657
7851
  documents: documents2,
7852
+ ...context.activeMap ? { activeMap: context.activeMap } : {},
7853
+ ...contextPack ? { contextPack } : {},
6658
7854
  ...implementationPrompt ? { implementationPrompt } : {}
6659
7855
  }
6660
7856
  });
@@ -6678,6 +7874,25 @@ async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
6678
7874
  securityPostureScan: { documents: documents2 }
6679
7875
  });
6680
7876
  }
7877
+ if (workItem.workKind === "appEvaluationScan") {
7878
+ const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
7879
+ return createWorkExecutionPrompt(workItem, {
7880
+ appEvaluationScan: { documents: documents2 }
7881
+ });
7882
+ }
7883
+ if (workItem.workKind === "projectContextRefresh") {
7884
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7885
+ const [{ documents: documents2 }, context] = await Promise.all([
7886
+ apiClient.listBrainDocuments(projectId),
7887
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
7888
+ ]);
7889
+ return createWorkExecutionPrompt(workItem, {
7890
+ projectContextRefresh: {
7891
+ documents: documents2,
7892
+ ...context.activeMap ? { activeMap: context.activeMap } : {}
7893
+ }
7894
+ });
7895
+ }
6681
7896
  if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
6682
7897
  return createWorkExecutionPrompt(workItem);
6683
7898
  }
@@ -6905,7 +8120,7 @@ function parseReasoningEffort(value) {
6905
8120
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
6906
8121
  }
6907
8122
  function inferRepoName(root) {
6908
- return path13.basename(path13.resolve(root)) || "repository";
8123
+ return path14.basename(path14.resolve(root)) || "repository";
6909
8124
  }
6910
8125
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
6911
8126
  return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");