pluribus-context 0.3.35 → 0.3.36

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/bin/pluribus.js +12 -0
  4. package/docs/agent-firewall-denial-audit.md +95 -0
  5. package/docs/ai-pr-review-receipts.md +20 -0
  6. package/docs/compaction-resume-receipts.md +43 -0
  7. package/docs/controlled-learning-queue.md +48 -0
  8. package/docs/install-plan-receipts.md +2 -0
  9. package/docs/loaded-resource-boundary.md +97 -0
  10. package/docs/memory-write-policy-receipts.md +41 -0
  11. package/docs/parallel-session-review-ledger.md +103 -0
  12. package/docs/phase-boundary-contracts.md +87 -0
  13. package/docs/review-primitive-gate.md +2 -0
  14. package/docs/skill-install-receipts.md +102 -0
  15. package/docs/skill-use-rate-receipts.md +104 -0
  16. package/examples/agent-firewall-denial-audit/README.md +14 -0
  17. package/examples/agent-firewall-denial-audit/check-denial-audit.mjs +116 -0
  18. package/examples/agent-firewall-denial-audit/denial-envelope.json +9 -0
  19. package/examples/agent-firewall-denial-audit/operator-audit-record.json +20 -0
  20. package/examples/ai-pr-review-receipts/.github/workflows/ai-pr-review-receipt.yml +25 -0
  21. package/examples/ai-pr-review-receipts/README.md +51 -1
  22. package/examples/ai-pr-review-receipts/incomplete-review-primitive-receipt.json +43 -0
  23. package/examples/ai-pr-review-receipts/review-primitive-receipt.json +60 -0
  24. package/examples/compaction-resume-receipts/README.md +12 -0
  25. package/examples/compaction-resume-receipts/check-resume-receipt.mjs +116 -0
  26. package/examples/compaction-resume-receipts/safe-resume-receipt.json +52 -0
  27. package/examples/compaction-resume-receipts/unsafe-resume-receipt.json +41 -0
  28. package/examples/controlled-learning-queue/README.md +26 -0
  29. package/examples/controlled-learning-queue/check-learning-queue.mjs +44 -0
  30. package/examples/controlled-learning-queue/leads/acme-job-card.md +12 -0
  31. package/examples/controlled-learning-queue/learning_queue.md +27 -0
  32. package/examples/controlled-learning-queue/memory/durable.md +10 -0
  33. package/examples/controlled-learning-queue/memory/working-notes.md +5 -0
  34. package/examples/controlled-learning-queue/role/job-contract.md +18 -0
  35. package/examples/controlled-learning-queue/skills/qualify-lead.md +17 -0
  36. package/examples/loaded-resource-boundary/README.md +22 -0
  37. package/examples/loaded-resource-boundary/check-loaded-resource-boundary.mjs +65 -0
  38. package/examples/loaded-resource-boundary/loaded-resource-boundary.json +69 -0
  39. package/examples/memory-write-policy/README.md +28 -0
  40. package/examples/memory-write-policy/approved-memory-update.json +48 -0
  41. package/examples/memory-write-policy/check-memory-update.mjs +120 -0
  42. package/examples/memory-write-policy/quarantined-memory-update.json +43 -0
  43. package/examples/parallel-session-review-ledger/README.md +13 -0
  44. package/examples/parallel-session-review-ledger/check-parallel-session-review-ledger.mjs +69 -0
  45. package/examples/parallel-session-review-ledger/parallel-session-review-ledger.json +72 -0
  46. package/examples/phase-boundary-contract/README.md +23 -0
  47. package/examples/phase-boundary-contract/check-phase-boundary.mjs +73 -0
  48. package/examples/phase-boundary-contract/phase-boundary-contract.json +68 -0
  49. package/examples/skill-install-receipts/README.md +31 -0
  50. package/examples/skill-install-receipts/check-skill-install-receipt.mjs +75 -0
  51. package/examples/skill-install-receipts/skill-install-receipt.json +79 -0
  52. package/examples/skill-use-rate-receipts/README.md +16 -0
  53. package/examples/skill-use-rate-receipts/check-skill-use-rate.mjs +89 -0
  54. package/examples/skill-use-rate-receipts/skill-use-rate-receipt.json +79 -0
  55. package/package.json +1 -1
  56. package/src/commands/demo.js +155 -0
  57. package/src/index.js +1 -0
  58. package/src/utils/version.js +1 -1
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ const [file] = process.argv.slice(2)
5
+
6
+ if (!file) {
7
+ console.error('Usage: node check-memory-update.mjs <memory-update-receipt.json>')
8
+ process.exit(2)
9
+ }
10
+
11
+ let receipt
12
+ try {
13
+ receipt = JSON.parse(readFileSync(file, 'utf8'))
14
+ } catch (error) {
15
+ console.error(JSON.stringify({ ok: false, file, errors: [`invalid JSON: ${error.message}`] }, null, 2))
16
+ process.exit(2)
17
+ }
18
+
19
+ const errors = []
20
+ const warnings = []
21
+
22
+ if (receipt.type !== 'agent.memory_update_receipt.v1') {
23
+ errors.push('type must be agent.memory_update_receipt.v1')
24
+ }
25
+
26
+ for (const key of ['update_id', 'run_id']) {
27
+ if (!receipt[key] || typeof receipt[key] !== 'string') {
28
+ errors.push(`${key} is required`)
29
+ }
30
+ }
31
+
32
+ const source = receipt.source || {}
33
+ if (!source.kind || typeof source.kind !== 'string') {
34
+ errors.push('source.kind is required')
35
+ }
36
+ if (!source.ref || typeof source.ref !== 'string') {
37
+ errors.push('source.ref is required')
38
+ }
39
+ if (!source.content_hash || typeof source.content_hash !== 'string') {
40
+ errors.push('source.content_hash is required; do not rely on raw memory text')
41
+ }
42
+
43
+ const scope = receipt.scope || {}
44
+ if (!scope.kind || !['repo', 'project', 'org', 'user'].includes(scope.kind)) {
45
+ errors.push('scope.kind must be repo, project, org, or user')
46
+ }
47
+ if (!scope.id || typeof scope.id !== 'string') {
48
+ errors.push('scope.id is required so a memory write cannot silently become global')
49
+ }
50
+ if (scope.kind === 'user') {
51
+ warnings.push('user-scoped durable memory is broad; prefer repo/project scope when possible')
52
+ }
53
+
54
+ const diff = receipt.proposed_diff || {}
55
+ const changed = ['adds', 'updates', 'supersedes', 'expires'].flatMap((key) => Array.isArray(diff[key]) ? diff[key] : [])
56
+ if (changed.length === 0) {
57
+ errors.push('proposed_diff must include at least one add, update, supersede, or expire entry')
58
+ }
59
+ for (const [index, item] of changed.entries()) {
60
+ if (!item.memory_ref || typeof item.memory_ref !== 'string') {
61
+ errors.push(`proposed_diff item ${index} is missing memory_ref`)
62
+ }
63
+ if (!item.summary_hash || typeof item.summary_hash !== 'string') {
64
+ errors.push(`proposed_diff item ${index} is missing summary_hash; log hashes, not raw memory bodies`)
65
+ }
66
+ if (item.raw_text) {
67
+ errors.push(`proposed_diff item ${index} must not include raw_text`)
68
+ }
69
+ }
70
+
71
+ const policy = receipt.write_policy || {}
72
+ if (policy.status !== 'approved') {
73
+ errors.push(`write_policy.status is ${policy.status || 'missing'}; shared memory write must remain proposed/quarantined until approved`)
74
+ }
75
+ if (!policy.policy_ref || typeof policy.policy_ref !== 'string') {
76
+ errors.push('write_policy.policy_ref is required')
77
+ }
78
+ if (!policy.approved_by || typeof policy.approved_by !== 'string') {
79
+ errors.push('write_policy.approved_by is required for durable writes')
80
+ }
81
+ if (policy.private_or_sensitive_detected !== false) {
82
+ errors.push('write_policy.private_or_sensitive_detected must be false before merge')
83
+ }
84
+
85
+ const lifecycle = receipt.lifecycle || {}
86
+ if (!lifecycle.expires_at && !lifecycle.review_after) {
87
+ errors.push('lifecycle.expires_at or lifecycle.review_after is required to avoid immortal stale facts')
88
+ }
89
+ if (lifecycle.supersedes_required === true && (!Array.isArray(diff.supersedes) || diff.supersedes.length === 0)) {
90
+ errors.push('lifecycle.supersedes_required is true but proposed_diff.supersedes is empty')
91
+ }
92
+
93
+ const visibility = receipt.injection_visibility || {}
94
+ if (visibility.next_session_visible !== true) {
95
+ errors.push('injection_visibility.next_session_visible must be true so future agents can see what memory was injected')
96
+ }
97
+ if (!visibility.preview_path || typeof visibility.preview_path !== 'string') {
98
+ warnings.push('injection_visibility.preview_path is recommended for human review')
99
+ }
100
+
101
+ const privacy = receipt.privacy || {}
102
+ for (const key of ['raw_memory_text_logged', 'raw_prompts_logged', 'raw_tool_output_logged', 'secrets_logged']) {
103
+ if (privacy[key] !== false) {
104
+ errors.push(`privacy.${key} must be false for this gate`)
105
+ }
106
+ }
107
+
108
+ const result = {
109
+ ok: errors.length === 0,
110
+ file,
111
+ update_id: receipt.update_id,
112
+ run_id: receipt.run_id,
113
+ scope: scope.kind && scope.id ? `${scope.kind}:${scope.id}` : undefined,
114
+ write_status: policy.status,
115
+ errors,
116
+ warnings
117
+ }
118
+
119
+ console.log(JSON.stringify(result, null, 2))
120
+ process.exit(result.ok ? 0 : 1)
@@ -0,0 +1,43 @@
1
+ {
2
+ "type": "agent.memory_update_receipt.v1",
3
+ "update_id": "memupd_2026_06_01_002",
4
+ "run_id": "agent_run_8743",
5
+ "source": {
6
+ "kind": "claude-code-session",
7
+ "ref": "repo:acme/shop#debug-chat",
8
+ "content_hash": "sha256:6d4fa2a2114f83fd8bd9d6eb3c632ff6f20252f978bc9deec0b9d85694e02d4b"
9
+ },
10
+ "scope": {
11
+ "kind": "user",
12
+ "id": "developer-laptop"
13
+ },
14
+ "proposed_diff": {
15
+ "adds": [
16
+ {
17
+ "memory_ref": "memory:global:production-debug-shortcut",
18
+ "summary_hash": "sha256:5ee95af43fd2fb8b90908ef8cc4a5e6f050a8f52c97c7c93fd7f37bd1dcf3c2a",
19
+ "raw_text": "do not store raw memory bodies here"
20
+ }
21
+ ],
22
+ "updates": [],
23
+ "supersedes": [],
24
+ "expires": []
25
+ },
26
+ "write_policy": {
27
+ "status": "quarantined",
28
+ "policy_ref": "repo-memory-policy:v2",
29
+ "private_or_sensitive_detected": true
30
+ },
31
+ "lifecycle": {
32
+ "supersedes_required": false
33
+ },
34
+ "injection_visibility": {
35
+ "next_session_visible": false
36
+ },
37
+ "privacy": {
38
+ "raw_memory_text_logged": true,
39
+ "raw_prompts_logged": false,
40
+ "raw_tool_output_logged": false,
41
+ "secrets_logged": false
42
+ }
43
+ }
@@ -0,0 +1,13 @@
1
+ # Parallel session review ledger example
2
+
3
+ This example is a copyable receipt for teams running multiple agent sessions at once. It is designed for the review bottleneck: deciding whether each session can be trusted, continued, or rejected without reading an entire transcript.
4
+
5
+ ```bash
6
+ node examples/parallel-session-review-ledger/check-parallel-session-review-ledger.mjs examples/parallel-session-review-ledger/parallel-session-review-ledger.json
7
+ ```
8
+
9
+ Expected output:
10
+
11
+ ```text
12
+ parallel session review ledger ok: 3 sessions checked
13
+ ```
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const file = process.argv[2] || path.join('examples', 'parallel-session-review-ledger', 'parallel-session-review-ledger.json');
6
+ const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
7
+ const errors = [];
8
+
9
+ const allowedStates = new Set(['complete', 'partial', 'blocked', 'unsafe_to_resume']);
10
+ const allowedNextActions = new Set([
11
+ 'review_diff',
12
+ 'run_missing_check',
13
+ 'continue_same_scope',
14
+ 'ask_human',
15
+ 'stop_manual_review'
16
+ ]);
17
+
18
+ function add(condition, message) {
19
+ if (!condition) errors.push(message);
20
+ }
21
+
22
+ function globPrefix(pattern) {
23
+ return pattern.replace(/\*\*.*$/, '').replace(/\*.*$/, '');
24
+ }
25
+
26
+ add(receipt.schema === 'pluribus.parallel_session_review_ledger.v1', 'schema must be pluribus.parallel_session_review_ledger.v1');
27
+ add(Array.isArray(receipt.sessions) && receipt.sessions.length > 0, 'sessions must be a non-empty array');
28
+
29
+ for (const session of receipt.sessions || []) {
30
+ const label = session.id || '<missing-id>';
31
+ add(Boolean(session.id), 'session id is required');
32
+ add(Boolean(session.assignment), `${label}: assignment is required`);
33
+ add(Boolean(session.branch), `${label}: branch is required`);
34
+ add(Boolean(session.allowed_scope), `${label}: allowed_scope is required`);
35
+ add(Array.isArray(session.allowed_scope?.files) && session.allowed_scope.files.length > 0, `${label}: allowed_scope.files must be non-empty`);
36
+ add(allowedStates.has(session.state), `${label}: invalid state ${session.state}`);
37
+ add(allowedNextActions.has(session.safe_next_action), `${label}: invalid safe_next_action ${session.safe_next_action}`);
38
+
39
+ const evidence = session.evidence || [];
40
+ const missingChecks = session.missing_checks || [];
41
+ const privacyFlags = session.privacy_flags || [];
42
+
43
+ if (session.state === 'complete') {
44
+ add(evidence.length > 0, `${label}: complete sessions need evidence`);
45
+ add(missingChecks.length === 0, `${label}: complete sessions cannot have missing_checks`);
46
+ add(privacyFlags.length === 0, `${label}: complete sessions cannot have privacy_flags`);
47
+ }
48
+
49
+ if (session.state === 'partial') {
50
+ add(missingChecks.length > 0, `${label}: partial sessions must name missing_checks`);
51
+ }
52
+
53
+ if (session.state === 'unsafe_to_resume') {
54
+ add(session.safe_next_action === 'stop_manual_review', `${label}: unsafe_to_resume must use stop_manual_review`);
55
+ }
56
+
57
+ const allowedPrefixes = (session.allowed_scope?.files || []).map(globPrefix);
58
+ for (const touched of session.touched_files || []) {
59
+ add(allowedPrefixes.some((prefix) => touched.startsWith(prefix)), `${label}: touched file outside allowed scope: ${touched}`);
60
+ }
61
+ }
62
+
63
+ if (errors.length > 0) {
64
+ console.error('parallel session review ledger invalid:');
65
+ for (const error of errors) console.error(`- ${error}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log(`parallel session review ledger ok: ${receipt.sessions.length} sessions checked`);
@@ -0,0 +1,72 @@
1
+ {
2
+ "schema": "pluribus.parallel_session_review_ledger.v1",
3
+ "generated_at": "2026-06-04T19:00:00Z",
4
+ "run": {
5
+ "orchestrator": "human",
6
+ "repo": "redacted-service",
7
+ "coordination_mode": "parallel_sessions"
8
+ },
9
+ "sessions": [
10
+ {
11
+ "id": "session-a",
12
+ "agent": "claude-code",
13
+ "assignment": "update validation for billing webhook retries",
14
+ "branch": "agent/billing-webhook-retry-validation",
15
+ "allowed_scope": {
16
+ "files": ["src/billing/**", "test/billing/**"],
17
+ "commands": ["npm test -- --test-name-pattern=billing"],
18
+ "network": "none"
19
+ },
20
+ "touched_files": ["src/billing/retries.js", "test/billing/retries.test.js"],
21
+ "agent_claim": "added retry validation and regression coverage",
22
+ "evidence": [
23
+ { "type": "commit", "ref": "abc1234" },
24
+ { "type": "test", "name": "billing retry validation", "status": "passed" }
25
+ ],
26
+ "missing_checks": [],
27
+ "privacy_flags": [],
28
+ "state": "complete",
29
+ "safe_next_action": "review_diff"
30
+ },
31
+ {
32
+ "id": "session-b",
33
+ "agent": "cursor-agent",
34
+ "assignment": "draft migration notes for webhook retry config",
35
+ "branch": "agent/retry-config-notes",
36
+ "allowed_scope": {
37
+ "files": ["docs/**"],
38
+ "commands": ["npm test -- --test-name-pattern=docs"],
39
+ "network": "none"
40
+ },
41
+ "touched_files": ["docs/webhook-retry-config.md"],
42
+ "agent_claim": "documented retry config and rollback notes",
43
+ "evidence": [
44
+ { "type": "diff", "ref": "docs-only" }
45
+ ],
46
+ "missing_checks": ["link-check docs/webhook-retry-config.md"],
47
+ "privacy_flags": [],
48
+ "state": "partial",
49
+ "safe_next_action": "run_missing_check"
50
+ },
51
+ {
52
+ "id": "session-c",
53
+ "agent": "codex-cli",
54
+ "assignment": "investigate retry metrics regression",
55
+ "branch": "agent/retry-metrics-investigation",
56
+ "allowed_scope": {
57
+ "files": ["src/metrics/**", "test/metrics/**"],
58
+ "commands": ["npm test -- --test-name-pattern=metrics"],
59
+ "network": "none"
60
+ },
61
+ "touched_files": ["src/metrics/retry-metrics.js"],
62
+ "agent_claim": "found possible metrics label mismatch, not fixed",
63
+ "evidence": [
64
+ { "type": "note", "ref": "suspected-label-mismatch" }
65
+ ],
66
+ "missing_checks": ["confirm label contract with owner"],
67
+ "privacy_flags": ["scope_expanded_without_approval"],
68
+ "state": "unsafe_to_resume",
69
+ "safe_next_action": "stop_manual_review"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,23 @@
1
+ # Phase-boundary contract example
2
+
3
+ This example is for multi-model coding workflows where one phase plans, another phase applies, and another verifies. It records the exact handoff boundary without storing prompts, transcripts, raw source, secrets, or full command output.
4
+
5
+ Run:
6
+
7
+ ```bash
8
+ node check-phase-boundary.mjs phase-boundary-contract.json
9
+ ```
10
+
11
+ Expected output:
12
+
13
+ ```text
14
+ phase-boundary contract ok: checkout-refactor-2026-06-03 apply->verify
15
+ ```
16
+
17
+ Use the shape as a small gate before moving from Apply to Verify:
18
+
19
+ - approved plan/task refs and hashes entered the phase;
20
+ - output artifact hash exists;
21
+ - changed file-set hash and tests are present;
22
+ - open risks are explicit;
23
+ - stale exploration transcript or rejected designs are intentionally dropped.
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+
4
+ const file = process.argv[2] || 'phase-boundary-contract.json';
5
+ const contract = JSON.parse(fs.readFileSync(file, 'utf8'));
6
+ const errors = [];
7
+
8
+ const phases = ['explore', 'propose', 'spec', 'design', 'tasks', 'apply', 'verify'];
9
+ const hashRe = /^sha256:[a-f0-9]{64}$/;
10
+ const unsafeRefRe = /(^\/|\.\.|[A-Za-z]:\\|secret|token|password|private_key)/i;
11
+ const requiredGateKeys = ['changed_files', 'tests_run', 'open_risks', 'stop_conditions'];
12
+
13
+ function expect(condition, message) {
14
+ if (!condition) errors.push(message);
15
+ }
16
+
17
+ expect(contract.schema === 'pluribus.phase-boundary-contract.v1', 'schema must be pluribus.phase-boundary-contract.v1');
18
+ expect(typeof contract.workflowId === 'string' && contract.workflowId.length >= 6, 'workflowId is required');
19
+ expect(phases.includes(contract.currentPhase), 'currentPhase must be a known phase');
20
+ expect(phases.includes(contract.nextPhase), 'nextPhase must be a known phase');
21
+ expect(contract.currentPhase !== contract.nextPhase, 'currentPhase and nextPhase must differ');
22
+
23
+ expect(Array.isArray(contract.allowedInput) && contract.allowedInput.length > 0, 'allowedInput must list at least one source');
24
+ for (const [index, input] of (contract.allowedInput || []).entries()) {
25
+ expect(typeof input.kind === 'string' && input.kind.length > 0, `allowedInput[${index}].kind is required`);
26
+ expect(typeof input.ref === 'string' && !unsafeRefRe.test(input.ref), `allowedInput[${index}].ref must be a non-secret relative/logical ref`);
27
+ expect(hashRe.test(input.contentHash || ''), `allowedInput[${index}].contentHash must be sha256:<64 hex>`);
28
+ }
29
+
30
+ expect(contract.outputArtifact && typeof contract.outputArtifact.kind === 'string', 'outputArtifact.kind is required');
31
+ expect(contract.outputArtifact && typeof contract.outputArtifact.ref === 'string' && !unsafeRefRe.test(contract.outputArtifact.ref), 'outputArtifact.ref must be a non-secret logical ref');
32
+ expect(contract.outputArtifact && hashRe.test(contract.outputArtifact.contentHash || ''), 'outputArtifact.contentHash must be sha256:<64 hex>');
33
+
34
+ const gate = contract.evidenceGate || {};
35
+ expect(gate.status === 'pass' || gate.status === 'needs_review' || gate.status === 'fail', 'evidenceGate.status must be pass, needs_review, or fail');
36
+ expect(Array.isArray(gate.requiredBeforeNextPhase), 'evidenceGate.requiredBeforeNextPhase must be an array');
37
+ for (const key of requiredGateKeys) {
38
+ expect((gate.requiredBeforeNextPhase || []).includes(key), `evidenceGate.requiredBeforeNextPhase must include ${key}`);
39
+ }
40
+ expect(gate.changedFiles && Number.isInteger(gate.changedFiles.count) && gate.changedFiles.count >= 0, 'evidenceGate.changedFiles.count is required');
41
+ expect(gate.changedFiles && hashRe.test(gate.changedFiles.fileSetHash || ''), 'evidenceGate.changedFiles.fileSetHash must be sha256:<64 hex>');
42
+ expect(Array.isArray(gate.testsRun), 'evidenceGate.testsRun must be an array');
43
+ for (const [index, test] of (gate.testsRun || []).entries()) {
44
+ expect(typeof test.name === 'string' && test.name.length > 0, `testsRun[${index}].name is required`);
45
+ expect(hashRe.test(test.commandHash || ''), `testsRun[${index}].commandHash must be sha256:<64 hex>`);
46
+ expect(['pass', 'fail', 'skipped'].includes(test.status), `testsRun[${index}].status must be pass/fail/skipped`);
47
+ }
48
+ expect(Array.isArray(gate.openRisks), 'evidenceGate.openRisks must be an array');
49
+ for (const [index, risk] of (gate.openRisks || []).entries()) {
50
+ expect(typeof risk.riskClass === 'string' && risk.riskClass.length > 0, `openRisks[${index}].riskClass is required`);
51
+ expect(['low', 'medium', 'high'].includes(risk.severity), `openRisks[${index}].severity must be low/medium/high`);
52
+ expect(typeof risk.safeToContinue === 'boolean', `openRisks[${index}].safeToContinue must be boolean`);
53
+ }
54
+
55
+ expect(Array.isArray(contract.droppedContext), 'droppedContext must be an array');
56
+ for (const [index, dropped] of (contract.droppedContext || []).entries()) {
57
+ expect(typeof dropped.kind === 'string' && dropped.kind.length > 0, `droppedContext[${index}].kind is required`);
58
+ expect(typeof dropped.reason === 'string' && dropped.reason.length > 0, `droppedContext[${index}].reason is required`);
59
+ }
60
+ expect(Array.isArray(contract.stopConditions), 'stopConditions must be an array');
61
+
62
+ if (gate.status === 'pass') {
63
+ expect((contract.stopConditions || []).length === 0, 'pass contracts must not have active stopConditions');
64
+ expect((gate.openRisks || []).every((risk) => risk.safeToContinue), 'pass contracts require all open risks to be safeToContinue');
65
+ }
66
+
67
+ if (errors.length) {
68
+ console.error(`phase-boundary contract failed (${errors.length}):`);
69
+ for (const error of errors) console.error(`- ${error}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ console.log(`phase-boundary contract ok: ${contract.workflowId} ${contract.currentPhase}->${contract.nextPhase}`);
@@ -0,0 +1,68 @@
1
+ {
2
+ "schema": "pluribus.phase-boundary-contract.v1",
3
+ "workflowId": "checkout-refactor-2026-06-03",
4
+ "currentPhase": "apply",
5
+ "nextPhase": "verify",
6
+ "allowedInput": [
7
+ {
8
+ "kind": "approved_plan",
9
+ "ref": "plans/checkout-refactor.md",
10
+ "contentHash": "sha256:0b70d75d6c8f0e54c74e1d7ea90e7c7b3d83e15dd3a3f7b4b9e9e73339d2b22e",
11
+ "required": true
12
+ },
13
+ {
14
+ "kind": "task_list",
15
+ "ref": "tasks/checkout-refactor.md#apply",
16
+ "contentHash": "sha256:63ccf4555f0adbe64bbff5a8f7c4b24708fdb2b5e4d0e417a5cb057b9ed39a76",
17
+ "required": true
18
+ }
19
+ ],
20
+ "outputArtifact": {
21
+ "kind": "patch",
22
+ "ref": "git:working-tree",
23
+ "contentHash": "sha256:3f9d6216dc7dcb53e6f29ad23433de97f0c172be6a6f46d574aa67e0edfd0789"
24
+ },
25
+ "evidenceGate": {
26
+ "requiredBeforeNextPhase": [
27
+ "changed_files",
28
+ "tests_run",
29
+ "open_risks",
30
+ "stop_conditions"
31
+ ],
32
+ "status": "pass",
33
+ "changedFiles": {
34
+ "count": 4,
35
+ "fileSetHash": "sha256:5f6c8b62349d527cd2e24c6913b8c5f0af8775be1a836bf942853c39efc11f91"
36
+ },
37
+ "testsRun": [
38
+ {
39
+ "name": "unit-tests",
40
+ "commandHash": "sha256:1f2cc9d3f4ce8cb118198c7da4d6951de2f21485af126eb1a0fe5f1d7e0139de",
41
+ "status": "pass"
42
+ },
43
+ {
44
+ "name": "typecheck",
45
+ "commandHash": "sha256:54d29f16fd215f74b4c03da61e37e71918f2e5c8db454a76f5f582b76471cb13",
46
+ "status": "pass"
47
+ }
48
+ ],
49
+ "openRisks": [
50
+ {
51
+ "riskClass": "manual_browser_flow_not_verified",
52
+ "severity": "medium",
53
+ "safeToContinue": true
54
+ }
55
+ ]
56
+ },
57
+ "droppedContext": [
58
+ {
59
+ "kind": "exploration_transcript",
60
+ "reason": "not authoritative after approved plan"
61
+ },
62
+ {
63
+ "kind": "rejected_design_option",
64
+ "reason": "kept as citation only; not input to verify phase"
65
+ }
66
+ ],
67
+ "stopConditions": []
68
+ }
@@ -0,0 +1,31 @@
1
+ # Skill install/load receipt example
2
+
3
+ This example is for setup tools that install Skills across multiple agents and then need to prove whether each target can discover/load the installed resource before the first real session.
4
+
5
+ It complements:
6
+
7
+ - `examples/install-plan-receipts/` — pre-write plan proof (`writes_started=false`);
8
+ - `examples/loaded-resource-boundary/` — runtime proof that an existing resource crossed discovery/attachment/injection/readability stages.
9
+
10
+ ## Smoke test
11
+
12
+ ```bash
13
+ node examples/skill-install-receipts/check-skill-install-receipt.mjs \
14
+ examples/skill-install-receipts/skill-install-receipt.json
15
+ ```
16
+
17
+ Expected output:
18
+
19
+ ```text
20
+ skill install receipt ok: 3 targets checked
21
+ ```
22
+
23
+ ## Review checklist
24
+
25
+ A useful receipt should answer:
26
+
27
+ - What installer/source ref was used, without credentials?
28
+ - Which agent targets and scopes were touched?
29
+ - Which targets were installed, skipped, discovered, deferred, injected, or readable?
30
+ - Was any required target unsafe before the first session?
31
+ - Did the receipt avoid raw skill bodies, prompts, transcripts, env dumps, secrets, and private paths?
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+
4
+ const file = process.argv[2] || new URL('./skill-install-receipt.json', import.meta.url);
5
+ const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
6
+
7
+ const allowedInstall = new Set(['installed', 'skipped', 'failed']);
8
+ const allowedDiscovery = new Set(['discovered', 'not_discovered', 'not_tested', 'failed']);
9
+ const allowedLoad = new Set(['injected', 'readable', 'activation_required', 'deferred', 'not_tested', 'failed']);
10
+ const allowedCost = new Set(['0-1k', '1k-5k', '5k-20k', 'over_budget', 'unknown']);
11
+ const requiredPrivacy = [
12
+ 'raw_skill_body',
13
+ 'raw_prompt',
14
+ 'transcript',
15
+ 'secrets',
16
+ 'env_dump',
17
+ 'private_absolute_path'
18
+ ];
19
+
20
+ function assert(condition, message) {
21
+ if (!condition) {
22
+ throw new Error(message);
23
+ }
24
+ }
25
+
26
+ assert(receipt.receipt_type === 'agent.skill_install_receipt.v1', 'unexpected receipt_type');
27
+ assert(receipt.run_id, 'missing run_id');
28
+ assert(receipt.installer?.name, 'missing installer.name');
29
+ assert(receipt.installer?.source?.kind, 'missing installer.source.kind');
30
+ assert(receipt.installer?.source?.ref, 'missing installer.source.ref');
31
+ assert(receipt.installer.source.credentials_in_ref === false, 'source ref must not carry credentials');
32
+ assert(Array.isArray(receipt.targets) && receipt.targets.length > 0, 'targets must be a non-empty array');
33
+ assert(Array.isArray(receipt.privacy_exclusions), 'missing privacy_exclusions');
34
+
35
+ for (const item of requiredPrivacy) {
36
+ assert(receipt.privacy_exclusions.includes(item), `privacy_exclusions must include ${item}`);
37
+ }
38
+
39
+ let computedOverallSafe = true;
40
+ for (const [index, target] of receipt.targets.entries()) {
41
+ assert(target.agent, `targets[${index}].agent missing`);
42
+ assert(['project', 'global', 'workspace', 'unknown'].includes(target.scope), `targets[${index}].scope invalid`);
43
+ assert(typeof target.required === 'boolean', `targets[${index}].required must be boolean`);
44
+ assert(allowedInstall.has(target.install_status), `targets[${index}].install_status invalid`);
45
+ assert(allowedDiscovery.has(target.discovery_status), `targets[${index}].discovery_status invalid`);
46
+ assert(allowedLoad.has(target.load_status), `targets[${index}].load_status invalid`);
47
+ assert(allowedCost.has(target.context_cost_bucket), `targets[${index}].context_cost_bucket invalid`);
48
+ assert(typeof target.safe_to_start_session === 'boolean', `targets[${index}].safe_to_start_session must be boolean`);
49
+
50
+ if (target.evidence) {
51
+ assert(target.evidence.raw_body_logged === false, `targets[${index}] must not log raw skill body`);
52
+ }
53
+
54
+ const requiredTargetUnsafe = target.required && (
55
+ target.install_status !== 'installed' ||
56
+ target.discovery_status !== 'discovered' ||
57
+ target.load_status === 'failed' ||
58
+ target.load_status === 'not_tested' ||
59
+ target.context_cost_bucket === 'over_budget' ||
60
+ target.safe_to_start_session !== true
61
+ );
62
+
63
+ if (requiredTargetUnsafe) {
64
+ computedOverallSafe = false;
65
+ }
66
+ }
67
+
68
+ assert(receipt.overall_safe_to_start_session === computedOverallSafe, 'overall_safe_to_start_session does not match required target safety');
69
+
70
+ const serialized = JSON.stringify(receipt).toLowerCase();
71
+ for (const forbidden of ['private_key', 'api_key=', 'ghp_', 'github_pat_', 'npm_', 'bearer ']) {
72
+ assert(!serialized.includes(forbidden), `receipt appears to include secret marker: ${forbidden}`);
73
+ }
74
+
75
+ console.log(`skill install receipt ok: ${receipt.targets.length} targets checked`);