@rosetears/aili-pi 0.1.0 → 0.1.3

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 (58) hide show
  1. package/README.md +15 -6
  2. package/THIRD_PARTY_NOTICES.md +14 -3
  3. package/extensions/header/index.ts +92 -0
  4. package/extensions/matrix/index.ts +375 -0
  5. package/extensions/zentui/config.ts +1014 -0
  6. package/extensions/zentui/extension-status.ts +96 -0
  7. package/extensions/zentui/fixed-editor/cluster.ts +98 -0
  8. package/extensions/zentui/fixed-editor/compositor.ts +719 -0
  9. package/extensions/zentui/fixed-editor/index.ts +223 -0
  10. package/extensions/zentui/fixed-editor/input.ts +85 -0
  11. package/extensions/zentui/fixed-editor/pi-compat.ts +296 -0
  12. package/extensions/zentui/fixed-editor/selection.ts +217 -0
  13. package/extensions/zentui/fixed-editor/terminal-modes.ts +75 -0
  14. package/extensions/zentui/fixed-editor/types.ts +37 -0
  15. package/extensions/zentui/footer-format.ts +279 -0
  16. package/extensions/zentui/footer.ts +595 -0
  17. package/extensions/zentui/format.ts +434 -0
  18. package/extensions/zentui/git.ts +384 -0
  19. package/extensions/zentui/gradient.ts +70 -0
  20. package/extensions/zentui/icons.ts +252 -0
  21. package/extensions/zentui/index.ts +580 -0
  22. package/extensions/zentui/live-context.ts +75 -0
  23. package/extensions/zentui/package-version.ts +650 -0
  24. package/extensions/zentui/project-refresh.ts +104 -0
  25. package/extensions/zentui/project-state.ts +59 -0
  26. package/extensions/zentui/prototype-patch-registry.ts +111 -0
  27. package/extensions/zentui/runtime.ts +841 -0
  28. package/extensions/zentui/selector-border.ts +77 -0
  29. package/extensions/zentui/session-lifecycle.ts +60 -0
  30. package/extensions/zentui/settings-command.ts +897 -0
  31. package/extensions/zentui/state.ts +55 -0
  32. package/extensions/zentui/style.ts +332 -0
  33. package/extensions/zentui/thinking-message.ts +159 -0
  34. package/extensions/zentui/tool-execution.ts +189 -0
  35. package/extensions/zentui/ui.ts +618 -0
  36. package/extensions/zentui/user-message.ts +252 -0
  37. package/licenses/pi-sakura-cyberdeck-MIT.txt +21 -0
  38. package/licenses/pi-zentui-MIT.txt +21 -0
  39. package/manifests/capabilities.json +1 -1
  40. package/manifests/live-verification.json +14 -8
  41. package/manifests/provenance.json +18 -5
  42. package/manifests/sbom.json +19 -3
  43. package/notices/pi-sakura-cyberdeck-NOTICE.txt +6 -0
  44. package/package.json +12 -3
  45. package/scripts/local-package-e2e.ts +1 -1
  46. package/scripts/sync-global-skills.d.mts +13 -0
  47. package/scripts/sync-global-skills.mjs +134 -0
  48. package/src/runtime/credential-guard.ts +58 -0
  49. package/src/runtime/doctor.ts +1 -1
  50. package/src/runtime/index.ts +2 -2
  51. package/src/runtime/path-boundaries.ts +1 -1
  52. package/src/runtime/registry.ts +6 -2
  53. package/src/runtime/rem-head.txt +38 -0
  54. package/src/runtime/rose-context.ts +1 -1
  55. package/src/runtime/subagents.ts +74 -403
  56. package/templates/APPEND_SYSTEM.md +10 -8
  57. package/themes/rem-cyberdeck.json +32 -0
  58. package/upstream/opencode-global-agents.lock.json +34 -0
