@codyswann/lisa 2.222.0 → 2.222.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.
Files changed (73) hide show
  1. package/dist/utils/usage-accounting.d.ts +3 -2
  2. package/dist/utils/usage-accounting.d.ts.map +1 -1
  3. package/dist/utils/usage-accounting.js +127 -48
  4. package/dist/utils/usage-accounting.js.map +1 -1
  5. package/package.json +1 -1
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-usage-accounting/SKILL.md +9 -1
  9. package/plugins/lisa/hooks/parity-safety-net.sh +58 -7
  10. package/plugins/lisa/rules/eager/usage-accounting.md +6 -2
  11. package/plugins/lisa/rules/reference/usage-accounting.md +42 -5
  12. package/plugins/lisa/skills/lisa-usage-accounting/SKILL.md +9 -1
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-agy/skills/lisa-usage-accounting/SKILL.md +9 -1
  15. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +58 -7
  22. package/plugins/lisa-copilot/rules/eager/usage-accounting.md +6 -2
  23. package/plugins/lisa-copilot/rules/reference/usage-accounting.md +42 -5
  24. package/plugins/lisa-copilot/skills/lisa-usage-accounting/SKILL.md +9 -1
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +58 -7
  27. package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +42 -5
  28. package/plugins/lisa-cursor/rules/usage-accounting.mdc +6 -2
  29. package/plugins/lisa-cursor/skills/lisa-usage-accounting/SKILL.md +9 -1
  30. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-agy/plugin.json +1 -1
  33. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  53. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-agy/plugin.json +1 -1
  58. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  63. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  68. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/src/base/hooks/parity-safety-net.sh +58 -7
  71. package/plugins/src/base/rules/eager/usage-accounting.md +6 -2
  72. package/plugins/src/base/rules/reference/usage-accounting.md +42 -5
  73. package/plugins/src/base/skills/lisa-usage-accounting/SKILL.md +9 -1
@@ -31,7 +31,7 @@ Each direct usage entry records one logical Lisa run or sub-run on one artifact.
31
31
  | `output_tokens` | Output/completion tokens, or `null` when unavailable. |
32
32
  | `reasoning_tokens` | Reasoning/internal tokens, or `null` when unavailable/not exposed. |
33
33
  | `total_tokens` | Total trustworthy tokens for the entry, or `null`. |
34
- | `measured_subset_tokens` | Trustworthy measured subtotal for a known subset of the run, or `null`. |
34
+ | `measured_subset_tokens` | Optional trustworthy measured subtotal for a known subset of the run. Omission is normalized to `null` so callers written before this field remain source-compatible. |
35
35
  | `cost` | Observed or estimated cost for this entry, or `null`. |
36
36
  | `currency` | ISO currency code when `cost` is known, otherwise `null`. |
37
37
  | `pricing_status` | `observed`, `estimated`, `missing`, or `unavailable`. |
@@ -69,16 +69,34 @@ total.
69
69
  - `unavailable`: the runtime exposed neither trustworthy cost nor enough trustworthy token data to estimate cost.
70
70
 
71
71
  Runtime-observed cost always wins over estimates. Estimated cost never overwrites an observed value. Missing pricing preserves token counts and a `null` cost.
72
+ Token completeness and cost trust are independent: a `measured-subset` entry may still carry an
73
+ observed trustworthy whole-run cost. Token rollups remain unknown, but that cost participates in
74
+ cost rollups under the normal pricing and currency rules.
72
75
 
73
76
  ## Machine-readable tokens
74
77
 
75
- Every visible direct entry row ends with exactly one machine-readable token:
78
+ Every visible direct entry row contains the backward-compatible 17-field primary token:
76
79
 
77
80
  ```text
78
- <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> measured_subset_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
81
+ <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
79
82
  ```
80
83
 
