open-research-protocol 0.4.4 → 0.4.6

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/cli/orp.py CHANGED
@@ -4170,11 +4170,38 @@ def _effective_remote_context(
4170
4170
  }
4171
4171
 
4172
4172
 
4173
- def _init_config_starter() -> str:
4173
+ def _init_kernel_task_template(repo_name: str) -> str:
4174
+ safe_name = str(repo_name or "").strip() or "my-project"
4175
+ return (
4176
+ 'schema_version: "1.0.0"\n'
4177
+ "artifact_class: task\n"
4178
+ f"object: bootstrap ORP governance for {safe_name}\n"
4179
+ "goal: establish a local-first ORP workflow with a promotable starter task artifact\n"
4180
+ "boundary:\n"
4181
+ " - repository bootstrap and governance setup\n"
4182
+ " - initial ORP validation and readiness loop\n"
4183
+ "constraints:\n"
4184
+ " - keep evidence in canonical artifact paths instead of ORP process metadata\n"
4185
+ " - preserve a clean handoff and checkpoint discipline from day one\n"
4186
+ "success_criteria:\n"
4187
+ " - default ORP gate profile passes\n"
4188
+ " - repo is ready for a first meaningful work branch and checkpoint sequence\n"
4189
+ "canonical_target:\n"
4190
+ " - orp/HANDOFF.md\n"
4191
+ " - orp/checkpoints/CHECKPOINT_LOG.md\n"
4192
+ "artifact_refs:\n"
4193
+ " - orp.yml\n"
4194
+ " - orp/HANDOFF.md\n"
4195
+ " - orp/checkpoints/CHECKPOINT_LOG.md\n"
4196
+ )
4197
+
4198
+
4199
+ def _init_config_starter(repo_name: str = "my-project") -> str:
4200
+ safe_name = str(repo_name or "").strip() or "my-project"
4174
4201
  return (
4175
4202
  'version: "1"\n'
4176
4203
  "project:\n"
4177
- " name: my-project\n"
4204
+ f" name: {safe_name}\n"
4178
4205
  " repo_root: .\n"
4179
4206
  " canonical_paths:\n"
4180
4207
  " code: src/\n"
@@ -4192,6 +4219,25 @@ def _init_config_starter() -> str:
4192
4219
  " blocked: blocked\n"
4193
4220
  " done: reviewed\n"
4194
4221
  "gates:\n"
4222
+ " - id: starter_kernel\n"
4223
+ " description: Validate the starter ORP reasoning-kernel task artifact\n"
4224
+ " phase: structure_kernel\n"
4225
+ " command: echo ORP_KERNEL_STARTER\n"
4226
+ " pass:\n"
4227
+ " exit_codes: [0]\n"
4228
+ " stdout_must_contain:\n"
4229
+ " - ORP_KERNEL_STARTER\n"
4230
+ " kernel:\n"
4231
+ " mode: hard\n"
4232
+ " artifacts:\n"
4233
+ " - path: analysis/orp.kernel.task.yml\n"
4234
+ " artifact_class: task\n"
4235
+ " evidence:\n"
4236
+ " status: process_only\n"
4237
+ " note: Starter kernel artifact records task structure, not evidence.\n"
4238
+ " paths:\n"
4239
+ " - analysis/orp.kernel.task.yml\n"
4240
+ " on_fail: stop\n"
4195
4241
  " - id: smoke\n"
4196
4242
  " description: Basic smoke gate\n"
4197
4243
  " phase: verification\n"
@@ -4206,6 +4252,7 @@ def _init_config_starter() -> str:
4206
4252
  " mode: discovery\n"
4207
4253
  " packet_kind: problem_scope\n"
4208
4254
  " gate_ids:\n"
4255
+ " - starter_kernel\n"
4209
4256
  " - smoke\n"
4210
4257
  )
4211
4258
 
@@ -5090,6 +5137,7 @@ def _about_payload() -> dict[str, Any]:
5090
5137
  "schemas": {
5091
5138
  "config": "spec/v1/orp.config.schema.json",
5092
5139
  "packet": "spec/v1/packet.schema.json",
5140
+ "kernel": "spec/v1/kernel.schema.json",
5093
5141
  "profile_pack": "spec/v1/profile-pack.schema.json",
5094
5142
  "link_project": "spec/v1/link-project.schema.json",
5095
5143
  "link_session": "spec/v1/link-session.schema.json",
@@ -5097,6 +5145,14 @@ def _about_payload() -> dict[str, Any]:
5097
5145
  "runner_runtime": "spec/v1/runner-runtime.schema.json",
5098
5146
  },
