@mmerterden/multi-agent-pipeline 10.9.0 → 11.0.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/CHANGELOG.md +170 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/channels.md +42 -6
- package/pipeline/commands/multi-agent/finish.md +15 -6
- package/pipeline/commands/multi-agent/generate.md +34 -0
- package/pipeline/commands/multi-agent/help.md +10 -0
- package/pipeline/commands/multi-agent/manual-test.md +9 -2
- package/pipeline/commands/multi-agent/refs/channels/pr.md +1 -1
- package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -6
- package/pipeline/commands/multi-agent/refs/generate-issue.md +267 -0
- package/pipeline/commands/multi-agent/refs/keychain.md +12 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +31 -2
- package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +2 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +2 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +6 -1
- package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +5 -1
- package/pipeline/commands/multi-agent/refs/progress-contract.md +1 -0
- package/pipeline/commands/multi-agent/refs/tracker-contract.md +56 -13
- package/pipeline/commands/multi-agent/resume.md +6 -2
- package/pipeline/commands/multi-agent/sync.md +9 -9
- package/pipeline/commands/multi-agent.md +2 -0
- package/pipeline/lib/figma-mcp-refresh.sh +79 -0
- package/pipeline/preferences-template.json +2 -1
- package/pipeline/schemas/prefs.schema.json +11 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
- package/pipeline/scripts/phase-tracker.sh +134 -22
- package/pipeline/scripts/smoke-channels-approval-gate.sh +60 -0
- package/pipeline/scripts/smoke-generate-issue.sh +119 -0
- package/pipeline/scripts/smoke-phase-tracker.sh +69 -0
- package/pipeline/scripts/smoke-progress-contract.sh +9 -0
- package/pipeline/scripts/smoke-tasklist-ordering.sh +1 -0
- package/pipeline/scripts/smoke-token-preflight.sh +82 -0
- package/pipeline/scripts/smoke-tracker-contract.sh +62 -0
- package/pipeline/skills/.skills-index.json +13 -4
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-generate/SKILL.md +51 -0
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +5 -5
- package/pipeline/skills/skills-index.md +4 -3
|
@@ -77,6 +77,7 @@ Lib scripts (`~/.claude/lib/`):
|
|
|
77
77
|
| `analysis ["<feature>"]` | Standalone feature-spec analizi: Figma / Swagger / Confluence / Jira / repo girdileri sabit 7-bölümlük şablona dökülür, humanizer'dan geçer, Local/Confluence/Jira hedef(ler)ine post edilir. Worktree veya commit yok. Dev'e zincirleme yok. |
|
|
78
78
|
| `build-optimize` | iOS-only Xcode build performance wrapper. Vendored `xcode-build-orchestrator`'a dispatch eder; benchmark + compilation / project / SPM analyzer'lar + recommend-first plan `.build-benchmark/optimization-plan.md`. Non-iOS stack'lerde fail-fast. |
|
|
79
79
|
| `channels [PR-url\|#N\|Jira-url\|Jira-id] [--channels pr,jira,confluence,wiki] [--content normal,test,auto-diff,note] [--message "..."]` | Post task report to multi-select channels (PR description, Jira comment, Confluence page, Wiki pages) with multi-select content sources. Humanizer pass per-channel. Bitbucket PR updates use reviewer-preserving PUT. Phase 7 delegates to this command; also invocable post-hoc for fixes made outside the pipeline. No worktree. |
|
|
80
|
+
| `generate ["desc"] [figma-url] [swagger-url]` | Create a standards-compliant Jira issue: asks the type (**Task** / **Bug** / **Story**), mines the project's recent same-type issues for conventions (summary format, labels, priority, test-scenario style), detects the active sprint, drafts from a standard template with auto-sizing sections (Design Reference / API Contract / Screenshots appear only when their source is given), asks about unknown fields, then full draft preview + explicit approval before create. No worktree, no commits. |
|
|
80
81
|
| `test` or `test [args]` | UI Bug Hunter - screenshot + tap + analyze on booted simulator via MCP (read `$HOME/.claude/commands/sim-test.md`). `/multi-agent:test` also resolves here via the `commands/multi-agent/test.md` delegate. |
|
|
81
82
|
| `manual-test [#id]` | Phase 5 standalone Manual Test - checks out the task branch, prints Xcode / SourceTree hints, waits for user verdict (`ok` / `fix: ...`). |
|
|
82
83
|
| `stack [ios\|android\|backend\|mobile\|all]` | Swap skills for next conversation. No arg = show current stack. |
|
|
@@ -116,6 +117,7 @@ This command uses lazy loading for token efficiency. Read the relevant sub-file
|
|
|
116
117
|
| `local-autopilot` | `$HOME/.claude/commands/multi-agent/local-autopilot.md` |
|
|
117
118
|
| `dev-local` | `$HOME/.claude/commands/multi-agent/dev-local.md` |
|
|
118
119
|
| `dev-local-autopilot` | `$HOME/.claude/commands/multi-agent/dev-local-autopilot.md` |
|
|
120
|
+
| `generate` | `$HOME/.claude/commands/multi-agent/generate.md` (loads `refs/generate-issue.md`) |
|
|
119
121
|
| `stack` | `$HOME/.claude/commands/multi-agent/stack.md` |
|
|
120
122
|
| `language` | Handled inline - set/show prompt language in preferences |
|
|
121
123
|
| SwiftUI component task (iOS) | `$HOME/.claude/commands/multi-agent/refs/swiftui-guide.md` |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# figma-mcp-refresh.sh - silent Figma MCP (figu_) token renewal via OAuth refresh grant.
|
|
3
|
+
#
|
|
4
|
+
# Used by the Phase 0 token pre-flight (refs/phases/phase-0-init.md Step 0.7) and the
|
|
5
|
+
# expired-token decision (refs/keychain.md). Never prints token values - status lines only.
|
|
6
|
+
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. Resolve the access-token Keychain name from prefs.global.keychainMapping.figma_mcp.
|
|
9
|
+
# 2. Read the refresh token from "<name>_Refresh" (written by the user's generation script).
|
|
10
|
+
# 3. Read client credentials from .figma-oauth.json next to prefs.global.tokenScripts.figma_mcp.
|
|
11
|
+
# 4. POST grant_type=refresh_token to the Figma token endpoint.
|
|
12
|
+
# 5. On success save the new access token (and rotated refresh token) back to the Keychain.
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 renewed (access token replaced in Keychain)
|
|
16
|
+
# 1 refresh grant rejected by Figma (refresh token dead - run the generation script)
|
|
17
|
+
# 2 prerequisites missing (no mapping / no refresh entry / no oauth config)
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
|
|
22
|
+
TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"
|
|
23
|
+
ACCOUNT="${USER:-$(whoami)}"
|
|
24
|
+
|
|
25
|
+
say() { printf '%s\n' "$*"; }
|
|
26
|
+
|
|
27
|
+
[ -f "$PREFS_FILE" ] || { say "figma-mcp-refresh: prefs file missing ($PREFS_FILE)"; exit 2; }
|
|
28
|
+
|
|
29
|
+
KEY_NAME=$(jq -r '.global.keychainMapping.figma_mcp // empty' "$PREFS_FILE")
|
|
30
|
+
[ -n "$KEY_NAME" ] || { say "figma-mcp-refresh: keychainMapping.figma_mcp not set"; exit 2; }
|
|
31
|
+
|
|
32
|
+
REFRESH_KEY="${KEY_NAME}_Refresh"
|
|
33
|
+
REFRESH_TOKEN=$(security find-generic-password -s "$REFRESH_KEY" -w 2>/dev/null || true)
|
|
34
|
+
[ -n "$REFRESH_TOKEN" ] || { say "figma-mcp-refresh: no refresh token at '$REFRESH_KEY'"; exit 2; }
|
|
35
|
+
|
|
36
|
+
SCRIPT_PATH=$(jq -r '.global.tokenScripts.figma_mcp // empty' "$PREFS_FILE")
|
|
37
|
+
OAUTH_CONFIG=""
|
|
38
|
+
if [ -n "$SCRIPT_PATH" ]; then
|
|
39
|
+
OAUTH_CONFIG="$(dirname "$SCRIPT_PATH")/.figma-oauth.json"
|
|
40
|
+
fi
|
|
41
|
+
[ -n "$OAUTH_CONFIG" ] && [ -f "$OAUTH_CONFIG" ] || { say "figma-mcp-refresh: .figma-oauth.json not found (tokenScripts.figma_mcp unset or sibling config missing)"; exit 2; }
|
|
42
|
+
|
|
43
|
+
CLIENT_ID=$(jq -r '.client_id // empty' "$OAUTH_CONFIG")
|
|
44
|
+
CLIENT_SECRET=$(jq -r '.client_secret // empty' "$OAUTH_CONFIG")
|
|
45
|
+
[ -n "$CLIENT_ID" ] || { say "figma-mcp-refresh: client_id missing in .figma-oauth.json"; exit 2; }
|
|
46
|
+
|
|
47
|
+
CURL_ARGS=(
|
|
48
|
+
--data-urlencode "grant_type=refresh_token"
|
|
49
|
+
--data-urlencode "refresh_token=${REFRESH_TOKEN}"
|
|
50
|
+
--data-urlencode "client_id=${CLIENT_ID}"
|
|
51
|
+
)
|
|
52
|
+
[ -n "$CLIENT_SECRET" ] && CURL_ARGS+=( --data-urlencode "client_secret=${CLIENT_SECRET}" )
|
|
53
|
+
|
|
54
|
+
RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
|
|
55
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
56
|
+
"${CURL_ARGS[@]}")
|
|
57
|
+
|
|
58
|
+
NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
|
|
59
|
+
NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
|
|
60
|
+
EXPIRES_IN=$(printf '%s' "$RESPONSE" | jq -r '.expires_in // "?"' 2>/dev/null || true)
|
|
61
|
+
|
|
62
|
+
if [ -z "$NEW_ACCESS" ]; then
|
|
63
|
+
ERR=$(printf '%s' "$RESPONSE" | jq -r '.error // .message // "unknown"' 2>/dev/null || echo "unknown")
|
|
64
|
+
say "figma-mcp-refresh: refresh grant rejected ($ERR) - regenerate via tokenScripts.figma_mcp"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Save new access token first, then the rotated refresh token - order matters so a
|
|
69
|
+
# mid-write failure never leaves us with a lost refresh token AND a stale access token.
|
|
70
|
+
security delete-generic-password -s "$KEY_NAME" -a "$ACCOUNT" >/dev/null 2>&1 || true
|
|
71
|
+
security add-generic-password -s "$KEY_NAME" -a "$ACCOUNT" -w "$NEW_ACCESS" -T "" -U
|
|
72
|
+
|
|
73
|
+
if [ -n "$NEW_REFRESH" ]; then
|
|
74
|
+
security delete-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" >/dev/null 2>&1 || true
|
|
75
|
+
security add-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" -w "$NEW_REFRESH" -T "" -U
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
say "figma-mcp-refresh: renewed '$KEY_NAME' (expires_in=${EXPIRES_IN}s, refresh $( [ -n "$NEW_REFRESH" ] && echo rotated || echo unchanged ))"
|
|
79
|
+
exit 0
|
|
@@ -179,6 +179,17 @@
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
},
|
|
182
|
+
"tokenScripts": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"additionalProperties": {
|
|
185
|
+
"type": [
|
|
186
|
+
"string",
|
|
187
|
+
"null"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
"default": {},
|
|
191
|
+
"description": "Maps service ids to user-owned token generation scripts (absolute paths, personal - never synced). Used by the Phase 0 token pre-flight: figma_mcp silent renewal reads the .figma-oauth.json next to tokenScripts.figma_mcp; the 'Regenerate now (script)' option runs the script itself."
|
|
192
|
+
},
|
|
182
193
|
"platformIdentityRouting": {
|
|
183
194
|
"type": "object",
|
|
184
195
|
"description": "v2.1.0+. Maps repo URL glob patterns to identity names. Used in Phase 0 to auto-select git identity + PAT based on repo origin. Prevents corporate identity leaking into personal repos and vice versa.",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
.claude/CLAUDE.md 1
|
|
2
2
|
.claude/agents 8
|
|
3
|
-
.claude/commands
|
|
4
|
-
.claude/lib
|
|
3
|
+
.claude/commands 92
|
|
4
|
+
.claude/lib 24
|
|
5
5
|
.claude/multi-agent-preferences.json 1
|
|
6
6
|
.claude/rules 12
|
|
7
7
|
.claude/schemas 23
|
|
8
|
-
.claude/scripts
|
|
8
|
+
.claude/scripts 174
|
|
9
9
|
.claude/settings.json 1
|
|
10
10
|
.claude/skills 560
|
|
11
11
|
.copilot/agents 8
|
|
12
12
|
.copilot/copilot-instructions.md 1
|
|
13
|
-
.copilot/lib
|
|
13
|
+
.copilot/lib 24
|
|
14
14
|
.copilot/schemas 23
|
|
15
|
-
.copilot/scripts
|
|
16
|
-
.copilot/skills
|
|
15
|
+
.copilot/scripts 174
|
|
16
|
+
.copilot/skills 598
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
#
|
|
15
15
|
# Commands:
|
|
16
16
|
# init <task_id> Reset tracker for this task
|
|
17
|
-
# add <phase_id> "<name>" Register a phase (status=pending)
|
|
17
|
+
# add <phase_id> "<name>" Register a phase (status=pending); idempotent - existing id is a no-op
|
|
18
18
|
# update <phase_id> <status> Change status + re-render
|
|
19
19
|
# sub <phase_id> <sub_id> "<name>" [status] Add or update a sub-phase
|
|
20
|
-
# tokens <phase_id> <in> <out>
|
|
20
|
+
# tokens <phase_id> <in> <out> [cached] Add to phase's token totals (in = fresh input, cache-exclusive)
|
|
21
21
|
# model <phase_id> <model_name> Tag phase with the LLM that produced its tokens (v8.3.0)
|
|
22
22
|
# meta <phase_id> <key> <value> Set phase context (account, repos, files, now, ...)
|
|
23
|
+
# now <phase_id> "<text>" Set the phase's live current-action line (quiet, 60-char cap)
|
|
24
|
+
# cost <phase_id>|total Print est. USD for one phase or the whole run ("-" when unpriceable)
|
|
23
25
|
# render Print current snapshot, no change
|
|
24
26
|
#
|
|
25
27
|
# Status enum: pending | in_progress | completed | failed | skipped
|
|
@@ -57,8 +59,11 @@ usage:
|
|
|
57
59
|
phase-tracker.sh add <phase_id> "<name>"
|
|
58
60
|
phase-tracker.sh update <phase_id> <pending|in_progress|completed|failed|skipped>
|
|
59
61
|
phase-tracker.sh sub <phase_id> <sub_id> "<name>" [status]
|
|
60
|
-
phase-tracker.sh tokens <phase_id> <in> <out>
|
|
62
|
+
phase-tracker.sh tokens <phase_id> <in> <out> [cached]
|
|
63
|
+
phase-tracker.sh model <phase_id> <model_name>
|
|
61
64
|
phase-tracker.sh meta <phase_id> <key> <value>
|
|
65
|
+
phase-tracker.sh now <phase_id> "<text>"
|
|
66
|
+
phase-tracker.sh cost <phase_id>|total
|
|
62
67
|
phase-tracker.sh render
|
|
63
68
|
USAGE
|
|
64
69
|
exit 64
|
|
@@ -239,6 +244,37 @@ format_elapsed() {
|
|
|
239
244
|
fi
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
# Cost table ships next to this script in every install tree (repo,
|
|
248
|
+
# ~/.claude/scripts, ~/.copilot/scripts). Pricing math matches
|
|
249
|
+
# render-agent-log-cost.sh exactly: tokens_in is FRESH input (cache-exclusive,
|
|
250
|
+
# per the disjoint token-count contract), tokens_cached is priced at the
|
|
251
|
+
# discounted cacheReadPerMtok rate, floored to cents. A missing/unparseable
|
|
252
|
+
# table never fails a caller - USD simply renders empty / "-".
|
|
253
|
+
COST_TABLE="$(cd "$(dirname "$0")" && pwd)/cost-table.json"
|
|
254
|
+
|
|
255
|
+
# Print est. USD ("0.21") for one phase index or, with "total", the sum across
|
|
256
|
+
# priceable phases. Prints "-" when nothing is priceable. Never non-zero exit.
|
|
257
|
+
phase_usd() {
|
|
258
|
+
local selector="$1" # integer phase index into .phases[], or "total"
|
|
259
|
+
[ -f "$COST_TABLE" ] || { echo "-"; return 0; }
|
|
260
|
+
local state; state="$(load_state)"
|
|
261
|
+
echo "$state" | jq -r --slurpfile prices "$COST_TABLE" --arg sel "$selector" '
|
|
262
|
+
def usd_of(p): ($prices[0].prices[p.model // ""] // null) as $rate |
|
|
263
|
+
if $rate == null then null
|
|
264
|
+
else ( ((p.tokens_in // 0) / 1000000) * $rate.inPerMtok
|
|
265
|
+
+ ((p.tokens_cached // 0) / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
|
|
266
|
+
+ ((p.tokens_out // 0) / 1000000) * $rate.outPerMtok )
|
|
267
|
+
end;
|
|
268
|
+
def floor_cents(u): if u == null then null else ((u * 100) | floor) / 100 end;
|
|
269
|
+
if $sel == "total" then
|
|
270
|
+
([.phases[] | usd_of(.) | select(. != null)] | if length == 0 then "-" else (add | floor_cents(.) | tostring) end)
|
|
271
|
+
else
|
|
272
|
+
(.phases[($sel | tonumber)] // null) as $p |
|
|
273
|
+
if $p == null then "-" else (usd_of($p) | floor_cents(.) | if . == null then "-" else tostring end) end
|
|
274
|
+
end
|
|
275
|
+
' 2>/dev/null || echo "-"
|
|
276
|
+
}
|
|
277
|
+
|
|
242
278
|
# Read state, atomically write it back. Atomic = write to .tmp, rename.
|
|
243
279
|
load_state() {
|
|
244
280
|
if [ ! -f "$TRACKER_FILE" ]; then
|
|
@@ -272,10 +308,25 @@ render() {
|
|
|
272
308
|
# consecutive tabs (tab is in the default whitespace IFS class), which
|
|
273
309
|
# silently drops empty middle columns and shifts later fields.
|
|
274
310
|
# Column order: status, started_at, completed_at, id, name,
|
|
275
|
-
# tokens_in, tokens_out, sub_count, meta_keys_csv
|
|
311
|
+
# tokens_in, tokens_out, sub_count, meta_keys_csv, usd
|
|
312
|
+
# The usd column is computed in the same pass with the shared pricing math;
|
|
313
|
+
# a missing/unparseable cost table yields empty usd cells, never a failure.
|
|
314
|
+
local prices_json='{"prices":{}}'
|
|
315
|
+
if [ -f "$COST_TABLE" ]; then
|
|
316
|
+
prices_json=$(cat "$COST_TABLE" 2>/dev/null) || prices_json='{"prices":{}}'
|
|
317
|
+
echo "$prices_json" | jq empty 2>/dev/null || prices_json='{"prices":{}}'
|
|
318
|
+
fi
|
|
276
319
|
local phase_data
|
|
277
|
-
phase_data=$(echo "$state" | jq -r '
|
|
278
|
-
.
|
|
320
|
+
phase_data=$(echo "$state" | jq -r --argjson prices "$prices_json" '
|
|
321
|
+
def usd_of(p): ($prices.prices[p.model // ""] // null) as $rate |
|
|
322
|
+
if $rate == null then null
|
|
323
|
+
else ( ((p.tokens_in // 0) / 1000000) * $rate.inPerMtok
|
|
324
|
+
+ ((p.tokens_cached // 0) / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
|
|
325
|
+
+ ((p.tokens_out // 0) / 1000000) * $rate.outPerMtok )
|
|
326
|
+
end;
|
|
327
|
+
.phases // []
|
|
328
|
+
| sort_by(.id | (tonumber? // 9999))
|
|
329
|
+
| .[] | [
|
|
279
330
|
(.status // ""),
|
|
280
331
|
(.started_at // ""),
|
|
281
332
|
(.completed_at // ""),
|
|
@@ -284,20 +335,21 @@ render() {
|
|
|
284
335
|
((.tokens_in // 0) | tostring),
|
|
285
336
|
((.tokens_out // 0) | tostring),
|
|
286
337
|
((.subs // []) | length | tostring),
|
|
287
|
-
((.meta // {}) | keys_unsorted | join(","))
|
|
338
|
+
((.meta // {}) | keys_unsorted | join(",")),
|
|
339
|
+
(usd_of(.) | if . == null then "" else (((. * 100) | floor) / 100 | tostring) end)
|
|
288
340
|
] | join("")
|
|
289
341
|
')
|
|
290
342
|
|
|
291
343
|
local phase_count=0
|
|
292
344
|
local -a row_status=() row_started=() row_completed=() row_id=() row_name=()
|
|
293
|
-
local -a row_tin=() row_tout=() row_subcount=() row_metakeys=()
|
|
345
|
+
local -a row_tin=() row_tout=() row_subcount=() row_metakeys=() row_usd=()
|
|
294
346
|
if [ -n "$phase_data" ]; then
|
|
295
|
-
local _s _st _ed _id _nm _ti _to _sc _mk
|
|
296
|
-
while IFS=$'\x1f' read -r _s _st _ed _id _nm _ti _to _sc _mk; do
|
|
347
|
+
local _s _st _ed _id _nm _ti _to _sc _mk _us
|
|
348
|
+
while IFS=$'\x1f' read -r _s _st _ed _id _nm _ti _to _sc _mk _us; do
|
|
297
349
|
row_status+=("$_s"); row_started+=("$_st"); row_completed+=("$_ed")
|
|
298
350
|
row_id+=("$_id"); row_name+=("$_nm")
|
|
299
351
|
row_tin+=("$_ti"); row_tout+=("$_to")
|
|
300
|
-
row_subcount+=("$_sc"); row_metakeys+=("$_mk")
|
|
352
|
+
row_subcount+=("$_sc"); row_metakeys+=("$_mk"); row_usd+=("$_us")
|
|
301
353
|
phase_count=$((phase_count + 1))
|
|
302
354
|
done <<< "$phase_data"
|
|
303
355
|
fi
|
|
@@ -367,6 +419,16 @@ render() {
|
|
|
367
419
|
local tok_label=""
|
|
368
420
|
[ "$p_ttotal" -gt 0 ] && tok_label="$(format_tokens $p_ttotal) tok"
|
|
369
421
|
|
|
422
|
+
# Est. USD suffix - only when the phase has a priced model AND tokens.
|
|
423
|
+
# "0" (floored below one cent) still renders as ~$0.00 so a priced phase
|
|
424
|
+
# is visibly priced; an unpriceable phase shows no USD at all.
|
|
425
|
+
local p_usd="${row_usd[$i]}"
|
|
426
|
+
local usd_label=""
|
|
427
|
+
if [ -n "$p_usd" ] && [ "$p_ttotal" -gt 0 ]; then
|
|
428
|
+
usd_label=$(printf '~$%.2f' "$p_usd" 2>/dev/null) || usd_label=""
|
|
429
|
+
fi
|
|
430
|
+
[ -n "$usd_label" ] && [ -n "$tok_label" ] && tok_label="${tok_label} · ${usd_label}"
|
|
431
|
+
|
|
370
432
|
local right_block=""
|
|
371
433
|
if [ -n "$el_label" ] && [ -n "$tok_label" ]; then
|
|
372
434
|
right_block=$(printf "%b%s%b ${C_DIM}·${C_RESET} %b%s%b" "$el_color" "$el_label" "$C_RESET" "$C_DIM" "$tok_label" "$C_RESET")
|
|
@@ -385,8 +447,8 @@ render() {
|
|
|
385
447
|
# Same Unit Separator rationale as the phase batch above.
|
|
386
448
|
if [ "$pstatus" = "in_progress" ] && [ -n "${row_metakeys[$i]}" ]; then
|
|
387
449
|
local meta_data key val
|
|
388
|
-
meta_data=$(echo "$state" | jq -r --
|
|
389
|
-
.phases[$
|
|
450
|
+
meta_data=$(echo "$state" | jq -r --arg pid "$pid" '
|
|
451
|
+
([.phases[] | select(.id == $pid)][0].meta) // {} | to_entries[] | [.key, (.value | tostring)] | join("")
|
|
390
452
|
')
|
|
391
453
|
while IFS=$'\x1f' read -r key val; do
|
|
392
454
|
[ -z "$key" ] && continue
|
|
@@ -399,8 +461,8 @@ render() {
|
|
|
399
461
|
local sub_count="${row_subcount[$i]}"
|
|
400
462
|
if [ "$sub_count" != "0" ] && [ "$sub_count" -gt 0 ] 2>/dev/null; then
|
|
401
463
|
local sub_data
|
|
402
|
-
sub_data=$(echo "$state" | jq -r --
|
|
403
|
-
.phases[$
|
|
464
|
+
sub_data=$(echo "$state" | jq -r --arg pid "$pid" '
|
|
465
|
+
([.phases[] | select(.id == $pid)][0].subs) // [] | .[] | [(.id // ""), (.name // ""), (.status // "")] | join("")
|
|
404
466
|
')
|
|
405
467
|
local j=0
|
|
406
468
|
local sid sname sstatus sg connector
|
|
@@ -417,14 +479,22 @@ render() {
|
|
|
417
479
|
done
|
|
418
480
|
fi
|
|
419
481
|
|
|
420
|
-
# Total
|
|
421
|
-
local total_tokens
|
|
482
|
+
# Total footer - tokens + est. USD (priceable phases) + cached tokens.
|
|
483
|
+
local total_tokens total_cached
|
|
422
484
|
total_tokens=$(echo "$state" | jq '[.phases[] | ((.tokens_in // 0) + (.tokens_out // 0))] | add // 0')
|
|
485
|
+
total_cached=$(echo "$state" | jq '[.phases[] | (.tokens_cached // 0)] | add // 0')
|
|
423
486
|
if [ "$total_tokens" -gt 0 ]; then
|
|
424
|
-
local total_label
|
|
487
|
+
local total_label total_usd total_suffix=""
|
|
425
488
|
total_label=$(format_tokens "$total_tokens")
|
|
489
|
+
total_usd=$(phase_usd total)
|
|
490
|
+
if [ -n "$total_usd" ] && [ "$total_usd" != "-" ]; then
|
|
491
|
+
total_suffix=$(printf ' · ~$%.2f' "$total_usd" 2>/dev/null) || total_suffix=""
|
|
492
|
+
fi
|
|
493
|
+
if [ "$total_cached" -gt 0 ] 2>/dev/null; then
|
|
494
|
+
total_suffix="${total_suffix} ($(format_tokens "$total_cached") cached)"
|
|
495
|
+
fi
|
|
426
496
|
printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_DIM}%s${C_RESET}\n" "$(printf '%*s' 56 '' | tr ' ' '─')"
|
|
427
|
-
printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_BOLD}Total${C_RESET} ${C_BOLD}${C_CYAN}%s tok${C_RESET}\n" "$total_label"
|
|
497
|
+
printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_BOLD}Total${C_RESET} ${C_BOLD}${C_CYAN}%s tok%s${C_RESET}\n" "$total_label" "$total_suffix"
|
|
428
498
|
fi
|
|
429
499
|
|
|
430
500
|
printf "${C_BOLD}${C_BLUE}╰────────────────────────────────────────────────────────────╯${C_RESET}\n"
|
|
@@ -459,8 +529,14 @@ case "$ACTION" in
|
|
|
459
529
|
[ "$#" -ge 2 ] || { echo "add needs <phase_id> <name>" >&2; exit 64; }
|
|
460
530
|
PID="$1"; PNAME="$2"
|
|
461
531
|
state=$(load_state)
|
|
462
|
-
|
|
463
|
-
|
|
532
|
+
# Idempotent: a phase id that already exists is a no-op (name, status, and
|
|
533
|
+
# token history are preserved). This is what lets continuation commands
|
|
534
|
+
# (/multi-agent:finish, resume) re-declare their phase set against a
|
|
535
|
+
# pre-existing tracker without duplicating or resetting tiles.
|
|
536
|
+
new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" '
|
|
537
|
+
if any(.phases[]?; .id == $id) then .
|
|
538
|
+
else .phases += [{"id":$id,"name":$name,"status":"pending","subs":[]}]
|
|
539
|
+
end')
|
|
464
540
|
save_state "$new"
|
|
465
541
|
render
|
|
466
542
|
;;
|
|
@@ -581,12 +657,48 @@ case "$ACTION" in
|
|
|
581
657
|
fi
|
|
582
658
|
;;
|
|
583
659
|
|
|
660
|
+
now)
|
|
661
|
+
need_jq
|
|
662
|
+
[ "$#" -ge 2 ] || { echo "now needs <phase_id> <text>" >&2; exit 64; }
|
|
663
|
+
PID="$1"; TEXT="$2"
|
|
664
|
+
# Live current-action line for the active phase. Quiet by construction:
|
|
665
|
+
# progress lines are frequent, so `now` never renders - the card picks the
|
|
666
|
+
# value up at the next boundary render/update. Truncated to 60 chars.
|
|
667
|
+
if [ "${#TEXT}" -gt 60 ]; then
|
|
668
|
+
TEXT="${TEXT:0:59}…"
|
|
669
|
+
fi
|
|
670
|
+
state=$(load_state)
|
|
671
|
+
new=$(echo "$state" | jq --arg id "$PID" --arg v "$TEXT" '
|
|
672
|
+
.phases |= map(
|
|
673
|
+
if .id == $id then .meta = ((.meta // {}) + {"Now": $v}) else . end
|
|
674
|
+
)')
|
|
675
|
+
save_state "$new"
|
|
676
|
+
emit_otel_span "phase.now" "$PID" "$TEXT" "{}"
|
|
677
|
+
;;
|
|
678
|
+
|
|
679
|
+
cost)
|
|
680
|
+
need_jq
|
|
681
|
+
[ "$#" -ge 1 ] || { echo "cost needs <phase_id>|total" >&2; exit 64; }
|
|
682
|
+
SEL="$1"
|
|
683
|
+
if [ "$SEL" = "total" ]; then
|
|
684
|
+
phase_usd total
|
|
685
|
+
else
|
|
686
|
+
# Resolve phase id -> array index (ids are strings like "3").
|
|
687
|
+
IDX=$(load_state | jq -r --arg id "$SEL" '[.phases[]?.id] | index($id) // "-"')
|
|
688
|
+
if [ "$IDX" = "-" ] || [ -z "$IDX" ]; then
|
|
689
|
+
echo "-"
|
|
690
|
+
else
|
|
691
|
+
phase_usd "$IDX"
|
|
692
|
+
fi
|
|
693
|
+
fi
|
|
694
|
+
;;
|
|
695
|
+
|
|
584
696
|
render)
|
|
585
697
|
render
|
|
586
698
|
;;
|
|
587
699
|
|
|
588
700
|
*)
|
|
589
|
-
echo "phase-tracker: unknown action '$ACTION' (use init|add|update|sub|tokens|model|meta|render)" >&2
|
|
701
|
+
echo "phase-tracker: unknown action '$ACTION' (use init|add|update|sub|tokens|model|meta|now|cost|render)" >&2
|
|
590
702
|
exit 64
|
|
591
703
|
;;
|
|
592
704
|
esac
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-channels-approval-gate.sh - contract for the channels Step 6 body preview + approval gate.
|
|
3
|
+
#
|
|
4
|
+
# Verifies:
|
|
5
|
+
# 1. channels.md has the Step 6 gate section with the four decision options.
|
|
6
|
+
# 2. The two (and only two) bypass cases are documented: autopilot + --dry-run.
|
|
7
|
+
# 3. Step 7 dispatch is gated on the Step 6 approval.
|
|
8
|
+
# 4. Byte-identical post rule + re-approval-on-regeneration rule present.
|
|
9
|
+
# 5. Phase 7 Step 1.5 (GitHub issue comment) references the same gate.
|
|
10
|
+
# 6. Required rules include the approval-before-post bullet.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
16
|
+
CHANNELS="$ROOT/pipeline/commands/multi-agent/channels.md"
|
|
17
|
+
PHASE7="$ROOT/pipeline/commands/multi-agent/refs/phases/phase-7-report.md"
|
|
18
|
+
|
|
19
|
+
PASS=0
|
|
20
|
+
FAIL=0
|
|
21
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
22
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
23
|
+
|
|
24
|
+
for f in "$CHANNELS" "$PHASE7"; do
|
|
25
|
+
[ -f "$f" ] || { echo "error: missing $f" >&2; exit 1; }
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
echo "→ 1. Step 6 gate section + decision options"
|
|
29
|
+
grep -Fq "Step 6 - Body preview + approval gate" "$CHANNELS" && pass "gate section present" || fail "gate section missing"
|
|
30
|
+
for opt in "Approve & post" "Edit" "Skip channels" "Cancel"; do
|
|
31
|
+
grep -Fq "$opt" "$CHANNELS" && pass "option: $opt" || fail "option missing: $opt"
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
echo ""
|
|
35
|
+
echo "→ 2. bypass cases are exactly autopilot + --dry-run"
|
|
36
|
+
grep -Fq "Skipped in exactly two cases" "$CHANNELS" && pass "two-case rule stated" || fail "two-case rule missing"
|
|
37
|
+
grep -Fq "MULTI_AGENT_AUTOPILOT=1" "$CHANNELS" && pass "autopilot bypass documented" || fail "autopilot bypass missing"
|
|
38
|
+
grep -Eq 'dry-run.*(redundant|without posting)' "$CHANNELS" && pass "dry-run bypass documented" || fail "dry-run bypass missing"
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo "→ 3. dispatch gated on approval"
|
|
42
|
+
grep -Fq "NEVER starts before the Step 6 gate resolves" "$CHANNELS" && pass "Step 6 gated" || fail "Step 6 gate wiring missing"
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "→ 4. byte-identical + re-approval rules"
|
|
46
|
+
grep -Fq "byte-identical to the approved preview" "$CHANNELS" && pass "byte-identical rule" || fail "byte-identical rule missing"
|
|
47
|
+
grep -Fq "invalidates the approval" "$CHANNELS" && pass "re-approval on regeneration" || fail "re-approval rule missing"
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo "→ 5. Phase 7 issue-comment path uses the same gate"
|
|
51
|
+
grep -Fq "same preview + approval gate as channels Step 6" "$PHASE7" && pass "Phase 7 Step 1.5 gated" || fail "Phase 7 Step 1.5 gate reference missing"
|
|
52
|
+
grep -Fq "Autopilot posts directly" "$PHASE7" && pass "Phase 7 autopilot bypass" || fail "Phase 7 autopilot bypass missing"
|
|
53
|
+
|
|
54
|
+
echo ""
|
|
55
|
+
echo "→ 6. required-rules bullet"
|
|
56
|
+
grep -Fq "**Approval before post**" "$CHANNELS" && pass "required-rules bullet present" || fail "required-rules bullet missing"
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo "══ channels-approval-gate smoke: $PASS passed, $FAIL failed ══"
|
|
60
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-generate-issue.sh - contract for /multi-agent:generate.
|
|
3
|
+
#
|
|
4
|
+
# Verifies:
|
|
5
|
+
# 1. All 3 files exist (1 command, shared ref, 1 Copilot/plugin SKILL.md).
|
|
6
|
+
# 2. Frontmatter keys present on both surfaces.
|
|
7
|
+
# 3. Command delegates to refs/generate-issue.md and asks the issue type.
|
|
8
|
+
# 4. Approval gate documented and marked unskippable in the ref.
|
|
9
|
+
# 5. Convention mining + createmeta + Agile sprint endpoints present.
|
|
10
|
+
# 6. UTF-8 verbatim POST path (--rawfile + --data-binary) present.
|
|
11
|
+
# 7. Standard template vocab: Task/Story (Scope/Acceptance/Test Scenarios), Bug (Steps/Expected/Actual/Environment), Swagger.
|
|
12
|
+
# 8. Registration: dispatcher rows, inventory lists, count consistency (36).
|
|
13
|
+
# 9. Forbidden strings absent (em-dash, section sign, the M-word, unicode_escape).
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
19
|
+
CMD="$ROOT/pipeline/commands/multi-agent/generate.md"
|
|
20
|
+
REF="$ROOT/pipeline/commands/multi-agent/refs/generate-issue.md"
|
|
21
|
+
SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-generate/SKILL.md"
|
|
22
|
+
DISPATCHER="$ROOT/pipeline/commands/multi-agent.md"
|
|
23
|
+
CONTRACT="$ROOT/pipeline/commands/multi-agent/refs/cross-cli-contract.md"
|
|
24
|
+
SYNC="$ROOT/pipeline/commands/multi-agent/sync.md"
|
|
25
|
+
HELP="$ROOT/pipeline/commands/multi-agent/help.md"
|
|
26
|
+
|
|
27
|
+
PASS=0
|
|
28
|
+
FAIL=0
|
|
29
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
30
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
31
|
+
|
|
32
|
+
echo "→ 1. files exist"
|
|
33
|
+
for f in "$CMD" "$REF" "$SKILL"; do
|
|
34
|
+
if [ -f "$f" ]; then pass "exists: ${f#$ROOT/}"; else fail "missing: ${f#$ROOT/}"; fi
|
|
35
|
+
done
|
|
36
|
+
# old split commands must be gone
|
|
37
|
+
for f in "$ROOT/pipeline/commands/multi-agent/generate-task.md" "$ROOT/pipeline/commands/multi-agent/generate-bug.md"; do
|
|
38
|
+
if [ -f "$f" ]; then fail "stale file present: ${f#$ROOT/}"; else pass "removed: ${f##*/}"; fi
|
|
39
|
+
done
|
|
40
|
+
[ "$FAIL" -eq 0 ] || { echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"; exit 1; }
|
|
41
|
+
|
|
42
|
+
echo ""
|
|
43
|
+
echo "→ 2. frontmatter keys"
|
|
44
|
+
head -5 "$CMD" | grep -q '^description:' && pass "description: ${CMD##*/}" || fail "description missing: ${CMD##*/}"
|
|
45
|
+
head -5 "$CMD" | grep -q '^argument-hint:' && pass "argument-hint: ${CMD##*/}" || fail "argument-hint missing: ${CMD##*/}"
|
|
46
|
+
head -8 "$SKILL" | grep -q '^name: multi-agent-generate$' && pass "name: multi-agent-generate" || fail "name wrong: $SKILL"
|
|
47
|
+
head -8 "$SKILL" | grep -q '^language: en' && pass "language en" || fail "language missing: $SKILL"
|
|
48
|
+
head -8 "$SKILL" | grep -q '^user-invocable: true' && pass "user-invocable" || fail "user-invocable missing: $SKILL"
|
|
49
|
+
|
|
50
|
+
echo ""
|
|
51
|
+
echo "→ 3. command delegates to the shared ref and asks the type"
|
|
52
|
+
grep -Fq "refs/generate-issue.md" "$CMD" && pass "ref link present" || fail "ref link missing"
|
|
53
|
+
for t in "Task" "Bug" "Story"; do
|
|
54
|
+
grep -Fq "$t" "$CMD" && pass "type offered: $t" || fail "type missing: $t"
|
|
55
|
+
done
|
|
56
|
+
grep -Fqi "Pick issue type" "$REF" && pass "type-selection step present" || fail "type-selection step missing"
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo "→ 4. approval gate documented and unskippable"
|
|
60
|
+
grep -Fq "Full preview + approval gate (never skipped)" "$REF" && pass "gate section present" || fail "gate section missing"
|
|
61
|
+
for marker in "Approve" "Edit" "Cancel" "no bypass" "Nothing was created"; do
|
|
62
|
+
grep -Fqi "$marker" "$REF" && pass "gate marker: $marker" || fail "gate marker missing: $marker"
|
|
63
|
+
done
|
|
64
|
+
grep -Fq "no autopilot variant" "$REF" && pass "no-autopilot rule" || fail "no-autopilot rule missing"
|
|
65
|
+
|
|
66
|
+
echo ""
|
|
67
|
+
echo "→ 5. mining + createmeta + agile endpoints"
|
|
68
|
+
for marker in "rest/api/2/search" "rest/api/2/issue/createmeta" "rest/agile/1.0/board" "sprint?state=active" 'sprint/${SPRINT_ID}/issue' "remotelink" "attachments"; do
|
|
69
|
+
grep -Fq "$marker" "$REF" && pass "endpoint marker: $marker" || fail "endpoint marker missing: $marker"
|
|
70
|
+
done
|
|
71
|
+
grep -Fq "gh-sprint" "$REF" && pass "generic sprint-field detection" || fail "generic sprint-field detection missing"
|
|
72
|
+
|
|
73
|
+
echo ""
|
|
74
|
+
echo "→ 6. UTF-8 verbatim POST path"
|
|
75
|
+
grep -Fq -- "--rawfile" "$REF" && pass "--rawfile present" || fail "--rawfile missing"
|
|
76
|
+
grep -Fq -- "--data-binary" "$REF" && pass "--data-binary present" || fail "--data-binary missing"
|
|
77
|
+
grep -Fqi "humanizer" "$REF" && pass "humanizer pass referenced" || fail "humanizer missing"
|
|
78
|
+
|
|
79
|
+
echo ""
|
|
80
|
+
echo "→ 7. standard template vocabulary + auto-sizing + swagger"
|
|
81
|
+
for s in "Scope" "Acceptance Criteria" "Test Scenarios"; do
|
|
82
|
+
grep -Fq "$s" "$REF" && pass "task/story section: $s" || fail "task/story section missing: $s"
|
|
83
|
+
done
|
|
84
|
+
for s in "Steps to Reproduce" "Expected Result" "Actual Result" "Environment"; do
|
|
85
|
+
grep -Fq "$s" "$REF" && pass "bug section: $s" || fail "bug section missing: $s"
|
|
86
|
+
done
|
|
87
|
+
grep -Fqi "auto-sizing" "$REF" && pass "auto-sizing rule present" || fail "auto-sizing rule missing"
|
|
88
|
+
grep -Fqi "swagger" "$REF" && pass "swagger context present" || fail "swagger context missing"
|
|
89
|
+
|
|
90
|
+
echo ""
|
|
91
|
+
echo "→ 8. registration + count consistency (36)"
|
|
92
|
+
grep -Fq '| `generate ' "$DISPATCHER" && pass "dispatcher routing: generate" || fail "dispatcher routing missing: generate"
|
|
93
|
+
grep -Fq "generate.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
|
|
94
|
+
grep -Fq "## 1. Command Inventory (36 commands)" "$CONTRACT" && pass "contract count = 36" || fail "contract count wrong"
|
|
95
|
+
grep -Eq '(^|[ ,])generate, help([ ,]|$)' "$CONTRACT" && pass "contract list has generate" || fail "contract list missing generate"
|
|
96
|
+
grep -Fq "**36 commands are synced**" "$SYNC" && pass "sync count = 36" || fail "sync count wrong"
|
|
97
|
+
SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
|
|
98
|
+
grep -Fq "**36 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 36" || fail "sync SKILL count wrong (Copilot parity)"
|
|
99
|
+
grep -Fq "generate" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists generate" || fail "help SKILL missing generate (Copilot parity)"
|
|
100
|
+
grep -c "multi-agent:generate" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
|
|
101
|
+
# no stale split-command references anywhere in the synced surfaces
|
|
102
|
+
if grep -rqE "generate-task|generate-bug" "$DISPATCHER" "$CONTRACT" "$SYNC" "$HELP" "$CMD" "$REF" "$SKILL"; then
|
|
103
|
+
fail "stale generate-task/generate-bug reference found"
|
|
104
|
+
else
|
|
105
|
+
pass "no stale split-command references"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
echo ""
|
|
109
|
+
echo "→ 9. forbidden strings absent"
|
|
110
|
+
MWORD="MAN""DATORY"
|
|
111
|
+
for f in "$CMD" "$REF" "$SKILL"; do
|
|
112
|
+
if grep -q $'—\|§\|…' "$f"; then fail "forbidden unicode punctuation in ${f##*/}"; else pass "punctuation clean: ${f##*/}"; fi
|
|
113
|
+
if grep -q "$MWORD" "$f"; then fail "forbidden word in ${f##*/}"; else pass "vocabulary clean: ${f##*/}"; fi
|
|
114
|
+
if grep -q "unicode_escape" "$f" && ! grep -q "Never round-trip" "$f"; then fail "unicode_escape misuse in ${f##*/}"; else pass "encoding rule clean: ${f##*/}"; fi
|
|
115
|
+
done
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"
|
|
119
|
+
[ "$FAIL" -eq 0 ]
|