quorumkit-engine 3.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/RELEASING.md ADDED
@@ -0,0 +1,151 @@
1
+ # QuorumKit Engine — Release Procedure
2
+
3
+ > **Audience:** maintainers cutting a release of `engine/`. Contributors should not need to read this.
4
+
5
+ This document is the operational counterpart to `engine/SECURITY.md` and
6
+ the authoritative procedure referenced by FR-010, FR-028, SEC-MED-004,
7
+ and SC-009 in `specs/047-repo-topology/spec.md`.
8
+
9
+ The engine ships through **two channels**, both released atomically from
10
+ the same `engine/` source on every signed `v*` tag:
11
+
12
+ 1. **GitHub Action ref** — consumers pin via
13
+ `uses: dmitry-nalivaika/quorumkit/engine@vX.Y.Z` (or `@vX` for floating major).
14
+ 2. **npm package `quorumkit-engine`** — published with `--provenance` via
15
+ OIDC trusted publishing (no `NPM_TOKEN`).
16
+
17
+ ---
18
+
19
+ ## 1. Release prerequisites (one-time setup)
20
+
21
+ | Setup task | Where | Who |
22
+ |---|---|---|
23
+ | Maintainer GPG key registered with GitHub & in the local keyring | `gpg --import` + `git config user.signingkey <FPR>` + `gpg --export <FPR>` uploaded to GitHub | Maintainer |
24
+ | `release` GitHub Environment with required-reviewer protection rule | Repo Settings → Environments → `release` | Maintainer (admin) |
25
+ | npm "Trusted Publisher" mapping for `quorumkit-engine` | <https://www.npmjs.com/package/quorumkit-engine/access> → Trusted Publishers → add this repo + workflow `engine-release.yml` | Maintainer + npm package owner |
26
+ | Branch-protection on `main` requires PR + verify-mirror green | Repo Settings → Branches → `main` | Maintainer (admin) |
27
+
28
+ If any of these are missing, the release workflow fails fast — you never
29
+ publish a half-configured artefact by accident.
30
+
31
+ ---
32
+
33
+ ## 2. Release procedure (per release)
34
+
35
+ ```bash
36
+ # 1. Make sure you are on a clean main with the new engine source committed.
37
+ git checkout main && git pull --ff-only
38
+ bash installer/verify-mirror.sh # M1–M9 must be green
39
+ (cd engine/orchestrator && npm test) # all engine tests pass
40
+ (cd engine && npm ci --ignore-scripts && npm run build)
41
+ git status # MUST be clean (dist/ in sync)
42
+
43
+ # 2. Bump quorumkit.yml (T-25) and CHANGELOG.md (T-24) in a release PR. Get review
44
+ # + Security Agent sign-off. Merge.
45
+
46
+ # 3. Cut a SIGNED tag at the merge commit on main.
47
+ git tag -s v3.0.0 -m "QuorumKit v3.0.0 — three-zone topology + engine distribution"
48
+ git push origin v3.0.0
49
+
50
+ # 4. The engine-release.yml workflow auto-runs:
51
+ # a. git verify-tag — refuses unsigned tags
52
+ # b. rebuild engine/dist/ via ncc — refuses tag → bundle drift
53
+ # c. npm publish --provenance — OIDC, no NPM_TOKEN
54
+ # d. github-script creates the GitHub Release with auto-notes
55
+ # The 'release' Environment requires a maintainer reviewer click before
56
+ # step (c) runs — that is the supply-chain gate.
57
+ ```
58
+
59
+ A successful run produces:
60
+
61
+ - A GitHub Release at <https://github.com/dmitry-nalivaika/quorumkit/releases/tag/vX.Y.Z>.
62
+ - An npm package at <https://www.npmjs.com/package/quorumkit-engine/v/X.Y.Z>
63
+ whose page shows the **"Built and signed on GitHub Actions"** provenance
64
+ badge with a link to the workflow run that produced it.
65
+ - A signed git tag verifiable with `git verify-tag vX.Y.Z`.
66
+
67
+ ---
68
+
69
+ ## 3. Verifying a release as a downstream user
70
+
71
+ ```bash
72
+ # Action ref (what consumer workflows pin):
73
+ gh api repos/dmitry-nalivaika/quorumkit/git/refs/tags/vX.Y.Z
74
+
75
+ # Tag signature:
76
+ git fetch --tags
77
+ git verify-tag vX.Y.Z
78
+
79
+ # npm provenance:
80
+ npm view quorumkit-engine@X.Y.Z --json | jq '.dist'
81
+ # Expect: dist.attestations.provenance is present and signed by the
82
+ # GitHub Actions OIDC issuer for this repository.
83
+ ```
84
+
85
+ The maintainer's GPG public key fingerprint is published at
86
+ `docs/architecture/adr-047-action-runtime.md` §Verifying-key. Rotate the
87
+ fingerprint there — and only there — when the maintainer's signing key
88
+ changes.
89
+
90
+ ---
91
+
92
+ ## 4. Floating major-version tag (`v3`)
93
+
94
+ After every patch / minor release on the `v3.x` series, fast-forward the
95
+ floating `v3` tag so consumers pinned to `@v3` automatically receive the
96
+ update:
97
+
98
+ ```bash
99
+ git tag -fs v3 -m "Track v3.0.1" # signed, force-update
100
+ git push --force origin v3
101
+ ```
102
+
103
+ Floating tags are **only** acceptable for first-party QuorumKit Actions inside
104
+ `templates/github/workflows/`. Third-party Actions remain SHA-pinned per
105
+ FR-031 / mirror gate M9 — Dependabot rotates them.
106
+
107
+ ---
108
+
109
+ ## 5. Rollback
110
+
111
+ If a release ships a regression:
112
+
113
+ 1. **Yank the npm version** (does not delete; warns installers):
114
+ ```bash
115
+ npm deprecate quorumkit-engine@X.Y.Z "Yanked — see #<issue>; upgrade to X.Y.Z+1"
116
+ ```
117
+ 2. **Re-publish the prior known-good** as the latest dist-tag:
118
+ ```bash
119
+ npm dist-tag add quorumkit-engine@X.Y.(Z-1) latest
120
+ ```
121
+ 3. **Move the floating major tag back** so consumers pinned to `@v3`
122
+ pick up the rollback on their next workflow run:
123
+ ```bash
124
+ git tag -fs v3 -m "Rollback to vX.Y.(Z-1)" <good-commit-sha>
125
+ git push --force origin v3
126
+ ```
127
+ 4. Open an incident issue with the `incident` label so the Incident Agent
128
+ timestamps the postmortem (per `.apm/agents/incident-agent.md`).
129
+ 5. Cut a fixed `vX.Y.(Z+1)` per §2 once the regression is patched.
130
+
131
+ `npm unpublish` is **forbidden** — it breaks reproducibility for any
132
+ consumer who already pinned to the bad version. Always deprecate + supersede.
133
+
134
+ ---
135
+
136
+ ## 6. Disaster recovery — fallback NPM token (SEC-HIGH-001)
137
+
138
+ If npm trusted publishing is unavailable (e.g. a registry-side outage or
139
+ the maintainer needs to publish from an air-gapped host), and only then,
140
+ a fallback token MAY be used under the following constraints:
141
+
142
+ - **Granular**, package-scoped (`quorumkit-engine` only), publish-only.
143
+ - **≤90-day expiry** — never rotated to a longer lifetime.
144
+ - Stored in the `release` GitHub Environment **only**; never in repo
145
+ secrets, never in `.env`, never in a maintainer's shell history.
146
+ - Rotation owner: the maintainer named in `quorumkit.yml` `owner.npm`.
147
+ - Each use of the fallback MUST be paired with a public Security Agent
148
+ comment on the corresponding incident issue.
149
+
150
+ Removing the fallback token after the trusted-publishing path is restored
151
+ is the responsibility of the same maintainer.
package/SECURITY.md ADDED
@@ -0,0 +1,82 @@
1
+ # QuorumKit Engine — Security & Permissions
2
+
3
+ > **Audience:** consumer-repo maintainers wiring `uses: dmitry-nalivaika/quorumkit/engine@<sha>` into a workflow, and QuorumKit contributors changing engine behaviour.
4
+
5
+ This document is the security contract of the QuorumKit engine Action. It is
6
+ authoritative for FR-014, ADR-047 amendment §4, and SEC-MED-001. Every
7
+ permission the engine consumes MUST appear in the table below with a
8
+ written justification; reviewers reject changes that broaden the table
9
+ without a paired spec amendment.
10
+
11
+ ---
12
+
13
+ ## 1. Default permission posture
14
+
15
+ The engine declares **zero** permissions in `engine/action.yml`. Permissions
16
+ are granted at the **call site** by each consumer workflow. The minimum
17
+ viable token for a feature pipeline is:
18
+
19
+ ```yaml
20
+ permissions:
21
+ contents: read # default — required for actions/checkout
22
+ issues: write # post audit comments, update labels
23
+ pull-requests: write # comment on PRs and update review state
24
+ actions: read # read workflow_run payloads
25
+ ```
26
+
27
+ The engine **MUST NOT** be invoked with `permissions: write-all`.
28
+ `installer/init.sh --upgrade` (T-20) refuses to broaden the consumer's
29
+ existing `permissions:` block.
30
+
31
+ ---
32
+
33
+ ## 2. Per-scope justification table
34
+
35
+ | Scope | Default access | Why the engine needs it | When to omit |
36
+ | ---------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
37
+ | `contents: read` | required | `actions/checkout` reads the consumer repo to load `.apm/pipelines/`, `.apm/agents/`, and other SoT files. | never — without this the engine cannot start. |
38
+ | `contents: write` | **off** | Only enable for consumer pipelines that include a step writing files via the orchestrator (e.g. dashboard regen). Most pipelines don't. | default — keep `contents: read`. |
39
+ | `issues: write` | required | Post audit comments (FR-026, ADR-002), `<QUORUMKIT-LIVE-STATUS>` updates, and label changes that drive the state machine. | only if the consumer does not use issue-driven pipelines. |
40
+ | `pull-requests: write` | required | Comment on PR threads, update review labels, and post timeline reconstructions. | only if the consumer disables PR-triggered pipelines. |
41
+ | `actions: read` | required | Read `workflow_run` payloads to recover issue context (ADR-007 §6) and to re-correlate audit comments after agent failures. | only if the pipeline never depends on `workflow_run` events. |
42
+ | `actions: write` | **off** | Never required by the engine itself. Forbid in consumer workflows unless a custom step explicitly needs `gh workflow run` capability. | always — no engine code path requests it. |
43
+ | `id-token: write` | release-only | Used **only** by the engine's own release workflow (`engine-release.yml`) for OIDC-trusted npm publishing. Never granted to consumer workflows. | always for consumers. |
44
+ | `packages: read/write` | **off** | The engine does not pull or push packages from `ghcr.io`. | always — keep off. |
45
+ | `deployments: write` | **off** | The engine never creates a Deployment resource. | always — keep off. |
46
+ | `security-events: write` | **off** | Code scanning / Dependabot is enforced by `dependabot.yml` and the GitHub-hosted scanners, not by the engine. | always — keep off. |
47
+
48
+ ---
49
+
50
+ ## 3. Threat model snapshot (SEC-MED-001 / SEC-HIGH-001)
51
+
52
+ | Threat | Mitigation | Tracked in |
53
+ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
54
+ | Compromised third-party `uses:` ref | All third-party Actions SHA-pinned (FR-031, M9 mirror gate). Dependabot opens PRs for tag→SHA rotation. | `installer/verify-mirror.sh` M9 |
55
+ | Untrusted YAML in `.apm/pipelines/**` deserialising arbitrary code | `yaml.load` pinned to `CORE_SCHEMA`; tag-aware loaders banned. Negative test: `engine/tests/api-version.test.js` `!!js/function` case. | FR-013, T-10 |
56
+ | Pipeline declares `apiVersion` newer than the pinned engine | `assertApiVersionSupported` fails fast with a remediation message naming both versions. | FR-013, T-10 |
57
+ | Workflow run with `permissions: write-all` (CWE-732 over-grant) | `installer/init.sh --upgrade` refuses to widen `permissions:` blocks; engine itself requests nothing. | FR-024, SEC-MED-002, T-20 |
58
+ | Long-lived `NPM_TOKEN` in repo secrets | Release workflow uses **OIDC trusted publishing + `npm publish --provenance`**; no `NPM_TOKEN` ever written to env. | SEC-HIGH-001, T-12 |
59
+ | Tampered release artefact | Signed `v*` tags + SLSA build provenance attached by `npm publish --provenance`. Verifying key documented in `engine/RELEASING.md`. | SC-009, SEC-MED-004, T-12, T-23 |
60
+ | Engine-bundle drift (unreviewed `dist/` change) | `engine-build-gate.yml` rebuilds via `ncc` on every PR touching `engine/**` and fails if `git diff --quiet engine/dist/` is non-empty. | FR-009, T-09 |
61
+
62
+ ---
63
+
64
+ ## 4. Reporting a vulnerability
65
+
66
+ Use GitHub's private vulnerability reporting on this repository
67
+ (<https://github.com/dmitry-nalivaika/quorumkit/security>) — do **not** open a
68
+ public issue. The Security Agent owns triage SLA and CVE assignment per
69
+ `SECURITY.md` at the repo root.
70
+
71
+ ---
72
+
73
+ ## 5. Change-control rules
74
+
75
+ 1. **Adding a permission scope to the table above** requires a paired
76
+ spec change (`specs/047-repo-topology/spec.md` or a successor) and
77
+ Security Agent sign-off on the PR.
78
+ 2. **Removing a scope** requires a deprecation note in `CHANGELOG.md`
79
+ and a major-version bump if any documented consumer pipeline used it.
80
+ 3. **Tag-aware YAML loading is forbidden** — any reintroduction of
81
+ `yaml.load(raw)` without `{ schema: yaml.CORE_SCHEMA }` (or stricter)
82
+ is a Security Agent veto.
package/action.yml ADDED
@@ -0,0 +1,25 @@
1
+ # =============================================================================
2
+ # QuorumKit Engine — Composite GitHub Action (Issue #47, ADR-047 amendment §1)
3
+ #
4
+ # Runtime: Node 20 (FR-008, AR-D §6).
5
+ # Bundle: dist/index.js — produced by `npm run build` (ncc) and committed
6
+ # (FR-009). CI re-runs the build and fails if `git diff --quiet
7
+ # dist/` is non-empty (engine-build-gate workflow).
8
+ #
9
+ # Inputs: none — the engine reads everything from the GitHub event payload
10
+ # and the consumer-side `.apm/` SoT directory (ADR-006 §3).
11
+ # Permissions: declared per-call by the consumer workflow; this Action does
12
+ # NOT request any. The minimum-viable scope is `contents: read`
13
+ # + `issues: write` + `pull-requests: write` (engine/SECURITY.md).
14
+ # =============================================================================
15
+ name: 'QuorumKit Engine'
16
+ description: 'Run the QuorumKit autonomous-agent orchestrator on a GitHub event payload.'
17
+ author: 'QuorumKit contributors'
18
+
19
+ branding:
20
+ icon: 'cpu'
21
+ color: 'purple'
22
+
23
+ runs:
24
+ using: 'node20'
25
+ main: 'dist/index.js'
@@ -0,0 +1,171 @@
1
+ export const id = 212;
2
+ export const ids = [212,563];
3
+ export const modules = {
4
+
5
+ /***/ 3563:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ isRetryable: () => (/* binding */ isRetryable),
11
+ /* harmony export */ withRetry: () => (/* binding */ withRetry)
12
+ /* harmony export */ });
13
+ /**
14
+ * runtimes/_retry.js
15
+ * Bounded exponential-backoff retry helper shared by all runtime adapters
16
+ * (ADR-007 §8, FR-030).
17
+ *
18
+ * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.
19
+ * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).
20
+ * Non-retryable errors are rethrown immediately.
21
+ *
22
+ * Returns the function's resolved value plus the number of retries used,
23
+ * so the caller can record `runtime_retries` in the audit comment without
24
+ * counting the invocation as a separate loop-budget iteration.
25
+ */
26
+
27
+ const DEFAULT_OPTS = Object.freeze({
28
+ maxAttempts: 3,
29
+ baseMs: 2_000,
30
+ maxTotalMs: 30_000,
31
+ jitter: 0.25,
32
+ });
33
+
34
+ /**
35
+ * Determine whether an error is retryable.
36
+ * @param {any} err
37
+ * @returns {boolean}
38
+ */
39
+ function isRetryable(err) {
40
+ if (!err) return false;
41
+ // Octokit / fetch error styles
42
+ if (err.status === 429) return true;
43
+ if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;
44
+ // Node fetch network errors
45
+ const code = err.code ?? err.cause?.code;
46
+ if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;
47
+ if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Run `fn` with bounded exponential backoff.
53
+ *
54
+ * @template T
55
+ * @param {() => Promise<T>} fn
56
+ * @param {object} [options]
57
+ * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }
58
+ * @returns {Promise<{ value: T, retries: number }>}
59
+ */
60
+ async function withRetry(fn, options = {}) {
61
+ const opts = { ...DEFAULT_OPTS, ...options };
62
+ const clock = options.clock ?? defaultClock;
63
+
64
+ const start = clock.now();
65
+ let attempt = 0;
66
+ let lastError;
67
+
68
+ while (attempt < opts.maxAttempts) {
69
+ try {
70
+ const value = await fn();
71
+ return { value, retries: attempt };
72
+ } catch (err) {
73
+ lastError = err;
74
+ attempt += 1;
75
+ if (!isRetryable(err) || attempt >= opts.maxAttempts) break;
76
+
77
+ const elapsed = clock.now() - start;
78
+ if (elapsed >= opts.maxTotalMs) break;
79
+
80
+ const exp = Math.pow(2, attempt - 1) * opts.baseMs;
81
+ const jitter = exp * opts.jitter * (Math.random() * 2 - 1);
82
+ const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));
83
+ await clock.sleep(wait);
84
+ }
85
+ }
86
+
87
+ throw lastError;
88
+ }
89
+
90
+ const defaultClock = {
91
+ now: () => Date.now(),
92
+ sleep: ms => new Promise(r => setTimeout(r, ms)),
93
+ };
94
+
95
+
96
+ /***/ }),
97
+
98
+ /***/ 1212:
99
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
100
+
101
+ __webpack_require__.r(__webpack_exports__);
102
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
103
+ /* harmony export */ KIND: () => (/* binding */ KIND),
104
+ /* harmony export */ invoke: () => (/* binding */ invoke),
105
+ /* harmony export */ requiredPermissions: () => (/* binding */ requiredPermissions)
106
+ /* harmony export */ });
107
+ /* harmony import */ var _retry_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3563);
108
+ /**
109
+ * runtimes/claude.js
110
+ * Adapter for the Claude (Anthropic) runtime kind (ADR-005, ADR-002).
111
+ *
112
+ * Dispatches the agent's Claude workflow file (`agent-<slug>.yml`) through
113
+ * the supplied client. Mirrors copilot.js with a smaller required-permissions
114
+ * set (no `models:` scope — Claude calls go to api.anthropic.com).
115
+ */
116
+
117
+
118
+
119
+ const KIND = 'claude';
120
+
121
+ const requiredPermissions = Object.freeze({
122
+ contents: 'read',
123
+ issues: 'write',
124
+ 'pull-requests': 'write',
125
+ });
126
+
127
+ function resolveCredential(runtime, env = process.env) {
128
+ const ref = runtime.credential_ref;
129
+ if (!ref) {
130
+ const e = new Error('runtime-credential-missing');
131
+ e.code = 'runtime-credential-missing';
132
+ e.credential_ref = '(none declared)';
133
+ throw e;
134
+ }
135
+ const value = env[ref];
136
+ if (!value) {
137
+ const e = new Error('runtime-credential-missing');
138
+ e.code = 'runtime-credential-missing';
139
+ e.credential_ref = ref;
140
+ throw e;
141
+ }
142
+ return value;
143
+ }
144
+
145
+ async function invoke(context) {
146
+ const env = context.env ?? process.env;
147
+ resolveCredential(context.runtime, env);
148
+
149
+ const workflow = `agent-${context.agent}.yml`;
150
+ const dispatchRef = context.ref ?? 'main';
151
+ const inputs = {
152
+ issue_number: String(context.issueNumber),
153
+ run_id: context.runId ?? '',
154
+ step: context.step ?? '',
155
+ iteration: String(context.iteration ?? 1),
156
+ runtime: context.runtimeName ?? '',
157
+ };
158
+
159
+ const { retries } = await (0,_retry_js__WEBPACK_IMPORTED_MODULE_0__.withRetry)(
160
+ () => context.client.triggerWorkflow(context.owner, context.repo, workflow, dispatchRef, inputs),
161
+ { clock: context.clock }
162
+ );
163
+ return { dispatched: true, retries, workflow };
164
+ }
165
+
166
+
167
+ /***/ })
168
+
169
+ };
170
+
171
+ //# sourceMappingURL=212.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"212.index.js","mappings":";;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AChFA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././orchestrator/runtimes/_retry.js",".././orchestrator/runtimes/claude.js"],"sourcesContent":["/**\n * runtimes/_retry.js\n * Bounded exponential-backoff retry helper shared by all runtime adapters\n * (ADR-007 §8, FR-030).\n *\n * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.\n * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).\n * Non-retryable errors are rethrown immediately.\n *\n * Returns the function's resolved value plus the number of retries used,\n * so the caller can record `runtime_retries` in the audit comment without\n * counting the invocation as a separate loop-budget iteration.\n */\n\nconst DEFAULT_OPTS = Object.freeze({\n maxAttempts: 3,\n baseMs: 2_000,\n maxTotalMs: 30_000,\n jitter: 0.25,\n});\n\n/**\n * Determine whether an error is retryable.\n * @param {any} err\n * @returns {boolean}\n */\nexport function isRetryable(err) {\n if (!err) return false;\n // Octokit / fetch error styles\n if (err.status === 429) return true;\n if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;\n // Node fetch network errors\n const code = err.code ?? err.cause?.code;\n if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;\n if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;\n return false;\n}\n\n/**\n * Run `fn` with bounded exponential backoff.\n *\n * @template T\n * @param {() => Promise<T>} fn\n * @param {object} [options]\n * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }\n * @returns {Promise<{ value: T, retries: number }>}\n */\nexport async function withRetry(fn, options = {}) {\n const opts = { ...DEFAULT_OPTS, ...options };\n const clock = options.clock ?? defaultClock;\n\n const start = clock.now();\n let attempt = 0;\n let lastError;\n\n while (attempt < opts.maxAttempts) {\n try {\n const value = await fn();\n return { value, retries: attempt };\n } catch (err) {\n lastError = err;\n attempt += 1;\n if (!isRetryable(err) || attempt >= opts.maxAttempts) break;\n\n const elapsed = clock.now() - start;\n if (elapsed >= opts.maxTotalMs) break;\n\n const exp = Math.pow(2, attempt - 1) * opts.baseMs;\n const jitter = exp * opts.jitter * (Math.random() * 2 - 1);\n const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));\n await clock.sleep(wait);\n }\n }\n\n throw lastError;\n}\n\nconst defaultClock = {\n now: () => Date.now(),\n sleep: ms => new Promise(r => setTimeout(r, ms)),\n};\n","/**\n * runtimes/claude.js\n * Adapter for the Claude (Anthropic) runtime kind (ADR-005, ADR-002).\n *\n * Dispatches the agent's Claude workflow file (`agent-<slug>.yml`) through\n * the supplied client. Mirrors copilot.js with a smaller required-permissions\n * set (no `models:` scope — Claude calls go to api.anthropic.com).\n */\n\nimport { withRetry } from './_retry.js';\n\nexport const KIND = 'claude';\n\nexport const requiredPermissions = Object.freeze({\n contents: 'read',\n issues: 'write',\n 'pull-requests': 'write',\n});\n\nfunction resolveCredential(runtime, env = process.env) {\n const ref = runtime.credential_ref;\n if (!ref) {\n const e = new Error('runtime-credential-missing');\n e.code = 'runtime-credential-missing';\n e.credential_ref = '(none declared)';\n throw e;\n }\n const value = env[ref];\n if (!value) {\n const e = new Error('runtime-credential-missing');\n e.code = 'runtime-credential-missing';\n e.credential_ref = ref;\n throw e;\n }\n return value;\n}\n\nexport async function invoke(context) {\n const env = context.env ?? process.env;\n resolveCredential(context.runtime, env);\n\n const workflow = `agent-${context.agent}.yml`;\n const dispatchRef = context.ref ?? 'main';\n const inputs = {\n issue_number: String(context.issueNumber),\n run_id: context.runId ?? '',\n step: context.step ?? '',\n iteration: String(context.iteration ?? 1),\n runtime: context.runtimeName ?? '',\n };\n\n const { retries } = await withRetry(\n () => context.client.triggerWorkflow(context.owner, context.repo, workflow, dispatchRef, inputs),\n { clock: context.clock }\n );\n return { dispatched: true, retries, workflow };\n}\n"],"names":[],"sourceRoot":""}
@@ -0,0 +1,100 @@
1
+ export const id = 563;
2
+ export const ids = [563];
3
+ export const modules = {
4
+
5
+ /***/ 3563:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ isRetryable: () => (/* binding */ isRetryable),
11
+ /* harmony export */ withRetry: () => (/* binding */ withRetry)
12
+ /* harmony export */ });
13
+ /**
14
+ * runtimes/_retry.js
15
+ * Bounded exponential-backoff retry helper shared by all runtime adapters
16
+ * (ADR-007 §8, FR-030).
17
+ *
18
+ * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.
19
+ * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).
20
+ * Non-retryable errors are rethrown immediately.
21
+ *
22
+ * Returns the function's resolved value plus the number of retries used,
23
+ * so the caller can record `runtime_retries` in the audit comment without
24
+ * counting the invocation as a separate loop-budget iteration.
25
+ */
26
+
27
+ const DEFAULT_OPTS = Object.freeze({
28
+ maxAttempts: 3,
29
+ baseMs: 2_000,
30
+ maxTotalMs: 30_000,
31
+ jitter: 0.25,
32
+ });
33
+
34
+ /**
35
+ * Determine whether an error is retryable.
36
+ * @param {any} err
37
+ * @returns {boolean}
38
+ */
39
+ function isRetryable(err) {
40
+ if (!err) return false;
41
+ // Octokit / fetch error styles
42
+ if (err.status === 429) return true;
43
+ if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;
44
+ // Node fetch network errors
45
+ const code = err.code ?? err.cause?.code;
46
+ if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;
47
+ if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Run `fn` with bounded exponential backoff.
53
+ *
54
+ * @template T
55
+ * @param {() => Promise<T>} fn
56
+ * @param {object} [options]
57
+ * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }
58
+ * @returns {Promise<{ value: T, retries: number }>}
59
+ */
60
+ async function withRetry(fn, options = {}) {
61
+ const opts = { ...DEFAULT_OPTS, ...options };
62
+ const clock = options.clock ?? defaultClock;
63
+
64
+ const start = clock.now();
65
+ let attempt = 0;
66
+ let lastError;
67
+
68
+ while (attempt < opts.maxAttempts) {
69
+ try {
70
+ const value = await fn();
71
+ return { value, retries: attempt };
72
+ } catch (err) {
73
+ lastError = err;
74
+ attempt += 1;
75
+ if (!isRetryable(err) || attempt >= opts.maxAttempts) break;
76
+
77
+ const elapsed = clock.now() - start;
78
+ if (elapsed >= opts.maxTotalMs) break;
79
+
80
+ const exp = Math.pow(2, attempt - 1) * opts.baseMs;
81
+ const jitter = exp * opts.jitter * (Math.random() * 2 - 1);
82
+ const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));
83
+ await clock.sleep(wait);
84
+ }
85
+ }
86
+
87
+ throw lastError;
88
+ }
89
+
90
+ const defaultClock = {
91
+ now: () => Date.now(),
92
+ sleep: ms => new Promise(r => setTimeout(r, ms)),
93
+ };
94
+
95
+
96
+ /***/ })
97
+
98
+ };
99
+
100
+ //# sourceMappingURL=563.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"563.index.js","mappings":";;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././orchestrator/runtimes/_retry.js"],"sourcesContent":["/**\n * runtimes/_retry.js\n * Bounded exponential-backoff retry helper shared by all runtime adapters\n * (ADR-007 §8, FR-030).\n *\n * Policy (defaults): 3 attempts, base 2 s, jitter ±25 %, max wait 30 s total.\n * Retries are triggered by network errors, HTTP 5xx, and HTTP 429 (rate-limit).\n * Non-retryable errors are rethrown immediately.\n *\n * Returns the function's resolved value plus the number of retries used,\n * so the caller can record `runtime_retries` in the audit comment without\n * counting the invocation as a separate loop-budget iteration.\n */\n\nconst DEFAULT_OPTS = Object.freeze({\n maxAttempts: 3,\n baseMs: 2_000,\n maxTotalMs: 30_000,\n jitter: 0.25,\n});\n\n/**\n * Determine whether an error is retryable.\n * @param {any} err\n * @returns {boolean}\n */\nexport function isRetryable(err) {\n if (!err) return false;\n // Octokit / fetch error styles\n if (err.status === 429) return true;\n if (typeof err.status === 'number' && err.status >= 500 && err.status < 600) return true;\n // Node fetch network errors\n const code = err.code ?? err.cause?.code;\n if (code && /^(ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|UND_ERR_SOCKET)$/.test(code)) return true;\n if (typeof err.message === 'string' && /timeout|network/i.test(err.message)) return true;\n return false;\n}\n\n/**\n * Run `fn` with bounded exponential backoff.\n *\n * @template T\n * @param {() => Promise<T>} fn\n * @param {object} [options]\n * @param {object} [options.clock] - injectable clock { now(): number, sleep(ms): Promise<void> }\n * @returns {Promise<{ value: T, retries: number }>}\n */\nexport async function withRetry(fn, options = {}) {\n const opts = { ...DEFAULT_OPTS, ...options };\n const clock = options.clock ?? defaultClock;\n\n const start = clock.now();\n let attempt = 0;\n let lastError;\n\n while (attempt < opts.maxAttempts) {\n try {\n const value = await fn();\n return { value, retries: attempt };\n } catch (err) {\n lastError = err;\n attempt += 1;\n if (!isRetryable(err) || attempt >= opts.maxAttempts) break;\n\n const elapsed = clock.now() - start;\n if (elapsed >= opts.maxTotalMs) break;\n\n const exp = Math.pow(2, attempt - 1) * opts.baseMs;\n const jitter = exp * opts.jitter * (Math.random() * 2 - 1);\n const wait = Math.max(0, Math.min(exp + jitter, opts.maxTotalMs - elapsed));\n await clock.sleep(wait);\n }\n }\n\n throw lastError;\n}\n\nconst defaultClock = {\n now: () => Date.now(),\n sleep: ms => new Promise(r => setTimeout(r, ms)),\n};\n"],"names":[],"sourceRoot":""}