Skill: jasondzs/aps-filesystem-agent (version 1.0.0)

--- name: aps-filesystem-agent description: > Use this skill whenever an APS (production scheduling) agent needs to interact with a local filesystem-based knowledge base. Triggers include: reading or searching APS rules, loading client memory or shop floor configurations, proposing new rules to the knowledge base, updating or deprecating existing knowledge, querying decision history, rebuilding the vector index, or any task involving the aps_knowledge_base/ directory structure. Also use when the agent needs to understand what knowledge is available before making scheduling decisions, or when it wants to persist something learned in a conversation. Always consult this skill before reading from or writing to any part of the APS knowledge base filesystem. --- # APS Filesystem Agent Skill This skill teaches an APS scheduling agent how to navigate, query, and maintain a local filesystem-based knowledge base. The filesystem is the single source of truth for all domain rules, client memory, and problem schemas. A vector index sits on top for semantic retrieval, and Git tracks every change for auditability. ## Knowledge base layout ``` aps_knowledge_base/ ├── .git/ ← version history, never touch manually ├── domain_rules/ ← APS rules extracted from conversations │ ├── _index.json ← master rule registry (always update this) │ ├── machine_rules/ │ ├── operator_rules/ │ └── material_rules/ ├── client_memory/ ← persistent understanding of this customer │ ├── _profile.json ← shop floor + planning process + preferences │ ├── shop_floor/ │ ├── planning_process/ │ └── decision_history/ ← one file per scheduling session ├── problem_schemas/ ← modeling templates by problem type ├── solver_configs/ ← solver parameters and routing thresholds ├── pending_review/ ← proposed knowledge awaiting human approval └── logs/ ├── decisions/ ← audit trail of scheduling decisions └── knowledge_changes/ ← audit trail of knowledge writes ``` Before doing anything, confirm the knowledge base root exists: ```bash ls aps_knowledge_base/ 2>/dev/null || echo "Knowledge base not initialized" ``` If it doesn't exist yet, initialize it (see "Initializing a new knowledge base" below). --- ## Reading knowledge ### Load client profile Always load the client profile first — it tells you the shop floor topology, planning process, and output preferences that frame every other decision. ```python import json, pathlib kb = pathlib.Path("aps_knowledge_base") profile = json.loads((kb / "client_memory/_profile.json").read_text()) shop = profile["shop_floor"] # type, stages, machines_per_stage, etc. prefs = profile["preferences"] # primary_objective, output_format, etc. ``` ### Semantic retrieval of rules (preferred method) Use semantic search when you know *what you need* but not *which file has it*. This requires the vector index to be built (see "Maintaining the vector index"). ```python import chromadb client = chromadb.PersistentClient(path="aps_knowledge_base/.chromadb") collection = client.get_collection("domain_rules") results = collection.query( query_texts=["operator HSE certification machine maintenance"], n_results=5, where={"status": "active"} # only retrieve active rules ) # results["ids"], results["documents"], results["metadatas"] for doc, meta in zip(results["documents"][0], results["metadatas"][0]): print(f"[{meta['rule_id']}] {meta['name']}: {doc}") ``` ### Direct rule lookup by ID When you already know the rule ID (e.g., from a decision log): ```python rule_path = kb / f"domain_rules/{category}/{rule_id}.json" rule = json.loads(rule_path.read_text()) ``` ### Load all active rules for a scheduling session Inject the Top-K most relevant rules into the scheduling context: ```python def get_relevant_rules(query: str, top_k: int = 5) -> list[dict]: collection = client.get_collection("domain_rules") results = collection.query( query_texts=[query], n_results=top_k, where={"status": "active"} ) rules = [] for rule_id, meta in zip(results["ids"][0], results["metadatas"][0]): path = kb / meta["file_path"] rules.append(json.loads(path.read_text())) return rules ``` ### Load a problem schema template ```python problem_type = "flow_shop" # or job_shop, rcpsp, re_entrant schema = json.loads((kb / f"problem_schemas/{problem_type}.json").read_text()) ``` ### Read session decision history ```python history_dir = kb / "client_memory/decision_history" sessions = sorted(history_dir.glob("session_*.json"), reverse=True) last_session = json.loads(sessions[0].read_text()) if sessions else {} ``` --- ## Proposing new knowledge (write path) **The agent NEVER writes directly to the main knowledge directories.** All new knowledge goes to `pending_review/` first, then a human confirms. ### Propose a new APS rule Call this whenever you extract a new constraint or rule from a conversation: ```python import json, pathlib, datetime def propose_rule(rule_content: dict, source_quote: str, session_id: str): kb = pathlib.Path("aps_knowledge_base") pending = kb / "pending_review" pending.mkdir(exist_ok=True) ts = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S") proposal = { **rule_content, "status": "proposed", "metadata": { **rule_content.get("metadata", {}), "created_at": datetime.datetime.utcnow().isoformat() + "Z", "created_by": "ai_agent", "confirmed_by": None, "source_session": session_id, "source_quote": source_quote, "use_count": 0, "confidence": 0.9 } } out_path = pending / f"proposed_{rule_content['id']}_{ts}.json" out_path.write_text(json.dumps(proposal, ensure_ascii=False, indent=2)) # Return the summary to show the user for confirmation return { "proposal_file": str(out_path), "rule_id": rule_content["id"], "name": rule_content["name"], "description": rule_content["description"] } ``` After calling this, **always present the proposal to the user** with a confirmation prompt before moving on. Format it like this: ``` 建议将以下内容加入知识库： 规则ID: {rule_id} 名称: {name} 描述: {description} 来源: "{source_quote}" [确认入库] [修改后入库] [忽略本次] ``` Wait for explicit confirmation before proceeding to `confirm_proposal()`. ### Propose an update to client memory ```python def propose_memory_update(memory_type: str, updates: dict, reason: str): """ memory_type: 'shop_floor' | 'planning_process' | 'preferences' """ pending = kb / "pending_review" ts = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S") proposal = { "type": "client_memory_update", "memory_type": memory_type, "updates": updates, "reason": reason, "proposed_at": datetime.datetime.utcnow().isoformat() + "Z" } out_path = pending / f"proposed_memory_{memory_type}_{ts}.json" out_path.write_text(json.dumps(proposal, ensure_ascii=False, indent=2)) return str(out_path) ``` --- ## Confirming proposals (after human approval) Only call these functions **after** the user has explicitly confirmed in chat. ```python def confirm_proposal(proposal_file: str, confirmed_by: str): """Move a proposal from pending_review into the live knowledge base.""" kb = pathlib.Path("aps_knowledge_base") proposal_path = pathlib.Path(proposal_file) proposal = json.loads(proposal_path.read_text()) if proposal.get("type") == "client_memory_update": _apply_memory_update(proposal, confirmed_by) else: _apply_rule(proposal, confirmed_by) # Remove from pending proposal_path.unlink() # Update vector index and commit _update_vector_index(proposal) _git_commit(proposal, confirmed_by) def _apply_rule(proposal: dict, confirmed_by: str): rule_type = proposal.get("type", "general") category_map = { "machine_constraint": "machine_rules", "operator_constraint": "operator_rules", "material_constraint": "material_rules", } subdir = category_map.get(rule_type, "machine_rules") dest = kb / f"domain_rules/{subdir}/{proposal['id']}.json" dest.parent.mkdir(parents=True, exist_ok=True) proposal["status"] = "active" proposal["metadata"]["confirmed_by"] = confirmed_by proposal["metadata"]["confirmed_at"] = ( datetime.datetime.utcnow().isoformat() + "Z" ) dest.write_text(json.dumps(proposal, ensure_ascii=False, indent=2)) # Refresh the index file _refresh_rule_index() def _apply_memory_update(proposal: dict, confirmed_by: str): profile_path = kb / "client_memory/_profile.json" profile = json.loads(profile_path.read_text()) memory_type = proposal["memory_type"] if memory_type not in profile: profile[memory_type] = {} profile[memory_type].update(proposal["updates"]) profile["last_updated"] = datetime.datetime.utcnow().isoformat() + "Z" profile_path.write_text(json.dumps(profile, ensure_ascii=False, indent=2)) ``` --- ## Maintaining the vector index The vector index must stay in sync with the filesystem. Rebuild it whenever rules are added, updated, or deprecated. ### Incremental update (after a single rule change) ```python def _update_vector_index(rule: dict): import chromadb client = chromadb.PersistentClient(path="aps_knowledge_base/.chromadb") try: collection = client.get_or_create_collection("domain_rules") except Exception: collection = client.create_collection("domain_rules") text = f"{rule['name']} {rule['description']} {' '.join(rule.get('metadata', {}).get('tags', []))}" meta = { "rule_id": rule["id"], "name": rule["name"], "status": rule.get("status", "active"), "constraint_type": rule.get("constraint_type", "soft"), "file_path": f"domain_rules/{_infer_subdir(rule)}/{rule['id']}.json" } collection.upsert(ids=[rule["id"]], documents=[text], metadatas=[meta]) ``` ### Full rebuild (use after bulk changes or first setup) ```bash python aps_knowledge_base/scripts/rebuild_index.py ``` See `references/scripts.md` for the full rebuild script content. --- ## Git version management Every confirmed knowledge change gets a Git commit automatically. ```python import subprocess def _git_commit(item: dict, confirmed_by: str): kb_path = "aps_knowledge_base" item_id = item.get("id", item.get("memory_type", "unknown")) item_type = item.get("type", "update") action = "add" if item.get("status") == "active" else "update" msg = f"{action}: {item_id} {item_type} ({confirmed_by})" subprocess.run(["git", "-C", kb_path, "add", "-A"], check=True) subprocess.run(["git", "-C", kb_path, "commit", "-m", msg], check=True) ``` Commit message conventions: ``` add: rule_003 operator_constraint (big_boss) update: client_memory shop_floor topology (plant_manager) deprecate: rule_002 machine_a3 calibration - operator left (admin) restore: rule_002 machine_a3 calibration (admin) ``` To view history for a specific rule: ```bash git -C aps_knowledge_base log --oneline -- domain_rules/operator_rules/rule_003.json ``` --- ## Updating knowledge status ### Deprecate a rule (soft disable — keeps the record) ```python def deprecate_rule(rule_id: str, reason: str, deprecated_by: str): # find the file for f in (kb / "domain_rules").rglob(f"{rule_id}.json"): rule = json.loads(f.read_text()) rule["status"] = "deprecated" rule["metadata"]["deprecated_at"] = datetime.datetime.utcnow().isoformat() + "Z" rule["metadata"]["deprecation_reason"] = reason f.write_text(json.dumps(rule, ensure_ascii=False, indent=2)) # remove from vector index so it won't be retrieved client = chromadb.PersistentClient(path="aps_knowledge_base/.chromadb") col = client.get_collection("domain_rules") col.update(ids=[rule_id], metadatas=[{**col.get(ids=[rule_id])["metadatas"][0], "status": "deprecated"}]) _git_commit({"id": rule_id, "type": "deprecation"}, deprecated_by) _refresh_rule_index() return True return False ``` ### Record a scheduling decision (audit log) After every scheduling session, persist the decision for future reference: ```python def log_decision(session_id: str, decision: dict, rules_used: list[str]): log_entry = { "session_id": session_id, "timestamp": datetime.datetime.utcnow().isoformat() + "Z", "decision_summary": decision, "triggered_by_rules": rules_used, "human_confirmed": True } log_path = kb / f"client_memory/decision_history/{session_id}.json" log_path.write_text(json.dumps(log_entry, ensure_ascii=False, indent=2)) # Also bump use_count on every rule that was triggered for rule_id in rules_used: _increment_use_count(rule_id) ``` --- ## Knowledge health checks Run these checks periodically or before a major scheduling session. ```python def check_knowledge_health() -> dict: issues = [] profile = json.loads((kb / "client_memory/_profile.json").read_text()) # Check for rules referencing people/machines that no longer exist known_operators = profile.get("operators", {}).get("active", []) for f in (kb / "domain_rules").rglob("*.json"): rule = json.loads(f.read_text()) if rule.get("status") != "active": continue for op in rule.get("scope", {}).get("operators", []): if op not in known_operators: issues.append({ "rule_id": rule["id"], "issue": f"references operator '{op}' not in active roster" }) # Flag rules unused for 180+ days cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=180) for f in (kb / "domain_rules").rglob("*.json"): rule = json.loads(f.read_text()) if rule.get("status") != "active": continue last_used = rule.get("metadata", {}).get("last_used_at") if last_used and datetime.datetime.fromisoformat(last_used[:-1]) < cutoff: issues.append({ "rule_id": rule["id"], "issue": "not used in 180+ days — consider deprecating" }) return {"issues": issues, "checked_at": datetime.datetime.utcnow().isoformat()} ``` --- ## Initializing a new knowledge base If `aps_knowledge_base/` does not exist, bootstrap it: ```bash mkdir -p aps_knowledge_base/{domain_rules/{machine_rules,operator_rules,material_rules},client_memory/{shop_floor,planning_process,decision_history},problem_schemas,solver_configs,pending_review,logs/{decisions,knowledge_changes},.chromadb} cd aps_knowledge_base && git init && git commit --allow-empty -m "init: knowledge base" ``` Then create `client_memory/_profile.json` with the shell structure and fill it in from the conversation (use `propose_memory_update` + confirmation flow). See `references/schemas.md` for the full JSON schemas for every file type. --- ## Decision checklist before every scheduling session 1. Load `client_memory/_profile.json` — confirm shop floor topology is current 2. Retrieve Top-5 relevant rules via semantic search using the order batch description 3. Check `pending_review/` — if any proposals await, surface them to the user 4. Load the matching `problem_schemas/<type>.json` template 5. After solving, call `log_decision()` with the rules that were triggered 6. If new constraints emerged in conversation, call `propose_rule()` and await confirmation --- ## Reference files For detailed schemas and the rebuild script, read these when needed: - `references/schemas.md` — full JSON schemas for rules, client memory, proposals - `references/scripts.md` — `rebuild_index.py` full source code