81
- Field order is fixed. A reader parses the usage ledger by matching `<!-- lisa:usage-entry ` lines only; it never needs to scrape prose or table cell positions. String fields are percent-encoded before rendering and decoded after parsing, so whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the token.
84
+ The row immediately follows it with a correlated measured-subset extension. Writers serialize an
85
+ omitted value as `null` instead of `undefined`:
86
+
87
+ ```text
88
+ <!-- lisa:usage-entry-measured-subset entry_id=<id> measured_subset_tokens=<n|null> -->
89
+ ```
90
+
91
+ The primary field order is fixed and deliberately matches the pre-measured-subset contract so
92
+ legacy 17-field readers continue to enumerate entries. Current readers correlate the extension by
93
+ its percent-encoded `entry_id`. They also accept the `@codyswann/lisa@2.222.0` transitional marker,
94
+ which placed `measured_subset_tokens` between `total_tokens` and `cost`, and migrate it to the
95
+ primary-plus-extension layout on rewrite. Only that transitional additive field accepts the literal
96
+ `undefined` emitted by an older caller and normalizes it to `null`; established numeric fields
97
+ remain strict. String fields are percent-encoded before rendering and decoded after parsing, so
98
+ whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the
99
+ token.
82
100
 
83
101
  Every managed section also ends with exactly one rollup token:
84
102
 
@@ -91,6 +109,23 @@ Every managed section also ends with exactly one rollup token:
91
109
  - `child_refs` enumerates the child artifacts consulted for the rollup.
92
110
  - `total_*` fields equal direct plus child totals over the deduped entry set.
93
111
 
112
+ If a direct entry or freshly resolved child entry is `measured-subset`, the corresponding token
113
+ scope and combined `total_tokens` are `null`; a measured subtotal is never added to complete token
114
+ counts. Other established nullable-entry behavior is unchanged. Cost rollups are computed
115
+ independently, so a trustworthy whole-run cost is not discarded merely because token completeness
116
+ is unknown.
117
+
118
+ When freshly resolved child work contains a measured subset, the primary rollup token is followed
119
+ by this optional backward-compatible extension:
120
+
121
+ ```text
122
+ <!-- lisa:usage-rollup-token-status child_tokens_incomplete=true -->
123
+ ```
124
+
125
+ The extension preserves child-token incompleteness across ordinary direct-entry rewrites that do
126
+ not resolve children again. It is omitted when child token totals are not known to be incomplete,
127
+ so legacy rollup objects and readers retain their established shape.
128
+
94
129
  The rollup token is the machine-readable summary. The visible rollup table mirrors it for humans. List fields are comma-delimited after encoding each item independently; commas inside an item are encoded as data, not treated as separators.
95
130
 
96
131
  ## Visible rendering contract
@@ -143,7 +178,9 @@ The rollup contract is additive across the hierarchy: PRDs may roll up Epics/Sto
143
178
  - Recompute the entire section on every write; never append ad hoc rows.
144
179
  - Sort direct entries deterministically by `(flow, run_id, entry_id)`.
145
180
  - Preserve existing entries with unchanged `entry_id` and refreshed field values.
146
- - Re-running with the same logical entry set must produce byte-identical output.
181
+ - Re-running with the same logical entry set must produce byte-identical output. Rewriting a legacy
182
+ primary-only marker or the 2.222.0 transitional marker migrates once to the canonical
183
+ primary-plus-extension layout; subsequent rewrites are byte-identical.
147
184
  - Do not include timestamps in the section preamble or token lines.
148
185
 
149
186
  Idempotency is enforced by `entry_id` for direct entries and by the fixed rollup token field order for totals.
@@ -69,7 +69,10 @@ fields.
69
69
 
70
70
  When the runtime exposes only a trustworthy subtotal, callers MUST use `source: measured-subset`,
71
71
  write the subtotal to `measured_subset_tokens`, and leave `total_tokens: null`. Do not coerce a
72
- known subset into `total_tokens`; rollups use `total_tokens` only for complete totals.
72
+ known subset into `total_tokens`; rollups use `total_tokens` only for complete totals. The
73
+ `measured_subset_tokens` field is optional for backward compatibility with callers that construct
74
+ ordinary observed, estimated, or unavailable entries; the shared serializer normalizes omission
75
+ to `null`. A trustworthy whole-run cost remains valid independently of incomplete token telemetry.
73
76
 
74
77
  ## Return shape
75
78
 
@@ -168,6 +171,11 @@ failures into a generic "usage update failed."
168
171
  canonical `usage-accounting` rule.
169
172
  - Never append a second `## Lisa Usage` section or a second managed usage comment.
170
173
  - Never treat missing usage as zero. Callers must record explicit `source: unavailable` entries.
