@akira-tl/forgerelay 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +47 -12
  3. package/dist/activity/mcp-query-tools.js +48 -2
  4. package/dist/activity/query-service.js +1 -0
  5. package/dist/cli.js +181 -4
  6. package/dist/composite-activity.js +155 -0
  7. package/dist/composite-workspaces.js +197 -0
  8. package/dist/config.js +4 -1
  9. package/dist/oauth/router.js +21 -1
  10. package/dist/oauth-provider.js +32 -4
  11. package/dist/oauth-store.js +9 -0
  12. package/dist/remote-auth.js +110 -0
  13. package/dist/remote-transport.js +196 -0
  14. package/dist/remote-workspace-relay.js +473 -0
  15. package/dist/server.js +824 -121
  16. package/dist/ui/.vite/manifest.json +33 -33
  17. package/dist/ui/activity-panel-app.html +3 -3
  18. package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
  19. package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
  20. package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
  21. package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
  22. package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
  23. package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
  24. package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
  25. package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
  26. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
  27. package/dist/ui/workspace-app.html +4 -4
  28. package/dist/ui/workspace-lifecycle-app.html +4 -4
  29. package/dist/user-config.js +131 -5
  30. package/docs/configuration.md +26 -3
  31. package/docs/debugging.md +7 -0
  32. package/docs/versioning.md +11 -19
  33. package/package.json +5 -2
  34. package/scripts/ci/verify.mjs +38 -0
  35. package/scripts/debug/runtime.mjs +23 -1
  36. package/scripts/debug/runtime.test.mjs +14 -2
  37. package/scripts/debug/serve.mjs +4 -4
  38. package/scripts/release/pack.mjs +36 -0
  39. package/scripts/release/publish.mjs +157 -0
  40. package/scripts/release/release-gate.test.mjs +73 -16
  41. package/scripts/release-parity.mjs +5 -13
  42. package/scripts/release-proof.mjs +28 -19
  43. package/scripts/release-proof.test.mjs +32 -11
  44. package/scripts/release-version.mjs +1 -1
  45. package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
  46. package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
  47. package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
  48. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
