owlrunkit 0.18.1 → 0.18.3

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 (32) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +85 -19
  3. package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +16 -5
  4. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +78 -4
  5. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +7 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -9
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.1.md +160 -0
  9. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +545 -0
  10. package/package.json +1 -1
  11. package/packages/attest/README.md +4 -3
  12. package/packages/attest/consumer-surface-v1.json +1 -0
  13. package/packages/attest/distribution-manifest-v1.json +12 -1
  14. package/packages/attest/schemas/quick-verification-receipt-v2.schema.json +232 -0
  15. package/packages/attest/src/index.mjs +4 -1
  16. package/packages/attest/src/quick-receipt-contract.mjs +13 -3
  17. package/packages/attest/src/quick.mjs +21 -2
  18. package/registry-release-policy-v1.json +5 -5
  19. package/sbom.spdx.json +5 -5
  20. package/scripts/runkit-contract/assurance-router.mjs +3 -1
  21. package/scripts/runkit-contract/core-contract.mjs +2 -1
  22. package/scripts/runkit-contract/onboarding-doctor.mjs +46 -18
  23. package/scripts/runkit-contract/profile-onboarding.mjs +4 -0
  24. package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
  25. package/scripts/runkit-contract/quick-metrics.mjs +3 -3
  26. package/scripts/runkit-contract/quick-receipt.mjs +110 -5
  27. package/scripts/runkit-contract/quick-verify.mjs +93 -11
  28. package/scripts/runkit-contract/runkit-cli.mjs +172 -24
  29. package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +2 -0
  30. package/scripts/runkit-contract/schemas/team-project-event-v2.schema.json +128 -0
  31. package/scripts/runkit-contract/team-project.mjs +310 -45
  32. package/scripts/runkit-contract/usage-mode.mjs +11 -13
@@ -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,56 @@ 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 cleanOutputLines(text) {
101
+ return text
102
+ .split(/\r?\n/u)
103
+ .map(cleanOutputLine)
104
+ .filter(Boolean);
105
+ }
106
+
107
+ function summarizeOutputBytes(bytes) {
108
+ if (bytes.byteLength <= OUTPUT_SUMMARY_BYTES) {
109
+ const lines = cleanOutputLines(bytes.toString("utf8"));
110
+ return {
111
+ headLines: lines.slice(0, OUTPUT_SUMMARY_LINES),
112
+ tailLines: lines.slice(-OUTPUT_SUMMARY_LINES),
113
+ truncated: lines.length > OUTPUT_SUMMARY_LINES * 2,
114
+ };
115
+ }
116
+
117
+ const windowBytes = Math.floor(OUTPUT_SUMMARY_BYTES / 2);
118
+ let headText = bytes.subarray(0, windowBytes).toString("utf8");
119
+ const lastHeadBreak = headText.lastIndexOf("\n");
120
+ if (lastHeadBreak >= 0) headText = headText.slice(0, lastHeadBreak + 1);
121
+
122
+ let tailText = bytes.subarray(bytes.byteLength - windowBytes).toString("utf8");
123
+ const firstTailBreak = tailText.indexOf("\n");
124
+ if (firstTailBreak >= 0) tailText = tailText.slice(firstTailBreak + 1);
125
+
126
+ return {
127
+ headLines: cleanOutputLines(headText).slice(0, OUTPUT_SUMMARY_LINES),
128
+ tailLines: cleanOutputLines(tailText).slice(-OUTPUT_SUMMARY_LINES),
129
+ truncated: true,
130
+ };
131
+ }
132
+
90
133
  function outputArtifact(workspaceRoot, filePath) {
91
134
  const bytes = readFileSync(filePath);
92
135
  return {
93
- path: path.relative(realpathSync(workspaceRoot), filePath).split(path.sep).join("/"),
94
- sha256: sha256Bytes(bytes),
95
- sizeBytes: bytes.byteLength,
136
+ artifact: {
137
+ path: path.relative(realpathSync(workspaceRoot), filePath).split(path.sep).join("/"),
138
+ sha256: sha256Bytes(bytes),
139
+ sizeBytes: bytes.byteLength,
140
+ },
141
+ summary: summarizeOutputBytes(bytes),
96
142
  };
97
143
  }
98
144
 
