mandrel 1.92.0 → 1.94.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 (144) hide show
  1. package/.agents/agents/acceptance-critic.md +129 -0
  2. package/.agents/agents/retro.md +42 -0
  3. package/.agents/agents/story-worker.md +162 -0
  4. package/.agents/docs/configuration.md +7 -1
  5. package/.agents/docs/execution-reference.md +27 -2
  6. package/.agents/instructions.md +43 -33
  7. package/.agents/personas/engineer.md +26 -112
  8. package/.agents/personas/security-engineer.md +1 -2
  9. package/.agents/rules/git-conventions-reference.md +225 -0
  10. package/.agents/rules/git-conventions.md +25 -200
  11. package/.agents/rules/security-baseline.md +5 -0
  12. package/.agents/rules/testing-standards.md +106 -13
  13. package/.agents/schemas/agentrc.schema.json +31 -1
  14. package/.agents/schemas/lifecycle/slice.end.schema.json +21 -0
  15. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +20 -0
  16. package/.agents/schemas/lifecycle/slice.start.schema.json +17 -0
  17. package/.agents/scripts/acceptance-eval.js +62 -18
  18. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  19. package/.agents/scripts/bookkeeping-reconcile.js +117 -0
  20. package/.agents/scripts/check-context-budget.js +62 -5
  21. package/.agents/scripts/diagnose-friction.js +0 -6
  22. package/.agents/scripts/epic-deliver-prepare.js +272 -10
  23. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +56 -18
  24. package/.agents/scripts/lib/close-validation/gates.js +159 -21
  25. package/.agents/scripts/lib/config/acceptance-eval.js +52 -5
  26. package/.agents/scripts/lib/config/delivery-routing.js +87 -0
  27. package/.agents/scripts/lib/config/explain.js +2 -0
  28. package/.agents/scripts/lib/config-resolver.js +1 -1
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +37 -3
  30. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -0
  31. package/.agents/scripts/lib/doc-tiers.js +37 -2
  32. package/.agents/scripts/lib/observability/active-story-env.js +111 -2
  33. package/.agents/scripts/lib/observability/hook-heartbeat.js +219 -0
  34. package/.agents/scripts/lib/observability/tool-trace-hook.js +15 -4
  35. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +111 -0
  36. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +32 -4
  37. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +270 -0
  38. package/.agents/scripts/lib/orchestration/ceremony-routing.js +141 -0
  39. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -124
  40. package/.agents/scripts/lib/orchestration/deliver-route.js +173 -0
  41. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +1 -1
  42. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +233 -0
  43. package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
  44. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +270 -0
  45. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +83 -2
  46. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +6 -0
  47. package/.agents/scripts/lib/orchestration/plan-context.js +189 -3
  48. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +3 -2
  49. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +38 -1
  51. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +16 -1
  52. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -0
  53. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +1 -0
  54. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -2
  55. package/.agents/scripts/lib/provider-factory.js +1 -1
  56. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  57. package/.agents/scripts/plan-context.js +28 -10
  58. package/.agents/scripts/post-structured-comment.js +38 -0
  59. package/.agents/scripts/slice-phase.js +361 -0
  60. package/.agents/scripts/sync-claude-agents.js +165 -0
  61. package/.agents/scripts/update-ticket-state.js +31 -0
  62. package/.agents/scripts/wave-tick.js +138 -9
  63. package/.agents/skills/core/api-and-interface-design/SKILL.md +5 -3
  64. package/.agents/skills/core/code-review-and-quality/SKILL.md +63 -7
  65. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +1 -1
  66. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +5 -5
  67. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +8 -8
  68. package/.agents/skills/core/epic-plan-premortem/SKILL.md +4 -4
  69. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +26 -56
  70. package/.agents/skills/core/gates-and-baselines/SKILL.md +149 -0
  71. package/.agents/skills/core/idea-refinement/SKILL.md +2 -8
  72. package/.agents/skills/core/qa-coverage-mapping/SKILL.md +7 -7
  73. package/.agents/skills/skills.index.json +11 -381
  74. package/.agents/workflows/deliver.md +47 -4
  75. package/.agents/workflows/helpers/acceptance-self-eval.md +38 -13
  76. package/.agents/workflows/helpers/deliver-epic-reference.md +18 -5
  77. package/.agents/workflows/helpers/deliver-epic-single.md +331 -0
  78. package/.agents/workflows/helpers/deliver-epic.md +51 -8
  79. package/.agents/workflows/helpers/deliver-stories.md +15 -5
  80. package/.agents/workflows/helpers/epic-deliver-story.md +12 -3
  81. package/.agents/workflows/helpers/mandrel-sync-config.md +1 -1
  82. package/.agents/workflows/helpers/plan-epic-reference.md +19 -8
  83. package/.agents/workflows/helpers/plan-epic.md +95 -27
  84. package/.agents/workflows/helpers/scope-triage-gate.md +9 -0
  85. package/.agents/workflows/mandrel-update.md +1 -1
  86. package/.agents/workflows/plan.md +16 -4
  87. package/docs/CHANGELOG.md +23 -0
  88. package/lib/cli/registry.js +95 -0
  89. package/package.json +4 -2
  90. package/.agents/personas/engineer-mobile.md +0 -120
  91. package/.agents/personas/engineer-web.md +0 -111
  92. package/.agents/personas/product.md +0 -94
  93. package/.agents/personas/refactorer.md +0 -113
  94. package/.agents/personas/sre.md +0 -86
  95. package/.agents/personas/ux-designer.md +0 -95
  96. package/.agents/scripts/epic-plan-decompose.js +0 -54
  97. package/.agents/scripts/epic-plan-spec.js +0 -64
  98. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +0 -109
  99. package/.agents/scripts/plan-critics.js +0 -227
  100. package/.agents/skills/core/baseline-refresh/SKILL.md +0 -181
  101. package/.agents/skills/core/ci-cd-and-automation/SKILL.md +0 -274
  102. package/.agents/skills/core/ci-cd-and-automation/examples.md +0 -211
  103. package/.agents/skills/core/code-simplification/SKILL.md +0 -389
  104. package/.agents/skills/core/context-engineering/SKILL.md +0 -309
  105. package/.agents/skills/core/context-engineering/examples.md +0 -58
  106. package/.agents/skills/core/deprecation-and-migration/SKILL.md +0 -250
  107. package/.agents/skills/core/frontend-ui-engineering/SKILL.md +0 -357
  108. package/.agents/skills/core/hydrate-context/SKILL.md +0 -123
  109. package/.agents/skills/core/idea-refinement/examples.md +0 -437
  110. package/.agents/skills/core/idea-refinement/frameworks.md +0 -135
  111. package/.agents/skills/core/incremental-implementation/SKILL.md +0 -271
  112. package/.agents/skills/core/introducing-a-baseline-gate/SKILL.md +0 -213
  113. package/.agents/skills/core/knowledge-transfer/SKILL.md +0 -180
  114. package/.agents/skills/core/mutation-survivor-remediation/SKILL.md +0 -117
  115. package/.agents/skills/core/performance-optimization/SKILL.md +0 -314
  116. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +0 -277
  117. package/.agents/skills/core/property-based-testing/SKILL.md +0 -148
  118. package/.agents/skills/core/refactoring-discipline/SKILL.md +0 -111
  119. package/.agents/skills/core/shipping-and-launch/SKILL.md +0 -328
  120. package/.agents/skills/core/spec-driven-development/SKILL.md +0 -252
  121. package/.agents/skills/core/test-driven-development/SKILL.md +0 -475
  122. package/.agents/skills/core/using-agent-skills/SKILL.md +0 -232
  123. package/.agents/skills/stack/architecture/monorepo-path-strategist/SKILL.md +0 -31
  124. package/.agents/skills/stack/architecture/structured-output-zod/SKILL.md +0 -51
  125. package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +0 -76
  126. package/.agents/skills/stack/backend/cloudflare-hono-architect/SKILL.md +0 -31
  127. package/.agents/skills/stack/backend/cloudflare-hono-architect/examples/route-template.ts +0 -33
  128. package/.agents/skills/stack/backend/cloudflare-queue-manager/SKILL.md +0 -31
  129. package/.agents/skills/stack/backend/cloudflare-workers/SKILL.md +0 -51
  130. package/.agents/skills/stack/backend/highlevel-crm/SKILL.md +0 -54
  131. package/.agents/skills/stack/backend/sqlite-drizzle-expert/SKILL.md +0 -29
  132. package/.agents/skills/stack/backend/sqlite-drizzle-expert/examples/schema-template.ts +0 -30
  133. package/.agents/skills/stack/backend/stripe-integration/SKILL.md +0 -57
  134. package/.agents/skills/stack/backend/stripe-integration/scripts/listen-stripe.sh +0 -9
  135. package/.agents/skills/stack/backend/turso-sqlite/SKILL.md +0 -48
  136. package/.agents/skills/stack/frontend/astro/SKILL.md +0 -62
  137. package/.agents/skills/stack/frontend/astro-react-island-strategist/SKILL.md +0 -30
  138. package/.agents/skills/stack/frontend/expo-react-native-developer/SKILL.md +0 -29
  139. package/.agents/skills/stack/frontend/google-analytics-v4/SKILL.md +0 -50
  140. package/.agents/skills/stack/frontend/tailwind-v4/SKILL.md +0 -58
  141. package/.agents/skills/stack/frontend/ui-accessibility-engineer/SKILL.md +0 -34
  142. package/.agents/skills/stack/qa/audit-accessibility/SKILL.md +0 -51
  143. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +0 -199
  144. package/.agents/skills/stack/security/backend-security-patterns/SKILL.md +0 -68
