@1aboveio/skills 0.10.1 → 0.11.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.
@@ -0,0 +1,193 @@
1
+ # Velocity Feature Calculation Best Practices
2
+
3
+ Use these practices for rolling counts and sums, distinct counterparties,
4
+ temporal velocity features, and FIFO/path calculations over event histories.
5
+ The snippets illustrate the design; adapt schemas and semantics to the job's
6
+ required behavior.
7
+
8
+ ## Use Built-Ins When Velocity Metrics Are Independent
9
+
10
+ For ordinary rolling counts and sums where same-time peers share one frame,
11
+ prefer a Spark range window:
12
+
13
+ ```python
14
+ from pyspark.sql import Window, functions as F
15
+
16
+ last_24h = (
17
+ Window.partitionBy("entity_id")
18
+ .orderBy(F.col("event_time").cast("long"))
19
+ .rangeBetween(-24 * 60 * 60, 0)
20
+ )
21
+
22
+ velocity = (
23
+ events.withColumn("event_count_24h", F.count(F.lit(1)).over(last_24h))
24
+ .withColumn("amount_usd_24h", F.sum("amount_usd").over(last_24h))
25
+ )
26
+ ```
27
+
28
+ This stays inside Catalyst and avoids Python serialization. It is not sufficient
29
+ when same-time visibility uses secondary keys, when distinct state requires
30
+ reference-counted eviction, or when velocity and FIFO transitions must share
31
+ one exact sequence.
32
+
33
+ ## Keep Coupled State In One Pure Sweep
34
+
35
+ When rolling windows and FIFO/path transitions share one exact sequence,
36
+ isolate that sequence as a pure algorithm before choosing how Spark executes
37
+ it:
38
+
39
+ ```python
40
+ from datetime import timedelta
41
+
42
+
43
+ def sweep_events(events):
44
+ ordered = sorted(
45
+ events,
46
+ key=lambda event: (
47
+ event.event_time,
48
+ event.transaction_id,
49
+ event.event_id,
50
+ ),
51
+ )
52
+ windows = {
53
+ "1h": RollingWindow(timedelta(hours=1)),
54
+ "24h": RollingWindow(timedelta(hours=24)),
55
+ }
56
+ fifo = FifoState()
57
+
58
+ for event in ordered:
59
+ for window in windows.values():
60
+ window.append(event)
61
+ path = fifo.advance(event)
62
+ yield build_result(event, windows, path)
63
+ ```
64
+
65
+ This is the semantic core, not a recommendation to wrap it in `F.udf`. Select
66
+ the Spark execution API using the decision table in
67
+ [Transformation Design](transformation-design.md): built-in windows first,
68
+ then bounded higher-order aggregation, grouped pandas only for bounded groups,
69
+ and a different engine/design for large sequential state.
70
+
71
+ Whichever bridge is chosen, emit one result per unique output identity and
72
+ measure it against the pair relation or repeated grouping it replaces.
73
+
74
+ ## Make Window Boundaries Literal
75
+
76
+ For a current-inclusive horizon `H`, evict only events strictly older than the
77
+ cutoff:
78
+
79
+ ```python
80
+ from collections import Counter, deque
81
+
82
+
83
+ class RollingWindow:
84
+ def __init__(self, horizon):
85
+ self.horizon = horizon
86
+ self.events = deque()
87
+ self.count = 0
88
+ self.counterparties = Counter()
89
+
90
+ def append(self, event):
91
+ cutoff = event.event_time - self.horizon
92
+ while self.events and self.events[0].event_time < cutoff:
93
+ self.remove(self.events.popleft())
94
+
95
+ self.events.append(event) # current event is visible
96
+ self.count += 1
97
+ if event.counterparty_key is not None:
98
+ self.counterparties[event.counterparty_key] += 1
99
+
100
+ def remove(self, event):
101
+ self.count -= 1
102
+ key = event.counterparty_key
103
+ if key is not None:
104
+ self.counterparties[key] -= 1
105
+ if self.counterparties[key] == 0:
106
+ del self.counterparties[key]
107
+ ```
108
+
109
+ Using `<= cutoff` makes the lower boundary exclusive. Test the exact boundary
110
+ and one smallest supported time unit beyond it.
111
+
112
+ A reference-counted map is required for distinct counterparties because a set
113
+ cannot represent repeated counterparties during eviction.
114
+
115
+ ## Define Same-Timestamp Visibility
116
+
117
+ Use the complete stable order, for example:
118
+
119
+ ```text
120
+ (event_time, transaction_id, event_id)
121
+ ```
122
+
123
+ If same-time events see earlier tie-ordered events, update and emit in that
124
+ order. If they should all see the same batch, emit the timestamp batch before
125
+ updating state. Never let incidental Spark input order define visibility.
126
+
127
+ ## Use Numerically Stable Rolling State
128
+
129
+ Repeated floating addition and eviction can leave residue. Use compensated
130
+ addition/removal, or exact decimal state when the domain requires exact money:
131
+
132
+ ```python
133
+ class CompensatedSum:
134
+ def __init__(self):
135
+ self.total = 0.0
136
+ self.correction = 0.0
137
+
138
+ def add(self, value):
139
+ updated = self.total + value
140
+ if abs(self.total) >= abs(value):
141
+ self.correction += (self.total - updated) + value
142
+ else:
143
+ self.correction += (value - updated) + self.total
144
+ self.total = updated
145
+
146
+ def remove(self, value):
147
+ self.add(-value)
148
+
149
+ def value(self):
150
+ return self.total + self.correction
151
+ ```
152
+
153
+ Test values with different magnitudes, evict them, and assert that the previous
154
+ state is restored.
155
+
156
+ ## Combine Related State
157
+
158
+ Within the same sweep:
159
+
160
+ 1. Evict expired events from each horizon.
161
+ 2. Add the current event according to visibility semantics.
162
+ 3. Emit counts, sums, missing-value counts, directional metrics, and distinct
163
+ counterparties.
164
+ 4. Advance FIFO/path state using the same stable order.
165
+ 5. Emit one result keyed by output identity.
166
+
167
+ This removes the pair relation and avoids grouping and sorting the same history
168
+ for each feature family.
169
+
170
+ ## Prove Parity And Plan Shape
171
+
172
+ Keep the old self-join only as a bounded test oracle. Cover exact 1h/24h
173
+ boundaries, one unit outside, same-time ties, nulls, both directions,
174
+ missing/present FX, repeated counterparties, path transitions, and floating
175
+ residue.
176
+
177
+ Compare full rows in both directions, add literal boundary assertions, and
178
+ assert the candidate physical plan has no customer/time interval join.
179
+
180
+ ## Measure Residual Per-Key Memory
181
+
182
+ `groupBy + collect_list` changes quadratic distributed work into linear per-key
183
+ work, but one task still holds the entire largest key. Measure and report
184
+ `max_rows_per_key`, then production-canary the largest known shape. Use bounded
185
+ state or exact temporal bucketing if one key no longer fits.
186
+
187
+ ## Avoid These False Fixes
188
+
189
+ - Do not rely on repartitioning to split one hot grouping key.
190
+ - Do not drop history, events, or feature formulas to gain speed.
191
+ - Do not present Arrow transport, broadcasting, or explicit persistence as the
192
+ performance fix; reduce the logical work instead.
193
+ - Do not report validate-only runtime as end-to-end write performance.
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env python3
2
+ """Summarize stage attempts and final SQL plans from a Spark JSON event log."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from collections.abc import Iterable
11
+ from typing import Any
12
+
13
+ METRICS = (
14
+ "input_bytes",
15
+ "input_records",
16
+ "output_bytes",
17
+ "output_records",
18
+ "shuffle_read_bytes",
19
+ "shuffle_read_records",
20
+ "shuffle_write_bytes",
21
+ "shuffle_write_records",
22
+ "memory_spill_bytes",
23
+ "disk_spill_bytes",
24
+ )
25
+
26
+
27
+ def _patterns(values: Iterable[str]) -> dict[str, re.Pattern[str]]:
28
+ result: dict[str, re.Pattern[str]] = {}
29
+ for value in values:
30
+ name, separator, pattern = value.partition("=")
31
+ if not separator or not name or not pattern:
32
+ raise ValueError(
33
+ f"invalid --plan-pattern {value!r}; expected NAME=REGEX"
34
+ )
35
+ result[name] = re.compile(pattern, re.IGNORECASE)
36
+ return result
37
+
38
+
39
+ def _add(stage: dict[str, Any], name: str, value: Any) -> None:
40
+ stage[name] = stage.get(name, 0) + (value or 0)
41
+
42
+
43
+ def summarize(
44
+ lines: Iterable[str], plan_patterns: dict[str, re.Pattern[str]]
45
+ ) -> dict[str, Any]:
46
+ app: dict[str, Any] = {}
47
+ stages: dict[tuple[int, int], dict[str, Any]] = {}
48
+ sql_execution_ids: set[Any] = set()
49
+ sql_plans: dict[Any, str] = {}
50
+ anonymous_sql_id = 0
51
+
52
+ for raw in lines:
53
+ try:
54
+ event = json.loads(raw)
55
+ except (json.JSONDecodeError, TypeError):
56
+ continue
57
+
58
+ kind = event.get("Event")
59
+ if kind == "SparkListenerApplicationStart":
60
+ app.update(
61
+ {
62
+ "app_id": event.get("App ID"),
63
+ "app_name": event.get("App Name"),
64
+ "start_time_ms": event.get("Timestamp"),
65
+ }
66
+ )
67
+ elif kind == "SparkListenerApplicationEnd":
68
+ app["end_time_ms"] = event.get("Timestamp")
69
+ elif kind in {
70
+ "org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionStart",
71
+ "org.apache.spark.sql.execution.ui.SparkListenerSQLAdaptiveExecutionUpdate",
72
+ }:
73
+ execution_id = event.get("executionId")
74
+ if execution_id is None:
75
+ execution_id = f"anonymous-{anonymous_sql_id}"
76
+ anonymous_sql_id += 1
77
+ sql_execution_ids.add(execution_id)
78
+ sql_plans[execution_id] = event.get("physicalPlanDescription") or ""
79
+ elif kind == "SparkListenerStageCompleted":
80
+ info = event.get("Stage Info") or {}
81
+ stage_id = info.get("Stage ID")
82
+ if stage_id is None:
83
+ continue
84
+ attempt_id = info.get("Stage Attempt ID", 0)
85
+ key = (stage_id, attempt_id)
86
+ stage = stages.setdefault(
87
+ key,
88
+ {"stage_id": stage_id, "stage_attempt_id": attempt_id},
89
+ )
90
+ stage.update(
91
+ {
92
+ "name": info.get("Stage Name"),
93
+ "task_count": info.get("Number of Tasks", 0),
94
+ "submission_time_ms": info.get("Submission Time"),
95
+ "completion_time_ms": info.get("Completion Time"),
96
+ }
97
+ )
98
+ if (
99
+ stage["submission_time_ms"] is not None
100
+ and stage["completion_time_ms"] is not None
101
+ ):
102
+ stage["wall_ms"] = (
103
+ stage["completion_time_ms"] - stage["submission_time_ms"]
104
+ )
105
+ elif kind == "SparkListenerTaskEnd":
106
+ stage_id = event.get("Stage ID")
107
+ if stage_id is None:
108
+ continue
109
+ attempt_id = event.get("Stage Attempt ID", 0)
110
+ key = (stage_id, attempt_id)
111
+ stage = stages.setdefault(
112
+ key,
113
+ {"stage_id": stage_id, "stage_attempt_id": attempt_id},
114
+ )
115
+ info = event.get("Task Info") or {}
116
+ metrics = event.get("Task Metrics") or {}
117
+ launch = info.get("Launch Time")
118
+ finish = info.get("Finish Time")
119
+ duration = finish - launch if launch is not None and finish is not None else 0
120
+ stage["max_task_ms"] = max(stage.get("max_task_ms", 0), duration)
121
+ _add(stage, "task_end_count", 1)
122
+ _add(stage, "memory_spill_bytes", metrics.get("Memory Bytes Spilled"))
123
+ _add(stage, "disk_spill_bytes", metrics.get("Disk Bytes Spilled"))
124
+
125
+ input_metrics = metrics.get("Input Metrics") or {}
126
+ output_metrics = metrics.get("Output Metrics") or {}
127
+ shuffle_read = metrics.get("Shuffle Read Metrics") or {}
128
+ shuffle_write = metrics.get("Shuffle Write Metrics") or {}
129
+ _add(stage, "input_bytes", input_metrics.get("Bytes Read"))
130
+ _add(stage, "input_records", input_metrics.get("Records Read"))
131
+ _add(stage, "output_bytes", output_metrics.get("Bytes Written"))
132
+ _add(stage, "output_records", output_metrics.get("Records Written"))
133
+ _add(
134
+ stage,
135
+ "shuffle_read_bytes",
136
+ (shuffle_read.get("Remote Bytes Read") or 0)
137
+ + (shuffle_read.get("Local Bytes Read") or 0),
138
+ )
139
+ _add(
140
+ stage,
141
+ "shuffle_read_records",
142
+ shuffle_read.get("Total Records Read"),
143
+ )
144
+ _add(
145
+ stage,
146
+ "shuffle_write_bytes",
147
+ shuffle_write.get("Shuffle Bytes Written"),
148
+ )
149
+ _add(
150
+ stage,
151
+ "shuffle_write_records",
152
+ shuffle_write.get("Shuffle Records Written"),
153
+ )
154
+
155
+ completed = sorted(
156
+ (stage for stage in stages.values() if "wall_ms" in stage),
157
+ key=lambda stage: (stage["stage_id"], stage["stage_attempt_id"]),
158
+ )
159
+ totals = {
160
+ name: sum(stage.get(name, 0) for stage in completed) for name in METRICS
161
+ }
162
+ sql = {
163
+ "execution_count": len(sql_execution_ids),
164
+ "plan_pattern_counts": {
165
+ name: sum(len(pattern.findall(plan)) for plan in sql_plans.values())
166
+ for name, pattern in plan_patterns.items()
167
+ },
168
+ }
169
+ if app.get("start_time_ms") is not None and app.get("end_time_ms") is not None:
170
+ app["eventlog_wall_ms"] = app["end_time_ms"] - app["start_time_ms"]
171
+
172
+ def largest(metric: str) -> dict[str, Any]:
173
+ return max(completed, key=lambda stage: stage.get(metric, 0), default={})
174
+
175
+ return {
176
+ "application": app,
177
+ "sql": sql,
178
+ "completed_stage_count": len({stage["stage_id"] for stage in completed}),
179
+ "completed_stage_attempt_count": len(completed),
180
+ "totals": totals,
181
+ "dominant_stage": largest("wall_ms"),
182
+ "max_task_stage": largest("max_task_ms"),
183
+ "max_shuffle_write_records_stage": largest("shuffle_write_records"),
184
+ "stages": completed,
185
+ }
186
+
187
+
188
+ def main() -> int:
189
+ parser = argparse.ArgumentParser(description=__doc__)
190
+ parser.add_argument(
191
+ "--plan-pattern",
192
+ action="append",
193
+ default=[],
194
+ metavar="NAME=REGEX",
195
+ help="count matching physical-plan fragments; may be repeated",
196
+ )
197
+ parser.add_argument(
198
+ "--include-stages",
199
+ action="store_true",
200
+ help="include every completed stage instead of only aggregate/top stages",
201
+ )
202
+ args = parser.parse_args()
203
+
204
+ try:
205
+ patterns = _patterns(args.plan_pattern)
206
+ except (ValueError, re.error) as error:
207
+ parser.error(str(error))
208
+
209
+ result = summarize(sys.stdin, patterns)
210
+ if not result["application"].get("app_id"):
211
+ parser.error(
212
+ "no SparkListenerApplicationStart record found; verify the event-log "
213
+ "path and use shell pipefail so upstream read failures remain visible"
214
+ )
215
+ if not args.include_stages:
216
+ result.pop("stages", None)
217
+ json.dump(result, sys.stdout, indent=2, sort_keys=True)
218
+ sys.stdout.write("\n")
219
+ return 0
220
+
221
+
222
+ if __name__ == "__main__":
223
+ raise SystemExit(main())
@@ -360,7 +360,7 @@
360
360
  "id": "first-party",
361
361
  "type": "first-party",
362
362
  "package": "@1aboveio/skills",
363
- "version": "0.10.1"
363
+ "version": "0.11.0"
364
364
  },
365
365
  "contentDigest": "eff6c7b5931bce5b2265a619bccddd371a89df6ce7bc2edc74f11d00060a71dd",
366
366
  "digestExcludes": []
@@ -373,7 +373,7 @@
373
373
  "id": "first-party",
374
374
  "type": "first-party",
375
375
  "package": "@1aboveio/skills",
376
- "version": "0.10.1"
376
+ "version": "0.11.0"
377
377
  },
378
378
  "contentDigest": "08230dc57a53d6526692b50138038abbd80abce1a067e01c52ad6acc182caf7e",
379
379
  "digestExcludes": []
@@ -386,7 +386,7 @@
386
386
  "id": "first-party",
387
387
  "type": "first-party",
388
388
  "package": "@1aboveio/skills",
389
- "version": "0.10.1"
389
+ "version": "0.11.0"
390
390
  },
391
391
  "contentDigest": "9eea7bfba348ddaea1b934c9a7fabdd9b101df8a146e1c93e947da4d335f1d98",
392
392
  "digestExcludes": []
@@ -399,7 +399,7 @@
399
399
  "id": "first-party",
400
400
  "type": "first-party",
401
401
  "package": "@1aboveio/skills",
402
- "version": "0.10.1"
402
+ "version": "0.11.0"
403
403
  },
404
404
  "contentDigest": "8e379be5d9412d5784c1286a55fc612af4b6479c2247bc1d53f4fb0183fecd98",
405
405
  "digestExcludes": []
@@ -412,9 +412,9 @@
412
412
  "id": "first-party",
413
413
  "type": "first-party",
414
414
  "package": "@1aboveio/skills",
415
- "version": "0.10.1"
415
+ "version": "0.11.0"
416
416
  },
417
- "contentDigest": "8c56881270957ad781e21881ab872a1b7b1b2cfad325cd04738b2a8d1cafa89b",
417
+ "contentDigest": "430b29b6b95e5cba5b06d365254ae5abd680684b7f053b1ca1694319c1b7f9fb",
418
418
  "digestExcludes": []
419
419
  },
420
420
  {
@@ -425,7 +425,7 @@
425
425
  "id": "first-party",
426
426
  "type": "first-party",
427
427
  "package": "@1aboveio/skills",
428
- "version": "0.10.1"
428
+ "version": "0.11.0"
429
429
  },
430
430
  "contentDigest": "b9b918b357b78f3c5d8f5110b66418b3b62a94825dc7f1905dcc5691a9476e86",
431
431
  "digestExcludes": []
@@ -438,7 +438,7 @@
438
438
  "id": "first-party",
439
439
  "type": "first-party",
440
440
  "package": "@1aboveio/skills",
441
- "version": "0.10.1"
441
+ "version": "0.11.0"
442
442
  },
443
443
  "contentDigest": "9cc85e87f5bfbb5570770692f14a95959aac6ade2d5364e827fc39b08871d346",
444
444
  "digestExcludes": []
@@ -451,7 +451,7 @@
451
451
  "id": "first-party",
452
452
  "type": "first-party",
453
453
  "package": "@1aboveio/skills",
454
- "version": "0.10.1"
454
+ "version": "0.11.0"
455
455
  },
456
456
  "contentDigest": "90a7c4e1ad6da1e632ea2c5259e4967ffebb84353adccbca8dfc5d6bc50b60c1",
457
457
  "digestExcludes": []
@@ -464,15 +464,15 @@
464
464
  "id": "first-party",
465
465
  "type": "first-party",
466
466
  "package": "@1aboveio/skills",
467
- "version": "0.10.1"
467
+ "version": "0.11.0"
468
468
  },
469
- "contentDigest": "785b998edafac2458b8d3cab6ed8af78220c61518e58be1afa6cf75fd8e895e6",
469
+ "contentDigest": "0b24d6220d6d2577f2c838ef2c4fa165384594cf4c626960d2e707fd7cd0a228",
470
470
  "digestExcludes": [
471
471
  "coherence/workflow.json"
472
472
  ]
473
473
  }
474
474
  ],
475
- "releaseIdentity": "1a553e637bd6cc46d59819d07b5ae19d7a3adc2d263f613d7b997f4e3fd1737b",
475
+ "releaseIdentity": "2e943acd0d8493f82146b6bd7e2dc769a4f876f2587155913f314b0a5efc120c",
476
476
  "lifecycleAuthority": "native-skills-cli",
477
477
  "repairRecipe": {
478
478
  "id": "engineering-workflow-dependency-first",
@@ -524,7 +524,7 @@
524
524
  "sourceId": "first-party",
525
525
  "sourceType": "first-party",
526
526
  "package": "@1aboveio/skills",
527
- "version": "0.10.1",
527
+ "version": "0.11.0",
528
528
  "installPath": null,
529
529
  "members": [
530
530
  "harness-runtime",
@@ -540,7 +540,7 @@
540
540
  "commands": [
541
541
  {
542
542
  "transport": "npm",
543
- "command": "npx @1aboveio/skills@0.10.1 install --group engineering-workflow"
543
+ "command": "npx @1aboveio/skills@0.11.0 install --group engineering-workflow"
544
544
  }
545
545
  ],
546
546
  "onFailure": {
@@ -93,7 +93,7 @@ export const WORKFLOW_TRUSTED_SOURCES = deepFreeze({
93
93
  id: 'first-party',
94
94
  type: 'first-party',
95
95
  package: '@1aboveio/skills',
96
- version: '0.10.1',
96
+ version: '0.11.0',
97
97
  },
98
98
  matt: {
99
99
  id: 'matt-pocock',
@@ -236,7 +236,7 @@
236
236
  "id": "first-party",
237
237
  "type": "first-party",
238
238
  "package": "@1aboveio/skills",
239
- "version": "0.10.1"
239
+ "version": "0.11.0"
240
240
  }
241
241
  },
242
242
  {
@@ -247,7 +247,7 @@
247
247
  "id": "first-party",
248
248
  "type": "first-party",
249
249
  "package": "@1aboveio/skills",
250
- "version": "0.10.1"
250
+ "version": "0.11.0"
251
251
  }
252
252
  },
253
253
  {
@@ -258,7 +258,7 @@
258
258
  "id": "first-party",
259
259
  "type": "first-party",
260
260
  "package": "@1aboveio/skills",
261
- "version": "0.10.1"
261
+ "version": "0.11.0"
262
262
  }
263
263
  },
264
264
  {
@@ -269,7 +269,7 @@
269
269
  "id": "first-party",
270
270
  "type": "first-party",
271
271
  "package": "@1aboveio/skills",
272
- "version": "0.10.1"
272
+ "version": "0.11.0"
273
273
  }
274
274
  },
275
275
  {
@@ -280,7 +280,7 @@
280
280
  "id": "first-party",
281
281
  "type": "first-party",
282
282
  "package": "@1aboveio/skills",
283
- "version": "0.10.1"
283
+ "version": "0.11.0"
284
284
  }
285
285
  },
286
286
  {
@@ -291,7 +291,7 @@
291
291
  "id": "first-party",
292
292
  "type": "first-party",
293
293
  "package": "@1aboveio/skills",
294
- "version": "0.10.1"
294
+ "version": "0.11.0"
295
295
  }
296
296
  },
297
297
  {
@@ -302,7 +302,7 @@
302
302
  "id": "first-party",
303
303
  "type": "first-party",
304
304
  "package": "@1aboveio/skills",
305
- "version": "0.10.1"
305
+ "version": "0.11.0"
306
306
  }
307
307
  },
308
308
  {
@@ -313,7 +313,7 @@
313
313
  "id": "first-party",
314
314
  "type": "first-party",
315
315
  "package": "@1aboveio/skills",
316
- "version": "0.10.1"
316
+ "version": "0.11.0"
317
317
  }
318
318
  },
319
319
  {
@@ -324,7 +324,7 @@
324
324
  "id": "first-party",
325
325
  "type": "first-party",
326
326
  "package": "@1aboveio/skills",
327
- "version": "0.10.1"
327
+ "version": "0.11.0"
328
328
  }
329
329
  }
330
330
  ],
@@ -419,7 +419,7 @@
419
419
  "sourceId": "first-party",
420
420
  "sourceType": "first-party",
421
421
  "package": "@1aboveio/skills",
422
- "version": "0.10.1",
422
+ "version": "0.11.0",
423
423
  "installPath": null,
424
424
  "members": [
425
425
  "harness-runtime",
@@ -435,7 +435,7 @@
435
435
  "commands": [
436
436
  {
437
437
  "transport": "npm",
438
- "command": "npx @1aboveio/skills@0.10.1 install --group engineering-workflow"
438
+ "command": "npx @1aboveio/skills@0.11.0 install --group engineering-workflow"
439
439
  }
440
440
  ],
441
441
  "onFailure": {
@@ -3412,7 +3412,7 @@ export const WORKFLOW_PREFLIGHT_COMMANDS = Object.freeze([
3412
3412
 
3413
3413
  const WORKFLOW_VERIFIER_URL = new URL('../../engineering-runtime/scripts/workflow-coherence.mjs', import.meta.url)
3414
3414
  const WORKFLOW_FALLBACK_POLICY_URL = new URL('../generated/workflow-repair-policy.json', import.meta.url)
3415
- export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = '6135740bbbe780364e78b49f2a2ef6ccc3736b9f8b47d04dcc53d927a6fb7153'
3415
+ export const WORKFLOW_TRUSTED_FALLBACK_POLICY_SHA256 = 'b8b3b30d3509ec8c74c4d42b88e53a5958fc17e0066fc18b07fa4b9cd7eb343a'
3416
3416
  const WORKFLOW_REPAIR_RECIPE_REFERENCE = Object.freeze({
3417
3417
  id: 'engineering-workflow-dependency-first',
3418
3418
  generatedFrom: 'skills/distribution/generated/recipes.json',