@@ -115,15 +161,66 @@ function validateQuickReceiptStore(workspaceRoot, store) {
115
161
  assertStorePath(root, store.receiptRoot, store.receiptRootIdentity, "directory");
116
162
  assertStorePath(root, store.stdoutPath, store.stdoutIdentity, "file");
117
163
  assertStorePath(root, store.stderrPath, store.stderrIdentity, "file");
164
+ if (store.stdinPath !== undefined) {
165
+ assertStorePath(root, store.stdinPath, store.stdinIdentity, "file");
166
+ const bytes = readFileSync(store.stdinPath);
167
+ if (
168
+ bytes.byteLength !== store.stdinArtifact.sizeBytes
169
+ || sha256Bytes(bytes) !== store.stdinArtifact.sha256
170
+ ) {
171
+ throw new Error(`Quick receipt store stdin material changed: ${store.stdinPath}`);
172
+ }
173
+ }
174
+ }
175
+
176
+ export function persistQuickInputArtifact({ workspaceRoot, store, bytes }) {
177
+ if (!Buffer.isBuffer(bytes)) throw new Error("Quick stdin material must be bytes.");
178
+ validateQuickReceiptStore(workspaceRoot, store);
179
+ const stdinPath = path.join(store.receiptRoot, "stdin.bin");
180
+ writeFileSync(stdinPath, bytes, { flag: "wx", mode: 0o600 });
181
+ const stat = lstatSync(stdinPath);
182
+ if (stat.isSymbolicLink() || !stat.isFile()) {
183
+ throw new Error(`Quick receipt store stdin material is not trusted: ${stdinPath}`);
184
+ }
185
+ const root = realpathSync(workspaceRoot);
186
+ const resolved = realpathSync(stdinPath);
187
+ if (!withinRoot(root, resolved)) {
188
+ throw new Error(`Quick receipt store stdin material escapes the workspace: ${stdinPath}`);
189
+ }
190
+ store.stdinPath = stdinPath;
191
+ store.stdinIdentity = { dev: stat.dev, ino: stat.ino };
192
+ store.stdinArtifact = {
193
+ path: path.relative(root, resolved).split(path.sep).join("/"),
194
+ sha256: sha256Bytes(bytes),
195
+ sizeBytes: bytes.byteLength,
196
+ };
197
+ return {
198
+ artifact: store.stdinArtifact,
199
+ inputPath: stdinPath,
200
+ };
118
201
  }
119
202
 
120
203
  export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
121
204
  validateQuickReceiptStore(workspaceRoot, store);
205
+ const receiptStdin = receipt.inputArtifacts?.stdin;
206
+ if (
207
+ (store.stdinArtifact === undefined) !== (receiptStdin === undefined)
208
+ || store.stdinArtifact !== undefined
209
+ && (
210
+ receiptStdin.path !== store.stdinArtifact.path
211
+ || receiptStdin.sha256 !== store.stdinArtifact.sha256
212
+ || receiptStdin.sizeBytes !== store.stdinArtifact.sizeBytes
213
+ )
214
+ ) {
215
+ throw new Error("Quick receipt stdin binding does not match the persisted material.");
216
+ }
217
+ const stdout = outputArtifact(workspaceRoot, store.stdoutPath);
218
+ const stderr = outputArtifact(workspaceRoot, store.stderrPath);
122
219
  const complete = {
123
220
  ...receipt,
124
221
  outputArtifacts: {
125
- stdout: outputArtifact(workspaceRoot, store.stdoutPath),
126
- stderr: outputArtifact(workspaceRoot, store.stderrPath),
222
+ stdout: stdout.artifact,
223
+ stderr: stderr.artifact,
127
224
  },
128
225
  };
129
226
  atomicWriteJson(store.receiptPath, complete);
@@ -131,6 +228,14 @@ export function persistQuickReceipt({ workspaceRoot, store, receipt }) {
131
228
  receipt: complete,
132
229
  receiptPath: store.receiptPath,
133
230
  receiptSha256: sha256Bytes(readFileSync(store.receiptPath)),
231
+ outputSummary: {
232
+ stdoutHead: stdout.summary.headLines,
233
+ stdoutTail: stdout.summary.tailLines,
234
+ stderrHead: stderr.summary.headLines,
235
+ stderrTail: stderr.summary.tailLines,
236
+ stdoutTruncated: stdout.summary.truncated,
237
+ stderrTruncated: stderr.summary.truncated,
238
+ },
134
239
  };
135
240
  }
136
241
 
