@piagent/platform 1.2.10 → 1.2.11
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.
- package/CHANGELOG.md +137 -0
- package/README.md +27 -4
- package/adapters/backend-api/profile.json +1 -1
- package/adapters/be-readonly-fe/profile.json +2 -2
- package/adapters/data/profile.json +1 -1
- package/adapters/devops/profile.json +1 -3
- package/adapters/docs/profile.json +1 -1
- package/adapters/fullstack/profile.json +2 -4
- package/adapters/generic/profile.json +1 -1
- package/adapters/mobile/profile.json +1 -2
- package/adapters/node-typescript/profile.json +1 -3
- package/benchmarks/core-v1/suite.json +51 -0
- package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
- package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
- package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
- package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
- package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
- package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
- package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
- package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
- package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
- package/catalog/capabilities.json +10 -10
- package/docs/command-reference-vietnamese.md +10 -6
- package/docs/context-engine.md +33 -11
- package/docs/distribution-standard.md +11 -8
- package/docs/mcp-and-tools.md +13 -7
- package/docs/model-options.md +7 -10
- package/docs/operator-manual-vietnamese.md +82 -33
- package/docs/package-architecture-notes.md +3 -1
- package/docs/publishing-for-teams.md +4 -0
- package/docs/quality-benchmark.md +225 -58
- package/docs/quickstart-vietnamese.md +5 -0
- package/docs/release-install-policy.md +41 -7
- package/docs/runtime-harness-standard.md +47 -29
- package/docs/runtime-policy-design.md +23 -12
- package/docs/runtime-quality-baseline.md +31 -17
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +113 -177
- package/docs/task-lifecycle-vietnamese.md +129 -125
- package/docs/team-onboarding.md +12 -4
- package/docs/usage-observability.md +15 -8
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/runtime.html +10 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +11 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/task-contract.invalid.json +15 -1
- package/evals/fixtures/task-contract.valid.json +18 -2
- package/package.json +3 -2
- package/packages/piagent-core/README.md +37 -3
- package/packages/piagent-core/benchmark/benchmark-core.js +691 -0
- package/packages/piagent-core/extensions/context-engine.js +81 -57
- package/packages/piagent-core/extensions/guard-types.ts +35 -3
- package/packages/piagent-core/extensions/local-state-path.js +59 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +1865 -171
- package/packages/piagent-core/extensions/policy-core.js +11 -1
- package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
- package/packages/piagent-core/extensions/state-retention.js +218 -0
- package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
- package/packages/piagent-core/extensions/task-state.js +630 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +1 -0
- package/packages/piagent-core/prompts/be-to-fe.md +14 -22
- package/packages/piagent-core/prompts/platform-improve.md +12 -17
- package/packages/piagent-core/prompts/review.md +7 -11
- package/packages/piagent-core/prompts/scout.md +6 -10
- package/packages/piagent-core/prompts/task.md +11 -17
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
- package/schemas/task-contract.schema.json +267 -104
- package/scripts/benchmark-runner.mjs +1172 -0
- package/scripts/install-global.sh +1 -1
- package/scripts/migrate-project-state.mjs +94 -3
- package/scripts/piagent-cli.mjs +1 -1
- package/scripts/update-global.mjs +36 -3
- package/scripts/verify-local.sh +15 -0
- package/templates/project/.pi/task-contract.template.json +19 -2
- 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
|
-
|
|
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
|
-
|
|
135
|
-
fs.
|
|
136
|
-
if (
|
|
137
|
-
|
|
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
|
-
|
|
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(
|
|
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(/[
|
|
464
|
-
return [...new Set(terms
|
|
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
|
|
469
|
-
return
|
|
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(
|
|
496
|
-
if (/\b(commit|pull request
|
|
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(
|
|
502
|
-
const tiny = text.length < 220 && (paths.length > 0 || /\b(rename|typo|label|copy|one line|small)\b/.test(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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: ${
|
|
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
|
|
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
|
|
1145
|
+
confidence,
|
|
1100
1146
|
status: search.status,
|
|
1101
|
-
candidates:
|
|
1147
|
+
candidates: rankedResults.length,
|
|
1102
1148
|
selected: selected.map(({ text: _text, ...item }) => item),
|
|
1103
1149
|
estimatedTokens: estimateContextTokens(text),
|
|
1104
|
-
finderRecommended:
|
|
1105
|
-
finderRequest:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
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
|
|
267
|
-
reviewLenses
|
|
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
|
+
}
|