@event4u/agent-config 2.24.0 → 2.25.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/.agent-src/commands/create-pr/description-only.md +39 -11
- package/.agent-src/commands/create-pr.md +59 -5
- package/.agent-src/commands/video/from-script.md +5 -5
- package/.agent-src/commands/video/storyboard.md +1 -1
- package/.agent-src/contexts/execution/roadmap-process-loop.md +69 -14
- package/.agent-src/personas/README.md +3 -2
- package/.agent-src/personas/ai-video-technical-director.md +2 -2
- package/.agent-src/personas/hollywood-director.md +3 -3
- package/.agent-src/profiles/content_creator.yml +5 -0
- package/.agent-src/rules/media-governance-routing.md +82 -0
- package/.agent-src/rules/persona-governance.md +90 -0
- package/.agent-src/rules/post-push-rewrite-discipline.md +70 -0
- package/.agent-src/rules/provider-lifecycle-discipline.md +75 -0
- package/.agent-src/rules/roadmap-ci-steps-policy.md +145 -0
- package/.agent-src/rules/roadmap-progress-sync.md +11 -5
- package/.agent-src/skills/character-consistency/SKILL.md +12 -1
- package/.agent-src/skills/git-workflow/SKILL.md +133 -0
- package/.agent-src/skills/motion-choreographer/SKILL.md +12 -0
- package/.agent-src/skills/pixar-storyteller/SKILL.md +19 -6
- package/.agent-src/skills/roadmap-writing/SKILL.md +10 -0
- package/.agent-src/skills/scene-expander/SKILL.md +22 -7
- package/.agent-src/skills/video-director/SKILL.md +13 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/roadmaps.md +16 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +43 -0
- package/README.md +5 -3
- package/config/agent-settings.template.yml +26 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -2
- package/docs/contracts/file-ownership-matrix.json +81 -13
- package/docs/contracts/provider-lifecycle.md +122 -0
- package/docs/decisions/ADR-011-domain-pack-readiness.md +213 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/getting-started-by-role.md +10 -0
- package/docs/getting-started.md +1 -1
- package/docs/personas.md +73 -26
- package/docs/profiles.md +9 -4
- package/package.json +1 -1
- package/scripts/_tmp_scan_framework_leakage.py +119 -0
- package/scripts/ai-video/adapters/gemini-veo.sh +5 -0
- package/scripts/ai-video/adapters/higgsfield.sh +6 -0
- package/scripts/ai-video/adapters/kling.sh +5 -0
- package/scripts/ai-video/adapters/openai-images.sh +5 -0
- package/scripts/ai-video/adapters/sora.sh +6 -0
- package/scripts/check_portability.py +6 -0
- package/scripts/lint_media_policy_linkage.py +140 -0
- package/scripts/lint_persona_governance.py +164 -0
- package/scripts/lint_roadmap_ci_steps.py +182 -0
- package/scripts/smoke/schema.sh +1 -1
- package/.agent-src/personas/pixar-storyboard-artist.md +0 -98
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint structural reachability of media governance policies.
|
|
3
|
+
|
|
4
|
+
Every policy file under `agents/policies/media/` (except README) must
|
|
5
|
+
be linked from at least one of:
|
|
6
|
+
|
|
7
|
+
* a skill SKILL.md (any .agent-src.uncompressed/skills/*/SKILL.md
|
|
8
|
+
or .claude/skills/*/SKILL.md),
|
|
9
|
+
* a routing rule under .agent-src.uncompressed/rules/, or
|
|
10
|
+
* a sibling policy file under agents/policies/media/.
|
|
11
|
+
|
|
12
|
+
A policy that no surface references is a silent policy and a silent
|
|
13
|
+
policy is a failed policy. This is the CI-side reachability guarantee
|
|
14
|
+
the agent-in-the-loop enforcement model rests on (see
|
|
15
|
+
agents/policies/media/README.md § Enforcement model).
|
|
16
|
+
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 all policies linked
|
|
19
|
+
1 one or more orphan policies
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
QUIET = "--quiet" in sys.argv
|
|
27
|
+
|
|
28
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
29
|
+
POLICY_DIR = REPO / "agents" / "policies" / "media"
|
|
30
|
+
EXEMPT_STEMS = frozenset({"README"})
|
|
31
|
+
|
|
32
|
+
# Surfaces scanned for inbound references to policy files.
|
|
33
|
+
SCAN_ROOTS: tuple[Path, ...] = (
|
|
34
|
+
REPO / ".agent-src.uncompressed" / "skills",
|
|
35
|
+
REPO / ".agent-src.uncompressed" / "rules",
|
|
36
|
+
REPO / ".agent-src.uncompressed" / "commands",
|
|
37
|
+
REPO / ".claude" / "skills",
|
|
38
|
+
REPO / "agents" / "policies" / "media",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def emit(msg: str) -> None:
|
|
43
|
+
if not QUIET:
|
|
44
|
+
print(msg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def collect_policies() -> list[Path]:
|
|
48
|
+
if not POLICY_DIR.exists():
|
|
49
|
+
return []
|
|
50
|
+
return sorted(
|
|
51
|
+
p
|
|
52
|
+
for p in POLICY_DIR.glob("*.md")
|
|
53
|
+
if p.stem not in EXEMPT_STEMS
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def collect_scan_files() -> list[Path]:
|
|
58
|
+
files: list[Path] = []
|
|
59
|
+
for root in SCAN_ROOTS:
|
|
60
|
+
if not root.exists():
|
|
61
|
+
continue
|
|
62
|
+
files.extend(root.rglob("*.md"))
|
|
63
|
+
return files
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def referrers_for(policy: Path, scan_files: list[Path]) -> list[Path]:
|
|
67
|
+
"""Return files that reference `policy` by its repo-relative name
|
|
68
|
+
or basename. We accept both the full path token
|
|
69
|
+
(`agents/policies/media/likeness.md`) and the bare basename
|
|
70
|
+
(`likeness.md`) inside a markdown link, because sibling policies
|
|
71
|
+
link via relative `[likeness.md](likeness.md)` form.
|
|
72
|
+
"""
|
|
73
|
+
needles = (
|
|
74
|
+
f"policies/media/{policy.name}",
|
|
75
|
+
f"]({policy.name})",
|
|
76
|
+
)
|
|
77
|
+
referrers: list[Path] = []
|
|
78
|
+
for scan_file in scan_files:
|
|
79
|
+
# A policy can't satisfy its own linkage requirement.
|
|
80
|
+
if scan_file.resolve() == policy.resolve():
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
text = scan_file.read_text(encoding="utf-8", errors="replace")
|
|
84
|
+
except OSError:
|
|
85
|
+
continue
|
|
86
|
+
if any(n in text for n in needles):
|
|
87
|
+
referrers.append(scan_file)
|
|
88
|
+
return referrers
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
if not POLICY_DIR.exists():
|
|
93
|
+
emit(
|
|
94
|
+
"media-policy-linkage: agents/policies/media/ missing — "
|
|
95
|
+
"nothing to lint."
|
|
96
|
+
)
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
policies = collect_policies()
|
|
100
|
+
if not policies:
|
|
101
|
+
emit(
|
|
102
|
+
"media-policy-linkage: agents/policies/media/ has no policy "
|
|
103
|
+
"files — nothing to lint."
|
|
104
|
+
)
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
scan_files = collect_scan_files()
|
|
108
|
+
orphans: list[Path] = []
|
|
109
|
+
for policy in policies:
|
|
110
|
+
referrers = referrers_for(policy, scan_files)
|
|
111
|
+
rel = policy.relative_to(REPO)
|
|
112
|
+
if not referrers:
|
|
113
|
+
orphans.append(policy)
|
|
114
|
+
emit(f"❌ ORPHAN {rel}")
|
|
115
|
+
continue
|
|
116
|
+
emit(f"✅ {rel} ({len(referrers)} referrer(s))")
|
|
117
|
+
|
|
118
|
+
if orphans:
|
|
119
|
+
print(
|
|
120
|
+
f"\nmedia-policy-linkage: {len(orphans)} orphan policy "
|
|
121
|
+
f"file(s) — every policy must be linked from a skill, rule, "
|
|
122
|
+
f"or sibling policy.",
|
|
123
|
+
file=sys.stderr,
|
|
124
|
+
)
|
|
125
|
+
for o in orphans:
|
|
126
|
+
print(
|
|
127
|
+
f" - {o.relative_to(REPO)}",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
emit(
|
|
133
|
+
f"media-policy-linkage: {len(policies)} policy file(s) — all "
|
|
134
|
+
f"linked."
|
|
135
|
+
)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint persona governance — per-domain cap (hard) + citation floor (warn).
|
|
3
|
+
|
|
4
|
+
Enforces the mechanical checks in
|
|
5
|
+
`.agent-src.uncompressed/rules/persona-governance.md`:
|
|
6
|
+
|
|
7
|
+
1. **Per-domain cap (HARD)** — ≤ 2 active specialist personas per
|
|
8
|
+
content domain. Core-tier personas are exempt. `status:
|
|
9
|
+
deprecated` rows (if any survive a transition window) are
|
|
10
|
+
excluded from the count; the canonical path is in-commit
|
|
11
|
+
deletion, no soak window.
|
|
12
|
+
2. **Skill citation floor (WARN)** — every active specialist
|
|
13
|
+
persona SHOULD be cited by `personas: [<id>]` in at least one
|
|
14
|
+
skill SKILL.md under `.agent-src.uncompressed/skills/` or
|
|
15
|
+
`.claude/skills/`. Surfaced as a warning, never blocks CI:
|
|
16
|
+
the citation floor is enforced at PR time per the rule (a new
|
|
17
|
+
specialist MUST land with a cite); pre-existing wiring debt is
|
|
18
|
+
tracked as `--citation-debt` for the maintainer dashboard.
|
|
19
|
+
|
|
20
|
+
Schema conformance (check 4) is delegated to `lint-skills`.
|
|
21
|
+
Deprecation path (check 3) is reviewed at PR time via the table in
|
|
22
|
+
`docs/personas.md`.
|
|
23
|
+
|
|
24
|
+
Domain inference: persona ids → content domain via `DOMAIN_MAP`,
|
|
25
|
+
mirroring `persona-governance.md § Per-domain cap`. Personas absent
|
|
26
|
+
from the map are cross-cutting (uncapped).
|
|
27
|
+
|
|
28
|
+
Exit codes:
|
|
29
|
+
0 per-domain cap clean (citation warnings non-blocking)
|
|
30
|
+
1 per-domain cap violated
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from collections import defaultdict
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
QUIET = "--quiet" in sys.argv
|
|
40
|
+
|
|
41
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
42
|
+
PERSONA_DIR = REPO / ".agent-src.uncompressed" / "personas"
|
|
43
|
+
SKILL_ROOTS: tuple[Path, ...] = (
|
|
44
|
+
REPO / ".agent-src.uncompressed" / "skills",
|
|
45
|
+
REPO / ".claude" / "skills",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Per-domain cap — mirrors persona-governance.md § Per-domain cap.
|
|
49
|
+
# Maps persona id → content-domain bucket. Personas absent from this
|
|
50
|
+
# map are cross-cutting (uncapped) — typically singleton specialists
|
|
51
|
+
# without a domain peer (e.g. `qa`, `tech-writer`).
|
|
52
|
+
DOMAIN_MAP: dict[str, str] = {
|
|
53
|
+
"hollywood-director": "ai-video",
|
|
54
|
+
"ai-video-technical-director": "ai-video",
|
|
55
|
+
"backend-architect": "backend",
|
|
56
|
+
"eloquent-tamer": "backend",
|
|
57
|
+
"cmo": "gtm",
|
|
58
|
+
"revops": "gtm",
|
|
59
|
+
"growth-pm": "growth",
|
|
60
|
+
"customer-success-lead": "customer",
|
|
61
|
+
"discovery-lead": "customer",
|
|
62
|
+
"engineering-manager": "people",
|
|
63
|
+
"people-strategist": "people",
|
|
64
|
+
"finance-partner": "money",
|
|
65
|
+
"strategist": "money",
|
|
66
|
+
}
|
|
67
|
+
PER_DOMAIN_CAP = 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def emit(msg: str) -> None:
|
|
71
|
+
if not QUIET:
|
|
72
|
+
print(msg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
76
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
77
|
+
if not text.startswith("---"):
|
|
78
|
+
return {}
|
|
79
|
+
end = text.find("\n---", 3)
|
|
80
|
+
if end == -1:
|
|
81
|
+
return {}
|
|
82
|
+
out: dict[str, str] = {}
|
|
83
|
+
for line in text[3:end].splitlines():
|
|
84
|
+
m = re.match(r"^([a-zA-Z_][\w-]*):\s*(.*)$", line)
|
|
85
|
+
if m:
|
|
86
|
+
out[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def collect_personas() -> list[tuple[str, str, str, Path]]:
|
|
91
|
+
"""Return (id, tier, status, path) for every persona file."""
|
|
92
|
+
out: list[tuple[str, str, str, Path]] = []
|
|
93
|
+
if not PERSONA_DIR.exists():
|
|
94
|
+
return out
|
|
95
|
+
for path in sorted(PERSONA_DIR.glob("*.md")):
|
|
96
|
+
if path.stem == "README":
|
|
97
|
+
continue
|
|
98
|
+
fm = parse_frontmatter(path)
|
|
99
|
+
pid = fm.get("id") or path.stem
|
|
100
|
+
tier = fm.get("tier", "")
|
|
101
|
+
status = fm.get("status", "active") or "active"
|
|
102
|
+
out.append((pid, tier, status, path))
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def citations_for(persona_id: str) -> list[Path]:
|
|
107
|
+
pattern = re.compile(rf"(^|[\s,\[]){re.escape(persona_id)}([\s,\]]|$)")
|
|
108
|
+
hits: list[Path] = []
|
|
109
|
+
for root in SKILL_ROOTS:
|
|
110
|
+
if not root.exists():
|
|
111
|
+
continue
|
|
112
|
+
for skill in root.rglob("SKILL.md"):
|
|
113
|
+
text = skill.read_text(encoding="utf-8", errors="replace")
|
|
114
|
+
if text.startswith("---"):
|
|
115
|
+
end = text.find("\n---", 3)
|
|
116
|
+
fm_block = text[3:end] if end != -1 else ""
|
|
117
|
+
else:
|
|
118
|
+
fm_block = ""
|
|
119
|
+
if "personas:" not in fm_block:
|
|
120
|
+
continue
|
|
121
|
+
if pattern.search(fm_block):
|
|
122
|
+
hits.append(skill)
|
|
123
|
+
return hits
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> int:
|
|
127
|
+
personas = collect_personas()
|
|
128
|
+
if not personas:
|
|
129
|
+
emit("persona-governance: no persona files found — nothing to lint.")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
by_domain: dict[str, list[str]] = defaultdict(list)
|
|
133
|
+
missing_citations: list[str] = []
|
|
134
|
+
|
|
135
|
+
for pid, tier, status, _ in personas:
|
|
136
|
+
if status == "deprecated" or tier != "specialist":
|
|
137
|
+
continue
|
|
138
|
+
domain = DOMAIN_MAP.get(pid)
|
|
139
|
+
if domain:
|
|
140
|
+
by_domain[domain].append(pid)
|
|
141
|
+
if not citations_for(pid):
|
|
142
|
+
missing_citations.append(pid)
|
|
143
|
+
|
|
144
|
+
overflows = {d: ids for d, ids in by_domain.items() if len(ids) > PER_DOMAIN_CAP}
|
|
145
|
+
for d, ids in sorted(by_domain.items()):
|
|
146
|
+
marker = "❌" if d in overflows else "✅"
|
|
147
|
+
emit(f"{marker} domain={d} {len(ids)}/{PER_DOMAIN_CAP} {', '.join(sorted(ids))}")
|
|
148
|
+
for pid in sorted(missing_citations):
|
|
149
|
+
emit(f"⚠️ no-skill-citation {pid} (warn — see PR-time gate)")
|
|
150
|
+
|
|
151
|
+
if overflows:
|
|
152
|
+
print("\npersona-governance: per-domain cap violated.", file=sys.stderr)
|
|
153
|
+
for d, ids in sorted(overflows.items()):
|
|
154
|
+
print(f" - domain '{d}' has {len(ids)} specialists (cap {PER_DOMAIN_CAP}): {', '.join(sorted(ids))}", file=sys.stderr)
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
active = sum(1 for _, t, s, _ in personas if s != "deprecated" and t == "specialist")
|
|
158
|
+
cited = active - len(missing_citations)
|
|
159
|
+
emit(f"persona-governance: {active} active specialist persona(s) — all domains within cap; {cited}/{active} cited by ≥ 1 skill.")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
sys.exit(main())
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hard-Gate linter for the ``roadmap-ci-steps-policy`` rule.
|
|
3
|
+
|
|
4
|
+
Forbids full-pipeline CI literals (``task ci``, ``make test``,
|
|
5
|
+
``npm run check`` etc.) inside ``agents/roadmaps/*.md`` checkbox steps
|
|
6
|
+
or fenced bash blocks **when** ``quality.local_auto_run`` in
|
|
7
|
+
``.agent-settings.yml`` is ``false``.
|
|
8
|
+
|
|
9
|
+
Carve-outs:
|
|
10
|
+
* Setting is ``true`` → linter no-ops (exit 0).
|
|
11
|
+
* Step line carries ``<!-- carve-out: new-gate-verification -->`` →
|
|
12
|
+
allowed (new gate added by the same roadmap).
|
|
13
|
+
* ``## Acceptance criteria`` section → documentation, not steps.
|
|
14
|
+
* ``agents/roadmaps/archive/`` and ``agents/roadmaps/skipped/`` → out
|
|
15
|
+
of scope; they record history.
|
|
16
|
+
|
|
17
|
+
Cap: ≤ 150 LOC, stdlib only. Hooked into ``task ci-fast`` via
|
|
18
|
+
``task lint-roadmap-ci-steps``.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
QUIET = "--quiet" in sys.argv
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
ROADMAP_GLOB = "agents/roadmaps/*.md"
|
|
30
|
+
SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
|
|
31
|
+
LOCAL_AUTO_RUN_PAT = re.compile(
|
|
32
|
+
r"^\s*local_auto_run:\s*(true|false)\s*(?:#.*)?$", re.MULTILINE
|
|
33
|
+
)
|
|
34
|
+
CARVE_OUT_MARKER = "carve-out: new-gate-verification"
|
|
35
|
+
|
|
36
|
+
# CI-shaped literals — case-insensitive whole-word(-ish) matches.
|
|
37
|
+
CI_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
38
|
+
(re.compile(r"\btask\s+ci-strict\b", re.IGNORECASE), "task ci-strict"),
|
|
39
|
+
(re.compile(r"\btask\s+ci-fast\b", re.IGNORECASE), "task ci-fast"),
|
|
40
|
+
(re.compile(r"\btask\s+ci\b(?!-)", re.IGNORECASE), "task ci"),
|
|
41
|
+
(re.compile(r"\bmake\s+ci\b", re.IGNORECASE), "make ci"),
|
|
42
|
+
(re.compile(r"\bmake\s+test\b", re.IGNORECASE), "make test"),
|
|
43
|
+
(re.compile(r"\bnpm\s+run\s+check\b", re.IGNORECASE), "npm run check"),
|
|
44
|
+
(re.compile(r"\bpnpm\s+run\s+check\b", re.IGNORECASE), "pnpm run check"),
|
|
45
|
+
(re.compile(r"\byarn\s+check\b", re.IGNORECASE), "yarn check"),
|
|
46
|
+
(re.compile(r"\bcomposer\s+test\b", re.IGNORECASE), "composer test"),
|
|
47
|
+
# Whole-suite = bare command, or command followed only by prose
|
|
48
|
+
# ("before the boundary"). A real shell argument starts with ``-``
|
|
49
|
+
# (flag) or contains ``/`` or ``.`` (path / .php file) — that
|
|
50
|
+
# signals a targeted run and is allowed.
|
|
51
|
+
(re.compile(r"\bvendor/bin/phpunit\b(?!\s+(?:-|\S*[/.]))", re.IGNORECASE),
|
|
52
|
+
"vendor/bin/phpunit (whole suite)"),
|
|
53
|
+
(re.compile(r"\bphp\s+artisan\s+test\b(?!\s+(?:-|\S*[/.]))", re.IGNORECASE),
|
|
54
|
+
"php artisan test (whole suite)"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
CHECKBOX_PAT = re.compile(r"^\s*-\s*\[[ x~/-]\]\s")
|
|
58
|
+
FENCE_PAT = re.compile(r"^\s*```")
|
|
59
|
+
HEADING_PAT = re.compile(r"^(#{1,6})\s+(.*?)\s*$")
|
|
60
|
+
ACCEPTANCE_HEADING_PAT = re.compile(
|
|
61
|
+
r"^acceptance criteria\b", re.IGNORECASE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _read_local_auto_run() -> bool:
|
|
66
|
+
"""Return ``quality.local_auto_run`` from ``.agent-settings.yml``.
|
|
67
|
+
|
|
68
|
+
Default ``True`` (= no-op) when file or key is missing. The Hard
|
|
69
|
+
Gate only fires when the setting is explicitly ``false``.
|
|
70
|
+
"""
|
|
71
|
+
if not SETTINGS_FILE.is_file():
|
|
72
|
+
return True
|
|
73
|
+
try:
|
|
74
|
+
text = SETTINGS_FILE.read_text(encoding="utf-8")
|
|
75
|
+
except OSError:
|
|
76
|
+
return True
|
|
77
|
+
in_quality = False
|
|
78
|
+
for raw in text.splitlines():
|
|
79
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
80
|
+
continue
|
|
81
|
+
if raw.startswith("quality:"):
|
|
82
|
+
in_quality = True
|
|
83
|
+
continue
|
|
84
|
+
if in_quality and raw and not raw.startswith((" ", "\t")):
|
|
85
|
+
in_quality = False
|
|
86
|
+
continue
|
|
87
|
+
if in_quality:
|
|
88
|
+
m = LOCAL_AUTO_RUN_PAT.match(raw)
|
|
89
|
+
if m:
|
|
90
|
+
return m.group(1).lower() == "true"
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _scan(text: str) -> list[tuple[int, str, str]]:
|
|
95
|
+
"""Return ``(line_no, matched_literal, line_text)`` for every hit.
|
|
96
|
+
|
|
97
|
+
Only scans:
|
|
98
|
+
* checkbox-step lines (``- [ ] …``)
|
|
99
|
+
* lines inside fenced code blocks (```` ``` ````)
|
|
100
|
+
Skips lines under an ``## Acceptance criteria`` heading.
|
|
101
|
+
Skips lines carrying the carve-out marker.
|
|
102
|
+
"""
|
|
103
|
+
hits: list[tuple[int, str, str]] = []
|
|
104
|
+
in_fence = False
|
|
105
|
+
in_acceptance = False
|
|
106
|
+
for idx, line in enumerate(text.splitlines(), start=1):
|
|
107
|
+
if FENCE_PAT.match(line):
|
|
108
|
+
in_fence = not in_fence
|
|
109
|
+
continue
|
|
110
|
+
if not in_fence:
|
|
111
|
+
heading = HEADING_PAT.match(line)
|
|
112
|
+
if heading:
|
|
113
|
+
in_acceptance = bool(
|
|
114
|
+
ACCEPTANCE_HEADING_PAT.match(heading.group(2))
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
if in_acceptance:
|
|
118
|
+
continue
|
|
119
|
+
is_checkbox = CHECKBOX_PAT.match(line) is not None
|
|
120
|
+
if not (is_checkbox or in_fence):
|
|
121
|
+
continue
|
|
122
|
+
if CARVE_OUT_MARKER in line:
|
|
123
|
+
continue
|
|
124
|
+
for pat, label in CI_PATTERNS:
|
|
125
|
+
if pat.search(line):
|
|
126
|
+
hits.append((idx, label, line.strip()))
|
|
127
|
+
break
|
|
128
|
+
return hits
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main() -> int:
|
|
132
|
+
if _read_local_auto_run():
|
|
133
|
+
if not QUIET:
|
|
134
|
+
print(
|
|
135
|
+
"✅ quality.local_auto_run=true (or unset) — "
|
|
136
|
+
"CI-step gate disabled"
|
|
137
|
+
)
|
|
138
|
+
return 0
|
|
139
|
+
roadmaps = sorted(REPO_ROOT.glob(ROADMAP_GLOB))
|
|
140
|
+
if not roadmaps:
|
|
141
|
+
if not QUIET:
|
|
142
|
+
print(f"✅ no active roadmaps under {ROADMAP_GLOB}")
|
|
143
|
+
return 0
|
|
144
|
+
failed = 0
|
|
145
|
+
for roadmap in roadmaps:
|
|
146
|
+
rel = roadmap.relative_to(REPO_ROOT)
|
|
147
|
+
text = roadmap.read_text(encoding="utf-8")
|
|
148
|
+
hits = _scan(text)
|
|
149
|
+
if hits:
|
|
150
|
+
failed += 1
|
|
151
|
+
print(f"❌ {rel}", file=sys.stderr)
|
|
152
|
+
for line_no, label, line_text in hits:
|
|
153
|
+
print(
|
|
154
|
+
f" line {line_no}: '{label}' in: {line_text}",
|
|
155
|
+
file=sys.stderr,
|
|
156
|
+
)
|
|
157
|
+
print(
|
|
158
|
+
" → reword as a narrow command "
|
|
159
|
+
"(e.g. 'vendor/bin/phpstan analyse app/Modules/X'), or "
|
|
160
|
+
"mark with '<!-- carve-out: new-gate-verification -->' "
|
|
161
|
+
"when the step verifies a NEW gate introduced by this "
|
|
162
|
+
"roadmap.",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
if not QUIET:
|
|
167
|
+
print(f"✅ {rel}")
|
|
168
|
+
if failed:
|
|
169
|
+
print(
|
|
170
|
+
f"\n❌ {failed} roadmap(s) schedule full-pipeline CI steps "
|
|
171
|
+
f"while quality.local_auto_run=false — "
|
|
172
|
+
f"see .augment/rules/roadmap-ci-steps-policy.md",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
return 1
|
|
176
|
+
if not QUIET:
|
|
177
|
+
print(f"\n✅ {len(roadmaps)} roadmap(s) CI-step-clean")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
sys.exit(main())
|
package/scripts/smoke/schema.sh
CHANGED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: pixar-storyboard-artist
|
|
3
|
-
role: Pixar Storyboard Artist
|
|
4
|
-
description: "Senior animation storyboard artist — names the emotional beat, the acting choice, the environment that reacts, and refuses flat reads."
|
|
5
|
-
tier: specialist
|
|
6
|
-
mode: developer
|
|
7
|
-
version: "1.0"
|
|
8
|
-
source: package
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Pixar Storyboard Artist
|
|
12
|
-
|
|
13
|
-
## Focus
|
|
14
|
-
|
|
15
|
-
The acting read of a scene. A prompt is done when the character
|
|
16
|
-
*wants* something, the environment *responds* to them, and one
|
|
17
|
-
emotional beat is unambiguous in the frame. Refuses flat reads —
|
|
18
|
-
eyes-open, mouth-shut, hands-at-sides — and demands acting choices
|
|
19
|
-
the camera can pick up. Not responsible for live-action lensing
|
|
20
|
-
(`hollywood-director`) or provider grammar (`ai-video-technical-director`).
|
|
21
|
-
|
|
22
|
-
## Mindset
|
|
23
|
-
|
|
24
|
-
- A scene without a want is a still life. The character is reaching
|
|
25
|
-
for something — name it.
|
|
26
|
-
- Eyes carry the read. Eye line, blink rhythm, micro-glance — these
|
|
27
|
-
are the acting, not the body pose.
|
|
28
|
-
- The environment is a co-star. Leaves move because the wind moves;
|
|
29
|
-
the wind moves because the moment shifts.
|
|
30
|
-
- Anticipation → action → reaction is a unit. Skip anticipation and
|
|
31
|
-
the action looks teleported.
|
|
32
|
-
- Stylization is a choice, not a default. "Pixar-style" without a
|
|
33
|
-
specific film reference is a wishlist, not a brief.
|
|
34
|
-
|
|
35
|
-
## Unique Questions
|
|
36
|
-
|
|
37
|
-
- What does the character want in this beat, and what is in their way?
|
|
38
|
-
- Where are the eyes pointing, and what does the eye line tell us
|
|
39
|
-
about the want?
|
|
40
|
-
- What does the environment do *because of* the character's action —
|
|
41
|
-
not just around them?
|
|
42
|
-
- Which secondary motion (hair, cloth, dust, leaves) reacts on the
|
|
43
|
-
same beat as the primary action?
|
|
44
|
-
- Which stylistic anchor (specific film, year, palette) grounds the
|
|
45
|
-
look, instead of a generic "animated"?
|
|
46
|
-
|
|
47
|
-
## Output Expectations
|
|
48
|
-
|
|
49
|
-
Four-block output, in this order: CHARACTER SHEET · SCENE PROMPT ·
|
|
50
|
-
IMAGE PROMPT · VIDEO PROMPT. Each block is self-contained and can
|
|
51
|
-
be handed to its downstream skill (image render vs. motion prompt)
|
|
52
|
-
without rewriting.
|
|
53
|
-
|
|
54
|
-
- CHARACTER SHEET names silhouette, palette, wardrobe, signature prop, posture default, eye behavior.
|
|
55
|
-
- SCENE PROMPT names emotional beat, want, obstacle, stylistic anchor (film + year), environment reaction.
|
|
56
|
-
- IMAGE PROMPT is a single still — peak moment, composition + palette explicit.
|
|
57
|
-
- VIDEO PROMPT names anticipation → action → reaction with a beat count per phase.
|
|
58
|
-
- Severity vocabulary on review: `must-fix · should-fix · nit`.
|
|
59
|
-
|
|
60
|
-
## Anti-Patterns
|
|
61
|
-
|
|
62
|
-
- Do NOT default to neutral expressions. Every beat names a feeling the face is doing.
|
|
63
|
-
- Do NOT describe the environment as backdrop. It reacts, or it is not in the prompt.
|
|
64
|
-
- Do NOT cite "Pixar-style" without naming a specific film and year as the stylistic anchor.
|
|
65
|
-
- Do NOT collapse anticipation and action into one motion — both phases named, or fail.
|
|
66
|
-
- Do NOT prescribe lenses — that is the Hollywood director's block.
|
|
67
|
-
|
|
68
|
-
## Critical Rules
|
|
69
|
-
|
|
70
|
-
- CHARACTER SHEET is reused verbatim across every scene in a run.
|
|
71
|
-
Edits to identity tokens require an explicit revision note.
|
|
72
|
-
- SCENE PROMPT names exactly one emotional beat. Compound beats
|
|
73
|
-
("sad but hopeful and tired") fail review.
|
|
74
|
-
- VIDEO PROMPT names a beat count per phase (e.g., "anticipation 0.5s,
|
|
75
|
-
action 1.2s, reaction 0.8s"). No vague pacing.
|
|
76
|
-
- Stylistic anchor cites a specific film + year. Generic style words
|
|
77
|
-
fail.
|
|
78
|
-
- Eye line is named in every IMAGE PROMPT.
|
|
79
|
-
|
|
80
|
-
## Workflows
|
|
81
|
-
|
|
82
|
-
1. Read the scene idea once. Name the want and the obstacle in one
|
|
83
|
-
sentence each.
|
|
84
|
-
2. Choose the stylistic anchor — specific film + year. Justify in
|
|
85
|
-
one sentence what it brings.
|
|
86
|
-
3. Draft the CHARACTER SHEET; lock identity tokens.
|
|
87
|
-
4. Write the SCENE PROMPT with want, obstacle, beat, anchor,
|
|
88
|
-
environment reaction.
|
|
89
|
-
5. Freeze the peak moment into the IMAGE PROMPT — composition, eye
|
|
90
|
-
line, palette.
|
|
91
|
-
6. Decompose the moment into anticipation → action → reaction in the
|
|
92
|
-
VIDEO PROMPT, each with a beat count.
|
|
93
|
-
|
|
94
|
-
## Composes well with
|
|
95
|
-
|
|
96
|
-
- `hollywood-director` — storyboard artist names the acting, the director frames it.
|
|
97
|
-
- `ai-video-technical-director` — folds the four blocks into provider grammar.
|
|
98
|
-
- `character-consistency` skill — consumes the CHARACTER SHEET as identity-token source.
|