174
+ - Never add a measured subset to complete token totals. A mixed complete-plus-measured-subset
175
+ direct or child scope has `null` token rollups, while trustworthy whole-run cost rolls up
176
+ independently.
177
+ - Never discard the optional child-token incompleteness state parsed from an existing rollup when
178
+ `child_refs` were not refreshed. Preserve it through ordinary direct-entry rewrites.
171
179
  - Never skip rollup dedupe. Child totals are keyed by stable `entry_id`, not by child ref count.
172
180
  - Never silently drop to comments. Return `outcome: comment-fallback` so the caller can surface the
173
181
  writable surface that actually holds the ledger.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -69,7 +69,10 @@ fields.
69
69
 
70
70
  When the runtime exposes only a trustworthy subtotal, callers MUST use `source: measured-subset`,
71
71
  write the subtotal to `measured_subset_tokens`, and leave `total_tokens: null`. Do not coerce a
72
- known subset into `total_tokens`; rollups use `total_tokens` only for complete totals.
72
+ known subset into `total_tokens`; rollups use `total_tokens` only for complete totals. The
73
+ `measured_subset_tokens` field is optional for backward compatibility with callers that construct
74
+ ordinary observed, estimated, or unavailable entries; the shared serializer normalizes omission
75
+ to `null`. A trustworthy whole-run cost remains valid independently of incomplete token telemetry.
73
76
 
74
77
  ## Return shape
75
78
 
@@ -168,6 +171,11 @@ failures into a generic "usage update failed."
168
171
  canonical `usage-accounting` rule.
169
172
  - Never append a second `## Lisa Usage` section or a second managed usage comment.
170
173
  - Never treat missing usage as zero. Callers must record explicit `source: unavailable` entries.
174
+ - Never add a measured subset to complete token totals. A mixed complete-plus-measured-subset
175
+ direct or child scope has `null` token rollups, while trustworthy whole-run cost rolls up
176
+ independently.
177
+ - Never discard the optional child-token incompleteness state parsed from an existing rollup when
178
+ `child_refs` were not refreshed. Preserve it through ordinary direct-entry rewrites.
171
179
  - Never skip rollup dedupe. Child totals are keyed by stable `entry_id`, not by child ref count.
172
180
  - Never silently drop to comments. Return `outcome: comment-fallback` so the caller can surface the
173
181
  writable surface that actually holds the ledger.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
32
32
  exit 0
33
33
  fi
34
34
 
35
+ command_for_guards="$command_str"
36
+ if command -v python3 >/dev/null 2>&1; then
37
+ command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
38
+ import os
39
+ import re
40
+
41
+ command = os.environ.get("SAFETY_NET_COMMAND", "")
42
+
43
+
44
+ def strip_heredocs(text: str) -> str:
45
+ lines = text.splitlines()
46
+ output = []
47
+ pending = []
48
+ # Quoted strings are matched (and thereby skipped over) as plain,
49
+ # non-capturing alternatives BEFORE the heredoc-marker alternative gets a
50
+ # chance to run, so a "<<MARKER"-looking sequence inside a string literal
51
+ # (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
52
+ # start. Only the heredoc-marker alternative captures a group.
53
+ marker_pattern = re.compile(
54
+ r'"(?:\\.|[^"])*"|\'[^\']*\''
55
+ r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
56
+ )
57
+ index = 0
58
+ while index < len(lines):
59
+ line = lines[index]
60
+ output.append(line)
61
+ pending.extend(
62
+ next(group for group in match.groups() if group)
63
+ for match in marker_pattern.finditer(line)
64
+ if any(match.groups())
65
+ )
66
+ index += 1
67
+ # Consume every pending heredoc body in order (chained same-line
68
+ # heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
69
+ # Do NOT stop after the first terminator — dropping the `break` lets
70
+ # this loop keep dropping body lines until each pending marker is
71
+ # matched, instead of leaking the second body back into `output`
72
+ # where the destructive-pattern guards would see it.
73
+ while pending and index < len(lines):
74
+ if lines[index].strip() == pending[0]:
75
+ output.append(lines[index])
76
+ pending.pop(0)
77
+ index += 1
78
+ return "\n".join(output)
79
+
80
+
81
+ print(strip_heredocs(command), end="")
82
+ PY
83
+ )"
84
+ fi
85
+
35
86
  # block() prints the reason to stderr (surfaced to the model) and exits 2 so the
36
87
  # Bash tool call is denied. $1 = human-readable reason for the block.