5099
5147
  "abilities": [
5148
+ {
5149
+ "id": "kernel",
5150
+ "description": "Reasoning-kernel artifact scaffolding and validation for promotable repository truth.",
5151
+ "entrypoints": [
5152
+ ["kernel", "validate"],
5153
+ ["kernel", "scaffold"],
5154
+ ],
5155
+ },
5100
5156
  {
5101
5157
  "id": "workspace",
5102
5158
  "description": "Hosted workspace auth, ideas, features, worlds, checkpoints, and worker operations.",
@@ -5185,6 +5241,8 @@ def _about_payload() -> dict[str, Any]:
5185
5241
  "commands": [
5186
5242
  {"name": "home", "path": ["home"], "json_output": True},
5187
5243
  {"name": "about", "path": ["about"], "json_output": True},
5244
+ {"name": "kernel_validate", "path": ["kernel", "validate"], "json_output": True},
5245
+ {"name": "kernel_scaffold", "path": ["kernel", "scaffold"], "json_output": True},
5188
5246
  {"name": "auth_login", "path": ["auth", "login"], "json_output": True},
5189
5247
  {"name": "auth_verify", "path": ["auth", "verify"], "json_output": True},
5190
5248
  {"name": "auth_logout", "path": ["auth", "logout"], "json_output": True},
@@ -5252,6 +5310,7 @@ def _about_payload() -> dict[str, Any]:
5252
5310
  "ORP files are process-only and are not evidence.",
5253
5311
  "Canonical evidence lives in repo artifact paths outside ORP docs.",
5254
5312
  "Default CLI output is human-readable; listed commands with json_output=true also support --json.",
5313
+ "Reasoning-kernel artifacts shape promotable repository truth for tasks, decisions, hypotheses, experiments, checkpoints, policies, and results.",
5255
5314
  "Discovery profiles in ORP are portable search-intent files managed directly by ORP.",
5256
5315
  "Collaboration is a built-in ORP ability exposed through `orp collaborate ...`.",
5257
5316
  "Project/session linking is a built-in ORP ability exposed through `orp link ...` and stored machine-locally under `.git/orp/link/`.",
@@ -5434,41 +5493,48 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
5434
5493
  )
5435
5494
  quick_actions.insert(
5436
5495
  3,
5496
+ {
5497
+ "label": "Validate the starter kernel artifact",
5498
+ "command": "orp kernel validate analysis/orp.kernel.task.yml --json",
5499
+ },
5500
+ )
5501
+ quick_actions.insert(
5502
+ 4,
5437
5503
  {
5438
5504
  "label": "Checkpoint and back up current work to a dedicated remote ref",
5439
5505
  "command": 'orp backup -m "backup current work" --json',
5440
5506
  },
5441
5507
  )
5442
5508
  quick_actions.insert(
5443
- 4,
5509
+ 5,
5444
5510
  {
5445
5511
  "label": "Mark the repo locally ready after validation",
5446
5512
  "command": "orp ready --json",
5447
5513
  },
5448
5514
  )
5449
5515
  quick_actions.insert(
5450
- 5,
5516
+ 6,
5451
5517
  {
5452
5518
  "label": "Inspect local project/session link state",
5453
5519
  "command": "orp link status --json",
5454
5520
  },
5455
5521
  )
5456
5522
  quick_actions.insert(
5457
- 6,
5523
+ 7,
5458
5524
  {
5459
5525
  "label": "Inspect machine runner state",
5460
5526
  "command": "orp runner status --json",
5461
5527
  },
5462
5528
  )
5463
5529
  quick_actions.insert(
5464
- 7,
5530
+ 8,
5465
5531
  {
5466
5532
  "label": "Inspect and repair governance health",
5467
5533
  "command": "orp doctor --json",
5468
5534
  },
5469
5535
  )
5470
5536
  quick_actions.insert(
5471
- 8,
5537
+ 9,
5472
5538
  {
5473
5539
  "label": "Inspect safe cleanup candidates",
5474
5540
  "command": "orp cleanup --json",
@@ -5929,6 +5995,7 @@ def _perform_github_discovery_scan(
5929
5995
  def cmd_init(args: argparse.Namespace) -> int:
5930
5996
  repo_root = Path(args.repo_root).resolve()
5931
5997
  repo_root.mkdir(parents=True, exist_ok=True)
5998
+ repo_name = repo_root.name or "my-project"
5932
5999
 
5933
6000
  default_branch = str(getattr(args, "default_branch", "main") or "main").strip() or "main"
5934
6001
  allow_protected_branch_work = bool(getattr(args, "allow_protected_branch_work", False))
@@ -5941,8 +6008,9 @@ def cmd_init(args: argparse.Namespace) -> int:
5941
6008
  config_path = repo_root / args.config
5942
6009
  config_action = "kept"
5943
6010
  if not config_path.exists():
5944
- config_path.write_text(_init_config_starter(), encoding="utf-8")
6011
+ config_path.write_text(_init_config_starter(repo_name), encoding="utf-8")
5945
6012
  config_action = "created"
6013
+ kernel_starter_path = repo_root / "analysis" / "orp.kernel.task.yml"
5946
6014
 
5947
6015
  initialized_at_utc = _now_utc()
5948
6016
  git_snapshot = _git_governance_snapshot(
@@ -5971,7 +6039,9 @@ def cmd_init(args: argparse.Namespace) -> int:
5971
6039
 
5972
6040
  if git_snapshot["dirty"]:
5973
6041
  warnings.append("working tree is dirty; create an explicit checkpoint commit before agent work.")
5974
- next_actions.append('git add orp.yml orp && git commit -m "checkpoint: bootstrap ORP governance"')
6042
+ next_actions.append(
6043
+ 'git add orp.yml orp analysis/orp.kernel.task.yml && git commit -m "checkpoint: bootstrap ORP governance"'
6044
+ )
5975
6045
 
5976
6046
  if not git_snapshot["has_commits"]:
5977
6047
  notes.append("repo has no commits yet; treat the first commit as the initial ORP checkpoint.")
@@ -6013,6 +6083,10 @@ def cmd_init(args: argparse.Namespace) -> int:
6013
6083
  "path": _path_for_state(checkpoint_log_path, repo_root),
6014
6084
  "action": _write_text_if_missing(checkpoint_log_path, _init_checkpoint_log_template()),
6015
6085
  }
6086
+ files["starter_kernel"] = {
6087
+ "path": _path_for_state(kernel_starter_path, repo_root),
6088
+ "action": _write_text_if_missing(kernel_starter_path, _init_kernel_task_template(repo_name)),
6089
+ }
6016
6090
 
6017
6091
  agent_policy_exists = agent_policy_path.exists()
6018
6092
  agent_policy = _agent_policy_payload(
@@ -6658,6 +6732,7 @@ def _apply_doctor_fixes(repo_root: Path, config_arg: str, status_payload: dict[s
6658
6732
  state_path = repo_root / "orp" / "state.json"
6659
6733
  state = _read_json_if_exists(state_path)
6660
6734
  governance_state = state.get("governance") if isinstance(state.get("governance"), dict) else {}
6735
+ repo_name = repo_root.name or "my-project"
6661
6736
 
6662
6737
  default_branch = str(governance_state.get("default_branch", "main")).strip() or "main"
6663
6738
  allow_protected_branch_work = bool(governance_state.get("allow_protected_branch_work", False))
@@ -6669,7 +6744,7 @@ def _apply_doctor_fixes(repo_root: Path, config_arg: str, status_payload: dict[s
6669
6744
  config_path = config_path.resolve()
6670
6745
 
6671
6746
  if not config_path.exists():
6672
- config_path.write_text(_init_config_starter(), encoding="utf-8")
6747
+ config_path.write_text(_init_config_starter(repo_name), encoding="utf-8")
6673
6748
 
6674
6749
  git_snapshot = _git_governance_snapshot(
6675
6750
  repo_root,
@@ -6685,6 +6760,7 @@ def _apply_doctor_fixes(repo_root: Path, config_arg: str, status_payload: dict[s
6685
6760
 
6686
6761
  handoff_path = repo_root / "orp" / "HANDOFF.md"
6687
6762
  checkpoint_log_path = repo_root / "orp" / "checkpoints" / "CHECKPOINT_LOG.md"
6763
+ kernel_starter_path = repo_root / "analysis" / "orp.kernel.task.yml"
6688
6764
  governance_path = repo_root / "orp" / "governance.json"
6689
6765
  agent_policy_path = repo_root / "orp" / "agent-policy.json"
6690
6766
 
@@ -6700,6 +6776,8 @@ def _apply_doctor_fixes(repo_root: Path, config_arg: str, status_payload: dict[s
6700
6776
  fixes_applied.append("created_handoff")
6701
6777
  if _write_text_if_missing(checkpoint_log_path, _init_checkpoint_log_template()) == "created":
6702
6778
  fixes_applied.append("created_checkpoint_log")
6779
+ if _write_text_if_missing(kernel_starter_path, _init_kernel_task_template(repo_name)) == "created":
6780
+ fixes_applied.append("created_starter_kernel")
6703
6781
 
6704
6782
  _write_json(
6705
6783
  agent_policy_path,
@@ -7178,6 +7256,326 @@ def _gate_map(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
7178
7256
  return out
7179
7257
 
7180
7258
 
7259
+ KERNEL_ARTIFACT_CLASS_REQUIREMENTS: dict[str, list[str]] = {
7260
+ "task": ["object", "goal", "boundary", "constraints", "success_criteria"],
7261
+ "decision": ["question", "chosen_path", "rejected_alternatives", "rationale", "consequences"],
7262
+ "hypothesis": ["claim", "boundary", "assumptions", "test_path", "falsifiers"],
7263
+ "experiment": ["objective", "method", "inputs", "outputs", "evidence_expectations", "interpretation_limits"],
7264
+ "checkpoint": ["completed_unit", "current_state", "risks", "next_handoff_target", "artifact_refs"],
7265
+ "policy": ["scope", "rule", "rationale", "invariants", "enforcement_surface"],
7266
+ "result": ["claim", "evidence_paths", "status", "interpretation_limits", "next_follow_up"],
7267
+ }
7268
+
7269
+
7270
+ def _kernel_field_present(value: Any) -> bool:
7271
+ if isinstance(value, str):
7272
+ return bool(value.strip())
7273
+ if isinstance(value, list):
7274
+ return any(_kernel_field_present(item) for item in value)
7275
+ if isinstance(value, dict):
7276
+ return len(value) > 0
7277
+ return value is not None
7278
+
7279
+
7280
+ def _kernel_validation_mode(gate: dict[str, Any]) -> str:
7281
+ kernel_cfg = gate.get("kernel") if isinstance(gate.get("kernel"), dict) else {}
7282
+ default_mode = "hard" if str(gate.get("phase", "")).strip() == "structure_kernel" else "soft"
7283
+ mode = str(kernel_cfg.get("mode", default_mode)).strip().lower()
7284
+ if mode in {"soft", "hard"}:
7285
+ return mode
7286
+ return default_mode
7287
+
7288
+
7289
+ def _kernel_artifact_specs(
7290
+ gate: dict[str, Any],
7291
+ repo_root: Path,
7292
+ vars_map: dict[str, str],
7293
+ ) -> list[dict[str, Any]]:
7294
+ kernel_cfg = gate.get("kernel") if isinstance(gate.get("kernel"), dict) else {}
7295
+ raw_artifacts = kernel_cfg.get("artifacts")
7296
+ if not isinstance(raw_artifacts, list):
7297
+ return []
7298
+
7299
+ specs: list[dict[str, Any]] = []
7300
+ for raw in raw_artifacts:
7301
+ if not isinstance(raw, dict):
7302
+ continue
7303
+ path_raw = str(raw.get("path", "")).strip()
7304
+ if not path_raw:
7305
+ continue
7306
+ replaced = _replace_vars(path_raw, vars_map)
7307
+ path = Path(replaced)
7308
+ if not path.is_absolute():
7309
+ path = repo_root / path
7310
+ specs.append(
7311
+ {
7312
+ "path": path.resolve(),
7313
+ "expected_artifact_class": str(raw.get("artifact_class", "")).strip(),
7314
+ "required": bool(raw.get("required", True)),
7315
+ "extra_required_fields": _unique_strings(
7316
+ [str(x).strip() for x in raw.get("extra_required_fields", []) if isinstance(x, str)]
7317
+ ),
7318
+ }
7319
+ )
7320
+ return specs
7321
+
7322
+
7323
+ def _validate_kernel_gate(
7324
+ gate: dict[str, Any],
7325
+ repo_root: Path,
7326
+ vars_map: dict[str, str],
7327
+ ) -> dict[str, Any] | None:
7328
+ if str(gate.get("phase", "")).strip() != "structure_kernel":
7329
+ return None
7330
+ if not isinstance(gate.get("kernel"), dict):
7331
+ return None
7332
+
7333
+ mode = _kernel_validation_mode(gate)
7334
+ specs = _kernel_artifact_specs(gate, repo_root, vars_map)
7335
+ issues: list[str] = []
7336
+ artifact_results: list[dict[str, Any]] = []
7337
+
7338
+ if not specs:
7339
+ issues.append("structure_kernel gate requires kernel.artifacts.")
7340
+
7341
+ for spec in specs:
7342
+ path = spec["path"]
7343
+ expected_class = str(spec.get("expected_artifact_class", "")).strip()
7344
+ required = bool(spec.get("required", True))
7345
+ extra_required_fields = [
7346
+ str(x).strip()
7347
+ for x in spec.get("extra_required_fields", [])
7348
+ if isinstance(x, str) and str(x).strip()
7349
+ ]
7350
+
7351
+ artifact_issues: list[str] = []
7352
+ missing_fields: list[str] = []
7353
+ exists = path.exists()
7354
+ optional_skipped = False
7355
+ payload: dict[str, Any] = {}
7356
+
7357
+ if not exists:
7358
+ if required:
7359
+ artifact_issues.append("kernel artifact file is missing.")
7360
+ else:
7361
+ optional_skipped = True
7362
+ else:
7363
+ try:
7364
+ loaded_payload = _load_config(path)
7365
+ if isinstance(loaded_payload, dict):
7366
+ payload = loaded_payload
7367
+ else:
7368
+ artifact_issues.append("kernel artifact root must be an object.")
7369
+ except Exception as exc:
7370
+ artifact_issues.append(f"failed to parse kernel artifact: {exc}")
7371
+
7372
+ actual_class = ""
7373
+ if payload:
7374
+ schema_version = str(payload.get("schema_version", "")).strip()
7375
+ if schema_version != "1.0.0":
7376
+ artifact_issues.append("schema_version must be `1.0.0`.")
7377
+
7378
+ actual_class = str(payload.get("artifact_class", "")).strip()
7379
+ if actual_class not in KERNEL_ARTIFACT_CLASS_REQUIREMENTS:
7380
+ artifact_issues.append(
7381
+ f"unsupported artifact_class: {actual_class or '(missing)'}."
7382
+ )
7383
+
7384
+ if expected_class and actual_class and expected_class != actual_class:
7385
+ artifact_issues.append(
7386
+ f"artifact_class mismatch: expected `{expected_class}`, found `{actual_class}`."
7387
+ )
7388
+
7389
+ field_class = actual_class or expected_class
7390
+ required_fields = list(KERNEL_ARTIFACT_CLASS_REQUIREMENTS.get(field_class, []))
7391
+ for field in extra_required_fields:
7392
+ if field not in required_fields:
7393
+ required_fields.append(field)
7394
+ for field in required_fields:
7395
+ if not _kernel_field_present(payload.get(field)):
7396
+ missing_fields.append(field)
7397
+ if missing_fields:
7398
+ artifact_issues.append("missing required fields: " + ", ".join(missing_fields))
7399
+
7400
+ valid = optional_skipped or (exists and not artifact_issues)
7401
+ path_state = _path_for_state(path, repo_root)
7402
+ artifact_results.append(
7403
+ {
7404
+ "path": path_state,
7405
+ "exists": exists,
7406
+ "required": required,
7407
+ "optional_skipped": optional_skipped,
7408
+ "artifact_class": actual_class,
7409
+ "expected_artifact_class": expected_class,
7410
+ "valid": valid,
7411
+ "missing_fields": missing_fields,
7412
+ "issues": artifact_issues,
7413
+ }
7414
+ )
7415
+ issues.extend([f"{path_state}: {issue}" for issue in artifact_issues])
7416
+
7417
+ valid = bool(specs) and all(bool(row.get("valid")) for row in artifact_results)
7418
+ return {
7419
+ "mode": mode,
7420
+ "valid": valid,
7421
+ "artifacts_total": len(artifact_results),
7422
+ "artifacts_valid": sum(1 for row in artifact_results if row.get("valid")),
7423
+ "issues": issues,
7424
+ "artifacts": artifact_results,
7425
+ }
7426
+
7427
+
7428
+ def _kernel_template_payload(artifact_class: str, name_hint: str = "") -> dict[str, Any]:
7429
+ hint = str(name_hint or "").strip() or "this artifact"
7430
+ templates: dict[str, dict[str, Any]] = {
7431
+ "task": {
7432
+ "object": f"describe the task object for {hint}",
7433
+ "goal": "describe the intended outcome",
7434
+ "boundary": ["define what is in scope", "define what is out of scope"],
7435
+ "constraints": ["list the main constraints"],
7436
+ "success_criteria": ["describe how success will be recognized"],
7437
+ },
7438
+ "decision": {
7439
+ "question": f"describe the decision question for {hint}",
7440
+ "chosen_path": "describe the selected path",
7441
+ "rejected_alternatives": ["list key rejected alternatives"],
7442
+ "rationale": "describe why this path was chosen",
7443
+ "consequences": ["list immediate consequences and tradeoffs"],
7444
+ },
7445
+ "hypothesis": {
7446
+ "claim": f"state the hypothesis for {hint}",
7447
+ "boundary": "describe the domain or conditions where the claim is meant to hold",
7448
+ "assumptions": ["list assumptions the claim depends on"],
7449
+ "test_path": "describe how the hypothesis will be tested",
7450
+ "falsifiers": ["list what would falsify the hypothesis"],
7451
+ },
7452
+ "experiment": {
7453
+ "objective": f"describe the experiment objective for {hint}",
7454
+ "method": "describe the method or procedure",
7455
+ "inputs": ["list the required inputs"],
7456
+ "outputs": ["list the expected outputs"],
7457
+ "evidence_expectations": ["list the evidence this experiment should produce"],
7458
+ "interpretation_limits": ["list limits on interpretation"],
7459
+ },
7460
+ "checkpoint": {
7461
+ "completed_unit": f"describe the completed unit for {hint}",
7462
+ "current_state": "describe the current state of work",
7463
+ "risks": ["list immediate risks or unresolved concerns"],
7464
+ "next_handoff_target": "describe what the next operator or agent should pick up",
7465
+ "artifact_refs": ["list the key artifact paths tied to this checkpoint"],
7466
+ },
7467
+ "policy": {
7468
+ "scope": f"describe the policy scope for {hint}",
7469
+ "rule": "state the rule",
7470
+ "rationale": "describe why the rule exists",
7471
+ "invariants": ["list the invariants the rule protects"],
7472
+ "enforcement_surface": "describe where ORP should enforce or check this policy",
7473
+ },
7474
+ "result": {
7475
+ "claim": f"state the resulting claim for {hint}",
7476
+ "evidence_paths": ["list canonical evidence paths"],
7477
+ "status": "describe the result status",
7478
+ "interpretation_limits": ["list the limits on what this result means"],
7479
+ "next_follow_up": "describe the next follow-up action",
7480
+ },
7481
+ }
7482
+ if artifact_class not in templates:
7483
+ raise RuntimeError(f"unsupported kernel artifact class: {artifact_class}")
7484
+ return {
7485
+ "schema_version": "1.0.0",
7486
+ "artifact_class": artifact_class,
7487
+ **templates[artifact_class],
7488
+ }
7489
+
7490
+
7491
+ def _write_structured_payload(path: Path, payload: dict[str, Any], *, format_hint: str = "") -> str:
7492
+ fmt = str(format_hint or "").strip().lower()
7493
+ if not fmt:
7494
+ if path.suffix.lower() == ".json":
7495
+ fmt = "json"
7496
+ elif path.suffix.lower() in {".yaml", ".yml"}:
7497
+ fmt = "yaml"
7498
+ else:
7499
+ fmt = "yaml"
7500
+
7501
+ if fmt == "json":
7502
+ _write_json(path, payload)
7503
+ return "json"
7504
+
7505
+ try:
7506
+ import yaml # type: ignore
7507
+ except Exception as exc:
7508
+ raise RuntimeError("YAML output requires PyYAML. Use --format json or install PyYAML.") from exc
7509
+ _write_text(path, yaml.safe_dump(payload, sort_keys=False, allow_unicode=False))
7510
+ return "yaml"
7511
+
7512
+
7513
+ def cmd_kernel_validate(args: argparse.Namespace) -> int:
7514
+ repo_root = Path(args.repo_root).resolve()
7515
+ artifact_path = _resolve_cli_path(args.artifact, repo_root)
7516
+ fake_gate = {
7517
+ "phase": "structure_kernel",
7518
+ "kernel": {
7519
+ "mode": "hard",
7520
+ "artifacts": [
7521
+ {
7522
+ "path": str(artifact_path),
7523
+ "artifact_class": str(getattr(args, "artifact_class", "") or "").strip(),
7524
+ "extra_required_fields": list(getattr(args, "required_field", []) or []),
7525
+ }
7526
+ ],
7527
+ },
7528
+ }
7529
+ validation = _validate_kernel_gate(fake_gate, repo_root, {})
7530
+ if validation is None:
7531
+ raise RuntimeError("failed to construct kernel validation request")
7532
+
7533
+ artifact_result = validation.get("artifacts", [None])[0] if isinstance(validation.get("artifacts"), list) else None
7534
+ result = {
7535
+ "ok": bool(validation.get("valid")),
7536
+ "artifact": _path_for_state(artifact_path, repo_root),
7537
+ "expected_artifact_class": str(getattr(args, "artifact_class", "") or "").strip(),
7538
+ "validation": validation,
7539
+ "artifact_result": artifact_result,
7540
+ }
7541
+ if args.json_output:
7542
+ _print_json(result)
7543
+ else:
7544
+ print(f"artifact={result['artifact']}")
7545
+ print(f"valid={'true' if result['ok'] else 'false'}")
7546
+ if artifact_result:
7547
+ print(f"artifact_class={artifact_result.get('artifact_class', '')}")
7548
+ missing = artifact_result.get("missing_fields", [])
7549
+ print("missing_fields=" + (",".join(missing) if missing else "(none)"))
7550
+ for issue in validation.get("issues", []):
7551
+ print(f"issue={issue}")
7552
+ return 0 if result["ok"] else 1
7553
+
7554
+
7555
+ def cmd_kernel_scaffold(args: argparse.Namespace) -> int:
7556
+ repo_root = Path(args.repo_root).resolve()
7557
+ out_path = _resolve_cli_path(args.out, repo_root)
7558
+ if out_path.exists() and not args.force:
7559
+ raise RuntimeError(
7560
+ f"kernel artifact already exists: {_path_for_state(out_path, repo_root)}. Use --force to overwrite."
7561
+ )
7562
+ payload = _kernel_template_payload(args.artifact_class, args.name or repo_root.name)
7563
+ emitted_format = _write_structured_payload(out_path, payload, format_hint=args.format)
7564
+ result = {
7565
+ "ok": True,
7566
+ "artifact_class": args.artifact_class,
7567
+ "path": _path_for_state(out_path, repo_root),
7568
+ "format": emitted_format,
7569
+ }
7570
+ if args.json_output:
7571
+ _print_json(result)
7572
+ else:
7573
+ print(f"path={result['path']}")
7574
+ print(f"artifact_class={result['artifact_class']}")
7575
+ print(f"format={result['format']}")
7576
+ return 0
7577
+
7578
+
7181
7579
  def cmd_gate_run(args: argparse.Namespace) -> int:
7182
7580
  repo_root = Path(args.repo_root).resolve()
7183
7581
  _ensure_dirs(repo_root)
@@ -7284,7 +7682,19 @@ def cmd_gate_run(args: argparse.Namespace) -> int:
7284
7682
  if not (repo_root / rel).exists():
7285
7683
  file_issues.append(f"required file missing: {rel}")
7286
7684
 
7287
- passed = ok_exit and ok_out and ok_err and (len(file_issues) == 0) and (exec_status == "ok")
7685
+ kernel_validation = _validate_kernel_gate(gate, repo_root, vars_map)
7686
+ kernel_ok = True
7687
+ if kernel_validation is not None and kernel_validation.get("mode") == "hard":
7688
+ kernel_ok = bool(kernel_validation.get("valid"))
7689
+
7690
+ passed = (
7691
+ ok_exit
7692
+ and ok_out
7693
+ and ok_err
7694
+ and (len(file_issues) == 0)
7695
+ and (exec_status == "ok")
7696
+ and kernel_ok
7697
+ )
7288
7698
  status = "pass" if passed else "fail"
7289
7699
  issues = []
7290
7700
  if not ok_exit:
@@ -7294,6 +7704,14 @@ def cmd_gate_run(args: argparse.Namespace) -> int:
7294
7704
  issues.extend(file_issues)
7295
7705
  if exec_status != "ok":
7296
7706
  issues.append(exec_status)
7707
+ if kernel_validation is not None and not kernel_validation.get("valid"):
7708
+ issues.extend(
7709
+ [
7710
+ f"kernel validation: {issue}"
7711
+ for issue in kernel_validation.get("issues", [])
7712
+ if isinstance(issue, str)
7713
+ ]
7714
+ )
7297
7715
 
7298
7716
  evidence_paths = _resolve_config_paths(
7299
7717
  evidence_cfg.get("paths", []) if isinstance(evidence_cfg, dict) else [],
@@ -7311,22 +7729,24 @@ def cmd_gate_run(args: argparse.Namespace) -> int:
7311
7729
  else ""
7312
7730
  )
7313
7731
 
7314
- run_results.append(
7315
- {
7316
- "gate_id": gate_id,
7317
- "phase": gate.get("phase", "custom"),
7318
- "command": cmd,
7319
- "status": status,
7320
- "exit_code": rc,
7321
- "duration_ms": dur_ms,
7322
- "stdout_path": str(stdout_path.relative_to(repo_root)),
7323
- "stderr_path": str(stderr_path.relative_to(repo_root)),
7324
- "rule_issues": issues,
7325
- "evidence_paths": evidence_paths,
7326
- "evidence_status": evidence_status,
7327
- "evidence_note": evidence_note,
7328
- }
7329
- )
7732
+ result_row = {
7733
+ "gate_id": gate_id,
7734
+ "phase": gate.get("phase", "custom"),
7735
+ "command": cmd,
7736
+ "status": status,
7737
+ "exit_code": rc,
7738
+ "duration_ms": dur_ms,
7739
+ "stdout_path": str(stdout_path.relative_to(repo_root)),
7740
+ "stderr_path": str(stderr_path.relative_to(repo_root)),
7741
+ "rule_issues": issues,
7742
+ "evidence_paths": evidence_paths,
7743
+ "evidence_status": evidence_status,
7744
+ "evidence_note": evidence_note,
7745
+ }
7746
+ if kernel_validation is not None:
7747
+ result_row["kernel_validation"] = kernel_validation
7748
+
7749
+ run_results.append(result_row)
7330
7750
 
7331
7751
  if not passed:
7332
7752
  on_fail = str(gate.get("on_fail", "stop"))
@@ -7704,6 +8124,7 @@ def cmd_about(args: argparse.Namespace) -> int:
7704
8124
  print(f"artifact.packet_json={payload['artifacts']['packet_json']}")
7705
8125
  print(f"schema.config={payload['schemas']['config']}")
7706
8126
  print(f"schema.packet={payload['schemas']['packet']}")
8127
+ print(f"schema.kernel={payload['schemas']['kernel']}")
7707
8128
  print(f"schema.profile_pack={payload['schemas']['profile_pack']}")
7708
8129
  print(f"packs.count={len(payload['packs'])}")
7709
8130
  for pack in payload["packs"]:
@@ -12181,6 +12602,62 @@ def build_parser() -> argparse.ArgumentParser:
12181
12602
  add_json_flag(s_cleanup)
12182
12603
  s_cleanup.set_defaults(func=cmd_cleanup, json_output=False)
12183
12604
 
12605
+ s_kernel = sub.add_parser("kernel", help="Reasoning-kernel artifact operations")
12606
+ kernel_sub = s_kernel.add_subparsers(dest="kernel_cmd", required=True)
12607
+
12608
+ s_kernel_validate = kernel_sub.add_parser(
12609
+ "validate",
12610
+ help="Validate one kernel artifact against typed ORP structure rules",
12611
+ )
12612
+ s_kernel_validate.add_argument("artifact", help="Kernel artifact path (.yml, .yaml, or .json)")
12613
+ s_kernel_validate.add_argument(
12614
+ "--artifact-class",
12615
+ default="",
12616
+ help="Optional expected artifact class override",
12617
+ )
12618
+ s_kernel_validate.add_argument(
12619
+ "--required-field",
12620
+ action="append",
12621
+ default=[],
12622
+ help="Extra required field to enforce during validation (repeatable)",
12623
+ )
12624
+ add_json_flag(s_kernel_validate)
12625
+ s_kernel_validate.set_defaults(func=cmd_kernel_validate, json_output=False)
12626
+
12627
+ s_kernel_scaffold = kernel_sub.add_parser(
12628
+ "scaffold",
12629
+ help="Write a starter kernel artifact template for a typed ORP artifact class",
12630
+ )
12631
+ s_kernel_scaffold.add_argument(
12632
+ "--artifact-class",
12633
+ required=True,
12634
+ choices=sorted(KERNEL_ARTIFACT_CLASS_REQUIREMENTS.keys()),
12635
+ help="Typed kernel artifact class to scaffold",
12636
+ )
12637
+ s_kernel_scaffold.add_argument(
12638
+ "--out",
12639
+ required=True,
12640
+ help="Output artifact path (.yml/.yaml or .json)",
12641
+ )
12642
+ s_kernel_scaffold.add_argument(
12643
+ "--name",
12644
+ default="",
12645
+ help="Optional name hint used in scaffold placeholders",
12646
+ )
12647
+ s_kernel_scaffold.add_argument(
12648
+ "--format",
12649
+ default="",
12650
+ choices=["", "yaml", "json"],
12651
+ help="Optional explicit output format (default: infer from file extension)",
12652
+ )
12653
+ s_kernel_scaffold.add_argument(
12654
+ "--force",
12655
+ action="store_true",
12656
+ help="Overwrite an existing artifact at --out",
12657
+ )
12658
+ add_json_flag(s_kernel_scaffold)
12659
+ s_kernel_scaffold.set_defaults(func=cmd_kernel_scaffold, json_output=False)
12660
+
12184
12661
  s_gate = sub.add_parser("gate", help="Gate operations")
12185
12662
  gate_sub = s_gate.add_subparsers(dest="gate_cmd", required=True)
12186
12663
  s_run = gate_sub.add_parser("run", help="Run configured gates for a profile")