yadflow 3.18.1 → 4.0.0-next.1
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 +355 -0
- package/README.md +79 -26
- package/bin/commands.mjs +41 -0
- package/bin/yad.mjs +437 -124
- package/cli/artifact-status.mjs +34 -15
- package/cli/checkpoint.mjs +69 -49
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/companion.mjs +2 -2
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1472 -97
- package/cli/epic-state.mjs +3478 -232
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +4 -1
- package/cli/gate.mjs +1002 -209
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +266 -55
- package/cli/hubcommit.mjs +6 -17
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +57 -7
- package/cli/lib.mjs +184 -18
- package/cli/manifest.mjs +367 -56
- package/cli/migrate.mjs +726 -53
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +349 -90
- package/cli/openpr.mjs +191 -39
- package/cli/people.mjs +654 -0
- package/cli/plan.mjs +417 -132
- package/cli/platform.mjs +110 -129
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +38 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +21 -15
- package/cli/review.mjs +24 -27
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +139 -287
- package/cli/ship.mjs +7 -6
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +42 -17
- package/cli/tidy.mjs +20 -20
- package/cli/update-commit.mjs +22 -22
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +166 -87
- package/skills/sdlc/module-help.csv +35 -35
- package/skills/yad-analysis/SKILL.md +125 -65
- package/skills/yad-architecture/SKILL.md +34 -23
- package/skills/yad-architecture/references/contract-format.md +10 -8
- package/skills/yad-backfill/SKILL.md +14 -8
- package/skills/yad-backfill/references/backfill.md +1 -1
- package/skills/yad-change/SKILL.md +127 -52
- package/skills/yad-change/references/triage.md +42 -28
- package/skills/yad-checks/SKILL.md +89 -45
- package/skills/yad-checks/references/check-gates.md +315 -92
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
- package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
- package/skills/yad-commit/SKILL.md +6 -6
- package/skills/yad-connect-design/SKILL.md +6 -6
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +12 -12
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +92 -54
- package/skills/yad-connect-repos/references/code-context.md +6 -6
- package/skills/yad-connect-repos/references/hub-config.md +68 -58
- package/skills/yad-connect-repos/references/repos-registry.md +10 -9
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +6 -6
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +8 -8
- package/skills/yad-discovery/SKILL.md +130 -94
- package/skills/yad-discovery/references/discovery-schema.md +23 -7
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +16 -11
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +32 -17
- package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +88 -24
- package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
- package/skills/yad-epic/SKILL.md +178 -100
- package/skills/yad-epic/references/state-schema.md +626 -117
- package/skills/yad-hub-bridge/SKILL.md +66 -48
- package/skills/yad-hub-bridge/references/bridge.md +110 -83
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
- package/skills/yad-implement/SKILL.md +29 -15
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +64 -29
- package/skills/yad-pair-review/SKILL.md +18 -16
- package/skills/yad-pair-review/references/session-state.md +4 -4
- package/skills/yad-pr-template/SKILL.md +48 -27
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
- package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +12 -9
- package/skills/yad-review-gate/SKILL.md +198 -79
- package/skills/yad-review-gate/references/gating.md +230 -54
- package/skills/yad-run/SKILL.md +86 -56
- package/skills/yad-run/references/run-loop.md +67 -45
- package/skills/yad-ship/SKILL.md +18 -14
- package/skills/yad-spec/SKILL.md +31 -17
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +114 -56
- package/skills/yad-stories/SKILL.md +42 -27
- package/skills/yad-stories/references/story-schema.md +10 -9
- package/skills/yad-stub/SKILL.md +59 -48
- package/skills/yad-sync-repos/SKILL.md +3 -3
- package/skills/yad-test-cases/SKILL.md +37 -30
- package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +46 -25
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
package/bin/yad.mjs
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
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, askYesNo } from '../cli/lib.mjs';
|
|
5
|
-
import { runSetup } from '../cli/setup.mjs';
|
|
6
|
-
import { reconcile } from '../cli/reconcile.mjs';
|
|
7
|
-
import { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer, gateWalkthrough, gateRepair } from '../cli/gate.mjs';
|
|
8
|
-
import { isValidEpicId } from '../cli/epic-state.mjs';
|
|
9
|
-
import { runCommit } from '../cli/commit.mjs';
|
|
10
|
-
import { runOpenPr } from '../cli/openpr.mjs';
|
|
11
|
-
import { reviewTrailer, reviewContext, reviewNudge, reviewReconcile, reviewWalkthrough } from '../cli/review.mjs';
|
|
12
|
-
import { runShip } from '../cli/ship.mjs';
|
|
13
|
-
import { runCheckpoint } from '../cli/checkpoint.mjs';
|
|
14
|
-
import { runTidy } from '../cli/tidy.mjs';
|
|
15
|
-
import { runRepo } from '../cli/repo.mjs';
|
|
16
|
-
import { runRoster } from '../cli/roster.mjs';
|
|
17
|
-
import { runDocs } from '../cli/docs.mjs';
|
|
18
|
-
import { runDoctor } from '../cli/doctor.mjs';
|
|
19
|
-
import { runMigrate } from '../cli/migrate.mjs';
|
|
20
|
-
import { runNext } from '../cli/next.mjs';
|
|
21
|
-
import { runSkip } from '../cli/skip.mjs';
|
|
22
|
-
import { syncStatuses } from '../cli/artifact-status.mjs';
|
|
23
|
-
import { runThread, runReconcile } from '../cli/thread.mjs';
|
|
24
|
-
import { runReport } from '../cli/report.mjs';
|
|
25
|
-
import { runUsage } from '../cli/usage.mjs';
|
|
4
|
+
import { c, log, warn, closePrompts, askYesNo, refuse, beginJSON, inJSON, emitJSON, jsonEmitted, jsonFailure, stripAnsi, isPlainObject, ENVELOPE_KEYS } from '../cli/lib.mjs';
|
|
26
5
|
import { runLedgerGuardHook } from '../cli/hook.mjs';
|
|
27
|
-
import { maybeNotifyUpdate } from '../cli/update-notice.mjs';
|
|
28
6
|
|
|
29
|
-
const
|
|
7
|
+
const helpText = (profiles) => `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
8
|
+
|
|
9
|
+
${c.dim('Every command but `yad hook` takes --json: one JSON object on stdout, every other line on stderr')}
|
|
10
|
+
${c.dim('(docs/CLI.md, "--json on every command").')}
|
|
30
11
|
|
|
31
12
|
${c.bold('Setup & maintenance')}
|
|
32
13
|
yad setup Guided first-run setup (profile interview, install, connect & wire repos)
|
|
@@ -42,7 +23,7 @@ ${c.bold('Setup & maintenance')}
|
|
|
42
23
|
it. Anything else it cannot account for is replaced only after
|
|
43
24
|
a <file>.yad-orig backup
|
|
44
25
|
yad update --push Also commit each repo's applied changes and push them straight to the
|
|
45
|
-
default branch of the
|
|
26
|
+
default branch of the Product + every connected repo (a chore(yad-update)
|
|
46
27
|
commit; no PR — the push-on-main yad-update-guard runs verified-commits
|
|
47
28
|
+ commit-message). Announce the team & pause merges first. Also works as
|
|
48
29
|
'yad check --fix --push'; --allow-branch permits a non-default branch
|
|
@@ -59,18 +40,13 @@ ${c.bold('Setup & maintenance')}
|
|
|
59
40
|
(no paths/hosts/repo names/logins/flag values). Also offered
|
|
60
41
|
automatically after an unexpected failure. YAD_NO_REPORT=1 disables.
|
|
61
42
|
yad hook ledger-guard ${c.dim('harness-invoked, not typed')} — refuse an agent's edit to the
|
|
62
|
-
CI-owned gate ledger in
|
|
43
|
+
CI-owned gate ledger in verified mode and name the command that owns
|
|
63
44
|
the transition. Reads a tool-call payload on stdin (or --path <p>);
|
|
64
|
-
exit 0 allows, exit 2 denies with the reason on stderr.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
yad roster add <login> Add/edit a member, then walk the connected repos for their roles
|
|
70
|
-
(--name, --email, --roles "hub=owner,reviewer backend=domain-owner")
|
|
71
|
-
yad roster grant <name> <repo> <role...> Grant role(s) for a connected repo (domain-owner|reviewer|owner)
|
|
72
|
-
yad roster revoke <name> <repo> <role...> Remove role(s) for a repo
|
|
73
|
-
yad roster remove <login> Delete a member from the roster
|
|
45
|
+
exit 0 allows, exit 2 denies with the reason on stderr. With
|
|
46
|
+
--format cursor the verdict is JSON on stdout instead, for a harness
|
|
47
|
+
whose pre-edit hook asks for permission rather than reading an exit
|
|
48
|
+
code. Wired into .claude/settings.json and .cursor/hooks.json by
|
|
49
|
+
setup / check --fix. YAD_HOOK_DISABLE=1 skips.
|
|
74
50
|
|
|
75
51
|
${c.bold('Team usage (EM adoption & behavior report)')}
|
|
76
52
|
yad usage Build a per-member report (HTML) from git + the SDLC ledgers
|
|
@@ -80,25 +56,99 @@ ${c.bold('Team usage (EM adoption & behavior report)')}
|
|
|
80
56
|
--member <name>, --format html|json|md, --repos (code commits)
|
|
81
57
|
|
|
82
58
|
${c.bold('Where am I / what next')}
|
|
59
|
+
yad epic new <slug> [--type <t>] [--profile <p>] [--stub] [--parent <epic> --inherits <bases>] [--json]
|
|
60
|
+
Seed a new epic's lifecycle: writes its step chain from a
|
|
61
|
+
profile, plus empty approval/comment ledgers and reviews/.
|
|
62
|
+
--type feature|chore (default feature). A change/defect/
|
|
63
|
+
hotfix needs --parent: it takes the parent's route, and
|
|
64
|
+
--inherits epic,architecture,contract,ui-design carries
|
|
65
|
+
those steps by reference (satisfied, bound to the owner's
|
|
66
|
+
hash). Carrying the contract writes a pointer-lock instead
|
|
67
|
+
of a second contract (E42). The yad-change skill triages.
|
|
68
|
+
--profile ${profiles.join('|')} (default classic).
|
|
69
|
+
classic and analysis-first are the full chains; chore and
|
|
70
|
+
spike are E40's short lanes — epic + stories, with the
|
|
71
|
+
analyst's brief in front for a spike. Neither carries an
|
|
72
|
+
architecture gate, so neither may move the contract surface.
|
|
73
|
+
--stub seeds a brownfield anchor instead: the same chain
|
|
74
|
+
with every step blocked behind backfill-pending, so a
|
|
75
|
+
defect can thread off a feature that shipped before the
|
|
76
|
+
Product existed (yad-backfill promote wakes it).
|
|
77
|
+
Writes no epic.md, no branch and no commit: run the skill
|
|
78
|
+
the chain names next. Refuses an epic that already has one
|
|
79
|
+
yad foundation new [--json] Seed the Product level (E75): the Foundation's two-step chain
|
|
80
|
+
in foundation/.sdlc/, plus empty ledgers and reviews/. Run once
|
|
81
|
+
per product, before any epic — then the yad-discovery skill
|
|
82
|
+
authors its sections. Refuses a second one, and a product still
|
|
83
|
+
on the old epics/EP-discovery/ (yad migrate converts that)
|
|
84
|
+
yad foundation status [--json] Which roadmap features are started: reads each proposed epic id
|
|
85
|
+
in roadmap.md and reports planned / in-shape / in-build /
|
|
86
|
+
shipped from the epic ledgers. Read-only — the roadmap's Status
|
|
87
|
+
column is no longer kept by hand
|
|
83
88
|
yad next Project-wide: the one next action to take (or run setup)
|
|
84
89
|
yad next <epic> The single next action for one epic (skill or yad command)
|
|
85
90
|
yad next <epic> --check <step> Exit 0 if <step> is runnable now, else 1 (precondition guard)
|
|
86
91
|
yad next --all Every active epic's next action at once
|
|
87
92
|
yad next [<epic>] --json The same answer as a machine-readable action object (for
|
|
88
93
|
agents/CI) — always every epic, so --all is implied
|
|
89
|
-
yad
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
yad skill list [--json] Which skill runs which step, and whether that is this
|
|
95
|
+
project's choice or the engine's default
|
|
96
|
+
yad skill bind <step> <skill> [<skill> ...]
|
|
97
|
+
Bind a step to a skill of your own. Several skills run in
|
|
98
|
+
the order given, one after another — each costs tokens
|
|
99
|
+
yad skill unbind <step> Drop the binding; the step goes back to the engine's default
|
|
100
|
+
yad skip <epic> <step> --reason <text> Mark an optional step N/A for this epic. Which steps
|
|
101
|
+
those are comes from the epic's lifecycle route — on classic
|
|
102
|
+
and analysis-first, ui-design: a backend/API/data epic with no
|
|
103
|
+
UI. Stays visible & auditable (reason recorded, gate
|
|
104
|
+
short-circuited); refused once any later step has started
|
|
105
|
+
yad unskip <epic> <step> Put a skipped step back in the chain (\`skip --undo\` does the
|
|
106
|
+
same), until the step that follows it is finished or work
|
|
107
|
+
past it starts — on classic, until stories are done
|
|
108
|
+
yad skip <epic> <story> --repo <name> --reason <text>
|
|
109
|
+
Skip a whole Build lane: this story needs no change in this
|
|
110
|
+
repo (E39). Written to build-state/<story>.json; commit it with
|
|
111
|
+
yad checkpoint --push. Refused before the stories review passes
|
|
112
|
+
(edit the story's repos: instead), for a repo the story does not
|
|
113
|
+
declare, once work started or a ship is recorded, and for the
|
|
114
|
+
story's last lane. No single Build step can be skipped
|
|
115
|
+
yad unskip <epic> <story> --repo <name>
|
|
116
|
+
Put a skipped Build lane back; yad-run adds it on its next run
|
|
117
|
+
yad defer <epic> <step> --reason <text> [--debt]
|
|
118
|
+
Set an optional step aside to do LATER: marks it deferred.
|
|
119
|
+
Say who is waiting for it in the reason. Same steps and same
|
|
120
|
+
refusals as skip; the chain goes on, its review is still owed.
|
|
121
|
+
--debt marks it owed back: yad next and yad doctor remind
|
|
122
|
+
you until its review passes
|
|
123
|
+
yad undefer <epic> <step> Put a deferred step back, at any time. After later work has
|
|
124
|
+
finished, the step re-opens beside that work, which stays done
|
|
125
|
+
yad unblock <epic> <step> Clear a recorded blocker once the wait is over: moves the
|
|
126
|
+
step off blocked and removes its record in one write
|
|
127
|
+
yad dial <step> [--to auto|human] [--json]
|
|
128
|
+
A Shape author step's advance dial, for the whole project (E34),
|
|
129
|
+
kept in .sdlc/automation.json. No --to shows it. A review gate is
|
|
130
|
+
always human. Recorded only for now: nothing drives a Shape step
|
|
131
|
+
on its own until the engine runs agents
|
|
132
|
+
yad dial <epic> <story> --repo <name> <step> [--to auto|human] [--json]
|
|
133
|
+
A Build lane step's dial (spec, tasks, implement, checks), in
|
|
134
|
+
build-state; on auto the yad-run skill moves past it after a clean
|
|
135
|
+
run. Shows the step's run record as advice, never as a rule
|
|
136
|
+
yad kill --reason <text> Kill switch: hold every step at advance: human. Recorded in
|
|
137
|
+
.sdlc/automation.json — who, when and why
|
|
138
|
+
yad unkill [--reason <text>] Turn the kill switch off; each step follows its own dial again
|
|
139
|
+
yad mode [solo --reason <text> | team [--reason <text>]] [--json]
|
|
140
|
+
Who must approve. Solo waives approvals on every review gate (the
|
|
141
|
+
merge still decides); team counts them. Records who, when and why.
|
|
142
|
+
No word reads the mode. Open reviews follow it from their next sync
|
|
93
143
|
|
|
94
|
-
${c.bold('Review gate (
|
|
144
|
+
${c.bold('Review gate (Shape)')}
|
|
95
145
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review. The review
|
|
96
146
|
branch must already be on origin (it is never created here)
|
|
97
147
|
yad gate sync <epic> [artifact] [--pr <n>]
|
|
98
148
|
Pull PR state -> ledger; advance on approved+resolved+merged.
|
|
99
149
|
With no recorded PR, resolves it from the review branch; --pr
|
|
100
150
|
names one (and overrides a stale recorded pointer). Advisory
|
|
101
|
-
in
|
|
151
|
+
in verified mode — there, recover with 'yad gate ci' below
|
|
102
152
|
yad gate comments <epic> [artifact] Fetch unresolved review comments to address
|
|
103
153
|
yad gate status <epic> Show each review step + approvals
|
|
104
154
|
yad gate repair <epic> [--push] Close an author step stranded behind a passed review gate
|
|
@@ -110,18 +160,18 @@ ${c.bold('Review gate (front half)')}
|
|
|
110
160
|
yad gate trailer <epic> [artifact] --body <text> [--pr <n>]
|
|
111
161
|
Upsert the companion's 60-sec briefing into the PR/MR description
|
|
112
162
|
yad gate ci [--branch <head>] [--pr <n>] [--merged]
|
|
113
|
-
CI entry (
|
|
163
|
+
CI entry (Product workflow): pre-merge is read-only (nothing pushed);
|
|
114
164
|
--merged advances the step + flips artifact status on the default branch
|
|
115
165
|
|
|
116
166
|
${c.bold('Build helpers')}
|
|
117
167
|
yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
|
|
118
168
|
yad open-pr [--repo <name>] Open a task PR/MR against the repo's DEFAULT branch (never a
|
|
119
|
-
hardcoded main; --base overrides) — stage-aware on the
|
|
120
|
-
review/EP-* branch opens the
|
|
121
|
-
(delegates to gate open), any other
|
|
169
|
+
hardcoded main; --base overrides) — stage-aware on the Product: a
|
|
170
|
+
review/EP-* branch opens the Shape artifact-review PR
|
|
171
|
+
(delegates to gate open), any other Product branch uses the
|
|
122
172
|
code-task template
|
|
123
173
|
yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
|
|
124
|
-
yad checkpoint [--push] Commit the machine-written
|
|
174
|
+
yad checkpoint [--push] Commit the machine-written Build state on the Product
|
|
125
175
|
(trust-log/build-log/build-state) — plus any story
|
|
126
176
|
status: flip (→ in-build/shipped) backed by a build-log
|
|
127
177
|
ship — as one audit-trail chore(hub) commit; default
|
|
@@ -131,10 +181,22 @@ ${c.bold('Build helpers')}
|
|
|
131
181
|
(merged before ledger tracking), then carry its status: shipped
|
|
132
182
|
flip in the same commit (--merge-commit <sha>, --task <t> opt.);
|
|
133
183
|
one repo per run — re-run per --repo for a multi-repo story
|
|
134
|
-
yad tidy up [<epic>] [--push] Fold FINISHED
|
|
184
|
+
yad tidy up [<epic>] [--push] Fold FINISHED Build shards (a shipped story's
|
|
135
185
|
trust-log/build-log entries) back into the single folded
|
|
136
186
|
ledger, as one chore(hub) commit — the manual "pack it up"
|
|
137
187
|
for the shard files; a no-op when nothing is foldable
|
|
188
|
+
yad index [--json] Rebuild .sdlc/index.json — one summary line per work item,
|
|
189
|
+
derived from their own files; the default branch only, with no
|
|
190
|
+
override; on a verified Product CI writes it.
|
|
191
|
+
--json prints it, built live, and writes nothing
|
|
192
|
+
yad history [list] [--type t] [--theme x] [--thread EP-…] [--open|--done] [--json]
|
|
193
|
+
Every work item, open and shape-done (Build is not counted),
|
|
194
|
+
newest first — built live from their own files; writes nothing
|
|
195
|
+
yad history show <id> [--json] One work item: every step with its state and closing record,
|
|
196
|
+
and the approvals recorded for each review step
|
|
197
|
+
yad history search <text> [filters] [--json]
|
|
198
|
+
Match text in ids, titles, themes, types, repos, and in who
|
|
199
|
+
closed or merged a step, its PR and its commit
|
|
138
200
|
yad review trailer --repo <r> --pr <n> --body <text> Post the companion's 60-sec briefing to a code PR/MR
|
|
139
201
|
yad review context --repo <r> --pr <n> Print the grounding bundle for cards/chat
|
|
140
202
|
yad review walkthrough --repo <r> --pr <n> Bundle + ordered risk-tagged stops for the
|
|
@@ -144,7 +206,17 @@ ${c.bold('Build helpers')}
|
|
|
144
206
|
yad repo list Show connected repos (fresh / stale)
|
|
145
207
|
yad repo refresh [name] [--push] Re-pack a stale repo (a human decision). --push commits the
|
|
146
208
|
refreshed code-maps + registry as a chore(hub): sync code-context … [skip ci]
|
|
147
|
-
audit commit and pushes it to the
|
|
209
|
+
audit commit and pushes it to the Product default branch (--allow-branch to override)
|
|
210
|
+
yad risk-map check [repo] [--json] Warn where a code repo's .sdlc/risk-map (a risk level per
|
|
211
|
+
directory, no names) has gone stale — advisory, never blocks
|
|
212
|
+
yad risk-map draft [repo] Add an 'unset' line for every directory the map does not cover
|
|
213
|
+
(--dry-run prints it);
|
|
214
|
+
never changes a line (the yad-connect-repos skill classifies them)
|
|
215
|
+
yad codeowners check [repo] [--json] [--platform github|gitlab]
|
|
216
|
+
Warn where a code repo's CODEOWNERS looks stale: a line that
|
|
217
|
+
matches no file, a file the platform never reads or will not load,
|
|
218
|
+
a line yad cannot read; plus a hint about @logins with no recent
|
|
219
|
+
commit — advisory, never blocks, never writes the file
|
|
148
220
|
|
|
149
221
|
${c.bold('Feature threads (post-lock change management)')}
|
|
150
222
|
yad thread List every feature thread (genesis → changes → defects)
|
|
@@ -169,7 +241,7 @@ ${c.bold('Options')}
|
|
|
169
241
|
--repo <name> open-pr: target a registered repo by name
|
|
170
242
|
--base <branch> open-pr: override the PR/MR base — default is the repo's own default
|
|
171
243
|
branch (repos.json default_branch, else hub.json default_branch for a PR
|
|
172
|
-
on the
|
|
244
|
+
on the Product itself, else the platform, else origin/HEAD, else main); a
|
|
173
245
|
non-default base loses the AI first pass (warns, never blocks)
|
|
174
246
|
--epic <id> docs: target one epic's site (EP-<slug>)
|
|
175
247
|
--overview docs: target the project SDLC-overview site
|
|
@@ -189,12 +261,14 @@ ${c.bold('Options')}
|
|
|
189
261
|
|
|
190
262
|
${c.bold('Environment')}
|
|
191
263
|
YAD_NO_UPDATE_NOTIFIER=1 Silence the "update available" notice (also off in CI)
|
|
192
|
-
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
264
|
+
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
265
|
+
YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
|
|
193
266
|
|
|
194
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--
|
|
267
|
+
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread']);
|
|
195
268
|
|
|
196
269
|
function parseArgs(argv) {
|
|
197
270
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
271
|
+
try {
|
|
198
272
|
for (let i = 0; i < argv.length; i++) {
|
|
199
273
|
const a = argv[i];
|
|
200
274
|
if (a === '--fix') o.fix = true;
|
|
@@ -212,7 +286,11 @@ function parseArgs(argv) {
|
|
|
212
286
|
// positional. `o._[0]` is the command, already pushed by the time `--check` is seen in normal use.
|
|
213
287
|
else if (a === '--check') { const v = argv[i + 1]; o.check = (o._[0] === 'next' && v !== undefined && !v.startsWith('-')) ? argv[++i] : true; }
|
|
214
288
|
else if (a === '--all') o.all = true;
|
|
289
|
+
else if (a === '--open') o.open = true;
|
|
290
|
+
else if (a === '--done') o.done = true;
|
|
215
291
|
else if (a === '--undo') o.undo = true;
|
|
292
|
+
else if (a === '--debt') o.debt = true;
|
|
293
|
+
else if (a === '--stub') o.stub = true;
|
|
216
294
|
// setup profile flags (pre-answer the Step 0 interview, for CI/scripts)
|
|
217
295
|
else if (a === '--solo') o.solo = true;
|
|
218
296
|
else if (a === '--greenfield') o.greenfield = true;
|
|
@@ -234,11 +312,60 @@ function parseArgs(argv) {
|
|
|
234
312
|
else if (a.startsWith('--scope=')) o.scope = a.slice('--scope='.length);
|
|
235
313
|
else if (a === '-m' || a === '--message') o.message = takeValue(argv, ++i, a);
|
|
236
314
|
else if (VALUE_FLAGS.has(a)) o[a.replace(/^--/, '')] = takeValue(argv, ++i, a);
|
|
237
|
-
|
|
315
|
+
// `--flag=value` for the same flags. Without it the token matched nothing and fell through to the
|
|
316
|
+
// positionals, where it was silently ignored — and for `yad hook ledger-guard --format=cursor`
|
|
317
|
+
// that is not a cosmetic miss: the format goes undefined, the exit protocol is selected, and under
|
|
318
|
+
// Cursor an empty stdout on an allow BLOCKS every file write. A flag spelling that silently
|
|
319
|
+
// inverts a guard is worth accepting rather than quietly dropping.
|
|
320
|
+
else if (a.startsWith('--') && a.includes('=') && VALUE_FLAGS.has(a.slice(0, a.indexOf('=')))) {
|
|
321
|
+
const eq = a.indexOf('=');
|
|
322
|
+
const value = a.slice(eq + 1);
|
|
323
|
+
if (!value) throw new Error(`${a.slice(0, eq)} expects a value`);
|
|
324
|
+
o[a.slice(2, eq)] = value;
|
|
325
|
+
} else o._.push(a);
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
// The command read so far, so the error handler knows WHICH command failed — the parse did not finish.
|
|
329
|
+
e.parsedCmd = o._[0] ?? null;
|
|
330
|
+
// The words read so far, so a refusal names `history show`, not the default action (E1 review).
|
|
331
|
+
e.parsedWords = o._.slice(0, 2);
|
|
332
|
+
throw e;
|
|
238
333
|
}
|
|
239
334
|
return o;
|
|
240
335
|
}
|
|
241
336
|
|
|
337
|
+
// The command `main` is running, for the error handler (set once the arguments are read).
|
|
338
|
+
let runningCmd = null;
|
|
339
|
+
|
|
340
|
+
// The `command` a --json answer names: the command word, plus its action where it has one, as the
|
|
341
|
+
// user would type it (`gate status`, `history show`). An action left out is the one that ran
|
|
342
|
+
// (`yad skill` is `skill list`); a word that is not one of the command's actions is not added, so a
|
|
343
|
+
// refusal of `yad gate frobnicate` names `gate`, not a command that does not exist.
|
|
344
|
+
const ACTIONS = {
|
|
345
|
+
epic: { known: ['new'] },
|
|
346
|
+
foundation: { known: ['new', 'status'] },
|
|
347
|
+
skill: { known: ['list', 'bind', 'unbind'], default: 'list' },
|
|
348
|
+
gate: { known: ['open', 'sync', 'comments', 'status', 'repair', 'review', 'walkthrough', 'trailer', 'ci'] },
|
|
349
|
+
review: { known: ['trailer', 'context', 'chat', 'cards', 'walkthrough', 'nudge', 'reconcile'] },
|
|
350
|
+
tidy: { known: ['up'] },
|
|
351
|
+
history: { known: ['list', 'show', 'search'], default: 'list' },
|
|
352
|
+
repo: { known: ['list', 'refresh', 'sync'], default: 'list' },
|
|
353
|
+
'risk-map': { known: ['check', 'draft'], default: 'check' },
|
|
354
|
+
codeowners: { known: ['check'], default: 'check' },
|
|
355
|
+
docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
|
|
356
|
+
reconcile: { known: ['check', 'refresh', 'wire'] },
|
|
357
|
+
hook: { known: ['ledger-guard'] },
|
|
358
|
+
};
|
|
359
|
+
const ALWAYS_JSON = new Set(['gate review', 'gate walkthrough', 'review context', 'review chat', 'review cards', 'review walkthrough']);
|
|
360
|
+
export function commandName(words) {
|
|
361
|
+
const [cmd, action] = words;
|
|
362
|
+
if (!cmd) return null;
|
|
363
|
+
const a = ACTIONS[cmd];
|
|
364
|
+
if (!a) return cmd;
|
|
365
|
+
if (action === undefined) return a.default ? `${cmd} ${a.default}` : cmd;
|
|
366
|
+
return a.known.includes(action) ? `${cmd} ${action}` : cmd;
|
|
367
|
+
}
|
|
368
|
+
|
|
242
369
|
// A value flag must be followed by a token; erroring beats silently passing `undefined` downstream.
|
|
243
370
|
function takeValue(argv, i, flag) {
|
|
244
371
|
const v = argv[i];
|
|
@@ -247,179 +374,361 @@ function takeValue(argv, i, flag) {
|
|
|
247
374
|
}
|
|
248
375
|
|
|
249
376
|
async function main() {
|
|
377
|
+
// `--json` is read from the raw arguments too, so a refusal from the parser itself (a flag with no
|
|
378
|
+
// value) still answers as one object.
|
|
379
|
+
if (process.argv.slice(2).includes('--json')) beginJSON(null);
|
|
250
380
|
const o = parseArgs(process.argv.slice(2));
|
|
251
381
|
const cmd = o._[0];
|
|
252
|
-
|
|
253
|
-
|
|
382
|
+
runningCmd = cmd ?? null;
|
|
383
|
+
// The grounding bundles a skill parses are JSON with or without the flag, so they are always a
|
|
384
|
+
// --json run: one object on stdout, a refusal included, and every other line on stderr.
|
|
385
|
+
if (o.json || ALWAYS_JSON.has(commandName(o._))) beginJSON(commandName(o._));
|
|
386
|
+
if (o.version) return inJSON() ? emitJSON({}) : log(VERSION);
|
|
387
|
+
|
|
388
|
+
// THE HOT PATH, handled before anything heavy is loaded. `yad hook ledger-guard` runs inside the
|
|
389
|
+
// agent's tool loop on every file-editing call, and it needs `cli/hook.mjs` alone. Everything else
|
|
390
|
+
// lives behind the single dynamic import below, so the hook no longer pays for 29 modules it does
|
|
391
|
+
// not use — measured at ~30ms of module loading against ~14ms for what it actually needs.
|
|
392
|
+
//
|
|
393
|
+
// It sits above the help and the newer-shape warning deliberately: `--help` is not a hook call, and
|
|
394
|
+
// the warning is already excluded for `hook` below because its stderr is the channel a block reason
|
|
395
|
+
// reaches the model on.
|
|
396
|
+
if (cmd === 'hook') {
|
|
397
|
+
const [, action] = o._;
|
|
398
|
+
// Its stdout is already a protocol (Claude Code reads the exit code, Cursor a permission object),
|
|
399
|
+
// so it takes no --json (E1).
|
|
400
|
+
if (inJSON()) return refuse('yad hook takes no --json — its output is the protocol the agent reads', 'run it without --json');
|
|
401
|
+
if (action !== 'ledger-guard') return refuse(`unknown hook: ${action ?? '(none)'} (ledger-guard)`);
|
|
402
|
+
runLedgerGuardHook({ paths: o.path ? [o.path] : [], format: o.format });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Every other command. One await, once, for a process that is about to do real work anyway.
|
|
407
|
+
const commands = await import('./commands.mjs');
|
|
408
|
+
if (o.help || !cmd) {
|
|
409
|
+
const text = helpText(commands.seedableProfiles());
|
|
410
|
+
return inJSON() ? emitJSON({ help: stripAnsi(text) }) : log(text);
|
|
411
|
+
}
|
|
412
|
+
// A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
|
|
413
|
+
// shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
|
|
414
|
+
// where the command reports the same thing itself (doctor, migrate) or runs before a project exists.
|
|
415
|
+
if (!['hook', 'doctor', 'migrate', 'setup', 'report'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
|
|
254
416
|
|
|
255
417
|
const today = new Date().toISOString().slice(0, 10);
|
|
418
|
+
// What the command returned: under --json, a command that did not answer itself answers with it.
|
|
419
|
+
let result;
|
|
256
420
|
switch (cmd) {
|
|
257
421
|
case 'setup':
|
|
258
|
-
await runSetup(o.dir, {
|
|
422
|
+
result = await commands.runSetup(o.dir, {
|
|
259
423
|
today, force: o.force,
|
|
260
424
|
solo: o.solo, team: o.team, greenfield: o.greenfield, brownfield: o.brownfield,
|
|
261
425
|
monorepo: o.monorepo, separate: o.separate, tools: o.tools,
|
|
426
|
+
// The agent directories to install into, as a pre-answer for CI/scripts exactly like `--solo`
|
|
427
|
+
// above (E11). Until this flag existed, `ideTargets` was reachable only programmatically: a
|
|
428
|
+
// non-interactive run took `ask`'s default and had NO way to choose. That was survivable while
|
|
429
|
+
// the default was one directory; it stopped being survivable when the default became two,
|
|
430
|
+
// because a scripted setup would write a second full copy of the skills with no way to say no.
|
|
431
|
+
// `o['ide-targets']`, not `o.ideTargets`: VALUE_FLAGS strips the leading `--` and nothing else,
|
|
432
|
+
// so a hyphenated flag keeps its hyphen as the key (`--retro-ship` is read the same way below).
|
|
433
|
+
ideTargets: o['ide-targets'] === undefined ? undefined : String(o['ide-targets']).split(',').map((t) => t.trim()).filter(Boolean),
|
|
262
434
|
});
|
|
263
435
|
break;
|
|
264
436
|
case 'check':
|
|
265
|
-
await reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
437
|
+
result = await commands.reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
266
438
|
break;
|
|
267
439
|
case 'update':
|
|
268
|
-
await reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
440
|
+
result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
269
441
|
break;
|
|
270
442
|
case 'doctor':
|
|
271
|
-
await runDoctor(o.dir, { json: o.json });
|
|
443
|
+
result = await commands.runDoctor(o.dir, { json: o.json });
|
|
272
444
|
break;
|
|
273
445
|
case 'migrate':
|
|
274
|
-
await runMigrate(o.dir, { apply: o.apply, json: o.json });
|
|
275
|
-
break;
|
|
276
|
-
// Harness-invoked, not typed by a human: a tool-call payload arrives on stdin and the exit code
|
|
277
|
-
// is the verdict (0 allow, 2 deny). See cli/hook.mjs for the contract.
|
|
278
|
-
case 'hook': {
|
|
279
|
-
const [, action] = o._;
|
|
280
|
-
if (action !== 'ledger-guard') {
|
|
281
|
-
log(c.red(`unknown hook: ${action ?? '(none)'} (ledger-guard)`));
|
|
282
|
-
process.exitCode = 1;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
runLedgerGuardHook({ paths: o.path ? [o.path] : [] });
|
|
446
|
+
result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json });
|
|
286
447
|
break;
|
|
287
|
-
}
|
|
288
448
|
case 'report':
|
|
289
|
-
await runReport(o.dir, { message: o.message });
|
|
449
|
+
result = await commands.runReport(o.dir, { message: o.message });
|
|
290
450
|
break;
|
|
291
451
|
case 'usage':
|
|
292
|
-
runUsage(o.dir, {
|
|
452
|
+
result = commands.runUsage(o.dir, {
|
|
293
453
|
out: o.out, since: o.since, until: o.until, all: o.all, member: o.member,
|
|
294
454
|
format: o.format, repos: o.repos, json: o.json, today,
|
|
295
455
|
});
|
|
296
456
|
break;
|
|
297
457
|
case 'sync-status': {
|
|
298
458
|
const [, epic] = o._;
|
|
299
|
-
if (epic && !isValidEpicId(epic)) {
|
|
300
|
-
await syncStatuses(o.dir, { epic, dryRun: o.dryRun });
|
|
459
|
+
if (epic && !commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
460
|
+
result = await commands.syncStatuses(o.dir, { epic, dryRun: o.dryRun });
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case 'epic': {
|
|
464
|
+
const [, action, slug, ...extra] = o._;
|
|
465
|
+
// A stray word is refused rather than dropped: `--inherits epic architecture` takes only `epic` as
|
|
466
|
+
// the value, and silently seeding without `architecture` would carry less than the author asked.
|
|
467
|
+
if (action === 'new' && extra.length) {
|
|
468
|
+
refuse(`unexpected argument(s): ${extra.join(' ')} — a list takes commas, e.g. --inherits epic,architecture`); break;
|
|
469
|
+
}
|
|
470
|
+
if (action !== 'new') {
|
|
471
|
+
refuse(`unknown epic action: ${action ?? '(none)'} (new)`, `usage: yad epic new <slug> [--type feature|chore|change|defect|hotfix] [--profile ${commands.seedableProfiles().join('|')}] [--parent EP-<slug> --inherits <bases>]`);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
result = await commands.runEpicNew(o.dir, { slug, type: o.type, profile: o.profile, stub: o.stub, parent: o.parent, inherits: o.inherits, today, json: o.json });
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'foundation': {
|
|
478
|
+
const [, action] = o._;
|
|
479
|
+
if (action === 'status') { result = await commands.runFoundationStatus(o.dir, { json: o.json }); break; }
|
|
480
|
+
if (action !== 'new') {
|
|
481
|
+
refuse(`unknown foundation action: ${action ?? '(none)'} (new, status)`, 'usage: yad foundation new [--json] | yad foundation status [--json]');
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
result = await commands.runFoundationNew(o.dir, { today, json: o.json });
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'skill': {
|
|
488
|
+
const [, action, step, ...rest] = o._;
|
|
489
|
+
if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json }); break; }
|
|
490
|
+
if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest }); break; }
|
|
491
|
+
if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step }); break; }
|
|
492
|
+
refuse(`unknown skill action: ${action} (list, bind, unbind)`, 'usage: yad skill list [--json] | yad skill bind <step> <skill> [<skill> ...] | yad skill unbind <step>');
|
|
301
493
|
break;
|
|
302
494
|
}
|
|
303
495
|
case 'next': {
|
|
304
496
|
const [, epic] = o._;
|
|
305
497
|
// `--check` with no step is a malformed guard call — fail loudly rather than silently print.
|
|
306
|
-
if (o.check === true) {
|
|
307
|
-
await runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all, json: o.json });
|
|
498
|
+
if (o.check === true) { refuse('usage: yad next <epic> --check <step>'); break; }
|
|
499
|
+
result = await commands.runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all, json: o.json });
|
|
308
500
|
break;
|
|
309
501
|
}
|
|
310
|
-
case 'skip':
|
|
502
|
+
case 'skip':
|
|
503
|
+
case 'unskip':
|
|
504
|
+
case 'defer':
|
|
505
|
+
case 'undefer': {
|
|
506
|
+
const [verb, epic, step] = o._;
|
|
507
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid or missing epic id: ${epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
508
|
+
// A STORY id where the step goes names a whole Build lane (E39), with --repo naming the repo. No Shape
|
|
509
|
+
// step id ends in -S<n>, so the two never overlap. A lane is skipped, never deferred.
|
|
510
|
+
if (step && /-S\d+$/i.test(step)) {
|
|
511
|
+
if (verb === 'defer' || verb === 'undefer') {
|
|
512
|
+
refuse(`a Build lane is skipped whole, never deferred: yad skip ${epic} ${step} --repo <name> --reason "<why>"`); break;
|
|
513
|
+
}
|
|
514
|
+
if (o.debt) warn('--debt is not used on a Build lane: a lane is skipped whole, never owed back');
|
|
515
|
+
result = await commands.runLaneSkip(o.dir, { epic, story: step, repo: o.repo, reason: o.reason, undo: verb === 'unskip' || o.undo, today });
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
if (o.repo) warn(`--repo is only for a Build lane (yad ${verb} ${epic} <story> --repo <name>) — ignored for a Shape step`);
|
|
519
|
+
// `unskip` / `undefer` are the verbs E36 / E37 named; `--undo` is the spelling that shipped first and stays.
|
|
520
|
+
const runVerb = verb === 'defer' || verb === 'undefer' ? commands.runDefer : commands.runSkip;
|
|
521
|
+
result = await runVerb(o.dir, { epic, step, reason: o.reason, debt: o.debt, undo: verb.startsWith('un') || o.undo, today });
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'dial': {
|
|
525
|
+
// One word names a Shape author step (project-wide). Three name a Build lane step: epic, story, step.
|
|
526
|
+
const args = o._.slice(1);
|
|
527
|
+
if (args.length === 1) {
|
|
528
|
+
result = await commands.runDial(o.dir, { step: args[0], to: o.to, json: o.json });
|
|
529
|
+
} else if (args.length === 3) {
|
|
530
|
+
if (!commands.isValidEpicId(args[0])) { refuse(`invalid epic id: ${args[0]} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
531
|
+
result = await commands.runDial(o.dir, { epic: args[0], story: args[1], step: args[2], repo: o.repo, to: o.to, json: o.json });
|
|
532
|
+
} else {
|
|
533
|
+
refuse('usage: yad dial <step> [--to auto|human] | yad dial <epic> <story> --repo <name> <step> [--to auto|human]');
|
|
534
|
+
}
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case 'mode': {
|
|
538
|
+
if (o._.length > 2) { refuse(`unexpected argument(s): ${o._.slice(2).join(' ')}`); break; }
|
|
539
|
+
result = await commands.runMode(o.dir, { to: o._[1] ?? null, reason: o.reason, json: o.json, today });
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 'kill':
|
|
543
|
+
case 'unkill': {
|
|
544
|
+
if (o._.length > 1) { refuse(`unexpected argument(s): ${o._.slice(1).join(' ')}`); break; }
|
|
545
|
+
result = await commands.runKill(o.dir, { on: o._[0] === 'kill', reason: o.reason, json: o.json, today });
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'unblock': {
|
|
311
549
|
const [, epic, step] = o._;
|
|
312
|
-
if (!epic || !isValidEpicId(epic)) {
|
|
313
|
-
await
|
|
550
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid or missing epic id: ${epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
551
|
+
result = await commands.runUnblock(o.dir, { epic, step });
|
|
314
552
|
break;
|
|
315
553
|
}
|
|
316
554
|
case 'gate': {
|
|
317
555
|
const [, action, epic, artifact] = o._;
|
|
318
556
|
// `gate ci` takes no positionals — epic/artifact come from --branch (or a sweep of all PRs).
|
|
319
|
-
if (action === 'ci') { await gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
|
|
320
|
-
if (!epic) {
|
|
557
|
+
if (action === 'ci') { result = await commands.gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
|
|
558
|
+
if (!epic) { refuse('usage: yad gate <open|sync|comments|status|repair|review|walkthrough|trailer|ci> <epic> [artifact]'); break; }
|
|
321
559
|
// The epic id becomes a path segment under epics/ — reject anything but EP-<slug> outright.
|
|
322
|
-
if (!isValidEpicId(epic)) {
|
|
323
|
-
// In
|
|
560
|
+
if (!commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
561
|
+
// In verified mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
|
|
324
562
|
// advisory (reads the platform, prints status, writes nothing). The artifact status flip is
|
|
325
|
-
// CI's job at merge — never wired into the local gate.
|
|
326
|
-
if (action === 'open') await gateOpen(o.dir, { epic, artifact });
|
|
327
|
-
else if (action === 'sync') await gateSync(o.dir, { epic, artifact, today, number: o.pr, local: true });
|
|
328
|
-
else if (action === 'comments') await gateComments(o.dir, { epic, artifact, today });
|
|
329
|
-
else if (action === 'status') await gateStatus(o.dir, { epic });
|
|
330
|
-
else if (action === 'repair') await gateRepair(o.dir, { epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun });
|
|
331
|
-
else if (action === 'review') await gateReview(o.dir, { epic, artifact });
|
|
332
|
-
else if (action === 'walkthrough') await gateWalkthrough(o.dir, { epic, artifact });
|
|
333
|
-
else if (action === 'trailer') await gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
|
|
334
|
-
else {
|
|
563
|
+
// CI's job at merge — never wired into the local gate. Local mode keeps local writes.
|
|
564
|
+
if (action === 'open') result = await commands.gateOpen(o.dir, { epic, artifact, today });
|
|
565
|
+
else if (action === 'sync') result = await commands.gateSync(o.dir, { epic, artifact, today, number: o.pr, local: true });
|
|
566
|
+
else if (action === 'comments') result = await commands.gateComments(o.dir, { epic, artifact, today });
|
|
567
|
+
else if (action === 'status') result = await commands.gateStatus(o.dir, { epic });
|
|
568
|
+
else if (action === 'repair') result = await commands.gateRepair(o.dir, { epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, today });
|
|
569
|
+
else if (action === 'review') result = await commands.gateReview(o.dir, { epic, artifact });
|
|
570
|
+
else if (action === 'walkthrough') result = await commands.gateWalkthrough(o.dir, { epic, artifact });
|
|
571
|
+
else if (action === 'trailer') result = await commands.gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
|
|
572
|
+
else { refuse(`unknown gate action: ${action} (open|sync|comments|status|repair|review|walkthrough|trailer|ci)`); }
|
|
335
573
|
break;
|
|
336
574
|
}
|
|
337
575
|
case 'review': {
|
|
338
576
|
const [, action] = o._;
|
|
339
|
-
if (action === 'trailer') await reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
|
|
340
|
-
else if (action === 'context' || action === 'chat' || action === 'cards') await reviewContext(o.dir, { repo: o.repo, pr: o.pr });
|
|
341
|
-
else if (action === 'walkthrough') await reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
|
|
342
|
-
else if (action === 'nudge') await reviewNudge(o.dir, { repo: o.repo, pr: o.pr });
|
|
577
|
+
if (action === 'trailer') result = await commands.reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
|
|
578
|
+
else if (action === 'context' || action === 'chat' || action === 'cards') result = await commands.reviewContext(o.dir, { repo: o.repo, pr: o.pr });
|
|
579
|
+
else if (action === 'walkthrough') result = await commands.reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
|
|
580
|
+
else if (action === 'nudge') result = await commands.reviewNudge(o.dir, { repo: o.repo, pr: o.pr });
|
|
343
581
|
else if (action === 'reconcile') {
|
|
344
582
|
// The epic becomes a path segment under epics/ — reject anything but EP-<slug> (no `../` escape).
|
|
345
|
-
if (!o.epic || !isValidEpicId(o.epic)) {
|
|
346
|
-
await reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, pr: o.pr });
|
|
583
|
+
if (!o.epic || !commands.isValidEpicId(o.epic)) { refuse(`invalid or missing --epic: ${o.epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
584
|
+
result = await commands.reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, pr: o.pr });
|
|
347
585
|
}
|
|
348
|
-
else {
|
|
586
|
+
else { refuse('usage: yad review <trailer|context|walkthrough|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]'); }
|
|
349
587
|
break;
|
|
350
588
|
}
|
|
351
589
|
case 'commit':
|
|
352
|
-
await runCommit(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force });
|
|
590
|
+
result = await commands.runCommit(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force });
|
|
353
591
|
break;
|
|
354
592
|
case 'open-pr':
|
|
355
|
-
await runOpenPr(o.dir, { repo: o.repo, platform: o.platform, base: o.base, title: o.title || o.message, task: o.task, risk: o.risk, contractChange: o.contractChange });
|
|
593
|
+
result = await commands.runOpenPr(o.dir, { repo: o.repo, platform: o.platform, base: o.base, title: o.title || o.message, task: o.task, risk: o.risk, contractChange: o.contractChange });
|
|
356
594
|
break;
|
|
357
595
|
case 'ship':
|
|
358
|
-
await runShip(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, repo: o.repo, platform: o.platform, base: o.base, title: o.title, risk: o.risk });
|
|
596
|
+
result = await commands.runShip(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, repo: o.repo, platform: o.platform, base: o.base, title: o.title, risk: o.risk });
|
|
359
597
|
break;
|
|
360
598
|
case 'checkpoint': {
|
|
361
599
|
let retroShip;
|
|
362
600
|
if (o['retro-ship']) {
|
|
363
601
|
const [epic, story] = String(o['retro-ship']).split('/');
|
|
364
|
-
if (!epic || !isValidEpicId(epic)) {
|
|
602
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid --retro-ship: expected <epic>/<story> with epic EP-<slug> (got ${o['retro-ship']})`); break; }
|
|
365
603
|
// The story id becomes a path element (stories/<story>.md) — pin it to the id shape (and to its
|
|
366
604
|
// own epic) so a `..` or a slash can never traverse out of the epic's stories dir, and a typo'd
|
|
367
605
|
// cross-epic id is caught here rather than failing obscurely later.
|
|
368
|
-
if (!story || !/^EP-[a-z0-9-]+-S\d+$/.test(story) || !story.startsWith(`${epic}-S`)) {
|
|
606
|
+
if (!story || !/^EP-[a-z0-9-]+-S\d+$/.test(story) || !story.startsWith(`${epic}-S`)) { refuse(`invalid --retro-ship: expected <epic>/<story> with story <epic>-S<NN> (got ${o['retro-ship']})`); break; }
|
|
369
607
|
retroShip = { epic, story, repo: o.repo, task: o.task, mergeCommit: o['merge-commit'], today };
|
|
370
608
|
}
|
|
371
|
-
await runCheckpoint(o.dir, { push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, retroShip });
|
|
609
|
+
result = await commands.runCheckpoint(o.dir, { push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, retroShip });
|
|
372
610
|
break;
|
|
373
611
|
}
|
|
374
612
|
case 'tidy': {
|
|
375
613
|
const [, action, epic] = o._;
|
|
376
|
-
if (action !== 'up') {
|
|
377
|
-
|
|
614
|
+
if (action !== 'up') {
|
|
615
|
+
// A bare `yad tidy` is a request for the usage, not a mistake: exit 0.
|
|
616
|
+
if (action) refuse(`unknown tidy action: ${action} (up)`, 'usage: yad tidy up [<epic>] [--push] [--dry-run]');
|
|
617
|
+
else if (inJSON()) emitJSON({ usage: 'yad tidy up [<epic>] [--push] [--dry-run]' });
|
|
618
|
+
else log(`usage: yad tidy up [<epic>] [--push] [--dry-run]`);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
result = await commands.runTidy(o.dir, { epic: epic || o.epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun });
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'index':
|
|
625
|
+
result = await commands.runIndex(o.dir, { json: o.json });
|
|
626
|
+
break;
|
|
627
|
+
case 'history': {
|
|
628
|
+
const [, action, ...args] = o._;
|
|
629
|
+
// Every flag that is not history's own is refused, not ignored: an ignored `--since` would read as
|
|
630
|
+
// a filter that was applied. Read from the RAW arguments, as typed — `parseArgs` turns a flag it
|
|
631
|
+
// does not know (`--limit`, a typo) into a plain word, which `search` would take as text, and it
|
|
632
|
+
// stores `--preview` under another name. A word that starts with `--`, or a one-letter flag such
|
|
633
|
+
// as `-m`, is a flag; a value (`--type chore`) never starts with `-`, because `parseArgs` refuses it.
|
|
634
|
+
const HISTORY_OWN = new Set(commands.HISTORY_FLAGS);
|
|
635
|
+
const unknownFlags = [...new Set(process.argv.slice(2)
|
|
636
|
+
.filter((t) => /^--./.test(t) || /^-[A-Za-z]$/.test(t))
|
|
637
|
+
.map((t) => t.split('=')[0])
|
|
638
|
+
.filter((f) => !HISTORY_OWN.has(f)))];
|
|
639
|
+
result = await commands.runHistory(o.dir, {
|
|
640
|
+
action: action || 'list', args, json: !!o.json, unknownFlags,
|
|
641
|
+
type: o.type ?? null, theme: o.theme ?? null, thread: o.thread ?? null, open: !!o.open, done: !!o.done,
|
|
642
|
+
});
|
|
378
643
|
break;
|
|
379
644
|
}
|
|
380
645
|
case 'repo': {
|
|
381
646
|
const [, action, name] = o._;
|
|
382
|
-
await runRepo(o.dir, { action: action || 'list', name, today, push: o.push, allowBranch: o.allowBranch });
|
|
647
|
+
result = await commands.runRepo(o.dir, { action: action || 'list', name, today, push: o.push, allowBranch: o.allowBranch });
|
|
383
648
|
break;
|
|
384
649
|
}
|
|
385
|
-
case '
|
|
386
|
-
const [, action,
|
|
387
|
-
await
|
|
650
|
+
case 'risk-map': {
|
|
651
|
+
const [, action, name] = o._;
|
|
652
|
+
result = await commands.runRiskMap(o.dir, { action: action || 'check', name, json: o.json, dryRun: o.dryRun });
|
|
388
653
|
break;
|
|
389
654
|
}
|
|
655
|
+
case 'codeowners': {
|
|
656
|
+
const [, action, name] = o._;
|
|
657
|
+
// `--write` was dropped (E69); wherever it is typed, it is refused with the reason — never ignored.
|
|
658
|
+
result = await commands.runCodeowners(o.dir, { action: action || 'check', name, json: o.json, platform: o.platform, write: o._.some((a) => a === '--write' || a.startsWith('--write=')) });
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case 'roster':
|
|
662
|
+
// Removed in E62. Kept as a word for one major so a script or a habit gets told where it went
|
|
663
|
+
// instead of "unknown command".
|
|
664
|
+
refuse('yad roster was removed — yadflow keeps no list of people',
|
|
665
|
+
'A gate needs one approval (not the author\'s own) from anyone with access to the repo; the platform records who approved. '
|
|
666
|
+
+ 'Request reviewers on the PR itself. Keep an existing `roster` in hub.json until every review with older approvals is closed: '
|
|
667
|
+
+ 'it decides nothing, but the first sync uses its name → login pairs to recognise those approvals.');
|
|
668
|
+
break;
|
|
390
669
|
case 'docs': {
|
|
391
670
|
const [, action] = o._;
|
|
392
|
-
if (o.epic && !isValidEpicId(o.epic)) {
|
|
671
|
+
if (o.epic && !commands.isValidEpicId(o.epic)) { refuse(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
393
672
|
const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
|
|
394
|
-
await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
673
|
+
result = await commands.runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
395
674
|
break;
|
|
396
675
|
}
|
|
397
676
|
case 'thread': {
|
|
398
677
|
const [, epic] = o._;
|
|
399
|
-
if (epic && !isValidEpicId(epic)) {
|
|
400
|
-
await runThread(o.dir, { epic, json: o.json });
|
|
678
|
+
if (epic && !commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
679
|
+
result = await commands.runThread(o.dir, { epic, json: o.json });
|
|
401
680
|
break;
|
|
402
681
|
}
|
|
403
682
|
case 'reconcile': {
|
|
404
683
|
const [, action] = o._;
|
|
405
684
|
const thread = o.epic || o.thread || null;
|
|
406
|
-
if (thread && !isValidEpicId(thread)) {
|
|
685
|
+
if (thread && !commands.isValidEpicId(thread)) { refuse(`invalid epic id: ${thread} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
407
686
|
const act = action || (o.wire ? 'wire' : o.refresh ? 'refresh' : 'check');
|
|
408
687
|
if (!['check', 'refresh', 'wire'].includes(act)) {
|
|
409
|
-
|
|
688
|
+
refuse(`unknown reconcile action: ${act} (check|refresh|wire)`); break;
|
|
410
689
|
}
|
|
411
|
-
await runReconcile(o.dir, { action: act, thread });
|
|
690
|
+
result = await commands.runReconcile(o.dir, { action: act, thread });
|
|
412
691
|
break;
|
|
413
692
|
}
|
|
414
693
|
default:
|
|
694
|
+
if (inJSON()) { refuse(`unknown command: ${cmd}`, '`yad --help` lists the commands'); break; }
|
|
415
695
|
log(c.red(`unknown command: ${cmd}`));
|
|
416
|
-
log(
|
|
696
|
+
log(helpText(commands.seedableProfiles()));
|
|
417
697
|
process.exitCode = 1;
|
|
418
698
|
}
|
|
699
|
+
return result;
|
|
419
700
|
}
|
|
420
701
|
|
|
421
702
|
main()
|
|
703
|
+
// A command that finished a --json run without answering is a yadflow bug. Stdout still gets one
|
|
704
|
+
// object saying so, never an empty stream a parser chokes on — and never a guessed `ok: true`.
|
|
705
|
+
.then((result) => {
|
|
706
|
+
if (!inJSON() || jsonEmitted()) return;
|
|
707
|
+
// A refusal said in prose (`fail` + its `hint`) becomes the JSON refusal.
|
|
708
|
+
// What the command DID rides along: a push that failed after the commit landed must not read as
|
|
709
|
+
// "nothing happened" (E1 review) — `ship` answers `committed: true` beside the refusal.
|
|
710
|
+
const said = process.exitCode ? jsonFailure() : null;
|
|
711
|
+
if (said) {
|
|
712
|
+
// Never a key the envelope or the refusal owns: a clash would throw inside the answer and lose the
|
|
713
|
+
// real refusal, and a `json` key is an option of `refuse`, not data.
|
|
714
|
+
const owned = new Set([...ENVELOPE_KEYS, 'ok', 'error', 'code', 'hint', 'warnings', 'json']);
|
|
715
|
+
const did = Object.fromEntries(Object.entries(isPlainObject(result) ? result : {}).filter(([k]) => !owned.has(k)));
|
|
716
|
+
return refuse(said.error, said.hint, { code: said.code, ...did });
|
|
717
|
+
}
|
|
718
|
+
// Otherwise the command's result object IS the answer — `ok` follows the exit code, as everywhere.
|
|
719
|
+
if (isPlainObject(result)) return emitJSON({ ...result, ok: !process.exitCode });
|
|
720
|
+
refuse(`yad ${runningCmd ?? ''} gave no JSON answer — this is a yadflow bug`.replace(' ', ' '), 'run it without --json to see what it printed, and report it with `yad report`');
|
|
721
|
+
})
|
|
422
722
|
.catch(async (err) => {
|
|
723
|
+
// Under --json every failure is still one object on stdout (E1) — a refusal from the parser, a
|
|
724
|
+
// thrown YadError, a bug. The `code` is the `YAD-` code README "Troubleshooting" is keyed on.
|
|
725
|
+
// If the command had already answered, stdout holds its object, and the failure goes to stderr.
|
|
726
|
+
if (inJSON() && !jsonEmitted()) {
|
|
727
|
+
if (err?.parsedCmd !== undefined && runningCmd === null) beginJSON(commandName(err.parsedWords ?? []));
|
|
728
|
+
const hint = err?.hint || (/expects a value$/.test(String(err?.message)) ? '`yad --help` lists the flags of each command' : null);
|
|
729
|
+
refuse(String(err?.message || err), hint, { code: err?.code && /^YAD-/.test(err.code) ? err.code : null });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
423
732
|
const code = err?.code && /^YAD-/.test(err.code) ? ` [${err.code}]` : '';
|
|
424
733
|
log(c.red(`\nyad failed${code}: ${err?.message || err}`));
|
|
425
734
|
if (err?.hint) log(c.yellow(` → ${err.hint}`));
|
|
@@ -429,11 +738,12 @@ main()
|
|
|
429
738
|
// YAD_NO_REPORT. `report` failing on its own would land here, so never re-offer for it.
|
|
430
739
|
// Require a TTY on BOTH ends: stdout for the message, stdin so the y/N prompt can be answered
|
|
431
740
|
// (a TTY stdout with piped/closed stdin would otherwise hang on readline).
|
|
432
|
-
const offerReport = process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
|
|
741
|
+
const offerReport = !inJSON() && process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
|
|
433
742
|
&& !process.env.YAD_NO_REPORT && process.argv[2] !== 'report';
|
|
434
743
|
if (offerReport) {
|
|
435
744
|
try {
|
|
436
745
|
if (await askYesNo('\nReport this failure to the yadflow team?', false)) {
|
|
746
|
+
const { runReport } = await import('./commands.mjs');
|
|
437
747
|
await runReport(process.cwd(), { error: err });
|
|
438
748
|
}
|
|
439
749
|
} catch { /* reporting is best-effort — never mask the original failure */ }
|
|
@@ -450,7 +760,10 @@ main()
|
|
|
450
760
|
// block reason travels on — an update banner there would land in front of a model.
|
|
451
761
|
// Resolved the way main() resolves it, NOT from argv[2]: that is the first raw argument, so
|
|
452
762
|
// `yad --dir <path> hook ledger-guard` puts `--dir` there and the banner slips through.
|
|
453
|
-
if (parseArgs(process.argv.slice(2))._[0] !== 'hook')
|
|
763
|
+
if (parseArgs(process.argv.slice(2))._[0] !== 'hook') {
|
|
764
|
+
const { maybeNotifyUpdate } = await import('./commands.mjs');
|
|
765
|
+
await maybeNotifyUpdate();
|
|
766
|
+
}
|
|
454
767
|
} catch { /* the notice is never worth failing or hanging a command over */ } finally {
|
|
455
768
|
closePrompts();
|
|
456
769
|
}
|