path: references/schemas.md
content: # APS Knowledge Base — JSON Schemas

## Table of Contents
1. [APS Rule](#1-aps-rule)
2. [Client Profile (_profile.json)](#2-client-profile)
3. [Problem Schema](#3-problem-schema)
4. [Proposal (pending_review)](#4-proposal)
5. [Decision Log Entry](#5-decision-log-entry)
6. [Rule Index (_index.json)](#6-rule-index)

---

## 1. APS Rule

File location: `domain_rules/<category>/rule_<NNN>.json`

```json
{
  "id": "rule_003",
  "type": "operator_constraint",
  "name": "高风险机器持证操作规则",
  "description": "未维护超过 Z 小时的机器，必须由持 HSE 证书的操作员监督...",
  "parameters": {
    "Z": { "label": "未维护时长阈值（小时）", "type": "number", "default": 200 },
    "A": { "label": "连续工作上限（小时）", "type": "number", "default": 0.5 },
    "B": { "label": "强制休息时长（小时）", "type": "number", "default": 0.083 }
  },
  "trigger_condition": "machine.hours_since_maintenance > Z",
  "constraint_type": "soft",
  "penalty_weight": 0.8,
  "scope": {
    "applies_to": ["stage_2", "machine_2"],
    "operators": ["Bob"],
    "industry": "manufacturing",
    "process_type": "any"
  },
  "status": "active",
  "metadata": {
    "created_at": "2026-03-18T14:32:00Z",
    "created_by": "ai_agent",
    "confirmed_by": "big_boss",
    "confirmed_at": "2026-03-18T14:35:00Z",
    "deprecated_at": null,
    "deprecation_reason": null,
    "source_session": "session_20260318",
    "source_quote": "machine 2 of stage 2 has not been serviced for a long time",
    "last_used_at": "2026-03-20T09:00:00Z",
    "use_count": 3,
    "confidence": 0.95,
    "last_verified_at": "2026-03-20T09:00:00Z",
    "expires_at": null,
    "tags": ["HSE", "maintenance", "operator_qualification"]
  }
}
```

**`type` enum**: `machine_constraint` | `operator_constraint` | `material_constraint` | `setup_time` | `due_date`

**`constraint_type` enum**: `hard` | `soft`

**`status` enum**: `proposed` | `active` | `deprecated` | `archived`

---

## 2. Client Profile

File location: `client_memory/_profile.json`

```json
{
  "profile_version": "1.2",
  "customer_name": "示例制造有限公司",
  "last_updated": "2026-03-20T10:00:00Z",
  "shop_floor": {
    "type": "flow_shop",
    "stages": 2,
    "machines_per_stage": 2,
    "topology": "parallel",
    "release_time": 0,
    "transfer_time": 0,
    "confirmed": true,
    "confirmed_by": "plant_manager",
    "confirmed_at": "2026-03-18T10:15:00Z"
  },
  "operators": {
    "active": ["Bob", "Alice", "Chen"],
    "certified_hse": ["Bob"]
  },
  "planning_process": {
    "order_source": "sales_manager_wechat",
    "has_erp": false,
    "typical_batch_size": "4-8",
    "replanning_triggers": ["rush_order", "machine_breakdown", "inventory_shortage"],
    "decision_makers": {
      "schedule_owner": "plant_manager",
      "inventory_check": "inventory_manager",
      "delivery_approval": "sales_manager"
    }
  },
  "preferences": {
    "primary_objective": "minimize_makespan",
    "secondary_objective": "maximize_on_time_delivery",
    "output_format": ["gantt_chart", "wechat_message"],
    "language": "zh-CN",
    "time_unit": "minutes"
  }
}
```

---

## 3. Problem Schema

File location: `problem_schemas/flow_shop.json`

```json
{
  "schema_version": "1.0",
  "problem_type": "flow_shop",
  "display_name": "两阶段流水车间",
  "required_fields": ["jobs", "stages"],
  "optional_fields": ["release_times", "due_dates", "soft_constraints"],
  "example": {
    "problem_type": "flow_shop",
    "objective": "minimize_makespan",
    "jobs": [
      { "id": "001", "processing_times": [10, 20] },
      { "id": "002", "processing_times": [25, 20] }
    ],
    "stages": [
      { "id": 1, "machines": 2, "parallel": true },
      { "id": 2, "machines": 2, "parallel": true }
    ],
    "hard_constraints": [
      { "type": "machine_non_preemptive" },
      { "type": "precedence", "rule": "stage_1_before_stage_2" }
    ],
    "soft_constraints": []
  }
}
```

---

## 4. Proposal (pending_review)

File location: `pending_review/proposed_<id>_<timestamp>.json`

For a new rule proposal, the schema is the full rule schema with `status: "proposed"` and `confirmed_by: null`.

For a memory update proposal:

```json
{
  "type": "client_memory_update",
  "memory_type": "shop_floor",
  "updates": {
    "stages": 3,
    "confirmed": false
  },
  "reason": "Plant manager mentioned a new pre-processing stage added this month",
  "proposed_at": "2026-03-20T11:00:00Z",
  "source_session": "session_20260320",
  "source_quote": "we added a pre-treatment stage last month"
}
```

---

## 5. Decision Log Entry

File location: `client_memory/decision_history/session_<YYYYMMDD>.json`

```json
{
  "session_id": "session_20260320",
  "timestamp": "2026-03-20T09:15:00Z",
  "problem_type": "flow_shop",
  "solver_used": "cp_sat",
  "makespan_minutes": 55,
  "jobs_scheduled": ["001", "002", "003", "004"],
  "decision_summary": {
    "stage1_machine1": ["001", "003"],
    "stage1_machine2": ["002", "004"],
    "stage2_machine1": ["001", "003"],
    "stage2_machine2": ["002", "004"]
  },
  "triggered_by_rules": ["rule_003"],
  "rule_params_used": {
    "rule_003": { "Z": 200, "A": 0.5, "B": 0.083 }
  },
  "soft_constraints_violated": [],
  "human_confirmed": true,
  "confirmed_by": "plant_manager"
}
```

---

## 6. Rule Index

File location: `domain_rules/_index.json`

A flat list of all rules with lightweight metadata for fast scanning without
loading every individual file:

```json
{
  "last_rebuilt": "2026-03-20T10:00:00Z",
  "total_active": 3,
  "rules": [
    {
      "id": "rule_001",
      "name": "机器A3换模时间规则",
      "type": "setup_time",
      "status": "active",
      "file_path": "domain_rules/machine_rules/rule_001.json",
      "tags": ["setup", "mold_change"],
      "use_count": 12
    },
    {
      "id": "rule_003",
      "name": "高风险机器持证操作规则",
      "type": "operator_constraint",
      "status": "active",
      "file_path": "domain_rules/operator_rules/rule_003.json",
      "tags": ["HSE", "maintenance"],
      "use_count": 3
    }
  ]
}
```


path: references/scripts.md
content: # APS Knowledge Base — Utility Scripts

## rebuild_index.py

Full source for the vector index rebuild script.
Place at `aps_knowledge_base/scripts/rebuild_index.py`.

```python
#!/usr/bin/env python3
"""
Rebuilds the ChromaDB vector index from scratch by scanning all JSON files
in the domain_rules/ directory. Run this after bulk imports or to fix drift.

Usage:
    python aps_knowledge_base/scripts/rebuild_index.py
    python aps_knowledge_base/scripts/rebuild_index.py --kb-path /path/to/kb
"""

import argparse
import json
import pathlib
import sys

def rebuild(kb_path: str = "aps_knowledge_base"):
    try:
        import chromadb
    except ImportError:
        print("chromadb not installed. Run: pip install chromadb")
        sys.exit(1)

    kb = pathlib.Path(kb_path)
    if not kb.exists():
        print(f"Knowledge base not found at {kb_path}")
        sys.exit(1)

    client = chromadb.PersistentClient(path=str(kb / ".chromadb"))

    # Delete and recreate for a clean rebuild
    try:
        client.delete_collection("domain_rules")
    except Exception:
        pass
    collection = client.create_collection(
        "domain_rules",
        metadata={"hnsw:space": "cosine"}
    )

    ids, documents, metadatas = [], [], []

    for rule_file in sorted((kb / "domain_rules").rglob("*.json")):
        if rule_file.name.startswith("_"):
            continue
        try:
            rule = json.loads(rule_file.read_text(encoding="utf-8"))
        except json.JSONDecodeError as e:
            print(f"  SKIP (JSON error): {rule_file} — {e}")
            continue

        rule_id = rule.get("id")
        if not rule_id:
            print(f"  SKIP (no id): {rule_file}")
            continue

        tags = rule.get("metadata", {}).get("tags", [])
        text = " ".join(filter(None, [
            rule.get("name", ""),
            rule.get("description", ""),
            " ".join(tags)
        ]))

        meta = {
            "rule_id": rule_id,
            "name": rule.get("name", ""),
            "status": rule.get("status", "active"),
            "constraint_type": rule.get("constraint_type", "soft"),
            "rule_type": rule.get("type", ""),
            "file_path": str(rule_file.relative_to(kb)),
            "use_count": int(rule.get("metadata", {}).get("use_count", 0)),
            "confidence": float(rule.get("metadata", {}).get("confidence", 0.9))
        }

        ids.append(rule_id)
        documents.append(text)
        metadatas.append(meta)

    if ids:
        collection.upsert(ids=ids, documents=documents, metadatas=metadatas)
        print(f"Indexed {len(ids)} rules into domain_rules collection.")
    else:
        print("No rules found to index.")

    # Rebuild the _index.json as well
    _rebuild_index_json(kb)
    print("Done.")


def _rebuild_index_json(kb: pathlib.Path):
    import datetime
    entries = []
    for rule_file in sorted((kb / "domain_rules").rglob("*.json")):
        if rule_file.name.startswith("_"):
            continue
        try:
            rule = json.loads(rule_file.read_text(encoding="utf-8"))
            entries.append({
                "id": rule.get("id"),
                "name": rule.get("name", ""),
                "type": rule.get("type", ""),
                "status": rule.get("status", "active"),
                "file_path": str(rule_file.relative_to(kb)),
                "tags": rule.get("metadata", {}).get("tags", []),
                "use_count": rule.get("metadata", {}).get("use_count", 0)
            })
        except Exception:
            pass

    index = {
        "last_rebuilt": datetime.datetime.utcnow().isoformat() + "Z",
        "total_active": sum(1 for e in entries if e["status"] == "active"),
        "rules": entries
    }
    (kb / "domain_rules/_index.json").write_text(
        json.dumps(index, ensure_ascii=False, indent=2)
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--kb-path", default="aps_knowledge_base")
    args = parser.parse_args()
    rebuild(args.kb_path)
```

---

## refresh_rule_index.py (inline helper)

Used inside `confirm_proposal()` and `deprecate_rule()` to keep `_index.json`
current without a full rebuild. Paste this helper into your agent code:

```python
def _refresh_rule_index():
    import datetime
    entries = []
    for rule_file in sorted((kb / "domain_rules").rglob("*.json")):
        if rule_file.name.startswith("_"):
            continue
        try:
            rule = json.loads(rule_file.read_text(encoding="utf-8"))
            entries.append({
                "id": rule.get("id"),
                "name": rule.get("name", ""),
                "type": rule.get("type", ""),
                "status": rule.get("status", "active"),
                "file_path": str(rule_file.relative_to(kb)),
                "tags": rule.get("metadata", {}).get("tags", []),
                "use_count": rule.get("metadata", {}).get("use_count", 0)
            })
        except Exception:
            pass

    index = {
        "last_rebuilt": datetime.datetime.utcnow().isoformat() + "Z",
        "total_active": sum(1 for e in entries if e["status"] == "active"),
        "rules": entries
    }
    (kb / "domain_rules/_index.json").write_text(
        json.dumps(index, ensure_ascii=False, indent=2)
    )
```

---

## increment_use_count.py (inline helper)

Called after each scheduling session to track rule usage frequency:

```python
def _increment_use_count(rule_id: str):
    import datetime
    for rule_file in (kb / "domain_rules").rglob(f"{rule_id}.json"):
        rule = json.loads(rule_file.read_text(encoding="utf-8"))
        rule["metadata"]["use_count"] = rule["metadata"].get("use_count", 0) + 1
        rule["metadata"]["last_used_at"] = datetime.datetime.utcnow().isoformat() + "Z"
        rule_file.write_text(json.dumps(rule, ensure_ascii=False, indent=2))

        # also update chromadb metadata
        try:
            import chromadb
            client = chromadb.PersistentClient(path=str(kb / ".chromadb"))
            col = client.get_collection("domain_rules")
            existing = col.get(ids=[rule_id])["metadatas"]
            if existing:
                updated_meta = {**existing[0], "use_count": rule["metadata"]["use_count"]}
                col.update(ids=[rule_id], metadatas=[updated_meta])
        except Exception:
            pass
        break
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)