software-defence-factory 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/definition.md +14 -3
- package/docs/npm.md +45 -4
- package/docs/setup.md +72 -0
- package/factory/execution-profile.mjs +7 -4
- package/factory/executor.mjs +2 -0
- package/factory/image/Dockerfile +1 -1
- package/factory/pi-context-budget.mjs +75 -0
- package/factory/pi-context-extension.mjs +64 -0
- package/factory/pi-local-launch.mjs +141 -46
- package/factory/pins.json +1 -1
- package/factory/updates.mjs +21 -5
- package/package.json +3 -2
package/docs/definition.md
CHANGED
|
@@ -255,7 +255,7 @@ The binding contract supports at most 16 named, keyless HTTP(S) endpoints. URLs
|
|
|
255
255
|
with user information, queries, fragments, escapes or malformed syntax are refused.
|
|
256
256
|
Model identifiers cannot contain shell syntax, whitespace or traversal. Context
|
|
257
257
|
must be 1,024–1,048,576 tokens; output must be 1–32,000 and smaller than context.
|
|
258
|
-
The output ceiling
|
|
258
|
+
The output ceiling remains 32,000 with the pinned Pi 0.87.1 chat adapter. Range acceptance
|
|
259
259
|
is **not** evidence of server allocation or useful task capacity. Only the three
|
|
260
260
|
shown compatibility fields are supported. Authentication, custom headers, shell
|
|
261
261
|
credential commands, arbitrary request fields, sampling overrides, custom thinking
|
|
@@ -271,7 +271,7 @@ The optional binding `reasoningEffort` accepts only these OpenAI-style requests:
|
|
|
271
271
|
| `low`, `medium`, `high` | `reasoning_effort` set to that exact value |
|
|
272
272
|
|
|
273
273
|
This is a request choice, not a measured thinking budget or quality claim. The
|
|
274
|
-
pinned Pi 0.
|
|
274
|
+
pinned Pi 0.87.1 adapter uses a fixed internal `thinkingLevelMap.off="none"` for
|
|
275
275
|
`none`; `--thinking off` alone does **not** disable a server's default thinking.
|
|
276
276
|
Low/medium/high use the corresponding explicit Pi thinking level. Existing
|
|
277
277
|
bindings with no choice retain their omitted request and private policy shape;
|
|
@@ -295,7 +295,12 @@ For the selected local role only, the executor creates a private single-model
|
|
|
295
295
|
`models.json` and deterministic launcher, mounted read-only at `/factory-local`.
|
|
296
296
|
`PI_CODING_AGENT_DIR` selects that directory; HOME is still ephemeral. The launcher
|
|
297
297
|
checks explicit provider/model argv, removes inherited credential/override inputs
|
|
298
|
-
and
|
|
298
|
+
and requires a completed JSON turn even if Pi exits zero. Explicit successful
|
|
299
|
+
retry/overflow recovery can clear an assistant error; failed/aborted compaction,
|
|
300
|
+
exhausted retries, malformed/truncated JSONL and missing completion remain failures.
|
|
301
|
+
The original event stream is retained in the bounded private log. The launcher also
|
|
302
|
+
passes the fixed placeholder as Pi's supported runtime `--api-key`, avoiding a
|
|
303
|
+
credential-store refresh against the read-only registry. No credentials are added. The registry uses
|
|
299
304
|
Pi's required fixed non-secret key placeholder (`factory-local-keyless`), which may
|
|
300
305
|
be sent as a bearer value; this is not endpoint authentication. No host home, provider
|
|
301
306
|
catalog or credential store is mounted. Repository files cannot replace this mount.
|
|
@@ -308,3 +313,9 @@ automatic skills/extensions/templates but explicitly adds `--skill /factory-skil
|
|
|
308
313
|
preserving the six packaged skills. Recommendations remain guidance, not per-role
|
|
309
314
|
access controls. Build still returns to Factory's independent Review and operator
|
|
310
315
|
handoff; no duplicate review workflow is launched.
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
For long tool-use runs, use the [context-boundary qualification and image migration
|
|
319
|
+
recipe](setup.md#local-context-budget-and-worker-image-0152-100). Declared context
|
|
320
|
+
and output budgets are the same shared binding fields in CLI, API and dashboard;
|
|
321
|
+
there are no Factory compaction knobs or automatic larger-window/model choices.
|
package/docs/npm.md
CHANGED
|
@@ -94,6 +94,18 @@ jobs and history are preserved. Existing processes keep their original code.
|
|
|
94
94
|
Set `SDF_AUTO_UPDATE=0` to skip automatic network checks for a command.
|
|
95
95
|
Source checkouts remain managed by Git and do not update themselves.
|
|
96
96
|
|
|
97
|
+
The exact selected release is downloaded with npm's `--prefer-online` metadata
|
|
98
|
+
refresh and `--prefer-offline=false`; this does not change account settings.
|
|
99
|
+
An `ETARGET` visibility failure
|
|
100
|
+
gets at most three attempts, separated by one second, within the same total
|
|
101
|
+
120-second download budget (including waits). Offline, authentication and package
|
|
102
|
+
identity errors are not retried by Factory. A successful download must match the
|
|
103
|
+
requested package name, version and CLI entry before immutable adoption. Exhaustion
|
|
104
|
+
cleans only that download's staging directory and retains the selected runtime,
|
|
105
|
+
configuration and job history. Managed updates release their idle reservations on
|
|
106
|
+
download failure; normal reservation/recovery rules in [services](services.md)
|
|
107
|
+
still apply. The retries add no extra download maintenance budget.
|
|
108
|
+
|
|
97
109
|
CLI updates do not rebuild or update Docker images automatically. Stop the
|
|
98
110
|
installation and run `install --state PATH` when intentionally adopting a new
|
|
99
111
|
runtime image; repeat the relevant qualification before resuming jobs. Install retains the old and new
|
|
@@ -104,8 +116,8 @@ installation or retained attempt needs them.
|
|
|
104
116
|
|
|
105
117
|
## Protected evidence compatibility
|
|
106
118
|
|
|
107
|
-
Factory 0.15.
|
|
108
|
-
**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 and 0.15.
|
|
119
|
+
Factory 0.15.2 recognizes version-1 execution profiles emitted by native
|
|
120
|
+
**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 and 0.15.2**. This is an exact allowlist in
|
|
109
121
|
`factory/execution-profile.mjs`, independent of the installed package version;
|
|
110
122
|
it is not a semver range or an automatic promise for later releases. Unknown
|
|
111
123
|
runtime strings, unknown profile formats and incomplete legacy acceptance
|
|
@@ -172,7 +184,7 @@ Verify evidence and cannot authorize acceptance or publication.
|
|
|
172
184
|
|
|
173
185
|
Version 0.14.0 preserves the v1 writer only for unchanged inherited installations:
|
|
174
186
|
policy bytes, candidate/check/review/acceptance bindings, mounts and credential
|
|
175
|
-
rules stay compatible. Adopted role overrides use **v2 from 0.14.0 and 0.15.
|
|
187
|
+
rules stay compatible. Adopted role overrides use **v2 from 0.14.0, 0.15.0, 0.15.1 and 0.15.2**, recording
|
|
176
188
|
role/provider/effort and an exact selection digest. The executor verifies the
|
|
177
189
|
frozen common configuration before phase selection; continuation and both delivery
|
|
178
190
|
validation paths require matching protected v2 evidence and private frozen config.
|
|
@@ -199,6 +211,15 @@ publishes an increased `package.json` version. Existing versions are skipped;
|
|
|
199
211
|
registry errors fail the release instead of masquerading as a missing version.
|
|
200
212
|
The workflow can also be started manually from `main`.
|
|
201
213
|
|
|
214
|
+
An accepted npm publication may still be processing. It is not yet a verified,
|
|
215
|
+
downloadable release: exact-version and `latest` metadata can appear at different
|
|
216
|
+
times, and an npm install can briefly report `ETARGET` even after both appear.
|
|
217
|
+
Verify an exact package download and compare its contents with the reviewed
|
|
218
|
+
artifact before reporting release availability. If the bounded updater reports
|
|
219
|
+
that the release is not yet downloadable, retain the prior runtime and retry the
|
|
220
|
+
update later. This does not establish whether a particular delay came from a
|
|
221
|
+
local cache or registry propagation.
|
|
222
|
+
|
|
202
223
|
For a release, update both manifests with `npm version patch --no-git-tag-version`,
|
|
203
224
|
review the change, and push through the project's normal review flow. A code
|
|
204
225
|
push without a version bump is tested but does not overwrite a published package.
|
|
@@ -233,7 +254,7 @@ The 0.15.0 compatibility audit preserves unchanged v1/v2 policy construction,
|
|
|
233
254
|
protected checks, candidate reconstruction, read-only review and acceptance guards.
|
|
234
255
|
Historical 0.14.0 v2 validation compares its actual writer version rather than
|
|
235
256
|
relabeling it as the installed version. Local registry selection is a new execution
|
|
236
|
-
contract: every phase of a local/hybrid attempt requires **v3 from 0.15.0** and its
|
|
257
|
+
contract: every phase of a local/hybrid attempt requires **v3 from 0.15.0, 0.15.1 or 0.15.2** and its
|
|
237
258
|
exact protected frozen configuration. v1/v2 cannot attest local binding fields,
|
|
238
259
|
providers or a common policy containing a selected local role. A selected endpoint,
|
|
239
260
|
model, limit or compatibility change invalidates the current policy; explicit
|
|
@@ -241,3 +262,23 @@ rollback can restore it without editing historical evidence. Controlled regressi
|
|
|
241
262
|
exercise legacy v2 readback and local v3 rejection/tamper cases. Installed-package
|
|
242
263
|
Docker and independent Review remain delivery qualification, not inferred from this
|
|
243
264
|
compatibility audit.
|
|
265
|
+
|
|
266
|
+
The 0.15.1 patch changes release downloads and preserves the 0.15.0 v1/v2/v3
|
|
267
|
+
writers, frozen configuration and policy bytes. The explicit allowlists include
|
|
268
|
+
both patch versions; supported v3 readback retains the original writer version
|
|
269
|
+
while still comparing every execution selection, phase and policy field. It does
|
|
270
|
+
not rewrite historical evidence or allow v1/v2 to attest local bindings.
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
The 0.15.2 compatibility audit adds that exact writer to the v1/v2/v3 allowlists.
|
|
274
|
+
It changes the local launcher's interpretation of explicit recovery/completion
|
|
275
|
+
and the standard image recipe, without changing the admitted command, binding
|
|
276
|
+
schema, frozen config/image/selection/policy hashes, protected checks, candidate
|
|
277
|
+
reconstruction or independent review/acceptance guards. Historical 0.15.0 and
|
|
278
|
+
0.15.1 evidence remains readable with its original bytes and writer version;
|
|
279
|
+
readability does not qualify those images for long local tasks. Regression cases
|
|
280
|
+
exercise inherited, cloud-role, hybrid and local profiles through all phases,
|
|
281
|
+
reject tampered selections/images/policies and unsupported future writers, and
|
|
282
|
+
verify that readback does not rewrite retained evidence. Image adoption is a
|
|
283
|
+
separate idle operator action; existing attempt images are not migrated by a CLI
|
|
284
|
+
update. See [the migration/rollback recipe](setup.md#local-context-budget-and-worker-image-0152-100).
|
package/docs/setup.md
CHANGED
|
@@ -430,3 +430,75 @@ leaves Pi usage unknown; controlled protocol fixtures are not token or quality
|
|
|
430
430
|
benchmarks. Include cloud Review in hybrid totals. No provider token billing for
|
|
431
431
|
local inference does not mean zero electricity, hardware or operator cost. Only
|
|
432
432
|
then qualify a suitable real Factory issue; no customer jobs are part of this slice.
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
## Local context budget and worker image (0.15.2, #100)
|
|
436
|
+
|
|
437
|
+
The standard worker recipe pins **`@earendil-works/pi-coding-agent@0.87.1`** from
|
|
438
|
+
[earendil-works/pi](https://github.com/earendil-works/pi/tree/v0.87.1), replacing
|
|
439
|
+
`@mariozechner/pi-coding-agent@0.73.1`. Pi requires **Node >=22.19.0**; the image
|
|
440
|
+
installation enforces dependency engines. Factory's controller Node requirement
|
|
441
|
+
is unchanged. The separate Codex pin remains **0.156.1**. A CLI upgrade does not
|
|
442
|
+
rebuild or replace an admitted job image. Source transport proof is documented in
|
|
443
|
+
[proof](proof.md#pi-context-boundary-first-slice-0152-100); installed image and real
|
|
444
|
+
local-model qualification are separate acceptance gates.
|
|
445
|
+
|
|
446
|
+
Set the selected private binding's `contextWindow` no higher than the server's
|
|
447
|
+
**actually allocated** context for that exact model/request, and set `maxTokens`
|
|
448
|
+
within both endpoint support and available headroom. These existing fields have
|
|
449
|
+
the same CLI/API/dashboard validation and frozen policy. An advertised registry
|
|
450
|
+
window allocates no server memory. For Ollama, configure allocation through its
|
|
451
|
+
supported server/model controls and inspect the running allocation with
|
|
452
|
+
`ollama ps`; see [Ollama context length](https://docs.ollama.com/context-length).
|
|
453
|
+
Factory does not pass an undocumented `num_ctx`, patch a model template or tune
|
|
454
|
+
the host. Unsupported endpoint controls require an explicit capability limit.
|
|
455
|
+
|
|
456
|
+
Pi 0.87.1's upstream defaults reserve 16,384 tokens and keep approximately 20,000
|
|
457
|
+
recent tokens. At a declared 65,536, its threshold is 49,152, checked **after tool
|
|
458
|
+
results and before the next assistant request in the same run**. This repairs a
|
|
459
|
+
missing check in 0.73.1; increasing its reserve alone cannot add that check. Output
|
|
460
|
+
reservation, tool-result growth, system/tools overhead and summarization must all
|
|
461
|
+
fit the allocation. Prefix compaction alone cannot shrink one large retained
|
|
462
|
+
assistant/tool-result group. The bundled local adapter therefore projects tool
|
|
463
|
+
text into a conservative byte budget before generation and summarization, keeping
|
|
464
|
+
the exact task and tool identities/order. Truncation notices tell the model to
|
|
465
|
+
re-read with offset/limit or narrower queries; raw events/files stay independent
|
|
466
|
+
of this projection. Every request reserves the full configured output, includes
|
|
467
|
+
schemas/arguments and adds template headroom. This is conservative estimation,
|
|
468
|
+
not measured token usage or an allocation/completion guarantee. Unshrinkable
|
|
469
|
+
instructions/arguments fail explicitly rather than being silently removed.
|
|
470
|
+
Pi estimates native history before the wire projection and can reduce output to
|
|
471
|
+
one token. The adapter restores the selected generation allowance only with a
|
|
472
|
+
fitting final payload; explicit upstream summary caps stay bounded separately.
|
|
473
|
+
Both admitted `max_tokens` and `max_completion_tokens` compatibility fields are
|
|
474
|
+
supported. The existing OpenAI `reasoning_effort` selection remains inside that
|
|
475
|
+
total output ceiling; no extra reasoning allowance is added. Numeric/nested
|
|
476
|
+
thinking-budget formats are unsupported and fail explicitly instead of being
|
|
477
|
+
guessed or silently rewritten. Do not add project Pi settings to bypass
|
|
478
|
+
the admitted binding or rely on custom tuning that has not been qualified.
|
|
479
|
+
See [pinned compaction behavior](https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/docs/compaction.md).
|
|
480
|
+
|
|
481
|
+
Before adoption, stop and reconcile all work, retain the old immutable image ID
|
|
482
|
+
and a stopped private-state backup. Build/qualify the exact candidate image, then
|
|
483
|
+
explicitly select it with `factory install --image LOCAL_IMAGE_REF --state PRIVATE_STATE`
|
|
484
|
+
while idle. The normal install path retains the previously selected image. Plain
|
|
485
|
+
`factory install` builds the standard recipe, now tagged `software-defence-factory-job:0.4.0`;
|
|
486
|
+
updating the CLI alone does neither. Confirm the selected image ID and `pi --version`
|
|
487
|
+
in that image. The protected local launcher requires exactly 0.87.1 and loads only
|
|
488
|
+
its bundled read-only adapter with automatic extension discovery disabled. Missing
|
|
489
|
+
adapter files or an older worker image fail explicitly; old evidence remains
|
|
490
|
+
readable and cloud profiles keep their existing wrapper. Roll back with the retained old image through the same idle install
|
|
491
|
+
path and restore the previously working role definition through revision-guarded
|
|
492
|
+
rollback. Do not change frozen attempts or relabel old failed results.
|
|
493
|
+
|
|
494
|
+
Lead's bounded actual-inference fixture must include enough tool results to trigger
|
|
495
|
+
summarization, resumed useful tool work, the required report, checks and independent
|
|
496
|
+
Review. Record the exact image/model digest, harness/server versions, allocated
|
|
497
|
+
window, output/reasoning request, tool growth, compaction ordering, elapsed time,
|
|
498
|
+
quality outcome and peak memory; unknown measurements remain null. Set an explicit
|
|
499
|
+
job deadline/resource budget and preserve any unfinished checkout and diagnostic
|
|
500
|
+
artifacts. A short successful prompt is insufficient. A larger actually allocated
|
|
501
|
+
window is a later measured comparison only if this evidence warrants it; do not
|
|
502
|
+
switch model or cloud provider automatically. The failed #83 local attempt is not
|
|
503
|
+
local delivery. An operator-selected fresh cloud retry and unfinished-checkpoint
|
|
504
|
+
recovery remain governed by [recovery](recovery.md) and #42.
|
|
@@ -21,16 +21,19 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
|
21
21
|
// selection digest. V1 can never attest an adopted role override.
|
|
22
22
|
// 0.15.0 keeps unchanged v1/v2 policies and writers; local selections require
|
|
23
23
|
// v3 for every phase. Legacy evidence cannot attest the new registry/launcher.
|
|
24
|
+
// 0.15.1 changes release downloads only; v1/v2/v3 writers and policy are unchanged.
|
|
25
|
+
// 0.15.2 updates the opt-in local launcher and standard image; schemas, frozen
|
|
26
|
+
// command/config/image hashes, protected checks and acceptance guards are unchanged.
|
|
24
27
|
// Older writers still require exact patch/tree reconstruction before publication.
|
|
25
28
|
// Deliberately independent of VERSION: a release bump is not
|
|
26
29
|
// evidence compatibility. Re-audit this list for every trust-relevant writer,
|
|
27
30
|
// isolation or validation change; remove versions whose guarantees no longer
|
|
28
31
|
// satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
|
|
29
|
-
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']);
|
|
32
|
+
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']);
|
|
30
33
|
|
|
31
34
|
export function isSupportedExecutionProfile(profile) {
|
|
32
|
-
if ([2, 3].includes(profile?.version)) return (profile.version === 3 ?
|
|
33
|
-
: ['0.14.0', '0.15.0'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
|
|
35
|
+
if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
|
|
36
|
+
: ['0.14.0', '0.15.0', '0.15.1', '0.15.2'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
|
|
34
37
|
&& profile.role === (Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === profile.phase) || null)
|
|
35
38
|
&& (profile.reasoningEffort === null || ['low', 'medium', 'high'].includes(profile.reasoningEffort))
|
|
36
39
|
&& /^[a-f0-9]{64}$/.test(profile.selectionHash || '');
|
|
@@ -132,7 +135,7 @@ export function assertFrozenExecution(config, execution, phase) {
|
|
|
132
135
|
delete expected.hostName;
|
|
133
136
|
// Audited legacy writers retain their real version. Never let v1/v2 attest
|
|
134
137
|
// a local binding, including a deterministic phase of a hybrid attempt.
|
|
135
|
-
if (
|
|
138
|
+
if (isSupportedExecutionProfile(execution) && execution.version === expected.version)
|
|
136
139
|
expected.runtimeVersion = execution.runtimeVersion;
|
|
137
140
|
const actual = { ...execution }; delete actual.hostName; delete actual.workerName;
|
|
138
141
|
if (!isDeepStrictEqual(expected, actual)) throw new Error('Admitted execution profile does not match this attempt');
|
package/factory/executor.mjs
CHANGED
|
@@ -110,6 +110,8 @@ async function container(mode, input, command, options = {}) {
|
|
|
110
110
|
mkdirSync(localDirectory, { mode: 0o700 });
|
|
111
111
|
const { reference, ...binding } = config.localBinding;
|
|
112
112
|
writeFileSync(join(localDirectory, 'launch.mjs'), readFileSync(join(ROOT, 'factory/pi-local-launch.mjs')), { mode: 0o600, flag: 'wx' });
|
|
113
|
+
for (const file of ['pi-context-extension.mjs', 'pi-context-budget.mjs'])
|
|
114
|
+
writeFileSync(join(localDirectory, file), readFileSync(join(ROOT, 'factory', file)), { mode: 0o600, flag: 'wx' });
|
|
113
115
|
writeFileSync(join(localDirectory, 'models.json'), JSON.stringify(localRegistry(binding)), { mode: 0o600, flag: 'wx' });
|
|
114
116
|
args.push('--mount', `type=bind,source=${localDirectory},target=${LOCAL_AGENT_DIR},readonly`,
|
|
115
117
|
'--env', `PI_CODING_AGENT_DIR=${LOCAL_AGENT_DIR}`);
|
package/factory/image/Dockerfile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
FROM node:22-bookworm-slim@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c
|
|
2
2
|
RUN apt-get update && apt-get install -y --no-install-recommends git python3 python3-venv ca-certificates procps \
|
|
3
3
|
&& rm -rf /var/lib/apt/lists/* \
|
|
4
|
-
&& npm install --global @openai/codex@0.156.1 @
|
|
4
|
+
&& npm install --global --engine-strict @openai/codex@0.156.1 @earendil-works/pi-coding-agent@0.87.1 \
|
|
5
5
|
&& npm cache clean --force
|
|
6
6
|
COPY mock.mjs /opt/factory/mock.mjs
|
|
7
7
|
USER node
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Request projection only. Never change raw tool events, files or session entries.
|
|
2
|
+
// UTF-8 bytes (not characters/4) conservatively cover text tokenization; explicit
|
|
3
|
+
// framing headroom also covers chat templates. This is a budget, not usage.
|
|
4
|
+
export const bytes = value => Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
5
|
+
export const projectedTokens = payload => bytes(payload) + 1024
|
|
6
|
+
+ 64 * ((payload.messages?.length || 0) + (payload.tools?.length || 0));
|
|
7
|
+
|
|
8
|
+
// Pi 0.87.1 clamps against native history BEFORE onPayload. Recover the caller's
|
|
9
|
+
// allowance, not that obsolete clamp. Explicit summary caps remain explicit.
|
|
10
|
+
// Only the selected adapter's OpenAI effort control is supported: numeric or
|
|
11
|
+
// nested thinking budgets could also have been clamped and must not be guessed.
|
|
12
|
+
export function restoreOutputBudget(payload, configured, requested, reasoning) {
|
|
13
|
+
const field = configured.compat.maxTokensField;
|
|
14
|
+
const fields = ['max_tokens', 'max_completion_tokens'].filter(key => Object.hasOwn(payload, key));
|
|
15
|
+
if (!Number.isSafeInteger(requested) || requested < 1 || requested > configured.maxTokens
|
|
16
|
+
|| fields.length !== 1 || fields[0] !== field
|
|
17
|
+
|| !Number.isSafeInteger(payload[field]) || payload[field] < 1 || payload[field] > requested)
|
|
18
|
+
throw new Error('Unsupported local output budget; refusing to override selected limits');
|
|
19
|
+
if ((reasoning !== undefined && !['off', 'low', 'medium', 'high'].includes(reasoning))
|
|
20
|
+
|| ['reasoning', 'thinking', 'thinking_token_budget', 'reasoning_budget', 'chat_template_kwargs', 'chat_template_args', 'enable_thinking']
|
|
21
|
+
.some(key => Object.hasOwn(payload, key)))
|
|
22
|
+
throw new Error('Unsupported local reasoning budget; qualify the endpoint controls before use');
|
|
23
|
+
const effort = configured.reasoning
|
|
24
|
+
? (!reasoning || reasoning === 'off' ? configured.thinkingLevelMap?.off : reasoning) : undefined;
|
|
25
|
+
if (payload.reasoning_effort !== effort)
|
|
26
|
+
throw new Error('Local reasoning effort does not match the selected request');
|
|
27
|
+
payload[field] = requested;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function excerpt(text, limit) {
|
|
31
|
+
if (text.length <= limit) return text;
|
|
32
|
+
// Do not split a UTF-16 surrogate pair. JSON escaping is counted afterward.
|
|
33
|
+
const prefix = text.slice(0, limit).replace(/[\uD800-\uDBFF]$/, '');
|
|
34
|
+
const shortened = `${prefix}\n[Factory context projection: shortened tool result (${Buffer.byteLength(text, 'utf8')} original UTF-8 bytes). Raw tool event is unchanged; source files are unchanged by projection. Do not infer omitted contents. Re-read the original path with read offset/limit (start with limit: 40), or run a narrower query; keep subsequent batches small.]`;
|
|
35
|
+
return Buffer.byteLength(shortened) < Buffer.byteLength(text) ? shortened : text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Uniform cap shares space among large results without penalizing small ones.
|
|
39
|
+
// Calls, arguments, identities, ordering and all non-tool text are immutable.
|
|
40
|
+
export function fitToolText(value, budget, measure = bytes, native = false) {
|
|
41
|
+
const original = structuredClone(value);
|
|
42
|
+
if (measure(original) <= budget) return original;
|
|
43
|
+
const messages = Array.isArray(original) ? original : original.messages;
|
|
44
|
+
const slots = [];
|
|
45
|
+
for (const message of messages) {
|
|
46
|
+
if (message.role !== (native ? 'toolResult' : 'tool')) continue;
|
|
47
|
+
if (typeof message.content === 'string') slots.push([message, 'content', message.content]);
|
|
48
|
+
else if (Array.isArray(message.content)) {
|
|
49
|
+
for (const part of message.content) if (part.type === 'text') slots.push([part, 'text', part.text]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const cap = limit => { for (const [object, key, text] of slots) object[key] = excerpt(text, limit); };
|
|
53
|
+
cap(0);
|
|
54
|
+
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
|
+
let low = 0, high = Math.max(0, ...slots.map(([, , text]) => text.length));
|
|
56
|
+
while (low < high) {
|
|
57
|
+
const mid = Math.ceil((low + high) / 2); cap(mid);
|
|
58
|
+
if (measure(original) <= budget) low = mid; else high = mid - 1;
|
|
59
|
+
}
|
|
60
|
+
cap(low);
|
|
61
|
+
return original;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validatePairs(messages) {
|
|
65
|
+
const pending = new Set(), seen = new Set();
|
|
66
|
+
for (const message of messages) {
|
|
67
|
+
if (message.role !== 'tool' && pending.size) throw new Error('Local Pi context has interrupted tool pairs');
|
|
68
|
+
for (const call of message.tool_calls || []) {
|
|
69
|
+
if (!call.id || seen.has(call.id)) throw new Error('Local Pi context has duplicate tool identity');
|
|
70
|
+
pending.add(call.id); seen.add(call.id);
|
|
71
|
+
}
|
|
72
|
+
if (message.role === 'tool' && !pending.delete(message.tool_call_id)) throw new Error('Local Pi context has orphan tool result');
|
|
73
|
+
}
|
|
74
|
+
if (pending.size) throw new Error('Local Pi context has missing tool results');
|
|
75
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Explicitly mounted Factory adapter, never discovered from the host/repository.
|
|
2
|
+
import { readFileSync, writeSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { openAICompletionsApi } from '@earendil-works/pi-ai';
|
|
5
|
+
import { bytes, fitToolText, projectedTokens, restoreOutputBudget, validatePairs } from './pi-context-budget.mjs';
|
|
6
|
+
|
|
7
|
+
export default function factoryLocalContext(pi) {
|
|
8
|
+
const registry = JSON.parse(readFileSync(join(process.env.PI_CODING_AGENT_DIR, 'models.json'), 'utf8'));
|
|
9
|
+
const configured = registry.providers['factory-local'].models[0];
|
|
10
|
+
const window = configured.contextWindow, output = configured.maxTokens;
|
|
11
|
+
let task;
|
|
12
|
+
// Pi catches extension exceptions and can continue. Budget violations must
|
|
13
|
+
// stop before transport, even during summarization, with a durable diagnostic.
|
|
14
|
+
const fatal = error => {
|
|
15
|
+
writeSync(2, `Factory local Pi context protection failed: ${error.message}\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
};
|
|
18
|
+
pi.on('session_before_compact', event => {
|
|
19
|
+
try {
|
|
20
|
+
// These are request-local copies. Upstream still selects cut points and
|
|
21
|
+
// generates the summary. Reserve space for its pinned summary templates;
|
|
22
|
+
// the provider wrapper checks the exact serialized request as well.
|
|
23
|
+
const preparation = event.preparation;
|
|
24
|
+
const budget = window - output - 8192 - bytes(preparation.previousSummary || '');
|
|
25
|
+
for (const key of ['messagesToSummarize', 'turnPrefixMessages'])
|
|
26
|
+
preparation[key] = fitToolText(preparation[key], budget, bytes, true);
|
|
27
|
+
} catch (error) { fatal(error); }
|
|
28
|
+
});
|
|
29
|
+
pi.registerProvider('factory-local', {
|
|
30
|
+
api: 'openai-completions',
|
|
31
|
+
streamSimple(model, context, options) {
|
|
32
|
+
if (model.id !== configured.id || model.provider !== 'factory-local') fatal(new Error('Selected local model changed'));
|
|
33
|
+
// Capture before streamSimple/buildBaseOptions clamps against unprojected
|
|
34
|
+
// history. Generation defaults to the binding; summaries supply their cap.
|
|
35
|
+
const requestedOutput = options?.maxTokens ?? output;
|
|
36
|
+
return openAICompletionsApi().streamSimple(model, context, {
|
|
37
|
+
...options,
|
|
38
|
+
onPayload: async (payload, requestModel) => {
|
|
39
|
+
try {
|
|
40
|
+
payload = await options?.onPayload?.(payload, requestModel) ?? payload;
|
|
41
|
+
payload = structuredClone(payload);
|
|
42
|
+
if (payload.model !== configured.id
|
|
43
|
+
|| !payload.messages?.some(m => m.role === 'system')) throw new Error('Unsupported local request structure or output budget');
|
|
44
|
+
restoreOutputBudget(payload, configured, requestedOutput, options?.reasoning);
|
|
45
|
+
if (payload.tools?.length) {
|
|
46
|
+
task ??= structuredClone(payload.messages.find(m => m.role === 'user'));
|
|
47
|
+
if (!task) throw new Error('Missing required task');
|
|
48
|
+
if (!payload.messages.some(m => m.role === 'user' && JSON.stringify(m) === JSON.stringify(task)))
|
|
49
|
+
payload.messages.splice(1, 0, structuredClone(task));
|
|
50
|
+
validatePairs(payload.messages);
|
|
51
|
+
}
|
|
52
|
+
if (!payload.messages.some(m => m.role === 'user')) throw new Error('Missing required user message');
|
|
53
|
+
// Restore BEFORE measuring so the final output field is counted.
|
|
54
|
+
// No request is sent unless projection fits with the full configured
|
|
55
|
+
// reserve, even when a summary requests a smaller bounded output.
|
|
56
|
+
return fitToolText(payload, window - output, projectedTokens);
|
|
57
|
+
} catch (error) { fatal(error); }
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
// Launcher requires this handshake; an extension load failure cannot bypass it.
|
|
63
|
+
writeSync(1, `${JSON.stringify({ type: 'factory_context_ready', version: 1 })}\n`);
|
|
64
|
+
}
|
|
@@ -1,48 +1,143 @@
|
|
|
1
|
-
// Mounted read-only beside the single selected registry.
|
|
2
|
-
//
|
|
1
|
+
// Mounted read-only beside the single selected registry. Keep all JSON evidence;
|
|
2
|
+
// process exit alone does not establish a successfully completed Pi turn.
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
// A long run can emit a multi-megabyte agent_end aggregate even after compaction.
|
|
9
|
+
// Bound parsing separately from the much smaller retained diagnostic log.
|
|
10
|
+
const MAX_EVENT_CHARACTERS = 8 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
// Pi 0.73.1 and 0.87.1 print JSONL. Errors can precede explicit recovery, but
|
|
13
|
+
// aborted/failed compaction, invalid framing and incomplete recovery stay fatal.
|
|
14
|
+
export class PiCompletion {
|
|
15
|
+
pending = '';
|
|
16
|
+
failed = false;
|
|
17
|
+
error = false;
|
|
18
|
+
retry = false;
|
|
19
|
+
compacting = false;
|
|
20
|
+
compactionReason = null;
|
|
21
|
+
recovery = false;
|
|
22
|
+
completed = false;
|
|
23
|
+
modern = false;
|
|
24
|
+
settled = false;
|
|
25
|
+
lastAssistant = null;
|
|
26
|
+
contextReady = false;
|
|
27
|
+
|
|
28
|
+
write(chunk) {
|
|
29
|
+
this.pending += chunk;
|
|
30
|
+
let end;
|
|
31
|
+
while ((end = this.pending.indexOf('\n')) !== -1) {
|
|
32
|
+
const line = this.pending.slice(0, end);
|
|
33
|
+
this.pending = this.pending.slice(end + 1);
|
|
34
|
+
if (line.length > MAX_EVENT_CHARACTERS) this.failed = true;
|
|
35
|
+
else if (line.trim()) {
|
|
36
|
+
try { this.event(JSON.parse(line)); } catch { this.failed = true; }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (this.pending.length > MAX_EVENT_CHARACTERS) { this.failed = true; this.pending = ''; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
event(value) {
|
|
43
|
+
if (!value || typeof value.type !== 'string') { this.failed = true; return; }
|
|
44
|
+
if (value.type === 'factory_context_ready' && value.version === 1) this.contextReady = true;
|
|
45
|
+
if (value.type === 'agent_start' || value.type === 'message_start') {
|
|
46
|
+
this.completed = false; this.settled = false;
|
|
47
|
+
}
|
|
48
|
+
if (value.type === 'message_end' && (!value.message || typeof value.message.role !== 'string')) this.failed = true;
|
|
49
|
+
if (value.type === 'message_end' && value.message?.role === 'assistant') {
|
|
50
|
+
this.completed = false;
|
|
51
|
+
this.settled = false;
|
|
52
|
+
this.lastAssistant = value.message;
|
|
53
|
+
if (!['stop', 'toolUse', 'length', 'error', 'aborted'].includes(value.message.stopReason) || !Array.isArray(value.message.content)) this.failed = true;
|
|
54
|
+
if (value.message.stopReason === 'aborted') this.failed = true;
|
|
55
|
+
else if (['error', 'length'].includes(value.message.stopReason)) { this.error = true; this.recovery = false; }
|
|
56
|
+
else if (['stop', 'toolUse'].includes(value.message.stopReason)) {
|
|
57
|
+
if (this.error && (this.retry || this.recovery)) this.error = false;
|
|
58
|
+
this.recovery = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (value.type === 'auto_retry_start') { this.retry = true; this.completed = false; this.settled = false; }
|
|
62
|
+
if (value.type === 'auto_retry_end') {
|
|
63
|
+
if (!this.retry || value.success !== true) this.failed = true;
|
|
64
|
+
this.retry = false;
|
|
65
|
+
}
|
|
66
|
+
// Both audited versions use compaction_*; retain the older public alias
|
|
67
|
+
// defensively so failed compaction never disappears as an ignored event.
|
|
68
|
+
if (['compaction_start', 'auto_compaction_start'].includes(value.type)) {
|
|
69
|
+
if (this.compacting || !['manual', 'threshold', 'overflow'].includes(value.reason)) this.failed = true;
|
|
70
|
+
this.compactionReason = value.reason;
|
|
71
|
+
this.compacting = true; this.settled = false;
|
|
72
|
+
}
|
|
73
|
+
if (['compaction_end', 'auto_compaction_end'].includes(value.type)) {
|
|
74
|
+
if (!this.compacting || value.reason !== this.compactionReason || typeof value.willRetry !== 'boolean'
|
|
75
|
+
|| value.aborted !== false || value.errorMessage
|
|
76
|
+
|| !value.result || typeof value.result.summary !== 'string' || !value.result.summary.trim()) this.failed = true;
|
|
77
|
+
this.compacting = false;
|
|
78
|
+
if (value.reason === 'overflow' && value.willRetry === true && !this.failed) this.recovery = true;
|
|
79
|
+
}
|
|
80
|
+
if (value.type === 'agent_end') {
|
|
81
|
+
this.settled = false;
|
|
82
|
+
if (Object.hasOwn(value, 'willRetry')) {
|
|
83
|
+
this.modern = true;
|
|
84
|
+
if (typeof value.willRetry !== 'boolean') this.failed = true;
|
|
85
|
+
}
|
|
86
|
+
const last = value.messages?.at(-1);
|
|
87
|
+
this.completed = value.willRetry !== true && last?.role === 'assistant' && last.stopReason === 'stop'
|
|
88
|
+
&& this.lastAssistant?.stopReason === 'stop'
|
|
89
|
+
&& JSON.stringify(last) === JSON.stringify(this.lastAssistant)
|
|
90
|
+
&& Array.isArray(last.content) && last.content.some(part => part.type === 'text' && part.text?.trim())
|
|
91
|
+
&& !last.content.some(part => part.type === 'toolCall');
|
|
92
|
+
}
|
|
93
|
+
if (value.type === 'agent_settled') this.settled = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
finish() {
|
|
97
|
+
return !this.pending && !this.failed && !this.error && !this.retry && !this.compacting && !this.recovery
|
|
98
|
+
&& this.completed && (!this.modern || this.settled);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function launch() {
|
|
103
|
+
const directory = dirname(fileURLToPath(import.meta.url));
|
|
104
|
+
const registry = JSON.parse(readFileSync(join(directory, 'models.json'), 'utf8'));
|
|
105
|
+
const provider = registry.providers?.['factory-local'];
|
|
106
|
+
const args = process.argv.slice(2);
|
|
107
|
+
const selected = flag => args.filter(arg => arg === flag).length === 1 ? args[args.indexOf(flag) + 1] : null;
|
|
108
|
+
const model = provider?.models?.[0];
|
|
109
|
+
const thinking = selected('--thinking');
|
|
110
|
+
const validThinking = model?.reasoning && model?.thinkingLevelMap?.off !== 'none'
|
|
111
|
+
? ['low', 'medium', 'high'].includes(thinking) : thinking === 'off';
|
|
112
|
+
if (Object.keys(registry.providers || {}).length !== 1 || provider?.models?.length !== 1
|
|
113
|
+
|| selected('--provider') !== 'factory-local' || selected('--model') !== provider.models[0].id
|
|
114
|
+
|| selected('--mode') !== 'json' || !validThinking)
|
|
115
|
+
throw new Error('Local Pi registry and explicit selection do not match; refusing fallback');
|
|
116
|
+
// Never inherit credential inputs, provider overrides, NODE_OPTIONS or external
|
|
117
|
+
// Pi directories. The runtime image supplies binaries; the repository supplies data.
|
|
118
|
+
const env = Object.fromEntries(['PATH', 'HOME', 'LANG', 'TMPDIR', 'FACTORY_PHASE'].filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]]));
|
|
119
|
+
env.PI_CODING_AGENT_DIR = directory;
|
|
120
|
+
for (const file of ['pi-context-extension.mjs', 'pi-context-budget.mjs']) readFileSync(join(directory, file));
|
|
121
|
+
const version = spawnSync('pi', ['--version'], { env, encoding: 'utf8', timeout: 10000 });
|
|
122
|
+
if (version.status !== 0 || version.stdout.trim() !== '0.87.1')
|
|
123
|
+
throw new Error('Local context protection requires pinned Pi 0.87.1; qualify and select the worker image while idle. No fallback.');
|
|
124
|
+
// The explicit non-secret runtime key bypasses Pi 0.87.1's credential-store
|
|
125
|
+
// refresh, which otherwise tries to lock auth.json in this read-only mount.
|
|
126
|
+
// --no-extensions still disables discovery. Only this read-only bundled path
|
|
127
|
+
// is explicitly enabled; it cannot load repository or host extensions.
|
|
128
|
+
const child = spawn('pi', [...args, '--extension', join(directory, 'pi-context-extension.mjs'), '--api-key', 'factory-local-keyless'], { env, stdio: ['inherit', 'pipe', 'inherit'] });
|
|
129
|
+
const completion = new PiCompletion();
|
|
130
|
+
child.stdout.setEncoding('utf8');
|
|
131
|
+
child.stdout.on('data', chunk => {
|
|
132
|
+
process.stdout.write(chunk);
|
|
133
|
+
completion.write(chunk);
|
|
134
|
+
});
|
|
135
|
+
child.on('error', error => { console.error(`Local Pi launch failed: ${error.message}`); process.exitCode = 1; });
|
|
136
|
+
child.on('close', code => {
|
|
137
|
+
const completed = completion.contextReady && completion.finish();
|
|
138
|
+
if (!completed) console.error('Local Pi did not complete successfully; inspect the private log. No fallback was selected.');
|
|
139
|
+
process.exitCode = code === 0 && completed ? 0 : (Number.isInteger(code) && code > 0 ? code : 1);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) launch();
|
package/factory/pins.json
CHANGED
package/factory/updates.mjs
CHANGED
|
@@ -65,17 +65,33 @@ function installedEntry(dataHome, version) {
|
|
|
65
65
|
return pkg.name === PACKAGE && pkg.version === version && existsSync(entry) ? entry : null;
|
|
66
66
|
} catch { return null; }
|
|
67
67
|
}
|
|
68
|
-
export function installRelease(version, dataHome = DATA_HOME, runner = spawnSync
|
|
68
|
+
export function installRelease(version, dataHome = DATA_HOME, runner = spawnSync, {
|
|
69
|
+
now = () => performance.now(),
|
|
70
|
+
sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
|
|
71
|
+
} = {}) {
|
|
69
72
|
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error('Invalid release version');
|
|
70
73
|
const existing = installedEntry(dataHome, version);
|
|
71
74
|
if (existing) return existing;
|
|
72
75
|
const releases = join(dataHome, 'releases'); privateDir(releases);
|
|
73
76
|
const staging = mkdtempSync(join(releases, '.download-'));
|
|
74
77
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
// Metadata refresh and visibility retries share the original download budget.
|
|
79
|
+
// Only ETARGET is retried; offline/auth/identity failures keep their own errors.
|
|
80
|
+
const deadline = now() + 120000;
|
|
81
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
82
|
+
const remaining = Math.floor(deadline - now());
|
|
83
|
+
if (remaining <= 0) throw new Error(`npm update download failed for ${PACKAGE}@${version}: 120-second download budget exhausted`);
|
|
84
|
+
const result = runner('npm', ['install', '--prefix', staging, '--no-save', '--package-lock=false',
|
|
85
|
+
'--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund', '--prefer-online', '--prefer-offline=false', '--registry', REGISTRY, `${PACKAGE}@${version}`],
|
|
86
|
+
{ encoding: 'utf8', timeout: remaining, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 });
|
|
87
|
+
if (!result.error && result.status === 0) break;
|
|
88
|
+
const detail = result.error?.message || result.stderr || result.status;
|
|
89
|
+
const visibilityDelay = !result.error && /^npm (?:ERR!|error) code ETARGET\s*$/m.test(result.stderr || '');
|
|
90
|
+
if (!visibilityDelay) throw new Error(`npm update download failed for ${PACKAGE}@${version}: ${detail}`);
|
|
91
|
+
if (attempt === 3 || deadline - now() <= 1000)
|
|
92
|
+
throw new Error(`npm release ${PACKAGE}@${version} is not yet downloadable; publication may still be processing. Retry the update later. ${detail}`);
|
|
93
|
+
sleep(1000);
|
|
94
|
+
}
|
|
79
95
|
const pkg = read(join(staging, 'node_modules', PACKAGE, 'package.json'));
|
|
80
96
|
if (pkg.name !== PACKAGE || pkg.version !== version || !existsSync(join(staging, 'node_modules', PACKAGE, 'bin/software-defence-factory.mjs')))
|
|
81
97
|
throw new Error('Downloaded package identity does not match the requested release');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "software-defence-factory",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Factory: scoped software delivery and defence investigations with isolated jobs, evidence and review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"test": "node --test tests/*.test.mjs",
|
|
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
|
-
"test:dashboard": "npm --prefix dashboard test"
|
|
60
|
+
"test:dashboard": "npm --prefix dashboard test",
|
|
61
|
+
"qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
64
|
"yaml": "2.9.1"
|