owlrunkit 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +49 -17
- package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +28 -4
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +4 -4
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +5 -0
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +3 -1
- package/scripts/runkit-contract/core-contract.mjs +1 -1
- package/scripts/runkit-contract/onboarding-doctor.mjs +46 -18
- package/scripts/runkit-contract/profile-onboarding.mjs +4 -0
- package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
- package/scripts/runkit-contract/quick-receipt.mjs +43 -5
- package/scripts/runkit-contract/quick-verify.mjs +5 -0
- package/scripts/runkit-contract/runkit-cli.mjs +83 -17
- package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +2 -0
- package/scripts/runkit-contract/usage-mode.mjs +11 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.2
|
|
4
|
+
|
|
5
|
+
- Makes `project checkpoint --help` action-specific and returns a complete,
|
|
6
|
+
shell-safe correction command when a measured checkpoint omits
|
|
7
|
+
`--completed-units`.
|
|
8
|
+
- Keeps symlinked workspace roots fail-closed while reporting the canonical
|
|
9
|
+
`--workspace` path, including the macOS `/tmp` to `/private/tmp` case.
|
|
10
|
+
- Returns a bounded stdout/stderr `outputSummary` at the top level of Quick
|
|
11
|
+
results without changing the receipt or weakening full-output binding.
|
|
12
|
+
- Retries one transient registry transport failure while retaining the existing
|
|
13
|
+
per-attempt timeout, and distinguishes DNS, connection, timeout, HTTP,
|
|
14
|
+
authentication, and missing-version failures.
|
|
15
|
+
- Aligns the packaged README with five-mode routing and existing profile
|
|
16
|
+
reconciliation instead of treating long-running, release, or production
|
|
17
|
+
labels as automatic Formal triggers.
|
|
18
|
+
- Adds action-specific `quick-verify --help`, lists canonical risk-category
|
|
19
|
+
values in mode help and validation errors, and marks formatting-only profile
|
|
20
|
+
reconcile plans with `normalizationOnly: true`.
|
|
21
|
+
|
|
3
22
|
## 0.18.1
|
|
4
23
|
|
|
5
24
|
- Adds read-only `mode recommend` and `mode status` commands with five
|
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@ continuity and assurance independently. It keeps ordinary development light
|
|
|
11
11
|
while preserving mandatory Formal routing for material safety and authority
|
|
12
12
|
facts.
|
|
13
13
|
|
|
14
|
+
RunKit 0.18.2 is a focused dogfood patch for clearer CLI help, actionable
|
|
15
|
+
checkpoint and canonical-path errors, bounded Quick output summaries, typed
|
|
16
|
+
registry diagnostics, and explicit profile normalization previews.
|
|
17
|
+
|
|
14
18
|
The 0.18 Project Driver is a bounded surface for new projects and controlled
|
|
15
19
|
dogfood. Its product name does not claim to be a general long-running,
|
|
16
20
|
multi-phase, or enterprise control plane. Version 0.18 adds one bounded
|
|
@@ -25,7 +29,7 @@ separate authority boundaries.
|
|
|
25
29
|
## Install
|
|
26
30
|
|
|
27
31
|
```bash
|
|
28
|
-
npm install --save-exact owlrunkit@0.18.
|
|
32
|
+
npm install --save-exact owlrunkit@0.18.2
|
|
29
33
|
```
|
|
30
34
|
|
|
31
35
|
The `owlrunkit` bootstrap uses the project's exact locally bound CLI for
|
|
@@ -112,9 +116,10 @@ Normal users supply the one definition JSON to `init`, then use typed flags and
|
|
|
112
116
|
values. They do not need to author request, event, receipt, or hash JSON. The
|
|
113
117
|
legacy `--request <event.json>` path remains available for existing automation
|
|
114
118
|
and compatibility. Multiple Agents do not make project coordination Formal by
|
|
115
|
-
themselves.
|
|
116
|
-
assurance lane follows
|
|
117
|
-
|
|
119
|
+
themselves. Project Driver continuity does not make project coordination Formal
|
|
120
|
+
by itself. The assurance lane follows actual writer and authority count,
|
|
121
|
+
rollback certainty, permissions, security, funds, irreversibility, schema/data
|
|
122
|
+
migration, and explicit acceptance contracts—not environment labels alone.
|
|
118
123
|
|
|
119
124
|
`project verification --defer` is a small deferred-verification ledger, not a
|
|
120
125
|
test scheduler. It records stable check IDs, why they were intentionally not
|
|
@@ -166,30 +171,52 @@ high-confidence candidates:
|
|
|
166
171
|
```bash
|
|
167
172
|
owlrunkit profiles detect --workspace . --dry-run
|
|
168
173
|
owlrunkit profiles detect --workspace . --apply
|
|
174
|
+
owlrunkit profiles reconcile --workspace . --dry-run
|
|
175
|
+
owlrunkit profiles reconcile --workspace . --apply
|
|
169
176
|
owlrunkit profiles validate --workspace .
|
|
170
177
|
```
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
and
|
|
176
|
-
|
|
179
|
+
`profiles detect --apply` is create-only. `profiles reconcile` previews or
|
|
180
|
+
atomically updates an existing profile set and leaves a migration receipt. Both
|
|
181
|
+
paths refuse ambiguous or low-confidence candidates, source drift, symlinks,
|
|
182
|
+
and launchers that do not resolve to exact local files. Detection directly
|
|
183
|
+
supports lock-bound npm plus exact project-installed pnpm, Yarn, and Bun
|
|
184
|
+
launchers. A PATH-only package manager remains review-required. A dry-run that
|
|
185
|
+
only canonicalizes JSON formatting reports `normalizationOnly: true` instead of
|
|
186
|
+
showing unexplained hash drift with an empty update list.
|
|
187
|
+
|
|
188
|
+
## Choose auto, off, light, managed, or formal
|
|
189
|
+
|
|
190
|
+
Start with the human-first read-only recommendation:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
owlrunkit mode recommend --workspace .
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **off / No RunKit** uses neither continuity nor assurance for analysis,
|
|
197
|
+
documentation, and disposable exploration that needs no reusable evidence.
|
|
198
|
+
- **light / Quick Verification** runs one exact low-risk, source-bound command.
|
|
199
|
+
- **managed** adds Project Driver continuity; each WorkItem may still use no
|
|
200
|
+
assurance, Quick, or Formal according to its own facts.
|
|
201
|
+
- **formal / Formal Delivery** is forced by multiple writers, permission or
|
|
202
|
+
security boundaries, funds, irreversible work, uncertain rollback,
|
|
203
|
+
schema/data migration, or an explicit Formal acceptance contract.
|
|
204
|
+
- **auto** derives the preset from supplied facts and is the default.
|
|
177
205
|
|
|
178
|
-
|
|
206
|
+
Production, release, deployment, long duration, and interruption recovery do
|
|
207
|
+
not force Formal by themselves. Long or interrupted work primarily selects
|
|
208
|
+
Project Driver continuity. Risk containment permits a fresh lower-assurance
|
|
209
|
+
recommendation; escalation is not sticky.
|
|
179
210
|
|
|
180
|
-
|
|
211
|
+
Machine integrations may route an explicit risk-facts document directly:
|
|
181
212
|
|
|
182
213
|
```bash
|
|
183
214
|
owlrunkit assurance route --workspace . \
|
|
184
215
|
--request ./scripts/runkit-contract/templates/assurance-request.json
|
|
185
216
|
```
|
|
186
217
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- **Quick Verification** is for one low-risk, source-bound command that needs a
|
|
190
|
-
reusable receipt but no writer lease or Formal acceptance.
|
|
191
|
-
- **Formal Delivery** is for multi-writer, long-running, migration, integration,
|
|
192
|
-
release, production, funds, destructive, or formally accepted work.
|
|
218
|
+
`mode recommend --help` lists every accepted `--risk-category`; an invalid
|
|
219
|
+
category reports both the rejected value and the canonical allowed list.
|
|
193
220
|
|
|
194
221
|
The router fails closed to Formal when required risk facts are incomplete.
|
|
195
222
|
Quick receipts cannot become Formal acceptance, and Formal acceptance does not
|
|
@@ -201,6 +228,11 @@ Run Quick with:
|
|
|
201
228
|
owlrunkit quick-verify --workspace . -- npm test
|
|
202
229
|
```
|
|
203
230
|
|
|
231
|
+
The top-level Quick result includes a bounded `outputSummary` with the last
|
|
232
|
+
meaningful stdout/stderr lines, so common summaries such as `155 passed` are
|
|
233
|
+
visible without opening `stdout.log`. The receipt still binds the complete
|
|
234
|
+
stdout and stderr artifacts; the summary is presentation only.
|
|
235
|
+
|
|
204
236
|
## Formal Delivery in three commands
|
|
205
237
|
|
|
206
238
|
Given a Goal Contract and the paths this work item may write, the happy path is:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OwlCoda RunKit 使用模式选择:Sierac MES Dogfood 产品决议
|
|
2
2
|
|
|
3
|
-
状态:**0.18.1
|
|
3
|
+
状态:**0.18.1 已发布;本文主体保留发布前产品决议,文末追加 0.18.2 dogfood 补丁候选**
|
|
4
4
|
日期:2026-08-11
|
|
5
5
|
|
|
6
6
|
## 已验证现场事实
|
|
@@ -125,8 +125,9 @@ global gate 仍正确失效。
|
|
|
125
125
|
|
|
126
126
|
## 不能声称的内容
|
|
127
127
|
|
|
128
|
-
-
|
|
129
|
-
|
|
128
|
+
- 本文主体形成时记录的是 0.18.1 发布前候选;0.18.1 后续发布事实以
|
|
129
|
+
`docs/PRODUCT-TRUTH.md` 和独立 release receipt 为准。本文本身不证明 npm、Core、
|
|
130
|
+
Skill 或任何项目已升级。
|
|
130
131
|
- `mode recommend` 根据调用者提供的结构化事实路由;它不会自动发现所有业务、
|
|
131
132
|
权限、资金或迁移风险。
|
|
132
133
|
- RunKit 降低进度、证据和完成状态幻觉,不直接解决 Sierac MES 业务问答幻觉。
|
|
@@ -136,7 +137,30 @@ global gate 仍正确失效。
|
|
|
136
137
|
|
|
137
138
|
## 版本建议
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
本组变更当时适合形成 `owlrunkit@0.18.1` 小版本候选:它保持 0.18 的 Project Driver
|
|
140
141
|
和回执合同,新增只读 CLI,并修复 dogfood 验证出的行为缺陷。候选只有在聚焦测试、
|
|
141
142
|
RunKit contract/package gate、Core 身份同步、pack 内容检查和安装 smoke 都通过后
|
|
142
143
|
才具备发布评审资格;发布、项目升级与运行时激活仍是之后的独立动作。
|
|
144
|
+
|
|
145
|
+
## 0.18.2 dogfood 补丁追加
|
|
146
|
+
|
|
147
|
+
0.18.1 发布后,Sierac MES 与 OwlChen 的项目级使用继续证明 `managed + quick` 是普通
|
|
148
|
+
跨会话源码 Wave 的合适默认,Formal 只应由真实高风险事实触发。OwlChen 已读回项目
|
|
149
|
+
本地 0.18.1 的 doctor、profiles、Formal、Quick、metrics 与模式选择器主路径;进程级
|
|
150
|
+
共享 Skill 因另一项目存在 active execution/lease 而停止升级,这是正确的安全边界,
|
|
151
|
+
不是缺陷。
|
|
152
|
+
|
|
153
|
+
这些后续 dogfood 暴露的缺口适合形成 0.18.2 patch,而不是新增平台能力:
|
|
154
|
+
|
|
155
|
+
- symlink workspace 拒绝时直接给出 canonical `--workspace`,包括 macOS
|
|
156
|
+
`/tmp` 到 `/private/tmp`;
|
|
157
|
+
- `project checkpoint --help` 和 measured WorkItem 缺参错误给出完整命令;
|
|
158
|
+
- Quick 顶层返回有界 stdout/stderr 摘要,同时完整日志继续由回执绑定;
|
|
159
|
+
- doctor 对 registry transport 做一次重试、每次保留原有超时上限,并区分 DNS、连接、
|
|
160
|
+
超时、HTTP、认证和版本缺失;
|
|
161
|
+
- `quick-verify --help` 可用,risk category 的帮助与错误列出同一组规范值;
|
|
162
|
+
- profile 纯格式规范化计划显式返回 `normalizationOnly: true`;
|
|
163
|
+
- package README 与五模式、profile reconcile 的已实现真值一致。
|
|
164
|
+
|
|
165
|
+
当前这些内容只是本地 `owlrunkit@0.18.2` 候选。它不代表 commit、tag、npm 发布、
|
|
166
|
+
全局 Skill 更新、OwlChen/OwlFootball 迁移或任何业务授权已经发生。
|
|
@@ -7,10 +7,10 @@ description: Run receipt-backed, project-owned work with OwlCoda RunKit. Use whe
|
|
|
7
7
|
|
|
8
8
|
Use the bundled deterministic Core to coordinate work through project artifacts rather than chat memory. Require Node.js 20 or later.
|
|
9
9
|
|
|
10
|
-
Bundled release: standalone `owlrunkit@0.18.
|
|
11
|
-
Core `0.18.
|
|
12
|
-
`sha256:
|
|
13
|
-
The published `owlrunkit@0.18.
|
|
10
|
+
Bundled release: standalone `owlrunkit@0.18.2`, bundling Contract `0.2` and
|
|
11
|
+
Core `0.18.2` with manifest
|
|
12
|
+
`sha256:abe17ff794b4019d8d476f09eb5d4105bae1070b1df0aab53e6c94c7ec8b65a6`.
|
|
13
|
+
The published `owlrunkit@0.18.1` package is the trusted prior for rollback;
|
|
14
14
|
verify exact official npm registry provenance before adoption. The immutable
|
|
15
15
|
rollback baseline for the prior release is `owlrunkit@0.16.1` with SHA-1
|
|
16
16
|
`1bc119d24879bdd8959af16b364af55ca9d61ea9` and canonical npm tarball URL
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schemaVersion": "OwlCodaRunKitConfigV2",
|
|
3
3
|
"core": {
|
|
4
4
|
"contractVersion": "0.2",
|
|
5
|
-
"coreVersion": "0.18.
|
|
6
|
-
"coreManifestSha256": "sha256:
|
|
7
|
-
"coreSourceRef": "artifact:sha256:
|
|
5
|
+
"coreVersion": "0.18.2",
|
|
6
|
+
"coreManifestSha256": "sha256:abe17ff794b4019d8d476f09eb5d4105bae1070b1df0aab53e6c94c7ec8b65a6",
|
|
7
|
+
"coreSourceRef": "artifact:sha256:abe17ff794b4019d8d476f09eb5d4105bae1070b1df0aab53e6c94c7ec8b65a6"
|
|
8
8
|
},
|
|
9
9
|
"authorizationPolicy": "external_explicit_authority_required"
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlrunkit",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Project Driver for cross-Agent project continuity and progression; execution, evidence, verification, and recovery are the underlying trust substrate.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"author": "yeemio <owlclaw@163.com>",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"sha256": "sha256:3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
|
|
37
37
|
},
|
|
38
38
|
"supportedProducerCoreIdentities": [
|
|
39
|
+
{
|
|
40
|
+
"contractVersion": "0.2",
|
|
41
|
+
"coreVersion": "0.18.2",
|
|
42
|
+
"coreManifestSha256": "sha256:abe17ff794b4019d8d476f09eb5d4105bae1070b1df0aab53e6c94c7ec8b65a6"
|
|
43
|
+
},
|
|
39
44
|
{
|
|
40
45
|
"contractVersion": "0.2",
|
|
41
46
|
"coreVersion": "0.18.1",
|
|
@@ -18,6 +18,11 @@ import { captureWorkspaceSnapshot } from "./workspace-snapshot.mjs";
|
|
|
18
18
|
export { validQuickReceiptShape } from "./quick-receipt-contract.mjs";
|
|
19
19
|
|
|
20
20
|
export const SUPPORTED_QUICK_CORE_IDENTITIES = Object.freeze([
|
|
21
|
+
{
|
|
22
|
+
contractVersion: "0.2",
|
|
23
|
+
coreVersion: "0.18.2",
|
|
24
|
+
coreManifestSha256: "sha256:abe17ff794b4019d8d476f09eb5d4105bae1070b1df0aab53e6c94c7ec8b65a6",
|
|
25
|
+
},
|
|
21
26
|
{
|
|
22
27
|
contractVersion: "0.2",
|
|
23
28
|
coreVersion: "0.18.1",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": "OwlCodaRunKitRegistryReleasePolicyV1",
|
|
3
3
|
"status": "release_candidate_unpublished",
|
|
4
4
|
"packageName": "owlrunkit",
|
|
5
|
-
"candidateVersion": "0.18.
|
|
5
|
+
"candidateVersion": "0.18.2",
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
7
7
|
"registryRequiredBeforeAdoption": true,
|
|
8
8
|
"exactVersionRequired": true,
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
],
|
|
24
24
|
"currentPublishedRelease": {
|
|
25
25
|
"packageName": "owlrunkit",
|
|
26
|
-
"version": "0.18.
|
|
27
|
-
"shasum": "
|
|
28
|
-
"integrity": "sha512-
|
|
29
|
-
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.18.
|
|
26
|
+
"version": "0.18.1",
|
|
27
|
+
"shasum": "6d35c15f90ed775c583356754b831856d1a3c0a6",
|
|
28
|
+
"integrity": "sha512-07wWnSfY6RUavcb+ZpUbZ1S0nBjN7/8VIFHyP6blOB+PIrDnnST/8f7CoLgADYpfwjZiF5gR9U6XFwXa0kgVrg==",
|
|
29
|
+
"tarballUrl": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.18.1.tgz"
|
|
30
30
|
},
|
|
31
31
|
"registryRelease": null,
|
|
32
32
|
"registryEvidenceMustBeExternal": true,
|
package/sbom.spdx.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "owlrunkit-0.18.
|
|
6
|
-
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.18.
|
|
5
|
+
"name": "owlrunkit-0.18.2",
|
|
6
|
+
"documentNamespace": "https://owlcoda.com/spdx/owlrunkit/0.18.2",
|
|
7
7
|
"creationInfo": {
|
|
8
8
|
"created": "2026-07-29T00:00:00Z",
|
|
9
9
|
"creators": [
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
{
|
|
15
15
|
"name": "owlrunkit",
|
|
16
16
|
"SPDXID": "SPDXRef-Package-OwlRunKit",
|
|
17
|
-
"versionInfo": "0.18.
|
|
18
|
-
"downloadLocation": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.18.
|
|
17
|
+
"versionInfo": "0.18.2",
|
|
18
|
+
"downloadLocation": "https://registry.npmjs.org/owlrunkit/-/owlrunkit-0.18.2.tgz",
|
|
19
19
|
"filesAnalyzed": false,
|
|
20
20
|
"licenseConcluded": "GPL-3.0-or-later",
|
|
21
21
|
"licenseDeclared": "GPL-3.0-or-later",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
26
26
|
"referenceType": "purl",
|
|
27
|
-
"referenceLocator": "pkg:npm/owlrunkit@0.18.
|
|
27
|
+
"referenceLocator": "pkg:npm/owlrunkit@0.18.2"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
30
|
}
|
|
@@ -14,7 +14,7 @@ const BOOLEAN_FACTS = [
|
|
|
14
14
|
"schemaOrDataMigration",
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
export const RUNKIT_RISK_CATEGORIES = Object.freeze([
|
|
18
18
|
"backtest",
|
|
19
19
|
"deployment",
|
|
20
20
|
"destructive",
|
|
@@ -26,6 +26,8 @@ const RISK_CATEGORIES = new Set([
|
|
|
26
26
|
"release",
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
const RISK_CATEGORIES = new Set(RUNKIT_RISK_CATEGORIES);
|
|
30
|
+
|
|
29
31
|
const MATERIAL_RISK_CATEGORIES = new Set([
|
|
30
32
|
"destructive",
|
|
31
33
|
"foreign_write",
|
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { parseProjectControlState } from "./project-control-parser.mjs";
|
|
15
15
|
|
|
16
16
|
export const CONTRACT_VERSION = "0.2";
|
|
17
|
-
export const CORE_VERSION = "0.18.
|
|
17
|
+
export const CORE_VERSION = "0.18.2";
|
|
18
18
|
export const RUNTIME_ROOT = ".owlcoda/runkit";
|
|
19
19
|
|
|
20
20
|
const CORE_FILES = [
|
|
@@ -190,11 +190,16 @@ function parseJsonStrict(text) {
|
|
|
190
190
|
export function resolveBoundedWorkspaceRoot(workspaceRoot) {
|
|
191
191
|
const requested = path.resolve(workspaceRoot);
|
|
192
192
|
const stat = lstatSync(requested);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
const resolved = realpathSync(requested);
|
|
194
|
+
if ((stat.isSymbolicLink() || resolved !== requested)
|
|
195
|
+
&& lstatSync(resolved).isDirectory()) {
|
|
196
|
+
const quoted = `'${resolved.replaceAll("'", `'"'"'`)}'`;
|
|
197
|
+
throw new Error(
|
|
198
|
+
"workspace root must be a real directory without symlink ancestors. "
|
|
199
|
+
+ `Retry with --workspace ${quoted}.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (!stat.isDirectory()) {
|
|
198
203
|
throw new Error("workspace root must be a real directory without symlink ancestors");
|
|
199
204
|
}
|
|
200
205
|
return requested;
|
|
@@ -317,28 +322,51 @@ export function readLocalInstallBinding({ workspaceRoot } = {}) {
|
|
|
317
322
|
};
|
|
318
323
|
}
|
|
319
324
|
|
|
325
|
+
function registryTransportFailure(error) {
|
|
326
|
+
const code = error?.cause?.code ?? error?.code ?? null;
|
|
327
|
+
if (error?.name === "TimeoutError"
|
|
328
|
+
|| error?.name === "AbortError"
|
|
329
|
+
|| code === "ETIMEDOUT") {
|
|
330
|
+
return { issueCode: "registry_timeout", retryable: true };
|
|
331
|
+
}
|
|
332
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
333
|
+
return { issueCode: "registry_dns_failed", retryable: true };
|
|
334
|
+
}
|
|
335
|
+
if (code === "ERR_INVALID_URL") {
|
|
336
|
+
return { issueCode: "registry_url_invalid", retryable: false };
|
|
337
|
+
}
|
|
338
|
+
return { issueCode: "registry_connection_failed", retryable: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
320
341
|
export async function readRegistryExact({
|
|
321
342
|
registryUrl,
|
|
322
343
|
packageName = PACKAGE_NAME,
|
|
323
344
|
version = EXPECTED_VERSION,
|
|
324
345
|
timeoutMs = 1_500,
|
|
346
|
+
fetchImpl = fetch,
|
|
325
347
|
} = {}) {
|
|
326
348
|
const base = registryUrl.replace(/\/+$/, "");
|
|
327
349
|
const url = `${base}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`;
|
|
328
350
|
let response;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
352
|
+
try {
|
|
353
|
+
response = await fetchImpl(url, {
|
|
354
|
+
headers: {
|
|
355
|
+
accept: "application/json",
|
|
356
|
+
},
|
|
357
|
+
redirect: "manual",
|
|
358
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
const failure = registryTransportFailure(error);
|
|
363
|
+
if (!failure.retryable || attempt === 1) {
|
|
364
|
+
return {
|
|
365
|
+
status: "unavailable",
|
|
366
|
+
issueCodes: [failure.issueCode],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
342
370
|
}
|
|
343
371
|
if (response.status === 401 || response.status === 403) {
|
|
344
372
|
return {
|
|
@@ -1511,10 +1511,14 @@ export function reconcileProfilesV2({
|
|
|
1511
1511
|
}
|
|
1512
1512
|
const proposedProfilesBytes = `${JSON.stringify(merged.proposedProfiles, null, 2)}\n`;
|
|
1513
1513
|
const proposedProfilesSha256 = sha256(proposedProfilesBytes);
|
|
1514
|
+
const normalizationOnly = proposedProfilesSha256 !== currentProfilesSha256
|
|
1515
|
+
&& merged.addedProfileIds.length === 0
|
|
1516
|
+
&& merged.updatedProfiles.length === 0;
|
|
1514
1517
|
const planBody = {
|
|
1515
1518
|
detectionSha256: detection.detectionSha256,
|
|
1516
1519
|
currentProfilesSha256,
|
|
1517
1520
|
proposedProfilesSha256,
|
|
1521
|
+
normalizationOnly,
|
|
1518
1522
|
addedProfileIds: merged.addedProfileIds,
|
|
1519
1523
|
updatedProfiles: merged.updatedProfiles,
|
|
1520
1524
|
};
|
|
@@ -18,6 +18,9 @@ const OFFICIAL_CLI_ENTRYPOINT =
|
|
|
18
18
|
const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
19
19
|
const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
|
|
20
20
|
const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
|
|
21
|
+
["0.18.1", new Set([
|
|
22
|
+
"sha256:349b4157d3d089462b172de1619ccd169b5c1669b9322e2580ac6ec4f8616b07",
|
|
23
|
+
])],
|
|
21
24
|
["0.18.0", new Set([
|
|
22
25
|
"sha256:72c481dbafe06a9c56cd940d74a3fe109a82b73a3c43d0bb54923ea6c584c04e",
|
|
23
26
|
])],
|
|
@@ -16,6 +16,8 @@ import path from "node:path";
|
|
|
16
16
|
import { sha256Bytes } from "./quick-canonical.mjs";
|
|
17
17
|
|
|
18
18
|
const QUICK_ROOT = ".owlcoda/runkit/quick";
|
|
19
|
+
const OUTPUT_SUMMARY_BYTES = 16 * 1024;
|
|
20
|
+
const OUTPUT_SUMMARY_LINES = 3;
|
|
19
21
|
|
|
20
22
|
function withinRoot(root, candidate) {
|
|
21
23
|
const remainder = path.relative(root, candidate);
|
|
@@ -87,12 +89,40 @@ export function closeQuickOutputFiles(store) {
|
|
|
87
89
|
closeSync(store.stderrFd);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function cleanOutputLine(line) {
|
|
93
|
+
const withoutAnsi = line.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "");
|
|
94
|
+
return [...withoutAnsi]
|
|
95
|
+
.filter((character) => character === "\t" || character.codePointAt(0) >= 32)
|
|
96
|
+
.join("")
|
|
97
|
+
.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function summarizeOutputBytes(bytes) {
|
|
101
|
+
const start = Math.max(0, bytes.byteLength - OUTPUT_SUMMARY_BYTES);
|
|
102
|
+
let text = bytes.subarray(start).toString("utf8");
|
|
103
|
+
if (start > 0) {
|
|
104
|
+
const firstLineBreak = text.indexOf("\n");
|
|
105
|
+
if (firstLineBreak >= 0) text = text.slice(firstLineBreak + 1);
|
|
106
|
+
}
|
|
107
|
+
const lines = text
|
|
108
|
+
.split(/\r?\n/u)
|
|
109
|
+
.map(cleanOutputLine)
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
return {
|
|
112
|
+
lines: lines.slice(-OUTPUT_SUMMARY_LINES),
|
|
113
|
+
truncated: start > 0 || lines.length > OUTPUT_SUMMARY_LINES,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
90
117
|
function outputArtifact(workspaceRoot, filePath) {
|
|
91
118
|
const bytes = readFileSync(filePath);
|
|
92
119
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
artifact: {
|
|
121
|
+
path: path.relative(realpathSync(workspaceRoot), filePath).split(path.sep).join("/"),
|
|
122
|
+
sha256: sha256Bytes(bytes),
|
|
123
|
+
sizeBytes: bytes.byteLength,
|
|
124
|
+
},
|
|
125
|
+
summary: summarizeOutputBytes(bytes),
|
|
96
126
|
};
|
|
97
127
|
}
|
|
98
128
|
|
|
@@ -119,11 +149,13 @@ function validateQuickReceiptStore(workspaceRoot, store) {
|
|
|
119
149
|
|
|
120
150
|
export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
|
|
121
151
|
validateQuickReceiptStore(workspaceRoot, store);
|
|
152
|
+
const stdout = outputArtifact(workspaceRoot, store.stdoutPath);
|
|
153
|
+
const stderr = outputArtifact(workspaceRoot, store.stderrPath);
|
|
122
154
|
const complete = {
|
|
123
155
|
...receipt,
|
|
124
156
|
outputArtifacts: {
|
|
125
|
-
stdout:
|
|
126
|
-
stderr:
|
|
157
|
+
stdout: stdout.artifact,
|
|
158
|
+
stderr: stderr.artifact,
|
|
127
159
|
},
|
|
128
160
|
};
|
|
129
161
|
atomicWriteJson(store.receiptPath, complete);
|
|
@@ -131,6 +163,12 @@ export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
|
|
|
131
163
|
receipt: complete,
|
|
132
164
|
receiptPath: store.receiptPath,
|
|
133
165
|
receiptSha256: sha256Bytes(readFileSync(store.receiptPath)),
|
|
166
|
+
outputSummary: {
|
|
167
|
+
stdoutTail: stdout.summary.lines,
|
|
168
|
+
stderrTail: stderr.summary.lines,
|
|
169
|
+
stdoutTruncated: stdout.summary.truncated,
|
|
170
|
+
stderrTruncated: stderr.summary.truncated,
|
|
171
|
+
},
|
|
134
172
|
};
|
|
135
173
|
}
|
|
136
174
|
|
|
@@ -160,6 +160,8 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
const { outputSummary } = persisted;
|
|
164
|
+
|
|
163
165
|
if (sourceMutated) {
|
|
164
166
|
const attestCommand = quickAttestCommand(root, persisted.receiptPath);
|
|
165
167
|
return {
|
|
@@ -171,6 +173,7 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
|
|
|
171
173
|
commandExitCode: execution.exitCode,
|
|
172
174
|
mutationDecision: "invalidated_by_command_source_mutation",
|
|
173
175
|
issueCodes,
|
|
176
|
+
outputSummary,
|
|
174
177
|
attestCommand,
|
|
175
178
|
issues: [],
|
|
176
179
|
nextAllowedAction: "restore_source_or_run_a_new_quick_verification",
|
|
@@ -188,6 +191,7 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
|
|
|
188
191
|
commandExitCode: execution.exitCode,
|
|
189
192
|
mutationDecision: "source_unchanged",
|
|
190
193
|
issueCodes,
|
|
194
|
+
outputSummary,
|
|
191
195
|
attestCommand,
|
|
192
196
|
issues: execution.launchError ? [execution.launchError] : [],
|
|
193
197
|
nextAllowedAction: "inspect_output_and_rerun",
|
|
@@ -204,6 +208,7 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
|
|
|
204
208
|
commandExitCode: 0,
|
|
205
209
|
mutationDecision: "source_unchanged",
|
|
206
210
|
issueCodes,
|
|
211
|
+
outputSummary,
|
|
207
212
|
attestCommand,
|
|
208
213
|
issues: [],
|
|
209
214
|
nextAllowedAction: attestCommand,
|
|
@@ -100,7 +100,10 @@ import {
|
|
|
100
100
|
replaceFleetRegistry,
|
|
101
101
|
rollbackFleetRegistry,
|
|
102
102
|
} from "./fleet-discovery.mjs";
|
|
103
|
-
import {
|
|
103
|
+
import {
|
|
104
|
+
RUNKIT_RISK_CATEGORIES,
|
|
105
|
+
routeRunKitAssuranceV1,
|
|
106
|
+
} from "./assurance-router.mjs";
|
|
104
107
|
import {
|
|
105
108
|
formatUsageModeHumanV1,
|
|
106
109
|
recommendUsageModeV1,
|
|
@@ -244,7 +247,7 @@ read-only and grant no Git, release, deployment, production, or business authori
|
|
|
244
247
|
[--irreversible] [--rollback-uncertain]
|
|
245
248
|
[--permission-sensitive] [--security-sensitive] [--funds-at-risk]
|
|
246
249
|
[--schema-or-data-migration] [--formal-acceptance]
|
|
247
|
-
[--risk-category
|
|
250
|
+
[--risk-category <${RUNKIT_RISK_CATEGORIES.join("|")}>] [--json]
|
|
248
251
|
|
|
249
252
|
Returns one user mode plus the independent continuity and assurance selections.
|
|
250
253
|
Persisted mode preferences are not part of this release.
|
|
@@ -259,6 +262,13 @@ Reads the current mode preference source without creating project state.
|
|
|
259
262
|
|
|
260
263
|
Attests one Quick Receipt against its bound materials and current workspace.
|
|
261
264
|
It never grants repository or business authority.
|
|
265
|
+
`,
|
|
266
|
+
"quick-verify": `Usage:
|
|
267
|
+
owlrunkit quick-verify --workspace <directory> [--json] -- <executable> [args...]
|
|
268
|
+
|
|
269
|
+
Runs one exact command, saves source-bound stdout/stderr and a Quick receipt,
|
|
270
|
+
and returns a bounded output summary. Supply the exact command after --.
|
|
271
|
+
Quick evidence is not Formal acceptance and grants no repository or business authority.
|
|
262
272
|
`,
|
|
263
273
|
fleet: `Usage:
|
|
264
274
|
owlrunkit fleet register-root --fleet-root <directory>
|
|
@@ -437,6 +447,18 @@ action without granting any authority.
|
|
|
437
447
|
Project status is derived from durable work items, Agent assignments,
|
|
438
448
|
dependencies, decisions, checkpoints, integration gates, and evidence refs.
|
|
439
449
|
No self-reported percentage is accepted.
|
|
450
|
+
`,
|
|
451
|
+
"project checkpoint": `Usage:
|
|
452
|
+
owlrunkit project checkpoint --workspace <directory> --checkpoint-id <id> --at <ISO-UTC>
|
|
453
|
+
--assignment-id <current> --work-item <id>
|
|
454
|
+
--state <active|waiting_dependency|waiting_decision|verifying|ready_to_integrate|completed|failed>
|
|
455
|
+
--summary <text> [--completed-units <nonnegative-int>]
|
|
456
|
+
[--evidence <ref> ...] [--blocker <ref> ...] [--decision <id> ...]
|
|
457
|
+
[--next <text>] [--source-fingerprint <sha256>] [--json]
|
|
458
|
+
|
|
459
|
+
Measured WorkItems require --completed-units on every checkpoint. A completed
|
|
460
|
+
checkpoint must use the WorkItem's declared total. Example:
|
|
461
|
+
owlrunkit project checkpoint --workspace . --checkpoint-id checkpoint-x --at 2026-08-11T00:00:00.000Z --assignment-id assign-x --work-item x --state completed --summary 'Completed x.' --completed-units 5 --evidence receipt:quick
|
|
440
462
|
`,
|
|
441
463
|
"project status": `Usage:
|
|
442
464
|
owlrunkit project status --workspace <directory> [--json]
|
|
@@ -465,6 +487,25 @@ function withHumanOutput(result, humanOutput) {
|
|
|
465
487
|
return result;
|
|
466
488
|
}
|
|
467
489
|
|
|
490
|
+
function shellCommandToken(value) {
|
|
491
|
+
const text = String(value);
|
|
492
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(text)
|
|
493
|
+
? text
|
|
494
|
+
: `'${text.replaceAll("'", `'"'"'`)}'`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function correctedCheckpointCommand(argv, completedUnits) {
|
|
498
|
+
const corrected = [...argv];
|
|
499
|
+
const jsonIndex = corrected.indexOf("--json");
|
|
500
|
+
corrected.splice(
|
|
501
|
+
jsonIndex === -1 ? corrected.length : jsonIndex,
|
|
502
|
+
0,
|
|
503
|
+
"--completed-units",
|
|
504
|
+
String(completedUnits),
|
|
505
|
+
);
|
|
506
|
+
return `owlrunkit ${corrected.map(shellCommandToken).join(" ")}`;
|
|
507
|
+
}
|
|
508
|
+
|
|
468
509
|
function formatTeamProjectAssignmentHuman(result) {
|
|
469
510
|
const disposition = result.resumed ? "Resumed" : "Assigned";
|
|
470
511
|
const nextAction = result.nextAction === null
|
|
@@ -3073,24 +3114,49 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
|
|
|
3073
3114
|
event: requestedEvent,
|
|
3074
3115
|
});
|
|
3075
3116
|
}
|
|
3117
|
+
const checkpointId = requireOption(options, "checkpoint-id");
|
|
3118
|
+
const occurredAt = requireOption(options, "at");
|
|
3119
|
+
const assignmentId = requireOption(options, "assignment-id");
|
|
3120
|
+
const workItemId = requireOption(options, "work-item");
|
|
3121
|
+
const state = requireOption(options, "state");
|
|
3122
|
+
const summary = requireOption(options, "summary");
|
|
3076
3123
|
const completedUnits = options["completed-units"] === undefined
|
|
3077
3124
|
? undefined
|
|
3078
3125
|
: parseCanonicalNonnegativeSafeInteger(options["completed-units"], "completed-units");
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3126
|
+
let checkpointed;
|
|
3127
|
+
try {
|
|
3128
|
+
checkpointed = checkpointTeamProjectV1({
|
|
3129
|
+
workspaceRoot: root,
|
|
3130
|
+
checkpointId,
|
|
3131
|
+
occurredAt,
|
|
3132
|
+
assignmentId,
|
|
3133
|
+
workItemId,
|
|
3134
|
+
state,
|
|
3135
|
+
summary,
|
|
3136
|
+
completedUnits,
|
|
3137
|
+
evidenceRefs: options.evidence ?? [],
|
|
3138
|
+
blockerRefs: options.blocker ?? [],
|
|
3139
|
+
decisionRefs: options.decision ?? [],
|
|
3140
|
+
nextAction: options.next ?? null,
|
|
3141
|
+
sourceFingerprint: options["source-fingerprint"],
|
|
3142
|
+
});
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
if (error instanceof Error
|
|
3145
|
+
&& error.message === "Measured work item checkpoint requires completedUnits.") {
|
|
3146
|
+
const projectStatus = readTeamProjectStatusV1({ workspaceRoot: root });
|
|
3147
|
+
const workItem = projectStatus.workItems.find((row) => row.workItemId === workItemId);
|
|
3148
|
+
const suggestedUnits = state === "completed"
|
|
3149
|
+
? workItem?.progress.total
|
|
3150
|
+
: workItem?.progress.completed;
|
|
3151
|
+
if (Number.isSafeInteger(suggestedUnits)) {
|
|
3152
|
+
throw new Error(
|
|
3153
|
+
"Measured work item checkpoint requires --completed-units. "
|
|
3154
|
+
+ `Corrected command: ${correctedCheckpointCommand(argv, suggestedUnits)}`,
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
throw error;
|
|
3159
|
+
}
|
|
3094
3160
|
return options.json === true
|
|
3095
3161
|
? checkpointed
|
|
3096
3162
|
: withHumanOutput(checkpointed, formatTeamProjectCheckpointHuman(checkpointed));
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"detectionSha256": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] },
|
|
17
17
|
"currentProfilesSha256": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] },
|
|
18
18
|
"proposedProfilesSha256": { "$ref": "#/$defs/hash" },
|
|
19
|
+
"normalizationOnly": { "type": "boolean" },
|
|
19
20
|
"addedProfileIds": { "$ref": "#/$defs/strings" },
|
|
20
21
|
"updatedProfiles": { "type": "array", "items": { "$ref": "#/$defs/update" } },
|
|
21
22
|
"proposedProfiles": { "$ref": "#/$defs/profiles" },
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"detectionSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
82
83
|
"currentProfilesSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
83
84
|
"proposedProfilesSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
85
|
+
"normalizationOnly": false,
|
|
84
86
|
"addedProfileIds": [],
|
|
85
87
|
"updatedProfiles": [{
|
|
86
88
|
"profileId": "node-quality",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RUNKIT_RISK_CATEGORIES } from "./assurance-router.mjs";
|
|
2
|
+
|
|
1
3
|
const USER_MODES = new Set(["auto", "off", "light", "managed", "formal"]);
|
|
2
4
|
|
|
3
5
|
const BOOLEAN_FACTS = [
|
|
@@ -24,17 +26,7 @@ const MATERIAL_RISK_CATEGORIES = new Map([
|
|
|
24
26
|
["migration", "schema_or_data_migration"],
|
|
25
27
|
]);
|
|
26
28
|
|
|
27
|
-
const KNOWN_RISK_CATEGORIES = new Set(
|
|
28
|
-
"backtest",
|
|
29
|
-
"deployment",
|
|
30
|
-
"destructive",
|
|
31
|
-
"foreign_write",
|
|
32
|
-
"funds",
|
|
33
|
-
"integration",
|
|
34
|
-
"migration",
|
|
35
|
-
"production",
|
|
36
|
-
"release",
|
|
37
|
-
]);
|
|
29
|
+
const KNOWN_RISK_CATEGORIES = new Set(RUNKIT_RISK_CATEGORIES);
|
|
38
30
|
|
|
39
31
|
const AUTHORITY_BOUNDARY = Object.freeze({
|
|
40
32
|
authorizationGranted: false,
|
|
@@ -65,8 +57,14 @@ function requireFacts(input) {
|
|
|
65
57
|
|| !input.riskCategories.every((value) => typeof value === "string")) {
|
|
66
58
|
throw new Error("riskCategories must be an array of strings");
|
|
67
59
|
}
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
const unknownCategories = uniqueSorted(
|
|
61
|
+
input.riskCategories.filter((value) => !KNOWN_RISK_CATEGORIES.has(value)),
|
|
62
|
+
);
|
|
63
|
+
if (unknownCategories.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`riskCategories contains an unknown category: ${unknownCategories.join(", ")}. `
|
|
66
|
+
+ `Allowed: ${RUNKIT_RISK_CATEGORIES.join(", ")}`,
|
|
67
|
+
);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
70
|
|