yadflow 3.3.1 → 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 +3 -3
- package/bin/yad.mjs +22 -2
- package/cli/doctor.mjs +10 -1
- package/cli/manifest.mjs +10 -2
- package/cli/platform.mjs +69 -0
- package/cli/report.mjs +227 -0
- package/package.json +1 -1
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +1 -0
- package/skills/yad-report/SKILL.md +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
+
"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",
|
package/skills/sdlc/install.sh
CHANGED
|
@@ -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
|
|
@@ -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.
|