NORTHSET
Proof-of-Pass Receipt
Receipt ID M-157
ISSUE / WORKPlugin loader: better error when export shape is wrong · PR #44
CONTRIBUTOR SELF-RUN — NOT MAINTAINER VERIFICATION
RUN→
01 / TECHNICAL RESULT
Technical result
declared commands passed
Recorded on the named code in the named execution environment. The evidence annex carries the exact scope.
- Upstream
- OPENmutable external state
- Attempts
- 4 totalcurrent
M-157· #4 - Cost record
- PARTIALunpriced
02 / RECORDED WORK SEQUENCE
Northset Proofline
Every task-bound attempt is shown in recorded order. Anatomy geometry follows each non-null elapsed duration’s share; labels retain exact recorded time.
- ATTEMPT 1
M-126FAILED ORACLEverification - ATTEMPT 2
M-136FAILED INFRA TERMINALinfrastructure - ATTEMPT 3
M-148FAILED ORACLEverification - ATTEMPT 4
M-157READY
ATTEMPT 4 / EXECUTION ANATOMY
03 / ECONOMIC NARRATIVE
Economic identity
Observed sponsorship, authorization, scope, transfer, and outcome. No estimates or inferred value.
01 / MANDATE
Sponsored and authorized
The recorded mandate names both its sponsor and initiative, with authorization tied to a named approver and approval time.
02 / WORK
Demand became scoped work
An external issue invitation became a stable task and a recorded code-and-test scope, without treating scope as a measure of value.
03 / EXTERNAL OUTCOME
Transfer and upstream state
The recorded maintainer transfer and its contingency are separate from upstream state, which remains a mutable external observation.
04 / COST COMPLETENESS
Known, unknown, and unpriced
Known: maintainer payment was none. A known zero external transfer is not a zero total cost.
Missing or unpriced components
- model inference
- actual host compute
- human review
- shared tooling
No complete total is recorded; see the missing or unpriced component record above.
Evidence annexEconomic · technical · provenance · limitations
01Economic evidence
Lineage, usage, caps, outcome, and cost provenance
Economic evidence
Lineage, usage, caps, outcome, and cost provenanceAttempt lineage
M-126attempt 1 · FAILED ORACLE · verificationM-136attempt 2 · FAILED INFRA TERMINAL · infrastructureM-148attempt 3 · FAILED ORACLE · verificationM-157attempt 4 · READY
Recorded usage
- requested reviewer model
- gpt-5.6-sol
- actual reviewer model
- not captured
- requested author model
- gpt-5.6-sol
- actual author model
- not captured
- networked setup phase
- 35.9s
- install-only duration
- not captured
- declared commands
- 17.2s
- unclassified executor time
- 18.2s
- CPU time
- not captured
- peak RSS
- not captured
Configured resource envelope
- CPU cap
- 2
- memory cap
- 4096 MB
- PID cap
- 512
- command wall cap
- 1800s
- output cap / stream
- 2000000 bytes
Configured limits are an envelope, not observed consumption.
Cost lines
- maintainer paymentobserved quantity unpriced · 0 external transfer · amount unavailable
Cost status: partial. Values without source-backed quantities and rates remain null.
Outcome facts
- technical checks passed
- true
- PR opened
- true
- CI
- pending
- merged
- false
- accepted as submitted
- not established
- external cycle time
- not captured
- released
- not observed
- deployed
- not observed
- business result
- not observed
02Technical evidence
Code, environment, exact commands, patch, and outputs
Technical evidence
Code, environment, exact commands, patch, and outputsProject
Work
Plugin loader: better error when export shape is wrong · PR #44
Verification execution
runtime: northset-oss executor v1
human operator: aeziz
Code
- base
e50a1befede455916214f2fc79bd0e017e5fef2a- recorded patch commit
a7f2567ab6b488bbeaa5d0ac931804ce4213f18a
declared metadata; not execution-bound- patch diff SHA-256
sha256:e1ba0cde52b853946a58519fd02acbe98c99a9297fcc662d05ae509df7b4fcd8
bound to executed patch bytes
Environment
- image reference
- node@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7
- repository digest
node@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7- immutable image ID
sha256:6ed6ff875e9714bb845b850b08f3ad231ccbb5848812647bcf11acdd4bd1d2f8- platform
- linux/arm64
- network
- phaseA:bridge,phaseB:none
Declared checks
Execution summary
1/1 declared command returned exit 0 in the recorded environment
npm test -- tests/plugins/manager.test.tsexit 0 · 17.2s
run wall (derived from recorded timestamps) 1m11s
PASS — 1/1 declared command
Every command listed returned exit 0 in the declared environment. Only the listed commands are in scope. Unlisted test, lint, typecheck, build, coverage, compiler, full-suite, and CI gates are not implied or recorded.
Public scope interpretation
The receipt proves the declared automated checks only; maintainer review and merge remain independent human decisions.
Committed patch.diff
diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts
index 0d29ad1582722bf3bc3bddacdd6156cdef231334..360c2704ee0adb4497ed864b4616ef72aa780829 100644
--- a/src/plugins/loader.ts
+++ b/src/plugins/loader.ts
@@ -105,7 +105,13 @@ export async function importPlugin(dir: string, manifest: PluginManifest): Promi
{ default?: PersonalAIPlugin; plugin?: PersonalAIPlugin }
const plugin = mod.default ?? mod.plugin
if (!plugin || typeof plugin.name !== 'string') {
- return { ok: false, error: 'module must export a PersonalAIPlugin as default or `plugin`' }
+ const availableExports = Object.keys(mod).join(', ')
+ const candidate = mod.default != null ? 'default' : mod.plugin != null ? '`plugin`' : undefined
+ const missingField = candidate ? `; ${candidate} export is missing required field: name` : ''
+ return {
+ ok: false,
+ error: `module must export a PersonalAIPlugin as default or \`plugin\`; available exports: ${availableExports}${missingField}`,
+ }
}
return { ok: true, plugin }
} catch (err) {
diff --git a/tests/plugins/manager.test.ts b/tests/plugins/manager.test.ts
index 68973c618e92376b73d9c5d6314ac8bcffb7d82a..838d6dab8784cdaec72a01a32cd99a788aa026f4 100644
--- a/tests/plugins/manager.test.ts
+++ b/tests/plugins/manager.test.ts
@@ -4,7 +4,7 @@ import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'
import { PluginManager } from '../../src/plugins/manager.js'
-import { validateManifest } from '../../src/plugins/loader.js'
+import { importPlugin, validateManifest } from '../../src/plugins/loader.js'
import { ToolRegistry } from '../../src/tools/registry.js'
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pai-plugins-'))
@@ -75,6 +75,30 @@ describe('manifest validation', () => {
})
})
+describe('plugin imports', () => {
+ it('reports available exports and a missing name on the default candidate', async () => {
+ const manifest = {
+ name: 'invalid-export', version: '1.0.0', description: 'invalid', main: './index.js', enabled: true,
+ }
+ writePlugin('invalid-export', manifest, `
+ export default { version: '1.0.0', description: 'missing name' }
+ export const helper = 'not a plugin'
+ `)
+
+ const pluginDir = path.join(tmpRoot, 'invalid-export')
+ try {
+ const result = await importPlugin(pluginDir, manifest)
+
+ expect(result).toEqual({
+ ok: false,
+ error: 'module must export a PersonalAIPlugin as default or `plugin`; available exports: default, helper; default export is missing required field: name',
+ })
+ } finally {
+ fs.rmSync(pluginDir, { recursive: true, force: true })
+ }
+ })
+})
+
describe('PluginManager', () => {
it('loads enabled valid plugins, skips invalid and disabled ones', async () => {
const registry = new ToolRegistry()
Redacted stdout
=== cmd 1: npm test -- tests/plugins/manager.test.ts ===
> @nandansai08/personal-ai@1.0.0 test
> vitest run tests/plugins/manager.test.ts
[1m[46m RUN [49m[22m [36mv3.2.6 [39m[90m/workspace[39m
[32m✓[39m tests/plugins/manager.test.ts [2m([22m[2m9 tests[22m[2m)[22m[33m 4163[2mms[22m[39m
[33m[2m✓[22m[39m PluginManager[2m > [22mruns beforePrompt and afterResponse hook chains [33m 2020[2mms[22m[39m
[33m[2m✓[22m[39m PluginManager[2m > [22ma timing-out hook falls back to the previous value and bumps failures [33m 2030[2mms[22m[39m
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
[2m Tests [22m [1m[32m9 passed[39m[22m[90m (9)[39m
[2m Start at [22m 21:41:25
[2m Duration [22m 7.93s[2m (transform 625ms, setup 0ms, collect 684ms, tests 4.16s, environment 0ms, prepare 797ms)[22m
Redacted stderr
=== cmd 1: npm test -- tests/plugins/manager.test.ts ===
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mloads enabled valid plugins, skips invalid and disabled ones
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mloads enabled valid plugins, skips invalid and disabled ones
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mloads enabled valid plugins, skips invalid and disabled ones
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mregisters plugin tools into the tool registry and they execute
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mregisters plugin tools into the tool registry and they execute
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mregisters plugin tools into the tool registry and they execute
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mruns beforePrompt and afterResponse hook chains
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mruns beforePrompt and afterResponse hook chains
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mruns beforePrompt and afterResponse hook chains
[22m[39m[33m[21:41:29] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2mruns beforePrompt and afterResponse hook chains
[22m[39m[33m[21:41:31] [WARN ] [plugins][0m slow-hook.beforePrompt failed: timed out after 2000ms
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2ma timing-out hook falls back to the previous value and bumps failures
[22m[39m[33m[21:41:31] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2ma timing-out hook falls back to the previous value and bumps failures
[22m[39m[33m[21:41:31] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2ma timing-out hook falls back to the previous value and bumps failures
[22m[39m[33m[21:41:31] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2ma timing-out hook falls back to the previous value and bumps failures
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m slow-hook.beforePrompt failed: timed out after 2000ms
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2munload removes tools; reload restores them
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2munload removes tools; reload restores them
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2munload removes tools; reload restores them
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2msetEnabled(false) persists to plugin.json
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2msetEnabled(false) persists to plugin.json
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m throws-on-init.initialize failed: boom
[90mstderr[2m | tests/plugins/manager.test.ts[2m > [22m[2mPluginManager[2m > [22m[2msetEnabled(false) persists to plugin.json
[22m[39m[33m[21:41:33] [WARN ] [plugins][0m ⚠ Plugin throws-on-init failed: initialize: boom
03Provenance & limitations
Bundle identity, attestation, verification, and claims boundary
Provenance & limitations
Bundle identity, attestation, verification, and claims boundaryRecord details
- payment
- none · not merge-contingent
- redactions
- none recorded
- Bundle contents digest
sha256:e2d2f810827e65fd59ab9255ba6aa69fb2122e99dbf1d6d96aa83753e6eac603- Signed asset SHA-256
sha256:1b718424be99791f0ec0a9b5072fcbc2d1e6c1902e7b03a7ce8a86a821c633ef- Signed provenance recorded
- verified 2026-07-16T22:49:26.997Z
NOT INCLUDED
- Does not prove code quality
- Does not prove security
- Contributor self-run record of Northset's own contribution; not the maintainer's verification.
- The receipt proves the declared automated checks only; maintainer review and merge remain independent human decisions.
Signed bundle
Verify this receipt
gh attestation verify run-record-M-157-e2d2f810827e.tar.gz --repo northset-oss/verification-pilot --signer-workflow northset-oss/verification-pilot/.github/workflows/attest-bundle.ymlAttestation confirms that Northset's signing workflow produced this exact bundle. The signer does not witness the recorded run, and verification does not turn it into maintainer verification.
QR → receipt page