@forgrit/shared
Cross-pillar contracts and resilience primitives used by Software Foundry, Software Orchestrator, and Workforce.
Status: Active internal package (0.3.0 in the manifest, not yet published — npm latest is 0.2.0) · Runtime: Node.js 20+ · Owner: @forgrit-legacy · CI: default-branch pipeline
What does NOT belong here
This package is scoped, cross-repo infrastructure only — not a dumping ground. Explicitly excluded (see docs/superpowers/plans/2026-08-17-software-foundry-rename-and-shared-package.md's Global Constraints for the full reasoning):
- Software Orchestrator's actual orchestration intelligence (
mission-control,kernel,planner,execution,brain,repository-*modules) - Software Foundry's product engine (
blueprint,codegen-types,debugger,design-*,shared/contracts-prompt) - Workforce's employee-governance model (
Principal,RoleCard,authorize()) - The Repository Intelligence "Frozen Five" contract (
Snapshot/Finding/Metric/Relationship/Insight) — gated behind its own stage-promotion rule, not ready yet software-orchestrator/modules/orchestrator/connector-runtime/RetryPolicy.tsorRateLimiter.tsas source material to copy from (both have known compile-error bugs; this package'sresiliencemodule uses their design ideas, not their code)
Purpose
This package is not a dumping ground for utility code. It hosts only infrastructure types and primitives that:
- Are consumed by more than one sibling repo (Foundry, Orchestrator, or Workforce)
- Carry cross-repo invariants (e.g., evidence ledger record structure)
- Do not belong in any single pillar's codebase
All new exports require evidence that the need appears in at least two consumers.
Subpath Exports
@forgrit/shared/design-system
The ForGrit design-token pipeline: immutable pulled revisions of the Claude Design kit, a role contract that names what those tokens mean, per-consumer bindings, emitters for the artefacts each consumer needs, and the gates a revision must pass before it can be adopted.
What the subpath ships
| Import | What it is |
|---|---|
@forgrit/shared/design-system/css/tokens.css | Plain CSS: :root and .dark custom properties, framework-free. |
@forgrit/shared/design-system/css/tailwind4.css | The same tokens plus a Tailwind 4 @theme inline block aliasing them. |
@forgrit/shared/design-system/tailwind3-preset | A Tailwind 3 preset — theme.extend, whose values are var(--token) references. |
@forgrit/shared/design-system/tokens.json | Every emitted token as data, with a $generated provenance string. |
@forgrit/shared/design-system/tokens | The same values as one typed TypeScript constant, DS. |
@forgrit/shared/design-system/contract | The 74-role contract — ROLES, ROLE_IDS, getRole, assertKnownRole. |
@forgrit/shared/design-system/adapters/contract | adaptTokenSystem — a Design Director's token spec projected onto the role contract. |
@forgrit/shared/design-system | DESIGN_SYSTEM_SCHEMA_VERSION and the shared types. |
forgrit-ds (bin) | The CLI: pull --seal, diff, emit, sync, check, report, assert-publishable. |
Every generated file carries the same first-line header — @generated by @forgrit/shared/design-system <version> from <family>/<revision> sha256:<contentHash> — in a CSS comment, a TypeScript comment or a JSON $generated field, so any artefact in any repository names the revision it came from.
The revision model
A revision is a directory that is written once and never rewritten: src/design-system/revisions/<family>/r<NNN>/, holding the pulled bytes (colors_and_type.css, _ds_manifest.json) and a revision.json whose contentHash is a SHA-256 over the canonical JSON of the sorted [{path, sha256}] file manifest — the design-corpus rule, so a revision hashes identically here and there. CURRENT is a one-line file naming the revision the package generates from, and design-sync.jsonl is an append-only log of the round trip a revision makes — push, pull, validate, reject, seal. pull --seal records the pull (from the pull's own .pull.json receipt) and the seal; a passing check run from a checkout records validate. Each step is recorded once per revision and content hash, so re-running it appends nothing on any day, and a line that disagrees about a revision's hash is refused rather than appended. Re-running pull --seal on identical bytes exits 0 and writes nothing; new bytes in a sealed ordinal are refused. Changing a sealed revision's bytes is caught by the revision-immutability gate rather than by a reviewer.
CURRENT names r001, the first revision pulled from Claude Design: provenance design-sync, parent r000, sealed on 2026-09-25 by the DesignSync runbook, which also records how the pull was done. Its stylesheet still carries the kit's own lineage comment ("Extracted from web/src/app/globals.css and layout.tsx"); the provenance is about where these bytes were pulled from, not where the kit's author first took the values. r000 stays in the tree, byte-for-byte unchanged, as r001's parent. It is provisional: its provenance is local-copy, meaning the bytes were copied in by hand rather than pulled. forgrit-ds assert-publishable (which prepublishOnly runs) refuses to publish while CURRENT names a local-copy revision, and emit, sync and check refuse one too unless --allow-local-copy is passed. report deliberately does not — describing a tree's drift is what a drift report is for, whatever the revision's provenance.
Revision bytes are public by design. A sealed revision ships inside the published package, so everything in one is world-readable the moment the package is published. Design tokens are the intended payload; nothing else belongs in a revision directory.
Consumer flow
npx forgrit-ds sync --root /path/to/consumer-repo # write the generated files
npx forgrit-ds check --root /path/to/consumer-repo # byte-diff them, and run all seven gates
sync writes the profile's targets into the consumer's tree — generated files that are committed to the consumer repository, so a build never depends on this package having been run. check regenerates the same targets into memory and byte-compares them with what is on disk, which is what a consumer's CI should run to prove nobody hand-edited a generated token. Neither command writes anything outside the target list.
Optional binding rows are emitted only when the revision authors them. --ember-strong and the --ember-fill* triad are bound as optional because r000 never authored them. r001 does — --ember-strong #B84C00 (#FF6A00 in .dark), --ember-fill #B84C00, --ember-fill-hover #C25000, --ember-fill-active #A34400 — so the package now emits all four with the kit's own values. The rule still decides what happens for a revision that omits them: after role resolution they do carry a value — the contract fills them from color.brand — and emitting that fallback would publish #FF6A00 under a name the revision never wrote. That would be actively harmful: #FF6A00 measures 2.87:1 on white and fails WCAG AA at body size, which is why the kit's own --ember-fill is #B84C00. So an optional row whose value came from a contract fallback is skipped, and only a value the revision itself authored is emitted.
Drift
forgrit-ds report --root <foundryRoot> [--orchestrator-root <dir>] classifies every custom property the consumers declare against the shipped revision, in both themes. The measured result for the shipped r001 is committed at docs/design-system/drift-r001.md; the earlier measurement against r000 stays at docs/design-system/drift-r000.md.
Not covered (deferred)
Phase 0 stops at a reproducible token pipeline. It does not do any of the following, and none of them is blocked by it:
- regenerating raster assets from a revision
- visual baselines in CI
- re-basing the consumers' shadcn
oklch()scaffold onto kit values - promoting the
rgb()/hsl()/oklch()value-format lint from advisory to a gate - shipping
woff2font binaries in the package - the premium theme for the ForGrit app (corpus-only today)
- a version column on
DesignCapsuleVersion/FrozenScreen - the
apps/apipreview lane - CI-side extraction of tokens from a running app
- DTCG (
design-tokens.json) export - automated publishing — releases stay manual
@forgrit/shared/engineering-contract
The versioned authority payload shared by customer control-plane and Workforce
callers of Software Orchestrator. parseExecutionTicket() validates untrusted
decoded input, rejects unknown fields (including raw credentials), enforces
issuer/principal pairing and expiry, and returns a defensive projection.
Signature verification, nonce/idempotency persistence, repository digest recomputation, and credential brokering remain receiver responsibilities; the shared package defines the interoperable payload rather than pretending schema validation is authentication.
@forgrit/shared/evidence
A hash-chained ledger for recording system decisions and side effects. Every record stores a SHA-256 hash over its own fields plus its predecessor's hash, so editing any historical record breaks the chain from that point onward and verifyChain reports exactly where.
What this gives you, and what it does not
Be precise about the security property, because the shape of the guarantee decides where the ledger is worth deploying:
- The hashes are unkeyed. Nothing here is signed. There is no HMAC, no key, no signature — only SHA-256 over a canonical serialization.
- The records are not immutable. They are plain JavaScript objects.
readonlyappears on array parameters and constrains this package's own functions; it does not freeze anything a caller holds. - It detects partial edits. An actor who rewrites one stored record — or a few — and leaves the rest alone is caught: the recomputed hash no longer matches, and every subsequent
prevHashlink is wrong. - It does not stop a full rewrite. An actor who can rewrite the whole stored ledger can recompute every hash forward from the edit and produce a chain that verifies cleanly. There is nothing in an unkeyed chain that such a writer cannot reproduce.
So this is tamper-evident against narrow tampering and accidental corruption, not a signature scheme. Its real value comes from pairing it with an append-only storage layer that restricts who can rewrite history: the storage layer makes wholesale rewriting hard, and the chain makes anything less than a wholesale rewrite visible. On its own, it is a corruption detector.
Invariants
seqis 0-indexed. The first event in a chain isseq: 0withprevHash: GENESIS_HASH; each later event hasseq === previous.seq + 1andprevHash === previous.hash.- The hash preimage is an explicit, versioned field projection —
v,seq,prevHash,type,subjectId,actorId,payload,observedAt— never a spread of whatever the caller passed. Extra fields on a stored record (a row id, an ORM's own columns) are outside the chain's protection and cannot perturb a recomputed hash.HASH_VERSIONis baked into the preimage so a future change to that projection is distinguishable rather than silently incompatible. payloadmust be JSON-safe.appendEventthrowsInvalidEvidenceEventErroron values whose canonical form would be lossy —Date,Map,Set,RegExp,Error, class instances,NaN/Infinity,bigint. Convert first:date.toISOString(),Object.fromEntries(map),{ name, message }for an error. This is deliberate: aDateused to serialize as{}, so two records with timestamps decades apart hashed identically.
import { appendEvent, verifyChain, type EvidenceEvent } from '@forgrit/shared/evidence';
let chain: EvidenceEvent[] = [];
// A null head starts a chain: seq 0, prevHash = GENESIS_HASH.
chain = [...chain, appendEvent(null, {
type: 'mission.completed', subjectId: 'mission-1', actorId: 'employee-1',
payload: { outcome: 'success' }, observedAt: new Date().toISOString(),
})];
// Afterwards, pass the head — just `{ seq, hash }`, not the whole history, so a
// database-backed caller appends after reading a single row. An EvidenceEvent
// satisfies that shape, so the previous event can be handed over directly.
chain = [...chain, appendEvent(chain[chain.length - 1], {
type: 'mission.archived', subjectId: 'mission-1', actorId: 'employee-1',
payload: { reason: 'completed' }, observedAt: new Date().toISOString(),
})];
verifyChain(chain); // { valid: true, length: 2 }
Verifying a page out of the middle of a long ledger, rather than loading all of it:
verifyChain(pageOfRows, {
expectedFirstSeq: 1000,
expectedFirstPrevHash: lastRowOfPreviousPage.hash,
});
With no options, verifyChain anchors at seq: 0 / GENESIS_HASH and verifies a whole chain.
@forgrit/shared/resilience
Resilience primitives for distributed systems: retry with exponential backoff, a circuit breaker, and a token-bucket rate limiter.
import { withRetry, CircuitBreaker, isTransientError } from '@forgrit/shared/resilience';
const breaker = new CircuitBreaker({ name: 'external-api', failureThreshold: 5 });
async function callApi(): Promise<Response> {
return breaker.execute(() =>
withRetry(() => fetch('https://api.example.com'), { maxAttempts: 3 }),
);
}
Retry classification. withRetry retries only what isTransientError accepts: ECONNREFUSED, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN. It reads a structured error.code first and falls back to the message text, but only where a code sits in machine-written position — so new Error('user typed ETIMEDOUT into the form') is not treated as a network timeout. It also walks the cause chain (and AggregateError.errors), which is what makes the fetch example above work: Node throws TypeError: fetch failed and puts the real network error in cause, where a message-only check would never find it.
ENOMEM and ENOSPC are deliberately not retried — memory and disk exhaustion do not clear because a caller tried again, and retrying adds load to a host already out of the resource.
Supply isRetryable to classify errors yourself. It replaces the built-in check rather than extending it; call isTransientError inside your predicate to keep both:
await withRetry(fn, {
isRetryable: (error) => isTransientError(error) || isRateLimited(error),
});
Circuit breaker probe limit. While HALF_OPEN, the breaker admits at most halfOpenMaxProbes calls at a time (default: halfOpenSuccessThreshold), and canExecute() returns false beyond that until the circuit transitions. The count is of probes in flight — admitted and not yet settled through recordSuccess/recordFailure.
Known limitation: a probe that never settles (the caller never reports its outcome — e.g. it hangs with no timeout of its own) holds its slot permanently. If enough probes hang to reach halfOpenMaxProbes, the circuit is wedged in HALF_OPEN indefinitely, with no time-based recovery — only an explicit reset() clears it. Give every probe you drive through the breaker its own timeout so it always settles.
Installation
npm install @forgrit/shared
Development
Use Node.js 20 or later. Install the locked development dependencies before making a change:
npm ci
Use a feature branch and open a merge request rather than pushing directly to main. See
CONTRIBUTING.md for the contribution workflow.
Verification
Run the complete local gate before opening a merge request:
npm run build
npm test
npm pack --dry-run
CI/CD
The default-branch pipeline verifies the package build and tests. It does not publish the package.
Release policy
Publishing remains manual. A maintainer reviews the package contents with npm pack --dry-run and
publishes only after the required verification succeeds.
Sibling projects
- Software Foundry builds product systems.
- Software Orchestrator validates and operates engineering work.
- Workforce enforces runtime authority and evidence.
- Design Corpus is the source of truth for authored design evidence.
Usage
Each subpath is independently importable:
// Import from design-system subpath
import { DESIGN_SYSTEM_SCHEMA_VERSION } from '@forgrit/shared/design-system';
import { ROLES, getRole } from '@forgrit/shared/design-system/contract';
import { DS } from '@forgrit/shared/design-system/tokens';
import '@forgrit/shared/design-system/css/tokens.css';
// Import from evidence subpath
import { /* ... */ } from '@forgrit/shared/evidence';
// Import from resilience subpath
import { /* ... */ } from '@forgrit/shared/resilience';
// Import from engineering-contract subpath
import { parseExecutionTicket } from '@forgrit/shared/engineering-contract';
There is no root export: every entry point above is a subpath, so require('@forgrit/shared')
fails with ERR_PACKAGE_PATH_NOT_EXPORTED by design.
License
MIT — see LICENSE.