owlrunkit 0.18.0 → 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.
Files changed (27) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +61 -17
  3. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +166 -0
  4. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +41 -16
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  6. package/package.json +1 -1
  7. package/packages/attest/distribution-manifest-v1.json +11 -1
  8. package/packages/attest/src/quick.mjs +11 -1
  9. package/registry-release-policy-v1.json +5 -5
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/assurance-router.mjs +31 -21
  12. package/scripts/runkit-contract/core-contract.mjs +5 -1
  13. package/scripts/runkit-contract/inspect-presentation.mjs +38 -5
  14. package/scripts/runkit-contract/install-codex-skill.mjs +60 -1
  15. package/scripts/runkit-contract/onboarding-doctor.mjs +65 -19
  16. package/scripts/runkit-contract/profile-onboarding.mjs +601 -22
  17. package/scripts/runkit-contract/project-cli-resolver.mjs +6 -0
  18. package/scripts/runkit-contract/quick-attest.mjs +1 -0
  19. package/scripts/runkit-contract/quick-metrics.mjs +43 -5
  20. package/scripts/runkit-contract/quick-receipt.mjs +43 -5
  21. package/scripts/runkit-contract/quick-verify.mjs +20 -4
  22. package/scripts/runkit-contract/runkit-cli.mjs +278 -24
  23. package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +107 -0
  24. package/scripts/runkit-contract/schemas/profile-reconcile-receipt-v1.schema.json +60 -0
  25. package/scripts/runkit-contract/schemas/usage-mode-recommendation-v1.schema.json +136 -0
  26. package/scripts/runkit-contract/templates/assurance-request.json +6 -0
  27. package/scripts/runkit-contract/usage-mode.mjs +227 -0
@@ -6,9 +6,15 @@ const BOOLEAN_FACTS = [
6
6
  "longRunning",
7
7
  "interruptionRecovery",
8
8
  "dirtyParallelWorkspace",
9
+ "irreversibleChange",
10
+ "rollbackUncertain",
11
+ "permissionSensitive",
12
+ "securitySensitive",
13
+ "fundsAtRisk",
14
+ "schemaOrDataMigration",
9
15
  ];
10
16
 
