@opengsd/gsd-core 1.7.0-rc.6 → 1.8.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +31 -3
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1157 -84
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +31 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +1866 -2434
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +341 -49
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-registry.cjs +157 -88
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +129 -26
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +129 -13
- package/gsd-core/bin/lib/config-loader.cjs +20 -4
- package/gsd-core/bin/lib/config.cjs +81 -18
- package/gsd-core/bin/lib/core-utils.cjs +14 -3
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +138 -60
- package/gsd-core/bin/lib/install-engine.cjs +301 -25
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +45 -6
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +463 -43
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +636 -72
- package/gsd-core/bin/lib/plan-scan.cjs +73 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
- package/gsd-core/bin/lib/roadmap.cjs +113 -52
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +73 -7
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +122 -46
- package/gsd-core/bin/lib/state.cjs +456 -137
- package/gsd-core/bin/lib/surface.cjs +53 -11
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +474 -13
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +224 -25
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +13 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +4 -2
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +27 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +30 -37
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +33 -5
- package/gsd-core/workflows/quick.md +21 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +123 -68
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +4 -1
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +252 -17
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +252 -17
- package/package.json +8 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +31 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/package.json +1 -1
package/hooks/gsd-statusline.js
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
// Namespace (not destructured) so tests can inject spawn failures by
|
|
10
|
+
// monkeypatching childProcess.execFileSync.
|
|
11
|
+
const childProcess = require('child_process');
|
|
9
12
|
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
10
13
|
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
14
|
+
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
|
11
15
|
|
|
12
16
|
// --- Config + last-command readers ------------------------------------------
|
|
13
17
|
|
|
@@ -286,6 +290,201 @@ function formatGsdState(s) {
|
|
|
286
290
|
return parts.join(' · ');
|
|
287
291
|
}
|
|
288
292
|
|
|
293
|
+
// --- Context token count (opt-in) ---------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Format a token count compactly: 156342 → '156k', 1234567 → '1.2M'.
|
|
297
|
+
*/
|
|
298
|
+
function formatTokens(tokens) {
|
|
299
|
+
// Promote to the M branch when k-rounding would reach 1000 (999,500-999,999
|
|
300
|
+
// must render "1.0M", never "1000k").
|
|
301
|
+
if (tokens >= 1000000 || Math.round(tokens / 1000) >= 1000) {
|
|
302
|
+
return (tokens / 1000000).toFixed(1) + 'M';
|
|
303
|
+
}
|
|
304
|
+
if (tokens >= 1000) return Math.round(tokens / 1000) + 'k';
|
|
305
|
+
return String(tokens);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Pure function: build the token-count suffix for the context meter from the
|
|
310
|
+
* hook input's context_window.current_usage block. Sums input, cache-creation,
|
|
311
|
+
* cache-read, and output tokens (the same total Claude Code's /context shows).
|
|
312
|
+
* Returns ' (156k)' or '' when usage is absent/empty.
|
|
313
|
+
*/
|
|
314
|
+
function contextTokenSuffix(currentUsage) {
|
|
315
|
+
if (!currentUsage || typeof currentUsage !== 'object') return '';
|
|
316
|
+
const total = (Number(currentUsage.input_tokens) || 0) +
|
|
317
|
+
(Number(currentUsage.cache_creation_input_tokens) || 0) +
|
|
318
|
+
(Number(currentUsage.cache_read_input_tokens) || 0) +
|
|
319
|
+
(Number(currentUsage.output_tokens) || 0);
|
|
320
|
+
return total > 0 ? ` (${formatTokens(total)})` : '';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Compact state format (opt-in) ---------------------------------------------
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Collapse GSD's free-text status (often a multi-sentence narrative) to a
|
|
327
|
+
* single keyword, built on the canonical normalizer (#2162 approval
|
|
328
|
+
* condition): normalizeStateStatus() in state-document.cjs owns the status
|
|
329
|
+
* vocabulary (discussing / planning / executing / verifying / completed /
|
|
330
|
+
* paused) so the two can't drift. "paused" — the canonical stuck state — is
|
|
331
|
+
* uppercased to PAUSED, the one state worth shouting about. Statuses the
|
|
332
|
+
* normalizer passes through unrecognized fall back to their first word,
|
|
333
|
+
* capped at 16 chars so a rogue STATE.md can't blow up the line.
|
|
334
|
+
* Returns null for empty input.
|
|
335
|
+
*/
|
|
336
|
+
const CANONICAL_STATUSES = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
|
|
337
|
+
|
|
338
|
+
function shortGsdStatus(status) {
|
|
339
|
+
if (!status) return null;
|
|
340
|
+
const norm = normalizeStateStatus(status, null);
|
|
341
|
+
if (CANONICAL_STATUSES.includes(norm)) {
|
|
342
|
+
return norm === 'paused' ? 'PAUSED' : norm;
|
|
343
|
+
}
|
|
344
|
+
// Unrecognized free text passes through normalizeStateStatus verbatim —
|
|
345
|
+
// fall back to the first word, capped.
|
|
346
|
+
const first = String(norm).trim().split(/[\s\u2014\u2013-]+/)[0] || '';
|
|
347
|
+
return first ? first.slice(0, 16) : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Compact alternative to formatGsdState, selected via
|
|
352
|
+
* `statusline.state_format: "compact"`:
|
|
353
|
+
*
|
|
354
|
+
* "v1.12 · P7/12 · executing" (phase active)
|
|
355
|
+
* "v2.0 · P4.5 · BLOCKED" (no total known)
|
|
356
|
+
* "v2.0 · complete" (milestone done)
|
|
357
|
+
* "v2.0 · next execute-phase 4.5" (idle with a queued action)
|
|
358
|
+
*
|
|
359
|
+
* Drops the milestone name and progress bar — the biggest width costs in the
|
|
360
|
+
* default format — and collapses narrative statuses via shortGsdStatus().
|
|
361
|
+
* The default "full" format is untouched.
|
|
362
|
+
*/
|
|
363
|
+
function formatGsdStateCompact(s) {
|
|
364
|
+
const parts = [];
|
|
365
|
+
|
|
366
|
+
if (s.milestone) parts.push(s.milestone);
|
|
367
|
+
|
|
368
|
+
const phaseId = s.activePhase || s.phaseNum;
|
|
369
|
+
if (phaseId) {
|
|
370
|
+
parts.push(s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
|
|
374
|
+
// phase (Scene 1, gated on activePhase ONLY — the legacy phaseNum shape
|
|
375
|
+
// still completes) wins over milestone-complete (Scene 3), even if a
|
|
376
|
+
// non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
|
|
377
|
+
const done = !s.activePhase && (Number(s.percent) === 100 ||
|
|
378
|
+
(s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases));
|
|
379
|
+
|
|
380
|
+
if (done) {
|
|
381
|
+
parts.push('complete');
|
|
382
|
+
} else {
|
|
383
|
+
const st = shortGsdStatus(s.status);
|
|
384
|
+
if (st) {
|
|
385
|
+
parts.push(st);
|
|
386
|
+
} else if (!phaseId && s.nextAction) {
|
|
387
|
+
const phasesStr = (s.nextPhases && s.nextPhases.length > 0) ? s.nextPhases.join('/') : '';
|
|
388
|
+
parts.push(`next ${s.nextAction}${phasesStr ? ' ' + phasesStr : ''}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return parts.join(' \u00b7 ');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --- Model name --------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Collapse the verbose " (… context)" model-name suffix Claude Code sends for
|
|
399
|
+
* long-context sessions (e.g. "Sonnet 4.5 (1M context)") to a compact badge
|
|
400
|
+
* (" (1M)"). The signal is preserved; the width isn't. Tolerant by design
|
|
401
|
+
* (issue #2160 approval condition): any trailing parenthesized token ending
|
|
402
|
+
* in "context" is collapsed — a future "(500K context)" becomes "(500K)"
|
|
403
|
+
* rather than silently no-opping. The token's own casing is preserved.
|
|
404
|
+
* Any other display name passes through unchanged.
|
|
405
|
+
*/
|
|
406
|
+
function compactModelName(name) {
|
|
407
|
+
if (typeof name !== 'string') return name;
|
|
408
|
+
return name.replace(/\s*\(([^)]+?)\s+(?:context|ctx)\)$/i, ' ($1)');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// --- Git segment (opt-in) ------------------------------------------------------
|
|
412
|
+
//
|
|
413
|
+
// Opt-in via `statusline.show_git: true` in .planning/config.json. Renders the
|
|
414
|
+
// current branch plus compact work-state markers after the directory segment:
|
|
415
|
+
// " │ main+2~1?3↑1" (staged / unstaged / untracked / ahead / behind)
|
|
416
|
+
// " │ main✓" (clean, in sync)
|
|
417
|
+
// One `git status --porcelain=v2 --branch` spawn per render — no shell, args
|
|
418
|
+
// are a fixed array, and the workspace dir is passed via -C. Fails silently
|
|
419
|
+
// (segment absent) outside a repo, without git, or on timeout.
|
|
420
|
+
|
|
421
|
+
const GIT_STATUS_TIMEOUT_MS = 1500;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Run `git status --porcelain=v2 --branch` in dir.
|
|
425
|
+
* Returns raw stdout, or null when git is missing, dir isn't a repo, or the
|
|
426
|
+
* call times out. Never throws.
|
|
427
|
+
*/
|
|
428
|
+
function readGitStatus(dir) {
|
|
429
|
+
try {
|
|
430
|
+
// 8 MiB maxBuffer (default 1 MiB) headroom for repos with very many changed
|
|
431
|
+
// or untracked files; overflow still degrades safely to segment-absent via
|
|
432
|
+
// the catch below.
|
|
433
|
+
return childProcess.execFileSync('git', ['-C', dir, 'status', '--porcelain=v2', '--branch'],
|
|
434
|
+
{ encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Pure function: parse `git status --porcelain=v2 --branch` output.
|
|
442
|
+
*
|
|
443
|
+
* Returns { branch, ahead, behind, staged, unstaged, untracked } or null when
|
|
444
|
+
* the text carries no branch header (not a repo / unparseable). Detached HEAD
|
|
445
|
+
* reports branch "(detached)" — porcelain v2's literal spelling, shown as-is.
|
|
446
|
+
* Unmerged (conflict) entries count as unstaged: they're pending work either way.
|
|
447
|
+
*/
|
|
448
|
+
function parseGitStatus(text) {
|
|
449
|
+
if (typeof text !== 'string') return null;
|
|
450
|
+
const info = { branch: null, ahead: 0, behind: 0, staged: 0, unstaged: 0, untracked: 0 };
|
|
451
|
+
for (const line of text.split('\n')) {
|
|
452
|
+
if (line.startsWith('# branch.head ')) {
|
|
453
|
+
info.branch = line.slice('# branch.head '.length).trim() || null;
|
|
454
|
+
} else if (line.startsWith('# branch.ab ')) {
|
|
455
|
+
const m = line.match(/\+(\d+) -(\d+)/);
|
|
456
|
+
if (m) { info.ahead = parseInt(m[1], 10); info.behind = parseInt(m[2], 10); }
|
|
457
|
+
} else if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
458
|
+
// Changed / renamed entries: XY pair at cols 2-3, '.' = unmodified side
|
|
459
|
+
const xy = line.slice(2, 4);
|
|
460
|
+
if (xy[0] !== '.') info.staged++;
|
|
461
|
+
if (xy[1] !== '.') info.unstaged++;
|
|
462
|
+
} else if (line.startsWith('u ')) {
|
|
463
|
+
info.unstaged++;
|
|
464
|
+
} else if (line.startsWith('? ')) {
|
|
465
|
+
info.untracked++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return info.branch ? info : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Pure function: format parsed git info into the statusline segment, divider
|
|
473
|
+
* included (mirrors lastCmdSuffix). Branch is dimmed to match the directory
|
|
474
|
+
* segment; markers keep their own colors. Returns '' when info is absent.
|
|
475
|
+
*/
|
|
476
|
+
function buildGitSegment(info) {
|
|
477
|
+
if (!info || !info.branch) return '';
|
|
478
|
+
const markers = [];
|
|
479
|
+
if (info.staged) markers.push(`\x1b[32m+${info.staged}\x1b[0m`);
|
|
480
|
+
if (info.unstaged) markers.push(`\x1b[33m~${info.unstaged}\x1b[0m`);
|
|
481
|
+
if (info.untracked) markers.push(`\x1b[31m?${info.untracked}\x1b[0m`);
|
|
482
|
+
if (info.ahead) markers.push(`\x1b[32m↑${info.ahead}\x1b[0m`);
|
|
483
|
+
if (info.behind) markers.push(`\x1b[31m↓${info.behind}\x1b[0m`);
|
|
484
|
+
const state = markers.length ? markers.join('') : '\x1b[32m✓\x1b[0m';
|
|
485
|
+
return ` │ \x1b[2m${info.branch}\x1b[0m${state}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
289
488
|
// --- stdin ------------------------------------------------------------------
|
|
290
489
|
|
|
291
490
|
function runStatusline() {
|
|
@@ -299,11 +498,16 @@ function runStatusline() {
|
|
|
299
498
|
clearTimeout(stdinTimeout);
|
|
300
499
|
try {
|
|
301
500
|
const data = JSON.parse(input);
|
|
302
|
-
const model = data.model?.display_name || 'Claude';
|
|
501
|
+
const model = compactModelName(data.model?.display_name || 'Claude');
|
|
303
502
|
const dir = data.workspace?.current_dir || process.cwd();
|
|
304
503
|
const session = data.session_id || '';
|
|
305
504
|
const remaining = data.context_window?.remaining_percentage;
|
|
306
505
|
|
|
506
|
+
// Read .planning config once — used by the context meter (token suffix)
|
|
507
|
+
// and the last-command/position block below. Fail-soft to {}.
|
|
508
|
+
let cfg = {};
|
|
509
|
+
try { cfg = readGsdConfig(dir); } catch (e) {}
|
|
510
|
+
|
|
307
511
|
// Context window display (shows USED percentage scaled to usable context)
|
|
308
512
|
// Claude Code reserves a buffer for autocompact. By default this is ~16.5%
|
|
309
513
|
// of the total window, but users can override it via CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
@@ -349,15 +553,21 @@ function runStatusline() {
|
|
|
349
553
|
const filled = Math.floor(used / 10);
|
|
350
554
|
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
351
555
|
|
|
556
|
+
// Opt-in absolute token count after the percentage (statusline.show_context_tokens)
|
|
557
|
+
let tokenSuffix = '';
|
|
558
|
+
if (getConfigValue(cfg, 'statusline.show_context_tokens') === true) {
|
|
559
|
+
tokenSuffix = contextTokenSuffix(data.context_window?.current_usage);
|
|
560
|
+
}
|
|
561
|
+
|
|
352
562
|
// Color based on usable context thresholds
|
|
353
563
|
if (used < 50) {
|
|
354
|
-
ctx = ` \x1b[32m${bar} ${used}
|
|
564
|
+
ctx = ` \x1b[32m${bar} ${used}%${tokenSuffix}\x1b[0m`;
|
|
355
565
|
} else if (used < 65) {
|
|
356
|
-
ctx = ` \x1b[33m${bar} ${used}
|
|
566
|
+
ctx = ` \x1b[33m${bar} ${used}%${tokenSuffix}\x1b[0m`;
|
|
357
567
|
} else if (used < 80) {
|
|
358
|
-
ctx = ` \x1b[38;5;208m${bar} ${used}
|
|
568
|
+
ctx = ` \x1b[38;5;208m${bar} ${used}%${tokenSuffix}\x1b[0m`;
|
|
359
569
|
} else {
|
|
360
|
-
ctx = ` \x1b[5;31m💀 ${bar} ${used}
|
|
570
|
+
ctx = ` \x1b[5;31m💀 ${bar} ${used}%${tokenSuffix}\x1b[0m`;
|
|
361
571
|
}
|
|
362
572
|
}
|
|
363
573
|
|
|
@@ -392,8 +602,9 @@ function runStatusline() {
|
|
|
392
602
|
}
|
|
393
603
|
}
|
|
394
604
|
|
|
395
|
-
// GSD state (milestone · status · phase) — shown when no todo task
|
|
396
|
-
|
|
605
|
+
// GSD state (milestone · status · phase) — shown when no todo task.
|
|
606
|
+
// Format resolved below once config is read (statusline.state_format).
|
|
607
|
+
let gsdStateStr = '';
|
|
397
608
|
|
|
398
609
|
// GSD update available?
|
|
399
610
|
// Read only the per-package shared cache file (#607). The legacy
|
|
@@ -421,8 +632,9 @@ function runStatusline() {
|
|
|
421
632
|
// Failure here must never break the statusline — wrap the entire lookup.
|
|
422
633
|
let lastCmdSuffix = '';
|
|
423
634
|
let position = 'end';
|
|
635
|
+
let stateFormat = 'full';
|
|
636
|
+
let gitSuffix = '';
|
|
424
637
|
try {
|
|
425
|
-
const cfg = readGsdConfig(dir);
|
|
426
638
|
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
|
427
639
|
const transcriptPath = data.transcript_path;
|
|
428
640
|
const lastCmd = readLastSlashCommand(transcriptPath);
|
|
@@ -432,8 +644,17 @@ function runStatusline() {
|
|
|
432
644
|
}
|
|
433
645
|
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
|
434
646
|
if (cfgPos != null) position = cfgPos;
|
|
647
|
+
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
|
648
|
+
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
|
649
|
+
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
|
650
|
+
}
|
|
435
651
|
} catch (e) {
|
|
436
|
-
// Never break the statusline on config/transcript errors
|
|
652
|
+
// Never break the statusline on config/transcript/git errors
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!task) {
|
|
656
|
+
const state = readGsdState(dir) || {};
|
|
657
|
+
gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
437
658
|
}
|
|
438
659
|
|
|
439
660
|
// Output
|
|
@@ -444,7 +665,7 @@ function runStatusline() {
|
|
|
444
665
|
? `\x1b[2m${gsdStateStr}\x1b[0m`
|
|
445
666
|
: null;
|
|
446
667
|
|
|
447
|
-
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, position }));
|
|
668
|
+
process.stdout.write(composeStatusline({ gsdUpdate, model, ctx, middle, dirname, lastCmdSuffix, gitSuffix, position }));
|
|
448
669
|
} catch (e) {
|
|
449
670
|
// Silent fail - don't break statusline on parse errors
|
|
450
671
|
}
|
|
@@ -463,6 +684,7 @@ function runStatusline() {
|
|
|
463
684
|
* @param {string|null} [opts.middle=null] - middle segment (todo task or GSD state), null = absent
|
|
464
685
|
* @param {string} opts.dirname - project directory basename (dim styling applied here)
|
|
465
686
|
* @param {string} [opts.lastCmdSuffix=''] - last-command suffix, e.g. ' │ last: /foo'
|
|
687
|
+
* @param {string} [opts.gitSuffix=''] - git branch/status segment, e.g. ' │ main✓' (after dirname)
|
|
466
688
|
* @param {'end'|'front'} [opts.position='end']
|
|
467
689
|
* - 'end' (default): ctx appended after dirname — preserved byte-for-byte
|
|
468
690
|
* - 'front': ctx immediately after model name so the meter stays visible in narrow terminals
|
|
@@ -478,6 +700,7 @@ function composeStatusline({
|
|
|
478
700
|
middle = null,
|
|
479
701
|
dirname,
|
|
480
702
|
lastCmdSuffix = '',
|
|
703
|
+
gitSuffix = '',
|
|
481
704
|
position = 'end',
|
|
482
705
|
} = {}) {
|
|
483
706
|
const modelSeg = `\x1b[2m${model}\x1b[0m`;
|
|
@@ -486,12 +709,12 @@ function composeStatusline({
|
|
|
486
709
|
const pos = position === 'front' ? 'front' : 'end';
|
|
487
710
|
|
|
488
711
|
if (pos === 'front') {
|
|
489
|
-
if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${lastCmdSuffix}`;
|
|
490
|
-
return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${lastCmdSuffix}`;
|
|
712
|
+
if (middle) return `${gsdUpdate}${modelSeg}${ctx} │ ${middle} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
|
|
713
|
+
return `${gsdUpdate}${modelSeg}${ctx} │ ${dirSeg}${gitSuffix}${lastCmdSuffix}`;
|
|
491
714
|
}
|
|
492
715
|
// 'end' — preserved byte-for-byte relative to original inline templates
|
|
493
|
-
if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
|
|
494
|
-
return `${gsdUpdate}${modelSeg} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
|
|
716
|
+
if (middle) return `${gsdUpdate}${modelSeg} │ ${middle} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
|
|
717
|
+
return `${gsdUpdate}${modelSeg} │ ${dirSeg}${gitSuffix}${ctx}${lastCmdSuffix}`;
|
|
495
718
|
}
|
|
496
719
|
|
|
497
720
|
function isInstalledAheadOfLatest(installed, latest) {
|
|
@@ -531,6 +754,11 @@ module.exports = {
|
|
|
531
754
|
composeStatusline,
|
|
532
755
|
isInstalledAheadOfLatest,
|
|
533
756
|
evaluateUpdateCache,
|
|
757
|
+
formatTokens,
|
|
758
|
+
contextTokenSuffix,
|
|
759
|
+
shortGsdStatus, formatGsdStateCompact,
|
|
760
|
+
compactModelName,
|
|
761
|
+
readGitStatus, parseGitStatus, buildGitSegment,
|
|
534
762
|
};
|
|
535
763
|
|
|
536
764
|
/**
|
|
@@ -538,12 +766,14 @@ module.exports = {
|
|
|
538
766
|
* testing without feeding stdin. Returns the rendered string.
|
|
539
767
|
*/
|
|
540
768
|
function renderStatusline(data) {
|
|
541
|
-
const model = data.model?.display_name || 'Claude';
|
|
769
|
+
const model = compactModelName(data.model?.display_name || 'Claude');
|
|
542
770
|
const dir = data.workspace?.current_dir || process.cwd();
|
|
543
771
|
const dirname = path.basename(dir);
|
|
544
772
|
|
|
545
773
|
let lastCmdSuffix = '';
|
|
546
774
|
let position = 'end';
|
|
775
|
+
let stateFormat = 'full';
|
|
776
|
+
let gitSuffix = '';
|
|
547
777
|
try {
|
|
548
778
|
const cfg = readGsdConfig(dir);
|
|
549
779
|
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
|
@@ -554,11 +784,16 @@ function renderStatusline(data) {
|
|
|
554
784
|
}
|
|
555
785
|
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
|
556
786
|
if (cfgPos != null) position = cfgPos;
|
|
787
|
+
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
|
788
|
+
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
|
789
|
+
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
|
790
|
+
}
|
|
557
791
|
} catch (e) { /* swallow */ }
|
|
558
792
|
|
|
559
|
-
const
|
|
793
|
+
const state = readGsdState(dir) || {};
|
|
794
|
+
const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
|
560
795
|
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
|
561
|
-
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, position });
|
|
796
|
+
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
|
|
562
797
|
}
|
|
563
798
|
|
|
564
799
|
module.exports.renderStatusline = renderStatusline;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,9 @@
|
|
|
70
70
|
"typescript-eslint": "^8.60.0"
|
|
71
71
|
},
|
|
72
72
|
"overrides": {
|
|
73
|
-
"qs": ">=6.15.2"
|
|
73
|
+
"qs": ">=6.15.2",
|
|
74
|
+
"body-parser": ">=2.3.0",
|
|
75
|
+
"@hono/node-server": ">=2.0.5"
|
|
74
76
|
},
|
|
75
77
|
"optionalDependencies": {
|
|
76
78
|
"fallow": "^2.70.0"
|
|
@@ -90,6 +92,7 @@
|
|
|
90
92
|
"gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
|
|
91
93
|
"gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
|
|
92
94
|
"gen:registry": "node scripts/gen-registry.cjs --write",
|
|
95
|
+
"gen:golden": "node scripts/gen-golden-install-parity-zcode.cjs && node scripts/gen-install-tree-fixtures.cjs",
|
|
93
96
|
"validate:registry": "node scripts/validate-registry.cjs",
|
|
94
97
|
"prepack": "npm run build:lib",
|
|
95
98
|
"prepare": "npm run build:lib",
|
|
@@ -99,7 +102,8 @@
|
|
|
99
102
|
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
100
103
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
101
104
|
"lint:fix": "eslint . --fix",
|
|
102
|
-
"lint:
|
|
105
|
+
"lint:table-schema-drift": "node scripts/lint-table-schema-drift.cjs",
|
|
106
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && npm run lint:generated-sync && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs && node scripts/lint-portable-timeout.cjs && node scripts/validate-registry.cjs && node scripts/lint-table-schema-drift.cjs",
|
|
103
107
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
104
108
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
105
109
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
@@ -107,7 +111,7 @@
|
|
|
107
111
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
|
108
112
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
|
109
113
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
|
110
|
-
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check",
|
|
114
|
+
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/check-glossary-refs.cjs --check",
|
|
111
115
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
112
116
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
113
117
|
"ci:test-scope": "node scripts/ci-test-scope.cjs",
|
package/pi/gsd.cjs
CHANGED
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
* This extension binds GSD's command surface to pi via the imperative adapter
|
|
10
10
|
* path — the programmatic-CLI peer of the OpenCode worked binding.
|
|
11
11
|
*
|
|
12
|
-
* Installation: copy this file to ~/.pi/agent/extensions/gsd.
|
|
13
|
-
*
|
|
12
|
+
* Installation: copy this file to ~/.pi/agent/extensions/gsd.js — note the
|
|
13
|
+
* `.js` DEST suffix, not `.cjs`. pi auto-discovers extensions/ entries through
|
|
14
|
+
* `isExtensionFile()`, which accepts only `.ts`/`.js` and skips anything else
|
|
15
|
+
* SILENTLY (no error, no log line), so a `.cjs` dest installs fine and is then
|
|
16
|
+
* never loaded (#2470). This source file keeps its `.cjs` suffix on purpose —
|
|
17
|
+
* tests `require()` it directly and `.cjs` is unambiguous CommonJS — and pi
|
|
18
|
+
* loads the copied file via jiti, which handles CommonJS and ESM alike, so the
|
|
19
|
+
* suffix gates discovery, not parsing. The engine is resolved from the installed
|
|
14
20
|
* GSD tree (walk-up like the OpenCode plugin). pi's shared hooks/ bundle
|
|
15
21
|
* (hooks/*.js + hooks/lib/git-cmd.js) is installed alongside the extension —
|
|
16
22
|
* capabilities/pi/capability.json does NOT set
|
|
@@ -65,10 +65,23 @@ function extractDocsExempt(body) {
|
|
|
65
65
|
// is CRLF-aware so Windows-authored fragments don't leave residual `\r`
|
|
66
66
|
// characters that would shift the `(#NNNN)` PR suffix to a blank line in
|
|
67
67
|
// the rendered CHANGELOG.md / GitHub release-notes bullet.
|
|
68
|
+
//
|
|
69
|
+
// Both leading AND trailing line terminators are stripped. `DOCS_EXEMPT_RE`
|
|
70
|
+
// removes the marker's own text but its `$` anchor (multiline mode) does
|
|
71
|
+
// not consume the `\n` that terminates the marker's line. When the marker
|
|
72
|
+
// is the FIRST line of the body, that leftover `\n` becomes the new first
|
|
73
|
+
// character of `body` — serializeChangelog then emits an empty `- ` bullet
|
|
74
|
+
// followed by an orphaned continuation paragraph, and parseChangelog's
|
|
75
|
+
// bullet-continuation check (which requires a leading `\s`) treats that
|
|
76
|
+
// non-indented paragraph as terminating the bullet, silently dropping the
|
|
77
|
+
// entry's content on re-parse. Stripping leading terminators here closes
|
|
78
|
+
// that gap the same way the trailing strip already does for the opposite
|
|
79
|
+
// (marker-last) position.
|
|
68
80
|
const cleaned = body
|
|
69
81
|
.replace(DOCS_EXEMPT_RE, '')
|
|
70
82
|
.replace(/[ \t\r]+$/gm, '') // strip trailing \r/spaces on each line
|
|
71
83
|
.replace(/(?:\r?\n){3,}/g, '\n\n') // collapse 3+ blank lines (CRLF-aware)
|
|
84
|
+
.replace(/^[\r\n]+/, '') // strip terminators left by a first-line marker
|
|
72
85
|
.replace(/[\r\n]+$/, ''); // strip every trailing line terminator
|
|
73
86
|
return { docsExempt: reason, body: cleaned };
|
|
74
87
|
}
|
|
@@ -105,6 +118,19 @@ function parseFragment(src) {
|
|
|
105
118
|
if (body.endsWith('\r\n')) verbatimBody = body.slice(0, -2);
|
|
106
119
|
else if (body.endsWith('\n')) verbatimBody = body.slice(0, -1);
|
|
107
120
|
else verbatimBody = body;
|
|
121
|
+
// Some fragments have a blank line between the closing frontmatter `---`
|
|
122
|
+
// and the first line of actual content (purely a stylistic authoring
|
|
123
|
+
// choice — the blank line carries no significant content, unlike
|
|
124
|
+
// indentation inside a code block). Strip any such leading blank line(s)
|
|
125
|
+
// here, mirroring the trailing-terminator strip above. Without this,
|
|
126
|
+
// `body` starts with `\n`/`\r\n`, serializeChangelog emits an empty `- `
|
|
127
|
+
// bullet followed by an orphaned paragraph, and parseChangelog's
|
|
128
|
+
// continuation check (requires a leading `\s` on the line) treats that
|
|
129
|
+
// non-indented paragraph as terminating the bullet — silently dropping
|
|
130
|
+
// the fragment's content on re-parse. This is the same downstream failure
|
|
131
|
+
// mode as a first-line docs-exempt marker (see extractDocsExempt below);
|
|
132
|
+
// it just arises from plain authoring whitespace instead of a marker.
|
|
133
|
+
verbatimBody = verbatimBody.replace(/^(?:[ \t]*\r?\n)+/, '');
|
|
108
134
|
const { docsExempt, body: visibleBody } = extractDocsExempt(verbatimBody);
|
|
109
135
|
if (!visibleBody.trim()) return { ok: false, reason: FRAGMENT_ERROR.EMPTY_BODY };
|
|
110
136
|
|