@mmerterden/multi-agent-pipeline 12.10.0 → 12.11.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
CHANGED
|
@@ -16,6 +16,65 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [12.11.0] - 2026-07-26
|
|
20
|
+
|
|
21
|
+
### Tracker tile titles rendered HTML entities
|
|
22
|
+
|
|
23
|
+
A live run showed this in the TaskList widget:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Phase 1: Build & Launch
|
|
27
|
+
Phase 3: Drive & Compare
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`rules.md` already forbids entities in "titles, commit messages, task subjects, or
|
|
31
|
+
body text" because nothing downstream decodes them. A tile title IS a task subject,
|
|
32
|
+
so the rule covered this exactly. `output-quality-check.sh` enforced it over the PR
|
|
33
|
+
body and the Jira comment and nowhere else, so the one surface where the rule was
|
|
34
|
+
actually broken was the one surface nothing inspected. The gap was in the check's
|
|
35
|
+
coverage, not in the rule.
|
|
36
|
+
|
|
37
|
+
`phase-tracker.sh add` is the single funnel for every tile title, so it decodes
|
|
38
|
+
there, and warns on stderr: decoding silently would fix the display and hide the
|
|
39
|
+
bug that produced it.
|
|
40
|
+
|
|
41
|
+
**All three encodings**, because a title can arrive in any of them:
|
|
42
|
+
|
|
43
|
+
| | ampersand | less-than | em-dash |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| named | `&` | `<` | `—` |
|
|
46
|
+
| decimal | `&` | `<` | `—` |
|
|
47
|
+
| hex | `&` | `<` | `—` |
|
|
48
|
+
|
|
49
|
+
The first version of this fix handled only the named column, which is the same bug
|
|
50
|
+
fixed for one spelling out of three.
|
|
51
|
+
|
|
52
|
+
Two subtleties, each pinned by a test:
|
|
53
|
+
|
|
54
|
+
- **Nesting.** `&lt;` means the literal text `<`, not `<`. One
|
|
55
|
+
left-to-right pass gets this right for free, because a global replace never
|
|
56
|
+
rescans its own output. An ordered-sed version needed `&` decoded last for
|
|
57
|
+
the same effect, and that ordering was a latent trap.
|
|
58
|
+
- **Fancy punctuation degrades to ASCII.** `—` becomes `-`, never an
|
|
59
|
+
em-dash. This repo bans em/en-dash and ellipsis in shipped text and gates it in
|
|
60
|
+
the scorecard, so decoding them faithfully would make this function INJECT what
|
|
61
|
+
another gate rejects. Applies to the numeric spellings too, or the named form
|
|
62
|
+
would be safe while `—` still injected.
|
|
63
|
+
|
|
64
|
+
An unknown named entity is left alone, so a title legitimately containing `&foo;`
|
|
65
|
+
survives.
|
|
66
|
+
|
|
67
|
+
`output-quality-check.sh` gains a tracker-title backstop covering all three
|
|
68
|
+
encodings: an entity reaching the stored state means the funnel was bypassed.
|
|
69
|
+
|
|
70
|
+
The decoder is fed through a QUOTED HEREDOC rather than `node -e '...'`. Inside a
|
|
71
|
+
single-quoted shell string every apostrophe in the JS terminates the string early -
|
|
72
|
+
including, at one point, the apostrophe in the comment explaining the problem.
|
|
73
|
+
shellcheck flags it (SC2140) and the first version worked only by accident of where
|
|
74
|
+
the quotes fell.
|
|
75
|
+
|
|
76
|
+
12 tests. 306 unit total.
|
|
77
|
+
|
|
19
78
|
## [12.10.0] - 2026-07-26
|
|
20
79
|
|
|
21
80
|
Two pieces of abandoned residue, and a gate that had inverted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.11.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code and Copilot CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -89,6 +89,40 @@ else
|
|
|
89
89
|
warn "no .pr-body.md in task dir (skipped PR-body check)"
|
|
90
90
|
fi
|
|
91
91
|
|
|
92
|
+
# 2b. Tracker tile titles
|
|
93
|
+
#
|
|
94
|
+
# `rules.md` forbids HTML entities in "titles, commit messages, task subjects, or
|
|
95
|
+
# body text", but this check only ever looked at the PR body and the Jira comment.
|
|
96
|
+
# Tile titles are task subjects, and that is where the rule actually broke: a real
|
|
97
|
+
# run rendered `Phase 1: Build & Launch` in the TaskList widget. The rule was
|
|
98
|
+
# right and its enforcement was scoped to the wrong two surfaces.
|
|
99
|
+
#
|
|
100
|
+
# `phase-tracker.sh add` now decodes at the funnel, so this is the backstop that
|
|
101
|
+
# says so: an entity reaching the stored state means the funnel was bypassed.
|
|
102
|
+
TRACKER_STATE="$TASK_DIR/tracker-state.json"
|
|
103
|
+
if [ -f "$TRACKER_STATE" ]; then
|
|
104
|
+
BAD_TITLES=$(python3 -c "
|
|
105
|
+
import json, re, sys
|
|
106
|
+
try:
|
|
107
|
+
d = json.load(open('$TRACKER_STATE'))
|
|
108
|
+
except Exception:
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
# named, decimal and hex - a pattern covering only the named forms would miss
|
|
111
|
+
# exactly the two spellings the first version of the decoder missed.
|
|
112
|
+
pat = re.compile(r'&(amp|lt|gt|quot|apos|nbsp|mdash|ndash|hellip|#[0-9]+|#[xX][0-9a-fA-F]+);')
|
|
113
|
+
for p in d.get('phases', []):
|
|
114
|
+
n = p.get('name', '')
|
|
115
|
+
if pat.search(n):
|
|
116
|
+
print('%s: %s' % (p.get('id'), n))
|
|
117
|
+
" 2>/dev/null)
|
|
118
|
+
if [ -n "$BAD_TITLES" ]; then
|
|
119
|
+
bad "tracker tile title(s) carry HTML entities (they render literally):"
|
|
120
|
+
printf '%s\n' "$BAD_TITLES" | sed 's/^/ /'
|
|
121
|
+
else
|
|
122
|
+
ok "tracker tile titles free of HTML entities"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
92
126
|
# 3. Jira comment
|
|
93
127
|
JIRA_COMMENT="$TASK_DIR/.jira-comment.txt"
|
|
94
128
|
if [ -f "$JIRA_COMMENT" ]; then
|
|
@@ -115,6 +115,84 @@ need_jq() {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
# Decode HTML entities in a tile title.
|
|
119
|
+
#
|
|
120
|
+
# `rules.md` forbids entities in "titles, commit messages, task subjects, or body
|
|
121
|
+
# text" because nothing downstream decodes them - they render literally. A tracker
|
|
122
|
+
# tile title IS a task subject, and it rendered exactly that way:
|
|
123
|
+
#
|
|
124
|
+
# Phase 1: Build & Launch
|
|
125
|
+
# Phase 3: Drive & Compare
|
|
126
|
+
#
|
|
127
|
+
# The rule existed and `output-quality-check.sh` enforced it, but only over the PR
|
|
128
|
+
# body and the Jira comment, never over a tile title. The one surface where the
|
|
129
|
+
# rule actually broke was the one surface nothing inspected.
|
|
130
|
+
#
|
|
131
|
+
# `add` is the single funnel for every tile title, so fixing it here fixes every
|
|
132
|
+
# caller. Decode rather than reject: the intent behind `&` is unambiguous, and
|
|
133
|
+
# a display path should render correctly now rather than abort a run over
|
|
134
|
+
# punctuation. The stderr warning keeps the caller mistake visible instead of
|
|
135
|
+
# silently papering over it.
|
|
136
|
+
#
|
|
137
|
+
# Handles all three encodings, because a title can arrive in any of them:
|
|
138
|
+
# named & < " —
|
|
139
|
+
# decimal & < "   —
|
|
140
|
+
# hex & < "   —
|
|
141
|
+
# A first version handled only the named forms, so the numeric spellings still
|
|
142
|
+
# rendered literally: the same bug fixed for one spelling out of three.
|
|
143
|
+
#
|
|
144
|
+
# ONE left-to-right pass, which also gets nesting right for free: a global replace
|
|
145
|
+
# never rescans its own output, so `&lt;` yields the literal text `<` rather
|
|
146
|
+
# than `<`. An ordered-sed version needed `&` decoded last for the same effect,
|
|
147
|
+
# and that ordering was a latent trap.
|
|
148
|
+
#
|
|
149
|
+
# Fancy punctuation degrades to ASCII instead of decoding faithfully. `—`
|
|
150
|
+
# becomes `-`, not an em-dash: this repo bans em/en-dash and ellipsis in shipped
|
|
151
|
+
# text and gates it in the scorecard, so decoding them would make this function
|
|
152
|
+
# INJECT what another gate rejects. Same for any numeric entity resolving to one.
|
|
153
|
+
#
|
|
154
|
+
# The program is fed through a QUOTED heredoc, not `node -e '...'`. Inside a
|
|
155
|
+
# single-quoted shell string every apostrophe in the JS (and in its comments)
|
|
156
|
+
# terminates the string early; shellcheck flags it as SC2140 and the first version
|
|
157
|
+
# here worked only by accident of where the quotes happened to fall. A quoted
|
|
158
|
+
# heredoc passes the body through verbatim, so the JS can use any quote it likes.
|
|
159
|
+
decode_entities() {
|
|
160
|
+
local raw="$1" out
|
|
161
|
+
out=$(MA_RAW="$raw" node - <<'MA_DECODE_JS'
|
|
162
|
+
const NAMED = {
|
|
163
|
+
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'",
|
|
164
|
+
nbsp: " ",
|
|
165
|
+
// Deliberately ASCII: see the note above this function.
|
|
166
|
+
ndash: "-", mdash: "-", hellip: "...", laquo: "<<", raquo: ">>",
|
|
167
|
+
};
|
|
168
|
+
// Codepoints that must degrade to ASCII rather than decode faithfully.
|
|
169
|
+
const ASCII_FOR = new Map([
|
|
170
|
+
[0x00a0, " "], [0x2013, "-"], [0x2014, "-"], [0x2026, "..."],
|
|
171
|
+
[0x2018, "'"], [0x2019, "'"], [0x201c, '"'], [0x201d, '"'],
|
|
172
|
+
[0x00a7, ""],
|
|
173
|
+
]);
|
|
174
|
+
const src = process.env.MA_RAW || "";
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
src.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => {
|
|
177
|
+
if (body[0] === "#") {
|
|
178
|
+
const hex = body[1] === "x" || body[1] === "X";
|
|
179
|
+
const cp = parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
180
|
+
if (!Number.isFinite(cp) || cp < 0 || cp > 0x10ffff) return m;
|
|
181
|
+
if (ASCII_FOR.has(cp)) return ASCII_FOR.get(cp);
|
|
182
|
+
try { return String.fromCodePoint(cp); } catch { return m; }
|
|
183
|
+
}
|
|
184
|
+
const key = body.toLowerCase();
|
|
185
|
+
// An unknown named entity is left alone: guessing would corrupt a title that
|
|
186
|
+
// legitimately contains "&foo;".
|
|
187
|
+
return Object.prototype.hasOwnProperty.call(NAMED, key) ? NAMED[key] : m;
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
MA_DECODE_JS
|
|
191
|
+
) || out="$raw"
|
|
192
|
+
[ "$out" != "$raw" ] && echo "phase-tracker: decoded HTML entities in title (rules.md forbids them): '$raw' -> '$out'" >&2
|
|
193
|
+
printf '%s' "$out"
|
|
194
|
+
}
|
|
195
|
+
|
|
118
196
|
# OTel-compatible span emission - v6.1.0+, opt-in via $MULTI_AGENT_OTEL_SPANS.
|
|
119
197
|
# Appends one JSON line to otel-spans.jsonl. Never fatal on failure.
|
|
120
198
|
#
|
|
@@ -575,7 +653,7 @@ case "$ACTION" in
|
|
|
575
653
|
add)
|
|
576
654
|
need_jq
|
|
577
655
|
[ "$#" -ge 2 ] || { echo "add needs <phase_id> <name>" >&2; exit 64; }
|
|
578
|
-
PID="$1"; PNAME="$2"
|
|
656
|
+
PID="$1"; PNAME="$(decode_entities "$2")"
|
|
579
657
|
acquire_state_lock
|
|
580
658
|
state=$(load_state)
|
|
581
659
|
# Idempotent: a phase id that already exists is a no-op (name, status, and
|