@amistio/cli 0.1.12 → 0.1.14

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()
@@ -825,11 +1099,26 @@ var securityFindingResultSchema = z.object({
825
1099
  safePaths: z.array(z.string().trim().min(1).max(300)).default([]),
826
1100
  status: securityFindingStatusSchema.default("open")
827
1101
  });
1102
+ var securityPostureGradeSchema = z.enum(["A", "B", "C", "D", "F", "Unknown"]);
1103
+ var securityPostureScanResultGradeSchema = z.preprocess((value) => {
1104
+ if (typeof value !== "string") {
1105
+ return value;
1106
+ }
1107
+ const trimmed = value.trim();
1108
+ if (trimmed.toUpperCase() === "UNKNOWN") {
1109
+ return "Unknown";
1110
+ }
1111
+ const gradeMatch = /^(?:GRADE\s*)?([ABCDF])(?:\s*[+-])?(?:\b|$)/i.exec(trimmed);
1112
+ if (gradeMatch?.[1]) {
1113
+ return gradeMatch[1].toUpperCase();
1114
+ }
1115
+ return "Unknown";
1116
+ }, securityPostureGradeSchema);
828
1117
  var securityPostureScanResultSchema = z.object({
829
1118
  summary: z.string().trim().min(1).max(2e3),
830
1119
  baselineVersion: z.string().trim().min(1).max(80),
831
1120
  postureScore: z.number().min(0).max(100).optional(),
832
- postureGrade: z.enum(["A", "B", "C", "D", "F", "Unknown"]).default("Unknown"),
1121
+ postureGrade: securityPostureScanResultGradeSchema.default("Unknown"),
833
1122
  categorySummaries: z.array(securityCategorySummarySchema).default([]),
834
1123
  findings: z.array(securityFindingResultSchema).default([]),
835
1124
  verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1)
@@ -843,6 +1132,7 @@ var securityScanItemSchema = baseItemSchema.extend({
843
1132
  status: securityScanStatusSchema,
844
1133
  baselineVersion: z.string().trim().min(1).max(80).default("amistio-security-baseline-v1"),
845
1134
  workItemId: z.string().min(1).optional(),
1135
+ repositoryLinkId: z.string().min(1).optional(),
846
1136
  runnerId: z.string().min(1).optional(),
847
1137
  findingIds: z.array(z.string().min(1)).default([]),
848
1138
  summary: z.string().trim().min(1).max(2e3).optional(),
@@ -887,13 +1177,82 @@ var securityPostureSnapshotItemSchema = baseItemSchema.extend({
887
1177
  status: z.enum(["unknown", "fresh", "stale", "running", "failed"]),
888
1178
  baselineVersion: z.string().trim().min(1).max(80).default("amistio-security-baseline-v1"),
889
1179
  postureScore: z.number().min(0).max(100).optional(),
890
- postureGrade: z.enum(["A", "B", "C", "D", "F", "Unknown"]).default("Unknown"),
1180
+ postureGrade: securityPostureGradeSchema.default("Unknown"),
891
1181
  severityCounts: securitySeverityCountsSchema.default(defaultSecuritySeverityCounts),
892
1182
  categorySummaries: z.array(securityCategorySummarySchema).default([]),
893
1183
  lastScannedAt: isoDateTimeSchema.optional(),
894
1184
  nextScheduledAt: isoDateTimeSchema.optional(),
895
1185
  summary: z.string().trim().min(1).max(2e3).optional()
896
1186
  });
1187
+ var appEvaluationFindingResultSchema = z.object({
1188
+ title: z.string().trim().min(1).max(200),
1189
+ category: appEvaluationFindingCategorySchema,
1190
+ severity: appEvaluationFindingSeveritySchema,
1191
+ confidence: appEvaluationFindingConfidenceSchema.default("medium"),
1192
+ summary: z.string().trim().min(1).max(2e3),
1193
+ affectedSurfaces: z.array(z.string().trim().min(1).max(300)).default([]),
1194
+ evidence: z.array(z.string().trim().min(1).max(600)).default([]),
1195
+ suggestedAction: z.string().trim().min(1).max(4e3),
1196
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
1197
+ safePaths: z.array(z.string().trim().min(1).max(300)).default([]),
1198
+ proposedLifecycleAction: appEvaluationPlanLifecycleActionSchema.default("none"),
1199
+ relatedArtifactIds: z.array(z.string().trim().min(1).max(160)).default([]),
1200
+ proposedPlanTitle: z.string().trim().min(1).max(200).optional(),
1201
+ proposedPlanRepoPath: z.string().trim().min(1).max(300).optional(),
1202
+ proposedPlanContent: z.string().trim().min(1).max(3e4).optional(),
1203
+ status: appEvaluationFindingStatusSchema.default("open")
1204
+ });
1205
+ var appEvaluationScanResultSchema = z.object({
1206
+ summary: z.string().trim().min(1).max(2e3),
1207
+ baselineVersion: z.string().trim().min(1).max(80),
1208
+ findings: z.array(appEvaluationFindingResultSchema).default([]),
1209
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1)
1210
+ });
1211
+ var appEvaluationScanItemSchema = baseItemSchema.extend({
1212
+ type: z.literal("appEvaluationScan"),
1213
+ projectId: z.string().min(1),
1214
+ appEvaluationScanId: z.string().min(1),
1215
+ hourBucket: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:00:00\.000Z$/),
1216
+ source: z.enum(["runner", "manual"]),
1217
+ status: appEvaluationScanStatusSchema,
1218
+ baselineVersion: z.string().trim().min(1).max(80).default("amistio-app-evaluation-v1"),
1219
+ workItemId: z.string().min(1).optional(),
1220
+ repositoryLinkId: z.string().min(1).optional(),
1221
+ runnerId: z.string().min(1).optional(),
1222
+ findingIds: z.array(z.string().min(1)).default([]),
1223
+ summary: z.string().trim().min(1).max(2e3).optional(),
1224
+ startedAt: isoDateTimeSchema.optional(),
1225
+ completedAt: isoDateTimeSchema.optional(),
1226
+ failedAt: isoDateTimeSchema.optional(),
1227
+ error: z.string().max(1e3).optional()
1228
+ });
1229
+ var appEvaluationFindingItemSchema = baseItemSchema.extend({
1230
+ type: z.literal("appEvaluationFinding"),
1231
+ projectId: z.string().min(1),
1232
+ appEvaluationFindingId: z.string().min(1),
1233
+ appEvaluationScanId: z.string().min(1),
1234
+ title: z.string().trim().min(1).max(200),
1235
+ category: appEvaluationFindingCategorySchema,
1236
+ severity: appEvaluationFindingSeveritySchema,
1237
+ confidence: appEvaluationFindingConfidenceSchema.default("medium"),
1238
+ status: appEvaluationFindingStatusSchema,
1239
+ approvalState: appEvaluationApprovalStateSchema.default("proposed"),
1240
+ summary: z.string().trim().min(1).max(2e3),
1241
+ affectedSurfaces: z.array(z.string().trim().min(1).max(300)).default([]),
1242
+ evidence: z.array(z.string().trim().min(1).max(600)).default([]),
1243
+ safePaths: z.array(z.string().trim().min(1).max(300)).default([]),
1244
+ suggestedAction: z.string().trim().min(1).max(4e3),
1245
+ verificationPlan: z.array(z.string().trim().min(1).max(300)).min(1),
1246
+ proposedLifecycleAction: appEvaluationPlanLifecycleActionSchema.default("none"),
1247
+ relatedArtifactIds: z.array(z.string().trim().min(1).max(160)).default([]),
1248
+ proposedPlanDocumentId: z.string().min(1).optional(),
1249
+ implementationWorkItemId: z.string().min(1).optional(),
1250
+ reviewNotes: z.string().trim().min(1).optional(),
1251
+ reviewedByUserId: z.string().min(1).optional(),
1252
+ reviewedAt: isoDateTimeSchema.optional(),
1253
+ resolvedAt: isoDateTimeSchema.optional(),
1254
+ lastReviewAction: z.enum(["approve", "requestChanges", "dismiss", "acceptRisk", "reopen"]).optional()
1255
+ });
897
1256
  var activityEventItemSchema = baseItemSchema.extend({
898
1257
  type: z.literal("activityEvent"),
899
1258
  projectId: z.string().min(1),
@@ -911,6 +1270,11 @@ var activityEventItemSchema = baseItemSchema.extend({
911
1270
  relatedIssueId: z.string().min(1).optional(),
912
1271
  relatedSecurityScanId: z.string().min(1).optional(),
913
1272
  relatedSecurityFindingId: z.string().min(1).optional(),
1273
+ relatedAppEvaluationScanId: z.string().min(1).optional(),
1274
+ relatedAppEvaluationFindingId: z.string().min(1).optional(),
1275
+ relatedProjectContextRefreshId: z.string().min(1).optional(),
1276
+ relatedProjectSystemDiagramId: z.string().min(1).optional(),
1277
+ relatedContextMissId: z.string().min(1).optional(),
914
1278
  generatedDraftId: z.string().min(1).optional(),
915
1279
  runnerId: z.string().min(1).optional(),
916
1280
  repositoryLinkId: z.string().min(1).optional(),
@@ -971,6 +1335,12 @@ var projectItemUnionSchema = z.discriminatedUnion("type", [
971
1335
  securityScanItemSchema,
972
1336
  securityFindingItemSchema,
973
1337
  securityPostureSnapshotItemSchema,
1338
+ appEvaluationScanItemSchema,
1339
+ appEvaluationFindingItemSchema,
1340
+ projectContextMapItemSchema,
1341
+ projectContextRefreshItemSchema,
1342
+ projectSystemDiagramItemSchema,
1343
+ contextMissItemSchema,
974
1344
  syncCursorItemSchema,
975
1345
  syncConflictItemSchema,
976
1346
  workItemSchema,
@@ -1010,16 +1380,43 @@ function isOfficialAmistioApiUrl(value) {
1010
1380
 
1011
1381
  // ../shared/src/brain-documents.ts
1012
1382
  var controlPlaneRoots = /* @__PURE__ */ new Set(["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"]);
1383
+ var documentTypeFolders = {
1384
+ architecture: "architecture",
1385
+ context: "context",
1386
+ decision: "decisions",
1387
+ feature: "features",
1388
+ memory: "memory",
1389
+ plan: "plans",
1390
+ prompt: "prompts",
1391
+ workflow: "workflows"
1392
+ };
1393
+ var documentTypeByFolder = Object.fromEntries(Object.entries(documentTypeFolders).map(([documentType, folder]) => [folder, documentType]));
1013
1394
  function isControlPlaneTemplateRepoPath(repoPath) {
1014
1395
  const normalized = normalizeRepoPath(repoPath);
1015
1396
  const segments = normalized.split("/").filter(Boolean);
1016
- const root = segments[0] === "docs" ? segments[1] : segments[0];
1397
+ const root = controlPlaneRootFromSegments(segments);
1017
1398
  const basename = segments[segments.length - 1]?.toLowerCase();
1018
1399
  return Boolean(root && controlPlaneRoots.has(root) && (basename === "_template.md" || basename === "_template.mdx"));
1019
1400
  }
1401
+ function documentTypeForBrainRepoPath(repoPath) {
1402
+ const normalized = normalizeRepoPath(repoPath);
1403
+ const segments = normalized.split("/").filter(Boolean);
1404
+ const root = controlPlaneRootFromSegments(segments);
1405
+ return root ? documentTypeByFolder[root] : void 0;
1406
+ }
1407
+ function inferContentFormatFromRepoPath(repoPath) {
1408
+ if (/\.html?$/i.test(repoPath)) return "html";
1409
+ if (/\.mdx?$/i.test(repoPath)) return "markdown";
1410
+ return void 0;
1411
+ }
1020
1412
  function normalizeRepoPath(repoPath) {
1021
1413
  return repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
1022
1414
  }
1415
+ function controlPlaneRootFromSegments(segments) {
1416
+ if (segments[0] === "docs" && segments[1] === "html") return segments[2];
1417
+ if (segments[0] === "docs") return segments[1];
1418
+ return segments[0];
1419
+ }
1023
1420
 
1024
1421
  // ../shared/src/repo-metadata.ts
1025
1422
  import { z as z2 } from "zod";
@@ -1033,6 +1430,7 @@ var repoLinkMetadataSchema = z2.object({
1033
1430
  var syncedDocumentFrontmatterSchema = z2.object({
1034
1431
  amistioDocumentId: z2.string().min(1),
1035
1432
  amistioDocumentType: z2.string().min(1),
1433
+ amistioContentFormat: z2.enum(["markdown", "html"]).default("markdown"),
1036
1434
  amistioRevision: z2.coerce.number().int().nonnegative(),
1037
1435
  amistioContentHash: z2.string().min(1),
1038
1436
  status: z2.string().optional()
@@ -1183,6 +1581,7 @@ function decideSyncState(state) {
1183
1581
 
1184
1582
  // ../shared/src/next-action.ts
1185
1583
  var nextActionRunnerHeartbeatFreshMs = 15 * 60 * 1e3;
1584
+ var nextActionCompletedWorkFreshMs = 60 * 60 * 1e3;
1186
1585
  function computeProjectNextAction(input) {
1187
1586
  const nowMs = input.nowMs ?? Date.now();
1188
1587
  if (!input.projectId) {
@@ -1280,7 +1679,7 @@ function computeProjectNextAction(input) {
1280
1679
  if (!readiness.ready) {
1281
1680
  return runnerWaitAction(readiness, "Runner setup needed");
1282
1681
  }
1283
- const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed"));
1682
+ const completedWork = latestWorkItem(input.workItems.filter((item) => item.status === "completed" && isFreshCompletedWork(item, nowMs)));
1284
1683
  if (completedWork) {
1285
1684
  return workAction(completedWork, "workCompleted", "none", "success", "Work completed", completedWork.lastStatusMessage ?? "The latest runner work is complete.");
1286
1685
  }
@@ -1374,6 +1773,10 @@ function workAction(workItem, kind, actor, tone, title, message) {
1374
1773
  function latestWorkItem(workItems) {
1375
1774
  return [...workItems].sort((first, second) => Date.parse(second.updatedAt) - Date.parse(first.updatedAt))[0];
1376
1775
  }
1776
+ function isFreshCompletedWork(workItem, nowMs) {
1777
+ const timestamp = Date.parse(workItem.lastStatusAt ?? workItem.updatedAt);
1778
+ return timestamp > 0 && nowMs - timestamp <= nextActionCompletedWorkFreshMs;
1779
+ }
1377
1780
  function latestTimestamp(values) {
1378
1781
  return values.sort((first, second) => Date.parse(second) - Date.parse(first))[0];
1379
1782
  }
@@ -1715,6 +2118,27 @@ var ApiClient = class {
1715
2118
  { method: "GET" }
1716
2119
  );
1717
2120
  }
2121
+ async listProjectContext(projectId) {
2122
+ return this.request(
2123
+ `/projects/${projectId}/project-context`,
2124
+ z3.object({
2125
+ maps: z3.array(projectContextMapItemSchema),
2126
+ refreshes: z3.array(projectContextRefreshItemSchema),
2127
+ misses: z3.array(contextMissItemSchema),
2128
+ activeMap: projectContextMapItemSchema.optional()
2129
+ }),
2130
+ { method: "GET" }
2131
+ );
2132
+ }
2133
+ async buildProjectContextPack(projectId, query, workItemId) {
2134
+ const params = new URLSearchParams({ packQuery: query });
2135
+ if (workItemId) params.set("workItemId", workItemId);
2136
+ return this.request(
2137
+ `/projects/${projectId}/project-context?${params.toString()}`,
2138
+ projectContextPackSchema,
2139
+ { method: "GET" }
2140
+ );
2141
+ }
1718
2142
  async pushBrainDocuments(projectId, documents) {
1719
2143
  return this.request(
1720
2144
  `/projects/${projectId}/brain-documents`,
@@ -1892,6 +2316,26 @@ var ApiClient = class {
1892
2316
  }
1893
2317
  );
1894
2318
  }
2319
+ async submitAppEvaluationScanResult(projectId, workItemId, result) {
2320
+ return this.request(
2321
+ `/projects/${projectId}/work-items/${workItemId}/app-evaluation-result`,
2322
+ z3.object({ scan: appEvaluationScanItemSchema, findings: z3.array(appEvaluationFindingItemSchema), workItem: workItemSchema }),
2323
+ {
2324
+ method: "POST",
2325
+ body: JSON.stringify(result)
2326
+ }
2327
+ );
2328
+ }
2329
+ async submitProjectContextRefreshResult(projectId, workItemId, result) {
2330
+ return this.request(
2331
+ `/projects/${projectId}/work-items/${workItemId}/project-context-result`,
2332
+ z3.object({ refresh: projectContextRefreshItemSchema, map: projectContextMapItemSchema.optional(), workItem: workItemSchema }),
2333
+ {
2334
+ method: "POST",
2335
+ body: JSON.stringify(result)
2336
+ }
2337
+ );
2338
+ }
1895
2339
  async request(urlPath, schema, init) {
1896
2340
  const response = await fetch(resolveApiUrl(this.options.apiUrl, urlPath), {
1897
2341
  ...init,
@@ -1938,8 +2382,8 @@ var toolSessionMutationSchema = z3.object({
1938
2382
  });
1939
2383
  function resolveApiUrl(apiUrl, urlPath) {
1940
2384
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
1941
- const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1942
- return new URL(`${base}${path14}`);
2385
+ const path15 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
2386
+ return new URL(`${base}${path15}`);
1943
2387
  }
1944
2388
 
1945
2389
  // src/orchestrator.ts
@@ -2831,6 +3275,15 @@ function currentRunnerMode() {
2831
3275
  function defaultRunnerMetadataDir() {
2832
3276
  return path6.join(os3.homedir(), ".config", "amistio", "runners");
2833
3277
  }
3278
+ function updatedCliRunnerLaunchOptions() {
3279
+ return { executablePath: "amistio", directExecutable: true };
3280
+ }
3281
+ function resolveRunnerDaemonLaunch(input) {
3282
+ if (input.directExecutable) {
3283
+ return { executablePath: input.executablePath ?? "amistio", args: input.args };
3284
+ }
3285
+ return { executablePath: input.executablePath ?? process.execPath, args: [input.scriptPath ?? process.argv[1], ...input.args] };
3286
+ }
2834
3287
  async function startRunnerDaemon(input) {
2835
3288
  const metadataDir = input.metadataDir ?? defaultRunnerMetadataDir();
2836
3289
  const existing = await readRunnerDaemonMetadata(input, metadataDir);
@@ -2840,7 +3293,12 @@ async function startRunnerDaemon(input) {
2840
3293
  await mkdir5(metadataDir, { recursive: true });
2841
3294
  const logPath = path6.join(metadataDir, `${runnerDaemonKey(input)}.log`);
2842
3295
  const logFd = openSync(logPath, "a");
2843
- const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...input.args], {
3296
+ const launch = resolveRunnerDaemonLaunch({
3297
+ args: input.args,
3298
+ ...input.executablePath ? { executablePath: input.executablePath } : {},
3299
+ ...input.scriptPath ? { scriptPath: input.scriptPath } : {}
3300
+ });
3301
+ const child = spawn2(launch.executablePath, launch.args, {
2844
3302
  cwd: input.rootDir,
2845
3303
  detached: true,
2846
3304
  env: {
@@ -2877,7 +3335,13 @@ async function restartRunnerDaemonProcess(metadata, args, input = {}) {
2877
3335
  await mkdir5(metadataDir, { recursive: true });
2878
3336
  const logPath = metadata.logPath ?? path6.join(metadataDir, `${runnerDaemonKey(metadata)}.log`);
2879
3337
  const logFd = openSync(logPath, "a");
2880
- const child = spawn2(input.executablePath ?? process.execPath, [input.scriptPath ?? process.argv[1], ...args], {
3338
+ const launch = resolveRunnerDaemonLaunch({
3339
+ args,
3340
+ ...input.executablePath ? { executablePath: input.executablePath } : {},
3341
+ ...input.scriptPath ? { scriptPath: input.scriptPath } : {},
3342
+ ...input.directExecutable ? { directExecutable: true } : {}
3343
+ });
3344
+ const child = spawn2(launch.executablePath, launch.args, {
2881
3345
  cwd: metadata.rootDir,
2882
3346
  detached: true,
2883
3347
  env: {
@@ -3356,6 +3820,7 @@ import { promisify as promisify3 } from "node:util";
3356
3820
  var execFileAsync3 = promisify3(execFile3);
3357
3821
  var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
3358
3822
  var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
3823
+ var htmlSyncRoot = "docs/html";
3359
3824
  var documentTypeByRoot = {
3360
3825
  architecture: "architecture",
3361
3826
  context: "context",
@@ -3443,15 +3908,15 @@ async function collectSyncStatus(rootDir, webDocuments = []) {
3443
3908
  return { status, items, counts };
3444
3909
  }
3445
3910
  async function readLocalSyncedDocuments(rootDir) {
3446
- const markdownFiles = await findMarkdownFiles(rootDir);
3911
+ const documentFiles = await findBrainDocumentFiles(rootDir);
3447
3912
  const documents = [];
3448
- for (const fullPath of markdownFiles) {
3913
+ for (const fullPath of documentFiles) {
3449
3914
  const raw = await readFile6(fullPath, "utf8");
3450
- const parsed = parseSyncedMarkdown(raw);
3915
+ const repoPath = toRepoPath(rootDir, fullPath);
3916
+ const parsed = parseSyncedDocument(raw, repoPath);
3451
3917
  if (!parsed) {
3452
3918
  continue;
3453
3919
  }
3454
- const repoPath = toRepoPath(rootDir, fullPath);
3455
3920
  if (isControlPlaneTemplateRepoPath(repoPath)) {
3456
3921
  continue;
3457
3922
  }
@@ -3497,7 +3962,7 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
3497
3962
  continue;
3498
3963
  }
3499
3964
  await mkdir7(path8.dirname(fullPath), { recursive: true });
3500
- await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
3965
+ await writeFile7(fullPath, createSyncedDocumentContent(document), "utf8");
3501
3966
  result.written.push(document.repoPath);
3502
3967
  }
3503
3968
  return result;
@@ -3515,6 +3980,7 @@ async function collectDirtyDocumentsForPush(rootDir, metadata) {
3515
3980
  projectId: metadata.amistioProjectId,
3516
3981
  documentId: document.frontmatter.amistioDocumentId,
3517
3982
  documentType,
3983
+ contentFormat: document.frontmatter.amistioContentFormat,
3518
3984
  title: inferTitle(document.content, document.repoPath),
3519
3985
  status: "reviewing",
3520
3986
  repoPath: document.repoPath,
@@ -3552,11 +4018,15 @@ function autoSyncSkipCounts(skipped) {
3552
4018
  unreadable: skipped.filter((item) => item.reason === "unreadable").length
3553
4019
  };
3554
4020
  }
3555
- function createSyncedDocumentMarkdown(document) {
4021
+ function createSyncedDocumentContent(document) {
4022
+ return (document.contentFormat ?? inferContentFormatFromRepoPath(document.repoPath) ?? "markdown") === "html" ? createSyncedDocumentHtml(document) : createSyncedDocumentMarkdownContent(document);
4023
+ }
4024
+ function createSyncedDocumentMarkdownContent(document) {
3556
4025
  return [
3557
4026
  "---",
3558
4027
  `amistioDocumentId: ${document.documentId}`,
3559
4028
  `amistioDocumentType: ${document.documentType}`,
4029
+ `amistioContentFormat: ${document.contentFormat ?? "markdown"}`,
3560
4030
  `amistioRevision: ${document.revision}`,
3561
4031
  `amistioContentHash: ${document.contentHash}`,
3562
4032
  `status: ${document.status}`,
@@ -3565,6 +4035,24 @@ function createSyncedDocumentMarkdown(document) {
3565
4035
  ""
3566
4036
  ].join("\n");
3567
4037
  }
4038
+ function createSyncedDocumentHtml(document) {
4039
+ return [
4040
+ "<!--",
4041
+ `amistioDocumentId: ${document.documentId}`,
4042
+ `amistioDocumentType: ${document.documentType}`,
4043
+ "amistioContentFormat: html",
4044
+ `amistioRevision: ${document.revision}`,
4045
+ `amistioContentHash: ${document.contentHash}`,
4046
+ `status: ${document.status}`,
4047
+ "-->",
4048
+ document.content,
4049
+ ""
4050
+ ].join("\n");
4051
+ }
4052
+ function parseSyncedDocument(content, repoPath) {
4053
+ const format = inferContentFormatFromRepoPath(repoPath);
4054
+ return format === "html" ? parseSyncedHtml(content) : parseSyncedMarkdown(content);
4055
+ }
3568
4056
  function parseSyncedMarkdown(content) {
3569
4057
  const rawFrontmatter = parseFrontmatter(content);
3570
4058
  const frontmatter = syncedDocumentFrontmatterSchema.safeParse(rawFrontmatter);
@@ -3579,10 +4067,32 @@ function parseSyncedMarkdown(content) {
3579
4067
  const bodyStart = closingLineEnd === -1 ? content.length : closingLineEnd + 1;
3580
4068
  return { frontmatter: frontmatter.data, content: content.slice(bodyStart).replace(/\n$/, "") };
3581
4069
  }
4070
+ function parseSyncedHtml(content) {
4071
+ const trimmedStart = content.trimStart();
4072
+ if (!trimmedStart.startsWith("<!--")) {
4073
+ return void 0;
4074
+ }
4075
+ const startOffset = content.indexOf("<!--");
4076
+ const closingMarker = content.indexOf("-->", startOffset + 4);
4077
+ if (closingMarker === -1) {
4078
+ return void 0;
4079
+ }
4080
+ const metadataLines = content.slice(startOffset + 4, closingMarker).split(/\r?\n/);
4081
+ const metadata = Object.fromEntries(metadataLines.map((line) => {
4082
+ const separator = line.indexOf(":");
4083
+ return separator === -1 ? void 0 : [line.slice(0, separator).trim(), line.slice(separator + 1).trim()];
4084
+ }).filter((entry) => Boolean(entry?.[0])));
4085
+ const frontmatter = syncedDocumentFrontmatterSchema.safeParse({ ...metadata, amistioContentFormat: "html" });
4086
+ if (!frontmatter.success) {
4087
+ return void 0;
4088
+ }
4089
+ const bodyStart = content.indexOf("\n", closingMarker + 3);
4090
+ return { frontmatter: frontmatter.data, content: content.slice(bodyStart === -1 ? closingMarker + 3 : bodyStart + 1).replace(/\n$/, "") };
4091
+ }
3582
4092
  async function readExistingSyncedDocument(fullPath) {
3583
4093
  try {
3584
4094
  const raw = await readFile6(fullPath, "utf8");
3585
- const parsed = parseSyncedMarkdown(raw);
4095
+ const parsed = parseSyncedDocument(raw, fullPath);
3586
4096
  if (!parsed) {
3587
4097
  return { exists: true };
3588
4098
  }
@@ -3603,23 +4113,23 @@ async function readExistingSyncedDocument(fullPath) {
3603
4113
  throw error;
3604
4114
  }
3605
4115
  }
3606
- async function findMarkdownFiles(rootDir) {
4116
+ async function findBrainDocumentFiles(rootDir) {
3607
4117
  const files = [];
3608
- for (const syncRoot of syncRoots) {
4118
+ for (const syncRoot of [...syncRoots, htmlSyncRoot]) {
3609
4119
  const fullRoot = path8.join(rootDir, syncRoot);
3610
4120
  if (!await exists2(fullRoot)) {
3611
4121
  continue;
3612
4122
  }
3613
- await walkMarkdownFiles(fullRoot, files);
4123
+ await walkBrainDocumentFiles(fullRoot, files);
3614
4124
  }
3615
4125
  return files;
3616
4126
  }
3617
- async function walkMarkdownFiles(directory, files) {
4127
+ async function walkBrainDocumentFiles(directory, files) {
3618
4128
  for (const entry of await readdir3(directory, { withFileTypes: true })) {
3619
4129
  const fullPath = path8.join(directory, entry.name);
3620
4130
  if (entry.isDirectory()) {
3621
- await walkMarkdownFiles(fullPath, files);
3622
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
4131
+ await walkBrainDocumentFiles(fullPath, files);
4132
+ } else if (entry.isFile() && /\.(md|mdx|html?)$/i.test(entry.name)) {
3623
4133
  files.push(fullPath);
3624
4134
  }
3625
4135
  }
@@ -3641,7 +4151,7 @@ function safeRepoPath(rootDir, repoPath) {
3641
4151
  }
3642
4152
  function isControlPlanePath(repoPath) {
3643
4153
  const normalized = path8.normalize(repoPath);
3644
- return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
4154
+ return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`)) || normalized === htmlSyncRoot || normalized.startsWith(`${htmlSyncRoot}${path8.sep}`);
3645
4155
  }
3646
4156
  function canonicalControlPlaneRepoPath(repoPath) {
3647
4157
  const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -3656,7 +4166,9 @@ function toRepoPath(rootDir, fullPath) {
3656
4166
  }
3657
4167
  function inferTitle(content, repoPath) {
3658
4168
  const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
3659
- return heading || path8.basename(repoPath, path8.extname(repoPath));
4169
+ if (heading) return heading;
4170
+ const htmlHeading = content.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
4171
+ return htmlHeading || path8.basename(repoPath, path8.extname(repoPath));
3660
4172
  }
3661
4173
  async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingDocuments, options) {
3662
4174
  const root = path8.resolve(rootDir);
@@ -3689,7 +4201,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
3689
4201
  skipped.push({ repoPath: normalizedRepoPath, reason: "unreadable" });
3690
4202
  continue;
3691
4203
  }
3692
- if (parseSyncedMarkdown(content)) {
4204
+ if (parseSyncedDocument(content, normalizedRepoPath)) {
3693
4205
  skipped.push({ repoPath: normalizedRepoPath, reason: "alreadyManaged" });
3694
4206
  continue;
3695
4207
  }
@@ -3719,6 +4231,7 @@ async function collectExternalBrainDocumentsForPush(rootDir, metadata, existingD
3719
4231
  projectId: metadata.amistioProjectId,
3720
4232
  documentId,
3721
4233
  documentType,
4234
+ contentFormat: inferContentFormatFromRepoPath(canonicalRepoPath) ?? "markdown",
3722
4235
  title: inferTitle(content, canonicalRepoPath),
3723
4236
  status: "reviewing",
3724
4237
  repoPath: canonicalRepoPath,
@@ -3747,7 +4260,7 @@ async function listAutoSyncCandidatePaths(rootDir) {
3747
4260
  return uniqueSortedRepoPaths(gitFiles.split("\n").map(normalizeRepoPath3).filter((repoPath) => repoPath && isRecognizedBrainRepoPath(repoPath)));
3748
4261
  }
3749
4262
  const files = [];
3750
- for (const syncRoot of [...syncRoots, ...legacySyncRoots]) {
4263
+ for (const syncRoot of [...syncRoots, htmlSyncRoot, ...legacySyncRoots]) {
3751
4264
  const fullRoot = path8.join(rootDir, syncRoot);
3752
4265
  if (await exists2(fullRoot)) {
3753
4266
  await walkAutoSyncFiles(rootDir, fullRoot, files);
@@ -3771,7 +4284,7 @@ async function walkAutoSyncFiles(rootDir, directory, files) {
3771
4284
  function autoSyncPathSkipReason(repoPath) {
3772
4285
  if (autoSyncMetadataPaths.has(repoPath)) return "metadata";
3773
4286
  if (isControlPlaneTemplateRepoPath(repoPath)) return "template";
3774
- if (!/\.(md|mdx)$/i.test(repoPath)) return "unsupported";
4287
+ if (!/\.(md|mdx|html?)$/i.test(repoPath)) return "unsupported";
3775
4288
  const basename = repoPath.split("/").at(-1)?.toLowerCase() ?? "";
3776
4289
  if (basename.startsWith(".") || basename.endsWith(".lock") || basename.includes(".env") || basename.includes("secret") || basename.includes("credential")) return "excluded";
3777
4290
  if (repoPath.split("/").some((segment) => autoSyncExcludedDirectoryNames.has(segment) || autoSyncGeneratedPathSegments.has(segment))) return "excluded";
@@ -3779,10 +4292,13 @@ function autoSyncPathSkipReason(repoPath) {
3779
4292
  }
3780
4293
  function isRecognizedBrainRepoPath(repoPath) {
3781
4294
  const normalized = normalizeRepoPath3(repoPath);
3782
- const [firstSegment, secondSegment] = normalized.split("/");
3783
- return Boolean(firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
4295
+ const [firstSegment, secondSegment, thirdSegment] = normalized.split("/");
4296
+ return Boolean(firstSegment === "docs" && secondSegment === "html" && thirdSegment && legacySyncRoots.includes(thirdSegment) || firstSegment === "docs" && secondSegment && legacySyncRoots.includes(secondSegment) || firstSegment && legacySyncRoots.includes(firstSegment));
3784
4297
  }
3785
4298
  function documentTypeForRepoPath(repoPath) {
4299
+ return documentTypeForBrainRepoPath(repoPath) ?? legacyDocumentTypeForRepoPath(repoPath);
4300
+ }
4301
+ function legacyDocumentTypeForRepoPath(repoPath) {
3786
4302
  const normalized = normalizeRepoPath3(repoPath);
3787
4303
  const segments = normalized.split("/");
3788
4304
  const root = segments[0] === "docs" ? segments[1] : segments[0];
@@ -3801,6 +4317,7 @@ function parseFrontmatterFromSyncedDocument(frontmatter) {
3801
4317
  return {
3802
4318
  amistioDocumentId: frontmatter.amistioDocumentId,
3803
4319
  amistioDocumentType: frontmatter.amistioDocumentType,
4320
+ amistioContentFormat: frontmatter.amistioContentFormat,
3804
4321
  amistioRevision: frontmatter.amistioRevision,
3805
4322
  amistioContentHash: frontmatter.amistioContentHash,
3806
4323
  ...frontmatter.status ? { status: frontmatter.status } : {}
@@ -3864,6 +4381,10 @@ var issueDiagnosisStart = "AMISTIO_ISSUE_DIAGNOSIS_START";
3864
4381
  var issueDiagnosisEnd = "AMISTIO_ISSUE_DIAGNOSIS_END";
3865
4382
  var securityPostureStart = "AMISTIO_SECURITY_POSTURE_START";
3866
4383
  var securityPostureEnd = "AMISTIO_SECURITY_POSTURE_END";
4384
+ var appEvaluationStart = "AMISTIO_APP_EVALUATION_START";
4385
+ var appEvaluationEnd = "AMISTIO_APP_EVALUATION_END";
4386
+ var projectContextRefreshStart = "AMISTIO_PROJECT_CONTEXT_REFRESH_START";
4387
+ var projectContextRefreshEnd = "AMISTIO_PROJECT_CONTEXT_REFRESH_END";
3867
4388
  function createWorkExecutionPrompt(workItem, context) {
3868
4389
  if (workItem.workKind === "brainGeneration") {
3869
4390
  return createBrainGenerationPrompt(workItem);
@@ -3883,6 +4404,12 @@ function createWorkExecutionPrompt(workItem, context) {
3883
4404
  if (workItem.workKind === "securityPostureScan") {
3884
4405
  return createSecurityPostureScanPrompt(workItem, context?.securityPostureScan);
3885
4406
  }
4407
+ if (workItem.workKind === "appEvaluationScan") {
4408
+ return createAppEvaluationScanPrompt(workItem, context?.appEvaluationScan);
4409
+ }
4410
+ if (workItem.workKind === "projectContextRefresh") {
4411
+ return createProjectContextRefreshPrompt(workItem, context?.projectContextRefresh);
4412
+ }
3886
4413
  return [
3887
4414
  "# Amistio Work Execution",
3888
4415
  "",
@@ -3911,6 +4438,77 @@ function createWorkExecutionPrompt(workItem, context) {
3911
4438
  "- Run relevant verification commands when feasible and summarize results."
3912
4439
  ].join("\n");
3913
4440
  }
4441
+ function createProjectContextRefreshPrompt(workItem, context) {
4442
+ const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 24).map((document) => [
4443
+ `### ${document.title}`,
4444
+ `documentId: ${document.documentId}`,
4445
+ `documentType: ${document.documentType}`,
4446
+ `repoPath: ${document.repoPath}`,
4447
+ `revision: ${document.revision}`,
4448
+ document.content.slice(0, 3e3)
4449
+ ].join("\n")).join("\n\n");
4450
+ const activeMap = context?.activeMap;
4451
+ const previousMap = activeMap ? [
4452
+ `Map ID: ${activeMap.projectContextMapId}`,
4453
+ `Version: ${activeMap.version}`,
4454
+ `Status: ${activeMap.status}`,
4455
+ `Coverage: ${activeMap.coverage.status}`,
4456
+ `Summary: ${activeMap.summary}`,
4457
+ "Slices:",
4458
+ ...activeMap.slices.slice(0, 20).map((slice) => `- ${slice.sliceId} / ${slice.kind} / ${slice.freshness}: ${slice.title} - ${slice.summary.slice(0, 500)}`)
4459
+ ].join("\n") : "No approved project context map was loaded. Build the initial map.";
4460
+ return [
4461
+ "# Amistio Living Project Context Refresh",
4462
+ "",
4463
+ "You are running locally through the Amistio CLI inside the user's repository.",
4464
+ "Run a read-only project context refresh that captures the whole app as bounded, reviewable knowledge.",
4465
+ "Do not modify files, create branches, commit, install packages, run implementation prompts, call external services, or make unaudited network calls.",
4466
+ "Use only safe local inspection and focused diagnostic commands that do not expose secrets.",
4467
+ "",
4468
+ "## Work Item",
4469
+ "",
4470
+ `Title: ${workItem.title}`,
4471
+ `Work item ID: ${workItem.workItemId}`,
4472
+ `Project ID: ${workItem.projectId}`,
4473
+ `Project context refresh ID: ${workItem.projectContextRefreshId ?? "unknown"}`,
4474
+ "",
4475
+ "## Refresh Request",
4476
+ "",
4477
+ workItem.sourceWish ?? "Refresh the living project context map.",
4478
+ "",
4479
+ "## Approved Project Brain Context",
4480
+ "",
4481
+ approvedContext || "No approved project-brain records were loaded. Inspect local repository files as needed and explain the gap in the coverage warnings.",
4482
+ "",
4483
+ "## Previous Context Map",
4484
+ "",
4485
+ previousMap,
4486
+ "",
4487
+ "## Mapping Requirements",
4488
+ "",
4489
+ "- Create slices for architecture, domain, data, API, frontend, backend, CLI, workflows, operations, security, and testing when those surfaces exist.",
4490
+ "- Capture entities and relations that explain how the app is put together and where future work should look first.",
4491
+ "- Prefer summaries, repository-relative paths, short citations, tags, and freshness status over raw source excerpts.",
4492
+ "- Mark stale or missing areas explicitly instead of guessing.",
4493
+ "- Keep repoPaths repository-relative; never include /absolute paths or ../ traversal.",
4494
+ "",
4495
+ "## Data Safety",
4496
+ "",
4497
+ "- Persist summaries, entities, relations, citations, and repository-relative paths only.",
4498
+ "- Do not include raw source dumps, secrets, env vars, process lists, absolute local paths, credential values, tokens, provider sessions, or destructive shell output.",
4499
+ "- Citation excerpts must be short evidence snippets, not code payloads.",
4500
+ "",
4501
+ "## Output Contract",
4502
+ "",
4503
+ "Print exactly one JSON object between the markers below. The CLI will submit only this structured refresh result back to Amistio.",
4504
+ "",
4505
+ projectContextRefreshStart,
4506
+ '{"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"]}',
4507
+ projectContextRefreshEnd,
4508
+ "",
4509
+ "Do not put Markdown fences around the markers. Do not implement any changes."
4510
+ ].join("\n");
4511
+ }
3914
4512
  function createSecurityPostureScanPrompt(workItem, context) {
3915
4513
  const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 20).map((document) => [
3916
4514
  `### ${document.title}`,
@@ -3968,6 +4566,63 @@ function createSecurityPostureScanPrompt(workItem, context) {
3968
4566
  "Do not put Markdown fences around the markers. Do not implement remediation."
3969
4567
  ].join("\n");
3970
4568
  }
4569
+ function createAppEvaluationScanPrompt(workItem, context) {
4570
+ const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 24).map((document) => [
4571
+ `### ${document.title}`,
4572
+ `documentId: ${document.documentId}`,
4573
+ `documentType: ${document.documentType}`,
4574
+ `repoPath: ${document.repoPath}`,
4575
+ `revision: ${document.revision}`,
4576
+ document.content.slice(0, 3e3)
4577
+ ].join("\n")).join("\n\n");
4578
+ return [
4579
+ "# Amistio App Evaluation Scan",
4580
+ "",
4581
+ "You are running locally through the Amistio CLI inside the user's repository.",
4582
+ "Run a read-only app evaluation scan and produce approval-gated improvement and cleanup findings.",
4583
+ "Do not modify files, create branches, commit, install packages, run implementation prompts, delete or archive plans, call external services, or make unaudited network calls.",
4584
+ "Use only safe local inspection and focused diagnostic commands that do not expose secrets.",
4585
+ "",
4586
+ "## Work Item",
4587
+ "",
4588
+ `Title: ${workItem.title}`,
4589
+ `Work item ID: ${workItem.workItemId}`,
4590
+ `Project ID: ${workItem.projectId}`,
4591
+ `App evaluation scan ID: ${workItem.appEvaluationScanId ?? "unknown"}`,
4592
+ "",
4593
+ "## Scan Request",
4594
+ "",
4595
+ workItem.sourceWish ?? "Run the Amistio app evaluation baseline.",
4596
+ "",
4597
+ "## Approved Project Brain Context",
4598
+ "",
4599
+ approvedContext || "No approved project-brain records were loaded. Inspect local repository files as needed and explain the gap in the summary.",
4600
+ "",
4601
+ "## Baseline Requirements",
4602
+ "",
4603
+ "- Check app verification health: lint, typecheck, test, build, CI references, flaky or missing gates.",
4604
+ "- Check product and docs drift between approved context, plans, prompts, and current implementation.",
4605
+ "- Check old plans and prompts for cleanup candidates, but only propose lifecycle actions such as markCompleted, markSuperseded, archive, keepActive, or none.",
4606
+ "- Check missing memory or workflow updates when repeated lessons or operational rules are visible.",
4607
+ "- Check release readiness, UX, accessibility, performance, reliability, and security-posture follow-through at a summary level.",
4608
+ "",
4609
+ "## Data Safety",
4610
+ "",
4611
+ "- Persist summaries, recommended actions, repository-relative paths, and redacted evidence only.",
4612
+ "- Do not include raw source dumps, secrets, env vars, process lists, absolute local paths, credential values, tokens, provider sessions, or destructive shell output.",
4613
+ "- Use safePaths for repo-relative paths only; never include /absolute paths or ../ traversal.",
4614
+ "",
4615
+ "## Output Contract",
4616
+ "",
4617
+ "Print exactly one JSON object between the markers below. The CLI will submit only this structured scan result back to Amistio.",
4618
+ "",
4619
+ appEvaluationStart,
4620
+ '{"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"]}',
4621
+ appEvaluationEnd,
4622
+ "",
4623
+ "Do not put Markdown fences around the markers. Do not implement improvements or cleanup."
4624
+ ].join("\n");
4625
+ }
3971
4626
  function createIssueDiagnosisPrompt(workItem, context) {
3972
4627
  const issue = context?.issue;
3973
4628
  const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
@@ -4018,11 +4673,69 @@ function createIssueDiagnosisPrompt(workItem, context) {
4018
4673
  "Do not put Markdown fences around the markers. Do not implement the fix."
4019
4674
  ].join("\n");
4020
4675
  }
4021
- function createImpactPreviewPrompt(workItem, context) {
4022
- const implementationPrompt = context?.implementationPrompt;
4023
- const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
4024
- `### ${document.title}`,
4025
- `documentId: ${document.documentId}`,
4676
+ function formatProjectContextMap(map, query, contextPack) {
4677
+ if ((!map || map.status !== "approved") && !contextPack) {
4678
+ return "No approved living context map was loaded. Use approved brain records and inspect local source only where needed.";
4679
+ }
4680
+ if (!map || map.status !== "approved") {
4681
+ return [
4682
+ `Context pack ID: ${contextPack?.packId ?? "unknown"}`,
4683
+ contextPack?.freshnessWarnings.length ? `Freshness warnings: ${contextPack.freshnessWarnings.join(" | ")}` : "Freshness warnings: none recorded",
4684
+ "No approved living context map was loaded. Use approved brain records and inspect local source only where needed."
4685
+ ].join("\n");
4686
+ }
4687
+ const tokens2 = tokenizeForContext(query);
4688
+ const candidateSlices = contextPack?.slices.length ? contextPack.slices : map.slices;
4689
+ 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));
4690
+ const selectedSlices = rankedSlices.filter((entry) => entry.score > 0).map((entry) => entry.slice).slice(0, 8);
4691
+ const slices = (selectedSlices.length ? selectedSlices : candidateSlices.slice(0, 8)).map((slice) => [
4692
+ `### ${slice.title}`,
4693
+ `sliceId: ${slice.sliceId}`,
4694
+ `kind: ${slice.kind}`,
4695
+ `freshness: ${slice.freshness}`,
4696
+ `confidence: ${Math.round(slice.confidence * 100)}%`,
4697
+ slice.repoPaths.length ? `repoPaths: ${slice.repoPaths.slice(0, 12).join(", ")}` : "repoPaths: none recorded",
4698
+ slice.summary
4699
+ ].join("\n")).join("\n\n");
4700
+ const entities = (contextPack?.entities.length ? contextPack.entities : map.entities).slice(0, 12).map((entity) => `- ${entity.name} (${entity.entityType}): ${entity.summary ?? entity.entityId}`).join("\n");
4701
+ const relations = (contextPack?.relations.length ? contextPack.relations : map.relations).slice(0, 12).map((relation) => `- ${relation.fromId} ${relation.relationType} ${relation.toId}: ${relation.summary}`).join("\n");
4702
+ return [
4703
+ contextPack ? `Context pack ID: ${contextPack.packId}` : "Context pack ID: not recorded",
4704
+ `Map ID: ${map.projectContextMapId}`,
4705
+ `Version: ${map.version}`,
4706
+ contextPack?.tokenEstimate !== void 0 ? `Pack token estimate: ${contextPack.tokenEstimate}` : "Pack token estimate: not recorded",
4707
+ `Coverage: ${map.coverage.status}; slices ${map.coverage.sliceCount}; stale ${map.coverage.staleSliceCount}`,
4708
+ contextPack?.freshnessWarnings.length ? `Freshness warnings: ${contextPack.freshnessWarnings.join(" | ")}` : "Freshness warnings: none recorded",
4709
+ map.coverage.missingAreas.length ? `Missing areas: ${map.coverage.missingAreas.join(", ")}` : "Missing areas: none recorded",
4710
+ map.coverage.warnings.length ? `Warnings: ${map.coverage.warnings.join(" | ")}` : "Warnings: none recorded",
4711
+ "",
4712
+ map.summary,
4713
+ "",
4714
+ "### Relevant Context Slices",
4715
+ slices || "No slices recorded.",
4716
+ "",
4717
+ "### Entities",
4718
+ entities || "No entities recorded.",
4719
+ "",
4720
+ "### Relations",
4721
+ relations || "No relations recorded."
4722
+ ].join("\n");
4723
+ }
4724
+ function tokenizeForContext(value) {
4725
+ return [...new Set(value.toLowerCase().match(/[a-z0-9][a-z0-9_-]{2,}/g) ?? [])].slice(0, 24);
4726
+ }
4727
+ function scoreContextSlice(tokens2, title, summary, metadata) {
4728
+ const haystackTitle = title.toLowerCase();
4729
+ const haystackSummary = summary.toLowerCase();
4730
+ const haystackMetadata = metadata.toLowerCase();
4731
+ return tokens2.reduce((score, token) => score + (haystackTitle.includes(token) ? 5 : 0) + (haystackMetadata.includes(token) ? 3 : 0) + (haystackSummary.includes(token) ? 1 : 0), 0);
4732
+ }
4733
+ function createImpactPreviewPrompt(workItem, context) {
4734
+ const implementationPrompt = context?.implementationPrompt;
4735
+ const livingContext = formatProjectContextMap(context?.activeMap, workItem.sourceWish ?? workItem.title, context?.contextPack);
4736
+ const approvedContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 16).map((document) => [
4737
+ `### ${document.title}`,
4738
+ `documentId: ${document.documentId}`,
4026
4739
  `documentType: ${document.documentType}`,
4027
4740
  `repoPath: ${document.repoPath}`,
4028
4741
  `revision: ${document.revision}`,
@@ -4048,6 +4761,10 @@ function createImpactPreviewPrompt(workItem, context) {
4048
4761
  "",
4049
4762
  implementationPrompt ? implementationPrompt.content : workItem.sourceWish ?? workItem.title,
4050
4763
  "",
4764
+ "## Approved Living Project Context Map",
4765
+ "",
4766
+ livingContext,
4767
+ "",
4051
4768
  "## Approved Project Brain Context",
4052
4769
  "",
4053
4770
  approvedContext || "No approved project-brain records were loaded. Inspect the local repository and explain the gap in the report.",
@@ -4073,6 +4790,7 @@ function createImpactPreviewPrompt(workItem, context) {
4073
4790
  }
4074
4791
  function createAssistantQuestionPrompt(workItem, context) {
4075
4792
  const question = context?.question.content ?? workItem.sourceWish ?? workItem.title;
4793
+ const livingContext = formatProjectContextMap(context?.activeMap, question, context?.contextPack);
4076
4794
  const priorMessages = (context?.messages ?? []).filter((message) => message.messageId !== context?.question.messageId).slice(-12).map((message) => `- ${message.role} / ${message.status}: ${message.content}`).join("\n");
4077
4795
  const brainContext = (context?.documents ?? []).filter((document) => document.status === "approved" || document.syncState === "approved" || document.syncState === "synced").slice(0, 12).map((document) => [
4078
4796
  `### ${document.title}`,
@@ -4099,6 +4817,10 @@ function createAssistantQuestionPrompt(workItem, context) {
4099
4817
  `Project ID: ${workItem.projectId}`,
4100
4818
  `Assistant message ID: ${workItem.assistantMessageId ?? context?.question.messageId ?? "unknown"}`,
4101
4819
  "",
4820
+ "## Approved Living Project Context Map",
4821
+ "",
4822
+ livingContext,
4823
+ "",
4102
4824
  "## Approved Project Brain Context",
4103
4825
  "",
4104
4826
  brainContext || "No approved project-brain records were loaded. Say what you inspected locally and avoid pretending citations exist.",
@@ -4112,6 +4834,7 @@ function createAssistantQuestionPrompt(workItem, context) {
4112
4834
  "- Keep repository source, secrets, tokens, local credential paths, and provider session references local.",
4113
4835
  "- Summarize findings instead of dumping source code. Tiny identifiers, filenames, and short excerpts are acceptable when necessary.",
4114
4836
  '- Use sourceBoundary "projectBrain" when the answer only uses project-brain records.',
4837
+ "- Treat the approved living context map as project-brain context; use it to avoid broad repository rereads when it is fresh and relevant.",
4115
4838
  '- Use sourceBoundary "localSource" when you inspected local repository files.',
4116
4839
  '- Use sourceBoundary "mixed" when both project-brain records and local files shaped the answer.',
4117
4840
  "- Include citations for project-brain records with documentId/title/repoPath where relevant.",
@@ -4214,8 +4937,35 @@ function parseSecurityPostureScanResult(output) {
4214
4937
  const parsed = JSON.parse(stripJsonFence(payload));
4215
4938
  return securityPostureScanResultSchema.parse(parsed);
4216
4939
  }
4940
+ function parseAppEvaluationScanResult(output) {
4941
+ const start = output.indexOf(appEvaluationStart);
4942
+ const end = output.indexOf(appEvaluationEnd, start + appEvaluationStart.length);
4943
+ if (start === -1 || end === -1 || end <= start) {
4944
+ throw new Error("Local AI scan did not return an Amistio app evaluation block.");
4945
+ }
4946
+ const payload = output.slice(start + appEvaluationStart.length, end).trim();
4947
+ const parsed = JSON.parse(stripJsonFence(payload));
4948
+ return appEvaluationScanResultSchema.parse(parsed);
4949
+ }
4950
+ function parseProjectContextRefreshResult(output) {
4951
+ const start = output.indexOf(projectContextRefreshStart);
4952
+ const end = output.indexOf(projectContextRefreshEnd, start + projectContextRefreshStart.length);
4953
+ if (start === -1 || end === -1 || end <= start) {
4954
+ throw new Error("Local AI refresh did not return an Amistio project context block.");
4955
+ }
4956
+ const payload = output.slice(start + projectContextRefreshStart.length, end).trim();
4957
+ const parsed = JSON.parse(stripJsonFence(payload));
4958
+ return projectContextRefreshResultSchema.parse(parsed);
4959
+ }
4960
+ function projectContextRefreshSubmissionFailureSummary(result) {
4961
+ if (result.refresh.status !== "failed" && result.workItem.status !== "failed") {
4962
+ return void 0;
4963
+ }
4964
+ return result.refresh.error ?? result.workItem.lastStatusMessage ?? "Server rejected the project context refresh result.";
4965
+ }
4217
4966
  function createBrainGenerationPrompt(workItem) {
4218
4967
  const wish = workItem.sourceWish ?? workItem.title;
4968
+ const artifactFormatPreference = workItem.artifactFormatPreference ?? "markdown";
4219
4969
  return [
4220
4970
  "# Amistio Brain Generation",
4221
4971
  "",
@@ -4232,6 +4982,7 @@ function createBrainGenerationPrompt(workItem) {
4232
4982
  `Work item ID: ${workItem.workItemId}`,
4233
4983
  `Project ID: ${workItem.projectId}`,
4234
4984
  `Generated draft ID: ${workItem.generatedDraftId ?? "unknown"}`,
4985
+ `Artifact format preference: ${artifactFormatPreference}`,
4235
4986
  "",
4236
4987
  "## Read First",
4237
4988
  "",
@@ -4241,7 +4992,7 @@ function createBrainGenerationPrompt(workItem) {
4241
4992
  "",
4242
4993
  "## Generate Artifacts",
4243
4994
  "",
4244
- "Return Markdown artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
4995
+ "Return artifacts that should enter human review before implementation. Use only these document types and matching repo roots:",
4245
4996
  "",
4246
4997
  "- architecture -> docs/architecture/",
4247
4998
  "- context -> docs/context/",
@@ -4251,20 +5002,35 @@ function createBrainGenerationPrompt(workItem) {
4251
5002
  "- plan -> docs/plans/",
4252
5003
  "- prompt -> docs/prompts/",
4253
5004
  "- workflow -> docs/workflows/",
5005
+ "- HTML companions -> docs/html/<same-folder>/ using .html, for example docs/html/plans/PLAN-example.html",
5006
+ "",
5007
+ artifactFormatPreferenceInstructions(artifactFormatPreference),
4254
5008
  "",
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.",
5009
+ "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
5010
  "",
4257
5011
  "## Output Contract",
4258
5012
  "",
4259
5013
  "Print exactly one JSON object between the markers below. The CLI will submit only this structured result back to Amistio.",
4260
5014
  "",
4261
5015
  generationResultStart,
4262
- '{"artifacts":[{"documentType":"plan","title":"Plan: Example","repoPath":"docs/plans/PLAN-example.md","content":"# Plan: Example\\n\\n## Goal\\n..."}]}',
5016
+ 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
5017
  generationResultEnd,
4264
5018
  "",
4265
5019
  "Do not put Markdown fences around the markers. Do not claim implementation is complete."
4266
5020
  ].join("\n");
4267
5021
  }
5022
+ function artifactFormatPreferenceInstructions(preference) {
5023
+ if (preference === "html") {
5024
+ return "The user chose HTML. Generate .html artifacts under docs/html/<folder>/ and set contentFormat to html.";
5025
+ }
5026
+ if (preference === "both") {
5027
+ 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.";
5028
+ }
5029
+ if (preference === "auto") {
5030
+ return "Choose Markdown or HTML based on the artifact content. Use Markdown for text-first docs and HTML for visual or layout-sensitive artifacts.";
5031
+ }
5032
+ return "The user chose Markdown. Generate Markdown artifacts under docs/<folder>/ and set contentFormat to markdown or omit it.";
5033
+ }
4268
5034
  function stripJsonFence(value) {
4269
5035
  const trimmed = value.trim();
4270
5036
  if (!trimmed.startsWith("```")) {
@@ -4289,7 +5055,13 @@ function formatWatchIdleLine(action, intervalSeconds) {
4289
5055
  }
4290
5056
  function shouldPrintWatchState(action, previous, nowMs, reminderMs = watchStateReminderMs) {
4291
5057
  const key = watchStateKey(action);
4292
- return !previous || previous.key !== key || nowMs - previous.printedAtMs >= reminderMs;
5058
+ if (!previous || previous.key !== key) {
5059
+ return true;
5060
+ }
5061
+ if (action.kind === "workCompleted") {
5062
+ return false;
5063
+ }
5064
+ return nowMs - previous.printedAtMs >= reminderMs;
4293
5065
  }
4294
5066
  function watchStateKey(action) {
4295
5067
  return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
@@ -4469,7 +5241,8 @@ async function scanLegacyDocuments(options) {
4469
5241
  skipped.push({ repoPath, reason: "excluded" });
4470
5242
  continue;
4471
5243
  }
4472
- if (!isMarkdownDocument(repoPath)) {
5244
+ const contentFormat = inferContentFormatFromRepoPath(repoPath);
5245
+ if (!contentFormat) {
4473
5246
  skipped.push({ repoPath, reason: "notMarkdown" });
4474
5247
  continue;
4475
5248
  }
@@ -4492,16 +5265,17 @@ async function scanLegacyDocuments(options) {
4492
5265
  skipped.push({ repoPath, reason: "unreadable" });
4493
5266
  continue;
4494
5267
  }
4495
- if (isAmistioManagedMarkdown(content)) {
5268
+ if (isAmistioManagedDocument(content)) {
4496
5269
  skipped.push({ repoPath, reason: "alreadyManaged" });
4497
5270
  continue;
4498
5271
  }
4499
5272
  const documentType = classifyLegacyDocument(repoPath, content);
4500
- const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType), repoPath, usedDestinationPaths);
5273
+ const destinationPath = uniqueDestinationPath(canonicalImportPath(repoPath, documentType, contentFormat), repoPath, usedDestinationPaths);
4501
5274
  candidates.push({
4502
5275
  sourcePath: repoPath,
4503
5276
  repoPath: destinationPath,
4504
5277
  documentType,
5278
+ contentFormat,
4505
5279
  title: inferTitle2(content, repoPath),
4506
5280
  content,
4507
5281
  contentHash: sha256ContentHash(content)
@@ -4524,6 +5298,7 @@ function buildImportedBrainDocuments(options) {
4524
5298
  projectId: options.projectId,
4525
5299
  documentId,
4526
5300
  documentType: candidate.documentType,
5301
+ contentFormat: candidate.contentFormat,
4527
5302
  title: candidate.title,
4528
5303
  status: "approved",
4529
5304
  repoPath: candidate.repoPath,
@@ -4602,9 +5377,6 @@ function wildcardMatch(pattern, repoPath) {
4602
5377
  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
5378
  return new RegExp(`^${escaped}$`).test(repoPath);
4604
5379
  }
4605
- function isMarkdownDocument(repoPath) {
4606
- return /\.(md|mdx)$/i.test(repoPath);
4607
- }
4608
5380
  function isExcludedRepoPath(repoPath) {
4609
5381
  if (isControlPlaneTemplateRepoPath(repoPath)) return true;
4610
5382
  if (excludedFileNames.has(repoPath)) return true;
@@ -4613,9 +5385,12 @@ function isExcludedRepoPath(repoPath) {
4613
5385
  const basename = segments[segments.length - 1]?.toLowerCase() ?? "";
4614
5386
  return basename.startsWith(".env") || basename.includes("secret") || basename.includes("token") || basename.includes("credential") || basename.endsWith(".lock");
4615
5387
  }
4616
- function isAmistioManagedMarkdown(content) {
5388
+ function isAmistioManagedDocument(content) {
4617
5389
  const frontmatter = parseFrontmatter(content);
4618
- return typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0;
5390
+ if (typeof frontmatter.amistioDocumentId === "string" && frontmatter.amistioDocumentId.trim().length > 0) {
5391
+ return true;
5392
+ }
5393
+ return /^\s*<!--[\s\S]*?amistioDocumentId\s*:/i.test(content);
4619
5394
  }
4620
5395
  function classifyLegacyDocument(repoPath, content) {
4621
5396
  const haystack = `${repoPath} ${inferTitle2(content, repoPath)}`.toLowerCase();
@@ -4628,7 +5403,7 @@ function classifyLegacyDocument(repoPath, content) {
4628
5403
  if (/\b(workflow|workflows|runbook|playbook|process|procedure|ops)\b/.test(haystack)) return "workflow";
4629
5404
  return "context";
4630
5405
  }
4631
- function canonicalImportPath(sourcePath, documentType) {
5406
+ function canonicalImportPath(sourcePath, documentType, contentFormat) {
4632
5407
  if (isCanonicalControlPlanePath(sourcePath)) {
4633
5408
  return sourcePath;
4634
5409
  }
@@ -4636,7 +5411,8 @@ function canonicalImportPath(sourcePath, documentType) {
4636
5411
  return `docs/${sourcePath}`;
4637
5412
  }
4638
5413
  const baseSlug = slugFromPath(sourcePath);
4639
- return `${documentFolderByType[documentType]}/imported/${baseSlug}.md`;
5414
+ const extension = contentFormat === "html" ? "html" : "md";
5415
+ return `${documentFolderByType[documentType]}/imported/${baseSlug}.${extension}`;
4640
5416
  }
4641
5417
  function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
4642
5418
  if (!usedPaths.has(basePath)) {
@@ -4651,8 +5427,8 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
4651
5427
  return uniquePath;
4652
5428
  }
4653
5429
  function isCanonicalControlPlanePath(repoPath) {
4654
- const [firstSegment, secondSegment] = normalizeRepoPath4(repoPath).split("/");
4655
- return firstSegment === "docs" && Boolean(secondSegment && controlPlaneRoots2.includes(secondSegment));
5430
+ const [firstSegment, secondSegment, thirdSegment] = normalizeRepoPath4(repoPath).split("/");
5431
+ return firstSegment === "docs" && Boolean(secondSegment === "html" && thirdSegment && controlPlaneRoots2.includes(thirdSegment) || secondSegment && controlPlaneRoots2.includes(secondSegment));
4656
5432
  }
4657
5433
  function isLegacyControlPlanePath(repoPath) {
4658
5434
  const [firstSegment] = repoPath.split("/");
@@ -4662,6 +5438,8 @@ function inferTitle2(content, repoPath) {
4662
5438
  const body = stripFrontmatter(content);
4663
5439
  const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
4664
5440
  if (heading) return heading;
5441
+ const htmlHeading = body.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i)?.[1]?.replace(/<[^>]+>/g, "").trim();
5442
+ if (htmlHeading) return htmlHeading;
4665
5443
  const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
4666
5444
  return titleCase(basename || "Imported Document");
4667
5445
  }
@@ -4673,7 +5451,7 @@ function stripFrontmatter(content) {
4673
5451
  return closingLineEnd === -1 ? "" : content.slice(closingLineEnd + 1);
4674
5452
  }
4675
5453
  function slugFromPath(repoPath) {
4676
- const withoutExtension = repoPath.replace(/\.(md|mdx)$/i, "");
5454
+ const withoutExtension = repoPath.replace(/\.(md|mdx|html?)$/i, "");
4677
5455
  const slug = withoutExtension.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 90);
4678
5456
  return slug || "imported-document";
4679
5457
  }
@@ -4772,6 +5550,7 @@ async function runOfficialCliUpdateWithRuntimeRefresh(options) {
4772
5550
  if (!options.restartBackgroundRunner) {
4773
5551
  return {
4774
5552
  succeeded: false,
5553
+ stopRunner: true,
4775
5554
  message: `${updateResult.message} Background runner restart was not available after update. Restart the runner manually to load the updated CLI version.`,
4776
5555
  error: "Background runner restart hook was not provided."
4777
5556
  };
@@ -4786,6 +5565,7 @@ async function runOfficialCliUpdateWithRuntimeRefresh(options) {
4786
5565
  }
4787
5566
  return {
4788
5567
  succeeded: false,
5568
+ stopRunner: true,
4789
5569
  message: `${updateResult.message} Background runner restart failed after update. Restart the runner manually to load the updated CLI version.`,
4790
5570
  error: restartResult.error ?? restartResult.message
4791
5571
  };
@@ -4907,6 +5687,186 @@ function errorMessage2(error) {
4907
5687
  return error instanceof Error ? error.message : String(error);
4908
5688
  }
4909
5689
 
5690
+ // src/implementation-handoff.ts
5691
+ import { execFile as execFile6 } from "node:child_process";
5692
+ import path13 from "node:path";
5693
+ import { promisify as promisify6 } from "node:util";
5694
+ var execFileAsync6 = promisify6(execFile6);
5695
+ async function completeImplementationHandoff(input) {
5696
+ const run = input.commandRunner ?? defaultCommandRunner;
5697
+ const headBranch = input.worktreeIsolation?.branch ?? input.workItem.executionBranch;
5698
+ const baseBranch = input.baseBranch ?? input.repositoryLink?.defaultBranch ?? "main";
5699
+ if (!headBranch) {
5700
+ return blockedHandoff({ baseBranch, message: "Implementation handoff needs an execution branch before it can create a pull request." });
5701
+ }
5702
+ const common = {
5703
+ provider: input.repositoryLink?.provider ?? "github",
5704
+ baseBranch,
5705
+ headBranch
5706
+ };
5707
+ try {
5708
+ const unmergedFiles = await gitOutput2(run, input.worktreePath, ["diff", "--name-only", "--diff-filter=U"]);
5709
+ if (unmergedFiles.trim()) {
5710
+ return blockedHandoff({ ...common, message: "Implementation handoff is blocked because the worktree has unresolved merge conflicts." });
5711
+ }
5712
+ const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
5713
+ if (!status) {
5714
+ return {
5715
+ ...common,
5716
+ status: "noChanges",
5717
+ cleanupStatus: "notApplicable",
5718
+ message: "Local execution completed with no repository changes to hand off."
5719
+ };
5720
+ }
5721
+ const remoteName = await resolveRemoteName(run, input.worktreePath);
5722
+ const remoteUrl = await gitOutput2(run, input.worktreePath, ["remote", "get-url", remoteName]);
5723
+ const provider = input.repositoryLink?.provider ?? inferProvider(remoteUrl);
5724
+ if (provider !== "github") {
5725
+ return blockedHandoff({
5726
+ ...common,
5727
+ provider,
5728
+ remoteName,
5729
+ message: "Automated pull request handoff currently requires a GitHub remote. Commit and push manually, or link a GitHub repository."
5730
+ });
5731
+ }
5732
+ await gitOutput2(run, input.worktreePath, ["add", "-A"]);
5733
+ await gitOutput2(run, input.worktreePath, ["commit", "-m", commitSubject(input.workItem), "-m", commitBody(input)]);
5734
+ const commitSha = await gitOutput2(run, input.worktreePath, ["rev-parse", "HEAD"]);
5735
+ await gitOutput2(run, input.worktreePath, ["push", "--set-upstream", remoteName, headBranch]);
5736
+ const pullRequest = await ensureGithubPullRequest(run, input.worktreePath, { baseBranch, headBranch, workItem: input.workItem, ...input.verificationSummary ? { verificationSummary: input.verificationSummary } : {} });
5737
+ const cleanup = await cleanupWorktree(run, input);
5738
+ return {
5739
+ provider: "github",
5740
+ status: "prReady",
5741
+ baseBranch,
5742
+ headBranch,
5743
+ remoteName,
5744
+ commitSha,
5745
+ prNumber: pullRequest.number,
5746
+ prUrl: pullRequest.url,
5747
+ cleanupStatus: cleanup.status,
5748
+ ...cleanup.message ? { cleanupMessage: cleanup.message } : {},
5749
+ 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."
5750
+ };
5751
+ } catch (error) {
5752
+ return blockedHandoff({ ...common, message: "Implementation handoff is blocked and the local worktree was preserved for recovery.", error: safeErrorMessage(error) });
5753
+ }
5754
+ }
5755
+ async function ensureGithubPullRequest(run, cwd, input) {
5756
+ const existing = await run("gh", ["pr", "list", "--head", input.headBranch, "--base", input.baseBranch, "--state", "open", "--json", "number,url", "--limit", "1"], { cwd });
5757
+ const parsed = parsePullRequestList(existing.stdout);
5758
+ if (parsed) {
5759
+ return parsed;
5760
+ }
5761
+ const created = await run("gh", ["pr", "create", "--base", input.baseBranch, "--head", input.headBranch, "--title", pullRequestTitle(input.workItem), "--body", pullRequestBody(input)], { cwd });
5762
+ const url = extractPullRequestUrl(created.stdout);
5763
+ if (!url) {
5764
+ throw new Error("GitHub CLI did not return a pull request URL.");
5765
+ }
5766
+ const number = pullRequestNumber(url);
5767
+ if (!number) {
5768
+ throw new Error("GitHub CLI returned a pull request URL without a parseable number.");
5769
+ }
5770
+ return { number, url };
5771
+ }
5772
+ async function cleanupWorktree(run, input) {
5773
+ const status = await gitOutput2(run, input.worktreePath, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
5774
+ if (status) {
5775
+ return { status: "failed", message: "Cleanup skipped because the worktree is not clean after PR handoff." };
5776
+ }
5777
+ try {
5778
+ await gitOutput2(run, input.primaryRepoRoot || path13.dirname(input.worktreePath), ["worktree", "remove", input.worktreePath]);
5779
+ return { status: "completed" };
5780
+ } catch (error) {
5781
+ return { status: "failed", message: `Cleanup failed: ${safeErrorMessage(error)}` };
5782
+ }
5783
+ }
5784
+ async function resolveRemoteName(run, cwd) {
5785
+ const remotes = (await gitOutput2(run, cwd, ["remote"])).split(/\r?\n/g).map((remote) => remote.trim()).filter(Boolean);
5786
+ if (!remotes.length) {
5787
+ throw new Error("No Git remote is configured for PR handoff.");
5788
+ }
5789
+ return remotes.includes("origin") ? "origin" : remotes[0];
5790
+ }
5791
+ async function gitOutput2(run, cwd, args) {
5792
+ const result = await run("git", args, { cwd });
5793
+ return result.stdout.trim();
5794
+ }
5795
+ function blockedHandoff(input) {
5796
+ return {
5797
+ status: "blocked",
5798
+ cleanupStatus: "pending",
5799
+ ...input
5800
+ };
5801
+ }
5802
+ function inferProvider(remoteUrl) {
5803
+ try {
5804
+ return parseRepositoryCloneUrl(remoteUrl).provider ?? "other";
5805
+ } catch {
5806
+ return "other";
5807
+ }
5808
+ }
5809
+ function parsePullRequestList(stdout) {
5810
+ const parsed = JSON.parse(stdout || "[]");
5811
+ const first = parsed[0];
5812
+ if (typeof first?.number === "number" && typeof first.url === "string" && first.url) {
5813
+ return { number: first.number, url: first.url };
5814
+ }
5815
+ return void 0;
5816
+ }
5817
+ function extractPullRequestUrl(stdout) {
5818
+ return stdout.split(/\s+/g).find((value) => /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/i.test(value));
5819
+ }
5820
+ function pullRequestNumber(url) {
5821
+ const match = /\/pull\/(\d+)(?:$|[?#])/.exec(url);
5822
+ return match ? Number.parseInt(match[1], 10) : void 0;
5823
+ }
5824
+ function commitSubject(workItem) {
5825
+ return truncate(`Amistio: ${workItem.title}`, 72);
5826
+ }
5827
+ function commitBody(input) {
5828
+ return [
5829
+ `Work item: ${input.workItem.workItemId}`,
5830
+ `Implementation scope: ${input.workItem.controllingAdrId ?? input.workItem.implementationScopeId ?? "unspecified"}`,
5831
+ "",
5832
+ "Generated by Amistio local runner.",
5833
+ "Do not include secrets or source patches in Amistio SaaS metadata."
5834
+ ].join("\n");
5835
+ }
5836
+ function pullRequestTitle(workItem) {
5837
+ return truncate(workItem.title, 120);
5838
+ }
5839
+ function pullRequestBody(input) {
5840
+ return [
5841
+ `Amistio work item: ${input.workItem.workItemId}`,
5842
+ `Implementation scope: ${input.workItem.controllingAdrId ?? input.workItem.implementationScopeId ?? "unspecified"}`,
5843
+ `Base branch: ${input.baseBranch}`,
5844
+ `Head branch: ${input.headBranch}`,
5845
+ "",
5846
+ input.verificationSummary ?? "Verification summary was not reported by the local runner."
5847
+ ].join("\n");
5848
+ }
5849
+ function truncate(value, maxLength) {
5850
+ return value.length <= maxLength ? value : value.slice(0, maxLength - 1).trimEnd();
5851
+ }
5852
+ async function defaultCommandRunner(command, args, options) {
5853
+ const { stdout, stderr } = await execFileAsync6(command, args, { cwd: options.cwd, maxBuffer: 1024 * 1024 });
5854
+ return { stdout, stderr };
5855
+ }
5856
+ function safeErrorMessage(error) {
5857
+ const scrub = (value) => redactLocalPaths(value.replace(/^Command failed:[^\n]*(\n)?/i, "").trim() || value);
5858
+ if (typeof error === "object" && error && "stderr" in error && typeof error.stderr === "string" && error.stderr.trim()) {
5859
+ return truncate(scrub(error.stderr.trim()), 600);
5860
+ }
5861
+ if (error instanceof Error) {
5862
+ return truncate(scrub(error.message), 600);
5863
+ }
5864
+ return truncate(scrub(String(error)), 600);
5865
+ }
5866
+ function redactLocalPaths(value) {
5867
+ 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>");
5868
+ }
5869
+
4910
5870
  // src/version.ts
4911
5871
  import { readFileSync } from "node:fs";
4912
5872
  function readCliPackageVersion() {
@@ -4926,7 +5886,7 @@ var DEFAULT_MAX_PREFLIGHT_ATTEMPTS = 3;
4926
5886
  var DEFAULT_TOOL_TIMEOUT_SECONDS = 30 * 60;
4927
5887
  var RUNNER_WORK_LEASE_SECONDS = 300;
4928
5888
  var RUNNER_WORK_LEASE_RENEWAL_MS = 12e4;
4929
- var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan"];
5889
+ var runnerSupportedWorkKinds = ["brainGeneration", "implementation", "planRevision", "assistantQuestion", "impactPreview", "issueDiagnosis", "securityPostureScan", "appEvaluationScan", "projectContextRefresh"];
4930
5890
  program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
4931
5891
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
4932
5892
  const created = await initControlPlane(options.root);
@@ -5304,7 +6264,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
5304
6264
  projectId: context.metadata.amistioProjectId,
5305
6265
  repositoryLinkId: context.metadata.repositoryLinkId,
5306
6266
  runnerId,
5307
- rootDir: path13.resolve(options.root),
6267
+ rootDir: path14.resolve(options.root),
5308
6268
  apiUrl: options.apiUrl,
5309
6269
  args: buildBackgroundRunnerArgs(resolvedOptions)
5310
6270
  });
@@ -5476,7 +6436,7 @@ runnerService.command("install").description("Install a user-level startup servi
5476
6436
  projectId: context.metadata.amistioProjectId,
5477
6437
  repositoryLinkId: context.metadata.repositoryLinkId,
5478
6438
  runnerId,
5479
- rootDir: path13.resolve(options.root),
6439
+ rootDir: path14.resolve(options.root),
5480
6440
  apiUrl: options.apiUrl,
5481
6441
  args,
5482
6442
  platform
@@ -5925,13 +6885,72 @@ async function runNextWorkItem({
5925
6885
  return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
5926
6886
  }
5927
6887
  }
5928
- const finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
6888
+ if (result.workItem.workKind === "appEvaluationScan") {
6889
+ try {
6890
+ return await finalizeAppEvaluationScanWork({
6891
+ apiClient,
6892
+ durationMs: Date.now() - startedAt,
6893
+ projectId,
6894
+ repositoryLinkId,
6895
+ runnerId,
6896
+ sessionContext,
6897
+ toolConfig,
6898
+ toolName: preview.toolName,
6899
+ toolResult,
6900
+ workItem: result.workItem
6901
+ });
6902
+ } catch (error) {
6903
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
6904
+ }
6905
+ }
6906
+ if (result.workItem.workKind === "projectContextRefresh") {
6907
+ try {
6908
+ return await finalizeProjectContextRefreshWork({
6909
+ apiClient,
6910
+ durationMs: Date.now() - startedAt,
6911
+ projectId,
6912
+ repositoryLinkId,
6913
+ runnerId,
6914
+ sessionContext,
6915
+ toolConfig,
6916
+ toolName: preview.toolName,
6917
+ toolResult,
6918
+ workItem: result.workItem
6919
+ });
6920
+ } catch (error) {
6921
+ return recordFinalizationFailure({ apiClient, error, isolationTelemetry, projectId, repositoryLinkId, runnerId, sessionContext, toolConfig, toolName: preview.toolName, workItem: result.workItem, durationMs: Date.now() - startedAt });
6922
+ }
6923
+ }
6924
+ let finalStatus = toolResult.exitCode === 0 ? "completed" : "failed";
5929
6925
  const durationMs = Date.now() - startedAt;
5930
6926
  const failureExcerpt = toolResult.exitCode === 0 ? void 0 : truncateLogExcerpt(toolResult.stderr || toolResult.stdout);
6927
+ let implementationHandoff;
6928
+ let finalMessage = `${preview.toolName} exited with code ${toolResult.exitCode}.`;
6929
+ let finalError = failureExcerpt;
6930
+ if (toolResult.exitCode === 0 && (result.workItem.workKind ?? "implementation") === "implementation") {
6931
+ await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
6932
+ status: "running",
6933
+ summary: "Preparing GitHub PR handoff for implementation work.",
6934
+ idempotencyKey: `runner_milestone_handoff_started_${result.workItem.workItemId}_${result.workItem.attempt}`,
6935
+ metadata: { executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "", executionBranch: isolationTelemetry.executionBranch ?? "" }
6936
+ });
6937
+ const repositoryLink = await loadWorkItemRepositoryLink(apiClient, projectId, result.workItem.repositoryLinkId ?? repositoryLinkId);
6938
+ implementationHandoff = await completeImplementationHandoff({
6939
+ primaryRepoRoot: root,
6940
+ ...repositoryLink ? { repositoryLink } : {},
6941
+ verificationSummary: "Local execution reported completion.",
6942
+ workItem: result.workItem,
6943
+ ...worktreeIsolation.isolation ? { worktreeIsolation: worktreeIsolation.isolation } : {},
6944
+ worktreePath: executionRoot
6945
+ });
6946
+ finalStatus = implementationHandoff.status === "prReady" || implementationHandoff.status === "noChanges" ? "completed" : "blocked";
6947
+ finalMessage = implementationHandoff.message ?? "Implementation handoff finished.";
6948
+ finalError = implementationHandoff.error;
6949
+ }
5931
6950
  const updatedToolSession = await finalizeToolSession({
5932
6951
  apiClient,
5933
6952
  projectId,
5934
- status: finalStatus,
6953
+ status: toolResult.exitCode === 0 ? "completed" : "failed",
5935
6954
  runnerId,
5936
6955
  workItemId: result.workItem.workItemId,
5937
6956
  stdout: toolResult.stdout,
@@ -5951,8 +6970,9 @@ async function runNextWorkItem({
5951
6970
  tool: preview.toolName,
5952
6971
  ...toolResult.model ? { model: toolResult.model } : {},
5953
6972
  durationMs,
5954
- message: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
6973
+ message: finalMessage,
5955
6974
  ...isolationTelemetry,
6975
+ ...finalStatus === "blocked" ? { blockerReason: finalMessage } : {},
5956
6976
  sessionPolicy: sessionContext.policy,
5957
6977
  sessionDecision: sessionContext.decision,
5958
6978
  sessionDecisionReason: sessionContext.reason,
@@ -5961,19 +6981,34 @@ async function runNextWorkItem({
5961
6981
  ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
5962
6982
  ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
5963
6983
  ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {},
5964
- ...failureExcerpt ? { error: failureExcerpt } : {}
6984
+ ...implementationHandoff ? { implementationHandoff } : {},
6985
+ ...finalError ? { error: finalError } : {}
5965
6986
  }
5966
6987
  );
5967
6988
  await recordRunnerMilestone(apiClient, projectId, result.workItem, runnerId, repositoryLinkId, {
5968
6989
  status: finalStatus,
5969
- summary: `${preview.toolName} exited with code ${toolResult.exitCode}.`,
6990
+ summary: finalMessage,
5970
6991
  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 ?? "" }
6992
+ metadata: {
6993
+ tool: preview.toolName,
6994
+ durationMs,
6995
+ exitCode: toolResult.exitCode,
6996
+ verificationSummary: finalStatus === "completed" ? "Local execution reported completion." : "Local execution reported failure.",
6997
+ executionWorktreeKey: isolationTelemetry.executionWorktreeKey ?? "",
6998
+ executionBranch: isolationTelemetry.executionBranch ?? "",
6999
+ ...implementationHandoff?.status ? { handoffStatus: implementationHandoff.status } : {},
7000
+ ...implementationHandoff?.prUrl ? { prUrl: implementationHandoff.prUrl } : {},
7001
+ ...implementationHandoff?.cleanupStatus ? { cleanupStatus: implementationHandoff.cleanupStatus } : {}
7002
+ }
5972
7003
  });
5973
7004
  await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
5974
7005
  const durationSeconds = Math.round(durationMs / 1e3);
5975
7006
  console.log(`Marked ${statusResult.workItem.workItemId} ${statusResult.workItem.status} after ${durationSeconds}s.`);
5976
- return { status: finalStatus, exitCode: toolResult.exitCode };
7007
+ return { status: finalStatus, exitCode: finalStatus === "completed" ? toolResult.exitCode : 1 };
7008
+ }
7009
+ async function loadWorkItemRepositoryLink(apiClient, projectId, repositoryLinkId) {
7010
+ const { repositoryLinks } = await apiClient.listRepositoryLinks(projectId);
7011
+ return repositoryLinks.find((link) => link.repositoryLinkId === repositoryLinkId && link.status !== "revoked");
5977
7012
  }
5978
7013
  async function prepareWorktreeForClaimedItem({ apiClient, maxPreflightAttempts, projectId, repositoryLinkId, root, runnerId, toolConfig, workItem }) {
5979
7014
  if (!needsGitWorktreeIsolation(workItem)) {
@@ -6177,10 +7212,10 @@ async function executeRunnerCommand(command, context) {
6177
7212
  }
6178
7213
  return runOfficialCliUpdateWithRuntimeRefresh({
6179
7214
  mode: currentRunnerMode(),
6180
- restartBackgroundRunner: () => restartCurrentRunner(context)
7215
+ restartBackgroundRunner: () => restartCurrentRunner(context, { useUpdatedCliExecutable: true })
6181
7216
  });
6182
7217
  }
6183
- async function restartCurrentRunner(context) {
7218
+ async function restartCurrentRunner(context, options = {}) {
6184
7219
  if (currentRunnerMode() !== "background") {
6185
7220
  return { succeeded: false, message: "Foreground runners cannot be restarted remotely. Stop and start the local command manually." };
6186
7221
  }
@@ -6189,7 +7224,7 @@ async function restartCurrentRunner(context) {
6189
7224
  return { succeeded: false, message: "Background runner metadata was not found, so restart needs manual action." };
6190
7225
  }
6191
7226
  try {
6192
- const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs);
7227
+ const replacement = await restartRunnerDaemonProcess(metadata, context.backgroundArgs, options.useUpdatedCliExecutable ? updatedCliRunnerLaunchOptions() : {});
6193
7228
  return { succeeded: true, stopRunner: true, message: `Replacement background runner started with PID ${replacement.pid}.` };
6194
7229
  } catch (error) {
6195
7230
  return { succeeded: false, message: "Background restart failed.", error: errorMessage3(error) };
@@ -6638,23 +7673,217 @@ ${toolResult.stderr}`);
6638
7673
  console.error(scanError ?? "Local runner security posture scan failed.");
6639
7674
  return { status: "failed", exitCode: toolResult.exitCode || 1 };
6640
7675
  }
7676
+ async function finalizeAppEvaluationScanWork({
7677
+ apiClient,
7678
+ durationMs,
7679
+ projectId,
7680
+ repositoryLinkId,
7681
+ runnerId,
7682
+ sessionContext,
7683
+ toolConfig,
7684
+ toolName,
7685
+ toolResult,
7686
+ workItem
7687
+ }) {
7688
+ let scanResult = void 0;
7689
+ let scanError;
7690
+ if (toolResult.exitCode === 0) {
7691
+ try {
7692
+ scanResult = parseAppEvaluationScanResult(`${toolResult.stdout}
7693
+ ${toolResult.stderr}`);
7694
+ } catch (error) {
7695
+ scanError = errorMessage3(error);
7696
+ }
7697
+ } else {
7698
+ scanError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
7699
+ }
7700
+ const finalStatus = scanResult ? "completed" : "failed";
7701
+ const updatedToolSession = await finalizeToolSession({
7702
+ apiClient,
7703
+ projectId,
7704
+ status: finalStatus,
7705
+ runnerId,
7706
+ workItemId: workItem.workItemId,
7707
+ stdout: toolResult.stdout,
7708
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
7709
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
7710
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
7711
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
7712
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
7713
+ });
7714
+ const sessionTelemetry = {
7715
+ sessionPolicy: sessionContext.policy,
7716
+ sessionDecision: sessionContext.decision,
7717
+ sessionDecisionReason: sessionContext.reason,
7718
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
7719
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
7720
+ };
7721
+ if (scanResult) {
7722
+ const result = await apiClient.submitAppEvaluationScanResult(projectId, workItem.workItemId, {
7723
+ status: "completed",
7724
+ runnerId,
7725
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID()}`,
7726
+ result: scanResult,
7727
+ tool: toolName,
7728
+ durationMs,
7729
+ ...sessionTelemetry,
7730
+ message: `${toolName} returned an app evaluation scan.`
7731
+ });
7732
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7733
+ status: "completed",
7734
+ summary: `${toolName} returned an app evaluation scan.`,
7735
+ idempotencyKey: `runner_milestone_app_evaluation_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7736
+ metadata: { tool: toolName, durationMs, findingCount: scanResult.findings.length, verificationSummary: scanResult.verificationPlan.join(" | ") }
7737
+ });
7738
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7739
+ console.log("App evaluation scan returned for review.");
7740
+ return { status: "completed", exitCode: 0 };
7741
+ }
7742
+ const failedResult = await apiClient.submitAppEvaluationScanResult(projectId, workItem.workItemId, {
7743
+ status: "failed",
7744
+ runnerId,
7745
+ idempotencyKey: `app_evaluation_${workItem.workItemId}_${randomUUID()}`,
7746
+ tool: toolName,
7747
+ durationMs,
7748
+ ...sessionTelemetry,
7749
+ message: `${toolName} did not produce a valid app evaluation scan.`,
7750
+ ...scanError ? { error: scanError } : {}
7751
+ });
7752
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7753
+ status: "failed",
7754
+ summary: scanError ?? `${toolName} did not produce a valid app evaluation scan.`,
7755
+ idempotencyKey: `runner_milestone_app_evaluation_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
7756
+ metadata: { tool: toolName, durationMs, verificationSummary: "App evaluation output did not include valid structured JSON." }
7757
+ });
7758
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7759
+ console.error(scanError ?? "Local runner app evaluation scan failed.");
7760
+ return { status: "failed", exitCode: toolResult.exitCode || 1 };
7761
+ }
7762
+ async function finalizeProjectContextRefreshWork({
7763
+ apiClient,
7764
+ durationMs,
7765
+ projectId,
7766
+ repositoryLinkId,
7767
+ runnerId,
7768
+ sessionContext,
7769
+ toolConfig,
7770
+ toolName,
7771
+ toolResult,
7772
+ workItem
7773
+ }) {
7774
+ let refreshResult = void 0;
7775
+ let refreshError;
7776
+ if (toolResult.exitCode === 0) {
7777
+ try {
7778
+ refreshResult = parseProjectContextRefreshResult(`${toolResult.stdout}
7779
+ ${toolResult.stderr}`);
7780
+ } catch (error) {
7781
+ refreshError = errorMessage3(error);
7782
+ }
7783
+ } else {
7784
+ refreshError = truncateLogExcerpt(toolResult.stderr || toolResult.stdout) || `${toolName} exited with code ${toolResult.exitCode}.`;
7785
+ }
7786
+ const finalStatus = refreshResult ? "completed" : "failed";
7787
+ const updatedToolSession = await finalizeToolSession({
7788
+ apiClient,
7789
+ projectId,
7790
+ status: finalStatus,
7791
+ runnerId,
7792
+ workItemId: workItem.workItemId,
7793
+ stdout: toolResult.stdout,
7794
+ ...sessionContext.toolSession ? { session: sessionContext.toolSession } : {},
7795
+ ...toolResult.messageCount !== void 0 ? { messageCount: toolResult.messageCount } : {},
7796
+ ...toolResult.tokensIn !== void 0 ? { tokensIn: toolResult.tokensIn } : {},
7797
+ ...toolResult.tokensOut !== void 0 ? { tokensOut: toolResult.tokensOut } : {},
7798
+ ...toolResult.costUsd !== void 0 ? { costUsd: toolResult.costUsd } : {}
7799
+ });
7800
+ const sessionTelemetry = {
7801
+ sessionPolicy: sessionContext.policy,
7802
+ sessionDecision: sessionContext.decision,
7803
+ sessionDecisionReason: sessionContext.reason,
7804
+ ...updatedToolSession ? { toolSessionId: updatedToolSession.toolSessionId } : {},
7805
+ ...updatedToolSession?.sessionGroupKey ? { sessionGroupKey: updatedToolSession.sessionGroupKey } : {}
7806
+ };
7807
+ if (refreshResult) {
7808
+ const result = await apiClient.submitProjectContextRefreshResult(projectId, workItem.workItemId, {
7809
+ status: "completed",
7810
+ runnerId,
7811
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID()}`,
7812
+ result: refreshResult,
7813
+ tool: toolName,
7814
+ durationMs,
7815
+ ...sessionTelemetry,
7816
+ message: `${toolName} returned a project context refresh.`
7817
+ });
7818
+ const failureSummary = projectContextRefreshSubmissionFailureSummary(result);
7819
+ if (failureSummary) {
7820
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7821
+ status: "failed",
7822
+ summary: failureSummary,
7823
+ idempotencyKey: `runner_milestone_project_context_failed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7824
+ metadata: { tool: toolName, durationMs, sliceCount: refreshResult.slices.length, entityCount: refreshResult.entities.length, verificationSummary: "Server rejected the project context refresh result." }
7825
+ });
7826
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7827
+ console.error(failureSummary);
7828
+ return { status: "failed", exitCode: 1 };
7829
+ }
7830
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7831
+ status: "completed",
7832
+ summary: `${toolName} returned a project context refresh.`,
7833
+ idempotencyKey: `runner_milestone_project_context_completed_${workItem.workItemId}_${result.workItem.idempotencyKey}`,
7834
+ metadata: { tool: toolName, durationMs, sliceCount: refreshResult.slices.length, entityCount: refreshResult.entities.length, mapId: result.map?.projectContextMapId ?? "", verificationSummary: refreshResult.verificationPlan.join(" | ") }
7835
+ });
7836
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7837
+ console.log("Project context refresh returned for review.");
7838
+ return { status: "completed", exitCode: 0 };
7839
+ }
7840
+ const failedResult = await apiClient.submitProjectContextRefreshResult(projectId, workItem.workItemId, {
7841
+ status: "failed",
7842
+ runnerId,
7843
+ idempotencyKey: `project_context_${workItem.workItemId}_${randomUUID()}`,
7844
+ tool: toolName,
7845
+ durationMs,
7846
+ ...sessionTelemetry,
7847
+ message: `${toolName} did not produce a valid project context refresh.`,
7848
+ ...refreshError ? { error: refreshError } : {}
7849
+ });
7850
+ await recordRunnerMilestone(apiClient, projectId, workItem, runnerId, repositoryLinkId, {
7851
+ status: "failed",
7852
+ summary: refreshError ?? `${toolName} did not produce a valid project context refresh.`,
7853
+ idempotencyKey: `runner_milestone_project_context_failed_${workItem.workItemId}_${failedResult.workItem.idempotencyKey}`,
7854
+ metadata: { tool: toolName, durationMs, verificationSummary: "Project context output did not include valid structured JSON." }
7855
+ });
7856
+ await apiClient.sendRunnerHeartbeat(projectId, runnerId, repositoryLinkId, "online", runnerHeartbeatMetadata(toolConfig));
7857
+ console.error(refreshError ?? "Local runner project context refresh failed.");
7858
+ return { status: "failed", exitCode: toolResult.exitCode || 1 };
7859
+ }
6641
7860
  async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
6642
7861
  if (workItem.workKind === "assistantQuestion") {
6643
- const [{ documents: documents2 }, { messages: messages2 }] = await Promise.all([
7862
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7863
+ const [{ documents: documents2 }, { messages: messages2 }, context] = await Promise.all([
6644
7864
  apiClient.listBrainDocuments(projectId),
6645
- apiClient.listAssistantMessages(projectId)
7865
+ apiClient.listAssistantMessages(projectId),
7866
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
6646
7867
  ]);
6647
7868
  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];
7869
+ const contextPack = await apiClient.buildProjectContextPack(projectId, question?.content ?? workItem.sourceWish ?? workItem.title, workItem.workItemId).catch(() => void 0);
6648
7870
  return createWorkExecutionPrompt(workItem, {
6649
- ...question ? { assistantQuestion: { question, messages: messages2, documents: documents2 } } : {}
7871
+ ...question ? { assistantQuestion: { question, messages: messages2, documents: documents2, ...context.activeMap ? { activeMap: context.activeMap } : {}, ...contextPack ? { contextPack } : {} } } : {}
6650
7872
  });
6651
7873
  }
6652
7874
  if (workItem.workKind === "impactPreview") {
6653
- const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
7875
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7876
+ const [{ documents: documents2 }, context] = await Promise.all([
7877
+ apiClient.listBrainDocuments(projectId),
7878
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
7879
+ ]);
6654
7880
  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"));
7881
+ const contextPack = await apiClient.buildProjectContextPack(projectId, implementationPrompt?.content ?? workItem.sourceWish ?? workItem.title, workItem.workItemId).catch(() => void 0);
6655
7882
  return createWorkExecutionPrompt(workItem, {
6656
7883
  impactPreview: {
6657
7884
  documents: documents2,
7885
+ ...context.activeMap ? { activeMap: context.activeMap } : {},
7886
+ ...contextPack ? { contextPack } : {},
6658
7887
  ...implementationPrompt ? { implementationPrompt } : {}
6659
7888
  }
6660
7889
  });
@@ -6678,6 +7907,25 @@ async function createRunnerWorkPrompt(apiClient, projectId, workItem) {
6678
7907
  securityPostureScan: { documents: documents2 }
6679
7908
  });
6680
7909
  }
7910
+ if (workItem.workKind === "appEvaluationScan") {
7911
+ const { documents: documents2 } = await apiClient.listBrainDocuments(projectId);
7912
+ return createWorkExecutionPrompt(workItem, {
7913
+ appEvaluationScan: { documents: documents2 }
7914
+ });
7915
+ }
7916
+ if (workItem.workKind === "projectContextRefresh") {
7917
+ const emptyProjectContext = { maps: [], refreshes: [], misses: [] };
7918
+ const [{ documents: documents2 }, context] = await Promise.all([
7919
+ apiClient.listBrainDocuments(projectId),
7920
+ apiClient.listProjectContext(projectId).catch(() => emptyProjectContext)
7921
+ ]);
7922
+ return createWorkExecutionPrompt(workItem, {
7923
+ projectContextRefresh: {
7924
+ documents: documents2,
7925
+ ...context.activeMap ? { activeMap: context.activeMap } : {}
7926
+ }
7927
+ });
7928
+ }
6681
7929
  if (workItem.workKind !== "planRevision" || !workItem.reviewDocumentId) {
6682
7930
  return createWorkExecutionPrompt(workItem);
6683
7931
  }
@@ -6905,7 +8153,7 @@ function parseReasoningEffort(value) {
6905
8153
  throw new Error(`Expected reasoning effort auto, low, medium, high, or xhigh; received ${value}.`);
6906
8154
  }
6907
8155
  function inferRepoName(root) {
6908
- return path13.basename(path13.resolve(root)) || "repository";
8156
+ return path14.basename(path14.resolve(root)) || "repository";
6909
8157
  }
6910
8158
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
6911
8159
  return createHash7("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");