11
- const RISK_CATEGORIES = new Set([
17
+ export const RUNKIT_RISK_CATEGORIES = Object.freeze([
12
18
  "backtest",
13
19
  "deployment",
14
20
  "destructive",
@@ -20,13 +26,13 @@ const RISK_CATEGORIES = new Set([
20
26
  "release",
21
27
  ]);
22
28
 
23
- const FULL_RISK_CATEGORIES = new Set([
24
- "deployment",
29
+ const RISK_CATEGORIES = new Set(RUNKIT_RISK_CATEGORIES);
30
+
31
+ const MATERIAL_RISK_CATEGORIES = new Set([
25
32
  "destructive",
26
33
  "foreign_write",
27
34
  "funds",
28
- "production",
29
- "release",
35
+ "migration",
30
36
  ]);
31
37
 
32
38
  function incompleteReasons(input) {
@@ -76,26 +82,21 @@ export function routeRunKitAssuranceV1(input) {
76
82
  const categories = [...new Set(input.riskCategories)].sort();
77
83
  const formalReasons = [
78
84
  ...(input.needsFormalAcceptance ? ["formal_acceptance_required"] : []),
79
- ...(input.mayMutateProject ? ["project_mutation"] : []),
80
- ...(input.externalSideEffects ? ["external_side_effects"] : []),
81
- ...(input.longRunning ? ["long_running_execution"] : []),
82
- ...(input.interruptionRecovery ? ["interruption_recovery_required"] : []),
83
- ...(input.dirtyParallelWorkspace ? ["dirty_parallel_workspace"] : []),
84
- ...(input.writerCount > 0 ? ["writer_scope_required"] : []),
85
- ...(input.needsDurableEvidence && input.exactCommandCount === 0
86
- ? ["exact_command_missing"]
87
- : []),
88
- ...(input.needsDurableEvidence && input.exactCommandCount > 1
89
- ? ["multiple_commands"]
90
- : []),
91
- ...categories.map(category => `risk_${category}`),
85
+ ...(input.writerCount > 1 ? ["multiple_writers"] : []),
86
+ ...(input.permissionSensitive === true ? ["permission_sensitive"] : []),
87
+ ...(input.securitySensitive === true ? ["security_sensitive"] : []),
88
+ ...(input.fundsAtRisk === true ? ["funds_at_risk"] : []),
89
+ ...(input.irreversibleChange === true ? ["irreversible_change"] : []),
90
+ ...(input.rollbackUncertain === true ? ["rollback_uncertain"] : []),
91
+ ...(input.schemaOrDataMigration === true ? ["schema_or_data_migration"] : []),
92
+ ...categories
93
+ .filter(category => MATERIAL_RISK_CATEGORIES.has(category))
94
+ .map(category => `risk_${category}`),
92
95
  ];
93
96
  if (formalReasons.length > 0) {
94
- const full = input.externalSideEffects
95
- || categories.some(category => FULL_RISK_CATEGORIES.has(category));
96
97
  return result({
97
98
  lane: "formal",
98
- riskMode: full ? "full" : "standard",
99
+ riskMode: "full",
99
100
  reasonCodes: [...new Set(formalReasons)].sort(),
100
101
  nextCommand: "owlrunkit formal start",
101
102
  });
@@ -110,6 +111,15 @@ export function routeRunKitAssuranceV1(input) {
110
111
  });
111
112
  }
112
113
 
114
+ if (input.exactCommandCount !== 1) {
115
+ return result({
116
+ lane: "none",
117
+ riskMode: "lightweight",
118
+ reasonCodes: ["quick_exact_command_required"],
119
+ nextCommand: null,
120
+ });
121
+ }
122
+
113
123
  return result({
114
124
  lane: "quick",
115
125
  riskMode: "lightweight",
@@ -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.0";
17
+ export const CORE_VERSION = "0.18.2";
18
18
  export const RUNTIME_ROOT = ".owlcoda/runkit";
19
19
 
20
20
  const CORE_FILES = [
@@ -68,6 +68,7 @@ const CORE_FILES = [
68
68
  "sync-release-identity.mjs",
69
69
  "team-project-successor.mjs",
70
70
  "team-project.mjs",
71
+ "usage-mode.mjs",
71
72
  "verification-envelope.mjs",
72
73
  "verification-envelope-check.mjs",
73
74
  "verification-receipt-gate.mjs",
@@ -101,6 +102,8 @@ const CORE_FILES = [
101
102
  "schemas/owner-deployment-decision-v1.schema.json",
102
103
  "schemas/profile-apply-receipt-v1.schema.json",
103
104
  "schemas/profile-detection-v2.schema.json",
105
+ "schemas/profile-reconcile-plan-v1.schema.json",
106
+ "schemas/profile-reconcile-receipt-v1.schema.json",
104
107
  "schemas/project-config.schema.json",
105
108
  "schemas/project-config-v2.schema.json",
106
109
  "schemas/profiles.schema.json",
@@ -125,6 +128,7 @@ const CORE_FILES = [
125
128
  "schemas/team-project-event-v1.schema.json",
126
129
  "schemas/team-project-successor-receipt-v1.schema.json",
127
130
  "schemas/team-project-status-v1.schema.json",
131
+ "schemas/usage-mode-recommendation-v1.schema.json",
128
132
  "schemas/verification-envelope-v1.schema.json",
129
133
  "schemas/verification-finalize-attempt-v1.schema.json",
130
134
  "schemas/verification-preflight-v1.schema.json",
@@ -40,7 +40,7 @@ function closeoutDecisionCounts(inspected) {
40
40
 
41
41
  export function buildInspectSummary(
42
42
  inspected,
43
- { maintenanceNextAction = null } = {},
43
+ { maintenanceNextAction = null, projectDriver = null } = {},
44
44
  ) {
45
45
  const humanStatus = deriveHumanStatusFromInspectV1(inspected);
46
46
  const focus = selectedFocus(inspected);
@@ -80,6 +80,21 @@ export function buildInspectSummary(
80
80
  ...(inspected.controlIssues ?? []),
81
81
  ...(selectedResource?.blockers ?? []),
82
82
  ].filter((reason, index, all) => all.indexOf(reason) === index);
83
+ const projectGapCode = projectDriver === null
84
+ ? null
85
+ : projectDriver.dominantGap.id === null
86
+ ? projectDriver.dominantGap.kind
87
+ : `${projectDriver.dominantGap.kind}:${projectDriver.dominantGap.id}`;
88
+ const projectDriverSummary = projectDriver === null
89
+ ? null
90
+ : {
91
+ projectId: projectDriver.projectId,
92
+ overall: projectDriver.overall,
93
+ headline: projectDriver.headline,
94
+ dominantGap: structuredClone(projectDriver.dominantGap),
95
+ nextAction: projectDriver.nextAction,
96
+ nextActorId: projectDriver.nextActorId,
97
+ };
83
98
  return {
84
99
  schemaVersion: "OwlCodaRunKitInspectSummaryV1",
85
100
  currentExecution: {
@@ -139,13 +154,22 @@ export function buildInspectSummary(
139
154
  resources: [...(selectedResource?.resources ?? [])],
140
155
  },
141
156
  dominantGap: {
142
- code: humanStatus.nextAllowedAction,
143
- reasons: [...reasons],
157
+ code: projectGapCode ?? humanStatus.nextAllowedAction,
158
+ reasons: projectDriver === null
159
+ ? [...reasons]
160
+ : [projectDriver.dominantGap.reason].filter(Boolean),
144
161
  },
162
+ ...(projectDriverSummary === null ? {} : { projectDriver: projectDriverSummary }),
145
163
  lifecycleNextAction: inspected.recovery.nextAllowedAction,
146
164
  maintenanceNextAction,
147
165
  optionalReviewAction: independentHistory ? "inspect_closed_history" : null,
148
166
  nextAllowedAction: humanStatus.nextAllowedAction,
167
+ ...(projectDriver === null
168
+ ? {}
169
+ : {
170
+ recommendedNextAction:
171
+ projectDriver.nextAction ?? humanStatus.nextAllowedAction,
172
+ }),
149
173
  authorizationGranted: false,
150
174
  gitAuthorization: false,
151
175
  releaseAuthorization: false,
@@ -192,8 +216,17 @@ function summaryLines(summary, humanStatus) {
192
216
  ? `$${summary.resourcePreflight.estimate.cost.valueUsd}`
193
217
  : "unknown"}`,
194
218
  `Receipt reuse: ${summary.resourcePreflight.receiptReuse.appliedCount}/${summary.resourcePreflight.receiptReuse.reusableCount}`,
195
- `Dominant gap: ${summary.dominantGap.code}`,
196
- `Next allowed action: ${summary.nextAllowedAction}`,
219
+ ...(summary.projectDriver
220
+ ? [
221
+ `Project dominant gap: ${summary.dominantGap.code}`,
222
+ `Recommended next action: ${value(summary.recommendedNextAction)}`,
223
+ `Execution state: ${summary.currentExecution.state}`,
224
+ `Execution next allowed action: ${summary.nextAllowedAction}`,
225
+ ]
226
+ : [
227
+ `Dominant gap: ${summary.dominantGap.code}`,
228
+ `Next allowed action: ${summary.nextAllowedAction}`,
229
+ ]),
197
230
  ...(summary.maintenanceNextAction
198
231
  ? [`Maintenance: ${summary.maintenanceNextAction}`]
199
232
  : []),
@@ -16,6 +16,7 @@ import path from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
 
18
18
  import {
19
+ currentCoreIdentity,
19
20
  initializeProjectRunKit,
20
21
  isDirectExecution,
21
22
  validateProjectConfigV2,
@@ -193,6 +194,64 @@ export async function inspectInstalledSkill({ targetRoot } = {}) {
193
194
  };
194
195
  }
195
196
 
197
+ export async function inspectInstalledSkillCompatibility({ targetRoot } = {}) {
198
+ const integrity = await inspectInstalledSkill({ targetRoot });
199
+ if (!integrity.valid) {
200
+ return {
201
+ ...integrity,
202
+ integrityValid: false,
203
+ authorizationGranted: false,
204
+ };
205
+ }
206
+
207
+ const expectedCore = currentCoreIdentity();
208
+ let installedSkillVersion = null;
209
+ let installedConfigCore = null;
210
+ const issues = [];
211
+ try {
212
+ const skillText = await readFile(path.join(targetRoot, "SKILL.md"), "utf8");
213
+ installedSkillVersion = skillText.match(/standalone `owlrunkit@(\d+\.\d+\.\d+)`/u)?.[1] ?? null;
214
+ installedConfigCore = JSON.parse(await readFile(
215
+ path.join(targetRoot, "assets/templates/config.json"),
216
+ "utf8",
217
+ )).core ?? null;
218
+ } catch {
219
+ issues.push("installed_skill_version_declaration_invalid");
220
+ }
221
+
222
+ const installedConfigCoreVersion = installedConfigCore?.coreVersion ?? null;
223
+ if (installedSkillVersion === null || installedConfigCoreVersion === null) {
224
+ issues.push("installed_skill_core_version_declaration_missing");
225
+ } else if (
226
+ installedSkillVersion !== expectedCore.coreVersion
227
+ || installedConfigCoreVersion !== expectedCore.coreVersion
228
+ ) {
229
+ issues.push(
230
+ `installed_skill_core_version_mismatch:expected_${expectedCore.coreVersion}`
231
+ + `:skill_${installedSkillVersion}:config_${installedConfigCoreVersion}`,
232
+ );
233
+ } else if (!sameCoreIdentity(installedConfigCore, expectedCore)) {
234
+ issues.push(
235
+ `installed_skill_core_identity_mismatch:expected_${expectedCore.coreManifestSha256}`
236
+ + `:config_${installedConfigCore?.coreManifestSha256 ?? "missing"}`,
237
+ );
238
+ }
239
+
240
+ return {
241
+ ...integrity,
242
+ status: issues.length === 0 ? "valid" : "version_mismatch",
243
+ valid: issues.length === 0,
244
+ integrityValid: true,
245
+ expectedCoreVersion: expectedCore.coreVersion,
246
+ expectedCore,
247
+ installedSkillVersion,
248
+ installedConfigCoreVersion,
249
+ installedConfigCore,
250
+ issues,
251
+ authorizationGranted: false,
252
+ };
253
+ }
254
+
196
255
  export async function inspectFleetForSkillChange({
197
256
  workspaceRoots = null,
198
257
  fleetManifestPath = null,
@@ -731,7 +790,7 @@ export async function runCli(argv = process.argv.slice(2)) {
731
790
  return { exitCode: 0, ...output };
732
791
  }
733
792
  if (command === "inspect") {
734
- const output = await inspectInstalledSkill({ targetRoot: options.target });
793
+ const output = await inspectInstalledSkillCompatibility({ targetRoot: options.target });
735
794
  return { exitCode: output.valid ? 0 : 2, ...output };
736
795
  }
737
796
  if (command === "restore") {
@@ -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
- if (
194
- stat.isSymbolicLink()
195
- || !stat.isDirectory()
196
- || realpathSync(requested) !== requested
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
- try {
330
- response = await fetch(url, {
331
- headers: {
332
- accept: "application/json",
333
- },
334
- redirect: "manual",
335
- signal: AbortSignal.timeout(timeoutMs),
336
- });
337
- } catch {
338
- return {
339
- status: "unavailable",
340
- issueCodes: ["registry_dns_or_timeout"],
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 {
@@ -583,6 +611,24 @@ function profilesCheck(root) {
583
611
  }
584
612
  }
585
613
 
614
+ async function doctorProfilesCheck(root) {
615
+ const launcherCheck = profilesCheck(root);
616
+ if (launcherCheck.status === "missing") return launcherCheck;
617
+ const { validateProfiles } = await import("./profile-onboarding.mjs");
618
+ const validation = validateProfiles({ workspaceRoot: root });
619
+ const issues = [...new Set([
620
+ ...(launcherCheck.launcherIssues ?? []),
621
+ ...validation.issues,
622
+ ])].sort();
623
+ return {
624
+ ...launcherCheck,
625
+ status: issues.length === 0 ? "ok" : "invalid",
626
+ validationStatus: validation.status,
627
+ validationIssues: validation.issues,
628
+ ...(issues.length > 0 ? { issues } : {}),
629
+ };
630
+ }
631
+
586
632
  export function readProfileLauncherReadiness({ workspaceRoot } = {}) {
587
633
  const profiles = profilesCheck(resolveBoundedWorkspaceRoot(workspaceRoot));
588
634
  const launcherWarnings = profiles.launcherWarnings ?? [];
@@ -646,7 +692,7 @@ export async function runDoctor({
646
692
  });
647
693
  const nodeOk = Number(process.versions.node.split(".")[0]) >= 20;
648
694
  const config = configCheck(root);
649
- const profiles = profilesCheck(root);
695
+ const profiles = await doctorProfilesCheck(root);
650
696
  const adoption = adoptionCheck(root, installed);
651
697
  const runtimeIsolation = runtimeIsolationCheck(root);
652
698
  const checks = {