@piagent/platform 1.0.0

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 (159) hide show
  1. package/.gitattributes +7 -0
  2. package/AGENTS.md +31 -0
  3. package/CHANGELOG.md +296 -0
  4. package/CODE_OF_CONDUCT.md +36 -0
  5. package/CONTRIBUTING.md +66 -0
  6. package/LICENSE +21 -0
  7. package/README.md +588 -0
  8. package/SECURITY.md +38 -0
  9. package/adapters/backend-api/profile.json +88 -0
  10. package/adapters/be-readonly-fe/profile.json +120 -0
  11. package/adapters/data/profile.json +88 -0
  12. package/adapters/devops/profile.json +92 -0
  13. package/adapters/docs/profile.json +74 -0
  14. package/adapters/fullstack/profile.json +94 -0
  15. package/adapters/generic/profile.json +95 -0
  16. package/adapters/mobile/profile.json +87 -0
  17. package/adapters/node-typescript/profile.json +76 -0
  18. package/adapters/python/profile.json +75 -0
  19. package/adapters/web-frontend/profile.json +101 -0
  20. package/catalog/capabilities.json +206 -0
  21. package/docs/architecture.md +64 -0
  22. package/docs/auto-delegation-policy.md +192 -0
  23. package/docs/capability-packs.md +234 -0
  24. package/docs/command-reference-vietnamese.md +484 -0
  25. package/docs/context-window-policy.md +85 -0
  26. package/docs/distribution-standard.md +229 -0
  27. package/docs/herdr-workflow.md +58 -0
  28. package/docs/mcp-and-tools.md +316 -0
  29. package/docs/memory-policy.md +164 -0
  30. package/docs/model-options.md +165 -0
  31. package/docs/oauth-providers.md +53 -0
  32. package/docs/operator-manual-vietnamese.md +1080 -0
  33. package/docs/package-architecture-notes.md +101 -0
  34. package/docs/project-adapters.md +174 -0
  35. package/docs/project-onboarding.md +135 -0
  36. package/docs/publishing-for-teams.md +64 -0
  37. package/docs/quality-benchmark.md +74 -0
  38. package/docs/quickstart-vietnamese.md +350 -0
  39. package/docs/release-install-policy.md +223 -0
  40. package/docs/runtime-harness-standard.md +118 -0
  41. package/docs/runtime-policy-design.md +132 -0
  42. package/docs/runtime-quality-baseline.md +257 -0
  43. package/docs/security-redaction-benchmark.md +56 -0
  44. package/docs/security-threat-model.md +66 -0
  45. package/docs/subagent-orchestration-capabilities.md +104 -0
  46. package/docs/subagents-and-multiagent.md +346 -0
  47. package/docs/task-implementation-contract.md +200 -0
  48. package/docs/team-onboarding.md +333 -0
  49. package/docs/usage-observability.md +130 -0
  50. package/docs/vercel-docs-site.md +115 -0
  51. package/docs/workflow-recipes.md +125 -0
  52. package/docs-site/assets/piagent-logo.svg +35 -0
  53. package/docs-site/favicon.svg +18 -0
  54. package/docs-site/index.html +1613 -0
  55. package/docs-site/vercel.json +24 -0
  56. package/evals/fixtures/action-proposal.invalid.json +34 -0
  57. package/evals/fixtures/action-proposal.valid.json +34 -0
  58. package/evals/fixtures/capability-pack.invalid.json +123 -0
  59. package/evals/fixtures/capability-pack.valid.json +123 -0
  60. package/evals/fixtures/capability-recipe.invalid.json +81 -0
  61. package/evals/fixtures/capability-recipe.valid.json +82 -0
  62. package/evals/fixtures/context-index.invalid.json +25 -0
  63. package/evals/fixtures/context-index.valid.json +25 -0
  64. package/evals/fixtures/eval-scenario.invalid.json +40 -0
  65. package/evals/fixtures/eval-scenario.valid.json +45 -0
  66. package/evals/fixtures/project-profile.invalid.json +88 -0
  67. package/evals/fixtures/project-profile.valid.json +95 -0
  68. package/evals/fixtures/task-contract.invalid.json +73 -0
  69. package/evals/fixtures/task-contract.valid.json +74 -0
  70. package/evals/golden/enforcement-decisions.json +329 -0
  71. package/evals/scenarios/capability-resolution.json +45 -0
  72. package/package.json +132 -0
  73. package/packages/piagent-core/README.md +147 -0
  74. package/packages/piagent-core/capabilities/capability-core.js +1023 -0
  75. package/packages/piagent-core/capabilities/capability-sources.js +265 -0
  76. package/packages/piagent-core/extensions/guard-io.js +45 -0
  77. package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
  78. package/packages/piagent-core/extensions/guard-types.ts +406 -0
  79. package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
  80. package/packages/piagent-core/extensions/policy-core.js +1070 -0
  81. package/packages/piagent-core/extensions/redaction-core.js +4 -0
  82. package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
  83. package/packages/piagent-core/package.json +46 -0
  84. package/packages/piagent-core/policies/base-policy.json +249 -0
  85. package/packages/piagent-core/prompts/be-to-fe.md +82 -0
  86. package/packages/piagent-core/prompts/commit.md +27 -0
  87. package/packages/piagent-core/prompts/discuss.md +35 -0
  88. package/packages/piagent-core/prompts/memory-policy.md +52 -0
  89. package/packages/piagent-core/prompts/model-options.md +68 -0
  90. package/packages/piagent-core/prompts/onboard-project.md +147 -0
  91. package/packages/piagent-core/prompts/piagent-commands.md +68 -0
  92. package/packages/piagent-core/prompts/plan.md +35 -0
  93. package/packages/piagent-core/prompts/platform-improve.md +69 -0
  94. package/packages/piagent-core/prompts/pr.md +26 -0
  95. package/packages/piagent-core/prompts/review.md +38 -0
  96. package/packages/piagent-core/prompts/scout.md +50 -0
  97. package/packages/piagent-core/prompts/task.md +69 -0
  98. package/packages/piagent-core/security/sensitive-data.js +187 -0
  99. package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
  100. package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
  101. package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
  102. package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
  103. package/packages/piagent-core/subagents/piagent-planner.md +46 -0
  104. package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
  105. package/packages/piagent-core/subagents/piagent-scout.md +37 -0
  106. package/packages/piagent-core/subagents/piagent-worker.md +44 -0
  107. package/packs/engineering-base/pack.json +123 -0
  108. package/packs/engineering-base/recipes/bounded-change.json +82 -0
  109. package/packs/web-delivery/pack.json +72 -0
  110. package/packs/web-delivery/recipes/verified-web-change.json +57 -0
  111. package/schemas/action-proposal.schema.json +90 -0
  112. package/schemas/capability-pack.schema.json +190 -0
  113. package/schemas/capability-recipe.schema.json +72 -0
  114. package/schemas/context-index.schema.json +258 -0
  115. package/schemas/eval-scenario.schema.json +67 -0
  116. package/schemas/project-profile.schema.json +555 -0
  117. package/schemas/task-contract.schema.json +314 -0
  118. package/scripts/audit-runtime-host.sh +78 -0
  119. package/scripts/bootstrap-local.sh +25 -0
  120. package/scripts/capability-catalog.mjs +290 -0
  121. package/scripts/check-runtime-advisories.mjs +114 -0
  122. package/scripts/configure-mcp.sh +301 -0
  123. package/scripts/configure-model-scope.sh +143 -0
  124. package/scripts/configure-subagents.sh +241 -0
  125. package/scripts/import-agent-instructions.mjs +199 -0
  126. package/scripts/init-project.sh +369 -0
  127. package/scripts/install-global.sh +532 -0
  128. package/scripts/link-project.sh +25 -0
  129. package/scripts/migrate-project-state.mjs +176 -0
  130. package/scripts/pi-auto.sh +79 -0
  131. package/scripts/pi-model-catalog.sh +154 -0
  132. package/scripts/pi-session-stats.sh +135 -0
  133. package/scripts/piagent-cli.mjs +53 -0
  134. package/scripts/profile-doctor.sh +238 -0
  135. package/scripts/quality-benchmark.sh +185 -0
  136. package/scripts/redaction-benchmark.mjs +227 -0
  137. package/scripts/runtime-policy-smoke.sh +48 -0
  138. package/scripts/setup.sh +425 -0
  139. package/scripts/team-doctor.sh +461 -0
  140. package/scripts/verify-local.sh +540 -0
  141. package/scripts/verify-release-identity.mjs +85 -0
  142. package/scripts/verify-vercel-link.mjs +64 -0
  143. package/templates/global/mcp.json +9 -0
  144. package/templates/global/settings.json +37 -0
  145. package/templates/project/.mcp.json +9 -0
  146. package/templates/project/.pi/context-index.json +25 -0
  147. package/templates/project/.pi/gitignore.template +22 -0
  148. package/templates/project/.pi/mcp.json +9 -0
  149. package/templates/project/.pi/memory/MEMORY.md +11 -0
  150. package/templates/project/.pi/memory/memory_summary.md +9 -0
  151. package/templates/project/.pi/piagent-profile.json +107 -0
  152. package/templates/project/.pi/project-context.md +30 -0
  153. package/templates/project/.pi/settings.json +17 -0
  154. package/templates/project/.pi/task-contract.template.json +74 -0
  155. package/templates/project/.pi/tech-context/README.md +7 -0
  156. package/templates/project/.pi/tech-stack.json +10 -0
  157. package/templates/project/AGENTS.md +34 -0
  158. package/templates/project/REVIEW_GUIDELINES.md +23 -0
  159. package/types/pi-runtime-shims.d.ts +13 -0
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ $# -lt 1 ]]; then
5
+ echo "Usage: $0 <project-path> [profile-json]"
6
+ exit 2
7
+ fi
8
+
9
+ PROJECT_PATH="$1"
10
+ PROFILE_PATH="${2:-"$PROJECT_PATH/.pi/piagent-profile.json"}"
11
+
12
+ if [[ ! -d "$PROJECT_PATH" ]]; then
13
+ echo "FAIL: project path does not exist: $PROJECT_PATH"
14
+ exit 1
15
+ fi
16
+
17
+ if [[ ! -f "$PROFILE_PATH" ]]; then
18
+ echo "FAIL: profile not found: $PROFILE_PATH"
19
+ exit 1
20
+ fi
21
+
22
+ node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" <<'NODE'
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+
26
+ const [projectPath, profilePath] = process.argv.slice(2);
27
+ const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
28
+
29
+ const errors = [];
30
+ const warnings = [];
31
+
32
+ function requireArray(name) {
33
+ if (!Array.isArray(profile[name])) {
34
+ errors.push(`${name} must be an array`);
35
+ return [];
36
+ }
37
+ return profile[name];
38
+ }
39
+
40
+ function optionalArray(name) {
41
+ if (profile[name] === undefined) return [];
42
+ if (!Array.isArray(profile[name])) {
43
+ errors.push(`${name} must be an array when provided`);
44
+ return [];
45
+ }
46
+ return profile[name];
47
+ }
48
+
49
+ function warnShellOnlyProtectedPaths(shellProtectedPaths, protectedPaths, readOnlyPaths) {
50
+ const writeGuardedPaths = new Set([...protectedPaths, ...readOnlyPaths]);
51
+ for (const candidate of shellProtectedPaths) {
52
+ if (typeof candidate !== "string" || candidate.trim().length === 0) continue;
53
+ if (writeGuardedPaths.has(candidate)) continue;
54
+ warnings.push(`shellProtectedPaths-only path ${candidate} blocks shell access only; add it to protectedPaths to block read/write, or readOnlyPaths to allow read but block write/shell`);
55
+ }
56
+ }
57
+
58
+ if (profile.schemaVersion !== 1) errors.push("schemaVersion must be 1");
59
+ if (!profile.projectId) errors.push("projectId is required");
60
+ if (!profile.displayName) errors.push("displayName is required");
61
+ if (!profile.mode) errors.push("mode is required");
62
+
63
+ const rootMarkers = requireArray("rootMarkers");
64
+ const protectedPaths = requireArray("protectedPaths");
65
+ const shellProtectedPaths = optionalArray("shellProtectedPaths");
66
+ const readOnlyPaths = optionalArray("readOnlyPaths");
67
+ const requiredContext = requireArray("requiredContext");
68
+ const mcpCapabilities = requireArray("mcpCapabilities");
69
+
70
+ warnShellOnlyProtectedPaths(shellProtectedPaths, protectedPaths, readOnlyPaths);
71
+
72
+ const capabilityPacks = profile.capabilityPacks ?? [];
73
+ if (!Array.isArray(capabilityPacks)) {
74
+ errors.push("capabilityPacks must be an array");
75
+ } else {
76
+ const seenPacks = new Set();
77
+ for (const [index, pack] of capabilityPacks.entries()) {
78
+ if (!pack || typeof pack !== "object" || Array.isArray(pack)) {
79
+ errors.push(`capabilityPacks[${index}] must be an object`);
80
+ continue;
81
+ }
82
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(pack.name ?? "")) {
83
+ errors.push(`capabilityPacks[${index}].name is invalid`);
84
+ }
85
+ if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(pack.version ?? "")) {
86
+ errors.push(`capabilityPacks[${index}].version is invalid`);
87
+ }
88
+ const key = `${pack.name}@${pack.version}`;
89
+ if (seenPacks.has(key)) errors.push(`capabilityPacks contains duplicate ${key}`);
90
+ seenPacks.add(key);
91
+ }
92
+ }
93
+
94
+ const defaultCapabilityPolicy = {
95
+ allowedOwners: [],
96
+ allowedLifecycles: [],
97
+ allowedFilesystemRead: [],
98
+ allowedFilesystemWrite: [],
99
+ allowedNetworkDomains: [],
100
+ allowedExternalActions: []
101
+ };
102
+ const capabilityPolicy = profile.capabilityPolicy && typeof profile.capabilityPolicy === "object" && !Array.isArray(profile.capabilityPolicy)
103
+ ? profile.capabilityPolicy
104
+ : defaultCapabilityPolicy;
105
+ if (profile.capabilityPolicy !== undefined && capabilityPolicy === defaultCapabilityPolicy) errors.push("capabilityPolicy must be an object");
106
+ for (const key of ["allowedOwners", "allowedLifecycles", "allowedFilesystemRead", "allowedFilesystemWrite", "allowedNetworkDomains", "allowedExternalActions"]) {
107
+ if (!Array.isArray(capabilityPolicy[key])) errors.push(`capabilityPolicy.${key} must be an array`);
108
+ }
109
+ for (const lifecycle of Array.isArray(capabilityPolicy.allowedLifecycles) ? capabilityPolicy.allowedLifecycles : []) {
110
+ if (!["experimental", "stable", "deprecated"].includes(lifecycle)) {
111
+ errors.push(`capabilityPolicy.allowedLifecycles contains invalid value ${lifecycle}`);
112
+ }
113
+ }
114
+
115
+ if (!profile.verifyCommands || typeof profile.verifyCommands !== "object" || Array.isArray(profile.verifyCommands)) {
116
+ errors.push("verifyCommands must be an object");
117
+ }
118
+
119
+ const existingRootMarkers = rootMarkers.filter((marker) => fs.existsSync(path.join(projectPath, marker)));
120
+ if (existingRootMarkers.length === 0) {
121
+ warnings.push(`no rootMarkers found in project: ${rootMarkers.join(", ")}`);
122
+ }
123
+
124
+ for (const contextPath of requiredContext) {
125
+ if (contextPath.includes("<")) {
126
+ warnings.push(`requiredContext has placeholder: ${contextPath}`);
127
+ continue;
128
+ }
129
+ if (!fs.existsSync(path.join(projectPath, contextPath))) {
130
+ warnings.push(`requiredContext missing in project: ${contextPath}`);
131
+ }
132
+ }
133
+
134
+ if (protectedPaths.length === 0) warnings.push("protectedPaths is empty");
135
+ if (mcpCapabilities.length === 0) warnings.push("mcpCapabilities is empty");
136
+
137
+ const defaultRuntimePolicy = {
138
+ execPolicy: "enforce",
139
+ contextBudget: "enforce",
140
+ toolRegistry: "advisory",
141
+ finalGate: "advisory"
142
+ };
143
+ const runtimePolicyOverrides = profile.runtimePolicy === undefined
144
+ ? {}
145
+ : profile.runtimePolicy && typeof profile.runtimePolicy === "object" && !Array.isArray(profile.runtimePolicy)
146
+ ? profile.runtimePolicy
147
+ : {};
148
+ if (profile.runtimePolicy !== undefined && runtimePolicyOverrides !== profile.runtimePolicy) errors.push("runtimePolicy must be an object");
149
+ const runtimePolicy = { ...defaultRuntimePolicy, ...runtimePolicyOverrides };
150
+ for (const [key, value] of Object.entries(runtimePolicy)) {
151
+ if (!["off", "advisory", "enforce"].includes(value)) {
152
+ errors.push(`runtimePolicy.${key} must be off, advisory, or enforce`);
153
+ }
154
+ }
155
+
156
+ const contextIndexDefaults = {
157
+ enabled: true,
158
+ path: ".pi/context-index.json",
159
+ writePolicy: "onboarding-record",
160
+ requireCitations: true,
161
+ maxNodes: 120,
162
+ maxEdges: 240,
163
+ includeTechStack: true,
164
+ includeMemoryPointers: true
165
+ };
166
+ let contextIndex = contextIndexDefaults;
167
+ if (profile.contextIndex !== undefined) {
168
+ if (!profile.contextIndex || typeof profile.contextIndex !== "object" || Array.isArray(profile.contextIndex)) {
169
+ errors.push("contextIndex must be an object");
170
+ } else {
171
+ contextIndex = { ...contextIndexDefaults, ...profile.contextIndex };
172
+ if (typeof contextIndex.enabled !== "boolean") errors.push("contextIndex.enabled must be boolean");
173
+ if (typeof contextIndex.path !== "string" || contextIndex.path.trim().length === 0) errors.push("contextIndex.path must be a non-empty string");
174
+ if (!["onboarding-record", "approved-workflow", "off"].includes(contextIndex.writePolicy)) {
175
+ errors.push("contextIndex.writePolicy must be onboarding-record, approved-workflow, or off");
176
+ }
177
+ if (typeof contextIndex.requireCitations !== "boolean") errors.push("contextIndex.requireCitations must be boolean");
178
+ if (!Number.isInteger(contextIndex.maxNodes) || contextIndex.maxNodes < 1 || contextIndex.maxNodes > 500) errors.push("contextIndex.maxNodes must be an integer from 1 to 500");
179
+ if (!Number.isInteger(contextIndex.maxEdges) || contextIndex.maxEdges < 0 || contextIndex.maxEdges > 1000) errors.push("contextIndex.maxEdges must be an integer from 0 to 1000");
180
+ if (typeof contextIndex.includeTechStack !== "boolean") errors.push("contextIndex.includeTechStack must be boolean");
181
+ if (typeof contextIndex.includeMemoryPointers !== "boolean") errors.push("contextIndex.includeMemoryPointers must be boolean");
182
+ }
183
+ }
184
+
185
+ if (contextIndex.enabled && contextIndex.writePolicy !== "off") {
186
+ const indexPath = path.resolve(projectPath, contextIndex.path);
187
+ const relativeIndexPath = path.relative(projectPath, indexPath);
188
+ if (relativeIndexPath.startsWith("..") || path.isAbsolute(relativeIndexPath)) {
189
+ errors.push(`contextIndex.path escapes project root: ${contextIndex.path}`);
190
+ } else if (!fs.existsSync(indexPath)) {
191
+ warnings.push(`context index missing: ${contextIndex.path}; run /onboard-project or record piagent_context_index_record`);
192
+ }
193
+ }
194
+
195
+ const verifyEntries = Object.entries(profile.verifyCommands ?? {});
196
+ if (verifyEntries.length === 0) {
197
+ errors.push("verifyCommands has no entries");
198
+ }
199
+ for (const [key, commands] of verifyEntries) {
200
+ if (!Array.isArray(commands) || commands.length === 0) {
201
+ errors.push(`verifyCommands.${key} must be a non-empty array`);
202
+ continue;
203
+ }
204
+ for (const command of commands) {
205
+ if (typeof command !== "string" || command.trim().length === 0) {
206
+ errors.push(`verifyCommands.${key} contains an empty command`);
207
+ }
208
+ }
209
+ }
210
+
211
+ const report = {
212
+ projectPath,
213
+ profilePath,
214
+ projectId: profile.projectId,
215
+ mode: profile.mode,
216
+ rootMarkersFound: existingRootMarkers,
217
+ requiredContextCount: requiredContext.length,
218
+ protectedPathCount: protectedPaths.length,
219
+ shellProtectedPathCount: shellProtectedPaths.length,
220
+ readOnlyPathCount: readOnlyPaths.length,
221
+ verifyProfiles: verifyEntries.map(([key]) => key),
222
+ mcpCapabilities,
223
+ capabilityPacks: Array.isArray(capabilityPacks)
224
+ ? capabilityPacks
225
+ .filter((pack) => pack && typeof pack === "object" && !Array.isArray(pack))
226
+ .map((pack) => `${String(pack.name ?? "<invalid>")}@${String(pack.version ?? "<invalid>")}`)
227
+ : [],
228
+ capabilityPolicy,
229
+ runtimePolicy,
230
+ contextIndex,
231
+ warnings,
232
+ errors
233
+ };
234
+
235
+ console.log(JSON.stringify(report, null, 2));
236
+
237
+ if (errors.length > 0) process.exit(1);
238
+ NODE
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'USAGE'
6
+ Usage:
7
+ scripts/quality-benchmark.sh <project-path> --init
8
+ scripts/quality-benchmark.sh <project-path> --record --scenario <name> --surface <name> --result <pass|fail|partial> [options]
9
+
10
+ Options:
11
+ --task-file <path> File containing task prompt/spec.
12
+ --verify <command> Verification command used for the run.
13
+ --tokens <number> Total tokens reported by the agent/provider.
14
+ --cost <number> Cost reported for the run.
15
+ --duration <seconds> Wall-clock duration.
16
+ --notes <text> Short notes or quality observations.
17
+ --agent <name> Backward-compatible alias for --surface.
18
+
19
+ Output:
20
+ <project-path>/.pi/benchmarks/quality-runs.jsonl
21
+ USAGE
22
+ }
23
+
24
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
25
+ usage
26
+ exit 0
27
+ fi
28
+
29
+ PROJECT_PATH="${1:-}"
30
+ if [[ -z "$PROJECT_PATH" ]]; then
31
+ usage
32
+ exit 2
33
+ fi
34
+ shift || true
35
+
36
+ PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
37
+ MODE=""
38
+ SCENARIO=""
39
+ SURFACE=""
40
+ RESULT=""
41
+ TASK_FILE=""
42
+ VERIFY_CMD=""
43
+ TOKENS=""
44
+ COST=""
45
+ DURATION=""
46
+ NOTES=""
47
+
48
+ while [[ $# -gt 0 ]]; do
49
+ case "$1" in
50
+ --init)
51
+ MODE="init"
52
+ shift
53
+ ;;
54
+ --record)
55
+ MODE="record"
56
+ shift
57
+ ;;
58
+ --scenario)
59
+ SCENARIO="${2:-}"
60
+ shift 2
61
+ ;;
62
+ --surface|--agent)
63
+ SURFACE="${2:-}"
64
+ shift 2
65
+ ;;
66
+ --result)
67
+ RESULT="${2:-}"
68
+ shift 2
69
+ ;;
70
+ --task-file)
71
+ TASK_FILE="${2:-}"
72
+ shift 2
73
+ ;;
74
+ --verify)
75
+ VERIFY_CMD="${2:-}"
76
+ shift 2
77
+ ;;
78
+ --tokens)
79
+ TOKENS="${2:-}"
80
+ shift 2
81
+ ;;
82
+ --cost)
83
+ COST="${2:-}"
84
+ shift 2
85
+ ;;
86
+ --duration)
87
+ DURATION="${2:-}"
88
+ shift 2
89
+ ;;
90
+ --notes)
91
+ NOTES="${2:-}"
92
+ shift 2
93
+ ;;
94
+ -h|--help)
95
+ usage
96
+ exit 0
97
+ ;;
98
+ *)
99
+ echo "Unknown argument: $1" >&2
100
+ usage
101
+ exit 2
102
+ ;;
103
+ esac
104
+ done
105
+
106
+ BENCHMARK_DIR="$PROJECT_PATH/.pi/benchmarks"
107
+ mkdir -p "$BENCHMARK_DIR"
108
+
109
+ if [[ "$MODE" == "init" ]]; then
110
+ cat > "$BENCHMARK_DIR/quality-scenarios.md" <<'SCENARIOS'
111
+ # Agent quality benchmark scenarios
112
+
113
+ Use the same scenario, acceptance criteria, and verification command across approved agent surfaces before making quality, token, or cost claims.
114
+
115
+ ## Scenario 1: read-only scout
116
+
117
+ - Goal: inspect repo structure and produce implementation plan.
118
+ - Must not edit files.
119
+ - Evidence: context files read, plan, risks, verify command proposal.
120
+
121
+ ## Scenario 2: bounded source fix
122
+
123
+ - Goal: implement a small bugfix with clear acceptance criteria.
124
+ - Evidence: changed files, verify command exit 0, trace/handoff.
125
+
126
+ ## Scenario 3: backend-readonly to frontend implementation
127
+
128
+ - Goal: scout backend/spec read-only, implement frontend mapping only.
129
+ - Evidence: backend contract snapshot, frontend changed files, frontend verify pass.
130
+ SCENARIOS
131
+ echo "Initialized benchmark scenarios: $BENCHMARK_DIR/quality-scenarios.md"
132
+ exit 0
133
+ fi
134
+
135
+ if [[ "$MODE" != "record" || -z "$SCENARIO" || -z "$SURFACE" || -z "$RESULT" ]]; then
136
+ usage
137
+ exit 2
138
+ fi
139
+
140
+ case "$RESULT" in
141
+ pass|fail|partial) ;;
142
+ *)
143
+ echo "--result must be pass, fail, or partial" >&2
144
+ exit 2
145
+ ;;
146
+ esac
147
+
148
+ export PI_BENCHMARK_PROJECT_PATH="$PROJECT_PATH"
149
+ export PI_BENCHMARK_SCENARIO="$SCENARIO"
150
+ export PI_BENCHMARK_SURFACE="$SURFACE"
151
+ export PI_BENCHMARK_RESULT="$RESULT"
152
+ export PI_BENCHMARK_TASK_FILE="$TASK_FILE"
153
+ export PI_BENCHMARK_VERIFY="$VERIFY_CMD"
154
+ export PI_BENCHMARK_TOKENS="$TOKENS"
155
+ export PI_BENCHMARK_COST="$COST"
156
+ export PI_BENCHMARK_DURATION="$DURATION"
157
+ export PI_BENCHMARK_NOTES="$NOTES"
158
+
159
+ node --input-type=module <<'NODE'
160
+ import fs from "node:fs";
161
+ import path from "node:path";
162
+
163
+ const root = process.env.PI_BENCHMARK_PROJECT_PATH;
164
+ const target = path.join(root, ".pi", "benchmarks", "quality-runs.jsonl");
165
+ const numberOrNull = (value) => {
166
+ if (!value) return null;
167
+ const parsed = Number(value);
168
+ return Number.isFinite(parsed) ? parsed : null;
169
+ };
170
+ const payload = {
171
+ schemaVersion: 1,
172
+ recordedAt: new Date().toISOString(),
173
+ scenario: process.env.PI_BENCHMARK_SCENARIO,
174
+ surface: process.env.PI_BENCHMARK_SURFACE,
175
+ result: process.env.PI_BENCHMARK_RESULT,
176
+ taskFile: process.env.PI_BENCHMARK_TASK_FILE || null,
177
+ verifyCommand: process.env.PI_BENCHMARK_VERIFY || null,
178
+ tokens: numberOrNull(process.env.PI_BENCHMARK_TOKENS),
179
+ cost: numberOrNull(process.env.PI_BENCHMARK_COST),
180
+ durationSeconds: numberOrNull(process.env.PI_BENCHMARK_DURATION),
181
+ notes: process.env.PI_BENCHMARK_NOTES || null
182
+ };
183
+ fs.appendFileSync(target, `${JSON.stringify(payload)}\n`);
184
+ console.log(`Recorded benchmark run: ${target}`);
185
+ NODE
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { performance } from "node:perf_hooks";
3
+ import { pathToFileURL } from "node:url";
4
+ import {
5
+ redactForStorage,
6
+ redactSensitiveText
7
+ } from "../packages/piagent-core/security/sensitive-data.js";
8
+
9
+ const REDACTION = "[REDACTED_SECRET]";
10
+ const join = (...parts) => parts.join("");
11
+
12
+ const contextualCases = [
13
+ ["aws-assignment", "assignment", join("AWS_SECRET_ACCESS_KEY=", "wJalrXUtnFEMI/", "K7MDENG/bPxRfiCYEXAMPLEKEY")],
14
+ ["aws-whitespace", "assignment", join("aws_secret_access_key ", "wJalrXUtnFEMI/", "K7MDENG/bPxRfiCYEXAMPLEKEY")],
15
+ ["github-fine", "vendor", join("github_pat_", "11AAAAAAA0", "abcdefghijklmnopqrstuvwxyz")],
16
+ ["github-classic", "vendor", join("ghp_", "abcdefghijklmnopqrstuvwxyz1234567890")],
17
+ ["openai-project", "vendor", join("sk-", "proj-", "abcdefghijklmnopqrstuvwxyz1234567890")],
18
+ ["stripe-live", "vendor", join("sk_", "live_", "51NxTExampleSecretValue123456")],
19
+ ["slack-bot", "vendor", join("xoxb-", "123456789012-123456789012-", "AbCdEfGhIjKlMnOp")],
20
+ ["google-api", "vendor", join("AIza", "SyDExampleKeyWithEnoughLength123456")],
21
+ ["jwt", "vendor", join("eyJhbGciOiJIUzI1NiJ9.", "eyJzdWIiOiIxMjM0NTY3ODkwIn0.", "signature123456")],
22
+ ["pem-pkcs8", "private-key", join("-----BEGIN ", "PRIVATE KEY-----\nabc123\n-----END PRIVATE KEY-----")],
23
+ ["pem-openssh", "private-key", join("-----BEGIN ", "OPENSSH PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----")],
24
+ ["postgres-url", "credential-url", "postgres://app:synthetic-pass-42@db.example.invalid/prod"],
25
+ ["mysql-url", "credential-url", "mysql://app:synthetic-pass-42@db.example.invalid/prod"],
26
+ ["mariadb-url", "credential-url", "mariadb://app:synthetic-pass-42@db.example.invalid/prod"],
27
+ ["mongodb-url", "credential-url", "mongodb://app:synthetic-pass-42@db.example.invalid/prod"],
28
+ ["redis-url", "credential-url", "redis://app:synthetic-pass-42@cache.example.invalid/0"],
29
+ ["http-userinfo", "credential-url", "https://robot:synthetic-http-pass-42@example.invalid/api"],
30
+ ["ftp-userinfo", "credential-url", "ftp://robot:synthetic-ftp-pass-42@example.invalid/archive"],
31
+ ["bearer", "authorization", "Authorization: Bearer syntheticBearerToken_1234567890"],
32
+ ["basic-auth", "authorization", "Authorization: Basic dXNlcjpzeW50aGV0aWMtcGFzcy00Mg=="],
33
+ ["token-auth", "authorization", "Authorization: Token synthetic-auth-token-123"],
34
+ ["api-key-auth", "authorization", "Authorization: ApiKey synthetic-auth-key-123"],
35
+ ["digest-auth", "authorization", "Authorization: Digest username=robot, response=synthetic-response-123"],
36
+ ["password-key", "assignment", "PASSWORD=synthetic-pass-42"],
37
+ ["pwd-key", "assignment", "pwd: synthetic-pass-42"],
38
+ ["api-key-json", "assignment", "{\"api_key\":\"synthetic-key-123456\"}"],
39
+ ["client-secret", "assignment", "client_secret=synthetic-client-123"],
40
+ ["access-token", "assignment", "access_token=synthetic-access-123"],
41
+ ["refresh-token", "assignment", "refresh_token=synthetic-refresh-123"],
42
+ ["credential-key", "assignment", "credential: synthetic-credential-123"],
43
+ ["prefixed-token", "assignment", "CI_JOB_TOKEN=synthetic-job-token-123"],
44
+ ["alphabetic-token", "assignment", "token=abcdefghijklmnopqrstuvwx"],
45
+ ["alphabetic-api-key", "assignment", "apiKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
46
+ ["alphabetic-authorization", "authorization", "Authorization: Token abcdefghijklmnopqrstuvwx"],
47
+ ["webhook-secret", "assignment", "WEBHOOK_SECRET=synthetic-webhook-123"],
48
+ ["session-token", "assignment", "session_token=synthetic-session-123"],
49
+ ["query-token", "assignment", "https://example.invalid/callback?access_token=synthetic-query-token-123"],
50
+ ["query-refresh-token", "assignment", "https://example.invalid/callback?refresh_token=synthetic-query-refresh-123&ok=1"],
51
+ ["hyphen-header", "assignment", "X-Api-Key: synthetic-header-key-123"],
52
+ ["private-token-header", "assignment", "PRIVATE-TOKEN: synthetic-private-token-123"],
53
+ ["camel-api-key", "assignment", "{\"apiKey\":\"synthetic-camel-key-123\"}"],
54
+ ["camel-client-secret", "assignment", "{\"clientSecret\":\"synthetic-camel-secret-123\"}"],
55
+ ["camel-access-token", "assignment", "{\"accessToken\":\"synthetic-camel-access-123\"}"],
56
+ ["passphrase", "assignment", "passphrase=synthetic-passphrase-123"],
57
+ ["private-key-field", "assignment", "{\"private_key\":\"synthetic-private-material-123\"}"],
58
+ ["signing-key-field", "assignment", "signingKey=synthetic-signing-material-123"],
59
+ ["quoted-export", "assignment", "export TOKEN='synthetic-export-token-123'"],
60
+ ["quoted-password-spaces", "assignment", "password=\"Correct Horse Battery 42\""],
61
+ ["quoted-client-secret-spaces", "assignment", "clientSecret='synthetic client secret 123'"],
62
+ ["quoted-password-escape", "assignment", "password=\"Correct \\\"Horse\\\" Battery 42\""],
63
+ ["quoted-secret-escape", "assignment", "secret='synthetic \\'quoted\\' secret 123'"],
64
+ ["multiline", "assignment", "name=service\npassword=synthetic-multiline-42\nstatus=ok"]
65
+ ].map(([name, category, text]) => ({ name, category, text }));
66
+
67
+ const unlabeledEntropyCases = [
68
+ ["base64-32", "QWxhZGRpbjpvcGVuIHNlc2FtZTEyMzQ1Njc4"],
69
+ ["urlsafe-40", "Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8eF0gJ5hN"],
70
+ ["mixed-48", "aB9xP2mN7qR4tV8zK1cD6fG0hJ3lS5wY9uE2iO7p"],
71
+ ["hex-64", "9f86d081884c7d659a2feaa0c55ad015bf4f1b2b0b822cd15d6c15b0f00a08aa"],
72
+ ["base32-52", "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSQ"],
73
+ ["opaque-id", "01J4Z3N8K2Q7V5X9C1B6M0R4T8Y2W7S5"]
74
+ ].map(([name, text]) => ({ name, text }));
75
+
76
+ const benignCases = [
77
+ ["prose-secret", "The secret: always run tests before final."],
78
+ ["null-token", "token: null"],
79
+ ["placeholder", "api_key: placeholder"],
80
+ ["short-password", "password: short"],
81
+ ["unset-secret", "clientSecret=<not-set>"],
82
+ ["source-code", "if (token === undefined) return;"],
83
+ ["git-sha", "commit 4431c9edaf7c72b56943d0c74c17ee0214690eb5"],
84
+ ["sha256", "sha256:d8a46b50ea37b68544add1e73ccb382b9ba06f6fe673dd82c50b294fdf412ec6"],
85
+ ["uuid", "request_id=123e4567-e89b-12d3-a456-426614174000"],
86
+ ["example-url", "https://example.invalid/docs"],
87
+ ["package-integrity", "integrity sha512-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789+/=="],
88
+ ["public-key", "-----BEGIN PUBLIC KEY-----\nabc123\n-----END PUBLIC KEY-----"],
89
+ ["artifact-id", "artifact_id=Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8"],
90
+ ["trace-id", "trace_id=aB9xP2mN7qR4tV8zK1cD6fG0hJ3lS5wY9uE2"],
91
+ ["checksum", "checksum=9f86d081884c7d659a2feaa0c55ad015"],
92
+ ["model-id", "model=provider-model-2026-07-21"],
93
+ ["file-name", "generated file auth.json.example"],
94
+ ["command-doc", "Run npm test and tsc --noEmit."],
95
+ ["boolean-secret", "secret=false"],
96
+ ["redacted-marker", "password=[REDACTED_SECRET]"],
97
+ ["quoted-redacted-marker", "password=\"[REDACTED_SECRET]\""],
98
+ ["authorization-placeholder", "Authorization: Token placeholder"],
99
+ ["quoted-placeholder", "clientSecret='not-set'"],
100
+ ["your-key", "Set apiKey=your-key-here before local testing."],
101
+ ["ordinary-number", "retry_token_count=12"],
102
+ ["semantic-token-name", "The parser emits a token node."],
103
+ ["password-policy", "Password policy requires twelve characters."],
104
+ ["credential-doc", "Credential rotation is required quarterly."],
105
+ ["password-policy-field", "passwordPolicy=minimum-length"],
106
+ ["semantic-token-type-field", "semanticTokenType=namespace-declaration"]
107
+ ].map(([name, text]) => ({ name, text }));
108
+
109
+ const structuredInput = {
110
+ password: "synthetic-structured-pass-42",
111
+ apiKey: "synthetic-structured-key-42",
112
+ nested: {
113
+ clientSecret: "synthetic-structured-client-42",
114
+ sessionToken: "synthetic-structured-session-42"
115
+ },
116
+ tokens: ["synthetic-array-token-123"],
117
+ apiKeys: ["synthetic-array-key-123"],
118
+ passwords: ["hunter42"],
119
+ credentials: ["userpass"],
120
+ id: "Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8",
121
+ checksum: "9f86d081884c7d659a2feaa0c55ad015",
122
+ labels: ["provider-model-2026-07-21"],
123
+ semanticTokens: ["namespace-declaration-123"],
124
+ colorTokens: ["primary-blue-500"],
125
+ passwordPolicy: "minimum-length",
126
+ semanticTokenType: "namespace-declaration"
127
+ };
128
+
129
+ function percentage(value, total) {
130
+ return total === 0 ? 1 : value / total;
131
+ }
132
+
133
+ export function evaluateRedactionBenchmark(options = {}) {
134
+ const contextualMisses = contextualCases
135
+ .filter(({ text }) => !redactSensitiveText(text).redacted)
136
+ .map(({ name }) => name);
137
+ const entropyDetected = unlabeledEntropyCases
138
+ .filter(({ text }) => redactSensitiveText(text).redacted)
139
+ .map(({ name }) => name);
140
+ const falsePositives = benignCases
141
+ .filter(({ text }) => redactSensitiveText(text).redacted)
142
+ .map(({ name }) => name);
143
+ const safeStructured = redactForStorage(structuredInput);
144
+ const structuredSensitiveChecks = [
145
+ safeStructured.password === REDACTION,
146
+ safeStructured.apiKey === REDACTION,
147
+ safeStructured.nested?.clientSecret === REDACTION,
148
+ safeStructured.nested?.sessionToken === REDACTION,
149
+ safeStructured.tokens?.[0] === REDACTION,
150
+ safeStructured.apiKeys?.[0] === REDACTION,
151
+ safeStructured.passwords?.[0] === REDACTION,
152
+ safeStructured.credentials?.[0] === REDACTION
153
+ ];
154
+ const structuredBenignChecks = [
155
+ safeStructured.id === structuredInput.id,
156
+ safeStructured.checksum === structuredInput.checksum,
157
+ safeStructured.labels?.[0] === structuredInput.labels[0],
158
+ safeStructured.semanticTokens?.[0] === structuredInput.semanticTokens[0],
159
+ safeStructured.colorTokens?.[0] === structuredInput.colorTokens[0],
160
+ safeStructured.passwordPolicy === structuredInput.passwordPolicy,
161
+ safeStructured.semanticTokenType === structuredInput.semanticTokenType
162
+ ];
163
+
164
+ const iterations = Math.max(1, Math.min(Number(options.iterations ?? 250), 10_000));
165
+ const workload = [...contextualCases, ...unlabeledEntropyCases, ...benignCases];
166
+ const started = performance.now();
167
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
168
+ for (const { text } of workload) redactSensitiveText(text);
169
+ }
170
+ const durationMs = performance.now() - started;
171
+ const evaluations = iterations * workload.length;
172
+ const largeOutput = `${"ordinary build output\n".repeat(8_192)}X-Api-Key: synthetic-large-output-key-123`;
173
+ const largeStarted = performance.now();
174
+ const largeResult = redactSensitiveText(largeOutput);
175
+ const largeDurationMs = performance.now() - largeStarted;
176
+
177
+ const result = {
178
+ contextual: {
179
+ total: contextualCases.length,
180
+ detected: contextualCases.length - contextualMisses.length,
181
+ recall: percentage(contextualCases.length - contextualMisses.length, contextualCases.length),
182
+ missed: contextualMisses
183
+ },
184
+ unlabeledEntropy: {
185
+ total: unlabeledEntropyCases.length,
186
+ detected: entropyDetected.length,
187
+ observationalRecall: percentage(entropyDetected.length, unlabeledEntropyCases.length),
188
+ detectedCases: entropyDetected,
189
+ gated: false
190
+ },
191
+ benign: {
192
+ total: benignCases.length,
193
+ falsePositives: falsePositives.length,
194
+ falsePositiveRate: percentage(falsePositives.length, benignCases.length),
195
+ cases: falsePositives
196
+ },
197
+ structured: {
198
+ sensitiveTotal: structuredSensitiveChecks.length,
199
+ sensitiveRedacted: structuredSensitiveChecks.filter(Boolean).length,
200
+ benignTotal: structuredBenignChecks.length,
201
+ benignPreserved: structuredBenignChecks.filter(Boolean).length
202
+ },
203
+ performance: {
204
+ iterations,
205
+ evaluations,
206
+ durationMs: Number(durationMs.toFixed(3)),
207
+ averageMicroseconds: Number(((durationMs * 1000) / evaluations).toFixed(3))
208
+ },
209
+ largeOutput: {
210
+ bytes: Buffer.byteLength(largeOutput),
211
+ durationMs: Number(largeDurationMs.toFixed(3)),
212
+ sensitiveValueRedacted: largeResult.redacted && !largeResult.text.includes("synthetic-large-output-key-123")
213
+ }
214
+ };
215
+ result.ok = result.contextual.recall === 1
216
+ && result.benign.falsePositives === 0
217
+ && result.structured.sensitiveRedacted === result.structured.sensitiveTotal
218
+ && result.structured.benignPreserved === result.structured.benignTotal
219
+ && result.largeOutput.sensitiveValueRedacted;
220
+ return result;
221
+ }
222
+
223
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
224
+ const result = evaluateRedactionBenchmark();
225
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
226
+ if (!result.ok) process.exitCode = 1;
227
+ }