jorgex-stack 1.0.24 → 1.0.26

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 CHANGED
@@ -9,7 +9,7 @@ Portable multi-agent harness: one configuration source — 15 agents, 17 skills,
9
9
  Install and run via npm without cloning the repository:
10
10
 
11
11
  ```
12
- pnpm dlx jorgex-stack install # apply config to your runtimes (interactive pick; idempotent, removes orphans)
12
+ pnpm dlx jorgex-stack install # install runtimes; first OpenCode setup selects connected models
13
13
  pnpm dlx jorgex-stack models # model picker by runtime and tier (strong/standard/cheap)
14
14
  pnpm dlx jorgex-stack sync # alias of install (same idempotent apply)
15
15
  pnpm dlx jorgex-stack doctor # checks that everything is healthy (Engram, drift, hooks, keys)
@@ -24,7 +24,7 @@ For development from a clone, run the same commands through `pnpm cli <command>`
24
24
 
25
25
  Every command supports `--dry-run`, `--yes`, and `--target-dir <dir>` for testing without touching the real config. Writes create automatic backups and verify idempotency; merges into user config are surgical (marked markdown sections, JSON/TOML upserts), so user-owned content is never touched.
26
26
 
27
- Runtime defaults are documented in [docs/references/permissions.md](docs/references/permissions.md) for permissions and [docs/references/models.md](docs/references/models.md) for model selection, GPT-5.6 subagent tiers, and orchestrator inheritance.
27
+ Runtime defaults are documented in [docs/references/permissions.md](docs/references/permissions.md) for permissions and [docs/references/models.md](docs/references/models.md) for provider-aware model selection, Codex tiers, and orchestrator inheritance. OpenCode has no provider defaults.
28
28
 
29
29
  ### Modes: Human and Programmatic
30
30
 
@@ -52,6 +52,8 @@ Flags:
52
52
 
53
53
  This installs into all detected runtimes. To be explicit, add `--agents opencode,claude-code,codex` or a comma-separated subset. Always pass `--mode programmatic`; without `--mode`, `--yes` and non-TTY installs default to `human`.
54
54
 
55
+ OpenCode also requires an existing selection in `~/.jorgex-stack/model-map.json`; run `pnpm dlx jorgex-stack models --agents opencode` interactively once before a headless install.
56
+
55
57
  - `--mode human` cannot be combined with `--subagent-concurrency`.
56
58
  - Without `--mode`, the first run asks interactively; `--yes`, non-TTY, and `--target-dir` default to `human`.
57
59
  - `pnpm dlx jorgex-stack sync` reuses the saved mode; pass `--mode` to change and save the preference.
package/dist/cli.js CHANGED
@@ -178,11 +178,6 @@ function resolveAgentModel(models, agentName, tier) {
178
178
  };
179
179
  }
180
180
  var DEFAULT_MODEL_MAP = {
181
- opencode: {
182
- strong: { model: "openai/gpt-5.6-terra", variant: "xhigh" },
183
- standard: { model: "openai/gpt-5.6-terra", variant: "xhigh" },
184
- cheap: { model: "openai/gpt-5.6-luna", variant: "medium" }
185
- },
186
181
  "claude-code": {
187
182
  strong: { model: "fable" },
188
183
  standard: { model: "sonnet" },
@@ -209,7 +204,7 @@ function loadModelMap() {
209
204
  } catch {
210
205
  return DEFAULT_MODEL_MAP;
211
206
  }
212
- const merged = {};
207
+ const merged = { ...fromDisk };
213
208
  for (const id of Object.keys(DEFAULT_MODEL_MAP)) {
214
209
  merged[id] = { ...DEFAULT_MODEL_MAP[id], ...fromDisk[id] ?? {} };
215
210
  }
@@ -1649,7 +1644,8 @@ async function runInstall(opts) {
1649
1644
  }
1650
1645
  const models = modelMap[id];
1651
1646
  if (!models) {
1652
- p.log.warn(`${adapter.name}: sin model-map para este runtime \u2014 omitido.`);
1647
+ p.log.error(`${adapter.name}: sin modelos seleccionados \u2014 ejecuta 'jorgex-stack models --agents ${id}'.`);
1648
+ exitCode = 1;
1653
1649
  continue;
1654
1650
  }
1655
1651
  const ctx = {
@@ -2867,8 +2863,12 @@ function agentsByTier() {
2867
2863
  }
2868
2864
  return grouped;
2869
2865
  }
2866
+ function isCompleteRuntimeModelMap(models) {
2867
+ return TIERS.every((tier) => models[tier]?.model);
2868
+ }
2870
2869
  async function askModel(det, subject, current) {
2871
2870
  if (det.id === "codex") {
2871
+ if (!current) throw new Error("Codex requiere un model-map base.");
2872
2872
  const effort = await p5.select({
2873
2873
  message: `${det.name} \xB7 ${subject} \u2014 reasoning effort`,
2874
2874
  options: EFFORTS.map((v) => ({ value: v, label: v })),
@@ -2902,16 +2902,18 @@ async function askModel(det, subject, current) {
2902
2902
  }
2903
2903
  const optionsList = det.options;
2904
2904
  const options = optionsList.map((m) => ({ value: m, label: m }));
2905
- if (!optionsList.includes(current.model)) options.unshift({ value: current.model, label: `${current.model} (actual)` });
2905
+ if (current && !optionsList.includes(current.model)) {
2906
+ options.unshift({ value: current.model, label: `${current.model} (actual)` });
2907
+ }
2906
2908
  const choice = await p5.select({
2907
2909
  message: `${det.name} \xB7 ${subject} \u2014 modelo`,
2908
2910
  options,
2909
- initialValue: current.model,
2911
+ initialValue: current?.model,
2910
2912
  maxItems: 12
2911
2913
  });
2912
2914
  if (p5.isCancel(choice)) return CANCEL;
2913
2915
  if (det.id === "claude-code") return { model: choice };
2914
- const keptCurrent = choice === current.model && current.variant ? current.variant : null;
2916
+ const keptCurrent = current && choice === current.model && current.variant ? current.variant : null;
2915
2917
  const variant = await p5.select({
2916
2918
  message: `${det.name} \xB7 ${subject} \u2014 reasoning effort (variant; solo si el modelo lo soporta)`,
2917
2919
  options: [
@@ -2926,12 +2928,16 @@ async function askModel(det, subject, current) {
2926
2928
  }
2927
2929
  async function runModelsPicker(opts) {
2928
2930
  const file = ensureModelMapFile();
2931
+ const map = loadModelMap();
2929
2932
  if (opts.yes || !process.stdout.isTTY) {
2930
- console.log(`Model-map en ${file} (defaults). Ed\xEDtalo o ejecuta 'models' sin --yes para el picker.`);
2933
+ if (opts.runtimes.includes("opencode") && !map.opencode) {
2934
+ console.error("OpenCode requiere selecci\xF3n interactiva desde los proveedores conectados; ejecuta 'models --agents opencode' sin --yes.");
2935
+ return 1;
2936
+ }
2937
+ console.log(`Model-map en ${file}. Ed\xEDtalo o ejecuta 'models' sin --yes para el picker.`);
2931
2938
  return 0;
2932
2939
  }
2933
2940
  p5.intro("jorgex-stack models \u2014 modelos por tier o por subagente");
2934
- const map = loadModelMap();
2935
2941
  const grouped = agentsByTier();
2936
2942
  const tierLine = (tier) => grouped[tier].join(", ");
2937
2943
  const agentCount = TIERS.reduce((n, tier) => n + grouped[tier].length, 0);
@@ -2960,7 +2966,10 @@ async function runModelsPicker(opts) {
2960
2966
  p5.log.warn("OpenCode: no se pudo listar `opencode models` \u2014 se mantiene la selecci\xF3n actual.");
2961
2967
  continue;
2962
2968
  }
2963
- const runtimeMap = { ...map[det.id] };
2969
+ const existingRuntimeMap = map[det.id];
2970
+ const runtimeMap = {
2971
+ ...existingRuntimeMap
2972
+ };
2964
2973
  p5.log.message(
2965
2974
  `${det.name} \u2014 tiers y sus subagentes:
