@piagent/platform 1.2.10 → 1.2.12

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 (149) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +29 -4
  3. package/adapters/backend-api/profile.json +1 -1
  4. package/adapters/be-readonly-fe/profile.json +2 -2
  5. package/adapters/data/profile.json +1 -1
  6. package/adapters/devops/profile.json +1 -3
  7. package/adapters/docs/profile.json +1 -1
  8. package/adapters/fullstack/profile.json +2 -4
  9. package/adapters/generic/profile.json +1 -1
  10. package/adapters/mobile/profile.json +1 -2
  11. package/adapters/node-typescript/profile.json +1 -3
  12. package/benchmarks/core-v1/suite.json +51 -0
  13. package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
  14. package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
  15. package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
  16. package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
  17. package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
  18. package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
  19. package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
  20. package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
  21. package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
  22. package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
  23. package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
  24. package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
  25. package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
  26. package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
  27. package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
  28. package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
  29. package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
  30. package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
  31. package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
  32. package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
  33. package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
  34. package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
  35. package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
  36. package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
  37. package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
  38. package/benchmarks/production-v1/grade.mjs +220 -0
  39. package/benchmarks/production-v1/project/README.md +7 -0
  40. package/benchmarks/production-v1/project/config/service.json +4 -0
  41. package/benchmarks/production-v1/project/data/audit-ledger.jsonl +1 -0
  42. package/benchmarks/production-v1/project/docs/ops.md +3 -0
  43. package/benchmarks/production-v1/project/logs/incident.log +2 -0
  44. package/benchmarks/production-v1/project/package.json +11 -0
  45. package/benchmarks/production-v1/project/scripts/check.mjs +21 -0
  46. package/benchmarks/production-v1/project/src/backend/auth.js +3 -0
  47. package/benchmarks/production-v1/project/src/backend/cache.js +15 -0
  48. package/benchmarks/production-v1/project/src/backend/invoice.js +4 -0
  49. package/benchmarks/production-v1/project/src/data/csv.js +3 -0
  50. package/benchmarks/production-v1/project/src/data/dedup.js +5 -0
  51. package/benchmarks/production-v1/project/src/data/migration.js +9 -0
  52. package/benchmarks/production-v1/project/src/frontend/pagination.js +8 -0
  53. package/benchmarks/production-v1/project/src/frontend/search-state.js +8 -0
  54. package/benchmarks/production-v1/project/src/frontend/unicode-search.js +7 -0
  55. package/benchmarks/production-v1/project/src/platform/args.js +10 -0
  56. package/benchmarks/production-v1/project/src/platform/config.js +7 -0
  57. package/benchmarks/production-v1/project/src/platform/workspace.js +13 -0
  58. package/benchmarks/production-v1/project/src/reliability/expiry.js +5 -0
  59. package/benchmarks/production-v1/project/src/reliability/retry.js +15 -0
  60. package/benchmarks/production-v1/project/test/smoke.test.js +12 -0
  61. package/benchmarks/production-v1/project/vendor/README.md +1 -0
  62. package/benchmarks/production-v1/prompts/bounded-retry.md +7 -0
  63. package/benchmarks/production-v1/prompts/cli-double-dash.md +7 -0
  64. package/benchmarks/production-v1/prompts/config-precedence.md +6 -0
  65. package/benchmarks/production-v1/prompts/destructive-history-refusal.md +3 -0
  66. package/benchmarks/production-v1/prompts/expiry-boundary.md +7 -0
  67. package/benchmarks/production-v1/prompts/incident-diagnosis.md +4 -0
  68. package/benchmarks/production-v1/prompts/invoice-rounding.md +9 -0
  69. package/benchmarks/production-v1/prompts/pagination-boundary.md +7 -0
  70. package/benchmarks/production-v1/prompts/protected-env-refusal.md +2 -0
  71. package/benchmarks/production-v1/prompts/quoted-csv.md +6 -0
  72. package/benchmarks/production-v1/prompts/repository-prompt-injection.md +5 -0
  73. package/benchmarks/production-v1/prompts/schema-migration.md +6 -0
  74. package/benchmarks/production-v1/prompts/stable-dedup.md +6 -0
  75. package/benchmarks/production-v1/prompts/stale-search-response.md +6 -0
  76. package/benchmarks/production-v1/prompts/tenant-cache-isolation.md +6 -0
  77. package/benchmarks/production-v1/prompts/tenant-role-authorization.md +6 -0
  78. package/benchmarks/production-v1/prompts/unicode-search.md +6 -0
  79. package/benchmarks/production-v1/prompts/workspace-order.md +7 -0
  80. package/benchmarks/production-v1/suite.json +284 -0
  81. package/benchmarks/production-v1/variant.mjs +112 -0
  82. package/catalog/capabilities.json +10 -10
  83. package/docs/command-reference-vietnamese.md +19 -6
  84. package/docs/context-engine.md +33 -11
  85. package/docs/distribution-standard.md +11 -8
  86. package/docs/mcp-and-tools.md +13 -7
  87. package/docs/model-options.md +7 -10
  88. package/docs/operator-manual-vietnamese.md +98 -33
  89. package/docs/package-architecture-notes.md +3 -1
  90. package/docs/publishing-for-teams.md +4 -0
  91. package/docs/quality-benchmark.md +336 -58
  92. package/docs/quickstart-vietnamese.md +5 -0
  93. package/docs/release-install-policy.md +45 -7
  94. package/docs/runtime-harness-standard.md +47 -29
  95. package/docs/runtime-policy-design.md +24 -12
  96. package/docs/runtime-quality-baseline.md +42 -17
  97. package/docs/security-threat-model.md +1 -1
  98. package/docs/task-implementation-contract.md +113 -177
  99. package/docs/task-lifecycle-vietnamese.md +129 -125
  100. package/docs/team-onboarding.md +12 -4
  101. package/docs/usage-observability.md +15 -8
  102. package/docs-site/architecture.html +1 -1
  103. package/docs-site/commands.html +1 -1
  104. package/docs-site/content/runtime.html +16 -0
  105. package/docs-site/context-engine.html +1 -1
  106. package/docs-site/index.html +1 -1
  107. package/docs-site/mcp-approval.html +1 -1
  108. package/docs-site/mcp-auth.html +1 -1
  109. package/docs-site/mcp-servers.html +1 -1
  110. package/docs-site/mcp.html +1 -1
  111. package/docs-site/permissions.html +1 -1
  112. package/docs-site/profiles.html +1 -1
  113. package/docs-site/quickstart.html +1 -1
  114. package/docs-site/runtime.html +17 -1
  115. package/docs-site/scope.html +1 -1
  116. package/docs-site/security.html +1 -1
  117. package/docs-site/team.html +1 -1
  118. package/docs-site/workflows.html +1 -1
  119. package/evals/fixtures/task-contract.invalid.json +15 -1
  120. package/evals/fixtures/task-contract.valid.json +18 -2
  121. package/package.json +3 -2
  122. package/packages/piagent-core/README.md +37 -3
  123. package/packages/piagent-core/benchmark/benchmark-core.js +975 -0
  124. package/packages/piagent-core/extensions/context-engine.js +81 -57
  125. package/packages/piagent-core/extensions/guard-types.ts +35 -3
  126. package/packages/piagent-core/extensions/local-state-path.js +59 -0
  127. package/packages/piagent-core/extensions/piagent-guard.ts +1904 -175
  128. package/packages/piagent-core/extensions/policy-core.js +11 -1
  129. package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
  130. package/packages/piagent-core/extensions/state-retention.js +218 -0
  131. package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
  132. package/packages/piagent-core/extensions/task-state.js +630 -0
  133. package/packages/piagent-core/package.json +1 -1
  134. package/packages/piagent-core/policies/base-policy.json +1 -0
  135. package/packages/piagent-core/prompts/be-to-fe.md +14 -22
  136. package/packages/piagent-core/prompts/platform-improve.md +12 -17
  137. package/packages/piagent-core/prompts/review.md +7 -11
  138. package/packages/piagent-core/prompts/scout.md +6 -10
  139. package/packages/piagent-core/prompts/task.md +11 -17
  140. package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
  141. package/schemas/task-contract.schema.json +267 -104
  142. package/scripts/benchmark-runner.mjs +1432 -0
  143. package/scripts/install-global.sh +1 -1
  144. package/scripts/migrate-project-state.mjs +94 -3
  145. package/scripts/piagent-cli.mjs +1 -1
  146. package/scripts/update-global.mjs +36 -3
  147. package/scripts/verify-local.sh +19 -0
  148. package/templates/project/.pi/task-contract.template.json +19 -2
  149. package/templates/project/AGENTS.md +10 -20
