@piagent/platform 1.2.9 → 1.2.10

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 (40) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +4 -4
  3. package/catalog/capabilities.json +2 -2
  4. package/docs/command-reference-vietnamese.md +7 -4
  5. package/docs/context-engine.md +23 -4
  6. package/docs/distribution-standard.md +7 -7
  7. package/docs/mcp-and-tools.md +7 -0
  8. package/docs/operator-manual-vietnamese.md +27 -11
  9. package/docs/publishing-for-teams.md +1 -0
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +7 -2
  12. package/docs/subagents-and-multiagent.md +1 -1
  13. package/docs/team-onboarding.md +2 -2
  14. package/docs-site/architecture.html +1 -1
  15. package/docs-site/commands.html +1 -1
  16. package/docs-site/content/context-engine.html +8 -1
  17. package/docs-site/content/security.html +1 -0
  18. package/docs-site/context-engine.html +9 -2
  19. package/docs-site/index.html +1 -1
  20. package/docs-site/mcp-approval.html +1 -1
  21. package/docs-site/mcp-auth.html +1 -1
  22. package/docs-site/mcp-servers.html +1 -1
  23. package/docs-site/mcp.html +1 -1
  24. package/docs-site/permissions.html +1 -1
  25. package/docs-site/profiles.html +1 -1
  26. package/docs-site/quickstart.html +1 -1
  27. package/docs-site/runtime.html +1 -1
  28. package/docs-site/scope.html +1 -1
  29. package/docs-site/security.html +2 -1
  30. package/docs-site/team.html +1 -1
  31. package/docs-site/workflows.html +1 -1
  32. package/package.json +1 -1
  33. package/packages/piagent-core/README.md +6 -1
  34. package/packages/piagent-core/extensions/context-engine.js +134 -69
  35. package/packages/piagent-core/extensions/context-index-policy.js +1 -1
  36. package/packages/piagent-core/extensions/piagent-guard.ts +191 -34
  37. package/packages/piagent-core/mcp/mcp-config-layers.js +52 -6
  38. package/packages/piagent-core/mcp/mcp-tool-naming.js +12 -1
  39. package/packages/piagent-core/package.json +1 -1
  40. package/packages/piagent-core/subagents/piagent-reviewer.md +3 -3
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -128,6 +128,7 @@
128
128
  <ul>
129
129
  <li>Có thể bị ảnh hưởng bởi symlink, encoding, nested payload, shell expansion hoặc tool mới chưa được policy hoá.</li>
130
130
  <li>Redaction là backstop giảm rò rỉ, không phải giấy phép để đưa secret thật vào prompt/output.</li>
131
+ <li>Context index là local cache có source body: mode owner-only giảm cross-account access, nhưng process chạy cùng OS account vẫn cần được tin cậy.</li>
131
132
  <li>Workload không tin cậy cần container/VM với giới hạn filesystem, process, network và credential.</li>
132
133
  </ul>
133
134
  </div>
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.9 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.10 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -147,10 +147,15 @@ Runtime task tools write local state to:
147
147
 
148
148
  Project-local state belongs in `.pi/.gitignore`.
149
149
 
150
+ The Context Engine SQLite index contains allowed source bodies. Its directory is
151
+ kept at mode `0700`, with database/WAL/SHM at `0600`; core and FTS5 secure-delete
152
+ cover normal refreshes, while exclusion-policy changes trigger a retryable FTS
153
+ rebuild and database vacuum before the new policy is considered clean.
154
+
150
155
  Passing final gates require an observed exit `0` command that exactly matches one entry in `task.verifyCommands`. Other observed commands are traceable but advisory.
151
156
 
152
157
  Raw path-like tool access to protected paths is blocked before execution. This includes Pi built-ins (`read`, `write`, `edit`, `grep`, `find`, `ls`) and custom/MCP tools with nested path-like strings, arrays, or `file://` URIs. Path-like strings are percent-decoded once, and input nesting above `MAX_TOOL_INPUT_INSPECTION_DEPTH=32` fails closed. Known content fields such as `content`, `query`, `pattern`, `text`, and `command` are excluded from generic extraction. `grep.glob` and `find.pattern` are checked when they explicitly target protected paths, while broad `grep`, `find`, and `ls` results are filtered so protected content lines or path metadata are redacted before reaching the model.
