youmd 0.10.2 → 0.12.0

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 (205) hide show
  1. package/dist/commands/brain.d.ts.map +1 -1
  2. package/dist/commands/brain.js +91 -28
  3. package/dist/commands/brain.js.map +1 -1
  4. package/dist/commands/chat.d.ts.map +1 -1
  5. package/dist/commands/chat.js +6 -1
  6. package/dist/commands/chat.js.map +1 -1
  7. package/dist/commands/conversations.d.ts +5 -0
  8. package/dist/commands/conversations.d.ts.map +1 -0
  9. package/dist/commands/conversations.js +116 -0
  10. package/dist/commands/conversations.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +1 -0
  12. package/dist/commands/doctor.d.ts.map +1 -1
  13. package/dist/commands/doctor.js +16 -1
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/founder.d.ts +13 -0
  16. package/dist/commands/founder.d.ts.map +1 -0
  17. package/dist/commands/founder.js +189 -0
  18. package/dist/commands/founder.js.map +1 -0
  19. package/dist/commands/handoff.d.ts +5 -0
  20. package/dist/commands/handoff.d.ts.map +1 -0
  21. package/dist/commands/handoff.js +432 -0
  22. package/dist/commands/handoff.js.map +1 -0
  23. package/dist/commands/orchestrate.d.ts +15 -0
  24. package/dist/commands/orchestrate.d.ts.map +1 -1
  25. package/dist/commands/orchestrate.js +294 -0
  26. package/dist/commands/orchestrate.js.map +1 -1
  27. package/dist/commands/project.d.ts.map +1 -1
  28. package/dist/commands/project.js +8 -1
  29. package/dist/commands/project.js.map +1 -1
  30. package/dist/commands/reconcile.d.ts +15 -0
  31. package/dist/commands/reconcile.d.ts.map +1 -0
  32. package/dist/commands/reconcile.js +260 -0
  33. package/dist/commands/reconcile.js.map +1 -0
  34. package/dist/commands/session.d.ts +28 -0
  35. package/dist/commands/session.d.ts.map +1 -0
  36. package/dist/commands/session.js +895 -0
  37. package/dist/commands/session.js.map +1 -0
  38. package/dist/commands/stack.d.ts +2 -0
  39. package/dist/commands/stack.d.ts.map +1 -1
  40. package/dist/commands/stack.js +26 -0
  41. package/dist/commands/stack.js.map +1 -1
  42. package/dist/commands/sync.d.ts.map +1 -1
  43. package/dist/commands/sync.js +11 -0
  44. package/dist/commands/sync.js.map +1 -1
  45. package/dist/commands/tasks.d.ts +57 -0
  46. package/dist/commands/tasks.d.ts.map +1 -1
  47. package/dist/commands/tasks.js +236 -31
  48. package/dist/commands/tasks.js.map +1 -1
  49. package/dist/generated/capability-registry.d.ts +311 -71
  50. package/dist/generated/capability-registry.d.ts.map +1 -1
  51. package/dist/generated/capability-registry.js +346 -71
  52. package/dist/generated/capability-registry.js.map +1 -1
  53. package/dist/index.js +113 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/api.d.ts +362 -1
  56. package/dist/lib/api.d.ts.map +1 -1
  57. package/dist/lib/api.js +171 -0
  58. package/dist/lib/api.js.map +1 -1
  59. package/dist/lib/brain/adapters.d.ts +20 -1
  60. package/dist/lib/brain/adapters.d.ts.map +1 -1
  61. package/dist/lib/brain/adapters.js +113 -2
  62. package/dist/lib/brain/adapters.js.map +1 -1
  63. package/dist/lib/brain/approval-key-trust.d.ts +29 -0
  64. package/dist/lib/brain/approval-key-trust.d.ts.map +1 -0
  65. package/dist/lib/brain/approval-key-trust.js +139 -0
  66. package/dist/lib/brain/approval-key-trust.js.map +1 -0
  67. package/dist/lib/brain/cache.d.ts +2 -2
  68. package/dist/lib/brain/cache.d.ts.map +1 -1
  69. package/dist/lib/brain/cache.js +4 -4
  70. package/dist/lib/brain/cache.js.map +1 -1
  71. package/dist/lib/brain/content.d.ts +56 -0
  72. package/dist/lib/brain/content.d.ts.map +1 -0
  73. package/dist/lib/brain/content.js +362 -0
  74. package/dist/lib/brain/content.js.map +1 -0
  75. package/dist/lib/brain/descriptor-validator.d.ts +11 -0
  76. package/dist/lib/brain/descriptor-validator.d.ts.map +1 -0
  77. package/dist/lib/brain/descriptor-validator.js +185 -0
  78. package/dist/lib/brain/descriptor-validator.js.map +1 -0
  79. package/dist/lib/brain/discovery.d.ts.map +1 -1
  80. package/dist/lib/brain/discovery.js +103 -60
  81. package/dist/lib/brain/discovery.js.map +1 -1
  82. package/dist/lib/brain/index.d.ts +2 -0
  83. package/dist/lib/brain/index.d.ts.map +1 -1
  84. package/dist/lib/brain/index.js +8 -2
  85. package/dist/lib/brain/index.js.map +1 -1
  86. package/dist/lib/brain/proposal.d.ts +53 -5
  87. package/dist/lib/brain/proposal.d.ts.map +1 -1
  88. package/dist/lib/brain/proposal.js +341 -64
  89. package/dist/lib/brain/proposal.js.map +1 -1
  90. package/dist/lib/brain/safe-file.d.ts +25 -1
  91. package/dist/lib/brain/safe-file.d.ts.map +1 -1
  92. package/dist/lib/brain/safe-file.js +250 -30
  93. package/dist/lib/brain/safe-file.js.map +1 -1
  94. package/dist/lib/brain/safe-open.d.ts +2 -0
  95. package/dist/lib/brain/safe-open.d.ts.map +1 -0
  96. package/dist/lib/brain/safe-open.js +77 -0
  97. package/dist/lib/brain/safe-open.js.map +1 -0
  98. package/dist/lib/brain/safety.d.ts.map +1 -1
  99. package/dist/lib/brain/safety.js +5 -0
  100. package/dist/lib/brain/safety.js.map +1 -1
  101. package/dist/lib/brain/staged-apply.d.ts +2 -1
  102. package/dist/lib/brain/staged-apply.d.ts.map +1 -1
  103. package/dist/lib/brain/staged-apply.js +6 -3
  104. package/dist/lib/brain/staged-apply.js.map +1 -1
  105. package/dist/lib/brain/store.d.ts +3 -0
  106. package/dist/lib/brain/store.d.ts.map +1 -1
  107. package/dist/lib/brain/store.js +68 -0
  108. package/dist/lib/brain/store.js.map +1 -1
  109. package/dist/lib/brain/sync.d.ts +87 -0
  110. package/dist/lib/brain/sync.d.ts.map +1 -0
  111. package/dist/lib/brain/sync.js +238 -0
  112. package/dist/lib/brain/sync.js.map +1 -0
  113. package/dist/lib/brain/types.d.ts +23 -0
  114. package/dist/lib/brain/types.d.ts.map +1 -1
  115. package/dist/lib/config.d.ts +7 -0
  116. package/dist/lib/config.d.ts.map +1 -1
  117. package/dist/lib/config.js +3 -0
  118. package/dist/lib/config.js.map +1 -1
  119. package/dist/lib/daemon.js +1 -1
  120. package/dist/lib/daemon.js.map +1 -1
  121. package/dist/lib/goalRegistryFile.d.ts +169 -0
  122. package/dist/lib/goalRegistryFile.d.ts.map +1 -0
  123. package/dist/lib/goalRegistryFile.js +518 -0
  124. package/dist/lib/goalRegistryFile.js.map +1 -0
  125. package/dist/lib/goalsFile.d.ts +101 -0
  126. package/dist/lib/goalsFile.d.ts.map +1 -0
  127. package/dist/lib/goalsFile.js +413 -0
  128. package/dist/lib/goalsFile.js.map +1 -0
  129. package/dist/lib/handoff.d.ts +170 -0
  130. package/dist/lib/handoff.d.ts.map +1 -0
  131. package/dist/lib/handoff.js +316 -0
  132. package/dist/lib/handoff.js.map +1 -0
  133. package/dist/lib/machine-identity.d.ts +2 -0
  134. package/dist/lib/machine-identity.d.ts.map +1 -1
  135. package/dist/lib/machine-identity.js +13 -6
  136. package/dist/lib/machine-identity.js.map +1 -1
  137. package/dist/lib/onboarding.d.ts +4 -1
  138. package/dist/lib/onboarding.d.ts.map +1 -1
  139. package/dist/lib/onboarding.js +11 -3
  140. package/dist/lib/onboarding.js.map +1 -1
  141. package/dist/lib/orchestrator/loop.d.ts +7 -0
  142. package/dist/lib/orchestrator/loop.d.ts.map +1 -1
  143. package/dist/lib/orchestrator/loop.js +53 -0
  144. package/dist/lib/orchestrator/loop.js.map +1 -1
  145. package/dist/lib/orchestrator/run-journal.d.ts +178 -0
  146. package/dist/lib/orchestrator/run-journal.d.ts.map +1 -0
  147. package/dist/lib/orchestrator/run-journal.js +912 -0
  148. package/dist/lib/orchestrator/run-journal.js.map +1 -0
  149. package/dist/lib/orchestrator/supervisor.d.ts +11 -0
  150. package/dist/lib/orchestrator/supervisor.d.ts.map +1 -1
  151. package/dist/lib/orchestrator/supervisor.js +304 -116
  152. package/dist/lib/orchestrator/supervisor.js.map +1 -1
  153. package/dist/lib/orchestrator/tools.d.ts +2 -0
  154. package/dist/lib/orchestrator/tools.d.ts.map +1 -1
  155. package/dist/lib/orchestrator/tools.js +214 -1
  156. package/dist/lib/orchestrator/tools.js.map +1 -1
  157. package/dist/lib/redact.d.ts.map +1 -1
  158. package/dist/lib/redact.js +31 -6
  159. package/dist/lib/redact.js.map +1 -1
  160. package/dist/lib/remote-agent-event.d.ts +19 -0
  161. package/dist/lib/remote-agent-event.d.ts.map +1 -0
  162. package/dist/lib/remote-agent-event.js +85 -0
  163. package/dist/lib/remote-agent-event.js.map +1 -0
  164. package/dist/lib/remote-command.d.ts.map +1 -1
  165. package/dist/lib/remote-command.js +19 -0
  166. package/dist/lib/remote-command.js.map +1 -1
  167. package/dist/lib/remote-executor.d.ts +7 -1
  168. package/dist/lib/remote-executor.d.ts.map +1 -1
  169. package/dist/lib/remote-executor.js +391 -0
  170. package/dist/lib/remote-executor.js.map +1 -1
  171. package/dist/lib/sessionCheckpoint.d.ts +104 -0
  172. package/dist/lib/sessionCheckpoint.d.ts.map +1 -0
  173. package/dist/lib/sessionCheckpoint.js +191 -0
  174. package/dist/lib/sessionCheckpoint.js.map +1 -0
  175. package/dist/lib/sessionHandoff.d.ts +47 -0
  176. package/dist/lib/sessionHandoff.d.ts.map +1 -0
  177. package/dist/lib/sessionHandoff.js +305 -0
  178. package/dist/lib/sessionHandoff.js.map +1 -0
  179. package/dist/lib/stream.d.ts +3 -1
  180. package/dist/lib/stream.d.ts.map +1 -1
  181. package/dist/lib/stream.js +8 -3
  182. package/dist/lib/stream.js.map +1 -1
  183. package/dist/lib/taskLedgerProposal.d.ts +102 -0
  184. package/dist/lib/taskLedgerProposal.d.ts.map +1 -0
  185. package/dist/lib/taskLedgerProposal.js +408 -0
  186. package/dist/lib/taskLedgerProposal.js.map +1 -0
  187. package/dist/lib/tasksFile.d.ts +23 -2
  188. package/dist/lib/tasksFile.d.ts.map +1 -1
  189. package/dist/lib/tasksFile.js +122 -10
  190. package/dist/lib/tasksFile.js.map +1 -1
  191. package/dist/mcp/registry.d.ts.map +1 -1
  192. package/dist/mcp/registry.js +110 -0
  193. package/dist/mcp/registry.js.map +1 -1
  194. package/native/prebuilds/darwin-universal/youmd_safe_open.node +0 -0
  195. package/native/safe-open/youmd_safe_open.c +224 -0
  196. package/package.json +11 -3
  197. package/scripts/build-safe-open.mjs +63 -0
  198. package/scripts/skillstack-sync/com.you.context-sync.plist +1 -1
  199. package/scripts/skillstack-sync/com.you.identity-sync.plist +1 -1
  200. package/scripts/skillstack-sync/com.you.orchestrator-watch.plist +1 -1
  201. package/scripts/skillstack-sync/com.you.realtime-sync.plist +7 -1
  202. package/scripts/skillstack-sync/com.you.skillstack-sync.plist +1 -1
  203. package/scripts/skillstack-sync/context-sync.sh +19 -7
  204. package/scripts/skillstack-sync/install-daemons-linux.sh +6 -0
  205. package/scripts/smoke-safe-open-node.cjs +50 -0
