mandrel 1.93.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/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-critic-conditions.js +3 -2
- 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 +1 -1
- package/.agents/scripts/lib/provider-factory.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- 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.md +25 -23
- package/.agents/workflows/mandrel-update.md +1 -1
- package/docs/CHANGELOG.md +16 -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 -199
- 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
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
* session and every subagent spawn (instructions.md § 4).
|
|
13
13
|
* - `mandatoryRead` — the resolved `project.docsContextFiles` set.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* It additionally enforces a **per-file** ceiling on the role-scoped agent-boot
|
|
16
|
+
* tier (`.agents/agents/*.md`, #4478): no single boot context may exceed
|
|
17
|
+
* `agentBoot.ceilingBytes` (default 8192). This is a per-agent cap, not a sum
|
|
18
|
+
* ratchet — each role def is a standalone system prompt a converted spawn boots
|
|
19
|
+
* on, and adding another role def is legitimate.
|
|
20
|
+
*
|
|
21
|
+
* A read-tier that resolves **empty** is skipped silently (the `docsContextFiles`
|
|
16
22
|
* half skips when unconfigured / its files are absent), so a repo with no
|
|
17
23
|
* `CLAUDE.md` and no context docs is a clean no-op.
|
|
18
24
|
*
|
|
@@ -60,6 +66,31 @@ export const GATED_TIERS = ['alwaysLoaded', 'mandatoryRead'];
|
|
|
60
66
|
*/
|
|
61
67
|
export const DEFAULT_TOLERANCE_BYTES = 2048;
|
|
62
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Per-file ceiling (bytes) for the role-scoped agent-boot tier (#4478). Unlike
|
|
71
|
+
* the read-tiers (gated by a total-byte ratchet), each `.agents/agents/*.md`
|
|
72
|
+
* boot context is a **standalone** system prompt a converted spawn boots on, so
|
|
73
|
+
* the meaningful budget is per-agent, not the sum: no single role def may
|
|
74
|
+
* exceed this ceiling. Adding another role def is legitimate — a per-file gate
|
|
75
|
+
* (rather than a sum ratchet) does not false-positive on that.
|
|
76
|
+
* @type {number}
|
|
77
|
+
*/
|
|
78
|
+
export const AGENT_BOOT_CEILING_BYTES = 8192;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return the agent-boot files that exceed the per-file ceiling.
|
|
82
|
+
*
|
|
83
|
+
* @param {{ tiers: Record<string, Array<{ path: string, bytes: number }>> }} tierMap
|
|
84
|
+
* @param {number} ceiling
|
|
85
|
+
* @returns {Array<{ path: string, bytes: number, ceiling: number }>}
|
|
86
|
+
*/
|
|
87
|
+
export function agentBootOverflow(tierMap, ceiling = AGENT_BOOT_CEILING_BYTES) {
|
|
88
|
+
const files = tierMap?.tiers?.agentBoot ?? [];
|
|
89
|
+
return files
|
|
90
|
+
.filter((f) => Number.isFinite(f?.bytes) && f.bytes > ceiling)
|
|
91
|
+
.map((f) => ({ path: f.path, bytes: f.bytes, ceiling }));
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
/**
|
|
64
95
|
* Parse argv for `--baseline <path>`, `--root <path>`, `--update`, `--json`.
|
|
65
96
|
* Exported so unit tests can pin the parser.
|
|
@@ -127,11 +158,19 @@ export function buildBaseline(tierMap, toleranceBytes) {
|
|
|
127
158
|
const files = tierMap.tiers[name] ?? [];
|
|
128
159
|
tiers[name] = { totalBytes: tierTotalBytes(files), files };
|
|
129
160
|
}
|
|
161
|
+
// The agent-boot tier is recorded top-level (not under `tiers`) because it is
|
|
162
|
+
// gated by a per-file ceiling, not the total-byte ratchet the `tiers` entries
|
|
163
|
+
// use — keeping it out of `tiers` keeps the ratchet diff loop unambiguous.
|
|
164
|
+
const agentBootFiles = tierMap.tiers.agentBoot ?? [];
|
|
130
165
|
return {
|
|
131
166
|
$schema: 'https://mandrel.dev/baselines/context-budget.schema.json',
|
|
132
167
|
generatedAt: new Date().toISOString(),
|
|
133
168
|
toleranceBytes,
|
|
134
169
|
tiers,
|
|
170
|
+
agentBoot: {
|
|
171
|
+
ceilingBytes: AGENT_BOOT_CEILING_BYTES,
|
|
172
|
+
files: agentBootFiles,
|
|
173
|
+
},
|
|
135
174
|
};
|
|
136
175
|
}
|
|
137
176
|
|
|
@@ -278,7 +317,11 @@ export async function runCli({
|
|
|
278
317
|
}
|
|
279
318
|
|
|
280
319
|
const diff = diffBudget(tierMap, baseline);
|
|
281
|
-
const
|
|
320
|
+
const ceiling = Number.isFinite(baseline?.agentBoot?.ceilingBytes)
|
|
321
|
+
? baseline.agentBoot.ceilingBytes
|
|
322
|
+
: AGENT_BOOT_CEILING_BYTES;
|
|
323
|
+
const bootOverflow = agentBootOverflow(tierMap, ceiling);
|
|
324
|
+
const exitCode = diff.grown.length > 0 || bootOverflow.length > 0 ? 1 : 0;
|
|
282
325
|
|
|
283
326
|
if (json) {
|
|
284
327
|
const envelope = {
|
|
@@ -293,17 +336,31 @@ export async function runCli({
|
|
|
293
336
|
grown: diff.grown,
|
|
294
337
|
shrunk: diff.shrunk,
|
|
295
338
|
skipped: diff.skipped,
|
|
339
|
+
agentBootCeilingBytes: ceiling,
|
|
340
|
+
agentBootOverflow: bootOverflow,
|
|
296
341
|
exitCode,
|
|
297
342
|
};
|
|
298
343
|
stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
299
344
|
} else {
|
|
300
345
|
stdout.write(`\n--- context-budget preview ---\n`);
|
|
301
346
|
stdout.write(`${renderDiff(diff)}\n`);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
347
|
+
for (const o of bootOverflow) {
|
|
348
|
+
stdout.write(
|
|
349
|
+
`+ agentBoot: ${o.path} is ${o.bytes} bytes, over the ${o.ceiling}-byte per-agent ceiling\n`,
|
|
305
350
|
);
|
|
306
351
|
}
|
|
352
|
+
if (exitCode === 1) {
|
|
353
|
+
if (bootOverflow.length > 0) {
|
|
354
|
+
stderr.write(
|
|
355
|
+
`[context-budget] ❌ a role-agent boot context exceeds the ${ceiling}-byte per-agent ceiling — trim the role def (the ceiling is a hard cap, not a starve target)\n`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (diff.grown.length > 0) {
|
|
359
|
+
stderr.write(
|
|
360
|
+
`[context-budget] ❌ a documentation tier grew beyond tolerance — refresh the budget consciously with \`node .agents/scripts/check-context-budget.js --update\` once the growth is intentional\n`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
307
364
|
}
|
|
308
365
|
|
|
309
366
|
return exitCode;
|
|
@@ -81,12 +81,6 @@ const FRICTION_RULES = [
|
|
|
81
81
|
remediation:
|
|
82
82
|
' - Syntax/parsing error. Check recently modified files for missing brackets, quotes, or invalid structures.',
|
|
83
83
|
},
|
|
84
|
-
{
|
|
85
|
-
markers: ['Astro', 'astro'],
|
|
86
|
-
category: 'Missing Skill',
|
|
87
|
-
remediation:
|
|
88
|
-
' - Framework error: Refer to `.agents/skills/stack/frontend/astro/SKILL.md` for Astro rules.',
|
|
89
|
-
},
|
|
90
84
|
];
|
|
91
85
|
|
|
92
86
|
const FRICTION_DEFAULT = {
|
|
@@ -39,10 +39,21 @@ import { parseArgs } from 'node:util';
|
|
|
39
39
|
import { runBootSweep } from './boot-sweep.js';
|
|
40
40
|
import { buildChecklistPayload } from './lib/audit-suite/index.js';
|
|
41
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
import {
|
|
43
|
+
getPaths,
|
|
44
|
+
getRunners,
|
|
45
|
+
resolveConfig,
|
|
46
|
+
resolveRuntime,
|
|
47
|
+
} from './lib/config-resolver.js';
|
|
48
|
+
import { cachedGitFetch } from './lib/git/cached-fetch.js';
|
|
49
|
+
import {
|
|
50
|
+
ensureEpicBranchRef,
|
|
51
|
+
currentBranch as gitCurrentBranch,
|
|
52
|
+
} from './lib/git-branch-lifecycle.js';
|
|
53
|
+
import { getEpicBranch, gitSpawn, gitSync } from './lib/git-utils.js';
|
|
45
54
|
import { Logger } from './lib/Logger.js';
|
|
55
|
+
import { ACCEPTANCE_NA, TYPE_LABELS } from './lib/label-constants.js';
|
|
56
|
+
import { parseDeliverySlicingTable } from './lib/orchestration/consolidation-precondition.js';
|
|
46
57
|
import { ensureDocsDigest } from './lib/orchestration/docs-digest.js';
|
|
47
58
|
import {
|
|
48
59
|
resolveOperator,
|
|
@@ -50,6 +61,7 @@ import {
|
|
|
50
61
|
} from './lib/orchestration/epic-deliver-lease-guard.js';
|
|
51
62
|
import {
|
|
52
63
|
initialize as initializeEpicRunState,
|
|
64
|
+
initializeSingle as initializeEpicRunStateSingle,
|
|
53
65
|
write as writeEpicRunState,
|
|
54
66
|
} from './lib/orchestration/epic-run-state-store.js';
|
|
55
67
|
import {
|
|
@@ -72,7 +84,7 @@ import {
|
|
|
72
84
|
} from './lib/orchestration/ticket-lease.js';
|
|
73
85
|
import { createProvider } from './lib/provider-factory.js';
|
|
74
86
|
|
|
75
|
-
const HELP = `Usage: node .agents/scripts/epic-deliver-prepare.js --epic <epicId> [--ignore-concurrency-hazards] [--steal] [--as <handle>]
|
|
87
|
+
const HELP = `Usage: node .agents/scripts/epic-deliver-prepare.js --epic <epicId> [--single] [--ignore-concurrency-hazards] [--steal] [--as <handle>]
|
|
76
88
|
|
|
77
89
|
Snapshots Epic #<id>, builds the wave DAG, initializes the epic-run-state
|
|
78
90
|
checkpoint, and prints the per-wave dispatch plan as JSON. Before any of that,
|
|
@@ -81,6 +93,15 @@ runs two fail-closed preflight guards (Story #3482): a checkout-safety check
|
|
|
81
93
|
(refuse on a live foreign claim).
|
|
82
94
|
|
|
83
95
|
Options:
|
|
96
|
+
--single Single-delivery prepare (Epic #4475). Short-
|
|
97
|
+
circuits Story enumeration: seeds epic/<id>,
|
|
98
|
+
materializes ONE worktree on it, and writes a
|
|
99
|
+
slice-map epic-run-state (deliveryShape:
|
|
100
|
+
"single", storyCount: 0) from the Epic body's
|
|
101
|
+
## Delivery Slicing table. Refuses
|
|
102
|
+
acceptance::n-a (fail-closed). INERT until
|
|
103
|
+
M4-B: nothing in production drives this flag
|
|
104
|
+
yet.
|
|
84
105
|
--ignore-concurrency-hazards Bypass the cross-Story concurrency-hazard
|
|
85
106
|
gate (Story #2297). The flag's use is
|
|
86
107
|
recorded on the Epic checkpoint so retro
|
|
@@ -424,6 +445,239 @@ export async function writeStoryChecklists({
|
|
|
424
445
|
);
|
|
425
446
|
}
|
|
426
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Seed `epic/<id>` and materialize the ONE worktree the single-delivery
|
|
450
|
+
* executor walks — the single-delivery counterpart to `single-story-init.js`'s
|
|
451
|
+
* single-worktree seed (Epic #4475). Fetches origin so remote-tracking refs
|
|
452
|
+
* are authoritative, publishes the Epic integration branch via the shared
|
|
453
|
+
* `ensureEpicBranchRef` seeder (the same helper `branch-initializer.js` uses),
|
|
454
|
+
* then adds a worktree at `.worktrees/epic-<id>/` on `epic/<id>` — idempotent,
|
|
455
|
+
* reused on a re-prepare. When worktree isolation is off it checks the branch
|
|
456
|
+
* out on the main tree instead (mirroring `provisionWorktree`).
|
|
457
|
+
*
|
|
458
|
+
* Skipped in the same injected-test shape the preflight guards use (a provider
|
|
459
|
+
* injected with no git seam) so unit tests never spawn real git.
|
|
460
|
+
*
|
|
461
|
+
* @param {{
|
|
462
|
+
* epicId: number,
|
|
463
|
+
* cwd: string,
|
|
464
|
+
* baseBranch: string,
|
|
465
|
+
* worktreeEnabled: boolean,
|
|
466
|
+
* progress?: (stage: string, msg: string) => void,
|
|
467
|
+
* }} args
|
|
468
|
+
* @returns {{ epicBranch: string, workCwd: string, worktreeCreated: boolean }}
|
|
469
|
+
*/
|
|
470
|
+
export function provisionEpicWorktree({
|
|
471
|
+
epicId,
|
|
472
|
+
cwd,
|
|
473
|
+
baseBranch,
|
|
474
|
+
worktreeEnabled,
|
|
475
|
+
progress = () => {},
|
|
476
|
+
}) {
|
|
477
|
+
const epicBranch = getEpicBranch(epicId);
|
|
478
|
+
|
|
479
|
+
// Fetch origin so `ensureEpicBranchRef` can read remote-tracking refs
|
|
480
|
+
// instead of a second network round-trip (mirrors materializeBaseBranch).
|
|
481
|
+
cachedGitFetch(cwd, 'origin');
|
|
482
|
+
ensureEpicBranchRef(epicBranch, baseBranch, cwd, { progress });
|
|
483
|
+
|
|
484
|
+
if (!worktreeEnabled) {
|
|
485
|
+
// Single-tree mode: check out the Epic integration branch in place.
|
|
486
|
+
gitSync(cwd, 'checkout', epicBranch);
|
|
487
|
+
progress('WORKTREE', `Checked out ${epicBranch} on the main tree.`);
|
|
488
|
+
return { epicBranch, workCwd: cwd, worktreeCreated: false };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const worktreeRoot = path.join(cwd, '.worktrees');
|
|
492
|
+
const wtPath = path.join(worktreeRoot, `epic-${epicId}`);
|
|
493
|
+
fs.mkdirSync(worktreeRoot, { recursive: true });
|
|
494
|
+
|
|
495
|
+
const listed = gitSpawn(cwd, 'worktree', 'list', '--porcelain');
|
|
496
|
+
const alreadyPresent =
|
|
497
|
+
listed.status === 0 && (listed.stdout ?? '').includes(wtPath);
|
|
498
|
+
if (alreadyPresent) {
|
|
499
|
+
progress('WORKTREE', `♻️ Reusing worktree: ${wtPath}`);
|
|
500
|
+
return { epicBranch, workCwd: wtPath, worktreeCreated: false };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const res = gitSpawn(cwd, 'worktree', 'add', wtPath, epicBranch);
|
|
504
|
+
if (res.status !== 0) {
|
|
505
|
+
const stderr = res.stderr || res.stdout || '';
|
|
506
|
+
if (/already (exists|checked out)/.test(stderr)) {
|
|
507
|
+
progress('WORKTREE', `♻️ Reusing worktree (race): ${wtPath}`);
|
|
508
|
+
return { epicBranch, workCwd: wtPath, worktreeCreated: false };
|
|
509
|
+
}
|
|
510
|
+
throw new Error(
|
|
511
|
+
`epic-deliver-prepare --single: git worktree add failed for epic-${epicId}: ${stderr}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
progress('WORKTREE', `✨ Created worktree: ${wtPath}`);
|
|
515
|
+
return { epicBranch, workCwd: wtPath, worktreeCreated: true };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Step 0/1 of `/deliver` for a single-delivery Epic (Epic #4475, design §S1).
|
|
520
|
+
*
|
|
521
|
+
* The single-delivery counterpart to `runEpicDeliverPrepare`. It short-circuits
|
|
522
|
+
* Story enumeration entirely — a spec-only plan authored NO Story tickets, so
|
|
523
|
+
* there is nothing to fan out. Instead it:
|
|
524
|
+
*
|
|
525
|
+
* 1. Refuses `acceptance::n-a` (fail-closed front gate). Under single
|
|
526
|
+
* delivery the non-waivable epic-level acceptance reconcile is the ONLY
|
|
527
|
+
* acceptance gate; an Epic that declares "no acceptance criteria" is
|
|
528
|
+
* structurally incoherent with that contract.
|
|
529
|
+
* 2. Runs the same fail-closed preflight guards (checkout-safety + Epic
|
|
530
|
+
* lease) as the fan-out prepare.
|
|
531
|
+
* 3. Seeds `epic/<id>` and materializes ONE worktree on it.
|
|
532
|
+
* 4. Parses the Epic body's `## Delivery Slicing` table and writes an
|
|
533
|
+
* `epic-run-state` **slice map** (`deliveryShape: "single"`,
|
|
534
|
+
* `storyCount: 0`, `concurrencyCap: 1`) — idempotent + resume-preserving
|
|
535
|
+
* (a re-run keeps every already-`done` slice).
|
|
536
|
+
* 5. Writes the per-Epic docs digest.
|
|
537
|
+
*
|
|
538
|
+
* BEHAVIOR-PRESERVING (M4-A): this function is reachable only through the
|
|
539
|
+
* `--single` flag, which nothing in production drives yet (the `deliver.md`
|
|
540
|
+
* router's single verdict falls through to the fan-out helper until M4-B). The
|
|
541
|
+
* slice-map checkpoint round-trips but no executor consumes it here.
|
|
542
|
+
*
|
|
543
|
+
* @param {object} args — same DI surface as `runEpicDeliverPrepare` minus the
|
|
544
|
+
* concurrency-hazard knobs (single delivery fans out nothing to gate).
|
|
545
|
+
* @returns {Promise<{
|
|
546
|
+
* epicId: number,
|
|
547
|
+
* deliveryShape: 'single',
|
|
548
|
+
* storyCount: 0,
|
|
549
|
+
* concurrencyCap: number,
|
|
550
|
+
* sliceCount: number,
|
|
551
|
+
* slices: Record<string, { status: string, title?: string }>,
|
|
552
|
+
* epicBranch: string,
|
|
553
|
+
* workCwd: string,
|
|
554
|
+
* worktreeCreated: boolean,
|
|
555
|
+
* checkpointInitializedAt: string,
|
|
556
|
+
* docsDigestPath: string|null,
|
|
557
|
+
* }>}
|
|
558
|
+
*/
|
|
559
|
+
export async function runEpicDeliverPrepareSingle({
|
|
560
|
+
epicId,
|
|
561
|
+
cwd,
|
|
562
|
+
injectedProvider,
|
|
563
|
+
injectedConfig,
|
|
564
|
+
asOperator,
|
|
565
|
+
steal = false,
|
|
566
|
+
injectedGit,
|
|
567
|
+
leaseHeartbeatAt,
|
|
568
|
+
leaseNow,
|
|
569
|
+
skipPreflightGuards = false,
|
|
570
|
+
} = {}) {
|
|
571
|
+
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
572
|
+
throw new TypeError(
|
|
573
|
+
'runEpicDeliverPrepareSingle: --epic must be a positive integer',
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const config = injectedConfig ?? resolveConfig({ cwd });
|
|
578
|
+
if (!config.github) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
'runEpicDeliverPrepareSingle: no github block in .agentrc.json',
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const provider = injectedProvider ?? createProvider(config);
|
|
584
|
+
// Single delivery collapses the whole Epic into ONE guarded in-session slice
|
|
585
|
+
// walk — the concurrency cap is 1 by definition (nothing fans out).
|
|
586
|
+
const concurrencyCap = 1;
|
|
587
|
+
|
|
588
|
+
const epic = await provider.getTicket(epicId);
|
|
589
|
+
const labels = Array.isArray(epic?.labels) ? epic.labels : [];
|
|
590
|
+
if (!labels.includes(TYPE_LABELS.EPIC)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`runEpicDeliverPrepareSingle: #${epicId} is not a ${TYPE_LABELS.EPIC} (labels: ${labels.join(', ') || 'none'}).`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Fail-closed front gate (design §"Non-waivable epic reconcile"): under
|
|
597
|
+
// single delivery the epic-level acceptance reconcile is the ONLY acceptance
|
|
598
|
+
// gate that runs — there is no per-Story self-eval critic tier behind it. An
|
|
599
|
+
// Epic labelled `acceptance::n-a` (no acceptance criteria) is therefore
|
|
600
|
+
// structurally incoherent with single delivery: it would waive the sole
|
|
601
|
+
// gate. Refuse loudly instead of silently shipping ungated.
|
|
602
|
+
if (labels.includes(ACCEPTANCE_NA)) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`[epic-deliver-prepare] BLOCKER: Epic #${epicId} carries ${ACCEPTANCE_NA}, ` +
|
|
605
|
+
'but single delivery makes the non-waivable epic-level acceptance ' +
|
|
606
|
+
'reconcile the ONLY acceptance gate — an Epic with no acceptance ' +
|
|
607
|
+
'criteria would ship ungated. Remove the label (author an ' +
|
|
608
|
+
'## Acceptance Table), or re-plan the Epic as fan-out.',
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
await runPreflightGuardsForPrepare({
|
|
613
|
+
epicId,
|
|
614
|
+
cwd,
|
|
615
|
+
config,
|
|
616
|
+
provider,
|
|
617
|
+
injectedProvider,
|
|
618
|
+
injectedGit,
|
|
619
|
+
asOperator,
|
|
620
|
+
steal,
|
|
621
|
+
leaseHeartbeatAt,
|
|
622
|
+
leaseNow,
|
|
623
|
+
skipPreflightGuards,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const baseBranch = config.project?.baseBranch ?? 'main';
|
|
627
|
+
const runtime = resolveRuntime({ config });
|
|
628
|
+
|
|
629
|
+
// Seed epic/<id> + materialize the one worktree. Skipped in the injected-
|
|
630
|
+
// test shape (a provider injected with no git seam) so unit tests never
|
|
631
|
+
// spawn real git — matching the preflight-guard suppression rule.
|
|
632
|
+
const worktreeSuppressed =
|
|
633
|
+
skipPreflightGuards || (Boolean(injectedProvider) && !injectedGit);
|
|
634
|
+
let epicBranch = getEpicBranch(epicId);
|
|
635
|
+
let workCwd = cwd ?? process.cwd();
|
|
636
|
+
let worktreeCreated = false;
|
|
637
|
+
if (!worktreeSuppressed) {
|
|
638
|
+
({ epicBranch, workCwd, worktreeCreated } = provisionEpicWorktree({
|
|
639
|
+
epicId,
|
|
640
|
+
cwd: cwd ?? process.cwd(),
|
|
641
|
+
baseBranch,
|
|
642
|
+
worktreeEnabled: runtime.worktreeEnabled,
|
|
643
|
+
progress: (stage, msg) =>
|
|
644
|
+
Logger.info(`[epic-deliver-prepare:single] ${stage} ${msg}`),
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Parse the Epic body's `## Delivery Slicing` table — the single mode's
|
|
649
|
+
// audit trail and the source of the slice map. A missing/unparseable table
|
|
650
|
+
// yields an empty slice set (the executor has nothing to walk); that is a
|
|
651
|
+
// plan-quality problem surfaced downstream, not a prepare-time throw.
|
|
652
|
+
const slices = parseDeliverySlicingTable(epic?.body ?? '') ?? [];
|
|
653
|
+
|
|
654
|
+
const checkpointState = await initializeEpicRunStateSingle({
|
|
655
|
+
provider,
|
|
656
|
+
epicId,
|
|
657
|
+
slices,
|
|
658
|
+
concurrencyCap,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const docsDigestPath = await writeDocsDigest({ epicId, cwd, config });
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
epicId,
|
|
665
|
+
deliveryShape: 'single',
|
|
666
|
+
storyCount: 0,
|
|
667
|
+
concurrencyCap,
|
|
668
|
+
sliceCount: Object.keys(checkpointState.slices ?? {}).length,
|
|
669
|
+
slices: checkpointState.slices ?? {},
|
|
670
|
+
epicBranch,
|
|
671
|
+
workCwd,
|
|
672
|
+
worktreeCreated,
|
|
673
|
+
checkpointInitializedAt:
|
|
674
|
+
checkpointState.startedAt ??
|
|
675
|
+
checkpointState.lastUpdatedAt ??
|
|
676
|
+
new Date().toISOString(),
|
|
677
|
+
docsDigestPath,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
427
681
|
export async function runEpicDeliverPrepare({
|
|
428
682
|
epicId,
|
|
429
683
|
cwd,
|
|
@@ -559,6 +813,7 @@ async function main() {
|
|
|
559
813
|
const { values } = parseArgs({
|
|
560
814
|
options: {
|
|
561
815
|
epic: { type: 'string' },
|
|
816
|
+
single: { type: 'boolean', default: false },
|
|
562
817
|
help: { type: 'boolean', short: 'h' },
|
|
563
818
|
'ignore-concurrency-hazards': { type: 'boolean', default: false },
|
|
564
819
|
steal: { type: 'boolean', default: false },
|
|
@@ -578,12 +833,19 @@ async function main() {
|
|
|
578
833
|
process.exit(2);
|
|
579
834
|
}
|
|
580
835
|
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
836
|
+
const asOperator = typeof values.as === 'string' ? values.as : undefined;
|
|
837
|
+
const steal = values.steal === true;
|
|
838
|
+
|
|
839
|
+
const result =
|
|
840
|
+
values.single === true
|
|
841
|
+
? await runEpicDeliverPrepareSingle({ epicId, asOperator, steal })
|
|
842
|
+
: await runEpicDeliverPrepare({
|
|
843
|
+
epicId,
|
|
844
|
+
ignoreConcurrencyHazards:
|
|
845
|
+
values['ignore-concurrency-hazards'] === true,
|
|
846
|
+
steal,
|
|
847
|
+
asOperator,
|
|
848
|
+
});
|
|
587
849
|
Logger.info(JSON.stringify(result, null, 2));
|
|
588
850
|
}
|
|
589
851
|
|
|
@@ -24,6 +24,12 @@ import { applyQualityBootstrap } from './quality-bootstrap.js';
|
|
|
24
24
|
|
|
25
25
|
export const SYNC_COMMAND = 'node .agents/scripts/sync-claude-commands.js';
|
|
26
26
|
|
|
27
|
+
// Epic #4478 (M7-B). The role-scoped agent projection runs in every place the
|
|
28
|
+
// command projection does — `sync:agents` script, `prepare`, and the bootstrap
|
|
29
|
+
// `sync` phase (`runSyncCommands`) — so `.claude/agents/*.md` materializes for
|
|
30
|
+
// hosts that read it, exactly as `.claude/commands/*.md` does.
|
|
31
|
+
export const SYNC_AGENTS_COMMAND = 'node .agents/scripts/sync-claude-agents.js';
|
|
32
|
+
|
|
27
33
|
export const BOOTSTRAP_COMMAND = 'node .agents/scripts/bootstrap.js';
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -202,6 +208,7 @@ export function ensurePackageJson(ctx) {
|
|
|
202
208
|
const outcomes = {
|
|
203
209
|
created: false,
|
|
204
210
|
scriptsSyncCommands: 'already-present',
|
|
211
|
+
scriptsSyncAgents: 'already-present',
|
|
205
212
|
scriptsPrepare: 'already-present',
|
|
206
213
|
scriptsBootstrap: 'already-present',
|
|
207
214
|
};
|
|
@@ -220,13 +227,29 @@ export function ensurePackageJson(ctx) {
|
|
|
220
227
|
pkg.scripts['sync:commands'] = SYNC_COMMAND;
|
|
221
228
|
outcomes.scriptsSyncCommands = 'added';
|
|
222
229
|
}
|
|
230
|
+
if (!pkg.scripts['sync:agents']) {
|
|
231
|
+
pkg.scripts['sync:agents'] = SYNC_AGENTS_COMMAND;
|
|
232
|
+
outcomes.scriptsSyncAgents = 'added';
|
|
233
|
+
}
|
|
223
234
|
const prepare = pkg.scripts.prepare;
|
|
224
235
|
if (!prepare) {
|
|
225
|
-
pkg.scripts.prepare = SYNC_COMMAND
|
|
236
|
+
pkg.scripts.prepare = `${SYNC_COMMAND} && ${SYNC_AGENTS_COMMAND}`;
|
|
226
237
|
outcomes.scriptsPrepare = 'added';
|
|
227
|
-
} else
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
} else {
|
|
239
|
+
// Append each projection independently so an existing prepare that already
|
|
240
|
+
// carries the command sync still gains the agent sync on the next bootstrap
|
|
241
|
+
// (and both are idempotent — a prepare carrying both is left untouched).
|
|
242
|
+
let next = prepare;
|
|
243
|
+
if (!next.includes('sync-claude-commands.js')) {
|
|
244
|
+
next = `${next} && ${SYNC_COMMAND}`;
|
|
245
|
+
}
|
|
246
|
+
if (!next.includes('sync-claude-agents.js')) {
|
|
247
|
+
next = `${next} && ${SYNC_AGENTS_COMMAND}`;
|
|
248
|
+
}
|
|
249
|
+
if (next !== prepare) {
|
|
250
|
+
pkg.scripts.prepare = next;
|
|
251
|
+
outcomes.scriptsPrepare = 'appended';
|
|
252
|
+
}
|
|
230
253
|
}
|
|
231
254
|
// Expose a discoverable `npm run bootstrap` alias for the framework
|
|
232
255
|
// setup command. An operator-defined `bootstrap` script always wins —
|
|
@@ -238,6 +261,7 @@ export function ensurePackageJson(ctx) {
|
|
|
238
261
|
const mutated =
|
|
239
262
|
outcomes.created ||
|
|
240
263
|
outcomes.scriptsSyncCommands === 'added' ||
|
|
264
|
+
outcomes.scriptsSyncAgents === 'added' ||
|
|
241
265
|
outcomes.scriptsPrepare !== 'already-present' ||
|
|
242
266
|
outcomes.scriptsBootstrap === 'added';
|
|
243
267
|
if (mutated) writeJson(pkgPath, pkg, fsImpl);
|
|
@@ -482,25 +506,39 @@ function ensureIssueFormsPhase(ctx) {
|
|
|
482
506
|
*/
|
|
483
507
|
export function runSyncCommands(ctx) {
|
|
484
508
|
const { spawnImpl = defaultSpawnSync } = ctx;
|
|
485
|
-
const
|
|
509
|
+
const scriptsDir = path.join(
|
|
486
510
|
ctx.agentRoot ?? path.join(ctx.projectRoot, '.agents'),
|
|
487
511
|
'scripts',
|
|
488
|
-
'sync-claude-commands.js',
|
|
489
512
|
);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
513
|
+
// Both projections run here (Epic #4478, M7-B): the command tree AND the
|
|
514
|
+
// role-scoped agent tree, so `mandrel sync` / the postinstall path
|
|
515
|
+
// materializes `.claude/agents/*.md` alongside `.claude/commands/*.md`.
|
|
516
|
+
const projections = [
|
|
517
|
+
{ label: 'sync-claude-commands.js', script: 'sync-claude-commands.js' },
|
|
518
|
+
{ label: 'sync-claude-agents.js', script: 'sync-claude-agents.js' },
|
|
519
|
+
];
|
|
520
|
+
const stdouts = [];
|
|
521
|
+
for (const { label, script } of projections) {
|
|
522
|
+
const result = spawnImpl(
|
|
523
|
+
process.execPath,
|
|
524
|
+
[path.join(scriptsDir, script)],
|
|
525
|
+
{
|
|
526
|
+
cwd: ctx.projectRoot,
|
|
527
|
+
encoding: 'utf8',
|
|
528
|
+
},
|
|
501
529
|
);
|
|
530
|
+
if (result.status !== 0) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`[Bootstrap] ${label} failed (exit ${result.status}): ${(
|
|
533
|
+
result.stderr ?? ''
|
|
534
|
+
)
|
|
535
|
+
.trim()
|
|
536
|
+
.slice(0, 400)}`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
stdouts.push((result.stdout ?? '').trim());
|
|
502
540
|
}
|
|
503
|
-
return { ok: true, stdout: (
|
|
541
|
+
return { ok: true, stdout: stdouts.filter(Boolean).join('\n') };
|
|
504
542
|
}
|
|
505
543
|
|
|
506
544
|
/**
|