153
158
 
154
159
  Raw `bash` access to protected paths is blocked through shell path extraction. `.pi/piagent-state/**` and `.pi/piagent-profile.json` are self-protected; use `piagent_context` and piagent task tools for governed access.
155
160
 
156
- The input hook also detects local image paths in chat prompts. Supported formats are `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.bmp`, including macOS screenshot paths under `/var/folders/...`. Up to 4 images are attached per input, with an 8 MB per-image cap. Oversized images should be read through Pi's `read` tool so Pi can resize them.
161
+ The input hook also detects local image paths in chat prompts. Supported formats are `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.bmp`. The canonical target must be inside the project or a directory granted through `additionalReadRoots`/`PIAGENT_ADDITIONAL_READ_ROOTS`; protected paths and project files outside the resolved filesystem read scope remain blocked. File bytes are sniffed before attachment, and the checked file identity must still match when opened. Up to 4 images are attached per input, with an 8 MB per-image cap. Oversized images should be read through Pi's `read` tool so Pi can resize them.
@@ -92,6 +92,21 @@ export function contextEnginePaths(cwd) {
92
92
  };
93
93
  }
94
94
 
95
+ function hardenContextDatabaseArtifacts(cwd) {
96
+ const database = contextEnginePaths(cwd).database;
97
+ for (const artifact of [database, `${database}-wal`, `${database}-shm`]) {
98
+ try {
99
+ const stat = fs.lstatSync(artifact);
100
+ if (!stat.isFile()) {
101
+ throw new Error(`Context index storage path must be a regular file: ${artifact}`);
102
+ }
103
+ fs.chmodSync(artifact, 0o600);
104
+ } catch (error) {
105
+ if (error?.code !== "ENOENT") throw error;
106
+ }
107
+ }
108
+ }
109
+
95
110
  async function loadSqlite() {
96
111
  if (!sqliteModulePromise) {
97
112
  sqliteModulePromise = (async () => {
@@ -117,53 +132,85 @@ async function loadSqlite() {
117
132
  async function openDatabase(cwd, { create = true } = {}) {
118
133
  const paths = contextEnginePaths(cwd);
119
134
  if (!create && !fs.existsSync(paths.database)) return undefined;
120
- fs.mkdirSync(paths.root, { recursive: true });
135
+ fs.mkdirSync(paths.root, { recursive: true, mode: 0o700 });
136
+ if (!fs.lstatSync(paths.root).isDirectory()) {
137
+ throw new Error(`Context index storage root must be a directory: ${paths.root}`);
138
+ }
139
+ fs.chmodSync(paths.root, 0o700);
140
+ hardenContextDatabaseArtifacts(cwd);
141
+ const descriptor = fs.openSync(
142
+ paths.database,
143
+ fs.constants.O_RDWR
144
+ | (create ? fs.constants.O_CREAT : 0)
145
+ | (fs.constants.O_NOFOLLOW ?? 0),
146
+ 0o600
147
+ );
148
+ try {
149
+ fs.fchmodSync(descriptor, 0o600);
150
+ } finally {
151
+ fs.closeSync(descriptor);
152
+ }
121
153
  const { DatabaseSync } = await loadSqlite();
122
154
  const db = new DatabaseSync(paths.database);
123
- db.exec(`
124
- PRAGMA journal_mode = WAL;
125
- PRAGMA synchronous = NORMAL;
126
- PRAGMA foreign_keys = ON;
127
- CREATE TABLE IF NOT EXISTS metadata (
128
- key TEXT PRIMARY KEY,
129
- value TEXT NOT NULL
130
- );
131
- CREATE TABLE IF NOT EXISTS files (
132
- path TEXT PRIMARY KEY,
133
- hash TEXT NOT NULL,
134
- bytes INTEGER NOT NULL,
135
- mtime_ms REAL NOT NULL,
136
- language TEXT NOT NULL,
137
- indexed_at TEXT NOT NULL
138
- );
139
- CREATE TABLE IF NOT EXISTS symbols (
140
- id INTEGER PRIMARY KEY,
141
- file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
142
- name TEXT NOT NULL,
143
- kind TEXT NOT NULL,
144
- line INTEGER NOT NULL,
145
- end_line INTEGER NOT NULL,
146
- signature TEXT NOT NULL
147
- );
148
- CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
149
- CREATE INDEX IF NOT EXISTS symbols_file_idx ON symbols(file_path);
150
- CREATE TABLE IF NOT EXISTS imports (
151
- id INTEGER PRIMARY KEY,
152
- file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
153
- specifier TEXT NOT NULL,
154
- target_path TEXT,
155
- line INTEGER NOT NULL
156
- );
157
- CREATE INDEX IF NOT EXISTS imports_file_idx ON imports(file_path);
158
- CREATE INDEX IF NOT EXISTS imports_target_idx ON imports(target_path);
159
- CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
160
- path UNINDEXED,
161
- body,
162
- symbol_names,
163
- tokenize = 'unicode61 remove_diacritics 2'
164
- );
165
- `);
166
- return db;
155
+ try {
156
+ db.exec(`
157
+ PRAGMA journal_mode = WAL;
158
+ PRAGMA synchronous = NORMAL;
159
+ PRAGMA foreign_keys = ON;
160
+ PRAGMA secure_delete = ON;
161
+ CREATE TABLE IF NOT EXISTS metadata (
162
+ key TEXT PRIMARY KEY,
163
+ value TEXT NOT NULL
164
+ );
165
+ CREATE TABLE IF NOT EXISTS files (
166
+ path TEXT PRIMARY KEY,
167
+ hash TEXT NOT NULL,
168
+ bytes INTEGER NOT NULL,
169
+ mtime_ms REAL NOT NULL,
170
+ language TEXT NOT NULL,
171
+ indexed_at TEXT NOT NULL
172
+ );
173
+ CREATE TABLE IF NOT EXISTS symbols (
174
+ id INTEGER PRIMARY KEY,
175
+ file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
176
+ name TEXT NOT NULL,
177
+ kind TEXT NOT NULL,
178
+ line INTEGER NOT NULL,
179
+ end_line INTEGER NOT NULL,
180
+ signature TEXT NOT NULL
181
+ );
182
+ CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
183
+ CREATE INDEX IF NOT EXISTS symbols_file_idx ON symbols(file_path);
184
+ CREATE TABLE IF NOT EXISTS imports (
185
+ id INTEGER PRIMARY KEY,
186
+ file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE,
187
+ specifier TEXT NOT NULL,
188
+ target_path TEXT,
189
+ line INTEGER NOT NULL
190
+ );
191
+ CREATE INDEX IF NOT EXISTS imports_file_idx ON imports(file_path);
192
+ CREATE INDEX IF NOT EXISTS imports_target_idx ON imports(target_path);
193
+ CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts5(
194
+ path UNINDEXED,
195
+ body,
196
+ symbol_names,
197
+ tokenize = 'unicode61 remove_diacritics 2'
198
+ );
199
+ `);
200
+ const ftsSecureDelete = db.prepare(`
201
+ SELECT v
202
+ FROM file_fts_config
203
+ WHERE k = 'secure-delete'
204
+ `).get();
205
+ if (Number(ftsSecureDelete?.v) !== 1) {
206
+ db.exec("INSERT INTO file_fts(file_fts, rank) VALUES('secure-delete', 1)");
207
+ }
208
+ hardenContextDatabaseArtifacts(cwd);
209
+ return db;
210
+ } catch (error) {
211
+ db.close();
212
+ throw error;
213
+ }
167
214
  }
168
215
 
169
216
  function metadataMap(db) {
@@ -465,6 +512,7 @@ export function classifyContextTask(prompt) {
465
512
 
466
513
  export async function buildContextIndexV2(cwd, options = {}) {
467
514
  const excludePatterns = requireExplicitExcludePatterns(options, "buildContextIndexV2");
515
+ const excludeDigest = contextIndexExcludeDigest(excludePatterns);
468
516
  const startedAt = Date.now();
469
517
  const projectRoot = fs.realpathSync.native(cwd);
470
518
  const directoryCache = new Map();
@@ -473,7 +521,10 @@ export async function buildContextIndexV2(cwd, options = {}) {
473
521
  const listed = gitFileList(cwd) ?? walkedFileList(cwd, maxFiles);
474
522
  const candidates = listed.filter((file) => shouldIndexPath(file, { ...options, excludePatterns })).slice(0, maxFiles).sort();
475
523
  const db = await openDatabase(cwd);
524
+ const previousMetadata = metadataMap(db);
476
525
  const existing = new Map(db.prepare("SELECT path, hash, bytes, mtime_ms FROM files").all().map((row) => [row.path, row]));
526
+ const purgeStaleContent = Boolean(previousMetadata.builtAt || existing.size > 0)
527
+ && (previousMetadata.purgePending === "1" || previousMetadata.excludeDigest !== excludeDigest);
477
528
  const current = new Set();
478
529
  const currentHashes = new Map();
479
530
  const changed = [];
@@ -525,8 +576,10 @@ export async function buildContextIndexV2(cwd, options = {}) {
525
576
  }
526
577
 
527
578
  const removed = [...existing.keys()].filter((file) => !current.has(file));
528
- db.exec("BEGIN IMMEDIATE");
579
+ let transactionOpen = false;
529
580
  try {
581
+ db.exec("BEGIN IMMEDIATE");
582
+ transactionOpen = true;
530
583
  const deleteFile = db.prepare("DELETE FROM files WHERE path = ?");
531
584
  const deleteFts = db.prepare("DELETE FROM file_fts WHERE path = ?");
532
585
  for (const relativePath of [...removed, ...changed.map((item) => item.relativePath)]) {
@@ -574,12 +627,28 @@ export async function buildContextIndexV2(cwd, options = {}) {
574
627
  importCount: counts.imports,
575
628
  maxFileBytes,
576
629
  excludePatterns: JSON.stringify(excludePatterns),
577
- excludeDigest: contextIndexExcludeDigest(excludePatterns),
630
+ excludeDigest,
578
631
  excludePolicyVersion: contextIndexExcludePolicyVersion(),
632
+ purgePending: purgeStaleContent ? 1 : 0,
579
633
  skippedPaths: JSON.stringify(skippedPaths)
580
634
  });
581
635
  db.exec("COMMIT");
582
- db.close();
636
+ transactionOpen = false;
637
+ if (purgeStaleContent) {
638
+ if (typeof options.onPurgeStart === "function") await options.onPurgeStart();
639
+ db.exec("INSERT INTO file_fts(file_fts) VALUES('rebuild')");
640
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
641
+ db.exec("VACUUM");
642
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
643
+ db.exec("BEGIN IMMEDIATE");
644
+ transactionOpen = true;
645
+ setMetadata(db, {
646
+ purgePending: 0,
647
+ purgedAt: nowIso()
648
+ });
649
+ db.exec("COMMIT");
650
+ transactionOpen = false;
651
+ }
583
652
  return {
584
653
  schemaVersion: INDEX_SCHEMA_VERSION,
585
654
  builtAt: indexedAt,
@@ -590,25 +659,24 @@ export async function buildContextIndexV2(cwd, options = {}) {
590
659
  removed: removed.length,
591
660
  skippedLarge,
592
661
  skippedBinary,
662
+ purgedStaleContent: purgeStaleContent,
593
663
  ...counts
594
664
  };
595
665
  } catch (error) {
596
- db.exec("ROLLBACK");
597
- db.close();
666
+ if (transactionOpen) db.exec("ROLLBACK");
598
667
  throw error;
668
+ } finally {
669
+ db.close();
670
+ hardenContextDatabaseArtifacts(cwd);
599
671
  }
600
672
  }
601
673
 
602
674
  export async function contextIndexV2Status(cwd, options = {}) {
675
+ const expectedExcludePatterns = requireExplicitExcludePatterns(options, "contextIndexV2Status");
676
+ const expectedExcludeDigest = contextIndexExcludeDigest(expectedExcludePatterns);
603
677
  const paths = contextEnginePaths(cwd);
604
678
  const projectRoot = fs.realpathSync.native(cwd);
605
679
  const directoryCache = new Map();
606
- const expectedExcludePatterns = options.excludePatterns === undefined
607
- ? undefined
608
- : normalizeContextIndexExcludePatterns(options.excludePatterns);
609
- const expectedExcludeDigest = expectedExcludePatterns
610
- ? contextIndexExcludeDigest(expectedExcludePatterns)
611
- : undefined;
612
680
  if (!fs.existsSync(paths.database)) {
613
681
  return {
614
682
  schemaVersion: INDEX_SCHEMA_VERSION,
@@ -617,6 +685,7 @@ export async function contextIndexV2Status(cwd, options = {}) {
617
685
  stale: false,
618
686
  policyStale: false,
619
687
  expectedExcludeDigest,
688
+ purgePending: false,
620
689
  warnings: ["index-v2 missing"]
621
690
  };
622
691
  }
@@ -628,20 +697,14 @@ export async function contextIndexV2Status(cwd, options = {}) {
628
697
  const symbols = Number(db.prepare("SELECT COUNT(*) count FROM symbols").get().count);
629
698
  const imports = Number(db.prepare("SELECT COUNT(*) count FROM imports").get().count);
630
699
  const indexedPaths = new Set(fileRows.map((row) => row.path));
631
- let storedExcludePatterns = [];
632
- try {
633
- const parsed = JSON.parse(metadata.excludePatterns ?? "[]");
634
- if (Array.isArray(parsed)) storedExcludePatterns = normalizeContextIndexExcludePatterns(parsed);
635
- } catch {
636
- storedExcludePatterns = [];
637
- }
638
- const activeExcludePatterns = expectedExcludePatterns ?? storedExcludePatterns;
639
700
  const storedExcludeDigest = metadata.excludeDigest;
640
- const policyStale = expectedExcludeDigest !== undefined
641
- && (typeof storedExcludeDigest !== "string" || storedExcludeDigest !== expectedExcludeDigest);
701
+ const purgePending = metadata.purgePending === "1";
702
+ const excludeDigestStale = typeof storedExcludeDigest !== "string"
703
+ || storedExcludeDigest !== expectedExcludeDigest;
704
+ const policyStale = purgePending || excludeDigestStale;
642
705
  const statChanged = [];
643
706
  for (const row of fileRows) {
644
- if (!shouldIndexPath(row.path, { excludePatterns: activeExcludePatterns })) continue;
707
+ if (!shouldIndexPath(row.path, { excludePatterns: expectedExcludePatterns })) continue;
645
708
  const file = projectFileInfo(cwd, row.path, projectRoot, directoryCache);
646
709
  if (
647
710
  !file
@@ -660,11 +723,12 @@ export async function contextIndexV2Status(cwd, options = {}) {
660
723
  }
661
724
  const listed = gitFileList(cwd);
662
725
  const newlyVisible = listed
663
- ? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns: activeExcludePatterns }))
726
+ ? listed.filter((file) => !indexedPaths.has(file) && !skippedPaths.has(file) && shouldIndexPath(file, { excludePatterns: expectedExcludePatterns }))
664
727
  : [];
665
728
  const stalePaths = [...new Set([...statChanged, ...newlyVisible])];
666
729
  const warnings = [];
667
- if (policyStale) warnings.push("context index exclusion policy changed");
730
+ if (purgePending) warnings.push("context index storage purge pending");
731
+ if (excludeDigestStale) warnings.push("context index exclusion policy changed");
668
732
  if (stalePaths.length > 0) warnings.push(`${stalePaths.length} changed file(s) since the last index build`);
669
733
  return {
670
734
  schemaVersion: Number(metadata.schemaVersion ?? INDEX_SCHEMA_VERSION),
@@ -676,6 +740,7 @@ export async function contextIndexV2Status(cwd, options = {}) {
676
740
  imports,
677
741
  stale: policyStale || stalePaths.length > 0,
678
742
  policyStale,
743
+ purgePending,
679
744
  excludePolicyVersion: metadata.excludePolicyVersion,
680
745
  excludeDigest: storedExcludeDigest,
681
746
  expectedExcludeDigest,
@@ -6,7 +6,7 @@ import { resolveProjectProfileDocument } from "../capabilities/project-profile.j
6
6
 
7
7
  const DEFAULT_CONTEXT_INDEX_PATH = ".pi/context-index.json";
8
8
  const CONTEXT_ENGINE_STATE_PATTERN = ".pi/piagent-state/**";
9
- const EXCLUDE_POLICY_VERSION = 1;
9
+ const EXCLUDE_POLICY_VERSION = 2;
10
10
 
11
11
  function stringArray(value) {
12
12
  return Array.isArray(value)