37
88
  block() {
@@ -49,11 +100,11 @@ EOF
49
100
  # wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
50
101
  # recursive and a force flag, AND name a catastrophic target. Splitting the
51
102
  # flag check from the target check keeps each regex legible and testable.
52
- if printf '%s' "$command_str" \
103
+ if printf '%s' "$command_for_guards" \
53
104
  | grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
54
- || printf '%s' "$command_str" \
105
+ || printf '%s' "$command_for_guards" \
55
106
  | grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
56
- if printf '%s' "$command_str" \
107
+ if printf '%s' "$command_for_guards" \
57
108
  | grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
58
109
  block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
59
110
  fi
@@ -76,7 +127,7 @@ fi
76
127
  # \<newline>main" splits into a segment matching --force but not `main`, letting a
77
128
  # protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
78
129
  # `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
79
- normalized_command_str="$(printf '%s' "$command_str" \
130
+ normalized_command_str="$(printf '%s' "$command_for_guards" \
80
131
  | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
81
132
 
82
133
  while IFS= read -r push_stmt; do
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
93
144
  # 3. `git reset --hard` while the working tree has uncommitted changes — this
94
145
  # silently discards them. Only blocks when the tree is actually dirty, so a
95
146
  # clean-tree reset (a legitimate workflow) still passes.
96
- if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
147
+ if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
97
148
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
98
149
  && [ -n "$(git status --porcelain 2>/dev/null)" ]; then
99
150
  block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
101
152
  fi
102
153
 
103
154
  # 4. Dropping or truncating a database / schema / table.
104
- if printf '%s' "$command_str" \
155
+ if printf '%s' "$command_for_guards" \
105
156
  | grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
106
157
  block "destructive SQL (DROP/TRUNCATE) detected"
107
158
  fi
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
113
164
  case "$rule" in
114
165
  '' | '#'*) continue ;;
115
166
  esac
116
- if printf '%s' "$command_str" | grep -Eiq -- "$rule"; then
167
+ if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
117
168
  block "matched a project custom safety rule (${rules_file##*/}): $rule"
118
169
  fi
119
170
  done <"$rules_file"
@@ -17,7 +17,7 @@ Every artifact with inline body content gets exactly one section:
17
17
  Each direct entry records ONE logical Lisa run on ONE artifact. `entry_id` is the stable dedupe key — rewriting the same logical run with the same `entry_id` updates in place; a different run gets a different `entry_id`.
18
18
 
19
19
  - **`source`**: `observed` (runtime supplied) / `estimated` (derived from trustworthy metadata + pricing contract) / `measured-subset` (a trustworthy subtotal exists, but the complete run total is unknown) / `unavailable`.
20
- - **`measured_subset_tokens`**: measured subtotal for `measured-subset` entries only. Keep `total_tokens = null` so rollups do not treat a subset as a complete total.
20
+ - **`measured_subset_tokens`**: optional measured subtotal for `measured-subset` entries only. Omission is normalized to `null` for backward-compatible callers. Keep `total_tokens = null` so rollups do not treat a subset as a complete total.
21
21
  - **`pricing_status`**: same trinary plus `missing` (cost not known but should be).
22
22
  - **Absence ≠ zero.** `null` means unknown; `0` means explicitly zero. Always write the entry — never silently omit.
23
23
  - Do NOT replace observed counts with estimates.
@@ -26,4 +26,8 @@ Each direct entry records ONE logical Lisa run on ONE artifact. `entry_id` is th
26
26
 
27
27
  Container artifacts (Epic, PRD, etc.) roll up usage from their direct children. Roll-up is recursive — a parent's `## Lisa Usage` aggregates its descendants' direct entries. Re-writes are idempotent: re-running an intake or lifecycle skill must not duplicate entries.
28
28
 
29
- Full schema (all 18 fields, pricing semantics, rollup math, idempotent-rewrite rules): [reference/usage-accounting.md](../reference/usage-accounting.md).
29
+ Measured-child incompleteness is durable across ordinary rewrites through the optional
30
+ `lisa:usage-rollup-token-status` extension. Do not infer completeness merely because a rewrite did
31
+ not re-fetch child ledgers.
32
+
33
+ Full schema (backward-compatible 17-field primary marker, correlated measured-subset extension, pricing semantics, rollup math, idempotent-migration rules): [reference/usage-accounting.md](../reference/usage-accounting.md).
@@ -31,7 +31,7 @@ Each direct usage entry records one logical Lisa run or sub-run on one artifact.
31
31
  | `output_tokens` | Output/completion tokens, or `null` when unavailable. |
