software-defence-factory 0.15.4 → 0.15.5

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/docs/npm.md CHANGED
@@ -282,3 +282,13 @@ reject tampered selections/images/policies and unsupported future writers, and
282
282
  verify that readback does not rewrite retained evidence. Image adoption is a
283
283
  separate idle operator action; existing attempt images are not migrated by a CLI
284
284
  update. See [the migration/rollback recipe](setup.md#local-context-budget-and-worker-image-0152-100).
285
+
286
+
287
+ The 0.15.5 compatibility audit adds that exact writer to the explicit v1/v2/v3
288
+ allowlists. The examined change is confined to local Pi projection, proactive
289
+ recovery and completion validation. Evidence schemas, frozen commands, model and
290
+ image selections, policy hashes, candidate reconstruction and acceptance guards
291
+ remain unchanged from 0.15.4. Profile tests exercise all phases and inherited,
292
+ cloud, hybrid and local definitions, reject changed selections and future writers,
293
+ and verify immutable readback. This does not upgrade or qualify historical local
294
+ attempts; installed image adoption and actual inference remain separate gates.
package/docs/recovery.md CHANGED
@@ -44,6 +44,22 @@ retry around a retained writer or reuse earlier story evidence for a changed
44
44
  candidate or policy. Browser traces and screenshots are private evidence and
45
45
  are presented through the shared interfaces. See [the web contract](web-verification.md).
46
46
 
47
+ ## Local Pi history exhaustion
48
+
49
+ The 0.15.5 candidate adds bounded proactive recovery for short histories, including
50
+ productive writes whose immutable arguments require fewer retained groups;
51
+ see [limits and image adoption](setup.md#local-context-budget-and-worker-image-0152-100).
52
+ A failed/empty summary, exhausted recovery limit, cancellation or irreducible
53
+ request remains a failed attempt even if a tool already wrote a report. Keep its
54
+ checkout, raw events and private diagnostic evidence. Reconcile the stopped
55
+ worker before admitting a smaller task or explicitly selecting another qualified
56
+ profile as a separate attempt. Do not enlarge the allocation, rewrite a failed
57
+ history or label an explicit cloud retry as local success. Successful transport
58
+ recovery still needs semantic checks and independent Review. The reviewed first
59
+ checkpoint failed real local qualification; the revised artifact needs a new
60
+ lead-owned installed and real-task pass before acceptance. See the
61
+ [rejected trial and source regression](proof.md#short-local-pi-history-recovery-0155-candidate-105).
62
+
47
63
  ## Review feedback or phase retry
48
64
 
49
65
  Use **Request changes** on a failed software review only when its validated
package/docs/setup.md CHANGED
@@ -509,6 +509,40 @@ guessed or silently rewritten. Do not add project Pi settings to bypass
509
509
  the admitted binding or rely on custom tuning that has not been qualified.
510
510
  See [pinned compaction behavior](https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/docs/compaction.md).
511
511
 
512
+ Starting with the **0.15.5 candidate (#105)**, the local adapter also checks
513
+ history pressure at Pi's completed-tool-turn boundary. This conservative byte
514
+ signal counts the system/task/tool envelope once, call arguments, non-tool text
515
+ and capped tool excerpts, separately from provider-reported usage. At 65% of the
516
+ input budget it considers Pi's supported summary generator and append-only
517
+ compaction entry. The newest complete tool group is retained; up to three older
518
+ complete groups can remain as additional evidence when the actual summary and
519
+ retained envelope fit. The original task is restored exactly on every generation;
520
+ raw events and existing session entries are not rewritten. Large-result bursts
521
+ still use upstream compaction and the final request projection. Projection gives
522
+ recent short results (up to 2 KiB each, 8 KiB total and at most a quarter of the
523
+ input budget) priority when they fit.
524
+
525
+ Recovery allows at most 16 proactive summaries per process and two retries per
526
+ summary. It must reduce pressure by at least 4 KiB and leave headroom of 8 KiB
527
+ (or 15% of the input budget when smaller). Its preferred reduction target adapts
528
+ to the immutable latest-group/envelope floor and actual summary size; it cannot
529
+ require that floor to fit an arbitrary fraction of the trigger. The next trigger
530
+ also leaves room for new work. If the latest write/group alone makes recovery
531
+ unproductive, the adapter defers to a later complete turn under the unchanged
532
+ wire guard, when that group can become summary input. This does not permit an
533
+ oversized request through transport. Empty, failed, cancelled or ineffective
534
+ summaries fail the attempt. Oversized instructions, arguments or retained groups
535
+ can still fail closed. These are bounded recovery limits, not automatic
536
+ context/output tuning or a guarantee of useful model work.
537
+
538
+ The selected context, generation output and model remain unchanged; summary
539
+ output is separately capped (6,553 for proactive summaries with 8,192 selected
540
+ output on pinned Pi). `factory_context_recovery` JSONL events record only
541
+ pressure, envelope/floor sizes, group counts and recovery progress, without
542
+ prompts/content. Pi token accounting stays partial; this summary path is not
543
+ added to ordinary assistant usage totals. See
544
+ [source measurements, rejected real trial and pending qualification](proof.md#short-local-pi-history-recovery-0155-candidate-105).
545
+
512
546
  Before adoption, stop and reconcile all work, retain the old immutable image ID
513
547
  and a stopped private-state backup. Build/qualify the exact candidate image, then
514
548
  explicitly select it with `factory install --image LOCAL_IMAGE_REF --state PRIVATE_STATE`
@@ -29,16 +29,18 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
29
29
  // compatibility does not qualify an old local startup or accept a failed attempt.
30
30
  // 0.15.4 adds Pi usage metadata only; protected evidence schemas, frozen
31
31
  // execution selection, isolation and acceptance policy remain unchanged.
32
+ // 0.15.5 changes only local context recovery/projection; v1/v2/v3 schemas,
33
+ // frozen selection hashes, provenance, checks and acceptance guards are unchanged.
32
34
  // Older writers still require exact patch/tree reconstruction before publication.
33
35
  // Deliberately independent of VERSION: a release bump is not
34
36
  // evidence compatibility. Re-audit this list for every trust-relevant writer,
35
37
  // isolation or validation change; remove versions whose guarantees no longer
36
38
  // satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
37
- const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0', '0.13.0', '0.13.1', '0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4']);
39
+ const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0', '0.13.0', '0.13.1', '0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5']);
38
40
 
39
41
  export function isSupportedExecutionProfile(profile) {
40
- if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
41
- : ['0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
42
+ if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
43
+ : ['0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
42
44
  && profile.role === (Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === profile.phase) || null)
43
45
  && (profile.reasoningEffort === null || ['low', 'medium', 'high'].includes(profile.reasoningEffort))
44
46
  && /^[a-f0-9]{64}$/.test(profile.selectionHash || '');
@@ -35,6 +35,16 @@ function excerpt(text, limit) {
35
35
  return Buffer.byteLength(shortened) < Buffer.byteLength(text) ? shortened : text;
36
36
  }
37
37
 
38
+ // A pressure signal, not a token estimate: cap large tool bodies so upstream's
39
+ // existing large-result recovery still owns bursts, but count every call/argument
40
+ // and a useful short excerpt. Provider usage cannot measure this growing floor.
41
+ export function historyPressure(messages, envelope) {
42
+ const copy = structuredClone(messages);
43
+ for (const message of copy) if (message.role === 'toolResult')
44
+ for (const part of message.content || []) if (part.type === 'text') part.text = excerpt(part.text, 1024);
45
+ return envelope + bytes(copy) + 64 * copy.length;
46
+ }
47
+
38
48
  // Uniform cap shares space among large results without penalizing small ones.
39
49
  // Calls, arguments, identities, ordering and all non-tool text are immutable.
40
50
  export function fitToolText(value, budget, measure = bytes, native = false) {
@@ -49,8 +59,27 @@ export function fitToolText(value, budget, measure = bytes, native = false) {
49
59
  for (const part of message.content) if (part.type === 'text') slots.push([part, 'text', part.text]);
50
60
  }
51
61
  }
52
- const cap = limit => { for (const [object, key, text] of slots) object[key] = excerpt(text, limit); };
62
+ // Prefer recent short discoveries within a bounded allowance; large batches
63
+ // still share the remaining space. Prefix stability is measured separately.
64
+ const recent = new Set();
65
+ let recentBytes = 0;
66
+ for (const slot of [...slots].reverse()) {
67
+ const size = bytes(slot[2]);
68
+ if (size <= 2048 && recentBytes + size <= Math.min(8192, budget / 4)) {
69
+ recent.add(slot); recentBytes += size;
70
+ }
71
+ }
72
+ const cap = limit => { for (const slot of slots) {
73
+ const [object, key, text] = slot;
74
+ object[key] = recent.has(slot) ? text : excerpt(text, limit);
75
+ } };
53
76
  cap(0);
77
+ // Release older preferences first if the complete preferred set cannot fit.
78
+ // Even one useful recent result is better than uniformly shortening them all.
79
+ for (const slot of [...recent].reverse()) {
80
+ if (measure(original) <= budget) break;
81
+ recent.delete(slot); cap(0);
82
+ }
54
83
  if (measure(original) > budget) throw new Error('Local Pi context budget cannot fit required instructions, tool pairs and truncation guidance; use smaller task/input or qualify the configured allocation. No fallback.');
55
84
  let low = 0, high = Math.max(0, ...slots.map(([, , text]) => text.length));
56
85
  while (low < high) {
@@ -1,19 +1,87 @@
1
1
  // Explicitly mounted Factory adapter, never discovered from the host/repository.
2
2
  import { readFileSync, writeSync } from 'node:fs';
3
3
  import { openAICompletionsApi } from '@earendil-works/pi-ai';
4
- import { bytes, fitToolText, projectedTokens, restoreOutputBudget, validatePairs } from './pi-context-budget.mjs';
4
+ import { generateSummaryWithUsage } from '@earendil-works/pi-coding-agent';
5
+ import { historyPressure, bytes, fitToolText, projectedTokens, restoreOutputBudget, validatePairs } from './pi-context-budget.mjs';
5
6
 
6
7
  export default function factoryLocalContext(pi) {
7
8
  const registry = JSON.parse(readFileSync(new URL('./models.json', import.meta.url), 'utf8'));
8
9
  const configured = registry.providers['factory-local'].models[0];
9
10
  const window = configured.contextWindow, output = configured.maxTokens;
10
- let task;
11
+ let task, taskEntryId, envelope = 0, recoveries = 0, nextRecovery = 0;
11
12
  // Pi catches extension exceptions and can continue. Budget violations must
12
13
  // stop before transport, even during summarization, with a durable diagnostic.
13
14
  const fatal = error => {
14
15
  writeSync(2, `Factory local Pi context protection failed: ${error.message}\n`);
15
16
  process.exit(1);
16
17
  };
18
+ // Pi 0.87.1's public manual compact() aborts the active run. Use its
19
+ // supported completed-turn compaction draft instead: all tools have finished,
20
+ // and the next request rebuilds the canonical projection from this entry.
21
+ pi.on('turn_end', async (event, ctx) => {
22
+ if (event.outcome !== 'completed' || event.message.stopReason !== 'toolUse') return;
23
+ try {
24
+ const entries = event.context.contextEntries;
25
+ taskEntryId ??= entries.find(entry => entry.messages.some(m => m.role === 'user'))?.sourceEntry.id;
26
+ // Pi includes the system message in its boundary projection. The wire
27
+ // envelope already counts it and the restored task: count each once and
28
+ // use the same basis before/after, so removal cannot fake progress.
29
+ const history = list => list.filter(entry => entry.sourceEntry.id !== taskEntryId)
30
+ .flatMap(entry => entry.messages).filter(message => message.role !== 'system');
31
+ const budget = window - output;
32
+ const threshold = Math.max(Math.floor(budget * 0.65), nextRecovery);
33
+ const pressure = historyPressure(history(entries), envelope);
34
+ if (pressure < threshold) return;
35
+ const starts = entries.flatMap((entry, index) => entry.messages.some(m => m.role === 'assistant'
36
+ && m.content?.some(p => p.type === 'toolCall')) ? [index] : []);
37
+ // The newest complete group is the retention floor, including immutable
38
+ // write arguments. Summarize the older prefix once; after seeing its real
39
+ // size we can retain up to four groups as additional recent evidence.
40
+ // Never split a group or summarize only part of a pending tool batch.
41
+ if (starts.length < 2) return;
42
+ const newest = starts.at(-1);
43
+ const floor = historyPressure(history(entries.slice(newest)), envelope);
44
+ const headroom = Math.min(8192, Math.floor(budget * 0.15));
45
+ // A large current call may leave no useful recovery yet. Let it reach the
46
+ // next ordinary turn under the unchanged wire guard, then it can enter
47
+ // the summarized prefix. Avoid paying for a predictably futile summary.
48
+ if (floor + 1024 > budget - headroom || pressure - floor < 4096) return;
49
+ if (++recoveries > 16) throw new Error('Local Pi proactive recovery limit reached; split the task. No fallback.');
50
+ if (ctx.signal?.aborted) throw new Error('Local Pi proactive recovery cancelled');
51
+ // Pi's summary serializer ignores system messages. Do not charge their
52
+ // bytes against the summary input projection; generation still keeps the
53
+ // exact system envelope. Keep the task and all summarized tool pairs.
54
+ const older = entries.slice(0, newest).flatMap(entry => entry.messages).filter(m => m.role !== 'system');
55
+ writeSync(1, `${JSON.stringify({ type: 'factory_context_recovery', phase: 'start', pressure, threshold, floor, envelope, groups: starts.length, recovery: recoveries })}\n`);
56
+ const input = fitToolText(older, window - output - 8192, bytes, true);
57
+ const result = await generateSummaryWithUsage(input, ctx.model, Math.min(16384, output),
58
+ 'factory-local-keyless', undefined, ctx.signal,
59
+ 'Preserve the task, verified edits, failed attempts, discoveries and remaining work. Do not claim completion without evidence.',
60
+ undefined, ctx.thinkingLevel, streamLocal, undefined, { enabled: true, maxRetries: 2, baseDelayMs: 250 });
61
+ if (ctx.signal?.aborted) throw new Error('Local Pi proactive recovery cancelled');
62
+ if (!result.text?.trim()) throw new Error('Local Pi proactive recovery returned an empty summary');
63
+ // Include framing for Pi's summary message. A fixed percentage cannot be
64
+ // required below the immutable envelope/newest-group floor. Bound both
65
+ // remaining headroom and actual progress instead, using the real summary.
66
+ const summaryBytes = bytes(result.text) + 1024;
67
+ const target = Math.min(budget - headroom, pressure - 4096,
68
+ Math.max(Math.floor(budget * 0.65 * 0.75), floor + summaryBytes));
69
+ let cut, after;
70
+ for (const candidate of starts.slice(-4)) {
71
+ const candidatePressure = historyPressure(history(entries.slice(candidate)), envelope + summaryBytes);
72
+ if (candidatePressure <= target) { cut = candidate; after = candidatePressure; break; }
73
+ }
74
+ if (cut === undefined) throw new Error('Local Pi proactive recovery did not free enough budget; split the task. No fallback.');
75
+ // Re-arm above the measured retained floor, so a productive large write
76
+ // does not cause an immediate repeat summary with nothing new to reclaim.
77
+ nextRecovery = Math.min(budget - Math.floor(headroom / 2), after + 4096);
78
+ writeSync(1, `${JSON.stringify({ type: 'factory_context_recovery', phase: 'complete', pressure: after, target,
79
+ summaryBytes, keptGroups: starts.filter(index => index >= cut).length, recovery: recoveries })}\n`);
80
+ return { entries: [{ type: 'compaction', summary: result.text,
81
+ firstKeptEntryId: entries[cut].sourceEntry.id, usage: result.usage,
82
+ details: { factoryLocalRecovery: 1 } }] };
83
+ } catch (error) { fatal(error); }
84
+ });
17
85
  pi.on('session_before_compact', event => {
18
86
  try {
19
87
  // These are request-local copies. Upstream still selects cut points and
@@ -25,39 +93,39 @@ export default function factoryLocalContext(pi) {
25
93
  preparation[key] = fitToolText(preparation[key], budget, bytes, true);
26
94
  } catch (error) { fatal(error); }
27
95
  });
28
- pi.registerProvider('factory-local', {
29
- api: 'openai-completions',
30
- streamSimple(model, context, options) {
31
- if (model.id !== configured.id || model.provider !== 'factory-local') fatal(new Error('Selected local model changed'));
32
- // Capture before streamSimple/buildBaseOptions clamps against unprojected
33
- // history. Generation defaults to the binding; summaries supply their cap.
34
- const requestedOutput = options?.maxTokens ?? output;
35
- return openAICompletionsApi().streamSimple(model, context, {
36
- ...options,
37
- onPayload: async (payload, requestModel) => {
38
- try {
39
- payload = await options?.onPayload?.(payload, requestModel) ?? payload;
40
- payload = structuredClone(payload);
41
- if (payload.model !== configured.id
42
- || !payload.messages?.some(m => m.role === 'system')) throw new Error('Unsupported local request structure or output budget');
43
- restoreOutputBudget(payload, configured, requestedOutput, options?.reasoning);
44
- if (payload.tools?.length) {
45
- task ??= structuredClone(payload.messages.find(m => m.role === 'user'));
46
- if (!task) throw new Error('Missing required task');
47
- if (!payload.messages.some(m => m.role === 'user' && JSON.stringify(m) === JSON.stringify(task)))
48
- payload.messages.splice(1, 0, structuredClone(task));
49
- validatePairs(payload.messages);
50
- }
51
- if (!payload.messages.some(m => m.role === 'user')) throw new Error('Missing required user message');
52
- // Restore BEFORE measuring so the final output field is counted.
53
- // No request is sent unless projection fits with the full configured
54
- // reserve, even when a summary requests a smaller bounded output.
55
- return fitToolText(payload, window - output, projectedTokens);
56
- } catch (error) { fatal(error); }
57
- },
58
- });
59
- },
60
- });
96
+ function streamLocal(model, context, options) {
97
+ if (model.id !== configured.id || model.provider !== 'factory-local') fatal(new Error('Selected local model changed'));
98
+ // Capture before streamSimple/buildBaseOptions clamps against unprojected
99
+ // history. Generation defaults to the binding; summaries supply their cap.
100
+ const requestedOutput = options?.maxTokens ?? output;
101
+ return openAICompletionsApi().streamSimple(model, context, {
102
+ ...options,
103
+ onPayload: async (payload, requestModel) => {
104
+ try {
105
+ payload = await options?.onPayload?.(payload, requestModel) ?? payload;
106
+ payload = structuredClone(payload);
107
+ if (payload.model !== configured.id
108
+ || !payload.messages?.some(m => m.role === 'system')) throw new Error('Unsupported local request structure or output budget');
109
+ restoreOutputBudget(payload, configured, requestedOutput, options?.reasoning);
110
+ if (payload.tools?.length) {
111
+ task ??= structuredClone(payload.messages.find(m => m.role === 'user'));
112
+ if (!task) throw new Error('Missing required task');
113
+ if (!payload.messages.some(m => m.role === 'user' && JSON.stringify(m) === JSON.stringify(task)))
114
+ payload.messages.splice(1, 0, structuredClone(task));
115
+ validatePairs(payload.messages);
116
+ envelope = bytes(payload.tools) + bytes(payload.messages.filter(m => m.role === 'system'))
117
+ + bytes(task) + 4096;
118
+ }
119
+ if (!payload.messages.some(m => m.role === 'user')) throw new Error('Missing required user message');
120
+ // Restore BEFORE measuring so the final output field is counted.
121
+ // No request is sent unless projection fits with the full configured
122
+ // reserve, even when a summary requests a smaller bounded output.
123
+ return fitToolText(payload, window - output, projectedTokens);
124
+ } catch (error) { fatal(error); }
125
+ },
126
+ });
127
+ }
128
+ pi.registerProvider('factory-local', { api: 'openai-completions', streamSimple: streamLocal });
61
129
  // Launcher requires this handshake; an extension load failure cannot bypass it.
62
130
  writeSync(1, `${JSON.stringify({ type: 'factory_context_ready', version: 1 })}\n`);
63
131
  }
@@ -24,6 +24,8 @@ export class PiCompletion {
24
24
  settled = false;
25
25
  lastAssistant = null;
26
26
  contextReady = false;
27
+ proactiveRecovery = false;
28
+ proactiveCount = 0;
27
29
 
28
30
  write(chunk) {
29
31
  this.pending += chunk;
@@ -42,6 +44,17 @@ export class PiCompletion {
42
44
  event(value) {
43
45
  if (!value || typeof value.type !== 'string') { this.failed = true; return; }
44
46
  if (value.type === 'factory_context_ready' && value.version === 1) this.contextReady = true;
47
+ if (value.type === 'factory_context_recovery') {
48
+ if (value.phase === 'start') {
49
+ if (this.proactiveRecovery || value.recovery !== this.proactiveCount + 1 || value.recovery > 16) this.failed = true;
50
+ this.proactiveCount = value.recovery;
51
+ this.proactiveRecovery = true;
52
+ this.settled = false;
53
+ } else if (value.phase === 'complete') {
54
+ if (!this.proactiveRecovery || value.recovery !== this.proactiveCount) this.failed = true;
55
+ this.proactiveRecovery = false;
56
+ } else this.failed = true;
57
+ }
45
58
  if (value.type === 'agent_start' || value.type === 'message_start') {
46
59
  this.completed = false; this.settled = false;
47
60
  }
@@ -95,7 +108,7 @@ export class PiCompletion {
95
108
 
96
109
  finish() {
97
110
  return !this.pending && !this.failed && !this.error && !this.retry && !this.compacting && !this.recovery
98
- && this.completed && (!this.modern || this.settled);
111
+ && !this.proactiveRecovery && this.completed && (!this.modern || this.settled);
99
112
  }
100
113
  }
101
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "software-defence-factory",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "Factory: scoped software delivery and defence investigations with isolated jobs, evidence and review",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,7 +58,7 @@
58
58
  "check": "node scripts/check.mjs && node --test tests/*.test.mjs && npm run test:dashboard",
59
59
  "build:dashboard": "npm --prefix dashboard ci --ignore-scripts && npm --prefix dashboard run build",
60
60
  "test:dashboard": "npm --prefix dashboard test",
61
- "qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-trust.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs"
61
+ "qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-trust.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs tests/pi-context-history.test.mjs tests/pi-context-productive.test.mjs"
62
62
  },
63
63
  "dependencies": {
64
64
  "yaml": "2.9.1"