runbrief 0.1.2 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +30 -3
  2. package/dist/cli.js +2341 -680
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ var __esm = (fn, res) => function __init() {
6
6
 
7
7
  // ../contracts/dist/index.js
8
8
  import { z } from "zod";
9
- var privacyModeSchema, enforcementSchema, skillStatusSchema, learningStatusSchema, findingSeveritySchema, prReadinessVerdictSchema, preCodingContextProofSchema, prReadinessCheckStatusSchema, prReadinessGateIdSchema, prReadinessGateStatusSchema, prReadinessStatusSchema, prReadinessDeliveryModeSchema, githubCheckConclusionSchema, agentSurfaceModeSchema, agentSurfaceCategorySchema, agentSurfaceCredentialStorageSchema, agentSurfaceRepoResolutionSchema, agentSurfaceTrustBoundarySchema, briefProofStateSchema, briefToolNameSchema, agentSurfaceContractSchema, briefRunPhaseSchema, briefRunContractSchema, briefProofContractSchema, repoRefSchema, scanStatsSchema, sourceDocumentSchema, ruleSchema, betaReadinessSchema, skillSchema, mapNodeSchema, reviewMemorySchema, wikiEvidenceItemSchema, wikiPageTypeSchema, wikiPageEvidenceMixSchema, wikiPageEntitySchema, wikiPageFreshnessSchema, wikiPageSourceProofSchema, wikiPageAgentBriefSchema, wikiPageQualitySchema, wikiPageSchema, prReadinessFindingSchema, prReadinessPullRequestSchema, prReadinessContextProofInputSchema, prReadinessCheckSchema, prReadinessGateResultSchema, prReadinessContractSchema, prReadinessReportRequestSchema, severityCountsSchema, prReadinessAnnotationSchema, prReadinessGithubCheckSchema, prReadinessReportSchema, prReadinessReportResponseSchema, repoScanSummarySchema, syncScanRequestSchema, teamGuidanceItemSchema, teamGuidanceDeliverySchema, contextPacketSchema, contextReceiptRequestSchema, missionRecipeIdSchema, missionWorkItemSchema, missionRuntimeEvidenceSchema, runtimeEvidencePullInstructionSchema, runtimeEvidencePacketSchema, missionHandoffValidationSchema, missionHandoffReviewReadinessSchema, missionHandoffReviewSchema, missionHandoffProofStatusSchema, missionHandoffValidationSummarySchema, missionHandoffArtifactEvidenceSchema, missionHandoffArtifactProofSchema, missionHandoffChangedFileAttributionSchema, missionHandoffProofBundleSchema, missionHandoffRequestSchema, evidenceRedactionFindingSchema, evidenceRedactionReportSchema, learningCreateRequestSchema, teamContextRequestSchema, teamContextResponseSchema, apiAcceptedResponseSchema;
9
+ var privacyModeSchema, loopRunStatusSchema, loopRunProofSchema, loopRunSummarySchema, createLoopRunRequestSchema, claimLoopRunRequestSchema, updateLoopRunRequestSchema, enforcementSchema, skillStatusSchema, learningStatusSchema, findingSeveritySchema, prReadinessVerdictSchema, preCodingContextProofSchema, prReadinessCheckStatusSchema, prReadinessGateIdSchema, prReadinessGateStatusSchema, prReadinessStatusSchema, prReadinessDeliveryModeSchema, githubCheckConclusionSchema, agentSurfaceModeSchema, agentSurfaceCategorySchema, agentSurfaceCredentialStorageSchema, agentSurfaceRepoResolutionSchema, agentSurfaceTrustBoundarySchema, briefProofStateSchema, briefToolNameSchema, agentSurfaceContractSchema, briefRunPhaseSchema, briefRunContractSchema, briefProofContractSchema, repoRefSchema, scanStatsSchema, sourceDocumentSchema, ruleSchema, betaReadinessSchema, skillSchema, mapNodeSchema, reviewMemorySchema, wikiEvidenceItemSchema, wikiPageTypeSchema, wikiPageEvidenceMixSchema, wikiPageEntitySchema, wikiPageFreshnessSchema, wikiPageSourceProofSchema, wikiPageAgentBriefSchema, wikiPageQualitySchema, wikiPageSchema, prReadinessFindingSchema, prReadinessPullRequestSchema, prReadinessContextProofInputSchema, prReadinessCheckSchema, prReadinessGateResultSchema, prReadinessContractSchema, prReadinessReportRequestSchema, severityCountsSchema, prReadinessAnnotationSchema, prReadinessGithubCheckSchema, prReadinessReportSchema, prReadinessReportResponseSchema, repoScanSummarySchema, syncScanRequestSchema, teamGuidanceItemSchema, teamGuidanceDeliverySchema, dependencyPackageSignalSchema, dependencyRemediationSchema, contextPacketSchema, contextReceiptRequestSchema, missionRecipeIdSchema, missionWorkItemSchema, missionRuntimeEvidenceSchema, runtimeEvidencePullInstructionSchema, runtimeEvidencePacketSchema, missionHandoffValidationSchema, missionHandoffReviewReadinessSchema, missionHandoffReviewSchema, missionHandoffProofStatusSchema, missionHandoffValidationSummarySchema, missionHandoffArtifactEvidenceSchema, missionHandoffArtifactProofSchema, missionHandoffChangedFileAttributionSchema, missionHandoffProofBundleSchema, missionHandoffRequestSchema, evidenceRedactionFindingSchema, evidenceRedactionReportSchema, learningCreateRequestSchema, teamContextRequestSchema, teamContextResponseSchema, apiAcceptedResponseSchema;
10
10
  var init_dist = __esm({
11
11
  "../contracts/dist/index.js"() {
12
12
  "use strict";
@@ -15,6 +15,56 @@ var init_dist = __esm({
15
15
  "cloud-sync-derived",
16
16
  "team-cloud"
17
17
  ]);
18
+ loopRunStatusSchema = z.enum([
19
+ "queued",
20
+ "running",
21
+ "succeeded",
22
+ "failed",
23
+ "cancelled"
24
+ ]);
25
+ loopRunProofSchema = z.record(z.string(), z.unknown());
26
+ loopRunSummarySchema = z.object({
27
+ id: z.string().uuid(),
28
+ workspaceId: z.string().uuid(),
29
+ loopId: z.string().uuid(),
30
+ loopName: z.string().min(1),
31
+ loopBody: z.string().min(1),
32
+ guardrails: z.array(z.string().min(1)).max(30),
33
+ loopVersion: z.number().int().positive(),
34
+ task: z.string().min(1).max(12e3),
35
+ repoFullName: z.string().min(1).max(240).optional(),
36
+ autonomy: z.enum(["read_only", "repo_edit", "validate", "handoff"]),
37
+ status: loopRunStatusSchema,
38
+ attemptCount: z.number().int().nonnegative(),
39
+ runnerId: z.string().min(1).max(160).nullable(),
40
+ leaseExpiresAt: z.string().datetime().nullable(),
41
+ lastEvent: z.string().max(2e3).nullable(),
42
+ resultSummary: z.string().max(12e3).nullable(),
43
+ proof: loopRunProofSchema,
44
+ errorMessage: z.string().max(4e3).nullable(),
45
+ createdAt: z.string().datetime(),
46
+ startedAt: z.string().datetime().nullable(),
47
+ completedAt: z.string().datetime().nullable()
48
+ });
49
+ createLoopRunRequestSchema = z.object({
50
+ loopId: z.string().uuid(),
51
+ task: z.string().trim().min(1).max(12e3),
52
+ repoFullName: z.string().trim().min(1).max(240).optional(),
53
+ idempotencyKey: z.string().trim().min(16).max(160)
54
+ });
55
+ claimLoopRunRequestSchema = z.object({
56
+ runnerId: z.string().trim().min(1).max(160),
57
+ leaseSeconds: z.number().int().min(30).max(900).default(180)
58
+ });
59
+ updateLoopRunRequestSchema = z.object({
60
+ runnerId: z.string().trim().min(1).max(160),
61
+ status: loopRunStatusSchema,
62
+ lastEvent: z.string().trim().max(2e3).optional(),
63
+ resultSummary: z.string().trim().max(12e3).optional(),
64
+ proof: loopRunProofSchema.optional(),
65
+ errorMessage: z.string().trim().max(4e3).optional(),
66
+ leaseSeconds: z.number().int().min(30).max(900).optional()
67
+ });
18
68
  enforcementSchema = z.enum([
19
69
  "advisory",
20
70
  "warn",
@@ -621,6 +671,46 @@ var init_dist = __esm({
621
671
  applied: z.array(teamGuidanceItemSchema).max(100),
622
672
  omittedCount: z.number().int().nonnegative()
623
673
  });
674
+ dependencyPackageSignalSchema = z.object({
675
+ name: z.string().min(1).max(160),
676
+ changeKind: z.enum([
677
+ "patch_or_minor",
678
+ "major",
679
+ "added_or_overridden",
680
+ "unknown"
681
+ ]),
682
+ manifestPaths: z.array(z.string().min(1).max(500)).max(20),
683
+ lockfilePaths: z.array(z.string().min(1).max(500)).max(20),
684
+ evidence: z.array(z.string().min(1).max(120)).max(10)
685
+ });
686
+ dependencyRemediationSchema = z.object({
687
+ id: z.string().min(1).max(120),
688
+ status: z.enum(["ready", "needs_evidence"]),
689
+ packageManager: z.enum([
690
+ "pnpm",
691
+ "npm",
692
+ "yarn",
693
+ "bun",
694
+ "pip",
695
+ "bundler",
696
+ "go",
697
+ "cargo",
698
+ "unknown"
699
+ ]),
700
+ manifestFiles: z.array(z.string().min(1).max(500)).max(30),
701
+ lockfiles: z.array(z.string().min(1).max(500)).max(20),
702
+ workspaceFiles: z.array(z.string().min(1).max(500)).max(20),
703
+ changedDependencyFiles: z.array(z.string().min(1).max(500)).max(30),
704
+ directDependencyFiles: z.array(z.string().min(1).max(500)).max(30),
705
+ packageSignals: z.array(dependencyPackageSignalSchema).max(20),
706
+ graphCommands: z.array(z.string().min(1).max(500)).max(20),
707
+ remediationCommands: z.array(z.string().min(1).max(500)).max(20),
708
+ validationCommands: z.array(z.string().min(1).max(500)).max(20),
709
+ residualRiskChecks: z.array(z.string().min(1).max(800)).max(20),
710
+ handoffRequirements: z.array(z.string().min(1).max(800)).max(20),
711
+ summary: z.string().max(1200),
712
+ confidence: z.number().min(0).max(100)
713
+ });
624
714
  contextPacketSchema = z.object({
625
715
  packetId: z.string().min(1),
626
716
  createdAt: z.string().datetime(),
@@ -643,7 +733,8 @@ var init_dist = __esm({
643
733
  appliedTeamGuidanceCount: z.number().int().nonnegative(),
644
734
  suggestedTestCount: z.number().int().nonnegative()
645
735
  }).optional(),
646
- teamGuidance: teamGuidanceDeliverySchema.optional()
736
+ teamGuidance: teamGuidanceDeliverySchema.optional(),
737
+ dependencyRemediation: dependencyRemediationSchema.optional()
647
738
  });
648
739
  contextReceiptRequestSchema = z.object({
649
740
  workspaceId: z.string().uuid().optional(),
@@ -674,6 +765,7 @@ var init_dist = __esm({
674
765
  "small_refactor",
675
766
  "bug_investigation",
676
767
  "qa_edge_cases",
768
+ "web_dogfood",
677
769
  "performance",
678
770
  "porting",
679
771
  "experiment",
@@ -5296,22 +5388,22 @@ function likelyRuleLine(line) {
5296
5388
  return hasStrongRule || hasOperationalRule;
5297
5389
  }
5298
5390
  function scopeFromPath(sourcePath, kind) {
5299
- const path20 = sourcePath.replaceAll("\\", "/");
5300
- const directoryScope = scopedDirectory(path20);
5301
- if (directoryScope && (path20.endsWith("/AGENTS.md") || path20.endsWith("/CLAUDE.md")))
5391
+ const path21 = sourcePath.replaceAll("\\", "/");
5392
+ const directoryScope = scopedDirectory(path21);
5393
+ if (directoryScope && (path21.endsWith("/AGENTS.md") || path21.endsWith("/CLAUDE.md")))
5302
5394
  return [directoryScope];
5303
- if (path20.includes("client/") || kind === "frontend")
5395
+ if (path21.includes("client/") || kind === "frontend")
5304
5396
  return ["client/**", "packages/ui/**"];
5305
- if (path20.includes("server/") || kind === "persistence")
5397
+ if (path21.includes("server/") || kind === "persistence")
5306
5398
  return ["server/**"];
5307
- if (path20.includes(".github/") || kind === "workflow")
5399
+ if (path21.includes(".github/") || kind === "workflow")
5308
5400
  return ["**/*"];
5309
- if (path20.includes("test") || kind === "testing")
5401
+ if (path21.includes("test") || kind === "testing")
5310
5402
  return ["**/*.test.*", "**/*.spec.*", "tests/**"];
5311
5403
  return ["**/*"];
5312
5404
  }
5313
- function scopedDirectory(path20) {
5314
- const parts = path20.split("/");
5405
+ function scopedDirectory(path21) {
5406
+ const parts = path21.split("/");
5315
5407
  if (parts.length <= 1)
5316
5408
  return null;
5317
5409
  const directory = parts.slice(0, -1).join("/");
@@ -5381,12 +5473,12 @@ function sourceReadLimit(type) {
5381
5473
  return CODE_SOURCE_MAX_BYTES;
5382
5474
  return void 0;
5383
5475
  }
5384
- function sourceDocument(pathName, type, content) {
5476
+ function sourceDocument(pathName2, type, content) {
5385
5477
  return {
5386
- id: idFor("src", `${pathName}:${content}`),
5387
- path: pathName,
5478
+ id: idFor("src", `${pathName2}:${content}`),
5479
+ path: pathName2,
5388
5480
  type,
5389
- title: titleFromPath(pathName),
5481
+ title: titleFromPath(pathName2),
5390
5482
  excerpt: firstLines(content, 14),
5391
5483
  content,
5392
5484
  contentHash: contentHash(content),
@@ -7531,12 +7623,12 @@ function extractReviewMemories(sources, rules) {
7531
7623
  }
7532
7624
  return memories.slice(0, 32);
7533
7625
  }
7534
- function mapNode(type, pathName, summary, tags, metadata) {
7626
+ function mapNode(type, pathName2, summary, tags, metadata) {
7535
7627
  const node = {
7536
- id: idFor("map", `${type}:${pathName}`),
7628
+ id: idFor("map", `${type}:${pathName2}`),
7537
7629
  type,
7538
- name: titleFromPath(pathName),
7539
- path: pathName,
7630
+ name: titleFromPath(pathName2),
7631
+ path: pathName2,
7540
7632
  summary,
7541
7633
  tags
7542
7634
  };
@@ -8605,7 +8697,7 @@ function agentPacketPrompt(input) {
8605
8697
  ].filter(Boolean).join(" ");
8606
8698
  }
8607
8699
  function inspectReasonForNode(node) {
8608
- const path20 = pathKey(node);
8700
+ const path21 = pathKey(node);
8609
8701
  const systems = metadataStrings2(node, "systems");
8610
8702
  const tables = metadataStrings2(node, "tables");
8611
8703
  const access = metadataStrings2(node, "accessControls");
@@ -8625,7 +8717,7 @@ function inspectReasonForNode(node) {
8625
8717
  return "Entrypoint that frames runtime flow.";
8626
8718
  if (node.type === "component")
8627
8719
  return "UI component surface in mapped scope.";
8628
- return `Mapped ${node.type} node for ${path20}.`;
8720
+ return `Mapped ${node.type} node for ${path21}.`;
8629
8721
  }
8630
8722
  function strongestEntityEvidenceKind(nodes, kind, name) {
8631
8723
  const matchingNodes = nodes.filter((node) => metadataStrings2(node, metadataKeyForEntity(kind)).includes(name));
@@ -8698,17 +8790,17 @@ function topFacet(nodes, key, limit) {
8698
8790
  function topAreaFacet(nodes, limit) {
8699
8791
  const areas = /* @__PURE__ */ new Map();
8700
8792
  for (const node of nodes) {
8701
- const path20 = pathKey(node);
8702
- const prefix = areaPrefix(path20);
8793
+ const path21 = pathKey(node);
8794
+ const prefix = areaPrefix(path21);
8703
8795
  const entry = areas.get(prefix) ?? {
8704
8796
  paths: /* @__PURE__ */ new Set(),
8705
8797
  codeSurfaceCount: 0,
8706
8798
  systems: /* @__PURE__ */ new Set()
8707
8799
  };
8708
- if (!entry.paths.has(path20) && CODE_SURFACE_TYPES.has(node.type)) {
8800
+ if (!entry.paths.has(path21) && CODE_SURFACE_TYPES.has(node.type)) {
8709
8801
  entry.codeSurfaceCount += 1;
8710
8802
  }
8711
- entry.paths.add(path20);
8803
+ entry.paths.add(path21);
8712
8804
  for (const system of metadataStrings2(node, "systems")) {
8713
8805
  entry.systems.add(system);
8714
8806
  }
@@ -10564,7 +10656,7 @@ function wikiBuildPipeline(scan, pages, operationalAtlas, loopPlaybooks, refresh
10564
10656
  },
10565
10657
  {
10566
10658
  id: "publish",
10567
- title: "Publish for loops",
10659
+ title: "Share with loops",
10568
10660
  status: "ready",
10569
10661
  summary: "Mission planning and context packets can cite wiki pages as source-grounded operating context.",
10570
10662
  evidence: ["brief.wiki_outline", "brief.context_for_task"]
@@ -10992,7 +11084,7 @@ function loopPlaybookSpec(loopId) {
10992
11084
  if (loopId === "session_library") {
10993
11085
  return {
10994
11086
  title: "Session library loop",
10995
- summary: "Promote proven agent runs into reusable team skills, loops, styles, and run cards without publishing raw transcripts by default.",
11087
+ summary: "Promote proven agent runs into reusable team skills, loops, styles, and run cards without sharing raw transcripts by default.",
10996
11088
  tools: [
10997
11089
  "brief.agent_session_insights",
10998
11090
  "brief.value_report",
@@ -11013,7 +11105,7 @@ function loopPlaybookSpec(loopId) {
11013
11105
  ],
11014
11106
  privacyBoundary: [
11015
11107
  ...common.privacyBoundary,
11016
- "Publish concise run cards and styles, not raw private sessions, unless explicitly approved."
11108
+ "Share concise run cards and styles, not raw private sessions, unless explicitly approved."
11017
11109
  ],
11018
11110
  systemHints: ["github", "ci"],
11019
11111
  queryHint: "agent session receipts run cards skills loops styles",
@@ -11204,7 +11296,7 @@ function wikiUsageReceipts(pages, options2) {
11204
11296
  loopId: "session_library",
11205
11297
  tool: "brief.mission_plan",
11206
11298
  pageIds: pageIds("Session Library", "Review Memory", "Team Standards", "Maintainers"),
11207
- reason: "Session library loops need approved examples, privacy decisions, maintainer context, and reusable team standards before publishing run cards or skills."
11299
+ reason: "Session library loops need approved examples, privacy decisions, maintainer context, and reusable team standards before sharing run cards or skills."
11208
11300
  },
11209
11301
  {
11210
11302
  id: `wiki_use_${contentHash("docs:overview:structure")}`,
@@ -11387,9 +11479,9 @@ var init_contextWiki = __esm({
11387
11479
  });
11388
11480
 
11389
11481
  // ../core/dist/pathIntent.js
11390
- function namedCodePathIntentScore(path20, task) {
11482
+ function namedCodePathIntentScore(path21, task) {
11391
11483
  const normalizedTask = task.toLowerCase().replace(/[_-]+/g, " ");
11392
- const normalizedPath = (path20.split("#", 1)[0] ?? path20).toLowerCase();
11484
+ const normalizedPath = (path21.split("#", 1)[0] ?? path21).toLowerCase();
11393
11485
  let score = 0;
11394
11486
  const namedTargets = [
11395
11487
  [/\breview diff\b/, /diffreview|diff[-_]review/],
@@ -11535,9 +11627,476 @@ var init_pathIntent = __esm({
11535
11627
  }
11536
11628
  });
11537
11629
 
11630
+ // ../core/dist/dependencyRemediation.js
11631
+ import path6 from "node:path";
11632
+ function buildDependencyRemediationPlan(repoRoot, request) {
11633
+ const files = walkFiles(repoRoot, request.maxFiles ?? 6e3);
11634
+ const manifests = files.filter(isDependencyManifest);
11635
+ const lockfiles = files.filter(isDependencyLockfile);
11636
+ const workspaceFiles = files.filter(isDependencyWorkspaceFile);
11637
+ const changedDependencyFiles = unique((request.changedFiles ?? []).filter(isDependencyFile));
11638
+ const packageManager = inferPackageManager(repoRoot, manifests, lockfiles);
11639
+ const packageSignalInput = {
11640
+ task: request.task,
11641
+ manifests,
11642
+ changedDependencyFiles
11643
+ };
11644
+ if (request.diff)
11645
+ packageSignalInput.diff = request.diff;
11646
+ const packageSignals = packageSignalsFor(repoRoot, packageSignalInput);
11647
+ const directDependencyFiles = unique(packageSignals.flatMap((signal2) => signal2.manifestPaths));
11648
+ const packageNames = packageSignals.map((signal2) => signal2.name);
11649
+ const validationCommands2 = validationCommandsFor2(repoRoot, packageManager);
11650
+ const graphCommands = dependencyGraphCommandsFor(packageManager, packageNames);
11651
+ const remediationCommands = remediationCommandsFor(packageManager, packageNames);
11652
+ const residualRiskChecks = residualRiskChecksFor(packageManager, packageNames, changedDependencyFiles);
11653
+ const confidence = confidenceFor({
11654
+ changedDependencyFiles,
11655
+ packageSignals,
11656
+ lockfiles,
11657
+ validationCommands: validationCommands2
11658
+ });
11659
+ return {
11660
+ id: `dep_${contentHash([
11661
+ request.task,
11662
+ packageManager,
11663
+ changedDependencyFiles.join(","),
11664
+ packageNames.join(",")
11665
+ ].join(":"))}`,
11666
+ status: changedDependencyFiles.length > 0 || packageSignals.length > 0 ? "ready" : "needs_evidence",
11667
+ packageManager,
11668
+ manifestFiles: manifests.slice(0, 20),
11669
+ lockfiles,
11670
+ workspaceFiles,
11671
+ changedDependencyFiles,
11672
+ directDependencyFiles,
11673
+ packageSignals,
11674
+ graphCommands,
11675
+ remediationCommands,
11676
+ validationCommands: validationCommands2,
11677
+ residualRiskChecks,
11678
+ handoffRequirements: [
11679
+ "List advisory ids or scanner finding ids that were fixed.",
11680
+ "Name packages still present in the lockfile with vulnerable versions, or state that the scan/audit is clean.",
11681
+ "Explain any major-version upgrades, overrides, peer dependency changes, skipped advisories, or remaining advisory ids.",
11682
+ "Attach validation commands and CI follow-up results before marking the security mission done."
11683
+ ],
11684
+ summary: summaryFor3({
11685
+ packageManager,
11686
+ manifests,
11687
+ lockfiles,
11688
+ changedDependencyFiles,
11689
+ packageSignals,
11690
+ validationCommands: validationCommands2
11691
+ }),
11692
+ confidence
11693
+ };
11694
+ }
11695
+ function isDependencyRemediationTask(task, files = []) {
11696
+ const normalized = task.toLowerCase();
11697
+ const hasSecuritySignal = /\b(dependabot|vulnerab\w*|cve-|ghsa-|security advisory|security finding|audit finding|osv|sca|dependency-review)\b/.test(normalized);
11698
+ const hasDependencyChangeSignal = /\b(dependency|dependencies|lockfile|package upgrade|package update|package bump|transitive|override|resolution)\b/.test(normalized);
11699
+ return hasSecuritySignal || hasDependencyChangeSignal && files.some(isDependencyFile);
11700
+ }
11701
+ function isDependencyFile(filePath) {
11702
+ const normalized = filePath.replaceAll("\\", "/");
11703
+ return dependencyFilePatterns.some((pattern) => normalized === pattern || normalized.endsWith(`/${pattern}`) || pattern === "package.json" && normalized.endsWith("/package.json"));
11704
+ }
11705
+ function isDependencyLockfile(filePath) {
11706
+ const normalized = filePath.replaceAll("\\", "/");
11707
+ return normalized === "pnpm-lock.yaml" || normalized.endsWith("/pnpm-lock.yaml") || normalized === "package-lock.json" || normalized.endsWith("/package-lock.json") || normalized === "yarn.lock" || normalized.endsWith("/yarn.lock") || normalized === "bun.lock" || normalized.endsWith("/bun.lock") || normalized === "npm-shrinkwrap.json" || normalized.endsWith("/npm-shrinkwrap.json") || normalized === "poetry.lock" || normalized.endsWith("/poetry.lock") || normalized === "Pipfile.lock" || normalized.endsWith("/Pipfile.lock") || normalized === "Gemfile.lock" || normalized.endsWith("/Gemfile.lock") || normalized === "go.sum" || normalized.endsWith("/go.sum") || normalized === "Cargo.lock" || normalized.endsWith("/Cargo.lock");
11708
+ }
11709
+ function isDependencyManifest(filePath) {
11710
+ const normalized = filePath.replaceAll("\\", "/");
11711
+ return normalized === "package.json" || normalized.endsWith("/package.json") || normalized === "requirements.txt" || normalized.endsWith("/requirements.txt") || normalized === "pyproject.toml" || normalized.endsWith("/pyproject.toml") || normalized === "Gemfile" || normalized.endsWith("/Gemfile") || normalized === "go.mod" || normalized.endsWith("/go.mod") || normalized === "Cargo.toml" || normalized.endsWith("/Cargo.toml");
11712
+ }
11713
+ function isDependencyWorkspaceFile(filePath) {
11714
+ const normalized = filePath.replaceAll("\\", "/");
11715
+ return normalized === "pnpm-workspace.yaml" || normalized.endsWith("/pnpm-workspace.yaml") || normalized === "lerna.json" || normalized.endsWith("/lerna.json") || normalized === "turbo.json" || normalized.endsWith("/turbo.json") || normalized === "nx.json" || normalized.endsWith("/nx.json");
11716
+ }
11717
+ function inferPackageManager(repoRoot, manifests, lockfiles) {
11718
+ const rootPackageJson = readPackageJson(repoRoot, "package.json");
11719
+ const packageManager = rootPackageJson?.packageManager;
11720
+ if (typeof packageManager === "string") {
11721
+ if (packageManager.startsWith("pnpm@"))
11722
+ return "pnpm";
11723
+ if (packageManager.startsWith("yarn@"))
11724
+ return "yarn";
11725
+ if (packageManager.startsWith("npm@"))
11726
+ return "npm";
11727
+ if (packageManager.startsWith("bun@"))
11728
+ return "bun";
11729
+ }
11730
+ if (lockfiles.some((file) => file.endsWith("pnpm-lock.yaml")))
11731
+ return "pnpm";
11732
+ if (lockfiles.some((file) => file.endsWith("yarn.lock")))
11733
+ return "yarn";
11734
+ if (lockfiles.some((file) => file.endsWith("package-lock.json"))) {
11735
+ return "npm";
11736
+ }
11737
+ if (lockfiles.some((file) => file.endsWith("bun.lock")))
11738
+ return "bun";
11739
+ if (manifests.some((file) => file.endsWith("requirements.txt") || file.endsWith("pyproject.toml") || file.endsWith("Pipfile"))) {
11740
+ return "pip";
11741
+ }
11742
+ if (manifests.some((file) => file.endsWith("Gemfile")))
11743
+ return "bundler";
11744
+ if (manifests.some((file) => file.endsWith("go.mod")))
11745
+ return "go";
11746
+ if (manifests.some((file) => file.endsWith("Cargo.toml")))
11747
+ return "cargo";
11748
+ return "unknown";
11749
+ }
11750
+ function packageSignalsFor(repoRoot, input) {
11751
+ const manifestDeps = dependencyNamesByManifest(repoRoot, input.manifests);
11752
+ const explicitPackageNames = unique([
11753
+ ...packagesMentionedInText(input.task),
11754
+ ...packagesMentionedInDiff(input.diff ?? "")
11755
+ ]).filter((name) => !looksLikeVersionOrAdvisory(name));
11756
+ const fallbackPackageNames = input.changedDependencyFiles.flatMap((file) => packagesFromChangedDependencyFile(repoRoot, file));
11757
+ const packageNames = unique(explicitPackageNames.length > 0 ? explicitPackageNames : fallbackPackageNames).filter((name) => !looksLikeVersionOrAdvisory(name));
11758
+ return packageNames.slice(0, 16).map((name) => ({
11759
+ name,
11760
+ changeKind: changeKindFor(name, input.diff ?? ""),
11761
+ manifestPaths: manifestsForPackage(name, manifestDeps),
11762
+ lockfilePaths: input.changedDependencyFiles.filter(isDependencyLockfile),
11763
+ evidence: evidenceForPackage(name, input.task, input.diff ?? "")
11764
+ }));
11765
+ }
11766
+ function dependencyNamesByManifest(repoRoot, manifests) {
11767
+ const byPackage = /* @__PURE__ */ new Map();
11768
+ for (const manifest of manifests) {
11769
+ if (!manifest.endsWith("package.json"))
11770
+ continue;
11771
+ const parsed = readPackageJson(repoRoot, manifest);
11772
+ if (!parsed)
11773
+ continue;
11774
+ for (const name of packageJsonDependencyNames(parsed)) {
11775
+ const current = byPackage.get(name) ?? [];
11776
+ current.push(manifest);
11777
+ byPackage.set(name, current);
11778
+ }
11779
+ }
11780
+ return byPackage;
11781
+ }
11782
+ function packageJsonDependencyNames(parsed) {
11783
+ return unique([
11784
+ "dependencies",
11785
+ "devDependencies",
11786
+ "optionalDependencies",
11787
+ "peerDependencies",
11788
+ "overrides",
11789
+ "resolutions",
11790
+ "pnpm"
11791
+ ].flatMap((field) => nestedDependencyNames(parsed[field])));
11792
+ }
11793
+ function nestedDependencyNames(value) {
11794
+ if (!value || typeof value !== "object")
11795
+ return [];
11796
+ return Object.entries(value).flatMap(([key, nested]) => [
11797
+ looksLikePackageName(key) ? key : "",
11798
+ ...nestedDependencyNames(nested)
11799
+ ]);
11800
+ }
11801
+ function packagesMentionedInText(text) {
11802
+ const names = [];
11803
+ const quoted = text.matchAll(/[`'"](@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)[`'"]/gi);
11804
+ for (const match of quoted)
11805
+ names.push(match[1] ?? "");
11806
+ const advisoryContext = text.matchAll(/\b(?:package|dependency|upgrade|update|override|transitive|vulnerable|dependabot|cve|ghsa|audit)\s+(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)/gi);
11807
+ for (const match of advisoryContext)
11808
+ names.push(match[1] ?? "");
11809
+ const atVersion = text.matchAll(/\b(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/gi);
11810
+ for (const match of atVersion)
11811
+ names.push(match[1] ?? "");
11812
+ if (/\b(dependabot|cve-|ghsa-|audit|vulnerab|advisory)\b/i.test(text)) {
11813
+ const affectedPackageContext = text.matchAll(/\b(?:for|affects?|in)\s+(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)/gi);
11814
+ for (const match of affectedPackageContext)
11815
+ names.push(match[1] ?? "");
11816
+ }
11817
+ return unique(names.map(cleanPackageName).filter(looksLikePackageName).filter((name) => !genericDependencyWords.has(name.toLowerCase())));
11818
+ }
11819
+ function packagesMentionedInDiff(diff) {
11820
+ const names = [];
11821
+ for (const line of diff.split(/\r?\n/)) {
11822
+ if (!/^[+-]/.test(line) || /^[+-]{3}/.test(line))
11823
+ continue;
11824
+ const packageJsonMatch = line.match(/"(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)"\s*:/i);
11825
+ if (packageJsonMatch?.[1])
11826
+ names.push(packageJsonMatch[1]);
11827
+ const pnpmMatch = line.match(/^\s*[+-]\s{0,8}(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/i);
11828
+ if (pnpmMatch?.[1])
11829
+ names.push(pnpmMatch[1]);
11830
+ const lockPathMatch = line.match(/\/(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/i);
11831
+ if (lockPathMatch?.[1])
11832
+ names.push(lockPathMatch[1]);
11833
+ }
11834
+ return unique(names.map(cleanPackageName).filter(looksLikePackageName).filter((name) => !genericDependencyWords.has(name.toLowerCase())));
11835
+ }
11836
+ function packagesFromChangedDependencyFile(repoRoot, filePath) {
11837
+ if (!filePath.endsWith("package.json"))
11838
+ return [];
11839
+ const parsed = readPackageJson(repoRoot, filePath);
11840
+ return parsed ? packageJsonDependencyNames(parsed).slice(0, 20) : [];
11841
+ }
11842
+ function readPackageJson(repoRoot, filePath) {
11843
+ const content = safeRead(path6.join(repoRoot, filePath), 4e5);
11844
+ if (!content)
11845
+ return null;
11846
+ try {
11847
+ const parsed = JSON.parse(content);
11848
+ return parsed && typeof parsed === "object" ? parsed : null;
11849
+ } catch {
11850
+ return null;
11851
+ }
11852
+ }
11853
+ function changeKindFor(packageName, diff) {
11854
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11855
+ const removedVersions = [
11856
+ ...diff.matchAll(new RegExp(`^-.*${escaped}.*?(\\d+\\.\\d+\\.\\d+)`, "gim"))
11857
+ ].map((match) => match[1] ?? "");
11858
+ const addedVersions = [
11859
+ ...diff.matchAll(new RegExp(`^\\+.*${escaped}.*?(\\d+\\.\\d+\\.\\d+)`, "gim"))
11860
+ ].map((match) => match[1] ?? "");
11861
+ const removed = removedVersions[0];
11862
+ const added = addedVersions[0];
11863
+ if (removed && added) {
11864
+ const [removedMajor] = removed.split(".");
11865
+ const [addedMajor] = added.split(".");
11866
+ return removedMajor && addedMajor && removedMajor !== addedMajor ? "major" : "patch_or_minor";
11867
+ }
11868
+ if (added && !removed)
11869
+ return "added_or_overridden";
11870
+ return "unknown";
11871
+ }
11872
+ function manifestsForPackage(packageName, manifestDeps) {
11873
+ return unique([
11874
+ ...manifestDeps.get(packageName) ?? [],
11875
+ ...[...manifestDeps.entries()].filter(([name]) => name.endsWith(`/${packageName}`)).flatMap(([, manifests]) => manifests)
11876
+ ]).slice(0, 8);
11877
+ }
11878
+ function evidenceForPackage(packageName, task, diff) {
11879
+ const evidence = [];
11880
+ if (task.toLowerCase().includes(packageName.toLowerCase())) {
11881
+ evidence.push("mentioned_in_task");
11882
+ }
11883
+ if (diff.toLowerCase().includes(packageName.toLowerCase())) {
11884
+ evidence.push("mentioned_in_diff");
11885
+ }
11886
+ if (evidence.length === 0)
11887
+ evidence.push("manifest_candidate");
11888
+ return evidence;
11889
+ }
11890
+ function validationCommandsFor2(repoRoot, packageManager) {
11891
+ const scripts = packageScripts(repoRoot);
11892
+ const run = packageRunCommand(packageManager);
11893
+ return unique([
11894
+ ...auditCommandsFor(packageManager),
11895
+ ...scriptCommand(scripts, "check", run),
11896
+ ...scriptCommand(scripts, "test", run),
11897
+ ...scriptCommand(scripts, "test:client", run),
11898
+ ...scriptCommand(scripts, "test:e2e:smoke", run),
11899
+ ...scriptCommand(scripts, "build", run)
11900
+ ]).slice(0, 10);
11901
+ }
11902
+ function packageScripts(repoRoot) {
11903
+ const parsed = readPackageJson(repoRoot, "package.json");
11904
+ const scripts = parsed?.scripts;
11905
+ return scripts && typeof scripts === "object" ? Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string")) : {};
11906
+ }
11907
+ function scriptCommand(scripts, scriptName, runCommand) {
11908
+ return scripts[scriptName] ? [`${runCommand} ${scriptName}`] : [];
11909
+ }
11910
+ function packageRunCommand(packageManager) {
11911
+ switch (packageManager) {
11912
+ case "npm":
11913
+ return "npm run";
11914
+ case "yarn":
11915
+ return "yarn";
11916
+ case "bun":
11917
+ return "bun run";
11918
+ default:
11919
+ return "pnpm";
11920
+ }
11921
+ }
11922
+ function auditCommandsFor(packageManager) {
11923
+ switch (packageManager) {
11924
+ case "pnpm":
11925
+ return ["pnpm audit --prod"];
11926
+ case "npm":
11927
+ return ["npm audit --omit=dev"];
11928
+ case "yarn":
11929
+ return ["yarn npm audit --environment production"];
11930
+ case "bun":
11931
+ return ["bun audit"];
11932
+ case "pip":
11933
+ return ["pip-audit"];
11934
+ case "bundler":
11935
+ return ["bundle audit check"];
11936
+ case "go":
11937
+ return ["govulncheck ./..."];
11938
+ case "cargo":
11939
+ return ["cargo audit"];
11940
+ default:
11941
+ return [];
11942
+ }
11943
+ }
11944
+ function dependencyGraphCommandsFor(packageManager, packageNames) {
11945
+ const selected = packageNames.slice(0, 5);
11946
+ if (selected.length === 0)
11947
+ return [];
11948
+ switch (packageManager) {
11949
+ case "pnpm":
11950
+ return selected.map((name) => `pnpm why ${name}`);
11951
+ case "npm":
11952
+ return selected.map((name) => `npm explain ${name}`);
11953
+ case "yarn":
11954
+ return selected.map((name) => `yarn why ${name}`);
11955
+ case "bun":
11956
+ return selected.map((name) => `bun pm why ${name}`);
11957
+ case "go":
11958
+ return selected.map((name) => `go mod why -m ${name}`);
11959
+ case "cargo":
11960
+ return selected.map((name) => `cargo tree -i ${name}`);
11961
+ default:
11962
+ return [];
11963
+ }
11964
+ }
11965
+ function remediationCommandsFor(packageManager, packageNames) {
11966
+ const selected = packageNames.slice(0, 5);
11967
+ if (selected.length === 0)
11968
+ return [];
11969
+ switch (packageManager) {
11970
+ case "pnpm":
11971
+ return selected.map((name) => `pnpm up ${name} --latest --interactive=false`);
11972
+ case "npm":
11973
+ return selected.map((name) => `npm update ${name}`);
11974
+ case "yarn":
11975
+ return selected.map((name) => `yarn up ${name}`);
11976
+ case "bun":
11977
+ return selected.map((name) => `bun update ${name}`);
11978
+ case "go":
11979
+ return selected.map((name) => `go get ${name}@latest`);
11980
+ case "cargo":
11981
+ return selected.map((name) => `cargo update -p ${name}`);
11982
+ default:
11983
+ return [];
11984
+ }
11985
+ }
11986
+ function residualRiskChecksFor(packageManager, packageNames, changedDependencyFiles) {
11987
+ return unique([
11988
+ packageNames.length > 0 ? `Confirm vulnerable versions are gone for: ${packageNames.slice(0, 8).join(", ")}.` : "Name each advisory/package from the scanner before editing.",
11989
+ changedDependencyFiles.some(isDependencyLockfile) ? "Inspect the lockfile diff for unexpected package churn outside the advisory path." : "Regenerate and inspect the lockfile if the fix changes dependency resolution.",
11990
+ "Split major-version upgrades or broad framework upgrades into a separate mission unless the advisory requires them.",
11991
+ "Record any remaining advisory ids, ignored dev-only findings, or transitive paths that still need owner judgment.",
11992
+ ...auditCommandsFor(packageManager).map((command2) => `Rerun ${command2} after the lockfile update.`)
11993
+ ]);
11994
+ }
11995
+ function confidenceFor(input) {
11996
+ let score = 35;
11997
+ if (input.changedDependencyFiles.length > 0)
11998
+ score += 20;
11999
+ if (input.packageSignals.length > 0)
12000
+ score += 20;
12001
+ if (input.lockfiles.length > 0)
12002
+ score += 10;
12003
+ if (input.validationCommands.length > 0)
12004
+ score += 10;
12005
+ return Math.min(95, score);
12006
+ }
12007
+ function summaryFor3(input) {
12008
+ const packages = input.packageSignals.map((signal2) => signal2.name);
12009
+ return [
12010
+ `Detected ${input.packageManager} dependency remediation context.`,
12011
+ `${input.manifests.length} manifest${input.manifests.length === 1 ? "" : "s"}, ${input.lockfiles.length} lockfile${input.lockfiles.length === 1 ? "" : "s"}.`,
12012
+ input.changedDependencyFiles.length > 0 ? `Changed dependency files: ${input.changedDependencyFiles.slice(0, 6).join(", ")}.` : "No dependency files are changed yet.",
12013
+ packages.length > 0 ? `Likely packages: ${packages.slice(0, 8).join(", ")}.` : "Package names still need advisory/scanner evidence.",
12014
+ input.validationCommands.length > 0 ? `Validation starts with ${input.validationCommands[0]}.` : "Validation commands need repo-specific confirmation."
12015
+ ].join(" ");
12016
+ }
12017
+ function cleanPackageName(input) {
12018
+ return input.replace(/[),.;\]]+$/g, "").trim();
12019
+ }
12020
+ function looksLikePackageName(input) {
12021
+ return /^@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?$/i.test(input);
12022
+ }
12023
+ function looksLikeVersionOrAdvisory(input) {
12024
+ const normalized = input.toLowerCase();
12025
+ return /^\d/.test(input) || /^cve-\d/i.test(input) || /^ghsa-/i.test(input) || [
12026
+ "advisory",
12027
+ "advisories",
12028
+ "change",
12029
+ "changes",
12030
+ "dependency",
12031
+ "dependencies",
12032
+ "dependabot",
12033
+ "finding",
12034
+ "findings",
12035
+ "package",
12036
+ "packages",
12037
+ "security",
12038
+ "transitive",
12039
+ "upgrade",
12040
+ "upgrades",
12041
+ "vulnerabilities",
12042
+ "vulnerability",
12043
+ "vulnerable"
12044
+ ].includes(normalized);
12045
+ }
12046
+ var dependencyFilePatterns, genericDependencyWords;
12047
+ var init_dependencyRemediation = __esm({
12048
+ "../core/dist/dependencyRemediation.js"() {
12049
+ "use strict";
12050
+ init_utils();
12051
+ dependencyFilePatterns = [
12052
+ "package.json",
12053
+ "pnpm-lock.yaml",
12054
+ "pnpm-workspace.yaml",
12055
+ "package-lock.json",
12056
+ "yarn.lock",
12057
+ "bun.lock",
12058
+ "npm-shrinkwrap.json",
12059
+ "requirements.txt",
12060
+ "poetry.lock",
12061
+ "pyproject.toml",
12062
+ "Pipfile.lock",
12063
+ "Gemfile",
12064
+ "Gemfile.lock",
12065
+ "go.mod",
12066
+ "go.sum",
12067
+ "Cargo.toml",
12068
+ "Cargo.lock"
12069
+ ];
12070
+ genericDependencyWords = /* @__PURE__ */ new Set([
12071
+ "advisory",
12072
+ "audit",
12073
+ "build",
12074
+ "change",
12075
+ "dependency",
12076
+ "dev",
12077
+ "exact",
12078
+ "existing",
12079
+ "focused",
12080
+ "for",
12081
+ "finding",
12082
+ "lockfile",
12083
+ "package",
12084
+ "path",
12085
+ "security",
12086
+ "test",
12087
+ "the",
12088
+ "toolchain",
12089
+ "transitive",
12090
+ "vulnerability",
12091
+ "workflow",
12092
+ "without"
12093
+ ]);
12094
+ }
12095
+ });
12096
+
11538
12097
  // ../core/dist/testRecommendations.js
11539
12098
  import { existsSync as existsSync3 } from "node:fs";
11540
- import path6 from "node:path";
12099
+ import path7 from "node:path";
11541
12100
  function recommendExactTestsForFiles(input) {
11542
12101
  const packageManager = detectPackageManager(input.repoRoot);
11543
12102
  if (isDocsOnlyChange(input.changedFiles)) {
@@ -11691,7 +12250,7 @@ function taskRelevantTests(repoRoot, scanMap, scanSources, task, excluded) {
11691
12250
  }));
11692
12251
  return [...mapTests, ...sourceTests].filter((test) => !excludedSet.has(test.path)).map((test) => {
11693
12252
  const pathScore = tokens.reduce((total, token) => total + (test.seed.includes(token) ? 2 : 0), 0);
11694
- const content = pathScore > 0 ? (safeRead(path6.join(repoRoot, test.path), 6e4) ?? "").toLowerCase() : "";
12253
+ const content = pathScore > 0 ? (safeRead(path7.join(repoRoot, test.path), 6e4) ?? "").toLowerCase() : "";
11695
12254
  return {
11696
12255
  path: test.path,
11697
12256
  score: pathScore + tokens.reduce((total, token) => total + (test.context.includes(token) ? 1 : 0), 0) + tokens.reduce((total, token) => total + (content.includes(token) ? 1 : 0), 0)
@@ -11755,8 +12314,8 @@ function testCommandForFile(repoRoot, file, packageManager) {
11755
12314
  if (packageManager === "pnpm") {
11756
12315
  const packageInfo = nearestPackageInfo(repoRoot, file);
11757
12316
  if (packageInfo?.name && packageInfo.root !== repoRoot) {
11758
- const packageRelativeFile = path6.relative(packageInfo.root, path6.join(repoRoot, file)).replaceAll("\\", "/");
11759
- const testScript = readPackageJson(packageInfo.root)?.scripts?.test;
12317
+ const packageRelativeFile = path7.relative(packageInfo.root, path7.join(repoRoot, file)).replaceAll("\\", "/");
12318
+ const testScript = readPackageJson2(packageInfo.root)?.scripts?.test;
11760
12319
  if (typeof testScript === "string") {
11761
12320
  if (/\bvitest\b/.test(testScript)) {
11762
12321
  return `pnpm --filter ${packageInfo.name} exec vitest run ${packageRelativeFile}`;
@@ -11774,11 +12333,11 @@ function testCommandForFile(repoRoot, file, packageManager) {
11774
12333
  return `${packageManager} test -- ${file}`;
11775
12334
  }
11776
12335
  function nearestPackageInfo(repoRoot, file) {
11777
- let current = path6.dirname(path6.join(repoRoot, file));
12336
+ let current = path7.dirname(path7.join(repoRoot, file));
11778
12337
  while (current.startsWith(repoRoot)) {
11779
- const packageJsonPath = path6.join(current, "package.json");
12338
+ const packageJsonPath = path7.join(current, "package.json");
11780
12339
  if (existsSync3(packageJsonPath)) {
11781
- const pkg = readPackageJson(current);
12340
+ const pkg = readPackageJson2(current);
11782
12341
  return {
11783
12342
  root: current,
11784
12343
  ...typeof pkg?.name === "string" ? { name: pkg.name } : {}
@@ -11786,18 +12345,18 @@ function nearestPackageInfo(repoRoot, file) {
11786
12345
  }
11787
12346
  if (current === repoRoot)
11788
12347
  break;
11789
- current = path6.dirname(current);
12348
+ current = path7.dirname(current);
11790
12349
  }
11791
12350
  return null;
11792
12351
  }
11793
12352
  function coLocatedTestCandidates(repoRoot, file) {
11794
- const parsed = path6.parse(file);
12353
+ const parsed = path7.parse(file);
11795
12354
  const extensions = parsed.ext === ".tsx" ? [".test.tsx", ".spec.tsx", ".test.ts", ".spec.ts"] : parsed.ext === ".jsx" ? [".test.jsx", ".spec.jsx", ".test.js", ".spec.js"] : [`.test${parsed.ext}`, `.spec${parsed.ext}`];
11796
12355
  const candidates = [
11797
- ...extensions.map((extension) => path6.join(parsed.dir, `${parsed.name}${extension}`)),
11798
- ...extensions.map((extension) => path6.join(parsed.dir, "__tests__", `${parsed.name}${extension}`))
12356
+ ...extensions.map((extension) => path7.join(parsed.dir, `${parsed.name}${extension}`)),
12357
+ ...extensions.map((extension) => path7.join(parsed.dir, "__tests__", `${parsed.name}${extension}`))
11799
12358
  ].map((candidate) => candidate.replaceAll("\\", "/"));
11800
- return candidates.filter((candidate) => existsSync3(path6.join(repoRoot, candidate)));
12359
+ return candidates.filter((candidate) => existsSync3(path7.join(repoRoot, candidate)));
11801
12360
  }
11802
12361
  function importedByTests(scanMap, sourceFiles) {
11803
12362
  const fromSourceMetadata = scanMap.filter((node) => sourceFiles.includes(sourcePathForNode(node))).flatMap((node) => metadataStrings3(node, "importedBy")).filter(isTestPath);
@@ -11819,11 +12378,11 @@ function isDocsOnlyChange(files) {
11819
12378
  }
11820
12379
  function isDocsOrProductDocPath(file) {
11821
12380
  const normalized = file.replaceAll("\\", "/");
11822
- const basename = path6.basename(normalized).toLowerCase();
12381
+ const basename = path7.basename(normalized).toLowerCase();
11823
12382
  return normalized.startsWith("docs/") || normalized.includes("/docs/") || normalized.startsWith(".github/") || basename === "readme.md" || basename === "agents.md" || basename === "claude.md" || basename.endsWith(".md") || basename.endsWith(".mdx") || basename.endsWith(".rst") || basename.endsWith(".adoc");
11824
12383
  }
11825
12384
  function docsOnlyRecommendations(repoRoot, packageManager, changedFiles) {
11826
- const pkg = readPackageJson(repoRoot);
12385
+ const pkg = readPackageJson2(repoRoot);
11827
12386
  const scripts = pkg?.scripts ?? {};
11828
12387
  const recommendations = [
11829
12388
  {
@@ -11847,7 +12406,7 @@ function docsOnlyRecommendations(repoRoot, packageManager, changedFiles) {
11847
12406
  if (typeof scripts[script] === "string") {
11848
12407
  recommendations.push({
11849
12408
  command: `${packageManager} ${script}`,
11850
- reason: `Repo package.json defines ${script}; run it when docs or agent guidance can affect generated output or published docs.`,
12409
+ reason: `Repo package.json defines ${script}; run it when docs or agent guidance can affect generated output or shared docs.`,
11851
12410
  files: [],
11852
12411
  source: "script",
11853
12412
  confidence: script === "check" ? "low" : "medium"
@@ -11857,7 +12416,7 @@ function docsOnlyRecommendations(repoRoot, packageManager, changedFiles) {
11857
12416
  return recommendations.slice(0, 4);
11858
12417
  }
11859
12418
  function packageScriptRecommendations(repoRoot, packageManager) {
11860
- const pkg = readPackageJson(repoRoot);
12419
+ const pkg = readPackageJson2(repoRoot);
11861
12420
  const scripts = pkg?.scripts ?? {};
11862
12421
  const commands = [];
11863
12422
  for (const script of [
@@ -11890,7 +12449,7 @@ function taskRelevantPackageScriptRecommendations(repoRoot, packageManager, task
11890
12449
  const packageRoots = unique(sourceFiles.map((file) => nearestPackageInfo(repoRoot, file)?.root).filter((root) => Boolean(root)));
11891
12450
  const recommendations = [];
11892
12451
  for (const packageRoot of packageRoots) {
11893
- const pkg = readPackageJson(packageRoot);
12452
+ const pkg = readPackageJson2(packageRoot);
11894
12453
  const scripts = pkg?.scripts ?? {};
11895
12454
  const packageName = typeof pkg?.name === "string" ? pkg.name : void 0;
11896
12455
  if (wantsPackageRelease) {
@@ -11923,8 +12482,8 @@ function packageScriptCommand(packageManager, packageRoot, repoRoot, packageName
11923
12482
  }
11924
12483
  return packageManager === "npm" ? `npm run ${script}` : `${packageManager} ${script}`;
11925
12484
  }
11926
- function readPackageJson(packageRoot) {
11927
- const content = safeRead(path6.join(packageRoot, "package.json"), 2e5);
12485
+ function readPackageJson2(packageRoot) {
12486
+ const content = safeRead(path7.join(packageRoot, "package.json"), 2e5);
11928
12487
  if (!content)
11929
12488
  return null;
11930
12489
  try {
@@ -11940,17 +12499,17 @@ function shellQuote(value) {
11940
12499
  return `'${value.replaceAll("'", `'"'"'`)}'`;
11941
12500
  }
11942
12501
  function detectPackageManager(repoRoot) {
11943
- const pkg = readPackageJson(repoRoot);
12502
+ const pkg = readPackageJson2(repoRoot);
11944
12503
  if (typeof pkg?.packageManager === "string") {
11945
12504
  const name = pkg.packageManager.split("@")[0];
11946
12505
  if (name)
11947
12506
  return name;
11948
12507
  }
11949
- if (existsSync3(path6.join(repoRoot, "pnpm-lock.yaml")))
12508
+ if (existsSync3(path7.join(repoRoot, "pnpm-lock.yaml")))
11950
12509
  return "pnpm";
11951
- if (existsSync3(path6.join(repoRoot, "yarn.lock")))
12510
+ if (existsSync3(path7.join(repoRoot, "yarn.lock")))
11952
12511
  return "yarn";
11953
- if (existsSync3(path6.join(repoRoot, "bun.lockb")))
12512
+ if (existsSync3(path7.join(repoRoot, "bun.lockb")))
11954
12513
  return "bun";
11955
12514
  return "npm";
11956
12515
  }
@@ -11967,6 +12526,10 @@ function contextForTask(scan, request) {
11967
12526
  const files = request.files ?? [];
11968
12527
  const maxTokens = request.maxTokens ?? 6e3;
11969
12528
  const scoped = files.length > 0;
12529
+ const dependencyRemediation = isDependencyRemediationTask(task, files) ? buildDependencyRemediationPlan(request.repoRoot ?? scan.repoRoot, {
12530
+ task,
12531
+ changedFiles: files
12532
+ }) : void 0;
11970
12533
  const rankedRules = rankRules(scan.rules, task, files);
11971
12534
  const initialRules = rankedRules.slice(0, scoped ? 8 : 12);
11972
12535
  const initialSkills = selectSkills(scan.skills, task, files, scoped).map(compactSkillForContext);
@@ -11974,13 +12537,18 @@ function contextForTask(scan, request) {
11974
12537
  const rankedMap = rankMap(mapCandidates, task, files);
11975
12538
  const selectedMap = takeGuidanceGroundedMapNodes(mapCandidates, scan.sources, rulesStronglyRelevantForGrounding(initialRules, task), initialSkills, rankedMap, files, scoped ? 14 : 18).map(compactMapNodeForContext);
11976
12539
  const suggestedTests = selectValidationForContext(scan.map, scan.sources, task, files, selectedMap, request.repoRoot);
12540
+ const dependencyTests = dependencyValidationForContext(dependencyRemediation, task);
12541
+ const focusedSuggestedTests = dependencyRemediation ? dependencyTests : suggestedTests;
11977
12542
  const initialSelection = {
11978
12543
  rules: initialRules,
11979
12544
  skills: initialSkills,
11980
12545
  map: selectedMap,
11981
- wikiPages: selectWikiPages(buildContextWiki(scan).pages, task, files, scoped, suggestedTests).map(compactWikiPageForContext),
12546
+ wikiPages: selectWikiPages(buildContextWiki(scan).pages, task, files, scoped, focusedSuggestedTests).map(compactWikiPageForContext),
11982
12547
  memories: rankMemories(scan.memories, task, files).slice(0, scoped ? 4 : 8).map(compactMemoryForContext),
11983
- suggestedTests
12548
+ suggestedTests: unique([...dependencyTests, ...focusedSuggestedTests]),
12549
+ ...dependencyRemediation ? {
12550
+ dependencyRemediation: compactDependencyRemediationForContext(dependencyRemediation)
12551
+ } : {}
11984
12552
  };
11985
12553
  const { selection, tokenEstimate, omitted } = fitSelectionToBudget(initialSelection, {
11986
12554
  maxTokens,
@@ -11991,32 +12559,39 @@ function contextForTask(scan, request) {
11991
12559
  const { rules: selectedRules, skills: selectedSkills, map: budgetedMap, wikiPages: selectedWikiPages, memories: selectedMemories } = selection;
11992
12560
  const finalMap = takeGuidanceGroundedMapNodes(mapCandidates, scan.sources, rulesStronglyRelevantForGrounding(selectedRules, task), selectedSkills, budgetedMap, files, Math.max(budgetedMap.length, scoped ? 2 : 4)).map(compactMapNodeForContext);
11993
12561
  const finalSuggestedTests = selectValidationForContext(scan.map, scan.sources, task, files, finalMap, request.repoRoot);
12562
+ const packetSuggestedTests = unique([
12563
+ ...dependencyRemediation ? dependencyValidationForContext(dependencyRemediation, task) : finalSuggestedTests
12564
+ ]);
11994
12565
  const owners = selectOwners(scan.map, files);
11995
12566
  const sources = unique([
11996
12567
  ...selectedRules.flatMap((rule) => rule.sourcePaths),
11997
12568
  ...selectedSkills.map((skill) => skill.sourcePath),
11998
12569
  ...finalMap.map(mapNodeSourcePath),
11999
12570
  ...selectedWikiPages.flatMap((page2) => page2.sourcePaths),
12000
- ...selectedMemories.flatMap((memory) => memory.sourcePaths)
12571
+ ...selectedMemories.flatMap((memory) => memory.sourcePaths),
12572
+ ...dependencySourcePaths(dependencyRemediation)
12001
12573
  ]);
12002
12574
  const teamGuidance = scan.teamGuidanceCatalog ? teamGuidanceDeliveryFor(scan, selectedRules, selectedSkills, selectedMemories) : void 0;
12003
12575
  const packet = {
12004
12576
  packetId: packetId(),
12005
12577
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
12006
12578
  task,
12007
- summary: summarizeContext(task, selectedRules, finalSuggestedTests, finalMap),
12579
+ summary: summarizeContext(task, selectedRules, packetSuggestedTests, finalMap),
12008
12580
  rules: selectedRules,
12009
12581
  skills: selectedSkills,
12010
12582
  map: finalMap,
12011
12583
  wikiPages: selectedWikiPages,
12012
12584
  memories: selectedMemories,
12013
- suggestedTests: finalSuggestedTests,
12585
+ suggestedTests: packetSuggestedTests,
12014
12586
  owners,
12015
12587
  sources,
12016
12588
  tokenEstimate,
12017
12589
  omitted,
12018
- valueSignals: contextValueSignals(finalMap, finalSuggestedTests, sources.length, teamGuidance?.applied.length ?? 0),
12019
- ...teamGuidance ? { teamGuidance } : {}
12590
+ valueSignals: contextValueSignals(finalMap, packetSuggestedTests, sources.length, teamGuidance?.applied.length ?? 0),
12591
+ ...teamGuidance ? { teamGuidance } : {},
12592
+ ...dependencyRemediation ? {
12593
+ dependencyRemediation: compactDependencyRemediationForContext(dependencyRemediation)
12594
+ } : {}
12020
12595
  };
12021
12596
  return enforceContextPacketBudget(packet, maxTokens, files);
12022
12597
  }
@@ -12050,6 +12625,47 @@ function contextValueSignals(map, suggestedTests, relevantSourceCount, appliedTe
12050
12625
  suggestedTestCount: suggestedTests.length
12051
12626
  };
12052
12627
  }
12628
+ function dependencyValidationForContext(plan, task) {
12629
+ if (!plan)
12630
+ return [];
12631
+ const allowsRuntimeProof = /\b(runtime|smoke|e2e|browser|production|integration)\b/i.test(task);
12632
+ return plan.validationCommands.filter((command2) => allowsRuntimeProof ? true : !/\b(e2e|smoke|ux)\b/i.test(command2)).slice(0, allowsRuntimeProof ? 5 : 4);
12633
+ }
12634
+ function dependencySourcePaths(plan) {
12635
+ if (!plan)
12636
+ return [];
12637
+ return unique([
12638
+ ...plan.manifestFiles,
12639
+ ...plan.lockfiles,
12640
+ ...plan.workspaceFiles,
12641
+ ...plan.changedDependencyFiles,
12642
+ ...plan.directDependencyFiles
12643
+ ]);
12644
+ }
12645
+ function compactDependencyRemediationForContext(plan) {
12646
+ if (!plan)
12647
+ return void 0;
12648
+ return {
12649
+ ...plan,
12650
+ manifestFiles: plan.manifestFiles.slice(0, 8),
12651
+ lockfiles: plan.lockfiles.slice(0, 6),
12652
+ workspaceFiles: plan.workspaceFiles.slice(0, 4),
12653
+ changedDependencyFiles: plan.changedDependencyFiles.slice(0, 8),
12654
+ directDependencyFiles: plan.directDependencyFiles.slice(0, 8),
12655
+ packageSignals: plan.packageSignals.slice(0, 8).map((signal2) => ({
12656
+ ...signal2,
12657
+ manifestPaths: signal2.manifestPaths.slice(0, 4),
12658
+ lockfilePaths: signal2.lockfilePaths.slice(0, 4),
12659
+ evidence: signal2.evidence.slice(0, 3)
12660
+ })),
12661
+ graphCommands: plan.graphCommands.slice(0, 6),
12662
+ remediationCommands: plan.remediationCommands.slice(0, 6),
12663
+ validationCommands: plan.validationCommands.slice(0, 8),
12664
+ residualRiskChecks: plan.residualRiskChecks.slice(0, 6),
12665
+ handoffRequirements: plan.handoffRequirements.slice(0, 4),
12666
+ summary: truncateText(plan.summary, 520)
12667
+ };
12668
+ }
12053
12669
  function codebaseMapForTask(scan, request, limit = 80) {
12054
12670
  const files = request.files ?? [];
12055
12671
  const hasSpecificScope = files.length > 0 || wordsFor3(request.task).length > 1;
@@ -12858,8 +13474,8 @@ function sameStem(testPath, sourcePath) {
12858
13474
  function isTestScriptNode(node) {
12859
13475
  return node.type === "workflow" && node.tags.some((tag) => /test|check|type|e2e|smoke/i.test(tag));
12860
13476
  }
12861
- function isTestPathCandidate(pathName) {
12862
- return /(^|\/|\.)(test|spec)\.[^.]+$/i.test(pathName);
13477
+ function isTestPathCandidate(pathName2) {
13478
+ return /(^|\/|\.)(test|spec)\.[^.]+$/i.test(pathName2);
12863
13479
  }
12864
13480
  function summarizeContext(task, rules, tests, map = []) {
12865
13481
  const implementationPaths = contextSummaryPaths(map);
@@ -12927,7 +13543,8 @@ function cloneSelection(selection) {
12927
13543
  map: [...selection.map],
12928
13544
  wikiPages: [...selection.wikiPages],
12929
13545
  memories: [...selection.memories],
12930
- suggestedTests: [...selection.suggestedTests]
13546
+ suggestedTests: [...selection.suggestedTests],
13547
+ ...selection.dependencyRemediation ? { dependencyRemediation: selection.dependencyRemediation } : {}
12931
13548
  };
12932
13549
  }
12933
13550
  function estimateSelectionTokens(selection, task) {
@@ -12942,6 +13559,7 @@ function estimateSelectionTokens(selection, task) {
12942
13559
  wikiPages: selection.wikiPages,
12943
13560
  memories: selection.memories,
12944
13561
  suggestedTests: selection.suggestedTests,
13562
+ ...selection.dependencyRemediation ? { dependencyRemediation: selection.dependencyRemediation } : {},
12945
13563
  owners: [],
12946
13564
  sources: selectionSources(selection),
12947
13565
  tokenEstimate: 0,
@@ -13038,10 +13656,6 @@ function trimContextPacket(packet, protectedMapPaths) {
13038
13656
  packet.suggestedTests.pop();
13039
13657
  return true;
13040
13658
  }
13041
- if (packet.rules.length > 0) {
13042
- packet.rules.pop();
13043
- return true;
13044
- }
13045
13659
  if (packet.map.length > protectedMapPaths.size) {
13046
13660
  packet.map.pop();
13047
13661
  return true;
@@ -13136,7 +13750,8 @@ function refreshContextPacketDerivedFields(packet) {
13136
13750
  map: packet.map,
13137
13751
  wikiPages: packet.wikiPages,
13138
13752
  memories: packet.memories,
13139
- suggestedTests: packet.suggestedTests
13753
+ suggestedTests: packet.suggestedTests,
13754
+ ...packet.dependencyRemediation ? { dependencyRemediation: packet.dependencyRemediation } : {}
13140
13755
  });
13141
13756
  refreshTeamGuidanceDelivery(packet);
13142
13757
  packet.valueSignals = contextValueSignals(packet.map, packet.suggestedTests, packet.sources.length, packet.teamGuidance?.applied.length ?? 0);
@@ -13188,7 +13803,8 @@ function selectionSources(selection) {
13188
13803
  ...selection.skills.map((skill) => skill.sourcePath),
13189
13804
  ...selection.map.map((node) => node.path),
13190
13805
  ...selection.wikiPages.flatMap((page2) => page2.sourcePaths),
13191
- ...selection.memories.flatMap((memory) => memory.sourcePaths)
13806
+ ...selection.memories.flatMap((memory) => memory.sourcePaths),
13807
+ ...dependencySourcePaths(selection.dependencyRemediation)
13192
13808
  ]);
13193
13809
  }
13194
13810
  function trimSelection(selection, minimums, protectedMapPaths) {
@@ -13333,6 +13949,7 @@ var init_context = __esm({
13333
13949
  init_scanner();
13334
13950
  init_contextWiki();
13335
13951
  init_pathIntent();
13952
+ init_dependencyRemediation();
13336
13953
  init_testRecommendations();
13337
13954
  init_utils();
13338
13955
  contextStopWords = /* @__PURE__ */ new Set([
@@ -13405,440 +14022,328 @@ var init_context = __esm({
13405
14022
  }
13406
14023
  });
13407
14024
 
13408
- // ../core/dist/dependencyRemediation.js
13409
- import path7 from "node:path";
13410
- function buildDependencyRemediationPlan(repoRoot, request) {
13411
- const files = walkFiles(repoRoot, request.maxFiles ?? 6e3);
13412
- const manifests = files.filter(isDependencyManifest);
13413
- const lockfiles = files.filter(isDependencyLockfile);
13414
- const workspaceFiles = files.filter(isDependencyWorkspaceFile);
13415
- const changedDependencyFiles = unique((request.changedFiles ?? []).filter(isDependencyFile));
13416
- const packageManager = inferPackageManager(repoRoot, manifests, lockfiles);
13417
- const packageSignalInput = {
13418
- task: request.task,
13419
- manifests,
13420
- changedDependencyFiles
13421
- };
13422
- if (request.diff)
13423
- packageSignalInput.diff = request.diff;
13424
- const packageSignals = packageSignalsFor(repoRoot, packageSignalInput);
13425
- const directDependencyFiles = unique(packageSignals.flatMap((signal2) => signal2.manifestPaths));
13426
- const packageNames = packageSignals.map((signal2) => signal2.name);
13427
- const validationCommands2 = validationCommandsFor2(repoRoot, packageManager);
13428
- const graphCommands = dependencyGraphCommandsFor(packageManager, packageNames);
13429
- const remediationCommands = remediationCommandsFor(packageManager, packageNames);
13430
- const residualRiskChecks = residualRiskChecksFor(packageManager, packageNames, changedDependencyFiles);
13431
- const confidence = confidenceFor({
13432
- changedDependencyFiles,
13433
- packageSignals,
13434
- lockfiles,
13435
- validationCommands: validationCommands2
14025
+ // ../core/dist/productIntelligence.js
14026
+ import { isIP } from "node:net";
14027
+ function loadProductIntelligence(repoRoot) {
14028
+ for (const filePath of briefCacheCandidatePaths(repoRoot, productIntelligenceFile)) {
14029
+ const content = readBriefLedgerFile(filePath);
14030
+ if (!content)
14031
+ continue;
14032
+ try {
14033
+ const parsed = JSON.parse(content);
14034
+ if (isProductIntelligenceStore(parsed)) {
14035
+ return withLocalProductLearnings(repoRoot, parsed);
14036
+ }
14037
+ } catch {
14038
+ }
14039
+ }
14040
+ return withLocalProductLearnings(repoRoot, emptyProductIntelligence(pathName(repoRoot)));
14041
+ }
14042
+ function saveProductIntelligence(repoRoot, input) {
14043
+ return withBriefCacheLock(repoRoot, "product-intelligence", () => {
14044
+ const current = loadProductIntelligence(repoRoot);
14045
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14046
+ const facts = [...current.facts];
14047
+ const savedFactIds = [];
14048
+ for (const candidate of input.facts) {
14049
+ const statement = normalizeText(candidate.statement, 1e3);
14050
+ if (!statement)
14051
+ continue;
14052
+ const sources = (candidate.sources ?? []).map((source) => normalizeSource(source, now));
14053
+ const existing = facts.find((fact2) => fact2.kind === candidate.kind && normalizeKey(fact2.statement) === normalizeKey(statement));
14054
+ if (existing) {
14055
+ existing.appliesTo = unique([
14056
+ ...existing.appliesTo,
14057
+ ...candidate.appliesTo ?? []
14058
+ ]).slice(0, 24);
14059
+ existing.sources = mergeSources(existing.sources, sources);
14060
+ existing.confidence = Math.max(existing.confidence, clampConfidence(candidate.confidence ?? existing.confidence));
14061
+ if (candidate.status)
14062
+ existing.status = candidate.status;
14063
+ existing.updatedAt = now;
14064
+ savedFactIds.push(existing.id);
14065
+ continue;
14066
+ }
14067
+ const fact = {
14068
+ id: idFor("product-fact", `${candidate.kind}:${statement}`),
14069
+ kind: candidate.kind,
14070
+ statement,
14071
+ appliesTo: unique(candidate.appliesTo ?? []).slice(0, 24),
14072
+ status: candidate.status ?? "proposed",
14073
+ confidence: clampConfidence(candidate.confidence ?? 0.65),
14074
+ sources,
14075
+ createdAt: now,
14076
+ updatedAt: now
14077
+ };
14078
+ facts.unshift(fact);
14079
+ savedFactIds.push(fact.id);
14080
+ }
14081
+ const store = {
14082
+ version: 1,
14083
+ repoName: input.repoName?.trim() || current.repoName,
14084
+ updatedAt: now,
14085
+ facts: facts.slice(0, 300),
14086
+ openQuestions: unique([
14087
+ ...current.openQuestions,
14088
+ ...input.openQuestions ?? []
14089
+ ]).map((item) => normalizeText(item, 300)).filter(Boolean).slice(0, 40),
14090
+ decisions: unique([...current.decisions, ...input.decisions ?? []]).map((item) => normalizeText(item, 500)).filter(Boolean).slice(0, 80)
14091
+ };
14092
+ writeBriefCacheFile(repoRoot, productIntelligenceFile, `${JSON.stringify(store, null, 2)}
14093
+ `);
14094
+ return { store, savedFactIds };
13436
14095
  });
14096
+ }
14097
+ function buildProductIntelligenceContext(store, task, maxLines = 14) {
14098
+ const now = Date.now();
14099
+ const ranked = store.facts.filter((fact) => fact.status !== "rejected").map((fact) => ({
14100
+ fact,
14101
+ stale: isStale(fact, now),
14102
+ score: factRelevance(fact, task)
14103
+ })).sort((a, b) => b.score - a.score || Number(b.fact.status === "approved") - Number(a.fact.status === "approved") || b.fact.updatedAt.localeCompare(a.fact.updatedAt));
14104
+ const relevant = ranked.filter((item) => item.score > 0);
14105
+ const selected = (relevant.length > 0 ? relevant : ranked).slice(0, 8);
14106
+ const relevantFacts = selected.map((item) => item.fact);
14107
+ const staleFactCount = ranked.filter((item) => item.stale).length;
14108
+ const sourceCount = new Set(store.facts.flatMap((fact) => fact.sources.map((source) => source.id))).size;
14109
+ const hasApproved = relevantFacts.some((fact) => fact.status === "approved");
14110
+ const hasProposed = relevantFacts.some((fact) => fact.status === "proposed");
14111
+ const status = relevantFacts.length === 0 ? "needs_research" : hasProposed ? "needs_review" : hasApproved && staleFactCount === 0 ? "ready" : "needs_research";
14112
+ const summary = relevantFacts.length === 0 ? "No product intelligence matches this task yet." : `${relevantFacts.length} product fact${relevantFacts.length === 1 ? "" : "s"} matched; ${sourceCount} cited source${sourceCount === 1 ? "" : "s"}${staleFactCount > 0 ? `, ${staleFactCount} may be stale` : ""}.`;
14113
+ const compactLines = [
14114
+ `Product intelligence: ${summary}`,
14115
+ ...relevantFacts.slice(0, 5).map((fact) => `- ${statusLabel(fact.status)} ${fact.kind}: ${fact.statement}`),
14116
+ ...store.openQuestions.length > 0 ? [`Open question: ${store.openQuestions[0]}`] : [],
14117
+ ...staleFactCount > 0 ? ["Refresh market, competitor, or integration evidence before treating it as current."] : []
14118
+ ];
14119
+ const compactText = compactLines.slice(0, Math.max(4, maxLines)).join("\n");
13437
14120
  return {
13438
- id: `dep_${contentHash([
13439
- request.task,
13440
- packageManager,
13441
- changedDependencyFiles.join(","),
13442
- packageNames.join(",")
13443
- ].join(":"))}`,
13444
- status: changedDependencyFiles.length > 0 || packageSignals.length > 0 ? "ready" : "needs_evidence",
13445
- packageManager,
13446
- manifestFiles: manifests.slice(0, 20),
13447
- lockfiles,
13448
- workspaceFiles,
13449
- changedDependencyFiles,
13450
- directDependencyFiles,
13451
- packageSignals,
13452
- graphCommands,
13453
- remediationCommands,
13454
- validationCommands: validationCommands2,
13455
- residualRiskChecks,
13456
- handoffRequirements: [
13457
- "List advisory ids or scanner finding ids that were fixed.",
13458
- "Name packages still present in the lockfile with vulnerable versions, or state that the scan/audit is clean.",
13459
- "Explain any major-version upgrades, overrides, peer dependency changes, skipped advisories, or remaining advisory ids.",
13460
- "Attach validation commands and CI follow-up results before marking the security mission done."
13461
- ],
13462
- summary: summaryFor3({
13463
- packageManager,
13464
- manifests,
13465
- lockfiles,
13466
- changedDependencyFiles,
13467
- packageSignals,
13468
- validationCommands: validationCommands2
13469
- }),
13470
- confidence
14121
+ status,
14122
+ summary,
14123
+ relevantFacts,
14124
+ staleFactCount,
14125
+ sourceCount,
14126
+ openQuestions: store.openQuestions.slice(0, 8),
14127
+ compactText,
14128
+ next: status === "needs_research" ? "Run brief.product_research_plan, use approved public sources where needed, then save cited observations as proposed context." : status === "needs_review" ? "Review proposed product facts with human judgment before treating them as team defaults." : "Use the cited product facts alongside repo rules; do not let market evidence override a direct user or team decision."
13471
14129
  };
13472
14130
  }
13473
- function isDependencyFile(filePath) {
13474
- const normalized = filePath.replaceAll("\\", "/");
13475
- return dependencyFilePatterns.some((pattern) => normalized === pattern || normalized.endsWith(`/${pattern}`) || pattern === "package.json" && normalized.endsWith("/package.json"));
13476
- }
13477
- function isDependencyLockfile(filePath) {
13478
- const normalized = filePath.replaceAll("\\", "/");
13479
- return normalized === "pnpm-lock.yaml" || normalized.endsWith("/pnpm-lock.yaml") || normalized === "package-lock.json" || normalized.endsWith("/package-lock.json") || normalized === "yarn.lock" || normalized.endsWith("/yarn.lock") || normalized === "bun.lock" || normalized.endsWith("/bun.lock") || normalized === "npm-shrinkwrap.json" || normalized.endsWith("/npm-shrinkwrap.json") || normalized === "poetry.lock" || normalized.endsWith("/poetry.lock") || normalized === "Pipfile.lock" || normalized.endsWith("/Pipfile.lock") || normalized === "Gemfile.lock" || normalized.endsWith("/Gemfile.lock") || normalized === "go.sum" || normalized.endsWith("/go.sum") || normalized === "Cargo.lock" || normalized.endsWith("/Cargo.lock");
13480
- }
13481
- function isDependencyManifest(filePath) {
13482
- const normalized = filePath.replaceAll("\\", "/");
13483
- return normalized === "package.json" || normalized.endsWith("/package.json") || normalized === "requirements.txt" || normalized.endsWith("/requirements.txt") || normalized === "pyproject.toml" || normalized.endsWith("/pyproject.toml") || normalized === "Gemfile" || normalized.endsWith("/Gemfile") || normalized === "go.mod" || normalized.endsWith("/go.mod") || normalized === "Cargo.toml" || normalized.endsWith("/Cargo.toml");
13484
- }
13485
- function isDependencyWorkspaceFile(filePath) {
13486
- const normalized = filePath.replaceAll("\\", "/");
13487
- return normalized === "pnpm-workspace.yaml" || normalized.endsWith("/pnpm-workspace.yaml") || normalized === "lerna.json" || normalized.endsWith("/lerna.json") || normalized === "turbo.json" || normalized.endsWith("/turbo.json") || normalized === "nx.json" || normalized.endsWith("/nx.json");
13488
- }
13489
- function inferPackageManager(repoRoot, manifests, lockfiles) {
13490
- const rootPackageJson = readPackageJson2(repoRoot, "package.json");
13491
- const packageManager = rootPackageJson?.packageManager;
13492
- if (typeof packageManager === "string") {
13493
- if (packageManager.startsWith("pnpm@"))
13494
- return "pnpm";
13495
- if (packageManager.startsWith("yarn@"))
13496
- return "yarn";
13497
- if (packageManager.startsWith("npm@"))
13498
- return "npm";
13499
- if (packageManager.startsWith("bun@"))
13500
- return "bun";
13501
- }
13502
- if (lockfiles.some((file) => file.endsWith("pnpm-lock.yaml")))
13503
- return "pnpm";
13504
- if (lockfiles.some((file) => file.endsWith("yarn.lock")))
13505
- return "yarn";
13506
- if (lockfiles.some((file) => file.endsWith("package-lock.json"))) {
13507
- return "npm";
13508
- }
13509
- if (lockfiles.some((file) => file.endsWith("bun.lock")))
13510
- return "bun";
13511
- if (manifests.some((file) => file.endsWith("requirements.txt") || file.endsWith("pyproject.toml") || file.endsWith("Pipfile"))) {
13512
- return "pip";
13513
- }
13514
- if (manifests.some((file) => file.endsWith("Gemfile")))
13515
- return "bundler";
13516
- if (manifests.some((file) => file.endsWith("go.mod")))
13517
- return "go";
13518
- if (manifests.some((file) => file.endsWith("Cargo.toml")))
13519
- return "cargo";
13520
- return "unknown";
14131
+ function buildProductResearchPlan(input) {
14132
+ const task = normalizeText(input.task, 1e3);
14133
+ const lower = task.toLowerCase();
14134
+ const audience = input.audience?.trim() || "the target user";
14135
+ const competitors = unique(input.competitors ?? []).slice(0, 8);
14136
+ const integrations = unique(input.integrations ?? []).slice(0, 8);
14137
+ const wantsTaste = /design|copy|content|microcopy|ux|ui|tone|visual|style/.test(lower);
14138
+ const wantsMarket = competitors.length > 0 || /competitor|market|alternative|position|pricing|category|landscape/.test(lower);
14139
+ const wantsIntegrations = integrations.length > 0 || /integration|connector|mcp|github|linear|jira|sentry|datadog|slack|figma/.test(lower);
14140
+ const focus = unique([
14141
+ "user job and desired outcome",
14142
+ ...wantsTaste ? ["interaction and language taste"] : [],
14143
+ ...wantsMarket ? ["category alternatives and differentiation"] : [],
14144
+ ...wantsIntegrations ? ["integration expectations and trust boundaries"] : [],
14145
+ "repo and team evidence that should constrain the decision"
14146
+ ]);
14147
+ const questions = [
14148
+ `What is ${audience} trying to accomplish, and what makes the current path hard?`,
14149
+ "Which existing behavior, workflow, or language should Brief preserve rather than reinvent?",
14150
+ ...wantsTaste ? [
14151
+ "What interaction, information hierarchy, and language patterns do users already recognize as high quality?",
14152
+ "Which patterns are genuinely useful versus decorative or over-automated?"
14153
+ ] : [],
14154
+ ...wantsMarket ? [
14155
+ "What do credible alternatives promise, and where do users still have to do the hard judgment themselves?",
14156
+ "What narrow outcome can Brief own without copying a competitor's category language?"
14157
+ ] : [],
14158
+ ...wantsIntegrations ? [
14159
+ "What setup, auth, permission, and failure behavior do users expect from this integration?",
14160
+ "Which integration signals are official and current enough to become product guidance?"
14161
+ ] : []
14162
+ ];
14163
+ const searchQueries = [
14164
+ `${task} official documentation workflow users`,
14165
+ ...competitors.length > 0 ? competitors.map((name) => `${name} official product docs ${task}`) : ["leading alternatives official docs product workflow"],
14166
+ ...integrations.length > 0 ? integrations.map((name) => `${name} official integration docs auth permissions`) : []
14167
+ ].slice(0, 10);
14168
+ return {
14169
+ status: "plan_ready",
14170
+ task,
14171
+ focus,
14172
+ questions: questions.slice(0, 8),
14173
+ searchQueries,
14174
+ sourcePolicy: [
14175
+ "Prefer official product documentation, changelogs, pricing, and security pages for current capabilities.",
14176
+ "Use user feedback, interviews, support, analytics, and dogfood receipts for needs and outcomes; label them separately from market claims.",
14177
+ "Attach the URL or repo path and access date to every saved observation; include a short excerpt only when it clarifies the claim.",
14178
+ "Save findings as proposed until a product owner approves them. Conflicts stay visible as open questions.",
14179
+ "Do not send private customer material or credentials to external research sources."
14180
+ ],
14181
+ deliverable: [
14182
+ "3-7 cited observations, each with a claim and implication for Brief.",
14183
+ "A short JTBD/persona or taste decision, if the evidence supports one.",
14184
+ "Competitor or integration notes separated from direct user evidence.",
14185
+ "Open questions and a decision owner rather than false certainty."
14186
+ ],
14187
+ next: "Research with the host's approved web/browser tools, then call brief.save_product_context with cited proposed facts."
14188
+ };
13521
14189
  }
13522
- function packageSignalsFor(repoRoot, input) {
13523
- const manifestDeps = dependencyNamesByManifest(repoRoot, input.manifests);
13524
- const explicitPackageNames = unique([
13525
- ...packagesMentionedInText(input.task),
13526
- ...packagesMentionedInDiff(input.diff ?? "")
13527
- ]).filter((name) => !looksLikeVersionOrAdvisory(name));
13528
- const fallbackPackageNames = input.changedDependencyFiles.flatMap((file) => packagesFromChangedDependencyFile(repoRoot, file));
13529
- const packageNames = unique(explicitPackageNames.length > 0 ? explicitPackageNames : fallbackPackageNames).filter((name) => !looksLikeVersionOrAdvisory(name));
13530
- return packageNames.slice(0, 16).map((name) => ({
13531
- name,
13532
- changeKind: changeKindFor(name, input.diff ?? ""),
13533
- manifestPaths: manifestsForPackage(name, manifestDeps),
13534
- lockfilePaths: input.changedDependencyFiles.filter(isDependencyLockfile),
13535
- evidence: evidenceForPackage(name, input.task, input.diff ?? "")
14190
+ function emptyProductIntelligence(repoName) {
14191
+ return {
14192
+ version: 1,
14193
+ repoName,
14194
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
14195
+ facts: [],
14196
+ openQuestions: [],
14197
+ decisions: []
14198
+ };
14199
+ }
14200
+ function withLocalProductLearnings(repoRoot, store) {
14201
+ const learnedFacts = loadLocalLearnings(repoRoot).filter((learning) => /product|design|content|copy|ux|ui|user|journey|taste|terminology|workflow|integration|competitor|market/i.test(`${learning.title} ${learning.body} ${learning.files.join(" ")}`)).slice(0, 80).map((learning) => ({
14202
+ id: idFor("product-learning", learning.id),
14203
+ kind: /copy|content|terminology|tone/i.test(`${learning.title} ${learning.body}`) ? "terminology" : /user|journey|job/i.test(`${learning.title} ${learning.body}`) ? "jtbd" : "principle",
14204
+ statement: normalizeText(learning.body, 1e3),
14205
+ appliesTo: learning.files,
14206
+ status: learning.status === "approved" ? "approved" : "proposed",
14207
+ confidence: clampConfidence(learning.confidence),
14208
+ sources: [
14209
+ {
14210
+ id: idFor("product-learning-source", learning.id),
14211
+ kind: "feedback",
14212
+ label: `Local learning: ${normalizeText(learning.title, 140)}`,
14213
+ path: ".brief-cache/learnings.json",
14214
+ accessedAt: learning.createdAt
14215
+ }
14216
+ ],
14217
+ createdAt: learning.createdAt,
14218
+ updatedAt: learning.createdAt
13536
14219
  }));
14220
+ const existingIds = new Set(store.facts.map((fact) => fact.id));
14221
+ const nextFacts = learnedFacts.filter((fact) => !existingIds.has(fact.id));
14222
+ return nextFacts.length === 0 ? store : { ...store, facts: [...nextFacts, ...store.facts].slice(0, 300) };
14223
+ }
14224
+ function pathName(repoRoot) {
14225
+ return repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? "repo";
14226
+ }
14227
+ function factRelevance(fact, task) {
14228
+ const text = `${task} ${fact.appliesTo.join(" ")}`.toLowerCase();
14229
+ const statement = fact.statement.toLowerCase();
14230
+ const words = unique(text.split(/[^a-z0-9]+/).filter((word) => word.length >= 4));
14231
+ let score = fact.status === "approved" ? 3 : 1;
14232
+ for (const word of words)
14233
+ if (statement.includes(word))
14234
+ score += 4;
14235
+ const kindTerms = {
14236
+ persona: ["user", "audience", "persona", "customer"],
14237
+ jtbd: ["job", "outcome", "workflow", "task"],
14238
+ principle: ["design", "ui", "ux", "copy", "taste", "quality"],
14239
+ terminology: ["copy", "content", "word", "term", "language", "tone"],
14240
+ competitor: ["competitor", "market", "alternative", "position"],
14241
+ integration: ["integration", "connector", "auth", "permission", "mcp"],
14242
+ metric: ["metric", "activation", "retention", "conversion", "usage"],
14243
+ decision: ["decision", "scope", "choose", "prioritize"]
14244
+ };
14245
+ if (kindTerms[fact.kind].some((term) => text.includes(term)))
14246
+ score += 8;
14247
+ return score;
13537
14248
  }
13538
- function dependencyNamesByManifest(repoRoot, manifests) {
13539
- const byPackage = /* @__PURE__ */ new Map();
13540
- for (const manifest of manifests) {
13541
- if (!manifest.endsWith("package.json"))
13542
- continue;
13543
- const parsed = readPackageJson2(repoRoot, manifest);
13544
- if (!parsed)
13545
- continue;
13546
- for (const name of packageJsonDependencyNames(parsed)) {
13547
- const current = byPackage.get(name) ?? [];
13548
- current.push(manifest);
13549
- byPackage.set(name, current);
13550
- }
13551
- }
13552
- return byPackage;
13553
- }
13554
- function packageJsonDependencyNames(parsed) {
13555
- return unique([
13556
- "dependencies",
13557
- "devDependencies",
13558
- "optionalDependencies",
13559
- "peerDependencies",
13560
- "overrides",
13561
- "resolutions",
13562
- "pnpm"
13563
- ].flatMap((field) => nestedDependencyNames(parsed[field])));
13564
- }
13565
- function nestedDependencyNames(value) {
13566
- if (!value || typeof value !== "object")
13567
- return [];
13568
- return Object.entries(value).flatMap(([key, nested]) => [
13569
- looksLikePackageName(key) ? key : "",
13570
- ...nestedDependencyNames(nested)
13571
- ]);
14249
+ function isStale(fact, now) {
14250
+ const ageDays = (now - Date.parse(fact.updatedAt)) / 864e5;
14251
+ const marketFact = ["competitor", "integration", "metric"].includes(fact.kind);
14252
+ return !Number.isFinite(ageDays) || ageDays > (marketFact ? staleMarketDays : staleProductDays);
13572
14253
  }
13573
- function packagesMentionedInText(text) {
13574
- const names = [];
13575
- const quoted = text.matchAll(/[`'"](@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)[`'"]/gi);
13576
- for (const match of quoted)
13577
- names.push(match[1] ?? "");
13578
- const advisoryContext = text.matchAll(/\b(?:package|dependency|upgrade|update|override|transitive|vulnerable|dependabot|cve|ghsa|audit)\s+(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)/gi);
13579
- for (const match of advisoryContext)
13580
- names.push(match[1] ?? "");
13581
- const atVersion = text.matchAll(/\b(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/gi);
13582
- for (const match of atVersion)
13583
- names.push(match[1] ?? "");
13584
- if (/\b(dependabot|cve-|ghsa-|audit|vulnerab|advisory)\b/i.test(text)) {
13585
- const affectedPackageContext = text.matchAll(/\b(?:for|affects?|in)\s+(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)/gi);
13586
- for (const match of affectedPackageContext)
13587
- names.push(match[1] ?? "");
13588
- }
13589
- return unique(names.map(cleanPackageName).filter(looksLikePackageName));
14254
+ function statusLabel(status) {
14255
+ return status === "approved" ? "[approved]" : "[proposed]";
13590
14256
  }
13591
- function packagesMentionedInDiff(diff) {
13592
- const names = [];
13593
- for (const line of diff.split(/\r?\n/)) {
13594
- if (!/^[+-]/.test(line) || /^[+-]{3}/.test(line))
13595
- continue;
13596
- const packageJsonMatch = line.match(/"(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)"\s*:/i);
13597
- if (packageJsonMatch?.[1])
13598
- names.push(packageJsonMatch[1]);
13599
- const pnpmMatch = line.match(/^\s*[+-]\s{0,8}(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/i);
13600
- if (pnpmMatch?.[1])
13601
- names.push(pnpmMatch[1]);
13602
- const lockPathMatch = line.match(/\/(@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?)@(?:\^|~)?\d/i);
13603
- if (lockPathMatch?.[1])
13604
- names.push(lockPathMatch[1]);
14257
+ function normalizeSource(input, fallbackAccessedAt) {
14258
+ const url = input.url?.trim();
14259
+ if (url)
14260
+ assertPublicResearchUrl(url);
14261
+ if (!input.path?.trim() && !url && input.kind !== "user" && input.kind !== "feedback" && input.kind !== "dogfood") {
14262
+ throw new Error(`A ${input.kind} source needs a public URL or repo path.`);
13605
14263
  }
13606
- return unique(names.map(cleanPackageName).filter(looksLikePackageName));
13607
- }
13608
- function packagesFromChangedDependencyFile(repoRoot, filePath) {
13609
- if (!filePath.endsWith("package.json"))
13610
- return [];
13611
- const parsed = readPackageJson2(repoRoot, filePath);
13612
- return parsed ? packageJsonDependencyNames(parsed).slice(0, 20) : [];
14264
+ const label = normalizeText(input.label, 180);
14265
+ return {
14266
+ id: idFor("product-source", `${input.kind}:${url ?? input.path ?? label}`),
14267
+ kind: input.kind,
14268
+ label,
14269
+ ...url ? { url } : {},
14270
+ ...input.path?.trim() ? { path: normalizeText(input.path, 300) } : {},
14271
+ accessedAt: input.accessedAt ?? fallbackAccessedAt,
14272
+ ...input.excerpt?.trim() ? { excerpt: normalizeText(input.excerpt, 420) } : {}
14273
+ };
13613
14274
  }
13614
- function readPackageJson2(repoRoot, filePath) {
13615
- const content = safeRead(path7.join(repoRoot, filePath), 4e5);
13616
- if (!content)
13617
- return null;
14275
+ function assertPublicResearchUrl(value) {
14276
+ let parsed;
13618
14277
  try {
13619
- const parsed = JSON.parse(content);
13620
- return parsed && typeof parsed === "object" ? parsed : null;
14278
+ parsed = new URL(value);
13621
14279
  } catch {
13622
- return null;
13623
- }
13624
- }
13625
- function changeKindFor(packageName, diff) {
13626
- const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13627
- const removedVersions = [
13628
- ...diff.matchAll(new RegExp(`^-.*${escaped}.*?(\\d+\\.\\d+\\.\\d+)`, "gim"))
13629
- ].map((match) => match[1] ?? "");
13630
- const addedVersions = [
13631
- ...diff.matchAll(new RegExp(`^\\+.*${escaped}.*?(\\d+\\.\\d+\\.\\d+)`, "gim"))
13632
- ].map((match) => match[1] ?? "");
13633
- const removed = removedVersions[0];
13634
- const added = addedVersions[0];
13635
- if (removed && added) {
13636
- const [removedMajor] = removed.split(".");
13637
- const [addedMajor] = added.split(".");
13638
- return removedMajor && addedMajor && removedMajor !== addedMajor ? "major" : "patch_or_minor";
14280
+ throw new Error("Product research URLs must be valid HTTPS URLs.");
13639
14281
  }
13640
- if (added && !removed)
13641
- return "added_or_overridden";
13642
- return "unknown";
13643
- }
13644
- function manifestsForPackage(packageName, manifestDeps) {
13645
- return unique([
13646
- ...manifestDeps.get(packageName) ?? [],
13647
- ...[...manifestDeps.entries()].filter(([name]) => name.endsWith(`/${packageName}`)).flatMap(([, manifests]) => manifests)
13648
- ]).slice(0, 8);
13649
- }
13650
- function evidenceForPackage(packageName, task, diff) {
13651
- const evidence = [];
13652
- if (task.toLowerCase().includes(packageName.toLowerCase())) {
13653
- evidence.push("mentioned_in_task");
14282
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
14283
+ throw new Error("Product research URLs must use HTTPS and cannot include credentials.");
13654
14284
  }
13655
- if (diff.toLowerCase().includes(packageName.toLowerCase())) {
13656
- evidence.push("mentioned_in_diff");
14285
+ const host = parsed.hostname.toLowerCase();
14286
+ if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || isPrivateIp(host)) {
14287
+ throw new Error("Product research URLs must point to public sources.");
13657
14288
  }
13658
- if (evidence.length === 0)
13659
- evidence.push("manifest_candidate");
13660
- return evidence;
13661
14289
  }
13662
- function validationCommandsFor2(repoRoot, packageManager) {
13663
- const scripts = packageScripts(repoRoot);
13664
- const run = packageRunCommand(packageManager);
13665
- return unique([
13666
- ...auditCommandsFor(packageManager),
13667
- ...scriptCommand(scripts, "check", run),
13668
- ...scriptCommand(scripts, "test", run),
13669
- ...scriptCommand(scripts, "test:client", run),
13670
- ...scriptCommand(scripts, "test:e2e:smoke", run),
13671
- ...scriptCommand(scripts, "build", run)
13672
- ]).slice(0, 10);
13673
- }
13674
- function packageScripts(repoRoot) {
13675
- const parsed = readPackageJson2(repoRoot, "package.json");
13676
- const scripts = parsed?.scripts;
13677
- return scripts && typeof scripts === "object" ? Object.fromEntries(Object.entries(scripts).filter((entry) => typeof entry[1] === "string")) : {};
13678
- }
13679
- function scriptCommand(scripts, scriptName, runCommand) {
13680
- return scripts[scriptName] ? [`${runCommand} ${scriptName}`] : [];
13681
- }
13682
- function packageRunCommand(packageManager) {
13683
- switch (packageManager) {
13684
- case "npm":
13685
- return "npm run";
13686
- case "yarn":
13687
- return "yarn";
13688
- case "bun":
13689
- return "bun run";
13690
- default:
13691
- return "pnpm";
13692
- }
13693
- }
13694
- function auditCommandsFor(packageManager) {
13695
- switch (packageManager) {
13696
- case "pnpm":
13697
- return ["pnpm audit --prod"];
13698
- case "npm":
13699
- return ["npm audit --omit=dev"];
13700
- case "yarn":
13701
- return ["yarn npm audit --environment production"];
13702
- case "bun":
13703
- return ["bun audit"];
13704
- case "pip":
13705
- return ["pip-audit"];
13706
- case "bundler":
13707
- return ["bundle audit check"];
13708
- case "go":
13709
- return ["govulncheck ./..."];
13710
- case "cargo":
13711
- return ["cargo audit"];
13712
- default:
13713
- return [];
13714
- }
13715
- }
13716
- function dependencyGraphCommandsFor(packageManager, packageNames) {
13717
- const selected = packageNames.slice(0, 5);
13718
- if (selected.length === 0)
13719
- return [];
13720
- switch (packageManager) {
13721
- case "pnpm":
13722
- return selected.map((name) => `pnpm why ${name}`);
13723
- case "npm":
13724
- return selected.map((name) => `npm explain ${name}`);
13725
- case "yarn":
13726
- return selected.map((name) => `yarn why ${name}`);
13727
- case "bun":
13728
- return selected.map((name) => `bun pm why ${name}`);
13729
- case "go":
13730
- return selected.map((name) => `go mod why -m ${name}`);
13731
- case "cargo":
13732
- return selected.map((name) => `cargo tree -i ${name}`);
13733
- default:
13734
- return [];
14290
+ function isPrivateIp(host) {
14291
+ const family = isIP(host);
14292
+ if (family === 4) {
14293
+ const octets = host.split(".").map(Number);
14294
+ return octets[0] === 10 || octets[0] === 127 || octets[0] === 169 && octets[1] === 254 || octets[0] === 172 && (octets[1] ?? -1) >= 16 && (octets[1] ?? -1) <= 31 || octets[0] === 192 && octets[1] === 168;
13735
14295
  }
14296
+ return family === 6 && (host.startsWith("fc") || host.startsWith("fd") || host === "::1");
13736
14297
  }
13737
- function remediationCommandsFor(packageManager, packageNames) {
13738
- const selected = packageNames.slice(0, 5);
13739
- if (selected.length === 0)
13740
- return [];
13741
- switch (packageManager) {
13742
- case "pnpm":
13743
- return selected.map((name) => `pnpm up ${name} --latest --interactive=false`);
13744
- case "npm":
13745
- return selected.map((name) => `npm update ${name}`);
13746
- case "yarn":
13747
- return selected.map((name) => `yarn up ${name}`);
13748
- case "bun":
13749
- return selected.map((name) => `bun update ${name}`);
13750
- case "go":
13751
- return selected.map((name) => `go get ${name}@latest`);
13752
- case "cargo":
13753
- return selected.map((name) => `cargo update -p ${name}`);
13754
- default:
13755
- return [];
14298
+ function mergeSources(current, incoming) {
14299
+ const merged = [...current];
14300
+ for (const source of incoming) {
14301
+ const existing = merged.find((item) => item.id === source.id);
14302
+ if (existing) {
14303
+ Object.assign(existing, source);
14304
+ } else {
14305
+ merged.push(source);
14306
+ }
13756
14307
  }
14308
+ return merged.slice(0, 12);
13757
14309
  }
13758
- function residualRiskChecksFor(packageManager, packageNames, changedDependencyFiles) {
13759
- return unique([
13760
- packageNames.length > 0 ? `Confirm vulnerable versions are gone for: ${packageNames.slice(0, 8).join(", ")}.` : "Name each advisory/package from the scanner before editing.",
13761
- changedDependencyFiles.some(isDependencyLockfile) ? "Inspect the lockfile diff for unexpected package churn outside the advisory path." : "Regenerate and inspect the lockfile if the fix changes dependency resolution.",
13762
- "Split major-version upgrades or broad framework upgrades into a separate mission unless the advisory requires them.",
13763
- "Record any remaining advisory ids, ignored dev-only findings, or transitive paths that still need owner judgment.",
13764
- ...auditCommandsFor(packageManager).map((command2) => `Rerun ${command2} after the lockfile update.`)
13765
- ]);
14310
+ function normalizeText(value, maxLength) {
14311
+ return value.trim().replace(/\s+/g, " ").slice(0, maxLength);
13766
14312
  }
13767
- function confidenceFor(input) {
13768
- let score = 35;
13769
- if (input.changedDependencyFiles.length > 0)
13770
- score += 20;
13771
- if (input.packageSignals.length > 0)
13772
- score += 20;
13773
- if (input.lockfiles.length > 0)
13774
- score += 10;
13775
- if (input.validationCommands.length > 0)
13776
- score += 10;
13777
- return Math.min(95, score);
14313
+ function normalizeKey(value) {
14314
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
13778
14315
  }
13779
- function summaryFor3(input) {
13780
- const packages = input.packageSignals.map((signal2) => signal2.name);
13781
- return [
13782
- `Detected ${input.packageManager} dependency remediation context.`,
13783
- `${input.manifests.length} manifest${input.manifests.length === 1 ? "" : "s"}, ${input.lockfiles.length} lockfile${input.lockfiles.length === 1 ? "" : "s"}.`,
13784
- input.changedDependencyFiles.length > 0 ? `Changed dependency files: ${input.changedDependencyFiles.slice(0, 6).join(", ")}.` : "No dependency files are changed yet.",
13785
- packages.length > 0 ? `Likely packages: ${packages.slice(0, 8).join(", ")}.` : "Package names still need advisory/scanner evidence.",
13786
- input.validationCommands.length > 0 ? `Validation starts with ${input.validationCommands[0]}.` : "Validation commands need repo-specific confirmation."
13787
- ].join(" ");
14316
+ function clampConfidence(value) {
14317
+ return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
13788
14318
  }
13789
- function cleanPackageName(input) {
13790
- return input.replace(/[),.;\]]+$/g, "").trim();
14319
+ function isProductIntelligenceStore(value) {
14320
+ if (!value || typeof value !== "object")
14321
+ return false;
14322
+ const item = value;
14323
+ return item.version === 1 && typeof item.repoName === "string" && typeof item.updatedAt === "string" && Array.isArray(item.facts) && item.facts.every(isFact) && Array.isArray(item.openQuestions) && item.openQuestions.every((question) => typeof question === "string") && Array.isArray(item.decisions) && item.decisions.every((decision) => typeof decision === "string");
13791
14324
  }
13792
- function looksLikePackageName(input) {
13793
- return /^@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9._-]+)?$/i.test(input);
14325
+ function isFact(value) {
14326
+ if (!value || typeof value !== "object")
14327
+ return false;
14328
+ const item = value;
14329
+ return typeof item.id === "string" && typeof item.kind === "string" && typeof item.statement === "string" && Array.isArray(item.appliesTo) && typeof item.status === "string" && typeof item.confidence === "number" && Array.isArray(item.sources) && item.sources.every(isSource) && typeof item.createdAt === "string" && typeof item.updatedAt === "string";
13794
14330
  }
13795
- function looksLikeVersionOrAdvisory(input) {
13796
- const normalized = input.toLowerCase();
13797
- return /^\d/.test(input) || /^cve-\d/i.test(input) || /^ghsa-/i.test(input) || [
13798
- "advisory",
13799
- "advisories",
13800
- "change",
13801
- "changes",
13802
- "dependency",
13803
- "dependencies",
13804
- "dependabot",
13805
- "finding",
13806
- "findings",
13807
- "package",
13808
- "packages",
13809
- "security",
13810
- "transitive",
13811
- "upgrade",
13812
- "upgrades",
13813
- "vulnerabilities",
13814
- "vulnerability",
13815
- "vulnerable"
13816
- ].includes(normalized);
14331
+ function isSource(value) {
14332
+ if (!value || typeof value !== "object")
14333
+ return false;
14334
+ const item = value;
14335
+ return typeof item.id === "string" && typeof item.kind === "string" && typeof item.label === "string" && typeof item.accessedAt === "string";
13817
14336
  }
13818
- var dependencyFilePatterns;
13819
- var init_dependencyRemediation = __esm({
13820
- "../core/dist/dependencyRemediation.js"() {
14337
+ var productIntelligenceFile, staleMarketDays, staleProductDays;
14338
+ var init_productIntelligence = __esm({
14339
+ "../core/dist/productIntelligence.js"() {
13821
14340
  "use strict";
14341
+ init_localCache();
14342
+ init_localLearning();
13822
14343
  init_utils();
13823
- dependencyFilePatterns = [
13824
- "package.json",
13825
- "pnpm-lock.yaml",
13826
- "pnpm-workspace.yaml",
13827
- "package-lock.json",
13828
- "yarn.lock",
13829
- "bun.lock",
13830
- "npm-shrinkwrap.json",
13831
- "requirements.txt",
13832
- "poetry.lock",
13833
- "pyproject.toml",
13834
- "Pipfile.lock",
13835
- "Gemfile",
13836
- "Gemfile.lock",
13837
- "go.mod",
13838
- "go.sum",
13839
- "Cargo.toml",
13840
- "Cargo.lock"
13841
- ];
14344
+ productIntelligenceFile = "product-intelligence.json";
14345
+ staleMarketDays = 90;
14346
+ staleProductDays = 180;
13842
14347
  }
13843
14348
  });
13844
14349
 
@@ -13863,6 +14368,16 @@ function buildProductOperatingContext(scan, request) {
13863
14368
  });
13864
14369
  const status = statusFor(taskClasses, reuseBeforeBuild, productRules);
13865
14370
  const score = scoreFor(status, reuseBeforeBuild, productRules, terminology);
14371
+ const productIntelligence = request.repoRoot ? buildProductIntelligenceContext(loadProductIntelligence(request.repoRoot), request.task, Math.max(4, Math.min(12, (request.maxLines ?? 32) - 8))) : {
14372
+ status: "not_available",
14373
+ summary: "Product intelligence is not available without a repo root.",
14374
+ relevantFacts: [],
14375
+ staleFactCount: 0,
14376
+ sourceCount: 0,
14377
+ openQuestions: [],
14378
+ compactText: "Product intelligence: not available without a repo root.",
14379
+ next: "Resolve the repository before reading product context."
14380
+ };
13866
14381
  const summary = status === "not_applicable" ? `${scan.repoName}: no product/design operating context was needed for this task.` : `${scan.repoName}: ${taskClasses.join(", ")} work should reuse ${reuseBeforeBuild.length} existing pattern${reuseBeforeBuild.length === 1 ? "" : "s"} and satisfy ${acceptanceCriteria.length} acceptance checks before handoff.`;
13867
14382
  const compactText = compactTextFor({
13868
14383
  summary,
@@ -13872,6 +14387,7 @@ function buildProductOperatingContext(scan, request) {
13872
14387
  acceptanceCriteria,
13873
14388
  terminology,
13874
14389
  stopSignals: stopSignals2,
14390
+ productIntelligence,
13875
14391
  maxLines: request.maxLines ?? 32
13876
14392
  });
13877
14393
  return {
@@ -13891,9 +14407,10 @@ function buildProductOperatingContext(scan, request) {
13891
14407
  validation,
13892
14408
  productMemoryCandidates,
13893
14409
  stopSignals: stopSignals2,
14410
+ productIntelligence,
13894
14411
  compactText,
13895
14412
  lineCount: compactText.split(/\r?\n/).length,
13896
- next: status === "not_applicable" ? "Continue with the normal before-you-code packet." : "Read compactText before editing; reuse or rule out the listed patterns, then validate the journey, terminology, state, and CTA checks before handoff."
14413
+ next: status === "not_applicable" ? "Continue with the normal before-you-code packet." : `Read compactText before editing; reuse or rule out the listed patterns, then validate the journey, terminology, state, and CTA checks before handoff. ${productIntelligence.next}`
13897
14414
  };
13898
14415
  }
13899
14416
  function needsProductOperatingContext(request) {
@@ -14351,6 +14868,7 @@ function compactTextFor(input) {
14351
14868
  ...input.terminology.preferred.length > 0 ? [
14352
14869
  `Terminology: use ${input.terminology.preferred.slice(0, 8).join(", ")}.`
14353
14870
  ] : [],
14871
+ ...input.productIntelligence.status !== "not_available" ? input.productIntelligence.compactText.split(/\r?\n/) : [],
14354
14872
  "Stop if:",
14355
14873
  ...itemLines(input.stopSignals, (item) => item, "No stop signals.", 4)
14356
14874
  ];
@@ -14379,6 +14897,7 @@ var init_productOperatingContext = __esm({
14379
14897
  "../core/dist/productOperatingContext.js"() {
14380
14898
  "use strict";
14381
14899
  init_utils();
14900
+ init_productIntelligence();
14382
14901
  PRODUCT_KEYWORDS = [
14383
14902
  "app",
14384
14903
  "cta",
@@ -14387,6 +14906,16 @@ var init_productOperatingContext = __esm({
14387
14906
  "feature",
14388
14907
  "flow",
14389
14908
  "journey",
14909
+ "jtbd",
14910
+ "job-to-be-done",
14911
+ "persona",
14912
+ "competitor",
14913
+ "market",
14914
+ "research",
14915
+ "taste",
14916
+ "integration",
14917
+ "customer",
14918
+ "feedback",
14390
14919
  "page",
14391
14920
  "product",
14392
14921
  "screen",
@@ -16624,6 +17153,9 @@ function recipeFor(explicitRecipe, task, request) {
16624
17153
  return missionRecipes.find((recipe) => recipe.id === explicitRecipe) ?? missionRecipes[0];
16625
17154
  }
16626
17155
  const lower = task.toLowerCase();
17156
+ if (/\b(dogfood|dogfooding|exploratory browser|web app smoke|browser smoke|product safari|try the app|use the app)\b/.test(lower) && /\b(app|site|web|browser|page|screen|flow|journey|ui|ux|localhost|staging|production)\b/.test(lower)) {
17157
+ return recipeById("web_dogfood");
17158
+ }
16627
17159
  if (isDesignLedChange(lower)) {
16628
17160
  return recipeById("design_change");
16629
17161
  }
@@ -16974,6 +17506,9 @@ function missionSummary(repoName, recipe, autonomy, request) {
16974
17506
  if (recipe.id === "qa_edge_cases") {
16975
17507
  return `${repoName} can run this as the QA edge-case loop: find missing coverage around risky behavior, add or improve tests, validate the signal, then review the diff.`;
16976
17508
  }
17509
+ if (recipe.id === "web_dogfood") {
17510
+ return `${repoName} can run this as a read-only browser dogfooding loop: name the critical journeys, exercise the app like a user, capture reproducible evidence, separate product friction from environment noise, then hand back the top fixes and reusable learning.`;
17511
+ }
16977
17512
  if (recipe.id === "performance") {
16978
17513
  return `${repoName} can run this as the performance loop: capture a baseline, form a bounded optimization hypothesis, measure before and after, review the diff, then save handoff proof.`;
16979
17514
  }
@@ -17116,6 +17651,15 @@ function missionGates(autonomy, recipe, options2) {
17116
17651
  failureMode: "Do not add low-signal tests or hide flaky checks. Ask for the behavior risk and validation target."
17117
17652
  });
17118
17653
  }
17654
+ if (recipe.id === "web_dogfood") {
17655
+ gates.push({
17656
+ id: "browser_journey",
17657
+ label: "Browser journey gate",
17658
+ requiredTool: "available browser",
17659
+ passCriteria: "The agent names safe starting state, critical journeys, expected outcomes, stop conditions, and evidence fields before interacting with the app.",
17660
+ failureMode: "Do not click through an unclear or destructive flow. Ask for a safe fixture, account, URL, or narrower journey list."
17661
+ });
17662
+ }
17119
17663
  if (recipe.id === "performance") {
17120
17664
  gates.push({
17121
17665
  id: "benchmark_baseline",
@@ -17164,7 +17708,7 @@ function missionGates(autonomy, recipe, options2) {
17164
17708
  label: "Library privacy gate",
17165
17709
  requiredTool: "brief.agent_session_insights",
17166
17710
  passCriteria: "Every candidate example is classified as derived-only, redacted transcript, or explicitly approved raw import before sharing.",
17167
- failureMode: "Do not publish raw prompts, secrets, customer data, private logs, or sensitive repo context without explicit approval."
17711
+ failureMode: "Do not share raw prompts, secrets, customer data, private logs, or sensitive repo context without explicit approval."
17168
17712
  }, {
17169
17713
  id: "reuse_quality",
17170
17714
  label: "Reuse quality gate",
@@ -17230,6 +17774,17 @@ function missionSteps(task, autonomy, recipe, options2) {
17230
17774
  gateId: "plan"
17231
17775
  }
17232
17776
  ];
17777
+ if (recipe.id === "web_dogfood") {
17778
+ steps.push({
17779
+ id: "browser_journeys",
17780
+ title: "Exercise critical browser journeys",
17781
+ objective: "Use the app like a real user and gather evidence before proposing a fix.",
17782
+ agentAction: "Start from the named safe state, exercise 2-5 critical journeys, capture expected versus actual behavior, and stop before destructive or credential-requiring actions.",
17783
+ briefTool: "available browser",
17784
+ expectedOutput: "Journey evidence with routes, actions, screenshots or DOM notes, viewport/accessibility checks, and console/network observations.",
17785
+ gateId: "browser_journey"
17786
+ });
17787
+ }
17233
17788
  if (recipe.id === "ticket_write") {
17234
17789
  steps.push({
17235
17790
  id: "capture_judgment",
@@ -17548,7 +18103,7 @@ function missionSteps(task, autonomy, recipe, options2) {
17548
18103
  id: "verify_receipts",
17549
18104
  title: "Verify ticket receipt ledger",
17550
18105
  objective: "Prove the ticket-writing receipt chain before another agent runs the ticket.",
17551
- agentAction: "Run Brief receipt verification and fix missing, mismatched, or broken-chain ticket proof before publishing the ticket.",
18106
+ agentAction: "Run Brief receipt verification and fix missing, mismatched, or broken-chain ticket proof before sending the ticket.",
17552
18107
  briefTool: "brief.verify_receipts",
17553
18108
  expectedOutput: "Receipt ledger verification with digest and chain-link status.",
17554
18109
  gateId: "ticket"
@@ -17670,6 +18225,17 @@ function selectMissionValidation(suggestedTests, recipe, needsRuntimeEvidence, n
17670
18225
  "Run brief.review_diff before PR handoff."
17671
18226
  ]);
17672
18227
  }
18228
+ if (recipe.id === "web_dogfood") {
18229
+ return unique([
18230
+ "Name 2-5 user journeys with a safe starting state, expected outcome, and stop condition.",
18231
+ "Exercise rendered, user-visible behavior before inferring implementation causes.",
18232
+ "For each finding capture route, reproducible steps, expected vs actual, severity, and screenshot or DOM evidence when useful.",
18233
+ "Check desktop and narrow/mobile layout, keyboard/focus, accessible names, console errors, and failed requests when relevant.",
18234
+ "Separate product friction from auth, fixture, network, or environment limitations.",
18235
+ "End with the top three fixes, what worked, and one reusable learning; do not edit the repo in read-only mode.",
18236
+ "Save the dogfooding report with brief.save_handoff and verify its receipts; use brief.save_learning only for repeatable lessons."
18237
+ ]);
18238
+ }
17673
18239
  if (recipe.id === "performance") {
17674
18240
  return unique([
17675
18241
  "Benchmark baseline names workload, command, metric, variance, and success threshold before edits.",
@@ -17759,25 +18325,31 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17759
18325
  const isTicketWriter = mode === "ticket_writer";
17760
18326
  const isTicketFix = mode === "ticket_fix";
17761
18327
  const isRuntimeFix = mode === "runtime_fix";
18328
+ const isWebDogfood = recipe.id === "web_dogfood";
17762
18329
  const isSecurityRecipe = recipe.id === "security_dependency" || recipe.id === "security_finding" || recipe.id === "security_testing";
17763
18330
  const handoffTarget = deliveryReviewTarget(deliveryMode);
17764
18331
  const phases = [
17765
18332
  {
17766
- id: isTicketWriter ? "ticket_intake" : "context_map",
17767
- title: isTicketWriter ? "Capture intent and judgment boundaries" : "Load team context and map the repo surface",
18333
+ id: isTicketWriter ? "ticket_intake" : isWebDogfood ? "context_and_surface" : "context_map",
18334
+ title: isTicketWriter ? "Capture intent and judgment boundaries" : isWebDogfood ? "Load app context and safe journey surface" : "Load team context and map the repo surface",
17768
18335
  required: true,
17769
18336
  tool: isTicketWriter ? "brief.write_ticket" : "brief.context_for_task + brief.codebase_map",
17770
- expectedArtifact: isTicketWriter ? "generatedArtifacts.automationTicket" : "context_receipt + map_proof",
18337
+ expectedArtifact: isTicketWriter ? "generatedArtifacts.automationTicket" : isWebDogfood ? "context_receipt + map_proof + browser target" : "context_receipt + map_proof",
17771
18338
  proofRequired: isTicketWriter ? [
17772
18339
  "ticketReadiness.status",
17773
18340
  "automationTicket.machineReadableFields",
17774
18341
  "automationTicket.executionContract"
18342
+ ] : isWebDogfood ? [
18343
+ "contextReceipt.packetId",
18344
+ "executionContract.evidenceContract.mapProof",
18345
+ "context.likelyFiles",
18346
+ "browser target and safe starting state"
17775
18347
  ] : [
17776
18348
  "contextReceipt.packetId",
17777
18349
  "executionContract.evidenceContract.mapProof",
17778
18350
  "context.likelyFiles"
17779
18351
  ],
17780
- stopIfMissing: isTicketWriter ? "Do not publish the ticket until product intent, owner judgment, acceptance criteria, and stop conditions are explicit." : "Do not edit until likely files, rules, tests, owners, and affected systems are grounded in Brief context."
18352
+ stopIfMissing: isTicketWriter ? "Do not send the ticket until product intent, owner judgment, acceptance criteria, and stop conditions are explicit." : isWebDogfood ? "Do not interact until the app URL, safe starting state, critical journeys, and destructive-action stop conditions are explicit." : "Do not edit until likely files, rules, tests, owners, and affected systems are grounded in Brief context."
17781
18353
  },
17782
18354
  ...isRuntimeFix ? [
17783
18355
  {
@@ -17797,6 +18369,23 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17797
18369
  stopIfMissing: "Do not code from a runtime symptom until the source, identifier, environment, time window, observed facts, and privacy boundary are present."
17798
18370
  }
17799
18371
  ] : [],
18372
+ ...isWebDogfood ? [
18373
+ {
18374
+ id: "browser_journeys",
18375
+ title: "Exercise critical user journeys",
18376
+ required: true,
18377
+ tool: "available browser",
18378
+ expectedArtifact: "generatedArtifacts.browserJourneyEvidence",
18379
+ proofRequired: [
18380
+ "journey name and starting state",
18381
+ "route and user-visible actions",
18382
+ "expected versus actual outcome",
18383
+ "screenshot or DOM evidence when useful",
18384
+ "viewport, keyboard/focus, console, and failed-request notes"
18385
+ ],
18386
+ stopIfMissing: "Stop before destructive actions, credential entry, or ambiguous state changes; record the environment limitation instead."
18387
+ }
18388
+ ] : [],
17800
18389
  {
17801
18390
  id: "plan_loop",
17802
18391
  title: "Convert the work into a bounded loop",
@@ -17813,53 +18402,70 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17813
18402
  },
17814
18403
  ...isTicketWriter ? [] : [
17815
18404
  {
17816
- id: "execute_change",
17817
- title: "Make the smallest bounded change",
17818
- required: autonomy !== "read_only",
17819
- tool: "coding agent",
17820
- expectedArtifact: "local diff or no-code-change explanation",
17821
- proofRequired: [
18405
+ id: isWebDogfood ? "read_only_pass" : "execute_change",
18406
+ title: isWebDogfood ? "Keep the pass read-only" : "Make the smallest bounded change",
18407
+ required: isWebDogfood ? true : autonomy !== "read_only",
18408
+ tool: isWebDogfood ? "available browser" : "coding agent",
18409
+ expectedArtifact: isWebDogfood ? "journey evidence or explicit environment gap" : "local diff or no-code-change explanation",
18410
+ proofRequired: isWebDogfood ? [
18411
+ "no repo files changed",
18412
+ "journey evidence is reproducible",
18413
+ "product friction is separated from environment noise"
18414
+ ] : [
17822
18415
  "changedFiles[]",
17823
18416
  "rootCause or explicit no-root-cause finding",
17824
18417
  "acceptanceCriteriaStatus[]"
17825
18418
  ],
17826
- stopIfMissing: "Stop when the likely fix exceeds the ticket, runtime evidence, or selected autonomy boundary."
18419
+ stopIfMissing: isWebDogfood ? "Stop when a journey needs an external side effect, private credential, or unsupported browser capability." : "Stop when the likely fix exceeds the ticket, runtime evidence, or selected autonomy boundary."
17827
18420
  },
17828
18421
  {
17829
- id: "validate_change",
17830
- title: "Run the validation loop",
18422
+ id: isWebDogfood ? "review_evidence" : "validate_change",
18423
+ title: isWebDogfood ? "Review and classify the evidence" : "Run the validation loop",
17831
18424
  required: true,
17832
- tool: "brief.find_tests + project commands",
17833
- expectedArtifact: "validation evidence",
17834
- proofRequired: validation.length ? validation.slice(0, 5) : [
18425
+ tool: isWebDogfood ? "browser evidence + agent judgment" : "brief.find_tests + project commands",
18426
+ expectedArtifact: isWebDogfood ? "classified dogfooding findings" : "validation evidence",
18427
+ proofRequired: isWebDogfood ? [
18428
+ "finding severity and reproducible steps",
18429
+ "bug versus UX friction versus environment classification",
18430
+ "top fixes and what worked"
18431
+ ] : validation.length ? validation.slice(0, 5) : [
17835
18432
  "selected validation command",
17836
18433
  "pass/fail/skipped result",
17837
18434
  "environment note"
17838
18435
  ],
17839
- stopIfMissing: `Do not ask for ${handoffTarget} until validation is run or the missing environment is named with an owner.`
18436
+ stopIfMissing: isWebDogfood ? "Do not report a product issue without a route, action, expected result, actual result, and evidence or an explicit environment limitation." : `Do not ask for ${handoffTarget} until validation is run or the missing environment is named with an owner.`
17840
18437
  },
17841
18438
  {
17842
- id: "challenge_review",
17843
- title: "Challenge the diff and the root cause",
18439
+ id: isWebDogfood ? "prioritize_findings" : "challenge_review",
18440
+ title: isWebDogfood ? "Prioritize the user impact" : "Challenge the diff and the root cause",
17844
18441
  required: true,
17845
- tool: "brief.review_diff",
17846
- expectedArtifact: "review receipt",
17847
- proofRequired: [
18442
+ tool: isWebDogfood ? "brief.save_learning (when repeatable)" : "brief.review_diff",
18443
+ expectedArtifact: isWebDogfood ? "prioritized product feedback" : "review receipt",
18444
+ proofRequired: isWebDogfood ? [
18445
+ "top three fixes by user impact and reproducibility",
18446
+ "one thing that worked",
18447
+ "one reusable learning or explicit no-learning decision"
18448
+ ] : [
17848
18449
  "review.verdict",
17849
18450
  "review.readiness.status",
17850
18451
  "review receipt id",
17851
18452
  "blocker/high findings resolved or explicitly accepted"
17852
18453
  ],
17853
- stopIfMissing: "Do not hand off when blocker or high review findings remain unresolved."
18454
+ stopIfMissing: isWebDogfood ? "Do not hand off a long undifferentiated bug list; rank the few findings that most affect a real user." : "Do not hand off when blocker or high review findings remain unresolved."
17854
18455
  }
17855
18456
  ],
17856
18457
  {
17857
18458
  id: "handoff_receipts",
17858
- title: isTicketWriter ? "Save the ticket contract and receipt ledger" : "Save handoff proof and verify receipts",
18459
+ title: isTicketWriter ? "Save the ticket contract and receipt ledger" : isWebDogfood ? "Save the dogfooding report and verify receipts" : "Save handoff proof and verify receipts",
17859
18460
  required: true,
17860
18461
  tool: "brief.save_handoff + brief.verify_receipts",
17861
- expectedArtifact: "proof bundle + receipt ledger verification",
17862
- proofRequired: [
18462
+ expectedArtifact: isWebDogfood ? "dogfooding report + receipt ledger verification" : "proof bundle + receipt ledger verification",
18463
+ proofRequired: isWebDogfood ? [
18464
+ "proofBundle.status",
18465
+ "proofBundle.receipts.handoffId",
18466
+ "proofBundle.missingEvidence",
18467
+ "receiptLedger.status"
18468
+ ] : [
17863
18469
  "proofBundle.status",
17864
18470
  "proofBundle.receipts.handoffId",
17865
18471
  "proofBundle.receipts.reviewReceiptId",
@@ -17893,7 +18499,7 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17893
18499
  "MCP-capable IDE",
17894
18500
  "console loop recipe"
17895
18501
  ],
17896
- evidenceSources: automationLoopEvidenceSources(mode, options2),
18502
+ evidenceSources: automationLoopEvidenceSources(mode, options2, recipe.id),
17897
18503
  dataBoundaries: [
17898
18504
  "Use derived or redacted runtime facts in prompts; do not paste raw customer payloads, secrets, tokens, or private logs.",
17899
18505
  "Keep observed facts, hypotheses, and human decisions separate in the ticket or handoff.",
@@ -17910,7 +18516,10 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17910
18516
  "Root cause is inferred from symptoms rather than proven by source, CI, logs, traces, or reproduction.",
17911
18517
  "Two plausible approaches compete or the change touches architecture, data, auth, migrations, billing, or customer-visible behavior.",
17912
18518
  "The agent changes files outside the mapped scope or selected autonomy.",
17913
- "brief.review_diff returns blocker or high findings.",
18519
+ ...isWebDogfood ? [] : ["brief.review_diff returns blocker or high findings."],
18520
+ ...isWebDogfood ? [
18521
+ "A journey result is ambiguous, not reproducible, or could be explained by auth, fixture, network, or deployment state."
18522
+ ] : [],
17914
18523
  ...isRuntimeFix ? [
17915
18524
  "Runtime evidence is stale, contradictory, privacy-sensitive, or cannot be tied to the changed code path."
17916
18525
  ] : [],
@@ -17940,7 +18549,7 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17940
18549
  ] : []
17941
18550
  ]),
17942
18551
  finalUpdate: {
17943
- destination: deliveryFinalDestination(deliveryMode, isTicketWriter ? "Linear, Jira, or GitHub issue body" : isRuntimeFix ? "source ticket, incident, alert thread, or PR handoff" : isTicketFix ? "source Linear, Jira, GitHub, or pasted ticket" : isSecurityRecipe ? "security alert, Dependabot item, scanner finding, PR description, or handoff notes" : "PR description or handoff notes"),
18552
+ destination: deliveryFinalDestination(deliveryMode, isTicketWriter ? "Linear, Jira, or GitHub issue body" : isWebDogfood ? "product feedback, bug triage, or follow-up mission" : isRuntimeFix ? "source ticket, incident, alert thread, or PR handoff" : isTicketFix ? "source Linear, Jira, GitHub, or pasted ticket" : isSecurityRecipe ? "security alert, Dependabot item, scanner finding, PR description, or handoff notes" : "PR description or handoff notes"),
17944
18553
  requiredSections: isTicketWriter ? [
17945
18554
  "Intent",
17946
18555
  "Automation Readiness",
@@ -17949,26 +18558,42 @@ function automationLoopContract(autonomy, recipe, validation, options2, delivery
17949
18558
  "Brief Evidence Contract",
17950
18559
  "Acceptance Criteria",
17951
18560
  "Stop Conditions"
18561
+ ] : isWebDogfood ? [
18562
+ "Journeys Tested",
18563
+ "What Worked",
18564
+ "Findings",
18565
+ "Environment Limits",
18566
+ "Top Fixes",
18567
+ "Reusable Learning"
17952
18568
  ] : ticketIssueUpdateSections(),
17953
18569
  proofFields: [
17954
18570
  "proofBundle.status",
17955
18571
  "proofBundle.receipts.handoffId",
17956
18572
  "proofBundle.receipts.contextReceiptId",
17957
- "proofBundle.receipts.reviewReceiptId",
17958
- "proofBundle.receipts.reviewVerdict",
18573
+ ...isWebDogfood ? [] : [
18574
+ "proofBundle.receipts.reviewReceiptId",
18575
+ "proofBundle.receipts.reviewVerdict"
18576
+ ],
17959
18577
  "proofBundle.validation.passed",
17960
18578
  "proofBundle.missingEvidence"
17961
18579
  ],
17962
18580
  postOnlyWhen: [
17963
- "brief.review_diff has no unresolved blocker/high findings.",
18581
+ ...isWebDogfood ? [
18582
+ "Each reported finding has reproducible steps and evidence or an explicit environment limitation."
18583
+ ] : ["brief.review_diff has no unresolved blocker/high findings."],
17964
18584
  "brief.verify_receipts is trusted or context_only with gaps explained.",
17965
- "Acceptance criteria have pass/fail/skipped evidence.",
17966
- "Remaining risk and owner are named."
18585
+ ...isWebDogfood ? ["The top user-impact fixes and what worked are named."] : [
18586
+ "Acceptance criteria have pass/fail/skipped evidence.",
18587
+ "Remaining risk and owner are named."
18588
+ ]
17967
18589
  ],
17968
18590
  doNotPostWhen: unique([
17969
18591
  "Required owner, product, security, data, or rollout judgment is missing.",
17970
18592
  "Validation is missing and no human owner accepted the gap.",
17971
18593
  "The change exceeded ticket scope, runtime evidence scope, or autonomy.",
18594
+ ...isWebDogfood ? [
18595
+ "A destructive action, private credential, or external side effect was required without approval."
18596
+ ] : [],
17972
18597
  ...isRuntimeFix ? [
17973
18598
  "Runtime evidence packet is missing, stale, contradictory, or contains unresolved privacy gaps."
17974
18599
  ] : [],
@@ -17989,8 +18614,15 @@ function automationLoopMode(recipe, options2) {
17989
18614
  return "ticket_fix";
17990
18615
  return "mission";
17991
18616
  }
17992
- function automationLoopEvidenceSources(mode, options2) {
18617
+ function automationLoopEvidenceSources(mode, options2, recipeId) {
17993
18618
  return unique([
18619
+ ...recipeId === "web_dogfood" ? [
18620
+ "available browser",
18621
+ "journey steps and expected outcomes",
18622
+ "screenshots or DOM snapshots",
18623
+ "console and network observations",
18624
+ "responsive and accessibility checks"
18625
+ ] : [],
17994
18626
  ...mode === "ticket_writer" ? [
17995
18627
  "ticket intent",
17996
18628
  "acceptance criteria",
@@ -18026,6 +18658,7 @@ function loopProofContract(recipe, options2, deliveryMode) {
18026
18658
  const isTicketWriter = mode === "ticket_writer";
18027
18659
  const needsRuntimeEvidence = mode === "runtime_fix";
18028
18660
  const needsWorkItem = mode === "ticket_fix" || options2.needsWorkItem;
18661
+ const isWebDogfood = recipe.id === "web_dogfood";
18029
18662
  const isSecurityDependency = recipe.id === "security_dependency";
18030
18663
  const isSecurityFinding = recipe.id === "security_finding";
18031
18664
  const isSecurityTesting = recipe.id === "security_testing";
@@ -18035,7 +18668,12 @@ function loopProofContract(recipe, options2, deliveryMode) {
18035
18668
  "map_proof",
18036
18669
  "team_rules_applied",
18037
18670
  "mission_receipt",
18038
- ...isTicketWriter ? ["ticket_readiness", "human_judgment_record", "automation_ticket"] : [
18671
+ ...isTicketWriter ? ["ticket_readiness", "human_judgment_record", "automation_ticket"] : isWebDogfood ? [
18672
+ "browser_journey_plan",
18673
+ "browser_journey_evidence",
18674
+ "dogfooding_report",
18675
+ "handoff_receipt"
18676
+ ] : [
18039
18677
  "acceptance_criteria_evidence",
18040
18678
  "validation_evidence",
18041
18679
  "review_readiness",
@@ -18064,7 +18702,8 @@ function loopProofContract(recipe, options2, deliveryMode) {
18064
18702
  ]),
18065
18703
  requiredReceipts: unique([
18066
18704
  "mission_plan",
18067
- ...isTicketWriter ? [] : ["review", "handoff"],
18705
+ ...isTicketWriter || isWebDogfood ? [] : ["review", "handoff"],
18706
+ ...isWebDogfood ? ["handoff"] : [],
18068
18707
  "receipt_ledger"
18069
18708
  ]),
18070
18709
  readyStatuses: {
@@ -18078,6 +18717,11 @@ function loopProofContract(recipe, options2, deliveryMode) {
18078
18717
  "Human judgment, acceptance criteria, non-goals, and owner boundaries are explicit.",
18079
18718
  "Machine-readable run fields and Brief evidence contract are present.",
18080
18719
  "Receipt ledger verifies the mission receipt digest."
18720
+ ] : isWebDogfood ? [
18721
+ "Browser journeys are named, exercised from a safe state, and reported with reproducible evidence.",
18722
+ "Product issues are separated from auth, fixture, network, browser-capability, and deployment limitations.",
18723
+ "The report is concise: top user-impact fixes, what worked, and reusable learning or an explicit no-learning decision.",
18724
+ "A handoff receipt and receipt-ledger verification are present; review_diff is optional because no repo diff is expected."
18081
18725
  ] : [
18082
18726
  "Runtime evidence packet is ready when runtime evidence is part of the mission.",
18083
18727
  ...isSecurityDependency ? [
@@ -18164,6 +18808,11 @@ function missionRunContract(autonomy, recipe, validation, options2, deliveryMode
18164
18808
  "generatedArtifacts.contentProofPack.readiness",
18165
18809
  "generatedArtifacts.productOperatingContext"
18166
18810
  ] : [],
18811
+ ...recipe.id === "web_dogfood" ? [
18812
+ "generatedArtifacts.browserJourneyEvidence",
18813
+ "generatedArtifacts.dogfoodingReport",
18814
+ "generatedArtifacts.dogfoodingReport.topFindings[]"
18815
+ ] : [],
18167
18816
  "runContract.exitCodes[]"
18168
18817
  ],
18169
18818
  artifacts,
@@ -18172,8 +18821,8 @@ function missionRunContract(autonomy, recipe, validation, options2, deliveryMode
18172
18821
  proofContract,
18173
18822
  successCriteria: [
18174
18823
  "The context receipt cites the rules, source paths, tests, owners, and memories used by the agent.",
18175
- recipe.id === "ticket_write" ? "The ticket separates human judgment from execution instructions and names unresolved decisions before automation starts." : recipe.id === "security_dependency" ? "The dependency vulnerability packet names advisory id, package, affected range, fixed range, manifest, lockfile, direct/transitive path, reachability, remediation choice, and rollback." : recipe.id === "security_finding" ? "The security finding packet names alert source, rule id, classification, source-to-sink or trust boundary, affected users/data, owner, exploitability, and validation path." : recipe.id === "security_testing" ? "The security test plan names the protected property, bad case, safe case, command, severity threshold, false-positive policy, owner, privacy boundary, and rollout mode." : recipe.id === "internal_app" ? "The app contract names the internal route, data source, auth context, allowed roles, row filters, audit events, and redaction rules before implementation." : recipe.id === "stale_pr" ? "The PR intent packet names original scope, unresolved review feedback, stale assumptions, validation, and handoff expectation." : recipe.id === "merge_conflict" ? "The conflict packet names conflicted files, both sides' intent, semantic conflicts, and validation before resolution." : recipe.id === "flaky_test" ? "The CI evidence packet names failing job or test, sample failure, platform, seed or timing signal, hypothesis, and validation path." : recipe.id === "small_refactor" ? "The mechanical scope packet names exact pattern, allowed files, exclusions, and expected behavior preservation." : recipe.id === "bug_investigation" ? "The hypothesis ledger names observed facts, prior attempts, plausible causes, disproven causes, next probes, and confidence." : recipe.id === "qa_edge_cases" ? "The edge-case inventory names current coverage, missing boundary cases, risky inputs, and validation commands." : recipe.id === "performance" ? "The benchmark packet names workload, command, metric, baseline, variance, success threshold, and rollback signal before edits." : recipe.id === "porting" ? "The source behavior packet names source paths, target paths, incompatible assumptions, owner decision, validation, and attribution notes." : recipe.id === "experiment" ? "The experiment contract names the hypothesis, success signal, throwaway boundary, rollback or discard path, and decision owner." : recipe.id === "session_review" ? "The session review uses receipt-derived activity and marks any need for raw prompts, logs, or transcripts explicitly." : recipe.id === "session_library" ? "The session library classifies every reusable example by proof quality, privacy posture, owner approval, and reuse trigger." : options2.needsRuntimeEvidence ? "The runtime evidence packet names the observed failure source, identifier, environment, time window, symptom, and open gaps." : options2.needsWorkItem ? "The work item packet names source, id, acceptance criteria, non-goals, and the update expected after execution." : recipe.id === "research" ? "The answer names sources, risks, and next steps without editing the repo." : "Required validation evidence is attached or the missing environment is explicitly explained.",
18176
- recipe.id === "ticket_write" ? "Acceptance criteria, constraints, validation plan, review bar, stop conditions, and update template are parseable from the ticket body." : recipe.id === "security_dependency" ? "Install integrity, build/typecheck, targeted tests, and scanner/audit evidence show the vulnerability is remediated without breaking the dependency path." : recipe.id === "security_finding" ? "The root cause is fixed with negative regression, safe abuse-case proof, or scanner rerun evidence; suppression-only fixes are explicitly rejected or owner-approved." : recipe.id === "security_testing" ? "The added test or scanner catches the intended bad case or has a clear preventive rationale, and rollout mode is calibrated for developer workflow." : recipe.id === "internal_app" ? "The built app follows repo-native UI/data patterns and validation proves the access-control contract." : recipe.id === "stale_pr" ? "The handoff names review feedback addressed, validation, remaining risk, and next reviewer action." : recipe.id === "merge_conflict" ? "The resolution preserves intended behavior from both sides or clearly escalates unresolved semantic conflicts." : recipe.id === "flaky_test" ? "The result stabilizes the test or explains the product root cause with evidence rather than hiding the signal." : recipe.id === "small_refactor" ? "The diff stays mechanical and validation proves no intended behavior changed." : recipe.id === "bug_investigation" ? "The root cause, fix, no-fix decision, or escalation is tied to evidence and includes a follow-up watch signal." : recipe.id === "qa_edge_cases" ? "The added or improved tests cover meaningful behavior risk and do not mute, skip, or obscure the signal." : recipe.id === "performance" ? "Before/after measurements, tradeoffs, and rollback signals are recorded before the optimization is accepted." : recipe.id === "porting" ? "The target behavior preserves source intent, attribution is handled, and intentional differences are explicit." : recipe.id === "experiment" ? "The keep, discard, or formalize decision is recorded with validation and remaining production-readiness gaps." : recipe.id === "session_review" ? "Run cards or skill drafts are tied to actual receipts, validation quality, review outcomes, and privacy posture." : recipe.id === "session_library" ? "The published examples are derived from approved receipts or explicitly approved imports and include first-run commands, stop conditions, and validation expectations." : options2.needsRuntimeEvidence ? "The root cause, fix, validation, and incident or ticket update are tied back to the runtime evidence." : "The mission output is tied back to the selected recipe and context receipt.",
18824
+ recipe.id === "ticket_write" ? "The ticket separates human judgment from execution instructions and names unresolved decisions before automation starts." : recipe.id === "security_dependency" ? "The dependency vulnerability packet names advisory id, package, affected range, fixed range, manifest, lockfile, direct/transitive path, reachability, remediation choice, and rollback." : recipe.id === "security_finding" ? "The security finding packet names alert source, rule id, classification, source-to-sink or trust boundary, affected users/data, owner, exploitability, and validation path." : recipe.id === "security_testing" ? "The security test plan names the protected property, bad case, safe case, command, severity threshold, false-positive policy, owner, privacy boundary, and rollout mode." : recipe.id === "web_dogfood" ? "The dogfooding packet names critical journeys, safe starting state, expected outcomes, stop conditions, and evidence policy." : recipe.id === "internal_app" ? "The app contract names the internal route, data source, auth context, allowed roles, row filters, audit events, and redaction rules before implementation." : recipe.id === "stale_pr" ? "The PR intent packet names original scope, unresolved review feedback, stale assumptions, validation, and handoff expectation." : recipe.id === "merge_conflict" ? "The conflict packet names conflicted files, both sides' intent, semantic conflicts, and validation before resolution." : recipe.id === "flaky_test" ? "The CI evidence packet names failing job or test, sample failure, platform, seed or timing signal, hypothesis, and validation path." : recipe.id === "small_refactor" ? "The mechanical scope packet names exact pattern, allowed files, exclusions, and expected behavior preservation." : recipe.id === "bug_investigation" ? "The hypothesis ledger names observed facts, prior attempts, plausible causes, disproven causes, next probes, and confidence." : recipe.id === "qa_edge_cases" ? "The edge-case inventory names current coverage, missing boundary cases, risky inputs, and validation commands." : recipe.id === "performance" ? "The benchmark packet names workload, command, metric, baseline, variance, success threshold, and rollback signal before edits." : recipe.id === "porting" ? "The source behavior packet names source paths, target paths, incompatible assumptions, owner decision, validation, and attribution notes." : recipe.id === "experiment" ? "The experiment contract names the hypothesis, success signal, throwaway boundary, rollback or discard path, and decision owner." : recipe.id === "session_review" ? "The session review uses receipt-derived activity and marks any need for raw prompts, logs, or transcripts explicitly." : recipe.id === "session_library" ? "The session library classifies every reusable example by proof quality, privacy posture, owner approval, and reuse trigger." : options2.needsRuntimeEvidence ? "The runtime evidence packet names the observed failure source, identifier, environment, time window, symptom, and open gaps." : options2.needsWorkItem ? "The work item packet names source, id, acceptance criteria, non-goals, and the update expected after execution." : recipe.id === "research" ? "The answer names sources, risks, and next steps without editing the repo." : "Required validation evidence is attached or the missing environment is explicitly explained.",
18825
+ recipe.id === "ticket_write" ? "Acceptance criteria, constraints, validation plan, review bar, stop conditions, and update template are parseable from the ticket body." : recipe.id === "security_dependency" ? "Install integrity, build/typecheck, targeted tests, and scanner/audit evidence show the vulnerability is remediated without breaking the dependency path." : recipe.id === "security_finding" ? "The root cause is fixed with negative regression, safe abuse-case proof, or scanner rerun evidence; suppression-only fixes are explicitly rejected or owner-approved." : recipe.id === "security_testing" ? "The added test or scanner catches the intended bad case or has a clear preventive rationale, and rollout mode is calibrated for developer workflow." : recipe.id === "web_dogfood" ? "The browser report contains reproducible journey evidence, separates product friction from environment noise, and ranks the top user-impact fixes." : recipe.id === "internal_app" ? "The built app follows repo-native UI/data patterns and validation proves the access-control contract." : recipe.id === "stale_pr" ? "The handoff names review feedback addressed, validation, remaining risk, and next reviewer action." : recipe.id === "merge_conflict" ? "The resolution preserves intended behavior from both sides or clearly escalates unresolved semantic conflicts." : recipe.id === "flaky_test" ? "The result stabilizes the test or explains the product root cause with evidence rather than hiding the signal." : recipe.id === "small_refactor" ? "The diff stays mechanical and validation proves no intended behavior changed." : recipe.id === "bug_investigation" ? "The root cause, fix, no-fix decision, or escalation is tied to evidence and includes a follow-up watch signal." : recipe.id === "qa_edge_cases" ? "The added or improved tests cover meaningful behavior risk and do not mute, skip, or obscure the signal." : recipe.id === "performance" ? "Before/after measurements, tradeoffs, and rollback signals are recorded before the optimization is accepted." : recipe.id === "porting" ? "The target behavior preserves source intent, attribution is handled, and intentional differences are explicit." : recipe.id === "experiment" ? "The keep, discard, or formalize decision is recorded with validation and remaining production-readiness gaps." : recipe.id === "session_review" ? "Run cards or skill drafts are tied to actual receipts, validation quality, review outcomes, and privacy posture." : recipe.id === "session_library" ? "The shared examples are derived from approved receipts or explicitly approved imports and include first-run commands, stop conditions, and validation expectations." : options2.needsRuntimeEvidence ? "The root cause, fix, validation, and incident or ticket update are tied back to the runtime evidence." : "The mission output is tied back to the selected recipe and context receipt.",
18177
18826
  autonomy === "read_only" ? "No repo files are changed." : "The local diff has no blocker or high Brief findings before human review.",
18178
18827
  "Useful repeated feedback is saved or intentionally skipped with a reason."
18179
18828
  ],
@@ -18221,6 +18870,12 @@ function missionRunContract(autonomy, recipe, validation, options2, deliveryMode
18221
18870
  "The proposed tests do not map to observable behavior risk or only assert implementation details.",
18222
18871
  "The change hides failures by muting, skipping, or weakening validation."
18223
18872
  ] : [],
18873
+ ...recipe.id === "web_dogfood" ? [
18874
+ "The app URL, safe starting state, critical journey, or expected outcome is missing.",
18875
+ "A journey requires destructive action, private credentials, production data, or an external side effect without explicit approval.",
18876
+ "A finding is reported without reproducible steps, route, expected versus actual behavior, and evidence or an explicit environment limitation.",
18877
+ "Browser, auth, fixture, network, or deployment noise is presented as a product defect without confirmation."
18878
+ ] : [],
18224
18879
  ...recipe.id === "performance" ? [
18225
18880
  "The benchmark, workload, baseline, variance, or success threshold is missing.",
18226
18881
  "The optimization makes readability, correctness, or operability worse without an explicit human tradeoff."
@@ -18373,6 +19028,30 @@ function missionArtifacts(recipe, autonomy, options2) {
18373
19028
  description: "Labels, helper text, descriptions, errors, announcements, and screen-reader context remain accurate after copy changes."
18374
19029
  });
18375
19030
  }
19031
+ if (recipe.id === "web_dogfood") {
19032
+ artifacts.push({
19033
+ id: "browser_journey_plan",
19034
+ label: "Browser journey plan",
19035
+ kind: "plan",
19036
+ producer: "brief.mission_plan",
19037
+ required: true,
19038
+ description: "Safe starting state, critical user journeys, expected outcomes, stop conditions, viewport plan, and evidence policy."
19039
+ }, {
19040
+ id: "browser_journey_evidence",
19041
+ label: "Browser journey evidence",
19042
+ kind: "validation",
19043
+ producer: "available browser",
19044
+ required: true,
19045
+ description: "Journey-by-journey route, actions, expected versus actual behavior, screenshot or DOM evidence, viewport, accessibility notes, and console/network result."
19046
+ }, {
19047
+ id: "dogfooding_report",
19048
+ label: "Dogfooding report",
19049
+ kind: "handoff",
19050
+ producer: "brief.save_handoff",
19051
+ required: true,
19052
+ description: "Concise findings ranked by user impact and reproducibility, environment limitations, top fixes, what worked, and reusable learning."
19053
+ });
19054
+ }
18376
19055
  if (recipe.id === "internal_app") {
18377
19056
  artifacts.push({
18378
19057
  id: "product_operating_context",
@@ -18841,6 +19520,30 @@ function missionHookEvents(recipe, autonomy, options2) {
18841
19520
  mode: "advisory"
18842
19521
  });
18843
19522
  }
19523
+ if (recipe.id === "web_dogfood") {
19524
+ events.push({
19525
+ id: "before_browser_journey",
19526
+ label: "Before browser journey",
19527
+ timing: "before",
19528
+ trigger: "An agent starts an exploratory pass through a local, staging, or deployed web app.",
19529
+ action: "Require safe starting state, 2-5 critical journeys, expected outcomes, viewport plan, evidence fields, and destructive-action stop conditions.",
19530
+ mode: "blocking"
19531
+ }, {
19532
+ id: "after_browser_journey",
19533
+ label: "After browser journey",
19534
+ timing: "after",
19535
+ trigger: "A browser journey or exploratory scenario completes.",
19536
+ action: "Classify findings as product bug, UX friction, accessibility issue, or environment limitation; attach reproducible evidence and rank the top user-impact fixes.",
19537
+ mode: "blocking"
19538
+ }, {
19539
+ id: "before_dogfood_handoff",
19540
+ label: "Before dogfooding handoff",
19541
+ timing: "before",
19542
+ trigger: "The agent prepares a browser dogfooding report.",
19543
+ action: "Save the concise report and receipt ledger; skip review_diff when no repo files changed, and save only repeatable lessons to shared learning.",
19544
+ mode: "blocking"
19545
+ });
19546
+ }
18844
19547
  if (recipe.id === "ticket_write") {
18845
19548
  events.push({
18846
19549
  id: "before_ticket_execution",
@@ -19012,10 +19715,10 @@ function missionHookEvents(recipe, autonomy, options2) {
19012
19715
  if (recipe.id === "session_library") {
19013
19716
  events.push({
19014
19717
  id: "before_session_library_publish",
19015
- label: "Before library publish",
19718
+ label: "Before team sharing",
19016
19719
  timing: "before",
19017
19720
  trigger: "An agent turns previous sessions, transcripts, prompts, or run cards into shared examples.",
19018
- action: "Require receipt proof, privacy classification, owner approval, and explicit raw-import approval before publishing examples to the team.",
19721
+ action: "Require receipt proof, privacy classification, owner approval, and explicit raw-import approval before sharing examples with the team.",
19019
19722
  mode: "blocking"
19020
19723
  });
19021
19724
  }
@@ -19472,6 +20175,31 @@ var init_missionPlan = __esm({
19472
20175
  "learn"
19473
20176
  ]
19474
20177
  },
20178
+ {
20179
+ id: "web_dogfood",
20180
+ title: "Web app dogfooding loop",
20181
+ whenToUse: "Exercise a local or deployed web app like a real user, find the highest-friction moments, and return evidence-backed product feedback before changing code.",
20182
+ defaultAutonomy: "read_only",
20183
+ primaryOutput: "Journey-based dogfooding report with reproducible findings, screenshots, DOM/console/network evidence, accessibility and responsive checks, and prioritized next actions.",
20184
+ requiredGates: [
20185
+ "brief.context_for_task",
20186
+ "brief.codebase_map",
20187
+ "brief.mission_plan",
20188
+ "browser journey evidence",
20189
+ "brief.save_handoff",
20190
+ "brief.verify_receipts",
20191
+ "brief.save_learning"
20192
+ ],
20193
+ loopIds: [
20194
+ "research",
20195
+ "journey",
20196
+ "browser",
20197
+ "validate",
20198
+ "review",
20199
+ "handoff",
20200
+ "learn"
20201
+ ]
20202
+ },
19475
20203
  {
19476
20204
  id: "performance",
19477
20205
  title: "Performance loop",
@@ -20106,6 +20834,51 @@ var init_missionPlan = __esm({
20106
20834
  "Signal quality improved rather than only coverage count."
20107
20835
  ]
20108
20836
  }),
20837
+ web_dogfood: workPattern({
20838
+ title: "Web app dogfooding pattern",
20839
+ teamValue: "Turns browser exploration into a compact, source-grounded product report: real user journeys, visual and interaction evidence, prioritized friction, and a decision about what to fix next.",
20840
+ goodFirstRun: 'brief.mission_plan({ recipe: "web_dogfood", task: "Dogfood <app URL> through <critical journeys>", autonomy: "read_only" })',
20841
+ recommendedSurfaces: [
20842
+ "Codex in-app browser",
20843
+ "Playwright",
20844
+ "Claude browser tools",
20845
+ "Browser MCP",
20846
+ "Storybook or app preview"
20847
+ ],
20848
+ dataContext: [
20849
+ "critical user journeys",
20850
+ "app URL and environment",
20851
+ "repo code map and route ownership",
20852
+ "design system and product rules",
20853
+ "browser screenshots and DOM snapshots",
20854
+ "console errors and failed network requests",
20855
+ "responsive and accessibility observations",
20856
+ "prior dogfooding feedback and known issues"
20857
+ ],
20858
+ validationBurden: [
20859
+ "Name the journeys, starting state, test account or safe fixture, and stop conditions before interacting.",
20860
+ "Use visible user behavior and stable labels; do not infer success from a click alone.",
20861
+ "Capture before/after or step evidence for each finding: route, action, expected result, actual result, severity, and reproduction.",
20862
+ "Check the critical path at desktop and narrow/mobile widths, including loading, empty, error, success, keyboard/focus, and long-content states when relevant.",
20863
+ "Inspect console errors and failed requests after a broken or suspicious interaction; distinguish product bugs from environment/auth failures.",
20864
+ "Keep the session read-only unless the user explicitly asks for a fix; a follow-up fix loop must cite this report.",
20865
+ "End with the top three fixes, what worked, and one proposed learning for the team."
20866
+ ],
20867
+ humanReviewFocus: [
20868
+ "The journey represents a real user job rather than a tour of every page.",
20869
+ "Findings are reproducible, prioritized by user impact, and backed by visible evidence.",
20870
+ "A confusing flow is separated from a purely visual preference or test-environment limitation.",
20871
+ "The report recommends the smallest next product change and names what should remain unchanged."
20872
+ ],
20873
+ parallelism: {
20874
+ fit: "lead_agent",
20875
+ guidance: [
20876
+ "Keep one lead agent responsible for the journey map, evidence quality, and final prioritization.",
20877
+ "Use a second browser session only for an independent mobile, accessibility, or fresh-user pass.",
20878
+ "Do not split one journey across agents; a single owner should observe the full interaction chain."
20879
+ ]
20880
+ }
20881
+ }),
20109
20882
  performance: workPattern({
20110
20883
  title: "Performance work pattern",
20111
20884
  teamValue: "Requires a baseline, workload, threshold, measured diff, and rollback signal before optimization work expands.",
@@ -20251,7 +21024,7 @@ var init_missionPlan = __esm({
20251
21024
  guidance: [
20252
21025
  "Keep one curator responsible for privacy classification and final library shape.",
20253
21026
  "Use parallel agents only to summarize candidate receipts or draft examples from approved sources.",
20254
- "Do not publish raw transcripts, secrets, logs, or customer data without explicit approval."
21027
+ "Do not share raw transcripts, secrets, logs, or customer data without explicit approval."
20255
21028
  ]
20256
21029
  }
20257
21030
  }),
@@ -20416,7 +21189,7 @@ function categoryForRecipe(recipeId) {
20416
21189
  if (recipeId === "incident_fix" || recipeId === "bug_investigation" || recipeId === "flaky_test") {
20417
21190
  return "runtime";
20418
21191
  }
20419
- if (recipeId === "feature" || recipeId === "design_change" || recipeId === "content_change" || recipeId === "qa_edge_cases" || recipeId === "small_refactor" || recipeId === "bugfix") {
21192
+ if (recipeId === "feature" || recipeId === "design_change" || recipeId === "content_change" || recipeId === "web_dogfood" || recipeId === "qa_edge_cases" || recipeId === "small_refactor" || recipeId === "bugfix") {
20420
21193
  return "quality";
20421
21194
  }
20422
21195
  if (recipeId === "internal_app")
@@ -20437,6 +21210,8 @@ function toolForRecipe(recipeId) {
20437
21210
  return "brief.write_ticket";
20438
21211
  if (recipeId === "ticket_fix")
20439
21212
  return "brief.fix_ticket";
21213
+ if (recipeId === "web_dogfood")
21214
+ return "browser + brief.save_handoff";
20440
21215
  if (recipeId === "incident_fix" || recipeId === "flaky_test") {
20441
21216
  return "brief.runtime_fix";
20442
21217
  }
@@ -20527,6 +21302,17 @@ function requiredInputsForRecipe(recipeId) {
20527
21302
  "privacy boundary for source, logs, secrets, and customer data"
20528
21303
  ];
20529
21304
  }
21305
+ if (recipeId === "web_dogfood") {
21306
+ return [
21307
+ "app URL, environment, and safe account or fixture",
21308
+ "two to five critical user journeys",
21309
+ "expected outcome and stop condition for each journey",
21310
+ "desktop and mobile or narrow viewport plan",
21311
+ "accessibility checks for changed interactions",
21312
+ "safe screenshot, DOM, console, and network evidence policy",
21313
+ "known feedback or product principles to test"
21314
+ ];
21315
+ }
20530
21316
  if (recipeId === "incident_fix" || recipeId === "flaky_test" || recipeId === "bug_investigation") {
20531
21317
  return [
20532
21318
  "observed failure source",
@@ -20659,6 +21445,17 @@ function proofBarForRecipe(recipeId) {
20659
21445
  "the new signal is saved as team context for future missions"
20660
21446
  ];
20661
21447
  }
21448
+ if (recipeId === "web_dogfood") {
21449
+ return [
21450
+ "journeys are named before exploration",
21451
+ "each finding has route, steps, expected result, actual result, severity, and evidence",
21452
+ "user-visible behavior is checked before implementation details",
21453
+ "console and failed-request evidence is captured for suspicious states",
21454
+ "desktop/narrow and keyboard/focus checks cover the critical path",
21455
+ "environment/auth failures are separated from product defects",
21456
+ "top fixes, what worked, and a team-learning candidate are prioritized"
21457
+ ];
21458
+ }
20662
21459
  if (recipeId === "incident_fix" || recipeId === "bug_investigation") {
20663
21460
  return [
20664
21461
  "observed facts",
@@ -20758,7 +21555,7 @@ function privacyBoundaryForRecipe(recipeId) {
20758
21555
  if (recipeId === "security_finding") {
20759
21556
  return [
20760
21557
  "redact raw secrets, tokens, cookies, customer payloads, and private logs before agent use",
20761
- "do not publish exploit strings or abuse-case details outside the approved repo/security channel",
21558
+ "do not share exploit strings or abuse-case details outside the approved repo/security channel",
20762
21559
  "provider rotation or invalidation status is tracked without exposing the replacement credential",
20763
21560
  "false-positive and accepted-risk decisions need named owner approval and audit trail"
20764
21561
  ];
@@ -20771,6 +21568,14 @@ function privacyBoundaryForRecipe(recipeId) {
20771
21568
  "keep scanner findings and test fixtures free of real credentials and production payloads"
20772
21569
  ];
20773
21570
  }
21571
+ if (recipeId === "web_dogfood") {
21572
+ return [
21573
+ "use safe fixtures and redact customer data, tokens, cookies, and private URLs",
21574
+ "do not upload screenshots, DOM, console, or network payloads unless the workspace allows it",
21575
+ "prefer derived evidence such as route, action, status, error class, and screenshot reference",
21576
+ "never submit external side effects, purchases, destructive actions, or production writes without explicit approval"
21577
+ ];
21578
+ }
20774
21579
  if (recipeId === "incident_fix" || recipeId === "flaky_test" || recipeId === "bug_investigation") {
20775
21580
  return [
20776
21581
  "summarize raw logs into observed facts first",
@@ -20789,7 +21594,7 @@ function privacyBoundaryForRecipe(recipeId) {
20789
21594
  return [
20790
21595
  "derive examples from receipts before raw transcripts",
20791
21596
  "require approval for prompts, logs, secrets, or customer data",
20792
- "publish concise run cards, not private session dumps"
21597
+ "share concise run cards, not private session dumps"
20793
21598
  ];
20794
21599
  }
20795
21600
  return [
@@ -20822,6 +21627,9 @@ function teamOutcomeForRecipe(recipeId) {
20822
21627
  if (recipeId === "security_testing") {
20823
21628
  return "Teams can add security tests and scanning gates that are useful enough to trust and calibrated enough not to slow every developer down.";
20824
21629
  }
21630
+ if (recipeId === "web_dogfood") {
21631
+ return "Teams get a repeatable user-centered browser pass that finds confusing flows, broken states, accessibility gaps, and product friction before customers do, with evidence a coding agent can act on.";
21632
+ }
20825
21633
  if (recipeId === "flaky_test") {
20826
21634
  return "Teams reduce CI noise without normalizing muted, skipped, or blindly retried failures.";
20827
21635
  }
@@ -20862,6 +21670,9 @@ function firstRunPrompts(items) {
20862
21670
  if (item.recipeId === "security_testing") {
20863
21671
  return 'Call brief.mission_plan with recipe "security_testing" for the security test, scanner, dependency-review, or CI gate you want to add.';
20864
21672
  }
21673
+ if (item.recipeId === "web_dogfood") {
21674
+ return 'Call brief.mission_plan with recipe "web_dogfood" for: <app URL and critical user journeys>.';
21675
+ }
20865
21676
  return `Call brief.mission_plan with recipe "${item.recipeId}" for: <describe the work>.`;
20866
21677
  });
20867
21678
  }
@@ -20919,7 +21730,7 @@ function querySynonyms(term) {
20919
21730
  return ["mcp", "client", "surface", "terminal", "ide"];
20920
21731
  }
20921
21732
  if (term === "dogfood" || term === "dogfooding") {
20922
- return ["first", "receipt", "review"];
21733
+ return ["web", "browser", "journey", "receipt", "review"];
20923
21734
  }
20924
21735
  if (term === "codex")
20925
21736
  return ["codex", "terminal", "mcp"];
@@ -20963,6 +21774,9 @@ function queryScore(item, terms) {
20963
21774
  if (termSet.has("quality") && (item.category === "quality" || item.recipeId === "pr_review")) {
20964
21775
  score += 4;
20965
21776
  }
21777
+ if ((termSet.has("quality") || termSet.has("review")) && item.recipeId === "pr_review") {
21778
+ score += 8;
21779
+ }
20966
21780
  if (termSet.has("product") && item.recipeId === "feature") {
20967
21781
  score += 4;
20968
21782
  }
@@ -20981,8 +21795,8 @@ function queryScore(item, terms) {
20981
21795
  if ((termSet.has("scanner") || termSet.has("scanning") || termSet.has("gate") || termSet.has("tests")) && item.recipeId === "security_testing") {
20982
21796
  score += 7;
20983
21797
  }
20984
- if (termSet.has("dogfood") && ["pr_review", "session_library", "small_refactor"].includes(item.recipeId)) {
20985
- score += 2;
21798
+ if (termSet.has("dogfood") && item.recipeId === "web_dogfood") {
21799
+ score += 12;
20986
21800
  }
20987
21801
  if (termSet.has("tdd") && ["feature", "bugfix", "flaky_test", "dependency"].includes(item.recipeId)) {
20988
21802
  score += 4;
@@ -21054,6 +21868,7 @@ var init_agentLoopCatalog = __esm({
21054
21868
  "security_testing",
21055
21869
  "flaky_test",
21056
21870
  "bug_investigation",
21871
+ "web_dogfood",
21057
21872
  "stale_pr",
21058
21873
  "internal_app",
21059
21874
  "pr_review",
@@ -24223,6 +25038,8 @@ function runCardTitle(recipeId) {
24223
25038
  return "Repeat bug investigation loop";
24224
25039
  case "qa_edge_cases":
24225
25040
  return "Repeat QA edge-case loop";
25041
+ case "web_dogfood":
25042
+ return "Repeat web app dogfooding loop";
24226
25043
  case "performance":
24227
25044
  return "Repeat performance loop";
24228
25045
  case "porting":
@@ -24271,6 +25088,8 @@ function runCardUseWhen(recipeId) {
24271
25088
  return "Use when a hard, rare, intermittent, or previously failed bug needs hypotheses, evidence, validation, and a watch signal.";
24272
25089
  case "qa_edge_cases":
24273
25090
  return "Use when important code paths need missing edge cases, fuzz/stress coverage, or stronger behavior tests.";
25091
+ case "web_dogfood":
25092
+ return "Use when a real user journey through a local or deployed web app needs evidence-backed product feedback.";
24274
25093
  case "performance":
24275
25094
  return "Use when latency, throughput, memory, CPU, or benchmark work needs a baseline before code changes.";
24276
25095
  case "porting":
@@ -24386,6 +25205,16 @@ function runCardSteps(recipeId) {
24386
25205
  "decision"
24387
25206
  ];
24388
25207
  }
25208
+ if (recipeId === "web_dogfood") {
25209
+ return [
25210
+ "journeys",
25211
+ "starting state",
25212
+ "browser pass",
25213
+ "evidence",
25214
+ "prioritized findings",
25215
+ "decision"
25216
+ ];
25217
+ }
24389
25218
  if (recipeId === "session_review") {
24390
25219
  return [
24391
25220
  "receipts",
@@ -24504,6 +25333,9 @@ function runCardStopConditions(recipeId) {
24504
25333
  if (recipeId === "experiment") {
24505
25334
  stops.unshift("Success signal, throwaway boundary, discard path, or decision owner is missing.");
24506
25335
  }
25336
+ if (recipeId === "web_dogfood") {
25337
+ stops.unshift("The journey, starting state, expected outcome, or evidence policy is missing.", "The agent is treating a click, route change, or visual preference as proof of success without checking user-visible outcome.", "A screenshot, DOM snapshot, console error, network failure, or auth/environment limitation is being shared without redaction or classification.");
25338
+ }
24507
25339
  if (recipeId === "session_review") {
24508
25340
  return [
24509
25341
  "Raw prompt transcripts, secrets, customer data, or private logs are required without explicit approval.",
@@ -24597,6 +25429,9 @@ function inferLoopId2(task) {
24597
25429
  if (/\b(edge cases?|missing tests?|test gaps?|coverage gaps?|property[- ]based|qa|quality pass|corner cases?)\b/.test(normalized)) {
24598
25430
  return "qa_edge_cases";
24599
25431
  }
25432
+ if (/\b(dogfood|dogfooding|exploratory browser|web app smoke|browser smoke|product safari|try the app|use the app)\b/.test(normalized) && /\b(app|site|web|browser|page|screen|flow|journey|ui|ux|localhost|staging|production)\b/.test(normalized)) {
25433
+ return "web_dogfood";
25434
+ }
24600
25435
  if (/\b(performance|latency|throughput|benchmark|slow|speed|optimi[sz]e|profile|memory|cpu)\b/.test(normalized)) {
24601
25436
  return "performance";
24602
25437
  }
@@ -25264,6 +26099,7 @@ function autoEngageInstruction(scan, skillLearning) {
25264
26099
  "- Read `brief.next.beforeYouCode.compactText` or `brief.start.beforeYouCode.compactText` before the first edit. Cite its target repo, branch, inspect-first files, reuse decision, likely tests, and stop signals in your working notes.",
25265
26100
  "- For micro or small cockpit passes, keep Brief lightweight: inspect, reuse or rule out the top candidate, validate narrowly, and skip mission_plan/save_handoff/verify_receipts unless scope grows.",
25266
26101
  "- For product, design, content, workflow, compliance, internal-app, or wiki work, read `beforeYouCode.productOperatingContext.compactText` or call `brief.product_context` before editing. Reuse or rule out listed patterns, then preserve its journey, terminology, CTA, state-machine, and acceptance checks.",
26102
+ "- When the task asks about users, JTBD, competitors, market alternatives, product taste, microcopy, integrations, or current external behavior, call `brief.product_research_plan` automatically. Use the host's approved web/browser tools for current public evidence, then call `brief.save_product_context` with cited proposed facts; never turn search snippets or competitor claims into defaults without review.",
25267
26103
  "- If the user gives a Linear, Jira, GitHub, Sentry, Datadog, CI, log, trace, or pasted issue reference, pass that exact reference into `brief.start` instead of inventing a separate plan.",
25268
26104
  "- Before creating a new helper, component, route, workflow, template, design pattern, command, or skill, call `brief.reference_implementations` and `brief.codebase_map` to look for reuse first.",
25269
26105
  "- When scope, files, risk, or validation changes mid-task, refresh with `brief.context_for_task`, `brief.rules_for_files`, `brief.codebase_map`, `brief.find_tests`, or `brief.review_memory` before continuing.",
@@ -25347,6 +26183,14 @@ function autoEngageTriggers() {
25347
26183
  agentBehavior: "Look for existing primitives and saved references before adding another implementation.",
25348
26184
  proof: "The final update names reused candidates or explains why new code was justified."
25349
26185
  },
26186
+ {
26187
+ id: "product_research",
26188
+ label: "Product research",
26189
+ when: "The task asks about users, JTBD, competitors, market alternatives, product taste, microcopy, integrations, or current external behavior.",
26190
+ briefCalls: ["brief.product_research_plan", "brief.save_product_context"],
26191
+ agentBehavior: "Create a focused research plan, use the host's approved public research tools, and save only cited proposed facts with an implication for the product. Keep direct user evidence, repo evidence, and market evidence distinct.",
26192
+ proof: "The task includes a research question, source URLs or repo paths, access dates, confidence, proposed/approved status, and open questions when evidence conflicts."
26193
+ },
25350
26194
  {
25351
26195
  id: "scope_change",
25352
26196
  label: "Scope changes",
@@ -26079,7 +26923,7 @@ function consoleLoopContract(scan) {
26079
26923
  `Use this when the current console cannot call MCP tools directly for ${scan.repoName}.`,
26080
26924
  "",
26081
26925
  "1. Ask the operator for a Brief context packet or to run `brief.context_for_task` for the task.",
26082
- "2. State the mission recipe you are following: feature, design_change, content_change, bugfix, ticket_write, ticket_fix, incident_fix, internal_app, stale_pr, merge_conflict, flaky_test, small_refactor, bug_investigation, qa_edge_cases, performance, porting, experiment, pr_review, session_review, session_library, research, docs_wiki, or dependency.",
26926
+ "2. State the mission recipe you are following: feature, design_change, content_change, bugfix, ticket_write, ticket_fix, incident_fix, internal_app, stale_pr, merge_conflict, flaky_test, small_refactor, bug_investigation, qa_edge_cases, web_dogfood, performance, porting, experiment, pr_review, session_review, session_library, research, docs_wiki, or dependency.",
26083
26927
  "3. Keep scope bounded to the cited files, owners, rules, tests, and wiki pages.",
26084
26928
  "4. Before suggesting handoff, list the validation commands to run and what each proves.",
26085
26929
  "5. Ask for `brief.review_diff`, `brief.save_handoff`, and `brief.verify_receipts` from an MCP-capable surface before PR review.",
@@ -26121,11 +26965,12 @@ function firstRunPromptsFor(label, scan) {
26121
26965
  "Ask the agent: Save one proven implementation reference with brief.save_reference, then list it with brief.reference_implementations.",
26122
26966
  "Ask the agent: Call brief.agent_pack_inventory and summarize skills, commands, hooks, MCP configs, host configs, installers, and high-risk trust boundaries.",
26123
26967
  "Ask the agent: Call brief.agent_pack_adoption_plan before importing a skill, command, or prompt pack.",
26968
+ 'For web app dogfooding: Call brief.mission_plan with recipe "web_dogfood", name 2-5 critical journeys and a safe starting state, then use the available browser and hand back the top fixes with evidence.',
26124
26969
  "For UI work: Call brief.design_system_map and use the team's templates, tokens, components, setup rules, and design_adherence gate.",
26125
26970
  "For a ticket: Call brief.fix_ticket with the work item, optional runtime evidence, and autonomy validate.",
26126
26971
  "For a runtime signal: Call brief.runtime_fix with the Sentry issue, Datadog trace, CI failure, log query, or incident id.",
26127
26972
  'For an internal analytics app: Call brief.mission_plan with recipe "internal_app", then follow the returned auth/data/access-control gates.',
26128
- 'For maintenance work: Call brief.mission_plan with recipe "stale_pr", "merge_conflict", "flaky_test", "small_refactor", "bug_investigation", "qa_edge_cases", "performance", "porting", or "experiment" before editing.',
26973
+ 'For maintenance work: Call brief.mission_plan with recipe "stale_pr", "merge_conflict", "flaky_test", "small_refactor", "bug_investigation", "qa_edge_cases", "web_dogfood", "performance", "porting", or "experiment" before editing.',
26129
26974
  "Before handoff: Call brief.review_diff, brief.save_handoff, and brief.verify_receipts so the final update includes review, proof bundle, and ledger status."
26130
26975
  ];
26131
26976
  }
@@ -26135,7 +26980,7 @@ function mcpJsonSnippet(installMode, briefSourceRoot, repoRoot) {
26135
26980
  args: ["-lc", localSourceMcpCommand(briefSourceRoot, repoRoot)]
26136
26981
  } : {
26137
26982
  command: "npx",
26138
- args: ["-y", briefPublishedPackageSpec]
26983
+ args: ["-y", briefPublishedPackageInstallSpec]
26139
26984
  };
26140
26985
  const env = {
26141
26986
  BRIEF_REPO_ROOT: repoRoot ?? "${workspaceFolder}",
@@ -26202,7 +27047,7 @@ function rolloutStepsFor(readiness) {
26202
27047
  "Promote the repo once rules, tests, owners, and memories are discoverable."
26203
27048
  ];
26204
27049
  }
26205
- var ALL_TARGETS, briefPublishedPackageSpec, TARGET_LABELS, INSTALL_COMMANDS, CONNECTOR_DESTINATIONS;
27050
+ var ALL_TARGETS, briefPublishedPackageVersion, briefPublishedPackageSpec, briefPublishedPackageInstallSpec, TARGET_LABELS, INSTALL_COMMANDS, CONNECTOR_DESTINATIONS;
26206
27051
  var init_agentSetup = __esm({
26207
27052
  "../core/dist/agentSetup.js"() {
26208
27053
  "use strict";
@@ -26220,7 +27065,9 @@ var init_agentSetup = __esm({
26220
27065
  "ide",
26221
27066
  "generic"
26222
27067
  ];
26223
- briefPublishedPackageSpec = "runbrief@0.1.2";
27068
+ briefPublishedPackageVersion = "0.1.4";
27069
+ briefPublishedPackageSpec = `runbrief@${briefPublishedPackageVersion}`;
27070
+ briefPublishedPackageInstallSpec = `runbrief@^${briefPublishedPackageVersion}`;
26224
27071
  TARGET_LABELS = {
26225
27072
  terminal: "Terminal agent",
26226
27073
  vscode: "VS Code",
@@ -26233,15 +27080,15 @@ var init_agentSetup = __esm({
26233
27080
  generic: "Any MCP client"
26234
27081
  };
26235
27082
  INSTALL_COMMANDS = {
26236
- terminal: `npx -y ${briefPublishedPackageSpec}`,
26237
- vscode: `npx -y ${briefPublishedPackageSpec}`,
26238
- claude: `claude mcp add --transport stdio --scope user brief -- npx -y ${briefPublishedPackageSpec}`,
27083
+ terminal: `npx -y ${briefPublishedPackageInstallSpec}`,
27084
+ vscode: `npx -y ${briefPublishedPackageInstallSpec}`,
27085
+ claude: `claude mcp add --transport stdio --scope user brief -- npx -y ${briefPublishedPackageInstallSpec}`,
26239
27086
  claude_console: "Paste the mission_plan loop recipe when direct MCP tools are unavailable.",
26240
- codex: `codex mcp add brief -- npx -y ${briefPublishedPackageSpec}`,
26241
- cursor: `npx -y ${briefPublishedPackageSpec}`,
27087
+ codex: `codex mcp add brief -- npx -y ${briefPublishedPackageInstallSpec}`,
27088
+ cursor: `npx -y ${briefPublishedPackageInstallSpec}`,
26242
27089
  cursor_console: "Paste the mission_plan loop recipe when direct MCP tools are unavailable.",
26243
- ide: `npx -y ${briefPublishedPackageSpec}`,
26244
- generic: `npx -y ${briefPublishedPackageSpec}`
27090
+ ide: `npx -y ${briefPublishedPackageInstallSpec}`,
27091
+ generic: `npx -y ${briefPublishedPackageInstallSpec}`
26245
27092
  };
26246
27093
  CONNECTOR_DESTINATIONS = {
26247
27094
  terminal: "Repo shell MCP server env",
@@ -26511,6 +27358,9 @@ function recipeForIntent(kind, request, explicitRecipe, runtimeEvidence) {
26511
27358
  return /\b(bug|broken|fix|incorrect|wrong)\b/i.test(request) ? "bugfix" : "feature";
26512
27359
  }
26513
27360
  const lower = request.toLowerCase();
27361
+ if (/\b(dogfood|dogfooding|exploratory browser|web app smoke|browser smoke|product safari|try the app|use the app)\b/.test(lower) && /\b(app|site|web|browser|page|screen|flow|journey|ui|ux|localhost|staging|production)\b/.test(lower)) {
27362
+ return "web_dogfood";
27363
+ }
26514
27364
  if (isDesignLedChange(lower)) {
26515
27365
  return "design_change";
26516
27366
  }
@@ -26547,7 +27397,7 @@ function isProductJourneyAudit(request) {
26547
27397
  return /\b(audit|assess|evaluate|inspect|study)\b/.test(request) && /\b(app|experience|flow|journey|launch readiness|onboarding|product|self[- ]serve|workspace)\b/.test(request);
26548
27398
  }
26549
27399
  function autonomyForIntent(kind, recipe) {
26550
- if (kind === "context_for_task" || kind === "write_ticket" || recipe === "research") {
27400
+ if (kind === "context_for_task" || kind === "write_ticket" || recipe === "research" || recipe === "web_dogfood") {
26551
27401
  return "read_only";
26552
27402
  }
26553
27403
  return "validate";
@@ -27208,6 +28058,9 @@ function summaryForIntent(kind, recipe, workItem, runtimeEvidence) {
27208
28058
  return "Brief inferred a compact pre-PR readiness pass.";
27209
28059
  if (kind === "context_for_task")
27210
28060
  return "Brief inferred a context-only preflight.";
28061
+ if (recipe === "web_dogfood") {
28062
+ return "Brief inferred a read-only browser dogfooding loop: exercise journeys, capture evidence, and rank the top user-impact fixes.";
28063
+ }
27211
28064
  return `Brief inferred a ${recipe ?? "mission"} loop.`;
27212
28065
  }
27213
28066
  function reasonForIntent(kind, request, workItem, runtimeEvidence) {
@@ -27281,7 +28134,7 @@ function summarizeBeforeYouCodeSharedContext(delivery) {
27281
28134
  next: "Shared guidance is not available in this session; keep new learning personal until the workspace connection is ready."
27282
28135
  };
27283
28136
  }
27284
- const next = delivery.status === "applied" ? "Use the applied shared guidance, then save a focused learning or reference when the pass proves a reusable pattern." : delivery.status === "available_not_applied" ? "Shared guidance exists but did not match this task; inspect the relevant team item before creating a new pattern." : "No approved shared guidance exists yet; keep this pass personal and publish only after the pattern is proven.";
28137
+ const next = delivery.status === "applied" ? "Use the applied shared guidance, then save a focused learning or reference when the pass proves a reusable pattern." : delivery.status === "available_not_applied" ? "Shared guidance exists but did not match this task; inspect the relevant team item before creating a new pattern." : "No approved shared guidance exists yet; keep this pass personal and make it available to the team only after the pattern is proven.";
27285
28138
  return {
27286
28139
  status: delivery.status,
27287
28140
  workspaceId: delivery.workspaceId,
@@ -27424,11 +28277,13 @@ function buildBeforeYouCodeMap(scan, input) {
27424
28277
  files,
27425
28278
  maxLines: 18
27426
28279
  });
28280
+ const mapCoverage = mapCoverageFor2(scan);
27427
28281
  const stopSignals2 = unique([
27428
28282
  "Wrong repo, branch, or worktree.",
27429
28283
  ...target.branch?.startsWith("detached@") ? ["Detached HEAD; select the intended feature branch before editing."] : [],
27430
28284
  ...insights.agentPacket.stopSignals,
27431
28285
  ...productOperatingContext.stopSignals.slice(0, 3),
28286
+ ...mapCoverage.status === "partial" ? [mapCoverage.summary] : [],
27432
28287
  ...target.dirty && files.length === 0 ? ["Dirty worktree is broad; classify baseline before editing."] : []
27433
28288
  ]).slice(0, 5);
27434
28289
  const status = inspectFirst.length === 0 && reuseCandidates.length === 0 ? "needs_narrower_scope" : "ready";
@@ -27448,6 +28303,7 @@ function buildBeforeYouCodeMap(scan, input) {
27448
28303
  reuseCandidates,
27449
28304
  likelyTests,
27450
28305
  productOperatingContext,
28306
+ mapCoverage,
27451
28307
  ...context.teamGuidance ? { teamGuidance: context.teamGuidance } : {},
27452
28308
  referenceFirst,
27453
28309
  validationChecklist,
@@ -27460,6 +28316,7 @@ function buildBeforeYouCodeMap(scan, input) {
27460
28316
  reuseCandidates,
27461
28317
  likelyTests,
27462
28318
  productOperatingContext,
28319
+ mapCoverage,
27463
28320
  validationChecklist,
27464
28321
  stopSignals: stopSignals2,
27465
28322
  ...context.teamGuidance ? { teamGuidance: context.teamGuidance } : {},
@@ -27479,7 +28336,9 @@ function buildBeforeYouCodeMap(scan, input) {
27479
28336
  candidates: reuseCandidates,
27480
28337
  decision: reuseCandidates.length > 0 ? "For each candidate, choose reuse, adapt, or rule out and record the decision before creating a new primitive." : "No reuse candidate is grounded yet; run one focused search before creating a new primitive."
27481
28338
  },
28339
+ mapCoverage,
27482
28340
  ...context.teamGuidance ? { teamGuidance: context.teamGuidance } : {},
28341
+ rules: context.rules.slice(0, 8),
27483
28342
  referenceFirst,
27484
28343
  likelyTests,
27485
28344
  exactTests,
@@ -27491,6 +28350,19 @@ function buildBeforeYouCodeMap(scan, input) {
27491
28350
  next: status === "ready" ? "Inspect these files and reuse candidates before editing; cite the reuse decision in the final handoff." : "Run brief.codebase_map or brief.context_for_task with a narrower task, folder, or file list before editing."
27492
28351
  };
27493
28352
  }
28353
+ function mapCoverageFor2(scan) {
28354
+ const mappedNodes = scan.stats.mapNodeCount;
28355
+ const omittedNodes = Math.max(0, scan.stats.mapOmittedCount ?? 0);
28356
+ const candidateNodes = Math.max(mappedNodes, scan.stats.mapTotalCandidateCount ?? mappedNodes + omittedNodes);
28357
+ const partial = Boolean(scan.stats.mapPartial) || omittedNodes > 0;
28358
+ return {
28359
+ status: partial ? "partial" : "complete",
28360
+ mappedNodes,
28361
+ candidateNodes,
28362
+ omittedNodes,
28363
+ summary: partial ? `Code map is partial: ${mappedNodes} of ${candidateNodes} candidate nodes are available; narrow by feature, folder, or symbol before treating a missing reuse candidate as proof none exists.` : `Code map covers all ${mappedNodes} indexed candidate nodes for this repo.`
28364
+ };
28365
+ }
27494
28366
  function reuseStatusFor(candidates) {
27495
28367
  if (candidates.length === 0)
27496
28368
  return "missing";
@@ -27611,6 +28483,7 @@ function cockpitFor(input) {
27611
28483
  "Broad e2e or full-suite tests unless routing, auth, persistence, or cross-page state changed."
27612
28484
  ];
27613
28485
  const stopSignals2 = unique([
28486
+ ...input.mapCoverage.status === "partial" ? [input.mapCoverage.summary] : [],
27614
28487
  ...input.stopSignals.slice(0, 3),
27615
28488
  ...mode === "lightweight" ? [
27616
28489
  "The tiny pass starts touching behavior, state, data, auth, or multiple surfaces."
@@ -27798,55 +28671,55 @@ function diversifyJourneyStages(ranked, task) {
27798
28671
  ];
27799
28672
  }
27800
28673
  function journeyStagePathPreference(stage, filePath) {
27801
- const path20 = filePath.toLowerCase();
28674
+ const path21 = filePath.toLowerCase();
27802
28675
  if (stage === "acquisition") {
27803
- if (/landinginteractive\.[cm]?[tj]sx?$/.test(path20))
28676
+ if (/landinginteractive\.[cm]?[tj]sx?$/.test(path21))
27804
28677
  return 40;
27805
- if (/apps\/web\/src\/app\/page\.[cm]?[tj]sx?$/.test(path20))
28678
+ if (/apps\/web\/src\/app\/page\.[cm]?[tj]sx?$/.test(path21))
27806
28679
  return 35;
27807
28680
  }
27808
28681
  if (stage === "access") {
27809
- if (/apps\/web\/src\/app\/login\/page\.[cm]?[tj]sx?$/.test(path20))
28682
+ if (/apps\/web\/src\/app\/login\/page\.[cm]?[tj]sx?$/.test(path21))
27810
28683
  return 40;
27811
- if (/api\/auth\/magic-link\/route\.[cm]?[tj]s$/.test(path20))
28684
+ if (/api\/auth\/magic-link\/route\.[cm]?[tj]s$/.test(path21))
27812
28685
  return 35;
27813
28686
  }
27814
28687
  if (stage === "workspace") {
27815
- if (/api\/v1\/workspaces\/bootstrap\/route\.[cm]?[tj]s$/.test(path20)) {
28688
+ if (/api\/v1\/workspaces\/bootstrap\/route\.[cm]?[tj]s$/.test(path21)) {
27816
28689
  return 40;
27817
28690
  }
27818
28691
  }
27819
28692
  if (stage === "activation") {
27820
- if (/api\/v1\/workspaces\/[^/]+\/api-keys\/route\.[cm]?[tj]s$/.test(path20)) {
28693
+ if (/api\/v1\/workspaces\/[^/]+\/api-keys\/route\.[cm]?[tj]s$/.test(path21)) {
27821
28694
  return 40;
27822
28695
  }
27823
- if (/workspace-agent-actions\.[cm]?[tj]s$/.test(path20))
28696
+ if (/workspace-agent-actions\.[cm]?[tj]s$/.test(path21))
27824
28697
  return 35;
27825
- if (/agent-handoff-composer\.[cm]?[tj]s$/.test(path20))
28698
+ if (/agent-handoff-composer\.[cm]?[tj]s$/.test(path21))
27826
28699
  return 30;
27827
28700
  }
27828
28701
  if (stage === "collaboration") {
27829
- if (/api\/v1\/workspaces\/[^/]+\/invitations\/route\.[cm]?[tj]s$/.test(path20)) {
28702
+ if (/api\/v1\/workspaces\/[^/]+\/invitations\/route\.[cm]?[tj]s$/.test(path21)) {
27830
28703
  return 40;
27831
28704
  }
27832
- if (/api\/v1\/workspaces\/[^/]+\/members\/[^/]+\/route/.test(path20)) {
28705
+ if (/api\/v1\/workspaces\/[^/]+\/members\/[^/]+\/route/.test(path21)) {
27833
28706
  return 30;
27834
28707
  }
27835
28708
  }
27836
28709
  if (stage === "monetization") {
27837
- if (/billing\/checkout\/route\.[cm]?[tj]s$/.test(path20))
28710
+ if (/billing\/checkout\/route\.[cm]?[tj]s$/.test(path21))
27838
28711
  return 40;
27839
- if (/pricing\/page\.[cm]?[tj]sx?$/.test(path20))
28712
+ if (/pricing\/page\.[cm]?[tj]sx?$/.test(path21))
27840
28713
  return 35;
27841
28714
  }
27842
28715
  return 0;
27843
28716
  }
27844
- function beforeYouCodePathIntentScore(path20, task) {
27845
- const lowerPath = (path20.split("#", 1)[0] ?? path20).toLowerCase();
28717
+ function beforeYouCodePathIntentScore(path21, task) {
28718
+ const lowerPath = (path21.split("#", 1)[0] ?? path21).toLowerCase();
27846
28719
  const lowerTask = task.toLowerCase();
27847
- let score = namedCodePathIntentScore(path20, task);
28720
+ let score = namedCodePathIntentScore(path21, task);
27848
28721
  const wantsInterface = /\b(app|design|header|interface|landing|layout|mobile|navigation|page|responsive|spacing|ui|ux|visual|workspace)\b/.test(lowerTask);
27849
- score += lexicalPathTaskScore(path20, task);
28722
+ score += lexicalPathTaskScore(path21, task);
27850
28723
  const wantsLanding = /\b(hero|homepage|landing)\b/.test(lowerTask);
27851
28724
  const wantsAppShell = /\b(app|header|internal|mobile|navigation|responsive|sidebar|workspace)\b/.test(lowerTask);
27852
28725
  const wantsAuth = /\b(auth|authentication|callback|login|magic link|otp|session|sign[ -]?in|sign[ -]?out)\b/.test(lowerTask);
@@ -27996,8 +28869,8 @@ function beforeYouCodePathIntentScore(path20, task) {
27996
28869
  }
27997
28870
  return score;
27998
28871
  }
27999
- function beforeYouCodeIntentSummary(path20, task) {
28000
- const normalizedPath = (path20.split("#", 1)[0] ?? path20).toLowerCase();
28872
+ function beforeYouCodeIntentSummary(path21, task) {
28873
+ const normalizedPath = (path21.split("#", 1)[0] ?? path21).toLowerCase();
28001
28874
  if (!isInternalAppTask(task))
28002
28875
  return void 0;
28003
28876
  if (/apps\/web\/src\/app\/app\/page\.[cm]?[tj]sx?$/.test(normalizedPath)) {
@@ -28023,7 +28896,7 @@ function prefersPrimaryWorkspaceTask(task) {
28023
28896
  return true;
28024
28897
  return /\b(design|designer|figma|screenshot|visual|ui|ux|layout|spacing|typography|responsive|mobile)\b/i.test(task);
28025
28898
  }
28026
- function lexicalPathTaskScore(path20, task) {
28899
+ function lexicalPathTaskScore(path21, task) {
28027
28900
  const ignored = /* @__PURE__ */ new Set([
28028
28901
  "agent",
28029
28902
  "brief",
@@ -28043,7 +28916,7 @@ function lexicalPathTaskScore(path20, task) {
28043
28916
  taskWords.add("workspace");
28044
28917
  taskWords.add("pane");
28045
28918
  }
28046
- const pathWords = path20.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 4 && !ignored.has(word));
28919
+ const pathWords = path21.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 4 && !ignored.has(word));
28047
28920
  const matches = unique(pathWords.filter((word) => taskWords.has(word)));
28048
28921
  const structuralBonus = matches.reduce((score, word) => {
28049
28922
  if (!["header", "pane", "shell", "sidebar", "workspace"].includes(word)) {
@@ -28117,6 +28990,7 @@ function compactTextFor2(input) {
28117
28990
  `Before coding: ${input.headline}`,
28118
28991
  `Target: ${input.target.repoName} | ${input.target.branch ?? "branch unknown"} | ${input.target.repoRoot}`,
28119
28992
  `Worktree: ${input.target.dirty ? "dirty" : "clean"}${input.target.changedFiles.length ? `; changed ${input.target.changedFiles.slice(0, 4).join(", ")}` : ""}${input.target.untrackedFiles.length ? `; untracked ${input.target.untrackedFiles.slice(0, 3).join(", ")}` : ""}`,
28993
+ `Map coverage: ${input.mapCoverage.summary}`,
28120
28994
  ...input.target.requestedFiles.length ? [`Scope files: ${input.target.requestedFiles.slice(0, 5).join(", ")}`] : [],
28121
28995
  ...input.teamGuidance ? [
28122
28996
  `Shared guidance: ${input.teamGuidance.applied.length > 0 ? input.teamGuidance.applied.slice(0, 2).map((item) => item.label).join(", ") : input.teamGuidance.status === "available_not_applied" ? "available, but none matched this task" : "none approved yet"}`
@@ -28167,16 +29041,20 @@ var init_agentBeforeYouCode = __esm({
28167
29041
  // ../core/dist/agentWorkPacket.js
28168
29042
  function buildAgentWorkPacket(input) {
28169
29043
  const before = input.beforeYouCode;
28170
- const rules = [
29044
+ const rules = uniqueRules2([
28171
29045
  ...(before.teamGuidance?.applied ?? []).map((item) => ({
28172
29046
  rule: item.label,
28173
29047
  source: `team:${item.kind}`
28174
29048
  })),
29049
+ ...(before.rules ?? []).map((item) => ({
29050
+ rule: compact(item.body || item.title, 180),
29051
+ source: item.sourcePaths[0] ?? "repo guidance"
29052
+ })),
28175
29053
  ...before.productOperatingContext.productRules.map((item) => ({
28176
29054
  rule: item.rule,
28177
29055
  source: item.source
28178
29056
  }))
28179
- ].slice(0, 3);
29057
+ ]).slice(0, 3);
28180
29058
  const tests = before.exactTests.length > 0 ? before.exactTests.slice(0, 3).map((test) => ({
28181
29059
  command: compact(test.command, 180),
28182
29060
  reason: compact(test.reason, 180),
@@ -28192,7 +29070,8 @@ function buildAgentWorkPacket(input) {
28192
29070
  ]).slice(0, 3);
28193
29071
  const stopConditions = unique2([
28194
29072
  ...before.stopSignals,
28195
- ...before.productOperatingContext.stopSignals
29073
+ ...before.productOperatingContext.stopSignals,
29074
+ ...before.mapCoverage.status === "partial" ? [before.mapCoverage.summary] : []
28196
29075
  ]).slice(0, 3);
28197
29076
  const nextAction = input.nextAction ?? before.next ?? "Inspect the listed files and make the reuse decision before editing.";
28198
29077
  const packet = {
@@ -28220,6 +29099,7 @@ function buildAgentWorkPacket(input) {
28220
29099
  why: compact(item.why, 140)
28221
29100
  }))
28222
29101
  },
29102
+ mapCoverage: before.mapCoverage,
28223
29103
  rules,
28224
29104
  tests,
28225
29105
  acceptance,
@@ -28236,6 +29116,7 @@ function formatAgentWorkPacket(packet) {
28236
29116
  `Interpretation: ${packet.interpretation}`,
28237
29117
  `Inspect first: ${inlineList(packet.inspectFirst.map((item) => `${item.path} - ${item.why}`))}`,
28238
29118
  `Reuse (${packet.reuse.status}): ${packet.reuse.decision}; ${inlineList(packet.reuse.candidates.map((item) => `${item.path} - ${item.action}`))}`,
29119
+ `Map: ${packet.mapCoverage.summary}`,
28239
29120
  `Rules: ${inlineList(packet.rules.map((item) => `${item.rule} [${item.source}]`))}`,
28240
29121
  `Focused tests: ${inlineList(packet.tests.map((item) => `${item.command} - ${item.reason}`))}`,
28241
29122
  `Acceptance: ${inlineList(packet.acceptance)}`,
@@ -28254,6 +29135,16 @@ function compact(value, maxLength) {
28254
29135
  function unique2(values) {
28255
29136
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
28256
29137
  }
29138
+ function uniqueRules2(rules) {
29139
+ const seen = /* @__PURE__ */ new Set();
29140
+ return rules.filter((item) => {
29141
+ const key = item.rule.toLowerCase();
29142
+ if (!item.rule || seen.has(key))
29143
+ return false;
29144
+ seen.add(key);
29145
+ return true;
29146
+ });
29147
+ }
28257
29148
  var init_agentWorkPacket = __esm({
28258
29149
  "../core/dist/agentWorkPacket.js"() {
28259
29150
  "use strict";
@@ -28469,7 +29360,7 @@ var init_governance = __esm({
28469
29360
  // ../core/dist/hookSetup.js
28470
29361
  function generateHookSetup(input = {}) {
28471
29362
  const mode = input.mode ?? "advisory";
28472
- const packageCommand = input.packageCommand ?? `npx -y ${briefPublishedPackageSpec}`;
29363
+ const packageCommand = input.packageCommand ?? `npx -y ${briefPublishedPackageInstallSpec}`;
28473
29364
  const failOn = mode === "blocking" ? "high" : "blocker";
28474
29365
  return {
28475
29366
  mode,
@@ -31192,6 +32083,7 @@ var init_diffReview = __esm({
31192
32083
  "handler",
31193
32084
  "helpers",
31194
32085
  "delete",
32086
+ "dynamic",
31195
32087
  "get",
31196
32088
  "head",
31197
32089
  "input",
@@ -33076,6 +33968,7 @@ var init_dist2 = __esm({
33076
33968
  init_agentBeforeYouCode();
33077
33969
  init_agentWorkPacket();
33078
33970
  init_productOperatingContext();
33971
+ init_productIntelligence();
33079
33972
  init_ticketMissionRuntime();
33080
33973
  init_governance();
33081
33974
  init_hookSetup();
@@ -33102,7 +33995,7 @@ function cloudSyncStatus(env = process.env) {
33102
33995
  return {
33103
33996
  configured: false,
33104
33997
  privacyMode,
33105
- reason: privacyMode === "local-only" ? "Cloud sync is disabled by privacy mode." : "Set BRIEF_API_URL and BRIEF_API_KEY to enable MCP cloud sync."
33998
+ reason: privacyMode === "local-only" ? "Cloud sync is disabled by privacy mode." : "Connect Brief from the app to authorize this device, or provide BRIEF_API_URL and BRIEF_API_KEY for an approved legacy setup."
33106
33999
  };
33107
34000
  }
33108
34001
  const status = {
@@ -33259,6 +34152,64 @@ async function fetchTeamContext(repoRoot, env = process.env, fetcher = fetch) {
33259
34152
  });
33260
34153
  return getTeamContextCloud(config, payload, fetcher);
33261
34154
  }
34155
+ async function claimRemoteLoopRun(runnerId, leaseSeconds = 180, env = process.env, fetcher = fetch) {
34156
+ const config = cloudConfigFromEnv(env);
34157
+ if (!config) throw new Error("Brief cloud connection is not configured for the runner.");
34158
+ const payload = claimLoopRunRequestSchema.parse({ runnerId, leaseSeconds });
34159
+ const body = await loopRunRequest(
34160
+ config,
34161
+ "/api/v1/mcp/loop-runs/claim",
34162
+ "POST",
34163
+ payload,
34164
+ fetcher
34165
+ );
34166
+ if (!body.ok) throw new Error(body.message ?? "Brief could not claim a loop run.");
34167
+ if (!body.run) return null;
34168
+ const parsed = loopRunSummarySchema.safeParse(body.run);
34169
+ if (!parsed.success) throw new Error("Brief returned an invalid loop run.");
34170
+ return parsed.data;
34171
+ }
34172
+ async function updateRemoteLoopRun(runId, input, env = process.env, fetcher = fetch) {
34173
+ const config = cloudConfigFromEnv(env);
34174
+ if (!config) throw new Error("Brief cloud connection is not configured for the runner.");
34175
+ const payload = updateLoopRunRequestSchema.parse(input);
34176
+ const body = await loopRunRequest(
34177
+ config,
34178
+ `/api/v1/mcp/loop-runs/${encodeURIComponent(runId)}`,
34179
+ "PATCH",
34180
+ payload,
34181
+ fetcher
34182
+ );
34183
+ if (!body.ok) throw new Error(body.message ?? "Brief could not update the loop run.");
34184
+ const parsed = loopRunSummarySchema.safeParse(body.run);
34185
+ if (!parsed.success) throw new Error("Brief returned an invalid updated loop run.");
34186
+ return parsed.data;
34187
+ }
34188
+ async function loopRunRequest(config, route, method, payload, fetcher) {
34189
+ const controller = new AbortController();
34190
+ const timeout = setTimeout(() => controller.abort(), syncTimeoutMs);
34191
+ try {
34192
+ const response = await fetcher(`${config.apiUrl}${route}`, {
34193
+ method,
34194
+ headers: {
34195
+ authorization: `Bearer ${config.apiKey}`,
34196
+ "content-type": "application/json"
34197
+ },
34198
+ body: JSON.stringify(payload),
34199
+ signal: controller.signal
34200
+ });
34201
+ const body = await safeJson(response);
34202
+ const record = body && typeof body === "object" && !Array.isArray(body) ? body : {};
34203
+ const result = {
34204
+ ok: response.ok && record.ok === true
34205
+ };
34206
+ if (typeof record.message === "string") result.message = record.message;
34207
+ if (record.run !== void 0) result.run = record.run;
34208
+ return result;
34209
+ } finally {
34210
+ clearTimeout(timeout);
34211
+ }
34212
+ }
33262
34213
  function toRepoScanSummary(scan, privacyMode) {
33263
34214
  return {
33264
34215
  repoName: scan.repoName,
@@ -33304,7 +34255,7 @@ function skippedResult(env) {
33304
34255
  return {
33305
34256
  status: "skipped",
33306
34257
  privacyMode,
33307
- reason: privacyMode === "local-only" ? "Cloud sync skipped because privacy mode is local-only." : "Cloud sync skipped because BRIEF_API_URL or BRIEF_API_KEY is missing."
34258
+ reason: privacyMode === "local-only" ? "Cloud sync skipped because privacy mode is local-only." : "Cloud sync skipped because this device is not connected. Create a browser-authorized connection from the Brief app, then restart the agent."
33308
34259
  };
33309
34260
  }
33310
34261
  function skippedTeamContextResult(env) {
@@ -33312,7 +34263,7 @@ function skippedTeamContextResult(env) {
33312
34263
  return {
33313
34264
  status: "skipped",
33314
34265
  privacyMode,
33315
- reason: privacyMode === "local-only" ? "Cloud sync skipped because privacy mode is local-only." : "Cloud sync skipped because BRIEF_API_URL or BRIEF_API_KEY is missing."
34266
+ reason: privacyMode === "local-only" ? "Cloud sync skipped because privacy mode is local-only." : "Cloud sync skipped because this device is not connected. Create a browser-authorized connection from the Brief app, then restart the agent."
33316
34267
  };
33317
34268
  }
33318
34269
  function normalizeApiUrl(apiUrl) {
@@ -33736,9 +34687,9 @@ function issuesValue(value) {
33736
34687
  if (typeof item === "string") return item;
33737
34688
  if (!item || typeof item !== "object") return "";
33738
34689
  const record = item;
33739
- const path20 = Array.isArray(record.path) ? record.path.join(".") : "";
34690
+ const path21 = Array.isArray(record.path) ? record.path.join(".") : "";
33740
34691
  const message = stringValue(record.message);
33741
- return [path20, message].filter(Boolean).join(": ");
34692
+ return [path21, message].filter(Boolean).join(": ");
33742
34693
  }).filter(Boolean).slice(0, 5).join("; ");
33743
34694
  }
33744
34695
  var syncTimeoutMs, canonicalBriefApiUrl, legacyBriefApiUrls;
@@ -34129,14 +35080,178 @@ var init_missionInputOptions = __esm({
34129
35080
  }
34130
35081
  });
34131
35082
 
34132
- // ../mcp/src/workspace.ts
34133
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
35083
+ // ../mcp/src/cloudCredentials.ts
35084
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
35085
+ import os from "node:os";
34134
35086
  import path14 from "node:path";
35087
+ async function bootstrapCloudCredentials(env = process.env) {
35088
+ if (env.BRIEF_PRIVACY_MODE === "local-only") {
35089
+ return { status: "skipped", reason: "local_only" };
35090
+ }
35091
+ if (env.BRIEF_API_URL && env.BRIEF_API_KEY) {
35092
+ return { status: "configured", source: "env" };
35093
+ }
35094
+ const credentialPath = credentialsPath(env);
35095
+ const requestedOrigin = originFrom(
35096
+ env.BRIEF_API_URL ?? env.BRIEF_CONNECT_URL
35097
+ );
35098
+ const requestedWorkspace = env.BRIEF_WORKSPACE_ID?.trim();
35099
+ const stored = readCredentialFile(credentialPath);
35100
+ const existing = stored.credentials.find((credential2) => {
35101
+ if (requestedOrigin && originFrom(credential2.apiUrl) !== requestedOrigin) {
35102
+ return false;
35103
+ }
35104
+ return !requestedWorkspace || credential2.workspaceId === requestedWorkspace;
35105
+ });
35106
+ if (existing) {
35107
+ applyCredential(env, existing);
35108
+ return { status: "configured", source: "local_store" };
35109
+ }
35110
+ const connectUrl = env.BRIEF_CONNECT_URL?.trim();
35111
+ const connectCode = env.BRIEF_CONNECT_CODE?.trim();
35112
+ if (!connectUrl || !connectCode) {
35113
+ return {
35114
+ status: "skipped",
35115
+ reason: "no_cloud_credential_or_pairing_setup"
35116
+ };
35117
+ }
35118
+ let response;
35119
+ try {
35120
+ response = await fetch(connectUrl, {
35121
+ method: "POST",
35122
+ headers: {
35123
+ accept: "application/json",
35124
+ "content-type": "application/json"
35125
+ },
35126
+ body: JSON.stringify({ code: connectCode }),
35127
+ signal: AbortSignal.timeout(1e4)
35128
+ });
35129
+ } catch {
35130
+ return {
35131
+ status: "failed",
35132
+ reason: "Could not reach the Brief connection service. Check your network and try a fresh setup from the Brief app."
35133
+ };
35134
+ }
35135
+ const body = await response.json().catch(() => null);
35136
+ if (!response.ok || !body?.ok || !body.apiUrl || !body.apiKey || !apiKeyPattern.test(body.apiKey) || !body.workspaceId || !body.privacyMode) {
35137
+ return {
35138
+ status: "failed",
35139
+ reason: body?.message ?? "This Brief setup link is expired or already used. Create a fresh connection from the Brief app."
35140
+ };
35141
+ }
35142
+ const credential = {
35143
+ apiUrl: body.apiUrl,
35144
+ apiKey: body.apiKey,
35145
+ workspaceId: body.workspaceId,
35146
+ privacyMode: body.privacyMode,
35147
+ ...body.connectorId ? { connectorId: body.connectorId } : {},
35148
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35149
+ };
35150
+ try {
35151
+ writeCredentialFile(credentialPath, [
35152
+ ...stored.credentials.filter(
35153
+ (item) => item.workspaceId !== credential.workspaceId || originFrom(item.apiUrl) !== originFrom(credential.apiUrl)
35154
+ ),
35155
+ credential
35156
+ ]);
35157
+ } catch {
35158
+ return {
35159
+ status: "failed",
35160
+ reason: "Brief connected, but could not save the local credential. Check permissions for the local Brief config directory and try again."
35161
+ };
35162
+ }
35163
+ applyCredential(env, credential);
35164
+ return { status: "configured", source: "pairing" };
35165
+ }
35166
+ function forgetCloudCredentials(env = process.env) {
35167
+ const credentialPath = credentialsPath(env);
35168
+ const stored = readCredentialFile(credentialPath);
35169
+ const requestedOrigin = originFrom(
35170
+ env.BRIEF_API_URL ?? env.BRIEF_CONNECT_URL
35171
+ );
35172
+ const requestedWorkspace = env.BRIEF_WORKSPACE_ID?.trim();
35173
+ const remaining = stored.credentials.filter((credential) => {
35174
+ if (requestedOrigin && originFrom(credential.apiUrl) !== requestedOrigin) {
35175
+ return true;
35176
+ }
35177
+ return Boolean(
35178
+ requestedWorkspace && credential.workspaceId !== requestedWorkspace
35179
+ );
35180
+ });
35181
+ writeCredentialFile(credentialPath, remaining);
35182
+ }
35183
+ function defaultCredentialsPath(env = process.env) {
35184
+ return credentialsPath(env);
35185
+ }
35186
+ function credentialsPath(env) {
35187
+ return env.BRIEF_CREDENTIALS_FILE?.trim() || path14.join(
35188
+ env.XDG_CONFIG_HOME?.trim() || path14.join(os.homedir(), ".config"),
35189
+ "runbrief",
35190
+ "credentials.json"
35191
+ );
35192
+ }
35193
+ function readCredentialFile(filePath) {
35194
+ try {
35195
+ const parsed = JSON.parse(readFileSync3(filePath, "utf8"));
35196
+ if (parsed.version !== 1 || !Array.isArray(parsed.credentials)) {
35197
+ return { version: 1, credentials: [] };
35198
+ }
35199
+ return {
35200
+ version: 1,
35201
+ credentials: parsed.credentials.filter(isStoredCredential)
35202
+ };
35203
+ } catch {
35204
+ return { version: 1, credentials: [] };
35205
+ }
35206
+ }
35207
+ function writeCredentialFile(filePath, credentials) {
35208
+ mkdirSync2(path14.dirname(filePath), { recursive: true, mode: 448 });
35209
+ chmodSync(path14.dirname(filePath), 448);
35210
+ writeFileSync5(
35211
+ filePath,
35212
+ `${JSON.stringify({ version: 1, credentials }, null, 2)}
35213
+ `,
35214
+ { encoding: "utf8", mode: 384 }
35215
+ );
35216
+ chmodSync(filePath, 384);
35217
+ }
35218
+ function applyCredential(env, credential) {
35219
+ env.BRIEF_API_URL = credential.apiUrl;
35220
+ env.BRIEF_API_KEY = credential.apiKey;
35221
+ env.BRIEF_WORKSPACE_ID = credential.workspaceId;
35222
+ env.BRIEF_PRIVACY_MODE = credential.privacyMode;
35223
+ }
35224
+ function originFrom(value) {
35225
+ if (!value) return void 0;
35226
+ try {
35227
+ return new URL(value).origin;
35228
+ } catch {
35229
+ return void 0;
35230
+ }
35231
+ }
35232
+ function isStoredCredential(value) {
35233
+ if (!value || typeof value !== "object") return false;
35234
+ const candidate = value;
35235
+ return Boolean(
35236
+ typeof candidate.apiUrl === "string" && typeof candidate.apiKey === "string" && apiKeyPattern.test(candidate.apiKey) && typeof candidate.workspaceId === "string" && typeof candidate.privacyMode === "string" && typeof candidate.updatedAt === "string"
35237
+ );
35238
+ }
35239
+ var apiKeyPattern;
35240
+ var init_cloudCredentials = __esm({
35241
+ "../mcp/src/cloudCredentials.ts"() {
35242
+ "use strict";
35243
+ apiKeyPattern = /^brf_(live|test)_[a-zA-Z0-9]{8,}_[a-zA-Z0-9]{24,}$/;
35244
+ }
35245
+ });
35246
+
35247
+ // ../mcp/src/workspace.ts
35248
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
35249
+ import path15 from "node:path";
34135
35250
  function resolveRepoRoot(repoPath, options2 = {}) {
34136
35251
  return resolveRepoRootDetails(repoPath, options2).repoRoot;
34137
35252
  }
34138
35253
  function resolveRepoRootDetails(repoPath, options2 = {}) {
34139
- const cwd = path14.resolve(options2.cwd ?? process.cwd());
35254
+ const cwd = path15.resolve(options2.cwd ?? process.cwd());
34140
35255
  const env = options2.env ?? process.env;
34141
35256
  const cwdResolution = resolveExistingRoot(cwd, "cwd");
34142
35257
  const cwdIsCustomerWorktree = isGitWorktree(cwdResolution.repoRoot) && !looksLikeBriefSourceCheckout(cwdResolution.repoRoot);
@@ -34150,7 +35265,7 @@ function resolveRepoRootDetails(repoPath, options2 = {}) {
34150
35265
  ]);
34151
35266
  const explicitRepoPath = repoPath?.trim();
34152
35267
  if (explicitRepoPath) {
34153
- const explicitPathKind = path14.isAbsolute(explicitRepoPath) ? "absolute" : "relative";
35268
+ const explicitPathKind = path15.isAbsolute(explicitRepoPath) ? "absolute" : "relative";
34154
35269
  if (explicitPathKind === "relative" && envCandidates.length > 1) {
34155
35270
  return {
34156
35271
  ...cwdResolution,
@@ -34162,7 +35277,7 @@ function resolveRepoRootDetails(repoPath, options2 = {}) {
34162
35277
  const relativeBase = cwdIsCustomerWorktree ? cwdResolution.repoRoot : envCandidates[0]?.repoRoot ?? cwd;
34163
35278
  return {
34164
35279
  ...resolveExistingRoot(
34165
- path14.resolve(
35280
+ path15.resolve(
34166
35281
  explicitPathKind === "absolute" ? cwd : relativeBase,
34167
35282
  explicitRepoPath
34168
35283
  ),
@@ -34200,7 +35315,7 @@ function resolveFromEnvVars(cwd, env, envVars, options2 = {}) {
34200
35315
  for (const envVar of envVars) {
34201
35316
  const value = cleanWorkspacePath(env[envVar]);
34202
35317
  if (!value) continue;
34203
- const start = path14.resolve(cwd, value);
35318
+ const start = path15.resolve(cwd, value);
34204
35319
  if (existsSync5(start)) {
34205
35320
  const resolution = resolveExistingRoot(start, "environment", envVar);
34206
35321
  if (options2.skipBriefSource && looksLikeBriefSourceCheckout(resolution.repoRoot)) {
@@ -34213,7 +35328,7 @@ function resolveFromEnvVars(cwd, env, envVars, options2 = {}) {
34213
35328
  }
34214
35329
  function repoTargetHealth(resolution, options2 = {}) {
34215
35330
  const env = options2.env ?? process.env;
34216
- const cwd = path14.resolve(options2.cwd ?? process.cwd());
35331
+ const cwd = path15.resolve(options2.cwd ?? process.cwd());
34217
35332
  const gitRoot = runGit(resolution.repoRoot, ["rev-parse", "--show-toplevel"]);
34218
35333
  const evidence = [
34219
35334
  ...rootEvidence(resolution, cwd, env),
@@ -34274,7 +35389,7 @@ function repoTargetHealth(resolution, options2 = {}) {
34274
35389
  };
34275
35390
  }
34276
35391
  function resolveExistingRoot(start, source, envVar) {
34277
- const gitStart = existsSync5(start) && !statSync4(start).isDirectory() ? path14.dirname(start) : start;
35392
+ const gitStart = existsSync5(start) && !statSync4(start).isDirectory() ? path15.dirname(start) : start;
34278
35393
  const gitRoot = runGit(gitStart, ["rev-parse", "--show-toplevel"]);
34279
35394
  return {
34280
35395
  repoRoot: gitRoot && existsSync5(gitRoot) ? gitRoot : start,
@@ -34325,12 +35440,12 @@ function preferPinnedRepoRoot(env) {
34325
35440
  return env.BRIEF_REPO_ROOT_MODE === "pinned" || env.BRIEF_REPO_ROOT_PINNED === "true";
34326
35441
  }
34327
35442
  function looksLikeBriefSourceCheckout(repoRoot) {
34328
- if (!existsSync5(path14.join(repoRoot, "packages", "mcp", "src", "server.ts"))) {
35443
+ if (!existsSync5(path15.join(repoRoot, "packages", "mcp", "src", "server.ts"))) {
34329
35444
  return false;
34330
35445
  }
34331
35446
  try {
34332
35447
  const pkg = JSON.parse(
34333
- readFileSync3(path14.join(repoRoot, "package.json"), "utf8")
35448
+ readFileSync4(path15.join(repoRoot, "package.json"), "utf8")
34334
35449
  );
34335
35450
  return pkg.name === "brief";
34336
35451
  } catch {
@@ -34359,7 +35474,7 @@ var init_workspace = __esm({
34359
35474
  });
34360
35475
 
34361
35476
  // ../mcp/src/connectorReadiness.ts
34362
- import path15 from "node:path";
35477
+ import path16 from "node:path";
34363
35478
  function connectorReadiness(input) {
34364
35479
  const runtimeLabel = input.mode === "cli" ? "Brief CLI process" : "Brief MCP connector";
34365
35480
  const installStatusTool = input.mode === "cli" ? "install-status or brief.install_status" : "brief.install_status";
@@ -34453,8 +35568,8 @@ function connectorReadiness(input) {
34453
35568
  };
34454
35569
  }
34455
35570
  function briefSourceCheckoutMismatch(input) {
34456
- const repoRoot = input.repoRoot ? path15.resolve(input.repoRoot) : void 0;
34457
- const sourceRoot = input.freshness.sourceRoot ? path15.resolve(input.freshness.sourceRoot) : void 0;
35571
+ const repoRoot = input.repoRoot ? path16.resolve(input.repoRoot) : void 0;
35572
+ const sourceRoot = input.freshness.sourceRoot ? path16.resolve(input.freshness.sourceRoot) : void 0;
34458
35573
  if (!repoRoot || !sourceRoot) return void 0;
34459
35574
  if (samePath(repoRoot, sourceRoot)) return void 0;
34460
35575
  if (!looksLikeBriefSourceCheckout(repoRoot)) return void 0;
@@ -34469,7 +35584,7 @@ function briefSourceCheckoutMismatch(input) {
34469
35584
  };
34470
35585
  }
34471
35586
  function samePath(left, right) {
34472
- return path15.normalize(left) === path15.normalize(right);
35587
+ return path16.normalize(left) === path16.normalize(right);
34473
35588
  }
34474
35589
  var init_connectorReadiness = __esm({
34475
35590
  "../mcp/src/connectorReadiness.ts"() {
@@ -34489,9 +35604,9 @@ function buildConnectorRecoveryPlan(input) {
34489
35604
  });
34490
35605
  const restartNeeded = readiness.status === "restart_required" || readiness.status === "source_mismatch";
34491
35606
  const needsVerification = readiness.status === "needs_verification";
34492
- const canSelfRestart = input.mode === "server" && readiness.status !== "source_mismatch";
35607
+ const canSelfRestart = input.mode === "server" && readiness.status !== "wrong_repo";
34493
35608
  const confirmed = input.confirm === briefRestartConfirmation;
34494
- const shouldSchedule = canSelfRestart && confirmed && (readiness.status === "restart_required" || input.force === true);
35609
+ const shouldSchedule = canSelfRestart && confirmed && (readiness.status === "restart_required" || readiness.status === "source_mismatch" || input.force === true);
34495
35610
  const status = shouldSchedule ? "restart_scheduled" : restartNeeded ? readiness.status : needsVerification ? "needs_verification" : "ready";
34496
35611
  const verifyExpect = [
34497
35612
  "connectorReadiness.status is ready",
@@ -34515,7 +35630,7 @@ function buildConnectorRecoveryPlan(input) {
34515
35630
  mcpSelfRestart: {
34516
35631
  tool: "brief.restart_connector",
34517
35632
  available: canSelfRestart,
34518
- behavior: "When confirmed, Brief returns this recovery packet, waits briefly, then exits the local MCP process. MCP hosts that supervise subprocesses can relaunch it; hosts that do not should be restarted or reloaded by the user.",
35633
+ behavior: "When confirmed, Brief returns this recovery packet, waits briefly, then exits the local MCP process. MCP hosts that supervise subprocesses can relaunch it; hosts that do not should be restarted or reloaded by the user. A restart refreshes the configured source; it does not change a host's source path.",
34519
35634
  call: {
34520
35635
  confirm: briefRestartConfirmation,
34521
35636
  surface: selectedSurface,
@@ -34538,7 +35653,8 @@ function buildConnectorRecoveryPlan(input) {
34538
35653
  }
34539
35654
  function summaryForStatus(status, readiness) {
34540
35655
  if (status === "restart_scheduled") {
34541
- return "Brief scheduled the local MCP process to exit so the host can relaunch a fresh connector.";
35656
+ const restartSummary = "Brief scheduled the local MCP process to exit so the host can relaunch a fresh connector.";
35657
+ return readiness.status === "source_mismatch" ? `${restartSummary} ${readiness.summary} Verify the host source path after reconnecting.` : restartSummary;
34542
35658
  }
34543
35659
  return readiness.summary;
34544
35660
  }
@@ -34603,8 +35719,8 @@ var init_connectorRecovery = __esm({
34603
35719
  });
34604
35720
 
34605
35721
  // ../mcp/src/serverFreshness.ts
34606
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync5 } from "node:fs";
34607
- import path16 from "node:path";
35722
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync5 } from "node:fs";
35723
+ import path17 from "node:path";
34608
35724
  function mcpServerFreshness(options2) {
34609
35725
  const cwd = options2.cwd ?? process.cwd();
34610
35726
  const now = options2.now ?? /* @__PURE__ */ new Date();
@@ -34615,10 +35731,12 @@ function mcpServerFreshness(options2) {
34615
35731
  cwd,
34616
35732
  sourceRoot: options2.sourceRoot
34617
35733
  });
35734
+ const sourceKind = options2.distribution ?? connectorDistributionFor(options2.entrypoint);
34618
35735
  const watchedGlobs = watchedSourceGlobs();
34619
35736
  const base = {
34620
35737
  serverStartedAt: options2.serverStartedAt.toISOString(),
34621
35738
  checkedAt: now.toISOString(),
35739
+ sourceKind,
34622
35740
  process: {
34623
35741
  pid: process.pid,
34624
35742
  cwd,
@@ -34633,6 +35751,15 @@ function mcpServerFreshness(options2) {
34633
35751
  changedSinceStartCount: 0
34634
35752
  };
34635
35753
  if (!sourceRoot) {
35754
+ if (sourceKind === "package") {
35755
+ return {
35756
+ status: "current",
35757
+ ...base,
35758
+ action: freshnessAction("current"),
35759
+ summary: "The published Brief connector is running from an immutable package; source-checkout freshness checks are not applicable.",
35760
+ next: "Continue using Brief normally. Fresh MCP starts resolve the latest compatible patch; reload the host to pick it up, and use a new generated setup for a future minor or major release."
35761
+ };
35762
+ }
34636
35763
  return {
34637
35764
  status: "unknown",
34638
35765
  ...base,
@@ -34673,6 +35800,21 @@ function mcpServerFreshness(options2) {
34673
35800
  next: status === "restart_recommended" ? "Call brief.restart_connector from the coding agent, then call brief.install_status again and confirm serverFreshness.status is current." : "Continue using Brief normally."
34674
35801
  };
34675
35802
  }
35803
+ function connectorDistributionFor(entrypoint) {
35804
+ const configured = process.env.BRIEF_CONNECTOR_DISTRIBUTION;
35805
+ if (configured === "package" || configured === "source_checkout") {
35806
+ return configured;
35807
+ }
35808
+ const normalizedEntrypoint = path17.normalize(entrypoint);
35809
+ if (normalizedEntrypoint.includes(
35810
+ `${path17.sep}node_modules${path17.sep}runbrief${path17.sep}`
35811
+ ) || normalizedEntrypoint.includes(
35812
+ `${path17.sep}packages${path17.sep}runbrief${path17.sep}`
35813
+ )) {
35814
+ return "package";
35815
+ }
35816
+ return "unknown";
35817
+ }
34676
35818
  function freshnessAction(status) {
34677
35819
  if (status === "current") {
34678
35820
  return {
@@ -34707,30 +35849,30 @@ function findBriefSourceRoot(options2) {
34707
35849
  const candidates = [
34708
35850
  options2.sourceRoot,
34709
35851
  process.env.BRIEF_SOURCE_ROOT,
34710
- options2.entrypoint ? path16.dirname(options2.entrypoint) : void 0,
35852
+ options2.entrypoint ? path17.dirname(options2.entrypoint) : void 0,
34711
35853
  options2.cwd
34712
35854
  ].filter((candidate) => Boolean(candidate));
34713
35855
  for (const candidate of candidates) {
34714
- const root = walkUpForBriefRoot(path16.resolve(candidate));
35856
+ const root = walkUpForBriefRoot(path17.resolve(candidate));
34715
35857
  if (root) return root;
34716
35858
  }
34717
35859
  return void 0;
34718
35860
  }
34719
35861
  function walkUpForBriefRoot(start) {
34720
- let current = existsSync6(start) && statSafe(start)?.isFile() ? path16.dirname(start) : start;
35862
+ let current = existsSync6(start) && statSafe(start)?.isFile() ? path17.dirname(start) : start;
34721
35863
  while (true) {
34722
35864
  if (isBriefSourceRoot(current)) return current;
34723
- const parent = path16.dirname(current);
35865
+ const parent = path17.dirname(current);
34724
35866
  if (parent === current) return void 0;
34725
35867
  current = parent;
34726
35868
  }
34727
35869
  }
34728
35870
  function isBriefSourceRoot(candidate) {
34729
- const packageJsonPath = path16.join(candidate, "package.json");
35871
+ const packageJsonPath = path17.join(candidate, "package.json");
34730
35872
  if (!existsSync6(packageJsonPath)) return false;
34731
35873
  const packageJson = readJson(packageJsonPath);
34732
35874
  if (packageJson?.name !== "brief") return false;
34733
- return existsSync6(path16.join(candidate, "packages", "mcp")) && existsSync6(path16.join(candidate, "packages", "core")) && existsSync6(path16.join(candidate, "packages", "contracts"));
35875
+ return existsSync6(path17.join(candidate, "packages", "mcp")) && existsSync6(path17.join(candidate, "packages", "core")) && existsSync6(path17.join(candidate, "packages", "contracts"));
34734
35876
  }
34735
35877
  function watchedSourceGlobs() {
34736
35878
  return [
@@ -34768,7 +35910,7 @@ function collectWatchedFiles(sourceRoot, maxWatchedFiles) {
34768
35910
  truncated = true;
34769
35911
  break;
34770
35912
  }
34771
- const absolutePath = path16.join(sourceRoot, relativePath);
35913
+ const absolutePath = path17.join(sourceRoot, relativePath);
34772
35914
  const stats = statSafe(absolutePath);
34773
35915
  if (!stats) continue;
34774
35916
  if (stats.isDirectory()) {
@@ -34785,7 +35927,7 @@ function collectWatchedFiles(sourceRoot, maxWatchedFiles) {
34785
35927
  break;
34786
35928
  }
34787
35929
  files.push({
34788
- path: normalizePath(path16.relative(sourceRoot, absolutePath)),
35930
+ path: normalizePath(path17.relative(sourceRoot, absolutePath)),
34789
35931
  modifiedAtMs: stats.mtimeMs
34790
35932
  });
34791
35933
  }
@@ -34796,7 +35938,7 @@ function walkFiles2(sourceRoot, directory, files, maxWatchedFiles) {
34796
35938
  const entries = readdirSafe(directory);
34797
35939
  for (const entry of entries) {
34798
35940
  if (files.length >= maxWatchedFiles) return { truncated: true };
34799
- const absolutePath = path16.join(directory, entry.name);
35941
+ const absolutePath = path17.join(directory, entry.name);
34800
35942
  if (entry.isDirectory()) {
34801
35943
  if (shouldSkipDirectory(entry.name)) continue;
34802
35944
  const result = walkFiles2(
@@ -34812,7 +35954,7 @@ function walkFiles2(sourceRoot, directory, files, maxWatchedFiles) {
34812
35954
  const stats = statSafe(absolutePath);
34813
35955
  if (!stats) continue;
34814
35956
  files.push({
34815
- path: normalizePath(path16.relative(sourceRoot, absolutePath)),
35957
+ path: normalizePath(path17.relative(sourceRoot, absolutePath)),
34816
35958
  modifiedAtMs: stats.mtimeMs
34817
35959
  });
34818
35960
  }
@@ -34831,13 +35973,13 @@ function shouldSkipDirectory(name) {
34831
35973
  );
34832
35974
  }
34833
35975
  function shouldWatchFile(filePath) {
34834
- const base = path16.basename(filePath);
35976
+ const base = path17.basename(filePath);
34835
35977
  if (base.endsWith(".tsbuildinfo")) return false;
34836
35978
  if (base === ".DS_Store") return false;
34837
35979
  return true;
34838
35980
  }
34839
35981
  function normalizePath(value) {
34840
- return value.split(path16.sep).join("/");
35982
+ return value.split(path17.sep).join("/");
34841
35983
  }
34842
35984
  function statSafe(filePath) {
34843
35985
  try {
@@ -34855,7 +35997,7 @@ function readdirSafe(directory) {
34855
35997
  }
34856
35998
  function readJson(filePath) {
34857
35999
  try {
34858
- return JSON.parse(readFileSync4(filePath, "utf8"));
36000
+ return JSON.parse(readFileSync5(filePath, "utf8"));
34859
36001
  } catch {
34860
36002
  return void 0;
34861
36003
  }
@@ -34918,13 +36060,16 @@ var init_serverInstructions = __esm({
34918
36060
  "../mcp/src/serverInstructions.ts"() {
34919
36061
  "use strict";
34920
36062
  briefMcpInstructions = [
34921
- "Brief is the default repo-context, reuse, review, and handoff companion for non-trivial coding work. This is the normal path, not an optional reminder. Do not wait for the user to repeat 'use Brief'.",
34922
- "At the first Brief-relevant task in a session, call brief.install_status once and confirm the repo target and connector freshness.",
34923
- "When the user asks for non-trivial work, call brief.next with the user's actual request before inspecting or editing files, then follow its compact workPacket and recommendation. Keep micro changes lightweight.",
34924
- "Before creating a helper, component, route, workflow, command, or skill, use the returned reuse candidates or call brief.reference_implementations and brief.codebase_map.",
34925
- "Refresh Brief context when the task, files, risk, or validation plan changes.",
36063
+ "Brief is the default repo-context, reuse, review, and handoff companion for non-trivial coding work. Do not wait for the user to repeat 'use Brief'.",
36064
+ "At the first task, call brief.install_status and confirm repo and connector freshness. Include task when known to get the first context packet.",
36065
+ "Local mode is the default and needs no account. Team mode adds approved shared guidance. If team sync is unavailable, continue local repo work and mention it only when it affects the task.",
36066
+ "Package-backed connectors pick up compatible patches on the next MCP start. Reload a long-lived host after an update; routine patches do not need a reinstall.",
36067
+ "For non-trivial work, call brief.next with the request before inspecting or editing and follow its compact recommendation. Auto-engage. Keep micro changes lightweight.",
36068
+ "Before creating a helper, component, route, workflow, command, or skill, use reuse candidates or call brief.reference_implementations and brief.codebase_map.",
36069
+ "For user, JTBD, competitor, market, taste, copy, or integration questions, call brief.product_research_plan, research with the host, then save cited proposed facts with brief.save_product_context.",
36070
+ "Refresh Brief when task, files, risk, or validation changes.",
34926
36071
  "Before a meaningful handoff, call brief.review_diff. Save and verify a handoff only for multi-step, high-risk, or durable work.",
34927
- "Keep Brief quiet in chat: report one concrete way it helped and the validation or remaining risk in one or two lines. Never invent Brief usage or paste full receipts unless asked."
36072
+ "Keep Brief quiet: report one concrete benefit and validation or risk in one or two lines. Never invent usage or paste receipts unless asked."
34928
36073
  ].join("\n");
34929
36074
  briefMcpInstructionSignals = [
34930
36075
  "brief.install_status",
@@ -34968,6 +36113,18 @@ function compactInstallStatus(input) {
34968
36113
  const degraded = !blocked && (Boolean(cloudIssue) || input.readiness.verdict !== "agent_ready" || input.workflowWiring.status === "missing" || input.workflowWiring.status === "manual_workflow_only");
34969
36114
  const stats = input.scan.stats;
34970
36115
  const mcpContractActive = input.serverInstructionsActive && input.connectorReadiness.safeToUseBrief;
36116
+ const teamSyncStatus = input.cloudSync.configured ? input.teamContext.status === "failed" ? "unavailable" : input.teamContext.status === "fetched" ? "connected" : "checking" : "not_connected";
36117
+ const connectionMode = teamSyncStatus === "unavailable" ? "team_degraded" : teamSyncStatus === "connected" ? "team" : "local";
36118
+ const connectorUpdate = input.serverFreshness.sourceKind === "package" ? {
36119
+ policy: "automatic_compatible_patches",
36120
+ installSpec: briefPublishedPackageInstallSpec,
36121
+ applies: "next_mcp_start",
36122
+ summary: "Package-backed connectors pick up the latest compatible patch when the MCP host starts them again."
36123
+ } : {
36124
+ policy: "manual_source_restart",
36125
+ applies: "host_restart",
36126
+ summary: "Source-checkout connectors use the files in their configured checkout; restart the host after local Brief changes."
36127
+ };
34971
36128
  const totalMapCandidates = stats.mapTotalCandidateCount ?? stats.mapNodeCount + (stats.mapOmittedCount ?? 0);
34972
36129
  const mapCoveragePercent = totalMapCandidates ? Math.round(stats.mapNodeCount / totalMapCandidates * 100) : 100;
34973
36130
  const repoGuidanceSignals = unique4(
@@ -34996,9 +36153,17 @@ function compactInstallStatus(input) {
34996
36153
  status: input.connectorReadiness.status,
34997
36154
  safeToUse: input.connectorReadiness.safeToUseBrief,
34998
36155
  fresh: input.serverFreshness.status === "current",
36156
+ distribution: input.serverFreshness.sourceKind,
34999
36157
  sourceRoot: input.serverFreshness.sourceRoot,
36158
+ update: connectorUpdate,
35000
36159
  summary: input.connectorReadiness.summary
35001
36160
  },
36161
+ connection: {
36162
+ mode: connectionMode,
36163
+ localAvailable: input.connectorReadiness.safeToUseBrief,
36164
+ teamSync: teamSyncStatus,
36165
+ summary: connectionMode === "team_degraded" ? "Local repo work is available. Team sync needs attention and will not block local analysis." : connectionMode === "team" ? "Local repo work and approved team context are available." : "Local repo work is available without an account. Team sync is optional."
36166
+ },
35002
36167
  cloud: {
35003
36168
  configured: input.cloudSync.configured,
35004
36169
  privacyMode: input.cloudSync.privacyMode,
@@ -35054,8 +36219,9 @@ var briefConnectorVersion;
35054
36219
  var init_installStatus = __esm({
35055
36220
  "../mcp/src/installStatus.ts"() {
35056
36221
  "use strict";
36222
+ init_dist2();
35057
36223
  init_serverInstructions();
35058
- briefConnectorVersion = "0.1.2";
36224
+ briefConnectorVersion = "0.1.4";
35059
36225
  }
35060
36226
  });
35061
36227
 
@@ -35075,8 +36241,7 @@ function buildCompactStartEnvelope(input) {
35075
36241
  route: {
35076
36242
  tool: start.intent.tool,
35077
36243
  ...start.intent.recipe ? { recipe: start.intent.recipe } : {},
35078
- ...start.intent.autonomy ? { autonomy: start.intent.autonomy } : {},
35079
- ...start.intent.reviewBar ? { reviewBar: start.intent.reviewBar } : {}
36244
+ ...start.intent.autonomy ? { autonomy: start.intent.autonomy } : {}
35080
36245
  },
35081
36246
  workPacket: (() => {
35082
36247
  const packet = buildAgentWorkPacket({
@@ -35086,7 +36251,6 @@ function buildCompactStartEnvelope(input) {
35086
36251
  nextAction: input.next
35087
36252
  });
35088
36253
  return {
35089
- schemaVersion: packet.schemaVersion,
35090
36254
  compactText: packet.compactText
35091
36255
  };
35092
36256
  })(),
@@ -35241,15 +36405,15 @@ var init_startSummary = __esm({
35241
36405
  });
35242
36406
 
35243
36407
  // ../mcp/src/wikiInit.ts
35244
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
35245
- import path17 from "node:path";
36408
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
36409
+ import path18 from "node:path";
35246
36410
  function writeWikiInitArtifacts(repoRoot, wiki, options2) {
35247
- const wikiDir = path17.join(repoRoot, ".brief", "wiki");
35248
- const pagesDir = path17.join(wikiDir, "pages");
35249
- mkdirSync2(pagesDir, { recursive: true });
36411
+ const wikiDir = path18.join(repoRoot, ".brief", "wiki");
36412
+ const pagesDir = path18.join(wikiDir, "pages");
36413
+ mkdirSync3(pagesDir, { recursive: true });
35250
36414
  const pageArtifacts = wiki.pages.map((page2) => {
35251
- const pagePath = path17.join(pagesDir, `${wikiPageSlug(page2)}.md`);
35252
- writeFileSync5(pagePath, renderWikiPageMarkdown(wiki, page2));
36415
+ const pagePath = path18.join(pagesDir, `${wikiPageSlug(page2)}.md`);
36416
+ writeFileSync6(pagePath, renderWikiPageMarkdown(wiki, page2));
35253
36417
  return {
35254
36418
  id: page2.id,
35255
36419
  title: page2.title,
@@ -35258,32 +36422,32 @@ function writeWikiInitArtifacts(repoRoot, wiki, options2) {
35258
36422
  quality: page2.quality.status
35259
36423
  };
35260
36424
  });
35261
- const readmePath = path17.join(wikiDir, "README.md");
35262
- writeFileSync5(readmePath, renderWikiReadme(wiki, pageArtifacts));
35263
- const manifestPath = path17.join(wikiDir, "manifest.json");
35264
- writeFileSync5(
36425
+ const readmePath = path18.join(wikiDir, "README.md");
36426
+ writeFileSync6(readmePath, renderWikiReadme(wiki, pageArtifacts));
36427
+ const manifestPath = path18.join(wikiDir, "manifest.json");
36428
+ writeFileSync6(
35265
36429
  manifestPath,
35266
36430
  `${JSON.stringify(wikiInitManifest(wiki, pageArtifacts), null, 2)}
35267
36431
  `
35268
36432
  );
35269
- const workflowTemplatePath = path17.join(
36433
+ const workflowTemplatePath = path18.join(
35270
36434
  wikiDir,
35271
36435
  "brief-wiki-refresh.github-actions.yml"
35272
36436
  );
35273
- writeFileSync5(workflowTemplatePath, renderWikiRefreshWorkflow());
36437
+ writeFileSync6(workflowTemplatePath, renderWikiRefreshWorkflow());
35274
36438
  const agentFiles = options2.writeAgentFiles ? ["AGENTS.md", "CLAUDE.md"].map(
35275
36439
  (fileName) => upsertWikiAgentInstructions(repoRoot, fileName)
35276
36440
  ) : [];
35277
36441
  let githubWorkflow2;
35278
36442
  if (options2.writeGithubWorkflow) {
35279
- const workflowPath = path17.join(
36443
+ const workflowPath = path18.join(
35280
36444
  repoRoot,
35281
36445
  ".github",
35282
36446
  "workflows",
35283
36447
  "brief-wiki-refresh.yml"
35284
36448
  );
35285
- mkdirSync2(path17.dirname(workflowPath), { recursive: true });
35286
- writeFileSync5(workflowPath, renderWikiRefreshWorkflow({ active: true }));
36449
+ mkdirSync3(path18.dirname(workflowPath), { recursive: true });
36450
+ writeFileSync6(workflowPath, renderWikiRefreshWorkflow({ active: true }));
35287
36451
  githubWorkflow2 = {
35288
36452
  path: repoRelativePath(repoRoot, workflowPath),
35289
36453
  status: "written",
@@ -35473,12 +36637,12 @@ function wikiInitManifest(wiki, pageArtifacts) {
35473
36637
  };
35474
36638
  }
35475
36639
  function upsertWikiAgentInstructions(repoRoot, fileName) {
35476
- const filePath = path17.join(repoRoot, fileName);
36640
+ const filePath = path18.join(repoRoot, fileName);
35477
36641
  const status = existsSync7(filePath) ? "updated" : "created";
35478
- const existing = status === "updated" ? readFileSync5(filePath, "utf8") : "";
36642
+ const existing = status === "updated" ? readFileSync6(filePath, "utf8") : "";
35479
36643
  const initial = existing || `# ${fileName}
35480
36644
  `;
35481
- writeFileSync5(filePath, upsertMarkedBlock(initial, wikiAgentBlock()));
36645
+ writeFileSync6(filePath, upsertMarkedBlock(initial, wikiAgentBlock()));
35482
36646
  return {
35483
36647
  path: repoRelativePath(repoRoot, filePath),
35484
36648
  status,
@@ -35550,7 +36714,7 @@ function renderWikiRefreshWorkflow(options2 = {}) {
35550
36714
  " with:",
35551
36715
  " node-version: 22",
35552
36716
  " - name: Refresh wiki",
35553
- ' run: npx -y runbrief@0.1.2 wiki init --repo "$GITHUB_WORKSPACE" --update --no-agent-files',
36717
+ ` run: npx -y ${briefPublishedPackageInstallSpec} wiki init --repo "$GITHUB_WORKSPACE" --update --no-agent-files`,
35554
36718
  " - name: Open refresh PR",
35555
36719
  " uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11",
35556
36720
  " with:",
@@ -35571,7 +36735,7 @@ function markdownList(items) {
35571
36735
  return items.map((item) => `- ${item}`).join("\n");
35572
36736
  }
35573
36737
  function repoRelativePath(repoRoot, filePath) {
35574
- return path17.relative(repoRoot, filePath).split(path17.sep).join("/");
36738
+ return path18.relative(repoRoot, filePath).split(path18.sep).join("/");
35575
36739
  }
35576
36740
  function wikiReadmeRelativePath(repoRelativeFilePath) {
35577
36741
  return repoRelativeFilePath.replace(/^\.brief\/wiki\//, "");
@@ -35666,8 +36830,8 @@ function bestMatchingReceipt(receipts, kind, input) {
35666
36830
  function missionIntentMatches(receipt, input) {
35667
36831
  if (input.recipeId && receipt.missionRecipeId === input.recipeId) return true;
35668
36832
  if (!input.task) return false;
35669
- const wantedTask = normalizeText(input.task);
35670
- const receiptTask = normalizeText(receipt.task);
36833
+ const wantedTask = normalizeText2(input.task);
36834
+ const receiptTask = normalizeText2(receipt.task);
35671
36835
  return receiptTask === wantedTask || wantedTask.length >= 20 && receiptTask.length >= 20 && (receiptTask.includes(wantedTask) || wantedTask.includes(receiptTask));
35672
36836
  }
35673
36837
  function matchScore(receipt, input) {
@@ -35677,8 +36841,8 @@ function matchScore(receipt, input) {
35677
36841
  }
35678
36842
  let score = input.branch && receiptBranch2 ? 15 : 0;
35679
36843
  if (input.task) {
35680
- const wantedTask = normalizeText(input.task);
35681
- const receiptTask = normalizeText(receipt.task);
36844
+ const wantedTask = normalizeText2(input.task);
36845
+ const receiptTask = normalizeText2(receipt.task);
35682
36846
  if (receiptTask === wantedTask) {
35683
36847
  score += 80;
35684
36848
  } else if (wantedTask.length >= 20 && receiptTask.length >= 20 && (receiptTask.includes(wantedTask) || wantedTask.includes(receiptTask))) {
@@ -35736,7 +36900,7 @@ function cleanValues(values, limit = 300) {
35736
36900
  function unique5(values) {
35737
36901
  return [...new Set(values)];
35738
36902
  }
35739
- function normalizeText(value) {
36903
+ function normalizeText2(value) {
35740
36904
  return value.trim().toLowerCase().replace(/\s+/g, " ");
35741
36905
  }
35742
36906
  function normalizeBranch(value) {
@@ -35875,8 +37039,8 @@ var init_workflowWiringResponse = __esm({
35875
37039
 
35876
37040
  // ../mcp/src/server.ts
35877
37041
  var server_exports = {};
35878
- import { chmodSync, mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
35879
- import path18 from "node:path";
37042
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
37043
+ import path19 from "node:path";
35880
37044
  import { fileURLToPath } from "node:url";
35881
37045
  import {
35882
37046
  McpServer
@@ -36137,7 +37301,8 @@ function serverFreshness() {
36137
37301
  return mcpServerFreshness({
36138
37302
  serverStartedAt,
36139
37303
  entrypoint: serverModulePath,
36140
- mode: "server"
37304
+ mode: "server",
37305
+ distribution: connectorDistributionFor(serverModulePath)
36141
37306
  });
36142
37307
  }
36143
37308
  function summarizePacket(packet) {
@@ -36155,7 +37320,56 @@ function summarizePacket(packet) {
36155
37320
  sources: packet.sources,
36156
37321
  omitted: packet.omitted,
36157
37322
  ...packet.valueSignals ? { valueSignals: packet.valueSignals } : {},
36158
- ...packet.teamGuidance ? { teamGuidance: packet.teamGuidance } : {}
37323
+ ...packet.teamGuidance ? { teamGuidance: packet.teamGuidance } : {},
37324
+ ...packet.dependencyRemediation ? { dependencyRemediation: packet.dependencyRemediation } : {}
37325
+ };
37326
+ }
37327
+ async function buildActivationContext(input) {
37328
+ const request = {
37329
+ task: input.task,
37330
+ repoRoot: input.repoRoot,
37331
+ agent: "mcp-activation",
37332
+ maxTokens: Math.min(input.maxTokens ?? 6e3, 6e3)
37333
+ };
37334
+ if (input.files) request.files = input.files;
37335
+ if (input.branch) request.branch = input.branch;
37336
+ const packet = contextForTask(input.scan, request);
37337
+ const localReceipt = saveLocalContextReceipt(input.repoRoot, packet, {
37338
+ kind: "context",
37339
+ agent: "mcp-activation",
37340
+ ...input.files ? { files: input.files } : {},
37341
+ ...input.branch ? { branch: input.branch } : {}
37342
+ });
37343
+ const cloudSync = await syncContextReceipt(packet, input.repoRoot, {
37344
+ agent: "mcp-activation",
37345
+ ...input.files ? { files: input.files } : {},
37346
+ ...input.branch ? { branch: input.branch } : {}
37347
+ });
37348
+ return {
37349
+ status: "context_ready",
37350
+ task: input.task,
37351
+ packetId: packet.packetId,
37352
+ summary: packet.summary,
37353
+ tokenEstimate: packet.tokenEstimate,
37354
+ inspectFirst: packet.map.slice(0, 5).map((node) => ({
37355
+ path: node.path,
37356
+ kind: node.type,
37357
+ name: node.name
37358
+ })),
37359
+ rules: packet.rules.slice(0, 4).map((rule) => ({
37360
+ text: rule.body,
37361
+ sources: rule.sourcePaths
37362
+ })),
37363
+ suggestedTests: packet.suggestedTests.slice(0, 4),
37364
+ sources: packet.sources.slice(0, 6),
37365
+ localReceipt: {
37366
+ id: localReceipt.id
37367
+ },
37368
+ cloudSync: {
37369
+ status: cloudSync.status,
37370
+ ..."reason" in cloudSync && cloudSync.reason ? { reason: cloudSync.reason } : {}
37371
+ },
37372
+ next: "Use this packet before editing. If the task or files change, call brief.next again."
36159
37373
  };
36160
37374
  }
36161
37375
  function summarizeReview(review, detail = "auto") {
@@ -36593,6 +37807,7 @@ function summarizeBeforeYouCodeForStart(beforeYouCode) {
36593
37807
  decision: beforeYouCode.reuseFirst.decision,
36594
37808
  candidates: beforeYouCode.reuseFirst.candidates.slice(0, 2)
36595
37809
  },
37810
+ mapCoverage: beforeYouCode.mapCoverage,
36596
37811
  sharedContext: summarizeBeforeYouCodeSharedContext(
36597
37812
  beforeYouCode.teamGuidance
36598
37813
  ),
@@ -36656,7 +37871,7 @@ function summarizeWorkflowWiringForStart(wiring) {
36656
37871
  defaultAsk: wiring.defaultAsk,
36657
37872
  gaps: wiring.gaps.slice(0, 3),
36658
37873
  chatFeedback: wiring.chatFeedback.slice(0, 2),
36659
- finalRecapPolicy: wiring.finalRecapPolicy,
37874
+ finalRecapPolicy: compactFinalRecapPolicy(wiring.finalRecapPolicy),
36660
37875
  nextActions: wiring.nextActions.slice(0, 3)
36661
37876
  };
36662
37877
  }
@@ -36665,7 +37880,7 @@ function summarizeAutoEngageForStart(autoEngage) {
36665
37880
  status: autoEngage.status,
36666
37881
  summary: autoEngage.summary,
36667
37882
  chatFeedback: autoEngage.chatFeedback.slice(0, 1),
36668
- finalRecapPolicy: autoEngage.finalRecapPolicy,
37883
+ finalRecapPolicy: compactFinalRecapPolicy(autoEngage.finalRecapPolicy),
36669
37884
  triggers: autoEngage.triggerMatrix.slice(0, 3).map((trigger) => ({
36670
37885
  id: trigger.id,
36671
37886
  label: trigger.label,
@@ -36674,6 +37889,13 @@ function summarizeAutoEngageForStart(autoEngage) {
36674
37889
  nextActions: autoEngage.nextActions.slice(0, 2)
36675
37890
  };
36676
37891
  }
37892
+ function compactFinalRecapPolicy(policy) {
37893
+ return {
37894
+ maxLines: policy.maxLines,
37895
+ template: policy.defaultTemplate,
37896
+ fallbackWhenNotHelpful: policy.fallbackWhenNotHelpful
37897
+ };
37898
+ }
36677
37899
  function summarizeWiki(wiki) {
36678
37900
  return {
36679
37901
  wikiId: wiki.wikiId,
@@ -36726,21 +37948,22 @@ function summarizeLocalReceipt(receipt) {
36726
37948
  function writeHookFiles(repoRoot, files) {
36727
37949
  const written = [];
36728
37950
  for (const file of files) {
36729
- const targetPath = path18.join(repoRoot, file.path);
36730
- mkdirSync3(path18.dirname(targetPath), { recursive: true });
36731
- writeFileSync6(targetPath, file.body);
36732
- if (file.path.endsWith(".sh")) chmodSync(targetPath, 493);
37951
+ const targetPath = path19.join(repoRoot, file.path);
37952
+ mkdirSync4(path19.dirname(targetPath), { recursive: true });
37953
+ writeFileSync7(targetPath, file.body);
37954
+ if (file.path.endsWith(".sh")) chmodSync2(targetPath, 493);
36733
37955
  written.push(file.path);
36734
37956
  }
36735
37957
  return written;
36736
37958
  }
36737
- var server, personalizationKindSchema, referenceImplementationKindSchema, referenceImplementationStatusSchema, savePersonalizationToolSchema, workItemSchema, runtimeEvidenceSchema, handoffValidationSchema, artifactEvidenceSchema, loopCatalogFocusSchema, missionRecipeIdSchema2, startIntentSchema, nextStageSchema, responseDetailSchema, serverStartedAt, serverModulePath, repoTargetGuardExemptTools;
37959
+ var cloudBootstrap, server, personalizationKindSchema, referenceImplementationKindSchema, referenceImplementationStatusSchema, savePersonalizationToolSchema, workItemSchema, runtimeEvidenceSchema, handoffValidationSchema, artifactEvidenceSchema, loopCatalogFocusSchema, missionRecipeIdSchema2, startIntentSchema, nextStageSchema, responseDetailSchema, serverStartedAt, serverModulePath, repoTargetGuardExemptTools;
36738
37960
  var init_server = __esm({
36739
- "../mcp/src/server.ts"() {
37961
+ async "../mcp/src/server.ts"() {
36740
37962
  "use strict";
36741
37963
  init_dist();
36742
37964
  init_dist2();
36743
37965
  init_cloud();
37966
+ init_cloudCredentials();
36744
37967
  init_teamContext();
36745
37968
  init_missionInputOptions();
36746
37969
  init_connectorReadiness();
@@ -36756,10 +37979,17 @@ var init_server = __esm({
36756
37979
  init_workspace();
36757
37980
  init_handoffAutofill();
36758
37981
  init_scanFreshness();
37982
+ cloudBootstrap = process.env.BRIEF_CLOUD_BOOTSTRAP_DONE === "1" ? { status: "skipped", reason: "already_bootstrapped" } : await bootstrapCloudCredentials();
37983
+ process.env.BRIEF_CLOUD_BOOTSTRAP_DONE = "1";
37984
+ if (cloudBootstrap.status === "failed") {
37985
+ console.error(
37986
+ `Brief team sync is unavailable: ${cloudBootstrap.reason} Continuing in local mode.`
37987
+ );
37988
+ }
36759
37989
  server = new McpServer(
36760
37990
  {
36761
37991
  name: "brief",
36762
- version: "0.1.2"
37992
+ version: "0.1.4"
36763
37993
  },
36764
37994
  {
36765
37995
  instructions: briefMcpInstructions
@@ -36859,6 +38089,7 @@ var init_server = __esm({
36859
38089
  "small_refactor",
36860
38090
  "bug_investigation",
36861
38091
  "qa_edge_cases",
38092
+ "web_dogfood",
36862
38093
  "performance",
36863
38094
  "porting",
36864
38095
  "experiment",
@@ -36899,14 +38130,20 @@ var init_server = __esm({
36899
38130
  ]);
36900
38131
  registerTool(
36901
38132
  "install_status",
36902
- "First Brief call once per agent session. Confirms MCP is connected to the correct repo/worktree, then points the agent toward brief.next for the actual task.",
38133
+ "First Brief call once per agent session. Confirms MCP is connected to the correct repo/worktree, scans it, and optionally returns the first compact context packet when task is provided. Never needs a reusable API key.",
36903
38134
  {
36904
38135
  repoPath: z2.string().optional().describe(
36905
38136
  "Path inside the repo. Defaults to BRIEF_REPO_ROOT captured from the agent workspace, then current working directory."
36906
38137
  ),
36907
- detail: responseDetailSchema
38138
+ detail: responseDetailSchema,
38139
+ task: z2.string().min(4).optional().describe(
38140
+ "The user's current task, when already known. Brief returns a compact first context packet in this same call."
38141
+ ),
38142
+ files: z2.array(z2.string()).optional(),
38143
+ branch: z2.string().optional(),
38144
+ maxTokens: z2.number().int().min(500).max(12e3).optional()
36908
38145
  },
36909
- async ({ repoPath, detail }) => {
38146
+ async ({ repoPath, detail, task, files, branch, maxTokens }) => {
36910
38147
  const rootResolution = resolveRepoRootDetails(repoPath);
36911
38148
  const repoRoot = rootResolution.repoRoot;
36912
38149
  const repoTarget = repoTargetHealth(rootResolution);
@@ -36930,8 +38167,16 @@ var init_server = __esm({
36930
38167
  );
36931
38168
  }
36932
38169
  const { scan, teamContext } = await scanRepoWithTeamContext(repoRoot, {
36933
- maxFiles: 400
38170
+ maxFiles: 1200
36934
38171
  });
38172
+ const activation = task?.trim() ? await buildActivationContext({
38173
+ scan,
38174
+ repoRoot,
38175
+ task: task.trim(),
38176
+ ...files ? { files } : {},
38177
+ ...branch ? { branch } : {},
38178
+ ...maxTokens !== void 0 ? { maxTokens } : {}
38179
+ }) : void 0;
36935
38180
  const report = buildLocalValueReport(repoRoot, scan.repoName);
36936
38181
  const setups = generateAgentSetup(scan, { target: "all" });
36937
38182
  const skillLearning = buildRepoSkillLearningReport(repoRoot, {
@@ -36958,6 +38203,7 @@ var init_server = __esm({
36958
38203
  skillLearning: summarizeSkillLearningForStart(skillLearning, void 0),
36959
38204
  connectorHealth: summarizeAgentConnectorHealth(setups),
36960
38205
  proofLoop: report.proofLoop,
38206
+ ...activation ? { activation } : {},
36961
38207
  value: {
36962
38208
  receiptCount: report.receiptCount,
36963
38209
  contextPacketCount: report.contextPacketCount,
@@ -36970,8 +38216,8 @@ var init_server = __esm({
36970
38216
  next: report.proofLoop.next
36971
38217
  };
36972
38218
  if (detail === "expanded") return jsonText(fullStatus);
36973
- return jsonText(
36974
- compactInstallStatus({
38219
+ return jsonText({
38220
+ ...compactInstallStatus({
36975
38221
  mode: fullStatus.mode,
36976
38222
  repoRoot,
36977
38223
  rootResolution,
@@ -36985,8 +38231,9 @@ var init_server = __esm({
36985
38231
  workflowWiring,
36986
38232
  valueReport: report,
36987
38233
  serverInstructionsActive: true
36988
- })
36989
- );
38234
+ }),
38235
+ ...activation ? { activation } : {}
38236
+ });
36990
38237
  }
36991
38238
  );
36992
38239
  registerTool(
@@ -37214,9 +38461,12 @@ var init_server = __esm({
37214
38461
  {
37215
38462
  repoPath: z2.string().optional().describe(
37216
38463
  "Path inside the repo. Defaults to BRIEF_REPO_ROOT captured from the agent workspace, then current working directory."
38464
+ ),
38465
+ detail: z2.enum(["compact", "expanded"]).optional().describe(
38466
+ "Use compact by default for the agent loop; request expanded only when diagnosing wiring or setup."
37217
38467
  )
37218
38468
  },
37219
- async ({ repoPath }) => {
38469
+ async ({ repoPath, detail }) => {
37220
38470
  const repoTargetInfo = resolveRepoTarget(repoPath);
37221
38471
  if (repoTargetInfo.repoTarget.status === "needs_repo_target") {
37222
38472
  return repoTargetBlock(repoTargetInfo);
@@ -37226,14 +38476,19 @@ var init_server = __esm({
37226
38476
  const skillLearning = buildRepoSkillLearningReport(repoRoot, {
37227
38477
  maxFiles: 6e3
37228
38478
  });
38479
+ const autoEngage = buildAgentAutoEngagementPlan(scan, skillLearning);
38480
+ const workflowWiring = evaluateAgentWorkflowWiring(scan);
38481
+ const responseDetail = detail ?? "compact";
37229
38482
  return jsonText({
37230
38483
  repoRoot,
37231
38484
  rootResolution,
37232
38485
  repoTarget,
37233
- autoEngage: buildAgentAutoEngagementPlan(scan, skillLearning),
37234
- workflowWiring: evaluateAgentWorkflowWiring(scan),
38486
+ detail: responseDetail,
38487
+ autoEngage: responseDetail === "expanded" ? autoEngage : summarizeAutoEngageForStart(autoEngage),
38488
+ workflowWiring: responseDetail === "expanded" ? workflowWiring : summarizeWorkflowWiringForStart(workflowWiring),
37235
38489
  skillLearning: summarizeSkillLearningForStart(skillLearning, void 0),
37236
- next: "Install the default instruction into always-on agent guidance, then run the smoke test from the connected coding agent to prove Brief stays engaged from natural task asks."
38490
+ next: responseDetail === "expanded" ? "Install the default instruction into always-on agent guidance, then run the smoke test from the connected coding agent to prove Brief stays engaged from natural task asks." : "Install the default instruction, then give the connected agent a normal task and confirm it calls brief.next before editing. Request detail: expanded only to diagnose wiring.",
38491
+ detailHint: 'Call brief.auto_engage with detail: "expanded" only when diagnosing setup or workflow wiring.'
37237
38492
  });
37238
38493
  }
37239
38494
  );
@@ -37352,7 +38607,7 @@ var init_server = __esm({
37352
38607
  skills: scan.stats.skillCount
37353
38608
  }
37354
38609
  },
37355
- next: teamContext.status === "fetched" ? "Use brief.context_for_task to route this team context into the next task." : "Set BRIEF_API_URL, BRIEF_API_KEY, and BRIEF_WORKSPACE_ID to fetch approved team context from the Brief app."
38610
+ next: teamContext.status === "fetched" ? "Use brief.context_for_task to route this team context into the next task." : "Create a browser-authorized connection from the Brief app, then restart the agent. Approved legacy setups can provide BRIEF_API_URL, BRIEF_API_KEY, and BRIEF_WORKSPACE_ID."
37356
38611
  });
37357
38612
  }
37358
38613
  );
@@ -38127,7 +39382,7 @@ var init_server = __esm({
38127
39382
  const sourceRepos = sourceRepoPaths && sourceRepoPaths.length > 0 ? sourceRepoPaths : [repoRoot];
38128
39383
  const inventories = sourceRepos.map(
38129
39384
  (sourceRepo) => buildAgentPackInventory(
38130
- path18.isAbsolute(sourceRepo) ? sourceRepo : path18.resolve(repoRoot, sourceRepo),
39385
+ path19.isAbsolute(sourceRepo) ? sourceRepo : path19.resolve(repoRoot, sourceRepo),
38131
39386
  maxFiles === void 0 ? {} : { maxFiles }
38132
39387
  )
38133
39388
  );
@@ -38202,6 +39457,109 @@ var init_server = __esm({
38202
39457
  });
38203
39458
  }
38204
39459
  );
39460
+ registerTool(
39461
+ "product_research_plan",
39462
+ "Create a focused, source-safe research plan for a product, UX, content, market, competitor, or integration decision. Use the host's approved web/browser tools to investigate it, then save only cited observations with brief.save_product_context.",
39463
+ {
39464
+ task: z2.string().min(4).describe("The product decision or user job to research."),
39465
+ audience: z2.string().max(200).optional(),
39466
+ competitors: z2.array(z2.string().trim().min(1).max(120)).max(8).optional(),
39467
+ integrations: z2.array(z2.string().trim().min(1).max(120)).max(8).optional()
39468
+ },
39469
+ async ({ task, audience, competitors, integrations }) => jsonText({
39470
+ ...buildProductResearchPlan({
39471
+ task,
39472
+ ...audience ? { audience } : {},
39473
+ ...competitors ? { competitors } : {},
39474
+ ...integrations ? { integrations } : {}
39475
+ }),
39476
+ next: "Use the host's approved public web/browser research. Do not treat search snippets as product truth; save the source URL, access date, claim, implication, and confidence with brief.save_product_context."
39477
+ })
39478
+ );
39479
+ registerTool(
39480
+ "save_product_context",
39481
+ "Save cited product intelligence locally for this repo. Facts remain proposed by default; use approved only after a human product decision. Store JTBD, personas, product principles, terminology, competitor/integration observations, metrics, and decisions without credentials or private source content.",
39482
+ {
39483
+ repoPath: z2.string().optional().describe(
39484
+ "Path inside the repo. Defaults to BRIEF_REPO_ROOT captured from the agent workspace, then current working directory."
39485
+ ),
39486
+ repoName: z2.string().trim().max(160).optional(),
39487
+ facts: z2.array(
39488
+ z2.object({
39489
+ kind: z2.enum([
39490
+ "persona",
39491
+ "jtbd",
39492
+ "principle",
39493
+ "terminology",
39494
+ "competitor",
39495
+ "integration",
39496
+ "metric",
39497
+ "decision"
39498
+ ]),
39499
+ statement: z2.string().trim().min(8).max(1e3),
39500
+ appliesTo: z2.array(z2.string().trim().min(1).max(240)).max(24).optional(),
39501
+ status: z2.enum(["proposed", "approved", "stale", "rejected"]).optional(),
39502
+ confidence: z2.number().min(0).max(1).optional(),
39503
+ sources: z2.array(
39504
+ z2.object({
39505
+ kind: z2.enum(["repo", "user", "research", "feedback", "dogfood"]),
39506
+ label: z2.string().trim().min(1).max(180),
39507
+ url: z2.string().url().optional(),
39508
+ path: z2.string().trim().max(300).optional(),
39509
+ accessedAt: z2.string().datetime().optional(),
39510
+ excerpt: z2.string().trim().max(420).optional()
39511
+ })
39512
+ ).max(12).optional()
39513
+ })
39514
+ ).min(1).max(40),
39515
+ openQuestions: z2.array(z2.string().trim().min(4).max(300)).max(40).optional(),
39516
+ decisions: z2.array(z2.string().trim().min(4).max(500)).max(80).optional()
39517
+ },
39518
+ async ({
39519
+ repoPath,
39520
+ repoName,
39521
+ facts,
39522
+ openQuestions,
39523
+ decisions
39524
+ }) => {
39525
+ const repoTargetInfo = resolveRepoTarget(repoPath);
39526
+ if (repoTargetInfo.repoTarget.status === "needs_repo_target") {
39527
+ return repoTargetBlock(repoTargetInfo, { facts: facts.length });
39528
+ }
39529
+ const { repoRoot, rootResolution, repoTarget } = repoTargetInfo;
39530
+ try {
39531
+ const result = saveProductIntelligence(repoRoot, {
39532
+ ...repoName ? { repoName } : {},
39533
+ facts,
39534
+ ...openQuestions ? { openQuestions } : {},
39535
+ ...decisions ? { decisions } : {}
39536
+ });
39537
+ return jsonText({
39538
+ status: "saved_product_context",
39539
+ repoRoot,
39540
+ rootResolution,
39541
+ repoTarget,
39542
+ savedFactIds: result.savedFactIds,
39543
+ factCount: result.store.facts.length,
39544
+ sourceCount: new Set(
39545
+ result.store.facts.flatMap(
39546
+ (fact) => fact.sources.map((source) => source.id)
39547
+ )
39548
+ ).size,
39549
+ next: "Call brief.product_context for a task to see the relevant facts. Proposed facts need human review before becoming team defaults."
39550
+ });
39551
+ } catch (error) {
39552
+ return jsonText({
39553
+ status: "product_context_save_failed",
39554
+ repoRoot,
39555
+ rootResolution,
39556
+ repoTarget,
39557
+ error: error instanceof Error ? error.message : "Could not save product context.",
39558
+ next: "Fix the source citation or repository target and retry; no context was saved from this request."
39559
+ });
39560
+ }
39561
+ }
39562
+ );
38205
39563
  registerTool(
38206
39564
  "context_for_task",
38207
39565
  "Return a compact, source-grounded context packet after brief.next or brief.start identifies that scoped context is needed. Use when scope changes, files are unclear, or an agent needs rules/tests/map evidence before continuing.",
@@ -39329,6 +40687,7 @@ var init_server = __esm({
39329
40687
  "small_refactor",
39330
40688
  "bug_investigation",
39331
40689
  "qa_edge_cases",
40690
+ "web_dogfood",
39332
40691
  "performance",
39333
40692
  "porting",
39334
40693
  "experiment",
@@ -39502,6 +40861,7 @@ init_dist2();
39502
40861
  init_teamContext();
39503
40862
  init_missionInputOptions();
39504
40863
  init_cloud();
40864
+ init_cloudCredentials();
39505
40865
  init_workspace();
39506
40866
  init_connectorReadiness();
39507
40867
  init_connectorRecovery();
@@ -39513,15 +40873,140 @@ init_wikiInit();
39513
40873
  init_handoffAutofill();
39514
40874
  init_mapResponse();
39515
40875
  init_scanFreshness();
39516
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "node:fs";
39517
- import path19 from "node:path";
40876
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
40877
+ import path20 from "node:path";
39518
40878
  import { fileURLToPath as fileURLToPath2 } from "node:url";
40879
+
40880
+ // ../mcp/src/runner.ts
40881
+ init_cloud();
40882
+ import { hostname } from "node:os";
40883
+ import { spawn } from "node:child_process";
40884
+ async function runLoopRunner(options2, log = console.log) {
40885
+ const runnerId = options2.runnerId ?? `${hostname()}-${process.pid}`;
40886
+ const intervalMs = Math.max(options2.intervalMs ?? 4e3, 1e3);
40887
+ const env = options2.env ?? process.env;
40888
+ let didRun = false;
40889
+ do {
40890
+ const run = await claimRemoteLoopRun(runnerId, 180, env);
40891
+ if (!run) {
40892
+ if (options2.once) return;
40893
+ await sleep(intervalMs);
40894
+ continue;
40895
+ }
40896
+ didRun = true;
40897
+ log({ status: "running", runId: run.id, loop: run.loopName });
40898
+ try {
40899
+ const result = await executeAgentCommand(
40900
+ options2.agentCommand,
40901
+ run,
40902
+ options2.repoRoot,
40903
+ options2.commandTimeoutMs ?? 30 * 60 * 1e3
40904
+ );
40905
+ const proof = {
40906
+ runnerId,
40907
+ exitCode: result.exitCode,
40908
+ stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
40909
+ stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
40910
+ completedLocallyAt: (/* @__PURE__ */ new Date()).toISOString()
40911
+ };
40912
+ const updated = await updateRemoteLoopRun(run.id, {
40913
+ runnerId,
40914
+ status: result.exitCode === 0 ? "succeeded" : "failed",
40915
+ lastEvent: result.exitCode === 0 ? "The local agent completed the loop." : "The local agent exited with a failure.",
40916
+ resultSummary: compactOutput(result.stdout || result.stderr),
40917
+ proof,
40918
+ ...result.exitCode === 0 ? {} : { errorMessage: compactOutput(result.stderr || result.stdout) }
40919
+ }, env);
40920
+ log({ status: updated.status, runId: updated.id, proof });
40921
+ } catch (caught) {
40922
+ const message = caught instanceof Error ? caught.message : "Local runner failed.";
40923
+ try {
40924
+ const updated = await updateRemoteLoopRun(run.id, {
40925
+ runnerId,
40926
+ status: "failed",
40927
+ lastEvent: "The local runner could not execute the agent command.",
40928
+ errorMessage: message,
40929
+ proof: { runnerId, completedLocallyAt: (/* @__PURE__ */ new Date()).toISOString() }
40930
+ }, env);
40931
+ log({ status: updated.status, runId: updated.id, error: message });
40932
+ } catch (updateError) {
40933
+ log({ status: "failed", runId: run.id, error: message, updateError });
40934
+ throw new Error(
40935
+ `The runner could not persist the failed loop state: ${updateError instanceof Error ? updateError.message : "unknown update error"}`
40936
+ );
40937
+ }
40938
+ }
40939
+ } while (!options2.once || !didRun);
40940
+ }
40941
+ async function executeAgentCommand(command2, run, repoRoot, timeoutMs) {
40942
+ const prompt = [
40943
+ `You are running the approved Brief team loop: ${run.loopName}.`,
40944
+ `Run id: ${run.id}`,
40945
+ `Repository: ${run.repoFullName ?? repoRoot}`,
40946
+ "",
40947
+ "Approved loop guidance:",
40948
+ run.loopBody,
40949
+ "",
40950
+ "Guardrails:",
40951
+ ...run.guardrails.length > 0 ? run.guardrails.map((item) => `- ${item}`) : ["- Stop before external side effects without approval."],
40952
+ "",
40953
+ "Task:",
40954
+ run.task,
40955
+ "",
40956
+ "Return a concise summary of what you changed, validation run, proof, and remaining risk."
40957
+ ].join("\n");
40958
+ return await new Promise((resolve, reject) => {
40959
+ const child = spawn("/bin/sh", ["-lc", command2], {
40960
+ cwd: repoRoot,
40961
+ env: process.env,
40962
+ stdio: ["pipe", "pipe", "pipe"]
40963
+ });
40964
+ let stdout = "";
40965
+ let stderr = "";
40966
+ const timer = setTimeout(() => {
40967
+ child.kill("SIGTERM");
40968
+ reject(new Error(`Agent command timed out after ${Math.round(timeoutMs / 6e4)} minutes.`));
40969
+ }, timeoutMs);
40970
+ child.stdout.on("data", (chunk) => {
40971
+ stdout = `${stdout}${chunk.toString()}`.slice(-6e4);
40972
+ });
40973
+ child.stderr.on("data", (chunk) => {
40974
+ stderr = `${stderr}${chunk.toString()}`.slice(-2e4);
40975
+ });
40976
+ child.on("error", (error) => {
40977
+ clearTimeout(timer);
40978
+ reject(error);
40979
+ });
40980
+ child.on("close", (code) => {
40981
+ clearTimeout(timer);
40982
+ resolve({ exitCode: code ?? 1, stdout, stderr });
40983
+ });
40984
+ child.stdin.end(prompt);
40985
+ });
40986
+ }
40987
+ function compactOutput(value) {
40988
+ return value.trim().slice(-12e3) || "The agent returned no summary.";
40989
+ }
40990
+ function sleep(ms) {
40991
+ return new Promise((resolve) => setTimeout(resolve, ms));
40992
+ }
40993
+
40994
+ // ../mcp/src/cli.ts
39519
40995
  var args = process.argv.slice(2);
39520
40996
  var command = args[0];
39521
40997
  var cliStartedAt = /* @__PURE__ */ new Date();
39522
40998
  var cliModulePath = fileURLToPath2(import.meta.url);
40999
+ if (!isCloudLogoutCommand(command)) {
41000
+ const cloudBootstrap2 = await bootstrapCloudCredentials();
41001
+ process.env.BRIEF_CLOUD_BOOTSTRAP_DONE = "1";
41002
+ if (cloudBootstrap2.status === "failed") {
41003
+ console.error(
41004
+ `Brief team sync is unavailable: ${cloudBootstrap2.reason} Continuing in local mode.`
41005
+ );
41006
+ }
41007
+ }
39523
41008
  if (!command || command === "--stdio" || command === "stdio" || command === "mcp") {
39524
- await Promise.resolve().then(() => (init_server(), server_exports));
41009
+ await init_server().then(() => server_exports);
39525
41010
  } else {
39526
41011
  await runCli(command, args.slice(1));
39527
41012
  }
@@ -39530,11 +41015,41 @@ async function runCli(commandName, commandArgs) {
39530
41015
  printJson(buildCliHelp(commandName));
39531
41016
  return;
39532
41017
  }
41018
+ if (isCloudLogoutCommand(commandName)) {
41019
+ forgetCloudCredentials();
41020
+ printJson({
41021
+ status: "cloud_credentials_removed",
41022
+ path: defaultCredentialsPath(),
41023
+ next: "Create a fresh browser-authorized connection from the Brief app before using team sync again."
41024
+ });
41025
+ return;
41026
+ }
39533
41027
  const rootResolution = resolveRepoRootDetails(
39534
41028
  option2(commandArgs, "repo") ?? option2(commandArgs, "repoPath") ?? option2(commandArgs, "repo-path")
39535
41029
  );
39536
41030
  const repoRoot = rootResolution.repoRoot;
39537
41031
  const repoTarget = repoTargetHealth(rootResolution);
41032
+ if (isCommand(commandName, "runner", "loop-runner", "run-loop")) {
41033
+ const agentCommand = option2(commandArgs, "agent-command") ?? process.env.BRIEF_AGENT_COMMAND;
41034
+ if (!agentCommand) {
41035
+ fail(
41036
+ "Missing --agent-command. Set BRIEF_AGENT_COMMAND to the local Claude, Codex, or compatible agent command."
41037
+ );
41038
+ }
41039
+ const runnerOptions = {
41040
+ repoRoot,
41041
+ agentCommand,
41042
+ once: hasFlag(commandArgs, "once")
41043
+ };
41044
+ const runnerId = option2(commandArgs, "runner-id");
41045
+ const intervalMs = numberOption(commandArgs, "interval-ms");
41046
+ const commandTimeoutMs = numberOption(commandArgs, "timeout-ms");
41047
+ if (runnerId) runnerOptions.runnerId = runnerId;
41048
+ if (intervalMs) runnerOptions.intervalMs = intervalMs;
41049
+ if (commandTimeoutMs) runnerOptions.commandTimeoutMs = commandTimeoutMs;
41050
+ await runLoopRunner(runnerOptions);
41051
+ return;
41052
+ }
39538
41053
  if (isCommand(commandName, "install-status", "install_status")) {
39539
41054
  const freshness = cliServerFreshness();
39540
41055
  const connector = connectorReadiness({
@@ -39667,6 +41182,66 @@ async function runCli(commandName, commandArgs) {
39667
41182
  process.exitCode = 1;
39668
41183
  return;
39669
41184
  }
41185
+ if (isCommand(
41186
+ commandName,
41187
+ "product-research-plan",
41188
+ "product_research_plan",
41189
+ "research-product"
41190
+ )) {
41191
+ const task = option2(commandArgs, "task") ?? positionalText(commandArgs);
41192
+ if (!task) {
41193
+ fail(
41194
+ 'Missing task for product-research-plan. Example: brief product-research-plan --repo "$PWD" "Choose the right issue-triage integration"'
41195
+ );
41196
+ }
41197
+ const audience = option2(commandArgs, "audience");
41198
+ const competitors = textListOption(commandArgs, "competitor");
41199
+ const integrations = textListOption(commandArgs, "integration");
41200
+ printJson(
41201
+ buildProductResearchPlan({
41202
+ task,
41203
+ ...audience ? { audience } : {},
41204
+ ...competitors ? { competitors } : {},
41205
+ ...integrations ? { integrations } : {}
41206
+ })
41207
+ );
41208
+ return;
41209
+ }
41210
+ if (isCommand(
41211
+ commandName,
41212
+ "save-product-context",
41213
+ "save_product_context",
41214
+ "product-context-save"
41215
+ )) {
41216
+ const factsFile = option2(commandArgs, "facts-file");
41217
+ if (!factsFile) {
41218
+ fail(
41219
+ 'Missing --facts-file. Pass a JSON file with { "facts": [{ "kind", "statement", "sources" }] } so citations stay reviewable.'
41220
+ );
41221
+ }
41222
+ let parsed;
41223
+ try {
41224
+ parsed = JSON.parse(readFileSync7(path20.resolve(repoRoot, factsFile), "utf8"));
41225
+ } catch (error) {
41226
+ fail(
41227
+ `Could not read --facts-file: ${error instanceof Error ? error.message : "invalid JSON"}`
41228
+ );
41229
+ }
41230
+ if (!parsed?.facts || !Array.isArray(parsed.facts)) {
41231
+ fail('The --facts-file must contain a top-level "facts" array.');
41232
+ }
41233
+ const result = saveProductIntelligence(repoRoot, parsed);
41234
+ printJson({
41235
+ status: "saved_product_context",
41236
+ repoRoot,
41237
+ rootResolution,
41238
+ repoTarget,
41239
+ savedFactIds: result.savedFactIds,
41240
+ factCount: result.store.facts.length,
41241
+ next: "Run brief product-context for a task to receive the relevant cited product facts."
41242
+ });
41243
+ return;
41244
+ }
39670
41245
  if (isCommand(
39671
41246
  commandName,
39672
41247
  "pre-pr",
@@ -40200,7 +41775,7 @@ async function runCli(commandName, commandArgs) {
40200
41775
  const sourceRepos = filesOption2(commandArgs, "source-repos") ?? filesOption2(commandArgs, "sourceRepos") ?? filesOption2(commandArgs, "sources") ?? [repoRoot];
40201
41776
  const inventories = sourceRepos.map(
40202
41777
  (sourceRepo) => buildAgentPackInventory(
40203
- path19.isAbsolute(sourceRepo) ? sourceRepo : path19.resolve(repoRoot, sourceRepo),
41778
+ path20.isAbsolute(sourceRepo) ? sourceRepo : path20.resolve(repoRoot, sourceRepo),
40204
41779
  maxFiles === void 0 ? {} : { maxFiles }
40205
41780
  )
40206
41781
  );
@@ -41072,7 +42647,7 @@ async function runCli(commandName, commandArgs) {
41072
42647
  skills: scan.stats.skillCount
41073
42648
  }
41074
42649
  },
41075
- next: teamContext.status === "fetched" ? "Use context_for_task to route this team context into the next task." : "Set BRIEF_API_URL, BRIEF_API_KEY, and BRIEF_WORKSPACE_ID to fetch approved team context from the Brief app."
42650
+ next: teamContext.status === "fetched" ? "Use context_for_task to route this team context into the next task." : "Create a browser-authorized connection from the Brief app, then restart the agent. Approved legacy setups can provide BRIEF_API_URL, BRIEF_API_KEY, and BRIEF_WORKSPACE_ID."
41076
42651
  });
41077
42652
  return;
41078
42653
  }
@@ -41269,6 +42844,11 @@ async function runCli(commandName, commandArgs) {
41269
42844
  function isCommand(commandName, ...aliases) {
41270
42845
  return aliases.includes(commandName);
41271
42846
  }
42847
+ function isCloudLogoutCommand(commandName) {
42848
+ return Boolean(
42849
+ commandName && ["cloud-logout", "cloud_logout", "disconnect-cloud"].includes(commandName)
42850
+ );
42851
+ }
41272
42852
  function wikiInitMode(argsList) {
41273
42853
  if (hasFlag(argsList, "init")) return "init";
41274
42854
  if (hasFlag(argsList, "update")) return "update";
@@ -41352,6 +42932,33 @@ function cliCommandHelp() {
41352
42932
  "After changing Brief source to confirm the running connector is fresh."
41353
42933
  ]
41354
42934
  },
42935
+ {
42936
+ command: "cloud-logout",
42937
+ aliases: ["cloud_logout", "disconnect-cloud"],
42938
+ purpose: "Remove the locally stored cloud credential so the next setup can pair a device again.",
42939
+ repoBound: false,
42940
+ usage: "brief cloud-logout",
42941
+ examples: ["brief cloud-logout"],
42942
+ whenToUse: [
42943
+ "When a device needs to be disconnected from a Brief workspace.",
42944
+ "When a one-time browser pairing has expired before the connector started."
42945
+ ]
42946
+ },
42947
+ {
42948
+ command: "runner",
42949
+ aliases: ["loop-runner", "run-loop"],
42950
+ purpose: "Poll for an approved team loop and execute it with the developer's local agent command.",
42951
+ repoBound: true,
42952
+ usage: 'runbrief runner --repo $PWD --agent-command "claude -p"',
42953
+ examples: [
42954
+ 'runbrief runner --repo $PWD --agent-command "claude -p"',
42955
+ 'runbrief runner --repo $PWD --agent-command "codex exec" --once'
42956
+ ],
42957
+ whenToUse: [
42958
+ "After pairing a connector from the Brief app.",
42959
+ "When a team-approved loop is queued in the web app."
42960
+ ]
42961
+ },
41355
42962
  {
41356
42963
  command: "next",
41357
42964
  aliases: ["brief-next", "brief_next"],
@@ -41438,6 +43045,36 @@ function cliCommandHelp() {
41438
43045
  "When the agent needs acceptance criteria and state checks before editing."
41439
43046
  ]
41440
43047
  },
43048
+ {
43049
+ command: "product-research-plan",
43050
+ aliases: ["product_research_plan", "research-product"],
43051
+ purpose: "Turn a product, user, competitor, taste, or integration question into a focused research plan with source and approval rules.",
43052
+ repoBound: true,
43053
+ usage: 'brief product-research-plan --repo $PWD "Choose the right issue-triage integration"',
43054
+ examples: [
43055
+ 'brief product-research-plan --repo $PWD "Improve onboarding copy for coding agents"',
43056
+ 'brief product-research-plan --repo $PWD --competitor Cursor --competitor Factory "Improve agent context"'
43057
+ ],
43058
+ whenToUse: [
43059
+ "When a PM, designer, or agent needs current market, competitor, integration, or user evidence.",
43060
+ "Before turning external research into a product rule, skill, style, or copy decision."
43061
+ ]
43062
+ },
43063
+ {
43064
+ command: "save-product-context",
43065
+ aliases: ["save_product_context", "product-context-save"],
43066
+ purpose: "Save cited product intelligence locally so future product, design, copy, and integration work receives relevant context.",
43067
+ repoBound: true,
43068
+ usage: "brief save-product-context --repo $PWD --facts-file /tmp/brief-product-facts.json",
43069
+ examples: [
43070
+ "brief save-product-context --repo $PWD --facts-file ./product-facts.json"
43071
+ ],
43072
+ whenToUse: [
43073
+ "After researching with an approved browser or web source.",
43074
+ "After a user, dogfood session, or product decision produces a reusable fact.",
43075
+ "Facts remain proposed by default until a human approves them."
43076
+ ]
43077
+ },
41441
43078
  {
41442
43079
  command: "map",
41443
43080
  aliases: ["codebase-map", "codebase_map"],
@@ -41696,8 +43333,21 @@ function positionalText(argsList) {
41696
43333
  "doNotCopy",
41697
43334
  "owner",
41698
43335
  "receipts",
43336
+ "audience",
43337
+ "competitor",
43338
+ "competitors",
43339
+ "integration",
43340
+ "integrations",
43341
+ "facts-file",
43342
+ "repo-name",
43343
+ "open-questions",
43344
+ "decisions",
41699
43345
  "added-by",
41700
- "addedBy"
43346
+ "addedBy",
43347
+ "agent-command",
43348
+ "runner-id",
43349
+ "interval-ms",
43350
+ "timeout-ms"
41701
43351
  ]);
41702
43352
  const parts = [];
41703
43353
  for (let index = 0; index < argsList.length; index += 1) {
@@ -41814,6 +43464,7 @@ function recipeOption(argsList) {
41814
43464
  "small_refactor",
41815
43465
  "bug_investigation",
41816
43466
  "qa_edge_cases",
43467
+ "web_dogfood",
41817
43468
  "performance",
41818
43469
  "porting",
41819
43470
  "experiment",
@@ -41953,7 +43604,7 @@ function artifactEvidenceFromOptions(argsList) {
41953
43604
  );
41954
43605
  const evidence = [];
41955
43606
  for (const raw of rawEvidenceValues) {
41956
- const json = raw.startsWith("@") ? readFileSync6(path19.resolve(raw.slice(1)), "utf8") : raw;
43607
+ const json = raw.startsWith("@") ? readFileSync7(path20.resolve(raw.slice(1)), "utf8") : raw;
41957
43608
  try {
41958
43609
  const parsed = JSON.parse(json);
41959
43610
  const items = Array.isArray(parsed) ? parsed : [parsed];
@@ -42031,7 +43682,7 @@ function missionSummaryFromLocalReceipt2(repoRoot, missionReceiptId) {
42031
43682
  function runtimeEvidencePacketFromOptions(argsList) {
42032
43683
  const raw = option2(argsList, "runtime-packet") ?? option2(argsList, "runtime-evidence-packet") ?? option2(argsList, "runtimeEvidencePacket");
42033
43684
  if (!raw) return void 0;
42034
- const json = raw.startsWith("@") ? readFileSync6(path19.resolve(raw.slice(1)), "utf8") : raw;
43685
+ const json = raw.startsWith("@") ? readFileSync7(path20.resolve(raw.slice(1)), "utf8") : raw;
42035
43686
  try {
42036
43687
  return cleanRuntimeEvidencePacket2(
42037
43688
  runtimeEvidencePacketSchema.parse(JSON.parse(json))
@@ -42200,6 +43851,15 @@ function printContextSummary(packet, localReceipt, teamContext) {
42200
43851
  teamContextLine(teamContext),
42201
43852
  teamGuidanceLine(packet),
42202
43853
  valueSignalLine(packet),
43854
+ ...packet.dependencyRemediation ? [
43855
+ "",
43856
+ "Dependency plan:",
43857
+ `- ${packet.dependencyRemediation.packageManager} \xB7 ${packet.dependencyRemediation.status}`,
43858
+ `- manifests: ${packet.dependencyRemediation.manifestFiles.slice(0, 4).join(", ") || "none found"}`,
43859
+ `- lockfiles: ${packet.dependencyRemediation.lockfiles.slice(0, 3).join(", ") || "none found"}`,
43860
+ `- packages: ${packet.dependencyRemediation.packageSignals.map((item) => item.name).join(", ") || "need advisory/scanner evidence"}`,
43861
+ `- graph proof: ${packet.dependencyRemediation.graphCommands.slice(0, 2).join(", ") || "name the direct/transitive path"}`
43862
+ ] : [],
42203
43863
  "",
42204
43864
  "Rules:",
42205
43865
  ...packet.rules.length > 0 ? packet.rules.slice(0, 8).map((rule) => `- [${rule.enforcement}] ${rule.title}`) : ["- none"],
@@ -42252,10 +43912,10 @@ function valueSignalLine(packet) {
42252
43912
  function writeHookFiles2(repoRoot, files) {
42253
43913
  const written = [];
42254
43914
  for (const file of files) {
42255
- const targetPath = path19.join(repoRoot, file.path);
42256
- mkdirSync4(path19.dirname(targetPath), { recursive: true });
42257
- writeFileSync7(targetPath, file.body);
42258
- if (file.path.endsWith(".sh")) chmodSync2(targetPath, 493);
43915
+ const targetPath = path20.join(repoRoot, file.path);
43916
+ mkdirSync5(path20.dirname(targetPath), { recursive: true });
43917
+ writeFileSync8(targetPath, file.body);
43918
+ if (file.path.endsWith(".sh")) chmodSync3(targetPath, 493);
42259
43919
  written.push(file.path);
42260
43920
  }
42261
43921
  return written;
@@ -42383,7 +44043,8 @@ function cliServerFreshness() {
42383
44043
  return mcpServerFreshness({
42384
44044
  serverStartedAt: cliStartedAt,
42385
44045
  entrypoint: cliModulePath,
42386
- mode: "cli"
44046
+ mode: "cli",
44047
+ distribution: connectorDistributionFor(cliModulePath)
42387
44048
  });
42388
44049
  }
42389
44050
  function printJson(value) {
@@ -42407,7 +44068,7 @@ function nextScanStep(cloudSync) {
42407
44068
  if (cloudSync.status === "failed") {
42408
44069
  return `The local scan succeeded, but the web app did not update because cloud sync failed: ${cloudSync.reason}. Fix cloud sync before trusting hosted freshness; refresh local wiki separately with brief.initialize_repo_wiki update: true.`;
42409
44070
  }
42410
- return "The local scan succeeded, but the web app did not update. Set BRIEF_API_URL, BRIEF_API_KEY, BRIEF_WORKSPACE_ID, and BRIEF_PRIVACY_MODE=cloud-sync-derived to sync scans to Brief. To refresh repo-local wiki docs, ask the agent to call brief.initialize_repo_wiki with update: true.";
44071
+ return "The local scan succeeded, but the web app did not update. Create a browser-authorized connection from the Brief app and restart the agent; approved legacy setups can provide BRIEF_API_URL, BRIEF_API_KEY, and BRIEF_WORKSPACE_ID. To refresh repo-local wiki docs, ask the agent to call brief.initialize_repo_wiki with update: true.";
42411
44072
  }
42412
44073
  function fail(message) {
42413
44074
  process.stderr.write(`${message}