@@ -0,0 +1,134 @@
1
+ import { cp, lstat, readdir, rename, rm } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
+ const PACKAGE_NAME_PATH = ["@rosetears", "aili-pi"];
9
+
10
+ async function directoryEntry(path) {
11
+ try {
12
+ const info = await lstat(path);
13
+ if (info.isSymbolicLink()) return "other";
14
+ return info.isDirectory() ? "directory" : "other";
15
+ } catch (error) {
16
+ if (error?.code === "ENOENT") return "missing";
17
+ throw error;
18
+ }
19
+ }
20
+
21
+ async function skillNames(sourceRoot) {
22
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
23
+ const names = [];
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
26
+ const skillFile = join(sourceRoot, entry.name, "SKILL.md");
27
+ try {
28
+ const info = await lstat(skillFile);
29
+ if (info.isFile() && !info.isSymbolicLink()) names.push(entry.name);
30
+ } catch (error) {
31
+ if (error?.code !== "ENOENT") throw error;
32
+ }
33
+ }
34
+ return names.sort((left, right) => left.localeCompare(right));
35
+ }
36
+
37
+ function temporaryPath(parent, name, purpose) {
38
+ return join(parent, `.${name}.aili-pi-${purpose}-${process.pid}-${randomUUID()}`);
39
+ }
40
+
41
+ /** True only for the two locations owned by Pi's package manager. */
42
+ export function isPiManagedNpmPackageRoot(packageRoot, home = homedir()) {
43
+ const expectedRoots = [
44
+ join(home, ".pi", "agent", "npm", "node_modules", ...PACKAGE_NAME_PATH),
45
+ join(home, ".pi", "npm", "node_modules", ...PACKAGE_NAME_PATH),
46
+ ].map((path) => resolve(path));
47
+ return expectedRoots.includes(resolve(packageRoot));
48
+ }
49
+
50
+ /**
51
+ * Replace only existing, real skill directories that have the same name as an
52
+ * embedded AILI snapshot skill. Unmatched user skills and package-only skills
53
+ * are intentionally left alone.
54
+ */
55
+ export async function syncExistingGlobalSkills(options = {}) {
56
+ const packageRoot = resolve(options.packageRoot ?? PACKAGE_ROOT);
57
+ const home = resolve(options.home ?? homedir());
58
+ const sourceRoot = join(packageRoot, "skills");
59
+ const targetRoot = join(home, ".agents", "skills");
60
+ const sourceNames = await skillNames(sourceRoot);
61
+ const rootKind = await directoryEntry(targetRoot);
62
+ if (rootKind === "missing") {
63
+ return { scanned: sourceNames.length, updated: [], skippedMissing: sourceNames, skippedUnsafe: [] };
64
+ }
65
+ if (rootKind !== "directory") {
66
+ throw new Error(`refusing to sync skills through a non-directory or symlink target: ${targetRoot}`);
67
+ }
68
+
69
+ const matched = [];
70
+ const skippedMissing = [];
71
+ const skippedUnsafe = [];
72
+ for (const name of sourceNames) {
73
+ const targetKind = await directoryEntry(join(targetRoot, name));
74
+ if (targetKind === "directory") matched.push(name);
75
+ else if (targetKind === "missing") skippedMissing.push(name);
76
+ else skippedUnsafe.push(name);
77
+ }
78
+
79
+ const stages = new Map();
80
+ try {
81
+ for (const name of matched) {
82
+ const stage = temporaryPath(targetRoot, name, "stage");
83
+ await cp(join(sourceRoot, name), stage, { recursive: true, errorOnExist: true, force: false });
84
+ stages.set(name, stage);
85
+ }
86
+
87
+ const updated = [];
88
+ for (const name of matched) {
89
+ const target = join(targetRoot, name);
90
+ const stage = stages.get(name);
91
+ const previous = temporaryPath(targetRoot, name, "previous");
92
+ let movedPrevious = false;
93
+ try {
94
+ await rename(target, previous);
95
+ movedPrevious = true;
96
+ await rename(stage, target);
97
+ stages.delete(name);
98
+ await rm(previous, { recursive: true, force: true });
99
+ updated.push(name);
100
+ } catch (error) {
101
+ if (movedPrevious) {
102
+ const targetKind = await directoryEntry(target);
103
+ if (targetKind === "missing") await rename(previous, target).catch(() => undefined);
104
+ }
105
+ throw error;
106
+ } finally {
107
+ await rm(previous, { recursive: true, force: true });
108
+ }
109
+ }
110
+
111
+ return { scanned: sourceNames.length, updated, skippedMissing, skippedUnsafe };
112
+ } finally {
113
+ await Promise.all([...stages.values()].map((path) => rm(path, { recursive: true, force: true })));
114
+ }
115
+ }
116
+
117
+ async function main() {
118
+ const args = process.argv.slice(2);
119
+ if (args.length > 1 || (args.length === 1 && args[0] !== "--if-pi-managed")) {
120
+ throw new Error("usage: sync-global-skills.mjs [--if-pi-managed]");
121
+ }
122
+ if (args[0] === "--if-pi-managed" && !isPiManagedNpmPackageRoot(PACKAGE_ROOT)) {
123
+ console.log("AILI skills: global sync skipped outside a Pi-managed npm package root");
124
+ return;
125
+ }
126
+ const report = await syncExistingGlobalSkills();
127
+ console.log(
128
+ `AILI skills: scanned=${report.scanned} updated=${report.updated.length} unchanged=${report.skippedMissing.length + report.skippedUnsafe.length}`,
129
+ );
130
+ }
131
+
132
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
133
+ await main();
134
+ }
@@ -0,0 +1,58 @@
1
+ import type { ExtensionAPI, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
2
+ import { homedir } from "node:os";
3
+ import { isProtectedCredentialPath } from "./path-boundaries.js";
4
+
5
+ const FILE_TOOLS = new Set(["read", "write", "edit", "ls", "grep", "find"]);
6
+
7
+ function normalizeShellCandidate(value: string): string {
8
+ return value
9
+ .trim()
10
+ .replace(/^['"`]+|['"`]+$/g, "")
11
+ .replaceAll("$HOME", homedir())
12
+ .replace(/^~(?=\/|$)/, homedir());
13
+ }
14
+
15
+ /**
16
+ * Conservatively finds path-shaped shell words, including arguments embedded
17
+ * in `bash -c` strings. A false positive only blocks a credential-like path;
18
+ * a false negative must never be treated as an allow decision elsewhere.
19
+ */
20
+ export function bashMentionsCredentialPath(command: string): boolean {
21
+ const candidates = command
22
+ .replace(/\\([\s'"`])/g, "$1")
23
+ .split(/[\s;|&(){}<>]+/)
24
+ .flatMap((part) => part.split(/["'`]/))
25
+ .map(normalizeShellCandidate)
26
+ .filter(Boolean);
27
+ return candidates.some((candidate) => isProtectedCredentialPath(candidate));
28
+ }
29
+
30
+ export async function isProtectedChildPath(cwd: string, path: string): Promise<boolean> {
31
+ // The predicate deliberately checks lexical credential names. Resolving a
32
+ // caller-selected external root is unnecessary for this hard denial and
33
+ // avoids treating a missing path as permission to read it later.
34
+ void cwd;
35
+ return isProtectedCredentialPath(normalizeShellCandidate(path));
36
+ }
37
+
38
+ /**
39
+ * This extension is injected by the AILI generic subagent wrapper for every
40
+ * child run. It is intentionally independent of caller-supplied role, cwd,
41
+ * sandbox, or extension settings and blocks only credential-path access.
42
+ */
43
+ export default function registerCredentialGuard(pi: ExtensionAPI): void {
44
+ pi.on("tool_call", async (event): Promise<ToolCallEventResult | undefined> => {
45
+ try {
46
+ const input = event.input as Record<string, unknown>;
47
+ if (FILE_TOOLS.has(event.toolName) && typeof input.path === "string" && await isProtectedChildPath(process.cwd(), input.path)) {
48
+ return { block: true, reason: "AILI child denied credential/auth/private-key path access" };
49
+ }
50
+ if (event.toolName === "bash" && typeof input.command === "string" && bashMentionsCredentialPath(input.command)) {
51
+ return { block: true, reason: "AILI child denied credential/auth/private-key path access in bash" };
52
+ }
53
+ return undefined;
54
+ } catch {
55
+ return { block: true, reason: "AILI child denied credential-path classification failure" };
56
+ }
57
+ });
58
+ }
@@ -41,7 +41,7 @@ export async function runDoctor(
41
41
  });
42
42
  results.push({ id: "package", status: dependencyState ? "PASS" : "ERROR", evidence: `version=${packageJson.version ?? "unverified"}; node=${packageJson.engines?.node ?? "unverified"}; native_dependencies=${dependencyState ? "exact" : "drift"}` });
43
43
  const resources = [...(packageJson.pi?.extensions ?? []), ...(packageJson.pi?.prompts ?? []), ...(packageJson.pi?.skills ?? [])];
44
- results.push({ id: "package.resources", status: resources.length === 8 ? "PASS" : "ERROR", evidence: `declared=${resources.length}` });
44
+ results.push({ id: "package.resources", status: resources.length === 11 ? "PASS" : "ERROR", evidence: `declared=${resources.length}` });
45
45
  } catch (error) {
46
46
  results.push({ id: "package", status: "ERROR", evidence: boundedError(error) });
47
47
  }
@@ -2,14 +2,14 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { RuntimeComponent } from "./contracts.js";
3
3
  import { registerRoseContext } from "./rose-context.js";
4
4
  import { registerDoctor } from "./doctor.js";
5
- import { registerAiliTask } from "./subagents.js";
5
+ import { registerSubagent } from "./subagents.js";
6
6
  import { registerNativeIntegrations } from "./native-integrations.js";
7
7
  import { registerGlobalResourceCommand } from "./global-resources.js";
8
8
 
9
9
  export const runtimeComponents: readonly RuntimeComponent[] = [
10
10
  { id: "rose-context", availability: "available", register: registerRoseContext },
11
11
  { id: "lifecycle-routing", availability: "available" },
12
- { id: "task-runtime", availability: "available", register: registerAiliTask },
12
+ { id: "task-runtime", availability: "available", register: registerSubagent },
13
13
  { id: "native-integrations", availability: "available", register: registerNativeIntegrations },
14
14
  { id: "global-resources", availability: "available", register: registerGlobalResourceCommand },
15
15
  { id: "capability-registry", availability: "available" },
@@ -53,7 +53,7 @@ function isMissingPathError(error: unknown): boolean {
53
53
  return error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
54
54
  }
55
55
 
56
- function isProtectedCredentialPath(path: string): boolean {
56
+ export function isProtectedCredentialPath(path: string): boolean {
57
57
  const normalized = path.replaceAll("\\", "/").toLowerCase();
58
58
  const basename = normalized.split("/").at(-1) ?? "";
59
59
  if (/^\.env(?:\..+)?$/.test(basename)) return true;
@@ -148,7 +148,11 @@ export async function validateLiveVerification(): Promise<string[]> {
148
148
  implementation?: Record<string, string>;
149
149
  }>("manifests/live-verification.json");
150
150
  if (evidence.schemaVersion !== 1 || evidence.platform !== "linux" || evidence.piVersion !== "0.81.1" || evidence.status !== "passed") errors.push("live verification: identity is incomplete or non-pass");
151
- if (evidence.probes?.length !== 2 || evidence.probes.some((probe) => probe.status !== "passed") || evidence.probes.find((probe) => probe.id === "code-scout-read-package")?.changedFiles !== 0) errors.push("live verification: required probes are missing, non-pass, or mutated files");
151
+ const genericProbe = evidence.probes?.find((probe) => probe.id === "generic-agentless-read-package");
152
+ const credentialProbe = evidence.probes?.find((probe) => probe.id === "generic-credential-guard");
153
+ if (evidence.probes?.length !== 3 || evidence.probes.some((probe) => probe.status !== "passed") || genericProbe?.changedFiles !== 0 || credentialProbe?.changedFiles !== 0) {
154
+ errors.push("live verification: required probes are missing, non-pass, or mutated files");
155
+ }
152
156
  for (const [filePath, expected] of Object.entries(evidence.implementation ?? {})) {
153
157
  const content = await readFile(new URL(filePath, ROOT), "utf8");
154
158
  const actual = createHash("sha256").update(content).digest("hex");
@@ -167,7 +171,7 @@ export async function validateProvenance(): Promise<string[]> {
167
171
  const provenance = await json<{ schemaVersion: number; sources: Array<{ name: string; revision: string; license: string; status: string; repository: string; sourceFiles: string[]; symbols: string[]; localChanges: string[]; verification: string[] }> }>("manifests/provenance.json");
168
172
  const sbom = await json<{ spdxVersion?: string; packages?: Array<{ SPDXID?: string; name?: string; licenseDeclared?: string }> }>("manifests/sbom.json");
169
173
  const notices = await readFile(new URL("THIRD_PARTY_NOTICES.md", ROOT), "utf8");
170
- if (provenance.schemaVersion !== 1 || provenance.sources.length !== 5) errors.push("provenance: expected five schema-v1 source records");
174
+ if (provenance.schemaVersion !== 1 || provenance.sources.length !== 6) errors.push("provenance: expected six schema-v1 source records");
171
175
  const names = new Set<string>();
172
176
  for (const source of provenance.sources) {
173
177
  if (!source.name || names.has(source.name)) errors.push(`provenance: duplicate or missing source ${source.name}`);
@@ -0,0 +1,38 @@
1
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⢠⢾⢿⣸⠃⡆⢸⠀⠀⠀⡁⠀⠀⠀⠀⠀⢀⢦⢸⡑⢮⣑⢺⣽⣮⢍⣛⣧⢯⡹⣍⢯⣙⢏⠿⣎⡭⢻⣌⣷⢩⠯⡽⣙⡻⢦⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⢻⣿⢧⣈⣻⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
2
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⢠⡟⡿⡆⡇⠸⠀⠸⠀⠀⠀⡁⠀⠀⣠⠆⡆⡜⣸⢼⢩⠲⡜⢢⣷⢿⣮⡱⢎⡷⣧⡙⢮⠜⣮⡙⠽⣾⡡⢿⡜⣏⠳⣍⠣⣝⢫⢧⢻⣿⣮⠱⡩⢿⣏⣿⣿⣿⡿⢿⣿⣿⢿⣿⢿⣿⣿⠚⠛⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
3
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣰⠃⡾⢹⢇⣹⠀⢂⡄⣘⠀⠀⠀⡅⢀⢠⢃⠞⣸⠰⣹⡼⣧⢛⡜⢣⢽⣾⣿⣧⣛⠼⣹⢮⡙⡞⡴⣩⠳⣍⢿⣚⣷⢭⡛⣬⠳⣌⢻⡞⣯⣿⣿⡱⣙⢬⣻⣿⢻⣿⣿⣷⣮⢽⣻⢿⡜⣿⣿⣧⠀⠘⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
4
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⡇⣸⠣⣸⢸⡏⠀⡞⠀⣇⠀⢀⡞⡇⡜⢢⢍⠺⣐⠳⡘⣗⣿⡇⢎⡱⢊⣿⣿⣿⣿⣜⡱⢎⢿⣜⡱⢣⡛⡜⣎⢿⣽⣎⠷⣡⠟⣬⢣⢿⣻⢿⣿⣷⡩⣶⣿⣿⣧⢻⣿⣿⣿⣿⣿⣞⣿⣿⣯⢿⣇⠀⣘⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
5
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢡⡏⢰⡟⢸⡇⠀⠃⡄⣷⠀⡏⡔⣯⠘⡥⢎⢣⢃⠯⡔⣻⢼⢿⢢⡱⣉⢾⣳⠘⢿⣿⡷⣍⠞⡼⢷⡫⣜⠵⣊⡎⢿⣿⡳⢥⡛⡴⣋⠾⣟⡧⢿⣽⢷⣿⣿⡞⣷⣫⢿⣿⣿⣿⣟⢿⣿⣷⣯⣿⣿⡄⢋⣸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
6
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡏⣼⠁⢸⡇⢸⡇⡀⢸⠡⢼⡒⡱⠌⣷⣉⠖⡭⢲⡉⠖⣱⢸⡞⣿⣇⠲⢥⣊⢿⡆⡈⠙⢿⣝⡻⣜⡣⢟⣮⡝⣲⣙⢎⢿⣏⢧⡝⡲⣍⠞⣧⣿⣩⣿⣿⠿⣿⣿⣿⢧⣯⣿⣿⣞⣿⣿⣿⣿⣿⡿⣿⣷⣤⣀⣻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
7
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣇⡏⠀⢸⡇⢸⡇⡜⢢⡙⢼⣣⠱⡉⢾⡤⢋⡔⢣⠜⡱⢌⡒⣻⢻⣷⡝⣢⠞⡸⣷⢡⠀⠀⠉⠳⣍⡟⣾⣔⡻⢧⣚⠼⣪⣿⡗⢮⠵⣎⢏⢿⣹⡦⢽⣻⣯⡽⣷⡿⣿⡿⡿⢿⣯⣞⡿⣝⣿⣞⣿⣿⣿⡌⠉⠻⣧⡀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⠀⠀⠀⠀
8
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠉⠉⠑⠀⠀⠤⢄⡀⠀⠀⢸⣻⠁⢀⣼⡇⢼⡗⣌⠣⡜⣸⣧⢣⠑⡘⣷⡡⢚⠤⣋⠴⢣⠱⣚⣿⣟⣷⡡⢞⣥⣿⣶⡤⠖⠒⠒⠚⠓⠧⣫⢟⣷⣽⣣⢇⣿⣿⡩⢞⡜⢮⣹⢿⣏⠾⣿⣿⣿⣞⣿⣿⣷⣏⢯⣿⣯⣿⠽⣾⣧⢟⣿⣿⣇⠀⠀⣿⠹⡆⠀⠀⠀⠠⣶⣻⡹⣼⡽⠃⠀⠀⠀
9
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡋⠀⠀⠀⢀⠠⠀⠂⢀⢠⠃⠀⠀⣿⣹⢀⡼⡰⣯⢘⣿⠤⡓⢬⠐⣿⡆⡍⢒⣻⣧⢩⠒⡥⢎⡣⡝⡤⢻⡽⣿⣿⠽⣋⠜⣳⡄⠀⠀⠀⠀⣀⣀⣈⠙⠲⢭⡻⢿⣾⣿⣱⢫⡜⢧⢺⡟⣧⡟⣿⣿⠟⠋⠀⣀⣀⡈⠙⢾⣿⣝⢿⣽⡷⣯⣿⣿⣿⠀⠀⣹⠆⢻⡄⠀⠀⠀⠀⠈⠉⠉⠀⠀⠀⠀⠀
10
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠒⠦⣤⣀⡐⣀⠖⠁⠀⠀⠀⣿⣼⠚⡥⡑⢿⣂⢿⡧⡙⢆⠩⢼⣷⡌⢣⠼⣿⣧⢹⠰⢣⡱⢎⡱⢣⢻⣿⣯⢷⡈⢞⡰⢳⡀⠴⢊⣉⣤⣴⣶⣶⣦⣤⣽⣛⡻⢿⣧⠳⡜⣭⢺⣏⣷⡹⣿⡇⠀⡴⠋⠀⠀⠈⠢⡀⢹⣿⣷⣝⣿⣵⣿⣿⣿⡄⠀⣼⠁⠸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
11
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠁⠀⠀⠀⠀⢰⣿⡏⡱⢢⣹⡞⣳⢺⣷⢉⡜⢌⡚⣿⣯⠒⣌⢻⣯⣧⡙⢦⡑⢎⡱⢍⡖⠦⢿⣧⢻⡔⣌⠣⢳⣴⣿⣿⣿⣿⣿⣿⠿⢿⣿⣿⣿⣿⣿⡱⣉⠦⣹⢶⣏⢷⣿⡇⢰⢥⣀⠀⠀⠀⠀⠱⡈⣿⣞⣿⣿⣛⣿⣿⣿⡇⠀⡾⠀⠀⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
12
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣟⡧⣑⣳⠃⠀⢸⡆⣿⠆⡜⢢⠜⡹⡿⣷⣸⣮⢿⣯⣷⡂⣍⠲⡘⢢⠜⢣⡍⣿⣧⠙⢦⠩⣽⣿⡿⢛⣿⣿⣿⣿⣷⣶⣿⡄⠙⣿⣿⠶⢥⠚⣼⢻⣏⢾⡻⡇⡞⠳⣌⠱⣄⠐⠀⠀⡇⢹⣾⢿⣿⣿⣯⣿⣿⡇⢠⠃⠀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
13
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠟⢳⠀⠀⠀⠀⢸⣿⣷⢠⢿⠀⠀⠈⡧⣹⡗⣌⢣⢎⡱⢻⣿⣯⠰⡩⢧⡙⢿⣤⠣⣙⢢⠙⢦⡘⡜⢯⣷⡀⠛⠭⠖⠋⢸⣿⢿⣋⡿⠯⡟⣹⡇⢠⠋⣿⡘⠦⡙⣼⣿⡹⣾⣇⠃⣧⠤⠼⠃⠈⠢⣌⣠⠇⣼⣿⣿⣻⣿⣿⢿⣿⡷⠎⠀⠀⢰⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
14
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⠀⠈⠇⠀⠀⠀⢸⣽⢻⡌⣿⠀⠀⠀⢳⡔⢿⣌⠲⡌⣵⢏⢻⡽⣷⡠⢍⢳⡌⠻⣷⣈⠦⣙⠢⢜⣰⡭⠟⠙⠀⠀⠀⠀⠀⠻⣮⠉⠂⢀⣀⡿⠗⠙⠀⣽⢌⢣⡑⣾⣷⣽⡿⠁⠀⢿⣀⡤⣖⠒⢄⠀⠀⢠⣿⣾⣿⣿⣿⡿⠀⣿⣿⣄⠀⠀⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
15
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡀⠀⠘⡄⠀⠀⠘⣾⠘⣷⢹⡄⠀⠀⠘⣇⠎⣿⡰⡽⢃⠎⣌⠿⣟⣷⣎⡴⡙⣦⡌⠻⢷⡦⠗⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠋⠉⠀⠀⠀⠀⠀⣿⢈⢦⡵⡿⠑⠋⠀⠀⠀⠈⣏⡧⠀⠙⠘⣀⠤⠞⠷⣿⣿⣿⣿⡇⠠⢿⠘⣟⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
16
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⠀⠀⠰⡄⠀⠀⣿⡇⢻⣞⣧⠀⠀⠀⢹⡎⡜⢿⡡⣉⠖⣩⣾⣿⣿⣿⢿⣿⣷⡿⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡾⠝⠋⠀⠀⠀⠀⠀⣠⠏⢉⠠⢄⡶⠋⠁⠀⠀⢀⣼⣿⢟⡿⠀⠀⡜⠀⠸⡞⣆⠀⠀⠀⠠⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀
17
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⠀⠀⠘⡄⠀⢸⡇⠜⣿⣻⡄⠀⠀⠀⢻⡰⣉⢷⣌⢲⣿⡟⣸⡏⣿⡿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣧⠴⠃⡠⠊⠀⠀⠀⠀⣰⣿⣿⠏⣼⠃⠀⢠⠇⠀⠀⢻⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
18
+ ⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⠀⠀⠘⢆⠀⣷⠐⢿⣿⣷⠀⠀⠀⠈⣿⣔⣊⢻⣿⣿⣴⠞⢻⣟⢻⠜⠀⡹⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠞⠁⠀⠀⠀⢀⣾⣿⣿⠋⢠⠃⠀⠀⠊⠀⠀⣀⠬⠤⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
19
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⠀⠀⠀⠣⡈⣇⠀⢻⣿⡄⠀⠀⠀⢸⣿⣷⣆⣻⣿⠟⠢⢀⠙⢻⠶⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⠋⠀⠀⠀⠀⢠⣾⣿⡿⠁⡰⠁⠀⠀⠀⡠⠔⠉⠀⠀⠀⠀⣸⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀
20
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⡀⠀⠀⠑⢜⢧⡀⠻⣿⠀⠀⠀⠀⢿⣺⣻⢿⡀⠀⠀⠈⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⠃⠀⠀⠀⠀⢠⣿⣿⠏⠀⠀⠀⠀⢀⠴⠊⠀⠀⠀⠀⢀⡠⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀
21
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⡄⠀⠀⠈⠫⣷⣤⡹⣇⠀⠀⠀⠘⣷⢿⣾⡽⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⡴⠖⠚⠓⠓⢦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⠏⠀⠀⠀⠀⢠⣿⡿⠁⠀⠀⠀⢀⠔⠁⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
22
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠤⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢄⠀⠀⠀⠈⠙⢛⡋⠀⠀⠀⠀⠺⣿⣞⣿⣯⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡭⠒⠉⠂⠉⠀⠁⠈⠘⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⡟⠀⠀⠀⠀⢠⠟⣹⣁⠀⠀⢀⠔⠁⠀⠀⠀⠀⢀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
23
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣆⠀⡑⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠔⠒⠈⢇⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠙⣿⣾⣽⣻⣿⠯⠗⠢⠀⠀⠀⠀⠀⠀⢰⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠏⣸⣿⡿⠀⠀⠀⠀⢀⠏⠀⠋⠉⠓⡴⠃⠀⠀⠀⠀⠀⡠⠋⠀⠀⠀⠀⠀⠀⠀⠀⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
24
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢧⡀⢸⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡖⠉⠀⠀⠤⡔⠒⢺⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣻⣽⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠦⡀⠀⠀⠀⠀⠀⠀⣠⠏⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞⡡⠎⣼⣿⠁⠀⠀⠀⠀⡎⠀⠀⠀⣠⠊⠀⠀⠀⠀⠀⣠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
25
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⠀⢀⠎⠙⠛⠒⠚⠓⠒⠉⠉⠑⢤⡀⠀⠀⠀⠈⣿⣿⣿⡾⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠢⠤⠤⠒⠋⠀⠀⠀⠀⠀⠀⠀⠀⢠⡼⢋⠴⡁⠎⢼⠇⠀⠀⠀⠐⣸⠁⠀⢀⠔⠁⠀⠀⠀⠀⢠⠞⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
26
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣦⣠⠋⠘⢄⠀⠀⠳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠢⣀⡀⠀⢹⣻⣿⣽⣻⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⢣⠐⠥⢂⠅⣨⠏⠀⠀⠀⠀⠀⢿⠀⢠⠊⠀⠀⠀⠀⢀⣴⠁⠀⠀⠀⠀⣀⣤⡶⣞⡿⣏⣯⣝⡻⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
27
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⢦⡀⠈⠳⡀⠀⠀⠑⠒⠰⢢⢶⡄⠀⠀⠀⠀⠀⠀⠉⠛⠉⣷⢺⡽⣿⣧⣽⣿⣿⣷⣦⣤⣄⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⢚⠡⡘⠤⢉⢂⣡⡸⠃⠀⠀⠀⠀⠀⠐⠚⡷⠃⠀⠀⠀⠀⢠⠞⠀⠀⢀⣠⣶⠿⣏⣷⡻⣵⣻⢞⡵⢮⡱⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
28
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⠈⠢⢤⣤⡴⣴⣯⣍⡿⡦⣀⠀⠀⠀⠀⠀⠀⠀⢹⣻⢼⡳⣿⢯⣟⣯⢟⣿⡛⠛⠉⣡⠮⣍⣹⠓⢶⡦⠤⣀⣀⡤⠖⠋⠰⠘⠒⠋⠉⠉⠉⡰⠁⠀⠀⠀⠀⠀⠀⠀⠈⢁⡤⠒⠲⡄⣰⠃⠀⡠⠒⣽⢯⣏⡿⡽⣶⢻⣳⡝⣮⡝⣮⢵⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
29
+ ⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⠈⠱⡤⠤⠤⠴⠒⠲⠚⢹⡑⡌⠳⡆⠀⠀⠀⠀⠀⢸⣽⢣⠿⣽⡿⡾⣽⢺⡜⣇⠀⢷⢧⡐⢠⠃⡍⡐⢧⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⣄⡜⠁⠀⠀⠀⠀⠀⠀⠀⡠⠖⠁⠀⠀⣴⠘⢧⠔⠉⠀⣸⣟⢮⣗⣻⡽⣞⢯⣓⢾⡱⣞⣥⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
30
+ ⠀⠀⠀⠀⠀⠀⠀⢮⣄⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠖⠚⠉⠙⠇⠀⠈⠋⠹⣇⠀⠀⠀⠀⠀⠀⠈⡇⠀⠀⠙⠀⠀⠀⠀⠀⣿⡜⣫⣟⣵⠟⠛⢳⠿⠾⣟⡷⡌⠳⣻⡏⠲⣤⠑⢺⡄⠀⠀⢀⡤⢦⠀⣠⠞⢁⣴⡫⠔⠒⠒⠢⢄⠀⢀⡤⠊⠀⠀⠀⢀⣴⠃⠀⠈⢧⡀⠀⢻⣞⢯⡞⣧⢻⡜⣮⢝⣮⢳⢧⣾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
31
+ ⠀⠀⠀⠀⠀⠀⠀⠈⠙⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠥⠒⠒⠋⣉⣽⡆⠀⠀⠀⠀⠹⣆⠀⠀⠀⠀⠀⠀⠹⡄⠀⠀⠀⠀⠀⠀⣸⡧⢫⡟⠉⠓⡦⢞⣫⡿⠟⠛⠓⠚⢒⡿⣅⠀⠈⢳⡈⡇⢀⠜⠁⠀⢸⡞⡡⠊⠁⠀⠀⠀⠀⠀⠀⠀⠉⠚⠤⣀⠀⠀⢀⡼⣏⢀⡤⠶⢌⣷⡀⢸⣿⢫⡞⣵⢣⣟⣼⠯⣞⡭⣾⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
32
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠠⠄⠀⠀⠠⠀⣤⠀⢀⣸⠤⠔⠂⠙⠋⠀⣇⠀⠀⠀⢨⢐⡽⠙⠢⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⣿⣷⠛⠶⣦⠟⣱⡿⠉⠀⠀⠀⣀⣀⣀⣬⠾⢆⠀⠀⢳⡿⠃⠀⠀⠀⢀⡼⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⣀⠀⠀⠉⣷⣩⠖⠋⠉⠀⠀⠈⢧⣿⢻⡜⣧⣻⠼⣛⠭⣎⢻⡜⣼⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
33
+ ⠀⠀⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⢀⠤⠒⠉⠀⠀⠀⠀⠀⠀⠀⠀⢓⠊⠁⠀⠀⠀⠀⠀⠠⠁⡏⠀⠀⢀⣊⡞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣏⣀⣴⢣⣾⠏⠀⠡⢉⡬⠛⠉⠀⠀⠈⠑⠺⣧⣤⠈⠁⠀⠀⢀⠴⢊⡇⠀⠀⠀⠀⠀⣀⠼⡏⠓⣲⠲⠤⣵⣲⢾⠋⡀⠀⠀⠀⣠⠆⠀⠈⢿⡷⡛⢩⠐⡍⢎⡳⡜⣣⢞⣹⣇⣀⠤⠾⢛⣿⡿⣟⣷⢲⣄⡀⠀⠀
34
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⢀⠂⡇⠀⢐⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⠃⠀⣹⢡⢯⠇⠀⣬⠖⠉⠀⠀⠀⠀⠀⢀⣠⣴⣿⣿⣿⠶⠀⣾⣿⠶⣾⠀⠀⠀⠀⠐⠞⠁⡐⢻⡀⠳⠖⠚⢩⡴⠃⡔⠸⣰⠖⠛⠳⣄⠠⠀⢨⣏⠹⣉⠟⠺⠶⣥⢳⣑⢮⡜⣻⣤⢦⡴⣾⢯⣗⢯⡞⣧⣛⢿⡀⠀
35
+ ⠀⠀⠀⠀⠀⠀⠀⠀⢠⠎⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠔⣲⠀⠀⢸⡄⠀⠀⠀⠀⠀⡤⠀⢸⣇⡴⠋⠸⢀⣀⠠⠤⠤⠢⢤⡤⡀⣀⣤⣶⠏⠉⣩⣿⣿⣟⢻⢡⢏⣎⠴⠋⠀⠀⠀⠀⣀⣤⣴⣾⣿⣿⣿⣿⣿⣿⡆⠀⠰⡀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠁⢼⠆⠀⠀⠀⢯⣠⣼⠥⠞⢁⠀⠀⠀⠉⠃⣴⢋⠆⣓⠰⢊⡑⠦⡈⠝⣯⣾⡽⣽⢿⣶⣽⡿⡹⢎⡿⡜⢧⡹⡚⡇⠀
36
+ ⠀⠀⠀⠀⠀⠀⠀⠰⡅⠀⠀⠀⠀⢀⡀⠤⠔⠂⠁⣠⠞⠉⡇⠀⠈⡷⠀⠀⠀⠀⡼⠁⣰⡟⠋⠀⣴⠟⠁⠀⠀⠀⢀⡀⡼⣲⠿⣛⠵⠎⢀⣴⡿⡟⠁⣈⡏⡼⡛⠁⠀⢀⣠⣴⣶⡿⠟⣫⣽⣿⣿⡏⠉⣿⡟⣿⣷⠀⠀⠉⠙⡇⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠟⡤⢋⠜⡠⢍⢢⠑⢢⠑⡌⡐⢻⡴⣩⢖⡎⢶⣱⣙⢧⡚⣭⠲⣡⢃⢷⡠
37
+ ⠀⠀⠀⠀⠀⣀⠤⠤⠽⢭⠉⠉⠉⠀⠀⠀⢀⣴⠞⠋⠀⠀⣇⠀⠀⢿⠀⡀⣀⣨⣵⠞⠁⡀⠠⢼⣧⡦⠶⠖⠚⣻⠉⣼⢳⢋⡞⣱⣮⣶⡿⠋⢀⣧⡵⢻⠀⣧⣧⣶⣿⠿⠛⠋⠀⣰⣾⣿⢿⣿⡟⠀⢰⣿⠇⢻⣿⡄⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⣡⠚⡄⢃⠎⡑⠈⠆⡉⢆⠱⢠⠑⠄⢻⡵⢎⣞⢣⡗⡺⣖⡹⢆⡻⢄⠣⢾⡱
38
+ ⣀⠀⠠⢊⠉⠀⣀⣠⠖⢹⠆⠀⠀⢀⠠⢔⠛⠁⠀⠀⠀⠀⢻⠀⡀⢾⡈⣭⡵⢾⢥⠀⠐⠀⠀⢸⣆⣠⢴⣴⣫⣇⣼⣏⣴⡿⠿⠟⠛⠉⠀⠀⡏⠉⠉⢹⠀⠻⣿⡍⠤⠒⢀⣠⣾⣿⠏⢡⣾⠟⠀⢀⣿⡿⠀⢸⣿⣧⠀⠀⠀⢸⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⠞⡡⢩⠄⠣⠘⠠⠈⠄⠁⠂⠈⠄⠊⠤⢉⡐⠂⣿⡹⣬⠳⡞⡵⣭⣹⠹⡜⡭⢊⡽⠀
@@ -14,7 +14,7 @@ export function buildRoseAppendix(event: BeforeAgentStartEvent, pi: ExtensionAPI
14
14
  const conflictState = conflicts.length === 0
15
15
  ? "lifecycle_conflicts=none-observed"
16
16
  : `lifecycle_conflicts=non-pass (${conflicts.map((conflict) => conflict.name).join(", ")})`;
17
- return `## AILI runtime summary\n- ${projectRuleState(event)}\n- ${conflictState}\n- rose_static_rules=global APPEND_SYSTEM marker resource (not injected by this Extension)\n- task_runtime=pi-subagent adapter (global AILI profiles required; concurrency 2; no resume/chain/background/worktree/recursion)\n- permission_runtime=pi-permission-modes (Default/Plan/Build/YOLO; /perm; Alt+M; sandbox availability is vendor-reported)\n- native_web=pi-web-access complete upstream surface; provider/network/filesystem side effects remain visible to the active permission policy\n- quota_status=pi-quota-status default enabled; its global state is maintained by the upstream extension\n- capability_registry=available; optional or unavailable capability decisions must report SKIP/WARN and must not claim work ran\n- doctor=available (/aili-doctor; current core remains non-pass until required provenance and release evidence pass)`;
17
+ return `## AILI runtime summary\n- ${projectRuleState(event)}\n- ${conflictState}\n- rose_static_rules=global APPEND_SYSTEM marker resource (not injected by this Extension)\n- task_runtime=generic pinned pi-subagent surface (optional global aili.<role> profiles; async/actions, bounded upstream fan-out, worktree/external cwd, and sandbox configuration available; credential paths remain denied)\n- permission_runtime=pi-permission-modes (Default/Plan/Build/YOLO; /perm; Alt+M; sandbox availability is vendor-reported)\n- native_web=pi-web-access complete upstream surface; provider/network/filesystem side effects remain visible to the active permission policy\n- quota_status=pi-quota-status default enabled; its global state is maintained by the upstream extension\n- capability_registry=available; optional or unavailable capability decisions must report SKIP/WARN and must not claim work ran\n- doctor=available (/aili-doctor; current core remains non-pass until required provenance and release evidence pass)`;
18
18
  }
19
19
 
20
20
  export function registerRoseContext(pi: ExtensionAPI): void {