youmd 0.11.0 → 0.13.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 (216) hide show
  1. package/dist/commands/brain.d.ts.map +1 -1
  2. package/dist/commands/brain.js +77 -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/connect.d.ts +113 -0
  8. package/dist/commands/connect.d.ts.map +1 -0
  9. package/dist/commands/connect.js +849 -0
  10. package/dist/commands/connect.js.map +1 -0
  11. package/dist/commands/conversations.d.ts +5 -0
  12. package/dist/commands/conversations.d.ts.map +1 -0
  13. package/dist/commands/conversations.js +116 -0
  14. package/dist/commands/conversations.js.map +1 -0
  15. package/dist/commands/founder.d.ts +16 -0
  16. package/dist/commands/founder.d.ts.map +1 -0
  17. package/dist/commands/founder.js +446 -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/sync.d.ts.map +1 -1
  39. package/dist/commands/sync.js +11 -0
  40. package/dist/commands/sync.js.map +1 -1
  41. package/dist/commands/tasks.d.ts +20 -0
  42. package/dist/commands/tasks.d.ts.map +1 -1
  43. package/dist/commands/tasks.js +90 -19
  44. package/dist/commands/tasks.js.map +1 -1
  45. package/dist/generated/capability-registry.d.ts +444 -76
  46. package/dist/generated/capability-registry.d.ts.map +1 -1
  47. package/dist/generated/capability-registry.js +497 -76
  48. package/dist/generated/capability-registry.js.map +1 -1
  49. package/dist/index.js +122 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/lib/api.d.ts +393 -2
  52. package/dist/lib/api.d.ts.map +1 -1
  53. package/dist/lib/api.js +195 -2
  54. package/dist/lib/api.js.map +1 -1
  55. package/dist/lib/brain/adapters.d.ts +22 -1
  56. package/dist/lib/brain/adapters.d.ts.map +1 -1
  57. package/dist/lib/brain/adapters.js +184 -2
  58. package/dist/lib/brain/adapters.js.map +1 -1
  59. package/dist/lib/brain/approval-key-trust.d.ts +29 -0
  60. package/dist/lib/brain/approval-key-trust.d.ts.map +1 -0
  61. package/dist/lib/brain/approval-key-trust.js +139 -0
  62. package/dist/lib/brain/approval-key-trust.js.map +1 -0
  63. package/dist/lib/brain/content.d.ts +10 -3
  64. package/dist/lib/brain/content.d.ts.map +1 -1
  65. package/dist/lib/brain/content.js +133 -30
  66. package/dist/lib/brain/content.js.map +1 -1
  67. package/dist/lib/brain/descriptor-validator.d.ts +11 -0
  68. package/dist/lib/brain/descriptor-validator.d.ts.map +1 -0
  69. package/dist/lib/brain/descriptor-validator.js +185 -0
  70. package/dist/lib/brain/descriptor-validator.js.map +1 -0
  71. package/dist/lib/brain/discovery.d.ts.map +1 -1
  72. package/dist/lib/brain/discovery.js +77 -50
  73. package/dist/lib/brain/discovery.js.map +1 -1
  74. package/dist/lib/brain/index.d.ts +2 -0
  75. package/dist/lib/brain/index.d.ts.map +1 -1
  76. package/dist/lib/brain/index.js +14 -4
  77. package/dist/lib/brain/index.js.map +1 -1
  78. package/dist/lib/brain/proposal.d.ts +53 -5
  79. package/dist/lib/brain/proposal.d.ts.map +1 -1
  80. package/dist/lib/brain/proposal.js +330 -94
  81. package/dist/lib/brain/proposal.js.map +1 -1
  82. package/dist/lib/brain/safe-file.d.ts +25 -1
  83. package/dist/lib/brain/safe-file.d.ts.map +1 -1
  84. package/dist/lib/brain/safe-file.js +250 -30
  85. package/dist/lib/brain/safe-file.js.map +1 -1
  86. package/dist/lib/brain/safety.d.ts.map +1 -1
  87. package/dist/lib/brain/safety.js +5 -0
  88. package/dist/lib/brain/safety.js.map +1 -1
  89. package/dist/lib/brain/staged-apply.d.ts +2 -1
  90. package/dist/lib/brain/staged-apply.d.ts.map +1 -1
  91. package/dist/lib/brain/staged-apply.js +6 -3
  92. package/dist/lib/brain/staged-apply.js.map +1 -1
  93. package/dist/lib/brain/store.d.ts +3 -0
  94. package/dist/lib/brain/store.d.ts.map +1 -1
  95. package/dist/lib/brain/store.js +68 -0
  96. package/dist/lib/brain/store.js.map +1 -1
  97. package/dist/lib/brain/sync.d.ts +48 -0
  98. package/dist/lib/brain/sync.d.ts.map +1 -1
  99. package/dist/lib/brain/sync.js +102 -0
  100. package/dist/lib/brain/sync.js.map +1 -1
  101. package/dist/lib/brain/types.d.ts +23 -0
  102. package/dist/lib/brain/types.d.ts.map +1 -1
  103. package/dist/lib/brain/vision.d.ts +35 -0
  104. package/dist/lib/brain/vision.d.ts.map +1 -0
  105. package/dist/lib/brain/vision.js +65 -0
  106. package/dist/lib/brain/vision.js.map +1 -0
  107. package/dist/lib/businessScaffold.d.ts +42 -0
  108. package/dist/lib/businessScaffold.d.ts.map +1 -0
  109. package/dist/lib/businessScaffold.js +150 -0
  110. package/dist/lib/businessScaffold.js.map +1 -0
  111. package/dist/lib/codex-device-auth.d.ts +94 -0
  112. package/dist/lib/codex-device-auth.d.ts.map +1 -0
  113. package/dist/lib/codex-device-auth.js +295 -0
  114. package/dist/lib/codex-device-auth.js.map +1 -0
  115. package/dist/lib/codex-job-control-plane.d.ts +67 -0
  116. package/dist/lib/codex-job-control-plane.d.ts.map +1 -0
  117. package/dist/lib/codex-job-control-plane.js +105 -0
  118. package/dist/lib/codex-job-control-plane.js.map +1 -0
  119. package/dist/lib/codex-readonly-job.d.ts +76 -0
  120. package/dist/lib/codex-readonly-job.d.ts.map +1 -0
  121. package/dist/lib/codex-readonly-job.js +196 -0
  122. package/dist/lib/codex-readonly-job.js.map +1 -0
  123. package/dist/lib/config.d.ts +7 -0
  124. package/dist/lib/config.d.ts.map +1 -1
  125. package/dist/lib/config.js +3 -0
  126. package/dist/lib/config.js.map +1 -1
  127. package/dist/lib/connected-access-native.d.ts +111 -0
  128. package/dist/lib/connected-access-native.d.ts.map +1 -0
  129. package/dist/lib/connected-access-native.js +403 -0
  130. package/dist/lib/connected-access-native.js.map +1 -0
  131. package/dist/lib/connected-context.d.ts +82 -0
  132. package/dist/lib/connected-context.d.ts.map +1 -0
  133. package/dist/lib/connected-context.js +610 -0
  134. package/dist/lib/connected-context.js.map +1 -0
  135. package/dist/lib/goalRegistryFile.d.ts +169 -0
  136. package/dist/lib/goalRegistryFile.d.ts.map +1 -0
  137. package/dist/lib/goalRegistryFile.js +518 -0
  138. package/dist/lib/goalRegistryFile.js.map +1 -0
  139. package/dist/lib/goalsFile.d.ts +101 -0
  140. package/dist/lib/goalsFile.d.ts.map +1 -0
  141. package/dist/lib/goalsFile.js +413 -0
  142. package/dist/lib/goalsFile.js.map +1 -0
  143. package/dist/lib/handoff.d.ts +170 -0
  144. package/dist/lib/handoff.d.ts.map +1 -0
  145. package/dist/lib/handoff.js +316 -0
  146. package/dist/lib/handoff.js.map +1 -0
  147. package/dist/lib/machine-identity.d.ts +2 -0
  148. package/dist/lib/machine-identity.d.ts.map +1 -1
  149. package/dist/lib/machine-identity.js +13 -6
  150. package/dist/lib/machine-identity.js.map +1 -1
  151. package/dist/lib/onboarding.d.ts +4 -1
  152. package/dist/lib/onboarding.d.ts.map +1 -1
  153. package/dist/lib/onboarding.js +11 -3
  154. package/dist/lib/onboarding.js.map +1 -1
  155. package/dist/lib/orchestrator/loop.d.ts.map +1 -1
  156. package/dist/lib/orchestrator/loop.js +8 -3
  157. package/dist/lib/orchestrator/loop.js.map +1 -1
  158. package/dist/lib/orchestrator/run-journal.d.ts +68 -3
  159. package/dist/lib/orchestrator/run-journal.d.ts.map +1 -1
  160. package/dist/lib/orchestrator/run-journal.js +391 -37
  161. package/dist/lib/orchestrator/run-journal.js.map +1 -1
  162. package/dist/lib/orchestrator/supervisor.d.ts +11 -0
  163. package/dist/lib/orchestrator/supervisor.d.ts.map +1 -1
  164. package/dist/lib/orchestrator/supervisor.js +304 -116
  165. package/dist/lib/orchestrator/supervisor.js.map +1 -1
  166. package/dist/lib/orchestrator/tools.d.ts.map +1 -1
  167. package/dist/lib/orchestrator/tools.js +76 -3
  168. package/dist/lib/orchestrator/tools.js.map +1 -1
  169. package/dist/lib/redact.d.ts.map +1 -1
  170. package/dist/lib/redact.js +40 -7
  171. package/dist/lib/redact.js.map +1 -1
  172. package/dist/lib/remote-agent-event.d.ts +19 -0
  173. package/dist/lib/remote-agent-event.d.ts.map +1 -0
  174. package/dist/lib/remote-agent-event.js +85 -0
  175. package/dist/lib/remote-agent-event.js.map +1 -0
  176. package/dist/lib/remote-command.d.ts.map +1 -1
  177. package/dist/lib/remote-command.js +19 -0
  178. package/dist/lib/remote-command.js.map +1 -1
  179. package/dist/lib/remote-executor.d.ts +7 -1
  180. package/dist/lib/remote-executor.d.ts.map +1 -1
  181. package/dist/lib/remote-executor.js +391 -0
  182. package/dist/lib/remote-executor.js.map +1 -1
  183. package/dist/lib/sessionCheckpoint.d.ts +104 -0
  184. package/dist/lib/sessionCheckpoint.d.ts.map +1 -0
  185. package/dist/lib/sessionCheckpoint.js +191 -0
  186. package/dist/lib/sessionCheckpoint.js.map +1 -0
  187. package/dist/lib/sessionHandoff.d.ts +47 -0
  188. package/dist/lib/sessionHandoff.d.ts.map +1 -0
  189. package/dist/lib/sessionHandoff.js +305 -0
  190. package/dist/lib/sessionHandoff.js.map +1 -0
  191. package/dist/lib/stream.d.ts +3 -1
  192. package/dist/lib/stream.d.ts.map +1 -1
  193. package/dist/lib/stream.js +8 -3
  194. package/dist/lib/stream.js.map +1 -1
  195. package/dist/lib/taskLedgerProposal.d.ts +2 -2
  196. package/dist/lib/taskLedgerProposal.d.ts.map +1 -1
  197. package/dist/lib/taskLedgerProposal.js +16 -8
  198. package/dist/lib/taskLedgerProposal.js.map +1 -1
  199. package/dist/lib/tasksFile.d.ts +14 -2
  200. package/dist/lib/tasksFile.d.ts.map +1 -1
  201. package/dist/lib/tasksFile.js +39 -5
  202. package/dist/lib/tasksFile.js.map +1 -1
  203. package/dist/mcp/registry.d.ts +4 -1
  204. package/dist/mcp/registry.d.ts.map +1 -1
  205. package/dist/mcp/registry.js +230 -1
  206. package/dist/mcp/registry.js.map +1 -1
  207. package/dist/mcp/server.d.ts +1 -1
  208. package/dist/mcp/server.d.ts.map +1 -1
  209. package/dist/mcp/server.js +18 -9
  210. package/dist/mcp/server.js.map +1 -1
  211. package/native/prebuilds/darwin-universal/youmd_safe_open.node +0 -0
  212. package/package.json +4 -2
  213. package/scripts/env-vault/README.md +16 -20
  214. package/scripts/env-vault/backup.sh +22 -57
  215. package/scripts/env-vault/restore.sh +18 -96
  216. 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,7 +514,7 @@ 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;
