@xhulz/nina 0.47.0 → 0.48.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/bin/nina.mjs +8 -2
- package/package.json +1 -1
- package/releases/0.48.0/core/GAPS.md +125 -0
- package/releases/0.48.0/core/answered.json +44 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/qa.md +7 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.48.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.48.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.48.0/core/tree/.claude/agents/dba.md +117 -0
- package/releases/0.48.0/core/tree/.claude/agents/devops.md +118 -0
- package/releases/0.48.0/core/tree/.claude/agents/implementer.md +160 -0
- package/releases/0.48.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.48.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.48.0/core/tree/.claude/agents/qa.md +150 -0
- package/releases/0.48.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.48.0/core/tree/.claude/agents/secops.md +120 -0
- package/releases/0.48.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.48.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.48.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.48.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.48.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.48.0/core/tree/.claude/patterns.md +251 -0
- package/releases/0.48.0/core/tree/.claude/pills/README.md +147 -0
- package/releases/0.48.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.48.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.48.0/core/tree/.claude/router.md +202 -0
- package/releases/0.48.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.48.0/core/tree/CLAUDE.md +164 -0
- package/releases/0.48.0/core/tree/scripts/cost-watch.mjs +25 -0
- package/releases/0.48.0/core/tree/scripts/edit-guard.mjs +24 -0
- package/releases/0.48.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.48.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.48.0/core/vocabulary.json +13 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.48.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.48.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.48.0/surfaces/d1/tree/.claude/agents/dba.md +13 -0
- package/releases/0.48.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
- package/releases/0.48.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.48.0/surfaces/d1/tree/.claude/patterns.md +8 -0
- package/releases/0.48.0/surfaces/d1/tree/CLAUDE.md +2 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/patterns.md +22 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.48.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.48.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.48.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
- package/releases/0.48.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
- package/releases/0.48.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.48.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.48.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.48.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.48.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.48.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.48.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.48.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.48.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.48.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.48.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.48.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.48.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.48.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.48.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.48.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.48.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/check.mjs +10 -0
- package/src/commands/gate.mjs +53 -2
- package/src/commands/init.mjs +9 -2
- package/src/gate.mjs +58 -19
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!-- nina:slot money.1 -->
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Monetary values & percentages
|
|
6
|
+
|
|
7
|
+
- **Money is an integer of the currency's minor unit**, end to end — {{MINOR_UNIT}}, never a float, never a type that can hold a fraction of one. Source of precision: `{{MONEY_MODULE}}`.
|
|
8
|
+
- **Percentages are basis points** (`40% → 4000`, `100% → 10000`) so a distribution is exact without floating point.
|
|
9
|
+
- **Parsing, formatting and percentage arithmetic live in one module and nowhere else.** A second place that turns a string into money is a second rounding rule, and the two will disagree on a value somebody is owed.
|
|
10
|
+
- **Never** convert to a floating-point type before formatting, and never let a display format round a stored value.
|
|
11
|
+
- Monetary database columns are integer. JSON responses serialize them as **strings**, not numbers, so no client parses them into a float. The service owns the conversion.
|
|
12
|
+
|
|
13
|
+
<!-- nina:slot money.2 -->
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Distribution invariants (enforced by reviewer + unit tests)
|
|
18
|
+
|
|
19
|
+
The distribution engine lives in `{{CORE_PKG}}`. It is pure and total over integer {{MINOR_UNIT}}:
|
|
20
|
+
|
|
21
|
+
- A destination is either a percentage (basis points of the amount) or a fixed value.
|
|
22
|
+
- `total = Σ destinations`, computed in {{MINOR_UNIT}}.
|
|
23
|
+
- `total > amount` → **reject.** Never clamp, never distribute what is not there.
|
|
24
|
+
- `total < amount` → **allow**, surface the shortfall, and leave the remainder **parked**. Silence is the defect here: an unexplained remainder is indistinguishable from a bug.
|
|
25
|
+
- `total = amount` → OK.
|
|
26
|
+
- A retention is just another destination — the same arithmetic applies to it, with no special case.
|
|
27
|
+
|
|
28
|
+
<!-- nina:slot money.3 -->
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## State machines & idempotency (enforced by reviewer)
|
|
33
|
+
|
|
34
|
+
- **Every money-moving entity is a state machine.** Its states and legal transitions are declared in `.claude/architecture.md`; the code follows the declaration, not the other way round. Illegal transitions throw; they are never silently coerced.
|
|
35
|
+
- **Every transition writes an audit row** (entity type, entity id, from, to, actor). No transition without a trail — the trail is what makes a money bug diagnosable after the money has moved.
|
|
36
|
+
- **Outbound transfers are idempotent.** Each carries a unique key, and dedupe happens before the side effect, at the serialization point.
|
|
37
|
+
- **One serialization point per balance.** Whatever computes and dispatches against a single balance never runs concurrently with itself. Two concurrent runs over one balance is how a double send happens even when every individual write is correct.
|
|
38
|
+
|
|
39
|
+
<!-- nina:slot money.4 -->
|
|
40
|
+
- **Money computation and dispatch go through the serialization point.** Routes, queue consumers and tools never run a distribution or a transfer synchronously in the request path.
|
|
41
|
+
|
|
42
|
+
<!-- nina:slot money.5 -->
|
|
43
|
+
- **The distribution engine lives in `{{CORE_PKG}}`.** Services call it as a pure function; they do not embed money logic inline.
|
|
44
|
+
|
|
45
|
+
<!-- nina:slot money.6 -->
|
|
46
|
+
- Idempotency on money-moving writes: the record carries an idempotency key, and the serialization point refuses the second attempt *before* the side effect.
|
|
47
|
+
|
|
48
|
+
<!-- nina:slot money.7 -->
|
|
49
|
+
- `{{CORE_PKG}}` is the most-tested package — pure functions over fixtures. Cover the distribution invariants exhaustively: over, under, exact, mixed percentage and fixed, and rounding.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- nina:slot money.1 -->
|
|
2
|
+
3. **An outbound transfer is idempotent — it is NEVER sent twice.** Every transfer carries an idempotency key, and deduplication happens at a single serialization point **before** the side effect, never after it. A retry, a re-delivered queue message, a double-click and a replayed webhook must all collapse to one transfer. This is the highest-blast-radius invariant in the system: almost everything else can be corrected afterwards, and a duplicated transfer cannot.
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot money.2 -->
|
|
5
|
+
4. **Conservation — what comes in equals what goes out plus what is retained.** A distribution may NEVER exceed the amount it distributes → **block**. If it is **less**, **alert** and leave the remainder **parked** and visible — never silently distributed, never silently absorbed. All arithmetic is integer {{MINOR_UNIT}}; percentages are basis points. The LLM never computes a monetary value.
|
|
6
|
+
|
|
7
|
+
<!-- nina:slot money.3 -->
|
|
8
|
+
5. **State-machine integrity.** Every money-moving entity has an explicit set of states and legal transitions, declared in `.claude/architecture.md` § *State machines* — not inferred from whatever code happens to exist. Only declared transitions are legal; an illegal one throws and is never silently coerced. **Every transition writes an audit row** (entity, id, from → to, actor, when). Skipping a state, inventing one, or landing a transition with no audit row is a hard reject.
|
|
9
|
+
|
|
10
|
+
<!-- nina:slot money.4 -->
|
|
11
|
+
- **money movement**: anything that creates, moves, settles or records a balance
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
- **Privacy.** No sensitive value in clear text where it must not be. The categories are declared in `.claude/architecture.md` § *Privacy* — they are not decided per query, and not decided by you. **Document bytes live only in the object store, referenced by key: the column holds the key, never the bytes.** Any column the architect's spec flagged for masking must be encrypted at rest. Reject a schema that widens what is stored in the clear.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
|
|
3
|
+
**Point it at an UNAUTHENTICATED route — a sign-in page is the right one.** Never sign in, and never
|
|
4
|
+
screenshot a signed-in page. Accounts on deployed environments hold real personal data, and a
|
|
5
|
+
screenshot or a console dump pulls it into agent transcripts and logs, where it persists and nobody
|
|
6
|
+
ever looks for it again. That is a privacy leak with no upside here: the sign-in page proves
|
|
7
|
+
everything this check is for. It renders from the same bundle, it exercises the same build-time env
|
|
8
|
+
inlining (a missing variable blanks it identically), and it needs no session.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
- Check for privacy regressions: **no sensitive value in a log, an error message — a library's own included — a URL or a test fixture**, per the categories in `.claude/architecture.md` § *Privacy*; document bytes only in the object store, never plaintext in the database; no leaked secret.
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot pii.2 -->
|
|
5
|
+
- **privacy** — the privacy check above, in full: no sensitive value in a log, an error message, a
|
|
6
|
+
URL or a test fixture; document bytes only in the object store; no leaked secret. Where
|
|
7
|
+
**tenant-and-privacy** is dispatched, its reviewer owns this too and there is no separate one; where
|
|
8
|
+
it is not, this is a dimension of its own. Either way it is never nobody's.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
- **Privacy (Hard Rule #8)** — the categories declared in `.claude/architecture.md` § *Privacy* must never appear in logs, in responses that do not need them, in error paths, or in the database in clear text. Check every endpoint that returns a person's record for fields it includes by accident: a `select` that quietly grew a column is the usual way this leaks. Check the error paths too — they are written last and reviewed least — and what the libraries on them print or carry in their own errors, bound values included.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
- **Nothing personal reaches the chain.** What is written on-chain is public, permanent, and beyond
|
|
3
|
+
deletion by anyone — including by you, and including after a subject asks. A hash of a personal
|
|
4
|
+
identifier is not a protection: it is a lookup table for whoever holds the input set. Reject a
|
|
5
|
+
personal datum stored in state, emitted in an event, or passed as calldata.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- nina:slot pii.1 -->
|
|
2
|
+
8. **Sensitive personal data is classified, and the classification is enforced.** What counts as sensitive here — identity documents, financial identifiers, anything a person would not expect to be readable by whoever can read a log — is declared in `.claude/architecture.md` § *Privacy*, along with the regime that governs it ({{PRIVACY_REGIME}}). Encrypted at rest. **No sensitive value in a log, an error message, a URL, an analytics event, or an agent transcript** — the places nobody thinks about are exactly the ones that persist. That includes what a library puts in its own errors and prints itself: a database client's error can carry the query and every bound value, and a framework can log a whole error past the logger it was given. A database error is mapped at the data layer to a typed error from a closed list before anything logs or returns it, and a test proves that a bound value it writes reaches no thrown error. Document bytes live only in the object store, referenced by key, never in the database in clear text.
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot pii.2 -->
|
|
5
|
+
- **personal data**: where it is collected, stored, logged or sent
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!-- nina:slot prisma.1 -->
|
|
2
|
+
| `prisma-cli` | **before ANY `prisma` CLI command** — `validate`, `format`, `migrate dev`, `migrate diff`, `migrate deploy`. Flags differ between versions, and one of them (`migrate diff --shadow-database-url` against a live database) destroys the database it points at |
|
|
3
|
+
| `prisma-client-api` | auditing a new or changed query — filters, operators, `$transaction` semantics. Through a driver adapter, cite the adapter's installed source for what a transaction does: the skill describes the default engine, and an adapter can roll back less |
|
|
4
|
+
|
|
5
|
+
<!-- nina:slot prisma.2 -->
|
|
6
|
+
Neither skill covers a **caching or edge layer sitting on top of the client** — cache strategy,
|
|
7
|
+
edge-client behaviour and the rule about never caching a read that authorizes a write belong to that
|
|
8
|
+
layer's own integration doc, declared like any other integration. A skill does not replace it.
|
|
9
|
+
|
|
10
|
+
<!-- nina:slot prisma.3 -->
|
|
11
|
+
- **With Prisma, both offline.** Run `pnpm --filter {{DB_PKG_NAME}} exec prisma validate`; it must pass. Then run `prisma migrate diff` in its **offline, file-only form** and read the generated SQL, not just the Prisma delta. Its flags changed in Prisma 7, so read the installed major (`prisma --version`) before writing the command:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
# Prisma 6. Compare the PREVIOUS schema against the working one — pure file-to-file, no database.
|
|
15
|
+
git show HEAD:{{DB_PKG}}/prisma/schema.prisma > /tmp/schema-head.prisma
|
|
16
|
+
pnpm --filter {{DB_PKG_NAME}} exec prisma migrate diff \
|
|
17
|
+
--from-schema-datamodel /tmp/schema-head.prisma \
|
|
18
|
+
--to-schema-datamodel prisma/schema.prisma \
|
|
19
|
+
--script
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
From Prisma 7 the two flags are `--from-schema` and `--to-schema`, and the schema engine starts from the
|
|
23
|
+
datasource in `prisma.config.ts` even for this diff: with none there, it prints nothing and exits 0. For
|
|
24
|
+
SQLite a placeholder the diff never opens is enough, a `file:` URL under a gitignored directory; for any
|
|
25
|
+
other provider, find out what the diff opens before you give it one. Never a real database's URL. A first schema has nothing at `HEAD`: diff it `--from-empty`. Compare the output against the
|
|
26
|
+
hand-written `migration.sql` to confirm the declarative parts match.
|
|
27
|
+
|
|
28
|
+
⚠️ **Do NOT reach for `--from-migrations`.** It looks like the natural choice and it is a trap:
|
|
29
|
+
Prisma refuses it without a shadow database (`--shadow-database-url`, or from Prisma 7 a
|
|
30
|
+
`shadowDatabaseUrl` in `prisma.config.ts`), which pushes you toward supplying a database URL — the exact move that destroys databases (below). The file-to-file form above answers the
|
|
31
|
+
same question with no database involved.
|
|
32
|
+
|
|
33
|
+
⛔ **NEVER point `--shadow-database-url`, `--from-url`, `--to-url`, `--from-schema-datasource` — or, from Prisma 7, a `shadowDatabaseUrl` or the config datasource behind `--from-config-datasource` — at a real database, most of all not the one `{{SECRETS_LOCAL}}` names.** Prisma **executes** the entire migration history against whatever database those flags name, which **DESTROYS ALL DATA** in it. A shadow database must be a disposable, empty database and nothing else. It does not announce itself: the command completes with a reassuring "empty migration" result and no error, having already destroyed everything in the database it was pointed at. If the offline form above cannot answer your question, **STOP and report that to the orchestrator** instead of reaching for a database URL.
|
|
34
|
+
|
|
35
|
+
<!-- nina:slot prisma.4 -->
|
|
36
|
+
- **Where reads are cached through Prisma Accelerate,** every `findUnique` / `findMany` on a hot read path names its `cacheStrategy` (`ttl` / `swr`), and one that feeds a correctness-critical write names none.
|
|
37
|
+
|
|
38
|
+
<!-- nina:slot prisma.5 -->
|
|
39
|
+
- **On an edge runtime,** new Prisma code paths use the edge client of the installed major: `@prisma/client/edge` up to Prisma 6; from 7, the client the `prisma-client` generator writes with the platform's `runtime` set, imported from where it is generated — `@prisma/client/edge` fails there with `MODULE_NOT_FOUND`.
|
|
40
|
+
|
|
41
|
+
<!-- nina:slot prisma.6 -->
|
|
42
|
+
- Run a Prisma command that writes to a real database — `prisma migrate dev`, `migrate deploy`, `migrate reset`, `migrate resolve`, `db push`, `db execute`, `$executeRaw*` — or **any `prisma migrate diff` variant carrying a database URL**, which silently wipes the database it points at.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- nina:slot prisma.1 -->
|
|
2
|
+
- **`prisma-cli`** — before `migrate deploy` or any other Prisma CLI command against a real database.
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot prisma.2 -->
|
|
5
|
+
- **Prisma migrations go out with `prisma migrate deploy`**, and nothing else changes a shared database's schema.
|
|
6
|
+
|
|
7
|
+
<!-- nina:slot prisma.3 -->
|
|
8
|
+
- Run `prisma migrate diff --shadow-database-url <url>` against any live database. It **executes** the migration history against that database, destroying everything in it, and reports success. Use a disposable shadow database or don't run it.
|
|
9
|
+
|
|
10
|
+
<!-- nina:slot prisma.4 -->
|
|
11
|
+
- Run `prisma migrate reset` against a shared database, ever.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- nina:slot prisma.1 -->
|
|
2
|
+
- **Database table/column names** (Prisma `@map` / `@@map`): `snake_case` in SQL, `camelCase` in TS.
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot prisma.2 -->
|
|
5
|
+
- Where Prisma reads are cached through Accelerate, `cacheStrategy` lives in `{{DB_PKG}}`: services do not add, strip, or override cache policies.
|
|
6
|
+
|
|
7
|
+
<!-- nina:slot prisma.3 -->
|
|
8
|
+
|
|
9
|
+
### Prisma
|
|
10
|
+
|
|
11
|
+
- Schema lives in `{{DB_PKG}}/prisma/schema.prisma`. Nowhere else.
|
|
12
|
+
- **Never write raw SQL** unless justified in a code comment with perf data.
|
|
13
|
+
- N+1 is solved with `include` / `select`, chosen deliberately.
|
|
14
|
+
- Where reads are cached through Accelerate, every `findUnique` / `findMany` on a hot path defines its `cacheStrategy`, and no direct connection string is embedded in code.
|
|
15
|
+
- Tests run a real Prisma client against a real database; the client is never mocked.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- nina:slot prisma.1 -->
|
|
2
|
+
| Run any `prisma` CLI command (the database gate, devops, and the integration gate where there is one) | `prisma-cli` | flags differ between versions; `migrate diff --shadow-database-url` against a live DB wiped dev/staging once |
|
|
3
|
+
|
|
4
|
+
<!-- nina:slot prisma.2 -->
|
|
5
|
+
| Write or audit a Prisma query (architect, implementer, the database gate, reviewer) | `prisma-client-api` | filters, operators, `$transaction` semantics. a caching or edge layer on top of the client is **NOT covered** — its cache strategy belongs to that layer's own integration doc |
|
package/src/commands/check.mjs
CHANGED
|
@@ -297,6 +297,16 @@ export async function check(argv, ctx) {
|
|
|
297
297
|
notes.push(`${label} names boundary "${entry.boundary}", which does not exist yet`);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
// The other direction: a doc no integration declares is retired, waits for its boundary, or lost its
|
|
301
|
+
// declaration, and nothing told a reader which. Said as a note, and as a deferral once the profile says why.
|
|
302
|
+
const declaredSlugs = new Set(integrations.map((entry) => entry?.slug));
|
|
303
|
+
const docs = await readdir(join(target, '.claude', 'integrations')).catch(() => []);
|
|
304
|
+
for (const doc of docs.filter((name) => name.endsWith('.md') && !declaredSlugs.has(name.slice(0, -'.md'.length))).sort()) {
|
|
305
|
+
const key = `.claude/integrations/${doc}`;
|
|
306
|
+
const why = deferredWhy(profile, key);
|
|
307
|
+
if (why) deferredUsed.add(key);
|
|
308
|
+
notes.push(why ? `deferred: ${key} — ${why}` : `${key} documents no declared integration — declare it, or say under "deferred" why not: retired, or waiting for its boundary`);
|
|
309
|
+
}
|
|
300
310
|
|
|
301
311
|
const slots = await projectSlots(resolved.dir, surfaces);
|
|
302
312
|
const filled = await filledSlots(target);
|
package/src/commands/gate.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* `nina gate` — the loop gate's own check, and a way to run it by hand.
|
|
3
3
|
*
|
|
4
4
|
* nina gate --selftest is the gate wired, has it failed, would it still hold a loop past its cap?
|
|
5
|
+
* nina gate --status the loops a session holds open, and what would close each
|
|
5
6
|
* nina gate --hook handle one hook event from stdin, the way the composed script does
|
|
6
7
|
*
|
|
7
8
|
* The gate lets every call through when anything goes wrong, which is right for a hook and makes a
|
|
@@ -20,7 +21,7 @@ import { tmpdir } from 'node:os';
|
|
|
20
21
|
import { join, resolve } from 'node:path';
|
|
21
22
|
import { readProfile, slugFor } from '../paths.mjs';
|
|
22
23
|
import { GATE, hookCommand, missingWiring, shippedScripts } from '../wiring.mjs';
|
|
23
|
-
import { LEDGER_TAIL, ledgerPath, loadProject, projectGateDir, readLedger, runGate } from '../gate.mjs';
|
|
24
|
+
import { LEDGER_TAIL, ledgerPath, loadProject, loopState, projectGateDir, readLedger, runGate } from '../gate.mjs';
|
|
24
25
|
import { projectRecords } from '../store.mjs';
|
|
25
26
|
import { layerRootFor } from './compose.mjs';
|
|
26
27
|
import { handsToModel } from '../detectors.mjs';
|
|
@@ -271,13 +272,63 @@ function dryRun(target) {
|
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
/**
|
|
274
|
-
*
|
|
275
|
+
* What the gate holds of one session: its loops, what would close each, and the loop-backs waiting on a fix —
|
|
276
|
+
* the same lines a compaction hands the orchestrator, at any time. The session is the one whose ledger was
|
|
277
|
+
* written last, which is the running one while it dispatches, or the one `--session` names by the start of its id.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} target - The project directory.
|
|
280
|
+
* @param {string|undefined} wanted - The start of a session id, or nothing for the latest.
|
|
281
|
+
* @returns {number} Process exit code.
|
|
282
|
+
*/
|
|
283
|
+
function loopStatus(target, wanted) {
|
|
284
|
+
const project = loadProject(target);
|
|
285
|
+
if (!project) {
|
|
286
|
+
console.error(' .claude/graph.md is not composed, so the gate has no loops to hold — run `nina compose`\n');
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
const dir = projectGateDir(target);
|
|
290
|
+
const ledgers = (existsSync(dir) ? readdirSync(dir) : [])
|
|
291
|
+
.filter((f) => f.endsWith('.jsonl') && f !== 'errors.jsonl')
|
|
292
|
+
.map((f) => ({ session: f.slice(0, -'.jsonl'.length), written: statSync(join(dir, f)).mtime }))
|
|
293
|
+
.sort((a, b) => b.written - a.written);
|
|
294
|
+
if (ledgers.length === 0 && !wanted) {
|
|
295
|
+
console.log(` the gate has no ledger here yet: no session has dispatched a stage since it was wired (${dir})`);
|
|
296
|
+
console.log('gate: no open loop');
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
const matching = wanted ? ledgers.filter((l) => l.session.startsWith(wanted)) : ledgers.slice(0, 1);
|
|
300
|
+
if (matching.length !== 1) {
|
|
301
|
+
const why = matching.length === 0 ? `no session starts with "${wanted}"` : `${matching.length} sessions start with "${wanted}" — give more of the id`;
|
|
302
|
+
console.error(` ${why} (${dir})\n`);
|
|
303
|
+
return 2;
|
|
304
|
+
}
|
|
305
|
+
const { session, written } = matching[0];
|
|
306
|
+
const state = loopState(project, readLedger(join(dir, `${session}.jsonl`)));
|
|
307
|
+
const others = ledgers.length > 1 ? `; ${ledgers.length - 1} other session(s) kept here, --session <id> for one` : '';
|
|
308
|
+
console.log(` session ${session}, its ledger last written ${written.toISOString().slice(0, 16).replace('T', ' ')} UTC${others}`);
|
|
309
|
+
for (const line of state.lines) console.log(` ${line}`);
|
|
310
|
+
const counts = [
|
|
311
|
+
state.open ? `${state.open} open loop(s)` : 'no open loop',
|
|
312
|
+
state.closing ? `${state.closing} settled, dropped at the next dispatch` : '',
|
|
313
|
+
state.waiting ? `${state.waiting} loop-back(s) waiting on a fix` : '',
|
|
314
|
+
];
|
|
315
|
+
console.log(`gate: ${counts.filter(Boolean).join(', ')}`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {string[]} argv - `[--selftest | --status [--session <id>] | --hook] [--project <dir>]`.
|
|
275
321
|
* @param {{root: string}} ctx - CLI context; `root` is the NINA install directory.
|
|
276
322
|
* @returns {Promise<number>} Process exit code.
|
|
277
323
|
*/
|
|
278
324
|
export async function gate(argv, ctx) {
|
|
279
325
|
const target = resolve(argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : '.');
|
|
280
326
|
if (argv.includes('--hook')) return runGate({ root: target });
|
|
327
|
+
if (argv.includes('--status')) return loopStatus(target, argv.includes('--session') ? argv[argv.indexOf('--session') + 1] : undefined);
|
|
328
|
+
if (argv.includes('--session')) {
|
|
329
|
+
console.error(' --session names the session --status reads, and means nothing without it\n');
|
|
330
|
+
return 2;
|
|
331
|
+
}
|
|
281
332
|
|
|
282
333
|
const { profile, error } = readProfile(target);
|
|
283
334
|
if (error) {
|
package/src/commands/init.mjs
CHANGED
|
@@ -218,14 +218,21 @@ async function interview(available, matched, impacts) {
|
|
|
218
218
|
const scope = lines.join(' ');
|
|
219
219
|
|
|
220
220
|
const order = available;
|
|
221
|
-
console.log(`\n ${strong('Surfaces')} — ${order.length} yes-or-no questions, one per surface.`);
|
|
221
|
+
console.log(`\n ${strong('Surfaces')} — up to ${order.length} yes-or-no questions, one per surface; a stack comes only after a yes to what it needs.`);
|
|
222
222
|
console.log(' A surface is a part of the harness only some projects need: its rules, its checks, sometimes a');
|
|
223
223
|
console.log(' whole role. Answer for what the project does today; one can be added later in .nina/profile.json.\n');
|
|
224
224
|
const surfaces = [];
|
|
225
|
+
let asked = 0;
|
|
225
226
|
for (const [n, s] of order.entries()) {
|
|
226
227
|
// A stack is asked about only where its concern was answered yes: asked whether data it does not
|
|
227
228
|
// own goes through Prisma, a project has nothing true to answer.
|
|
228
229
|
if (missingNeeds(s, surfaces).length > 0) continue;
|
|
230
|
+
// Counted by what is asked, not by place in the list: numbered by place, a no to the database read
|
|
231
|
+
// 1/10 then 4/10, and the header promised ten questions to someone asked seven. The total is what is
|
|
232
|
+
// asked so far plus what can still be, so a no shrinks it; the last question is n/n while the order
|
|
233
|
+
// ends with a surface that needs nothing, as it does.
|
|
234
|
+
asked += 1;
|
|
235
|
+
const total = asked + order.slice(n + 1).filter((t) => missingNeeds(t, [...surfaces, ...order.slice(n)]).length === 0).length;
|
|
229
236
|
const i = impacts[s];
|
|
230
237
|
const reason = matched.find((d) => d.surface === s)?.why;
|
|
231
238
|
// What a yes brings, in what a person would recognise: roles, rules, agents. The count of
|
|
@@ -235,7 +242,7 @@ async function interview(available, matched, impacts) {
|
|
|
235
242
|
...(i.rules ? [`${i.rules} hard rule${i.rules > 1 ? 's' : ''}`] : []),
|
|
236
243
|
...(i.agents.length ? [`changes to ${i.agents.length} agent${i.agents.length > 1 ? 's' : ''}`] : []),
|
|
237
244
|
];
|
|
238
|
-
console.log(` ${faint(`${
|
|
245
|
+
console.log(` ${faint(`${asked}/${total}`)} ${strong(s)}`);
|
|
239
246
|
console.log(` ${questionFor(s)}`);
|
|
240
247
|
console.log(faint(` if yes: ${brings.length ? brings.join(' · ') : `guidance in ${i.fragments} places`}`));
|
|
241
248
|
if (reason) console.log(found(` already here: ${reason}`));
|
package/src/gate.mjs
CHANGED
|
@@ -280,11 +280,13 @@ function append(path, entry) {
|
|
|
280
280
|
* @param {object[]} entries - The ledger, oldest first.
|
|
281
281
|
* @param {ReturnType<typeof loopEdges>} loops - The capped loop-back edges.
|
|
282
282
|
* @param {Map<string, Set<string>>} [forward] - Stage → the sources that hand passing work on to it.
|
|
283
|
-
* @returns {{effect: (to: string) => {rounds: object[]}, rounds: object[]}}
|
|
283
|
+
* @returns {{effect: (to: string) => {rounds: object[]}, rounds: object[], open: object[], waiting: object[]}}
|
|
284
284
|
* A round is `{source, target, token, round, max, edgeRound, ceiling, agents}`, plus `issue` (the id
|
|
285
285
|
* that decides it) and `issues` (every id with its count) when it was counted by issue.
|
|
286
286
|
* `effect` says what a dispatch to a stage would make now, without making it; `rounds` is every round
|
|
287
|
-
* the recorded dispatches made
|
|
287
|
+
* the recorded dispatches made; `open` each loop still counted, with whether a pass already closes it
|
|
288
|
+
* (`closing`), the stages a dispatch to which would (`targets`) and the stages whose pass would (`onward`);
|
|
289
|
+
* `waiting` each loop-back no fix has acted on yet.
|
|
288
290
|
*/
|
|
289
291
|
export function replay(entries, loops, forward = new Map()) {
|
|
290
292
|
// When each dispatch went out. A foreground dispatch's `dispatch` entry is written after the agent
|
|
@@ -329,15 +331,23 @@ export function replay(entries, loops, forward = new Map()) {
|
|
|
329
331
|
for (const map of [streak, grace]) for (const key of [...map.keys()]) if (key.startsWith(`${source}|`)) map.delete(key);
|
|
330
332
|
};
|
|
331
333
|
|
|
334
|
+
/**
|
|
335
|
+
* A source's waiting verdicts, settled: the rejections no later review of a fix cancelled, and whether a pass
|
|
336
|
+
* closes its loops — one that began after the latest fix, with no rejection running beside it.
|
|
337
|
+
*/
|
|
338
|
+
const settle = (source, batch) => {
|
|
339
|
+
const passes = batch.filter((v) => !isLoopBack(v.verdict));
|
|
340
|
+
const live = batch.filter((v) => isLoopBack(v.verdict) && !passes.some((p) => p.launch > v.at && fixBetween(source, v.at, p.launch)));
|
|
341
|
+
return { live, closes: passes.some((p) => p.launch > (lastFix.get(source) ?? -1) && !live.some((v) => v.launch < p.at)) };
|
|
342
|
+
};
|
|
343
|
+
|
|
332
344
|
const effect = (to, at) => {
|
|
333
345
|
const rounds = [];
|
|
334
346
|
const outcome = [];
|
|
335
347
|
for (const [source, batch] of pending) {
|
|
336
348
|
const byToken = loops.get(source);
|
|
337
349
|
if (!byToken || batch.length === 0 || !targetsOf(source).has(to)) continue;
|
|
338
|
-
const
|
|
339
|
-
const live = batch.filter((v) => isLoopBack(v.verdict) && !passes.some((p) => p.launch > v.at && fixBetween(source, v.at, p.launch)));
|
|
340
|
-
const closes = passes.some((p) => p.launch > (lastFix.get(source) ?? -1) && !live.some((v) => v.launch < p.at));
|
|
350
|
+
const { live, closes } = settle(source, batch);
|
|
341
351
|
const routed = live.filter((v) => byToken.get(v.verdict)?.has(to));
|
|
342
352
|
const fresh = routed.filter((v) => {
|
|
343
353
|
const last = streak.get(`${source}|${v.verdict}`);
|
|
@@ -434,13 +444,18 @@ export function replay(entries, loops, forward = new Map()) {
|
|
|
434
444
|
});
|
|
435
445
|
// What is open when the replay ends: each loop with its rounds, the rounds the owner's replies gave it and the
|
|
436
446
|
// issues it counted by, and each loop-back no fix has acted on yet — what a compaction takes from the orchestrator.
|
|
447
|
+
// A loop whose source passed after the latest fix is closed by the next dispatch it could act on, and until then
|
|
448
|
+
// is still in the count; it says so, and what would close each of the others. Listed as open, three loops
|
|
449
|
+
// approved the evening before a compaction were handed back as capped, and the orchestrator went looking for why.
|
|
437
450
|
const open = [...streak.entries()]
|
|
438
451
|
.filter(([key]) => key.split('|').length === 2)
|
|
439
452
|
.map(([key, s]) => {
|
|
440
453
|
const [source, token] = key.split('|');
|
|
441
454
|
const cap = Math.max(0, ...(loops.get(source)?.get(token)?.values() ?? []));
|
|
442
455
|
const issues = [...streak.keys()].filter((k) => k.startsWith(`${key}|`)).map((k) => k.slice(key.length + 1));
|
|
443
|
-
|
|
456
|
+
const closing = settle(source, pending.get(source) ?? []).closes;
|
|
457
|
+
const onward = [...forward].filter(([, sources]) => sources.has(source)).map(([stage]) => stage);
|
|
458
|
+
return { source, token, rounds: s.n, cap, given: grace.get(key) ?? 0, issues, closing, targets: [...targetsOf(source)], onward };
|
|
444
459
|
});
|
|
445
460
|
const waiting = [...pending.entries()].flatMap(([source, batch]) => batch.filter((v) => isLoopBack(v.verdict)).map((v) => ({ source, verdict: v.verdict, issues: v.issues ?? [] })));
|
|
446
461
|
return { effect: (to) => effect(to, entries.length), rounds: made, open, waiting };
|
|
@@ -671,32 +686,56 @@ function onPre(input, project, path, at) {
|
|
|
671
686
|
return confirmation(worst);
|
|
672
687
|
}
|
|
673
688
|
|
|
689
|
+
/** Stages as a sentence lists them: `qa`, `implementer or architect`, `a, b or c`. */
|
|
690
|
+
const either = (stages) => (stages.length < 2 ? stages.join('') : `${stages.slice(0, -1).join(', ')} or ${stages.at(-1)}`);
|
|
691
|
+
|
|
674
692
|
/**
|
|
675
|
-
* What
|
|
676
|
-
* loop-backs
|
|
677
|
-
*
|
|
678
|
-
* report; the router told the orchestrator to write "round 2 of max 2" in a dispatch so "you after a
|
|
679
|
-
* compaction" could see it, and after a compaction it sees only a summary.
|
|
693
|
+
* What a session's ledger says of its loops, a line each: every loop still counted, and what would close it;
|
|
694
|
+
* the loop-backs no fix has acted on yet; and the latest dispatches. It is what a compaction hands the
|
|
695
|
+
* orchestrator and what `nina gate --status` prints, so the two cannot disagree.
|
|
680
696
|
*
|
|
681
697
|
* @param {object} project - `loadProject`'s.
|
|
682
|
-
* @param {
|
|
683
|
-
* @
|
|
684
|
-
*
|
|
698
|
+
* @param {object[]} entries - The session's ledger, oldest first.
|
|
699
|
+
* @returns {{lines: string[], open: number, closing: number, waiting: number}} The lines; the loops open, the
|
|
700
|
+
* loops a pass already settled, whose count the next dispatch drops, and the loop-backs waiting on a fix.
|
|
685
701
|
*/
|
|
686
|
-
function
|
|
687
|
-
const entries = readLedger(path);
|
|
702
|
+
export function loopState(project, entries) {
|
|
688
703
|
const { open, waiting } = replay(entries, project.loops, project.forward);
|
|
689
|
-
const lines = [
|
|
704
|
+
const lines = [];
|
|
690
705
|
for (const l of open) {
|
|
706
|
+
const count =
|
|
707
|
+
`${l.source} ${l.token} — ${l.rounds} round(s), capped at ${l.cap}${l.given ? ` and ${l.given} more from the owner's replies` : ''}` +
|
|
708
|
+
(l.issues.length ? `, issues ${l.issues.join(', ')}` : '');
|
|
709
|
+
const fix = waiting.some((w) => w.source === l.source) ? 'the fix it waits on' : 'the latest fix';
|
|
691
710
|
lines.push(
|
|
692
|
-
|
|
693
|
-
|
|
711
|
+
l.closing
|
|
712
|
+
? `- passed after its latest fix: ${count}. Those rounds are settled, and the gate drops them at the next dispatch to ${either(l.targets)}`
|
|
713
|
+
: `- open loop: ${count}. It closes when a ${l.source} launched after ${fix} passes with no rejection beside it` +
|
|
714
|
+
(l.onward.length ? `, or when ${either(l.onward)} passes, sent out after the ${l.source} last reported` : ''),
|
|
694
715
|
);
|
|
695
716
|
}
|
|
696
717
|
for (const w of waiting) lines.push(`- waiting on a fix: ${w.source} ${w.verdict}${w.issues.length ? `, issues ${w.issues.join(', ')}` : ''}`);
|
|
697
718
|
if (open.length === 0 && waiting.length === 0) lines.push('- no loop is open');
|
|
698
719
|
const dispatched = entries.filter((e) => e.k === 'dispatch' && e.role).slice(-4);
|
|
699
720
|
if (dispatched.length) lines.push(`- the latest dispatches: ${dispatched.map((e) => e.role).join(', ')}, the newest at ${String(dispatched.at(-1).at ?? '').slice(11, 16)} UTC`);
|
|
721
|
+
const closing = open.filter((l) => l.closing).length;
|
|
722
|
+
return { lines, open: open.length - closing, closing, waiting: waiting.length };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* What the gate hands the orchestrator after its conversation was compacted: the loops still open and the
|
|
727
|
+
* loop-backs waiting on a fix, the latest dispatches, where the checkout stands, and the two documents to read
|
|
728
|
+
* again. The graph and the router are read once, at a session's start, and a compaction drops them with every
|
|
729
|
+
* report; the router told the orchestrator to write "round 2 of max 2" in a dispatch so "you after a
|
|
730
|
+
* compaction" could see it, and after a compaction it sees only a summary.
|
|
731
|
+
*
|
|
732
|
+
* @param {object} project - `loadProject`'s.
|
|
733
|
+
* @param {string} path - The session's ledger.
|
|
734
|
+
* @param {string} root - The project.
|
|
735
|
+
* @returns {object}
|
|
736
|
+
*/
|
|
737
|
+
function afterCompaction(project, path, root) {
|
|
738
|
+
const lines = ['The conversation was compacted. What the NINA loop gate kept of this session:', ...loopState(project, readLedger(path)).lines];
|
|
700
739
|
const git = (...args) => spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', timeout: 2000 }).stdout?.trim() ?? '';
|
|
701
740
|
const branch = git('rev-parse', '--abbrev-ref', 'HEAD');
|
|
702
741
|
const last = git('log', '-1', '--format=%h %s');
|