@event4u/agent-config 1.19.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 +4 -7
- package/.agent-src/commands/create-pr.md +28 -8
- 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/direct-answers.md +10 -2
- package/.agent-src/rules/language-and-tone.md +37 -6
- package/.agent-src/rules/no-attribution-footers.md +48 -0
- package/.agent-src/rules/no-roadmap-references.md +1 -1
- package/.agent-src/rules/skill-quality.md +49 -0
- package/.agent-src/rules/user-interaction.md +21 -5
- 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/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/templates/agent-settings.md +5 -26
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +7 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +0 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +0 -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/memory_visibility.py +2 -3
- package/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +8 -4
- package/AGENTS.md +44 -3
- package/CHANGELOG.md +111 -0
- package/README.md +6 -6
- package/config/agent-settings.template.yml +19 -13
- package/config/gitignore-block.txt +4 -4
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +14 -12
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/file-ownership-matrix.json +341 -126
- package/docs/contracts/hook-architecture-v1.md +8 -1
- package/docs/contracts/memory-visibility-v1.md +8 -24
- package/docs/customization.md +1 -1
- package/docs/getting-started.md +21 -29
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- 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 +1 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/clients.py +1 -1
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/pricing.py +10 -9
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/chat_history.py +952 -596
- package/scripts/check_references.py +12 -2
- package/scripts/council_cli.py +54 -4
- package/scripts/hook_manifest.yaml +33 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/dispatch_hook.py +35 -0
- package/scripts/hooks_status.py +12 -1
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.sh +37 -0
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +2 -1
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +163 -1
- package/scripts/update_prices.py +3 -3
- 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 -143
- package/.agent-src/rules/chat-history-ownership.md +0 -124
- package/.agent-src/rules/chat-history-visibility.md +0 -97
- 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
|
@@ -57,7 +57,8 @@ EVENT_VOCABULARY: set[str] = {
|
|
|
57
57
|
# Known platform identifiers. New platforms MUST be added here as they
|
|
58
58
|
# land — the linter is the gate that proves no orphan slot escapes.
|
|
59
59
|
KNOWN_PLATFORMS: set[str] = {
|
|
60
|
-
"augment", "claude", "
|
|
60
|
+
"augment", "claude", "cowork",
|
|
61
|
+
"cursor", "cline", "windsurf", "gemini", "copilot",
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Redact captured hook payloads for the verified-platforms roadmap.
|
|
3
|
+
|
|
4
|
+
Reads JSON capture files written by ``dispatch_hook.py`` (when
|
|
5
|
+
``AGENT_HOOK_CAPTURE_DIR`` is set) and produces a redacted version
|
|
6
|
+
suitable for pasting into
|
|
7
|
+
``agents/roadmaps/road-to-verified-chat-history-platforms.md``.
|
|
8
|
+
|
|
9
|
+
Redaction policy (per the roadmap's Capture-and-redact protocol):
|
|
10
|
+
|
|
11
|
+
- Replace string values at known user-content paths with
|
|
12
|
+
``<REDACTED>``. Default field allowlist mirrors the fallback list
|
|
13
|
+
in ``scripts/chat_history.py::_extract_hook_text`` plus Augment's
|
|
14
|
+
nested ``conversation.*`` shape.
|
|
15
|
+
- Preserve envelope keys (``hook_event_name``, ``session_id``,
|
|
16
|
+
``platform``, ``event``, ``cwd``, ``workspace_roots``,
|
|
17
|
+
``transcript_path``, ``model``, ``cursor_version``, …) so the
|
|
18
|
+
schema is reviewable.
|
|
19
|
+
- ``--strict`` redacts any string longer than ``--max-len`` (default
|
|
20
|
+
120) chars regardless of key, as a safety net for unknown fields.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
|
|
24
|
+
python3 scripts/redact_hook_capture.py <input> [--out <path>] [--strict]
|
|
25
|
+
|
|
26
|
+
Input may be a single JSON file or a directory; with a directory,
|
|
27
|
+
every ``*.json`` is redacted and written next to the original with
|
|
28
|
+
the suffix ``.redacted.json``.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
REDACTED = "<REDACTED>"
|
|
39
|
+
|
|
40
|
+
# Field names that carry user / agent content (from
|
|
41
|
+
# scripts/chat_history.py::_extract_hook_text fallback list +
|
|
42
|
+
# nested Augment shape). Matched case-insensitively against the
|
|
43
|
+
# leaf key.
|
|
44
|
+
_USER_CONTENT_KEYS = {
|
|
45
|
+
"prompt", "user_prompt", "userprompt", "first_user_msg",
|
|
46
|
+
"firstusermsg", "usermessage", "user_message", "text",
|
|
47
|
+
"response", "message", "content",
|
|
48
|
+
# Augment Code with includeConversationData
|
|
49
|
+
"agenttextresponse", "agent_text_response",
|
|
50
|
+
"agentcoderesponse", "agent_code_response",
|
|
51
|
+
# Cursor / generic
|
|
52
|
+
"submitted_prompt", "submittedprompt",
|
|
53
|
+
# Free-form transcript bodies (path stays — content is in another file)
|
|
54
|
+
"transcript", "transcript_text",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Keys whose value is a structural / schema marker — keep as-is even
|
|
58
|
+
# when --strict would otherwise redact long values.
|
|
59
|
+
_ENVELOPE_KEYS_KEEP = {
|
|
60
|
+
"hook_event_name", "session_id", "transcript_path", "transcriptpath",
|
|
61
|
+
"platform", "event", "native_event", "captured_at", "cwd",
|
|
62
|
+
"workspace_roots", "model", "cursor_version", "user_email",
|
|
63
|
+
"conversation_id", "generation_id", "agent", "type",
|
|
64
|
+
"schema_version", "started_at", "completed_at", "_raw_text",
|
|
65
|
+
"path", "changetype", "change_type",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _redact_value(val: Any, *, key: str | None, strict: bool,
|
|
70
|
+
max_len: int) -> Any:
|
|
71
|
+
"""Recursively redact a value."""
|
|
72
|
+
norm_key = (key or "").lower().replace("-", "_")
|
|
73
|
+
if isinstance(val, dict):
|
|
74
|
+
return {k: _redact_value(v, key=k, strict=strict, max_len=max_len)
|
|
75
|
+
for k, v in val.items()}
|
|
76
|
+
if isinstance(val, list):
|
|
77
|
+
return [_redact_value(item, key=key, strict=strict, max_len=max_len)
|
|
78
|
+
for item in val]
|
|
79
|
+
if isinstance(val, str):
|
|
80
|
+
if norm_key in _ENVELOPE_KEYS_KEEP:
|
|
81
|
+
return val
|
|
82
|
+
if norm_key in _USER_CONTENT_KEYS:
|
|
83
|
+
return REDACTED
|
|
84
|
+
if strict and len(val) > max_len:
|
|
85
|
+
return REDACTED
|
|
86
|
+
return val
|
|
87
|
+
return val
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def redact(record: dict, *, strict: bool = False, max_len: int = 120) -> dict:
|
|
91
|
+
"""Redact a single capture record. Top-level envelope is preserved."""
|
|
92
|
+
out: dict = {}
|
|
93
|
+
for k, v in record.items():
|
|
94
|
+
if k == "raw_payload":
|
|
95
|
+
out[k] = _redact_value(v, key=None, strict=strict,
|
|
96
|
+
max_len=max_len)
|
|
97
|
+
else:
|
|
98
|
+
out[k] = _redact_value(v, key=k, strict=strict,
|
|
99
|
+
max_len=max_len)
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _process_file(path: Path, *, out: Path | None, strict: bool,
|
|
104
|
+
max_len: int) -> Path:
|
|
105
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
106
|
+
redacted = redact(record, strict=strict, max_len=max_len)
|
|
107
|
+
target = out or path.with_suffix(".redacted.json")
|
|
108
|
+
target.write_text(json.dumps(redacted, indent=2) + "\n",
|
|
109
|
+
encoding="utf-8")
|
|
110
|
+
return target
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
115
|
+
parser.add_argument("input", help="capture file or directory")
|
|
116
|
+
parser.add_argument("--out", default=None,
|
|
117
|
+
help="output path (single-file mode only)")
|
|
118
|
+
parser.add_argument("--strict", action="store_true",
|
|
119
|
+
help="redact any string longer than --max-len")
|
|
120
|
+
parser.add_argument("--max-len", type=int, default=120,
|
|
121
|
+
help="strict-mode length threshold (default 120)")
|
|
122
|
+
args = parser.parse_args(argv)
|
|
123
|
+
|
|
124
|
+
src = Path(args.input).expanduser()
|
|
125
|
+
if not src.exists():
|
|
126
|
+
sys.stderr.write(f"redact: input not found: {src}\n")
|
|
127
|
+
return 2
|
|
128
|
+
|
|
129
|
+
if src.is_dir():
|
|
130
|
+
if args.out:
|
|
131
|
+
sys.stderr.write("redact: --out is single-file only\n")
|
|
132
|
+
return 2
|
|
133
|
+
files = sorted(p for p in src.glob("*.json")
|
|
134
|
+
if not p.name.endswith(".redacted.json"))
|
|
135
|
+
for path in files:
|
|
136
|
+
target = _process_file(path, out=None, strict=args.strict,
|
|
137
|
+
max_len=args.max_len)
|
|
138
|
+
print(f"redacted: {target}")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
target = _process_file(src, out=Path(args.out) if args.out else None,
|
|
142
|
+
strict=args.strict, max_len=args.max_len)
|
|
143
|
+
print(f"redacted: {target}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|
|
@@ -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:
|
package/scripts/update_prices.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Refresh
|
|
2
|
+
"""Refresh `agents/.agent-prices.md` from the LiteLLM model-prices feed.
|
|
3
3
|
|
|
4
4
|
Source: https://raw.githubusercontent.com/BerriAI/litellm/main/
|
|
5
5
|
model_prices_and_context_window.json
|
|
@@ -9,7 +9,7 @@ Network failure or invalid response → fall back to
|
|
|
9
9
|
always written. Stdlib only; no extra dependency.
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
python3 scripts/update_prices.py # writes
|
|
12
|
+
python3 scripts/update_prices.py # writes agents/.agent-prices.md
|
|
13
13
|
python3 scripts/update_prices.py --check # exit 1 if file is stale
|
|
14
14
|
"""
|
|
15
15
|
|
|
@@ -81,7 +81,7 @@ def _to_rows_from_litellm(payload: dict[str, dict[str, object]]) -> list[tuple[s
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def refresh(path: Path = PRICES_FILE) -> str:
|
|
84
|
-
"""Write a fresh
|
|
84
|
+
"""Write a fresh `agents/.agent-prices.md`. Returns the source label used."""
|
|
85
85
|
payload = _fetch_litellm()
|
|
86
86
|
if payload is not None:
|
|
87
87
|
rows = _to_rows_from_litellm(payload)
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: chat-history:checkpoint
|
|
3
|
-
cluster: chat-history
|
|
4
|
-
sub: checkpoint
|
|
5
|
-
description: Append a phase-boundary entry to .agent-chat-history — CHECKPOINT fallback for platforms without a native hook (Augment IDE, Cursor pre-1.7, Cline non-Mac/Linux). ~1s.
|
|
6
|
-
disable-model-invocation: true
|
|
7
|
-
suggestion:
|
|
8
|
-
eligible: true
|
|
9
|
-
trigger_description: "User wants to flush a chat-history phase boundary on a CHECKPOINT-class platform (Augment IDE, Cursor < 1.7, Cline on Windows) — phrases like 'checkpoint chat history', 'log a phase boundary', 'manual chat-history append'."
|
|
10
|
-
trigger_context: "chat_history.path == checkpoint AND a phase-shaped boundary just completed (decision recorded, multi-tool sequence finished, task-list item closed)."
|
|
11
|
-
---
|
|
12
|
-
<!-- cloud_safe: noop -->
|
|
13
|
-
|
|
14
|
-
# /chat-history checkpoint
|
|
15
|
-
Force-append a `phase`-typed entry to `.agent-chat-history`. CHECKPOINT
|
|
16
|
-
fallback for platforms without native hooks — see
|
|
17
|
-
[`agents/contexts/chat-history-platform-hooks.md`](../../../agents/contexts/chat-history-platform-hooks.md)
|
|
18
|
-
for the per-platform classification.
|
|
19
|
-
|
|
20
|
-
Use this at decision points, end of phase, or after a meaningful tool
|
|
21
|
-
sequence on a CHECKPOINT/MANUAL platform. On HOOK platforms (Claude
|
|
22
|
-
Code, Augment CLI, Cursor 1.7+, Cline non-Windows, Windsurf, Gemini
|
|
23
|
-
CLI), the platform fires hooks automatically — manual use is allowed
|
|
24
|
-
but rarely needed.
|
|
25
|
-
|
|
26
|
-
## When to use
|
|
27
|
-
|
|
28
|
-
- IDE plugin without hooks (Augment Code IDE plugin as of 2026-04-30).
|
|
29
|
-
- Long-running tool sequence on any platform that did not flush.
|
|
30
|
-
- Explicit phase-boundary marker after a multi-tool refactor.
|
|
31
|
-
- Crash-recovery rehearsal — verifies the append path works before a
|
|
32
|
-
real outage.
|
|
33
|
-
|
|
34
|
-
## When NOT to use
|
|
35
|
-
|
|
36
|
-
- HOOK platform mid-session — the platform already records turn-level
|
|
37
|
-
cadence; an extra checkpoint just adds noise.
|
|
38
|
-
- After every line of agent output — that's `per_turn` cadence and is
|
|
39
|
-
configured in `.agent-settings.yml`, not via this command.
|
|
40
|
-
- To inspect the log → [`/chat-history`](chat-history.md).
|
|
41
|
-
- To wipe the log → [`/chat-history-clear`](chat-history-clear.md).
|
|
42
|
-
- To reload the log into context → [`/chat-history-resume`](chat-history-resume.md).
|
|
43
|
-
|
|
44
|
-
## Steps
|
|
45
|
-
|
|
46
|
-
### 1. Check if enabled
|
|
47
|
-
|
|
48
|
-
Read `chat_history.enabled` from `.agent-settings.yml`. If `false` or
|
|
49
|
-
the section is missing, say so and stop:
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
> 📒 chat-history is disabled (chat_history.enabled = false).
|
|
53
|
-
> Nothing to checkpoint. Enable in .agent-settings.yml first.
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 2. Determine the phase label
|
|
57
|
-
|
|
58
|
-
Pick a short label (2–6 words) that names what just happened:
|
|
59
|
-
|
|
60
|
-
- "phase-1-done", "refactor-extracted", "tests-green",
|
|
61
|
-
"review-comments-fixed", "merge-ready".
|
|
62
|
-
|
|
63
|
-
If the user invoked the command without context, ask once with
|
|
64
|
-
numbered options per [`user-interaction`](../rules/user-interaction.md):
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
> 1. Use a free-text label — type 2–6 words for the checkpoint
|
|
68
|
-
> 2. Use a generic "manual-checkpoint" label
|
|
69
|
-
> 3. Skip — close without writing
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 3. Append the checkpoint
|
|
73
|
-
|
|
74
|
-
Invoke the master CLI:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
./agent-config chat-history:checkpoint \
|
|
78
|
-
--first-user-msg "<the conversation's first user message>" \
|
|
79
|
-
--payload '{"phase": "<label>"}'
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The wrapper delegates to `scripts/chat_history.py hook-append --event phase`,
|
|
83
|
-
which performs cadence filtering and ownership checks. Cadence is read
|
|
84
|
-
from `chat_history.frequency` in `.agent-settings.yml` — `per_turn` /
|
|
85
|
-
`per_phase` / `per_tool`. `per_tool` cadence drops the `phase` event;
|
|
86
|
-
say so explicitly if that is the active mode.
|
|
87
|
-
|
|
88
|
-
### 4. Confirm
|
|
89
|
-
|
|
90
|
-
Render a one-line confirmation, mirroring the user's language:
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
> 📒 Checkpoint logged: <label> (entries: N → N+1)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
If the helper returned `skipped_cadence`, surface it:
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
> 📒 Checkpoint skipped — current cadence is per_tool, phase events are dropped.
|
|
100
|
-
> Switch chat_history.frequency to per_phase or per_turn to capture phase boundaries.
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Gotchas
|
|
104
|
-
|
|
105
|
-
- The command writes through the same ownership-state machine as
|
|
106
|
-
hooks — a `foreign` log triggers the
|
|
107
|
-
[`chat-history`](../rules/chat-history-ownership.md) Foreign-Prompt before any
|
|
108
|
-
append. This is intentional; the checkpoint must not silently
|
|
109
|
-
hijack another session's log.
|
|
110
|
-
- The `phase` payload key is required. Other keys are accepted but
|
|
111
|
-
ignored by the JSONL schema (forward-compat — they may be promoted
|
|
112
|
-
to first-class fields later).
|
|
113
|
-
- On HOOK platforms, hook entries and checkpoint entries coexist
|
|
114
|
-
cleanly. The schema does not deduplicate; if you checkpoint
|
|
115
|
-
immediately after a hook fires, expect two adjacent entries with
|
|
116
|
-
different `source` values.
|
|
117
|
-
|
|
118
|
-
## See also
|
|
119
|
-
|
|
120
|
-
- [`chat-history`](../rules/chat-history-ownership.md) — the rule defining the
|
|
121
|
-
conditional Iron Law (HOOK platforms vs CHECKPOINT/MANUAL platforms)
|
|
122
|
-
- [`/chat-history`](chat-history.md) — read-only status display
|
|
123
|
-
- [`/chat-history-resume`](chat-history-resume.md) — adopt + load
|
|
124
|
-
- [`/chat-history-clear`](chat-history-clear.md) — wipe
|
|
125
|
-
- [`agents/contexts/chat-history-platform-hooks.md`](../../../agents/contexts/chat-history-platform-hooks.md) — per-platform strategy table
|
|
126
|
-
- [`scripts/chat_history.py`](../../../scripts/chat_history.py) — `hook-append` API
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: chat-history:clear
|
|
3
|
-
cluster: chat-history
|
|
4
|
-
sub: clear
|
|
5
|
-
description: Manually delete the persistent chat-history log — asks for confirmation, optionally archives to a timestamped backup before wiping
|
|
6
|
-
disable-model-invocation: true
|
|
7
|
-
suggestion:
|
|
8
|
-
eligible: false
|
|
9
|
-
rationale: "Destructive log wipe — must be deliberate."
|
|
10
|
-
---
|
|
11
|
-
<!-- cloud_safe: noop -->
|
|
12
|
-
|
|
13
|
-
# /chat-history clear
|
|
14
|
-
Wipes `.agent-chat-history`. Use when the log is stale (wrong session),
|
|
15
|
-
bloated beyond usefulness, or contains information you do not want
|
|
16
|
-
persisted on disk.
|
|
17
|
-
|
|
18
|
-
This command is **destructive** — always asks for confirmation before
|
|
19
|
-
touching the file, unless the file does not exist in the first place.
|
|
20
|
-
|
|
21
|
-
## When NOT to use
|
|
22
|
-
|
|
23
|
-
- Inspect before deleting → [`/chat-history`](chat-history.md).
|
|
24
|
-
- Keep the entries but re-point the header → [`/chat-history-resume`](chat-history-resume.md).
|
|
25
|
-
- Disable logging entirely → set `chat_history.enabled: false` in
|
|
26
|
-
`.agent-settings.yml`; see
|
|
27
|
-
[`layered-settings`](../../docs/guidelines/agent-infra/layered-settings.md#section-aware-merge-rules).
|
|
28
|
-
Disabling does not delete the existing file; run this command
|
|
29
|
-
afterwards if you also want it gone.
|
|
30
|
-
|
|
31
|
-
## Steps
|
|
32
|
-
|
|
33
|
-
### 1. Check current state
|
|
34
|
-
|
|
35
|
-
Run `scripts/chat_history.py status`. If `exists: false`, tell the user
|
|
36
|
-
and stop:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
> 📒 No .agent-chat-history to clear.
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### 2. Show what is about to be deleted
|
|
43
|
-
|
|
44
|
-
Render a short preview so the user sees what they are wiping:
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
> 📒 About to clear .agent-chat-history
|
|
48
|
-
>
|
|
49
|
-
> Size: {size_kb} KB
|
|
50
|
-
> Entries: {entries}
|
|
51
|
-
> Session: {short_fp} (started {created_at_relative})
|
|
52
|
-
>
|
|
53
|
-
> 1. Archive — rename to .agent-chat-history.{YYYYMMDD-HHMMSS}.bak, then start fresh
|
|
54
|
-
> 2. Delete — permanent, no backup
|
|
55
|
-
> 3. Cancel — keep the file as-is
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 3. Act on the choice
|
|
59
|
-
|
|
60
|
-
- `1` (Archive) → `mv .agent-chat-history
|
|
61
|
-
.agent-chat-history.{timestamp}.bak`. The rule will create a fresh
|
|
62
|
-
file on the next append.
|
|
63
|
-
- `2` (Delete) → run `scripts/chat_history.py clear`. Permanent.
|
|
64
|
-
- `3` (Cancel) → stop. Make no changes.
|
|
65
|
-
|
|
66
|
-
Free-text replies ("abbrechen", "keep it", "nevermind") count as `3`.
|
|
67
|
-
An unrecognized reply also counts as `3` — never delete on ambiguous
|
|
68
|
-
input.
|
|
69
|
-
|
|
70
|
-
### 4. Confirm
|
|
71
|
-
|
|
72
|
-
After a successful archive or delete, print a one-line confirmation:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
> 📒 Archived to .agent-chat-history.{timestamp}.bak
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
or
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
> 📒 .agent-chat-history deleted.
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Do **not** re-enable logging or change `.agent-settings.yml` as a side
|
|
85
|
-
effect — this command is scoped to the file on disk only.
|
|
86
|
-
|
|
87
|
-
## Gotchas
|
|
88
|
-
|
|
89
|
-
- `.agent-chat-history.*.bak` files are also git-ignored by the
|
|
90
|
-
installer's `.gitignore` block. They accumulate if archived often —
|
|
91
|
-
users can delete them manually.
|
|
92
|
-
- If `chat_history.enabled: false`, the file will **not** be recreated
|
|
93
|
-
after clearing. That is usually fine, but mention it so the user
|
|
94
|
-
knows the log is now silent.
|
|
95
|
-
- Deletion cannot be undone. When in doubt, prefer option `1` (Archive).
|
|
96
|
-
|
|
97
|
-
## See also
|
|
98
|
-
|
|
99
|
-
- [`chat-history`](../rules/chat-history-ownership.md) — the rule that writes the file
|
|
100
|
-
- [`/chat-history`](chat-history.md) — status inspection
|
|
101
|
-
- [`/chat-history-resume`](chat-history-resume.md) — load + adopt instead of wipe
|
|
102
|
-
- [`agent-settings` template](../templates/agent-settings.md) — `chat_history.*` reference
|
|
103
|
-
- [`scripts/chat_history.py`](../../../scripts/chat_history.py) — helper API
|