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
@@ -1,4 +1,5 @@
1
1
  export {
2
2
  attestQuickReceipt,
3
3
  attestQuickReceiptDetails,
4
+ validQuickReceiptShape,
4
5
  } from "../../packages/attest/src/quick.mjs";
@@ -6,9 +6,40 @@ import {
6
6
  } from "node:fs";
7
7
  import path from "node:path";
8
8
 
9
- import { attestQuickReceipt } from "./quick-attest.mjs";
9
+ import {
10
+ decodeUtf8Strict,
11
+ parseJsonStrict,
12
+ readFileBytesBounded,
13
+ } from "../../packages/attest/src/formal.mjs";
14
+ import { quickSnapshotFingerprintValid } from "../../packages/attest/src/quick-receipt-contract.mjs";
15
+ import {
16
+ attestQuickReceipt,
17
+ validQuickReceiptShape,
18
+ } from "./quick-attest.mjs";
10
19
  import { quickReceiptRoot } from "./quick-receipt.mjs";
11
20
 
21
+ const INVALID_ATTESTATION_ISSUES = new Set([
22
+ "attestation_material_missing",
23
+ "receipt_material_hash_mismatch",
24
+ ]);
25
+
26
+ function readHistoricalReceipt(receiptPath) {
27
+ const { bytes } = readFileBytesBounded(receiptPath);
28
+ const receipt = parseJsonStrict(decodeUtf8Strict(bytes));
29
+ if (!validQuickReceiptShape(receipt)) throw new Error("quick_receipt_schema_invalid");
30
+ const snapshotsValid = quickSnapshotFingerprintValid(receipt.workspaceBefore)
31
+ && quickSnapshotFingerprintValid(receipt.workspaceAfter);
32
+ const sourceChanged = receipt.workspaceBefore.sourceFingerprint
33
+ !== receipt.workspaceAfter.sourceFingerprint;
34
+ const mutationBindingValid = receipt.mutationDecision === "source_unchanged"
35
+ ? !sourceChanged && !receipt.issueCodes.includes("source_mutated_during_verification")
36
+ : sourceChanged && receipt.issueCodes.includes("source_mutated_during_verification");
37
+ if (!snapshotsValid || !mutationBindingValid) {
38
+ throw new Error("quick_receipt_source_binding_invalid");
39
+ }
40
+ return receipt;
41
+ }
42
+
12
43
  function trustedReceiptRoot(workspaceRoot) {
13
44
  const workspace = realpathSync(workspaceRoot);
14
45
  const root = quickReceiptRoot(workspace);
@@ -65,10 +96,17 @@ export function readLocalQuickMetrics(workspaceRoot) {
65
96
  };
66
97
  for (const receiptPath of receiptPaths) {
67
98
  try {
68
- const result = attestQuickReceipt({ receiptPath, workspaceRoot });
69
- if (result.decision === "GO") totals.passed += 1;
70
- else if (result.issueCodes.includes("receipt_source_mismatch")) totals.sourceMutated += 1;
71
- else totals.failed += 1;
99
+ const receipt = readHistoricalReceipt(receiptPath);
100
+ const attestation = attestQuickReceipt({ receiptPath, workspaceRoot });
101
+ if (attestation.issueCodes.some((issue) => INVALID_ATTESTATION_ISSUES.has(issue))) {
102
+ totals.invalid += 1;
103
+ } else if (receipt.mutationDecision === "invalidated_by_command_source_mutation") {
104
+ totals.sourceMutated += 1;
105
+ } else if (receipt.exitResult.exitCode === 0 && receipt.exitResult.signal === null) {
106
+ totals.passed += 1;
107
+ } else {
108
+ totals.failed += 1;
109
+ }
72
110
  } catch {
73
111
  totals.invalid += 1;
74
112
  }
@@ -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
- path: path.relative(realpathSync(workspaceRoot), filePath).split(path.sep).join("/"),
94
- sha256: sha256Bytes(bytes),
95
- sizeBytes: bytes.byteLength,
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: outputArtifact(workspaceRoot, store.stdoutPath),
126
- stderr: outputArtifact(workspaceRoot, store.stderrPath),
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
 
@@ -9,6 +9,14 @@ import {
9
9
  } from "./quick-receipt.mjs";
10
10
  import { captureWorkspaceSnapshot } from "./quick-workspace-snapshot.mjs";
11
11
 
12
+ function shellQuote(value) {
13
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
14
+ }
15
+
16
+ function quickAttestCommand(workspaceRoot, receiptPath) {
17
+ return `owlrunkit quick-attest --workspace ${shellQuote(workspaceRoot)} --receipt ${shellQuote(receiptPath)}`;
18
+ }
19
+
12
20
  function runExactCommand({ executable, argv, cwd, stdoutFd, stderrFd }) {
13
21
  return new Promise((resolve) => {
14
22
  let settled = false;
@@ -152,7 +160,10 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
152
160
  };
153
161
  }
154
162
 
163
+ const { outputSummary } = persisted;
164
+
155
165
  if (sourceMutated) {
166
+ const attestCommand = quickAttestCommand(root, persisted.receiptPath);
156
167
  return {
157
168
  status: "quick_verification_source_mutated",
158
169
  exitCode: 2,
@@ -162,13 +173,15 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
162
173
  commandExitCode: execution.exitCode,
163
174
  mutationDecision: "invalidated_by_command_source_mutation",
164
175
  issueCodes,
165
- attestCommand: `owlcoda attest ${persisted.receiptPath}`,
176
+ outputSummary,
177
+ attestCommand,
166
178
  issues: [],
167
179
  nextAllowedAction: "restore_source_or_run_a_new_quick_verification",
168
180
  authorizationGranted: false,
169
181
  };
170
182
  }
171
183
  if (execution.launchError || execution.exitCode !== 0) {
184
+ const attestCommand = quickAttestCommand(root, persisted.receiptPath);
172
185
  return {
173
186
  status: "quick_verification_failed",
174
187
  exitCode: 1,
@@ -178,12 +191,14 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
178
191
  commandExitCode: execution.exitCode,
179
192
  mutationDecision: "source_unchanged",
180
193
  issueCodes,
181
- attestCommand: `owlcoda attest ${persisted.receiptPath}`,
194
+ outputSummary,
195
+ attestCommand,
182
196
  issues: execution.launchError ? [execution.launchError] : [],
183
197
  nextAllowedAction: "inspect_output_and_rerun",
184
198
  authorizationGranted: false,
185
199
  };
186
200
  }
201
+ const attestCommand = quickAttestCommand(root, persisted.receiptPath);
187
202
  return {
188
203
  status: "quick_verification_passed",
189
204
  exitCode: 0,
@@ -193,9 +208,10 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
193
208
  commandExitCode: 0,
194
209
  mutationDecision: "source_unchanged",
195
210
  issueCodes,
196
- attestCommand: `owlcoda attest ${persisted.receiptPath}`,
211
+ outputSummary,
212
+ attestCommand,
197
213
  issues: [],
198
- nextAllowedAction: `owlcoda attest ${persisted.receiptPath}`,
214
+ nextAllowedAction: attestCommand,
199
215
  authorizationGranted: false,
200
216
  };
201
217
  }
@@ -89,6 +89,7 @@ import {
89
89
  applyDetectedProfilesV2,
90
90
  detectProfiles,
91
91
  detectProfilesV2,
92
+ reconcileProfilesV2,
92
93
  resolveProfilesImpact,
93
94
  validateProfiles,
94
95
  } from "./profile-onboarding.mjs";
@@ -99,7 +100,14 @@ import {
99
100
  replaceFleetRegistry,
100
101
  rollbackFleetRegistry,
101
102
  } from "./fleet-discovery.mjs";
102
- import { routeRunKitAssuranceV1 } from "./assurance-router.mjs";
103
+ import {
104
+ RUNKIT_RISK_CATEGORIES,
105
+ routeRunKitAssuranceV1,
106
+ } from "./assurance-router.mjs";
107
+ import {
108
+ formatUsageModeHumanV1,
109
+ recommendUsageModeV1,
110
+ } from "./usage-mode.mjs";
103
111
  import { deriveHumanStatusFromInspectV1 } from "./human-status.mjs";
104
112
  import {
105
113
  closeDeploymentExecuteChildRun,
@@ -163,20 +171,20 @@ Usage:
163
171
  owlrunkit <command> [options]
164
172
 
165
173
  Start here:
174
+ owlrunkit mode recommend --workspace .
166
175
  owlrunkit doctor --workspace .
167
176
  owlrunkit profiles detect --workspace . --dry-run
168
177
  owlrunkit profiles detect --workspace . --apply
169
178
  owlrunkit assurance route --workspace . --request <risk-facts.json>
170
179
  owlrunkit adopt --workspace . --exact owlrunkit@${CORE_VERSION}
171
-
172
180
  Quick Verification
173
181
  owlrunkit quick-verify --workspace . -- <command> [args...]
174
182
  Low-risk, source-bound reusable evidence without writer leases or acceptance.
175
183
 
176
184
  Formal Delivery
177
185
  owlrunkit formal <start|check|finish> --workspace . --run-id <id> [...]
178
- Use for multi-writer, long-running, migration, integration, release, funds,
179
- production, or formal acceptance work.
186
+ Use for multi-writer, permissions, security, funds, irreversible change,
187
+ uncertain rollback, schema/data migration, or explicit Formal acceptance.
180
188
 
181
189
  Frozen dirty source
182
190
  owlrunkit candidate <freeze|verify|materialize> --workspace . [...]
@@ -204,6 +212,13 @@ Formal accepted does not grant Git or release authority; Quick cannot become For
204
212
  `;
205
213
 
206
214
  const COMMAND_HELP = Object.freeze({
215
+ adopt: `Usage:
216
+ owlrunkit adopt --workspace <directory> --exact owlrunkit@${CORE_VERSION}
217
+ [--registry <url>] [--timeout-ms <milliseconds>] [--json]
218
+
219
+ Verifies the exact registry release against the project-local install and writes
220
+ project-owned adoption evidence. It grants no Git or release authority.
221
+ `,
207
222
  assurance: `Usage:
208
223
  owlrunkit assurance route --workspace <directory> --request <risk-facts.json>
209
224
 
@@ -215,6 +230,45 @@ repository, release, deployment, or destructive authority.
215
230
 
216
231
  Required:
217
232
  --request <file> Structured assurance risk facts.
233
+ `,
234
+ mode: `Usage:
235
+ owlrunkit mode recommend --workspace <directory> [risk facts]
236
+ owlrunkit mode status --workspace <directory> [--json]
237
+
238
+ Selects continuity and assurance independently. Recommendation and status are
239
+ read-only and grant no Git, release, deployment, production, or business authority.
240
+ `,
241
+ "mode recommend": `Usage:
242
+ owlrunkit mode recommend --workspace <directory>
243
+ [--requested-mode auto|off|light|managed|formal]
244
+ [--needs-evidence] [--exact-command-count <count>] [--writer-count <count>]
245
+ [--cross-session] [--long-running] [--interruption-recovery]
246
+ [--may-mutate-project] [--external-side-effects]
247
+ [--irreversible] [--rollback-uncertain]
248
+ [--permission-sensitive] [--security-sensitive] [--funds-at-risk]
249
+ [--schema-or-data-migration] [--formal-acceptance]
250
+ [--risk-category <${RUNKIT_RISK_CATEGORIES.join("|")}>] [--json]
251
+
252
+ Returns one user mode plus the independent continuity and assurance selections.
253
+ Persisted mode preferences are not part of this release.
254
+ `,
255
+ "mode status": `Usage:
256
+ owlrunkit mode status --workspace <directory> [--json]
257
+
258
+ Reads the current mode preference source without creating project state.
259
+ `,
260
+ "quick-attest": `Usage:
261
+ owlrunkit quick-attest --workspace <directory> --receipt <receipt.json> [--json]
262
+
263
+ Attests one Quick Receipt against its bound materials and current workspace.
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.
218
272
  `,
219
273
  fleet: `Usage:
220
274
  owlrunkit fleet register-root --fleet-root <directory>
@@ -254,6 +308,7 @@ With no source option, discovers the complete fleet inside registered roots.
254
308
  `,
255
309
  profiles: `Usage:
256
310
  owlrunkit profiles detect --workspace <directory> [--dry-run | --apply]
311
+ owlrunkit profiles reconcile --workspace <directory> (--dry-run | --apply)
257
312
  owlrunkit profiles validate --workspace <directory>
258
313
  owlrunkit profiles impact --workspace <directory> --changed <path>
259
314
  `,
@@ -262,6 +317,12 @@ With no source option, discovers the complete fleet inside registered roots.
262
317
 
263
318
  --dry-run reports deterministic candidates. --apply atomically adopts only a
264
319
  high-confidence unambiguous profile.
320
+ `,
321
+ "profiles reconcile": `Usage:
322
+ owlrunkit profiles reconcile --workspace <directory> (--dry-run | --apply)
323
+
324
+ --dry-run previews deterministic additions and command updates without writing.
325
+ --apply atomically updates profiles.json and writes a hash-bound migration receipt.
265
326
  `,
266
327
  formal: `Usage:
267
328
  owlrunkit formal start --workspace <directory> --run-id <id> [...]
@@ -386,6 +447,18 @@ action without granting any authority.
386
447
  Project status is derived from durable work items, Agent assignments,
387
448
  dependencies, decisions, checkpoints, integration gates, and evidence refs.
388
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
389
462
  `,
390
463
  "project status": `Usage:
391
464
  owlrunkit project status --workspace <directory> [--json]
@@ -414,6 +487,25 @@ function withHumanOutput(result, humanOutput) {
414
487
  return result;
415
488
  }
416
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
+
417
509
  function formatTeamProjectAssignmentHuman(result) {
418
510
  const disposition = result.resumed ? "Resumed" : "Assigned";
419
511
  const nextAction = result.nextAction === null
@@ -1550,8 +1642,13 @@ function inspect(options) {
1550
1642
  throw new Error(`Execution was not found: ${requestedRunId}`);
1551
1643
  }
1552
1644
  const onboarding = onboardingProjection(root, recovery);
1645
+ const projectDefinitionPath = runtimePath(root, "project", "definition.json");
1646
+ const projectDriver = pathEntryExists(projectDefinitionPath)
1647
+ ? readTeamProjectStatusV1({ workspaceRoot: root })
1648
+ : null;
1553
1649
  const summary = buildInspectSummary(result, {
1554
1650
  maintenanceNextAction: onboarding.maintenanceNextAction ?? null,
1651
+ projectDriver,
1555
1652
  });
1556
1653
  const view = options.history
1557
1654
  ? { mode: "history" }
@@ -1563,7 +1660,13 @@ function inspect(options) {
1563
1660
  execution: executions.find(execution => execution.runId === requestedRunId),
1564
1661
  }
1565
1662
  : { mode: "summary" };
1566
- return { ...result, summary, view, onboarding };
1663
+ return {
1664
+ ...result,
1665
+ summary,
1666
+ view,
1667
+ onboarding,
1668
+ ...(projectDriver === null ? {} : { projectDriver }),
1669
+ };
1567
1670
  }
1568
1671
 
1569
1672
  function closeout(options) {
@@ -1784,6 +1887,7 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1784
1887
  "fleet",
1785
1888
  "formal",
1786
1889
  "lease",
1890
+ "mode",
1787
1891
  "profiles",
1788
1892
  "project",
1789
1893
  ]).has(command);
@@ -1806,7 +1910,9 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1806
1910
  }
1807
1911
  }
1808
1912
  const options = parseOptions(nested ? nestedOptions : optionValues, {
1809
- multi: command === "profiles" && action === "impact"
1913
+ multi: command === "mode"
1914
+ ? ["risk-category"]
1915
+ : command === "profiles" && action === "impact"
1810
1916
  ? ["changed"]
1811
1917
  : (command === "fleet" && action === "discover")
1812
1918
  || (command === "core-successor" && action === "plan")
@@ -1826,7 +1932,27 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1826
1932
  : command === "project" && action === "decision"
1827
1933
  ? ["blocking-work-item", "option", "evidence"]
1828
1934
  : [],
1829
- boolean: command === "inspect"
1935
+ boolean: command === "mode" && action === "recommend"
1936
+ ? [
1937
+ "json",
1938
+ "needs-evidence",
1939
+ "may-mutate-project",
1940
+ "external-side-effects",
1941
+ "cross-session",
1942
+ "long-running",
1943
+ "interruption-recovery",
1944
+ "dirty-parallel-workspace",
1945
+ "irreversible",
1946
+ "rollback-uncertain",
1947
+ "permission-sensitive",
1948
+ "security-sensitive",
1949
+ "funds-at-risk",
1950
+ "schema-or-data-migration",
1951
+ "formal-acceptance",
1952
+ ]
1953
+ : command === "mode" && action === "status"
1954
+ ? ["json"]
1955
+ : command === "inspect"
1830
1956
  ? ["json", "verbose", "history"]
1831
1957
  : command === "init"
1832
1958
  ? ["json"]
@@ -1887,6 +2013,97 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1887
2013
  const requestPath = realpathSync(requireOption(options, "request"));
1888
2014
  return routeRunKitAssuranceV1(readJson(requestPath));
1889
2015
  }
2016
+ if (command === "mode") {
2017
+ const root = workspace(options);
2018
+ if (action === "recommend") {
2019
+ assertOnlyOptions(options, [
2020
+ "workspace",
2021
+ "requested-mode",
2022
+ "needs-evidence",
2023
+ "exact-command-count",
2024
+ "writer-count",
2025
+ "may-mutate-project",
2026
+ "external-side-effects",
2027
+ "cross-session",
2028
+ "long-running",
2029
+ "interruption-recovery",
2030
+ "dirty-parallel-workspace",
2031
+ "irreversible",
2032
+ "rollback-uncertain",
2033
+ "permission-sensitive",
2034
+ "security-sensitive",
2035
+ "funds-at-risk",
2036
+ "schema-or-data-migration",
2037
+ "formal-acceptance",
2038
+ "risk-category",
2039
+ "json",
2040
+ ]);
2041
+ const recommendation = recommendUsageModeV1({
2042
+ workspaceRoot: root,
2043
+ requestedMode: options["requested-mode"] ?? "auto",
2044
+ needsDurableEvidence: options["needs-evidence"] === true,
2045
+ exactCommandCount: parseCanonicalNonnegativeSafeInteger(
2046
+ options["exact-command-count"] ?? "0",
2047
+ "exact-command-count",
2048
+ ),
2049
+ writerCount: parseCanonicalNonnegativeSafeInteger(
2050
+ options["writer-count"] ?? "1",
2051
+ "writer-count",
2052
+ ),
2053
+ mayMutateProject: options["may-mutate-project"] === true,
2054
+ externalSideEffects: options["external-side-effects"] === true,
2055
+ crossSession: options["cross-session"] === true,
2056
+ longRunning: options["long-running"] === true,
2057
+ interruptionRecovery: options["interruption-recovery"] === true,
2058
+ dirtyParallelWorkspace: options["dirty-parallel-workspace"] === true,
2059
+ irreversibleChange: options.irreversible === true,
2060
+ rollbackUncertain: options["rollback-uncertain"] === true,
2061
+ permissionSensitive: options["permission-sensitive"] === true,
2062
+ securitySensitive: options["security-sensitive"] === true,
2063
+ fundsAtRisk: options["funds-at-risk"] === true,
2064
+ schemaOrDataMigration: options["schema-or-data-migration"] === true,
2065
+ needsFormalAcceptance: options["formal-acceptance"] === true,
2066
+ riskCategories: options["risk-category"] ?? [],
2067
+ });
2068
+ return options.json === true
2069
+ ? recommendation
2070
+ : withHumanOutput(recommendation, formatUsageModeHumanV1(recommendation));
2071
+ }
2072
+ if (action === "status") {
2073
+ assertOnlyOptions(options, ["workspace", "json"]);
2074
+ const recommendation = recommendUsageModeV1({
2075
+ workspaceRoot: root,
2076
+ requestedMode: "auto",
2077
+ needsDurableEvidence: false,
2078
+ exactCommandCount: 0,
2079
+ writerCount: 1,
2080
+ mayMutateProject: false,
2081
+ externalSideEffects: false,
2082
+ crossSession: false,
2083
+ longRunning: false,
2084
+ interruptionRecovery: false,
2085
+ dirtyParallelWorkspace: false,
2086
+ irreversibleChange: false,
2087
+ rollbackUncertain: false,
2088
+ permissionSensitive: false,
2089
+ securitySensitive: false,
2090
+ fundsAtRisk: false,
2091
+ schemaOrDataMigration: false,
2092
+ needsFormalAcceptance: false,
2093
+ riskCategories: [],
2094
+ });
2095
+ const status = {
2096
+ ...recommendation,
2097
+ status: "mode_status",
2098
+ preferenceSource: "default_auto",
2099
+ persistedPreference: null,
2100
+ };
2101
+ return options.json === true
2102
+ ? status
2103
+ : withHumanOutput(status, formatUsageModeHumanV1(status));
2104
+ }
2105
+ throw new Error("Usage: owlrunkit mode <recommend|status> --workspace <directory>");
2106
+ }
1890
2107
  if (command === "fleet") {
1891
2108
  if (action === "register-root") {
1892
2109
  const registered = await registerFleetCoverageRoot({
@@ -2897,24 +3114,49 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
2897
3114
  event: requestedEvent,
2898
3115
  });
2899
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");
2900
3123
  const completedUnits = options["completed-units"] === undefined
2901
3124
  ? undefined
2902
3125
  : parseCanonicalNonnegativeSafeInteger(options["completed-units"], "completed-units");
2903
- const checkpointed = checkpointTeamProjectV1({
2904
- workspaceRoot: root,
2905
- checkpointId: requireOption(options, "checkpoint-id"),
2906
- occurredAt: requireOption(options, "at"),
2907
- assignmentId: requireOption(options, "assignment-id"),
2908
- workItemId: requireOption(options, "work-item"),
2909
- state: requireOption(options, "state"),
2910
- summary: requireOption(options, "summary"),
2911
- completedUnits,
2912
- evidenceRefs: options.evidence ?? [],
2913
- blockerRefs: options.blocker ?? [],
2914
- decisionRefs: options.decision ?? [],
2915
- nextAction: options.next ?? null,
2916
- sourceFingerprint: options["source-fingerprint"],
2917
- });
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
+ }
2918
3160
  return options.json === true
2919
3161
  ? checkpointed
2920
3162
  : withHumanOutput(checkpointed, formatTeamProjectCheckpointHuman(checkpointed));
@@ -3002,13 +3244,25 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3002
3244
  ? applyDetectedProfilesV2({ workspaceRoot: root, detection })
3003
3245
  : detection;
3004
3246
  }
3247
+ if (action === "reconcile") {
3248
+ if (options.apply === true && options["dry-run"] === true) {
3249
+ throw new Error("profiles reconcile accepts either --apply or --dry-run, not both.");
3250
+ }
3251
+ if (options.apply !== true && options["dry-run"] !== true) {
3252
+ throw new Error("profiles reconcile requires --dry-run or --apply.");
3253
+ }
3254
+ return reconcileProfilesV2({
3255
+ workspaceRoot: root,
3256
+ apply: options.apply === true,
3257
+ });
3258
+ }
3005
3259
  if (action === "validate") return validateProfiles({ workspaceRoot: root });
3006
3260
  if (action === "impact") {
3007
3261
  const changedPaths = options.changed ?? [];
3008
3262
  if (changedPaths.length === 0) throw new Error("profiles impact requires --changed.");
3009
3263
  return resolveProfilesImpact({ workspaceRoot: root, changedPaths });
3010
3264
  }
3011
- throw new Error("Usage: owlrunkit profiles <detect|validate|impact> [options]");
3265
+ throw new Error("Usage: owlrunkit profiles <detect|reconcile|validate|impact> [options]");
3012
3266
  }
3013
3267
  if (command === "lease") {
3014
3268
  const root = workspace(options);
@@ -3151,7 +3405,7 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3151
3405
  return withControlTransaction(root, () => closeout(options));
3152
3406
  }
3153
3407
  if (command === "finish") return finishExecution(options);
3154
- throw new Error("Usage: owlrunkit <init|doctor|adopt|assurance|fleet|profiles|project|status|formal|candidate|deployment|core-successor|start|verify|repair|offline-export|offline-import|quick-verify|quick-attest|quick-metrics|finish|plan|inspect|lease|delivery|coverage-adopt|resume|verify-plan|resource-preflight|snapshot|visual-smoke|finalize|ready-for-commit|closeout> [options]");
3408
+ throw new Error("Usage: owlrunkit <init|doctor|adopt|mode|assurance|fleet|profiles|project|status|formal|candidate|deployment|core-successor|start|verify|repair|offline-export|offline-import|quick-verify|quick-attest|quick-metrics|finish|plan|inspect|lease|delivery|coverage-adopt|resume|verify-plan|resource-preflight|snapshot|visual-smoke|finalize|ready-for-commit|closeout> [options]");
3155
3409
  } catch (error) {
3156
3410
  return {
3157
3411
  status: "invalid_input",