@@ -1,10 +1,18 @@
1
1
  import { spawn } from "node:child_process";
2
- import { realpathSync } from "node:fs";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ openSync,
6
+ realpathSync,
7
+ } from "node:fs";
8
+ import path from "node:path";
3
9
 
10
+ import { readFileBytesBounded } from "../../packages/attest/src/formal.mjs";
4
11
  import { currentCoreIdentity } from "./core-contract.mjs";
5
12
  import {
6
13
  closeQuickOutputFiles,
7
14
  createQuickReceiptStore,
15
+ persistQuickInputArtifact,
8
16
  persistQuickReceipt,
9
17
  } from "./quick-receipt.mjs";
10
18
  import { captureWorkspaceSnapshot } from "./quick-workspace-snapshot.mjs";
@@ -17,15 +25,31 @@ function quickAttestCommand(workspaceRoot, receiptPath) {
17
25
  return `owlrunkit quick-attest --workspace ${shellQuote(workspaceRoot)} --receipt ${shellQuote(receiptPath)}`;
18
26
  }
19
27
 
20
- function runExactCommand({ executable, argv, cwd, stdoutFd, stderrFd }) {
28
+ function runExactCommand({ executable, argv, cwd, stdinPath, stdoutFd, stderrFd }) {
21
29
  return new Promise((resolve) => {
22
30
  let settled = false;
23
- const child = spawn(executable, argv, {
24
- cwd,
25
- shell: false,
26
- stdio: ["ignore", stdoutFd, stderrFd],
27
- env: process.env,
28
- });
31
+ let stdinFd;
32
+ let child;
33
+ try {
34
+ stdinFd = stdinPath === undefined
35
+ ? undefined
36
+ : openSync(stdinPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
37
+ child = spawn(executable, argv, {
38
+ cwd,
39
+ shell: false,
40
+ stdio: [stdinFd ?? "ignore", stdoutFd, stderrFd],
41
+ env: process.env,
42
+ });
43
+ } catch (error) {
44
+ if (stdinFd !== undefined) closeSync(stdinFd);
45
+ resolve({
46
+ exitCode: null,
47
+ signal: null,
48
+ launchError: error instanceof Error ? error.message : String(error),
49
+ });
50
+ return;
51
+ }
52
+ if (stdinFd !== undefined) closeSync(stdinFd);
29
53
  child.once("error", (error) => {
30
54
  if (settled) return;
31
55
  settled = true;
@@ -39,7 +63,13 @@ function runExactCommand({ executable, argv, cwd, stdoutFd, stderrFd }) {
39
63
  });
40
64
  }
41
65
 
42
- export async function runQuickVerification({ workspaceRoot, commandArgv }) {
66
+ function inputArtifactResult(stdinArtifact) {
67
+ return stdinArtifact === null
68
+ ? {}
69
+ : { inputArtifacts: { stdin: stdinArtifact } };
70
+ }
71
+
72
+ export async function runQuickVerification({ workspaceRoot, commandArgv, stdinFile }) {
43
73
  if (!Array.isArray(commandArgv) || commandArgv.length === 0 || !commandArgv.every((value) => typeof value === "string")) {
44
74
  return {
45
75
  status: "quick_verification_input_invalid",
@@ -60,12 +90,49 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
60
90
  };
61
91
  }
62
92
 
93
+ let stdinBytes;
94
+ if (stdinFile !== undefined) {
95
+ if (typeof stdinFile !== "string" || stdinFile.length === 0) {
96
+ return {
97
+ status: "quick_verification_input_invalid",
98
+ exitCode: 3,
99
+ issues: ["--stdin-file requires a non-empty regular file path."],
100
+ authorizationGranted: false,
101
+ };
102
+ }
103
+ try {
104
+ const selected = path.isAbsolute(stdinFile)
105
+ ? stdinFile
106
+ : path.resolve(root, stdinFile);
107
+ stdinBytes = readFileBytesBounded(selected).bytes;
108
+ } catch (error) {
109
+ return {
110
+ status: "quick_verification_input_invalid",
111
+ exitCode: 3,
112
+ issues: [`Quick stdin file is invalid: ${error instanceof Error ? error.message : String(error)}`],
113
+ authorizationGranted: false,
114
+ };
115
+ }
116
+ }
117
+
63
118
  let workspaceBefore;
64
119
  let store;
120
+ let stdinArtifact = null;
121
+ let persistedStdinPath;
65
122
  try {
66
123
  workspaceBefore = captureWorkspaceSnapshot(root);
67
124
  store = createQuickReceiptStore(root);
125
+ if (stdinBytes !== undefined) {
126
+ const persistedInput = persistQuickInputArtifact({
127
+ workspaceRoot: root,
128
+ store,
129
+ bytes: stdinBytes,
130
+ });
131
+ stdinArtifact = persistedInput.artifact;
132
+ persistedStdinPath = persistedInput.inputPath;
133
+ }
68
134
  } catch (error) {
135
+ if (store !== undefined) closeQuickOutputFiles(store);
69
136
  return {
70
137
  status: "quick_verification_input_invalid",
71
138
  exitCode: 3,
@@ -80,6 +147,7 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
80
147
  executable,
81
148
  argv,
82
149
  cwd: root,
150
+ stdinPath: persistedStdinPath,
83
151
  stdoutFd: store.stdoutFd,
84
152
  stderrFd: store.stderrFd,
85
153
  });
@@ -101,7 +169,7 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
101
169
  }
102
170
 
103
171
  const sourceMutated = workspaceBefore.sourceFingerprint !== workspaceAfter.sourceFingerprint;
104
- const issueCodes = ["quick_ignored_artifact_unbound"];
172
+ const issueCodes = [];
105
173
  if (sourceMutated) issueCodes.push("source_mutated_during_verification");
106
174
  let persisted;
107
175
  try {
@@ -109,7 +177,9 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
109
177
  workspaceRoot: root,
110
178
  store,
111
179
  receipt: {
112
- schemaVersion: "OwlCodaQuickVerificationReceiptV1",
180
+ schemaVersion: stdinArtifact === null
181
+ ? "OwlCodaQuickVerificationReceiptV1"
182
+ : "OwlCodaQuickVerificationReceiptV2",
113
183
  receiptId: store.receiptId,
114
184
  assurance: "captured_verification",
115
185
  authorizationGranted: false,
@@ -124,6 +194,9 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
124
194
  argv,
125
195
  cwd: root,
126
196
  },
197
+ ...(stdinArtifact === null
198
+ ? {}
199
+ : { inputArtifacts: { stdin: stdinArtifact } }),
127
200
  verificationContext: {
128
201
  platform: process.platform,
129
202
  architecture: process.arch,
@@ -154,12 +227,15 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
154
227
  ? "invalidated_by_command_source_mutation"
155
228
  : "source_unchanged",
156
229
  issueCodes: [...issueCodes, "quick_receipt_store_invalid"],
230
+ ...inputArtifactResult(stdinArtifact),
157
231
  issues: [error instanceof Error ? error.message : String(error)],
158
232
  nextAllowedAction: "repair_quick_receipt_store_and_rerun",
159
233
  authorizationGranted: false,
160
234
  };
161
235
  }
162
236
 
237
+ const { outputSummary } = persisted;
238
+
163
239
  if (sourceMutated) {
164
240
  const attestCommand = quickAttestCommand(root, persisted.receiptPath);
165
241
  return {
@@ -171,6 +247,8 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
171
247
  commandExitCode: execution.exitCode,
172
248
  mutationDecision: "invalidated_by_command_source_mutation",
173
249
  issueCodes,
250
+ ...inputArtifactResult(stdinArtifact),
251
+ outputSummary,
174
252
  attestCommand,
175
253
  issues: [],
176
254
  nextAllowedAction: "restore_source_or_run_a_new_quick_verification",
@@ -188,6 +266,8 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
188
266
  commandExitCode: execution.exitCode,
189
267
  mutationDecision: "source_unchanged",
190
268
  issueCodes,
269
+ ...inputArtifactResult(stdinArtifact),
270
+ outputSummary,
191
271
  attestCommand,
192
272
  issues: execution.launchError ? [execution.launchError] : [],
193
273
  nextAllowedAction: "inspect_output_and_rerun",
@@ -204,6 +284,8 @@ export async function runQuickVerification({ workspaceRoot, commandArgv }) {
204
284
  commandExitCode: 0,
205
285
  mutationDecision: "source_unchanged",
206
286
  issueCodes,
287
+ ...inputArtifactResult(stdinArtifact),
288
+ outputSummary,
207
289
  attestCommand,
208
290
  issues: [],
209
291
  nextAllowedAction: attestCommand,
@@ -100,7 +100,10 @@ import {
100
100
  replaceFleetRegistry,
101
101
  rollbackFleetRegistry,
102
102
  } from "./fleet-discovery.mjs";
103
- import { routeRunKitAssuranceV1 } from "./assurance-router.mjs";
103
+ import {
104
+ RUNKIT_RISK_CATEGORIES,
105
+ routeRunKitAssuranceV1,
106
+ } from "./assurance-router.mjs";
104
107
  import {
105
108
  formatUsageModeHumanV1,
106
109
  recommendUsageModeV1,
@@ -155,6 +158,7 @@ import {
155
158
  integrateTeamProjectV1,
156
159
  openTeamProjectDecisionV1,
157
160
  readTeamProjectStatusV1,
161
+ rejectAndReturnTeamProjectV1,
158
162
  resolveTeamProjectDecisionV1,
159
163
  } from "./team-project.mjs";
160
164
  import {
@@ -197,7 +201,7 @@ Fleet and status
197
201
  owlrunkit fleet discover; owlrunkit status --workspace .
198
202
 
199
203
  Agent team projects
200
- owlrunkit project <init|assign|checkpoint|handoff|decision|verification|capture|status|takeover|successor> [options]
204
+ owlrunkit project <init|assign|checkpoint|handoff|reject-and-return|decision|verification|capture|status|takeover|successor> [options]
201
205
  Durable Agent work, dependencies, decisions, gates, progress, and handoffs.
202
206
  Core successor
203
207
  owlrunkit core-successor <plan|apply|resume> --workspace . [...]
@@ -244,7 +248,7 @@ read-only and grant no Git, release, deployment, production, or business authori
244
248
  [--irreversible] [--rollback-uncertain]
245
249
  [--permission-sensitive] [--security-sensitive] [--funds-at-risk]
246
250
  [--schema-or-data-migration] [--formal-acceptance]
247
- [--risk-category <category>] [--json]
251
+ [--risk-category <${RUNKIT_RISK_CATEGORIES.join("|")}>] [--json]
248
252
 
249
253
  Returns one user mode plus the independent continuity and assurance selections.
250
254
  Persisted mode preferences are not part of this release.
@@ -259,6 +263,22 @@ Reads the current mode preference source without creating project state.
259
263
 
260
264
  Attests one Quick Receipt against its bound materials and current workspace.
261
265
  It never grants repository or business authority.
266
+ `,
267
+ "quick-verify": `Usage:
268
+ owlrunkit quick-verify --workspace <directory> [--stdin-file <file>] [--attest] [--json]
269
+ -- <executable> [args...]
270
+
271
+ Runs one exact command, saves source-bound stdout/stderr and a Quick receipt,
272
+ and returns a bounded head/tail output summary. --stdin-file copies and hash-binds
273
+ up to 1 MiB of exact stdin bytes in the receipt store. --attest immediately
274
+ attests the saved receipt without merging Quick and attestation semantics.
275
+ Quick evidence is not Formal acceptance and grants no repository or business authority.
276
+ `,
277
+ "quick-metrics": `Usage:
278
+ owlrunkit quick-metrics --workspace <directory> --local [--verbose] [--json]
279
+
280
+ Returns aggregate local-only Quick counts with no telemetry or network access.
281
+ --verbose additionally lists the receipt paths used for the aggregation.
262
282
  `,
263
283
  fleet: `Usage:
264
284
  owlrunkit fleet register-root --fleet-root <directory>
@@ -409,6 +429,9 @@ action without granting any authority.
409
429
  owlrunkit project handoff --workspace <directory> --handoff-id <id> --at <ISO-UTC>
410
430
  --assignment-id <current> --work-item <id> --from-agent <id> --to-agent <id>
411
431
  --summary <text> --next <text> --evidence <ref> [--evidence <ref> ...] [--json]
432
+ owlrunkit project reject-and-return --workspace <directory> --rework-id <id> --at <ISO-UTC>
433
+ --work-item <failed-id> --reviewer-agent <id> --to-agent <id>
434
+ --reason <text> --next <text> --evidence <ref> [--evidence <ref> ...] [--json]
412
435
  owlrunkit project decision --workspace <directory> --open --decision-id <id> --at <ISO-UTC>
413
436
  --title <text> --question <text> --owner-agent <id>
414
437
  [--blocking-work-item <id> ...] --option <text> [--option <text> ...] [--json]
@@ -437,6 +460,27 @@ action without granting any authority.
437
460
  Project status is derived from durable work items, Agent assignments,
438
461
  dependencies, decisions, checkpoints, integration gates, and evidence refs.
439
462
  No self-reported percentage is accepted.
463
+ `,
464
+ "project checkpoint": `Usage:
465
+ owlrunkit project checkpoint --workspace <directory> --checkpoint-id <id> --at <ISO-UTC>
466
+ --assignment-id <current> --work-item <id>
467
+ --state <active|waiting_dependency|waiting_decision|verifying|ready_to_integrate|completed|failed>
468
+ --summary <text> [--completed-units <nonnegative-int>]
469
+ [--evidence <ref> ...] [--blocker <ref> ...] [--decision <id> ...]
470
+ [--next <text>] [--source-fingerprint <sha256>] [--json]
471
+
472
+ Measured WorkItems require --completed-units on every checkpoint. A completed
473
+ checkpoint must use the WorkItem's declared total. Example:
474
+ 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
475
+ `,
476
+ "project reject-and-return": `Usage:
477
+ owlrunkit project reject-and-return --workspace <directory> --rework-id <id> --at <ISO-UTC>
478
+ --work-item <failed-id> --reviewer-agent <id> --to-agent <id>
479
+ --reason <text> --next <text> --evidence <ref> [--evidence <ref> ...] [--json]
480
+
481
+ Atomically records the rejection and opens a new assigned rework attempt. The
482
+ failed attempt remains immutable and evidence-linked. This action does not
483
+ grant Git, release, deployment, production, or business authority.
440
484
  `,
441
485
  "project status": `Usage:
442
486
  owlrunkit project status --workspace <directory> [--json]
@@ -465,6 +509,25 @@ function withHumanOutput(result, humanOutput) {
465
509
  return result;
466
510
  }
467
511
 
512
+ function shellCommandToken(value) {
513
+ const text = String(value);
514
+ return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(text)
515
+ ? text
516
+ : `'${text.replaceAll("'", `'"'"'`)}'`;
517
+ }
518
+
519
+ function correctedCheckpointCommand(argv, completedUnits) {
520
+ const corrected = [...argv];
521
+ const jsonIndex = corrected.indexOf("--json");
522
+ corrected.splice(
523
+ jsonIndex === -1 ? corrected.length : jsonIndex,
524
+ 0,
525
+ "--completed-units",
526
+ String(completedUnits),
527
+ );
528
+ return `owlrunkit ${corrected.map(shellCommandToken).join(" ")}`;
529
+ }
530
+
468
531
  function formatTeamProjectAssignmentHuman(result) {
469
532
  const disposition = result.resumed ? "Resumed" : "Assigned";
470
533
  const nextAction = result.nextAction === null
@@ -495,6 +558,20 @@ function formatTeamProjectHandoffHuman(result) {
495
558
  ].join("\n");
496
559
  }
497
560
 
561
+ function formatTeamProjectReworkHuman(result) {
562
+ const disposition = result.resumed ? "Resumed" : "Returned";
563
+ const nextAction = result.nextAction === null
564
+ ? "none"
565
+ : String(result.nextAction).replace(/\s+/gu, " ").trim();
566
+ return [
567
+ `${disposition} failed WorkItem ${result.workItemId} to Agent ${result.toAgentId} for rework attempt ${result.reworkAttempt}.`,
568
+ `Failed checkpoint preserved: ${result.failedCheckpointEventId}`,
569
+ `Next: ${nextAction}`,
570
+ `Rework assignment: ${result.assignmentId}`,
571
+ "",
572
+ ].join("\n");
573
+ }
574
+
498
575
  function formatTeamProjectDecisionHuman(result) {
499
576
  const disposition = result.resumed
500
577
  ? "Resumed"
@@ -1882,6 +1959,8 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1882
1959
  ? ["owned-path"]
1883
1960
  : command === "project" && action === "handoff"
1884
1961
  ? ["evidence"]
1962
+ : command === "project" && action === "reject-and-return"
1963
+ ? ["evidence"]
1885
1964
  : command === "project" && action === "integrate"
1886
1965
  ? ["evidence"]
1887
1966
  : command === "project" && action === "verification"
@@ -1919,7 +1998,7 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1919
1998
  ? ["json", "open", "resolve"]
1920
1999
  : command === "project" && action === "verification"
1921
2000
  ? ["json", "defer", "close"]
1922
- : command === "project" && new Set(["assign", "handoff", "checkpoint", "integrate", "status", "takeover", "successor"]).has(action)
2001
+ : command === "project" && new Set(["assign", "handoff", "reject-and-return", "checkpoint", "integrate", "status", "takeover", "successor"]).has(action)
1923
2002
  ? ["json"]
1924
2003
  : command === "doctor" || command === "adopt" || command === "status"
1925
2004
  ? ["json"]
@@ -1933,8 +2012,12 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
1933
2012
  ? ["adopt-orphan-success-receipts"]
1934
2013
  : command === "deployment" && action === "execute"
1935
2014
  ? ["resume"]
1936
- : new Set(["quick-verify", "quick-attest", "quick-metrics"]).has(command)
1937
- ? ["json", "local"]
2015
+ : command === "quick-verify"
2016
+ ? ["json", "attest"]
2017
+ : command === "quick-attest"
2018
+ ? ["json"]
2019
+ : command === "quick-metrics"
2020
+ ? ["json", "local", "verbose"]
1938
2021
  : [],
1939
2022
  });
1940
2023
  if (command === "init") return await initialize(options);
@@ -2770,6 +2853,10 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
2770
2853
  "workspace", "request", "handoff-id", "at", "assignment-id", "work-item",
2771
2854
  "from-agent", "to-agent", "summary", "next", "evidence", "json",
2772
2855
  ],
2856
+ "reject-and-return": [
2857
+ "workspace", "rework-id", "at", "work-item", "reviewer-agent",
2858
+ "to-agent", "reason", "next", "evidence", "json",
2859
+ ],
2773
2860
  decision: [
2774
2861
  "workspace", "request", "open", "resolve", "decision-id", "at", "title",
2775
2862
  "question", "owner-agent", "blocking-work-item", "option", "resolution",
@@ -2877,6 +2964,22 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
2877
2964
  ? handedOff
2878
2965
  : withHumanOutput(handedOff, formatTeamProjectHandoffHuman(handedOff));
2879
2966
  }
2967
+ if (action === "reject-and-return") {
2968
+ const returned = rejectAndReturnTeamProjectV1({
2969
+ workspaceRoot: root,
2970
+ reworkId: requireOption(options, "rework-id"),
2971
+ occurredAt: requireOption(options, "at"),
2972
+ workItemId: requireOption(options, "work-item"),
2973
+ reviewerAgentId: requireOption(options, "reviewer-agent"),
2974
+ toAgentId: requireOption(options, "to-agent"),
2975
+ reason: requireOption(options, "reason"),
2976
+ nextAction: requireOption(options, "next"),
2977
+ evidenceRefs: options.evidence ?? [],
2978
+ });
2979
+ return options.json === true
2980
+ ? returned
2981
+ : withHumanOutput(returned, formatTeamProjectReworkHuman(returned));
2982
+ }
2880
2983
  if (action === "decision") {
2881
2984
  const directDecisionFields = [
2882
2985
  "open",
@@ -3073,24 +3176,49 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3073
3176
  event: requestedEvent,
3074
3177
  });
3075
3178
  }
3179
+ const checkpointId = requireOption(options, "checkpoint-id");
3180
+ const occurredAt = requireOption(options, "at");
3181
+ const assignmentId = requireOption(options, "assignment-id");
3182
+ const workItemId = requireOption(options, "work-item");
3183
+ const state = requireOption(options, "state");
3184
+ const summary = requireOption(options, "summary");
3076
3185
  const completedUnits = options["completed-units"] === undefined
3077
3186
  ? undefined
3078
3187
  : parseCanonicalNonnegativeSafeInteger(options["completed-units"], "completed-units");
3079
- const checkpointed = checkpointTeamProjectV1({
3080
- workspaceRoot: root,
3081
- checkpointId: requireOption(options, "checkpoint-id"),
3082
- occurredAt: requireOption(options, "at"),
3083
- assignmentId: requireOption(options, "assignment-id"),
3084
- workItemId: requireOption(options, "work-item"),
3085
- state: requireOption(options, "state"),
3086
- summary: requireOption(options, "summary"),
3087
- completedUnits,
3088
- evidenceRefs: options.evidence ?? [],
3089
- blockerRefs: options.blocker ?? [],
3090
- decisionRefs: options.decision ?? [],
3091
- nextAction: options.next ?? null,
3092
- sourceFingerprint: options["source-fingerprint"],
3093
- });
3188
+ let checkpointed;
3189
+ try {
3190
+ checkpointed = checkpointTeamProjectV1({
3191
+ workspaceRoot: root,
3192
+ checkpointId,
3193
+ occurredAt,
3194
+ assignmentId,
3195
+ workItemId,
3196
+ state,
3197
+ summary,
3198
+ completedUnits,
3199
+ evidenceRefs: options.evidence ?? [],
3200
+ blockerRefs: options.blocker ?? [],
3201
+ decisionRefs: options.decision ?? [],
3202
+ nextAction: options.next ?? null,
3203
+ sourceFingerprint: options["source-fingerprint"],
3204
+ });
3205
+ } catch (error) {
3206
+ if (error instanceof Error
3207
+ && error.message === "Measured work item checkpoint requires completedUnits.") {
3208
+ const projectStatus = readTeamProjectStatusV1({ workspaceRoot: root });
3209
+ const workItem = projectStatus.workItems.find((row) => row.workItemId === workItemId);
3210
+ const suggestedUnits = state === "completed"
3211
+ ? workItem?.progress.total
3212
+ : workItem?.progress.completed;
3213
+ if (Number.isSafeInteger(suggestedUnits)) {
3214
+ throw new Error(
3215
+ "Measured work item checkpoint requires --completed-units. "
3216
+ + `Corrected command: ${correctedCheckpointCommand(argv, suggestedUnits)}`,
3217
+ );
3218
+ }
3219
+ }
3220
+ throw error;
3221
+ }
3094
3222
  return options.json === true
3095
3223
  ? checkpointed
3096
3224
  : withHumanOutput(checkpointed, formatTeamProjectCheckpointHuman(checkpointed));
@@ -3162,7 +3290,7 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3162
3290
  ? successor
3163
3291
  : withHumanOutput(successor, formatTeamProjectSuccessorHumanV1(successor));
3164
3292
  }
3165
- throw new Error("Usage: owlrunkit project <init|assign|checkpoint|handoff|decision|verification|integrate|capture|status|takeover|successor> [options]");
3293
+ throw new Error("Usage: owlrunkit project <init|assign|checkpoint|handoff|reject-and-return|decision|verification|integrate|capture|status|takeover|successor> [options]");
3166
3294
  }
3167
3295
  if (command === "profiles") {
3168
3296
  const root = requireOption(options, "workspace");
@@ -3248,10 +3376,28 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3248
3376
  if (commandArgv.length === 0) {
3249
3377
  throw new Error("Usage: runkit-cli.mjs quick-verify --workspace <path> -- <executable> [args...]");
3250
3378
  }
3251
- return runQuickVerification({
3379
+ const verification = await runQuickVerification({
3252
3380
  workspaceRoot: workspace(options),
3253
3381
  commandArgv,
3382
+ stdinFile: options["stdin-file"],
3383
+ });
3384
+ if (options.attest !== true || typeof verification.receiptPath !== "string") {
3385
+ return verification;
3386
+ }
3387
+ const details = attestQuickReceiptDetails({
3388
+ receiptPath: verification.receiptPath,
3389
+ workspaceRoot: workspace(options),
3254
3390
  });
3391
+ return {
3392
+ ...verification,
3393
+ exitCode: details.exitCode === 0 ? verification.exitCode : details.exitCode,
3394
+ attestationDecision: details.attestation.decision,
3395
+ attestation: details.attestation,
3396
+ nextAllowedAction: details.attestation.decision === "GO"
3397
+ ? "consume_attestation"
3398
+ : "inspect_attestation_issues",
3399
+ authorizationGranted: false,
3400
+ };
3255
3401
  }
3256
3402
  if (command === "quick-attest") {
3257
3403
  const details = attestQuickReceiptDetails({
@@ -3276,7 +3422,9 @@ export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {
3276
3422
  if (options.local !== true) {
3277
3423
  throw new Error("Quick metrics requires --local; network telemetry is not available.");
3278
3424
  }
3279
- const local = readLocalQuickMetrics(workspace(options));
3425
+ const local = readLocalQuickMetrics(workspace(options), {
3426
+ verbose: options.verbose === true,
3427
+ });
3280
3428
  return {
3281
3429
  ...local,
3282
3430
  metrics: local.quick,