@@ -1,20 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PROJECT_BRAIN_PROPOSAL_VERSION = exports.PROJECT_BRAIN_PROPOSAL_SCHEMA = exports.PROJECT_BRAIN_OVERRIDE_VERSION = void 0;
3
+ exports.PROJECT_BRAIN_TASK_MUTATION_VERSION = exports.PROJECT_BRAIN_PROPOSAL_VERSION = exports.PROJECT_BRAIN_PROPOSAL_SCHEMA = exports.PROJECT_BRAIN_OVERRIDE_VERSION = void 0;
4
4
  exports.validateProjectBrainPatch = validateProjectBrainPatch;
5
5
  exports.validateProjectBrainUnifiedDiffPatch = validateProjectBrainUnifiedDiffPatch;
6
6
  exports.validateProjectBrainProposal = validateProjectBrainProposal;
7
+ exports.validateProjectBrainAdoptionDescriptor = validateProjectBrainAdoptionDescriptor;
8
+ exports.proposeProjectBrainAdoptions = proposeProjectBrainAdoptions;
7
9
  exports.proposeProjectBrainAdoption = proposeProjectBrainAdoption;
8
10
  exports.importProjectBrainOverrides = importProjectBrainOverrides;
9
11
  const redact_1 = require("../redact");
10
12
  const canonical_1 = require("./canonical");
