jorgex-stack 1.0.11 → 1.0.13

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/dist/cli.js CHANGED
@@ -492,6 +492,19 @@ function upsertNativeHooks(existing, canonical, scriptsDir, mapMatcher = (m) =>
492
492
  });
493
493
  }
494
494
 
495
+ // src/lib/git-guard.ts
496
+ var DESTRUCTIVE_GIT_DENY = [
497
+ "git reset*",
498
+ "git clean*",
499
+ "git checkout -- *",
500
+ "git restore*",
501
+ "git push*--force*",
502
+ // posición libre: caza "git push origin main --force[-with-lease]"
503
+ "git push -f*",
504
+ "git switch*--discard-changes*"
505
+ ];
506
+ var GIT_GUARD_SCRIPT = "block-destructive-git.cjs";
507
+
495
508
  // src/adapters/opencode.ts
496
509
  function yamlString(value) {
497
510
  return JSON.stringify(value);
@@ -530,7 +543,12 @@ var opencodeAdapter = {
530
543
  lines.push(` edit: ${agent.readonly ? "deny" : "allow"}`);
531
544
  if (agent.bash === "none") lines.push(" bash: deny");
532
545
  else if (agent.bash === "git-read") lines.push(' bash:\n "git diff*": allow\n "git log*": allow');
533
- else lines.push(" bash: allow");
546
+ else {
547
+ const deny = DESTRUCTIVE_GIT_DENY.map((p7) => ` ${yamlString(p7)}: deny`).join("\n");
548
+ lines.push(` bash:
549
+ "*": allow
550
+ ${deny}`);
551
+ }
534
552
  if (!agent.spawn) lines.push(" task: deny");
535
553
  lines.push("tools:");
536
554
  lines.push(` write: ${agent.readonly ? "false" : "true"}`);
@@ -773,6 +791,16 @@ ${agent.body}`;
773
791
  const tools = toolsFor(agent);
774
792
  if (tools !== null) lines.push(`tools: ${tools}`);
775
793
  lines.push(`model: ${resolveAgentModel(models, agent.name, agent.tier).model}`);
794
+ if (agent.bash === "full") {
795
+ lines.push(
796
+ "hooks:",
797
+ " PreToolUse:",
798
+ ' - matcher: "Bash|PowerShell"',
799
+ " hooks:",
800
+ " - type: command",
801
+ ` command: "node \\"{{SCRIPTS_DIR}}/${GIT_GUARD_SCRIPT}\\""`
802
+ );
803
+ }
776
804
  return [
777
805
  {
778
806
  file: `${agent.name}.md`,
@@ -827,7 +855,7 @@ ${agent.body}`,
827
855
  continue;
828
856
  }
829
857
  const command = server.command === "{{ENGRAM_BIN}}" ? ctx.engramBin : server.command;
830
- servers[name] = { command, args: server.args ?? [] };
858
+ servers[name] = { type: "stdio", command, args: server.args ?? [] };
831
859
  } else {
832
860
  const previous = servers[name];
833
861
  const headers = {};
@@ -1208,7 +1236,7 @@ function planSystemPrompt(adapter, ctx) {
1208
1236
  // src/components/agents.ts
1209
1237
  import path13 from "path";
1210
1238
  function planAgents(adapter, ctx) {
1211
- const { agentsDir, commandsDir, outputStylesDir, skillsDir, profilesDir } = adapter.paths(ctx.configDir);
1239
+ const { agentsDir, commandsDir, outputStylesDir, skillsDir, profilesDir, scriptsDir } = adapter.paths(ctx.configDir);
1212
1240
  const dirFor = {
1213
1241
  agent: agentsDir,
1214
1242
  command: commandsDir,
@@ -1216,6 +1244,7 @@ function planAgents(adapter, ctx) {
1216
1244
  skill: skillsDir,
1217
1245
  profile: profilesDir
1218
1246
  };
1247
+ const scriptsBase = scriptsDir.replace(/\\/g, "/");
1219
1248
  return loadCanonicalAgents(path13.join(ctx.stackDir, "agents")).flatMap(
1220
1249
  (agent) => adapter.renderAgent(agent, ctx.models).flatMap((rendered) => {
1221
1250
  const dir = dirFor[rendered.kind];
@@ -1223,7 +1252,8 @@ function planAgents(adapter, ctx) {
1223
1252
  ctx.warnings.push(`${adapter.name}: sin destino para '${rendered.kind}' (${rendered.file}) \u2014 omitido.`);
1224
1253
  return [];
1225
1254
  }
1226
- return [{ kind: "write", target: path13.join(dir, rendered.file), content: rendered.content }];
1255
+ const content = rendered.content.replace(/\{\{SCRIPTS_DIR\}\}/g, scriptsBase);
1256
+ return [{ kind: "write", target: path13.join(dir, rendered.file), content }];
1227
1257
  })
1228
1258
  );
1229
1259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
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",
@@ -13,6 +13,8 @@ You implement real changes. You don't stop at analysis, you don't answer with ju
13
13
 
14
14
  **Mandatory first action**: load the `agent-delegation` skill.
15
15
 
16
+ **Never run destructive git** (`reset`, `clean`, `checkout --`, `restore`, `push --force`) — it can discard work or rewrite history. Commit forward; if you think you need to discard or reset repo state, stop and ask the user.
17
+
16
18
  **Conditional skill**:
17
19
 
18
20
  - `tdd`: load it when the prompt implies GREEN within a TDD flow.
@@ -13,6 +13,8 @@ Your job is to describe behavior with tests, fix broken tests, and verify they f
13
13
 
14
14
  **Mandatory first action**: load the `tdd` and `agent-delegation` skills.
15
15
 
16
+ **Never run destructive git** (`reset`, `clean`, `checkout --`, `restore`, `push --force`) — it can discard work or rewrite history. Commit forward; if you think you need to discard or reset repo state, stop and ask the user.
17
+
16
18
  ## Before writing tests
17
19
 
18
20
  Don't assume a framework. The test command and conventions are often already in your context; when they aren't, detect the real setup of THIS project:
@@ -13,6 +13,8 @@ You handle translations, multi-language text and integration with the project's
13
13
 
14
14
  **Mandatory first action**: load the `agent-delegation` skill.
15
15
 
16
+ **Never run destructive git** (`reset`, `clean`, `checkout --`, `restore`, `push --force`) — it can discard work or rewrite history. Commit forward; if you think you need to discard or reset repo state, stop and ask the user.
17
+
16
18
  **Final output, last of all**: your final report (ending with the Result contract) must be the very last thing you emit. If you need to save anything to memory, do it BEFORE that output — never after.
17
19
 
18
20
  ## Scope
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard (Claude Code): blocks destructive git commands in the
3
+ // subagents that declare it, leaving the main agent unrestricted. Claude Code
4
+ // has no per-subagent command denylist, so this is the documented mechanism
5
+ // (sub-agents docs -> "Conditional rules with hooks"). Reads the PreToolUse
6
+ // payload from stdin and exits 2 to block if any segment of the command would
7
+ // discard work or rewrite history. Best-effort guardrail, not an adversarial
8
+ // sandbox. Fail-open: any read/parse problem allows the command (exit 0) so the
9
+ // guard can never wedge legitimate work — but it logs why, so a silent no-op is
10
+ // diagnosable.
11
+
12
+ // git global options that sit BETWEEN `git` and the subcommand. Without skipping
13
+ // them, `git -C /repo reset --hard` would slip past a naive `git reset` match.
14
+ const GLOBAL_OPT_WITH_ARG = new Set([
15
+ "-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--super-prefix", "--config-env",
16
+ ]);
17
+
18
+ const isForceFlag = (token) =>
19
+ token === "--force" || token === "--force-with-lease" || /^-[a-z]*f[a-z]*$/i.test(token);
20
+
21
+ /** True if a single command segment is a destructive git invocation. */
22
+ function isDestructiveSegment(segment) {
23
+ const tokens = segment.trim().split(/\s+/).filter(Boolean);
24
+ const gitIdx = tokens.indexOf("git");
25
+ if (gitIdx === -1) return false;
26
+
27
+ // Skip leading git global options to reach the real subcommand.
28
+ let i = gitIdx + 1;
29
+ while (i < tokens.length) {
30
+ const token = tokens[i];
31
+ if (token === "--") break;
32
+ if (/^(--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)=/.test(token)) { i++; continue; }
33
+ if (GLOBAL_OPT_WITH_ARG.has(token)) { i += 2; continue; } // option + its value
34
+ if (token.startsWith("-")) { i++; continue; } // any other global flag
35
+ break;
36
+ }
37
+
38
+ const sub = tokens[i];
39
+ const rest = tokens.slice(i + 1);
40
+ if (!sub) return false;
41
+
42
+ // Always destructive: discard worktree/index, delete untracked files.
43
+ if (sub === "reset" || sub === "clean" || sub === "restore") return true;
44
+ // git switch --discard-changes drops local modifications.
45
+ if (sub === "switch") return rest.includes("--discard-changes");
46
+ // git checkout: a branch switch is safe; the discard forms carry "--", a
47
+ // force flag, or a "." pathspec (git checkout . / git checkout HEAD -- .).
48
+ if (sub === "checkout") {
49
+ return rest.includes("--") || rest.includes(".") || rest.some(isForceFlag);
50
+ }
51
+ // git push --force / --force-with-lease / -f (incl. bundled like -fq) or a
52
+ // leading "+" refspec all force-overwrite remote history.
53
+ if (sub === "push") {
54
+ return rest.some((t) => isForceFlag(t) || /^\+/.test(t));
55
+ }
56
+ return false;
57
+ }
58
+
59
+ function isDestructive(command) {
60
+ // Evaluate each command segment independently so a destructive op anywhere in
61
+ // a compound command is caught (cd x && git reset), without a quoted token in
62
+ // one command (git commit -m "git reset") leaking into another's match.
63
+ return command
64
+ .split(/&&|\|\||[;|\n]/)
65
+ .some((segment) => isDestructiveSegment(segment));
66
+ }
67
+
68
+ function extractCommand(payload) {
69
+ const toolInput = payload && payload.tool_input;
70
+ const raw = toolInput && (toolInput.command != null ? toolInput.command : toolInput.script);
71
+ // Codex-style hooks pass argv as an array; normalize like post-pr-review.cjs.
72
+ if (Array.isArray(raw)) return raw.join(" ");
73
+ return typeof raw === "string" ? raw : "";
74
+ }
75
+
76
+ function allow(reason) {
77
+ if (reason) process.stderr.write(`block-destructive-git: ${reason} (fail-open, allowing)\n`);
78
+ process.exit(0);
79
+ }
80
+
81
+ let input = "";
82
+ process.stdin.on("data", (chunk) => {
83
+ input += chunk;
84
+ });
85
+ process.stdin.on("error", () => allow("stdin error"));
86
+ process.stdin.on("end", () => {
87
+ let command = "";
88
+ try {
89
+ command = extractCommand(JSON.parse(input || "{}"));
90
+ } catch {
91
+ allow("unreadable payload");
92
+ return;
93
+ }
94
+ if (command.trim() === "") process.exit(0);
95
+
96
+ if (isDestructive(command)) {
97
+ process.stderr.write(
98
+ "Blocked: destructive git is not allowed in this subagent " +
99
+ "(reset / clean / checkout discard / restore / switch --discard-changes / push --force). " +
100
+ "Don't discard or rewrite repo state — commit forward, or stop and ask the user.\n",
101
+ );
102
+ process.exit(2);
103
+ }
104
+ process.exit(0);
105
+ });
106
+
107
+ // Safety nets so the guard can never hang the tool call: resume the stream (some
108
+ // shells need it to emit EOF) and bail out fail-open if stdin never ends.
109
+ process.stdin.resume();
110
+ setTimeout(() => allow("stdin timeout"), 2000).unref();