@piagent/platform 1.2.12 → 1.2.14

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 (70) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +17 -3
  3. package/README.vi.md +90 -0
  4. package/architecture/layers.json +117 -0
  5. package/docs/README.md +23 -0
  6. package/docs/architecture.md +7 -61
  7. package/docs/command-reference-vietnamese.md +6 -6
  8. package/docs/distribution-standard.md +7 -7
  9. package/docs/en/README.md +13 -0
  10. package/docs/en/architecture.md +81 -0
  11. package/docs/en/maintainer-guide.md +121 -0
  12. package/docs/languages.json +40 -0
  13. package/docs/operator-manual-vietnamese.md +7 -7
  14. package/docs/package-architecture-notes.md +9 -0
  15. package/docs/quality-benchmark.md +93 -0
  16. package/docs/release-install-policy.md +6 -6
  17. package/docs/security-threat-model.md +1 -1
  18. package/docs/team-onboarding.md +2 -2
  19. package/docs/vi/README.md +13 -0
  20. package/docs/vi/architecture.md +81 -0
  21. package/docs/vi/maintainer-guide.md +121 -0
  22. package/docs-site/architecture.html +39 -14
  23. package/docs-site/assets/docs.css +407 -0
  24. package/docs-site/benchmark.html +341 -0
  25. package/docs-site/commands.html +4 -1
  26. package/docs-site/content/architecture.html +35 -13
  27. package/docs-site/content/benchmark.html +214 -0
  28. package/docs-site/content/index.html +5 -0
  29. package/docs-site/context-engine.html +6 -3
  30. package/docs-site/index.html +9 -1
  31. package/docs-site/mcp-approval.html +4 -1
  32. package/docs-site/mcp-auth.html +4 -1
  33. package/docs-site/mcp-servers.html +4 -1
  34. package/docs-site/mcp.html +4 -1
  35. package/docs-site/permissions.html +4 -1
  36. package/docs-site/profiles.html +4 -1
  37. package/docs-site/quickstart.html +4 -1
  38. package/docs-site/runtime.html +6 -3
  39. package/docs-site/scope.html +4 -1
  40. package/docs-site/security.html +4 -1
  41. package/docs-site/team.html +4 -1
  42. package/docs-site/workflows.html +4 -1
  43. package/package.json +5 -1
  44. package/packages/piagent-core/README.md +6 -2
  45. package/packages/piagent-core/capabilities/capability-core.js +3 -0
  46. package/packages/piagent-core/capabilities/runtime-integrity.js +18 -0
  47. package/packages/piagent-core/extensions/guard-types.ts +0 -37
  48. package/packages/piagent-core/extensions/piagent-guard.ts +170 -2099
  49. package/packages/piagent-core/extensions/task-state.js +22 -3
  50. package/packages/piagent-core/package.json +1 -1
  51. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +213 -0
  52. package/packages/piagent-core/runtime/hooks/completion-hook.ts +203 -0
  53. package/packages/piagent-core/runtime/hooks/input-hook.ts +122 -0
  54. package/packages/piagent-core/runtime/hooks/session-hooks.ts +104 -0
  55. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +140 -0
  56. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +316 -0
  57. package/packages/piagent-core/runtime/input/chat-images.ts +299 -0
  58. package/packages/piagent-core/runtime/runtime-limits.ts +21 -0
  59. package/packages/piagent-core/runtime/session/message-signals.ts +64 -0
  60. package/packages/piagent-core/runtime/session/runtime-state.ts +175 -0
  61. package/packages/piagent-core/runtime/session/system-prompt.ts +52 -0
  62. package/packages/piagent-core/runtime/session/tool-result-compaction.ts +352 -0
  63. package/packages/piagent-core/runtime/session/usage.ts +178 -0
  64. package/packages/piagent-core/runtime/tools/tool-groups.ts +86 -0
  65. package/packages/piagent-core/runtime/workflows/input-routing.ts +113 -0
  66. package/packages/piagent-core/runtime/workflows/task-intake.ts +73 -0
  67. package/scripts/build-docs-site.mjs +10 -1
  68. package/scripts/check-architecture.mjs +132 -0
  69. package/scripts/check-doc-languages.mjs +67 -0
  70. package/scripts/verify-local.sh +31 -0