@@ -428,8 +523,9 @@ function validateProjectBrainProposal(value) {
428
523
  }
429
524
  const descriptorAdoptionPatchValid = proposal.patchFormat === "rfc6902"
430
525
  && Array.isArray(proposal.patch) && proposal.patch.length === 1
526
+ && proposal.target?.exists === true
431
527
  && proposal.patch[0]?.path === ""
432
- && proposal.patch[0]?.op === (proposal.target?.exists ? "replace" : "add")
528
+ && proposal.patch[0]?.op === "replace"
433
529
  && "value" in proposal.patch[0];
434
530
  const typeValid = proposal.proposalType === "descriptor_adoption"
435
531
  ? proposal.requestedCapability === "brain.descriptor.adopt" && proposal.patchFormat === "rfc6902"
@@ -443,6 +539,12 @@ function validateProjectBrainProposal(value) {
443
539
  && proposal.target.kind === "repository_file" && typeof proposal.target.path === "string" && proposal.target.exists === true;
444
540
  if (!typeValid)
445
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
+ }
446
548
  return errors.length ? { valid: false, errors } : { valid: true, errors: [], proposal };
447
549
  }
448
550
  function buildProposal(seed) {
@@ -470,6 +572,7 @@ function buildProposal(seed) {
470
572
  function stripFactProvenance(project) {
471
573
  const paths = new Set(project.artifacts.map((artifact) => artifact.path));
472
574
  return project.artifacts
575
+ .filter((artifact) => artifact.path !== ".you-project" && artifact.path !== ".youmd-project")
473
576
  .filter((artifact) => !(artifact.kind === "tasks" && artifact.path.endsWith("/tasks.md")
474
577
  && paths.has(`${artifact.path.slice(0, -"tasks.md".length)}tasks.json`)))
475
578
  .map((artifact) => ({
@@ -479,7 +582,7 @@ function stripFactProvenance(project) {
479
582
  path: artifact.path,
480
583
  owner: artifact.owner,
481
584
  authority: artifact.authority,
482
- writePolicy: "read_only",
585
+ writePolicy: artifact.owner === "repository" && artifact.authority === "canonical" ? "propose" : "read_only",
483
586
  provenance: {
484
587
  source: artifact.provenance.source,
485
588
  sourceRef: artifact.provenance.sourceRef,
@@ -487,12 +590,53 @@ function stripFactProvenance(project) {
487
590
  indexedAt: artifact.provenance.indexedAt,
488
591
  freshness: artifact.provenance.freshness,
489
592
  },
490
- }));
593
+ }))
594
+ .sort((a, b) => (0, canonical_1.canonicalize)(a).localeCompare((0, canonical_1.canonicalize)(b)));
491
595
  }
492
596
  const PROJECT_REVISION_FIELDS = [
493
- "$schema", "schemaVersion", "kind", "projectRef", "slug", "name", "contextRoot",
494
- "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",
495
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
+ }
496
640
  function withoutVolatile(value) {
497
641
  if (Array.isArray(value))
498
642
  return value.map(withoutVolatile);
@@ -504,25 +648,50 @@ function withoutVolatile(value) {
504
648
  result[key] = withoutVolatile(child);
505
649
  return result;
506
650
  }
507
- 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];
508
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));
509
672
  const descriptor = {
510
673
  $schema: "https://you.md/schemas/project-brain/v1",
511
674
  schemaVersion: "you-md/project-brain/v1",
512
675
  kind: "project",
513
676
  projectRef: project.projectRef,
514
677
  slug,
515
- name: name?.trim() || slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "),
678
+ name: displayName,
679
+ aliases: [slug],
516
680
  contextRoot: project.contextRoot,
517
- 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,
518
687
  artifacts: stripFactProvenance(project),
519
688
  relationships: [],
520
- writePolicy: { default: "read_only", managedPaths: [], taskWrite: { enabled: false } },
689
+ writePolicy: { default: "propose", managedPaths: [], taskWrite: { enabled: false } },
521
690
  generatedExcludes: [...scan.generatedExcludes],
522
691
  provenance: {
523
- source: project.management === "legacy-adopted" ? "legacy-adoption" : "declared",
692
+ source: project.management === "legacy-adopted" ? "legacy-adoption" : "heuristic-adoption",
524
693
  descriptorPath: ".you-project",
525
- sourceRevision: scan.revision.digest,
694
+ sourceRevision: baseRevision,
526
695
  indexedAt: project.provenance.indexedAt,
527
696
  freshness: project.provenance.freshness,
528
697
  },
@@ -542,94 +711,161 @@ function adoptionDescriptor(scan, project, name) {
542
711
  };
543
712
  return normalizeJson(descriptor);
544
713
  }
