gsd-pi 2.32.0-dev.1e39869 → 2.32.0-dev.d792ba5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,21 +24,24 @@ One command. Walk away. Come back to a built project with clean git history.
24
24
 
25
25
  ---
26
26
 
27
- ## What's New in v2.29
28
-
29
- - **Node.js 24 LTS** — CI, Docker, and package config all upgraded to Node 24 (Krypton)
30
- - **`searchExcludeDirs` setting** — blacklist directories from `@` file autocomplete (e.g., `node_modules`, `dist`)
31
- - **Automated releases** — prod-release now auto-generates changelogs, bumps versions, and publishes to npm
32
- - **`/gsd logs`**browse activity, debug, and metrics logs from within a session
33
- - **Configurable screenshots** — browser-tools now support custom resolution, format, and quality
34
- - **Pre-commit secret scanning** — automatic detection of hardcoded secrets in CI and locally
35
- - **Per-project MCP config** — `.gsd/mcp.json` for project-scoped MCP server definitions
36
- - **API request metrics** — track request counts for Copilot/subscription users
37
- - **`/gsd keys`**full API key lifecycle management (list, add, remove, test, rotate, doctor)
38
- - **Advisory verification gate** — auto-discovered checks (lint/test from package.json) no longer doom-loop on pre-existing errors
39
- - **Worktree living doc sync** — DECISIONS, REQUIREMENTS, PROJECT, and KNOWLEDGE now sync between worktree and project root
40
- - **Windows non-ASCII path support** — `cpSync` fallback for usernames with special characters
41
- - **`needs-discussion` routing** — milestones with draft context now route to the interactive discussion flow instead of stopping
27
+ ## What's New in v2.32
28
+
29
+ - **Simplified pipeline** — research merged into planning, mechanical completion (ADR-003)
30
+ - **Always-on health widget** — 🟢🟡🔴 traffic-light indicator in the progress widget and visualizer health tab
31
+ - **Environment health checks** — progress scoring and status integration for auto-mode
32
+ - **Extension registry**user-managed enable/disable for bundled and custom extensions
33
+ - **Built-in skill authoring** — create and distribute custom skills from within GSD
34
+ - **Workflow templates** — right-sized workflows for every task type (research, plan, execute, complete)
35
+ - **AWS Bedrock auth** — automatic credential refresh via the new `aws-auth` extension
36
+ - **`-w` / `--worktree` CLI flag** — launch isolated worktree sessions from the command line
37
+ - **Native MCP client** replaced MCPorter with a built-in MCP client for better reliability
38
+ - **External state directory** — `.gsd/` now lives in `~/.gsd/projects/` with a symlink (ADR-002)
39
+ - **Model health indicator** — live health status based on error trends and consecutive failures
40
+ - **Quick-task branch cleanup** — `/gsd quick` branches auto-merge back after completion
41
+ - **Windows EPERM fallback** — migration rename uses copy+delete when NTFS blocks rename
42
+ - **Worktree identity fix** — stable project hash across worktrees and main repo
43
+ - **Crash recovery guidance** — actionable next-step messages based on what was interrupted
44
+ - **UAT verdict gating** — non-PASS verdicts now block slice progression instead of being ignored
42
45
 
43
46
  See the full [Changelog](./CHANGELOG.md) for details.
44
47
 
@@ -65,6 +68,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
65
68
  - **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
