@marcomigozzi/trailhead 0.12.1 → 0.14.0
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.it.md +38 -13
- package/README.md +48 -13
- package/bin/lib/codex-projection.js +18 -11
- package/bin/lib/codex-projection.test.js +39 -11
- package/bin/lib/install-verify.js +10 -5
- package/bin/lib/install-verify.test.js +51 -0
- package/bin/trailhead.js +37 -25
- package/bin/trailhead.test.js +162 -5
- package/package.json +4 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
- package/plugins/trailhead/commands/panel.md +10 -0
- package/plugins/trailhead/hooks/hooks.json +3 -18
- package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
- package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
- package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
- package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
- package/plugins/trailhead/hooks/lib/shell-scan.js +496 -64
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +420 -0
- package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
- package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +252 -0
- package/plugins/trailhead/hooks/mod/README.md +187 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +453 -0
- package/plugins/trailhead/hooks/mod/map-pane.tsx +663 -0
- package/plugins/trailhead/hooks/mod/register.tsx +280 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
- package/plugins/trailhead/hooks/mod/work-state-view.ts +93 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
- package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
- package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
- package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-install-guard.js +22 -12
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +79 -7
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-search-guard.js +19 -9
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +2 -1
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
- package/plugins/trailhead/skills/_shared/choices.md +3 -1
- package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -4
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +15 -16
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -1
- package/plugins/trailhead/templates/trailhead-work-state.js +199 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +1374 -0
- package/plugins/trailhead/tests/map-pane-view.test.ts +565 -0
- package/plugins/trailhead/tests/mod.test.ts +14 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
- package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
- package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
- package/plugins/trailhead/tests/work-state-view.test.ts +86 -0
- package/plugins/trailhead/types/index.d.ts +69 -0
|
@@ -49,6 +49,21 @@
|
|
|
49
49
|
// stray keyword token in a genuinely non-write command from being
|
|
50
50
|
// misclassified as a write.
|
|
51
51
|
//
|
|
52
|
+
// Third follow-up fix (#219): the walk compared raw, still-quoted tokens, but bash
|
|
53
|
+
// removes quotes before `gh` runs, so `"gh" issue comment`, `gh 'issue' comment`,
|
|
54
|
+
// `gh issue com""ment`, `\gh ...` or `$'gh' ...` hid the write from both guards.
|
|
55
|
+
// Every token is now compared (and `sub`/`verb` returned) in its quote-removed
|
|
56
|
+
// form. This slightly over-detects (a quote inside a single-quoted word is
|
|
57
|
+
// dropped too), which is the safe direction for a guard.
|
|
58
|
+
//
|
|
59
|
+
// Fourth follow-up fix (#225): the verb was read as the token right after the
|
|
60
|
+
// subcommand, so a flag between them (`gh issue -R o/r comment 5`,
|
|
61
|
+
// `gh pr --repo=o/r edit 5`) made the verb the flag itself and hid the write
|
|
62
|
+
// from both guards. Flags after the subcommand are now skipped with the same
|
|
63
|
+
// arity model as the global walk (a glued `--flag=value` or a boolean global
|
|
64
|
+
// flag takes no value, any other flag consumes one following non-flag token)
|
|
65
|
+
// before the verb is read, in the primary walk and in the fail-safe fallback.
|
|
66
|
+
//
|
|
52
67
|
// Self-contained. No requires.
|
|
53
68
|
|
|
54
69
|
// gh's boolean global flags (take NO value). gh's only value-taking global
|
|
@@ -62,11 +77,83 @@ const BOOLEAN_GLOBAL_FLAGS = new Set(['-h', '--help', '--version']);
|
|
|
62
77
|
// below to recover from an unknown boolean flag swallowing the subcommand.
|
|
63
78
|
const WRITE_SUBCOMMANDS = new Set(['issue', 'pr', 'api']);
|
|
64
79
|
|
|
80
|
+
// Single-letter ANSI-C escapes (`$'\n'`).
|
|
81
|
+
const ANSI_C_SIMPLE = { n: '\n', t: '\t', r: '\r', a: '\x07', b: '\b', f: '\f', v: '\v', e: '\x1b' };
|
|
82
|
+
|
|
83
|
+
// Decode the escape at word[i] (the char after a backslash) inside a `$'...'` span:
|
|
84
|
+
// { out, next }. Unknown pairs keep the escaped char, so nothing is lost.
|
|
85
|
+
function ansiEscape(word, i) {
|
|
86
|
+
const n = word[i];
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(ANSI_C_SIMPLE, n)) return { out: ANSI_C_SIMPLE[n], next: i + 1 };
|
|
88
|
+
const rest = word.slice(i);
|
|
89
|
+
let m;
|
|
90
|
+
if ((m = /^x([0-9a-fA-F]{1,2})/.exec(rest)) || (m = /^u([0-9a-fA-F]{1,4})/.exec(rest)) || (m = /^U([0-9a-fA-F]{1,8})/.exec(rest))) {
|
|
91
|
+
const cp = parseInt(m[1], 16);
|
|
92
|
+
return { out: cp <= 0x10ffff ? String.fromCodePoint(cp) : m[0], next: i + m[0].length };
|
|
93
|
+
}
|
|
94
|
+
if ((m = /^[0-7]{1,3}/.exec(rest))) return { out: String.fromCharCode(parseInt(m[0], 8) & 0xff), next: i + m[0].length };
|
|
95
|
+
return { out: n, next: i + 1 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A word as bash passes it once quoting is removed: a `$'...'` span is decoded
|
|
99
|
+
// (\xHH, \NNN, \uHHHH, \n ...), `$` before a `"` is dropped, a backslash escape keeps
|
|
100
|
+
// the escaped char, quote characters are dropped (#219). Also returns, per output
|
|
101
|
+
// char, the id of the quoted span it came from (-1 for unquoted text), so a caller can
|
|
102
|
+
// tell `"-F x"` (one span, free text) from `-"F"/"p a"` (the flag, then a value).
|
|
103
|
+
function dequoteSpans(word) {
|
|
104
|
+
const w = String(word);
|
|
105
|
+
let text = '';
|
|
106
|
+
const spans = [];
|
|
107
|
+
const put = (str, id) => { text += str; for (let k = 0; k < str.length; k++) spans.push(id); };
|
|
108
|
+
let q = null; // the open quote: ' or "
|
|
109
|
+
let id = 0;
|
|
110
|
+
for (let i = 0; i < w.length; i++) {
|
|
111
|
+
const c = w[i];
|
|
112
|
+
if (q === "'") { if (c === "'") q = null; else put(c, id); continue; }
|
|
113
|
+
if (q === '"') {
|
|
114
|
+
if (c === '"') q = null;
|
|
115
|
+
else if (c === '\\' && i + 1 < w.length && '$`"\\\n'.includes(w[i + 1])) {
|
|
116
|
+
// bash: inside "..." a backslash only escapes $ ` " \ and newline (which vanishes)
|
|
117
|
+
i++;
|
|
118
|
+
if (w[i] !== '\n') put(w[i], id);
|
|
119
|
+
} else put(c, id);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c === '$' && w[i + 1] === "'") {
|
|
123
|
+
id++;
|
|
124
|
+
i += 2;
|
|
125
|
+
while (i < w.length && w[i] !== "'") {
|
|
126
|
+
if (w[i] === '\\' && i + 1 < w.length) { const e = ansiEscape(w, i + 1); put(e.out, id); i = e.next; } else put(w[i++], id);
|
|
127
|
+
}
|
|
128
|
+
} else if (c === '$' && w[i + 1] === '"') {
|
|
129
|
+
continue; // locale quoting: the `$` vanishes, the double-quoted span follows
|
|
130
|
+
} else if (c === '\\' && i + 1 < w.length) put(w[++i], -1);
|
|
131
|
+
else if (c === "'" || c === '"') { q = c; id++; }
|
|
132
|
+
else put(c, -1);
|
|
133
|
+
}
|
|
134
|
+
return { text, spans };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dequote = (word) => dequoteSpans(word).text;
|
|
138
|
+
|
|
139
|
+
// Skip the flags starting at toks[i] and return the index of the first
|
|
140
|
+
// non-flag token. A glued `--flag=value` or a BOOLEAN_GLOBAL_FLAGS member takes
|
|
141
|
+
// no value; any other flag consumes one following non-flag token (#153, #225).
|
|
142
|
+
function skipFlags(toks, i) {
|
|
143
|
+
while (i < toks.length && toks[i].startsWith('-')) {
|
|
144
|
+
const flag = toks[i];
|
|
145
|
+
i++; // the flag token itself
|
|
146
|
+
if (flag.includes('=') || BOOLEAN_GLOBAL_FLAGS.has(flag)) continue; // no separate value
|
|
147
|
+
if (i < toks.length && !toks[i].startsWith('-')) i++; // consume its value
|
|
148
|
+
}
|
|
149
|
+
return i;
|
|
150
|
+
}
|
|
151
|
+
|
|
65
152
|
// Parse a raw shell command string and locate the gh subcommand/verb.
|
|
66
153
|
// Returns { sub, verb } (verb may be undefined) or null when this is not a
|
|
67
154
|
// gh invocation at all.
|
|
68
155
|
function parseGhSubcommand(cmd) {
|
|
69
|
-
const toks = String(cmd).trim().split(/\s+/);
|
|
156
|
+
const toks = String(cmd).trim().split(/\s+/).map(dequote);
|
|
70
157
|
let i = 0;
|
|
71
158
|
while (i < toks.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])) i++; // env prefix
|
|
72
159
|
if (i >= toks.length || !/(^|\/)gh$/.test(toks[i])) return null; // the gh binary
|
|
@@ -75,14 +162,9 @@ function parseGhSubcommand(cmd) {
|
|
|
75
162
|
// Primary positional walk. -R/--repo (and any non-boolean, non-glued flag)
|
|
76
163
|
// is treated as value-taking: consume one following non-flag token so an
|
|
77
164
|
// unknown two-token global flag's value cannot shift the subcommand (#153).
|
|
78
|
-
|
|
79
|
-
const flag = toks[i];
|
|
80
|
-
i++; // the flag token itself
|
|
81
|
-
if (flag.includes('=') || BOOLEAN_GLOBAL_FLAGS.has(flag)) continue; // no separate value
|
|
82
|
-
if (i < toks.length && !toks[i].startsWith('-')) i++; // consume its value
|
|
83
|
-
}
|
|
165
|
+
i = skipFlags(toks, i);
|
|
84
166
|
let sub = toks[i];
|
|
85
|
-
let verb = toks[i + 1];
|
|
167
|
+
let verb = toks[skipFlags(toks, i + 1)];
|
|
86
168
|
// Fail-safe fallback: if the walk did NOT land on a recognized write
|
|
87
169
|
// subcommand, an unknown *boolean* flag before the subcommand may have
|
|
88
170
|
// consumed the subcommand itself as if it were the flag's value (the mirror
|
|
@@ -93,10 +175,10 @@ function parseGhSubcommand(cmd) {
|
|
|
93
175
|
// non-write command does not become a spurious write.
|
|
94
176
|
if (!WRITE_SUBCOMMANDS.has(sub)) {
|
|
95
177
|
for (let j = start; j < toks.length; j++) {
|
|
96
|
-
if (WRITE_SUBCOMMANDS.has(toks[j])) { sub = toks[j]; verb = toks[j + 1]; break; }
|
|
178
|
+
if (WRITE_SUBCOMMANDS.has(toks[j])) { sub = toks[j]; verb = toks[skipFlags(toks, j + 1)]; break; }
|
|
97
179
|
}
|
|
98
180
|
}
|
|
99
181
|
return { sub, verb };
|
|
100
182
|
}
|
|
101
183
|
|
|
102
|
-
module.exports = { parseGhSubcommand, BOOLEAN_GLOBAL_FLAGS, WRITE_SUBCOMMANDS };
|
|
184
|
+
module.exports = { parseGhSubcommand, dequote, dequoteSpans, BOOLEAN_GLOBAL_FLAGS, WRITE_SUBCOMMANDS };
|
|
@@ -96,4 +96,59 @@ ok('gh --foo --bar issue edit 5 resolves issue/edit (two unknown boolean flags)'
|
|
|
96
96
|
ok('gh repo view owner/repo resolves sub "repo" (not forced into a write keyword)',
|
|
97
97
|
(() => { const r = parseGhSubcommand('gh repo view owner/repo'); return r.sub === 'repo'; })());
|
|
98
98
|
|
|
99
|
+
// --- #219: a quoted or split word is the same word for bash ---
|
|
100
|
+
ok('"gh" issue comment resolves issue/comment',
|
|
101
|
+
(() => { const r = parseGhSubcommand('"gh" issue comment 5'); return r && r.sub === 'issue' && r.verb === 'comment'; })());
|
|
102
|
+
ok("gh 'issue' comment resolves issue/comment",
|
|
103
|
+
(() => { const r = parseGhSubcommand("gh 'issue' comment 5"); return r.sub === 'issue' && r.verb === 'comment'; })());
|
|
104
|
+
ok('gh issue com""ment resolves the verb comment',
|
|
105
|
+
(() => { const r = parseGhSubcommand('gh issue com""ment 5'); return r.sub === 'issue' && r.verb === 'comment'; })());
|
|
106
|
+
ok('"/usr/bin/gh" api resolves api',
|
|
107
|
+
(() => { const r = parseGhSubcommand('"/usr/bin/gh" api x'); return r && r.sub === 'api'; })());
|
|
108
|
+
ok('\\gh and g"h" and $\'gh\' are the gh binary',
|
|
109
|
+
['\\gh issue edit 5', 'g"h" issue edit 5', "$'gh' issue edit 5"].every((c) => { const r = parseGhSubcommand(c); return r && r.sub === 'issue' && r.verb === 'edit'; }));
|
|
110
|
+
ok('gh is\\sue comment resolves issue',
|
|
111
|
+
(() => { const r = parseGhSubcommand('gh is\\sue comment 5'); return r.sub === 'issue' && r.verb === 'comment'; })());
|
|
112
|
+
ok('"ghx" issue is not the gh binary', parseGhSubcommand('"ghx" issue comment 5') === null);
|
|
113
|
+
ok("gh 'repo' view still resolves repo",
|
|
114
|
+
(() => { const r = parseGhSubcommand("gh 'repo' view"); return r.sub === 'repo' && r.verb === 'view'; })());
|
|
115
|
+
|
|
116
|
+
// --- #219: dequote decodes ANSI-C spans and drops quotes and escapes ---
|
|
117
|
+
{
|
|
118
|
+
const { dequote } = require('./gh-subcommand.js');
|
|
119
|
+
ok('dequote drops quote characters', dequote(`a'b'"c"`) === 'abc');
|
|
120
|
+
ok('dequote keeps an escaped char', dequote('\\gh') === 'gh');
|
|
121
|
+
ok('dequote decodes a hex escape', dequote(`$'\\x67h'`) === 'gh');
|
|
122
|
+
ok('dequote decodes an octal escape', dequote(`$'\\151ssue'`) === 'issue');
|
|
123
|
+
ok('dequote decodes a unicode escape', dequote(`$'\\u0067h'`) === 'gh');
|
|
124
|
+
ok('dequote decodes single-letter escapes', dequote(`$'a\\tb\\\\c\\'d'`) === "a\tb\\c'd");
|
|
125
|
+
ok('dequote keeps a backslash that bash keeps inside double quotes', dequote('"\\nn"') === '\\nn');
|
|
126
|
+
ok('dequote drops the backslash of \\$ inside double quotes', dequote('"\\$x"') === '$x');
|
|
127
|
+
ok('dequote drops a backslash-newline inside double quotes', dequote('"a\\\nb"') === 'ab');
|
|
128
|
+
ok('dequote keeps a backslash literal inside single quotes', dequote("'a\\b'") === 'a\\b');
|
|
129
|
+
ok('dequote drops the $ of a locale-quoted span', dequote('$"gh"') === 'gh');
|
|
130
|
+
ok('dequote leaves a bare variable alone', dequote('$HOME') === '$HOME');
|
|
131
|
+
const { dequoteSpans } = require('./gh-subcommand.js');
|
|
132
|
+
ok('dequoteSpans tags each char with its quoted span', JSON.stringify(dequoteSpans(`-"F"/"p a"`)) === JSON.stringify({ text: '-F/p a', spans: [-1, 1, -1, 2, 2, 2] }));
|
|
133
|
+
ok('dequoteSpans keeps one span for "-F handling"', dequoteSpans('"-F x"').spans.every((x) => x === 1));
|
|
134
|
+
ok('body-sources re-exports the same dequote', require('./body-sources.js').dequote === dequote);
|
|
135
|
+
const { flagValues } = require('./body-sources.js');
|
|
136
|
+
const J = JSON.stringify;
|
|
137
|
+
ok("flagValues sees a quoted long flag", J(flagValues(["'--body-file'", 'f'], '--body-file', '-F')) === J(['f']));
|
|
138
|
+
ok('flagValues sees a split long flag', J(flagValues(['--body-fi""le', 'f'], '--body-file', '-F')) === J(['f']));
|
|
139
|
+
ok('flagValues sees a quoted short flag', J(flagValues([`"-F"`, 'f'], '--body-file', '-F')) === J(['f']));
|
|
140
|
+
ok('flagValues sees a fully quoted --flag=value', J(flagValues(['"--body-file=/p"'], '--body-file', '-F')) === J(['/p']));
|
|
141
|
+
ok('flagValues keeps a raw value untouched', J(flagValues(['--body', `'a b'`], '--body', '-b')) === J([`'a b'`]));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- #225: a flag between the subcommand and the verb is skipped ---
|
|
145
|
+
{
|
|
146
|
+
const r = (c) => { const x = parseGhSubcommand(c); return x.sub + '/' + x.verb; };
|
|
147
|
+
ok('gh issue -R o/r comment 5 resolves issue/comment', r('gh issue -R o/r comment 5') === 'issue/comment');
|
|
148
|
+
ok('gh pr --repo o/r edit 5 resolves pr/edit', r('gh pr --repo o/r edit 5') === 'pr/edit');
|
|
149
|
+
ok('gh issue --repo=o/r comment 5 resolves issue/comment', r('gh issue --repo=o/r comment 5') === 'issue/comment');
|
|
150
|
+
ok('gh -R a/b issue -R o/r edit 5 resolves issue/edit', r('gh -R a/b issue -R o/r edit 5') === 'issue/edit');
|
|
151
|
+
ok('gh issue --help comment: boolean flag takes no value', r('gh issue --help comment') === 'issue/comment');
|
|
152
|
+
}
|
|
153
|
+
|
|
99
154
|
console.log(`✓ gh-subcommand: ${passed} assertions passed`);
|
|
@@ -25,12 +25,28 @@ function questionText(q) {
|
|
|
25
25
|
return [q.question, q.header, ...labels].filter((s) => typeof s === 'string').join('\n');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const questionsOf = (input) => ((input && Array.isArray(input.questions)) ? input.questions : []);
|
|
29
|
+
|
|
30
|
+
// Is this single question itself a mockup-approval ask?
|
|
31
|
+
function isApprovalQuestion(q) {
|
|
32
|
+
const t = q ? questionText(q) : '';
|
|
33
|
+
return MOCKUP_WORD.test(t) && APPROVAL_WORD.test(t);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
// AskUserQuestion input: does any single question ask to approve a mockup?
|
|
29
37
|
function isMockupApprovalAsk(input) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
return questionsOf(input).some(isApprovalQuestion);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Does a mockup-approval question carry the link itself (question, header,
|
|
42
|
+
// option label or description)? A link in an unrelated question of the same
|
|
43
|
+
// ask does not count.
|
|
44
|
+
function askHasLink(input) {
|
|
45
|
+
return questionsOf(input).some((q) => {
|
|
46
|
+
if (!isApprovalQuestion(q)) return false;
|
|
47
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
48
|
+
const parts = [q.question, q.header, ...opts.flatMap((o) => [o && o.label, o && o.description])];
|
|
49
|
+
return parts.some(hasLink);
|
|
34
50
|
});
|
|
35
51
|
}
|
|
36
52
|
|
|
@@ -47,4 +63,4 @@ const BLOCK_REASON =
|
|
|
47
63
|
'screen/variant) in the plain prose immediately before the question, not the project ' +
|
|
48
64
|
'name, then ask again. See "Surface the mockup" in _shared/techniques/prototype.md.';
|
|
49
65
|
|
|
50
|
-
module.exports = { hasLink, isMockupApprovalAsk, isMockupApprovalMessage, BLOCK_REASON };
|
|
66
|
+
module.exports = { hasLink, isMockupApprovalAsk, askHasLink, isMockupApprovalMessage, BLOCK_REASON };
|