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
|
|
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
|
-
|
|
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
|
@@ -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.
|
package/stack/agents/tester.md
CHANGED
|
@@ -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();
|