66
69
  - **[Remote Questions](./docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
67
70
  - **[Dynamic Model Routing](./docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
71
+ - **[Pipeline Simplification (ADR-003)](./docs/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
68
72
  - **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
69
73
 
70
74
  ---
@@ -141,12 +145,12 @@ The iron rule: **a task must fit in one context window.** If it can't, it's two
141
145
  Each slice flows through phases automatically:
142
146
 
143
147
  ```
144
- Research Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
145
- ↓ (all slices done)
146
- Validate Milestone → Complete Milestone
148
+ Plan (with integrated research) → Execute (per task) → Complete → Reassess Roadmap → Next Slice
149
+ ↓ (all slices done)
150
+ Validate Milestone → Complete Milestone
147
151
  ```
148
152
 
149
- **Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
153
+ **Plan** scouts the codebase, researches relevant docs, and decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
150
154
 
151
155
  ### `/gsd auto` — The Main Event
152
156
 
@@ -326,6 +330,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
326
330
  | `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
327
331
  | `gsd headless query` | Instant JSON snapshot — state, next dispatch, costs (no LLM) |
328
332
  | `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
333
+ | `gsd --worktree` (`-w`) | Launch an isolated worktree session for the active milestone |
329
334
  | `gsd sessions` | Interactive session picker — browse and resume any saved session |
330
335
 
331
336
  ---
@@ -483,7 +488,7 @@ See the full [Token Optimization Guide](./docs/token-optimization.md) for detail
483
488
 
484
489
  ### Bundled Tools
485
490
 
486
- GSD ships with 16 extensions, all loaded automatically:
491
+ GSD ships with 18 extensions, all loaded automatically:
487
492
 
488
493
  | Extension | What it provides |
489
494
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -503,6 +508,8 @@ GSD ships with 16 extensions, all loaded automatically:
503
508
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
504
509
  | **Remote Questions** | Route decisions to Slack/Discord when human input is needed in headless/CI mode |
505
510
  | **Universal Config** | Discover and import MCP servers and rules from other AI coding tools |
511
+ | **AWS Auth** | Automatic Bedrock credential refresh for AWS-hosted models |
512
+ | **TTSR** | Tool-use type-safe runtime validation |
506
513
 
507
514
  ### Bundled Agents
508
515
 
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
98
98
 
99
99
  /** Format crash info for display or injection into a prompt. */
100
100
  export function formatCrashInfo(lock: LockData): string {
101
- return [
101
+ const lines = [
102
102
  `Previous auto-mode session was interrupted.`,
103
103
  ` Was executing: ${lock.unitType} (${lock.unitId})`,
104
104
  ` Started at: ${lock.unitStartedAt}`,
105
105
  ` Units completed before crash: ${lock.completedUnits}`,
106
106
  ` PID: ${lock.pid}`,
107
- ].join("\n");
107
+ ];
108
+
109
+ // Add recovery guidance based on what was happening when it crashed
110
+ if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
111
+ lines.push(`No work was lost. Run /gsd auto to restart.`);
112
+ } else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
113
+ lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
114
+ } else if (lock.unitType.includes("execute")) {
115
+ lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
116
+ } else if (lock.unitType.includes("complete")) {
117
+ lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
118
+ }
119
+
120
+ return lines.join("\n");
108
121
  }
@@ -795,6 +795,12 @@ export default function (pi: ExtensionAPI) {
795
795
 
796
796
  // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
797
797
  pi.on("agent_end", async (event, ctx: ExtensionContext) => {
798
+ // Clean up quick-task branch if one just completed (#1269)
799
+ try {
800
+ const { cleanupQuickBranch } = await import("./quick.js");
801
+ cleanupQuickBranch();
802
+ } catch { /* non-fatal */ }
803
+
798
804
  // If discuss phase just finished, start auto-mode
799
805
  if (checkAutoStartAfterDiscuss()) {
800
806
  depthVerifiedMilestones.clear();
@@ -57,8 +57,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
57
57
  // mkdir -p the external dir
58
58
  mkdirSync(externalPath, { recursive: true });
59
59
 
60
- // Rename .gsd -> .gsd.migrating (atomic lock)
61
- renameSync(localGsd, migratingPath);
60
+ // Rename .gsd -> .gsd.migrating (atomic lock).
61
+ // On Windows, NTFS may reject rename with EPERM if file descriptors are
62
+ // open (VS Code watchers, antivirus on-access scan). Fall back to
63
+ // copy+delete (#1292).
64
+ try {
65
+ renameSync(localGsd, migratingPath);
66
+ } catch (renameErr: any) {
67
+ if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
68
+ try {
69
+ cpSync(localGsd, migratingPath, { recursive: true, force: true });
70
+ rmSync(localGsd, { recursive: true, force: true });
71
+ } catch (copyErr) {
72
+ return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
73
+ }
74
+ } else {
75
+ throw renameErr;
76
+ }
77
+ }
62
78
 
63
79
  // Copy contents to external dir, skipping worktrees/
64
80
  const entries = readdirSync(migratingPath, { withFileTypes: true });
@@ -107,10 +107,11 @@ export async function handleQuick(
107
107
  const skipBranch = git.prefs.isolation === "none";
108
108
 
109
109
  let branchCreated = false;
110
+ let originalBranch: string | undefined;
110
111
  if (!skipBranch) {
111
112
  try {
112
- const current = git.getCurrentBranch();
113
- if (current !== branchName) {
113
+ originalBranch = git.getCurrentBranch();
114
+ if (originalBranch !== branchName) {
114
115
  // Auto-commit any dirty state before switching
115
116
  try {
116
117
  git.autoCommit("quick-task", `Q${taskNum}`, []);
@@ -154,4 +155,57 @@ export async function handleQuick(
154
155
  },
155
156
  { triggerTurn: true },
156
157
  );
158
+
159
+ // Schedule branch merge-back after the quick task agent session ends.
160
+ // Without this, auto-mode resumes on the quick-task branch (#1269).
161
+ if (branchCreated && originalBranch) {
162
+ _pendingQuickBranchReturn = {
163
+ basePath,
164
+ originalBranch,
165
+ quickBranch: branchName,
166
+ taskNum,
167
+ slug,
168
+ description,
169
+ };
170
+ }
171
+ }
172
+
173
+ /** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
174
+ let _pendingQuickBranchReturn: {
175
+ basePath: string;
176
+ originalBranch: string;
177
+ quickBranch: string;
178
+ taskNum: number;
179
+ slug: string;
180
+ description: string;
181
+ } | null = null;
182
+
183
+ /**
184
+ * Merge the quick-task branch back to the original branch and switch.
185
+ * Called from the agent_end handler after a quick task completes.
186
+ * Returns true if a branch return was performed.
187
+ */
188
+ export function cleanupQuickBranch(): boolean {
189
+ if (!_pendingQuickBranchReturn) return false;
190
+ const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
191
+ _pendingQuickBranchReturn = null;
192
+
193
+ try {
194
+ // Auto-commit any remaining work
195
+ try { runGit(basePath, ["add", "-A"]); } catch {}
196
+ try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
197
+
198
+ // Switch back and merge
199
+ runGit(basePath, ["checkout", originalBranch]);
200
+ try {
201
+ runGit(basePath, ["merge", "--squash", quickBranch]);
202
+ runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
203
+ } catch { /* merge conflict or nothing — non-fatal */ }
204
+
205
+ // Clean up quick branch
206
+ try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
157
211
  }
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
- import { join, resolve } from "node:path";
13
+ import { join, resolve, sep } from "node:path";
14
14
 
15
15
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
16
 
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
37
37
  */
38
38
  function resolveGitRoot(basePath: string): string {
39
39
  try {
40
+ // In a worktree, --show-toplevel returns the worktree path, not the main
41
+ // repo root. Use --git-common-dir to find the shared .git directory,
42
+ // then derive the main repo root from it (#1288).
43
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
44
+ cwd: basePath,
45
+ encoding: "utf-8",
46
+ stdio: ["ignore", "pipe", "ignore"],
47
+ timeout: 5_000,
48
+ }).trim();
49
+
50
+ // If commonDir ends with .git/worktrees/<name>, the main repo is two
51
+ // levels up from the worktrees dir. If it's just .git, resolve normally.
52
+ if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
53
+ // e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
54
+ // or /path/to/project/.git/worktrees/M001 → /path/to/project
55
+ const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
56
+ const mainRoot = resolve(gitDir, "..");
57
+ return mainRoot;
58
+ }
59
+
60
+ // Not in a worktree — use --show-toplevel as usual
40
61
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
41
62
  cwd: basePath,
42
63
  encoding: "utf-8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.32.0-dev.1e39869",
3
+ "version": "2.32.0-dev.d792ba5",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
98
98
 
99
99
  /** Format crash info for display or injection into a prompt. */
100
100
  export function formatCrashInfo(lock: LockData): string {
101
- return [
101
+ const lines = [
102
102
  `Previous auto-mode session was interrupted.`,
103
103
  ` Was executing: ${lock.unitType} (${lock.unitId})`,
104
104
  ` Started at: ${lock.unitStartedAt}`,
105
105
  ` Units completed before crash: ${lock.completedUnits}`,
106
106
  ` PID: ${lock.pid}`,
107
- ].join("\n");
107
+ ];
108
+
109
+ // Add recovery guidance based on what was happening when it crashed
110
+ if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
111
+ lines.push(`No work was lost. Run /gsd auto to restart.`);
112
+ } else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
113
+ lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
114
+ } else if (lock.unitType.includes("execute")) {
115
+ lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
116
+ } else if (lock.unitType.includes("complete")) {
117
+ lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
118
+ }
119
+
120
+ return lines.join("\n");
108
121
  }
@@ -795,6 +795,12 @@ export default function (pi: ExtensionAPI) {
795
795
 
796
796
  // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
797
797
  pi.on("agent_end", async (event, ctx: ExtensionContext) => {
798
+ // Clean up quick-task branch if one just completed (#1269)
799
+ try {
800
+ const { cleanupQuickBranch } = await import("./quick.js");
801
+ cleanupQuickBranch();
802
+ } catch { /* non-fatal */ }
803
+
798
804
  // If discuss phase just finished, start auto-mode
799
805
  if (checkAutoStartAfterDiscuss()) {
800
806
  depthVerifiedMilestones.clear();
@@ -57,8 +57,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
57
57
  // mkdir -p the external dir
58
58
  mkdirSync(externalPath, { recursive: true });
59
59
 
60
- // Rename .gsd -> .gsd.migrating (atomic lock)
61
- renameSync(localGsd, migratingPath);
60
+ // Rename .gsd -> .gsd.migrating (atomic lock).
61
+ // On Windows, NTFS may reject rename with EPERM if file descriptors are
62
+ // open (VS Code watchers, antivirus on-access scan). Fall back to
63
+ // copy+delete (#1292).
64
+ try {
65
+ renameSync(localGsd, migratingPath);
66
+ } catch (renameErr: any) {
67
+ if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
68
+ try {
69
+ cpSync(localGsd, migratingPath, { recursive: true, force: true });
70
+ rmSync(localGsd, { recursive: true, force: true });
71
+ } catch (copyErr) {
72
+ return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
73
+ }
74
+ } else {
75
+ throw renameErr;
76
+ }
77
+ }
62
78
 
63
79
  // Copy contents to external dir, skipping worktrees/
64
80
  const entries = readdirSync(migratingPath, { withFileTypes: true });
@@ -107,10 +107,11 @@ export async function handleQuick(
107
107
  const skipBranch = git.prefs.isolation === "none";
108
108
 
109
109
  let branchCreated = false;
110
+ let originalBranch: string | undefined;
110
111
  if (!skipBranch) {
111
112
  try {
112
- const current = git.getCurrentBranch();
113
- if (current !== branchName) {
113
+ originalBranch = git.getCurrentBranch();
114
+ if (originalBranch !== branchName) {
114
115
  // Auto-commit any dirty state before switching
115
116
  try {
116
117
  git.autoCommit("quick-task", `Q${taskNum}`, []);
@@ -154,4 +155,57 @@ export async function handleQuick(
154
155
  },
155
156
  { triggerTurn: true },
156
157
  );
158
+
159
+ // Schedule branch merge-back after the quick task agent session ends.
160
+ // Without this, auto-mode resumes on the quick-task branch (#1269).
161
+ if (branchCreated && originalBranch) {
162
+ _pendingQuickBranchReturn = {
163
+ basePath,
164
+ originalBranch,
165
+ quickBranch: branchName,
166
+ taskNum,
167
+ slug,
168
+ description,
169
+ };
170
+ }
171
+ }
172
+
173
+ /** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
174
+ let _pendingQuickBranchReturn: {
175
+ basePath: string;
176
+ originalBranch: string;
177
+ quickBranch: string;
178
+ taskNum: number;
179
+ slug: string;
180
+ description: string;
181
+ } | null = null;
182
+
183
+ /**
184
+ * Merge the quick-task branch back to the original branch and switch.
185
+ * Called from the agent_end handler after a quick task completes.
186
+ * Returns true if a branch return was performed.
187
+ */
188
+ export function cleanupQuickBranch(): boolean {
189
+ if (!_pendingQuickBranchReturn) return false;
190
+ const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
191
+ _pendingQuickBranchReturn = null;
192
+
193
+ try {
194
+ // Auto-commit any remaining work
195
+ try { runGit(basePath, ["add", "-A"]); } catch {}
196
+ try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
197
+
198
+ // Switch back and merge
199
+ runGit(basePath, ["checkout", originalBranch]);
200
+ try {
201
+ runGit(basePath, ["merge", "--squash", quickBranch]);
202
+ runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
203
+ } catch { /* merge conflict or nothing — non-fatal */ }
204
+
205
+ // Clean up quick branch
206
+ try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
157
211
  }
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
- import { join, resolve } from "node:path";
13
+ import { join, resolve, sep } from "node:path";
14
14
 
15
15
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
16
 
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
37
37
  */
38
38
  function resolveGitRoot(basePath: string): string {
39
39
  try {
40
+ // In a worktree, --show-toplevel returns the worktree path, not the main
41
+ // repo root. Use --git-common-dir to find the shared .git directory,
42
+ // then derive the main repo root from it (#1288).
43
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
44
+ cwd: basePath,
45
+ encoding: "utf-8",
46
+ stdio: ["ignore", "pipe", "ignore"],
47
+ timeout: 5_000,
48
+ }).trim();
49
+
50
+ // If commonDir ends with .git/worktrees/<name>, the main repo is two
51
+ // levels up from the worktrees dir. If it's just .git, resolve normally.
52
+ if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
53
+ // e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
54
+ // or /path/to/project/.git/worktrees/M001 → /path/to/project
55
+ const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
56
+ const mainRoot = resolve(gitDir, "..");
57
+ return mainRoot;
58
+ }
59
+
60
+ // Not in a worktree — use --show-toplevel as usual
40
61
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
41
62
  cwd: basePath,
42
63
  encoding: "utf-8",