32
32
  | `reasoning_tokens` | Reasoning/internal tokens, or `null` when unavailable/not exposed. |
33
33
  | `total_tokens` | Total trustworthy tokens for the entry, or `null`. |
34
- | `measured_subset_tokens` | Trustworthy measured subtotal for a known subset of the run, or `null`. |
34
+ | `measured_subset_tokens` | Optional trustworthy measured subtotal for a known subset of the run. Omission is normalized to `null` so callers written before this field remain source-compatible. |
35
35
  | `cost` | Observed or estimated cost for this entry, or `null`. |
36
36
  | `currency` | ISO currency code when `cost` is known, otherwise `null`. |
37
37
  | `pricing_status` | `observed`, `estimated`, `missing`, or `unavailable`. |
@@ -69,16 +69,34 @@ total.
69
69
  - `unavailable`: the runtime exposed neither trustworthy cost nor enough trustworthy token data to estimate cost.
70
70
 
71
71
  Runtime-observed cost always wins over estimates. Estimated cost never overwrites an observed value. Missing pricing preserves token counts and a `null` cost.
72
+ Token completeness and cost trust are independent: a `measured-subset` entry may still carry an
73
+ observed trustworthy whole-run cost. Token rollups remain unknown, but that cost participates in
74
+ cost rollups under the normal pricing and currency rules.
72
75
 
73
76
  ## Machine-readable tokens
74
77
 
75
- Every visible direct entry row ends with exactly one machine-readable token:
78
+ Every visible direct entry row contains the backward-compatible 17-field primary token:
76
79
 
77
80
  ```text
78
- <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> measured_subset_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
81
+ <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
79
82
  ```
80
83
 
81
- Field order is fixed. A reader parses the usage ledger by matching `<!-- lisa:usage-entry ` lines only; it never needs to scrape prose or table cell positions. String fields are percent-encoded before rendering and decoded after parsing, so whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the token.
84
+ The row immediately follows it with a correlated measured-subset extension. Writers serialize an
85
+ omitted value as `null` instead of `undefined`:
86
+
87
+ ```text
88
+ <!-- lisa:usage-entry-measured-subset entry_id=<id> measured_subset_tokens=<n|null> -->
89
+ ```
90
+
91
+ The primary field order is fixed and deliberately matches the pre-measured-subset contract so
92
+ legacy 17-field readers continue to enumerate entries. Current readers correlate the extension by
93
+ its percent-encoded `entry_id`. They also accept the `@codyswann/lisa@2.222.0` transitional marker,
94
+ which placed `measured_subset_tokens` between `total_tokens` and `cost`, and migrate it to the
95
+ primary-plus-extension layout on rewrite. Only that transitional additive field accepts the literal
96
+ `undefined` emitted by an older caller and normalizes it to `null`; established numeric fields
97
+ remain strict. String fields are percent-encoded before rendering and decoded after parsing, so
98
+ whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the
99
+ token.
82
100
 
83
101
  Every managed section also ends with exactly one rollup token:
84
102
 
@@ -91,6 +109,23 @@ Every managed section also ends with exactly one rollup token:
91
109
  - `child_refs` enumerates the child artifacts consulted for the rollup.
92
110
  - `total_*` fields equal direct plus child totals over the deduped entry set.
93
111
 
112
+ If a direct entry or freshly resolved child entry is `measured-subset`, the corresponding token
113
+ scope and combined `total_tokens` are `null`; a measured subtotal is never added to complete token
114
+ counts. Other established nullable-entry behavior is unchanged. Cost rollups are computed
115
+ independently, so a trustworthy whole-run cost is not discarded merely because token completeness
116
+ is unknown.
117
+
118
+ When freshly resolved child work contains a measured subset, the primary rollup token is followed
119
+ by this optional backward-compatible extension:
120
+
121
+ ```text
122
+ <!-- lisa:usage-rollup-token-status child_tokens_incomplete=true -->
123
+ ```
124
+
125
+ The extension preserves child-token incompleteness across ordinary direct-entry rewrites that do
126
+ not resolve children again. It is omitted when child token totals are not known to be incomplete,
127
+ so legacy rollup objects and readers retain their established shape.
128
+
94
129
  The rollup token is the machine-readable summary. The visible rollup table mirrors it for humans. List fields are comma-delimited after encoding each item independently; commas inside an item are encoded as data, not treated as separators.