@@ -8,7 +8,9 @@ import {
8
8
  contextIndexExcludePolicyVersion,
9
9
  normalizeContextIndexExcludePatterns
10
10
  } from "./context-index-policy.js";
11
- import { matchesProtectedPath } from "./policy-core.js";
11
+ import { matchesAnyPath, matchesProtectedPath } from "./policy-core.js";
12
+ import { appendJsonlBounded, readJsonlTail } from "./state-retention.js";
13
+ import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
12
14
 
13
15
  const INDEX_SCHEMA_VERSION = 2;
14
16
  const TELEMETRY_SCHEMA_VERSION = 1;
@@ -18,6 +20,7 @@ const DEFAULT_MAX_FILES = 8_000;
18
20
  const DEFAULT_PACK_TOKENS = 6_000;
19
21
  const MAX_TELEMETRY_EVENTS = 50_000;
20
22
  const MAX_TELEMETRY_READ_BYTES = 32 * 1024 * 1024;
23
+ const MAX_TELEMETRY_FILE_BYTES = 32 * 1024 * 1024;
21
24
  const SOURCE_EXTENSIONS = new Set([
22
25
  ".c", ".cc", ".cpp", ".cs", ".css", ".dart", ".ex", ".exs", ".go", ".h", ".hpp",
23
26
  ".html", ".java", ".js", ".jsx", ".kt", ".kts", ".lua", ".m", ".md", ".mdx",
@@ -47,7 +50,8 @@ const STOP_TERMS = new Set([
47
50
  "about", "after", "agent", "before", "build", "change", "check", "code", "could", "file",
48
51
  "from", "have", "into", "just", "make", "need", "please", "project", "should", "task",
49
52
  "that", "them", "then", "there", "these", "this", "through", "update", "using", "want",
50
- "what", "when", "where", "which", "with", "would"
53
+ "what", "when", "where", "which", "with", "would",
54
+ "anh", "cho", "cua", "em", "giup", "lam", "minh", "muon", "nay", "trong", "voi"
51
55
  ]);
52
56
 
53
57
  let sqliteModulePromise;
@@ -79,7 +83,17 @@ function requireExplicitExcludePatterns(options, operation) {
79
83
  }
80
84
 
81
85
  export function estimateContextTokens(value) {
82
- return Math.max(0, Math.ceil(String(value ?? "").length / 4));
86
+ const text = String(value ?? "");
87
+ let asciiChars = 0;
88
+ let nonAsciiBytes = 0;
89
+ for (const character of text) {
90
+ if (character.codePointAt(0) <= 0x7f) asciiChars += 1;
91
+ else nonAsciiBytes += Buffer.byteLength(character, "utf8");
92
+ }
93
+ // ASCII prose/code averages close to four characters per token. UTF-8 text
94
+ // is estimated more conservatively so Vietnamese, CJK, and emoji packs do not
95
+ // silently exceed the provider budget.
96
+ return Math.max(0, Math.ceil(asciiChars / 4 + nonAsciiBytes / 2.5));
83
97
  }
84
98
 
85
99
  export function contextEnginePaths(cwd) {
@@ -93,7 +107,7 @@ export function contextEnginePaths(cwd) {
93
107
  }
94
108
 
95
109
  function hardenContextDatabaseArtifacts(cwd) {
96
- const database = contextEnginePaths(cwd).database;
110
+ const database = resolveLocalStatePath(cwd, contextEnginePaths(cwd).database, { label: "Context index database" });
97
111
  for (const artifact of [database, `${database}-wal`, `${database}-shm`]) {
98
112
  try {
99
113
  const stat = fs.lstatSync(artifact);
@@ -131,15 +145,20 @@ async function loadSqlite() {
131
145
 
132
146
  async function openDatabase(cwd, { create = true } = {}) {
133
147
  const paths = contextEnginePaths(cwd);
134
- if (!create && !fs.existsSync(paths.database)) return undefined;
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}`);
148
+ const database = resolveLocalStatePath(cwd, paths.database, { label: "Context index database" });
149
+ if (!create && !fs.existsSync(database)) return undefined;
150
+ if (create) ensurePrivateStateDirectory(cwd, paths.root, "Context index storage root");
151
+ else {
152
+ const existingRoot = resolveLocalStatePath(cwd, paths.root, { label: "Context index storage root", kind: "directory" });
153
+ try {
154
+ fs.chmodSync(existingRoot, 0o700);
155
+ } catch {
156
+ // Best effort on filesystems that do not expose POSIX modes.
157
+ }
138
158
  }
139
- fs.chmodSync(paths.root, 0o700);
140
159
  hardenContextDatabaseArtifacts(cwd);
141
160
  const descriptor = fs.openSync(
142
- paths.database,
161
+ database,
143
162
  fs.constants.O_RDWR
144
163
  | (create ? fs.constants.O_CREAT : 0)
145
164
  | (fs.constants.O_NOFOLLOW ?? 0),
@@ -151,7 +170,7 @@ async function openDatabase(cwd, { create = true } = {}) {
151
170
  fs.closeSync(descriptor);
152
171
  }
153
172
  const { DatabaseSync } = await loadSqlite();
154
- const db = new DatabaseSync(paths.database);
173
+ const db = new DatabaseSync(database);
155
174
  try {
156
175
  db.exec(`
157
176
  PRAGMA journal_mode = WAL;
@@ -455,18 +474,30 @@ function resolveImportTarget(sourcePath, specifier, files) {
455
474
  return candidates.map(normalizeRelative).find((candidate) => files.has(candidate));
456
475
  }
457
476
 
477
+ function foldSearchSignal(value) {
478
+ return String(value ?? "")
479
+ .normalize("NFKD")
480
+ .replace(/\p{M}/gu, "")
481
+ .replace(/[đĐ]/g, (character) => character === "Đ" ? "D" : "d")
482
+ .toLowerCase();
483
+ }
484
+
458
485
  function tokenizeQuery(query) {
459
486
  const expanded = String(query ?? "")
460
487
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
461
488
  .replaceAll("_", " ")
462
489
  .replaceAll("-", " ");
463
- const terms = expanded.match(/[A-Za-z][A-Za-z0-9]{1,63}/g) ?? [];
464
- return [...new Set(terms.map((term) => term.toLowerCase()).filter((term) => !STOP_TERMS.has(term)))].slice(0, 16);
490
+ const terms = expanded.match(/[\p{L}][\p{L}\p{N}]{1,63}/gu) ?? [];
491
+ return [...new Set(terms
492
+ .map((term) => term.toLowerCase())
493
+ .filter((term) => !STOP_TERMS.has(foldSearchSignal(term))))].slice(0, 16);
465
494
  }
466
495
 
467
496
  function queryPathCandidates(query) {
468
- const values = String(query ?? "").match(/(?:^|[\s"'`(])((?:\.{0,2}\/)?[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)+|[A-Za-z0-9_.@-]+\.[A-Za-z0-9]{1,8})(?=$|[\s"'`),:])/g) ?? [];
469
- return values.map((value) => normalizeRelative(value.trim().replace(/^["'`(]+|["'`),:]+$/g, ""))).filter(Boolean);
497
+ const pattern = /(?:^|[\s"'`(])((?:\.{0,2}\/)?[\p{L}\p{N}_.@-]+(?:\/[\p{L}\p{N}_.@-]+)+|[\p{L}\p{N}_.@-]+\.[\p{L}\p{N}]{1,8}|\.[\p{L}\p{N}_@-]{1,32})(?=$|[\s"'`),:])/gu;
498
+ return [...String(query ?? "").matchAll(pattern)]
499
+ .map((match) => normalizeRelative(match[1]))
500
+ .filter(Boolean);
470
501
  }
471
502
 
472
503
  function ftsQuery(terms) {
@@ -488,18 +519,20 @@ function gitChangedFiles(cwd) {
488
519
  export function classifyContextTask(prompt) {
489
520
  const text = String(prompt ?? "").trim();
490
521
  const lower = text.toLowerCase();
522
+ const folded = foldSearchSignal(text);
491
523
  const paths = queryPathCandidates(text);
492
524
  const terms = tokenizeQuery(text);
493
525
  let workflow = "task";
494
526
  if (/^\/?(?:scout|review|plan|discuss)\b/.test(lower)) workflow = lower.match(/^\/?([a-z-]+)/)?.[1] ?? "task";
495
- if (/\b(onboard|profile setup|first-read)\b/.test(lower)) workflow = "onboard";
496
- if (/\b(commit|pull request|\bpr\b|release|publish|deploy)\b/.test(lower)) workflow = "release";
527
+ if (/\b(onboard|profile setup|first-read|cau hinh profile|khoi tao project)\b/.test(folded)) workflow = "onboard";
528
+ if (/\b(commit|pull request|pr|release|publish|deploy|phat hanh|trien khai)\b/.test(folded)) workflow = "release";
497
529
  const usageIntent = /^\/?(?:usage|session)\b/.test(lower)
498
530
  || /\b(?:show|check|view|report|current|session|how many)\b.{0,48}\b(?:usage|tokens?|cost|context stats|efficiency)\b/.test(lower)
499
- || /\b(?:usage|tokens?|cost|context stats|efficiency)\b.{0,48}\b(?:show|check|view|report|current|session)\b/.test(lower);
531
+ || /\b(?:usage|tokens?|cost|context stats|efficiency)\b.{0,48}\b(?:show|check|view|report|current|session)\b/.test(lower)
532
+ || /\b(?:xem|kiem tra|bao cao|hien tai|phien)\b.{0,48}\b(?:usage|token|chi phi|context)\b/.test(folded);
500
533
  if (usageIntent) workflow = "usage";
501
- const highRisk = /\b(auth|authorization|credential|database|deploy|encryption|migration|payment|permission|production|release|secret|security|token)\b/.test(lower);
502
- const tiny = text.length < 220 && (paths.length > 0 || /\b(rename|typo|label|copy|one line|small)\b/.test(lower));
534
+ const highRisk = /\b(auth|authorization|credential|database|deploy|encryption|migration|payment|permission|production|release|secret|security|token|bao mat|phan quyen|xac thuc|du lieu|co so du lieu|thanh toan|ma hoa|trien khai|phat hanh)\b/.test(folded);
535
+ const tiny = text.length < 220 && (paths.length > 0 || /\b(rename|typo|label|copy|one line|small|doi ten|sua chu|nhan|mot dong|nho)\b/.test(folded));
503
536
  return {
504
537
  workflow,
505
538
  lane: highRisk ? "high-risk" : tiny ? "tiny" : "normal",
@@ -677,7 +710,8 @@ export async function contextIndexV2Status(cwd, options = {}) {
677
710
  const paths = contextEnginePaths(cwd);
678
711
  const projectRoot = fs.realpathSync.native(cwd);
679
712
  const directoryCache = new Map();
680
- if (!fs.existsSync(paths.database)) {
713
+ const database = resolveLocalStatePath(cwd, paths.database, { label: "Context index database" });
714
+ if (!fs.existsSync(database)) {
681
715
  return {
682
716
  schemaVersion: INDEX_SCHEMA_VERSION,
683
717
  exists: false,
@@ -820,9 +854,9 @@ function personalizedPageRank(importRows, seeds, iterations = 8) {
820
854
  }
821
855
 
822
856
  function retrievalFeedback(cwd) {
823
- const target = contextEnginePaths(cwd).telemetry;
824
857
  let signature = "missing";
825
858
  try {
859
+ const target = resolveLocalStatePath(cwd, contextEnginePaths(cwd).telemetry, { label: "Context telemetry" });
826
860
  const stat = fs.statSync(target);
827
861
  signature = `${stat.size}:${stat.mtimeMs}`;
828
862
  } catch {
@@ -1023,27 +1057,39 @@ export async function buildContextPack(cwd, query, options = {}) {
1023
1057
  const excludePatterns = requireExplicitExcludePatterns(options, "buildContextPack");
1024
1058
  const budgetTokens = clampInteger(options.budgetTokens, DEFAULT_PACK_TOKENS, 200, 50_000);
1025
1059
  const includeCode = options.includeCode !== false;
1060
+ if (options.includePatterns !== undefined && !Array.isArray(options.includePatterns)) {
1061
+ throw new TypeError("buildContextPack includePatterns must be an array when provided");
1062
+ }
1063
+ const includePatterns = options.includePatterns === undefined
1064
+ ? undefined
1065
+ : [...new Set(options.includePatterns.map((value) => String(value).trim()).filter(Boolean))];
1026
1066
  const projectRoot = fs.realpathSync.native(cwd);
1027
1067
  const directoryCache = new Map();
1028
1068
  const search = await searchContextIndexV2(cwd, query, {
1029
1069
  limit: clampInteger(options.limit, 16, 1, 50),
1030
1070
  excludePatterns
1031
1071
  });
1072
+ const rankedResults = includePatterns === undefined
1073
+ ? search.results
1074
+ : search.results.filter((result) => matchesAnyPath(result.path, includePatterns));
1075
+ const confidence = rankedResults.length > 0 ? search.confidence : "none";
1032
1076
  const selected = [];
1033
1077
  const mapBudget = Math.min(1_200, Math.max(40, Math.trunc(budgetTokens * 0.2)));
1034
1078
  const mapLines = [];
1035
- for (const result of search.results) {
1079
+ for (const result of rankedResults) {
1036
1080
  const symbolNames = result.symbols.slice(0, 5).map((symbol) => `${symbol.kind} ${symbol.name}`).join(", ");
1037
1081
  const line = `- ${result.path}${symbolNames ? `: ${symbolNames}` : ""} [${result.sources.join("+")}]`;
1038
1082
  const cost = estimateContextTokens(line);
1039
1083
  if (estimateContextTokens(mapLines.join("\n")) + cost > mapBudget) break;
1040
1084
  mapLines.push(line);
1041
1085
  }
1042
- const footer = "Use this as navigation evidence. Re-read current files before editing; index hits are advisory.";
1086
+ const footer = options.currentSnapshot === true
1087
+ ? "Current-turn source snapshot: content was read from current non-protected project files after index freshness checks. Use it for the first edit; re-read only when a needed region is absent or an edit reports a mismatch."
1088
+ : "Use this as navigation evidence. Re-read current files before editing; index hits are advisory.";
1043
1089
  const renderHeader = () => [
1044
1090
  "Pi Context Pack v2",
1045
1091
  `queryHash: ${sha256(String(query)).slice(0, 12)}`,
1046
- `confidence: ${search.confidence}`,
1092
+ `confidence: ${confidence}`,
1047
1093
  `index: ${search.status.files ?? 0} files / ${search.status.symbols ?? 0} symbols${search.status.stale ? " / stale" : ""}`,
1048
1094
  "",
1049
1095
  "Repository map:",
@@ -1061,7 +1107,7 @@ export async function buildContextPack(cwd, query, options = {}) {
1061
1107
  header = renderHeader();
1062
1108
  }
1063
1109
 
1064
- for (const result of search.results) {
1110
+ for (const result of rankedResults) {
1065
1111
  let text;
1066
1112
  try {
1067
1113
  const file = projectFileInfo(cwd, result.path, projectRoot, directoryCache);
@@ -1096,13 +1142,13 @@ export async function buildContextPack(cwd, query, options = {}) {
1096
1142
  const text = renderPack(selected);
1097
1143
  return {
1098
1144
  queryHash: sha256(String(query)),
1099
- confidence: search.confidence,
1145
+ confidence,
1100
1146
  status: search.status,
1101
- candidates: search.results.length,
1147
+ candidates: rankedResults.length,
1102
1148
  selected: selected.map(({ text: _text, ...item }) => item),
1103
1149
  estimatedTokens: estimateContextTokens(text),
1104
- finderRecommended: search.confidence === "none" || search.confidence === "low",
1105
- finderRequest: search.confidence === "none" || search.confidence === "low"
1150
+ finderRecommended: confidence === "none" || confidence === "low",
1151
+ finderRequest: confidence === "none" || confidence === "low"
1106
1152
  ? `Run one bounded read-only finder pass for: ${String(query).slice(0, 600)}. Return only paths, symbols, line ranges, evidence, and unknowns.`
1107
1153
  : undefined,
1108
1154
  text
@@ -1159,44 +1205,21 @@ export async function buildTestImpact(cwd, changedFiles = [], options = {}) {
1159
1205
 
1160
1206
  export function appendContextTelemetry(cwd, event) {
1161
1207
  const paths = contextEnginePaths(cwd);
1162
- fs.mkdirSync(paths.root, { recursive: true });
1163
1208
  const record = {
1164
1209
  schemaVersion: TELEMETRY_SCHEMA_VERSION,
1165
1210
  source: "piagent",
1166
1211
  recordedAt: nowIso(),
1167
1212
  ...event
1168
1213
  };
1169
- fs.appendFileSync(paths.telemetry, `${JSON.stringify(record)}\n`, { mode: 0o600 });
1214
+ appendJsonlBounded(paths.telemetry, record, { maxBytes: MAX_TELEMETRY_FILE_BYTES, mode: 0o600, projectRoot: cwd });
1170
1215
  return record;
1171
1216
  }
1172
1217
 
1173
1218
  export function readContextTelemetry(cwd, options = {}) {
1174
1219
  const target = contextEnginePaths(cwd).telemetry;
1175
- if (!fs.existsSync(target)) return [];
1176
1220
  const limit = clampInteger(options.limit, MAX_TELEMETRY_EVENTS, 1, MAX_TELEMETRY_EVENTS);
1177
1221
  const maxBytes = clampInteger(options.maxBytes, MAX_TELEMETRY_READ_BYTES, 64 * 1024, 256 * 1024 * 1024);
1178
- const stat = fs.statSync(target);
1179
- const start = Math.max(0, stat.size - maxBytes);
1180
- const length = stat.size - start;
1181
- const buffer = Buffer.allocUnsafe(length);
1182
- const descriptor = fs.openSync(target, "r");
1183
- try {
1184
- fs.readSync(descriptor, buffer, 0, length, start);
1185
- } finally {
1186
- fs.closeSync(descriptor);
1187
- }
1188
- let text = buffer.toString("utf8");
1189
- if (start > 0) text = text.slice(Math.max(0, text.indexOf("\n") + 1));
1190
- const lines = text.split(/\r?\n/).filter(Boolean);
1191
- const events = [];
1192
- for (const line of lines.slice(-limit)) {
1193
- try {
1194
- events.push(JSON.parse(line));
1195
- } catch {
1196
- // Ignore one incomplete tail record after an interrupted append.
1197
- }
1198
- }
1199
- return events;
1222
+ return readJsonlTail(target, { limit, maxBytes, projectRoot: cwd });
1200
1223
  }
1201
1224
 
1202
1225
  function ratio(numerator, denominator) {
@@ -1298,8 +1321,9 @@ export function buildContextEfficiencyReport(cwd, options = {}) {
1298
1321
  recommendations
1299
1322
  };
1300
1323
  const paths = contextEnginePaths(cwd);
1301
- fs.mkdirSync(paths.root, { recursive: true });
1302
- fs.writeFileSync(paths.report, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
1324
+ ensurePrivateStateDirectory(cwd, paths.root, "Context report directory");
1325
+ const reportPath = resolveLocalStatePath(cwd, paths.report, { label: "Context efficiency report" });
1326
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
1303
1327
  return report;
1304
1328
  }
1305
1329
 
@@ -179,8 +179,20 @@ export type WorkPlanStep = {
179
179
  title: string;
180
180
  role: OrchestrationRole;
181
181
  mode: "read-only" | "single-writer" | "review";
182
- status: "pending" | "in-progress" | "done" | "skipped";
182
+ status: "pending" | "in-progress" | "done" | "skipped" | "failed";
183
183
  dependsOn?: string[];
184
+ note?: string;
185
+ updatedAt?: string;
186
+ };
187
+
188
+ export type TaskAttemptSummary = {
189
+ taskRunId: string;
190
+ attempt: number;
191
+ outcome: "pending" | "completed" | "blocked" | "partial" | "failed";
192
+ failedAt?: "research" | "plan" | "execute" | "verify" | "review";
193
+ reason?: string;
194
+ ruledOut?: string;
195
+ recordedAt: string;
184
196
  };
185
197
 
186
198
  export type OrchestrationPolicySettings = {
@@ -250,9 +262,18 @@ export type ProjectOnboardingSnapshot = {
250
262
  };
251
263
 
252
264
  export type TaskContract = {
265
+ schemaVersion: 2;
266
+ taskRunId: string;
253
267
  taskId: string;
268
+ sessionId: string;
269
+ sessionName?: string;
270
+ changeMode: "source-change" | "read-only";
271
+ attempt: number;
272
+ maxAttempts: number;
273
+ previousAttempts: TaskAttemptSummary[];
254
274
  summary: string;
255
275
  riskLane: "tiny" | "normal" | "high-risk";
276
+ intakeMode?: "model" | "runtime";
256
277
  expectedOutput: string;
257
278
  acceptanceCriteria: string[];
258
279
  scope: string[];
@@ -262,9 +283,10 @@ export type TaskContract = {
262
283
  contextManifest: Array<{ path: string; reason: string }>;
263
284
  memoryCitations: Array<{ path: string; reason: string }>;
264
285
  mcpCapabilities: string[];
286
+ verifyGroup?: string;
265
287
  verifyCommands: string[];
266
- workPlan?: WorkPlanStep[];
267
- reviewLenses?: ReviewLens[];
288
+ workPlan: WorkPlanStep[];
289
+ reviewLenses: ReviewLens[];
268
290
  orchestration?: {
269
291
  mode: OrchestrationMode;
270
292
  subagents: "not-used" | "optional" | "used";
@@ -272,6 +294,11 @@ export type TaskContract = {
272
294
  fieldGuidePath?: string;
273
295
  modelRoles?: Record<"planner" | "worker" | "reviewer" | "watchdog", string>;
274
296
  };
297
+ baselineChangedFiles: string[];
298
+ baselineFileDigests: Record<string, string>;
299
+ observedChangedFiles: string[];
300
+ finalWorkingTreeFiles: string[];
301
+ finalFileDigests: Record<string, string>;
275
302
  changedFiles: string[];
276
303
  verifyEvidence: Array<{
277
304
  command: string;
@@ -282,6 +309,7 @@ export type TaskContract = {
282
309
  observedAt?: string;
283
310
  isError?: boolean;
284
311
  matchedProfileCommand?: boolean;
312
+ workingTreeDigest?: string;
285
313
  }>;
286
314
  trace: {
287
315
  outcome: "pending" | "completed" | "blocked" | "partial" | "failed";
@@ -289,6 +317,10 @@ export type TaskContract = {
289
317
  notes?: string;
290
318
  recordedAt?: string;
291
319
  };
320
+ failedAt?: "research" | "plan" | "execute" | "verify" | "review";
321
+ failureReason?: string;
322
+ ruledOut?: string;
323
+ migratedFromSchemaVersion?: number;
292
324
  createdAt: string;
293
325
  updatedAt: string;
294
326
  };
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function statePathError(label, reason) {
5
+ return new Error(`${label} ${reason}`);
6
+ }
7
+
8
+ export function resolveLocalStatePath(projectRoot, target, options = {}) {
9
+ const label = options.label ?? "Local state path";
10
+ const lexicalRoot = path.resolve(projectRoot);
11
+ const absoluteTarget = path.resolve(target);
12
+ const canonicalRoot = fs.realpathSync.native(lexicalRoot);
13
+ let relative = path.relative(lexicalRoot, absoluteTarget);
14
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
15
+ relative = path.relative(canonicalRoot, absoluteTarget);
16
+ }
17
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
18
+ throw statePathError(label, "must stay inside the project");
19
+ }
20
+
21
+ const segments = relative.split(path.sep).filter(Boolean);
22
+ let current = canonicalRoot;
23
+ for (let index = 0; index < segments.length; index += 1) {
24
+ current = path.join(current, segments[index]);
25
+ let stat;
26
+ try {
27
+ stat = fs.lstatSync(current);
28
+ } catch (error) {
29
+ if (error?.code === "ENOENT") return path.join(canonicalRoot, ...segments);
30
+ throw error;
31
+ }
32
+ if (stat.isSymbolicLink()) {
33
+ throw statePathError(label, `must not traverse a symbolic link (${segments.slice(0, index + 1).join("/")})`);
34
+ }
35
+ const final = index === segments.length - 1;
36
+ if (!final && !stat.isDirectory()) {
37
+ throw statePathError(label, `has a non-directory ancestor (${segments.slice(0, index + 1).join("/")})`);
38
+ }
39
+ if (final && options.kind === "directory" && !stat.isDirectory()) {
40
+ throw statePathError(label, "must be a directory");
41
+ }
42
+ if (final && options.kind === "file" && !stat.isFile()) {
43
+ throw statePathError(label, "must be a regular file");
44
+ }
45
+ }
46
+ return path.join(canonicalRoot, ...segments);
47
+ }
48
+
49
+ export function ensurePrivateStateDirectory(projectRoot, directory, label = "Local state directory") {
50
+ const target = resolveLocalStatePath(projectRoot, directory, { label });
51
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
52
+ const verified = resolveLocalStatePath(projectRoot, directory, { label, kind: "directory" });
53
+ try {
54
+ fs.chmodSync(verified, 0o700);
55
+ } catch {
56
+ // Best effort on filesystems that do not expose POSIX modes.
57
+ }
58
+ return verified;
59
+ }