delimit-cli 4.7.1 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [4.7.2] - 2026-06-04
5
+
6
+ ### Fixed
7
+
8
+ - **Tool-usage telemetry now persists.** `record_call` previously only kept an
9
+ in-memory per-session counter that vanished on restart, so tool utilization
10
+ was unobservable. It now appends every call to `~/.delimit/tool_usage.jsonl`
11
+ (crash-safe, append-only), making utilization and dormancy measurable.
12
+
13
+ ### Added
14
+
15
+ - **`delimit_toolcard_cache action="usage"`** — durable tool-utilization +
16
+ dormancy report (per-tool call counts, `last_seen`, and registered tools never
17
+ called).
18
+ - Additive next-step suggestions wiring under-surfaced tools into the governance
19
+ loop: `lint → impact`, `agent_dispatch → agent_link`, `deploy_verify →
20
+ seal_verify`, `evidence_collect → seal_verify`, `os_plan → os_gates`.
21
+
22
+ Purely additive — no tool signature or behavior changed. Published via OIDC
23
+ trusted publishing with provenance.
24
+
25
+
4
26
  ## [4.7.1] - 2026-06-03
5
27
 
6
28
  Release-infrastructure update. No functional changes to the package versus 4.7.0.
@@ -742,6 +742,7 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
742
742
  "lint": [
743
743
  {"tool": "delimit_explain", "reason": "Get migration guide for breaking changes", "suggested_args": {"template": "migration"}, "is_premium": False},
744
744
  {"tool": "delimit_semver", "reason": "Determine the version bump for these changes", "suggested_args": {}, "is_premium": False},
745
+ {"tool": "delimit_impact", "reason": "Enumerate downstream callers affected by a breaking change before it ships", "suggested_args": {}, "is_premium": True},
745
746
  ],
746
747
  "diff": [
747
748
  {"tool": "delimit_semver", "reason": "Classify the semver bump for these changes", "suggested_args": {}, "is_premium": False},
@@ -766,7 +767,9 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
766
767
  {"tool": "delimit_diagnose", "reason": "Check environment and tool status", "suggested_args": {}, "is_premium": False},
767
768
  ],
768
769
  # --- Tier 2 Platform (Pro) ---
769
- "os_plan": [],
770
+ "os_plan": [
771
+ {"tool": "delimit_os_gates", "reason": "Check whether the new OS plan passes its governance gates", "suggested_args": {}, "is_premium": True},
772
+ ],
770
773
  "os_status": [],
771
774
  "os_gates": [],
