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
@@ -38,13 +38,14 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
- exports.MAX_RUN_EVENTS = exports.RUN_WORKER_ROLES = void 0;
41
+ exports.MAX_RUN_EVENTS = exports.RUN_EVIDENCE_KINDS = exports.RUN_WORKER_ROLES = void 0;
42
42
  exports.isRunWorkerRole = isRunWorkerRole;
43
43
  exports.validateAssignmentPreflight = validateAssignmentPreflight;
44
44
  exports.loadRunJournal = loadRunJournal;
45
45
  exports.saveRunJournal = saveRunJournal;
46
46
  exports.createOrLoadRun = createOrLoadRun;
47
47
  exports.assignRunWorker = assignRunWorker;
48
+ exports.recordRunWorkerAssertion = recordRunWorkerAssertion;
48
49
  exports.completeRunReview = completeRunReview;
49
50
  exports.findBoundUnfinishedRunIds = findBoundUnfinishedRunIds;
50
51
  exports.runFinishBlocker = runFinishBlocker;
@@ -52,23 +53,31 @@ const fs = __importStar(require("fs"));
52
53
  const path = __importStar(require("path"));
53
54
  const config_1 = require("../config");
54
55
  exports.RUN_WORKER_ROLES = ["planner", "specialist", "qa", "design", "reviewer"];
56
+ exports.RUN_EVIDENCE_KINDS = ["test", "artifact", "screenshot", "log", "review", "metric"];
55
57
  exports.MAX_RUN_EVENTS = 256;
56
- const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
58
+ const RUN_ID_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
59
+ const MAX_RUN_ID_LENGTH = 128;
57
60
  const DEFAULT_MAX_ATTEMPTS = 3;
61
+ const DEFAULT_MAX_WORKER_ASSIGNMENTS = 20;
62
+ const DEFAULT_MAX_EVIDENCE_ITEMS = 64;
58
63
  const MAX_EVENT_TEXT = 1000;
64
+ const MAX_EVIDENCE_REF = 1000;
59
65
  const MAX_RUN_WORKER_IDENTITIES = 512;
60
66
  function runsDir(options = {}) {
61
67
  return options.dir ?? path.join((0, config_1.getWritableHomeBundleDir)(), "orchestrator", "runs");
62
68
  }
63
69
  function validRunId(runId) {
64
- return RUN_ID_RE.test(runId);
70
+ if (!runId || runId.length > MAX_RUN_ID_LENGTH)
71
+ return false;
72
+ const token = runId.startsWith("run:") ? runId.slice(4) : runId;
73
+ return RUN_ID_TOKEN_RE.test(token);
65
74
  }
66
75
  function isRunWorkerRole(role) {
67
76
  return exports.RUN_WORKER_ROLES.includes(role);
68
77
  }
69
78
  function runPath(runId, options = {}) {
70
79
  if (!validRunId(runId))
71
- throw new Error("run_id must use 1-128 letters, numbers, dots, dashes, or underscores");
80
+ throw new Error("run_id must use an optional run: prefix and a bounded safe token");
72
81
  return path.join(runsDir(options), `${runId}.json`);
73
82
  }
