yadflow 2.16.0 → 2.17.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/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [2.16.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.15.0...v2.16.0) (2026-06-25)
1
+ # [2.17.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.16.1...v2.17.0) (2026-06-26)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * make hub pr-title/pr-template gates branch-aware so tooling PRs pass ([#79](https://github.com/abdelrahmannasr/yadflow/issues/79)) ([68050e0](https://github.com/abdelrahmannasr/yadflow/commit/68050e000010b4d98f304a7e5e1f6a39bc0c229c))
6
+ * **discovery:** add yad-discovery project front-zero phase ([#82](https://github.com/abdelrahmannasr/yadflow/issues/82)) ([4bb2a92](https://github.com/abdelrahmannasr/yadflow/commit/4bb2a928ea37cbd7d7b21f7c28a96a20492f527e))
7
7
 
8
8
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
9
9
 
package/README.md CHANGED
@@ -23,10 +23,11 @@ a scaffolded module that installs cleanly, and a working **team review gate** yo
23
23
 
24
24
  ## The workflow at a glance
25
25
 
26
- The whole lifecycle, from an empty project to shipped code. Setup is one-time; the **front half**
27
- is human-gated and runs once per epic in the product hub; the **build half** runs once per story
28
- per code repo; **automation** is opt-in and earned. `yad-status` reads it all; `yad-hub-bridge`
29
- mirrors front-half reviews to real PR/MRs.
26
+ The whole lifecycle, from an empty project to shipped code. Setup is one-time; the optional
27
+ **front-zero** (`yad-discovery`) frames the whole project once market, feasibility, and a phased
28
+ roadmap; the **front half** is human-gated and runs once per epic in the product hub; the **build
29
+ half** runs once per story per code repo; **automation** is opt-in and earned. `yad-status` reads it
30
+ all; `yad-hub-bridge` mirrors front-half reviews to real PR/MRs.
30
31
 
31
32
  <!-- Source: docs/diagrams/sdlc-overview.mmd — edit the .mmd and run `npm run diagrams` to regenerate -->
32
33
  ![Yadflow SDLC overview — setup, human-gated front half, per-story build half, earned automation](https://raw.githubusercontent.com/abdelrahmannasr/yadflow/main/docs/diagrams/sdlc-overview.svg)
@@ -44,6 +45,7 @@ human**. Detailed walkthroughs for each phase follow below.
44
45
  | `RESEARCH-NOTES.md` | Verified Phase 0 facts about BMAD, Spec Kit, Repomix, Impeccable + deviations. |
45
46
  | `skills/sdlc/` | Module source of truth (`config.yaml`, `module-help.csv`, `install.sh`). Survives BMAD updates. |
46
47
  | `bin/`, `cli/` | The `yad` setup/update CLI (published to npm as `yadflow`). |
48
+ | `skills/yad-discovery/` | Optional front-zero (once per project, greenfield + brownfield): market research, competitor study, feasibility, current-state, requirements (functional + non-functional) and a phased roadmap (MVP+) under the reserved `EP-discovery`. `roadmap.md` is the menu of features each epic reads. |
47
49
  | `skills/yad-analysis/` | Optional front state 1: pressure-test the idea with the analyst into `analysis.md` (skippable). |
48
50
  | `skills/yad-epic/` | Front state 1: author an epic with AI assist, assign its `EP-<slug>` ID, seed state. |
49
51
  | `skills/yad-architecture/` | Front state 3: author `architecture.md` + the locked `contract.md`; hash-lock the contract surface. |
@@ -98,8 +100,8 @@ with `npx` from your **product hub** repo — no clone needed.
98
100
  | `yad gate status <epic>` | Show each review step and its recorded approvals. |
99
101
  | `yad gate ci [--branch <head>] [--pr <n>]` | The CI entry the hub workflow calls on review/merge events: derive the epic/artifact from the `review/EP-*` branch, run the same sync, and commit **only the ledger** to the hub default branch (sweep every open review PR when no `--branch`). |
100
102
  | `yad commit --type <t> -m <subject>` | Commit by the SDLC convention — Conventional subject, `Task`/`Contract-Change`/`Co-Authored-By` trailers, atomic-file guard. |
101
- | `yad open-pr [--repo <name>]` | Open a code-repo **task** PR/MR from the repo's platform template (build half). |
102
- | `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`). |
103
+ | `yad open-pr [--repo <name>]` | Open a **task** PR/MR from the platform template (build half). **Stage-aware on the hub:** a `review/EP-*` branch opens the front-half artifact-review PR (delegates to `yad gate open`); any other hub branch uses the code-task template (so hub tooling PRs pass the `pr-template` gate). |
104
+ | `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`) — stage-aware, same as `open-pr`. |
103
105
  | `yad repo list` / `yad repo refresh [name]` | List connected repos as **fresh / stale**, and re-pack a stale one — staleness is now an explicit human decision, never an automatic skill side-effect. |
104
106
  | `yad repo sync [name]` | Switch every connected repo to its **default branch** and fast-forward it from origin (one or all). Dirty repos are skipped, never overwritten; fast-forward only. |
105
107
  | `npx yadflow --version` | Print the installed CLI version. |
@@ -150,7 +152,7 @@ does / why / what to enter / what skipping means), and the step count adapts.
150
152
  0. **Profile** — the three questions above, plus "configure optional tools now?". Pre-answer for
151
153
  CI/scripts with `--solo`/`--team <n>`, `--greenfield`/`--brownfield`, `--monorepo`/`--separate`, `--tools`.
152
154
  1. **Preflight** — confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