95
130
 
96
131
  ## Visible rendering contract
@@ -143,7 +178,9 @@ The rollup contract is additive across the hierarchy: PRDs may roll up Epics/Sto
143
178
  - Recompute the entire section on every write; never append ad hoc rows.
144
179
  - Sort direct entries deterministically by `(flow, run_id, entry_id)`.
145
180
  - Preserve existing entries with unchanged `entry_id` and refreshed field values.
146
- - Re-running with the same logical entry set must produce byte-identical output.
181
+ - Re-running with the same logical entry set must produce byte-identical output. Rewriting a legacy
182
+ primary-only marker or the 2.222.0 transitional marker migrates once to the canonical
183
+ primary-plus-extension layout; subsequent rewrites are byte-identical.
147
184
  - Do not include timestamps in the section preamble or token lines.
148
185
 
149
186
  Idempotency is enforced by `entry_id` for direct entries and by the fixed rollup token field order for totals.
@@ -69,7 +69,10 @@ fields.
69
69
 
70
70
  When the runtime exposes only a trustworthy subtotal, callers MUST use `source: measured-subset`,
71
71
  write the subtotal to `measured_subset_tokens`, and leave `total_tokens: null`. Do not coerce a
72
- known subset into `total_tokens`; rollups use `total_tokens` only for complete totals.
72
+ known subset into `total_tokens`; rollups use `total_tokens` only for complete totals. The
73
+ `measured_subset_tokens` field is optional for backward compatibility with callers that construct
74
+ ordinary observed, estimated, or unavailable entries; the shared serializer normalizes omission
75
+ to `null`. A trustworthy whole-run cost remains valid independently of incomplete token telemetry.
73
76
 
74
77
  ## Return shape
75
78
 
@@ -168,6 +171,11 @@ failures into a generic "usage update failed."
168
171
  canonical `usage-accounting` rule.
169
172
  - Never append a second `## Lisa Usage` section or a second managed usage comment.
170
173
  - Never treat missing usage as zero. Callers must record explicit `source: unavailable` entries.
174
+ - Never add a measured subset to complete token totals. A mixed complete-plus-measured-subset
175
+ direct or child scope has `null` token rollups, while trustworthy whole-run cost rolls up
176
+ independently.
177
+ - Never discard the optional child-token incompleteness state parsed from an existing rollup when
178
+ `child_refs` were not refreshed. Preserve it through ordinary direct-entry rewrites.
171
179
  - Never skip rollup dedupe. Child totals are keyed by stable `entry_id`, not by child ref count.
172
180
  - Never silently drop to comments. Return `outcome: comment-fallback` so the caller can surface the
173
181
  writable surface that actually holds the ledger.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.222.0",
3
+ "version": "2.222.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
32
32
  exit 0
33
33
  fi
34
34
 
35
+ command_for_guards="$command_str"
36
+ if command -v python3 >/dev/null 2>&1; then
37
+ command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
38
+ import os
39
+ import re
40
+
41
+ command = os.environ.get("SAFETY_NET_COMMAND", "")
42
+
43
+
44
+ def strip_heredocs(text: str) -> str:
45
+ lines = text.splitlines()
46
+ output = []
47
+ pending = []
48
+ # Quoted strings are matched (and thereby skipped over) as plain,
49
+ # non-capturing alternatives BEFORE the heredoc-marker alternative gets a
50
+ # chance to run, so a "<<MARKER"-looking sequence inside a string literal
51
+ # (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
52
+ # start. Only the heredoc-marker alternative captures a group.
53
+ marker_pattern = re.compile(
54
+ r'"(?:\\.|[^"])*"|\'[^\']*\''
55
+ r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
56
+ )
57
+ index = 0
58
+ while index < len(lines):
59
+ line = lines[index]
60
+ output.append(line)
61
+ pending.extend(
62
+ next(group for group in match.groups() if group)
63
+ for match in marker_pattern.finditer(line)
64
+ if any(match.groups())
65
+ )
66
+ index += 1
67
+ # Consume every pending heredoc body in order (chained same-line
68
+ # heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
69
+ # Do NOT stop after the first terminator — dropping the `break` lets
70
+ # this loop keep dropping body lines until each pending marker is
71
+ # matched, instead of leaking the second body back into `output`
72
+ # where the destructive-pattern guards would see it.
73
+ while pending and index < len(lines):
74
+ if lines[index].strip() == pending[0]:
75
+ output.append(lines[index])
76
+ pending.pop(0)
77
+ index += 1
78
+ return "\n".join(output)
79
+
80
+
81
+ print(strip_heredocs(command), end="")
82
+ PY
83
+ )"
84
+ fi
85
+
35
86
  # block() prints the reason to stderr (surfaced to the model) and exits 2 so the
