yadflow 3.3.0 → 3.4.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
- # [3.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.2.0...v3.3.0) (2026-06-30)
1
+ # [3.4.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.3.1...v3.4.0) (2026-07-01)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * **next:** surface build-half sub-steps in yad next ([603b129](https://github.com/abdelrahmannasr/yadflow/commit/603b1294194e436c35de842bc687ccb4f51c2075))
6
+ * **report:** add self issue reporter with auto-scrubbed diagnostics ([cd70965](https://github.com/abdelrahmannasr/yadflow/commit/cd7096568995d52501a2e9b57a7ac091a22620f1))
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/bin/yad.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // `yad` — setup/maintenance + the PR-driven review gate + build helpers for the SDLC module.
3
3
  import { VERSION } from '../cli/manifest.mjs';
4
- import { c, log, closePrompts } from '../cli/lib.mjs';
4
+ import { c, log, closePrompts, askYesNo } from '../cli/lib.mjs';
5
5
  import { runSetup } from '../cli/setup.mjs';
6
6
  import { reconcile } from '../cli/reconcile.mjs';
7
7
  import { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer, gateWalkthrough } from '../cli/gate.mjs';
@@ -17,6 +17,7 @@ import { runDoctor } from '../cli/doctor.mjs';
17
17
  import { runNext } from '../cli/next.mjs';
18
18
  import { syncStatuses } from '../cli/artifact-status.mjs';
19
19
  import { runThread, runReconcile } from '../cli/thread.mjs';
20
+ import { runReport } from '../cli/report.mjs';
20
21
 
21
22
  const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
22
23
 
@@ -33,6 +34,9 @@ ${c.bold('Setup & maintenance')}
33
34
  repo paths, epic ledgers (exit 1 on any failure)
34
35
  yad sync-status [epic] Update artifact frontmatter status (draft/in-review/approved)
35
36
  from .sdlc/state.json — all epics if omitted (--dry-run to preview)
37
+ yad report [-m <text>] File a bug in the yadflow repo with auto-scrubbed diagnostics
38
+ (no paths/hosts/repo names/logins/flag values). Also offered
39
+ automatically after an unexpected failure. YAD_NO_REPORT=1 disables.
36
40
 
37
41
  ${c.bold('Reviewer roster')}
38
42
  yad roster list Show every member + their roles per scope (hub + each repo)
@@ -181,6 +185,9 @@ async function main() {
181
185
  case 'doctor':
182
186
  await runDoctor(o.dir, { json: o.json });
183
187
  break;
188
+ case 'report':
189
+ await runReport(o.dir, { message: o.message });
190
+ break;
184
191
  case 'sync-status': {
185
192
  const [, epic] = o._;
186
193
  if (epic && !isValidEpicId(epic)) { log(c.red(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
@@ -279,11 +286,24 @@ async function main() {
279
286
  }
280
287
 
281
288
  main()
282
- .catch((err) => {
289
+ .catch(async (err) => {
283
290
  const code = err?.code && /^YAD-/.test(err.code) ? ` [${err.code}]` : '';
284
291
  log(c.red(`\nyad failed${code}: ${err?.message || err}`));
285
292
  if (err?.hint) log(c.yellow(` → ${err.hint}`));
286
293
  if (code) log(c.dim(' (see README "Troubleshooting" for this code, or run `yad doctor`)'));
287
294
  process.exitCode = 1;
295
+ // Offer to report the failure — but only interactively (never in CI), and opt-out via
296
+ // YAD_NO_REPORT. `report` failing on its own would land here, so never re-offer for it.
297
+ // Require a TTY on BOTH ends: stdout for the message, stdin so the y/N prompt can be answered
298
+ // (a TTY stdout with piped/closed stdin would otherwise hang on readline).
299
+ const offerReport = process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
300
+ && !process.env.YAD_NO_REPORT && process.argv[2] !== 'report';
301
+ if (offerReport) {
302
+ try {
303
+ if (await askYesNo('\nReport this failure to the yadflow team?', false)) {
304
+ await runReport(process.cwd(), { error: err });
305
+ }
306
+ } catch { /* reporting is best-effort — never mask the original failure */ }
307
+ }
288
308
  })
289
309
  .finally(closePrompts);
package/cli/doctor.mjs CHANGED
@@ -304,12 +304,21 @@ export function threadChecks(checks, root) {
304
304
  }
305
305
  }
306
306
 
307
- export async function runDoctor(root, { json = false } = {}) {
307
+ // Run every check section and return the diagnostic object without printing. This is the shared
308
+ // core of `runDoctor` — the reporter (cli/report.mjs) consumes it to derive a *scrubbed* safe
309
+ // subset (never the raw checks, which carry names + paths). Same shape `--json` prints.
310
+ export function collectDoctor(root) {
308
311
  const checks = [];
309
312
  envChecks(checks);
310
313
  projectChecks(checks, root);
311
314
  epicChecks(checks, root);
312
315
  threadChecks(checks, root);
316
+ const failed = checks.filter((x) => x.status === 'fail');
317
+ return { version: VERSION, ok: failed.length === 0, checks };
318
+ }
319
+
320
+ export async function runDoctor(root, { json = false } = {}) {
321
+ const { checks } = collectDoctor(root);
313
322
 
314
323
  const failed = checks.filter((x) => x.status === 'fail');
315
324
  const warned = checks.filter((x) => x.status === 'warn');
package/cli/manifest.mjs CHANGED
@@ -7,8 +7,15 @@ import { readFileSync } from 'node:fs';
7
7
  // tracks the semantic-release-managed version — never a hardcoded constant
8
8
  // that would drift after a release. package.json ships in the npm tarball and
9
9
  // sits at the package root, one level up from this cli/ dir.
10
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
- export const VERSION = version;
10
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
+ export const VERSION = pkg.version;
12
+
13
+ // The upstream yadflow repo, as `owner/name` — where `yad report` files issues. Derived from
14
+ // package.json `bugs.url` (the single source of truth) so it tracks a fork/rename automatically;
15
+ // falls back to the canonical slug if the field is ever malformed.
16
+ export const UPSTREAM_REPO =
17
+ (pkg.bugs?.url || '').match(/github\.com\/([^/]+\/[^/]+?)(?:\/issues)?\/?$/i)?.[1]
18
+ || 'abdelrahmannasr/yadflow';
12
19
 
13
20
  // The hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
14
21
  export const SKILLS = [
@@ -44,6 +51,7 @@ export const SKILLS = [
44
51
  'yad-review-companion',
45
52
  'yad-pair-review',
46
53
  'yad-status',
54
+ 'yad-report',
47
55
  'yad-change',
48
56
  'yad-timeline',
49
57
  'yad-defects',
package/cli/platform.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  // skills/yad-hub-bridge/references/bridge.md. Everything runs as the local user (gh/glab own auth);
3
3
  // no tokens are stored. Pure mapping fns (resolveLogin/mapApprovers) are exported for unit tests;
4
4
  // readPr is injectable so the gate can be tested with a fake.
5
+ import { URLSearchParams } from 'node:url';
5
6
  import { run, has } from './lib.mjs';
6
7
  import { parseEngagement } from './companion.mjs';
7
8
 
@@ -424,3 +425,71 @@ export function createPr(platform, opts = {}) {
424
425
  }
425
426
  return { ok: true, url, reviewers: reviewers.slice(0, 1), mentioned, dropped };
426
427
  }
428
+
429
+ // ---- issues (for `yad report`) ------------------------------------------------------------------
430
+ // Filing a bug against the upstream yadflow repo. Same local-user auth as everything else: the call
431
+ // inherits the user's own gh/glab session, no tokens handled. `repo` is an `owner/name` slug; the
432
+ // upstream lives on GitHub, so the github path is the primary one (glab kept for symmetry).
433
+ // `runner` is injectable so cli/report.mjs — and its tests — never shell out.
434
+
435
+ // Is the platform CLI present AND authenticated? A best-effort probe (mirrors doctor's auth check).
436
+ // Used to decide direct-file vs the URL fallback; a false here is not an error, just "use the URL".
437
+ export function platformAuthed(platform, { runner = run } = {}) {
438
+ const cli = cliFor(platform);
439
+ if (!cli || !has(cli)) return false;
440
+ return runner(cli, ['auth', 'status']).ok;
441
+ }
442
+
443
+ // Open issues whose title/body match `query`. Returns { ok, matches: [{number, title, url}] }.
444
+ // A failed/absent CLI returns ok:false so the caller can skip dedup rather than block filing.
445
+ export function searchIssues(platform, repo, query, { runner = run, limit = 5 } = {}) {
446
+ if (platform === 'gitlab') {
447
+ const r = runner('glab', ['issue', 'list', '--repo', repo, '--search', query, '-P', String(limit), '-F', 'json']);
448
+ if (!r.ok) return { ok: false, matches: [] };
449
+ try {
450
+ const rows = JSON.parse(r.stdout || '[]');
451
+ return { ok: true, matches: rows.map((i) => ({ number: i.iid, title: i.title, url: i.web_url })) };
452
+ } catch { return { ok: false, matches: [] }; }
453
+ }
454
+ const r = runner('gh', ['issue', 'list', '--repo', repo, '--search', query, '--state', 'open', '--limit', String(limit), '--json', 'number,title,url']);
455
+ if (!r.ok) return { ok: false, matches: [] };
456
+ try {
457
+ return { ok: true, matches: JSON.parse(r.stdout || '[]') };
458
+ } catch { return { ok: false, matches: [] }; }
459
+ }
460
+
461
+ // The issue URL from a create command's stdout: the last line that looks like one (tolerates any
462
+ // trailing notice the CLI may print after it), falling back to the last non-empty line.
463
+ const urlFromStdout = (stdout = '') => {
464
+ const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
465
+ return [...lines].reverse().find((l) => /^https?:\/\//.test(l)) || lines.pop() || '';
466
+ };
467
+
468
+ // Create an issue. Returns { ok, url } or { ok:false, reason }. Mirrors createPr's shape.
469
+ export function createIssue(platform, repo, { title, body, labels = [] } = {}, { runner = run } = {}) {
470
+ if (platform === 'gitlab') {
471
+ const args = ['issue', 'create', '--repo', repo, '--title', title, '--description', body];
472
+ for (const l of labels) args.push('--label', l);
473
+ const r = runner('glab', args);
474
+ if (!r.ok) return { ok: false, reason: r.stderr };
475
+ return { ok: true, url: urlFromStdout(r.stdout) };
476
+ }
477
+ const args = ['issue', 'create', '--repo', repo, '--title', title, '--body', body];
478
+ for (const l of labels) args.push('--label', l);
479
+ const r = runner('gh', args);
480
+ if (!r.ok) return { ok: false, reason: r.stderr };
481
+ return { ok: true, url: urlFromStdout(r.stdout) };
482
+ }
483
+
484
+ // The prefilled `issues/new` URL — the always-works fallback when the CLI is missing/unauthenticated.
485
+ // GitHub honours ?title=&body= (and &labels=); GitLab uses issue[title]/issue[description].
486
+ export function issueUrl(platform, repo, { title = '', body = '', labels = [] } = {}) {
487
+ if (platform === 'gitlab') {
488
+ const q = new URLSearchParams({ 'issue[title]': title, 'issue[description]': body });
489
+ if (labels.length) q.set('issue[label_names][]', labels.join(','));
490
+ return `https://gitlab.com/${repo}/-/issues/new?${q.toString()}`;
491
+ }
492
+ const q = new URLSearchParams({ title, body });
493
+ if (labels.length) q.set('labels', labels.join(','));
494
+ return `https://github.com/${repo}/issues/new?${q.toString()}`;
495
+ }
package/cli/report.mjs ADDED
@@ -0,0 +1,227 @@
1
+ // `yad report` — the self issue reporter. When a flow breaks, help the user file a well-formed bug
2
+ // in the upstream yadflow repo with diagnostics attached, so recurring issues surface to maintainers.
3
+ //
4
+ // PRIVACY IS THE POINT: the issue posts to a PUBLIC repo, so this module is allowlist-first. It
5
+ // assembles ONLY a known-safe set of fields (version, node/os, tool booleans, platform enum, error
6
+ // code/hint, a path-scrubbed message, and command + flag NAMES) and actively strips everything else —
7
+ // no absolute paths, hostnames, git URLs, repo names, roster logins/emails, epic/story IDs, branch
8
+ // names, or flag values ever leave the machine. The user sees the exact payload and confirms before
9
+ // anything is posted. See memory: no-private-data-in-reports.
10
+ import path from 'node:path';
11
+ import { c, log, info, ok, warn, note, ask, askYesNo, has, readJSON, run } from './lib.mjs';
12
+ import { VERSION, UPSTREAM_REPO, PROJECT_FILES } from './manifest.mjs';
13
+ import { createIssue, searchIssues, issueUrl, platformAuthed } from './platform.mjs';
14
+
15
+ // The upstream lives on GitHub — file there regardless of the user's own hub platform.
16
+ const UPSTREAM_PLATFORM = 'github';
17
+ const PLACEHOLDER = '‹redacted›';
18
+
19
+ // ---- scrubbing ----------------------------------------------------------------------------------
20
+
21
+ // Strip anything that could carry private context from a free-text string. Ordered so broader
22
+ // structures (URLs, hosts) go first. Over-redaction is the safe direction — a redacted timestamp or
23
+ // filename beats a leaked home directory, internal hostname, or branch name. Covers: URLs of any
24
+ // scheme, scp-style ssh remotes, emails, IPv4/IPv6 (+port), multi-label hostnames, Windows UNC and
25
+ // drive paths, unix/home paths, epic/story IDs, and remaining `a/b`-style refs (branch names).
26
+ export function scrub(s = '') {
27
+ return String(s)
28
+ .replace(/\b[a-z][\w+.-]*:\/\/\S+/gi, '‹url›') // scheme://... (http, https, ssh, git, ftp, file)
29
+ .replace(/\b[\w.+-]+@[\w.-]+:[^\s'"]+/g, '‹url›') // scp-style ssh remote: user@host:path
30
+ .replace(/\b[\w.+-]+@[\w.-]+\.\w+/g, PLACEHOLDER) // email
31
+ .replace(/\b\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?\b/g, '‹ip›') // IPv4 (+ optional :port)
32
+ .replace(/\b(?:[0-9a-f]{1,4}:){3,}[0-9a-f]{0,4}\b/gi, '‹ip›') // IPv6 (>=3 hextet groups)
33
+ .replace(/\b(?:[\w-]+\.){2,}[a-z]{2,}\b/gi, '‹host›') // FQDN (>=3 labels — spares 2-label filenames)
34
+ .replace(/\\\\[\w.-]+(?:\\[\w.$-]+)+/g, '‹path›') // Windows UNC \\host\share\...
35
+ .replace(/\b[A-Za-z]:\\[\w.\\-]+/g, '‹path›') // Windows drive path C:\...
36
+ .replace(/(?:~)?\/[\w.-]+(?:\/[\w.-]+)*/g, '‹path›') // unix/home path (>=1 segment)
37
+ .replace(/\bEP-[a-z0-9-]+\b/gi, '‹id›') // epic / story IDs (standalone)
38
+ .replace(/\b[\w.-]+\/[\w.-]+(?:\/[\w.-]+)*\b/g, '‹ref›'); // remaining a/b refs (branch names, origin/x)
39
+ }
40
+
41
+ // The verbs `yad` understands (top-level commands + their sub-actions). Anything NOT in this set —
42
+ // roster logins, repo names, roles, epic IDs, filenames, and every flag VALUE — is dropped, so the
43
+ // reported command carries only structure, never data.
44
+ const SAFE_VERBS = new Set([
45
+ 'setup', 'check', 'update', 'doctor', 'report', 'sync-status', 'next', 'gate', 'review', 'commit',
46
+ 'open-pr', 'ship', 'repo', 'roster', 'docs', 'thread', 'reconcile',
47
+ 'open', 'sync', 'comments', 'status', 'walkthrough', 'trailer', 'ci', 'context', 'chat', 'cards',
48
+ 'nudge', 'list', 'add', 'grant', 'revoke', 'remove', 'build', 'deploy', 'refresh', 'wire',
49
+ ]);
50
+
51
+ // Reduce an argv to a safe command line: the leading verb chain (at most `command subcommand`, and
52
+ // only tokens in SAFE_VERBS) plus flag NAMES only. Every positional that isn't a known verb — epic
53
+ // IDs (`EP-foo`), logins, repo names, roles, filenames — and every flag VALUE is dropped, so both
54
+ // `yad gate sync EP-x --dir /p -m "secret"` and `yad roster add joesmith` reduce to structure only.
55
+ export function sanitizeArgv(argv = []) {
56
+ const out = [];
57
+ let i = 0;
58
+ for (; i < argv.length && out.length < 2; i++) {
59
+ const t = argv[i];
60
+ if (t.startsWith('-') || !SAFE_VERBS.has(t)) break; // stop at the first flag or non-verb token
61
+ out.push(t);
62
+ }
63
+ for (; i < argv.length; i++) {
64
+ const t = argv[i];
65
+ if (t.startsWith('-')) out.push(t.split('=')[0]); // flag names only — values and positionals are never kept
66
+ }
67
+ return out.join(' ');
68
+ }
69
+
70
+ // ---- context (allowlist) ------------------------------------------------------------------------
71
+
72
+ // Build the safe, postable context — the ONLY data that can reach the issue. Reads doctor for tool
73
+ // state but keeps just the booleans, never the raw checks (which carry names + paths).
74
+ export function sanitizeContext(dir, { error = null, argv = process.argv.slice(2) } = {}) {
75
+ const hub = readJSON(path.join(dir, PROJECT_FILES.hubConfig), null);
76
+ const platform = hub && ['github', 'gitlab'].includes(hub.platform) ? hub.platform : 'file-only';
77
+ // Derive tool auth from doctor's checks without keeping any check text.
78
+ const toolState = (cli, p) => (has(cli) ? (platformAuthed(p) ? 'present + authenticated' : 'present, not authenticated') : 'not installed');
79
+ const ctx = {
80
+ version: VERSION,
81
+ node: process.versions.node,
82
+ os: process.platform,
83
+ git: has('git') ? 'present' : 'not found',
84
+ gh: toolState('gh', 'github'),
85
+ // The hub CLI's auth is a useful diagnostic for a GitLab user (issues still file to GitHub upstream).
86
+ ...(platform === 'gitlab' ? { glab: toolState('glab', 'gitlab') } : {}),
87
+ platform,
88
+ command: sanitizeArgv(argv) || '(none)',
89
+ };
90
+ if (error) {
91
+ ctx.error = {
92
+ code: /^YAD-/.test(error.code || '') ? error.code : null,
93
+ message: scrub(error.message || String(error)),
94
+ hint: error.hint ? scrub(error.hint) : null,
95
+ };
96
+ }
97
+ return ctx;
98
+ }
99
+
100
+ // ---- body assembly ------------------------------------------------------------------------------
101
+
102
+ // Title: prefer the error code + command; fall back to the (scrubbed) summary.
103
+ export function buildTitle(ctx, summary) {
104
+ const code = ctx.error?.code ? ` [${ctx.error.code}]` : '';
105
+ const cmd = ctx.command && ctx.command !== '(none)' ? `\`yad ${ctx.command}\`` : 'yad';
106
+ if (ctx.error) return `[report] ${cmd} failed${code}`;
107
+ return `[report] ${summary ? summary.slice(0, 72) : 'yad issue'}`;
108
+ }
109
+
110
+ // Body mirrors the fields of .github/ISSUE_TEMPLATE/bug_report.yml — but only the safe subset.
111
+ export function buildBody(ctx, summary) {
112
+ const lines = [];
113
+ lines.push('### What happened?', '', summary || '_(no summary provided)_', '');
114
+ lines.push(`Command: \`yad ${ctx.command}\``, '');
115
+ lines.push('### Environment');
116
+ lines.push(`- yadflow: \`${ctx.version}\``);
117
+ lines.push(`- node: \`${ctx.node}\``);
118
+ lines.push(`- os: \`${ctx.os}\``);
119
+ lines.push(`- git: ${ctx.git}`);
120
+ lines.push(`- gh: ${ctx.gh}`);
121
+ if (ctx.glab) lines.push(`- glab: ${ctx.glab}`);
122
+ lines.push(`- platform: ${ctx.platform}`);
123
+ lines.push('');
124
+ if (ctx.error) {
125
+ lines.push('### Error');
126
+ if (ctx.error.code) lines.push(`- code: \`${ctx.error.code}\``);
127
+ lines.push(`- message: ${ctx.error.message}`);
128
+ if (ctx.error.hint) lines.push(`- hint: ${ctx.error.hint}`);
129
+ lines.push('');
130
+ }
131
+ lines.push('---', '_Filed via `yad report`. Diagnostics are auto-scrubbed — no paths, hostnames, repo names, logins, or flag values._');
132
+ return lines.join('\n');
133
+ }
134
+
135
+ // ---- open a URL (best-effort) -------------------------------------------------------------------
136
+ function openUrl(url, { runner = run } = {}) {
137
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
138
+ if (has(cmd)) runner(cmd, [url]);
139
+ }
140
+
141
+ // ---- the flow -----------------------------------------------------------------------------------
142
+
143
+ // Never throws — a failure while reporting must not crash the CLI (and, from the top-level catch,
144
+ // must not re-trigger a report). Returns { filed, url } for tests/callers.
145
+ export async function runReport(dir, opts = {}) {
146
+ const {
147
+ error = null,
148
+ message = null,
149
+ // injectable seams (tests never shell out or prompt)
150
+ filer = createIssue,
151
+ searcher = searchIssues,
152
+ prompter = askYesNo,
153
+ asker = ask,
154
+ opener = openUrl,
155
+ authed = () => platformAuthed(UPSTREAM_PLATFORM),
156
+ argv = process.argv.slice(2),
157
+ interactive = !process.env.SDLC_NONINTERACTIVE,
158
+ } = opts;
159
+
160
+ try {
161
+ const ctx = sanitizeContext(dir, { error, argv });
162
+
163
+ // Summary — the user's own description (scrubbed for stray paths/urls). Ask if interactive.
164
+ let summary = message ? scrub(message) : '';
165
+ if (!summary && interactive) {
166
+ const a = await asker('One line — what went wrong?', '');
167
+ summary = a ? scrub(a) : '';
168
+ }
169
+
170
+ const title = buildTitle(ctx, summary);
171
+ const body = buildBody(ctx, summary);
172
+ const labels = ['bug'];
173
+
174
+ // Dedup — search open issues by the error code (or the first summary word). Advisory: a failed
175
+ // search just skips this step, it never blocks filing.
176
+ const query = ctx.error?.code || (summary.split(/\s+/)[0] || '').replace(/[^\w-]/g, '');
177
+ if (query) {
178
+ const { ok: searchedOk, matches } = searcher(UPSTREAM_PLATFORM, UPSTREAM_REPO, `${query} in:title`);
179
+ if (searchedOk && matches.length) {
180
+ log(c.bold(`\nFound ${matches.length} possibly-related open issue(s):`));
181
+ for (const m of matches) info(`#${m.number} — ${m.title} ${c.dim(m.url)}`);
182
+ if (interactive && await prompter('Open an existing issue instead of filing a new one?', true)) {
183
+ opener(matches[0].url);
184
+ log(`\n → ${c.cyan(matches[0].url)}`);
185
+ return { filed: false, url: matches[0].url, deduped: true };
186
+ }
187
+ }
188
+ }
189
+
190
+ // Preview — show EXACTLY what will be posted to the public repo before anything leaves.
191
+ log(c.bold(`\nThis will be posted to ${c.cyan(UPSTREAM_REPO)} (a public repo):\n`));
192
+ log(c.bold(title));
193
+ log(c.dim('─'.repeat(60)));
194
+ log(body);
195
+ log(c.dim('─'.repeat(60)));
196
+
197
+ // Posting to a public repo is always a deliberate act — require an explicit yes. Non-interactive
198
+ // runs never auto-post; they hand back the prefilled URL instead.
199
+ const confirmed = interactive ? await prompter('Post this now?', false) : false;
200
+ if (!confirmed) {
201
+ const url = issueUrl(UPSTREAM_PLATFORM, UPSTREAM_REPO, { title, body, labels });
202
+ note(interactive ? 'Not posted. You can file it yourself here (prefilled):' : 'Non-interactive — not posted. File it here (prefilled):');
203
+ log(` → ${c.cyan(url)}`);
204
+ return { filed: false, url };
205
+ }
206
+
207
+ // File directly when the CLI is authenticated; otherwise fall back to the prefilled URL.
208
+ if (authed()) {
209
+ const r = filer(UPSTREAM_PLATFORM, UPSTREAM_REPO, { title, body, labels });
210
+ if (r.ok) {
211
+ ok(`Issue filed: ${c.cyan(r.url)}`);
212
+ return { filed: true, url: r.url };
213
+ }
214
+ warn(`Could not file automatically: ${r.reason || 'unknown error'}`);
215
+ } else {
216
+ note('gh is not authenticated — opening a prefilled issue in your browser instead.');
217
+ }
218
+ const url = issueUrl(UPSTREAM_PLATFORM, UPSTREAM_REPO, { title, body, labels });
219
+ opener(url);
220
+ log(` → ${c.cyan(url)}`);
221
+ return { filed: false, url };
222
+ } catch (e) {
223
+ // Reporting itself must never crash the CLI.
224
+ note(`could not complete the report (${e?.message || e}) — please file manually at https://github.com/${UPSTREAM_REPO}/issues`);
225
+ return { filed: false, url: null, failed: true };
226
+ }
227
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
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, thread, reconcile). A BMAD module + 34 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -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-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-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-review-companion yad-pair-review yad-status yad-change yad-timeline yad-defects yad-reconcile)
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-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-review-companion yad-pair-review yad-status yad-report yad-change yad-timeline yad-defects yad-reconcile)
15
15
 
16
16
  # Skills removed in a later release: this installer only refreshes names still in SKILLS, so a
17
17
  # rerun would otherwise leave a dropped skill sitting in the IDE dirs. Purge any lingering copy
@@ -27,6 +27,7 @@ SDLC Workflow,yad-engineer-review,Engineer Review & Merge,ER,"Build-half Step E:
27
27
  SDLC Workflow,yad-backfill,Backfill Specs,BF,"Build-half Step G: generate specs for already-built features in an existing repo. Confirm Repomix (npx repomix CLI), pack ONE feature (compress + git logs, secret-scan), feed to AI with a 'describe what exists, do not invent' prompt, write a DRAFT spec marked verified: false. Human approval (reuse yad-review-gate) makes it real. Boundary auto-proposed and human-confirmed. A change is blocked only until the features it touches have approved specs. Never auto-advances.",,{repo: <repo>} {feature: <name + globs>} {action: pack|draft|approve|gate},3-build,,,false,demo-repos/<repo>/specs/backfill/<feature>/,spec.md backfill-check.sh
28
28
  SDLC Workflow,yad-run,Run (Automation),RN,"Phase 4 orchestrator: drive a story's back-half loop (spec→tasks→implement→checks) in one code repo, reading each step's automation dial from build-state — on machine_advance it advances on its own, on human_approve it stops for a human. Records every run in the trust log. Realizes Step B (a clean checks pass auto-advances to the engineer review when earned; any FAIL / scope overrun / contract touch HALTS). set-dial earns/reverts a step's automation (machine_advance gated by the trust threshold; front states and engineer-review refused); kill/unkill toggles the system-wide kill switch. Front states and the human merge gate never auto-advance.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {repo: <repo>} {action: run|set-dial|kill|unkill} {step: <back-step>} {to: human_approve|machine_advance},4-automate,yad-spec,yad-engineer-review,false,epics/EP-<slug>/.sdlc/,build-state/<story-id>.json trust-log.json
29
29
  SDLC Workflow,yad-status,SDLC Status,SS,"Read-only: print the full front-state chain, per-step dials, contract lock, story repo tags, and pending approvals at the active gate. For stories in the build half, also print each back-half step's automation dial and status, the trust record (runs / % approved-unchanged / earned vs gathering evidence), and the system-wide kill-switch state.",,{epic: EP-<slug>},1-front,,,false,,
30
+ SDLC Workflow,yad-report,Report Issue,RP,"Self issue reporter: when a yad flow breaks, file a well-formed bug in the upstream yadflow repo with AUTO-SCRUBBED diagnostics (yadflow/node/os version, tool present+authenticated booleans, hub platform enum, the YadError code/hint, a path-scrubbed message, and the failing command + flag NAMES only) — never absolute paths, hostnames, git URLs, repo names, roster logins/emails, epic/story IDs, branch names, or flag values. Searches open issues first to avoid duplicates, previews the exact payload, and asks before posting to the public repo; files via an authenticated gh/glab or a prefilled issues/new URL fallback. Also offered automatically after an unexpected failure (interactive only; YAD_NO_REPORT=1 disables). Never a gate, never touches epic state.",,{message: one-line summary},,,,false,,
30
31
  SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connect a docs/Pages publishing target so the interactive-docs steps can deploy the generated site (not just commit its source). Auto-detects the platform from .sdlc/hub.json (github->github-pages, gitlab->gitlab-pages, null->build-only), resolves the Vite base path, and records the target in .sdlc/docs.json (local-user auth, no stored tokens). Degrades to build-only when gh/glab is absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {target: github-pages|gitlab-pages|none} {scope: hub|<repo>|dedicated} {base_path: <override>},0-setup,,yad-docs,false,.sdlc/,docs.json
31
32
  SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
32
33
  SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
@@ -31,7 +31,8 @@ jobs:
31
31
  - uses: actions/checkout@v4
32
32
  with: { fetch-depth: 0 }
33
33
  - uses: actions/setup-node@v4
34
- with: { node-version: "20" }
34
+ with: { node-version: "20", cache: "npm" }
35
+ - run: npm ci # install deps so the real lint/build/test toolchain can run (mirrors the GitLab template)
35
36
  - run: bash checks/build-test-lint.sh
36
37
 
37
38
  # Phase 6 — feature-thread gates. lineage-check: the change links a real threaded epic. epic-open:
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: yad-report
3
+ description: 'Self issue reporter for the yad CLI. When a yadflow flow breaks, file a well-formed bug in the upstream yadflow repo (abdelrahmannasr/yadflow) with auto-scrubbed diagnostics attached, so recurring issues surface to the maintainers. Drives the `yad report` command: it captures ONLY a privacy-safe allowlist (yadflow/node/os version, tool present+authenticated booleans, hub platform enum, the YadError code/hint, a path-scrubbed error message, and the failing command + flag NAMES) — never absolute paths, hostnames, git URLs, repo names, roster logins/emails, epic/story IDs, branch names, or flag values. It searches open issues first to avoid duplicates, shows the exact payload, and asks before posting anything to the public repo. Files directly via an authenticated gh/glab, or falls back to a prefilled issues/new URL. Also offered automatically after an unexpected failure (interactive only; YAD_NO_REPORT=1 or SDLC_NONINTERACTIVE disables it). Use when the user says "yad report", "report this bug", "file an issue", or "something broke in the flow".'
4
+ ---
5
+
6
+ # yad — Self Issue Reporter
7
+
8
+ **Goal:** turn a broken flow into a fixable, well-formed bug report in the upstream yadflow repo —
9
+ without leaking any private data. This skill drives the `yad report` CLI command; it does not
10
+ hand-craft issues.
11
+
12
+ ## When it runs
13
+ - **Manually:** the user runs `yad report` (optionally `yad report -m "one-line summary"`).
14
+ - **Automatically:** after an unexpected failure, the CLI's top-level handler offers
15
+ *"Report this failure to the yadflow team?"*. This is **interactive only** — it never fires in CI
16
+ (`SDLC_NONINTERACTIVE`) and is disabled by `YAD_NO_REPORT=1`.
17
+
18
+ ## Privacy is the contract
19
+ Issues post to a **public** repo, so the reporter is **allowlist-first**. It sends ONLY:
20
+ - `yadflow` version, Node version, OS platform;
21
+ - tool state as booleans (`git` present; `gh` present + authenticated);
22
+ - the hub **platform enum** (`github` / `gitlab` / `file-only`) — never the URL, host, or roster;
23
+ - the `YadError` **code** + **hint**, and a **path-scrubbed** error message;
24
+ - the failing **command name and flag names only** — never flag values.
25
+
26
+ It **never** sends absolute paths, cwd, hostnames, git remote URLs, repo names/paths, roster logins
27
+ or emails, epic/story IDs or titles, branch names, `-m` free text, or the raw `yad doctor --json`
28
+ check list. Free-text fields (the summary, the error message) are scrubbed of paths / URLs / emails.
29
+
30
+ ## On activation
31
+ 1. **Assemble the safe context** (`sanitizeContext`) and, if interactive and no `-m` was given, ask
32
+ the user for a one-line summary.
33
+ 2. **Deduplicate:** search open issues in `abdelrahmannasr/yadflow` by the error code (or the first
34
+ summary word). If matches exist, list them and offer to open an existing one instead of filing a
35
+ duplicate.
36
+ 3. **Preview + consent:** print the exact title + body that will be posted to the public repo, then
37
+ ask *"Post this now?"*. Nothing leaves the machine without this confirmation.
38
+ 4. **File:** with an authenticated `gh`/`glab`, create the issue directly (label `bug`) and print the
39
+ URL. Otherwise, fall back to a **prefilled `issues/new` URL** (printed and, on a TTY, opened in the
40
+ browser) so the user can complete it manually.
41
+
42
+ ## Hard rules
43
+ - Reuse the `yad report` command — do not craft issues by hand or shell out to `gh` directly.
44
+ - Reporting is best-effort: it must never crash the CLI or mask the original failure.
45
+ - Respect `SDLC_NONINTERACTIVE` and `YAD_NO_REPORT` — no prompts, no auto-offer when they are set.
46
+ - Diagnostics are the maintainers' input to fix bugs; keep the payload safe and truthful.