74
83
  function isStringRecord(value) {
@@ -81,7 +90,83 @@ function isStringArray(value) {
81
90
  return Array.isArray(value) && value.every((entry) => typeof entry === "string" && !!entry.trim());
82
91
  }
83
92
  function boundedText(value, max = MAX_EVENT_TEXT) {
84
- return value.trim().slice(0, max);
93
+ return value
94
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
95
+ .replace(/\s+/g, " ")
96
+ .trim()
97
+ .slice(0, max);
98
+ }
99
+ function isSafeEvidenceRef(value) {
100
+ const ref = value.trim();
101
+ if (!ref || ref.length > MAX_EVIDENCE_REF || /[\u0000-\u001f]/.test(ref))
102
+ return false;
103
+ if (/^https?:\/\//i.test(ref)) {
104
+ try {
105
+ const url = new URL(ref);
106
+ return url.protocol === "https:" || url.protocol === "http:";
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ if (path.isAbsolute(ref) || ref.split(/[\\/]+/).includes(".."))
113
+ return false;
114
+ return true;
115
+ }
116
+ function isRunEvidence(value) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value))
118
+ return false;
119
+ const row = value;
120
+ return (exports.RUN_EVIDENCE_KINDS.includes(String(row.kind)) &&
121
+ typeof row.source === "string" &&
122
+ !!row.source.trim() &&
123
+ typeof row.workerId === "string" &&
124
+ !!row.workerId.trim() &&
125
+ isRunWorkerRole(String(row.role)) &&
126
+ typeof row.summary === "string" &&
127
+ !!row.summary.trim() &&
128
+ typeof row.recordedAt === "string" &&
129
+ (row.ref === undefined || (typeof row.ref === "string" && isSafeEvidenceRef(row.ref))) &&
130
+ (row.sha256 === undefined || (typeof row.sha256 === "string" && /^[a-f0-9]{64}$/i.test(row.sha256))));
131
+ }
132
+ function isDesignApplicability(value) {
133
+ if (!value || typeof value !== "object" || Array.isArray(value))
134
+ return false;
135
+ const row = value;
136
+ return ((row.status === "required" || row.status === "not_applicable") &&
137
+ typeof row.reason === "string" &&
138
+ (row.status === "required" || !!row.reason.trim()));
139
+ }
140
+ function normalizeEvidence(values, workerId, role, recordedAt) {
141
+ const evidence = [];
142
+ for (const value of values) {
143
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
144
+ return { error: "evidence must use typed provenance objects, not free-form strings" };
145
+ }
146
+ const row = value;
147
+ const kind = String(row.kind || "");
148
+ const source = boundedText(String(row.source || ""), 256);
149
+ const summary = boundedText(String(row.summary || ""));
150
+ const ref = row.ref === undefined ? undefined : String(row.ref).trim();
151
+ const sha256 = row.sha256 === undefined ? undefined : String(row.sha256).trim().toLowerCase();
152
+ const candidate = {
153
+ kind,
154
+ source,
155
+ workerId,
156
+ role,
157
+ summary,
158
+ ...(ref ? { ref } : {}),
159
+ ...(sha256 ? { sha256 } : {}),
160
+ recordedAt,
161
+ };
162
+ if (!isRunEvidence(candidate)) {
163
+ return {
164
+ error: "evidence requires kind=test|artifact|screenshot|log|review|metric, bounded source/summary, a safe relative/http(s) ref, and optional sha256",
165
+ };
166
+ }
167
+ evidence.push(candidate);
168
+ }
169
+ return { evidence };
85
170
  }
