mandrel 2.66.0 → 2.68.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 (85) hide show
  1. package/.agents/agents/acceptance-critic.md +2 -2
  2. package/.agents/agents/story-worker.md +15 -11
  3. package/.agents/docs/agentrc-reference.json +5 -2
  4. package/.agents/docs/configuration.md +38 -2
  5. package/.agents/docs/workflows.md +4 -2
  6. package/.agents/instructions.md +2 -1
  7. package/.agents/rules/git-conventions-reference.md +5 -5
  8. package/.agents/rules/git-conventions.md +1 -1
  9. package/.agents/schemas/agentrc.schema.json +20 -2
  10. package/.agents/schemas/story-deliver-terminal.schema.json +23 -1
  11. package/.agents/schemas/validation-evidence.schema.json +3 -1
  12. package/.agents/scripts/boot-sweep.js +97 -9
  13. package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
  14. package/.agents/scripts/clean-temp.js +54 -0
  15. package/.agents/scripts/clean-worktrees.js +593 -0
  16. package/.agents/scripts/coverage-capture.js +65 -9
  17. package/.agents/scripts/drain-pending-cleanup.js +5 -4
  18. package/.agents/scripts/evidence-gate.js +106 -8
  19. package/.agents/scripts/lib/baselines/coverage-refresh-scope.js +60 -0
  20. package/.agents/scripts/lib/baselines/crap-updater-cli.js +101 -4
  21. package/.agents/scripts/lib/baselines/refresh-service.js +1 -1
  22. package/.agents/scripts/lib/baselines/seat-missing.js +228 -0
  23. package/.agents/scripts/lib/child-exec.js +39 -1
  24. package/.agents/scripts/lib/clean-temp.js +440 -0
  25. package/.agents/scripts/lib/close-validation/gates.js +59 -19
  26. package/.agents/scripts/lib/close-validation/process.js +23 -24
  27. package/.agents/scripts/lib/close-validation/runner.js +71 -40
  28. package/.agents/scripts/lib/config/gates/coverage.schema.js +21 -0
  29. package/.agents/scripts/lib/config/quality.js +7 -1
  30. package/.agents/scripts/lib/config/temp-paths.js +15 -0
  31. package/.agents/scripts/lib/config-settings-schema-delivery.js +12 -3
  32. package/.agents/scripts/lib/coverage-baseline.js +78 -5
  33. package/.agents/scripts/lib/coverage-capture-affected.js +345 -0
  34. package/.agents/scripts/lib/coverage-capture-delta.js +180 -0
  35. package/.agents/scripts/lib/coverage-capture-fullscope.js +53 -32
  36. package/.agents/scripts/lib/coverage-capture-incremental.js +49 -26
  37. package/.agents/scripts/lib/coverage-capture-usage.js +1 -1
  38. package/.agents/scripts/lib/coverage-capture.js +121 -81
  39. package/.agents/scripts/lib/full-suite-lock.js +49 -46
  40. package/.agents/scripts/lib/full-suite-queue.js +83 -8
  41. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  42. package/.agents/scripts/lib/observability/source-classifier.js +4 -1
  43. package/.agents/scripts/lib/orchestration/code-review.js +15 -3
  44. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  45. package/.agents/scripts/lib/orchestration/merge-poll.js +5 -0
  46. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
  47. package/.agents/scripts/lib/orchestration/review-deposit.js +219 -0
  48. package/.agents/scripts/lib/orchestration/review-providers/code-review.js +11 -7
  49. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +29 -10
  50. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +124 -73
  51. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +38 -20
  52. package/.agents/scripts/lib/orchestration/single-story-close/phases/lock-wait-pending.js +8 -2
  53. package/.agents/scripts/lib/orchestration/single-story-close/review-overlap.js +161 -0
  54. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +47 -7
  55. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +8 -16
  56. package/.agents/scripts/lib/process-group.js +1 -1
  57. package/.agents/scripts/lib/single-story-sweep.js +2 -2
  58. package/.agents/scripts/lib/supervised-suite.js +247 -0
  59. package/.agents/scripts/lib/temp-removal.js +110 -0
  60. package/.agents/scripts/lib/temp-retention.js +122 -73
  61. package/.agents/scripts/lib/wave-runner/cross-run-overlap.js +120 -0
  62. package/.agents/scripts/lib/wave-runner/live-probe.js +5 -1
  63. package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
  64. package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
  65. package/.agents/scripts/quality-preview.js +112 -14
  66. package/.agents/scripts/single-story-init.js +120 -17
  67. package/.agents/scripts/stories-wave-tick.js +47 -0
  68. package/.agents/scripts/story-review-compute.js +207 -0
  69. package/.agents/scripts/update-coverage-baseline.js +15 -10
  70. package/.agents/scripts/update-crap-baseline.js +12 -2
  71. package/.agents/scripts/update-maintainability-baseline.js +12 -2
  72. package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
  73. package/.agents/workflows/clean-temp.md +67 -0
  74. package/.agents/workflows/clean-worktrees.md +63 -0
  75. package/.agents/workflows/git-deliver.md +1 -1
  76. package/.agents/workflows/helpers/acceptance-self-eval.md +3 -2
  77. package/.agents/workflows/helpers/code-review.md +7 -5
  78. package/.agents/workflows/helpers/deliver-digest.md +39 -36
  79. package/.agents/workflows/helpers/deliver-reference.md +115 -5
  80. package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
  81. package/.agents/workflows/helpers/deliver-story.md +2 -1
  82. package/docs/CHANGELOG.md +39 -0
  83. package/lib/cli/registry.js +125 -18
  84. package/lib/migrations/steps/strip-removed-agentrc-keys.js +0 -5
  85. package/package.json +1 -1