153
- 2. **Install the module** — copy all 30 `yad-*` skills into the IDE skill dirs you pick
155
+ 2. **Install the module** — copy all 31 `yad-*` skills into the IDE skill dirs you pick
154
156
  (`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
155
157
  3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
156
158
  **Solo skips the roster** (you review by merging your own PR). Edit the roster any time with `yad roster`.
@@ -204,7 +206,7 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
204
206
 
205
207
  Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
206
208
 
207
- ## Agent skills (all 30)
209
+ ## Agent skills (all 31)
208
210
 
209
211
  The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
210
212
  AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
@@ -274,6 +276,17 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
274
276
  never touches epic state, approvals, or the contract lock. *AI builds, the hand decides* — and now the
275
277
  hand can also learn, on demand, what it is deciding about.
276
278
 
279
+ ### Front-zero — frame the whole project (once per project, optional, human-gated)
280
+
281
+ - **`yad-discovery`** — *Optional* front-zero, for **greenfield and brownfield**. With the analyst
282
+ and pm, run market research, a **competitor study** (both modes), a feasibility study, and — in
283
+ brownfield — a code-aware current-state study, then distil a **functional + non-functional
284
+ requirements** list and a **phased roadmap** (an explicit **MVP** phase, then later phases) under the
285
+ reserved `EP-discovery` ("epic zero"). It is gated by the same review gate (base rule: owner + 1
286
+ reviewer); on approval it terminates at `discovery-done` (no build half). Its `roadmap.md` is the menu
287
+ of features — each `yad-epic` reads it for project context (reference-only; discovery never
288
+ auto-seeds epics).
289
+
277
290
  ### Front half — author the "thinking" (once per epic, human-gated)
278
291
 
279
292
  - **`yad-analysis`** — *Optional* front state 1. With the analyst, pressure-test a feature idea
@@ -423,6 +436,10 @@ drive it deterministically with the **`yad gate`** CLI (`open → sync → …
423
436
  the per-step PR/MR and the step **auto-advances on merge** once approvals are satisfied and all comment
424
437
  threads are resolved. Details: **“Run the full front half by hand”** below.
425
438
 
439
+ 0. *(optional, once per project)* `yad-discovery` → the discovery set (`market-research.md`,
440
+ `competitor-analysis.md`, `current-state.md`, `feasibility.md`, `requirements.md`, `roadmap.md`)
441
+ under the reserved `EP-discovery` → review (base rule) → `currentStep: discovery-done`. The whole
442
+ set is required to review; its `roadmap.md` then frames each epic below (read once it is approved).
426
443
  6. `yad-epic` → `epic.md` (assigns `EP-<slug>`, seeds state) → review (base rule).
427
444
  7. `yad-architecture` → `architecture.md` + locked `contract.md` → review (**escalated**: contract).
428
445
  8. `yad-ui` → `ui-design.md` + `DESIGN.md` → review (base rule).
@@ -468,7 +485,10 @@ Details: **“Run the back half on the dial”** below.
468
485
 
469
486
  ## Run the full front half by hand
470
487
 
471
- The front half walks **epic review architecture+contract review UI design → review → stories
488
+ Optionally preceded once per project by the **front-zero** **`yad-discovery` → review →
489
+ `discovery-done`** — which frames the whole product (market, competitor, feasibility, requirements,
490
+ roadmap) under the reserved `EP-discovery`; its approved `roadmap.md` then feeds each epic. The front
491
+ half itself walks **epic → review → architecture+contract → review → UI design → review → stories
472
492
  → review → `ready-for-build`**, then **test cases → review** runs as a **parallel, non-blocking track**
473
493
  alongside the build half. It is all files under `epics/EP-<slug>/`. The skills below guide you, but you
474
494
  can also edit the files directly — that's the point.
package/bin/yad.mjs CHANGED
@@ -56,8 +56,10 @@ ${c.bold('Review gate (front half)')}
56
56
 
57
57
  ${c.bold('Build helpers')}
58
58
  yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
59
- yad open-pr [--repo <name>] Open a code-repo task PR/MR from the template
60
- yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step
59
+ yad open-pr [--repo <name>] Open a task PR/MR stage-aware on the hub: a review/EP-*
60
+ branch opens the front-half artifact-review PR (delegates to
61
+ gate open), any other hub branch uses the code-task template
62
+ yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
61
63
  yad repo list Show connected repos (fresh / stale)
62
64
  yad repo refresh [name] Re-pack a stale repo (a human decision)
63
65
 
@@ -181,7 +183,7 @@ async function main() {
181
183
  // In bridge mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
182
184
  // advisory (reads the platform, prints status, writes nothing). The artifact status flip is
183
185
  // CI's job at merge — never wired into the local gate. File-only mode keeps local writes.
184
- if (action === 'open') await gateOpen(o.dir, { epic, artifact, today });
186
+ if (action === 'open') await gateOpen(o.dir, { epic, artifact });
185
187
  else if (action === 'sync') await gateSync(o.dir, { epic, artifact, today, local: true });
186
188
  else if (action === 'comments') await gateComments(o.dir, { epic, artifact, today });
187
189
  else if (action === 'status') await gateStatus(o.dir, { epic });
@@ -6,7 +6,7 @@
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { c, log, ok, info, readJSONStrict } from './lib.mjs';
9
- import { epicRoot, artifactBase, artifactFromBase, findReviewStep } from './epic-state.mjs';
9
+ import { epicRoot, artifactBase, artifactFromBase, findReviewStep, DISCOVERY_FILES } from './epic-state.mjs';
10
10
  import { epicFiles } from './manifest.mjs';
11
11
 
12
12
  // The front-gate lifecycle this command manages. Forward-only: a status is only ever moved UP this
@@ -75,6 +75,11 @@ export async function syncStatuses(root, { epic, dryRun = false } = {}) {
75
75
  files.push({ base: 'stories', file: path.join(storiesDir, f) });
76
76
  }
77
77
  }
78
+ // Discovery ("epic zero") set: the project-discovery files all key to the single discovery /
79
+ // discovery-review step pair, so they reconcile together (mirrors the stories/ set above).
80
+ for (const f of DISCOVERY_FILES) {
81
+ if (fs.existsSync(path.join(dir, f))) files.push({ base: 'discovery', file: path.join(dir, f) });
82
+ }
78
83
 
79
84
  for (const { base, file } of files) {
80
85
  if (!fs.existsSync(file)) continue;
package/cli/commit.mjs CHANGED
@@ -7,7 +7,7 @@ import fs from 'node:fs';
7
7
  import { c, log, ok, info, warn, fail, run, exists } from './lib.mjs';
8
8
  import {
9
9
  COMMIT_TYPES, AI_COAUTHORS, ATOMIC_FILE_LIMIT,
10
- TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER,
10
+ TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER, PROJECT_FILES,
11
11
  } from './manifest.mjs';
12
12
 
13
13
  // PURE — unit tested directly. Build the full commit message text.
@@ -51,7 +51,14 @@ export async function runCommit(root, opts = {}) {
51
51
 
52
52
  const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root }).stdout;
53
53
  const task = opts.task || taskFromBranch(branch);
54
- if (!task) warn('no Task trailer (none given and branch has no -S0N-T0N) — spec-link gate will fail on a code repo');
54
+ if (!task) {
55
+ // spec-link is a code-repo gate (REPO_WIRING.common), not a hub gate — so a missing Task trailer
56
+ // is expected on a hub PR (front-half artifact review or hub tooling) and only matters on a repo.
57
+ const onHub = exists(path.join(root, PROJECT_FILES.hubConfig));
58
+ warn(onHub
59
+ ? 'no Task trailer (none given and branch has no -S0N-T0N) — fine for a hub PR; required on code-repo tasks (spec-link gate)'
60
+ : 'no Task trailer (none given and branch has no -S0N-T0N) — spec-link gate will fail on a code repo');
61
+ }
55
62
 
56
63
  let message;
57
64
  try {
@@ -38,6 +38,7 @@ export function parseReviewBranch(branch = '') {
38
38
  // (storiesHash fingerprints the directory), so any story branch syncs the same review step.
39
39
  export function artifactFromBase(base) {
40
40
  if (base === 'stories' || /^stories-S\d+$/i.test(base)) return 'stories/';
41
+ if (base === 'discovery') return 'discovery/';
41
42
  return `${base}.md`;
42
43
  }
43
44
 
@@ -47,6 +48,7 @@ export function artifactFromBase(base) {
47
48
  export function artifactPaths(base) {
48
49
  if (base === 'architecture') return ['architecture.md', 'contract.md', '.sdlc/contract-lock.json'];
49
50
  if (base === 'stories') return ['stories'];
51
+ if (base === 'discovery') return [...DISCOVERY_FILES];
50
52
  return [`${base}.md`];
51
53
  }
52
54
 
@@ -87,13 +89,41 @@ export function storiesHash(epicDir) {
87
89
  return 'sha256:' + createHash('sha256').update(parts.join('\n')).digest('hex');
88
90
  }
89
91
 
92
+ // The reserved id of the project front-zero ("epic zero"). yad-discovery seeds it; yad-epic /
93
+ // yad-analysis must never pick this slug for a feature.
94
+ export const DISCOVERY_EPIC = 'EP-discovery';
95
+
96
+ // The project-discovery artifact set (EP-discovery / "epic zero"). The `discovery-review` step binds
97
+ // to the whole set, mirroring how stories-review binds to the stories/ directory — editing any file
98
+ // revokes prior approvals. A fixed list (not a dir scan) because the files live in the epic root.
99
+ export const DISCOVERY_FILES = [
100
+ 'market-research.md',
101
+ 'competitor-analysis.md',
102
+ 'current-state.md',
103
+ 'feasibility.md',
104
+ 'requirements.md',
105
+ 'roadmap.md',
106
+ ];
107
+
108
+ // Deterministic fingerprint of the discovery set: hash every file in the fixed DISCOVERY_FILES order,
109
+ // combine. The WHOLE set is the reviewable unit — if any required artifact is missing the discovery is
110
+ // incomplete and NON-REVIEWABLE, so this returns null (no hash to bind an approval to), the same
111
+ // "nothing to lock" signal storiesHash/contractSurfaceHash give for an absent/malformed surface. Once
112
+ // the full set exists, an edit (or deletion) of any file changes the hash and revokes prior approvals.
113
+ export function discoveryHash(epicDir) {
114
+ if (!DISCOVERY_FILES.every((f) => fs.existsSync(path.join(epicDir, f)))) return null;
115
+ const parts = DISCOVERY_FILES.map((f) => `${f}:${fileSha(path.join(epicDir, f))}`);
116
+ return 'sha256:' + createHash('sha256').update(parts.join('\n')).digest('hex');
117
+ }
118
+
90
119
  // The content fingerprint an approval is bound to. For architecture the fingerprint is the locked
91
- // contract surface (a re-lock => stale); for stories it is the whole stories/ set; for every other
92
- // artifact it is the file's bytes.
120
+ // contract surface (a re-lock => stale); for stories it is the whole stories/ set; for discovery it is
121
+ // the whole discovery file set; for every other artifact it is the file's bytes.
93
122
  export function artifactHash(epicDir, artifact) {
94
123
  const b = artifactBase(artifact);
95
124
  if (b === 'architecture') return contractSurfaceHash(epicDir);
96
125
  if (b === 'stories') return storiesHash(epicDir);
126
+ if (b === 'discovery') return discoveryHash(epicDir);
97
127
  return fileSha(path.join(epicDir, artifact.replace(/\/$/, '')));
98
128
  }
99
129
 
@@ -221,6 +251,13 @@ export function advanceState(state, step) {
221
251
  state.currentStep = 'ready-for-build';
222
252
  return state;
223
253
  }
254
+ // Discovery is the project front-zero ("epic zero"): it has no build half, so its review terminates
255
+ // at a `discovery-done` sentinel rather than `ready-for-build` (which would make `yad next` claim the
256
+ // build half can run). The roadmap it approved is the input the real feature epics read.
257
+ if (step.id === 'discovery-review') {
258
+ state.currentStep = 'discovery-done';
259
+ return state;
260
+ }
224
261
  const next = state.steps[i + 1];
225
262
  if (next) {
226
263
  next.status = next.type === 'review+approve' ? 'in_review' : 'in_progress';
@@ -244,6 +281,7 @@ export function markInReview(state, step) {
244
281
  // The front authoring step a `yad next` action maps to — the skill the user invokes for that step.
245
282
  // Review (review+approve) steps are driven by the `yad gate` CLI, not a skill, so they are not here.
246
283
  export const STEP_SKILL = {
284
+ discovery: 'yad-discovery',
247
285
  analysis: 'yad-analysis',
248
286
  epic: 'yad-epic',
249
287
  architecture: 'yad-architecture',
@@ -258,8 +296,8 @@ export const STEP_SKILL = {
258
296
  // (the Phase B rail) and by the driver. No FS / network.
259
297
  export function preconditionsMet(state, stepId) {
260
298
  if (!state || !Array.isArray(state.steps)) {
261
- const ok = stepId === 'epic' || stepId === 'analysis';
262
- return { ok, blockedBy: null, reason: ok ? 'entry step (no epic seeded yet)' : `start with yad-epic — no epic state for '${stepId}'` };
299
+ const ok = stepId === 'epic' || stepId === 'analysis' || stepId === 'discovery';
300
+ return { ok, blockedBy: null, reason: ok ? 'entry step (no state seeded yet)' : `start with yad-epic — no epic state for '${stepId}'` };
263
301
  }
264
302
  const i = state.steps.findIndex((s) => s.id === stepId);
265
303
  if (i === -1) return { ok: false, blockedBy: null, reason: `unknown step '${stepId}'` };
@@ -281,6 +319,30 @@ export function nextAction(ledger, { epic } = {}) {
281
319
  const epicId = epic || state?.epicId || null;
282
320
  if (!state) return { epicId, kind: 'new', skill: 'yad-epic', why: 'no epic state yet — seed it with yad-epic' };
283
321
 
322
+ // EP-discovery ("epic zero") is the project front-zero: a 2-step author→review chain with no build
323
+ // half and no parallel track. Resolve its action in isolation so the feature-epic logic below never
324
+ // applies to it.
325
+ if (state.kind === 'discovery') {
326
+ if (state.currentStep === 'discovery-done') {
327
+ return { epicId, kind: 'discovery-done', step: 'discovery-done', status: 'done',
328
+ why: 'discovery approved — seed feature epics with yad-epic (each reads roadmap.md)' };
329
+ }
330
+ const dstep = state.steps.find((s) => s.id === state.currentStep)
331
+ || state.steps.find((s) => s.status !== 'done');
332
+ if (!dstep) return { epicId, kind: 'discovery-done', step: 'discovery-done', why: 'discovery is done' };
333
+ if (dstep.type === 'author') {
334
+ return { epicId, kind: 'author', step: dstep.id, status: dstep.status,
335
+ skill: STEP_SKILL[dstep.id] || null, artifact: dstep.artifact,
336
+ why: `${dstep.id} is ${dstep.status} — author ${dstep.artifact}` };
337
+ }
338
+ const dpr = (ledger.hubPrs || []).find((p) => artifactBase(p.artifact) === artifactBase(dstep.artifact));
339
+ const dverb = dpr ? 'sync' : 'open';
340
+ return { epicId, kind: dpr ? 'review-sync' : 'review-open', step: dstep.id, status: dstep.status,
341
+ artifact: dstep.artifact, pr: dpr ? dpr.number : null,
342
+ command: `yad gate ${dverb} ${epicId} ${dstep.artifact}`,
343
+ why: dpr ? `review PR #${dpr.number} is open — sync its state to advance` : `${dstep.id} is open — create the review PR/MR` };
344
+ }
345
+
284
346
  // The parallel test-cases track stays workable even once the epic is ready-for-build.
285
347
  const tc = state.steps.find((s) => s.id === 'test-cases');
286
348
  const tcOpen = !!tc && tc.status !== 'done' && tc.status !== 'blocked';
package/cli/gate.mjs CHANGED
@@ -11,7 +11,7 @@ import { PROJECT_FILES } from './manifest.mjs';
11
11
  import {
12
12
  epicRoot, loadLedger, findReviewStep, artifactBase, artifactHash, gatePredicate,
13
13
  advanceState, markInReview, isEscalated, parseReviewBranch, artifactFromBase,
14
- upsertHubPr,
14
+ upsertHubPr, DISCOVERY_FILES,
15
15
  } from './epic-state.mjs';
16
16
  import { readPr, mapApprovers, createPr, reviewersForScopes, resolveCommitterLogin } from './platform.mjs';
17
17
  import { syncStatuses } from './artifact-status.mjs';
@@ -48,7 +48,12 @@ export function touchedDomains(epicDir, step) {
48
48
  return frontmatter(path.join(epicDir, 'epic.md')).repos || [];
49
49
  }
50
50
 
51
- const ownerOf = (epicDir) => frontmatter(path.join(epicDir, 'epic.md')).owner || '<owner>';
51
+ // The artifact owner shown in the review PR/MR body. Feature epics carry it in epic.md; the discovery
52
+ // front-zero (EP-discovery) has no epic.md, so fall back to roadmap.md's frontmatter owner.
53
+ const ownerOf = (epicDir) =>
54
+ frontmatter(path.join(epicDir, 'epic.md')).owner
55
+ || frontmatter(path.join(epicDir, 'roadmap.md')).owner
56
+ || '<owner>';
52
57
 
53
58
  // A null architecture hash with a BEGIN marker present means the surface block is malformed
54
59
  // (no END, or empty) — approvals would not be hash-bound, so make that visible.
@@ -61,6 +66,16 @@ function warnUnlockedContract(epicDir, artifact) {
61
66
  }
62
67
  }
63
68
 
69
+ // A null discovery hash means the discovery set is incomplete (a required artifact is missing), so the
70
+ // review is not yet reviewable and an approval would not be hash-bound. Name the missing files so the
71
+ // owner can complete the set before the gate is opened/advanced (mirrors warnUnlockedContract).
72
+ function warnIncompleteDiscovery(epicDir, artifact) {
73
+ if (artifactBase(artifact) !== 'discovery') return;
74
+ if (artifactHash(epicDir, artifact) !== null) return;
75
+ const missing = DISCOVERY_FILES.filter((f) => !fs.existsSync(path.join(epicDir, f)));
76
+ warn(`discovery set incomplete — missing ${missing.join(', ')}; review is not yet reviewable (approvals will not be hash-bound until the full set exists)`);
77
+ }
78
+
64
79
  // Fail fast on a corrupt or wrong-shape hub config: a silently-defaulted hub.json would degrade
65
80
  // every gate to file-only without anyone noticing, and a typo'd platform would read as "no bridge".
66
81
  function loadHub(root) {
@@ -202,6 +217,7 @@ export async function gateSync(root, { epic, artifact, today, reader = readPr, l
202
217
 
203
218
  const curHash = artifactHash(epicDir, pr.artifact);
204
219
  warnUnlockedContract(epicDir, pr.artifact);
220
+ warnIncompleteDiscovery(epicDir, pr.artifact);
205
221
  const recs = mapApprovers(pull.reviews, { roster, repos, touchedDomains: domains, headOid: pull.headOid });
206
222
  approvals = upsertBridge(approvals, recs, { stepId: step.id, artifact: pr.artifact, curHash, today });
207
223
 
@@ -449,7 +465,11 @@ export async function gateStatus(root, { epic } = {}) {
449
465
  }
450
466
  }
451
467
 
452
- export async function gateOpen(root, { epic, artifact } = {}) {
468
+ // `head` overrides the review branch the PR is opened against — `open-pr` delegates here after pushing
469
+ // the user's checked-out branch, which for a per-story review (review/EP-*/stories-S01) does NOT equal
470
+ // the branch this would otherwise recompute (artifactFromBase collapses stories-S01 → stories/). Pass
471
+ // the real pushed head so the PR targets a branch that exists. `creator` is injected in tests.
472
+ export async function gateOpen(root, { epic, artifact, head, creator = createPr } = {}) {
453
473
  const { hub } = loadHub(root);
454
474
  const epicDir = epicRoot(root, epic);
455
475
  const ledger = loadLedger(epicDir);
@@ -458,9 +478,10 @@ export async function gateOpen(root, { epic, artifact } = {}) {
458
478
  const step = findReviewStep(ledger.state, artifact);
459
479
  if (!step) { fail(`no review step for ${artifact}`); process.exitCode = 1; return; }
460
480
  const b = base(artifact);
461
- const branch = `review/${epic}/${b}`;
481
+ const branch = head || `review/${epic}/${b}`;
462
482
  const domains = touchedDomains(epicDir, step);
463
483
  warnUnlockedContract(epicDir, artifact);
484
+ warnIncompleteDiscovery(epicDir, artifact);
464
485
 
465
486
  const bridge = isBridge(hub);
466
487
  // Outside bridge mode (file-only, OR a platform with no gate-sync CI) there is no CI to write the
@@ -487,7 +508,7 @@ export async function gateOpen(root, { epic, artifact } = {}) {
487
508
  const assignees = committer ? [committer] : [];
488
509
  const labels = isEscalated(step) ? domains.map((d) => `domain:${d}`) : [];
489
510
  info(`opening review ${hub.platform === 'gitlab' ? 'MR' : 'PR'} on branch ${branch} …`);
490
- const r = createPr(hub.platform, { title: `review: ${artifact} (${epic})`, body, base: hub.default_branch || 'main', head: branch, reviewers, assignees, labels, cwd: root });
511
+ const r = creator(hub.platform, { title: `review: ${artifact} (${epic})`, body, base: hub.default_branch || 'main', head: branch, reviewers, assignees, labels, cwd: root });
491
512
  if (!r.ok) { warn(`could not open PR (${r.reason || 'unknown'})${bridge ? ' — open it manually; CI records the gate on merge' : '; step is in_review file-only'}`); return; }
492
513
 
493
514
  if (!bridge) {
@@ -498,6 +519,7 @@ export async function gateOpen(root, { epic, artifact } = {}) {
498
519
  hand(bridge
499
520
  ? 'reviewers approve/comment there; CI advances the gate on the default branch when it is merged'
500
521
  : `reviewers approve/comment there; then run \`yad gate sync ${epic} ${artifact}\``);
522
+ return { url: r.url };
501
523
  }
502
524
 
503
525
  // ---- helpers ------------------------------------------------------------------------------------
package/cli/manifest.mjs CHANGED
@@ -10,8 +10,9 @@ import { readFileSync } from 'node:fs';
10
10
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
11
  export const VERSION = version;
12
12
 
13
- // The 30 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
13
+ // The hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
14
14
  export const SKILLS = [
15
+ 'yad-discovery',
15
16
  'yad-analysis',
16
17
  'yad-epic',
17
18
  'yad-architecture',
package/cli/next.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // `yad next` — the unified next-step driver. Read-only: it never writes state or acts. It reads the
2
2
  // file ledger and prints the ONE concrete, copy-pasteable next action (and a one-line why), so a user
3
- // never has to remember which of the 30 skills / gate commands comes next. "Guide, don't act" — the
3
+ // never has to remember which of the 31 skills / gate commands comes next. "Guide, don't act" — the
4
4
  // front half still never auto-advances.
5
5
  //
6
6
  // yad next general orientation across the whole project
@@ -11,7 +11,7 @@ import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { c, log, ok, info, warn, hand, fail, readJSON, exists } from './lib.mjs';
13
13
  import { PROJECT_FILES } from './manifest.mjs';
14
- import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId } from './epic-state.mjs';
14
+ import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId, DISCOVERY_EPIC } from './epic-state.mjs';
15
15
 
16
16
  // Is solo mode on? Persisted in hub.json by setup (Phase C/D); default false. Read defensively so a
17
17
  // missing/old hub.json never breaks the driver.
@@ -47,6 +47,8 @@ function actionLine(a, { solo } = {}) {
47
47
  return `${c.bold(a.command)}${solo ? c.dim(' (solo: no approval needed — just merge your own PR)') : ''}`;
48
48
  case 'build':
49
49
  return `${c.bold('yad-run')} ${c.dim('(or per story: yad-spec → yad-implement → yad ship → yad-engineer-review)')}`;
50
+ case 'discovery-done':
51
+ return `invoke the ${c.bold('yad-epic')} skill ${c.dim('(seed a feature epic from roadmap.md)')}`;
50
52
  default:
51
53
  return c.dim('nothing to do');
52
54
  }
@@ -67,23 +69,36 @@ function generalNext(root, { all } = {}) {
67
69
  hand(`run ${c.bold('yad setup')} ${c.dim('(then come back to `yad next`)')}`);
68
70
  return;
69
71
  }
70
- const epics = listEpics(root);
71
- if (!epics.length) {
72
- const brownfield = profileOf(root)?.codebase === 'brownfield';
73
- log(`\n ${c.bold('Set up no epics yet.')}`);
72
+ const solo = isSolo(root);
73
+ const brownfield = profileOf(root)?.codebase === 'brownfield';
74
+ // The project front-zero (EP-discovery / "epic zero") is not a feature epic — split it out so it is
75
+ // surfaced on its own line and never mixed into the feature-epic roll-up.
76
+ const allEpics = listEpics(root);
77
+ const hasDiscovery = allEpics.includes(DISCOVERY_EPIC);
78
+ const featureEpics = allEpics.filter((id) => id !== DISCOVERY_EPIC);
79
+ const discoveryAction = hasDiscovery
80
+ ? nextAction(loadLedger(epicRoot(root, DISCOVERY_EPIC)), { epic: DISCOVERY_EPIC })
81
+ : null;
82
+ const discoveryOpen = !!discoveryAction && discoveryAction.kind !== 'discovery-done';
83
+
84
+ if (!featureEpics.length) {
85
+ if (discoveryOpen) { printAction(discoveryAction, { solo }); return; }
86
+ log(`\n ${c.bold('Set up — no feature epics yet.')}`);
74
87
  if (brownfield) hand(`capture what already exists first: invoke the ${c.bold('yad-backfill')} skill`);
75
- hand(`start your first epic: invoke the ${c.bold('yad-epic')} skill`);
88
+ if (!hasDiscovery) hand(`frame the whole project (market, feasibility, roadmap): invoke the ${c.bold('yad-discovery')} skill ${c.dim('(optional front-zero)')}`);
89
+ hand(`start your first epic: invoke the ${c.bold('yad-epic')} skill${hasDiscovery ? c.dim(' (it reads the approved roadmap.md)') : ''}`);
76
90
  return;
77
91
  }
78
- const solo = isSolo(root);
79
- const actions = epics.map((id) => nextAction(loadLedger(epicRoot(root, id)), { epic: id }));
80
92
 
81
- if (epics.length === 1 || all) {
93
+ const actions = featureEpics.map((id) => nextAction(loadLedger(epicRoot(root, id)), { epic: id }));
94
+ if (discoveryOpen) printAction(discoveryAction, { solo }); // an unfinished discovery comes first
95
+
96
+ if (featureEpics.length === 1 || all) {
82
97
  for (const a of actions) printAction(a, { solo });
83
98
  return;
84
99
  }
85
100
  // Several epics — list each with a one-liner, then point at the per-epic / --all views.
86
- log(`\n ${c.bold(`${epics.length} epics`)} ${c.dim('— next action each:')}`);
101
+ log(`\n ${c.bold(`${featureEpics.length} epics`)} ${c.dim('— next action each:')}`);
87
102
  for (const a of actions) log(` ${c.cyan(a.epicId)} ${actionLine(a, { solo })}`);
88
103
  info(c.dim(`detail: ${c.bold('yad next <epic>')} • all at once: ${c.bold('yad next --all')}`));
89
104
  }
package/cli/openpr.mjs CHANGED
@@ -8,6 +8,8 @@ import { c, log, ok, info, hand, fail, run, exists, readJSON } from './lib.mjs';
8
8
  import { PROJECT_FILES } from './manifest.mjs';
9
9
  import { detectPlatform, createPr, reviewersForScopes, resolveCommitterLogin } from './platform.mjs';
10
10
  import { taskFromBranch } from './commit.mjs';
11
+ import { parseReviewBranch, artifactFromBase } from './epic-state.mjs';
12
+ import { gateOpen } from './gate.mjs';
11
13
 
12
14
  // Resolve the target code repo: --repo <name> from the registry, else --dir, else cwd.
13
15
  function resolveRepo(root, { repo, dir }) {
@@ -19,11 +21,59 @@ function resolveRepo(root, { repo, dir }) {
19
21
  return { repoRoot: path.resolve(root, dir || '.'), meta: null };
20
22
  }
21
23
 
22
- function templateBody(repoRoot, platform, { task, risk, contract, domains }) {
23
- const tplPath = platform === 'gitlab'
24
- ? path.join(repoRoot, '.gitlab/merge_request_templates/Default.md')
25
- : path.join(repoRoot, '.github/pull_request_template.md');
26
- const base = exists(tplPath) ? fs.readFileSync(tplPath, 'utf8') : '## Summary\n\n## Impact & Risk\n';
24
+ // Which SDLC stage is this PR? The hub serves two vehicles; a code repo only one. Mirrors the
25
+ // `--head` split the hub pattern gates (pr-title.sh/pr-template.sh) already apply:
26
+ // code-repo NOT the product hub (a registry repo via --repo, or root is not a hub).
27
+ // hub-front the hub itself AND head is a review/EP-* branch (artifact-review PR).
28
+ // hub-tooling the hub itself AND head is anything else (a tooling/CI change to the hub).
29
+ // `meta` (truthy when resolved from the repos registry via --repo) is a connected code repo, so it is
30
+ // never the hub regardless of its path. Otherwise "is the hub" = repoRoot resolves to root AND root
31
+ // carries .sdlc/hub.json. path.resolve normalises `--dir .` / trailing slashes.
32
+ export function detectStage(root, repoRoot, head, meta) {
33
+ if (meta) return 'code-repo';
34
+ const isHub = path.resolve(repoRoot) === path.resolve(root)
35
+ && exists(path.join(root, PROJECT_FILES.hubConfig));
36
+ if (!isHub) return 'code-repo';
37
+ return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-front' : 'hub-tooling';
38
+ }
39
+
40
+ // The bundled code-task template — the same file `REPO_WIRING` installs into code repos, resolved
41
+ // from the package (mirrors how manifest.mjs reads ../package.json). Used for a hub-tooling PR, whose
42
+ // `.github/pull_request_template.md` is the ARTIFACT-REVIEW template (wrong shape for the code-task
43
+ // hub gate). Falls back to a minimal body that still carries every section the gate requires.
44
+ function codeTaskTemplate(platform) {
45
+ const rel = platform === 'gitlab'
46
+ ? '../skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md'
47
+ : '../skills/yad-pr-template/templates/github/pull_request_template.md';
48
+ try {
49
+ return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
50
+ } catch {
51
+ return [
52
+ '## Summary', '',
53
+ '## Impact & Risk',
54
+ '- **Domains / repos touched:** <repo>',
55
+ '- **Contract surface touched:** no',
56
+ '- **Risk level:** low',
57
+ '',
58
+ '## Checklist',
59
+ '- [ ] Lint, build, and tests pass (build/test/lint gate)',
60
+ '',
61
+ ].join('\n');
62
+ }
63
+ }
64
+
65
+ export function templateBody(repoRoot, platform, { task, risk, contract, domains, stage }) {
66
+ // hub-tooling: the hub's own template is artifact-review — use the bundled code-task template so the
67
+ // body matches the shape the hub `pr-template` gate demands for a non-review head.
68
+ let base;
69
+ if (stage === 'hub-tooling') {
70
+ base = codeTaskTemplate(platform);
71
+ } else {
72
+ const tplPath = platform === 'gitlab'
73
+ ? path.join(repoRoot, '.gitlab/merge_request_templates/Default.md')
74
+ : path.join(repoRoot, '.github/pull_request_template.md');
75
+ base = exists(tplPath) ? fs.readFileSync(tplPath, 'utf8') : codeTaskTemplate(platform);
76
+ }
27
77
  // Fill the obvious fields; leave the rest of the committed template intact for the author.
28
78
  return base
29
79
  .replace(/EP-<slug>-S0N-T0N/g, task || 'EP-<slug>-S0N-T0N')
@@ -45,6 +95,28 @@ export async function runOpenPr(root, opts = {}) {
45
95
  const baseBranch = opts.base || meta?.default_branch || 'main';
46
96
  if (branch === baseBranch) { fail(`on ${baseBranch} — switch to your task branch first`); process.exitCode = 1; return; }
47
97
 
98
+ const stage = detectStage(root, repoRoot, branch, meta);
99
+
100
+ // hub-front: this is a front-half artifact-review PR (review/EP-*/<artifact> head on the hub). The
101
+ // artifact-review title, body, and ledger bookkeeping all live in `yad gate open` — delegate to it
102
+ // rather than emit the code-task shape (which the hub gate would reject). Push first (gateOpen does
103
+ // not push), then hand off; any --title/--message is dropped (gateOpen sets `review: …`).
104
+ if (stage === 'hub-front') {
105
+ const parsed = parseReviewBranch(branch);
106
+ if (!parsed) { fail(`could not parse review branch '${branch}' (expected review/EP-<slug>/<artifact>)`); process.exitCode = 1; return; }
107
+ info(`pushing ${branch} …`);
108
+ const fpush = run('git', ['push', '-u', 'origin', branch], { cwd: repoRoot });
109
+ if (!fpush.ok) { fail(`git push failed — ${fpush.stderr.split('\n')[0] || 'unknown'}`); process.exitCode = 1; return; }
110
+ // Pass the branch we just pushed as the head so gateOpen opens the PR against it (its own
111
+ // recompute would collapse a per-story base). gateOpen signals failure by returning no url —
112
+ // mirror open-pr's own error contract so `ship` sees the non-zero exit and never reports success.
113
+ // (On a platform-less hub gateOpen marks the step in_review file-only and returns no url; open-pr's
114
+ // job is to open a PR, so "no PR opened" is a non-zero outcome here, unlike `yad gate open`.)
115
+ const res = await gateOpen(root, { epic: parsed.epic, artifact: artifactFromBase(parsed.base), head: branch });
116
+ if (!res?.url) process.exitCode = 1;
117
+ return res;
118
+ }
119
+
48
120
  // Push the branch (sets upstream) using the user's own auth. Abort on failure — creating a PR for a
49
121
  // branch that is not on the remote just fails with a more confusing error.
50
122
  info(`pushing ${branch} …`);
@@ -54,7 +126,7 @@ export async function runOpenPr(root, opts = {}) {
54
126
  const task = opts.task || taskFromBranch(branch);
55
127
  const title = opts.title || run('git', ['log', '-1', '--format=%s'], { cwd: repoRoot }).stdout || `task ${task || branch}`;
56
128
  const body = templateBody(repoRoot, platform, {
57
- task, risk: opts.risk || 'low', contract: !!opts.contractChange, domains: meta?.name,
129
+ task, risk: opts.risk || 'low', contract: !!opts.contractChange, domains: meta?.name, stage,
58
130
  });
59
131
 
60
132
  // Auto-assign from the hub roster, scoped to this repo: assignee = the committer (resolved from
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.16.0",
4
- "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
3
+ "version": "2.17.0",
4
+ "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 31 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
7
7
  "license": "MIT",
@@ -10,6 +10,19 @@ methodology: gated-team-multirepo-sdlc
10
10
  product_root: "{project-root}"
11
11
  epics_folder: "{project-root}/epics"
12
12
 
13
+ # Project discovery (yad-discovery) — the OPTIONAL front-zero, run once per project (greenfield AND
14
+ # brownfield). It is modelled as the reserved "epic zero" `EP-discovery` so the entire review gate +
15
+ # PR/MR bridge + CI sync + `yad next` operate on it unchanged. A 2-step author→review chain whose
16
+ # review binds to the whole artifact set; on approval it terminates at `discovery-done` (no build half).
17
+ # Output: a phased roadmap (incl. MVP) that each feature epic reads (yad-epic Step 2c) — reference-only,
18
+ # never auto-seeds epics.
19
+ discovery:
20
+ epic_id: "EP-discovery" # reserved id; yad-epic/yad-analysis never pick it
21
+ location: "{project-root}/epics/EP-discovery/" # discovery artifacts + ledger live here
22
+ optional: true # skip-able; a team that knows what to build starts at yad-epic
23
+ modes: [greenfield, brownfield] # current-state is code-aware in brownfield; competitor study in both
24
+ artifacts: [market-research.md, competitor-analysis.md, current-state.md, feasibility.md, requirements.md, roadmap.md]
25
+
13
26
  # Core configuration values (inherited convention from _bmad/config.toml).
14
27
  project_name: yadflow
15
28
  communication_language: English
@@ -20,11 +33,12 @@ output_folder: "{project-root}/_bmad-output"
20
33
  defaults:
21
34
  assistance: review # none | review | heavy
22
35
  automation: human_approve # human_approve | machine_advance
23
- # Front steps (analysis [optional], epic, architecture, ui-design, stories, test-cases) are locked to
24
- # human_approve and may NOT be set to machine_advance in this version (build plan §1, §8.7).
36
+ # Front steps (discovery [optional front-zero], analysis [optional], epic, architecture, ui-design,
37
+ # stories, test-cases) are locked to human_approve and may NOT be set to machine_advance in this
38
+ # version (build plan §1, §8.7).
25
39
  front_steps_locked: true
26
40
  # Each front authoring step opens its own branch at the start of the step (the <step> is the step id:
27
- # analysis | epic | architecture | ui-design | stories | test-cases). Git/greenfield-safe; distinct
41
+ # discovery | analysis | epic | architecture | ui-design | stories | test-cases). Git/greenfield-safe; distinct
28
42
  # from the bridge's review branch (hub.artifact_branch). See yad-epic/references/state-schema.md.
29
43
  front_authoring_branch: "<step>/EP-<slug>"
30
44
 
@@ -238,7 +252,7 @@ automation:
238
252
  # Hard lock — the dial-setter REFUSES machine_advance for these, regardless of trust evidence.
239
253
  # The front authoring steps (already locked:true in state.json; analysis is optional) + the human
240
254
  # merge gate.
241
- locked_steps: [analysis, epic, architecture, ui-design, stories, test-cases, engineer-review]
255
+ locked_steps: [discovery, analysis, epic, architecture, ui-design, stories, test-cases, engineer-review]
242
256
  # Kill switch (phase-4-build-plan.md §Safety): true => every step forced to human_approve
243
257
  # system-wide, no per-step edits. One line, instantly reversible. Toggle via `yad-run action: kill`.
244
258
  kill_switch: false
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
12
  cd "$ROOT"
13
13
 
14
- SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
14
+ SKILLS=(yad-discovery yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-sync-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
15
15
 
16
16
  echo "Installing sdlc module from $ROOT/skills ..."
17
17
 
@@ -1,4 +1,5 @@
1
1
  module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
2
+ SDLC Workflow,yad-discovery,Project Discovery,DI,"Optional front-zero (once per project, greenfield AND brownfield): with the analyst + pm run market research, a competitor study, a feasibility study, and (brownfield) a current-state study, then distil functional + non-functional requirements and a phased roadmap (MVP and beyond) under the reserved EP-discovery. roadmap.md becomes the menu of features each yad-epic reads. Never auto-advances.",,{idea: one-line product idea} {mode: greenfield|brownfield},0-front,,yad-review-gate,false,epics/EP-discovery/,market-research.md competitor-analysis.md current-state.md feasibility.md requirements.md roadmap.md state.json
2
3
  SDLC Workflow,yad-analysis,Author Analysis,AN,"Optional front state: with the analyst pressure-test a feature idea and write the discovery brief into analysis.md. Assigns the EP-<slug> ID and seeds .sdlc state (the chain that puts analysis before epic). If skipped, the epic step does this shaping inline. Never auto-advances.",,{idea: one-line feature idea},1-front,,yad-review-gate,false,epics/EP-<slug>/,analysis.md state.json
3
4
  SDLC Workflow,yad-epic,Author Epic,AE,"Front state 1: shape an idea with analyst then pm into epic.md; assign EP-<slug> ID and seed .sdlc state. Never auto-advances.",,{idea: one-line feature idea},1-front,,yad-review-gate,true,epics/EP-<slug>/,epic.md state.json
4
5
  SDLC Workflow,yad-review-gate,Team Review Gate,RG,"Reusable review+approve gate for all five reviews. Shares an artifact for review, records comments and approvals as files, enforces owner + 1 reviewer (escalates on contract/auth/payments; per-repo routing for stories), advances state only when approved.",,{artifact: file under the epic} {action: open|comment|approve|advance},1-front,,,true,epics/EP-<slug>/reviews/,reviews/*.md approvals.json state.json
@@ -50,9 +50,20 @@ connected repo** (the epic's `repos` are not chosen yet), load the lightweight c
50
50
  stamp `code-context: stale` in the frontmatter.
51
51
  - **Traceability:** record which maps you loaded in the analysis frontmatter `code-context:` field.
52
52
 
53
+ ### Step 2c — Read the project roadmap (project context, only once discovery is APPROVED)
54
+ Consume the project front-zero (`yad-discovery`) **only after its review gate has passed** — never a
55
+ draft or in-review roadmap (that would bypass `discovery-review`). Gate on the **state**, not file
56
+ existence: read `{project-root}/epics/EP-discovery/.sdlc/state.json` and proceed **only when
57
+ `currentStep == "discovery-done"`**. When it is, read its `roadmap.md` and sibling `requirements.md`
58
+ for the project framing: which phase (MVP / later) this feature belongs to and the requirements it
59
+ carries — so the analysis's **Problem / Options / Recommendation** stay consistent with the approved
60
+ roadmap. **Optional & non-blocking:** if there is no discovery, or it has not yet reached
61
+ `discovery-done`, proceed unchanged — do not consume an unapproved roadmap.
62
+
53
63
  ### Step 3 — Generate the Epic ID (engine-assigned, never by hand)
54
64
  Derive `EP-<slug>` where `slug` is **2–4 lowercase words joined by hyphens**, drawn from the idea
55
- (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. **The ID is assigned once and
65
+ (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. `EP-discovery` is **reserved**
66
+ for the project front-zero — never use it for a feature. **The ID is assigned once and
56
67
  never renamed** — renaming breaks every downstream link (build plan §6b). Check
57
68
  `{project-root}/epics/` for collisions; if the slug exists, append a distinguishing word.
58
69
 
@@ -9,11 +9,17 @@
9
9
  name: yad-hub-checks
10
10
  on:
11
11
  pull_request:
12
+ # `edited` (beyond the opened/synchronize/reopened defaults) so a PR title/body correction
13
+ # re-runs the pattern gates without a close/reopen — e.g. fixing a body the pr-template gate held.
14
+ types: [opened, synchronize, reopened, edited]
12
15
  branches: ["**"]
13
16
 
14
17
  jobs:
18
+ # commit-message + ledger-guard read the commit range, not the PR title/body — skip them on a bare
19
+ # `edited` event (only pr-title/pr-template need to re-check a title/body fix).
15
20
  commit-message:
16
21
  runs-on: ubuntu-latest
22
+ if: github.event.action != 'edited'
17
23
  steps:
18
24
  - uses: actions/checkout@v4
19
25
  with: { fetch-depth: 0 }
@@ -52,6 +58,7 @@ jobs:
52
58
  # The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
53
59
  ledger-guard:
54
60
  runs-on: ubuntu-latest
61
+ if: github.event.action != 'edited'
55
62
  steps:
56
63
  - uses: actions/checkout@v4
57
64
  with: { fetch-depth: 0 }
@@ -21,7 +21,8 @@ and `yad-ship` use. It **never auto-advances**; it just commits.
21
21
  `feat|fix|docs|refactor|test|perf|build|ci|chore|revert`; proper nouns/acronyms keep their case.
22
22
  - **Task trailer** — required on a code repo (anchors the `spec-link` + `commit-message` gates). Given
23
23
  with `--task`, else derived from the branch (`feat/<story>-<task>-…`). Hub commits are not
24
- task-scoped, so the trailer is optional there.
24
+ task-scoped, so the trailer is optional there — a missing-Task warning is informational on the hub
25
+ (`spec-link` is a code-repo gate) and only flags a real gate failure in a code repo.
25
26
  - **Contract-Change trailer** — `--contract-change` only when the diff touches the locked contract
26
27
  surface; it routes the change back to the architecture gate.
27
28
  - **AI co-author footer — OFF by default.** No `Co-Authored-By` trailer is written unless `--ai <id>`
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: yad-discovery
3
+ description: 'Optional front-zero of the gated SDLC — the once-per-project discovery phase. With the field-expert lenses (analyst + pm), run market research, a competitor study, a feasibility study, and (brownfield) a current-state study, then distil a functional + non-functional requirements list and a phased roadmap (MVP and beyond) into the reserved EP-discovery. Greenfield AND brownfield. Its roadmap.md becomes the menu of features each yad-epic reads. Seeds the EP-discovery state and hands off to the team review gate; never auto-advances. Use when the user says "start the project", "do discovery", "market research / feasibility / roadmap", or "what should we build first".'
4
+ ---
5
+
6
+ # SDLC — Project Discovery (optional front-zero, "epic zero")
7
+
8
+ **Goal:** Produce a human-authored, AI-assisted **project-level discovery set** — the field expert's
9
+ requirement-gathering for the whole product — under the reserved `EP-discovery` ("epic zero"), then
10
+ hand off to `yad-review-gate`. The output `roadmap.md` is the menu of features; each feature is later
11
+ taken into the normal `yad-epic` flow, which reads the roadmap for project context.
12
+
13
+ This is a **front state**: human-authored with AI assist and **never auto-advances**. It runs **once
14
+ per project** and is **optional** — a team that already knows what to build can skip it and start at
15
+ `yad-epic`. It supports **both greenfield and brownfield**, and produces a **competitor study in both**.
16
+
17
+ This skill enforces the build plan's core rules: all state lives in files; IDs are engine-assigned
18
+ (the reserved `EP-discovery`, never a typed feature slug); front steps are locked to `human_approve`.
19
+
20
+ ## Conventions
21
+
22
+ - `{project-root}` resolves from the project working directory.
23
+ - Discovery artifacts live under `{project-root}/epics/EP-discovery/` (the reserved "epic zero").
24
+ - Speak in the configured `communication_language`; write documents in `document_output_language`.
25
+
26
+ ## On Activation
27
+
28
+ ### Step 1 — Entry guard (runs once per project)
29
+ The id is the reserved `EP-discovery` — never a feature slug. Discovery seeds its state exactly once:
30
+ if `{project-root}/epics/EP-discovery/.sdlc/state.json` already exists, **STOP** and point the user at
31
+ `yad next EP-discovery` (the phase is in review or done; edit the artifacts in place, don't re-seed).
32
+ When no `state.json` exists yet, proceed and seed state in Step 5.
33
+
34
+ Detect the project mode from `{project-root}/.sdlc/hub.json` `profile.codebase`
35
+ (`greenfield` | `brownfield`, set by `yad setup`). If absent, ask the user; default `greenfield`.
36
+
37
+ ### Step 2 — Shape with the field-expert lenses (assist: analyst + pm)
38
+ Adopt the **analyst** lens (`bmad-agent-analyst`, Mary) and the **pm** lens (`bmad-agent-pm`) to gather
39
+ requirements as a domain expert would. Drive the existing BMAD research skills as the assist — they
40
+ already exist in this project:
41
+ - `bmad-market-research` — market size, segments, demand, trends, positioning.
42
+ - `bmad-domain-research` — the problem domain, regulations, and constraints of the field.
43
+ - `bmad-product-brief` — personas, value proposition, success metrics.
44
+
45
+ Pressure-test: who are the users, what problem, what is the market, **who are the competitors and how
46
+ do we differ** (required in BOTH modes), what is feasible, what is the smallest valuable slice (MVP),
47
+ and what sequences after it.
48
+
49
+ ### Step 2b — Brownfield current-state (make discovery code-aware)
50
+ Read the registry `{project-root}/.sdlc/repos.json` (`config.yaml` `code_context`). For **every
51
+ connected repo**, load the lightweight code-map `{project-root}/.sdlc/code-context/<repo>/code-map.md`
52
+ and base `current-state.md` on **what already exists** — modules, endpoints, data, gaps — so the
53
+ roadmap extends the real system rather than re-proposing it.
54
+
55
+ - **Greenfield-safe:** if `repos.json` is absent/empty (greenfield), `current-state.md` is a short
56
+ "clean slate / assumptions & non-goals" note, and you proceed.
57
+ - **Staleness:** if a repo's current HEAD (`git -C <path> rev-parse HEAD`) ≠ its registry `syncedHead`,
58
+ warn and suggest `yad repo refresh <repo>` (a human decision — flag, never auto-refresh).
59
+ - **Backfill pointer:** for an existing codebase, point the user at `yad-backfill` to capture specs for
60
+ already-built features; discovery frames the *forward* roadmap, backfill captures the *current* one.
61
+
62
+ ### Step 3 — Open the authoring branch
63
+ Open the discovery authoring branch `discovery/EP-discovery` per the shared procedure
64
+ (`../yad-epic/references/state-schema.md` → "Authoring branches"): git-safe (skip with a note if
65
+ `{project-root}` is not a git work tree), check out the branch if it exists, else create it from the
66
+ hub's default branch. Author and commit the discovery set on it. Distinct from the bridge's
67
+ `review/EP-discovery/discovery` branch.
68
+
69
+ ### Step 4 — Write the discovery set
70
+ Write these files under `{project-root}/epics/EP-discovery/`. Each is a normal Markdown artifact; the
71
+ gate binds to the **whole set** (editing any one revokes approvals). `roadmap.md` summarises and links
72
+ the others and is the spine of the review.
73
+
74
+ - `market-research.md` — market, segments, demand, trends (assist: `bmad-market-research`).
75
+ - `competitor-analysis.md` — competitors, capabilities, gaps, our differentiation (**both modes**).
76
+ - `current-state.md` — brownfield: what exists today (Step 2b); greenfield: clean-slate assumptions.
77
+ - `feasibility.md` — technical/operational/economic feasibility, risks, viability, go/no-go.
78
+ - `requirements.md` — the consolidated requirements list, **functional AND non-functional**, as a
79
+ table (see `references/discovery-schema.md`). Functional rows are candidate features (registration,
80
+ login, …); non-functional rows are cross-cutting (performance, security, accessibility, i18n …).
81
+ - `roadmap.md` — the phased plan with an explicit **MVP** phase, then later phases. Each feature row
82
+ carries a proposed `EP-<slug>` id, its target phase, and a `status:` of `planned`
83
+ (see `references/discovery-schema.md` for the exact templates).
84
+
85
+ Leave `owner` for the user to set in each frontmatter. Fill the bodies with the user.
86
+
87
+ ### Step 5 — Seed the state machine
88
+ Create `{project-root}/epics/EP-discovery/.sdlc/state.json` describing the **2-step** front-zero
89
+ sequence, both steps `automation: human_approve` and `locked`, with the `kind: "discovery"` marker the
90
+ engine keys off. Use this exact shape (see `references/discovery-schema.md`):
91
+
92
+ ```json
93
+ {
94
+ "epicId": "EP-discovery",
95
+ "kind": "discovery",
96
+ "createdAt": "<YYYY-MM-DD>",
97
+ "currentStep": "discovery-review",
98
+ "steps": [
99
+ { "id": "discovery", "type": "author", "artifact": "discovery/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "risk_tags": [] },
100
+ { "id": "discovery-review", "type": "review+approve", "artifact": "discovery/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "in_review", "risk_tags": [] }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ Notes:
106
+ - The review step's artifact is the virtual base `discovery/` — the gate fingerprints the whole
107
+ discovery file set (`market-research`, `competitor-analysis`, `current-state`, `feasibility`,
108
+ `requirements`, `roadmap`), so editing any of them revokes prior approvals (mirrors `stories/`).
109
+ **All six must exist to review:** if any is missing the set is incomplete and non-reviewable (the
110
+ hash is `null`) and `yad gate open`/`sync` warn — so write all six (Step 4) before the gate.
111
+ - `discovery-review` carries no `risk_tags` — it is the **base** rule (owner + 1 reviewer); discovery
112
+ never escalates to domain owners (no contract surface is touched yet).
113
+ - Also create an empty approvals ledger `.sdlc/approvals.json` and comments ledger
114
+ `.sdlc/comments.json`, each containing `[]`, and the `reviews/` directory.
115
+
116
+ ### Step 6 — Stop at the gate (do NOT advance)
117
+ Report: the path to the discovery set, and that the next action is **review** via `yad-review-gate`
118
+ (base rule: owner + 1 reviewer) on the virtual artifact `discovery/`. **Never mark discovery-review
119
+ approved here** — only real reviewers do that through the gate. When the discovery gate passes, the
120
+ state moves to the `discovery-done` sentinel (not `ready-for-build` — discovery has no build half); the
121
+ roadmap is now the input that each `yad-epic` reads (its "Step 2c — read the roadmap"). When the hub
122
+ has a platform, the gate opens a review PR on the hub (via `yad-hub-bridge`) and
123
+ `yad-review-gate action: sync` pulls platform approvals/comments into the ledger; otherwise the review
124
+ is recorded file-only.
125
+
126
+ ## Reference
127
+ - Discovery artifact templates + the 2-step state shape: `references/discovery-schema.md`.
128
+ - State schema, chain shapes, and the authoring-branch procedure:
129
+ `../yad-epic/references/state-schema.md`.
130
+ - The epic step that consumes `roadmap.md`: `../yad-epic/SKILL.md` (Step 2c).
131
+ - Capturing already-built features in a brownfield codebase: `../yad-backfill/SKILL.md`.
132
+ - Connecting code repos + the code-context the brain reads: `../yad-connect-repos/SKILL.md`.
@@ -0,0 +1,106 @@
1
+ # Discovery schema — artifacts + the front-zero state shape
2
+
3
+ The project discovery phase ("epic zero") lives under `{project-root}/epics/EP-discovery/`. It reuses
4
+ the per-epic ledger files (`.sdlc/state.json`, `approvals.json`, `comments.json`, `reviews/`,
5
+ `hub-prs.json`) unchanged — `EP-discovery` is a valid epic id, so the existing gate, PR/MR bridge, CI
6
+ sync, and `yad next` all operate on it. What is special is the `kind: "discovery"` marker on the state
7
+ object and the 2-step chain.
8
+
9
+ ## State (`.sdlc/state.json`)
10
+
11
+ ```json
12
+ {
13
+ "epicId": "EP-discovery",
14
+ "kind": "discovery",
15
+ "createdAt": "<YYYY-MM-DD>",
16
+ "currentStep": "discovery-review",
17
+ "steps": [
18
+ { "id": "discovery", "type": "author", "artifact": "discovery/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "done", "risk_tags": [] },
19
+ { "id": "discovery-review", "type": "review+approve", "artifact": "discovery/", "assistance": "review", "automation": "human_approve", "locked": true, "status": "in_review", "risk_tags": [] }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ - `artifact: "discovery/"` is a **virtual** base: `artifactHash` fingerprints the whole discovery file
25
+ set (`discoveryHash` in `cli/epic-state.mjs`), so an edit to any discovery file revokes prior
26
+ approvals — exactly like `stories/` fingerprints the stories directory.
27
+ - The **full set is required to review**: if any of the six files is missing, `discoveryHash` returns
28
+ `null` — the discovery is **incomplete and non-reviewable** (no hash to bind an approval to), and
29
+ `yad gate open` / `yad gate sync` warn with the missing filenames. Write all six (in greenfield,
30
+ `current-state.md` is a short clean-slate note) before handing off to the gate.
31
+ - On approval the gate sets `currentStep: "discovery-done"` (a terminal sentinel — discovery has **no**
32
+ build half, so it never becomes `ready-for-build`).
33
+ - The discovery files (relative to the epic dir) the gate commits on the review branch and re-hashes at
34
+ merge are: `market-research.md`, `competitor-analysis.md`, `current-state.md`, `feasibility.md`,
35
+ `requirements.md`, `roadmap.md` (the `DISCOVERY_FILES` list).
36
+
37
+ ## Artifact templates
38
+
39
+ ### `requirements.md`
40
+
41
+ ```markdown
42
+ ---
43
+ id: EP-discovery
44
+ artifact: requirements
45
+ status: draft
46
+ owner:
47
+ ---
48
+
49
+ ## Functional requirements
50
+ <!-- candidate features — each becomes (or seeds) a feature epic later -->
51
+
52
+ | Ref | Requirement | Description | Priority | MVP? |
53
+ |-----|-------------|-------------|----------|------|
54
+ | F-01 | Registration | A new user can create an account | must | yes |
55
+ | F-02 | Login | A returning user can authenticate | must | yes |
56
+
57
+ ## Non-functional requirements
58
+ <!-- cross-cutting qualities the whole product must hold -->
59
+
60
+ | Ref | Category | Requirement | Target / acceptance |
61
+ |-----|----------|-------------|---------------------|
62
+ | N-01 | Performance | p95 page load | < 2s on 4G |
63
+ | N-02 | Security | auth + data-at-rest | OWASP ASVS L1; encrypted at rest |
64
+ | N-03 | Accessibility | WCAG conformance | AA |
65
+ ```
66
+
67
+ ### `roadmap.md` (the spine of the review)
68
+
69
+ ```markdown
70
+ ---
71
+ id: EP-discovery
72
+ artifact: roadmap
73
+ status: draft
74
+ owner:
75
+ ---
76
+
77
+ ## Summary
78
+ <!-- the product thesis in 2–3 lines; links to market-research, competitor-analysis,
79
+ current-state, feasibility, requirements -->
80
+
81
+ ## Phase 1 — MVP
82
+ <!-- the smallest valuable slice; the features here are built first -->
83
+
84
+ | Feature | Proposed epic id | Requirements | Status |
85
+ |---------|------------------|--------------|--------|
86
+ | Registration | EP-registration | F-01 | planned |
87
+ | Login | EP-login | F-02 | planned |
88
+
89
+ ## Phase 2 — <name>
90
+ | Feature | Proposed epic id | Requirements | Status |
91
+ |---------|------------------|--------------|--------|
92
+ | … | EP-… | F-… | planned |
93
+
94
+ ## Later / parked
95
+ <!-- explicitly deferred, with why -->
96
+ ```
97
+
98
+ Per-feature `status:` lifecycle (set by hand): `planned` → `epic-started` (a feature epic has been
99
+ seeded with `yad-epic`) → `shipped`. The proposed `EP-<slug>` ids are suggestions for the eventual
100
+ `yad-epic` runs — `yad-epic` still assigns the id (and skips the reserved `EP-discovery`).
101
+
102
+ The other four artifacts (`market-research.md`, `competitor-analysis.md`, `current-state.md`,
103
+ `feasibility.md`) are free-form Markdown with a `--- id / artifact / status / owner ---` frontmatter
104
+ block matching the two above. `competitor-analysis.md` is required in BOTH greenfield and brownfield;
105
+ `current-state.md` is the substantive code-aware study in brownfield and a short clean-slate note in
106
+ greenfield.
@@ -32,6 +32,18 @@ and never gates.
32
32
  | `yad-connect-learning` | `learning.json` |
33
33
  | `yad-connect-docs` | `docs.json` |
34
34
 
35
+ ### Path: Front-zero (`phase: 0-front`)
36
+ The OPTIONAL once-per-project discovery phase, modelled as the reserved "epic zero" `EP-discovery`.
37
+ Greenfield AND brownfield; a 2-step author→review chain whose review binds to the whole artifact set
38
+ and terminates at `discovery-done` (no build half).
39
+
40
+ | Step (skill) | Gate | Outputs / sideEffects |
41
+ |--------------|------|------------------------|
42
+ | `yad-discovery` *(optional)* | → `discovery-review` (base rule) | `market-research.md`, `competitor-analysis.md`, `current-state.md`, `feasibility.md`, `requirements.md`, `roadmap.md`; seeds `EP-discovery/.sdlc/state.json` |
43
+
44
+ `roadmap.md` is the menu of features each `yad-epic` reads (Step 2c) — reference-only, never
45
+ auto-seeds epics.
46
+
35
47
  ### Path: Front half (`phase: 1-front`)
36
48
  The gated authoring chain + the reusable review gate (10 steps, or 12 with the optional analysis).
37
49
 
@@ -85,8 +97,8 @@ The eight yadflow lenses, each to its relevant phase sections + paths:
85
97
 
86
98
  | Lens | Relevant phases / sections |
87
99
  |------|----------------------------|
88
- | analyst | Setup intent, analysis step, front-half discovery |
89
- | pm | epic, stories, roadmap; the front gates |
100
+ | analyst | Setup intent, project discovery (front-zero), analysis step, front-half discovery |
101
+ | pm | project discovery (market/feasibility/roadmap), epic, stories; the front gates |
90
102
  | architect | architecture + the locked contract; escalation |
91
103
  | ux | UI design, design tool connection, the design system |
92
104
  | dev | build half: spec → implement, the per-repo loop |
@@ -68,10 +68,23 @@ and let it inform which `repos` the epic should touch.
68
68
  - For depth on a specific area not in the map, do a live on-demand read (see `yad-connect-repos`
69
69
  `references/code-context.md`) — do not block on it.
70
70
 
71
+ ### Step 2c — Read the project roadmap (project context, only once discovery is APPROVED)
72
+ Consume the project front-zero (`yad-discovery`) **only after its review gate has passed** — never a
73
+ draft or in-review roadmap (that would bypass `discovery-review`). Gate on the **state**, not file
74
+ existence: read `{project-root}/epics/EP-discovery/.sdlc/state.json` and proceed **only when
75
+ `currentStep == "discovery-done"`**. When it is, read its `roadmap.md` and sibling `requirements.md`
76
+ for the project framing: which phase (MVP / later) this feature belongs to, the functional/non-functional
77
+ requirements it carries, and how it fits the wider plan — so the epic's **Goal / Scope / Acceptance
78
+ signals** stay consistent with the approved roadmap. **Optional & non-blocking:** if there is no
79
+ discovery, or it has not yet reached `discovery-done` (absent / still draft / in-review), proceed
80
+ unchanged — do not consume an unapproved roadmap. After seeding the epic, the matching roadmap row's
81
+ `status:` can be bumped `planned → epic-started` by hand (a human edit; discovery never auto-seeds epics).
82
+
71
83
  ### Step 3 — Generate the Epic ID (engine-assigned, never by hand) — analysis-skipped only
72
84
  *(Skip when analysis ran — the ID was already assigned by `yad-analysis`.)*
73
85
  Derive `EP-<slug>` where `slug` is **2–4 lowercase words joined by hyphens**, drawn from the idea
74
- (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. **The ID is assigned once and
86
+ (e.g. `EP-istifta-inquiries`). Lowercase except the fixed `EP` prefix. `EP-discovery` is **reserved**
87
+ for the project front-zero — never use it for a feature. **The ID is assigned once and
75
88
  never renamed** — renaming breaks every downstream link (build plan §6b).
76
89
  Check `{project-root}/epics/` for collisions; if the slug exists, append a distinguishing word.
77
90
 
@@ -22,6 +22,14 @@ the product hub.
22
22
  `.gitlab/merge_request_templates/Default.md`) with `Task:`, `Risk level:`, `Contract surface
23
23
  touched:`, and `Domains` prefilled; the rest is left for the author. This satisfies the `pr-template`
24
24
  gate.
25
+ - **Stage-aware on the product hub** — `open-pr` mirrors the `--head` split the hub gates apply:
26
+ - a **`review/EP-*/<artifact>`** branch is a front-half artifact-review PR → it **delegates to
27
+ `yad gate open`** (artifact-review title `review: <artifact> (EP-<slug>)`, the hub artifact-review
28
+ body, and the gate ledger bookkeeping all in one place). Any `--title`/`-m` is ignored here.
29
+ - any **other hub branch** is a tooling/CI change → it uses the bundled **code-task** template
30
+ (`## Summary` / `Risk level:` / `## Checklist`) instead of the hub's artifact-review
31
+ `pull_request_template.md`, so the hub `pr-template` gate passes.
32
+ In a code repo nothing changes — it reads the repo's own committed code-task template.
25
33
  - **Auto-assign** — from the hub roster scoped to this repo: assignee = the committer (resolved from
26
34
  the local git identity), reviewers = the repo's `reviewer`/`domain-owner` logins minus the committer.
27
35
  Degrades cleanly when there is no roster.
@@ -22,6 +22,10 @@ its own and **never merges**. The engineer review + merge are Step E (`yad-engin
22
22
  roster auto-assign, risk routing (`../yad-open-pr/SKILL.md`).
23
23
  - **Order matters:** the PR/MR is opened **only if the commit lands**. A failed commit, a tripped
24
24
  atomic guard, or `--dry-run` stops the step before anything is pushed.
25
+ - **Stage-aware on the product hub** (via `yad-open-pr`): on a `review/EP-*` branch `ship` opens the
26
+ front-half **artifact-review** PR (delegating to `yad gate open` — `--title` is ignored); on any
27
+ other hub branch it opens the **code-task** PR from the bundled code-task template. In a code repo
28
+ it is unchanged.
25
29
 
26
30
  ## Inputs
27
31