2966
2975
  strong \u2192 ${tierLine("strong")}
@@ -2978,7 +2987,7 @@ async function runModelsPicker(opts) {
2978
2987
  if (p5.isCancel(mode)) return cancelled();
2979
2988
  if (mode === "tier") {
2980
2989
  for (const tier of TIERS) {
2981
- const asked = await askModel(det, `tier ${tier} (${tierLine(tier)})`, runtimeMap[tier]);
2990
+ const asked = await askModel(det, `tier ${tier} (${tierLine(tier)})`, existingRuntimeMap?.[tier]);
2982
2991
  if (asked === CANCEL) return cancelled();
2983
2992
  runtimeMap[tier] = asked;
2984
2993
  }
@@ -2991,10 +3000,15 @@ async function runModelsPicker(opts) {
2991
3000
  } else {
2992
3001
  const overrides = { ...runtimeMap.overrides ?? {} };
2993
3002
  for (const tier of TIERS) {
2994
- const base = runtimeMap[tier];
3003
+ let base = runtimeMap[tier];
2995
3004
  for (const name of grouped[tier]) {
2996
- const asked = await askModel(det, `${name} (tier ${tier})`, resolveAgentModel(runtimeMap, name, tier));
3005
+ const current = existingRuntimeMap ? resolveAgentModel(existingRuntimeMap, name, tier) : void 0;
3006
+ const asked = await askModel(det, `${name} (tier ${tier})`, current);
2997
3007
  if (asked === CANCEL) return cancelled();
3008
+ if (!base) {
3009
+ base = asked;
3010
+ runtimeMap[tier] = base;
3011
+ }
2998
3012
  const sameAsTier = asked.model === base.model && (asked.variant ?? "") === (base.variant ?? "");
2999
3013
  if (sameAsTier) {
3000
3014
  delete overrides[name];
@@ -3009,6 +3023,9 @@ async function runModelsPicker(opts) {
3009
3023
  if (Object.keys(overrides).length > 0) runtimeMap.overrides = overrides;
3010
3024
  else delete runtimeMap.overrides;
3011
3025
  }
3026
+ if (!isCompleteRuntimeModelMap(runtimeMap)) {
3027
+ throw new Error(`${det.name}: selecci\xF3n de modelos incompleta.`);
3028
+ }
3012
3029
  map[det.id] = runtimeMap;
3013
3030
  }
3014
3031
  writeText(file, JSON.stringify(map, null, 2) + "\n");
@@ -3052,6 +3069,18 @@ function readPackageMetadata() {
3052
3069
  // src/cli.ts
3053
3070
  var VERSION = readPackageVersion();
3054
3071
  var COMMANDS = ["install", "sync", "models", "update", "doctor", "restore", "uninstall"];
3072
+ async function ensureOpenCodeModelsForInstall(command, flags, runtimes) {
3073
+ if (!runtimes.includes("opencode") || loadModelMap().opencode) return true;
3074
+ const canPrompt = command === "install" && !flags.yes && !flags.dryRun && process.stdout.isTTY;
3075
+ if (canPrompt) {
3076
+ const code = await runModelsPicker({ yes: false, runtimes: ["opencode"] });
3077
+ if (code === 0 && loadModelMap().opencode) return true;
3078
+ }
3079
+ console.error(
3080
+ "OpenCode no tiene modelos configurados. Ejecuta 'jorgex-stack models --agents opencode' de forma interactiva antes de install/sync."
3081
+ );
3082
+ return false;
3083
+ }
3055
3084
  function parseFlags(args) {
3056
3085
  const flags = {
3057
3086
  agents: [],
@@ -3204,9 +3233,9 @@ function printHelp() {
3204
3233
  Uso: pnpm dlx jorgex-stack [comando] [opciones]
3205
3234
 
3206
3235
  Comandos:
3207
- install Instala el stack en los runtimes elegidos (default, interactivo)
3208
- sync Re-aplica la config (idempotente; alias de install)
3209
- models Picker de modelos por tier (OpenCode: lista en vivo de 'opencode models')
3236
+ install Instala el stack; OpenCode fresh exige elegir modelos conectados
3237
+ sync Re-aplica la config y el model-map existente (idempotente; sin picker)
3238
+ models Picker por tier o subagente (OpenCode: 'opencode models' en vivo)
3210
3239
  update --check: compara stack/Engram/skills con sus upstreams
3211
3240
  doctor Estado: Engram, drift de config, hooks de Codex, key de context7
3212
3241
  restore --list para ver backups \xB7 'restore <id>' para restaurar
@@ -3266,6 +3295,10 @@ Flags disponibles: jorgex-stack --help`
3266
3295
  process.exitCode = 1;
3267
3296
  return;
3268
3297
  }
3298
+ if (!await ensureOpenCodeModelsForInstall(command, flags, runtimes)) {
3299
+ process.exitCode = 1;
3300
+ return;
3301
+ }
3269
3302
  process.exitCode = await runInstall({ runtimes, targetDir: flags.targetDir, dryRun: flags.dryRun, yes: flags.yes, mode });
3270
3303
  return;
3271
3304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI y OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -99,7 +99,7 @@ The `work-lifecycle` skill is the single source of this flow. Summary — every
99
99
  - `work/{name}/` (gitignored, exists only while the work is in progress) holds the human-reviewed artifacts: `PRD.md` and `plan.md`. They stay resident across intermediate PR merges; `plan.md` is the ONLY task status board — flip statuses with surgical edits; don't re-read the whole plan after every task (re-read it on resume).
100
100
  - The full spec of each atomic task → Engram, one `mem_save` per task under `work/{name}/task/{NN}`. When you delegate a task, pass the subagent its topic_key + title — never the task content inline; it retrieves the spec itself.
101
101
  - Phase outcomes, decisions and PR checkpoints → Engram under `work/{name}/{phase}` and `work/{name}/pr/{NN}`; tell each subagent which topic_key to use for its saves.
102
- - Pending work → the project's single `work/backlog` topic_key (one upserted list), or issues (`to-issues`) if the project uses a tracker. Never a TODOs folder.
102
+ - Pending work → the project's single `work/backlog` topic_key, or issues (`to-issues`) if the project uses a tracker. Never a TODOs folder. For Engram, you are the **single writer**: before every change, retrieve the exact observation with `mem_get_observation`, preserve unrelated entries, send the complete content with `mem_update`, then read it again to verify. Never write it concurrently or use a blind topic-key upsert. Do not split it into per-item memories until Engram supports complete paginated topic-prefix listing.
103
103
  - On final close: `mem_save` the outcome under `work/{name}/done`, move the PRD to the project's docs only if it has lasting documentation value, then delete `work/{name}/`. `work/{name}/done` is only for the last PR / final outcome. History is memory + git.
104
104
 
105
105
  ## Delegation map
@@ -173,7 +173,7 @@ When the plan is fully applied and VERIFY passes:
173
173
  - **Critical Issues (must fix)**: apply ALL of them — the PR must not reach merge with these open.
174
174
  - **Important Improvements (should fix)**: apply the ones worth doing now, at your judgment.
175
175
  - **Suggestions (nice to have)**: apply only if trivial and safe.
176
- 3. Every finding you decide NOT to apply now goes to the project's `work/backlog` single topic_key — one line each: what + why deferred.
176
+ 3. Every finding you decide NOT to apply now goes to the project's `work/backlog` single topic_key — one line each: what + why deferred. Apply the safe serialized backlog protocol above; subagents only return candidate lines.
177
177
  4. For what you DO apply: add the new tasks to plan.md and one `mem_save` per task spec, execute them as in EXECUTE, re-verify, and push the fixes to the PR branch.
178
178
  5. The review fires once per PR creation — pushing fixes does not re-trigger it. Re-run `/xreview` only if the fixes were large.
179
179
 
@@ -21,7 +21,7 @@ Every piece of work gets a **canonical kebab-case name** when it starts (e.g. `c
21
21
  | PR checkpoint outcome | Engram `work/{name}/pr/{NN}` | Intermediate PR merge record |
22
22
  | Phase outcomes, decisions, findings | Engram `work/{name}/{phase}` | History — must survive the folder and compactions |
23
23
  | Final outcome | Engram `work/{name}/done` | Permanent record of what shipped after the last PR |
24
- | Pending / backlog items | Engram `work/backlog` — ONE key per project | All pending ideas in a single upserted list |
24
+ | Pending / backlog items | Engram `work/backlog` — ONE key per project | All pending ideas in one serialized list |
25
25
 
26
26
  `work/` is **scaffolding, not product**: add it to the project's `.gitignore`. It contains ONLY work in progress — an empty `work/` means nothing is half-done. No `1-TODOs/`, no `3-finalized/`, no phase subfolders.
27
27
 
@@ -51,7 +51,18 @@ When presenting the PRD or the plan for human review on non-trivial work, OFFER
51
51
 
52
52
  ## Pending work (backlog)
53
53
 
54
- All pending or future work of a project lives under the SINGLE topic_key `work/backlog` (upsert): one list, each item a short title + one-liner. Never one topic_key per idea, never a TODOs folder. When an item starts, it graduates: remove it from the backlog and create its `work/{name}/`. Review findings deliberately NOT applied also land here, one line each (what + why deferred).
54
+ All pending or future work of a project lives under the SINGLE topic_key `work/backlog`: one list, each item a short title + one-liner. Never one topic_key per idea, never a TODOs folder. When an item starts, it graduates: remove it from the backlog and create its `work/{name}/`. Review findings deliberately NOT applied also land here, one line each (what + why deferred).
55
+
56
+ ### Safe backlog mutation
57
+
58
+ Engram replaces an observation's complete content on both `mem_update` and a `mem_save` topic-key upsert. Until Engram offers complete, paginated listing by topic-key prefix, use this serialized protocol for every backlog add, edit or removal:
59
+
60
+ 1. The active coordinator/orchestrator is the **single writer**. Subagents return candidate backlog lines; they never mutate `work/backlog` themselves. Never run backlog writes concurrently.
61
+ 2. Find the exact `work/backlog` observation, then call `mem_get_observation` to read its full, untruncated content. If it does not exist, create it once with `mem_save`.
62
+ 3. Change only the intended lines while preserving every unrelated entry, then call `mem_update` on that exact observation ID with the **complete content**. Never send only the delta and never use a blind `mem_save` upsert for an existing backlog.
63
+ 4. Call `mem_get_observation` again and **verify** both the intended change and the preserved entries.
64
+
65
+ Engram has no atomic append or compare-and-swap, so concurrent writers can still lose data even if both read first. Do not split items into `work/backlog/{slug}` yet: `mem_search` is capped and has no paginated topic-prefix listing, so older active items could become invisible. Once that capability exists, one observation per item is the preferred migration. If the project has an issue tracker, use issues instead now and do not keep an Engram backlog too.
55
66
 
56
67
  If the project manages work through an issue tracker, issues (`to-issues`) take this role instead — don't keep both.
57
68
 
@@ -91,6 +102,7 @@ For single-PR work, the PR checkpoint and final work close happen together: one
91
102
  - Don't mix several distinct pieces of work under the same name/topic_key.
92
103
  - Same evolving phase → same topic_key (upsert). Different phases and different tasks must not overwrite each other.
93
104
  - Never persist the same artifact in two homes — no file + memory copies, no hybrid writes.
105
+ - Treat `work/backlog` as the exceptional serialized list described above; ordinary topic-key upserts are not a safe substitute for its read-modify-write protocol.
94
106
 
95
107
  ## Legacy `work/` folders
96
108
 
@@ -161,7 +161,7 @@ Status, wave and dependencies live in the plan.md table (single home) — do NOT
161
161
 
162
162
  ## Backlog entry — Template (`work/backlog`)
163
163
 
164
- ONE observation per project holds every pending idea (topic_key `work/backlog`, upserted). Each item is just:
164
+ ONE observation per project holds every pending idea (topic_key `work/backlog`). Each item is just:
165
165
 
166
166
  ```markdown
167
167
  - **[short title]** — [one-line description of the idea and its value]
@@ -169,6 +169,8 @@ ONE observation per project holds every pending idea (topic_key `work/backlog`,
169
169
 
170
170
  When an item starts, remove it from this list and create its `work/[name]/`.
171
171
 
172
+ Mutation is serialized: the coordinator is the single writer, reads the exact observation with `mem_get_observation`, preserves all unrelated lines, sends the complete content through `mem_update`, then reads it again to verify. Never send a delta or use a blind topic-key upsert. See **Safe backlog mutation** in the parent skill for the full protocol and the current reason not to use one observation per item.
173
+
172
174
  ---
173
175
 
174
176
  ## Task creation rules
@@ -93,7 +93,7 @@ Every piece of information about a piece of work has exactly ONE home — never
93
93
  - In-progress work lives in `work/{name}/` (gitignored): `PRD.md` + `plan.md`. They stay there across intermediate PR merges; `plan.md` is the ONLY task status board — update statuses with surgical edits. An empty `work/` means nothing is half-done.
94
94
  - Execution worktrees and their branches always use the same name. Resolve the root with `git rev-parse --show-toplevel`, ensure `worktrees/` is ignored in the repo-local `.git/info/exclude`, then create/use `<project-root>/worktrees/<canonical-name>` for single-PR work or `<project-root>/worktrees/<canonical-name>-prNN` for multi-PR checkpoints; never create worktrees next to the repo, in the repo root, under `work/`, or in external temp/shared folders.
95
95
  - Full task specs, phase outcomes, PR checkpoints and history live in Engram: `work/{name}/task/{NN}`, `work/{name}/{phase}`, `work/{name}/pr/{NN}`, `work/{name}/done`. Subagents receive a topic_key + title, never the task content inline.
96
- - Pending work: the project's single `work/backlog` topic_key (one upserted list — never one key per idea), or issues (`to-issues`) if the project uses a tracker. Never a TODOs folder.
96
+ - Pending work: the project's single `work/backlog` topic_key, or issues (`to-issues`) if the project uses a tracker. Never a TODOs folder. The coordinator/orchestrator is its **single writer**: retrieve the exact observation with `mem_get_observation`, preserve unrelated entries, send the complete content with `mem_update`, then read it again to verify; never mutate it concurrently or use a blind topic-key upsert. Do not split it into one memory per item until Engram supports complete paginated topic-prefix listing.
97
97
  - On intermediate PR merge: save the checkpoint under `work/{name}/pr/{NN}` and keep `work/{name}/` alive for the remaining PRs.
98
98
  - On final close: save the outcome under `work/{name}/done`, move the PRD to the project's docs only if it has lasting value, then delete `work/{name}/`. `work/{name}/done` is the final outcome only. History is memory + git — no archive folders.
99
99
 
@@ -42,6 +42,8 @@ Use the `engram` subagent for non-trivial memory reads — it filters and return
42
42
 
43
43
  Work tracking follows the `work-lifecycle` skill: `work/{name}/plan.md` (file) is the only status board; memory holds the task specs (`work/{name}/task/{NN}`), phase outcomes (`work/{name}/{phase}`), PR checkpoints (`work/{name}/pr/{NN}`), and the final outcome in `work/{name}/done` only after the last PR; the project backlog stays under the single key `work/backlog`. Subagents retrieve their task by the topic_key the orchestrator passes them and save their phase outcome under the topic_key they were given BEFORE their final report.
44
44
 
45
+ `work/backlog` has a stricter mutation rule because Engram replaces complete content rather than applying a patch. The coordinator/orchestrator is the **single writer**; subagents only return candidates. Before every add, edit or removal, locate the exact observation and call `mem_get_observation`; preserve all unrelated entries, pass the complete content to `mem_update`, then read it again to verify. Never write it concurrently and never use a blind `mem_save` upsert. Separate `work/backlog/{slug}` memories are not safe yet because Engram lacks complete paginated topic-prefix listing; use tracker issues instead when available.
46
+
45
47
  ## Before ending a session
46
48
 
47
49
  Call `mem_session_summary` with: Goal, Instructions, Discoveries, Accomplished, Next Steps, Relevant Files. This is NOT optional — without it the next session starts blind.