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

1/1

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.

  1. ATTEMPT 1M-126FAILED ORACLEverification
  2. ATTEMPT 2M-136FAILED INFRA TERMINALinfrastructure
  3. ATTEMPT 3M-148FAILED ORACLEverification
  4. ATTEMPT 4M-157READY

ATTEMPT 4 / EXECUTION ANATOMY

01Discoverynot captured
02Qualification2m17s26.2%
03Authoring5m16s60.3%
04Verification1m11s13.6%

03 / ECONOMIC NARRATIVE

Economic identity

Observed sponsorship, authorization, scope, transfer, and outcome. No estimates or inferred value.

  1. 01 / MANDATE

    Sponsored and authorized

    The recorded mandate names both its sponsor and initiative, with authorization tied to a named approver and approval time.

    SPONSORNorthset OSS Fund · OSS mission experimentation

    AUTHORIZEDinternal-user:aeziz ·

  2. 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.

    TASKTASK-OSS-07620330CE4A2FE3 · defect fix · attempt 4

    INVITATIONIssue #28 · label

    SCOPE2 files · 34 changed lines · 1 production files · 1 test files · 1 declared check

  3. 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.

    MAINTAINER TRANSFERnone · not merge contingent

    UPSTREAMOPEN · 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
TOTAL COSTUNPRICED

No complete total is recorded; see the missing or unpriced component record above.

Evidence annexEconomic · technical · provenance · limitations
01

Economic evidence

Lineage, usage, caps, outcome, and cost provenance

Attempt lineage

  • M-126attempt 1 · FAILED ORACLE · verification
  • M-136attempt 2 · FAILED INFRA TERMINAL · infrastructure
  • M-148attempt 3 · FAILED ORACLE · verification
  • M-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
02

Technical evidence

Code, environment, exact commands, patch, and outputs

Project

nandansai08/personal-ai

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

  1. npm test -- tests/plugins/manager.test.ts

    exit 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


 RUN  v3.2.6 /workspace

 ✓ tests/plugins/manager.test.ts (9 tests) 4163ms
   ✓ PluginManager > runs beforePrompt and afterResponse hook chains  2020ms
   ✓ PluginManager > a timing-out hook falls back to the previous value and bumps failures  2030ms

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Start at  21:41:25
   Duration  7.93s (transform 625ms, setup 0ms, collect 684ms, tests 4.16s, environment 0ms, prepare 797ms)

Redacted stderr
=== cmd 1: npm test -- tests/plugins/manager.test.ts ===
stderr | tests/plugins/manager.test.ts > PluginManager > loads enabled valid plugins, skips invalid and disabled ones
[21:41:29] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > loads enabled valid plugins, skips invalid and disabled ones
[21:41:29] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > loads enabled valid plugins, skips invalid and disabled ones
[21:41:29] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

stderr | tests/plugins/manager.test.ts > PluginManager > registers plugin tools into the tool registry and they execute
[21:41:29] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > registers plugin tools into the tool registry and they execute
[21:41:29] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > registers plugin tools into the tool registry and they execute
[21:41:29] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

stderr | tests/plugins/manager.test.ts > PluginManager > runs beforePrompt and afterResponse hook chains
[21:41:29] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > runs beforePrompt and afterResponse hook chains
[21:41:29] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > runs beforePrompt and afterResponse hook chains
[21:41:29] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

stderr | tests/plugins/manager.test.ts > PluginManager > runs beforePrompt and afterResponse hook chains
[21:41:31] [WARN ] [plugins] slow-hook.beforePrompt failed: timed out after 2000ms

stderr | tests/plugins/manager.test.ts > PluginManager > a timing-out hook falls back to the previous value and bumps failures
[21:41:31] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > a timing-out hook falls back to the previous value and bumps failures
[21:41:31] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > a timing-out hook falls back to the previous value and bumps failures
[21:41:31] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

stderr | tests/plugins/manager.test.ts > PluginManager > a timing-out hook falls back to the previous value and bumps failures
[21:41:33] [WARN ] [plugins] slow-hook.beforePrompt failed: timed out after 2000ms

stderr | tests/plugins/manager.test.ts > PluginManager > unload removes tools; reload restores them
[21:41:33] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > unload removes tools; reload restores them
[21:41:33] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > unload removes tools; reload restores them
[21:41:33] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

stderr | tests/plugins/manager.test.ts > PluginManager > setEnabled(false) persists to plugin.json
[21:41:33] [WARN ] [plugins] rejected broken-manifest: invalid name (kebab-case required): BAD NAME!!

stderr | tests/plugins/manager.test.ts > PluginManager > setEnabled(false) persists to plugin.json
[21:41:33] [WARN ] [plugins] throws-on-init.initialize failed: boom

stderr | tests/plugins/manager.test.ts > PluginManager > setEnabled(false) persists to plugin.json
[21:41:33] [WARN ] [plugins] ⚠ Plugin throws-on-init failed: initialize: boom

03

Provenance & limitations

Bundle identity, attestation, verification, and claims boundary

Record 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

Download 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.yml

Attestation 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
Northset contributed this fix and ran its declared checks. Contributor self-run. Not maintainer verification.

Evidence of what ran — not a verdict that the code is good.

SELF-FUNDED FIELD-TESTING.

FOR MAINTAINERS

Request a private run

Maintain an open-source project? Send Northset a PR already in your queue. We run its repository-declared checks in an isolated container and return the run record privately. We do not modify the PR. Nothing is published without your approval. Free during the pilot.

The issue form is public. Do not include secrets or private repository details there; use email instead.

Already onboarded? Add northset-verify to a PR to request a run on that PR.

Claims boundary

This page reports scoped proof-of-pass receipt evidence. It does not prove code quality, security, full CI coverage, production readiness, or maintainer approval. An attestation confirms bundle provenance; it does not broaden the receipt's claim.

Read the full Claims Boundary policy.