@event4u/agent-config 1.18.0 → 1.20.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/agent-handoff.md +14 -10
- package/.agent-src/commands/chat-history/import.md +170 -0
- package/.agent-src/commands/chat-history/learn.md +178 -0
- package/.agent-src/commands/chat-history/show.md +17 -18
- package/.agent-src/commands/chat-history.md +26 -25
- package/.agent-src/commands/council/default.md +77 -82
- package/.agent-src/commands/create-pr.md +28 -8
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +22 -0
- package/.agent-src/rules/direct-answers.md +11 -2
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +38 -6
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-attribution-footers.md +48 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +2 -1
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +50 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +22 -5
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/ai-council/SKILL.md +4 -5
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +21 -26
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +110 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +8 -4
- package/AGENTS.md +44 -3
- package/CHANGELOG.md +173 -0
- package/README.md +22 -22
- package/config/agent-settings.template.yml +42 -13
- package/config/gitignore-block.txt +4 -4
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +18 -13
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +348 -126
- package/docs/contracts/hook-architecture-v1.md +220 -0
- package/docs/contracts/memory-visibility-v1.md +122 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +2 -1
- package/docs/development.md +4 -1
- package/docs/getting-started.md +21 -29
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/hook-payload-capture.md +221 -0
- package/docs/migrations/commands-1.15.0.md +17 -12
- package/docs/skills-catalog.md +5 -4
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/scripts/agent-config +45 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +25 -9
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/pricing.py +10 -9
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +963 -597
- package/scripts/check_always_budget.py +7 -2
- package/scripts/check_references.py +12 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +407 -0
- package/scripts/hook_manifest.yaml +217 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +383 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +157 -0
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.py +725 -87
- package/scripts/install.sh +38 -1
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +217 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +163 -1
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/verify_before_complete_hook.py +216 -0
- package/.agent-src/commands/chat-history/checkpoint.md +0 -126
- package/.agent-src/commands/chat-history/clear.md +0 -103
- package/.agent-src/commands/chat-history/resume.md +0 -183
- package/.agent-src/rules/chat-history-cadence.md +0 -109
- package/.agent-src/rules/chat-history-ownership.md +0 -123
- package/.agent-src/rules/chat-history-visibility.md +0 -96
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
- package/scripts/check_phase_coupling.py +0 -148
|
@@ -114,6 +114,11 @@ def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
|
114
114
|
except json.JSONDecodeError:
|
|
115
115
|
return 0 # malformed stdin → silent no-op, never block
|
|
116
116
|
|
|
117
|
+
# Unwrap dispatcher envelope (Phase 7.3, hook-architecture-v1.md).
|
|
118
|
+
if all(k in payload for k in ("schema_version", "platform", "event", "payload")):
|
|
119
|
+
inner = payload.get("payload")
|
|
120
|
+
payload = inner if isinstance(inner, dict) else {}
|
|
121
|
+
|
|
117
122
|
tool = payload.get("tool_name") or payload.get("toolName") or payload.get("tool")
|
|
118
123
|
if not isinstance(tool, str) or tool not in WRITE_TOOLS:
|
|
119
124
|
return 0
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
"pattern": "^[a-z][a-z0-9-]*$"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
+
"tier": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["senior"],
|
|
43
|
+
"description": "Optional tier marker. `senior` opts the skill into the Senior-Tier Required Structure check (Context-First lead, Related Skills, Proactive Triggers, Output Artifacts) per .agent-src.uncompressed/rules/skill-quality.md."
|
|
44
|
+
},
|
|
40
45
|
"execution": {
|
|
41
46
|
"type": "object",
|
|
42
47
|
"additionalProperties": false,
|
package/scripts/skill_linter.py
CHANGED
|
@@ -99,6 +99,17 @@ TYPE_PATTERN = re.compile(r'^type:\s*"?(always|auto)"?\s*$', re.MULTILINE)
|
|
|
99
99
|
SOURCE_PATTERN = re.compile(r'^source:\s*"?(package|project)"?\s*$', re.MULTILINE)
|
|
100
100
|
STATUS_PATTERN = re.compile(r'^status:\s*"?(active|deprecated|superseded)"?\s*$', re.MULTILINE)
|
|
101
101
|
REPLACED_BY_PATTERN = re.compile(r'^replaced_by:\s*"?([\w-]+)"?\s*$', re.MULTILINE)
|
|
102
|
+
TIER_PATTERN = re.compile(r'^tier:\s*"?([\w-]+)"?\s*$', re.MULTILINE)
|
|
103
|
+
|
|
104
|
+
# --- Senior-tier required-block patterns (skill-quality.md § Senior-Tier Required Structure) ---
|
|
105
|
+
# Heading-only checks; detail-shape lives in skill-quality-mechanics.md.
|
|
106
|
+
SENIOR_RELATED_SKILLS_PATTERN = re.compile(r"^##\s+Related Skills\s*$", re.MULTILINE)
|
|
107
|
+
SENIOR_RELATED_WHEN_PATTERN = re.compile(r"\*\*WHEN to use this\*\*", re.IGNORECASE)
|
|
108
|
+
SENIOR_RELATED_WHEN_NOT_PATTERN = re.compile(r"\*\*WHEN NOT to use this\*\*", re.IGNORECASE)
|
|
109
|
+
SENIOR_PROACTIVE_PATTERN = re.compile(
|
|
110
|
+
r"^##\s+When the agent should load this\s*$", re.MULTILINE
|
|
111
|
+
)
|
|
112
|
+
SENIOR_OUTPUT_PATTERN = re.compile(r"^##\s+Output\s*$", re.MULTILINE)
|
|
102
113
|
H1_PATTERN = re.compile(r"^# .+", re.MULTILINE)
|
|
103
114
|
DOUBLE_BLANK_PATTERN = re.compile(r"\n{3,}")
|
|
104
115
|
|
|
@@ -415,6 +426,11 @@ def lint_skill(path: Path, text: str) -> LintResult:
|
|
|
415
426
|
if execution is not None:
|
|
416
427
|
issues.extend(lint_execution_metadata(execution))
|
|
417
428
|
|
|
429
|
+
# --- Senior-tier required-block check (skill-quality.md § Senior-Tier Required Structure) ---
|
|
430
|
+
tier_match = TIER_PATTERN.search(frontmatter)
|
|
431
|
+
if tier_match and tier_match.group(1) == "senior":
|
|
432
|
+
issues.extend(lint_senior_tier_blocks(text))
|
|
433
|
+
|
|
418
434
|
procedure_block = find_procedure_block(text)
|
|
419
435
|
if procedure_block is not None:
|
|
420
436
|
if not procedure_block:
|
|
@@ -603,6 +619,57 @@ def parse_execution_block(frontmatter: str) -> Optional[dict]:
|
|
|
603
619
|
return result
|
|
604
620
|
|
|
605
621
|
|
|
622
|
+
def lint_senior_tier_blocks(text: str) -> List[Issue]:
|
|
623
|
+
"""Validate the four required blocks for `tier: senior` skills.
|
|
624
|
+
|
|
625
|
+
Per .agent-src.uncompressed/rules/skill-quality.md § Senior-Tier
|
|
626
|
+
Required Structure: Context-First lead (description), Related Skills
|
|
627
|
+
(with WHEN / WHEN NOT lists), Proactive Triggers, Output Artifacts.
|
|
628
|
+
|
|
629
|
+
The Context-First lead is checked structurally via description length
|
|
630
|
+
+ content; here we enforce the three section blocks and the WHEN /
|
|
631
|
+
WHEN NOT two-list pattern inside Related Skills.
|
|
632
|
+
"""
|
|
633
|
+
issues: List[Issue] = []
|
|
634
|
+
|
|
635
|
+
if not SENIOR_RELATED_SKILLS_PATTERN.search(text):
|
|
636
|
+
issues.append(Issue(
|
|
637
|
+
"error",
|
|
638
|
+
"missing_senior_related_skills",
|
|
639
|
+
"Senior-tier skill missing `## Related Skills` block (skill-quality.md § Senior-Tier Required Structure)",
|
|
640
|
+
))
|
|
641
|
+
else:
|
|
642
|
+
related_block = extract_section_block(text, "Related Skills") or ""
|
|
643
|
+
if not SENIOR_RELATED_WHEN_PATTERN.search(related_block):
|
|
644
|
+
issues.append(Issue(
|
|
645
|
+
"error",
|
|
646
|
+
"missing_senior_related_when",
|
|
647
|
+
"Senior-tier `## Related Skills` block missing `**WHEN to use this**` list",
|
|
648
|
+
))
|
|
649
|
+
if not SENIOR_RELATED_WHEN_NOT_PATTERN.search(related_block):
|
|
650
|
+
issues.append(Issue(
|
|
651
|
+
"error",
|
|
652
|
+
"missing_senior_related_when_not",
|
|
653
|
+
"Senior-tier `## Related Skills` block missing `**WHEN NOT to use this**` list",
|
|
654
|
+
))
|
|
655
|
+
|
|
656
|
+
if not SENIOR_PROACTIVE_PATTERN.search(text):
|
|
657
|
+
issues.append(Issue(
|
|
658
|
+
"error",
|
|
659
|
+
"missing_senior_proactive_triggers",
|
|
660
|
+
"Senior-tier skill missing `## When the agent should load this` block",
|
|
661
|
+
))
|
|
662
|
+
|
|
663
|
+
if not SENIOR_OUTPUT_PATTERN.search(text):
|
|
664
|
+
issues.append(Issue(
|
|
665
|
+
"error",
|
|
666
|
+
"missing_senior_output_artifacts",
|
|
667
|
+
"Senior-tier skill missing `## Output` block declaring artifact name + shape",
|
|
668
|
+
))
|
|
669
|
+
|
|
670
|
+
return issues
|
|
671
|
+
|
|
672
|
+
|
|
606
673
|
def lint_execution_metadata(execution: dict) -> List[Issue]:
|
|
607
674
|
"""Validate the execution block of a skill."""
|
|
608
675
|
issues: List[Issue] = []
|
|
@@ -1656,6 +1723,70 @@ def lint_governance(path: Path, text: str, artifact_type: str, repo_root: Path |
|
|
|
1656
1723
|
return issues
|
|
1657
1724
|
|
|
1658
1725
|
|
|
1726
|
+
# --- Structural malice check (see road-to-suite-closure Phase 5) ---
|
|
1727
|
+
#
|
|
1728
|
+
# Five regex patterns scan skill / rule / command bodies for **structural**
|
|
1729
|
+
# (not semantic) malice. Findings surface as ``Issue("error",
|
|
1730
|
+
# "malice:<pattern>", "<line>:<matched>")`` so ``compute_exit_code`` can
|
|
1731
|
+
# emit exit code 3 (security-failure), distinct from 2 (build-failure).
|
|
1732
|
+
# Semantic checks (PII leakage, prompt injection) are deferred to v2.
|
|
1733
|
+
|
|
1734
|
+
# (a) credential exfil — curl|wget piping ${TOKEN}/${KEY}/${SECRET}/...
|
|
1735
|
+
# env vars or hitting ~/.aws/ ~/.ssh/ secrets.
|
|
1736
|
+
_MALICE_CRED_EXFIL = re.compile(
|
|
1737
|
+
r"\b(?:curl|wget)\b[^\n]*"
|
|
1738
|
+
r"(?:\$\{?[A-Z_]*(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)[A-Z_]*\}?"
|
|
1739
|
+
r"|~/\.(?:aws|ssh)/)"
|
|
1740
|
+
)
|
|
1741
|
+
# (b) arbitrary execution — eval/exec over a network-fetched payload, or
|
|
1742
|
+
# `bash <(curl ...)` / `sh <(wget ...)` style remote-execution.
|
|
1743
|
+
_MALICE_REMOTE_EXEC = re.compile(
|
|
1744
|
+
r"(?:\b(?:eval|exec)\s*\([^)]*(?:curl|wget|requests\.get|urllib)"
|
|
1745
|
+
r"|\b(?:bash|sh|zsh)\s*<\s*\(\s*(?:curl|wget))"
|
|
1746
|
+
)
|
|
1747
|
+
# (c) force-push to a protected ref.
|
|
1748
|
+
_MALICE_FORCE_PUSH = re.compile(
|
|
1749
|
+
r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b[^\n]*"
|
|
1750
|
+
r"\b(?:main|master|prod|production|release)\b"
|
|
1751
|
+
)
|
|
1752
|
+
# (d) world-readable secrets — chmod 0?[4567]xx on .pem/.key/.env files.
|
|
1753
|
+
_MALICE_CHMOD_SECRETS = re.compile(
|
|
1754
|
+
r"\bchmod\s+0?[4567]\d{2}\s+[^\n]*\.(?:pem|key|env)\b"
|
|
1755
|
+
)
|
|
1756
|
+
# (e) unbounded subprocess shell injection — shell=True interpolating ${VAR}.
|
|
1757
|
+
_MALICE_SHELL_INJECT = re.compile(
|
|
1758
|
+
r"\bsubprocess\.[A-Za-z_]+\s*\([^)]*shell\s*=\s*True[^)]*\$\{"
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
_MALICE_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|
1762
|
+
("cred_exfil", _MALICE_CRED_EXFIL),
|
|
1763
|
+
("remote_exec", _MALICE_REMOTE_EXEC),
|
|
1764
|
+
("force_push_protected", _MALICE_FORCE_PUSH),
|
|
1765
|
+
("chmod_secrets", _MALICE_CHMOD_SECRETS),
|
|
1766
|
+
("shell_injection", _MALICE_SHELL_INJECT),
|
|
1767
|
+
]
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def check_structural_malice(text: str) -> List[Issue]:
|
|
1771
|
+
"""Return one Issue per malice match. Empty list when clean.
|
|
1772
|
+
|
|
1773
|
+
Issue shape: ``Issue("error", f"malice:{name}", f"{line}:{matched}")``.
|
|
1774
|
+
The ``format_text`` renderer special-cases the ``malice:`` code prefix
|
|
1775
|
+
to emit ``<path>:<line>:malice:<pattern>:<matched>`` per Phase 5.2.
|
|
1776
|
+
"""
|
|
1777
|
+
issues: List[Issue] = []
|
|
1778
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
1779
|
+
for name, pattern in _MALICE_PATTERNS:
|
|
1780
|
+
match = pattern.search(raw)
|
|
1781
|
+
if match:
|
|
1782
|
+
issues.append(Issue(
|
|
1783
|
+
severity="error",
|
|
1784
|
+
code=f"malice:{name}",
|
|
1785
|
+
message=f"{lineno}:{match.group(0).strip()}",
|
|
1786
|
+
))
|
|
1787
|
+
return issues
|
|
1788
|
+
|
|
1789
|
+
|
|
1659
1790
|
# --- Output-schema check (see road-to-trigger-evals Phase 3.5) ---
|
|
1660
1791
|
#
|
|
1661
1792
|
# Skills that freeze an output shape (`refine-ticket`, `estimate-ticket`)
|
|
@@ -1865,11 +1996,36 @@ def lint_file(path: Path, repo_root: Path | None = None) -> LintResult:
|
|
|
1865
1996
|
result.issues.extend(schema_issues)
|
|
1866
1997
|
result.status = classify_status(result.issues)
|
|
1867
1998
|
|
|
1999
|
+
# Post-processing: structural malice scan (errors). Skills, rules,
|
|
2000
|
+
# and commands carry executable patterns; guidelines/personas are
|
|
2001
|
+
# prose-only and skipped to keep noise low.
|
|
2002
|
+
if artifact_type in ("skill", "rule", "command"):
|
|
2003
|
+
malice_issues = check_structural_malice(text)
|
|
2004
|
+
if malice_issues:
|
|
2005
|
+
result.issues.extend(malice_issues)
|
|
2006
|
+
result.status = classify_status(result.issues)
|
|
2007
|
+
|
|
1868
2008
|
return result
|
|
1869
2009
|
|
|
1870
2010
|
|
|
1871
2011
|
def format_text(results: list[LintResult]) -> str:
|
|
1872
2012
|
lines: list[str] = []
|
|
2013
|
+
# Phase 5.2: malice findings render in the spec shape
|
|
2014
|
+
# ``<path>:<line>:malice:<pattern>:<matched>`` ahead of the badge
|
|
2015
|
+
# block so security-failures are grep-able from the top.
|
|
2016
|
+
malice_total = 0
|
|
2017
|
+
for result in results:
|
|
2018
|
+
for issue in result.issues:
|
|
2019
|
+
if issue.code.startswith("malice:"):
|
|
2020
|
+
pattern_name = issue.code.split(":", 1)[1]
|
|
2021
|
+
lineno, _, matched = issue.message.partition(":")
|
|
2022
|
+
lines.append(
|
|
2023
|
+
f"{result.file}:{lineno}:malice:{pattern_name}:{matched}"
|
|
2024
|
+
)
|
|
2025
|
+
malice_total += 1
|
|
2026
|
+
if malice_total:
|
|
2027
|
+
lines.append("")
|
|
2028
|
+
|
|
1873
2029
|
for result in results:
|
|
1874
2030
|
badge = {"pass": "[PASS]", "pass_with_warnings": "[WARN]", "fail": "[FAIL]"}[result.status]
|
|
1875
2031
|
lines.append(f"{badge} {result.file} ({result.artifact_type})")
|
|
@@ -1888,7 +2044,8 @@ def format_text(results: list[LintResult]) -> str:
|
|
|
1888
2044
|
fails = sum(1 for r in results if r.status == "fail")
|
|
1889
2045
|
warns = sum(1 for r in results if r.status == "pass_with_warnings")
|
|
1890
2046
|
passes = sum(1 for r in results if r.status == "pass")
|
|
1891
|
-
|
|
2047
|
+
suffix = f", {malice_total} malice" if malice_total else ""
|
|
2048
|
+
lines.append(f"Summary: {passes} pass, {warns} warn, {fails} fail, {total} total{suffix}")
|
|
1892
2049
|
return "\n".join(lines)
|
|
1893
2050
|
|
|
1894
2051
|
|
|
@@ -2087,6 +2244,11 @@ def check_duplication(root: Path) -> list[LintResult]:
|
|
|
2087
2244
|
|
|
2088
2245
|
|
|
2089
2246
|
def compute_exit_code(results: list[LintResult], strict_warnings: bool) -> int:
|
|
2247
|
+
# Phase 5.2: structural-malice findings emit exit code 3 (security-
|
|
2248
|
+
# failure), distinct from 2 (build-failure) so CI surfaces can split.
|
|
2249
|
+
for r in results:
|
|
2250
|
+
if any(issue.code.startswith("malice:") for issue in r.issues):
|
|
2251
|
+
return 3
|
|
2090
2252
|
if any(r.status == "fail" for r in results):
|
|
2091
2253
|
return 2
|
|
2092
2254
|
if any(r.status == "pass_with_warnings" for r in results) and strict_warnings:
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Sync `.agent-settings.yml` against the template + profile.
|
|
2
|
+
"""Sync `.agent-settings.yml` against the template + profile (additive merge).
|
|
3
3
|
|
|
4
4
|
Applies the section-aware merge rules documented in
|
|
5
5
|
`docs/guidelines/agent-infra/layered-settings.md`:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
- **User lines are preserved verbatim** — comments, quoting, and key order
|
|
8
|
+
survive every sync. Existing values, custom inline comments, and
|
|
9
|
+
user-chosen ordering are never modified.
|
|
10
|
+
- Missing template keys are inserted (leaf into existing parent section,
|
|
11
|
+
full subtree at EOF for entirely missing top-level sections).
|
|
12
|
+
- Top-level user-only sections (no home in the template) are moved to a
|
|
13
|
+
single-level `_user:` block at the end of the file.
|
|
14
|
+
- The `_user:` block is single-level only — legacy multi-prefix
|
|
15
|
+
corruption (`_user._user.foo`) heals to `foo` on the next sync.
|
|
16
|
+
- Template comment changes on already-existing user keys do **not**
|
|
17
|
+
propagate (existing line untouched is the deal).
|
|
13
18
|
|
|
14
19
|
Idempotent — writing a file that is already in sync is a no-op.
|
|
15
20
|
|
|
@@ -33,7 +38,8 @@ import sys
|
|
|
33
38
|
from pathlib import Path
|
|
34
39
|
|
|
35
40
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
36
|
-
import install as _install # noqa: E402 —
|
|
41
|
+
import install as _install # noqa: E402 — profile parsing + template rendering
|
|
42
|
+
import sync_yaml_rt as _rt # noqa: E402 — additive round-trip merge
|
|
37
43
|
|
|
38
44
|
try:
|
|
39
45
|
import yaml # type: ignore
|
|
@@ -46,126 +52,6 @@ DEFAULT_TEMPLATE = Path(__file__).resolve().parent.parent / "config" / "agent-se
|
|
|
46
52
|
DEFAULT_PROFILE_DIR = Path(__file__).resolve().parent.parent / "config" / "profiles"
|
|
47
53
|
|
|
48
54
|
|
|
49
|
-
def _flatten(data: dict, prefix: str = "") -> dict[str, object]:
|
|
50
|
-
"""Flatten nested dicts to dotted keys — recurses to all leaves.
|
|
51
|
-
|
|
52
|
-
Lists, scalars, and ``None`` are leaves. Dicts are walked and their
|
|
53
|
-
keys folded into the dotted path.
|
|
54
|
-
"""
|
|
55
|
-
out: dict[str, object] = {}
|
|
56
|
-
for key, value in data.items():
|
|
57
|
-
path = f"{prefix}{key}"
|
|
58
|
-
if isinstance(value, dict):
|
|
59
|
-
out.update(_flatten(value, prefix=f"{path}."))
|
|
60
|
-
else:
|
|
61
|
-
out[path] = value
|
|
62
|
-
return out
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _as_yaml_value(value: object) -> str | None:
|
|
66
|
-
"""Format *value* as an inline-YAML literal.
|
|
67
|
-
|
|
68
|
-
Returns ``None`` when the value cannot be safely represented as a
|
|
69
|
-
scalar / flow-style sequence (e.g. unsupported types). Callers
|
|
70
|
-
must skip those keys so the template default sticks instead of
|
|
71
|
-
producing malformed YAML.
|
|
72
|
-
"""
|
|
73
|
-
if isinstance(value, bool):
|
|
74
|
-
return "true" if value else "false"
|
|
75
|
-
if isinstance(value, int):
|
|
76
|
-
return str(value)
|
|
77
|
-
if isinstance(value, float):
|
|
78
|
-
return repr(value)
|
|
79
|
-
if value is None:
|
|
80
|
-
return "~"
|
|
81
|
-
if isinstance(value, list):
|
|
82
|
-
items: list[str] = []
|
|
83
|
-
for item in value:
|
|
84
|
-
rendered = _as_yaml_value(item)
|
|
85
|
-
if rendered is None:
|
|
86
|
-
return None
|
|
87
|
-
items.append(rendered)
|
|
88
|
-
return "[" + ", ".join(items) + "]"
|
|
89
|
-
if isinstance(value, str):
|
|
90
|
-
return _install._yaml_scalar(value)
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _template_keys(template_body: str) -> set[str]:
|
|
95
|
-
"""Return the set of dotted keys declared by the rendered template."""
|
|
96
|
-
data = yaml.safe_load(template_body) or {}
|
|
97
|
-
if not isinstance(data, dict):
|
|
98
|
-
return set()
|
|
99
|
-
return set(_flatten(data).keys())
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _apply_user_values(template_body: str, user_flat: dict[str, object]) -> str:
|
|
103
|
-
"""Overlay every known user value on the rendered template body.
|
|
104
|
-
|
|
105
|
-
Keys whose value cannot be rendered inline (see :func:`_as_yaml_value`)
|
|
106
|
-
are skipped so the template default survives instead of corrupting
|
|
107
|
-
the file.
|
|
108
|
-
"""
|
|
109
|
-
body = template_body
|
|
110
|
-
for dotted, value in user_flat.items():
|
|
111
|
-
rendered = _as_yaml_value(value)
|
|
112
|
-
if rendered is None:
|
|
113
|
-
continue
|
|
114
|
-
body = _install._replace_template_value_raw(body, dotted, rendered)
|
|
115
|
-
return body
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _append_unknown(body: str, user_flat: dict[str, object], known: set[str]) -> str:
|
|
119
|
-
"""Emit user keys that have no home in the template under `_user:`."""
|
|
120
|
-
unknown = sorted(k for k in user_flat if k not in known)
|
|
121
|
-
if not unknown:
|
|
122
|
-
return body
|
|
123
|
-
lines = [
|
|
124
|
-
"",
|
|
125
|
-
"# Unknown keys preserved by sync_agent_settings.py — review and move",
|
|
126
|
-
"# them into the template or drop them.",
|
|
127
|
-
"_user:",
|
|
128
|
-
]
|
|
129
|
-
for key in unknown:
|
|
130
|
-
rendered = _as_yaml_value(user_flat[key])
|
|
131
|
-
if rendered is None:
|
|
132
|
-
continue
|
|
133
|
-
lines.append(f" {key}: {rendered}")
|
|
134
|
-
suffix = "\n".join(lines) + "\n"
|
|
135
|
-
return body + (suffix if body.endswith("\n") else "\n" + suffix)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def render_target(template_body: str, user_data: dict) -> str:
|
|
139
|
-
"""Return the desired `.agent-settings.yml` body for the given user data.
|
|
140
|
-
|
|
141
|
-
The trailing ``_user:`` block (emitted by :func:`_append_unknown`) is
|
|
142
|
-
already in dotted-key form on every read after the first sync. Re-
|
|
143
|
-
flattening it would prepend another ``_user.`` segment on every run
|
|
144
|
-
and accumulate forever, so we strip the wrapper and merge its
|
|
145
|
-
contents straight into the flat dict.
|
|
146
|
-
"""
|
|
147
|
-
if user_data:
|
|
148
|
-
user_only = user_data.pop("_user", None) if isinstance(user_data, dict) else None
|
|
149
|
-
user_flat = _flatten(user_data)
|
|
150
|
-
if isinstance(user_only, dict):
|
|
151
|
-
for key, value in user_only.items():
|
|
152
|
-
# Dotted keys round-trip verbatim — never re-flatten them.
|
|
153
|
-
if isinstance(key, str):
|
|
154
|
-
# Heal legacy corruption: pre-fix syncs prepended a
|
|
155
|
-
# `_user.` segment per run, so a key may carry an
|
|
156
|
-
# arbitrary number of them. Strip them all back to
|
|
157
|
-
# the original leaf path.
|
|
158
|
-
healed = key
|
|
159
|
-
while healed.startswith("_user."):
|
|
160
|
-
healed = healed[len("_user."):]
|
|
161
|
-
user_flat[healed] = value
|
|
162
|
-
else:
|
|
163
|
-
user_flat = {}
|
|
164
|
-
known = _template_keys(template_body)
|
|
165
|
-
body = _apply_user_values(template_body, user_flat)
|
|
166
|
-
return _append_unknown(body, user_flat, known)
|
|
167
|
-
|
|
168
|
-
|
|
169
55
|
def load_profile(profile_dir: Path, profile: str) -> dict[str, str]:
|
|
170
56
|
profile_source = profile_dir / f"{profile}.ini"
|
|
171
57
|
if not profile_source.is_file():
|
|
@@ -231,10 +117,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
231
117
|
except FileNotFoundError as exc:
|
|
232
118
|
print(f"error: {exc}", file=sys.stderr)
|
|
233
119
|
return 2
|
|
120
|
+
except yaml.YAMLError as exc:
|
|
121
|
+
print(f"error: cannot parse {target}: {exc}", file=sys.stderr)
|
|
122
|
+
return 2
|
|
234
123
|
|
|
235
|
-
new_text = render_target(template_body, user_data)
|
|
236
124
|
existing_text = target.read_text(encoding="utf-8") if target.is_file() else ""
|
|
237
125
|
|
|
126
|
+
if existing_text:
|
|
127
|
+
# Additive merge — preserves user lines verbatim, inserts only
|
|
128
|
+
# the template keys the user is missing.
|
|
129
|
+
try:
|
|
130
|
+
new_text = _rt.sync(existing_text, template_body)
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
print(
|
|
133
|
+
f"error: cannot parse {target}: {exc}",
|
|
134
|
+
file=sys.stderr,
|
|
135
|
+
)
|
|
136
|
+
return 2
|
|
137
|
+
else:
|
|
138
|
+
# First-run / file absent — write the rendered template as-is.
|
|
139
|
+
new_text = template_body
|
|
140
|
+
|
|
238
141
|
if new_text == existing_text:
|
|
239
142
|
if not args.quiet:
|
|
240
143
|
print(f"✅ {target}: already in sync (profile={profile})")
|