@@ -131,18 +131,84 @@ export function isFirstInLine({
131
131
  return true;
132
132
  }
133
133
 
134
+ /**
135
+ * @typedef {{ ownerId: string|null, pid: number|null, ageSeconds: number|null }} LockHolder
136
+ */
137
+
138
+ /**
139
+ * Lockfile lines: owner id, creation stamp, pid. Unreadable parts are null.
140
+ *
141
+ * @param {string} lockPath
142
+ * @param {{ fsImpl?: object, nowFn?: () => number }} [opts]
143
+ * @returns {LockHolder}
144
+ */
145
+ export function readLockHolder(
146
+ lockPath,
147
+ { fsImpl = fs, nowFn = Date.now } = {},
148
+ ) {
149
+ let lines = [];
150
+ try {
151
+ lines = String(fsImpl.readFileSync(lockPath, 'utf8')).split('\n');
152
+ } catch {
153
+ // No lockfile.
154
+ }
155
+ const created = Date.parse(lines[1] ?? '');
156
+ return {
157
+ ownerId: lines[0] ? lines[0] : null,
158
+ pid: readLockHolderPid(lockPath, fsImpl),
159
+ ageSeconds: Number.isFinite(created)
160
+ ? Math.max(0, Math.round((nowFn() - created) / 1000))
161
+ : null,
162
+ };
163
+ }
164
+
165
+ /** @param {LockHolder} holder */
166
+ function describeHolder({ ownerId, pid, ageSeconds }) {
167
+ return `holder ${ownerId ?? 'unknown'}, pid ${pid ?? 'unknown'}, lock age ${ageSeconds ?? 'unknown'}s`;
168
+ }
169
+
170
+ const HOLDER_RE =
171
+ /holder ([^,\s]+), pid (\d+|unknown), lock age (\d+|unknown)s/u;
172
+
134
173
  /**
135
174
  * Log lines are the one channel that reaches close from a child capture.
136
175
  *
137
176
  * @param {string} line
138
- * @returns {{ waitedSeconds: number, expired: boolean }|null}
177
+ * @returns {{ waitedSeconds: number, expired: boolean, holder?: LockHolder }|null}
139
178
  */
140
179
  export function parseLockWaitOutcome(line) {
180
+ const text = String(line ?? '');
141
181
  const match = /\[full-suite-lock\] (✅|⌛) [^\n]*\(waited (\d+)s[,)]/u.exec(
142
- String(line ?? ''),
182
+ text,
143
183
  );
144
184
  if (!match) return null;
145
- return { waitedSeconds: Number(match[2]), expired: match[1] === '⌛' };
185
+ return withHolder(
186
+ { waitedSeconds: Number(match[2]), expired: match[1] === '⌛' },
187
+ text,
188
+ );
189
+ }
190
+
191
+ /** An expiry line also names the holder it gave up on. */
192
+ function withHolder(outcome, text) {
193
+ const holder = outcome.expired ? HOLDER_RE.exec(text) : null;
194
+ if (!holder) return outcome;
195
+ const [, ownerId, pid, ageSeconds] = holder.map(unknownAsNull);
196
+ return {
197
+ ...outcome,
198
+ holder: {
199
+ ownerId,
200
+ pid: numberOrNull(pid),
201
+ ageSeconds: numberOrNull(ageSeconds),
202
+ },
203
+ };
204
+ }
205
+
206
+ function unknownAsNull(raw) {
207
+ return raw === 'unknown' ? null : raw;
208
+ }
209
+
210
+ function numberOrNull(raw) {
211
+ return raw === null ? null : Number(raw);
146
212
  }
147
213
 
148
214
  function holderLabel(lockPath, fsImpl) {
@@ -154,9 +220,9 @@ function holderLabel(lockPath, fsImpl) {
154
220
  * lockPath: string, waitMs: number, pollMs: number, staleMs: number,
155
221
  * reportMs: number, fsImpl: object, nowFn: () => number,
156
222
  * sleepFn: (ms: number) => Promise<void>, acquireOnceFn: Function,
157
- * log: (m: string) => void, expiryNote: string,
223
+ * log: (m: string) => void, rerunCommand: string,
158
224
  * }} opts
159
- * @returns {Promise<{ held: object|null, expired: boolean, waited: true }>}
225
+ * @returns {Promise<{ held: object|null, expired: boolean, waited: true, waitedMs: number, holder?: LockHolder }>}
160
226
  */
161
227
  export async function waitInLine(opts) {
162
228
  const { lockPath, waitMs, fsImpl, nowFn, log } = opts;
@@ -164,6 +230,7 @@ export async function waitInLine(opts) {
164
230
  const clock = {
165
231
  deadline: startedAt + Math.max(0, waitMs),
166
232
  nextReport: startedAt + opts.reportMs,
233
+ waitedMs: () => Math.max(0, nowFn() - startedAt),
167
234
  waited: () => Math.round((nowFn() - startedAt) / 1000),
168
235
  };
169
236
  log(
@@ -172,14 +239,22 @@ export async function waitInLine(opts) {
172
239
  const ticket = enqueueWaiter({ lockPath, nowFn, fsImpl });
173
240
  try {
174
241
  const outcome = await pollForTurn(opts, ticket, clock);
175
- if (outcome) return { ...outcome, waited: true };
242
+ if (outcome)
243
+ return { ...outcome, waited: true, waitedMs: clock.waitedMs() };
176
244
  } finally {
177
245
  dequeueWaiter(ticket, { fsImpl });
178
246
  }
247
+ const holder = readLockHolder(lockPath, { fsImpl, nowFn });
179
248
  log(
180
- `${LOCK_TAG} ⌛ gave up waiting for the full-suite lock (waited ${clock.waited()}s, ${holderLabel(lockPath, fsImpl)}) — ${opts.expiryNote}.`,
249
+ `${LOCK_TAG} ⌛ gave up waiting for the full-suite lock (waited ${clock.waited()}s, ${describeHolder(holder)}) — not spawning, so two full suites never overlap. Re-run once it finishes: ${opts.rerunCommand}`,
181
250
  );
182
- return { held: null, expired: true, waited: true };
251
+ return {
252
+ held: null,
253
+ expired: true,
254
+ waited: true,
255
+ waitedMs: clock.waitedMs(),
256
+ holder,
257
+ };
183
258
  }
184
259
 
185
260
  /** `null` means the deadline passed. */