@@ -0,0 +1 @@
1
+ .forgerelay-panel{border:1px solid var(--tool-card-border);background:var(--tool-card-header-bg);width:100%;color:var(--color-text-primary,#f5f5f6);border-radius:12px;overflow:hidden}.workspace-panel{background:var(--tool-card-header-bg);width:100%}.workspace-panel-header{grid-template-columns:22px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:58px;padding:9px 12px;display:grid}.workspace-panel-icon{background:color-mix(in srgb, var(--tool-accent) 9%, transparent);width:22px;height:22px;color:color-mix(in srgb, var(--tool-accent) 72%, var(--color-text-tertiary,#a3a3aa));border-radius:6px;place-items:center;display:grid}.workspace-panel-icon-svg{stroke-width:1.8px;width:14px;height:14px}.workspace-panel-title-group{gap:2px;min-width:0;display:grid}.workspace-panel-title{font-size:var(--font-text-sm-size,14px);font-weight:600;line-height:1.3}.workspace-panel-subtitle,.workspace-panel-mode{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,11px);line-height:1.35}.workspace-panel-subtitle{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.workspace-panel-mode{border:1px solid color-mix(in srgb, var(--tool-card-divider) 76%, transparent);white-space:nowrap;border-radius:999px;padding:2px 7px}.workspace-panel .workspace-details{border-top:1px solid var(--tool-card-divider);background:var(--tool-card-body-bg);max-height:none}.workspace-panel-pending-dot{background:var(--color-text-info,#38bdf8);width:8px;height:8px;box-shadow:0 0 0 3px color-mix(in srgb, var(--color-text-info,#38bdf8) 12%, transparent);border-radius:999px;justify-self:center}.activity-panel{border:1px solid var(--tool-card-border);background:var(--tool-card-header-bg);width:100%;color:var(--color-text-primary,#f5f5f6);border-radius:12px;overflow:hidden}.forgerelay-panel .activity-panel{border:0;border-top:1px solid var(--tool-card-divider);border-radius:0}.activity-panel-header{width:100%;min-height:58px;color:inherit;cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;grid-template-columns:14px minmax(0,1fr) auto 20px;align-items:center;gap:10px;padding:9px 12px;display:grid}.activity-panel-header:hover{background:var(--tool-card-hover-bg)}.activity-panel-header:focus-visible{outline:2px solid color-mix(in srgb, var(--color-text-info,#38bdf8) 72%, transparent);outline-offset:-2px}.activity-panel-header-pending{cursor:default}.activity-panel-header-pending:hover{background:0 0}.activity-panel-pending-spacer{width:20px}.activity-panel-status{background:var(--color-text-info,#38bdf8);width:8px;height:8px;box-shadow:0 0 0 3px color-mix(in srgb, var(--color-text-info,#38bdf8) 12%, transparent);border-radius:9999px;justify-self:center}.activity-panel-status.state-done{background:var(--color-success-text,#6fda83);box-shadow:0 0 0 3px color-mix(in srgb, var(--color-success-text,#6fda83) 12%, transparent)}.activity-panel-status.state-error{background:var(--color-danger-text,#ee7676);box-shadow:0 0 0 3px color-mix(in srgb, var(--color-danger-text,#ee7676) 12%, transparent)}.activity-panel-title-group{gap:2px;min-width:0;display:grid}.activity-panel-title{font-size:var(--font-text-sm-size,14px);font-weight:600;line-height:1.3}.activity-panel-subtitle,.activity-panel-count{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,11px);line-height:1.35}.activity-panel-count{white-space:nowrap}.activity-panel-body{border-top:1px solid var(--tool-card-divider);background:var(--tool-card-body-bg);display:grid}.activity-viewport{overscroll-behavior:contain;max-height:420px;overflow:hidden auto}.activity-list{display:grid}.activity-group+.activity-group{border-top:1px solid var(--tool-card-divider)}.activity-group.grouped>.activity-row.parent{background:color-mix(in srgb, var(--color-background-secondary,#272727) 54%, transparent)}.activity-children{border-top:1px solid color-mix(in srgb, var(--tool-card-divider) 72%, transparent);display:grid}.activity-row{--activity-accent:var(--color-text-secondary,#b6b6bd);--activity-phase:var(--color-text-tertiary,#a3a3aa);width:100%;min-height:46px;color:inherit;font:inherit;text-align:left;background:0 0;border:0;grid-template-columns:28px minmax(0,1fr) auto 16px;align-items:center;gap:10px;padding:7px 12px;display:grid}.activity-row.interactive{cursor:pointer}.activity-row.interactive:hover{background:var(--tool-card-hover-bg)}.activity-row.interactive:focus-visible{outline:2px solid color-mix(in srgb, var(--activity-accent) 68%, transparent);outline-offset:-2px}.activity-row.child{padding-left:34px;position:relative}.activity-row.child:before{background:color-mix(in srgb, var(--activity-accent) 26%, var(--tool-card-divider));content:"";width:1px;position:absolute;top:0;bottom:0;left:20px}.activity-row.child+.activity-row.child{border-top:1px solid color-mix(in srgb, var(--tool-card-divider) 52%, transparent)}.activity-row.kind-read{--activity-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 32%, #06b6d4 68%)}.activity-row.kind-write{--activity-accent:var(--color-success-text,#6fda83)}.activity-row.kind-edit,.activity-row.kind-rename{--activity-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 28%, #d99742 72%)}.activity-row.kind-delete{--activity-accent:var(--color-danger-text,#ee7676)}.activity-row.kind-shell{--activity-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 42%, #64748b 58%)}.activity-row.kind-shell-result{--activity-accent:color-mix(in srgb, var(--color-success-text,#6fda83) 72%, var(--color-text-secondary,#b6b6bd))}.activity-row.kind-capability{--activity-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 34%, #3b82f6 66%)}.activity-row.kind-batch{--activity-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 34%, #8b5cf6 66%)}.activity-row.phase-executing{--activity-phase:var(--color-text-info,#38bdf8)}.activity-row.phase-returned{--activity-phase:var(--color-warning-text,#e6b566)}.activity-row.phase-done{--activity-phase:var(--color-success-text,#6fda83)}.activity-row.phase-error{--activity-phase:var(--color-danger-text,#ee7676)}.activity-row.shell-result{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--activity-accent) 68%, transparent)}.activity-icon{border:1px solid color-mix(in srgb, var(--activity-accent) 18%, transparent);background:color-mix(in srgb, var(--activity-accent) 9%, transparent);width:28px;height:28px;color:var(--activity-accent);border-radius:7px;place-items:center;display:grid}.activity-icon-svg{stroke-width:1.8px;width:15px;height:15px}.activity-main{gap:4px;min-width:0;display:grid}.activity-title-line{align-items:baseline;gap:8px;min-width:0;display:flex}.activity-title{color:var(--color-text-primary,#f5f5f6);font-size:var(--font-text-sm-size,12px);flex:none;font-weight:600;line-height:1.35}.activity-member{color:var(--color-text-secondary,#d4d4d8);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,10px);flex:none;line-height:1.4}.activity-target{min-width:0;color:var(--color-text-tertiary,#a3a3aa);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,11px);text-overflow:ellipsis;white-space:nowrap;line-height:1.4;overflow:hidden}.activity-meta{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,10px);font-variant-numeric:tabular-nums;white-space:nowrap;justify-content:flex-end;align-items:center;gap:8px;display:inline-flex}.activity-phase{color:var(--activity-phase);align-items:center;gap:5px;display:inline-flex}.activity-phase:before{content:"";background:currentColor;border-radius:9999px;width:6px;height:6px}.activity-progress-wrap{grid-template-columns:auto minmax(48px,110px);align-items:center;gap:8px;max-width:220px;display:grid}.activity-progress-counts{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,10px);font-variant-numeric:tabular-nums;white-space:nowrap}.activity-progress-track{background:color-mix(in srgb, var(--activity-accent) 14%, var(--tool-card-divider));border-radius:9999px;height:3px;display:block;overflow:hidden}.activity-progress-fill{border-radius:inherit;background:var(--activity-accent);height:100%;display:block}.activity-empty,.activity-refresh-error{color:var(--color-text-secondary,#b7b7bf);font-size:var(--font-text-sm-size,12px);padding:12px}.activity-refresh-error{border-top:1px solid var(--tool-card-divider);color:var(--color-danger-text,#ee7676)}.activity-detail-chevron,.activity-detail-spacer,.activity-detail-chevron.chevron{width:16px;height:16px}.activity-detail-chevron .icon-svg{width:13px;height:13px}.activity-entry.expanded>.activity-row{background:color-mix(in srgb, var(--activity-accent) 6%, transparent)}.activity-detail{border-top:1px solid color-mix(in srgb, var(--tool-card-divider) 72%, transparent);background:color-mix(in srgb, var(--color-background-primary,#101114) 90%, transparent);display:grid}.activity-detail-section+.activity-detail-section{border-top:1px solid color-mix(in srgb, var(--tool-card-divider) 62%, transparent)}.activity-detail-label{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,10px);letter-spacing:.02em;padding:8px 12px 0;font-weight:600}.activity-detail-section.error .activity-detail-label,.activity-detail-section.error .activity-detail-value{color:var(--color-danger-text,#ee7676)}.activity-detail-value{max-height:260px;color:var(--color-text-secondary,#c7c7ce);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,11px);white-space:pre-wrap;overflow-wrap:break-word;margin:0;padding:6px 12px 10px;line-height:1.5;overflow:auto}.activity-detail-status{color:var(--color-text-secondary,#b7b7bf);font-size:var(--font-text-sm-size,12px);padding:10px 12px}.activity-detail-status.error{color:var(--color-danger-text,#ee7676)}.activity-terminal{background:var(--color-background-primary,#101114)}.activity-terminal-command,.activity-terminal-output{color:var(--color-text-primary,#f5f5f6);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,11px);white-space:pre-wrap;overflow-wrap:break-word;background:0 0;border:0;border-radius:0;margin:0;line-height:1.55}.activity-terminal-command{border-bottom:1px solid color-mix(in srgb, var(--tool-card-divider) 74%, transparent);color:var(--color-text-secondary,#c7c7ce);padding:9px 12px}.activity-terminal-output{max-height:320px;padding:10px 12px;overflow:auto}.activity-terminal-meta{border-top:1px solid color-mix(in srgb, var(--tool-card-divider) 74%, transparent);color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-xs-size,10px);font-variant-numeric:tabular-nums;padding:7px 12px}.activity-terminal-meta.status-running{color:var(--color-text-info,#38bdf8)}.activity-terminal-meta.status-done{color:var(--color-success-text,#6fda83)}.activity-terminal-meta.status-failed{color:var(--color-danger-text,#ee7676)}@media (width<=520px){.activity-panel-header{grid-template-columns:12px minmax(0,1fr) auto 18px;gap:8px;min-height:54px;padding:8px 10px}.activity-panel-subtitle,.activity-panel-count{font-size:10px}.activity-row{grid-template-columns:26px minmax(0,1fr) 18px;gap:6px 8px;min-height:48px;padding:8px 10px}.activity-row.child{padding-left:28px}.activity-row.child:before{left:16px}.activity-icon{align-self:start;width:26px;height:26px}.activity-title-line{gap:2px;display:grid}.activity-meta{grid-column:2;justify-content:flex-start}.activity-detail-chevron,.activity-detail-spacer{grid-area:1/3/span 2}.activity-progress-wrap{grid-template-columns:auto minmax(40px,1fr);max-width:none}}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;font-family:var(--font-sans,ui-sans-serif, system-ui, sans-serif);color:var(--color-text-primary,#f5f5f6);--tool-card-border:color-mix(in srgb, var(--color-border-primary,#414141) 74%, transparent);--tool-card-header-bg:color-mix(in srgb, var(--color-background-secondary,#272727) 88%, transparent);--tool-card-body-bg:color-mix(in srgb, var(--color-background-primary,#181818) 94%, transparent);--tool-card-hover-bg:color-mix(in srgb, var(--color-background-tertiary,#343434) 46%, transparent);--tool-card-divider:color-mix(in srgb, var(--color-border-primary,#414141) 66%, transparent);--tool-accent:var(--color-text-secondary,#b6b6bd);--scrollbar-thumb:color-mix(in srgb, var(--color-text-tertiary,#8a8a8a) 56%, transparent);--scrollbar-thumb-hover:color-mix(in srgb, var(--color-text-secondary,#a8a8a8) 82%, transparent);background:0 0}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}*{box-sizing:border-box}html,body{background:0 0;margin:0;overflow:hidden}.shell{width:100%;padding:0;overflow:hidden}.empty,.tool-card{--tool-accent-soft:color-mix(in srgb, var(--tool-accent) 12%, transparent);border:1px solid var(--tool-card-border);background:var(--tool-card-header-bg);width:100%;box-shadow:none;color:var(--color-text-primary,#f5f5f6);border-radius:12px;overflow:hidden}.tool-card.workspace,.tool-card.directory{--tool-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 34%, #3b82f6 66%)}.tool-card.read,.tool-card.search{--tool-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 32%, #06b6d4 68%)}.tool-card.write{--tool-accent:var(--color-success-text,#6fda83)}.tool-card.edit,.tool-card.review{--tool-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 28%, #d99742 72%)}.tool-card.delete{--tool-accent:var(--color-danger-text,#ee7676)}.tool-card.shell{--tool-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 42%, #64748b 58%)}.tool-card.state-success{--tool-accent:var(--color-success-text,#6fda83)}.tool-card.state-error{--tool-accent:var(--color-danger-text,#ee7676)}.tool-card.state-running{--tool-accent:color-mix(in srgb, var(--color-text-primary,#f5f5f6) 30%, #38bdf8 70%)}@supports selector(::-webkit-scrollbar){.pretty-scrollbar::-webkit-scrollbar{width:12px;height:12px}.pretty-scrollbar::-webkit-scrollbar-button{width:0;height:0;display:none}.pretty-scrollbar::-webkit-scrollbar-track{background:0 0}.pretty-scrollbar::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb);background-clip:content-box;border:4px solid #0000;border-radius:9999px}.pretty-scrollbar::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover)}.pretty-scrollbar::-webkit-scrollbar-thumb:active{background-color:var(--scrollbar-thumb-hover)}.pretty-scrollbar::-webkit-scrollbar-corner{background:0 0}}.empty{color:var(--color-text-secondary,#b6b6bd);font-size:var(--font-text-sm-size,13px);padding:14px 16px}.tool-header{width:100%;min-height:64px;color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;border-radius:11px;grid-template-columns:40px minmax(0,1fr) auto 20px;align-items:center;gap:12px;padding:10px 12px;display:grid}.tool-header:focus-visible,.review-diff-file-header:focus-visible,.review-more:focus-visible{outline:2px solid color-mix(in srgb, var(--tool-accent) 72%, transparent);outline-offset:-2px}.tool-header:hover:not(:disabled){background:var(--tool-card-hover-bg)}.tool-header:disabled{cursor:default}.tool-icon{border:1px solid color-mix(in srgb, var(--tool-accent) 18%, transparent);background:var(--tool-accent-soft);width:40px;height:40px;color:var(--tool-accent);border-radius:10px;place-items:center;display:grid}.icon-svg{stroke-width:1.8px;width:20px;height:20px;display:block}.tool-main{gap:2px;min-width:0;display:grid}.tool-title{color:var(--color-text-primary,#f5f5f6);font-size:var(--font-text-sm-size,14px);font-weight:550;line-height:1.3}.tool-label{color:var(--color-text-tertiary,#a3a3aa);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,12px);text-overflow:ellipsis;white-space:nowrap;line-height:1.4;overflow:hidden}.stats{font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-xs-size,12px);font-variant-numeric:tabular-nums;white-space:nowrap;align-items:center;gap:5px;display:inline-flex}.header-meta{color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-sm-size,12px);text-align:right;white-space:nowrap;line-height:1.35}.header-meta.empty{width:0}.add{color:var(--color-success-text,#6fda83)}.remove{color:var(--color-danger-text,#ee7676)}.chevron{width:20px;height:20px;color:var(--color-text-tertiary,#a3a3aa);border-radius:7px;place-items:center;transition:background .14s,color .14s,transform .14s;display:grid}.tool-header:hover:not(:disabled) .chevron{color:var(--color-text-primary,#f5f5f6)}.chevron .icon-svg{width:15px;height:15px}.chevron.expanded{transform:rotate(180deg)}.chevron.loading{transform:none}.chevron.loading .icon-svg{fill:none;stroke-linecap:round;stroke-dasharray:38 14;animation:.7s linear infinite payload-spinner}@keyframes payload-spinner{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.chevron.loading .icon-svg{animation:none}}.tool-body{border-top:1px solid var(--tool-card-divider);background:var(--tool-card-body-bg)}.workspace-details{max-height:420px;display:grid;overflow:auto}.workspace-rows{padding:4px 0;display:grid}.workspace-row{grid-template-columns:22px minmax(116px,.24fr) minmax(0,1fr);align-items:center;gap:10px;min-height:40px;padding:7px 12px;display:grid}.workspace-row-icon{background:color-mix(in srgb, var(--tool-accent) 9%, transparent);width:22px;height:22px;color:color-mix(in srgb, var(--tool-accent) 72%, var(--color-text-tertiary,#a3a3aa));border-radius:6px;place-items:center;display:grid}.workspace-row-icon-svg{stroke-width:1.8px;width:14px;height:14px}.workspace-key{min-height:22px;color:var(--color-text-tertiary,#a3a3aa);font-size:var(--font-text-sm-size,12px);align-items:center;font-weight:500;display:flex}.workspace-value{min-width:0;color:var(--color-text-secondary,#c7c7ce);font-size:var(--font-text-sm-size,12px);text-overflow:ellipsis;white-space:nowrap;line-height:1.45;overflow:hidden}.workspace-value.mono{font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace)}.workspace-base-value{align-items:center;gap:7px;min-width:0;display:flex}.workspace-base-value .workspace-value{flex:0 auto}.workspace-base-warning{width:18px;height:18px;color:var(--color-warning-text,#e6b566);cursor:help;flex:none;place-items:center;display:grid}.workspace-base-warning-svg{stroke-width:2px;width:14px;height:14px}.workspace-chip-list{flex-wrap:nowrap;align-items:center;gap:6px;min-width:0;display:flex;overflow:hidden}.workspace-chip{border:1px solid color-mix(in srgb, var(--tool-accent) 16%, var(--tool-card-divider));background:color-mix(in srgb, var(--tool-accent) 7%, transparent);max-width:100%;min-height:24px;color:var(--color-text-secondary,#c7c7ce);text-overflow:ellipsis;white-space:nowrap;border-radius:9999px;flex:none;align-items:center;gap:5px;padding:3px 8px;font-size:11px;line-height:1.25;display:inline-flex;overflow:hidden}.workspace-chip-logo{object-fit:contain;flex:none;width:13px;height:13px;display:block}.workspace-chip-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.workspace-agent-profile{border:0;border-bottom:1px solid color-mix(in srgb, var(--tool-accent) 34%, var(--tool-card-divider));max-width:100%;min-height:24px;color:var(--color-text-secondary,#c7c7ce);white-space:nowrap;background:0 0;border-radius:0;align-items:center;gap:5px;padding:3px 2px 4px;font-size:11px;line-height:1.25;display:inline-flex;overflow:hidden}.workspace-agent-profile-logo{object-fit:contain;flex:none;width:14px;height:14px;display:block}.workspace-agent-profile:hover{border-bottom-color:var(--tool-accent);color:var(--color-text-primary,#f5f5f6)}.workspace-agent-profile.muted{color:var(--color-text-tertiary,#a3a3aa);opacity:.72;border-bottom-style:dashed}.workspace-provider-logo{cursor:help;flex:none;place-items:center;width:20px;height:24px;display:inline-grid}.workspace-provider-logo-image{object-fit:contain;width:16px;height:16px;display:block}.workspace-provider-logo.muted{opacity:.62}.workspace-chip.muted{color:var(--color-text-tertiary,#a3a3aa);opacity:.72;border-style:dashed}.workspace-skills-row,.workspace-instructions-row,.workspace-agents-row{align-items:start}.workspace-skills-list,.workspace-agents-list{flex-wrap:wrap;overflow:visible}.workspace-instruction-status{border-radius:5px;flex:none;place-items:center;width:18px;height:18px;display:grid}.workspace-instruction-status.loaded{background:color-mix(in srgb, var(--color-success-text,#6fda83) 12%, transparent);color:var(--color-success-text,#6fda83)}.workspace-instruction-status.available{background:color-mix(in srgb, var(--color-text-tertiary,#a3a3aa) 9%, transparent);color:var(--color-text-tertiary,#a3a3aa)}.workspace-instruction-status-svg{stroke-width:1.9px;width:12px;height:12px}.workspace-instruction-list{border:1px solid var(--tool-card-divider);background:color-mix(in srgb, var(--tool-card-body-bg) 88%, transparent);border-radius:9px;min-width:0;display:grid;overflow:hidden}.workspace-instructions-content{min-width:0;display:block}.workspace-instructions-toggle{border:0;border-top:1px solid var(--tool-card-divider);background:color-mix(in srgb, var(--tool-card-body-bg) 72%, transparent);width:100%;min-height:32px;color:var(--tool-accent);cursor:pointer;font:inherit;font-size:var(--font-text-sm-size,11px);white-space:nowrap;border-radius:0 0 9px 9px;justify-content:center;align-items:center;padding:6px 10px;font-weight:550;line-height:1.25;display:flex}.workspace-instructions-toggle:hover{background:var(--tool-card-hover-bg)}.workspace-instructions-toggle:focus-visible{outline:2px solid color-mix(in srgb, var(--tool-accent) 72%, transparent);outline-offset:2px}.workspace-instruction-item+.workspace-instruction-item{border-top:1px solid var(--tool-card-divider)}.workspace-instruction-header{width:100%;min-width:0;color:inherit;font:inherit;text-align:left;background:0 0;border:0;grid-template-columns:22px minmax(0,1fr) 18px;align-items:center;gap:9px;padding:8px 10px;display:grid}.workspace-instruction-header.interactive{cursor:pointer}.workspace-instruction-header.interactive:hover{background:var(--tool-card-hover-bg)}.workspace-instruction-header.interactive:focus-visible{outline:2px solid color-mix(in srgb, var(--tool-accent) 72%, transparent);outline-offset:-2px}.workspace-instruction-text{gap:2px;min-width:0;display:grid}.workspace-instruction-name{min-width:0;color:var(--color-text-primary,#f5f5f6);font-size:var(--font-text-sm-size,12px);text-overflow:ellipsis;white-space:nowrap;font-weight:550;overflow:hidden}.workspace-instruction-path{min-width:0;color:var(--color-text-tertiary,#a3a3aa);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:1.35;overflow:hidden}.workspace-instruction-chevron{width:18px;height:18px;color:var(--color-text-tertiary,#a3a3aa);place-items:center;transition:transform .14s;display:grid}.workspace-instruction-item.expanded .workspace-instruction-chevron{transform:rotate(180deg)}.workspace-instruction-chevron-svg{width:14px;height:14px}.workspace-instruction-preview{border-top:1px solid var(--tool-card-divider);background:color-mix(in srgb, var(--color-background-primary,#101114) 92%, transparent);max-height:300px;color:var(--color-text-secondary,#c7c7ce);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);white-space:pre-wrap;overflow-wrap:break-word;margin:0;padding:10px 12px;font-size:11px;line-height:1.55;overflow:auto}.workspace-instruction-preview[hidden]{display:none}.review-header{grid-template-columns:40px minmax(0,1fr) auto 20px}.review-title-group{gap:3px;min-width:0;display:grid}.review-summary{border-top:1px solid var(--tool-card-divider);background:var(--tool-card-body-bg);display:grid}.review-diff-file-stats{align-items:center;gap:8px;display:flex}.review-empty{color:var(--color-text-secondary,#b7b7bf);font-size:var(--font-text-sm-size,13px)}.review-more{border:0;border-top:1px solid var(--tool-card-divider);width:100%;min-height:40px;color:var(--color-text-tertiary,#a3a3aa);cursor:pointer;font:inherit;font-size:var(--font-text-sm-size,12px);text-align:left;background:0 0;padding:0 12px}.review-more:hover{background:var(--tool-card-hover-bg);color:var(--color-text-primary,#f5f5f6)}.review-diff{max-height:520px;display:grid;overflow:hidden auto}.review-diff-files{gap:0;padding:0;display:grid}.review-diff-file{border:0;border-radius:0;overflow:hidden}.review-diff-file+.review-diff-file{border-top:1px solid var(--tool-card-divider)}.review-diff-file-header{width:100%;min-height:42px;color:var(--color-text-primary,#f5f5f6);cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;grid-template-columns:22px minmax(0,1fr) auto;align-items:center;gap:10px;padding:0 12px;display:grid}.review-file-kind{background:color-mix(in srgb, var(--color-text-tertiary,#a3a3aa) 10%, transparent);width:20px;height:20px;color:var(--color-text-tertiary,#a3a3aa);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);border-radius:6px;align-self:center;place-items:center;font-size:10px;font-weight:700;display:grid}.review-file-kind.added{background:color-mix(in srgb, var(--color-success-text,#6fda83) 12%, transparent);color:var(--color-success-text,#6fda83)}.review-file-kind.edited,.review-file-kind.renamed,.review-file-kind.renamed-edited{background:color-mix(in srgb, var(--color-warning-text,#e6b566) 12%, transparent);color:var(--color-warning-text,#e6b566)}.review-file-kind.deleted{background:color-mix(in srgb, var(--color-danger-text,#ee7676) 12%, transparent);color:var(--color-danger-text,#ee7676)}.review-single-file{overflow:hidden}.review-diff-file-header:hover{background:var(--tool-card-hover-bg)}.review-diff-file-name,.review-diff-file-stats{text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:20px;overflow:hidden}.review-diff-file-name{font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace)}.review-diff-file-name.renamed{text-overflow:clip;align-items:center;gap:6px;min-width:0;display:flex}.review-diff-file-path{text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:calc(50% - 10px);overflow:hidden}.review-diff-file-path.previous{color:var(--color-text-tertiary,#a3a3aa)}.review-diff-file-path.current{color:var(--color-text-primary,#f5f5f6)}.review-diff-file-arrow{color:var(--color-text-tertiary,#a3a3aa);font-family:var(--font-sans,ui-sans-serif, system-ui, sans-serif);flex:none}.review-diff-file-stats{font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-sm-size,12px);font-variant-numeric:tabular-nums;justify-content:flex-end;overflow:visible}.status{font-size:var(--font-text-sm-size,12px);padding:10px 12px}.status.muted{color:var(--color-text-secondary,#b7b7bf)}.status.error{color:var(--color-danger-text,#ee7676)}.pierre-diff,.pierre-file{--diffs-bg:var(--tool-payload-bg,var(--color-background-primary,#101114));--diffs-light-bg:var(--color-background-primary,#fff);--diffs-dark-bg:var(--tool-payload-bg,var(--color-background-primary,#101114));--diffs-font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);--diffs-header-font-family:var(--font-sans,ui-sans-serif, system-ui, sans-serif);--diffs-font-size:var(--font-text-sm-size,12px);--diffs-line-height:20px;border-bottom-right-radius:8px;border-bottom-left-radius:8px;max-height:420px;display:block;overflow:auto}.text-payload{max-height:420px;color:var(--color-text-secondary,#c7c7ce);font-family:var(--font-mono,ui-monospace, SFMono-Regular, monospace);font-size:var(--font-text-sm-size,12px);white-space:pre-wrap;overflow-wrap:break-word;margin:0;padding:10px 12px;line-height:1.55;overflow:auto}.text-payload.bash{color:var(--color-text-primary,#f5f5f6);background:var(--color-background-primary,#101114)}@media (width<=520px){.tool-header{grid-template-columns:36px minmax(0,1fr) auto 18px;gap:9px;min-height:58px;padding:9px 10px}.tool-icon{border-radius:9px;width:36px;height:36px}.chevron{width:18px;height:18px}.review-header{grid-template-columns:36px minmax(0,1fr) auto 18px}.workspace-row{grid-template-columns:22px minmax(0,1fr);gap:2px 8px;padding-block:8px}.workspace-row-icon{grid-row:1/span 2;align-self:start}.workspace-row>.workspace-key,.workspace-row>.workspace-value,.workspace-row>.workspace-chip-list,.workspace-row>.workspace-instructions-content{grid-column:2}}
@@ -0,0 +1 @@
1
+ import"./workspace-app-CwbJnb_w.js";import"./workspace-app-BztEvZIC.js";document.documentElement.dataset.forgerelayApp=`historical-tool-card`;
@@ -0,0 +1 @@
1
+ import"./workspace-app-CwbJnb_w.js";import"./workspace-app-BztEvZIC.js";document.documentElement.dataset.forgerelayApp=`workspace-lifecycle-compatibility`;
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ForgeRelay Diff</title>
7
- <script type="module" crossorigin src="./assets/workspace-app-DJmkPYJC.js"></script>
7
+ <script type="module" crossorigin src="./assets/workspace-app-rKuhdae8.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/chunk-EyZ2wyi3.js">
9
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-DkAiSl_0.js">
10
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-QyauBrJX.js">
11
- <link rel="stylesheet" crossorigin href="./assets/workspace-app-CcrHAUIn.css">
9
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-CwbJnb_w.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-BztEvZIC.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/workspace-app-YnUST8IP.css">
12
12
  </head>
13
13
  <body>
14
14
  <main id="app" class="shell">
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ForgeRelay Workspace Lifecycle</title>
7
- <script type="module" crossorigin src="./assets/workspace-lifecycle-app-BIXEo53I.js"></script>
7
+ <script type="module" crossorigin src="./assets/workspace-lifecycle-app-CEfMdudP.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/chunk-EyZ2wyi3.js">
9
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-DkAiSl_0.js">
10
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-QyauBrJX.js">
11
- <link rel="stylesheet" crossorigin href="./assets/workspace-app-CcrHAUIn.css">
9
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-CwbJnb_w.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-BztEvZIC.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/workspace-app-YnUST8IP.css">
12
12
  </head>
13
13
  <body>
14
14
  <main id="app" class="shell">
@@ -1,9 +1,13 @@
1
- import { randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { expandHomePath } from "./roots.js";
6
6
  import { mergeHookConfigs, parseHookFile, } from "./hooks.js";
7
+ const AUTH_LOCK_RETRY_MS = 10;
8
+ const AUTH_LOCK_TIMEOUT_MS = 5_000;
9
+ const AUTH_LOCK_STALE_MS = 30_000;
10
+ const AUTH_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
7
11
  export function forgerelayConfigDir(env = process.env) {
8
12
  const explicit = env.FORGERELAY_CONFIG_DIR ?? env.DEVSPACE_CONFIG_DIR;
9
13
  if (explicit)
@@ -64,12 +68,81 @@ export function writeForgeRelayConfig(config, env = process.env) {
64
68
  export function writeForgeRelayAuth(auth, env = process.env) {
65
69
  const filePath = forgerelayAuthPath(env);
66
70
  mkdirSync(forgerelayConfigDir(env), { recursive: true });
67
- writeJsonFile(filePath, auth, 0o600);
68
- return filePath;
71
+ return withAuthFileLock(filePath, () => {
72
+ writeJsonFile(filePath, auth, 0o600);
73
+ return filePath;
74
+ });
69
75
  }
70
76
  export function generateOwnerToken() {
71
77
  return randomBytes(32).toString("base64url");
72
78
  }
79
+ export function generateInstanceId() {
80
+ return `forge-${randomUUID()}`;
81
+ }
82
+ export function ensureForgeRelayInstanceId(env = process.env) {
83
+ const existing = loadForgeRelayFiles(env).auth.instanceId?.trim();
84
+ if (existing)
85
+ return existing;
86
+ let resolved = "";
87
+ updateForgeRelayAuth((auth) => {
88
+ const current = auth.instanceId?.trim();
89
+ if (current) {
90
+ resolved = current;
91
+ return auth;
92
+ }
93
+ resolved = generateInstanceId();
94
+ return { ...auth, instanceId: resolved };
95
+ }, env);
96
+ return resolved;
97
+ }
98
+ function normalizeRemoteAlias(alias) {
99
+ const normalized = alias.trim();
100
+ if (!normalized || /\s/.test(normalized)) {
101
+ throw new Error("Remote alias must be a non-empty name without whitespace.");
102
+ }
103
+ return normalized;
104
+ }
105
+ export function writeForgeRelayRemote(alias, remote, env = process.env) {
106
+ const normalizedAlias = normalizeRemoteAlias(alias);
107
+ return updateForgeRelayAuth((auth) => {
108
+ const remotes = { ...(auth.remotes ?? {}) };
109
+ const existing = remotes[normalizedAlias];
110
+ if (existing && existing.instanceId !== remote.instanceId) {
111
+ throw new Error(`Remote alias ${normalizedAlias} already belongs to another ForgeRelay instance.`);
112
+ }
113
+ const duplicate = Object.entries(remotes).find(([name, record]) => name !== normalizedAlias && record.instanceId === remote.instanceId);
114
+ if (duplicate) {
115
+ throw new Error(`ForgeRelay instance is already registered as ${duplicate[0]}; rename that remote instead.`);
116
+ }
117
+ remotes[normalizedAlias] = remote;
118
+ return { ...auth, remotes };
119
+ }, env);
120
+ }
121
+ export function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
122
+ const from = normalizeRemoteAlias(fromAlias);
123
+ const to = normalizeRemoteAlias(toAlias);
124
+ return updateForgeRelayAuth((auth) => {
125
+ const remotes = { ...(auth.remotes ?? {}) };
126
+ const remote = remotes[from];
127
+ if (!remote)
128
+ throw new Error(`Unknown remote alias: ${from}`);
129
+ if (from !== to && remotes[to])
130
+ throw new Error(`Remote alias already exists: ${to}`);
131
+ delete remotes[from];
132
+ remotes[to] = remote;
133
+ return { ...auth, remotes };
134
+ }, env);
135
+ }
136
+ export function removeForgeRelayRemote(alias, env = process.env) {
137
+ const normalizedAlias = normalizeRemoteAlias(alias);
138
+ return updateForgeRelayAuth((auth) => {
139
+ const remotes = { ...(auth.remotes ?? {}) };
140
+ if (!remotes[normalizedAlias])
141
+ throw new Error(`Unknown remote alias: ${normalizedAlias}`);
142
+ delete remotes[normalizedAlias];
143
+ return { ...auth, remotes };
144
+ }, env);
145
+ }
73
146
  export function ensureForgeRelayDefaultSkills(env = process.env) {
74
147
  const targetPath = join(forgerelaySkillsDir(env), "subagent-delegation", "SKILL.md");
75
148
  if (existsSync(targetPath))
@@ -124,6 +197,59 @@ function readJsonFile(filePath) {
124
197
  throw new Error(`Unable to read ${filePath}: ${reason}`);
125
198
  }
126
199
  }
200
+ function updateForgeRelayAuth(update, env) {
201
+ const filePath = forgerelayAuthPath(env);
202
+ mkdirSync(forgerelayConfigDir(env), { recursive: true });
203
+ return withAuthFileLock(filePath, () => {
204
+ const auth = existsSync(filePath) ? readJsonFile(filePath) : {};
205
+ writeJsonFile(filePath, update(auth), 0o600);
206
+ return filePath;
207
+ });
208
+ }
209
+ function withAuthFileLock(filePath, operation) {
210
+ const lockPath = `${filePath}.lock`;
211
+ const deadline = Date.now() + AUTH_LOCK_TIMEOUT_MS;
212
+ for (;;) {
213
+ try {
214
+ const fd = openSync(lockPath, "wx", 0o600);
215
+ closeSync(fd);
216
+ break;
217
+ }
218
+ catch (error) {
219
+ const code = error.code;
220
+ if (code !== "EEXIST")
221
+ throw error;
222
+ try {
223
+ if (Date.now() - statSync(lockPath).mtimeMs > AUTH_LOCK_STALE_MS) {
224
+ rmSync(lockPath, { force: true });
225
+ continue;
226
+ }
227
+ }
228
+ catch (statError) {
229
+ if (statError.code === "ENOENT")
230
+ continue;
231
+ throw statError;
232
+ }
233
+ if (Date.now() >= deadline) {
234
+ throw new Error(`Timed out waiting for ForgeRelay auth lock: ${lockPath}`);
235
+ }
236
+ Atomics.wait(AUTH_LOCK_SLEEP, 0, 0, AUTH_LOCK_RETRY_MS);
237
+ }
238
+ }
239
+ try {
240
+ return operation();
241
+ }
242
+ finally {
243
+ rmSync(lockPath, { force: true });
244
+ }
245
+ }
127
246
  function writeJsonFile(filePath, value, mode) {
128
- writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", { mode });
247
+ const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
248
+ try {
249
+ writeFileSync(tempPath, JSON.stringify(value, null, 2) + "\n", { mode });
250
+ renameSync(tempPath, filePath);
251
+ }
252
+ finally {
253
+ rmSync(tempPath, { force: true });
254
+ }
129
255
  }
@@ -305,6 +305,25 @@ switching a logical workspace therefore does not by itself cause unchanged proje
305
305
  context to be injected again, while changed context produces a new fingerprint and
306
306
  is delivered on the next `auto` open.
307
307
 
308
+ Composite Workspaces use the same `open_workspace` entry point with
309
+ `kind="composite"` and a human-readable `name`. They have no filesystem root of
310
+ their own and may contain zero or more named members. Member management also stays
311
+ on `open_workspace` through `action="member"`; an added member either references an
312
+ existing `workspaceId` or reuses the normal local/managed/relay Workspace open
313
+ definition (`path`, optional `relay`, `mode`, and worktree options). Every Composite
314
+ open returns its `kind`, name, and member purposes regardless of
315
+ `context="auto"|"full"|"none"`. To receive one member's heavy project bootstrap,
316
+ reopen the Composite with `memberName=<name>`; this does not create an implicit
317
+ current member. Core work calls against a Composite require an explicit `member`
318
+ and retain the `cws_...` ID as the Host-facing Workspace identity.
319
+
320
+ Composite member purpose text is descriptive only. ForgeRelay never routes by tool
321
+ type, hardware role, load, or availability, and a missing/offline member never falls
322
+ back to another member or local execution. Member filesystems, Git state, processes,
323
+ Hooks, Skills, language services, and Activity facts remain owned by the underlying
324
+ Workspace. The Composite Activity Panel only aggregates their presentation into one
325
+ Host Turn.
326
+
308
327
  Use `open_workspace(action="list")` only when the Agent needs to continue an older
309
328
  logical workspace, choose among multiple handles, or organize workspace state. The
310
329
  inventory is paginated (50 records by default, at most 100) and can filter by
@@ -321,6 +340,10 @@ or externally removed managed-worktree root can therefore remain diagnostically
321
340
  files. For a managed-worktree-backed workspace, `close_workspace` requires
322
341
  `commitMessage` and runs the existing safe worktree finalize lifecycle: close Hooks,
323
342
  commit when needed, fast-forward-only integration, cleanup, and alias invalidation.
343
+ For a Composite Workspace, the same tool means dissolve: it removes only the
344
+ Composite identity, membership, and Composite-facing panel/lifecycle state. It does
345
+ not close members, finalize member worktrees, interrupt member processes, delete
346
+ files, or remove remote registrations and relayed Workspace routes.
324
347
 
325
348
  Hot workspace/session activity timestamps are coalesced in memory and flushed to the
326
349
  SQLite state database in a transaction at most every five minutes; normal shutdown
@@ -386,7 +409,7 @@ Hooks v1 是自动生命周期规则。规则由用户或 Agent 主动写入;
386
409
  <workspace>/.forgerelay/hooks/<hook-name>.json
387
410
  ```
388
411
 
389
- 文件名去掉 `.json` 后就是 Hook 名,也是日志和 Agent-visible report 中显示的名称。例如 `release-tag-local-ci.json` 会显示为 `release-tag-local-ci`。目录内按文件名字典序执行;需要显式排序时可以使用 `10-release-verify.json`、`20-package-inspection.json` 这样的前缀。ForgeRelay 只读取普通 `*.json` 文件,所以临时停用某条 Hook 时可以把扩展名改掉。
412
+ 文件名去掉 `.json` 后就是 Hook 名,也是日志和 Agent-visible report 中显示的名称。例如 `release-tag-gate.json` 会显示为 `release-tag-gate`。目录内按文件名字典序执行;需要显式排序时可以使用 `10-release-verify.json`、`20-package-inspection.json` 这样的前缀。ForgeRelay 只读取普通 `*.json` 文件,所以临时停用某条 Hook 时可以把扩展名改掉。
390
413
 
391
414
  全局 Hook 在 server 启动时读取,修改后需要重启 ForgeRelay;项目目录在每次事件时重新读取,所以 Agent 修改项目 Hook 后不需要重启。全局规则先执行,项目规则随后执行,两边都只做追加,不互相覆盖。
392
415
 
@@ -405,7 +428,7 @@ Hooks v1 是自动生命周期规则。规则由用户或 Agent 主动写入;
405
428
  }
406
429
  ```
407
430
 
408
- 这个例子可以保存为 `.forgerelay/hooks/release-tag-local-ci.json`。耗时的 `npm run release:verify` 应在已提交的 release-ready HEAD 上提前运行,并在 `.git/forgerelay/` 写入 release proof。Agent 之后通过 ForgeRelay `bash` 请求推送稳定版本 tag 时,Hook 只快速校验 proof、当前 HEAD/package version、clean working tree(含 untracked)与本地 tag 指向;全部一致才执行原始 `git push`。因此发布 gate 不再依赖一个持续数分钟的单次 MCP request,同时任何验证后的代码变化都会使 proof 失效并阻断推送。
431
+ 这个例子可以保存为 `.forgerelay/hooks/release-tag-gate.json`。Agent 通过 ForgeRelay `bash` 请求推送稳定版本 tag 时,Hook 只执行轻量仓库状态门禁:拒绝 force/delete 形式,校验 clean working tree(含 untracked)、tag 与 package version 一致,并要求本地 tag 指向当前 HEAD。Hook 不运行、也不要求本地 CI;tag 推送后由 GitHub Actions Linux/macOS/Windows 矩阵执行权威验证,全部通过后发布 job 才会继续。`npm run release:verify` 仅用于需要时本地复现云端环境。
409
432
 
410
433
  独立 Hook 文件支持这些顶层字段:
411
434
 
@@ -472,7 +495,7 @@ Matcher 匹配 ForgeRelay 收到的那次 tool request,不会窥探该命令
472
495
 
473
496
  ```text
474
497
  Hook results:
475
- ✓ release-tag-local-ci (BeforeTool, project) passed in 42ms
498
+ ✓ release-tag-gate (BeforeTool, project) passed in 42ms
476
499
  ```
477
500
 
478
501
  阻断失败会明确显示 `failed`。ForgeRelay 的 server instructions 要求 Agent 在出现 Hook results 时,向用户说明有意义的 Hook 是否通过或阻断了操作。异步 subagent 的 `SubagentStart` / `SubagentStop` 报告会随 session 持久化,并由 `forgerelay agents show` 展示。
package/docs/debugging.md CHANGED
@@ -50,6 +50,13 @@ To relocate the entire interactive debug config explicitly, use:
50
50
  FORGERELAY_DEBUG_CONFIG_DIR=/path/to/debug-config npm run dev
51
51
  ```
52
52
 
53
+ The selected debug config is authoritative for its own `host` and `port`; the
54
+ launcher reports endpoints derived from that file instead of assuming `7677`.
55
+ This makes parallel isolated instances straightforward: give each instance its
56
+ own config, state, and worktree directories and configure different ports (for
57
+ example `7677` for a Gateway and `6768` for an Execution ForgeRelay), then launch
58
+ each with its own `FORGERELAY_DEBUG_CONFIG_DIR`.
59
+
53
60
  ## Run a local MCP Apps Host
54
61
 
55
62
  Use MCP Inspector as the fast local Host for MCP App/resource iteration:
@@ -79,25 +79,20 @@ The bump commands update:
79
79
 
80
80
  They do not commit, tag, push, or publish.
81
81
 
82
- Run the full local release gate with:
82
+ For local diagnosis or cloud-parity reproduction, you can optionally run:
83
83
 
84
84
  ```bash
85
85
  npm run release:verify
86
86
  ```
87
87
 
88
- `release:verify` checks the current committed release-ready runtime and then runs a focused
89
- `release:parity` gate in an isolated Node 22.19.0 sandbox. After every check passes it
90
- records a local release proof under `.git/forgerelay/`, binding that verification to
91
- current HEAD and the package version. The parity sandbox
92
- performs its own `npm ci` so native addons use the same Node ABI as cloud CI,
93
- then reruns the LSP/release tests most sensitive to event-loop timing, process
94
- lifecycle, path canonicalization, executable discovery, and cleanup behavior.
95
- It also tests that a command which exists on `PATH` but fails its `--version`
96
- preflight is treated as unavailable rather than as an installed Language server.
88
+ `release:verify` runs the same focused parity checks in an isolated Node 22.19.0 sandbox
89
+ and records a local proof under `.git/forgerelay/` for debugging/audit purposes. It is
90
+ not a prerequisite for pushing a release tag. The authoritative release verification
91
+ is the tag-triggered GitHub Actions matrix on Linux, macOS, and Windows.
97
92
 
98
- Cloud verification and the publication job are both pinned to Node 22.19.0, the
99
- minimum supported Node release, so local parity and the release runners use the
100
- same runtime instead of drifting across separate Node 22/24 variants.
93
+ Cloud verification and the publication job are pinned to Node 22.19.0, the minimum
94
+ supported Node release. The optional local parity tool uses the same runtime so a cloud
95
+ failure can be reproduced locally without making local execution part of the release gate.
101
96
 
102
97
  Validate a specific tag with:
103
98
 
@@ -172,11 +167,8 @@ npm publishing token.
172
167
  3. Run the appropriate `release:patch`, `release:minor`, or `release:major`
173
168
  command.
174
169
  4. Review the generated version and changelog diff, then commit the release-ready code and metadata.
175
- 5. Run `npm run release:verify` locally on that clean committed HEAD. This full local gate includes the isolated
176
- Node 22.19.0 parity sandbox and records the local release proof consumed by the tag-push Hook; ordinary development
177
- pushes do not need to run the full release gate.
178
- 6. Push the verified `main` commit without changing it afterward.
179
- 7. Create the exact version tag, for example:
170
+ 5. Push the release-ready `main` commit without changing it afterward.
171
+ 6. Create the exact version tag, for example:
180
172
 
181
173
  ```bash
182
174
  git tag v0.2.0
@@ -185,7 +177,7 @@ npm publishing token.
185
177
 
186
178
  The tag push is the publication action. The release workflow publishes npm only after cloud CI passes, then extracts the matching `CHANGELOG.md` release section as the GitHub Release body. Keep `Unreleased` user-facing and structured (`Added`, `Changed`, `Fixed`, `Security`) because those notes are what users see on the Release page.
187
179
 
188
- Project release Hooks match the stable tag-push command as a substring of the ForgeRelay shell request. A compound command is allowed: when `commandRegex` matches `git push origin vX.Y.Z`, the Hook receives that matched command as `FORGERELAY_HOOK_PAYLOAD.command` and retains the complete shell request as `originalCommand` when they differ. The release Hook does **not** rerun the multi-minute local gate inside the tag-push MCP request. Instead it quickly verifies the proof written by `release:verify`, requires a clean working tree including untracked files, requires the proof HEAD/package version to equal the current release state, and requires the local tag to resolve to that same verified HEAD. Any change after verification invalidates the proof and blocks the push until `release:verify` is rerun.
180
+ Project release Hooks match the stable tag-push command as a substring of the ForgeRelay shell request. A compound command is allowed: when `commandRegex` matches `git push origin vX.Y.Z`, the Hook receives that matched command as `FORGERELAY_HOOK_PAYLOAD.command` and retains the complete shell request as `originalCommand` when they differ. The release Hook is intentionally lightweight: it rejects force/deletion forms, requires a clean working tree including untracked files, requires the tag to match the package version, and requires the local tag to resolve to current HEAD. It does not run or require local CI. The pushed tag then starts the authoritative Linux/macOS/Windows cloud verification; publication cannot run unless that matrix succeeds.
189
181
 
190
182
  ## Attribution guardrails
191
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -41,10 +41,13 @@
41
41
  "debug:serve": "node scripts/debug/serve.mjs",
42
42
  "debug:accept": "node scripts/debug/accept.mjs",
43
43
  "lsp:interop": "node scripts/lsp-interop.mjs",
44
+ "ci:verify": "node scripts/ci/verify.mjs",
44
45
  "release:parity": "node scripts/release-parity.mjs",
46
+ "release:pack": "node scripts/release/pack.mjs",
47
+ "release:publish": "node scripts/release/publish.mjs",
45
48
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
46
49
  "start": "node dist/cli.js serve",
47
- "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/bash-output-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/activity/query-service.test.ts && tsx src/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
50
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/remote-auth-cli.test.ts && tsx src/remote-ssh-auth-cli.test.ts && tsx src/remote-workspace-relay.test.ts && tsx src/remote-workspace-relay-process.test.ts && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/bash-output-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/activity/query-service.test.ts && tsx src/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
48
51
  "typecheck": "tsc -p tsconfig.json --noEmit",
49
52
  "release:check": "node scripts/release-version.mjs check",
50
53
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ const npmCli = process.env.npm_execpath;
6
+ if (!npmCli) {
7
+ throw new Error("ci:verify must be launched through npm so npm_execpath is available");
8
+ }
9
+
10
+ console.log(`CI environment: ${process.platform}/${process.arch} ${process.version}`);
11
+ runNpm(["--version"], "npm version");
12
+ runNpm(["run", "release:check"], "Release metadata");
13
+ runNpm(["run", "typecheck"], "Typecheck");
14
+ runNpm(["test"], "Full test suite");
15
+ runNpm(["run", "build"], "Build");
16
+ runNpm(["run", "lsp:interop"], "Optional LSP interoperability");
17
+ run(process.execPath, ["dist/cli.js", "doctor"], "Doctor");
18
+
19
+ console.log("CI verification passed.");
20
+
21
+ function runNpm(args, label) {
22
+ run(process.execPath, [npmCli, ...args], label);
23
+ }
24
+
25
+ function run(command, args, label) {
26
+ console.log(`\n== ${label} ==`);
27
+ const result = spawnSync(command, args, {
28
+ cwd: process.cwd(),
29
+ env: process.env,
30
+ stdio: "inherit",
31
+ windowsHide: true,
32
+ shell: false,
33
+ });
34
+ if (result.error) throw result.error;
35
+ if (result.status !== 0) {
36
+ throw new Error(`${label} failed with exit ${result.status ?? "unknown"}`);
37
+ }
38
+ }
@@ -22,6 +22,27 @@ export function interactiveDebugConfigDir({ env = process.env, home = homedir()
22
22
  return resolve(join(home, ".forgerelay", "debug"));
23
23
  }
24
24
 
25
+ export function interactiveDebugUrls(configDir) {
26
+ let config = {};
27
+ try {
28
+ config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8"));
29
+ } catch {
30
+ // Keep the historical debug defaults when the selected config has not been created yet.
31
+ }
32
+ const configuredHost = typeof config.host === "string" && config.host.trim()
33
+ ? config.host.trim()
34
+ : "127.0.0.1";
35
+ const host = configuredHost === "0.0.0.0" || configuredHost === "::"
36
+ ? "127.0.0.1"
37
+ : configuredHost;
38
+ const port = Number.isInteger(config.port) && config.port > 0 && config.port <= 65_535
39
+ ? config.port
40
+ : 7677;
41
+ const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
42
+ const baseUrl = `http://${displayHost}:${port}`;
43
+ return { baseUrl, mcpUrl: `${baseUrl}/mcp` };
44
+ }
45
+
25
46
  function interactiveDebugOwnerToken(configDir) {
26
47
  const authPath = join(configDir, "auth.json");
27
48
  let auth;
@@ -45,6 +66,7 @@ export function createInteractiveDebugEnvironment({
45
66
  } = {}) {
46
67
  const configDir = interactiveDebugConfigDir({ env, home });
47
68
  const ownerToken = interactiveDebugOwnerToken(configDir);
69
+ const { baseUrl, mcpUrl } = interactiveDebugUrls(configDir);
48
70
  mkdirSync(debugRoot, { recursive: true });
49
71
 
50
72
  const debugEnv = { ...env };
@@ -67,7 +89,7 @@ export function createInteractiveDebugEnvironment({
67
89
  }
68
90
  debugEnv.FORGERELAY_CONFIG_DIR = configDir;
69
91
 
70
- return { ownerToken, configDir, env: debugEnv };
92
+ return { ownerToken, configDir, baseUrl, mcpUrl, env: debugEnv };
71
93
  }
72
94
 
73
95
  export function createDebugEnvironment({
@@ -4,7 +4,11 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
6
6
 
7
- import { createInteractiveDebugEnvironment, interactiveDebugConfigDir } from "./runtime.mjs";
7
+ import {
8
+ createInteractiveDebugEnvironment,
9
+ interactiveDebugConfigDir,
10
+ interactiveDebugUrls,
11
+ } from "./runtime.mjs";
8
12
 
9
13
  async function withDebugConfig(t) {
10
14
  const home = await mkdtemp(join(tmpdir(), "forgerelay-interactive-debug-"));
@@ -45,6 +49,12 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
45
49
  assert.equal(result.ownerToken, "debug-owner-password-123456");
46
50
  assert.equal(result.configDir, configDir);
47
51
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, configDir);
52
+ assert.equal(result.baseUrl, "http://127.0.0.1:7677");
53
+ assert.equal(result.mcpUrl, "http://127.0.0.1:7677/mcp");
54
+ assert.deepEqual(interactiveDebugUrls(configDir), {
55
+ baseUrl: "http://127.0.0.1:7677",
56
+ mcpUrl: "http://127.0.0.1:7677/mcp",
57
+ });
48
58
  assert.equal(result.env.HOST, undefined);
49
59
  assert.equal(result.env.PORT, undefined);
50
60
  assert.equal(result.env.FORGERELAY_PUBLIC_BASE_URL, undefined);
@@ -60,7 +70,7 @@ test("interactive debug config directory may be explicitly relocated without fal
60
70
  const { home } = await withDebugConfig(t);
61
71
  const customDir = join(home, "custom-debug-config");
62
72
  await mkdir(customDir, { recursive: true });
63
- await writeFile(join(customDir, "config.json"), "{}\n");
73
+ await writeFile(join(customDir, "config.json"), JSON.stringify({ host: "127.0.0.1", port: 6768 }) + "\n");
64
74
  await writeFile(join(customDir, "auth.json"), JSON.stringify({ ownerToken: "custom-debug-password-123456" }) + "\n");
65
75
 
66
76
  const result = createInteractiveDebugEnvironment({
@@ -71,4 +81,6 @@ test("interactive debug config directory may be explicitly relocated without fal
71
81
  assert.equal(result.configDir, customDir);
72
82
  assert.equal(result.ownerToken, "custom-debug-password-123456");
73
83
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, customDir);
84
+ assert.equal(result.baseUrl, "http://127.0.0.1:6768");
85
+ assert.equal(result.mcpUrl, "http://127.0.0.1:6768/mcp");
74
86
  });
@@ -1,12 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createInteractiveDebugEnvironment, debugBaseUrl, debugMcpUrl, repoRoot } from "./runtime.mjs";
2
+ import { createInteractiveDebugEnvironment, repoRoot } from "./runtime.mjs";
3
3
 
4
- const { configDir, env } = createInteractiveDebugEnvironment();
4
+ const { configDir, baseUrl, mcpUrl, env } = createInteractiveDebugEnvironment();
5
5
 
6
6
  console.error("[forgerelay:debug] local debug server");
7
7
  console.error(`[forgerelay:debug] debug config: ${configDir}/config.json`);
8
- console.error(`[forgerelay:debug] health: ${debugBaseUrl}/healthz`);
9
- console.error(`[forgerelay:debug] MCP: ${debugMcpUrl}`);
8
+ console.error(`[forgerelay:debug] health: ${baseUrl}/healthz`);
9
+ console.error(`[forgerelay:debug] MCP: ${mcpUrl}`);
10
10
  console.error(`[forgerelay:debug] Owner password loaded from ${configDir}/auth.json`);
11
11
  console.error("[forgerelay:debug] all runtime state is under .forgerelay-debug/");
12
12
 
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { mkdirSync, readdirSync, rmSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ const npmCli = process.env.npm_execpath;
8
+ if (!npmCli) {
9
+ throw new Error("release:pack must be launched through npm so npm_execpath is available");
10
+ }
11
+
12
+ const outputDir = resolve(process.cwd(), process.env.RELEASE_ARTIFACT_DIR ?? ".release-artifacts");
13
+ rmSync(outputDir, { recursive: true, force: true });
14
+ mkdirSync(outputDir, { recursive: true });
15
+
16
+ const result = spawnSync(
17
+ process.execPath,
18
+ [npmCli, "pack", "--pack-destination", outputDir],
19
+ {
20
+ cwd: process.cwd(),
21
+ env: process.env,
22
+ stdio: "inherit",
23
+ windowsHide: true,
24
+ shell: false,
25
+ },
26
+ );
27
+ if (result.error) throw result.error;
28
+ if (result.status !== 0) {
29
+ throw new Error(`npm pack failed with exit ${result.status ?? "unknown"}`);
30
+ }
31
+
32
+ const packages = readdirSync(outputDir).filter((name) => name.endsWith(".tgz"));
33
+ if (packages.length !== 1) {
34
+ throw new Error(`release:pack expected exactly one .tgz artifact, found ${packages.length}`);
35
+ }
36
+ console.log(`Verified npm artifact: ${packages[0]}`);