86
171
  function isPreflight(value) {
87
172
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -107,6 +192,7 @@ function isRunEvent(value) {
107
192
  "worker_assigned",
108
193
  "handoff",
109
194
  "review_started",
195
+ "worker_result",
110
196
  "review_recorded",
111
197
  "retry_required",
112
198
  "blocked",
@@ -158,13 +244,17 @@ function isJournal(value) {
158
244
  if (!value || typeof value !== "object" || Array.isArray(value))
159
245
  return false;
160
246
  const row = value;
161
- const evidenceValid = Array.isArray(row.evidence) &&
162
- row.evidence.every((item) => !!item &&
247
+ const evidenceValid = Array.isArray(row.evidence) && row.evidence.every((item) => {
248
+ if (isRunEvidence(item))
249
+ return true;
250
+ // Read-only compatibility for v1 journals; loadRunJournal upgrades these before mutation.
251
+ return (!!item &&
163
252
  typeof item === "object" &&
164
253
  typeof item.workerId === "string" &&
165
254
  typeof item.role === "string" &&
166
255
  typeof item.summary === "string" &&
167
256
  typeof item.recordedAt === "string");
257
+ });
168
258
  const verdictValid = row.reviewerVerdict === undefined ||
169
259
  (!!row.reviewerVerdict &&
170
260
  ["approved", "changes_requested"].includes(row.reviewerVerdict.status) &&
@@ -172,6 +262,9 @@ function isJournal(value) {
172
262
  typeof row.reviewerVerdict.summary === "string" &&
173
263
  Array.isArray(row.reviewerVerdict.issues) &&
174
264
  row.reviewerVerdict.issues.every((issue) => typeof issue === "string") &&
265
+ (row.reviewerVerdict.semanticSuccess === undefined ||
266
+ (typeof row.reviewerVerdict.semanticSuccess.met === "boolean" &&
267
+ typeof row.reviewerVerdict.semanticSuccess.summary === "string")) &&
175
268
  typeof row.reviewerVerdict.recordedAt === "string");
176
269
  const eventsValid = row.events === undefined ||
177
270
  (Array.isArray(row.events) && row.events.length <= exports.MAX_RUN_EVENTS && row.events.every(isRunEvent));
@@ -186,6 +279,33 @@ function isJournal(value) {
186
279
  evidenceValid &&
187
280
  eventsValid &&
188
281
  verdictValid &&
282
+ (row.workflowVersion === undefined || row.workflowVersion === 2) &&
283
+ (row.designApplicability === undefined || isDesignApplicability(row.designApplicability)) &&
284
+ (row.cursor === undefined || [...exports.RUN_WORKER_ROLES, "done", "blocked"].includes(row.cursor)) &&
285
+ (row.workerAssertions === undefined ||
286
+ (Array.isArray(row.workerAssertions) &&
287
+ row.workerAssertions.every((assertion) => !!assertion &&
288
+ Number.isInteger(assertion.attempt) &&
289
+ assertion.attempt >= 1 &&
290
+ typeof assertion.workerId === "string" &&
291
+ ["planner", "specialist", "qa", "design"].includes(String(assertion.role)) &&
292
+ (assertion.status === "passed" || assertion.status === "failed") &&
293
+ typeof assertion.summary === "string" &&
294
+ Array.isArray(assertion.evidence) &&
295
+ assertion.evidence.every(isRunEvidence) &&
296
+ typeof assertion.recordedAt === "string"))) &&
297
+ (row.budget === undefined ||
298
+ (Number.isInteger(row.budget.maxAttempts) &&
299
+ row.budget.maxAttempts >= 1 &&
300
+ Number.isInteger(row.budget.maxWorkerAssignments) &&
301
+ row.budget.maxWorkerAssignments >= 1 &&
302
+ Number.isInteger(row.budget.maxEvidenceItems) &&
303
+ row.budget.maxEvidenceItems >= 1)) &&
304
+ (row.usage === undefined ||
305
+ (Number.isInteger(row.usage.workerAssignments) &&
306
+ row.usage.workerAssignments >= 0 &&
307
+ Number.isInteger(row.usage.evidenceItems) &&
308
+ row.usage.evidenceItems >= 0)) &&
189
309
  (row.taskId === undefined || typeof row.taskId === "string") &&
190
310
  (row.sessionId === undefined || typeof row.sessionId === "string") &&
191
311
  (row.executionWorkerIds === undefined ||
@@ -217,6 +337,38 @@ function loadRunJournal(runId, options = {}) {
217
337
  const journal = parsed;
218
338
  // v1 journals predate append-only events. They remain readable and gain events on the next mutation.
219
339
  journal.events = Array.isArray(journal.events) ? journal.events : [];
340
+ journal.evidence = journal.evidence.map((item) => isRunEvidence(item)
341
+ ? item
342
+ : (() => {
343
+ const legacy = item;
344
+ return {
345
+ kind: "log",
346
+ source: `legacy:${boundedText(legacy.workerId || "unknown", 128)}`,
347
+ workerId: boundedText(legacy.workerId || "unknown", 128),
348
+ role: isRunWorkerRole(legacy.role || "") ? legacy.role : "reviewer",
349
+ summary: boundedText(legacy.summary || "legacy evidence"),
350
+ recordedAt: legacy.recordedAt || new Date(0).toISOString(),
351
+ };
352
+ })());
353
+ if (journal.reviewerVerdict && !journal.reviewerVerdict.semanticSuccess) {
354
+ journal.reviewerVerdict.semanticSuccess = {
355
+ met: journal.reviewerVerdict.status === "approved",
356
+ summary: "legacy reviewer verdict (semantic-success field was not recorded)",
357
+ };
358
+ }
359
+ if (journal.workflowVersion === 2) {
360
+ journal.workerAssertions = Array.isArray(journal.workerAssertions) ? journal.workerAssertions : [];
361
+ journal.budget = journal.budget ?? {
362
+ maxAttempts: journal.maxAttempts,
363
+ maxWorkerAssignments: DEFAULT_MAX_WORKER_ASSIGNMENTS,
364
+ maxEvidenceItems: DEFAULT_MAX_EVIDENCE_ITEMS,
365
+ };
366
+ journal.usage = journal.usage ?? {
367
+ workerAssignments: journal.events.filter((event) => event.type === "worker_assigned").length,
368
+ evidenceItems: journal.evidence.length,
369
+ };
370
+ journal.cursor = journal.cursor ?? (journal.stage === "completed" ? "done" : "planner");
371
+ }
220
372
  // Older journals only have mutable role slots. Seed the durable identity sets from every
221
373
  // retained assignment event plus current slots; future reassignments can no longer erase them.
222
374
  const assignedEvents = journal.events.filter((event) => event.type === "worker_assigned");
@@ -292,14 +444,35 @@ function createOrLoadRun(input, options = {}) {
292
444
  }
293
445
  return { ok: true, journal: existing };
294
446
  }
447
+ if (input.workflowVersion === 2 && !isDesignApplicability(input.designApplicability)) {
448
+ return {
449
+ ok: false,
450
+ error: "strict governed runs require designApplicability.status=required|not_applicable; not_applicable also requires a reason",
451
+ };
452
+ }
295
453
  const requestedMax = Number.isFinite(input.maxAttempts) ? Number(input.maxAttempts) : DEFAULT_MAX_ATTEMPTS;
296
454
  const maxAttempts = Math.max(1, Math.min(10, Math.trunc(requestedMax)));
455
+ const maxWorkerAssignments = Math.max(4, Math.min(100, Math.trunc(Number(input.maxWorkerAssignments) || DEFAULT_MAX_WORKER_ASSIGNMENTS)));
456
+ const maxEvidenceItems = Math.max(4, Math.min(256, Math.trunc(Number(input.maxEvidenceItems) || DEFAULT_MAX_EVIDENCE_ITEMS)));
297
457
  const timestamp = (options.now?.() ?? new Date()).toISOString();
298
458
  const journal = {
299
459
  runId: input.runId,
300
460
  taskId: input.taskId?.trim() || undefined,
301
461
  sessionId: input.sessionId ? boundedText(input.sessionId, 256) : undefined,
302
462
  stage: "working",
463
+ ...(input.workflowVersion === 2
464
+ ? {
465
+ workflowVersion: 2,
466
+ designApplicability: {
467
+ status: input.designApplicability.status,
468
+ reason: boundedText(input.designApplicability.reason),
469
+ },
470
+ cursor: "planner",
471
+ workerAssertions: [],
472
+ budget: { maxAttempts, maxWorkerAssignments, maxEvidenceItems },
473
+ usage: { workerAssignments: 0, evidenceItems: 0 },
474
+ }
475
+ : {}),
303
476
  workers: {},
304
477
  executionWorkerIds: [],
305
478
  reviewerWorkerIds: [],
@@ -331,6 +504,10 @@ function assignRunWorker(input, options = {}) {
331
504
  const preflightError = validateAssignmentPreflight(input.preflight);
332
505
  if (preflightError)
333
506
  return { ok: false, error: preflightError };
507
+ const before = loadRunJournal(input.runId, options);
508
+ if (!before && input.workflowVersion === 2 && role !== "planner") {
509
+ return { ok: false, error: "strict governed runs must start with role=planner" };
510
+ }
334
511
  const created = createOrLoadRun(input, options);
335
512
  if (!created.ok || !created.journal)
336
513
  return created;
@@ -338,6 +515,23 @@ function assignRunWorker(input, options = {}) {
338
515
  if (journal.stage === "completed" || journal.stage === "blocked") {
339
516
  return { ok: false, error: `run ${journal.runId} is already ${journal.stage}` };
340
517
  }
518
+ if (journal.workflowVersion === 2) {
519
+ if (!journal.cursor || !journal.budget || !journal.usage || !journal.designApplicability) {
520
+ return { ok: false, error: `run ${journal.runId} is missing strict workflow state` };
521
+ }
522
+ if (journal.cursor !== role) {
523
+ return {
524
+ ok: false,
525
+ error: `run ${journal.runId} expects role=${journal.cursor}; cannot assign ${role}`,
526
+ };
527
+ }
528
+ if (journal.workers[role]) {
529
+ return { ok: false, error: `role=${role} already has an assigned worker for attempt ${journal.attempt}` };
530
+ }
531
+ if (journal.usage.workerAssignments >= journal.budget.maxWorkerAssignments) {
532
+ return { ok: false, error: "run worker-assignment budget exhausted" };
533
+ }
534
+ }
341
535
  const fromStage = journal.stage;
342
536
  const timestamp = (options.now?.() ?? new Date()).toISOString();
343
537
  const preflight = {
@@ -376,6 +570,8 @@ function assignRunWorker(input, options = {}) {
376
570
  else if (!journal.executionWorkerIds.includes(workerId)) {
377
571
  journal.executionWorkerIds.push(workerId);
378
572
  }
573
+ if (journal.workflowVersion === 2 && journal.usage)
574
+ journal.usage.workerAssignments += 1;
379
575
  appendRunEvent(journal, {
380
576
  type: "worker_assigned",
381
577
  at: timestamp,
@@ -412,6 +608,115 @@ function assignRunWorker(input, options = {}) {
412
608
  saveRunJournal(journal, options);
413
609
  return { ok: true, journal };
414
610
  }
611
+ function requiredExecutionRoles(journal) {
612
+ return journal.designApplicability?.status === "required"
613
+ ? ["planner", "specialist", "qa", "design"]
614
+ : ["planner", "specialist", "qa"];
615
+ }
616
+ function moveToRetryOrBlocked(journal, timestamp, summary, workerId, role) {
617
+ const fromStage = journal.stage;
618
+ if (journal.attempt < journal.maxAttempts) {
619
+ journal.attempt += 1;
620
+ journal.stage = "retry_required";
621
+ journal.cursor = "planner";
622
+ journal.workers = {};
623
+ appendRunEvent(journal, {
624
+ type: "retry_required",
625
+ at: timestamp,
626
+ attempt: journal.attempt,
627
+ fromStage,
628
+ toStage: "retry_required",
629
+ role,
630
+ workerId,
631
+ summary,
632
+ });
633
+ }
634
+ else {
635
+ journal.stage = "blocked";
636
+ journal.cursor = "blocked";
637
+ journal.blockedAt = timestamp;
638
+ appendRunEvent(journal, {
639
+ type: "blocked",
640
+ at: timestamp,
641
+ fromStage,
642
+ toStage: "blocked",
643
+ role,
644
+ workerId,
645
+ summary,
646
+ });
647
+ }
648
+ }
649
+ /** Record a terminal planner/specialist/QA/design assertion and advance the fixed v2 cursor. */
650
+ function recordRunWorkerAssertion(input, options = {}) {
651
+ const journal = loadRunJournal(input.runId, options);
652
+ if (!journal)
653
+ return { ok: false, error: `run ${input.runId} was not found or is corrupt` };
654
+ if (journal.workflowVersion !== 2 || !journal.cursor || !journal.budget || !journal.usage) {
655
+ return { ok: false, error: "worker assertions require a strict v2 governed run" };
656
+ }
657
+ if (journal.stage === "completed" || journal.stage === "blocked") {
658
+ return { ok: false, error: `run ${journal.runId} is already ${journal.stage}` };
659
+ }
660
+ const role = input.role.trim().toLowerCase();
661
+ if (!isRunWorkerRole(role) || role === "reviewer") {
662
+ return { ok: false, error: "worker result role must be planner, specialist, qa, or design" };
663
+ }
664
+ if (journal.cursor !== role || journal.workers[role] !== input.workerId) {
665
+ return { ok: false, error: `worker result must match the current role=${journal.cursor} assignment` };
666
+ }
667
+ if (!input.workerTerminal)
668
+ return { ok: false, error: "worker must be terminal before recording its result" };
669
+ if (input.status === "passed" && !input.workerSucceeded) {
670
+ return { ok: false, error: "a stopped, failed, or non-zero worker cannot assert passed" };
671
+ }
672
+ const summary = boundedText(input.summary || "");
673
+ if (!summary)
674
+ return { ok: false, error: "worker result requires a non-empty summary" };
675
+ const timestamp = (options.now?.() ?? new Date()).toISOString();
676
+ const normalized = normalizeEvidence(input.evidence ?? [], input.workerId, role, timestamp);
677
+ if (normalized.error)
678
+ return { ok: false, error: normalized.error };
679
+ const evidence = normalized.evidence ?? [];
680
+ if (input.status === "passed" && evidence.length === 0) {
681
+ return { ok: false, error: "passed worker result requires typed evidence" };
682
+ }
683
+ if (journal.usage.evidenceItems + evidence.length > journal.budget.maxEvidenceItems) {
684
+ return { ok: false, error: "run evidence budget exhausted" };
685
+ }
686
+ const assertion = {
687
+ attempt: journal.attempt,
688
+ workerId: input.workerId,
689
+ role,
690
+ status: input.status,
691
+ summary,
692
+ evidence,
693
+ recordedAt: timestamp,
694
+ };
695
+ journal.workerAssertions = [...(journal.workerAssertions ?? []), assertion];
696
+ journal.evidence.push(...evidence);
697
+ journal.usage.evidenceItems += evidence.length;
698
+ appendRunEvent(journal, {
699
+ type: "worker_result",
700
+ at: timestamp,
701
+ fromStage: journal.stage,
702
+ toStage: journal.stage,
703
+ role,
704
+ workerId: input.workerId,
705
+ summary: `${input.status}: ${summary}`,
706
+ });
707
+ if (input.status === "failed") {
708
+ moveToRetryOrBlocked(journal, timestamp, summary, input.workerId, role);
709
+ }
710
+ else {
711
+ const roles = requiredExecutionRoles(journal);
712
+ const currentIndex = roles.indexOf(role);
713
+ journal.cursor = currentIndex === roles.length - 1 ? "reviewer" : roles[currentIndex + 1];
714
+ delete journal.workers[role];
715
+ }
716
+ journal.updatedAt = timestamp;
717
+ saveRunJournal(journal, options);
718
+ return { ok: true, journal };
719
+ }
415
720
  /** Apply a terminal review. Approval completes; rejection retries or blocks at the budget. */
416
721
  function completeRunReview(input, options = {}) {
417
722
  const journal = loadRunJournal(input.runId, options);
@@ -429,21 +734,78 @@ function completeRunReview(input, options = {}) {
429
734
  if (!input.reviewerTerminal) {
430
735
  return { ok: false, error: "reviewer must be terminal before complete_run" };
431
736
  }
737
+ if (journal.workflowVersion === 2 && !input.reviewerSucceeded) {
738
+ return { ok: false, error: "reviewer must exit successfully; stopped, failed, and non-zero reviewers cannot approve" };
739
+ }
432
740
  const summary = input.verdict.summary?.trim();
433
741
  if (!summary || !["approved", "changes_requested"].includes(input.verdict.status)) {
434
742
  return { ok: false, error: "verdict requires status=approved|changes_requested and a non-empty summary" };
435
743
  }
436
- const concreteEvidence = (input.evidence ?? []).map(String).map((item) => item.trim()).filter(Boolean);
437
- if (input.verdict.status === "approved" && concreteEvidence.length === 0) {
438
- return { ok: false, error: "approved verdict requires at least one non-empty concrete evidence item" };
439
- }
440
744
  const timestamp = (options.now?.() ?? new Date()).toISOString();
745
+ let concreteEvidence;
746
+ if (journal.workflowVersion === 2) {
747
+ if (journal.cursor !== "reviewer" || journal.stage !== "review") {
748
+ return { ok: false, error: `run ${journal.runId} is not ready for review (cursor=${journal.cursor})` };
749
+ }
750
+ const passedRoles = new Set((journal.workerAssertions ?? [])
751
+ .filter((assertion) => assertion.attempt === journal.attempt && assertion.status === "passed")
752
+ .map((assertion) => assertion.role));
753
+ const missing = requiredExecutionRoles(journal).filter((role) => !passedRoles.has(role));
754
+ if (missing.length)
755
+ return { ok: false, error: `review cannot complete; successful assertions missing: ${missing.join(", ")}` };
756
+ const normalized = normalizeEvidence(input.evidence ?? [], input.reviewerId, "reviewer", timestamp);
757
+ if (normalized.error)
758
+ return { ok: false, error: normalized.error };
759
+ concreteEvidence = normalized.evidence ?? [];
760
+ if (!journal.budget || !journal.usage)
761
+ return { ok: false, error: "run is missing budget state" };
762
+ if (journal.usage.evidenceItems + concreteEvidence.length > journal.budget.maxEvidenceItems) {
763
+ return { ok: false, error: "run evidence budget exhausted" };
764
+ }
765
+ const semantic = input.verdict.semanticSuccess;
766
+ if (!semantic || typeof semantic.met !== "boolean" || !boundedText(semantic.summary || "")) {
767
+ return { ok: false, error: "strict verdict requires semanticSuccess.met and a non-empty summary" };
768
+ }
769
+ if (input.verdict.status === "approved" && !semantic.met) {
770
+ return { ok: false, error: "approved verdict requires semanticSuccess.met=true" };
771
+ }
772
+ const issues = (input.verdict.issues ?? []).map(String).map((issue) => boundedText(issue)).filter(Boolean);
773
+ if (input.verdict.status === "approved" && issues.length > 0) {
774
+ return { ok: false, error: "approved verdict cannot carry unresolved issues" };
775
+ }
776
+ if (input.verdict.status === "changes_requested" && issues.length === 0) {
777
+ return { ok: false, error: "changes_requested verdict requires at least one actionable issue" };
778
+ }
779
+ if (input.verdict.status === "approved" && concreteEvidence.length === 0) {
780
+ return { ok: false, error: "approved verdict requires typed reviewer evidence" };
781
+ }
782
+ }
783
+ else {
784
+ concreteEvidence = (input.evidence ?? [])
785
+ .map(String)
786
+ .map((item) => item.trim())
787
+ .filter(Boolean)
788
+ .map((item) => ({
789
+ kind: "review",
790
+ source: `legacy:${input.reviewerId}`,
791
+ workerId: input.reviewerId,
792
+ role: "reviewer",
793
+ summary: boundedText(item),
794
+ recordedAt: timestamp,
795
+ }));
796
+ if (input.verdict.status === "approved" && concreteEvidence.length === 0) {
797
+ return { ok: false, error: "approved verdict requires at least one non-empty concrete evidence item" };
798
+ }
799
+ }
441
800
  const fromStage = journal.stage;
442
801
  const verdict = {
443
802
  status: input.verdict.status,
444
803
  reviewerId: input.reviewerId,
445
804
  summary,
446
- issues: (input.verdict.issues ?? []).map(String).map((issue) => issue.trim()).filter(Boolean),
805
+ issues: (input.verdict.issues ?? []).map(String).map((issue) => boundedText(issue)).filter(Boolean),
806
+ semanticSuccess: input.verdict.semanticSuccess
807
+ ? { met: input.verdict.semanticSuccess.met, summary: boundedText(input.verdict.semanticSuccess.summary) }
808
+ : { met: input.verdict.status === "approved", summary: "legacy reviewer verdict" },
447
809
  recordedAt: timestamp,
448
810
  };
449
811
  journal.reviewerVerdict = verdict;
@@ -456,16 +818,12 @@ function completeRunReview(input, options = {}) {
456
818
  workerId: input.reviewerId,
457
819
  summary: `${verdict.status}: ${verdict.summary}`,
458
820
  });
459
- for (const evidence of concreteEvidence) {
460
- journal.evidence.push({
461
- workerId: input.reviewerId,
462
- role: "reviewer",
463
- summary: evidence,
464
- recordedAt: timestamp,
465
- });
466
- }
821
+ journal.evidence.push(...concreteEvidence);
822
+ if (journal.workflowVersion === 2 && journal.usage)
823
+ journal.usage.evidenceItems += concreteEvidence.length;
467
824
  if (verdict.status === "approved") {
468
825
  journal.stage = "completed";
826
+ journal.cursor = "done";
469
827
  journal.completedAt = timestamp;
470
828
  appendRunEvent(journal, {
471
829
  type: "completed",
@@ -477,32 +835,24 @@ function completeRunReview(input, options = {}) {
477
835
  summary: verdict.summary,
478
836
  });
479
837
  }
838
+ else if (journal.workflowVersion === 2) {
839
+ moveToRetryOrBlocked(journal, timestamp, verdict.summary, input.reviewerId, "reviewer");
840
+ }
480
841
  else if (journal.attempt < journal.maxAttempts) {
481
842
  journal.attempt += 1;
482
843
  journal.stage = "retry_required";
483
844
  delete journal.workers.reviewer;
484
845
  appendRunEvent(journal, {
485
- type: "retry_required",
486
- at: timestamp,
487
- attempt: journal.attempt,
488
- fromStage,
489
- toStage: "retry_required",
490
- role: "reviewer",
491
- workerId: input.reviewerId,
492
- summary: verdict.summary,
846
+ type: "retry_required", at: timestamp, attempt: journal.attempt, fromStage,
847
+ toStage: "retry_required", role: "reviewer", workerId: input.reviewerId, summary: verdict.summary,
493
848
  });
494
849
  }
495
850
  else {
496
851
  journal.stage = "blocked";
497
852
  journal.blockedAt = timestamp;
498
853
  appendRunEvent(journal, {
499
- type: "blocked",
500
- at: timestamp,
501
- fromStage,
502
- toStage: "blocked",
503
- role: "reviewer",
504
- workerId: input.reviewerId,
505
- summary: verdict.summary,
854
+ type: "blocked", at: timestamp, fromStage, toStage: "blocked", role: "reviewer",
855
+ workerId: input.reviewerId, summary: verdict.summary,
506
856
  });
507
857
  }
508
858
  journal.updatedAt = timestamp;
@@ -537,7 +887,9 @@ function findBoundUnfinishedRunIds(binding, options = {}) {
537
887
  const bound = taskId ? journal.taskId === taskId : Boolean(sessionId && journal.sessionId === sessionId);
538
888
  if (!bound)
539
889
  continue;
540
- if (journal.stage === "completed" && journal.reviewerVerdict?.status === "approved")
890
+ if (journal.stage === "completed" &&
891
+ journal.reviewerVerdict?.status === "approved" &&
892
+ (journal.workflowVersion !== 2 || journal.reviewerVerdict.semanticSuccess.met))
541
893
  continue;
542
894
  matches.push(runId);
543
895
  }
@@ -549,7 +901,9 @@ function runFinishBlocker(runIds, options = {}) {
549
901
  const journal = loadRunJournal(runId, options);
550
902
  if (!journal)
551
903
  return `governed run ${runId} is missing or corrupt`;
552
- if (journal.stage !== "completed" || journal.reviewerVerdict?.status !== "approved") {
904
+ if (journal.stage !== "completed" ||
905
+ journal.reviewerVerdict?.status !== "approved" ||
906
+ (journal.workflowVersion === 2 && !journal.reviewerVerdict.semanticSuccess.met)) {
553
907
  return `governed run ${runId} is ${journal.stage} (attempt ${journal.attempt}/${journal.maxAttempts}); a distinct terminal reviewer must approve it through complete_run`;
554
908
  }
555
909
  }