@mmerterden/multi-agent-pipeline 12.0.0 → 12.1.1
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 +28 -0
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +2 -0
- package/docs/adr/README.md +1 -0
- package/docs/features.md +2 -2
- package/install/claude.mjs +26 -0
- package/package.json +2 -2
- package/pipeline/lib/multi-repo-pipeline.sh +6 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +1 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -0
- package/pipeline/multi-agent-refs/channels/jira.md +1 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +1 -0
- package/pipeline/scripts/_cost.mjs +35 -0
- package/pipeline/scripts/agent-guard.py +8 -3
- package/pipeline/scripts/cost-budget-check.mjs +2 -1
- package/pipeline/scripts/cost-lib.sh +43 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +2 -2
- package/pipeline/scripts/phase-tracker.sh +10 -19
- package/pipeline/scripts/render-agent-log-cost.sh +7 -13
- package/pipeline/scripts/render-cost-summary.sh +4 -7
- package/pipeline/scripts/run-aggregator.mjs +2 -3
- package/pipeline/scripts/smoke-source-parity.sh +85 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,34 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [12.1.1] - 2026-07-21
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Channel outputs no longer carry decorative/emotive emoji or smileys.** Jira/issue comments, Confluence & Wiki pages, and PR bodies are plain technical prose. Added an explicit no-emoji hard-rule to each channel template (`multi-agent-refs/channels/{jira,issue-comment,confluence,wiki}.md`) and an emoji-stripping pattern to the humanizer (`shared/external/humanizer`). Functional status/severity marks a fixed template defines (`✅/⏳` phase ticks, `🔴/🟡` severity labels) are unaffected.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [12.1.0] - 2026-07-21
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
- **`agent-guard.sh` is now wired by the default installer** (was opt-in via the setup template). A plain `install` now OS-enforces the two load-bearing git gates on every `Bash` tool call: no AI/assistant attribution in commit messages, and no force-push to a protected branch (`main`/`master`/`develop`).
|
|
28
|
+
- **Force-push protection now fails CLOSED.** A detected force-push that can't be tokenized, or a bare force-push while the current branch can't be confirmed non-protected, is blocked rather than allowed. (The general guard stays fail-open; only the data-loss path is fail-closed.)
|
|
29
|
+
- **Multi-repo clone hardening.** `cmd_prepare` disables git's `ext::`/`fd::` remote helpers (`protocol.ext.allow=never`, `protocol.fd.allow=never`) and ends options with `--`, closing an arbitrary-command-execution / option-injection vector on repo URLs sourced from `.gitmodules` suggestions. Local/file/https/ssh clones are unaffected.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Node floor raised to 20** (`.nvmrc`, `engines.node` → `>=20.0.0`). Node 18 was EOL and untested by CI; the matrix already covers 20/22.
|
|
33
|
+
|
|
34
|
+
### Refactored
|
|
35
|
+
- **Token-cost pricing is now a single source.** The per-Mtok cost formula, previously copy-pasted across ~6 bash/mjs scripts, lives in `cost-lib.sh` (bash) + `_cost.mjs` (Node), both reading the shared `cost-table.json`. Zero runtime dependencies preserved.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **`smoke-source-parity.sh`** — guards feature-set drift between the two source trees (`commands/multi-agent/<name>` vs `skills/shared/core/multi-agent-<name>`); fails if a feature exists in one tree but not the other.
|
|
39
|
+
|
|
40
|
+
### Docs
|
|
41
|
+
- Refreshed post-v12 doc drift: `SECURITY.md` + `ROADMAP.md` support/version markers → 12.x; `docs/features.md` smoke-suite count `10` → `100+`; ADR index now lists 0008 and 0008 carries a v10.7.0 amendment (the `_adapters.mjs` module was removed); `CONTRIBUTING.md` coverage claim clarified (local-only) and the stale phase-docs path corrected to `pipeline/multi-agent-refs/phases/`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
17
45
|
## [12.0.0] - 2026-07-20
|
|
18
46
|
|
|
19
47
|
Command rename (breaking), a full-cleanup uninstall mode, outputLanguage-aware
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** Accepted · 2026-04-27 (v8.0.0)
|
|
4
4
|
|
|
5
|
+
> **Amended v10.7.0:** the `_adapters.mjs` module and its third-party adapter dispatch were removed when the pipeline narrowed to Claude Code + Copilot CLI (see ADR 0007). `install/` now ships **8** modules, not 9; the module list below records the v8.0.0 decision as it shipped at the time.
|
|
6
|
+
|
|
5
7
|
## Context
|
|
6
8
|
|
|
7
9
|
Two pressures collided during the v7.x line:
|
package/docs/adr/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Format: lightly adapted from [Michael Nygard's ADR template](https://cognitect.c
|
|
|
17
17
|
| [0005](./0005-lazy-phase-docs.md) | Lazy-loaded phase docs with per-phase token budget | Accepted |
|
|
18
18
|
| [0006](./0006-skills-core-external-split.md) | `shared/core/` vs `shared/external/` source org | Accepted |
|
|
19
19
|
| [0007](./0007-multi-tool-adapter-framework.md) | Multi-tool adapter framework + token-preserving uninstall | Superseded by v10.7.0 (adapters removed; Claude Code + Copilot CLI only) |
|
|
20
|
+
| [0008](./0008-installer-modularization-and-secret-leak-defense.md) | Installer modularization + secret-leak defense | Accepted (amended v10.7.0: adapter module removed) |
|
|
20
21
|
|
|
21
22
|
## Writing a New ADR
|
|
22
23
|
|
package/docs/features.md
CHANGED
|
@@ -201,11 +201,11 @@ All critical state files are schema-validated at read and write time:
|
|
|
201
201
|
|
|
202
202
|
### Smoke Test Suites
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
100+ suites, auto-discovered from `smoke-*.sh` (no hardcoded list). Representative: add-detail (body-preservation assertions), review-triage (validator exit codes), prefs (schema round-trip), state (agent-state lifecycle), metrics (telemetry emission), sync (instruction parity), secret-scan (hook patterns), phase-banner (terminal UI), token-budget (per-phase limits), phase-tracker (progress tracking).
|
|
205
205
|
|
|
206
206
|
### Adversarial Eval Fixtures
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
Adversarial fixtures that test triage resilience against adversarial reviewer output: over-rejection, hallucinated findings, contradictions, invalid JSON, schema violations, duplicate findings, scope creep, empty results, timeout simulation, and combined edge cases.
|
|
209
209
|
|
|
210
210
|
### Sync Parity Check
|
|
211
211
|
|
package/install/claude.mjs
CHANGED
|
@@ -474,6 +474,32 @@ export function configureSettings(home) {
|
|
|
474
474
|
settingsChanged = true;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
// agent-guard.sh: the second load-bearing gate - blocks AI/assistant
|
|
478
|
+
// attribution in commit messages and force-push to a protected branch.
|
|
479
|
+
// Like pre-commit-check.sh it self-filters on the Bash command via stdin
|
|
480
|
+
// JSON, so the matcher is the tool name "Bash". Wired by default (was
|
|
481
|
+
// previously opt-in via the setup template, leaving force-push and
|
|
482
|
+
// attribution unguarded on a plain install).
|
|
483
|
+
const isAgentGuardEntry = (h) =>
|
|
484
|
+
Array.isArray(h.hooks) &&
|
|
485
|
+
h.hooks.some(
|
|
486
|
+
(sub) => typeof sub.command === "string" && sub.command.includes("agent-guard.sh"),
|
|
487
|
+
);
|
|
488
|
+
if (!settings.hooks.PreToolUse.some(isAgentGuardEntry)) {
|
|
489
|
+
settings.hooks.PreToolUse.push({
|
|
490
|
+
matcher: "Bash",
|
|
491
|
+
hooks: [
|
|
492
|
+
{
|
|
493
|
+
type: "command",
|
|
494
|
+
command: "bash $HOME/.claude/scripts/agent-guard.sh",
|
|
495
|
+
timeout: 10,
|
|
496
|
+
statusMessage: "Checking commit/push safety (attribution + force-push)...",
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
});
|
|
500
|
+
settingsChanged = true;
|
|
501
|
+
}
|
|
502
|
+
|
|
477
503
|
if (settingsChanged && isDryRun()) {
|
|
478
504
|
console.log(` [dry-run] would update ${SETTINGS_PATH} (hooks + env)`);
|
|
479
505
|
} else if (settingsChanged) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.1",
|
|
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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"url": "https://github.com/mmerterden/multi-agent-pipeline/issues"
|
|
59
59
|
},
|
|
60
60
|
"engines": {
|
|
61
|
-
"node": ">=
|
|
61
|
+
"node": ">=20.0.0"
|
|
62
62
|
},
|
|
63
63
|
"files": [
|
|
64
64
|
"index.js",
|
|
@@ -67,7 +67,12 @@ cmd_prepare() {
|
|
|
67
67
|
echo "[$name] worktree already present at $target"
|
|
68
68
|
else
|
|
69
69
|
echo "[$name] cloning into $target"
|
|
70
|
-
|
|
70
|
+
# Disable git's `ext::`/`fd::` remote helpers (arbitrary command execution
|
|
71
|
+
# at clone time) in case a repo URL ever comes from an untrusted
|
|
72
|
+
# .gitmodules suggestion, and end options with `--` so a URL like
|
|
73
|
+
# `--upload-pack=...` can't inject a clone option. https/ssh/git@ and
|
|
74
|
+
# local/file paths all still work.
|
|
75
|
+
git -c protocol.ext.allow=never -c protocol.fd.allow=never clone --quiet -- "$url" "$target" || { echo "ERR: clone failed for $name" >&2; return 3; }
|
|
71
76
|
fi
|
|
72
77
|
cd "$target"
|
|
73
78
|
if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
|
|
@@ -149,5 +149,6 @@ Successful POST → URL prepended to `prefs.projects[<project>].confluenceUrls`
|
|
|
149
149
|
- Section order is fixed: `overview` → `flow` (cond.) → `technical_details` → `api_contracts` (cond.) → `references` (cond.). Conditional sections may be omitted but never reordered.
|
|
150
150
|
- Humanizer pass runs **after** body assembly and **before** storage-XML conversion. Tone: formal, stakeholder-readable. No "we are excited", no first-person plural.
|
|
151
151
|
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, API paths, file paths, type names, JSON keys, and the storage-XML markup stay verbatim. The template (this doc) is English because `promptLanguage="en"` is locked.
|
|
152
|
+
- No decorative/emotive emoji or smileys (😊 🙂 🎉 👍 🚀 ✨) anywhere in the page body. Stakeholder-readable technical prose only.
|
|
152
153
|
- Adapter failures are non-blocking. 401 / network timeout / parent-not-found → `failed` status, loop continues.
|
|
153
154
|
- If `--dry-run`, print the assembled storage XML to stdout and return without POST.
|
|
@@ -85,6 +85,7 @@ Ref: #<issueNumber>
|
|
|
85
85
|
|
|
86
86
|
- No "Co-Authored-By: Claude" trailer. No "🤖 Generated with ..." footer. No mention of Claude / Copilot / AI / model names anywhere in the body.
|
|
87
87
|
- No em-dashes (` - `) in prose. Use ` · ` or `→` per `feedback_no_em_dash.md`.
|
|
88
|
+
- No decorative/emotive emoji or smileys (😊 🙂 🎉 👍 🚀 ✨) in the body prose. Only a fixed-template functional mark (e.g. a `✅/⏳` scope tick) is allowed; free text carries none.
|
|
88
89
|
- No HTML entities. Use raw markdown.
|
|
89
90
|
- No links to internal `/tmp/*` or local-machine paths.
|
|
90
91
|
- No screenshots embedded as base64 - link to wiki / Figma instead.
|
|
@@ -124,5 +124,6 @@ When the **Wiki** adapter writes pages on the same run AND `prefs.global.wikiToJ
|
|
|
124
124
|
- UTF-8 in, UTF-8 out - the body file is UTF-8 and `--data-binary` ships its bytes verbatim. Never round-trip the body through `unicode_escape`, `latin-1`, or any re-encode step, and never hand-roll a Python/curl helper that re-decodes it: that mangles Turkish chars (ç ş ı ö ü ğ) into mojibake (`Çözüm` → `Ãözüm`). Use the `jq --rawfile` + `--data-binary @file` path above as-is. Same rule for the PR / Confluence / Wiki adapters.
|
|
125
125
|
- Section order is fixed: `summary` → `test_scenarios` → `context_refs`. Never insert sections between them; never reorder.
|
|
126
126
|
- Humanizer pass runs **after** body assembly and **before** wiki-markup conversion. Tone target: informal but technical. No marketing voice, no "we are excited", no "I have...".
|
|
127
|
+
- **No decorative/emotive emoji or smileys** (😊 🙂 🎉 👍 🚀 ✨) anywhere in the comment body. The comment is plain technical prose; emoji do not belong in it.
|
|
127
128
|
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, branch names, and the wiki-markup syntax stay verbatim. The `promptLanguage="en"` lock means any LLM prompt that produces the body is in English; the body itself is then rendered in the user's language.
|
|
128
129
|
- Adapter failures are non-blocking - a missing token or 401 returns `skipped`/`failed`, the loop keeps going for PR / Confluence / Wiki.
|
|
@@ -63,4 +63,5 @@ Adapter implementation chooses the right git push target + commit message format
|
|
|
63
63
|
- Per the `figma-pipeline.md` rules, every component wiki page must include a variant matrix, accessibility identifiers, analytics events, and 3-layer test summary.
|
|
64
64
|
- Screenshots cover light/dark + LTR/RTL - wiki adapter refuses to commit if any quadrant is missing.
|
|
65
65
|
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, design token names, and wiki markup stay verbatim across languages. The template (this doc) is English because `promptLanguage="en"` is locked.
|
|
66
|
+
- No decorative/emotive emoji or smileys (😊 🙂 🎉 👍 🚀 ✨) in the page prose. Only functional/structural marks a fixed template defines are allowed.
|
|
66
67
|
- Autopilot always pauses at the channels menu (per `phase-7-report.md` autopilot contract) - even in autopilot mode the user gets to confirm wiki scope.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _cost.mjs - shared token-cost pricing math for the .mjs cost scripts
|
|
3
|
+
* (cost-budget-check.mjs, run-aggregator.mjs).
|
|
4
|
+
*
|
|
5
|
+
* The single source of the pricing formula for Node callers. Rate DATA still
|
|
6
|
+
* lives in cost-table.json; this module owns only the arithmetic so the formula
|
|
7
|
+
* is not copy-pasted across scripts. Zero runtime dependencies (ADR-0004).
|
|
8
|
+
*
|
|
9
|
+
* @module pipeline/scripts/_cost
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* USD cost for a token trio given a rate object.
|
|
14
|
+
*
|
|
15
|
+
* tokensIn is fresh (cache-exclusive) input; tokensCached is priced at the
|
|
16
|
+
* discounted cacheReadPerMtok rate, falling back to inPerMtok when absent.
|
|
17
|
+
* Missing per-Mtok rate fields default to 0. Returns null when `rate` is falsy.
|
|
18
|
+
*
|
|
19
|
+
* @param {{inPerMtok?:number,outPerMtok?:number,cacheReadPerMtok?:number}|null|undefined} rate
|
|
20
|
+
* @param {number} [tokensIn]
|
|
21
|
+
* @param {number} [tokensOut]
|
|
22
|
+
* @param {number} [tokensCached]
|
|
23
|
+
* @returns {number|null}
|
|
24
|
+
*/
|
|
25
|
+
export function costUsd(rate, tokensIn = 0, tokensOut = 0, tokensCached = 0) {
|
|
26
|
+
if (!rate) return null;
|
|
27
|
+
const rin = rate.inPerMtok || 0;
|
|
28
|
+
const rout = rate.outPerMtok || 0;
|
|
29
|
+
const rcache = rate.cacheReadPerMtok ?? rin;
|
|
30
|
+
return (
|
|
31
|
+
((tokensIn || 0) / 1_000_000) * rin +
|
|
32
|
+
((tokensCached || 0) / 1_000_000) * rcache +
|
|
33
|
+
((tokensOut || 0) / 1_000_000) * rout
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -40,7 +40,8 @@ def decide(cmd: str) -> str:
|
|
|
40
40
|
try:
|
|
41
41
|
toks = shlex.split(cmd)
|
|
42
42
|
except Exception:
|
|
43
|
-
|
|
43
|
+
# A force-push we can't tokenize -> can't prove it's safe -> block.
|
|
44
|
+
return "BLOCK_FORCE"
|
|
44
45
|
if any(t in PROTECTED or t.split(":")[-1] in PROTECTED for t in toks):
|
|
45
46
|
return "BLOCK_FORCE"
|
|
46
47
|
# non-flag args after `push`; only a remote (or nothing) means a bare push
|
|
@@ -52,8 +53,12 @@ def decide(cmd: str) -> str:
|
|
|
52
53
|
if seen and not t.startswith("-"):
|
|
53
54
|
args.append(t)
|
|
54
55
|
nonremote = [a for a in args if a not in REMOTES]
|
|
55
|
-
if not nonremote
|
|
56
|
-
|
|
56
|
+
if not nonremote:
|
|
57
|
+
# Bare force-push: the target is the current branch. Block if it's
|
|
58
|
+
# protected OR undeterminable (fail-closed for a data-loss guard).
|
|
59
|
+
cur = os.environ.get("CUR_BRANCH", "")
|
|
60
|
+
if cur in PROTECTED or cur == "":
|
|
61
|
+
return "BLOCK_FORCE"
|
|
57
62
|
|
|
58
63
|
return "OK"
|
|
59
64
|
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { readFileSync, existsSync } from "fs";
|
|
35
35
|
import { dirname, join } from "path";
|
|
36
36
|
import { fileURLToPath } from "url";
|
|
37
|
+
import { costUsd } from "./_cost.mjs";
|
|
37
38
|
|
|
38
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
40
|
|
|
@@ -138,7 +139,7 @@ for (const p of Object.values(tracker.phases || {})) {
|
|
|
138
139
|
tokensOut += Number(p?.tokens_out || 0);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const usd = (
|
|
142
|
+
const usd = costUsd(rate, tokensIn, tokensOut);
|
|
142
143
|
const pctOfMax = (usd / cfg.maxUsd) * 100;
|
|
143
144
|
const usdStr = `$${usd.toFixed(2)}`;
|
|
144
145
|
const totalTok = tokensIn + tokensOut;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cost-lib.sh - shared token-cost pricing math, sourced by the bash cost
|
|
3
|
+
# renderers (phase-tracker.sh, render-agent-log-cost.sh, render-cost-summary.sh).
|
|
4
|
+
#
|
|
5
|
+
# The single source of the pricing formula for shell callers. Rate DATA still
|
|
6
|
+
# lives in cost-table.json; this file owns only the arithmetic so the formula
|
|
7
|
+
# is not copy-pasted across scripts.
|
|
8
|
+
#
|
|
9
|
+
# COST_JQ_DEFS is a jq prelude string callers prepend to their own jq program:
|
|
10
|
+
#
|
|
11
|
+
# cost_usd_of($rate; $tin; $tout; $tcached)
|
|
12
|
+
# -> USD for one row given a rate object (or null) and token counts.
|
|
13
|
+
# tokens_in is fresh input (cache-exclusive); tokens_cached is priced at
|
|
14
|
+
# the discounted cacheReadPerMtok rate (falling back to inPerMtok when
|
|
15
|
+
# absent). Returns null when $rate is null.
|
|
16
|
+
# cost_floor_cents($u)
|
|
17
|
+
# -> floor USD to whole cents; null passes through.
|
|
18
|
+
#
|
|
19
|
+
# cost_usd <rate-json> <tin> <tout> [tcached] - scalar convenience wrapper
|
|
20
|
+
# that prints the raw (unrounded) USD via jq. Prints nothing when jq is absent.
|
|
21
|
+
|
|
22
|
+
COST_JQ_DEFS='
|
|
23
|
+
def cost_usd_of($rate; $tin; $tout; $tcached):
|
|
24
|
+
if $rate == null then null
|
|
25
|
+
else ( ($tin / 1000000) * $rate.inPerMtok
|
|
26
|
+
+ ($tcached / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
|
|
27
|
+
+ ($tout / 1000000) * $rate.outPerMtok )
|
|
28
|
+
end;
|
|
29
|
+
def cost_floor_cents($u):
|
|
30
|
+
if $u == null then null else (($u * 100) | floor) / 100 end;
|
|
31
|
+
'
|
|
32
|
+
|
|
33
|
+
cost_usd() {
|
|
34
|
+
local rate="$1" tin="$2" tout="$3" tcached="${4:-0}"
|
|
35
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
36
|
+
jq -n \
|
|
37
|
+
--argjson rate "$rate" \
|
|
38
|
+
--argjson tin "$tin" \
|
|
39
|
+
--argjson tout "$tout" \
|
|
40
|
+
--argjson tcached "$tcached" \
|
|
41
|
+
"$COST_JQ_DEFS"'
|
|
42
|
+
cost_usd_of($rate; $tin; $tout; $tcached)'
|
|
43
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
.claude/multi-agent-refs 51
|
|
8
8
|
.claude/rules 12
|
|
9
9
|
.claude/schemas 23
|
|
10
|
-
.claude/scripts
|
|
10
|
+
.claude/scripts 193
|
|
11
11
|
.claude/settings.json 1
|
|
12
12
|
.claude/skills 428
|
|
13
13
|
.copilot/.pipeline-version 1
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
.copilot/copilot-instructions.md 1
|
|
16
16
|
.copilot/lib 25
|
|
17
17
|
.copilot/schemas 23
|
|
18
|
-
.copilot/scripts
|
|
18
|
+
.copilot/scripts 193
|
|
19
19
|
.copilot/skills 467
|
|
@@ -245,12 +245,14 @@ format_elapsed() {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
# Cost table ships next to this script in every install tree (repo,
|
|
248
|
-
# ~/.claude/scripts, ~/.copilot/scripts). Pricing math
|
|
249
|
-
#
|
|
248
|
+
# ~/.claude/scripts, ~/.copilot/scripts). Pricing math comes from cost-lib.sh
|
|
249
|
+
# (shared with the other renderers): tokens_in is FRESH input (cache-exclusive,
|
|
250
250
|
# per the disjoint token-count contract), tokens_cached is priced at the
|
|
251
251
|
# discounted cacheReadPerMtok rate, floored to cents. A missing/unparseable
|
|
252
252
|
# table never fails a caller - USD simply renders empty / "-".
|
|
253
253
|
COST_TABLE="$(cd "$(dirname "$0")" && pwd)/cost-table.json"
|
|
254
|
+
COST_LIB="$(cd "$(dirname "$0")" && pwd)/cost-lib.sh"
|
|
255
|
+
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
254
256
|
|
|
255
257
|
# Print est. USD ("0.21") for one phase index or, with "total", the sum across
|
|
256
258
|
# priceable phases. Prints "-" when nothing is priceable. Never non-zero exit.
|
|
@@ -258,19 +260,13 @@ phase_usd() {
|
|
|
258
260
|
local selector="$1" # integer phase index into .phases[], or "total"
|
|
259
261
|
[ -f "$COST_TABLE" ] || { echo "-"; return 0; }
|
|
260
262
|
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)
|
|
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;
|
|
263
|
+
echo "$state" | jq -r --slurpfile prices "$COST_TABLE" --arg sel "$selector" "$COST_JQ_DEFS"'
|
|
264
|
+
def usd_of(p): cost_usd_of($prices[0].prices[p.model // ""] // null; (p.tokens_in // 0); (p.tokens_out // 0); (p.tokens_cached // 0));
|
|
269
265
|
if $sel == "total" then
|
|
270
|
-
([.phases[] | usd_of(.) | select(. != null)] | if length == 0 then "-" else (add |
|
|
266
|
+
([.phases[] | usd_of(.) | select(. != null)] | if length == 0 then "-" else (add | cost_floor_cents(.) | tostring) end)
|
|
271
267
|
else
|
|
272
268
|
(.phases[($sel | tonumber)] // null) as $p |
|
|
273
|
-
if $p == null then "-" else (usd_of($p) |
|
|
269
|
+
if $p == null then "-" else (usd_of($p) | cost_floor_cents(.) | if . == null then "-" else tostring end) end
|
|
274
270
|
end
|
|
275
271
|
' 2>/dev/null || echo "-"
|
|
276
272
|
}
|
|
@@ -372,13 +368,8 @@ render() {
|
|
|
372
368
|
echo "$prices_json" | jq empty 2>/dev/null || prices_json='{"prices":{}}'
|
|
373
369
|
fi
|
|
374
370
|
local phase_data
|
|
375
|
-
phase_data=$(echo "$state" | jq -r --argjson prices "$prices_json" '
|
|
376
|
-
def usd_of(p): ($prices.prices[p.model // ""] // null)
|
|
377
|
-
if $rate == null then null
|
|
378
|
-
else ( ((p.tokens_in // 0) / 1000000) * $rate.inPerMtok
|
|
379
|
-
+ ((p.tokens_cached // 0) / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
|
|
380
|
-
+ ((p.tokens_out // 0) / 1000000) * $rate.outPerMtok )
|
|
381
|
-
end;
|
|
371
|
+
phase_data=$(echo "$state" | jq -r --argjson prices "$prices_json" "$COST_JQ_DEFS"'
|
|
372
|
+
def usd_of(p): cost_usd_of($prices.prices[p.model // ""] // null; (p.tokens_in // 0); (p.tokens_out // 0); (p.tokens_cached // 0));
|
|
382
373
|
.phases // []
|
|
383
374
|
| sort_by(.id | (tonumber? // 9999))
|
|
384
375
|
| .[] | [
|
|
@@ -32,6 +32,8 @@ done
|
|
|
32
32
|
|
|
33
33
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
34
34
|
COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
|
|
35
|
+
COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
|
|
36
|
+
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
35
37
|
|
|
36
38
|
command -v jq >/dev/null 2>&1 || { echo "render-agent-log-cost: jq required" >&2; exit 2; }
|
|
37
39
|
|
|
@@ -94,27 +96,19 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
|
94
96
|
' "$SPANS_FILE")
|
|
95
97
|
fi
|
|
96
98
|
|
|
97
|
-
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" '
|
|
99
|
+
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$COST_JQ_DEFS"'
|
|
98
100
|
$rows | map(
|
|
99
101
|
. as $r |
|
|
100
102
|
($prices[0].prices[$r.model] // null) as $p |
|
|
101
103
|
# tokens_in is freshly-billed input (cache-exclusive, matching the host
|
|
102
104
|
# input_tokens field); tokens_cached is the separate prompt-cache-read
|
|
103
|
-
# count, billed at the discounted cacheReadPerMtok rate
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# renderer and aggregate-metrics.mjs agree on what "total input" means.
|
|
107
|
-
$r.tokens_in as $fresh |
|
|
105
|
+
# count, billed at the discounted cacheReadPerMtok rate. The two counts are
|
|
106
|
+
# disjoint - never subtract one from the other - so this renderer and
|
|
107
|
+
# aggregate-metrics.mjs agree on what "total input" means.
|
|
108
108
|
($r.tokens_cached // 0) as $cached |
|
|
109
109
|
. + {
|
|
110
110
|
tokens_cached: $cached,
|
|
111
|
-
usd: (
|
|
112
|
-
if $p == null then null
|
|
113
|
-
else ( ($fresh / 1000000) * $p.inPerMtok
|
|
114
|
-
+ ($cached / 1000000) * ($p.cacheReadPerMtok // $p.inPerMtok)
|
|
115
|
-
+ ($r.tokens_out / 1000000) * $p.outPerMtok )
|
|
116
|
-
end
|
|
117
|
-
)
|
|
111
|
+
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; $cached)
|
|
118
112
|
}
|
|
119
113
|
)
|
|
120
114
|
')
|
|
@@ -26,6 +26,8 @@ done
|
|
|
26
26
|
|
|
27
27
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
28
28
|
COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
|
|
29
|
+
COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
|
|
30
|
+
if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
|
|
29
31
|
|
|
30
32
|
command -v jq >/dev/null 2>&1 || { echo "render-cost-summary: jq required" >&2; exit 2; }
|
|
31
33
|
|
|
@@ -78,17 +80,12 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
|
|
|
78
80
|
fi
|
|
79
81
|
|
|
80
82
|
# Compute USD per row using cost-table
|
|
81
|
-
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" '
|
|
83
|
+
joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$COST_JQ_DEFS"'
|
|
82
84
|
$rows | map(
|
|
83
85
|
. as $r |
|
|
84
86
|
($prices[0].prices[$r.model] // null) as $p |
|
|
85
87
|
. + {
|
|
86
|
-
usd: (
|
|
87
|
-
if $p == null then null
|
|
88
|
-
else ( ($r.tokens_in / 1000000) * $p.inPerMtok
|
|
89
|
-
+ ($r.tokens_out / 1000000) * $p.outPerMtok )
|
|
90
|
-
end
|
|
91
|
-
)
|
|
88
|
+
usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; 0)
|
|
92
89
|
}
|
|
93
90
|
)
|
|
94
91
|
')
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
38
38
|
import { join, dirname } from "node:path";
|
|
39
39
|
import { fileURLToPath } from "node:url";
|
|
40
|
+
import { costUsd } from "./_cost.mjs";
|
|
40
41
|
|
|
41
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
43
|
|
|
@@ -135,9 +136,7 @@ function computeCost(modelKey, tokensIn, tokensOut, costTable) {
|
|
|
135
136
|
if (!price || (typeof price.inPerMtok !== "number" && typeof price.outPerMtok !== "number")) {
|
|
136
137
|
return { cost_usd: 0, cost_unknown_model: true };
|
|
137
138
|
}
|
|
138
|
-
const usd =
|
|
139
|
-
((tokensIn || 0) / 1_000_000) * (price.inPerMtok || 0) +
|
|
140
|
-
((tokensOut || 0) / 1_000_000) * (price.outPerMtok || 0);
|
|
139
|
+
const usd = costUsd(price, tokensIn, tokensOut);
|
|
141
140
|
return { cost_usd: Number(usd.toFixed(6)) };
|
|
142
141
|
}
|
|
143
142
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-source-parity.sh - guard the two divergent SOURCE trees of the same
|
|
3
|
+
# feature set from drifting out of alignment.
|
|
4
|
+
#
|
|
5
|
+
# The pipeline ships each sub-command twice, in parallel trees:
|
|
6
|
+
# 1. pipeline/commands/multi-agent/<name>/ (Claude slash commands)
|
|
7
|
+
# 2. pipeline/skills/shared/core/multi-agent-<name>/ (Copilot skills)
|
|
8
|
+
# Both are meant to cover the SAME feature SET. A feature present in one tree
|
|
9
|
+
# but absent from the other is the single biggest maintainability liability
|
|
10
|
+
# here: users on one CLI silently lose a capability the other CLI advertises.
|
|
11
|
+
#
|
|
12
|
+
# This is a STRUCTURAL set-parity check only. It auto-discovers feature names
|
|
13
|
+
# from the directory listings (nothing is hardcoded), strips the
|
|
14
|
+
# 'multi-agent-' prefix from the skills tree, and FAILS if the symmetric
|
|
15
|
+
# difference of the two name sets is non-empty. It deliberately does NOT diff
|
|
16
|
+
# prose / behavior content between a matched command and its skill - content
|
|
17
|
+
# drift is a separate concern (see smoke-sync-parity.sh / sync-parity-check).
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
22
|
+
CMDS_DIR="$REPO_ROOT/pipeline/commands/multi-agent"
|
|
23
|
+
SKILLS_DIR="$REPO_ROOT/pipeline/skills/shared/core"
|
|
24
|
+
|
|
25
|
+
PASS=0
|
|
26
|
+
FAIL=0
|
|
27
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
28
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
29
|
+
|
|
30
|
+
if [ ! -d "$CMDS_DIR" ]; then
|
|
31
|
+
echo "FAIL: commands tree missing at $CMDS_DIR" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
if [ ! -d "$SKILLS_DIR" ]; then
|
|
35
|
+
echo "FAIL: skills tree missing at $SKILLS_DIR" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo "→ 1. discover feature names in each source tree"
|
|
40
|
+
|
|
41
|
+
# Claude slash commands: every immediate sub-directory is a feature.
|
|
42
|
+
CMD_FEATURES=$(find "$CMDS_DIR" -mindepth 1 -maxdepth 1 -type d \
|
|
43
|
+
-exec basename {} \; | sort -u)
|
|
44
|
+
|
|
45
|
+
# Copilot skills: multi-agent-<name> sub-directories; strip the prefix.
|
|
46
|
+
# Non-prefixed skill dirs (e.g. apple-archive-compliance) are standalone
|
|
47
|
+
# skills without a slash-command twin, so they are intentionally excluded.
|
|
48
|
+
SKILL_FEATURES=$(find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d \
|
|
49
|
+
-name 'multi-agent-*' -exec basename {} \; \
|
|
50
|
+
| sed 's/^multi-agent-//' | sort -u)
|
|
51
|
+
|
|
52
|
+
CMD_COUNT=$(printf '%s\n' "$CMD_FEATURES" | grep -c . || true)
|
|
53
|
+
SKILL_COUNT=$(printf '%s\n' "$SKILL_FEATURES" | grep -c . || true)
|
|
54
|
+
pass "commands tree: $CMD_COUNT features"
|
|
55
|
+
pass "skills tree: $SKILL_COUNT features"
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
echo "→ 2. set symmetric difference must be empty"
|
|
59
|
+
|
|
60
|
+
# In commands tree but missing from skills tree.
|
|
61
|
+
ONLY_CMD=$(comm -23 \
|
|
62
|
+
<(printf '%s\n' "$CMD_FEATURES") \
|
|
63
|
+
<(printf '%s\n' "$SKILL_FEATURES"))
|
|
64
|
+
# In skills tree but missing from commands tree.
|
|
65
|
+
ONLY_SKILL=$(comm -13 \
|
|
66
|
+
<(printf '%s\n' "$CMD_FEATURES") \
|
|
67
|
+
<(printf '%s\n' "$SKILL_FEATURES"))
|
|
68
|
+
|
|
69
|
+
if [ -z "$ONLY_CMD" ]; then
|
|
70
|
+
pass "no command-only features (all present as skills)"
|
|
71
|
+
else
|
|
72
|
+
fail "features in commands/ but NOT in skills tree:"
|
|
73
|
+
printf ' - %s\n' $ONLY_CMD
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [ -z "$ONLY_SKILL" ]; then
|
|
77
|
+
pass "no skill-only features (all present as commands)"
|
|
78
|
+
else
|
|
79
|
+
fail "features in skills tree but NOT in commands/:"
|
|
80
|
+
printf ' - %s\n' $ONLY_SKILL
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "══ source-parity smoke: $PASS passed, $FAIL failed ══"
|
|
85
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -32,6 +32,17 @@ AI inflates importance. Cut it.
|
|
|
32
32
|
| Vague authority | "Experts believe", "Industry observers note" | Name the source or remove |
|
|
33
33
|
| Formulaic challenges | "Despite challenges... continues to thrive" | State specific challenges |
|
|
34
34
|
|
|
35
|
+
### Emoji & Decorative Marks
|
|
36
|
+
|
|
37
|
+
AI sprinkles friendly/emotive emoji into prose. Strip them.
|
|
38
|
+
|
|
39
|
+
| Pattern | Example | Fix |
|
|
40
|
+
|---------|---------|-----|
|
|
41
|
+
| Emotive / smiley emoji | "Done 🎉", "Hi there 🙂", "😊", "👍 looks good" | Remove the emoji, keep the words |
|
|
42
|
+
| Emoji as decoration/bullet | "✨ Feature:", "🚀 Shipped", "🔥 Fast" | Plain text |
|
|
43
|
+
|
|
44
|
+
Keep ONLY functional status/severity marks that a fixed template explicitly defines (e.g. a `✅/⏳` phase-tick strip, `🔴/🟡` severity labels). Everywhere else in prose — issue/Jira comments, Confluence/Wiki pages, PR bodies, commit messages — carry **no** emoji.
|
|
45
|
+
|
|
35
46
|
### AI Vocabulary
|
|
36
47
|
|
|
37
48
|
These words appear far more in AI output than human writing. Replace or remove them.
|