13
+ const descriptor_validator_1 = require("./descriptor-validator");
11
14
  exports.PROJECT_BRAIN_OVERRIDE_VERSION = "you-md/project-brain-override/v1";
12
15
  exports.PROJECT_BRAIN_PROPOSAL_SCHEMA = "https://you.md/schemas/project-brain-proposal/v1";
13
16
  exports.PROJECT_BRAIN_PROPOSAL_VERSION = "you-md/project-brain-proposal/v1";
17
+ exports.PROJECT_BRAIN_TASK_MUTATION_VERSION = "you-md/task-mutation-proposal/v1";
14
18
  const PROJECT_REF_PATTERN = /^project:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
15
19
  const REVISION_PATTERN = /^sha256:[a-f0-9]{64}$/;
16
20
  const OVERRIDE_SOURCE_PATTERN = /^(?:overrides|notes)\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\.json$/;
17
21
  const ARTIFACT_REF_PATTERN = /^artifact:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/[a-z0-9][a-z0-9._-]{0,95}$/;
22
+ const SAFE_RELATIVE_PATH = /^(?!\/)(?![A-Za-z]:)(?!.*\\)(?!.*\/\/)(?!.*(?:^|\/)(?:\.{1,2})(?:\/|$))(?!.*[\u0000-\u001f\u007f])(?!.*\/$)[^/]+(?:\/[^/]+)*$/;
18
23
  const FORBIDDEN_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
19
24
  const MAX_PATCH_OPERATIONS = 100;
20
25
  const MAX_PATCH_BYTES = 256 * 1024;
@@ -29,6 +34,22 @@ const APPROVAL_BINDINGS = [
29
34
  "patchFormat",
30
35
  "patchDigest",
31
36
  ];
37
+ const TASK_APPROVAL_BINDINGS = [
38
+ ...APPROVAL_BINDINGS,
39
+ "taskMutation.mutationRef",
40
+ ];
41
+ const TASK_MUTATION_REF_PATTERN = /^task-mutation:sha256:[a-f0-9]{64}$/;
42
+ const TASK_ID_PATTERN = /^t-[A-Za-z0-9][A-Za-z0-9_-]{0,94}$/;
43
+ const EXACT_ISO_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
44
+ const TASK_STATUSES = new Set(["proposed", "open", "in_progress", "done", "snoozed", "cancelled"]);
45
+ const TASK_PRIORITIES = new Set(["low", "normal", "high", "urgent"]);
46
+ const TASK_TAG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
47
+ const TASK_TEXT_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
48
+ const TASK_CREATE_RICH_KEYS = [
49
+ "requestedDescription", "requestedGoal", "requestedSuccessCriteria", "requestedParentId",
50
+ "requestedDependencyIds", "requestedScope", "requestedBlockers", "requestedClarification",
51
+ "requestedReview", "requestedEvidence", "requestedTags", "requestedVerify", "requestedOwnerLabel",
52
+ ];
32
53
  function proposalError(sourceRef, code, message, operationIndex) {
33
54
  return operationIndex === undefined ? { sourceRef, code, message } : { sourceRef, code, message, operationIndex };
34
55
  }
@@ -353,6 +374,77 @@ function validateProjectBrainUnifiedDiffPatch(raw, sourceRef = "proposal:input")
353
374
  return { valid: false, errors: [proposalError(sourceRef, "secret_detected", "unified diff contains a secret or private-data pattern")] };
354
375
  return { valid: true, errors: [], patch, patchDigest: digest(normalizeJson(patch)) };
355
376
  }