545
- /** Generate review-only `.you-project` adoption proposals. No target file is created or changed. */
546
- 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) {
547
732
  const proposals = [];
548
733
  const errors = [];
549
- const projects = scan.projects.filter((project) => !options.projectRef || project.projectRef === options.projectRef);
550
- if (options.projectRef && projects.length === 0)
551
- errors.push(proposalError(options.projectRef, "unknown_project", "project is not present in the exact scan"));
552
- for (const project of projects) {
553
- if (project.management === "declared" || project.descriptorPath === ".you-project") {
554
- errors.push(proposalError(project.projectRef, "not_adoptable", "project already has a declared .you-project descriptor"));
555
- continue;
556
- }
557
- if (project.artifacts.length === 0) {
558
- errors.push(proposalError(project.projectRef, "not_adoptable", "project has no attributable artifacts for descriptor adoption"));
559
- continue;
560
- }
561
- const descriptor = adoptionDescriptor(scan, project, options.name);
562
- const existingDescriptor = project.artifacts.find((artifact) => artifact.path === ".you-project");
563
- if (existingDescriptor && !/^sha256:[a-f0-9]{64}$/.test(existingDescriptor.provenance.sourceRevision)) {
564
- errors.push(proposalError(project.projectRef, "not_adoptable", "existing .you-project lacks a verified content hash; rescan in content mode"));
565
- continue;
566
- }
567
- const patchResult = existingDescriptor
568
- ? (() => {
569
- const patch = [{ op: "replace", path: "", value: descriptor }];
570
- const normalized = normalizePatch(patch, project.projectRef);
571
- if (!normalized.patch)
572
- return { valid: false, errors: normalized.errors };
573
- return {
574
- valid: true,
575
- errors: [],
576
- patch: normalized.patch,
577
- patchDigest: digest(normalized.patch),
578
- result: descriptor,
579
- diff: {
580
- format: "rfc6902",
581
- beforeDigest: existingDescriptor.provenance.sourceRevision,
582
- afterDigest: digest(descriptor),
583
- operations: [{
584
- index: 0,
585
- op: "replace",
586
- path: "",
587
- before: { present: true },
588
- after: { present: true, value: descriptor },
589
- }],
590
- },
591
- };
592
- })()
593
- : validateProjectBrainPatch(null, [{ op: "add", path: "", value: descriptor }], project.projectRef);
594
- if (!patchResult.valid || !patchResult.patch || !patchResult.patchDigest || !patchResult.diff) {
595
- 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"));
596
856
  continue;
597
857
  }
598
- const slug = project.projectRef.slice("project:".length);
599
- proposals.push(buildProposal({
600
- proposalType: "descriptor_adoption",
601
- projectRef: project.projectRef,
602
- machineRef: scan.workspaceIdentity.machineRef,
603
- workspaceRef: scan.workspaceRef,
604
- baseRevision: scan.revision.digest,
605
- requestedCapability: "brain.descriptor.adopt",
606
- patchFormat: "rfc6902",
607
- patchDigest: patchResult.patchDigest,
608
- rationale: options.rationale?.trim() || `Adopt ${project.projectRef} with a reviewable .you-project descriptor.`,
609
- target: {
610
- kind: "repository_file",
611
- artifactRef: `artifact:${slug}/project-descriptor`,
612
- path: ".you-project",
613
- owner: "repository",
614
- authority: "canonical",
615
- humanOwned: true,
616
- exists: Boolean(existingDescriptor),
617
- baseContentHash: existingDescriptor?.provenance.sourceRevision || digest(null),
618
- },
619
- patch: patchResult.patch,
620
- diff: patchResult.diff,
621
- source: {
622
- kind: project.management === "legacy-adopted" ? "legacy_marker" : "heuristic_scan",
623
- sourceRef: project.provenance.sourceRef,
624
- sourceRevision: project.provenance.sourceRevision,
625
- ...(project.legacyMarkerPath ? { path: project.legacyMarkerPath } : {}),
626
- },
627
- }));
858
+ const generated = proposalForAdoptionGroup(workspaces, options);
859
+ proposals.push(...generated.proposals);
860
+ errors.push(...generated.errors);
628
861
  }
629
- proposals.sort((a, b) => a.proposalRef.localeCompare(b.proposalRef));
862
+ proposals.sort((a, b) => a.projectRef.localeCompare(b.projectRef) || a.proposalRef.localeCompare(b.proposalRef));
630
863
  errors.sort((a, b) => a.sourceRef.localeCompare(b.sourceRef) || a.code.localeCompare(b.code));
631
864
  return { proposals, errors };
632
865
  }
866
+ function proposeProjectBrainAdoption(scan, options = {}) {
867
+ return proposeProjectBrainAdoptions([scan], options);
868
+ }
633
869
  function parseOverride(value) {
634
870
  if (!value || typeof value !== "object" || Array.isArray(value))
635
871
  return null;