pi-harness-runtime 1.1.320 → 1.1.322

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 (117) hide show
  1. package/RULES.md +19 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/dist/extension.d.ts.map +1 -1
  5. package/packages/agent-policy/dist/extension.js +33 -0
  6. package/packages/agent-policy/dist/extension.js.map +1 -1
  7. package/packages/agent-policy/dist/policy-loader.d.ts +11 -0
  8. package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -1
  9. package/packages/agent-policy/dist/policy-loader.js +44 -19
  10. package/packages/agent-policy/dist/policy-loader.js.map +1 -1
  11. package/packages/agent-policy/dist/ssh-guard.d.ts +51 -0
  12. package/packages/agent-policy/dist/ssh-guard.d.ts.map +1 -0
  13. package/packages/agent-policy/dist/ssh-guard.js +172 -0
  14. package/packages/agent-policy/dist/ssh-guard.js.map +1 -0
  15. package/packages/agent-policy/package.json +1 -1
  16. package/packages/agent-policy/src/extension.ts +23 -0
  17. package/packages/agent-policy/src/policy-loader.ts +49 -20
  18. package/packages/agent-policy/src/ssh-guard.ts +204 -0
  19. package/packages/agent-policy/test/policy-retention.test.ts +299 -0
  20. package/packages/architecture-generator/package.json +1 -1
  21. package/packages/auth/package.json +1 -1
  22. package/packages/autonomous-refactor/package.json +1 -1
  23. package/packages/autonomous-runtime/package.json +1 -1
  24. package/packages/cache-strategy/package.json +1 -1
  25. package/packages/capability-registry/package.json +1 -1
  26. package/packages/checkpoint/package.json +1 -1
  27. package/packages/cli-plugin-sdk/package.json +1 -1
  28. package/packages/clipboard/package.json +1 -1
  29. package/packages/clipboard-plugin/package.json +1 -1
  30. package/packages/code-generation/package.json +1 -1
  31. package/packages/code-review/package.json +1 -1
  32. package/packages/codex-adapter/package.json +1 -1
  33. package/packages/codex-watcher/package.json +1 -1
  34. package/packages/config-capture/package.json +1 -1
  35. package/packages/context-compiler/package.json +1 -1
  36. package/packages/context-discovery/package.json +1 -1
  37. package/packages/context-manager/package.json +1 -1
  38. package/packages/cookie-sanitizer/package.json +1 -1
  39. package/packages/cost-optimizer/package.json +1 -1
  40. package/packages/dependency-analyzer/package.json +1 -1
  41. package/packages/django-plugin/package.json +1 -1
  42. package/packages/doc-generator/package.json +1 -1
  43. package/packages/durable-review/package.json +1 -1
  44. package/packages/e2e-tester/package.json +1 -1
  45. package/packages/evaluation-engine/package.json +1 -1
  46. package/packages/evaluation-runner/package.json +1 -1
  47. package/packages/event-bus/package.json +1 -1
  48. package/packages/event-store/package.json +1 -1
  49. package/packages/experience-replay/package.json +1 -1
  50. package/packages/feedback-collector/package.json +1 -1
  51. package/packages/file-copy-helper/package.json +1 -1
  52. package/packages/framework-detector/package.json +1 -1
  53. package/packages/framework-plugin-sdk/package.json +1 -1
  54. package/packages/frappe-plugin/package.json +1 -1
  55. package/packages/generic-web-plugin/package.json +1 -1
  56. package/packages/health-monitor/package.json +1 -1
  57. package/packages/intent-analyzer/package.json +1 -1
  58. package/packages/jev-judge/package.json +1 -1
  59. package/packages/knowledge-graph/package.json +1 -1
  60. package/packages/knowledge-retrieval/package.json +1 -1
  61. package/packages/laravel-plugin/package.json +1 -1
  62. package/packages/learning-engine/package.json +1 -1
  63. package/packages/mcp-adapter/package.json +1 -1
  64. package/packages/memory-engine/package.json +1 -1
  65. package/packages/milestone-manager/package.json +1 -1
  66. package/packages/model-registry/package.json +1 -1
  67. package/packages/nextjs-plugin/package.json +1 -1
  68. package/packages/notification/package.json +1 -1
  69. package/packages/observability/package.json +1 -1
  70. package/packages/okf-indexer/package.json +1 -1
  71. package/packages/paperclip-integration/package.json +1 -1
  72. package/packages/performance-optimizer/package.json +1 -1
  73. package/packages/privilege-broker/package.json +1 -1
  74. package/packages/project-analyzer/package.json +1 -1
  75. package/packages/project-bootstrap/package.json +1 -1
  76. package/packages/projection-engine/package.json +1 -1
  77. package/packages/prompt-compiler/package.json +1 -1
  78. package/packages/prompt-versioning/package.json +1 -1
  79. package/packages/provider-adapter-sdk/package.json +1 -1
  80. package/packages/provider-router/package.json +1 -1
  81. package/packages/provider-selector/package.json +1 -1
  82. package/packages/providers/package.json +1 -1
  83. package/packages/qdrant-skills/package.json +1 -1
  84. package/packages/quota-manager/package.json +1 -1
  85. package/packages/rate-limiter/package.json +1 -1
  86. package/packages/react-vite-plugin/package.json +1 -1
  87. package/packages/release-manager/package.json +1 -1
  88. package/packages/requirement-compiler/package.json +1 -1
  89. package/packages/runtime/package.json +1 -1
  90. package/packages/scheduler/package.json +1 -1
  91. package/packages/scheduler-adapter/package.json +1 -1
  92. package/packages/session/package.json +1 -1
  93. package/packages/session-api/package.json +1 -1
  94. package/packages/session-export/package.json +1 -1
  95. package/packages/shared-context/package.json +1 -1
  96. package/packages/skill-mcp-client/package.json +1 -1
  97. package/packages/skill-registry/package.json +1 -1
  98. package/packages/skills/package.json +1 -1
  99. package/packages/sprint-planner/package.json +1 -1
  100. package/packages/subscription-engine/package.json +1 -1
  101. package/packages/task-compiler/package.json +1 -1
  102. package/packages/telegram/package.json +1 -1
  103. package/packages/tencentdb-memory/package.json +1 -1
  104. package/packages/tencentdb-sync/package.json +1 -1
  105. package/packages/test-data-generator/package.json +1 -1
  106. package/packages/test-generator/package.json +1 -1
  107. package/packages/todo-bd-sync/package.json +1 -1
  108. package/packages/token-estimation/package.json +1 -1
  109. package/packages/token-optimizer/package.json +1 -1
  110. package/packages/trajectory/package.json +1 -1
  111. package/packages/tui/package.json +1 -1
  112. package/packages/types/package.json +1 -1
  113. package/packages/webmcp-tester/package.json +1 -1
  114. package/packages/workflow-events/package.json +1 -1
  115. package/packages/workspace-scanner/package.json +1 -1
  116. package/packages/worktree/package.json +1 -1
  117. package/packages/write-review/package.json +1 -1