377
+ function validTaskMutationText(value, max) {
378
+ return value === undefined || typeof value === "string" && Boolean(value.trim()) && value === value.trim()
379
+ && value.length <= max && !TASK_TEXT_CONTROL_PATTERN.test(value);
380
+ }
381
+ function validTaskMutationList(value, maxItems, maxText, pattern) {
382
+ return value === undefined || Array.isArray(value) && value.length <= maxItems
383
+ && new Set(value).size === value.length
384
+ && value.every((item) => validTaskMutationText(item, maxText)
385
+ && typeof item === "string" && (!pattern || pattern.test(item)));
386
+ }
387
+ function validateTaskMutation(value, baseContentHash, patchDigest) {
388
+ if (!value || typeof value !== "object" || Array.isArray(value)
389
+ || !REVISION_PATTERN.test(baseContentHash || "") || !REVISION_PATTERN.test(patchDigest || ""))
390
+ return false;
391
+ const mutation = value;
392
+ const commonValid = mutation.schemaVersion === exports.PROJECT_BRAIN_TASK_MUTATION_VERSION
393
+ && typeof mutation.mutationRef === "string" && TASK_MUTATION_REF_PATTERN.test(mutation.mutationRef)
394
+ && typeof mutation.localTaskId === "string" && TASK_ID_PATTERN.test(mutation.localTaskId)
395
+ && typeof mutation.requestedAt === "string" && EXACT_ISO_PATTERN.test(mutation.requestedAt)
396
+ && Number.isFinite(Date.parse(mutation.requestedAt));
397
+ if (!commonValid)
398
+ return false;
399
+ const commonKeys = ["schemaVersion", "mutationRef", "operation", "localTaskId", "requestedStatus", "requestedAt"];
400
+ if (mutation.operation === "create") {
401
+ const requiredKeys = [...commonKeys, "requestedTitle", "requestedOwner", "requestedPriority"];
402
+ const allowedKeys = new Set([...requiredKeys, ...TASK_CREATE_RICH_KEYS]);
403
+ if (requiredKeys.some((key) => !Object.prototype.hasOwnProperty.call(mutation, key))
404
+ || Object.keys(mutation).some((key) => !allowedKeys.has(key))
405
+ || typeof mutation.requestedTitle !== "string" || !mutation.requestedTitle.trim()
406
+ || mutation.requestedTitle !== mutation.requestedTitle.trim() || mutation.requestedTitle.length > 240
407
+ || TASK_TEXT_CONTROL_PATTERN.test(mutation.requestedTitle) || mutation.requestedStatus !== "open"
408
+ || (mutation.requestedOwner !== "human" && mutation.requestedOwner !== "agent")
409
+ || typeof mutation.requestedPriority !== "string" || !TASK_PRIORITIES.has(mutation.requestedPriority)
410
+ || !validTaskMutationText(mutation.requestedDescription, 10000)
411
+ || !validTaskMutationText(mutation.requestedGoal, 10000)
412
+ || !validTaskMutationText(mutation.requestedSuccessCriteria, 10000)
413
+ || !validTaskMutationText(mutation.requestedParentId, 95)
414
+ || (mutation.requestedParentId !== undefined
415
+ && (typeof mutation.requestedParentId !== "string" || !TASK_ID_PATTERN.test(mutation.requestedParentId)))
416
+ || !validTaskMutationList(mutation.requestedDependencyIds, 256, 95, TASK_ID_PATTERN)
417
+ || !validTaskMutationText(mutation.requestedScope, 4000)
418
+ || !validTaskMutationList(mutation.requestedBlockers, 128, 2000)
419
+ || !validTaskMutationText(mutation.requestedClarification, 10000)
420
+ || !validTaskMutationText(mutation.requestedReview, 10000)
421
+ || !validTaskMutationList(mutation.requestedEvidence, 256, 4000)
422
+ || !validTaskMutationList(mutation.requestedTags, 32, 64, TASK_TAG_PATTERN)
423
+ || !validTaskMutationText(mutation.requestedVerify, 2000)
424
+ || !validTaskMutationText(mutation.requestedOwnerLabel, 120)
425
+ || mutation.requestedParentId === mutation.localTaskId
426
+ || (Array.isArray(mutation.requestedDependencyIds)
427
+ && mutation.requestedDependencyIds.includes(mutation.localTaskId)))
428
+ return false;
429
+ }
430
+ else if (mutation.operation === "update_status") {
431
+ const keys = new Set([...commonKeys, "portfolioTaskId", "expectedPortfolioUpdatedAt"]);
432
+ if (Object.keys(mutation).length !== keys.size || Object.keys(mutation).some((key) => !keys.has(key))
433
+ || typeof mutation.portfolioTaskId !== "string" || !mutation.portfolioTaskId
434
+ || mutation.portfolioTaskId.length > 160 || /[\u0000-\u001f\u007f]/.test(mutation.portfolioTaskId)
435
+ || typeof mutation.expectedPortfolioUpdatedAt !== "number" || !Number.isFinite(mutation.expectedPortfolioUpdatedAt)
436
+ || typeof mutation.requestedStatus !== "string" || !TASK_STATUSES.has(mutation.requestedStatus))
437
+ return false;
438
+ }
439
+ else
440
+ return false;
441
+ const { mutationRef: _mutationRef, ...bindingWithoutRef } = mutation;
442
+ return mutation.mutationRef === `task-mutation:${digest(normalizeJson({
443
+ ...bindingWithoutRef,
444
+ baseContentHash,
445
+ patchDigest,
446
+ }))}`;
447
+ }
356
448
  /** Validate a transport proposal envelope without approving or applying it. */
357
449
  function validateProjectBrainProposal(value) {
358
450
  const sourceRef = value && typeof value === "object" && typeof value.proposalRef === "string"
@@ -371,11 +463,14 @@ function validateProjectBrainProposal(value) {
371
463
  const proposal = normalized;
372
464
  const record = normalized;
373
465
  const errors = [];
466
+ const hasTaskMutation = Object.prototype.hasOwnProperty.call(record, "taskMutation");
374
467
  const requiredKeys = new Set([
375
468
  "$schema", "schemaVersion", "kind", "proposalRef", "proposalType", "projectRef", "machineRef", "workspaceRef",
376
469
  "baseRevision", "requestedCapability", "rationale", "status", "target", "patchFormat", "patchDigest", "patch", "diff",
377
470
  "approval", "source",
378
471
  ]);
472
+ if (hasTaskMutation)
473
+ requiredKeys.add("taskMutation");
379
474
  if (Object.keys(record).length !== requiredKeys.size || Object.keys(record).some((key) => !requiredKeys.has(key))
380
475
  || proposal.$schema !== exports.PROJECT_BRAIN_PROPOSAL_SCHEMA || proposal.schemaVersion !== exports.PROJECT_BRAIN_PROPOSAL_VERSION
381
476
  || proposal.kind !== "project-brain-proposal" || proposal.status !== "proposed"
@@ -384,7 +479,7 @@ function validateProjectBrainProposal(value) {
384
479
  || typeof proposal.rationale !== "string" || proposal.rationale.trim().length < 3 || proposal.rationale.length > 1000
385
480
  || !proposal.target || proposal.target.humanOwned !== true || !ARTIFACT_REF_PATTERN.test(proposal.target.artifactRef)
386
481
  || !REVISION_PATTERN.test(proposal.target.baseContentHash) || !proposal.source
387
- || !/^(?:sha256|heuristic|git):[A-Za-z0-9._-]{7,128}$/.test(proposal.source.sourceRevision)) {
482
+ || !/^(?:git|sha256|foldermd|eve|heuristic):[A-Za-z0-9._-]{7,128}$/.test(proposal.source.sourceRevision)) {
388
483
  errors.push(proposalError(sourceRef, "invalid_proposal", "proposal envelope or identity is invalid"));
389
484
  }
390
485
  if (!safeSecretScan(proposal))
@@ -419,16 +514,23 @@ function validateProjectBrainProposal(value) {
419
514
  errors.push(proposalError(sourceRef, "invalid_proposal", "proposal patchFormat is unsupported"));
420
515
  if (!proposal.approval || proposal.approval.required !== true || proposal.approval.policy !== "fresh_exact_patch"
421
516
  || proposal.approval.reusable !== false || proposal.approval.maxAgeSeconds !== 900
422
- || JSON.stringify(proposal.approval.bindingFields) !== JSON.stringify(APPROVAL_BINDINGS)) {
517
+ || JSON.stringify(proposal.approval.bindingFields) !== JSON.stringify(hasTaskMutation ? TASK_APPROVAL_BINDINGS : APPROVAL_BINDINGS)) {
423
518
  errors.push(proposalError(sourceRef, "approval_binding_mismatch", "proposal requires a fresh approval bound to every exact identity and patch field"));
424
519
  }
425
520
  const { proposalRef: _proposalRef, ...preimage } = proposal;
426
521
  if (proposal.proposalRef !== `proposal:brain/${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(preimage))}`) {
427
522
  errors.push(proposalError(sourceRef, "proposal_ref_mismatch", "proposalRef does not match the canonical proposal envelope"));
428
523
  }
524
+ const descriptorAdoptionPatchValid = proposal.patchFormat === "rfc6902"
525
+ && Array.isArray(proposal.patch) && proposal.patch.length === 1
526
+ && proposal.target?.exists === true
527
+ && proposal.patch[0]?.path === ""
528
+ && proposal.patch[0]?.op === "replace"
529
+ && "value" in proposal.patch[0];
429
530
  const typeValid = proposal.proposalType === "descriptor_adoption"
430
531
  ? proposal.requestedCapability === "brain.descriptor.adopt" && proposal.patchFormat === "rfc6902"
431
- && proposal.target.kind === "repository_file" && proposal.target.path === ".you-project" && proposal.target.exists === false
532
+ && proposal.target.kind === "repository_file" && proposal.target.path === ".you-project"
533
+ && descriptorAdoptionPatchValid
432
534
  : proposal.proposalType === "personal_override"
433
535
  ? proposal.requestedCapability === "brain.override.propose" && proposal.patchFormat === "rfc6902"
434
536
  && proposal.target.kind === "project_snapshot" && proposal.target.path === undefined && proposal.target.exists === true
@@ -437,6 +539,12 @@ function validateProjectBrainProposal(value) {
437
539
  && proposal.target.kind === "repository_file" && typeof proposal.target.path === "string" && proposal.target.exists === true;
438
540
  if (!typeValid)
439
541
  errors.push(proposalError(sourceRef, "invalid_proposal", "proposal type, capability, target, and patch format are inconsistent"));
542
+ if (hasTaskMutation && (!validateTaskMutation(proposal.taskMutation, proposal.target?.baseContentHash, proposal.patchDigest)
543
+ || proposal.proposalType !== "artifact_update" || proposal.requestedCapability !== "brain.artifact.update"
544
+ || proposal.patchFormat !== "unified_diff" || proposal.target?.kind !== "repository_file"
545
+ || proposal.target.path?.split("/").at(-1) !== "tasks.json")) {
546
+ errors.push(proposalError(sourceRef, "invalid_proposal", "task mutation is not an exact, safe repository task binding"));
547
+ }
440
548
  return errors.length ? { valid: false, errors } : { valid: true, errors: [], proposal };
441
549
  }
442
550
  function buildProposal(seed) {
@@ -462,14 +570,19 @@ function buildProposal(seed) {
462
570
  return { ...preimage, proposalRef: `proposal:brain/${(0, canonical_1.sha256)((0, canonical_1.canonicalize)(preimage))}` };
463
571
  }
464
572
  function stripFactProvenance(project) {
465
- return project.artifacts.map((artifact) => ({
573
+ const paths = new Set(project.artifacts.map((artifact) => artifact.path));
574
+ return project.artifacts
575
+ .filter((artifact) => artifact.path !== ".you-project" && artifact.path !== ".youmd-project")
576
+ .filter((artifact) => !(artifact.kind === "tasks" && artifact.path.endsWith("/tasks.md")
577
+ && paths.has(`${artifact.path.slice(0, -"tasks.md".length)}tasks.json`)))
578
+ .map((artifact) => ({
466
579
  artifactRef: artifact.artifactRef,
467
580
  subjectRef: artifact.subjectRef,
468
581
  kind: artifact.kind,
469
582
  path: artifact.path,
470
583
  owner: artifact.owner,
471
584
  authority: artifact.authority,
472
- writePolicy: "read_only",
585
+ writePolicy: artifact.owner === "repository" && artifact.authority === "canonical" ? "propose" : "read_only",
473
586
  provenance: {
474
587
  source: artifact.provenance.source,
475
588
  sourceRef: artifact.provenance.sourceRef,
@@ -477,12 +590,53 @@ function stripFactProvenance(project) {
477
590
  indexedAt: artifact.provenance.indexedAt,
478
591
  freshness: artifact.provenance.freshness,
479
592
  },
480
- }));
593
+ }))
594
+ .sort((a, b) => (0, canonical_1.canonicalize)(a).localeCompare((0, canonical_1.canonicalize)(b)));
481
595
  }
482
596
  const PROJECT_REVISION_FIELDS = [
483
- "$schema", "schemaVersion", "kind", "projectRef", "slug", "name", "contextRoot",
484
- "agentRoots", "artifacts", "relationships", "writePolicy", "generatedExcludes", "provenance",
597
+ "$schema", "schemaVersion", "kind", "projectRef", "slug", "name", "aliases", "contextRoot",
598
+ "agentRoots", "workspaces", "repositoryBinding", "legacySource", "artifacts", "relationships",
599
+ "writePolicy", "generatedExcludes", "provenance",
485
600
  ];
601
+ /**
602
+ * Validate the deliberately authority-free descriptor produced by first-time
603
+ * adoption. Adoption may establish catalog ownership, but it must never grant
604
+ * managed or task writes as a side effect of replacing a legacy marker.
605
+ */
606
+ function validateProjectBrainAdoptionDescriptor(value, projectRef) {
607
+ const validation = (0, descriptor_validator_1.validateProjectBrainDescriptor)(value);
608
+ if (!validation.valid)
609
+ return null;
610
+ let descriptor;
611
+ try {
612
+ descriptor = normalizeJson(value);
613
+ }
614
+ catch {
615
+ return null;
616
+ }
617
+ const record = descriptor;
618
+ const writePolicy = record.writePolicy;
619
+ if (record.projectRef !== projectRef || !writePolicy || typeof writePolicy !== "object" || Array.isArray(writePolicy))
620
+ return null;
621
+ const policy = writePolicy;
622
+ const taskWrite = policy.taskWrite;
623
+ const artifacts = record.artifacts;
624
+ if (policy.default !== "propose" || !Array.isArray(policy.managedPaths) || policy.managedPaths.length !== 0
625
+ || !taskWrite || typeof taskWrite !== "object" || Array.isArray(taskWrite)
626
+ || taskWrite.enabled !== false
627
+ || Object.keys(taskWrite).some((key) => key !== "enabled"))
628
+ return null;
629
+ if (!Array.isArray(artifacts))
630
+ return null;
631
+ for (const artifact of artifacts) {
632
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact))
633
+ return null;
634
+ const claim = artifact;
635
+ if (claim.writePolicy !== "propose" && claim.writePolicy !== "read_only")
636
+ return null;
637
+ }
638
+ return descriptor;
639
+ }
486
640
  function withoutVolatile(value) {
487
641
  if (Array.isArray(value))
488
642
  return value.map(withoutVolatile);
@@ -494,25 +648,50 @@ function withoutVolatile(value) {
494
648
  result[key] = withoutVolatile(child);
495
649
  return result;
496
650
  }
497
- function adoptionDescriptor(scan, project, name) {
651
+ function adoptionBaseRevision(workspaces) {
652
+ if (workspaces.length === 1)
653
+ return workspaces[0].scan.revision.digest;
654
+ return digest(workspaces.map(({ scan }) => ({ workspaceRef: scan.workspaceRef, revision: scan.revision.digest })));
655
+ }
656
+ function adoptionDescriptor(workspaces, name) {
657
+ const { scan, project } = workspaces[0];
498
658
  const slug = project.projectRef.slice("project:".length);
659
+ const baseRevision = adoptionBaseRevision(workspaces);
660
+ const repositoryRef = scan.workspaceIdentity.repositoryRef;
661
+ const repositoryBinding = project.repositoryBinding || {
662
+ repositoryRef,
663
+ evidence: { source: "local_workspace", sourceRevision: repositoryRef.replace(/^repository:/, "") },
664
+ };
665
+ const displayName = name?.trim() || project.displayName?.trim()
666
+ || slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
667
+ const workspaceBindings = workspaces.map(({ scan: workspace }) => ({
668
+ workspaceRef: workspace.workspaceRef,
669
+ ...workspace.workspaceIdentity,
670
+ sourceRevision: workspace.revision.digest,
671
+ })).sort((a, b) => a.workspaceRef.localeCompare(b.workspaceRef));
499
672
  const descriptor = {
500
673
  $schema: "https://you.md/schemas/project-brain/v1",
501
674
  schemaVersion: "you-md/project-brain/v1",
502
675
  kind: "project",
503
676
  projectRef: project.projectRef,
504
677
  slug,
505
- name: name?.trim() || slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "),
678
+ name: displayName,
679
+ aliases: [slug],
506
680
  contextRoot: project.contextRoot,
507
- agentRoots: project.artifacts.filter((artifact) => artifact.kind === "agent").map((artifact) => artifact.path),
681
+ agentRoots: [...new Set(project.artifacts.filter((artifact) => artifact.kind === "agent").map((artifact) => artifact.path))].sort(),
682
+ workspaces: workspaceBindings,
683
+ repositoryBinding: normalizeJson(repositoryBinding),
684
+ legacySource: project.management === "legacy-adopted"
685
+ ? { path: ".youmd-project", baseContentHash: project.provenance.sourceRevision, preserved: true }
686
+ : null,
508
687
  artifacts: stripFactProvenance(project),
509
688
  relationships: [],
510
- writePolicy: { default: "read_only", managedPaths: [], taskWrite: { enabled: false } },
689
+ writePolicy: { default: "propose", managedPaths: [], taskWrite: { enabled: false } },
511
690
  generatedExcludes: [...scan.generatedExcludes],
512
691
  provenance: {
513
- source: project.management === "legacy-adopted" ? "legacy-adoption" : "declared",
692
+ source: project.management === "legacy-adopted" ? "legacy-adoption" : "heuristic-adoption",
514
693
  descriptorPath: ".you-project",
515
- sourceRevision: scan.revision.digest,
694
+ sourceRevision: baseRevision,
516
695
  indexedAt: project.provenance.indexedAt,
517
696
  freshness: project.provenance.freshness,
518
697
  },
@@ -532,63 +711,161 @@ function adoptionDescriptor(scan, project, name) {
532
711
  };
533
712
  return normalizeJson(descriptor);
534
713
  }
535
- /** Generate review-only `.you-project` adoption proposals. No target file is created or changed. */
536
- function proposeProjectBrainAdoption(scan, options = {}) {
714
+ function adoptionSemantics(project) {
715
+ return (0, canonical_1.canonicalize)({
716
+ contextRoot: project.contextRoot,
717
+ artifacts: stripFactProvenance(project).map((artifact) => {
718
+ const value = artifact;
719
+ return {
720
+ artifactRef: value.artifactRef,
721
+ subjectRef: value.subjectRef,
722
+ kind: value.kind,
723
+ path: value.path,
724
+ owner: value.owner,
725
+ authority: value.authority,
726
+ writePolicy: value.writePolicy,
727
+ };
728
+ }),
729
+ });
730
+ }
731
+ function proposalForAdoptionGroup(workspaces, options) {
537
732
  const proposals = [];
538
733
  const errors = [];
539
- const projects = scan.projects.filter((project) => !options.projectRef || project.projectRef === options.projectRef);
540
- if (options.projectRef && projects.length === 0)
541
- errors.push(proposalError(options.projectRef, "unknown_project", "project is not present in the exact scan"));
542
- for (const project of projects) {
543
- if (project.management === "declared" || project.descriptorPath === ".you-project") {
544
- errors.push(proposalError(project.projectRef, "not_adoptable", "project already has a declared .you-project descriptor"));
545
- continue;
546
- }
547
- if (project.artifacts.length === 0) {
548
- errors.push(proposalError(project.projectRef, "not_adoptable", "project has no attributable artifacts for descriptor adoption"));
549
- continue;
550
- }
551
- const descriptor = adoptionDescriptor(scan, project, options.name);
552
- const patchResult = validateProjectBrainPatch(null, [{ op: "add", path: "", value: descriptor }], project.projectRef);
553
- if (!patchResult.valid || !patchResult.patch || !patchResult.patchDigest || !patchResult.diff) {
554
- errors.push(...patchResult.errors);
734
+ const primary = workspaces[0];
735
+ const { project, scan } = primary;
736
+ if (workspaces.some(({ project: candidate }) => candidate.management === "declared" || candidate.descriptorPath === ".you-project")) {
737
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "project already has a declared .you-project descriptor")] };
738
+ }
739
+ if (stripFactProvenance(project).length === 0) {
740
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "project has no attributable non-marker artifacts for descriptor adoption")] };
741
+ }
742
+ if (!SAFE_RELATIVE_PATH.test(project.contextRoot)
743
+ || workspaces.some(({ project: candidate }) => candidate.artifacts.some((artifact) => !SAFE_RELATIVE_PATH.test(artifact.path)))) {
744
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "project contains an unsafe descriptor path")] };
745
+ }
746
+ const repositoryRefs = new Set(workspaces.map(({ scan: candidate }) => candidate.workspaceIdentity.repositoryRef));
747
+ const bindingRefs = new Set(workspaces.map(({ project: candidate }) => candidate.repositoryBinding?.repositoryRef).filter(Boolean));
748
+ const githubSubjects = new Set(workspaces.map(({ project: candidate }) => candidate.repositoryBinding?.subjectRef).filter(Boolean));
749
+ if (repositoryRefs.size !== 1 || bindingRefs.size > 1 || githubSubjects.size > 1
750
+ || (bindingRefs.size === 1 && !bindingRefs.has(scan.workspaceIdentity.repositoryRef))) {
751
+ return { proposals, errors: [proposalError(project.projectRef, "identity_conflict", "logical project maps to conflicting repository evidence; owner review is required")] };
752
+ }
753
+ const semantics = new Set(workspaces.map(({ project: candidate }) => adoptionSemantics(candidate)));
754
+ const legacyRevisions = new Set(workspaces
755
+ .filter(({ project: candidate }) => candidate.management === "legacy-adopted")
756
+ .map(({ project: candidate }) => candidate.provenance.sourceRevision));
757
+ if (semantics.size !== 1 || legacyRevisions.size > 1) {
758
+ return { proposals, errors: [proposalError(project.projectRef, "semantic_review_required", "workspace artifact or legacy-marker claims differ; exact automatic adoption is unsafe")] };
759
+ }
760
+ if (project.management === "legacy-adopted" && !/^sha256:[a-f0-9]{64}$/.test(project.provenance.sourceRevision)) {
761
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "legacy .youmd-project lacks a verified byte hash; rescan in content mode")] };
762
+ }
763
+ const existingDescriptors = workspaces.flatMap(({ project: candidate }) => candidate.artifacts.filter((artifact) => artifact.path === ".you-project"));
764
+ const existingHashes = new Set(existingDescriptors.map((artifact) => artifact.provenance.sourceRevision));
765
+ if (existingDescriptors.some((artifact) => !/^sha256:[a-f0-9]{64}$/.test(artifact.provenance.sourceRevision))) {
766
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "existing .you-project lacks a verified content hash; rescan in content mode")] };
767
+ }
768
+ if (existingHashes.size > 1 || (existingDescriptors.length > 0 && existingDescriptors.length !== workspaces.length)) {
769
+ return { proposals, errors: [proposalError(project.projectRef, "semantic_review_required", "workspaces do not share the same exact .you-project base")] };
770
+ }
771
+ if (existingDescriptors.length !== workspaces.length) {
772
+ return { proposals, errors: [proposalError(project.projectRef, "not_adoptable", "descriptor adoption apply is replacement-only; create a valid .you-project through an explicit reviewed repository change before adoption")] };
773
+ }
774
+ const descriptor = adoptionDescriptor(workspaces, options.name);
775
+ const existingDescriptor = existingDescriptors[0];
776
+ const patchResult = (() => {
777
+ const patch = [{ op: "replace", path: "", value: descriptor }];
778
+ const normalized = normalizePatch(patch, project.projectRef);
779
+ if (!normalized.patch)
780
+ return { valid: false, errors: normalized.errors };
781
+ return {
782
+ valid: true,
783
+ errors: [],
784
+ patch: normalized.patch,
785
+ patchDigest: digest(normalized.patch),
786
+ result: descriptor,
787
+ diff: {
788
+ format: "rfc6902",
789
+ beforeDigest: existingDescriptor.provenance.sourceRevision,
790
+ afterDigest: digest(descriptor),
791
+ operations: [{
792
+ index: 0,
793
+ op: "replace",
794
+ path: "",
795
+ before: { present: true },
796
+ after: { present: true, value: descriptor },
797
+ }],
798
+ },
799
+ };
800
+ })();
801
+ if (!patchResult.valid || !patchResult.patch || !patchResult.patchDigest || !patchResult.diff) {
802
+ errors.push(...patchResult.errors);
803
+ return { proposals, errors };
804
+ }
805
+ const slug = project.projectRef.slice("project:".length);
806
+ const baseRevision = adoptionBaseRevision(workspaces);
807
+ proposals.push(buildProposal({
808
+ proposalType: "descriptor_adoption",
809
+ projectRef: project.projectRef,
810
+ machineRef: scan.workspaceIdentity.machineRef,
811
+ workspaceRef: scan.workspaceRef,
812
+ baseRevision,
813
+ requestedCapability: "brain.descriptor.adopt",
814
+ patchFormat: "rfc6902",
815
+ patchDigest: patchResult.patchDigest,
816
+ rationale: options.rationale?.trim() || `Adopt ${project.projectRef} with a reviewable .you-project descriptor.`,
817
+ target: {
818
+ kind: "repository_file",
819
+ artifactRef: `artifact:${slug}/project-descriptor`,
820
+ path: ".you-project",
821
+ owner: "repository",
822
+ authority: "canonical",
823
+ humanOwned: true,
824
+ exists: Boolean(existingDescriptor),
825
+ baseContentHash: existingDescriptor?.provenance.sourceRevision || digest(null),
826
+ },
827
+ patch: patchResult.patch,
828
+ diff: patchResult.diff,
829
+ source: {
830
+ kind: project.management === "legacy-adopted" ? "legacy_marker" : "heuristic_scan",
831
+ sourceRef: project.provenance.sourceRef,
832
+ sourceRevision: project.management === "legacy-adopted" ? project.provenance.sourceRevision : baseRevision,
833
+ ...(project.legacyMarkerPath ? { path: project.legacyMarkerPath } : {}),
834
+ },
835
+ }));
836
+ return { proposals, errors };
837
+ }
838
+ /** Generate review-only replacement proposals for an existing invalid `.you-project`. */
839
+ function proposeProjectBrainAdoptions(scans, options = {}) {
840
+ const proposals = [];
841
+ const errors = [];
842
+ const candidates = scans.flatMap((scan) => scan.projects.map((project) => ({ scan, project })))
843
+ .filter(({ project }) => !options.projectRef || project.projectRef === options.projectRef)
844
+ .sort((a, b) => a.project.projectRef.localeCompare(b.project.projectRef)
845
+ || a.scan.workspaceRef.localeCompare(b.scan.workspaceRef)
846
+ || a.scan.revision.digest.localeCompare(b.scan.revision.digest));
847
+ if (options.projectRef && candidates.length === 0) {
848
+ return { proposals, errors: [proposalError(options.projectRef, "unknown_project", "project is not present in the exact scans")] };
849
+ }
850
+ const grouped = new Map();
851
+ for (const candidate of candidates)
852
+ grouped.set(candidate.project.projectRef, [...(grouped.get(candidate.project.projectRef) || []), candidate]);
853
+ for (const [projectRef, workspaces] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
854
+ if (options.name && !options.projectRef && grouped.size > 1) {
855
+ errors.push(proposalError(projectRef, "semantic_review_required", "--name requires a selected project when adopting multiple logical projects"));
555
856
  continue;
556
857
  }
557
- const slug = project.projectRef.slice("project:".length);
558
- proposals.push(buildProposal({
559
- proposalType: "descriptor_adoption",
560
- projectRef: project.projectRef,
561
- machineRef: scan.workspaceIdentity.machineRef,
562
- workspaceRef: scan.workspaceRef,
563
- baseRevision: scan.revision.digest,
564
- requestedCapability: "brain.descriptor.adopt",
565
- patchFormat: "rfc6902",
566
- patchDigest: patchResult.patchDigest,
567
- rationale: options.rationale?.trim() || `Adopt ${project.projectRef} with a reviewable .you-project descriptor.`,
568
- target: {
569
- kind: "repository_file",
570
- artifactRef: `artifact:${slug}/project-descriptor`,
571
- path: ".you-project",
572
- owner: "repository",
573
- authority: "canonical",
574
- humanOwned: true,
575
- exists: false,
576
- baseContentHash: digest(null),
577
- },
578
- patch: patchResult.patch,
579
- diff: patchResult.diff,
580
- source: {
581
- kind: project.management === "legacy-adopted" ? "legacy_marker" : "heuristic_scan",
582
- sourceRef: project.provenance.sourceRef,
583
- sourceRevision: project.provenance.sourceRevision,
584
- ...(project.legacyMarkerPath ? { path: project.legacyMarkerPath } : {}),
585
- },
586
- }));
858
+ const generated = proposalForAdoptionGroup(workspaces, options);
859
+ proposals.push(...generated.proposals);
860
+ errors.push(...generated.errors);
587
861
  }
588
- proposals.sort((a, b) => a.proposalRef.localeCompare(b.proposalRef));
862
+ proposals.sort((a, b) => a.projectRef.localeCompare(b.projectRef) || a.proposalRef.localeCompare(b.proposalRef));
589
863
  errors.sort((a, b) => a.sourceRef.localeCompare(b.sourceRef) || a.code.localeCompare(b.code));
590
864
  return { proposals, errors };
591
865
  }
866
+ function proposeProjectBrainAdoption(scan, options = {}) {
867
+ return proposeProjectBrainAdoptions([scan], options);
868
+ }
592
869
  function parseOverride(value) {
593
870
  if (!value || typeof value !== "object" || Array.isArray(value))
594
871
  return null;