36
87
  # Bash tool call is denied. $1 = human-readable reason for the block.
37
88
  block() {
@@ -49,11 +100,11 @@ EOF
49
100
  # wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
50
101
  # recursive and a force flag, AND name a catastrophic target. Splitting the
51
102
  # flag check from the target check keeps each regex legible and testable.
52
- if printf '%s' "$command_str" \
103
+ if printf '%s' "$command_for_guards" \
53
104
  | grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
54
- || printf '%s' "$command_str" \
105
+ || printf '%s' "$command_for_guards" \
55
106
  | grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
56
- if printf '%s' "$command_str" \
107
+ if printf '%s' "$command_for_guards" \
57
108
  | grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
58
109
  block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
59
110
  fi
@@ -76,7 +127,7 @@ fi
76
127
  # \<newline>main" splits into a segment matching --force but not `main`, letting a
77
128
  # protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
78
129
  # `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
79
- normalized_command_str="$(printf '%s' "$command_str" \
130
+ normalized_command_str="$(printf '%s' "$command_for_guards" \
80
131
  | awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
81
132
 
82
133
  while IFS= read -r push_stmt; do
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
93
144
  # 3. `git reset --hard` while the working tree has uncommitted changes — this
94
145
  # silently discards them. Only blocks when the tree is actually dirty, so a
95
146
  # clean-tree reset (a legitimate workflow) still passes.
96
- if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
147
+ if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
97
148
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
98
149
  && [ -n "$(git status --porcelain 2>/dev/null)" ]; then
99
150
  block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
101
152
  fi
102
153
 
103
154
  # 4. Dropping or truncating a database / schema / table.
104
- if printf '%s' "$command_str" \
155
+ if printf '%s' "$command_for_guards" \
105
156
  | grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
106
157
  block "destructive SQL (DROP/TRUNCATE) detected"
107
158
  fi
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
113
164
  case "$rule" in
114
165
  '' | '#'*) continue ;;
115
166
  esac
116
- if printf '%s' "$command_str" | grep -Eiq -- "$rule"; then
167
+ if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
117
168
  block "matched a project custom safety rule (${rules_file##*/}): $rule"
118
169
  fi
119
170
  done <"$rules_file"
@@ -36,7 +36,7 @@ Each direct usage entry records one logical Lisa run or sub-run on one artifact.
36
36
  | `output_tokens` | Output/completion tokens, or `null` when unavailable. |
37
37
  | `reasoning_tokens` | Reasoning/internal tokens, or `null` when unavailable/not exposed. |
38
38
  | `total_tokens` | Total trustworthy tokens for the entry, or `null`. |
39
- | `measured_subset_tokens` | Trustworthy measured subtotal for a known subset of the run, or `null`. |
39
+ | `measured_subset_tokens` | Optional trustworthy measured subtotal for a known subset of the run. Omission is normalized to `null` so callers written before this field remain source-compatible. |
40
40
  | `cost` | Observed or estimated cost for this entry, or `null`. |
41
41
  | `currency` | ISO currency code when `cost` is known, otherwise `null`. |
42
42
  | `pricing_status` | `observed`, `estimated`, `missing`, or `unavailable`. |
@@ -74,16 +74,34 @@ total.
74
74
  - `unavailable`: the runtime exposed neither trustworthy cost nor enough trustworthy token data to estimate cost.
75
75
 
76
76
  Runtime-observed cost always wins over estimates. Estimated cost never overwrites an observed value. Missing pricing preserves token counts and a `null` cost.
77
+ Token completeness and cost trust are independent: a `measured-subset` entry may still carry an
78
+ observed trustworthy whole-run cost. Token rollups remain unknown, but that cost participates in
79
+ cost rollups under the normal pricing and currency rules.
77
80
 
78
81
  ## Machine-readable tokens
79
82
 
