@phi-code-admin/phi-code 0.84.1 → 0.85.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.
@@ -1,14 +1,14 @@
1
- export interface LatestPiRelease {
1
+ export interface LatestRelease {
2
2
  version: string;
3
3
  packageName?: string;
4
4
  }
5
5
  export declare function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined;
6
6
  export declare function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean;
7
- export declare function getLatestPiRelease(currentVersion: string, options?: {
7
+ export declare function getLatestRelease(currentVersion: string, options?: {
8
8
  timeoutMs?: number;
9
- }): Promise<LatestPiRelease | undefined>;
10
- export declare function getLatestPiVersion(currentVersion: string, options?: {
9
+ }): Promise<LatestRelease | undefined>;
10
+ export declare function getLatestVersion(currentVersion: string, options?: {
11
11
  timeoutMs?: number;
12
12
  }): Promise<string | undefined>;
13
- export declare function checkForNewPiVersion(currentVersion: string): Promise<string | undefined>;
13
+ export declare function checkForNewVersion(currentVersion: string): Promise<string | undefined>;
14
14
  //# sourceMappingURL=version-check.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAsBD,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcpG;AAED,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAM/F;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAmBtC;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED,wBAAsB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU9F","sourcesContent":["import { getPiUserAgent } from \"./pi-user-agent.js\";\n\nconst LATEST_VERSION_URL = \"https://pi.dev/api/latest-version\";\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestPiRelease {\n\tversion: string;\n\tpackageName?: string;\n}\n\ninterface ParsedVersion {\n\tmajor: number;\n\tminor: number;\n\tpatch: number;\n\tprerelease?: string;\n}\n\nfunction parsePackageVersion(version: string): ParsedVersion | undefined {\n\tconst match = version.trim().match(/^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+.*)?$/);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tmajor: Number.parseInt(match[1], 10),\n\t\tminor: Number.parseInt(match[2], 10),\n\t\tpatch: Number.parseInt(match[3], 10),\n\t\tprerelease: match[4],\n\t};\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = parsePackageVersion(leftVersion);\n\tconst right = parsePackageVersion(rightVersion);\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\n\tif (left.major !== right.major) return left.major - right.major;\n\tif (left.minor !== right.minor) return left.minor - right.minor;\n\tif (left.patch !== right.patch) return left.patch - right.patch;\n\tif (left.prerelease === right.prerelease) return 0;\n\tif (!left.prerelease) return 1;\n\tif (!right.prerelease) return -1;\n\treturn left.prerelease.localeCompare(right.prerelease);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestPiRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(LATEST_VERSION_URL, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { packageName?: unknown; version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\tconst packageName =\n\t\ttypeof data.packageName === \"string\" && data.packageName.trim() ? data.packageName.trim() : undefined;\n\treturn { version: data.version.trim(), packageName };\n}\n\nexport async function getLatestPiVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestPiRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewPiVersion(currentVersion: string): Promise<string | undefined> {\n\ttry {\n\t\tconst latestVersion = await getLatestPiVersion(currentVersion);\n\t\tif (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {\n\t\t\treturn latestVersion;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAsBD,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcpG;AAED,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAM/F;AAED,wBAAsB,gBAAgB,CACrC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAkBpC;AAED,wBAAsB,gBAAgB,CACrC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED,wBAAsB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU5F","sourcesContent":["import { PACKAGE_NAME } from \"../config.js\";\nimport { getPiUserAgent } from \"./pi-user-agent.js\";\n\n// Version checks query the npm registry for the published phi-code package.\n// (The inherited flow queried pi.dev, the upstream Pi endpoint, which reported\n// upstream Pi releases and made \"Update Available\" fire for versions of the\n// wrong product.)\nconst NPM_REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestRelease {\n\tversion: string;\n\tpackageName?: string;\n}\n\ninterface ParsedVersion {\n\tmajor: number;\n\tminor: number;\n\tpatch: number;\n\tprerelease?: string;\n}\n\nfunction parsePackageVersion(version: string): ParsedVersion | undefined {\n\tconst match = version.trim().match(/^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+.*)?$/);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tmajor: Number.parseInt(match[1], 10),\n\t\tminor: Number.parseInt(match[2], 10),\n\t\tpatch: Number.parseInt(match[3], 10),\n\t\tprerelease: match[4],\n\t};\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = parsePackageVersion(leftVersion);\n\tconst right = parsePackageVersion(rightVersion);\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\n\tif (left.major !== right.major) return left.major - right.major;\n\tif (left.minor !== right.minor) return left.minor - right.minor;\n\tif (left.patch !== right.patch) return left.patch - right.patch;\n\tif (left.prerelease === right.prerelease) return 0;\n\tif (!left.prerelease) return 1;\n\tif (!right.prerelease) return -1;\n\treturn left.prerelease.localeCompare(right.prerelease);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { name?: unknown; version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\tconst packageName = typeof data.name === \"string\" && data.name.trim() ? data.name.trim() : undefined;\n\treturn { version: data.version.trim(), packageName };\n}\n\nexport async function getLatestVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {\n\ttry {\n\t\tconst latestVersion = await getLatestVersion(currentVersion);\n\t\tif (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {\n\t\t\treturn latestVersion;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -1,5 +1,10 @@
1
+ import { PACKAGE_NAME } from "../config.js";
1
2
  import { getPiUserAgent } from "./pi-user-agent.js";
2
- const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
3
+ // Version checks query the npm registry for the published phi-code package.
4
+ // (The inherited flow queried pi.dev, the upstream Pi endpoint, which reported
5
+ // upstream Pi releases and made "Update Available" fire for versions of the
6
+ // wrong product.)
7
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
3
8
  const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
4
9
  function parsePackageVersion(version) {
5
10
  const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
@@ -40,10 +45,10 @@ export function isNewerPackageVersion(candidateVersion, currentVersion) {
40
45
  }
41
46
  return candidateVersion.trim() !== currentVersion.trim();
42
47
  }
43
- export async function getLatestPiRelease(currentVersion, options = {}) {
48
+ export async function getLatestRelease(currentVersion, options = {}) {
44
49
  if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
45
50
  return undefined;
46
- const response = await fetch(LATEST_VERSION_URL, {
51
+ const response = await fetch(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`, {
47
52
  headers: {
48
53
  "User-Agent": getPiUserAgent(currentVersion),
49
54
  accept: "application/json",
@@ -56,15 +61,15 @@ export async function getLatestPiRelease(currentVersion, options = {}) {
56
61
  if (typeof data.version !== "string" || !data.version.trim()) {
57
62
  return undefined;
58
63
  }
59
- const packageName = typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined;
64
+ const packageName = typeof data.name === "string" && data.name.trim() ? data.name.trim() : undefined;
60
65
  return { version: data.version.trim(), packageName };
61
66
  }
62
- export async function getLatestPiVersion(currentVersion, options = {}) {
63
- return (await getLatestPiRelease(currentVersion, options))?.version;
67
+ export async function getLatestVersion(currentVersion, options = {}) {
68
+ return (await getLatestRelease(currentVersion, options))?.version;
64
69
  }
65
- export async function checkForNewPiVersion(currentVersion) {
70
+ export async function checkForNewVersion(currentVersion) {
66
71
  try {
67
- const latestVersion = await getLatestPiVersion(currentVersion);
72
+ const latestVersion = await getLatestVersion(currentVersion);
68
73
  if (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {
69
74
  return latestVersion;
70
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"version-check.js","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"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","sourcesContent":["import { getPiUserAgent } from \"./pi-user-agent.js\";\n\nconst LATEST_VERSION_URL = \"https://pi.dev/api/latest-version\";\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestPiRelease {\n\tversion: string;\n\tpackageName?: string;\n}\n\ninterface ParsedVersion {\n\tmajor: number;\n\tminor: number;\n\tpatch: number;\n\tprerelease?: string;\n}\n\nfunction parsePackageVersion(version: string): ParsedVersion | undefined {\n\tconst match = version.trim().match(/^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+.*)?$/);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tmajor: Number.parseInt(match[1], 10),\n\t\tminor: Number.parseInt(match[2], 10),\n\t\tpatch: Number.parseInt(match[3], 10),\n\t\tprerelease: match[4],\n\t};\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = parsePackageVersion(leftVersion);\n\tconst right = parsePackageVersion(rightVersion);\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\n\tif (left.major !== right.major) return left.major - right.major;\n\tif (left.minor !== right.minor) return left.minor - right.minor;\n\tif (left.patch !== right.patch) return left.patch - right.patch;\n\tif (left.prerelease === right.prerelease) return 0;\n\tif (!left.prerelease) return 1;\n\tif (!right.prerelease) return -1;\n\treturn left.prerelease.localeCompare(right.prerelease);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestPiRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(LATEST_VERSION_URL, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { packageName?: unknown; version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\tconst packageName =\n\t\ttypeof data.packageName === \"string\" && data.packageName.trim() ? data.packageName.trim() : undefined;\n\treturn { version: data.version.trim(), packageName };\n}\n\nexport async function getLatestPiVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestPiRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewPiVersion(currentVersion: string): Promise<string | undefined> {\n\ttry {\n\t\tconst latestVersion = await getLatestPiVersion(currentVersion);\n\t\tif (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {\n\t\t\treturn latestVersion;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"version-check.js","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"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","sourcesContent":["import { PACKAGE_NAME } from \"../config.js\";\nimport { getPiUserAgent } from \"./pi-user-agent.js\";\n\n// Version checks query the npm registry for the published phi-code package.\n// (The inherited flow queried pi.dev, the upstream Pi endpoint, which reported\n// upstream Pi releases and made \"Update Available\" fire for versions of the\n// wrong product.)\nconst NPM_REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestRelease {\n\tversion: string;\n\tpackageName?: string;\n}\n\ninterface ParsedVersion {\n\tmajor: number;\n\tminor: number;\n\tpatch: number;\n\tprerelease?: string;\n}\n\nfunction parsePackageVersion(version: string): ParsedVersion | undefined {\n\tconst match = version.trim().match(/^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+.*)?$/);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tmajor: Number.parseInt(match[1], 10),\n\t\tminor: Number.parseInt(match[2], 10),\n\t\tpatch: Number.parseInt(match[3], 10),\n\t\tprerelease: match[4],\n\t};\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = parsePackageVersion(leftVersion);\n\tconst right = parsePackageVersion(rightVersion);\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\n\tif (left.major !== right.major) return left.major - right.major;\n\tif (left.minor !== right.minor) return left.minor - right.minor;\n\tif (left.patch !== right.patch) return left.patch - right.patch;\n\tif (left.prerelease === right.prerelease) return 0;\n\tif (!left.prerelease) return 1;\n\tif (!right.prerelease) return -1;\n\treturn left.prerelease.localeCompare(right.prerelease);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { name?: unknown; version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\tconst packageName = typeof data.name === \"string\" && data.name.trim() ? data.name.trim() : undefined;\n\treturn { version: data.version.trim(), packageName };\n}\n\nexport async function getLatestVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {\n\ttry {\n\t\tconst latestVersion = await getLatestVersion(currentVersion);\n\t\tif (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {\n\t\t\treturn latestVersion;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
package/docs/settings.md CHANGED
@@ -50,9 +50,9 @@ Edit directly or use `/settings` for common options.
50
50
 
51
51
  ### Telemetry and update checks
52
52
 
53
- `enableInstallTelemetry` only controls the anonymous install/update ping to `https://pi.dev/api/report-install`. Opting out of telemetry does not disable update checks; Pi can still fetch `https://pi.dev/api/latest-version` to look for the latest version.
53
+ phi-code sends no install/update telemetry (the inherited upstream ping to `pi.dev` was removed; `enableInstallTelemetry` is kept for settings compatibility but has no effect). The update check fetches `https://registry.npmjs.org/@phi-code-admin/phi-code/latest` to look for a newer phi-code release.
54
54
 
55
- Set `PI_SKIP_VERSION_CHECK=1` to disable the Pi version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry.
55
+ Set `PI_SKIP_VERSION_CHECK=1` to disable the phi-code version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks and package update checks.
56
56
 
57
57
  ### Warnings
58
58
 
package/docs/usage.md CHANGED
@@ -263,7 +263,7 @@ pi --tools read,grep,find,ls -p "Review the code"
263
263
  | `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory; overridden by `--session-dir` |
264
264
  | `PI_PACKAGE_DIR` | Override package directory, useful for Nix/Guix store paths |
265
265
  | `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
266
- | `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the `pi.dev` latest-version request |
266
+ | `PI_SKIP_VERSION_CHECK` | Skip the phi-code version update check at startup. This prevents the npm registry latest-version request |
267
267
  | `PI_TELEMETRY` | Override install/update telemetry: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable update checks |
268
268
  | `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache where supported |
269
269
  | `VISUAL`, `EDITOR` | External editor for Ctrl+G |
@@ -26,7 +26,7 @@ if (process.env.MY_ANTHROPIC_KEY) {
26
26
  // Model registry with no custom models.json
27
27
  const modelRegistry = ModelRegistry.inMemory(authStorage);
28
28
 
29
- const model = getModel("anthropic", "claude-sonnet-4-20250514");
29
+ const model = getModel("anthropic", "claude-sonnet-4-5");
30
30
  if (!model) throw new Error("Model not found");
31
31
 
32
32
  // In-memory settings with overrides
@@ -69,10 +69,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
69
69
  return;
70
70
  }
71
71
 
72
- if (Object.keys(config.mcpServers).length === 0) {
73
- // No servers configured silently exit. Users can create mcp.json later.
74
- return;
75
- }
72
+ // Even with zero configured servers we proceed so the bundled `/mcp` command
73
+ // is always available (discoverability); it then guides the user to create an
74
+ // mcp.json. Servers are still only connected when one is actually configured.
76
75
 
77
76
  // ── 2. Initialize bridge components ──────────────────────────────────────
78
77
  // Auth callbacks — opens browser and notifies user when OAuth is needed
@@ -172,6 +171,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
172
171
  .filter(Boolean)
173
172
  .join("\n");
174
173
  ctx.ui.notify(detail, "info");
174
+ } else if (manager.getAllServers().length === 0) {
175
+ // No servers configured yet: guide the user instead of showing nothing.
176
+ ctx.ui.notify(
177
+ 'No MCP servers configured. Create ~/.phi/agent/mcp.json (global) or .phi/mcp.json (project) with an "mcpServers" block, e.g.:\n' +
178
+ '{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."], "lifecycle": "eager" } } }',
179
+ "info",
180
+ );
175
181
  } else {
176
182
  // Summary view: all servers
177
183
  ctx.ui.notify(manager.getStatusSummary(), "info");
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import { ApiKeyStore, type ConfigWatcher, type ExtensionAPI, getApiKeyStore, getConfigWatcher } from "phi-code";
19
+ import { getModels } from "phi-code-ai";
19
20
  import {
20
21
  buildOpenCodeGoAnthropicProviderConfig,
21
22
  buildOpenCodeGoProviderConfig,
@@ -25,7 +26,9 @@ import { formatWindow, inferContextWindow, parseContextWindow } from "./provider
25
26
  import { fetchLiveModels, peekCache, resetLiveModelsCache, toPersistedModel } from "./providers/live-models.js";
26
27
 
27
28
  const PROVIDER_DISPLAY: Record<string, string> = {
29
+ opencode: "OpenCode Zen",
28
30
  "opencode-go": "OpenCode Go",
31
+ "opencode-go-anthropic": "OpenCode Go (Anthropic-compat)",
29
32
  "alibaba-codingplan": "Alibaba Coding Plan (OpenAI-compat)",
30
33
  "alibaba-codingplan-anthropic": "Alibaba Coding Plan (Anthropic-compat)",
31
34
  openai: "OpenAI",
@@ -37,10 +40,62 @@ const PROVIDER_DISPLAY: Record<string, string> = {
37
40
  "lm-studio": "LM Studio (local)",
38
41
  };
39
42
 
43
+ /**
44
+ * Providers the live-models dispatcher can actually re-fetch (see
45
+ * live-models.ts dispatchFetch + refreshOpenCodeGo). Used to extend the
46
+ * startup/manual refresh to providers that are authenticated via auth.json or
47
+ * env vars but have no models.json entry yet — without it, a user who set a
48
+ * key with /auth (and never ran /setup) would never see new upstream models.
49
+ */
50
+ const REFRESHABLE_PROVIDERS: ReadonlySet<string> = new Set([
51
+ "opencode",
52
+ "opencode-go",
53
+ "opencode-go-anthropic",
54
+ "alibaba-codingplan",
55
+ "alibaba-codingplan-anthropic",
56
+ "openai",
57
+ "anthropic",
58
+ "google",
59
+ "openrouter",
60
+ "groq",
61
+ "ollama",
62
+ "lm-studio",
63
+ ]);
64
+
40
65
  function displayName(id: string): string {
41
66
  return PROVIDER_DISPLAY[id] ?? id;
42
67
  }
43
68
 
69
+ /** Default discovery base URLs for providers whose models.json entry is created by a refresh. */
70
+ const DEFAULT_BASE_URLS: Record<string, string> = {
71
+ opencode: "https://opencode.ai/zen/v1",
72
+ "opencode-go": "https://opencode.ai/zen/go/v1",
73
+ "alibaba-codingplan": "https://coding-intl.dashscope.aliyuncs.com/v1",
74
+ "alibaba-codingplan-anthropic": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
75
+ openai: "https://api.openai.com/v1",
76
+ anthropic: "https://api.anthropic.com/v1",
77
+ google: "https://generativelanguage.googleapis.com/v1beta",
78
+ openrouter: "https://openrouter.ai/api/v1",
79
+ groq: "https://api.groq.com/openai/v1",
80
+ ollama: "http://localhost:11434/v1",
81
+ "lm-studio": "http://localhost:1234/v1",
82
+ };
83
+
84
+ /**
85
+ * Built-in (models.generated.ts) model ids for a provider. Persisting only the
86
+ * models NOT in this set keeps the rich built-in definitions (costs, image
87
+ * input, thinking-level maps) authoritative — models.json carries just the
88
+ * delta the static catalog does not know about yet.
89
+ */
90
+ function builtinModelIds(providerId: string): Set<string> {
91
+ try {
92
+ const models = getModels(providerId as Parameters<typeof getModels>[0]) as Array<{ id: string }>;
93
+ return new Set(models.map((m) => m.id));
94
+ } catch {
95
+ return new Set();
96
+ }
97
+ }
98
+
44
99
  interface RefreshOutcome {
45
100
  provider: string;
46
101
  source: "live" | "cache" | "fallback" | "unsupported" | "skipped";
@@ -68,7 +123,21 @@ async function refreshOpenCodeGo(
68
123
  ? buildOpenCodeGoAnthropicProviderConfig(keyForBuild, models)
69
124
  : buildOpenCodeGoProviderConfig(keyForBuild, models);
70
125
 
71
- if (config.models.length === 0) {
126
+ // Persist only models the built-in catalog does not know yet; built-ins stay
127
+ // authoritative (costs, image input) and models.json carries the delta.
128
+ const builtin = builtinModelIds(providerId);
129
+ const newModels = config.models.filter((m) => !builtin.has(m.id));
130
+
131
+ if (newModels.length === 0) {
132
+ if (stored && Array.isArray(stored.models) && stored.models.length > 0) {
133
+ // Clean up previously persisted models that have since become built-in.
134
+ watcher.muteForWrite("models_json_changed");
135
+ store.setKey(providerId, stored.apiKey ?? apiKey ?? "local", {
136
+ baseUrl: stored.baseUrl ?? config.baseUrl,
137
+ api: stored.api ?? config.api,
138
+ models: [],
139
+ });
140
+ }
72
141
  return { provider: providerId, source: source === "fallback" ? "fallback" : "skipped", count: 0 };
73
142
  }
74
143
 
@@ -76,22 +145,26 @@ async function refreshOpenCodeGo(
76
145
  store.setKey(providerId, stored?.apiKey ?? apiKey ?? "local", {
77
146
  baseUrl: stored?.baseUrl ?? config.baseUrl,
78
147
  api: stored?.api ?? config.api,
79
- models: config.models,
148
+ models: newModels,
80
149
  });
81
150
 
82
151
  const outcomeSource = source === "live" ? "live" : source === "cache" ? "cache" : "fallback";
83
- return { provider: providerId, source: outcomeSource, count: config.models.length };
152
+ return { provider: providerId, source: outcomeSource, count: newModels.length };
84
153
  }
85
154
 
86
155
  async function refreshOne(
87
156
  store: ApiKeyStore,
88
157
  watcher: ConfigWatcher,
89
158
  providerId: string,
159
+ resolvedApiKey?: string,
90
160
  ): Promise<RefreshOutcome> {
91
161
  const stored = store.getProvider(providerId);
92
- const apiKey = stored?.apiKey && !stored.apiKey.startsWith("$") && stored.apiKey !== "local"
162
+ const storedKey = stored?.apiKey && !stored.apiKey.startsWith("$") && stored.apiKey !== "local"
93
163
  ? stored.apiKey
94
164
  : undefined;
165
+ // Prefer the key stored in models.json, else the one resolved from
166
+ // auth.json/env by the model registry (providers set up via /auth only).
167
+ const apiKey = storedKey ?? resolvedApiKey;
95
168
 
96
169
  // OpenCode Go is a provider pair the generic fetchLiveModels path can't express
97
170
  // (and never handled the Anthropic side), so refresh it from the shared catalog.
@@ -110,40 +183,30 @@ async function refreshOne(
110
183
  return { provider: providerId, source: "skipped", count: 0, error: result.error };
111
184
  }
112
185
 
113
- const persisted = result.models.map(toPersistedModel);
114
- if (persisted.length === 0) {
115
- return { provider: providerId, source: result.source, count: 0, error: result.error };
116
- }
186
+ // Persist only the delta the built-in catalog does not know yet (see
187
+ // builtinModelIds). Built-in definitions keep their costs/capabilities.
188
+ const builtin = builtinModelIds(providerId);
189
+ const persisted = result.models.map(toPersistedModel).filter((m) => !builtin.has(m.id));
117
190
 
118
191
  // Preserve baseUrl/api/apiKey/headers from existing config; only models change.
119
- const baseUrl =
120
- stored?.baseUrl ??
121
- (providerId === "opencode-go"
122
- ? "https://opencode.ai/zen/go/v1"
123
- : providerId === "alibaba-codingplan"
124
- ? "https://coding-intl.dashscope.aliyuncs.com/v1"
125
- : providerId === "alibaba-codingplan-anthropic"
126
- ? "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
127
- : providerId === "openai"
128
- ? "https://api.openai.com/v1"
129
- : providerId === "anthropic"
130
- ? "https://api.anthropic.com/v1"
131
- : providerId === "google"
132
- ? "https://generativelanguage.googleapis.com/v1beta"
133
- : providerId === "openrouter"
134
- ? "https://openrouter.ai/api/v1"
135
- : providerId === "groq"
136
- ? "https://api.groq.com/openai/v1"
137
- : providerId === "ollama"
138
- ? "http://localhost:11434/v1"
139
- : providerId === "lm-studio"
140
- ? "http://localhost:1234/v1"
141
- : undefined);
142
-
192
+ const baseUrl = stored?.baseUrl ?? DEFAULT_BASE_URLS[providerId];
143
193
  if (!baseUrl) {
144
194
  return { provider: providerId, source: "skipped", count: 0, error: "unknown baseUrl" };
145
195
  }
146
196
 
197
+ if (persisted.length === 0) {
198
+ if (stored && Array.isArray(stored.models) && stored.models.length > 0) {
199
+ // Clean up previously persisted models that have since become built-in.
200
+ watcher.muteForWrite("models_json_changed");
201
+ store.setKey(providerId, stored.apiKey ?? "local", {
202
+ baseUrl,
203
+ api: stored.api,
204
+ models: [],
205
+ });
206
+ }
207
+ return { provider: providerId, source: result.source, count: 0, error: result.error };
208
+ }
209
+
147
210
  // Mute the config watcher so it does not echo this programmatic write back
148
211
  // as a models_json_changed event (which would trigger a spurious reload +
149
212
  // "Keys reloaded" notification). Mute per-write because refresh loops can
@@ -311,29 +374,73 @@ export default function modelsExtension(pi: ExtensionAPI) {
311
374
  ctx.ui.notify(out, "info");
312
375
  }
313
376
 
377
+ interface RefreshTarget {
378
+ id: string;
379
+ resolvedApiKey?: string;
380
+ }
381
+
382
+ /**
383
+ * Providers to refresh: every provider persisted in models.json, plus every
384
+ * refreshable provider that is authenticated (auth.json / env vars) but has
385
+ * no models.json entry yet. API keys are resolved through the registry so
386
+ * providers configured via /auth alone still get authenticated listings.
387
+ */
388
+ async function resolveRefreshTargets(registry: {
389
+ getAvailable(): Array<{ provider: string }>;
390
+ getApiKeyForProvider(provider: string): Promise<string | undefined>;
391
+ }): Promise<RefreshTarget[]> {
392
+ const targets = new Map<string, RefreshTarget>();
393
+ for (const id of store.listProviders()) {
394
+ targets.set(id, { id });
395
+ }
396
+ try {
397
+ for (const model of registry.getAvailable()) {
398
+ const id = model.provider;
399
+ if (!targets.has(id) && REFRESHABLE_PROVIDERS.has(id)) {
400
+ targets.set(id, { id });
401
+ }
402
+ }
403
+ } catch {
404
+ // registry unavailable — fall back to models.json providers only
405
+ }
406
+ for (const target of targets.values()) {
407
+ try {
408
+ target.resolvedApiKey = await registry.getApiKeyForProvider(target.id);
409
+ } catch {
410
+ // no resolvable key — refreshOne will try the stored/keyless path
411
+ }
412
+ }
413
+ return [...targets.values()];
414
+ }
415
+
314
416
  // Background refresh on session_start so every new Phi Code session reflects
315
417
  // the latest provider catalogs without the user typing `/models refresh`.
316
418
  // Failures are silent — startup must never be blocked by upstream API hiccups.
317
419
  pi.on("session_start", async (_event, ctx) => {
420
+ if (process.env.PI_OFFLINE) return;
318
421
  try {
319
422
  store.load();
320
423
  } catch {
321
424
  // no models.json yet
322
425
  }
323
- const providers = store.listProviders();
324
- if (providers.length === 0) return;
426
+ const targets = await resolveRefreshTargets(ctx.modelRegistry);
427
+ if (targets.length === 0) return;
325
428
 
326
429
  // Fire-and-forget. Hot-reload via models_json_changed event surfaces results.
327
430
  void (async () => {
328
- let changed = 0;
329
- for (const id of providers) {
330
- const outcome = await refreshOne(store, watcher, id).catch(() => undefined);
331
- if (outcome && outcome.source === "live" && outcome.count > 0) changed++;
431
+ let discovered = 0;
432
+ let changedProviders = 0;
433
+ for (const target of targets) {
434
+ const outcome = await refreshOne(store, watcher, target.id, target.resolvedApiKey).catch(() => undefined);
435
+ if (outcome && outcome.source === "live" && outcome.count > 0) {
436
+ changedProviders++;
437
+ discovered += outcome.count;
438
+ }
332
439
  }
333
- if (changed > 0) {
440
+ if (changedProviders > 0) {
334
441
  try {
335
442
  ctx.ui.notify(
336
- `Refreshed ${changed}/${providers.length} provider catalog(s) in the background.`,
443
+ `Discovered ${discovered} new model(s) across ${changedProviders} provider(s). See /model.`,
337
444
  "info",
338
445
  );
339
446
  } catch {
@@ -346,20 +453,33 @@ export default function modelsExtension(pi: ExtensionAPI) {
346
453
 
347
454
  async function refreshCommand(
348
455
  target: string | undefined,
349
- ctx: { ui: { notify: (m: string, t?: "info" | "warning" | "error") => void; setStatus?: (k: string, v?: string) => void } },
456
+ ctx: {
457
+ ui: { notify: (m: string, t?: "info" | "warning" | "error") => void; setStatus?: (k: string, v?: string) => void };
458
+ modelRegistry: {
459
+ getAvailable(): Array<{ provider: string }>;
460
+ getApiKeyForProvider(provider: string): Promise<string | undefined>;
461
+ };
462
+ },
350
463
  ): Promise<void> {
351
- const providers = target ? [target] : store.listProviders();
352
- if (providers.length === 0) {
464
+ const targets = target ? [{ id: target } as RefreshTarget] : await resolveRefreshTargets(ctx.modelRegistry);
465
+ if (target) {
466
+ try {
467
+ targets[0].resolvedApiKey = await ctx.modelRegistry.getApiKeyForProvider(target);
468
+ } catch {
469
+ // keep undefined
470
+ }
471
+ }
472
+ if (targets.length === 0) {
353
473
  ctx.ui.notify("No providers configured.", "warning");
354
474
  return;
355
475
  }
356
- ctx.ui.notify(`Refreshing ${providers.length} provider(s)...`, "info");
476
+ ctx.ui.notify(`Refreshing ${targets.length} provider(s)...`, "info");
357
477
  ctx.ui.setStatus?.("models-refresh", "Fetching live model catalogs...");
358
478
 
359
479
  const outcomes: RefreshOutcome[] = [];
360
- for (const id of providers) {
361
- const outcome = await refreshOne(store, watcher, id).catch((err) => ({
362
- provider: id,
480
+ for (const t of targets) {
481
+ const outcome = await refreshOne(store, watcher, t.id, t.resolvedApiKey).catch((err) => ({
482
+ provider: t.id,
363
483
  source: "skipped" as const,
364
484
  count: 0,
365
485
  error: err instanceof Error ? err.message : String(err),
@@ -371,9 +491,10 @@ export default function modelsExtension(pi: ExtensionAPI) {
371
491
  let out = "**Refresh report:**\n";
372
492
  for (const o of outcomes) {
373
493
  const icon = o.source === "live" ? "[ok]" : o.source === "fallback" ? "[fb]" : o.source === "cache" ? "[c]" : "[--]";
374
- out += ` ${icon} ${displayName(o.provider)} \`${o.provider}\` — ${o.count} model(s) (${o.source}${o.error ? `, ${o.error}` : ""})\n`;
494
+ out += ` ${icon} ${displayName(o.provider)} \`${o.provider}\` — ${o.count} new model(s) (${o.source}${o.error ? `, ${o.error}` : ""})\n`;
375
495
  }
376
- out += `\nModels persisted to \`${store.configPath}\`. \`/model\` picker now reflects this catalog.`;
496
+ out += `\nOnly models missing from the built-in catalog are persisted to \`${store.configPath}\`;\n`;
497
+ out += `built-in models stay available either way. \`/model\` picker reflects the merged catalog.`;
377
498
  ctx.ui.notify(out, "info");
378
499
  pi.events.emit("models_json_changed", { source: "models-refresh" });
379
500
  }
@@ -359,6 +359,28 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
359
359
  return null;
360
360
  }
361
361
 
362
+ /**
363
+ * Load a skill body (SKILL.md) so a phase instruction can embed it verbatim.
364
+ * Search order mirrors loadAgentDef: project .phi/skills first, then the
365
+ * global ~/.phi/agent/skills (where postinstall copies the bundled skills).
366
+ * YAML frontmatter is stripped. Returns null when the skill is not installed.
367
+ */
368
+ function loadSkillContent(name: string): string | null {
369
+ const dirs = [
370
+ join(process.cwd(), ".phi", "skills"),
371
+ join(homedir(), ".phi", "agent", "skills"),
372
+ ];
373
+ for (const dir of dirs) {
374
+ try {
375
+ const content = readFileSync(join(dir, name, "SKILL.md"), "utf-8");
376
+ const fmMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
377
+ const body = (fmMatch ? fmMatch[1] : content).trim();
378
+ if (body) return body;
379
+ } catch { /* try next dir */ }
380
+ }
381
+ return null;
382
+ }
383
+
362
384
  /**
363
385
  * Load routing config and build phase queue with model assignments + agent definitions.
364
386
  * Each phase now reads outputs from previous phases and writes structured outputs.
@@ -392,6 +414,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
392
414
  : '';
393
415
  const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
394
416
 
417
+ // Embed the prompt-architect skill so the PLAN phase applies it to every
418
+ // task it writes. The skill ships bundled and is copied to
419
+ // ~/.phi/agent/skills by postinstall; when missing, the inline task format
420
+ // still enforces the [CONTEXT]/[TASK]/[FORMAT]/[CONSTRAINTS] structure.
421
+ const promptArchitectSkill = loadSkillContent("prompt-architect");
422
+ const promptArchitectSection = promptArchitectSkill
423
+ ? `\n**Prompt-architect skill (apply it to every task you write):**\n\n${promptArchitectSkill}\n`
424
+ : "";
425
+
395
426
  const phases: OrchestratorPhase[] = [
396
427
  {
397
428
  key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
@@ -466,29 +497,37 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
466
497
  **Step 1:** Read \`.phi/plans/brief-*.md\` (created by the explore phase)
467
498
  **Step 2:** Read \`.phi/plans/explore-*.md\` to understand the codebase analysis
468
499
  **Step 3:** Design the architecture based on findings
469
- **Step 4:** Create a DETAILED TODO LIST in \`.phi/plans/todo-${ts}.md\`:
470
- For each task:
471
- - Task number and title
472
- - Agent assignment (code/test)
473
- - Files to create/modify
474
- - Specific implementation details
475
- - Dependencies on other tasks
500
+ **Step 4:** Create a DETAILED TODO LIST in \`.phi/plans/todo-${ts}.md\`.
501
+ Every task is a PROMPT for the CODE agent, which starts with ZERO memory of
502
+ this conversation. Write each task with the prompt-architect structure:
503
+ - **[CONTEXT]** what exists and why this task (real paths from the explore phase)
504
+ - **[TASK]** exactly what to do — files to create/modify with full paths
505
+ - **[FORMAT]** expected deliverable — exports, signatures, style, tests
506
+ - **[CONSTRAINTS]** what NOT to break, patterns to respect
507
+ Plus: task number + title, agent assignment (code/test), dependencies.
476
508
 
477
509
  **Format for the todo list:**
478
510
  \`\`\`markdown
479
511
  # TODO: Project Tasks
480
512
 
481
513
  ## Task 1: [Task Title] [agent-type]
482
- - [ ] Specific implementation details
483
- - [ ] Files to create: path/to/file.ext
484
- - [ ] Expected behavior
514
+ **[CONTEXT]** [What exists now, why this task, real paths (src/x.ts:42)]
515
+ **[TASK]**
516
+ - [ ] Create path/to/file.ext with [specific behavior]
517
+ - [ ] Modify path/to/other.ext: [exact change]
518
+ **[FORMAT]** [Expected exports/signatures/tests]
519
+ **[CONSTRAINTS]** [What not to break, patterns to follow]
485
520
  - Dependencies: None
486
521
 
487
522
  ## Task 2: [Task Title] [agent-type]
488
- - [ ] Implementation details
523
+ **[CONTEXT]** ...
524
+ **[TASK]**
525
+ - [ ] ...
526
+ **[FORMAT]** ...
527
+ **[CONSTRAINTS]** ...
489
528
  - Dependencies: Task 1
490
529
  \`\`\`
491
-
530
+ ${promptArchitectSection}
492
531
  Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` + runtimeInfo + COMMON_PHASE_RULES,
493
532
  },
494
533
  {