@@ -0,0 +1,270 @@
1
+ /**
2
+ * bookkeeping-outbox.js — Epic #4476 (M5): buffered GitHub bookkeeping.
3
+ *
4
+ * Delivery bookkeeping — structured-comment upserts (progress, friction,
5
+ * wave-stall, …) and `agent::*` label flips — is human-visible surface that,
6
+ * in an **unattended** (`--yes` / headless) run, no operator is watching in
7
+ * real time. Emitting each one as a live GitHub round-trip mid-run costs an
8
+ * LLM turn per transition for a surface nobody reads until the run finishes.
9
+ *
10
+ * This module lets those non-urgent mutations be **buffered to a local NDJSON
11
+ * outbox** during the run and **reconciled to GitHub once at finalize**.
12
+ * GitHub stays the source of truth *at rest* (post-reconcile); only the
13
+ * per-transition chatter moves off the token stream. The outbox file survives
14
+ * a crash, so crash recovery drains whatever was buffered before the finalize
15
+ * reconcile ran.
16
+ *
17
+ * ## What is NEVER buffered
18
+ * The `agent::blocked` HITL gate (`.agents/instructions.md` §1.J) is the single
19
+ * authoritative runtime pause point. A genuine blocker MUST surface on GitHub
20
+ * **immediately**, not batched to finalize — an operator can only resume a run
21
+ * they can see is blocked. The {@link transitionStateOrBuffer} facade forces
22
+ * `agent::blocked` (and any caller-marked `urgent`) transition through the
23
+ * live path regardless of headless mode.
24
+ *
25
+ * ## Attended runs are unchanged
26
+ * Buffering is gated on the explicit `headless` signal (Story #4427's
27
+ * `--headless`/`--yes` plumbing). An attended run passes `headless: false` and
28
+ * every comment/label posts live exactly as before — byte-for-byte behaviour.
29
+ */
30
+
31
+ import {
32
+ appendFileSync,
33
+ existsSync,
34
+ mkdirSync,
35
+ readFileSync,
36
+ writeFileSync,
37
+ } from 'node:fs';
38
+ import path from 'node:path';
39
+
40
+ import { epicTempDir } from '../config/temp-paths.js';
41
+ import { STATE_LABELS } from './ticketing/reads.js';
42
+ import { upsertStructuredComment } from './ticketing/state.js';
43
+ import { transitionTicketState } from './ticketing/transition.js';
44
+
45
+ /** Canonical basename for the per-Epic bookkeeping outbox. */
46
+ const OUTBOX_BASENAME = 'bookkeeping-outbox.ndjson';
47
+
48
+ /**
49
+ * Resolve the canonical outbox path for an Epic:
50
+ * `temp/epic-<id>/bookkeeping-outbox.ndjson`.
51
+ *
52
+ * @param {number} epicId
53
+ * @param {object} [config] Resolved config (tempRoot).
54
+ * @returns {string}
55
+ */
56
+ export function outboxPathFor(epicId, config) {
57
+ return path.join(epicTempDir(epicId, config), OUTBOX_BASENAME);
58
+ }
59
+
60
+ /**
61
+ * Append one operation record to the outbox, creating the parent dir on
62
+ * demand. Best-effort at the storage layer is the caller's concern; here we
63
+ * let a genuine fs failure propagate so a mis-configured outbox path surfaces
64
+ * loudly in tests.
65
+ *
66
+ * @param {string} outboxPath
67
+ * @param {object} op
68
+ */
69
+ function appendOp(outboxPath, op) {
70
+ mkdirSync(path.dirname(outboxPath), { recursive: true });
71
+ appendFileSync(outboxPath, `${JSON.stringify(op)}\n`, 'utf8');
72
+ }
73
+
74
+ /**
75
+ * Buffer a structured-comment upsert. The op captures everything
76
+ * `upsertStructuredComment` needs at drain time so the reconcile is a pure
77
+ * replay with no re-derivation.
78
+ *
79
+ * @param {{ outboxPath: string, ticketId: number, marker: string,
80
+ * body: string, attrs?: Record<string, string|number>|null,
81
+ * ts?: string }} args
82
+ */
83
+ export function enqueueComment({
84
+ outboxPath,
85
+ ticketId,
86
+ marker,
87
+ body,
88
+ attrs = null,
89
+ ts = new Date().toISOString(),
90
+ }) {
91
+ appendOp(outboxPath, {
92
+ kind: 'comment',
93
+ ts,
94
+ ticketId,
95
+ marker,
96
+ body,
97
+ ...(attrs ? { attrs } : {}),
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Buffer an `agent::*` label transition.
103
+ *
104
+ * @param {{ outboxPath: string, ticketId: number, state: string,
105
+ * ts?: string }} args
106
+ */
107
+ export function enqueueLabel({
108
+ outboxPath,
109
+ ticketId,
110
+ state,
111
+ ts = new Date().toISOString(),
112
+ }) {
113
+ appendOp(outboxPath, { kind: 'label', ts, ticketId, state });
114
+ }
115
+
116
+ /**
117
+ * Parse the outbox into an ordered array of operation records. Malformed
118
+ * lines are skipped (never throw). A missing / empty outbox yields `[]`.
119
+ *
120
+ * @param {string} outboxPath
121
+ * @returns {Array<object>}
122
+ */
123
+ export function readOutbox(outboxPath) {
124
+ if (!outboxPath || !existsSync(outboxPath)) return [];
125
+ let raw;
126
+ try {
127
+ raw = readFileSync(outboxPath, 'utf8');
128
+ } catch {
129
+ return [];
130
+ }
131
+ if (!raw) return [];
132
+ const ops = [];
133
+ for (const line of raw.split(/\r?\n/)) {
134
+ if (!line) continue;
135
+ try {
136
+ const op = JSON.parse(line);
137
+ if (op && typeof op === 'object') ops.push(op);
138
+ } catch {
139
+ // skip a torn / malformed line — a partial write from a crash is not
140
+ // fatal to the rest of the batch.
141
+ }
142
+ }
143
+ return ops;
144
+ }
145
+
146
+ /**
147
+ * Drain the outbox to GitHub in FIFO order and clear it on success. Both sink
148
+ * operations are idempotent (comment upserts are marker-scoped;
149
+ * `transitionTicketState` applies one state via the canonical
150
+ * remove-all-then-add path), so a re-run after a partial drain converges. A
151
+ * per-op failure is recorded and the drain continues — a single bad ticket
152
+ * must not strand the rest of the batch. The outbox file is only truncated
153
+ * when EVERY op succeeded, so a crash mid-drain leaves the un-applied
154
+ * remainder for the next reconcile.
155
+ *
156
+ * @param {{ outboxPath: string,
157
+ * provider: import('../ITicketingProvider.js').ITicketingProvider,
158
+ * logger?: { warn?: (m: string) => void } }} args
159
+ * @returns {Promise<{ drained: number, comments: number, labels: number,
160
+ * errors: Array<{ op: object, error: string }>, cleared: boolean }>}
161
+ */
162
+ export async function reconcileOutbox({ outboxPath, provider, logger }) {
163
+ const ops = readOutbox(outboxPath);
164
+ const result = {
165
+ drained: 0,
166
+ comments: 0,
167
+ labels: 0,
168
+ errors: [],
169
+ cleared: false,
170
+ };
171
+ if (ops.length === 0) {
172
+ result.cleared = true;
173
+ return result;
174
+ }
175
+
176
+ for (const op of ops) {
177
+ try {
178
+ if (op.kind === 'comment') {
179
+ await upsertStructuredComment(
180
+ provider,
181
+ op.ticketId,
182
+ op.marker,
183
+ op.body,
184
+ op.attrs ?? null,
185
+ );
186
+ result.comments += 1;
187
+ result.drained += 1;
188
+ } else if (op.kind === 'label') {
189
+ await transitionTicketState(provider, op.ticketId, op.state);
190
+ result.labels += 1;
191
+ result.drained += 1;
192
+ } else {
193
+ result.errors.push({ op, error: `unknown op kind "${op.kind}"` });
194
+ }
195
+ } catch (err) {
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ result.errors.push({ op, error: message });
198
+ logger?.warn?.(
199
+ `[bookkeeping-outbox] reconcile op failed (${op.kind} #${op.ticketId}): ${message}`,
200
+ );
201
+ }
202
+ }
203
+
204
+ // Only truncate when the whole batch landed — otherwise the un-applied
205
+ // ops must survive for the next reconcile (crash-recovery contract).
206
+ if (result.errors.length === 0) {
207
+ try {
208
+ writeFileSync(outboxPath, '', 'utf8');
209
+ result.cleared = true;
210
+ } catch {
211
+ // A clear failure is non-fatal: the ops all landed, and the idempotent
212
+ // sinks make a redundant re-drain a no-op.
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+
218
+ /**
219
+ * Facade: post a structured comment live, OR buffer it to the outbox when the
220
+ * run is headless. Comments are never urgent (the operator reads them at
221
+ * finalize), so headless always buffers when an `outboxPath` is available.
222
+ *
223
+ * @param {{ provider: object, ticketId: number, marker: string, body: string,
224
+ * attrs?: Record<string, string|number>|null, headless?: boolean,
225
+ * outboxPath?: string|null }} args
226
+ * @returns {Promise<{ buffered: boolean }>}
227
+ */
228
+ export async function postCommentOrBuffer({
229
+ provider,
230
+ ticketId,
231
+ marker,
232
+ body,
233
+ attrs = null,
234
+ headless = false,
235
+ outboxPath = null,
236
+ }) {
237
+ if (headless && outboxPath) {
238
+ enqueueComment({ outboxPath, ticketId, marker, body, attrs });
239
+ return { buffered: true };
240
+ }
241
+ await upsertStructuredComment(provider, ticketId, marker, body, attrs);
242
+ return { buffered: false };
243
+ }
244
+
245
+ /**
246
+ * Facade: flip an `agent::*` state live, OR buffer it when headless — EXCEPT
247
+ * `agent::blocked` (and any caller-marked `urgent` flip), which ALWAYS goes
248
+ * live so the HITL gate surfaces immediately (§1.J).
249
+ *
250
+ * @param {{ provider: object, ticketId: number, state: string,
251
+ * headless?: boolean, outboxPath?: string|null,
252
+ * urgent?: boolean }} args
253
+ * @returns {Promise<{ buffered: boolean }>}
254
+ */
255
+ export async function transitionStateOrBuffer({
256
+ provider,
257
+ ticketId,
258
+ state,
259
+ headless = false,
260
+ outboxPath = null,
261
+ urgent = false,
262
+ }) {
263
+ const mustSurfaceNow = urgent || state === STATE_LABELS.BLOCKED;
264
+ if (headless && outboxPath && !mustSurfaceNow) {
265
+ enqueueLabel({ outboxPath, ticketId, state });
266
+ return { buffered: true };
267
+ }
268
+ await transitionTicketState(provider, ticketId, state);
269
+ return { buffered: false };
270
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * lib/orchestration/ceremony-routing.js — risk-routed acceptance ceremony
3
+ * resolver (Epic #4478, M7-B, Part 2).
4
+ *
5
+ * The sibling of `review-depth.js` (risk → review depth) and
6
+ * `code-review.js#resolveAuditLenses` (risk → audit lens): it folds the
7
+ * planner-judged risk envelope into a per-cluster ceremony decision for the
8
+ * single-delivery acceptance critic — **fresh-context spawn** vs the
9
+ * contract-identical **inline** critic. It does NOT invent a new risk score
10
+ * and it does NOT own clustering.
11
+ *
12
+ * ## The load-bearing invariant (M4-B acceptance floor — DO NOT VIOLATE)
13
+ *
14
+ * Risk-routing chooses fresh-vs-inline **PER CLUSTER**. It NEVER changes the
15
+ * cluster COUNT. The cluster count is `ceil(totalACs / clusterCeiling)` with
16
+ * the non-disableable `[1, 8]` clamp, owned entirely by
17
+ * `acceptance-clusters.js` and untouched here. A low-risk Epic still gets one
18
+ * verdict per cluster — just possibly authored inline instead of by a fresh
19
+ * sub-agent. This module takes the cluster index as an INPUT and returns a
20
+ * decision for that one cluster; it has no way to add or remove clusters.
21
+ *
22
+ * ## Tier rules (per cluster)
23
+ *
24
+ * - `high` risk → `fresh` (a fresh-context maker-blind spawn).
25
+ * - `medium` risk → `fresh` (fail toward more ceremony, never less —
26
+ * matches `review-depth`'s fail-to-middle).
27
+ * - `low` risk → `inline` (the contract-identical inline critic),
28
+ * UNLESS the maker-checker sampling floor
29
+ * selects this cluster → `fresh`.
30
+ * - missing / unknown → `fresh` (fail-safe: an Epic that skipped `/plan`
31
+ * has no risk verdict; treat it as needing
32
+ * the full fresh-context ceremony, exactly
33
+ * as `review-depth` degrades to `standard`
34
+ * and `deriveRiskEnvelope` degrades to
35
+ * review-required).
36
+ *
37
+ * ## Maker-checker sampling floor
38
+ *
39
+ * Even at `low` risk, a fraction of clusters (`freshCriticSampleRate`, default
40
+ * 0.2) is forced `fresh` so low risk never means zero independent checking. The
41
+ * selection is **deterministic** in the cluster index (a fixed stride), so it
42
+ * is stable across re-runs and — critically — never changes the cluster count:
43
+ * it only re-labels which of the fixed set of clusters run fresh.
44
+ *
45
+ * Pure and total: inputs in, decision out. No I/O, no throws. `null` /
46
+ * `undefined` / malformed inputs degrade to `fresh` + `full` ceremony.
47
+ *
48
+ * @typedef {'fresh'|'inline'} CeremonyMode
49
+ * @typedef {'low'|'medium'|'high'} RiskLevel
50
+ */
51
+
52
+ /**
53
+ * Decide whether the sampling floor forces this low-risk cluster fresh.
54
+ *
55
+ * Deterministic in the cluster index: with rate `r` (0 < r ≤ 1) the stride is
56
+ * `round(1 / r)` and every `stride`-th cluster (0-based indices 0, stride,
57
+ * 2·stride, …) is forced fresh, yielding ≈`r` of clusters fresh. `r <= 0`
58
+ * disables the floor (no cluster forced); `r >= 1` forces every cluster.
59
+ *
60
+ * @param {number} clusterIndex Zero-based cluster position (from the fixed
61
+ * `ceil(totalACs / clusterCeiling)` fan-out — an INPUT, never mutated here).
62
+ * @param {number} rate Sampling rate, already clamped into [0, 1] by
63
+ * `getDeliveryRouting`.
64
+ * @returns {boolean} `true` when the floor forces this cluster fresh.
65
+ */
66
+ export function sampledFresh(clusterIndex, rate) {
67
+ if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
68
+ return false;
69
+ }
70
+ if (rate >= 1) return true;
71
+ const idx =
72
+ typeof clusterIndex === 'number' &&
73
+ Number.isInteger(clusterIndex) &&
74
+ clusterIndex >= 0
75
+ ? clusterIndex
76
+ : 0;
77
+ const stride = Math.max(1, Math.round(1 / rate));
78
+ return idx % stride === 0;
79
+ }
80
+
81
+ /**
82
+ * Resolve the acceptance ceremony for one cluster from the judged risk level
83
+ * and the maker-checker sampling floor. See the module header for the tier
84
+ * rules and the untouchable cluster-count invariant.
85
+ *
86
+ * @param {{
87
+ * overallLevel?: (RiskLevel|string|null|undefined),
88
+ * clusterIndex?: (number|null|undefined),
89
+ * freshCriticSampleRate?: (number|null|undefined),
90
+ * }} [input]
91
+ * @returns {{ mode: CeremonyMode, reason: string, sampled: boolean }}
92
+ */
93
+ export function resolveCeremonyForRisk(input = {}) {
94
+ const overallLevel =
95
+ input && typeof input === 'object' ? input.overallLevel : undefined;
96
+ const clusterIndex =
97
+ input && typeof input === 'object' ? input.clusterIndex : undefined;
98
+ const rate =
99
+ input && typeof input === 'object'
100
+ ? input.freshCriticSampleRate
101
+ : undefined;
102
+
103
+ if (overallLevel === 'high') {
104
+ return {
105
+ mode: 'fresh',
106
+ reason: 'high-risk: fresh-context critic',
107
+ sampled: false,
108
+ };
109
+ }
110
+ if (overallLevel === 'medium') {
111
+ return {
112
+ mode: 'fresh',
113
+ reason: 'medium-risk: fresh-context critic (fail toward more ceremony)',
114
+ sampled: false,
115
+ };
116
+ }
117
+ if (overallLevel === 'low') {
118
+ if (sampledFresh(clusterIndex, rate)) {
119
+ return {
120
+ mode: 'fresh',
121
+ reason:
122
+ 'low-risk cluster forced fresh by the maker-checker sampling floor',
123
+ sampled: true,
124
+ };
125
+ }
126
+ return {
127
+ mode: 'inline',
128
+ reason: 'low-risk: contract-identical inline critic',
129
+ sampled: false,
130
+ };
131
+ }
132
+ // Missing / unknown / malformed risk → fail-safe fresh + full ceremony,
133
+ // matching how review-depth.js and deriveRiskEnvelope degrade on an
134
+ // unjudged Epic.
135
+ return {
136
+ mode: 'fresh',
137
+ reason:
138
+ 'risk absent/unknown: fail-safe fresh-context critic + full ceremony',
139
+ sampled: false,
140
+ };
141
+ }
@@ -35,12 +35,10 @@ import {
35
35
  elideEnvelope,
36
36
  envelopeToPrompt,
37
37
  } from './context-envelope.js';
38
- import { loadSkillCapsule } from './skill-capsule-loader.js';
39
38
 
40
39
  // ---------------------------------------------------------------------------
41
- // File-content cache — the agent-protocol template and persona files are
42
- // read-only during a dispatch run. Skill bodies are loaded via
43
- // `skills.index.json` + `loadSkillCapsule` (not cached here).
40
+ // File-content cache — the agent-protocol template is read-only during a
41
+ // dispatch run.
44
42
  // ---------------------------------------------------------------------------
45
43
 
46
44
  const _fileCache = new Map();
@@ -52,81 +50,6 @@ function readFileCached(absPath) {
52
50
  return content;
53
51
  }
54
52
 
55
- let _skillsIndexCache = null;
56
-
57
- function loadSkillsIndex() {
58
- if (!_skillsIndexCache) {
59
- const indexPath = path.join(
60
- PROJECT_ROOT,
61
- '.agents',
62
- 'skills',
63
- 'skills.index.json',
64
- );
65
- _skillsIndexCache = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
66
- }
67
- return _skillsIndexCache;
68
- }
69
-
70
- /**
71
- * Test-only seam: clear the persona/skill/template cache between runs.
72
- * The `__` prefix matches the project convention for test-only exports
73
- * (see `git-utils.__setGitRunners`, `git-utils.__setSleep`).
74
- */
75
- export function __resetContextCache() {
76
- _fileCache.clear();
77
- _skillsIndexCache = null;
78
- }
79
-
80
- /**
81
- * Resolve activated skills to Policy Capsule payloads via `skills.index.json`.
82
- *
83
- * Capsule-only is the contract (Story #3863, hard cutover): only the Policy
84
- * Capsule is hydrated. The full `SKILL.md` body is never inlined into a task
85
- * prompt — the sub-agent reads it on demand via the rendered pointer path.
86
- *
87
- * @param {object} task - Normalized task (skills[]).
88
- * @param {object} skillsIndex - Parsed `skills.index.json` body.
89
- * @returns {Array<{ skill: string, capsule: string, source: string, path: string }>}
90
- */
91
- export function buildSkillCapsuleSections(task, skillsIndex) {
92
- const entries = [];
93
-
94
- for (const skill of task.skills ?? []) {
95
- try {
96
- const {
97
- capsule,
98
- source,
99
- path: skillPath,
100
- } = loadSkillCapsule(skill, skillsIndex);
101
- entries.push({ skill, capsule, source, path: skillPath });
102
- } catch (err) {
103
- Logger.warn(`[Hydrator] Failed to load skill ${skill}: ${err.message}`);
104
- }
105
- }
106
-
107
- return entries;
108
- }
109
-
110
- /**
111
- * Render skill capsule entries for the prose prompt and envelope
112
- * `skillCapsules` section. Each entry carries its capsule plus a pointer
113
- * instruction so the sub-agent knows to `Read` the full `SKILL.md` when the
114
- * task needs the playbook beyond the capsule's non-negotiables. The source is
115
- * recorded per skill for auditors.
116
- *
117
- * @param {Array<{ skill: string, capsule: string, source: string, path: string }>} entries
118
- * @returns {string}
119
- */
120
- export function formatSkillCapsulesSection(entries) {
121
- if (!entries.length) return '';
122
- let out = '## Activated Skills\n\n';
123
- for (const { skill, capsule, source, path: skillPath } of entries) {
124
- out += `### Skill: ${skill} (source: ${source})\n${capsule}\n\n`;
125
- out += `Read the full playbook on demand: \`Read ${skillPath}\`.\n\n`;
126
- }
127
- return out.trimEnd();
128
- }
129
-
130
53
  // ---------------------------------------------------------------------------
131
54
  // Internal helpers
132
55
  // ---------------------------------------------------------------------------
@@ -377,8 +300,6 @@ function envelopeTaskFrom(task) {
377
300
  return {
378
301
  id: task.id,
379
302
  title: task.title,
380
- persona: task.persona,
381
- skills: task.skills,
382
303
  protocolVersion: task.protocolVersion,
383
304
  };
384
305
  }
@@ -446,7 +367,7 @@ async function buildHierarchySections(task, provider, epicId, agentSettings) {
446
367
 
447
368
  /**
448
369
  * @param {object} task
449
- * @param {{ templatesRoot: string, personasRoot: string, skillsRoot: string }} paths
370
+ * @param {{ templatesRoot: string }} paths
450
371
  * @param {object} agentSettings - Legacy-shim settings bag or resolved config.
451
372
  * @param {string} currentVersion
452
373
  * @param {string} taskBranch
@@ -491,48 +412,6 @@ function buildStaticSections(
491
412
  });
492
413
  }
493
414
 
494
- if (task.persona) {
495
- try {
496
- const pPath = path.join(
497
- PROJECT_ROOT,
498
- paths.personasRoot,
499
- `${task.persona}.md`,
500
- );
501
- if (fs.existsSync(pPath)) {
502
- sections.push({
503
- name: 'persona',
504
- priority: DEFAULT_SECTION_PRIORITIES.persona,
505
- elideWhenOverBudget: DEFAULT_ELIDE_POLICIES.persona,
506
- content: `## Persona: ${task.persona}\n\n${readFileCached(pPath)}`,
507
- source: { kind: 'file', ref: `personas/${task.persona}.md` },
508
- });
509
- }
510
- } catch (err) {
511
- Logger.warn(
512
- `[Hydrator] Failed to load persona ${task.persona}: ${err.message}`,
513
- );
514
- }
515
- }
516
-
517
- if (task.skills?.length > 0) {
518
- try {
519
- const skillsIndex = loadSkillsIndex();
520
- const entries = buildSkillCapsuleSections(task, skillsIndex);
521
- const skillsContext = formatSkillCapsulesSection(entries);
522
- if (skillsContext) {
523
- sections.push({
524
- name: 'skillCapsules',
525
- priority: DEFAULT_SECTION_PRIORITIES.skillCapsules,
526
- elideWhenOverBudget: DEFAULT_ELIDE_POLICIES.skillCapsules,
527
- content: skillsContext,
528
- source: { kind: 'derived', ref: 'activated-skills' },
529
- });
530
- }
531
- } catch (err) {
532
- Logger.warn(`[Hydrator] Failed to load skills index: ${err.message}`);
533
- }
534
- }
535
-
536
415
  // Track which dedicated section(s) were emitted so taskInstructions drops
537
416
  // only the inline sections that were actually reproduced elsewhere in the
538
417
  // envelope — keeping each binding acceptance/verify item present exactly