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.
- package/.agents/agents/acceptance-critic.md +129 -0
- package/.agents/agents/retro.md +42 -0
- package/.agents/agents/story-worker.md +162 -0
- package/.agents/docs/configuration.md +7 -1
- package/.agents/docs/execution-reference.md +27 -2
- package/.agents/instructions.md +43 -33
- package/.agents/personas/engineer.md +26 -112
- package/.agents/personas/security-engineer.md +1 -2
- package/.agents/rules/git-conventions-reference.md +225 -0
- package/.agents/rules/git-conventions.md +25 -200
- package/.agents/rules/security-baseline.md +5 -0
- package/.agents/rules/testing-standards.md +106 -13
- package/.agents/schemas/agentrc.schema.json +31 -1
- package/.agents/schemas/lifecycle/slice.end.schema.json +21 -0
- package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +20 -0
- package/.agents/schemas/lifecycle/slice.start.schema.json +17 -0
- package/.agents/scripts/acceptance-eval.js +62 -18
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bookkeeping-reconcile.js +117 -0
- package/.agents/scripts/check-context-budget.js +62 -5
- package/.agents/scripts/diagnose-friction.js +0 -6
- package/.agents/scripts/epic-deliver-prepare.js +272 -10
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +56 -18
- package/.agents/scripts/lib/close-validation/gates.js +159 -21
- package/.agents/scripts/lib/config/acceptance-eval.js +52 -5
- package/.agents/scripts/lib/config/delivery-routing.js +87 -0
- package/.agents/scripts/lib/config/explain.js +2 -0
- package/.agents/scripts/lib/config-resolver.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +37 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -0
- package/.agents/scripts/lib/doc-tiers.js +37 -2
- package/.agents/scripts/lib/observability/active-story-env.js +111 -2
- package/.agents/scripts/lib/observability/hook-heartbeat.js +219 -0
- package/.agents/scripts/lib/observability/tool-trace-hook.js +15 -4
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +111 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +32 -4
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +270 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +141 -0
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +3 -124
- package/.agents/scripts/lib/orchestration/deliver-route.js +173 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +233 -0
- package/.agents/scripts/lib/orchestration/file-assumptions.js +68 -7
- package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +270 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +83 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -3
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +3 -2
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +99 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +38 -1
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +16 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +1 -0
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +1 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -2
- package/.agents/scripts/lib/provider-factory.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/plan-context.js +28 -10
- package/.agents/scripts/post-structured-comment.js +38 -0
- package/.agents/scripts/slice-phase.js +361 -0
- package/.agents/scripts/sync-claude-agents.js +165 -0
- package/.agents/scripts/update-ticket-state.js +31 -0
- package/.agents/scripts/wave-tick.js +138 -9
- package/.agents/skills/core/api-and-interface-design/SKILL.md +5 -3
- package/.agents/skills/core/code-review-and-quality/SKILL.md +63 -7
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +1 -1
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +5 -5
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +8 -8
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +4 -4
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +26 -56
- package/.agents/skills/core/gates-and-baselines/SKILL.md +149 -0
- package/.agents/skills/core/idea-refinement/SKILL.md +2 -8
- package/.agents/skills/core/qa-coverage-mapping/SKILL.md +7 -7
- package/.agents/skills/skills.index.json +11 -381
- package/.agents/workflows/deliver.md +47 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +38 -13
- package/.agents/workflows/helpers/deliver-epic-reference.md +18 -5
- package/.agents/workflows/helpers/deliver-epic-single.md +331 -0
- package/.agents/workflows/helpers/deliver-epic.md +51 -8
- package/.agents/workflows/helpers/deliver-stories.md +15 -5
- package/.agents/workflows/helpers/epic-deliver-story.md +12 -3
- package/.agents/workflows/helpers/mandrel-sync-config.md +1 -1
- package/.agents/workflows/helpers/plan-epic-reference.md +19 -8
- package/.agents/workflows/helpers/plan-epic.md +95 -27
- package/.agents/workflows/helpers/scope-triage-gate.md +9 -0
- package/.agents/workflows/mandrel-update.md +1 -1
- package/.agents/workflows/plan.md +16 -4
- package/docs/CHANGELOG.md +23 -0
- package/lib/cli/registry.js +95 -0
- package/package.json +4 -2
- package/.agents/personas/engineer-mobile.md +0 -120
- package/.agents/personas/engineer-web.md +0 -111
- package/.agents/personas/product.md +0 -94
- package/.agents/personas/refactorer.md +0 -113
- package/.agents/personas/sre.md +0 -86
- package/.agents/personas/ux-designer.md +0 -95
- package/.agents/scripts/epic-plan-decompose.js +0 -54
- package/.agents/scripts/epic-plan-spec.js +0 -64
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +0 -109
- package/.agents/scripts/plan-critics.js +0 -227
- package/.agents/skills/core/baseline-refresh/SKILL.md +0 -181
- package/.agents/skills/core/ci-cd-and-automation/SKILL.md +0 -274
- package/.agents/skills/core/ci-cd-and-automation/examples.md +0 -211
- package/.agents/skills/core/code-simplification/SKILL.md +0 -389
- package/.agents/skills/core/context-engineering/SKILL.md +0 -309
- package/.agents/skills/core/context-engineering/examples.md +0 -58
- package/.agents/skills/core/deprecation-and-migration/SKILL.md +0 -250
- package/.agents/skills/core/frontend-ui-engineering/SKILL.md +0 -357
- package/.agents/skills/core/hydrate-context/SKILL.md +0 -123
- package/.agents/skills/core/idea-refinement/examples.md +0 -437
- package/.agents/skills/core/idea-refinement/frameworks.md +0 -135
- package/.agents/skills/core/incremental-implementation/SKILL.md +0 -271
- package/.agents/skills/core/introducing-a-baseline-gate/SKILL.md +0 -213
- package/.agents/skills/core/knowledge-transfer/SKILL.md +0 -180
- package/.agents/skills/core/mutation-survivor-remediation/SKILL.md +0 -117
- package/.agents/skills/core/performance-optimization/SKILL.md +0 -314
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +0 -277
- package/.agents/skills/core/property-based-testing/SKILL.md +0 -148
- package/.agents/skills/core/refactoring-discipline/SKILL.md +0 -111
- package/.agents/skills/core/shipping-and-launch/SKILL.md +0 -328
- package/.agents/skills/core/spec-driven-development/SKILL.md +0 -252
- package/.agents/skills/core/test-driven-development/SKILL.md +0 -475
- package/.agents/skills/core/using-agent-skills/SKILL.md +0 -232
- package/.agents/skills/stack/architecture/monorepo-path-strategist/SKILL.md +0 -31
- package/.agents/skills/stack/architecture/structured-output-zod/SKILL.md +0 -51
- package/.agents/skills/stack/architecture/subagent-orchestration/SKILL.md +0 -76
- package/.agents/skills/stack/backend/cloudflare-hono-architect/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-hono-architect/examples/route-template.ts +0 -33
- package/.agents/skills/stack/backend/cloudflare-queue-manager/SKILL.md +0 -31
- package/.agents/skills/stack/backend/cloudflare-workers/SKILL.md +0 -51
- package/.agents/skills/stack/backend/highlevel-crm/SKILL.md +0 -54
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/SKILL.md +0 -29
- package/.agents/skills/stack/backend/sqlite-drizzle-expert/examples/schema-template.ts +0 -30
- package/.agents/skills/stack/backend/stripe-integration/SKILL.md +0 -57
- package/.agents/skills/stack/backend/stripe-integration/scripts/listen-stripe.sh +0 -9
- package/.agents/skills/stack/backend/turso-sqlite/SKILL.md +0 -48
- package/.agents/skills/stack/frontend/astro/SKILL.md +0 -62
- package/.agents/skills/stack/frontend/astro-react-island-strategist/SKILL.md +0 -30
- package/.agents/skills/stack/frontend/expo-react-native-developer/SKILL.md +0 -29
- package/.agents/skills/stack/frontend/google-analytics-v4/SKILL.md +0 -50
- package/.agents/skills/stack/frontend/tailwind-v4/SKILL.md +0 -58
- package/.agents/skills/stack/frontend/ui-accessibility-engineer/SKILL.md +0 -34
- package/.agents/skills/stack/qa/audit-accessibility/SKILL.md +0 -51
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +0 -199
- package/.agents/skills/stack/security/backend-security-patterns/SKILL.md +0 -68
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* runner (`INDEPENDENT_GATE_NAMES` / `partitionGates`).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
import { _internals as baselineReaderInternals } from '../baselines/reader.js';
|
|
12
|
+
import { getQuality } from '../config/quality.js';
|
|
9
13
|
import { hasNpmScript, readPackageScripts } from '../npm-scripts.js';
|
|
14
|
+
import { KNOWN_KINDS } from '../orchestration/check-baselines/phases/parse-args.js';
|
|
10
15
|
import {
|
|
11
16
|
buildFormatHint,
|
|
12
17
|
FORMAT_CHECK_FALLBACK,
|
|
@@ -115,6 +120,106 @@ function buildTestGateEntry(coverageCaptureActive) {
|
|
|
115
120
|
return [{ name: 'test', cmd: 'npm', args: ['test'] }];
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
const CHECK_BASELINES_HINT =
|
|
124
|
+
'Unified baselines gate breached. Inspect the JSON report (`node .agents/scripts/check-baselines.js`) to see which kind/component/axis fell below floor; remediate the underlying file(s) or — when the regression is intentional — refresh the relevant baseline through its per-kind update script and commit with a `baseline-refresh:` tagged subject.';
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Baseline kinds the resolved config enables for the unified
|
|
128
|
+
* `check-baselines` gate. Mirrors `selectEnabledGates` in the check-baselines
|
|
129
|
+
* pipeline (a kind runs when its `gates.<kind>` block is present and not
|
|
130
|
+
* explicitly disabled) so the registration probe's view of "what will run"
|
|
131
|
+
* matches the gate's own view exactly.
|
|
132
|
+
*
|
|
133
|
+
* @param {object|undefined|null} config canonical resolved config
|
|
134
|
+
* @returns {string[]}
|
|
135
|
+
*/
|
|
136
|
+
function enabledBaselineKinds(config) {
|
|
137
|
+
const gates = getQuality(config)?.gates ?? {};
|
|
138
|
+
return KNOWN_KINDS.filter((kind) => {
|
|
139
|
+
const block = gates[kind];
|
|
140
|
+
return block && typeof block === 'object' && block.enabled !== false;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether the consumer opted into fail-closed baseline enforcement via
|
|
146
|
+
* `delivery.quality.requireBaselines: true`. Default false — a consumer that
|
|
147
|
+
* enables baseline gates but has not committed baseline artifacts gets a
|
|
148
|
+
* clean skip (see `probeBaselinesGate`) rather than a deterministic first-try
|
|
149
|
+
* close failure. Mirrors the `delivery.ci.requireChecks` escape hatch (#4472).
|
|
150
|
+
*
|
|
151
|
+
* @param {object|undefined|null} config
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
function baselinesRequiredByConfig(config) {
|
|
155
|
+
return config?.delivery?.quality?.requireBaselines === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toKindSet(presentBaselines) {
|
|
159
|
+
if (presentBaselines instanceof Set) return presentBaselines;
|
|
160
|
+
if (Array.isArray(presentBaselines)) return new Set(presentBaselines);
|
|
161
|
+
return new Set();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Probe whether the `check-baselines` consumer contract is satisfied before
|
|
166
|
+
* registering the gate (#4495 — mirrors the #4473/#4480 coverage-capture
|
|
167
|
+
* remedy). The contract: every enabled baseline kind carries a committed
|
|
168
|
+
* baseline artifact on disk (the same path the gate's reader resolves).
|
|
169
|
+
*
|
|
170
|
+
* Decision shape:
|
|
171
|
+
* - `{ register: false, reason }` — skip (caller logs the reason). Baseline
|
|
172
|
+
* gates ARE enabled but none of the enabled kinds carry a committed
|
|
173
|
+
* baseline artifact and the consumer has not set `requireBaselines`. This
|
|
174
|
+
* is the bench/greenfield case: the gate would otherwise fail
|
|
175
|
+
* deterministically on first try reading a non-existent
|
|
176
|
+
* `baselines/<kind>.json`.
|
|
177
|
+
* - `{ register: true }` — at least one committed baseline artifact is
|
|
178
|
+
* present, OR no baseline kinds are enabled at all (the gate then self-
|
|
179
|
+
* skips every kind and exits a clean empty PASS — no failure to avoid, so
|
|
180
|
+
* the gate stays registered exactly as pre-#4495); run the gate.
|
|
181
|
+
* - `{ register: true, hint }` — baselines are required-by-config
|
|
182
|
+
* (`requireBaselines: true`) but absent; keep the gate registered so it
|
|
183
|
+
* fails, with a preflight hint naming the fix.
|
|
184
|
+
*
|
|
185
|
+
* @param {{ config?: object, cwd?: string, presentBaselines?: string[]|Set<string> }} opts
|
|
186
|
+
* `presentBaselines` injects the set of kinds whose baseline artifact
|
|
187
|
+
* exists (tests), short-circuiting the on-disk probe.
|
|
188
|
+
* @returns {{ register: boolean, reason?: string, hint?: string }}
|
|
189
|
+
*/
|
|
190
|
+
function probeBaselinesGate({ config, cwd, presentBaselines } = {}) {
|
|
191
|
+
const enabled = enabledBaselineKinds(config);
|
|
192
|
+
if (enabled.length === 0) {
|
|
193
|
+
// No enabled baseline kinds → `check-baselines.js` self-skips every kind
|
|
194
|
+
// and exits clean (an empty PASS). There is no deterministic-failure risk
|
|
195
|
+
// to avoid, so keep the gate registered exactly as it was pre-#4495; the
|
|
196
|
+
// #4495 skip is confined strictly to the read-miss-would-fail case below.
|
|
197
|
+
return { register: true };
|
|
198
|
+
}
|
|
199
|
+
const injected =
|
|
200
|
+
presentBaselines != null ? toKindSet(presentBaselines) : null;
|
|
201
|
+
const present = enabled.filter((kind) =>
|
|
202
|
+
injected
|
|
203
|
+
? injected.has(kind)
|
|
204
|
+
: existsSync(baselineReaderInternals.resolveBaselinePath(kind, { cwd })),
|
|
205
|
+
);
|
|
206
|
+
if (present.length > 0) return { register: true };
|
|
207
|
+
if (baselinesRequiredByConfig(config)) {
|
|
208
|
+
return {
|
|
209
|
+
register: true,
|
|
210
|
+
hint:
|
|
211
|
+
`Baselines are required (delivery.quality.requireBaselines) but no committed baseline artifact was found for enabled kind(s): ${enabled.join(', ')}. ` +
|
|
212
|
+
'Generate the baseline(s) with the per-kind update script (e.g. `npm run crap:update`, `npm run maintainability:update`) and commit them, or unset requireBaselines to skip the gate until baselines exist.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
register: false,
|
|
217
|
+
reason:
|
|
218
|
+
`check-baselines skipped — enabled kind(s) ${enabled.join(', ')} have no committed baseline artifact under baselines/ ` +
|
|
219
|
+
'and delivery.quality.requireBaselines is not set. Commit baseline artifacts (or set requireBaselines to enforce them) to activate the gate.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
118
223
|
/**
|
|
119
224
|
* Build the canonical close-validation gate list.
|
|
120
225
|
*
|
|
@@ -152,15 +257,30 @@ function buildTestGateEntry(coverageCaptureActive) {
|
|
|
152
257
|
* instead of a deterministic close failure with no test gate at all. The
|
|
153
258
|
* probe reads `package.json` at `cwd` (the gate execution directory).
|
|
154
259
|
*
|
|
155
|
-
*
|
|
260
|
+
* Story #4495 — the unified `check-baselines` gate reads a committed
|
|
261
|
+
* `baselines/<kind>.json` for each enabled kind; a consumer that enables
|
|
262
|
+
* baseline gates but ships no `baselines/` tree (every bench sandbox, any
|
|
263
|
+
* greenfield consumer) failed the gate deterministically on first try. The
|
|
264
|
+
* gate is now registered only when its consumer contract is satisfied
|
|
265
|
+
* (`probeBaselinesGate`): at least one enabled kind carries a committed
|
|
266
|
+
* baseline, OR the consumer opted into fail-closed enforcement via
|
|
267
|
+
* `delivery.quality.requireBaselines`. When no baselines are committed and
|
|
268
|
+
* none are required, the gate is skipped with a logged reason (via `log`)
|
|
269
|
+
* instead of a blocking failure.
|
|
270
|
+
*
|
|
271
|
+
* @param {{ config?: object, epicBranch?: string, cwd?: string, packageScripts?: Record<string, string>, presentBaselines?: string[]|Set<string>, log?: (message: string) => void }} [opts]
|
|
156
272
|
* `config` is the canonical resolved config (`{ project, delivery, ... }`);
|
|
157
273
|
* gate commands resolve from `project.commands` and the CRAP toggle from
|
|
158
274
|
* `delivery.quality.gates.crap.enabled`. `epicBranch` is the close run's
|
|
159
275
|
* integration branch (`epic/<id>` for Epic-attached Stories, the base
|
|
160
276
|
* branch for standalone Stories). `cwd` is where the `package.json`
|
|
161
|
-
* coverage-script probe
|
|
162
|
-
*
|
|
163
|
-
* circuits the disk
|
|
277
|
+
* coverage-script probe and the `baselines/<kind>.json` presence probe
|
|
278
|
+
* read from (defaults to `process.cwd()`); `packageScripts` injects the
|
|
279
|
+
* scripts map directly (tests) and short-circuits the coverage-script disk
|
|
280
|
+
* read; `presentBaselines` injects the set of kinds whose baseline artifact
|
|
281
|
+
* exists (tests) and short-circuits the baseline-presence disk read; `log`
|
|
282
|
+
* receives the skip reason when the `check-baselines` gate is not
|
|
283
|
+
* registered.
|
|
164
284
|
* @returns {Gate[]}
|
|
165
285
|
*/
|
|
166
286
|
export function buildDefaultGates({
|
|
@@ -168,6 +288,8 @@ export function buildDefaultGates({
|
|
|
168
288
|
epicBranch,
|
|
169
289
|
cwd,
|
|
170
290
|
packageScripts,
|
|
291
|
+
presentBaselines,
|
|
292
|
+
log,
|
|
171
293
|
} = {}) {
|
|
172
294
|
const scripts = packageScripts ?? readPackageScripts(cwd);
|
|
173
295
|
const coverageCaptureActive =
|
|
@@ -186,6 +308,14 @@ export function buildDefaultGates({
|
|
|
186
308
|
? buildChangedFileScope(epicBranch)
|
|
187
309
|
: null;
|
|
188
310
|
const baselinesGateEnv = buildBaselinesGateEnv(epicBranch);
|
|
311
|
+
const baselinesDecision = probeBaselinesGate({
|
|
312
|
+
config,
|
|
313
|
+
cwd,
|
|
314
|
+
presentBaselines,
|
|
315
|
+
});
|
|
316
|
+
if (!baselinesDecision.register && baselinesDecision.reason) {
|
|
317
|
+
log?.(`[close-validation] ${baselinesDecision.reason}`);
|
|
318
|
+
}
|
|
189
319
|
return [
|
|
190
320
|
{
|
|
191
321
|
name: 'typecheck',
|
|
@@ -218,23 +348,31 @@ export function buildDefaultGates({
|
|
|
218
348
|
},
|
|
219
349
|
]
|
|
220
350
|
: []),
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
351
|
+
// Story #2210 — unified `check-baselines` gate is the only path for
|
|
352
|
+
// per-kind regression enforcement. The legacy per-kind in-process gates
|
|
353
|
+
// were retired because their regression-compare semantics are fully
|
|
354
|
+
// subsumed by this gate's attribution-wired floor + tolerance + schema
|
|
355
|
+
// enforcement, and running both paths in series was redundant and
|
|
356
|
+
// conflict-prone.
|
|
357
|
+
//
|
|
358
|
+
// `check-baselines.js` self-skips per-kind gates whose `enabled === false`
|
|
359
|
+
// is configured. Story #4495: it is now also skipped entirely when the
|
|
360
|
+
// consumer enables baseline gates but ships no committed baseline artifact
|
|
361
|
+
// (and has not set `delivery.quality.requireBaselines`) — otherwise the
|
|
362
|
+
// gate fails deterministically on first try reading a non-existent
|
|
363
|
+
// `baselines/<kind>.json` (`probeBaselinesGate`). When required-by-config
|
|
364
|
+
// but absent, it stays registered with a preflight hint naming the fix.
|
|
365
|
+
...(baselinesDecision.register
|
|
366
|
+
? [
|
|
367
|
+
{
|
|
368
|
+
name: 'check-baselines',
|
|
369
|
+
cmd: 'node',
|
|
370
|
+
args: ['.agents/scripts/check-baselines.js', '--format', 'text'],
|
|
371
|
+
hint: baselinesDecision.hint ?? CHECK_BASELINES_HINT,
|
|
372
|
+
...(baselinesGateEnv ? { env: baselinesGateEnv } : {}),
|
|
373
|
+
},
|
|
374
|
+
]
|
|
375
|
+
: []),
|
|
238
376
|
];
|
|
239
377
|
}
|
|
240
378
|
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
|
|
40
40
|
maxRounds: 2,
|
|
41
|
+
clusterCeiling: 4,
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -50,6 +51,22 @@ export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
|
|
|
50
51
|
*/
|
|
51
52
|
export const ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING = 5;
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Hard, undisableable ceiling on `clusterCeiling` — the max ACs one
|
|
56
|
+
* single-delivery acceptance critic scores in a single fresh-context pass
|
|
57
|
+
* (Epic #4475, M4-B, design §S2a). It is the **acceptance-dilution guard**:
|
|
58
|
+
* single delivery collapses the whole Epic into one session, so the only
|
|
59
|
+
* acceptance coverage left is the per-AC-cluster critic fan-out
|
|
60
|
+
* (`ceil(totalACs / clusterCeiling)` independent maker-blind passes). A
|
|
61
|
+
* pathologically large `clusterCeiling` would collapse that fan-out to a
|
|
62
|
+
* single critic scoring every AC at once — exactly the dilution the design
|
|
63
|
+
* forecloses — so a configured value above this ceiling is clamped down to
|
|
64
|
+
* it. Kept small on purpose.
|
|
65
|
+
*
|
|
66
|
+
* @type {number}
|
|
67
|
+
*/
|
|
68
|
+
export const ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX = 8;
|
|
69
|
+
|
|
53
70
|
/**
|
|
54
71
|
* Clamp a candidate round count into the inviolable `[1, ceiling]` range.
|
|
55
72
|
* Non-integer / non-finite inputs fall back to the documented default.
|
|
@@ -68,19 +85,43 @@ function clampRounds(value, fallback) {
|
|
|
68
85
|
return candidate;
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Clamp a candidate cluster ceiling into the inviolable
|
|
90
|
+
* `[1, ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX]` range. Non-integer / non-finite
|
|
91
|
+
* inputs fall back to the documented default. Mirrors `clampRounds` — the
|
|
92
|
+
* anti-dilution guard cannot be disabled (`clusterCeiling: 0` or a negative
|
|
93
|
+
* clamps up to 1; an over-max value clamps down to the hard cap).
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} value
|
|
96
|
+
* @param {number} fallback
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
function clampClusterCeiling(value, fallback) {
|
|
100
|
+
const candidate =
|
|
101
|
+
typeof value === 'number' && Number.isInteger(value) ? value : fallback;
|
|
102
|
+
if (candidate < 1) return 1;
|
|
103
|
+
if (candidate > ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX) {
|
|
104
|
+
return ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX;
|
|
105
|
+
}
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
/**
|
|
72
110
|
* Read the merged acceptance-eval block. Returns the canonical shape:
|
|
73
111
|
*
|
|
74
112
|
* {
|
|
75
|
-
* maxRounds: number,
|
|
76
|
-
* ceiling: number,
|
|
113
|
+
* maxRounds: number, // clamped into [1, roundsCeiling]
|
|
114
|
+
* ceiling: number, // the undisableable hard cap on rounds
|
|
115
|
+
* clusterCeiling: number, // clamped into [1, clusterCeilingMax]
|
|
116
|
+
* clusterCeilingMax: number // the undisableable hard cap on cluster size
|
|
77
117
|
* }
|
|
78
118
|
*
|
|
79
|
-
* `maxRounds` is always a positive integer no greater than `ceiling
|
|
80
|
-
*
|
|
119
|
+
* `maxRounds` is always a positive integer no greater than `ceiling`;
|
|
120
|
+
* `clusterCeiling` is always a positive integer no greater than
|
|
121
|
+
* `clusterCeilingMax`, regardless of what the resolved config carried.
|
|
81
122
|
*
|
|
82
123
|
* @param {object | null | undefined} config
|
|
83
|
-
* @returns {{ maxRounds: number, ceiling: number }}
|
|
124
|
+
* @returns {{ maxRounds: number, ceiling: number, clusterCeiling: number, clusterCeilingMax: number }}
|
|
84
125
|
*/
|
|
85
126
|
export function getAcceptanceEval(config) {
|
|
86
127
|
const user = config?.delivery?.acceptanceEval ?? {};
|
|
@@ -88,8 +129,14 @@ export function getAcceptanceEval(config) {
|
|
|
88
129
|
user.maxRounds,
|
|
89
130
|
ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
|
|
90
131
|
);
|
|
132
|
+
const clusterCeiling = clampClusterCeiling(
|
|
133
|
+
user.clusterCeiling,
|
|
134
|
+
ACCEPTANCE_EVAL_DEFAULTS.clusterCeiling,
|
|
135
|
+
);
|
|
91
136
|
return {
|
|
92
137
|
maxRounds,
|
|
93
138
|
ceiling: ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING,
|
|
139
|
+
clusterCeiling,
|
|
140
|
+
clusterCeilingMax: ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX,
|
|
94
141
|
};
|
|
95
142
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `delivery.routing` accessor + framework defaults — Epic #4475 (M4-A),
|
|
3
|
+
* the single-delivery-as-default foundation, plus Epic #4478 (M7-B), the
|
|
4
|
+
* role-scoped-boot-context flip and the maker-checker sampling floor.
|
|
5
|
+
*
|
|
6
|
+
* `delivery.routing.singleDelivery` is the **global kill-switch** for the
|
|
7
|
+
* single-delivery route. It defaults to `true` (single-delivery is the
|
|
8
|
+
* default shape for epic-shaped work), and is shipped INERT in M4-A: the
|
|
9
|
+
* `deliver.md` router's single verdict currently falls through to the
|
|
10
|
+
* fan-out helper, so flipping this knob has no observable effect until
|
|
11
|
+
* M4-B wires `deliver-epic-single.md`.
|
|
12
|
+
*
|
|
13
|
+
* When set to `false`, `resolveEpicDeliveryRoute` forces EVERY Epic — even
|
|
14
|
+
* one carrying the `delivery::single` label or a `decompose.shape:"single"`
|
|
15
|
+
* checkpoint — down the fan-out path. This is the instant, per-consumer
|
|
16
|
+
* global revert that ships BEFORE the default flips: no code rollback, no
|
|
17
|
+
* re-plan, just a config edit.
|
|
18
|
+
*
|
|
19
|
+
* `delivery.routing.roleScopedAgents` is the **kill-switch for the role-scoped
|
|
20
|
+
* boot contexts** (Epic #4478, M7-B). It mirrors the `singleDelivery` shape
|
|
21
|
+
* exactly. It defaults to `true`: a converted spawn (`story-worker`,
|
|
22
|
+
* `acceptance-critic`) boots on its own `.claude/agents/<role>.md` system
|
|
23
|
+
* prompt instead of re-paying the full `CLAUDE.md` @-import closure, which is
|
|
24
|
+
* the whole payoff of the context diet (≈50KB → ≈8KB per spawn). When set to
|
|
25
|
+
* `false`, every converted spawn falls back to `subagent_type: general-purpose`
|
|
26
|
+
* — the instant, code-rollback-free per-consumer revert, and the universal
|
|
27
|
+
* escape for hosts that ignore `.claude/agents/`. Flipping it off never drops a
|
|
28
|
+
* gate: the fallback is the full-closure agent that ran before M7-B.
|
|
29
|
+
*
|
|
30
|
+
* `delivery.routing.freshCriticSampleRate` is the **maker-checker sampling
|
|
31
|
+
* floor** (Epic #4478, M7-B, Part 2). Risk-routed ceremony sends a low-risk
|
|
32
|
+
* acceptance cluster down the contract-identical *inline* critic path, but a
|
|
33
|
+
* fraction of low-risk clusters are still forced through a *fresh-context*
|
|
34
|
+
* critic so "low risk" never degrades to zero independent checking. The rate is
|
|
35
|
+
* clamped into `[0, 1]`; `0` disables the floor (pure risk routing), `1` forces
|
|
36
|
+
* every cluster fresh. The default is `0.2`. See `resolveCeremonyForRisk` in
|
|
37
|
+
* `lib/orchestration/ceremony-routing.js`.
|
|
38
|
+
*
|
|
39
|
+
* Framework-defaults pattern mirrors `lib/config/ci.js#getCiDelivery`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export const DELIVERY_ROUTING_DEFAULTS = Object.freeze({
|
|
43
|
+
singleDelivery: true,
|
|
44
|
+
roleScopedAgents: true,
|
|
45
|
+
freshCriticSampleRate: 0.2,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clamp a candidate sample rate into `[0, 1]`. Non-finite / non-number inputs
|
|
50
|
+
* fall back to the framework default so a degraded config never yields a
|
|
51
|
+
* NaN-driven or out-of-range floor.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
function clampSampleRate(value) {
|
|
57
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
58
|
+
return DELIVERY_ROUTING_DEFAULTS.freshCriticSampleRate;
|
|
59
|
+
}
|
|
60
|
+
if (value < 0) return 0;
|
|
61
|
+
if (value > 1) return 1;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read the merged `delivery.routing` block, applying framework defaults for
|
|
67
|
+
* any field the operator omitted. Accepts the full resolved config, the bare
|
|
68
|
+
* `delivery` bag, or the bare `routing` bag — mirroring `getCiDelivery`'s
|
|
69
|
+
* tolerant unwrap so callers can pass whichever shape they hold.
|
|
70
|
+
*
|
|
71
|
+
* @param {object | null | undefined} config
|
|
72
|
+
* @returns {{ singleDelivery: boolean, roleScopedAgents: boolean, freshCriticSampleRate: number }}
|
|
73
|
+
*/
|
|
74
|
+
export function getDeliveryRouting(config) {
|
|
75
|
+
const routing = config?.delivery?.routing ?? config?.routing ?? config ?? {};
|
|
76
|
+
return {
|
|
77
|
+
singleDelivery:
|
|
78
|
+
typeof routing.singleDelivery === 'boolean'
|
|
79
|
+
? routing.singleDelivery
|
|
80
|
+
: DELIVERY_ROUTING_DEFAULTS.singleDelivery,
|
|
81
|
+
roleScopedAgents:
|
|
82
|
+
typeof routing.roleScopedAgents === 'boolean'
|
|
83
|
+
? routing.roleScopedAgents
|
|
84
|
+
: DELIVERY_ROUTING_DEFAULTS.roleScopedAgents,
|
|
85
|
+
freshCriticSampleRate: clampSampleRate(routing.freshCriticSampleRate),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -204,6 +204,8 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
204
204
|
'Whether a dedicated refactor stage runs during delivery.',
|
|
205
205
|
'delivery.acceptanceEval.maxRounds':
|
|
206
206
|
'Redraft rounds the per-Story acceptance self-eval loop runs before escalating to agent::blocked (default 2; clamped to a hard ceiling that cannot be disabled).',
|
|
207
|
+
'delivery.acceptanceEval.clusterCeiling':
|
|
208
|
+
'Max acceptance criteria one single-delivery acceptance critic scores per fresh-context pass; the Epic ACs are split into ceil(totalACs / clusterCeiling) maker-blind critic clusters (default 4; clamped to [1, 8]).',
|
|
207
209
|
|
|
208
210
|
// qa.*
|
|
209
211
|
'qa.featureRoot': 'Root directory holding the QA harness .feature files.',
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* deleted from the resolver wrapper. Every internal call site reads the
|
|
20
20
|
* canonical `project` / `github` / `planning` / `delivery` blocks
|
|
21
21
|
* directly; consumers upgrade in lockstep with the framework bump
|
|
22
|
-
* (see `.agents/rules/git-conventions.md#contract-cutovers-—-no-shim-layer`).
|
|
22
|
+
* (see `.agents/rules/git-conventions-reference.md#contract-cutovers-—-no-shim-layer`).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import fs from 'node:fs';
|
|
@@ -233,6 +233,33 @@ const EPIC_AUDIT_SCHEMA = {
|
|
|
233
233
|
additionalProperties: false,
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
+
// Epic #4475 (M4-A) — single-delivery routing kill-switch.
|
|
237
|
+
// `delivery.routing.singleDelivery` (default true via getDeliveryRouting) is
|
|
238
|
+
// the global kill-switch for the single-delivery route. When true (the
|
|
239
|
+
// default), an Epic marked `delivery::single` at plan time routes to the
|
|
240
|
+
// single-delivery helper; when false, EVERY Epic — even a single-marked one —
|
|
241
|
+
// is forced down the fan-out path (the instant, code-rollback-free per-consumer
|
|
242
|
+
// revert). Shipped INERT in M4-A: the router's single verdict falls through to
|
|
243
|
+
// fan-out until M4-B wires the executor, so the knob has no observable effect
|
|
244
|
+
// yet.
|
|
245
|
+
// Epic #4478 (M7-B) — role-scoped-agent kill-switch + maker-checker floor.
|
|
246
|
+
// `delivery.routing.roleScopedAgents` (default true via getDeliveryRouting)
|
|
247
|
+
// flips converted delivery spawns onto their `.claude/agents/<role>.md` boot
|
|
248
|
+
// context; false falls back to `subagent_type: general-purpose` (the instant
|
|
249
|
+
// per-consumer revert + the escape for hosts that ignore `.claude/agents/`).
|
|
250
|
+
// `delivery.routing.freshCriticSampleRate` (default 0.2, clamped [0, 1]) is the
|
|
251
|
+
// maker-checker sampling floor forcing a fraction of low-risk acceptance
|
|
252
|
+
// clusters through a fresh critic.
|
|
253
|
+
const ROUTING_SCHEMA = {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
singleDelivery: { type: 'boolean' },
|
|
257
|
+
roleScopedAgents: { type: 'boolean' },
|
|
258
|
+
freshCriticSampleRate: { type: 'number', minimum: 0, maximum: 1 },
|
|
259
|
+
},
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
};
|
|
262
|
+
|
|
236
263
|
// Story #2899 (Epic #2880) — performance defaults + preflight (F13).
|
|
237
264
|
// `delivery.ci.skipForStoryPushes` (default true via getCiDelivery): when
|
|
238
265
|
// true, pre-push tooling appends a `[skip ci]` trailer to Story-branch
|
|
@@ -296,8 +323,8 @@ const PREFLIGHT_SCHEMA = {
|
|
|
296
323
|
* checkpoint wired into story-deliver (Story #3430, Epic #3418). Strictly
|
|
297
324
|
* additive and default-OFF: when `enabled` is unset or `false`, story-deliver
|
|
298
325
|
* behaves exactly as before. When `true`, the worker runs an advisory
|
|
299
|
-
* post-green refactor pass (the `
|
|
300
|
-
*
|
|
326
|
+
* post-green refactor pass (the `core/code-review-and-quality` skill's
|
|
327
|
+
* Post-Green Refactor Pass) after the suite is green. The stage is
|
|
301
328
|
* advisory only — it never changes existing close-validation gate semantics.
|
|
302
329
|
*/
|
|
303
330
|
const REFACTOR_STAGE_SCHEMA = {
|
|
@@ -306,7 +333,7 @@ const REFACTOR_STAGE_SCHEMA = {
|
|
|
306
333
|
enabled: {
|
|
307
334
|
type: 'boolean',
|
|
308
335
|
description:
|
|
309
|
-
'When true, story-deliver runs an advisory post-green refactor stage (
|
|
336
|
+
'When true, story-deliver runs an advisory post-green refactor stage (core/code-review-and-quality skill, Post-Green Refactor Pass) after the suite is green. Default false — when unset the stage is skipped and close-validation gate semantics are unchanged.',
|
|
310
337
|
},
|
|
311
338
|
},
|
|
312
339
|
additionalProperties: false,
|
|
@@ -337,6 +364,12 @@ const ACCEPTANCE_EVAL_SCHEMA = {
|
|
|
337
364
|
description:
|
|
338
365
|
'Maximum number of redraft rounds the acceptance self-eval loop runs before escalating to agent::blocked when criteria remain unmet. Default 2; clamped into [1, hard ceiling] by the resolver so the cap can never be disabled.',
|
|
339
366
|
},
|
|
367
|
+
clusterCeiling: {
|
|
368
|
+
type: 'integer',
|
|
369
|
+
minimum: 1,
|
|
370
|
+
description:
|
|
371
|
+
'Epic #4475 (M4-B). Max acceptance criteria one single-delivery acceptance critic scores in a single fresh-context pass. Single delivery clusters the Epic ## Acceptance Table ACs into ceil(totalACs / clusterCeiling) groups and spawns one maker-blind critic per cluster, restoring the distributed acceptance coverage the per-Story critic fan-out gave for free. Default 4; clamped into [1, 8] by the resolver so a large value cannot collapse the fan-out to a single diluted critic. Ignored on the fan-out route.',
|
|
372
|
+
},
|
|
340
373
|
},
|
|
341
374
|
additionalProperties: false,
|
|
342
375
|
};
|
|
@@ -384,6 +417,7 @@ export const DELIVERY_SCHEMA = {
|
|
|
384
417
|
acceptanceEval: ACCEPTANCE_EVAL_SCHEMA,
|
|
385
418
|
feedbackLoop: FEEDBACK_LOOP_SCHEMA,
|
|
386
419
|
ci: CI_DELIVERY_SCHEMA,
|
|
420
|
+
routing: ROUTING_SCHEMA,
|
|
387
421
|
preflight: PREFLIGHT_SCHEMA,
|
|
388
422
|
// Cross-Story concurrency-hazard gate (Story #2297). When true,
|
|
389
423
|
// `epic-deliver-prepare` refuses to flip the Epic to
|
|
@@ -96,6 +96,15 @@ export const QUALITY_SCHEMA = {
|
|
|
96
96
|
codingGuardrails: CODING_GUARDRAILS_SCHEMA,
|
|
97
97
|
autoRefresh: AUTO_REFRESH_SCHEMA,
|
|
98
98
|
baselineEpsilon: BASELINE_EPSILON_SCHEMA,
|
|
99
|
+
// Story #4495. Fail-closed baseline-enforcement policy for the unified
|
|
100
|
+
// check-baselines close-validation gate. Default false: a consumer that
|
|
101
|
+
// enables baseline gates but has not committed baseline artifacts under
|
|
102
|
+
// baselines/ gets a clean skip-with-reason from buildDefaultGates rather
|
|
103
|
+
// than a deterministic first-try close failure. Set true to keep the gate
|
|
104
|
+
// registered so an absent baseline artifact fails close-validation with a
|
|
105
|
+
// preflight hint (the fail-closed posture, analogous to
|
|
106
|
+
// delivery.ci.requireChecks).
|
|
107
|
+
requireBaselines: { type: 'boolean' },
|
|
99
108
|
// Navigability lens + post-wave integration gate config (Epic #4131,
|
|
100
109
|
// F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and the
|
|
101
110
|
// deliver-epic.md Phase 6.5 gate (journey suite). Opt-in: absent or empty
|
|
@@ -24,10 +24,16 @@
|
|
|
24
24
|
* - `onDemand` — the on-demand `.agents/rules/*.md` set (instructions.md
|
|
25
25
|
* § 1.F): every rule file that is **not** part of the
|
|
26
26
|
* always-on core already captured in `alwaysLoaded`.
|
|
27
|
+
* - `agentBoot` — the role-scoped boot contexts `.agents/agents/*.md`
|
|
28
|
+
* (issue #4478). Each is a standalone system prompt a
|
|
29
|
+
* converted spawn boots on **instead of** the always-loaded
|
|
30
|
+
* closure, so it is budgeted independently (per-file ≤8KB
|
|
31
|
+
* ceiling gated by `check-context-budget.js`).
|
|
27
32
|
*
|
|
28
33
|
* A file that could appear in more than one tier is kept in its **highest**
|
|
29
34
|
* tier only (alwaysLoaded > mandatoryRead > digestVisible > onDemand), so the
|
|
30
|
-
*
|
|
35
|
+
* arrays partition the doc set with no double-counting. `agentBoot` is disjoint
|
|
36
|
+
* from the read-tiers (it lives under `.agents/agents/`, not the doc/rules set).
|
|
31
37
|
*
|
|
32
38
|
* The closure is discovered by parsing `@`-import references and following
|
|
33
39
|
* them recursively (cycle-safe via a visited set). A candidate `@`-token only
|
|
@@ -216,6 +222,7 @@ export function docsContextPaths(config) {
|
|
|
216
222
|
* mandatoryRead: Array<{ path: string, bytes: number }>,
|
|
217
223
|
* digestVisible: Array<{ path: string, bytes: number }>,
|
|
218
224
|
* onDemand: Array<{ path: string, bytes: number }>,
|
|
225
|
+
* agentBoot: Array<{ path: string, bytes: number }>,
|
|
219
226
|
* } }}
|
|
220
227
|
*/
|
|
221
228
|
export function resolveDocTiers(
|
|
@@ -254,7 +261,35 @@ export function resolveDocTiers(
|
|
|
254
261
|
// (the always-on ones already live in the alwaysLoaded closure).
|
|
255
262
|
const onDemand = collect(listOnDemandRules(root, fs));
|
|
256
263
|
|
|
257
|
-
|
|
264
|
+
// 5. agent-boot: role-scoped boot contexts .agents/agents/*.md (#4478). These
|
|
265
|
+
// are standalone system prompts, disjoint from the doc read-tiers.
|
|
266
|
+
const agentBoot = collect(listAgentDefs(root, fs));
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
tiers: { alwaysLoaded, mandatoryRead, digestVisible, onDemand, agentBoot },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* List the role-scoped agent-boot defs (repo-relative posix): every
|
|
275
|
+
* `.agents/agents/*.md`. Returns [] when the directory is absent.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} root absolute repo root
|
|
278
|
+
* @param {FsLike} fs
|
|
279
|
+
* @returns {string[]}
|
|
280
|
+
*/
|
|
281
|
+
function listAgentDefs(root, fs) {
|
|
282
|
+
const agentsDir = path.resolve(root, '.agents', 'agents');
|
|
283
|
+
let names;
|
|
284
|
+
try {
|
|
285
|
+
names = fs.readdirSync(agentsDir);
|
|
286
|
+
} catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
return names
|
|
290
|
+
.filter((n) => n.endsWith('.md'))
|
|
291
|
+
.map((n) => path.posix.join('.agents', 'agents', n))
|
|
292
|
+
.sort();
|
|
258
293
|
}
|
|
259
294
|
|
|
260
295
|
/**
|