yadflow 4.0.0-next.3 → 4.0.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 +699 -0
- package/README.md +85 -22
- package/bin/commands.mjs +3 -1
- package/bin/yad.mjs +183 -29
- package/cli/capture.mjs +4 -2
- package/cli/checkpoint.mjs +10 -10
- package/cli/claims.mjs +4 -4
- package/cli/commit.mjs +48 -12
- package/cli/docs.mjs +15 -13
- package/cli/doctor.mjs +557 -141
- package/cli/epic-state.mjs +41 -18
- package/cli/errors.mjs +3 -2
- package/cli/find-product.mjs +197 -0
- package/cli/fold.mjs +4 -4
- package/cli/gate-local.mjs +13 -13
- package/cli/gate.mjs +111 -103
- package/cli/history.mjs +13 -13
- package/cli/hook.mjs +201 -30
- package/cli/index-command.mjs +6 -6
- package/cli/lib.mjs +91 -29
- package/cli/manifest.mjs +171 -104
- package/cli/migrate.mjs +208 -17
- package/cli/mode.mjs +22 -22
- package/cli/next.mjs +6 -6
- package/cli/openpr.mjs +32 -24
- package/cli/people.mjs +22 -10
- package/cli/plan.mjs +718 -45
- package/cli/platform.mjs +13 -7
- package/cli/product-index.mjs +4 -4
- package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
- package/cli/protection.mjs +19 -9
- package/cli/reconcile.mjs +98 -23
- package/cli/repo-publish.mjs +22 -16
- package/cli/repo.mjs +128 -33
- package/cli/report.mjs +4 -2
- package/cli/review.mjs +26 -19
- package/cli/setup.mjs +105 -36
- package/cli/ship.mjs +1 -1
- package/cli/skip.mjs +4 -4
- package/cli/tidy.mjs +7 -7
- package/cli/update-commit.mjs +22 -19
- package/cli/workspace.mjs +495 -0
- package/package.json +7 -4
- package/skills/sdlc/config.yaml +14 -13
- package/skills/sdlc/module-help.csv +8 -8
- package/skills/yad-analysis/SKILL.md +6 -4
- package/skills/yad-architecture/SKILL.md +4 -3
- package/skills/yad-backfill/SKILL.md +11 -5
- package/skills/yad-backfill/references/backfill.md +33 -3
- package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
- package/skills/yad-change/SKILL.md +1 -1
- package/skills/yad-checks/SKILL.md +48 -27
- package/skills/yad-checks/references/check-gates.md +213 -53
- package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
- package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
- package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
- package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
- package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
- package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
- package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
- package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
- package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
- package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
- package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
- package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
- package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
- package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
- package/skills/yad-commit/SKILL.md +18 -7
- package/skills/yad-connect-design/SKILL.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +1 -1
- package/skills/yad-connect-docs/SKILL.md +16 -11
- package/skills/yad-connect-docs/references/docs-registry.md +8 -8
- package/skills/yad-connect-learning/SKILL.md +1 -1
- package/skills/yad-connect-learning/references/learning-registry.md +1 -1
- package/skills/yad-connect-repos/SKILL.md +31 -8
- package/skills/yad-connect-repos/references/code-context.md +1 -1
- package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
- package/skills/yad-connect-repos/references/repos-registry.md +1 -1
- package/skills/yad-connect-testing/SKILL.md +1 -1
- package/skills/yad-connect-testing/references/testing-registry.md +1 -1
- package/skills/yad-discovery/SKILL.md +2 -2
- package/skills/yad-discovery/references/discovery-schema.md +1 -1
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-docs/references/data-mapping.md +1 -1
- package/skills/yad-docs/templates/app/package-lock.json +8 -8
- package/skills/yad-docs/templates/app/package.json +1 -1
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
- package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
- package/skills/yad-engineer-review/SKILL.md +2 -2
- package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
- package/skills/yad-epic/SKILL.md +6 -4
- package/skills/yad-epic/references/state-schema.md +13 -12
- package/skills/yad-open-pr/SKILL.md +1 -1
- package/skills/yad-pair-review/SKILL.md +5 -5
- package/skills/yad-pair-review/references/session-state.md +2 -2
- package/skills/yad-pr-template/SKILL.md +14 -12
- package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
- package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
- package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
- package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
- package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
- package/skills/yad-report/SKILL.md +1 -1
- package/skills/yad-review-companion/SKILL.md +3 -3
- package/skills/yad-review-gate/SKILL.md +22 -23
- package/skills/yad-review-gate/references/gating.md +13 -13
- package/skills/yad-run/SKILL.md +2 -2
- package/skills/yad-run/references/run-loop.md +2 -2
- package/skills/yad-spec/SKILL.md +2 -1
- package/skills/yad-spec/references/spec-handoff.md +19 -10
- package/skills/yad-stories/SKILL.md +2 -2
- package/skills/yad-sync-repos/SKILL.md +19 -3
- package/skills/yad-test-cases/SKILL.md +2 -2
- package/skills/yad-ui/SKILL.md +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
- package/skills/yad-checks/templates/hooks/yad-capture.sh +0 -53
|
@@ -22,7 +22,7 @@ trigger is a parameter, not a hardcoded human.
|
|
|
22
22
|
- Operate on one epic: `{project-root}/epics/EP-<slug>/`. The Product level is the exception:
|
|
23
23
|
`EP-foundation` lives at `{project-root}/foundation/` (its `.sdlc/` and `reviews/` are there).
|
|
24
24
|
- State files: `.sdlc/state.json`, `.sdlc/approvals.json`, `.sdlc/comments.json`, and (when the verified ledger is
|
|
25
|
-
used) `.sdlc/hub-prs.json
|
|
25
|
+
used) `.sdlc/product-prs.json` (written together with its older name `hub-prs.json`). Review records: `reviews/`.
|
|
26
26
|
- The artifact base name drops the extension (`epic.md` → `epic`; story `stories/...S01.md` → `stories-S01`).
|
|
27
27
|
|
|
28
28
|
## Inputs
|
|
@@ -32,7 +32,7 @@ trigger is a parameter, not a hardcoded human.
|
|
|
32
32
|
`comment` / `approve` / `advance` are the `yad gate comment|approve|advance` commands (E112).
|
|
33
33
|
- For `comment` / `approve`: the reviewer's platform login (or the name they give, when there is no
|
|
34
34
|
platform). No role and no domain. Ask if not provided.
|
|
35
|
-
- `sync` needs no reviewer input — it reads the platform PR/MR review state (via `yad-
|
|
35
|
+
- `sync` needs no reviewer input — it reads the platform PR/MR review state (via `yad-product-bridge`).
|
|
36
36
|
|
|
37
37
|
## On Activation
|
|
38
38
|
|
|
@@ -54,9 +54,8 @@ roles — there are no roles, and yadflow keeps no list of people:
|
|
|
54
54
|
(`short`); never block on it. Why: the count of people errs high in the normal case. A commit is
|
|
55
55
|
counted by its git name, an approval by its platform login, and yadflow never joins the two without
|
|
56
56
|
exact evidence — so a two-person team can read as four. At four the cap lowers nothing, and an enforced
|
|
57
|
-
contract gate would ask three approvals of a team with one person who is not the author.
|
|
58
|
-
|
|
59
|
-
is accurate.
|
|
57
|
+
contract gate would ask three approvals of a team with one person who is not the author. Enforcing the
|
|
58
|
+
capped count is parked (roadmap E108) until the count is accurate.
|
|
60
59
|
- **Relay a warning line.** Under a team gate that has not passed, `yad gate status` and `yad gate sync`
|
|
61
60
|
print `! may not be met: …` (today's one required approval may have nobody but the author to give
|
|
62
61
|
it) or `! if the risk step were enforced: …` (a what-if about the extra approvals) when the count of
|
|
@@ -66,7 +65,7 @@ roles — there are no roles, and yadflow keeps no list of people:
|
|
|
66
65
|
one person may work on the Product (E74). Relay it as a suggestion to run `yad mode team`; never switch
|
|
67
66
|
the mode yourself, and never read it as a fault.
|
|
68
67
|
- **When the people cannot be counted** (`active: null` — for example Product CI, which checks out only
|
|
69
|
-
the
|
|
68
|
+
the Product, so the connected repos are not on disk): **no cap is computed or shown**, and the base holds as
|
|
70
69
|
always. `yad gate status` and `yad gate sync` print the arithmetic — read it from there rather than
|
|
71
70
|
recomputing it.
|
|
72
71
|
- **Touched domains** only name and label the review; they add no approvals. For a step with a risk
|
|
@@ -78,7 +77,7 @@ and nothing else.
|
|
|
78
77
|
|
|
79
78
|
### Step 2 — Dispatch on `action`
|
|
80
79
|
|
|
81
|
-
> **Check the mode first — in verified mode you write nothing to the ledger.** Read `.sdlc/hub.json
|
|
80
|
+
> **Check the mode first — in verified mode you write nothing to the ledger.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
|
|
82
81
|
> **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present. Under the verified ledger
|
|
83
82
|
> the ledger is CI-owned — `ledger-guard` rejects any non-bot commit touching
|
|
84
83
|
> `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or `epics/*/reviews/*.md` (and the
|
|
@@ -117,14 +116,14 @@ transition at merge.
|
|
|
117
116
|
|
|
118
117
|
Do not advance.
|
|
119
118
|
|
|
120
|
-
If `.sdlc/hub.json` has a non-null `platform` and `gh`/`glab` is authenticated, **`yad gate open` also
|
|
121
|
-
opens the review PR/MR on the Product** (the recipe is `yad-
|
|
119
|
+
If `.sdlc/product.json` (`.sdlc/hub.json` on an older Product) has a non-null `platform` and `gh`/`glab` is authenticated, **`yad gate open` also
|
|
120
|
+
opens the review PR/MR on the Product** (the recipe is `yad-product-bridge action: open`; do not open a
|
|
122
121
|
second one), and you report the URL. This holds for a verified ledger and for a local one. The PR
|
|
123
122
|
requests **no reviewers**; the command prints
|
|
124
123
|
`no reviewers were requested — ask them on the PR itself`, so tell the author to ask people on the PR.
|
|
125
|
-
The PR is recorded in `epics/<epic>/.sdlc/
|
|
124
|
+
The PR is recorded in `epics/<epic>/.sdlc/product-prs.json` and `hub-prs.json`, same bytes (`{step, artifact, platform, number, url, branch, lastSyncedAt}`):
|
|
126
125
|
**by CI at merge** in verified mode (`ledger: "verified"`, or, before `yad migrate`, `bridge_enabled: true` /
|
|
127
|
-
legacy `bridge: true` — `.sdlc/hub.json` is the only source the CLI reads, see `isVerifiedLedger` in
|
|
126
|
+
legacy `bridge: true` — `.sdlc/product.json` (else `.sdlc/hub.json`) is the only source the CLI reads, see `isVerifiedLedger` in
|
|
128
127
|
`cli/manifest.mjs`), and by `yad gate open` itself on a local ledger. Never write it by hand (see `sync`
|
|
129
128
|
below). With no platform, or when the PR cannot be opened on a local ledger, the step is still
|
|
130
129
|
`in_review` locally — no error. (In verified mode a failed open writes nothing; open the PR by hand and
|
|
@@ -173,7 +172,7 @@ replaced. This is the **no-platform** path (E112): with a platform the approvals
|
|
|
173
172
|
command refuses and names `yad gate sync`; on a verified ledger CI writes them at merge.
|
|
174
173
|
`engagement` records whether the approval came through the [Review Companion](../yad-review-companion/SKILL.md)
|
|
175
174
|
(a real trailer/cards/chat session = `verified`) or as a bare click (`none`). It is soft by default
|
|
176
|
-
(both count; a bare approve draws a friendly nudge) and only gates when `
|
|
175
|
+
(both count; a bare approve draws a friendly nudge) and only gates when `review.requireEngagement` (Product settings)
|
|
177
176
|
is on — see `references/gating.md`. The signal is gameable by design ("visible, not impossible").
|
|
178
177
|
Then write/refresh `reviews/<artifact-base>--<YYYY-MM-DD>--approved.md` as a **named record** — the command
|
|
179
178
|
writes the ledger only, never this file, so rewrite it after every `yad gate approve`. It has three
|
|
@@ -200,18 +199,18 @@ The command prints the verdict (Step 3) itself. Recording an approval does NOT a
|
|
|
200
199
|
separate, explicit act (`advance`), the way approving a PR never merges it.
|
|
201
200
|
|
|
202
201
|
**`sync`** — (the platform bridge input path) Pull the Product review PR/MR's review state into the ledger,
|
|
203
|
-
then re-evaluate the rule (Step 3). Read the PR for this step from `.sdlc/hub-prs.json` and use
|
|
204
|
-
`yad-
|
|
202
|
+
then re-evaluate the rule (Step 3). Read the PR for this step from `.sdlc/product-prs.json` (`hub-prs.json` when that is absent) and use
|
|
203
|
+
`yad-product-bridge`'s read recipes (`../yad-product-bridge/references/bridge.md`) to fetch reviews + comments
|
|
205
204
|
via the local user's `gh`/`glab`. For each:
|
|
206
205
|
- the platform `login` is the name written — `approver` / `commenter` is the login itself. There is no
|
|
207
|
-
lookup and no role (see `../yad-
|
|
206
|
+
lookup and no role (see `../yad-product-bridge/references/login-roster.md`);
|
|
208
207
|
- an `APPROVED` review / MR approval → append an `approved` record to `approvals.json` tagged
|
|
209
208
|
`"source": "bridge"`; a `CHANGES_REQUESTED` review or an unresolved thread → write to
|
|
210
209
|
`reviews/<artifact-base>--<YYYY-MM-DD>--comments.md` + `comments.json` (never an approval). A resolved
|
|
211
210
|
thread, and a companion comment marked `<!-- yad:noblock -->`, is not written.
|
|
212
211
|
**Idempotent:** upsert bridge approvals by `(step, approver)` — one record per person. An older record
|
|
213
212
|
that still carries `role`/`domain` and names a person by their old roster name is recognised as
|
|
214
|
-
`../yad-
|
|
213
|
+
`../yad-product-bridge/references/login-roster.md` → "Older records" describes, and replaced by one
|
|
215
214
|
login-named record that keeps its fingerprint. Every sync write (`yad gate sync`, `yad gate ci`) also
|
|
216
215
|
records the login on the older records the roster can place for certain, on every step (same reference → "Recording the login on older records").
|
|
217
216
|
A bridge approval records the platform's evidence — `approvedAt`, and on GitHub `commit`, `url` and
|
|
@@ -223,7 +222,7 @@ it. **Manual approvals (no `source` tag) are never touched.** For the architectu
|
|
|
223
222
|
fingerprint a bridge approval is bound to is the contract surface, so a re-lock makes every approval of
|
|
224
223
|
the old surface stale: it stays on disk and no longer counts (re-lock invalidates platform approvals
|
|
225
224
|
too). Then refresh the `approved.md`
|
|
226
|
-
record, set the PR ledger's `lastSyncedAt`, and **re-evaluate Step 3**. **Never hand-write either PR-ledger file.** It lives under two names
|
|
225
|
+
record, set the PR ledger's `lastSyncedAt`, and **re-evaluate Step 3**. **Never hand-write either PR-ledger file.** It lives under two names until v5 — `product-prs.json` (read first) and `hub-prs.json` — and the engine writes both together. Writing one leaves the pair disagreeing, and then every yad command refuses (YAD-STATE-008) until `yad migrate --apply` settles it. Use `yad gate`, which keeps them in step. Under the PR-driven CLI (`yad
|
|
227
226
|
gate sync`), `sync` advances the step when Step 3 passes on a **merged**, fully-resolved, approved PR
|
|
228
227
|
(the merge is the human act); otherwise it records state and holds the step `in_review`.
|
|
229
228
|
|
|
@@ -239,7 +238,7 @@ The step may advance **iff ALL hold**:
|
|
|
239
238
|
the base holds. Report the shortfall in the record; do not hold the step on it.
|
|
240
239
|
This matches `gatePredicate`, which returns `rule: "count"`, the count as `gateRule`/`have`/`short`,
|
|
241
240
|
`active`, and `cap` (null when `active` is null), and never puts the risk step in `missing`.
|
|
242
|
-
`active: null` means no source could be read, never "nobody". With `
|
|
241
|
+
`active: null` means no source could be read, never "nobody". With `review.requireEngagement` (Product settings)
|
|
243
242
|
on, only `verified` approvals count.
|
|
244
243
|
2. The artifact has not changed since the latest approval round (no newer authored edit than the
|
|
245
244
|
newest `approved` record). If it changed, approvals are stale → return to `comment`. For the
|
|
@@ -296,7 +295,7 @@ records this skill describes. The skill's
|
|
|
296
295
|
job is the human half: presenting the artifact, helping the owner address comments, and narrating the
|
|
297
296
|
gate. Local `yad gate sync` is advisory in verified mode (reads the platform, prints status, writes
|
|
298
297
|
nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it, and the
|
|
299
|
-
`hooks/ledger-guard.
|
|
298
|
+
`hooks/ledger-guard.mjs` harness hook refuses an agent the edit up front, naming `yad gate open`
|
|
300
299
|
instead — see `yad-checks`). The single
|
|
301
300
|
exception is an epic's **seed** — no CI path can create a ledger, so a brand-new epic's `.sdlc/` rides
|
|
302
301
|
its **first** review PR/MR, cut from the authoring branch (creation, not mutation, #162).
|
|
@@ -312,7 +311,7 @@ PR/MR are recorded and bound to the new content. `yad gate status` then shows th
|
|
|
312
311
|
approvals are live against what is in the file today, and how many were revoked — instead of a `done`
|
|
313
312
|
step whose approvals all belong to the version before the edit.
|
|
314
313
|
|
|
315
|
-
The flow is **merge-driven** (wired by `yad-
|
|
314
|
+
The flow is **merge-driven** (wired by `yad-product-bridge` `wire`): during review CI writes nothing — the
|
|
316
315
|
platform PR/MR is the source of truth (native approvals + threads), and CI never touches the review
|
|
317
316
|
branch (so an in-flight approval is never dismissed and required checks never strand). On the human
|
|
318
317
|
**merge** CI re-reads approvals, advances the step, and flips the artifact `status:` on the **default
|
|
@@ -323,8 +322,8 @@ write path.
|
|
|
323
322
|
### Hard rules (build plan §1, §5)
|
|
324
323
|
- **The merge click is the human approval act.** A Shape step advances only when a human merges the
|
|
325
324
|
approved, fully-resolved review PR — or, on a Product with no platform, when a person runs
|
|
326
|
-
`yad gate advance` — there is no machine-driven advance. A
|
|
327
|
-
|
|
325
|
+
`yad gate advance` — there is no machine-driven advance. A review gate is never `advance: auto`
|
|
326
|
+
(`yad dial` refuses it); refuse such a request.
|
|
328
327
|
- **Approvals are revoked when the reviewed artifact changes.** `sync` re-hashes the artifact (the locked
|
|
329
328
|
contract surface for architecture; every other file without its frontmatter `status:` line, which the
|
|
330
329
|
gate and Build rewrite after review) and drops any approval bound to a stale hash, so a reviewer must
|
|
@@ -338,4 +337,4 @@ write path.
|
|
|
338
337
|
|
|
339
338
|
## Reference
|
|
340
339
|
- Gating details and worked example: `references/gating.md`.
|
|
341
|
-
- The platform PR/MR bridge (`open`/`sync` mechanics, read recipes, login attribution): `../yad-
|
|
340
|
+
- The platform PR/MR bridge (`open`/`sync` mechanics, read recipes, login attribution): `../yad-product-bridge/SKILL.md`.
|
|
@@ -55,8 +55,8 @@ the risk step.
|
|
|
55
55
|
counted by its git name, an approval by its platform login, and yadflow never joins the two without
|
|
56
56
|
exact evidence. So an ordinary two-person team can read as **four**. At four the cap lowers nothing,
|
|
57
57
|
and an enforced contract gate would ask three approvals of a team with one person who is not the author
|
|
58
|
-
— a gate it could never pass.
|
|
59
|
-
|
|
58
|
+
— a gate it could never pass. Enforcing the capped count, with a way out for a gate that cannot be
|
|
59
|
+
met, is roadmap row E108, which is parked. Until then the count is advice only.
|
|
60
60
|
|
|
61
61
|
**When a gate may not pass (E73).** `yad gate status` and `yad gate sync` print a warning line under a
|
|
62
62
|
team review gate that has not passed yet, when the count of people suggests the gate may not pass. It is a
|
|
@@ -116,7 +116,7 @@ people, because the connected repos are not on disk there.
|
|
|
116
116
|
|
|
117
117
|
**When the people cannot be counted** (`active: null` — a source could not be read), **no cap is
|
|
118
118
|
computed or shown**, and the base holds as always. The engine never guesses a small number from an
|
|
119
|
-
unknown. Product CI is usually this case: it checks out only the
|
|
119
|
+
unknown. Product CI is usually this case: it checks out only the Product, so on a Product with connected
|
|
120
120
|
repos the repos are not on disk.
|
|
121
121
|
|
|
122
122
|
**Every cap is recorded.** When a **team** gate passes on its counted approvals while the cap lowered
|
|
@@ -132,7 +132,7 @@ Why count people: the rule names no person, no role and no step. A stored list o
|
|
|
132
132
|
stale; repository access decides who can approve.
|
|
133
133
|
|
|
134
134
|
Four surfaces print the count with its cap: `yad gate sync`, `yad gate status`, the generated
|
|
135
|
-
review-PR body and `yad open-pr`. (`checks/risk-route.sh` and `checks/
|
|
135
|
+
review-PR body and `yad open-pr`. (`checks/risk-route.sh` and `checks/product-route.sh` print the count
|
|
136
136
|
without the cap — they cannot count people.) `yad gate sync` and `yad gate status` share one suffix; the review-PR body and `yad
|
|
137
137
|
open-pr` word the cap their own way (below). The shared suffix appears only when the step has a risk
|
|
138
138
|
step. It names the cap only when the cap lowered the ask, and always ends by saying what holds:
|
|
@@ -177,7 +177,7 @@ not be counted, nothing is suggested: `yad mode` and `yad doctor` (a warning) sa
|
|
|
177
177
|
`verified` when it was recorded through the companion (a real trailer/cards/chat session), `none` for a
|
|
178
178
|
bare UI click. By **default (soft)** both count: a bare approve still passes the gate but is recorded
|
|
179
179
|
`none` and draws a friendly public @-mention nudge, so review *quality* is visible without blocking
|
|
180
|
-
anyone. When `
|
|
180
|
+
anyone. When `review.requireEngagement: true` in the Product settings, only `verified` approvals are counted toward the
|
|
181
181
|
approvers above; when that leaves the gate short, it adds a line saying how many approvals did not count (a determined faker
|
|
182
182
|
can still run an empty session — the signal is **gameable by design**; it raises the cost of a
|
|
183
183
|
rubber-stamp and makes laziness visible, it does not prove a human read the artifact). Philosophy:
|
|
@@ -249,7 +249,7 @@ not "resolve to pass" — it ignores them). A reviewer's *genuine* concern is po
|
|
|
249
249
|
marker and blocks normally, exactly as a `CHANGES_REQUESTED` or any unresolved human thread does.
|
|
250
250
|
|
|
251
251
|
## Platform-backed input (the verified ledger)
|
|
252
|
-
When the Product has a platform (`.sdlc/
|
|
252
|
+
When the Product has a platform (`.sdlc/product.json`), reviewers can approve/comment
|
|
253
253
|
on a real PR/MR instead — and only there: with a platform, `yad gate approve` / `comment` / `advance`
|
|
254
254
|
refuse. The sync reads that platform
|
|
255
255
|
state with the local user's own `gh`/`glab` and writes the **same**
|
|
@@ -262,26 +262,26 @@ unchanged**: it counts distinct approvers regardless of how they were recorded.
|
|
|
262
262
|
- On PR/MR open the assignee is whoever opens it: `@me` on GitHub, and the login `glab` reports on GitLab.
|
|
263
263
|
**No reviewers are requested** — the command prints `no reviewers were requested — ask them on the
|
|
264
264
|
PR itself`. `domain:<repo>` labels for the touched domains are still applied. See
|
|
265
|
-
`../yad-
|
|
265
|
+
`../yad-product-bridge/references/login-roster.md`.
|
|
266
266
|
- `sync` is idempotent (upsert by `(step, approver)`, one record per person; comment records by
|
|
267
267
|
`(step, commenter, round)`, an unchanged round rewritten in place) and never touches **manual** approvals. An older bridge record that carries `role`/`domain` is
|
|
268
|
-
recognised as `../yad-
|
|
268
|
+
recognised as `../yad-product-bridge/references/login-roster.md` → "Older records" describes, and replaced
|
|
269
269
|
by one login-named record that keeps its fingerprint. Every sync write (`yad gate sync`, `yad gate ci`)
|
|
270
270
|
also records the login on the older records the roster can place for certain; only those it cannot
|
|
271
271
|
place still need the roster on a later sync. A bridge approval records the
|
|
272
272
|
platform's evidence (`approvedAt`, and on GitHub `commit`, `url`, `reviewId`) — for the record only. A revoked approval is superseded **while the step is open**; once
|
|
273
273
|
the step is `done` its approvals are kept as the record of why it passed, and a re-sync only re-binds
|
|
274
|
-
new ones (see `../yad-
|
|
274
|
+
new ones (see `../yad-product-bridge/references/bridge.md` → "Idempotent re-sync").
|
|
275
275
|
- The architecture+contract staleness rule applies to bridge approvals too: a re-lock changes the
|
|
276
276
|
surface fingerprint, so bridge approvals of the old surface stop counting (they stay on disk as revoked).
|
|
277
|
-
- No platform / no CLI → the gate runs local with no error. Detail: `../yad-
|
|
277
|
+
- No platform / no CLI → the gate runs local with no error. Detail: `../yad-product-bridge/references/bridge.md`.
|
|
278
278
|
|
|
279
279
|
## Why this shape
|
|
280
280
|
- One approver who is not the author keeps review load low on a small team (design priority 2) while
|
|
281
281
|
still requiring a second pair of eyes (priority 1, code quality / production safety).
|
|
282
282
|
- The risk step asks for more people only where a change can break a shared surface
|
|
283
|
-
(contract/auth/payments). It is advisory for now: the capacity cap (E72) is reported.
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
(contract/auth/payments). It is advisory for now: the capacity cap (E72) is reported. Enforcing it, with a way
|
|
284
|
+
out for a gate that asks for more people than the team has, is roadmap row E108, which is parked. Until
|
|
285
|
+
then E73 only warns when a gate may not pass.
|
|
286
286
|
- No stored list of people: it goes stale, and repository access already decides who can approve.
|
|
287
287
|
- Everything is a file, so a future service can drive the same gate by writing the same records.
|
package/skills/yad-run/SKILL.md
CHANGED
|
@@ -117,9 +117,9 @@ Walk the steps for `repo` starting at `from`/`currentStep`. For each step:
|
|
|
117
117
|
|
|
118
118
|
**Commit the machine-written state.** After each iteration's writes (the trust-log shard in 2 and the
|
|
119
119
|
build-state change in 4), run `yad checkpoint --push` from `{project-root}`. It commits *only* the
|
|
120
|
-
`trust-log/` shards + `build-state/<story>.json` (never a Shape gate file) as one `chore(
|
|
120
|
+
`trust-log/` shards + `build-state/<story>.json` (never a Shape gate file) as one `chore(product): …`
|
|
121
121
|
audit-trail commit, and only ever on the default branch. It is a safe no-op when nothing changed, so
|
|
122
|
-
call it every iteration — teammates don't review these machine writes, but CI and `yad
|
|
122
|
+
call it every iteration — teammates don't review these machine writes, but CI and the `yad-status` skill on
|
|
123
123
|
other machines must see current trust evidence. Never run it off the default branch (it will refuse):
|
|
124
124
|
an unpushed or branch-stranded trust log leaves the run record `yad dial` shows out of date.
|
|
125
125
|
|
|
@@ -83,12 +83,12 @@ also the `build-log/` shards at engineer-review) — the shard dirs are what che
|
|
|
83
83
|
way `git gc` folds loose objects later (`yad tidy up` folds finished shards into the folded
|
|
84
84
|
`trust-log.json` / `build-log.json`) — plus any story `status:` flip (→ in-build/shipped) once that
|
|
85
85
|
story has a build-log ship (#112) — as one
|
|
86
|
-
`chore(
|
|
86
|
+
`chore(product): sync Build state — <epic>/<story> by @<login> [skip ci]`
|
|
87
87
|
audit-trail commit (`@<login>` when the platform login is known, else the git `user.name`, else `unknown`), on the default branch only,
|
|
88
88
|
staging *only* those files by an explicit allowlist (never a Shape gate file — so `ledger-guard`
|
|
89
89
|
never trips). It is idempotent (a no-op when nothing changed), so calling it after every transition —
|
|
90
90
|
including a halt — is safe and keeps the shared trust evidence current for CI, teammates, and
|
|
91
|
-
`yad
|
|
91
|
+
the `yad-status` skill on other machines. It refuses to run off the default branch (an unsigned `[skip ci]`
|
|
92
92
|
commit inside a future PR range would fail `verified-commits` and strand the PR).
|
|
93
93
|
|
|
94
94
|
## Effective dial (the kill switch and gates always win)
|
package/skills/yad-spec/SKILL.md
CHANGED
|
@@ -95,7 +95,8 @@ to read. See `references/spec-handoff.md` for the exact file map and per-file de
|
|
|
95
95
|
### Step 6 — Write link.md back to the story
|
|
96
96
|
Write `demo-repos/<repo>/specs/<story>/link.md` (template in `references/spec-handoff.md`) with
|
|
97
97
|
frontmatter linking the spec back to the product repo: `story`, `epic`, `repo`, `feature-id`,
|
|
98
|
-
`product-repo` (path
|
|
98
|
+
`product-repo` (path — when the code repo has `.sdlc/product-link.json`, write `../../<its path>`, the
|
|
99
|
+
place CI checks the Product out; the gates read that record first anyway), `contract-lock` (the hash **copied** from `contract-lock.json`, NOT recomputed
|
|
99
100
|
in the code repo — or the literal `none` on a short-lane epic, which has no lock; never a made-up or
|
|
100
101
|
empty hash, which the CI gate reads as a broken lock and fails), `speckit`
|
|
101
102
|
(`installed | not-installed`), `generated` (date). This `link.md` plus the
|
|
@@ -94,19 +94,28 @@ The contract surface above is **referenced, not re-defined**. Any change to the
|
|
|
94
94
|
back to the architecture gate in the product repo — it is never widened from this code repo.
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
`epic` must be the story ID's prefix (`EP-<slug>-S0N` → `EP-<slug>`); lineage-check, epic-open and
|
|
98
|
+
reconcile-debt-check FAIL on any other value (E118).
|
|
99
|
+
|
|
97
100
|
`product-repo` is the one field CI resolves on disk (contract-check, lineage-check, epic-open,
|
|
98
101
|
reconcile-debt all read it). Every gate resolves it the SAME way: an **absolute** path is used as-is; a
|
|
99
|
-
**relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out
|
|
100
|
-
|
|
102
|
+
**relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out of
|
|
103
|
+
the story dir and out of `specs/` to the code-repo root, so a Product checked out beside the code repo
|
|
101
104
|
is `../../../<product-dir>`. A relative value that only resolves from the **repo root** still works too
|
|
102
|
-
(what contract-check historically did), so older `link.md` files keep gating. Write it as an
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
(what contract-check historically did), so older `link.md` files keep gating. Write it as an **unquoted
|
|
106
|
+
scalar** — `"..."` or `'...'` is taken literally, and a leading `~`/`$VAR` is never expanded. An
|
|
107
|
+
unreachable path degrades the Product-reading gates to a PASS-with-note rather than failing them, and
|
|
108
|
+
each one now says so in its output. **CI reads `product-repo` from `link.md` as it stands on the base
|
|
109
|
+
branch** once one is merged (E117): a PR that changes it is read by the old value, and the new one
|
|
110
|
+
counts after the merge. **A merged `.sdlc/product-link.json` in the code repo comes before all of it
|
|
111
|
+
(E120):** its `path` (from the repo root, where CI checks the Product out) is what the gates read, and
|
|
112
|
+
`link.md`'s `product-repo` is only a fallback for a repo without one — so write `../../<that path>` here
|
|
113
|
+
when the record exists. A Product folder that the code repo itself tracks (a monorepo) is read as it
|
|
114
|
+
stands on the base commit, so a re-lock must merge before the slice change that pins it.
|
|
115
|
+
**Reachable-but-lockless is a different case and does NOT degrade:** when the Product resolves and
|
|
116
|
+
`epics/<epic>/.sdlc/` is there with no `contract-lock.json` in it, a claimed `Contract-Change: yes`
|
|
117
|
+
FAILS, because nothing exists for the claim to be true of. That is the ordinary state of a short-lane
|
|
118
|
+
epic, of a stub, and of a classic epic that has not reached its architecture gate yet.
|
|
110
119
|
|
|
111
120
|
## Do not re-invent the contract
|
|
112
121
|
|
|
@@ -113,7 +113,7 @@ As a <role>, I want <capability>, so that <outcome>.
|
|
|
113
113
|
`repos` is the field the later build phase reads to know where to scaffold specs — set it precisely.
|
|
114
114
|
|
|
115
115
|
### Step 6 — Advance the authoring step (NOT the gate)
|
|
116
|
-
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json
|
|
116
|
+
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
|
|
117
117
|
**verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
|
|
118
118
|
|
|
119
119
|
**verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
|
|
@@ -145,7 +145,7 @@ reviewers — the team asks them on the review PR itself. When this gate
|
|
|
145
145
|
passes the epic becomes **`ready-for-build`** — Build can start **and** the parallel
|
|
146
146
|
**`test-cases`** track opens for the tester (`yad-test-cases`); the two run at the same time. **Never record
|
|
147
147
|
approval here.** Shape steps do not auto-advance. When the Product has a platform, the gate opens a review
|
|
148
|
-
PR on the Product (via `yad-
|
|
148
|
+
PR on the Product (via `yad-product-bridge`, with a `domain:<repo>` label per touched repo) and
|
|
149
149
|
`yad-review-gate action: sync` pulls platform approvals/comments into the ledger; otherwise the review
|
|
150
150
|
is recorded local.
|
|
151
151
|
|
|
@@ -35,8 +35,21 @@ tree (branch + fast-forward). It writes **nothing** back to the registry.
|
|
|
35
35
|
Run `yad repo sync [<repo>]`. For each target repo it performs, in order:
|
|
36
36
|
|
|
37
37
|
### Step 1 — Locate and validate the repo
|
|
38
|
-
Resolve the local path from the registry.
|
|
39
|
-
|
|
38
|
+
Resolve the local path from the registry. The registry is **shared content** — anyone who can push to
|
|
39
|
+
the Product wrote it — so the path is judged before any git runs (E81), the same way `yad join` and
|
|
40
|
+
`yad repo clone` judge it. The repo is **skipped with the reason** when:
|
|
41
|
+
- the path is outside the workspace (the Product folder's parent), or runs through a `.git` folder;
|
|
42
|
+
- nothing is there yet — **not cloned on this machine**: run `yad repo clone` to clone it;
|
|
43
|
+
- a folder is there but is not a checkout (a folder the Product commits could be shaped like a bare
|
|
44
|
+
git repo, and git run inside it would read its `config`, which can run a command). A folder inside a
|
|
45
|
+
checkout with no `.git` of its own (`apps/web` in a monorepo) is not refused (unless a link inside a
|
|
46
|
+
repo's tree or a folder holding an entry named `HEAD` is on its way — git can take such a folder for
|
|
47
|
+
a bare repo), but sync still skips it: switching its branch would switch the whole checkout, so sync
|
|
48
|
+
that checkout instead;
|
|
49
|
+
- a folder on the way, **inside a repo's tree**, is a link — the Product's `evil -> ../../..`. A link of
|
|
50
|
+
your own directly in the workspace folder (`ws/backend -> /src/backend`) is followed.
|
|
51
|
+
|
|
52
|
+
If the path is a checkout but `HEAD` is unreadable, warn and skip — there is nothing to switch.
|
|
40
53
|
|
|
41
54
|
### Step 2 — Skip a dirty working tree (never overwrite local work)
|
|
42
55
|
If `git status --porcelain` is non-empty, the repo has uncommitted changes: **skip it and warn**
|
|
@@ -45,6 +58,9 @@ preserving local work is a hard rule.
|
|
|
45
58
|
|
|
46
59
|
### Step 3 — Determine the default branch
|
|
47
60
|
Use the registry `default_branch`. If absent, fall back to the remote's `origin/HEAD`, else `main`.
|
|
61
|
+
A recorded name must be a valid `refs/heads/<name>` (`git check-ref-format`) and must not start with
|
|
62
|
+
`-` or `+`: the name is handed to `git fetch`, where `--upload-pack=<command>` would run a command.
|
|
63
|
+
Any other name skips the repo with the reason (fix it in `.sdlc/repos.json`).
|
|
48
64
|
|
|
49
65
|
### Step 4 — Fetch the latest
|
|
50
66
|
If the repo has an `origin` remote: `git fetch origin <default_branch> --prune`. A local-only repo
|
|
@@ -65,7 +81,7 @@ so any repo whose `HEAD` now differs from its registry `syncedHead` has a **stal
|
|
|
65
81
|
the command ends by pointing the human at `yad repo refresh` to repack (that is a separate human
|
|
66
82
|
decision; this skill never repacks or writes the registry). After the repack + AI code-map
|
|
67
83
|
regeneration, `yad repo refresh --push` publishes the refreshed code-maps + registry to the Product's
|
|
68
|
-
default branch as a `chore(
|
|
84
|
+
default branch as a `chore(product): sync code-context … [skip ci]` audit commit.
|
|
69
85
|
|
|
70
86
|
## Hard rules
|
|
71
87
|
|
|
@@ -164,7 +164,7 @@ Keep the `## Automation (<tool>)` section of `test-cases.md` in step with this f
|
|
|
164
164
|
degraded (`testing: none`), do **not** write `test-links.json`.
|
|
165
165
|
|
|
166
166
|
### Step 5 — Advance the authoring step (NOT the gate)
|
|
167
|
-
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json
|
|
167
|
+
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
|
|
168
168
|
**verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
|
|
169
169
|
|
|
170
170
|
**verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
|
|
@@ -195,7 +195,7 @@ Report: the path to `test-cases.md`, the connected testing tool and what it prod
|
|
|
195
195
|
Build may already be underway in parallel, and that the next action is **review** via
|
|
196
196
|
`yad-review-gate` (base rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not
|
|
197
197
|
auto-advance. When the Product has a platform, the gate opens a review
|
|
198
|
-
PR on the Product (via `yad-
|
|
198
|
+
PR on the Product (via `yad-product-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments
|
|
199
199
|
into the ledger; otherwise the review is recorded local.
|
|
200
200
|
|
|
201
201
|
## Reference
|
package/skills/yad-ui/SKILL.md
CHANGED
|
@@ -184,7 +184,7 @@ Keep the `## Design (<tool>)` section of `ui-design.md` in step with this file.
|
|
|
184
184
|
(`design: none`), do **not** write `design-links.json`.
|
|
185
185
|
|
|
186
186
|
### Step 5 — Advance the authoring step (NOT the gate)
|
|
187
|
-
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json
|
|
187
|
+
**Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
|
|
188
188
|
**verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
|
|
189
189
|
|
|
190
190
|
**verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
|
|
@@ -216,7 +216,7 @@ Report: the paths to `ui-design.md` and `DESIGN.md`, whether Impeccable was used
|
|
|
216
216
|
tool and what it produced (e.g. "Figma — 4 screens generated", the file URL + `design-links.json` path,
|
|
217
217
|
or "no design tool — markdown-only"), and that the next action is **review** via `yad-review-gate` (base
|
|
218
218
|
rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not auto-advance. When the Product has a platform, the gate opens a review PR on the
|
|
219
|
-
Product (via `yad-
|
|
219
|
+
Product (via `yad-product-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
|
|
220
220
|
the ledger; otherwise the review is recorded local.
|
|
221
221
|
|
|
222
222
|
## Reference
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ledger-guard CURSOR ADAPTER — the same guard, answered in the protocol Cursor requires (E11).
|
|
3
|
-
#
|
|
4
|
-
# Cursor's `preToolUse` is a PERMISSION hook, and its docs are explicit: for a permission hook,
|
|
5
|
-
# "invalid JSON or a response that doesn't match the hook's schema blocks the action". Empty stdout is
|
|
6
|
-
# invalid JSON. The plain `ledger-guard.sh` prints nothing when it allows — so wiring it directly
|
|
7
|
-
# would have blocked EVERY file write in a verified project, which is the exact opposite of this
|
|
8
|
-
# guard's fail-open design.
|
|
9
|
-
#
|
|
10
|
-
# This wrapper guarantees one thing: a permission answer on stdout, always, whatever happens below.
|
|
11
|
-
#
|
|
12
|
-
# allow {"permission":"allow"} exit 0
|
|
13
|
-
# deny {"permission":"deny","user_message":"<reason>","agent_message":"<reason>"} exit 0
|
|
14
|
-
#
|
|
15
|
-
# A deny exits 0 because the JSON is the authoritative answer and exit 0 is what tells Cursor to read
|
|
16
|
-
# it; exit 2 blocks too, but is documented as the code for "no JSON to read" and would discard the
|
|
17
|
-
# reason — and naming the command that owns the transition is the whole point of speaking at edit time.
|
|
18
|
-
#
|
|
19
|
-
# It takes NO arguments, deliberately. Cursor's `hooks.json` holds one command STRING and does not
|
|
20
|
-
# document whether it is run through a shell, so a command with a flag in it might never be split into
|
|
21
|
-
# program and argument — a command not found, silently, on every call.
|
|
22
|
-
set -uo pipefail
|
|
23
|
-
|
|
24
|
-
# `CDPATH=` is load-bearing, and the Cursor entry is what makes it so: that entry invokes the script
|
|
25
|
-
# by a RELATIVE path, so `${BASH_SOURCE[0]}` is relative and `cd` consults $CDPATH before the current
|
|
26
|
-
# directory. An exported CDPATH in a login profile — an ordinary thing to have, and inherited by the
|
|
27
|
-
# harness — would resolve `hooks` to some other tree entirely, and the guard would fail open on every
|
|
28
|
-
# write for as long as that profile lived.
|
|
29
|
-
#
|
|
30
|
-
# The loop resolves a symlinked SCRIPT, which `cd -P` does not: `-P` resolves the directory path, and
|
|
31
|
-
# a script reached through a link in another directory would otherwise look for its sibling beside the
|
|
32
|
-
# LINK and not find it. `readlink` without `-f` because `-f` is not portable to macOS.
|
|
33
|
-
_src="${BASH_SOURCE[0]}"
|
|
34
|
-
while [ -L "$_src" ]; do
|
|
35
|
-
_dir="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
|
|
36
|
-
_src="$(readlink -- "$_src")"
|
|
37
|
-
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
38
|
-
done
|
|
39
|
-
HOOK_DIR="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
|
|
40
|
-
ALLOW='{"permission":"allow"}'
|
|
41
|
-
|
|
42
|
-
# Every failure below this line resolves to ALLOW, because this guard fails open by design and CI is
|
|
43
|
-
# what fails closed. A wrapper that blocked when it could not reach `yad` would stop an agent editing
|
|
44
|
-
# anything at all the moment an install went sideways.
|
|
45
|
-
if [ ! -x "$HOOK_DIR/ledger-guard.sh" ]; then
|
|
46
|
-
echo " • yad hook: $HOOK_DIR/ledger-guard.sh is missing or not executable — allowing (run \`yad check --fix\`)" >&2
|
|
47
|
-
echo "$ALLOW"
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# stdout is CAPTURED (the JSON verdict), stderr is not (it flows through to Cursor's log). stdin is
|
|
52
|
-
# inherited by the command substitution, so the tool-call payload still reaches the guard.
|
|
53
|
-
raw="$("$HOOK_DIR/ledger-guard.sh" --format cursor)"
|
|
54
|
-
rc=$?
|
|
55
|
-
|
|
56
|
-
# THE VERDICT IS THE LAST NON-EMPTY LINE, not the whole of stdout, and it is recognised by containing
|
|
57
|
-
# a `"permission"` key rather than by starting with one.
|
|
58
|
-
#
|
|
59
|
-
# Both halves of that matter. `yad`'s own top-level failure handler prints to STDOUT, so a usage error
|
|
60
|
-
# or a crash banner can land in front of the JSON; a prefix test would then fail to recognise our own
|
|
61
|
-
# deny and fall through to the allow below, turning a real refusal into a permitted write with nothing
|
|
62
|
-
# on stderr to say so. And a containment test does not care what order the JSON's properties are in,
|
|
63
|
-
# so the deny answer's shape on the other side of this language boundary is not silently load-bearing.
|
|
64
|
-
verdict="$(printf '%s\n' "$raw" | awk 'NF { line = $0 } END { print line }')"
|
|
65
|
-
|
|
66
|
-
# `ledger-guard.sh` has its own fail-open branches that exit 0 with EMPTY stdout — no `yad` on PATH,
|
|
67
|
-
# an install it cannot resolve — and each one of those would otherwise reach Cursor as invalid JSON
|
|
68
|
-
# and block the write.
|
|
69
|
-
case "$verdict" in
|
|
70
|
-
*'"permission"'*) echo "$verdict"; exit 0 ;;
|
|
71
|
-
esac
|
|
72
|
-
|
|
73
|
-
# EXIT 2 IS A DENY, even with nothing on stdout — and this branch is reached in a real, ordinary case.
|
|
74
|
-
# `ledger-guard.sh` resolves `yad` from the Product's own `node_modules/yadflow` BEFORE `PATH`, so a
|
|
75
|
-
# project pinned to a yadflow older than `--format` parses the flag, ignores it, and answers in the
|
|
76
|
-
# exit protocol: exit 2, empty stdout. Falling through to the generic allow below would turn a real
|
|
77
|
-
# refusal into a permitted write, and print "exited 2 — allowing" while doing it.
|
|
78
|
-
#
|
|
79
|
-
# The message is a FIXED literal with nothing interpolated. The reason the older `yad` printed has
|
|
80
|
-
# already gone to stderr, where Cursor logs it; escaping arbitrary multi-line text into JSON in bash
|
|
81
|
-
# is exactly the sort of thing that produces an off-schema response, and there is no need to risk it.
|
|
82
|
-
if [ "$rc" -eq 2 ]; then
|
|
83
|
-
echo '{"permission":"deny","user_message":"yad: this edit touches CI-owned gate state.","agent_message":"This edit touches CI-owned gate state, which CI is the sole writer of. Do not edit the ledger by hand: use `yad gate open` for the transition, and commit only the artifact. The installed yadflow is older than this hook, so the full reason is in the hook log rather than here — `yad doctor` will name the version gap."}'
|
|
84
|
-
exit 0
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
if [ "$rc" -ne 0 ]; then
|
|
88
|
-
echo " • yad hook: ledger-guard.sh exited $rc — allowing (run \`yad doctor\` to check the install)" >&2
|
|
89
|
-
fi
|
|
90
|
-
echo "$ALLOW"
|
|
91
|
-
exit 0
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ledger-guard HARNESS HOOK — the local half of the CI gate of the same name (#171).
|
|
3
|
-
#
|
|
4
|
-
# The gate ledger is CI-owned in verified mode: `checks/ledger-guard.sh` rejects any non-bot commit
|
|
5
|
-
# that changes `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or `epics/*/reviews/*.md`,
|
|
6
|
-
# and the same files for the Foundation in `foundation/.sdlc/` and `foundation/reviews/` (E75).
|
|
7
|
-
# This hook says so at the moment an agent tries the edit, instead of twenty minutes later in a
|
|
8
|
-
# failed pipeline, and names the command that owns the transition (`yad gate open`).
|
|
9
|
-
#
|
|
10
|
-
# This file is only the ADAPTER. It locates `yad` and hands the tool-call payload to
|
|
11
|
-
# `yad hook ledger-guard`, which holds the decision — so the wiring never hard-codes an install path
|
|
12
|
-
# and the logic stays testable. The contract it passes through:
|
|
13
|
-
#
|
|
14
|
-
# stdin the harness's tool-call payload as JSON (optional)
|
|
15
|
-
# exit 0 allow
|
|
16
|
-
# exit 2 deny, reason on stderr
|
|
17
|
-
#
|
|
18
|
-
# Wired by `yad check --fix` for every harness that can refuse a write BEFORE it lands:
|
|
19
|
-
# Claude Code a `PreToolUse` hook in `.claude/settings.json`, pointing straight at THIS script
|
|
20
|
-
# Cursor a `preToolUse` hook in `.cursor/hooks.json`, pointing at `ledger-guard-cursor.sh`
|
|
21
|
-
#
|
|
22
|
-
# DO NOT WIRE THIS SCRIPT INTO CURSOR DIRECTLY, and check the same thing before wiring it into any
|
|
23
|
-
# other harness by hand. Cursor's `preToolUse` is a PERMISSION hook: it wants a JSON verdict on
|
|
24
|
-
# stdout, and treats an empty or off-schema answer as a refusal. This script prints NOTHING when it
|
|
25
|
-
# allows — so wired there directly it would block every file write in the project, which is the
|
|
26
|
-
# opposite of the fail-open stance below. `ledger-guard-cursor.sh` exists to speak that protocol.
|
|
27
|
-
#
|
|
28
|
-
# A harness that reads the EXIT CODE can use this script as it stands. A hook that only fires AFTER
|
|
29
|
-
# the write — Cursor's `afterFileEdit`, for one — is deliberately not used: it could report the edit
|
|
30
|
-
# but never refuse it, and reporting is what the CI gate already does.
|
|
31
|
-
#
|
|
32
|
-
# The project root is resolved from this script's own location below, so it does not matter which
|
|
33
|
-
# variable the harness sets ($CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR) or where it runs the command
|
|
34
|
-
# from; those variables appear only in the wiring, to point at this file.
|
|
35
|
-
#
|
|
36
|
-
# FAIL-OPEN: if no `yad` can be found, this ALLOWS and says why on stderr. A guardrail that blocked
|
|
37
|
-
# every edit the moment an install went sideways would be worse than the problem. The CI gate fails
|
|
38
|
-
# CLOSED and is what actually protects the ledger.
|
|
39
|
-
set -uo pipefail
|
|
40
|
-
|
|
41
|
-
# The Product root is this script's grandparent — hooks/ledger-guard.sh — so the resolution below does
|
|
42
|
-
# not depend on the harness's working directory.
|
|
43
|
-
# `CDPATH=` is load-bearing, and the Cursor entry is what makes it so: that entry invokes the script
|
|
44
|
-
# by a RELATIVE path, so `${BASH_SOURCE[0]}` is relative and `cd` consults $CDPATH before the current
|
|
45
|
-
# directory. An exported CDPATH in a login profile — an ordinary thing to have, and inherited by the
|
|
46
|
-
# harness — would resolve `hooks` to some other tree entirely, and the guard would fail open on every
|
|
47
|
-
# write for as long as that profile lived.
|
|
48
|
-
#
|
|
49
|
-
# The loop resolves a symlinked SCRIPT, which `cd -P` does not: `-P` resolves the directory path, and
|
|
50
|
-
# a script reached through a link in another directory would otherwise look for its sibling beside the
|
|
51
|
-
# LINK and not find it. `readlink` without `-f` because `-f` is not portable to macOS.
|
|
52
|
-
_src="${BASH_SOURCE[0]}"
|
|
53
|
-
while [ -L "$_src" ]; do
|
|
54
|
-
_dir="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
|
|
55
|
-
_src="$(readlink -- "$_src")"
|
|
56
|
-
case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
|
|
57
|
-
done
|
|
58
|
-
HOOK_DIR="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
|
|
59
|
-
HUB_ROOT="$(dirname -- "$HOOK_DIR")"
|
|
60
|
-
|
|
61
|
-
# Resolution order, cheapest and most specific first: an explicit override, then the copy installed
|
|
62
|
-
# in this Product, then whatever is on PATH, then a network-free npx. `--no-install` matters — a hook
|
|
63
|
-
# runs on every tool call and must never pause an agent to download a package.
|
|
64
|
-
CMD=()
|
|
65
|
-
_yad_bin="${YAD_BIN:-}"
|
|
66
|
-
if [ -n "${_yad_bin//[[:space:]]/}" ]; then
|
|
67
|
-
# Split deliberately: YAD_BIN is commonly an interpreter + script ("node /path/to/yad.mjs").
|
|
68
|
-
# The whitespace-stripped test above matters: a YAD_BIN of only spaces would leave CMD empty, and
|
|
69
|
-
# macOS's bash 3.2 treats "${CMD[@]}" on an empty array as an unbound variable under `set -u` —
|
|
70
|
-
# aborting the script with a confusing 127 instead of taking one of the branches below.
|
|
71
|
-
read -r -a CMD <<< "$_yad_bin"
|
|
72
|
-
elif [ -f "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
73
|
-
CMD=(node "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs")
|
|
74
|
-
elif command -v yad >/dev/null 2>&1; then
|
|
75
|
-
CMD=(yad)
|
|
76
|
-
elif command -v npx >/dev/null 2>&1; then
|
|
77
|
-
CMD=(npx --no-install yadflow)
|
|
78
|
-
else
|
|
79
|
-
echo " • yad hook: no \`yad\` on PATH and none installed in $HUB_ROOT — allowing (install yadflow to re-arm the ledger guard)" >&2
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# Belt and braces for bash 3.2's empty-array-is-unbound rule: every branch above sets CMD, but an
|
|
84
|
-
# unexpanded array under `set -u` would abort the script rather than allow, so check before using it.
|
|
85
|
-
if [ "${#CMD[@]}" -eq 0 ]; then
|
|
86
|
-
echo " • yad hook: could not resolve a \`yad\` to run — allowing" >&2
|
|
87
|
-
exit 0
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
# Run it rather than `exec`, so the exit code can be mapped. ONLY an explicit deny (2) blocks: a
|
|
91
|
-
# `yad` that is present but cannot run — an `npx --no-install` with no yadflow to find, a crash, a
|
|
92
|
-
# broken install — must not read as a refusal. Fail-open is the whole stance of this hook; the CI
|
|
93
|
-
# gate is what fails closed.
|
|
94
|
-
"${CMD[@]}" hook ledger-guard "$@"
|
|
95
|
-
rc=$?
|
|
96
|
-
[ "$rc" -eq 2 ] && exit 2
|
|
97
|
-
if [ "$rc" -ne 0 ]; then
|
|
98
|
-
echo " • yad hook: \`${CMD[*]} hook ledger-guard\` exited $rc — allowing (run \`yad doctor\` to check the install)" >&2
|
|
99
|
-
fi
|
|
100
|
-
exit 0
|