@@ -0,0 +1,113 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { redactSensitiveText } from "../../extensions/redaction-core.js";
5
+ import { LONG_INPUT_CHARS, MAX_INLINE_COLLAPSED_TASK_CHARS } from "../runtime-limits.ts";
6
+
7
+ export type FreshWorkflow = "task" | "scout" | "be-to-fe";
8
+
9
+ export function looksLikeGovernedBoilerplate(text: string): boolean {
10
+ const lower = text.toLowerCase();
11
+ const markers = [
12
+ "mandatory flow",
13
+ "piagent_context",
14
+ "piagent_task_start",
15
+ "piagent_context_record",
16
+ "piagent_verify_record",
17
+ "piagent_task_gate_check",
18
+ "output format"
19
+ ];
20
+ return markers.filter((marker) => lower.includes(marker)).length >= 3;
21
+ }
22
+
23
+ function extractFencedBlockAfter(label: RegExp, text: string): string | undefined {
24
+ const labelMatch = label.exec(text);
25
+ if (!labelMatch || labelMatch.index === undefined) return undefined;
26
+ const rest = text.slice(labelMatch.index + labelMatch[0].length);
27
+ const fenced = /```(?:text|md|markdown)?\s*([\s\S]*?)```/i.exec(rest);
28
+ return fenced?.[1]?.trim();
29
+ }
30
+
31
+ export function stripLeadingWorkflowCommand(input: string): string {
32
+ return input
33
+ .replace(/^\/(?:piagent-workflow|workflow)\s+(?:task|scout|be-to-fe|review|plan|platform-improve|discuss|commit|pr)\b\s*/i, "")
34
+ .replace(/^\/(?:task|scout|be-to-fe|review|plan|platform-improve|discuss|commit|pr)\b\s*/i, "")
35
+ .trim();
36
+ }
37
+
38
+ export function extractTaskRequest(text: string): string {
39
+ const labeled =
40
+ extractFencedBlockAfter(/(?:implement|scout|review|plan)\s+(?:this\s+)?task\s*:?\s*/i, text) ??
41
+ extractFencedBlockAfter(/request\s*:?\s*/i, text);
42
+ if (labeled) return stripLeadingWorkflowCommand(labeled);
43
+
44
+ const firstFence = /```(?:text|md|markdown)?\s*([\s\S]*?)```/i.exec(text);
45
+ if (firstFence?.[1]?.trim()) return stripLeadingWorkflowCommand(firstFence[1].trim());
46
+
47
+ return stripLeadingWorkflowCommand(text.trim());
48
+ }
49
+
50
+ export function trimTaskForInline(input: string): string {
51
+ const normalized = stripLeadingWorkflowCommand(input).trim().replace(/\n{3,}/g, "\n\n");
52
+ if (normalized.length <= MAX_INLINE_COLLAPSED_TASK_CHARS) return normalized;
53
+ return `${normalized.slice(0, MAX_INLINE_COLLAPSED_TASK_CHARS).trim()}\n\n[Input truncated by piagent preflight. Put the full spec in a project file and reference that file.]`;
54
+ }
55
+
56
+ export function chooseFreshWorkflow(original: string, task: string): FreshWorkflow {
57
+ const semantic = stripLeadingWorkflowCommand(task || original).toLowerCase();
58
+ const starts = original.trim().toLowerCase();
59
+ const workflowStart = starts.match(/^\/(?:piagent-workflow|workflow)\s+(task|scout|be-to-fe)\b/);
60
+ if (workflowStart?.[1] === "be-to-fe") return "be-to-fe";
61
+ if (workflowStart?.[1] === "scout") return "scout";
62
+ if (workflowStart?.[1] === "task") return "task";
63
+ if (starts.startsWith("/be-to-fe")) return "be-to-fe";
64
+ if (starts.startsWith("/scout")) return "scout";
65
+ const asksForWrite = /\b(implement|support|surface|consume|write|change|fix)\b/.test(semantic);
66
+ if (/\b(scout|read-only|read only|audit|mapping|mapping matrix|map contract)\b/.test(semantic) && !asksForWrite) {
67
+ return "scout";
68
+ }
69
+ if (/\b(be|backend)\b/.test(semantic) && /\b(fe|frontend)\b/.test(semantic) && asksForWrite) {
70
+ return "be-to-fe";
71
+ }
72
+ return "task";
73
+ }
74
+
75
+ export function isPiagentWorkflowInput(text: string): boolean {
76
+ return /^\/(?:piagent-workflow|workflow|task|be-to-fe|scout|review|plan|platform-improve)\b/i.test(text.trim());
77
+ }
78
+
79
+ export function isFreshOrUtilityInput(text: string): boolean {
80
+ return /^\/(?:usage|logs|context|commands|permission|memory|onboard|name|fresh|fresh-task|fresh-scout|fresh-be-to-fe|task-preflight|piagent-usage|piagent-logs|piagent-session|piagent-context|piagent-commands|piagent-permission|model-options|memory-policy|onboard-project|setname|session|compact)\b/i.test(text.trim());
81
+ }
82
+
83
+ function taskInboxDir(cwd: string): string {
84
+ return path.join(cwd, ".pi", "task-inbox");
85
+ }
86
+
87
+ function writeTaskInbox(cwd: string, workflow: string, text: string): string {
88
+ fs.mkdirSync(taskInboxDir(cwd), { recursive: true });
89
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
90
+ const safeWorkflow = workflow.replace(/[^a-z0-9-]+/gi, "-").toLowerCase() || "task";
91
+ const fileName = `${stamp}-${safeWorkflow}.md`;
92
+ const absolute = path.join(taskInboxDir(cwd), fileName);
93
+ fs.writeFileSync(absolute, `${redactSensitiveText(text).text}\n`);
94
+ return path.relative(cwd, absolute).split(path.sep).join("/");
95
+ }
96
+
97
+ export function buildFreshCommand(cwd: string, workflow: FreshWorkflow, originalText: string, reason: string): string {
98
+ const task = extractTaskRequest(originalText);
99
+ if (originalText.length >= LONG_INPUT_CHARS) {
100
+ const intakePath = writeTaskInbox(cwd, workflow, originalText);
101
+ return `/fresh ${workflow} Read task intake from ${intakePath}. ${reason}`;
102
+ }
103
+ return `/fresh ${workflow} ${trimTaskForInline(task)}`;
104
+ }
105
+
106
+ export function shortTaskLabel(text: string): string {
107
+ const compact = text
108
+ .replace(/```[\s\S]*?```/g, " ")
109
+ .replace(/[^A-Za-z0-9\u00C0-\u1EF9_-]+/g, " ")
110
+ .trim()
111
+ .slice(0, 64);
112
+ return compact || "piagent task";
113
+ }
@@ -0,0 +1,73 @@
1
+ import { classifyContextTask } from "../../extensions/context-engine.js";
2
+ import { matchesProtectedPath, normalizePathCandidate } from "../../extensions/policy-core.js";
3
+ import type { ReviewLens } from "../../extensions/guard-types.js";
4
+ import { LONG_INPUT_CHARS } from "../runtime-limits.ts";
5
+
6
+ const AUTO_INTAKE_MAX_PROMPT_CHARS = 700;
7
+ const AUTO_INTAKE_CHANGE_INTENT = /\b(?:add|build|change|correct|create|fix|implement|modify|refactor|remove|rename|replace|update|write|sua|them|doi|cap nhat|xoa|tao)\b/i;
8
+ const AUTO_INTAKE_READ_ONLY_LEAD = /^\s*\/?(?:analy[sz]e|audit|check|discuss|explain|inspect|plan|research|review|scout|summari[sz]e|why|how|can\s+(?:you|we)|kiem tra|nghien cuu|giai thich|danh gia)\b/i;
9
+ const AUTO_INTAKE_MANUAL_RISK = /\b(?:credential|database|deploy|destructive|encryption|external provider|migration|payment|permission|production|publish|release|secret|schema migration|token rotation)\b/i;
10
+
11
+ export const AUTO_INTAKE_SNAPSHOT_PATTERNS = [
12
+ "src/**", "app/**", "lib/**", "packages/**", "test/**", "tests/**", "spec/**", "__tests__/**"
13
+ ];
14
+
15
+ function uniqueStrings(values: string[]): string[] {
16
+ return [...new Set(values)];
17
+ }
18
+
19
+ export function automaticTaskIntakeEligible(prompt: string, readProtectedPaths: string[]): boolean {
20
+ const text = String(prompt ?? "").trim();
21
+ if (!text || text.length > AUTO_INTAKE_MAX_PROMPT_CHARS) return false;
22
+ const signal = classifyContextTask(text);
23
+ if (signal.workflow !== "task" || !AUTO_INTAKE_CHANGE_INTENT.test(text)) return false;
24
+ if (AUTO_INTAKE_READ_ONLY_LEAD.test(text) || AUTO_INTAKE_MANUAL_RISK.test(text)) return false;
25
+ if (/\bpiagent_task_start\b/i.test(text)) return false;
26
+ return !signal.paths.some((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
27
+ }
28
+
29
+ export function manualTaskIntakeEligible(prompt: string, readProtectedPaths: string[]): boolean {
30
+ const text = String(prompt ?? "").trim();
31
+ if (!text || text.length > LONG_INPUT_CHARS) return false;
32
+ const signal = classifyContextTask(text);
33
+ if (signal.workflow !== "task" || !AUTO_INTAKE_CHANGE_INTENT.test(text)) return false;
34
+ if (AUTO_INTAKE_READ_ONLY_LEAD.test(text) || /\bpiagent_task_start\b/i.test(text)) return false;
35
+ if (signal.paths.length === 0) return true;
36
+ return signal.paths.some((candidate) => !matchesProtectedPath(candidate, readProtectedPaths));
37
+ }
38
+
39
+ export function automaticTaskScope(prompt: string, context: Array<{ path: string }>): string[] {
40
+ const signal = classifyContextTask(prompt);
41
+ const explicit = signal.paths.filter((candidate) => candidate && candidate !== "." && !candidate.startsWith(".pi/"));
42
+ const navigated = context
43
+ .map((item) => normalizePathCandidate(item.path))
44
+ .filter((candidate): candidate is string => Boolean(
45
+ candidate
46
+ && candidate !== "."
47
+ && !candidate.startsWith(".pi/")
48
+ && !["AGENTS.md", "README.md", "REVIEW_GUIDELINES.md"].includes(candidate)
49
+ && /^(?:app|lib|packages|spec|src|test|tests|__tests__)\//.test(candidate)
50
+ ))
51
+ .slice(0, 8);
52
+ const scope = uniqueStrings([...explicit, ...navigated]);
53
+ if (scope.length === 0) scope.push("src/**", "app/**", "lib/**");
54
+ if (/\b(?:test|tests|verification|verify|kiem thu)\b/i.test(prompt)) {
55
+ scope.push("test/**", "tests/**", "spec/**", "__tests__/**");
56
+ }
57
+ return uniqueStrings(scope);
58
+ }
59
+
60
+ export function automaticReviewLenses(prompt: string): ReviewLens[] {
61
+ const lenses: ReviewLens[] = ["correctness", "tests", "scope"];
62
+ if (/\b(?:auth|authorization|credential|permission|security|session|secret|xac thuc|phan quyen|bao mat)\b/i.test(prompt)) {
63
+ lenses.push("security");
64
+ }
65
+ return lenses;
66
+ }
67
+
68
+ export function validTaskScopePattern(value: string): boolean {
69
+ const candidate = String(value ?? "").trim().replaceAll("\\", "/");
70
+ if (!candidate || /\s|\0/.test(candidate)) return false;
71
+ if (candidate.startsWith("/") || candidate.startsWith("~/") || /^[A-Za-z]:\//.test(candidate)) return false;
72
+ return !candidate.split("/").includes("..");
73
+ }
@@ -21,6 +21,7 @@ const SITE = {
21
21
  title: "Pi Agent Platform Docs",
22
22
  origin: "https://piagent.io.vn",
23
23
  repo: "https://github.com/Vt-mmm/piagent",
24
+ englishDocs: "https://github.com/Vt-mmm/piagent/blob/main/docs/en/README.md",
24
25
  facebook: "https://www.facebook.com/vinhtam0544/"
25
26
  };
26
27
 
@@ -130,6 +131,12 @@ const NAV = [
130
131
  title: "Ít context hơn, giữ nguyên chất lượng model",
131
132
  lead: "Index local, retrieval có xếp hạng, tool loading theo task, semantic compaction và telemetry minh bạch cho Agent Watch."
132
133
  },
134
+ {
135
+ slug: "benchmark",
136
+ nav: "Benchmark",
137
+ title: "Bằng chứng chất lượng và hiệu suất",
138
+ lead: "Production benchmark so sánh Piagent với surface codex-cli trên cùng model, cùng thinking level, 18 nhóm bài và 108 session được chấm tự động."
139
+ },
133
140
  {
134
141
  slug: "runtime",
135
142
  nav: "Runtime tools",
@@ -294,7 +301,8 @@ function renderPage(page, index, body, version) {
294
301
  <span>Pi Agent Platform</span>
295
302
  </a>
296
303
  <div class="top-actions">
297
- <span class="pill"><span class="pill-dot"></span>v${version} docs</span>
304
+ <span class="pill"><span class="pill-dot"></span>v${version} docs · VI</span>
305
+ <a class="button" href="${SITE.englishDocs}" lang="en" target="_blank" rel="noopener noreferrer">English docs</a>
298
306
  <a class="button social" href="${SITE.repo}" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
299
307
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
300
308
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -329,6 +337,7 @@ ${renderPager(index)}
329
337
  <div class="footer-links" aria-label="External links">
330
338
  <a href="${SITE.origin}" target="_blank" rel="noopener noreferrer">piagent.io.vn</a>
331
339
  <a href="${SITE.repo}" target="_blank" rel="noopener noreferrer">GitHub repository</a>
340
+ <a href="${SITE.englishDocs}" lang="en" target="_blank" rel="noopener noreferrer">English docs</a>
332
341
  <a href="${SITE.facebook}" target="_blank" rel="noopener noreferrer">Facebook profile</a>
333
342
  </div>
334
343
  </footer>
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const defaultConfigPath = path.join(repoRoot, "architecture", "layers.json");
8
+ const SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
9
+
10
+ function normalizeRelative(value) {
11
+ return value.split(path.sep).join("/").replace(/^\.\//, "");
12
+ }
13
+
14
+ function sourceLineCount(text) {
15
+ if (!text) return 0;
16
+ const lines = text.split(/\r?\n/);
17
+ return lines.at(-1) === "" ? lines.length - 1 : lines.length;
18
+ }
19
+
20
+ function readConfig(configPath = defaultConfigPath) {
21
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
22
+ if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.layers)) {
23
+ throw new Error(`Unsupported architecture config: ${normalizeRelative(path.relative(repoRoot, configPath))}`);
24
+ }
25
+ return parsed;
26
+ }
27
+
28
+ function layerFor(relativeFile, config) {
29
+ const normalized = normalizeRelative(relativeFile);
30
+ for (const layer of config.layers) {
31
+ if ((layer.files ?? []).includes(normalized)) return layer.name;
32
+ if ((layer.roots ?? []).some((root) => normalized === root || normalized.startsWith(`${root}/`))) return layer.name;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ function walkSources(root) {
38
+ if (!fs.existsSync(root)) return [];
39
+ const files = [];
40
+ const stack = [root];
41
+ while (stack.length > 0) {
42
+ const current = stack.pop();
43
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
44
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") continue;
45
+ const absolute = path.join(current, entry.name);
46
+ if (entry.isDirectory()) stack.push(absolute);
47
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) files.push(absolute);
48
+ }
49
+ }
50
+ return files.sort();
51
+ }
52
+
53
+ function moduleSpecifiers(file, text) {
54
+ const specifiers = [];
55
+ const patterns = [
56
+ /\b(?:import|export)\s+(?:type\s+)?[\w*{},\s]+?\s+from\s+["']([^"']+)["']/g,
57
+ /\bimport\s*["']([^"']+)["']/g,
58
+ /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g
59
+ ];
60
+ for (const pattern of patterns) {
61
+ for (const match of text.matchAll(pattern)) specifiers.push(match[1]);
62
+ }
63
+ return specifiers;
64
+ }
65
+
66
+ function inspectArchitecture({ root = repoRoot, config = readConfig() } = {}) {
67
+ const errors = [];
68
+ const files = [
69
+ ...walkSources(path.join(root, "packages", "piagent-core")),
70
+ ...walkSources(path.join(root, "scripts"))
71
+ ];
72
+ const counts = {};
73
+
74
+ for (const absolute of files) {
75
+ const relative = normalizeRelative(path.relative(root, absolute));
76
+ const layer = layerFor(relative, config);
77
+ if (!layer) {
78
+ errors.push(`${relative}: source file is not assigned to an architecture layer`);
79
+ continue;
80
+ }
81
+ counts[layer] = (counts[layer] ?? 0) + 1;
82
+ const text = fs.readFileSync(absolute, "utf8");
83
+ const lineCount = sourceLineCount(text);
84
+ const budget = config.lineBudgets.files[relative] ?? config.lineBudgets.defaults[layer];
85
+ if (Number.isInteger(budget) && lineCount > budget) {
86
+ errors.push(`${relative}: ${lineCount} lines exceeds the ${layer} budget of ${budget}`);
87
+ }
88
+
89
+ for (const specifier of moduleSpecifiers(relative, text)) {
90
+ if (!specifier.startsWith(".")) continue;
91
+ const target = normalizeRelative(path.relative(root, path.resolve(path.dirname(absolute), specifier)));
92
+ const targetLayer = layerFor(target, config);
93
+ if (!targetLayer) continue;
94
+ const allowed = config.allowedDependencies[layer] ?? [];
95
+ if (!allowed.includes(targetLayer)) {
96
+ errors.push(`${relative}: ${layer} cannot import ${targetLayer} module ${specifier}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ return { ok: errors.length === 0, errors, files: files.length, layers: counts };
102
+ }
103
+
104
+ function main(argv = process.argv.slice(2)) {
105
+ const json = argv.includes("--json");
106
+ const unknown = argv.filter((argument) => argument !== "--json");
107
+ if (unknown.length > 0) {
108
+ process.stderr.write(`FAIL: unknown option ${unknown[0]}\n`);
109
+ return 1;
110
+ }
111
+ let result;
112
+ try {
113
+ result = inspectArchitecture();
114
+ } catch (error) {
115
+ process.stderr.write(`FAIL: ${error instanceof Error ? error.message : String(error)}\n`);
116
+ return 1;
117
+ }
118
+ if (!result.ok) {
119
+ process.stderr.write(`FAIL: architecture check found ${result.errors.length} problem(s)\n${result.errors.map((error) => `- ${error}`).join("\n")}\n`);
120
+ return 1;
121
+ }
122
+ process.stdout.write(json
123
+ ? `${JSON.stringify(result)}\n`
124
+ : `PASS: ${result.files} source files respect architecture boundaries and line budgets\n`);
125
+ return 0;
126
+ }
127
+
128
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
129
+ process.exit(main());
130
+ }
131
+
132
+ export { inspectArchitecture, layerFor, main, readConfig, sourceLineCount };
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const manifestPath = path.join(repoRoot, "docs", "languages.json");
8
+
9
+ function markdownRelativeLink(from, to) {
10
+ const relative = path.relative(path.dirname(from), to).split(path.sep).join("/");
11
+ return relative.startsWith(".") ? relative : `./${relative}`;
12
+ }
13
+
14
+ function inspectDocLanguages(root = repoRoot) {
15
+ const manifestFile = path.join(root, "docs", "languages.json");
16
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
17
+ if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.pairs)) {
18
+ throw new Error("docs/languages.json has an unsupported schema");
19
+ }
20
+ const errors = [];
21
+ const topics = new Set();
22
+ for (const pair of manifest.pairs) {
23
+ if (!pair.topic || topics.has(pair.topic)) errors.push(`duplicate or missing topic: ${pair.topic ?? "unknown"}`);
24
+ topics.add(pair.topic);
25
+ const en = path.join(root, pair.en);
26
+ const vi = path.join(root, pair.vi);
27
+ if (!fs.existsSync(en)) errors.push(`${pair.topic}: missing English file ${pair.en}`);
28
+ if (!fs.existsSync(vi)) errors.push(`${pair.topic}: missing Vietnamese file ${pair.vi}`);
29
+ if (!fs.existsSync(en) || !fs.existsSync(vi)) continue;
30
+ const enText = fs.readFileSync(en, "utf8");
31
+ const viText = fs.readFileSync(vi, "utf8");
32
+ const viLink = markdownRelativeLink(en, vi).replace(/^\.\//, "");
33
+ const enLink = markdownRelativeLink(vi, en).replace(/^\.\//, "");
34
+ if (!enText.includes(viLink)) errors.push(`${pair.topic}: English file does not link to ${viLink}`);
35
+ if (!viText.includes(enLink)) errors.push(`${pair.topic}: Vietnamese file does not link to ${enLink}`);
36
+ if (!/[À-ỹĐđ]/u.test(viText)) errors.push(`${pair.topic}: Vietnamese file has no Vietnamese diacritics`);
37
+ }
38
+ return { ok: errors.length === 0, errors, pairs: manifest.pairs.length, terms: manifest.preservedTechnicalTerms };
39
+ }
40
+
41
+ function main(argv = process.argv.slice(2)) {
42
+ const json = argv.includes("--json");
43
+ const unknown = argv.filter((argument) => argument !== "--json");
44
+ if (unknown.length > 0) {
45
+ process.stderr.write(`FAIL: unknown option ${unknown[0]}\n`);
46
+ return 1;
47
+ }
48
+ let result;
49
+ try {
50
+ result = inspectDocLanguages();
51
+ } catch (error) {
52
+ process.stderr.write(`FAIL: ${error instanceof Error ? error.message : String(error)}\n`);
53
+ return 1;
54
+ }
55
+ if (!result.ok) {
56
+ process.stderr.write(`FAIL: documentation language check found ${result.errors.length} problem(s)\n${result.errors.map((error) => `- ${error}`).join("\n")}\n`);
57
+ return 1;
58
+ }
59
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `PASS: ${result.pairs} EN/VI documentation pairs are complete\n`);
60
+ return 0;
61
+ }
62
+
63
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
64
+ process.exit(main());
65
+ }
66
+
67
+ export { inspectDocLanguages, main, markdownRelativeLink };
@@ -34,6 +34,7 @@ done
34
34
 
35
35
  required_files=(
36
36
  "$ROOT/README.md"
37
+ "$ROOT/README.vi.md"
37
38
  "$ROOT/SECURITY.md"
38
39
  "$ROOT/AGENTS.md"
39
40
  "$ROOT/CHANGELOG.md"
@@ -51,6 +52,22 @@ required_files=(
51
52
  "$ROOT/.pi/project-context.md"
52
53
  "$ROOT/packages/piagent-core/package.json"
53
54
  "$ROOT/packages/piagent-core/extensions/piagent-guard.ts"
55
+ "$ROOT/packages/piagent-core/runtime/runtime-limits.ts"
56
+ "$ROOT/packages/piagent-core/runtime/hooks/agent-start-hook.ts"
57
+ "$ROOT/packages/piagent-core/runtime/hooks/completion-hook.ts"
58
+ "$ROOT/packages/piagent-core/runtime/hooks/input-hook.ts"
59
+ "$ROOT/packages/piagent-core/runtime/hooks/session-hooks.ts"
60
+ "$ROOT/packages/piagent-core/runtime/hooks/session-start-hook.ts"
61
+ "$ROOT/packages/piagent-core/runtime/hooks/tool-result-hook.ts"
62
+ "$ROOT/packages/piagent-core/runtime/input/chat-images.ts"
63
+ "$ROOT/packages/piagent-core/runtime/session/message-signals.ts"
64
+ "$ROOT/packages/piagent-core/runtime/session/runtime-state.ts"
65
+ "$ROOT/packages/piagent-core/runtime/session/system-prompt.ts"
66
+ "$ROOT/packages/piagent-core/runtime/session/tool-result-compaction.ts"
67
+ "$ROOT/packages/piagent-core/runtime/session/usage.ts"
68
+ "$ROOT/packages/piagent-core/runtime/tools/tool-groups.ts"
69
+ "$ROOT/packages/piagent-core/runtime/workflows/input-routing.ts"
70
+ "$ROOT/packages/piagent-core/runtime/workflows/task-intake.ts"
54
71
  "$ROOT/packages/piagent-core/extensions/context-engine.js"
55
72
  "$ROOT/packages/piagent-core/extensions/context-index-policy.js"
56
73
  "$ROOT/packages/piagent-core/extensions/local-state-path.js"
@@ -104,6 +121,14 @@ required_files=(
104
121
  "$ROOT/templates/project/.pi/gitignore.template"
105
122
  "$ROOT/templates/project/REVIEW_GUIDELINES.md"
106
123
  "$ROOT/docs/quickstart-vietnamese.md"
124
+ "$ROOT/docs/README.md"
125
+ "$ROOT/docs/languages.json"
126
+ "$ROOT/docs/en/README.md"
127
+ "$ROOT/docs/en/architecture.md"
128
+ "$ROOT/docs/en/maintainer-guide.md"
129
+ "$ROOT/docs/vi/README.md"
130
+ "$ROOT/docs/vi/architecture.md"
131
+ "$ROOT/docs/vi/maintainer-guide.md"
107
132
  "$ROOT/docs/command-reference-vietnamese.md"
108
133
  "$ROOT/docs/auto-delegation-policy.md"
109
134
  "$ROOT/docs/subagent-orchestration-capabilities.md"
@@ -138,6 +163,9 @@ required_files=(
138
163
  "$ROOT/docs-site/content/mcp.html"
139
164
  "$ROOT/docs-site/vercel.json"
140
165
  "$ROOT/scripts/build-docs-site.mjs"
166
+ "$ROOT/scripts/check-architecture.mjs"
167
+ "$ROOT/scripts/check-doc-languages.mjs"
168
+ "$ROOT/architecture/layers.json"
141
169
  "$ROOT/scripts/context-engine.mjs"
142
170
  "$ROOT/scripts/preview-docs-site.mjs"
143
171
  "$ROOT/schemas/project-profile.schema.json"
@@ -553,6 +581,7 @@ if grep -R -E -i \
553
581
  "$ROOT/packages/piagent-core/prompts" \
554
582
  "$ROOT/packages/piagent-core/mcp" \
555
583
  "$ROOT/packages/piagent-core/extensions" \
584
+ "$ROOT/packages/piagent-core/runtime" \
556
585
  "$ROOT/templates/project/AGENTS.md" >/dev/null; then
557
586
  echo "Public docs contain non-neutral platform wording"
558
587
  exit 1
@@ -582,6 +611,8 @@ node --check "$ROOT/scripts/check-runtime-advisories.mjs" >/dev/null
582
611
  node --check "$ROOT/scripts/check-published-site.mjs" >/dev/null
583
612
  node --check "$ROOT/scripts/build-docs-site.mjs" >/dev/null
584
613
  node --check "$ROOT/scripts/preview-docs-site.mjs" >/dev/null
614
+ node "$ROOT/scripts/check-architecture.mjs" >/dev/null
615
+ node "$ROOT/scripts/check-doc-languages.mjs" >/dev/null
585
616
  run_quietly "npm test" npm test
586
617
  if [[ -x "$ROOT/node_modules/.bin/tsc" ]]; then
587
618
  run_quietly "npm run typecheck" npm run typecheck