Skip to content

Commit d36fb96

Browse files
authored
Merge pull request #107 from AxmeAI/fix/axme-gate-regex-20260414
fix(safety): parseAxmeGate strips trailing quote/punct from pr/repo (B-008)
2 parents 601c01c + 3111c8e commit d36fb96

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/storage/safety.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,22 @@ export function checkBash(rules: SafetyRules, command: string): SafetyVerdict {
329329
* sees the full command before execution and parses the metadata.
330330
*
331331
* Returns null if no #!axme marker found.
332+
*
333+
* Value capture: `[^\s"'` + "`" + `]+` rather than `\S+`, so a stray
334+
* closing quote/backtick from a surrounding `-m "..."` string doesn't
335+
* get glued onto the captured value (B-008). Trailing punctuation
336+
* (`)`, `,`, `;`, `.`) is stripped defensively for the same reason —
337+
* common when the marker ends up inside a HEREDOC or after a `$(...)`.
332338
*/
339+
const GATE_VALUE = /[^\s"'`]+/.source;
340+
const PR_RE = new RegExp(`\\bpr=(${GATE_VALUE})`);
341+
const REPO_RE = new RegExp(`\\brepo=(${GATE_VALUE})`);
342+
const TRAILING_PUNCT_RE = /[)\],;.]+$/;
343+
344+
function cleanGateValue(raw: string): string {
345+
return raw.replace(TRAILING_PUNCT_RE, "");
346+
}
347+
333348
export function parseAxmeGate(command: string): { pr: string; repo: string } | null {
334349
// Use the LAST occurrence of #!axme (the suffix), not one inside a commit message.
335350
const lastIdx = command.lastIndexOf("#!axme");
@@ -338,16 +353,21 @@ export function parseAxmeGate(command: string): { pr: string; repo: string } | n
338353
const match = suffix.match(/^#!axme\s+(.*)/);
339354
if (!match) return null;
340355
const pairs = match[1].trim();
341-
const prMatch = pairs.match(/\bpr=(\S+)/);
342-
const repoMatch = pairs.match(/\brepo=(\S+)/);
356+
const prMatch = pairs.match(PR_RE);
357+
const repoMatch = pairs.match(REPO_RE);
343358
if (!prMatch || !repoMatch) return null;
344-
return { pr: prMatch[1], repo: repoMatch[1] };
359+
const pr = cleanGateValue(prMatch[1]);
360+
const repo = cleanGateValue(repoMatch[1]);
361+
if (!pr || !repo) return null;
362+
return { pr, repo };
345363
}
346364

347365
const AXME_GATE_INSTRUCTION =
348366
'BLOCKED: git commit/push requires #!axme safety metadata. ' +
349367
'Retry with: `<your command> #!axme pr=<PR_NUMBER|none> repo=<OWNER/REPO>` ' +
350-
'(pr=none if no PR created yet).';
368+
'(pr=none if no PR created yet). ' +
369+
'Place the marker AFTER the closing `"` of any `-m "..."` argument, ' +
370+
'not inside it — otherwise the closing quote gets parsed as part of the repo name.';
351371

352372
/**
353373
* Check if a PR is merged via gh CLI. Returns true if merged, false otherwise.

test/axme-gate.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,40 @@ describe("parseAxmeGate", () => {
8181
const result = parseAxmeGate(cmd);
8282
assert.deepEqual(result, { pr: "42", repo: "AxmeAI/test" });
8383
});
84+
85+
// --- B-008 regression: greedy \S+ used to swallow the closing quote ---
86+
87+
it("strips trailing closing quote when marker is inside -m \"...\" string", () => {
88+
// This is the B-008 reproducer: marker placed INSIDE the quoted message.
89+
const cmd = 'git commit -m "fix: blah #!axme pr=6 repo=AxmeAI/axme-blog"';
90+
const result = parseAxmeGate(cmd);
91+
assert.deepEqual(result, { pr: "6", repo: "AxmeAI/axme-blog" });
92+
});
93+
94+
it("strips trailing single quote", () => {
95+
const cmd = "git commit -m 'fix #!axme pr=6 repo=AxmeAI/axme-blog'";
96+
const result = parseAxmeGate(cmd);
97+
assert.deepEqual(result, { pr: "6", repo: "AxmeAI/axme-blog" });
98+
});
99+
100+
it("strips trailing backtick", () => {
101+
const cmd = "git commit -m `fix #!axme pr=6 repo=AxmeAI/axme-blog`";
102+
const result = parseAxmeGate(cmd);
103+
assert.deepEqual(result, { pr: "6", repo: "AxmeAI/axme-blog" });
104+
});
105+
106+
it("strips trailing punctuation like ) and ,", () => {
107+
const cmd1 = 'git commit -m "$(echo fix #!axme pr=6 repo=AxmeAI/axme-blog)"';
108+
assert.deepEqual(parseAxmeGate(cmd1), { pr: "6", repo: "AxmeAI/axme-blog" });
109+
const cmd2 = 'git commit -m "fix #!axme pr=6 repo=AxmeAI/axme-blog,"';
110+
assert.deepEqual(parseAxmeGate(cmd2), { pr: "6", repo: "AxmeAI/axme-blog" });
111+
});
112+
113+
it("returns null when stripping leaves an empty value", () => {
114+
// pr=" — value is just a quote, after strip nothing left.
115+
const cmd = 'git commit -m "x #!axme pr=" repo=AxmeAI/x"';
116+
assert.equal(parseAxmeGate(cmd), null);
117+
});
84118
});
85119

86120
// ===== checkGit - gate enforcement =====

0 commit comments

Comments
 (0)