Compare commits
2 Commits
c839f1eb72
...
26a2bb5e56
| Author | SHA1 | Date |
|---|---|---|
|
|
26a2bb5e56 | |
|
|
694b161bb1 |
55
README.md
55
README.md
|
|
@ -1,14 +1,17 @@
|
||||||
# Project Template
|
# Project Template
|
||||||
|
|
||||||
A working documentation skeleton, the tracker convention that goes with it, and
|
A working documentation skeleton, the tracker convention that goes with it, and
|
||||||
the scripts that enforce both. Copy `docs/` into a new project, replace the
|
the scripts that enforce both. Adopt it with
|
||||||
angle-bracket placeholders, delete the paragraphs that do not apply — then
|
`docs/architecture/scripts/scaffold.sh`, which never overwrites a file that is
|
||||||
create the labels and the first milestone in the repository's issue tracker.
|
already there — the two *Start Here* documents below say which path to take.
|
||||||
|
Then replace the angle-bracket placeholders, delete the paragraphs that do not
|
||||||
|
apply, and create the labels and the first milestone in the repository's issue
|
||||||
|
tracker.
|
||||||
|
|
||||||
```
|
```
|
||||||
Status: Current
|
Status: Current
|
||||||
Owner: _null
|
Owner: _null
|
||||||
Last reviewed: 2026-08-12
|
Last reviewed: 2026-08-22
|
||||||
Governs: README.md, START-HERE-New-Project.md, START-HERE-Existing-Project.md
|
Governs: README.md, START-HERE-New-Project.md, START-HERE-Existing-Project.md
|
||||||
Review trigger: Any change to how a project adopts this template — scaffold.sh's
|
Review trigger: Any change to how a project adopts this template — scaffold.sh's
|
||||||
file list, the tracker conventions or the label names, or what
|
file list, the tracker conventions or the label names, or what
|
||||||
|
|
@ -151,26 +154,30 @@ and a row on the site only when a human puts it there.
|
||||||
|
|
||||||
### Where the tracker is
|
### Where the tracker is
|
||||||
|
|
||||||
**[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and
|
**A Forgejo instance**, and that it is Forgejo is not a detail. The convention
|
||||||
naming it is not a detail. The convention below is portable: milestones, issues
|
below is portable: milestones, issues and labels exist on GitHub, GitLab and
|
||||||
and labels exist on GitHub, GitLab and Jira, and every rule in it would work
|
Jira, and every rule in it would work there unchanged. **The reporting is not
|
||||||
there unchanged. **The reporting is not portable at all.** Every figure the
|
portable at all.** Every figure the Command Center shows — the headline
|
||||||
Command Center shows — the headline percentage, the milestone count, milestone
|
percentage, the milestone count, milestone coverage, the alert an opened `P0`
|
||||||
coverage, the alert an opened `P0` raises — is read from a Forgejo repository
|
raises — is read from a Forgejo repository mapped to the project. A project that
|
||||||
mapped to the project. A project that follows every rule here in a different
|
follows every rule here in a different tracker satisfies the convention
|
||||||
tracker satisfies the convention completely and appears on privacyllc.dev as
|
completely and appears on privacyllc.dev as having no tracker at all.
|
||||||
having no tracker at all.
|
|
||||||
|
|
||||||
Credentials are in **`~/.openclaw/docker-registry.env`**, which carries
|
The instance and its credentials are operator configuration, not repository
|
||||||
`FORGEJO_REGISTRY`, `FORGEJO_REGISTRY_USER` and `FORGEJO_REGISTRY_TOKEN`:
|
content: they live in this machine's credential store and are loaded into the
|
||||||
|
environment before the call. **Nothing here names either**, and neither should
|
||||||
|
anything you add — a template is copied into repositories that may end up
|
||||||
|
public, and a document naming a host, a credential file and what that token can
|
||||||
|
reach is a map even when it holds no secret.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
set -a; . ~/.openclaw/docker-registry.env; set +a
|
# Load the tracker credentials from wherever this machine keeps them, then:
|
||||||
python3 docs/architecture/scripts/forgejo-issue.py list
|
python3 docs/architecture/scripts/forgejo-issue.py list
|
||||||
```
|
```
|
||||||
|
|
||||||
Despite its name that token is not registry-scoped — it works across the whole
|
Whatever that token turns out to reach, treat it as reaching everything until
|
||||||
API, with push and pull. Never print it into a log or a chat.
|
somebody has checked: an API token named for one job is routinely scoped for
|
||||||
|
none. Never print it into a log or a chat.
|
||||||
|
|
||||||
Two things that cost an hour each otherwise. **Cloudflare fronts the instance
|
Two things that cost an hour each otherwise. **Cloudflare fronts the instance
|
||||||
and 1010-blocks clients that do not look like a browser or curl** — Python's
|
and 1010-blocks clients that do not look like a browser or curl** — Python's
|
||||||
|
|
@ -186,10 +193,6 @@ cannot file into another project's tracker by accident. `--repo owner/name`
|
||||||
overrides that deliberately, and `--dry-run` prints the payloads and changes
|
overrides that deliberately, and `--dry-run` prints the payloads and changes
|
||||||
nothing.
|
nothing.
|
||||||
|
|
||||||
> This block names one instance and one path on the machine that runs it. If
|
|
||||||
> this repository will be public, replace both with your own before the first
|
|
||||||
> push.
|
|
||||||
|
|
||||||
### The rules
|
### The rules
|
||||||
|
|
||||||
- **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a
|
- **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a
|
||||||
|
|
@ -202,6 +205,14 @@ nothing.
|
||||||
inventing one.
|
inventing one.
|
||||||
- **Issue = deliverable.** One per item, in its milestone, ending with a
|
- **Issue = deliverable.** One per item, in its milestone, ending with a
|
||||||
`Verify:` line stating the acceptance check.
|
`Verify:` line stating the acceptance check.
|
||||||
|
- **An issue that produces a data shape names its surface.** A schema, a stored
|
||||||
|
figure, a computed field or an API response is not a deliverable by itself —
|
||||||
|
somebody has to be able to see it. Say which screen shows it, link the issue
|
||||||
|
that provides one, or record the decision that it is deliberately not shown.
|
||||||
|
Skip it and the `Verify:` line drifts toward *"a caller can retrieve it"*,
|
||||||
|
which is satisfiable with nothing visible: a batch of twenty-eight issues
|
||||||
|
closed that way leaves the product exactly as it was, and every figure it
|
||||||
|
built is reachable only by whoever knows the JSON.
|
||||||
- **Labels:** `P0` ships broken or loses data · `P1` materially wrong but
|
- **Labels:** `P0` ships broken or loses data · `P1` materially wrong but
|
||||||
shippable · `P2` cosmetic or low impact · `release-blocker` means a release
|
shippable · `P2` cosmetic or low impact · `release-blocker` means a release
|
||||||
built today would be wrong rather than merely incomplete. Exactly these
|
built today would be wrong rather than merely incomplete. Exactly these
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ disagree, and nothing will say which one is right.
|
||||||
| How do I release, back up, or check this? | `scripts/` |
|
| How do I release, back up, or check this? | `scripts/` |
|
||||||
| Which script do I run, and can it stop me? | `docs/TOOLS.md` — the signpost; `docs/architecture/README.md` has the table |
|
| Which script do I run, and can it stop me? | `docs/TOOLS.md` — the signpost; `docs/architecture/README.md` has the table |
|
||||||
| What runs before a commit? | `.githooks/` |
|
| What runs before a commit? | `.githooks/` |
|
||||||
|
| What stops an agent reporting work it did not verify? | `docs/architecture/scripts/verify-before-done.sh`, and `docs/TOOLS.md` for why it is a second layer rather than the layer |
|
||||||
| How do I write a check that will actually catch something? | `docs/architecture/GUARDS.md` |
|
| How do I write a check that will actually catch something? | `docs/architecture/GUARDS.md` |
|
||||||
|
|
||||||
**Next action and blockers are recorded at the end of every piece of work, not
|
**Next action and blockers are recorded at the end of every piece of work, not
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,26 @@ The hooks are the other place work gets stopped:
|
||||||
[`architecture/githooks/`](architecture/githooks/README.md) has the one install
|
[`architecture/githooks/`](architecture/githooks/README.md) has the one install
|
||||||
command and the table of what each hook runs.
|
command and the table of what each hook runs.
|
||||||
|
|
||||||
|
**And a third place, which stops something the other two cannot: the claim.**
|
||||||
|
`scripts/verify-before-done.sh` is a Claude Code `TaskCompleted` hook that runs
|
||||||
|
`verify.sh` and refuses to let a task be reported as finished when it fails.
|
||||||
|
Every other gate here fires on an **artifact** — a commit, a build, a tag — so
|
||||||
|
an agent that says "done" without committing trips none of them, which is the
|
||||||
|
gap `WORK_CYCLE.md` is describing when it says *"Done" is not a close*.
|
||||||
|
|
||||||
|
Two things to know before installing it, both in the script's header at length:
|
||||||
|
|
||||||
|
- **It exits `2` to block, and `1` fails open.** Claude Code reads a `1` from a
|
||||||
|
hook as "the hook broke" and continues, so a gate written the ordinary way
|
||||||
|
lets through precisely what it was installed to catch, and looks identical
|
||||||
|
doing it. This is the exit-code rule above with the numbers swapped, and it is
|
||||||
|
the one place in this template where that is true.
|
||||||
|
- **It gates Claude Code and nothing else.** A Codex session, a human, or any
|
||||||
|
other agent in the same checkout writes past it without knowing it is there.
|
||||||
|
It is a second layer; `.githooks/pre-commit` is the layer, because git runs
|
||||||
|
that whoever is driving. If this project's real suite is not wired into that
|
||||||
|
hook, wiring it there is worth more than installing this.
|
||||||
|
|
||||||
## Where to start in a fresh clone
|
## Where to start in a fresh clone
|
||||||
|
|
||||||
1. `bash docs/architecture/scripts/check-env.sh` — what is configured and what
|
1. `bash docs/architecture/scripts/check-env.sh` — what is configured and what
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ can stop you, and where to start in a fresh clone.
|
||||||
| `scripts/scaffold.sh` | lay out a new project in this shape |
|
| `scripts/scaffold.sh` | lay out a new project in this shape |
|
||||||
| `scripts/doc-triggers.py` | which documents a **pending** change fires, read from the `Governs:` headers of everything under `docs/` and of the documents at the repository root, narrowed by the optional `Fires on:` — the kinds of change (added, deleted, moved, changed) a document's trigger actually names, so one governing `docs/**` for existence changes alone does not fire on every edit. The `Review trigger` on each document names the change that should send somebody back to it; this is the check that asks before the commit rather than after |
|
| `scripts/doc-triggers.py` | which documents a **pending** change fires, read from the `Governs:` headers of everything under `docs/` and of the documents at the repository root, narrowed by the optional `Fires on:` — the kinds of change (added, deleted, moved, changed) a document's trigger actually names, so one governing `docs/**` for existence changes alone does not fire on every edit. The `Review trigger` on each document names the change that should send somebody back to it; this is the check that asks before the commit rather than after |
|
||||||
| `scripts/prove-guard.sh` | breaks the thing a guard protects, requires the guard to go red, restores the file from a trap. `GUARDS.md` §1 written out as a command, including the count — one failing test reported on six lines is not six failures |
|
| `scripts/prove-guard.sh` | breaks the thing a guard protects, requires the guard to go red, restores the file from a trap. `GUARDS.md` §1 written out as a command, including the count — one failing test reported on six lines is not six failures |
|
||||||
|
| `scripts/verify-before-done.sh` | a Claude Code `TaskCompleted` hook: runs `verify.sh` and **blocks the claim of being finished** when it fails. One step earlier than `GUARDS.md` §6 — every other guard here fires on an artifact, and an agent that says "done" without committing produces none. Wired to `verify.sh` rather than the test command because `npm test` on a repo with no tests exits 0, and exit **2** (verified nothing) is treated as a failure, not a pass. Exits 2 to block: a hook exiting 1 fails **open**. Gates Claude Code only — a Codex session in the same checkout writes past it, so `.githooks/pre-commit` remains the gate that catches every writer |
|
||||||
| `scripts/commit-mine.sh` | commits only the paths you name, by pathspec, after the secret scan. For a tree something else is also writing: what anyone else has staged is reported and left exactly as it was |
|
| `scripts/commit-mine.sh` | commits only the paths you name, by pathspec, after the secret scan. For a tree something else is also writing: what anyone else has staged is reported and left exactly as it was |
|
||||||
| `scripts/doc-claims.sh` | every file a document names must exist, and (`--covers`) every file that exists is named — the second is the one that catches a list missing rows |
|
| `scripts/doc-claims.sh` | every file a document names must exist, and (`--covers`) every file that exists is named — the second is the one that catches a list missing rows |
|
||||||
| `scripts/duplication.py` | code that exists twice, tuned so what it reports is worth reading |
|
| `scripts/duplication.py` | code that exists twice, tuned so what it reports is worth reading |
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,36 @@ def check_verify_line(body: str, title: str) -> str:
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def check_surface_named(body: str, title: str) -> str:
|
||||||
|
"""An issue that produces a data shape names its surface.
|
||||||
|
|
||||||
|
Both tests read the `Verify:` line, not the body — bodies say "page" and
|
||||||
|
"renders" incidentally all the time, and the acceptance check is the thing
|
||||||
|
that decides whether the issue can close with nothing visible.
|
||||||
|
|
||||||
|
Deliberately narrow. It fires only when the acceptance check's subject is
|
||||||
|
the data itself — retrievable, returned, reported, recorded — and nothing in
|
||||||
|
it requires a person to see the result. A warning that fires on most issues
|
||||||
|
is one nobody reads, and it would take the `Verify:`-not-last warning down
|
||||||
|
with it.
|
||||||
|
|
||||||
|
A warning rather than a refusal: API-only is a legitimate decision, it just
|
||||||
|
has to be a decision rather than an omission."""
|
||||||
|
lines = [l for l in body.strip().splitlines() if l.strip()]
|
||||||
|
verify = next((l for l in reversed(lines) if l.strip().startswith("Verify:")), "")
|
||||||
|
data_is_the_subject = re.search(
|
||||||
|
r"(retrievable|returns the|--json\b|a caller|is stored|recorded with|"
|
||||||
|
r"\breports\b|available (?:from|via) the API)", verify, re.I)
|
||||||
|
a_person_sees_it = re.search(
|
||||||
|
r"\b(admin|screen|page|shown|shows|displayed|visible|sees|operator|"
|
||||||
|
r"renders|dialog|button|UI)\b", verify, re.I)
|
||||||
|
if data_is_the_subject and not a_person_sees_it:
|
||||||
|
print(f'warning: "{title}" accepts on the data being retrievable and never on\n'
|
||||||
|
" anyone seeing it. Name the screen, link the issue that provides\n"
|
||||||
|
" one, or state that it is deliberately API-only.",
|
||||||
|
file=sys.stderr)
|
||||||
|
return body
|
||||||
|
|
||||||
def resolve_labels(names, available: dict[str, int]) -> list[int]:
|
def resolve_labels(names, available: dict[str, int]) -> list[int]:
|
||||||
"""Names → ids, failing loudly. A typo'd severity label is reported by the
|
"""Names → ids, failing loudly. A typo'd severity label is reported by the
|
||||||
Command Center as *not adopted*, not as zero defects — silently dropping it
|
Command Center as *not adopted*, not as zero defects — silently dropping it
|
||||||
|
|
@ -390,6 +420,7 @@ def create_one(api: Api, spec: dict, labels_map, ms_map, existing,
|
||||||
allow_dup=False) -> dict | None:
|
allow_dup=False) -> dict | None:
|
||||||
title = spec["title"].strip()
|
title = spec["title"].strip()
|
||||||
body = check_verify_line(spec.get("body", ""), title)
|
body = check_verify_line(spec.get("body", ""), title)
|
||||||
|
body = check_surface_named(body, title)
|
||||||
|
|
||||||
dup = find_duplicate(title, existing)
|
dup = find_duplicate(title, existing)
|
||||||
if dup and not allow_dup:
|
if dup and not allow_dup:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Gate the CLAIM of being finished, not the artifact. A Claude Code
|
||||||
|
# `TaskCompleted` hook.
|
||||||
|
#
|
||||||
|
# ## The hole this fills
|
||||||
|
#
|
||||||
|
# Every other guard in this template fires on an artifact: `githooks/pre-commit`
|
||||||
|
# on a commit, `audit-gate.mjs` and `preflight.sh` on a release, `verify.sh`
|
||||||
|
# when somebody runs it. `GUARDS.md` §6 is the rule they follow -- guards belong
|
||||||
|
# before the artifact exists.
|
||||||
|
#
|
||||||
|
# An agent that says "done" and does not commit produces no artifact, so it
|
||||||
|
# trips none of them. That is not a hypothetical: `WORK_CYCLE.md` opens by
|
||||||
|
# saying "Done" is not a close, the tracker convention refuses a `close` under
|
||||||
|
# fifteen characters of evidence, and both exist because the claim arrives
|
||||||
|
# without the work behind it often enough to need a rule. A rule is a thing a
|
||||||
|
# reader can skip. This is the same rule with an exit code.
|
||||||
|
#
|
||||||
|
# So: one step earlier than §6. Before the artifact exists, and before the
|
||||||
|
# sentence claiming it does.
|
||||||
|
#
|
||||||
|
# ## Why it calls verify.sh and not the test command
|
||||||
|
#
|
||||||
|
# `npm test` in a repo with no tests exits 0. A gate wired to it passes
|
||||||
|
# vacuously and reports green on a repository that verified nothing, which is
|
||||||
|
# `GUARDS.md` §4 and §8's entire subject -- the guard that cannot fail is worse
|
||||||
|
# than the guard nobody wrote, because it is believed.
|
||||||
|
#
|
||||||
|
# `verify.sh` already answers the right question. It runs every check the
|
||||||
|
# project actually has, in one command, and its exit codes are built for this:
|
||||||
|
#
|
||||||
|
# 0 everything that ran passed
|
||||||
|
# 1 something failed, or the run could not start
|
||||||
|
# 2 NOTHING WAS VERIFIED -- no checks detected, or every step skipped
|
||||||
|
#
|
||||||
|
# Two is not a pass and this script does not treat it as one. A repository that
|
||||||
|
# has not got as far as having checks fails this gate, loudly, with the reason.
|
||||||
|
# That is the intended answer: wire the gate up when there is something for it
|
||||||
|
# to run, and until then know that there is not.
|
||||||
|
#
|
||||||
|
# ## Install
|
||||||
|
#
|
||||||
|
# cp docs/architecture/scripts/verify-before-done.sh scripts/
|
||||||
|
# chmod +x scripts/verify-before-done.sh
|
||||||
|
#
|
||||||
|
# and in `.claude/settings.json` at the project root:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# "hooks": {
|
||||||
|
# "TaskCompleted": [
|
||||||
|
# { "hooks": [ { "type": "command",
|
||||||
|
# "command": "${CLAUDE_PROJECT_DIR}/scripts/verify-before-done.sh",
|
||||||
|
# "timeout": 900 } ] }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# **Set `timeout` from this repository's own suite time, not from a number
|
||||||
|
# copied out of a blog post.** Time `bash scripts/verify.sh` once and give it
|
||||||
|
# headroom. A hook killed by its own timeout reports as a failure, so a budget
|
||||||
|
# that is too small turns a passing suite into a blocked task and teaches
|
||||||
|
# everybody to remove the hook -- `GUARDS.md` §5, a guard that is often wrong is
|
||||||
|
# worse than none.
|
||||||
|
#
|
||||||
|
# ## What it costs, said plainly
|
||||||
|
#
|
||||||
|
# It runs the suite every time a task completes, not once at the end of the
|
||||||
|
# session, so a long session runs it many times. On a repository whose suite
|
||||||
|
# takes minutes that is real time and real usage. `VERIFY_BEFORE_DONE_ARGS`
|
||||||
|
# exists for that: set it to `--quick` and the gate runs the checks that are
|
||||||
|
# cheap and names the test step as skipped rather than pretending it ran.
|
||||||
|
#
|
||||||
|
# A skipped step is visible in verify.sh's table. A gate that quietly stopped
|
||||||
|
# running is not. That is the whole difference.
|
||||||
|
#
|
||||||
|
# ## THE LIMITATION, AND IT IS NOT SMALL
|
||||||
|
#
|
||||||
|
# This is a Claude Code harness feature. It gates Claude Code and nothing else.
|
||||||
|
# A Codex session, a human, or any other agent working in the same checkout
|
||||||
|
# writes past it without noticing it exists -- and at least one repository here
|
||||||
|
# shares its checkout with Codex.
|
||||||
|
#
|
||||||
|
# So this is a second layer and never the layer. The gate that catches every
|
||||||
|
# writer is `.githooks/pre-commit`, because git runs it whoever is driving. If
|
||||||
|
# this repository's real suite is not wired into that hook, wiring it there is
|
||||||
|
# worth more than installing this, and neither one replaces the other.
|
||||||
|
#
|
||||||
|
# ## Exit codes -- the hook contract, which is not the usual one
|
||||||
|
#
|
||||||
|
# 0 allow the task to complete
|
||||||
|
# 2 BLOCK it, and feed stderr back to the model as the reason
|
||||||
|
#
|
||||||
|
# Two, specifically. A hook exiting 1 is a hook that broke, and Claude Code
|
||||||
|
# treats it as non-blocking -- so a gate that returns 1 on failure fails OPEN
|
||||||
|
# and lets exactly the thing it was installed to catch straight through. It
|
||||||
|
# looks identical from the outside. Prove this one blocks before trusting it:
|
||||||
|
# break something the suite covers and watch a task refuse to finish.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
cd "${CLAUDE_PROJECT_DIR:-.}" || {
|
||||||
|
echo "verify-before-done: cannot enter ${CLAUDE_PROJECT_DIR:-.}" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f scripts/verify.sh ]; then
|
||||||
|
# Fail closed. "There is no verify.sh" is a fact about the repository worth
|
||||||
|
# blocking on -- the alternative is a gate that silently allows everything on
|
||||||
|
# exactly the projects with no other checks.
|
||||||
|
echo "verify-before-done: scripts/verify.sh not found in $PWD." >&2
|
||||||
|
echo "Install it from the template, or remove this hook from .claude/settings.json." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Word-split on purpose: this is a small set of flags, not a path.
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
out="$(bash scripts/verify.sh ${VERIFY_BEFORE_DONE_ARGS:-} 2>&1)"
|
||||||
|
code=$?
|
||||||
|
|
||||||
|
if [ "$code" -eq 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The tail, because the model reads this and the table verify.sh prints last is
|
||||||
|
# the part that says which step failed. A full log of a large suite buries it.
|
||||||
|
{
|
||||||
|
if [ "$code" -eq 2 ]; then
|
||||||
|
echo "verify.sh verified NOTHING (exit 2): no checks were detected, or every"
|
||||||
|
echo "step skipped. That is not a pass. Do not report this task as finished."
|
||||||
|
else
|
||||||
|
echo "verify.sh FAILED (exit $code). Do not report this task as finished."
|
||||||
|
fi
|
||||||
|
echo "Fix what it names, or say plainly that it is failing and why."
|
||||||
|
echo
|
||||||
|
printf '%s\n' "$out" | tail -n 200
|
||||||
|
} >&2
|
||||||
|
exit 2
|
||||||
Loading…
Reference in New Issue