80
- Every visible direct entry row ends with exactly one machine-readable token:
83
+ Every visible direct entry row contains the backward-compatible 17-field primary token:
81
84
 
82
85
  ```text
83
- <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> measured_subset_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
86
+ <!-- lisa:usage-entry entry_id=<id> flow=<flow> run_id=<run-id> provider=<provider> model=<model> source=<source> input_tokens=<n|null> cached_input_tokens=<n|null> output_tokens=<n|null> reasoning_tokens=<n|null> total_tokens=<n|null> cost=<decimal|null> currency=<code|null> pricing_status=<status> pricing_source=<ref|null> artifact_ref=<ref> parent_artifact_ref=<ref-or-empty> -->
84
87
  ```
85
88
 
86
- Field order is fixed. A reader parses the usage ledger by matching `<!-- lisa:usage-entry ` lines only; it never needs to scrape prose or table cell positions. String fields are percent-encoded before rendering and decoded after parsing, so whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the token.
89
+ The row immediately follows it with a correlated measured-subset extension. Writers serialize an
90
+ omitted value as `null` instead of `undefined`:
91
+
92
+ ```text
93
+ <!-- lisa:usage-entry-measured-subset entry_id=<id> measured_subset_tokens=<n|null> -->
94
+ ```
95
+
96
+ The primary field order is fixed and deliberately matches the pre-measured-subset contract so
97
+ legacy 17-field readers continue to enumerate entries. Current readers correlate the extension by
98
+ its percent-encoded `entry_id`. They also accept the `@codyswann/lisa@2.222.0` transitional marker,
99
+ which placed `measured_subset_tokens` between `total_tokens` and `cost`, and migrate it to the
100
+ primary-plus-extension layout on rewrite. Only that transitional additive field accepts the literal
101
+ `undefined` emitted by an older caller and normalizes it to `null`; established numeric fields
102
+ remain strict. String fields are percent-encoded before rendering and decoded after parsing, so
103
+ whitespace, commas, and HTML comment terminators inside source values cannot split or truncate the
104
+ token.
87
105
 
88
106
  Every managed section also ends with exactly one rollup token:
89
107
 
@@ -96,6 +114,23 @@ Every managed section also ends with exactly one rollup token:
96
114
  - `child_refs` enumerates the child artifacts consulted for the rollup.
97
115
  - `total_*` fields equal direct plus child totals over the deduped entry set.
98
116
 
117
+ If a direct entry or freshly resolved child entry is `measured-subset`, the corresponding token
118
+ scope and combined `total_tokens` are `null`; a measured subtotal is never added to complete token
119
+ counts. Other established nullable-entry behavior is unchanged. Cost rollups are computed
120
+ independently, so a trustworthy whole-run cost is not discarded merely because token completeness
121
+ is unknown.
122
+
123
+ When freshly resolved child work contains a measured subset, the primary rollup token is followed
124
+ by this optional backward-compatible extension:
125
+
126
+ ```text
127
+ <!-- lisa:usage-rollup-token-status child_tokens_incomplete=true -->
128
+ ```
129
+
130
+ The extension preserves child-token incompleteness across ordinary direct-entry rewrites that do
131
+ not resolve children again. It is omitted when child token totals are not known to be incomplete,
132
+ so legacy rollup objects and readers retain their established shape.
133
+
99
134
  The rollup token is the machine-readable summary. The visible rollup table mirrors it for humans. List fields are comma-delimited after encoding each item independently; commas inside an item are encoded as data, not treated as separators.
100
135
 
101
136
  ## Visible rendering contract
@@ -148,7 +183,9 @@ The rollup contract is additive across the hierarchy: PRDs may roll up Epics/Sto
148
183
  - Recompute the entire section on every write; never append ad hoc rows.
149
184
  - Sort direct entries deterministically by `(flow, run_id, entry_id)`.
150
185
  - Preserve existing entries with unchanged `entry_id` and refreshed field values.
151
- - Re-running with the same logical entry set must produce byte-identical output.
186
+ - Re-running with the same logical entry set must produce byte-identical output. Rewriting a legacy
187
+ primary-only marker or the 2.222.0 transitional marker migrates once to the canonical
188
+ primary-plus-extension layout; subsequent rewrites are byte-identical.
152
189
  - Do not include timestamps in the section preamble or token lines.
153
190
 
154
191
  Idempotency is enforced by `entry_id` for direct entries and by the fixed rollup token field order for totals.