@@ -86,13 +86,33 @@ export class PolicyLoader {
86
86
 
87
87
  /**
88
88
  * Load policy for a specific target file (considers scoped rules).
89
+ * Resolves applicable rules from workspace toward the target directory.
89
90
  */
90
91
  async loadForTarget(projectRoot: string, targetPath?: string): Promise<PolicyManifest> {
91
- // The current loader collects only project-root and ancestor rules, all of
92
- // which apply to every target. Use the same manifest as harness_rules.
93
- // Filtering here mutated the cached manifest, dropped ancestor rules, and
94
- // hashed advisory sources that load() deliberately excludes from revision.
95
- return this.load(projectRoot);
92
+ // Load base manifest first
93
+ const baseManifest = await this.load(projectRoot);
94
+
95
+ // If no target path, return base manifest
96
+ if (!targetPath) {
97
+ return baseManifest;
98
+ }
99
+
100
+ // Determine target scope by finding the most specific matching rule
101
+ const targetDir = resolve(targetPath);
102
+
103
+ // Filter sources to only those applicable to this target
104
+ // Rules apply if their scope is empty (root) or is a prefix of the target path
105
+ const applicableSources = baseManifest.sources.filter(source => {
106
+ if (!source.scope) return true; // Root scope applies everywhere
107
+ const scopePath = resolve(projectRoot, source.scope);
108
+ return targetDir.startsWith(scopePath) || targetDir.startsWith(source.scope);
109
+ });
110
+
111
+ // Return manifest with filtered sources
112
+ return {
113
+ ...baseManifest,
114
+ sources: applicableSources,
115
+ };
96
116
  }
97
117
 
98
118
  /**
@@ -117,17 +137,13 @@ export class PolicyLoader {
117
137
  const harnessSources = await this.loadHarnessRules();
118
138
  sources.push(...harnessSources);
119
139
 
120
- // 2. Load project rules (if project is trusted)
140
+ // 2. Load project rules
141
+ // Load both AGENTS.md (mandatory if present) and RULES.md (advisory if present)
142
+ const projectSources = await this.loadProjectRules(projectRoot);
143
+ sources.push(...projectSources);
144
+
145
+ // Evaluate trust after loading rules (trust is determined by host, not just files)
121
146
  const trust = this.evaluateTrust(projectRoot);
122
-
123
- if (trust !== "unknown") {
124
- const projectSources = await this.loadProjectRules(projectRoot);
125
- sources.push(...projectSources);
126
- } else {
127
- // Project not trusted, but we still try to load AGENTS.md as advisory
128
- const untrustedSources = await this.loadProjectRules(projectRoot, true);
129
- sources.push(...untrustedSources);
130
- }
131
147
 
132
148
  // 3. Compute revision from mandatory sources
133
149
  const mandatorySources = sources.filter(s => s.priority === "mandatory");
@@ -251,26 +267,39 @@ export class PolicyLoader {
251
267
 
252
268
  /**
253
269
  * Evaluate trust level for a project.
270
+ *
271
+ * NOTE: Trust should ultimately be determined by host authorization (ctx.isProjectTrusted()),
272
+ * not by file presence alone. This method provides a fallback assessment.
273
+ * The presence of .git or package.json indicates a repository but does not confer trust
274
+ * without host authorization.
254
275
  */
255
276
  private evaluateTrust(projectRoot: string): "trusted" | "untrusted" | "unknown" {
277
+ // Check for trust indicators
256
278
  let trustIndicatorsFound = 0;
257
- let untrustIndicatorsFound = 0;
258
-
259
279
  for (const indicator of TRUST_INDICATORS) {
260
280
  if (existsSync(join(projectRoot, indicator))) {
261
281
  trustIndicatorsFound++;
262
282
  }
263
283
  }
264
284
 
265
- // Check for untrusted indicators (e.g., downloaded zip, unknown origin)
266
- // For now, presence of .git or package.json means trusted
285
+ // Trust is "unknown" unless explicitly set by host authorization
286
+ // File presence alone does not determine trust
267
287
  if (trustIndicatorsFound > 0) {
268
- return "trusted";
288
+ // Repository detected, but trust requires host authorization
289
+ return "unknown";
269
290
  }
270
291
 
271
292
  return "unknown";
272
293
  }
273
294
 
295
+ /**
296
+ * Check if project has explicit trust from host.
297
+ * This should be called with host authorization state.
298
+ */
299
+ hasExplicitTrust(projectRoot: string): boolean {
300
+ return this.evaluateTrust(projectRoot) === "trusted";
301
+ }
302
+
274
303
  /**
275
304
  * Infer the scope from a relative path.
276
305
  */
@@ -0,0 +1,204 @@
1
+ /**
2
+ * SSH Guard Tool
3
+ *
4
+ * Intercepts SSH commands and checks if the destination matches the local environment.
5
+ * Combines automatic checking with a fresh reminder when the agent proposes SSH.
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { createHash } from "node:crypto";
10
+ import dns from "node:dns";
11
+ import { promisify } from "node:util";
12
+
13
+ const lookupAsync = promisify(dns.lookup);
14
+
15
+ /**
16
+ * Result of SSH guard check
17
+ */
18
+ export interface SshGuardResult {
19
+ /** Whether SSH is allowed to proceed */
20
+ allowed: boolean;
21
+ /** Message explaining the decision */
22
+ message: string;
23
+ /** Whether the destination matches local */
24
+ isLocal: boolean;
25
+ /** Resolved destination address */
26
+ destinationAddress?: string;
27
+ }
28
+
29
+ /**
30
+ * Parse SSH destination from command line
31
+ */
32
+ function parseSshDestination(command: string): string | null {
33
+ // Match patterns like:
34
+ // ssh user@host
35
+ // ssh host
36
+ // ssh -p port user@host
37
+ // ssh host.domain.com
38
+ const match = command.match(/ssh\s+(?:-[a-z]+\s+)*((?:[a-zA-Z0-9_-]+\@)?[a-zA-Z0-9._-]+)/);
39
+ return match ? match[1] : null;
40
+ }
41
+
42
+ /**
43
+ * Get local IP addresses (cross-platform)
44
+ */
45
+ function getLocalAddresses(): string[] {
46
+ try {
47
+ const isWindows = process.platform === "win32";
48
+ let output: string;
49
+
50
+ if (isWindows) {
51
+ // Windows: use ipconfig
52
+ output = execSync("ipconfig", { encoding: "utf8", timeout: 5000, windowsHide: true });
53
+ } else {
54
+ // Unix-like: use ip address
55
+ output = execSync("ip -j address 2>/dev/null || ip address 2>/dev/null", {
56
+ encoding: "utf8",
57
+ timeout: 5000,
58
+ });
59
+ }
60
+
61
+ const addresses: string[] = ["127.0.0.1", "::1", "localhost"];
62
+
63
+ // Parse JSON output if available (Linux)
64
+ try {
65
+ const json = JSON.parse(output);
66
+ for (const iface of json) {
67
+ for (const addr of iface.addr_info || []) {
68
+ if (addr.local !== addr.peer) {
69
+ addresses.push(addr.local);
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ // Fallback to text parsing
75
+ if (isWindows) {
76
+ // Windows ipconfig: parse "IPv4 Address. . . . . . . . . . : X.X.X.X"
77
+ const windowsMatch = output.matchAll(/IPv4[^:]*:\s*([0-9.]+)/gi);
78
+ for (const match of windowsMatch) {
79
+ const ip = match[1];
80
+ if (ip !== "127.0.0.1") {
81
+ addresses.push(ip);
82
+ }
83
+ }
84
+ // Also check for IPv6
85
+ const ipv6Match = output.matchAll(/IPv6[^:]*:\s*([0-9a-f:]+)/gi);
86
+ for (const match of ipv6Match) {
87
+ addresses.push(match[1]);
88
+ }
89
+ } else {
90
+ // Unix-like: parse "inet X.X.X.X"
91
+ const ipMatch = output.match(/inet\s+([0-9.]+)/g);
92
+ if (ipMatch) {
93
+ for (const match of ipMatch) {
94
+ const ip = match.replace("inet ", "");
95
+ if (ip !== "127.0.0.1") {
96
+ addresses.push(ip);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return [...new Set(addresses)];
104
+ } catch {
105
+ return ["127.0.0.1", "::1", "localhost"];
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Resolve hostname to IP address
111
+ */
112
+ async function resolveHost(hostname: string): Promise<string | null> {
113
+ // Skip if already an IP
114
+ if (/^[0-9.]+$/.test(hostname)) return hostname;
115
+ if (/^[0-9a-f:]+$/i.test(hostname)) return hostname;
116
+
117
+ try {
118
+ const result = await lookupAsync(hostname);
119
+ return result.address;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if destination is local
127
+ */
128
+ export async function checkSshNecessity(command: string): Promise<SshGuardResult> {
129
+ const destination = parseSshDestination(command);
130
+
131
+ if (!destination) {
132
+ return {
133
+ allowed: true,
134
+ message: "Could not parse SSH destination",
135
+ isLocal: false,
136
+ };
137
+ }
138
+
139
+ const localAddresses = getLocalAddresses();
140
+ const resolved = await resolveHost(destination);
141
+
142
+ if (!resolved) {
143
+ return {
144
+ allowed: true,
145
+ message: `Could not resolve SSH destination: ${destination}`,
146
+ isLocal: false,
147
+ destinationAddress: undefined,
148
+ };
149
+ }
150
+
151
+ const isLocal = localAddresses.some(
152
+ (addr) => addr === resolved || resolved === addr
153
+ );
154
+
155
+ if (isLocal) {
156
+ return {
157
+ allowed: false,
158
+ message: `SSH destination ${destination} (${resolved}) matches this environment (local addresses: ${localAddresses.join(", ")}). Run the command locally, or explain why a separate SSH session is required.`,
159
+ isLocal: true,
160
+ destinationAddress: resolved,
161
+ };
162
+ }
163
+
164
+ return {
165
+ allowed: true,
166
+ message: `SSH to ${destination} (${resolved}) appears necessary - destination differs from local addresses`,
167
+ isLocal: false,
168
+ destinationAddress: resolved,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Tool definition for SSH guard
174
+ */
175
+ export const SshGuardTool = {
176
+ name: "ssh_guard_check",
177
+ description: "Check if SSH destination matches local environment before allowing SSH",
178
+ parameters: {
179
+ type: "object",
180
+ properties: {
181
+ command: {
182
+ type: "string",
183
+ description: "The SSH command to check",
184
+ },
185
+ },
186
+ required: ["command"],
187
+ },
188
+ } as const;
189
+
190
+ /**
191
+ * Execute SSH guard check
192
+ */
193
+ export async function executeSshGuard(
194
+ args: { command: string },
195
+ _api: unknown,
196
+ _context: unknown
197
+ ): Promise<{ allowed: boolean; message: string; isLocal: boolean }> {
198
+ const result = await checkSshNecessity(args.command);
199
+ return {
200
+ allowed: result.allowed,
201
+ message: result.message,
202
+ isLocal: result.isLocal,
203
+ };
204
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Policy Retention Tests
3
+ *
4
+ * Tests for lifecycle retention after compaction, reset, resume.
5
+ */
6
+
7
+ import { describe, it, before, after } from "node:test";
8
+ import assert from "node:assert";
9
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { randomUUID } from "node:crypto";
12
+ import type { PolicyManifest } from "../dist/types.js";
13
+ import { PolicyLoader } from "../dist/policy-loader.js";
14
+ import { getPolicyStore, resetPolicyStore } from "../dist/policy-store.js";
15
+
16
+ // Test workspace
17
+ const TEST_DIR = "/tmp/policy-retention-test-" + Date.now();
18
+ const AGENTS_MD = join(TEST_DIR, "AGENTS.md");
19
+ const RULES_MD = join(TEST_DIR, "RULES.md");
20
+
21
+ describe("Policy Retention Tests", () => {
22
+ before(() => {
23
+ mkdirSync(TEST_DIR, { recursive: true });
24
+ resetPolicyStore();
25
+ });
26
+
27
+ after(() => {
28
+ try {
29
+ rmSync(TEST_DIR, { recursive: true, force: true });
30
+ } catch {
31
+ // ignore cleanup errors
32
+ }
33
+ resetPolicyStore();
34
+ });
35
+
36
+ // --- Session Epoch Tests ---
37
+ describe("Session epochs", () => {
38
+ it("should track session epochs separately", async () => {
39
+ const store = getPolicyStore();
40
+ const session1 = "session-1-" + randomUUID().slice(0, 8);
41
+ const session2 = "session-2-" + randomUUID().slice(0, 8);
42
+
43
+ // Create a manifest
44
+ const manifest: PolicyManifest = {
45
+ revision: "abc123",
46
+ projectRoot: TEST_DIR,
47
+ trust: "unknown",
48
+ coverage: "complete",
49
+ sources: [],
50
+ loadedAt: Date.now(),
51
+ };
52
+
53
+ // Note delivery for session 1
54
+ const receipt1 = store.noteDelivered(session1, manifest, "system_prompt");
55
+ assert.strictEqual(receipt1.sessionId, session1);
56
+
57
+ // Session 2 should NOT have a receipt
58
+ const receipt2 = store.getReceipt(session2, TEST_DIR);
59
+ assert.strictEqual(receipt2, null);
60
+ });
61
+
62
+ it("should not leak receipts between sessions after reset", async () => {
63
+ const store = getPolicyStore();
64
+ const sessionId = "session-reset-" + randomUUID().slice(0, 8);
65
+
66
+ const manifest: PolicyManifest = {
67
+ revision: "abc123",
68
+ projectRoot: TEST_DIR,
69
+ trust: "unknown",
70
+ coverage: "complete",
71
+ sources: [],
72
+ loadedAt: Date.now(),
73
+ };
74
+
75
+ // Deliver to session
76
+ store.noteDelivered(sessionId, manifest, "system_prompt");
77
+
78
+ // Clear session
79
+ store.clearSession(sessionId);
80
+
81
+ // Receipt should be gone
82
+ const receipt = store.getReceipt(sessionId, TEST_DIR);
83
+ assert.strictEqual(receipt, null);
84
+ });
85
+ });
86
+
87
+ // --- Source Change Tests ---
88
+ describe("Source changes", () => {
89
+ it("should invalidate receipts when AGENTS.md changes", async () => {
90
+ const store = getPolicyStore();
91
+ const sessionId = "session-change-" + randomUUID().slice(0, 8);
92
+
93
+ // Write initial content
94
+ writeFileSync(AGENTS_MD, "# Original Agents\n\nFollow the rules.", "utf8");
95
+
96
+ const loader = new PolicyLoader({
97
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
98
+ });
99
+
100
+ // Load and deliver
101
+ const manifest1 = await loader.load(TEST_DIR);
102
+ store.noteDelivered(sessionId, manifest1, "system_prompt");
103
+
104
+ // Modify file
105
+ writeFileSync(AGENTS_MD, "# Updated Agents\n\nDifferent rules.", "utf8");
106
+
107
+ // Invalidate cache
108
+ loader.invalidateCache(TEST_DIR);
109
+
110
+ // Load new manifest
111
+ const manifest2 = await loader.load(TEST_DIR);
112
+
113
+ // Revision should be different
114
+ assert.notStrictEqual(manifest1.revision, manifest2.revision);
115
+
116
+ // Old receipt should NOT cover new manifest
117
+ const hasCoverage = store.hasValidReceipt(sessionId, manifest2);
118
+ assert.strictEqual(hasCoverage, false);
119
+ });
120
+
121
+ it("should include RULES.md in sources", async () => {
122
+ // Write RULES.md
123
+ writeFileSync(RULES_MD, "# Rules\n\nThese are rules.", "utf8");
124
+
125
+ const loader = new PolicyLoader({
126
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
127
+ });
128
+
129
+ const manifest = await loader.load(TEST_DIR);
130
+
131
+ // RULES.md should be included as advisory at minimum
132
+ const rulesSource = manifest.sources.find(s => s.path.includes("RULES.md"));
133
+ assert.ok(rulesSource, "RULES.md should be included in sources");
134
+
135
+ // Clean up
136
+ unlinkSync(RULES_MD);
137
+ });
138
+ });
139
+
140
+ // --- Bounded Delivery Tests ---
141
+ describe("Bounded delivery", () => {
142
+ it("should track exact revision and source coverage", async () => {
143
+ const store = getPolicyStore();
144
+ const sessionId = "session-exact-" + randomUUID().slice(0, 8);
145
+
146
+ writeFileSync(AGENTS_MD, "# Agents\n\nContent.", "utf8");
147
+
148
+ const loader = new PolicyLoader({
149
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
150
+ });
151
+
152
+ const manifest = await loader.load(TEST_DIR);
153
+
154
+ // Note delivery with specific sections
155
+ const receipt = store.noteDelivered(sessionId, manifest, "tool", ["section-1"]);
156
+
157
+ assert.strictEqual(receipt.revision, manifest.revision);
158
+ assert.deepStrictEqual(receipt.sectionsDelivered, ["section-1"]);
159
+ });
160
+
161
+ it("should reject acknowledgement of wrong revision", async () => {
162
+ const store = getPolicyStore();
163
+ const sessionId = "session-wrong-rev-" + randomUUID().slice(0, 8);
164
+
165
+ writeFileSync(AGENTS_MD, "# Agents v1\n\nContent.", "utf8");
166
+
167
+ const loader = new PolicyLoader({
168
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
169
+ });
170
+
171
+ const manifest1 = await loader.load(TEST_DIR);
172
+ store.noteDelivered(sessionId, manifest1, "system_prompt");
173
+
174
+ // Update content
175
+ writeFileSync(AGENTS_MD, "# Agents v2\n\nDifferent content.", "utf8");
176
+ loader.invalidateCache(TEST_DIR);
177
+ const manifest2 = await loader.load(TEST_DIR);
178
+
179
+ // Old receipt should NOT cover new manifest
180
+ const hasCoverage = store.hasValidReceipt(sessionId, manifest2);
181
+ assert.strictEqual(hasCoverage, false);
182
+ });
183
+ });
184
+
185
+ // --- Trust Evaluation Tests ---
186
+ describe("Trust evaluation", () => {
187
+ it("should evaluate trust appropriately", async () => {
188
+ // Create a .git directory (indicates repository)
189
+ mkdirSync(join(TEST_DIR, ".git"), { recursive: true });
190
+
191
+ const loader = new PolicyLoader({
192
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
193
+ });
194
+
195
+ const manifest = await loader.load(TEST_DIR);
196
+
197
+ // Trust should be evaluated
198
+ assert.ok(["trusted", "untrusted", "unknown"].includes(manifest.trust));
199
+
200
+ // Clean up
201
+ rmSync(join(TEST_DIR, ".git"), { recursive: true, force: true });
202
+ });
203
+ });
204
+
205
+ // --- Compaction Recovery Tests ---
206
+ describe("Compaction recovery", () => {
207
+ it("should invalidate receipts on session_compact event", async () => {
208
+ const store = getPolicyStore();
209
+ const sessionId = "session-compact-" + randomUUID().slice(0, 8);
210
+
211
+ writeFileSync(AGENTS_MD, "# Agents\n\nContent.", "utf8");
212
+
213
+ const loader = new PolicyLoader({
214
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
215
+ });
216
+
217
+ const manifest = await loader.load(TEST_DIR);
218
+ store.noteDelivered(sessionId, manifest, "system_prompt");
219
+
220
+ // Simulate compaction: invalidate all
221
+ store.invalidateAll();
222
+
223
+ // Receipt should be gone
224
+ const receipt = store.getReceipt(sessionId, TEST_DIR);
225
+ assert.strictEqual(receipt, null);
226
+ });
227
+ });
228
+
229
+ // --- Nested Scope Tests ---
230
+ describe("Nested scope resolution", () => {
231
+ it("should load scoped rules for subdirectories", async () => {
232
+ // Create nested structure
233
+ mkdirSync(join(TEST_DIR, "packages", "my-pkg"), { recursive: true });
234
+ writeFileSync(AGENTS_MD, "# Root Agents\n\nRoot rules.", "utf8");
235
+ writeFileSync(join(TEST_DIR, "packages", "AGENTS.md"), "# Package Agents\n\nPackage rules.", "utf8");
236
+
237
+ const loader = new PolicyLoader({
238
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
239
+ });
240
+
241
+ const manifest = await loader.load(TEST_DIR);
242
+
243
+ // Should have both root and package rules
244
+ assert.ok(manifest.sources.length >= 2, "Should include both root and package rules");
245
+
246
+ // Clean up nested structure
247
+ rmSync(join(TEST_DIR, "packages"), { recursive: true, force: true });
248
+ });
249
+ });
250
+
251
+ // --- Delivery Deduplication Tests ---
252
+ describe("Delivery deduplication", () => {
253
+ it("should not create duplicate receipts in same session", async () => {
254
+ const store = getPolicyStore();
255
+ const sessionId = "session-dedup-" + randomUUID().slice(0, 8);
256
+
257
+ writeFileSync(AGENTS_MD, "# Agents\n\nContent.", "utf8");
258
+
259
+ const loader = new PolicyLoader({
260
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
261
+ });
262
+
263
+ const manifest = await loader.load(TEST_DIR);
264
+
265
+ // First delivery
266
+ const receipt1 = store.noteDelivered(sessionId, manifest, "system_prompt");
267
+
268
+ // Second delivery should update, not create duplicate
269
+ const receipt2 = store.noteDelivered(sessionId, manifest, "system_prompt");
270
+
271
+ // Should be same session
272
+ assert.strictEqual(receipt1.sessionId, receipt2.sessionId);
273
+
274
+ // Should have same revision
275
+ assert.strictEqual(receipt1.revision, receipt2.revision);
276
+ });
277
+ });
278
+
279
+ // --- Partial Read Tracking Tests ---
280
+ describe("Partial read tracking", () => {
281
+ it("should track which sections were actually delivered", async () => {
282
+ const store = getPolicyStore();
283
+ const sessionId = "session-partial-" + randomUUID().slice(0, 8);
284
+
285
+ writeFileSync(AGENTS_MD, "# Agents\n\n## Section 1\n\nContent 1.\n\n## Section 2\n\nContent 2.", "utf8");
286
+
287
+ const loader = new PolicyLoader({
288
+ harnessRulesPath: new URL(`file://${AGENTS_MD}`),
289
+ });
290
+
291
+ const manifest = await loader.load(TEST_DIR);
292
+
293
+ // Note partial delivery (only section 1)
294
+ const receipt = store.noteDelivered(sessionId, manifest, "tool", ["section-1"]);
295
+
296
+ assert.deepStrictEqual(receipt.sectionsDelivered, ["section-1"]);
297
+ });
298
+ });
299
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cli-plugin-sdk",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "type": "module",
5
5
  "description": "CLI Plugin SDK for pi-harness — install, list, remove, update, invoke plugins",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.320",
3
+ "version": "1.1.322",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {