open-research-protocol 0.4.17 → 0.4.19
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/README.md +6 -1
- package/cli/orp.py +67 -27
- package/docs/PROFILE_PACKS.md +8 -0
- package/package.json +1 -1
- package/packages/orp-workspace-launcher/src/ledger.js +173 -41
- package/packages/orp-workspace-launcher/src/orp-command.js +14 -14
- package/packages/orp-workspace-launcher/test/ledger.test.js +119 -0
- package/packages/orp-workspace-launcher/test/orp-command.test.js +6 -0
- package/packs/erdos-open-problems/pack.yml +54 -0
- package/packs/external-pr-governance/pack.yml +17 -0
- package/packs/issue-smashers/pack.yml +25 -0
- package/scripts/orp-pack-install.py +90 -11
- package/spec/v1/profile-pack.schema.json +48 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ORP — Open Research Protocol
|
|
2
2
|
|
|
3
|
-
Maintained by Fractal Research Group (
|
|
3
|
+
Maintained by SproutSeeds. Research stewardship: Fractal Research Group ([frg.earth](https://frg.earth)).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/open-research-protocol)
|
|
6
6
|
[](https://www.npmjs.com/package/open-research-protocol)
|
|
@@ -443,6 +443,11 @@ If you are using ORP normally, prefer:
|
|
|
443
443
|
Reach for `orp pack ...` when you are doing advanced installs, ORP maintenance,
|
|
444
444
|
or direct domain-template work.
|
|
445
445
|
|
|
446
|
+
Pack installation is pack-owned: `pack.yml` can describe installable
|
|
447
|
+
components, default includes, dependency checks, and report naming. That lets
|
|
448
|
+
ORP consume repo-owned external packs through `--pack-path` without baking
|
|
449
|
+
domain-specific install rules into ORP core.
|
|
450
|
+
|
|
446
451
|
Install pack configs into a target repo (recommended):
|
|
447
452
|
|
|
448
453
|
```bash
|
package/cli/orp.py
CHANGED
|
@@ -8940,7 +8940,7 @@ def _about_payload() -> dict[str, Any]:
|
|
|
8940
8940
|
"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.",
|
|
8941
8941
|
"Agent modes are lightweight optional overlays for taste, perspective shifts, and fresh movement; `orp mode nudge sleek-minimal-progressive --json` gives agents a deterministic reminder they can call on when they want a deeper, wider, top-down, or rotated lens without changing ORP's core artifact boundaries.",
|
|
8942
8942
|
"Project/session linking is a built-in ORP ability exposed through `orp link ...` and stored machine-locally under `.git/orp/link/`.",
|
|
8943
|
-
"
|
|
8943
|
+
"Secrets are easiest to understand as saved keys and tokens: humans usually run `orp secrets add ...` and paste the value at the prompt, agents usually pipe the value with `--value-stdin`, and local macOS Keychain caching plus hosted sync are optional layers on top.",
|
|
8944
8944
|
"Machine runner identity, heartbeat, hosted sync, prompt-job execution, and lease control are built into ORP through `orp runner status`, `orp runner enable`, `orp runner disable`, `orp runner heartbeat`, `orp runner sync`, `orp runner work`, `orp runner cancel`, and `orp runner retry`.",
|
|
8945
8945
|
"Repo governance is built into ORP through `orp init`, `orp status`, `orp branch start`, `orp checkpoint create`, `orp backup`, `orp ready`, `orp doctor`, and `orp cleanup`.",
|
|
8946
8946
|
"Hosted workspace operations are built directly into ORP under `orp workspaces ...`, plus the linked auth/ideas/feature/world/checkpoint/agent surfaces.",
|
|
@@ -9046,11 +9046,11 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
9046
9046
|
"command": "orp workspace list",
|
|
9047
9047
|
},
|
|
9048
9048
|
{
|
|
9049
|
-
"label": "
|
|
9049
|
+
"label": "Inspect saved paths and exact recovery commands for the main workspace",
|
|
9050
9050
|
"command": "orp workspace tabs main",
|
|
9051
9051
|
},
|
|
9052
9052
|
{
|
|
9053
|
-
"label": "
|
|
9053
|
+
"label": "Save a new API key or token interactively when you need one",
|
|
9054
9054
|
"command": 'orp secrets add --alias <alias> --label "<label>" --provider <provider>',
|
|
9055
9055
|
},
|
|
9056
9056
|
{
|
|
@@ -9076,10 +9076,6 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
9076
9076
|
"label": "Inspect the saved workspace ledger inventory",
|
|
9077
9077
|
"command": "orp workspace list",
|
|
9078
9078
|
},
|
|
9079
|
-
{
|
|
9080
|
-
"label": "Print exact copyable crash-recovery commands for the main workspace",
|
|
9081
|
-
"command": "orp workspace tabs main",
|
|
9082
|
-
},
|
|
9083
9079
|
{
|
|
9084
9080
|
"label": "Inspect the saved tabs in the main workspace ledger",
|
|
9085
9081
|
"command": "orp workspace tabs main",
|
|
@@ -9113,11 +9109,11 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
9113
9109
|
"command": "orp workspaces list --json",
|
|
9114
9110
|
},
|
|
9115
9111
|
{
|
|
9116
|
-
"label": "Inspect
|
|
9112
|
+
"label": "Inspect saved keys and tokens already known to ORP",
|
|
9117
9113
|
"command": "orp secrets list --json",
|
|
9118
9114
|
},
|
|
9119
9115
|
{
|
|
9120
|
-
"label": "Reuse a saved
|
|
9116
|
+
"label": "Reuse a saved key or prompt for it and save it for this project",
|
|
9121
9117
|
"command": "orp secrets ensure --alias <alias> --provider <provider> --current-project --json",
|
|
9122
9118
|
},
|
|
9123
9119
|
{
|
|
@@ -9408,11 +9404,11 @@ def _home_payload(repo_root: Path, config_arg: str) -> dict[str, Any]:
|
|
|
9408
9404
|
},
|
|
9409
9405
|
{
|
|
9410
9406
|
"id": "secrets",
|
|
9411
|
-
"description": "Saved API keys and tokens, with
|
|
9407
|
+
"description": "Saved API keys and tokens, with an interactive human flow, a stdin agent flow, optional local macOS Keychain caching, and optional hosted sync.",
|
|
9412
9408
|
"entrypoints": [
|
|
9413
9409
|
"orp secrets list --json",
|
|
9414
9410
|
"orp secrets show <alias-or-id> --json",
|
|
9415
|
-
|
|
9411
|
+
'orp secrets add --alias <alias> --label "<label>" --provider <provider>',
|
|
9416
9412
|
"orp secrets ensure --alias <alias> --provider <provider> --current-project --json",
|
|
9417
9413
|
"orp secrets keychain-list --json",
|
|
9418
9414
|
"orp secrets keychain-show <alias-or-id> --json",
|
|
@@ -9614,14 +9610,34 @@ def _render_home_screen(payload: dict[str, Any]) -> str:
|
|
|
9614
9610
|
lines.append("")
|
|
9615
9611
|
lines.append("Command Families")
|
|
9616
9612
|
if isinstance(abilities, list) and abilities:
|
|
9617
|
-
|
|
9613
|
+
ability_map = {
|
|
9614
|
+
str(row.get("id", "")).strip(): row
|
|
9615
|
+
for row in abilities
|
|
9616
|
+
if isinstance(row, dict) and str(row.get("id", "")).strip()
|
|
9617
|
+
}
|
|
9618
|
+
visible_ability_ids = [
|
|
9619
|
+
"workspace",
|
|
9620
|
+
"secrets",
|
|
9621
|
+
"governance",
|
|
9622
|
+
"frontier",
|
|
9623
|
+
"schedule",
|
|
9624
|
+
"modes",
|
|
9625
|
+
"hosted",
|
|
9626
|
+
"discover",
|
|
9627
|
+
]
|
|
9628
|
+
shown = 0
|
|
9629
|
+
for ability_id in visible_ability_ids:
|
|
9630
|
+
row = ability_map.get(ability_id)
|
|
9618
9631
|
if not isinstance(row, dict):
|
|
9619
9632
|
continue
|
|
9620
|
-
ability_id = str(row.get("id", "")).strip()
|
|
9621
9633
|
desc = _truncate(str(row.get("description", "")).strip())
|
|
9622
9634
|
lines.append(f" - {ability_id}")
|
|
9623
9635
|
if desc:
|
|
9624
9636
|
lines.append(f" {desc}")
|
|
9637
|
+
shown += 1
|
|
9638
|
+
remaining = max(len(ability_map) - shown, 0)
|
|
9639
|
+
if remaining:
|
|
9640
|
+
lines.append(f" - ... and {remaining} more in `orp about --json`")
|
|
9625
9641
|
|
|
9626
9642
|
lines.append("")
|
|
9627
9643
|
lines.append("Collaboration")
|
|
@@ -9652,7 +9668,7 @@ def _render_home_screen(payload: dict[str, Any]) -> str:
|
|
|
9652
9668
|
lines.append("")
|
|
9653
9669
|
lines.append("Quick Actions")
|
|
9654
9670
|
if isinstance(quick_actions, list):
|
|
9655
|
-
for row in quick_actions[:
|
|
9671
|
+
for row in quick_actions[:10]:
|
|
9656
9672
|
if not isinstance(row, dict):
|
|
9657
9673
|
continue
|
|
9658
9674
|
label = str(row.get("label", "")).strip()
|
|
@@ -9661,7 +9677,7 @@ def _render_home_screen(payload: dict[str, Any]) -> str:
|
|
|
9661
9677
|
continue
|
|
9662
9678
|
lines.append(f" - {label}")
|
|
9663
9679
|
lines.append(f" {command}")
|
|
9664
|
-
remaining = max(len(quick_actions) -
|
|
9680
|
+
remaining = max(len(quick_actions) - 10, 0)
|
|
9665
9681
|
if remaining:
|
|
9666
9682
|
lines.append(f" - ... and {remaining} more in `orp home --json`")
|
|
9667
9683
|
|
|
@@ -18544,10 +18560,31 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18544
18560
|
add_json_flag(s_youtube_inspect)
|
|
18545
18561
|
s_youtube_inspect.set_defaults(func=cmd_youtube_inspect, json_output=False)
|
|
18546
18562
|
|
|
18547
|
-
s_secrets = sub.add_parser(
|
|
18563
|
+
s_secrets = sub.add_parser(
|
|
18564
|
+
"secrets",
|
|
18565
|
+
help="Save and reuse API keys and tokens locally, with optional hosted sync",
|
|
18566
|
+
description=(
|
|
18567
|
+
"ORP secrets are easiest to understand as saved keys and tokens.\n\n"
|
|
18568
|
+
"Human flow:\n"
|
|
18569
|
+
" 1. Run `orp secrets add ...`\n"
|
|
18570
|
+
" 2. Paste the value when ORP prompts `Secret value:`\n"
|
|
18571
|
+
" 3. Later run `orp secrets list` or `orp secrets resolve ...`\n\n"
|
|
18572
|
+
"Agent flow:\n"
|
|
18573
|
+
" - Pipe the value with `--value-stdin` instead of typing it interactively.\n\n"
|
|
18574
|
+
"Local macOS Keychain caching and hosted sync are optional layers on top."
|
|
18575
|
+
),
|
|
18576
|
+
epilog=(
|
|
18577
|
+
"Examples:\n"
|
|
18578
|
+
" orp secrets add --alias openai-primary --label \"OpenAI Primary\" --provider openai\n"
|
|
18579
|
+
" printf '%s' 'sk-...' | orp secrets add --alias openai-primary --label \"OpenAI Primary\" --provider openai --value-stdin\n"
|
|
18580
|
+
" orp secrets list\n"
|
|
18581
|
+
" orp secrets resolve openai-primary --reveal"
|
|
18582
|
+
),
|
|
18583
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
18584
|
+
)
|
|
18548
18585
|
secrets_sub = s_secrets.add_subparsers(dest="secrets_cmd", required=True)
|
|
18549
18586
|
|
|
18550
|
-
s_secrets_list = secrets_sub.add_parser("list", help="List
|
|
18587
|
+
s_secrets_list = secrets_sub.add_parser("list", help="List saved secrets known to ORP")
|
|
18551
18588
|
s_secrets_list.add_argument("--provider", default="", help="Optional provider filter")
|
|
18552
18589
|
add_secret_scope_flags(s_secrets_list)
|
|
18553
18590
|
s_secrets_list.add_argument(
|
|
@@ -18559,13 +18596,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18559
18596
|
add_json_flag(s_secrets_list)
|
|
18560
18597
|
s_secrets_list.set_defaults(func=cmd_secrets_list, json_output=False)
|
|
18561
18598
|
|
|
18562
|
-
s_secrets_show = secrets_sub.add_parser("show", help="Show one
|
|
18599
|
+
s_secrets_show = secrets_sub.add_parser("show", help="Show one saved secret by alias or id")
|
|
18563
18600
|
s_secrets_show.add_argument("secret_ref", help="Secret alias or id")
|
|
18564
18601
|
add_base_url_flag(s_secrets_show)
|
|
18565
18602
|
add_json_flag(s_secrets_show)
|
|
18566
18603
|
s_secrets_show.set_defaults(func=cmd_secrets_show, json_output=False)
|
|
18567
18604
|
|
|
18568
|
-
s_secrets_add = secrets_sub.add_parser(
|
|
18605
|
+
s_secrets_add = secrets_sub.add_parser(
|
|
18606
|
+
"add",
|
|
18607
|
+
help="Save a new secret; ORP prompts for the value unless you pass --value-stdin",
|
|
18608
|
+
)
|
|
18569
18609
|
s_secrets_add.add_argument("--alias", required=True, help="Stable secret alias")
|
|
18570
18610
|
s_secrets_add.add_argument("--label", required=True, help="Human label for the secret")
|
|
18571
18611
|
s_secrets_add.add_argument("--provider", required=True, help="Provider slug, for example openai")
|
|
@@ -18596,7 +18636,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18596
18636
|
|
|
18597
18637
|
s_secrets_ensure = secrets_sub.add_parser(
|
|
18598
18638
|
"ensure",
|
|
18599
|
-
help="
|
|
18639
|
+
help="Reuse a saved secret or prompt for it and save it when missing",
|
|
18600
18640
|
)
|
|
18601
18641
|
s_secrets_ensure.add_argument("--alias", required=True, help="Stable secret alias")
|
|
18602
18642
|
s_secrets_ensure.add_argument("--label", default="", help="Human label for create-if-missing flows")
|
|
@@ -18637,7 +18677,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18637
18677
|
|
|
18638
18678
|
s_secrets_keychain_list = secrets_sub.add_parser(
|
|
18639
18679
|
"keychain-list",
|
|
18640
|
-
help="List
|
|
18680
|
+
help="List local macOS Keychain copies known to ORP on this machine",
|
|
18641
18681
|
)
|
|
18642
18682
|
s_secrets_keychain_list.add_argument("--provider", default="", help="Optional provider filter")
|
|
18643
18683
|
add_secret_scope_flags(s_secrets_keychain_list)
|
|
@@ -18646,7 +18686,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18646
18686
|
|
|
18647
18687
|
s_secrets_keychain_show = secrets_sub.add_parser(
|
|
18648
18688
|
"keychain-show",
|
|
18649
|
-
help="Show one
|
|
18689
|
+
help="Show one local macOS Keychain copy by alias or id",
|
|
18650
18690
|
)
|
|
18651
18691
|
s_secrets_keychain_show.add_argument("secret_ref", help="Secret alias or id")
|
|
18652
18692
|
s_secrets_keychain_show.add_argument(
|
|
@@ -18659,7 +18699,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18659
18699
|
|
|
18660
18700
|
s_secrets_sync_keychain = secrets_sub.add_parser(
|
|
18661
18701
|
"sync-keychain",
|
|
18662
|
-
help="
|
|
18702
|
+
help="Copy one saved secret into the local macOS Keychain",
|
|
18663
18703
|
)
|
|
18664
18704
|
s_secrets_sync_keychain.add_argument("secret_ref", nargs="?", default="", help="Optional secret alias or id")
|
|
18665
18705
|
s_secrets_sync_keychain.add_argument("--provider", default="", help="Provider slug for project-scoped sync")
|
|
@@ -18673,7 +18713,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18673
18713
|
add_json_flag(s_secrets_sync_keychain)
|
|
18674
18714
|
s_secrets_sync_keychain.set_defaults(func=cmd_secrets_sync_keychain, json_output=False)
|
|
18675
18715
|
|
|
18676
|
-
s_secrets_update = secrets_sub.add_parser("update", help="Update one
|
|
18716
|
+
s_secrets_update = secrets_sub.add_parser("update", help="Update one saved secret")
|
|
18677
18717
|
s_secrets_update.add_argument("secret_ref", help="Secret alias or id")
|
|
18678
18718
|
s_secrets_update.add_argument("--alias", default=None, help="New alias")
|
|
18679
18719
|
s_secrets_update.add_argument("--label", default=None, help="New label")
|
|
@@ -18702,13 +18742,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18702
18742
|
add_json_flag(s_secrets_update)
|
|
18703
18743
|
s_secrets_update.set_defaults(func=cmd_secrets_update, json_output=False)
|
|
18704
18744
|
|
|
18705
|
-
s_secrets_archive = secrets_sub.add_parser("archive", help="Archive one
|
|
18745
|
+
s_secrets_archive = secrets_sub.add_parser("archive", help="Archive one saved secret")
|
|
18706
18746
|
s_secrets_archive.add_argument("secret_ref", help="Secret alias or id")
|
|
18707
18747
|
add_base_url_flag(s_secrets_archive)
|
|
18708
18748
|
add_json_flag(s_secrets_archive)
|
|
18709
18749
|
s_secrets_archive.set_defaults(func=cmd_secrets_archive, json_output=False)
|
|
18710
18750
|
|
|
18711
|
-
s_secrets_bind = secrets_sub.add_parser("bind", help="Bind one secret to a hosted project/world")
|
|
18751
|
+
s_secrets_bind = secrets_sub.add_parser("bind", help="Bind one saved secret to a hosted project/world")
|
|
18712
18752
|
s_secrets_bind.add_argument("secret_ref", help="Secret alias or id")
|
|
18713
18753
|
add_secret_scope_flags(s_secrets_bind)
|
|
18714
18754
|
s_secrets_bind.add_argument("--purpose", default="", help="Optional project usage note")
|
|
@@ -18729,7 +18769,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
18729
18769
|
|
|
18730
18770
|
s_secrets_resolve = secrets_sub.add_parser(
|
|
18731
18771
|
"resolve",
|
|
18732
|
-
help="Resolve one
|
|
18772
|
+
help="Resolve one saved secret by alias/id or by provider plus project scope",
|
|
18733
18773
|
)
|
|
18734
18774
|
s_secrets_resolve.add_argument("secret_ref", nargs="?", default="", help="Optional secret alias or id")
|
|
18735
18775
|
s_secrets_resolve.add_argument("--provider", default="", help="Provider slug for project-scoped resolution")
|
package/docs/PROFILE_PACKS.md
CHANGED
|
@@ -39,6 +39,7 @@ Canonical fields:
|
|
|
39
39
|
- `pack_id`: stable id
|
|
40
40
|
- `name`, `version`, `description`
|
|
41
41
|
- `orp_version_min`: optional compatibility floor
|
|
42
|
+
- `install`: optional install contract for `orp pack install`
|
|
42
43
|
- `variables`: render-time variables (for example `TARGET_REPO_ROOT`)
|
|
43
44
|
- `templates`: available config templates
|
|
44
45
|
|
|
@@ -80,6 +81,10 @@ orp pack fetch \
|
|
|
80
81
|
--install-target /path/to/repo
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
Repo-owned local packs can also be installed directly with `--pack-path` as
|
|
85
|
+
long as their `pack.yml` includes an `install` block describing components,
|
|
86
|
+
default includes, and dependency-audit paths.
|
|
87
|
+
|
|
83
88
|
This installs rendered config files and writes a dependency audit report:
|
|
84
89
|
|
|
85
90
|
- `./orp.erdos-catalog-sync.yml`
|
|
@@ -218,6 +223,9 @@ orp erdos sync --problem-id 857 --problem-id 20
|
|
|
218
223
|
- Packs can live in this repo (`packs/`) or external repos.
|
|
219
224
|
- Users can copy/install packs without changing ORP core.
|
|
220
225
|
- Version packs independently (for example `0.1.0`, `0.2.0`).
|
|
226
|
+
- Repo-owned packs can ship their own install metadata in `pack.yml`, so ORP
|
|
227
|
+
can install external domain packs without hardcoding those domains into ORP
|
|
228
|
+
core.
|
|
221
229
|
|
|
222
230
|
## Quality guidance
|
|
223
231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-research-protocol",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
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>",
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
buildDirectCommand,
|
|
7
7
|
deriveBaseTitle,
|
|
8
8
|
normalizeWorkspaceManifest,
|
|
9
9
|
parseWorkspaceSource,
|
|
@@ -44,6 +44,17 @@ function validateAbsolutePath(value, label) {
|
|
|
44
44
|
return normalized;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function resolveCurrentCodexResume() {
|
|
48
|
+
const sessionId = normalizeOptionalString(process.env.CODEX_THREAD_ID);
|
|
49
|
+
if (!sessionId) {
|
|
50
|
+
throw new Error("`--current-codex` requires `CODEX_THREAD_ID` in the current environment.");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
resumeTool: "codex",
|
|
54
|
+
resumeSessionId: sessionId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function serializeManifest(manifest) {
|
|
48
59
|
return `${JSON.stringify(materializeWorkspaceManifest(manifest), null, 2)}\n`;
|
|
49
60
|
}
|
|
@@ -63,6 +74,26 @@ function materializeWorkspaceTab(tab) {
|
|
|
63
74
|
);
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
function buildWorkspaceResultTab(tab) {
|
|
78
|
+
if (!tab) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const materialized = materializeWorkspaceTab(tab);
|
|
82
|
+
return {
|
|
83
|
+
...materialized,
|
|
84
|
+
restartCommand: buildDirectCommand(
|
|
85
|
+
{
|
|
86
|
+
path: materialized.path,
|
|
87
|
+
resumeCommand: materialized.resumeCommand || null,
|
|
88
|
+
resumeTool: materialized.resumeTool || null,
|
|
89
|
+
sessionId: materialized.resumeSessionId || null,
|
|
90
|
+
resumeSessionId: materialized.resumeSessionId || null,
|
|
91
|
+
},
|
|
92
|
+
{ resume: true },
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
function materializeWorkspaceManifest(manifest) {
|
|
67
98
|
const normalized = normalizeWorkspaceManifest(manifest);
|
|
68
99
|
return Object.fromEntries(
|
|
@@ -122,10 +153,16 @@ function normalizeEditableManifest(source, parsed) {
|
|
|
122
153
|
return normalizeWorkspaceManifest(baseManifest);
|
|
123
154
|
}
|
|
124
155
|
|
|
125
|
-
function parseLedgerSelectorArgs(
|
|
156
|
+
function parseLedgerSelectorArgs(
|
|
157
|
+
argv = [],
|
|
158
|
+
{ commandName, requirePath = false, requireSelector = true, allowAppend = false, allowHere = false, allowCurrentCodex = false } = {},
|
|
159
|
+
) {
|
|
126
160
|
const options = {
|
|
127
161
|
json: false,
|
|
128
162
|
all: false,
|
|
163
|
+
append: false,
|
|
164
|
+
here: false,
|
|
165
|
+
currentCodex: false,
|
|
129
166
|
};
|
|
130
167
|
|
|
131
168
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -143,6 +180,18 @@ function parseLedgerSelectorArgs(argv = [], { commandName, requirePath = false,
|
|
|
143
180
|
options.all = true;
|
|
144
181
|
continue;
|
|
145
182
|
}
|
|
183
|
+
if (allowAppend && arg === "--append") {
|
|
184
|
+
options.append = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (allowHere && arg === "--here") {
|
|
188
|
+
options.here = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (allowCurrentCodex && arg === "--current-codex") {
|
|
192
|
+
options.currentCodex = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
146
195
|
if (arg.startsWith("--")) {
|
|
147
196
|
const next = argv[index + 1];
|
|
148
197
|
if (next == null || next.startsWith("--")) {
|
|
@@ -190,12 +239,24 @@ function parseLedgerSelectorArgs(argv = [], { commandName, requirePath = false,
|
|
|
190
239
|
if (requireSelector && !options.ideaId && !options.workspaceFile && !options.hostedWorkspaceId) {
|
|
191
240
|
throw new Error(`Provide a workspace selector for \`${commandName}\`.`);
|
|
192
241
|
}
|
|
193
|
-
if (
|
|
194
|
-
throw new Error(`--path
|
|
242
|
+
if (options.here && options.path) {
|
|
243
|
+
throw new Error("Use either `--path` or `--here`, not both.");
|
|
244
|
+
}
|
|
245
|
+
if (requirePath && !options.path && !options.here) {
|
|
246
|
+
throw new Error(`\`--path\` or \`--here\` is required for \`${commandName}\`.`);
|
|
247
|
+
}
|
|
248
|
+
if (options.here) {
|
|
249
|
+
options.path = path.resolve(process.cwd());
|
|
195
250
|
}
|
|
196
251
|
if (options.path) {
|
|
197
252
|
options.path = validateAbsolutePath(options.path, "--path");
|
|
198
253
|
}
|
|
254
|
+
if (options.currentCodex) {
|
|
255
|
+
if (options.resumeCommand || options.resumeTool || options.resumeSessionId) {
|
|
256
|
+
throw new Error("`--current-codex` cannot be combined with explicit resume metadata.");
|
|
257
|
+
}
|
|
258
|
+
Object.assign(options, resolveCurrentCodexResume());
|
|
259
|
+
}
|
|
199
260
|
if (options.index != null) {
|
|
200
261
|
const parsed = Number.parseInt(String(options.index), 10);
|
|
201
262
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
@@ -212,6 +273,9 @@ export function parseWorkspaceAddTabArgs(argv = []) {
|
|
|
212
273
|
commandName: "orp workspace add-tab",
|
|
213
274
|
requirePath: true,
|
|
214
275
|
requireSelector: true,
|
|
276
|
+
allowAppend: true,
|
|
277
|
+
allowHere: true,
|
|
278
|
+
allowCurrentCodex: true,
|
|
215
279
|
});
|
|
216
280
|
}
|
|
217
281
|
|
|
@@ -306,45 +370,107 @@ export function addTabToManifest(manifest, options = {}) {
|
|
|
306
370
|
...manifest,
|
|
307
371
|
tabs: manifest.tabs.map((tab) => ({ ...tab })),
|
|
308
372
|
});
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
Object.entries({
|
|
316
|
-
title: normalizeOptionalString(options.title) || undefined,
|
|
317
|
-
path: validateAbsolutePath(options.path, "--path"),
|
|
318
|
-
resumeCommand: resume.resumeCommand || undefined,
|
|
319
|
-
resumeTool: resume.resumeTool || undefined,
|
|
320
|
-
resumeSessionId: resume.resumeSessionId || undefined,
|
|
321
|
-
codexSessionId: resume.resumeTool === "codex" ? resume.resumeSessionId || undefined : undefined,
|
|
322
|
-
claudeSessionId: resume.resumeTool === "claude" ? resume.resumeSessionId || undefined : undefined,
|
|
323
|
-
}).filter(([, value]) => value !== undefined),
|
|
373
|
+
const normalizedPath = validateAbsolutePath(options.path, "--path");
|
|
374
|
+
const normalizedTitle = normalizeOptionalString(options.title);
|
|
375
|
+
const explicitResumeRequested = Boolean(
|
|
376
|
+
normalizeOptionalString(options.resumeCommand) ||
|
|
377
|
+
normalizeOptionalString(options.resumeTool) ||
|
|
378
|
+
normalizeOptionalString(options.resumeSessionId),
|
|
324
379
|
);
|
|
380
|
+
const requestedResume = explicitResumeRequested
|
|
381
|
+
? resolveResumeMetadata({
|
|
382
|
+
resumeCommand: options.resumeCommand,
|
|
383
|
+
resumeTool: options.resumeTool,
|
|
384
|
+
resumeSessionId: options.resumeSessionId,
|
|
385
|
+
})
|
|
386
|
+
: null;
|
|
387
|
+
|
|
388
|
+
const buildTab = (existingTab = null) => {
|
|
389
|
+
const existingResume = resolveResumeMetadata(existingTab || {});
|
|
390
|
+
const chosenResume = requestedResume || existingResume;
|
|
391
|
+
return Object.fromEntries(
|
|
392
|
+
Object.entries({
|
|
393
|
+
title: normalizedTitle || normalizeOptionalString(existingTab?.title) || undefined,
|
|
394
|
+
path: normalizedPath,
|
|
395
|
+
resumeCommand: chosenResume.resumeCommand || undefined,
|
|
396
|
+
resumeTool: chosenResume.resumeTool || undefined,
|
|
397
|
+
resumeSessionId: chosenResume.resumeSessionId || undefined,
|
|
398
|
+
codexSessionId: chosenResume.resumeTool === "codex" ? chosenResume.resumeSessionId || undefined : undefined,
|
|
399
|
+
claudeSessionId: chosenResume.resumeTool === "claude" ? chosenResume.resumeSessionId || undefined : undefined,
|
|
400
|
+
}).filter(([, value]) => value !== undefined),
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const nextTab = buildTab();
|
|
405
|
+
if (options.append) {
|
|
406
|
+
nextManifest.tabs.push(nextTab);
|
|
407
|
+
return {
|
|
408
|
+
manifest: normalizeWorkspaceManifest(nextManifest),
|
|
409
|
+
added: true,
|
|
410
|
+
updated: false,
|
|
411
|
+
unchanged: false,
|
|
412
|
+
mutation: "added",
|
|
413
|
+
tab: nextTab,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
325
416
|
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
417
|
+
const pathMatchIndexes = nextManifest.tabs
|
|
418
|
+
.map((tab, index) => (tab.path === normalizedPath ? index : -1))
|
|
419
|
+
.filter((index) => index >= 0);
|
|
420
|
+
let matchedIndexes = [];
|
|
421
|
+
|
|
422
|
+
if (normalizedTitle) {
|
|
423
|
+
matchedIndexes = pathMatchIndexes.filter(
|
|
424
|
+
(index) => normalizeOptionalString(nextManifest.tabs[index]?.title) === normalizedTitle,
|
|
332
425
|
);
|
|
333
|
-
|
|
426
|
+
if (matchedIndexes.length === 0 && pathMatchIndexes.length === 1) {
|
|
427
|
+
matchedIndexes = pathMatchIndexes;
|
|
428
|
+
}
|
|
429
|
+
} else if (pathMatchIndexes.length > 0) {
|
|
430
|
+
const uniqueTitles = new Set(
|
|
431
|
+
pathMatchIndexes.map((index) => normalizeOptionalString(nextManifest.tabs[index]?.title) || ""),
|
|
432
|
+
);
|
|
433
|
+
if (pathMatchIndexes.length === 1 || uniqueTitles.size <= 1) {
|
|
434
|
+
matchedIndexes = pathMatchIndexes;
|
|
435
|
+
} else {
|
|
436
|
+
throw new Error(
|
|
437
|
+
"Multiple saved tabs already use this path. Re-run with `--title` to target one tab or `--append` to add another.",
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
334
441
|
|
|
335
|
-
if (
|
|
442
|
+
if (matchedIndexes.length === 0) {
|
|
443
|
+
nextManifest.tabs.push(nextTab);
|
|
336
444
|
return {
|
|
337
|
-
manifest: nextManifest,
|
|
338
|
-
added:
|
|
339
|
-
|
|
445
|
+
manifest: normalizeWorkspaceManifest(nextManifest),
|
|
446
|
+
added: true,
|
|
447
|
+
updated: false,
|
|
448
|
+
unchanged: false,
|
|
449
|
+
mutation: "added",
|
|
450
|
+
tab: nextTab,
|
|
340
451
|
};
|
|
341
452
|
}
|
|
342
453
|
|
|
343
|
-
|
|
454
|
+
const [primaryIndex, ...duplicateIndexes] = matchedIndexes;
|
|
455
|
+
const currentTab = nextManifest.tabs[primaryIndex];
|
|
456
|
+
const updatedTab = buildTab(currentTab);
|
|
457
|
+
const currentMaterialized = JSON.stringify(materializeWorkspaceTab(currentTab));
|
|
458
|
+
const updatedMaterialized = JSON.stringify(materializeWorkspaceTab(updatedTab));
|
|
459
|
+
nextManifest.tabs[primaryIndex] = updatedTab;
|
|
460
|
+
|
|
461
|
+
if (duplicateIndexes.length > 0) {
|
|
462
|
+
const removalSet = new Set(duplicateIndexes);
|
|
463
|
+
nextManifest.tabs = nextManifest.tabs.filter((_, index) => !removalSet.has(index));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const changed = currentMaterialized !== updatedMaterialized || duplicateIndexes.length > 0;
|
|
344
467
|
return {
|
|
345
468
|
manifest: normalizeWorkspaceManifest(nextManifest),
|
|
346
|
-
added:
|
|
347
|
-
|
|
469
|
+
added: false,
|
|
470
|
+
updated: changed,
|
|
471
|
+
unchanged: !changed,
|
|
472
|
+
mutation: changed ? "updated" : "unchanged",
|
|
473
|
+
tab: updatedTab,
|
|
348
474
|
};
|
|
349
475
|
}
|
|
350
476
|
|
|
@@ -481,16 +607,19 @@ function printWorkspaceAddTabHelp() {
|
|
|
481
607
|
console.log(`ORP workspace add-tab
|
|
482
608
|
|
|
483
609
|
Usage:
|
|
484
|
-
orp workspace add-tab <name-or-id> --path <absolute-path> [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
|
|
485
|
-
orp workspace add-tab --hosted-workspace-id <workspace-id> --path <absolute-path> [--json]
|
|
486
|
-
orp workspace add-tab --workspace-file <path> --path <absolute-path> [--json]
|
|
610
|
+
orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
|
|
611
|
+
orp workspace add-tab --hosted-workspace-id <workspace-id> (--path <absolute-path> | --here) [--json]
|
|
612
|
+
orp workspace add-tab --workspace-file <path> (--path <absolute-path> | --here) [--json]
|
|
487
613
|
|
|
488
614
|
Options:
|
|
489
615
|
--path <absolute-path> Add this local project path to the saved workspace
|
|
616
|
+
--here Use the current working directory as the saved path
|
|
490
617
|
--title <title> Optional saved tab title
|
|
491
618
|
--resume-command <text> Exact saved resume command, like \`codex resume ...\` or \`claude --resume ...\`
|
|
492
619
|
--resume-tool <tool> Build the resume command from \`codex\` or \`claude\`
|
|
493
620
|
--resume-session-id <id> Resume session id to save with the tab
|
|
621
|
+
--current-codex Save the current \`CODEX_THREAD_ID\` as a Codex resume target
|
|
622
|
+
--append Always append a new saved tab instead of updating an existing matching tab
|
|
494
623
|
--hosted-workspace-id <id> Edit a first-class hosted workspace directly
|
|
495
624
|
--workspace-file <path> Edit a local structured workspace manifest
|
|
496
625
|
--json Print the updated workspace edit result as JSON
|
|
@@ -498,6 +627,7 @@ Options:
|
|
|
498
627
|
|
|
499
628
|
Examples:
|
|
500
629
|
orp workspace add-tab main --path /absolute/path/to/new-project
|
|
630
|
+
orp workspace add-tab main --here --current-codex
|
|
501
631
|
orp workspace add-tab main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
|
|
502
632
|
orp workspace add-tab main --path /absolute/path/to/new-project --resume-tool claude --resume-session-id claude-456
|
|
503
633
|
`);
|
|
@@ -560,14 +690,15 @@ function summarizeWorkspaceLedgerMutation(result) {
|
|
|
560
690
|
const lines = [
|
|
561
691
|
`Workspace: ${result.workspaceTitle || result.workspaceId || "workspace"}`,
|
|
562
692
|
`Action: ${result.action}`,
|
|
693
|
+
...(result.action === "add-tab" && result.mutation ? [`Result: ${result.mutation}`] : []),
|
|
563
694
|
`Saved tabs: ${result.tabCount}`,
|
|
564
695
|
];
|
|
565
696
|
|
|
566
697
|
if (result.action === "add-tab") {
|
|
567
|
-
lines.push(`
|
|
698
|
+
lines.push(`Tab: ${result.tab?.title || path.basename(result.tab?.path || "") || result.tab?.path}`);
|
|
568
699
|
lines.push(`Path: ${result.tab?.path}`);
|
|
569
|
-
if (result.tab?.resumeCommand) {
|
|
570
|
-
lines.push(`Resume: ${result.tab.
|
|
700
|
+
if (result.tab?.resumeCommand && result.tab?.restartCommand) {
|
|
701
|
+
lines.push(`Resume: ${result.tab.restartCommand}`);
|
|
571
702
|
}
|
|
572
703
|
} else if (result.action === "remove-tab") {
|
|
573
704
|
lines.push(`Removed: ${result.removedTabs.length}`);
|
|
@@ -603,9 +734,10 @@ async function runWorkspaceLedgerMutation(options, mutate, action) {
|
|
|
603
734
|
action,
|
|
604
735
|
workspaceId: finalManifest.workspaceId,
|
|
605
736
|
workspaceTitle: finalManifest.title || source.title || null,
|
|
737
|
+
mutation: mutated.mutation || null,
|
|
606
738
|
tabCount: finalManifest.tabs.length,
|
|
607
|
-
tab:
|
|
608
|
-
removedTabs: (mutated.removedTabs || []).map((tab) =>
|
|
739
|
+
tab: buildWorkspaceResultTab(mutated.tab),
|
|
740
|
+
removedTabs: (mutated.removedTabs || []).map((tab) => buildWorkspaceResultTab(tab)),
|
|
609
741
|
persistedTo: persisted.persistedTo,
|
|
610
742
|
ideaId: persisted.ideaId || null,
|
|
611
743
|
workspaceSourceId: persisted.workspaceId || null,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { runWorkspaceCommands } from "./commands.js";
|
|
2
1
|
import { runWorkspaceAddTab, runWorkspaceCreate, runWorkspaceRemoveTab } from "./ledger.js";
|
|
3
2
|
import { runWorkspaceList } from "./list.js";
|
|
4
3
|
import { runWorkspaceSlot } from "./slot.js";
|
|
@@ -10,36 +9,36 @@ function printWorkspaceHelp() {
|
|
|
10
9
|
|
|
11
10
|
Usage:
|
|
12
11
|
orp workspace create <title-slug> [--workspace-file <path>] [--slot <main|offhand>] [--path <absolute-path>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
|
|
13
|
-
orp workspace
|
|
14
|
-
orp workspace ledger add <name-or-id> --path <absolute-path> [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
|
|
15
|
-
orp workspace ledger remove <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
|
|
12
|
+
orp workspace list [--json]
|
|
16
13
|
orp workspace tabs <name-or-id> [--json]
|
|
17
14
|
orp workspace tabs --hosted-workspace-id <workspace-id> [--json]
|
|
18
15
|
orp workspace tabs --notes-file <path> [--json]
|
|
19
16
|
orp workspace tabs --workspace-file <path> [--json]
|
|
20
|
-
orp workspace add-tab <name-or-id> --path <absolute-path> [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id>] [--json]
|
|
17
|
+
orp workspace add-tab <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
|
|
21
18
|
orp workspace remove-tab <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
|
|
22
|
-
orp workspace list [--json]
|
|
23
19
|
orp workspace slot <list|set|clear> ...
|
|
24
20
|
orp workspace sync <name-or-id> [--workspace-file <path> | --notes-file <path>] [--dry-run] [--json]
|
|
21
|
+
orp workspace ledger <name-or-id> [--json]
|
|
22
|
+
orp workspace ledger add <name-or-id> (--path <absolute-path> | --here) [--title <title>] [--resume-command <text> | --resume-tool <codex|claude> --resume-session-id <id> | --current-codex] [--append] [--json]
|
|
23
|
+
orp workspace ledger remove <name-or-id> (--index <n> | --path <absolute-path> | --title <title> | --resume-session-id <id> | --resume-command <text>) [--all] [--json]
|
|
25
24
|
orp workspace -h
|
|
26
25
|
|
|
27
26
|
Commands:
|
|
28
27
|
create Create a local workspace ledger so ORP works without a hosted account
|
|
29
|
-
|
|
28
|
+
list List one merged inventory of hosted ORP workspaces and local manifests
|
|
30
29
|
tabs List the saved tabs inside a workspace with copyable resume/recovery lines
|
|
31
30
|
add-tab Add a saved tab/path/session to the workspace ledger directly
|
|
32
31
|
remove-tab Remove one or more saved tabs from the workspace ledger directly
|
|
33
|
-
list List one merged inventory of hosted ORP workspaces and local manifests
|
|
34
32
|
slot Assign and inspect named workspace slots like main and offhand
|
|
35
33
|
sync Post a CLI-authored workspace manifest back to the hosted ORP idea
|
|
34
|
+
ledger Compatibility alias for the same tabs/add/remove ledger flow
|
|
36
35
|
|
|
37
36
|
Notes:
|
|
38
|
-
- Local-only usage works: create a workspace with \`orp workspace create <title-slug>\`, then use \`orp workspace
|
|
39
|
-
- The ledger-first flow is: \`orp workspace ledger <workspace>\`, \`orp workspace ledger add ...\`, \`orp workspace ledger remove ...\`, and \`orp workspace tabs <workspace>\`.
|
|
37
|
+
- Local-only usage works: create a workspace with \`orp workspace create <title-slug>\`, then use \`orp workspace tabs ...\`, \`orp workspace add-tab ...\`, and \`orp workspace remove-tab ...\` without authenticating.
|
|
40
38
|
- Use \`orp workspace list\` for the combined hosted + local workspace inventory.
|
|
41
39
|
- Use \`orp workspace tabs <workspace>\` when you want saved paths plus copyable \`cd ... && codex resume ...\` / \`claude --resume ...\` recovery lines.
|
|
42
40
|
- Use \`orp workspace add-tab ...\` and \`orp workspace remove-tab ...\` when you want to edit the saved workspace ledger explicitly from Terminal.app or any other shell.
|
|
41
|
+
- If you prefer the older ledger-prefixed wording, \`orp workspace ledger\`, \`orp workspace ledger add\`, and \`orp workspace ledger remove\` stay available as aliases.
|
|
43
42
|
- \`main\` and \`offhand\` are reserved slot selectors; use \`orp workspace slot set ...\` to assign them.
|
|
44
43
|
- Syncing or editing a hosted workspace writes a managed local cache on this Mac.
|
|
45
44
|
- \`<name-or-id>\` can be a saved workspace title, workspace id, idea id, or local tracked workspace title/id.
|
|
@@ -47,17 +46,18 @@ Notes:
|
|
|
47
46
|
Examples:
|
|
48
47
|
orp workspace create main-cody-1
|
|
49
48
|
orp workspace create main-cody-1 --slot main
|
|
50
|
-
orp workspace
|
|
51
|
-
orp workspace ledger add main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
|
|
52
|
-
orp workspace ledger remove main --title frg-site
|
|
49
|
+
orp workspace list
|
|
53
50
|
orp workspace tabs main-cody-1
|
|
51
|
+
orp workspace add-tab main --here --current-codex
|
|
54
52
|
orp workspace add-tab main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
|
|
55
53
|
orp workspace remove-tab main --path /absolute/path/to/frg-site --resume-session-id 019d348d-5031-78e1-9840-a66deaac33ae
|
|
56
54
|
orp workspace slot set main main-cody-1
|
|
57
55
|
orp workspace slot set offhand research-lab
|
|
58
56
|
orp workspace slot list
|
|
57
|
+
orp workspace ledger main
|
|
58
|
+
orp workspace ledger add main --path /absolute/path/to/new-project --resume-command "codex resume 019d..."
|
|
59
|
+
orp workspace ledger remove main --title frg-site
|
|
59
60
|
orp workspace tabs --hosted-workspace-id ws_orp_main
|
|
60
|
-
orp workspace list
|
|
61
61
|
orp workspace sync main --workspace-file ./workspace.json
|
|
62
62
|
`);
|
|
63
63
|
}
|
|
@@ -97,6 +97,32 @@ test("parseWorkspaceAddTabArgs accepts explicit resume metadata", () => {
|
|
|
97
97
|
assert.equal(parsed.json, true);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
test("parseWorkspaceAddTabArgs resolves --here and --current-codex", () => {
|
|
101
|
+
const originalThreadId = process.env.CODEX_THREAD_ID;
|
|
102
|
+
const originalCwd = process.cwd();
|
|
103
|
+
process.env.CODEX_THREAD_ID = "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9";
|
|
104
|
+
process.chdir("/Volumes/Code_2TB/code/orp");
|
|
105
|
+
try {
|
|
106
|
+
const parsed = parseWorkspaceAddTabArgs([
|
|
107
|
+
"main",
|
|
108
|
+
"--here",
|
|
109
|
+
"--current-codex",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
assert.equal(parsed.ideaId, "main");
|
|
113
|
+
assert.equal(parsed.path, "/Volumes/Code_2TB/code/orp");
|
|
114
|
+
assert.equal(parsed.resumeTool, "codex");
|
|
115
|
+
assert.equal(parsed.resumeSessionId, "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9");
|
|
116
|
+
} finally {
|
|
117
|
+
process.chdir(originalCwd);
|
|
118
|
+
if (originalThreadId == null) {
|
|
119
|
+
delete process.env.CODEX_THREAD_ID;
|
|
120
|
+
} else {
|
|
121
|
+
process.env.CODEX_THREAD_ID = originalThreadId;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
100
126
|
test("parseWorkspaceCreateArgs validates slug titles and optional seed metadata", () => {
|
|
101
127
|
const parsed = parseWorkspaceCreateArgs([
|
|
102
128
|
"main-cody-1",
|
|
@@ -144,6 +170,63 @@ test("addTabToManifest canonicalizes Claude resume commands from tool plus sessi
|
|
|
144
170
|
assert.equal(result.manifest.tabs[2]?.sessionId, "claude-456");
|
|
145
171
|
});
|
|
146
172
|
|
|
173
|
+
test("addTabToManifest updates an existing matching tab instead of appending a duplicate", () => {
|
|
174
|
+
const result = addTabToManifest(sampleManifest(), {
|
|
175
|
+
path: "/Volumes/Code_2TB/code/orp",
|
|
176
|
+
title: "orp",
|
|
177
|
+
resumeTool: "codex",
|
|
178
|
+
resumeSessionId: "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
assert.equal(result.added, false);
|
|
182
|
+
assert.equal(result.updated, true);
|
|
183
|
+
assert.equal(result.mutation, "updated");
|
|
184
|
+
assert.equal(result.manifest.tabs.length, 2);
|
|
185
|
+
assert.equal(result.manifest.tabs[0]?.resumeCommand, "codex resume 019d4f24-c8ba-78b2-a726-48b1ce9f0fe9");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("addTabToManifest preserves existing resume metadata when no new resume is provided", () => {
|
|
189
|
+
const result = addTabToManifest(sampleManifest(), {
|
|
190
|
+
path: "/Volumes/Code_2TB/code/frg-site",
|
|
191
|
+
title: "frg-site",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
assert.equal(result.added, false);
|
|
195
|
+
assert.equal(result.updated, false);
|
|
196
|
+
assert.equal(result.unchanged, true);
|
|
197
|
+
assert.equal(result.mutation, "unchanged");
|
|
198
|
+
assert.equal(result.manifest.tabs.length, 2);
|
|
199
|
+
assert.equal(result.manifest.tabs[1]?.resumeCommand, "codex resume 019d348d-5031-78e1-9840-a66deaac33ae");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("addTabToManifest asks for a title when multiple saved tabs share a path", () => {
|
|
203
|
+
const manifest = normalizeWorkspaceManifest({
|
|
204
|
+
version: "1",
|
|
205
|
+
workspaceId: "main-cody-1",
|
|
206
|
+
title: "main-cody-1",
|
|
207
|
+
tabs: [
|
|
208
|
+
{
|
|
209
|
+
title: "longevity-research",
|
|
210
|
+
path: "/Volumes/Code_2TB/code/longevity-research",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
title: "longevity-research (2)",
|
|
214
|
+
path: "/Volumes/Code_2TB/code/longevity-research",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
assert.throws(
|
|
220
|
+
() =>
|
|
221
|
+
addTabToManifest(manifest, {
|
|
222
|
+
path: "/Volumes/Code_2TB/code/longevity-research",
|
|
223
|
+
resumeTool: "codex",
|
|
224
|
+
resumeSessionId: "019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
|
|
225
|
+
}),
|
|
226
|
+
/Multiple saved tabs already use this path/,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
147
230
|
test("removeTabsFromManifest can target a saved tab by path and resume session id", () => {
|
|
148
231
|
const result = removeTabsFromManifest(sampleManifest(), {
|
|
149
232
|
path: "/Volumes/Code_2TB/code/frg-site",
|
|
@@ -186,6 +269,42 @@ test("runWorkspaceAddTab updates a local workspace manifest file", async () => {
|
|
|
186
269
|
});
|
|
187
270
|
});
|
|
188
271
|
|
|
272
|
+
test("runWorkspaceAddTab upserts an existing tab and returns the rendered recovery command", async () => {
|
|
273
|
+
await withTempConfigHome(async () => {
|
|
274
|
+
const tempDir = await makeTempDir();
|
|
275
|
+
const manifestPath = path.join(tempDir, "workspace.json");
|
|
276
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(sampleManifest(), null, 2)}\n`, "utf8");
|
|
277
|
+
|
|
278
|
+
const { code, stdout } = await captureStdout(() =>
|
|
279
|
+
runWorkspaceAddTab([
|
|
280
|
+
"--workspace-file",
|
|
281
|
+
manifestPath,
|
|
282
|
+
"--path",
|
|
283
|
+
"/Volumes/Code_2TB/code/orp",
|
|
284
|
+
"--resume-tool",
|
|
285
|
+
"codex",
|
|
286
|
+
"--resume-session-id",
|
|
287
|
+
"019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
|
|
288
|
+
"--json",
|
|
289
|
+
]),
|
|
290
|
+
);
|
|
291
|
+
const payload = JSON.parse(stdout);
|
|
292
|
+
const saved = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
293
|
+
|
|
294
|
+
assert.equal(code, 0);
|
|
295
|
+
assert.equal(payload.action, "add-tab");
|
|
296
|
+
assert.equal(payload.mutation, "updated");
|
|
297
|
+
assert.equal(payload.tabCount, 2);
|
|
298
|
+
assert.equal(payload.tab.resumeCommand, "codex resume 019d4f24-c8ba-78b2-a726-48b1ce9f0fe9");
|
|
299
|
+
assert.equal(
|
|
300
|
+
payload.tab.restartCommand,
|
|
301
|
+
"cd '/Volumes/Code_2TB/code/orp' && codex resume 019d4f24-c8ba-78b2-a726-48b1ce9f0fe9",
|
|
302
|
+
);
|
|
303
|
+
assert.equal(saved.tabs.length, 2);
|
|
304
|
+
assert.equal(saved.tabs[0]?.resumeCommand, "codex resume 019d4f24-c8ba-78b2-a726-48b1ce9f0fe9");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
189
308
|
test("runWorkspaceRemoveTab updates a local workspace manifest file", async () => {
|
|
190
309
|
await withTempConfigHome(async () => {
|
|
191
310
|
const tempDir = await makeTempDir();
|
|
@@ -29,10 +29,16 @@ test("runOrpWorkspaceCommand shows the ledger-first help surface", async () => {
|
|
|
29
29
|
const { code, stdout } = await captureStdout(() => runOrpWorkspaceCommand(["-h"]));
|
|
30
30
|
|
|
31
31
|
assert.equal(code, 0);
|
|
32
|
+
assert.match(stdout, /orp workspace list \[--json\]/);
|
|
33
|
+
assert.match(stdout, /orp workspace add-tab <name-or-id>/);
|
|
34
|
+
assert.match(stdout, /--here/);
|
|
35
|
+
assert.match(stdout, /--current-codex/);
|
|
36
|
+
assert.match(stdout, /orp workspace remove-tab <name-or-id>/);
|
|
32
37
|
assert.match(stdout, /orp workspace ledger <name-or-id>/);
|
|
33
38
|
assert.match(stdout, /orp workspace ledger add <name-or-id>/);
|
|
34
39
|
assert.match(stdout, /orp workspace ledger remove <name-or-id>/);
|
|
35
40
|
assert.match(stdout, /orp workspace tabs <name-or-id>/);
|
|
41
|
+
assert.match(stdout, /Compatibility alias for the same tabs\/add\/remove ledger flow/);
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
test("runOrpWorkspaceCommand routes ledger help to the tabs help surface", async () => {
|
|
@@ -129,3 +129,57 @@ templates:
|
|
|
129
129
|
output_hint: orp.erdos-catalog-sync.yml
|
|
130
130
|
default_profiles:
|
|
131
131
|
- erdos_catalog_sync_active
|
|
132
|
+
|
|
133
|
+
install:
|
|
134
|
+
default_includes:
|
|
135
|
+
- catalog
|
|
136
|
+
- live_compare
|
|
137
|
+
- problem857
|
|
138
|
+
report_name: orp.erdos.pack-install-report.md
|
|
139
|
+
components:
|
|
140
|
+
catalog:
|
|
141
|
+
template_id: erdos_problems_catalog_sync
|
|
142
|
+
output_name: orp.erdos-catalog-sync.yml
|
|
143
|
+
description: Erdos catalog sync (all/open/closed/active snapshots).
|
|
144
|
+
live_compare:
|
|
145
|
+
template_id: sunflower_live_compare_suite
|
|
146
|
+
output_name: orp.erdos-live-compare.yml
|
|
147
|
+
description: Side-by-side atomic-board compare for Problems 857/20/367.
|
|
148
|
+
required_paths:
|
|
149
|
+
- analysis/problem857_counting_gateboard.json
|
|
150
|
+
- analysis/problem20_k3_gateboard.json
|
|
151
|
+
- analysis/problem367_sharp_gateboard.json
|
|
152
|
+
- scripts/problem857_ops_board.py
|
|
153
|
+
- scripts/problem20_ops_board.py
|
|
154
|
+
- scripts/problem367_ops_board.py
|
|
155
|
+
- scripts/frontier_status.py
|
|
156
|
+
problem857:
|
|
157
|
+
template_id: sunflower_problem857_discovery
|
|
158
|
+
output_name: orp.erdos-problem857.yml
|
|
159
|
+
description: Problem 857 discovery profile (board refresh/ready/spec/lean/frontier).
|
|
160
|
+
required_paths:
|
|
161
|
+
- analysis/problem857_counting_gateboard.json
|
|
162
|
+
- docs/PROBLEM857_COUNTING_OPS_BOARD.md
|
|
163
|
+
- orchestrator/v2/scopes/problem_857.yaml
|
|
164
|
+
- orchestrator/problem857_public_spec_check.py
|
|
165
|
+
- scripts/problem857_ops_board.py
|
|
166
|
+
- scripts/frontier_status.py
|
|
167
|
+
- sunflower_lean
|
|
168
|
+
governance:
|
|
169
|
+
template_id: sunflower_mathlib_pr_governance
|
|
170
|
+
output_name: orp.erdos-mathlib-pr-governance.yml
|
|
171
|
+
description: Mathlib PR governance profile set (pre-open, draft-readiness, full flow).
|
|
172
|
+
required_paths:
|
|
173
|
+
- docs/MATHLIB_SUBMISSION_CHECKLIST.md
|
|
174
|
+
- docs/MATHLIB_DRAFT_PR_TEMPLATE.md
|
|
175
|
+
- docs/MATHLIB_ISSUE_VIABILITY_GATE.md
|
|
176
|
+
- docs/UPSTREAM_PR_LANE.md
|
|
177
|
+
- analysis/UPSTREAM_PR_PLAN.yaml
|
|
178
|
+
- scripts/upstream-pr-plan.py
|
|
179
|
+
- scripts/upstream-pr-lane.sh
|
|
180
|
+
- scripts/mathlib-issue-viability-gate.py
|
|
181
|
+
- scripts/mathlib-naturality-snippet.sh
|
|
182
|
+
- scripts/mathlib-issue-local-gate.sh
|
|
183
|
+
- scripts/mathlib-tighten-fine-tooth-gate.sh
|
|
184
|
+
- scripts/mathlib-ready-to-draft-gate.sh
|
|
185
|
+
- scripts/mathlib-pr-body-preflight.py
|
|
@@ -144,3 +144,20 @@ templates:
|
|
|
144
144
|
output_hint: orp.external-pr-feedback-hardening.yml
|
|
145
145
|
default_profiles:
|
|
146
146
|
- external_feedback_hardening
|
|
147
|
+
|
|
148
|
+
install:
|
|
149
|
+
default_includes:
|
|
150
|
+
- governance
|
|
151
|
+
- feedback_hardening
|
|
152
|
+
report_name: orp.external-pr.pack-install-report.md
|
|
153
|
+
components:
|
|
154
|
+
governance:
|
|
155
|
+
template_id: oss_pr_governance
|
|
156
|
+
output_name: orp.external-pr-governance.yml
|
|
157
|
+
description: Generic external contribution governance profiles (watch/select, pre-open, local-readiness, draft transition, draft lifecycle, full flow).
|
|
158
|
+
required_paths:
|
|
159
|
+
- analysis/PR_DRAFT_BODY.md
|
|
160
|
+
feedback_hardening:
|
|
161
|
+
template_id: oss_feedback_hardening
|
|
162
|
+
output_name: orp.external-pr-feedback-hardening.yml
|
|
163
|
+
description: Generic maintainer-feedback hardening profile.
|
|
@@ -176,3 +176,28 @@ templates:
|
|
|
176
176
|
output_hint: orp.issue-smashers-feedback-hardening.yml
|
|
177
177
|
default_profiles:
|
|
178
178
|
- issue_smashers_feedback_hardening
|
|
179
|
+
|
|
180
|
+
install:
|
|
181
|
+
default_includes:
|
|
182
|
+
- workspace
|
|
183
|
+
- feedback_hardening
|
|
184
|
+
report_name: orp.issue-smashers.pack-install-report.md
|
|
185
|
+
components:
|
|
186
|
+
workspace:
|
|
187
|
+
template_id: issue_smashers_workspace
|
|
188
|
+
output_name: orp.issue-smashers.yml
|
|
189
|
+
description: Opinionated issue-smashers workspace and external contribution governance profiles.
|
|
190
|
+
required_paths:
|
|
191
|
+
- issue-smashers/README.md
|
|
192
|
+
- issue-smashers/WORKSPACE_RULES.md
|
|
193
|
+
- issue-smashers/setup-issue-smashers.sh
|
|
194
|
+
- issue-smashers/analysis/ISSUE_SMASHERS_WATCHLIST.json
|
|
195
|
+
- issue-smashers/analysis/ISSUE_SMASHERS_STATUS.md
|
|
196
|
+
- issue-smashers/analysis/PR_DRAFT_BODY.md
|
|
197
|
+
feedback_hardening:
|
|
198
|
+
template_id: issue_smashers_feedback_hardening
|
|
199
|
+
output_name: orp.issue-smashers-feedback-hardening.yml
|
|
200
|
+
description: Issue-smashers maintainer-feedback hardening profile.
|
|
201
|
+
required_paths:
|
|
202
|
+
- issue-smashers/WORKSPACE_RULES.md
|
|
203
|
+
- issue-smashers/analysis/ISSUE_SMASHERS_STATUS.md
|
|
@@ -24,7 +24,7 @@ from typing import Any
|
|
|
24
24
|
import yaml
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
LEGACY_PACK_SPECS: dict[str, dict[str, Any]] = {
|
|
28
28
|
"erdos-open-problems": {
|
|
29
29
|
"default_includes": ["catalog", "live_compare", "problem857"],
|
|
30
30
|
"report_name": "orp.erdos.pack-install-report.md",
|
|
@@ -1267,18 +1267,99 @@ def _load_yaml(path: Path) -> dict[str, Any]:
|
|
|
1267
1267
|
return payload
|
|
1268
1268
|
|
|
1269
1269
|
|
|
1270
|
-
def
|
|
1271
|
-
spec =
|
|
1270
|
+
def _legacy_pack_spec(pack_id: str) -> dict[str, Any]:
|
|
1271
|
+
spec = LEGACY_PACK_SPECS.get(pack_id)
|
|
1272
1272
|
if not isinstance(spec, dict):
|
|
1273
1273
|
raise RuntimeError(f"unsupported pack for install flow: {pack_id}")
|
|
1274
1274
|
return spec
|
|
1275
1275
|
|
|
1276
1276
|
|
|
1277
|
-
def
|
|
1278
|
-
|
|
1277
|
+
def _pack_install_spec(pack_meta: dict[str, Any], pack_id: str) -> dict[str, Any]:
|
|
1278
|
+
install = pack_meta.get("install")
|
|
1279
|
+
if isinstance(install, dict):
|
|
1280
|
+
return install
|
|
1281
|
+
return _legacy_pack_spec(pack_id)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def _pack_templates(pack_meta: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
1285
|
+
raw = pack_meta.get("templates", {})
|
|
1286
|
+
if not isinstance(raw, dict):
|
|
1287
|
+
return {}
|
|
1288
|
+
out: dict[str, dict[str, Any]] = {}
|
|
1289
|
+
for key, value in raw.items():
|
|
1290
|
+
if isinstance(key, str) and isinstance(value, dict):
|
|
1291
|
+
out[key] = value
|
|
1292
|
+
return out
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _normalize_install_component(
|
|
1296
|
+
component_key: str,
|
|
1297
|
+
raw_component: dict[str, Any],
|
|
1298
|
+
*,
|
|
1299
|
+
templates: dict[str, dict[str, Any]],
|
|
1300
|
+
) -> dict[str, Any]:
|
|
1301
|
+
template_id = str(raw_component.get("template_id", "")).strip()
|
|
1302
|
+
if not template_id:
|
|
1303
|
+
raise RuntimeError(f"install component missing template_id: {component_key}")
|
|
1304
|
+
|
|
1305
|
+
template_meta = templates.get(template_id)
|
|
1306
|
+
if not isinstance(template_meta, dict):
|
|
1307
|
+
raise RuntimeError(
|
|
1308
|
+
f"install component {component_key} references unknown template_id: {template_id}"
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
output_name = str(raw_component.get("output_name") or template_meta.get("output_hint") or "").strip()
|
|
1312
|
+
if not output_name:
|
|
1313
|
+
raise RuntimeError(
|
|
1314
|
+
f"install component {component_key} is missing output_name and template {template_id} has no output_hint"
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
description = str(raw_component.get("description") or template_meta.get("description") or "").strip()
|
|
1318
|
+
required_paths_raw = raw_component.get("required_paths", [])
|
|
1319
|
+
if required_paths_raw is None:
|
|
1320
|
+
required_paths: list[str] = []
|
|
1321
|
+
elif isinstance(required_paths_raw, list):
|
|
1322
|
+
required_paths = [str(path) for path in required_paths_raw]
|
|
1323
|
+
else:
|
|
1324
|
+
raise RuntimeError(f"install component required_paths must be a list: {component_key}")
|
|
1325
|
+
|
|
1326
|
+
return {
|
|
1327
|
+
"template_id": template_id,
|
|
1328
|
+
"output_name": output_name,
|
|
1329
|
+
"description": description,
|
|
1330
|
+
"required_paths": required_paths,
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _pack_components(pack_meta: dict[str, Any], pack_id: str) -> dict[str, dict[str, Any]]:
|
|
1335
|
+
install = _pack_install_spec(pack_meta, pack_id)
|
|
1336
|
+
components = install.get("components", {})
|
|
1279
1337
|
if not isinstance(components, dict) or not components:
|
|
1280
1338
|
raise RuntimeError(f"pack has no installable components: {pack_id}")
|
|
1281
|
-
|
|
1339
|
+
|
|
1340
|
+
templates = _pack_templates(pack_meta)
|
|
1341
|
+
out: dict[str, dict[str, Any]] = {}
|
|
1342
|
+
for key, value in components.items():
|
|
1343
|
+
if not isinstance(key, str) or not isinstance(value, dict):
|
|
1344
|
+
raise RuntimeError(f"invalid install component entry in pack {pack_id}: {key!r}")
|
|
1345
|
+
out[key] = _normalize_install_component(key, value, templates=templates)
|
|
1346
|
+
return out
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _pack_default_includes(pack_meta: dict[str, Any], pack_id: str) -> list[str]:
|
|
1350
|
+
install = _pack_install_spec(pack_meta, pack_id)
|
|
1351
|
+
raw = install.get("default_includes", [])
|
|
1352
|
+
if not isinstance(raw, list):
|
|
1353
|
+
return []
|
|
1354
|
+
return [str(x) for x in raw if isinstance(x, str)]
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def _pack_report_name(pack_meta: dict[str, Any], pack_id: str) -> str:
|
|
1358
|
+
install = _pack_install_spec(pack_meta, pack_id)
|
|
1359
|
+
raw = install.get("report_name")
|
|
1360
|
+
if isinstance(raw, str) and raw.strip():
|
|
1361
|
+
return raw.strip()
|
|
1362
|
+
return f"orp.{pack_id}.pack-install-report.md"
|
|
1282
1363
|
|
|
1283
1364
|
|
|
1284
1365
|
def _validate_var(raw: str) -> str:
|
|
@@ -2153,15 +2234,13 @@ def main() -> int:
|
|
|
2153
2234
|
pack_id = str(pack_meta.get("pack_id", args.pack_id))
|
|
2154
2235
|
pack_version = str(pack_meta.get("version", "unknown"))
|
|
2155
2236
|
generated_at_utc = _now_utc()
|
|
2156
|
-
components = _pack_components(pack_id)
|
|
2237
|
+
components = _pack_components(pack_meta, pack_id)
|
|
2157
2238
|
effective_vars = _vars_map(pack_meta, list(args.var or []))
|
|
2158
2239
|
problem857_source_mode = _problem857_source_mode(effective_vars)
|
|
2159
2240
|
|
|
2160
2241
|
includes = list(args.include or [])
|
|
2161
2242
|
if not includes:
|
|
2162
|
-
|
|
2163
|
-
if isinstance(default_includes, list):
|
|
2164
|
-
includes = [str(x) for x in default_includes if isinstance(x, str)]
|
|
2243
|
+
includes = _pack_default_includes(pack_meta, pack_id)
|
|
2165
2244
|
if not includes:
|
|
2166
2245
|
includes = sorted(components.keys())
|
|
2167
2246
|
|
|
@@ -2227,7 +2306,7 @@ def main() -> int:
|
|
|
2227
2306
|
else:
|
|
2228
2307
|
report_path = report_path.resolve()
|
|
2229
2308
|
else:
|
|
2230
|
-
report_name =
|
|
2309
|
+
report_name = _pack_report_name(pack_meta, pack_id)
|
|
2231
2310
|
report_path = (target_repo_root / report_name).resolve()
|
|
2232
2311
|
|
|
2233
2312
|
_write_report(
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"orp_version_min": {
|
|
35
35
|
"type": "string"
|
|
36
36
|
},
|
|
37
|
+
"install": {
|
|
38
|
+
"$ref": "#/$defs/install"
|
|
39
|
+
},
|
|
37
40
|
"variables": {
|
|
38
41
|
"type": "object",
|
|
39
42
|
"additionalProperties": {
|
|
@@ -90,6 +93,51 @@
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
},
|
|
97
|
+
"install": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"properties": {
|
|
101
|
+
"default_includes": {
|
|
102
|
+
"type": "array",
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "string"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"report_name": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
},
|
|
110
|
+
"components": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"additionalProperties": {
|
|
113
|
+
"$ref": "#/$defs/installComponent"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"installComponent": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"required": [
|
|
122
|
+
"template_id"
|
|
123
|
+
],
|
|
124
|
+
"properties": {
|
|
125
|
+
"template_id": {
|
|
126
|
+
"type": "string"
|
|
127
|
+
},
|
|
128
|
+
"output_name": {
|
|
129
|
+
"type": "string"
|
|
130
|
+
},
|
|
131
|
+
"description": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
},
|
|
134
|
+
"required_paths": {
|
|
135
|
+
"type": "array",
|
|
136
|
+
"items": {
|
|
137
|
+
"type": "string"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
93
141
|
}
|
|
94
142
|
}
|
|
95
143
|
}
|