772
775
  "gov_health": [
@@ -799,6 +802,7 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
799
802
  # --- Agent Orchestration (Pro) ---
800
803
  "agent_dispatch": [
801
804
  {"tool": "delimit_agent_status", "reason": "Check the status of your dispatched task", "suggested_args": {}, "is_premium": True},
805
+ {"tool": "delimit_agent_link", "reason": "Link this dispatched task to its ledger item for the replay/audit trail (operating-model mandate)", "suggested_args": {}, "is_premium": True},
802
806
  ],
803
807
  "agent_status": [
804
808
  {"tool": "delimit_agent_complete", "reason": "Mark a task as complete when done", "suggested_args": {}, "is_premium": True},
@@ -871,6 +875,10 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
871
875
  "deploy_publish": [
872
876
  {"tool": "delimit_deploy_verify", "reason": "Verify deployment health after publish", "suggested_args": {}, "is_premium": True},
873
877
  ],
878
+ "deploy_verify": [
879
+ {"tool": "delimit_evidence_collect", "reason": "Collect a deploy evidence bundle", "suggested_args": {}, "is_premium": True},
880
+ {"tool": "delimit_seal_verify", "reason": "Verify the signed, replayable attestation produced by the deploy", "suggested_args": {}, "is_premium": False},
881
+ ],
874
882
  "deploy_rollback": [],
875
883
  "deploy_status": [],
876
884
  "generate_template": [],
@@ -880,9 +888,13 @@ NEXT_STEPS_REGISTRY: Dict[str, List[Dict[str, Any]]] = {
880
888
  ],
881
889
  "evidence_collect": [
882
890
  {"tool": "delimit_evidence_verify", "reason": "Verify evidence bundle integrity", "suggested_args": {}, "is_premium": True},
891
+ {"tool": "delimit_seal_verify", "reason": "Verify the Seal attestation receipt (Ed25519 + content-pin) for this artifact", "suggested_args": {}, "is_premium": False},
883
892
  ],
884
893
  "evidence_verify": [],
885
- "seal_verify": [],
894
+ "seal_verify": [
895
+ {"tool": "delimit_evidence_collect", "reason": "Collect an evidence bundle for the verified attestation", "suggested_args": {}, "is_premium": True},
896
+ {"tool": "delimit_notify", "reason": "Notify stakeholders that the merge attestation verified", "suggested_args": {}, "is_premium": True},
897
+ ],
886
898
  "security_audit": [
887
899
  {"tool": "delimit_security_scan", "reason": "Run deeper security scan on flagged areas", "suggested_args": {}, "is_premium": True},
888
900
  {"tool": "delimit_evidence_collect", "reason": "Collect evidence of security findings", "suggested_args": {}, "is_premium": True},
@@ -13235,7 +13247,7 @@ def delimit_loop_config(session_id: Annotated[str, Field(description="Session to
13235
13247
 
13236
13248
  @mcp.tool()
13237
13249
  def delimit_toolcard_cache(
13238
- action: Annotated[str, Field(description="One of \"status\" (default), \"register\", \"delta\", \"clear\", \"estimate\", \"flush\".")] = "status",
13250
+ action: Annotated[str, Field(description="One of \"status\" (default), \"register\", \"delta\", \"clear\", \"estimate\", \"flush\", \"usage\" (durable tool utilization + dormancy report).")] = "status",
13239
13251
  tool_schemas: Annotated[Optional[str], Field(description="JSON array of tool schema objects (for register/ estimate).")] = None,
13240
13252
  tool_names: Annotated[Optional[str], Field(description="Comma-separated tool names (for delta).")] = None,
13241
13253
  ) -> Dict[str, Any]:
@@ -13255,13 +13267,16 @@ def delimit_toolcard_cache(
13255
13267
 
13256
13268
  Args:
13257
13269
  action: One of "status" (default), "register", "delta",
13258
- "clear", "estimate", "flush".
13270
+ "clear", "estimate", "flush", "usage". "usage" returns the
13271
+ durable tool-utilization + dormancy report (per-tool call
13272
+ counts, last_seen, and registry tools never called).
13259
13273
  tool_schemas: JSON array of tool schema objects (for register/
13260
13274
  estimate).
13261
13275
  tool_names: Comma-separated tool names (for delta).
13262
13276
 
13263
13277
  Returns:
13264
- Dict with the action result (stats, delta names, estimate, etc).
13278
+ Dict with the action result (stats, delta names, estimate,
13279
+ usage/dormancy summary, etc).
13265
13280
  """
13266
13281
  from ai.license import require_premium
13267
13282
  gate = require_premium("toolcard_cache")
@@ -13306,8 +13321,26 @@ def delimit_toolcard_cache(
13306
13321
  r = cache.estimate_savings(schemas)
13307
13322
  elif action == "flush":
13308
13323
  r = cache.flush_session()
13324
+ elif action == "usage":
13325
+ # Durable tool utilization / dormancy report. record_call now persists
13326
+ # every call to ~/.delimit/tool_usage.jsonl; usage_summary aggregates
13327
+ # counts + last_seen and, given the registered-tool list, flags tools
13328
+ # never called as `dormant`. Mechanizes the dormant-tool audit.
13329
+ registry = None
13330
+ try:
13331
+ tm = getattr(mcp, "_tool_manager", None)
13332
+ if tm is not None:
13333
+ if hasattr(tm, "list_tools"):
13334
+ registry = [getattr(t, "name", None) or t.get("name") for t in tm.list_tools()]
13335
+ elif hasattr(tm, "_tools"):
13336
+ registry = list(tm._tools.keys())
13337
+ if registry:
13338
+ registry = sorted(n for n in registry if n)
13339
+ except Exception:
13340
+ registry = None
13341
+ r = cache.usage_summary(registry=registry)
13309
13342
  else:
13310
- r = {"error": "unknown_action", "message": f"Unknown action: {action}. Use: status, register, delta, clear, estimate, flush"}
13343
+ r = {"error": "unknown_action", "message": f"Unknown action: {action}. Use: status, register, delta, clear, estimate, flush, usage"}
13311
13344
 
13312
13345
  return _with_next_steps("toolcard_cache", r)
13313
13346
 
@@ -209,8 +209,71 @@ class ToolcardCache:
209
209
  }
210
210
 
211
211
  def record_call(self, tool_name: str) -> None:
212
- """Record that a tool was called in the current session."""
212
+ """Record that a tool was called: in-memory (session) AND durably.
213
+
214
+ The prior implementation only kept an in-memory counter that vanished on
215
+ process restart, so tool utilization was never observable across sessions
216
+ (usage.json stayed empty and dormancy was unmeasurable). We now also
217
+ append to a crash-safe, append-only JSONL event log so utilization is
218
+ measurable over time. Append is O(1) with no read-modify-write race.
219
+ Analytics must never break a tool call, so all failures are swallowed.
220
+ """
213
221
  self._session_calls[tool_name] = self._session_calls.get(tool_name, 0) + 1
222
+ try:
223
+ usage_log = self._cache_file.parent / "tool_usage.jsonl"
224
+ usage_log.parent.mkdir(parents=True, exist_ok=True)
225
+ with open(usage_log, "a") as f:
226
+ f.write(json.dumps({
227
+ "ts": datetime.now(timezone.utc).isoformat(),
228
+ "tool": tool_name,
229
+ }) + "\n")
230
+ except Exception:
231
+ pass
232
+
233
+ def usage_summary(self, registry: Optional[List[str]] = None) -> Dict[str, Any]:
234
+ """Aggregate the durable usage log into per-tool counts + dormancy.
235
+
236
+ Reads tool_usage.jsonl (written by record_call). When `registry` (the
237
+ full list of registered tool names) is provided, tools that appear in
238
+ the registry but never in the log are reported as `dormant`.
239
+ """
240
+ usage_log = self._cache_file.parent / "tool_usage.jsonl"
241
+ counts: Dict[str, int] = {}
242
+ last_seen: Dict[str, str] = {}
243
+ total = 0
244
+ try:
245
+ with open(usage_log, "r") as f:
246
+ for line in f:
247
+ line = line.strip()
248
+ if not line:
249
+ continue
250
+ try:
251
+ rec = json.loads(line)
252
+ except Exception:
253
+ continue
254
+ name = rec.get("tool")
255
+ if not name:
256
+ continue
257
+ counts[name] = counts.get(name, 0) + 1
258
+ ts = rec.get("ts")
259
+ if ts and (name not in last_seen or ts > last_seen[name]):
260
+ last_seen[name] = ts
261
+ total += 1
262
+ except FileNotFoundError:
263
+ pass
264
+ result: Dict[str, Any] = {
265
+ "total_calls": total,
266
+ "distinct_tools_used": len(counts),
267
+ "counts": dict(sorted(counts.items(), key=lambda x: x[1], reverse=True)),
268
+ "last_seen": last_seen,
269
+ "usage_log": str(usage_log),
270
+ }
271
+ if registry is not None:
272
+ used = set(counts)
273
+ result["registry_size"] = len(registry)
274
+ result["dormant"] = sorted(t for t in registry if t not in used)
275
+ result["dormant_count"] = len(result["dormant"])
276
+ return result
214
277
 
215
278
  def get_stats(self) -> Dict[str, Any]:
216
279
  """Return cache stats: total tools, cached, cache hit rate, token savings."""
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.7.1",
4
+ "version": "4.7.2",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [