Skip to content

Commit 494d334

Browse files
fix: parse review stamp flags position-independently (#607)
Co-authored-by: T <t@t.com>
1 parent 4625728 commit 494d334

3 files changed

Lines changed: 86 additions & 34 deletions

File tree

.safeword/hooks/write-review-stamp.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,46 @@ function bareName(value: string, label: string): string {
5656
return value;
5757
}
5858

59-
// Optional leading flags `--ticket <folder>` and `--model <id>` (any order),
60-
// then the positional command. `--model` is the reviewing model, supplied by the
61-
// orchestrator that assigned it (NOT self-reported by the reviewer — Claude Code
62-
// withholds model identity from subagents, ticket MR5M3A).
63-
let positional = process.argv.slice(2);
64-
let explicitTicket: string | undefined;
65-
let reviewerModel: string | undefined;
66-
while (positional[0] === '--ticket' || positional[0] === '--model') {
67-
const flag = positional[0];
68-
const value = positional[1];
69-
if (value === undefined || value === '') fail(`${flag} requires a value`);
70-
if (flag === '--ticket') {
71-
explicitTicket = bareName(value, '--ticket');
72-
} else {
73-
if (/\s/.test(value)) fail('--model id must not contain whitespace');
74-
reviewerModel = value;
59+
interface ParsedArguments {
60+
positional: string[];
61+
explicitTicket: string | undefined;
62+
reviewerModel: string | undefined;
63+
}
64+
65+
// Optional global flags `--ticket <folder>` and `--model <id>` may appear before
66+
// or after the positional command. `--model` is the reviewing model, supplied by
67+
// the orchestrator that assigned it (NOT self-reported by the reviewer — Claude
68+
// Code withholds model identity from subagents, ticket MR5M3A).
69+
function parseArguments(argv: string[]): ParsedArguments {
70+
const positional: string[] = [];
71+
let explicitTicket: string | undefined;
72+
let reviewerModel: string | undefined;
73+
74+
for (let index = 2; index < argv.length; index += 1) {
75+
const arg = argv[index];
76+
if (arg === undefined) fail('missing argument');
77+
if (arg !== '--ticket' && arg !== '--model') {
78+
positional.push(arg);
79+
continue;
80+
}
81+
82+
const flag = arg;
83+
const value = argv[index + 1];
84+
if (value === undefined || value === '') fail(`${flag} requires a value`);
85+
if (flag === '--ticket') {
86+
explicitTicket = bareName(value, '--ticket');
87+
} else {
88+
if (/\s/.test(value)) fail('--model id must not contain whitespace');
89+
reviewerModel = value;
90+
}
91+
index += 1;
7592
}
76-
positional = positional.slice(2);
93+
94+
return { positional, explicitTicket, reviewerModel };
7795
}
7896

97+
const { positional, explicitTicket, reviewerModel } = parseArguments(process.argv);
98+
7999
// Resolve the ticket the same way the gate does conceptually (the one being worked),
80100
// failing loudly on ambiguity instead of stamping a ticket the gate isn't checking.
81101
function resolveTicketFolder(): string {

packages/cli/templates/hooks/write-review-stamp.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,46 @@ function bareName(value: string, label: string): string {
5656
return value;
5757
}
5858

59-
// Optional leading flags `--ticket <folder>` and `--model <id>` (any order),
60-
// then the positional command. `--model` is the reviewing model, supplied by the
61-
// orchestrator that assigned it (NOT self-reported by the reviewer — Claude Code
62-
// withholds model identity from subagents, ticket MR5M3A).
63-
let positional = process.argv.slice(2);
64-
let explicitTicket: string | undefined;
65-
let reviewerModel: string | undefined;
66-
while (positional[0] === '--ticket' || positional[0] === '--model') {
67-
const flag = positional[0];
68-
const value = positional[1];
69-
if (value === undefined || value === '') fail(`${flag} requires a value`);
70-
if (flag === '--ticket') {
71-
explicitTicket = bareName(value, '--ticket');
72-
} else {
73-
if (/\s/.test(value)) fail('--model id must not contain whitespace');
74-
reviewerModel = value;
59+
interface ParsedArguments {
60+
positional: string[];
61+
explicitTicket: string | undefined;
62+
reviewerModel: string | undefined;
63+
}
64+
65+
// Optional global flags `--ticket <folder>` and `--model <id>` may appear before
66+
// or after the positional command. `--model` is the reviewing model, supplied by
67+
// the orchestrator that assigned it (NOT self-reported by the reviewer — Claude
68+
// Code withholds model identity from subagents, ticket MR5M3A).
69+
function parseArguments(argv: string[]): ParsedArguments {
70+
const positional: string[] = [];
71+
let explicitTicket: string | undefined;
72+
let reviewerModel: string | undefined;
73+
74+
for (let index = 2; index < argv.length; index += 1) {
75+
const arg = argv[index];
76+
if (arg === undefined) fail('missing argument');
77+
if (arg !== '--ticket' && arg !== '--model') {
78+
positional.push(arg);
79+
continue;
80+
}
81+
82+
const flag = arg;
83+
const value = argv[index + 1];
84+
if (value === undefined || value === '') fail(`${flag} requires a value`);
85+
if (flag === '--ticket') {
86+
explicitTicket = bareName(value, '--ticket');
87+
} else {
88+
if (/\s/.test(value)) fail('--model id must not contain whitespace');
89+
reviewerModel = value;
90+
}
91+
index += 1;
7592
}
76-
positional = positional.slice(2);
93+
94+
return { positional, explicitTicket, reviewerModel };
7795
}
7896

97+
const { positional, explicitTicket, reviewerModel } = parseArguments(process.argv);
98+
7999
// Resolve the ticket the same way the gate does conceptually (the one being worked),
80100
// failing loudly on ambiguity instead of stamping a ticket the gate isn't checking.
81101
function resolveTicketFolder(): string {

packages/cli/tests/integration/review-stamp.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,18 @@ describe('NMSD94 stamp-earning step (write-review-stamp.ts)', () => {
204204
expect(readLog()).toContain(`review:${reviewScope(TICKET_ID, 'spec', hashArtifact(SPEC))}`);
205205
});
206206

207+
it('--ticket after --phase disambiguates when more than one ticket is in_progress', () => {
208+
const second = nodePath.join(projectRoot, '.safeword-project', 'tickets', 'XYZ789');
209+
mkdirSync(second, { recursive: true });
210+
writeFileSync(
211+
nodePath.join(second, 'ticket.md'),
212+
'---\nid: XYZ789\ntype: feature\nphase: intake\nstatus: in_progress\n---\n',
213+
);
214+
const stamp = runStamp('--phase', 'define-behavior', '--ticket', TICKET_ID);
215+
expect(stamp.status).toBe(0);
216+
expect(readLog()).toContain(`review:${reviewScope(TICKET_ID, 'phase', 'define-behavior')}`);
217+
});
218+
207219
it('fails visibly instead of stamping unknown-session when no runtime identity is available', () => {
208220
const stamp = runStampWithoutRuntimeIdentity('spec');
209221

0 commit comments

Comments
 (0)