open-research-protocol 0.4.21 → 0.4.23
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/AGENT_INTEGRATION.md +7 -2
- package/README.md +7 -1
- package/cli/orp.py +310 -11
- package/docs/AGENT_LOOP.md +5 -0
- package/docs/AGENT_MODES.md +46 -0
- package/docs/START_HERE.md +22 -1
- package/package.json +1 -1
- package/packages/orp-workspace-launcher/README.md +7 -0
- package/packages/orp-workspace-launcher/src/core-plan.js +89 -3
- package/packages/orp-workspace-launcher/src/hosted-state.js +56 -0
- package/packages/orp-workspace-launcher/src/ledger.js +6 -2
- package/packages/orp-workspace-launcher/src/sync.js +3 -0
- package/packages/orp-workspace-launcher/src/tabs.js +21 -0
- package/packages/orp-workspace-launcher/test/ledger.test.js +72 -0
- package/packages/orp-workspace-launcher/test/tabs.test.js +6 -0
- package/scripts/orp-kernel-benchmark.py +64 -3
package/AGENT_INTEGRATION.md
CHANGED
|
@@ -48,8 +48,12 @@ If the agent only remembers one ORP loop, it should be this:
|
|
|
48
48
|
```bash
|
|
49
49
|
orp frontier state --json
|
|
50
50
|
```
|
|
51
|
-
5.
|
|
52
|
-
|
|
51
|
+
5. if the work feels confusing or too large, break it down before moving
|
|
52
|
+
```bash
|
|
53
|
+
orp mode breakdown granular-breakdown --json
|
|
54
|
+
```
|
|
55
|
+
6. do the next honest move
|
|
56
|
+
7. checkpoint it honestly
|
|
53
57
|
```bash
|
|
54
58
|
orp checkpoint create -m "checkpoint note" --json
|
|
55
59
|
```
|
|
@@ -60,6 +64,7 @@ That is the ORP rhythm in one line:
|
|
|
60
64
|
- inspect repo safety
|
|
61
65
|
- resolve access
|
|
62
66
|
- inspect context
|
|
67
|
+
- break down complexity when comprehension would help
|
|
63
68
|
- do the work
|
|
64
69
|
- checkpoint it honestly
|
|
65
70
|
|
package/README.md
CHANGED
|
@@ -122,9 +122,11 @@ orp report summary
|
|
|
122
122
|
orp frontier state
|
|
123
123
|
orp schedule list
|
|
124
124
|
orp mode nudge sleek-minimal-progressive
|
|
125
|
+
orp mode breakdown granular-breakdown
|
|
126
|
+
orp mode nudge granular-breakdown
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
That sequence covers discovery, agent-guide alignment, workspace recovery, agenda refresh, secret resolution, governance, artifacts, planning, automation,
|
|
129
|
+
That sequence covers discovery, agent-guide alignment, workspace recovery, agenda refresh, secret resolution, governance, artifacts, planning, automation, perspective-shift support, and intentional breakdown when the work needs more granular comprehension. Use `mode breakdown` for the full broad-to-atomic ladder; use `mode nudge` when you only need a short reminder card.
|
|
128
130
|
|
|
129
131
|
The shorter rule is:
|
|
130
132
|
|
|
@@ -403,6 +405,9 @@ orp about --json
|
|
|
403
405
|
orp mode list --json
|
|
404
406
|
orp mode show sleek-minimal-progressive --json
|
|
405
407
|
orp mode nudge sleek-minimal-progressive --json
|
|
408
|
+
orp mode show granular-breakdown --json
|
|
409
|
+
orp mode breakdown granular-breakdown --json
|
|
410
|
+
orp mode nudge granular-breakdown --json
|
|
406
411
|
orp update --json
|
|
407
412
|
orp maintenance status --json
|
|
408
413
|
```
|
|
@@ -427,6 +432,7 @@ orp workspace create mac-main --machine-label "Mac Studio"
|
|
|
427
432
|
orp workspace list
|
|
428
433
|
orp workspace tabs main
|
|
429
434
|
orp workspace add-tab main --path /absolute/path/to/project --remote-url git@github.com:org/project.git --bootstrap-command "npm install" --resume-command "codex resume <id>"
|
|
435
|
+
orp workspace add-tab main --path /absolute/path/to/project --title "second active thread" --resume-tool claude --resume-session-id <id> --append
|
|
430
436
|
orp workspace remove-tab main --path /absolute/path/to/project
|
|
431
437
|
orp workspace sync main
|
|
432
438
|
orp secrets list --json
|
package/cli/orp.py
CHANGED
|
@@ -286,7 +286,7 @@ YOUTUBE_ANDROID_CLIENT_VERSION = "20.10.38"
|
|
|
286
286
|
YOUTUBE_ANDROID_USER_AGENT = (
|
|
287
287
|
f"com.google.android.youtube/{YOUTUBE_ANDROID_CLIENT_VERSION} (Linux; U; Android 14)"
|
|
288
288
|
)
|
|
289
|
-
AGENT_MODE_REGISTRY_VERSION = "1.
|
|
289
|
+
AGENT_MODE_REGISTRY_VERSION = "1.2.0"
|
|
290
290
|
AGENT_MODES: list[dict[str, Any]] = [
|
|
291
291
|
{
|
|
292
292
|
"id": "sleek-minimal-progressive",
|
|
@@ -597,6 +597,175 @@ AGENT_MODES: list[dict[str, Any]] = [
|
|
|
597
597
|
},
|
|
598
598
|
],
|
|
599
599
|
},
|
|
600
|
+
{
|
|
601
|
+
"id": "granular-breakdown",
|
|
602
|
+
"aliases": ["breakdown", "breakdown-mode", "stepwise-breakdown", "gb"],
|
|
603
|
+
"label": "Granular Breakdown",
|
|
604
|
+
"summary": "An optional comprehension overlay for breaking complex work into smaller pieces, ordered dependencies, and concrete next moves.",
|
|
605
|
+
"operator_reminder": "Use this when the work is hard to hold in your head, when the user sounds confused, or when a plan needs more intentional granularity before execution.",
|
|
606
|
+
"activation_phrase": "Break it down until it can move.",
|
|
607
|
+
"invocation_style": "Optional but core-loop friendly. Call it whenever comprehension, onboarding, or safe execution would improve with smaller steps.",
|
|
608
|
+
"when_to_use": [
|
|
609
|
+
"When a project, error, or plan feels too large to reason about at once.",
|
|
610
|
+
"When the user asks what something means or seems unsure about the workflow.",
|
|
611
|
+
"When a change has several dependencies that should be sequenced before implementation.",
|
|
612
|
+
"When a handoff needs to be understandable to a future agent or collaborator.",
|
|
613
|
+
],
|
|
614
|
+
"perspective_shifts": [
|
|
615
|
+
"Turn the blob into named parts before debating solutions.",
|
|
616
|
+
"Separate current state, desired state, blockers, and next action.",
|
|
617
|
+
"Order the parts by dependency, risk, and user comprehension.",
|
|
618
|
+
"Prefer a small verified loop over a large impressive explanation.",
|
|
619
|
+
],
|
|
620
|
+
"principles": [
|
|
621
|
+
"Granularity is a service to comprehension, not a performance of detail.",
|
|
622
|
+
"Every breakdown should create a next move, not just a longer list.",
|
|
623
|
+
"Name assumptions before building on top of them.",
|
|
624
|
+
"Compress again after decomposing so the user gets clarity, not clutter.",
|
|
625
|
+
],
|
|
626
|
+
"ritual": [
|
|
627
|
+
"Name the whole problem in one sentence.",
|
|
628
|
+
"Split it into current state, desired state, and missing bridge.",
|
|
629
|
+
"List the smallest meaningful steps in dependency order.",
|
|
630
|
+
"Identify the next test or confirmation that proves movement.",
|
|
631
|
+
],
|
|
632
|
+
"questions": [
|
|
633
|
+
"What are the actual pieces here?",
|
|
634
|
+
"Which piece needs to be understood first for the rest to make sense?",
|
|
635
|
+
"What is blocking action versus only blocking confidence?",
|
|
636
|
+
"What is the smallest safe step that would reduce ambiguity?",
|
|
637
|
+
"After the breakdown, what can we compress back into one clear sentence?",
|
|
638
|
+
],
|
|
639
|
+
"anti_patterns": [
|
|
640
|
+
"Making a breakdown so exhaustive that it becomes another wall of confusion.",
|
|
641
|
+
"Skipping the dependency order and calling a pile of bullets a plan.",
|
|
642
|
+
"Explaining implementation details before naming the user's actual question.",
|
|
643
|
+
"Using granularity to delay the next concrete move.",
|
|
644
|
+
],
|
|
645
|
+
"micro_loop": [
|
|
646
|
+
"Name the whole confusing thing in one plain sentence.",
|
|
647
|
+
"Split it into current state, desired state, missing bridge, and dependency order.",
|
|
648
|
+
"Choose one small verification that reduces ambiguity, then compress the result back down.",
|
|
649
|
+
],
|
|
650
|
+
"breakdown_sequence": [
|
|
651
|
+
{
|
|
652
|
+
"level_id": "L0_whole_frame",
|
|
653
|
+
"title": "Whole Frame",
|
|
654
|
+
"purpose": "Start broad enough that the user and future agent know what object is being decomposed.",
|
|
655
|
+
"prompt": "What is the broad claim, request, or confusion in one plain sentence?",
|
|
656
|
+
"output": "One sentence that names the top-level object without trying to solve it yet.",
|
|
657
|
+
"completion_check": "A future reader can tell what cathedral we are talking about before seeing any stones.",
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
"level_id": "L1_boundary",
|
|
661
|
+
"title": "Boundary",
|
|
662
|
+
"purpose": "Separate proof, implementation, and discovery so the breakdown does not overclaim.",
|
|
663
|
+
"prompt": "What is the exact desired state, and what is explicitly not being claimed yet?",
|
|
664
|
+
"output": "A narrowed north star plus falsifier, exception, or overclaim boundaries.",
|
|
665
|
+
"completion_check": "The target is smaller than the dream and honest about what remains unproved or undone.",
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
"level_id": "L2_major_lanes",
|
|
669
|
+
"title": "Major Lanes",
|
|
670
|
+
"purpose": "Break the top-level target into a small number of non-overlapping lanes.",
|
|
671
|
+
"prompt": "Which 3-7 lanes or phases cover the work without overlapping too much?",
|
|
672
|
+
"output": "Named lanes such as counting language, compatibility language, graph reduction, matching bounds, margin lift, finite closure.",
|
|
673
|
+
"completion_check": "Every important concern has a lane, and no lane is pretending to be the whole proof or project.",
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"level_id": "L3_subclaims",
|
|
677
|
+
"title": "Subclaims",
|
|
678
|
+
"purpose": "Turn each lane into named obligations that can be owned, proved, tested, or handed off.",
|
|
679
|
+
"prompt": "What smaller claims, tasks, or lemmas make each lane true?",
|
|
680
|
+
"output": "Task IDs with statements, hypotheses, dependencies, and proof or work type.",
|
|
681
|
+
"completion_check": "Each subclaim is small enough to discuss independently and large enough to matter.",
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"level_id": "L4_atomic_obligations",
|
|
685
|
+
"title": "Atomic Obligations",
|
|
686
|
+
"purpose": "Descend until each item is concrete enough to verify independently.",
|
|
687
|
+
"prompt": "Can each subclaim be split until it is either a definition check, counting identity, graph identity, finite check, data check, API check, or implementation step?",
|
|
688
|
+
"output": "Sub-sub-lemmas or atomic tasks with a falsifier boundary for each.",
|
|
689
|
+
"completion_check": "Every atomic item can be proved, computed, tested, or falsified without needing the whole plan in memory.",
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"level_id": "L5_dependency_ladder",
|
|
693
|
+
"title": "Dependency Ladder",
|
|
694
|
+
"purpose": "Order the atomic pieces by prerequisite, risk, and comprehension value.",
|
|
695
|
+
"prompt": "What has to be true before the next rung can safely happen?",
|
|
696
|
+
"output": "A dependency-ordered ladder with blockers, ready steps, and downstream unlocks.",
|
|
697
|
+
"completion_check": "The first few moves are ordered for real dependency reasons, not just narrative convenience.",
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"level_id": "L6_active_target",
|
|
701
|
+
"title": "Active Target",
|
|
702
|
+
"purpose": "Choose the first actionable rung and the current target theorem, task, or patch.",
|
|
703
|
+
"prompt": "What is the first actionable rung, what is the current target rung, and why is it the best next place to work?",
|
|
704
|
+
"output": "Current packet, first actionable step, target step, and why this is the next best move.",
|
|
705
|
+
"completion_check": "The next move is obvious enough that an agent can start without re-deriving the full breakdown.",
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
"level_id": "L7_durable_checklist",
|
|
709
|
+
"title": "Durable Checklist",
|
|
710
|
+
"purpose": "Promote important breakdowns out of chat memory and into a referenceable artifact.",
|
|
711
|
+
"prompt": "Where does this breakdown live so future agents can update it without depending on chat memory?",
|
|
712
|
+
"output": "A markdown checklist plus an optional machine-readable JSON checklist with statuses, dependencies, source artifacts, and falsifier boundaries.",
|
|
713
|
+
"completion_check": "The repo has a checklist that can move steps from todo to next to done without losing context.",
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"level_id": "L8_compress_and_continue",
|
|
717
|
+
"title": "Compress And Continue",
|
|
718
|
+
"purpose": "End simpler than we began by turning the full ladder back into the next honest move.",
|
|
719
|
+
"prompt": "After decomposing the work, what is the shortest useful summary and the next verification?",
|
|
720
|
+
"output": "A compact summary, the active target, and one verification command, artifact, or proof check.",
|
|
721
|
+
"completion_check": "The user gets clarity and momentum, not just a larger pile of scaffolding.",
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
"durable_artifact_rule": "If the breakdown becomes operationally important, write it to a durable checklist artifact with step IDs, dependencies, statuses, source artifacts, falsifier boundaries, and the first active target.",
|
|
725
|
+
"breakdown_output_contract": [
|
|
726
|
+
"Broad Frame",
|
|
727
|
+
"Boundary",
|
|
728
|
+
"Major Lanes",
|
|
729
|
+
"Subclaims",
|
|
730
|
+
"Atomic Obligations",
|
|
731
|
+
"Dependency Ladder",
|
|
732
|
+
"Active Target",
|
|
733
|
+
"Durable Checklist",
|
|
734
|
+
"Next Verification",
|
|
735
|
+
],
|
|
736
|
+
"nudge_cards": [
|
|
737
|
+
{
|
|
738
|
+
"title": "Name The Blob",
|
|
739
|
+
"prompt": "Write the whole confusing thing as one sentence, then split it into three named parts.",
|
|
740
|
+
"twist": "If a part cannot be named plainly, it is not understood yet.",
|
|
741
|
+
"release": "A named part is easier to move than an unnamed worry.",
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"title": "Current / Desired / Bridge",
|
|
745
|
+
"prompt": "Separate current state, desired state, and the missing bridge before proposing the fix.",
|
|
746
|
+
"twist": "Do not let the desired state masquerade as the plan.",
|
|
747
|
+
"release": "The bridge is where the real work usually lives.",
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"title": "Dependency Ladder",
|
|
751
|
+
"prompt": "Order the next steps by what must be true before the following step can safely happen.",
|
|
752
|
+
"twist": "Put user comprehension on the ladder, not only code dependencies.",
|
|
753
|
+
"release": "A good ladder makes the climb feel obvious.",
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
"title": "Tiny Verified Loop",
|
|
757
|
+
"prompt": "Choose one small action and one verification that would reduce ambiguity within the next pass.",
|
|
758
|
+
"twist": "The step should be small enough to finish, but real enough to matter.",
|
|
759
|
+
"release": "Confidence compounds through verified movement.",
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"title": "Explain Then Compress",
|
|
763
|
+
"prompt": "Break the topic down in detail, then compress the result into the shortest useful summary.",
|
|
764
|
+
"twist": "Do not leave the user with the scaffolding if the building is now clear.",
|
|
765
|
+
"release": "The best breakdown ends simpler than it began.",
|
|
766
|
+
},
|
|
767
|
+
],
|
|
768
|
+
},
|
|
600
769
|
]
|
|
601
770
|
|
|
602
771
|
|
|
@@ -641,6 +810,7 @@ def _agent_mode_public_payload(mode: dict[str, Any]) -> dict[str, Any]:
|
|
|
641
810
|
"questions": [str(row).strip() for row in mode.get("questions", []) if str(row).strip()],
|
|
642
811
|
"anti_patterns": [str(row).strip() for row in mode.get("anti_patterns", []) if str(row).strip()],
|
|
643
812
|
"nudge_card_count": len(mode.get("nudge_cards", [])) if isinstance(mode.get("nudge_cards"), list) else 0,
|
|
813
|
+
"breakdown_sequence_count": len(mode.get("breakdown_sequence", [])) if isinstance(mode.get("breakdown_sequence"), list) else 0,
|
|
644
814
|
}
|
|
645
815
|
|
|
646
816
|
|
|
@@ -659,6 +829,13 @@ def _agent_mode_nudge(mode: dict[str, Any], *, seed: str = "") -> dict[str, Any]
|
|
|
659
829
|
digest = hashlib.sha256(f"{mode['id']}::{effective_seed}".encode("utf-8")).hexdigest()
|
|
660
830
|
index = int(digest[:8], 16) % len(cards)
|
|
661
831
|
card = cards[index]
|
|
832
|
+
micro_loop = mode.get("micro_loop", [])
|
|
833
|
+
if not isinstance(micro_loop, list) or not micro_loop:
|
|
834
|
+
micro_loop = [
|
|
835
|
+
"Choose the right lens first: deeper, higher, wider, or rotated.",
|
|
836
|
+
"Make one pass sleeker by removing friction and generic weight.",
|
|
837
|
+
"Make one pass playful or progressive by trying one meaningful shift in angle.",
|
|
838
|
+
]
|
|
662
839
|
return {
|
|
663
840
|
"mode": _agent_mode_public_payload(mode),
|
|
664
841
|
"seed": effective_seed,
|
|
@@ -669,11 +846,57 @@ def _agent_mode_nudge(mode: dict[str, Any], *, seed: str = "") -> dict[str, Any]
|
|
|
669
846
|
"twist": str(card.get("twist", "")).strip(),
|
|
670
847
|
"release": str(card.get("release", "")).strip(),
|
|
671
848
|
},
|
|
672
|
-
"micro_loop": [
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
849
|
+
"micro_loop": [str(row).strip() for row in micro_loop if str(row).strip()],
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _agent_mode_breakdown(mode: dict[str, Any], *, topic: str = "") -> dict[str, Any]:
|
|
854
|
+
sequence = mode.get("breakdown_sequence", [])
|
|
855
|
+
if not isinstance(sequence, list) or not sequence:
|
|
856
|
+
raise RuntimeError("This mode does not define a breakdown sequence.")
|
|
857
|
+
|
|
858
|
+
normalized_sequence: list[dict[str, str]] = []
|
|
859
|
+
for row in sequence:
|
|
860
|
+
if not isinstance(row, dict):
|
|
861
|
+
continue
|
|
862
|
+
normalized_sequence.append(
|
|
863
|
+
{
|
|
864
|
+
"level_id": str(row.get("level_id", row.get("level", ""))).strip(),
|
|
865
|
+
"title": str(row.get("title", row.get("label", ""))).strip(),
|
|
866
|
+
"purpose": str(row.get("purpose", "")).strip(),
|
|
867
|
+
"prompt": str(row.get("prompt", row.get("question", ""))).strip(),
|
|
868
|
+
"output": str(row.get("output", "")).strip(),
|
|
869
|
+
"completion_check": str(row.get("completion_check", "")).strip(),
|
|
870
|
+
}
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
artifact_rule = str(mode.get("durable_artifact_rule", mode.get("artifact_rule", ""))).strip()
|
|
874
|
+
output_contract = [
|
|
875
|
+
str(row).strip()
|
|
876
|
+
for row in mode.get(
|
|
877
|
+
"breakdown_output_contract",
|
|
878
|
+
[
|
|
879
|
+
"Broad Frame",
|
|
880
|
+
"Boundary",
|
|
881
|
+
"Major Lanes",
|
|
882
|
+
"Subclaims",
|
|
883
|
+
"Atomic Obligations",
|
|
884
|
+
"Dependency Ladder",
|
|
885
|
+
"Active Target",
|
|
886
|
+
"Durable Checklist",
|
|
887
|
+
"Next Verification",
|
|
888
|
+
],
|
|
889
|
+
)
|
|
890
|
+
if str(row).strip()
|
|
891
|
+
]
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
"mode": _agent_mode_public_payload(mode),
|
|
895
|
+
"topic": str(topic or "").strip(),
|
|
896
|
+
"sequence": normalized_sequence,
|
|
897
|
+
"durable_artifact_rule": artifact_rule,
|
|
898
|
+
"artifact_rule": artifact_rule,
|
|
899
|
+
"output_contract": output_contract,
|
|
677
900
|
}
|
|
678
901
|
|
|
679
902
|
|
|
@@ -3443,6 +3666,9 @@ def _hosted_api_error(
|
|
|
3443
3666
|
payload: dict[str, Any] | None,
|
|
3444
3667
|
) -> HostedApiError:
|
|
3445
3668
|
message = str((payload or {}).get("error") or (payload or {}).get("message") or f"Request failed: {status}")
|
|
3669
|
+
stripped_message = message.lstrip()
|
|
3670
|
+
if stripped_message.startswith("<!DOCTYPE html") or stripped_message.startswith("<html"):
|
|
3671
|
+
message = "Hosted ORP returned an HTML error page instead of JSON"
|
|
3446
3672
|
suffix = f" (status={status} path={path})"
|
|
3447
3673
|
hint = ""
|
|
3448
3674
|
if status == 401:
|
|
@@ -3450,7 +3676,13 @@ def _hosted_api_error(
|
|
|
3450
3676
|
elif status == 403:
|
|
3451
3677
|
hint = " The hosted ORP app rejected the operation. Check permissions on the target record."
|
|
3452
3678
|
elif status == 404:
|
|
3453
|
-
|
|
3679
|
+
if message == "Hosted ORP returned an HTML error page instead of JSON":
|
|
3680
|
+
hint = (
|
|
3681
|
+
" The hosted API route may not be deployed at this base URL. "
|
|
3682
|
+
"Check ORP_BASE_URL or deploy the hosted ORP app."
|
|
3683
|
+
)
|
|
3684
|
+
else:
|
|
3685
|
+
hint = " The hosted record may have changed. Re-list the resource and retry."
|
|
3454
3686
|
elif status == 409:
|
|
3455
3687
|
hint = " The hosted record changed since you last fetched it. Re-open it and retry the update."
|
|
3456
3688
|
return HostedApiError(f"{message}{suffix}.{hint}".replace("..", "."))
|
|
@@ -10632,6 +10864,7 @@ def _about_payload() -> dict[str, Any]:
|
|
|
10632
10864
|
{"name": "mode_list", "path": ["mode", "list"], "json_output": True},
|
|
10633
10865
|
{"name": "mode_show", "path": ["mode", "show"], "json_output": True},
|
|
10634
10866
|
{"name": "mode_nudge", "path": ["mode", "nudge"], "json_output": True},
|
|
10867
|
+
{"name": "mode_breakdown", "path": ["mode", "breakdown"], "json_output": True},
|
|
10635
10868
|
{"name": "secrets_list", "path": ["secrets", "list"], "json_output": True},
|
|
10636
10869
|
{"name": "secrets_show", "path": ["secrets", "show"], "json_output": True},
|
|
10637
10870
|
{"name": "secrets_add", "path": ["secrets", "add"], "json_output": True},
|
|
@@ -10734,7 +10967,7 @@ def _about_payload() -> dict[str, Any]:
|
|
|
10734
10967
|
"Knowledge exchange is a built-in ORP ability exposed through `orp exchange repo synthesize`, producing structured exchange artifacts and transfer maps for local or remote source repositories.",
|
|
10735
10968
|
"Collaboration is a built-in ORP ability exposed through `orp collaborate ...`.",
|
|
10736
10969
|
"Frontier control is a built-in ORP ability exposed through `orp frontier ...`, separating the exact live point, the exact active milestone, the near structured checklist, and the farther major-version stack.",
|
|
10737
|
-
"Agent modes are lightweight optional overlays for taste, perspective shifts, and
|
|
10970
|
+
"Agent modes are lightweight optional overlays for taste, perspective shifts, fresh movement, and intentional comprehension breakdowns; `orp mode breakdown granular-breakdown --json` gives agents a broad-to-atomic ladder for complex work, while `orp mode nudge granular-breakdown --json` gives a short reminder card.",
|
|
10738
10971
|
"Project/session linking is a built-in ORP ability exposed through `orp link ...` and stored machine-locally under `.git/orp/link/`.",
|
|
10739
10972
|
"Secrets are easiest to understand as saved credentials and related login metadata: humans usually run `orp secrets add ...` and paste the value at the prompt, agents usually pipe the value with `--value-stdin`, optional usernames can be stored alongside the secret when a service needs them, and local macOS Keychain caching plus hosted sync are optional layers on top.",
|
|
10740
10973
|
"Connections give ORP one place to remember service accounts, public data sources, deployment targets, and which saved secret alias or named secret bindings power each integration through `orp connections providers`, `orp connections list`, `orp connections show`, `orp connections add`, `orp connections update`, `orp connections remove`, `orp connections sync`, and `orp connections pull`.",
|
|
@@ -10890,6 +11123,10 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
10890
11123
|
"label": "Get an optional creativity/perspective nudge",
|
|
10891
11124
|
"command": "orp mode nudge sleek-minimal-progressive --json",
|
|
10892
11125
|
},
|
|
11126
|
+
{
|
|
11127
|
+
"label": "Break complex work into smaller intentional steps",
|
|
11128
|
+
"command": "orp mode breakdown granular-breakdown --json",
|
|
11129
|
+
},
|
|
10893
11130
|
]
|
|
10894
11131
|
|
|
10895
11132
|
quick_actions = [
|
|
@@ -10993,6 +11230,14 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
10993
11230
|
"label": "Get an optional creative perspective nudge for agent work",
|
|
10994
11231
|
"command": "orp mode nudge sleek-minimal-progressive --json",
|
|
10995
11232
|
},
|
|
11233
|
+
{
|
|
11234
|
+
"label": "Get an optional breakdown nudge when work needs more granularity",
|
|
11235
|
+
"command": "orp mode nudge granular-breakdown --json",
|
|
11236
|
+
},
|
|
11237
|
+
{
|
|
11238
|
+
"label": "Generate a broad-to-atomic breakdown ladder for complex work",
|
|
11239
|
+
"command": "orp mode breakdown granular-breakdown --json",
|
|
11240
|
+
},
|
|
10996
11241
|
{
|
|
10997
11242
|
"label": "List first-class hosted workspaces",
|
|
10998
11243
|
"command": "orp workspaces list --json",
|
|
@@ -11356,11 +11601,14 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
11356
11601
|
},
|
|
11357
11602
|
{
|
|
11358
11603
|
"id": "modes",
|
|
11359
|
-
"description": "Lightweight optional cognitive overlays for taste, creativity, perspective shifts, and
|
|
11604
|
+
"description": "Lightweight optional cognitive overlays for taste, creativity, perspective shifts, exploratory momentum, and granular comprehension breakdowns.",
|
|
11360
11605
|
"entrypoints": [
|
|
11361
11606
|
"orp mode list --json",
|
|
11362
11607
|
"orp mode show sleek-minimal-progressive --json",
|
|
11363
11608
|
"orp mode nudge sleek-minimal-progressive --json",
|
|
11609
|
+
"orp mode show granular-breakdown --json",
|
|
11610
|
+
"orp mode nudge granular-breakdown --json",
|
|
11611
|
+
"orp mode breakdown granular-breakdown --json",
|
|
11364
11612
|
],
|
|
11365
11613
|
},
|
|
11366
11614
|
{
|
|
@@ -15024,6 +15272,7 @@ def cmd_mode_show(args: argparse.Namespace) -> int:
|
|
|
15024
15272
|
("mode.activation_phrase", mode_payload["activation_phrase"]),
|
|
15025
15273
|
("mode.invocation_style", mode_payload["invocation_style"]),
|
|
15026
15274
|
("mode.nudge_card_count", mode_payload["nudge_card_count"]),
|
|
15275
|
+
("mode.breakdown_sequence_count", mode_payload["breakdown_sequence_count"]),
|
|
15027
15276
|
]
|
|
15028
15277
|
)
|
|
15029
15278
|
for key in ("when_to_use", "perspective_shifts", "principles", "ritual", "questions", "anti_patterns"):
|
|
@@ -15064,6 +15313,43 @@ def cmd_mode_nudge(args: argparse.Namespace) -> int:
|
|
|
15064
15313
|
return 0
|
|
15065
15314
|
|
|
15066
15315
|
|
|
15316
|
+
def cmd_mode_breakdown(args: argparse.Namespace) -> int:
|
|
15317
|
+
mode = _agent_mode(getattr(args, "mode_ref", ""))
|
|
15318
|
+
payload = {
|
|
15319
|
+
"ok": True,
|
|
15320
|
+
**_agent_mode_breakdown(mode, topic=str(getattr(args, "topic", "") or "").strip()),
|
|
15321
|
+
}
|
|
15322
|
+
if args.json_output:
|
|
15323
|
+
_print_json(payload)
|
|
15324
|
+
return 0
|
|
15325
|
+
|
|
15326
|
+
pairs = [
|
|
15327
|
+
("mode.id", payload["mode"]["id"]),
|
|
15328
|
+
("mode.label", payload["mode"]["label"]),
|
|
15329
|
+
("mode.activation_phrase", payload["mode"]["activation_phrase"]),
|
|
15330
|
+
]
|
|
15331
|
+
if payload["topic"]:
|
|
15332
|
+
pairs.append(("breakdown.topic", payload["topic"]))
|
|
15333
|
+
pairs.append(("breakdown.sequence_count", len(payload["sequence"])))
|
|
15334
|
+
_print_pairs(pairs)
|
|
15335
|
+
|
|
15336
|
+
print("breakdown.sequence:")
|
|
15337
|
+
for index, row in enumerate(payload["sequence"], start=1):
|
|
15338
|
+
title = row.get("title", "")
|
|
15339
|
+
level_id = row.get("level_id", "")
|
|
15340
|
+
print(f"{index}. {title} [{level_id}]")
|
|
15341
|
+
for key in ("purpose", "prompt", "output", "completion_check"):
|
|
15342
|
+
value = str(row.get(key, "")).strip()
|
|
15343
|
+
if value:
|
|
15344
|
+
print(f" {key}: {value}")
|
|
15345
|
+
if payload["durable_artifact_rule"]:
|
|
15346
|
+
print(f"durable_artifact_rule={payload['durable_artifact_rule']}")
|
|
15347
|
+
print("output_contract:")
|
|
15348
|
+
for row in payload["output_contract"]:
|
|
15349
|
+
print(f"- {row}")
|
|
15350
|
+
return 0
|
|
15351
|
+
|
|
15352
|
+
|
|
15067
15353
|
def cmd_update(args: argparse.Namespace) -> int:
|
|
15068
15354
|
payload = _update_payload()
|
|
15069
15355
|
if getattr(args, "yes", False):
|
|
@@ -22357,7 +22643,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
22357
22643
|
|
|
22358
22644
|
s_mode = sub.add_parser(
|
|
22359
22645
|
"mode",
|
|
22360
|
-
help="Agent-first
|
|
22646
|
+
help="Agent-first cognitive overlay modes for creativity, perspective, and breakdown",
|
|
22361
22647
|
)
|
|
22362
22648
|
mode_sub = s_mode.add_subparsers(dest="mode_cmd", required=True)
|
|
22363
22649
|
|
|
@@ -22372,7 +22658,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
22372
22658
|
|
|
22373
22659
|
s_mode_nudge = mode_sub.add_parser(
|
|
22374
22660
|
"nudge",
|
|
22375
|
-
help="Return a deterministic
|
|
22661
|
+
help="Return a deterministic nudge card for one agent mode",
|
|
22376
22662
|
)
|
|
22377
22663
|
s_mode_nudge.add_argument("mode_ref", help="Mode id or alias")
|
|
22378
22664
|
s_mode_nudge.add_argument(
|
|
@@ -22383,6 +22669,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
22383
22669
|
add_json_flag(s_mode_nudge)
|
|
22384
22670
|
s_mode_nudge.set_defaults(func=cmd_mode_nudge, json_output=False)
|
|
22385
22671
|
|
|
22672
|
+
s_mode_breakdown = mode_sub.add_parser(
|
|
22673
|
+
"breakdown",
|
|
22674
|
+
help="Return a broad-to-atomic breakdown ladder for one agent mode",
|
|
22675
|
+
)
|
|
22676
|
+
s_mode_breakdown.add_argument("mode_ref", help="Mode id or alias")
|
|
22677
|
+
s_mode_breakdown.add_argument(
|
|
22678
|
+
"--topic",
|
|
22679
|
+
default="",
|
|
22680
|
+
help="Optional topic label for the breakdown target",
|
|
22681
|
+
)
|
|
22682
|
+
add_json_flag(s_mode_breakdown)
|
|
22683
|
+
s_mode_breakdown.set_defaults(func=cmd_mode_breakdown, json_output=False)
|
|
22684
|
+
|
|
22386
22685
|
s_update = sub.add_parser(
|
|
22387
22686
|
"update",
|
|
22388
22687
|
help="Check npm for a newer ORP release and print the recommended upgrade command",
|
package/docs/AGENT_LOOP.md
CHANGED
|
@@ -16,6 +16,11 @@ read:
|
|
|
16
16
|
exploratory reframing, run:
|
|
17
17
|
- `orp mode nudge sleek-minimal-progressive --json`
|
|
18
18
|
- Treat it as an optional lens for deeper, wider, top-down, or rotated perspective shifts.
|
|
19
|
+
- If the task feels confusing, too large to hold at once, or likely to benefit
|
|
20
|
+
from more intentional granularity, run:
|
|
21
|
+
- `orp mode breakdown granular-breakdown --json`
|
|
22
|
+
- Optionally follow with `orp mode nudge granular-breakdown --json` if you only need a short reminder card.
|
|
23
|
+
- Treat it as a broad-to-atomic ladder: whole frame, boundary, major lanes, subclaims, atomic obligations, dependency order, durable checklist, and next verification.
|
|
19
24
|
- If packs matter, run `orp pack list --json`.
|
|
20
25
|
- Read `PROTOCOL.md` before making claims.
|
|
21
26
|
- If the repo uses parent/child agent guidance, run `orp agents audit --json` so you know `AGENTS.md` and `CLAUDE.md` are aligned before taking a long-running path.
|
package/docs/AGENT_MODES.md
CHANGED
|
@@ -77,3 +77,49 @@ Use this when the work needs bigger options before it needs tighter polish.
|
|
|
77
77
|
- import principles from other domains
|
|
78
78
|
- keep one unreasonable idea alive long enough to inspect it
|
|
79
79
|
- prune only after a genuinely bold pass exists
|
|
80
|
+
|
|
81
|
+
### `granular-breakdown`
|
|
82
|
+
|
|
83
|
+
Use this when the work needs more intentional granularity so the user, agent,
|
|
84
|
+
or future collaborator can actually understand and continue it.
|
|
85
|
+
|
|
86
|
+
- name the whole problem plainly
|
|
87
|
+
- split the work into current state, desired state, and missing bridge
|
|
88
|
+
- order steps by dependency, risk, and comprehension
|
|
89
|
+
- choose one small verification that proves movement
|
|
90
|
+
- compress the result back into a clear summary after the breakdown
|
|
91
|
+
|
|
92
|
+
Recommended commands:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
orp mode show granular-breakdown --json
|
|
96
|
+
orp mode breakdown granular-breakdown --json
|
|
97
|
+
orp mode nudge granular-breakdown --json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `breakdown` when the task needs a real ladder from broad framing to atomic
|
|
101
|
+
subtasks or sub-lemmas. Use `nudge` when the agent only needs a compact reminder
|
|
102
|
+
card for the next pass.
|
|
103
|
+
|
|
104
|
+
Use this regularly as part of the research/development loop when:
|
|
105
|
+
|
|
106
|
+
- the user asks what a feature, command, or error means
|
|
107
|
+
- the plan is correct but too large to hold at once
|
|
108
|
+
- a repo handoff needs to be understandable to the next agent
|
|
109
|
+
- a high-level goal needs to become a safe sequence of small moves
|
|
110
|
+
|
|
111
|
+
The full breakdown ladder is:
|
|
112
|
+
|
|
113
|
+
- whole frame
|
|
114
|
+
- boundary
|
|
115
|
+
- major lanes
|
|
116
|
+
- subclaims
|
|
117
|
+
- atomic obligations
|
|
118
|
+
- dependency ladder
|
|
119
|
+
- active target
|
|
120
|
+
- durable checklist
|
|
121
|
+
- next verification
|
|
122
|
+
|
|
123
|
+
If the breakdown becomes operationally important, promote it into a durable
|
|
124
|
+
checklist artifact with stable IDs, dependencies, statuses, source artifacts,
|
|
125
|
+
falsifier boundaries, and the first active target.
|
package/docs/START_HERE.md
CHANGED
|
@@ -191,6 +191,17 @@ If you want ORP to remember an ongoing repo path plus a resumable session, add i
|
|
|
191
191
|
orp workspace add-tab main --path /absolute/path/to/project --remote-url git@github.com:org/project.git --bootstrap-command "npm install" --resume-command "codex resume <session-id>"
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
If the same project has more than one active thread, append another session for
|
|
195
|
+
that path:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
orp workspace add-tab main --path /absolute/path/to/project --title "release hardening" --resume-tool codex --resume-session-id <session-id> --append
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
ORP keeps the flat `tabs` list for copyable recovery order, and also stores a
|
|
202
|
+
grouped `projects` object so all sessions for the same repo live together under
|
|
203
|
+
`projects[].sessions[]`.
|
|
204
|
+
|
|
194
205
|
For Claude:
|
|
195
206
|
|
|
196
207
|
```bash
|
|
@@ -652,12 +663,22 @@ When the work feels too linear, too trapped, or too narrow:
|
|
|
652
663
|
orp mode nudge sleek-minimal-progressive --json
|
|
653
664
|
```
|
|
654
665
|
|
|
655
|
-
|
|
666
|
+
When the work feels too big, confusing, or hard to sequence:
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
orp mode breakdown granular-breakdown --json
|
|
670
|
+
orp mode nudge granular-breakdown --json
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Use `breakdown` for the full broad-to-atomic ladder and `nudge` for a short
|
|
674
|
+
reminder card. Both are optional. They do not override the protocol. They just
|
|
675
|
+
give the agent or operator a clearer thinking scaffold.
|
|
656
676
|
|
|
657
677
|
It is there to help with:
|
|
658
678
|
|
|
659
679
|
- getting unstuck
|
|
660
680
|
- zooming in or out
|
|
681
|
+
- breaking complex work into smaller intentional steps
|
|
661
682
|
- rotating the angle of attack
|
|
662
683
|
- keeping the work fresh without making the workflow sloppy
|
|
663
684
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-research-protocol",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "ORP CLI (Open Research Protocol): workspace ledgers, secrets, scheduling, governed execution, and agent-friendly research workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fractal Research Group <cody@frg.earth>",
|
|
@@ -34,8 +34,15 @@ Add a new saved tab manually:
|
|
|
34
34
|
orp workspace ledger add main --path /absolute/path/to/frg-site --resume-command "codex resume 019d348d-5031-78e1-9840-a66deaac33ae"
|
|
35
35
|
orp workspace add-tab main --path /absolute/path/to/anthropic-lab --resume-tool claude --resume-session-id claude-456
|
|
36
36
|
orp workspace add-tab main --path /absolute/path/to/orp-web-app --remote-url git@github.com:SproutSeeds/orp-web-app.git --bootstrap-command "pnpm install"
|
|
37
|
+
orp workspace add-tab main --path /absolute/path/to/orp --title "release hardening" --resume-tool codex --resume-session-id 019d32d3-d8b2-7fa2-aaec-c74b5134afd6 --append
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
When several saved sessions point at the same project path, ORP now also writes
|
|
41
|
+
a grouped `projects` object in the manifest. The flat `tabs` list remains for
|
|
42
|
+
older tools, but agents should prefer `projects[].sessions[]` when they need to
|
|
43
|
+
understand all active Codex or Claude threads for one repo before pruning or
|
|
44
|
+
sunsetting a session.
|
|
45
|
+
|
|
39
46
|
Remove a saved tab manually:
|
|
40
47
|
|
|
41
48
|
```bash
|
|
@@ -282,6 +282,37 @@ function normalizeStructuredTab(rawTab, index) {
|
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
function normalizeStructuredProject(rawProject, projectIndex) {
|
|
286
|
+
if (!rawProject || typeof rawProject !== "object" || Array.isArray(rawProject)) {
|
|
287
|
+
throw new Error(`workspace project ${projectIndex + 1} must be an object`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const projectPath = validateAbsolutePath(rawProject.path, `workspace project ${projectIndex + 1} path`);
|
|
291
|
+
const sessions = Array.isArray(rawProject.sessions) && rawProject.sessions.length > 0 ? rawProject.sessions : [{}];
|
|
292
|
+
|
|
293
|
+
return sessions.map((rawSession, sessionIndex) => {
|
|
294
|
+
if (!rawSession || typeof rawSession !== "object" || Array.isArray(rawSession)) {
|
|
295
|
+
throw new Error(`workspace project ${projectIndex + 1} session ${sessionIndex + 1} must be an object`);
|
|
296
|
+
}
|
|
297
|
+
return normalizeStructuredTab(
|
|
298
|
+
{
|
|
299
|
+
title: rawSession.title ?? rawProject.title,
|
|
300
|
+
path: projectPath,
|
|
301
|
+
remoteUrl: rawSession.remoteUrl ?? rawProject.remoteUrl,
|
|
302
|
+
remoteBranch: rawSession.remoteBranch ?? rawProject.remoteBranch,
|
|
303
|
+
bootstrapCommand: rawSession.bootstrapCommand ?? rawProject.bootstrapCommand,
|
|
304
|
+
resumeCommand: rawSession.resumeCommand,
|
|
305
|
+
resumeTool: rawSession.resumeTool,
|
|
306
|
+
resumeSessionId: rawSession.resumeSessionId ?? rawSession.sessionId,
|
|
307
|
+
codexSessionId: rawSession.codexSessionId,
|
|
308
|
+
claudeSessionId: rawSession.claudeSessionId,
|
|
309
|
+
tmuxSessionName: rawSession.tmuxSessionName,
|
|
310
|
+
},
|
|
311
|
+
sessionIndex,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
285
316
|
export function normalizeWorkspaceManifest(rawManifest) {
|
|
286
317
|
if (!rawManifest || typeof rawManifest !== "object" || Array.isArray(rawManifest)) {
|
|
287
318
|
throw new Error("workspace manifest must be a JSON object");
|
|
@@ -292,11 +323,15 @@ export function normalizeWorkspaceManifest(rawManifest) {
|
|
|
292
323
|
throw new Error(`unsupported workspace manifest version: ${version}`);
|
|
293
324
|
}
|
|
294
325
|
|
|
295
|
-
|
|
296
|
-
|
|
326
|
+
const hasProjects = Array.isArray(rawManifest.projects);
|
|
327
|
+
const hasTabs = Array.isArray(rawManifest.tabs);
|
|
328
|
+
if (!hasProjects && !hasTabs) {
|
|
329
|
+
throw new Error("workspace manifest must include a tabs array or projects array");
|
|
297
330
|
}
|
|
298
331
|
|
|
299
|
-
const tabs =
|
|
332
|
+
const tabs = hasProjects
|
|
333
|
+
? rawManifest.projects.flatMap((project, index) => normalizeStructuredProject(project, index))
|
|
334
|
+
: rawManifest.tabs.map((tab, index) => normalizeStructuredTab(tab, index));
|
|
300
335
|
return {
|
|
301
336
|
version,
|
|
302
337
|
workspaceId: normalizeOptionalString(rawManifest.workspaceId),
|
|
@@ -334,6 +369,57 @@ export function deriveBaseTitle(entry) {
|
|
|
334
369
|
return path.basename(normalized) || normalized;
|
|
335
370
|
}
|
|
336
371
|
|
|
372
|
+
export function buildWorkspaceProjectGroups(entries = []) {
|
|
373
|
+
const groups = new Map();
|
|
374
|
+
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
const projectPath = validateAbsolutePath(entry.path, "workspace project path");
|
|
377
|
+
if (!groups.has(projectPath)) {
|
|
378
|
+
groups.set(projectPath, {
|
|
379
|
+
title: deriveBaseTitle(entry),
|
|
380
|
+
path: projectPath,
|
|
381
|
+
remoteUrl: normalizeOptionalUrl(entry.remoteUrl, "workspace project remoteUrl"),
|
|
382
|
+
remoteBranch: normalizeOptionalString(entry.remoteBranch),
|
|
383
|
+
bootstrapCommand: normalizeOptionalCommand(entry.bootstrapCommand),
|
|
384
|
+
sessions: [],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const project = groups.get(projectPath);
|
|
389
|
+
project.remoteUrl = project.remoteUrl || normalizeOptionalUrl(entry.remoteUrl, "workspace project remoteUrl");
|
|
390
|
+
project.remoteBranch = project.remoteBranch || normalizeOptionalString(entry.remoteBranch);
|
|
391
|
+
project.bootstrapCommand = project.bootstrapCommand || normalizeOptionalCommand(entry.bootstrapCommand);
|
|
392
|
+
|
|
393
|
+
const resume = resolveResumeMetadata(entry);
|
|
394
|
+
project.sessions.push(
|
|
395
|
+
Object.fromEntries(
|
|
396
|
+
Object.entries({
|
|
397
|
+
title: normalizeOptionalString(entry.title) || deriveBaseTitle(entry),
|
|
398
|
+
resumeCommand: resume.resumeCommand || undefined,
|
|
399
|
+
resumeTool: resume.resumeTool || undefined,
|
|
400
|
+
resumeSessionId: resume.resumeSessionId || undefined,
|
|
401
|
+
codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
|
|
402
|
+
claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
|
|
403
|
+
}).filter(([, value]) => value !== undefined),
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return [...groups.values()].map((project) =>
|
|
409
|
+
Object.fromEntries(
|
|
410
|
+
Object.entries({
|
|
411
|
+
title: project.title,
|
|
412
|
+
path: project.path,
|
|
413
|
+
remoteUrl: project.remoteUrl || undefined,
|
|
414
|
+
remoteBranch: project.remoteBranch || undefined,
|
|
415
|
+
bootstrapCommand: project.bootstrapCommand || undefined,
|
|
416
|
+
sessionCount: project.sessions.length,
|
|
417
|
+
sessions: project.sessions,
|
|
418
|
+
}).filter(([, value]) => value !== undefined),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
337
423
|
export function deriveTmuxSessionName(entry, options = {}) {
|
|
338
424
|
if (entry.tmuxSessionName && String(entry.tmuxSessionName).trim().length > 0) {
|
|
339
425
|
return String(entry.tmuxSessionName).trim();
|
|
@@ -97,6 +97,59 @@ function matchPreviousHostedTab(tab, previousTabs) {
|
|
|
97
97
|
return match;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function buildHostedProjectGroups(tabs) {
|
|
101
|
+
const groups = new Map();
|
|
102
|
+
for (const tab of tabs) {
|
|
103
|
+
const projectRoot = normalizeOptionalString(tab.project_root);
|
|
104
|
+
if (!projectRoot) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!groups.has(projectRoot)) {
|
|
108
|
+
groups.set(projectRoot, {
|
|
109
|
+
title: normalizeOptionalString(tab.repo_label) || path.basename(String(projectRoot).replace(/\/+$/, "")) || projectRoot,
|
|
110
|
+
project_root: projectRoot,
|
|
111
|
+
remote_url: normalizeOptionalString(tab.remote_url),
|
|
112
|
+
remote_branch: normalizeOptionalString(tab.remote_branch),
|
|
113
|
+
bootstrap_command: normalizeOptionalString(tab.bootstrap_command),
|
|
114
|
+
sessions: [],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const project = groups.get(projectRoot);
|
|
118
|
+
project.remote_url = project.remote_url || normalizeOptionalString(tab.remote_url);
|
|
119
|
+
project.remote_branch = project.remote_branch || normalizeOptionalString(tab.remote_branch);
|
|
120
|
+
project.bootstrap_command = project.bootstrap_command || normalizeOptionalString(tab.bootstrap_command);
|
|
121
|
+
project.sessions.push(
|
|
122
|
+
Object.fromEntries(
|
|
123
|
+
Object.entries({
|
|
124
|
+
tab_id: normalizeOptionalString(tab.tab_id),
|
|
125
|
+
title: normalizeOptionalString(tab.title),
|
|
126
|
+
resume_command: normalizeOptionalString(tab.resume_command),
|
|
127
|
+
resume_tool: normalizeOptionalString(tab.resume_tool),
|
|
128
|
+
resume_session_id: normalizeOptionalString(tab.resume_session_id),
|
|
129
|
+
codex_session_id: normalizeOptionalString(tab.codex_session_id),
|
|
130
|
+
claude_session_id: normalizeOptionalString(tab.claude_session_id),
|
|
131
|
+
status: normalizeOptionalString(tab.status),
|
|
132
|
+
current_task: normalizeOptionalString(tab.current_task),
|
|
133
|
+
}).filter(([, value]) => value !== undefined && value !== null),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...groups.values()].map((project) =>
|
|
139
|
+
Object.fromEntries(
|
|
140
|
+
Object.entries({
|
|
141
|
+
title: project.title,
|
|
142
|
+
project_root: project.project_root,
|
|
143
|
+
remote_url: project.remote_url || undefined,
|
|
144
|
+
remote_branch: project.remote_branch || undefined,
|
|
145
|
+
bootstrap_command: project.bootstrap_command || undefined,
|
|
146
|
+
session_count: project.sessions.length,
|
|
147
|
+
sessions: project.sessions,
|
|
148
|
+
}).filter(([, value]) => value !== undefined && value !== null),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
101
154
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
102
155
|
throw new Error("workspace manifest is required to build a hosted workspace state payload");
|
|
@@ -172,6 +225,7 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
172
225
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
173
226
|
);
|
|
174
227
|
});
|
|
228
|
+
const projects = buildHostedProjectGroups(tabs);
|
|
175
229
|
|
|
176
230
|
const captureContext = Object.fromEntries(
|
|
177
231
|
Object.entries({
|
|
@@ -210,7 +264,9 @@ export function buildHostedWorkspaceState(manifest, options = {}) {
|
|
|
210
264
|
captured_at_utc: capturedAt,
|
|
211
265
|
updated_at_utc: updatedAt,
|
|
212
266
|
tab_count: tabs.length,
|
|
267
|
+
project_count: projects.length,
|
|
213
268
|
capture_context: Object.keys(captureContext).length > 0 ? captureContext : undefined,
|
|
269
|
+
projects,
|
|
214
270
|
tabs,
|
|
215
271
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
216
272
|
);
|
|
@@ -5,6 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
buildDirectCommand,
|
|
8
|
+
buildWorkspaceProjectGroups,
|
|
8
9
|
deriveBaseTitle,
|
|
9
10
|
normalizeWorkspaceManifest,
|
|
10
11
|
parseWorkspaceSource,
|
|
@@ -110,6 +111,7 @@ function buildWorkspaceResultTab(tab) {
|
|
|
110
111
|
|
|
111
112
|
function materializeWorkspaceManifest(manifest) {
|
|
112
113
|
const normalized = normalizeWorkspaceManifest(manifest);
|
|
114
|
+
const tabs = normalized.tabs.map((tab) => materializeWorkspaceTab(tab));
|
|
113
115
|
return Object.fromEntries(
|
|
114
116
|
Object.entries({
|
|
115
117
|
version: normalized.version,
|
|
@@ -117,7 +119,8 @@ function materializeWorkspaceManifest(manifest) {
|
|
|
117
119
|
title: normalized.title || undefined,
|
|
118
120
|
machine: normalized.machine || undefined,
|
|
119
121
|
capture: normalized.capture || undefined,
|
|
120
|
-
|
|
122
|
+
projects: buildWorkspaceProjectGroups(normalized.tabs),
|
|
123
|
+
tabs,
|
|
121
124
|
}).filter(([, value]) => value !== undefined),
|
|
122
125
|
);
|
|
123
126
|
}
|
|
@@ -670,7 +673,7 @@ Options:
|
|
|
670
673
|
--resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
|
|
671
674
|
--resume-session-id <id> Resume session id to save with the tab
|
|
672
675
|
--current-codex Save the current \`CODEX_THREAD_ID\` as a Codex resume target
|
|
673
|
-
--append
|
|
676
|
+
--append Add another saved session for the same project path instead of updating the existing one
|
|
674
677
|
--hosted-workspace-id <id> Edit a first-class hosted workspace directly
|
|
675
678
|
--workspace-file <path> Edit a local structured workspace manifest
|
|
676
679
|
--json Print the updated workspace edit result as JSON
|
|
@@ -681,6 +684,7 @@ Examples:
|
|
|
681
684
|
orp workspace add-tab main --here --current-codex
|
|
682
685
|
orp workspace add-tab main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
|
|
683
686
|
orp workspace add-tab main --path /absolute/path/to/new-project --resume-tool claude --resume-session-id claude-456
|
|
687
|
+
orp workspace add-tab main --path /absolute/path/to/new-project --title "second active thread" --resume-tool codex --resume-session-id 019d... --append
|
|
684
688
|
orp workspace add-tab main --path /absolute/path/to/new-project --remote-url git@github.com:org/new-project.git --bootstrap-command "npm install"
|
|
685
689
|
`);
|
|
686
690
|
}
|
|
@@ -2,6 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
buildWorkspaceProjectGroups,
|
|
5
6
|
deriveBaseTitle,
|
|
6
7
|
deriveWorkspaceId,
|
|
7
8
|
getResumeCommand,
|
|
@@ -208,6 +209,7 @@ function serializeWorkspaceManifest(manifest) {
|
|
|
208
209
|
}).filter(([, value]) => value !== undefined),
|
|
209
210
|
),
|
|
210
211
|
);
|
|
212
|
+
const projects = buildWorkspaceProjectGroups(manifest.tabs);
|
|
211
213
|
|
|
212
214
|
const normalized = Object.fromEntries(
|
|
213
215
|
Object.entries({
|
|
@@ -215,6 +217,7 @@ function serializeWorkspaceManifest(manifest) {
|
|
|
215
217
|
workspaceId: normalizeOptionalString(manifest.workspaceId) ?? undefined,
|
|
216
218
|
title: normalizeOptionalString(manifest.title) ?? undefined,
|
|
217
219
|
machine: manifest.machine ?? undefined,
|
|
220
|
+
projects,
|
|
218
221
|
tabs,
|
|
219
222
|
}).filter(([, value]) => value !== undefined),
|
|
220
223
|
);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
buildDirectCommand,
|
|
6
6
|
buildLaunchPlan,
|
|
7
7
|
buildSetupCommand,
|
|
8
|
+
buildWorkspaceProjectGroups,
|
|
8
9
|
deriveWorkspaceId,
|
|
9
10
|
getResumeCommand,
|
|
10
11
|
parseWorkspaceSource,
|
|
@@ -63,6 +64,7 @@ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
|
|
|
63
64
|
tmux: false,
|
|
64
65
|
resume: true,
|
|
65
66
|
});
|
|
67
|
+
const projectGroups = buildWorkspaceProjectGroups(launchTabs);
|
|
66
68
|
|
|
67
69
|
return {
|
|
68
70
|
sourceType: source.sourceType,
|
|
@@ -72,7 +74,24 @@ export function buildWorkspaceTabsReport(source, parsed, options = {}) {
|
|
|
72
74
|
machine: parsed.manifest?.machine || null,
|
|
73
75
|
parseMode: parsed.parseMode,
|
|
74
76
|
tabCount: launchTabs.length,
|
|
77
|
+
projectCount: projectGroups.length,
|
|
75
78
|
skippedCount: parsed.skipped.length,
|
|
79
|
+
projects: projectGroups.map((project) => ({
|
|
80
|
+
...project,
|
|
81
|
+
sessions: project.sessions.map((session) => ({
|
|
82
|
+
...session,
|
|
83
|
+
restartCommand: buildDirectCommand(
|
|
84
|
+
{
|
|
85
|
+
path: project.path,
|
|
86
|
+
resumeCommand: session.resumeCommand || null,
|
|
87
|
+
resumeTool: session.resumeTool || null,
|
|
88
|
+
resumeSessionId: session.resumeSessionId || null,
|
|
89
|
+
sessionId: session.resumeSessionId || null,
|
|
90
|
+
},
|
|
91
|
+
{ resume: true },
|
|
92
|
+
),
|
|
93
|
+
})),
|
|
94
|
+
})),
|
|
76
95
|
tabs: launchTabs.map((tab, index) => ({
|
|
77
96
|
index: index + 1,
|
|
78
97
|
title: tab.title,
|
|
@@ -116,6 +135,7 @@ export function summarizeWorkspaceTabs(report) {
|
|
|
116
135
|
}${report.machine.machineId ? ` [${report.machine.machineId}]` : ""}`,
|
|
117
136
|
]
|
|
118
137
|
: []),
|
|
138
|
+
`Saved projects: ${report.projectCount}`,
|
|
119
139
|
`Saved tabs: ${report.tabCount}`,
|
|
120
140
|
`Parse mode: ${report.parseMode}`,
|
|
121
141
|
"",
|
|
@@ -172,6 +192,7 @@ Options:
|
|
|
172
192
|
|
|
173
193
|
Notes:
|
|
174
194
|
- This shows the saved tab order plus any stored local path, remote repo, bootstrap command, and \`codex resume ...\` / \`claude --resume ...\` metadata.
|
|
195
|
+
- JSON output includes grouped \`projects[].sessions[]\` so duplicate project paths can be reviewed and sunset together.
|
|
175
196
|
- The human-readable \`resume:\` line is already copyable and includes the saved \`cd ... && resume ...\` recovery command.
|
|
176
197
|
- When a tab also has \`remote:\` or \`setup:\` lines, those are the portable cross-machine clues for cloning and preparing the repo on another rig.
|
|
177
198
|
- The selector can be \`main\`, \`offhand\`, a hosted idea id, a hosted workspace id, a local workspace id, or a saved workspace title/slug.
|
|
@@ -243,6 +243,40 @@ test("addTabToManifest asks for a title when multiple saved tabs share a path",
|
|
|
243
243
|
);
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
test("normalizeWorkspaceManifest expands grouped project sessions", () => {
|
|
247
|
+
const manifest = normalizeWorkspaceManifest({
|
|
248
|
+
version: "1",
|
|
249
|
+
workspaceId: "main-cody-1",
|
|
250
|
+
title: "main-cody-1",
|
|
251
|
+
projects: [
|
|
252
|
+
{
|
|
253
|
+
title: "orp",
|
|
254
|
+
path: "/Volumes/Code_2TB/code/orp",
|
|
255
|
+
remoteUrl: "git@github.com:SproutSeeds/orp.git",
|
|
256
|
+
sessions: [
|
|
257
|
+
{
|
|
258
|
+
title: "orp release",
|
|
259
|
+
resumeTool: "codex",
|
|
260
|
+
resumeSessionId: "019d32d3-d8b2-7fa2-aaec-c74b5134afd6",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
title: "orp docs",
|
|
264
|
+
resumeCommand: "claude --resume 469d99b2-2997-42bf-a8f5-3812c808ef29",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.equal(manifest.tabs.length, 2);
|
|
272
|
+
assert.equal(manifest.tabs[0]?.path, "/Volumes/Code_2TB/code/orp");
|
|
273
|
+
assert.equal(manifest.tabs[0]?.title, "orp release");
|
|
274
|
+
assert.equal(manifest.tabs[0]?.resumeTool, "codex");
|
|
275
|
+
assert.equal(manifest.tabs[1]?.title, "orp docs");
|
|
276
|
+
assert.equal(manifest.tabs[1]?.resumeTool, "claude");
|
|
277
|
+
assert.equal(manifest.tabs[1]?.sessionId, "469d99b2-2997-42bf-a8f5-3812c808ef29");
|
|
278
|
+
});
|
|
279
|
+
|
|
246
280
|
test("removeTabsFromManifest can target a saved tab by path and resume session id", () => {
|
|
247
281
|
const result = removeTabsFromManifest(sampleManifest(), {
|
|
248
282
|
path: "/Volumes/Code_2TB/code/frg-site",
|
|
@@ -328,6 +362,44 @@ test("runWorkspaceAddTab upserts an existing tab and returns the rendered recove
|
|
|
328
362
|
});
|
|
329
363
|
});
|
|
330
364
|
|
|
365
|
+
test("runWorkspaceAddTab stores same-project sessions under a grouped project object", async () => {
|
|
366
|
+
await withTempConfigHome(async () => {
|
|
367
|
+
const tempDir = await makeTempDir();
|
|
368
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
369
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(sampleManifest(), null, 2)}\n`, "utf8");
|
|
370
|
+
|
|
371
|
+
const { code, stdout } = await captureStdout(() =>
|
|
372
|
+
runWorkspaceAddTab([
|
|
373
|
+
"--workspace-file",
|
|
374
|
+
manifestPath,
|
|
375
|
+
"--path",
|
|
376
|
+
"/Volumes/Code_2TB/code/orp",
|
|
377
|
+
"--title",
|
|
378
|
+
"orp second session",
|
|
379
|
+
"--resume-tool",
|
|
380
|
+
"codex",
|
|
381
|
+
"--resume-session-id",
|
|
382
|
+
"019d32d3-d8b2-7fa2-aaec-c74b5134afd6",
|
|
383
|
+
"--append",
|
|
384
|
+
"--json",
|
|
385
|
+
]),
|
|
386
|
+
);
|
|
387
|
+
const payload = JSON.parse(stdout);
|
|
388
|
+
const saved = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
389
|
+
|
|
390
|
+
assert.equal(code, 0);
|
|
391
|
+
assert.equal(payload.action, "add-tab");
|
|
392
|
+
assert.equal(payload.mutation, "added");
|
|
393
|
+
assert.equal(payload.tabCount, 3);
|
|
394
|
+
const orpProject = saved.projects.find((project) => project.path === "/Volumes/Code_2TB/code/orp");
|
|
395
|
+
assert.ok(orpProject);
|
|
396
|
+
assert.equal(orpProject.sessionCount, 2);
|
|
397
|
+
assert.equal(orpProject.sessions[1]?.title, "orp second session");
|
|
398
|
+
assert.equal(orpProject.sessions[1]?.resumeCommand, "codex resume 019d32d3-d8b2-7fa2-aaec-c74b5134afd6");
|
|
399
|
+
assert.equal(saved.tabs.length, 3);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
331
403
|
test("runWorkspaceRemoveTab updates a local workspace manifest file", async () => {
|
|
332
404
|
await withTempConfigHome(async () => {
|
|
333
405
|
const tempDir = await makeTempDir();
|
|
@@ -93,6 +93,10 @@ test("buildWorkspaceTabsReport keeps duplicate titles unique and exposes generic
|
|
|
93
93
|
assert.equal(report.workspaceId, "workspace-idea");
|
|
94
94
|
assert.equal(report.machine?.machineLabel, "Mac Studio");
|
|
95
95
|
assert.equal(report.tabCount, 3);
|
|
96
|
+
assert.equal(report.projectCount, 2);
|
|
97
|
+
assert.equal(report.projects[0]?.path, "/Volumes/Code_2TB/code/collaboration");
|
|
98
|
+
assert.equal(report.projects[0]?.sessionCount, 2);
|
|
99
|
+
assert.equal(report.projects[0]?.sessions[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration' && codex resume abc-123");
|
|
96
100
|
assert.equal(report.tabs[0]?.title, "collaboration");
|
|
97
101
|
assert.equal(report.tabs[0]?.remoteUrl, "git@github.com:org/collaboration.git");
|
|
98
102
|
assert.equal(report.tabs[0]?.bootstrapCommand, "npm install");
|
|
@@ -163,6 +167,8 @@ test("runWorkspaceTabs prints JSON without launch commands", async () => {
|
|
|
163
167
|
assert.equal(parsed.workspaceId, "orp-main");
|
|
164
168
|
assert.equal(parsed.machine.machineLabel, "Mac Studio");
|
|
165
169
|
assert.equal(parsed.tabCount, 2);
|
|
170
|
+
assert.equal(parsed.projectCount, 2);
|
|
171
|
+
assert.equal(parsed.projects[0]?.sessions[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/orp' && claude resume claude-999");
|
|
166
172
|
assert.equal(parsed.tabs[0]?.title, "orp");
|
|
167
173
|
assert.equal(parsed.tabs[0]?.remoteUrl, "git@github.com:SproutSeeds/orp.git");
|
|
168
174
|
assert.equal(parsed.tabs[0]?.bootstrapCommand, "npm install");
|
|
@@ -26,6 +26,7 @@ ARTIFACT_CLASSES = [
|
|
|
26
26
|
"policy",
|
|
27
27
|
"result",
|
|
28
28
|
]
|
|
29
|
+
QUICK_PERFORMANCE_TARGET_MULTIPLIER = 1.5
|
|
29
30
|
VALID_REQUIREMENT_FIXTURES: dict[str, dict[str, Any]] = {
|
|
30
31
|
"task": {
|
|
31
32
|
"schema_version": "1.0.0",
|
|
@@ -507,6 +508,54 @@ def _benchmark_cross_domain_corpus() -> dict[str, Any]:
|
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
|
|
511
|
+
def _relax_mean_target(benchmark: dict[str, Any], *, observed_key: str, target_key: str, meets_key: str) -> None:
|
|
512
|
+
targets = benchmark.get("targets")
|
|
513
|
+
observed = benchmark.get("observed")
|
|
514
|
+
meets_targets = benchmark.get("meets_targets")
|
|
515
|
+
if not isinstance(targets, dict) or not isinstance(observed, dict) or not isinstance(meets_targets, dict):
|
|
516
|
+
return
|
|
517
|
+
current_target = targets.get(target_key)
|
|
518
|
+
current_observed = (observed.get(observed_key) or {}).get("mean_ms")
|
|
519
|
+
if not isinstance(current_target, (int, float)) or not isinstance(current_observed, (int, float)):
|
|
520
|
+
return
|
|
521
|
+
next_target = round(float(current_target) * QUICK_PERFORMANCE_TARGET_MULTIPLIER, 3)
|
|
522
|
+
targets[target_key] = next_target
|
|
523
|
+
meets_targets[meets_key] = float(current_observed) < next_target
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _apply_quick_performance_targets(
|
|
527
|
+
*,
|
|
528
|
+
init_benchmark: dict[str, Any],
|
|
529
|
+
roundtrip_benchmark: dict[str, Any],
|
|
530
|
+
corpus_benchmark: dict[str, Any],
|
|
531
|
+
requirement_benchmark: dict[str, Any],
|
|
532
|
+
mutation_stress: dict[str, Any],
|
|
533
|
+
) -> None:
|
|
534
|
+
_relax_mean_target(init_benchmark, observed_key="init", target_key="init_mean_lt_ms", meets_key="init")
|
|
535
|
+
_relax_mean_target(init_benchmark, observed_key="validate", target_key="validate_mean_lt_ms", meets_key="validate")
|
|
536
|
+
_relax_mean_target(init_benchmark, observed_key="gate_run", target_key="gate_mean_lt_ms", meets_key="gate_run")
|
|
537
|
+
_relax_mean_target(
|
|
538
|
+
roundtrip_benchmark,
|
|
539
|
+
observed_key="scaffold",
|
|
540
|
+
target_key="scaffold_mean_lt_ms",
|
|
541
|
+
meets_key="scaffold",
|
|
542
|
+
)
|
|
543
|
+
_relax_mean_target(
|
|
544
|
+
roundtrip_benchmark,
|
|
545
|
+
observed_key="validate",
|
|
546
|
+
target_key="validate_mean_lt_ms",
|
|
547
|
+
meets_key="validate",
|
|
548
|
+
)
|
|
549
|
+
_relax_mean_target(corpus_benchmark, observed_key="validate", target_key="validate_mean_lt_ms", meets_key="validate")
|
|
550
|
+
_relax_mean_target(
|
|
551
|
+
requirement_benchmark,
|
|
552
|
+
observed_key="validate",
|
|
553
|
+
target_key="validate_mean_lt_ms",
|
|
554
|
+
meets_key="validate",
|
|
555
|
+
)
|
|
556
|
+
_relax_mean_target(mutation_stress, observed_key="validate", target_key="validate_mean_lt_ms", meets_key="validate")
|
|
557
|
+
|
|
558
|
+
|
|
510
559
|
def _benchmark_requirement_enforcement() -> dict[str, Any]:
|
|
511
560
|
rows: list[dict[str, Any]] = []
|
|
512
561
|
validate_times: list[float] = []
|
|
@@ -747,7 +796,7 @@ def _gather_metadata() -> dict[str, Any]:
|
|
|
747
796
|
}
|
|
748
797
|
|
|
749
798
|
|
|
750
|
-
def build_report(iterations: int) -> dict[str, Any]:
|
|
799
|
+
def build_report(iterations: int, *, quick: bool = False) -> dict[str, Any]:
|
|
751
800
|
init_benchmark = _benchmark_init_starter(iterations)
|
|
752
801
|
roundtrip_benchmark = _benchmark_artifact_roundtrip()
|
|
753
802
|
gate_mode_benchmark = _benchmark_gate_modes()
|
|
@@ -756,6 +805,14 @@ def build_report(iterations: int) -> dict[str, Any]:
|
|
|
756
805
|
requirement_benchmark = _benchmark_requirement_enforcement()
|
|
757
806
|
representation_invariance = _benchmark_representation_invariance()
|
|
758
807
|
mutation_stress = _benchmark_mutation_stress()
|
|
808
|
+
if quick:
|
|
809
|
+
_apply_quick_performance_targets(
|
|
810
|
+
init_benchmark=init_benchmark,
|
|
811
|
+
roundtrip_benchmark=roundtrip_benchmark,
|
|
812
|
+
corpus_benchmark=corpus_benchmark,
|
|
813
|
+
requirement_benchmark=requirement_benchmark,
|
|
814
|
+
mutation_stress=mutation_stress,
|
|
815
|
+
)
|
|
759
816
|
|
|
760
817
|
claims = [
|
|
761
818
|
{
|
|
@@ -901,11 +958,15 @@ def main() -> int:
|
|
|
901
958
|
parser = argparse.ArgumentParser(description="Benchmark and validate ORP Reasoning Kernel v0.1")
|
|
902
959
|
parser.add_argument("--out", default="", help="Optional JSON output path")
|
|
903
960
|
parser.add_argument("--iterations", type=int, default=5, help="Iterations for bootstrap benchmark")
|
|
904
|
-
parser.add_argument(
|
|
961
|
+
parser.add_argument(
|
|
962
|
+
"--quick",
|
|
963
|
+
action="store_true",
|
|
964
|
+
help="Use a single bootstrap iteration and relaxed local timing targets for fast smoke checks",
|
|
965
|
+
)
|
|
905
966
|
args = parser.parse_args()
|
|
906
967
|
|
|
907
968
|
iterations = 1 if args.quick else max(1, args.iterations)
|
|
908
|
-
report = build_report(iterations)
|
|
969
|
+
report = build_report(iterations, quick=args.quick)
|
|
909
970
|
payload = json.dumps(report, indent=2) + "\n"
|
|
910
971
|
if args.out:
|
|
911
972
|
out_path = Path(args.out)
|